plan 0006: NeoPixel strip subsystem — pace pip, RPM scale, status flashers, purple sectors - #142
Open
TheAngryRaven wants to merge 8 commits into
Open
plan 0006: NeoPixel strip subsystem — pace pip, RPM scale, status flashers, purple sectors#142TheAngryRaven wants to merge 8 commits into
TheAngryRaven wants to merge 8 commits into
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011PaL2sQSbU3KCPF4FaZsPP
…ction led_frame owns the pixel layout and the single global-brightness choke point (applyCap; post-condition tested channel<=cap). led_modes owns the pace pip (ms/m, slower=left/red), the generic ScaleSpec fill (RPM red past halfway), and the StatusAction hysteresis/flash table — the phase-2 assignability hook. led_animations renders boot + purple as pure functions of (t, seed). sector_purple detects session-best sectors with open-time best snapshots and a derived S3 so the library's lap-line updateBestSectors() can't race the comparison. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011PaL2sQSbU3KCPF4FaZsPP
BIRDSEYE_ENABLE_NEOPIXEL (default 0, beta-on) guards the whole subsystem — a flag-off build never writes UICR or touches the NFC pads. led_brightness (0-255, 0 = disabled) and rev_limit (true RPM, 1000-20000) join the settings table and the boot read block. Three sprint-first activeTimer* sector accessors feed the purple monitor; WaypointLapTimer sessions report no sectors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011PaL2sQSbU3KCPF4FaZsPP
…p hooks
neopixel.{h,ino}: one-time UICR NFC->GPIO write (before SoftDevice/WDT,
single self-reset), boost EN + strip bring-up, 30 Hz frame loop
(priority: boot anim > purple anim > parked/menu off > race pace-or-RPM
strip + status flashers), applyCap as the single brightness choke
point. Wired into setup() (pre-CAMERA_SETUP), the main frame after
CAMERA_LOOP, both parked branches (blank, not freeze), enterShutdown
(blank -> data LOW -> boost EN LOW, retained through System OFF) and
the charging soft-resume. Sim: module excluded from the TU, surface
stubbed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011PaL2sQSbU3KCPF4FaZsPP
All three build workflows install Adafruit NeoPixel; beta.yml and BETA-targeted compile-sketch runs pass -DBIRDSEYE_ENABLE_NEOPIXEL=1. Master/release keep the flag off. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011PaL2sQSbU3KCPF4FaZsPP
…flag table, CHANGELOG Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011PaL2sQSbU3KCPF4FaZsPP
Coverage — host-testable units📂 Overall coverage
📄 File coverage
|
…pe include order const-qualify the locals clang-tidy flagged (misc-const-correctness), replace the (int)(x + 0.5f) casts with lroundf (bugprone-incorrect-roundings), and pull led_frame.h into neopixel.h: Arduino's generated prototype for npxPushFrame(led_frame::Frame&) lands before neopixel.ino's own includes, so the type must be visible from BirdsEye.ino's include block — the exact include-order trap documented in CLAUDE.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011PaL2sQSbU3KCPF4FaZsPP
Chain pixel 0 is the RIGHTMOST LED on this build, which mirrors the entire chain (status LEDs included), not just the strip — so the strip-only kStripReversed flip is replaced by kChainReversed + led_frame::physicalIndex(): renderers stay in logical left-to-right space and the push path maps logical->wire once. Involution-tested. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011PaL2sQSbU3KCPF4FaZsPP
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
New NeoPixel strip subsystem (plan 0006, design record in
docs/plans/0006-neopixel-strip.md): 11 WS2812 pixels on the NFC pads converted to GPIO — pixels 0/10 are status alert LEDs, pixels 1–9 the strip with a centerline. Beta-channel only viaBIRDSEYE_ENABLE_NEOPIXEL(SensorEgg precedent); master/release compile it out entirely.#defines inneopixel.h— swap if wired the other way.led_frame::physicalIndex()(kChainReversed = true) maps logical→wire once, at push time.UICR->NFCPINSwrite inNEOPIXEL_SETUP()+ a single self-reset, instead of-DCONFIG_NFCT_PINS_AS_GPIOS(that define lives in the core'ssystem_nrf52840.c, whichcompiler.cpp.extra_flagscan't reach, and would miss IDE builds). Runs before the SoftDevice comes up and before the WDT arms. One-way, as discussed — first beta boot on existing units will reset once.led_frame::applyCap()is the single choke point, applied once per frame at push time; host tests sweep caps × saturated frames asserting no channel ever exceeds it. Newled_brightnesssetting (default 64/255; 0 disables the LEDs and never enables the boost).rev_limitsetting, default 15000) until pace is valid, then the pace pip —getPaceDifference()ms/m, full deflection ±1.0 ms/m, slower = left of center in red, faster = right in green, deadband = dim-white centerline.ScaleSpecis generic so temp scales reuse it in phase 2.StatusActionPODs (source/threshold/hysteresis/color/flash) — the phase-2 assignability hook. Defaults: logical-left = rev-limit flasher (red, ≥ rev_limit, 100 ms), logical-right = EGT flasher (orange, ≥ 650 °C, 250 ms; NaN/stale → off AND latch released,isNanF-guarded).(t, seed), golden-testable.sector_purpleunit): the library updates best sectors at the start/finish crossing, not at sector lines, so the monitor snapshots bests at sector open and derives S3 fromlastLapTime − s1 − s2at the lap edge — immune to the lap-line race (explicitly tested). No purple on lap 1. Three new sprint-firstactiveTimer*sector wrappers feed it.NEOPIXEL_LOOP()(30 Hz self-throttled) afterCAMERA_LOOP()and inside both parked branches (blanks instead of freezing);NEOPIXEL_SLEEP()inenterShutdown()(blank → data LOW → EN LOW, strip dark while charging too);NEOPIXEL_WAKE()in the charging soft-resume.show()uses a free PWM instance via EasyDMA with interrupts ON (~0.4 ms/frame); this sketch uses no tone/analogWrite, so PWM0–2 are free and TIMER3/tach ISRs are unaffected.Type of change
How it was verified
ctest --test-dir tests/build) — 433 cases / 5249 assertions, including 4 new suites (cap invariant + chain mirror, pip/scale/status tables, animation determinism + lifecycle, purple detection incl. the S3 lap-line race)clang-tidyclean — new units added to the workflow list; verified locally and in CIBETA(also the compile-check of the sector accessor names)kChainReversed, set for data-in-right wiring), real current at cap 64, sleep current with EN lowChecklist
CHANGELOG.mdupdated under[Unreleased](bumped slated version to 4.1.0)ARCHITECTURE.md/CLAUDE.mdupdated (File Map, pin map, subsystem 16, key constants, settings, libraries, flags) +CONTRIBUTING.mdflag tabletests/Related issues
None — user-requested feature (phase 2, settings-driven mode/action assignment, deliberately deferred; the POD tables are the interface it fills in).
🤖 Generated with Claude Code
https://claude.ai/code/session_011PaL2sQSbU3KCPF4FaZsPP