Skip to content

SD watchdog follow-ups + release-build guards - #165

Open
TheAngryRaven wants to merge 5 commits into
BETAfrom
claude/project-thread-xnz9wt-sd
Open

TheAngryRaven wants to merge 5 commits into
BETAfrom
claude/project-thread-xnz9wt-sd

Conversation

@TheAngryRaven

Copy link
Copy Markdown
Owner

Requested by Dove · project thread

Summary

Follow-ups to #163 from the 4.2.0 release review of #160. One commit per finding so each can be cherry-picked or reverted on its own.

Before: after a soft reset the carried-over watchdog could still fire during the boot track scan on a big track set or slow card, which lands mid-SD-read and causes the wedge #163 fixes. A HAS_DEBUG build reset forever with no terminal attached. The BETA → master PR never compiled BETA source the way release.yml builds it, and a tag that disagreed with project.h would publish an update devices could never satisfy.

After: every unbounded SD walk feeds the watchdog, the release PR compiles the exact release configuration for both boards, and a mismatched tag fails before anything is built or published.

Commit Finding
SD boot: feed the WDT through the track-directory scan scanTrackDir() fed per entry; audit also feeds ensureDefaultSettings() (per key), buildReplayFileList() (per entry) and the OTA stage-file CRC readback (per 512 B block)
Debug boot: feed the WDT while waiting for the serial terminal while (!Serial) wdtPet();
CI docs: correct the stale DovesLapTimer-pin notes v4.3.0's src/ is identical to the library's BETA; comments in compile-sketch.yml and the CLAUDE.md sim row said otherwise
CI: compile the exact release configuration on the BETA -> master PR flags-off arm is now a matrix over both boards; on the release PR (head_ref == BETA) it uses the release library pin v4.3.0 and release.yml's exact flags
Release: refuse a tag that disagrees with FIRMWARE_VERSION first step of the tag build checks project.h's literal == tag and that CHANGELOG has a ## [x.y.z] heading

The version number itself is not changed here. It still needs setting before tagging.

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)
  • clang-tidy clean (CI)
  • Compiles for the XIAO nRF52840 Sense (CI; arduino-cli wasn't available locally)
  • Tested on real hardware

Also: the native sim passes all 6 ctests, both workflow YAMLs parse, and the tag-check script was run locally (v4.1.0 passes; v4.2.0 fails on the version; a bumped project.h with no CHANGELOG heading fails on the CHANGELOG). The watchdog feeds are Arduino-only code with no pure logic to unit-test.

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/ (no new pure logic)
  • Branch is focused

Related issues

Follow-up to #163; findings from the #160 release review.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SdhRyfZ4eYuooXi9uy28aw


Generated by Claude Code

scanTrackDir() opens and ArduinoJson-parses every file under /TRACKS and
/TRACKS/SPRINT (up to MAX_LOCATIONS, 8 KB each at the 2 MHz SPI clock)
inside one call. setup() only pets before and after buildTrackList(), so
on a soft-reset boot — where the WDT carried over from the previous
session is already counting (#163) — a large track set or a slow card
could outlast the ~4 s deadline and reset the device mid-SD-read: the
exact card wedge #163 exists to prevent. The same walk re-runs after a
BLE track upload/delete with the WDT armed.

Feed it per directory entry. Audit of the other SD walks reachable at
boot or under the armed WDT:
- ensureDefaultSettings(): one full file round trip per key (~25) inside
  SETTINGS_SETUP() — now fed per key.
- buildReplayFileList(): capped by .dovex files FOUND, not entries scanned,
  so unbounded over a busy root — now fed per entry.
- fwCrcOfStageFile(): reads back up to a 408 KiB image in 512 B blocks
  in one loop() iteration — now fed per block.
- BLE LIST/TLIST already feed per entry; parseTrackFile, the course
  creator write and sdPlanSprintPrune are a single bounded file each.

wdtPet() is a plain register write, a no-op before the WDT runs, and is
already prototyped for the sim TU (sim_prototypes.h). No host test: the
pets are Arduino glue with nothing pure to extract.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdhRyfZ4eYuooXi9uy28aw
HAS_DEBUG builds block in setup() on `while (!Serial);` right after the
single wdtBootCheck() pet. After a soft reset the WDT carried over from
the previous session is already counting, so with no terminal attached
the wait outlived the ~4 s deadline and the device reset — into the same
wait, forever. Feed it inside the wait. No other unbounded busy-wait
sits on the setup() path (the NVMC READY spins in the one-time UICR
write are microseconds, and setup() pets around that step already).

Developer builds only; nothing shipped defines HAS_DEBUG.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdhRyfZ4eYuooXi9uy28aw
compile-sketch.yml said a master/release build needed "a DovesLapTimer
release tag carrying CrossingEngine/SprintTimer" before BETA could be
promoted, and justified the flags-off arm's BETA library ref with
"pinning the release tag would fail on BETA-era library APIs". Both
stopped being true with v4.3.0: its src/ is byte-identical to the
library's BETA branch (git diff v4.3.0 origin/BETA -- src is empty).
CLAUDE.md's Simulator table also said the sim's CMakeLists pins
DovesLapTimer BETA "(matches CI channel)", but it defaults
DOVESLAPTIMER_REF to v4.3.0 and sim-build.yml overrides it to BETA only
for BETA-targeted builds.

Stale pin notes are how the wrong library gets shipped, so reword them
to what is true and name every place the pin lives. Comments/docs only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdhRyfZ4eYuooXi9uy28aw
The release PR's CI never built what release.yml ships. Its `compile`
job correctly follows the SOURCE channel (BETA library + beta flags),
and the flags-off `compile-stock-arm` job linked the library's BETA
branch and built the Sense board only. So BETA source met the pinned
release library (v4.3.0) — and the non-Sense board met the flags-off
configuration — for the first time on the tag push, after the merge.

compile-stock-arm is now a matrix over both boards, and on the release
PR (head_ref == BETA) it links the release pin instead of BETA, with
exactly release.yml's extra_flags. On feature PRs into BETA and BETA
pushes it keeps the BETA library so the flags stay the only variable.
The Sense job keeps its old check name.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdhRyfZ4eYuooXi9uy28aw
release.yml derives the OTA manifest's version from the git tag, but
the image reports FIRMWARE_VERSION from BirdsEye/project.h over BLE, and
nothing tied the two together. Tagging v4.2.0 on a tree whose project.h
still says "4.1.0" published a manifest no device could ever satisfy:
every unit would install the update, still report 4.1.0, and be offered
it again forever.

Add a first step to the build job, tag builds only, that extracts the
quoted literal from the non-override FIRMWARE_VERSION branch (asserting
exactly one exists), fails unless it equals ${GITHUB_REF_NAME#v}, and
also requires a `## [x.y.z]` CHANGELOG heading. It fails before the
compile, and release/pages need `build`, so nothing is published.
Manual (workflow_dispatch) runs have no tag and skip it. The version
number itself is unchanged.

Verified locally by running the step's script against v4.1.0 (pass),
v4.2.0 and v4.0.0 (version mismatch), and a patched tree at 9.9.9 with
no CHANGELOG heading (heading failure).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdhRyfZ4eYuooXi9uy28aw
@TheAngryRaven TheAngryRaven self-assigned this Sep 27, 2026
@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%)

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