Eleven Aircraft From a Paperclip: Bringing Up an ADS-B Receiver
Do you know that feeling when the parcel arrives, you lay every part out on the desk in a satisfying row, and then notice that the one component which actually decides whether any of it works is the one you did not order? That was 23 October. I had 5 520 Kč of receiver on the table and nothing to screw into the front of it.
What follows is the week after that: hardware that is still not installed, an experiment that measured nothing, a paperclip, and a NATO flight I heard perfectly for three minutes while failing to put it on a map for two and a half of them. That last one is why the post exists.
The kit, minus the thing that matters
Everything from rpishop.cz, all in stock, ordered on 23 October 2026.
| Part | Kč |
|---|---|
| FlightAware Pro Stick Plus (SDR + 1090 MHz SAW filter + 19 dB LNA, SMA female) | 1 879 |
| Raspberry Pi 5, 4 GB | 2 879 |
| Pi 27 W USB-C PSU | 314 |
| GOODRAM 32 GB microSD | 299 |
| Pi 5 Active Cooler | 149 |
| total | ~5 520 |
No antenna. The cheapest component in the system is the only one that decides the result, and it was the one I left in a browser tab. The shortlist I should have ordered from, same shop:
| Antenna | Kč |
|---|---|
| 3 dBi magnetic base | 189 |
| 60 cm 1090 MHz colinear, 5.5 dBd, N-female, outdoor-rated, pole mount | 919 |
| Vinnant COL1090/5-SU | 1 249 |
I went with the 919 Kč 60 cm colinear and a 1 m RG58 jumper, SMA male to N male, 199 Kč — or a 3 m LMR-400 N-to-SMA at 590 Kč if the dongle cannot sit within a metre of the antenna.
It has not arrived and it is not installed. Everything below is therefore the baseline the real antenna will be judged against, measured deliberately without one.
Why the Pro Stick Plus, and not just any stick
Any RTL2832U DVB-T tuner is an ADS-B receiver, ideally one with an R820T2. Those sticks were sold as television tuners; the SDR crowd worked out they are wideband radios with a USB connector. A 15 EUR dongle gets you decoding tonight.
What you pay 1 879 Kč for is the word Plus: a 1090 MHz SAW filter ahead of the amplifier. Prague is full of strong broadcast transmitters, and out-of-band energy is the usual reason a city install performs badly while identical hardware on a hill works beautifully. The Plus also carries a 19 dB LNA, which comes back to bite me later.
The connector that screws on perfectly and connects nothing
The same shop sells RP-SMA versions of the same cables, right next to the SMA ones. RP-SMA reverses only the centre contact: socket meets socket, no pin anywhere. It threads together with a satisfying click and makes no electrical connection at all.
That does not read as a wrong cable. It reads as a dead receiver.
Cable length is the real decision, and it is a trade
At 1090 MHz, 5 m of RG58 costs about 4 dB — more than half of what the 5.5 dBd antenna gives you in the first place. LMR-400 is much better per metre, and also stiff and 590 Kč. Passive USB 2.0 tops out around 5 m, which bounds the other end of the problem. So there are two sane layouts and no third:
- Dongle at the antenna: short coax, long USB. Best RF, worst serviceability.
- Dongle indoors: long coax, short USB. Easy to reach, pays the loss.
You choose which cable eats your signal.
One free dB while you are there: USB 3 controllers radiate broadband hash straight across 1090 MHz, and a receiver with a 19 dB front end will happily amplify your own computer along with the aircraft. A 50 cm USB extension to get the stick away from the host is the cheapest dB you will ever buy.
The window is part of the antenna system
One more placement fact, and it is measurable rather than arguable: modern low-E and metallised glazing is brutal at 1090 MHz — easily 10–20 dB, which can be most of your range. So do not argue about it. Count positioned aircraft with the window closed, then open, same ten minutes. If opening it roughly doubles the count, the coating is the limiting factor and a balcony beats any amount of software tuning.
The stick declares 500 mA required against 500 mA available on a MacBook’s USB-C port, so an unpowered hub shared with other devices is a plausible route to intermittent enumeration, or none. And Docker Desktop on macOS cannot pass USB through at all, so the decoder has to run natively — which is why the Pi is the real home and the Mac only ever a smoke test.
Bring-up on macOS, and two log lines that look like faults
brew install librtlsdr dump1090-fa The formula is dump1090-fa but it installs the binary as dump1090. Then, with nothing plugged in:
$ rtl_test -t
No supported devices found. That message is ambiguous on every platform I have seen it on: an absent stick, a kernel DVB driver holding the device, and a permissions problem all produce exactly that line. The disambiguator is one level down:
system_profiler SPUSBDataType | grep -i rtl Nothing on the bus at all means there is nothing to debug yet. Two cheap observations where one leaves the cause open — that is the whole technique.
Plugged in, the bus and the test tool agree:
RTL2832U Vendor ID 0x0bda (Realtek) Product ID 0x2832 SN 00001000
Found 1 device(s): 0: Realtek, RTL2832U, SN: 00001000
Using device 0: Generic RTL2832U
Found Rafael Micro R820T tuner
Supported gain values (29): 0.0 0.9 1.4 2.7 ... 44.5 48.0 49.6
[R82XX] PLL not locked!
Sampling at 2048000 S/s.
No E4000 tuner found, aborting. Two of those lines look like failures and are not:
No E4000 tuner found, aborting—rtl_test -tis specifically the E4000 tuner-range test, and an R820T correctly declines to run it. Everything printed above that line is the result.[R82XX] PLL not locked!— emitted while the driver sweeps outside the tuner’s range during init. Harmless.dump1090then tuned and decoded without complaint.
Also: a genuine Pro Stick Plus enumerates as Generic RTL2832U, serial 00001000, and says FlightAware nowhere. Normal, not a counterfeit signal.
The capture command, used unchanged for everything below:
dump1090 --device-type rtlsdr --gain 49.6 --write-json /tmp/adsb \
--write-json-every 1 --quiet Act one: decoding with an empty socket
Before the antenna question, the useful question is whether the chain works at all. So: 45 seconds, maximum gain, absolutely nothing in the SMA jack.
49d488 OKISR rssi -12.2 65 msgs no position A Czech-registered aircraft, 65 messages, from a receiver with a bare socket where the antenna goes. Later runs, same empty jack:
49d488 rssi -25.5 52 msgs (same aircraft, 13 dB weaker minutes later)
48c1a8 5 325 ft rssi -30.2 19 msgs
40756a EZY42ZL FL390 rssi -30.6 30 msgs Two bits of hex-block trivia worth carrying around: 49d is Czech, so OK-registered, and 48c is the Netherlands.
What confused me first was hearing an easyJet at FL390 and not hearing approach traffic a few kilometres away at 3 000 ft. That is not a contradiction. FL390 is nearly 12 km straight up, in clear line of sight above every building. The approach traffic is lower, closer, and behind several hundred thousand tonnes of Prague.
Act two: the gain sweep that measured nothing
My reasoning was “no antenna, so take all the sensitivity you can get” — hence --gain 49.6, the maximum the R820T offers. That is wrong for this stick, because the Plus already has a 19 dB LNA ahead of the tuner, and piling maximum tuner gain on top amplifies noise and intermodulation products as enthusiastically as it amplifies aircraft.
Good hypothesis. So I ran a sweep: 49.6, 44.5, 40.2, 33.8, and AGC, 55 seconds each, same empty jack, same spot on the desk.
It printed five rows of zeros.
Three things were wrong with that, and they get progressively more interesting:
- The
agcrow was an argument error.--gain -10is not valid in this build — AGC is--enable-agc— and I had redirected the log and never read it. - The four other runs overwrote each other’s logs.
- Then the positive control failed. 120 seconds at 49.6 dB — the exact setting that had caught EZY42ZL twenty minutes earlier — returned zero aircraft, with no error, with the stick still enumerated.
The control failing is the most valuable result of the week, because it proves the experiment was never answerable. My hit rate with an empty socket was roughly one aircraft every two minutes, so a 55-second sample is a coin flip and zero is the expected outcome at any gain setting. The variance was larger than the effect I was trying to measure.
Without a control I already knew worked, “max gain is wrong for this stick” would have looked comprehensively confirmed by five rows of zeros. It was not confirmed. It was not even tested. The hypothesis is still plausible, still untested, and it stays untestable until there is an antenna on the end of the cable.
Act three: 68.8 mm of paperclip
If the problem is that there is nothing in the socket, the fix does not have to be expensive. It has to be the right length.
λ at 1090 MHz = 275 mm → quarter wave = 68.8 mm So: 6.9 cm of straightened paperclip, pushed into the centre of the SMA jack. The proper free version is the same 69 mm vertical with four 69 mm radials folded down at 45 degrees — a quarter-wave groundplane, which plenty of feeders run permanently and happily.
This is a legitimate first measurement rather than a bodge. Aircraft on approach are a few kilometres away transmitting 100–500 W, so you do not need much. And you especially do not want your first number arriving through a mismatched antenna, because a pessimistic first number gets blamed on the location or on the glass — and the location is the one thing you cannot change your mind about later.
Identical 120-second window, 16:24:14–16:26:14 UTC, same gain, same spot on the desk:
| Socket | Aircraft | Positions |
|---|---|---|
| empty | 1 | 0 |
| 68.8 mm of paperclip | 11 | 3 |
Here is what eleven looks like:
hex callsign alt ft gs kt msgs rssi position
4c2655 PNC8J 40400 410.0 914 -12.1 49.5116,14.7052
ae123a RCH4960 34000 - 631 -15.4 -
4d03c2 NATO09 32025 442.1 378 -14.1 49.5020,15.1008
4cad2a RYR923F 37000 401.5 353 -14.3 -
48cb15 TVS49A 3350 189.3 296 -7.7 50.1718,14.4604 PNC8J was 35.4 nm from the centre of Prague, NATO09 43.0 nm, TVS49A 5.8 nm. ae is the US military block and RCH is “Reach”, Air Mobility Command. TVS49A is a Smartwings low and slow at 189 kt — the strongest contact in that window at rssi -7.7.
Three minutes later the list had grown to 12 aircraft, 5 positioned, now including RYR621W at 8 650 ft, 7.9 nm, rssi -3.0, and THY6XV at 4 425 ft, 11.4 nm, rssi -4.9 — both on approach to LKPR. Compare rssi -3.0 at 7.9 nm against about -15 at 40 nm and you have the entire geometry of the problem in two numbers.
One warning about that count: in the same list, 48cb15 showed last seen 198.5s ago with no altitude and no position. dump1090 keeps stale entries around for a while, so a naive “12 aircraft” includes ghosts. Read the seen column, not the row count.
NATO09, or why hearing is not seeing
The aircraft worth the whole evening is 4d03c2, callsign NATO09. The 4d0 block is Luxembourg, which is where the NATO AWACS fleet registers. It closed from 43.0 to 29.5 to 26.2 nm over a few minutes, and I tracked it continuously for three of them, sampling every 10 seconds, off a paperclip.
16:29:24 FL320 439 kt trk 288° 26.2 nm 49.6401,14.4737 (seen 4.3 s)
16:29:34 FL320 439 kt trk 288° 26.2 nm (same fix, seen 14.7 s)
16:29:44 FL320 439 kt trk 288° 26.2 nm (same fix, seen 24.0 s)
16:29:54 FL320 - - - position dropped
16:30:04
… FL320 continuously, rssi -11 to -16, seen 0.1–2 s,
16:32:14 altitude 32025 → 32050 → 32025, NO position for 2.5 minutes The aircraft was never lost. Messages kept arriving 0.1 to 2 seconds old for two and a half minutes straight, at a comfortable -11 to -16 dB, and the altitude was live — it ticks 32025, 32050, 32025, a real aircraft holding a level with normal barometric jitter, not a cached value.
And it was not on the map.
The mechanism is CPR, compact position reporting. Altitude rides in essentially any single position message, so one decoded frame gives you a current altitude. Position does not work that way. ADS-B encodes latitude and longitude in a compressed form split across even and odd frames, and a receiver needs a matched even/odd pair from the same aircraft, within roughly 10 seconds, to resolve an unambiguous global fix. Decode about half the frames and you get altitude almost always and a matched pair only occasionally.
The failure is not graceful. It is binary. The aircraft is simply not there.
The first three rows show the other half of the story: an identical fix with seen climbing 4.3 → 14.7 → 24.0 s. That is not a stationary aircraft at FL320, it is one stale position ageing out. Then it vanishes rather than drifting, because dump1090 declines to draw a fix it can no longer refresh.
So “11 heard, 3 placed” is not a receiver failing to hear. It is a receiver hearing eleven aircraft and being able to place three of them. Identity without geometry. Everything an antenna upgrade buys you — that is the number it moves.
The software half
The receiver is half the project. The other half is a small Go agent that pushes what it hears into the traffic ingest of a flight-sim platform I am building. Six things that survived contact with reality.
Two dialects, one client. readsb exposes its own re-api (/?all_with_pos, /?circle=lat,lon,radius); stock dump1090 and tar1090 serve /data/aircraft.json. A client that speaks only one of them works on your desk and fails on the Pi, or exactly the reverse. Ours speaks both.
Count what you could not place. The library reports heard and excluded separately instead of quietly dropping aircraft without a fix, because an aircraft heard without a position is a real observation about reception, not a decode failure. The push log reads heard=2 stored=2 rejected=0 excluded=8, and later heard=5 stored=5 excluded=9. That ratio is the antenna argument, in a form you can graph.
Push, do not pull. The aggregate feed a feeder is entitled to is bound to the station’s IP, so a service egressing from a shared cluster address can never fetch it — and nothing inside a cluster can reach a receiver in a flat. So the agent runs beside the antenna and pushes inward. That also deleted a rate limit which had previously got a puller suspended, and left a standing rule: do not hammer somebody’s free API, ever.
Stamp the receiver’s clock, not the agent’s. Every per-aircraft seen is relative to the receiver’s own now. Use the agent’s clock and you will cheerfully describe aircraft seen after the snapshot that contains them — and a badly skewed box looks perfectly current while every position in it is hours old.
Report an empty sky. Suppressing a zero-aircraft push makes a dead receiver and a quiet night look identical. The first real push was heard=0 excluded=1 — one Mode S aircraft, no fix — and sending it was the entire point.
Do not model coverage as a circle. A window or balcony antenna does not give you a radius, it gives you a wedge with a hard building shadow, and a circular model will confidently claim empty sky in directions that are solid wall. Bin real observations by bearing × distance × altitude and the shape falls out: a tight low lobe along the approach corridor, and a wide shallow one for en-route traffic overhead. Two footprints through one window. Measure it, do not derive it.
It cross-compiles to the Pi as a 21 MB static binary, which is the pleasant part of doing this in Go:
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -tags nodynamic -o bin/site-agent-arm64 ./cmd/site-agent Chapter two
The antenna is the only thing left, and it arrives with a pile of questions I have not answered.
24/7 on the Pi. The Pi 5 takes over from the laptop: readsb instead of dump1090, a systemd unit, the push agent on an interval. The deciding questions are the boring ones — SD card wear, what happens after a power cut, how I find out it died. A receiver that honestly reports an empty sky is what makes the last one detectable.
Outdoors for real, which is where IP65 arrives. The 919 Kč antenna is outdoor-rated. The connector, the dongle and the Pi are not. So: an IP65 enclosure at the mast with a short pigtail — best RF, worst serviceability, and now power and USB or Ethernet have to go up there too — or the dongle indoors paying coax loss. Then the unglamorous half: a drip loop, self-amalgamating tape over the N connector, UV-rated cable ties, and the fact that a balcony mount you already own is worth more than any antenna you can buy. Plan, not done.
Giving back. Feeding the public networks — adsb.lol, FlightAware, airplanes.live, adsb.fi, OpenSky, ADSBHub — is also what grants access to the aggregate, because one receiver sees a region and not a planet. FlightAware’s feeder credit is a small bonus. Underneath sits a licensing question — ODbL obligations on open aggregates versus data you received yourself — that wants settling before anything commercial is built on it. I have not settled it.
And the real ACARS. NATO09 was caught on 1090 MHz, Mode S and ADS-B — position and identity. That is not ACARS, however often the two get conflated. Reading the aircraft’s own messages is a different receiver chain: ACARS around 131 MHz, VDL2 at 136 MHz, a second dongle, acarsdec or dumpvdl2.
Which means the next parcel also needs an antenna. You would think I had learned something.