面向复杂协议调度场景的ISM频段非授权信号仿真数据集构建

1. 背景与研究目标

在物理层安全(Physical Layer Security)研究中,尤其是针对 宽频段电磁频谱异常信号、非授权信号及入侵信号检测 的课题,获取高质量、带标签的训练数据是最大的痛点。真实环境采集的数据往往标签混乱、信噪比不可控,且很难捕捉到稀有的异常攻击行为。

因此,我们采用 GNU Radio 仿真 的方式构建数据集。我们的目标是生成 ISM (Industrial, Scientific, and Medical) 频段(主要是 2.4GHz)上常见的业务信号及异常信号,重点模拟各种协议在物理层上的时频特征(如跳频规律、带宽、占空比、调制形态),以便后续通过 STFT(短时傅里叶变换)将 IQ 数据转化为时频图,进行深度学习或信号处理算法的研究。

仿真环境为GNU Radio,生成复数IQ信号后保存为iq.bin文件。

image

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 区别于其他信号的关键。

image

查看代码

"""
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 调制使其边缘较为光滑.

image

查看代码
 """
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 信号在时频域上呈现出一种极度稀疏且机械化的节奏感,非常容易从背景噪声中分离出来。

image

 

查看代码
 """
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 频段中典型的低速物联网业务特征。

image

查看代码
 """
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 或蓝牙信号。

image

查看代码
 """
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”字形,能够瞬间切断经过的所有通信链路。这种信号不携带任何有效数据,其唯一目的就是破坏电磁环境,具有极其显著的非通信特征。

image

查看代码
 """
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

 

毕竟可以控制频点和带宽,所以形成一些更复杂点的图案也不是不行,本身对于复杂调度的场景来说,这种信号其实并不少见。

imageimage

查看代码
 """
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
posted @ 2025-12-01 16:56  拾一贰叁  阅读(17)  评论(0)    收藏  举报