Skip to content

plan 0018: SensorEgg GATT link — the logger connects to the paired egg - #164

Open
TheAngryRaven wants to merge 2 commits into
BETAfrom
claude/sensoregg-gatt-link
Open

plan 0018: SensorEgg GATT link — the logger connects to the paired egg#164
TheAngryRaven wants to merge 2 commits into
BETAfrom
claude/sensoregg-gatt-link

Conversation

@TheAngryRaven

Copy link
Copy Markdown
Owner

Summary

The logger becomes BLE central toward the paired egg and consumes its PerchWerks Sensor Service end-to-end (design record: docs/plans/0018-sensoregg-gatt-link.md; companion firmware: DovesSensorEgg PR #16 — flash that egg before benching this).

  • New pure unit sensoregg_gatt.{h,cpp} (host-tested): Descriptor decode (strides by declared record_len — forward-compatible), Sample frame decode, Clock decode, sampleToReal (0x8000 sentinel → NaN before scaling), descriptor-driven role mapping (normative names + quantity — nothing egg-specific hardcoded; a future pod with different channels parses with zero logger changes), and the v1 clock fit (request/response midpoint anchor, boot_id epoch check, wrap-safe both directions). Fixtures are byte-identical to the egg repo's pw_gatt_encode goldens — the wire contract pinned from both ends, exactly like sensoregg_protocolpw_adv_encode for the beacon.
  • Central role: bleCoreEnsureInit() runs flag-gated Bluefruit.begin(1, 1) with configCentralConn(247, 6, 1, 1) — a 7.5 ms event cap so the pod link can never crowd the camera; stock builds stay begin(1, 0).
  • Link state machine (IDLE → WAIT_ADV → CONNECTING → BRINGUP → STREAMING, 5 s backoff, 10 s connect timeout): the scan callback doubles as the connect trigger; bring-up (discover → MTU 247 → read Descriptor → timed Clock read = the fit anchor → enableNotify) runs in the Bluefruit callback task — a documented deviation from "callbacks only copy", since each blocking client op waits a connection interval and would stall the 25 Hz DOVEX row engine on the main loop. The notify data plane keeps the copy-only discipline: an 8-slot frame ring drained by SENSOREGG_LOOP().
  • Same surface, either transport: decoded samples land in the exact eggReading/eggRxMs/eggSeqMon surface the beacon feeds — every accessor, DOVEX column, race page and LED path untouched. Zombie detection feeds from the fastest channel's frame seq only. A boot_id mismatch logs "pod rebooted", drops the link, and the reconnect re-anchors.
  • Gate discipline unchanged: link wanted = today's scan gate + paired; the scanner runs only while pairing or no link is engaged (streaming pays zero scan duty — the connected egg is silent anyway); capturing a NEW egg drops the old link; SENSOREGG_SLEEP() cancels/drops the central link, which covers BLE transfer mode and shutdown for free. The beacon remains the unclaimed/disconnected fallback and the pairing transport.
  • EGG TEST: rf:GATT state + live M<mtu>; degradations recorded in the plan doc (tcFault false while streaming — frames carry no MCP STATUS; protoVersion/pairingFlag hold last beacon values).
  • Stale be80-era central prose in bluetooth.{h,ino} fixed; subsystem-14's "do not improve this into a connection" rewritten — the camera-priority rule survives as mechanism (skinny params + gate + SLEEP), not prohibition.

Type of change

  • Bug fix (no user-visible behavior change beyond the fix)
  • New feature / behavior
  • Refactor (no behavior change)
  • Tests only
  • CI / tooling / docs
  • Breaking change (track files, log format, BLE protocol, or a removed mode)

How it was verified

  • Host unit tests pass (ctest --test-dir tests/build) — new sensoregg_gatt matrix: golden descriptor/frame/clock (egg-identical bytes), stride-by-28 forward compat, role mapping, sentinel, fit anchor/epoch/millis-wrap
  • clang-tidy clean — left to CI
  • Compiles for the XIAO nRF52840 Sense — both flag arms locally (CI only builds flag-ON): ON 47% flash / 36% RAM, OFF 45% (the begin(1,0)/(1,1) split makes flag-OFF a real risk)
  • Sim builds and golden hashes pass unchanged (6/6 — the flag gate doesn't leak into stock renders)
  • Tested on real hardware — pending owner bench (flash egg PR Reduce display update rate to 1Hz during BLE transfers #16 first): pair via window → EGG TEST shows rf:GATT M247 with live values; power-yank the egg → --- within 1 s, auto-reconnect + "pod rebooted" log on return; unpair → beacon fallback (rf:OK); transfer mode drops the link, download throughput matches plan-0012 numbers; camera soak (headline risk): repeated connect/record cycles + one full download with the egg link streaming; shutdown/charge park with link up → clean System OFF

Checklist

  • CHANGELOG.md updated under [Unreleased] (if user-visible)
  • ARCHITECTURE.md / CLAUDE.md updated (if a module or interface changed) — subsystem 6 + 14, file map, begin() notes
  • New testable logic has a matching test in tests/
  • Branch is focused — refactors / behavior / tests are not mixed together

Related issues

Companion: DovesSensorEgg PR #16 (Phases 2+3 — the service this consumes). Either merges first; bench together.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AcdBRTn52QpCQWkyVm5Lkg


Generated by Claude Code

…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
@github-actions

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%)

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