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
37 changes: 37 additions & 0 deletions docs/backlog/backlog-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,43 @@ Forward-looking to-build items for the **core / infrastructure** domain (`src/co
- **Live RMII Ethernet reconfigure** — runtime PHY/pin config shipped (`ethType` + pin controls in NetworkModule, per-board defaults in `deviceModels.json`, `platform::setEthConfig`/`ethInit` dispatch). W5500 (SPI) on S3 applies **live** — `ethStop()` tears down the SPI bus and `ethInit()` re-runs on the next `loop1s()` with no reboot. RMII (classic/P4 internal EMAC) still saves config and asks for a restart to apply, because the EMAC bring-up is fiddlier to hot-cycle cleanly. Make RMII live too: a hot `esp_eth_stop` + EMAC/netif teardown + re-init on config change, matching the W5500 path, so every interface honours the no-reboot principle.
- **Installer UX polish** — clear "Pre-release (beta)" warning on RC/latest picks, yank-by-asset-tag instead of yank-by-release-deletion.
- **Offer projectMM/MoonLight as a library** — a downstream sketch where another firmware/app consumes the light pipeline (or a subset) as an embeddable dependency rather than running the whole binary. `library.json` is already a PlatformIO *library* manifest, so the seed exists. When this is designed, give it a small public **identity surface**: one runtime constant the consumer reads (a `kProjectName`, likely a `ProjectInfo` bundle of name + version + url) that the network wire-strings (ArtNet/E1.31 source-name + CID), the UI banner, and any "About" string all *derive from* — the one place a consumer queries "what am I embedding." This is the genuine home for the name-centralisation that the rename ([rename-to-moonlight.md § Phase 1.3](rename-to-moonlight.md)) deliberately *didn't* do: the rename is a one-time sweep (a constant would just split it), but a library consumer references the identity ongoing and widely, which is the test a constant must pass. Build it *then*, against the real library API, not speculatively now.
- **ESP32-P4 panics with `Cache error` every few minutes, pre-existing** (2026-08-19): the bench
P4 (Waveshare P4-NANO, `esp32p4-eth`) reboots roughly every four minutes while IDLE, with
`Guru Meditation Error: Core 0 panic'ed (Cache error)`, sometimes followed by an
`Illegal instruction` and a `CHIP_LP_WDT_RESET` on the way down.

**What the device reports about its own restarts:** `bootReason` alternates between `PANIC` and
the watchdog. The serial shows why: the `Cache error` panic sometimes completes its dump and
reboots cleanly (`PANIC`), and sometimes the panic HANDLER itself then dies with an
`Illegal instruction` before it finishes, leaving the low-power watchdog to reset the chip
(`rst:0x10 CHIP_LP_WDT_RESET`, `W boot.esp32p4: CPU has been reset by WDT`). So a WDT boot reason
here is a SYMPTOM of the same fault, not a second one: nothing is hanging a task. Worth checking
`bootReason` over several restarts rather than one, because either value can appear.

**Not caused by MoonLive, and not a regression.** Established by two independent checks: the board
runs the DEFAULT module tree (GridLayout + NoiseEffect, no MoonLive module at all, so none of that
code executes), and a firmware built from a clean `main` crashes identically. The filesystem is
healthy throughout: LittleFS mounts, `/.config` lists, and writes succeed.

What is known about the fault site: `MEPC` resolves to `pxPortGetCoprocArea`
(`freertos/.../portable/riscv/port.c`) reached from `rtos_int_enter` (`portasm.S`), which is FreeRTOS's
RISC-V coprocessor-context save on INTERRUPT ENTRY. That is a symptom of something faulting inside
an ISR context rather than a bug in the kernel itself, and the P4 is the only RISC-V target with a
coprocessor, which is why no other board shows it. The prior art at
[Plan-20260718](../history/plans/Plan-20260718%20-%20MoonI80%20lapping-v2%20clock-oracle%20ring%20(shipped).md)
is a DIFFERENT cause with the same panic name (an ISR reading PSRAM while a flash write disabled
the cache, fixed with a `spi_flash_cache_enabled()` defer guard) and is worth re-reading first:
the same shape on another ISR would present exactly like this.

Next step is a decoded backtrace from the full panic dump rather than the register line, then
bisecting which ISR is live (audio, the LED driver, ethernet) by disabling each. Two hypotheses
were tested and falsified during the session that found it, so start from evidence.

A SEPARATE P4 boot loop, also found that session, WAS a real regression and is fixed: the MoonLive
engine had grown to 1440 bytes held by value in every scripted module, and `registerType`'s `T
probe` constructs each module on the main task's stack at boot. Re-indexing its seeded-member table
by member rather than by arena byte took it to 784 bytes and the board boots clean.

- **ESP32-P4 DHCP hostname not shown by the router (recheck later)** — the device sets its DHCP hostname (option 12 = `deviceName`, default `MM-XXXX`) in the `ETHERNET_EVENT_CONNECTED` handler, verified working on two boards: the S3 over WiFi (router shows `MM-70BC`) and the Olimex over RMII Ethernet (`MM-BD3C`) — the *same* `ethEventHandler` code path the P4 uses. Yet the bench P4 (Waveshare P4-NANO, RMII) still shows as blank/"Unknown" in the GL.iNet client list, while serial confirms `set_hostname` succeeds with no error. Two unconfirmed suspects, neither our logic: (1) the router holds a **sticky lease** for the P4's MAC and won't relearn the hostname until it fully expires (the per-client "forget" isn't exposed in this GL.iNet UI, and a plain reboot didn't clear it); (2) a P4-specific IDF netif quirk serializing option 12 differently on the newer P4 Ethernet path. Since the shared code path is proven on two other boards, this is not treated as a code bug. Recheck after the P4's lease naturally expires, or on a different router, before spending more on it.

### DevicesModule — interop plugins + the command half (discovery shipped)
Expand Down
53 changes: 42 additions & 11 deletions docs/backlog/backlog-light.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,17 +292,6 @@ The LED-driver increments **shipped**: increment 1 (RMT/WS2812B single-strand on

- **A scripted modifier that reshapes the grid** (2026-08-10). `ModifierBase::modifyLogicalSize` lets a modifier change the logical `width`/`height`/`depth` — a Multiply kaleidoscope grows the grid, a crop shrinks it — and a compiled modifier uses it. A SCRIPTED one cannot: system variables are read-only, so `MoonLiveModifier` writes the box in and never reads it back. Needs a writable system variable — the binding reads the slots after the script returns and reports the result through `modifyLogicalSize` — which is a new `SysVarKind` (or a mutable flag on `SysVar`) plus the read-back, not a new builtin. Until then a scripted modifier can fold coordinates but not resize the grid they live in.

- **Editing a script's CONTENTS through /api/file does not recompile it** (2026-08-14). A binding
caches `compiledHash_` and skips the compile while it is non-zero; the hash is cleared when the
script NAME changes (`onControlChanged`, `setScript`), but a write to `/moonlive/<same-name>` via
the File Manager leaves it set, so the layout keeps running the previous code until the name is
touched or the device reboots. `MoonLiveModifier` does not have this: it re-hashes the source on
every prepare and compares, which is the shape to copy.

The fix belongs at the filesystem seam rather than in the binding — a write under `/moonlive/`
invalidates whatever compiled from that path — so it is a small core/HTTP change, not a MoonLive
one. Pre-existing, not introduced by the stack-machine work.

- **MoonLive has no x86-64 backend — scripts do not run on Windows** (2026-08-14). The desktop
assembler (`moonlive_asm_host.cpp`) is arm64-only, so `MM_MOONLIVE_HAS_HOST_JIT` is 0 on x86-64
Windows, x86-64 Linux and Intel macOS. `compileSource` fails cleanly there and scripted modules
Expand All @@ -317,6 +306,48 @@ The LED-driver increments **shipped**: increment 1 (RMT/WS2812B single-strand on
`disasm.py --isa x86_64` should land with it, since no test executes emitted bytes for any backend
but the host's.

- **The compile-failure latch is not provable on the host** (2026-08-18). `MoonLiveScript::sync`
refuses to re-attempt a script that failed until its (name, content) changes. The latch exists for
a device-only reason: each attempt is two LittleFS reads (~5 ms on an S3), a layout is asked from
`lightCount()`/`placeLights()` as well as `prepare()`, and the pipeline asks repeatedly while
sizing a fixture, so the retries starve the render task until the watchdog resets the device.

On the host a re-read costs microseconds and nothing observable differs. Four test shapes were
tried and each still passed with the latch REMOVED ENTIRELY, so none was kept: what survives pins
only that a script fixed in place compiles without a rename, which is control-checked. Closing
this needs either a counting seam (a compile counter the test can read) or a platform fake whose
reads are observable. Until then the latch is protected by its comment and by hardware, not by a
test.

- **Catch device-backend operand defects on the host** (2026-08-18). Two array-codegen bugs shipped
to an S3 while all 1313 host tests stayed green, and both were control-checked: reintroducing
either one leaves the suite fully passing. `IrInst::c`/`d` are VREG fields the spill pass
renumbers, so a width parked there becomes a register number; and `sourcesOf` writes its sources
back POSITIONALLY, so reporting `kArg4` first shifts every real operand one place along. arm64's
register map absorbs both, which is exactly why the suite cannot see them.

Three test shapes were tried and deleted for failing their control run: emitted-bytes difference
tests (wrong bytes still differ from other wrong bytes) and a register-liveness walk (the index
satisfies it whatever happens to the value). What DOES work is asserting an assembler primitive
directly, as `unit_moonlive_codegen_xtensa.cpp` now does for `addImm`. The general form is
probably an IR-level invariant check rather than a bytes-level one: assert that no op reports a
fixed ABI vreg among its positional sources, and that non-register operands never occupy c/d.
That is a property of the IR the host CAN evaluate, unlike the emitted code.

- **Size the MoonLive control arena to the script** (2026-08-18). `kCtrlBytes` is a fixed 64 bytes
per engine (three engines per pipeline), so a script declaring one byte pays for 64 and one
wanting a 128-light array is refused. The arena is already `platform::alloc`'d, so the constant is
habit rather than necessity, and the member byte count is known at compile time.

What blocks it: the system variables sit ABOVE the script region at compile-time constant offsets
(`kSysWidth = kCtrlBytes + 0`), baked into emitted code as `LoadCtrl` immediates and cached as
slot POINTERS by the bindings. A script-sized region moves every one of them. Closing it means
putting the system variables BELOW the script region so their addresses stop depending on it,
which touches the sysvar table, the bindings and every emitted immediate. The hard ceiling stays
255 either way, since an arena offset is a `uint8_t` in the record, in `controlSlot` and in the
instruction. Until then, raising the constant is one edit and the failure is a clear compile
error naming the arena, so hitting it is visible rather than silent.

- **Drain MoonLive's `print()` through a queue** (2026-08-09). `print(v)` writes to serial directly, and an EFFECT script runs on the render tick — so a print inside one blocks the frame for as long as the UART takes. The burst cap bounds it (a handful of writes per compile, then a compare and a return), but bounded is not free, and `tick()` is annotated `MM_NONBLOCKING`.

**What it costs when it comes:** a small preallocated record queue the built-in writes into, drained from a housekeeping path through the existing platform output seam. The budget and the burst-spent message stay as they are; only where the bytes are written moves. Worth doing when a script is left with a print in it on a real fixture, which is the case the cap exists for.
Expand Down
Loading
Loading