Conversation
hahuang65
commented
Jun 18, 2026
Member
- FIX: render via the active matrix backend
- FIX: reuse the canvas returned by SwapOnVSync
- FEATURE: make LED panel tuning configurable
- CHORE: add LED panel diagnostic scripts
- DOCS: record panel bring-up findings in HARDWARE.md
- FIX: verify HTTPS against the system CA bundle
- FEATURE: add Raspberry Pi deployment tooling
- FIX: default row_address_type to 0 and expose panel scan knobs
- FIX: render every row red to work around the faulty panel
- DOCS: add new-panel verification checklist and fault finding
- FIX: handle empty aircraft list in refresh_flight
- FIX: keep the render loop alive when a fetch cycle fails
- FIX: fetch on a background thread so the display never freezes
- CHORE: rename Pi run targets and run them locally on the Pi
- CHORE: add route-accuracy comparison tool
- FEATURE: switch flight data to a single AirLabs source
- CHORE: exclude .pytest_cache and .ruff_cache from deploy
- CHORE: remove one-off panel-diagnostic scripts
- CHORE: post-migration tidy (dotenv dep, docstring, README targets)
- FIX: cycle the LOADING animation instead of freezing on "LOADING"
- FEATURE: run jetset as a boot-enabled systemd service
- FEATURE: airline logos on flight cards
- FIX: harden Pi deploy and the jetset systemd service
- FIX: render airline logos on the Pi panel
- CHORE: add LED panel logo diagnostic probes
- FEATURE: restore full-colour rendering on a properly-powered panel
- FEATURE: switch airline logo provider to the RadarBox set
- FEATURE: backfill missing RadarBox logos from the FlightAware set
- REFACTOR: drop dead panel tuning and centralize matrix setup
- REFACTOR: global logo dir from config; project-wide lint clean
- REFACTOR: extract a Renderer class owning the canvas and logo dir
- CHORE: add bin/ for ops tooling and a bin/script helper runner
- CHORE: Add make target for easy SSH-ing to the Pi
- FIX: replace black-silhouette logos with a colour source
- STYLE: tighten metric labels and rework vertical-rate display
- CHORE: run ty in CI via make lint/test targets
The renderer hardcoded `from RGBMatrixEmulator import graphics`, so on real hardware it drew text with the emulator's graphics engine onto an rgbmatrix canvas. The two libraries' internals differ, which garbled all output on the panel while the emulator looked perfect (there both halves came from the emulator). Add a single backend selector (backend.py) that imports RGBMatrix, RGBMatrixOptions, and graphics from rgbmatrix on hardware and from the emulator otherwise, so the matrix and the drawing engine can never drift apart. A regression test pins renderer.graphics to the active backend. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
SwapOnVSync is double-buffered on hardware: it displays the given canvas and returns the now-offscreen buffer to draw the next frame into. The loop discarded that return value and kept drawing into the same object, so on the panel it ping-ponged between two physical buffers while only ever updating one - producing flashing and old frames showing through. The emulator has one effective buffer, so it was invisible there. Thread the returned canvas back through _render_frame/_render_loading and reassign it each loop iteration, matching the library's canonical usage. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Expose panel_type, gpio_slowdown, and rgb_sequence via the hardware section of the config YAML (and JETSET_CONFIG), instead of hardcoding them. main() now loads config first and applies hardware-only options behind IS_HARDWARE so the emulator path is unaffected. Defaults reflect hardware bring-up findings: - gpio_slowdown = 5: the Pi 3 A+ / Adafruit HAT needs 5 for a stable signal; 4 produced intermittent corruption that looked like scrambled pixels. - rgb_sequence = "RGB": the standard order; override per-panel if colors come out swapped. - panel_type = "": no chip-specific init by default. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Standalone scripts used to bring up and characterize a HUB75 panel directly via rgbmatrix, isolating hardware behavior from the app: backend/geometry probes, multiplexing and row-address sweeps, color-channel and rgb-sequence tests, and GPIO-slowdown stability checks. Useful for validating the next panel quickly. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Capture the hardware lessons from debugging so they are not relearned: gpio_slowdown=5 is required on the Pi 3 A+ / Adafruit HAT (4 corrupts static images), use indoor standard 1/16-scan panels and avoid outdoor multiplexed ones, rgb_sequence defaults to RGB, the original test panel was defective, and the scripts/probe-* tooling characterizes a new panel. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
On the Pi the bundled certifi store did not match the OS trust store, breaking TLS verification for API calls. Detect a CA bundle from REQUESTS_CA_BUNDLE / SSL_CERT_FILE or common system locations and use it for the session's verify setting, falling back to the default when none is found. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Provide the path from dev machine to running on the Pi: - Makefile targets: deploy (rsync), setup-pi (remote deps + build rpi-rgb-led-matrix), start / debug-start (run over SSH with GPIO root). - scripts/setup-pi.sh: installs system deps, adds swap, builds the rgbmatrix wheel, and adds it as a project dependency. - scripts/jetset.service: systemd unit to run the display on boot. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
A leftover experimental row_address_type=1 in main() collapsed the panel to two horizontal lines on hardware. Restore the standard 1/16-scan value 0, and make row_address_type and multiplexing configurable (defaults 0/0) alongside the other hardware knobs so they no longer need code edits. Also set the rgb_sequence default back to "RBG" for the panel currently deployed; a standard panel uses "RGB" (verify with scripts/panel-colors.py). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The deployed panel suppresses other color channels whenever red is present in a frame (and its blue channel is dead), so multi-color cards render with missing rows. Draw all four rows red so the live display is fully readable on this panel. Temporary workaround: when a standard panel is installed, switch the rows back to ORANGE/CYAN/GREEN/BLUE (the constants are still defined) and set rgb_sequence to "RGB". See HARDWARE.md. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Record that the original panel is a faulty unit (not an incompatibility) - it addresses correctly and drives red perfectly; only its color channels are defective. Add a step-by-step checklist for verifying a replacement panel (channels, geometry, restore palette, live app), including the test that tells HAT/cable faults apart from panel faults. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The /callsign endpoint can return valid JSON with an empty "ac" list when the aircraft is no longer airborne. Indexing [0] on it raised an uncaught IndexError that crashed the render loop. Check the list is non-empty before indexing and otherwise return the original flight, matching the documented "show stale data rather than nothing" behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
An unexpected error during fetch (anything not already handled inside nearby_flights/refresh_flight) propagated out and crashed the whole display. Wrap the fetch in _safe_fetch: log the error and keep rendering existing flights. last_fetch is advanced on failure so a persistent outage backs off to the normal refresh interval instead of retrying every frame. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Fetching (nearby flights + sequential route lookups + stale refreshes) ran synchronously on the render loop and could take ~30s, freezing the display on one frame the whole time. Move fetching to a daemon thread that refreshes the shared buffer on the refresh interval, while the render loop only renders and keeps cycling smoothly. The buffer is guarded by a lock held solely for fast mutations - network calls happen outside it, so the render thread never blocks on I/O. KeyboardInterrupt signals the fetch thread to stop before clearing the panel. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Rename start / debug-start to run-pi / debug-pi (parallel to the local run / debug targets), and run them — plus setup-pi — directly instead of over SSH, since work now happens on the Pi itself. deploy still rsyncs from a dev machine. Update the make-target references in setup-pi.sh and HARDWARE.md. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
scripts/route-compare.py pulls nearby flights through the normal pipeline and compares each callsign's route across adsbdb (ours), AirLabs, and hexdb.io, scored against AeroAPI as ground truth. AeroAPI is opt-in via --truth (it costs ~$0.05/call); the default run uses only free sources. Adds python-dotenv (dev) so keys load from a .env file. Used to measure route accuracy: adsbdb proved unreliable while AirLabs matched AeroAPI, motivating the route-source switch. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Replace the adsb.lol + adsbdb + hexdb + plausibility-filter stack with one AirLabsAdapter: a single /flights?bbox= call returns every nearby flight with positions, metrics, AND route, so no route lookup, cross-track plausibility check, or second source is needed (adsbdb measured ~20% correct; its great-circle filter had a bug — see README's data-source history). - App refreshes every 45 min (one bbox call ≈ 960/month, within the free tier) and replaces the buffer wholesale; the display slides a window of 5 across all captured flights so fresh aircraft trickle in between refreshes. - FlightBuffer is now a plain list (capture-all, no bounded deque/push/dedup). - Retire the old pipeline: delete geo.py, AdsbLolAdapter, RouteCache, and the FlightRoute geo helpers; repoint route-compare and save_fixtures to AirLabs. - Display: show the full altitude (e.g. "18238 ft") and a cardinal heading (e.g. "270° W") to distinguish track from temperature. - AIRLABS_API_KEY loads from the environment or a .env file. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
These local tooling caches don't belong on the Pi; add them to the deploy rsync excludes alongside .venv/__pycache__/wheels. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The probe/sweep scripts written to characterize the original faulty panel (color channels, multiplexing, scan, signal stability) are no longer needed now that the panel is diagnosed. Keep only panel-colors.py and probe-text.py for bringing up a replacement panel, route-compare.py for data accuracy, and the deploy/fixture tooling. Git retains the removed scripts. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
- Move python-dotenv from dev to main dependencies: __main__ calls load_dotenv() at runtime to read AIRLABS_API_KEY, so it is a runtime dep (a --no-dev sync would otherwise drop .env support). - Update the stale __main__ module docstring (no longer an emulator smoke test). - Refresh the README Targets table with the current Make targets (deploy, setup-pi, run-pi, debug-pi, fixtures, debug). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
_render_loading passed the raw frame counter to loading_label, which only returns dotted variants for pages 1-3 and "LOADING" otherwise — so once the frame passed 3 the animation froze. The threaded render loop free-runs while the buffer is empty (startup, or no flights returned), making it obvious. Pass frame % 4 so the dots keep cycling, matching the flight-card paging. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
- Render jetset.service.template → unit at install (@HOME@ → $HOME); systemd can't expand $HOME and %h is /root for a root service. - setup-pi.sh installs + enables the service (boot); make deploy auto-runs setup-pi.sh when the unit's missing, then restarts. - Build fixes: clone into $HOME, Cython into the venv, drop unused system cython3/python3-pil.
Render an airline's logo (cached under JETSET_LOGO_DIR) on each flight card; scaled once and memoized, drawn as red silhouettes while MONOCHROME_RED is on for the faulty panel. The systemd unit gains JETSET_LOGO_DIR and pillow becomes a runtime dependency.
deploy re-adds the rgbmatrix wheel via an absolute uv path (uv isn't on a non-interactive ssh PATH) so a synced dev pyproject can't drop the hardware backend and silently fall back to the emulator; backend warns when rgbmatrix is present but fails to import; extract install-service.sh and run it every deploy so unit/env changes propagate without re-running setup-pi.
Logos were loading as None on the running display: rpi-rgb-led-matrix drops privileges to the 'daemon' user after GPIO init, and daemon can't traverse /home/pi (mode 700), so load_logo (called lazily in the render loop) failed. Fonts escaped this because they load at import, before the matrix drops privileges. Keep the service as root (drop_privileges=False) — it already runs as User=root and needs root for GPIO. Also tune the logo for the panel: size it 23x23 in the top-right with a 1px margin off the top and right edges, and floor luminance at 120 so dark airline colors clear the panel's low-PWM (pwm_bits=6) lighting threshold.
The hardware probes used to track down the logo failures, kept alongside panel-colors.py and probe-text.py for the next panel: - probe-logo.py: render a logo at varying brightness/floor to tune visibility - probe-card.py: render the full card through render_flight_card on hardware - diag-logo.py: compare logo-load access as root vs the pi user (found the drop_privileges bug)
The panel was never faulty — it had been running on parasitic ribbon power (the HUB75 ribbon carries data + ground only, not the LEDs' 5V), which starved the higher-forward-voltage channels and mimicked a dead blue / white-collapses-to-red panel. Powered through its own VH4 header, every channel renders true. - renderer: MONOCHROME_RED = False — full ORANGE/CYAN/GREEN/BLUE palette and full-colour logos - config: hardware_rgb_sequence = "RGB" (standard panel, verified) - panel-colors.py: take gpio_slowdown/rgb_sequence/panel_type as args and default slowdown to 5 (the hardcoded 4 was itself unstable on the Pi 3 A+) - HARDWARE.md: replace the faulty-panel notes with the power-header lesson - tests: rows assert the colour palette; rgb_sequence default is RGB
Source logos from Jxck-S/airline-logos (radarbox_logos) instead of
sexym0nk3y/airline-logos. Same convention — ICAO-keyed PNGs — so the
{ICAO}.png lookup and the renderer are unchanged; only the download URLs
move. Switching on a Pi needs the cache cleared first (the downloader
skips existing files): rm -rf ~/.cache/jetset/logos, re-run
download_logos.py, restart the service.
Also add scripts/logo-gallery.py — cycles every cached logo on the panel
(ICAO at the callsign spot, logo top-right, N seconds each, optional
prefix filter) to review the whole set without waiting for a live match.
RadarBox is missing some airlines (e.g. ASA/Alaska). Process logo
sources in priority order — RadarBox, then FlightAware — and since the
download skips files that already exist, RadarBox stays preferred while
FlightAware only fills the gaps. Same ICAO {code}.png naming, so the
lookup and renderer are unchanged.
With the panel healthy on proper power, the hardware-tuning knobs and the monochrome-red fallback were dead weight. - __main__/config: remove gpio_slowdown, multiplexing, row_address_type, panel_type, pwm_bits, led_rgb_sequence, disable_hardware_pulsing and the config fields behind them; only adafruit-hat + drop_privileges remain - backend: add build_matrix() — one place for panel setup so the app and the scripts/ probes never drift - renderer: remove the MONOCHROME_RED / LOGO_RED_FLOOR fallback; always full colour - scripts: panel-colors/probe-text/logo-gallery use build_matrix(); delete the obsolete probe-logo/probe-card/diag-logo diagnostics - docs: strip the removed knobs from README/HARDWARE
Two cleanups that overlap in renderer/tests, landed together. Config-driven global logo directory: - AppConfig gains logo_dir, defaulting to the global /var/lib/jetset/logos (not a user home), so the root service and the deploy-user downloader resolve the same path - thread logo_dir App -> render_flight_card -> render_logo -> load_logo; download_logos and logo-gallery read it from AppConfig - drop the JETSET_LOGO_DIR env, the SUDO_USER juggling, and the home-cache default; the systemd unit no longer sets JETSET_LOGO_DIR and setup-pi.sh creates /var/lib/jetset/logos owned by the deploy user Lint clean across the whole project (ruff . + ty .): - run ruff and ty over . instead of src/ only - exclude .venv from ty; justified inline ignores for the rgbmatrix conditional import, the hardware-only drop_privileges attr, and the emulator's mistyped Font.CharacterWidth stub - real type fixes: Image.Resampling.LANCZOS, cast the RGBA getpixel tuple, widen the scaled annotation, http base_url as an instance attr (not ClassVar), route-compare dotenv no-op fallback - narrow Optional access in tests with assert ... is not None
Most render functions threaded canvas + logo_dir through every call. Make them a Renderer that holds both as state, instantiated by App. - Renderer(matrix, logo_dir) owns the double-buffered canvas; flight_card() and loading() take neither canvas nor logo_dir, and present() encapsulates the VSync swap (clear() blanks the panel) - App.loop() builds the matrix + Renderer and the per-frame helpers (_render_frame/_render_loading/_after_render) collapse into the loop; __main__ just calls app.loop() - _scaled_logo stays a module-level lru_cache function (a cache on a method would key on self and leak instances); draw_text stays a module helper - logo-gallery renders via renderer.flight_card(Flight(callsign=code)) - tests drive the Renderer/loop directly instead of the removed free functions
- bin/script: run a scripts/ helper as root for GPIO on the Pi (stops the jetset service for the run, restarts it on exit). e.g. bin/script logo-gallery 3 UAL - move setup-pi.sh and install-service.sh from scripts/ to bin/ (ops/lifecycle tooling); scripts/ keeps the domain helpers (probes, download_logos, route-compare, save_fixtures) - update Makefile + setup-pi.sh references to the new paths
RadarBox is the preferred logo set, but it ships a handful of pure-black silhouette placeholders (e.g. ASA). The colour renderer skips black pixels, so those airlines drew no logo at all — and the downloader's skip-if-exists rule meant FlightAware's real colour logo never replaced them. Treat an all-black logo as a gap: a file already on disk is kept only if it is usable, so a later source supplies a real colour logo. Re-running healed 10 silhouettes that were silently blank.
The 5x7 panel font is monospace, so a single space is a full character cell and read as an oversized gap between each value and its unit. Drop the spaces (35000ft, 450kn, 270°W) so units sit against their numbers. Vertical rate: move the trend marker before the number and add a level marker, so the three states read in parallel — ▲1500ft/min, ▼1200ft/min, ▬0ft/min — replacing the standalone LVL. Spell the unit ft/min, and use the ISO knot symbol kn for speed.
CI ran ruff check src/ only, so type errors and lint issues outside src/ (scripts/, tests/) slipped past CI even though make lint catches them locally. Point the CI lint and test jobs at the make targets so CI and local checks share one definition and can't drift. Add ty to the dev dependency group so uv sync installs it in CI.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.