Driving PiFinder headlessly
SkillWeb & browsingRun the PiFinder app headlessly and drive it like a user — launch it with no pygame window or physical display, send keypad presses to navigate menus, capture the screen as a PNG, read live state (plate solve, location, IMU, SQM), and stop it cleanly. Use this whenever you need to actually operate or observe the running PiFinder UI rather than just read its code: "launch/start/run PiFinder", "navigate to <menu/screen>", "take a screenshot of PiFinder", "press UP/DOWN/SQUARE", "what's on the PiFinder screen", "what is it solving / where is it pointing", "check the UI shows X", "reproduce this UI bug", "drive the PiFinder interface", or "stop/shut down PiFinder". Also use it to verify UI-affecting changes by running the app and looking at the screen.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Driving PiFinder headlessly skill
What this skill tells your AI
The instructions your AI receives, as published by brickbots/pifinder in .claude/skills/pifinder-remote/SKILL.md and read by ahel’s review.
PiFinder normally renders to a physical OLED/LCD (or a pygame emulator window) and is driven by a hardware keypad. This skill runs it in a headless mode — no pygame, no SDL window, no hardware — while still rendering every frame, and drives it entirely over PiFinder's HTTP API. That lets you launch the app, navigate its menus, see exactly what's on screen, read its live state, and shut it down, all from the command line.
Everything goes through one helper: scripts/pf_remote.py (Python standard
library only — runs under any python3; it finds and uses the PiFinder venv
itself when launching). Run python3 <skill>/scripts/pf_remote.py -h for the
full command list.
The loop
S=.claude/skills/pifinder-remote/scripts/pf_remote.py # adjust path as needed
python3 $S launch # start cedar + headless PiFinder (rev4, 176x176), wait for API
python3 $S launch -fb # ...with the rev4 battery icon + a simulated discharge
python3 $S launch --display headless # the 128x128 v3/v2.5 panel instead
python3 $S screen -o /tmp/pf.png # capture the current screen as PNG
python3 $S key DOWN DOWN RIGHT # send keypad presses, in order
python3 $S status # aggregated live state as JSON
python3 $S stop # graceful shutdown, then guaranteed teardown
After launch succeeds, the other commands find the running instance (URL +
PIDs) from a small state file in the temp dir — you don't need to pass the URL.
Read the PNG that screen writes (use the Read tool on the path it prints)
to see the UI; that is how you "look at" PiFinder.
Commands
| Command | What it does |
|---|---|
launch | Starts cedar-detect-server then PiFinder headless (-fh --camera debug --keyboard none --display headless_176 -x), in its own process group, and waits until /api/status answers. Records state for the other commands. --display {headless_176,headless,headless_320} picks the panel — default headless_176, rev4's 176×176; headless is the 128×128 panel on v3/v2.5. -fb/--fakebattery adds the rev4 battery monitor: the title-bar battery icon plus a full simulated discharge (low-battery warnings, blind-floor shutdown). |
screen [-o PATH] | Saves GET /api/screen (the live display) as a PNG and prints the path. 176×176 by default, 128×128 under --display headless. |
key BTN [BTN ...] | POSTs each button to /api/key in order (default 0.4s apart, --delay to change). |
status / solution / location | Pretty-prints GET /api/status / /api/solution / /api/location. |
get PATH | GETs any other endpoint, e.g. get /api/imu, get /api/sqm. |
stop | Best-effort graceful /api/stop, then escalates to SIGTERM/SIGKILL on the process group so nothing is orphaned. |
kill | Skips the graceful step; force-kills the recorded process groups (recovery). |
logs [-n N] | Tails the captured PiFinder and cedar startup logs (first place to look if launch fails). |
ready | Polls until the API answers; useful after a manual launch. |
Keypad buttons
PiFinder has a small keypad. Pass these names to key (case-sensitive), or a
raw integer keycode:
- Directions / actions:
UPDOWNLEFTRIGHTPLUSMINUSSQUARE - Long press (held):
LNG_LEFTLNG_UPLNG_DOWNLNG_RIGHTLNG_SQUARE - Alt (function) variants:
ALT_UPALT_DOWNALT_LEFTALT_RIGHTALT_PLUSALT_MINUSALT_SQUAREALT_0 - Number keys: pass the digit as an integer, e.g.
key 1 2 3
Navigation model: RIGHT (or SQUARE) generally drills into the highlighted
item, LEFT goes back, UP/DOWN move the selection, LNG_LEFT returns to
the top menu. After any input, take a fresh screen to confirm what happened —
the screen is the ground truth, menu order changes between versions.
How it works (and why stop is built this way)
- Headless rendering. The launch passes
--display headless_176, which selects the in-memoryDisplayHeadless176driver (python/PiFinder/displays.py, backed byluma.core.device.dummy). It renders at 176×176 with rev4'sLayout176font and spacing profile, so what you capture matches the real rev4 panel rather than merely being scaled up.--display headlessgives the 128×128DisplayHeadlessused by v3/v2.5. The UI render loop already callsshared_state.set_screen()beside every hardware draw, so the current frame is always available atGET /api/screenregardless of display driver. No pygame/SDL/X is needed. - Input.
--keyboard noneruns a no-op keyboard process;POST /api/keyinjects into the samekeyboard_queuethe main loop reads, so menu navigation behaves exactly as with the real keypad. - Solving works headless. With
--camera debug, PiFinder cycles through sample frames andcedar-detect-server+ the solver produce real plate solves, sostatus/solutionreturn live RA/Dec/constellation. - Stopping is deliberately two-stage. PiFinder is multi-process; its workers
run bare
while Trueloops, aren't daemonized, and don't watch a stop flag. Clean shutdown depends onSIGINTreaching the whole process group at once (what a terminal Ctrl-C does).POST /api/stopdoes that from inside the app, which reliably stops the worker processes — but the main process can still hang in its teardown (a multiprocessing-manager shutdown race). Solaunchstarts PiFinder in its own session/process group, isolated from the launcher, andstopescalates withSIGTERM→SIGKILLon that group after a short grace period.stopreturns as soon as the process is actually gone. This guarantees no orphaned processes even when the graceful path stalls.
Running in a git worktree
A fresh worktree (one created via git worktree add or EnterWorktree) is
missing two things PiFinder needs to start, because git worktrees only check
out tracked files and don't share git-ignored files or submodule contents with
the main checkout. Copy them in from the main checkout before the first
launch:
- Hipparcos catalog —
astro_data/hip_main.dat(~53 MB) is git-ignored (seepython/.gitignore) but required byUIChart/UIAlign. The chart screen will crash on construction if it's missing. tetra3submodule —python/PiFinder/tetra3/is a git submodule (https://github.com/smroid/cedar-solve).git submodule update --initinside a worktree typically fails ("Unable to find current revision in submodule path") because the submodule's git metadata isn't replicated to the worktree. Copying the populated directory in from the main checkout is the reliable path.
Both copies are one-time per worktree. Run from the worktree root:
# Adjust MAIN to point at the main checkout (typically three dirs up from a
# .claude/worktrees/<name>/ worktree).
MAIN=../../..
cp "$MAIN/astro_data/hip_main.dat" astro_data/
cp -R "$MAIN/python/PiFinder/tetra3/." python/PiFinder/tetra3/
Venv: the worktree has no venv of its own and pf_remote.py launch only
auto-discovers a venv inside the repo it was invoked from (python/venv,
python/.venv, .venv, venv) — it does not cross into the main checkout
to find one. So activate the main checkout's venv in your shell before
launching, so pf_remote inherits it via sys.executable:
source "$MAIN/python/venv/bin/activate" # or python/.venv if that's where yours is
Alternatively, point pf_remote.py at the main checkout with --repo "$MAIN" so it finds the venv there — but then it also runs PiFinder from
that checkout, not your worktree, which defeats the purpose.
When startup fails
launch waits up to --timeout seconds (default 90) for the API. The first
launch in a fresh checkout rebuilds the catalog cache (~90s, one time); later
launches come up in a few seconds. If it times out:
python3 $S logs— read the PiFinder/cedar startup logs.- Confirm the venv exists (
python/venvorpython/.venv) and the object DB is present (astro_data/pifinder_objects.db). - If you're in a git worktree, confirm
astro_data/hip_main.datandpython/PiFinder/tetra3/are populated — see "Running in a git worktree". - The API binds port 80 when it can, else 8080;
launchprobes both and records the winner. Pass--base-url http://host:portto override. - If a previous run left something behind,
python3 $S killclears it.
Notes
- Repo location is auto-detected (this skill lives under the repo). Override
with
--repo /path/to/PiFinderor thePIFINDER_REPOenv var. - This skill requires two small pieces of in-repo support that ship with it:
the
headlessdisplay driver and thePOST /api/stopendpoint (python/PiFinder/api_extensions.py). They're already in this branch. - The screen is 176×176 (rev4's panel, the default) or 128×128 under
--display headless, and is rendered mostly in the red channel (PiFinder's night-vision palette); that's expected, not a rendering bug. - Plain
-fhemulates rev3: no battery process and no title-bar battery icon. Add-fbwhen the shot or check involves the battery.
Signals
- GitHub stars
- 248
- Forks
- 63
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
pifinder-remote- Source
- github.com/brickbots/pifinder