Skip to content

Beta 4.2.0 - drag timer - #160

Open
TheAngryRaven wants to merge 19 commits into
masterfrom
BETA
Open

TheAngryRaven wants to merge 19 commits into
masterfrom
BETA

Conversation

@TheAngryRaven

@TheAngryRaven TheAngryRaven commented Aug 26, 2026 •

Copy link
Copy Markdown
Owner

Requested by Dove · project thread

Summary

Release PR: BETA → master for firmware 4.2.0. Headline feature is drag mode (automatic + manual christmas tree). It also carries the SD/watchdog boot hardening, and two beta-only SensorEgg features that compile out of the release image.

Before: the logger timed circuit laps and sprint runs, but had no way to time a straight-line pass. A firmware-initiated reboot could hand the next boot a running watchdog, which on a slow card wedged the SD and offered a healthy card for formatting on every reboot.

After: main menu → Drag → pick a distance and Automatic or Manual, and the device times ET, trap speed, 0-60 (and reaction time in Manual) with no track file. Soft reboots survive a slow SD init, and the format page only offers an erase on consistent evidence.

What ships in the release image (master build: NeoPixel on, SensorEgg off, profiling off)

Drag mode — automatic (plan 0015, #148)

  • Main menu Drag → distance picker: 1/8 mile, 1000 ft, 1/4 mile, 1/2 mile, 1 mile (+ Back). No track file, no course detection.
  • Stages at a standstill (re-latching anchor), starts the clock rollout-style 11.25 in past the staged position, ends at the target distance with interpolated crossings. Reports ET, trap speed, 0-60 mph; re-arms automatically for the next pass.
  • Runs duck-type as laps (lapHistory, laps line in the DOVEX header). Header gets race_mode=DRAG and a course name like DRAG 1/8 MILE; same backwards-compatible trailing-column scheme as SPRINT.
  • Idle grace re-arms on every fresh stage and every completed run, so a moving queue doesn't end the session.
  • The run state machine is the new host-tested drag_timer pure unit (tests/drag_timer_test.cpp); review fixes (time base, launch guards, unit hygiene) in 7e7dd93.

Drag mode — manual christmas tree (plan 0016, #161)

  • After the distance the menu asks Automatic or Manual.
  • Manual stages like a strip: STOP TO STAGE → white staging pip → three yellows at the sportsman 500 ms cadence → green, mirrored on the LED bar and as a big OLED 3…2…1 / flashing GO.
  • Moving during the yellows is a RED LIGHT foul; sitting still 5 s after green is FAILED TO LAUNCH. Every run ends on a results screen (ET, trap, 0-60, reaction time) that re-arms on any button.
  • The screen is pinned to staging info for the whole session; hold Select 2 s to end it. The physics launch gate opens only on green.
  • The tree is the new host-tested drag_tree pure unit (tests/drag_tree_test.cpp). New sim golden fixtures cover the drag pages.

Boot / SD hardening (#163)

  • The nRF52 WDT survives NVIC_SystemReset(), so every soft reboot (BLE/USB exit, OTA, format page, reboot combo) handed the next boot a ~4 s deadline. setup() now detects a carried-over WDT first thing (wdtBootCheck()), feeds it between slow boot steps, and wdtSetup() no longer writes to a running (locked) WDT.
  • SD_SETUP() re-probes once after a 250 ms settle and only offers a format when the card answered both times and the volume mounted neither time (new sd_probe pure unit). A card that stops answering between probes goes to FAULT, never an erase offer.
  • After SD.format() the volume must mount before "Format OK"; otherwise the page says Formatted: no mount / Power-cycle the unit instead of reboot-looping.
  • Format page and SD FAULT page show the boot cause and SdFat's last error (e.g. boot:WDT err:20); wake_cause gains short names and a watchdog cause.
  • Recovered-on-probe path now records the active SPI clock.

Docs (#159 and the feature PRs)

  • Plans 0015–0018 added; plan 0011 records the wall-clocked hardware profiling run (9.1 kHz loop, ~96 µs mean iteration, 37 ms worst — the nRF52840 has ample headroom).
  • CLAUDE.md, ARCHITECTURE.md, CHANGELOG, page-id and settings tables updated.

Beta channel only (BIRDSEYE_ENABLE_SENSOREGG=1; compiled out of the release image)

SensorEgg pairing menu + EGG TEST page (plan 0017, #162)

  • Egg main-menu row: 2-minute window-gated pairing (long-press the egg's button), stored to the new sensoregg_mac setting and applied live; paired page offers Back / Test / Unpair (persist-first, like the camera). Unpaired = accept any egg, as before.
  • EGG TEST bench page: rf link state, protocol version, EGT/CJ/AUX/battery, sequence + packet rate, PAIR/FAULT flags, active MAC filter. The camera test page's egg soak line works at the desk again.
  • MAC parse/format/byte-order helpers are host-tested in sensoregg_protocol.

SensorEgg GATT link (plan 0018, #164)

  • A paired egg is now connected to as BLE central during races and on the test pages: reads the pod's self-describing channel table, anchors a clock fit with boot_id reboot detection, and streams per-channel sample frames into the same data surface as the beacon (DOVEX columns, race pages, LEDs unchanged). Beacon stays the fallback and pairing transport; no 44 % scan duty while streaming.
  • SensorEgg builds run Bluefruit.begin(1, 1) with skinny central parameters; the camera link still wins. SENSOREGG_SLEEP() drops the pod link for transfers and shutdown.
  • Decoders are the new host-tested sensoregg_gatt pure unit.

Type of change

  • Bug fix (SD/watchdog boot hardening)
  • New feature / behavior (drag mode; beta-only SensorEgg pairing + GATT)
  • Refactor (no behavior change)
  • Tests only (new pure-unit suites alongside each feature)
  • CI / tooling / docs
  • Breaking change — track files, BLE protocol and existing log formats are unchanged; race_mode=DRAG is a new value in an existing trailing header column.

How it was verified

  • Host unit tests pass (ctest --test-dir tests/build) — new suites: drag_timer, drag_tree, sd_probe, sensoregg_gatt, plus additions to sensoregg_protocol and wake_cause
  • clang-tidy clean (static analysis check green)
  • Compiles for the XIAO nRF52840 Sense and plain nRF52840 (beta flags), plus a flags-off Sense build
  • Native + WASM sim builds, goldens (incl. new drag pages) and lap oracles pass
  • Tested on real hardware — describe what you exercised:

Checklist

  • CHANGELOG.md updated under [Unreleased] (if user-visible)
  • ARCHITECTURE.md / CLAUDE.md updated (if a module or interface changed)
  • New testable logic has a matching test in tests/
  • Branch is focused — this is the long-lived BETA → master integration PR

Before tagging v4.2.0

  • Set FIRMWARE_VERSION "4.2.0" in BirdsEye/project.h (still 4.1.0 on BETA — the OTA manifest takes its version from the tag, so a mismatch makes the companion offer the update forever)
  • Rename ## [Unreleased] to ## [4.2.0] - <date> in CHANGELOG.md
  • Keep the release workflow's DovesLapTimer pin at v4.3.0 (its src/ is identical to the library's BETA)

Related issues

Merges #148, #159, #161, #162, #163, #164.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SdhRyfZ4eYuooXi9uy28aw

TheAngryRaven and others added 5 commits August 25, 2026 00:54
… is answered

9134 Hz, 96 us mean, 37 ms worst iteration, SLP 12.0, with a GPS lock,
logging and the camera recording.

The instrument validates itself: 9134 Hz x 96 us = 87.7% of the second
inside loop(), and SLP — computed by a different route — reads 12.0%.
All fourteen slots sum to ~99.8.

SLP 12% also settles what the previous build could not distinguish: the
CPU is not sleeping through the second, so the loop genuinely runs at
~9 kHz and the old pegged rate was the display clamp rather than the
timebase bug. There was no way to know that until the window was
wall-clocked.

Headroom, against the binding constraint (one iteration per 40 ms PVT
frame): ~420x on the mean, 43x on the worst iteration against the 4 KB
GPS ring, 108x against the watchdog. Nothing here justifies an nRF5340
on throughput, and SLP 12% caps what leaving the Arduino core could
reclaim.

Also records where the 88% goes — it is polling, not work. GPS is called
9134 times a second to service a 25 Hz stream; DSP renders 3 times and
early-returns ~9000; BTN is six digitalReads per iteration with nobody
touching a button. Rate-limiting the polls would reclaim most of it
without changing silicon.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CqENxo91S1VL3sM9PTWMf1
Main menu -> Drag -> pick 1/8 Mile / 1000 ft / 1/4 Mile / 1/2 Mile /
1 Mile and the session starts: no track file, no detection. Stage at a
standstill, rollout-style launch (11.25 in past a re-latching standstill
anchor, ET start interpolated between 25 Hz fixes), run ends at the
target distance with interpolated ET + trap speed and a 0-60 split, then
re-arms automatically for the next pass. All passes land in one DOVEX
session with race_mode=DRAG (laps line = run ETs, same trailing-column
scheme as SPRINT).

- New host-tested pure unit drag_timer.{h,cpp}: the whole run state
  machine (ARMED/STAGED/LAUNCHED), the distance table, silent aborts
  (mid-run standstill, >=2 s fix gap — also how queue-creep phantom
  launches self-cancel), 14 doctest cases.
- Sketch glue mirrors sprint: dragTimer != nullptr IS drag mode, drag
  branch first in every activeTimer*() helper, run capture on the
  run-count edge, trackDetected latched, idle-grace re-arm on each run
  AND each fresh STAGED latch.
- UI: Drag main-menu row, PAGE_DRAG_DISTANCE picker (scrolling window),
  drag branches on the lap-time/pace/best pages (live ET, *staged*,
  trap/0-60 subtext, live 0-60 readout), LED pace pip suppressed between
  runs like sprint.
- Sim: golden walk updated for the new menu row + a picker fixture;
  goldens regenerated; soak/determinism/goldens/oracles all pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BKa62h4fhn61pKn1LygrMK
…ranch-8jmgzr

docs(0011): record the wall-clocked hardware run — the board question…
Six findings from the branch review, all fixed:

- Time base: the glue fed getGpsTimeInMilliseconds() (time-of-day, wraps
  to zero at UTC midnight — evening sessions) into a unit whose contract
  says epoch ms. Both call sites now feed getGpsUnixTimestampMillis(),
  and the unit's non-monotonic guard resyncs on a backwards step instead
  of rejecting every subsequent fix — one step used to wedge the drag
  timer for the rest of the session.
- Launch edge: a >=2 s fix gap while STAGED now re-stages instead of
  evaluating the launch, so a launch that happened inside a GPS dropout
  can't interpolate its ET start back to a parked-car fix.
- Prove-out gate: a launch must reach 15 mph within 5 s of the ET start
  or the run is silently abandoned — a wave-off driven to the pits at
  4 mph never holds the sub-2 mph standstill abort and used to record
  660 ft of pit road as a ~90 s "run".
- activeTimerTotalDistance() converts the drag branch to meters, matching
  every other branch of the accessor.
- distanceFeet() delegates to the haversine unit (x 5280) instead of
  carrying a second copy of the formula and Earth radius.
- The seconds.hundredths split rendering is now one function
  (displayPrintSplitSeconds) shared by the results subtext and the pace
  page's live 0-60 readout.

Five new regression tests cover the wedge recovery, the staged-gap
launch, the wave-off, a slow-but-real pass, and duplicate timestamps.
Plan 0015 doc + CLAUDE.md constants updated to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BKa62h4fhn61pKn1LygrMK
…mggk0b

plan 0015: Drag mode — distance runs without a track
@github-actions

github-actions Bot commented Aug 26, 2026 •

Copy link
Copy Markdown

Coverage — host-testable units

📂 Overall coverage

Metric Coverage
Lines 🟢 2267/2301 (98.5%)
Functions 🟢 234/235 (99.6%)
Branches 🟢 1648/1830 (90.1%)

📄 File coverage

File Lines Functions Branches
BirdsEye/ble_stream.cpp 🟢 34/34 (100.0%) 🟢 8/8 (100.0%) 🟡 17/20 (85.0%)
BirdsEye/camera_fsm.cpp 🟢 238/246 (96.7%) 🟢 20/20 (100.0%) 🟡 142/160 (88.8%)
BirdsEye/course_creator.cpp 🟢 213/221 (96.4%) 🟢 21/21 (100.0%) 🟡 119/136 (87.5%)
BirdsEye/course_prune.cpp 🟢 37/37 (100.0%) 🟢 5/5 (100.0%) 🟢 47/50 (94.0%)
BirdsEye/crc32.cpp 🟢 30/30 (100.0%) 🟢 4/4 (100.0%) 🟢 24/24 (100.0%)
BirdsEye/crossing_pattern.cpp 🟢 15/15 (100.0%) 🟢 1/1 (100.0%) 🟢 12/12 (100.0%)
BirdsEye/dovex_header.cpp 🟢 106/107 (99.1%) 🟢 7/7 (100.0%) 🔴 62/88 (70.5%)
BirdsEye/drag_timer.cpp 🟢 150/154 (97.4%) 🟢 10/10 (100.0%) 🟡 76/94 (80.9%)
BirdsEye/drag_tree.cpp 🟢 112/114 (98.2%) 🟡 7/8 (87.5%) 🟢 87/94 (92.6%)
BirdsEye/filename_validator.cpp 🟢 14/14 (100.0%) 🟢 1/1 (100.0%) 🟢 30/30 (100.0%)
BirdsEye/gps_stats.cpp 🟢 25/25 (100.0%) 🟢 3/3 (100.0%) 🟢 8/8 (100.0%)
BirdsEye/gps_status_page.cpp 🟢 29/29 (100.0%) 🟢 4/4 (100.0%) 🟢 28/28 (100.0%)
BirdsEye/gps_time.cpp 🟢 45/45 (100.0%) 🟢 6/6 (100.0%) 🟢 30/32 (93.8%)
BirdsEye/gps_validation.cpp 🟢 24/24 (100.0%) 🟢 2/2 (100.0%) 🟢 66/66 (100.0%)
BirdsEye/haversine.cpp 🟢 8/8 (100.0%) 🟢 1/1 (100.0%) ⚫ 0/0 (0.0%)
BirdsEye/idle_policy.cpp 🟢 17/17 (100.0%) 🟢 2/2 (100.0%) 🟢 14/14 (100.0%)
BirdsEye/insta360_protocol.cpp 🟢 140/140 (100.0%) 🟢 16/16 (100.0%) 🟡 86/98 (87.8%)
BirdsEye/lap_format.cpp 🟢 18/18 (100.0%) 🟢 1/1 (100.0%) 🟢 9/9 (100.0%)
BirdsEye/led_animations.cpp 🟢 84/84 (100.0%) 🟢 7/7 (100.0%) 🟢 43/46 (93.5%)
BirdsEye/led_frame.cpp 🟢 21/21 (100.0%) 🟢 7/7 (100.0%) 🟢 6/6 (100.0%)
BirdsEye/led_modes.cpp 🟢 67/68 (98.5%) 🟢 6/6 (100.0%) 🟢 50/52 (96.2%)
BirdsEye/led_status.cpp 🟢 106/108 (98.1%) 🟢 11/11 (100.0%) 🟢 71/75 (94.7%)
BirdsEye/local_time.cpp 🟢 48/48 (100.0%) 🟢 6/6 (100.0%) 🟢 46/50 (92.0%)
BirdsEye/loop_profile.cpp 🟢 65/65 (100.0%) 🟢 7/7 (100.0%) 🟢 35/36 (97.2%)
BirdsEye/sat_bars.cpp 🟢 33/33 (100.0%) 🟢 2/2 (100.0%) 🟢 51/54 (94.4%)
BirdsEye/sd_access_policy.cpp 🟢 9/9 (100.0%) 🟢 3/3 (100.0%) 🟢 18/18 (100.0%)
BirdsEye/sd_format_page.cpp 🟢 25/25 (100.0%) 🟢 3/3 (100.0%) 🟢 25/26 (96.2%)
BirdsEye/sd_probe.cpp 🟢 6/6 (100.0%) 🟢 1/1 (100.0%) 🟢 8/8 (100.0%)
BirdsEye/sector_purple.cpp 🟢 84/85 (98.8%) 🟢 3/3 (100.0%) 🟡 57/64 (89.1%)
BirdsEye/sensoregg_gatt.cpp 🟢 93/93 (100.0%) 🟢 12/12 (100.0%) 🟡 57/68 (83.8%)
BirdsEye/sensoregg_protocol.cpp 🟢 87/88 (98.9%) 🟢 13/13 (100.0%) 🟢 74/76 (97.4%)
BirdsEye/setting_parse.cpp 🟢 29/30 (96.7%) 🟢 2/2 (100.0%) 🟢 38/42 (90.5%)
BirdsEye/sprint_select.cpp 🟢 25/25 (100.0%) 🟢 4/4 (100.0%) 🟢 46/48 (95.8%)
BirdsEye/tach_filter.cpp 🟢 91/91 (100.0%) 🟢 13/13 (100.0%) 🟡 72/82 (87.8%)
BirdsEye/track_json.cpp 🟢 116/120 (96.7%) 🟢 12/12 (100.0%) 🟡 67/88 (76.1%)
BirdsEye/wake_cause.cpp 🟢 23/24 (95.8%) 🟢 3/3 (100.0%) 🟢 27/28 (96.4%)

claude and others added 14 commits August 26, 2026 04:07
Drag now asks Automatic or Manual after the distance. Manual stages
like a strip: stop, and the 9-px bar lights a white staging pip, three
yellows at the sportsman 500 ms cadence, then green — mirrored on the
OLED as STOP TO STAGE, a big 3..2..1, and a flashing GO. Movement
during the yellows is a RED LIGHT foul; 5 s still after green is
FAILED TO LAUNCH; both flash red and wait for a button. Runs end on a
results screen (ET, trap, 0-60, and a new reaction time = green ->
interpolated rollout crossing, both epoch ms). The screen is pinned to
staging info for the whole manual session; hold Select 2 s to end it.
Automatic mode is behavior-identical (one extra menu hop).

- New host-tested pure unit drag_tree.{h,cpp}: the ONE state machine
  driving both the LED strip (renderStrip) and the OLED (countdownDigit
  / flashPhase) so they can never disagree; 17 doctest cases (exact
  stage timings, fouls from every yellow, exit-hold semantics, golden
  strip frames). Flash half-period 500 ms — the 3 Hz OLED aliases
  anything faster.
- drag_timer grows setLaunchEnabled (default true; while disabled a
  rollout-at-speed re-arms — a pre-green move is the tree's foul, never
  a run) and runStartEpochMs() for the RT computation; 3 new tests.
- Glue: dragStagingLoop() in the gpsStatusPageLoop slot (Inputs/step/
  Effects, resetButtons on consumption); the launch gate re-asserted
  every loop (open exactly while green); pin cleared only by
  endRaceSession so every session ender releases it.
- UI: PAGE_DRAG_MODE (-18) Auto/Manual/Back; PAGE_DRAG_STAGING (-19)
  pinned via the gpsLockHold construction. Fixed a plan-0015 layout
  bug while here: subtext after size-3 text needs a size-3 newline or
  it renders on the ET's bottom glyph rows.
- NeoPixel: tree renders strip-only as a new cascade arm (status LEDs
  live, search pip wins without a fix, normal scale during the run);
  paceValid gains !dragManualActive() (constant false for auto).
- Sim: 6 new golden fixtures including a FULL manual pass through the
  real pipeline (park -> tree -> green -> 70 mph -> results with RT);
  goldens regenerated (23 fixtures), soak/determinism/oracles pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BKa62h4fhn61pKn1LygrMK
…mggk0b

plan 0016: Manual drag mode — the christmas tree
parseMac/formatMac (settings-string codec), macReverse (human <->
ble_gap_addr_t LSB-first, aliasing-safe), macIsWildcard, macAccepts —
the byte-order reversal the scan callback depends on finally moves
under host test, with a case pinning that a same-order compare rejects.
kPairingTimeoutMs (120 s, camera parity) and kMacStrLen live with the
other tunables.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AcdBRTn52QpCQWkyVm5Lkg
Runtime MAC filter boots from the SENSOREGG_MAC fallback and loads
sensoregg_mac at SENSOREGG_SETUP(); the scan callback stays protocol-
blind (it additionally memcpys the advertiser address into a per-slot
field of the existing double buffer, ready-flag-last preserved) and
bypasses the MAC filter while the capture window is open. Capture runs
in the main-loop drain on the parsed pairingActive bit: persist first,
then the RAM filter, then close the window — a failed SD write retries
on the next frame. 120 s window timeout; bench latch (eggTestActive)
and the pairing window now force the race-gated scanner on; packet-rate
meter for the test page. Twelve new public accessors with no-op twins.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AcdBRTn52QpCQWkyVm5Lkg
Main-menu row 'Egg' (index 6, appended after Camera so nothing shifts),
PAGE_PAIR_EGG (-20) cloning the camera page's shape — paired =
Back/Test/Unpair with Back at index 0, unpaired = window-gated capture
status with B2 cancel and B1 reserved for future manual entry — and
PAGE_EGG_TEST (-21): rf tri-state + proto version, EGT/CJ/AUX/BAT,
sequence + measured packet rate, live PAIR/FAULT flags, active MAC
filter, Back. All five display_ui coupling points wired under the
build flag; the camera test page now latches the egg bench scan on
enter/exit, restoring the desk-soak behavior its comment promised
(stale since plan 0012's race gate — comment fixed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AcdBRTn52QpCQWkyVm5Lkg
Flag-0 sim never renders the Egg pages; stubs mirror the module's full
surface so a future flag-1 sim build links. Golden hashes verified
UNCHANGED — the gate does not leak into stock renders.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AcdBRTn52QpCQWkyVm5Lkg
…ables

Also fixes the two stale subsystem-14 blocks that still described a
PW-ADV-1-only 14-byte parser and hardcoded-MAC-only pairing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AcdBRTn52QpCQWkyVm5Lkg
plan 0017: SensorEgg pairing menu + live-data test page
…, show the boot cause

Field report (2026-09): a new unit claimed its SD card was unformatted,
the on-device format reported OK and rebooted, and the same page came
straight back — until a full power cycle, after which the card mounted
fine with no further format. The format had worked; the boot after it
had not.

Root cause: the nRF52 hardware watchdog survives NVIC_SystemReset()
(only a pin, brown-out, power-on or System OFF reset clears it). Every
firmware-initiated reboot therefore hands the next boot a running ~4 s
WDT that setup() never fed until wdtSetup() at its very end. A clean
boot fits; a slow SD init (SdFat's 2 s ACMD41 timeout, three attempts)
does not, and the WDT reset the device mid-SD-transaction — leaving a
card that firmware cannot reset (CS grounded, no power switch) reading
as "card answers, no FAT volume" on every following soft boot.

- setup() now calls wdtBootCheck() first: a WDT already running is fed
  immediately (wdtCarriedOver), setup() pets between every slow step
  (display delays, each SD.begin() attempt, the probe settle, GPS
  probe, camera/egg/strip init — all no-ops on a clean boot), and
  wdtSetup() leaves a running, register-locked WDT alone.
- sdPerformFormat() requires the fresh volume to mount before "Format
  OK": a mount failure stays on the confirm page as "Formatted: no
  mount / Power-cycle the unit" instead of rebooting into an identical
  loop. sdFormatLastFailed becomes sdFormatFailure (NONE/ERASE/MOUNT).
- SD_SETUP() re-probes the volume once after a 250 ms settle and only
  declares the card unformatted on consecutive no-volume probes with
  the card layer answering both times; a card-layer flap is "dead"
  (FAULT, never an erase offer). The rule is the new host-tested
  sd_probe unit. The recovered-on-probe path also records the SPI
  clock it had been leaving at 0.
- wake_cause::shortName() puts the boot cause on the debug line, the
  format page's last line (boot:WDT err:20, with SdFat's error code)
  and the SD FAULT page, so a watchdog-induced loop is visible.

Sim: sd_probe added to the core sources, sdErrorCode()/sdErrorData()
stubbed on the SdFat shim, wdtBootCheck prototype added. Goldens
unchanged (the GPS status page is untouched).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VhDTiCUTGJhV7XvogdU9CQ
…e-mlctto

SD format page: survive the WDT across a soft reset, verify the mount, show the boot cause
…ers)

parseDescriptor (strides by declared record_len — forward compat),
parseSampleFrame, parseClock, sampleToReal (sentinel -> NaN before
scaling), descriptor-driven role mapping (normative names + quantity,
never hardcoded channel ids), fastestChannel, and the v1 clock fit
(request/response midpoint anchor, boot_id epoch check, wrap-safe
mapping both directions). Fixtures are byte-identical to the egg
repo's pw_gatt_encode goldens — encode == decode pinned across repos,
the sensoregg_protocol <-> pw_adv_encode discipline applied to the
GATT contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AcdBRTn52QpCQWkyVm5Lkg
bleCoreEnsureInit grows a flag-gated central slot (begin(1,1) +
configCentralConn(247, 6, 1, 1) — 7.5 ms event cap so the pod link can
never crowd the camera; stock builds stay begin(1,0)). The scan
callback doubles as the connect trigger (paired connectable report
while WAIT_ADV -> Central.connect, no resume on that path); the
central connect callback runs the blocking bring-up sequence in the
callback task (documented deviation — client ops each wait a conn
interval and would stall the 25 Hz row engine on the main loop):
discover, MTU 247, read Descriptor, timed Clock read (the fit anchor),
enableNotify, ready-flag-last. The notify data plane keeps the
copy-only discipline: an 8-slot frame ring drained by SENSOREGG_LOOP,
which parses, epoch-checks boot_id (mismatch -> log + reconnect
re-anchors), routes the latest sample per frame by descriptor-driven
role mapping into the SAME eggReading surface the beacon feeds, and
feeds the zombie monitor from the fastest channel's frame seq only.

Gate split keeps plan-0012 discipline uniform: eggLinkWanted = radio
gate + paired; eggScanWanted = radio gate && (pairing window || no
link engaged) — streaming pays no scan duty; capturing a NEW egg
drops the old link. SENSOREGG_SLEEP cancels/drops the central link
(covers transfers + shutdown; the quiesce settle gives it airtime).
EGG TEST: rf:GATT state + live MTU. Stale be80-era central prose in
bluetooth.{h,ino} fixed; subsystem-14 radio-role rewritten (the
camera-priority rule survives as mechanism, not prohibition).

Verified: host suite green (fixtures byte-identical to the egg's
pw_gatt_encode goldens), sim goldens unchanged, both flag arms
compile (ON 47%/36%, OFF 45%).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AcdBRTn52QpCQWkyVm5Lkg
plan 0018: SensorEgg GATT link — the logger connects to the paired egg

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants