Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
d6ca71e
FIX: render via the active matrix backend
hahuang65 Jun 16, 2026
135cf6f
FIX: reuse the canvas returned by SwapOnVSync
hahuang65 Jun 16, 2026
8b47af7
FEATURE: make LED panel tuning configurable
hahuang65 Jun 16, 2026
a68605b
CHORE: add LED panel diagnostic scripts
hahuang65 Jun 16, 2026
41cbf6f
DOCS: record panel bring-up findings in HARDWARE.md
hahuang65 Jun 16, 2026
87cd2c4
FIX: verify HTTPS against the system CA bundle
hahuang65 Jun 16, 2026
15801a2
FEATURE: add Raspberry Pi deployment tooling
hahuang65 Jun 16, 2026
45ca1d7
FIX: default row_address_type to 0 and expose panel scan knobs
hahuang65 Jun 16, 2026
8f30b78
FIX: render every row red to work around the faulty panel
hahuang65 Jun 16, 2026
7d7a3a4
DOCS: add new-panel verification checklist and fault finding
hahuang65 Jun 16, 2026
1915106
FIX: handle empty aircraft list in refresh_flight
hahuang65 Jun 16, 2026
6c50548
FIX: keep the render loop alive when a fetch cycle fails
hahuang65 Jun 16, 2026
a065420
FIX: fetch on a background thread so the display never freezes
hahuang65 Jun 16, 2026
23f2d89
CHORE: rename Pi run targets and run them locally on the Pi
hahuang65 Jun 16, 2026
2d468fe
CHORE: add route-accuracy comparison tool
hahuang65 Jun 16, 2026
c1c4c89
FEATURE: switch flight data to a single AirLabs source
hahuang65 Jun 16, 2026
65e425e
CHORE: exclude .pytest_cache and .ruff_cache from deploy
hahuang65 Jun 16, 2026
fbe72dd
CHORE: remove one-off panel-diagnostic scripts
hahuang65 Jun 16, 2026
e27f1c4
CHORE: post-migration tidy (dotenv dep, docstring, README targets)
hahuang65 Jun 16, 2026
0156170
FIX: cycle the LOADING animation instead of freezing on "LOADING"
hahuang65 Jun 16, 2026
78a246f
FEATURE: run jetset as a boot-enabled systemd service
hahuang65 Jun 17, 2026
c0f0239
FEATURE: airline logos on flight cards
hahuang65 Jun 17, 2026
8baacf6
FIX: harden Pi deploy and the jetset systemd service
hahuang65 Jun 17, 2026
3e6e832
FIX: render airline logos on the Pi panel
hahuang65 Jun 17, 2026
515139c
CHORE: add LED panel logo diagnostic probes
hahuang65 Jun 17, 2026
60ef0ce
FEATURE: restore full-colour rendering on a properly-powered panel
hahuang65 Jun 17, 2026
f2e7035
FEATURE: switch airline logo provider to the RadarBox set
hahuang65 Jun 17, 2026
83170b9
FEATURE: backfill missing RadarBox logos from the FlightAware set
hahuang65 Jun 17, 2026
8b161e2
REFACTOR: drop dead panel tuning and centralize matrix setup
hahuang65 Jun 18, 2026
f201610
REFACTOR: global logo dir from config; project-wide lint clean
hahuang65 Jun 18, 2026
28ab311
REFACTOR: extract a Renderer class owning the canvas and logo dir
hahuang65 Jun 18, 2026
4164f65
CHORE: add bin/ for ops tooling and a bin/script helper runner
hahuang65 Jun 18, 2026
1824868
CHORE: Add make target for easy SSH-ing to the Pi
hahuang65 Jun 18, 2026
a3b6f74
FIX: replace black-silhouette logos with a colour source
hahuang65 Jun 18, 2026
0a5d0e1
STYLE: tighten metric labels and rework vertical-rate display
hahuang65 Jun 18, 2026
cc4650d
CHORE: run ty in CI via make lint/test targets
hahuang65 Jun 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ jobs:
- name: Install dependencies
run: uv sync

- name: Run linter
run: uv run ruff check src/
- name: Run linters
run: make lint

test:
runs-on: ubuntu-latest
Expand All @@ -38,4 +38,4 @@ jobs:
run: uv sync

- name: Run tests
run: uv run pytest -v
run: make test
45 changes: 45 additions & 0 deletions HARDWARE.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,48 @@
4. Plug 5V 4A supply into HAT's barrel jack (powers panel)
5. Plug micro USB into Pi from phone charger (powers Pi)
6. SSH in, install software, run

## Panel notes & troubleshooting

Hard-won lessons from bring-up:

- **A healthy, properly-powered panel needs no tuning.** The app sets only the
`adafruit-hat` mapping and runs on rpi-rgb-led-matrix defaults (gpio_slowdown,
multiplexing, row_address_type, rgb_sequence, pwm_bits). Early bring-up *seemed*
to need gpio_slowdown 5 and an RBG sequence, but that was the power-starvation
artefact below — once the panel was powered through its own header, defaults
rendered cleanly. If a future panel ever misbehaves, those knobs live in
rpi-rgb-led-matrix; re-add the ones you need in `src/jetset/__main__.py`.
- **Use INDOOR, standard 1/16-scan panels.** They map 1:1 on the defaults.
**Avoid OUTDOOR / multiplexed panels** — their scrambled internal wiring needs
a custom multiplex-mapper compiled into rpi-rgb-led-matrix (hzeller #1640).
- **Power the panel through its OWN power header (VH4 / 4-pin), not the ribbon.**
The 16-pin HUB75 ribbon carries DATA + ground only — *not* the LEDs' 5V. Fed
only through the ribbon, the panel starves on parasitic ground current and the
symptom mimics a dead panel: red (lowest forward voltage) limps on, green
weak, blue dead, white collapses to red. We chased this as a "faulty panel"
across two panels + a new ribbon — it was always the VH4 power header not
being wired to 5V. Wire it and every channel works. Capacity isn't the issue
(the panel is ≤12W / 2.5A); a missing/starved power-header connection is.
- **Bring-up tooling:** `scripts/panel-colors.py` (solid red/green/blue/white
fills to confirm the three channels) and `scripts/probe-text.py` (the real
4-row text layout) drive a panel directly via rgbmatrix, independent of the
app. Run colors first, then text. (The many one-off probes/sweeps used to
characterize the original faulty panel were removed once diagnosed — see git
history if you need them again.)

### Verifying a new panel

When swapping in a replacement panel (reuse the same HAT + ribbon cable):

1. **Power:** wire the panel's own power header (VH4 / 4-pin) to 5V — the ribbon
does NOT carry LED power. A red-only / blue-dead / white→red panel is almost
always starved here, not faulty.
2. `make deploy`
3. **Channels:** `sudo -E env PATH=$PATH uv run python scripts/panel-colors.py`
- red→red, green→green, **blue→blue**, white→white. A *dead/dim* channel ⇒
power (step 1) or the HAT/cable. A *swapped* channel ⇒ the panel's subpixel
order differs; re-add `led_rgb_sequence` in `backend.build_matrix` to fix it.
4. **Geometry + combination colors:** `... scripts/probe-text.py white`
- four stable rows, and white renders white (no collapse-to-red / garbling).
5. **Live app:** `make run-pi` — confirm flight cards render in full colour.
41 changes: 38 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.PHONY: run debug test lint fixtures
.PHONY: run debug test lint fixtures deploy setup-pi run-pi logs-pi debug-pi

# Local dev
run: ## Run the emulator smoke test
uv run python -m jetset

Expand All @@ -10,7 +11,41 @@ test: ## Run all tests
uv run pytest -v

lint: ## Lint and format check
uv run ruff check src/
uv run ruff check .
uv run ty check .

fixtures: ## Save live AeroAPI response as test fixture
fixtures: ## Save a live AirLabs response as a test fixture
uv run python scripts/save_fixtures.py

# Pi Deployment
JETSET_USER ?= pi
JETSET_HOST ?= jetset.local
JETSET_PATH ?= /home/$(JETSET_USER)/jetset

deploy: ## rsync to the Pi, run first-time setup if needed, restart the service
rsync -avz --exclude '.venv' --exclude '__pycache__' --exclude '.git' \
--exclude '*.pyc' --exclude 'wheels' \
--exclude '.pytest_cache' --exclude '.ruff_cache' \
. $(JETSET_USER)@$(JETSET_HOST):$(JETSET_PATH)/
ssh -t $(JETSET_USER)@$(JETSET_HOST) \
"cd $(JETSET_PATH) && \
{ [ -f /etc/systemd/system/jetset.service ] || bash bin/setup-pi.sh; } && \
/home/$(JETSET_USER)/.local/bin/uv add ./wheels/rgbmatrix-*.whl && \
bash bin/install-service.sh && \
sudo systemctl restart jetset"

ssh:
ssh -t $(JETSET_USER)@$(JETSET_HOST) "cd $(JETSET_PATH)/ && exec $$SHELL -l"

setup-pi: ## Install deps and build rpi-rgb-led-matrix (run on the Pi)
bash bin/setup-pi.sh

run-pi: ## (Re)start the jetset service on the Pi
sudo systemctl restart jetset

logs-pi: ## Tail the jetset service logs on the Pi
journalctl -u jetset -f

debug-pi: ## Stop the service and run in the foreground with debug logging
sudo systemctl stop jetset
sudo -E env PATH="$$PATH" JETSET_DEBUG=1 uv run python -m jetset
40 changes: 39 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,44 @@ uv sync

| Command | What it does |
|---|---|
| `make run` | Run the display emulator at http://localhost:8888/ |
| `make run` | Run the display in the emulator at http://localhost:8888/ |
| `make debug` | Run the emulator with debug logging |
| `make test` | Run tests (pytest) |
| `make lint` | Lint check (ruff) |
| `make fixtures` | Save a live AirLabs response as a test fixture |
| `make deploy` | rsync the code to the Pi |
| `make setup-pi` | Install deps + build rpi-rgb-led-matrix (run on the Pi) |
| `make run-pi` | (Re)start the jetset service on the Pi |
| `make logs-pi` | Tail the jetset service logs (`journalctl -u jetset -f`) |
| `make debug-pi` | Stop the service and run in the foreground with debug logging |

## Configuration

Settings load from a YAML file pointed to by the `JETSET_CONFIG` environment
variable (defaults built in when unset) — `home` coordinates, `range`, `pause`,
and `refresh`. The panel needs no tuning beyond the Adafruit HAT mapping, which
the app sets automatically on hardware.

Flights come from a single source, **AirLabs** (`api_source: airlabs`): one
`/flights?bbox=` call returns every nearby flight with positions, metrics, and
route. `AIRLABS_API_KEY` is read from the environment or a `.env` file. The
free tier allows ~1000 requests/month, so the app refreshes every 45 minutes
(`refresh: 2700`) — one call per refresh — and the display slides a window
across all captured flights so fresh aircraft trickle in between refreshes.

## Data source history

How we got to a single AirLabs source (kept here so the decisions aren't relearned):

- **adsb.lol** (free) gave positions/metrics; **adsbdb** (free) gave routes by
callsign; a **plausibility filter** (great-circle bearing + cross-track
distance, in `geo.py`) tried to reject wrong routes; **hexdb.io** was a free
cross-check.
- Measuring against **FlightAware AeroAPI** (authoritative, `scripts/route-compare.py`)
showed adsbdb routes were only ~20% correct, and the plausibility filter had a
great-circle bug (a route's *extended* great circle can pass near the receiver,
so far-away routes slipped through). hexdb was sparse and years-stale.
- **AirLabs** matched AeroAPI in testing and returns positions, metrics, and
route in one bbox call — so it replaced adsb.lol + adsbdb + hexdb + the
plausibility filter. `AdsbLolAdapter` and `geo.py` remain only for the
`route-compare` diagnostic and are slated for removal.
10 changes: 10 additions & 0 deletions bin/install-service.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/bash
# Render scripts/jetset.service.template (@HOME@ -> $HOME) and install + enable
# the systemd unit. Idempotent; run on the Pi. Callers restart the service.
set -e
cd "$(dirname "$0")/.."

sed "s|@HOME@|$HOME|g" scripts/jetset.service.template \
| sudo tee /etc/systemd/system/jetset.service > /dev/null
sudo systemctl daemon-reload
sudo systemctl enable jetset
28 changes: 28 additions & 0 deletions bin/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Run a scripts/ helper as root for GPIO on the Pi, with the jetset service
# stopped for the duration (restarted on exit — even on Ctrl-C or error).
#
# bin/script <name> [args...] e.g. bin/script logo-gallery 3 UAL
#
# <name> is the helper's filename without the .py extension.
set -euo pipefail

if [ $# -lt 1 ]; then
echo "usage: $(basename "$0") <name> [args...] e.g. $(basename "$0") logo-gallery 3 UAL" >&2
exit 1
fi

# Run from the project root so fonts/ and uv resolve.
cd "$(dirname "$0")/.."

name="$1"
shift
target="scripts/${name}.py"
if [ ! -f "$target" ]; then
echo "no such script: $target" >&2
exit 1
fi

sudo systemctl stop jetset
trap 'sudo systemctl start jetset' EXIT
sudo -E env PATH="$PATH" uv run python "$target" "$@"
87 changes: 87 additions & 0 deletions bin/setup-pi.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#!/bin/bash
# setup-pi.sh: Run locally on the Pi after `make deploy`
# Installs system deps, builds rpi-rgb-led-matrix, and adds it to the project.
set -e

# 1. Add swap if missing
SWAPON=$(command -v swapon 2>/dev/null || echo "/usr/sbin/swapon")
if [ -x "$SWAPON" ]; then
if ! $SWAPON --show 2>/dev/null | grep -q /swapfile; then
echo "=== Creating 1GB swapfile ==="
sudo fallocate -l 1G /swapfile 2>/dev/null || sudo dd if=/dev/zero of=/swapfile bs=1M count=1024
sudo chmod 600 /swapfile
sudo mkswap /swapfile 2>/dev/null || true
sudo swapon /swapfile 2>/dev/null || true
fi
else
echo "=== Skipping swap (swapon not available) ==="
fi

# 2. Install system deps (Cython and Pillow live in the venv, not here)
sudo apt update && sudo apt install -y python3-dev build-essential cmake

# 3. Install uv if missing
if ! command -v uv &> /dev/null; then
echo "=== Installing uv ==="
curl -LsSf https://astral.sh/uv/install.sh | sh
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
export PATH="$HOME/.local/bin:$PATH"
else
export PATH="$HOME/.local/bin:$PATH"
fi

# 4. Sync project deps and add the rgbmatrix build backend (into the venv —
# the wheel builds with --no-build-isolation, so Cython/scikit-build-core must
# live in the venv, not just the system Python from apt).
cd ~/jetset
# Fix permissions if previous builds ran as root
sudo chown -R $(whoami):$(whoami) .venv 2>/dev/null || true
uv sync
uv pip install scikit-build-core cython

# 5. Clone, build, and add rgbmatrix to the project
WHEEL_FILE=$(ls ~/jetset/wheels/rgbmatrix-*.whl 2>/dev/null | head -1)
if [ ! -f "$WHEEL_FILE" ]; then
echo "=== Building rpi-rgb-led-matrix ==="
[ -d "$HOME/rpi-rgb-led-matrix" ] || \
git clone https://github.com/hzeller/rpi-rgb-led-matrix.git "$HOME/rpi-rgb-led-matrix"
cd "$HOME/rpi-rgb-led-matrix"
make -j4
cd ~/jetset

# Ensure Pillow headers are available for the pillow shim
if [ ! -d "/tmp/Pillow" ]; then
git clone --depth 1 https://github.com/python-pillow/Pillow.git /tmp/Pillow
fi

# Build a local wheel so it becomes a proper project dependency
mkdir -p wheels
CFLAGS="-I/tmp/Pillow/src/libImaging" uv build --no-build-isolation --wheel \
"$HOME/rpi-rgb-led-matrix" --out-dir "$PWD/wheels"

# Add wheel as a path dependency in pyproject.toml (persists across uv sync)
uv add ./wheels/rgbmatrix-*.whl

# Free up space
rm -rf /tmp/Pillow
else
echo "=== rgbmatrix wheel already built ==="
fi

# 6. Download airline logos into the global cache (AppConfig.logo_dir =
# /var/lib/jetset/logos). Owned by the deploy user so this download can write;
# the root service only reads. A global dir means root and the deploy user
# resolve the same path — no per-home/JETSET_LOGO_DIR juggling.
echo "=== Downloading airline logos ==="
sudo mkdir -p /var/lib/jetset/logos
sudo chown "$(whoami):$(whoami)" /var/lib/jetset/logos
uv run python scripts/download_logos.py || echo "(logo download incomplete — re-run later)"

# 7. Install + enable the systemd service (runs on boot)
echo "=== Installing jetset systemd service ==="
bash bin/install-service.sh
sudo systemctl restart jetset

echo "=== Setup complete! The jetset service is enabled and running. ==="
echo " Logs: journalctl -u jetset -f"
echo " (foreground debug run: \`make debug-pi\` — stop the service first)"
1 change: 1 addition & 0 deletions mise.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[tools]
python = "3.14.6"
ty = "latest"
uv = "0.11.20"
11 changes: 10 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ dependencies = [
"rgbmatrixemulator>=0.18.1",
"pyyaml>=6",
"requests>=2.34.2",
"python-dotenv>=1.2.2",
"pillow>=11",
]

[project.scripts]
Expand All @@ -20,6 +22,9 @@ line-length = 100
[tool.ruff.lint]
select = ["E", "F", "I", "N", "W"]

[tool.ty.src]
exclude = [".venv"]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
Expand All @@ -35,4 +40,8 @@ filterwarnings = [
]

[dependency-groups]
dev = ["pytest>=9.0.3", "ruff>=0.15.16"]
dev = [
"pytest>=9.0.3",
"ruff>=0.15.16",
"ty>=0.0.49",
]
Loading
Loading