Okay, doing this: the Pi 5 asks the system to mount the card with one option the card format doesn't accept, so mounting fails every time and it quietly records to the internal card instead. Drop that one option.
This might happen: recording to a plugged-in card starts working. Right now it never does — confirmed live on vgan: mount fails, the receiver silently falls back to internal, and nobody is told.
This might not happen: the option was there to push file changes to disk promptly so a yanked card isn't corrupted. Removing it needs the same protection kept another way — that is the only real work here, the fix itself is one line.
Nothing might happen: units already recording to their internal card see no change.
Drop the option only for this card format, keep it for the older one best
Let the system pick the mount settings itself — simpler, but gives up the corruption protection.
Mount it outside the receiver at boot — what I did on vgan as a stopgap; per-device, not a fleet fix.
Okay, doing this: when a storage drive is pulled or goes bad, the hat's status check hangs on the dead drive and reports everything as missing — so the app says "camera and motion sensor not detected" when the camera is perfectly fine. Make the status check survive a dead drive, and say what is actually wrong.
This might happen: the operator gets told "storage drive not responding" instead of being sent to chase a camera that was never broken.
This might not happen: it won't rescue the pulled drive — a corrupted card still needs repair. This is about not lying about which part failed.
Nothing might happen: with a healthy drive, nothing changes.
Status check never blocks on storage + name the real fault best
Just fix the message — honest, but the hat still wedges until someone restarts it.
Okay, doing this: the Pi 5 accepts any plugged-in drive as the recording target. On vgan it picked the 512 MB boot partition of a spare OS card — that is the mysterious "0.4 GB free". Skip system and boot partitions.
This might happen: no more recording onto a drive that fills in three minutes, and no risk of writing over a system card.
This might not happen: it does not change which real card gets used — a proper recording card is still picked automatically.
Nothing might happen: if only a real recording card is ever plugged in, this never triggers.
Skip boot/system partitions, and require the card to be named for recording best
Keep accepting any drive — convenient, but it picked a boot partition on a live unit.
Okay, doing this: today a hat can show a full 150-minute recording with no error while the camera delivered zero frames — the timer is just wall-clock and nothing checks frames. Fix: define "recording" as frames actually landing on the card, top to bottom, so the app can never show a session as recorded when it's empty.
This might happen: the silent loss stops — a stalled camera fails loud and immediately, so the operator re-records on the spot instead of finding out at decryption. This is the fix for today's incident report.
This might not happen: it's not one switch — it's a few coordinated changes (duration from frames, status gated on real progress, a stall watchdog, a post-session integrity check). Landing only some of them still leaves a hole.
Nothing might happen: on a healthy recording nothing changes — same behavior, just now provably true.
Frame-derived timer + progress-gated status + stall watchdog, then an app post-session check best
App-only check after each session — catches it after the fact, but the device still lies during recording.
Just the stall watchdog — stops runaway empties, but the false timer stays.
Okay, doing this: put the hat's ID in the session name so two hats can never produce the same file name, and take the time from the operator's phone (already sent as the witness clock t) instead of the board's own clock, which is wrong until NTP because these boards have no battery clock.
This might happen: the duplicate file names across devices stop, so nothing gets overwritten at offload; timestamps become trustworthy and correctly dated.
This might not happen: the phone-clock part is not new — the Pi 5 line already names from t (better than waiting for NTP, no boot delay). But Pi 5 is still second-resolution with no hat ID, so two hats in the same second (a 4-of-4 "Record all") still collide there too. The hat ID is the only part that actually closes it.
Nothing might happen: a hat recording alone, already on time, sees no difference.
Adopt Pi 5's phone-witness-clock naming on standard + add the hat ID best
PR #16's "wait for the board's NTP" — slower (boot delay), can time out, and still no hat ID.
PR #16 — likely superseded: the Pi 5 witness-clock approach beats its NTP-wait; still needs the hat-ID naming, which neither has
Okay, doing this: replace vague or missing failures with a specific, plain-language reason and a next step — "camera not detected", "camera stuck in recovery, power-cycle it", "no space left", "camera warming up" — written with the UX doctrine so a field operator knows what to do without calling support.
This might happen: the operator fixes it in the moment (reseat the cable, power-cycle, swap the card) instead of finding a dead session hours later; fewer silent failures walk off site.
This might not happen: it doesn't repair the fault — a genuinely broken camera still can't record, but now the person holding it knows why and what to try.
Nothing might happen: a clean start shows nothing new.
Map each real fault to a doctrine-written operator message + next step best
Keep the generic "couldn't start, turn it off and on" — what mostly exists; hides the actual cause.
Okay, doing this: watch bluetoothd; if it pegs a core and stays there while advertising is up, restart the bluetooth service, and only touch the shared radio if that alone doesn't clear it.
This might happen: a connected phone drops and reconnects; discovery blips for a few seconds; on Zero 3W / A7Z the shared radio can make Wi-Fi hiccup.
This might not happen: recording is not interrupted and no footage is lost.
Nothing might happen: if bluetooth never stalls, it never fires. Pure insurance.
Restart the light service first, escalate to the radio only if needed best
Always bounce the whole radio — simpler, but blips Wi-Fi every time.
Okay, doing this: when a record attempt hits the stale-key block, set the leftover key aside and retry, so the next tap records.
This might happen: the hat records immediately — no power-cycle, no field visit.
This might not happen: nothing is deleted (the key is kept aside, never destroyed); the normal path is untouched.
Nothing might happen: a hat that never lost power mid-record has no stale key, so it never triggers.
Clear it in place, records on the next tap best
Heal only on the next reboot — what the current fix does; still needs a power-cycle.
PR #26 — the next-boot fix, open, ready to test
Okay, doing this: drop the every-5s Wi-Fi check and update the app's connection status from NetworkManager's own change signals instead.
This might happen: the repeated background check disappears; the app still shows live connection status.
This might not happen: no change to the radio (it never scanned) and no change to how it auto-connects.
Nothing might happen: the operator sees the same status as before.
Event-driven from NetworkManager signals best
Just slow the check to ~15s — trivial, keeps the background work.
Read only when asked — simplest, but loses the live "Wi-Fi dropped" update.
Okay, doing this: the auto-enroll already works — on first internet a new hat installs Tailscale, joins with its one-time key, retries until it's on, then wipes the key. The change is to stop copy-pasting those ~150 lines into every board's first-boot script and keep one shared copy.
This might happen: one place to fix enrollment; the four board images stop drifting apart (they already have).
This might not happen: nothing changes in how a new hat actually enrolls — same steps, same behavior.
Nothing might happen: hats already on the tailnet are untouched; this only affects freshly-imaged cards.
One shared enroll unit every board image uses best
Leave it duplicated per board — works today, but drifts and has to be fixed four times.
Okay, doing this: if a hat's update state is stuck on "checking" past a timeout, reset it to idle automatically on the next check, instead of someone having to fix it by hand.
This might happen: a wedged hat can check and update on its own again; no manual intervention.
This might not happen: no change to a healthy update, and the safe A/B install + auto-rollback stays exactly as-is.
Nothing might happen: hats that never wedge never notice. The rest of the OTA is already clean — nothing else to simplify there.
Auto-reset a stale "checking" on the next check best
Keep resetting it by hand — what happens today.
Okay, doing this: ship the live camera preview (see through the hat before and during recording) and the polished Pi 5-line UI into the everyday app, for every hat — not just the Pi 5 test build.
This might happen: the operator sees the camera is actually capturing before committing to a long session — a black preview means no frames, which is exactly the silent-loss failure we just found. Preview becomes the human catch for it.
This might not happen (watch this one): it will not be free — turning preview on in the shipping app re-wakes the "recordings auto-stop a few seconds in" bug, which is dormant today only because production has no preview. The one-line record-start lock has to land first, or this reintroduces the loss it's meant to prevent.
Nothing might happen: on iPhone (preview unsupported today) nothing changes there yet.
Ship the existing preview module everywhere, after the start-lock fix best
Keep preview on the Pi 5 build only — safe, but the fork and the drift stay.
Okay, doing this: keep one set of screens and flows for the whole app; the transport underneath (BLE, direct preview) and the hat type swap behind a capability layer, so the UI never forks per device.
This might happen: design and fix once, and it lands on every hat and phone; the per-line drift (like the four first-boot scripts that already diverged) stops happening.
This might not happen: hardware differences don't vanish — they move behind the capability layer, not into separate screens. And this is the app UI only; the Pi 5 receiver firmware stays its own channel as before.
Nothing might happen: the operator sees the same app no matter which hat — ideally they never know which adapter is underneath.
One UI, device differences behind a capability layer best
Keep the live-preview build as a separate fork — what we have now; it drifts.
Lowest common denominator — drop device-specific features to force sameness; loses capability.
What we found: the receiver has no auto-stop — no inactivity, disconnect, timeout, or second-phone takeover ends a recording on its own. The one exception, a bluetoothd restart, self-stops cleanly with error=""; that was verified absent on the unit checked (no bluez_owner_lost, boot continuity intact). So all three stops were genuine external stop commands, and the app only emits a stop from the "Stop all" button.
So the realistic causes are: a "Stop all" press from the second connected phone, an accidental toggle, or the LAN API. The req-id cross-check against the app logs is what separates them.
Side note: the bluetoothd-restart exception is already shipping (present in live 1.8.2, and in 1.8.6 — built from product/standard). Nothing to merge.
Okay, doing this: make it a hard rule, enforced in the build, that every OTA release is built from the product/standard branch — never a feature or release side-branch — so nothing ships that isn't on the canonical, tested line.
This might happen: the gap between "what's tested on standard" and "what's on devices" closes; fixes that land on standard actually reach the fleet.
This might not happen: it doesn't slow a release — it just fixes which branch the artifact comes from.
Nothing might happen: if a build was already from standard, nothing changes; this makes it explicit and unbreakable.
Enforce in the build/CI — refuse to build from a non-standard ref best
Rely on discipline — what we have; it drifts.