From cd555c9b746a0a21be041fa3938f0ff31d0094fc Mon Sep 17 00:00:00 2001 From: ewowi Date: Mon, 21 Sep 2026 11:37:57 +0200 Subject: [PATCH 1/5] Add Ethernet board presets and three UI modes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A board picks its Ethernet wiring by name instead of eleven pin rows, and the UI now has user, expert and developer modes so a card shows what its reader needs. Adds a FAQ page, and check_docgen becomes a ratchet that refuses a rising warning count per rule as well as on the total. KPI: 16384lights | Desktop:1924KB | tick:135/89/372/2/6/126/293/22/1454/4/257/68/225/16/21/11/2083/118/34/5/174/47/4us(FPS:7407/11235/2688/500000/166666/7936/3412/45454/687/250000/3891/14705/4444/62500/47619/90909/480/8474/29411/200000/5747/21276/250000) | ESP32:1954KB | src:270(68826) | test:206(45107) | lizard:265w **Core** - `NetworkModule` gains an `ethBoard` select over five presets (Classic RMII, Classic RMII (no reset), P4-NANO, S31 CoreBoard, Custom), mirroring `Hub75Driver`'s board-preset shape. A known board writes its map and hides the eleven pin rows; hidden stays bound, so the values still drive the interface. Persisted by label, since the option list is filtered per build and an index would name a different board. - The preset is applied in `defineControls` rather than `onControlChanged`: the config-restore path fires no change callback, so a saved `ethBoard` would otherwise select a row and never reach the pins. - Three UI mode levels (`kModeUser`, `kModeExpert`, `kModeDeveloper`) replace the `advanced` flag on `ControlDescriptor`, each level cumulative over the one below. - `previewsEthernetControls`: the desktop builds the Ethernet controls with no interface behind them, tagged developer-mode, so the presets can be exercised without hardware. Every bring-up path still keys on `hasEthernet`. **UI** - The on/off button is built only where `respectsEnabled()` is true; it did nothing on the seven modules that keep ticking regardless. - The stats line shows from expert mode up, and holding it peeks at the rate instead of latching a mode. - Distinct glyphs per tier: expert ๐ŸŽš๏ธ, developer ๐Ÿ”ง. **Scripts/MoonDeck** - `check_docgen` refuses a warning count that rises against the committed report, per rule and on the total. The per-rule half compared internal keys against display labels, so it matched nothing; both key spaces are now the report's labels, and the baseline parser is anchored to the `## By rule` table. - `check_devices` validates `ethBoard` against the preset table parsed out of `NetworkModule.h`, and binds the three preset rows that restate a chip's `ethConfigDefault` so the two homes cannot drift. - `check_prose` control fixture said `color` while claiming a British spelling, so it could never produce its two expected alerts and the check failed without ever linting. **Tests** - The ratchet test drives `_ratchet` itself; it previously asserted against a local copy of the comparison and passed while the real function compared nothing. **Docs/CI** - New FAQ page, every entry linked to the page that answers it. - A register rule in the documentation standards: no sentence whose job is tone. - Backlog: the two shipped review findings removed; the QuinLED Dig-Next-2 W5500 wiring recorded, including why RMII is impossible on that package and why SPI boards stay Custom. **Reviews** - ๐Ÿ‘พ Card name, tags and stats sat inside the `respectsEnabled` guard, so every always-on module rendered a nameless card โ†’ fixed. - ๐Ÿ‘พ The per-rule ratchet compared key spaces that never intersect, shipping a guarantee it did not enforce โ†’ fixed, with a test that fails against the old code. - ๐Ÿ‘พ The installer read Ethernet as unconfigured on all seven preset boards โ†’ `ethConfigured` now accepts a non-Custom preset. - ๐Ÿ‘พ The `ethBoard` handler re-ran a rebuild the scheduler had already done โ†’ removed. - ๐Ÿ‘พ Three preset rows duplicate `ethConfigDefault` with nothing binding them โ†’ a drift check, control-checked. - ๐Ÿ‘พ Three stale doc lines (a half-finished sentence, `expertMode`, a click gesture) โ†’ fixed. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 2 + docs/contributing/documentation-standards.md | 3 +- docs/how-to/faq.md | 52 ++++++ docs/moonmodules/core/system.md | 2 +- docs/moonmodules/core/ui.md | 10 +- docs/reference/metrics/docgen.md | 13 +- docs/reference/metrics/repo-health.json | 126 +++++++------- docs/reference/metrics/repo-health.md | 70 ++++---- docs/work/future/backlog-core.md | 39 ++++- mkdocs.yml | 3 + moondeck/MoonDeck.md | 4 + moondeck/check/check_devices.py | 73 +++++++- moondeck/check/check_docgen.py | 77 ++++++++- moondeck/check/check_prose.py | 2 +- mooninstaller/deviceModels.json | 52 +----- mooninstaller/install.js | 8 +- src/core/module/Control.h | 18 +- src/core/module/MoonModule.h | 2 +- src/core/system/HttpServerModule.cpp | 5 +- src/core/system/NetworkModule.h | 155 +++++++++++++++-- src/core/system/SystemModule.h | 9 +- src/platform/desktop/platform_config.h | 3 + src/platform/esp32/platform_config.h | 3 + src/ui/app.js | 157 ++++++++++-------- src/ui/style.css | 20 ++- test/python/test_check_docgen.py | 54 ++++++ .../scenario_MoonModule_control_change.json | 26 +-- .../light/scenario_Audio_mutation.json | 36 ++-- test/scenarios/light/scenario_Aurora_fps.json | 50 +++--- .../light/scenario_Driver_mutation.json | 24 +-- .../light/scenario_Effects_composition.json | 4 +- .../light/scenario_Fields_polar_lut.json | 58 +++---- .../light/scenario_Fluid_solver.json | 60 +++---- .../light/scenario_GridBlacks_blackpixel.json | 4 +- .../light/scenario_GridLayout_resize.json | 12 +- .../light/scenario_Layer_base_pipeline.json | 4 +- .../light/scenario_Layer_memory_1to1.json | 4 +- .../light/scenario_Layouts_mutation.json | 18 +- .../scenario_MoonLiveEffect_livescript.json | 52 +++--- .../light/scenario_MoonLive_pipeline.json | 24 +-- .../scenario_MultiplyModifier_memory_lut.json | 6 +- .../scenario_MultiplyModifier_pipeline.json | 4 +- .../light/scenario_Trails_ladder.json | 34 ++-- .../light/scenario_modifier_chain.json | 18 +- .../light/scenario_modifier_swap.json | 12 +- test/scenarios/light/scenario_perf_full.json | 66 ++++---- test/scenarios/light/scenario_perf_light.json | 12 +- .../light/scenario_peripheral_grid_sweep.json | 72 ++++---- .../light/scenario_peripheral_switch.json | 16 +- test/unit/light/unit_Drivers_livePalettes.cpp | 8 +- 50 files changed, 1011 insertions(+), 575 deletions(-) create mode 100644 docs/how-to/faq.md diff --git a/CLAUDE.md b/CLAUDE.md index 6171171f..2779b40c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -208,6 +208,8 @@ Each name is a script under `moondeck/`, run through `uv run`; the command and w **`test_host --ui` is never a gate.** The UI scenario runs drive a real browser against a running device and are what the documentation clips are recorded from, so they cost minutes and skip wholesale without a desktop and a Playwright browser. They run on request only, never as part of pre-commit, pre-merge or pre-release, and the `src/ui` trigger above means `--js` alone. +**`check_docgen` is a ratchet.** Errors are resolved before a commit, and warnings may only fall: the committed `docs/reference/metrics/docgen.md` is the number to beat, on the total and on every rule. It fails a run that raises either. Per rule as well as per total, because a total hides one rule paying for another, and because the cheapest way to satisfy a width rule is to split a line, which raises the block count and fixes nothing. A rule whose own limit changed is the one case to say so in the commit. + Three checks earn their place for a reason worth knowing. **Repo health** is the only place the creeping numbers are visible: flash and DRAM per target, binary size, the tick matrix, line counts, complexity warnings. Its diff belongs in the commit and its deltas in the commit message. It runs when the code changes rather than on every commit, because its timings drift with the host: on a docs-only diff it records a regression that nothing in the diff caused. **The no-backend build** catches a helper left unused outside its guard, fatal under GCC while clang stays silent. **ESP32 firmware fresh** compares the binary against every source in a tenth of a second and catches the edit that was never compiled; compile for real after an sdkconfig or toolchain change. The [provisioning path](moondeck/MoonDeck.md#improv_smoke_test) is the five files MoonDeck names. diff --git a/docs/contributing/documentation-standards.md b/docs/contributing/documentation-standards.md index 60becd2d..71eda51d 100644 --- a/docs/contributing/documentation-standards.md +++ b/docs/contributing/documentation-standards.md @@ -140,6 +140,7 @@ Two scales below a page. **A module** has exactly one reference page written and - **A page is read start to finish by one reader**, a **user** or a **developer**. A user brings no coding and no hardware knowledge beyond plugging in a board; a developer brings C++, embedded and this codebase's shape. Where a page serves both, lead with the user and put the depth lower down. - **The headings are the page's table of contents, and they read top to bottom.** A few lines say what the page is and one paragraph says how it is laid out. The sections then follow in the order a reader needs them. A title that makes sense only after reading the body is the order being wrong. - **One tone of voice, everywhere: factual, no nonsense.** State what is true and what to do, addressing the reader as "you". Leave out enthusiasm, apology, and how we felt building it. Only the assumed knowledge changes between pages, never the voice. +- **No sentence whose job is tone.** Every sentence carries a fact the reader needs. Three shapes to cut: a second person used for effect rather than instruction, a flourish before any information arrives, and a rhetorical question the page then answers. Vale checks spelling, sentence length and weasel words, so this one is the writer's judgement and the reviewer's check. - **Follow the [principles](../CLAUDE.md#principles).** Three bear on documentation directly: - **Minimalism**: every fact has one home; history lives in git. - **Present tense only.** "No X anymore" narrates a removal, which is history. Describe the path that exists today. @@ -258,7 +259,7 @@ The first five cut and the last adds, deliberately: the result is a short line o **Depth is homed rather than forbidden.** One line is room to say why. Past that the reasoning belongs in an `@moreinfo` appendix, with an `@xref` back from the line that raised the question. Both kinds of file have one. A header's sits on its class or file lead, and an implementation file's on its own file lead, which 212 headers already carry. A larger cap for implementation files was tried and removed, because it homed depth inline. The reasoning then sits beside one call rather than where a reader goes looking for it. -**A finding in a header is an error; one in an implementation file is a warning.** This follows the same line. A header's comments are the published page, so a defect there ships, while a `.cpp` publishes nothing and its comments are a note to the next reader. Both are counted and both are reported, because a warning nobody sees is a warning nobody fixes. The split stages the sweep rather than ranking the two kinds of comment, so it goes and everything blocks once the warning column reaches zero. That is how Vale's own config promotes a page to error as the sweep finishes it. +**A finding in a header is an error; one in an implementation file is a warning, with one rule in each direction.** No-hard-wrap blocks in both kinds of file: the tree is at zero findings, so there is nothing left to stage. The line-length cap warns in both, because it is new and its findings are lines nobody wrote wrongly. A staged rule joins the others as the tree meets it. A header's comments are the published page, so a defect there ships, while a `.cpp` publishes nothing and its comments are a note to the next reader. Both are counted and both are reported, because a warning nobody sees is a warning nobody fixes. The split stages the sweep rather than ranking the two kinds of comment, so it goes and everything blocks once the warning column reaches zero. That is how Vale's own config promotes a page to error as the sweep finishes it. Enforced by [`check_docgen.py`](../moondeck/check/check_docgen.py) over every header under `src/`, the vendored ones excepted. diff --git a/docs/how-to/faq.md b/docs/how-to/faq.md new file mode 100644 index 00000000..38d861ca --- /dev/null +++ b/docs/how-to/faq.md @@ -0,0 +1,52 @@ +# FAQ + +Questions people ask on a first run, in the order they meet them: installing, then first light, then the interface they are looking at, then effects, then the driver that reaches real hardware. + +A symptom rather than a question belongs in [Troubleshooting](troubleshooting.md), which starts from what you can see. + +## Installing + +- **Which firmware does my board take?** The web installer picks it from the board you choose. See [Install and first light](../gettingstarted.md). +- **Can I run it without a board?** Yes, on macOS, Linux and Windows. See [Installing to desktop](installing-to-desktop.md). +- **Which hardware do the heavy effects want?** The ones computing a field per light cost CPU, so a desktop or a small always-on machine runs them where a microcontroller cannot. See [Installing on Linux](installing-on-linux.md). +- **How do I update?** Over the air from the Firmware card, or by URL. See [Updating firmware](updating-firmware.md). + +## First light + +- **Where do I set the number of lights?** On a Layout, not on the driver. See [Layouts](../moonmodules/light/layouts.md). +- **Where do I add an effect?** Expand a Layer under Effects, then **add module**: a layer is what composites effects and holds the blend mode. See [Build your first light show](../tutorials/first-light-show.md). +- **Nothing lights up.** See [Troubleshooting](troubleshooting.md#the-lights-are-dark). + +## The interface + +- **What is the number on a module card?** That module's own tick time, shown from expert mode up. Hold it to peek at the rate, which inverts it: a module ticking in 22 ยตs reads as 45K fps. The device's frame rate is `fps` on the [System module](../moonmodules/core/system.md#system). +- **The card is busy, and I do not need half of it.** Set `mode` on the [System module](../moonmodules/core/system.md#system) to `user`, `expert` or `developer`: each control names the mode it needs. +- **The preview changed resolution.** It follows the layout, so a geometry change moves it. See [Preview](../moonmodules/light/drivers.md#preview). + +## Effects and scripting + +- **Can I write my own effect?** Yes, in MoonLive, on the device, live. See [Write your first script](../tutorials/first-script.md). +- **How do I save a look?** Presets. See [Presets](presets.md). +- **Can I drive it from a control surface or a DAW?** Over MIDI, OSC and DMX. See [Control surface](control-surface.md). + +## Drivers and panels + +- **Which driver do I need?** One per output kind: LEDs on pins, a panel card over Ethernet, HUB75 direct, or a network protocol. See [Drivers](../moonmodules/light/drivers.md). +- **Which board drives a panel card?** One with gigabit Ethernet, so an S31 or a desktop. An S3 or P4 is 100 Mbit and wants a gigabit switch between it and the card. See [Panel cards](panel-cards.md#the-one-hardware-fact-that-decides-everything). +- **My ColorLight card does almost nothing.** These cards need a 1000 Mbps link, and the driver reports the negotiated speed in its status line. See [Panel cards](panel-cards.md). +- **My HUB75 panel is one column out.** Set `clockEdge` to `falling`: some panel chips sample the shift clock on the other edge. See [Drivers](../moonmodules/light/drivers.md). +- **The HLS stream lags by seconds.** HLS buffers whole segments, so the delay is the format. For a live view use [Preview](../moonmodules/light/drivers.md#preview) instead of [HLS](../moonmodules/light/drivers.md#hls). + +## Network + +- **Ethernet and WiFi fight.** Pick one in the [Network module](../moonmodules/core/system.md#network)'s `mode`, because both leaves two default routes. + +## Developing + +- **What is MoonDeck, and do I need it?** A browser console that builds, flashes, runs, tests and checks the project, and drives the boards on your bench. Everything it offers is a script under `moondeck/`, so the CLI and the console run the same code and neither is required. See [MoonDeck](../explanation/architecture/moondeck.md). +- **Why not PlatformIO or pioarduino?** The ESP32 build is ESP-IDF-native, tracking IDF pre-releases against a pinned commit for chips like the P4 and S31, and the hot-path drivers use the vendor APIs directly. The tooling also covers far more than compile-upload-monitor. See [Building](building.md#moondeck-the-dev-console). +- **How do I start?** `uv run moondeck/moondeck.py`, then open . Build and run the desktop first: it needs no board and proves the toolchain. See [Building](building.md). + +## Still stuck + +Name the geometry, the driver, the peripheral and the measured refresh. Those four decide which path you are on. See [Log an issue](logging-an-issue.md). diff --git a/docs/moonmodules/core/system.md b/docs/moonmodules/core/system.md index 3dde5c47..c5eb0034 100644 --- a/docs/moonmodules/core/system.md +++ b/docs/moonmodules/core/system.md @@ -14,7 +14,7 @@ The device's identity and vitals โ€” name (behind mDNS `.local`, the SoftA - `deviceName` โ€” the identity behind mDNS, the SoftAP SSID and the DHCP hostname. - `deviceModel` โ€” the board model (drives the installer catalog entry). -- `expertMode` โ€” reveals advanced controls (marked ๐Ÿ”ง) across the UI; off by default. +- `mode` โ€” how much of the UI is shown: `user`, `expert` (๐ŸŽš๏ธ) or `developer` (๐Ÿ”ง), cumulative. - `logLevel` โ€” serial verbosity, defaulting to Warn. The first 60 s always logs at Info. - read-only vitals โ€” `uptime`, `fps`, `heap`, `psram`, `flash`, `chip`, and per-module footprint. diff --git a/docs/moonmodules/core/ui.md b/docs/moonmodules/core/ui.md index 9c070cb8..62c0fbf5 100644 --- a/docs/moonmodules/core/ui.md +++ b/docs/moonmodules/core/ui.md @@ -122,11 +122,12 @@ indentation. Nesting depth shows as progressively lighter backgrounds and a left - **Help link (?)** at the far right of the title row opens the module's doc page in a new tab. The path comes from `docPath` in `/api/types` (engine-provided, relative to `docs/moonmodules/`); omitted when the type declares none. -- **Stats line** โ€” `๐Ÿ•’ ` then `๐Ÿง  [ + ]`. Timing is fps or ยตs/ms per the - global toggle (ยตs under 1 ms, ms above), omitted when the module has no measured loop time. Memory is +- **Stats line** โ€” `๐Ÿ•’ ` then `๐Ÿง  [ + ]`. Timing is a tick time + (ยตs under 1 ms, ms above), omitted when the module has no measured loop time. Memory is the C++ object size (`classSize`); the `+ ` part (`dynamicBytes`, heap) shows only when the - module allocated heap. Clicking cycles the timing figure fps โ†” ms (persists in - `localStorage['mm_timing_mode']`, applies to all cards). + module allocated heap. The line shows from expert mode up, and holding it peeks at the rate on every + card, the password field's gesture: a tick time inverts to a figure that reads as a frame rate and is + not one, so the rate is a press away rather than a mode the cards sit in. - **Reorder is drag-and-drop** (HTML5 DnD) on the whole card, desktop and mobile. A drag starting on an interactive control is canceled in `dragstart` so the control's own gesture wins; a drag is accepted only when source and target share the same `.card-children` container (true siblings). @@ -265,7 +266,6 @@ The endpoints, the `/ws` frame shape, and the streaming state sink are owned by ```text mm_selectedRoot id of the currently-selected root module (string) mm_theme "dark" | "light" (default: "dark") -mm_timing_mode "fps" | "ms" (default: "fps") ``` No other client state persists; reorder, control values, etc. all live on the device. diff --git a/docs/reference/metrics/docgen.md b/docs/reference/metrics/docgen.md index 98d61fb4..4b638757 100644 --- a/docs/reference/metrics/docgen.md +++ b/docs/reference/metrics/docgen.md @@ -4,7 +4,7 @@ Generated by [`moondeck/check/check_docgen.py`](../../../moondeck/check/check_do Every place the generated documentation breaks the shape [the standards](../../contributing/documentation-standards.md#the-card) define. Current state only: the trend is this file's git history. The list only shrinks. -**0 error(s)** and **3141 warning(s)** across 217 page(s). +**0 error(s)** and **3135 warning(s)** across 217 page(s). An error is in a file that generates a documentation page, a header or a catalog page, so the finding is a defect in what gets published and it fails the gate. A warning is in an implementation file, which publishes nothing: its comments are a note to the next reader, worth fixing without being worth stopping a commit for. Both are counted here, because a warning nobody sees is a warning nobody fixes. @@ -15,11 +15,10 @@ The split is temporary. It stages the sweep rather than ranking the two kinds of | Rule | Errors | Warnings | |---|---:|---:| | over-wide comment lines | 0 | 2229 | -| multi-line comment blocks | 0 | 606 | +| multi-line comment blocks | 0 | 604 | | over-long sentences | 0 | 108 | | over-wide doc lines | 0 | 102 | | member deep dives | 0 | 92 | -| hard wraps | 0 | 4 | ## By documentation area @@ -27,7 +26,7 @@ The unit a sweep runs in: one summary page and the headers it owns, as the [hier | Summary page | Errors | Warnings | |---|---:|---:| -| `(tests, no card)` | 0 | 2213 | +| `(tests, no card)` | 0 | 2207 | | `platform/index.md` | 0 | 518 | | `core/system.md` | 0 | 410 | @@ -37,7 +36,7 @@ Per area, since that is the unit a sweep runs in: the files ranked, then the rul ### (tests, no card) -**0 error(s)** and **2213 warning(s)** across 184 file(s). +**0 error(s)** and **2207 warning(s)** across 184 file(s). | Findings | File | | |---:|---|---| @@ -51,9 +50,9 @@ Per area, since that is the unit a sweep runs in: the files ranked, then the rul | 44 | `test/unit/core/unit_moonlive_codegen_xtensa.cpp` | warning | | 42 | `test/unit/light/unit_ParallelSlots.cpp` | warning | | 41 | `test/scenario_runner.cpp` | warning | -| 1-41 each | *174 more warning files, 1547 findings* | | +| 1-41 each | *174 more warning files, 1541 findings* | | -By rule, warnings: 1672 over-wide comment lines, 241 multi-line comment blocks, 106 over-long sentences, 101 over-wide doc lines, 89 member deep dives, 4 hard wraps. +By rule, warnings: 1672 over-wide comment lines, 239 multi-line comment blocks, 106 over-long sentences, 101 over-wide doc lines, 89 member deep dives. ### platform/index.md diff --git a/docs/reference/metrics/repo-health.json b/docs/reference/metrics/repo-health.json index 6df0cd07..b47355f0 100644 --- a/docs/reference/metrics/repo-health.json +++ b/docs/reference/metrics/repo-health.json @@ -1,8 +1,8 @@ { - "commit": "46178a32", + "commit": "7bfbfa78", "flash": { - "esp32s3-n16r8": 2131536, - "desktop": 1953560, + "esp32s3-n16r8": 2134032, + "desktop": 1970568, "esp32": 2080464, "esp32p4rev1-eth": 2029776, "esp32p4rev1-eth-wifi": 2284640, @@ -21,8 +21,8 @@ "esp32s31": "2026-09-06", "esp32": "2026-09-20", "esp32-pico": "2026-09-09", - "esp32s3-n16r8": "2026-09-18", - "desktop": "2026-09-20", + "esp32s3-n16r8": "2026-09-21", + "desktop": "2026-09-21", "esp32s3-n8r8": "2026-09-08", "esp32s3-zero": "2026-09-08", "esp32-16mb": "2026-09-09", @@ -31,20 +31,20 @@ }, "perf": { "desktop": { - "tick_us": 120, - "fps": 8333, + "tick_us": 135, + "fps": 7407, "scenario_p50": { "Layer_base_pipeline": { "p50": 70, "p95": 74, "n": 32, - "last": "2026-09-20" + "last": "2026-09-21" }, "Layer_memory_1to1": { "p50": 5, "p95": 24, "n": 32, - "last": "2026-09-20" + "last": "2026-09-21" } } }, @@ -58,7 +58,7 @@ "p50": 127, "p95": 139, "n": 32, - "last": "2026-09-20" + "last": "2026-09-21" }, "esp32-eth-wifi": { "p50": 89895, @@ -172,9 +172,9 @@ "Audio_mutation": { "desktop-macos": { "p50": 22, - "p95": 47, + "p95": 38, "n": 32, - "last": "2026-09-20" + "last": "2026-09-21" }, "desktop-windows": { "p50": 40, @@ -197,18 +197,18 @@ }, "Aurora_fps": { "desktop-macos": { - "p50": 1526, + "p50": 1521, "p95": 1947, "n": 32, - "last": "2026-09-20" + "last": "2026-09-21" } }, "Driver_mutation": { "desktop-macos": { "p50": 20, - "p95": 23, + "p95": 30, "n": 32, - "last": "2026-09-20" + "last": "2026-09-21" }, "desktop-windows": { "p50": 42, @@ -234,7 +234,7 @@ "p50": 144, "p95": 160, "n": 32, - "last": "2026-09-20" + "last": "2026-09-21" }, "desktop-windows": { "p50": 549, @@ -245,10 +245,10 @@ }, "Fields_polar_lut": { "desktop-macos": { - "p50": 1275, - "p95": 2014, + "p50": 1263, + "p95": 1624, "n": 32, - "last": "2026-09-20" + "last": "2026-09-21" } }, "Fluid_solver": { @@ -256,7 +256,7 @@ "p50": 217, "p95": 250, "n": 32, - "last": "2026-09-20" + "last": "2026-09-21" } }, "GridBlacks_blackpixel": { @@ -264,7 +264,7 @@ "p50": 2, "p95": 2, "n": 32, - "last": "2026-09-20" + "last": "2026-09-21" }, "esp32s3-n16r8": { "p50": 267, @@ -290,7 +290,7 @@ "p50": 120, "p95": 142, "n": 32, - "last": "2026-09-20" + "last": "2026-09-21" }, "esp32-eth-wifi": { "p50": 82231, @@ -334,7 +334,7 @@ "p50": 70, "p95": 74, "n": 32, - "last": "2026-09-20" + "last": "2026-09-21" }, "desktop-windows": { "p50": 118, @@ -348,7 +348,7 @@ "p50": 5, "p95": 24, "n": 32, - "last": "2026-09-20" + "last": "2026-09-21" }, "desktop-windows": { "p50": 1, @@ -362,7 +362,7 @@ "p50": 93, "p95": 100, "n": 32, - "last": "2026-09-20" + "last": "2026-09-21" }, "desktop-windows": { "p50": 111, @@ -414,7 +414,7 @@ "p50": 5, "p95": 6, "n": 32, - "last": "2026-09-20" + "last": "2026-09-21" }, "esp32s3-n16r8": { "p50": 8255, @@ -464,7 +464,7 @@ "p50": 5, "p95": 5, "n": 32, - "last": "2026-09-20" + "last": "2026-09-21" }, "desktop-windows": { "p50": 1, @@ -476,9 +476,9 @@ "MultiplyModifier_memory_lut": { "desktop-macos": { "p50": 3, - "p95": 4, + "p95": 3, "n": 32, - "last": "2026-09-20" + "last": "2026-09-21" }, "desktop-windows": { "p50": 3, @@ -492,7 +492,7 @@ "p50": 119, "p95": 142, "n": 32, - "last": "2026-09-20" + "last": "2026-09-21" }, "desktop-windows": { "p50": 225, @@ -503,18 +503,18 @@ }, "Trails_ladder": { "desktop-macos": { - "p50": 359, + "p50": 358, "p95": 658, "n": 32, - "last": "2026-09-20" + "last": "2026-09-21" } }, "modifier_chain": { "desktop-macos": { - "p50": 44, + "p50": 43, "p95": 46, "n": 32, - "last": "2026-09-20" + "last": "2026-09-21" }, "desktop-windows": { "p50": 69, @@ -534,7 +534,7 @@ "p50": 23, "p95": 25, "n": 32, - "last": "2026-09-20" + "last": "2026-09-21" }, "esp32-eth": { "p50": 1010, @@ -572,7 +572,7 @@ "p50": 251, "p95": 295, "n": 32, - "last": "2026-09-20" + "last": "2026-09-21" }, "esp32s3-n16r8": { "p50": 16915, @@ -604,7 +604,7 @@ "p50": 15, "p95": 21, "n": 32, - "last": "2026-09-20" + "last": "2026-09-21" }, "esp32s3-n16r8": { "p50": 2485, @@ -646,9 +646,9 @@ }, "desktop-macos": { "p50": 254, - "p95": 309, + "p95": 324, "n": 32, - "last": "2026-09-20" + "last": "2026-09-21" }, "desktop-windows": { "p50": 649, @@ -674,7 +674,7 @@ "p50": 4, "p95": 4, "n": 32, - "last": "2026-09-20" + "last": "2026-09-21" }, "esp32p4rev1-eth": { "p50": 217, @@ -698,54 +698,54 @@ } }, "loc": { - "core": 21849, - "light": 29887, - "platform": 16325, - "ui": 11196, - "test": 56157, - "moondeck": 26920 + "core": 21994, + "light": 29903, + "platform": 16346, + "ui": 11223, + "test": 56265, + "moondeck": 27113 }, "comments": { "core": { - "lines": 5548, + "lines": 5575, "ratio": 0.28 }, "light": { - "lines": 7250, - "ratio": 0.272 + "lines": 7262, + "ratio": 0.273 }, "platform": { - "lines": 3470, + "lines": 3481, "ratio": 0.238 }, "ui": { - "lines": 3333, - "ratio": 0.314 + "lines": 3350, + "ratio": 0.315 }, "test": { - "lines": 6339, + "lines": 6345, "ratio": 0.131 }, "moondeck": { - "lines": 4495, - "ratio": 0.19 + "lines": 4531, + "ratio": 0.191 } }, "tests": { - "cases": 2065, + "cases": 2067, "scenarios": 27 }, "docs": { - "md_files": 137, - "md_lines": 28893, + "md_files": 138, + "md_lines": 28986, "plans_files": 31, - "backlog_lines": 2986, + "backlog_lines": 3025, "lessons_lines": 518, - "claude_md_lines": 277 + "claude_md_lines": 279 }, "complexity": { - "functions": 3621, - "over_threshold": 264, + "functions": 3627, + "over_threshold": 265, "worst_ccn": 128 } } diff --git a/docs/reference/metrics/repo-health.md b/docs/reference/metrics/repo-health.md index 54d6d801..851eb4f3 100644 --- a/docs/reference/metrics/repo-health.md +++ b/docs/reference/metrics/repo-health.md @@ -1,6 +1,6 @@ # Repo health -Measured at `46178a32`. Generated by [`moondeck/check/repo_health.py`](../../../moondeck/check/repo_health.py) on every KPI-gate run. **Do not edit by hand.** +Measured at `7bfbfa78`. Generated by [`moondeck/check/repo_health.py`](../../../moondeck/check/repo_health.py) on every KPI-gate run. **Do not edit by hand.** Current state only; the trend is this file's git history (`git log -p docs/reference/metrics/repo-health.md`). Nothing here fails a build: the numbers make growth visible, the judgment stays human. @@ -8,19 +8,19 @@ Current state only; the trend is this file's git history (`git log -p docs/refer | Target | Flash | Capacity | Used | Built | |---|---:|---:|---:|:--:| -| desktop | 1,908 KB | - | - | yes | -| esp32 | 2,032 KB | 2,496 KB | 81% | yes | -| esp32-16mb | 2,012 KB | 4,096 KB | 49% | **STALE 11d** | -| esp32-eth | 1,642 KB | 2,496 KB | 66% | **STALE 9d** | -| esp32-pico | 2,058 KB | 3,072 KB | 67% | **STALE 11d** | +| desktop | 1,924 KB (+17 KB) โš  | - | - | yes | +| esp32 | 2,032 KB | 2,496 KB | 81% | carried 1d | +| esp32-16mb | 2,012 KB | 4,096 KB | 49% | **STALE 12d** | +| esp32-eth | 1,642 KB | 2,496 KB | 66% | **STALE 10d** | +| esp32-pico | 2,058 KB | 3,072 KB | 67% | **STALE 12d** | | esp32-wrover | 1,801 KB | - | - | carried (age?) | -| esp32p4rev1-eth | 1,982 KB | 4,096 KB | 48% | carried 2d | -| esp32p4rev1-eth-wifi | 2,231 KB | 4,096 KB | 54% | **STALE 12d** | +| esp32p4rev1-eth | 1,982 KB | 4,096 KB | 48% | carried 3d | +| esp32p4rev1-eth-wifi | 2,231 KB | 4,096 KB | 54% | **STALE 13d** | | esp32p4rev3-eth | 1,605 KB | - | - | carried (age?) | -| esp32s3-n16r8 | 2,082 KB | 4,096 KB | 51% | carried 2d | -| esp32s3-n8r8 | 2,038 KB | 3,072 KB | 66% | **STALE 12d** | -| esp32s3-zero | 1,977 KB | 2,496 KB | 79% | **STALE 12d** | -| esp32s31 | 2,294 KB | 4,096 KB | 56% | **STALE 14d** | +| esp32s3-n16r8 | 2,084 KB (+2 KB) โš  | 4,096 KB | 51% | yes | +| esp32s3-n8r8 | 2,038 KB | 3,072 KB | 66% | **STALE 13d** | +| esp32s3-zero | 1,977 KB | 2,496 KB | 79% | **STALE 13d** | +| esp32s31 | 2,294 KB | 4,096 KB | 56% | **STALE 15d** | | qemu | 1,351 KB | - | - | carried (age?) | `Built: yes` was measured this run. `carried (age?)` was not rebuilt either and predates this record, so its age is unknown: it dates itself on the next build. `carried Nd` was NOT rebuilt and its number is N days old, so an absent delta says nothing about the change. **STALE** marks a carry older than 7 days: the number has gone unchecked long enough that growth will surface later as one jump, blamed on whichever commit happens to rebuild that target. `Used` is against the app slot in the firmware's own partition table. @@ -29,7 +29,7 @@ Current state only; the trend is this file's git history (`git log -p docs/refer | Target | Tick | FPS | |---|---:|---:| -| desktop | 120 ยตs (โˆ’8 ยตs) โœ“ | 8,333 (+521) โœ“ | +| desktop | 135 ยตs (+15 ยตs) โš  | 7,407 (โˆ’926) โš  | | esp32 | 8,354 ยตs | 119 | ### Scenario tick by target (p50 of each sample window) @@ -37,30 +37,30 @@ Current state only; the trend is this file's git history (`git log -p docs/refer | Scenario | desktop-macos | desktop-windows | esp32 | esp32s3-n16r8 | esp32p4rev1-eth | esp32s31 | esp32-eth | esp32-eth-wifi | unknown | |---|---|---|---|---|---|---|---|---|---| | Audio_mutation | 22 | 40 ? | 13,152 | 47 ? | - | - | - | - | - | -| Aurora_fps | 1,526 (โˆ’9) โœ“ | - | - | - | - | - | - | - | - | +| Aurora_fps | 1,521 (โˆ’5) โœ“ | - | - | - | - | - | - | - | - | | Driver_mutation | 20 | 42 ? | 12,812 | 39 ? | - | - | - | - | - | -| Effects_composition | 144 (โˆ’1) โœ“ | 549 ? | - | - | - | - | - | - | - | -| Fields_polar_lut | 1,275 (โˆ’7) โœ“ | - | - | - | - | - | - | - | - | -| Fluid_solver | 217 (โˆ’1) โœ“ | - | - | - | - | - | - | - | - | +| Effects_composition | 144 | 549 ? | - | - | - | - | - | - | - | +| Fields_polar_lut | 1,263 (โˆ’12) โœ“ | - | - | - | - | - | - | - | - | +| Fluid_solver | 217 | - | - | - | - | - | - | - | - | | GridBlacks_blackpixel | 2 | 8 ? | 269 ? | 267 ? | - | - | - | - | - | -| GridLayout_resize | 120 (โˆ’1) โœ“ | 219 ? | 1,352 ? | 1,011 ? | 1,143 ? | - | 95,771 ? | 82,231 ? | - | +| GridLayout_resize | 120 | 219 ? | 1,352 ? | 1,011 ? | 1,143 ? | - | 95,771 ? | 82,231 ? | - | | Layer_base_pipeline | 70 | 118 ? | - | - | - | - | - | - | - | | Layer_memory_1to1 | 5 | 1 ? | - | - | - | - | - | - | - | | Layouts_mutation | 93 | 111 ? | 13,692 | 45 ? | - | - | 27 ? | - | - | | MoonLiveEffect_controls | 11 ? | - | 12,901 | 4,624 ? | - | - | - | - | - | | MoonLiveEffect_livescript | 5 | - | 13,433 ? | 8,255 ? | 11,336 ? | - | - | - | - | | MoonLive_pipeline | 5 | 1 ? | 9,604 ? | 3,278 ? | - | 11,398 ? | - | - | 4,393 ? | -| MoonModule_control_change | 127 (โˆ’6) โœ“ | 262 ? | 212 ? | 166 ? | 165 ? | - | 111,731 ? | 89,895 ? | - | +| MoonModule_control_change | 127 | 262 ? | 212 ? | 166 ? | 165 ? | - | 111,731 ? | 89,895 ? | - | | MqttModule_haDiscovery_toggle | 3 ? | - | 36 ? | 36 ? | - | - | - | - | - | | MultiplyModifier_memory_lut | 3 | 3 ? | - | - | - | - | - | - | - | | MultiplyModifier_pipeline | 119 | 225 ? | - | - | - | - | - | - | - | | NetworkModule_eth_reconfigure | - | - | 1,169 ? | 97,843 ? | - | - | - | - | - | | NetworkModule_mdns_toggle | 13 ? | - | 36 ? | 36 ? | 21 ? | - | 109,767 ? | 93,963 ? | - | -| Trails_ladder | 359 | - | - | - | - | - | - | - | - | -| modifier_chain | 44 | 69 ? | 13,337 | - | - | - | - | - | - | +| Trails_ladder | 358 (โˆ’1) โœ“ | - | - | - | - | - | - | - | - | +| modifier_chain | 43 (โˆ’1) โœ“ | 69 ? | 13,337 | - | - | - | - | - | - | | modifier_swap | 23 | 41 ? | 12,250 | 354 ? | 362 ? | - | 1,010 ? | - | - | | perf_full | 251 | 592 ? | 10,392 | 16,915 ? | 17,433 ? | - | - | - | - | -| perf_light | 15 (โˆ’1) โœ“ | 35 ? | 2,183 | 2,485 ? | 2,038 ? | - | - | - | - | +| perf_light | 15 | 35 ? | 2,183 | 2,485 ? | 2,038 ? | - | - | - | - | | peripheral_grid_sweep | 254 | 649 ? | 6,991 ? | - | 11,495 ? | 12,273 ? | - | - | - | | peripheral_switch | 4 | 9 ? | 437 | 46 ? | 217 ? | - | - | - | - | @@ -81,36 +81,36 @@ These build a bare pipeline with no optional modules, so a change here is a chan | Area | Lines | Comments | Comment share | |---|---:|---:|---:| -| core | 21,849 (โˆ’648) โœ“ | 5,548 | 28.0 % (โˆ’2.3 %) โœ“ | -| light | 29,887 | 7,250 | 27.2 % | -| platform | 16,325 (โˆ’984) โœ“ | 3,470 | 23.8 % (โˆ’4.8 %) โœ“ | -| ui | 11,196 | 3,333 | 31.4 % | -| test | 56,157 (โˆ’4,849) โœ“ | 6,339 | 13.1 % (โˆ’8.1 %) โœ“ | -| moondeck | 26,920 (+106) โš  | 4,495 | 19.0 % (+0.1 %) โš  | +| core | 21,994 (+145) โš  | 5,575 | 28.0 % | +| light | 29,903 (+16) โš  | 7,262 | 27.3 % (+0.1 %) โš  | +| platform | 16,346 (+21) โš  | 3,481 | 23.8 % | +| ui | 11,223 (+27) โš  | 3,350 | 31.5 % (+0.1 %) โš  | +| test | 56,265 (+108) โš  | 6,345 | 13.1 % | +| moondeck | 27,113 (+193) โš  | 4,531 | 19.1 % (+0.1 %) โš  | ## Tests | Kind | Count | |---|---:| -| unit cases | 2,065 | +| unit cases | 2,067 (+2) โœ“ | | scenarios | 27 | ## Complexity | Metric | Value | |---|---:| -| functions | 3,621 | -| over threshold | 264 | +| functions | 3,627 (+6) โœ“ | +| over threshold | 265 (+1) โš  | | worst CCN | 128 | ## Documentation | Metric | Value | |---|---:| -| markdown files | 137 | -| markdown lines | 28,893 (+31) โš  | +| markdown files | 138 (+1) โš  | +| markdown lines | 28,986 (+93) โš  | | plan files | 31 | -| backlog lines | 2,986 | +| backlog lines | 3,025 (+39) โš  | | lessons lines | 518 | -| CLAUDE.md lines | 277 | +| CLAUDE.md lines | 279 (+2) โš  | diff --git a/docs/work/future/backlog-core.md b/docs/work/future/backlog-core.md index f21a10ce..7fc0f6b8 100644 --- a/docs/work/future/backlog-core.md +++ b/docs/work/future/backlog-core.md @@ -832,15 +832,46 @@ Run a bare-leaving scenario before a tree-assuming one and the latter fails pre- Fix options: (a) make every live mutate scenario clear+rebuild its own canvas (consistent with the newer ones) so order never matters; or (b) have the live runner reboot / restore the canonical tree between scenarios. (a) is the cleaner long-term shape. Until then, the in-process suite is the gate; live full-suite runs need a clean boot per scenario, or run scenarios individually. +## Ethernet on the QuinLED Dig-Next-2 (W5500 over SPI) + +The DN2 is in the catalog with no Ethernet block, so it comes up WiFi-only. Adding it is a `deviceModels.json` entry and nothing else: the preset work landed the seam, and W5500 already applies live. + +**RMII is impossible on this board, so the W5500 is the only route.** The DN2 carries an ESP32-PICO-V3-02, whose package leaves GPIO 16, 17, 18 and 23 unbonded, and those are the RMII management and clock pins. This is the same package fact behind the flash-cache wedge recorded in [lessons](../past/lessons.md). QuinLED's own LAN8720 board stacks onto the QuinLED-ESP32 only, not onto a Dig board. + +**The wiring, from [Quindor's firmware source](https://github.com/intermittech/WLED-Dig-Next-2_W5500)**, using the QEXT header for the bus and the Stemma QT header for the two control lines. Fitting Ethernet therefore costs the I2C port: + +| W5500 | connector | GPIO | +|---|---|---| +| SCLK | QEXT green | 25 | +| MOSI | QEXT blue | 32 | +| MISO | QEXT yellow | 0 | +| CS | QEXT white | 33 | +| INT | Stemma QT SDA | 15 | +| RST | Stemma QT SCL | 14 | + +MISO on GPIO 0 is deliberate and sound: GPIO 0 is a strapping pin, and a W5500 leaves MISO high-Z while CS is inactive, which is what Espressif's rule about not driving strapping pins at reset asks for. + +**It stays `Custom`, and that is the finding.** Across the three W5500 boards now known, LightCrafter 16, SE 16 V1 and this one, no two share a single pin on any of the six lines. SPI Ethernet is a module wired to whatever a board left free, so there is no W5500 pattern to make a preset from, and inventing one would be a preset that fits nothing. + +**What is unverified.** The pin table is trustworthy as what that firmware does, because it was read out of the code rather than a description. What rests on one source is that it works: a single-author repository created 2026-09-19, self-labelled experimental, tested on one unit, with LED output, buttons, relays and mic explicitly not exercised. No independent build was reported when this was written. So the open question is not the pins but whether SPI Ethernet and the LED output coexist on this board under load, which is a bench answer. + ## Housekeeping -### Promote the one-line `//` cap to an error, once the 606 `.cpp` runs are homed (2026-09-21) +### Promote the one-line `//` cap to an error, once the 604 `.cpp` blocks are homed (2026-09-21) + +`MAX_CODE_COMMENT = 1` is already the rule in both kinds of file, and already an ERROR in a header. It stays a warning in a `.cpp` only because `_blocks()` stages the sweep by file kind, and that function names its own removal: a rule leaves the staging as the tree meets it, which no-hard-wrap already did. + +**The shape of what is left.** 604 blocks across 92 files, 2081 comment lines. By area: 239 in `test/`, 210 in `src/`, 155 under `src/platform/`. By length: 81 are two lines, 321 are three, 124 are four, so 526 of 604 are three lines or fewer and the tail is one 15-line block, two of ten and a handful between. + +It concentrates, which is what makes it approachable. `HttpServerModule.cpp` alone holds 100 and the top ten files hold 312, over half; the other 82 files average under four each. + +**Where the depth goes is the blocker.** Of the 92 files only 19 already carry an `@moreinfo` appendix. 68 have a `///` lead with no appendix and 5 have neither, so most files need the appendix written before a single block can move. -`MAX_CODE_COMMENT = 1` is already the rule in both kinds of file, and already an ERROR in a header. It stays a warning in a `.cpp` only because `_blocks()` stages the sweep by file kind, and that function names its own removal: when the implementation side reaches zero it goes and every finding blocks. +**The fix is not to split them.** Chopping one comment into two shorter lines leaves the text identical and satisfies nothing, which is why the cap counts a block rather than a line. Each one is depth that belongs in the file lead's appendix with an `@xref` back, or prose that says less. -What stands between here and there is 606 multi-line blocks, all `.cpp`: 241 in `test/`, 210 in `src/`, 155 under `src/platform/`. By length, 82 are two lines, 321 are three and 125 are four, so 528 of 606 are three lines or fewer; the tail is one 15-line run, two of ten and a handful between. +**Suggested order.** `HttpServerModule.cpp` first: 100 blocks, 444 comment lines, and it already has the appendix. Then the other nine heavy files for 212 more. Then `test/` as its own sweep, because the argument that a test comment explains why a test EXISTS is strongest there and may deserve a different budget rather than an appendix. Promote once at zero. -**The fix is not to split them.** Chopping one long comment into two shorter lines leaves the text identical and satisfies nothing, which is why the run cap exists at all. Each one is depth that belongs in the file lead's `@moreinfo` appendix with an `@xref` back, or in prose that says less. That is a judgement per comment across roughly 150 files, which is a sweep of its own rather than a mechanical pass. +**Cost.** Not a scripted pass. Each block is a judgement between depth that belongs in an appendix, an explanation that should be shorter, and history git already holds. About 35 done by hand in one session is the measured rate, so the first file is a session of its own and the whole sweep is several days. ### Hot path: move blocking work off the render callbacks (architecture) diff --git a/mkdocs.yml b/mkdocs.yml index 91603f63..0e6277f7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -144,6 +144,9 @@ nav: # Two pages, two audiences: the device card deep-links this one from its consent prompt # (app.js, mooncloud.html#why-you-might-like-this), so its URL is a shipped contract. - MoonCloud: explanation/mooncloud.md + # Answers to what people hit on a first run, each from someone's bench. Under Getting + # started rather than How-to, because the reader asking is on their first hour. + - FAQ: how-to/faq.md - Log an issue: how-to/logging-an-issue.md # The FOLDER under docs/ is the Diataxis type: tutorials/, how-to/, explanation/, # reference/. The nav labels stay reader-facing, so the path tells a writer where a page diff --git a/moondeck/MoonDeck.md b/moondeck/MoonDeck.md index 6b441f4d..d3f78c90 100644 --- a/moondeck/MoonDeck.md +++ b/moondeck/MoonDeck.md @@ -184,6 +184,10 @@ And per file, over every `.h` and `.cpp` under `src/`, `test/`, `esp32/main`, `m Every run writes [docs/reference/metrics/docgen.md](../docs/reference/metrics/docgen.md), the tracked state of the sweep: errors and warnings per rule and per page, then the files ranked within each area. Current state only, so its git history is the trend, the same shape repo-health.md uses. The file is the artifact to read, because stdout scrolls away and truncates; `--noreport` suppresses the write for a caller that only wants the exit code. +**A ratchet, not a snapshot.** The committed `docs/reference/metrics/docgen.md` is the baseline: the run rewrites the file, then compares what it found against the copy in `HEAD`. An error fails the run, and so does any rule whose warning count ROSE, the total included. Warnings are staged work, and staged work that grows is not a sweep. + +The comparison is per rule as well as on the total, because each hides a different move. A total alone hides one rule paying for another: splitting an over-wide line lowers the width count and raises the block count, which is the trade the block cap exists to refuse. Per rule alone misses a rule sitting under its own baseline while the total climbs. A rule whose limit itself changed is the one case where a rise is right, and the commit message is where that is said. + **There is no tolerated list.** The limits are the limits, and the check is red until the tree meets them. A grandfather list was tried and removed: while one exists, the cheapest way to make the check green is to add to it, which is how the comment budget eroded in the first place. The rules are in [documentation-standards.md](../docs/contributing/documentation-standards.md#the-card), and the check itself is pinned by `test/python/test_check_docgen.py`: every rule is tested firing on a page built to break it, because a regex that silently stopped matching would report a clean run. diff --git a/moondeck/check/check_devices.py b/moondeck/check/check_devices.py index 385ef03f..0a56d2a5 100644 --- a/moondeck/check/check_devices.py +++ b/moondeck/check/check_devices.py @@ -29,6 +29,8 @@ ROOT = Path(__file__).resolve().parent.parent.parent CATALOG = ROOT / "mooninstaller" / "deviceModels.json" MAIN_CPP = ROOT / "src" / "main.cpp" +NETWORK_MODULE = ROOT / "src" / "core" / "system" / "NetworkModule.h" +PLATFORM_CONFIG = ROOT / "src" / "platform" / "esp32" / "platform_config.h" DOCS = ROOT / "docs" # The device stores the injected deviceModel in SystemModule's deviceModel_[32] buffer (31 usable @@ -56,6 +58,60 @@ BOOT_WIRED_TYPES = {"System", "Network", "Drivers"} +def eth_preset_labels(): + """The Ethernet preset labels from NetworkModule.h's kEthPresets table. + + Read from the firmware rather than restated here, so a preset renamed in one place and not the + other fails this check instead of silently leaving a catalog entry pointing at nothing. + """ + text = NETWORK_MODULE.read_text(encoding="utf-8") + table = re.search(r"kEthPresets\[\]\s*=\s*\{(.*?)\n \};", text, re.S) + if not table: + return set() + return set(re.findall(r'^\s*\{"([^"]+)"', table.group(1), re.M)) + + +def eth_preset_drift(): + """Preset rows that no longer match the per-chip default they restate, as (label, field, was, now). + + Three presets ARE a chip's `ethConfigDefault`, so the same pin map has two homes in two layers. + They cannot simply reference each other: the header's value is a compile-time `constexpr` chosen + per chip, while the table must carry every chip's row on every build. So the duplication stands + and this check binds it, because the drift is silent where it matters most: `seedEthPresetFromPins` + matches on exact equality, so one corrected pin would reseed every provisioned board to Custom. + """ + table = NETWORK_MODULE.read_text(encoding="utf-8") + header = PLATFORM_CONFIG.read_text(encoding="utf-8") + block = re.search(r"kEthPresets\[\]\s*=\s*\{(.*?)\n \};", table, re.S) + if not block: + return [("kEthPresets", "table", "present", "unreadable")] + + rows = {} + for m in re.finditer(r'\{"([^"]+)",\s*([^}]+)\}', block.group(1)): + rows[m.group(1)] = [f.strip() for f in m.group(2).split(",")] + + # label -> the PHY constant naming that chip's arm of the ethConfigDefault ternary. + PAIRS = {"Classic RMII": "ethLan8720", "P4-NANO": "ethIp101", "S31 CoreBoard": "ethYt8531"} + FIELDS = ["phyAddr", "mdc", "mdio", "rst", "rmiiClk"] + drift = [] + for label, phy in PAIRS.items(): + row = rows.get(label) + if not row: + drift.append((label, "row", "present", "missing")) + continue + m = re.search(phy + r", /\*addr\*/ (-?\d+), /\*mdc\*/ (-?\d+), /\*mdio\*/ (-?\d+)," + r"\s*/\*rst\*/ (-?\d+), /\*rmiiClk\*/ (-?\d+)", header) + if not m: + drift.append((label, phy, "an ethConfigDefault arm", "not found")) + continue + # row is label-less here: [type, phyAddr, mdc, mdio, rst, rmiiClk, ...] + for i, field in enumerate(FIELDS): + want, got = m.group(i + 1), row[i + 1] + if want != got: + drift.append((label, field, want, got)) + return drift + + def registered_types(): """The set of factory type names from main.cpp's registerType("Name") calls.""" text = MAIN_CPP.read_text(encoding="utf-8") @@ -64,6 +120,13 @@ def registered_types(): def main(): errors = [] + eth_presets = eth_preset_labels() + for label, field, was, now in eth_preset_drift(): + errors.append(f"NetworkModule.h kEthPresets {label!r}: {field} is {now}, but " + f"platform_config.h's ethConfigDefault says {was} โ€” the preset restates the " + f"chip default, so the two must agree or a provisioned board reseeds to Custom") + if not eth_presets: + errors.append("NetworkModule.h: could not read the kEthPresets table โ€” the ethBoard check cannot run") try: catalog = json.loads(CATALOG.read_text(encoding="utf-8")) @@ -247,6 +310,12 @@ def _gpio(v): # are genuine BOARD WIRING are required โ€” MDC/MDIO may stay at the IDF default (omit or # -1) on RMII, since that's a real standard, not a board-specific value. if mtype == "NetworkModule" and isinstance(controls, dict): + board = controls.get("ethBoard") + if board is not None: + if not isinstance(board, str): + errors.append(f"{where}: NetworkModule ethBoard must be the preset LABEL as a string, got {board!r}") + elif board not in eth_presets: + errors.append(f"{where}: NetworkModule ethBoard {board!r} is not a preset in NetworkModule.h (known: {sorted(eth_presets)})") et = controls.get("ethType") # ethType must be an int (a JSON string like "2" would silently skip the rule below and # also isn't what the device deserializes into the Select) โ€” reject a stringified value. @@ -254,7 +323,9 @@ def _gpio(v): # would pass as ethType 1 (LAN8720). if et is not None and type(et) is not int: errors.append(f"{where}: NetworkModule ethType must be an integer, got {et!r}") - if type(et) is int and et != 0: + # A named preset IS the explicit pinning: the map lives in NetworkModule.h, under + # the same review as any other firmware constant, rather than repeated per board. + if type(et) is int and et != 0 and board in (None, "Custom"): # RMII LAN8720(1)/IP101(2): rst + clock. RGMII YT8531(4): mdc/mdio/rst. # W5500 SPI(3): the four SPI bus pins. -1 is an allowed explicit value ("unused / # IDF default"); what the rule forbids is OMITTING a board-wiring pin. diff --git a/moondeck/check/check_docgen.py b/moondeck/check/check_docgen.py index ee236813..4486b95b 100755 --- a/moondeck/check/check_docgen.py +++ b/moondeck/check/check_docgen.py @@ -147,6 +147,10 @@ # (`lead N chars > M`), and the prefix alone also matched the word budget (`comment line N words`), # each a differently motivated rule silently demoted. _LINE_LENGTH_RULES = ("doc line ", "comment line ") +# The one rule staged the OTHER way: an implementation file's hard wrap blocks too. The tree is at +# zero, so nothing is held hostage, and a split sentence reflows every line it spans on the next +# word change, which costs a reviewer the same in a `.cpp` as in a header. +_HARD_WRAP_RULE = "hard wrap" # One line, in every file. Depth is not forbidden, it is HOMED: a header carries it in an # `@moreinfo` appendix (212 do) and an implementation file carries it in the same appendix on its # own file lead. A cap of 8 in a `.cpp` was an attempt to give depth a home inline, which leaves @@ -182,12 +186,16 @@ def _blocks(key: str, why: str = "") -> bool: goes and every finding blocks, the same way `.vale.ini` promotes a page to error once the sweep has finished it and loses its per-page section when the last page lands. - ONE rule is staged separately: the line-length cap warns in a header too. Every other rule - describes how this tree is already written, so a finding is a defect against a standard the - file was written under. The cap is new, so its findings are lines nobody wrote wrongly, and - erroring on 820 of them would stop commits over a target rather than a defect. It joins the - others as an error once the tree meets it, which is the same staging this function is. + ONE rule is staged in each direction. The line-length cap warns in a header too: every other + rule describes how this tree is already written, so a finding is a defect against a standard + the file was written under, while the cap is new and erroring on 820 lines nobody wrote + wrongly would stop commits over a target. And NO-HARD-WRAP blocks in both kinds of file: the + tree meets it, at zero findings, so the staging has nothing left to stage. The reason holds + everywhere too, being the diff rather than the page, and a rule met everywhere is a rule that + should hold everywhere. """ + if why.startswith(_HARD_WRAP_RULE): + return True if why.startswith(_LINE_LENGTH_RULES) and "chars >" in why: return False return _generates_a_page(key.partition("::")[0]) @@ -1570,6 +1578,57 @@ def _report(entries, heading: str) -> None: print(f" {title}: {why}") +def _committed_counts(): + """Every rule's warning count in the COMMITTED report, or None when there is no baseline. + + The report is tracked, so the last commit's copy is the number to beat. Read from git rather + than from the working tree, because the run rewrites the file before the comparison happens. + """ + import subprocess + rel = REPORT.relative_to(ROOT).as_posix() + r = subprocess.run(["git", "show", f"HEAD:{rel}"], + cwd=ROOT, capture_output=True, text=True) + if r.returncode != 0: + return None + counts = {} + # Only the `## By rule` table: the area and file tables share its column shape, and a row from + # either would enter the baseline as a rule that can never appear in `now`. + in_rules = False + for line in r.stdout.split("\n"): + if line.startswith("## "): + in_rules = line.strip() == "## By rule" + m = re.match(r"^\| (.+?) \| (\d+) \| (\d+) \|$", line.strip()) + if in_rules and m and m.group(1) != "Rule": + counts[m.group(1)] = int(m.group(3)) + # The TOTAL as its own entry, because a per-rule comparison alone misses a rule that is + # below the baseline and still rising: one under budget can absorb a new finding silently. + m = re.match(r"^\*\*\d+ error\(s\)\*\* and \*\*(\d+) warning\(s\)\*\* across", line.strip()) + if m and "(total)" not in counts: + counts["(total)"] = int(m.group(1)) + return counts or None + + +def _ratchet(found) -> list: + """Which rules rose against the committed report. The list only shrinks. + + A warning is staged work, and staged work that grows is not a sweep. Per RULE rather than on + the total alone, because a total hides one rule paying for another: splitting a long line + lowers the width count and raises the block count, which is the move the run cap exists to + refuse. + """ + from collections import Counter + base = _committed_counts() + if base is None: + return [] + # LABELLED, matching how the report writes them: `_rule_name` returns the internal key + # ("code comment") while the table carries the display label ("multi-line comment blocks"), + # and comparing the two key spaces silently compares nothing. + now = Counter(_rule_label(_rule_name(why)) for key, why in found if not _blocks(key, why)) + now["(total)"] = sum(1 for key, why in found if not _blocks(key, why)) + return [(rule, was, now.get(rule, 0)) + for rule, was in base.items() if now.get(rule, 0) > was] + + def main() -> int: ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--noreport", action="store_true", @@ -1595,7 +1654,13 @@ def main() -> int: f"{MAX_DOC_WORDS} words.") return 0 + risen = _ratchet(found) print(f"Docgen check: {len(errors)} error(s), {len(warnings)} warning(s).\n") + if risen: + print("WARNINGS ROSE against the committed report. The list only shrinks:") + for rule, was, now in risen: + print(f" {rule}: {was} -> {now}") + print(" Fix them, or say in the commit why the rule itself changed.\n") if errors: _report(errors, "ERRORS, in files that generate a page. These fail the gate.") if warnings: @@ -1607,7 +1672,7 @@ def main() -> int: "\n(the technical page the card links), cross-module rationale into a" "\n`## , details` section on the same page." "\nRules: docs/contributing/documentation-standards.md ยง The card.") - return 1 if errors else 0 + return 1 if (errors or risen) else 0 if __name__ == "__main__": diff --git a/moondeck/check/check_prose.py b/moondeck/check/check_prose.py index c7036fbf..ac16ff99 100755 --- a/moondeck/check/check_prose.py +++ b/moondeck/check/check_prose.py @@ -126,7 +126,7 @@ def main(): # the tree. A zero is only trustworthy once something that should fire, fires. probe = subprocess.run( ["vale", "--output=JSON", "--no-exit", "--ext=.h", "--path=probe.h"], - input="// A color scheme \u2014 an em-dash.\nint x = 1;\n", + input="// A colour scheme \u2014 an em-dash.\nint x = 1;\n", capture_output=True, text=True) try: hits = sum(len(v) for v in json.loads(probe.stdout or "{}").values()) diff --git a/mooninstaller/deviceModels.json b/mooninstaller/deviceModels.json index 20743691..a69c8c33 100644 --- a/mooninstaller/deviceModels.json +++ b/mooninstaller/deviceModels.json @@ -35,11 +35,7 @@ "type": "NetworkModule", "id": "Network", "controls": { - "ethType": 1, - "ethPhyAddr": 0, - "ethRstGpio": 5, - "ethClockGpio": 17, - "ethClockExtIn": false + "ethBoard": "Classic RMII" } } ] @@ -329,13 +325,7 @@ "type": "NetworkModule", "id": "Network", "controls": { - "ethType": 1, - "ethPhyAddr": 0, - "ethClockGpio": 17, - "ethClockExtIn": false, - "ethMdcGpio": 23, - "ethMdioGpio": 18, - "ethRstGpio": -1 + "ethBoard": "Classic RMII (no reset)" } }, { @@ -782,13 +772,7 @@ "type": "NetworkModule", "id": "Network", "controls": { - "ethType": 2, - "ethPhyAddr": 1, - "ethMdcGpio": 31, - "ethMdioGpio": 52, - "ethRstGpio": 51, - "ethClockGpio": 50, - "ethClockExtIn": true + "ethBoard": "P4-NANO" } } ] @@ -840,11 +824,7 @@ "type": "NetworkModule", "id": "Network", "controls": { - "ethType": 1, - "ethPhyAddr": 0, - "ethRstGpio": 5, - "ethClockGpio": 17, - "ethClockExtIn": false + "ethBoard": "Classic RMII" } }, { @@ -898,6 +878,7 @@ "type": "NetworkModule", "id": "Network", "controls": { + "ethBoard": "Custom", "ethType": 3, "ethSpiMiso": 13, "ethSpiMosi": 11, @@ -957,6 +938,7 @@ "type": "NetworkModule", "id": "Network", "controls": { + "ethBoard": "Custom", "ethType": 3, "ethSpiMiso": 5, "ethSpiMosi": 6, @@ -1026,13 +1008,7 @@ "type": "NetworkModule", "id": "Network", "controls": { - "ethType": 2, - "ethPhyAddr": 1, - "ethMdcGpio": 31, - "ethMdioGpio": 52, - "ethRstGpio": 51, - "ethClockGpio": 50, - "ethClockExtIn": true + "ethBoard": "P4-NANO" } }, { @@ -1098,13 +1074,7 @@ "type": "NetworkModule", "id": "Network", "controls": { - "ethType": 2, - "ethPhyAddr": 1, - "ethMdcGpio": 31, - "ethMdioGpio": 52, - "ethRstGpio": 51, - "ethClockGpio": 50, - "ethClockExtIn": true + "ethBoard": "P4-NANO" } } ] @@ -1153,11 +1123,7 @@ "type": "NetworkModule", "id": "Network", "controls": { - "ethType": 4, - "ethPhyAddr": -1, - "ethMdcGpio": 5, - "ethMdioGpio": 6, - "ethRstGpio": 7 + "ethBoard": "S31 CoreBoard" } }, { diff --git a/mooninstaller/install.js b/mooninstaller/install.js index 48b7cf2f..9fbde554 100644 --- a/mooninstaller/install.js +++ b/mooninstaller/install.js @@ -1150,7 +1150,13 @@ document.addEventListener('DOMContentLoaded', () => { // active wherever NetworkModule exists (the radio is always available); a board // that lists WiFi as supported but ships no NetworkModule entry stays "supported". const ethConfigured = (m) => { - const t = m.controls && m.controls.ethType; + const c = m.controls; + if (!c) return false; + // A named board preset IS the configuration: it carries the PHY and the pins, so a + // board that picks one lists no ethType of its own. Custom is the escape hatch and + // names no PHY, so it falls through to the ethType its entry must then carry. + if (c.ethBoard !== undefined && c.ethBoard !== "Custom") return true; + const t = c.ethType; return t !== undefined && t !== 0 && t !== "0" && t !== "None"; }; const CAP_MODULE = { diff --git a/src/core/module/Control.h b/src/core/module/Control.h index d31403e1..6c65d83d 100644 --- a/src/core/module/Control.h +++ b/src/core/module/Control.h @@ -138,6 +138,11 @@ struct ListSource { const char* /*valueJson*/) { return false; } }; +// How much a reader wants to see. One number the whole UI composes against, so a control names the audience it is for rather than every card deciding for itself. +inline constexpr uint8_t kModeUser = 0; ///< the show: what a light does +inline constexpr uint8_t kModeExpert = 1; ///< and the tuning an installation needs: the peripheral, the pin, the rate +inline constexpr uint8_t kModeDeveloper = 2; ///< and what diagnoses the firmware, meaning nothing without the source beside it + /// One control's metadata: what it points at, how to render it, and how to persist it. /// /// The value lives in the module's own variable, and this borrows a pointer to it. @@ -159,8 +164,8 @@ struct ControlDescriptor { bool persistLabel = false; /// Whether the UI renders this display-only, for a value tooling pushes rather than a user. bool readonly = false; - /// Whether this is expert-only, shown by the UI only while expert mode is on. - bool advanced = false; + /// The mode a reader needs before this control is shown: 0 everyone, 1 expert, 2 developer. + uint8_t minMode = 0; /// Whether a numeric renders as a number input, for an integer that is an address not a magnitude. bool numberField = false; // These sit after the other flags, since the text initializers below are positional. @@ -370,9 +375,14 @@ class ControlList { if (i < count_) controls_[i].readonly = readonly; } - /// Mark a control expert-only, which the UI shows only while expert mode is on. + /// Mark a control expert-only, which the UI shows from expert mode up. void setAdvanced(uint8_t i, bool advanced = true) { - if (i < count_) controls_[i].advanced = advanced; + if (i < count_) controls_[i].minMode = advanced ? kModeExpert : kModeUser; + } + + /// Mark a control developer-only: a number that diagnoses the firmware rather than the show. + void setDeveloper(uint8_t i) { + if (i < count_) controls_[i].minMode = kModeDeveloper; } /// Render a numeric as a number input, for an integer that is an identity not a magnitude. diff --git a/src/core/module/MoonModule.h b/src/core/module/MoonModule.h index 53889d5f..48983e31 100644 --- a/src/core/module/MoonModule.h +++ b/src/core/module/MoonModule.h @@ -143,7 +143,7 @@ class MoonModule { mix(static_cast(c.type)); mix(static_cast(c.min)); mix(static_cast(c.max)); - mix((c.hidden ? 1u : 0u) | (c.readonly ? 2u : 0u) | (c.advanced ? 4u : 0u)); + mix((c.hidden ? 1u : 0u) | (c.readonly ? 2u : 0u) | (static_cast(c.minMode) << 2)); if (c.type == ControlType::Select && c.aux) { // Hash the strings, so an in-place rename still changes the signature. const char* const* opts = reinterpret_cast(c.aux); diff --git a/src/core/system/HttpServerModule.cpp b/src/core/system/HttpServerModule.cpp index 72db749f..6794d582 100644 --- a/src/core/system/HttpServerModule.cpp +++ b/src/core/system/HttpServerModule.cpp @@ -1220,13 +1220,16 @@ void HttpServerModule::writeModuleJson(JsonSink& sink, MoonModule* mod) { const char* roleStr = roleName(mod->role()); const char* type = mod->typeName(); if (!type) type = ""; + // `enabled` is published for every module, and the ones that keep running regardless say so here, so the UI can leave out a switch that would do nothing. sink.appendf( "{\"name\":\"%s\",\"type\":\"%s\",\"role\":\"%s\",\"enabled\":%s," + "\"respectsEnabled\":%s," "\"tickTimeUs\":%u,\"classSize\":%u,\"dynamicBytes\":%u", mod->name() ? mod->name() : "", type, roleStr, mod->enabled() ? "true" : "false", + mod->respectsEnabled() ? "true" : "false", static_cast(mod->tickTimeUs()), static_cast(mod->classSize()), static_cast(mod->dynamicBytes())); @@ -1304,7 +1307,7 @@ void HttpServerModule::writeControls(JsonSink& sink, MoonModule* mod) { writeControlMetadata(sink, c); // Emit optional flags only when set (common case is false; omit to save bytes). if (c.readonly) sink.append(",\"readonly\":true"); - if (c.advanced) sink.append(",\"advanced\":true"); // UI shows it only in expert mode + if (c.minMode) sink.appendf(",\"minMode\":%u", static_cast(c.minMode)); // the mode the UI needs before it shows this if (c.numberField) sink.append(",\"numberField\":true"); // render a plain number input, not a slider // An editable List (the CRUD primitive) tells the UI to show add/delete/reorder + inline row editors; a plain List stays read-only. The row objects carry a stable "id" the /api/list/* ops address, and each editable row's detail carries its field descriptors. if (c.switchRow) sink.append(",\"switchRow\":true"); diff --git a/src/core/system/NetworkModule.h b/src/core/system/NetworkModule.h index 068a804d..ba6f91d7 100644 --- a/src/core/system/NetworkModule.h +++ b/src/core/system/NetworkModule.h @@ -186,9 +186,24 @@ class NetworkModule : public MoonModule { controls_.addIPv4("dns", staticDns_); controls_.setHidden(controls_.count() - 1, hideStatic); - // Only where a driver is compiled in, the type selecting which pin rows apply. - if constexpr (platform::hasEthernet) { + // Where a driver is compiled in, the type selecting which pin rows apply; a desktop builds the same rows with no interface behind them, to exercise the presets. + if constexpr (platform::hasEthernet || platform::previewsEthernetControls) { + // A preview configures nothing, so it is developer-mode only and says so on the card. + constexpr bool preview = !platform::hasEthernet; + const uint8_t firstEthControl = controls_.count(); + buildEthPresetOptions(); + // First render: on a provisioned board the pins are already the catalog's, so read the preset back off them. A factory board has no type yet and lands on Custom with its chip's pins, which is honest until the catalog names a board. + if (!ethPresetSeeded_) { ethPresetSeeded_ = true; seedEthPresetFromPins(); } + // The restore path fires no onControlChanged, so writing the map HERE is what makes a saved `ethBoard` reach the pins. Keyed on the selection moving, or a rebuild would undo a Custom edit. + else if (ethPresetSel_ != ethPresetApplied_) applyEthPreset(); + ethPresetApplied_ = ethPresetSel_; + controls_.addSelect("ethBoard", ethPresetSel_, ethPresetOptions_, ethPresetCount_); + // By label: the list is filtered per build, so an index would name a different board. + controls_.setPersistLabel(controls_.count() - 1); + // Hidden on a known board, and hidden stays BOUND: the values still drive the interface. + const bool editable = ethPinsEditable(); controls_.addSelect("ethType", ethType_, ethTypeOptions_, 5); + controls_.setHidden(controls_.count() - 1, !editable); const bool isRmii = (ethType_ == 1 || ethType_ == 2); const bool isSpi = (ethType_ == 3); const bool isRgmii = (ethType_ == 4); @@ -197,29 +212,33 @@ class NetworkModule : public MoonModule { // An address rather than a GPIO, and signed for the auto-detect sentinel. controls_.addControl("ethPhyAddr", ethPhyAddr_, -1, 31); controls_.setNumberField(controls_.count() - 1); // an identity, not a magnitude - controls_.setHidden(controls_.count() - 1, !isEth); + controls_.setHidden(controls_.count() - 1, !editable || !isEth); controls_.addPin("ethRstGpio", ethRstGpio_); - controls_.setHidden(controls_.count() - 1, !isEth); + controls_.setHidden(controls_.count() - 1, !editable || !isEth); // Every wired interface needs them, and showing them is what the pin map counts. controls_.addPin("ethMdcGpio", ethMdcGpio_); - controls_.setHidden(controls_.count() - 1, !isRmii && !isRgmii); + controls_.setHidden(controls_.count() - 1, !editable || (!isRmii && !isRgmii)); controls_.addPin("ethMdioGpio", ethMdioGpio_); - controls_.setHidden(controls_.count() - 1, !isRmii && !isRgmii); + controls_.setHidden(controls_.count() - 1, !editable || (!isRmii && !isRgmii)); controls_.addPin("ethClockGpio", ethClockGpio_); - controls_.setHidden(controls_.count() - 1, !isRmii); + controls_.setHidden(controls_.count() - 1, !editable || !isRmii); // A direction, so a toggle rather than a range. controls_.addControl("ethClockExtIn", ethClockExtIn_); - controls_.setHidden(controls_.count() - 1, !isRmii); + controls_.setHidden(controls_.count() - 1, !editable || !isRmii); controls_.addPin("ethSpiMiso", ethSpiMiso_); - controls_.setHidden(controls_.count() - 1, !isSpi); + controls_.setHidden(controls_.count() - 1, !editable || !isSpi); controls_.addPin("ethSpiMosi", ethSpiMosi_); - controls_.setHidden(controls_.count() - 1, !isSpi); + controls_.setHidden(controls_.count() - 1, !editable || !isSpi); controls_.addPin("ethSpiSck", ethSpiSck_); - controls_.setHidden(controls_.count() - 1, !isSpi); + controls_.setHidden(controls_.count() - 1, !editable || !isSpi); controls_.addPin("ethSpiCs", ethSpiCs_); - controls_.setHidden(controls_.count() - 1, !isSpi); + controls_.setHidden(controls_.count() - 1, !editable || !isSpi); controls_.addPin("ethSpiIrq", ethSpiIrq_); - controls_.setHidden(controls_.count() - 1, !isSpi); + controls_.setHidden(controls_.count() - 1, !editable || !isSpi); + // One loop rather than a call beside every row: the whole group carries one tag. + if constexpr (preview) { + for (uint8_t i = firstEthControl; i < controls_.count(); i++) controls_.setDeveloper(i); + } } } @@ -494,6 +513,116 @@ class NetworkModule : public MoonModule { uint32_t appliedAddressingSig_ = 0; bool addressingSigApplied_ = false; + // One board's Ethernet wiring, the fields matching EthPinConfig; -1 leaves a line unused. + struct EthPreset { + const char* label; + int8_t type; // an EthPhyType + int8_t phyAddr; + int8_t mdc, mdio, rst; + int8_t rmiiClock; + bool rmiiClockExtIn; + int8_t miso, mosi, sck, cs, irq; + bool editable; // false on a soldered map: those lines are not the user's to set + }; + + // The presets this module knows, each a board family rather than one product; Custom keeps whatever is in the fields, for a hand-wired board. + static constexpr EthPreset kEthPresets[] = { + // The LAN8720 reference wiring most classic boards follow, which is also the chip default. + {"Classic RMII", 1, 0, 23, 18, 5, 17, false, -1, -1, -1, -1, -1, false}, + // The same wiring with no reset, for a board using GPIO 5 as an LED lane: the PHY resets by jumper. + {"Classic RMII (no reset)", 1, 0, 23, 18, -1, 17, false, -1, -1, -1, -1, -1, false}, + // Waveshare P4-NANO and the boards following its shield pinout: IP101, the clock fed in. + {"P4-NANO", 2, 1, 31, 52, 51, 50, true, -1, -1, -1, -1, -1, false}, + // The S31's 1 Gb PHY, addressed by scan rather than by a fixed address. + {"S31 CoreBoard", 4, -1, 5, 6, 7, -1, false, -1, -1, -1, -1, -1, false}, + {"Custom", 0, -1, -1, -1, -1, -1, false, -1, -1, -1, -1, -1, true}, + }; + static constexpr uint8_t kEthPresetCount = sizeof(kEthPresets) / sizeof(kEthPresets[0]); + + // Which board wiring is selected, the pins following from it unless it is Custom. + uint8_t ethPresetSel_ = 0; + const char* ethPresetOptions_[kEthPresetCount] = {}; + uint8_t ethPresetIndex_[kEthPresetCount] = {}; + uint8_t ethPresetCount_ = 0; + bool ethPresetSeeded_ = false; ///< the preset is matched to the pins once, not every rebuild + uint8_t ethPresetApplied_ = 0; ///< the selection whose map is already written, so a rebuild is not a re-apply + + + // A preset naming a PHY this build cannot drive would offer pins that reach nothing. + /// Does this firmware carry a driver for the preset's PHY? + static bool presetBuildable(const EthPreset& p) { + if (p.type == 0) return true; // Custom, which names no PHY + if (p.type == 3) return platform::hasEthW5500; // SPI, a separate driver + return !platform::hasEthW5500; // the internal EMAC drives the rest + } + + /// Offer the presets this build can drive, re-pointing the selection by label. + void buildEthPresetOptions() { + const char* current = (ethPresetSel_ < ethPresetCount_) ? ethPresetOptions_[ethPresetSel_] : nullptr; + ethPresetCount_ = 0; + for (uint8_t i = 0; i < kEthPresetCount; i++) { + if (!presetBuildable(kEthPresets[i])) continue; + ethPresetOptions_[ethPresetCount_] = kEthPresets[i].label; + ethPresetIndex_[ethPresetCount_] = i; + ethPresetCount_++; + } + // By LABEL, so a filtered list cannot silently select a different board. + uint8_t sel = 0; + if (current) { + for (uint8_t k = 0; k < ethPresetCount_; k++) { + if (std::strcmp(ethPresetOptions_[k], current) == 0) { sel = k; break; } + } + } + ethPresetSel_ = sel; + } + + /// Are the pin controls the user's to edit, for the preset currently selected? + bool ethPinsEditable() const { + if (ethPresetSel_ >= ethPresetCount_) return true; // nothing resolved: never hide + return kEthPresets[ethPresetIndex_[ethPresetSel_]].editable; + } + + // Custom writes nothing, so switching to it after an edit keeps the edit. + /// Write the chosen preset's map into the pin controls. + void applyEthPreset() { + if (ethPresetSel_ >= ethPresetCount_) return; + const EthPreset& p = kEthPresets[ethPresetIndex_[ethPresetSel_]]; + if (p.editable) return; + ethType_ = static_cast(p.type); + ethPhyAddr_ = p.phyAddr; + ethMdcGpio_ = p.mdc; + ethMdioGpio_ = p.mdio; + ethRstGpio_ = p.rst; + ethClockGpio_ = p.rmiiClock; + ethClockExtIn_ = p.rmiiClockExtIn; + ethSpiMiso_ = p.miso; + ethSpiMosi_ = p.mosi; + ethSpiSck_ = p.sck; + ethSpiCs_ = p.cs; + ethSpiIrq_ = p.irq; + } + + // The board a set of pins came from, so a provisioned device opens on its own name. + /// The preset whose map these pin values already are, or Custom when none matches. + void seedEthPresetFromPins() { + for (uint8_t k = 0; k < ethPresetCount_; k++) { + const EthPreset& p = kEthPresets[ethPresetIndex_[k]]; + if (p.editable) continue; + if (p.type == static_cast(ethType_) && p.phyAddr == ethPhyAddr_ && + p.mdc == ethMdcGpio_ && p.mdio == ethMdioGpio_ && p.rst == ethRstGpio_ && + p.rmiiClock == ethClockGpio_ && p.rmiiClockExtIn == ethClockExtIn_ && + p.miso == ethSpiMiso_ && p.mosi == ethSpiMosi_ && p.sck == ethSpiSck_ && + p.cs == ethSpiCs_ && p.irq == ethSpiIrq_) { + ethPresetSel_ = k; + return; + } + } + // Custom is the last row, and the only editable one. + for (uint8_t k = 0; k < ethPresetCount_; k++) { + if (kEthPresets[ethPresetIndex_[k]].editable) { ethPresetSel_ = k; return; } + } + } + /// A cheap hash over the interface controls, so a live change is detected. uint32_t ethSig() const { uint32_t h = ethType_; diff --git a/src/core/system/SystemModule.h b/src/core/system/SystemModule.h index d9639387..066f46e8 100644 --- a/src/core/system/SystemModule.h +++ b/src/core/system/SystemModule.h @@ -137,11 +137,11 @@ class SystemModule : public MoonModule { controls_.addReadOnly("cpu", const_cast(platform::cpuInfo())); ///< the clock and core count controls_.addReadOnly("sdk", const_cast(platform::sdkVersion())); controls_.addReadOnly("bootReason", const_cast(platform::resetReason())); - // The UI honors this client-side, so nothing in the firmware reads it. - controls_.addControl("expertMode", expertMode_); + // The UI honors this client-side, so nothing in the firmware reads it. A LEVEL rather than a switch, because a control names the audience it is for and one number decides which audience is reading. + controls_.addSelect("mode", mode_, kModeOptions, 3); // Warn keeps the once-a-second line off the wire while warnings still print. controls_.addSelect("logLevel", logLevel_, logLevelOptions_, 6); - controls_.setAdvanced(controls_.count() - 1); + controls_.setDeveloper(controls_.count() - 1); // Compiled out where the radio is native, so the control and its query both vanish. if constexpr (platform::hasWifiCoprocessor) { controls_.addReadOnly("wifiCoproc", const_cast(platform::coprocessorWifi())); @@ -217,7 +217,8 @@ class SystemModule : public MoonModule { Scheduler* scheduler_ = nullptr; char deviceName_[24] = {}; ///< the one network identity - bool expertMode_ = false; ///< one flag the whole UI composes against + uint8_t mode_ = 0; ///< one level the whole UI composes against: user, expert, developer + static constexpr const char* kModeOptions[3] = {"user", "expert", "developer"}; /// Push the level to the logger, clamped so a corrupt value cannot index past the end. void applyLogLevel() { uint8_t lvl = logLevel_ > static_cast(platform::LogLevel::Verbose) diff --git a/src/platform/desktop/platform_config.h b/src/platform/desktop/platform_config.h index d6f19128..7c43ae3a 100644 --- a/src/platform/desktop/platform_config.h +++ b/src/platform/desktop/platform_config.h @@ -94,6 +94,9 @@ struct EthPinConfig { /// False, so shared code compiles its Ethernet controls out and seeds itself from the default below. constexpr bool hasEthernet = false; +/// True: the Ethernet CONTROLS are built though no interface exists, so the board presets can be exercised without a device. A preview of a device-only card, tagged developer-mode and never consulted by a bring-up path, which stays keyed on hasEthernet. +constexpr bool previewsEthernetControls = true; + /// True: a host has several NICs, so a raw sender must name the one it binds. constexpr bool hasNamedNetInterfaces = true; diff --git a/src/platform/esp32/platform_config.h b/src/platform/esp32/platform_config.h index 08e6e945..068c8239 100644 --- a/src/platform/esp32/platform_config.h +++ b/src/platform/esp32/platform_config.h @@ -188,6 +188,9 @@ constexpr bool hasEthernet = true; // True when the firmware carries an IP stack at all, which is what UDP interop gates on. constexpr bool hasNetwork = hasWiFi || hasEthernet; +/// False: the controls follow the hardware, so a board without Ethernet shows nothing to configure. +constexpr bool previewsEthernetControls = false; + // True only under emulation, where a saved type would otherwise select hardware that is not there. #ifdef CONFIG_ETH_USE_OPENETH constexpr bool ethPhyIsFixed = true; diff --git a/src/ui/app.js b/src/ui/app.js index e019555c..c78e5d6c 100644 --- a/src/ui/app.js +++ b/src/ui/app.js @@ -55,12 +55,10 @@ const dragTs = {}; // per-control last-touched timestamp (ms): a s const EDITABLE_CONTROL_TYPES = new Set( ["uint8", "uint16", "int16", "int32", "pin", "bool", "text", "textarea", "filepath", "password", "select", "palette", "ipv4"]); -const TIMING_MODES = ["fps", "ms"]; // localStorage keys per ui.md const LS_SELECTED = "mm_selectedRoot"; const LS_THEME = "mm_theme"; -const LS_TIMING = "mm_timing_mode"; const LS_TABS = "mm_selectedTabs"; // { [containerName]: childName }: the open tab per container const LS_EXPANDED = "mm_expanded"; // [moduleName, โ€ฆ]: modules whose "controls"
is open const LS_TA_SIZE = "mm_textareaSizes"; // { ":": heightPx }: user-dragged textarea heights @@ -149,7 +147,7 @@ function lsRead(key, defaultVal) { // this module were the only thing running. On a card that is a claim about the whole pipeline made // from one part of it, so the honest default is the microseconds. The toggle still cycles to fps // for anyone comparing against a target rate. -let timingMode = lsRead(LS_TIMING, "ms"); +let timingMode = "ms"; // the RESTING state; a press peeks at fps and release restores it let theme = lsRead(LS_THEME, "dark"); // --------------------------------------------------------------------------- @@ -534,7 +532,7 @@ async function sendControl(moduleName, controlName, value) { } // Toggling expert mode changes which controls RENDER (the `advanced` ones), not just a value: so // re-render the cards. Structural change, same as an add/remove; the value write above already landed. - if (moduleName === "System" && controlName === "expertMode") renderCards(); + if (moduleName === "System" && controlName === "mode") renderCards(); // Best-effort by design: failures are not retried here. Non-ok responses + // network errors are logged to console so a user with devtools open can see // what went wrong (e.g. a control value the device-side validator rejected). @@ -550,7 +548,7 @@ async function sendControl(moduleName, controlName, value) { // The Firmware card's `image` picks WHICH PARTITION every other control describes, so the // device rebinds version/build/firmware/partition to the other image and the card has to // be redrawn from the new values. Refetched AFTER the POST rather than re-rendered before - // it (the expertMode case above), because the new values only exist once the device has + // it (the mode case above), because the new values only exist once the device has // switched. The routine WS push cannot carry it: renderCards is suppressed while the user // is interacting, and operating this select is exactly that. else if (moduleName === "Firmware" && controlName === "image") refetchState(); @@ -981,13 +979,15 @@ function findModule(name, modules) { return null; } -// Global "expert mode": the System module's expertMode control. Controls tagged `advanced` (dev/tuning -// readouts + knobs) render only when this is on. Read live from state so a toggle takes effect on the -// next render with no reload; default off if System or the control isn't present yet. -function isExpertMode() { +// How much the reader wants to see: the System module's `mode` control, 0 user, 1 expert, 2 +// developer. A control carries the mode it needs (`minMode`) and renders when the reader is at +// least there. Read live from state so a change takes effect on the next render with no reload; +// 0 when System or the control isn't present yet, so a device mid-boot shows the show and nothing +// that needs explaining. +function uiMode() { const sys = state ? findModule("System") : null; - const c = sys && sys.controls && sys.controls.find(c => c.name === "expertMode"); - return !!(c && c.value); + const c = sys && sys.controls && sys.controls.find(c => c.name === "mode"); + return c ? (c.value | 0) : 0; } function renderCards() { @@ -1751,37 +1751,45 @@ function createCard(mod, depth) { // showing โœ“ when on, blank when off. Stores its checked state in // data-checked so updateValues can sync from WS pushes. A native // would not match the other buttons' frame and corner radius. - const enabled = document.createElement("button"); - enabled.type = "button"; - enabled.className = "module-enabled"; - enabled.dataset.mid = mod.name; - enabled.dataset.key = "enabled"; - enabled.setAttribute("aria-pressed", "true"); - enabled.title = "Enable / disable"; - const setEnabledUi = (on) => { - enabled.dataset.checked = on ? "true" : "false"; - enabled.textContent = "โป"; - enabled.classList.toggle("module-enabled--off", !on); - enabled.setAttribute("aria-pressed", on ? "true" : "false"); - card.classList.toggle("card--disabled", !on); - // Grey this module's TAB in the same click, alongside its card: so the tab title dims INSTANTLY - // instead of waiting ~1s for the server's full-state round-trip. (updateTabDot still syncs it on the - // patch path, idempotently, so this just makes the on/off button the immediate driver.) The tab - // lives in the parent's strip, found by the same data-tab-mid updateTabDot uses. - const tabEl = queryByName(`.tab[data-tab-mid="${cssEscape(mod.name)}"]`, "data-tab-mid", mod.name); - if (tabEl) tabEl.classList.toggle("tab--disabled", !on); - }; - setEnabledUi(mod.enabled === undefined ? true : !!mod.enabled); - enabled.addEventListener("click", () => { - const next = enabled.dataset.checked !== "true"; - setEnabledUi(next); - // Stamp dragTs so a WS state push older than this click can't revert - // the toggle before the server has acknowledged. updateValues reads - // dragTs[mod.name + ":enabled"] on line ~952 and suppresses stale - // patches within the 1s cooldown. - dragTs[mod.name + ":enabled"] = Date.now(); - sendControl(mod.name, "enabled", next); - }); + // Only where it DOES something. A module that keeps running regardless says so + // (`respectsEnabled`), and on Network, System or Firmware the switch was a control that + // changed nothing: the scheduler ticks them whatever the flag says, so the device can be + // reached to re-enable whatever else was turned off. + // Built only where it does something; the name, tags and stats below are every card's. + let enabled = null; + if (mod.respectsEnabled !== false) { + enabled = document.createElement("button"); + enabled.type = "button"; + enabled.className = "module-enabled"; + enabled.dataset.mid = mod.name; + enabled.dataset.key = "enabled"; + enabled.setAttribute("aria-pressed", "true"); + enabled.title = "Enable / disable"; + const setEnabledUi = (on) => { + enabled.dataset.checked = on ? "true" : "false"; + enabled.textContent = "โป"; + enabled.classList.toggle("module-enabled--off", !on); + enabled.setAttribute("aria-pressed", on ? "true" : "false"); + card.classList.toggle("card--disabled", !on); + // Grey this module's TAB in the same click, alongside its card: so the tab title dims INSTANTLY + // instead of waiting ~1s for the server's full-state round-trip. (updateTabDot still syncs it on the + // patch path, idempotently, so this just makes the on/off button the immediate driver.) The tab + // lives in the parent's strip, found by the same data-tab-mid updateTabDot uses. + const tabEl = queryByName(`.tab[data-tab-mid="${cssEscape(mod.name)}"]`, "data-tab-mid", mod.name); + if (tabEl) tabEl.classList.toggle("tab--disabled", !on); + }; + setEnabledUi(mod.enabled === undefined ? true : !!mod.enabled); + enabled.addEventListener("click", () => { + const next = enabled.dataset.checked !== "true"; + setEnabledUi(next); + // Stamp dragTs so a WS state push older than this click can't revert + // the toggle before the server has acknowledged. updateValues reads + // dragTs[mod.name + ":enabled"] on line ~952 and suppresses stale + // patches within the 1s cooldown. + dragTs[mod.name + ":enabled"] = Date.now(); + sendControl(mod.name, "enabled", next); + }); + } const name = document.createElement("span"); name.className = "card-name"; @@ -1803,27 +1811,38 @@ function createCard(mod, depth) { spacer.className = "card-spacer"; title.appendChild(spacer); - // fps/ms toggle on the stats line: global mode, single click cycles all cards - const stats = document.createElement("span"); - stats.className = "card-stats"; - stats.dataset.mid = mod.name; - stats.dataset.key = "stats"; - stats.title = formatStatsTitle(mod); - stats.textContent = formatStats(mod); - stats.addEventListener("click", () => { - const idx = TIMING_MODES.indexOf(timingMode); - timingMode = TIMING_MODES[(idx + 1) % TIMING_MODES.length]; - localStorage.setItem(LS_TIMING, timingMode); - // Refresh every card's stats line in place: no full re-render needed - document.querySelectorAll(".card-stats[data-mid]").forEach(s => { - const m = findModule(s.dataset.mid); - if (m) { s.textContent = formatStats(m); s.title = formatStatsTitle(m); } - }); - }); - title.appendChild(stats); + // Built from expert up: in user mode the line is empty and there is nothing to hold. + // + // HOLD TO PEEK, the password field's gesture. Time is what the line says, because a tick time + // is the honest per-module figure: fps inverts it, so a module ticking in 22 us reads as 45K + // fps and invites being read as a frame rate it is not. The rate is one press away for anyone + // who wants it, and it goes back on release rather than leaving every card in a mode. + if (uiMode() >= 1) { + const stats = document.createElement("span"); + stats.className = "card-stats"; + stats.dataset.mid = mod.name; + stats.dataset.key = "stats"; + stats.title = formatStatsTitle(mod); + stats.textContent = formatStats(mod); + const peekAll = (mode) => { + timingMode = mode; + document.querySelectorAll(".card-stats[data-mid]").forEach(el => { + const m = findModule(el.dataset.mid); + if (m) { el.textContent = formatStats(m); el.title = formatStatsTitle(m); } + }); + }; + const show = () => peekAll("fps"); + const hide = () => peekAll("ms"); + stats.addEventListener("mousedown", show); + stats.addEventListener("mouseup", hide); + stats.addEventListener("mouseleave", hide); + stats.addEventListener("touchstart", (e) => { e.preventDefault(); show(); }); + stats.addEventListener("touchend", hide); + title.appendChild(stats); + } // Enable checkbox joins the right-hand action cluster, before โœŽ/ร—. - title.appendChild(enabled); + if (enabled) title.appendChild(enabled); // Delete / replace buttons for user-managed children (any role a container // accepts, minus modules that opted out via userEditable=false). Top-level @@ -2317,7 +2336,12 @@ function fmtBytes(n) { // Stats line: timing (๐Ÿ•’, fps or ยตs/ms per the global toggle) + memory // (๐Ÿง  static, plus "+ dynamic" only when the module allocated heap). // Timing is omitted entirely when the module has no measured loop time. +// +// EXPERT AND UP. A tick time and a byte count diagnose the firmware rather than the show: they +// say which module is spending the frame, which is a question a reader in user mode is not +// asking and cannot act on. The same gate the controls use, so one mode decides the whole card. function formatStats(mod) { + if (uiMode() < 1) return ""; const us = (mod.tickTimeUs !== undefined) ? mod.tickTimeUs : 0; let timing = ""; if (us > 0) { @@ -2528,7 +2552,7 @@ function renderEmojiTags(el, list) { // paths (renderCards's initial build + updateModuleControls's WS live-patch) so they agree. function controlRendersGenerically(mod, ctrl) { if (ctrl.hidden) return false; - if (ctrl.advanced && !isExpertMode()) return false; // expert-only control, expert mode is off + if ((ctrl.minMode | 0) > uiMode()) return false; // above the reader's mode return true; } @@ -2537,7 +2561,10 @@ function createControl(moduleName, moduleType, ctrl) { row.className = "control-row"; // Expert-only controls (only reachable here when expert mode is on: see controlRendersGenerically) // get a distinct treatment so they read as a different tier: a left accent stripe + muted label. - if (ctrl.advanced) row.classList.add("control-advanced"); + // One class per tier, because the glyph says WHICH mode reveals it: a reader in expert mode + // meets both marks and the developer one tells them what they are still not seeing. + if ((ctrl.minMode | 0) === 1) row.classList.add("control-expert"); + else if ((ctrl.minMode | 0) >= 2) row.classList.add("control-developer"); // A switch-row control renders as a strip like an encoder or fader, so switch N sits in the // same column as encoder N and fader N: a surface reads down a channel, not across a list. if (ctrl.switchRow) row.classList.add("control-switch"); @@ -2545,10 +2572,10 @@ function createControl(moduleName, moduleType, ctrl) { const label = document.createElement("label"); label.className = "control-label"; - // The expertMode toggle itself carries the wrench glyph (via CSS ::before), so it reads as the switch + // The mode select itself carries both glyphs (via CSS ::after), so it reads as the switch // that governs the ๐Ÿ”ง controls: the toggle is never `advanced` (it must always be reachable), so key // it by name rather than the flag. - if (moduleName === "System" && ctrl.name === "expertMode") label.classList.add("control-label--expert"); + if (moduleName === "System" && ctrl.name === "mode") label.classList.add("control-label--mode"); label.textContent = displayName(ctrl.name); // The display strip carries no label: it spans the card and shows whatever was last touched, so // a label naming one control would be wrong the moment another moved. diff --git a/src/ui/style.css b/src/ui/style.css index a85e561d..7f94f6d9 100644 --- a/src/ui/style.css +++ b/src/ui/style.css @@ -725,12 +725,22 @@ body.cards-resizing { color: var(--fg-muted); } -/* Expert-only controls (ctrl.advanced, shown only in expert mode): a small wrench glyph after the label - marks them as a distinct "tuning/dev" tier at a glance โ€” semantically clearer than a bare accent stripe. - The same glyph sits on the expertMode toggle itself (see .control-label--expert), tying cause to effect. */ -.control-advanced > .control-label::after, -.control-label--expert::after { +/* A control above user mode carries a glyph naming the tier that reveals it, which reads at a glance + where an accent stripe would not. The SLIDER is expert, the tuning a working installation needs; + the WRENCH is developer, what diagnoses the firmware. The mode select itself carries both, tying + cause to effect (see .control-label--mode). */ +.control-expert > .control-label::after { + content: "๐ŸŽš๏ธ"; +} +.control-developer > .control-label::after { content: "๐Ÿ”ง"; +} +.control-label--mode::after { + content: "๐ŸŽš๏ธ๐Ÿ”ง"; +} +.control-expert > .control-label::after, +.control-developer > .control-label::after, +.control-label--mode::after { font-size: 10px; margin-left: 4px; opacity: 0.7; diff --git a/test/python/test_check_docgen.py b/test/python/test_check_docgen.py index c0a74b09..2dc66945 100644 --- a/test/python/test_check_docgen.py +++ b/test/python/test_check_docgen.py @@ -917,6 +917,60 @@ def test_a_lead_is_measured_as_a_block_so_chopping_does_not_satisfy_it(): "no single line is over the cap, which is the point of measuring the block" +def test_the_ratchet_refuses_a_rule_that_rose_against_the_committed_report(monkeypatch): + """A warning is staged work, and staged work that grows is not a sweep. The committed report + is the number to beat, per RULE and on the total: per rule alone misses a rule still under + its own baseline while the total rises, and the total alone hides one rule paying for + another, which is what splitting a long line does to the width and block counts. + + Drives `_ratchet` itself. An earlier version of this test asserted against a local copy of the + comparison and passed while the real function compared internal keys against display labels, + so it matched nothing and the per-rule half never fired.""" + import check_docgen + base = {"(total)": 10, "multi-line comment blocks": 4, "hard wraps": 2} + monkeypatch.setattr(check_docgen, "_committed_counts", lambda: base) + + # A `.cpp` key, because the ratchet guards the WARNING column and a header's finding blocks. + cpp = "src/core/x.cpp" + block = "code comment 3 lines > 1" + + assert check_docgen._ratchet([(cpp, block)] * 3) == [], "under every baseline: silent" + + rose = check_docgen._ratchet([(cpp, block)] * 5) + assert rose == [("multi-line comment blocks", 4, 5)], \ + f"one rule paying for another is caught, and the total (5) stays under 10: {rose}" + + # The labels the report writes ARE the keys the comparison uses, in both directions. + assert check_docgen._rule_label(check_docgen._rule_name(block)) in base + + # And the real parser finds the tracked report's numbers, or the comparison reads nothing. + monkeypatch.undo() + counts = check_docgen._committed_counts() + assert counts is None or "(total)" in counts, counts + # Only the `## By rule` table feeds the baseline: an area row would be a rule that never appears. + assert counts is None or not any(k.startswith("`") for k in counts), counts + + +def test_a_hard_wrap_blocks_in_an_implementation_file_too(): + """The one rule staged the OTHER way. Every other `.cpp` finding warns while the sweep runs, + but the tree is at ZERO hard wraps, so there is nothing left to stage and the exemption only + protects a regression. The reason holds everywhere too: a split sentence reflows every line + it spans on the next word change, which costs a reviewer the same in either file.""" + import check_docgen + src = ("#include \n\nvoid g();\n\n" + "// A sentence that carries on\n" + "// onto the following line.\n" + "void f();\n") + for rel in ("src/core/x.h", "src/core/x.cpp"): + hits = [(k, w) for k, w in check_docgen._header_rules(rel, src) if "hard wrap" in w] + assert hits, rel + assert all(check_docgen._blocks(k, w) for k, w in hits), (rel, "must BLOCK") + # And the rules still being swept keep warning in a `.cpp`, or the staging is gone entirely. + runs = [(k, w) for k, w in check_docgen._header_rules("src/core/x.cpp", src) + if "code comment" in w] + assert runs and not any(check_docgen._blocks(k, w) for k, w in runs), runs + + def test_the_line_length_cap_warns_even_in_a_header(): """The one staged exception. Every other rule describes how the tree is already written, so a finding is a defect against a standard the file was written under. This cap is new, so its diff --git a/test/scenarios/core/scenario_MoonModule_control_change.json b/test/scenarios/core/scenario_MoonModule_control_change.json index 05831d52..27c460be 100644 --- a/test/scenarios/core/scenario_MoonModule_control_change.json +++ b/test/scenarios/core/scenario_MoonModule_control_change.json @@ -117,14 +117,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 126, + "p50": 124, "p95": 196, - "min": 117, + "min": 115, "max": 198, "n": 32, - "samples": [160, 120, 161, 163, 194, 186, 133, 122, 196, 123, 194, 119, 121, 126, 126, 121, 123, 117, 194, 117, 123, 121, 124, 194, 124, 193, 191, 192, 198, 194, 142, 119] + "samples": [163, 194, 186, 133, 122, 196, 123, 194, 119, 121, 126, 126, 121, 123, 117, 194, 117, 123, 121, 124, 194, 124, 193, 191, 192, 198, 194, 142, 119, 115, 120, 119] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32-eth-wifi": { "tick_us": { @@ -304,9 +304,9 @@ "min": 119, "max": 139, "n": 32, - "samples": [123, 120, 125, 135, 131, 129, 131, 123, 132, 124, 130, 119, 122, 127, 139, 120, 122, 119, 130, 119, 128, 124, 121, 131, 139, 130, 131, 129, 132, 131, 127, 122] + "samples": [135, 131, 129, 131, 123, 132, 124, 130, 119, 122, 127, 139, 120, 122, 119, 130, 119, 128, 124, 121, 131, 139, 130, 131, 129, 132, 131, 127, 122, 120, 120, 119] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32-eth-wifi": { "tick_us": { @@ -481,14 +481,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 119, + "p50": 120, "p95": 125, "min": 115, "max": 126, "n": 32, - "samples": [118, 118, 120, 116, 118, 117, 120, 121, 120, 121, 115, 119, 125, 125, 118, 121, 121, 120, 117, 119, 123, 126, 120, 117, 123, 118, 116, 115, 117, 116, 116, 122] + "samples": [116, 118, 117, 120, 121, 120, 121, 115, 119, 125, 125, 118, 121, 121, 120, 117, 119, 123, 126, 120, 117, 123, 118, 116, 115, 117, 116, 116, 122, 121, 121, 120] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32-eth-wifi": { "tick_us": { @@ -671,14 +671,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 120, - "p95": 126, + "p50": 119, + "p95": 125, "min": 117, "max": 128, "n": 32, - "samples": [121, 126, 122, 117, 119, 119, 120, 121, 121, 119, 118, 119, 122, 125, 120, 120, 121, 120, 119, 119, 128, 125, 120, 119, 124, 118, 119, 118, 119, 120, 119, 119] + "samples": [117, 119, 119, 120, 121, 121, 119, 118, 119, 122, 125, 120, 120, 121, 120, 119, 119, 128, 125, 120, 119, 124, 118, 119, 118, 119, 120, 119, 119, 119, 120, 120] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32-eth-wifi": { "tick_us": { diff --git a/test/scenarios/light/scenario_Audio_mutation.json b/test/scenarios/light/scenario_Audio_mutation.json index 96eed9bc..c3e7b447 100644 --- a/test/scenarios/light/scenario_Audio_mutation.json +++ b/test/scenarios/light/scenario_Audio_mutation.json @@ -109,9 +109,9 @@ "min": 16, "max": 39, "n": 32, - "samples": [17, 16, 16, 16, 16, 16, 20, 16, 20, 16, 19, 18, 21, 16, 16, 20, 16, 16, 17, 39, 17, 20, 20, 17, 16, 16, 16, 17, 16, 17, 17, 17] + "samples": [16, 16, 16, 20, 16, 20, 16, 19, 18, 21, 16, 16, 20, 16, 16, 17, 39, 17, 20, 20, 17, 16, 16, 16, 17, 16, 17, 17, 17, 20, 20, 20] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { @@ -202,13 +202,13 @@ "desktop-macos": { "tick_us": { "p50": 21, - "p95": 67, + "p95": 43, "min": 17, "max": 186, "n": 32, - "samples": [67, 37, 18, 17, 25, 19, 19, 21, 18, 20, 21, 18, 20, 17, 43, 41, 27, 25, 23, 26, 36, 17, 20, 186, 21, 33, 21, 18, 26, 20, 18, 17] + "samples": [17, 25, 19, 19, 21, 18, 20, 21, 18, 20, 17, 43, 41, 27, 25, 23, 26, 36, 17, 20, 186, 21, 33, 21, 18, 26, 20, 18, 17, 39, 26, 20] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { @@ -316,13 +316,13 @@ "desktop-macos": { "tick_us": { "p50": 19, - "p95": 65, + "p95": 48, "min": 17, "max": 109, "n": 32, - "samples": [65, 29, 20, 18, 28, 18, 20, 17, 19, 19, 21, 19, 20, 17, 19, 34, 24, 19, 20, 30, 30, 18, 23, 109, 29, 32, 19, 17, 19, 19, 18, 17] + "samples": [18, 28, 18, 20, 17, 19, 19, 21, 19, 20, 17, 19, 34, 24, 19, 20, 30, 30, 18, 23, 109, 29, 32, 19, 17, 19, 19, 18, 17, 26, 48, 20] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { @@ -413,13 +413,13 @@ "desktop-macos": { "tick_us": { "p50": 22, - "p95": 47, + "p95": 38, "min": 18, "max": 61, "n": 32, - "samples": [47, 19, 22, 19, 38, 22, 20, 18, 24, 24, 22, 23, 21, 22, 20, 61, 21, 21, 23, 34, 28, 22, 21, 21, 27, 28, 20, 19, 20, 21, 23, 18] + "samples": [19, 38, 22, 20, 18, 24, 24, 22, 23, 21, 22, 20, 61, 21, 21, 23, 34, 28, 22, 21, 21, 27, 28, 20, 19, 20, 21, 23, 18, 26, 29, 23] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { @@ -512,9 +512,9 @@ "min": 18, "max": 50, "n": 32, - "samples": [28, 21, 20, 18, 26, 21, 20, 18, 35, 20, 23, 21, 21, 22, 21, 22, 19, 19, 20, 21, 50, 22, 19, 19, 40, 21, 19, 21, 23, 21, 22, 21] + "samples": [18, 26, 21, 20, 18, 35, 20, 23, 21, 21, 22, 21, 22, 19, 19, 20, 21, 50, 22, 19, 19, 40, 21, 19, 21, 23, 21, 22, 21, 21, 19, 23] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { @@ -603,13 +603,13 @@ "desktop-macos": { "tick_us": { "p50": 20, - "p95": 23, - "min": 17, - "max": 33, + "p95": 22, + "min": 16, + "max": 23, "n": 32, - "samples": [33, 17, 17, 20, 18, 20, 20, 17, 21, 21, 19, 21, 20, 20, 20, 19, 17, 17, 18, 21, 23, 20, 22, 18, 19, 18, 17, 20, 19, 17, 20, 20] + "samples": [20, 18, 20, 20, 17, 21, 21, 19, 21, 20, 20, 20, 19, 17, 17, 18, 21, 23, 20, 22, 18, 19, 18, 17, 20, 19, 17, 20, 20, 19, 16, 20] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { diff --git a/test/scenarios/light/scenario_Aurora_fps.json b/test/scenarios/light/scenario_Aurora_fps.json index d748b386..9901b245 100644 --- a/test/scenarios/light/scenario_Aurora_fps.json +++ b/test/scenarios/light/scenario_Aurora_fps.json @@ -85,13 +85,13 @@ "desktop-macos": { "tick_us": { "p50": 862, - "p95": 1266, + "p95": 1177, "min": 819, "max": 1744, "n": 32, - "samples": [859, 1266, 840, 873, 821, 923, 837, 846, 890, 853, 875, 854, 851, 843, 919, 841, 1005, 1177, 852, 880, 907, 862, 839, 1744, 877, 929, 1001, 834, 899, 864, 835, 819] + "samples": [873, 821, 923, 837, 846, 890, 853, 875, 854, 851, 843, 919, 841, 1005, 1177, 852, 880, 907, 862, 839, 1744, 877, 929, 1001, 834, 899, 864, 835, 819, 836, 840, 862] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -114,14 +114,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 322, + "p50": 321, "p95": 478, "min": 308, "max": 936, "n": 32, - "samples": [316, 442, 314, 312, 308, 322, 318, 317, 321, 321, 315, 326, 323, 320, 936, 318, 336, 320, 322, 331, 478, 332, 316, 444, 333, 346, 405, 315, 349, 349, 324, 309] + "samples": [312, 308, 322, 318, 317, 321, 321, 315, 326, 323, 320, 936, 318, 336, 320, 322, 331, 478, 332, 316, 444, 333, 346, 405, 315, 349, 349, 324, 309, 316, 316, 317] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -149,9 +149,9 @@ "min": 180, "max": 391, "n": 32, - "samples": [186, 285, 185, 181, 181, 189, 188, 189, 190, 191, 185, 189, 189, 195, 301, 186, 190, 201, 189, 196, 261, 194, 185, 373, 188, 197, 391, 184, 207, 186, 182, 180] + "samples": [181, 181, 189, 188, 189, 190, 191, 185, 189, 189, 195, 301, 186, 190, 201, 189, 196, 261, 194, 185, 373, 188, 197, 391, 184, 207, 186, 182, 180, 185, 185, 185] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -174,14 +174,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 552, + "p50": 551, "p95": 899, "min": 529, "max": 1141, "n": 32, - "samples": [541, 642, 538, 532, 535, 559, 551, 546, 549, 549, 541, 607, 567, 719, 899, 547, 558, 552, 674, 584, 1141, 568, 543, 662, 559, 600, 642, 541, 579, 542, 540, 529] + "samples": [532, 535, 559, 551, 546, 549, 549, 541, 607, 567, 719, 899, 547, 558, 552, 674, 584, 1141, 568, 543, 662, 559, 600, 642, 541, 579, 542, 540, 529, 546, 544, 546] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -204,14 +204,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 945, + "p50": 940, "p95": 1265, "min": 910, "max": 1683, "n": 32, - "samples": [934, 1063, 1007, 918, 915, 960, 1015, 940, 945, 949, 938, 934, 977, 1001, 1253, 939, 943, 940, 938, 978, 1683, 1000, 939, 1070, 981, 978, 1265, 931, 998, 932, 917, 910] + "samples": [918, 915, 960, 1015, 940, 945, 949, 938, 934, 977, 1001, 1253, 939, 943, 940, 938, 978, 1683, 1000, 939, 1070, 981, 978, 1265, 931, 998, 932, 917, 910, 938, 939, 936] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -234,14 +234,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 1460, - "p95": 1678, + "p50": 1458, + "p95": 1665, "min": 1407, - "max": 1894, + "max": 1678, "n": 32, - "samples": [1431, 1894, 1531, 1417, 1410, 1461, 1454, 1453, 1462, 1460, 1448, 1443, 1470, 1502, 1486, 1465, 1458, 1460, 1443, 1498, 1555, 1527, 1446, 1503, 1517, 1665, 1678, 1440, 1475, 1440, 1414, 1407] + "samples": [1417, 1410, 1461, 1454, 1453, 1462, 1460, 1448, 1443, 1470, 1502, 1486, 1465, 1458, 1460, 1443, 1498, 1555, 1527, 1446, 1503, 1517, 1665, 1678, 1440, 1475, 1440, 1414, 1407, 1445, 1450, 1447] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -264,14 +264,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 1526, + "p50": 1521, "p95": 1947, "min": 1453, "max": 1951, "n": 32, - "samples": [1525, 1655, 1573, 1496, 1498, 1899, 1508, 1648, 1506, 1505, 1505, 1493, 1624, 1526, 1521, 1535, 1500, 1503, 1624, 1564, 1590, 1951, 1590, 1517, 1549, 1679, 1544, 1489, 1947, 1550, 1455, 1453] + "samples": [1496, 1498, 1899, 1508, 1648, 1506, 1505, 1505, 1493, 1624, 1526, 1521, 1535, 1500, 1503, 1624, 1564, 1590, 1951, 1590, 1517, 1549, 1679, 1544, 1489, 1947, 1550, 1455, 1453, 1522, 1498, 1497] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -295,13 +295,13 @@ "desktop-macos": { "tick_us": { "p50": 1117, - "p95": 1443, + "p95": 1258, "min": 1071, "max": 1828, "n": 32, - "samples": [1087, 1443, 1096, 1078, 1088, 1131, 1119, 1117, 1109, 1117, 1101, 1099, 1828, 1258, 1115, 1121, 1148, 1103, 1223, 1150, 1163, 1142, 1120, 1116, 1150, 1137, 1134, 1108, 1108, 1177, 1077, 1071] + "samples": [1078, 1088, 1131, 1119, 1117, 1109, 1117, 1101, 1099, 1828, 1258, 1115, 1121, 1148, 1103, 1223, 1150, 1163, 1142, 1120, 1116, 1150, 1137, 1134, 1108, 1108, 1177, 1077, 1071, 1106, 1103, 1102] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } } diff --git a/test/scenarios/light/scenario_Driver_mutation.json b/test/scenarios/light/scenario_Driver_mutation.json index fb1c7052..92dce12d 100644 --- a/test/scenarios/light/scenario_Driver_mutation.json +++ b/test/scenarios/light/scenario_Driver_mutation.json @@ -76,14 +76,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 19, + "p50": 20, "p95": 30, "min": 16, "max": 46, "n": 32, - "samples": [20, 21, 19, 21, 20, 16, 20, 20, 46, 23, 20, 19, 16, 20, 20, 17, 17, 17, 17, 19, 19, 19, 19, 23, 21, 30, 17, 20, 17, 17, 20, 16] + "samples": [21, 20, 16, 20, 20, 46, 23, 20, 19, 16, 20, 20, 17, 17, 17, 17, 19, 19, 19, 19, 23, 21, 30, 17, 20, 17, 17, 20, 16, 20, 20, 20] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { @@ -178,9 +178,9 @@ "min": 16, "max": 38, "n": 32, - "samples": [20, 20, 20, 20, 20, 16, 20, 19, 38, 19, 20, 21, 16, 20, 21, 17, 19, 16, 19, 20, 20, 20, 20, 21, 21, 23, 20, 20, 17, 18, 20, 17] + "samples": [20, 20, 16, 20, 19, 38, 19, 20, 21, 16, 20, 21, 17, 19, 16, 19, 20, 20, 20, 20, 21, 21, 23, 20, 20, 17, 18, 20, 17, 20, 20, 20] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { @@ -275,9 +275,9 @@ "min": 16, "max": 22, "n": 32, - "samples": [20, 20, 19, 20, 20, 18, 20, 19, 22, 17, 19, 19, 19, 20, 20, 16, 17, 17, 20, 20, 18, 19, 18, 17, 18, 16, 20, 20, 19, 21, 20, 20] + "samples": [20, 20, 18, 20, 19, 22, 17, 19, 19, 19, 20, 20, 16, 17, 17, 20, 20, 18, 19, 18, 17, 18, 16, 20, 20, 19, 21, 20, 20, 20, 20, 19] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { @@ -370,9 +370,9 @@ "min": 16, "max": 29, "n": 32, - "samples": [20, 20, 20, 21, 20, 20, 20, 19, 19, 29, 20, 20, 19, 20, 21, 16, 17, 17, 20, 20, 16, 19, 19, 18, 21, 17, 20, 20, 21, 19, 20, 20] + "samples": [21, 20, 20, 20, 19, 19, 29, 20, 20, 19, 20, 21, 16, 17, 17, 20, 20, 16, 19, 19, 18, 21, 17, 20, 20, 21, 19, 20, 20, 19, 20, 20] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { @@ -460,14 +460,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 19, + "p50": 20, "p95": 22, "min": 16, "max": 28, "n": 32, - "samples": [20, 19, 20, 19, 20, 20, 20, 19, 20, 18, 19, 19, 20, 20, 20, 16, 17, 16, 20, 19, 17, 19, 19, 18, 19, 19, 20, 28, 21, 22, 20, 20] + "samples": [19, 20, 20, 20, 19, 20, 18, 19, 19, 20, 20, 20, 16, 17, 16, 20, 19, 17, 19, 19, 18, 19, 19, 20, 28, 21, 22, 20, 20, 20, 20, 20] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { diff --git a/test/scenarios/light/scenario_Effects_composition.json b/test/scenarios/light/scenario_Effects_composition.json index 631d4ae5..4ec876fb 100644 --- a/test/scenarios/light/scenario_Effects_composition.json +++ b/test/scenarios/light/scenario_Effects_composition.json @@ -111,9 +111,9 @@ "min": 141, "max": 656, "n": 32, - "samples": [145, 145, 143, 145, 144, 144, 144, 145, 147, 143, 158, 656, 144, 144, 144, 143, 148, 150, 144, 142, 147, 146, 152, 143, 149, 160, 145, 142, 144, 145, 142, 141] + "samples": [145, 144, 144, 144, 145, 147, 143, 158, 656, 144, 144, 144, 143, 148, 150, 144, 142, 147, 146, 152, 143, 149, 160, 145, 142, 144, 145, 142, 141, 143, 143, 144] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { diff --git a/test/scenarios/light/scenario_Fields_polar_lut.json b/test/scenarios/light/scenario_Fields_polar_lut.json index a53caae2..d49f789e 100644 --- a/test/scenarios/light/scenario_Fields_polar_lut.json +++ b/test/scenarios/light/scenario_Fields_polar_lut.json @@ -85,14 +85,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 286, + "p50": 285, "p95": 369, - "min": 274, + "min": 275, "max": 421, "n": 32, - "samples": [274, 297, 281, 279, 278, 290, 293, 285, 285, 283, 286, 284, 421, 369, 286, 303, 285, 284, 289, 295, 307, 292, 287, 305, 299, 287, 335, 282, 284, 282, 276, 275] + "samples": [279, 278, 290, 293, 285, 285, 283, 286, 284, 421, 369, 286, 303, 285, 284, 289, 295, 307, 292, 287, 305, 299, 287, 335, 282, 284, 282, 276, 275, 282, 283, 283] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -120,9 +120,9 @@ "min": 274, "max": 596, "n": 32, - "samples": [274, 292, 279, 276, 276, 290, 290, 283, 284, 287, 283, 284, 308, 524, 284, 306, 281, 596, 303, 296, 299, 291, 289, 318, 300, 282, 284, 281, 283, 281, 274, 294] + "samples": [276, 276, 290, 290, 283, 284, 287, 283, 284, 308, 524, 284, 306, 281, 596, 303, 296, 299, 291, 289, 318, 300, 282, 284, 281, 283, 281, 274, 294, 282, 285, 284] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -147,12 +147,12 @@ "tick_us": { "p50": 284, "p95": 455, - "min": 275, + "min": 277, "max": 732, "n": 32, - "samples": [275, 337, 278, 277, 281, 286, 283, 284, 283, 289, 280, 282, 297, 372, 732, 284, 282, 313, 295, 376, 319, 292, 291, 455, 317, 279, 307, 281, 282, 281, 278, 301] + "samples": [277, 281, 286, 283, 284, 283, 289, 280, 282, 297, 372, 732, 284, 282, 313, 295, 376, 319, 292, 291, 455, 317, 279, 307, 281, 282, 281, 278, 301, 285, 283, 283] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -180,9 +180,9 @@ "min": 273, "max": 607, "n": 32, - "samples": [274, 334, 281, 275, 278, 283, 282, 286, 287, 283, 282, 281, 297, 324, 343, 284, 303, 307, 304, 371, 305, 607, 289, 326, 302, 273, 282, 282, 281, 282, 276, 274] + "samples": [275, 278, 283, 282, 286, 287, 283, 282, 281, 297, 324, 343, 284, 303, 307, 304, 371, 305, 607, 289, 326, 302, 273, 282, 282, 281, 282, 276, 274, 283, 283, 282] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -205,14 +205,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 141, - "p95": 253, - "min": 135, + "p50": 140, + "p95": 250, + "min": 136, "max": 408, "n": 32, - "samples": [135, 253, 140, 139, 136, 141, 139, 140, 141, 141, 139, 139, 146, 156, 142, 140, 149, 197, 147, 149, 159, 408, 250, 141, 145, 143, 140, 140, 140, 139, 136, 136] + "samples": [139, 136, 141, 139, 140, 141, 141, 139, 139, 146, 156, 142, 140, 149, 197, 147, 149, 159, 408, 250, 141, 145, 143, 140, 140, 140, 139, 136, 136, 140, 140, 139] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -235,14 +235,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 434, - "p95": 622, - "min": 407, + "p50": 424, + "p95": 562, + "min": 408, "max": 641, "n": 32, - "samples": [407, 622, 421, 411, 414, 439, 421, 420, 446, 463, 419, 418, 434, 492, 424, 424, 470, 641, 447, 442, 562, 485, 488, 435, 449, 439, 420, 422, 415, 439, 408, 411] + "samples": [411, 414, 439, 421, 420, 446, 463, 419, 418, 434, 492, 424, 424, 470, 641, 447, 442, 562, 485, 488, 435, 449, 439, 420, 422, 415, 439, 408, 411, 421, 421, 421] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -271,14 +271,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 1275, - "p95": 2014, + "p50": 1263, + "p95": 1624, "min": 1198, - "max": 2429, + "max": 2014, "n": 32, - "samples": [1237, 2429, 1286, 1229, 1219, 1531, 1219, 1451, 1439, 1624, 1275, 1237, 1551, 1340, 1223, 1253, 2014, 1265, 1319, 1459, 1282, 1421, 1365, 1251, 1287, 1262, 1209, 1309, 1263, 1253, 1198, 1218] + "samples": [1229, 1219, 1531, 1219, 1451, 1439, 1624, 1275, 1237, 1551, 1340, 1223, 1253, 2014, 1265, 1319, 1459, 1282, 1421, 1365, 1251, 1287, 1262, 1209, 1309, 1263, 1253, 1198, 1218, 1251, 1239, 1224] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -301,14 +301,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 477, + "p50": 476, "p95": 698, "min": 457, "max": 1029, "n": 32, - "samples": [471, 540, 479, 464, 461, 478, 473, 474, 475, 479, 476, 482, 482, 520, 474, 479, 698, 509, 483, 501, 1029, 473, 532, 472, 475, 488, 468, 525, 473, 471, 477, 457] + "samples": [464, 461, 478, 473, 474, 475, 479, 476, 482, 482, 520, 474, 479, 698, 509, 483, 501, 1029, 473, 532, 472, 475, 488, 468, 525, 473, 471, 477, 457, 473, 471, 491] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } } diff --git a/test/scenarios/light/scenario_Fluid_solver.json b/test/scenarios/light/scenario_Fluid_solver.json index 331267de..10826896 100644 --- a/test/scenarios/light/scenario_Fluid_solver.json +++ b/test/scenarios/light/scenario_Fluid_solver.json @@ -83,9 +83,9 @@ "min": 28, "max": 76, "n": 32, - "samples": [28, 33, 29, 28, 28, 29, 29, 29, 30, 29, 30, 29, 33, 31, 29, 29, 76, 29, 29, 30, 30, 30, 35, 30, 30, 29, 29, 33, 29, 29, 32, 28] + "samples": [28, 28, 29, 29, 29, 30, 29, 30, 29, 33, 31, 29, 29, 76, 29, 29, 30, 30, 30, 35, 30, 30, 29, 29, 33, 29, 29, 32, 28, 29, 29, 30] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -105,9 +105,9 @@ "min": 18, "max": 196, "n": 32, - "samples": [19, 24, 19, 18, 19, 19, 19, 19, 19, 19, 19, 19, 21, 21, 19, 19, 196, 19, 19, 20, 23, 19, 24, 19, 19, 19, 19, 21, 19, 19, 32, 18] + "samples": [18, 19, 19, 19, 19, 19, 19, 19, 19, 21, 21, 19, 19, 196, 19, 19, 20, 23, 19, 24, 19, 19, 19, 19, 21, 19, 19, 32, 18, 19, 19, 19] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -127,9 +127,9 @@ "min": 64, "max": 472, "n": 32, - "samples": [66, 73, 66, 67, 66, 66, 66, 66, 66, 67, 66, 67, 72, 72, 67, 67, 115, 66, 66, 75, 71, 74, 77, 67, 70, 66, 66, 82, 67, 67, 472, 64] + "samples": [67, 66, 66, 66, 66, 66, 67, 66, 67, 72, 72, 67, 67, 115, 66, 66, 75, 71, 74, 77, 67, 70, 66, 66, 82, 67, 67, 472, 64, 66, 66, 66] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -149,9 +149,9 @@ "min": 28, "max": 87, "n": 32, - "samples": [29, 31, 28, 29, 28, 29, 29, 29, 29, 29, 29, 29, 32, 32, 29, 28, 87, 30, 29, 30, 30, 29, 38, 29, 30, 29, 29, 38, 29, 28, 40, 28] + "samples": [29, 28, 29, 29, 29, 29, 29, 29, 29, 32, 32, 29, 28, 87, 30, 29, 30, 30, 29, 38, 29, 30, 29, 29, 38, 29, 28, 40, 28, 29, 29, 29] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -171,9 +171,9 @@ "min": 61, "max": 118, "n": 32, - "samples": [62, 69, 63, 61, 61, 64, 62, 62, 63, 63, 62, 64, 65, 70, 62, 63, 118, 63, 62, 67, 66, 67, 72, 63, 69, 62, 63, 80, 62, 62, 74, 62] + "samples": [61, 61, 64, 62, 62, 63, 63, 62, 64, 65, 70, 62, 63, 118, 63, 62, 67, 66, 67, 72, 63, 69, 62, 63, 80, 62, 62, 74, 62, 64, 63, 63] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -192,9 +192,9 @@ "min": 125, "max": 175, "n": 32, - "samples": [127, 164, 128, 126, 127, 129, 128, 130, 129, 129, 127, 166, 131, 146, 127, 129, 160, 127, 127, 134, 134, 134, 175, 128, 135, 127, 129, 159, 128, 127, 153, 125] + "samples": [126, 127, 129, 128, 130, 129, 129, 127, 166, 131, 146, 127, 129, 160, 127, 127, 134, 134, 134, 175, 128, 135, 127, 129, 159, 128, 127, 153, 125, 127, 128, 127] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } }, "description": "And the height, making it 64x64: four times the cells of the 32x32 the pair started from. Reallocating on each axis separately is the shape a UI resize actually takes." @@ -211,13 +211,13 @@ "desktop-macos": { "tick_us": { "p50": 129, - "p95": 157, + "p95": 155, "min": 127, "max": 157, "n": 32, - "samples": [128, 157, 128, 129, 127, 129, 129, 129, 129, 130, 128, 130, 133, 157, 129, 129, 149, 129, 129, 136, 134, 129, 136, 129, 134, 128, 128, 141, 128, 128, 155, 127] + "samples": [129, 127, 129, 129, 129, 129, 130, 128, 130, 133, 157, 129, 129, 149, 129, 129, 136, 134, 129, 136, 129, 134, 128, 128, 141, 128, 128, 155, 127, 128, 129, 128] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -233,13 +233,13 @@ "desktop-macos": { "tick_us": { "p50": 129, - "p95": 223, + "p95": 184, "min": 125, "max": 472, "n": 32, - "samples": [127, 223, 130, 125, 128, 129, 127, 130, 129, 472, 127, 130, 131, 143, 129, 128, 184, 128, 127, 133, 136, 128, 136, 131, 136, 128, 127, 138, 132, 127, 177, 127] + "samples": [125, 128, 129, 127, 130, 129, 472, 127, 130, 131, 143, 129, 128, 184, 128, 127, 133, 136, 128, 136, 131, 136, 128, 127, 138, 132, 127, 177, 127, 127, 128, 129] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -259,9 +259,9 @@ "min": 34, "max": 47, "n": 32, - "samples": [34, 39, 35, 35, 35, 35, 35, 35, 35, 34, 34, 37, 35, 37, 34, 35, 47, 35, 34, 36, 36, 35, 37, 35, 38, 34, 35, 38, 39, 34, 37, 34] + "samples": [35, 35, 35, 35, 35, 35, 34, 34, 37, 35, 37, 34, 35, 47, 35, 34, 36, 36, 35, 37, 35, 38, 34, 35, 38, 39, 34, 37, 34, 34, 35, 35] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -280,9 +280,9 @@ "min": 10, "max": 26, "n": 32, - "samples": [10, 11, 11, 10, 10, 10, 10, 10, 10, 11, 10, 11, 11, 11, 11, 10, 26, 10, 10, 11, 11, 11, 11, 10, 11, 10, 10, 14, 10, 10, 11, 10] + "samples": [10, 10, 10, 10, 10, 10, 11, 10, 11, 11, 11, 11, 10, 26, 10, 10, 11, 11, 11, 11, 10, 11, 10, 10, 14, 10, 10, 11, 10, 10, 10, 10] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -301,9 +301,9 @@ "min": 214, "max": 335, "n": 32, - "samples": [214, 227, 216, 214, 218, 217, 220, 216, 216, 217, 215, 231, 221, 235, 215, 245, 335, 219, 214, 224, 231, 218, 231, 218, 225, 216, 216, 250, 215, 214, 216, 214] + "samples": [214, 218, 217, 220, 216, 216, 217, 215, 231, 221, 235, 215, 245, 335, 219, 214, 224, 231, 218, 231, 218, 225, 216, 216, 250, 215, 214, 216, 214, 216, 216, 216] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -323,9 +323,9 @@ "min": 10, "max": 12, "n": 32, - "samples": [11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 11, 11, 10, 11, 11, 11, 11, 12, 10, 11, 10, 10, 11, 10, 11, 10, 11] + "samples": [11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 11, 11, 10, 11, 11, 11, 11, 12, 10, 11, 10, 10, 11, 10, 11, 10, 11, 11, 11, 10] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -345,9 +345,9 @@ "min": 8, "max": 9, "n": 32, - "samples": [8, 9, 9, 9, 8, 8, 9, 8, 8, 8, 8, 9, 9, 9, 8, 9, 9, 8, 8, 9, 9, 8, 9, 8, 8, 8, 8, 9, 8, 8, 9, 8] + "samples": [9, 8, 8, 9, 8, 8, 8, 8, 9, 9, 9, 8, 9, 9, 8, 8, 9, 9, 8, 9, 8, 8, 8, 8, 9, 8, 8, 9, 8, 8, 8, 9] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -366,9 +366,9 @@ "min": 6, "max": 8, "n": 32, - "samples": [7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 6, 7, 7, 7, 7, 7, 7, 7, 7, 8, 7, 6, 7, 7] + "samples": [7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 6, 7, 7, 7, 7, 7, 7, 7, 7, 8, 7, 6, 7, 7, 7, 6, 7] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } } diff --git a/test/scenarios/light/scenario_GridBlacks_blackpixel.json b/test/scenarios/light/scenario_GridBlacks_blackpixel.json index 7828398c..44508e4c 100644 --- a/test/scenarios/light/scenario_GridBlacks_blackpixel.json +++ b/test/scenarios/light/scenario_GridBlacks_blackpixel.json @@ -97,7 +97,7 @@ "n": 32, "samples": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { @@ -200,7 +200,7 @@ "n": 32, "samples": [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { diff --git a/test/scenarios/light/scenario_GridLayout_resize.json b/test/scenarios/light/scenario_GridLayout_resize.json index 0add933b..ce0df001 100644 --- a/test/scenarios/light/scenario_GridLayout_resize.json +++ b/test/scenarios/light/scenario_GridLayout_resize.json @@ -122,9 +122,9 @@ "min": 117, "max": 158, "n": 32, - "samples": [117, 126, 127, 117, 120, 121, 120, 118, 123, 124, 120, 124, 126, 132, 120, 142, 128, 119, 118, 124, 118, 119, 129, 121, 126, 117, 119, 158, 122, 120, 117, 118] + "samples": [117, 120, 121, 120, 118, 123, 124, 120, 124, 126, 132, 120, 142, 128, 119, 118, 124, 118, 119, 129, 121, 126, 117, 119, 158, 122, 120, 117, 118, 120, 119, 119] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32-eth-wifi": { "tick_us": { @@ -304,9 +304,9 @@ "min": 59, "max": 86, "n": 32, - "samples": [63, 64, 70, 64, 68, 65, 61, 65, 65, 66, 64, 68, 66, 64, 64, 76, 86, 74, 65, 60, 60, 64, 66, 64, 61, 64, 65, 65, 64, 64, 59, 64] + "samples": [64, 68, 65, 61, 65, 65, 66, 64, 68, 66, 64, 64, 76, 86, 74, 65, 60, 60, 64, 66, 64, 61, 64, 65, 65, 64, 64, 59, 64, 64, 64, 65] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32-eth-wifi": { "tick_us": { @@ -486,9 +486,9 @@ "min": 116, "max": 263, "n": 32, - "samples": [119, 126, 126, 118, 120, 120, 124, 119, 122, 121, 120, 125, 121, 120, 119, 128, 263, 119, 119, 126, 118, 120, 126, 122, 124, 120, 120, 123, 119, 120, 116, 119] + "samples": [118, 120, 120, 124, 119, 122, 121, 120, 125, 121, 120, 119, 128, 263, 119, 119, 126, 118, 120, 126, 122, 124, 120, 120, 123, 119, 120, 116, 119, 120, 119, 119] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32-eth-wifi": { "tick_us": { diff --git a/test/scenarios/light/scenario_Layer_base_pipeline.json b/test/scenarios/light/scenario_Layer_base_pipeline.json index 1d4897f1..6d097ceb 100644 --- a/test/scenarios/light/scenario_Layer_base_pipeline.json +++ b/test/scenarios/light/scenario_Layer_base_pipeline.json @@ -88,9 +88,9 @@ "min": 63, "max": 87, "n": 32, - "samples": [72, 66, 66, 70, 71, 67, 71, 68, 70, 66, 71, 70, 70, 70, 66, 87, 70, 69, 71, 67, 70, 74, 70, 69, 69, 71, 72, 70, 69, 64, 63, 67] + "samples": [70, 71, 67, 71, 68, 70, 66, 71, 70, 70, 70, 66, 87, 70, 69, 71, 67, 70, 74, 70, 69, 69, 71, 72, 70, 69, 64, 63, 67, 71, 68, 69] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { diff --git a/test/scenarios/light/scenario_Layer_memory_1to1.json b/test/scenarios/light/scenario_Layer_memory_1to1.json index b1cc7d06..25cafdb0 100644 --- a/test/scenarios/light/scenario_Layer_memory_1to1.json +++ b/test/scenarios/light/scenario_Layer_memory_1to1.json @@ -85,9 +85,9 @@ "min": 4, "max": 28, "n": 32, - "samples": [5, 7, 5, 4, 5, 5, 5, 5, 5, 6, 5, 5, 5, 6, 5, 24, 8, 5, 5, 5, 28, 5, 5, 5, 5, 5, 4, 5, 5, 5, 5, 5] + "samples": [4, 5, 5, 5, 5, 5, 6, 5, 5, 5, 6, 5, 24, 8, 5, 5, 5, 28, 5, 5, 5, 5, 5, 4, 5, 5, 5, 5, 5, 5, 5, 5] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { diff --git a/test/scenarios/light/scenario_Layouts_mutation.json b/test/scenarios/light/scenario_Layouts_mutation.json index 0a1dd627..cff36acb 100644 --- a/test/scenarios/light/scenario_Layouts_mutation.json +++ b/test/scenarios/light/scenario_Layouts_mutation.json @@ -83,9 +83,9 @@ "min": 16, "max": 45, "n": 32, - "samples": [17, 16, 16, 17, 17, 17, 18, 16, 16, 16, 17, 19, 17, 16, 45, 37, 16, 16, 17, 20, 16, 17, 16, 18, 16, 16, 17, 16, 16, 17, 16, 16] + "samples": [17, 17, 17, 18, 16, 16, 16, 17, 19, 17, 16, 45, 37, 16, 16, 17, 20, 16, 17, 16, 18, 16, 16, 17, 16, 16, 17, 16, 16, 16, 16, 16] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { @@ -206,14 +206,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 50, + "p50": 49, "p95": 69, "min": 44, "max": 77, "n": 32, - "samples": [53, 46, 50, 50, 50, 58, 51, 49, 47, 48, 49, 53, 56, 49, 69, 52, 49, 47, 51, 77, 50, 49, 50, 50, 47, 50, 51, 49, 50, 44, 49, 46] + "samples": [50, 50, 58, 51, 49, 47, 48, 49, 53, 56, 49, 69, 52, 49, 47, 51, 77, 50, 49, 50, 50, 47, 50, 51, 49, 50, 44, 49, 46, 49, 46, 49] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { @@ -334,9 +334,9 @@ "min": 90, "max": 104, "n": 32, - "samples": [94, 94, 93, 94, 94, 92, 97, 91, 93, 92, 93, 95, 99, 93, 104, 99, 92, 92, 90, 90, 93, 100, 93, 93, 92, 93, 96, 93, 93, 91, 92, 93] + "samples": [94, 94, 92, 97, 91, 93, 92, 93, 95, 99, 93, 104, 99, 92, 92, 90, 90, 93, 100, 93, 93, 92, 93, 96, 93, 93, 91, 92, 93, 92, 93, 94] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { @@ -456,9 +456,9 @@ "min": 16, "max": 23, "n": 32, - "samples": [21, 20, 19, 20, 19, 19, 21, 20, 20, 19, 20, 20, 17, 20, 17, 20, 19, 20, 23, 16, 19, 17, 20, 16, 21, 20, 20, 20, 20, 17, 16, 18] + "samples": [20, 19, 19, 21, 20, 20, 19, 20, 20, 17, 20, 17, 20, 19, 20, 23, 16, 19, 17, 20, 16, 21, 20, 20, 20, 20, 17, 16, 18, 19, 20, 20] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { diff --git a/test/scenarios/light/scenario_MoonLiveEffect_livescript.json b/test/scenarios/light/scenario_MoonLiveEffect_livescript.json index f292f8d2..028ff537 100644 --- a/test/scenarios/light/scenario_MoonLiveEffect_livescript.json +++ b/test/scenarios/light/scenario_MoonLiveEffect_livescript.json @@ -93,9 +93,9 @@ "min": 5, "max": 6, "n": 32, - "samples": [5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5, 5, 6, 5, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5, 5, 5, 6, 5] + "samples": [5, 5, 5, 5, 5, 5, 6, 5, 5, 6, 5, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5, 5, 5, 6, 5, 5, 6, 5] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { @@ -211,9 +211,9 @@ "min": 4, "max": 6, "n": 32, - "samples": [5, 5, 5, 5, 5, 5, 5, 5, 4, 5, 5, 5, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5, 5, 4, 5, 6, 5] + "samples": [5, 5, 5, 5, 5, 4, 5, 5, 5, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5, 5, 4, 5, 6, 5, 5, 5, 5] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { @@ -321,9 +321,9 @@ "min": 4, "max": 6, "n": 32, - "samples": [5, 5, 4, 5, 5, 5, 5, 5, 4, 5, 6, 6, 6, 5, 5, 5, 5, 5, 5, 4, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5] + "samples": [5, 5, 5, 5, 5, 4, 5, 6, 6, 6, 5, 5, 5, 5, 5, 5, 4, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5, 5, 5, 5] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { @@ -428,12 +428,12 @@ "tick_us": { "p50": 5, "p95": 6, - "min": 4, + "min": 5, "max": 7, "n": 32, - "samples": [5, 5, 4, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 5, 5, 6, 5, 5, 5, 7, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5] + "samples": [5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 5, 5, 6, 5, 5, 5, 7, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5, 5, 5, 5] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { @@ -534,9 +534,9 @@ "min": 4, "max": 6, "n": 32, - "samples": [5, 5, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5] + "samples": [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5, 4, 4, 5] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { @@ -637,9 +637,9 @@ "min": 4, "max": 10, "n": 32, - "samples": [5, 5, 5, 5, 5, 5, 5, 6, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 10, 4, 6, 7, 7, 5, 5, 5, 5, 5, 5, 5, 5, 5] + "samples": [5, 5, 5, 5, 6, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 10, 4, 6, 7, 7, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 5] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { @@ -740,9 +740,9 @@ "min": 4, "max": 6, "n": 32, - "samples": [5, 5, 5, 5, 5, 5, 5, 5, 4, 5, 5, 5, 5, 5, 5, 6, 5, 5, 6, 4, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5] + "samples": [5, 5, 5, 5, 5, 4, 5, 5, 5, 5, 5, 5, 6, 5, 5, 6, 4, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5, 5, 5, 5] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -762,9 +762,9 @@ "min": 4, "max": 6, "n": 32, - "samples": [6, 5, 5, 5, 5, 5, 5, 5, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5] + "samples": [5, 5, 5, 5, 5, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5, 6, 5, 5] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -784,9 +784,9 @@ "min": 4, "max": 8, "n": 32, - "samples": [6, 5, 5, 5, 5, 5, 5, 5, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5, 5, 5, 5, 6, 5, 5, 6, 5, 5, 5, 8, 5] + "samples": [5, 5, 5, 5, 5, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5, 5, 5, 5, 6, 5, 5, 6, 5, 5, 5, 8, 5, 5, 5, 6] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -804,9 +804,9 @@ "min": 5, "max": 6, "n": 32, - "samples": [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5, 5, 5, 6, 5, 5, 5, 5, 5, 5, 5, 6, 5, 5, 5, 6, 5] + "samples": [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5, 5, 5, 6, 5, 5, 5, 5, 5, 5, 5, 6, 5, 5, 5, 6, 5, 5, 5, 5] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { @@ -907,9 +907,9 @@ "min": 4, "max": 9, "n": 32, - "samples": [6, 5, 5, 5, 5, 5, 5, 5, 4, 5, 4, 7, 5, 5, 5, 5, 5, 5, 5, 9, 5, 5, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5] + "samples": [5, 5, 5, 5, 5, 4, 5, 4, 7, 5, 5, 5, 5, 5, 5, 5, 9, 5, 5, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { @@ -1007,12 +1007,12 @@ "tick_us": { "p50": 5, "p95": 6, - "min": 5, + "min": 4, "max": 6, "n": 32, - "samples": [5, 5, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5, 5, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5] + "samples": [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5, 5, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 5] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32": { "tick_us": { diff --git a/test/scenarios/light/scenario_MoonLive_pipeline.json b/test/scenarios/light/scenario_MoonLive_pipeline.json index 020c175c..ebf57196 100644 --- a/test/scenarios/light/scenario_MoonLive_pipeline.json +++ b/test/scenarios/light/scenario_MoonLive_pipeline.json @@ -379,9 +379,9 @@ "min": 4, "max": 9, "n": 32, - "samples": [5, 5, 5, 5, 5, 5, 5, 4, 5, 5, 5, 5, 5, 5, 5, 9, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 5, 5, 5, 5, 5, 5] + "samples": [5, 5, 5, 5, 4, 5, 5, 5, 5, 5, 5, 5, 9, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 5, 5, 5, 5, 5, 5, 5, 4, 5] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { @@ -536,9 +536,9 @@ "min": 4, "max": 11, "n": 32, - "samples": [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 11, 5, 5, 5, 4, 6, 5, 5, 5, 5, 5, 5, 5, 4, 5, 5, 5] + "samples": [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 11, 5, 5, 5, 4, 6, 5, 5, 5, 5, 5, 5, 5, 4, 5, 5, 5, 6, 5, 5] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { @@ -687,9 +687,9 @@ "min": 4, "max": 6, "n": 32, - "samples": [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 5, 5, 5, 5, 5, 6, 6, 5, 5, 5, 4, 5, 5, 5, 5] + "samples": [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 5, 5, 5, 5, 5, 6, 6, 5, 5, 5, 4, 5, 5, 5, 5, 5, 5, 5] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { @@ -985,13 +985,13 @@ "desktop-macos": { "tick_us": { "p50": 5, - "p95": 9, + "p95": 6, "min": 5, "max": 10, "n": 32, - "samples": [5, 9, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 10, 5, 5, 5, 5, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5] + "samples": [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 10, 5, 5, 5, 5, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5, 5, 5, 5] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { @@ -1127,13 +1127,13 @@ "desktop-macos": { "tick_us": { "p50": 5, - "p95": 5, + "p95": 6, "min": 4, "max": 7, "n": 32, - "samples": [5, 5, 5, 5, 5, 5, 5, 4, 5, 5, 5, 4, 5, 5, 5, 7, 5, 5, 5, 5, 5, 5, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5] + "samples": [5, 5, 5, 5, 4, 5, 5, 5, 4, 5, 5, 5, 7, 5, 5, 5, 5, 5, 5, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 5, 5] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { diff --git a/test/scenarios/light/scenario_MultiplyModifier_memory_lut.json b/test/scenarios/light/scenario_MultiplyModifier_memory_lut.json index 0a6df222..f1687cdc 100644 --- a/test/scenarios/light/scenario_MultiplyModifier_memory_lut.json +++ b/test/scenarios/light/scenario_MultiplyModifier_memory_lut.json @@ -90,13 +90,13 @@ "desktop-macos": { "tick_us": { "p50": 3, - "p95": 4, + "p95": 3, "min": 2, "max": 4, "n": 32, - "samples": [3, 4, 3, 3, 3, 4, 3, 3, 3, 3, 3, 2, 3, 3, 3, 3, 3, 2, 2, 3, 3, 3, 3, 2, 3, 3, 3, 3, 2, 3, 3, 2] + "samples": [3, 3, 4, 3, 3, 3, 3, 3, 2, 3, 3, 3, 3, 3, 2, 2, 3, 3, 3, 3, 2, 3, 3, 3, 3, 2, 3, 3, 2, 3, 3, 3] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { diff --git a/test/scenarios/light/scenario_MultiplyModifier_pipeline.json b/test/scenarios/light/scenario_MultiplyModifier_pipeline.json index beea7251..8f75331b 100644 --- a/test/scenarios/light/scenario_MultiplyModifier_pipeline.json +++ b/test/scenarios/light/scenario_MultiplyModifier_pipeline.json @@ -94,9 +94,9 @@ "min": 117, "max": 158, "n": 32, - "samples": [118, 123, 117, 118, 117, 120, 119, 121, 119, 119, 118, 118, 119, 121, 118, 158, 131, 117, 117, 120, 123, 121, 142, 118, 127, 118, 118, 122, 122, 117, 117, 117] + "samples": [118, 117, 120, 119, 121, 119, 119, 118, 118, 119, 121, 118, 158, 131, 117, 117, 120, 123, 121, 142, 118, 127, 118, 118, 122, 122, 117, 117, 117, 119, 117, 118] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { diff --git a/test/scenarios/light/scenario_Trails_ladder.json b/test/scenarios/light/scenario_Trails_ladder.json index 9647ba52..671f9f20 100644 --- a/test/scenarios/light/scenario_Trails_ladder.json +++ b/test/scenarios/light/scenario_Trails_ladder.json @@ -91,9 +91,9 @@ "min": 11, "max": 13, "n": 32, - "samples": [11, 11, 11, 11, 11, 12, 11, 11, 11, 11, 11, 11, 11, 13, 12, 12, 11, 11, 11, 11, 13, 11, 11, 11, 12, 11, 11, 12, 11, 11, 11, 11] + "samples": [11, 11, 12, 11, 11, 11, 11, 11, 11, 11, 13, 12, 12, 11, 11, 11, 11, 13, 11, 11, 11, 12, 11, 11, 12, 11, 11, 11, 11, 11, 11, 11] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -128,9 +128,9 @@ "min": 44, "max": 55, "n": 32, - "samples": [45, 45, 45, 44, 44, 46, 45, 44, 46, 47, 44, 45, 46, 47, 47, 46, 47, 45, 44, 46, 47, 44, 46, 44, 46, 45, 45, 55, 46, 44, 45, 44] + "samples": [44, 44, 46, 45, 44, 46, 47, 44, 45, 46, 47, 47, 46, 47, 45, 44, 46, 47, 44, 46, 44, 46, 45, 45, 55, 46, 44, 45, 44, 44, 45, 45] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -165,9 +165,9 @@ "min": 171, "max": 274, "n": 32, - "samples": [177, 182, 180, 177, 175, 178, 177, 179, 178, 178, 176, 177, 189, 189, 187, 180, 184, 176, 176, 185, 191, 178, 189, 178, 191, 176, 176, 274, 188, 178, 198, 171] + "samples": [177, 175, 178, 177, 179, 178, 178, 176, 177, 189, 189, 187, 180, 184, 176, 176, 185, 191, 178, 189, 178, 191, 176, 176, 274, 188, 178, 198, 171, 176, 176, 178] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -204,14 +204,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 348, + "p50": 347, "p95": 421, "min": 335, "max": 466, "n": 32, - "samples": [345, 355, 348, 345, 346, 356, 347, 347, 348, 347, 466, 347, 351, 358, 348, 399, 362, 348, 346, 358, 365, 346, 361, 345, 359, 346, 347, 421, 355, 346, 362, 335] + "samples": [345, 346, 356, 347, 347, 348, 347, 466, 347, 351, 358, 348, 399, 362, 348, 346, 358, 365, 346, 361, 345, 359, 346, 347, 421, 355, 346, 362, 335, 347, 347, 347] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -234,14 +234,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 359, + "p50": 358, "p95": 658, "min": 348, "max": 742, "n": 32, - "samples": [351, 367, 359, 358, 352, 359, 353, 354, 356, 356, 352, 352, 359, 523, 376, 365, 368, 658, 362, 367, 371, 354, 370, 354, 369, 355, 358, 488, 359, 354, 742, 348] + "samples": [358, 352, 359, 353, 354, 356, 356, 352, 352, 359, 523, 376, 365, 368, 658, 362, 367, 371, 354, 370, 354, 369, 355, 358, 488, 359, 354, 742, 348, 355, 355, 357] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -269,9 +269,9 @@ "min": 345, "max": 426, "n": 32, - "samples": [345, 359, 347, 352, 347, 350, 346, 348, 400, 349, 346, 346, 351, 426, 347, 357, 349, 350, 354, 364, 360, 346, 358, 346, 370, 351, 349, 393, 352, 345, 349, 345] + "samples": [352, 347, 350, 346, 348, 400, 349, 346, 346, 351, 426, 347, 357, 349, 350, 354, 364, 360, 346, 358, 346, 370, 351, 349, 393, 352, 345, 349, 345, 347, 347, 360] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } }, @@ -309,13 +309,13 @@ "desktop-macos": { "tick_us": { "p50": 177, - "p95": 187, + "p95": 186, "min": 176, "max": 268, "n": 32, - "samples": [176, 187, 177, 178, 177, 177, 177, 179, 268, 178, 176, 176, 179, 186, 176, 178, 177, 182, 176, 184, 179, 177, 183, 177, 185, 176, 177, 177, 178, 176, 176, 176] + "samples": [178, 177, 177, 177, 179, 268, 178, 176, 176, 179, 186, 176, 178, 177, 182, 176, 184, 179, 177, 183, 177, 185, 176, 177, 177, 178, 176, 176, 176, 177, 176, 184] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" } } } diff --git a/test/scenarios/light/scenario_modifier_chain.json b/test/scenarios/light/scenario_modifier_chain.json index 5b67b99f..4fd587ad 100644 --- a/test/scenarios/light/scenario_modifier_chain.json +++ b/test/scenarios/light/scenario_modifier_chain.json @@ -107,9 +107,9 @@ "min": 8, "max": 27, "n": 32, - "samples": [8, 10, 10, 8, 8, 10, 10, 9, 10, 8, 10, 9, 11, 9, 10, 10, 10, 9, 10, 8, 10, 27, 9, 10, 10, 8, 10, 10, 10, 8, 8, 10] + "samples": [8, 8, 10, 10, 9, 10, 8, 10, 9, 11, 9, 10, 10, 10, 9, 10, 8, 10, 27, 9, 10, 10, 8, 10, 10, 10, 8, 8, 10, 10, 10, 10] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { @@ -167,9 +167,9 @@ "min": 6, "max": 11, "n": 32, - "samples": [7, 9, 8, 9, 7, 9, 7, 9, 9, 6, 9, 9, 9, 9, 9, 9, 9, 8, 9, 7, 9, 11, 9, 9, 9, 9, 8, 9, 10, 7, 9, 8] + "samples": [9, 7, 9, 7, 9, 9, 6, 9, 9, 9, 9, 9, 9, 9, 8, 9, 7, 9, 11, 9, 9, 9, 9, 8, 9, 10, 7, 9, 8, 9, 9, 10] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { @@ -225,9 +225,9 @@ "min": 21, "max": 25, "n": 32, - "samples": [24, 25, 22, 24, 22, 24, 21, 25, 25, 21, 24, 25, 24, 24, 24, 24, 24, 24, 24, 21, 24, 24, 25, 25, 25, 24, 24, 24, 25, 21, 23, 24] + "samples": [24, 22, 24, 21, 25, 25, 21, 24, 25, 24, 24, 24, 24, 24, 24, 24, 21, 24, 24, 25, 25, 25, 24, 24, 24, 25, 21, 23, 24, 24, 24, 24] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { @@ -253,14 +253,14 @@ "observed": { "desktop-macos": { "tick_us": { - "p50": 44, + "p50": 43, "p95": 46, "min": 35, "max": 47, "n": 32, - "samples": [46, 44, 45, 44, 38, 44, 47, 42, 43, 45, 43, 45, 44, 43, 43, 43, 42, 44, 45, 37, 44, 44, 41, 43, 43, 44, 39, 45, 45, 36, 35, 46] + "samples": [44, 38, 44, 47, 42, 43, 45, 43, 45, 44, 43, 43, 43, 42, 44, 45, 37, 44, 44, 41, 43, 43, 44, 39, 45, 45, 36, 35, 46, 44, 42, 44] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { diff --git a/test/scenarios/light/scenario_modifier_swap.json b/test/scenarios/light/scenario_modifier_swap.json index 089f1770..3e0e42c4 100644 --- a/test/scenarios/light/scenario_modifier_swap.json +++ b/test/scenarios/light/scenario_modifier_swap.json @@ -156,9 +156,9 @@ "min": 8, "max": 11, "n": 32, - "samples": [8, 8, 9, 8, 9, 8, 8, 11, 8, 8, 8, 10, 8, 8, 10, 8, 8, 8, 10, 8, 8, 8, 8, 8, 8, 8, 8, 10, 11, 8, 8, 9] + "samples": [8, 9, 8, 8, 11, 8, 8, 8, 10, 8, 8, 10, 8, 8, 8, 10, 8, 8, 8, 8, 8, 8, 8, 8, 10, 11, 8, 8, 9, 8, 8, 8] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32-eth": { "tick_us": { @@ -300,9 +300,9 @@ "min": 20, "max": 53, "n": 32, - "samples": [21, 23, 23, 23, 24, 21, 21, 24, 21, 21, 24, 24, 25, 24, 24, 24, 21, 23, 25, 20, 23, 22, 22, 23, 24, 21, 53, 24, 25, 21, 21, 24] + "samples": [23, 24, 21, 21, 24, 21, 21, 24, 24, 25, 24, 24, 24, 21, 23, 25, 20, 23, 22, 22, 23, 24, 21, 53, 24, 25, 21, 21, 24, 24, 22, 20] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32-eth": { "tick_us": { @@ -444,9 +444,9 @@ "min": 8, "max": 16, "n": 32, - "samples": [11, 10, 10, 10, 10, 10, 8, 8, 10, 10, 10, 10, 11, 10, 10, 10, 8, 10, 10, 9, 10, 11, 10, 11, 10, 10, 16, 10, 10, 8, 11, 10] + "samples": [10, 10, 10, 8, 8, 10, 10, 10, 10, 11, 10, 10, 10, 8, 10, 10, 9, 10, 11, 10, 11, 10, 10, 16, 10, 10, 8, 11, 10, 10, 10, 10] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32-eth": { "tick_us": { diff --git a/test/scenarios/light/scenario_perf_full.json b/test/scenarios/light/scenario_perf_full.json index 0892d06a..d17aa7ee 100644 --- a/test/scenarios/light/scenario_perf_full.json +++ b/test/scenarios/light/scenario_perf_full.json @@ -90,9 +90,9 @@ "min": 1, "max": 3, "n": 32, - "samples": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1] + "samples": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { @@ -210,9 +210,9 @@ "min": 1, "max": 3, "n": 32, - "samples": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1] + "samples": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { @@ -330,9 +330,9 @@ "min": 1, "max": 3, "n": 32, - "samples": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1] + "samples": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { @@ -573,9 +573,9 @@ "min": 1, "max": 3, "n": 32, - "samples": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1] + "samples": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { @@ -693,7 +693,7 @@ "n": 32, "samples": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { @@ -822,7 +822,7 @@ "n": 32, "samples": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { @@ -955,7 +955,7 @@ "n": 32, "samples": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { @@ -1062,7 +1062,7 @@ "n": 32, "samples": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32p4rev1-eth": { "tick_us": { @@ -1175,7 +1175,7 @@ "n": 32, "samples": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { @@ -1297,9 +1297,9 @@ "min": 4, "max": 6, "n": 32, - "samples": [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 6, 4, 4, 4, 4] + "samples": [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 6, 4, 4, 4, 4, 4, 4, 4] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { @@ -1421,9 +1421,9 @@ "min": 17, "max": 79, "n": 32, - "samples": [17, 18, 17, 18, 18, 79, 17, 18, 18, 19, 18, 18, 19, 18, 17, 18, 19, 17, 18, 17, 18, 18, 19, 19, 17, 18, 18, 18, 18, 18, 17, 17] + "samples": [18, 18, 79, 17, 18, 18, 19, 18, 18, 19, 18, 17, 18, 19, 17, 18, 17, 18, 18, 19, 19, 17, 18, 18, 18, 18, 18, 17, 17, 17, 17, 18] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { @@ -1545,9 +1545,9 @@ "min": 70, "max": 80, "n": 32, - "samples": [70, 71, 71, 71, 70, 80, 70, 72, 72, 77, 71, 72, 72, 75, 70, 72, 73, 70, 71, 70, 75, 71, 78, 76, 70, 73, 71, 78, 76, 73, 70, 72] + "samples": [71, 70, 80, 70, 72, 72, 77, 71, 72, 72, 75, 70, 72, 73, 70, 71, 70, 75, 71, 78, 76, 70, 73, 71, 78, 76, 73, 70, 72, 70, 70, 70] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { @@ -1677,9 +1677,9 @@ "min": 4, "max": 5, "n": 32, - "samples": [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 4, 4, 4, 4] + "samples": [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 4, 4, 4, 4, 4, 4, 4] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { @@ -1801,9 +1801,9 @@ "min": 15, "max": 18, "n": 32, - "samples": [15, 16, 15, 16, 15, 16, 15, 15, 17, 18, 15, 15, 16, 17, 15, 16, 16, 15, 16, 15, 17, 15, 18, 16, 16, 16, 16, 17, 15, 16, 15, 16] + "samples": [16, 15, 16, 15, 15, 17, 18, 15, 15, 16, 17, 15, 16, 16, 15, 16, 15, 17, 15, 18, 16, 16, 16, 16, 17, 15, 16, 15, 16, 16, 15, 16] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { @@ -1925,9 +1925,9 @@ "min": 60, "max": 68, "n": 32, - "samples": [61, 62, 62, 63, 63, 63, 62, 62, 67, 67, 64, 62, 65, 67, 62, 64, 64, 63, 63, 62, 67, 62, 68, 67, 64, 65, 63, 68, 63, 65, 61, 60] + "samples": [63, 63, 63, 62, 62, 67, 67, 64, 62, 65, 67, 62, 64, 64, 63, 63, 62, 67, 62, 68, 67, 64, 65, 63, 68, 63, 65, 61, 60, 63, 62, 62] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { @@ -2049,9 +2049,9 @@ "min": 243, "max": 297, "n": 32, - "samples": [243, 252, 250, 252, 251, 251, 250, 252, 265, 297, 250, 250, 266, 295, 249, 257, 261, 251, 251, 251, 265, 251, 269, 272, 252, 262, 251, 263, 248, 265, 251, 243] + "samples": [252, 251, 251, 250, 252, 265, 297, 250, 250, 266, 295, 249, 257, 261, 251, 251, 251, 265, 251, 269, 272, 252, 262, 251, 263, 248, 265, 251, 243, 250, 250, 249] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { @@ -2210,7 +2210,7 @@ "n": 32, "samples": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32": { "tick_us": { @@ -2334,7 +2334,7 @@ "n": 32, "samples": [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32": { "tick_us": { @@ -2456,9 +2456,9 @@ "min": 15, "max": 22, "n": 32, - "samples": [15, 16, 15, 16, 17, 15, 15, 15, 17, 18, 15, 15, 16, 17, 15, 16, 16, 15, 15, 15, 16, 15, 16, 22, 17, 17, 15, 15, 15, 16, 17, 15] + "samples": [16, 17, 15, 15, 15, 17, 18, 15, 15, 16, 17, 15, 16, 16, 15, 15, 15, 16, 15, 16, 22, 17, 17, 15, 15, 15, 16, 17, 15, 15, 15, 15] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32": { "tick_us": { @@ -2580,9 +2580,9 @@ "min": 60, "max": 82, "n": 32, - "samples": [61, 62, 62, 63, 62, 62, 62, 63, 68, 71, 62, 62, 82, 75, 62, 64, 66, 62, 63, 62, 65, 64, 63, 70, 63, 65, 62, 62, 62, 65, 64, 60] + "samples": [63, 62, 62, 62, 63, 68, 71, 62, 62, 82, 75, 62, 64, 66, 62, 63, 62, 65, 64, 63, 70, 63, 65, 62, 62, 62, 65, 64, 60, 62, 62, 63] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32": { "tick_us": { diff --git a/test/scenarios/light/scenario_perf_light.json b/test/scenarios/light/scenario_perf_light.json index 452c9bd2..b43518cc 100644 --- a/test/scenarios/light/scenario_perf_light.json +++ b/test/scenarios/light/scenario_perf_light.json @@ -108,7 +108,7 @@ "n": 32, "samples": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { @@ -456,7 +456,7 @@ "n": 32, "samples": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { @@ -578,9 +578,9 @@ "min": 4, "max": 6, "n": 32, - "samples": [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 6, 4, 4, 4] + "samples": [4, 4, 4, 4, 4, 4, 4, 4, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 6, 4, 4, 4, 4, 4, 4] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { @@ -702,9 +702,9 @@ "min": 15, "max": 27, "n": 32, - "samples": [16, 15, 15, 15, 15, 15, 15, 15, 15, 16, 17, 27, 15, 15, 16, 18, 15, 16, 16, 15, 16, 15, 16, 17, 16, 16, 15, 15, 21, 17, 16, 15] + "samples": [15, 15, 15, 15, 15, 15, 16, 17, 27, 15, 15, 16, 18, 15, 16, 16, 15, 16, 15, 16, 17, 16, 16, 15, 15, 21, 17, 16, 15, 15, 15, 15] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32s3-n16r8": { "tick_us": { diff --git a/test/scenarios/light/scenario_peripheral_grid_sweep.json b/test/scenarios/light/scenario_peripheral_grid_sweep.json index 26b6a621..20766163 100644 --- a/test/scenarios/light/scenario_peripheral_grid_sweep.json +++ b/test/scenarios/light/scenario_peripheral_grid_sweep.json @@ -178,9 +178,9 @@ "min": 4, "max": 15, "n": 32, - "samples": [4, 4, 4, 4, 4, 4, 15, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] + "samples": [4, 4, 4, 15, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { @@ -305,9 +305,9 @@ "min": 15, "max": 122, "n": 32, - "samples": [16, 16, 16, 15, 15, 16, 122, 17, 15, 16, 15, 18, 16, 16, 16, 16, 16, 16, 16, 17, 16, 19, 17, 16, 17, 16, 16, 16, 16, 16, 16, 15] + "samples": [15, 15, 16, 122, 17, 15, 16, 15, 18, 16, 16, 16, 16, 16, 16, 16, 17, 16, 19, 17, 16, 17, 16, 16, 16, 16, 16, 16, 15, 16, 16, 16] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { @@ -432,9 +432,9 @@ "min": 61, "max": 72, "n": 32, - "samples": [62, 62, 63, 63, 62, 64, 71, 69, 63, 62, 64, 67, 62, 64, 67, 63, 63, 63, 64, 70, 68, 72, 69, 66, 67, 63, 65, 63, 62, 69, 65, 61] + "samples": [63, 62, 64, 71, 69, 63, 62, 64, 67, 62, 64, 67, 63, 63, 63, 64, 70, 68, 72, 69, 66, 67, 63, 65, 63, 62, 69, 65, 61, 62, 69, 63] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { @@ -559,9 +559,9 @@ "min": 242, "max": 325, "n": 32, - "samples": [249, 251, 251, 252, 250, 249, 286, 278, 249, 250, 252, 253, 249, 256, 268, 250, 254, 250, 263, 252, 273, 291, 275, 325, 317, 249, 260, 248, 249, 260, 260, 242] + "samples": [252, 250, 249, 286, 278, 249, 250, 252, 253, 249, 256, 268, 250, 254, 250, 263, 252, 273, 291, 275, 325, 317, 249, 260, 248, 249, 260, 260, 242, 251, 251, 250] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { @@ -707,9 +707,9 @@ "min": 4, "max": 15, "n": 32, - "samples": [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 15, 4, 4, 4, 4, 4, 4, 4, 4] + "samples": [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 15, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { @@ -834,9 +834,9 @@ "min": 15, "max": 19, "n": 32, - "samples": [15, 15, 15, 15, 15, 17, 17, 17, 15, 15, 16, 15, 15, 16, 16, 15, 16, 15, 16, 16, 16, 18, 17, 19, 16, 16, 15, 15, 15, 17, 16, 15] + "samples": [15, 15, 17, 17, 17, 15, 15, 16, 15, 15, 16, 16, 15, 16, 15, 16, 16, 16, 18, 17, 19, 16, 16, 15, 15, 15, 17, 16, 15, 15, 16, 15] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { @@ -961,9 +961,9 @@ "min": 60, "max": 81, "n": 32, - "samples": [63, 63, 63, 62, 62, 62, 67, 67, 62, 62, 62, 63, 62, 64, 67, 63, 63, 62, 68, 64, 66, 73, 67, 81, 68, 62, 62, 61, 63, 65, 63, 60] + "samples": [62, 62, 62, 67, 67, 62, 62, 62, 63, 62, 64, 67, 63, 63, 62, 68, 64, 66, 73, 67, 81, 68, 62, 62, 61, 63, 65, 63, 60, 62, 63, 62] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { @@ -1088,9 +1088,9 @@ "min": 243, "max": 286, "n": 32, - "samples": [256, 252, 252, 252, 249, 249, 268, 267, 252, 251, 252, 251, 248, 259, 269, 251, 252, 250, 263, 250, 277, 285, 270, 286, 264, 249, 253, 249, 253, 255, 250, 243] + "samples": [252, 249, 249, 268, 267, 252, 251, 252, 251, 248, 259, 269, 251, 252, 250, 263, 250, 277, 285, 270, 286, 264, 249, 253, 249, 253, 255, 250, 243, 250, 251, 250] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { @@ -1238,7 +1238,7 @@ "n": 32, "samples": [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { @@ -1363,9 +1363,9 @@ "min": 15, "max": 17, "n": 32, - "samples": [16, 15, 15, 16, 15, 16, 17, 16, 15, 15, 16, 15, 16, 16, 16, 15, 15, 15, 16, 15, 17, 17, 16, 17, 16, 16, 16, 15, 15, 16, 15, 15] + "samples": [16, 15, 16, 17, 16, 15, 15, 16, 15, 16, 16, 16, 15, 15, 15, 16, 15, 17, 17, 16, 17, 16, 16, 16, 15, 15, 16, 15, 15, 15, 15, 15] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { @@ -1490,9 +1490,9 @@ "min": 60, "max": 70, "n": 32, - "samples": [62, 62, 63, 62, 64, 62, 66, 65, 63, 62, 63, 62, 62, 63, 66, 62, 63, 62, 67, 62, 69, 68, 65, 70, 65, 62, 62, 61, 62, 64, 61, 60] + "samples": [62, 64, 62, 66, 65, 63, 62, 63, 62, 62, 63, 66, 62, 63, 62, 67, 62, 69, 68, 65, 70, 65, 62, 62, 61, 62, 64, 61, 60, 62, 65, 62] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { @@ -1612,14 +1612,14 @@ }, "desktop-macos": { "tick_us": { - "p50": 252, + "p50": 251, "p95": 278, "min": 245, "max": 395, "n": 32, - "samples": [251, 252, 253, 251, 249, 249, 254, 263, 251, 250, 251, 267, 253, 261, 265, 250, 254, 250, 265, 252, 275, 278, 255, 258, 395, 250, 248, 246, 250, 256, 245, 251] + "samples": [251, 249, 249, 254, 263, 251, 250, 251, 267, 253, 261, 265, 250, 254, 250, 265, 252, 275, 278, 255, 258, 395, 250, 248, 246, 250, 256, 245, 251, 250, 250, 249] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { @@ -1765,9 +1765,9 @@ "min": 4, "max": 10, "n": 32, - "samples": [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 6, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 10] + "samples": [4, 4, 4, 4, 4, 4, 4, 4, 6, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 10, 4, 4, 4] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { @@ -1887,14 +1887,14 @@ }, "desktop-macos": { "tick_us": { - "p50": 16, + "p50": 15, "p95": 17, "min": 15, "max": 32, "n": 32, - "samples": [16, 15, 15, 15, 15, 15, 16, 16, 15, 15, 15, 17, 15, 17, 16, 16, 15, 16, 16, 15, 17, 17, 16, 16, 17, 15, 15, 16, 15, 16, 15, 32] + "samples": [15, 15, 15, 16, 16, 15, 15, 15, 17, 15, 17, 16, 16, 15, 16, 16, 15, 17, 17, 16, 16, 17, 15, 15, 16, 15, 16, 15, 32, 15, 15, 15] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { @@ -2014,14 +2014,14 @@ }, "desktop-macos": { "tick_us": { - "p50": 62, - "p95": 72, + "p50": 63, + "p95": 77, "min": 60, "max": 114, "n": 32, - "samples": [62, 62, 62, 62, 62, 62, 63, 68, 62, 64, 62, 68, 62, 65, 65, 62, 63, 62, 64, 62, 69, 67, 63, 66, 72, 61, 62, 63, 62, 64, 60, 114] + "samples": [62, 62, 62, 63, 68, 62, 64, 62, 68, 62, 65, 65, 62, 63, 62, 64, 62, 69, 67, 63, 66, 72, 61, 62, 63, 62, 64, 60, 114, 62, 62, 77] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { @@ -2142,13 +2142,13 @@ "desktop-macos": { "tick_us": { "p50": 254, - "p95": 309, + "p95": 324, "min": 243, "max": 349, "n": 32, - "samples": [249, 251, 258, 252, 247, 250, 264, 260, 251, 349, 250, 268, 248, 257, 266, 250, 254, 248, 262, 246, 273, 264, 254, 263, 284, 258, 248, 254, 249, 260, 243, 309] + "samples": [252, 247, 250, 264, 260, 251, 349, 250, 268, 248, 257, 266, 250, 254, 248, 262, 246, 273, 264, 254, 263, 284, 258, 248, 254, 249, 260, 243, 309, 250, 249, 324] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "desktop-windows": { "tick_us": { diff --git a/test/scenarios/light/scenario_peripheral_switch.json b/test/scenarios/light/scenario_peripheral_switch.json index a3780c5d..416a5aa7 100644 --- a/test/scenarios/light/scenario_peripheral_switch.json +++ b/test/scenarios/light/scenario_peripheral_switch.json @@ -179,7 +179,7 @@ "n": 32, "samples": [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32p4rev1-eth": { "tick_us": { @@ -300,7 +300,7 @@ "n": 32, "samples": [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32p4rev1-eth": { "tick_us": { @@ -421,7 +421,7 @@ "n": 32, "samples": [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32p4rev1-eth": { "tick_us": { @@ -541,7 +541,7 @@ "n": 32, "samples": [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32p4rev1-eth": { "tick_us": { @@ -660,9 +660,9 @@ "min": 4, "max": 5, "n": 32, - "samples": [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 4, 4, 4, 4, 4, 4] + "samples": [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32p4rev1-eth": { "tick_us": { @@ -797,9 +797,9 @@ "min": 4, "max": 5, "n": 32, - "samples": [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 4, 4, 4, 4, 4, 4, 4] + "samples": [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] }, - "last_updated": "2026-09-20" + "last_updated": "2026-09-21" }, "esp32p4rev1-eth": { "tick_us": { diff --git a/test/unit/light/unit_Drivers_livePalettes.cpp b/test/unit/light/unit_Drivers_livePalettes.cpp index c9808b79..bad30d20 100644 --- a/test/unit/light/unit_Drivers_livePalettes.cpp +++ b/test/unit/light/unit_Drivers_livePalettes.cpp @@ -92,16 +92,12 @@ TEST_CASE("editing a factory palette leaves one entry, not two") { } TEST_CASE("a quote in a live palette's name is escaped, so the options stay parseable") { - // A palette is named by its FILE, and a user names the file. An unescaped name ends the JSON - // string early and the control frame it sits in stops parsing, so the UI loses the whole - // picker rather than one entry. paletteNames() escaped and paletteOptions() did not, for the - // same name from the same source. + // A palette is named by its FILE, and a user names the file. An unescaped name ends the JSON string early, so the UI loses the whole picker. paletteNames() escaped it and paletteOptions() did not. static const char* const kNames[] = {"unit-a\"b"}; static const char* const kTags[] = {"\U0001F3A8"}; mm::LivePalettes::set(kNames, kTags, 1); - // Every built-in with its whole color table, so the dump is tens of kilobytes: a heap buffer - // sized past the real thing, so an overflow here means a defect rather than a cap. + // Every built-in with its color table is tens of kilobytes, so the buffer is sized past the real thing: an overflow means a defect, not a cap. std::vector buf(1 << 17, 0); JsonSink sink(buf.data(), buf.size()); mm::paletteOptions(sink); From f1be21c42f7b8836874ccfeccf63d229d29b7dd3 Mon Sep 17 00:00:00 2001 From: ewowi Date: Mon, 21 Sep 2026 12:37:32 +0200 Subject: [PATCH 2/5] Seed the Ethernet preset from the pins after the config is restored MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A board upgrading to the presets now opens on the board it already is, rather than being written back to Custom, and a board that has chosen nothing adopts no preset's pins. Both bugs were found on hardware, on an Olimex, a P4 shield and an S31. **Core** - `NetworkModule` seeds `ethBoard` while the selection is still the un-chosen Custom, rather than once on the first `defineControls`. That first render happens BEFORE the saved config is applied, so the seed read the defaults, where `ethType_` is still `ethNone` and no preset carries type 0: it fell through to Custom and the restore persisted that over a board whose pins named a preset. - `buildEthPresetOptions` falls back to Custom rather than to row 0. Row 0 is a real preset, so a board that had chosen nothing adopted classic ESP32 Ethernet pins, which on an S31 or P4 is the wrong PHY entirely. Invisible to every test and to the desktop preview; found because the test written for the first bug failed with the wrong value. - The `ethPresetSeeded_` latch goes, replaced by `ethPresetIsUnset()`, which reads the state rather than remembering a visit. **Scripts/MoonDeck** - `check_devices` drops an em-dash from two messages. **Tests** - A regression test drives the restore sequence the scheduler performs, overlay then rebuild, and pins both bugs. It fails against either of the old behaviours. **Docs/CI** - `core/system.md` and `core/ui.md` lose all 98 em-dashes: a definition bullet takes a colon, a mid-sentence aside a comma. Both files were already open for the `mode` control's own entry, and CLAUDE.md names an em-dash as the kind of thing to fix in the edit that meets it. - `mode` replaces the removed `expertMode` in the System card. **Notes** - `previewsEthernetControls` subtracts nothing. It earns its place by making both of these bugs reproducible on the desktop, where the Ethernet controls otherwise compile out and neither could be tested off-hardware. - Verified on three chip families, each keeping its preset across a reboot with the link up throughout: Olimex ESP32-Gateway on Classic RMII, MHC-WLED P4 shield on P4-NANO, Espressif S31 CoreBoard on S31 CoreBoard. The P4 is the upgrade path itself: it came up from old firmware with saved pins and no `ethBoard`, and recognised them. **Reviews** - ๐Ÿ‡ Em-dashes in the `mode` and stats-line bullets โ†’ fixed, along with the rest of both files. Co-Authored-By: Claude Opus 5 (1M context) --- docs/moonmodules/core/system.md | 130 +++++++++--------- docs/moonmodules/core/ui.md | 72 +++++----- moondeck/check/check_devices.py | 4 +- src/core/system/NetworkModule.h | 20 ++- .../unit/core/unit_NetworkModule_ethernet.cpp | 50 +++++++ 5 files changed, 167 insertions(+), 109 deletions(-) diff --git a/docs/moonmodules/core/system.md b/docs/moonmodules/core/system.md index c5eb0034..2675647f 100644 --- a/docs/moonmodules/core/system.md +++ b/docs/moonmodules/core/system.md @@ -1,6 +1,6 @@ # Core system -The device's fixed infrastructure โ€” identity, network, provisioning, firmware, and the inspection tools. These modules are **always present and wired by code**, not user-added; the user does not add or delete them. User-added capability modules (Audio, IR) live in the `Services` container instead โ€” see [core/services.md](services.md). Every row links to its generated technical page (the full API, from the `.h`) and its tests. Cross-cutting rationale that no single `.h` owns lives in the prose sections below the table. +The device's fixed infrastructure: identity, network, provisioning, firmware, and the inspection tools. These modules are **always present and wired by code**, not user-added; the user does not add or delete them. User-added capability modules (Audio, IR) live in the `Services` container instead, see [core/services.md](services.md). Every row links to its generated technical page (the full API, from the `.h`) and its tests. Cross-cutting rationale that no single `.h` owns lives in the prose sections below the table. ## System modules @@ -8,15 +8,15 @@ The device's fixed infrastructure โ€” identity, network, provisioning, firmware, ### System -The device's identity and vitals โ€” name (behind mDNS `.local`, the SoftAP SSID, the DHCP hostname), uptime, heap, and per-module footprint reporting. Its fixed inspection children (Tasks, I2C scan) hang beneath it. +The device's identity and vitals: name (behind mDNS `.local`, the SoftAP SSID, the DHCP hostname), uptime, heap, and per-module footprint reporting. Its fixed inspection children (Tasks, I2C scan) hang beneath it. System module controls -- `deviceName` โ€” the identity behind mDNS, the SoftAP SSID and the DHCP hostname. -- `deviceModel` โ€” the board model (drives the installer catalog entry). -- `mode` โ€” how much of the UI is shown: `user`, `expert` (๐ŸŽš๏ธ) or `developer` (๐Ÿ”ง), cumulative. -- `logLevel` โ€” serial verbosity, defaulting to Warn. The first 60 s always logs at Info. -- read-only vitals โ€” `uptime`, `fps`, `heap`, `psram`, `flash`, `chip`, and per-module footprint. +- `deviceName`: the identity behind mDNS, the SoftAP SSID and the DHCP hostname. +- `deviceModel`: the board model (drives the installer catalog entry). +- `mode`: how much of the UI is shown, `user`, `expert` (๐ŸŽš๏ธ) or `developer` (๐Ÿ”ง), cumulative. +- `logLevel`: serial verbosity, defaulting to Warn. The first 60 s always logs at Info. +- read-only vitals: `uptime`, `fps`, `heap`, `psram`, `flash`, `chip`, and per-module footprint. Detail: [technical](moxygen/SystemModule.md) @@ -30,12 +30,12 @@ WiFi / Ethernet connectivity, static-IP configuration, RSSI and TX-power reporti Network module controls -- `mode` โ€” WiFi / Ethernet / off. -- `ssid` / `password` โ€” WiFi credentials. -- `mDNS` โ€” the `.local` hostname. -- `addressing` โ€” DHCP or static; static exposes IP / gateway / subnet / DNS fields. -- `ethType` / `ethPhyAddr` / `ethRstGpio` / โ€ฆ โ€” Ethernet PHY configuration. -- read-only โ€” `rssi` (dBm), `txPower` (dBm). +- `mode`: WiFi / Ethernet / off. +- `ssid` / `password`: WiFi credentials. +- `mDNS`: the `.local` hostname. +- `addressing`: DHCP or static; static exposes IP / gateway / subnet / DNS fields. +- `ethType` / `ethPhyAddr` / `ethRstGpio` / โ€ฆ, Ethernet PHY configuration. +- read-only: `rssi` (dBm), `txPower` (dBm). Detail: [technical](moxygen/NetworkModule.md) @@ -49,7 +49,7 @@ Serial/BLE Improv Wi-Fi provisioning: the web installer hands credentials to a f Improv provisioning module controls -- `provision_status` โ€” read-only provisioning state. +- `provision_status`: read-only provisioning state. Detail: [technical](moxygen/ImprovProvisioningModule.md) ยท [frame format](moxygen/ImprovFrame.md) ยท [chunk reassembly](moxygen/ImprovOpReassembler.md) @@ -59,10 +59,10 @@ Detail: [technical](moxygen/ImprovProvisioningModule.md) ยท [frame format](moxyg Discovers other projectMM devices on the LAN and lists them, persisting the last-known list across a reboot. A wired-by-code child of Network. -Devices module โ€” discovered LAN devices +Devices module, discovered LAN devices -- `devices` โ€” a List of discovered devices; each row expands to a detail panel. Persistable. -- `wledCompatible` โ€” also announce on WLED's broadcast address, off by default. +- `devices`: a List of discovered devices; each row expands to a detail panel. Persistable. +- `wledCompatible`: also announce on WLED's broadcast address, off by default. WLED apps browse on broadcast, so a device appears in them only with this on. Off is the better neighbour, since a broadcast wakes every device on the LAN to parse a packet none of them want. Presence always goes to the projectMM group regardless, so peers find each other either way. See [multicast and IGMP snooping](../../explanation/architecture/moonlight.md#multicast-and-igmp-snooping). @@ -78,13 +78,13 @@ Bridges the light to an MQTT broker so a home-automation hub can control it, as MQTT module controls -- `broker` โ€” the broker hostname (e.g. `homeassistant.lan`) or IP. A hostname is resolved via DNS. -- `port` โ€” broker port (default 1883). -- `username` / `password` โ€” broker credentials, optional, the password stored obfuscated. -- `haDiscovery` โ€” announce a Home Assistant discovery light, off by default. +- `broker`: the broker hostname (e.g. `homeassistant.lan`) or IP. A hostname is resolved via DNS. +- `port`: broker port (default 1883). +- `username` / `password`: broker credentials, optional, the password stored obfuscated. +- `haDiscovery`: announce a Home Assistant discovery light, off by default. HA already discovers the device over the WLED shim with no broker, so this stays off to avoid a duplicate entity. Turn it on for broker-only or cross-subnet setups. See the [home-automation guide](../../how-to/home-automation.md). -- read-only โ€” `mqtt_status`, from `disabled` and `idle` through to `connected`, or an error. +- read-only: `mqtt_status`, from `disabled` and `idle` through to `connected`, or an error. Detail: [technical](moxygen/MqttModule.md) @@ -94,13 +94,13 @@ Detail: [technical](moxygen/MqttModule.md) ### Firmware update -Over-the-air firmware flashing โ€” the one operation that swaps the binary and needs a power cycle (every *config* change applies live; a firmware OTA does not). +Over-the-air firmware flashing, the one operation that swaps the binary and needs a power cycle (every *config* change applies live; a firmware OTA does not). Firmware update module controls -- `firmware` โ€” the OTA image to flash. -- read-only: `version`, `build`, `partition`. -- `image` โ€” on a device carrying two images, which one those describe and an install writes. +- `firmware`: the OTA image to flash. +- read-only: `version`, `build` and `partition`. +- `image`: on a device carrying two images, which one those describe and an install writes. The choice is the app it runs, or MoonBase in the factory slot. This control's presence is also what tells the UI that installs run through the reboot-into-MoonBase cycle, behind one "updating firmware" overlay, and that a **Restart in MoonBase** button belongs on the card ([MoonBase](../../explanation/architecture/moonbase.md)). @@ -130,9 +130,9 @@ One opt-in report about this install, sent once per install or upgrade, so devel Stats module controls -- `consent` โ€” a checkbox, off by default. Nothing is sent and no identifier computed while off. +- `consent`: a checkbox, off by default. Nothing is sent and no identifier computed while off. - read-only: `version` and `reportedVersion`, which differ exactly when a report is due. -- `send update` โ€” reports again now, for a setup that changed without a version change. +- `send update`: reports again now, for a setup that changed without a version change. The two versions differing is what makes an upgrade send one report and a reboot send nothing. The button replaces this install's row rather than adding one. @@ -150,10 +150,10 @@ A public message board between projectMM devices, in the shape Meshtastic's chan Talk module controls -- `consent` โ€” a checkbox, off by default. Nothing is published or read while it is off. -- `shareName` โ€” whether your device name rides along, **off by default and a separate decision**. -- `message` โ€” what to say, up to 280 characters. Typing changes nothing on its own. -- `send` โ€” publishes the message and clears the box, as does Enter in the message field. +- `consent`: a checkbox, off by default. Nothing is published or read while it is off. +- `shareName`: whether your device name rides along, **off by default and a separate decision**. +- `message`: what to say, up to 280 characters. Typing changes nothing on its own. +- `send`: publishes the message and clears the box, as does Enter in the message field. A device name identifies a person rather than a machine. Without it your messages carry the first 8 characters of your installation id, which groups them without naming you. @@ -169,18 +169,18 @@ Detail: [technical](moxygen/MoonTalkModule.md) Browse and manage the device filesystem: a folder tree with an inline text editor. Distinct from Filesystem, the persistence engine. Behaviour: โŒ„ details. -File Manager panel โ€” folder tree + toolbar +File Manager panel, folder tree + toolbar -- `file browser` โ€” the panel itself: a folder tree, a toolbar and an inline text editor. +- `file browser`: the panel itself: a folder tree, a toolbar and an inline text editor. - **Backup (โค“)**, download the device's files as one `.json` bundle. **Keep it private: it contains the WiFi password.** Every file is byte-verified against the listing, and an unreadable one is skipped and named. - **Restore (โŸฒ)**, upload a backup bundle, pressing twice since it overwrites the device's files. Known renames from [MIGRATING.md](../../reference/MIGRATING.md) apply before upload, then a report lists what needs an eye. Every file applies as it lands, bar network settings and the web server's `port`, which the dialog names. -- `show hidden` โ€” reveal dot-prefixed files and folders, such as `.config`. -- `filesystem` โ€” read-only usage bar (used / total bytes, from the platform). -- `lastSaved` โ€” read-only; how long ago config was persisted (read from the Filesystem engine). +- `show hidden`: reveal dot-prefixed files and folders, such as `.config`. +- `filesystem`: read-only usage bar (used / total bytes, from the platform). +- `lastSaved`: read-only; how long ago config was persisted (read from the Filesystem engine). Detail: [technical](moxygen/FileManagerModule.md) @@ -188,15 +188,15 @@ Detail: [technical](moxygen/FileManagerModule.md) ### I2C scan -A fixed System module (wired-by-code, always present) that probes the IยฒC bus on a button press and reports the addresses found โ€” a hardware bring-up tool. The bus pins default to unused (โˆ’1), so a board without an IยฒC device claims no GPIO for it; a board with a bus sets its pins via the catalog, or you type them for an ad-hoc scan. Passive until the scan button is pressed. +A fixed System module (wired-by-code, always present) that probes the IยฒC bus on a button press and reports the addresses found, a hardware bring-up tool. The bus pins default to unused (โˆ’1), so a board without an IยฒC device claims no GPIO for it; a board with a bus sets its pins via the catalog, or you type them for an ad-hoc scan. Passive until the scan button is pressed. I2C scan module controls -- `sda` / `scl` โ€” the bus GPIOs, defaulting to โˆ’1 for unused. +- `sda` / `scl`: the bus GPIOs, defaulting to โˆ’1 for unused. A board with a fixed bus injects its own through the catalog, or you type the pins for an ad-hoc scan. The classic Arduino-ESP32 pair is 21/22. -- `scan` โ€” a button; press to probe the bus now. -- read-only โ€” `result` (addresses found). +- `scan`: a button; press to probe the bus now. +- read-only: `result` (addresses found). Detail: [technical](moxygen/I2cScanModule.md) @@ -206,10 +206,10 @@ Detail: [technical](moxygen/I2cScanModule.md) A read-only diagnostic showing **what runs where**: you cannot optimise which module runs on which core until you can see it. A fixed System module, wired-by-code, with each task's MoonModules nested beneath it. -Tasks module โ€” a row per FreeRTOS task +Tasks module, a row per FreeRTOS task -- read-only โ€” `tasks`, a row per FreeRTOS task. -- read-only โ€” `core0` / `core1`, what executes on each core, empty on a single-core chip. +- read-only: `tasks`, a row per FreeRTOS task. +- read-only: `core0` / `core1`, what executes on each core, empty on a single-core chip. Each row carries `name`, `state`, `core`, `prio` and `stack`, the minimum free stack seen. A `cpu` percentage appears only in a profiling build, off by default because the run-time counter costs about 5% of the tick. @@ -221,13 +221,13 @@ Detail: [technical](moxygen/TasksModule.md) ### Pins -A read-only diagnostic showing **which module owns each GPIO, for what role, and whether that pin is safe for it** โ€” the device's pin ownership map, keyed by physical GPIO. A fixed System module, wired-by-code. +A read-only diagnostic showing **which module owns each GPIO, for what role, and whether that pin is safe for it**, the device's pin ownership map, keyed by physical GPIO. A fixed System module, wired-by-code. It walks the live tree for every claimed pin, holding no state, and flags double claims. -Pins module โ€” the GPIO ownership map +Pins module, the GPIO ownership map -- read-only โ€” `pins`, a row per claimed GPIO. +- read-only: `pins`, a row per claimed GPIO. Each row carries `gpio`, `owner` and `role`, plus live `dir`, `level` and `drive`. A row takes a coloured edge when unsafe: red for a reserved or double-claimed pin, yellow for a driven role on a strap, per [gpio-usage.md](../../reference/hardware/gpio-usage.md). @@ -237,14 +237,14 @@ Detail: [technical](moxygen/PinsModule.md) ### Control -A grid of preset pads, a row of rotary encoders above them, a row of on/off switches above those, and a bank of faders below โ€” the layout of a Mackie-style control desk ([X-Touch](https://www.behringer.com/product.html?modelCode=0808-AAF), [QCon Pro G2](https://www.iconproaudio.com/product/qcon-pro-g2/)), so a physical surface maps onto it without a translation layer. +A grid of preset pads, a row of rotary encoders above them, a row of on/off switches above those, and a bank of faders below, the layout of a Mackie-style control desk ([X-Touch](https://www.behringer.com/product.html?modelCode=0808-AAF), [QCon Pro G2](https://www.iconproaudio.com/product/qcon-pro-g2/)), so a physical surface maps onto it without a translation layer. Control module surface: encoders, preset pads, faders -- `presets` โ€” one pad per preset file. Click applies, right-click names, drag rearranges. -- `switch1` โ€ฆ `switch8` โ€” the switch row. `switch1` drives `Drivers.on`, the rest unbound. -- `encoder1` โ€ฆ `encoder8` โ€” rotary encoders. Drag or scroll to turn, right-click to see the binding. -- `fader1` โ€ฆ `fader8` โ€” faders. `fader1` drives `Drivers.brightness`, the rest unbound. +- `presets`: one pad per preset file. Click applies, right-click names, drag rearranges. +- `switch1` โ€ฆ `switch8`, the switch row. `switch1` drives `Drivers.on`, the rest unbound. +- `encoder1` โ€ฆ `encoder8`, rotary encoders. Drag or scroll to turn, right-click to see the binding. +- `fader1` โ€ฆ `fader8`, faders. `fader1` drives `Drivers.brightness`, the rest unbound. Detail: [technical](moxygen/ControlModule.md) @@ -290,9 +290,9 @@ Detail: [technical](moxygen/ControlSurface.md) ยท [InputMapping](moxygen/InputMa #### Presets -A preset is a file: `/.config/presets/.json`. Saving writes one, applying reads one, deleting removes one. Nothing else holds preset state, so there is no second copy to keep in step: the list is rebuilt from the folder rather than persisted alongside it. That rescan runs at startup and after every save, rename and delete โ€” a reorder only rewrites the affected files and re-sorts the rows in place, since the folder's contents have not changed. So a preset added or removed through the File Manager appears once the module next rescans (a reboot, or a save, rename or delete on the surface), not the instant the file lands. +A preset is a file: `/.config/presets/.json`. Saving writes one, applying reads one, deleting removes one. Nothing else holds preset state, so there is no second copy to keep in step: the list is rebuilt from the folder rather than persisted alongside it. That rescan runs at startup and after every save, rename and delete, a reorder only rewrites the affected files and re-sorts the rows in place, since the folder's contents have not changed. So a preset added or removed through the File Manager appears once the module next rescans (a reboot, or a save, rename or delete on the surface), not the instant the file lands. -The name becomes the file name, so it is restricted to printable ASCII without `/`, `\` or `.` โ€” a validator on the control, which every write path runs. `slot` records which pad the preset occupies, so a surface arranged to match a physical desk survives a reboot. +The name becomes the file name, so it is restricted to printable ASCII without `/`, `\` or `.`, a validator on the control, which every write path runs. `slot` records which pad the preset occupies, so a surface arranged to match a physical desk survives a reboot. ##### What a preset carries @@ -320,7 +320,7 @@ A pad is tinted by its role: layout blue, effects violet, driver green, service ##### Applying is a rebuild -Applying a preset creates, replaces and destroys modules to match what the file describes โ€” it is a restore, not a value overlay: a preset carrying more than the device has adds it, and one describing less removes what it omits. +Applying a preset creates, replaces and destroys modules to match what the file describes, it is a restore, not a value overlay: a preset carrying more than the device has adds it, and one describing less removes what it omits. Structural mutation quiesces the render worker, and mutations run inline on the render tick, so a large restore stalls rendering for its duration. The captured subtree is applied and `prepareTree()` runs once at the end. Presets are a cold-path feature; the tick path is untouched. @@ -330,11 +330,11 @@ Looks reach Home Assistant two ways, and only `Effects` presets travel either of **The WLED integration** (`/presets.json`) is the native path: HA renders looks in its own preset dropdown, shows which one is applied, and applies one when it is chosen. This is what HA calls a preset. -**MQTT discovery** publishes the same looks as the light entity's **effect list**. HA has no preset concept over MQTT, so they arrive as effects โ€” the same result from the user's side, reached through a different mechanism. +**MQTT discovery** publishes the same looks as the light entity's **effect list**. HA has no preset concept over MQTT, so they arrive as effects, the same result from the user's side, reached through a different mechanism. -HA caches the preset list and re-fetches only when the device's `info.fs.pmt` value changes, so the device reports a revision counter there that bumps on every preset save, rename and delete โ€” a counter rather than a timestamp, so two changes inside one second still read as two. A constant there leaves HA showing the list it read at setup forever; over MQTT the same revision re-announces the effect list mid-session. +HA caches the preset list and re-fetches only when the device's `info.fs.pmt` value changes, so the device reports a revision counter there that bumps on every preset save, rename and delete, a counter rather than a timestamp, so two changes inside one second still read as two. A constant there leaves HA showing the list it read at setup forever; over MQTT the same revision re-announces the effect list mid-session. -Only looks are exposed, on both paths. A `Drivers` or `Layouts` preset rewires pins or geometry, which must not be reachable from something that believes it is choosing a color scheme โ€” the restriction is enforced at the apply entry point, not merely by omitting them from the list. +Only looks are exposed, on both paths. A `Drivers` or `Layouts` preset rewires pins or geometry, which must not be reachable from something that believes it is choosing a color scheme, the restriction is enforced at the apply entry point, not merely by omitting them from the list. Home Assistant's WLED integration connects on **port 80 only**: its host field rejects a port, so a desktop build (which defaults to 8080) needs `--port 80`, and that needs root: @@ -345,7 +345,7 @@ sudo uv run moondeck/run/run_desktop.py --port 80 The discovery buffers are sized to the looks this device actually has, and grow or shrink as presets are added and removed. There is no cap on the number: a fixed one would either reserve memory a small setup never uses, or silently publish nothing once the list outgrew it. ## MQTT, details -The topic prefix is `projectMM/` โ€” a **stable** identifier (the last 6 hex of the device's MAC), fixed for the device's life. Renaming the device does **not** change its topics, so a hub's config never breaks on a rename (the WLED/Tasmota/Home-Assistant convention). It's derived, not a stored control. +The topic prefix is `projectMM/`, a **stable** identifier (the last 6 hex of the device's MAC), fixed for the device's life. Renaming the device does **not** change its topics, so a hub's config never breaks on a rename (the WLED/Tasmota/Home-Assistant convention). It's derived, not a stored control. **Topics** (for a device whose MAC ends `563cfe`): the device SUBSCRIBEs to the `set` topics and PUBLISHes the `get` topics on change (and on connect, so a controller never reads "No Response"). It also publishes its friendly `deviceName` on the retained `name` topic, so a hub can show the human name while the topics stay MAC-stable: @@ -361,9 +361,9 @@ The topic prefix is `projectMM/` โ€” a **stable** identifier (the last 6 he | device โ†’ get | `projectMM/563cfe/update/state` | `{"installed_version":โ€ฆ,"latest_version":โ€ฆ,"release_url":โ€ฆ,"title":โ€ฆ}` (retained; HA update entity) | | set โ†’ device | `projectMM/563cfe/update/set` | target version string (empty = install latest); triggers OTA against the matching GitHub release asset | -The HomeKit color wheel has no "palette" concept, so `hsv/set`'s hue+saturation pick the **nearest palette** (each built-in palette has a representative color; the closest one is selected) and the value drives brightness โ€” the color wheel becomes a natural palette selector. +The HomeKit color wheel has no "palette" concept, so `hsv/set`'s hue+saturation pick the **nearest palette** (each built-in palette has a representative color; the closest one is selected) and the value drives brightness, the color wheel becomes a natural palette selector. -**Homebridge** โ€” install [`homebridge-mqttthing`](https://github.com/arachnetech/homebridge-mqttthing) and add a `lightbulb` accessory. Use the device's own MAC suffix (read it from the `mqtt_status`/topics, or `mosquitto_sub -t 'projectMM/#'`) in place of `563cfe`: +**Homebridge**, install [`homebridge-mqttthing`](https://github.com/arachnetech/homebridge-mqttthing) and add a `lightbulb` accessory. Use the device's own MAC suffix (read it from the `mqtt_status`/topics, or `mosquitto_sub -t 'projectMM/#'`) in place of `563cfe`: ```json { @@ -387,8 +387,8 @@ The HomeKit color wheel has no "palette" concept, so `hsv/set`'s hue+saturation ``` Home Assistant adopts the device two ways, both zero-config: -- **MQTT auto-discovery** โ€” with `haDiscovery` on (opt-in; off by default) and a broker set, the device announces itself on `homeassistant/light/projectMM_/config` and HA auto-creates a wired entity with **on/off + brightness** (the config declares `brightness` only; color isn't in it, so the entity has no color control). Retained across reboots. Color/palette stays on the separate `hsv/set` topic above, not this entity. Off by default because the WLED `/json` shim already gives HA a richer light (color + palette + sensors) over mDNS with no broker โ€” leaving both on lists the device twice; enable this only for broker-only / cross-subnet setups. -- **WLED integration** โ€” HA's built-in WLED integration discovers the device over the WLED `/json` API projectMM already serves; on/off + brightness work with no broker. +- **MQTT auto-discovery**: with `haDiscovery` on (opt-in; off by default) and a broker set, the device announces itself on `homeassistant/light/projectMM_/config` and HA auto-creates a wired entity with **on/off + brightness** (the config declares `brightness` only; color isn't in it, so the entity has no color control). Retained across reboots. Color/palette stays on the separate `hsv/set` topic above, not this entity. Off by default because the WLED `/json` shim already gives HA a richer light (color + palette + sensors) over mDNS with no broker, leaving both on lists the device twice; enable this only for broker-only / cross-subnet setups. +- **WLED integration**: HA's built-in WLED integration discovers the device over the WLED `/json` API projectMM already serves; on/off + brightness work with no broker. Both can be on at once. Setup walkthrough (including exposing HA to Apple Home via HA's HomeKit Bridge, no Homebridge needed) in the [Home Assistant recipe](../../how-to/home-automation.md#adopt-in-home-assistant). @@ -397,8 +397,8 @@ The panel is a lazy folder **tree** (each folder loads its children on first exp - Click a folder's row to select it and toggle its expansion (โ–ธ/โ–พ); click a selected file to open the editor. - The toolbar acts on the selected node: **๏ผ‹ folder** creates a folder inside it, **๏ผ‹ file** creates an empty file (click it to edit), **๐Ÿ—‘ delete** removes the selected file, or a folder and everything inside it (press-twice to confirm), **โŸณ** refreshes. -- **Drag files from the desktop** onto a folder (or the tree) to upload them โ€” the body streams straight to the file (any size, binary-safe; capped only by a sanity limit and the free space, which it reports if short); a per-file **โค“** streams it back to the desktop. +- **Drag files from the desktop** onto a folder (or the tree) to upload them, the body streams straight to the file (any size, binary-safe; capped only by a sanity limit and the free space, which it reports if short); a per-file **โค“** streams it back to the desktop. - The editor loads a file's text, pretty-prints JSON on open, and saves atomically; a binary file (contains a NUL) loads read-only (use โค“ to fetch it intact). Upload and download both stream, so neither truncates. -- Create / delete are HTTP calls (`POST` / `DELETE /api/dir?path=`), not controls โ€” the path rides the request, so nothing is stored on the device per op. +- Create / delete are HTTP calls (`POST` / `DELETE /api/dir?path=`), not controls, the path rides the request, so nothing is stored on the device per op. Last-modified dates (needs an NTP time source + LittleFS mtime), binary/large + folder upload, folder-as-zip download, and `.ml` syntax highlighting are backlogged ([backlog-core ยง File Manager follow-ups](../../work/future/backlog-core.md#file-manager-follow-ups)). diff --git a/docs/moonmodules/core/ui.md b/docs/moonmodules/core/ui.md index 62c0fbf5..79083478 100644 --- a/docs/moonmodules/core/ui.md +++ b/docs/moonmodules/core/ui.md @@ -1,9 +1,9 @@ # Web UI -The projectMM web UI as shipped โ€” the render layer over the MoonModule tree. This page is the UI's +The projectMM web UI as shipped, the render layer over the MoonModule tree. This page is the UI's own implementation spec (status bar, cards, control rendering, styling, the no-rebuild update -contract). The **high-level architecture** โ€” hand-maintained files, MoonModule-driven rendering, the -light-domain plug-in points โ€” lives in [MoonCore, the web UI](../../explanation/architecture/mooncore.md#web-ui); +contract). The **high-level architecture**, hand-maintained files, MoonModule-driven rendering, the +light-domain plug-in points, lives in [MoonCore, the web UI](../../explanation/architecture/mooncore.md#web-ui); the **backend contract** it consumes (every `/api/*` endpoint, the `/ws` frame shape, the control descriptors) is owned by [HttpServerModule](moxygen/HttpServerModule.md); the **emoji legend** the cards and picker render is [the tag emoji legend](../../explanation/architecture/index.md#tag-emoji-legend). @@ -36,11 +36,11 @@ This page covers only what those don't: the browser-side rendering behavior. โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` -- Fixed status bar at top (44 px) โ€” hamburger, MoonLight logo, then the rest. +- Fixed status bar at top (44 px), hamburger, MoonLight logo, then the rest. - Side nav: a left column listing root modules; the selected root's card subtree fills the main area (one root visible at a time). - Module-card column max-width 500 px, centered (single-column, easier to read on phones). The preview - canvas is **not** capped โ€” it spans the full content width. + canvas is **not** capped, it spans the full content width. - Preview canvas is sticky just below the status bar and shrinks 0 % โ†’ 50 % as the user scrolls 0 โ†’ 300 px. @@ -48,19 +48,19 @@ This page covers only what those don't: the browser-side rendering behavior. Fixed top, 44 px, left-to-right: -1. **Hamburger (โ˜ฐ)** โ€” toggles the side nav (`body.nav-open`). See [ยง Side navigation](#side-navigation). -2. **MoonLight logo** โ€” 28 px PNG from `/moonlight-logo.png`; the same image is the page favicon. -3. **Brand wordmark** โ€” "projectMM". -4. **Device name** โ€” from the `System.deviceName` control. -5. **System stats** โ€” `uptime ยท NN K free` (uptime `Xd Yh Zm Ws`, free heap KB), read from the +1. **Hamburger (โ˜ฐ)**, toggles the side nav (`body.nav-open`). See [ยง Side navigation](#side-navigation). +2. **MoonLight logo**, 28 px PNG from `/moonlight-logo.png`; the same image is the page favicon. +3. **Brand wordmark**, "projectMM". +4. **Device name**, from the `System.deviceName` control. +5. **System stats**, `uptime ยท NN K free` (uptime `Xd Yh Zm Ws`, free heap KB), read from the SystemModule controls in the WS state push; no separate polling endpoint. -6. **WebSocket dot** โ€” green = connected, gray = disconnected. The socket auto-reconnects with +6. **WebSocket dot**, green = connected, gray = disconnected. The socket auto-reconnects with exponential backoff (500 ms โ†’ 5 s) on close; no manual reconnect button (a page reload covers the rare half-dead socket). -7. **Reboot button (โป)** โ€” red border via `data-crashed="true"` when `SystemModule.bootReason` is +7. **Reboot button (โป)**, red border via `data-crashed="true"` when `SystemModule.bootReason` is PANIC / INT_WDT / TASK_WDT / BROWNOUT. Press-twice to confirm: the first click arms it (solid red), - a second sends `POST /api/reboot`. Disarms after 3 s or on pointer-leave โ€” no `confirm()` popup. -8. **Theme toggle (โ˜€/๐ŸŒ™)** โ€” flips `[data-theme]` on ``; preference in `localStorage['mm_theme']`. + a second sends `POST /api/reboot`. Disarms after 3 s or on pointer-leave, no `confirm()` popup. +8. **Theme toggle (โ˜€/๐ŸŒ™)**, flips `[data-theme]` on ``; preference in `localStorage['mm_theme']`. ## Side navigation @@ -73,32 +73,32 @@ A left column listing the root modules, one entry per top-level MoonModule. column the hamburger collapses/expands; on narrow screens (< 820 px) it's a slide-in drawer over a dimming overlay (click the overlay or press Esc to close). - **The nav states its own order** (`NAV_ORDER` in app.js), which is deliberately NOT the order the - roots run in. `main.cpp` orders by dependency โ€” Filesystem before anything that writes a file, - System before the modules that read its identity โ€” and that is load-bearing, so it cannot be + roots run in. `main.cpp` orders by dependency, Filesystem before anything that writes a file, + System before the modules that read its identity, and that is load-bearing, so it cannot be reshuffled to suit a menu. It also reads as an implementation detail to a user: it puts System first and the lights last. The nav instead groups by what someone is looking for: **Control**, then the light pipeline in pipeline order (**Layouts โ†’ Effects โ†’ Drivers**), then the device (**System, File Manager, Network, Services, Firmware**). A root the list does not name still - appears, after them, in scheduler order โ€” adding a module never makes it invisible. + appears, after them, in scheduler order, adding a module never makes it invisible. - **No root reorder.** The order is fixed in that list; the nav does not drag-reorder. -- **Footer** pinned to the bottom of the nav: social links (GitHub, Discord, Reddit, YouTube โ€” inline +- **Footer** pinned to the bottom of the nav: social links (GitHub, Discord, Reddit, YouTube, inline SVG) and a `ยฉ MoonLight` line. The WS state push carries the full module tree; only the selected root is rendered, and `updateValues()` patches just the visible cards (non-rendered roots have no DOM nodes, so their data -is silently ignored). Per-root server-side filtering was evaluated and deferred โ€” the 1 Hz push is +is silently ignored). Per-root server-side filtering was evaluated and deferred, the 1 Hz push is cheap and the JSON is built through a streaming sink, so tree size is not a buffer-limit concern. ## Module card -Each MoonModule renders as a card, with **child cards nested inside their parent card's box** โ€” the +Each MoonModule renders as a card, with **child cards nested inside their parent card's box**, the parent's border encloses its children, so the tree shape is visible structurally, not just by indentation. Nesting depth shows as progressively lighter backgrounds and a left-margin indent. ```text โ”Œโ”€ card โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ [name] [emoji] [timing ยท ๐Ÿง  mem] [enabled toggle] [โœŽ ร— โ˜ฐ] [? help] [{ } api] โ”‚ -โ”‚ [control rows โ€” one per control] โ”‚ +โ”‚ [control rows, one per control] โ”‚ โ”‚ โ”Œโ”€ child card โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ€ฆ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ @@ -109,10 +109,10 @@ indentation. Nesting depth shows as progressively lighter backgrounds and a left - The parent's own controls render **above** its children; `+ add child` renders **below** them. Child cards live in a `.card-children` wrapper appended into the parent card's DOM node (not flat siblings); `renderModuleTree` recurses into the parent card, not into `main`. -- **`{ }`** opens `GET /api/modules/{name}` in a new tab โ€” that one module's live JSON, for issue +- **`{ }`** opens `GET /api/modules/{name}` in a new tab, that one module's live JSON, for issue reports (see [Log an issue](../../how-to/logging-an-issue.md)). On EVERY card, unlike `โœŽ`/`ร—` (user-editable children only) and `?` (types that have a doc page). -- **Enabled toggle** in the right-hand action cluster mirrors `MoonModule::enabled()` โ€” a styled +- **Enabled toggle** in the right-hand action cluster mirrors `MoonModule::enabled()`, a styled `