deskradar

The main Python application — polls ADS-B data and drives the display.

Overview

The deskradar app is a continuously-running Python process. Each polling cycle it:

  1. Fetches aircraft data from a local dump1090 instance
  2. Filters and normalises the data
  3. Maintains per-flight state across cycles (position history, trail colors)
  4. Sends the current frame as a pixel list to the MatrixPortal via HTTP
  5. Updates the LCD with the nearest aircraft callsign

Requirements

RequirementVersion
Python≥ 3.13
PlatformLinux / POSIX (Raspberry Pi OS)
dump1090Running on the same machine, port 8080

Running

Foreground

cd /opt/deskradar
.venv/bin/python -m app.main

Background (start_silent.sh)

nohup .venv/bin/python -m app.main >log.log 2>&1 &
# Prints the PID — use it to kill the process later

Via systemd (production)

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 Reference

ModulePurpose
app/main.pyMain polling loop. Fetches data, runs the pipeline, sends to display, manages LCD spinner.
app/config.pyPydantic config model. Loads and validates config.json, computes derived fields (lat/lon bounds, display ranges).
app/dump1090.pyFetches JSON from the dump1090 API and filters aircraft by required fields, altitude, and geographic bounds.
app/iterate.pyMerges new aircraft data into the per-flight store, triggers formatting, removes condemned flights, applies closest highlighting.
app/format.pyOrchestrates the 10-stage per-track processing pipeline. Returns (blocks, is_condemned).
app/formatters.pyAll individual pipeline stages: plot, colour, deduplicate, fill gaps, smooth, fade, enforce history limits.
app/plotting.pyGeospatial math (lat/lon bounds via geopy, pixel range arrays via NumPy) and altitude-to-RGB565 conversion.
app/draw.pyConverts the store to a flat pixel list. Deduplicates by position (keeps highest altitude per pixel), strips internal fields.
app/send.pyHTTP client for the MatrixPortal: /draw, /empty, /no-change.
app/lcd.pyFormats and sends the closest aircraft callsign to the LCD service. Manages spinner animation.
app/cleanup.pyRemoves empty entries from the flight store.
app/constants.pyShared constants: required dump1090 fields, altitude match threshold (500 ft).

Format Pipeline Detail

Each flight track runs through these stages in order on every polling cycle:

#FunctionWhat it does
1clear_hidden()Removes _hidden flags left over from the previous cycle's smoothing pass.
2clear_closest_marker()Resets the is_closest flag so the colour isn't permanently overridden.
3enforce_max_history()Trims the track to the last MAX_HISTORY points. Older points are dropped.
4enforce_max_time()Marks the track as condemned if the aircraft has not been seen for MAX_UNSEEN_SECS.
5plot()Converts each point's lat/lon to pixel coordinates using the display's configured radius and center.
6remove_latest_dupe()Drops the newest point if it occupies the same pixel as the previous one (within ALT_MATCH_THRESHOLD = 500 ft altitude tolerance).
7fill_gaps()If two consecutive points are more than one pixel apart, interpolates intermediate points to create a continuous line.
8smooth_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.
9add_colour()Applies altitude-to-RGB mapping to each point. Stale aircraft (unseen for UNSEEN_MARK_SECS) are colored red.
10fade()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.

Closest Aircraft

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.

HTTP Endpoints Used

EndpointWhenPayload
POST MATRIX_HTTP_URL/drawPixel data changed from previous cycleJSON array of {x, y, r, g, b} objects
POST MATRIX_HTTP_URL/emptyNo aircraft in storeEmpty body
POST MATRIX_HTTP_URL/no-changePixel data identical to previous cycleEmpty body (keepalive)
POST LCD_SEND_URL/displayEvery cycle (deduplicated server-side){line1, line2}

ADS-B Data Filtering

Before aircraft enter the pipeline, sanitise_new_data() drops any aircraft that:

Dependencies

PackageUse
FastAPI + UvicornNot used at runtime; present for potential future HTTP server
PydanticConfig model validation
geopyCalculates lat/lon bounds from center + radius in nautical miles
NumPyLinspace for pixel range arrays; color interpolation
requestsHTTP client (with session pooling) for dump1090, MatrixPortal, LCD
RPi.GPIORaspberry Pi GPIO (declared, available for future hardware use)
RPLCDLCD driver library (declared; LCD communication handled via HTTP service instead)
pyserialSerial communication (declared, available)
python-dotenvEnvironment variable loading

Tests

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.