Mender-enabled OS images for Raspberry Pi 5 radar nodes. The image comes pre-loaded with Docker, SDRplay API, Avahi mDNS, and a WiFi captive portal for easy setup.
- Docker CE with Compose plugin
- SDRplay API v3.15 for RSPDuo hardware
- SDRconnect v1.0.5 for standalone SDR analysis
- Chrony for NTP clock disciplining
- Cloudflared for secure tunneling
- Avahi mDNS for
ret<node_id>.localper-node discovery, plus a sharedowl.local - WiFi Connect captive portal for network setup
- Mender client for OTA updates
-
Flash the image to an SD card (64GB+) using Raspberry Pi Imager
- Download from Releases
- Select
owl-os-vx.x.x.imgas custom OS - Do not apply OS customisation settings
-
Boot and connect to WiFi
- Connect to the
node-setupWiFi network - Captive portal opens automatically (or go to http://192.168.42.1)
- Enter your WiFi credentials - node reboots and connects
- Connect to the
-
Accept device in Mender
- Node appears as "pending" once online
- Accept to enable OTA updates
-
Deploy retina-node stack via Mender OTA
Cloud services (mender-authd, mender-updated, mender-connect) are handled differently between the two build artifacts:
.img(fresh flash) — cloud services are disabled by default.mender.confis backed up to/data/mender-cloud-disabled/and the three Mender systemd services are masked. The user must consent via the retina-gui install flow to enable them..mender(OTA artifact) — cloud services are re-enabled viadebugfsin theimage2menderpost-processing step. This is required becausemender-updatedmust start after reboot to runArtifactVerifyRebootand commit the update — without it the update hangs and rolls back.
After install, users can toggle cloud services on/off from http://owl.local. The preference persists across reboots and OTA updates. Toggling is blocked while any update is in progress.
During a retina-node install, retina-gui holds an install.lock in /data/retina-gui/ to prevent concurrent installs and block cloud service toggling mid-update. The lock is released on completion (success or failure) with a 40-minute stale timeout as a safety net. Mender state scripts write a separate mender-update.status file for real-time progress polling (downloading → installing → done).
OTA deployments of the retina-node stack run through a fork of Mender's packaged
docker-compose Update Module. The packaged file is moved aside with
dpkg-divert rather than overwritten, so apt stays consistent and the upstream
version is kept for comparison. The fork changes four things:
- Images are loaded before the running stack is stopped. The packaged module stops first and loads second, which held the radar down for the length of the image load. Loading first cut a measured outage from around 148 seconds to about 11.
- It takes the restart lock in
/data/retina-gui/restart.lock, so an update cannot interleave with a restart started by retina-gui or the RSPduo watchdog. - A failed install no longer stops the stack it was replacing. The packaged
module ran
downagainst the new composition while the old one was still live, and because both share one compose project name that took the running stack down with it. - A rollback with nothing staged does nothing. See below.
rollback_artifact can be entered in four different store states, and what is
safe depends on which. The store lives at /data/mender-docker-compose.
| State | Directories present | Correct action |
|---|---|---|
| Nothing staged (e.g. Download cut short) | current/ |
leave the running stack alone |
After commit, before Cleanup |
current/ + cleanup/ |
rotate, stop the new one, restart the old |
| Install failed after the rotation | new/ + previous/ |
stop new/, restart previous/ |
| Install cut short before the rotation | new/ + current/ |
leave the running stack alone |
The first row is the one upstream gets wrong, and it is not hypothetical: a power
cut during the Download phase left a node with no stack at all. Upstream's
post-commit branch only asks whether new/ and previous/ are absent, which is
equally true when nothing was ever staged. It renames the live current/ to
new/ and stops it, nothing is restored, and the module still exits 0, so the
deployment is recorded as successfully rolled back on a node that is no longer
running anything.
cleanup/ is what separates the two: commit_artifact leaves the superseded
composition there, so a genuine post-commit rollback always has it. The guard
therefore returns early only when new/, previous/ and cleanup/ are all
absent and current/ is present.
After deploying retina-node, visit http://owl.local to configure capture settings, location, ADS-B truth source, and tar1090. See retina-node for details.
Each node has a permanent name of its own, ret<node_id>.local, derived from the Mender node_id and unchanged for the life of the board. It is shown under Configuration > This node and is the address to bookmark.
owl.local is a shared entry point published by every node at once, so it reaches whichever one answers first. With a single node on the network it redirects to that node. With more than one it shows a list of every node found, each linking to its own ret<node_id>.local. A node dropping off the network does not take owl.local with it — the others were already answering it.
Use owl.local to find a node, and ret<node_id>.local to work with one. In particular do not use owl.local for SSH: it can resolve to a different node between connections, which will trip REMOTE HOST IDENTIFICATION HAS CHANGED.
Nodes can be given a friendly name under Configuration > This node. It is only a label for the node list, so renaming never breaks a bookmark or an SSH config. That section also shows the node's own ret<node_id>.local address.
To enable Cloudflare tunnel forwarding, create a token file on the node:
sudo mkdir -p /data/cloudflared
echo "YOUR_TUNNEL_TOKEN" | sudo tee /data/cloudflared/tunnel-token
sudo chmod 600 /data/cloudflared/tunnel-token
sudo systemctl restart cloudflaredThe token persists across OTA updates.
End users: Add your SSH key via the web GUI at http://owl.local after boot. Once added, connect with:
ssh node@ret<node_id>.local
# or by IP
ssh node@<ip-address>Always SSH to the node's own ret<node_id>.local, never to owl.local — that name is answered by every node on the network, so which host you land on can change between connections and SSH will refuse on the host key mismatch. Configuration > This node shows its address.
Keys persist across reboots and OTA updates.
Developers: Public keys can be baked into the image at build time by adding them to ssh_pub_keys/:
cp ~/.ssh/id_ed25519.pub ssh_pub_keys/yourname.pubTag a commit with os-vx.x.x and push:
git tag os-v1.0.0
git push origin os-v1.0.0This triggers the GHA workflow (.github/workflows/build_os.yml) which:
- Builds OS image and Mender artifact
- Uploads to GitHub Releases
- Uploads Mender artifact to OffWorld Lab Mender server
Note: Currently triggers on any
os-v*tag. TODO: Change to only PR merges into main.
Run in server mode (headless device):
/opt/sdrconnect/SDRconnect --serverConnect from a SDRconnect client on another machine using the Pi's IP.
Warning: Conflicts with blah2 - stop containers first:
cd /data/mender-docker-compose/current/manifests && docker compose -p retina-node down
For GitHub Actions: tenant token is added via GH secrets.
For local builds: Create a custom config file:
echo 'mender_tenant_token: "YOUR_TOKEN_HERE"' > configuration/mender/mender_custom.ymlThis file is .gitignored to prevent accidental commits.
Install EDI and dependencies:
Ubuntu 24.04 or newer:
sudo apt install buildah containers-storage crun curl distrobox \
dosfstools e2fsprogs fakeroot genimage git mender-artifact \
mmdebstrap mtools parted python3-sphinx python3-testinfra \
podman rsync zerofreeedi -v project make owl-os-pi5.ymlOutput artifacts:
owl-os-vx.x.x.img- Flashable OS image with A/B partitioningowl-os-vx.x.x.mender- OTA update artifact
edi -v project clean owl-os-pi5.yml- Username:
node - Password:
raspberry