How the components connect, the boot sequence, and the data flow.
DeskRadar operates in one of two network modes:
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.
Laptop/Phone ──── WiFi (SSID: deskradar) ────► Pi (AP, 10.42.0.50)
│
MatrixPortal S3
also on this AP
Once a home WiFi network is configured via the web UI, both the Pi and the MatrixPortal join it. The Pi's boot service resolves deskradar-portal.local via mDNS to find the MatrixPortal's IP and writes it to the config file.
Home Router ──── Pi (deskradar.local)
└─── MatrixPortal S3 (deskradar-portal.local)
systemd enforces this ordering on every Pi boot:
1. deskradar-lcd.service starts
└─ FastAPI server on :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 (retries every 2s, 30s timeout)
├─ Sends status updates to LCD service throughout
└─ Writes resolved IP to /etc/deskradar/config.json as MATRIX_HTTP_URL
3. deskradar.service starts (after bootstrap + network-online)
└─ Main aircraft tracking app, reads config, polls dump1090
4. deskradar-configurator.service starts (after bootstrap + network-online)
└─ Flask web UI on :5000
deskradar.service and deskradar-configurator.service may start without a valid MATRIX_HTTP_URL. The main app will log connection errors until the MatrixPortal becomes reachable.
network-online.target
│
▼
deskradar-lcd ──────────────────────────────────┐
│ │
▼ (Requires) │
deskradar-bootstrap (oneshot) │
│ │
├──► deskradar (After) (After) ◄─┘
│ │
└──► deskradar-configurator (After) ◄─────┘
| Service | Type | Restart | User |
|---|---|---|---|
deskradar-lcd | simple | always | (default) |
deskradar-bootstrap | oneshot | on-failure | root |
deskradar | simple | always | root |
deskradar-configurator | simple | always | (default) |
dump1090 API (:8080/data/aircraft.json)
│
│ HTTP GET
▼
fetch raw JSON
│
▼
sanitise_new_data()
├─ drop aircraft missing required fields
├─ drop altitude below MIN_ALT
└─ drop aircraft outside lat/lon bounds
│
▼
iterate_data() — merge new points into per-flight store
│
▼
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() remove if unseen > MAX_UNSEEN_SECS
│ 5. plot() lat/lon → 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 90° corners from track
│ 9. add_colour() altitude → RGB (white→yellow→green→blue→purple→red)
│ 10. fade() dim trail colours by FADE_FACTOR each cycle
│
▼
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)
The main app maintains an in-memory dictionary keyed by ICAO flight callsign. Each entry is a list of data points accumulated over time:
store = {
"BAW123": [
{
"timestamp": 1745000000.0,
"hex": "400abc",
"flight": "BAW123",
"seen": 0.5,
"alt": 28000,
"lat": 51.234,
"lon": -0.456,
"x": 34,
"y": 21,
"r": 30,
"g": 80,
"b": 255
},
...
],
...
}
Points accumulate up to MAX_HISTORY. Aircraft not seen for MAX_UNSEEN_SECS are removed from the store entirely.
The radar_altitude_to_rgb() function maps altitude (0–42,000 ft) to a smooth gradient across 8 stops. Colors are linearly interpolated between stops:
| Altitude | Color | RGB |
|---|---|---|
| 0 ft | White | 255, 255, 255 |
| 6,000 ft | Yellow | 255, 255, 0 |
| 12,000 ft | Green | 0, 200, 0 |
| 18,000 ft | Light Blue | 0, 200, 255 |
| 24,000 ft | Mid Blue | 0, 80, 255 |
| 30,000 ft | Dark Blue | 0, 0, 200 |
| 36,000 ft | Purple | 150, 0, 200 |
| 42,000 ft | Red | 255, 0, 0 |
Stale aircraft (seen but not updated recently) are rendered in red regardless of altitude. The closest aircraft is recolored to CLOSEST_COLOUR (white by default).
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.
Browser → POST /config (Flask configurator)
│
▼
app.py merges form values into existing config dict
│
▼
calls update_config.py subprocess (JSON on stdin)
│
├─ writes /etc/deskradar/config.json
└─ sudo systemctl restart deskradar