Skip to content

PXE-netboot SDR collector nodes

Diskless Raspberry Pi SDR collector nodes that network-boot from the Rafe server, so you can scatter cheap RTL-SDR dongles around the site (different antennas, different bands) with no SD card to image or maintain per Pi. Each node identifies itself, gets its assignment, runs its collector, and reports back to Rafe.

Typical roles: Inmarsat (STD-C/AERO via SatDump), AIS (ships), and rtl_tcp relays that let the central server run the native DVB-S/S2 DATV or TETRA decoders against a remote antenna — the DATV feed can then be viewed in the RafePad iPad app.

                    ┌──────────────── Rafe server (prod) ────────────────┐
   Raspberry Pi     │  dnsmasq (proxyDHCP + TFTP)   NFS root   Rafe app  │
   (no SD card) ────┤  ├─ answers "Raspberry Pi Boot"  /srv/nfs  /api/    │
   RTL-SDR          │  └─ serves kernel/root over TFTP+NFS      nodes     │
        │           └────────────────────────────────────────────────────┘
        │  netboot: DHCP → TFTP(firmware/kernel) → NFS(root)
        │  then: rafe-collector.service starts the assigned collector
   Inmarsat / AIS / rtl_tcp(DATV,TETRA) ──heartbeat + data──▶ Rafe / iPad

The router keeps doing DHCP. This design uses proxyDHCP — dnsmasq never assigns an IP and never touches leases; it only adds the boot parameters when a Raspberry Pi asks. It cannot disrupt the LAN's existing DHCP. As extra insurance it only answers the official Raspberry Pi MAC OUIs.


1. One-time server setup (on prod)

From the repo checkout on the server:

sudo bash deploy/pxe/install-pxe-server.sh      # dnsmasq proxyDHCP + TFTP + NFS
sudo COLLECTOR_EXTRAS=satdump,aiscatcher \
     bash deploy/pxe/build-rpi-root.sh          # build the shared Pi netboot root

install-pxe-server.sh auto-detects the LAN interface/subnet and the server IP, writes /etc/dnsmasq.d/rafe-pxe.conf, validates it (dnsmasq --test) before restarting, and records everything in /etc/rafe-pxe/config.

build-rpi-root.sh builds two shared roots so every Pi model is covered, customising each in a qemu chroot (rtl-sdr, the collector agent, and — if COLLECTOR_EXTRAS lists them — SatDump and AIS-catcher):

  • /srv/nfs/base-arm64 + /srv/tftp/boot-shared-arm64 — 64-bit, Pi 3+
  • /srv/nfs/base-armhf + /srv/tftp/boot-shared-armhf — 32-bit, any Pi

A single Raspberry Pi OS boot partition already carries the firmware and every kernel (kernel.img ARMv6, kernel7.img/kernel7l.img ARMv7, kernel8.img arm64) plus every model's DTB, so the bootloader auto-selects per SoC. register-node.sh picks the arch per node.

Overrides: TFTP_ROOT, NFS_ROOT, SUBNET, RAFE_URL (server installer); ARCH="arm64 armhf", COLLECTOR_EXTRAS (root builder). Downloaded images are cached in /var/lib/rafe-pxe/images.


2. Enable network boot on each Pi (one-time, per Pi)

Raspberry Pi network boot is not BIOS PXE — it lives in the bootloader, and the mechanism differs by generation. Every model is supported:

Model SoC / arch How it netboots Default arch
Pi 5, 500, CM5 BCM2712 arm64 EEPROM BOOT_ORDER ⊃ 2 (no card) arm64
Pi 4, 400, CM4 BCM2711 arm64 EEPROM BOOT_ORDER ⊃ 2 (no card) arm64
Pi 3B+, 3B, CM3 BCM2837 arm64 SD boot shim (or Pi 3 OTP bit) arm64
Zero 2 W BCM2710 arm64 SD boot shim (USB-Ethernet) arm64
Pi 2B (v1.2) BCM2837 arm64 SD boot shim arm64
Pi 2B (v1.1) BCM2836 ARMv7 SD boot shim armhf
Pi 1 A/B/A+/B+, CM1 BCM2835 ARMv6 SD boot shim (USB-Ethernet) armhf
Zero / Zero W BCM2835 ARMv6 SD boot shim (USB-Ethernet) armhf
  • EEPROM models (Pi 4/5/CM4/CM5) — enable network boot in the EEPROM, then no SD card is needed at all:
    sudo rpi-eeprom-config --edit
    #   BOOT_ORDER=0xf21     # SD(1), USB(4)… NETWORK(2), repeat(f)
    #   (use 0xf2 to prefer network first)
    
  • Everything older (Pi 1/2/3/Zero) — write the one-file netboot SD shim once; the card holds only bootcode.bin and the Pi fetches the rest over the network. The whole OS + config still lives centrally on the server.
    sudo bash deploy/pxe/make-boot-shim.sh /dev/sdX     # ⚠ erases the card
    
    (Pi 3/3B+ can instead use the permanent OTP boot bit — program_usb_boot_mode=1 — and run cardless; the shim is the universal, no-OTP option that also covers Pi 1/2/Zero. Pi 1/Zero have no Ethernet, so add a USB-Ethernet adapter + hub for the RTL-SDR.)

Find a Pi's serial (the TFTP directory key) from the Pi (grep Serial /proc/cpuinfo, last 8 hex) or from the server's dnsmasq log when it first tries to boot (journalctl -u dnsmasq -f shows the requested <serial>/…).


3. Register a node and assign what it collects

# roof Pi doing Inmarsat STD-C:
sudo bash deploy/pxe/register-node.sh a1b2c3d4 inmarsat \
     freq=1541450000 gain=40 name=roof-inmarsat

# a Pi relaying IQ so the server runs the native DATV decoder (iPad feed):
sudo bash deploy/pxe/register-node.sh 5e6f7a8b datv \
     freq=741500000 rate=2000000 name=shed-datv

# ships:
sudo bash deploy/pxe/register-node.sh 9c0d1e2f ais name=mast-ais

This creates the node's per-serial TFTP dir (shared boot files symlinked in + a per-node cmdline.txt pointing at its NFS root), its writable NFS root (/srv/nfs/nodes/<serial>), and its collector config (/etc/rafe-collector/node.yml). Re-run with a different role to reassign; add --update to also re-sync the base OS into the node root after a rebuild.

Roles (deploy/pxe/rpi-collector/node.example.yml documents every key):

role what runs on the Pi how Rafe uses it
inmarsat SatDump STD-C/AERO decode messages posted to Rafe
ais AIS-catcher → NMEA UDP server ingests on :10110
datv rtl_tcp relay server runs native DVB-S/S2 → iPad
tetra rtl_tcp relay server runs native TETRA decode
adsb dump1090 --net (or relay) server pulls BEAST/JSON
rtltcp bare rtl_tcp server drives any native SDR mode
none idle registered but unassigned

4. Boot it

Plug the RTL-SDR into the Pi, connect Ethernet, power on with no SD card (Pi 4/5) or the bootloader-only card (Pi 3B+). It DHCPs (IP from your router), TFTPs the firmware/kernel from <serial>/, NFS-mounts its root, and rafe-collector.service starts the assigned collector. Within ~15 s the node appears in Rafe's COLLECTOR NODES panel (under the digimode pane) showing ● name role freq status @endpoint.

The agent identifies the node by CPU serial, runs the collector, restarts it on failure with backoff, and heartbeats to POST /api/nodes/heartbeat. Authenticated users see the fleet via GET /api/nodes.


3b. WiFi nodes — a standalone SD card (no netboot)

WiFi-only Pis (Zero W, Zero 2 W, Pi 3A+) — or any Pi you'd rather run over WiFi — cannot network-boot, because the boot ROM/EEPROM only netboots over wired Ethernet. For those, write a self-contained SD card from the same built root: same collector agent, WiFi credentials and SSH key baked in. It still heartbeats to Rafe and is managed from the web app exactly like a netboot node.

sudo bash deploy/pxe/make-sd-card.sh /dev/sdX auto inmarsat \
     wifi_ssid=MyNet wifi_pass=secret wifi_country=GB \
     freq=1541450000 gain=40 name=roof-inmarsat
# …or write a flashable image instead of a card:
sudo bash deploy/pxe/make-sd-card.sh roof.img auto datv \
     wifi_ssid=MyNet wifi_pass=secret arch=arm64

Use auto for the serial to let the agent use the Pi's CPU serial (it appears in the app after first boot). Reassigning a WiFi node from the web app works because the agent pulls its assignment from the server (/api/nodes/<serial>/desired) — so a role/frequency change in the UI is applied without reflashing.

4. SSH management (all images)

Every image — netboot roots and SD cards — enables SSH and installs a management user rafe authorised by a key created (or supplied) at install time:

# provide your own key…
sudo RAFE_SSH_PUBKEY="$(cat ~/.ssh/id_ed25519.pub)" \
     bash deploy/pxe/install-pxe-server.sh
# …or let it generate one (printed; private key at /etc/rafe-pxe/manage_key)

Then, on any node:

ssh -i /etc/rafe-pxe/manage_key rafe@<node-ip>

Rebuild the roots after changing the key so it's baked in (build-rpi-root.sh); re-flash SD cards likewise.

4a. Manage from the web app

Instead of the CLI, you can register, reassign, reboot and remove nodes from the COLLECTOR NODES panel in the app. This is opt-in — it grants the web-app user passwordless root for exactly one tightly-validated helper (rafe-nodectl), so enable it deliberately:

sudo ENABLE_WEB_MANAGE=1 bash deploy/pxe/install-pxe-server.sh

That installs /usr/local/sbin/rafe-nodectl and a locked-down /etc/sudoers.d/rafe-nodectl for the service account. Remove that sudoers file to disable web management instantly. With it on, the panel gains:

  • — add/provision a node: serial, role, freq, gain, name, arch.
  • Click a node row to load it into the form and reassign it (change role or frequency, APPLY).
  • reboot / remove per node.

Reboot/reload are delivered to the node in its next heartbeat response (no inbound SSH needed); the agent applies them within ~15 s. Every argument is validated twice — in the app (nodes_manage) and again in rafe-nodectl — so the root boundary only ever sees well-formed collector operations. All management endpoints require a logged-in user.

5. Design notes & safety

  • proxyDHCP, not DHCP. port=0 (no DNS), dhcp-range=<subnet>,proxy (no address assignment), and MAC-OUI gating mean this can only ever add a boot option for a Raspberry Pi — it cannot hand out addresses, answer DNS, or affect any non-Pi device. See deploy/pxe/dnsmasq-rpi-netboot.conf.
  • Diskless & stateless. The root is NFS; /tmp, /var/tmp, /var/log are tmpfs; the collector service runs under ProtectSystem=strict. A node can be re-imaged centrally by rebuilding the base and re-syncing (register-node.sh --update <serial> <role>).
  • Shared base + per-node root. The current implementation rsync-copies the shared base into each node root (simple and robust; ~2.5 GB/node). For a large fleet, switch to a single overlay export (lower=base ro, upper=per-node) — noted as a future optimisation.
  • The heartbeat endpoint is unauthenticated (LAN nodes can't hold a session) but is status-ingest only — no control surface — and is size/count-capped (app/nodes.py). Read-back (/api/nodes) requires a login.
  • One dongle, one job. Each Pi's SDR does one role at a time (as with the server's local SDR). Use more Pis for more simultaneous captures.

6. Troubleshooting

  • journalctl -u dnsmasq -f on the server — watch the Pi's TFTP requests; the requested path names the serial to register.
  • On the node: journalctl -u rafe-collector -f, and check the dongle with rtl_test -t.
  • SatDump/AIS-catcher missing → rebuild the root with the extra in COLLECTOR_EXTRAS; the agent's status shows missing:<binary> meanwhile.
  • Verified: shell scripts pass shellcheck; the collector agent's role command-building and the node registry are unit-tested. The netboot chain itself (TFTP/NFS/EEPROM) must be validated on real hardware — it could not be exercised from the dev machine.