Dropping OpenSky to Receive It Myself — Feeding RTL-SDR ADS-B into My Terminal Globe ttymap

本文为原文前 6,000 字符的节选翻译,完整内容请查看原文。

I wanted to receive it myself Earlier I wrote about building ttymap, a globe you spin inside the terminal. A braille-character globe with planes, earthquakes, the ISS and more layered on top. The aircraft plugin that shows those planes hit OpenSky Network’s public ADS-B feed. Handy, but really you’re just watching the result someone else’s server received and formatted. The radio those overhead planes emit is raining down on my own room right now too. So I’d rather receive it on my own antenna and put it on my own globe. A receive-only RTL-SDR needs no radio license here. I bought a dongle and a dipole antenna. This post is the record of what happened from plugging that dongle in to “planes I received myself” drifting across my homemade globe.

我想要亲自接收这些数据。早些时候,我写过关于构建 ttymap 的文章,这是一个可以在终端里旋转的地球仪,上面叠加了飞机、地震、国际空间站等信息。显示这些飞机的插件连接的是 OpenSky Network 的公共 ADS-B 数据源。这很方便,但实际上你只是在观看别人服务器接收并格式化后的结果。那些头顶飞过的飞机所发射的无线电信号此刻也正洒落在我的房间里。所以我更愿意用自己的天线接收,并把它们显示在自己的地球仪上。在这里,仅接收的 RTL-SDR 不需要无线电执照。我买了一个加密狗(dongle)和一根偶极天线。这篇文章记录了从插入加密狗到“我亲自接收的飞机”在我自制的地球仪上漂移的全过程。

Plug it in and it’s a TV tuner Plug it into USB, look at lsusb, and you get this. Bus 003 Device 007: ID 0bda:2838 Realtek Semiconductor Corp. RTL2838 DVB-T DVB-T. A digital terrestrial television tuner. No surprise: the RTL2832U chip inside an RTL-SDR was originally sold in bulk as a “cheap USB digital-TV tuner,” and only later did someone discover that “if you make it spit out raw, pre-demodulation I/Q samples you can receive any radio = it becomes an SDR,” giving it a second life. So the moment you plug it in, the Linux kernel grabs it as a TV tuner. A driver called dvb_usb_rtl28xxu. That’s the problem: ┌─ dvb_usb_rtl28xxu (kernel / for TV) ← if it grabs the device, SDR can’t open it RTL2832U ───┤ └─ librtlsdr (userspace / for SDR) ← this is the one I want for reception Two drivers can’t grab one device at once. To use it as an SDR, you blacklist the TV driver so it doesn’t claim it. Arch’s rtl-sdr package, thankfully, does this out of the box in /usr/lib/modprobe.d/rtlsdr.conf. blacklist dvb_usb_rtl28xxu blacklist e4000 blacklist rtl2832 Check the dongle and tuner model with rtl_test. $ rtl_test -t Found 1 device(s): 0: Realtek, RTL2838UHIDIR, SN: 00000001 Found Rafael Micro R820T tuner R820T. It reaches up to 1090MHz, so it picks up ADS-B (1090MHz) just fine. The hardware is alive.

插上它,它就是一个电视调谐器。把它插进 USB 接口,查看 lsusb,你会得到这样的结果:Bus 003 Device 007: ID 0bda:2838 Realtek Semiconductor Corp. RTL2838 DVB-T DVB-T。这是一个数字地面电视调谐器。这并不奇怪:RTL-SDR 内部的 RTL2832U 芯片最初是作为“廉价 USB 数字电视调谐器”批量销售的,后来才有人发现“如果你让它输出原始的、解调前的 I/Q 采样数据,你就可以接收任何无线电信号 = 它就变成了一个 SDR”,从而赋予了它第二次生命。所以当你插上它时,Linux 内核会把它当作电视调谐器来抓取,使用名为 dvb_usb_rtl28xxu 的驱动程序。这就是问题所在:┌─ dvb_usb_rtl28xxu(内核/用于电视)← 如果它占用了设备,SDR 就无法打开它;RTL2832U ───┤ └─ librtlsdr(用户空间/用于 SDR)← 这才是我接收信号想要的。两个驱动程序不能同时占用一个设备。要将其用作 SDR,你需要将电视驱动程序列入黑名单,这样它就不会占用设备。幸运的是,Arch 的 rtl-sdr 软件包在 /usr/lib/modprobe.d/rtlsdr.conf 中默认完成了这一操作。使用 rtl_test 检查加密狗和调谐器型号。$ rtl_test -t 找到 1 个设备:0: Realtek, RTL2838UHIDIR, SN: 00000001 找到 Rafael Micro R820T 调谐器。R820T 的频率范围高达 1090MHz,因此它可以很好地接收 ADS-B(1090MHz)信号。硬件已经激活。

You can catch raw frames. But they aren’t coordinates The easiest way to catch ADS-B is to just run rtl_adsb, bundled with the rtl-sdr package. $ rtl_adsb *8d8622ae58c906dd3078d6822f73; *8d84b7b699088411219a359bb254; *8d9632ae58c906dd…; … Lines starting with *8d are DF17 (ADS-B Extended Squitter), the payload messages carrying position and velocity. More than 30 of them in 20 seconds, across several aircraft. The dipole → R820T → librtlsdr reception chain works. …but these aren’t coordinates. They’re raw Mode-S frames. Getting lat/lon out of here needs one more step: CPR (Compact Position Reporting) decoding. Why isn’t the raw lat/lon just sitting in there? An ADS-B message is only 112 bits, and packing full-precision latitude/longitude every time would eat too much bandwidth. So CPR encodes position split into two kinds, “even frames” and “odd frames.” Pair an even with an odd and you can uniquely recover the position anywhere on Earth (global decoding). Alternatively, if you roughly know the receiver’s position, you can recover it locally from a single frame. The latter is easier to implement. I know my receiver’s (home) coordinates, so I use that one. Writing CPR by hand is painful, so I throw it at pure-Python pyModeS. v3 has an API where you hand it one message and it returns every field as a dict; pass a rough receiver position to reference and it decodes locally. import pyModeS as pms r = pms.decode(msg, reference=(MY_LAT, MY_LON)) # r[“latitude”], r[“longitude”], r[“altitude”], r[“callsign”], r[“crc_valid”] … Write a small script that just pipes rtl_adsb’s output into this, run it, and coordinates start scrolling in the terminal. 21:29:39 89912B EVA195 lat= 34.8097 lon= 136.9325 alt=37975ft 21:29:39 851826 JAL229 lat= 34.5754 lon= 136.7913 alt=27600ft 21:29:50 861F00 JAL616 lat= 34.3943 lon= 136.7036 alt=41000ft Callsign, ICAO address, lat/lon, altitude. The same aircraft’s coordinates move smoothly and consecutively (JAL616 heading south, JAL229 descending), so the decode is correct. A real plane I received on my own antenna has become coordinates.

你可以捕获原始帧,但它们不是坐标。捕获 ADS-B 最简单的方法是运行 rtl-sdr 软件包中捆绑的 rtl_adsb。$ rtl_adsb *8d8622ae58c906dd3078d6822f73; *8d84b7b699088411219a359bb254; *8d9632ae58c906dd…; … 以 *8d 开头的行是 DF17(ADS-B 扩展电文),即携带位置和速度信息的有效载荷消息。20 秒内有超过 30 条,来自几架不同的飞机。偶极天线 → R820T → librtlsdr 的接收链工作正常。……但这些不是坐标,它们是原始的 Mode-S 帧。从中获取经纬度还需要一步:CPR(紧凑位置报告)解码。为什么原始经纬度没有直接放在里面?一条 ADS-B 消息只有 112 位,每次都打包全精度的经纬度会占用太多带宽。因此,CPR 将位置编码分为“偶数帧”和“奇数帧”两种。将偶数帧和奇数帧配对,你就可以唯一地恢复地球上任何位置(全局解码)。或者,如果你大致知道接收器的位置,也可以从单个帧中进行本地恢复。后者更容易实现。我知道我接收器(家)的坐标,所以我使用这种方法。手动编写 CPR 很痛苦,所以我把它交给纯 Python 的 pyModeS。v3 版本有一个 API,你给它一条消息,它就会以字典形式返回所有字段;传入一个粗略的接收器位置作为参考,它就能进行本地解码。编写一个小脚本,将 rtl_adsb 的输出通过管道传给它,运行起来,坐标就会在终端中滚动显示。呼号、ICAO 地址、经纬度、高度。同一架飞机的坐标平滑且连续地移动(JAL616 向南飞行,JAL229 正在下降),所以解码是正确的。我用自己的天线接收到的真实飞机已经变成了坐标。

Feeding it into ttymap — bridge a JSON server in between Now I want this on my homemade globe, ttymap. The intake a ttymap plugin uses for data is ttymap.http:fetch(url) — basically, fetch JSON over HTTP. The earlier aircraft plugin also did http:fetch against OpenSky’s REST API and plotted points with map:point(lon, lat, …). But this rtl_adsb | pyModeS only spits text to stdout; it has no HTTP intake. The shapes don’t match. So I bridge a small thing in between. I wrote an HTTP server in Python’s standard library that runs rtl_adsb as a child process, holds the aircraft state pyModeS decodes keyed by ICAO, and just serves it at 127.0.0.1:8888/aircraft.json. rtl_adsb ──▶ serve.py (decode with pyModeS + hold aircraft state) ──HTTP /aircraft.json──▶ ttymap The key was changing the decoder from “one frame → print one line” into “accumulate state per ICAO.” Position arrives in position messages, callsign in identification messages, heading in velocity messages (velocity’s track) — each comes in piecemeal from different messages, so you grow a record per aircraft and drop it once it goes unheard for a while. The JSON it serves looks like this. { “aircraft”: [ { “icao”: “861f00”, “callsign”: “JAL616”, “lat”: 34.39, “lon”: 136.7, “alt”: 41000, “heading”: 182.8, “on_ground”: false } ] }

将其输入 ttymap — 在中间架设一个 JSON 服务器。现在我想在我的自制地球仪 ttymap 上显示这些数据。ttymap 插件使用的数据接口是 ttymap.http:fetch(url) — 基本上就是通过 HTTP 获取 JSON。早期的飞机插件也是通过 http:fetch 请求 OpenSky 的 REST API,并用 map:point(lon, lat, …) 绘制点。但这个 rtl_adsb | pyModeS 组合只会向标准输出打印文本;它没有 HTTP 接口。形状不匹配。所以我架设了一个中间件。我用 Python 标准库写了一个 HTTP 服务器,它将 rtl_adsb 作为子进程运行,保存 pyModeS 解码后的飞机状态(以 ICAO 为键),并将其提供给 127.0.0.1:8888/aircraft.json。关键在于将解码器从“一帧 → 打印一行”改为“按 ICAO 累积状态”。位置信息出现在位置消息中,呼号出现在识别消息中,航向出现在速度消息中(速度的航迹)——每一项都零散地来自不同的消息,所以你需要为每架飞机建立记录,并在一段时间没有收到信号后将其丢弃。它提供的 JSON 看起来像这样。

Writing the plugin — heading as an arrow A ttymap user plugin goes in ~/.config/ttymap/lua/plugin/.lua, and you enable it with require “plugin.” in ~/.config/ttymap/init.lua. The body is almost entirely cribbed from the bundled aircraft plugin, with only the fetch target swapped for the local server. The crux is, inside on_tick (called every frame), to fire http:fetch at a fixed interval, parse the returned JSON, and plot points. To match the earlier aircraft, I render the heading as an 8-direction arrow (↑↗→↘↓↙←↖).

编写插件 — 将航向显示为箭头。ttymap 用户插件位于 ~/.config/ttymap/lua/plugin/.lua,你可以在 ~/.config/ttymap/init.lua 中通过 require “plugin.” 来启用它。插件主体几乎完全照搬了捆绑的飞机插件,只是将获取目标换成了本地服务器。关键在于,在 on_tick(每帧调用)内部,以固定的时间间隔触发 http:fetch,解析返回的 JSON,并绘制点。为了匹配之前的飞机显示,我将航向渲染为 8 方向箭头(↑↗→↘↓↙←↖)。