The main Python application — polls ADS-B data and drives the display.
The deskradar app is a continuously-running Python process. Each polling cycle it:
| Requirement | Version |
|---|---|
| Python | ≥ 3.13 |
| Platform | Linux / POSIX (Raspberry Pi OS) |
| dump1090 | Running on the same machine, port 8080 |
cd /opt/deskradar .venv/bin/python -m app.main
nohup .venv/bin/python -m app.main >log.log 2>&1 & # Prints the PID — use it to kill the process later
sudo systemctl start deskradar sudo systemctl status deskradar sudo journalctl -u deskradar -f
The config file path defaults to /etc/deskradar/config.json. Override by passing a path as the first command-line argument:
python -m app.main /path/to/config.json
| Module | Purpose |
|---|---|
app/main.py | Main polling loop. Fetches data, runs the pipeline, sends to display, manages LCD spinner. |
app/config.py | Pydantic config model. Loads and validates config.json, computes derived fields (lat/lon bounds, display ranges). |
app/dump1090.py | Fetches JSON from the dump1090 API and filters aircraft by required fields, altitude, and geographic bounds. |
app/iterate.py | Merges new aircraft data into the per-flight store, triggers formatting, removes condemned flights, applies closest highlighting. |
app/format.py | Orchestrates the 10-stage per-track processing pipeline. Returns (blocks, is_condemned). |
app/formatters.py | All individual pipeline stages: plot, colour, deduplicate, fill gaps, smooth, fade, enforce history limits. |
app/plotting.py | Geospatial math (lat/lon bounds via geopy, pixel range arrays via NumPy) and altitude-to-RGB565 conversion. |
app/draw.py | Converts the store to a flat pixel list. Deduplicates by position (keeps highest altitude per pixel), strips internal fields. |
app/send.py | HTTP client for the MatrixPortal: /draw, /empty, /no-change. |
app/lcd.py | Formats and sends the closest aircraft callsign to the LCD service. Manages spinner animation. |
app/cleanup.py | Removes empty entries from the flight store. |
app/constants.py | Shared constants: required dump1090 fields, altitude match threshold (500 ft). |
Each flight track runs through these stages in order on every polling cycle:
| # | Function | What it does |
|---|---|---|
| 1 | clear_hidden() | Removes _hidden flags left over from the previous cycle's smoothing pass. |
| 2 | clear_closest_marker() | Resets the is_closest flag so the colour isn't permanently overridden. |
| 3 | enforce_max_history() | Trims the track to the last MAX_HISTORY points. Older points are dropped. |
| 4 | enforce_max_time() | Marks the track as condemned if the aircraft has not been seen for MAX_UNSEEN_SECS. |
| 5 | plot() | Converts each point's lat/lon to pixel coordinates using the display's configured radius and center. |
| 6 | remove_latest_dupe() | Drops the newest point if it occupies the same pixel as the previous one (within ALT_MATCH_THRESHOLD = 500 ft altitude tolerance). |
| 7 | fill_gaps() | If two consecutive points are more than one pixel apart, interpolates intermediate points to create a continuous line. |
| 8 | smooth_steps_deferred_head() | Removes 90° corners from the track by marking the corner point as hidden and re-routing around it. The "deferred head" variant avoids modifying the most recent point. |
| 9 | add_colour() | Applies altitude-to-RGB mapping to each point. Stale aircraft (unseen for UNSEEN_MARK_SECS) are colored red. |
| 10 | fade() | Multiplies each point's RGB values by FADE_FACTOR (default 0.95), gradually dimming older trail points. New points from this cycle are not faded. |
After all tracks are formatted, get_closest_key() finds the flight whose most recent point is nearest the display center (using Euclidean pixel distance). highlight_closest() then overwrites that track's colors with CLOSEST_COLOUR.
The closest flight callsign is also sent to the LCD service. The 16×2 LCD shows the callsign on line 1 with a spinning animation character in position 16.
| Endpoint | When | Payload |
|---|---|---|
POST MATRIX_HTTP_URL/draw | Pixel data changed from previous cycle | JSON array of {x, y, r, g, b} objects |
POST MATRIX_HTTP_URL/empty | No aircraft in store | Empty body |
POST MATRIX_HTTP_URL/no-change | Pixel data identical to previous cycle | Empty body (keepalive) |
POST LCD_SEND_URL/display | Every cycle (deduplicated server-side) | {line1, line2} |
Before aircraft enter the pipeline, sanitise_new_data() drops any aircraft that:
seen, lon, lat, alt_geom, hex, flightMIN_ALT (default 1,000 ft) — suppresses ground trafficLAT, LON, and DISPLAY_RADIUS_NM| Package | Use |
|---|---|
| FastAPI + Uvicorn | Not used at runtime; present for potential future HTTP server |
| Pydantic | Config model validation |
| geopy | Calculates lat/lon bounds from center + radius in nautical miles |
| NumPy | Linspace for pixel range arrays; color interpolation |
| requests | HTTP client (with session pooling) for dump1090, MatrixPortal, LCD |
| RPi.GPIO | Raspberry Pi GPIO (declared, available for future hardware use) |
| RPLCD | LCD driver library (declared; LCD communication handled via HTTP service instead) |
| pyserial | Serial communication (declared, available) |
| python-dotenv | Environment variable loading |
Tests live in tests/ and mirror the app/ module structure. Run with:
cd /opt/deskradar .venv/bin/pytest
Coverage is reported via pytest-cov. The test suite includes approximately 60 tests across 12 files covering all modules. Key fixtures in conftest.py provide a pre-validated config object, factory functions for test data blocks, and a time-freezing helper for testing time-dependent fade and timeout logic.