Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 73 additions & 34 deletions .claude/settings.local.cleaned.json
Original file line number Diff line number Diff line change
@@ -1,40 +1,79 @@
{
"permissions": {
"allow": [
"PowerShell(& \"C:\\\\Users\\\\ewoud\\\\.local\\\\bin\\\\uv.exe\" run moondeck/*)",
"PowerShell(& \"C:\\\\Users\\\\ewoud\\\\.local\\\\bin\\\\uv.exe\" run --with * python *)",
"PowerShell(& \"C:\\\\Users\\\\ewoud\\\\.local\\\\bin\\\\uv.exe\" run python *)",
"PowerShell(& \"C:\\\\Users\\\\ewoud\\\\.local\\\\bin\\\\uv.exe\" pip *)",
"PowerShell(& \"C:\\\\Program Files\\\\CMake\\\\bin\\\\cmake.exe\" *)",
"PowerShell($env:Path = \"C:\\\\Program Files\\\\CMake\\\\bin;\" + $env:Path; *)",
"PowerShell($env:Path = \"C:\\\\Program Files\\\\GitHub CLI;\" + $env:Path; *)",
"PowerShell(Get-PnpDevice*)",
"PowerShell(Get-Item *)",
"PowerShell(Get-ItemProperty *)",
"PowerShell(Get-ChildItem *)",
"PowerShell(Get-Command * -ErrorAction SilentlyContinue*)",
"PowerShell(Get-Content *)",
"PowerShell(Get-Process *)",
"PowerShell(Get-WmiObject *)",
"PowerShell(Test-Path *)",
"PowerShell(Write-Host *)",
"PowerShell(Start-Sleep *)",
"PowerShell(Start-Process *)",
"PowerShell(New-Item *)",
"PowerShell(Remove-Item *)",
"PowerShell(Rename-Item *)",
"PowerShell(& gh *)",
"PowerShell(& git *)",
"PowerShell(Invoke-RestMethod *)",
"PowerShell(winget install *)",
"PowerShell(winget --version)",
"PowerShell($venv * python.exe *)",
"Bash(grep *)",
"Bash(Test-Path *)",
"Bash(Start-Sleep *)",
"Bash(Write-Host *)",
"Read(//c/Users/ewoud/esp/esp-idf/tools/**)",
"WebFetch(domain:github.com)"
"Bash(./build/projectMM:*)",
"Bash(./build/test/mm_scenarios:*)",
"Bash(./build/test/mm_tests:*)",
"Bash(arp -a)",
"Bash(awk:*)",
"Bash(cat:*)",
"Bash(clang++:*)",
"Bash(clang:*)",
"Bash(cmake:*)",
"Bash(cp .claude/settings.local.json .claude/settings.local.cleaned.json)",
"Bash(ctest:*)",
"Bash(curl -s*github.com/MoonModules*)",
"Bash(curl -s*raw.githubusercontent.com*)",
"Bash(echo:*)",
"Bash(gh api *)",
"Bash(gh auth *)",
"Bash(gh pr *)",
"Bash(gh run *)",
"Bash(gh search *)",
"Bash(git check-ignore *)",
"Bash(git fetch *)",
"Bash(git ls-remote *)",
"Bash(git mv:*)",
"Bash(grep:*)",
"Bash(gunzip:*)",
"Bash(head:*)",
"Bash(ipconfig getsummary *)",
"Bash(ls:*)",
"Bash(lsof:*)",
"Bash(networksetup -listallhardwareports)",
"Bash(node --check:*)",
"Bash(node --input-type=module -e ' *)",
"Bash(node --test:*)",
"Bash(node -e ' *)",
"Bash(npm install *)",
"Bash(npm test *)",
"Bash(objcopy:*)",
"Bash(objdump:*)",
"Bash(otool:*)",
"Bash(pkill:*)",
"Bash(ps:*)",
"Bash(sed:*)",
"Bash(sort:*)",
"Bash(system_profiler SPUSBDataType)",
"Bash(tail:*)",
"Bash(timeout 5 dns-sd -B _http._tcp local.)",
"Bash(uv run *)",
"Edit",
"Read(//Users/ewoud/**)",
"Read(//Users/ewoud/esp/esp-idf/**)",
"Read(//dev/**)",
"WebFetch(domain:components.espressif.com)",
"WebFetch(domain:components101.com)",
"WebFetch(domain:developer.espressif.com)",
"WebFetch(domain:devices.esphome.io)",
"WebFetch(domain:docs.espressif.com)",
"WebFetch(domain:easyelecmodule.com)",
"WebFetch(domain:files.waveshare.com)",
"WebFetch(domain:github.com)",
"WebFetch(domain:kno.wled.ge)",
"WebFetch(domain:mm.kno.wled.ge)",
"WebFetch(domain:moonmodules.org)",
"WebFetch(domain:raw.githubusercontent.com)",
"WebFetch(domain:www.digikey.com)",
"WebFetch(domain:www.farnell.com)",
"WebFetch(domain:www.waveshare.com)",
"WebSearch",
"Write"
],
"additionalDirectories": [
"/tmp",
"/Users/ewoud/esp/esp-idf/components/esp_mm/include",
"/Users/ewoud/Developer/GitHub/MoonModules/projectMM/.claude"
]
}
}
14 changes: 9 additions & 5 deletions .github/codeql-config.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
name: "projectMM CodeQL config"

# Code we did not write and will not change is not a finding we can act on. doctest.h is the
# vendored single-header test framework (test/doctest.h) and produced the only critical alert
# in the tree that we do not own — excluding it here is the standard mechanism, rather than
# dismissing the same alert by hand after every scan.
# vendored single-header test framework (test/doctest.h) and the only third-party source in the
# repo; everything else under src/ and test/ is ours. Add to this list only for genuinely
# third-party code, never to quiet a finding in our own.
#
# This is the ONLY vendored source in the repo; everything else under src/ and test/ is ours.
# Add to this list only for genuinely third-party code, never to quiet a finding in our own.
# NB it does NOT suppress findings inside doctest.h. For C/C++ `paths-ignore` filters which files
# are ANALYZED, not which are extracted through an `#include`: 143 test files include this header,
# so its code reaches the database via them and alerts still land on it (measured — the critical
# `cpp/use-after-free` at doctest.h:3705 stayed open across scans after this entry was added, and
# was dismissed in the Security tab instead). Kept because it still drops the header as a
# standalone analysis target; the dismissal is what actually silences it.
paths-ignore:
- test/doctest.h
31 changes: 24 additions & 7 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ name: CodeQL
# `build-mode: none` scans C/C++ WITHOUT building it, inferring compile flags per file. That
# matters here because the real firmware build is an ESP-IDF cross-compile for Xtensa/RISC-V a
# runner would have to reproduce. The trade is some extraction noise on macro/template-dense
# headers.
# headers — and, more sharply, that what it analyzes is the DESKTOP configuration: anything whose
# truth depends on a target typedef (`nrOfLightsType` is uint32_t here, uint16_t on a no-PSRAM
# ESP32) is judged against the wrong build. Read findings of that shape with the target in mind.
#
# Free on public repos, so the cost is wall-clock only.

Expand All @@ -32,10 +34,14 @@ on:
- main
- next-iteration
paths:
# Only when C/C++ or the workflow itself changes — a docs commit has nothing new to
# analyse and would just burn a runner.
# Only when C/C++ or the analysis setup itself changes — a docs commit has nothing new to
# analyze and would just burn a runner. Both setup files are listed: a change to the query
# set or the ignore list changes what a scan FINDS, so it has to trigger one. Listing only
# the workflow left an edit to codeql-config.yml silently unscanned until something under
# src/ happened to change.
- 'src/**'
- '.github/workflows/codeql.yml'
- '.github/codeql-config.yml'
Comment on lines +37 to +44

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Trigger CodeQL when test C/C++ changes.

The comment says C/C++ changes trigger analysis, but test/** is absent. Test alerts remain stale until an unrelated src/** or configuration change triggers a scan, despite the reporter explicitly surfacing them.

       - 'src/**'
+      - 'test/**'
       - '.github/workflows/codeql.yml'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Only when C/C++ or the analysis setup itself changes — a docs commit has nothing new to
# analyze and would just burn a runner. Both setup files are listed: a change to the query
# set or the ignore list changes what a scan FINDS, so it has to trigger one. Listing only
# the workflow left an edit to codeql-config.yml silently unscanned until something under
# src/ happened to change.
- 'src/**'
- '.github/workflows/codeql.yml'
- '.github/codeql-config.yml'
# Only when C/C++ or the analysis setup itself changes — a docs commit has nothing new to
# analyze and would just burn a runner. Both setup files are listed: a change to the query
# set or the ignore list changes what a scan FINDS, so it has to trigger one. Listing only
# the workflow left an edit to codeql-config.yml silently unscanned until something under
# src/ happened to change.
- 'src/**'
- 'test/**'
- '.github/workflows/codeql.yml'
- '.github/codeql-config.yml'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/codeql.yml around lines 37 - 44, Add the test source path
to the CodeQL workflow’s paths trigger alongside the existing src and
configuration entries, so changes under test/** initiate analysis and refresh
test alerts. Preserve the current exclusions and trigger behavior for the
existing paths.

workflow_dispatch:
schedule:
# Monday 04:00 UTC. Weekly is enough for a codebase this size, and keeps the signal
Expand Down Expand Up @@ -64,10 +70,21 @@ jobs:
with:
languages: c-cpp
build-mode: none
# security-and-quality is the widest stock set: the security queries answer the
# question above, and the quality ones tell us whether CodeQL sees anything our
# existing checks miss. Narrow it later if the noise is not worth it.
queries: security-and-quality
# security-extended, NOT security-and-quality. The quality pack was included to answer
# one question — does CodeQL see anything our existing checks miss — and after two runs
# the answer is no: its findings land in territory that already has an owner (unused
# variables and commented-out code are clang-tidy's, long/trivial switches are lizard's),
# which is the one-rule-one-owner rule this stack is built on.
#
# It was also actively misleading here. `build-mode: none` analyzes the DESKTOP
# configuration only, so 7 `cpp/constant-comparison` alerts flagged the layouts' overflow
# clamps as always-false — correct for that build, wrong for the target: `nrOfLightsType`
# is uint16_t on a no-PSRAM ESP32, where those guards are load-bearing. Build-dependent
# dead code is exactly what the quality pack hunts and exactly what it cannot judge here.
#
# The security queries are untouched, and they are the reason CodeQL has a slot at all:
# whole-program taint across the six packet parsers on a device with no MMU.
queries: security-extended
# Excludes vendored code (test/doctest.h) — see the file for why.
config-file: ./.github/codeql-config.yml

Expand Down
6 changes: 4 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ fix, keeping main clean) — creating one silently moves work somewhere the PO i

1. **Pick.** One module/effect/driver/capability — the product owner picks what to build next.
2. **Spec.** Specs before code: the module spec and the UI spec sufficient to implement from (a draft may sit in the backlog until it ships); when in doubt, ask.
3. **Plan.** Plan mode before every feature; save the approved plan to `docs/history/plans/` as `Plan-YYYYMMDD - <title>.md` — a temporary document: it ends up as the PR description and the file is deleted once the plan is realized; the merged PR is the design record. For a restructure ("make it simpler/cleaner"): enumerate 2–4 end states, name what each gains and loses, pick the leanest that solves the actual problem; propose as a question, implement only what's picked; surface follow-ups before starting so it's one coherent refactor.
3. **Plan.** Plan mode before every feature; save the approved plan to `docs/history/plans/` as `Plan-YYYYMMDD - <title>.md` — a temporary document: it ends up as the PR description and the file is deleted once the plan is realized; the merged PR is the design record. **Deleting a plan is the product owner's call — never the agent's.** "The code is written" is not "the plan is realized": a plan is realized when its *verification* is done too, including the judgement steps (thresholds tuned, results read together, the bench check). Ask; do not infer it from a green build. For a restructure ("make it simpler/cleaner"): enumerate 2–4 end states, name what each gains and loses, pick the leanest that solves the actual problem; propose as a question, implement only what's picked; surface follow-ups before starting so it's one coherent refactor.

### Build

Expand Down Expand Up @@ -74,6 +74,8 @@ Commit message: title ≤ 72 characters, imperative. Then a 1–3 sentence end-u

**Reviewer at commit-time:** run the Reviewer on the staged diff when the commit is large (roughly ten files or more across areas) or on PO request — start it first so the other checks run in parallel; findings fixed or accepted-with-reason before "commit now".

**Handling review findings** — from the Reviewer, CodeRabbit, or a human: *verify each finding against current code; fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.* A reviewer reads a snapshot and can be wrong or already out of date, so a finding is a claim to check, not an instruction to apply. Work through **every** finding, lowest severity first — a nit is a one-line fix while attention is cheap, and leaving the small ones for later means they are never done. Rising to the serious findings last also means the cheap context is already loaded.

### Merge

The PO pushes the branch; external review runs on the PR; findings are processed on the branch. On "run pre-merge": `uv run moondeck/event/premerge.py`, which re-runs the mechanical checks over the whole branch diff and lists the judgment gates it cannot decide.
Expand Down Expand Up @@ -126,7 +128,7 @@ Published at [moonmodules.org/projectMM](https://moonmodules.org/projectMM/); so
- [backlog/](https://moonmodules.org/projectMM/backlog/index.html) — forward-looking to-build lists (core / light / mixed)
- [adr/](https://moonmodules.org/projectMM/adr/index.html) — immutable architecture decision records (Nygard format)
- [history/](https://moonmodules.org/projectMM/history/index.html) — lessons, prior-project inventories, friend-repo digests
- [moonmodules/](docs/moonmodules/) — module catalog pages + generated technical pages
- [moonmodules/](https://github.com/MoonModules/projectMM/tree/main/docs/moonmodules) — module catalog pages + generated technical pages

Docs describe the system as it is; git is the history; specs precede implementation. **Documentation model**: [coding-standards.md § Documentation model](docs/coding-standards.md#documentation-model).

Expand Down
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ find_program(UV_EXECUTABLE NAMES uv REQUIRED
# here so both the application and the test binary link them.
add_library(mm_core STATIC
src/core/Control.cpp
src/core/JsonUtil.cpp
src/core/ScratchBuffer.cpp
src/core/HttpServerModule.cpp
src/core/FilesystemModule.cpp
Expand Down
18 changes: 16 additions & 2 deletions docs/MIGRATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,28 @@ projectMM ships **no migration code**: the persistence layer is robust by defaul

## Unreleased (`next-iteration`)

### The `peripheral` options are renamed to name the peripheral, not the bus protocol (2026-07-30)

The `peripheral` dropdown no longer says `i80` / `MoonI80`. "i80" is the Intel 8080 bus shape `esp_lcd` speaks — it is not a peripheral any ESP32 datasheet lists, and it matched nothing a user could look up: on the classic ESP32 that backend **is the I2S peripheral**, on the S3/P4/S31 it is the **LCD** peripheral. The new labels name the silicon block plus who drives it, which is the actual choice being made.

| Old | New (classic ESP32) | New (S3 / P4 / S31) |
|---|---|---|
| `i80` | `I2S-IDF` | `LCD-IDF` |
| `MoonI80` | — (not available) | `LCD-MM` |
| `Parlio` | — | `Parlio` (unchanged — it *is* the peripheral's name) |

`-IDF` = driven through ESP-IDF's `esp_lcd`; `-MM` = driven by our own GDMA layer below it, which is what buys the streaming ring and the 74HCT595 pin expander.

**Action: re-set the `peripheral` control** — but only on a device that already holds a persisted parallel driver AND had a non-default peripheral selected. The stored string no longer matches any option, so the loader falls back to the board's default backend; if that was already your choice, nothing changes. The web installer's board catalog ships the new names, so a fresh install or catalog re-inject is correct without action.

### The three parallel LED drivers merge into one `ParallelLedDriver` with a `peripheral` selector (2026-07-23)

`MultiPinLedDriver`, `MoonLedDriver`, and `ParlioLedDriver` are now one registered module, **`ParallelLedDriver`**, whose `peripheral` control picks which DMA peripheral drives the parallel WS2812 bus. They were always the same driver with a different bus backend; the merge makes that one card with a dropdown, offering only the peripherals the chip supports.

| Old registered type | New |
|---|---|
| `MultiPinLedDriver` | `ParallelLedDriver` + `peripheral` = `i80` (esp_lcd: LCD_CAM on S3/P4, I2S on classic) |
| `MoonLedDriver` | `ParallelLedDriver` + `peripheral` = `MoonI80` (own-GDMA below esp_lcd, LCD_CAM) |
| `MultiPinLedDriver` | `ParallelLedDriver` + `peripheral` = `i80` (esp_lcd: LCD_CAM on S3/P4, I2S on classic) — renamed again below |
| `MoonLedDriver` | `ParallelLedDriver` + `peripheral` = `MoonI80` (own-GDMA below esp_lcd, LCD_CAM) — renamed again below |
| `ParlioLedDriver` | `ParallelLedDriver` + `peripheral` = `Parlio` (P4) |

**Action: re-add the driver.** A persisted module whose type is one of the three old names no longer resolves (the type isn't registered), so the robust loader drops it on boot — the driver, and its pins/settings, vanish from the tree. Add a **Parallel LED** driver again, choose the `peripheral` your board uses (the same backend the old type named — see the table), and re-enter its `pins` / `ledsPerPin` plus whatever the chosen peripheral needs: `i80` has `clockPin`/`dcPin`, `MoonI80` has `clockPin` + the ring/expander controls, `Parlio` has no clock or DC pins at all. The web installer's board catalog already names the new type, so a fresh install or a catalog re-inject wires it correctly; only a device carrying an OLD persisted tree needs the manual re-add.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@ The alternatives weighed and rejected: **keep CRTP + one registered wrapper** (s

The migration cost is documented, not coded (per [ADR-0013](0013-no-migration-code-robust-persistence-plus-documented-breaks.md)): a field device's persisted `MoonLedDriver`/`MultiPinLedDriver`/`ParlioLedDriver` type no longer resolves, so the module drops on boot and the user re-adds a Parallel LED driver and picks the peripheral — a `MIGRATING.md` entry covers it, and the web-installer catalog names the new type so a fresh install is correct.

The design intent and staged plan are the [consolidation plan](../history/plans/); this ADR is the decision record.
The design intent and staged plan are the [consolidation plan](../history/plans/README.md); this ADR is the decision record.
13 changes: 13 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,19 @@ Abstractions are added when a concrete implementation needs them, not pre-design

**Platform boundary (hard rule).** All `#ifdef`, `#if defined`, platform-specific `#include`s, and hardware API calls live exclusively in `src/platform/`. Everything outside `src/platform/` compiles on every target without modification. Compile-time platform branching uses `if constexpr` on `platform_config.h` flags, never a preprocessor `#ifdef`. The boundary is enforced by [`moondeck/check/check_platform_boundary.py`](../moondeck/check/check_platform_boundary.py), a commit gate (see [CLAUDE.md § The Process](../CLAUDE.md#the-process)).

**The desktop build runs everything (hard rule).** Every module, effect and driver in the repo
links and runs on the host — the platform layer simply has no silicon behind the call. Where a
peripheral is absent the host *emulates* it rather than declaring itself incapable: the parallel
WS2812 buses are backed by heap buffers, `lcdLanes` / `parlioLanes` / `rmtTxChannels` report a
real chip's counts, and `hasLcdCam` is true. Code excluded from the host binary is code that
cannot be unit-tested, cannot be seen by any AST-based check, and only ever runs where it is
hardest to debug — which is exactly what the LED drivers were until they were linked here.

A capability flag therefore answers *"can this build exercise the path?"*, not *"is this real
hardware?"*. Where a flag must mean the latter (`hasLcdCam` gating the pin expander), that is a
deliberate, commented exception. Timing, wire protocol and pin state are NOT emulated: they need
silicon, and faking them would let a self-test report on hardware it never touched.

## Firmware vs deviceModel vs board

Three distinct things, kept distinct in the vocabulary:
Expand Down
Binary file added docs/assets/light/drivers/PanelCardDriver.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading