Bench procedure for building one feeder Pi, per unit, start to finish. Do not skip steps and do not box a unit that hasn't passed step 8. The condensed pre-ship list lives in docs/shipping-checklist.md.
Hardware per unit: Pi 4 (64-bit), quality SD card, PSU, two RTL-SDR sticks (1090 + 978) with appropriate filters/antennas — one stick if the site is single-band.
Flash Raspberry Pi OS Lite 64-bit (Raspberry Pi Imager: set hostname
adsb-<site>, enable SSH, set a strong password, do NOT configure WiFi yet).
Boot on bench ethernet; ssh in.
sudo apt-get update && sudo apt-get install -y git
git clone https://github.com/gemivnet/adsb-vm-config.git
cd adsb-vm-config/node
cp site.env.example site.env
vi site.envFill in SITE_ID, SITE_NAME, surveyed coordinates and altitude of the
actual install location (not the bench), SITE_DUAL_BAND, GAIN_978, and a
fresh tagged (tag:adsb-node), non-ephemeral, pre-auth Tailscale key.
Tagged nodes don't key-expire — the unit can sit in a closet for months and
still join.
Device selection is by serial, never by USB index. With only the 1090 stick plugged in:
sudo scripts/serialize-sdrs.sh 1090Re-plug and verify (rtl_eeprom should show 00001090). Repeat with only
the 978 stick: sudo scripts/serialize-sdrs.sh 978 → 00000978.
Label the sticks physically.
sudo ./install.shInstalls readsb/tar1090/graphs1090 (+dump978-fa if dual band), nftables
(30005/30978/8080 reachable from tailnet + localhost only), Tailscale
(adsb-<site>, tag:adsb-node, tailscale ssh), security-only unattended
upgrades, volatile journal. Re-run any time to upgrade or after editing
site.env.
Confirm tar1090 at http://<bench-ip>:8080 shows local traffic on both bands
(or 1090 with a test antenna).
Get the exact SSID/PSK from the host beforehand; confirm the network is 2.4 GHz-visible. Ethernet stays the fallback — leave it configured.
sudo nmcli connection add type wifi ifname wlan0 con-name site-wifi \
ssid "<SSID>" wifi-sec.key-mgmt wpa-psk wifi-sec.psk "<PSK>" \
connection.autoconnect yesThe node must already appear in tailscale status as adsb-<site>. On the
hub:
- Register the site:
sites.d/<site>.envfrom the example (matching SITE_ID/coords, next freeSITE_INDEX, freshuuidgenfor SITE_UUID). - Register the site with each aggregator and fill
hub/ingest/feeds/<site>/feeds.env— FR24 key, FlightAware feeder ID (generate + claim), ADSBx is automatic. Full instructions: hub/ingest/feeds/README.md. sudo hub/install.sh, then:
check-node.sh <site>
docker ps # feeds.<site>.* containers upAll ports open, message rate > 0, feed containers running and their logs
show a connected/logged-in state → proceed. (Aggregator keys can also be
added later — rerun hub/install.sh whenever feeds.env changes.)
Reboot the Pi on an isolated network or phone hotspot (no bench ethernet)
mimicking the destination LAN. It must come back on the tailnet unattended
and check-node.sh <site> must pass again. This is the step that catches
WiFi typos, 5 GHz-only assumptions, and firewall surprises — only then box
it.
Box with PSU, antennas, labeled sticks, and a card with the host's minimal
instructions ("plug into power; that's it"). The site is already registered
on the hub from step 6; after the unit comes up at its destination, run
check-node.sh <site> once more and check the status page row goes green.
- There are no custom systemd units on nodes: vendor units from
readsb/dump978-fa/tailscaled are configured via
/etc/default/*files written by install.sh. - Upgrades later:
git pull && sudo ./install.shovertailscale ssh. - The Tailscale ACL (see docs/tailscale-acl.md) gives nodes no outbound rights on the tailnet; the hub pulls from them.