Docker + GNU Radio + USRP B210 实战:WiFi 前导码 IQ 采集环境搭建全流程
完整可复用的容器化采集环境:UHD + GNU Radio 3.10 + 三个源码编译的 OOT 模块(gr-foo / gr-ieee802-11 / gr-rftap),支持 USB 直通、固件自动上传、X11 GUI 转发与中断可续的采集台账。面向射频指纹(RFF)数据集构建场景。

〇、环境要求
硬件 : USRP B210(USB 3.0 接口)
宿主系统 : Ubuntu 20.04+(带图形界面)
必要软件 : Docker、Docker Compose、X11
下游用途 : Wi-Fi 前导码 IQ 采集 → 射频指纹数据集
同型号设备射频指纹的现实规模 (供参考,来自 SMoRFFI 数据集):123 台同型号 IEEE 802.11g 设备、3,542 万条前导码 IQ 采样、185 万条 RF 特征,随机森林基线识别率 89.06%。同型号意味着 OUI 相同,个体区分只能依赖硬件缺陷在波形上的痕迹。

一、目录结构
wifi-iq-capture/
├── Dockerfile # UHD + GNU Radio + GUI 依赖 + OOT 模块编译
├── docker-compose.yml # USB 直通 / host 网络 / GUI 转发 / 卷挂载
├── start_capture.sh # 启动脚本(交互式 + --rebuild/--no-rebuild)
├── init_usrp_via_container.sh # USRP 固件初始化(带重试)
├── check_usrp_permission.py # 容器内 USRP 三态诊断
├── gnuradio_prefs.conf # GR 虚拟环形缓冲配置
├── udev/
│ └── 90-usrp.rules # 宿主 udev 规则(Ettus 2500 / NI 3923)
├── data/
│ └── mac_address.csv # 采集台账(MAC, 已采集帧数)
└── src/
├── gr-foo # 自定义块集合(依赖)
├── gr-ieee802-11-maint-3.10 # 802.11 a/p/g 收发链(二次开发)
└── gr-rftap-maint-3.10_202509m # RFtap 元数据封装(二次开发)
模块名后缀的含义(关键):
| 后缀 | 含义 |
|---|---|
maint-3.10 |
只与 GNU Radio 3.10 的 ABI 兼容 |
_202509m |
二次开发时间戳标记(2025-09) |
GNU Radio 的 OOT 模块通过 Python 绑定 + C++ 共享库加载,ABI 不匹配会直接崩,不是"功能异常"。 所以整个 Dockerfile 里最重要的一行是版本锁定。

二、Dockerfile 逐段解析
2.1 基础环境与版本锁定
dockerfile
FROM ubuntu:22.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y software-properties-common && \
add-apt-repository -y ppa:gnuradio/gnuradio-releases && \
apt-get update && apt-get install -y \
gnuradio=3.10.* \
gnuradio-dev=3.10.* \
uhd-host \
libuhd-dev \
python3 \
python3-pip \
python3-uhd \
gr-osmosdr \
net-tools \
iputils-ping \
libsndfile-dev \
dbus-x11 \
libgtk-3-0 \
usbutils \
udev \
git \
doxygen \
python3-gi \
gir1.2-gtk-3.0 \
build-essential \
cmake && \
apt-get clean && rm -rf /var/lib/apt/lists/*
(1)gnuradio=3.10.* / gnuradio-dev=3.10.* 是全局最关键的一行。 官方源的 GR 版本与 OOT 要求的 maint-3.10 分支不一定一致,必须显式锁定。注意注释与实际版本要保持同步 (原文件注释写的是 3.9,实际装 3.10,容易误导后来者)。
(2)四个"与信号处理无关"的包是 GRC 能否启动的关键:
dockerfile
dbus-x11 libgtk-3-0 python3-gi gir1.2-gtk-3.0
(3)收尾清理 :apt-get clean && rm -rf /var/lib/apt/lists/* 必须与 install 在同一个 RUN 里,否则缓存会永久留在镜像层。
(4)建议补充:构建期版本自检。
dockerfile
RUN gnuradio-config-info --version && \
uhd_config_info --version && \
ldconfig -p | grep -c gnuradio
让镜像在构建日志里自己"报版本",避免"我到底装了什么"靠猜。
2.2 下载 USRP 固件镜像
dockerfile
RUN uhd_images_downloader
这一行解决的是"USRP 在容器里找不到"的第一大根因。 B210 上电后只运行 USB bootloader,必须由 UHD 推送 FX3 固件 + FPGA bitstream:
上电 → 以 bootloader PID 枚举 → UHD 下载固件 → 设备复位重新枚举 → 正常运行 PID
优化:不带参数会下载全部 UHD 设备镜像(1--2 GB)。只玩 B210 时:
dockerfile
RUN uhd_images_downloader -t b2
(不同 UHD 版本参数名有差异,先用 uhd_images_downloader --help 确认。)
把固件下载放在构建期 ,采集现场就完全不需要网络 ------外场实验的关键。

2.3 编译三个 OOT 模块
dockerfile
COPY src/ /app/src/
WORKDIR /app/src/gr-foo
RUN rm -rf build && mkdir build && cd build && \
cmake .. && make -j$(nproc) && make install && ldconfig
WORKDIR /app/src/gr-ieee802-11-maint-3.10
RUN rm -rf build && mkdir build && cd build && \
cmake .. && make -j$(nproc) && make install && ldconfig
WORKDIR /app/src/gr-rftap-maint-3.10_202509m
RUN rm -rf build && mkdir build && cd build && \
cmake .. && make -j$(nproc) && make install && ldconfig
WORKDIR /app
CMD ["/bin/bash"]
要点:
| 要点 | 说明 |
|---|---|
| 顺序 | gr-foo 是依赖,必须先装;后两个模块的 CMake 会去找它 |
make install && ldconfig |
缺 ldconfig → import 报 libxxx.so: cannot open shared object file(源码编译 OOT 最高频报错) |
-j$(nproc) |
低内存机器(云主机/WSL)会 OOM,且报错与内存无关;失败时改 -j2 重试 |
| 层膨胀 | 构建完的 build/ 会留在镜像层,三个模块可能白占几百 MB |
层膨胀的改进写法(同层清理):
dockerfile
RUN cd /app/src/gr-foo && rm -rf build && mkdir -p build && cd build && \
cmake .. && make -j$(nproc) && make install && ldconfig && \
cd .. && rm -rf build
多阶段构建更彻底,但 GR OOT 的 Python 绑定路径与 ldconfig 缓存跨阶段复制麻烦,建议先用同层清理这个低风险方案。

三、docker-compose.yml 逐项说明
yaml
version: "3.8"
services:
wifi-capture:
build: .
container_name: wifi-capture
privileged: true
devices:
- /dev/bus/usb:/dev/bus/usb
network_mode: "host"
environment:
- DISPLAY=$DISPLAY
volumes:
- ./data:/data
- /tmp/.X11-unix:/tmp/.X11-unix
- ./gnuradio_prefs.conf:/root/.gnuradio/prefs.conf
- ./root/data:/root/data
stdin_open: true
tty: true
| 配置 | 作用 | 注意事项 |
|---|---|---|
privileged: true |
规避各类权限问题 | 安全代价大,见 §9 |
devices: /dev/bus/usb |
USB 设备节点直通 | 启动时快照,见 §6.2 |
network_mode: "host" |
UHD 设备发现依赖广播;免端口映射 | 与宿主共享网络栈,端口冲突风险 |
DISPLAY + /tmp/.X11-unix |
GUI 转发到宿主 X server | 需宿主 xhost 授权 |
gnuradio_prefs.conf |
固定 GR 虚拟环形缓冲类型 | 见 §8 |
./data 与 ./root/data |
两个落盘位置 | ⚠️ 容易找不到数据,见 §7 |
stdin_open + tty |
保持交互式 shell | docker compose run 必需 |
3.1 关于 gnuradio_prefs.conf
ini
gr.vmcircbuf_default_type = gr_vmcircbuf_sysv_shm
GNU Radio 流图运行时在模块间传递数据流,底层用虚拟环形缓冲(VM circular buffer) ,有多种后端实现(SysV 共享内存、mmap 等)。容器受限环境里默认后端可能不可用,直接后果是流图起不来或一跑就崩。 把它固定为 SysV 共享内存是容器化 GR 的实用保底设置。
配置来源唯一化 :文件既然通过卷挂载进容器,启动脚本就不应该再写它。否则两个来源互相覆盖,出现"改了不生效"或"重启后被覆盖回去"的现象。
3.2 关于 network_mode: "host"
UHD 的设备发现依赖网络广播。换成默认 bridge 网络,最常见的结果就是"设备明明插着,就是找不到"。
- 单机、临时、交互式采集 →
host简单可靠; - 多容器并行采集 → 必须改回 bridge,并精确配置多播与 UDP 端口。
四、宿主机前置配置
4.1 安装 udev 规则(必需)
bash
sudo cp udev/90-usrp.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules
sudo udevadm trigger
规则内容:
# Ettus USRP (e.g., B200/B210)
SUBSYSTEM=="usb", ATTR{idVendor}=="2500", MODE="0666", GROUP="plugdev"
# NI USRP (e.g., USRP-2901)
SUBSYSTEM=="usb", ATTR{idVendor}=="3923", MODE="0666", GROUP="plugdev"
两个 VID 都要写 :2500 = 原厂 Ettus,3923 = NI 版。同一块 B210 装的固件品牌不同,VID 就不同。
两个高频误区:
- 只
reload不trigger→ 对"规则生效前已插好"的设备无效。reload只是重新读规则,trigger才对现有设备重新应用。 - 最直观的验证 :
reload+trigger后把 USRP 拔下来重插一次 ,能排除 90% 的"权限配了却没生效"。

4.2 (可选)宿主安装 UHD 作为回退
bash
sudo apt install uhd-host
sudo uhd_images_downloader
注意 :这套工程的设计是由容器内的 UHD 完成固件上传 ,宿主不需要手动跑 uhd_find_devices。装宿主 UHD 只是为了多一条回退路径。
五、启动流程与脚本参数
5.1 启动脚本
bash
./start_capture.sh # 交互式:镜像已存在时询问是否重建
./start_capture.sh --rebuild # 强制重建镜像
./start_capture.sh --no-rebuild # 跳过重建
脚本内部的三段逻辑:
bash
# ① 自动探测 Compose 命令(V2 与 V1 语法不同)
if command -v docker compose &> /dev/null; then
DC_CMD="docker compose"
elif command -v docker-compose &> /dev/null; then
DC_CMD="docker-compose"
else
echo "[!] Neither 'docker compose' nor 'docker-compose' is available."
exit 1
fi
# ② X11 授权(成对出现!)
xhost +local:root
trap 'xhost -local:root' EXIT
# ③ 先清理旧容器
$DC_CMD down
trap 'xhost -local:root' EXIT 是必写项。 xhost +local:root 会持久化 写入 X server 的访问控制列表,脚本退出不会自动失效。只加不撤 = 在机器上留一个长期后门。
$DC_CMD down 在启动前执行:避免旧容器占着 USB。
启动脚本的完整流程:
探测 compose 命令 → xhost 授权 + trap → down 旧容器
→ 询问/决定是否重建镜像 → 初始化 USRP(失败即 exit 1)
→ $DC_CMD run --rm wifi-capture bash
注意最后一句用的是 run --rm(一次性容器) ,这意味着 compose 里的 container_name 对这条命令不完全适用(实际容器名由项目名派生)。因此不要在其他脚本里硬编码容器名去 docker exec ,改用 docker compose exec 或从 docker ps 动态取容器 ID。
5.2 USRP 初始化脚本(带重试)
bash
MAX_RETRY=3
DELAY_BETWEEN_RETRIES=3
# 判据:PID 为 3923:7814 = 固件已加载完成
is_usrp_ready() {
lsusb | grep -i "3923:7814" | grep -qi "USRP"
}
for i in $(seq 1 $MAX_RETRY); do
echo "[*] Checking for initialized USRP device... (attempt $i)"
if is_usrp_ready; then
echo "✅ USRP device is ready:"
print_usrp_info
exit 0
fi
echo "❌ USRP not initialized. Running container to upload firmware..."
$DC_CMD run --rm "$SERVICE_NAME" bash -c \
"uhd_find_devices 2>&1 | awk '/USB open failed/ {skip=1; next} \
/See the application notes/ {if(skip) {skip=0; next}} \
{skip=0; if (NF) print}' || true"
echo "[*] Waiting $DELAY_BETWEEN_RETRIES seconds for USB re-enumeration..."
sleep $DELAY_BETWEEN_RETRIES
done
echo "❌ Failed to initialize USRP after $MAX_RETRY attempts."
exit 1
三个关键设计:
(1)grep 3923:7814 判断的是"固件上传完成没有",不是"设备插没插"。 设备刚上电时 PID 是 bootloader 阶段的,与 7814 不同。
(2)用容器内的 UHD 触发固件上传。 让"固件来源"与"运行时环境"保持同一份,消除宿主/容器 UHD 版本不一致的隐患。
(3)输出必须过滤。 权限不足时 UHD 会打印十几行排查提示,会污染解析。那条 awk 做的事是"跳过从 USB open failed 到 See the application notes 之间的所有行":
awk
/USB open failed/ {skip=1; next} # 进入跳过状态
/See the application notes/ {if(skip) {skip=0; next}} # 退出跳过状态
{skip=0; if (NF) print} # 其余行照常输出(剔除空行)
(4)初始化失败则中止启动:
bash
./init_usrp_via_container.sh "$DC_CMD" "$IMAGE_NAME"
if [ $? -ne 0 ]; then
echo "❌ Failed to initialize USRP. Aborting container launch."
exit 1
fi
fail fast,不要把用户扔进一个设备坏掉的交互式 shell。
六、容器内操作流程
6.1 确认设备状态
bash
python3 check_usrp_permission.py
完整脚本:
python
import subprocess
def has_permission_and_presence():
try:
out = subprocess.check_output(
["uhd_find_devices"],
stderr=subprocess.STDOUT # 必须合并 stderr
).decode()
if "Device Address" in out:
print("✅ USRP is connected and accessible.")
else:
print("❌ No USRP device found.")
print("📎 Hint: check USB cable and udev rules.")
except subprocess.CalledProcessError as e:
output = e.output.decode()
if "USB open failed: insufficient permissions" in output:
print("⚠️ USRP detected but access is denied.")
print("👉 Ensure the host has the proper udev rules installed.")
print("🔁 Then restart the container to refresh USB mapping.")
print("💡 Alternatively, run `uhd_find_devices` once on the host to initialize the device.")
else:
print("❌ UHD error:\n", output)
if __name__ == "__main__":
has_permission_and_presence()
它区分三态并直接给出动作:
| 输出 | 含义 | 下一步 |
|---|---|---|
connected and accessible |
设备正常 | 继续 |
No USRP device found |
无设备 | 查 USB 线 / udev 规则 |
insufficient permissions |
有设备但无权限 | 装 udev 规则 + 重启容器 |
stderr=subprocess.STDOUT 是必需项------UHD 的错误信息走哪个流不稳定,不合并会漏掉关键判据。
6.2 关于 /dev/bus/usb 的"快照"语义
yaml
devices:
- /dev/bus/usb:/dev/bus/usb
这不是"传递 USB 能力",而是"把宿主当前 /dev/bus/usb 目录树绑定挂载进容器"。 bind mount 的挂载点在容器启动时确定:
容器启动时:设备节点还不存在(设备没插)
之后插上 :宿主内核创建新节点,但容器里那份目录树不会更新
正确顺序:先插设备 → 再启动容器。
顺序反了的补救:
bash
# ① 宿主上触发固件初始化
uhd_find_devices
# ② 重启容器(不能只 start,必须重新创建容器)
docker compose down && ./start_capture.sh
注意 :docker start 一个已存在的容器不会刷新 bind mount,必须 down 后重新 run。
6.3 启动 GRC 并选择采样通道
bash
gnuradio-companion
打开并运行:
/src/gr-ieee802-11-maint-3.10/example/wifi_rx.grc
采样通道选择:在对应模块中设置目标 Wi-Fi 信道(可通过路由器后台确认),然后启动 IQ 采样。
先验证 GUI 转发: 宿主执行 xhost +local:root(或直接用 ./start_capture.sh,它已内置授权 + 自动撤销)。
七、数据落盘方案
7.1 位置问题:/data 还是 /root/data?
compose 里挂了两条:
yaml
- ./data:/data
- ./root/data:/root/data
而文档只写"stored in the Docker container under the data directory"。后果 :往 /data 写能在仓库 data/ 找到;但如果流图里用相对路径 data/xxx.csv、而工作目录是 /root,就落到了 /root/data。两套目录、两份数据。
建议 :统一约定只用一个 (推荐绝对路径 /data),并在文档里写死。
推荐的目录规范:
/data/
├── raw/ # 原始 IQ(二进制)
│ └── <mac>/ # 每设备一个子目录
│ ├── frame_0001.cf64
│ └── ...
├── meta/ # 元数据(每帧一个 JSON)
│ └── <mac>/
│ └── frame_0001.json
└── ledger.csv # 采集台账
元数据 JSON 的最低字段要求:
json
{
"mac": "78:21:84:93:57:54",
"device_label": "dev_0007",
"fs": 20000000,
"center_freq": 2437000000,
"gain_db": 30,
"channel": 6,
"timestamp_utc": "2026-09-12T08:31:22.415Z",
"n_samples": 4096,
"dtype": "complex64",
"endianness": "little",
"operator": "op_01",
"notes": "L-LTF window"
}
没有元数据的 IQ 文件,半年后连自己都读不懂。 采样率、中心频率、增益、时间戳这四项缺一项,这批数据基本就废了。
7.2 格式问题:CSV 还是二进制?
先算量级:3,542 万条复数采样 = 7,084 万个 float32 ≈ 283 MB 二进制 ;写成 CSV 文本约 0.7--1.4 GB 。对数据集总量,CSV 的体积可以接受。
问题在采集瞬间的写盘速率:
| 场景 | 二进制 | CSV(文本) |
|---|---|---|
| B210 单通道 20 MS/s 连续流 | ~160 MB/s(SSD 可扛) | 约 1.6--2.2 GB/s 等效(根本跟不上) |
| 前导码短帧突发写入(每帧几百采样) | 无压力 | 勉强可行 |
结论:
| 场景 | 建议格式 |
|---|---|
| 只采前导码短帧 | CSV 可行,注意总容量与解析开销 |
| 连续流采集 / 长时间录波 | 必须二进制 :complex64(.cf64)或 SigMF |
GNU Radio 里的实现选择:
| 方式 | 说明 |
|---|---|
File Sink(binary 模式) |
最省事、最不容易错 |
ZMQ PUB → 独立进程落盘 |
解耦采样与写盘,抗 IO 抖动,适合长时间采集 |
| 自定义 Python block | 灵活但要自己保证实时性 |
另外注意 :仓库里 capture_writer.py 目前只有一行 # capture_writer placeholder------落盘逻辑需要自己实现 。建议把它定位为"后处理/命名/元数据打包"工具,而把实时写盘交给 GRC 的 File Sink。
八、采集台账(mac_address.csv)的规范与解析
8.1 台账格式
两列,第一列是目标设备 MAC,第二列是已采集帧数:
csv
78:21:84:93:57:54,1000
78:21:84:93:5e:ec,7
24:d7:eb:38:cd:5c,1000
10:6f:d9:d1:80:89,90
60:3e:5f:35:0f:39,0
第二列的三种状态构成"断点续采"的配额机制:
| 值 | 含义 |
|---|---|
0 |
尚未开始 |
1000 |
配额已满(该设备目标帧数) |
其他(如 7、90) |
采集中断,可续采 |
8.2 必须做的三个归一化(附可直接运行的解析脚本)
python
import json
from collections import Counter
def load_ledger(path):
rows = []
with open(path, newline='', encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line:
continue
parts = line.split(',')
mac = parts[0].strip().lower() # ★ 归一化 1:统一小写
cnt = int(parts[1]) if len(parts) > 1 and parts[1].strip() else 0
rows.append((mac, cnt))
return rows
def is_randomized(mac):
"""U/L 位(第一字节 bit1)为 1 → 本地管理地址,即 MAC 随机化"""
first = int(mac.split(':')[0], 16)
return bool(first & 0x02)
def report(path):
ledger = load_ledger(path)
macs = [m for m, _ in ledger]
print(f'记录数 : {len(ledger)}')
print(f'去重后 : {len(set(macs))}')
dups = [m for m, c in Counter(macs).items() if c > 1]
if dups:
print(f'⚠️ 重复 MAC : {dups}')
oui = Counter(':'.join(m.split(':')[:3]) for m in macs)
print('Top OUI :', oui.most_common(5))
rnd = [m for m in macs if is_randomized(m)]
print(f'随机化 MAC : {len(rnd)}/{len(macs)} = {100*len(rnd)/len(macs):.1f}%')
print(f'配额已满 : {sum(1 for _, c in ledger if c >= 1000)}')
print(f'未开始 : {sum(1 for _, c in ledger if c == 0)}')
print(f'部分完成 : {sum(1 for _, c in ledger if 0 < c < 1000)}')
# ★ 归一化 2:去重断言
assert len(macs) == len(set(macs)), '台账存在重复 MAC,请归一化后再用'
return ledger
ledger = report('data/mac_address.csv')
# ★ 归一化 3:把设备标签写成映射表,供后续数据集切分使用
label_map = {mac: i for i, (mac, _) in enumerate(ledger)}
with open('data/label_map.json', 'w', encoding='utf-8') as f:
json.dump(label_map, f, indent=2, ensure_ascii=False)
这个脚本暴露的三类问题(在真实台账里都出现过):
(1)MAC 大小写不一致 → 同一设备被当成两个类别。
台账里存在这样的两条记录:
80:B9:89:F0:2F:4A,0 ← 全大写
80:b9:89:f0:2f:4a,0 ← 全小写
还有大小写混杂的:
3e:ab:1d:C6:31:ab,0
如果不做 .lower() 归一化,这两条会被当成独立类别,直接污染标签空间。 上面的 assert 就是为了在加载阶段立刻拦住它。
(2)随机化 MAC → 标签会随时间漂移。
MAC 第一字节的 U/L 位 (bit 1)为 1 表示"本地管理地址",即设备自己生成的随机化 MAC(现代手机默认开启)。真实台账里这类条目约占 9%。
为什么这对 RFF 是硬伤 :随机化 MAC 会漂移,同一台设备不同时间可能对应多个 MAC。把 MAC 当分类标签,标签就不稳定。
两个处理方向:① 采集时关闭被测设备的 MAC 随机化(可控实验最省事);② 把标签体系从"MAC"改成"设备指纹"------用聚类自动发现个体,MAC 只做辅助校验。
(3)同型号设备的 OUI 相同 → 特征区分度是核心难点。
台账里两大主簇:
| OUI 前缀 | 条数 |
|---|---|
78:21:84:93:* |
83 |
24:d7:eb:38:* |
40 |
| 合计 | 123 |
这个 123 正好对应数据集论文里的 123 台同型号设备。 同型号 → OUI 完全相同 → 从 MAC 前三字节根本区分不出个体。
这正是射频指纹识别要解决的问题:唯一可用的信息是硬件缺陷在 IQ 波形上留下的差异。 也解释了为什么 3,542 万条采样对应的基线准确率是 89.06%------同型号识别是 RFF 里最难的任务。
8.3 并发写的风险
台账是增量维护 的(每攒够一批回写一次),好处是断点续采、类别均衡可控。但如果多路采集同时写同一个 CSV,后写的会覆盖先写的,导致丢更新。
建议:多路采集时改成"每设备一个文件"或加文件锁,最后再合并。
九、安全加固清单
这套环境的"能跑"依赖三处宽松配置。它们是可以、也应该被收紧的:
| 配置 | 风险 | 收紧方向 |
|---|---|---|
privileged: true |
几乎放弃全部容器隔离(可访问所有设备、改内核参数、挂载文件系统) | 先试只保留 devices: /dev/bus/usb + 宿主 udev 规则 (多数情况够用);GUI 不需要 privileged;确实需要时用 cap_add 最小能力集(如 SYS_NICE) |
MODE="0666" |
所有用户可读写 USB | 只保留 GROUP="plugdev" 并把用户加进组;桌面系统可用 TAG+="uaccess" |
xhost +local:root |
持久化授权,脚本退出不失效 | 用 trap 'xhost -local:root' EXIT 自动撤销 |
推荐的收紧流程:调试期全开 → 确认链路通 → 逐项删除 → 每删一项重跑一次 → 把最小可用集合写回 compose 文件。
十、常见故障排查表
| 现象 | 根因 | 判据 | 处理 |
|---|---|---|---|
lsusb 有设备,uhd_find_devices 无输出 |
固件未上传 | USB PID 不是 7814(处于 bootloader 阶段) |
触发 uhd_find_devices,等待 USB 重新枚举 |
报 USB open failed: insufficient permissions |
udev 权限不足 | 错误信息含关键字 | 装 udev 规则 + reload + trigger + 重启容器 |
| 宿主能看到,容器看不到 | bind mount 快照 | 设备是容器启动后插的 | down 后重新 run(start 无效) |
import gnuradio 崩 / 流图崩 |
GR/OOT ABI 不匹配 | gnuradio-config-info --version ≠ 模块分支 |
锁版本重建镜像 |
libxxx.so: cannot open shared object file |
编译后未刷新链接缓存 | --- | make install 后补 ldconfig |
| 编译中途"莫名失败"、报错与内存无关 | -j$(nproc) 导致 OOM |
dmesg 看 OOM killer |
改 -j2 重试 |
| GRC 启动报 GTK 相关错误 | 缺 GUI 依赖 | --- | 装 dbus-x11 libgtk-3-0 python3-gi gir1.2-gtk-3.0 |
| GRC 打不开显示 | X11 未转发 | DISPLAY 为空 |
宿主 xhost +local:root;SSH 场景先 ssh -X |
| 流图启动即崩 | 虚拟环形缓冲后端不可用 | --- | 固定 gr.vmcircbuf_default_type = gr_vmcircbuf_sysv_shm |
| 找不到输出的 CSV | 落盘位置不一致 | /data vs /root/data |
统一绝对路径,只保留一个挂载点 |
| 同一设备被分成两个类别 | MAC 大小写不一致 | 台账里有大小写不同的同一条 | .lower() 归一化 + 去重断言 |
docker exec wifi-capture 找不到容器 |
run --rm 生成的容器名与 container_name 不同 |
docker ps 查看实际名称 |
改用 docker compose exec 或动态取 ID |
| CSV 写盘跟不上、丢样本 | 文本格式 IO 瓶颈 | 采样率 × 2 × ~12 字节/值 | 改二进制 complex64 或 SigMF |
十一、参考文献
- Z. Guo, Z. Jia, J. Zhu, W. Huang, Y. Chen, "SMoRFFI: A Large-Scale Same-Model 2.4 GHz Wi-Fi Dataset and Reproducible Framework for RF Fingerprinting," arXiv:2511.07770, 2025(Computer Networks, 2026).
- Ettus Research / NI, UHD and USRP Manual :
uhd_find_devices、uhd_images_downloader、uhd_usrp_probe、udev 规则与权限说明. - GNU Radio Project, GNU Radio Manual 与 Out-of-Tree Module Development Guide(VM circular buffer 配置).
- bastibl, gr-foo:GNU Radio 自定义块集合.
- bastibl, gr-ieee802-11:IEEE 802.11 a/g/p Transceiver for GNU Radio.
- Docker Inc., Compose File Reference :
devices、network_mode、privileged、bind mount 语义. - IEEE Std 802.11-2020, Wireless LAN MAC and PHY Specifications(LTF 前导码与 MAC 地址结构).
十二、小结
| 模块 | 关键实现要点 |
|---|---|
| 镜像 | Ubuntu 22.04 + GR PPA + gnuradio=3.10.* 锁版本 + GUI 依赖四件套 |
| 固件 | 构建期 uhd_images_downloader(可用 -t b2 省体积),保证离线可用 |
| OOT 模块 | 顺序 gr-foo → gr-ieee802-11 → gr-rftap;每个都 make install && ldconfig;构建后同层清理 |
| 设备直通 | devices: /dev/bus/usb(启动时快照)+ 宿主 udev 规则(2500 / 3923) |
| 固件初始化 | 以 PID 3923:7814 为"固件已加载"判据 + 重试循环 + awk 过滤 UHD 噪音 |
| 诊断 | 三态识别(正常 / 无设备 / 权限不足),并给出下一步动作 |
| 安全 | privileged 尽量收紧、xhost 用 trap 成对撤销、MODE 改为组权限 |
| 数据 | 统一 /data 绝对路径;短帧 CSV 可行、连续流必须二进制;元数据 JSON 是刚需 |
| 台账 | MAC .lower() 归一化 + 去重断言;识别随机化 MAC;多路采集避免并发写 |
一句话总结这套环境的工程价值:把"环境问题"从每次采集都要重新面对的变量,变成了一个构建一次、处处一致的常量。 对需要跨设备(123 台)、跨场地、长时间采集的数据集项目来说,这不是优化,而是前提。