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:
- Everything older (Pi 1/2/3/Zero) — write the one-file netboot SD shim
once; the card holds only
bootcode.binand the Pi fetches the rest over the network. The whole OS + config still lives centrally on the server. (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:
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:
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. Seedeploy/pxe/dnsmasq-rpi-netboot.conf. - Diskless & stateless. The root is NFS;
/tmp,/var/tmp,/var/logare tmpfs; the collector service runs underProtectSystem=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 -fon 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 withrtl_test -t. - SatDump/AIS-catcher missing → rebuild the root with the extra in
COLLECTOR_EXTRAS; the agent's status showsmissing:<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.