Hyperized

A pure-Go ADS-B stack

I rewrote the ADS-B receive chain in Go as four separate layers, to learn how it works and to do it my way

A ClockworkPi uConsole with two antennas screwed into the top, running uAirwaves on its built-in screen: a radar scope full of labelled aircraft, a distance-sorted list, and a stats panel reading 92 tracked, 75 positioned, farthest 154.8 nm.
uAirwaves on the uConsole's own screen, 92 aircraft tracked

I like Go, I like radio, and I like aviation. ADS-B sits exactly where those three meet.

Every aircraft overhead is continuously broadcasting who it is, where it is, how high and how fast, in the clear, on 1090 MHz. Structured telemetry, published by the plane itself, is free to anyone with thirty euros of receiver . It just tickles my brain.

There has also been a trend of very nice hardware built on ADS-B lately. Skylight puts a Raspberry Pi 5, an RTL-SDR and a short-throw projector together to paint the traffic overhead onto the ceiling , comet trails and all. ESP32 Plane Radar does a sonar-style sweep on a 1.28 inch round display , and has since been cloned onto AliExpress as a desk gadget.

I initially started uAirwaves a few years ago, pointed at a BEAST feed on my network. But seeing those inspiring projects is what pushed me to make my own stack. I wanted to know how ADS-B reception actually works layer by layer. The result is four repositories, all pure Go, each one a library that comes with its own tooling for (visual) learning.

I did at one point drive to an airport to test, because from my roof every aircraft is already in the air, so the receiver had never once heard one sitting on the ground. On the ground there is no altitude field at all, and in the air it can be metric, or Gray-coded on a scheme from the 1950s. Which is how I learned that altitude is not always a number. Sometimes it is a string.

Four layers

rtl2832u drives the USB dongle. It speaks in USB to the RTL2832U demodulator and the R820T2 / R860 tuner, and produces raw IQ samples. The rtl-probe tool opens the tuner, reports what the front end is doing, and captures IQ to a file. This can help you tweak your antenna setup.

rtl-probe running: a magnitude histogram concentrated near zero, a strip chart showing RMS 25.2 and saturation 0.67 percent, and an FFT spectrum with a peak at the tuned frequency. The header reads chain GOOD, no changes recommended.
rtl-probe: RMS, saturation, DC offset and a Welch-averaged spectrum

demod1090 turns IQ into frames. Magnitude, preamble detection, bit decisions, CRC-24 validation. Its TUI shows what survived: 1284 frames out of 1322 preambles here, a 97 % yield, the downlink-format mix, and the frames scrolling past with their magnitudes.

demod1090 in TUI mode: a downlink-format histogram with ACAS, surveillance, all-call, ADS-B and Comm-B counts, next to a scrolling list of recent frames with timestamps, ICAO addresses, magnitudes and raw hex.
demod1090: preamble yield and the downlink-format mix

modes turns frames into meaning. Message types, callsigns, altitudes, velocities, and position through Compact Position Reporting. It implements ICAO Annex 10 Volume IV and RTCA DO-260B, has no TUI, but its modes-decode reads hex frames on stdin.

uAirwaves draws the radar and owns everything stateful: per-aircraft tracking, GPS, self-locate, and the coverage picture.

uAirwaves tracking twenty aircraft: flight details for the nearest contact, a radar scope with range rings, a distance-sorted plane list, and a coverage panel binned by altitude.
uAirwaves: the top of the stack

The seams between the layers are formats rather than function calls. IQ files between driver and demodulator, Beast over TCP between demodulator and decoder. I can capture at any seam, replay it, and get the same decode every time, which is also how the stack tests itself. beastmux lives on the Beast seam and merges several receivers into one deduplicated feed.

Why not fork dump1090

I wanted to learn how this works, and forking for me skips the part worth learning. dump1090 and readsb are C as well, so starting there would have meant carrying libusb and CGo into everything built on top. Plus my C isn’t any good.

dump1090 was published in January 2013, and readsb is four forks downstream of it: Malcolm Robb, then dump1090-mutability, then dump1090-fa, then Mictronics, then wiedehopf. Thirteen years and five maintainers.

It also comes with thirteen years of decisions that have stopped reading as decisions. readsb for example scans preambles coarsely and skips multi-phase decode, which was correct for the hardware it was written for. That hardware is several generations old now, and the CPU it was rationing is no longer scarce.

I did run a direct comparison, anecdotally, of course. Two identical Pi 4s, each with their own dongle and the same antenna model, on one roof, recording and then replaying the same 90-second captures through both so neither was listening to a different minute of sky.

Averaged over four captures, demod1090 resolved 30 unique aircraft against readsb’s 27, roughly 12 % more. demod1090 spent 63 seconds of wall time doing it against readsb’s 3. It buys the extra aircraft with CPU. Since each capture holds 90 seconds of signal, taking 63 seconds to work through one means it would still keep up with a live receiver, with about a third of the time to spare.

The only ’legacy’ choice I made is transliterating the gain tables from osmocom/rtl-sdr with attribution in the source.

No CGo

rtl2832u speaks Linux usbfs directly through golang.org/x/sys/unix ioctls. No librtlsdr, no libusb, no CGo anywhere in the stack.

Every tool is therefore one statically linked file. Stripped, for arm64: modes-decode is 1.8 MB, rtl-probe 4.1 MB, demod1090 4.9 MB, uAirwaves 4.9 MB. Copying one onto a machine is the entire install. Nothing to apt-get, no shared library version to match, no drift between the box it was built on and the box it runs on. That is the part I care about: a single file and it runs.

There is one more thing it does not need on the target. These dongles are repurposed DVB-T television receivers, so Linux binds dvb_usb_rtl28xxu to them the moment they are plugged in and holds the device; the usual first step with any RTL-SDR is to blacklist that module in /etc/modprobe.d and reboot before anything can open it. rtl2832u detaches the kernel driver over usbfs when it opens the device, so there is no blacklist to write and no reboot to sit through.

In the field

On the uConsole with the HackerGadgets AIO board it is a real handheld: SDR and GNSS already on the board, a small active 1090Mhz antenna on the socket, and the dongle’s bias-tee feeding that antenna for the best ‘view’.

uAirwaves demodulates and decodes everything locally, and it works out where it is from gpsd (if present) and failing that, from triangulation of knowing where the airplanes are.

That’s possible because the radio antenna’s field is basically a large inverted conical shape with the antenna itself at the bottom. The airplanes do carry GPS on board and knowing their location, we can roughly estimate our own.

Two panels. Left, a side view: the reception volume is a cone standing on its point, twice as wide at 40,000 feet as at 10,000, so an aircraft at cruise is audible about 240 nautical miles across and one at ten thousand feet about 120. Right, a plan view: hearing an aircraft means being somewhere inside a circle of that radius drawn around the position it broadcast, and three such circles overlap in one small area, which is where the antenna must be.
Self-locate: every aircraft heard is a circle the antenna must be standing inside.

It is, of course, also able to read BEAST TCP streams and also able to deduce the antenna’s location from its data.

How I built it

I wrote most of uAirwaves myself.

The three layers under it were built LLM-assisted, against strict rules: 100 % line coverage, golangci-lint with default: all and revive at enable-all-rules, no third-party dependencies in the decoder, and every register write in the driver cited to the RTL2832U or R860 datasheet.

Nothing counted as finished until it had run on hardware. Every layer has pulled real aircraft off a real antenna, and the replay corpus exists so that a regression shows up as a decode that changed rather than a test that turned red in isolation.

Feedback

I’m open to feedback and improvements on these four projects, feel free to reach on Github !