面向复杂协议调度场景的ISM频段非授权信号仿真数据集构建
1. 背景与研究目标
在物理层安全(Physical Layer Security)研究中,尤其是针对 宽频段电磁频谱异常信号、非授权信号及入侵信号检测 的课题,获取高质量、带标签的训练数据是最大的痛点。真实环境采集的数据往往标签混乱、信噪比不可控,且很难捕捉到稀有的异常攻击行为。
因此,我们采用 GNU Radio 仿真 的方式构建数据集。我们的目标是生成 ISM (Industrial, Scientific, and Medical) 频段(主要是 2.4GHz)上常见的业务信号及异常信号,重点模拟各种协议在物理层上的时频特征(如跳频规律、带宽、占空比、调制形态),以便后续通过 STFT(短时傅里叶变换)将 IQ 数据转化为时频图,进行深度学习或信号处理算法的研究。
仿真环境为GNU Radio,生成复数IQ信号后保存为iq.bin文件。

2.仿真策略:1:80 等比缩放 (Key Strategy)
这是理解本数据集最关键的一点。
为什么要缩放?
真实的 ISM 频段带宽约为 80MHz (2400MHz - 2480MHz)。如果我们在 GNU Radio 中直接设置采样率为 80MHz 或 100MHz:普通计算机 CPU 无法实时处理如此巨大的数据流,导致仿真卡顿、溢出(O-run)。而且几十秒的数据就可能占用数十 GB 的硬盘空间,难以进行大规模数据集的构建。
我们的解决方案
假如我们在python block里把采样率设为80M,来模拟产生ISM频段比如2400MHz到2480MHz这80MHz频段的复数信号,这个采样率会导致程序直接卡死。
所以这里我们采用 1:80 的等比缩放策略。
-
采样率设置:固定为 1 MHz。
-
频率映射:仿真中的 1 MHz 带宽代表真实世界的 80 MHz 带宽。
-
时间映射:仿真中的时间流速与真实世界保持一定比例,但为了观察方便,我们在代码中调整了脉冲的持续时间,使其在 STFT 图上清晰可见。
换算公式:
-
仿真带宽 $B_{sim}$ = 真实带宽 $B_{real} \div 80$
-
仿真频偏 $F_{offset\_sim}$ = 真实频偏 $F_{offset\_real} \div 80$
比如说:一个真实带宽为 20MHz 的 WiFi 信号,在我们的仿真数据中,其带宽被设定为20 / 80 = 0.25 MHz。虽然它变“窄”了,但在 1MHz 的观察窗口里,它的相对占比和真实世界是一模一样的。
后续FFT时我们可以直接把坐标轴放缩回80MHz。
3. 仿真场景设计与信号特征详解
我们设计了 8 种典型的通信场景,涵盖了从高吞吐量业务到低功耗传感器,以及恶意攻击信号。以下是每种信号的详细说明及对应的文件名。
WiFi 业务信号 (IEEE 802.11)
我们要模拟的是最常见的 WiFi 终端连接过程,文件名定为 iq_data_wifi.bin。在真实场景中,WiFi 信号通常占用 20MHz 或 40MHz 的带宽,采用 OFDM(正交频分复用)调制,在频谱上呈现出标志性的“矩形平顶”特征。为了在我们的 1MHz 仿真带宽中体现这一特性,我们将信号带宽设定为 0.15MHz(对应真实带宽约 12MHz,为了视觉清晰略微缩小)。在行为逻辑上,该仿真信号高度还原了手机的“主动扫描-连接-待机”循环:首先,你会看到信号在频谱的左侧(对应信道1)、中间(对应信道6)和右侧(对应信道11)进行阶梯状的快速跳变,这是终端在发送探测请求;随后,信号会锁定在中间频点,进行高密度的突发数据传输,模拟用户刷视频带来的高速下载流量;最后,设备进入长达数秒的深度静默期,模拟真实的低功耗待机行为。这种间歇性、宽带且伴随阶段性跳频的特征,是 WiFi 区别于其他信号的关键。

查看代码
"""
Scenario: WiFi Lifecycle V3 (Safe Mode)
Feature: Includes automatic stop after N seconds to prevent huge files.
"""
import numpy as np
from gnuradio import gr
from scipy import signal
class wifi_lifecycle_v3_safe(gr.sync_block):
def __init__(self, sample_rate=1e6):
gr.sync_block.__init__(
self,
name='WiFi Lifecycle V3 (Auto-Stop)',
in_sig=None,
out_sig=[np.complex64]
)
self.sample_rate = sample_rate
self.rng = np.random.default_rng()
# ================= 安全限制 (新增) =================
self.duration_limit_sec = 120.0 # 限制只生成 120 秒数据
self.total_samples_limit = int(self.sample_rate * self.duration_limit_sec)
self.total_samples_produced = 0
# =================================================
# --- 之前的参数 ---
self.amplitude = 10**(-40.0/20)
self.channels = [-0.35e6, 0.0, 0.35e6]
self.sim_bandwidth = 0.15e6
# 时间参数
self.scan_dwell_time = int(sample_rate * 0.050)
self.min_data_dur = 0.1
self.max_data_dur = 0.4
self.min_idle_dur = 0.2
self.max_idle_dur = 0.8
# 滤波器
cutoff_hz = self.sim_bandwidth / 2
num_taps = 201
self.taps = signal.firwin(num_taps, cutoff_hz, fs=sample_rate, window=('kaiser', 14))
self.zi = np.zeros(len(self.taps)-1, dtype=np.complex64)
# 状态
self.ramp_len = 20
self.ramp_up_window = np.linspace(0, 1, self.ramp_len).astype(np.float32)
self.ramp_down_window = np.linspace(1, 0, self.ramp_len).astype(np.float32)
self.state = 0
self.counter = 0
self.phase_timer = 0
self.target_phase_duration = 0
self.current_data_freq = 0.0
self.is_transmitting = False
self.curr_burst_len = 0
self.burst_counter = 0
def _apply_window(self, sig, total_burst_len, current_counter):
# ... (保持之前的代码不变) ...
n_sig = len(sig)
if current_counter < self.ramp_len:
w_start = current_counter
w_end = min(self.ramp_len, current_counter + n_sig)
sig[0:w_end-w_start] *= self.ramp_up_window[w_start:w_end]
decay_start_idx = total_burst_len - self.ramp_len
current_end = current_counter + n_sig
if current_end > decay_start_idx:
s_start = max(0, decay_start_idx - current_counter)
w_start = (current_counter + s_start) - decay_start_idx
w_len = n_sig - s_start
w_end = min(self.ramp_len, w_start + w_len)
if w_end > w_start:
sig[s_start : s_start+(w_end-w_start)] *= self.ramp_down_window[w_start:w_end]
return sig
def work(self, input_items, output_items):
out = output_items[0]
n_out = len(out)
# ================= 安全检查 (新增) =================
# 如果已经生成的点数超过限制,返回 -1 通知 GNU Radio 停止流图
if self.total_samples_produced >= self.total_samples_limit:
return -1
# 如果这一批次生成完就会超过限制,则只生成剩下的部分
remaining_quota = self.total_samples_limit - self.total_samples_produced
if n_out > remaining_quota:
n_out = remaining_quota
# 截断 output buffer,虽然 work 函数通常不这么做,但为了安全停止
out = out[:n_out]
# =================================================
produced = 0
while produced < n_out:
remaining = n_out - produced
# ... (保持原来的状态机逻辑完全不变) ...
# ... 这里的代码复制之前的逻辑 ...
# 为了简洁,我这里只写出结构,请把之前的状态机逻辑(if self.state < len(self.channels)...)完整填入
# --- START OF LOGIC COPY ---
if self.state < len(self.channels):
# Scanning logic...
target_freq = self.channels[self.state]
probe_len = int(self.sample_rate * 0.005)
start_tx = int(self.scan_dwell_time * 0.2)
end_tx = start_tx + probe_len
if start_tx <= self.counter < end_tx:
to_gen = min(remaining, end_tx - self.counter)
sig = self._generate_ofdm_noise(to_gen)
sig = self._apply_window(sig, probe_len, self.counter - start_tx)
t = np.arange(produced, produced + to_gen) / self.sample_rate
shift = np.exp(1j * 2 * np.pi * target_freq * t)
out[produced:produced+to_gen] = sig * shift
else:
to_gen = min(remaining, self.scan_dwell_time - self.counter)
out[produced:produced+to_gen] = 0j
self.counter += to_gen
produced += to_gen
if self.counter >= self.scan_dwell_time:
self.counter = 0
self.state += 1
if self.state >= len(self.channels):
self.state = 100
self.phase_timer = 0
self.target_phase_duration = self.sample_rate * self.rng.uniform(self.min_data_dur, self.max_data_dur)
self.current_data_freq = self.rng.choice(self.channels)
self.is_transmitting = False
self.zi = np.zeros(len(self.taps)-1, dtype=np.complex64)
elif self.state == 100:
# Data logic...
freq = self.current_data_freq
if not self.is_transmitting:
if self.counter == 0:
self.curr_burst_len = int(self.sample_rate * self.rng.uniform(0.001, 0.005))
to_gen = min(remaining, self.curr_burst_len - self.counter)
out[produced:produced+to_gen] = 0j
self.counter += to_gen
produced += to_gen
self.phase_timer += to_gen
if self.counter >= self.curr_burst_len:
self.is_transmitting = True
self.counter = 0
self.burst_counter = 0
self.curr_burst_len = int(self.sample_rate * self.rng.uniform(0.002, 0.015))
else:
to_gen = min(remaining, self.curr_burst_len - self.burst_counter)
sig = self._generate_ofdm_noise(to_gen)
sig = self._apply_window(sig, self.curr_burst_len, self.burst_counter)
t = np.arange(produced, produced + to_gen) / self.sample_rate
shift = np.exp(1j * 2 * np.pi * freq * t)
out[produced:produced+to_gen] = sig * shift
self.burst_counter += to_gen
produced += to_gen
self.phase_timer += to_gen
if self.burst_counter >= self.curr_burst_len:
self.is_transmitting = False
self.counter = 0
if self.phase_timer >= self.target_phase_duration:
self.state = 200
self.counter = 0
self.phase_timer = 0
self.target_phase_duration = self.sample_rate * self.rng.uniform(self.min_idle_dur, self.max_idle_dur)
elif self.state == 200:
# Idle logic...
to_gen = min(remaining, int(self.target_phase_duration) - self.phase_timer)
out[produced:produced+to_gen] = 0j
produced += to_gen
self.phase_timer += to_gen
if self.phase_timer >= int(self.target_phase_duration):
self.state = 0
self.counter = 0
self.phase_timer = 0
# --- END OF LOGIC COPY ---
# 更新全局计数器
self.total_samples_produced += produced
return produced
def _generate_ofdm_noise(self, n_samples):
# 保持不变
raw_noise = (self.rng.standard_normal(n_samples) +
1j * self.rng.standard_normal(n_samples)).astype(np.complex64)
filtered, self.zi = signal.lfilter(self.taps, 1.0, raw_noise, zi=self.zi)
return filtered * self.amplitude * 3.0
经典蓝牙信号 (Classic Bluetooth / A2DP)
与 WiFi 的“宽带大块头”不同,经典蓝牙(iq_data_bluetooth_classic.bin)模拟的是蓝牙耳机传输音频流的场景,其核心特征是“窄带”与“高频跳变”。在真实物理层,蓝牙信号带宽仅为 1MHz,拥有 79 个信道。在我们的仿真中,这被映射为宽度仅为 12.5kHz 的细窄谱线。为了体现其 FHSS(跳频扩频)特性,仿真代码生成了覆盖 -0.45MHz 到 +0.45MHz 全频段的随机跳频序列。在时域上,为了让观察者能看清跳频过程,我们没有采用真实世界中每秒 1600 跳的极高跳速,而是将其调整为“3ms 发射、20ms 静默”的稀疏模式。在频谱瀑布图上,这种信号表现为如同雨滴般随机散落在整个频带上的短促亮线,采用 GFSK 调制使其边缘较为光滑.

查看代码
"""
Scenario: Classic Bluetooth (A2DP Streaming)
Scale: 1:80
Sample Rate: 1MHz
Feature: 79 Channels FHSS, GFSK, Auto-stop after 60s
"""
import numpy as np
from gnuradio import gr
class bluetooth_classic_sim(gr.sync_block):
def __init__(self, sample_rate=1e6):
gr.sync_block.__init__(
self,
name='Bluetooth Classic (A2DP)',
in_sig=None,
out_sig=[np.complex64]
)
self.sample_rate = sample_rate
self.rng = np.random.default_rng()
# ==================== 1. 自动停止限制 (1分钟) ====================
self.duration_limit_sec = 60.0
self.total_samples_limit = int(self.sample_rate * self.duration_limit_sec)
self.total_samples_produced = 0
# ==================== 2. 蓝牙物理层参数 ====================
self.amplitude = 10**(-45.0/20) # -45dB (比WiFi稍微弱一点点,或者设为一样)
# 79个信道 (仿真映射)
# 真实范围 80MHz -> 仿真范围 1MHz
# 我们生成从 -0.45MHz 到 +0.45MHz 均匀分布的 79 个频点
self.hop_channels = np.linspace(-0.45e6, 0.45e6, 79)
# GFSK 调制参数
# 真实符号率 1Msps -> 仿真符号率 12.5 ksps
self.symbol_rate = 12500.0
self.sps = int(self.sample_rate / self.symbol_rate) # Samples per symbol (~80)
# 频偏 (Modulation Index h=0.32 approx)
# Deviation = SymbolRate * h / 2 = 12500 * 0.32 / 2 ~= 2000 Hz
self.freq_dev = 2000.0
# ==================== 3. 时序参数 ====================
# 模拟 DH5 数据包 (5个时隙)
# 真实 5 slots ~ 3.125ms -> 仿真里我们稍微拉长一点便于观察: 8ms
self.burst_duration = 0.008
self.burst_len = int(self.sample_rate * self.burst_duration)
# 保护间隔 (Guard Interval / Hopping time)
self.guard_duration = 0.01 # 10ms
self.guard_len = int(self.sample_rate * self.guard_duration)
# ==================== 4. 窗函数 (旁瓣抑制) ====================
self.ramp_len = 50
self.ramp_up = np.linspace(0, 1, self.ramp_len).astype(np.float32)
self.ramp_down = np.linspace(1, 0, self.ramp_len).astype(np.float32)
# 内部状态
self.state = 0 # 0: Hop/Guard, 1: Transmit
self.counter = 0
self.current_freq = 0.0
self.phase = 0.0
def work(self, input_items, output_items):
out = output_items[0]
n_out = len(out)
# [安全停止检查]
if self.total_samples_produced >= self.total_samples_limit:
return -1 # 返回 -1 通知 GNU Radio 停止运行
remaining_quota = self.total_samples_limit - self.total_samples_produced
if n_out > remaining_quota:
n_out = remaining_quota
out = out[:n_out]
produced = 0
while produced < n_out:
remaining = n_out - produced
if self.state == 0: # Hopping / Guard Interval
# 这段时间是静默的,设备在切换频率
to_gen = min(remaining, self.guard_len - self.counter)
out[produced:produced+to_gen] = 0j
self.counter += to_gen
produced += to_gen
if self.counter >= self.guard_len:
self.state = 1
self.counter = 0
# 随机跳到下一个频点
self.current_freq = self.rng.choice(self.hop_channels)
# 重置相位以模拟非相干跳频 (或者为了简化计算)
self.phase = 0.0
elif self.state == 1: # Transmit (DH5 Packet)
to_gen = min(remaining, self.burst_len - self.counter)
# --- 生成 GFSK ---
# 1. 生成随机比特流 (+1, -1)
# 为了计算方便,我们直接生成每个样本对应的频率偏移
# 每 sps 个样本对应一个符号
n_syms = int(np.ceil(to_gen / self.sps)) + 1
bits = self.rng.choice([-1.0, 1.0], size=n_syms)
# 简单的高斯滤波模拟 (用重复+平滑代替)
# 这里为了性能,简化为直接重复并通过相位积分平滑
freq_stream = np.repeat(bits, self.sps)[:to_gen] * self.freq_dev
# 2. 加上中心频率偏移 (跳频)
freq_stream += self.current_freq
# 3. 积分得到相位
# phase[n] = phase[n-1] + 2*pi*f[n]*dt
phase_steps = 2 * np.pi * freq_stream / self.sample_rate
phases = np.cumsum(phase_steps) + self.phase
self.phase = phases[-1] % (2*np.pi) # 保存相位给下一块
# 4. 生成复信号
sig = self.amplitude * np.exp(1j * phases)
# --- 加窗 (Windowing) ---
# 头部
if self.counter < self.ramp_len:
w_len = min(self.ramp_len - self.counter, to_gen)
sig[:w_len] *= self.ramp_up[self.counter : self.counter+w_len]
# 尾部
dist_from_end = self.burst_len - self.counter
if dist_from_end <= self.ramp_len + to_gen:
# 我们接近尾部了,需要计算重叠部分
# 这是一个简化的尾部处理,确保最后 ramp_len 个点衰减
start_decay_idx = self.burst_len - self.ramp_len
curr_abs_idx = self.counter
# 找出 sig 中需要衰减的部分
for i in range(len(sig)):
abs_i = curr_abs_idx + i
if abs_i >= start_decay_idx:
ramp_idx = abs_i - start_decay_idx
if ramp_idx < self.ramp_len:
sig[i] *= self.ramp_down[ramp_idx]
out[produced:produced+to_gen] = sig
self.counter += to_gen
produced += to_gen
if self.counter >= self.burst_len:
self.state = 0 # 传输完毕,准备下一跳
self.counter = 0
self.total_samples_produced += produced
return produced
低功耗蓝牙信号 (BLE Advertising)
BLE 的广播行为(iq_data_ble_adv.bin)具有极强的规律性。真实的 BLE 设备在广播时,只会轮询 37、38、39 这三个特定的信道,分别位于 2.4GHz 频段的最左端、中心和最右端。在我们的仿真代码中,被映射为 -0.45MHz、0MHz 和 +0.45MHz 三个频点。信号会严格按照“左-中-右”的顺序依次闪烁,然后进入长达数百毫秒的休眠。这种极低的占空比和固定的三点轮询特征,使得 BLE 信号在时频域上呈现出一种极度稀疏且机械化的节奏感,非常容易从背景噪声中分离出来。

查看代码
"""
Scenario: BLE Advertising (Beacon)
Scale: 1:80
Sample Rate: 1MHz
Feature: Fixed 3-Channel Sequence (37->38->39), Long Sleep, Auto-stop
"""
import numpy as np
from gnuradio import gr
class ble_adv_sim(gr.sync_block):
def __init__(self, sample_rate=1e6):
gr.sync_block.__init__(
self,
name='BLE Advertising (Seq)',
in_sig=None,
out_sig=[np.complex64]
)
self.sample_rate = sample_rate
self.rng = np.random.default_rng()
# ==================== 1. 自动停止 (120秒) ====================
self.duration_limit_sec = 120.0
self.total_samples_limit = int(self.sample_rate * self.duration_limit_sec)
self.total_samples_produced = 0
# ==================== 2. BLE 频率定义 ====================
self.amplitude = 10**(-45.0/20) # -45dB
# 广播信道频率 (相对于中心频点)
# 37(2402), 38(2426), 39(2480)
# 映射到 1MHz 仿真带宽:
self.adv_channels = [
-0.45e6, # Ch 37
0.0, # Ch 38
0.45e6 # Ch 39
]
# GFSK 参数
self.symbol_rate = 12500.0 # Simulating 1Msps scaled
self.sps = int(self.sample_rate / self.symbol_rate)
self.freq_dev = 2500.0 # h=0.5 -> deviation slightly higher than Classic BT
# ==================== 3. 时序参数 ====================
# 单个包时长
self.packet_len = int(self.sample_rate * 0.002) # 2ms
# 信道间隙 (T_IFS)
self.inter_ch_gap = int(self.sample_rate * 0.001) # 1ms
# 广播间隔 (Advertising Interval) - 睡眠时间
self.min_adv_int = 0.100 # 100ms
self.max_adv_int = 0.500 # 500ms
self.current_sleep_len = 0
# ==================== 4. 状态机与窗函数 ====================
# State definition:
# 0: Ch 37 Transmit
# 1: Gap
# 2: Ch 38 Transmit
# 3: Gap
# 4: Ch 39 Transmit
# 5: Long Sleep (Adv Interval)
self.state = 5 # Start with sleep
self.counter = 0
self.phase = 0.0
# Window
self.ramp_len = 50
self.ramp_up = np.linspace(0, 1, self.ramp_len).astype(np.float32)
self.ramp_down = np.linspace(1, 0, self.ramp_len).astype(np.float32)
def work(self, input_items, output_items):
out = output_items[0]
n_out = len(out)
# 安全停止检查
if self.total_samples_produced >= self.total_samples_limit:
return -1
remaining_quota = self.total_samples_limit - self.total_samples_produced
if n_out > remaining_quota:
n_out = remaining_quota
out = out[:n_out]
produced = 0
while produced < n_out:
remaining = n_out - produced
# === 发射状态 (State 0, 2, 4) ===
if self.state in [0, 2, 4]:
# 确定当前频率
if self.state == 0: freq = self.adv_channels[0]
elif self.state == 2: freq = self.adv_channels[1]
else: freq = self.adv_channels[2]
to_gen = min(remaining, self.packet_len - self.counter)
# 生成 GFSK
n_syms = int(np.ceil(to_gen / self.sps)) + 1
bits = self.rng.choice([-1.0, 1.0], size=n_syms)
freq_stream = np.repeat(bits, self.sps)[:to_gen] * self.freq_dev
freq_stream += freq
phase_steps = 2 * np.pi * freq_stream / self.sample_rate
phases = np.cumsum(phase_steps) + self.phase
self.phase = phases[-1] % (2*np.pi)
sig = self.amplitude * np.exp(1j * phases)
# 加窗
# Head
if self.counter < self.ramp_len:
w_len = min(self.ramp_len - self.counter, to_gen)
sig[:w_len] *= self.ramp_up[self.counter : self.counter+w_len]
# Tail
dist_from_end = self.packet_len - self.counter
if dist_from_end <= self.ramp_len + to_gen:
start_decay = self.packet_len - self.ramp_len
curr = self.counter
for i in range(len(sig)):
if (curr + i) >= start_decay:
idx = (curr + i) - start_decay
if idx < self.ramp_len:
sig[i] *= self.ramp_down[idx]
out[produced:produced+to_gen] = sig
self.counter += to_gen
produced += to_gen
if self.counter >= self.packet_len:
self.counter = 0
self.state += 1 # 进入下一个间隙
# === 短间隙状态 (State 1, 3) ===
elif self.state in [1, 3]:
to_gen = min(remaining, self.inter_ch_gap - self.counter)
out[produced:produced+to_gen] = 0j
self.counter += to_gen
produced += to_gen
if self.counter >= self.inter_ch_gap:
self.counter = 0
self.state += 1 # 进入下一个发射信道
# === 长睡眠状态 (State 5) ===
elif self.state == 5:
# 如果刚进入睡眠,决定睡多久
if self.current_sleep_len == 0:
self.current_sleep_len = int(self.sample_rate * self.rng.uniform(self.min_adv_int, self.max_adv_int))
to_gen = min(remaining, self.current_sleep_len - self.counter)
out[produced:produced+to_gen] = 0j
self.counter += to_gen
produced += to_gen
if self.counter >= self.current_sleep_len:
self.counter = 0
self.state = 0 # 醒来,回到 Ch 37
self.current_sleep_len = 0 # 重置睡眠计时
self.total_samples_produced += produced
return produced
ZigBee 传感器信号 (IEEE 802.15.4)
为了模拟智能家居中的温湿度传感器,我们设计了 ZigBee 仿真信号(iq_data_zigbee.bin)。ZigBee 的典型特征是“死守信道”和“特殊调制”。它通常工作在固定的信道上(例如真实信道 20,即 2450MHz),在我们的仿真中,这体现为相对于中心频率 +0.125MHz 的固定频偏。与 WiFi 的平顶频谱不同,ZigBee 采用 O-QPSK 调制并结合半正弦脉冲成形(Half-Sine Pulse Shaping),这使得其频谱形状看起来像一个圆润的“馒头”。在时域行为上,它表现为周期性的心跳包发射,偶尔因为模拟的通信干扰而出现“双连发”的重传机制。这种位置固定、形状圆润且极其稀疏的窄带信号,是 ISM 频段中典型的低速物联网业务特征。

查看代码
"""
Scenario: ZigBee (IEEE 802.15.4) Sensor
Scale: 1:80
Sample Rate: 1MHz
Feature: Fixed Channel, O-QPSK-like Spectrum, Periodic Heartbeat
"""
import numpy as np
from gnuradio import gr
class zigbee_sensor_sim(gr.sync_block):
def __init__(self, sample_rate=1e6):
gr.sync_block.__init__(
self,
name='ZigBee Sensor (Fixed Ch)',
in_sig=None,
out_sig=[np.complex64]
)
self.sample_rate = sample_rate
self.rng = np.random.default_rng()
# ==================== 1. 自动停止 (120秒) ====================
self.duration_limit_sec = 120.0
self.total_samples_limit = int(self.sample_rate * self.duration_limit_sec)
self.total_samples_produced = 0
# ==================== 2. ZigBee 物理层参数 ====================
# 信号较弱,通常比 WiFi 低很多
self.amplitude = 10**(-50.0/20)
# 固定信道: Channel 20 (Real 2450 MHz)
# Relative to 2440 MHz -> +10 MHz
# Sim Offset -> +0.125 MHz
self.center_freq = 0.125e6
# 调制参数 (模拟 O-QPSK 的频谱形状)
# 真实 ZigBee 码片速率 2Mcps. 1:80 -> Sim 25kcps
# 为了视觉上看起来像 2MHz 宽,我们设定符号率
self.symbol_rate = 25000.0
self.sps = int(self.sample_rate / self.symbol_rate) # 40 samples/symbol
# 半正弦脉冲成形 (Half-Sine Pulse Shaping)
# 这是 802.15.4 的标准特征,使得频谱主瓣较圆
t_pulse = np.linspace(0, np.pi, self.sps)
self.pulse_shape = np.sin(t_pulse).astype(np.complex64)
# ==================== 3. 时序参数 ====================
# 数据包时长 (短)
self.packet_len_samples = int(self.sample_rate * 0.004) # 4ms
# 心跳间隔 (长)
self.min_interval = 1.0 # 1秒
self.max_interval = 2.0 # 2秒
self.current_sleep = 0
# 重传概率 (模拟干扰下的重传)
self.retransmit_prob = 0.3
self.retransmitting = False # 标记当前是否在重传状态
self.retransmit_gap = int(self.sample_rate * 0.010) # 10ms gap
# ==================== 4. 状态机 ====================
# 0: Sleep
# 1: Transmit
# 2: Gap (for retransmit)
self.state = 0
self.counter = 0
def work(self, input_items, output_items):
out = output_items[0]
n_out = len(out)
# [安全停止]
if self.total_samples_produced >= self.total_samples_limit:
return -1
remaining_quota = self.total_samples_limit - self.total_samples_produced
if n_out > remaining_quota:
n_out = remaining_quota
out = out[:n_out]
produced = 0
while produced < n_out:
remaining = n_out - produced
# === State 0: Sleep / Idle ===
if self.state == 0:
# 初始化睡眠时间
if self.current_sleep == 0:
self.current_sleep = int(self.sample_rate * self.rng.uniform(self.min_interval, self.max_interval))
to_gen = min(remaining, self.current_sleep - self.counter)
out[produced:produced+to_gen] = 0j
self.counter += to_gen
produced += to_gen
if self.counter >= self.current_sleep:
self.state = 1 # 醒来发射
self.counter = 0
self.current_sleep = 0
# === State 1: Transmit Packet ===
elif self.state == 1:
to_gen = min(remaining, self.packet_len_samples - self.counter)
# 生成 O-QPSK 类似信号
# 简化生成:随机符号 * 脉冲成形
# 每次生成整数个符号以保证脉冲完整性 (简化逻辑: 按点生成,注意对齐)
# 计算当前块包含多少个完整符号起始
# 为了简单高效,我们直接生成随机相位信号并应用成形
# 这里使用 "Direct Sequence" 的视觉模拟:
# 每个符号持续 sps 个点,形状是 sin(0..pi)
# 确定当前时间点对应符号内的位置
t_indices = np.arange(self.counter, self.counter + to_gen)
symbol_indices = t_indices // self.sps
offset_indices = t_indices % self.sps
# 生成(或哈希)每个符号的随机相位 (0, pi/2, pi, 3pi/2)
# 使用伪随机生成,基于 symbol_index 确保跨 buffer 连续性
# 简单的 trick: 用 symbol_index 做种子
# 但 Python random 不太好向量化。
# 既然是仿真,我们直接实时生成随机数,忽略跨 buffer 的极小相位不连续风险(O-QPSK本身相位就跳变)
# 或者更严谨:预生成一大串不够,这里用 numpy 生成随机复数
# 生成足够多的随机符号
num_needed = int(np.ceil((self.counter + to_gen)/self.sps)) - int(self.counter/self.sps) + 1
rand_syms = self.rng.choice([1, -1, 1j, -1j], size=num_needed)
# 这是一个简化的映射逻辑,仅用于视觉仿真:
# 实际上我们需要对齐 symbol_indices 到 rand_syms 的索引
# 让我们换一种更鲁棒的“流式”方法:
# 只有在 counter % sps == 0 时切换符号
# 重新生成信号 buffer
sig = np.zeros(to_gen, dtype=np.complex64)
# 这是一个逐点填充的简单循环,Python里稍慢但逻辑清晰,对于 1MHz 没问题
# 优化:分块处理
start_sym = self.counter // self.sps
end_sym = (self.counter + to_gen - 1) // self.sps
# 生成当前需要的随机符号
current_syms = self.rng.choice([1, -1, 1j, -1j], size=(end_sym - start_sym + 1))
# 填充 Pulse Shape
for i in range(to_gen):
global_idx = self.counter + i
sym_idx = global_idx // self.sps
pulse_idx = global_idx % self.sps
# 找到对应的随机符号 (相对于 current_syms 的偏移)
rel_sym_idx = sym_idx - start_sym
# 半正弦成形: sin(t) * symbol
sig[i] = current_syms[rel_sym_idx] * self.pulse_shape[pulse_idx]
# 频移到 +0.125 MHz
t = np.arange(produced, produced + to_gen) / self.sample_rate
shift = np.exp(1j * 2 * np.pi * self.center_freq * t)
out[produced:produced+to_gen] = sig * shift * self.amplitude
self.counter += to_gen
produced += to_gen
if self.counter >= self.packet_len_samples:
self.counter = 0
# 发送结束,决定是去睡觉,还是重传
if not self.retransmitting and self.rng.random() < self.retransmit_prob:
self.state = 2 # 去 Gap 准备重传
self.retransmitting = True
else:
self.state = 0 # 去睡觉
self.retransmitting = False
# === State 2: Retransmit Gap (Short Wait) ===
elif self.state == 2:
to_gen = min(remaining, self.retransmit_gap - self.counter)
out[produced:produced+to_gen] = 0j
self.counter += to_gen
produced += to_gen
if self.counter >= self.retransmit_gap:
self.state = 1 # 回去重传
self.counter = 0
# retransmitting 标志位保持为 True,这样下次回来就会强制去睡觉
self.total_samples_produced += produced
return produced
无人机高清图传信号 (UAV Video Link)
无人机图传信号(iq_data_uav_video.bin)模拟了 DJI OcuSync 等协议的高清视频传输场景。这类信号为了保证视频流的实时性,具有极高的占空比,几乎不间断地发射。在仿真中,我们生成了一个带宽为 0.125MHz(对应真实 10MHz)的宽带 OFDM 信号,它在瀑布图上表现为一条持续明亮的“高速公路”。为了模拟其抗干扰机制,代码引入了慢速跳频逻辑:每隔几秒钟,整条“高速公路”会瞬间中断,并随机跳变到另一个频点继续传输。这种“宽带常亮”配合“慢速切换”的特征,使其极易覆盖掉同频段的 WiFi 或蓝牙信号。

查看代码
"""
Scenario: UAV Video Link (OcuSync-like)
Scale: 1:80
Sample Rate: 1MHz
Feature: Wideband OFDM, High Duty Cycle, Slow Hopping, Auto-stop
"""
import numpy as np
from gnuradio import gr
from scipy import signal
class uav_video_sim(gr.sync_block):
def __init__(self, sample_rate=1e6):
gr.sync_block.__init__(
self,
name='UAV Video Link (Slow Hop)',
in_sig=None,
out_sig=[np.complex64]
)
self.sample_rate = sample_rate
self.rng = np.random.default_rng()
# ==================== 1. 自动停止 (120秒) ====================
self.duration_limit_sec = 120.0
self.total_samples_limit = int(self.sample_rate * self.duration_limit_sec)
self.total_samples_produced = 0
# ==================== 2. 信号参数 ====================
self.amplitude = 10**(-40.0/20) # -40dB (强度较高,因为在空中视距传输)
# 仿真带宽 125kHz (对应真实 10MHz)
self.sim_bandwidth = 0.125e6
# 可用频点列表 (慢跳频目标)
self.freq_list = [
-0.30e6, # 左侧
0.0, # 中间
0.30e6 # 右侧
]
self.current_freq = 0.0
# ==================== 3. 滤波器设计 (OFDM 频谱整形) ====================
# 制造一个平顶、陡峭边缘的矩形频谱
cutoff_hz = self.sim_bandwidth / 2
num_taps = 301 # 阶数越高,边缘越陡峭,越像 OFDM
# Kaiser 窗 beta=10,旁瓣抑制优秀
self.taps = signal.firwin(num_taps, cutoff_hz, fs=sample_rate, window=('kaiser', 10))
self.zi = np.zeros(len(self.taps)-1, dtype=np.complex64)
# ==================== 4. 状态机 (慢跳频) ====================
# State 0: Transmission (Long duration)
# State 1: Switching Gap (Short duration)
self.state = 1 # Start with a gap to pick frequency
self.counter = 0
# 动态时长控制
self.current_duration = 0
self.min_tx_dur = 2.0 # 最少传 2秒
self.max_tx_dur = 5.0 # 最多传 5秒
self.gap_dur = int(self.sample_rate * 0.05) # 50ms 切换间隙
def work(self, input_items, output_items):
out = output_items[0]
n_out = len(out)
# [安全停止]
if self.total_samples_produced >= self.total_samples_limit:
return -1
remaining_quota = self.total_samples_limit - self.total_samples_produced
if n_out > remaining_quota:
n_out = remaining_quota
out = out[:n_out]
produced = 0
while produced < n_out:
remaining = n_out - produced
# === State 0: 视频流传输 ===
if self.state == 0:
to_gen = min(remaining, self.current_duration - self.counter)
# 1. 生成白噪声
raw_noise = (self.rng.standard_normal(to_gen) +
1j * self.rng.standard_normal(to_gen)).astype(np.complex64)
# 2. 滤波整形 (模拟 OFDM 宽带特性)
# 保持 zi 状态,确保流是连续的
filtered_sig, self.zi = signal.lfilter(self.taps, 1.0, raw_noise, zi=self.zi)
# 3. 频移 (移到当前选定的频点)
t = np.arange(produced, produced + to_gen) / self.sample_rate
shift = np.exp(1j * 2 * np.pi * self.current_freq * t)
# 4. 幅度调整 (滤波会改变能量,这里简单补偿)
out[produced:produced+to_gen] = filtered_sig * shift * self.amplitude * 2.0
self.counter += to_gen
produced += to_gen
if self.counter >= self.current_duration:
self.state = 1 # 传输时间到了,切换频率
self.counter = 0
# === State 1: 切换间隙 (Gap) ===
elif self.state == 1:
# 在这个间隙选择下一个频点
if self.counter == 0:
# 随机选一个频点 (可以优化为不选当前频点)
self.current_freq = self.rng.choice(self.freq_list)
# 决定下一次传输传多久
self.current_duration = int(self.sample_rate * self.rng.uniform(self.min_tx_dur, self.max_tx_dur))
# 重置滤波器状态? 对于非相干跳频,重置不重置都行,重置更干净
self.zi = np.zeros(len(self.taps)-1, dtype=np.complex64)
to_gen = min(remaining, self.gap_dur - self.counter)
out[produced:produced+to_gen] = 0j
self.counter += to_gen
produced += to_gen
if self.counter >= self.gap_dur:
self.state = 0 # 开始传输
self.counter = 0
self.total_samples_produced += produced
return produced
扫频干扰信号 (Sweep Jamming)
模拟一种典型的物理层入侵攻击——扫频干扰(iq_data_sweep_jammer.bin)。这是一个高功率的线性调频(Chirp)信号,其频率随时间快速线性变化。在仿真中,该信号在 20ms 内从 -0.45MHz 快速扫描至 +0.45MHz,横扫整个仿真频段。在时频图上,它表现为一道道锋利的斜线或“V”字形,能够瞬间切断经过的所有通信链路。这种信号不携带任何有效数据,其唯一目的就是破坏电磁环境,具有极其显著的非通信特征。

查看代码
"""
Scenario: Sweep Jammer (Intrusion/Attack)
Scale: 1:80
Feature: Fast Chirp across full bandwidth, High Power
"""
import numpy as np
from gnuradio import gr
class sweep_jammer_sim(gr.sync_block):
def __init__(self, sample_rate=1e6):
gr.sync_block.__init__(
self,
name='Sweep Jammer (Attack)',
in_sig=None,
out_sig=[np.complex64]
)
self.sample_rate = sample_rate
# [自动停止: 120s]
self.duration_limit_sec = 120.0
self.total_samples_limit = int(self.sample_rate * self.duration_limit_sec)
self.total_samples_produced = 0
# === 干扰参数 ===
self.amplitude = 10**(-30.0/20) # 强干扰
# 扫描范围
self.freq_start = -0.45e6
self.freq_stop = 0.45e6
self.bw = self.freq_stop - self.freq_start
# 扫描速率
# 20ms 完成一次全频段扫描 (50Hz 扫描率)
self.sweep_period = 0.020
self.sweep_samples = int(self.sample_rate * self.sweep_period)
# Chirp 斜率 k = BW / T
self.slope = self.bw / self.sweep_period
# 状态
self.counter = 0 # 记录当前在扫描周期内的位置
# 为了保证相位连续性(模拟连续波干扰),我们需要一个累积相位
# 但为了计算简单,每个周期重置相位也能模拟出干扰效果
# 这里使用连续相位计算:Phase = int(f(t))
self.current_time_in_sweep = 0.0
def work(self, input_items, output_items):
out = output_items[0]
n_out = len(out)
# [安全停止]
if self.total_samples_produced >= self.total_samples_limit:
return -1
remaining_quota = self.total_samples_limit - self.total_samples_produced
if n_out > remaining_quota:
n_out = remaining_quota
out = out[:n_out]
# 生成时间向量 t (相对于当前扫描周期的起始)
# 我们需要分段处理,因为 n_out 可能跨越了周期的边界
produced = 0
while produced < n_out:
remaining_in_buffer = n_out - produced
remaining_in_sweep = self.sweep_samples - self.counter
to_gen = min(remaining_in_buffer, remaining_in_sweep)
# 构造时间 t
t_start = self.counter / self.sample_rate
t = np.arange(to_gen) / self.sample_rate + t_start
# 线性调频信号 (LFM / Chirp)
# f(t) = f_start + slope * t
# Phase(t) = 2*pi * (f_start * t + 0.5 * slope * t^2)
phase = 2 * np.pi * (self.freq_start * t + 0.5 * self.slope * t**2)
sig = self.amplitude * np.exp(1j * phase)
out[produced : produced + to_gen] = sig
self.counter += to_gen
produced += to_gen
if self.counter >= self.sweep_samples:
self.counter = 0 # 重置,开始下一轮扫描
self.total_samples_produced += produced
return produced
毕竟可以控制频点和带宽,所以形成一些更复杂点的图案也不是不行,本身对于复杂调度的场景来说,这种信号其实并不少见。


查看代码
"""
Scenario: Spectrum Painting "CAS"
Scale: 1:80
Sample Rate: 1MHz
Feature: Generates visual patterns (Letters CAS) on the spectrogram
"""
import numpy as np
from gnuradio import gr
class spectrum_painter_cas(gr.sync_block):
def __init__(self, sample_rate=1e6):
gr.sync_block.__init__(
self,
name='Spectrum Painter (CAS)',
in_sig=None,
out_sig=[np.complex64]
)
self.sample_rate = sample_rate
self.rng = np.random.default_rng()
# ==================== 1. 绘画参数 ====================
self.fft_size = 1024 # 频谱分辨率 (画布宽度)
self.image_height = 512 # 持续时间 (画布高度)
# 创建画布:0=黑, 1=亮
# 视觉坐标系:索引 0 是最左侧,索引 1024 是最右侧
self.mask_matrix = np.zeros((self.image_height, self.fft_size), dtype=np.float32)
# ==================== 2. 定义字模 (CAS) ====================
# 为了美观,我们留一点边距,字母宽度大概在 200 左右
# --- 字母 "C" (左侧: 100-300) ---
c_start = 100
c_end = 300
# 上横
self.mask_matrix[50:100, c_start:c_end] = 1.0
# 下横
self.mask_matrix[412:462, c_start:c_end] = 1.0
# 左竖
self.mask_matrix[50:462, c_start:c_start+50] = 1.0
# --- 字母 "A" (中间: 412-612) ---
a_start = 412
a_end = 612
# 左竖
self.mask_matrix[50:462, a_start:a_start+50] = 1.0
# 右竖
self.mask_matrix[50:462, a_end-50:a_end] = 1.0
# 上横 (封顶)
self.mask_matrix[50:100, a_start:a_end] = 1.0
# 中横 (横梁)
self.mask_matrix[230:280, a_start:a_end] = 1.0
# --- 字母 "S" (右侧: 724-924) ---
s_start = 724
s_end = 924
# 上横
self.mask_matrix[50:100, s_start:s_end] = 1.0
# 中横
self.mask_matrix[230:280, s_start:s_end] = 1.0
# 下横
self.mask_matrix[412:462, s_start:s_end] = 1.0
# 左上竖 (连接上和中)
self.mask_matrix[50:230, s_start:s_start+50] = 1.0
# 右下竖 (连接中和下)
self.mask_matrix[280:462, s_end-50:s_end] = 1.0
# ==================== 3. 预计算时域数据 ====================
# 1. 生成随机相位 (让能量均匀分布)
random_phase = self.rng.uniform(0, 2*np.pi, self.mask_matrix.shape)
freq_domain_signal = self.mask_matrix * np.exp(1j * random_phase)
# 2. 【关键】执行 IFFT Shift
# 将 "左-中-右" 的视觉顺序转换为 IFFT 需要的 "直流-正-负" 顺序
freq_domain_signal = np.fft.ifftshift(freq_domain_signal, axes=1)
# 3. IFFT 变换
time_domain_matrix = np.fft.ifft(freq_domain_signal, axis=1)
# 4. 展平并处理幅度
self.iq_pattern = time_domain_matrix.flatten().astype(np.complex64)
# 归一化并提升亮度 (-20dB)
max_val = np.max(np.abs(self.iq_pattern))
if max_val > 0:
self.iq_pattern /= max_val
self.iq_pattern *= 10**(-20.0/20)
# 增加 1秒 静默间隔
self.gap_len = int(self.sample_rate * 1.0)
self.gap_samples = np.zeros(self.gap_len, dtype=np.complex64)
# 拼接单次循环数据
self.one_cycle_data = np.concatenate((self.iq_pattern, self.gap_samples))
self.cycle_len = len(self.one_cycle_data)
# ==================== 4. 控制逻辑 ====================
self.max_cycles = 3 # 画3次停止
self.cycles_done = 0
self.index_in_cycle = 0
self.total_samples_limit = int(sample_rate * 120.0)
self.total_produced = 0
def work(self, input_items, output_items):
out = output_items[0]
n_out = len(out)
# 停止条件
if self.cycles_done >= self.max_cycles or self.total_produced >= self.total_samples_limit:
return -1
produced = 0
while produced < n_out:
if self.cycles_done >= self.max_cycles:
break
remaining_in_buffer = n_out - produced
remaining_in_cycle = self.cycle_len - self.index_in_cycle
to_copy = min(remaining_in_buffer, remaining_in_cycle)
out[produced : produced + to_copy] = \
self.one_cycle_data[self.index_in_cycle : self.index_in_cycle + to_copy]
produced += to_copy
self.index_in_cycle += to_copy
if self.index_in_cycle >= self.cycle_len:
self.index_in_cycle = 0
self.cycles_done += 1
self.total_produced += produced
return produced

浙公网安备 33010602011771号