☰
基于Remote ID广播的无人机操控者定位搭建指南
2026/10/16 7:55:37 网站建设 项目流程

简介:该资源面向无人机安全研究、无线电信号分析及飞行器定位追踪方向的开发者,提供了一套基于 DJI OcuSync 2.0 / Drone-ID 协议的接收器实现方案。项目既支持通过 USRP B205-mini 等 SDR 设备进行实时接收,也支持对预先录制的捕获文件离线解析,核心代码覆盖 QPSK 解调、序列检测、数据包解码及地图定位展示,实测兼容 DJI Mini 2 与 Mavic Air 2 等机型,适合具备 Python 与无线通信基础的进阶学习者参考。资源包共 22 个文件,约 4.92MB,包含 12 个 Python 源码文件、4 张原理或效果图示、2 个采集样本及相关说明文档;其中实时接收、离线分析、频谱捕获、地图展示等脚本分工明确,配合 README 可快速理解整体调用关系。当前已有 1600 余人浏览学习,下载后可直接对照源码开展信号捕获实验、复现论文场景,或在此基础上扩展适配更多 DJI 机型与分析功能,是研究无人机空域安全与隐私追踪问题的一份实用工具包。

1. 先泼一盆冷水:要“追踪定位操控者”,别去解 OcuSync 2.0,接它旁边的公开广播

拿到需求时,“无人机 ID 接收器”和“追踪定位操控者位置”这两句话放在一起,谁都会先去打 DJI OcuSync 2.0 的主意:毕竟图传、遥控都走这条链路,操控者坐标理应也在里面。但真按这个思路立项,你会被一个硬事实挡住——OcuSync 2.0 是对外不公开的私有空口,普通硬件根本不具备加入其链路的资格,更别提从里面解出坐标。后来我们把方案改成了接收同设备上的另一路公开广播:符合 Remote ID(远程识别)规范的 DJI 机型,会通过 Wi-Fi 和蓝牙周期广播无人机 ID、无人机位置和操控者位置。这篇就把这条最小可行路线拆给你:从硬件选型到抓包,再到最关键的消息解析和现场排错,照着做可以复现。

2. Remote ID 广播与 OcuSync 2.0 的关系:信号为什么开放、操控者坐标藏在哪条消息里

2.1 OcuSync 2.0 是私有空口,Remote ID 是强制公开广播,别把两件事混成一件事

OcuSync 2.0 承担的是图传下行和遥控上行,链路本身做了私有协议封装和双向认证。你想做一个“接收器”去直接吃这套空口,第一步就卡在物理层接入:没有 DJI 授权,连握手都完不成。这不是软件写得好不好的问题,是协议根本不对外开放。所以标题里“适用于 DJI OcuSync 2.0”这句话,正确理解方式是:你面对的设备是采用 OcuSync 2.0 的机型,但你要接收的是这台设备上另一路按 Remote ID 规范广播的公开信号,而不是去逆向 OcuSync 本身。

这路公开广播是怎么来的?Remote ID 的出发点就是让地面上的任何人都能感知附近无人机的基本身份和位置,所以它必须用开放、明文的方式发出来。合规机型通常同时开两条链路:Wi-Fi(2.4GHz / 5GHz 的 Beacon 帧和 Service Discovery Frame)和蓝牙(BT 4.0 Legacy 广播)。DJI 这类厂商在硬件上把 Remote ID 广播模块和 OcuSync 模块做在同一套射频前端里,但数据面完全独立——飞行器把操作员坐标、自身坐标按标准格式组包,周期广播出去。你要做的不是“破解图传”,而是当一个普通的 Wi-Fi/蓝牙接收者,把公开广播收下来。

2.2 ASTM F3411 的消息集:一条一条拆开看,Operator Location 在 System 消息里

Remote ID 的消息内容统一在 ASTM F3411 规范里定义。这是一个公开规范,你不需要去猜厂商私有的东西。实际抓包中最常遇到的四类消息如下:

消息类型携带内容对追踪操控者的作用
Basic ID无人机序列号或登记号用于区分每一台无人机,避免多机混包
Location/Vector无人机实时经纬度、高度、速度、航向告诉你飞机在哪、往哪飞
System运营商注册号、操控者经纬度、区域半径操控者位置的关键来源
Operator ID操控者注册号用于关联到具体操作员身份

注意一个关键点:我们说的“追踪定位操控者”,本质是读取无人机广播里的 Operator Location 字段,而不是去扫描遥控器的射频信号。遥控器本身不会持续广播自身位置,是无人机在替它报位置。所以这套方案的定位要摆正:它回答的是“无人机告诉地面,它的操控者站在哪个经纬度”,适合用于飞手自检、场地低空管理、赛事安保这类有明确合法边界的场景,而不是去三角测向找遥控器。

坐标编码也是公开的:经纬度用 32 位有符号整数表示,单位是 1e-7 度,也就是说报文里存的 305000000 对应 30.5 度,负数表示南纬或西经。解析时不做符号处理,你就可能把东经 113 度解析成西经 -113 度,这是后文避坑章节最常见的问题之一。

2.3 接收路线怎么选:封闭专用一体机 vs 自建公开广播接收器

既然知道了信号是公开广播,接下来面临一个产品路线选择。市面上存在两类“无人机 ID 接收器”:一类是面向安防市场的专用一体化接收机,多通道、带云平台、开箱即用,但价格高、采购流程长、协议封闭,数据格式往往只能对接它自家平台;另一类就是我们这篇文章要讲的路线——用通用 Wi-Fi/蓝牙接收硬件加开源解析逻辑,自己搭一套 Remote ID 接收器。两条路对比:

对比项专用一体化接收机自建公开广播接收器
能否拿到操控者坐标能,且通常带可视化平台能,解析 System 消息即可
成本高,且往往需要资质流程低成本,一套单板机加网卡
协议开放性封闭,数据接口受限完全自主,想怎么接都行
部署灵活度固定式,布点位置受限可移动,几分钟换一个点
技术门槛无门槛,但不可定制需要会抓包、写脚本

我的建议是:如果你是高校实验室、赛事场馆、园区低空管理这类需要长期固定的场景,专用一体机省心;但如果你要验证一个区域能不能收到、要快速换点位、要把数据接进自己的管理系统,自建路线是唯一现实的选择,而且它能让你把协议细节吃透,后续换机型、调天线都有据可依。本文后续全部按照自建路线展开,所有命令和脚本都在 ARM Linux 单板机上跑通。

3. 硬件选型与现场布位:决定能不能收到的三个前置条件

3.1 最省钱的接收组合:ARM Linux 单板机 + 双频监听网卡 + 蓝牙备援

先给出我常用的基础组合,成本控制在几百元以内。核心是要有一块支持监听模式(Monitor Mode)的双频 USB 无线网卡,因为 Remote ID 的 Wi-Fi 广播可能落在 2.4GHz,也可能落在 5GHz,只有单频网卡会漏掉一半概率。主控用树莓派这类 ARM Linux 单板机就行,性能绰绰有余,毕竟只是抓包和轻量解析。

组件选型取向作用备注
主控树莓派或同类的 ARM Linux 单板机跑抓包脚本与解析程序内存 1GB 以上即可
Wi-Fi 网卡支持 Monitor Mode 的双频 USB 网卡,芯片组优先选 88x2bu 系列接收 Wi-Fi Beacon 与 SDF 帧单频网卡不建议
蓝牙适配器BT 4.0 USB Dongle接收蓝牙 Legacy 广播,作为备援链路并非所有机型都会发蓝牙
天线5dBi 全向天线,或 8dBi 定向天线提升弱信号下的接收成功率可拆卸天线优先
供电移动电源 5V/2A 或 POE 供电现场长时间运行野外布点必备

选 88x2bu 系网卡的原因是它在 Linux 下监听模式驱动最成熟,踩坑资料多,遇到问题你能搜到解决方案。其他芯片组不是不能用,但可能会在 5GHz 信道切换和 Beacon 帧接收上出各种兼容问题。蓝牙这条线我建议作为辅助验证手段,主力走 Wi-Fi,因为 Wi-Fi 帧里能同时拿到无人机坐标和操控者坐标,信息更完整。

3.2 把网卡推进监听模式:命令与三处易错参数

拿到网卡后,第一步是确认驱动加载和监听模式可用。以下命令在树莓派这类 Debian 系系统上直接执行:

# 查看 USB 网卡是否被识别 lsusb # 确认无线网卡接口名,通常是 wlan0 或 wlan1 iw dev # 进入监听模式(假设接口是 wlan0) sudo ip link set wlan0 down sudo iw dev wlan0 set type monitor sudo ip link set wlan0 up # 确认模式已切换成功,Mode 应为 Monitor iw dev wlan0 info

这里有三处参数容易翻车。第一,ip link set wlan0 down这步不能省,直接切监听模式会报command failed: Device or resource busy (-16)。第二,切换后必须重新ip link set wlan0 up,否则网卡是 down 状态,tcpdump 起来后一个包都抓不到。第三,有些系统里网卡名字可能是wlx001122334455这种 USB 网卡命名,不叫 wlan0,用iw dev确认后再替换命令里的接口名。

切换成功后,建议先用iw dev wlan0 set channel 36 HT20手动固定一个信道测试抓包,确认能收到 Beacon 帧后再进入真正的工作流程。不要一上来就扫全信道,Wireless 网卡同一时刻只能监听一个信道,全频段扫描会漏掉大量周期性广播。

3.3 布位不是越高越好:天线高度、极化与遮挡的实测取舍

很多第一次做接收点的人会把设备放到楼顶最高处,觉得越高视野越好。实测下来这不一定对。Remote ID 的 Wi-Fi 广播发射功率不高,接近于普通 Wi-Fi 终端的发射功率,而且广播源(无人机)在空中,接收点如果架得太高、周围没有反射面,反而可能错过低空路径上的信号。

我一般会按三个原则布位。第一,天线高度放在 1.5 到 3 米之间,和人站在地面巡视时手持设备的高度接近,这样无人机在低空飞行时的信号路径是一条较平坦的视线,不容易被楼体切掉。第二,全向天线要垂直放置,保持极化方向与无人机内置天线一致;很多无人机内置天线是垂直极化的,你把网卡天线水平放置,信号会直接掉 10dB 以上。第三,接收点不要放在飞行区域正下方,而是放在飞行区域边缘的 5 到 10 米处,让无人机广播时天线方向图的主瓣能覆盖到接收点,而不是让接收点对着机身天线盲区。

还有一个容易被忽略的因素:人体本身会吸收 Wi-Fi 信号。现场人员如果站在接收天线和飞行区域之间,信号衰减会非常明显。布点时把天线稍微抬高离开人群头部高度,并用一条 USB 延长线把网卡从天线上引出来,是成本最低、收益最明显的改动。

4. 最小可跑通方案:抓包、标定 OUI、解析出操控者坐标

4.1 先抓二十分钟底噪,再抓目标机器:用同一套命令同时完成

整个方案的数据链路是:网卡监听模式接收 Wi-Fi 管理帧,tcpdump 落盘为 pcap 文件,Python 脚本离线和在线解析。先给出抓包命令:

# 在监听模式下抓取 Beacon 帧和 Action 帧(SDF 属于 Action 帧) sudo tcpdump -i wlan0 -e -nn "wlan[0] == 0x80 or wlan[0] == 0xd0" -w remote_id_bg.pcap # 抓满 20 分钟后 Ctrl+C 结束,得到底噪样本;然后打开无人机, # 再用同样命令抓 5 分钟,得到含 Remote ID 广播的样本 sudo tcpdump -i wlan0 -e -nn "wlan[0] == 0x80 or wlan[0] == 0xd0" -w remote_id_target.pcap

过滤表达式里wlan[0] == 0x80匹配 Beacon 帧,wlan[0] == 0xd0匹配 Action 帧。Remote ID 在 Wi-Fi 上有两种承载方式:一种直接放在 Beacon 帧的厂商自定义信息元素里,另一种放在 Service Discovery Frame(SDF)中,而 SDF 的管理帧类型正是 Action。-e参数让 tcpdump 打印链路层地址,方便你确认发送端 MAC。注意监听模式只能同时听一个信道,如果不知道目标无人机广播在哪个信道,先花 5 分钟按 5GHz 主流信道循环切换一遍,观察哪个信道出现持续数据,再固定那个信道正式抓包。

抓包时长有个经验值:底噪样本抓满 20 分钟,目的是把场馆内所有正常的 Wi-Fi 设备厂商 OUI 都采集全,后面标定 Remote ID 的 OUI 时需要和这份名单做差集。目标样本抓 5 分钟足够,Remote ID 广播周期通常在 1 秒左右,5 分钟能收集几百条消息。

4.2 从底噪和样本里标定 Remote ID 的 OUI:一份可直接跑的 Python 脚本

拿到两份 pcap 后,第一件事不是解析 Remote ID,而是先搞清楚“该过滤什么”。Remote ID 的厂商自定义信息元素(Vendor Specific IE)有固定 OUI,但不同厂商、不同固件版本的 OUI 可能不同,直接道听途说写死一个值最容易翻车。正确做法是用脚本统计两份 pcap 中所有出现过的 OUI 及其出现频率,对比后找出“目标样本里新增且频繁出现”的那个 OUI。

#!/usr/bin/env python3 """统计 pcap 中 Beacon 帧 Vendor Specific IE 的 OUI 出现频率,用于标定 Remote ID OUI。""" from scapy.all import rdpcap, Dot11Beacon, Dot11Elt from collections import Counter def collect_oui(pcap_file: str) -> Counter: count = Counter() for pkt in rdpcap(pcap_file): if not pkt.haslayer(Dot11Beacon): continue elt = pkt.getlayer(Dot11Elt) while isinstance(elt, Dot11Elt): # 0xDD 是 Vendor Specific IE 的 Tag 号 if elt.ID == 0xDD and len(elt.info) >= 3: count[elt.info[:3].hex()] += 1 elt = elt.payload return count if __name__ == "__main__": bg = collect_oui("remote_id_bg.pcap") target = collect_oui("remote_id_target.pcap") print("底噪样本 Top OUI:") for oui, n in bg.most_common(10): print(f" {oui} {n}") print("目标样本新增 OUI:") for oui, n in (target - bg).most_common(10): print(f" {oui} {n}")

脚本逻辑很简单:逐包遍历 Beacon 帧,取出所有 Tag 号为 0xDD 的厂商自定义信息元素,截取前 3 字节作为 OUI 统计。target - bg利用 Counter 的差集运算,直接找出目标样本里新增的 OUI。这些新增的 OUI 里,出现频率最高的那个大概率就是 Remote ID 广播,因为普通 Wi-Fi 设备的 Beacon 是持续存在的,不会因为某一台无人机开机而产生全新 OUI。

拿到这个 OUI 后,把它记下来。这里有个血泪经验:不要直接用一次抓包的结论当永久配置,不同固件版本的机型可能换 OUI,换一台机器后重新做一次标定,整个过程只要三分钟。

4.3 解析 System 消息并输出操控者经纬度:核心脚本与字段说明

标定出 OUI 后,核心的解析脚本就登场了。下面的脚本读取 pcap,过滤 Beacon 帧的厂商特定 IE,提取 Remote ID 消息本体,解析 System 消息里的操控者经纬度,并输出 GeoJSON 方便直接画到地图上。

#!/usr/bin/env python3 """从 pcap 中提取 Remote ID 广播,解析操控者经纬度并输出 GeoJSON。""" import json from scapy.all import rdpcap, Dot11Beacon, Dot11Elt REMOTE_ID_OUI = "xxxxxx" # 用 4.2 脚本标定出的 OUI 替换 # F3411 消息类型 MSG_SYSTEM = 0x2 def parse_system_message(msg_body: bytes) -> dict: """解析 System 消息体,提取操控者经纬度。 消息体字段顺序遵循 ASTM F3411 的公开字段定义: 偏移 0-3:Operator Latitude,int32,1e-7 度 偏移 4-7:Operator Longitude,int32,1e-7 度 """ if len(msg_body) < 8: return None lat = int.from_bytes(msg_body[0:4], byteorder="big", signed=True) / 1e7 lon = int.from_bytes(msg_body[4:8], byteorder="big", signed=True) / 1e7 # 合理性校验,防止把非 System 消息误解析成坐标 if not (-90 <= lat <= 90 and -180 <= lon <= 180): return None return {"operator_lat": round(lat, 7), "operator_lon": round(lon, 7)} def parse_pcap(pcap_file: str): features = [] for pkt in rdpcap(pcap_file): if not pkt.haslayer(Dot11Beacon): continue elt = pkt.getlayer(Dot11Elt) while isinstance(elt, Dot11Elt): if elt.ID == 0xDD and len(elt.info) >= 3 and elt.info[:3].hex() == REMOTE_ID_OUI: payload = elt.info[3:] # 去掉 OUI,剩余是消息体 if payload and (payload[0] & 0x0F) == MSG_SYSTEM: loc = parse_system_message(payload[1:]) # payload[0] 是消息头 if loc: features.append({ "type": "Feature", "geometry": {"type": "Point", "coordinates": [loc["operator_lon"], loc["operator_lat"]]}, "properties": {"drone_mac": pkt.addr2, "ts": float(pkt.time)} }) elt = elt.payload return features if __name__ == "__main__": feats = parse_pcap("remote_id_target.pcap") geojson = {"type": "FeatureCollection", "features": feats} with open("operator_locations.json", "w", encoding="utf-8") as f: json.dump(geojson, f, ensure_ascii=False, indent=2) print(f"解析出 {len(feats)} 条操控者坐标,已写入 operator_locations.json")

这段代码里有两个参数需要说明。第一,消息类型判断用的是payload[0] & 0x0F,F3411 消息头的低 4 位是消息类型,高 4 位是协议版本,做这个掩码操作是为了避免把版本号当成类型。第二,/ 1e7是把整数经纬度还原成十进制度数,signed=True保证南纬西经的负值能正确解析,如果你漏了 signed 参数,南半球和西经地区的坐标会直接错乱成 180 度之外的非法值。

脚本里只解析了 System 消息,因为它是 Operator Location 的主要承载者。实际抓包中还会遇到 Basic ID、Location/Vector 消息,如果你需要同时输出无人机坐标和 ID 用于关联,可以在同一个循环里把所有消息类型都接出来,按第 6 章的合并思路做多字段关联。

4.4 坐标验证:拿手机 GPS 走一步便知真假

解析出来的坐标对不对,别急着接平台,先做一次现场验证。找一个开阔场地,让飞手站在一个用手机 GPS 能明确读出经纬度的位置(比如广场地砖的交叉点),起飞无人机,抓 2 分钟包,解析出 Operator Location,再对比飞手手机 GPS 读数。误差在 5 到 10 米内都是正常的,因为 1e-7 度的精度换算成地面距离大约是 1 厘米,误差主要来自无人机 GPS 模块本身的定位精度和广播时的坐标快照时刻。如果误差超过 100 米,大概率是你把无人机自身位置(Location/Vector 消息)误当成了操控者位置,回代码里检查消息类型。

这个验证步骤务必在正式部署前做掉,因为后续所有平台显示、区域告警都建立在这个坐标是可信的基础上。一次现场标定,能省掉后面十次线上排查。

5. 避坑:六次现场排查出的典型翻车记录

5.1 等十分钟一个有效包都没有:先确认固件区域模式,再确认信道

现象:设备全部就位,监听模式正常,tcpdump 跑起来也有 Beacon 帧在滚动,但用标定脚本对比后找不到任何新增 OUI,更别提 Remote ID 消息。

原因排查下来通常有两个方向。第一,无人机固件处于不支持 Remote ID 广播的区域或版本,比如国内购买的部分机型没有开放美标 Remote ID 广播;第二,网卡监听的信道不对,Remote ID 广播落在 5GHz 的某个 DFS 信道上,而你固定监听的是 36 信道。

解决:先在无人机配套设置里确认机型支持 Remote ID 广播,再让无人机开机、离地悬停,用信道循环命令逐个监听 5GHz 主流信道,观察哪个信道出现持续的管理帧流量。不要忽略 149 到 165 这个频段,有些机型会把广播放在这组信道上。信道不对时,抓再久也是白抓。

5.2 OUI 标定出一长串,分不清哪台是无人机

现象:底噪和目标样本做差集后,新增了七八个 OUI,频率都差不多,无法判断哪个是 Remote ID 广播。

原因:现场有其他无线设备在目标样本抓包期间被触发开机,比如工作人员的手机、场馆的无线麦克风接收器,它们同样新增了 Beacon 广播。单纯看频率已经分不出来了。

解决:把目标样本抓包时间缩短到 1 分钟,并在抓包时只让目标无人机开机,其他无线设备全部保持原状;更可靠的办法是让无人机在抓包中途起飞,起飞前后 OUI 频率会出现明显变化。频率变化最剧烈、且在飞行过程中持续出现的那个 OUI,才是 Remote ID 广播。

5.3 解析出来全是 0.0 或 192.xxx:坐标合理性校验不能省

现象:脚本能跑通,但输出的经纬度要么是 0.0,要么是 192.37 这种明显超出合法范围的数值。

原因:消息体偏移没对齐。不同厂商固件在 F3411 基础上可能加私有扩展字段,System 消息里 Operator Latitude 的实际偏移可能不是从消息体第 0 字节开始。你把厂商扩展字段当成了坐标数据,自然解析出乱值。

解决:先用 Wireshark 打开一条抓到的 Remote ID Beacon 帧,把厂商特定 IE 的原始字节逐字节列出来,先人工找到经纬度字段——特征是四个字节一组、数值范围在 -900000000 到 900000000 之间的整数。找到后修正脚本里的偏移量。这就是为什么 4.2 里强调先标定 OUI,确认 OUI 后还要再确认字段偏移,两件事都不能想当然。

5.4 飞机坐标有了,操控者坐标缺失

现象:Location/Vector 消息正常解析出无人机经纬度,但 System 消息迟迟不出现,或者解析出来的 Operator Location 永远是起飞点的值。

原因:ASTM F3411 对 Operator Location 的填充有状态依赖,部分实现里无人机只有在完成离场动作后才持续填充操控者坐标,悬停在地面调试时可能不发或发默认值。另外,如果飞手一直站在起飞点不动,坐标看起来像是“不更新”,其实只是确实没动。

解决:让无人机完成一次完整的起飞-巡航-降落流程,全程抓包;如果巡航阶段依然没有 System 消息,说明这个固件版本对 Operator Location 支持不完整,只能退而求其次用起飞点作为初始位置参考,并明确标注数据来源是起飞点而非实时操控者坐标。

5.5 坐标突然跳到几公里外

现象:操控者坐标大多数时候稳定,但每隔十几秒会跳出一个离当前位置几公里甚至几百公里的点。

原因:无人机 GPS 定位在信号遮挡时会产生跳变,比如在楼宇间穿行时接收到的卫星几何分布变差;也可能是因为收到了另一台无人机的广播,两台设备的广播混在同一信道里,而你的脚本没有按无人机 MAC 区分数据源。

解决:解析时以 Basic ID 或发送方 MAC 为维度分流,每个 MAC 维护独立的最近值;同时对坐标做“跳变抑制”,当新坐标与前一帧坐标距离超过 200 米且时间间隔小于 2 秒时,判定为跳点丢弃。Remote ID 广播本身没有强校验,这些合理性过滤必须在接收端自己做。

5.6 隔一棵树信号就断

现象:飞行器绕到接收点侧后方、中间隔了一排行道树后,Remote ID 广播消失,等飞出来又恢复。

原因:Remote ID 广播用的是普通 Wi-Fi 发射功率,5GHz 信号对树叶遮挡极其敏感,衰减比 2.4GHz 严重得多;而飞行器机体本身也会遮挡天线,当机身挡在广播天线和接收点之间时,信号会有超过 20dB 的衰减。

解决:优先用双频采集,把 2.4GHz 和 5GHz 都收进来,其中一路断了另一路还在;更有效的是按 3.3 的方案增加一个接收点,两个点布在飞行区域的对角线方向,当机体姿态遮挡了一个点时,另一个点大概率还保持着视线路径。

6. 从一个点覆盖到稳定追踪:多接收点合并与监管平台接入的两步进阶

6.1 用两个接收点做数据合并,绕开单点非视距断流

单点接收最大的局限是:只要飞行器姿态或环境遮挡挡住信号路径,整条数据链就断了。解决思路是部署两个接收点,分别抓包后把解析结果合并。合并逻辑不复杂,但要处理好时间对齐和去重。

#!/usr/bin/env python3 """合并两个接收点的操控者坐标事件,按无人机 MAC 和 2 秒时间窗去重。""" import json, time def load_events(path): with open(path, encoding="utf-8") as f: geojson = json.load(f) return [ { "drone_mac": feat["properties"]["drone_mac"], "ts": feat["properties"]["ts"], "op_lon": feat["geometry"]["coordinates"][0], "op_lat": feat["geometry"]["coordinates"][1], } for feat in geojson["features"] ] def merge_events(events_a, events_b, window=2.0): combined = sorted(events_a + events_b, key=lambda e: e["ts"]) last_seen = {} # drone_mac -> 最近一次事件时间 merged = [] for ev in combined: mac = ev["drone_mac"] if mac in last_seen and ev["ts"] - last_seen[mac] < window: continue # 两个接收点收到同一次广播,只保留先到的一条 last_seen[mac] = ev["ts"] merged.append(ev) return merged if __name__ == "__main__": events = merge_events( load_events("point_a_parsed.json"), load_events("point_b_parsed.json"), ) print(f"合并后有效事件 {len(events)} 条")

合并逻辑里,window=2.0是去重时间窗,取 2 秒是因为 Remote ID 广播周期通常为 1 秒,两个接收点收到同一条广播的时间差不会超过 1 秒,2 秒窗口足够去重又不会误删正常的不同时刻事件。实际操作时,两个接收点的系统时间要先做 NTP 同步,否则时间戳错位会导致去重失效,这是多站点方案最容易忽略的基础设施问题。

6.2 把坐标流转成标准事件,推给监管页面或低空管理平台

解析和合并做完后,最终落地是把坐标变成可消费的事件流。最省事的方式是输出 GeoJSON 供离线画图,但做低空管理平台接入时,通常要转成轻量 JSON 事件,通过 HTTP 推给后端。推荐的最小事件字段如下:

字段类型说明
drone_idstring无人机 MAC 或序列号
drone_lat / drone_lonfloat无人机实时位置
operator_lat / operator_lonfloat操控者位置
tsint事件时间戳(Unix 秒)
sourcestring接收点标识,便于多站点溯源

推送代码很简单,核心是按周期把最新事件 POST 到平台接口:

import requests, json def push_event(endpoint: str, event: dict): try: resp = requests.post(endpoint, json=event, timeout=2) resp.raise_for_status() except requests.RequestException as exc: # 失败不阻塞本地解析,事件先落盘,后续补偿 with open("push_failed.jsonl", "a", encoding="utf-8") as f: f.write(json.dumps(event) + "\n") if __name__ == "__main__": event = { "drone_id": "aa:bb:cc:dd:ee:ff", "drone_lat": 30.1234567, "drone_lon": 120.1234567, "operator_lat": 30.1234000, "operator_lon": 120.1234000, "ts": int(time.time()), "source": "point_a", } push_event("http://your-low-altitude-platform.local/api/drone_events", event)

事件推送失败时不要阻塞本地解析,这是我做现场系统的一个习惯:接收端永远先保证本地数据完整,网络抖动导致的推送失败靠落盘补偿。低空管理平台通常要求 1 秒一次的事件频率,按这个节奏推即可;如果平台没有明确要求,宁可把推送频率降一点,也要保证每条事件的坐标是经过跳变抑制和去重后的干净数据,而不是原始的抓包噪声。

这套方案从硬件到解析再到平台对接,跑通之后你会形成一个新的工作习惯:每次去现场,第一件事永远是花二十分钟抓底噪标定 OUI,而不是直接抓目标。这个习惯救过我很多次——场馆的 Wi-Fi 设备一个月内就可能换一批,上次标定的 OUI 这次可能已经完全不一样了。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询