Architecture
For nerds.
System Overview
DeskRadar is split across three repositories and two runtime devices:
| Part | Runtime | Responsibility |
|---|---|---|
deskradar | Raspberry Pi | Main Python loop. Polls dump1090, filters and formats aircraft tracks, posts pixels to the MatrixPortal, and updates the LCD. |
deskradar-pi-services | Raspberry Pi | systemd units, boot-time MatrixPortal discovery, Flask configurator, NetworkManager WiFi switching, and FastAPI LCD service. |
deskradar-matrixportals3 | Adafruit MatrixPortal S3 | CircuitPython HTTP server that renders pixels to the 64x64 HUB75 matrix. |
Network Topology
DeskRadar operates in one of two network modes:
AP Mode (initial setup / fallback)
The Pi creates a WiFi access point named deskradar (password: deskradaradsb) with IP 10.42.0.50. The MatrixPortal S3 joins this AP automatically (its wifi.txt is pre-configured with these credentials). The web configurator is reachable at http://10.42.0.50:5000.
LAN Mode
Once a home WiFi network is configured, both the Pi and the MatrixPortal should join the same WiFi. The Pi is advertised as deskradar.local. has been run. The MatrixPortal advertises deskradar-portal.local from CircuitPython mDNS. The Pi boot check resolves that hostname and writes the MatrixPortal URL to /etc/deskradar/config.json.
WIP!
The web configurator can switch the Pi to WiFi and write the MatrixPortal wifi.txt only when the MatrixPortal CIRCUITPY volume is mounted under /media/deskradar/CIRCUITPY*. See Moving to LAN for manual steps.
The FlightAware web page is available from the Pi desktop on port 80 at http://localhost/.
Boot Sequence
systemd enforces this ordering on every Pi boot:
1. deskradar-lcd.service starts
- FastAPI server on 0.0.0.0:8010 ready to accept LCD messages
2. deskradar-bootstrap.service starts (oneshot, requires lcd)
- Waits 10 seconds for network to stabilise
- Resolves deskradar-portal.local, retrying every 2s for 30s
- Sends best-effort status updates to the LCD service
- Writes MATRIX_HTTP_URL as http://<matrix-ip>:80
3. deskradar.service starts (after bootstrap + network-online)
- Main aircraft tracking app reads config and polls dump1090
4. deskradar-configurator.service starts (after bootstrap + network-online)
- Flask app served by Waitress on 0.0.0.0:5000
deskradar.service and deskradar-configurator.service are ordered after the bootstrap service, but they do not require it. If mDNS resolution fails, they can still start with whatever MATRIX_HTTP_URL is currently in the config file, and the main app will log matrix connection errors until the MatrixPortal is reachable.Service Dependency Graph
network-online.target
|
v
deskradar.service
deskradar-configurator.service
^
| After
deskradar-bootstrap.service
^
| Requires
deskradar-lcd.service
| Service | Type | Restart | User |
|---|---|---|---|
deskradar-lcd | simple | always | root, by systemd default |
deskradar-bootstrap | oneshot | none configured | root |
deskradar | simple | always | root |
deskradar-configurator | simple | always | root, by systemd default |
Data Flow
Per polling cycle (every POLL_CADENCE seconds)
dump1090 API (:8080/data/aircraft.json)
|
| HTTP GET
v
fetch raw JSON
|
v
sanitise_new_data()
- drop aircraft missing required fields
- drop ground traffic or altitude below MIN_ALT
- drop aircraft outside lat/lon bounds
|
v
iterate_data() - merge new points into per-flight store
|
v
format_blocks() - 10-stage pipeline per flight
1. clear_hidden() remove deferred smoothing flags
2. clear_closest_marker() reset previous closest colour
3. enforce_max_history() trim to MAX_HISTORY points
4. enforce_max_time() pop oldest point if latest is stale
5. plot() lat/lon to pixel coordinates
6. remove_latest_dupe() drop points at same pixel (within 500ft alt)
7. fill_gaps() interpolate across gaps > 1 pixel
8. smooth_steps_deferred_head() remove right-angle corners from track
9. add_colour() altitude to RGB gradient
10. fade() dim trail colours by FADE_FACTOR each cycle
|
v
extract_draw()
- skip _hidden points
- deduplicate by pixel: keep highest-altitude point per (x,y)
|
+--> send_matrix() HTTP POST /draw to MatrixPortal
| or send_no_change() if pixel data unchanged
|
+--> manage_closest() HTTP POST /display to LCD service
(sends closest aircraft callsign + spinner)
Altitude Color Mapping
The radar_altitude_to_rgb() function maps altitude from 0 to 42,000 ft to a smooth 8-stop RGB gradient. Values below 0 clamp to white. Values above 42,000 ft clamp to red. The stop positions are percentages of that range, so the early colors occupy narrower altitude bands than the later colors.
| Approx altitude | Stop | RGB |
|---|---|---|
| 0 ft | White | 255, 255, 255 |
| 840 ft | Yellow | 255, 255, 0 |
| 4,200 ft | Green | 0, 255, 0 |
| 10,500 ft | Light blue | 0, 255, 255 |
| 21,000 ft | Dark blue | 0, 0, 255 |
| 31,500 ft | Purple | 128, 0, 128 |
| 37,800 ft | Deep purple | 160, 0, 160 |
| 42,000 ft | Red | 255, 0, 0 |
If the latest point for a track is older than MAX_UNSEEN_SECS, the formatter pops one old point per cycle, marks the visible head red, and fades the remaining trail. UNSEEN_MARK_SECS is present in the config file and UI but is not used by the current main app. The closest aircraft is recolored to CLOSEST_COLOUR only when closest highlighting is enabled and more than one aircraft is present.
LCD Display Protocol
Any service that wants to show text on the LCD sends a POST request to the LCD service on the Pi:
POST http://127.0.0.1:8010/display
Content-Type: application/json
{
"line1": "BAW123",
"line2": ""
}
The LCD service deduplicates: if the message hasn't changed it returns {"status": "ok", "skipped": true} and does not re-send I2C commands.
MatrixPortal HTTP API
The MatrixPortal firmware serves HTTP on port 80 after joining WiFi. It advertises itself as deskradar-portal.local and keeps a 60 second request watchdog. Any valid request to /ping, /draw, /empty, /no-change, or /add-constant resets that watchdog.
| Endpoint | Method | Purpose |
|---|---|---|
/ping | GET | Returns the MatrixPortal IP address as plain text. |
/draw | POST | Accepts a JSON list of x, y, r, g, and b pixels, clears the bitmap, draws those pixels, and redraws constant centre pixels. |
/empty | POST | Clears the display and redraws the constant centre pixels. |
/no-change | POST | Confirms the Pi is still alive when the pixel payload has not changed. |
/add-constant | POST | Adds extra constant pixels that are redrawn after each refresh. |