GNU USRP动态调制发射

grc脚本

image

Embedded Python block代码:

import numpy as np
from gnuradio import gr
import time

# --- 各调制函数 ---
def mod_BPSK(bits, freq, t):
    return np.exp(1j*(2*np.pi*freq*t + np.pi*bits))

def mod_QPSK(bits, freq, t):
    # 保证比特数为偶数
    n_sym = len(bits) // 2
    bits = bits[:n_sym*2]
    idx = bits[0::2]*2 + bits[1::2]
    phases = np.pi/4 + np.pi/2*idx
    # 生成每个符号的复数值
    sym = np.exp(1j*phases)
    # 重复符号以匹配采样点数量
    out = np.repeat(sym, int(np.ceil(len(t)/len(sym))))[:len(t)]
    return out * np.exp(1j*2*np.pi*freq*t)
    
    
def mod_GFSK(freq, t, bt=0.35):
    N = len(t)
    data = np.random.choice([-1,1], N)
    samples_per_bit = max(2, int(len(t)/N))
    gauss = np.exp(-0.5*(np.linspace(-2,2,samples_per_bit*2)**2)/bt**2)
    phase_dev = np.convolve(data, gauss, mode="same")
    phase = np.cumsum(phase_dev)*(np.pi/samples_per_bit)
    return np.exp(1j*(2*np.pi*freq*t + phase))

def mod_16QAM(bits, freq, t):
    # 4 bit 一个符号
    n_sym = len(bits) // 4
    bits = bits[:n_sym*4]
    b = bits.reshape(-1,4)
    mI = (2*b[:,0]+b[:,1])*2-3
    mQ = (2*b[:,2]+b[:,3])*2-3
    sym = (mI + 1j*mQ)/np.sqrt(10)
    out = np.repeat(sym, int(np.ceil(len(t)/len(sym))))[:len(t)]
    return out * np.exp(1j*2*np.pi*freq*t)

# -------- 主 Block --------
class blk(gr.sync_block):
    def __init__(self):
        gr.sync_block.__init__(self,
            name="Simple Scheduler",
            in_sig=None,
            out_sig=[np.complex64]
        )
        self.samp_rate = 1e6
        self.freq_carrier = 1e6
        self.last_switch = time.time()
        self.state = "GFSK"   # 当前模式

    def work(self, input_items, output_items):
        out = output_items[0]
        N = len(out)
        t = np.arange(N)/self.samp_rate
        
        
        
        # --- 限制符号速率,插值平滑 ---
        symbol_rate = 1e3
        samples_per_symbol = max(1, int(self.samp_rate / symbol_rate))
        num_symbols = int(np.ceil(N / samples_per_symbol))
        bits = np.random.randint(0, 2, num_symbols)
        bits = np.repeat(bits, samples_per_symbol)[:N]


        elapsed = time.time() - self.last_switch

        # -------- 显式调度逻辑 --------
        if elapsed < 3:
            self.state = "GFSK"
        elif elapsed < 6:
            self.state = "BPSK"
        elif elapsed < 8:
            self.state = "QPSK"
        elif elapsed < 9:
            self.state = "16QAM"
        else:
            self.last_switch = time.time()  # 重新开始
            self.state = "GFSK"

        # -------- 调制调用 --------
        if self.state == "GFSK":
            out[:] = mod_GFSK(self.freq_carrier, t)
        elif self.state == "QPSK":
            out[:] = mod_QPSK(bits, self.freq_carrier, t)
        elif self.state == "16QAM":
            out[:] = mod_16QAM(bits, self.freq_carrier, t)
        else:
            out[:] = mod_BPSK(bits, self.freq_carrier, t)

        return len(out)
        

大致效果:
image

更多调制方式的版本
 import numpy as np
from gnuradio import gr
import time

# --- 各调制函数 ---
def mod_BPSK(bits, freq, t):
    return np.exp(1j*(2*np.pi*freq*t + np.pi*bits))

def mod_QPSK(bits, freq, t):
    # 保证比特数为偶数
    n_sym = len(bits) // 2
    bits = bits[:n_sym*2]
    idx = bits[0::2]*2 + bits[1::2]
    phases = np.pi/4 + np.pi/2*idx
    # 生成每个符号的复数值
    sym = np.exp(1j*phases)
    # 重复符号以匹配采样点数量
    out = np.repeat(sym, int(np.ceil(len(t)/len(sym))))[:len(t)]
    return out * np.exp(1j*2*np.pi*freq*t)
    
    
def mod_GFSK(freq, t, bt=0.35):
    N = len(t)
    data = np.random.choice([-1,1], N)
    samples_per_bit = max(2, int(len(t)/N))
    gauss = np.exp(-0.5*(np.linspace(-2,2,samples_per_bit*2)**2)/bt**2)
    phase_dev = np.convolve(data, gauss, mode="same")
    phase = np.cumsum(phase_dev)*(np.pi/samples_per_bit)
    return np.exp(1j*(2*np.pi*freq*t + phase))

def mod_16QAM(bits, freq, t):
    # 4 bit 一个符号
    n_sym = len(bits) // 4
    bits = bits[:n_sym*4]
    b = bits.reshape(-1,4)
    mI = (2*b[:,0]+b[:,1])*2-3
    mQ = (2*b[:,2]+b[:,3])*2-3
    sym = (mI + 1j*mQ)/np.sqrt(10)
    out = np.repeat(sym, int(np.ceil(len(t)/len(sym))))[:len(t)]
    return out * np.exp(1j*2*np.pi*freq*t)


def mod_2FSK(bits, freq, t, df=5e3):
    """二进制频移键控(2-FSK)"""
    # bits=0 → freq - df, bits=1 → freq + df
    freq_inst = freq + (2*bits-1)*df
    phase = np.cumsum(2*np.pi*freq_inst/len(freq_inst))
    return np.exp(1j*phase)

def mod_OFDM(bits, freq, t, n_subcarriers=8, sub_spacing=1e3):
    """简化版 OFDM:多载波正交叠加"""
    # 生成多个子载波信号叠加
    carriers = []
    for k in range(n_subcarriers):
        sub_freq = freq + (k - n_subcarriers/2) * sub_spacing
        phase = 2*np.pi*sub_freq*t + np.pi*bits[k % len(bits)]
        carriers.append(np.exp(1j*phase))
    sig = np.sum(carriers, axis=0) / n_subcarriers
    return sig



# -------- 主 Block --------
class blk(gr.sync_block):
    def __init__(self):
        gr.sync_block.__init__(self,
            name="Simple Scheduler",
            in_sig=None,
            out_sig=[np.complex64]
        )
        self.samp_rate = 1e6
        self.freq_carrier = 1e6
        self.last_switch = time.time()
        self.state = "GFSK"   # 当前模式

    def work(self, input_items, output_items):
        out = output_items[0]
        N = len(out)
        t = np.arange(N)/self.samp_rate
        
        
        
        # --- 限制符号速率,插值平滑 ---
        symbol_rate = 1e3
        samples_per_symbol = max(1, int(self.samp_rate / symbol_rate))
        num_symbols = int(np.ceil(N / samples_per_symbol))
        bits = np.random.randint(0, 2, num_symbols)
        bits = np.repeat(bits, samples_per_symbol)[:N]


        elapsed = time.time() - self.last_switch

        # -------- 显式调度逻辑 --------
        if elapsed < 0.2:
            self.state = "GFSK"
        elif elapsed < 0.5:
            self.state = "BPSK"
        elif elapsed < 0.5:
            self.state = "QPSK"
        elif elapsed < 0.8:
            self.state = "16QAM"
        elif elapsed < 1.1:
            self.state = "2FSK"
        elif elapsed < 1.4:
            self.state = "OFDM"
        elif elapsed < 1.7:
            self.state = "IDLE"
        else:
            self.last_switch = time.time()  # 重新开始
            self.state = "GFSK"

        # -------- 调制调用 --------
        if self.state == "GFSK":
            out[:] = mod_GFSK(self.freq_carrier, t,bt=0.1)
        elif self.state == "QPSK":
            out[:] = mod_QPSK(bits, self.freq_carrier+5e4, t)
        elif self.state == "16QAM":
            out[:] = mod_16QAM(bits, self.freq_carrier, t)
        elif self.state == "BPSK":
            out[:] = mod_BPSK(bits, self.freq_carrier, t)
        elif self.state == "2FSK":
            out[:] = mod_2FSK(bits, self.freq_carrier, t, df=2e3)
        elif self.state == "OFDM":
            out[:] = mod_OFDM(bits, self.freq_carrier, t, n_subcarriers=8, sub_spacing=2e3)
        elif self.state == "IDLE":
            out[:] = 0
        
        else:
            out[:] = 0

        return len(out)

复杂调度方式的实现:

基础跳频通信(单信号)

  • 调制:GFSK(bt=0.2~0.35)或 2FSK(Δf 固定)。

  • 采样率:samp_rate = 1e6

  • 符号率:sym_rate = 100e3(samples/symbol=10)

  • 跳频节拍:hop_period = 5 ms(可做 1/2/5/10ms 四档)

  • 频点栅格:grid = 200 kHz;中心f0,范围±2 MHz → 21 个栅格。

  • 频偏选择:每个 hop 从 {f0 + k*grid} 随机选(避免相邻重复)

  • 时长:每段 2–5 s

  • 功率:常数或轻微抖动±2 dB

  • 标注(每 hop 一条):{mode:'GFSK', hop_id, f_offset, bt/Δf, sym_rate, SNR, t_start, t_end}

  • 实现要点:在 GFSK/2FSK 分支里每到 hop_period 随机更新 self.freq_carrier(±2 MHz 栅格)。

代码:

# epy_block.py —— 随机跳频的窄带 FH-GFSK
FS = 10e6            # 采样率
FRAME_MS = 3.0      # 10 ms 调度
DUTY = 0.8           # 80% 发射 + 20% 静默
FH_GRID_HZ = 2e6     # 跳频栅格
FH_SPAN_HZ = 6e6     # ±6 MHz 范围(去掉 DC)
GFSK_SYM = 250e3     # Rs,窄带
H = 0.35             # 调制指数
BT = 0.3             # 高斯 BT
FADE_MAX_MS = 0.5    # 渐入/出上限
W_FH = 1.0
RNG_SEED = None      # 设成整数则复现同一随机序列

import numpy as np
from gnuradio import gr

def raised_cosine_envelope(tau, on_s, frame_s, fade_s):
    e = np.zeros_like(tau, dtype=np.float64)
    on = tau < on_s
    if not np.any(on): return e
    x = tau[on]
    y = np.ones_like(x)
    m = x < fade_s
    if np.any(m): y[m] = 0.5*(1 - np.cos(np.pi*x[m]/fade_s))
    m2 = x > (on_s - fade_s)
    if np.any(m2): y[m2] = 0.5*(1 - np.cos(np.pi*(on_s - x[m2])/fade_s))
    e[on] = y
    return e

def gaussian_taps(sps, BT, span_syms=6):
    t = np.arange(-span_syms*sps, span_syms*sps + 1, dtype=np.float64)/float(sps)
    taps = np.exp(-2.0*(np.pi*BT*t)**2)
    taps /= np.sum(taps)
    return taps.astype(np.float64)

def gfsk_baseband(N, Fs, Rs, h, BT, phase0=0.0, span_syms=6):
    if N == 0: return np.zeros(0, np.complex64), float(phase0)
    sps = max(2, int(round(Fs/float(Rs))))
    taps = gaussian_taps(sps, BT, span_syms=span_syms)
    pad = span_syms + 2
    n_sym = int(np.ceil((N + pad*sps*2)/sps)) + 1
    bits = np.random.randint(0, 2, n_sym)*2 - 1   # ±1
    up = np.zeros(n_sym*sps, dtype=np.float64); up[::sps] = bits
    m = np.convolve(up, taps, mode="same")
    start = pad*sps
    m = m[start:start+N]
    df = h*Rs/2.0
    w = 2*np.pi*df * m / Fs
    phase = phase0 + np.cumsum(w)
    bb = np.exp(1j*phase).astype(np.complex64)
    return bb, float(phase[-1])

class blk(gr.sync_block):
    """
    随机 FH-GFSK(无宽带背景):
      - 每帧随机选一条栅格信道
      - 禁止与上帧相同,尽量避开相邻信道
      - 10 ms 帧,80% 占空,窄带 GFSK,边界清晰
    """
    def __init__(self):
        gr.sync_block.__init__(self, name="Random FH-GFSK", in_sig=None, out_sig=[np.complex64])
        self.Fs = float(FS)
        self.frame_s = float(FRAME_MS)*1e-3
        self.on_s = float(DUTY)*self.frame_s
        self.fade_s = min(0.01*self.frame_s, float(FADE_MAX_MS)*1e-3)

        grid = np.arange(-FH_SPAN_HZ, FH_SPAN_HZ + 1, FH_GRID_HZ, dtype=np.float64)
        grid = grid[np.abs(grid) > 1e-6]         # 去 DC
        lim = 0.45*self.Fs
        grid = np.clip(grid, -lim, lim)
        assert grid.size >= 2, "FH 栅格不足"
        self.fh_grid = grid
        self.n_chan = len(grid)

        # 随机源与逐帧缓存
        self.rng = np.random.default_rng(RNG_SEED)
        self._frame2idx = {}         # frame -> channel index
        self._last_idx = None
        self._last_assigned = -1

        # GFSK
        self.Rs = float(GFSK_SYM)
        self.h = float(H)
        self.bt = float(BT)
        self._phi = 0.0

        self.w_fh = float(W_FH)
        self._n0 = 0

    def _pick_next_idx(self, last_idx):
        cand = np.arange(self.n_chan)
        if last_idx is not None:
            # 尽量避开相邻;若通道过少则只避相同
            ban = {last_idx}
            if self.n_chan >= 4:
                if last_idx - 1 >= 0: ban.add(last_idx - 1)
                if last_idx + 1 < self.n_chan: ban.add(last_idx + 1)
            cand = np.array([i for i in cand if i not in ban], dtype=int)
            if cand.size == 0:
                cand = np.array([i for i in range(self.n_chan) if i != last_idx], dtype=int)
        return int(self.rng.choice(cand))

    def _idx_for_frame(self, k):
        if k in self._frame2idx:
            return self._frame2idx[k]
        # 逐帧补齐到 k
        while self._last_assigned < k:
            next_idx = self._pick_next_idx(self._last_idx)
            self._last_assigned += 1
            self._frame2idx[self._last_assigned] = next_idx
            self._last_idx = next_idx
        return self._frame2idx[k]

    def work(self, input_items, output_items):
        out = output_items[0]; N = len(out)
        if N == 0: return 0

        dt = 1.0/self.Fs
        n = self._n0 + np.arange(N, dtype=np.float64)
        t = n*dt

        frame_idx = np.floor(t/self.frame_s).astype(np.int64)
        tau = t - frame_idx*self.frame_s
        env = raised_cosine_envelope(tau, self.on_s, self.frame_s, self.fade_s)

        bb, self._phi = gfsk_baseband(N, self.Fs, self.Rs, self.h, self.bt, self._phi, span_syms=6)

        k_unique = np.unique(frame_idx)
        idx_map = {int(k): self._idx_for_frame(int(k)) for k in k_unique}
        f0 = np.array([ self.fh_grid[idx_map[int(k)]] for k in frame_idx ], dtype=np.float64)

        sig = bb * np.exp(1j*2*np.pi*f0*t).astype(np.complex64)
        sig *= env.astype(np.float64)
        peak = np.max(np.abs(sig))
        if peak > 0.99: sig = sig/peak*0.95

        out[:] = (self.w_fh*sig).astype(np.complex64)
        self._n0 += N
        return N

效果:

image

(加噪声至snr=30db后)

双信号共存(静态+跳频)

  • 信号A(静态“锚点”):QPSK,sym_rate = 200e3f_offset = +500 kHz,持续全程。

  • 信号B(跳频“入侵者”):GFSK 或 2FSK,参数同 Level 1。

  • 叠加方式:A 与 B 相加(用两个发生器更干净;或一块中在不同时段切换两组参数并加噪)

  • 功率关系:P_B - P_A ∈ { -10, -5, 0, +5 } dB

  • 时长:每样本 5–10 s

  • 标注:为每条流各自产一条 meta,或统一为区间列表:

    • A:{mode:'QPSK', f_offset:+500k, sym_rate, SNR_A}

    • B:{mode:'GFSK', hop_id, f_offset, hop_period, SNR_B}

  • 实现要点:若只用现在的一块 block,做时间复用:例如 10 ms 内 8 ms 生成 A,2 ms 生成 B 并与 A 相加(把 B 的输出单独缓存在数组后再相加)。更简单的做法是在 flowgraph 里加第二个 block + Add。

代码

import numpy as np
from gnuradio import gr
import time


# --- 各调制函数 ---
def mod_BPSK(bits, freq, t):
    return np.exp(1j * (2 * np.pi * freq * t + np.pi * bits))


def mod_QPSK(bits, freq, t):
    # 保证比特数为偶数
    n_sym = len(bits) // 2
    bits = bits[:n_sym * 2]
    idx = bits[0::2] * 2 + bits[1::2]
    phases = np.pi / 4 + np.pi / 2 * idx
    # 生成每个符号的复数值
    sym = np.exp(1j * phases)
    # 重复符号以匹配采样点数量
    out = np.repeat(sym, int(np.ceil(len(t) / len(sym))))[:len(t)]
    return out * np.exp(1j * 2 * np.pi * freq * t)


def mod_GFSK(freq, t, bt=0.35):
    N = len(t)
    data = np.random.choice([-1, 1], N)
    samples_per_bit = max(2, int(len(t) / N))
    gauss = np.exp(-0.5 * (np.linspace(-2, 2, samples_per_bit * 2) ** 2) / bt ** 2)
    phase_dev = np.convolve(data, gauss, mode="same")
    phase = np.cumsum(phase_dev) * (np.pi / samples_per_bit)
    return np.exp(1j * (2 * np.pi * freq * t + phase))


def mod_16QAM(bits, freq, t):
    # 4 bit 一个符号
    n_sym = len(bits) // 4
    bits = bits[:n_sym * 4]
    b = bits.reshape(-1, 4)
    mI = (2 * b[:, 0] + b[:, 1]) * 2 - 3
    mQ = (2 * b[:, 2] + b[:, 3]) * 2 - 3
    sym = (mI + 1j * mQ) / np.sqrt(10)
    out = np.repeat(sym, int(np.ceil(len(t) / len(sym))))[:len(t)]
    return out * np.exp(1j * 2 * np.pi * freq * t)


def mod_2FSK(bits, freq, t, df=5e3):
    """二进制频移键控(2-FSK)"""
    # bits=0 → freq - df, bits=1 → freq + df
    freq_inst = freq + (2 * bits - 1) * df
    phase = np.cumsum(2 * np.pi * freq_inst / len(freq_inst))
    return np.exp(1j * phase)


def mod_OFDM(bits, freq, t, n_subcarriers=8, sub_spacing=1e3):
    """简化版 OFDM:多载波正交叠加"""
    # 生成多个子载波信号叠加
    carriers = []
    for k in range(n_subcarriers):
        sub_freq = freq + (k - n_subcarriers / 2) * sub_spacing
        phase = 2 * np.pi * sub_freq * t + np.pi * bits[k % len(bits)]
        carriers.append(np.exp(1j * phase))
    sig = np.sum(carriers, axis=0) / n_subcarriers
    return sig


# -------- 主 Block --------
class blk(gr.sync_block):
    def __init__(self):
        gr.sync_block.__init__(self,
                               name="Simple Scheduler",
                               in_sig=None,
                               out_sig=[np.complex64]
                               )
        self.samp_rate = 1e6
        self.freq_carrier = 1e6
        self.last_switch = time.time()
        self.state = "GFSK"  # 当前模式

    def work(self, input_items, output_items):
        out = output_items[0]
        N = len(out)
        t = np.arange(N) / self.samp_rate

        # --- 比特序列 ---
        symbol_rate = 1e3
        samples_per_symbol = max(1, int(self.samp_rate / symbol_rate))
        num_symbols = int(np.ceil(N / samples_per_symbol))
        bits = np.random.randint(0, 2, num_symbols)
        bits = np.repeat(bits, samples_per_symbol)[:N]

        elapsed = (time.time() - self.last_switch) * 1000  # 毫秒级

        # -------- 跳频信号参数 --------
        hop_period = 10.0  # 每 10 ms 跳一次
        hop_range = 2e6
        hop_step = 2e5
        if elapsed >= hop_period:
            f_list = np.arange(-hop_range, hop_range + hop_step, hop_step)
            self.freq_carrier = float(np.random.choice(f_list))
            self.last_switch = time.time()
            print(f"[Hop] GFSK carrier = {self.freq_carrier / 1e3:.1f} kHz")

        # -------- 跳频 GFSK 信号 --------
        sig_hop = mod_GFSK(self.freq_carrier, t, bt=0.1)

        # -------- 静态 QPSK 信号 --------
        static_freq = 0
        sig_static = mod_QPSK(bits, static_freq, t)

        # -------- 叠加两路信号 --------
        power_ratio = 0.5  # 控制相对功率(0.5 ≈ –6 dB)
        out[:] = sig_static + power_ratio * sig_hop
        return len(out)

工业帧式调度(周期 100 ms)

  • 时间轴(例):

    • 0–3 ms:同步/前导 BPSK,f_offset = 0

    • 3–10 ms:信标 GFSK,f_offset = +50 kHz

    • 10–60 ms:业务数据 QPSK 或 16QAM,f_offset = +200 kHz

    • 60–65 ms:跳频控制 GFSK,f_offset ∈ ±{100, 300, 500} kHz

    • 65–75 ms:应答窗口 BPSK(可置静默以模拟无应答)

    • 75–100 ms:空口(IDLE,纯零)

  • 速率建议:

    • BPSK:sym_rate=50e3

    • GFSK:sym_rate=100e3, bt=0.15

    • QPSK/16QAM:sym_rate=250e3

  • SNR 级别:{5, 10, 20} dB(在输出末端加 AWGN)

  • 标注(每帧一条,内含段落列表):

    {
      frame_id,
      segments:[
        {type:'SYNC', mode:'BPSK', t:[0,3ms], f:+0},
        {type:'BEACON', mode:'GFSK', t:[3,10ms], f:+50k, bt:0.15},
        {type:'DATA', mode:'QPSK', t:[10,60ms], f:+200k, sym:250k},
        {type:'FH_CTRL', mode:'GFSK', t:[60,65ms], f:+random},
        {type:'ACK', mode:'BPSK', t:[65,75ms], present:0/1},
        {type:'IDLE', t:[75,100ms]}
      ],
      SNR
    }
  • 实现要点: elapsed 判断直接照此表写。IDLE 段 out[:] = 0。FH 段内随机换 freq_carrier 1–2 次(子跳)。

代码

import numpy as np
from gnuradio import gr
import time


# --- 各调制函数 ---
def mod_BPSK(bits, freq, t):
    return np.exp(1j * (2 * np.pi * freq * t + np.pi * bits))


def mod_QPSK(bits, freq, t):
    # 保证比特数为偶数
    n_sym = len(bits) // 2
    bits = bits[:n_sym * 2]
    idx = bits[0::2] * 2 + bits[1::2]
    phases = np.pi / 4 + np.pi / 2 * idx
    # 生成每个符号的复数值
    sym = np.exp(1j * phases)
    # 重复符号以匹配采样点数量
    out = np.repeat(sym, int(np.ceil(len(t) / len(sym))))[:len(t)]
    return out * np.exp(1j * 2 * np.pi * freq * t)


def mod_GFSK(freq, t, bt=0.35):
    N = len(t)
    data = np.random.choice([-1, 1], N)
    samples_per_bit = max(2, int(len(t) / N))
    gauss = np.exp(-0.5 * (np.linspace(-2, 2, samples_per_bit * 2) ** 2) / bt ** 2)
    phase_dev = np.convolve(data, gauss, mode="same")
    phase = np.cumsum(phase_dev) * (np.pi / samples_per_bit)
    return np.exp(1j * (2 * np.pi * freq * t + phase))


def mod_16QAM(bits, freq, t):
    # 4 bit 一个符号
    n_sym = len(bits) // 4
    bits = bits[:n_sym * 4]
    b = bits.reshape(-1, 4)
    mI = (2 * b[:, 0] + b[:, 1]) * 2 - 3
    mQ = (2 * b[:, 2] + b[:, 3]) * 2 - 3
    sym = (mI + 1j * mQ) / np.sqrt(10)
    out = np.repeat(sym, int(np.ceil(len(t) / len(sym))))[:len(t)]
    return out * np.exp(1j * 2 * np.pi * freq * t)


def mod_2FSK(bits, freq, t, df=5e3):
    """二进制频移键控(2-FSK)"""
    # bits=0 → freq - df, bits=1 → freq + df
    freq_inst = freq + (2 * bits - 1) * df
    phase = np.cumsum(2 * np.pi * freq_inst / len(freq_inst))
    return np.exp(1j * phase)


def mod_OFDM(bits, freq, t, n_subcarriers=8, sub_spacing=1e3):
    """简化版 OFDM:多载波正交叠加"""
    # 生成多个子载波信号叠加
    carriers = []
    for k in range(n_subcarriers):
        sub_freq = freq + (k - n_subcarriers / 2) * sub_spacing
        phase = 2 * np.pi * sub_freq * t + np.pi * bits[k % len(bits)]
        carriers.append(np.exp(1j * phase))
    sig = np.sum(carriers, axis=0) / n_subcarriers
    return sig


# -------- 主 Block --------
class blk(gr.sync_block):
    def __init__(self):
        gr.sync_block.__init__(self,
                               name="Simple Scheduler",
                               in_sig=None,
                               out_sig=[np.complex64]
                               )
        self.samp_rate = 1e6
        self.freq_carrier = 1e6
        self.last_switch = time.time()
        self.state = "GFSK"  # 当前模式

    def work(self, input_items, output_items):
        out = output_items[0]
        N = len(out)
        t = np.arange(N) / self.samp_rate

        # --- 生成比特序列 ---
        symbol_rate = 1e3
        samples_per_symbol = max(1, int(self.samp_rate / symbol_rate))
        num_symbols = int(np.ceil(N / samples_per_symbol))
        bits = np.random.randint(0, 2, num_symbols)
        bits = np.repeat(bits, samples_per_symbol)[:N]

        # --- 周期计时(毫秒)---
        elapsed = (time.time() - self.last_switch) * 1000
        frame_period = 100.0  # 100 ms 周期

        # -------- 段落判断 --------
        if elapsed < 3:  # 同步/前导
            out[:] = mod_BPSK(bits, 0, t)

        elif elapsed < 10:  # 信标
            out[:] = mod_GFSK(+5e4, t, bt=0.15)

        elif elapsed < 60:  # 业务数据
            # 可切换 QPSK / 16QAM
            if np.random.rand() < 0.5:
                out[:] = mod_QPSK(bits, +2e5, t)
            else:
                out[:] = mod_16QAM(bits, +2e5, t)

        elif elapsed < 65:  # 跳频控制
            f = np.random.choice([+1e5, -1e5, +3e5, -3e5, +5e5, -5e5])
            out[:] = mod_GFSK(f, t, bt=0.2)

        elif elapsed < 75:  # 应答窗口(可置静默)
            if np.random.rand() < 0.7:  # 70 % 有应答
                out[:] = mod_BPSK(bits, 0, t)
            else:
                out[:] = 0

        elif elapsed < 100:  # 空口
            out[:] = 0

        else:  # 周期重启
            self.last_switch = time.time()
            out[:] = 0

        return len(out)

多业务混合

  • 三路并行概念(推荐用三块发生器+Add;若坚持单块,做时分叠加):

    1. FH 短突发:2FSK,hop_period=2 ms,占空 50%,f_offset 在 ±1 MHz 栅格随机。

    2. 宽带数据:16QAM,sym_rate=400e3,固定 f_offset=+300 kHz,间歇 20 ms ON / 10 ms OFF。

    3. 周期信标:GFSK,sym_rate=100e3f_offset=-150 kHz,每 50 ms 发 5 ms。

  • 功率:三路功率比 {0 dB, -6 dB, -12 dB} 的排列组合。

  • SNR:全局加噪,或对单路先缩放再相加。

  • 标注:按路记录,再加总体混合表。

代码:

import numpy as np
from gnuradio import gr
import time


# ======== 工具:RRC 滤波器 ========
def rrc_taps(beta=0.35, sps=10, span=6):
    N = span * sps
    t = np.arange(-N / 2, N / 2 + 1) / sps
    taps = np.zeros_like(t)
    for i, x in enumerate(t):
        if np.isclose(x, 0.0):
            taps[i] = 1.0 - beta + 4 * beta / np.pi
        elif np.isclose(abs(x), 1 / (4 * beta)):
            taps[i] = (beta / np.sqrt(2)) * (
                    (1 + 2 / np.pi) * np.sin(np.pi / (4 * beta)) +
                    (1 - 2 / np.pi) * np.cos(np.pi / (4 * beta))
            )
        else:
            num = np.sin(np.pi * x * (1 - beta)) + 4 * beta * x * np.cos(np.pi * x * (1 + beta))
            den = np.pi * x * (1 - (4 * beta * x) ** 2)
            taps[i] = num / den
    taps /= np.sqrt(np.sum(taps ** 2))
    return taps


# ======== 各调制函数 ========

def mod_2FSK(bits, freq, t, df=5e3):
    N = len(t)
    if N == 0:
        return np.zeros(0, np.complex64)
    dt = (t[1] - t[0]) if N > 1 else 0.0
    freq_inst = freq + (2 * bits.astype(np.float64) - 1) * df
    phase = 2 * np.pi * np.cumsum(freq_inst) * dt
    return np.exp(1j * phase).astype(np.complex64)


def mod_GFSK(freq, t, samp_rate, bt=0.35, sym_rate=1e5):
    N = len(t)
    if N == 0:
        return np.zeros(0, np.complex64)

    # 用实际采样率计算每符号采样点
    sps = max(2, int(round(samp_rate / float(sym_rate))))

    # 生成符号并上采样到 >= N
    n_sym = max(1, int(np.ceil(N / sps)))
    data = np.random.choice([-1, 1], n_sym)
    up = np.repeat(data, sps)
    if len(up) < N:
        up = np.pad(up, (0, N - len(up)), mode="edge")
    up = up[:N]  # 保证长度 = N

    # 高斯脉冲成形
    L = 5
    gauss = np.exp(-0.5 * (np.linspace(-2, 2, L) ** 2) / (bt ** 2))
    phase_dev = np.convolve(up, gauss, mode="same")

    # 双保险:长度对齐
    if len(phase_dev) > N:
        phase_dev = phase_dev[:N]
    elif len(phase_dev) < N:
        phase_dev = np.pad(phase_dev, (0, N - len(phase_dev)), mode="edge")

    phase = np.cumsum(phase_dev) * (np.pi / sps)
    sig = np.exp(1j * (2 * np.pi * freq * t + phase))
    return sig.astype(np.complex64)


def mod_16QAM_rrc(freq, t, samp_rate, sym_rate=4e5, beta=0.4):
    """16QAM + RRC 成形,去掉周期伪谱"""
    N = len(t)
    if N == 0:
        return np.zeros(0, np.complex64)
    sps = max(2, int(round(samp_rate / sym_rate)))
    n_sym = int(np.ceil(N / sps)) + 8

    bits = np.random.randint(0, 2, size=(n_sym, 4))
    mI = (2 * bits[:, 0] + bits[:, 1]) * 2 - 3
    mQ = (2 * bits[:, 2] + bits[:, 3]) * 2 - 3
    sym = (mI + 1j * mQ) / np.sqrt(10)
    sym *= np.exp(1j * 2 * np.pi * np.random.rand(n_sym))  # 打散周期

    up = np.zeros(n_sym * sps, dtype=np.complex128)
    up[::sps] = sym
    taps = rrc_taps(beta=beta, sps=sps, span=6)
    shaped = np.convolve(up, taps, mode="full")
    if len(shaped) < N:
        shaped = np.pad(shaped, (0, N - len(shaped)))
    baseband = shaped[:N]

    # 微弱相位噪声防固定纹理
    phi_noise = np.cumsum(np.random.randn(N) * 1e-3)
    baseband *= np.exp(1j * phi_noise)
    return (baseband * np.exp(1j * 2 * np.pi * freq * t)).astype(np.complex64)


class blk(gr.sync_block):
    """
    FH 2FSK 远跳 + 相位连续 + 慢速AGC(提清晰度)+ 仅溢出保护
    """

    def __init__(self, samp_rate=10e6):
        gr.sync_block.__init__(
            self,
            name="Scheduler (farther FH + phase continuity + slow AGC)",
            in_sig=None,
            out_sig=[np.complex64],
        )
        self.samp_rate = float(samp_rate)
        self.t0 = time.time()

        # --- FH 2FSK ---
        self.fh_hop_ms = 2.0
        self.fh_duty = 0.5
        self.fh_grid = np.arange(-1_000_000, 1_000_000 + 1, 200_000, dtype=np.int64)
        self.fh_guard_hz = 250e3
        self.fh_far_bias = 5.0
        self.fh_min_step_hz = 800e3
        self.fh_curr = np.inf
        self._fh_last_win = -1
        self._phi_fh = 0.0
        self._df_fh = 5e3

        # --- 宽带 16QAM ---
        self.wide_sym_rate = 4e5
        self.wide_offset = +300e3
        self.wide_beta = 0.5
        self.wide_period_ms = 30.0
        self.wide_on_ms = 20.0

        # --- 周期信标 GFSK ---
        self.bcn_sym_rate = 1e5
        self.bcn_offset = -150e3
        self.bcn_period_ms = 50.0
        self.bcn_on_ms = 5.0

        # 幅度权重与总增益
        self.w_fh, self.w_wide, self.w_bcn = 0.25, 2.0, 0.2
        self.master_gain = 1.0  # 可按需微调整体电平

        # --- 慢速 AGC(跨块RMS恒定) ---
        self.target_rms = 0.35          # 目标均方根幅度,给足余量避免削波
        self.agc_tau = 0.25             # 时间常数,秒;越大越慢
        self._ema_power = (self.target_rms ** 2)  # 初始化到目标功率
        self._last_gain = 1.0

    # ---------- 工具 ----------
    def _safe_off(self, f_off):
        limit = 0.45 * self.samp_rate
        return float(np.clip(f_off, -limit, limit))

    def _apply_fade(self, sig, fade_ratio=0.06):
        N = len(sig)
        if N < 8:
            return sig
        edge = max(4, int(fade_ratio * N))
        edge = min(edge, N // 2)
        if edge <= 0:
            return sig
        win = np.ones(N, dtype=np.float64)
        taper = 0.5 * (1 - np.cos(np.linspace(0, np.pi, edge)))
        win[:edge] = taper
        win[-edge:] = taper[::-1]
        return (sig * win).astype(sig.dtype)

    def _fh_pick_away_from(self, avoid_center, avoid_bw):
        lo = avoid_center - 0.5 * avoid_bw - self.fh_guard_hz
        hi = avoid_center + 0.5 * avoid_bw + self.fh_guard_hz

        base_allowed = self.fh_grid[(self.fh_grid < lo) | (self.fh_grid > hi)]
        if base_allowed.size == 0:
            base_allowed = self.fh_grid

        allowed = base_allowed
        if np.isfinite(self.fh_curr):
            m = np.abs(allowed - self.fh_curr) >= self.fh_min_step_hz
            if np.any(m):
                allowed = allowed[m]
            else:
                allowed = base_allowed

        if allowed.size == 1:
            return float(allowed[0])

        d_avoid = np.where(allowed < lo, lo - allowed, allowed - hi).astype(np.float64)
        d_prev = (np.abs(allowed - (self.fh_curr if np.isfinite(self.fh_curr) else 0.0))
                  .astype(np.float64))
        d_prev = np.maximum(d_prev, 1.0)
        w = (d_avoid ** max(self.fh_far_bias, 1.0)) * d_prev
        p = w / np.sum(w)
        return float(np.random.choice(allowed, p=p))

    def _slow_agc(self, sig):
        """
        跨块RMS保持:对平均功率做指数滑动估计,再把当前块缩放到 target_rms。
        设软限幅只在临近满刻度时启动。
        """
        # 指数平均的alpha由块长度和时间常数决定
        N = sig.size
        if N == 0:
            return sig
        alpha = 1.0 - np.exp(-N / (self.samp_rate * max(self.agc_tau, 1e-3)))

        p_hat = np.mean(np.real(sig)**2 + np.imag(sig)**2)
        self._ema_power = (1 - alpha) * self._ema_power + alpha * p_hat

        # 目标增益
        eps = 1e-9
        gain = self.target_rms / np.sqrt(max(self._ema_power, eps))

        # 抑制突变:对增益做小范围限速
        max_step = 1.25
        min_step = 0.8
        gain = np.clip(gain, self._last_gain * min_step, self._last_gain * max_step)
        self._last_gain = gain

        y = sig * (gain * self.master_gain)

        # 溢出保护:仅当幅度>0.98时做轻度压缩
        mag = np.abs(y)
        over = mag > 0.98
        if np.any(over):
            k = 0.5  # 压缩强度
            y[over] = y[over] / (1 + k * (mag[over] - 0.98))

        return y

    # ---------- 主工作 ----------
    def work(self, input_items, output_items):
        out = output_items[0]
        N = len(out)
        if N == 0:
            return 0

        t = np.arange(N, dtype=np.float64) / self.samp_rate
        dt = 1.0 / self.samp_rate
        now = time.time()
        elap_ms = (now - self.t0) * 1000.0

        # === FH 2FSK 调度 ===
        fh_win = int(elap_ms // self.fh_hop_ms)
        if fh_win != self._fh_last_win:
            self._fh_last_win = fh_win
            wide_bw = (1.0 + self.wide_beta) * self.wide_sym_rate
            self.fh_curr = self._safe_off(self._fh_pick_away_from(self.wide_offset, wide_bw))

        fh_phase_ms = elap_ms % self.fh_hop_ms
        fh_on = fh_phase_ms < (self.fh_hop_ms * self.fh_duty)

        sr_fh = 1e3
        sps_fh = max(1, int(self.samp_rate // sr_fh))
        n_fh = int(np.ceil(N / sps_fh))
        bits_fh = np.repeat(np.random.randint(0, 2, n_fh), sps_fh)[:N]

        if fh_on:
            # 基带相位跨块连续:先生成相对相位,再乘累计相位
            sig_fh_rel = mod_2FSK(bits_fh, self.fh_curr, t, df=self._df_fh)
            sig_fh = (sig_fh_rel * np.exp(1j * self._phi_fh)).astype(np.complex64)

            # 更新累计相位(本块内积分)
            freq_inst = self.fh_curr + (2 * bits_fh.astype(np.float64) - 1) * self._df_fh
            self._phi_fh = (self._phi_fh + 2 * np.pi * dt * np.sum(freq_inst)) % (2 * np.pi)

            # 渐入渐出与 hop 匹配
            hop_samples = max(1, int(self.fh_hop_ms * 1e-3 * self.samp_rate))
            fade_ratio_fh = float(np.clip(0.12 * hop_samples / max(N, 1), 0.04, 0.18))
            sig_fh = self._apply_fade(sig_fh, fade_ratio=fade_ratio_fh)
        else:
            sig_fh = np.zeros(N, np.complex64)

        # === 宽带 16QAM ===
        wide_phase_ms = elap_ms % self.wide_period_ms
        wide_on = (wide_phase_ms < self.wide_on_ms)
        wide_off = self._safe_off(self.wide_offset)

        if wide_on:
            sig_wide = mod_16QAM_rrc(
                wide_off, t, self.samp_rate,
                sym_rate=self.wide_sym_rate, beta=self.wide_beta
            )
            sig_wide = self._apply_fade(sig_wide, fade_ratio=0.05)
        else:
            sig_wide = np.zeros(N, np.complex64)

        # === 周期信标 GFSK ===
        bcn_phase_ms = elap_ms % self.bcn_period_ms
        bcn_on = (bcn_phase_ms < self.bcn_on_ms)
        bcn_off = self._safe_off(self.bcn_offset)

        if bcn_on:
            sig_bcn = mod_GFSK(bcn_off, t, self.samp_rate, bt=0.2, sym_rate=self.bcn_sym_rate)
            sig_bcn = self._apply_fade(sig_bcn, fade_ratio=0.05)
        else:
            sig_bcn = np.zeros(N, np.complex64)

        # 相位随机化仅对非 FH 分量
        rot = np.exp(1j * 2 * np.pi * np.random.rand(2))
        sig_wide *= rot[0]
        sig_bcn *= rot[1]

        # 叠加
        sig = self.w_fh * sig_fh + self.w_wide * sig_wide + self.w_bcn * sig_bcn

        # 慢速AGC提升清晰度,软限幅只做溢出保护
        sig = self._slow_agc(sig)

        out[:] = sig.astype(np.complex64)
        return N

效果:
image

BLE广播

# =============================================================================
# 名称
#   BLE Advertising(主广播 37→38→39 轮发)— GFSK 合成(低占空、干净边沿)
#
# 生成的是什么信号?
#   - 只模拟 BLE 主广播事件:按 37→38→39 的次序在三个主广播信道各发一帧短突发;
#   - 帧间插固定静默 IFG(默认 10 ms);事件之间按 ADV_INTERVAL_MS 加 0..ADV_JITTER_MS 抖动;
#   - 物理近似:GFSK(BT≈0.5,调制指数 h≈0.5);符号率默认 1 Msym/s(可改 2 Msym/s);
#   - 频率映射:三信道中心为 2402/2426/2480 MHz。为兼容低采样率,带宽按比例压缩至 ±0.45·Fs 内,
#     只保持三者的相对间隔(24 MHz 与 78 MHz 的比例)。需要“真跨度”,提高 SAMP_RATE。
#
# 行为与时序
#   - 一个“广播事件”包含 3 帧:ch37 → ch38 → ch39;
#   - 每帧持续 ADV_PDU_MS_RANGE 内的随机时长;帧间固定 IFG_MS 静默;
#   - 下一事件开始时间 = 上一事件开始时间 + ADV_INTERVAL_MS + U[0, ADV_JITTER_MS];
#   - 广播占空很低;FFT/瀑布上表现为三条稀疏、短促的 2 MHz 级窄带突发。
#
# 旁瓣与“横纹”控制
#   - 所有突发用余弦渐入/出(EDGE_MS);无按块 RMS 归一化,只用固定线性增益 OUT_LEVEL;
#   - 频点在静默中切换,避免频率阶跃引起的宽带横纹;跨块相位连续。
#
# 使用
#   1) 作为 epy_block 放入 GNU Radio 流图。仿真接 Throttle,外发接 USRP Sink。采样率用 SAMP_RATE;
#   2) 若要把三信道搬到某 RF 中心,设 F_SHIFT_HZ(例:USRP=2.440 GHz,要把 ch37≈2.402 GHz
#      落在此处,可设 F_SHIFT_HZ ≈ 2.402e9-2.440e9;其余两信道会按比例相对分布);
#   3) 想要更低占空:增大 ADV_INTERVAL_MS 或缩短 ADV_PDU_MS_RANGE 上限;想 2 Msym/s,将 BLE_SYM_RATE 设 2e6。
#
# 限制
#   - 非真实 BLE 协议栈:不生成接入地址、包头、CRC、跳频图或白化;仅复现“GFSK + 三信道 + 时序”的外观。
# =============================================================================

import numpy as np
from gnuradio import gr

# ====================== 顶部参数(无需 GUI 配置) ======================
SAMP_RATE         = 1e6          # 采样率;低 Fs 下三信道总跨度会按比例压缩
BLE_SYM_RATE      = 1.0e6        # 1e6 或 2e6(符号率);实际会被 sps≥SPS_MIN 约束
BT_GAUSS          = 0.50         # 高斯滤波 BT(BLE 规范约 0.5)
H_INDEX           = 0.50         # 调制指数 h(≈0.5 → 峰偏 df=0.25*Rs)

SPS_MIN           = 8            # 每符号最小采样点,保证成形质量
GAUSS_SPAN_SYM    = 4            # 高斯脉冲跨越的符号数(时域滤波长度)

# —— 广播事件时序(单位 ms) ——
ADV_INTERVAL_MS   = 30.0        # 广播事件基础间隔(20 ms..10240 ms 合理)
ADV_JITTER_MS     = 1         # 事件间隔叠加的 [0, JITTER] 抖动
ADV_PDU_MS_RANGE  = (1.2, 1.6)   # 单帧持续(BLE 广播包近似)
IFG_MS            = 3.0        # 帧间静默(ch37→ch38→ch39 之间)
EDGE_MS           = 0.20         # 每帧渐入/出时长(每侧)

# —— 三个主广播信道(真实差值:24 MHz、78 MHz) → 按 Fs 压缩 ——
AVOID_DC          = True         # 避开 0 Hz:整体加半个最小间隔的偏置
F_SHIFT_HZ        = 0.0          # 全局统一移频(限制在 ±0.45·Fs)
OUT_LEVEL         = 0.5          # 固定线性增益(不做按块 RMS)

SEED              = 37           # 随机种子(保证复现)

# ====================== 工具:高斯 GFSK 成形(流式) ======================
def _gauss_taps(bt=0.5, sps=8, span=4):
    """单位面积的离散高斯脉冲(σ 按 BT 推导,单位=符号时间)"""
    sigma_ts = np.sqrt(np.log(2.0)) / (2*np.pi*bt)
    L = span * sps
    t = (np.arange(-L/2, L/2 + 1, dtype=np.float64)) / sps
    g = np.exp(-(t**2) / (2*sigma_ts**2))
    g /= np.sum(g)
    return g

class GFSKStreamer:
    """
    连续 GFSK:NRZ±1 → 高斯滤波 → 频偏积分为相位。保持跨块滤波/相位/符号状态。
    """
    def __init__(self, fs, Rs_target, bt, h, sps_min=SPS_MIN, span_sym=GAUSS_SPAN_SYM, seed=0):
        self.fs = float(fs)
        Rs = min(Rs_target, self.fs / float(sps_min))
        self.sps = max(sps_min, int(round(self.fs / Rs)))
        self.Rs = self.fs / self.sps
        self.bt = float(bt)
        self.h  = float(h)
        self.span = int(span_sym)
        self.Ts = 1.0 / self.fs
        self.df = 0.5 * self.h * self.Rs  # 峰频偏
        self.taps = _gauss_taps(self.bt, self.sps, self.span)
        self.L = len(self.taps)
        self.prev_up = np.zeros(self.L-1, dtype=np.float64)
        self.phase = 0.0
        self.rng = np.random.default_rng(seed)
        self.cur_sym = 1.0 if self.rng.random() < 0.5 else -1.0
        self.rep_left = 0

    def _make_up(self, N):
        up = np.empty(N, dtype=np.float64)
        i = 0
        while i < N:
            if self.rep_left == 0:
                self.cur_sym = 1.0 if self.rng.random() < 0.5 else -1.0
                self.rep_left = self.sps
            take = min(self.rep_left, N - i)
            up[i:i+take] = self.cur_sym
            self.rep_left -= take
            i += take
        return up

    def next(self, N):
        up  = self._make_up(N)
        arr = np.concatenate([self.prev_up, up])
        filt= np.convolve(arr, self.taps, mode='valid')       # 长度恰为 N
        self.prev_up = arr[-(self.L-1):]
        dphi = 2*np.pi * (self.df * filt) * self.Ts
        phi  = self.phase + np.cumsum(dphi)
        self.phase = float(phi[-1] % (2*np.pi))
        return np.exp(1j * phi).astype(np.complex64)

# ====================== 突发包络工具 ======================
def _add_burst(env, start, end, fs, edge_samp, amp=1.0):
    """
    在 env 上叠加 [start,end) 的包络,边缘使用余弦渐入/出;amp 为峰值。
    env 为 0..1 浮点数组。
    """
    if end <= start:
        return
    N = len(env)
    i0 = int(np.floor(start * fs))
    i1 = int(np.ceil(end   * fs))
    if i1 <= 0 or i0 >= N:
        return
    i0c = max(0, i0); i1c = min(N, i1)
    L = i1c - i0c
    if L <= 0:
        return
    e = int(edge_samp)
    if 2*e >= L:
        # 极短突发:用 Hann
        w = 0.5*(1 - np.cos(2*np.pi*np.arange(L)/(L-1 if L>1 else 1)))
    else:
        w = np.ones(L, dtype=np.float64)
        w[:e]  = 0.5*(1 - np.cos(np.linspace(0, np.pi, e, endpoint=False)))
        w[-e:] = 0.5*(1 - np.cos(np.linspace(np.pi, 0, e, endpoint=False)))
    env[i0c:i1c] = np.maximum(env[i0c:i1c], amp*np.clip(w,0,1))

# ====================== epy_block:BLE 广播 ======================
class blk(gr.sync_block):
    """
    BLE_Advertising_Approx
    输出:complex64 基带;三主广播信道轮发的低占空 GFSK 突发。
    """
    def __init__(self):
        gr.sync_block.__init__(
            self, name="BLE_Advertising_Approx",
            in_sig=None, out_sig=[np.complex64],
        )
        self.fs = float(SAMP_RATE)
        self.rng = np.random.default_rng(SEED)
        self.t  = 0.0

        # —— GFSK 调制器 ——(自适应保证 sps≥SPS_MIN)
        self.mod = GFSKStreamer(self.fs, BLE_SYM_RATE, BT_GAUSS, H_INDEX, seed=SEED+1)

        # —— 三信道等比例压缩到 ±0.45·Fs 内 ——
        # 真实中心差:ch37→ch38 = 24 MHz;ch37→ch39 = 78 MHz
        span_real = 78e6
        scale = min(1.0, 0.90*self.fs / span_real)
        off_min = 24e6 * scale
        off_max = 78e6 * scale
        dc_off  = 0.5*off_min if AVOID_DC else 0.0
        # 基带内中心(Hz)
        self.ch_freqs = np.array([dc_off + 0.0,
                                  dc_off + off_min,
                                  dc_off + off_max], dtype=np.float64)
        self.f_shift  = float(np.clip(F_SHIFT_HZ, -0.45*self.fs, 0.45*self.fs))

        # —— 广播事件调度 ——
        self.edge_samp = int(max(1, round(float(EDGE_MS)*1e-3*self.fs)))
        self.ifg_s     = float(IFG_MS) * 1e-3
        self.pdu_min_s = float(ADV_PDU_MS_RANGE[0]) * 1e-3
        self.pdu_max_s = float(ADV_PDU_MS_RANGE[1]) * 1e-3
        self.T_ev_base = float(ADV_INTERVAL_MS) * 1e-3
        self.T_ev_jit  = float(ADV_JITTER_MS)  * 1e-3

        self.next_event = 0.0  # t=0 立即来一组
        self.mix_phase  = 0.0  # 混频相位连续

        # 固定线性增益(不做按块 RMS)
        self.fixed_gain = float(OUT_LEVEL)

        # 记录(可用作标注):真实中心 MHz
        self.channel_MHz = [2402.0, 2426.0, 2480.0]

    def _schedule_event_into(self, t0, t1, env, fseq):
        """
        把所有落入 [t0,t1) 的广播事件叠加到 env,并在对应段落设置 fseq。
        事件:ch37→ch38→ch39;帧间隔 IFG;下一事件从“上一事件开始时刻”加抖动间隔计算。
        """
        Fs = self.fs
        while self.next_event < t1 + 1e-12:
            ev_start = self.next_event
            # 三帧的起止
            t_cur = ev_start
            for ch_i in (0, 1, 2):  # 37→38→39
                dur = self.rng.uniform(self.pdu_min_s, self.pdu_max_s)
                # 叠加包络
                _add_burst(env, t_cur - t0, t_cur + dur - t0, Fs, self.edge_samp, amp=1.0)
                # 在该段填充对应中心频率(静默段 fseq 无关)
                i0 = int(max(0, np.floor((t_cur - t0)*Fs)))
                i1 = int(min(len(fseq), np.ceil((t_cur + dur - t0)*Fs)))
                if i1 > i0:
                    fseq[i0:i1] = self.ch_freqs[ch_i]
                t_cur += dur + self.ifg_s
            # 下一事件:从当前事件“开始时刻”起按间隔+抖动
            self.next_event = ev_start + self.T_ev_base + self.rng.uniform(0.0, self.T_ev_jit)

    def work(self, input_items, output_items):
        out = output_items[0]
        N = len(out)
        if N == 0:
            return 0

        fs = self.fs
        t0 = self.t
        t1 = t0 + N/fs

        # 1) 连续 GFSK 基底
        base = self.mod.next(N)  # complex64,幅度≈1

        # 2) 事件包络与频率序列
        env  = np.zeros(N, dtype=np.float64)
        fseq = np.zeros(N, dtype=np.float64)  # 仅在突发段赋值;静默段无所谓
        self._schedule_event_into(t0, t1, env, fseq)

        # 3) 逐样混频(跨块相位连续)
        dphi = 2*np.pi * (fseq + self.f_shift) / fs
        ph   = self.mix_phase + np.cumsum(dphi)
        self.mix_phase = float(ph[-1] % (2*np.pi))
        mix  = np.exp(1j * ph).astype(np.complex64)

        # 4) 合成与固定增益
        sig = base * mix * env.astype(np.float64)
        sig *= self.fixed_gain

        out[:] = sig.astype(np.complex64)
        self.t = t1
        return N

image

 

Bluetooth Classic FHSS 持续链路

# =============================================================================
# Bluetooth Classic (BR/EDR) FHSS — 稀疏版(慢跳频 + 低占空 + 子集信道)— 修复横纹
# 关键修复:
#   [FIX-1] 驻留幅度在跳变处做余弦交叉渐变(不再整段阶跃),避免全频横纹
#   [FIX-2] 取消按块 RMS 归一化,使用固定线性增益,消除块间幅度跳步
# =============================================================================

import numpy as np
from gnuradio import gr

# ---------------------- 顶部参数 ----------------------
SAMP_RATE        = 1e6
MODE             = "BR"         # "BR" | "EDR2" | "EDR3"
SEED             = 11

# BR(GFSK)
BR_SYM_RATE      = 1.0e6
BR_BT            = 0.50
BR_H             = 0.32

# EDR(DPSK 近似)
EDR2_SYM_RATE    = 2.0e6
EDR3_SYM_RATE    = 3.0e6
RRC_BETA         = 0.35
RRC_SPAN_SYM     = 12

# 跳频与频栅格
N_CHANNELS       = 79
GRID_HZ_TARGET   = 1.0e6
HOP_DWELL_US     = 2500.0       # 慢跳频(可调)
HOP_EDGE_FRAC    = 0.25         # 跳变渐变宽度(每侧占驻留比例)
HOP_DIP_DB       = 12.0         # 跳变“凹口”深度(dB)
AVOID_DC         = True

# 稀疏信道
ACTIVE_CH_COUNT  = 20
CHANNEL_SELECTION= "SPACED"     # "SPACED" | "RANDOM"

# 业务占空(两层)
ONOFF_ENABLE     = True
ON_MS_RANGE      = (5.0, 15.0)
OFF_MS_RANGE     = (5.0, 50.0)
ONOFF_EDGE_MS    = 1.0

IDLE_PROB_PER_HOP= 0.30         # ON 状态下,每个驻留段静音的概率
SILENCE_FLOOR_DB = 60.0         # 静音地板幅度

# 统一移频与电平
F_SHIFT_HZ       = 0.0
OUT_LEVEL        = 0.5          # [FIX-2] 固定线性增益(不做按块RMS)

# 数值稳定
SPS_MIN          = 8
GAUSS_SPAN_SYM   = 4

# ---------------------- 工具函数 ----------------------
def _clip_off(f_off, fs):
    return float(np.clip(f_off, -0.45*fs, 0.45*fs))

def _rcos_taps(beta=0.35, sps=8, span=12):
    N = span * sps
    t = np.arange(-N/2, N/2 + 1, dtype=np.float64) / sps
    h = np.zeros_like(t)
    for i, x in enumerate(t):
        ax = abs(x)
        if np.isclose(x, 0.0):
            h[i] = 1.0 - beta + 4*beta/np.pi
        elif np.isclose(ax, 1.0/(4*beta)):
            h[i] = (beta/np.sqrt(2.0))*((1+2/np.pi)*np.sin(np.pi/(4*beta)) + (1-2/np.pi)*np.cos(np.pi/(4*beta)))
        else:
            h[i] = (np.sin(np.pi*x*(1-beta)) + 4*beta*x*np.cos(np.pi*x*(1+beta))) / (np.pi*x*(1-(4*beta*x)**2))
    h /= np.sqrt(np.sum(h**2) + 1e-15)
    return h

def _gauss_taps(bt=0.5, sps=8, span=4):
    sigma_ts = np.sqrt(np.log(2.0)) / (2*np.pi*bt)
    L = span * sps
    t = (np.arange(-L/2, L/2 + 1, dtype=np.float64)) / sps
    g = np.exp(-(t**2) / (2*sigma_ts**2))
    g /= np.sum(g)
    return g

def _apply_valley(env, idx, edge, floor_amp):
    """跳变中心 idx 处乘一个对称‘凹口’,抑制频率阶跃泄露。"""
    N = len(env)
    floor_amp = float(np.clip(floor_amp, 0.0, 1.0))
    i0 = max(0, idx - edge)
    i1 = min(N, idx + edge)
    if i1 <= i0 or edge <= 1:
        return
    left_L  = min(edge, idx - i0)
    right_L = min(edge, i1 - idx)
    if left_L > 0:
        k = np.arange(left_L)
        w = floor_amp + (1.0 - floor_amp) * 0.5 * (1 + np.cos(np.pi * (k+1)/left_L))
        env[idx-left_L:idx] *= w
    if right_L > 0:
        k = np.arange(right_L)
        w = floor_amp + (1.0 - floor_amp) * 0.5 * (1 + np.cos(np.pi * (right_L-k)/right_L))
        env[idx:idx+right_L] *= w

def _apply_fade(mask, edge_samp):
    if edge_samp <= 1:
        return mask.astype(np.float64)
    N = len(mask)
    out = mask.astype(np.float64).copy()
    up = np.flatnonzero(np.diff(np.pad(mask.astype(np.int8),(1,0)))==1)
    for i0 in up:
        i1 = min(N, i0 + edge_samp)
        ramp = 0.5*(1 - np.cos(np.linspace(0, np.pi, i1-i0)))
        out[i0:i1] *= ramp
    dn = np.flatnonzero(np.diff(np.pad(mask.astype(np.int8),(0,1)))==-1)
    for j1 in dn:
        j0 = max(0, j1 - edge_samp)
        ramp = 0.5*(1 - np.cos(np.linspace(np.pi, 0, j1-j0)))
        out[j0:j1] *= ramp
    return out

def _crossfade_amp(env_amp, idx, edge, a_prev, a_next):
    """
    [FIX-1] 在跳变中心 idx 两侧各 edge 样本内,将幅度从 a_prev 余弦平滑过渡到 a_next。
    只写绝对幅度包络(非乘法),避免整段阶跃导致的‘横纹’。
    """
    N = len(env_amp)
    i0 = max(0, idx - edge)
    i1 = min(N, idx + edge)
    if i1 <= i0 or edge <= 0:
        return
    L = i1 - i0
    # 0..1 的余弦权重
    r = 0.5*(1 - np.cos(2*np.pi*np.arange(L)/(L-1 if L>1 else 1)))
    env_amp[i0:idx] = a_prev*(1 - r[:idx-i0]) + a_next*r[:idx-i0]
    env_amp[idx:i1] = a_prev*(1 - r[idx-i0:])  + a_next*r[idx-i0:]

# ---------------------- 调制器 ----------------------
class GFSKStreamer:
    def __init__(self, fs, Rs_target, bt, h, sps_min=SPS_MIN, span_sym=GAUSS_SPAN_SYM, seed=0):
        self.fs = float(fs)
        Rs = min(Rs_target, self.fs / float(sps_min))
        self.sps = max(sps_min, int(round(self.fs / Rs)))
        self.Rs = self.fs / self.sps
        self.bt = float(bt); self.h = float(h)
        self.span = int(span_sym)
        self.Ts = 1.0 / self.fs
        self.df = 0.5 * self.h * self.Rs
        self.taps = _gauss_taps(self.bt, self.sps, self.span)
        self.L = len(self.taps)
        self.prev_up = np.zeros(self.L-1, dtype=np.float64)
        self.phase = 0.0
        self.rng = np.random.default_rng(seed)
        self.cur_sym = 1.0 if self.rng.random() < 0.5 else -1.0
        self.rep_left = 0

    def _make_up(self, N):
        up = np.empty(N, dtype=np.float64)
        i = 0
        while i < N:
            if self.rep_left == 0:
                self.cur_sym = 1.0 if self.rng.random() < 0.5 else -1.0
                self.rep_left = self.sps
            take = min(self.rep_left, N - i)
            up[i:i+take] = self.cur_sym
            self.rep_left -= take
            i += take
        return up

    def next(self, N):
        up = self._make_up(N)
        arr = np.concatenate([self.prev_up, up])
        filt = np.convolve(arr, self.taps, mode='valid')
        self.prev_up = arr[-(self.L-1):]
        dphi = 2*np.pi * (self.df * filt) * self.Ts
        phi = self.phase + np.cumsum(dphi)
        self.phase = float(phi[-1] % (2*np.pi))
        return np.exp(1j * phi).astype(np.complex64)

class DPSKStreamer:
    def __init__(self, fs, Rs_target, M, beta=0.35, sps_min=SPS_MIN, span_sym=RRC_SPAN_SYM, seed=0):
        self.fs = float(fs)
        Rs = min(Rs_target, self.fs / float(sps_min))
        self.sps = max(sps_min, int(round(self.fs / Rs)))
        self.Rs = self.fs / self.sps
        self.M = int(M); self.beta = float(beta)
        self.span = int(span_sym)
        self.taps = _rcos_taps(self.beta, self.sps, self.span)
        self.L = len(self.taps)
        self.prev_up = np.zeros(self.L-1, dtype=np.complex128)
        self.rng = np.random.default_rng(seed)
        self.phase = 0.0

    def _symbol_stream(self, Nsym):
        k = self.rng.integers(0, self.M, size=Nsym, dtype=np.int32)
        dtheta = 2*np.pi * k / self.M
        theta = np.cumsum(dtheta) + self.phase
        self.phase = float(theta[-1] % (2*np.pi))
        return np.exp(1j * theta)

    def next(self, N):
        Nsym = int(np.ceil(N / self.sps)) + self.span
        sym = self._symbol_stream(Nsym)
        up = np.zeros(Nsym * self.sps, dtype=np.complex128)
        up[::self.sps] = sym
        arr = np.concatenate([self.prev_up, up])
        shaped = np.convolve(arr, self.taps, mode='valid')[:N]
        self.prev_up = arr[-(self.L-1):]
        return shaped.astype(np.complex64)

# ---------------------- epy_block ----------------------
class blk(gr.sync_block):
    """
    BT_Classic_FHSS_Sparse_Fixed
    修复‘横纹’后的稀疏 FHSS。
    """
    def __init__(self):
        gr.sync_block.__init__(
            self, name="BT_Classic_FHSS_Sparse_Fixed",
            in_sig=None, out_sig=[np.complex64],
        )
        self.fs = float(SAMP_RATE)
        self.rng = np.random.default_rng(SEED)
        self.t = 0.0

        # 调制器
        mode = str(MODE).upper()
        if mode == "BR":
            self.mod = GFSKStreamer(self.fs, BR_SYM_RATE, BR_BT, BR_H, seed=SEED+1)
        elif mode == "EDR2":
            self.mod = DPSKStreamer(self.fs, EDR2_SYM_RATE, M=4, beta=RRC_BETA, seed=SEED+2)
        elif mode == "EDR3":
            self.mod = DPSKStreamer(self.fs, EDR3_SYM_RATE, M=8, beta=RRC_BETA, seed=SEED+3)
        else:
            raise ValueError("MODE must be 'BR' | 'EDR2' | 'EDR3'")

        # 栅格(自动压缩)
        full_grid = min(GRID_HZ_TARGET, 0.90*self.fs / (N_CHANNELS-1))
        self.grid_hz = full_grid
        dc_off = 0.5*full_grid if AVOID_DC else 0.0
        k = np.arange(N_CHANNELS) - (N_CHANNELS-1)/2.0
        all_freqs = k * full_grid + dc_off

        # 子集信道
        M = int(np.clip(ACTIVE_CH_COUNT, 1, N_CHANNELS))
        if CHANNEL_SELECTION.upper() == "SPACED":
            idx = np.round(np.linspace(0, N_CHANNELS-1, M)).astype(int)
        else:
            idx = self.rng.choice(N_CHANNELS, size=M, replace=False); idx.sort()
        self.chan_idx = idx
        self.chan_freqs = all_freqs[self.chan_idx]

        # 跳频时序与包络参数
        self.dwell = float(HOP_DWELL_US) * 1e-6
        self.edge_samp = max(2, int(self.dwell * self.fs * float(HOP_EDGE_FRAC)))
        self.valley = 10**(-float(HOP_DIP_DB)/20.0)
        self.cur_ch_i = int(self.rng.integers(0, len(self.chan_freqs)))
        self.next_hop = self.dwell

        # [FIX-1] 驻留幅度状态,用于跨块与跨跳的平滑
        self.silence_floor = 10**(-float(SILENCE_FLOOR_DB)/20.0)
        self.cur_amp = 1.0  # 当前驻留幅度
        # 在 ON 状态下,下一驻留目标幅度按概率为 1 或 floor
        self.next_amp = 1.0

        # ON/OFF
        self.onoff_enable = bool(ONOFF_ENABLE)
        self.onoff_state = 1
        self.onoff_edge = int(max(1, ONOFF_EDGE_MS*1e-3*self.fs))
        self.onoff_next = self.t + self.rng.uniform(*ON_MS_RANGE)*1e-3

        # 混频相位与平移
        self.mix_phase = 0.0
        self.f_shift = _clip_off(F_SHIFT_HZ, self.fs)

        # [FIX-2] 固定线性增益
        self.fixed_gain = float(OUT_LEVEL)

    def _draw_next_index(self, prev_i):
        M = len(self.chan_freqs)
        j = int(self.rng.integers(0, M))
        if j == prev_i:
            j = (j + 1) % M
        return j

    def _onoff_env(self, t0, N):
        Fs = self.fs
        t  = t0 + np.arange(N)/Fs
        mask = np.ones(N, dtype=np.int8)
        if not self.onoff_enable:
            return mask.astype(np.float64)
        state = self.onoff_state
        next_flip = self.onoff_next
        cur = t0
        while True:
            seg_end = min(t[-1], next_flip)
            if state == 0:
                k0 = int(max(0, np.floor((cur - t0)*Fs)))
                k1 = int(min(N, np.ceil((seg_end - t0)*Fs)))
                if k1 > k0:
                    mask[k0:k1] = 0
            if seg_end >= t[-1] - 1e-12:
                break
            state = 1 - state
            cur = seg_end
            dur_ms = self.rng.uniform(*(ON_MS_RANGE if state==1 else OFF_MS_RANGE))
            next_flip = seg_end + dur_ms*1e-3
        self.onoff_state = state
        self.onoff_next  = next_flip
        return _apply_fade(mask, self.onoff_edge)

    def work(self, input_items, output_items):
        out = output_items[0]; N = len(out)
        if N == 0: return 0

        fs = self.fs
        t0 = self.t
        t1 = t0 + N/fs

        # 调制
        base = self.mod.next(N)  # 幅度≈1

        # 频率序列、跳变凹口、幅度包络(含交叉渐变)
        fseq = np.empty(N, dtype=np.float64); fseq[:] = self.chan_freqs[self.cur_ch_i]
        env_valley = np.ones(N, dtype=np.float64)
        env_amp    = np.empty(N, dtype=np.float64); env_amp[:] = float(self.cur_amp)

        # 处理本块内的所有跳变(按时间顺序)
        while self.next_hop < t1 + 1e-12:
            idx = int(np.floor((self.next_hop - t0) * fs))
            if 0 <= idx < N:
                # 跳变:频率切换 + 凹口
                _apply_valley(env_valley, idx, self.edge_samp, self.valley)
                prev_i = self.cur_ch_i
                self.cur_ch_i = self._draw_next_index(prev_i)
                new_f = self.chan_freqs[self.cur_ch_i]
                fseq[idx:] = new_f

                # [FIX-1] 幅度交叉渐变:从 self.cur_amp → amp_next
                # 仅在 ON 状态下按概率静音;OFF 时强制静音地板
                if (not self.onoff_enable) or (self.onoff_state == 1):
                    amp_next = 1.0 if (self.rng.random() >= float(IDLE_PROB_PER_HOP)) else self.silence_floor
                else:
                    amp_next = self.silence_floor
                _crossfade_amp(env_amp, idx, self.edge_samp, float(self.cur_amp), float(amp_next))
                self.cur_amp = amp_next

            self.next_hop += self.dwell

        # ON/OFF 大包络
        env_onoff = self._onoff_env(t0, N)

        # 混频(跨块相位连续)
        dphi = 2*np.pi * (fseq + self.f_shift) / fs
        ph = self.mix_phase + np.cumsum(dphi)
        self.mix_phase = float(ph[-1] % (2*np.pi))
        mix = np.exp(1j * ph).astype(np.complex64)

        # 合成:基带 * 混频 * 三层包络(凹口 × 驻留幅度 × ON/OFF)
        env = env_valley * env_amp * env_onoff
        sig = base * mix * env.astype(np.float64)

        # [FIX-2] 固定线性增益(不做按块 RMS)
        sig *= self.fixed_gain

        out[:] = sig.astype(np.complex64)
        self.t = t1
        return N

image

Wi-Fi “空闲+信标"

# =============================================================================
# 名称
#   Wi-Fi 2.4 GHz “空闲 + 信标” 频谱占用近似(低占空、窄突发、低旁瓣)
#
# 生成的是什么信号?
#   - 固定信道(不跳频)的“空闲 AP”外观:大部分时间静默,仅周期性信标突发;
#   - 偶尔插入“探测/管理帧”类的短包,使占空上到 1–3% 以内;
#   - 非真实 802.11 PHY:用 16QAM + RRC 的“宽带随机符号流”近似 OFDM 的频域占用,仅复现宏观节律。
#
# 行为与节律
#   - 信标周期:100 TU ≈ 102.4 ms(可加抖动);每次信标为 0.3–1.0 ms 的短突发;
#   - 探测/管理帧:按概率在相邻信标之间随机插入 0.8–2.0 ms 的短突发,电平可与信标不同;
#   - 占空目标:默认 ~0.5%(仅信标)到 ~2%(含偶发管理帧)。
#
# 旁瓣控制
#   - 先成形(RRC,sps≥8,span≥12)后移频;
#   - 所有突发边缘做余弦渐入/出(避免硬门控泄露);
#   - 可选 Kaiser 窗 FIR 低通进一步压带外(~80 dB 级,取决于 Fs 与参数)。
#
# 使用
#   1) 将本文件作为 epy_block 接到 Throttle(仿真)或 USRP Sink(外发),采样率用 SAMP_RATE;
#   2) 设 F_SHIFT_HZ 把基带搬到目标中心(例:USRP=2.440 GHz,要合成信道1中心→F_SHIFT_HZ=2.412e9-2.440e9);
#   3) 想更低占空:缩短 BEACON_LEN_MS 或调低 MGMT_PROB、拉大 MGMT_GAP_MS_RANGE。
# =============================================================================

import numpy as np
from gnuradio import gr

# ====================== 固定参数(无需 GUI 配置) ======================
SAMP_RATE             = 1e6         # 采样率;低 Fs 运行轻快;升高可得更宽合成占用
CHANNEL               = 6           # 仅记录:1..13 → 2412+5*(ch-1) MHz
BETA                  = 0.35        # RRC 滚降
TARGET_BW_HZ          = 20e6        # 目标带宽;实际带宽≈(1+β)*R_s,会按 sps 约束自适应
SPS_TARGET            = 8           # 成形过采样下限;≥8 抑制泄露更稳
RRC_SPAN_SYM          = 12          # RRC 脉冲长度(符号数)

# —— 信标(Beacon)设置 ——
BEACON_INTERVAL_MS    = 102.4       # 周期 100 TU
BEACON_JITTER_MS      = 2.0         # 周期抖动(±)上限,0 表示无抖动
BEACON_LEN_MS_RANGE   = (0.4, 0.8)  # 单次信标时长范围
BEACON_EDGE_MS        = 0.20        # 渐入/出时长(每侧)

# —— 偶发探测/管理帧(低占空突发)设置 ——
MGMT_ENABLE           = True
MGMT_PROB_PER_BEACON  = 0.20        # 每个信标周期后插入一次管理帧的概率
MGMT_DELAY_MS_RANGE   = (4.0, 40.0) # 管理帧相对上次信标的延迟
MGMT_LEN_MS_RANGE     = (0.8, 2.0)  # 管理帧时长
MGMT_EDGE_MS          = 0.30        # 管理帧渐入/出时长
MGMT_REL_AMP_DB       = -3.0        # 相对信标的电平(dB)

# 频率与电平
DATA_CFO_HZ           = 0.0         # 轻微 CFO(可 0)
F_SHIFT_HZ            = 0.0         # 统一移频(限制在 ±0.45*Fs 内)
OUT_LEVEL             = 0.5         # 输出 RMS 归一化目标
SEED                  = 2025        # 随机种子

# 带外抑制 FIR(Kaiser 低通,可选)
LPF_ENABLE            = True
LPF_ATTEN_DB          = 80.0        # 阻带衰减指标
LPF_PASS_KEEP         = 0.90        # 通过带截止=0.5*(1+β)*R_s*PASS_KEEP
LPF_TRANS_FRAC        = 0.20        # 过渡带宽占目标带宽的比例

# ====================== 滤波与调制工具 ======================
def rrc_taps(beta=0.35, sps=8, span=12):
    N = span * sps
    t = np.arange(-N/2, N/2 + 1, dtype=np.float64) / sps
    taps = np.zeros_like(t, dtype=np.float64)
    for i, x in enumerate(t):
        ax = abs(x)
        if np.isclose(x, 0.0):
            taps[i] = 1.0 - beta + 4*beta/np.pi
        elif np.isclose(ax, 1.0/(4*beta)):
            taps[i] = (beta/np.sqrt(2.0)) * (
                (1 + 2/np.pi) * np.sin(np.pi/(4*beta)) +
                (1 - 2/np.pi) * np.cos(np.pi/(4*beta))
            )
        else:
            num = (np.sin(np.pi * x * (1 - beta))
                   + 4*beta*x*np.cos(np.pi * x * (1 + beta)))
            den = np.pi * x * (1 - (4*beta*x)**2)
            taps[i] = num / den
    taps /= np.sqrt(np.sum(taps**2) + 1e-15)
    return taps

def fir_lpf_kaiser(fs, f_pass, f_trans, atten_db):
    dw = 2*np.pi * (f_trans / fs)
    if atten_db > 50:
        beta = 0.1102*(atten_db - 8.7)
    elif atten_db >= 21:
        beta = 0.5842*(atten_db - 21)**0.4 + 0.07886*(atten_db - 21)
    else:
        beta = 0.0
    M = int(np.ceil((atten_db - 8.0) / (2.285 * dw))) | 1
    n = np.arange(M) - (M-1)/2
    h = 2*(f_pass/fs) * np.sinc(2*f_pass*n/fs)
    w = np.kaiser(M, beta)
    taps = h * w
    taps /= np.sum(taps)
    return taps.astype(np.float64)

def mod_16QAM_rrc(freq, t, samp_rate, sym_rate, beta, rng, sps, span):
    """16QAM + RRC,上采样后先成形再移频;相位随 t 连续。"""
    N = len(t)
    if N == 0:
        return np.zeros(0, np.complex64)
    n_sym = int(np.ceil(N / sps)) + span
    bits = rng.integers(0, 2, size=(n_sym, 4), dtype=np.int8)
    I = (2*bits[:,0]+bits[:,1]) - 1.5
    Q = (2*bits[:,2]+bits[:,3]) - 1.5
    symbols = (I + 1j*Q) / np.sqrt(10)
    up = np.zeros(n_sym * sps, dtype=np.complex128)
    up[::sps] = symbols
    taps = rrc_taps(beta=beta, sps=sps, span=span)
    shaped = np.convolve(up, taps, mode="same")[:N]
    return (shaped * np.exp(1j * 2*np.pi * freq * t)).astype(np.complex64)

# ====================== 突发包络工具 ======================
def _add_burst(env, start, end, fs, edge_samp, amp=1.0):
    """在 env 上叠加一个 [start,end) 的突发,边缘余弦渐入/出,峰值 amp。"""
    if end <= start:
        return
    N = len(env)
    i0 = int(np.floor(start * fs))
    i1 = int(np.ceil(end   * fs))
    if i1 <= 0 or i0 >= N:
        return
    i0_clip = max(0, i0); i1_clip = min(N, i1)
    L = i1_clip - i0_clip
    if L <= 0:
        return
    e = int(edge_samp)
    if 2*e >= L:
        # 短包:用 Hann(单周期余弦)整形
        w = 0.5*(1 - np.cos(2*np.pi*np.arange(L)/(L-1 if L>1 else 1)))
    else:
        w = np.ones(L, dtype=np.float64)
        # 上升沿
        w[:e] = 0.5*(1 - np.cos(np.linspace(0, np.pi, e, endpoint=False)))
        # 下降沿
        w[-e:] = 0.5*(1 - np.cos(np.linspace(np.pi, 0, e, endpoint=False)))
    env[i0_clip:i1_clip] = np.maximum(env[i0_clip:i1_clip], amp*np.clip(w,0,1))

# ====================== epy_block 实现 ======================
class blk(gr.sync_block):
    """
    WiFi_IdleWithBeacon_Approx
    输出:低占空的“空闲+信标”基带信号(complex64)。
    """
    def __init__(self):
        gr.sync_block.__init__(
            self, name="WiFi_IdleWithBeacon_Approx",
            in_sig=None, out_sig=[np.complex64],
        )
        # 采样与成形自适应
        self.samp_rate = float(SAMP_RATE)
        self.beta = float(BETA)
        self.rng = np.random.default_rng(SEED)
        self.t = 0.0

        Rs_need = TARGET_BW_HZ / (1.0 + self.beta)
        Rs_sps  = self.samp_rate / float(SPS_TARGET)
        self.Rs = min(Rs_need, Rs_sps)
        self.sps = max(SPS_TARGET, int(round(self.samp_rate / self.Rs)))
        self.Rs  = self.samp_rate / self.sps
        self.span = int(RRC_SPAN_SYM)

        # 记录用中心频率
        self.channel = int(CHANNEL)
        self.fc_MHz = 2412 + 5*(self.channel-1)

        # 事件调度状态
        self.beacon_T  = float(BEACON_INTERVAL_MS) * 1e-3
        self.beacon_j  = float(BEACON_JITTER_MS) * 1e-3
        self.next_beacon = self.beacon_T  # 从 t=0 起,先来一次
        self.data_cfo  = float(DATA_CFO_HZ)
        self.f_shift   = float(F_SHIFT_HZ)
        self.out_level = float(OUT_LEVEL)

        # FIR 状态
        self.lpf_taps  = None
        self.lpf_state = np.zeros(0, np.complex64)
        if LPF_ENABLE:
            tgt_bw  = (1.0 + self.beta) * self.Rs
            f_pass  = 0.5 * tgt_bw * float(LPF_PASS_KEEP)
            f_trans = max(200.0, float(LPF_TRANS_FRAC) * tgt_bw)
            self.lpf_taps  = fir_lpf_kaiser(self.samp_rate, f_pass, f_trans, LPF_ATTEN_DB)
            self.lpf_state = np.zeros(len(self.lpf_taps)-1, np.complex64)

        # 预先把管理帧相对幅度转为线性
        self.mgmt_amp = 10**(float(MGMT_REL_AMP_DB)/20.0)

    def _schedule_events(self, t0, t1, env):
        """在 [t0,t1) 内向 env 叠加信标与管理帧包络。"""
        Fs = self.samp_rate
        # 迭代添加跨度内的所有信标
        while self.next_beacon < t1 + 1e-12:
            b_start = self.next_beacon
            b_len   = self.rng.uniform(*BEACON_LEN_MS_RANGE) * 1e-3
            b_edge  = int(round(float(BEACON_EDGE_MS) * 1e-3 * Fs))
            _add_burst(env, b_start - t0, b_start + b_len - t0, Fs, b_edge, amp=1.0)

            # 可能插入一帧管理帧
            if MGMT_ENABLE and (self.rng.random() < float(MGMT_PROB_PER_BEACON)):
                delay = self.rng.uniform(*MGMT_DELAY_MS_RANGE) * 1e-3
                m_start = b_start + delay
                m_len   = self.rng.uniform(*MGMT_LEN_MS_RANGE) * 1e-3
                m_edge  = int(round(float(MGMT_EDGE_MS) * 1e-3 * Fs))
                _add_burst(env, m_start - t0, m_start + m_len - t0, Fs, m_edge, amp=self.mgmt_amp)

            # 下一次信标时间(带对称抖动)
            jitter = self.rng.uniform(-self.beacon_j, self.beacon_j)
            # 保证不低于 50% 周期,避免极端重叠
            self.next_beacon += max(0.5*self.beacon_T, self.beacon_T + jitter)

    def work(self, input_items, output_items):
        out = output_items[0]
        N = len(out)
        if N == 0:
            return 0
        Fs = self.samp_rate
        t_vec = self.t + np.arange(N)/Fs

        # 生成“宽带占用近似”的基底
        base = mod_16QAM_rrc(self.data_cfo, t_vec, Fs,
                             sym_rate=self.Rs, beta=self.beta,
                             rng=self.rng, sps=self.sps, span=self.span)

        # 事件包络(0..1),只在突发时非零
        env = np.zeros(N, dtype=np.float64)
        self._schedule_events(self.t, self.t + N/Fs, env)

        # 施加包络
        sig = base * env.astype(np.float64)

        # RMS 归一化
        p = np.sqrt(np.mean(np.abs(sig)**2) + 1e-12)
        if p > 0:
            sig *= (self.out_level / p)

        # 带外抑制 FIR(有状态卷积)
        if self.lpf_taps is not None:
            buf  = np.concatenate([self.lpf_state, sig.astype(np.complex64)])
            full = np.convolve(buf, self.lpf_taps, mode='full')
            sig  = full[len(self.lpf_taps)-1 : len(self.lpf_taps)-1 + N]
            self.lpf_state = buf[-(len(self.lpf_taps)-1):]

        # 统一移频
        if F_SHIFT_HZ != 0.0:
            sig *= np.exp(1j * 2*np.pi * np.clip(F_SHIFT_HZ, -0.45*Fs, 0.45*Fs) * t_vec)

        out[:] = sig.astype(np.complex64)
        self.t += N / Fs
        return N

 image

 Wi-Fi 20 MHz OFDM“忙业务”

# =============================================================================
# 名称
#   Wi-Fi 2.4 GHz “20 MHz OFDM 忙业务” 频谱占用近似(带 ON-OFF 业务门控,低旁瓣版)
#
# 生成的是什么信号?
#   - 固定信道(不跳频)的“Wi-Fi 类宽带占用”外观,用于干扰/占用检测的数据集合成。
#   - 用 16QAM + RRC 成形的宽带随机符号流来近似 20 MHz OFDM 的带宽与“丰满”度。
#   - 行为特征:
#       1) 数据流近似“持续帧 + 短 ACK 缝”(ACK 缝为浅凹并做长余弦斜坡,避免硬门控展宽)。
#       2) 每 102.4 ms 产生一个“信标”短突发(独立包络,OFF 时仍可保留以模拟 AP 信标)。
#       3) 业务 ON-OFF 门控:模拟 CSMA/CA 下“有时发、有时不发”的忙闲交替(非周期,区间随机)。
#   - 非真实 802.11:不包含 STF/LTF/导频/编码/CSMA 过程,仅复现“频域占用 + 时域节律”的宏观特征。
#
# 为什么不是跳频?
#   - Wi-Fi 在 2.4/5/6 GHz 使用固定信道(DSSS/CCK 或 OFDM),不做 FHSS。你若需要跳频,请改用 BT/BLE FHSS 合成块。
#
# 旁瓣控制策略
#   - 先成形(RRC,sps≥8,span≥12 符号)后移频,保证过渡带陡峭。
#   - ACK/信标边界做余弦渐入/出;ACK 采用“浅凹地板”(非硬 0),显著降低门控泄露。
#   - 可选的 Kaiser 窗 FIR 低通(砖墙化),把带外能量压到 ~−80 dB 级别(取决于 Fs 与参数)。
#   - 全流程线性(无软限幅),避免非线性再生旁瓣。
#
# 关键参数(全部在下方常量中设定,GUI 无需配置)
#   - SAMP_RATE:采样率。低 Fs 运行轻,带宽会按 sps 约束自动缩小;要“更宽”,升 Fs。
#   - TARGET_BW_HZ:目标带宽(如 20e6)。实际带宽≈(1+β)*R_s;本块会在“sps≥SPS_TARGET”前提下自适应 R_s。
#   - BETA、SPS_TARGET、RRC_SPAN_SYM:决定成形滚降与过采样品质。
#   - ACK_*、BEACON_*:ACK 凹槽与信标的节律与长度。
#   - ONOFF_*:业务门控的 ON/OFF 随机区间与渐入/出时长;OFF 时可仅保留信标。
#   - F_SHIFT_HZ:最终统一移频。例:USRP 设 2.440 GHz,要合成“信道 1 中心” → F_SHIFT_HZ=2.412e9-2.440e9。
#   - LPF_*:Kaiser 低通开关与指标;提高 LPF_ATTEN_DB 或减小 LPF_PASS_KEEP 可进一步压低带外。
#
# 建议可视化设置(GNU Radio QT GUI Sink / FFT)
#   - Window=Hann 或 Blackman;FFT Size≥4096;Average=20-50;关闭 Autoscale,手动 −120…0 dB。
#
# 典型用法
#   1) 将本文件作为 epy_block 加入流图,采样率用 SAMP_RATE。仿真接 Throttle,外发接 USRP Sink。
#   2) 需要 1/6/11 三信道并发:复制本块 3 次,分别设 F_SHIFT_HZ 到 2412/2437/2462 MHz 的相对偏移。
#   3) 需要“更断断续续”:增大 OFF_MS_RANGE 上限,或缩短 ON_MS_RANGE。
# =============================================================================

import numpy as np
from gnuradio import gr

# ====================== 固定参数(无需 GUI 配置) ======================
SAMP_RATE          = 1e6        # 采样率;升高可得到更宽的合成占用
CHANNEL            = 6          # 仅记录:1..13 → 2412+5*(ch-1) MHz
BETA               = 0.35       # RRC 滚降因子 β
TARGET_BW_HZ       = 20e6       # 目标“20 MHz”外观;实际带宽由 R_s 和 β 决定
SPS_TARGET         = 8          # 成形过采样比下限(≥8 有利于抑制泄露)
RRC_SPAN_SYM       = 12         # RRC 脉冲长度(符号数)

# ACK/信标:模拟“持续帧 + 短 ACK 缝 + 周期信标”
ACK_GAP_US         = 30.0       # 每个 ACK 凹槽名义宽度(微秒)
ACK_PERIOD_MS      = 1.2        # ACK 凹槽平均周期(毫秒)
ACK_DIP_DB         = 12.0       # ACK 凹槽深度(dB),越大越“浅凹”
ACK_EDGE_FRAC      = 0.6        # ACK 渐入/出相对凹槽宽度的比例
BEACON_INTERVAL_MS = 102.4      # 信标周期(100 TU)
BEACON_LEN_MS      = 0.5        # 信标突发长度(毫秒)

# 业务 ON-OFF 门控(OFF 时可仅保留信标,近似“空闲信道”)
ONOFF_ENABLE       = True
ON_MS_RANGE        = (2.0, 8.0)     # 每次“忙”持续时长范围(毫秒,均匀分布)
OFF_MS_RANGE       = (0.2, 15.0)    # 每次“闲”持续时长范围(毫秒,均匀分布)
ONOFF_EDGE_MS      = 1.0            # ON/OFF 渐入出时长(毫秒)

# 频率与电平
DATA_CFO_HZ        = 0.0        # 给数据流一个极小 CFO(可为 0)
F_SHIFT_HZ         = 0.0        # 最终统一移频(搬到目标中心)
OUT_LEVEL          = 0.5        # 线性电平目标(RMS 归一化)
SEED               = 1          # 随机种子(保证可复现)

# 带外抑制 FIR(Kaiser 低通,可选)
LPF_ENABLE         = True
LPF_ATTEN_DB       = 80.0       # 阻带衰减指标(dB)
LPF_PASS_KEEP      = 0.90       # 通过带截止=0.5*(1+β)*R_s*PASS_KEEP(略留余量防过削)
LPF_TRANS_FRAC     = 0.20       # 过渡带宽占目标带宽的比例(越小越陡,阶数越高)

# ====================== 滤波与调制工具 ======================
def rrc_taps(beta: float = 0.35, sps: int = 8, span: int = 12) -> np.ndarray:
    """
    生成 RRC(Root-Raised-Cosine)脉冲的离散抽头。
    参数:
      beta: RRC 滚降因子 β
      sps:  每符号采样点数(过采样比)
      span: 脉冲跨越的符号数(总长=span*sps+1)
    返回:
      实系数 1D float64 数组,能量归一化(∑h^2=1)
    """
    N = span * sps
    t = np.arange(-N/2, N/2 + 1, dtype=np.float64) / sps
    taps = np.zeros_like(t, dtype=np.float64)
    for i, x in enumerate(t):
        ax = abs(x)
        if np.isclose(x, 0.0):
            taps[i] = 1.0 - beta + 4*beta/np.pi
        elif np.isclose(ax, 1.0/(4*beta)):
            taps[i] = (beta/np.sqrt(2.0)) * (
                (1 + 2/np.pi) * np.sin(np.pi/(4*beta)) +
                (1 - 2/np.pi) * np.cos(np.pi/(4*beta))
            )
        else:
            num = (np.sin(np.pi * x * (1 - beta))
                   + 4*beta*x*np.cos(np.pi * x * (1 + beta)))
            den = np.pi * x * (1 - (4*beta*x)**2)
            taps[i] = num / den
    taps /= np.sqrt(np.sum(taps**2) + 1e-15)
    return taps

def fir_lpf_kaiser(fs: float, f_pass: float, f_trans: float, atten_db: float) -> np.ndarray:
    """
    Kaiser 窗低通 FIR 设计(实系数)。
    参数:
      fs:       采样率
      f_pass:   通带截止频率(Hz)
      f_trans:  过渡带宽(Hz)
      atten_db: 阻带衰减(dB)
    返回:
      FIR 抽头,单位增益(∑h=1)
    """
    dw = 2*np.pi * (f_trans / fs)                   # 归一化过渡宽度
    if atten_db > 50:
        beta = 0.1102*(atten_db - 8.7)
    elif atten_db >= 21:
        beta = 0.5842*(atten_db - 21)**0.4 + 0.07886*(atten_db - 21)
    else:
        beta = 0.0
    M = int(np.ceil((atten_db - 8.0) / (2.285 * dw))) | 1   # 阶数(取奇数)
    n = np.arange(M) - (M-1)/2
    # 理想低通 sinc,注意 np.sinc(x)=sin(pi x)/(pi x)
    h = 2*(f_pass/fs) * np.sinc(2*f_pass*n/fs)
    w = np.kaiser(M, beta)
    taps = h * w
    taps /= np.sum(taps)                                   # 单位增益
    return taps.astype(np.float64)

def mod_16QAM_rrc(freq: float, t: np.ndarray, samp_rate: float, sym_rate: float,
                  beta: float, rng: np.random.Generator, sps: int, span: int) -> np.ndarray:
    """
    16QAM 基带符号 → RRC 成形上采样 → 频移到 freq,保持跨块相位连续。
    参数:
      freq:      基带频移(Hz),常用于注入轻微 CFO
      t:         绝对时间戳数组(长度=输出长度)
      samp_rate: 采样率
      sym_rate:  符号率 R_s
      beta:      RRC 滚降
      rng:       随机源
      sps/span:  成形参数
    返回:
      complex64 基带信号段(与 t 等长)
    """
    N = len(t)
    if N == 0:
        return np.zeros(0, np.complex64)
    n_sym = int(np.ceil(N / sps)) + span            # 尾部冗余,避免截断泄露
    bits = rng.integers(0, 2, size=(n_sym, 4), dtype=np.int8)
    I = (2*bits[:,0] + bits[:,1]) - 1.5
    Q = (2*bits[:,2] + bits[:,3]) - 1.5
    symbols = (I + 1j*Q) / np.sqrt(10)              # 单位平均功率
    up = np.zeros(n_sym * sps, dtype=np.complex128)
    up[::sps] = symbols
    taps = rrc_taps(beta=beta, sps=sps, span=span)
    shaped = np.convolve(up, taps, mode="same")[:N]
    # 先成形后移频;相位随 t 自然连续
    return (shaped * np.exp(1j * 2*np.pi * freq * t)).astype(np.complex64)

# ====================== 辅助:门控与地板 ======================
def _apply_fade(mask: np.ndarray, edge_samp: int) -> np.ndarray:
    """
    对 0/1 掩码边缘做余弦渐入/出,返回 [0,1] 浮点包络。
    edge_samp:渐变样本数(越大越平滑,旁瓣越低)。
    """
    if edge_samp <= 1:
        return mask.astype(np.float64)
    N = len(mask)
    out = mask.astype(np.float64).copy()

    # 上升沿
    up = np.flatnonzero(np.diff(np.pad(mask.astype(np.int8),(1,0)))==1)
    for i0 in up:
        i1 = min(N, i0 + edge_samp)
        ramp = 0.5*(1 - np.cos(np.linspace(0, np.pi, i1-i0)))
        out[i0:i1] *= ramp

    # 下降沿
    dn = np.flatnonzero(np.diff(np.pad(mask.astype(np.int8),(0,1)))==-1)
    for j1 in dn:
        j0 = max(0, j1 - edge_samp)
        ramp = 0.5*(1 - np.cos(np.linspace(np.pi, 0, j1-j0)))
        out[j0:j1] *= ramp

    return out

def _with_floor(env01: np.ndarray, floor_amp: float) -> np.ndarray:
    """
    将 [0,1] 包络映射到 [floor_amp, 1],用于“浅凹”(非硬 0)。
    floor_amp=10^(-D/20),如 D=12 dB → floor≈0.25。
    """
    f = float(np.clip(floor_amp, 0.0, 1.0))
    return f + (1.0 - f) * env01

# ====================== epy_block 实现 ======================
class blk(gr.sync_block):
    """
    WiFi20MHzBusyApprox_ONOFF
    - 输出:complex64 基带信号,包含数据宽带占用、短 ACK 凹槽、周期信标,以及 ON-OFF 业务门控。
    - 采样率与带宽会在“保证 sps≥SPS_TARGET”的前提下自适应,低 Fs 时自动缩小实际带宽。
    """
    def __init__(self):
        gr.sync_block.__init__(
            self, name="WiFi20MHzBusyApprox_ONOFF",
            in_sig=None, out_sig=[np.complex64],
        )
        # —— 基本配置与速率自适应 ——
        self.samp_rate = float(SAMP_RATE)
        self.beta = float(BETA)
        self.rng = np.random.default_rng(SEED)
        self.t = 0.0

        Rs_need = TARGET_BW_HZ / (1.0 + self.beta)     # 期望 R_s
        Rs_sps  = self.samp_rate / float(SPS_TARGET)   # sps 约束下的最大 R_s
        self.Rs = min(Rs_need, Rs_sps)
        self.sps = max(SPS_TARGET, int(round(self.samp_rate / self.Rs)))
        self.Rs  = self.samp_rate / self.sps           # 回写保证 sps 精确
        self.span = int(RRC_SPAN_SYM)

        # —— 元数据记录(不直接改变频移) ——
        self.channel = int(CHANNEL)
        self.fc_MHz  = 2412 + 5*(self.channel-1)

        # —— 时序参数(ACK/信标) ——
        self.ack_gap     = float(ACK_GAP_US) * 1e-6
        self.ack_period  = float(ACK_PERIOD_MS) * 1e-3
        self.beacon_T    = float(BEACON_INTERVAL_MS) * 1e-3
        self.beacon_len  = float(BEACON_LEN_MS) * 1e-3
        self.next_ack    = 0.5*self.ack_period
        self.next_beacon = self.beacon_T
        self.data_cfo    = float(DATA_CFO_HZ)
        self.f_shift     = float(F_SHIFT_HZ)
        self.out_level   = float(OUT_LEVEL)

        # —— 渐变与浅凹设置 ——
        self.edge_ack    = max(int(self.ack_gap * self.samp_rate * ACK_EDGE_FRAC),
                               self.sps * self.span)
        self.edge_beacon = max(int(self.beacon_len * self.samp_rate * 0.3),
                               self.sps * self.span)
        self.ack_floor   = 10**(-float(ACK_DIP_DB)/20.0)

        # —— ON-OFF 业务门控(OFF 时可仅保留信标) ——
        self.onoff_enable = bool(ONOFF_ENABLE)
        self.onoff_edge   = int(max(1, ONOFF_EDGE_MS * 1e-3 * self.samp_rate))
        self.onoff_state  = 1  # 1=ON, 0=OFF
        # 下一次翻转的绝对时间戳(初始化为“从当前时刻起”的一段 ON)
        self.onoff_next   = self.t + self.rng.uniform(*ON_MS_RANGE)*1e-3

        # —— 带外抑制 FIR(Kaiser)状态 ——
        self.lpf_taps  = None
        self.lpf_state = np.zeros(0, np.complex64)
        if LPF_ENABLE:
            tgt_bw   = (1.0 + self.beta) * self.Rs
            f_pass   = 0.5 * tgt_bw * float(LPF_PASS_KEEP)
            f_trans  = max(200.0, float(LPF_TRANS_FRAC) * tgt_bw)
            self.lpf_taps  = fir_lpf_kaiser(self.samp_rate, f_pass, f_trans, LPF_ATTEN_DB)
            self.lpf_state = np.zeros(len(self.lpf_taps)-1, np.complex64)

    # —— 生成 ACK/信标/ON-OFF 包络(返回 0..1 的浮点包络) ——
    def _make_masks(self, t0: float, N: int):
        Fs = self.samp_rate
        t  = t0 + np.arange(N)/Fs

        # ACK 凹槽:置 0,再做渐变并映射为浅凹
        on = np.ones(N, dtype=np.int8)
        i = 0
        while self.next_ack < t[-1] + 1e-12:
            g0 = self.next_ack
            g1 = g0 + max(10e-6, self.ack_gap * (0.8 + 0.4*self.rng.random()))
            self.next_ack += max(0.2e-3, self.ack_period * (0.6 + 0.8*self.rng.random()))
            i0 = int(max(0, np.floor((g0 - t0)*Fs)))
            i1 = int(min(N, np.ceil((g1 - t0)*Fs)))
            if i1 > i0:
                on[i0:i1] = 0
            i += 1
            if i > 64:  # 防止极端情况下过多事件
                break

        # 信标:独立短突发
        beacon = np.zeros(N, dtype=np.int8)
        j = 0
        while self.next_beacon < t[-1] + 1e-12:
            b0 = self.next_beacon
            b1 = b0 + self.beacon_len
            self.next_beacon += self.beacon_T
            j0 = int(max(0, np.floor((b0 - t0)*Fs)))
            j1 = int(min(N, np.ceil((b1 - t0)*Fs)))
            if j1 > j0:
                beacon[j0:j1] = 1
            j += 1
            if j > 8:
                break

        # 业务 ON-OFF:仅作用在“数据”上,不屏蔽信标
        if self.onoff_enable:
            onoff = np.ones(N, dtype=np.int8)
            state = self.onoff_state
            next_flip = self.onoff_next
            cur = t0
            while True:
                seg_end = min(t[-1], next_flip)
                if state == 0:
                    k0 = int(max(0, np.floor((cur - t0)*Fs)))
                    k1 = int(min(N, np.ceil((seg_end - t0)*Fs)))
                    if k1 > k0:
                        onoff[k0:k1] = 0
                if seg_end >= t[-1] - 1e-12:
                    break
                # 翻转并抽下一段
                state = 1 - state
                cur = seg_end
                dur_ms = self.rng.uniform(*(ON_MS_RANGE if state==1 else OFF_MS_RANGE))
                next_flip = seg_end + dur_ms*1e-3
            # 更新到块末状态
            self.onoff_state = state
            self.onoff_next  = next_flip
            onoff_env = _apply_fade(onoff, self.onoff_edge)
        else:
            onoff_env = np.ones(N, dtype=np.float64)

        # ACK/信标平滑 + ACK 浅凹
        on_env = _apply_fade(on, max(self.edge_ack, 16))
        on_env = _with_floor(on_env, self.ack_floor)
        be_env = _apply_fade(beacon, max(self.edge_beacon, 16))
        return on_env, be_env, onoff_env

    # —— 主工作函数:生成一段基带并施加包络、带外抑制、频移 ——
    def work(self, input_items, output_items):
        out = output_items[0]
        N = len(out)
        if N == 0:
            return 0

        Fs = self.samp_rate
        t  = self.t + np.arange(N)/Fs

        # 生成两路宽带占用(数据与信标),相位随 t 连续
        sig_data = mod_16QAM_rrc(DATA_CFO_HZ, t, Fs,
                                 sym_rate=self.Rs, beta=self.beta,
                                 rng=self.rng, sps=self.sps, span=self.span)
        sig_beac = mod_16QAM_rrc(0.0, t, Fs,
                                 sym_rate=self.Rs, beta=self.beta,
                                 rng=self.rng, sps=self.sps, span=self.span)

        # 包络:数据受 ACK 与 ON-OFF 共同约束;信标仅受自身包络
        on_env, be_env, onoff_env = self._make_masks(self.t, N)
        sig = sig_data * (on_env * onoff_env) + 0.6 * sig_beac * be_env

        # 线性电平(RMS 归一化到 OUT_LEVEL)
        sig *= self.out_level / (np.sqrt(np.mean(np.abs(sig)**2) + 1e-12))

        # 可选:带外抑制 FIR(Kaiser),简单的“有状态卷积”
        if LPF_ENABLE and self.lpf_taps is not None:
            buf  = np.concatenate([self.lpf_state, sig.astype(np.complex64)])
            full = np.convolve(buf, self.lpf_taps, mode='full')
            sig  = full[len(self.lpf_taps)-1 : len(self.lpf_taps)-1 + N]
            self.lpf_state = buf[-(len(self.lpf_taps)-1):]

        # 统一移频到目标中心(限制在 ±0.45*Fs 内避免混叠)
        if F_SHIFT_HZ != 0.0:
            sig *= np.exp(1j * 2*np.pi * np.clip(F_SHIFT_HZ, -0.45*Fs, 0.45*Fs) * t)

        out[:] = sig.astype(np.complex64)
        self.t += N / Fs
        return N

 image

 Zigbee[IEEE 802.15.4](2.4 GHz)

# =============================================================================
# 名称
#   Zigbee / IEEE 802.15.4 (2.4 GHz) 近似:QPSK+RRC 窄带占用 + 成组短帧(非跳频)
#
# 生成的是什么信号?
#   - 固定信道(默认不跳频)的 Zigbee 类外观:窄而干净的 ~2 MHz 带宽占用;
#   - 时序:短帧、成组 burst,组与组之间随机间隔(类似 CSMA/CA 业务突发);
#   - 物理近似:用 QPSK + RRC 成形代替 O-QPSK DSSS(2 Mchip/s,净 250 kb/s),仅复现频域宽度与突发节律。
#
# 频道映射(记录用途)
#   - 2.4 GHz 有 16 个信道:CH=11..26,中心频率 f_c(MHz)=2405 + 5*(CH-11)
#   - 本块不直接设绝对射频频点;通过 F_BASE_HZ 与 CH_NUM 计算“基带内中心”,再用 F_SHIFT_HZ 做统一平移。
#
# 旁瓣控制
#   - 先成形(RRC,sps≥8,span≥12)后移频;突发边缘余弦渐入/出;可选 Kaiser 低通压带外。
#
# 使用
#   1) 作为 epy_block 接到 Throttle(仿真)或 USRP Sink(外发),采样率用 SAMP_RATE;
#   2) 设 CH_NUM 选择频道;如需把频道中心搬到你的射频中心,设 F_SHIFT_HZ(例:USRP=2.440 GHz,
#      要发 CH15=2425 MHz → F_SHIFT_HZ=2.425e9-2.440e9);或直接调 USRP 载频;
#   3) 想切换频道:把 CH_SWITCH_ENABLE=True(默认 False),频点按 5 MHz 栅格在组间切换。
# =============================================================================

import numpy as np
from gnuradio import gr

# ====================== 固定参数(无需 GUI 配置) ======================
SAMP_RATE            = 1e6        # 采样率;低 Fs 下会自动缩窄带宽以保证 sps≥8
BETA                 = 0.35       # RRC 滚降
TARGET_BW_HZ         = 2.0e6      # 目标“~2 MHz”占用(实际≈(1+β)*R_s,自适应)
SPS_TARGET           = 8          # 成形过采样下限;≥8 抑制泄露更稳
RRC_SPAN_SYM         = 12         # RRC 脉冲跨越符号数

# —— 频道设置 ——
CH_NUM               = 15         # 11..26(记录与混频用)
F_BASE_HZ            = 0.0        # 基带内“CH11”中心相对 0 Hz 的偏移;通常为 0
F_GRID_HZ            = 5.0e6      # 频道间隔(5 MHz)
CH_SWITCH_ENABLE     = False      # True=按组在频道间切换(默认关闭)
CH_SWITCH_PROB       = 0.25       # 每组结束后切换频道的概率
CH_ALLOWED           = list(range(11, 27))  # 允许的频道集合(11..26)

# —— 成组短帧时序(单位 ms) ——
GROUP_GAP_MS_RANGE   = (20.0, 120.0)   # 组与组之间的空闲
FRAMES_PER_GROUP     = (3, 8)          # 每组帧数(含起止)
FRAME_LEN_MS_RANGE   = (0.4, 1.2)      # 单帧持续时间
INTER_FRAME_MS_RANGE = (0.2, 2.0)      # 帧间隔
EDGE_MS              = 0.20            # 每帧渐入/出时长(每侧)
JITTER_CFO_HZ        = 0.0             # 可选极小 CFO 抖动(0 关闭)

# —— 统一移频与电平 ——
F_SHIFT_HZ           = 0.0             # 把整体搬到目标中心(限制在 ±0.45·Fs)
OUT_LEVEL            = 0.5             # 固定线性增益(不做按块 RMS)

# —— 带外抑制 FIR(Kaiser 低通,可选) ——
LPF_ENABLE           = True
LPF_ATTEN_DB         = 80.0
LPF_PASS_KEEP        = 0.90            # 通带截止=0.5*(1+β)*R_s*PASS_KEEP
LPF_TRANS_FRAC       = 0.25            # 过渡带宽占目标带宽的比例

SEED                 = 26

# ====================== 滤波与调制工具 ======================
def rrc_taps(beta=0.35, sps=8, span=12):
    N = span * sps
    t = np.arange(-N/2, N/2 + 1, dtype=np.float64) / sps
    taps = np.zeros_like(t, dtype=np.float64)
    for i, x in enumerate(t):
        ax = abs(x)
        if np.isclose(x, 0.0):
            taps[i] = 1.0 - beta + 4*beta/np.pi
        elif np.isclose(ax, 1.0/(4*beta)):
            taps[i] = (beta/np.sqrt(2.0)) * (
                (1 + 2/np.pi) * np.sin(np.pi/(4*beta)) +
                (1 - 2/np.pi) * np.cos(np.pi/(4*beta))
            )
        else:
            num = (np.sin(np.pi * x * (1 - beta))
                   + 4*beta*x*np.cos(np.pi * x * (1 + beta)))
            den = np.pi * x * (1 - (4*beta*x)**2)
            taps[i] = num / den
    taps /= np.sqrt(np.sum(taps**2) + 1e-15)
    return taps

def fir_lpf_kaiser(fs, f_pass, f_trans, atten_db):
    dw = 2*np.pi * (f_trans / fs)
    if atten_db > 50:
        beta = 0.1102*(atten_db - 8.7)
    elif atten_db >= 21:
        beta = 0.5842*(atten_db - 21)**0.4 + 0.07886*(atten_db - 21)
    else:
        beta = 0.0
    M = int(np.ceil((atten_db - 8.0) / (2.285 * dw))) | 1
    n = np.arange(M) - (M-1)/2
    h = 2*(f_pass/fs) * np.sinc(2*f_pass*n/fs)
    w = np.kaiser(M, beta)
    taps = h * w
    taps /= np.sum(taps)
    return taps.astype(np.float64)

def mod_qpsk_rrc(freq, t, samp_rate, sym_rate, beta, rng, sps, span):
    """
    QPSK + RRC:生成与 t 等长的基带段,先成形后移频;相位随 t 连续。
    """
    N = len(t)
    if N == 0:
        return np.zeros(0, np.complex64)
    n_sym = int(np.ceil(N / sps)) + span
    # 均匀随机符号,Gray 映射等价:±1/±1
    bits = rng.integers(0, 2, size=(n_sym, 2), dtype=np.int8)
    I = 2*bits[:,0] - 1
    Q = 2*bits[:,1] - 1
    symbols = (I + 1j*Q) / np.sqrt(2)
    up = np.zeros(n_sym * sps, dtype=np.complex128)
    up[::sps] = symbols
    taps = rrc_taps(beta=beta, sps=sps, span=span)
    shaped = np.convolve(up, taps, mode="same")[:N]
    return (shaped * np.exp(1j * 2*np.pi * freq * t)).astype(np.complex64)

# ====================== 突发包络工具 ======================
def _add_burst(env, start, end, fs, edge_samp, amp=1.0):
    """在 env 上叠加一个 [start,end) 的突发,边缘余弦渐入/出,峰值 amp。"""
    if end <= start:
        return
    N = len(env)
    i0 = int(np.floor(start * fs))
    i1 = int(np.ceil(end   * fs))
    if i1 <= 0 or i0 >= N:
        return
    i0c = max(0, i0); i1c = min(N, i1)
    L = i1c - i0c
    if L <= 0:
        return
    e = int(edge_samp)
    if 2*e >= L:
        w = 0.5*(1 - np.cos(2*np.pi*np.arange(L)/(L-1 if L>1 else 1)))
    else:
        w = np.ones(L, dtype=np.float64)
        w[:e]  = 0.5*(1 - np.cos(np.linspace(0, np.pi, e, endpoint=False)))
        w[-e:] = 0.5*(1 - np.cos(np.linspace(np.pi, 0, e, endpoint=False)))
    env[i0c:i1c] = np.maximum(env[i0c:i1c], amp*np.clip(w,0,1))

# ====================== epy_block 实现 ======================
class blk(gr.sync_block):
    """
    Zigbee_802154_QPSK_Approx
    输出:complex64 基带;窄带占用 + 成组短帧;默认不跳频。
    """
    def __init__(self):
        gr.sync_block.__init__(
            self, name="Zigbee_802154_QPSK_Approx",
            in_sig=None, out_sig=[np.complex64],
        )
        self.fs  = float(SAMP_RATE)
        self.beta= float(BETA)
        self.rng = np.random.default_rng(SEED)
        self.t   = 0.0

        # —— 符号率自适应(保证 sps≥SPS_TARGET) ——
        Rs_need = TARGET_BW_HZ / (1.0 + self.beta)
        Rs_sps  = self.fs / float(SPS_TARGET)
        self.Rs = min(Rs_need, Rs_sps)
        self.sps = max(SPS_TARGET, int(round(self.fs / self.Rs)))
        self.Rs  = self.fs / self.sps
        self.span= int(RRC_SPAN_SYM)

        # —— 频道中心(基带内) ——
        self.ch = int(CH_NUM)
        self.f_base = float(F_BASE_HZ)
        self.f_grid = float(F_GRID_HZ)
        self.f_center = self._center_of(self.ch)   # 基带内中心
        self.f_shift  = float(np.clip(F_SHIFT_HZ, -0.45*self.fs, 0.45*self.fs))

        # —— 事件调度:成组短帧 ——
        self.next_group = self._now() + self.rng.uniform(*GROUP_GAP_MS_RANGE)*1e-3

        # —— FIR 状态(可选带外抑制) ——
        self.lpf_taps  = None
        self.lpf_state = np.zeros(0, np.complex64)
        if LPF_ENABLE:
            tgt_bw  = (1.0 + self.beta) * self.Rs
            f_pass  = 0.5 * tgt_bw * float(LPF_PASS_KEEP)
            f_trans = max(200.0, float(LPF_TRANS_FRAC) * tgt_bw)
            self.lpf_taps  = fir_lpf_kaiser(self.fs, f_pass, f_trans, LPF_ATTEN_DB)
            self.lpf_state = np.zeros(len(self.lpf_taps)-1, np.complex64)

        # —— 固定线性增益(避免块间幅度跳步) ——
        self.fixed_gain = float(OUT_LEVEL)

        # —— 可选极小 CFO 抖动 ——
        self.cfo_rng = self.rng.random()  # 初始偏置
        self.cfo = float(JITTER_CFO_HZ)

    def _now(self):
        return self.t

    def _center_of(self, ch):
        # CH=11..26 映射到基带:F_BASE_HZ + (ch-11)*5 MHz
        return self.f_base + (int(ch) - 11) * self.f_grid

    def _schedule_group(self, t0, t1, env):
        """在 [t0,t1) 内把所有落入的‘组 + 帧’叠加到 env。"""
        Fs = self.fs
        edge = int(round(float(EDGE_MS)*1e-3*Fs))
        while self.next_group < t1 + 1e-12:
            g_start = self.next_group
            # 抽一组帧
            n_frames = self.rng.integers(FRAMES_PER_GROUP[0], FRAMES_PER_GROUP[1]+1)
            t_cur = g_start
            for _ in range(int(n_frames)):
                dur = self.rng.uniform(*FRAME_LEN_MS_RANGE) * 1e-3
                _add_burst(env, t_cur - t0, t_cur + dur - t0, Fs, edge, amp=1.0)
                gap = self.rng.uniform(*INTER_FRAME_MS_RANGE) * 1e-3
                t_cur += dur + gap
            # 下一组起始时间
            self.next_group = t_cur + self.rng.uniform(*GROUP_GAP_MS_RANGE) * 1e-3

            # 可选:组间切换频道(按概率)
            if CH_SWITCH_ENABLE and (self.rng.random() < float(CH_SWITCH_PROB)):
                ch_list = list(CH_ALLOWED)
                if int(self.ch) in ch_list and len(ch_list) > 1:
                    ch_list.remove(int(self.ch))
                self.ch = int(self.rng.choice(ch_list))
                self.f_center = self._center_of(self.ch)

    def work(self, input_items, output_items):
        out = output_items[0]
        N = len(out)
        if N == 0:
            return 0

        Fs = self.fs
        t_vec = self.t + np.arange(N)/Fs

        # 基底:QPSK+RRC;可叠加极小 CFO 抖动
        freq_cfo = self.cfo * (0.5 - (0.5 - self.cfo_rng))  # 常值;默认 0
        base = mod_qpsk_rrc(freq_cfo, t_vec, Fs,
                            sym_rate=self.Rs, beta=self.beta,
                            rng=self.rng, sps=self.sps, span=self.span)

        # 组/帧包络(0..1)
        env = np.zeros(N, dtype=np.float64)
        self._schedule_group(self.t, self.t + N/Fs, env)

        # 施加包络
        sig = base * env.astype(np.float64)

        # 可选带外抑制 FIR(有状态)
        if self.lpf_taps is not None:
            buf  = np.concatenate([self.lpf_state, sig.astype(np.complex64)])
            full = np.convolve(buf, self.lpf_taps, mode='full')
            sig  = full[len(self.lpf_taps)-1 : len(self.lpf_taps)-1 + N]
            self.lpf_state = buf[-(len(self.lpf_taps)-1):]

        # 统一混频到“频道中心 + 全局平移”处;跨块相位连续
        # 逐样相位增量
        dphi = 2*np.pi * (self.f_center + self.f_shift) / Fs
        # 用累加器保持相位连续
        if not hasattr(self, "mix_phase"):
            self.mix_phase = 0.0
        ph = self.mix_phase + dphi * np.arange(1, N+1)
        self.mix_phase = float(ph[-1] % (2*np.pi))
        sig *= np.exp(1j * ph).astype(np.complex64)

        # 固定线性增益
        sig *= self.fixed_gain

        out[:] = sig.astype(np.complex64)
        self.t += N / Fs
        return N

 image

 

posted @ 2025-10-17 20:34  拾一贰叁  阅读(20)  评论(0)    收藏  举报