用Python解析ADS-B:从1090MHz广播到航班数据
简介适用于Python的ADS-B工具源码包面向航空数据开发、飞行追踪及空域分析场景的Python工程师用于解析和处理ADS-B广播消息可作为学习协议解析或构建追踪应用的起点。项目处于开发初期核心功能精炼适合中低级开发者研究。资源包共49个文件压缩包体积仅55KB其中23个py文件构成核心代码与测试代码8个rst文档用于文档站点渲染5个txt文件涵盖依赖清单、更新日志及测试用ADS-B消息日志yml与makefile分别承担CI和构建配置。源码按标准Python项目组织包含核心模块、tests测试目录、examples示例及sbs相关子目录同时附带多份消息日志供调试比对可帮助读者理解消息格式与处理流程并在此基础上快速扩展自己的工具是深入ADS-B协议细节的便捷入口。已有1013人学习下载尤其适合需要快速上手ADS-B数据处理或二次开发的Python工程师参考。 上周末我在阳台试了一套新的接收设备天线随手绑在晾衣杆上屏幕上弹出一架三公里外航班的瞬间还是忍不住多看了两眼。位置、高度、速度、呼号十几秒刷新一次而这一切的起点不过是民航客机自动广播的一小段 1090MHz 数字信号。ADS-B 在航空圈不算新鲜但对大多数写 Python 的人来说它是一块很值得琢磨的数字协议积木。这篇文章就围绕一个叫 adsb 的 Python 工具包展开讲清楚怎么把空中的射频广播变成一行行可分析的数据以及这条链路上最容易翻车的地方。1. 先看清这条 1090MHz 广播里到底装着什么1.1 112bit 报文的数据布局ADS-BAutomatic Dependent Surveillance–Broadcast广播式自动相关监视的逻辑很简单飞机把自身的经度、纬度、高度、速度、呼号等信息打包成固定格式的数字报文用 1090MHz 频率周期性向外广播。任何一台能收到这个频段信号的接收机都能被“反向监视”这些飞机这也是 Flightradar24 这类平台的数据来源。报文本身是 Mode S 长报文的一种叫 extended squitter固定 112 bit。我用代码解析的时间长了习惯把注意力放在前几个关键字段上字段bit 位置说明DFDownlink Format0~4消息类型112bit 长报文通常是 17 或 18ICAO 地址9~32飞机的 24bit 唯一地址类似“数字车牌”TCType Code33~37报文类型1~4 是呼号4/19 是速度9~18 是位置ME 载荷33~88真正承载业务数据的 56bitCRC 校验位89~112校验用防错帧和干扰帧混进来TC 是解析的入口相当于目录页。TC 在 1 到 4说明这条报文是航班呼号TC 等于 4说明是空中速度TC 在 9 到 18说明是位置报文。只有先看懂 TC后面那句“这个报文要不要解析、怎么解析”才有答案。1.2 为什么 Python 特别适合干这件事ADS-B 报文本质上是二进制流而 Python 正好在处理二进制位操作上有非常直白的语法比如int(hex_str, 16)、(value offset) mask这种组合。相比 C 或 RustPython 的开发速度对个人接收项目太友好了拿一根几十块钱的接收棒配合一个解析库晚饭前就能跑通一个能实时显示附近飞机的脚本。再加上这些年航空数据社区沉淀了 pyModeS、readsB、tar1090 等一批成熟项目Python 在整个 ADS-B 生态里的定位更像是“胶水层”——把接收软件输出的帧流接进来解码出结构化数据再路由到数据库、地图或者统计数据。性能不是主要矛盾因为一架飞机大约每秒发几条报文一个繁忙机场上空同时出现的飞机数量也就几百架这个数据量对 Python 完全不是压力。2. adsb 工具包把“位操作”变成“对象属性”2.1 安装与最小解码示例adsb 这个工具包的定位很纯粹输入一条 hex 表示的 Mode S 帧输出一个解析好的 Python 对象。它不碰射频采集也不负责网络收包只做“帧到字段”这一层。安装很简单pip install adsb我当时跑通第一个示例大概只花了两分钟from adsb.decoder import Decoder dec Decoder() frame 8D40621D58C382D690C8AC2863A7 msg dec.decode(frame) print(msg.icao) # 飞机 24bit 地址 print(msg.downlink_format) # 下行格式正常是 17 print(msg.type_code) # 报文类型参考上文的分类如果你拿到的版本方法名略有差异不用慌直接在交互环境里dir(Decoder)看一眼就能找到对应入口。这类工具包的 API 变化不算频繁但不同分支的命名习惯确实不一样。输出里最有意思的是type_code。拿到 TC 之后再按 TC 的类别进一步取字段比如 TC 是 4 时读速度TC 是 11 时读位置这就避免了拿着位置报文的解析逻辑去处理呼号报文的尴尬。2.2 没有它时你要手动补的三层逻辑为什么需要这样一个工具包因为自己从零解析 ADS-B 报文至少躲不开三层麻烦。第一层是位提取。112bit 报文按字段切分先用 hex 转成整数然后靠移位和掩码抠出 ICAO、TC、高度、经纬度。这一步本身不难但全是机械活而且因为 56bit 的 ME 字段里各个子字段的位长并不规整写错一个偏移量整条报文就全错了。第二层是专业编码。高度字段用的是 Gray code格雷码而不是普通二进制位置字段用的是 CPRCompact Position Reporting紧致位置报告编码速度字段里还区分亚音速和超音速两种布局。这些编码规则单独看都不复杂但合在一起就是一套完整的协议新手从零啃起来容易丧失耐心。第三层是 CRC 校验。射频环境里干扰帧、叠帧到处都是不校验就把数据收进来数据库会被脏数据淹没。adsb 这类包会在内部完成 24bit CRC 计算省掉你实现“多项式除法取余”的工夫。有这三层背景再用工具包就会有一种“别人把路铺好了你只管开车”的顺畅感。3. 一条完整链路天线、接收棒、dump1090、Python3.1 硬件选型与天线摆放很多第一次碰 ADS-B 的读者容易高估硬件门槛。实际上一根支持 1090MHz 的 RTL-SDR 接收棒几十块钱级别加上一根简单的 1090MHz 天线就足够接收周边十几公里甚至更远的飞机广播。天线位置比天线价格重要得多。我第一次测试时把天线放在窗边只能收到两架飞机。后来把天线挪到阳台外侧靠近机场进近航路那侧之后同时在线十几架是常态。核心原则只有一条尽量减少金属遮挡让天线对空场的视野尽可能开阔。楼层高、朝向机场方向效果会明显不同。3.2 用 dump1090/readsb 把射频流转成协议帧接收棒拿到的是一堆 I/Q 采样数据要变成 ADS-B 报文通常是先用 dump1090 这类软件做解调。它负责把射频信号解出 1090MHz 上的 Mode S 脉冲再把脉冲解码成 hex 帧。在 Linux 上最简单的运行方式dump1090 --net --aggressive--net表示开启网络输出默认会把 JSON 数据推到 8080 端口把原始帧推到 30005 端口。--aggressive是开启更激进的多帧搜索能多解出一些弱信号条件下的帧代价是 CPU 占用和误帧率略有上升。如果想拿到最原始的 hex 帧给 adsb 用可以用dump1090 --raw这个模式会把解调出的原始 Mode S 帧直接打到标准输出一行一帧格式类似8D40621D58C382D690C8AC2863A7readsb 也是一个不错的选择它是 dump1090 的重写版资源占用更小在树莓派上跑更稳。3.3 adsb 接入数据流的完整代码拿到原始帧之后就能把 adsb 接进网络流里实时解码。举个 TCP 客户端示例import socket from adsb.decoder import Decoder dec Decoder() sock socket.create_connection((localhost, 30005)) buffer b while True: data sock.recv(4096) if not data: break buffer data while b\n in buffer: line, buffer buffer.split(b\n, 1) line line.decode(errorsignore).strip() # 不同接收软件的行格式略有差异取最后一个逗号后的 hex 部分 frame line.split(,)[-1] try: msg dec.decode(frame) if msg and msg.type_code in (4, 11): # 速度或位置报文 print(msg.icao, msg.latitude, msg.longitude, msg.altitude) except Exception: continue如果使用 30005 端口的 Beast 二进制协议就不能像上面这样按文本行切了需要先解析 Beast 帧头。不想处理二进制的话推荐直接开 JSON 输出走 8080 端口或者用 dump1090 的--raw管道回放给脚本。先跑通文本链路再考虑二进制协议压力会小很多。4. 手拆一条真实报文CPR 奇偶配对才是定位的关键4.1 报文拆位从 hex 到字段拿刚才那条示例帧8D40621D58C382D690C8AC2863A7来说开头两个字符8D转成 8bit 二进制就是10001101前五位10001对应十进制 17也就是 DF17说明这是一条 112bit 的 extended squitter 报文。ICAO 地址是第 9 到第 32 位对应 hex 字符串里的40621D。TC 在第 33 到第 37 位58的二进制是01011000取第 33 到 37 位得到01011即十进制的 11。TC11 表示机载位置报文那这条报文的 ME 字段里装的很可能就是经纬度和气压高度。这种逐位拆解正是解析库在后台替你干的事。用 adsb 的时候你可以不看这些细节但理解了拆位逻辑出了问题至少知道去哪里排查。4.2 为啥单条报文解不出完整经纬度ADS-B 位置报文的重点和难点都在 CPR 编码上。CPR 的设计目标是少用比特位传递高精度位置它把全球经纬度划分成很多大小不同的“条带”再在条带内部编码相对位置。问题是单条报文只包含“模糊”的位置信息必须把相邻时间内的奇编号报文和偶编号报文配对才能算出一个无歧义的全球坐标。所以实际使用中经常出现这样一幕第一条位置报文解析出来的经纬度完全不对第二条报文一到两条一配对位置瞬间跳到正确的坐标上。这不是 bug而是 CPR 的工作方式。adsb 内部会维护一个奇偶报文配对的状态机所以我在 3.3 节的示例里强调要处理“数据流”而不是“单帧”——喂给它单独一条位置报文等于让它在信息不足的情况下强行输出结果自然不可靠。4.3 高度和速度字段的隐藏细节位置报文里的高度字段用的是 Gray code常见分辨率是 25 英尺。解码时先做 Gray code 到二进制的转换再乘以 25。有一点容易忽略这个高度一般是气压高度和 GNSS 几何高度不一定一致。如果你在对比不同数据源的高度看到几十米的差异不必惊讶。速度报文TC4里的信息包括地速、航迹角、垂直速率但它的位布局要么是亚音速格式要么是超音速格式超音速格式的地速单位和步进都不一样。解析库一般会根据标志位自动选择布局但自己做二次开发时建议保留原始字段方便回溯。呼号报文就更直白了8 个字符的航班呼号按 6bit 字符编码不足 8 位补空格。这里容易出现的坑是大小写和前后置空格入库前记得strip()。5. 实测两周后我认为最该避开的四个坑5.1 CRC 校验不过的帧果断扔射频环境里噪声、多径反射、信号叠帧都会产生错误帧。我会在解码前先跑一遍 CRC校验不过的直接丢弃。不要试图“纠正”错误帧——ADS-B 的 24bit CRC 只能检测错误本身不携带纠错信息硬着头皮解析只会污染数据。用 adsb 时留意解码结果里是否包含校验状态字段。如果工具包默认不筛你自己也得加一层过滤。实测下来在信号一般的环境下无效帧比例可以轻松超过三成不处理的话热力图和轨迹统计全都会失真。5.2 第一个坐标经常“漂到另一个半球”这是 CPR 配对的经典现象。新飞机第一次进入视野时如果只收到一条位置报文解析库可能给出一个完全离谱的坐标比如把一架在广州上空的飞机解到地中海去。等到奇偶报文配对成功坐标才恢复正常。我的处理策略很简单位置报文至少连续收到两帧且帧间隔小于 3 秒才开始记录轨迹。宁可少记一帧也别把假坐标写进数据库。5.3 匿名 ICAO 会让航迹断开有些私人飞机的驾驶员会开启隐私模式应答机会随机生成一个临时 ICAO 地址过一段时间再换一个。从地面角度看就像这架飞机的“车牌号”突然变了航迹自然断了重新再续一段。在做长时跟踪统计时遇到这种断线不要归咎于接收故障。如果数据里出现大量短命 ICAO多半就是匿名应答机。对于爱好者项目直接用原 ICAO 分桶统计就行不需要专门去猜真实身份。5.4 时间戳比你想的更值钱接收软件输出的帧里通常没有精确到毫秒的时间戳需要自己打。很多初学者只存“飞机到哪了”不存“什么时候到”等到想算速度、画轨迹、对比历史时才发现缺了时间轴只能重跑采集。我后来统一用收到帧的本地时间加微秒级时间戳入库并且把时间源校正到 NTP。单机接收下不需要达到 PTP 纳秒级精度但一分钟漂移好几秒的系统时钟会让航迹动画明显卡顿。5.5 接收棒过载问题容易被忽视RTL-SDR 是宽开接收机前端没有针对 1090MHz 的滤波附近如果有强广播、手机基站信号会出现过载表现是屏幕上大量无效帧和错误 ICAO。如果你家楼顶信号源密集花几十块加一个 1090MHz SAW 滤波器无效帧比例立刻降一个量级。没有滤波器之前我一度以为是天线方向不对来回折腾了两天最后才发现是过载导致的问题。调试这类小细节的时候养成把原始 hex 帧留一份存档的习惯。后面无论是复现 bug、测试新解析版本还是分享给社区排查原始帧都比加工过的数据可靠得多。我后来把接收脚本跑成了常驻服务数据推入 SQLite再用 Web 地图前端展示附近飞机的实时位置。整个项目最有成就感的部分不是最后的可视化而是搞清楚每一条报文从哪里来、为什么能变成屏幕上那个移动的小点。这个链路跑通之后你会发现天空其实没有想象中那么远。本文还有配套的精品资源点击获取