diff --git a/.github/workflows/compile-sketch.yml b/.github/workflows/compile-sketch.yml index dbd9e92..5e07000 100644 --- a/.github/workflows/compile-sketch.yml +++ b/.github/workflows/compile-sketch.yml @@ -37,7 +37,10 @@ jobs: # SensorEgg POC and the NeoPixel strip exactly as beta.yml does, so # the flag-on build is compile-checked on the PR rather than first # failing on the publish workflow. Everything else builds the - # master/release defaults (off). + # project.h defaults — which since 4.1.0 means the NeoPixel strip is + # ON (it is a core feature now; the flag defaults to 1) and only the + # SensorEgg POC is off. So the difference between the two arms is + # now just SensorEgg and the DovesLapTimer ref. FEATURE_FLAGS: ${{ (github.base_ref == 'BETA' || github.head_ref == 'BETA' || github.ref_name == 'BETA') && '-DBIRDSEYE_ENABLE_SENSOREGG=1 -DBIRDSEYE_ENABLE_NEOPIXEL=1' || '' }} # Build both XIAO nRF52840 variants. The Sense board has the onboard # LSM6DS3 IMU; the plain board does not (accelerometer logging degrades diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 05f16b3..ccdc70c 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -142,8 +142,10 @@ to the matching `*_LOOP()`. `led_brightness` by day, `led_brightness_night` after dark (see *Local time* below). The strip's 5 V boost converter has its EN pin driven low in sleep, so System OFF really - powers the LEDs down. Gated on `BIRDSEYE_ENABLE_NEOPIXEL`: on in - beta, off (fully compiled out, no UICR write) in master/release. + powers the LEDs down. `BIRDSEYE_ENABLE_NEOPIXEL` is on in **every** + channel as of 4.1.0, which is what makes the strip a core feature — at + the price of a one-way, fleet-wide UICR NFC→GPIO conversion on the + first boot after updating. - **Course creator** (`course_creator` + `track_json` pure units, glued into the menu/pages/SD modules) — authors a track course on the device by walking to each cone and holding for a 3 s GPS average. Autocross diff --git a/BirdsEye/BirdsEye.ino b/BirdsEye/BirdsEye.ino index 3f13b6f..cc6bfec 100644 --- a/BirdsEye/BirdsEye.ino +++ b/BirdsEye/BirdsEye.ino @@ -1078,8 +1078,10 @@ void setup() { // beta channel): the one-time UICR NFC->GPIO write needs direct NVMC // access, which is illegal once the SoftDevice is up. Also before // wdtSetup() so the one-time self-reset can't race the watchdog. Needs - // SETTINGS_SETUP (led_brightness) — a no-op unless - // BIRDSEYE_ENABLE_NEOPIXEL is set (beta channel only). + // SETTINGS_SETUP (led_brightness). Since 4.1.0 this runs on EVERY + // channel — BIRDSEYE_ENABLE_NEOPIXEL defaults to 1 — so the first boot + // of any 4.1.0+ image is the one that spends the NFC pads and resets + // once. See project.h. NEOPIXEL_SETUP(); // Camera auto-record: load the persisted Insta360 serial + init the FSM diff --git a/BirdsEye/project.h b/BirdsEye/project.h index 9d8ec19..3a82384 100644 --- a/BirdsEye/project.h +++ b/BirdsEye/project.h @@ -31,10 +31,10 @@ #ifdef FIRMWARE_VERSION_OVERRIDE #define FIRMWARE_VERSION _BE_TOSTRING(FIRMWARE_VERSION_OVERRIDE) #else - // The 4.0.0 release cut (matches the v4.0.0 tag). The webapp still keys + // The 4.1.0 release cut (matches the v4.1.0 tag). The webapp still keys // the track JSON budget off this — 8 KB at or above 3.2.0 — so never // stamp a build below that line again. - #define FIRMWARE_VERSION "4.0.0" + #define FIRMWARE_VERSION "4.1.0" #endif /////////////////////////////////////////// @@ -84,21 +84,31 @@ // ---- NeoPixel strip (11 px: 2 status + 9-px pace/RPM strip) ---- // -// 0 (default — master and release): the whole subsystem is compiled out. -// The module's entry points become no-ops and, critically, the firmware -// NEVER writes UICR->NFCPINS and never drives pins 30/31 (P0.09/P0.10, -// the NFC pads) — a flag-off build leaves the pads exactly as it found -// them. +// 1 (default — master, beta and release all ship this as of 4.1.0): the +// strip is a CORE feature, present in every image so a logger works the +// moment someone wires LEDs to it. On first boot NEOPIXEL_SETUP() +// converts the NFC pads to GPIO by programming UICR->NFCPINS and +// self-resets once so the pin latch takes effect. After that: pin 30 = +// boost converter EN, pin 31 = WS2812 data. See plan 0006 and neopixel.h. // -// 1 (the beta channel passes -DBIRDSEYE_ENABLE_NEOPIXEL=1): on first -// boot NEOPIXEL_SETUP() converts the NFC pads to GPIO by programming -// UICR->NFCPINS (a ONE-WAY change — undoing it needs a full chip erase, -// i.e. a bootloader reflash; accepted, NFC is never used on this -// hardware) and self-resets once so the pin latch takes effect. After -// that: pin 30 = boost converter EN, pin 31 = WS2812 data. See plan -// 0006 and neopixel.h. +// KNOW WHAT THIS COSTS, because it is charged to every unit in the field, +// not just the ones with LEDs on them. The UICR write is ONE-WAY — undoing +// it needs a full chip erase, i.e. a bootloader reflash over USB — so the +// first boot after updating to 4.1.0 permanently spends the NFC pads and +// reboots itself once, on every device, wired for LEDs or not. That was the +// deliberate 4.1.0 decision (NFC is not used on this hardware and the pads +// are otherwise idle); it is recorded here rather than in a commit message +// because nothing about a later build can undo it. +// +// 0 (no shipped channel sets this; -DBIRDSEYE_ENABLE_NEOPIXEL=0 forces it): +// the subsystem is compiled out — no Adafruit_NeoPixel dependency, and on a +// board that has NOT already been converted the firmware never writes UICR +// and never drives pins 30/31. On one that HAS (it ran a flag-on build +// before), the stubs still hold the boost EN pin low, because a floating EN +// leaves the 5 V rail up through System OFF — see the #else block in +// neopixel.ino. #ifndef BIRDSEYE_ENABLE_NEOPIXEL - #define BIRDSEYE_ENABLE_NEOPIXEL 0 + #define BIRDSEYE_ENABLE_NEOPIXEL 1 #endif /////////////////////////////////////////// diff --git a/BirdsEye/settings.ino b/BirdsEye/settings.ino index 876378a..02ec442 100644 --- a/BirdsEye/settings.ino +++ b/BirdsEye/settings.ino @@ -139,9 +139,9 @@ static void ensureDefaultSettings() { // starts the rotation at the speed page — end users never see the // diagnostic counters; "show" restores them for development/tuning. { "debug_pages", "hide" }, - // NeoPixel strip (plan 0006). Written on every channel so - // SETTINGS.json has a uniform shape (SensorEgg DOVEX-column - // precedent); only a BIRDSEYE_ENABLE_NEOPIXEL build reads them. + // NeoPixel strip (plan 0006). Read on every channel since 4.1.0 — + // BIRDSEYE_ENABLE_NEOPIXEL defaults to 1, so these are live + // settings on a stock logger, not beta-only bookkeeping. { "led_brightness", "64" }, // global cap 0-255; 0 = LEDs disabled { "rev_limit", "15000" }, // true RPM: LED scale ceiling + rev flasher + OVER REV header // Plan 0007: the PROBLEM limit (0 = disabled) — whole LED chain diff --git a/CHANGELOG.md b/CHANGELOG.md index 4544d7a..6c8a712 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,12 +12,45 @@ and this project aims to follow [Semantic Versioning](https://semver.org/spec/v2 ## [Unreleased] -Slated to release as **4.1.0** (minor — new NeoPixel subsystem on the -beta channel, plus the fixes below). +Nothing yet. + +## [4.1.0] - 2026-08-22 + +MINOR — new settings and device behaviour, all backwards compatible. Track +files, the DOVEX log format, the log filenames and the BLE command protocol +are byte-for-byte unchanged from 4.0.0, so 4.0.0 logs, tracks and companion +apps keep working. + +> ### Read this before updating +> +> **This release permanently converts the two NFC pads to GPIO, on every +> device, and reboots once while doing it.** +> +> The LED strip stops being a beta-only experiment in 4.1.0 and ships in the +> normal firmware, so a logger lights up the moment someone wires a strip to +> it — no special build. Making those two pads usable as GPIO means writing +> the chip's UICR, and **that write cannot be undone by any later firmware**; +> reversing it needs a full chip erase and a bootloader reflash over USB. +> Your logger does the write the first time it boots after updating, then +> resets itself once so the change takes effect. That single extra reboot +> during the update is expected — nothing is wrong. +> +> This happens whether or not you have LEDs attached, because the firmware +> cannot know. The trade was made deliberately: this hardware does not use +> NFC for anything, the pads are otherwise idle, and requiring a separate +> build to use a headline feature had kept it out of everyone's hands. If you +> have some future use for the NFC pads on your device, **do not install +> 4.1.0.** +> +> With no strip wired, nothing else changes — the pins simply sit there. + +The SensorEgg wireless-EGT proof of concept remains beta-only +(`BIRDSEYE_ENABLE_SENSOREGG`, off in the published images), but it gained +nothing this release — so every entry below is live for every user. ### Added -- **NeoPixel LED strip subsystem** (beta channel only, - `BIRDSEYE_ENABLE_NEOPIXEL`, plan 0006): 11 WS2812 pixels on the NFC +- **NeoPixel LED strip subsystem** (plan 0006) — **now in every build**, + see the upgrade note above: 11 WS2812 pixels on the NFC pads converted to GPIO — 2 status indicators + a 9-px strip with a centerline. A global brightness cap (`led_brightness` setting, 0 disables the LEDs entirely) that no LED can ever exceed; a pace pip @@ -30,8 +63,7 @@ beta channel, plus the fixes below). sleep truly powers the LEDs off. **First boot of a flag-on build performs a one-way NFC-pads-to-GPIO conversion (UICR write) and resets once.** -- **LED day/night brightness + a device timezone** (beta channel, plan - 0010): new `utc_offset_min` setting (minutes east of UTC, default 0) +- **LED day/night brightness + a device timezone** (plan 0010): new `utc_offset_min` setting (minutes east of UTC, default 0) gives the device a local wall clock, and the LED strip swaps to `led_brightness_night` (default 16) between `led_night_start_hour` (19) and `led_day_start_hour` (7) — local hours, so 7am is the @@ -41,12 +73,14 @@ beta channel, plus the fixes below). every filename stay exactly as they were, and timezone presentation remains the viewing app's job. - **Overrev alert** (plan 0007): new `overrev_limit` setting (default 0 - = disabled) — past it the whole LED chain flashes red until RPM falls - back below the normal `rev_limit`. The rev limit warns the engine is - at its ceiling; the overrev limit says it's broken. + = disabled) — past it the whole LED chain flashes red and the tach + page's `*OVER REV*` header trips, both until RPM falls back below the + normal `rev_limit`. The rev limit warns the engine is at its ceiling; + the overrev limit says it's broken. - **Temp1 alert threshold setting** (`temp1_alert_c`, default 650 °C): - the right status LED is now a tri-state — flashing red at/above the - limit, off when good, solid blue when there is no probe signal. + the right status LED is now a tri-state — flashing red + at/above the limit, off when good, solid blue when there is no probe + signal. - **`tach_filter` setting** (default `smooth`, plan 0009): picks the RPM estimator, so the tach can be A/B'd against a live engine at the track instead of argued about from a plotted log. `smooth` is the new filter @@ -60,9 +94,13 @@ beta channel, plus the fixes below). thrown away this power-cycle. A count that climbs with RPM is ignition ringing or missed sparks reaching the ISR, i.e. the pickup rather than the filter. -- **GPS-search pip**: in race mode without a full GPS lock the strip - shows a green pixel bouncing end-to-end instead of the RPM scale, so - a not-yet-timing session is visibly "searching". +- **GPS-search pip**: in race mode without a full GPS lock the strip shows a green pixel bouncing end-to-end instead of the + RPM scale, so a not-yet-timing session is visibly "searching". +- **The browser-sim harness can fake a GPS fix.** A "GPS fix" toggle (plus + an mph field) streams a deterministic synthetic 25 Hz fix parked on the + bundled OKC track's start line, so fix-gated flows — most usefully the + on-device course creator, including its 3 s point-averaging hold — can be + exercised in the simulator without loading a log file. ### Changed - **Settings file and JSON document buffers raised 512 -> 1024 bytes** @@ -100,16 +138,14 @@ beta channel, plus the fixes below). a flat ~20 RPM (worst case under 300) from 1500 to 14 000 RPM. The cost is ~90 ms more lag on a 5500 RPM/s pull. Set `tach_filter` to `legacy` to get the old behaviour back. -- **Engine dies mid-session** (tach-proven sessions): the LED bar goes - dark and the pace page shows `STOPPED` instead of a still-counting - pace — status LEDs (temp alert) stay live. Clears on restart. +- **Engine dies mid-session** (tach-proven sessions): the pace page shows + `STOPPED` instead of a still-counting pace, and the LED bar goes dark + while the status LEDs (temp alert) stay live. Clears on restart. - **Tach page `*OVER REV*` header** now means actual overrev: it shows only when `overrev_limit` is enabled and RPM reaches it (was a hardcoded 9999 RPM). The `rev_limit` warning stays on the LED only. - **Temp status LED flashes red** (was orange), matching the rev flasher's alert language. - -### Changed - **Bluetooth downloads are faster, and now say why when they are not** (plan 0008). A 3.3 MB session downloading at 28.8 KB/s on an iPad prompted a look at the whole transfer path. Three things were capping @@ -179,13 +215,6 @@ beta channel, plus the fixes below). (16 columns after the `Temp1`/`Junction1`/`Temp2` additions) injected nothing. It now accepts 13+ and reads the stable first 13. -### Added -- **The browser-sim harness can fake a GPS fix.** A "GPS fix" toggle (plus - an mph field) streams a deterministic synthetic 25 Hz fix parked on the - bundled OKC track's start line, so fix-gated flows — most usefully the - on-device course creator, including its 3 s point-averaging hold — can be - exercised in the simulator without loading a log file. - ## [4.0.0] - 2026-08-10 ### Added @@ -1419,7 +1448,9 @@ Initial tagged release. Core capabilities: - 8+ OLED display pages, Bluetooth LE file download / settings / track sync, and a low-power sleep mode. -[Unreleased]: https://github.com/TheAngryRaven/DovesDataLogger/compare/v3.1.0...HEAD +[Unreleased]: https://github.com/TheAngryRaven/DovesDataLogger/compare/v4.1.0...HEAD +[4.1.0]: https://github.com/TheAngryRaven/DovesDataLogger/compare/v4.0.0...v4.1.0 +[4.0.0]: https://github.com/TheAngryRaven/DovesDataLogger/compare/v3.1.0...v4.0.0 [3.1.0]: https://github.com/TheAngryRaven/DovesDataLogger/compare/v3.0.2...v3.1.0 [3.0.2]: https://github.com/TheAngryRaven/DovesDataLogger/compare/v3.0.1...v3.0.2 [3.0.1]: https://github.com/TheAngryRaven/DovesDataLogger/compare/v3.0.0...v3.0.1 diff --git a/CLAUDE.md b/CLAUDE.md index b070ef7..4588b86 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -67,7 +67,7 @@ Core capabilities: DovesSensorEgg thermocouple pod's advertising broadcasts (`PW-ADV` v1 and v2), logs `Temp1`/`Junction1`/`Temp2` DOVEX columns + Temp1/Temp2 race pages (subsystem 14) -- **NeoPixel strip (beta)**: 11 WS2812 pixels on the NFC pads converted +- **NeoPixel strip**: 11 WS2812 pixels on the NFC pads converted to GPIO — 2 status alert LEDs + a 9-px pace-pip / RPM-scale strip with a global brightness cap, boot animation, and a purple session-best sector celebration (subsystem 16) @@ -176,7 +176,7 @@ handoff spec. | Path | Contents | |---|---| -| `.github/workflows/` | CI: compile-sketch (+ flash-size gate), arduino-lint, unit-tests, clang-tidy, coverage, sim-build (native sim TU + 60 s boot soak + determinism + goldens + lap oracles + two-session carryover, plus a wasm job: emsdk 3.1.61 build + node smoke + `birdseye-sim-wasm` artifact), release (dual-board build + GitHub Release + prod OTA manifest to `gh-pages`), beta (dual-board build on `BETA`-branch push → latest-only `beta/` OTA channel on `gh-pages`, no Release). Per-channel build config: `BETA` builds track DovesLapTimer's `BETA` branch and pass `-DBIRDSEYE_ENABLE_SENSOREGG=1`; master/release pin `v4.3.0` and build the all-flags-off defaults | +| `.github/workflows/` | CI: compile-sketch (+ flash-size gate), arduino-lint, unit-tests, clang-tidy, coverage, sim-build (native sim TU + 60 s boot soak + determinism + goldens + lap oracles + two-session carryover, plus a wasm job: emsdk 3.1.61 build + node smoke + `birdseye-sim-wasm` artifact), release (dual-board build + GitHub Release + prod OTA manifest to `gh-pages`), beta (dual-board build on `BETA`-branch push → latest-only `beta/` OTA channel on `gh-pages`, no Release). Per-channel build config: `BETA` builds track DovesLapTimer's `BETA` branch and pass `-DBIRDSEYE_ENABLE_SENSOREGG=1`; master/release pin `v4.3.0` and build the `project.h` defaults — which since 4.1.0 means NeoPixel ON, SensorEgg off | | `tests/` | Host doctest harness (CMake) for the pure-logic units | | `docs/plans/` | Numbered design records (`NNNN-slug.md`, see its README) — the rationale behind each chunk of work; plan-executing commits cite the number. Same convention as DovesDataViewer | | `CHANGELOG.md` | Keep-a-Changelog history; release workflow ties to version tags | @@ -1312,12 +1312,17 @@ hardware needs no power switch. Wake = chip reset = fresh `setup()`. ### 16. NeoPixel Strip (`neopixel.{h,ino}`, `led_frame/led_modes/led_animations/sector_purple.{h,cpp}`) -- **BUILD FLAG — `BIRDSEYE_ENABLE_NEOPIXEL` (`project.h`)**: `0` - (master/release default) compiles the whole subsystem down to no-op - `NEOPIXEL_*` entry points — no UICR write, no pin driving, no - Adafruit NeoPixel dependency in the image. `1` (beta channel, passed - by `beta.yml` + BETA-targeted `compile-sketch.yml` runs) is - everything below. +- **BUILD FLAG — `BIRDSEYE_ENABLE_NEOPIXEL` (`project.h`)**: `1` on + **every** channel as of 4.1.0 — master, beta and release — so the strip + is a core feature rather than a special build. Everything below is in + every image. The cost is charged fleet-wide and cannot be taken back: + the first boot after updating performs the ONE-WAY UICR NFC→GPIO + conversion and self-resets once, on every device, LEDs wired or not. + `0` (no shipped channel sets it) compiles the subsystem out — no + Adafruit NeoPixel dependency, and no UICR write on a board that has not + already been converted; on one that HAS, the `#else` stubs still hold + boost EN low, because a floating EN leaves the 5 V rail up through + System OFF. - **Hardware**: 11 WS2812 pixels fed by an Adafruit 5 V boost converter. Pixels 0 and 10 are status indicators; pixels 1–9 are the strip with pixel 5 the centerline. Pin 30 (P0.09/NFC1) drives the boost EN @@ -1699,7 +1704,7 @@ the one loaded). Sector lines stay optional — zero, one, or two. | SensorEgg scanner self-heal | 30 s no packet → stop+start kick | `sensoregg_protocol.h` | | SensorEgg RSSI floor | −90 dBm | `sensoregg_protocol.h` | | SensorEgg pairing MAC | `SENSOREGG_MAC` (all-zeros = any egg) | `sensoregg.h` | -| NeoPixel strip flag | `BIRDSEYE_ENABLE_NEOPIXEL`, default 0; 1 on the beta channel | `project.h` | +| NeoPixel strip flag | `BIRDSEYE_ENABLE_NEOPIXEL`, default **1** on every channel since 4.1.0 | `project.h` | | NeoPixel pins | 30 = boost EN, 31 = data (NFC pads, post-UICR) | `neopixel.h` | | NeoPixel layout | 11 px: status 0 + strip 1–9 (center px 5) + status 10 | `led_frame.h` | | LED frame rate | 30 Hz (`NPX_FRAME_INTERVAL_MS` 33) | `neopixel.ino` | @@ -1781,8 +1786,9 @@ This device operates in ignition-noise environments. Three layers of defense: `compiler.cpp.extra_flags` property — a second `--build-property` for one key replaces the first). Local setup: CONTRIBUTING.md "Local build flags". -- **Feature flags** (`project.h`, all default `0`, all tested with `#if` - so an explicit `-DFLAG=0` wins): +- **Feature flags** (`project.h`, tested with `#if` so an explicit + `-DFLAG=0` wins; `BIRDSEYE_ENABLE_NEOPIXEL` defaults to `1`, the rest + to `0`): - `BIRDSEYE_ENABLE_ONBOARD_CHARGING` — off in **every** channel. See subsystem 10: HICHG hold + the USB charging UX. The hardware now has an external charging circuit. @@ -1790,10 +1796,12 @@ This device operates in ignition-noise environments. Three layers of defense: (`beta.yml`, plus `compile-sketch.yml` for PRs targeting `BETA` so the flag-on build is compile-checked before it reaches the publish workflow). See subsystem 14. - - `BIRDSEYE_ENABLE_NEOPIXEL` — off in master/release, **on in beta** - (same two workflows as SENSOREGG). First flag-on boot performs the - ONE-WAY UICR NFC→GPIO conversion and self-resets once. See - subsystem 16. + - `BIRDSEYE_ENABLE_NEOPIXEL` — **on everywhere since 4.1.0** (the + `project.h` default; no workflow needs to pass it). The first boot of + any 4.1.0+ image performs the ONE-WAY UICR NFC→GPIO conversion and + self-resets once, on every device. This is the one flag whose default + is 1 — see subsystem 16 and the upgrade note at the top of + CHANGELOG.md's 4.1.0 section. When adding a flag: give it a `#ifndef` default in `project.h`, decide its per-channel value in the workflows, and document it here + in CONTRIBUTING.md's flag table. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 374b285..2bacb00 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -80,7 +80,7 @@ so an explicit `-DFLAG=0` still turns the feature off. |---|---|---|---| | `BIRDSEYE_ENABLE_ONBOARD_CHARGING` | `0` | nothing — off in every channel | Holds the BQ25100 HICHG pin high for ~100 mA fast charge and runs the USB charging UX (VBUS wake shortcuts to the charge screen; the main menu drops into the charging loop after `USB_MENU_CHARGE_IDLE_MS`). Off, the firmware leaves HICHG alone and an external charging circuit owns the battery. The VBUS park at shutdown happens either way — see ARCHITECTURE.md. | | `BIRDSEYE_ENABLE_SENSOREGG` | `0` | `beta.yml`, and `compile-sketch.yml` for PRs targeting `BETA` | Compiles in the wireless-EGT POC: passive BLE scanner, Temp1 race page, and BLE core up at boot. Off, the accessors return NaN, so `Temp1`/`Junction1` still log as `nan` and the log format is unchanged. | -| `BIRDSEYE_ENABLE_NEOPIXEL` | `0` | `beta.yml`, and `compile-sketch.yml` for PRs targeting `BETA` | Compiles in the NeoPixel strip subsystem (plan 0006): on first boot it programs `UICR->NFCPINS` to convert the NFC pads to GPIO (**one-way** — undoing it needs a full chip erase) and self-resets once, then drives pin 30 as the 5 V boost EN and pin 31 as WS2812 data. Off, the firmware never touches UICR or those pins. Needs the `Adafruit NeoPixel` library. | +| `BIRDSEYE_ENABLE_NEOPIXEL` | **`1`** | every channel (the `project.h` default — no workflow passes it) | Compiles in the NeoPixel strip subsystem (plan 0006): on first boot it programs `UICR->NFCPINS` to convert the NFC pads to GPIO (**one-way** — undoing it needs a full chip erase) and self-resets once, then drives pin 30 as the 5 V boost EN and pin 31 as WS2812 data. **The only flag defaulting to 1**, since 4.1.0 — that conversion is therefore charged to every device in the field, LEDs wired or not. Forcing `=0` compiles it out; on a board already converted the stubs still hold boost EN low. Needs the `Adafruit NeoPixel` library. | ### arduino-cli The exact invocation CI uses is in