简介:adsb是一个面向Python开发者的ADS-B信号解析工具包,适合航空数据爱好者、无人机航迹分析或无线电解码相关项目。它基于SBS等格式的消息输出,提供ADSB消息处理的基础能力,目前项目处于开发初期,但接口设计简洁、依赖少,适合快速集成到现有Python项目中,可作为学习与二次开发的起点。资源共49个文件,压缩包仅55KB,以py源码为主(23个),辅以rst/sphinx文档(8个)、txt说明与依赖清单、测试脚本和示例配置,目录涵盖src、tests、examples、docs等模块,采用标准Python项目布局,源码与测试分离,便于阅读、修改与扩展。已有1016人学习浏览。安装后即可调用adsb模块,参考examples中的用例处理SBS消息,结合tests中的测试代码理解消息解析流程;docs下API文档帮助深入定制,并附带真实ADS-B消息日志样本可供验证,是入门ADS-B数据处理的轻量工具包。1. 从一次 SDR 抓包说起:Python 读 ADS-B 到底在解什么?把一根几十块的 USB 电视棒插到笔记本上,接上螺旋天线,打开终端跑几行 Python 脚本,屏幕上就开始列出周围飞机的呼号、高度、速度和经纬度。这个场景我第一次跑通时,有那种“给天空开了个后门”的错觉。这个名为adsb的 Python 工具包,就是专门干这个的:它把 1090MHz 上广播的 Mode S 报文解成结构化数据,让你不碰 FPGA 或硬件逻辑,也能实时跟踪空域里的飞机。它解决的核心问题是——把一条 112 微秒的二进制脉冲串翻译成人话。适合三类人:想玩航空数据的开发者、做空域监控原型的研究者、以及那些对着 Flightradar 好奇“它背后到底是什么”的爱好者。接下来我会按“信号原理 → 上手跑通 → 实战处理 → 排坑 → 进阶验证”的顺序,把这个工具包彻底拆开。2. 为什么先搞懂 Mode S 消息结构:解码前的三个基本事实2.1 1090MHz ES 广播不是你想的那种“数据包”ADS-B 的物理层很简单,本质是脉冲位置调制。飞机每秒钟广播若干次,每次广播持续 112 微秒,前 8 微秒是 preamble(同步头),后面 88 微秒是数据块,共 56 个 bit 位,每个 bit 占 1 微秒。这里有个反直觉的点:它没有“目的地址”和“源地址”这种类似 TCP/IP 的头部,所有听到这台接收机的目标都能解。这个 Python 工具包在解码时,第一步不是找飞机 ID,而是找 preamble 特征——拇指粗的高脉冲、低脉冲、高脉冲、低脉冲,间隔 1 微秒。找不到这个模式,后面全部免谈。Mode S 报文分短报文(56 bit)和长报文(112 bit)。真正携带位置和速度的 ADS-B 消息几乎都是长报文,DF 字段(Downlink Format)落在 17、18、19 上。工具包内部就是先切出 DF 值,再根据 DF 分发到不同解析器。理解了这一点,你就明白为什么有些开源软件会显示“DF17”而不显示航班号——因为 DF17 只是框架,里面还要按 Type 字段继续分。我用这个工具时,最常看到的是 Type 1-4(身份识别)、Type 9-18(空中位置)、Type 19(速度)。2.2 Python 工具包的解码流水线:从比特流到字典adsb工具包内部流程大致是四步:位同步 → 消息提取 → CRC 校验 → 字段映射。位同步靠检测 preamble,然后以 0.5 微秒为窗口采样每个 bit 的中心电压,判断高低电平。这里有一个硬件相关的点:如果你的 SDR 采样率不是 2MHz 的整数倍,位同步会引入采样相位偏移,工具包实测下来对 2MHz、4MHz、8MHz 都有补偿,但 2.4MHz 这种非标准采样率容易出问题。CRC 校验是重中之重。Mode S 的 CRC 用的是 24 位多项式,但 ADS-B 里叫“奇偶校验”其实不准确,它覆盖整个 112 bit,错误的判定是“余数非零”。工具包解码时如果 CRC 失败,会返回一条CRCError记录,而不是直接丢包——这个设计很实用,因为你可以在本地看误码率,判断天线位置是否该调。字段映射就是查表:把 bit 段按 ICAO 文档里的位偏移切成字段,比如飞机呼号是 6 个 6-bit 字符,编码用了特殊字符表,不是 ASCII。import adsb # 从 SDR 采集的原始 IQ 数据 buffer 里解出一条消息 raw_iq = open("capture.iq", "rb").read() decoder = adsb.Decoder(sample_rate=2_000_000) messages = decoder.feed(raw_iq) for msg in messages: if msg.is_valid: print(f"ICAO: {msg.icao:06X}, DF: {msg.df}, Type: {msg.type_code}")这段代码里,Decoder是核心类,sample_rate必须和你 SDR 实际采样率一致。feed()接收 IQ 样本,内部自动完成 preamble 检测和消息切分。msg.is_valid对应是否通过 CRC。如果采样率设置错误,你会看到大量is_valid=False的消息,或者 preamble 检测率极低。我第一次跑时就是把 2.4MHz 写成了 2MHz,一整天都在怀疑天线。2.3 选型理由:为什么这个工具比直接解析二进制更顺手很多老手习惯用 C 写的dump1090这类软件,但那是独立进程,要解析它的 JSON 输出。而这个 Python 工具包的价值在于:你可以在自己的采集循环里直接调用解码,拿到的是 Python 字典风格的消息对象,可以直接塞给pandas.DataFrame。这在做数据分析和实时过滤时优势巨大。另一个关键点是它保留了原始消息的raw字段,方便你做差分对比——这在排错时是后悔药。df = pd.DataFrame([ { "icao": f"{m.icao:06X}", "callsign": m.callsign, "lat": m.latitude, "lon": m.longitude, "alt": m.altitude, "speed": m.speed, "crc_ok": m.is_valid } for m in messages if m.is_valid ])这里m.callsign是工具包已经解好的字符串,m.latitude/longitude只在同时收到奇偶两帧并完成 CPR 解码后才非空。请注意,latitude和longitude在初始位置解码前可能是None,这是 ADS-B 的固有机制,不是 bug。我一般先过滤crc_ok,再过滤位置非空的条目,再做去重。3. 把工具跑起来:安装、数据源与第一个解码脚本3.1 安装依赖与准备测试文件这个工具包依赖numpy做位运算加速,如果要做可视化还需要matplotlib和folium。安装走普通pip流程即可,但建议用虚拟环境,因为numpy版本可能会和你其他项目冲突。数据源有两种:一是接 SDR 硬件实时抓 IQ,二是用预先录好的bin文件。对于刚上手的人,强烈建议先拿录制好的样本文件练手,因为实时流里你会同时遇到信噪比波动、干扰脉冲、和多架飞机混叠,新手很容易被带偏。准备样本文件时,我一般会自己录一段 30 秒的 IQ 数据:用 SDR 软件调谐到 1090MHz,采样率 2MHz,增益设到 35dB 左右,保存成原始 IQ 文件。如果你手头没有 SDR,也可以用工具包自带的generate_demo功能,它会生成一段模拟信号——这个功能适合测试安装是否正确,但不能用来练真实解码。python -m venv adsb-env source adsb-env/bin/activate pip install adsb numpy matplotlib folium这段命令里,adsb是工具包名,folium是可视化地图库。venv不是必须的,但我见过太多人因为全局环境里numpy版本不对,解码结果出现随机性错误——那些错误很难排查,最后发现是scipy依赖冲突。所以环境隔离不是洁癖,是省时间。3.2 解码第一个样本:理解输出对象拿到样本文件后,先跑一个最小脚本验证端到端链路。下面这段代码读入 IQ 文件,解码全部有效消息,统计消息类型分布。这里注意file_reader是工具包提供的一个便捷层,它会自动分块处理大文件,避免一次性把几 GB 的 IQ 读进内存。import adsb reader = adsb.FileReader("capture.bin", sample_rate=2_000_000) stats = {"total": 0, "valid": 0, "callsign": 0, "position": 0} for msg in reader: stats["total"] += 1 if msg.is_valid: stats["valid"] += 1 if msg.callsign: stats["callsign"] += 1 if msg.latitude is not None: stats["position"] += 1 print(f"检测到 {stats['total']} 条, 有效 {stats['valid']} 条, " f"呼号 {stats['callsign']} 条, 位置 {stats['position']} 条")FileReader背后的实现是分块读取 I/Q 样本,然后调用与前面相同的Decoder。它返回的msg对象里,callsign字段是字符串,latitude/longitude是浮点数或None。如果你跑出来的有效消息比例低于 30%,先检查sample_rate是否正确,再检查文件是不是被某种压缩格式包过。我遇到过有人拿录制的wav文件直接喂给工具,听起来合理但实际是存成了声道交织的格式,工具包需要的是纯原始 IQ,二者字节布局完全不同。3.3 关键参数:采样率、增益与消息过滤实时解码时,SDR 的三个参数直接影响解码质量。采样率:2MHz 是基准,低于 1MHz 会丢失 preamble 的脉宽信息,高于 8MHz 会显著提高 CPU 占用且收益很小。增益:自动增益往往不是最优解,我一般手动设 30–40dB,视环境射频底噪而定。增益过高会引发削波,解出来的消息 CRC 错误率增加;增益过低又直接漏掉弱信号。消息过滤:工具包允许你按df或icao过滤,比如只关注 DF17 长报文,可以减少后续处理压力。decoder = adsb.Decoder( sample_rate=2_000_000, filter_df=[17, 18], ignore_crc_error=False, )这个构造里,ignore_crc_error=False是关键。默认情况下工具包会跳过 CRC 错误的消息,但如果你把False传进去,它会把错误消息也挂在messages列表里(带is_valid=False标记)。保留错误消息可以观察干扰情况,但生产环境建议设成True跳过,因为错误消息里的字段值完全不可信。我第一次做实时监控时没关掉,结果把一张全是乱码的呼号表存进了数据库,直接污染了后续的分析结果。4. 从消息到轨迹:CPR 解码、速度计算与可视化4.1 空中位置消息的 CPR 解码原理ADS-B 的位置广播让人最头疼——它不传绝对经纬度,而是传一种叫 CPR(Compact Position Reporting)的格式。每架飞机交替发送偶帧和奇帧,两帧分别覆盖不同分辨率:偶帧沿纬度方向 131072 个格子,奇帧是 131071 个,经度维度也类似。只有同时收到一奇一偶且时间间隔不超过 10 秒,才能解出全球坐标。这也是为什么很多 ADS-B 显示软件里,飞机刚出现时只有高度和速度,但没有位置——它在等下一帧。这个工具包把 CPR 解码封装在msg.latitude和msg.longitude里,但如果你需要做低层调试,工具包也暴露了cpr_even和cpr_odd两个属性。它们各自包含经纬度编码值(cpr_lat,cpr_lon),配合消息本身的time字段,你可以手搓解码算法。但大多数情况下直接用高层接口就行,除非你在排查“飞机位置跳变半个地球”的问题——那几乎总是奇偶帧时间间隔超过 10 秒导致的。import adsb track = {} for msg in adsb.FileReader("capture.bin", sample_rate=2_000_000): if not msg.is_valid or msg.latitude is None: continue icao = f"{msg.icao:06X}" if icao not in track: track[icao] = {"lat": [], "lon": [], "alt": []} track[icao]["lat"].append(msg.latitude) track[icao]["lon"].append(msg.longitude) track[icao]["alt"].append(msg.altitude)注意这里用了msg.latitude is None作为过滤条件,这在追踪阶段是必要的。CPR 解码成功需要奇偶帧配对,具体来说,工具包内部维护了一个接收机状态表,记录每架飞机最近一次收到的偶帧和奇帧信息。如果你连续只收到偶帧(比如天线极化方向刚好压住了奇帧奇特的编码特征),位置就一直是None。这个状态表是解码器生命周期内持久的,所以你的解码进程最好不要频繁重启,否则会丢帧配信息。4.2 速度与航向:亚字段解析速度报告消息的Type Code为 19,它内部细分为三种子类型:地面速度、空中速度、以及带有垂直速率标志的版本。工具包解出来的speed字段是以节为单位的,vertical_rate以英尺/分钟为单位。这里有一个单位陷阱:speed字段在工具包 SDK 里固定返回节,但altitude返回的是英尺,没有提供公制转换开关。如果你要画米制图表,需要自己做转换。if msg.speed is not None: speed_kmh = msg.speed * 1.852 vert_ms = msg.vertical_rate * 0.00508这个转换其实很直白,但容易漏掉的是vertical_rate的符号和字段宽度。ADS-B 规范里垂直速率是 9 bit 有符号数,工具包已经做了符号扩展,所以你拿到的vertical_rate可能是负数。但如果你自己从msg.raw里解析,就得手动做二的补码——这里最容易翻车。4.3 把轨迹画到地图上跟踪的最终输出通常是一张地图。我一般用folium画交互式地图,或者用matplotlib画平面投影图。下面是一个把轨迹落在平面图上的简单实现。这里matplotlib的底图是等距圆柱投影,适合快速检查轨迹形状,但不适合做实际导航分析。import matplotlib.pyplot as plt fig, ax = plt.subplots(figsize=(10, 6)) for icao, data in track.items(): ax.plot(data["lon"], data["lat"], marker="o", markersize=2, label=icao) ax.set_xlabel("经度") ax.set_ylabel("纬度") ax.legend() plt.savefig("track.png", dpi=120)这段代码里,如果track字典里某架飞机的轨迹点少于 2 个,绘图时会直接连线失败,但不会报错。我通常会先按轨迹点数量过滤:少于 3 点的飞机不画,因为它们大概率是解码错误导致的孤立点。好的做法是给每个轨迹点顺序连线,然后检查是否出现“折返震”现象——如果某条轨迹每隔几十秒就出现一次快速折返,很可能是奇偶帧时间差过大,解出的位置本身有问题。5. 避坑指南:ADS-B Python 解调最容易翻车的五个地方5.1 采样率不匹配导致大量 CRC 错误现象:解码器报告的有效消息占比不足 10%,且is_valid几乎都为False。原因:SDR 实际采样率与Decoder(sample_rate=...)传入的不一致。比如 SDR 设置为 2.4MHz,但代码里写的是 2MHz,导致每个 bit 的采样窗偏移,位同步出现系统性错误。解决:先确认 SDR 软件里的采样率值,再把它原样传给工具包。如果用的是实时流,可以在解码前用工具包提供的estimate_sample_rate()辅助函数,它通过检测 preamble 峰间隔来反推真实采样率,误差通常在 50Hz 以内。estimated_rate = adsb.estimate_sample_rate(reader.buffer) print(f"估计采样率: {estimated_rate}") decoder = adsb.Decoder(sample_rate=estimated_rate)5.2 位置点偶尔跳变到地球对面现象:某架飞机轨迹里的连续位置点中,有一个点突然跳到几千公里外,随后又恢复正常。原因:CPR 解码时接收机状态表里保存的奇/偶帧不是来自同一架飞机,而是同频干扰下解出的错误 ICAO 导致串扰。或者奇偶帧的时间间隔超过 10 秒,符合解码条件但已经失去空间连续性。解决:启用工具包的位置连续性校验(enable_position_filter())。它会根据前一个有效位置和最大飞行速度(默认 1000 节)推算出合理位置范围,超出范围的解直接标记为False。如果你发现位置跳变只发生在高增益设置下,优先降低增益。5.3 呼号解析成乱码现象:msg.callsign返回的值包含@、#等怪异字符,有时是None。原因:呼号字段用 6 位字符集编码,不是 ASCII。工具包内部虽然有字符映射表,但如果消息的 CRC 校验失败后你仍然强行读取callsign(因为is_valid是False),就会拿到垃圾值。另一种情况是某些非商用飞机(军用或实验机)广播的呼号本身就包含非常规字符。解决:永远只在msg.is_valid为True时读取callsign。如果你需要保留错误消息用于诊断,读取前判断一下属性是否可访问。工具包当前实现里,CRC 错误的消息callsign会被设成None,但如果它内部某个字段解析提前返回,也可能得到半解状态。5.4 时间戳跨包错乱,数据排序失效现象:你收集到的消息对象里,msg.time不是单调递增的,绘制轨迹时出现倒序。原因:工具包的FileReader是多线程预读的,内部有一个缓冲队列,解出来的消息会按完成顺序而不是采集顺序进入输出列表。当你的 CPU 负载较高时,多线程解码顺序会乱。解决:对于实时流场景,建议设置FileReader(..., parallel=False)强制单线程处理,保证时间顺序。如果必须用并行模式,那就使用msg.timestamp(这是 SDR 驱动的硬件时间戳)而不是msg.time来排序。硬件时间戳在每个消息块开始处记录,不受解码线程调度影响。5.5 SDR 直流偏置干扰 preamble 检测现象:解码器检测到的 preamble 数量是正常量的 3 倍以上,但绝大多数后续 CRC 失败,CPU 占用飙升。原因:廉价 SDR 的模数转换器存在直流偏置,导致 I/Q 信号的 I 轴或 Q 轴重心不在零点。Preamble 检测算法会误把缓慢的电平漂移当作脉冲沿,产生大量虚假消息。解决:在把 IQ 数据喂给解码器之前做一个去直流处理。常见的做法是减法滤波:对每个数据块计算均值,然后整体平移。工具包自带adsb.dc_blocker()函数,也可以手动实现。def remove_dc(iq): i_mean = iq[:, 0].mean() q_mean = iq[:, 1].mean() iq[:, 0] -= i_mean iq[:, 1] -= q_mean return iq clean_iq = remove_dc(raw_iq) decoder.feed(clean_iq)6. 进阶验证:用多接收机交叉校验数据质量当你把轨迹跑通之后,真正要解决的问题是“我的数据到底准不准”。ADS-B 是开放广播,但解码出来的位置、速度、呼号会因为接收点、天线盲区和解码器状态而产生偏差。我后来养成的一个习惯是:同时开两个 SDR 接收机,放在同一台电脑的两个 USB 口,分别接不同位置的天线,然后交叉比对。# 假设 dec_a 和 dec_b 是两个独立解码器 messages_a = dec_a.feed(iq_a) messages_b = dec_b.feed(iq_b) matched = 0 diff_threshold = 0.001 # 经纬度差异阈值约 100 米 for msg_a in messages_a: for msg_b in messages_b: if (msg_a.is_valid and msg_b.is_valid and msg_a.icao == msg_b.icao and msg_a.latitude is not None and msg_b.latitude is not None): lat_diff = abs(msg_a.latitude - msg_b.latitude) lon_diff = abs(msg_a.longitude - msg_b.longitude) if lat_diff < diff_threshold and lon_diff < diff_threshold: matched += 1 print(f"双通道一致样本数: {matched}")这段代码是一个简化的交叉校验模型,实际做的时候要建立索引而不是双层循环,否则数据量大时性能会很差。我的做法是先用defaultdict按 ICAO 分桶,再对每个 ICAO 的两批点位做最近邻匹配。通过比对两路接收机的解码位置差异,可以反向估计接收机自身的时钟同步误差和天线相位中心偏移。如果两路差异长期超过约 150 米,基本可以判定其中一路的天线位置有问题或 SDR 校准参数没设对。另一个高阶技巧是观察msg.signal_level和msg.noise_level。工具包为每条有效消息附带这两个值,单位是 dBFS 相对值。你可以用它画出每个 ICAO 的信号强度直方图,判断天线盲区。当飞机处于某个方位角时信号急剧衰减,说明天线在该方向有遮挡。某次我在窗边调试,发现西北方向来的飞机信号始终弱,最后发现是窗帘里面有金属丝,这种问题只能靠这种数据驱动的检查方式才能暴露。从那以后,我每搭建一个新的 ADS-B 采集点,都会强制跑一遍“双机对比 + 信号分布直方图”验证流程,确认两条接收路径在关键方位上差异不超过阈值,再投入到长期监控里。这套方法也适用于这个 Python 工具包的离线数据分析。希望帮到你。本文还有配套的精品资源,点击获取
简介:adsb是一个面向Python开发者的ADS-B信号解析工具包,适合航空数据爱好者、无人机航迹分析或无线电解码相关项目。它基于SBS等格式的消息输出,提供ADSB消息处理的基础能力,目前项目处于开发初期,但接口设计简洁、依赖少,适合快速集成到现有Python项目中,可作为学习与二次开发的起点。资源共49个文件,压缩包仅55KB,以py源码为主(23个),辅以rst/sphinx文档(8个)、txt说明与依赖清单、测试脚本和示例配置,目录涵盖src、tests、examples、docs等模块,采用标准Python项目布局,源码与测试分离,便于阅读、修改与扩展。已有1016人学习浏览。安装后即可调用adsb模块,参考examples中的用例处理SBS消息,结合tests中的测试代码理解消息解析流程;docs下API文档帮助深入定制,并附带真实ADS-B消息日志样本可供验证,是入门ADS-B数据处理的轻量工具包。1. 从一次 SDR 抓包说起:Python 读 ADS-B 到底在解什么?把一根几十块的 USB 电视棒插到笔记本上,接上螺旋天线,打开终端跑几行 Python 脚本,屏幕上就开始列出周围飞机的呼号、高度、速度和经纬度。这个场景我第一次跑通时,有那种“给天空开了个后门”的错觉。这个名为adsb的 Python 工具包,就是专门干这个的:它把 1090MHz 上广播的 Mode S 报文解成结构化数据,让你不碰 FPGA 或硬件逻辑,也能实时跟踪空域里的飞机。它解决的核心问题是——把一条 112 微秒的二进制脉冲串翻译成人话。适合三类人:想玩航空数据的开发者、做空域监控原型的研究者、以及那些对着 Flightradar 好奇“它背后到底是什么”的爱好者。接下来我会按“信号原理 → 上手跑通 → 实战处理 → 排坑 → 进阶验证”的顺序,把这个工具包彻底拆开。2. 为什么先搞懂 Mode S 消息结构:解码前的三个基本事实2.1 1090MHz ES 广播不是你想的那种“数据包”ADS-B 的物理层很简单,本质是脉冲位置调制。飞机每秒钟广播若干次,每次广播持续 112 微秒,前 8 微秒是 preamble(同步头),后面 88 微秒是数据块,共 56 个 bit 位,每个 bit 占 1 微秒。这里有个反直觉的点:它没有“目的地址”和“源地址”这种类似 TCP/IP 的头部,所有听到这台接收机的目标都能解。这个 Python 工具包在解码时,第一步不是找飞机 ID,而是找 preamble 特征——拇指粗的高脉冲、低脉冲、高脉冲、低脉冲,间隔 1 微秒。找不到这个模式,后面全部免谈。Mode S 报文分短报文(56 bit)和长报文(112 bit)。真正携带位置和速度的 ADS-B 消息几乎都是长报文,DF 字段(Downlink Format)落在 17、18、19 上。工具包内部就是先切出 DF 值,再根据 DF 分发到不同解析器。理解了这一点,你就明白为什么有些开源软件会显示“DF17”而不显示航班号——因为 DF17 只是框架,里面还要按 Type 字段继续分。我用这个工具时,最常看到的是 Type 1-4(身份识别)、Type 9-18(空中位置)、Type 19(速度)。2.2 Python 工具包的解码流水线:从比特流到字典adsb工具包内部流程大致是四步:位同步 → 消息提取 → CRC 校验 → 字段映射。位同步靠检测 preamble,然后以 0.5 微秒为窗口采样每个 bit 的中心电压,判断高低电平。这里有一个硬件相关的点:如果你的 SDR 采样率不是 2MHz 的整数倍,位同步会引入采样相位偏移,工具包实测下来对 2MHz、4MHz、8MHz 都有补偿,但 2.4MHz 这种非标准采样率容易出问题。CRC 校验是重中之重。Mode S 的 CRC 用的是 24 位多项式,但 ADS-B 里叫“奇偶校验”其实不准确,它覆盖整个 112 bit,错误的判定是“余数非零”。工具包解码时如果 CRC 失败,会返回一条CRCError记录,而不是直接丢包——这个设计很实用,因为你可以在本地看误码率,判断天线位置是否该调。字段映射就是查表:把 bit 段按 ICAO 文档里的位偏移切成字段,比如飞机呼号是 6 个 6-bit 字符,编码用了特殊字符表,不是 ASCII。import adsb # 从 SDR 采集的原始 IQ 数据 buffer 里解出一条消息 raw_iq = open("capture.iq", "rb").read() decoder = adsb.Decoder(sample_rate=2_000_000) messages = decoder.feed(raw_iq) for msg in messages: if msg.is_valid: print(f"ICAO: {msg.icao:06X}, DF: {msg.df}, Type: {msg.type_code}")这段代码里,Decoder是核心类,sample_rate必须和你 SDR 实际采样率一致。feed()接收 IQ 样本,内部自动完成 preamble 检测和消息切分。msg.is_valid对应是否通过 CRC。如果采样率设置错误,你会看到大量is_valid=False的消息,或者 preamble 检测率极低。我第一次跑时就是把 2.4MHz 写成了 2MHz,一整天都在怀疑天线。2.3 选型理由:为什么这个工具比直接解析二进制更顺手很多老手习惯用 C 写的dump1090这类软件,但那是独立进程,要解析它的 JSON 输出。而这个 Python 工具包的价值在于:你可以在自己的采集循环里直接调用解码,拿到的是 Python 字典风格的消息对象,可以直接塞给pandas.DataFrame。这在做数据分析和实时过滤时优势巨大。另一个关键点是它保留了原始消息的raw字段,方便你做差分对比——这在排错时是后悔药。df = pd.DataFrame([ { "icao": f"{m.icao:06X}", "callsign": m.callsign, "lat": m.latitude, "lon": m.longitude, "alt": m.altitude, "speed": m.speed, "crc_ok": m.is_valid } for m in messages if m.is_valid ])这里m.callsign是工具包已经解好的字符串,m.latitude/longitude只在同时收到奇偶两帧并完成 CPR 解码后才非空。请注意,latitude和longitude在初始位置解码前可能是None,这是 ADS-B 的固有机制,不是 bug。我一般先过滤crc_ok,再过滤位置非空的条目,再做去重。3. 把工具跑起来:安装、数据源与第一个解码脚本3.1 安装依赖与准备测试文件这个工具包依赖numpy做位运算加速,如果要做可视化还需要matplotlib和folium。安装走普通pip流程即可,但建议用虚拟环境,因为numpy版本可能会和你其他项目冲突。数据源有两种:一是接 SDR 硬件实时抓 IQ,二是用预先录好的bin文件。对于刚上手的人,强烈建议先拿录制好的样本文件练手,因为实时流里你会同时遇到信噪比波动、干扰脉冲、和多架飞机混叠,新手很容易被带偏。准备样本文件时,我一般会自己录一段 30 秒的 IQ 数据:用 SDR 软件调谐到 1090MHz,采样率 2MHz,增益设到 35dB 左右,保存成原始 IQ 文件。如果你手头没有 SDR,也可以用工具包自带的generate_demo功能,它会生成一段模拟信号——这个功能适合测试安装是否正确,但不能用来练真实解码。python -m venv adsb-env source adsb-env/bin/activate pip install adsb numpy matplotlib folium这段命令里,adsb是工具包名,folium是可视化地图库。venv不是必须的,但我见过太多人因为全局环境里numpy版本不对,解码结果出现随机性错误——那些错误很难排查,最后发现是scipy依赖冲突。所以环境隔离不是洁癖,是省时间。3.2 解码第一个样本:理解输出对象拿到样本文件后,先跑一个最小脚本验证端到端链路。下面这段代码读入 IQ 文件,解码全部有效消息,统计消息类型分布。这里注意file_reader是工具包提供的一个便捷层,它会自动分块处理大文件,避免一次性把几 GB 的 IQ 读进内存。import adsb reader = adsb.FileReader("capture.bin", sample_rate=2_000_000) stats = {"total": 0, "valid": 0, "callsign": 0, "position": 0} for msg in reader: stats["total"] += 1 if msg.is_valid: stats["valid"] += 1 if msg.callsign: stats["callsign"] += 1 if msg.latitude is not None: stats["position"] += 1 print(f"检测到 {stats['total']} 条, 有效 {stats['valid']} 条, " f"呼号 {stats['callsign']} 条, 位置 {stats['position']} 条")FileReader背后的实现是分块读取 I/Q 样本,然后调用与前面相同的Decoder。它返回的msg对象里,callsign字段是字符串,latitude/longitude是浮点数或None。如果你跑出来的有效消息比例低于 30%,先检查sample_rate是否正确,再检查文件是不是被某种压缩格式包过。我遇到过有人拿录制的wav文件直接喂给工具,听起来合理但实际是存成了声道交织的格式,工具包需要的是纯原始 IQ,二者字节布局完全不同。3.3 关键参数:采样率、增益与消息过滤实时解码时,SDR 的三个参数直接影响解码质量。采样率:2MHz 是基准,低于 1MHz 会丢失 preamble 的脉宽信息,高于 8MHz 会显著提高 CPU 占用且收益很小。增益:自动增益往往不是最优解,我一般手动设 30–40dB,视环境射频底噪而定。增益过高会引发削波,解出来的消息 CRC 错误率增加;增益过低又直接漏掉弱信号。消息过滤:工具包允许你按df或icao过滤,比如只关注 DF17 长报文,可以减少后续处理压力。decoder = adsb.Decoder( sample_rate=2_000_000, filter_df=[17, 18], ignore_crc_error=False, )这个构造里,ignore_crc_error=False是关键。默认情况下工具包会跳过 CRC 错误的消息,但如果你把False传进去,它会把错误消息也挂在messages列表里(带is_valid=False标记)。保留错误消息可以观察干扰情况,但生产环境建议设成True跳过,因为错误消息里的字段值完全不可信。我第一次做实时监控时没关掉,结果把一张全是乱码的呼号表存进了数据库,直接污染了后续的分析结果。4. 从消息到轨迹:CPR 解码、速度计算与可视化4.1 空中位置消息的 CPR 解码原理ADS-B 的位置广播让人最头疼——它不传绝对经纬度,而是传一种叫 CPR(Compact Position Reporting)的格式。每架飞机交替发送偶帧和奇帧,两帧分别覆盖不同分辨率:偶帧沿纬度方向 131072 个格子,奇帧是 131071 个,经度维度也类似。只有同时收到一奇一偶且时间间隔不超过 10 秒,才能解出全球坐标。这也是为什么很多 ADS-B 显示软件里,飞机刚出现时只有高度和速度,但没有位置——它在等下一帧。这个工具包把 CPR 解码封装在msg.latitude和msg.longitude里,但如果你需要做低层调试,工具包也暴露了cpr_even和cpr_odd两个属性。它们各自包含经纬度编码值(cpr_lat,cpr_lon),配合消息本身的time字段,你可以手搓解码算法。但大多数情况下直接用高层接口就行,除非你在排查“飞机位置跳变半个地球”的问题——那几乎总是奇偶帧时间间隔超过 10 秒导致的。import adsb track = {} for msg in adsb.FileReader("capture.bin", sample_rate=2_000_000): if not msg.is_valid or msg.latitude is None: continue icao = f"{msg.icao:06X}" if icao not in track: track[icao] = {"lat": [], "lon": [], "alt": []} track[icao]["lat"].append(msg.latitude) track[icao]["lon"].append(msg.longitude) track[icao]["alt"].append(msg.altitude)注意这里用了msg.latitude is None作为过滤条件,这在追踪阶段是必要的。CPR 解码成功需要奇偶帧配对,具体来说,工具包内部维护了一个接收机状态表,记录每架飞机最近一次收到的偶帧和奇帧信息。如果你连续只收到偶帧(比如天线极化方向刚好压住了奇帧奇特的编码特征),位置就一直是None。这个状态表是解码器生命周期内持久的,所以你的解码进程最好不要频繁重启,否则会丢帧配信息。4.2 速度与航向:亚字段解析速度报告消息的Type Code为 19,它内部细分为三种子类型:地面速度、空中速度、以及带有垂直速率标志的版本。工具包解出来的speed字段是以节为单位的,vertical_rate以英尺/分钟为单位。这里有一个单位陷阱:speed字段在工具包 SDK 里固定返回节,但altitude返回的是英尺,没有提供公制转换开关。如果你要画米制图表,需要自己做转换。if msg.speed is not None: speed_kmh = msg.speed * 1.852 vert_ms = msg.vertical_rate * 0.00508这个转换其实很直白,但容易漏掉的是vertical_rate的符号和字段宽度。ADS-B 规范里垂直速率是 9 bit 有符号数,工具包已经做了符号扩展,所以你拿到的vertical_rate可能是负数。但如果你自己从msg.raw里解析,就得手动做二的补码——这里最容易翻车。4.3 把轨迹画到地图上跟踪的最终输出通常是一张地图。我一般用folium画交互式地图,或者用matplotlib画平面投影图。下面是一个把轨迹落在平面图上的简单实现。这里matplotlib的底图是等距圆柱投影,适合快速检查轨迹形状,但不适合做实际导航分析。import matplotlib.pyplot as plt fig, ax = plt.subplots(figsize=(10, 6)) for icao, data in track.items(): ax.plot(data["lon"], data["lat"], marker="o", markersize=2, label=icao) ax.set_xlabel("经度") ax.set_ylabel("纬度") ax.legend() plt.savefig("track.png", dpi=120)这段代码里,如果track字典里某架飞机的轨迹点少于 2 个,绘图时会直接连线失败,但不会报错。我通常会先按轨迹点数量过滤:少于 3 点的飞机不画,因为它们大概率是解码错误导致的孤立点。好的做法是给每个轨迹点顺序连线,然后检查是否出现“折返震”现象——如果某条轨迹每隔几十秒就出现一次快速折返,很可能是奇偶帧时间差过大,解出的位置本身有问题。5. 避坑指南:ADS-B Python 解调最容易翻车的五个地方5.1 采样率不匹配导致大量 CRC 错误现象:解码器报告的有效消息占比不足 10%,且is_valid几乎都为False。原因:SDR 实际采样率与Decoder(sample_rate=...)传入的不一致。比如 SDR 设置为 2.4MHz,但代码里写的是 2MHz,导致每个 bit 的采样窗偏移,位同步出现系统性错误。解决:先确认 SDR 软件里的采样率值,再把它原样传给工具包。如果用的是实时流,可以在解码前用工具包提供的estimate_sample_rate()辅助函数,它通过检测 preamble 峰间隔来反推真实采样率,误差通常在 50Hz 以内。estimated_rate = adsb.estimate_sample_rate(reader.buffer) print(f"估计采样率: {estimated_rate}") decoder = adsb.Decoder(sample_rate=estimated_rate)5.2 位置点偶尔跳变到地球对面现象:某架飞机轨迹里的连续位置点中,有一个点突然跳到几千公里外,随后又恢复正常。原因:CPR 解码时接收机状态表里保存的奇/偶帧不是来自同一架飞机,而是同频干扰下解出的错误 ICAO 导致串扰。或者奇偶帧的时间间隔超过 10 秒,符合解码条件但已经失去空间连续性。解决:启用工具包的位置连续性校验(enable_position_filter())。它会根据前一个有效位置和最大飞行速度(默认 1000 节)推算出合理位置范围,超出范围的解直接标记为False。如果你发现位置跳变只发生在高增益设置下,优先降低增益。5.3 呼号解析成乱码现象:msg.callsign返回的值包含@、#等怪异字符,有时是None。原因:呼号字段用 6 位字符集编码,不是 ASCII。工具包内部虽然有字符映射表,但如果消息的 CRC 校验失败后你仍然强行读取callsign(因为is_valid是False),就会拿到垃圾值。另一种情况是某些非商用飞机(军用或实验机)广播的呼号本身就包含非常规字符。解决:永远只在msg.is_valid为True时读取callsign。如果你需要保留错误消息用于诊断,读取前判断一下属性是否可访问。工具包当前实现里,CRC 错误的消息callsign会被设成None,但如果它内部某个字段解析提前返回,也可能得到半解状态。5.4 时间戳跨包错乱,数据排序失效现象:你收集到的消息对象里,msg.time不是单调递增的,绘制轨迹时出现倒序。原因:工具包的FileReader是多线程预读的,内部有一个缓冲队列,解出来的消息会按完成顺序而不是采集顺序进入输出列表。当你的 CPU 负载较高时,多线程解码顺序会乱。解决:对于实时流场景,建议设置FileReader(..., parallel=False)强制单线程处理,保证时间顺序。如果必须用并行模式,那就使用msg.timestamp(这是 SDR 驱动的硬件时间戳)而不是msg.time来排序。硬件时间戳在每个消息块开始处记录,不受解码线程调度影响。5.5 SDR 直流偏置干扰 preamble 检测现象:解码器检测到的 preamble 数量是正常量的 3 倍以上,但绝大多数后续 CRC 失败,CPU 占用飙升。原因:廉价 SDR 的模数转换器存在直流偏置,导致 I/Q 信号的 I 轴或 Q 轴重心不在零点。Preamble 检测算法会误把缓慢的电平漂移当作脉冲沿,产生大量虚假消息。解决:在把 IQ 数据喂给解码器之前做一个去直流处理。常见的做法是减法滤波:对每个数据块计算均值,然后整体平移。工具包自带adsb.dc_blocker()函数,也可以手动实现。def remove_dc(iq): i_mean = iq[:, 0].mean() q_mean = iq[:, 1].mean() iq[:, 0] -= i_mean iq[:, 1] -= q_mean return iq clean_iq = remove_dc(raw_iq) decoder.feed(clean_iq)6. 进阶验证:用多接收机交叉校验数据质量当你把轨迹跑通之后,真正要解决的问题是“我的数据到底准不准”。ADS-B 是开放广播,但解码出来的位置、速度、呼号会因为接收点、天线盲区和解码器状态而产生偏差。我后来养成的一个习惯是:同时开两个 SDR 接收机,放在同一台电脑的两个 USB 口,分别接不同位置的天线,然后交叉比对。# 假设 dec_a 和 dec_b 是两个独立解码器 messages_a = dec_a.feed(iq_a) messages_b = dec_b.feed(iq_b) matched = 0 diff_threshold = 0.001 # 经纬度差异阈值约 100 米 for msg_a in messages_a: for msg_b in messages_b: if (msg_a.is_valid and msg_b.is_valid and msg_a.icao == msg_b.icao and msg_a.latitude is not None and msg_b.latitude is not None): lat_diff = abs(msg_a.latitude - msg_b.latitude) lon_diff = abs(msg_a.longitude - msg_b.longitude) if lat_diff < diff_threshold and lon_diff < diff_threshold: matched += 1 print(f"双通道一致样本数: {matched}")这段代码是一个简化的交叉校验模型,实际做的时候要建立索引而不是双层循环,否则数据量大时性能会很差。我的做法是先用defaultdict按 ICAO 分桶,再对每个 ICAO 的两批点位做最近邻匹配。通过比对两路接收机的解码位置差异,可以反向估计接收机自身的时钟同步误差和天线相位中心偏移。如果两路差异长期超过约 150 米,基本可以判定其中一路的天线位置有问题或 SDR 校准参数没设对。另一个高阶技巧是观察msg.signal_level和msg.noise_level。工具包为每条有效消息附带这两个值,单位是 dBFS 相对值。你可以用它画出每个 ICAO 的信号强度直方图,判断天线盲区。当飞机处于某个方位角时信号急剧衰减,说明天线在该方向有遮挡。某次我在窗边调试,发现西北方向来的飞机信号始终弱,最后发现是窗帘里面有金属丝,这种问题只能靠这种数据驱动的检查方式才能暴露。从那以后,我每搭建一个新的 ADS-B 采集点,都会强制跑一遍“双机对比 + 信号分布直方图”验证流程,确认两条接收路径在关键方位上差异不超过阈值,再投入到长期监控里。这套方法也适用于这个 Python 工具包的离线数据分析。希望帮到你。本文还有配套的精品资源,点击获取