Architecture

For nerds.

System Overview

DeskRadar is split across three repositories and two runtime devices:

PartRuntimeResponsibility
deskradarRaspberry PiMain Python loop. Polls dump1090, filters and formats aircraft tracks, posts pixels to the MatrixPortal, and updates the LCD.
deskradar-pi-servicesRaspberry Pisystemd units, boot-time MatrixPortal discovery, Flask configurator, NetworkManager WiFi switching, and FastAPI LCD service.
deskradar-matrixportals3Adafruit MatrixPortal S3CircuitPython 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
ServiceTypeRestartUser
deskradar-lcdsimplealwaysroot, by systemd default
deskradar-bootstraponeshotnone configuredroot
deskradarsimplealwaysroot
deskradar-configuratorsimplealwaysroot, 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 altitudeStopRGB
0 ftWhite255, 255, 255
840 ftYellow255, 255, 0
4,200 ftGreen0, 255, 0
10,500 ftLight blue0, 255, 255
21,000 ftDark blue0, 0, 255
31,500 ftPurple128, 0, 128
37,800 ftDeep purple160, 0, 160
42,000 ftRed255, 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.

EndpointMethodPurpose
/pingGETReturns the MatrixPortal IP address as plain text.
/drawPOSTAccepts a JSON list of x, y, r, g, and b pixels, clears the bitmap, draws those pixels, and redraws constant centre pixels.
/emptyPOSTClears the display and redraws the constant centre pixels.
/no-changePOSTConfirms the Pi is still alive when the pixel payload has not changed.
/add-constantPOSTAdds extra constant pixels that are redrawn after each refresh.