From 5699c11d3e4b38797ca8363f33d11ed30655ea9d Mon Sep 17 00:00:00 2001 From: ewowi Date: Sun, 11 Oct 2026 14:19:58 +0200 Subject: [PATCH 01/12] Run MoonLight on the Teensy 4.1 with Ethernet and LED output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MoonLight now runs on a Teensy 4.1: the full UI over a W5500 Ethernet module, settings kept in flash, and WS2812B LEDs on up to 16 pins through the same Parallel LED driver the ESP32 uses. MoonDeck gains a Teensy tab that installs the SDK, builds, flashes and monitors, and a Repo tab now holds the repository checks. KPI: 256lights | Desktop:2220KB | tick:7/8/7/2/12/8/270/7/7/9us(FPS:142857/125000/142857/500000/83333/125000/3703/142857/142857/111111) | src:312(79393) | test:231(52579) | functions-over:330 Commit: src +1551/-147 lines | test +3223/-131 lines | code findings 1620 -> 1609 (complex function -2, deeply nested -1, duplicated block -8) | duplicated lines 3733 -> 3688 (-45) Core - src/platform/teensy: the Teensy platform layer on PJRC's core: time, heap, LittleFS on program flash, GPIO, identity, reset reason with CrashReport, Ethernet and sockets through QNEthernet's W5500 driver, mDNS with the _http and _wled records. - The W5500's SPI pins are refused while Ethernet runs, so a pin control can no longer take the network down. - eth_config.h holds the Ethernet PHY types and wiring the three platform configs each copied; loopback_verify.h holds the loopback pattern, argument check and bit verify the RMT, i80, MoonI80, Parlio and Teensy paths shared by copy. - MM_THREAD_LOCAL: per-thread state is plain static state on a target with one thread and no TLS. - teensy/: CMake toolchain over a pinned SDK, an XIP linker-script patch, and patches for QNEthernet (mDNS NSEC answer, bounded W5500 waits) and teensy_loader_cli (matches only its own devices on macOS). Light domain - GpioDmaPeripheral: a QTimer paces one eDMA channel through the crossbar, writing each slot widened to a 32-bit port word into GPIO1; the next frame encodes while the current one is on the wire; frames past 32767 slots run as a descriptor chain. - The loopback self-test captures edges with the cycle counter and passes bit for bit on the bench; it now refuses a receive pin the platform reserves. - LedPeripheral::busHeapBytes lets each bus report what it holds, so the driver's memory readout includes the wider copy. Scripts/MoonDeck - setup_teensy, build_teensy, flash_teensy and monitor_teensy, with a Teensy tab; the Desktop tab splits into Desktop and Repo, Repo first. - repo-health records the Teensy image against its program flash, adds teensy41 to the scenario matrix, and marks a firmware the release lacked as new. Tests - unit_GpioDmaPeripheral: lane slicing, frame sizing, the single-buffer contract, the memory readout, and the reserved receive pin. - host_bus.h holds the wire, frame-size, setup/release and loopbackTxPin helpers the four parallel-peripheral tests copied. - The live scenarios ran on the Teensy for the first time and record observed[teensy41]: 19 pass, and the 5 failures are on the plan's backlog. Docs/CI - The Teensy 4.1 hardware page with an example breadboard, the GPIO-DMA section in the drivers page, the Teensy section in building.md, the platform index naming the third target and the two shared headers. - The plan records phase 3 as built, what the comparison with another timer-and-DMA driver taught, the memory bound on lane length, the web installer, and the backlog for the next commit; two shipped backlog items are deleted and two new ones (Pi GPIO output, a Service calling other devices' APIs) are added. - Touched files leave the docgen list, except four ESP32 driver files and three peripheral tests holding about 400 older comment-width warnings, a sweep of their own. Reviews - ๐Ÿ‘พ Descriptor flags leaked between frames of different lengths โ†’ done: each descriptor starts from a cleared control register; verified at 512 lights on one lane and across the loopback. - ๐Ÿ‘พ frameTime showed the loop period โ†’ done: counted from the slots; the loopback keeps a measured wall time. - ๐Ÿ‘พ The Teensy app built with -Wall only โ†’ done: -Wall -Wextra -Werror, with the SDK headers as system includes. - ๐Ÿ‘พ platform_teensy.cpp over 1000 lines โ†’ done: the GPIO-DMA code moved to platform_teensy_gpio_dma.cpp. - ๐Ÿ‘พ __IMXRT1062__ in module_types.cpp โ†’ accepted: every peripheral there is gated by its chip's macro, and changing one would make two mechanisms. - ๐Ÿ‘พ Stale FlexIO comments in the Teensy platform config โ†’ done. - ๐Ÿ‘พ flash_teensy pointed at Input Monitoring โ†’ done: it names the cable and the program button. - ๐Ÿ‘พ Nothing refused the W5500's SPI pins โ†’ done, with a unit test that fails without the fix. - ๐Ÿ‘พ The socket pools were guessed โ†’ done: sized from lwIP's control-block counts, and a refused UDP bind is logged. - ๐Ÿ‘พ Two backlog items this branch completed โ†’ done: deleted. - ๐Ÿ‘พ Transmit ignored the driver's wait budget โ†’ done: it takes the budget, and gpioDmaWs2812Wait leaves the platform interface. - ๐Ÿ‘พ MM_THREAD_LOCAL beside ThreadSlot โ†’ accepted: ThreadSlot needs a claim and release a function-local flag lacks, and static is exact on one thread. - ๐Ÿ‘พ Nits โ†’ done, except the intrusive loopback's status on targets without a ride, deferred as a driver change of its own. Co-Authored-By: Claude Opus 5.5 --- docs/assets/hardware/teensy41-breadboard.svg | 1 + docs/how-to/building.md | 12 +- docs/moonmodules/light/drivers.md | 12 +- docs/moonmodules/platform/index.md | 9 +- docs/reference/hardware/teensy-4-1.md | 151 ++++ docs/reference/metrics/code.md | 37 +- docs/reference/metrics/docgen.md | 57 +- docs/reference/metrics/prose.md | 10 +- docs/reference/metrics/repo-health.json | 170 +++- docs/reference/metrics/repo-health.md | 137 ++- docs/work/future/backlog-core.md | 8 +- docs/work/future/backlog-light.md | 13 +- .../present/Plan-20261010 - Teensy 4.1.md | 123 +++ mkdocs.yml | 1 + moondeck/MoonDeck.md | 137 ++- moondeck/build/build_teensy.py | 46 ++ moondeck/build/flash_teensy.py | 64 ++ moondeck/build/setup_teensy.py | 194 +++++ moondeck/check/repo_health.py | 23 +- moondeck/moondeck.py | 4 +- moondeck/moondeck_config.json | 84 +- moondeck/moondeck_ui/app.js | 2 + moondeck/moondeck_ui/index.html | 16 +- moondeck/run/monitor_teensy.py | 63 ++ src/core/moonlive/moonlive_emit.h | 9 +- src/light/drivers/GpioDmaPeripheral.h | 67 ++ src/light/drivers/LedPeripheral.h | 7 +- src/light/drivers/ParallelLedDriver.h | 19 +- src/module_types.cpp | 32 +- src/platform/desktop/platform_config.h | 30 +- src/platform/desktop/platform_desktop.cpp | 25 + src/platform/esp32/platform_config.h | 38 +- src/platform/esp32/platform_esp32_i80.cpp | 9 +- .../esp32/platform_esp32_moon_i80.cpp | 9 +- src/platform/esp32/platform_esp32_parlio.cpp | 7 +- src/platform/esp32/platform_esp32_rmt.cpp | 51 +- src/platform/eth_config.h | 36 + src/platform/loopback_verify.h | 92 +++ src/platform/platform.h | 30 + src/platform/teensy/platform_config.h | 110 +++ src/platform/teensy/platform_teensy.cpp | 780 ++++++++++++++++++ .../teensy/platform_teensy_gpio_dma.cpp | 347 ++++++++ teensy/CMakeLists.txt | 111 +++ teensy/main.cpp | 22 + teensy/patches/QNEthernet-mdns-nsec.patch | 84 ++ .../QNEthernet-w5500-bounded-waits.patch | 109 +++ .../teensy_loader_cli-match-own-devices.patch | 45 + teensy/teensy41.cmake | 34 + test/CMakeLists.txt | 1 + test/python/test_repo_health_baseline.py | 10 +- ...lette_preset_changes_only_the_palette.json | 27 + ...ario_Firmware_reports_what_is_running.json | 54 ++ ...io_Network_hardware_reconfigures_live.json | 108 +++ ...ment_adds_an_effect_with_its_controls.json | 27 + .../scenarios/device/scenario_Aurora_fps.json | 189 +++++ .../device/scenario_Fluid_solver.json | 351 ++++++++ .../device/scenario_GridLayout_resize.json | 81 ++ test/scenarios/device/scenario_perf_full.json | 567 +++++++++++++ .../scenarios/device/scenario_perf_light.json | 162 ++++ .../scenario_peripheral_grid_sweep.json | 432 ++++++++++ .../device/scenario_peripheral_switch.json | 162 ++++ ...cenario_Drivers_output_and_brightness.json | 27 + .../scenario_Effects_swap_while_running.json | 54 ++ ...cts_teardown_under_a_running_pipeline.json | 189 +++++ .../scenario_Layers_stack_and_blend_live.json | 216 +++++ ...nario_Layouts_resize_reallocates_live.json | 81 ++ ...cenario_Modifiers_reshape_the_mapping.json | 189 +++++ ..._Palettes_a_script_drives_the_effects.json | 54 ++ test/unit/light/host_bus.h | 75 +- test/unit/light/unit_GpioDmaPeripheral.cpp | 151 ++++ test/unit/light/unit_I80Peripheral.cpp | 65 +- test/unit/light/unit_MoonI80Peripheral.cpp | 11 +- test/unit/light/unit_ParlioPeripheral.cpp | 71 +- 73 files changed, 6310 insertions(+), 521 deletions(-) create mode 100644 docs/assets/hardware/teensy41-breadboard.svg create mode 100644 docs/reference/hardware/teensy-4-1.md create mode 100644 docs/work/present/Plan-20261010 - Teensy 4.1.md create mode 100644 moondeck/build/build_teensy.py create mode 100644 moondeck/build/flash_teensy.py create mode 100644 moondeck/build/setup_teensy.py create mode 100644 moondeck/run/monitor_teensy.py create mode 100644 src/light/drivers/GpioDmaPeripheral.h create mode 100644 src/platform/eth_config.h create mode 100644 src/platform/loopback_verify.h create mode 100644 src/platform/teensy/platform_config.h create mode 100644 src/platform/teensy/platform_teensy.cpp create mode 100644 src/platform/teensy/platform_teensy_gpio_dma.cpp create mode 100644 teensy/CMakeLists.txt create mode 100644 teensy/main.cpp create mode 100644 teensy/patches/QNEthernet-mdns-nsec.patch create mode 100644 teensy/patches/QNEthernet-w5500-bounded-waits.patch create mode 100644 teensy/patches/teensy_loader_cli-match-own-devices.patch create mode 100644 teensy/teensy41.cmake create mode 100644 test/unit/light/unit_GpioDmaPeripheral.cpp diff --git a/docs/assets/hardware/teensy41-breadboard.svg b/docs/assets/hardware/teensy41-breadboard.svg new file mode 100644 index 000000000..5aed269e1 --- /dev/null +++ b/docs/assets/hardware/teensy41-breadboard.svg @@ -0,0 +1 @@ +JIHGFEDCBA151015202530354045505560โˆ’+ offโˆ’+ 3.3 V (Teensy)MB102both rail jumpersoffpower in: USBor 7โ€“12 V DC5 V outUSBTeensy 4.1 (top view)VINGNDGND03V31232223214205196187178169151014111312GND3V3412440253926382737283629353034313332RJ45W5500MPU6050 (GY-521)VCCGNDSCLSDAXDAXCLAD0INTINMP441L/RWSSCKSDVDDGNDVCCOUTGNDโ†’ PIR cableGNDGNDGND3V3MOSI3V3SCLKNCSCSnRSTINTMISO+5VGNDDINWS2812B strip (data direction โ†’), +5V from the Teensy’s VIN,the USB 5 V: a short strip at brightness 20 or lessbridge if the rail is split heremagenta at pins 22โ€“23 (J16 to J15): the loopback jumper, left in placered dashed, MB102 5 V to VIN (I12): standalone only, plugged in with the Teensy’s USB unpluggedthe INMP441’s pin order varies by maker: follow its silkscreen \ No newline at end of file diff --git a/docs/how-to/building.md b/docs/how-to/building.md index 883fb3da1..efb21b3f5 100644 --- a/docs/how-to/building.md +++ b/docs/how-to/building.md @@ -334,9 +334,17 @@ older layout adopts a new one only through a full serial flash (see the note und ## Teensy -Teensy 4.x is in the supported target list. Buffers and pipeline configuration scale to 1 MB of internal RAM; OctoWS2811 gives excellent DMA-based LED output. Ethernet is built in on Teensy 4.1 and optional on 4.0. +The `teensy41` firmware runs on a Teensy 4.1, with a W5500 module for Ethernet; pins, wiring and power are on [the Teensy 4.1 page](../reference/hardware/teensy-4-1.md). +It builds with CMake over an SDK at pinned versions, and MoonDeck's [Teensy tab](../../moondeck/MoonDeck.md#teensy-tab) wraps every step: + +```bash +uv run moondeck/build/setup_teensy.py # once: the SDK into ~/teensy +uv run moondeck/build/build_teensy.py # build/teensy41/MoonLight.hex +uv run moondeck/build/flash_teensy.py # over USB, no button press +uv run moondeck/run/monitor_teensy.py # USB serial +``` -Build flow is via the root `CMakeLists.txt` with a Teensy toolchain file. The platform layer for Teensy is added when the first hardware target is wired in. +Settings live in 1 MB of the program flash and survive a flash. Firmware updates go over USB. ## Pre-compilation steps diff --git a/docs/moonmodules/light/drivers.md b/docs/moonmodules/light/drivers.md index 025731725..11ba76a8e 100644 --- a/docs/moonmodules/light/drivers.md +++ b/docs/moonmodules/light/drivers.md @@ -75,7 +75,7 @@ Plus the [shared controls](#shared-driver-controls) above: Tests: [shared + peripherals](../../reference/tests/unit-tests.md#parallelleddriver) -Detail: [technical](moxygen/ParallelLedDriver.md) ยท peripherals: [i80](moxygen/I80Peripheral.md) ยท [MoonI80](moxygen/MoonI80Peripheral.md) ยท [Parlio](moxygen/ParlioPeripheral.md) +Detail: [technical](moxygen/ParallelLedDriver.md) ยท peripherals: [i80](moxygen/I80Peripheral.md) ยท [MoonI80](moxygen/MoonI80Peripheral.md) ยท [Parlio](moxygen/ParlioPeripheral.md) ยท [GPIO-DMA](moxygen/GpioDmaPeripheral.md) @@ -244,7 +244,7 @@ Detail: [technical](moxygen/RtspDriver.md) ยท [the RTP packetiser](moxygen/RtpH2 ## Shared, details -**A `fixture` reference survives some changes and not others.** The driver holds the profile's stable id at runtime, so **reordering** profiles never disturbs it, and the reference **survives a reboot** because the profile's name is persisted and re-resolved on load. The caveat is **renaming**: within a session the id keeps the link, but after a reboot a renamed profile no longer matches the persisted name and the driver falls back to the default. Re-pick it if you rename a profile a driver uses. +**A `fixture` reference survives some changes and not others.** The driver holds the profile's stable id at runtime, so **reordering** profiles never disturbs it, and the reference **survives a reboot** because the profile's name is persisted and re-resolved on load. The caveat is **renaming**: within a session the id keeps the link, but after a reboot a renamed profile misses the persisted name and the driver falls back to the default. Re-pick it if you rename a profile a driver uses. **`start` and `count` are how several drivers share one buffer.** Blank `count` drives every light; a number drives only that slice. An onboard status LED takes `start 0, count 1` while the main strip runs from `start 1`, both reading the same buffer. @@ -260,6 +260,8 @@ Detail: [technical](moxygen/RtspDriver.md) ยท [the RTP packetiser](moxygen/RtpH2 **Whose work this is.** The clockless I2S / RMT / Parlio techniques come from [hpwit (Yves Bazin)](https://github.com/hpwit), whose work is why a single board can drive dozens of parallel strands at all ([analysis](../../work/future/leddriver-analysis-top-down.md)), on WS2812B prior art from FastLED and WLED. +**`GPIO-DMA` on the Teensy 4.1.** A timer ticks every 373 ns, and on each tick one DMA channel writes the next slot of the encoded frame into the GPIO port, so the strands change together and the CPU is free while the frame clocks out. The encoding is the one every peripheral here shares: three slots per WS2812 bit. The port takes only 32-bit writes, so the platform widens each slot into a port word before the transfer. The DMA reads that wider copy, so the next frame encodes while the current one is still on the wire: the overlap `doubleBuffer` buys elsewhere, without a second buffer, and with the control having no effect. A 256-light strand clocks out in 7.2 ms. The lanes are the 16 pins of one GPIO port, in any order. The driver is MoonLight's own; OctoWS2811 and ObjectFLED were the prior art for the timer and DMA setup. Wiring, power and an example breadboard: [the Teensy 4.1 page](../../reference/hardware/teensy-4-1.md). + **Which one do I want?** | Want | Use | @@ -268,6 +270,7 @@ Detail: [technical](moxygen/RtspDriver.md) ยท [the RTP packetiser](moxygen/RtpH2 | Many strands, up to 16 | Parallel LED on **`i80`**, the proven scale path | | Up to 16 strands on a **P4** | Parallel LED on **`Parlio`**, which needs no clock pin | | More lights than one DMA buffer holds, or more strands than you have GPIOs | Parallel LED on **`MoonI80`** | +| Up to 16 strands on a **Teensy 4.1** | Parallel LED on **`GPIO-DMA`**, the one peripheral there | Start with `i80`. Choose `MoonI80` only when you hit one of those two limits: it drives the same pins the same way, streams the frame instead of holding it whole, and can drive a 74HCT595 pin expander so 6 pins reach 48 strands. Both are `peripheral` choices on the one Parallel LED driver, so switching is a control change on one board with no reflash. @@ -278,6 +281,7 @@ Start with `i80`. Choose `MoonI80` only when you hit one of those two limits: it | **`i80`** | S3, P4, S31, and the classic ESP32 | 1 to 16 | | **`MoonI80`** | S3, P4, S31 | 1 to 16, or 8 per pin through an expander | | **`Parlio`** | P4 | 1 to 16 | +| **`GPIO-DMA`** | Teensy 4.1 | 1 to 16, on pins 0, 1, 14 to 27 and 38 to 41 | RMT is its own driver rather than a `peripheral`, and runs on any ESP32: one strand per RMT channel, which is 8 on the classic and 4 on an S3 or P4. @@ -285,7 +289,7 @@ RMT is its own driver rather than a `peripheral`, and runs on any ESP32: one str **Lane, pin, strand.** A lane is one bus data line, a strand is one chain of LEDs. Wired directly, one pin is one lane is one strand. Through an expander each data pin feeds one '595 and fans out to 8 strands, so 8 data pins reach the driver's ceiling of 64. -How the frame is built, the DMA each peripheral programs, and the expert `ring*` tuning are on the API pages: [Parallel LED](moxygen/ParallelLedDriver.md), [i80](moxygen/I80Peripheral.md), [MoonI80](moxygen/MoonI80Peripheral.md), [Parlio](moxygen/ParlioPeripheral.md), [RMT](moxygen/RmtLedDriver.md), [slot encoder](moxygen/ParallelSlots.md). +How the frame is built, the DMA each peripheral programs, and the expert `ring*` tuning are on the API pages: [Parallel LED](moxygen/ParallelLedDriver.md), [i80](moxygen/I80Peripheral.md), [MoonI80](moxygen/MoonI80Peripheral.md), [Parlio](moxygen/ParlioPeripheral.md), [GPIO-DMA](moxygen/GpioDmaPeripheral.md), [RMT](moxygen/RmtLedDriver.md), [slot encoder](moxygen/ParallelSlots.md). @@ -341,7 +345,7 @@ An encoder your ffmpeg lacks starts and exits immediately; the status then reads **The bitrate is derived, not a setting.** It follows from the grid size and `targetFps` at about 0.1 bits per pixel per frame, which puts a 512x512 wall at 30 fps near 800 kbit; a 128x128 lands under the 500 kbit floor the derivation clamps to. `targetFps` is the knob for bandwidth, and the better trade for LED content: fewer frames rather than a blockier picture. -**Where the segments live.** On desktop, the transient `/.hls/` directory, served at `/hls/` and excluded from config backups. Large grids trade framerate, the render loop being single-threaded: 512x512 streams smoothly, TV-native resolutions do not yet. +**Where the segments live.** On desktop, the transient `/.hls/` directory, served at `/hls/` and excluded from config backups. Large grids trade framerate, the render loop being single-threaded: 512x512 streams smoothly, TV-native resolutions stutter. diff --git a/docs/moonmodules/platform/index.md b/docs/moonmodules/platform/index.md index b3058d12d..e32daefff 100644 --- a/docs/moonmodules/platform/index.md +++ b/docs/moonmodules/platform/index.md @@ -4,7 +4,7 @@ The one interface every module reaches hardware through. Core and the light doma Why the boundary is drawn here, and what is allowed to cross it, is [MoonCore's platform abstraction](../../explanation/architecture/mooncore.md#platform-abstraction). This page is the reference half: what the interface offers, and where each target implements it. -`platform.h` declares the interface; `desktop/` and `esp32/` implement it. A module that needs something the interface does not offer gets a new function here rather than an `#ifdef` at the call site, which is what keeps the domains target-blind. +`platform.h` declares the interface; `desktop/`, `esp32/` and `teensy/` implement it. A module that needs something the interface does not offer gets a new function here rather than an `#ifdef` at the call site, which is what keeps the domains target-blind. ## Time @@ -54,9 +54,9 @@ NDI and HLS. Present where the target can carry them and absent elsewhere, which Detail: [technical](moxygen/platform.md) -## Two implementations, one contract +## Three implementations, one contract -`src/platform/desktop/` and `src/platform/esp32/` implement the same declarations. The desktop one exists so the whole system runs, is tested and is developed without hardware, which is what makes the desktop-first rule possible. +`src/platform/desktop/`, `src/platform/esp32/` and `src/platform/teensy/` implement the same declarations. The desktop one exists so the whole system runs, is tested and is developed without hardware, which is what makes the desktop-first rule possible. The MoonLive assemblers live here too, one per instruction set, because emitting machine code is the most target-specific thing the system does. The engine and its lowering stay in core; only the encoding is per target. @@ -66,7 +66,8 @@ The MoonLive assemblers live here too, one per instruction set, because emitting | [moonlive_asm_riscv](moxygen/moonlive_asm_riscv.md) | ESP32-P4 | | [moonlive_asm_host](moxygen/moonlive_asm_host.md) | desktop arm64 and x86-64 | -Each satisfies the assembler contract [the lowering](../light/moonlive.md) names, so adding an instruction set is a new file behind an unchanged IR. Per-target build settings are in each half's own `platform_config`: [desktop](moxygen/desktop_platform_config.md) and [ESP32](moxygen/esp32_platform_config.md), the latter carrying the per-chip capability table. +Each satisfies the assembler contract [the lowering](../light/moonlive.md) names, so adding an instruction set is a new file behind an unchanged IR. Per-target build settings are in each target's own `platform_config`: [desktop](moxygen/desktop_platform_config.md), [ESP32](moxygen/esp32_platform_config.md), which carries the per-chip capability table, and [Teensy 4.1](moxygen/teensy_platform_config.md). +Two headers serve every target: [the Ethernet wiring](moxygen/eth_config.md) each config's default is written in, and [the loopback bit check](moxygen/loopback_verify.md) every LED self-test ends in. ## What does not belong here diff --git a/docs/reference/hardware/teensy-4-1.md b/docs/reference/hardware/teensy-4-1.md new file mode 100644 index 000000000..63b67668c --- /dev/null +++ b/docs/reference/hardware/teensy-4-1.md @@ -0,0 +1,151 @@ +# Teensy 4.1 hardware reference + +Pins, power and wiring for running MoonLight on a PJRC **Teensy 4.1**. +A WIZnet W5500 module gives it Ethernet, and the board's GPIO drives WS2812B LEDs by DMA. +The firmware is `teensy41`, built and flashed from MoonDeck's [Teensy tab](../../../moondeck/MoonDeck.md#teensy-tab). + +**Sources** +- Board: +- W5500 module pin map (WIZ850io layout): +- Network library: QNEthernet, +- Prior art for the LED output: OctoWS2811, , and ObjectFLED, + +## The board + +- NXP i.MX RT1062: one Cortex-M7 at 600 MHz, no WiFi, one core. +- 1 MB RAM: 512 KB of tightly coupled memory, shared between code and the stack, and 512 KB of OCRAM, which holds the heap. +- 8 MB flash, with 1 MB of it holding MoonLight's settings (LittleFS), kept across power cycles and USB uploads. +- Pads for 8 or 16 MB PSRAM, empty on the boards MoonLight runs on so far. +- A microSD slot (pins 42 to 47) and USB host pins. +- The **Teensy 4.1-NE** has no Ethernet chip, so the W5500 below gives it a network. + A regular Teensy 4.1 has one behind PJRC's Ethernet kit; `teensy41` selects the W5500 at build time. + +## Ethernet: a W5500 on SPI + +The W5500 runs on the SPI port at 30 MHz, at about 1.6 MB/s. +The module below is the WIZ850io layout: two rows of six pins, one on each side of the RJ45 jack. +Seen from the jack side with the cable opening pointing away, each row starts at the cable end. +The left row (J1) reads GND, GND, MOSI, SCLK, SCSn, INTn; the right row (J2) reads GND, 3.3V, 3.3V, NC, RSTn, MISO. + +| Module pin | Teensy 4.1 pin | Note | +|---|---|---| +| 3.3V | 3.3V | never 5 V: neither the Teensy's pins nor the W5500 are 5 V tolerant | +| GND | GND | one of the three is enough | +| MOSI | 11 | | +| MISO | 12 | | +| SCLK | 13 | pin 13 also drives the orange LED, which flickers with traffic | +| SCSn | 10 | chip select | +| RSTn | 9 | optional, open by default | +| INTn, NC | open | the driver polls the chip | + +Keep the SPI wires under 10 cm. +The device opens by name (`http://MM-Teensy41.local`) through mDNS. + +## LED outputs + +Up to 16 WS2812B lanes, on any of the pins of GPIO port 6: **0, 1, 14 to 27 and 38 to 41**, in any order. +The [Parallel LED](../../moonmodules/light/drivers.md#parallel-led-details) driver's `GPIO-DMA` peripheral drives them. +One DMA channel, paced by a timer, writes each 373 ns slot into the port, so the CPU is free while a frame clocks out. +A frame of 256 lights on a lane takes 7.2 ms (a 138 fps ceiling). +The data runs at 3.3 V, which a short wire to the first LED tolerates. + +## Loopback self-test + +The ParallelLedDriver's loopback test sends a known frame and checks every bit it reads back, which proves the DMA, the timer and the WS2812 timing. +A jumper from a lane-capable pin (`loopbackTxPin`) to any input pin (`loopbackRxPin`) can stay in place: with the test sending on its own pin, the LEDs never receive the test pattern. + +## Power + +- **USB** powers the board during development and is the data path for flashing and serial. +- **VIN** takes 3.6 to 5.5 V. On the board it is joined to the USB's 5 V by a trace between two pads labeled VIN and VUSB. + So while USB powers the board, VIN carries the USB 5 V out, and an external supply on VIN must never meet a plugged-in USB cable. + Cutting that trace (a solder blob restores it) separates the two: USB then carries only data and VIN always powers the board. +- **3.3V** supplies 250 mA at most; the W5500 takes about 130 mA of it. +- **LEDs**: a small panel can take its 5 V from VIN while USB powers the board. + USB gives 500 mA in all, and the Teensy with the W5500 takes about 230 mA. + The remaining 270 mA covers an 8ร—8 WS2812B panel at brightness 20 or less. + The 3.3 V pin is no supply for LEDs, below the WS2812B's 3.5 V minimum. A larger installation gets its own 5 V supply, its GND joined to the board's. + +## An example setup on a breadboard + +One way to set it up, on an 830-point breadboard with an MB102 power module; any wiring that keeps the pins above works as well. +One USB cable powers everything during development: the Teensy, the W5500 through the Teensy's 3.3 V pin on the bottom rail, and a short LED strip through VIN. +The MB102's rail jumpers stay off; it powers the device through VIN only when it runs standalone, without USB. + +![A Teensy 4.1 and a W5500 module on a breadboard, with an LED strip, an INMP441 microphone, an MPU6050 and a PIR connector](../../assets/hardware/teensy41-breadboard.svg) + +Columns are the numbers printed along the top edge; both boards straddle the center gap. +The Teensy's USB end faces left, with room before the MB102 for the plug. +The USB and Ethernet cables then leave at opposite ends. +Columns 37 to 48 hold the sensors, which keeps the SPI wires near 7 cm. + +| Part | Where | +|---|---| +| Teensy 4.1 | rows D and H, columns 12 to 35, USB end at column 12 | +| Teensy pins | row D: GND, 0 to 12, 3V3; row H: VIN, GND, 3V3, 23 to 13 | +| W5500 | J1 in row I and J2 in row C, pin 1 of each at column 55 and pin 6 at column 50, the RJ45 opening facing right | +| Sensors | columns 37 to 48, below | +| LED strip | above the board | +| MB102 | both rail jumpers off | + +| Wire | From | To | +|---|---|---| +| 3.3 V | A26 (Teensy 3V3) | bottom rail + | +| 3.3 V | A54 (W5500 3.3V) | bottom rail + | +| GND | A12 (Teensy GND) and A55 (W5500 GND) | bottom rail โˆ’ | +| SCLK | J52 | J25 (pin 13) | +| MOSI | J53 | B24 (pin 11), down column 49 | +| SCSn | J51 | A23 (pin 10), down column 49 | +| MISO | B50 | B25 (pin 12) | +| LED data | J20 (pin 18) | the strip's DIN | +| LED GND | J13 (Teensy GND) | the strip's GND | +| LED 5 V | J12 (VIN) | the strip's +5V | +| loopback | J16 (pin 22) | J15 (pin 23), with `loopbackTxPin` 22 and `loopbackRxPin` 23 | +| VIN, standalone only | the MB102's 5 V output | I12 (VIN), with USB unplugged | + +1. With everything unpowered, seat the boards, then the wires, GND first. +2. Check whether the bottom rail is split at column 30, where its printed lines break. + If it is, bridge both โˆ’ and + across the gap: the Teensy feeds the rail at column 26 and the W5500 draws at column 54. +3. Keep both MB102 rail jumpers off, or the MB102 and the Teensy drive one 3.3 V net from two regulators. +4. Plug in USB: the W5500's link LEDs light, the device boots, and the strip lights. + +### With sensors + +The free columns take a microphone, an IMU and a PIR. +Here that is an INMP441, an MPU6050 on a GY-521 breakout, and a mini PIR such as the AM312, which runs on 3.3 V. +Two wires carry the bottom rail's 3.3 V and GND to the top rail, which the MB102 leaves unfed with its top jumper off. +The `teensy41` firmware drives no I2S or I2C peripheral at this point; this wiring is ready for them. + +| Part | Where | +|---|---| +| MPU6050 (GY-521) | VCC, GND, SCL, SDA, XDA, XCL, AD0, INT in row J, columns 38 to 45, its body over the top rail | +| INMP441 | six pins in row A, columns 38 to 43, its body over the bottom rail | +| INMP441 pin order | varies by maker: follow its silkscreen | +| PIR connector | VCC, OUT, GND in row A, columns 45 to 47, the sensor on a cable | + +| Wire | From | To | +|---|---|---| +| top rail 3.3 V and GND | bottom rail + and โˆ’ | top rail + and โˆ’, at columns 35 and 36 | +| MPU6050 VCC, GND | I38, I39 | top rail +, โˆ’ | +| MPU6050 SCL | H40 | B27 (pin 24) | +| MPU6050 SDA | G41 | C28 (pin 25) | +| INMP441 L/R | B38 | bottom rail โˆ’, the left channel | +| INMP441 WS | C39 | I18 (pin 20) | +| INMP441 SCK | D40 | J17 (pin 21) | +| INMP441 SD | E41 | C21 (pin 8) | +| INMP441 VDD, GND | B42, B43 | bottom rail +, โˆ’ | +| PIR VCC, GND | B45, B47 | bottom rail +, โˆ’ | +| PIR OUT | C46 | I27 (pin 41) | + +On the bench board of this example, pin 19 disturbed the W5500 when it toggled, so the example leaves it unused. +Every other lane pin tested clean. + +## Possible deployments + +These boards put a Teensy, LED outputs and Ethernet in one unit. +Each routes its LED outputs to OctoWS2811's pin set, which reaches beyond port 6. +So MoonLight drives them once its GPIO-DMA backend writes to any pin, as OctoWS2811 does. + +- **OctoWS2811 adaptor** (PJRC, ; SparkFun PRT-28372): eight buffered 5 V outputs over two RJ45 jacks, sized for a CableGuard CG-500 outdoor enclosure. +- **TeensyLED** (open hardware, ): a 32-output Teensy 4.1 pixel controller with on-board Ethernet and optional RS-485. +- **Teensy 4.1 Pixel Pusher** (Amos Computing, ): 32 outputs on the Teensy's own Ethernet, made for CableGuard enclosures; listed as retired. diff --git a/docs/reference/metrics/code.md b/docs/reference/metrics/code.md index e4c89ea89..bd260ede2 100644 --- a/docs/reference/metrics/code.md +++ b/docs/reference/metrics/code.md @@ -4,16 +4,16 @@ Generated by [`moondeck/check/check_code.py`](../../../moondeck/check/check_code Every function, block and file over a limit the [coding standards](../../contributing/coding-standards.md#static-checks) set. Current state only: the trend is this file's git history. The list only shrinks, per rule and in total, and a touched file clears its own rows. -**1620 finding(s)** across 318 file(s). 2.88% of all lines are duplicated, 3733 lines. +**1609 finding(s)** across 319 file(s). 2.82% of all lines are duplicated, 3688 lines. ## By rule | Rule | Findings | |---|---:| -| duplicated block | 768 | -| complex function | 283 | +| duplicated block | 760 | +| complex function | 281 | | blocking call on the render path | 189 | -| deeply nested | 113 | +| deeply nested | 112 | | module type in the UI | 71 | | long function | 49 | | large stack frame | 38 | @@ -44,10 +44,10 @@ Each rule is a code smell with a named move from the refactoring catalog, the ta | Area | Findings | |---|---:| -| `test` | 570 | +| `test` | 562 | | `light` | 421 | | `core` | 351 | -| `platform` | 173 | +| `platform` | 170 | | `ui` | 74 | | `moondeck` | 24 | | `src` | 5 | @@ -69,7 +69,6 @@ The next function worth simplifying is at the top of each file's list. | `src/light/drivers/PreviewDriver.h` | 27 | `PreviewDriver::buildCoordTable`, complex function 37 | | `test/unit/core/unit_FilesystemModule_persistence.cpp` | 24 | `lines 181-194, also in test/unit/core/unit_FilesystemModule_persistence.cpp:146`, duplicated block 14 | | `src/light/drivers/RtspDriver.h` | 23 | `RtspDriver::serveControl, line 185`, large stack frame 1680 | -| `test/unit/light/unit_I80Peripheral.cpp` | 23 | `lines 151-170, also in test/unit/light/unit_ParlioPeripheral.cpp:214`, duplicated block 20 | | `test/unit/light/unit_PanelCardDriver.cpp` | 22 | `lines 22-39, also in test/unit/light/unit_NdiDriver.cpp:29`, duplicated block 18 | | `test/unit/light/unit_RainbowEffect.cpp` | 22 | `lines 69-85, also in test/unit/light/unit_ParticlesEffect.cpp:32`, duplicated block 17 | | `src/light/powerfunctions/draw.h` | 21 | `draw::scroll`, complex function 28 | @@ -78,6 +77,7 @@ The next function worth simplifying is at the top of each file's list. | `src/light/drivers/HueDriver.h` | 19 | `HueDriver::pollPairing, line 297`, large stack frame 1184 | | `src/platform/esp32/platform_esp32.cpp` | 19 | `platform::httpRequest`, complex function 30 | | `test/unit/core/unit_HttpServerModule_apply.cpp` | 19 | `lines 536-547, also in test/unit/core/unit_HttpServerModule_apply.cpp:122`, duplicated block 12 | +| `test/unit/light/unit_I80Peripheral.cpp` | 19 | `lines 132-151, also in test/unit/light/unit_ParlioPeripheral.cpp:197`, duplicated block 20 | | `src/platform/esp32/platform_esp32_moon_i80.cpp` | 18 | `platform::createRingState`, complex function 32 | | `test/unit/core/unit_PinsModule.cpp` | 18 | `lines 46-57, also in test/unit/light/unit_Hub75Driver_pins.cpp:66`, duplicated block 12 | | `src/core/services/AudioService.h` | 17 | `AudioService in 49 files`, name spread 49 | @@ -85,7 +85,6 @@ The next function worth simplifying is at the top of each file's list. | `src/platform/esp32/platform_esp32_ota.cpp` | 15 | `platform::otaWriteMoonBase`, complex function 33 | | `test/unit/light/unit_PaintBrushEffect.cpp` | 15 | `lines 44-61, also in test/unit/light/unit_GEQ3DEffect.cpp:58`, duplicated block 18 | | `test/unit/light/unit_SolidEffect.cpp` | 15 | `lines 9-22, also in test/unit/light/unit_FixedRectangleEffect.cpp:90`, duplicated block 14 | -| `test/unit/light/unit_ParlioPeripheral.cpp` | 14 | `lines 214-233, also in test/unit/light/unit_I80Peripheral.cpp:151`, duplicated block 20 | | `src/core/module/Control.cpp` | 13 | `applyControlValue`, complex function 74 | | `src/light/layers/Layer.h` | 13 | `Layer in 130 files`, name spread 130 | | `test/unit/light/unit_HlsDriver.cpp` | 13 | `lines 30-47, also in test/unit/light/unit_PanelCardDriver.cpp:22`, duplicated block 18 | @@ -94,9 +93,9 @@ The next function worth simplifying is at the top of each file's list. | `src/light/drivers/HlsDriver.h` | 12 | `lines 105-117, also in src/light/drivers/RtspDriver.h:92`, duplicated block 13 | | `src/light/moonlive/MoonLiveBuiltins_light.h` | 12 | `moonlive::mm_light_escape`, complex function 13 | | `test/unit/light/unit_RubiksCubeEffect.cpp` | 12 | `lines 60-78, also in test/unit/light/unit_RubiksCubeEffect.cpp:28`, duplicated block 19 | -| `src/platform/esp32/platform_esp32_i80.cpp` | 11 | `platform::createState`, complex function 27 | +| `test/unit/light/unit_ParlioPeripheral.cpp` | 11 | `lines 197-216, also in test/unit/light/unit_I80Peripheral.cpp:132`, duplicated block 20 | | `test/unit/light/unit_StarFieldEffect.cpp` | 11 | `lines 88-103, also in test/unit/light/unit_StarFieldEffect.cpp:13`, duplicated block 16 | -| `src/light/drivers/Drivers.h` | 10 | `Drivers in 67 files`, name spread 67 | +| `src/light/drivers/Drivers.h` | 10 | `Drivers in 68 files`, name spread 68 | | `src/light/effects/GEQ3DEffect.h` | 10 | `lines 140-150, also in src/light/effects/GEQ3DEffect.h:113`, duplicated block 11 | | `src/light/effects/GameOfLifeEffect.h` | 10 | `GameOfLifeEffect::evolveAutomaton`, complex function 67 | | `test/unit/light/unit_BlendMap.cpp` | 10 | `lines 283-289, also in test/unit/light/unit_BlendMap.cpp:212`, duplicated block 7 | @@ -105,12 +104,12 @@ The next function worth simplifying is at the top of each file's list. | `src/core/services/OscModule.h` | 9 | `OscModule::handle`, deeply nested 5 | | `src/core/system/ControlModule.h` | 9 | `ControlModule::moveListRow`, complex function 15 | | `src/light/drivers/PanelCardDriver.h` | 9 | `lines 187-195, also in src/light/drivers/NetworkSendDriver.h:210`, duplicated block 9 | +| `src/platform/esp32/platform_esp32_i80.cpp` | 9 | `platform::createState`, complex function 27 | | `test/unit/light/unit_ParallelLedDriver_swap.cpp` | 9 | `lines 176-184, also in test/unit/light/unit_ParallelLedDriver_swap.cpp:112`, duplicated block 9 | | `src/core/system/WiFiModule.h` | 8 | `lines 352-359, also in src/light/drivers/FixtureProfilesModule.h:245`, duplicated block 8 | | `src/light/effects/FixedPointEffect.h` | 8 | `FixedPointEffect::drawBoids`, complex function 19 | | `src/light/effects/FlyingToastersEffect.h` | 8 | `lines 60-69, also in src/light/effects/FlyingToastersEffect.h:40`, duplicated block 10 | | `src/light/effects/SpaceInvadersEffect.h` | 8 | `lines 436-444, also in src/light/effects/SpriteCast.h:59`, duplicated block 9 | -| `src/platform/esp32/platform_esp32_rmt.cpp` | 8 | `platform::detail::captureAndVerifyFrame`, complex function 39 | | `test/unit/light/unit_GEQEffect.cpp` | 8 | `lines 159-177, also in test/unit/light/unit_BlurzEffect.cpp:137`, duplicated block 19 | | `test/unit/light/unit_LissajousEffect.cpp` | 8 | `lines 81-97, also in test/unit/light/unit_LissajousEffect.cpp:13`, duplicated block 17 | | `test/unit/light/unit_MovingHeadEffect.cpp` | 8 | `lines 107-118, also in test/unit/light/unit_MovingHeadEffect.cpp:45`, duplicated block 12 | @@ -122,7 +121,7 @@ The next function worth simplifying is at the top of each file's list. | `src/core/util/JsonUtil.h` | 7 | `json::parseString`, complex function 34 | | `src/light/effects/FishTankEffect.h` | 7 | `lines 244-258, also in src/light/effects/PacmanEffect.h:243`, duplicated block 15 | | `src/platform/esp32/platform_esp32_hub75.cpp` | 7 | `platform::hub75Init`, complex function 32 | -| `src/platform/esp32/platform_esp32_parlio.cpp` | 7 | `lines 277-286, also in src/platform/esp32/platform_esp32_i80.cpp:425`, duplicated block 10 | +| `src/platform/esp32/platform_esp32_rmt.cpp` | 7 | `platform::rmtWs2812LoopbackFrame`, complex function 31 | | `test/js/ui-ws-scheme.test.mjs` | 7 | `lines 10-22, also in test/js/ui-render-guard.test.mjs:13`, duplicated block 13 | | `test/unit/core/unit_NetworkModule.cpp` | 7 | `lines 556-564, also in test/unit/core/unit_NetworkModule.cpp:190`, duplicated block 9 | | `test/unit/light/unit_NdiDriver.cpp` | 7 | `lines 29-46, also in test/unit/light/unit_PanelCardDriver.cpp:22`, duplicated block 18 | @@ -135,6 +134,7 @@ The next function worth simplifying is at the top of each file's list. | `src/platform/desktop/moonlive_asm_x86_64.cpp` | 6 | `lines 557-564, also in src/platform/desktop/moonlive_asm_x86_64.cpp:515`, duplicated block 8 | | `src/platform/esp32/platform_esp32_fs.cpp` | 6 | `lines 211-221, also in src/platform/esp32/platform_esp32_fs.cpp:161`, duplicated block 11 | | `src/platform/esp32/platform_esp32_improv.cpp` | 6 | `platform::improvTask`, complex function 22 | +| `src/platform/esp32/platform_esp32_parlio.cpp` | 6 | `lines 278-287, also in src/platform/esp32/platform_esp32_i80.cpp:426`, duplicated block 10 | | `test/unit/core/unit_AccessPointModule.cpp` | 6 | `lines 340-348, also in test/unit/core/unit_AccessPointModule.cpp:311`, duplicated block 9 | | `test/unit/core/unit_ImprovFrame.cpp` | 6 | `lines 179-187, also in test/unit/core/unit_ImprovFrame.cpp:152`, duplicated block 9 | | `test/unit/light/unit_Correction.cpp` | 6 | `lines 280-286, also in test/unit/light/unit_Correction.cpp:237`, duplicated block 7 | @@ -194,7 +194,6 @@ The next function worth simplifying is at the top of each file's list. | `test/unit/light/unit_NetworkSendDriver_packet.cpp` | 4 | `lines 344-352, also in test/unit/light/unit_NetworkSendDriver_packet.cpp:290`, duplicated block 9 | | `test/unit/light/unit_ParallelSlots.cpp` | 4 | `lines 522-532, also in test/unit/light/unit_ParallelSlots.cpp:493`, duplicated block 11 | | `test/unit/light/unit_PraxisEffect.cpp` | 4 | `lines 9-21, also in test/unit/light/unit_RandomMapModifier.cpp:115`, duplicated block 13 | -| `test/unit/light/unit_RmtLedDriver_lifecycle.cpp` | 4 | `lines 19-30, also in test/unit/light/unit_RmtLedDriver_pins.cpp:17`, duplicated block 12 | | `test/unit/light/unit_fluid.cpp` | 4 | `lines 160-169, also in test/unit/light/unit_TrailsEffect.cpp:54`, duplicated block 10 | | `src/core/moonlive/MoonLiveBuiltins_common.h` | 3 | `moonlive::mm_ml_addControl`, complex function 12 | | `src/core/services/GamepadService.h` | 3 | `lines 108-130, also in src/core/services/InfraredService.h:113`, duplicated block 23 | @@ -216,6 +215,7 @@ The next function worth simplifying is at the top of each file's list. | `test/unit/light/unit_NetworkReceiveEffect_protocols.cpp` | 3 | `lines 23-44, also in test/unit/light/unit_NetworkReceiveEffect.cpp:18`, duplicated block 22 | | `test/unit/light/unit_PolarLut_equivalence.cpp` | 3 | `lines 40-55, also in test/unit/light/unit_NoiseEffect.cpp:33`, duplicated block 16 | | `test/unit/light/unit_PongEffect.cpp` | 3 | `lines 80-91, also in test/unit/light/unit_PongEffect.cpp:10`, duplicated block 12 | +| `test/unit/light/unit_RmtLedDriver_lifecycle.cpp` | 3 | `lines 19-30, also in test/unit/light/unit_RmtLedDriver_pins.cpp:17`, duplicated block 12 | | `test/unit/light/unit_RmtLedDriver_pins.cpp` | 3 | `lines 17-28, also in test/unit/light/unit_RmtLedDriver_lifecycle.cpp:19`, duplicated block 12 | | `test/unit/light/unit_Splat.cpp` | 3 | `lines 6-20, also in test/unit/light/unit_Scroll.cpp:6`, duplicated block 15 | | `test/unit/light/unit_TetrixEffect.cpp` | 3 | `lines 20-29, also in test/unit/light/unit_BreakoutEffect.cpp:18`, duplicated block 10 | @@ -271,12 +271,12 @@ The next function worth simplifying is at the top of each file's list. | `test/unit/light/unit_DistortionWavesEffect.cpp` | 2 | `lines 26-34, also in test/unit/light/unit_SineEffect.cpp:22`, duplicated block 9 | | `test/unit/light/unit_Effects_gridsweep.cpp` | 2 | `lines 224-236, also in test/unit/light/unit_Effects_gridsweep.cpp:157`, duplicated block 13 | | `test/unit/light/unit_FixedPointEffect.cpp` | 2 | `lines 44-49, also in test/unit/light/unit_FixedPointEffect.cpp:33`, duplicated block 6 | +| `test/unit/light/unit_GpioDmaPeripheral.cpp` | 2 | `lines 42-49, also in test/unit/light/unit_ParlioPeripheral.cpp:43`, duplicated block 8 | | `test/unit/light/unit_Hub75Driver_pins.cpp` | 2 | `lines 66-77, also in test/unit/core/unit_PinsModule.cpp:46`, duplicated block 12 | | `test/unit/light/unit_MoonLiveDrawing.cpp` | 2 | `lines 26-39, also in test/unit/light/unit_MoonLiveParticles.cpp:34`, duplicated block 14 | | `test/unit/light/unit_MoonLiveMotion.cpp` | 2 | `lines 209-214, also in test/unit/light/unit_MoonLiveMotion.cpp:118`, duplicated block 6 | | `test/unit/light/unit_MoonLiveScriptResolve.cpp` | 2 | `lines 90-97, also in test/unit/light/unit_MoonLiveScriptResolve.cpp:78`, duplicated block 8 | | `test/unit/light/unit_NoiseEffect.cpp` | 2 | `lines 33-48, also in test/unit/light/unit_PolarLut_equivalence.cpp:40`, duplicated block 16 | -| `test/unit/light/unit_ParallelLedDriver_doublebuffer.cpp` | 2 | `lines 93-104, also in test/unit/light/unit_ParallelLedDriver_pinexpander.cpp:94`, duplicated block 12 | | `test/unit/light/unit_Scroll.cpp` | 2 | `lines 6-20, also in test/unit/light/unit_Splat.cpp:6`, duplicated block 15 | | `test/unit/light/unit_SineEffect.cpp` | 2 | `lines 22-30, also in test/unit/light/unit_DistortionWavesEffect.cpp:26`, duplicated block 9 | | `test/unit/light/unit_SingleColumnLayout.cpp` | 2 | `lines 12-25, also in test/unit/light/unit_GridLayout.cpp:8`, duplicated block 14 | @@ -341,12 +341,13 @@ The next function worth simplifying is at the top of each file's list. | `src/light/util/E131Packet.h` | 1 | `parseE131Packet`, complex function 17 | | `src/module_types.cpp` | 1 | `registerModuleTypes`, long function 149 | | `src/platform/desktop/moonlive_asm_host.h` | 1 | `lines 36-43, also in src/platform/esp32/moonlive_asm_riscv.h:36`, duplicated block 8 | -| `src/platform/desktop/platform_config.h` | 1 | `lines 71-85, also in src/platform/esp32/platform_config.h:236`, duplicated block 15 | | `src/platform/esp32/moonlive_asm_xtensa.h` | 1 | `lines 79-96, also in src/platform/esp32/moonlive_asm_riscv.h:74`, duplicated block 18 | -| `src/platform/esp32/platform_config.h` | 1 | `lines 236-250, also in src/platform/desktop/platform_config.h:71`, duplicated block 15 | | `src/platform/esp32/platform_esp32_i2s.cpp` | 1 | `platform::audioMicInit`, complex function 11 | | `src/platform/esp32/platform_esp32_tasks.cpp` | 1 | `platform::taskSnapshot`, complex function 12 | -| `src/platform/platform.h` | 1 | `platform.h`, large file 1301 | +| `src/platform/esp32/platform_esp32_usb_midi.cpp` | 1 | `lines 254-262, also in src/platform/teensy/platform_teensy.cpp:772`, duplicated block 9 | +| `src/platform/platform.h` | 1 | `platform.h`, large file 1331 | +| `src/platform/teensy/platform_teensy.cpp` | 1 | `lines 772-780, also in src/platform/esp32/platform_esp32_usb_midi.cpp:254`, duplicated block 9 | +| `src/platform/teensy/platform_teensy_gpio_dma.cpp` | 1 | `lines 311-316, also in src/platform/esp32/platform_esp32_parlio.cpp:287`, duplicated block 6 | | `src/ui/install-picker.js` | 1 | `install-picker.js`, large file 1048 | | `src/ui/preview3d.js` | 1 | `preview3d.js`, large file 1431 | | `test/js/installer-eth-only.test.mjs` | 1 | `lines 13-23, also in test/js/ui-mooncloud.test.mjs:6`, duplicated block 11 | @@ -370,8 +371,8 @@ The next function worth simplifying is at the top of each file's list. | `test/unit/light/unit_GridLayout.cpp` | 1 | `lines 8-21, also in test/unit/light/unit_SingleColumnLayout.cpp:12`, duplicated block 14 | | `test/unit/light/unit_Layer_persistence.cpp` | 1 | `lines 45-51, also in test/unit/light/unit_TextEffect.cpp:18`, duplicated block 7 | | `test/unit/light/unit_Layer_phase_animation.cpp` | 1 | `lines 77-85, also in test/unit/light/unit_RandomMapModifier.cpp:115`, duplicated block 9 | -| `test/unit/light/unit_MoonI80Peripheral.cpp` | 1 | `lines 34-45, also in test/unit/light/unit_ParallelLedDriver_doublebuffer.cpp:93`, duplicated block 12 | | `test/unit/light/unit_NetworkReceiveEffect.cpp` | 1 | `lines 18-39, also in test/unit/light/unit_NetworkReceiveEffect_protocols.cpp:23`, duplicated block 22 | +| `test/unit/light/unit_ParallelLedDriver_doublebuffer.cpp` | 1 | `lines 93-104, also in test/unit/light/unit_ParallelLedDriver_pinexpander.cpp:94`, duplicated block 12 | | `test/unit/light/unit_Rings241Layout.cpp` | 1 | `lines 18-30, also in test/unit/light/unit_SingleColumnLayout.cpp:19`, duplicated block 13 | | `test/unit/light/unit_SpaceInvadersEffect.cpp` | 1 | `lines 13-20, also in test/unit/light/unit_SpritePopulation.cpp:23`, duplicated block 8 | | `test/unit/light/unit_SpritePopulation.cpp` | 1 | `lines 23-30, also in test/unit/light/unit_SpaceInvadersEffect.cpp:13`, duplicated block 8 | diff --git a/docs/reference/metrics/docgen.md b/docs/reference/metrics/docgen.md index d0bf9091f..cb5972244 100644 --- a/docs/reference/metrics/docgen.md +++ b/docs/reference/metrics/docgen.md @@ -4,9 +4,9 @@ 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 **4286 warning(s)** across 413 page(s). +**0 error(s)** and **4246 warning(s)** across 406 page(s). -**5460 appendix line(s)** after `@moreinfo`. This only falls as well: a cleanup cuts first, and moves only what neither the code, the test name nor the spec says. +**5440 appendix line(s)** after `@moreinfo`. This only falls as well: a cleanup cuts first, and moves only what neither the code, the test name nor the spec says. 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 worth fixing without being worth stopping a commit for: any finding in an implementation file, which publishes nothing, and the rules still staged as counts in a header too, the over-wide line and the appendix section nothing refers to. Both are counted here, because a warning nobody sees is a warning nobody fixes. @@ -16,10 +16,10 @@ The split is temporary. It stages the sweep rather than ranking the two kinds of | Rule | Errors | Warnings | |---|---:|---:| -| split paragraphs | 0 | 1791 | -| over-wide comment lines | 0 | 1582 | -| appendix sections nothing refers to | 0 | 369 | -| multi-line comment blocks | 0 | 317 | +| split paragraphs | 0 | 1768 | +| over-wide comment lines | 0 | 1575 | +| appendix sections nothing refers to | 0 | 360 | +| multi-line comment blocks | 0 | 316 | | over-long sentences | 0 | 82 | | over-wide doc lines | 0 | 78 | | member deep dives | 0 | 67 | @@ -30,19 +30,18 @@ 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 | 2064 | -| `platform/index.md` | 0 | 743 | +| `(tests, no card)` | 0 | 2051 | +| `platform/index.md` | 0 | 732 | | `light/effects.md` | 0 | 403 | | `core/system.md` | 0 | 203 | | `core/supporting.md` | 0 | 181 | | `light/power-functions.md` | 0 | 147 | | `core/services.md` | 0 | 123 | -| `light/drivers.md` | 0 | 120 | +| `light/drivers.md` | 0 | 117 | | `light/modifiers.md` | 0 | 79 | | `light/supporting.md` | 0 | 77 | | `light/layouts.md` | 0 | 75 | -| `light/moonlive.md` | 0 | 59 | -| `(unassigned)` | 0 | 11 | +| `light/moonlive.md` | 0 | 57 | | `core/ui.md` | 0 | 1 | ## Where the work is @@ -51,7 +50,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 **2064 warning(s)** across 158 file(s). +**0 error(s)** and **2051 warning(s)** across 157 file(s). | Findings | File | | |---:|---|---| @@ -65,13 +64,13 @@ Per area, since that is the unit a sweep runs in: the files ranked, then the rul | 48 | `test/unit/light/unit_Effects_gridsweep.cpp` | warning | | 45 | `test/unit/core/unit_moonlive_codegen_xtensa.cpp` | warning | | 42 | `test/unit/core/unit_FilesystemModule_persistence.cpp` | warning | -| 1-42 each | *148 more warning files, 1298 findings* | | +| 1-42 each | *147 more warning files, 1285 findings* | | -By rule, warnings: 1360 over-wide comment lines, 271 split paragraphs, 197 multi-line comment blocks, 82 over-long sentences, 78 over-wide doc lines, 67 member deep dives, 9 appendix sections nothing refers to. +By rule, warnings: 1354 over-wide comment lines, 266 split paragraphs, 196 multi-line comment blocks, 82 over-long sentences, 78 over-wide doc lines, 67 member deep dives, 8 appendix sections nothing refers to. ### platform/index.md -**0 error(s)** and **743 warning(s)** across 31 file(s). +**0 error(s)** and **732 warning(s)** across 29 file(s). | Findings | File | | |---:|---|---| @@ -79,15 +78,15 @@ By rule, warnings: 1360 over-wide comment lines, 271 split paragraphs, 197 multi | 72 | `src/platform/desktop/moonlive_asm_x86_64.cpp` | warning | | 68 | `src/platform/esp32/moonlive_asm_xtensa.cpp` | warning | | 67 | `src/platform/esp32/platform_esp32_i80.cpp` | warning | -| 61 | `src/platform/esp32/platform_esp32_rmt.cpp` | warning | +| 59 | `src/platform/esp32/platform_esp32_rmt.cpp` | warning | | 42 | `src/platform/desktop/moonlive_asm_arm64.cpp` | warning | | 33 | `src/platform/esp32/platform_esp32_parlio.cpp` | warning | | 32 | `src/platform/esp32/moonlive_asm_riscv.cpp` | warning | | 23 | `src/platform/esp32/platform_esp32_i2s.cpp` | warning | | 20 | `src/platform/esp32/platform_esp32_improv.cpp` | warning | -| 1-19 each | *21 more warning files, 146 findings* | | +| 1-19 each | *19 more warning files, 137 findings* | | -By rule, warnings: 398 split paragraphs, 217 over-wide comment lines, 114 multi-line comment blocks, 14 appendix sections nothing refers to. +By rule, warnings: 392 split paragraphs, 217 over-wide comment lines, 114 multi-line comment blocks, 9 appendix sections nothing refers to. ### light/effects.md @@ -191,7 +190,7 @@ By rule, warnings: 99 split paragraphs, 24 appendix sections nothing refers to. ### light/drivers.md -**0 error(s)** and **120 warning(s)** across 17 file(s). +**0 error(s)** and **117 warning(s)** across 15 file(s). | Findings | File | | |---:|---|---| @@ -205,9 +204,9 @@ By rule, warnings: 99 split paragraphs, 24 appendix sections nothing refers to. | 3 | `src/light/drivers/HlsDriver.h` | warning | | 3 | `src/light/drivers/MoonI80Peripheral.h` | warning | | 3 | `src/light/drivers/RmtSymbol.h` | warning | -| 1-2 each | *7 more warning files, 13 findings* | | +| 2 each | *5 more warning files, 10 findings* | | -By rule, warnings: 70 split paragraphs, 50 appendix sections nothing refers to. +By rule, warnings: 69 split paragraphs, 48 appendix sections nothing refers to. ### light/modifiers.md @@ -271,7 +270,7 @@ By rule, warnings: 69 split paragraphs, 6 appendix sections nothing refers to. ### light/moonlive.md -**0 error(s)** and **59 warning(s)** across 19 file(s). +**0 error(s)** and **57 warning(s)** across 18 file(s). | Findings | File | | |---:|---|---| @@ -285,19 +284,9 @@ By rule, warnings: 69 split paragraphs, 6 appendix sections nothing refers to. | 4 | `src/light/moonlive/MoonLiveParticles.h` | warning | | 3 | `src/core/moonlive/MoonLiveBuiltins.h` | warning | | 3 | `src/core/moonlive/MoonLiveBuiltins_common.h` | warning | -| 1-3 each | *9 more warning files, 15 findings* | | +| 1-3 each | *8 more warning files, 13 findings* | | -By rule, warnings: 38 split paragraphs, 21 appendix sections nothing refers to. - -### (unassigned) - -**0 error(s)** and **11 warning(s)** across 1 file(s). - -| Findings | File | | -|---:|---|---| -| 11 | `src/module_types.cpp` | warning | - -By rule, warnings: 10 split paragraphs, 1 over-wide comment lines. +By rule, warnings: 37 split paragraphs, 20 appendix sections nothing refers to. ### core/ui.md diff --git a/docs/reference/metrics/prose.md b/docs/reference/metrics/prose.md index 2f2798e20..b14b524da 100644 --- a/docs/reference/metrics/prose.md +++ b/docs/reference/metrics/prose.md @@ -9,13 +9,13 @@ This number is the debt behind that gate, and it RATCHETS: a rise fails the chec docgen.md does. A comment is prose wherever it lives, so a `//` in the web interface and a `#` in a script are counted beside a `///` in a header. -**2946 finding(s)** across 370 file(s). +**2941 finding(s)** across 370 file(s). | rule | findings | |---|---| -| `MoonLight.EmDash` | 1528 | -| `MoonLight.Weasel` | 688 | -| `MoonLight.SentenceLength` | 430 | +| `MoonLight.EmDash` | 1525 | +| `MoonLight.Weasel` | 687 | +| `MoonLight.SentenceLength` | 429 | | `MoonLight.Temporal` | 215 | | `MoonLight.Spelling` | 42 | | `MoonLight.SelfReference` | 30 | @@ -25,7 +25,7 @@ docgen.md does. A comment is prose wherever it lives, so a `//` in the web inter | file | findings | |---|---| -| `moondeck/MoonDeck.md` | 244 | +| `moondeck/MoonDeck.md` | 240 | | `moondeck/moondeck.py` | 157 | | `docs/how-to/home-automation.md` | 117 | | `mooninstaller/install-orchestrator.js` | 93 | diff --git a/docs/reference/metrics/repo-health.json b/docs/reference/metrics/repo-health.json index 7401f233e..197c6681d 100644 --- a/docs/reference/metrics/repo-health.json +++ b/docs/reference/metrics/repo-health.json @@ -1,8 +1,8 @@ { - "commit": "48ab0058", + "commit": "17dae99d", "flash": { - "esp32s3-n16r8": 2336304, - "esp32": 2226256, + "esp32s3-n16r8": 2336688, + "esp32": 2234704, "esp32p4rev1-eth": 2177776, "esp32p4rev1-eth-wifi": 2358896, "esp32s3-n8r8": 2336304, @@ -11,28 +11,30 @@ "esp32-eth": 1681040, "esp32-wrover": 1843760, "qemu": 1383648, - "esp32p4rev3-eth": 2203712, + "esp32p4rev3-eth": 2229440, "esp32s3-zero": 2219120, "esp32-pico": 2107168, - "desktop-macos": 2270440, + "desktop-macos": 2273704, "desktop-windows": 1925120, - "esp32c3": 2335072 + "esp32c3": 2335072, + "teensy41": 1172440 }, "measured": { "esp32p4rev1-eth": "2026-10-07", "esp32s31": "2026-10-10", - "esp32": "2026-10-09", + "esp32": "2026-10-11", "esp32-pico": "2026-09-09", - "esp32s3-n16r8": "2026-10-10", + "esp32s3-n16r8": "2026-10-11", "esp32s3-n8r8": "2026-10-10", "esp32s3-zero": "2026-10-10", "esp32-16mb": "2026-09-09", "esp32p4rev1-eth-wifi": "2026-09-30", "esp32-eth": "2026-09-11", - "esp32p4rev3-eth": "2026-10-08", - "desktop-macos": "2026-10-10", + "esp32p4rev3-eth": "2026-10-11", + "desktop-macos": "2026-10-11", "desktop-windows": "2026-09-30", - "esp32c3": "2026-10-10" + "esp32c3": "2026-10-10", + "teensy41": "2026-10-11" }, "perf": { "esp32": { @@ -52,6 +54,12 @@ "p95": 2744, "n": 4, "last": "2026-10-04" + }, + "teensy41": { + "p50": 2277, + "p95": 2277, + "n": 1, + "last": "2026-10-11" } }, "Firmware_reports_what_is_running": { @@ -78,6 +86,12 @@ "p95": 215, "n": 1, "last": "2026-09-30" + }, + "teensy41": { + "p50": 2142, + "p95": 2142, + "n": 1, + "last": "2026-10-11" } }, "Network_hardware_reconfigures_live": { @@ -104,6 +118,12 @@ "p95": 21, "n": 1, "last": "2026-09-30" + }, + "teensy41": { + "p50": 2147, + "p95": 2147, + "n": 1, + "last": "2026-10-11" } }, "State_one_document_adds_an_effect_with_its_controls": { @@ -118,6 +138,12 @@ "p95": 2800, "n": 3, "last": "2026-10-04" + }, + "teensy41": { + "p50": 8301, + "p95": 8301, + "n": 1, + "last": "2026-10-11" } }, "Aurora_fps": { @@ -168,6 +194,12 @@ "p95": 7532, "n": 1, "last": "2026-09-30" + }, + "teensy41": { + "p50": 33192, + "p95": 33192, + "n": 1, + "last": "2026-10-11" } }, "Fluid_solver": { @@ -218,6 +250,12 @@ "p95": 3371, "n": 1, "last": "2026-09-30" + }, + "teensy41": { + "p50": 14853, + "p95": 14853, + "n": 1, + "last": "2026-10-11" } }, "GridLayout_resize": { @@ -280,6 +318,12 @@ "p95": 2005, "n": 1, "last": "2026-09-30" + }, + "teensy41": { + "p50": 1521, + "p95": 1521, + "n": 1, + "last": "2026-10-11" } }, "perf_full": { @@ -330,6 +374,12 @@ "p95": 2438, "n": 1, "last": "2026-09-30" + }, + "teensy41": { + "p50": 6797, + "p95": 6797, + "n": 1, + "last": "2026-10-11" } }, "perf_light": { @@ -380,6 +430,12 @@ "p95": 780, "n": 1, "last": "2026-09-30" + }, + "teensy41": { + "p50": 662, + "p95": 662, + "n": 1, + "last": "2026-10-11" } }, "peripheral_grid_sweep": { @@ -430,6 +486,12 @@ "p95": 2577, "n": 1, "last": "2026-09-30" + }, + "teensy41": { + "p50": 6618, + "p95": 6618, + "n": 1, + "last": "2026-10-11" } }, "peripheral_switch": { @@ -480,6 +542,12 @@ "p95": 341, "n": 1, "last": "2026-09-30" + }, + "teensy41": { + "p50": 111, + "p95": 111, + "n": 1, + "last": "2026-10-11" } }, "Drivers_output_and_brightness": { @@ -506,6 +574,12 @@ "p95": 218, "n": 1, "last": "2026-09-30" + }, + "teensy41": { + "p50": 2173, + "p95": 2173, + "n": 1, + "last": "2026-10-11" } }, "Effects_pipeline_builds_and_renders": { @@ -546,6 +620,12 @@ "p95": 287, "n": 1, "last": "2026-09-30" + }, + "teensy41": { + "p50": 2236, + "p95": 2236, + "n": 1, + "last": "2026-10-11" } }, "Effects_teardown_under_a_running_pipeline": { @@ -572,6 +652,12 @@ "p95": 482, "n": 1, "last": "2026-09-30" + }, + "teensy41": { + "p50": 2160, + "p95": 2160, + "n": 1, + "last": "2026-10-11" } }, "Layers_stack_and_blend_live": { @@ -580,6 +666,12 @@ "p95": 355, "n": 32, "last": "2026-10-10" + }, + "teensy41": { + "p50": 7479, + "p95": 7479, + "n": 1, + "last": "2026-10-11" } }, "Layouts_resize_reallocates_live": { @@ -606,6 +698,12 @@ "p95": 579, "n": 1, "last": "2026-09-30" + }, + "teensy41": { + "p50": 7788, + "p95": 7788, + "n": 1, + "last": "2026-10-11" } }, "Modifiers_reshape_the_mapping": { @@ -632,6 +730,12 @@ "p95": 273, "n": 1, "last": "2026-09-30" + }, + "teensy41": { + "p50": 131, + "p95": 131, + "n": 1, + "last": "2026-10-11" } }, "Palettes_a_script_drives_the_effects": { @@ -640,6 +744,12 @@ "p95": 74, "n": 32, "last": "2026-10-10" + }, + "teensy41": { + "p50": 7763, + "p95": 7763, + "n": 1, + "last": "2026-10-11" } } }, @@ -681,54 +791,54 @@ } }, "loc": { - "core": 28271, - "light": 31027, - "platform": 17930, + "core": 28270, + "light": 31098, + "platform": 19268, "ui": 12307, - "test": 64597, - "moondeck": 30600 + "test": 64718, + "moondeck": 30980 }, "comments": { "core": { - "lines": 6165, + "lines": 6164, "ratio": 0.242 }, "light": { - "lines": 7439, + "lines": 7462, "ratio": 0.269 }, "platform": { - "lines": 3659, - "ratio": 0.228 + "lines": 3790, + "ratio": 0.22 }, "ui": { "lines": 3464, "ratio": 0.298 }, "test": { - "lines": 6968, + "lines": 6966, "ratio": 0.125 }, "moondeck": { - "lines": 4906, - "ratio": 0.183 + "lines": 4931, + "ratio": 0.182 } }, "tests": { - "cases": 2398, + "cases": 2409, "scenarios": 30 }, "docs": { - "md_files": 150, - "md_lines": 32271, - "plans_files": 44, - "backlog_lines": 3319, + "md_files": 152, + "md_lines": 32560, + "plans_files": 45, + "backlog_lines": 3326, "lessons_lines": 532, "claude_md_lines": 283 }, "complexity": { - "functions": 4389, - "over_threshold": 331, + "functions": 4602, + "over_threshold": 330, "worst_ccn": 128 } } diff --git a/docs/reference/metrics/repo-health.md b/docs/reference/metrics/repo-health.md index d488dc2e4..68406f9a2 100644 --- a/docs/reference/metrics/repo-health.md +++ b/docs/reference/metrics/repo-health.md @@ -1,6 +1,6 @@ # Repo health -Measured at `48ab0058`. Generated by [`moondeck/check/repo_health.py`](../../../moondeck/check/repo_health.py) on every KPI-gate run. **Do not edit by hand.** +Measured at `17dae99d`. 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,23 +8,25 @@ Current state only; the trend is this file's git history (`git log -p docs/refer | Metric | v6.0.0 | Now | Change | |---|---:|---:|---:| -| flash desktop-macos | 1,963 KB | 2,217 KB | +254.2 KB (+12.9%) | -| flash esp32 | 2,061 KB | 2,174 KB | +112.9 KB (+5.5%) | +| flash desktop-macos | 1,963 KB | 2,220 KB | +257.4 KB (+13.1%) | +| flash esp32 | 2,061 KB | 2,182 KB | +121.1 KB (+5.9%) | +| flash esp32c3 | - | 2,280 KB | new | | flash esp32p4rev1-eth | 2,022 KB | 2,127 KB | +105.0 KB (+5.2%) | -| flash esp32p4rev3-eth | 2,025 KB | 2,152 KB | +126.8 KB (+6.3%) | -| flash esp32s3-n16r8 | 2,111 KB | 2,282 KB | +170.1 KB (+8.1%) | +| flash esp32p4rev3-eth | 2,025 KB | 2,177 KB | +151.9 KB (+7.5%) | +| flash esp32s3-n16r8 | 2,111 KB | 2,282 KB | +170.4 KB (+8.1%) | | flash esp32s3-n8r8 | 2,038 KB | 2,282 KB | +243.3 KB (+11.9%) | | flash esp32s3-zero | 1,977 KB | 2,167 KB | +190.4 KB (+9.6%) | | flash esp32s31 | 2,395 KB | 2,534 KB | +139.0 KB (+5.8%) | +| flash teensy41 | - | 1,145 KB | new | | tick Effects_pipeline_builds_and_renders (desktop-macos) | 7 ยตs | 7 ยตs | +0 (+0.0%) | | tick Layouts_resize_reallocates_live (desktop-macos) | 12 ยตs | 10 ยตs | -2 (-16.7%) | -| lines core | 22,299 | 28,271 | +5,972 (+26.8%) | -| lines light | 31,054 | 31,027 | -27 (-0.1%) | -| lines platform | 16,798 | 17,930 | +1,132 (+6.7%) | +| lines core | 22,299 | 28,270 | +5,971 (+26.8%) | +| lines light | 31,054 | 31,098 | +44 (+0.1%) | +| lines platform | 16,798 | 19,268 | +2,470 (+14.7%) | | lines ui | 11,394 | 12,307 | +913 (+8.0%) | -| lines test | 57,759 | 64,597 | +6,838 (+11.8%) | -| lines moondeck | 29,325 | 30,600 | +1,275 (+4.3%) | -| unit tests | 2,120 | 2,398 | +278 (+13.1%) | +| lines test | 57,759 | 64,718 | +6,959 (+12.0%) | +| lines moondeck | 29,325 | 30,980 | +1,655 (+5.6%) | +| unit tests | 2,120 | 2,409 | +289 (+13.6%) | | scenarios | 21 | 30 | +9 (+42.9%) | Against the release `v6.0.0`, the fixed point a user compares with; the tables below compare against the last commit. A firmware not rebuilt since `v6.0.0` is left out, and a measure the release did not record yet, such as the code report, joins from the next one. @@ -33,22 +35,23 @@ Against the release `v6.0.0`, the fixed point a user compares with; the tables b | Target | Flash | Capacity | Used | Built | |---|---:|---:|---:|:--:| -| desktop-macos | 2,217 KB (โˆ’48 B) โœ“ | - | - | yes | -| desktop-windows | 1,880 KB | - | - | **STALE 10d** | -| esp32 | 2,174 KB | 2,496 KB | 87% | carried 1d | -| esp32-16mb | 2,012 KB | 4,096 KB | 49% | **STALE 31d** | -| esp32-eth | 1,642 KB | 2,496 KB | 66% | **STALE 29d** | -| esp32-pico | 2,058 KB | 3,072 KB | 67% | **STALE 31d** | +| desktop-macos | 2,220 KB (+3 KB) โš  | - | - | yes | +| desktop-windows | 1,880 KB | - | - | **STALE 11d** | +| esp32 | 2,182 KB (+8 KB) โš  | 2,496 KB | 87% | yes | +| esp32-16mb | 2,012 KB | 4,096 KB | 49% | **STALE 32d** | +| esp32-eth | 1,642 KB | 2,496 KB | 66% | **STALE 30d** | +| esp32-pico | 2,058 KB | 3,072 KB | 67% | **STALE 32d** | | esp32-wrover | 1,801 KB | 2,496 KB | 72% | carried (age?) | -| esp32c3 | 2,280 KB (+544 B) โš  | 2,496 KB | 91% | yes | -| esp32p4rev1-eth | 2,127 KB | 4,096 KB | 52% | carried 3d | -| esp32p4rev1-eth-wifi | 2,304 KB | 4,096 KB | 56% | **STALE 10d** | -| esp32p4rev3-eth | 2,152 KB | 4,096 KB | 53% | carried 2d | -| esp32s3-n16r8 | 2,282 KB (+800 B) โš  | 4,096 KB | 56% | yes | -| esp32s3-n8r8 | 2,282 KB (+800 B) โš  | 3,072 KB | 74% | yes | -| esp32s3-zero | 2,167 KB (+832 B) โš  | 2,496 KB | 87% | yes | -| esp32s31 | 2,534 KB (+816 B) โš  | 4,096 KB | 62% | yes | +| esp32c3 | 2,280 KB | 2,496 KB | 91% | carried 1d | +| esp32p4rev1-eth | 2,127 KB | 4,096 KB | 52% | carried 4d | +| esp32p4rev1-eth-wifi | 2,304 KB | 4,096 KB | 56% | **STALE 11d** | +| esp32p4rev3-eth | 2,177 KB (+25 KB) โš  | 4,096 KB | 53% | yes | +| esp32s3-n16r8 | 2,282 KB (+384 B) โš  | 4,096 KB | 56% | yes | +| esp32s3-n8r8 | 2,282 KB | 3,072 KB | 74% | carried 1d | +| esp32s3-zero | 2,167 KB | 2,496 KB | 87% | carried 1d | +| esp32s31 | 2,534 KB | 4,096 KB | 62% | carried 1d | | qemu | 1,351 KB | - | - | carried (age?) | +| teensy41 | 1,145 KB | 6,912 KB | 17% | yes | `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. @@ -56,19 +59,15 @@ Against the release `v6.0.0`, the fixed point a user compares with; the tables b | Target | Net | New | Removed | Grown | Shrunk | Other | Since | |---|---:|---:|---:|---:|---:|---:|---| -| esp32c3 | +0.5 KB | +1.0 KB | +0.0 KB | +0.6 KB | -0.5 KB | -0.5 KB | `48ab0058` | -| esp32s3-n16r8 | +0.8 KB | +0.1 KB | -0.1 KB | +0.9 KB | -0.2 KB | +0.1 KB | `48ab0058` | -| esp32s3-n8r8 | +0.8 KB | +0.1 KB | -0.1 KB | +0.9 KB | -0.2 KB | +0.1 KB | `48ab0058` | -| esp32s3-zero | +0.8 KB | +0.1 KB | -0.1 KB | +0.9 KB | -0.1 KB | +0.1 KB | `48ab0058` | -| esp32s31 | +0.8 KB | +1.1 KB | -0.2 KB | +0.8 KB | -0.5 KB | -0.4 KB | `48ab0058` | +| esp32 | +8.2 KB | +9.3 KB | -4.8 KB | +12.0 KB | -7.4 KB | -0.9 KB | `17dae99d` | +| esp32p4rev3-eth | +25.1 KB | +41.1 KB | -28.6 KB | +16.0 KB | -13.4 KB | +10.1 KB | `17dae99d` | +| esp32s3-n16r8 | +0.4 KB | +0.4 KB | +0.0 KB | +0.2 KB | -0.4 KB | +0.1 KB | `17dae99d` | Largest moves: -- **esp32c3**: `mm::moonlive::lowerWith(mm::mo` +504 B, `mm::moonlive::lowerWith(mm::mo` +504 B, `unsigned int mm::moonlive::lowerWith(mm::mo` +504 B, `mm::moonlive::lowerWith(mm::mo` +504 B, `unsigned int mm::moonlive::lowerWith=3.0 are "huge hardware difference" (one binary can't cover both). **DONE (2026-08-19): `esp32p4rev3-eth` and `esp32p4rev3-eth-wifi` ship** (`REV_MIN_300`, which covers v3.0-v3.99), reusing the rev1 board fragment so the partition table and EMAC config stay in one place. **Still open: neither has ever been booted** โ€” both bench boards are v1.3 engineering samples, so the rev3 images are flagged experimental in the installer and need a v3 board to verify. Espressif does not recommend v0.x/v1.x for new designs, so a board bought today is v3.x and needs these. - **Retire the OTA fallback repository, once a device has another recovery route.** `FirmwareUpdateModule.h` names two: `kReleaseRepo` is tried first and `kFallbackRepo` catches a device whose firmware predates the move. It is tempting to call the second dead code after the rename, and it is not: `platform_esp32_ota.cpp:195` retries the alternate address whenever `esp_https_ota_begin` FAILS, which covers DNS, TLS, an outage and a variant whose asset is missing, rather than only a repository that has moved. So a v6.0.0 device on the happy path never reaches it and a v6.0.0 device whose first request fails still does. Removing it deletes that second chance for every board, which wants a replacement rather than a deletion: a configurable update URL, or a retry against the same address, would give a device somewhere to go. The one cost of keeping it is a wasted request on the failure path, which is already the slow path. -- **Teensy 4.1** โ€” toolchain-file build, `.hex` for Teensy Loader. - **Raspberry Pi** โ€” ARM64, cross-built or native. - **macOS code-signing (Developer ID)** โ€” the release `.dmg` is now ad-hoc signed, which turns Gatekeeper's outright refusal into the "unidentified developer" prompt a user can accept via right-click Open. A paid Developer ID certificate plus notarization would drop that prompt too. - **Windows code-signing** โ€” drops the SmartScreen warning on first run of `MoonLight.exe`. Same shape as macOS signing; needs an EV / OV code-signing certificate (Microsoft Trusted Signing is the cheapest current option). Until then, the README notes the SmartScreen prompt. @@ -383,6 +382,13 @@ declared rather than for a buffer to fill, and to time out on stall rather than - **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. +### A Service that calls another device's API (robwomp, 2026-10-11) + +Suggested on Discord: panels driven by another stack take their pixels from MoonLight over DDP or Art-Net, but their settings stay out of reach. +A user-added Service holds a list of HTTP requests, each a method, a URL and a body, and fires one when a control it watches changes, so a button, a MIDI knob or an OSC message drives any device that answers `curl`. +Inbound control already exists (`/api/control`, MQTT, OSC, MIDI); this is its outbound twin, and the generic complement to the typed command half of DevicesModule below, which knows each ecosystem's API. +Requests leave the render thread, since a slow peer must never stall a frame. + ### DevicesModule โ€” interop plugins + the command half (discovery shipped) DevicesModule discovers via **passive UDP presence** (UDP 65506) feeding a [`DevicePlugin`](../../src/core/util/DevicePlugin.h) seam (shipped: MoonLight + WLED plugins). mDNS is advertise-only so MoonLight appears in the native WLED apps + Home Assistant; the WLED-app interop (list + live color + brightness control) is shipped too. What remains is *growth on the seam*, each piece additive (one plugin file, no core change): diff --git a/docs/work/future/backlog-light.md b/docs/work/future/backlog-light.md index 8b4f50a6f..9249c8c2c 100644 --- a/docs/work/future/backlog-light.md +++ b/docs/work/future/backlog-light.md @@ -155,6 +155,13 @@ Record before and after in performance.md per target. ## Drivers +### LED output from a Raspberry Pi's own pins (robwomp, 2026-10-11) + +Suggested on Discord after the Teensy port: a Pi runs MoonLight today as the Linux desktop build, so its LEDs are reached only through the network drivers. +Driving a strip from the Pi's own header needs the platform backend that does not exist yet (`src/platform/rpi/`, the same prerequisite as the sensor item below), and a `LedPeripheral` behind it. +The routes differ by model: on a Pi 4 and earlier, DMA through the PWM, PCM or SPI peripheral, the way rpi_ws281x does; on a Pi 5 the header sits behind the RP1 chip, whose PIO blocks are the route. +SPI is the one path every model shares, one lane per SPI bus with each WS2812 bit encoded in SPI bits, and the place to start. + ### Logarithmic brightness, and a power budget the device knows about (2026-09-02) `Drivers.brightness` scales the output linearly, and perceived lightness is not linear: the eye is @@ -340,10 +347,6 @@ ESP32-PICO-V3-02. **Prerequisite / sequencing:** tier 2 wants the **ISR-refill + `MM_HOT`** work done first (same as the classic ring above), since a high-rate Parlio refill benefits from the ISR-grade determinism. Confirm the CURRENT P4 Parlio tested ceiling first (re-measure on the P4 bench) before claiming a head-to-head โ€” the ~21K figure is troyhacks' *buffer-fit* limit, not a verified-on-wire number, and his code steps the clock down at 256/512 LEDs per lane (signal integrity at long strands), which our own measurement must account for. Reference: the parlio source is `src/MoonLight/Nodes/Drivers/parlio.cpp` in the predecessor; our Parlio backend is `platform_esp32_parlio.cpp` + `ParlioLedDriver.h`. -### Extract shared lane-driver scaffolding when the 3rd parallel backend lands (deferred) - -The `I80LedDriver` (classic-ESP32 I2S + S3/P4 LCD_CAM, both via the i80 bus) and `ParlioLedDriver` (P4 Parlio) share ~245 of 362 lines, and their platform-side loopback capture+verify is ~100 lines byte-for-byte identical (`platform_esp32_parlio.cpp` even notes "The RX capture half is byte-for-byte identical" to the i80 one). The status-string lifecycle (`failBuf_` / `configErr_` / `clearFailBuf` / `clearConfigErr`) is triplicated across all three LED drivers (RMT/i80/Parlio), ~60 lines. The branch deliberately extracted the *encoders* (`ParallelSlots.h` shared by i80+Parlio, `RmtSymbol.h`, `PinList.h`) on the "extract when the second user lands" rule, but stopped at the lifecycle/loopback scaffolding. **Accepted for this merge** (the reviewer agreed driver-level extraction can wait): the duplication is in mechanical lifecycle/test scaffolding, not domain logic, and a DriverBase-level refactor touching three drivers is riskier than the duplication it removes. **Do it when the third parallel backend arrives** (16-lane widening, or Teensy FlexIO), at which point the pattern is proven three ways: (a) a `detail::` platform helper for capture+verify (the only per-peripheral difference is the transmit call, pass a callback, beside the already-shared `loopbackJumperOk`), and (b) a small owned-status helper or DriverBase members for the fail/config strings. Until then the cost is line count, not correctness. - ### Multicore Step 2b โ€” ping-pong second output buffer (deferred, workload-gated) The shipped renderโ†”encode split (Step 2a, `multicore` control) uses one `Drivers::outputBuffer_`, so the composite is serialized at the boundary. A **second** output buffer would overlap even that (core 0 composites into B while core 1 encodes A), at the cost of one full frame buffer (~48 KB at 16K lights). The read-only `stall` KPI was built as the trigger metric, and it splits cleanly: a **heavy effect** (render-bound) shows `stall` ~1 ยตs (S3) / ~15 ยตs (classic) โ€” 2b recovers *nothing*, core 0 already fills the encode window; a **light effect on many lights** (output-bound โ€” a solid color or slow gradient across 16K lights) shows `stall` 6โ€“11 ms โ€” 2b recovers that whole wait. So it's a real but narrow win: **gated on the product owner naming that light-effect/many-lights workload as worth the 48 KB**, not on a generic fps gain. (Design context in the shipped Plan-20260713 - Multicore Step 2.) @@ -962,8 +965,6 @@ The LED-driver increments **shipped**: increment 1 (RMT/WS2812B single-strand on standard I2S microphone (INMP441) is content on I2S1 and coexists today when the LED bus claims I2S0 first. -(The shared lane-driver scaffolding extraction โ€” when a 3rd parallel backend lands โ€” is tracked separately under [ยง Extract shared lane-driver scaffolding](#extract-shared-lane-driver-scaffolding-when-the-3rd-parallel-backend-lands-deferred) above.) - ## FixedPoint fades outside the Layer's aggregation (2026-09-07) `FixedPointEffect` calls `draw::fade` directly where every other fading effect calls diff --git a/docs/work/present/Plan-20261010 - Teensy 4.1.md b/docs/work/present/Plan-20261010 - Teensy 4.1.md new file mode 100644 index 000000000..3f1096d75 --- /dev/null +++ b/docs/work/present/Plan-20261010 - Teensy 4.1.md @@ -0,0 +1,123 @@ +# Plan: MoonLight on the Teensy 4.1 + +The README and the platform layer name Teensy as a target, but no Teensy platform exists: `src/platform` holds `esp32` and `desktop`. This plan adds `src/platform/teensy` behind the existing seam (`platform.h`), in phases that each end in something visible on the board. + +The bench board is a **Teensy 4.1-NE**: no Ethernet PHY, so the PJRC MagJack kit has nothing to plug into. Ethernet comes from a **W5500 SPI module** through QNEthernet's W5500 driver, which runs lwIP on the chip's raw frames, so the W5500's own 8-socket limit does not apply. A regular Teensy 4.1 with the kit is the same code with QNEthernet's native driver, selected at build time. + +## The hardware + +The board, the W5500 wiring, the LED pins, power and an example breadboard setup live in the [Teensy 4.1 hardware reference](../../reference/hardware/teensy-4-1.md). + +![The example breadboard setup](../../assets/hardware/teensy41-breadboard.svg) + +## Decisions for the product owner + +1. **Network library license.** QNEthernet is AGPL-3.0-or-later; MoonLight is GPL-3.0. GPLv3 section 13 allows the combination, and the Teensy firmware then carries the AGPL's network term: a user of it over the network can obtain the source, which a "source" link in the UI satisfies. The alternative is plain lwIP (BSD) with our own W5500 frame driver, more work. Recommendation: QNEthernet, with the source link. +2. **Build system.** No maintained CMake build of the Teensy cores exists, so the compile flags and the linker script adjustments are ours to keep. PlatformIO's `teensy` platform owns those, at the price of a second build system. Chosen: a CMake toolchain file over an SDK that `setup_teensy.py` installs into `~/teensy` at pinned versions, each from its publisher: PJRC's cores at Teensyduino's tag, LittleFS and SPI at that release's commits, Arm's GNU toolchain (checksum-verified), and teensy_loader_cli compiled from its tag. `build_teensy.py`, `flash_teensy.py` and `monitor_teensy.py` wrap the rest. +3. **Where settings live.** Decided by measurement: LittleFS in program flash keeps `/.config` across a power cycle and across a teensy_loader_cli upload, so settings stay in program flash. The SD card remains an option for bulk files once the `/sd` mount point exists (PR #134's rework). + +## Why PJRC's core, and what replaces it + +The Teensy has no vendor SDK in the way an ESP32 has ESP-IDF: PJRC's Teensy 4 core is the lowest layer, writing the i.MX RT registers directly behind the Arduino API. +It provides the startup (clocks, the FlexRAM bank split, the MPU, the caches), USB serial with the reboot request the loader uses, timing, GPIO and the ADC, and the libraries the later phases build on (LittleFS, QNEthernet, OctoWS2811 or ObjectFLED, FlasherX). +Its headers appear only in `src/platform/teensy` and `teensy/main.cpp`, as ESP-IDF's appear only in `src/platform/esp32`. + +Two bases could replace it: + +| Base | What it is | Cost | +|---|---|---| +| NXP MCUXpresso SDK | NXP's own SDK for the chip, the true ESP-IDF equivalent | We write the startup, the USB stack and the loader's reboot protocol ourselves. QNEthernet, LittleFS_Program, OctoWS2811 and FlasherX all depend on PJRC's core, so each one is replaced | +| Zephyr | An RTOS with an official Teensy 4.1 board port, plus W5500 and native Ethernet drivers, LittleFS, USB and threads | A second build system (west and devicetree), a large footprint, and none of the Teensy LED libraries | + +Either costs weeks for what the platform boundary already gives, so PJRC's core stays, confined to the platform layer. + +## Phases + +**Phase 1: boots and runs on serial.** Toolchain, `src/platform/teensy/platform_config.h` (`hasWiFi`, `hasOta`, `hasImprov` false, every LED lane count 0, `MM_MOONLIVE_HAS_HOST_JIT 0`), and the easy seam groups: time, heap, filesystem (LittleFS), GPIO table, logging, boot record (a LittleFS file), reboot, identity. `mm_main` ticks and prints its line on USB serial; a setting survives a soft reboot. Proves the C++20 build on the Teensy toolchain and the RAM fit. Works on the NE alone. + +**Phase 2: the UI over Ethernet.** QNEthernet with the W5500 driver: `TcpServer`, `TcpConnection::writeSome`, `UdpSocket` with multicast, mDNS, DHCP and static IP, rewritten onto its callback API (there is no BSD socket layer). The browser opens the device by name, shows the module tree, and the preview renders. Proves the socket port and the heap under HTTP and WebSocket load. This phase is the largest unknown and goes before LEDs. + +**Phase 3: LEDs.** A `GpioDmaPeripheral` as a new `LedPeripheral` for `ParallelLedDriver`: one eDMA channel, paced by a QTimer4 channel through the XBAR's edge-detected DMA request, writes each slot of the encoder's frame, widened to a 32-bit port word, into `GPIO1_DR`. The slot is 56 ticks of the 150 MHz bus clock (373 ns, a 1.12 ยตs WS2812 bit); the trailing zero slots the driver already appends are the latch. The lanes are any 16 of the port's pins, in any order, all on the headers and clear of the W5500's pins 9 to 13: 0, 1, 14 to 27 and 38 to 41. Pin 19 disturbed the W5500 on the bench board, so the bench leaves it unused. Each lane pin moves from the fast GPIO6 to GPIO1 through `IOMUXC_GPR_GPR26`, since DMA reaches only GPIO1 to 4, which also keeps every other pin deaf to the write. The frame sits in OCRAM, flushed from the D-cache before each transfer; above about 455 lights per lane at 8 lanes the transfer is a scatter/gather chain of DMA descriptors, since one descriptor counts at most 32767 slots. The loopback self-test captures the rx pin's edges with the cycle counter, the shape of `rmtWs2812RxCapture`. + +Measured on the bench: `GPIO1_DR` ignores 8- and 16-bit DMA writes and honours 32-bit ones (a byte and a halfword written into bits 16 to 31 left the register unchanged; the same transfer at 32 bits set it and the pins followed). So every slot is a 32-bit word with the lanes in its upper half, 12 bytes per WS2812 bit: about 74 KB per buffer for 16 lanes of 256 lights. OctoWS2811's three-channel shape stores 4 bytes per bit and is the scale path once a wall needs 1,000 or more lights per lane: + +| | One DMA channel, 32-bit slots (built first) | OctoWS2811's three-channel shape (the scale path) | +|---|---|---| +| Memory per WS2812 bit | 3 slots of 4 bytes, 12 bytes | one 4-byte word of the lanes that go low, 4 bytes | +| 16 lanes of 256 lights | about 74 KB per buffer | about 25 KB | +| 16 lanes of 1,000 lights | about 288 KB, no double buffer, near the 380 KB heap | about 96 KB | +| Encoder | the existing slot encoder | a second encoder variant, reusing the bit transposes | +| Timing | one DMA channel, one QTimer channel | three DMA channels and three QTimer channels, phase-locked | + +Steps, each verified on the bench: +1. Loopback, done: a jumper from pin 22 (`loopbackTxPin`) to pin 23 (`loopbackRxPin`) stays in place, and the self-test captures all 6144 pulses of a 256-light frame and decodes every bit. +2. One strand, done: an 8x8 WS2812B panel on pin 18, driven directly at 3.3 V and powered from VIN, shows the colors right. A 256-light frame takes 7.2 ms on the wire, and the next frame encodes during it, so the tick is the wire time plus the effects. +3. Eight lanes, then sixteen, with the frame time and the encode time measured on one core. + +Backlog: the first LED of an 8x8 panel on pin 18 flickers under the Lines effect, though the loopback decodes every bit and the slot matches the ESP32 drivers' 375 ns. The first LED is the only one fed the Teensy's raw 3.3 V edge, so the lane pad's drive strength and slew rate are the first suspects; a DMA request delayed under network load would glitch the whole panel instead. + +What a comparison with another timer-and-DMA driver for this chip taught, each to be measured before it is adopted: +- **Pad settings.** A lower drive strength (DSE 3), the slow slew rate and the lowest speed on every lane pad give cleaner edges than the core's `pinMode` defaults (DSE 6, speed 2). This is the first suspect for the flicker above, tested with the loopback and the panel. +- **Timing margin.** The 0-bit high is 373 ns, the top of the newer WS2812B's window (220 to 380 ns), where 250 to 300 ns sits in its middle. The 3-slot grid ties the 0-bit high to one slot, so a shorter one means a shorter slot, or a 4-slot grid at four thirds the memory. Real strips decide. +- **Beyond 16 lanes.** One DMA request can write the same slot to GPIO1 to 4 through the minor loop's destination offset, so the lanes are no longer bounded by one port. +- **Loopback in hardware.** A FlexPWM channel captures both edges into a DMA buffer, so the self-test needs neither interrupts off nor a 7 ms busy loop. +- **Pin advisories.** A lane on a UART, SPI or I2C pin earns a warning in the status. + +Memory bounds a lane's length. The frame stays resident twice, the encoded slots and the wider copy the DMA reads: 16 lanes of 256 lights take about 116 KB, and 16 lanes of 600 about 265 KB, near what the 512 KB heap has left beside the layers. The step past it streams the wide copy through a ring of DMA descriptors over a few small buffers, each widened at its half-complete interrupt. That caps the memory at the ring and brings back a refill deadline of a few hundred microseconds, comfortable at 600 MHz. The three-channel shape in the table above is the other route, at 4 bytes per bit, and PSRAM on the empty pads a third. + +Live scenarios on the Teensy, for the next commit: `run_live_scenario --host` ran 30, of which 19 passed, 5 failed and 6 skipped (5 run only on request, 1 only in-process). +- `Palettes_a_script_drives_the_effects`: writing a script returned HTTP 507, so the 1 MB LittleFS is full; it held 720 KB at boot, and its growth is unexplained. +- `State_one_document_adds_an_effect_with_its_controls`: applying the cards back refuses `System.deviceModel`, which is empty on the Teensy. +- `Effects_swap_while_running`: the added Pulse effect answers 404. +- `Modifiers_reshape_the_mapping` and `Effects_teardown_under_a_running_pipeline`: the grid reads 32 and 8 wide where the scenario expects 16. +- After the run the driver stayed on "GPIO DMA init failed" with valid pins until a grid change rebuilt the bus: the bus-rebuild robustness test, still open. +- At 128x128 the Layer degrades to 64x64, its status reporting too little memory, with 165 KB free and a 120 KB largest block, where one 128x128 buffer is 48 KB, so the Layer asks for more than that buffer, or reads the largest block wrong. + +Considered and set aside: FlexIO in parallel mode, since no 8 contiguous FlexIO1 or FlexIO2 pins remain free beside the W5500, and FlexIO3, whose 20 contiguous pins have no DMA. OctoWS2811 and ObjectFLED (MIT) are the prior art for the timer, XBAR and DMA setup. + +**Phase 4: services.** WLED audio sync and OSC over UDP (already covered by phase 2), I2C, PWM, an I2S microphone, USB MIDI host for a desk, the watchdog (WDOG1). Each one a seam group, each verified on the bench. + +Pins for the first sensors, clear of SPI (10 to 13), the LED lane (18), pin 19 and the loopback (22, 23); the breadboard keeps columns 37 to 48 for them: + +| Sensor | Bus | Teensy pins | +|---|---|---| +| I2S microphone (INMP441) | I2S1 | SCK to 21 (BCLK1), WS to 20 (LRCLK1), SD to 8 (IN1), L/R to GND, VDD to the 3.3 V rail | +| IMU (MPU6050) | Wire2 | SDA to 25, SCL to 24; Wire on 18 and 19 would take the LED lane and pin 19 | +| PIR (AM312) | GPIO | OUT to 41, powered from the 3.3 V rail | + +**Phase 5: MoonLive and updates.** A Thumb-2 backend in the shape of the RISC-V one (MicroPython's `asmthumb.c` is MIT prior art), with the MPU's NOEXEC lifted on one RAM region and the D-cache cleaned after a write. Firmware updates over the network through FlasherX, an image under half the flash. Catalog page, firmware variant, MIGRATING entry if anything breaks. + +**After the phases: the web installer.** The installer flashes through esptool-js over Web Serial, which speaks only the ESP32's ROM bootloader. A Teensy's bootloader is a USB HID device, so the installer needs a second writer over WebHID (Chrome and Edge on a desktop): parse the HEX, then send it in 1024-byte blocks, each behind a 64-byte header carrying its address, the protocol `teensy_loader_cli` speaks. A running firmware enters the bootloader on the same 134-baud serial request `flash_teensy` sends, or on the program button. The rest fits what exists: a `teensy41` row in `firmwares.json`, a device model for the Teensy with its W5500, and the release building the HEX. Provisioning is lighter than on an ESP32, since Ethernet takes DHCP and Improv's WiFi step has no role. Medium effort, best done with FlasherX, so a board installed from the browser then updates over the network. + +**After the phases: offer the patches upstream.** The two files in `teensy/patches/` fix upstream code, so each goes back to its maintainer once the port is done, which lets the patch files go. +- `teensy_loader_cli-match-own-devices.patch`: a pull request to PaulStoffregen/teensy_loader_cli, fixing its open issue #60 ("Error opening HID Manager" on macOS) at the cause. The macOS HID manager matches every HID device, keyboards included, which needs the Input Monitoring permission; matching only the bootloader and the rebootor needs none. +- `QNEthernet-mdns-nsec.patch`: an issue with the patch to ssilverman/QNEthernet, asking whether it belongs in QNEthernet's lwIP copy or in lwIP's patch tracker first. lwIP's mDNS responder sends no NSEC answer (still a TODO on lwIP master), so a build without IPv6 leaves a client's AAAA question unanswered and a `.local` lookup waits 5 s; the patch answers it per RFC 6762 ยง6.1. + +## What the core already handles + +- One core: the render and encode split switches itself off when a worker task cannot be spawned (`Drivers.h`), so phase 1 needs no core change for it. +- No FreeRTOS outside `src/platform`: the boundary holds. +- No MoonLive JIT: the no-JIT build compiles scripts to a refusal and runs every other module. + +## Risks, riskiest first + +1. The socket port onto QNEthernet's callback API, about 600 lines, and the non-blocking partial writes the HTTP server relies on. +2. The W5500 driver is lightly tested by its author; throughput caps large DDP or Art-Net streams. +3. Program-flash LittleFS disables interrupts while it writes, which stalls LED output during a save. +4. Single-core timing: render and encode share one core, so the frame budget is the sum. + +## What comes out + +The backlog line "Teensy 4.1: toolchain-file build, `.hex` for Teensy Loader" in `backlog-core.md` moves into this plan. `building.md`'s Teensy section ("Ethernet is built in on Teensy 4.1 and optional on 4.0", "OctoWS2811 gives excellent DMA-based LED output") is rewritten from what ships. + +## Sources + +- Teensy 4.1 and its variants: https://www.pjrc.com/store/teensy41.html +- Ethernet kit (MagJack, no PHY): https://www.pjrc.com/store/ethernet_kit.html +- QNEthernet, drivers for the Teensy 4.1 and the W5500: https://github.com/ssilverman/QNEthernet +- Throughput, native and W5500: https://github.com/grblHAL/teensy41_ethernet +- Linker script and MPU setup: https://github.com/PaulStoffregen/cores/tree/master/teensy4 +- OctoWS2811: https://github.com/PaulStoffregen/OctoWS2811 ; ObjectFLED: https://github.com/KurtMF/ObjectFLED +- teensy_loader_cli: https://github.com/PaulStoffregen/teensy_loader_cli +- FlasherX: https://github.com/joepasquariello/FlasherX +- MicroPython Thumb emitter: https://github.com/micropython/micropython/blob/master/py/asmthumb.c diff --git a/mkdocs.yml b/mkdocs.yml index 85ba84ca7..5fe36a1fd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -237,6 +237,7 @@ nav: - LightCrafter 16: reference/hardware/lightcrafter-16.md - MHC-WLED ESP32-P4 shield: reference/hardware/mhc-wled-esp32-p4-shield.md - Waveshare ESP32-P4-ETH: reference/hardware/esp32-p4-eth.md + - Teensy 4.1: reference/hardware/teensy-4-1.md - Light fixtures and DMX nodes: reference/hardware/light-fixtures.md - Control surfaces: reference/hardware/control-surfaces.md # The rules bind a contributor rather than serving a reader need, so they sit outside diff --git a/moondeck/MoonDeck.md b/moondeck/MoonDeck.md index 99296196f..ae3e8ff91 100644 --- a/moondeck/MoonDeck.md +++ b/moondeck/MoonDeck.md @@ -2,7 +2,9 @@ MoonDeck is MoonLight's browser-based developer console: one page that builds, flashes, runs, tests, monitors, and checks the project across every target, and discovers and drives devices on the network. Every action it offers is a thin wrapper around a script under `moondeck/`, so the CLI (`uv run moondeck//.py`) and MoonDeck run exactly the same code: agents typically use the CLI, humans use MoonDeck. For what MoonDeck *is* and where it sits in the workflow see [docs/how-to/building.md ยง MoonDeck](../docs/how-to/building.md#moondeck--the-dev-console). What follows is the per-script reference. -Launch it with `uv run moondeck/moondeck.py` and open . The console has three tabs โ€” **Desktop** (build build / run / test), **ESP32** (chip + port, build / flash / monitor), and **Live** (discovery and live runs against networked devices) โ€” above a network bar and per-device deviceModel pickers. Script definitions live in `moondeck/moondeck_config.json` (committed); runtime state (selected network, devices, ports) persists in `moondeck/moondeck.json` (gitignored). +Launch it with `uv run moondeck/moondeck.py` and open . The console has five tabs. **Repo**, where MoonDeck opens, holds the tests, checks, tools and docs. +**Desktop**, **ESP32** and **Teensy** build, run or flash, and monitor a target, and **Live** discovers networked devices and runs against them. +A network bar and per-device deviceModel pickers sit above them. Script definitions live in `moondeck/moondeck_config.json` (committed); runtime state (selected network, devices, ports) persists in `moondeck/moondeck.json` (gitignored). Below: the UI behaviors common to every card, described once, then one section per script grouped by the tab it appears on. Each section gives the equivalent CLI invocation, so the page doubles as the command reference for running anything without the browser. @@ -34,6 +36,46 @@ uv run moondeck/build/build_desktop.py Runs `cmake -B build/ -DCMAKE_BUILD_TYPE=Release` then `cmake --build build/ --target MoonLight`, where `` is `macos`, `linux`, or `windows` depending on the OS this script runs on. It builds ONLY the firmware binary โ€” not the ~130-file test suite โ€” so the "just give me the binary to run" path stays fast; compile the tests separately (see `compile_tests`). The per-host directory keeps an experimental Linux build from clobbering a macOS one on the same machine, and mirrors the ESP32 side's `build/esp32-/` shape. +### run_desktop + +Launch the desktop executable as a detached background process and exit. The app keeps running across other MoonDeck scripts and outlives MoonDeck itself, the way a flashed ESP32 runs independently of this console. + +```bash +uv run moondeck/run/run_desktop.py +``` + +Re-running is idempotent: any existing `MoonLight` instance is stopped first, then a fresh one is launched. Output goes to `build//MoonLight.log`. Build first. + +While the app is running, MoonDeck shows the button as **Stop** (a 5-second poll on `/api/running` detects the live process via `process_name`). Pressing Stop terminates the app; pressing Run again restarts it. From the CLI: `pkill -f build//MoonLight` (or `pkill MoonLight` if you don't have multiple host builds active). + +### run_open_stage_control + +Launch [Open Stage Control](https://openstagecontrol.ammd.net/) as a control surface for the running +device: eight faders, eight encoders and eight switches, wired to the +[OSC module](../docs/moonmodules/core/services.md) in both directions. + +```bash +uv run moondeck/run/run_open_stage_control.py # device on this machine +uv run moondeck/run/run_open_stage_control.py --host 192.168.1.42 +``` + +Then open **http://127.0.0.1:8088**. Nothing needs typing into Open Stage Control's settings panel: +the session, the send address and the listen port are all command-line arguments, and the session's +own `autosync` widget asks the device for its current state on every page load, so the widgets are +correct immediately rather than showing the layout file's defaults. + +On the device, the OSC module needs `listen` and `feedback` on. + +It runs headless (a web server, no desktop window), which is what makes the surface reachable from a +phone or another laptop on the same network. Options: `--host` / `--port` for the device, +`--listen` for where feedback arrives (the device's `feedbackPort`), `--ui-port` for the surface +itself, and `--app` when the binary is not on PATH or in the usual place. Install Open Stage Control +first; it is a free download. + +## Repo Tab + +The work on the source tree, whatever the target: tests, the checks and tools, the repo-health record, and the docs. + ### compile_tests Compile the test binaries (unit + scenario) without running them. @@ -76,42 +118,6 @@ It drives a real browser against a running MoonLight with [pytest-playwright](ht The run files are the same ones `moondeck/moontube/mtvideo.py` records the videos from, so a failure means the UI does not do what the video shows. It skips rather than fails when nothing answers on `localhost:8080` (override with `MOONLIGHT_HOST`), for the same reason the JS lane skips without node. -### run_desktop - -Launch the desktop executable as a detached background process and exit. The app keeps running across other MoonDeck scripts and outlives MoonDeck itself โ€” the same model as flashing an ESP32, where the device runs independently of this console. - -```bash -uv run moondeck/run/run_desktop.py -``` - -Re-running is idempotent: any existing `MoonLight` instance is stopped first, then a fresh one is launched. Output goes to `build//MoonLight.log`. Build first. - -While the app is running, MoonDeck shows the button as **Stop** (a 5-second poll on `/api/running` detects the live process via `process_name`). Pressing Stop terminates the app; pressing Run again restarts it. From the CLI: `pkill -f build//MoonLight` (or `pkill MoonLight` if you don't have multiple host builds active). - -### run_open_stage_control - -Launch [Open Stage Control](https://openstagecontrol.ammd.net/) as a control surface for the running -device: eight faders, eight encoders and eight switches, wired to the -[OSC module](../docs/moonmodules/core/services.md) in both directions. - -```bash -uv run moondeck/run/run_open_stage_control.py # device on this machine -uv run moondeck/run/run_open_stage_control.py --host 192.168.1.42 -``` - -Then open **http://127.0.0.1:8088**. Nothing needs typing into Open Stage Control's settings panel: -the session, the send address and the listen port are all command-line arguments, and the session's -own `autosync` widget asks the device for its current state on every page load, so the widgets are -correct immediately rather than showing the layout file's defaults. - -On the device, the OSC module needs `listen` and `feedback` on. - -It runs headless (a web server, no desktop window), which is what makes the surface reachable from a -phone or another laptop on the same network. Options: `--host` / `--port` for the device, -`--listen` for where feedback arrives (the device's `feedbackPort`), `--ui-port` for the surface -itself, and `--app` when the binary is not on PATH or in the usual place. Install Open Stage Control -first; it is a free download. - ### preview_installer ![Installer](../docs/assets/ui/installer.png) @@ -1299,3 +1305,60 @@ No MoonLight crash reports found in DiagnosticReports. === Last 40 lines of MoonLight.log === tick: 1234us (FPS: 800) free: 0 ... ``` + +## Teensy Tab + +One firmware for the Teensy 4.1, and its port found by PJRC's USB vendor id, so the tab has no dropdowns: Setup once, then Build, Flash and Monitor. + +### setup_teensy + +Install the Teensy SDK at its pinned versions. + +```bash +uv run moondeck/build/setup_teensy.py +``` + +Installs into `~/teensy` (`TEENSY_SDK` overrides), each part from its publisher. +PJRC's Teensy 4 core comes at Teensyduino's tag, LittleFS and SPI at that release's commits, and Arm's GNU toolchain checked against Arm's published sums. +teensy_loader_cli is compiled from its tagged source on macOS and Linux. +Every version is pinned in this script, which `build_teensy` and `flash_teensy` read. +A part already at its pin is left alone, so a re-run after a pin bump fetches only what changed. +Our changes to those parts live as patch files in `teensy/patches/`, applied after each checkout. +lwIP's mDNS answers an IPv6 address question with "none", so `.local` resolves at once. +The W5500 driver bounds its waits on the chip, so a misbehaving chip costs packets rather than the device. +The macOS loader looks only for the Teensy bootloader, so flashing needs no Input Monitoring permission. + +### build_teensy + +Build the Teensy 4.1 firmware. + +```bash +uv run moondeck/build/build_teensy.py +``` + +Builds into `build/teensy41/`: `MoonLight.elf`, and `MoonLight.hex` for the loader, printing the section sizes. +Configures CMake with `teensy/teensy41.cmake` on first use, passing the SDK paths from `setup_teensy`. + +### flash_teensy + +Flash the built firmware over USB. + +```bash +uv run moondeck/build/flash_teensy.py +``` + +A running MoonLight is asked into its bootloader through its USB serial port, so no button press is needed; a board without a running firmware waits for its program button. +`--port` names the port when more than one Teensy is attached. +Gives up after 60 seconds with what to check. + +### monitor_teensy + +Monitor the Teensy's USB serial output. Long-running, shows a Stop button. + +```bash +uv run moondeck/run/monitor_teensy.py +uv run moondeck/run/monitor_teensy.py --seconds 10 +``` + +Waits for the port to appear, so it catches the boot lines straight after a flash: the firmware waits up to two seconds for a monitor before it starts. +Opening a Teensy's port does not reset it. Output is saved to `build/teensy41/monitor.log`. diff --git a/moondeck/build/build_teensy.py b/moondeck/build/build_teensy.py new file mode 100644 index 000000000..b7aed1ec5 --- /dev/null +++ b/moondeck/build/build_teensy.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +"""Build the Teensy 4.1 firmware into build/teensy41/: MoonLight.elf, and MoonLight.hex for the loader. + +Configures CMake on first use with the SDK setup_teensy.py installed, at the versions pinned there. +""" + +import argparse +import subprocess +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from setup_teensy import gcc_bin, missing, sdk_root, teensyduino_define # noqa: E402 + +ROOT = Path(__file__).resolve().parent.parent.parent +BUILD_DIR = ROOT / "build" / "teensy41" +HEX = BUILD_DIR / "MoonLight.hex" +# What platform_teensy.cpp leaves the program of the 8 MB flash: LittleFS takes 1 MB and the reserved top 256 KB. +PROGRAM_FLASH_BYTES = (8192 - 1024 - 256) * 1024 + + +def hex_image_bytes(path: Path) -> int: + """The bytes an Intel HEX image writes to flash: the sum of its data records (type 00).""" + total = 0 + for line in path.read_text().splitlines(): + if line.startswith(":") and line[7:9] == "00": + total += int(line[1:3], 16) + return total + + +def main(): + argparse.ArgumentParser(description=__doc__).parse_args() + left = missing() + if left: + sys.exit(f"Teensy SDK incomplete ({', '.join(left)}): run uv run moondeck/build/setup_teensy.py") + r = subprocess.run(["cmake", "-S", str(ROOT / "teensy"), "-B", str(BUILD_DIR), "-G", "Ninja", + f"-DCMAKE_TOOLCHAIN_FILE={ROOT / 'teensy' / 'teensy41.cmake'}", + f"-DTEENSY_SDK={sdk_root()}", f"-DTEENSY_GCC_BIN={gcc_bin()}", + f"-DTEENSYDUINO={teensyduino_define()}"], cwd=ROOT) + if r.returncode != 0: + sys.exit(r.returncode) + sys.exit(subprocess.run(["cmake", "--build", str(BUILD_DIR)], cwd=ROOT).returncode) + + +if __name__ == "__main__": + main() diff --git a/moondeck/build/flash_teensy.py b/moondeck/build/flash_teensy.py new file mode 100644 index 000000000..d9a0983f9 --- /dev/null +++ b/moondeck/build/flash_teensy.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# /// script +# dependencies = ["pyserial"] +# /// +"""Flash build/teensy41/MoonLight.hex onto a Teensy 4.1 over USB with teensy_loader_cli. + +A running MoonLight is asked into its bootloader through its USB serial port, so no button press is needed. +A board without a running firmware waits for its program button. +""" + +import argparse +import subprocess +import sys +import time +from pathlib import Path + +import serial +from serial.tools import list_ports + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from setup_teensy import loader_path # noqa: E402 +from build_teensy import HEX # noqa: E402 + +ROOT = Path(__file__).resolve().parent.parent.parent +PJRC_VID = 0x16C0 +# PJRC's convention: the core reboots into its bootloader when the USB serial port is set to 134 baud. +BOOTLOADER_BAUD = 134 +LOADER_TIMEOUT_S = 60 + + +def teensy_port(): + """The first Teensy USB serial port, or None.""" + return next((p.device for p in list_ports.comports() if p.vid == PJRC_VID), None) + + +def main(): + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--port", help="the Teensy's serial port; found by its USB vendor id when omitted") + args = ap.parse_args() + if not HEX.exists(): + sys.exit(f"No {HEX.relative_to(ROOT)}: run uv run moondeck/build/build_teensy.py") + if not loader_path().exists(): + sys.exit("No teensy_loader_cli: run uv run moondeck/build/setup_teensy.py") + port = args.port or teensy_port() + if port: + print(f"Asking the Teensy on {port} into its bootloader ...", flush=True) + try: + serial.Serial(port, BOOTLOADER_BAUD).close() + except serial.SerialException as e: + print(f" {e}; press the program button instead", flush=True) + time.sleep(0.5) + else: + print("No running Teensy found: press its program button", flush=True) + try: + r = subprocess.run([str(loader_path()), "--mcu=TEENSY41", "-w", "-v", str(HEX)], timeout=LOADER_TIMEOUT_S) + except subprocess.TimeoutExpired: + r = None + if r is None or r.returncode != 0: + sys.exit("The loader did not reach the Teensy's bootloader: check the USB cable carries data," + " then press the program button and flash again.") + + +if __name__ == "__main__": + main() diff --git a/moondeck/build/setup_teensy.py b/moondeck/build/setup_teensy.py new file mode 100644 index 000000000..92de2fdc3 --- /dev/null +++ b/moondeck/build/setup_teensy.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +# /// script +# dependencies = ["truststore"] +# /// +"""Install the Teensy 4.1 SDK at its pinned versions into ~/teensy (TEENSY_SDK overrides). + +Four parts, each from its publisher at a pinned version: +PJRC's Teensy 4 core, LittleFS and SPI, and QNEthernet, from GitHub, +Arm's GNU toolchain for bare-metal Arm (checksum-verified), +and teensy_loader_cli, compiled from its tagged source on macOS and Linux. + +Idempotent: a part already at its pin is left alone, so a re-run after a pin bump fetches only what changed. +build_teensy.py reads the pins and paths from here, so every version has this one home. +""" + +import argparse +import hashlib +import os +import platform +import shutil +import subprocess +import sys +import tarfile +import urllib.request +import zipfile +from pathlib import Path + +# PJRC's sources: the core at Teensyduino's release tag, the libraries (untagged) at the commit that release ships. +TEENSYDUINO = "1.62" +REPOS = { + "cores": ("https://github.com/PaulStoffregen/cores.git", "a664effb008d1ac8d8f00f3f19b47c0d1ea46e3b"), + "LittleFS": ("https://github.com/PaulStoffregen/LittleFS.git", "8c2badab74a01795931ea14adc85ec6da3db235f"), + "SPI": ("https://github.com/PaulStoffregen/SPI.git", "7c83d0726746b652af37319e70cd3932c253ecae"), + # lwIP and the W5500 driver (AGPL-3.0-or-later), at release v0.37.0. + "QNEthernet": ("https://github.com/ssilverman/QNEthernet.git", "9b6018d81d91a86c6f04fa7814fb8884ed8ef128"), +} + +GCC_VERSION = "15.3.rel2" +GCC_URL = ("https://gitlab.arm.com/api/v4/projects/tooling%2Fgnu-toolchains-for-arm/packages/generic/" + f"gnu-toolchain/{GCC_VERSION}/") +# Arm publishes these four hosts; the checksums are Arm's own .sha256asc files. +GCC_HOSTS = { + ("Darwin", "arm64"): ("darwin-arm64", "tar.xz", "b6ccf805aa4fb8c50406256f99cf802385894b86c0cb6c0185dc0e2b5f8288b9"), + ("Linux", "x86_64"): ("x86_64", "tar.xz", "0662b2f01e0cd8b951fd110911d339c35b05998c68a12b3530bbbb4bf6c7874e"), + ("Linux", "aarch64"): ("aarch64", "tar.xz", "9fc9e6baedf2b6779bd39ce4cc458d3529e200bc92bebe28be18f8942a456ad8"), + ("Windows", "AMD64"): ("mingw-w64-x86_64", "zip", "062888d4f8a213f930e367189804910e5215c4e623e5063e64494c302f94b889"), +} + +LOADER_VERSION = "2.3" +LOADER_REPO = ("https://github.com/PaulStoffregen/teensy_loader_cli.git", "03fca4156c244c7ad36bd368cf6e24531dbd566a") +# One C file; its Makefile assumes a long-gone macOS SDK path, so it is compiled directly. +LOADER_BUILD = { + "Darwin": ["-DUSE_APPLE_IOKIT", "-framework", "IOKit", "-framework", "CoreFoundation"], + "Linux": ["-DUSE_LIBUSB", "-lusb"], +} + + +def sdk_root() -> Path: + return Path(os.environ.get("TEENSY_SDK", Path.home() / "teensy")) + + +def teensyduino_define() -> int: + """The TEENSYDUINO value the core's sources expect, 162 for 1.62.""" + major, minor = TEENSYDUINO.split(".") + return int(major) * 100 + int(minor) + + +def gcc_host(): + host = GCC_HOSTS.get((platform.system(), platform.machine())) + if not host: + sys.exit(f"Arm publishes no {GCC_VERSION} toolchain for {platform.system()} {platform.machine()}") + return host + + +def gcc_bin() -> Path: + return sdk_root() / f"arm-gnu-toolchain-{GCC_VERSION}-{gcc_host()[0]}-arm-none-eabi" / "bin" + + +def loader_path() -> Path: + return sdk_root() / "teensy_loader_cli" / "teensy_loader_cli" + + +def head(repo: Path) -> str: + r = subprocess.run(["git", "-C", str(repo), "rev-parse", "HEAD"], capture_output=True, text=True) + return r.stdout.strip() if r.returncode == 0 else "" + + +def missing() -> list: + """The parts not installed at their pin, empty when the SDK is ready to build and flash.""" + out = [name for name, (_, pin) in REPOS.items() if head(sdk_root() / name) != pin] + out += [p.name for name in REPOS for p in patches_for(name) if not patch_applied(sdk_root() / name, p)] + if not (gcc_bin() / "arm-none-eabi-g++").exists() and not (gcc_bin() / "arm-none-eabi-g++.exe").exists(): + out.append(f"arm-gnu-toolchain {GCC_VERSION}") + if platform.system() in LOADER_BUILD and not loader_path().exists(): + out.append("teensy_loader_cli") + return out + + +def git_at(url: str, ref: str, dest: Path): + if not (dest / ".git").exists(): + subprocess.run(["git", "clone", "--filter=blob:none", url, str(dest)], check=True) + if head(dest) == ref: + return + subprocess.run(["git", "-C", str(dest), "fetch", "--tags", "origin"], check=True) + # Forced, since the tree carries our patches, which apply_patches puts back on the new pin. + subprocess.run(["git", "-C", str(dest), "checkout", "--force", "--detach", ref], check=True) + + +def install_gcc(): + if (gcc_bin() / "arm-none-eabi-g++").exists() or (gcc_bin() / "arm-none-eabi-g++.exe").exists(): + return + host, ext, sha = gcc_host() + name = f"arm-gnu-toolchain-{GCC_VERSION}-{host}-arm-none-eabi.{ext}" + archive = sdk_root() / name + # The system's certificate store, as curl and git use, so a network with its own root certificate downloads too. + import truststore + truststore.inject_into_ssl() + print(f"Downloading {name} ...") + with urllib.request.urlopen(GCC_URL + name) as resp, open(archive, "wb") as f: + shutil.copyfileobj(resp, f) + digest = hashlib.sha256(archive.read_bytes()).hexdigest() + if digest != sha: + archive.unlink() + sys.exit(f"{name}: checksum {digest} does not match Arm's {sha}") + print("Unpacking ...") + if ext == "zip": + with zipfile.ZipFile(archive) as z: + z.extractall(sdk_root()) + else: + with tarfile.open(archive) as t: + t.extractall(sdk_root(), filter="tar") + archive.unlink() + + +# Our changes to an upstream part, one file per change under teensy/patches/, named after the part they apply to. +PATCH_DIR = Path(__file__).resolve().parent.parent.parent / "teensy" / "patches" + + +def patches_for(part: str) -> list: + return sorted(PATCH_DIR.glob(f"{part}-*.patch")) + + +def patch_applied(repo: Path, patch: Path) -> bool: + """Whether the patch is already in the working tree: its reverse applies cleanly.""" + return subprocess.run(["git", "-C", str(repo), "apply", "--reverse", "--check", str(patch)], + capture_output=True).returncode == 0 + + +def apply_patches(part: str, repo: Path) -> bool: + """Apply the part's patches when its tree lacks any; True when they were applied.""" + patches = patches_for(part) + if all(patch_applied(repo, patch) for patch in patches): + return False + # An edited patch meets the tree its earlier version changed, so every patch starts from the pinned tree. + subprocess.run(["git", "-C", str(repo), "checkout", "--force", "."], check=True) + for patch in patches: + if subprocess.run(["git", "-C", str(repo), "apply", str(patch)]).returncode != 0: + sys.exit(f"{patch.name} does not apply to {part} at its pin; update the patch with the pin") + print(f"Applied {patch.name}") + return True + + +def install_loader(): + flags = LOADER_BUILD.get(platform.system()) + if not flags: + print("teensy_loader_cli: no source build for this host; flash with PJRC's Teensy app instead.") + return + src = sdk_root() / "teensy_loader_cli" + git_at(LOADER_REPO[0], LOADER_REPO[1], src) + patched = apply_patches("teensy_loader_cli", src) + if loader_path().exists() and not patched: + return + r = subprocess.run(["cc", "-O2", "-Wall", "teensy_loader_cli.c", "-o", "teensy_loader_cli", *flags], cwd=src) + if r.returncode != 0: + hint = " (install libusb-dev, the libusb 0.1 headers)" if platform.system() == "Linux" else "" + sys.exit(f"teensy_loader_cli did not compile{hint}") + + +def main(): + argparse.ArgumentParser(description=__doc__).parse_args() + sdk_root().mkdir(parents=True, exist_ok=True) + for name, (url, pin) in REPOS.items(): + git_at(url, pin, sdk_root() / name) + apply_patches(name, sdk_root() / name) + install_gcc() + install_loader() + left = missing() + if left: + sys.exit(f"Still missing: {', '.join(left)}") + print(f"Teensy SDK ready in {sdk_root()}: Teensyduino {TEENSYDUINO} sources, Arm GCC {GCC_VERSION}, teensy_loader_cli {LOADER_VERSION}") + + +if __name__ == "__main__": + main() diff --git a/moondeck/check/repo_health.py b/moondeck/check/repo_health.py index f7d966139..6d27463bc 100644 --- a/moondeck/check/repo_health.py +++ b/moondeck/check/repo_health.py @@ -43,6 +43,7 @@ from build_esp32 import APP_BIN, FIRMWARES, table_from_fragments, find_idf, idf_env # noqa: E402 import flash_split # noqa: E402 (beside this file; the commit's flash change by kind) from build_desktop import desktop_binary # noqa: E402 (one definition of where it lands) +from build_teensy import HEX as TEENSY_HEX, PROGRAM_FLASH_BYTES as TEENSY_PROGRAM_BYTES, hex_image_bytes # noqa: E402 HEALTH_FILE = ROOT / "docs" / "reference" / "metrics" / "repo-health.json" # The same snapshot as a table a human reads: units applied, ratios as percentages, areas # grouped. The JSON stays the source the delta is computed from; this is the view. Both @@ -239,6 +240,11 @@ def measure_flash(): parts = flash_split.measure(d, elf, st.st_size, _head(), env) if parts: FLASH_SPLIT[firmware] = parts + # The Teensy image, under the same freshness rule: the bytes its HEX file writes to flash. + if TEENSY_HEX.exists() and (_time.time() - TEENSY_HEX.stat().st_mtime) <= MEASURED_WITHIN_HOURS * 3600: + flash["teensy41"] = hex_image_bytes(TEENSY_HEX) + MEASURED_THIS_RUN.add("teensy41") + MEASURED_DATES["teensy41"] = _dt.date.today().isoformat() # The desktop binary, located by build_desktop.desktop_binary() so this and collect_kpi.py # cannot name different files in the same run. A bare build/MoonLight matched nothing off # macOS, so this metric silently carried a foreign machine's number forward while reading as @@ -532,7 +538,7 @@ def _arrow(new, old, key, fmt=str, lower_is_better=True): # happens to carry: a new target should be a deliberate addition, and "unknown" (a scenario run # before targets were named) is a data defect, not a device. MATRIX_TARGETS = ("desktop-macos", "desktop-windows", "esp32", "esp32s3-n16r8", - "esp32p4rev1-eth", "esp32s31", "esp32-eth", "esp32-eth-wifi") + "esp32p4rev1-eth", "esp32s31", "esp32-eth", "esp32-eth-wifi", "teensy41") def reference_tag(): @@ -558,16 +564,18 @@ def load_reference(tag): def gains(new, ref, since="", code=(None, None)): - """The numbers a user feels, as (metric, unit, reference, now), for each one both snapshots hold. + """The numbers a user feels, as (metric, unit, reference, now), for each one both snapshots hold; a firmware the release lacks comes with reference None. A target's flash counts only when it was rebuilt after `since`, the release date: a number carried forward unchanged would read as no growth. `code` is the code report's total at the release and now. """ rows = [] measured = new.get("measured", {}) - for k in sorted(set(new.get("flash", {})) & set(ref.get("flash", {}))): - if since and str(measured.get(k, "")) <= since: + # Without a release flash table every target would read as new, so then only the shared ones count. + released = ref.get("flash", {}) + for k in sorted(new.get("flash", {})): + if (since and str(measured.get(k, "")) <= since) or (not released and k not in released): continue - rows.append((f"flash {k}", "KB", ref["flash"][k], new["flash"][k])) + rows.append((f"flash {k}", "KB", released.get(k), new["flash"][k])) heap_ref = (ref.get("perf", {}).get("esp32") or {}).get("heap_free") heap_now = (new.get("perf", {}).get("esp32") or {}).get("heap_free") if heap_ref and heap_now: @@ -614,6 +622,9 @@ def render_gains(new, ref, tag): return [] L = [f"## Since {tag}", "", f"| Metric | {tag} | Now | Change |", "|---|---:|---:|---:|"] for metric, unit, was, now in rows: + if was is None: # a target the release did not ship + L.append(f"| {metric} | - | {_gain_cell(unit, now)} | new |") + continue diff = now - was pct = f" ({100.0 * diff / was:+.1f}%)" if was else "" change = (f"{diff / 1024:+.1f} KB" if unit == "KB" else f"{diff:+,}") + pct @@ -646,7 +657,7 @@ def render_markdown(new, old): L += ["## Firmware size", "", "| Target | Flash | Capacity | Used | Built |", "|---|---:|---:|---:|:--:|"] for k, v in sorted(new["flash"].items()): - cap = app_partition_bytes(k) if k.startswith("esp32") else 0 + cap = app_partition_bytes(k) if k.startswith("esp32") else TEENSY_PROGRAM_BYTES if k == "teensy41" else 0 cap_s = _kb(cap) if cap else "-" used = f"{(100.0 * v / cap):.0f}%" if cap else "-" built = _built_label(k, new.get("measured", {}).get(k)) diff --git a/moondeck/moondeck.py b/moondeck/moondeck.py index 713629e0b..9028060a1 100644 --- a/moondeck/moondeck.py +++ b/moondeck/moondeck.py @@ -537,7 +537,7 @@ def load_state(): if "networks" not in state and ("devices" in state or "port" in state): state = _migrate_to_networks(state) return state - return {"networks": [], "active_network": "", "tab": "desktop"} + return {"networks": [], "active_network": "", "tab": "repo"} def _migrate_to_networks(old_state: dict) -> dict: @@ -584,7 +584,7 @@ def _migrate_to_networks(old_state: dict) -> dict: if k not in ("devices", "port", "env")} new_state["networks"] = networks new_state["active_network"] = networks[0]["name"] if networks else "" - new_state.setdefault("tab", "desktop") + new_state.setdefault("tab", "repo") print(f"moondeck: migrated {len(devices)} device(s) into {len(networks)} " f"network(s): {', '.join(n['name'] for n in networks)}", file=sys.stderr) return new_state diff --git a/moondeck/moondeck_config.json b/moondeck/moondeck_config.json index 4c346d97a..0518147b0 100644 --- a/moondeck/moondeck_config.json +++ b/moondeck/moondeck_config.json @@ -11,7 +11,7 @@ }, { "id": "compile_tests", - "tab": "desktop", + "tab": "repo", "group": "test", "label": "Compile Tests", "speed": "slow", @@ -23,7 +23,7 @@ }, { "id": "test_desktop", - "tab": "desktop", + "tab": "repo", "group": "test", "label": "Unit Test", "speed": "medium", @@ -33,7 +33,7 @@ }, { "id": "test_host", - "tab": "desktop", + "tab": "repo", "group": "test", "label": "Host Tests", "speed": "medium", @@ -42,7 +42,7 @@ }, { "id": "scenario_pipeline", - "tab": "desktop", + "tab": "repo", "group": "test", "label": "Scenarios", "speed": "medium", @@ -73,7 +73,7 @@ }, { "id": "preview_installer", - "tab": "desktop", + "tab": "repo", "group": "run", "label": "Installer Preview", "speed": "medium", @@ -95,7 +95,7 @@ }, { "id": "check_module", - "tab": "desktop", + "tab": "repo", "group": "tools", "label": "All Tools on Module", "speed": "medium", @@ -105,7 +105,7 @@ }, { "id": "check_clang_tidy", - "tab": "desktop", + "tab": "repo", "group": "tools", "label": "clang-tidy", "speed": "slow", @@ -115,7 +115,7 @@ }, { "id": "check_clang_query", - "tab": "desktop", + "tab": "repo", "group": "tools", "label": "clang-query", "speed": "medium", @@ -124,7 +124,7 @@ }, { "id": "check_nonblocking", - "tab": "desktop", + "tab": "repo", "group": "tools", "label": "clang-hotpath", "speed": "slow", @@ -133,7 +133,7 @@ }, { "id": "check_code", - "tab": "desktop", + "tab": "repo", "group": "tools", "label": "Code report", "speed": "medium", @@ -142,7 +142,7 @@ }, { "id": "check_codeql", - "tab": "desktop", + "tab": "repo", "group": "tools", "label": "CodeQL (CI alerts)", "speed": "instant", @@ -159,7 +159,7 @@ }, { "id": "check_footprint", - "tab": "desktop", + "tab": "repo", "group": "tools", "label": "Footprint (ESP32)", "speed": "instant", @@ -168,7 +168,7 @@ }, { "id": "check_specs", - "tab": "desktop", + "tab": "repo", "group": "check", "label": "Spec Check", "speed": "instant", @@ -177,7 +177,7 @@ }, { "id": "check_docgen", - "tab": "desktop", + "tab": "repo", "group": "check", "label": "Docgen Check", "speed": "instant", @@ -196,7 +196,7 @@ }, { "id": "check_devices", - "tab": "desktop", + "tab": "repo", "group": "check", "label": "Board Catalog", "speed": "instant", @@ -205,7 +205,7 @@ }, { "id": "check_firmwares", - "tab": "desktop", + "tab": "repo", "group": "check", "label": "Firmware List", "speed": "instant", @@ -214,7 +214,7 @@ }, { "id": "collect_kpi", - "tab": "desktop", + "tab": "repo", "group": "check", "label": "KPI Report", "speed": "slow", @@ -223,7 +223,7 @@ }, { "id": "repo_health", - "tab": "desktop", + "tab": "repo", "group": "check", "label": "Repo Health", "speed": "medium", @@ -232,7 +232,7 @@ }, { "id": "history_report", - "tab": "desktop", + "tab": "repo", "group": "check", "label": "History Report", "speed": "instant", @@ -241,7 +241,7 @@ }, { "id": "install_playwright", - "tab": "desktop", + "tab": "repo", "group": "docs", "label": "Install Playwright", "speed": "slow", @@ -250,7 +250,7 @@ }, { "id": "screenshot_modules", - "tab": "desktop", + "tab": "repo", "group": "docs", "label": "Screenshot Modules", "speed": "slow", @@ -275,7 +275,7 @@ }, { "id": "update_module_docs", - "tab": "desktop", + "tab": "repo", "group": "docs", "label": "Update Module Docs", "speed": "slow", @@ -284,7 +284,7 @@ }, { "id": "build_docs", - "tab": "desktop", + "tab": "repo", "group": "docs", "label": "Preview Docs Site", "speed": "slow", @@ -488,6 +488,44 @@ "help": "wled_migration_test", "script": "build/wled_migration_test.py", "needs_port": true + }, + { + "id": "setup_teensy", + "tab": "teensy", + "group": "setup", + "label": "Setup SDK", + "speed": "slow", + "help": "setup_teensy", + "script": "build/setup_teensy.py" + }, + { + "id": "build_teensy", + "tab": "teensy", + "group": "build", + "label": "Build", + "speed": "slow", + "help": "build_teensy", + "script": "build/build_teensy.py" + }, + { + "id": "flash_teensy", + "tab": "teensy", + "group": "flash", + "label": "Flash", + "speed": "medium", + "help": "flash_teensy", + "script": "build/flash_teensy.py" + }, + { + "id": "monitor_teensy", + "tab": "teensy", + "group": "run", + "label": "Monitor", + "speed": "medium", + "help": "monitor_teensy", + "script": "run/monitor_teensy.py", + "long_running": true, + "process_name": "monitor_teensy" } ] } diff --git a/moondeck/moondeck_ui/app.js b/moondeck/moondeck_ui/app.js index d0b60cb0f..6688a96c9 100644 --- a/moondeck/moondeck_ui/app.js +++ b/moondeck/moondeck_ui/app.js @@ -325,7 +325,9 @@ function renderScripts() { const containers = { desktop: document.getElementById("scripts-desktop"), esp32: document.getElementById("scripts-esp32"), + teensy: document.getElementById("scripts-teensy"), live: document.getElementById("scripts-live"), + repo: document.getElementById("scripts-repo"), }; // The esp32 tab splits its scripts across three containers so the // dropdowns (Firmware, Port) can sit *between* the script groups they diff --git a/moondeck/moondeck_ui/index.html b/moondeck/moondeck_ui/index.html index 2111ceed2..55c769395 100644 --- a/moondeck/moondeck_ui/index.html +++ b/moondeck/moondeck_ui/index.html @@ -13,8 +13,10 @@

MoonDeck

v{{VERSION}} @@ -37,7 +39,7 @@

MoonDeck

-
+
@@ -82,6 +84,16 @@

MoonDeck

+ +
+
+
+ + +
+
+
+
+
+
+ +
+ +
+
+ + +
+
Flashed. The board prints its network address for its first minute: click below and pick the Teensy's serial port to show it.
+ +
+ + +
+
+
Preparing to flashโ€ฆ
diff --git a/mooninstaller/install-orchestrator.js b/mooninstaller/install-orchestrator.js index 6af2be9d3..4ff900f49 100644 --- a/mooninstaller/install-orchestrator.js +++ b/mooninstaller/install-orchestrator.js @@ -71,6 +71,15 @@ import { encodeApplyOpFrames, stateFrames, } from "./improv-frame.js"; +import { + TEENSY_FAMILY, + imageProblem, + isTeensySerial, + rebootToBootloader, + rebootedSerialPort, + requestHalfKay, + writeImage, +} from "./teensy-writer.js"; // --------------------------------------------------------------------------- // Manifest parser @@ -445,6 +454,82 @@ export function flashPercent(sizes, idx, written, total) { return pct < 0 ? 0 : pct > 100 ? 100 : pct; } +// Installs a Teensy 4.1 through its HalfKay bootloader (teensy-writer.js), from a running board's serial port or, with no port, from a board held in its bootloader by the program button. +// The outcome is the address its boot log prints, and the catalog defaults go over the same serial port as the ESP32's do (APPLY_OP); there is no WiFi to provision. +async function installTeensy({ manifest, port, device, applyDefaults, eraseBefore, trackProgress, onProgress, onLog, uiGrantBootloader, uiFindBoard, onSuccess }) { + if (!navigator.hid) throw new Error("WebHID is not available: install a Teensy from Chrome or Edge on a desktop computer"); + if (!uiGrantBootloader) throw new Error("the installer cannot ask for the Teensy bootloader"); + const log = (line) => { if (onLog) onLog(`[teensy] ${line}`); }; + + // The bootloader, picked in the browser's chooser. + // The chooser needs a recent click and does not remember the grant, so it opens straight from the Install click while that still counts (a few seconds), listing the bootloader as it appears. + // When the click has expired, or the user cancels, the page says what to do and a click of its own opens the chooser. + const bootloader = async (pressButton) => { + try { + const dev = await requestHalfKay(); + if (dev) return dev; + log("no bootloader picked in the browser's list"); + } catch (e) { + log(`the browser's list needs a fresh click (${e && e.message || e})`); + } + trackProgress("grant-bootloader", { pressButton }); + log("waiting for the bootloader to be chosen"); + return await uiGrantBootloader(); + }; + + // A board held in its bootloader has no serial port: the bootloader is the target from the start. + let halfkay = port ? null : await bootloader(true); + + trackProgress("fetch-firmware"); + log(`downloading ${manifest.parts[0].url}`); + const url = manifest.parts[0].url; + const res = await fetch(url); + if (!res.ok) throw new Error(`firmware fetch HTTP ${res.status}: ${url}`); + const image = new Uint8Array(await res.arrayBuffer()); + const problem = imageProblem(image); + if (problem) throw new Error(problem); + + log(`image ${image.length} bytes`); + if (!halfkay) { + trackProgress("reboot-bootloader"); + log("asking the running board into its bootloader (134 baud)"); + await rebootToBootloader(port); + halfkay = await bootloader(false); + } + + trackProgress("flash", { pct: 0 }); + await writeImage(halfkay, image, { onLog, eraseSettings: eraseBefore, onProgress: (pct) => onProgress("flash", { pct }) }); + + trackProgress("reboot"); + let ip = "", mdns = "", defaultsApplied = false; + let serial = await rebootedSerialPort(15000); + if (!serial && uiFindBoard) { + // The board was installed from its bootloader, so the installer may not read its serial port yet: one click grants it. + log("no granted serial port for the board: asking for one"); + trackProgress("find-board"); + serial = await uiFindBoard(); + } + log(serial ? "reading the board's address from its serial log" : "no serial port for the board: no address to show"); + if (serial) { + trackProgress("read-address"); + try { + await serial.open({ baudRate: 115200 }); + ({ ip, mdns } = await readBootIp(serial, 20000)); + defaultsApplied = await pushDefaultsOverSerial(serial, device, applyDefaults, trackProgress, onLog); + } catch (e) { + if (onLog) onLog(`[teensy] could not read the boot log (${e && e.message || e})`); + } finally { + try { await serial.close(); } catch (_) { /* ignore */ } + } + } + trackProgress("done"); + if (ip) { + onSuccess({ url: `http://${ip}/`, mdns, device: device || "", applyDefaults, defaultsApplied, viaHttp: true, alreadyOnline: true }); + } else { + onSuccess({ url: "", device: device || "", applyDefaults, defaultsApplied, ethOnlyNoLink: true }); + } +} + export const installer = { /** * Drive the full install flow: request port, flash via esptool-js, @@ -532,13 +617,20 @@ export const installer = { * When provided the orchestrator skips its own requestPort() prompt * and uses this handle directly. Falls back to requestPort() when * omitted/null OR when opening this handle fails (stale grant after - * the device was unplugged and replugged). + * the device was unplugged and replugged). A Teensy manifest with no + * port means a board held in its bootloader by its program button. + * @param {() => Promise} [opts.uiGrantBootloader] + * Teensy only: host shows a button whose click calls requestHalfKay(), + * and resolves once the user has picked the bootloader in the browser's list. + * @param {() => Promise} [opts.uiFindBoard] + * Teensy only: host shows a button whose click calls requestTeensySerial(), + * resolving with the board's port, or null when the user skips. */ async start({ manifestUrl, device, applyDefaults = true, txPower = null, eraseBefore = false, ethOnly = false, port: prePickedPort, onProgress, uiWaitForCreds, uiWaitForIp, uiShowNeedsIpRetrying, - uiWaitForPortRetry, + uiWaitForPortRetry, uiGrantBootloader, uiFindBoard, onSuccess, onError, onLog }) { if (!navigator.serial) { onError("setup", new Error("Web Serial API not available โ€” use Chrome, Edge, or Opera")); @@ -595,6 +687,14 @@ export const installer = { }); try { + // The manifest's chip family picks the writer, before anything touches the device. + const manifest = await fetchManifest(manifestUrl); + if (manifest.chipFamily === TEENSY_FAMILY) { + if (_detected) await releaseDetected(); + await installTeensy({ manifest, port: prePickedPort, device, applyDefaults, eraseBefore, trackProgress, + onProgress, onLog, uiGrantBootloader, uiFindBoard, onSuccess }); + return; + } trackProgress("request-port"); // Use the pre-picked port when the host page provided one (the // "USB Port" dropdown on the install page). Otherwise surface @@ -715,7 +815,6 @@ export const installer = { // Failing here (CDN issue, bad manifest) leaves the device // intact โ€” better than mid-flash failure with an erased chip. trackProgress("fetch-firmware"); - const manifest = await fetchManifest(manifestUrl); const fileArray = []; for (const part of manifest.parts) { const res = await fetch(part.url); @@ -1144,6 +1243,8 @@ export const installer = { await releaseDetected(); const port = prePickedPort || await navigator.serial.requestPort({}); + // A Teensy names itself by its USB ids, and an ESP32 handshake would only confuse it. + if (isTeensySerial(port.getInfo())) return TEENSY_FAMILY; // Trimmed espTerminal (same shape as start()'s) so chip-info lines reach // the log panel; the single-char filter drops esptool's "." / "_" noise. const espTerminal = onLog ? { diff --git a/mooninstaller/install.js b/mooninstaller/install.js index 67807f6a9..bd931e058 100644 --- a/mooninstaller/install.js +++ b/mooninstaller/install.js @@ -18,6 +18,7 @@ import { installPicker } from "./install-picker.js"; import { myDevices } from "./devices.js"; import { installer, ESPTOOL_JS_VERSION } from "./install-orchestrator.js"; +import { TEENSY_FAMILY, requestHalfKay, requestTeensySerial } from "./teensy-writer.js"; // Device catalog + chip detection: mooninstaller only, kept out of the // firmware-embedded install-picker.js and injected here via deviceSupport. import * as deviceSupport from "./install-picker-devices.js"; @@ -104,6 +105,8 @@ document.addEventListener('DOMContentLoaded', () => { const sections = { connecting: document.getElementById("section-connecting"), wrongPort: document.getElementById("section-wrong-port"), + grantBootloader: document.getElementById("section-grant-bootloader"), + findBoard: document.getElementById("section-find-board"), flashing: document.getElementById("section-flashing"), wifiForm: document.getElementById("section-wifi-form"), provisioning: document.getElementById("section-provisioning"), @@ -265,6 +268,27 @@ document.addEventListener('DOMContentLoaded', () => { bar.style.width = `${pct}%`; break; } + case "reboot-bootloader": + showSection("connecting"); + document.getElementById("connecting-detail").textContent = + "Asking the Teensy into its bootloaderโ€ฆ"; + break; + case "grant-bootloader": + showSection("grantBootloader"); + document.getElementById("grant-bootloader-text").textContent = detail && detail.pressButton + ? "Press the program button on the Teensy now, then click below and pick \"Unknown device (16C0:0478)\", the bootloader, in the browser's list." + : "The Teensy is in its bootloader. Click below and pick \"Unknown device (16C0:0478)\", the bootloader, in the browser's list."; + document.getElementById("grant-bootloader-hint").hidden = true; + break; + case "find-board": + showSection("findBoard"); + document.getElementById("find-board-hint").hidden = true; + break; + case "read-address": + showSection("connecting"); + document.getElementById("connecting-detail").textContent = + "Waiting for the board's network addressโ€ฆ"; + break; case "reboot": showSection("connecting"); document.getElementById("connecting-detail").textContent = @@ -429,6 +453,52 @@ document.addEventListener('DOMContentLoaded', () => { }); } + // Resolves with the Teensy bootloader device the user picks in the browser's list. + // requestDevice needs a user gesture, so it runs inside the button's click; a cancel or an empty list keeps the step open with a hint. + function uiGrantBootloader() { + return new Promise((resolve) => { + const btn = document.getElementById("grant-bootloader"); + const hint = document.getElementById("grant-bootloader-hint"); + const onClick = async () => { + let dev = null; + try { dev = await requestHalfKay(); } catch (_) { /* canceled */ } + if (!dev) { + hint.textContent = "No bootloader chosen. Is the Teensy plugged in, and was its program button pressed? Then click again."; + hint.hidden = false; + return; + } + btn.removeEventListener("click", onClick); + resolve(dev); + }; + btn.addEventListener("click", onClick); + }); + } + + // Resolves with the rebooted Teensy's serial port, picked in the browser's list, or null on Skip. + // requestPort needs a user gesture, so it runs inside the button's click; a cancel keeps the step open. + function uiFindBoard() { + return new Promise((resolve) => { + const btn = document.getElementById("find-board"); + const skip = document.getElementById("find-board-skip"); + const hint = document.getElementById("find-board-hint"); + const done = (port) => { + btn.removeEventListener("click", onClick); + skip.removeEventListener("click", onSkip); + resolve(port); + }; + const onClick = async () => { + let port = null; + try { port = await requestTeensySerial(); } catch (_) { /* canceled */ } + if (port) { done(port); return; } + hint.textContent = "No port chosen. Pick the Teensy's serial port in the list, or skip."; + hint.hidden = false; + }; + const onSkip = () => done(null); + btn.addEventListener("click", onClick); + skip.addEventListener("click", onSkip); + }); + } + // Symmetric with uiWaitForCreds. Normalization of the typed value lives // in the orchestrator so the host page stays UI-only. The retry action // is the orchestrator's signal to re-run Improv `initialize()`; while @@ -605,6 +675,7 @@ document.addEventListener('DOMContentLoaded', () => { // vocabulary build_esp32's TARGET_TO_FAMILY uses. Used as the chip fallback when no // device is picked (OTA / generic flash). "" for an unrecognized key. function firmwareToChip(firmware) { + if (/^teensy41/.test(String(firmware || ""))) return TEENSY_FAMILY; const m = String(firmware || "").match(/^esp32(s31|s3|s2|p4|c\d+|h\d+)/); return m ? (m[1] === "s31" ? "ESP32-S31" : "ESP32-" + m[1].toUpperCase()) : (firmware ? "ESP32" : ""); } @@ -667,9 +738,13 @@ document.addEventListener('DOMContentLoaded', () => { // differs. Kept beside pickedPort because the two are mutually exclusive states of the // one "what am I installing onto" question the Port row asks. let desktopMode = false; // this page is for flashing an ESP32; the desktop download is the alternative + // A Teensy held in its bootloader by its program button: no serial port, the bootloader itself is the target. + // Not a dropdown choice: the port picker offers it when no Teensy is listed, the one case that needs it. + let teensyBootMode = false; const portSelect = document.getElementById("port-select"); const PICK_NEW = "__pick_new__"; // sentinel value for the "pick another" option const DESKTOP = "__desktop__"; // sentinel value for the "this computer" option + const TEENSY_BOOT = "__teensy_boot__"; // sentinel value naming the bootloader mode while it is active const IDLE = "__idle__"; // neutral placeholder: nothing chosen yet // Name this computer the way the picker names its archive, or null on an OS we do not @@ -697,12 +772,20 @@ document.addEventListener('DOMContentLoaded', () => { // have to grant a serial port to discover it exists. const desktopLabel = thisComputerLabel(); const addDesktopOpt = () => { + // Shown only while active, so the row names the target the hint below the port picker chose. + if (teensyBootMode) { + const teensyOpt = document.createElement("option"); + teensyOpt.value = TEENSY_BOOT; + teensyOpt.textContent = "A Teensy in its bootloader"; + portSelect.appendChild(teensyOpt); + } if (!desktopLabel) return; const opt = document.createElement("option"); opt.value = DESKTOP; opt.textContent = `This computer (${desktopLabel})`; portSelect.appendChild(opt); }; + const modeValue = () => desktopMode ? DESKTOP : teensyBootMode ? TEENSY_BOOT : null; if (!pickedPort) { // A neutral placeholder sits first and stays selected until the user chooses. It is // what makes every pick a real `change` event, including re-picking the same option @@ -716,11 +799,11 @@ document.addEventListener('DOMContentLoaded', () => { if ("serial" in navigator) { const pickOpt = document.createElement("option"); pickOpt.value = PICK_NEW; - pickOpt.textContent = "An ESP32 on a USB portโ€ฆ"; + pickOpt.textContent = "A board on a USB portโ€ฆ"; portSelect.appendChild(pickOpt); } addDesktopOpt(); - portSelect.value = desktopMode ? DESKTOP : IDLE; + portSelect.value = modeValue() || IDLE; return; } // Picked state: name the selected port by the chip Detect read from it @@ -737,7 +820,7 @@ document.addEventListener('DOMContentLoaded', () => { pickOpt.textContent = "Pick another USB portโ€ฆ"; portSelect.appendChild(pickOpt); addDesktopOpt(); - portSelect.value = desktopMode ? DESKTOP : "current"; + portSelect.value = modeValue() || "current"; } // Reflect the port state in the picker's Install gate after every rebuild @@ -755,6 +838,7 @@ document.addEventListener('DOMContentLoaded', () => { if (!ports.length || pickedPort) return; pickedPort = ports[0]; desktopMode = false; + teensyBootMode = false; syncPortState(); } catch { // No grant, no port, or no permission: the picker stays as it was. @@ -775,8 +859,8 @@ document.addEventListener('DOMContentLoaded', () => { const eraseRow = document.getElementById("erase-row"); // The button's own control-row, so hiding it doesn't leave an empty row behind. const monitorRow = document.getElementById("monitor-btn")?.closest(".control-row"); - if (eraseRow) eraseRow.hidden = desktopMode; - if (monitorRow) monitorRow.hidden = desktopMode; + if (eraseRow) eraseRow.hidden = desktopMode || teensyBootMode; + if (monitorRow) monitorRow.hidden = desktopMode || teensyBootMode; } syncPortState(); @@ -802,6 +886,7 @@ document.addEventListener('DOMContentLoaded', () => { portPromptOpen = false; } syncPortState(); + if (!granted) offerTeensyBootloader(); // Auto-detect right after a fresh grant โ€” the ESP Web Tools / ESPHome // model where picking the device detects it immediately, so the device // list narrows without a second click. Only on a genuine new grant (not @@ -817,12 +902,31 @@ document.addEventListener('DOMContentLoaded', () => { } } + // A Teensy with no serial port (a crashed program, another USB type) is not in the port list: after a canceled pick, the status line offers its bootloader instead, which WebHID reaches once the program button is pressed. + function offerTeensyBootloader() { + if (!("hid" in navigator) || pickedPort || teensyBootMode) return; + const status = document.getElementById("detect-status"); + const link = document.createElement("a"); + link.href = "#"; + link.textContent = "Teensy not listed? Press its program button, then click here."; + link.addEventListener("click", (e) => { + e.preventDefault(); + teensyBootMode = true; + desktopMode = false; + syncPortState(); + // Narrow the device list to the Teensy, as a detected port would. + installPicker.runDetect((text) => { status.textContent = text; }); + }); + status.replaceChildren(link); + } + function applyPortChoice(value) { - if (value === IDLE) return; // the placeholder is not a choice - if (value === DESKTOP) { desktopMode = true; syncPortState(); return; } + if (value === IDLE || value === TEENSY_BOOT) return; // the placeholder, and the active bootloader mode, are not choices + if (value === DESKTOP) { desktopMode = true; teensyBootMode = false; syncPortState(); return; } // Any other choice is about a device again. openPortPicker ends in its own // syncPortState, so only sync here when nothing else will. desktopMode = false; + teensyBootMode = false; if (value === PICK_NEW) openPortPicker(); else syncPortState(); } @@ -986,8 +1090,9 @@ document.addEventListener('DOMContentLoaded', () => { // below re-evaluates the button whenever pickedPort changes.) // Desktop mode has nothing to open, so the port gate does not apply: the button is a // download and is enabled as soon as an archive for this OS exists. - hasPort: () => desktopMode || !!pickedPort, + hasPort: () => desktopMode || teensyBootMode || !!pickedPort, onDetect: async () => { + if (teensyBootMode) return TEENSY_FAMILY; // The monitor (if open) holds the port โ€” release it before esptool // claims it, same as the install path. detect() keeps the port open // afterwards so the following Install reuses it. @@ -1034,8 +1139,8 @@ document.addEventListener('DOMContentLoaded', () => { // (the build has no WIFI_SETTINGS RPC) and tell the user to connect Ethernet instead. const ethOnly = /-eth$/.test(firmware); await installer.start({ - // pickedPort may be null โ€” orchestrator falls back to requestPort. - port: pickedPort, + // pickedPort may be null, and the orchestrator falls back to requestPort. In Teensy bootloader mode there is no port. + port: teensyBootMode ? null : pickedPort, manifestUrl: localUrl, device, // names the install title + identifies the catalog entry applyDefaults, // gates the APPLY_OP config push (not txPower, sent earlier) @@ -1047,6 +1152,8 @@ document.addEventListener('DOMContentLoaded', () => { uiWaitForIp, uiShowNeedsIpRetrying: showNeedsIpRetrying, uiWaitForPortRetry, + uiGrantBootloader, + uiFindBoard, onSuccess: handleSuccess, onError: handleError, onLog: appendLog, diff --git a/mooninstaller/teensy-writer.js b/mooninstaller/teensy-writer.js new file mode 100644 index 000000000..a4351de4c --- /dev/null +++ b/mooninstaller/teensy-writer.js @@ -0,0 +1,166 @@ +// The installer's Teensy 4.1 writer: PJRC's HalfKay bootloader protocol over WebHID, as teensy_loader_cli speaks it. +// +// Dependency-free and import-safe in node (browser APIs are touched only inside the async functions), so test/js pins the report layout. +// +// The protocol, from teensy_loader_cli.c: +// - a running board enters its bootloader when its USB serial port is opened at 134 baud; +// - the bootloader is a HID device, 16C0:0478, taking 1088-byte output reports: a 64-byte header whose first 3 bytes are the block's offset from the flash base, little-endian, then 1024 image bytes; +// - block 0 goes first because it erases the chip, so the first writes wait up to 45 s and the rest 0.5 s; +// - a block of only 0xFF after the first is skipped, since the chip is erased; +// - a report whose first 3 bytes are 0xFF boots the new program. + +export const PJRC_VID = 0x16C0; +export const TEENSY_SERIAL_PID = 0x0483; // a Teensy running a program built with USB type Serial, as MoonLight and the factory blink are +export const HALFKAY_PID = 0x0478; +export const TEENSY_FAMILY = "Teensy 4.1"; + +const BLOCK_BYTES = 1024; +const HEADER_BYTES = 64; +export const REPORT_BYTES = HEADER_BYTES + BLOCK_BYTES; +// The program flash HalfKay writes on a Teensy 4.1: 8 MB less the 256 KB the core reserves at the top. +export const TEENSY41_CODE_BYTES = 8126464; +// MoonLight's settings: a 1 MB LittleFS directly below that reserved 256 KB (platform_teensy.cpp, ProgramFlashFs), in 4 KB blocks. +// Zeroing its first two blocks, the superblock pair, leaves nothing to mount, so the firmware formats it at boot: an erase of +// every setting that leaves the program alone, which the bootloader cannot do otherwise, since it erases the program area only. +export const TEENSY41_SETTINGS_OFFSET = TEENSY41_CODE_BYTES - 1024 * 1024; +const SETTINGS_SUPERBLOCK_BYTES = 2 * 4096; +const ERASE_WRITES = 5; // teensy_loader_cli's `block_count <= 4`: the writes the chip erase can stall +const ERASE_TIMEOUT_MS = 45000; +const BLOCK_TIMEOUT_MS = 500; +const BOOTLOADER_BAUD = 134; + +export function isTeensySerial(info) { + return !!info && info.usbVendorId === PJRC_VID && info.usbProductId === TEENSY_SERIAL_PID; +} + +// Why an image cannot go to a Teensy 4.1, or "" when it can: the flat image starts with the FlexSPI configuration block, "FCFB". +export function imageProblem(image) { + if (!(image instanceof Uint8Array) || image.length === 0) return "the image is empty"; + if (image[0] === 0x3A) return "that is a HEX file, the installer writes the .bin"; + if (image.length < 4 || String.fromCharCode(...image.subarray(0, 4)) !== "FCFB") return "not a Teensy 4 image"; + if (image.length > TEENSY41_CODE_BYTES) return `the image is ${image.length} bytes, more than the ${TEENSY41_CODE_BYTES} a Teensy 4.1 holds`; + return ""; +} + +// The reports that write an image, in order: block 0 always, every later block unless it is all 0xFF. +export function halfkayReports(image) { + const reports = []; + for (let offset = 0; offset < image.length; offset += BLOCK_BYTES) { + const block = image.subarray(offset, offset + BLOCK_BYTES); + if (offset > 0 && block.every(b => b === 0xFF)) continue; + const report = new Uint8Array(REPORT_BYTES).fill(0); + report[0] = offset & 0xFF; + report[1] = (offset >> 8) & 0xFF; + report[2] = (offset >> 16) & 0xFF; + report.fill(0xFF, HEADER_BYTES); // a short last block reads as erased flash beyond the image + report.set(block, HEADER_BYTES); + reports.push({ offset, report }); + } + return reports; +} + +// The reports that zero the settings' superblock pair. +export function eraseSettingsReports() { + const reports = []; + for (let offset = TEENSY41_SETTINGS_OFFSET; offset < TEENSY41_SETTINGS_OFFSET + SETTINGS_SUPERBLOCK_BYTES; offset += BLOCK_BYTES) { + const report = new Uint8Array(REPORT_BYTES).fill(0); + report[0] = offset & 0xFF; + report[1] = (offset >> 8) & 0xFF; + report[2] = (offset >> 16) & 0xFF; + reports.push({ offset, report }); + } + return reports; +} + +export function bootReport() { + const report = new Uint8Array(REPORT_BYTES).fill(0); + report[0] = report[1] = report[2] = 0xFF; + return report; +} + +const sleep = (ms) => new Promise(r => setTimeout(r, ms)); + +// Asks a running Teensy into its bootloader: PJRC's core reboots when its serial port is set to 134 baud. +export async function rebootToBootloader(port) { + try { + await port.open({ baudRate: BOOTLOADER_BAUD }); + } catch (e) { + // Held open by the installer at another rate: close it and open it again at the bootloader's. + if (e && e.name === "InvalidStateError") { + try { + await port.close(); + await port.open({ baudRate: BOOTLOADER_BAUD }); + } catch (_) { /* held by a reader, or already rebooting: the bootloader step that follows says what to do */ } + } + } + await sleep(100); + try { await port.close(); } catch (_) { /* the device left the bus */ } +} + +const HALFKAY_FILTER = { vendorId: PJRC_VID, productId: HALFKAY_PID }; + +// The browser's device chooser for the bootloader; it needs a user gesture, so call it inside a click. Null when the user cancels. +// Chrome does not remember the grant (seen on the bench, although the bootloader reports a USB serial number): every install asks. +export async function requestHalfKay() { + const devices = await navigator.hid.requestDevice({ filters: [HALFKAY_FILTER] }); + return devices && devices.length ? devices[0] : null; +} + +// One report, retried while the bootloader is busy (erasing), as teensy_loader_cli's teensy_write retries until its timeout. +async function sendReport(device, report, timeoutMs) { + const deadline = Date.now() + timeoutMs; + for (;;) { + const remaining = deadline - Date.now(); + try { + await Promise.race([ + device.sendReport(0, report), + sleep(Math.max(remaining, 1)).then(() => { throw new Error("timeout"); }), + ]); + return; + } catch (e) { + if (Date.now() >= deadline) throw new Error(`the Teensy bootloader stopped answering (${e && e.message || e})`); + await sleep(10); + } + } +} + +// Writes an image through an opened or unopened HalfKay device, with `eraseSettings` also the settings, and boots it; onProgress gets 0..100. +export async function writeImage(device, image, { onProgress, onLog, eraseSettings = false } = {}) { + const problem = imageProblem(image); + if (problem) throw new Error(problem); + if (!device.opened) await device.open(); + const reports = halfkayReports(image); + if (eraseSettings) { + if (onLog) onLog("[teensy] erasing the settings as well"); + reports.push(...eraseSettingsReports()); + } + if (onLog) onLog(`[teensy] writing ${reports.length} blocks of ${BLOCK_BYTES} bytes`); + for (let i = 0; i < reports.length; i++) { + await sendReport(device, reports[i].report, i < ERASE_WRITES ? ERASE_TIMEOUT_MS : BLOCK_TIMEOUT_MS); + if (onProgress) onProgress(Math.round(((i + 1) * 100) / reports.length)); + } + if (onLog) onLog("[teensy] booting the new program"); + try { + await device.sendReport(0, bootReport()); + } catch (_) { + // The bootloader may leave the bus before it acknowledges the boot report. + } + try { await device.close(); } catch (_) { /* already gone */ } +} + +// The browser's port chooser narrowed to a running Teensy; it needs a user gesture, so call it inside a click. +export async function requestTeensySerial() { + return await navigator.serial.requestPort({ filters: [{ usbVendorId: PJRC_VID, usbProductId: TEENSY_SERIAL_PID }] }); +} + +// The board's serial port after the reboot, which re-enumerates; null when none of this browser's granted ports appears in `waitMs`. +export async function rebootedSerialPort(waitMs) { + const deadline = Date.now() + waitMs; + for (;;) { + const ports = await navigator.serial.getPorts(); + const port = ports.find(p => isTeensySerial(p.getInfo())); + if (port) return port; + if (Date.now() >= deadline) return null; + await sleep(500); + } +} diff --git a/src/core/system/HttpServerModule.cpp b/src/core/system/HttpServerModule.cpp index 4bddb6a1f..cbcb7834a 100644 --- a/src/core/system/HttpServerModule.cpp +++ b/src/core/system/HttpServerModule.cpp @@ -2222,6 +2222,12 @@ void HttpServerModule::handleFirmwareUrl(platform::TcpConnection& conn, const ch "{\"error\":\"url must start with http:// or https://\"}"); return; } + // A device without TLS has no firmware fetcher at all, so it says so before starting: the UI then fetches the image itself and uploads it. + if (!platform::httpsAvailable()) { + sendResponse(conn, 501, "application/json", + "{\"error\":\"this device cannot fetch a URL: upload the image\"}"); + return; + } // A MoonBase device runs from its one app slot, so stage the URL in NVS and reboot into MoonBase to install; 202 with {"moonbase":true} marks this flow. if (platform::otaHasMoonBase() && !platform::otaRunningMoonBase()) { diff --git a/src/core/system/ImprovProvisioningModule.h b/src/core/system/ImprovProvisioningModule.h index 71fb44577..8b5396f7b 100644 --- a/src/core/system/ImprovProvisioningModule.h +++ b/src/core/system/ImprovProvisioningModule.h @@ -26,7 +26,7 @@ namespace mm { /// /// ## Transports /// -/// The listener serves an external USB-to-UART bridge and a native USB port alike. +/// The listener serves an external USB-to-UART bridge and a native USB port alike; on a Teensy it is the USB serial port, polled from this module's tick. /// Without either, the access-point flow remains, the device serving its own network. /// /// ## The commands @@ -105,6 +105,7 @@ class ImprovProvisioningModule : public MoonModule { /// Apply any pending operation, polled every tick so a burst of them installs briskly. void tick() MM_NONBLOCKING override { + if constexpr (platform::hasImprov) platform::improvPoll(); if (pendingOpReady_.load(std::memory_order_acquire) && scheduler_) { // The frame was acknowledged on receipt, so a failure must surface here instead. const StateDocumentResult r = applyStateDocument(*scheduler_, pendingOp_); diff --git a/src/core/system/MoonCloudModule.h b/src/core/system/MoonCloudModule.h index 3801d5540..e1ca24045 100644 --- a/src/core/system/MoonCloudModule.h +++ b/src/core/system/MoonCloudModule.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include "core/module/MoonModule.h" @@ -15,29 +16,79 @@ namespace mm { /// The container for everything MoonLight does with a server we run. /// -/// It holds no controls and does no work of its own. -/// Each thing MoonCloud does is a child with its own consent: Stats, Talk, and Sync to come. -/// A user who wants a joint lightshow has not agreed to usage reporting. +/// It does no work of its own. Each thing MoonCloud does is a child with its own consent: Stats, Talk, and Sync to come. A user who wants a joint lightshow has not agreed to usage reporting. +/// +/// @moreinfo +/// +/// ## The browser relay, on a board without HTTPS +/// +/// A board with no TLS cannot post, so its one outbound call goes into a one-slot outbox instead, a hidden control the open page reads. +/// The page posts it to the server, which accepts a cross-origin request, and presses the hidden `outbox sent` button to empty it. A report then goes out whenever someone has the page open, enough for a usage count. +/// Two pages open at once can both post the waiting request before either empties it, a rare double the count tolerates. Neither control exists on a board with HTTPS, so the relay costs it nothing but the check. class MoonCloudModule : public MoonModule { public: + /// A board without HTTPS gets its outbox at construction, so the relay's controls are declared with the rest. + explicit MoonCloudModule(bool relay = !platform::httpsAvailable()) + : outbox_(relay ? static_cast(std::calloc(kOutboxBytes, 1)) : nullptr) {} + /// Free the outbox, where there is one. + ~MoonCloudModule() override { std::free(outbox_); } + /// Always runs: the children carry the real consent, so disabling the container is ambiguous. bool respectsEnabled() const MM_NONBLOCKING override { return false; } /// How long an outbound call may take before it is abandoned. static constexpr uint32_t kTimeoutMs = 4000; - /// The one outbound call, over HTTPS alone since the address is a compiled-in constant. - bool post(const char* path, const char* body) const { + /// The relay's two hidden controls where there is an outbox, then the children's. + void defineControls() override { + controls_.clear(); + if (outbox_) { + // Read-only, which also keeps it out of the saved settings: a request is transient. + controls_.addText("outbox", outbox_, kOutboxBytes); + controls_.setReadOnly(controls_.count() - 1, true); + controls_.setHidden(controls_.count() - 1, true); + controls_.addButton("outbox sent"); + controls_.setHidden(controls_.count() - 1, true); + } + MoonModule::defineControls(); + } + + /// Empty the outbox once the page has delivered it. + void onControlChanged(const char* name) override { + if (outbox_ && name && std::strcmp(name, "outbox sent") == 0) { + outbox_[0] = 0; + notifyValuesChanged(); + } + } + + /// Whether a post can leave this board at all: over HTTPS, or through the page. + bool canSend() const { return platform::httpsAvailable() || outbox_ != nullptr; } + /// Whether a post waits for the page rather than leaving at once. + bool relaysThroughBrowser() const { return outbox_ != nullptr; } + + /// Whether a request still waits for the page to deliver it. + bool outboxWaiting() const { return outbox_ && outbox_[0]; } + + /// The one outbound call, over HTTPS or into the outbox for the page (@xref{the-browser-relay-on-a-board-without-https}); false when it could not be handed off. + bool post(const char* path, const char* body) { + if (outbox_) { + if (outbox_[0]) return false; + const int n = std::snprintf(outbox_, kOutboxBytes, "{\"path\":\"%s\",\"body\":%s}", path, body); + if (n < 0 || static_cast(n) >= kOutboxBytes) { outbox_[0] = 0; return false; } + notifyValuesChanged(); // to the page on the next transport tick + return true; + } char url[192]; std::snprintf(url, sizeof(url), "https://%s%s", kHost, path); return platform::httpsPost(url, body, kTimeoutMs); } - - private: // Compiled in rather than configurable; this name because a custom domain's CA is absent. static constexpr const char* kHost = "mooncloud-stats.moonmodules.workers.dev"; + // One request: a usage report is well under this, and a talk message is capped at 280 characters. + static constexpr uint16_t kOutboxBytes = 2048; + char* outbox_ = nullptr; ///< the request waiting for the page, on a board without HTTPS only }; diff --git a/src/core/system/MoonStatsModule.h b/src/core/system/MoonStatsModule.h index 735c89a5d..d7f75d5e0 100644 --- a/src/core/system/MoonStatsModule.h +++ b/src/core/system/MoonStatsModule.h @@ -207,7 +207,13 @@ class MoonStatsModule : public MoonModule { /// Say what this setting exchanges, on the status slot rather than in a control of its own. void refreshStatus() { - if (consent_) clearStatus(); + // Where the report cannot leave at once, consent says how it does, or that it cannot, rather than letting the charts stay silent. + if (consent_ && cloudRelays()) + setStatus("This board has no HTTPS client, so its open page sends the report for it.", Severity::Status); + else if (consent_ && !canSend()) + setStatus("This board has no HTTPS client, so it cannot send its report: nothing from it reaches the charts.", + Severity::Warning); + else if (consent_) clearStatus(); else setStatus("Off. Switch on to share what hardware you run, once per install or upgrade " "and whenever you press send update: " "the empty charts below are exactly what it contributes to, and what you get " @@ -224,7 +230,7 @@ class MoonStatsModule : public MoonModule { // Every outcome says something, and consent is re-read since a write can arrive from the API. if (!consent_) { setOwnStatus("Switch consent on first: nothing is sent while it is off.", Severity::Warning); return; } - if (!platform::httpsAvailable()) { + if (!canSend()) { setOwnStatus("This build cannot send: it was compiled without an HTTPS client.", Severity::Error); return; @@ -240,7 +246,8 @@ class MoonStatsModule : public MoonModule { return; } if (sendReport(MoonStatsEvent::Refresh)) { - setOwnStatus("Sent. The charts below now describe this device as it is now.", + setOwnStatus(cloudRelays() ? "Handed to this page, which sends it within a second." + : "Sent. The charts below now describe this device as it is now.", Severity::Status); } else { setOwnStatus("Could not reach the server. Nothing was sent; press send update to try again.", @@ -310,9 +317,18 @@ class MoonStatsModule : public MoonModule { /// Send the automatic report once every guard below passes. void tick1s() MM_NONBLOCKING override { MoonModule::tick1s(); + // A report handed to the page is marked once the outbox empties, and nothing else goes until then. + if (relayPending_) { + const auto* cloud = static_cast(parent()); + if (!cloud || !cloud->outboxWaiting()) { + markReported(); + relayPending_ = false; + } + return; + } if (!reportDue()) return; // Never sends, so there is nothing to hand off and nothing to mark. - if (!platform::httpsAvailable()) return; + if (!canSend()) return; if (!platform::networkReady()) return; // try again next second // Our own AP means no route out, so a send would fail and mark itself reported. if (inApMode()) return; @@ -322,6 +338,14 @@ class MoonStatsModule : public MoonModule { sendReport(dueEvent()); } + /// Whether a report can leave: over HTTPS, or through the page where the container relays. + bool canSend() const { return platform::httpsAvailable() || cloudRelays(); } + /// Whether the container hands the report to the open page rather than sending it. + bool cloudRelays() const { + const auto* cloud = static_cast(parent()); + return cloud && cloud->relaysThroughBrowser(); + } + /// Whether we serve our own access point, asked of the platform so no copy goes stale. bool inApMode() const { return platform::wifiApConnected(); } @@ -353,9 +377,14 @@ class MoonStatsModule : public MoonModule { // Through the container, which owns the address. Whether it was accepted is reported. bool sent = false; - if (auto* cloud = static_cast(parent())) { + if (auto* cloud = static_cast(parent())) { sent = cloud->post("/api/report", body.data()); } + // Through the page, the report counts once the page has delivered it, so a reboot before then sends it again. + if (cloudRelays()) { + relayPending_ = sent; + return sent; + } // The automatic report marks itself either way; a failed press must not. if (kind != MoonStatsEvent::Refresh || sent) markReported(); return sent; @@ -383,6 +412,7 @@ class MoonStatsModule : public MoonModule { private: bool consent_ = false; ///< whether the user has agreed to report + bool relayPending_ = false; ///< a report waits in the container's outbox for the page char reportedVersion_[32] = {}; ///< the version that last reported char runningVersion_[32] = {}; ///< the version running now }; diff --git a/src/core/system/MoonTalkModule.h b/src/core/system/MoonTalkModule.h index f7ef2e965..d32cfa105 100644 --- a/src/core/system/MoonTalkModule.h +++ b/src/core/system/MoonTalkModule.h @@ -120,7 +120,7 @@ class MoonTalkModule : public MoonModule { body.flush(); // Through the container, which owns the address. - auto* cloud = static_cast(parent()); + auto* cloud = static_cast(parent()); return cloud && cloud->post("/api/talk", body.data()); } diff --git a/src/core/util/ImprovOpReassembler.h b/src/core/util/ImprovOpReassembler.h index f8e55e6b7..a9cbcfcbd 100644 --- a/src/core/util/ImprovOpReassembler.h +++ b/src/core/util/ImprovOpReassembler.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -13,15 +14,13 @@ namespace mm { /// ## Where it sits /// /// This is the pure logic behind the device's 0xFC handler, which treats Improv as REST over serial. -/// The platform layer in `platform_esp32_improv.cpp` owns the serial I/O, reading frames, sending acks and errors, and holding the single-buffer `opReady` atomic; it hands each chunk's `[seq][last][bytes]` here. +/// Each platform's listener owns its transport and hands each APPLY_OP payload to @ref improvApplyOp, which feeds the chunks here and holds the `opReady` handshake. /// That is the same core-against-platform line `ImprovFrame.h` draws, so the reassembly and its sequence guard are proven on the desktop without hardware. /// /// ## Why the sequence guard is real /// -/// A chunk carries `[seq][last][chunk bytes]`. -/// Sequence 0 starts a fresh op and resets the buffer, and every later chunk must be the next sequence in order. -/// A duplicate, which an installer retry on a misread timeout produces, or an out-of-order chunk would splice garbage into the buffer. -/// Both are rejected and the buffer is reset. +/// A chunk carries `[seq][last][chunk bytes]`. Sequence 0 starts a fresh op and resets the buffer, and every later chunk must be the next sequence in order. +/// A duplicate, which an installer retry on a misread timeout produces, or an out-of-order chunk would splice garbage into the buffer. Both are rejected and the buffer is reset. /// USB serial delivers in order, but the installer's send is open-loop and can re-emit a chunk, so the guard guards something that happens. /// /// Joins the chunks of one op into a NUL-terminated buffer the caller owns. @@ -39,7 +38,7 @@ class ImprovOpReassembler { /// Feed one chunk, `seq` being its 0-based index and `last` true on the final one. Result feed(uint8_t seq, bool last, const uint8_t* chunk, size_t chunkLen) { - // seq 0 always starts fresh, so a new op recovers after one that errored mid-stream. + // seq 0 always starts fresh, so a new op recovers after one that errored mid-stream: @xref{why-the-sequence-guard-is-real}. if (seq == 0) { len_ = 0; nextSeq_ = 1; @@ -86,5 +85,26 @@ class ImprovOpReassembler { uint8_t nextSeq_ = 0; // next chunk index expected (0 = awaiting a fresh op) }; +/// The vendor command carrying one state document over serial, and the error a refused chunk replies with. +inline constexpr uint8_t kImprovCmdApplyOp = 0xFC; +inline constexpr uint8_t kImprovErrorInvalidOp = 0x82; + +/// What a transport replies to one APPLY_OP frame: an empty response for a taken chunk, the invalid-op error otherwise. +enum class ImprovApplyOpReply : uint8_t { Ack, Refused }; + +/// Handle one APPLY_OP payload, `[0xFC][seq][last][chunkโ€ฆ]`, for every transport (@xref{where-it-sits}): refused while a document waits or when malformed, and a last chunk sets `opReady`. +inline ImprovApplyOpReply improvApplyOp(ImprovOpReassembler& reasm, const uint8_t* payload, size_t len, std::atomic& opReady) { + // Single-buffered: the acquire pairs with the consumer's release when it clears the flag after applying. + if (opReady.load(std::memory_order_acquire)) return ImprovApplyOpReply::Refused; + // `last` is a flag on the wire, so anything but 0 or 1 is a malformed frame rather than "more chunks". + if (len < 3 || payload[2] > 1) return ImprovApplyOpReply::Refused; + switch (reasm.feed(payload[1], payload[2] != 0, payload + 3, len - 3)) { + case ImprovOpReassembler::Result::Error: return ImprovApplyOpReply::Refused; + case ImprovOpReassembler::Result::Ready: opReady.store(true, std::memory_order_release); break; + case ImprovOpReassembler::Result::Continue: break; + } + return ImprovApplyOpReply::Ack; +} + } // namespace mm diff --git a/src/platform/desktop/platform_desktop.cpp b/src/platform/desktop/platform_desktop.cpp index 46ab998a7..9a915857b 100644 --- a/src/platform/desktop/platform_desktop.cpp +++ b/src/platform/desktop/platform_desktop.cpp @@ -1907,6 +1907,7 @@ bool improvProvisioningInit(const ImprovDeviceInfo& /*info*/, } return false; } +void improvPoll() MM_NONBLOCKING {} void reboot() { // The device is the host process, so exit cleanly and let the supervisor restart it, which matches what the browser's reconnect expects. diff --git a/src/platform/esp32/platform_esp32_improv.cpp b/src/platform/esp32/platform_esp32_improv.cpp index b61b785af..9df4efd1d 100644 --- a/src/platform/esp32/platform_esp32_improv.cpp +++ b/src/platform/esp32/platform_esp32_improv.cpp @@ -260,51 +260,21 @@ static void improvHandleSetTxPower(const uint8_t* payload, uint8_t len) { // The apply operation, carrying one request as a document in the same shape the network interface takes: @xref{the-two-vendor-operations|why over serial}. // The payload is a chunk index, a last flag and a slice. // The first index resets the buffer and the last one marks it ready for the main loop to apply. -static constexpr uint8_t IMPROV_CMD_APPLY_OP = 0xFC; -static constexpr uint8_t IMPROV_ERROR_INVALID_OP = 0x82; - static void improvHandleApplyOp(const uint8_t* payload, uint8_t len) { if (!g_improv.opOut || !g_improv.opReady) { improvSendError(improv::ERROR_UNKNOWN_RPC); return; } - // Single-buffered: refuse a new op while the module hasn't consumed the previous one (opReady still set), so a fast installer can't overwrite an unapplied op. The installer treats this error as "retry shortly" and re-sends. Acquire-load pairs with the module's release-store when it clears the flag after applying. - if (g_improv.opReady->load(std::memory_order_acquire)) { - improvSendError(static_cast(IMPROV_ERROR_INVALID_OP)); - return; - } - // [0xFC][seq][last] header = 3 bytes; chunk is the rest. - if (len < 3) { - improvSendError(static_cast(IMPROV_ERROR_INVALID_OP)); - return; - } - uint8_t seq = payload[1]; - uint8_t last = payload[2]; - const uint8_t* chunk = payload + 3; - size_t chunkLen = static_cast(len) - 3; - - // `last` is a boolean flag on the wire; anything but 0/1 is a malformed frame (a desync the parser's checksum didn't catch, or a non-conforming sender). Reject before reassembly rather than coerce a stray value to "more chunks". - if (last > 1) { - improvSendError(static_cast(IMPROV_ERROR_INVALID_OP)); - return; - } - - // Reassembly and the duplicate guard live in a core helper tested without hardware, leaving only the serial handling here. Bound once at the first call, the buffer being set before the task starts and one task living for the device's lifetime, so it never sees a stale one. + // The handling lives in core, shared with every transport and tested without hardware; this owns only the replies. Bound once at the first call, the buffer being set before the task starts and one task living for the device's lifetime. static mm::ImprovOpReassembler reasm(g_improv.opOut, g_improv.opOutLen); - switch (reasm.feed(seq, last, chunk, chunkLen)) { - case mm::ImprovOpReassembler::Result::Error: - improvSendError(static_cast(IMPROV_ERROR_INVALID_OP)); - return; - case mm::ImprovOpReassembler::Result::Continue: - break; - case mm::ImprovOpReassembler::Result::Ready: - // release-store pairs with the module's acquire-load before it applies. - g_improv.opReady->store(true, std::memory_order_release); - break; + if (mm::improvApplyOp(reasm, payload, len, *g_improv.opReady) == mm::ImprovApplyOpReply::Refused) { + // The installer treats this as "retry shortly", which a busy buffer needs. + improvSendError(static_cast(mm::kImprovErrorInvalidOp)); + return; } // Ack every chunk (empty RpcResponse for 0xFC) so the installer awaits each. auto rpc = improv::build_rpc_response( - static_cast(IMPROV_CMD_APPLY_OP), + static_cast(mm::kImprovCmdApplyOp), std::vector{}, false); improvSend(ImprovFrameType::RpcResponse, rpc); } @@ -319,7 +289,7 @@ static void improvDispatchFrame(const ImprovFrameParser& parser) { improvHandleSetTxPower(raw, rawLen); return; } - if (rawLen >= 1 && raw[0] == IMPROV_CMD_APPLY_OP) { + if (rawLen >= 1 && raw[0] == mm::kImprovCmdApplyOp) { improvHandleApplyOp(raw, rawLen); return; } @@ -511,4 +481,7 @@ bool improvProvisioningInit(const ImprovDeviceInfo& info, return true; } +// The task drains the transports, so the tick has nothing to add. +void improvPoll() MM_NONBLOCKING {} + } // namespace mm::platform diff --git a/src/platform/platform.h b/src/platform/platform.h index 3dcd07809..a7fee58cb 100644 --- a/src/platform/platform.h +++ b/src/platform/platform.h @@ -709,6 +709,8 @@ bool improvProvisioningInit(const ImprovDeviceInfo& info, std::atomic* txPowerReady = nullptr, char* opOut = nullptr, size_t opOutLen = 0, std::atomic* opReady = nullptr); +/// Drain the serial listener, called every tick: the work itself where the listener has no task of its own, a no-op where a task runs it. +void improvPoll() MM_NONBLOCKING; /// One UDP socket: the wire every light protocol sends and receives on. class UdpSocket { diff --git a/src/platform/teensy/platform_config.h b/src/platform/teensy/platform_config.h index a5acf309a..c8c734884 100644 --- a/src/platform/teensy/platform_config.h +++ b/src/platform/teensy/platform_config.h @@ -57,7 +57,7 @@ constexpr bool hasAudioCapture = false; /// No WiFi on a Teensy. constexpr bool hasWiFi = false; -/// The on-board PHY, or a W5500 on the SPI port in the `teensy41-w5500` firmware. +/// The on-board PHY, or on a board without one a W5500 on the SPI port, found at boot. constexpr bool hasEthernet = true; /// False: no Ethernet controls without an interface. constexpr bool previewsEthernetControls = false; @@ -79,7 +79,7 @@ constexpr bool hasFsSegments = false; /// Whether any IP stack is present, which is what UDP interop gates on. constexpr bool hasNetwork = hasWiFi || hasEthernet; -/// True: each firmware drives one interface, chosen at build time, so the wiring is not a setting. +/// True: the firmware finds the interface at boot, the board's own PHY or else a W5500, so the wiring is not a setting. constexpr bool ethPhyIsFixed = true; /// A 600 MHz core with a double-precision FPU has room for a per-pixel float algorithm. @@ -87,27 +87,19 @@ constexpr bool hasHeavyCompute = true; // A whole effect can be compiled out only by `#if`, which a constexpr cannot drive; keep the two in step. #define MM_HEAVY_COMPUTE 1 -// QNEthernet's own driver switch, which the build sets for the `teensy41-w5500` firmware and leaves unset for the on-board PHY. -#ifdef QNETHERNET_DRIVER_W5500 -/// The W5500 driver is compiled in. +/// True: the W5500 driver is compiled in beside the on-board PHY's, and used on a board that has no PHY of its own. constexpr bool hasEthW5500 = true; -/// The SPI port's pins (MISO 12, MOSI 11, SCK 13) and the driver's chip select 10; reset and interrupt unwired. +/// The W5500's wiring, used only on a board without its own PHY: MISO 12, MOSI 11, SCK 13 and chip select 10, reset and interrupt unwired. constexpr EthPinConfig ethConfigDefault{ ethW5500, 0, -1, -1, -1, -1, false, 12, 11, 13, 10, -1 }; -#else -/// False: the on-board PHY, through the chip's own Ethernet MAC. -constexpr bool hasEthW5500 = false; -/// The on-board PHY, whose RMII lines reach no header pin. -constexpr EthPinConfig ethConfigDefault{ ethDp83825, 0, -1, -1, -1, -1, false, -1, -1, -1, -1, -1 }; -#endif /// False: no WiFi co-processor. constexpr bool hasWifiCoprocessor = false; -/// False: updates go over USB until FlasherX. -constexpr bool hasOta = false; +/// True: an uploaded image is staged above the program and copied over it at the reboot. +constexpr bool hasOta = true; -/// False: no serial provisioning; the Teensy has no WiFi to provision. -constexpr bool hasImprov = false; +/// True: the USB serial port takes the installer's state documents (APPLY_OP); there is no WiFi to provision. +constexpr bool hasImprov = true; /// The bits a call to emitted code adds to its address. Thumb state: a Cortex-M7 runs only Thumb code, and a call address carries it in bit 0. constexpr uintptr_t codeAddressBits = 1; diff --git a/src/platform/teensy/platform_teensy.cpp b/src/platform/teensy/platform_teensy.cpp index fa754f1f7..a05304664 100644 --- a/src/platform/teensy/platform_teensy.cpp +++ b/src/platform/teensy/platform_teensy.cpp @@ -8,6 +8,7 @@ #include #include #include +#include "qnethernet/qnethernet_dual_driver.h" #include #include @@ -327,7 +328,10 @@ const BootRecord& bootRecord() { void setLogLevel(LogLevel) {} // stdout goes straight to USB serial +void applyStagedFirmware(); // platform_teensy_ota.cpp: a checked upload replaces the program here + [[noreturn]] void reboot() { + applyStagedFirmware(); // returns only when no update waits SCB_AIRCR = 0x05FA0004; // SYSRESETREQ with the write key for (;;) {} } @@ -529,7 +533,7 @@ void copyIp(const IPAddress& from, uint8_t out[4]) { // Reconfiguring an SPI pin takes it from the bus, and the network with it until a reboot. bool ethOwnsPin(uint8_t gpio) { - if (!g_ethStarted) return false; + if (!g_ethStarted || !mmEthernetIsW5500()) return false; // the on-board PHY's lines reach no header pin const EthPinConfig& c = g_ethConfig; return gpio == c.spiMiso || gpio == c.spiMosi || gpio == c.spiSck || gpio == c.spiCs; } @@ -540,7 +544,7 @@ void setEthConfig(const EthPinConfig& cfg) { g_ethConfig = cfg; } // Starts DHCP and returns at once; false when the build's interface does not answer. bool ethInit() { g_ethStarted = Ethernet.begin(); - std::printf("Ethernet: %s %s\n", hasEthW5500 ? "W5500" : "PHY", + std::printf("Ethernet: %s %s\n", mmEthernetIsW5500() ? "W5500" : "PHY", g_ethStarted ? "found, DHCP started" : "not found, check the wiring"); return g_ethStarted; } @@ -888,22 +892,6 @@ void TcpServer::close() { fd_ = -1; } -// --- Updates: over USB until FlasherX ----------------------------------------------------------- - -bool http_fetch_to_ota(const char*, char* statusBuf, size_t statusBufLen, uint32_t* bytesReadOut, uint32_t* bytesTotalOut, const char*) { - if (statusBuf && statusBufLen) std::snprintf(statusBuf, statusBufLen, "updates go over USB"); - if (bytesReadOut) *bytesReadOut = 0; - if (bytesTotalOut) *bytesTotalOut = 0; - return false; -} -bool otaHasMoonBase() { return false; } -bool otaBootMoonBase() { return false; } -bool otaRunningMoonBase() { return false; } -bool otaMoonBaseVersion(char*, size_t) { return false; } -bool otaMoonBaseBuild(char*, size_t) { return false; } -bool otaMoonBaseSize(uint32_t*, uint32_t*) { return false; } -void moonbaseClearStagedUrl() {} - // --- Peripherals that arrive in phase 4 --------------------------------------------------------- bool i2cBusOpen(uint16_t, uint16_t) { return false; } diff --git a/src/platform/teensy/platform_teensy_improv.cpp b/src/platform/teensy/platform_teensy_improv.cpp new file mode 100644 index 000000000..803879b92 --- /dev/null +++ b/src/platform/teensy/platform_teensy_improv.cpp @@ -0,0 +1,88 @@ +/// @defgroup platform_teensy_improv The serial listener on the Teensy 4.1 +/// @{ +/// The installer's state documents over the USB serial port, so a device model's defaults apply right after an install. +/// +/// @moreinfo +/// +/// ## Only the state document +/// +/// The Teensy has no WiFi to provision, so it takes the one serial command that carries a state document (APPLY_OP) and refuses the rest. The same core helper the ESP32 uses handles it. +/// It shares the port with the log, as the ESP32's listener does: a frame's start marker and checksum pick it out of everything else. +/// +/// ## Polled, since there is no task +/// +/// The core has no RTOS, so the module's tick drains the port through @ref improvPoll, a bounded batch per tick so a flood cannot hold the render loop. + +#include "platform/platform.h" +#include "core/util/ImprovFrame.h" +#include "core/util/ImprovOpReassembler.h" + +#include + +#include +#include + +namespace mm::platform { + +namespace { +char* g_opOut = nullptr; +size_t g_opOutLen = 0; +std::atomic* g_opReady = nullptr; +ImprovFrameParser g_parser; +// The protocol's own error codes: a frame failing its checksum, and a command this listener does not take. +constexpr uint8_t kImprovErrorInvalidRpc = 0x01; +constexpr uint8_t kImprovErrorUnknownRpc = 0x02; + +// A reply goes out only when it fits the USB buffer at once, so a host that stopped reading cannot stall the tick; the installer does not wait for one. +void send(ImprovFrameType type, const uint8_t* payload, size_t len) MM_NONBLOCKING { + uint8_t frame[6 + 1 + 1 + 1 + kImprovMaxPayload + 1]; + const size_t n = buildImprovFrame(type, payload, len, frame, sizeof(frame)); + if (n && static_cast(Serial.availableForWrite()) >= n) Serial.write(frame, n); +} + +void dispatch() MM_NONBLOCKING { + if (g_parser.lastType() != static_cast(ImprovFrameType::Rpc)) return; + const uint8_t* payload = g_parser.lastPayload(); + const uint8_t len = g_parser.lastPayloadLen(); + if (len >= 1 && payload[0] == kImprovCmdApplyOp) { + static ImprovOpReassembler reasm(g_opOut, g_opOutLen); // bound once, the buffer living as long as the module + if (improvApplyOp(reasm, payload, len, *g_opReady) == ImprovApplyOpReply::Ack) { + const uint8_t ack[] = {kImprovCmdApplyOp, 0}; // an empty response for the command + send(ImprovFrameType::RpcResponse, ack, sizeof(ack)); + } else { + send(ImprovFrameType::ErrorState, &kImprovErrorInvalidOp, 1); + } + return; + } + send(ImprovFrameType::ErrorState, &kImprovErrorUnknownRpc, 1); +} +} // namespace + +bool improvProvisioningInit(const ImprovDeviceInfo&, char*, size_t, char*, size_t, std::atomic*, + char* statusBuf, size_t statusBufLen, uint8_t*, std::atomic*, + char* opOut, size_t opOutLen, std::atomic* opReady) { + if (!opOut || opOutLen == 0 || !opReady) { + if (statusBuf && statusBufLen) std::snprintf(statusBuf, statusBufLen, "error: no buffer for state documents"); + return false; + } + g_opOut = opOut; + g_opOutLen = opOutLen; + g_opReady = opReady; + if (statusBuf && statusBufLen) std::snprintf(statusBuf, statusBufLen, "listening on USB serial"); + return true; +} + +void improvPoll() MM_NONBLOCKING { + if (!g_opReady) return; + for (int i = 0; i < 256 && Serial.available() > 0; i++) { + switch (g_parser.feed(static_cast(Serial.read()))) { + case ImprovFeedResult::FrameReady: dispatch(); break; + case ImprovFeedResult::BadChecksum: send(ImprovFrameType::ErrorState, &kImprovErrorInvalidRpc, 1); break; + default: break; + } + } +} + +} // namespace mm::platform + +/// @} diff --git a/src/platform/teensy/platform_teensy_ota.cpp b/src/platform/teensy/platform_teensy_ota.cpp new file mode 100644 index 000000000..4a9ee1e98 --- /dev/null +++ b/src/platform/teensy/platform_teensy_ota.cpp @@ -0,0 +1,232 @@ +/// @defgroup platform_teensy_ota Firmware updates on the Teensy 4.1 +/// @{ +/// An uploaded flat image is staged in the free program flash above the running one, and checked. At the next reboot a routine running from RAM copies it over the program. +/// +/// @moreinfo +/// +/// ## One slot, so the copy waits for the reboot +/// +/// An ESP32 writes its update into a second app slot and flips a boot pointer. The Teensy's program flash holds one image, so the upload goes into the erased space above it, where nothing runs. +/// The copy over the program happens in `reboot()`, after the HTTP answer is out. A power loss during the upload leaves the running image untouched. +/// One during the copy, a few seconds, leaves the board to PJRC's bootloader chip and its program button, never a hard brick. +/// Both sides work in 64 KB blocks, the flash's largest erase, and the copy skips every block the new image shares with the running one. +/// +/// ## Why the copy masks every interrupt source +/// +/// The application executes from flash, and the copy erases it. +/// The core's flash routines turn interrupts back on after every command. So the copy disables every interrupt source at the NVIC and stops SysTick instead, and nothing can fire into a half-erased image. +/// It runs from ITCM, as the core's flash routines do. It passes each sector through RAM, since the flash cannot be read while it is being programmed. +/// +/// ## The image mark +/// +/// Every image carries its firmware name, and an upload must carry the running one's: another program would leave the board off the network until a USB flash. +/// The search reads its prefix from the mark itself, so the prefix occurs once per image, and that reference keeps the linker from collecting the mark. + +#include "platform/platform.h" +#include "core/util/build_info.h" // MM_FIRMWARE_NAME, the variant an image must match + +#include + +#include +#include + +extern "C" { +void eepromemu_flash_write(void* addr, const void* data, uint32_t len); +void eepromemu_flash_erase_64K_block(void* addr); +} + +namespace mm::platform { + +namespace { +constexpr uint32_t kFlashBase = 0x60000000; +constexpr uint32_t kSectorBytes = 4096; +constexpr uint32_t kBlockBytes = 65536; // the erase unit: one block erase costs about what one sector erase does, so 16x fewer of them +constexpr uint32_t kPageBytes = 256; // the most one program command writes +constexpr uint32_t kIvtOffset = 0x1000; // the image vector table the boot ROM reads, its first byte 0xD1 + +// The firmware name an upload must carry: @xref{the-image-mark}. +constexpr char kImageMark[] = "MoonLight image for " MM_FIRMWARE_NAME ";"; +constexpr uint32_t kMarkPrefixLen = sizeof("MoonLight image for ") - 1; + +alignas(4) uint8_t g_sector[kSectorBytes]; // one sector in RAM, for the upload and for the copy +uint32_t g_stagedBytes = 0; // nonzero once a checked image waits for the reboot + +// Block-aligned, so erasing a block of the program never touches a staged block not yet copied. +uint32_t stagingBase() { return (kFlashBase + static_cast(firmwareSize()) + kBlockBytes - 1) & ~(kBlockBytes - 1); } +uint32_t programEnd() { return kFlashBase + static_cast(firmwarePartition()); } + +bool allErased(const uint8_t* p, uint32_t n) { + for (uint32_t i = 0; i < n; i++) + if (p[i] != 0xFF) return false; + return true; +} + +// One sector of the staged image, written page by page; its block is erased as the upload enters it, unless already erased. +void stageSector(uint32_t addr) { + if (addr % kBlockBytes == 0 && !allErased(reinterpret_cast(addr), kBlockBytes)) + eepromemu_flash_erase_64K_block(reinterpret_cast(addr)); + for (uint32_t p = 0; p < kSectorBytes; p += kPageBytes) + if (!allErased(g_sector + p, kPageBytes)) eepromemu_flash_write(reinterpret_cast(addr + p), g_sector + p, kPageBytes); +} + +// The variant named in an image's mark, or null when the image carries none. +const char* markedVariant(const uint8_t* image, uint32_t bytes, char* out, size_t cap) { + const uint32_t n = kMarkPrefixLen; + for (uint32_t i = 0; i + n < bytes; i++) { + if (image[i] != static_cast(kImageMark[0]) || std::memcmp(image + i, kImageMark, n) != 0) continue; + size_t k = 0; + for (uint32_t j = i + n; j < bytes && image[j] != ';' && k + 1 < cap; j++) out[k++] = static_cast(image[j]); + out[k] = 0; + return out; + } + return nullptr; +} + +// Copies the staged image over the program and resets, from ITCM with interrupts off; never inlined, since its caller runs from the flash it erases. +FASTRUN __attribute__((noinline)) [[noreturn]] void copyStagedImageAndReset(uint32_t from, uint32_t bytes) { + __disable_irq(); + SYST_CSR = 0; + for (uint32_t i = 0; i < (NVIC_NUM_INTERRUPTS + 31) / 32; i++) { + (&NVIC_ICER0)[i] = 0xFFFFFFFF; // disable every source, which the flash routines' __enable_irq cannot undo + (reinterpret_cast(0xE000E280))[i] = 0xFFFFFFFF; // NVIC_ICPR: drop what is already pending + } + for (uint32_t block = 0; block < bytes; block += kBlockBytes) { + WDOG1_WSR = 0x5555; + WDOG1_WSR = 0xAAAA; + const uint32_t end = bytes - block < kBlockBytes ? bytes : block + kBlockBytes; + const uint32_t* src = reinterpret_cast(from + block); + const uint32_t* dst = reinterpret_cast(kFlashBase + block); + bool same = true; + for (uint32_t w = 0; same && w < (end - block) / 4; w++) same = dst[w] == src[w]; + if (same) continue; // a block the new image shares with the running one is left alone + eepromemu_flash_erase_64K_block(reinterpret_cast(kFlashBase + block)); + for (uint32_t off = block; off < end; off += kSectorBytes) { + const uint32_t* sector = reinterpret_cast(from + off); + uint32_t* buf = reinterpret_cast(g_sector); + for (uint32_t w = 0; w < kSectorBytes / 4; w++) buf[w] = sector[w]; + for (uint32_t p = 0; p < kSectorBytes; p += kPageBytes) { + bool erased = true; + for (uint32_t b = 0; b < kPageBytes; b++) erased = erased && g_sector[p + b] == 0xFF; + if (!erased) eepromemu_flash_write(reinterpret_cast(kFlashBase + off + p), g_sector + p, kPageBytes); + } + } + } + SCB_AIRCR = 0x05FA0004; // SYSRESETREQ: boot the new image + for (;;) {} +} +} // namespace + +// Called by reboot(): a checked upload waits for this moment, so the HTTP answer goes out before the program is replaced. +void applyStagedFirmware() { + if (g_stagedBytes) copyStagedImageAndReset(stagingBase(), g_stagedBytes); +} + +namespace { +// One sector of the upload into g_sector, its tail padded with what erased flash reads; false when the client aborted. +bool readSector(FsWriteSrc src, void* user, uint32_t& fill, bool& eof) { + fill = 0; + eof = false; + while (fill < kSectorBytes) { + bool abort = false; + const size_t n = src(reinterpret_cast(g_sector + fill), kSectorBytes - fill, user, &abort); + if (abort) return false; + if (n == 0) { eof = true; break; } + fill += static_cast(n); + } + std::memset(g_sector + fill, 0xFF, kSectorBytes - fill); + return true; +} + +// Why the first sector of an upload is no Teensy 4 image, or null. +const char* headerProblem() { + if (g_sector[0] == ':') return "error: that is a HEX file, upload the .bin"; + if (std::memcmp(g_sector, "FCFB", 4) != 0) return "error: not a Teensy 4 image"; + return nullptr; +} + +// Whether a whole staged image is refused, the reason written to `status`. +bool imageRefused(const uint8_t* staged, uint32_t bytes, char* status, size_t statusLen) { + if (bytes <= kIvtOffset || staged[kIvtOffset] != 0xD1) { + std::snprintf(status, statusLen, "error: not a Teensy 4 image"); + return true; + } + char name[32]; + if (!markedVariant(staged, bytes, name, sizeof(name))) { + std::snprintf(status, statusLen, "error: that image is not MoonLight"); + return true; + } + if (std::strcmp(name, MM_FIRMWARE_NAME) != 0) { + std::snprintf(status, statusLen, "error: image is %.20s, board runs " MM_FIRMWARE_NAME, name); + return true; + } + return false; +} +} // namespace + +bool otaWriteStream(FsWriteSrc src, void* user, size_t contentLen, + char* statusBuf, size_t statusBufLen, uint32_t* bytesReadOut) { + if (!src || !statusBuf || statusBufLen == 0 || !bytesReadOut) return false; + // A macro rather than a lambda, so the format reaches snprintf as a literal and keeps its check. + #define setStatus(...) std::snprintf(statusBuf, statusBufLen, __VA_ARGS__) + g_stagedBytes = 0; + const uint32_t base = stagingBase(); + const uint32_t room = programEnd() - base; + if (contentLen > room) { + setStatus("error: image too large (%u > %u)", static_cast(contentLen), static_cast(room)); + return false; + } + setStatus("flashing"); + uint32_t written = 0; + for (;;) { + uint32_t fill = 0; + bool eof = false; + if (!readSector(src, user, fill, eof)) { setStatus("error: upload aborted"); return false; } + if (fill == 0) break; + const char* why = (written == 0) ? headerProblem() : nullptr; + if (why) { setStatus("%s", why); return false; } + if (written + fill > room) { setStatus("error: image too large for the free flash"); return false; } + stageSector(base + written); + written += fill; + *bytesReadOut = written; + taskWdtReset(); // a megabyte of erases runs on the render loop, which the watchdog guards + if (eof) break; + } + if (contentLen && written < contentLen) { + setStatus("error: incomplete upload (%u/%u)", static_cast(written), static_cast(contentLen)); + return false; + } + if (imageRefused(reinterpret_cast(base), written, statusBuf, statusBufLen)) return false; + g_stagedBytes = written; + setStatus("rebooting"); + return true; + #undef setStatus +} + +// GitHub serves release assets over HTTPS only, which this board cannot speak. +bool http_fetch_to_ota(const char*, char* statusBuf, size_t statusBufLen, uint32_t* bytesReadOut, uint32_t* bytesTotalOut, const char*) { + if (statusBuf && statusBufLen) std::snprintf(statusBuf, statusBufLen, "error: no TLS on this board, upload the .bin instead"); + if (bytesReadOut) *bytesReadOut = 0; + if (bytesTotalOut) *bytesTotalOut = 0; + return false; +} + +// One image and no recovery image beside it: the MoonBase questions all answer no. +bool otaWriteMoonBase(FsWriteSrc, void*, size_t, char* statusBuf, size_t statusBufLen, uint32_t*) { + if (statusBuf && statusBufLen) std::snprintf(statusBuf, statusBufLen, "error: no MoonBase on this board"); + return false; +} +bool otaFetchMoonBaseUrl(const char*, char* statusBuf, size_t statusBufLen, uint32_t* bytesReadOut, uint32_t* bytesTotalOut) { + return http_fetch_to_ota(nullptr, statusBuf, statusBufLen, bytesReadOut, bytesTotalOut, nullptr); +} +bool moonbaseStageInstallUrl(const char*) { return false; } +bool otaHasMoonBase() { return false; } +bool otaBootMoonBase() { return false; } +bool otaRunningMoonBase() { return false; } +bool otaMoonBaseVersion(char*, size_t) { return false; } +bool otaMoonBaseBuild(char*, size_t) { return false; } +bool otaMoonBaseSize(uint32_t*, uint32_t*) { return false; } +void moonbaseClearStagedUrl() {} + +} // namespace mm::platform + +/// @} diff --git a/src/platform/teensy/qnethernet/qnethernet_dual_driver.cpp b/src/platform/teensy/qnethernet/qnethernet_dual_driver.cpp new file mode 100644 index 000000000..aa6795c02 --- /dev/null +++ b/src/platform/teensy/qnethernet/qnethernet_dual_driver.cpp @@ -0,0 +1,91 @@ +/// @defgroup platform_teensy_eth_driver One firmware, both Ethernet chips +/// @{ +/// QNEthernet's driver contract, answered by whichever chip the board has: its own PHY, else a W5500 on the SPI port. +/// +/// @moreinfo +/// +/// ## Why one firmware carries both +/// +/// A chip's firmware carries every Ethernet driver the chip can host, and which one a board uses is found or configured at run time, never a build choice. +/// QNEthernet compiles one driver per build, so both of its built-in drivers are compiled with their `driver` namespace renamed per file. This file is the external driver its hook takes. +/// +/// ## Detection +/// +/// The on-board PHY answers on its management bus, and QNEthernet's own driver already reports a 4.1-NE, which has none, as having no hardware. +/// So the PHY is asked first and the W5500 second, once, at the first call; a board with neither keeps the PHY driver, which reports no hardware. + +#include "qnethernet/lwip_driver.h" +#include "qnethernet_dual_driver.h" + +namespace qindesign::network { + +// The two built-in drivers' copies of the contract, renamed by the build. +#define MM_QNETHERNET_DRIVER_API \ + void get_capabilities(DriverCapabilities* dc); \ + bool is_unknown(); \ + void get_system_mac(uint8_t mac[ETH_HWADDR_LEN]); \ + bool get_mac(uint8_t mac[ETH_HWADDR_LEN]); \ + bool set_mac(const uint8_t mac[ETH_HWADDR_LEN]); \ + bool has_hardware(); \ + void set_chip_select_pin(int pin); \ + bool init(); \ + void deinit(); \ + struct pbuf* proc_input(struct netif* netif, int counter); \ + void poll(struct netif* netif); \ + void get_link_info(LinkInfo* li); \ + bool set_link(const LinkSettings* ls); \ + err_t output(struct pbuf* p); \ + bool output_frame(const void* frame, size_t len); \ + bool set_incoming_mac_address_allowed(const uint8_t mac[ETH_HWADDR_LEN], bool allow); +namespace driver_t41 { MM_QNETHERNET_DRIVER_API } +namespace driver_w5500 { MM_QNETHERNET_DRIVER_API } +#undef MM_QNETHERNET_DRIVER_API + +namespace { +bool s_chosen = false; +bool s_w5500 = false; + +bool w5500() { + if (!s_chosen) { + s_w5500 = !driver_t41::has_hardware() && driver_w5500::has_hardware(); + s_chosen = true; + } + return s_w5500; +} +} // namespace + +// Every call goes to the chosen driver. +#define MM_ONE_OF(call) (w5500() ? driver_w5500::call : driver_t41::call) + +namespace driver { +void get_capabilities(DriverCapabilities* dc) { MM_ONE_OF(get_capabilities(dc)); } +bool is_unknown() { return MM_ONE_OF(is_unknown()); } +void get_system_mac(uint8_t mac[ETH_HWADDR_LEN]) { MM_ONE_OF(get_system_mac(mac)); } +bool get_mac(uint8_t mac[ETH_HWADDR_LEN]) { return MM_ONE_OF(get_mac(mac)); } +bool set_mac(const uint8_t mac[ETH_HWADDR_LEN]) { return MM_ONE_OF(set_mac(mac)); } +bool has_hardware() { return MM_ONE_OF(has_hardware()); } +// Before detection, which probes the W5500 through this pin, so both hear it. +void set_chip_select_pin(int pin) { + driver_t41::set_chip_select_pin(pin); + driver_w5500::set_chip_select_pin(pin); +} +bool init() { return MM_ONE_OF(init()); } +void deinit() { MM_ONE_OF(deinit()); } +struct pbuf* proc_input(struct netif* netif, int counter) { return MM_ONE_OF(proc_input(netif, counter)); } +void poll(struct netif* netif) { MM_ONE_OF(poll(netif)); } +void get_link_info(LinkInfo* li) { MM_ONE_OF(get_link_info(li)); } +bool set_link(const LinkSettings* ls) { return MM_ONE_OF(set_link(ls)); } +err_t output(struct pbuf* p) { return MM_ONE_OF(output(p)); } +bool output_frame(const void* frame, size_t len) { return MM_ONE_OF(output_frame(frame, len)); } +bool set_incoming_mac_address_allowed(const uint8_t mac[ETH_HWADDR_LEN], bool allow) { + return MM_ONE_OF(set_incoming_mac_address_allowed(mac, allow)); +} +} // namespace driver + +#undef MM_ONE_OF + +} // namespace qindesign::network + +bool mmEthernetIsW5500() { return qindesign::network::w5500(); } + +/// @} diff --git a/src/platform/teensy/qnethernet/qnethernet_dual_driver.h b/src/platform/teensy/qnethernet/qnethernet_dual_driver.h new file mode 100644 index 000000000..ec585fa75 --- /dev/null +++ b/src/platform/teensy/qnethernet/qnethernet_dual_driver.h @@ -0,0 +1,5 @@ +/// What the dual Ethernet driver (qnethernet_dual_driver.cpp) tells the platform. +#pragma once + +/// Whether the board talks through a W5500 rather than its own PHY, found at the first driver call. +bool mmEthernetIsW5500(); diff --git a/src/platform/teensy/qnethernet/qnethernet_external_driver.h b/src/platform/teensy/qnethernet/qnethernet_external_driver.h new file mode 100644 index 000000000..1624bbd36 --- /dev/null +++ b/src/platform/teensy/qnethernet/qnethernet_external_driver.h @@ -0,0 +1,8 @@ +/// QNEthernet's external-driver hook, which its driver_select.h takes when this header exists. One firmware carries the board's own Ethernet and a W5500, and qnethernet_dual_driver.cpp picks the one the board has at boot. +#pragma once + +// Both built-in drivers declare these, at the same values. +#define MTU 1500 +#define MAX_FRAME_LEN 1518 /* without the 4-byte FCS */ + +// lwIP's default options, which both drivers support: no frame pad, and checksums in software, since the W5500 computes none. diff --git a/src/ui/app.js b/src/ui/app.js index 609ba3d43..b9b42205a 100644 --- a/src/ui/app.js +++ b/src/ui/app.js @@ -209,6 +209,7 @@ function connectWs() { if (state && Array.isArray(state.modules)) { applyStatePatch(data.patch); updateValues(); + relayMoonCloudOutbox(); // A targetFps slider move arrives as a PATCH, and the preview controller aims // at whatever it last heard: without this, the advertised detail/smoothness // chooser only takes effect on the next reconnect. @@ -221,6 +222,7 @@ function connectWs() { // without a `modules` array, or it would clobber `state` and blank the module view. if (!Array.isArray(data.modules)) return; state = data; + relayMoonCloudOutbox(); // Defer the DOM rebuild while the user is mid-interaction: a full state arrives on every // (re)connect, and rebuilding under an open dropdown makes it unselectable. The state is // already stored above, so updateValues() below still shows fresh values; the structural @@ -1531,6 +1533,36 @@ function installProgress(text) { // // Not to be confused with moonbaseUpdateFlow, which carries an install across TWO reboots (into // MoonBase and back) and has its own retry and handoff handling. +// The device's own Firmware status, or null when it is not answering. +async function probeFirmwareStatus() { + const ctl = new AbortController(); + const timer = setTimeout(() => ctl.abort(), 2500); + try { + const r = await fetch("/api/modules/Firmware", { cache: "no-store", signal: ctl.signal }); + if (!r.ok) return null; + const mod = await r.json(); + return (mod && mod.status) || ""; + } catch (_) { + return null; + } finally { + clearTimeout(timer); + } +} + +// Wait for a device that reboots after an install: gone at least once and answering again, or answering throughout past a grace period, false at the deadline. +// A fixed wait reloaded a Teensy still copying its image onto the browser's connection-refused page. +async function waitForReboot(timeoutMs = 120000) { + const start = Date.now(); + let wentDown = false; + while (Date.now() - start < timeoutMs) { + const up = (await probeFirmwareStatus()) !== null; + if (!up) wentDown = true; + else if (wentDown || Date.now() - start > 15000) return true; + await fwSleep(1000); + } + return false; +} + function watchInstall(kind) { const ui = showUpdateOverlay(); ui.status(`Installing ${kind.label.toLowerCase()}\u2026`); @@ -1539,22 +1571,7 @@ function watchInstall(kind) { // the install task has written anything, and rest or silence then means "not started yet". let sawWork = false; let silent = 0; - - // The device's own Firmware status, or null when it is not answering. - const probe = async () => { - const ctl = new AbortController(); - const timer = setTimeout(() => ctl.abort(), 2500); - try { - const r = await fetch("/api/modules/Firmware", { cache: "no-store", signal: ctl.signal }); - if (!r.ok) return null; - const mod = await r.json(); - return (mod && mod.status) || ""; - } catch (_) { - return null; - } finally { - clearTimeout(timer); - } - }; + const probe = probeFirmwareStatus; const tick = async () => { if (Date.now() > deadline) { ui.fail("Install timed out."); return; } @@ -1564,7 +1581,7 @@ function watchInstall(kind) { // Not answering. During a rebooting install that is the reboot itself. silent++; if (kind.reboots && sawWork) { - ui.status("Rebooting\u2026"); + ui.busy("Rebooting\u2026"); if (silent > 90) { ui.fail(MOONBASE_SILENT_MSG); return; } } else if (silent >= 4) { ui.status("The device is not answering\u2026"); @@ -1619,6 +1636,7 @@ function watchInstall(kind) { function uploadWithProgress(route, file, ui) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); + ui.watchStalls(); xhr.open("POST", route); xhr.setRequestHeader("Content-Type", "application/octet-stream"); xhr.upload.addEventListener("progress", (e) => { @@ -1648,10 +1666,19 @@ function showUpdateOverlay() { const msg = document.createElement("p"); msg.className = "fw-overlay-msg"; const bar = document.createElement("progress"); // no value = indeterminate sweep + // A browser upload pauses while the device erases flash, which on a Teensy stops it reading the network for seconds at a time. + // After a moment without progress the overlay says so, so a pause reads as work rather than a hang. + // Armed by the upload only: a device-reported install moves once per status poll, which would read as a stall between polls. + let base = "", read = 0, total = 0, lastMove = 0, stalled = false, watch = 0; + const show = (on) => { + stalled = on; + msg.textContent = on ? "Writing to flash\u2026" : base; + bar.classList.toggle("fw-stalled", on); + }; const dismiss = document.createElement("button"); dismiss.textContent = "close"; dismiss.style.display = "none"; - dismiss.addEventListener("click", () => ov.remove()); + dismiss.addEventListener("click", () => { clearInterval(watch); ov.remove(); }); const cancel = document.createElement("button"); cancel.textContent = "cancel install"; let uploadCtl = null; // the in-flight file upload's AbortController, set by the flow @@ -1665,9 +1692,22 @@ function showUpdateOverlay() { ov.appendChild(box); document.body.appendChild(ov); return { - status(text) { msg.textContent = text; }, - progress(read, total) { if (total > 0) { bar.max = total; bar.value = read; } }, - fail(text) { msg.textContent = text; bar.remove(); cancel.remove(); dismiss.style.display = ""; }, + status(text) { base = text; show(false); }, + // A wait with nothing to measure, such as the reboot: the text, and the bar pulsing as it does through a flash write. + busy(text) { clearInterval(watch); base = text; show(false); bar.classList.add("fw-stalled"); }, + progress(r, t) { + if (t <= 0) return; + bar.max = t; + bar.value = r; + if (r !== read) { read = r; total = t; lastMove = Date.now(); if (stalled) show(false); } + }, + fail(text) { clearInterval(watch); base = text; show(false); bar.remove(); cancel.remove(); dismiss.style.display = ""; }, + watchStalls() { + watch = setInterval(() => { + const on = read > 0 && read < total && Date.now() - lastMove > 1500; + if (on !== stalled) show(on); + }, 500); + }, setUpload(ctl) { uploadCtl = ctl; }, }; } @@ -2186,7 +2226,9 @@ function createCard(mod, depth) { // card-build. The picker reads SystemModule.firmware (already in // /api/state) to filter to OTA-compatible releases. On install, the // device fetches the binary via /api/firmware/url: no browser CORS in - // the data path. See docs/explanation/architecture/mooninstaller.md ยง The three words. + // the data path. A device without TLS answers that route 501, and the + // browser fetches the image from the release's Pages mirror and uploads it. + // See docs/explanation/architecture/mooninstaller.md ยง The three words. if (mod.type === "FirmwareUpdateModule") { // A nudge where someone has just thought about versions, which is the moment the aggregate // is worth something to them. Shown only while Stats consent is off, so it disappears the @@ -2286,6 +2328,46 @@ function createCard(mod, depth) { status.className = "fw-upload-status"; const say = (t) => { status.textContent = t; }; + // Upload an image the browser holds, behind the overlay every install shows. The device + // cannot report this transfer's progress (the request stays open for it), so the bar + // follows the upload itself. + const installBlob = async (blob, ui = showUpdateOverlay()) => { + ui.status(`Installing ${fmSize(blob.size)}โ€ฆ`); + try { + await uploadWithProgress(k.uploadRoute, blob, ui); + if (k.reboots) { + ui.busy("Rebootingโ€ฆ"); + if (!await waitForReboot()) { ui.fail("The device did not come back within two minutes: check it, then reload."); return; } + } else { + ui.status(`${k.label} installed`); + await fwSleep(1200); + } + location.reload(); + } catch (err) { + ui.fail("Install failed: " + err.message); + } + }; + + // For a device that cannot fetch a URL (no TLS): the browser downloads the image and uploads it. + // GitHub's release assets refuse a cross-origin read, so a release asset comes from its + // copy on the installer's Pages site, which allows one. + const installViaBrowser = async (url) => { + const m = /\/releases\/download\/([^/]+)\/([^/]+)$/.exec(url); + const source = m ? `https://moonmodules.org/MoonLight/install/releases/${m[1]}/${m[2]}` : url; + const ui = showUpdateOverlay(); + ui.status("Downloading the imageโ€ฆ"); + let blob; + try { + const res = await fetch(source); + if (!res.ok) throw new Error(`HTTP ${res.status} from ${source}`); + blob = await res.blob(); + } catch (err) { + ui.fail("Download failed: " + err.message); + return; + } + await installBlob(blob, ui); + }; + // Hand a URL to the device. Every install route answers 202 and works on its own task, so // the overlay reports progress rather than this response. const installUrl = async (url) => { @@ -2300,6 +2382,7 @@ function createCard(mod, depth) { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url }), }); + if (res.status === 501 && kind === "app") { await installViaBrowser(url); return; } if (!res.ok) throw new Error(await errorMessage(res)); watchInstall(k); }; @@ -2361,24 +2444,8 @@ function createCard(mod, depth) { return; } upBtn.disabled = true; - // The overlay, like every other install. This is the one whose progress the DEVICE - // cannot report (the request stays open for the transfer), so the bar is driven from - // the upload itself, but a user should not have to know which install they picked to - // know where the progress appears. - const ui = showUpdateOverlay(); - ui.status(`Installing ${fmSize(file.size)}โ€ฆ`); try { - await uploadWithProgress(k.uploadRoute, file, ui); - if (k.reboots) { - ui.status("Rebootingโ€ฆ"); - await fwSleep(4000); - } else { - ui.status(`${k.label} installed`); - await fwSleep(1200); - } - location.reload(); - } catch (err) { - ui.fail("Install failed: " + err.message); + await installBlob(file); } finally { upBtn.disabled = false; } @@ -6388,6 +6455,34 @@ function consented(mod) { const kMoonCloudUrl = "https://mooncloud-stats.moonmodules.workers.dev"; +// A board without HTTPS leaves its MoonCloud request in the hidden `outbox` control; the device page, which has HTTPS, posts it and presses `outbox sent` to empty it. +// One request at a time, and after a failed post a pause, so an unreachable server is not asked once a second. +let moonCloudRelayBusy = false; +let moonCloudRelayRetryAt = 0; +async function relayMoonCloudOutbox() { + if (moonCloudRelayBusy || Date.now() < moonCloudRelayRetryAt) return; + const cloud = allModules().find(m => m.type === "MoonCloudModule"); + const raw = cloud?.controls?.find(c => c.name === "outbox")?.value; + if (!raw) return; + let req; + try { req = JSON.parse(raw); } catch (_) { return; } + if (typeof req.path !== "string" || !req.path.startsWith("/api/")) return; + moonCloudRelayBusy = true; + try { + const res = await fetch(kMoonCloudUrl + req.path, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(req.body), + }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + await sendControl(cloud.name, "outbox sent", 1); + } catch (_) { + moonCloudRelayRetryAt = Date.now() + 30000; + } finally { + moonCloudRelayBusy = false; + } +} + let moonCloudStatsCache = new Map(); // filter query -> stats, so switching back is free let moonCloudFilter = {}; // dimension -> value the reader chose let moonCloudGeneration = 0; // bumped on a control write; older answers are dropped diff --git a/src/ui/style.css b/src/ui/style.css index 1a5d8f6f9..ec6904f66 100644 --- a/src/ui/style.css +++ b/src/ui/style.css @@ -2159,6 +2159,9 @@ button.fileedit-pick:disabled { opacity: 0.6; cursor: default; } gap: 1rem; } .fw-overlay-box progress { width: 100%; } +/* A wait the bar cannot measure (a flash write, the reboot): a pulse, so the pause reads as work. */ +.fw-overlay-box progress.fw-stalled { animation: fw-pulse 0.8s ease-in-out infinite alternate; } +@keyframes fw-pulse { to { opacity: 0.45; } } .fw-overlay-msg { color: var(--fg-muted); margin: 0; overflow-wrap: anywhere; } /* ASSIGN MODE: while it is on, a tap on a surface control opens its target picker rather than moving diff --git a/teensy/CMakeLists.txt b/teensy/CMakeLists.txt index 46509c371..0a7fd494a 100644 --- a/teensy/CMakeLists.txt +++ b/teensy/CMakeLists.txt @@ -23,19 +23,18 @@ target_include_directories(teensy_libs SYSTEM PUBLIC "${TEENSY_SDK}/LittleFS/src target_link_libraries(teensy_libs PUBLIC teensy_core) target_compile_options(teensy_libs PRIVATE $<$:-std=gnu++17>) -# Which binary this is: `teensy41` drives the on-board PHY, `teensy41-w5500` a W5500 on the SPI port, for a 4.1-NE. -set(MM_FIRMWARE "teensy41" CACHE STRING "the Teensy firmware variant: teensy41 or teensy41-w5500") -if(NOT MM_FIRMWARE MATCHES "^teensy41(-w5500)?$") - message(FATAL_ERROR "MM_FIRMWARE=${MM_FIRMWARE}: expected teensy41 or teensy41-w5500") -endif() +# Which binary this is and which Ethernet driver it carries, both set by build_teensy.py from the firmware registry, which names and checks the variants. +set(MM_FIRMWARE "teensy41" CACHE STRING "the firmware name the binary reports") -# The network stack: lwIP with QNEthernet, whose driver follows the firmware; the W5500 sits on the hardware SPI port and chip select 10. +# The network stack: lwIP with QNEthernet, carrying both its drivers, the board's own PHY and a W5500 on the hardware SPI port and chip select 10. +# QNEthernet builds one driver, so each is compiled with its `driver` namespace renamed, and our external driver (its hook) picks the chip the board has at boot. file(GLOB_RECURSE QNETHERNET_SOURCES "${TEENSY_SDK}/QNEthernet/src/*.c" "${TEENSY_SDK}/QNEthernet/src/*.cpp") -add_library(qnethernet STATIC ${QNETHERNET_SOURCES}) -target_include_directories(qnethernet SYSTEM PUBLIC "${TEENSY_SDK}/QNEthernet/src") -if(MM_FIRMWARE STREQUAL "teensy41-w5500") - target_compile_definitions(qnethernet PUBLIC QNETHERNET_DRIVER_W5500) -endif() +add_library(qnethernet STATIC ${QNETHERNET_SOURCES} ${REPO}/src/platform/teensy/qnethernet/qnethernet_dual_driver.cpp) +target_include_directories(qnethernet SYSTEM PUBLIC "${TEENSY_SDK}/QNEthernet/src" "${REPO}/src/platform/teensy/qnethernet") +set_source_files_properties("${TEENSY_SDK}/QNEthernet/src/qnethernet/drivers/driver_teensy41.cpp" + PROPERTIES COMPILE_DEFINITIONS "QNETHERNET_INTERNAL_DRIVER_TEENSY41;driver=driver_t41") +set_source_files_properties("${TEENSY_SDK}/QNEthernet/src/qnethernet/drivers/driver_w5500.cpp" + PROPERTIES COMPILE_DEFINITIONS "QNETHERNET_INTERNAL_DRIVER_W5500;driver=driver_w5500") # The preview drains one send buffer per 20 ms tick, so the buffer bounds its rate: 16 segments, with the segment pool and lwIP's heap to match. target_compile_definitions(qnethernet PUBLIC TCP_SND_BUF=23360 MEMP_NUM_TCP_SEG=64 MEM_SIZE=48000) target_link_libraries(qnethernet PUBLIC teensy_libs) @@ -106,6 +105,8 @@ add_executable(MoonLight.elf ${REPO}/src/platform/teensy/moonlive_asm_thumb2.cpp ${REPO}/src/platform/teensy/platform_teensy.cpp ${REPO}/src/platform/teensy/platform_teensy_gpio_dma.cpp + ${REPO}/src/platform/teensy/platform_teensy_ota.cpp + ${REPO}/src/platform/teensy/platform_teensy_improv.cpp ) add_dependencies(MoonLight.elf build_info_gen ui_embed moonlive_catalog) target_include_directories(MoonLight.elf PRIVATE ${REPO}/src ${REPO}/src/platform/teensy) @@ -121,8 +122,9 @@ endif() target_link_libraries(MoonLight.elf PRIVATE qnethernet teensy_libs teensy_core m stdc++) target_link_options(MoonLight.elf PRIVATE "-T${CMAKE_BINARY_DIR}/imxrt1062_t41.ld") -# The image Teensy Loader writes. +# The HEX Teensy Loader writes, and the same image flat for the writers that take raw bytes at an offset: from the HEX, which holds only the flash records at 0x60000000, and gap-filled with 0xFF, what erased flash reads. add_custom_command(TARGET MoonLight.elf POST_BUILD COMMAND ${CMAKE_OBJCOPY} -O ihex -R .eeprom MoonLight.elf MoonLight.hex + COMMAND ${CMAKE_OBJCOPY} -I ihex -O binary --gap-fill 0xff MoonLight.hex MoonLight.bin COMMAND ${CMAKE_SIZE} -A MoonLight.elf - COMMENT "MoonLight.hex") + COMMENT "MoonLight.hex, MoonLight.bin") diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 3edd20057..bc54db443 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -94,6 +94,7 @@ add_executable(mm_tests unit/core/unit_MoonStatsModule.cpp unit/core/unit_MoonTalkModule.cpp unit/core/unit_InstallationId.cpp + unit/core/unit_MoonCloudModule.cpp unit/core/unit_sha256.cpp unit/core/unit_MoonStatsReport.cpp unit/core/unit_SystemModule.cpp diff --git a/test/js/installer-eth-only.test.mjs b/test/js/installer-eth-only.test.mjs index 9134a45d0..059e9d5b6 100644 --- a/test/js/installer-eth-only.test.mjs +++ b/test/js/installer-eth-only.test.mjs @@ -28,9 +28,11 @@ const firmwares = JSON.parse( // Ethernet-only; `-eth-wifi` (the P4 co-processor WiFi build) is NOT, since it doesn't end in `-eth`. const isEthOnlyByName = (name) => /-eth$/.test(name); -test("the installer's /-eth$/ rule matches firmwares.json eth_only for every firmware", () => { - assert.ok(firmwares.length > 0, "firmwares.json has no firmwares"); - for (const f of firmwares) { +// The rule belongs to the ESP32 flashing path; a Teensy is Ethernet-only by nature and gets its own writer. +test("the installer's /-eth$/ rule matches firmwares.json eth_only for every ESP32 firmware", () => { + const esp32 = firmwares.filter((f) => f.platform === "esp32"); + assert.ok(esp32.length > 0, "firmwares.json has no ESP32 firmwares"); + for (const f of esp32) { assert.equal( isEthOnlyByName(f.name), !!f.eth_only, diff --git a/test/js/installer-teensy-writer.test.mjs b/test/js/installer-teensy-writer.test.mjs new file mode 100644 index 000000000..32bee39bc --- /dev/null +++ b/test/js/installer-teensy-writer.test.mjs @@ -0,0 +1,83 @@ +// The installer's Teensy writer speaks PJRC's HalfKay protocol as teensy_loader_cli does: these tests pin the report layout, +// the blocks it skips, and the images it refuses, so a browser install writes what the loader would. +// +// Run: `node --test test/js`. + +import { test } from "node:test"; +import assert from "node:assert/strict"; + +import { + REPORT_BYTES, + TEENSY41_CODE_BYTES, + TEENSY41_SETTINGS_OFFSET, + bootReport, + eraseSettingsReports, + halfkayReports, + imageProblem, + isTeensySerial, +} from "../../mooninstaller/teensy-writer.js"; + +// A flat image as the build writes it: the "FCFB" configuration block first, then code, with erased gaps. +function image(bytes) { + const img = new Uint8Array(bytes).fill(0xFF); + img.set([0x46, 0x43, 0x46, 0x42], 0); // "FCFB" + return img; +} + +test("a report is a 64-byte header carrying the block's offset little-endian, then 1024 image bytes", () => { + const img = image(0x30000 + 1024); + img[0x30000] = 0x5A; + const last = halfkayReports(img).at(-1); + assert.equal(last.report.length, REPORT_BYTES); + assert.deepEqual([...last.report.subarray(0, 3)], [0x00, 0x00, 0x03]); + assert.ok(last.report.subarray(3, 64).every(b => b === 0)); + assert.equal(last.report[64], 0x5A); +}); + +test("block 0 is always written, since it erases the chip, and a later all-0xFF block is skipped", () => { + const img = image(4 * 1024); + img[2 * 1024 + 7] = 0x01; + assert.deepEqual(halfkayReports(img).map(r => r.offset), [0, 2048]); +}); + +test("writing the reports onto erased flash reproduces the image byte for byte", () => { + const img = image(10 * 1024 + 300); // a short last block, as a real image ends + for (let i = 4; i < img.length; i += 997) img[i] = i & 0xFF; + const flash = new Uint8Array(11 * 1024).fill(0xFF); + for (const { offset, report } of halfkayReports(img)) flash.set(report.subarray(64), offset); + assert.deepEqual(flash.subarray(0, img.length), img); + assert.ok(flash.subarray(img.length).every(b => b === 0xFF), "the short last block pads with erased flash"); +}); + +test("the boot report is 0xFF in its three offset bytes and zero elsewhere", () => { + const r = bootReport(); + assert.equal(r.length, REPORT_BYTES); + assert.deepEqual([...r.subarray(0, 3)], [0xFF, 0xFF, 0xFF]); + assert.ok(r.subarray(3).every(b => b === 0)); +}); + +test("an image is refused when it is a HEX file, lacks the FCFB block, or exceeds the program flash", () => { + assert.match(imageProblem(new TextEncoder().encode(":020000040000FA")), /HEX/); + assert.match(imageProblem(new Uint8Array([1, 2, 3, 4, 5])), /not a Teensy 4 image/); + assert.match(imageProblem(image(TEENSY41_CODE_BYTES + 1)), /more than/); + assert.match(imageProblem(new Uint8Array(0)), /empty/); + assert.equal(imageProblem(image(4096)), ""); +}); + +test("a Teensy is recognized by PJRC's vendor id and the USB-serial product id", () => { + assert.ok(isTeensySerial({ usbVendorId: 0x16C0, usbProductId: 0x0483 })); + assert.ok(!isTeensySerial({ usbVendorId: 0x16C0, usbProductId: 0x0478 }), "the bootloader is HID, not a serial port"); + assert.ok(!isTeensySerial({ usbVendorId: 0x303A, usbProductId: 0x1001 })); + assert.ok(!isTeensySerial({})); +}); + +test("an erase zeroes the settings' superblock pair, 8 KB at the bottom of the 1 MB settings area, so the firmware formats it at boot", () => { + const reports = eraseSettingsReports(); + assert.equal(TEENSY41_SETTINGS_OFFSET, 0x6C0000); + assert.deepEqual(reports.map(r => r.offset), [0, 1, 2, 3, 4, 5, 6, 7].map(i => 0x6C0000 + i * 1024)); + for (const { offset, report } of reports) { + assert.deepEqual([...report.subarray(0, 3)], [offset & 0xFF, (offset >> 8) & 0xFF, offset >> 16]); + assert.ok(report.subarray(64).every(b => b === 0)); + assert.ok(offset + 1024 <= TEENSY41_CODE_BYTES, "inside what the bootloader writes"); + } +}); diff --git a/test/python/test_installer_manifests.py b/test/python/test_installer_manifests.py index 36cb1ae69..75a6ecdb7 100644 --- a/test/python/test_installer_manifests.py +++ b/test/python/test_installer_manifests.py @@ -15,8 +15,9 @@ 1. every `ships: true` firmware in mooninstaller/firmwares.json generates a valid manifest, and 2. every part path in every manifest matches one of the staged-file globs above. -Self-contained: it feeds generate_manifest.py a synthetic flasher_args.json (the real one only -exists after an ESP-IDF build), so it runs in plain CI with no firmware build. +Self-contained: it feeds generate_manifest.py a synthetic flasher_args.json for an ESP32 firmware +(the real one only exists after an ESP-IDF build), so it runs in plain CI with no firmware build. +A Teensy firmware's manifest needs no build input: it is the flat image at offset 0. """ import fnmatch @@ -54,27 +55,37 @@ VERSION = "9.9.9" # arbitrary; the test asserts the version flows into the filenames +def _rows(): + return json.loads(FIRMWARES_JSON.read_text(encoding="utf-8"))["firmwares"] + + def _shipping_firmwares(): # encoding='utf-8' โ€” the JSON carries a fs/documentation-note character # (byte 0x8f is part of a UTF-8 multi-byte sequence for a non-ASCII glyph # in one of the comment fields) and Windows Python 3.14 defaults read_text # to cp1252, which fails with UnicodeDecodeError. Same class of Windows- # only encoding bug as verify_version's stdout crash. - data = json.loads(FIRMWARES_JSON.read_text(encoding="utf-8")) - return [f["name"] for f in data["firmwares"] if f.get("ships")] + return [f["name"] for f in _rows() if f.get("ships")] + + +def _platform(firmware): + return next(f["platform"] for f in _rows() if f["name"] == firmware) def _generate(firmware, tmp_path): """Run generate_manifest.py for one firmware with a synthetic flasher_args; return the manifest.""" - flasher = tmp_path / f"flasher-{firmware}.json" - flasher.write_text(json.dumps(FLASHER_ARGS)) + args = [] + if _platform(firmware) == "esp32": + flasher = tmp_path / f"flasher-{firmware}.json" + flasher.write_text(json.dumps(FLASHER_ARGS)) + args = ["--flasher-args", str(flasher)] out = tmp_path / f"manifest-{firmware}.json" res = subprocess.run( [sys.executable, str(GENERATE), "--firmware", firmware, "--version", VERSION, "--release-url", ".", # relative paths, as the Pages installer needs - "--flasher-args", str(flasher), + *args, "--out", str(out)], capture_output=True, text=True, ) @@ -112,8 +123,18 @@ def test_app_and_bootloader_carry_the_version(tmp_path): """The per-firmware app + bootloader filenames must embed the firmware key and version, so a release dir's files line up with the manifest the same dir serves (the v stamp is what differs per release).""" - fw = _shipping_firmwares()[0] + fw = next(f for f in _shipping_firmwares() if _platform(f) == "esp32") manifest = _generate(fw, tmp_path) names = [p["path"].rsplit("/", 1)[-1] for p in manifest["builds"][0]["parts"]] assert f"firmware-{fw}-v{VERSION}.bin" in names, f"{fw}: app part not named for firmware+version: {names}" assert f"firmware-{fw}-v{VERSION}-bootloader.bin" in names, f"{fw}: bootloader part misnamed: {names}" + + +def test_a_teensy_manifest_is_its_flat_image_at_offset_zero(tmp_path): + """The installer's Teensy writer takes one image starting at the flash base, so the manifest names the release's `.bin` at 0 under the Teensy family.""" + teensy = [f for f in _shipping_firmwares() if _platform(f) == "teensy"] + assert teensy, "no shipping Teensy firmware" + for fw in teensy: + build = _generate(fw, tmp_path)["builds"][0] + assert build["chipFamily"] == "Teensy 4.1" + assert [(p["path"].rsplit("/", 1)[-1], p["offset"]) for p in build["parts"]] == [(f"firmware-{fw}-v{VERSION}.bin", 0)] diff --git a/test/python/test_teensy_flash_map.py b/test/python/test_teensy_flash_map.py new file mode 100644 index 000000000..d86a65283 --- /dev/null +++ b/test/python/test_teensy_flash_map.py @@ -0,0 +1,45 @@ +"""The Teensy 4.1's flash map has three readers that must agree: the platform, which places the program and the settings; the web installer's writer, which erases the settings by offset; and the build, which reports the program's room. + +The platform is the one home (platform_teensy.cpp); the other two restate it in their own language, so this test reads all three and fails on drift. +An installer erase aimed at the wrong offset would silently leave the settings in place, or zero part of the program. +""" + +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent.parent +sys.path.insert(0, str(ROOT / "moondeck" / "build")) +import build_teensy # noqa: E402 + + +def _cpp_constant(name): + text = (ROOT / "src" / "platform" / "teensy" / "platform_teensy.cpp").read_text(encoding="utf-8") + m = re.search(rf"constexpr uint32_t {name} = ([0-9 *]+);", text) + assert m, f"{name} not found in platform_teensy.cpp" + return eval(m.group(1)) # a product of integer literals only, matched above + + +def _js_constant(name, known): + text = (ROOT / "mooninstaller" / "teensy-writer.js").read_text(encoding="utf-8") + m = re.search(rf"export const {name} = ([A-Z0-9_ *+-]+);", text) + assert m, f"{name} not found in teensy-writer.js" + return eval(m.group(1), {}, dict(known)) # literals and the constants named in `known` + + +FLASH = _cpp_constant("kFlashBytes") +RESERVED = _cpp_constant("kReservedBytes") +SETTINGS = _cpp_constant("kFsBytes") + + +def test_the_installer_writes_exactly_the_flash_below_the_reserved_top(): + assert _js_constant("TEENSY41_CODE_BYTES", {}) == FLASH - RESERVED + + +def test_the_installer_erases_the_settings_where_the_platform_keeps_them(): + code = _js_constant("TEENSY41_CODE_BYTES", {}) + assert _js_constant("TEENSY41_SETTINGS_OFFSET", {"TEENSY41_CODE_BYTES": code}) == FLASH - RESERVED - SETTINGS + + +def test_the_build_reports_the_program_room_the_platform_leaves(): + assert build_teensy.PROGRAM_FLASH_BYTES == FLASH - RESERVED - SETTINGS diff --git a/test/scenarios/core/scenario_Control_a_palette_preset_changes_only_the_palette.json b/test/scenarios/core/scenario_Control_a_palette_preset_changes_only_the_palette.json index af981105d..72421a5b1 100644 --- a/test/scenarios/core/scenario_Control_a_palette_preset_changes_only_the_palette.json +++ b/test/scenarios/core/scenario_Control_a_palette_preset_changes_only_the_palette.json @@ -210,7 +210,7 @@ }, "last_updated": "2026-10-04" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 2277, "p95": 2277, diff --git a/test/scenarios/core/scenario_Firmware_reports_what_is_running.json b/test/scenarios/core/scenario_Firmware_reports_what_is_running.json index bc65f8bd7..f59183830 100644 --- a/test/scenarios/core/scenario_Firmware_reports_what_is_running.json +++ b/test/scenarios/core/scenario_Firmware_reports_what_is_running.json @@ -76,7 +76,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 2142, "p95": 2142, @@ -185,7 +185,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 2140, "p95": 2140, diff --git a/test/scenarios/core/scenario_Network_hardware_reconfigures_live.json b/test/scenarios/core/scenario_Network_hardware_reconfigures_live.json index 37532ba70..96d68273d 100644 --- a/test/scenarios/core/scenario_Network_hardware_reconfigures_live.json +++ b/test/scenarios/core/scenario_Network_hardware_reconfigures_live.json @@ -121,7 +121,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 2146, "p95": 2146, @@ -238,7 +238,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 2146, "p95": 2146, @@ -355,7 +355,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 2147, "p95": 2147, @@ -488,7 +488,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 2144, "p95": 2144, diff --git a/test/scenarios/core/scenario_State_one_document_adds_an_effect_with_its_controls.json b/test/scenarios/core/scenario_State_one_document_adds_an_effect_with_its_controls.json index bbaa3171c..779c89089 100644 --- a/test/scenarios/core/scenario_State_one_document_adds_an_effect_with_its_controls.json +++ b/test/scenarios/core/scenario_State_one_document_adds_an_effect_with_its_controls.json @@ -73,10 +73,10 @@ "p95": 387, "min": 1, "max": 555, - "n": 29, - "samples": [367, 555, 1, 387, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 4, 1, 7, 7, 8, 7, 7, 8, 7, 7, 7, 7, 8] + "n": 30, + "samples": [367, 555, 1, 387, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 4, 1, 7, 7, 8, 7, 7, 8, 7, 7, 7, 7, 8, 7] }, - "last_updated": "2026-10-10" + "last_updated": "2026-10-12" }, "esp32s3-n16r8": { "tick_us": { @@ -105,7 +105,7 @@ }, "last_updated": "2026-10-04" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 2478, "p95": 8301, diff --git a/test/scenarios/device/scenario_Aurora_fps.json b/test/scenarios/device/scenario_Aurora_fps.json index ce0d5c7d0..4266f33cc 100644 --- a/test/scenarios/device/scenario_Aurora_fps.json +++ b/test/scenarios/device/scenario_Aurora_fps.json @@ -258,7 +258,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 7999, "p95": 7999, @@ -472,7 +472,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 7771, "p95": 7771, @@ -686,7 +686,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 13049, "p95": 13049, @@ -900,7 +900,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 24127, "p95": 24127, @@ -1114,7 +1114,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 33192, "p95": 33192, @@ -1328,7 +1328,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 30785, "p95": 30785, @@ -1542,7 +1542,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 25053, "p95": 25053, diff --git a/test/scenarios/device/scenario_Fluid_solver.json b/test/scenarios/device/scenario_Fluid_solver.json index d0550ca6d..da537028b 100644 --- a/test/scenarios/device/scenario_Fluid_solver.json +++ b/test/scenarios/device/scenario_Fluid_solver.json @@ -252,7 +252,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 8124, "p95": 8124, @@ -458,7 +458,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 11353, "p95": 11353, @@ -664,7 +664,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 7825, "p95": 7825, @@ -870,7 +870,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 8222, "p95": 8222, @@ -1075,7 +1075,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 14799, "p95": 14799, @@ -1282,7 +1282,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 14836, "p95": 14836, @@ -1488,7 +1488,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 14853, "p95": 14853, @@ -1694,7 +1694,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 7851, "p95": 7851, @@ -1899,7 +1899,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 7855, "p95": 7855, @@ -2104,7 +2104,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 7855, "p95": 7855, @@ -2310,7 +2310,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 7861, "p95": 7861, @@ -2516,7 +2516,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 7870, "p95": 7870, @@ -2721,7 +2721,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 8081, "p95": 8081, diff --git a/test/scenarios/device/scenario_GridLayout_resize.json b/test/scenarios/device/scenario_GridLayout_resize.json index 91fb1fd71..68274841b 100644 --- a/test/scenarios/device/scenario_GridLayout_resize.json +++ b/test/scenarios/device/scenario_GridLayout_resize.json @@ -348,7 +348,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 1521, "p95": 1521, @@ -622,7 +622,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 1227, "p95": 1227, @@ -896,7 +896,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 1444, "p95": 1444, diff --git a/test/scenarios/device/scenario_perf_full.json b/test/scenarios/device/scenario_perf_full.json index b5339ab86..7ce7c666f 100644 --- a/test/scenarios/device/scenario_perf_full.json +++ b/test/scenarios/device/scenario_perf_full.json @@ -261,7 +261,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 45, "p95": 45, @@ -473,7 +473,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 44, "p95": 44, @@ -676,7 +676,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 45, "p95": 45, @@ -892,7 +892,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 37, "p95": 37, @@ -1119,7 +1119,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 49, "p95": 49, @@ -1333,7 +1333,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 53, "p95": 53, @@ -1560,7 +1560,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 43, "p95": 43, @@ -1788,7 +1788,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 44, "p95": 44, @@ -2017,7 +2017,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 2250, "p95": 2250, @@ -2257,7 +2257,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 46, "p95": 46, @@ -2473,7 +2473,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 154, "p95": 154, @@ -2689,7 +2689,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 588, "p95": 588, @@ -2905,7 +2905,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 2264, "p95": 2264, @@ -3135,7 +3135,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 95, "p95": 95, @@ -3351,7 +3351,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 374, "p95": 374, @@ -3567,7 +3567,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 1617, "p95": 1617, @@ -3783,7 +3783,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 6797, "p95": 6797, @@ -4013,7 +4013,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 45, "p95": 45, @@ -4229,7 +4229,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 160, "p95": 160, @@ -4445,7 +4445,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 616, "p95": 616, @@ -4661,7 +4661,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 2838, "p95": 2838, diff --git a/test/scenarios/device/scenario_perf_light.json b/test/scenarios/device/scenario_perf_light.json index 6692f598b..cd476f6fe 100644 --- a/test/scenarios/device/scenario_perf_light.json +++ b/test/scenarios/device/scenario_perf_light.json @@ -268,7 +268,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 43, "p95": 43, @@ -485,7 +485,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 35, "p95": 35, @@ -688,7 +688,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 35, "p95": 35, @@ -904,7 +904,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 47, "p95": 47, @@ -1120,7 +1120,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 168, "p95": 168, @@ -1336,7 +1336,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 662, "p95": 662, diff --git a/test/scenarios/device/scenario_peripheral_grid_sweep.json b/test/scenarios/device/scenario_peripheral_grid_sweep.json index 709d8ccd0..c626469e0 100644 --- a/test/scenarios/device/scenario_peripheral_grid_sweep.json +++ b/test/scenarios/device/scenario_peripheral_grid_sweep.json @@ -284,7 +284,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 97, "p95": 97, @@ -503,7 +503,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 366, "p95": 366, @@ -722,7 +722,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 1524, "p95": 1524, @@ -941,7 +941,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 6618, "p95": 6618, @@ -1184,7 +1184,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 97, "p95": 97, @@ -1403,7 +1403,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 365, "p95": 365, @@ -1622,7 +1622,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 1404, "p95": 1404, @@ -1841,7 +1841,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 6282, "p95": 6282, @@ -2084,7 +2084,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 110, "p95": 110, @@ -2303,7 +2303,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 444, "p95": 444, @@ -2522,7 +2522,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 1370, "p95": 1370, @@ -2741,7 +2741,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 6045, "p95": 6045, @@ -2984,7 +2984,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 105, "p95": 105, @@ -3203,7 +3203,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 407, "p95": 407, @@ -3422,7 +3422,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 1852, "p95": 1852, @@ -3641,7 +3641,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 6129, "p95": 6129, diff --git a/test/scenarios/device/scenario_peripheral_switch.json b/test/scenarios/device/scenario_peripheral_switch.json index 6ca05aaa0..48381a0f4 100644 --- a/test/scenarios/device/scenario_peripheral_switch.json +++ b/test/scenarios/device/scenario_peripheral_switch.json @@ -319,7 +319,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 103, "p95": 103, @@ -532,7 +532,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 107, "p95": 107, @@ -745,7 +745,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 111, "p95": 111, @@ -957,7 +957,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 109, "p95": 109, @@ -1170,7 +1170,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 97, "p95": 97, @@ -1399,7 +1399,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 98, "p95": 98, diff --git a/test/scenarios/light/scenario_Drivers_output_and_brightness.json b/test/scenarios/light/scenario_Drivers_output_and_brightness.json index 3866a77ee..691b5b302 100644 --- a/test/scenarios/light/scenario_Drivers_output_and_brightness.json +++ b/test/scenarios/light/scenario_Drivers_output_and_brightness.json @@ -160,7 +160,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 2144, "p95": 2173, diff --git a/test/scenarios/light/scenario_Effects_swap_while_running.json b/test/scenarios/light/scenario_Effects_swap_while_running.json index 325e71e58..078c71e9a 100644 --- a/test/scenarios/light/scenario_Effects_swap_while_running.json +++ b/test/scenarios/light/scenario_Effects_swap_while_running.json @@ -139,7 +139,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 2146, "p95": 2165, @@ -248,7 +248,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 2167, "p95": 2482, diff --git a/test/scenarios/light/scenario_Effects_teardown_under_a_running_pipeline.json b/test/scenarios/light/scenario_Effects_teardown_under_a_running_pipeline.json index f8df7b840..643ba2d45 100644 --- a/test/scenarios/light/scenario_Effects_teardown_under_a_running_pipeline.json +++ b/test/scenarios/light/scenario_Effects_teardown_under_a_running_pipeline.json @@ -121,7 +121,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 2145, "p95": 2145, @@ -229,7 +229,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 2157, "p95": 2161, @@ -333,7 +333,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 2153, "p95": 2155, @@ -447,7 +447,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 2158, "p95": 2164, @@ -567,7 +567,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 2156, "p95": 2167, @@ -683,7 +683,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 2159, "p95": 2160, @@ -815,7 +815,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 84, "p95": 94, diff --git a/test/scenarios/light/scenario_Layers_stack_and_blend_live.json b/test/scenarios/light/scenario_Layers_stack_and_blend_live.json index 4ccaef4c4..96a106d46 100644 --- a/test/scenarios/light/scenario_Layers_stack_and_blend_live.json +++ b/test/scenarios/light/scenario_Layers_stack_and_blend_live.json @@ -96,7 +96,7 @@ }, "last_updated": "2026-10-10" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 117, "p95": 117, @@ -167,7 +167,7 @@ }, "last_updated": "2026-10-10" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 444, "p95": 444, @@ -282,7 +282,7 @@ }, "last_updated": "2026-10-10" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 641, "p95": 641, @@ -349,7 +349,7 @@ }, "last_updated": "2026-10-10" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 3365, "p95": 3365, @@ -416,7 +416,7 @@ }, "last_updated": "2026-10-10" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 7479, "p95": 7479, @@ -480,7 +480,7 @@ }, "last_updated": "2026-10-10" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 2639, "p95": 2639, @@ -533,7 +533,7 @@ }, "last_updated": "2026-10-10" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 2642, "p95": 2642, @@ -596,7 +596,7 @@ }, "last_updated": "2026-10-10" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 2643, "p95": 2643, diff --git a/test/scenarios/light/scenario_Layouts_resize_reallocates_live.json b/test/scenarios/light/scenario_Layouts_resize_reallocates_live.json index f89bbe9e8..8168cfce0 100644 --- a/test/scenarios/light/scenario_Layouts_resize_reallocates_live.json +++ b/test/scenarios/light/scenario_Layouts_resize_reallocates_live.json @@ -135,7 +135,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 7472, "p95": 7472, @@ -244,7 +244,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 7477, "p95": 7477, @@ -360,7 +360,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 7788, "p95": 7788, diff --git a/test/scenarios/light/scenario_Modifiers_reshape_the_mapping.json b/test/scenarios/light/scenario_Modifiers_reshape_the_mapping.json index e60614e2f..567dacf36 100644 --- a/test/scenarios/light/scenario_Modifiers_reshape_the_mapping.json +++ b/test/scenarios/light/scenario_Modifiers_reshape_the_mapping.json @@ -120,7 +120,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 2145, "p95": 2146, @@ -236,7 +236,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 2165, "p95": 2175, @@ -345,7 +345,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 2156, "p95": 2162, @@ -453,7 +453,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 2163, "p95": 2164, @@ -567,7 +567,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 2154, "p95": 2166, @@ -683,7 +683,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 2159, "p95": 2167, @@ -797,7 +797,7 @@ }, "last_updated": "2026-09-30" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 2156, "p95": 2156, diff --git a/test/scenarios/light/scenario_Palettes_a_script_drives_the_effects.json b/test/scenarios/light/scenario_Palettes_a_script_drives_the_effects.json index c283e3cb2..31dcd4de7 100644 --- a/test/scenarios/light/scenario_Palettes_a_script_drives_the_effects.json +++ b/test/scenarios/light/scenario_Palettes_a_script_drives_the_effects.json @@ -111,7 +111,7 @@ }, "last_updated": "2026-10-11" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 2162, "p95": 7558, @@ -206,7 +206,7 @@ }, "last_updated": "2026-10-11" }, - "teensy41-w5500": { + "teensy41": { "tick_us": { "p50": 2150, "p95": 7763, diff --git a/test/unit/core/unit_FirmwareImage.cpp b/test/unit/core/unit_FirmwareImage.cpp index 05b45aacb..cf90659f2 100644 --- a/test/unit/core/unit_FirmwareImage.cpp +++ b/test/unit/core/unit_FirmwareImage.cpp @@ -72,6 +72,9 @@ std::vector> builtImages() { if (!std::filesystem::exists(build)) return out; for (const auto& e : std::filesystem::directory_iterator(build)) { if (!e.is_directory()) continue; + // The ESP32 builds' directories: a Teensy build writes a MoonLight.bin too, an image of another format. + const std::string dir = e.path().filename().string(); + if (dir.rfind("esp32-", 0) != 0 && dir.rfind("moonbase-", 0) != 0) continue; for (const auto& [file, project] : {std::pair{"MoonLight-moonbase.bin", "MoonLight-moonbase"}, std::pair{"MoonLight.bin", "MoonLight"}}) { diff --git a/test/unit/core/unit_ImprovOpReassembler.cpp b/test/unit/core/unit_ImprovOpReassembler.cpp index f996015fc..d3ed42631 100644 --- a/test/unit/core/unit_ImprovOpReassembler.cpp +++ b/test/unit/core/unit_ImprovOpReassembler.cpp @@ -6,6 +6,7 @@ #include "doctest.h" #include "core/util/ImprovOpReassembler.h" +#include #include #include @@ -110,3 +111,37 @@ TEST_CASE("reset() drops a partial op") { CHECK(feedStr(r, 1, true, "x") == R::Error); CHECK(feedStr(r, 0, true, "ok") == R::Ready); } + +// One APPLY_OP payload as the wire carries it: [0xFC][seq][last][chunk bytes]. +static ImprovApplyOpReply applyStr(ImprovOpReassembler& r, std::atomic& ready, uint8_t seq, uint8_t last, const std::string& s) { + std::string p; + p += static_cast(kImprovCmdApplyOp); + p += static_cast(seq); + p += static_cast(last); + p += s; + return improvApplyOp(r, reinterpret_cast(p.data()), p.size(), ready); +} + +TEST_CASE("an APPLY_OP document is acknowledged chunk by chunk and marked ready on its last chunk, on every transport alike") { + char buf[128]; + ImprovOpReassembler r(buf, sizeof(buf)); + std::atomic ready{false}; + CHECK(applyStr(r, ready, 0, 0, "{\"System\":") == ImprovApplyOpReply::Ack); + CHECK_FALSE(ready.load()); + CHECK(applyStr(r, ready, 1, 1, "{}}") == ImprovApplyOpReply::Ack); + CHECK(ready.load()); + CHECK(std::string(r.out()) == "{\"System\":{}}"); +} + +TEST_CASE("an APPLY_OP chunk is refused while the previous document waits, or when malformed, so nothing overwrites an unapplied document") { + char buf[128]; + ImprovOpReassembler r(buf, sizeof(buf)); + std::atomic ready{true}; + CHECK(applyStr(r, ready, 0, 1, "{}") == ImprovApplyOpReply::Refused); // busy + ready = false; + CHECK(applyStr(r, ready, 0, 2, "{}") == ImprovApplyOpReply::Refused); // `last` is 0 or 1 + const uint8_t shortFrame[] = {kImprovCmdApplyOp, 0}; + CHECK(improvApplyOp(r, shortFrame, sizeof(shortFrame), ready) == ImprovApplyOpReply::Refused); + CHECK(applyStr(r, ready, 3, 1, "{}") == ImprovApplyOpReply::Refused); // out of order + CHECK_FALSE(ready.load()); +} diff --git a/test/unit/core/unit_MoonCloudModule.cpp b/test/unit/core/unit_MoonCloudModule.cpp new file mode 100644 index 000000000..f01441acd --- /dev/null +++ b/test/unit/core/unit_MoonCloudModule.cpp @@ -0,0 +1,67 @@ +/// @module MoonCloudModule + +/// The browser relay: a board without HTTPS hands its one MoonCloud request to the open page through a hidden outbox control. + +#include "doctest.h" +#include "core/system/MoonCloudModule.h" + +#include +#include + +namespace { +const mm::ControlDescriptor* control(const mm::MoonModule& m, const char* name) { + const auto& cs = m.controls(); + for (uint8_t i = 0; i < cs.count(); i++) + if (cs[i].name && std::strcmp(cs[i].name, name) == 0) return &cs[i]; + return nullptr; +} +std::string outbox(const mm::MoonModule& m) { + const auto* c = control(m, "outbox"); + return c ? static_cast(c->ptr) : ""; +} +} // namespace + +TEST_CASE("the relay's controls exist only on a board that relays, hidden and never saved") { + mm::MoonCloudModule plain(false); + plain.defineControls(); + CHECK(control(plain, "outbox") == nullptr); + CHECK(control(plain, "outbox sent") == nullptr); + + mm::MoonCloudModule relay(true); + relay.defineControls(); + const auto* box = control(relay, "outbox"); + REQUIRE(box != nullptr); + CHECK(box->hidden); + CHECK(box->readonly); // read-only is what keeps a transient request out of the saved settings + const auto* sent = control(relay, "outbox sent"); + REQUIRE(sent != nullptr); + CHECK(sent->hidden); +} + +TEST_CASE("a post waits in the outbox as its path and body, and the next is refused until the page empties it") { + mm::MoonCloudModule cloud(true); + cloud.defineControls(); + CHECK(cloud.canSend()); + CHECK(cloud.relaysThroughBrowser()); + CHECK(outbox(cloud).empty()); + + CHECK(cloud.post("/api/report", "{\"event\":\"install\"}")); + CHECK(outbox(cloud) == "{\"path\":\"/api/report\",\"body\":{\"event\":\"install\"}}"); + + CHECK_FALSE(cloud.post("/api/talk", "{\"text\":\"hi\"}")); // one slot: the report still waits + CHECK(outbox(cloud) == "{\"path\":\"/api/report\",\"body\":{\"event\":\"install\"}}"); + + CHECK(cloud.outboxWaiting()); + cloud.onControlChanged("outbox sent"); + CHECK(outbox(cloud).empty()); + CHECK_FALSE(cloud.outboxWaiting()); + CHECK(cloud.post("/api/talk", "{\"text\":\"hi\"}")); +} + +TEST_CASE("a request too large for the outbox is refused rather than truncated") { + mm::MoonCloudModule cloud(true); + cloud.defineControls(); + const std::string body = "{\"text\":\"" + std::string(4096, 'x') + "\"}"; + CHECK_FALSE(cloud.post("/api/talk", body.c_str())); + CHECK(outbox(cloud).empty()); +} diff --git a/test/unit/core/unit_MoonStatsModule.cpp b/test/unit/core/unit_MoonStatsModule.cpp index 2eb237a44..e49309e1d 100644 --- a/test/unit/core/unit_MoonStatsModule.cpp +++ b/test/unit/core/unit_MoonStatsModule.cpp @@ -2,6 +2,7 @@ #include "doctest.h" #include "core/system/FilesystemModule.h" +#include "core/system/MoonCloudModule.h" #include "core/system/MoonStatsModule.h" #include "core/module/Scheduler.h" @@ -316,3 +317,30 @@ TEST_CASE("the consent explanation is a status that follows the answer") { stats.setConsent(false); REQUIRE(stats.status() != nullptr); // and it comes back if they change their mind } + +/// On a board without HTTPS the report waits in MoonCloud's outbox for the open page, and counts as sent only once the page has delivered it. +/// So a reboot before any page opens sends it again rather than losing it. +TEST_CASE("a report handed to the page counts as sent only once the page delivers it") { + mm::Scheduler scheduler; + auto* cloud = new mm::MoonCloudModule(true); // the relay a board without HTTPS gets + cloud->setName("MoonCloud"); + auto* stats = new mm::MoonStatsModule(); + stats->setName("Stats"); + cloud->addChild(stats); + scheduler.addModule(cloud); + scheduler.setup(); + cloud->defineControls(); + + stats->setConsent(true); + CHECK(stats->reportDue()); + stats->onControlChanged("send update"); + REQUIRE(cloud->outboxWaiting()); + + stats->tick1s(); // handed over but not delivered: still due, nothing else queued + CHECK(stats->reportDue()); + CHECK(cloud->outboxWaiting()); + + cloud->onControlChanged("outbox sent"); // the page delivered it + stats->tick1s(); + CHECK_FALSE(stats->reportDue()); +} From 396a11102e89c69331d258f6fdeb419c411f8c2d Mon Sep 17 00:00:00 2001 From: ewowi Date: Mon, 12 Oct 2026 16:09:08 +0200 Subject: [PATCH 06/12] Add RGB and Coord3D to MoonLive, with conditions and arithmetic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MoonLive scripts gain RGB and Coord3D values, conditions joined by &&, || and !, and abs, min, max and clamp; a light's color and a palette color read back as one RGB through getRGB and paletteRGB. Switching a scripted effect between scripts no longer carries the last script's buffers, so a large script compiles on a classic ESP32 after any other, and the script compiler takes less flash than before. KPI: 256lights | Desktop:2257KB | tick:7/13/7/2/8/7/268/7/7/10us(FPS:142857/76923/142857/500000/125000/142857/3731/142857/142857/100000) | ESP32:2062KB | src:319(81367) | test:235(53505) | functions-over:348 Commit: src +978/-300 lines | test +722/-126 lines | code findings 1613 -> 1631 (complex function +12, deeply nested +2, duplicated block +2, long function +2) | duplicated lines 3687 -> 3690 (+3) Flash: classic -9.1 KB, S3 -7.7 KB, P4 rev3 -4.6 KB. The P4 rev1 +47.8 KB compares against a committed figure from an Oct 6 build, so it spans earlier commits. The code-findings rise is the new parser paths (conditions, struct values, struct arguments, negation, the struct lookahead), each a branching recursive-descent step; parsePrimary was split into named steps (59 -> 54), and splitting further would only satisfy the counter. Core - MoonLive compiler: RGB and Coord3D as host-registered structs, expanded into scalar IR: members, locals, parameters, returns, field access, whole-struct assignment, the constructor, struct arguments filling builtin positions, struct returns through argument slots and the script-argument block. - Conditions: &&, || and ! as short-circuit jumping code; ! binds as in C, and a comparison after a negated value is refused; a condition operator used as a value gets its own error. - abs, min, max, clamp as common builtins on whole and fixed values; script function parameters 4 -> 8 words; a function named after a builtin is refused. - IR labels 40 -> 96, assembler labels and fixups 96 -> 255, a fixup 4 bytes; the IR array grows when a script outgrows the token estimate; struct types stored as a one-byte index, so the compile stack is unchanged. - MoonLiveCompiler.cpp built with -Os on ESP32: cold code, the emitted script code unchanged. - Xtensa shrImm encodes 16..31 through extui, so a byte local (byte b = x + 1;) compiles on ESP32. - The define hook tells a binding a compile is starting; MoonCloud acknowledges an outbox request by its id. Light domain - getRGB and paletteRGB replace getR/G/B and paletteR/G/B. - MoonLiveEffect frees its trail planes and particle pool before a recompile. - Drivers marks the scripted-palette list scanned inside the scan itself, which made one test depend on the order tests ran. - breathe, chase, ember, glow, pulse, sparkle and spectrum rewritten with the new forms. UI - The MoonCloud relay pauses after a failed acknowledgement; a browser install rewrites only MoonLight's GitHub release URLs to the Pages mirror, and cancel aborts both the download and the upload. - Installer: the Teensy recovery link follows every canceled pick, and after a reboot the installer reads only the installed Teensy, never another granted one. Scripts/MoonDeck - check_devices reports a non-string chip instead of raising. Tests - An IR golden table: every shipped script compiles to the same IR, so only the seven rewritten scripts moved. - Structs, conditions and the builtins run on the host; every device backend compiles them; refusals pinned; hpwit's loop-variable case; a script dense in struct copies; Xtensa extui against the toolchain's bytes; buffer release on a script switch and a failed compile; the MoonCloud id cases; the Teensy port exclusion; the scripts-are-C++ prelude generates the structs and their overloads. - The MoonStats consent test expects the warning on a build without HTTPS, which failed the sanitizer jobs. Docs/CI - The MoonLive language page, MIGRATING (the removed calls and reserved names), the roadmap's shipped items, the plan, building.md's repo links and its stale ships sentence, the Teensy plan's driver line, and the P4 WiFi regression on IDF v6.1 in the backlog. Reviews - ๐Ÿ‡ building.md links resolve from the repo root -> done, all 13 in the file. - ๐Ÿ‡ roadmap builtin limit 64 -> done, it was item 1, now 96. - ๐Ÿ‡ Teensy plan driver selection -> done. - ๐Ÿ‡ check_devices chip type -> done. - ๐Ÿ‡ install.js recovery link guard -> done. - ๐Ÿ‡ teensy-writer port by USB ids only -> done, other boards' ports excluded, with a test. - ๐Ÿ‡ MoonCloud acknowledgement by id -> done, with tests. - ๐Ÿ‡ MoonStats consent test -> done; it was also the sanitizer failure. - ๐Ÿ‡ OTA comment "not yet copied" -> done. - ๐Ÿ‡ relay acknowledgement failure cooldown -> done. - ๐Ÿ‡ release-asset rewrite host check -> done. - ๐Ÿ‡ cancel aborts download and upload -> done. - ๐Ÿ‘พ lookahead looped forever on a lexer error inside a call -> done, with a test. - ๐Ÿ‘พ a nested same-type call clobbered the outer argument type -> done, with tests for the value and the refusal. - ๐Ÿ‘พ ! bound looser than a comparison -> done, C precedence, with a test. - ๐Ÿ‘พ a struct member writable by index -> done, refused. - ๐Ÿ‘พ a test title claimed an order it cannot see -> done, retitled. - ๐Ÿ‘พ nits (spelling, em-dashes on edited lines, a stale test title, the duplicate test fixture, label counts, the docs example's palette index, the plan's Risk wording, an addStruct guard) -> done. - ๐Ÿ‘พ four identical Fixup tables across the assemblers -> deferred: duplication that predates this change, a shared type in core is its own change. Co-Authored-By: Claude Opus 5.5 --- docs/how-to/building.md | 21 +- docs/moonmodules/light/moonlive.md | 39 +- docs/reference/MIGRATING.md | 11 + docs/reference/integrating.md | 2 +- docs/reference/metrics/code.md | 59 +- docs/reference/metrics/docgen.md | 28 +- docs/reference/metrics/prose.md | 12 +- docs/reference/metrics/repo-health.json | 66 +- docs/reference/metrics/repo-health.md | 73 +- docs/work/future/backlog-core.md | 3 +- docs/work/future/input-mapping-analysis.md | 2 +- docs/work/future/moonlive-language-roadmap.md | 67 +- ... mapping and scripted sensors (partial).md | 2 +- .../present/Plan-20261010 - Teensy 4.1.md | 2 +- ...ayer compositing, masks and transitions.md | 2 +- ...261011 - MoonLive Coord3D and RGB types.md | 33 +- esp32/main/CMakeLists.txt | 3 + moondeck/check/check_devices.py | 3 +- mooninstaller/install-orchestrator.js | 5 +- mooninstaller/install.js | 3 +- mooninstaller/teensy-writer.js | 13 +- moonlive/effects/breathe.mle | 2 +- moonlive/effects/chase.mle | 2 +- moonlive/effects/ember.mle | 5 +- moonlive/effects/glow.mle | 10 +- moonlive/effects/pulse.mle | 9 +- moonlive/effects/sparkle.mle | 5 +- moonlive/effects/spectrum.mle | 16 +- src/core/moonlive/MoonLiveBuiltins.h | 48 +- src/core/moonlive/MoonLiveBuiltins_common.h | 43 +- src/core/moonlive/MoonLiveCompiler.cpp | 960 ++++++++++++++---- src/core/moonlive/MoonLiveCompiler.h | 2 +- src/core/moonlive/MoonLiveIr.h | 32 +- src/core/moonlive/MoonLiveScript.h | 2 + src/core/moonlive/MoonLiveSpill.cpp | 10 +- src/core/moonlive/moonlive_lower.h | 7 +- src/core/system/MoonCloudModule.h | 24 +- src/light/drivers/Drivers.h | 2 +- src/light/moonlive/MoonLiveBuiltins_light.h | 54 +- src/light/moonlive/MoonLiveEffect.h | 15 +- src/platform/desktop/moonlive_asm_host.h | 6 +- src/platform/esp32/moonlive_asm_riscv.cpp | 4 +- src/platform/esp32/moonlive_asm_riscv.h | 2 +- src/platform/esp32/moonlive_asm_xtensa.cpp | 14 +- src/platform/esp32/moonlive_asm_xtensa.h | 2 +- src/platform/teensy/moonlive_asm_thumb2.cpp | 4 +- src/platform/teensy/moonlive_asm_thumb2.h | 2 +- src/platform/teensy/platform_teensy_ota.cpp | 2 +- src/ui/app.js | 43 +- test/CMakeLists.txt | 2 + test/js/installer-teensy-writer.test.mjs | 21 + test/python/test_scripts_are_cpp.py | 38 +- ...lette_preset_changes_only_the_palette.json | 6 +- ..._Palettes_a_script_drives_the_effects.json | 8 +- test/unit/core/moonlive_device_codegen.inc | 68 +- test/unit/core/unit_MoonCloudModule.cpp | 42 +- test/unit/core/unit_MoonStatsModule.cpp | 9 +- .../core/unit_moonlive_codegen_xtensa.cpp | 20 + test/unit/core/unit_moonlive_compiler.cpp | 76 +- test/unit/light/MoonLiveEffectScene.h | 45 + test/unit/light/moonlive_ir_golden.inc | 64 ++ test/unit/light/unit_MoonLiveDrawing.cpp | 50 +- test/unit/light/unit_MoonLiveParticles.cpp | 65 +- .../light/unit_MoonLiveScripts_ir_golden.cpp | 123 +++ test/unit/light/unit_MoonLiveStructs.cpp | 211 ++++ 65 files changed, 1958 insertions(+), 666 deletions(-) create mode 100644 test/unit/light/MoonLiveEffectScene.h create mode 100644 test/unit/light/moonlive_ir_golden.inc create mode 100644 test/unit/light/unit_MoonLiveScripts_ir_golden.cpp create mode 100644 test/unit/light/unit_MoonLiveStructs.cpp diff --git a/docs/how-to/building.md b/docs/how-to/building.md index c498cce5f..7e6fb327e 100644 --- a/docs/how-to/building.md +++ b/docs/how-to/building.md @@ -4,7 +4,7 @@ How to get the system running on a desktop, an ESP32, a Teensy, or a Raspberry P ## MoonDeck โ€” the dev console -Everything that builds, flashes, runs, tests, monitors, or checks the project โ€” for every target โ€” lives as a script under `moondeck/`. The full per-script reference is [moondeck/MoonDeck.md](../moondeck/MoonDeck.md). +Everything that builds, flashes, runs, tests, monitors, or checks the project, for every target, lives as a script under `moondeck/`. The full per-script reference is [moondeck/MoonDeck.md](../../moondeck/MoonDeck.md). @@ -57,9 +57,10 @@ The distinction matters because a shipped binary is launched from a download fol ### Editor setup (clangd) -Diagnostics appear **as you type**, from the same [`.clang-tidy`](../.clang-tidy) config CI uses, so a finding shows up while the code is still in your head, not ten minutes later in a pipeline. +Diagnostics appear **as you type**, from the same [`.clang-tidy`](../../.clang-tidy) config CI uses. +A finding shows up while the code is fresh in your head, not ten minutes later in a pipeline. -Once per machine: install the **clangd** extension (`llvm-vs-code-extensions.vscode-clangd`) and **disable Microsoft's C/C++ IntelliSense**, running both produces duplicated and contradictory diagnostics. Nothing else to configure: [`.clangd`](../.clangd) at the repo root points at the compilation database, and `CMAKE_EXPORT_COMPILE_COMMANDS` (set in `CMakeLists.txt`) means any normal build refreshes it. +Once per machine: install the **clangd** extension (`llvm-vs-code-extensions.vscode-clangd`) and **disable Microsoft's C/C++ IntelliSense**, running both produces duplicated and contradictory diagnostics. Nothing else to configure: [`.clangd`](../../.clangd) at the repo root points at the compilation database, and `CMAKE_EXPORT_COMPILE_COMMANDS` (set in `CMakeLists.txt`) means any normal build refreshes it. Two things worth knowing: @@ -255,7 +256,7 @@ Moving to a different release is never automatic: bump `PINNED_IDF_COMMIT` / `PI #### Adopting the v6.x ecosystem changes -v6.0 introduced ecosystem-level changes beyond the API surface. The stance, under [ยง Principles โ†’ Industry standards](../CLAUDE.md#principles), is to **embrace these as the ESP32 standard**. If the IDF makes something the recognized way to build, install, provision, or ship, that is the path we want rather than a bespoke one we maintain alone. We adopt them **step by step**, each its own commit and hardware re-test, rather than all at once, and only after they clear the **v6.0-floor rule** above. The default is *yes, adopt*, with the burden of argument on *why not*. +v6.0 introduced ecosystem-level changes beyond the API surface. The stance, under [ยง Principles โ†’ Industry standards](../../CLAUDE.md#principles), is to **embrace these as the ESP32 standard**. If the IDF makes something the recognized way to build, install, provision, or ship, that is the path we want rather than a bespoke one we maintain alone. We adopt them **step by step**, each its own commit and hardware re-test, rather than all at once, and only after they clear the **v6.0-floor rule** above. The default is *yes, adopt*, with the burden of argument on *why not*. Two guardrails bound the "embrace everything" stance: @@ -267,10 +268,10 @@ Where we are on each. The adoption plan and per-item triggers are filed in [back | Change | Where we are now | |---|---| | **EIM** (ESP-IDF Installation Manager) โ€” the new default, cross-platform installer; Espressif says `install.sh` / `idf_tools.py` are "no longer needed" | `setup_esp_idf.py` drives the legacy `install.sh` / `install.bat`. Works, but is the *old* documented path. | -| **PSA Crypto** โ€” legacy mbedTLS crypto APIs deprecated in favour of the PSA API | No direct exposure: we never call mbedTLS ourselves; OTA uses `esp_https_ota` + `esp_crt_bundle_attach` ([platform_esp32_ota.cpp](../src/platform/esp32/platform_esp32_ota.cpp)), which wrap crypto internally. | -| **`network_provisioning`** โ€” Espressif's Unified Provisioning subsystem, renamed from `wifi_provisioning` in v6.0. Transports: **BLE (GATT)** + **Wi-Fi SoftAP**. Clients: official iOS/Android apps for both, plus `esp_prov` (a Python CLI on Linux/macOS/Windows). Transport-agnostic but ships no web/serial client. | We provision over [Improv](../src/core/system/ImprovProvisioningModule.h) โ€” serial (USB) + BLE, driven from the **browser** (ESP Web Tools) or a serial CLI, which covers mooninstaller / no-app onboarding. The IDF-native **phone-app + SoftAP** flow is a coverage gap rather than a duplicate: the two standards meet only on BLE and own different front-ends. | +| **PSA Crypto**: legacy mbedTLS crypto APIs deprecated in favor of the PSA API | No direct exposure: we never call mbedTLS ourselves; OTA uses `esp_https_ota` + `esp_crt_bundle_attach` ([platform_esp32_ota.cpp](../../src/platform/esp32/platform_esp32_ota.cpp)), which wrap crypto internally. | +| **`network_provisioning`**: Espressif's Unified Provisioning subsystem, renamed from `wifi_provisioning` in v6.0. Transports: **BLE (GATT)** + **Wi-Fi SoftAP**. Clients: official iOS/Android apps for both, plus `esp_prov` (a Python CLI on Linux/macOS/Windows). Transport-agnostic but ships no web/serial client. | We provision over [Improv](../../src/core/system/ImprovProvisioningModule.h): serial (USB) + BLE, driven from the **browser** (ESP Web Tools) or a serial CLI, which covers mooninstaller / no-app onboarding. The IDF-native **phone-app + SoftAP** flow is a coverage gap rather than a duplicate: the two standards meet only on BLE and own different front-ends. | | **CMake Build System v2** โ€” the named successor to the current build system; technical preview in v6.0/6.1, has its own migration guide | Standard `idf.py` build (v1). Our component is a thin `idf_component_register()` wrapper, so the migration surface is small. | -| **Built-in MCP server** (`idf.py mcp-server`) โ€” lets an AI assistant drive build/flash/monitor/debug directly | Not used. Agents and humans both go through the `moondeck//*.py` layer (the uniform interface in [moondeck/MoonDeck.md](../moondeck/MoonDeck.md)), which wraps pin-drift checks, per-firmware build dirs, and KPI collection. | +| **Built-in MCP server** (`idf.py mcp-server`): lets an AI assistant drive build/flash/monitor/debug directly | Not used. Agents and humans both go through the `moondeck//*.py` layer (the uniform interface in [moondeck/MoonDeck.md](../../moondeck/MoonDeck.md)), which wraps pin-drift checks, per-firmware build dirs, and KPI collection. | The general rule: **anything already in v6.0 we adopt proactively** (it clears the floor), while **preview / not-yet-in-v6.0 features wait** until they are stable *and* in our floor. Each adoption is its own commit with its own hardware re-test, and none may regress the Teensy / desktop paths. @@ -278,7 +279,7 @@ The general rule: **anything already in v6.0 we adopt proactively** (it clears t `build_esp32.py --firmware` selects one of the shipping variants. The key combines chip name + feature flags + (for SKU-sensitive chips) module. ("Firmware" here is the compiled binary; the physical product (deviceModel) is a separate concept: see [MoonInstaller, firmware vs deviceModel vs board](../explanation/architecture/mooninstaller.md#the-three-words).) `build_esp32.py --help` lists the full set. -The canonical list is the **firmware registry**, [`moondeck/build/firmwares.py`](../moondeck/build/firmwares.py). It joins the `FIRMWARES` dicts in [`build_esp32.py`](../moondeck/build/build_esp32.py) and [`build_teensy.py`](../moondeck/build/build_teensy.py) and tags every row with its `platform`. Each slice keeps its own build details: sdkconfig `fragments` on an ESP32, the Ethernet driver on a Teensy. Its machine-readable projection is [`mooninstaller/firmwares.json`](../mooninstaller/firmwares.json), generated by `generate_firmwares.py` and drift-guarded by `check_firmwares.py`. The CI release matrix, the manifest loops, the Teensy builds and MoonDeck all read it, so the list lives in exactly one place. `esp32p4rev1-eth-wifi` has `ships: false` (its C6-slave Kconfig isn't reproducible in CI yet), so it builds from the CLI but stays out of the release matrix. +The canonical list is the **firmware registry**, [`moondeck/build/firmwares.py`](../../moondeck/build/firmwares.py). It joins the `FIRMWARES` dicts in [`build_esp32.py`](../../moondeck/build/build_esp32.py) and [`build_teensy.py`](../../moondeck/build/build_teensy.py) and tags every row with its `platform`. Each slice keeps its own build details: sdkconfig `fragments` on an ESP32, the Ethernet driver on a Teensy. Its machine-readable projection is [`mooninstaller/firmwares.json`](../../mooninstaller/firmwares.json), generated by `generate_firmwares.py` and drift-guarded by `check_firmwares.py`. The CI release matrix, the manifest loops, the Teensy builds and MoonDeck all read it, so the list lives in exactly one place. A row with `ships: false` builds from the CLI but stays out of the release matrix. ESP-IDF v6.x has no `CONFIG_ESP_WIFI_ENABLED` switch (the symbol is forced on for WiFi-capable SoCs), so dropping WiFi at compile time happens via `EXCLUDE_COMPONENTS` plus `MM_NO_WIFI` (set when `MM_ETH_ONLY=1`, applied in `esp32/main/CMakeLists.txt`). The `esp32-eth` variant takes this path; the default `esp32` keeps both stacks compiled in and uses the runtime cascade in `NetworkModule` (Ethernet first, WiFi fallback when no PHY responds). @@ -308,7 +309,7 @@ curl --http1.1 -H "Expect:" --data-binary @build/esp32-/.bin \ http:///api/firmware/upload ``` -`.bin` is the app image in that build directory; [flash_esp32](../moondeck/MoonDeck.md#flash_esp32) names it. +`.bin` is the app image in that build directory; [flash_esp32](../../moondeck/MoonDeck.md#flash_esp32) names it. The second request ends with **no HTTP status** (curl reports 000): the device reboots into the new image as the write completes, so the socket closes before a response arrives. That is success, not failure. Confirm by reading the build back: @@ -363,4 +364,4 @@ Both are defined in the root `CMakeLists.txt` (desktop) and `esp32/main/CMakeLis The system serves the web UI from its embedded HTTP server. Open `http:///` in a browser; on desktop that's typically `http://localhost:8080/`. From the UI you can change effects and modifiers, configure controls, see the 3D preview. The settings persist across reboot. -To run the test suite or any of the checks (platform boundary, specs, KPI), see the MoonDeck reference linked above. The release-readiness gates that wrap these into a checklist live in [CLAUDE.md ยง The Process](../CLAUDE.md#the-process). +To run the test suite or any of the checks (platform boundary, specs, KPI), see the MoonDeck reference linked above. The release-readiness gates that wrap these into a checklist live in [CLAUDE.md ยง The Process](../../CLAUDE.md#the-process). diff --git a/docs/moonmodules/light/moonlive.md b/docs/moonmodules/light/moonlive.md index b49c2905e..d5a3d9acd 100644 --- a/docs/moonmodules/light/moonlive.md +++ b/docs/moonmodules/light/moonlive.md @@ -26,6 +26,8 @@ class CrosshairEffect { ``` These are real calls, not pasted-in text: the callee gets its own frame when it runs, which is what lets one helper call another and lets a function recurse. Arguments are passed BY VALUE, so a function that writes a parameter changes its own copy and the caller's variable is untouched. +A function takes up to eight words of parameters, an `RGB` or `Coord3D` taking one per field. +A function's name may not be a builtin's, since the builtin is found first and the function would never run. `effects/crosshair.mle` is the worked example; `layouts/sixteen-rings.mll` is the one where the arguments earn their place, calling one helper sixteen times with different coordinates. ### A script is C++ @@ -41,7 +43,7 @@ Anything else a compiler rejects is a divergence, and the test is where it surfa ### Every function declares what it returns A script does this the way the compiled module it stands in for does. -`void` acts and answers nothing, `int` hands back a number, and `string` hands back one of the script's own literals. +`void` acts and answers nothing, `int` hands back a number, `string` hands back one of the script's own literals, and `RGB` or `Coord3D` hands back a whole [struct](#rgb-and-coord3d). `return` leaves a function, with a value or without one. Inside `tick()` a bare `return;` is an early exit, which is what a guard wants: @@ -76,6 +78,10 @@ if (heat[i] > 40) { setRGB(i, 255, 90, 0); } else { setRGB(i, 0, 0, 0); } ``` +Conditions join with `&&` and `||` and negate with `!`, as in C++: `&&` binds tighter than `||`, parentheses group, and evaluation stops at the first side that decides the answer, so `if (i > 0 && heat[i - 1] > 40)` never reads before the array. +A bare value is true when it is not zero, so `if (audioBeat())` reads as it says. +A condition decides a branch rather than being a value, so `int both = a && b;` is refused. + ### Wider members, and arrays `byte` spans 0..255, and `int` spans @@ -141,6 +147,32 @@ That is where width still earns its keep: a `byte[]` heat map costs a quarter of An initializer is range-checked against its type, so `byte n = 300;` is a compile error naming the member rather than a silent 44. A `string` array is refused, since there is no runtime string to fill one with. +## RGB and Coord3D + +A color and a position travel as one value, under the names the compiled modules use for them. +`RGB` is three `byte` fields, `r`, `g` and `b`, and `Coord3D` is three `int` fields, `x`, `y` and `z`. + +```c +class Warm { + RGB last; // a member starts at zero + + RGB toward(RGB c, int k) { return RGB(c.r, c.g / k, 0); } + + void tick() { + RGB c = paletteRGB(mod(t / 16, 256), 255); // one call for the whole color + last = toward(c, 2); + for (int i = 0; i < width; i = i + 1) { setRGB(i, last); } + setRGB(0, 255, getRGB(1).g, 0); // one field of a struct a call answers + } +} +``` + +A struct is a member, a local, a parameter and a return value, read and written by field or as a whole. +`RGB(r, g, b)` builds one, and a local declared without a value starts at zero. +Passed to a call, a struct fills one position per field, so `setRGB(i, c)` is `setRGB(i, c.r, c.g, c.b)` and every builtin taking three channels takes an `RGB`. +The compiler expands every use into the operations a script would write field by field, so a struct costs nothing a script written without one does not. +A member `RGB` takes 3 bytes of the class's member budget and a `Coord3D` 12; a local takes one frame slot per field. + ## Fractional arithmetic with `fixed` `fixed` is Q16.16: the number is stored scaled by 65,536, which is how every coordinate in the engine works. @@ -189,7 +221,7 @@ The core owns the grammar and a generic call mechanism, and nothing else. | call | does | |---|---| | `setRGB(index, r, g, b)` | write one light | -| `getR(index)`, `getG`, `getB` | read one channel of a light, what earlier effects in the layer drew | +| `getRGB(index)` | read a light's color as an `RGB`, what earlier effects in the layer drew | | `mulRGB(index, k)` | scale one light by `k` out of 255, the dimming a mask applies | | `addRGB(index, r, g, b)` | add a color to one light, saturating at 255 | | `blendRGB(index, r, g, b, amount)` | mix one light toward a color by `amount` out of 255 | @@ -200,6 +232,7 @@ The core owns the grammar and a generic call mechanism, and nothing else. | `circle(cx, cy, radius, thickness, r, g, b)` | a circle outline of a given stroke width, via the shared `draw::ring` | | `random16(n)` | a value in `[0, n)` | | `mod(a, b)` | the wrap a cyclic animation needs | +| `abs(v)`, `min(a, b)`, `max(a, b)`, `clamp(v, lo, hi)` | magnitude, the smaller, the larger, and a value held between bounds; on whole or `fixed` values, the result following them | | `beat(bpm, t)` | a `0..65535` sawtooth at `bpm` | | `beatsin(bpm, t, high)` | a sine `0..high` at `bpm` | | `noise(x, y, z)` | `0..255` gradient noise, the field behind fire, clouds and plasma | @@ -221,7 +254,7 @@ The core owns the grammar and a generic call mechanism, and nothing else. | `escape(cx, cy, jx, jy, iters)` | the Mandelbrot escape count, `0..255`, `0` inside the set | | `setPaletteColor(x, y, index, bri)` | one light from the active palette, in one call | | `setPaletteColorZ(x, y, z, index, bri)` | the same, addressing a light in a volume | -| `paletteR(i, bri)`, `paletteG`, `paletteB` | one palette channel, for a value rather than a pixel | +| `paletteRGB(i, bri)` | a palette color at a brightness, as an `RGB`, for a value rather than a pixel | | `trail(1)` | ask for a trail plane, from `defineControls()`. Returns whether it got one | | `flowNoise(zoom, strength)`, `flowCurl(zoom, strength)` | carry the trail plane one frame along a flow | | `trailDecay(halfLifeMs)` | fade the trail by a half-life, so a tail holds its length at any framerate | diff --git a/docs/reference/MIGRATING.md b/docs/reference/MIGRATING.md index 6be9345f3..5942a0adf 100644 --- a/docs/reference/MIGRATING.md +++ b/docs/reference/MIGRATING.md @@ -30,6 +30,17 @@ For a device still on old firmware (no Backup button yet), the [installer page]( ## Unreleased +### MoonLive scripts: colors read as one RGB, and struct, arithmetic and builtin names are reserved + +**Action: *update a file*, only for a user script that uses one of these names or calls.** + +`getR`, `getG` and `getB` are gone: `getRGB(i)` answers the whole color, and `getRGB(i).r` one channel. +`paletteR`, `paletteG` and `paletteB` are gone the same way: `paletteRGB(i, bri)`, and `paletteRGB(i, bri).r` for one channel. +The shipped effects use the new calls. + +`RGB` and `Coord3D` are types, and `abs`, `min`, `max`, `clamp`, `getRGB` and `paletteRGB` are builtins, so a script variable, member or function of one of those names stops compiling, with the reason as its error. +A script function named after any builtin is refused too: the builtin was found first, so the function never ran. Rename it. + ### The access point carries the first known network's password, under an anonymous name **Action: *nothing*, unless someone joins the access point with a password of its own: they use the home WiFi's password instead.** diff --git a/docs/reference/integrating.md b/docs/reference/integrating.md index ad40fcec7..f515225e1 100644 --- a/docs/reference/integrating.md +++ b/docs/reference/integrating.md @@ -128,7 +128,7 @@ curl -X POST "http:///api/file?path=/moonlive/hello.mle" --data-binary ' class HelloEffect { byte hue = 0; void defineControls() { addControl("hue", hue, 0, 255); } - void tick() { fill(paletteR(hue, 255), paletteG(hue, 255), paletteB(hue, 255)); } + void tick() { fill(paletteRGB(hue, 255)); } }' # Read it back. diff --git a/docs/reference/metrics/code.md b/docs/reference/metrics/code.md index bc6296b91..2ee5b33cd 100644 --- a/docs/reference/metrics/code.md +++ b/docs/reference/metrics/code.md @@ -4,18 +4,18 @@ Generated by [`moondeck/check/check_code.py`](../../../moondeck/check/check_code Every function, block and file over a limit the [coding standards](../../contributing/coding-standards.md#static-checks) set. Current state only: the trend is this file's git history. The list only shrinks, per rule and in total, and a touched file clears its own rows. -**1613 finding(s)** across 321 file(s). 2.77% of all lines are duplicated, 3687 lines. +**1631 finding(s)** across 322 file(s). 2.76% of all lines are duplicated, 3690 lines. ## By rule | Rule | Findings | |---|---:| -| duplicated block | 760 | -| complex function | 284 | +| duplicated block | 762 | +| complex function | 296 | | blocking call on the render path | 190 | -| deeply nested | 113 | +| deeply nested | 115 | | module type in the UI | 72 | -| long function | 49 | +| long function | 51 | | large stack frame | 35 | | long parameter list | 35 | | raw allocation in light | 28 | @@ -44,10 +44,10 @@ Each rule is a code smell with a named move from the refactoring catalog, the ta | Area | Findings | |---|---:| -| `test` | 562 | -| `light` | 421 | -| `core` | 350 | -| `platform` | 174 | +| `test` | 560 | +| `light` | 422 | +| `core` | 365 | +| `platform` | 178 | | `ui` | 75 | | `moondeck` | 24 | | `src` | 5 | @@ -61,10 +61,10 @@ The next function worth simplifying is at the top of each file's list. |---|---:|---| | `src/core/system/HttpServerModule.cpp` | 44 | `HttpServerModule::handleConnection`, complex function 121 | | `src/ui/migrate.js` | 43 | `line 29: 'I2cBusModule'`, module type in the UI 1 | +| `src/core/moonlive/MoonLiveCompiler.cpp` | 42 | `moonlive::Parser::parseDecl`, complex function 54 | | `src/platform/desktop/platform_desktop.cpp` | 41 | `platform::spawnEncoderProcess`, complex function 58 | | `src/core/system/MqttModule.cpp` | 31 | `MqttModule::routePublish`, complex function 29 | -| `src/ui/app.js` | 30 | `app.js`, large file 9128 | -| `src/core/moonlive/MoonLiveCompiler.cpp` | 27 | `moonlive::Parser::parseCall`, complex function 59 | +| `src/ui/app.js` | 30 | `app.js`, large file 9143 | | `src/light/drivers/ParallelLedDriver.h` | 27 | `ParallelLedDriver::reinit`, complex function 36 | | `src/light/drivers/PreviewDriver.h` | 27 | `PreviewDriver::buildCoordTable`, complex function 37 | | `test/unit/core/unit_FilesystemModule_persistence.cpp` | 24 | `lines 181-194, also in test/unit/core/unit_FilesystemModule_persistence.cpp:146`, duplicated block 14 | @@ -86,12 +86,12 @@ The next function worth simplifying is at the top of each file's list. | `test/unit/light/unit_PaintBrushEffect.cpp` | 15 | `lines 44-61, also in test/unit/light/unit_GEQ3DEffect.cpp:58`, duplicated block 18 | | `test/unit/light/unit_SolidEffect.cpp` | 15 | `lines 9-22, also in test/unit/light/unit_FixedRectangleEffect.cpp:90`, duplicated block 14 | | `src/core/module/Control.cpp` | 13 | `applyControlValue`, complex function 74 | -| `src/light/layers/Layer.h` | 13 | `Layer in 131 files`, name spread 131 | +| `src/light/layers/Layer.h` | 13 | `Layer in 129 files`, name spread 129 | +| `src/light/moonlive/MoonLiveBuiltins_light.h` | 13 | `moonlive::mm_light_escape`, complex function 13 | | `test/unit/light/unit_HlsDriver.cpp` | 13 | `lines 30-47, also in test/unit/light/unit_PanelCardDriver.cpp:22`, duplicated block 18 | | `test/unit/light/unit_RandomMapModifier.cpp` | 13 | `lines 116-132, also in test/unit/light/unit_BlurzEffect.cpp:49`, duplicated block 17 | | `src/core/system/DevicesModule.h` | 12 | `lines 73-80, also in src/core/system/DevicesModule.h:59`, duplicated block 8 | | `src/light/drivers/HlsDriver.h` | 12 | `lines 105-117, also in src/light/drivers/RtspDriver.h:92`, duplicated block 13 | -| `src/light/moonlive/MoonLiveBuiltins_light.h` | 12 | `moonlive::mm_light_escape`, complex function 13 | | `test/unit/light/unit_RubiksCubeEffect.cpp` | 12 | `lines 60-78, also in test/unit/light/unit_RubiksCubeEffect.cpp:28`, duplicated block 19 | | `test/unit/light/unit_ParlioPeripheral.cpp` | 11 | `lines 197-216, also in test/unit/light/unit_I80Peripheral.cpp:132`, duplicated block 20 | | `test/unit/light/unit_StarFieldEffect.cpp` | 11 | `lines 88-103, also in test/unit/light/unit_StarFieldEffect.cpp:13`, duplicated block 16 | @@ -138,6 +138,7 @@ The next function worth simplifying is at the top of each file's list. | `test/unit/core/unit_ImprovFrame.cpp` | 6 | `lines 179-187, also in test/unit/core/unit_ImprovFrame.cpp:152`, duplicated block 9 | | `test/unit/light/unit_Correction.cpp` | 6 | `lines 280-286, also in test/unit/light/unit_Correction.cpp:237`, duplicated block 7 | | `test/unit/light/unit_MetaballsEffect.cpp` | 6 | `lines 22-37, also in test/unit/light/unit_PlasmaEffect.cpp:24`, duplicated block 16 | +| `test/unit/light/unit_MoonLiveScripts.cpp` | 6 | `lines 55-66, also in test/unit/light/unit_MoonLiveScripts_ir_golden.cpp:74`, duplicated block 12 | | `test/unit/light/unit_MpegTs.cpp` | 6 | `lines 59-67, also in test/unit/light/unit_MpegTs.cpp:29`, duplicated block 9 | | `test/unit/light/unit_ParallelLedDriver_pinexpander.cpp` | 6 | `lines 94-105, also in test/unit/light/unit_ParallelLedDriver_doublebuffer.cpp:93`, duplicated block 12 | | `test/unit/light/unit_Particles.cpp` | 6 | `lines 474-481, also in test/unit/light/unit_Particles.cpp:463`, duplicated block 8 | @@ -155,12 +156,12 @@ The next function worth simplifying is at the top of each file's list. | `src/light/layouts/CarLightsLayout.h` | 5 | `lines 49-65, also in src/light/layouts/TorontoBarGourdsLayout.h:52`, duplicated block 17 | | `src/light/layouts/RingLayout.h` | 5 | `lines 85-100, also in src/light/layouts/CarLightsLayout.h:95`, duplicated block 16 | | `src/light/layouts/TorontoBarGourdsLayout.h` | 5 | `lines 52-68, also in src/light/layouts/CarLightsLayout.h:49`, duplicated block 17 | +| `src/platform/esp32/moonlive_asm_riscv.cpp` | 5 | `lines 297-303, also in src/platform/esp32/moonlive_asm_xtensa.cpp:483`, duplicated block 7 | | `src/platform/esp32/platform_esp32_h264.cpp` | 5 | `platform::hlsSegment`, complex function 26 | | `test/unit/light/unit_Drivers_rendersplit.cpp` | 5 | `(static local variable) in tick`, blocking call on the render path 1 | | `test/unit/light/unit_FireEffect.cpp` | 5 | `lines 29-42, also in test/unit/light/unit_RainbowEffect.cpp:69`, duplicated block 14 | | `test/unit/light/unit_FreqSawsEffect.cpp` | 5 | `lines 19-29, also in test/unit/light/unit_RainbowEffect.cpp:69`, duplicated block 11 | | `test/unit/light/unit_GEQ3DEffect.cpp` | 5 | `lines 58-75, also in test/unit/light/unit_PaintBrushEffect.cpp:44`, duplicated block 18 | -| `test/unit/light/unit_MoonLiveScripts.cpp` | 5 | `lines 287-295, also in test/unit/light/unit_MoonLiveScripts.cpp:259`, duplicated block 9 | | `test/unit/light/unit_RandomEffect.cpp` | 5 | `lines 10-22, also in test/unit/light/unit_RandomMapModifier.cpp:115`, duplicated block 13 | | `src/core/module/Scheduler.cpp` | 4 | `Scheduler::getControlWide`, complex function 18 | | `src/core/moonlive/MoonLive.cpp` | 4 | `moonlive::MoonLive::ensureArena`, complex function 21 | @@ -182,7 +183,6 @@ The next function worth simplifying is at the top of each file's list. | `src/light/util/RtspSession.h` | 4 | `rtsp::parseTransport`, complex function 28 | | `src/main.cpp` | 4 | `mm_main`, complex function 27 | | `src/platform/desktop/main_desktop.cpp` | 4 | `main`, deeply nested 5 | -| `src/platform/esp32/moonlive_asm_riscv.cpp` | 4 | `lines 297-303, also in src/platform/esp32/moonlive_asm_xtensa.cpp:481`, duplicated block 7 | | `test/js/ui-mooncloud.test.mjs` | 4 | `lines 6-17, also in test/js/installer-s31-webflash.test.mjs:11`, duplicated block 12 | | `test/unit/light/unit_BlurzEffect.cpp` | 4 | `lines 137-155, also in test/unit/light/unit_GEQEffect.cpp:159`, duplicated block 19 | | `test/unit/light/unit_BouncingBallsEffect.cpp` | 4 | `lines 75-88, also in test/unit/light/unit_RubiksCubeEffect.cpp:103`, duplicated block 14 | @@ -195,7 +195,7 @@ The next function worth simplifying is at the top of each file's list. | `test/unit/light/unit_PraxisEffect.cpp` | 4 | `lines 9-21, also in test/unit/light/unit_RandomMapModifier.cpp:115`, duplicated block 13 | | `test/unit/light/unit_fluid.cpp` | 4 | `lines 160-169, also in test/unit/light/unit_TrailsEffect.cpp:54`, duplicated block 10 | | `src/core/moonlive/MoonLiveBuiltins_common.h` | 3 | `moonlive::mm_ml_addControl`, complex function 12 | -| `src/core/moonlive/moonlive_lower.h` | 3 | `moonlive::lowerWith`, complex function 84 | +| `src/core/moonlive/moonlive_lower.h` | 3 | `moonlive::lowerWith`, complex function 85 | | `src/core/services/GamepadService.h` | 3 | `lines 108-130, also in src/core/services/InfraredService.h:113`, duplicated block 23 | | `src/core/system/AccessPointModule.h` | 3 | `platform::UdpSocket::recvFrom in tick20ms`, blocking call on the render path 1 | | `src/core/system/PinsModule.h` | 3 | `PinsModule::PinListSource::collect`, deeply nested 5 | @@ -206,14 +206,13 @@ The next function worth simplifying is at the top of each file's list. | `src/light/effects/AuroraEffect.h` | 3 | `lines 132-140, also in src/light/effects/TunnelEffect.h:92`, duplicated block 9 | | `src/light/effects/PongEffect.h` | 3 | `PongEffect::render`, complex function 15 | | `src/light/effects/SolidEffect.h` | 3 | `SolidEffect::static_cast`, complex function 36 | -| `src/light/layouts/GridLayout.h` | 3 | `GridLayout in 87 files`, name spread 87 | +| `src/light/layouts/GridLayout.h` | 3 | `GridLayout in 85 files`, name spread 85 | | `src/platform/desktop/moonlive_asm_arm64.cpp` | 3 | `lines 35-40, also in src/platform/esp32/moonlive_asm_riscv.cpp:34`, duplicated block 6 | +| `src/platform/esp32/moonlive_asm_xtensa.cpp` | 3 | `lines 483-489, also in src/platform/esp32/moonlive_asm_riscv.cpp:297`, duplicated block 7 | | `src/platform/teensy/platform_teensy_ota.cpp` | 3 | `platform::otaWriteStream`, complex function 16 | | `test/unit/core/unit_NetworkModule_ethernet.cpp` | 3 | `lines 276-285, also in test/unit/core/unit_NetworkModule_ethernet.cpp:193`, duplicated block 10 | | `test/unit/light/unit_AuroraEffect.cpp` | 3 | `lines 40-48, also in test/unit/light/unit_PolarLut_equivalence.cpp:40`, duplicated block 9 | | `test/unit/light/unit_MoonLiveLayout.cpp` | 3 | `lines 685-935, also in test/unit/light/unit_MoonLiveLayout.cpp:403`, duplicated block 251 | -| `test/unit/light/unit_MoonLiveParticles.cpp` | 3 | `lines 34-47, also in test/unit/light/unit_MoonLiveDrawing.cpp:26`, duplicated block 14 | -| `test/unit/light/unit_NetworkReceiveEffect_protocols.cpp` | 3 | `lines 23-44, also in test/unit/light/unit_NetworkReceiveEffect.cpp:18`, duplicated block 22 | | `test/unit/light/unit_PolarLut_equivalence.cpp` | 3 | `lines 40-55, also in test/unit/light/unit_NoiseEffect.cpp:33`, duplicated block 16 | | `test/unit/light/unit_PongEffect.cpp` | 3 | `lines 80-91, also in test/unit/light/unit_PongEffect.cpp:10`, duplicated block 12 | | `test/unit/light/unit_RmtLedDriver_lifecycle.cpp` | 3 | `lines 19-30, also in test/unit/light/unit_RmtLedDriver_pins.cpp:17`, duplicated block 12 | @@ -226,7 +225,7 @@ The next function worth simplifying is at the top of each file's list. | `moondeck/docs/generate_test_docs.py` | 2 | `lines 327-335, also in moondeck/docs/generate_test_docs.py:57`, duplicated block 9 | | `moondeck/moontube/reset_device.py` | 2 | `lines 241-246, also in moondeck/moontube/reset_device.py:219`, duplicated block 6 | | `moondeck/scenario/run_scenario.py` | 2 | `lines 33-39, also in moondeck/docs/build_docs.py:43`, duplicated block 7 | -| `src/core/moonlive/MoonLiveBuiltins.h` | 2 | `lines 208-219, also in src/core/moonlive/MoonLiveBuiltins.h:111`, duplicated block 12 | +| `src/core/moonlive/MoonLiveBuiltins.h` | 2 | `lines 254-265, also in src/core/moonlive/MoonLiveBuiltins.h:157`, duplicated block 12 | | `src/core/services/MidiService.h` | 2 | `MidiService::serveShare in tick20ms`, blocking call on the render path 1 | | `src/core/system/DevicePlugin.h` | 2 | `lines 94-100, also in src/core/system/DevicePlugin.h:75`, duplicated block 7 | | `src/core/system/HttpServerModule.h` | 2 | `HttpServerModule in 17 files`, name spread 17 | @@ -251,15 +250,15 @@ The next function worth simplifying is at the top of each file's list. | `src/light/effects/StarFieldEffect.h` | 2 | `StarFieldEffect::for`, deeply nested 4 | | `src/light/effects/TetrixEffect.h` | 2 | `TetrixEffect::static_cast`, complex function 28 | | `src/light/layouts/GridBlacksLayout.h` | 2 | `GridBlacksLayout::placeLights`, deeply nested 4 | -| `src/light/moonlive/MoonLiveEffect.h` | 2 | `MoonLiveEffect in 14 files`, name spread 14 | +| `src/light/moonlive/MoonLiveEffect.h` | 2 | `MoonLiveEffect in 15 files`, name spread 15 | | `src/light/powerfunctions/polar.h` | 2 | `PolarLut::prepare`, complex function 41 | | `src/light/powerfunctions/shader.h` | 2 | `shader::cosPalette`, long parameter list 13 | | `src/light/util/MpegTs.h` | 2 | `ts::Writer::writeAccessUnit`, complex function 23 | | `src/light/util/Palette.h` | 2 | `paletteOptions`, complex function 12 | | `src/light/util/RtpH264.h` | 2 | `rtp::Packetiser::writeNal`, long parameter list 8 | | `src/platform/esp32/moonlive_asm_riscv.h` | 2 | `lines 74-91, also in src/platform/esp32/moonlive_asm_xtensa.h:79`, duplicated block 18 | -| `src/platform/esp32/moonlive_asm_xtensa.cpp` | 2 | `lines 481-487, also in src/platform/esp32/moonlive_asm_riscv.cpp:297`, duplicated block 7 | | `src/platform/esp32/moonlive_emit_riscv.cpp` | 2 | `lines 52-57, also in src/platform/esp32/moonlive_emit_riscv.cpp:25`, duplicated block 6 | +| `src/platform/teensy/moonlive_asm_thumb2.cpp` | 2 | `lines 67-72, also in src/platform/esp32/moonlive_asm_xtensa.cpp:190`, duplicated block 6 | | `test/js/ui-midi-bridge.test.mjs` | 2 | `lines 4-16, also in test/js/ui-gamepad-bridge.test.mjs:4`, duplicated block 13 | | `test/unit/core/unit_AudioService_sync.cpp` | 2 | `lines 313-322, also in test/unit/core/unit_AudioService_sync.cpp:269`, duplicated block 10 | | `test/unit/core/unit_DevicesModule_hue.cpp` | 2 | `lines 63-69, also in test/unit/core/unit_DevicesModule_hue.cpp:42`, duplicated block 7 | @@ -274,9 +273,10 @@ The next function worth simplifying is at the top of each file's list. | `test/unit/light/unit_FixedPointEffect.cpp` | 2 | `lines 44-49, also in test/unit/light/unit_FixedPointEffect.cpp:33`, duplicated block 6 | | `test/unit/light/unit_GpioDmaPeripheral.cpp` | 2 | `lines 43-50, also in test/unit/light/unit_ParlioPeripheral.cpp:43`, duplicated block 8 | | `test/unit/light/unit_Hub75Driver_pins.cpp` | 2 | `lines 66-77, also in test/unit/core/unit_PinsModule.cpp:46`, duplicated block 12 | -| `test/unit/light/unit_MoonLiveDrawing.cpp` | 2 | `lines 26-39, also in test/unit/light/unit_MoonLiveParticles.cpp:34`, duplicated block 14 | | `test/unit/light/unit_MoonLiveMotion.cpp` | 2 | `lines 209-214, also in test/unit/light/unit_MoonLiveMotion.cpp:118`, duplicated block 6 | +| `test/unit/light/unit_MoonLiveParticles.cpp` | 2 | `lines 158-163, also in test/unit/light/unit_MoonLiveParticles.cpp:142`, duplicated block 6 | | `test/unit/light/unit_MoonLiveScriptResolve.cpp` | 2 | `lines 90-97, also in test/unit/light/unit_MoonLiveScriptResolve.cpp:78`, duplicated block 8 | +| `test/unit/light/unit_NetworkReceiveEffect_protocols.cpp` | 2 | `lines 23-44, also in test/unit/light/unit_NetworkReceiveEffect.cpp:18`, duplicated block 22 | | `test/unit/light/unit_NoiseEffect.cpp` | 2 | `lines 33-48, also in test/unit/light/unit_PolarLut_equivalence.cpp:40`, duplicated block 16 | | `test/unit/light/unit_Scroll.cpp` | 2 | `lines 6-20, also in test/unit/light/unit_Splat.cpp:6`, duplicated block 15 | | `test/unit/light/unit_SineEffect.cpp` | 2 | `lines 22-30, also in test/unit/light/unit_DistortionWavesEffect.cpp:26`, duplicated block 9 | @@ -293,12 +293,12 @@ The next function worth simplifying is at the top of each file's list. | `moondeck/moontube/mtnarrate.py` | 1 | `lines 91-98, also in moondeck/moontube/mtmeasure.py:42`, duplicated block 8 | | `moondeck/moontube/mtrun.py` | 1 | `mtrun.py`, large file 1711 | | `moondeck/scenario/run_live_scenario.py` | 1 | `run_live_scenario.py`, large file 1677 | -| `mooninstaller/install-orchestrator.js` | 1 | `install-orchestrator.js`, large file 1383 | -| `mooninstaller/install.js` | 1 | `install.js`, large file 1551 | +| `mooninstaller/install-orchestrator.js` | 1 | `install-orchestrator.js`, large file 1386 | +| `mooninstaller/install.js` | 1 | `install.js`, large file 1552 | | `src/core/module/Control.h` | 1 | `sanitizeHostname`, complex function 16 | | `src/core/moonlive/MoonLive.h` | 1 | `moonlive::MoonLive::addDeclaredControl`, complex function 16 | -| `src/core/moonlive/MoonLiveIr.h` | 1 | `moonlive::IrProgram::push`, complex function 11 | -| `src/core/moonlive/MoonLiveScript.h` | 1 | `moonlive::MoonLiveScript::sync`, complex function 19 | +| `src/core/moonlive/MoonLiveIr.h` | 1 | `moonlive::IrProgram::push`, complex function 12 | +| `src/core/moonlive/MoonLiveScript.h` | 1 | `moonlive::MoonLiveScript::sync`, complex function 20 | | `src/core/services/AudioLevel.h` | 1 | `computeLevel`, complex function 14 | | `src/core/services/MoonLiveService.h` | 1 | `moonlive::MoonLive::runValue in tick20ms`, blocking call on the render path 1 | | `src/core/services/OscPacket.h` | 1 | `osc::parse`, complex function 32 | @@ -330,7 +330,7 @@ The next function worth simplifying is at the top of each file's list. | `src/light/layers/Effects.h` | 1 | `Effects in 77 files`, name spread 77 | | `src/light/layouts/CubeLayout.h` | 1 | `lines 81-88, also in src/light/layouts/GridLayout.h:43`, duplicated block 8 | | `src/light/layouts/HumanSizedCubeLayout.h` | 1 | `HumanSizedCubeLayout::placeLights`, complex function 21 | -| `src/light/layouts/Layouts.h` | 1 | `Layouts in 111 files`, name spread 111 | +| `src/light/layouts/Layouts.h` | 1 | `Layouts in 109 files`, name spread 109 | | `src/light/layouts/PanelLayout.h` | 1 | `lines 54-69, also in src/light/layouts/PanelsLayout.h:64`, duplicated block 16 | | `src/light/layouts/PanelsLayout.h` | 1 | `lines 64-79, also in src/light/layouts/PanelLayout.h:54`, duplicated block 16 | | `src/light/layouts/SphereLayout.h` | 1 | `SphereLayout::forEachShellPoint`, deeply nested 4 | @@ -365,7 +365,7 @@ The next function worth simplifying is at the top of each file's list. | `test/scenario_runner.cpp` | 1 | `scenario_runner.cpp`, large file 1075 | | `test/unit/core/unit_ControlModule.cpp` | 1 | `unit_ControlModule.cpp`, large file 2152 | | `test/unit/core/unit_Control_apply_absent_key.cpp` | 1 | `lines 92-103, also in test/unit/core/unit_HttpServerModule_apply.cpp:40`, duplicated block 12 | -| `test/unit/core/unit_moonlive_compiler.cpp` | 1 | `unit_moonlive_compiler.cpp`, large file 1865 | +| `test/unit/core/unit_moonlive_compiler.cpp` | 1 | `unit_moonlive_compiler.cpp`, large file 1921 | | `test/unit/core/unit_moonlive_fill.cpp` | 1 | `unit_moonlive_fill.cpp`, large file 1380 | | `test/unit/light/unit_BreakoutEffect.cpp` | 1 | `lines 18-27, also in test/unit/light/unit_TetrixEffect.cpp:20`, duplicated block 10 | | `test/unit/light/unit_Canvas.cpp` | 1 | `lines 21-26, also in test/unit/light/unit_Splat.cpp:15`, duplicated block 6 | @@ -373,6 +373,7 @@ The next function worth simplifying is at the top of each file's list. | `test/unit/light/unit_GridLayout.cpp` | 1 | `lines 8-21, also in test/unit/light/unit_SingleColumnLayout.cpp:12`, duplicated block 14 | | `test/unit/light/unit_Layer_persistence.cpp` | 1 | `lines 45-51, also in test/unit/light/unit_TextEffect.cpp:18`, duplicated block 7 | | `test/unit/light/unit_Layer_phase_animation.cpp` | 1 | `lines 77-85, also in test/unit/light/unit_RandomMapModifier.cpp:115`, duplicated block 9 | +| `test/unit/light/unit_MoonLiveScripts_ir_golden.cpp` | 1 | `lines 74-85, also in test/unit/light/unit_MoonLiveScripts.cpp:55`, duplicated block 12 | | `test/unit/light/unit_NetworkReceiveEffect.cpp` | 1 | `lines 18-39, also in test/unit/light/unit_NetworkReceiveEffect_protocols.cpp:23`, duplicated block 22 | | `test/unit/light/unit_ParallelLedDriver_doublebuffer.cpp` | 1 | `lines 93-104, also in test/unit/light/unit_ParallelLedDriver_pinexpander.cpp:94`, duplicated block 12 | | `test/unit/light/unit_Rings241Layout.cpp` | 1 | `lines 18-30, also in test/unit/light/unit_SingleColumnLayout.cpp:19`, duplicated block 13 | diff --git a/docs/reference/metrics/docgen.md b/docs/reference/metrics/docgen.md index 8fcc9e5ad..4b99398f7 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 **4233 warning(s)** across 403 page(s). +**0 error(s)** and **4221 warning(s)** across 402 page(s). **5481 appendix line(s)** after `@moreinfo`. This only falls as well: a cleanup cuts first, and moves only what neither the code, the test name nor the spec says. @@ -17,12 +17,12 @@ The split is temporary. It stages the sweep rather than ranking the two kinds of | Rule | Errors | Warnings | |---|---:|---:| | split paragraphs | 0 | 1758 | -| over-wide comment lines | 0 | 1573 | +| over-wide comment lines | 0 | 1566 | | appendix sections nothing refers to | 0 | 358 | | multi-line comment blocks | 0 | 316 | -| over-long sentences | 0 | 82 | -| over-wide doc lines | 0 | 78 | -| member deep dives | 0 | 68 | +| over-long sentences | 0 | 80 | +| over-wide doc lines | 0 | 76 | +| member deep dives | 0 | 67 | ## By documentation area @@ -30,8 +30,8 @@ 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 | 2053 | -| `platform/index.md` | 0 | 730 | +| `(tests, no card)` | 0 | 2043 | +| `platform/index.md` | 0 | 728 | | `light/effects.md` | 0 | 403 | | `core/system.md` | 0 | 201 | | `core/supporting.md` | 0 | 175 | @@ -50,11 +50,11 @@ Per area, since that is the unit a sweep runs in: the files ranked, then the rul ### (tests, no card) -**0 error(s)** and **2053 warning(s)** across 157 file(s). +**0 error(s)** and **2043 warning(s)** across 156 file(s). | Findings | File | | |---:|---|---| -| 147 | `test/unit/core/unit_moonlive_compiler.cpp` | warning | +| 146 | `test/unit/core/unit_moonlive_compiler.cpp` | warning | | 115 | `test/unit/light/unit_ParallelLedDriver_ring.cpp` | warning | | 107 | `test/unit/core/unit_moonlive_fill.cpp` | warning | | 94 | `test/unit/light/unit_MoonLiveLayout.cpp` | warning | @@ -64,20 +64,20 @@ Per area, since that is the unit a sweep runs in: the files ranked, then the rul | 48 | `test/unit/light/unit_Effects_gridsweep.cpp` | warning | | 45 | `test/unit/core/unit_moonlive_codegen_xtensa.cpp` | warning | | 42 | `test/unit/core/unit_FilesystemModule_persistence.cpp` | warning | -| 1-42 each | *147 more warning files, 1287 findings* | | +| 1-42 each | *146 more warning files, 1278 findings* | | -By rule, warnings: 1354 over-wide comment lines, 267 split paragraphs, 196 multi-line comment blocks, 82 over-long sentences, 78 over-wide doc lines, 68 member deep dives, 8 appendix sections nothing refers to. +By rule, warnings: 1349 over-wide comment lines, 267 split paragraphs, 196 multi-line comment blocks, 80 over-long sentences, 76 over-wide doc lines, 67 member deep dives, 8 appendix sections nothing refers to. ### platform/index.md -**0 error(s)** and **730 warning(s)** across 29 file(s). +**0 error(s)** and **728 warning(s)** across 29 file(s). | Findings | File | | |---:|---|---| | 179 | `src/platform/esp32/platform_esp32_moon_i80.cpp` | warning | | 72 | `src/platform/desktop/moonlive_asm_x86_64.cpp` | warning | -| 68 | `src/platform/esp32/moonlive_asm_xtensa.cpp` | warning | | 67 | `src/platform/esp32/platform_esp32_i80.cpp` | warning | +| 66 | `src/platform/esp32/moonlive_asm_xtensa.cpp` | warning | | 59 | `src/platform/esp32/platform_esp32_rmt.cpp` | warning | | 42 | `src/platform/desktop/moonlive_asm_arm64.cpp` | warning | | 33 | `src/platform/esp32/platform_esp32_parlio.cpp` | warning | @@ -86,7 +86,7 @@ By rule, warnings: 1354 over-wide comment lines, 267 split paragraphs, 196 multi | 19 | `src/platform/esp32/platform_esp32_gpio.cpp` | warning | | 1-18 each | *19 more warning files, 136 findings* | | -By rule, warnings: 392 split paragraphs, 215 over-wide comment lines, 114 multi-line comment blocks, 9 appendix sections nothing refers to. +By rule, warnings: 392 split paragraphs, 213 over-wide comment lines, 114 multi-line comment blocks, 9 appendix sections nothing refers to. ### light/effects.md diff --git a/docs/reference/metrics/prose.md b/docs/reference/metrics/prose.md index 96bc27793..9800fb2db 100644 --- a/docs/reference/metrics/prose.md +++ b/docs/reference/metrics/prose.md @@ -9,14 +9,14 @@ This number is the debt behind that gate, and it RATCHETS: a rise fails the chec docgen.md does. A comment is prose wherever it lives, so a `//` in the web interface and a `#` in a script are counted beside a `///` in a header. -**2922 finding(s)** across 368 file(s). +**2912 finding(s)** across 367 file(s). | rule | findings | |---|---| -| `MoonLight.EmDash` | 1507 | -| `MoonLight.Weasel` | 687 | -| `MoonLight.SentenceLength` | 428 | -| `MoonLight.Temporal` | 215 | +| `MoonLight.EmDash` | 1500 | +| `MoonLight.Weasel` | 686 | +| `MoonLight.SentenceLength` | 427 | +| `MoonLight.Temporal` | 214 | | `MoonLight.Spelling` | 42 | | `MoonLight.SelfReference` | 30 | | `MoonLight.NegatedHeading` | 13 | @@ -31,7 +31,7 @@ docgen.md does. A comment is prose wherever it lives, so a `//` in the web inter | `mooninstaller/install-orchestrator.js` | 93 | | `docs/tutorials/build-your-own-moonmodules.md` | 92 | | `moondeck/moondeck_ui/app.js` | 92 | -| `docs/how-to/building.md` | 85 | +| `docs/how-to/building.md` | 78 | | `moondeck/check/check_clang_query.py` | 73 | | `src/ui/app.js` | 71 | | `mooninstaller/install.js` | 69 | diff --git a/docs/reference/metrics/repo-health.json b/docs/reference/metrics/repo-health.json index 3e1d25c7a..78e84a796 100644 --- a/docs/reference/metrics/repo-health.json +++ b/docs/reference/metrics/repo-health.json @@ -1,37 +1,37 @@ { - "commit": "97280e3a", + "commit": "779606f3", "flash": { - "esp32s3-n16r8": 2341088, - "esp32": 2241136, - "esp32p4rev1-eth": 2177776, - "esp32p4rev1-eth-wifi": 2358896, + "esp32s3-n16r8": 2333168, + "esp32": 2231776, + "esp32p4rev1-eth": 2226672, + "esp32p4rev1-eth-wifi": 2523840, "esp32s3-n8r8": 2336304, "esp32s31": 2595200, "esp32-16mb": 2060368, "esp32-eth": 1681040, "esp32-wrover": 1843760, "qemu": 1383648, - "esp32p4rev3-eth": 2236512, + "esp32p4rev3-eth": 2231792, "esp32s3-zero": 2219120, "esp32-pico": 2107168, - "desktop-macos": 2293816, + "desktop-macos": 2311464, "desktop-windows": 1925120, "esp32c3": 2335072, "teensy41-w5500": 1189848, "teensy41": 1202152 }, "measured": { - "esp32p4rev1-eth": "2026-10-07", + "esp32p4rev1-eth": "2026-10-12", "esp32s31": "2026-10-10", "esp32": "2026-10-12", "esp32-pico": "2026-09-09", - "esp32s3-n16r8": "2026-10-11", + "esp32s3-n16r8": "2026-10-12", "esp32s3-n8r8": "2026-10-10", "esp32s3-zero": "2026-10-10", "esp32-16mb": "2026-09-09", - "esp32p4rev1-eth-wifi": "2026-09-30", + "esp32p4rev1-eth-wifi": "2026-10-12", "esp32-eth": "2026-09-11", - "esp32p4rev3-eth": "2026-10-11", + "esp32p4rev3-eth": "2026-10-12", "desktop-macos": "2026-10-12", "desktop-windows": "2026-09-30", "esp32c3": "2026-10-10", @@ -48,8 +48,8 @@ "desktop-macos": { "p50": 7, "p95": 324, - "n": 30, - "last": "2026-10-10" + "n": 31, + "last": "2026-10-12" }, "esp32s3-n16r8": { "p50": 2073, @@ -745,7 +745,7 @@ "p50": 7, "p95": 74, "n": 32, - "last": "2026-10-11" + "last": "2026-10-12" }, "teensy41": { "p50": 2162, @@ -793,54 +793,54 @@ } }, "loc": { - "core": 28403, - "light": 31289, - "platform": 20255, - "ui": 12402, - "test": 65315, - "moondeck": 31233 + "core": 29053, + "light": 31300, + "platform": 20257, + "ui": 12417, + "test": 65810, + "moondeck": 31234 }, "comments": { "core": { - "lines": 6192, - "ratio": 0.242 + "lines": 6285, + "ratio": 0.24 }, "light": { - "lines": 7512, + "lines": 7513, "ratio": 0.269 }, "platform": { - "lines": 3980, + "lines": 3981, "ratio": 0.22 }, "ui": { - "lines": 3476, - "ratio": 0.297 + "lines": 3478, + "ratio": 0.296 }, "test": { - "lines": 7012, + "lines": 7041, "ratio": 0.124 }, "moondeck": { - "lines": 4938, + "lines": 4939, "ratio": 0.18 } }, "tests": { - "cases": 2436, + "cases": 2454, "scenarios": 30 }, "docs": { "md_files": 156, - "md_lines": 33066, + "md_lines": 33069, "plans_files": 47, - "backlog_lines": 3336, + "backlog_lines": 3337, "lessons_lines": 532, "claude_md_lines": 283 }, "complexity": { - "functions": 4766, - "over_threshold": 334, + "functions": 4800, + "over_threshold": 348, "worst_ccn": 128 } } diff --git a/docs/reference/metrics/repo-health.md b/docs/reference/metrics/repo-health.md index fa9c400a4..168885c97 100644 --- a/docs/reference/metrics/repo-health.md +++ b/docs/reference/metrics/repo-health.md @@ -1,6 +1,6 @@ # Repo health -Measured at `97280e3a`. Generated by [`moondeck/check/repo_health.py`](../../../moondeck/check/repo_health.py) on every KPI-gate run. **Do not edit by hand.** +Measured at `779606f3`. 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,12 +8,13 @@ Current state only; the trend is this file's git history (`git log -p docs/refer | Metric | v6.0.0 | Now | Change | |---|---:|---:|---:| -| flash desktop-macos | 1,963 KB | 2,240 KB | +277.0 KB (+14.1%) | -| flash esp32 | 2,061 KB | 2,189 KB | +127.4 KB (+6.2%) | +| flash desktop-macos | 1,963 KB | 2,257 KB | +294.3 KB (+15.0%) | +| flash esp32 | 2,061 KB | 2,179 KB | +118.3 KB (+5.7%) | | flash esp32c3 | - | 2,280 KB | new | -| flash esp32p4rev1-eth | 2,022 KB | 2,127 KB | +105.0 KB (+5.2%) | -| flash esp32p4rev3-eth | 2,025 KB | 2,184 KB | +158.8 KB (+7.8%) | -| flash esp32s3-n16r8 | 2,111 KB | 2,286 KB | +174.7 KB (+8.3%) | +| flash esp32p4rev1-eth | 2,022 KB | 2,174 KB | +152.8 KB (+7.6%) | +| flash esp32p4rev1-eth-wifi | 2,304 KB | 2,465 KB | +161.1 KB (+7.0%) | +| flash esp32p4rev3-eth | 2,025 KB | 2,179 KB | +154.2 KB (+7.6%) | +| flash esp32s3-n16r8 | 2,111 KB | 2,278 KB | +167.0 KB (+7.9%) | | flash esp32s3-n8r8 | 2,038 KB | 2,282 KB | +243.3 KB (+11.9%) | | flash esp32s3-zero | 1,977 KB | 2,167 KB | +190.4 KB (+9.6%) | | flash esp32s31 | 2,395 KB | 2,534 KB | +139.0 KB (+5.8%) | @@ -21,13 +22,13 @@ Current state only; the trend is this file's git history (`git log -p docs/refer | flash teensy41-w5500 | - | 1,162 KB | new | | tick Effects_pipeline_builds_and_renders (desktop-macos) | 7 ยตs | 7 ยตs | +0 (+0.0%) | | tick Layouts_resize_reallocates_live (desktop-macos) | 12 ยตs | 10 ยตs | -2 (-16.7%) | -| lines core | 22,299 | 28,403 | +6,104 (+27.4%) | -| lines light | 31,054 | 31,289 | +235 (+0.8%) | -| lines platform | 16,798 | 20,255 | +3,457 (+20.6%) | -| lines ui | 11,394 | 12,402 | +1,008 (+8.8%) | -| lines test | 57,759 | 65,315 | +7,556 (+13.1%) | -| lines moondeck | 29,325 | 31,233 | +1,908 (+6.5%) | -| unit tests | 2,120 | 2,436 | +316 (+14.9%) | +| lines core | 22,299 | 29,053 | +6,754 (+30.3%) | +| lines light | 31,054 | 31,300 | +246 (+0.8%) | +| lines platform | 16,798 | 20,257 | +3,459 (+20.6%) | +| lines ui | 11,394 | 12,417 | +1,023 (+9.0%) | +| lines test | 57,759 | 65,810 | +8,051 (+13.9%) | +| lines moondeck | 29,325 | 31,234 | +1,909 (+6.5%) | +| unit tests | 2,120 | 2,454 | +334 (+15.8%) | | scenarios | 21 | 30 | +9 (+42.9%) | Against the release `v6.0.0`, the fixed point a user compares with; the tables below compare against the last commit. A firmware not rebuilt since `v6.0.0` is left out, and a measure the release did not record yet, such as the code report, joins from the next one. @@ -36,23 +37,23 @@ Against the release `v6.0.0`, the fixed point a user compares with; the tables b | Target | Flash | Capacity | Used | Built | |---|---:|---:|---:|:--:| -| desktop-macos | 2,240 KB (+192 B) โš  | - | - | yes | +| desktop-macos | 2,257 KB (+17 KB) โš  | - | - | yes | | desktop-windows | 1,880 KB | - | - | **STALE 12d** | -| esp32 | 2,189 KB (+3 KB) โš  | 2,496 KB | 88% | yes | +| esp32 | 2,179 KB (โˆ’9 KB) โœ“ | 2,496 KB | 87% | yes | | esp32-16mb | 2,012 KB | 4,096 KB | 49% | **STALE 33d** | | esp32-eth | 1,642 KB | 2,496 KB | 66% | **STALE 31d** | | esp32-pico | 2,058 KB | 3,072 KB | 67% | **STALE 33d** | | esp32-wrover | 1,801 KB | 2,496 KB | 72% | carried (age?) | | esp32c3 | 2,280 KB | 2,496 KB | 91% | carried 2d | -| esp32p4rev1-eth | 2,127 KB | 4,096 KB | 52% | carried 5d | -| esp32p4rev1-eth-wifi | 2,304 KB | 4,096 KB | 56% | **STALE 12d** | -| esp32p4rev3-eth | 2,184 KB | 4,096 KB | 53% | carried 1d | -| esp32s3-n16r8 | 2,286 KB | 4,096 KB | 56% | carried 1d | +| esp32p4rev1-eth | 2,174 KB (+48 KB) โš  | 4,096 KB | 53% | yes | +| esp32p4rev1-eth-wifi | 2,465 KB (+161 KB) โš  | 4,096 KB | 60% | yes | +| esp32p4rev3-eth | 2,179 KB (โˆ’5 KB) โœ“ | 4,096 KB | 53% | yes | +| esp32s3-n16r8 | 2,278 KB (โˆ’8 KB) โœ“ | 4,096 KB | 56% | yes | | esp32s3-n8r8 | 2,282 KB | 3,072 KB | 74% | carried 2d | | esp32s3-zero | 2,167 KB | 2,496 KB | 87% | carried 2d | | esp32s31 | 2,534 KB | 4,096 KB | 62% | carried 2d | | qemu | 1,351 KB | - | - | carried (age?) | -| teensy41 | 1,174 KB (+16 KB) โš  | 6,912 KB | 17% | yes | +| teensy41 | 1,174 KB | 6,912 KB | 17% | yes | | teensy41-w5500 | 1,162 KB | - | - | carried 1d | `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. @@ -61,11 +62,17 @@ Against the release `v6.0.0`, the fixed point a user compares with; the tables b | Target | Net | New | Removed | Grown | Shrunk | Other | Since | |---|---:|---:|---:|---:|---:|---:|---| -| esp32 | +2.6 KB | +0.2 KB | +0.0 KB | +2.2 KB | -0.1 KB | +0.4 KB | `97280e3a` | +| esp32 | -9.1 KB | +6.4 KB | -2.0 KB | +2.8 KB | -16.3 KB | -0.0 KB | `779606f3` | +| esp32p4rev1-eth | +47.8 KB | +71.6 KB | -34.2 KB | +32.1 KB | -31.8 KB | +10.0 KB | `779606f3` | +| esp32p4rev3-eth | -4.6 KB | +6.3 KB | -1.5 KB | +6.8 KB | -16.3 KB | +0.1 KB | `779606f3` | +| esp32s3-n16r8 | -7.7 KB | +5.4 KB | -1.1 KB | +4.4 KB | -16.7 KB | +0.3 KB | `779606f3` | Largest moves: -- **esp32**: `mm::ui::appJs` +1,398 B, `mm::MoonCloudModule::defineControls()` +159 B, `mm::MoonStatsModule::sendReport(mm::MoonStatsEvent)` +140 B, `mm::MoonTalkModule::send()` +134 B, `mm::FixedPointEffect::tick()` -118 B +- **esp32**: `mm::moonlive::(anonymous namespace)::Parser::parseCall(unsig` -3,423 B, `mm::moonlive::compileSource(char const*, mm::moonlive::Built` -2,903 B, `mm::moonlive::(anonymous namespace)::Parser::parsePrimary()` -2,636 B, `mm::moonlive::(anonymous namespace)::Parser::parseIf()` -1,560 B, `mm::moonlive::(anonymous namespace)::Parser::parseTerm()` -1,402 B +- **esp32p4rev1-eth**: `mm::(anonymous namespace)::Applier::applyMember(mm::MoonModu` +3,818 B, `mm::HttpServerModule::handleConnection(mm::platform::TcpConn` +3,648 B, `mm::HttpServerModule::handleConnection(mm::platform::TcpConn` -3,588 B, `mm::moonlive::(anonymous namespace)::Parser::parseCall(unsig` -3,400 B, `mm::(anonymous namespace)::Applier::apply(mm::MoonModule*, m` -3,154 B +- **esp32p4rev3-eth**: `mm::moonlive::(anonymous namespace)::Parser::parseCall(unsig` -3,400 B, `mm::moonlive::(anonymous namespace)::Parser::parsePrimary()` -2,588 B, `mm::moonlive::compileSource(char const*, mm::moonlive::Built` -2,496 B, `mm::moonlive::lightBuiltins()::{lambda()#1}::operator()() co` +1,690 B, `mm::moonlive::(anonymous namespace)::Parser::parseTerm()` -1,600 B +- **esp32s3-n16r8**: `mm::moonlive::(anonymous namespace)::Parser::parseCall(unsig` -3,479 B, `mm::moonlive::compileSource(char const*, mm::moonlive::Built` -2,939 B, `mm::moonlive::(anonymous namespace)::Parser::parsePrimary()` -2,668 B, `mm::moonlive::(anonymous namespace)::Parser::parseIf()` -1,704 B, `mm::moonlive::(anonymous namespace)::Parser::parseTerm()` -1,458 B What this commit's flash went to, symbol by symbol against the measurement at `Since`. New is new functionality, removed and shrunk are what a cleanup saved, and other is what no symbol owns, such as merged strings and alignment. A rename reads as removed plus new, which the largest moves expose. @@ -127,26 +134,26 @@ These build a bare pipeline with no optional modules, so a change here is a chan | Area | Lines | Comments | Comment share | |---|---:|---:|---:| -| core | 28,403 (+108) โš  | 6,192 | 24.2 % | -| light | 31,289 | 7,512 | 26.9 % | -| platform | 20,255 (+380) โš  | 3,980 | 22.0 % | -| ui | 12,402 (+95) โš  | 3,476 | 29.7 % (โˆ’0.1 %) โœ“ | -| test | 65,315 (+199) โš  | 7,012 | 12.4 % | -| moondeck | 31,233 (+95) โš  | 4,938 | 18.0 % (โˆ’0.1 %) โœ“ | +| core | 29,053 (+650) โš  | 6,285 | 24.0 % (โˆ’0.2 %) โœ“ | +| light | 31,300 (+11) โš  | 7,513 | 26.9 % | +| platform | 20,257 (+2) โš  | 3,981 | 22.0 % | +| ui | 12,417 (+15) โš  | 3,478 | 29.6 % (โˆ’0.1 %) โœ“ | +| test | 65,810 (+495) โš  | 7,041 | 12.4 % | +| moondeck | 31,234 (+1) โš  | 4,939 | 18.0 % | ## Tests | Kind | Count | |---|---:| -| unit cases | 2,436 (+6) โœ“ | +| unit cases | 2,454 (+18) โœ“ | | scenarios | 30 | ## Complexity | Metric | Value | |---|---:| -| functions | 4,766 (+49) โœ“ | -| over threshold | 334 (+4) โš  | +| functions | 4,800 (+34) โœ“ | +| over threshold | 348 (+14) โš  | | worst CCN | 128 | ## Documentation @@ -154,9 +161,9 @@ These build a bare pipeline with no optional modules, so a change here is a chan | Metric | Value | |---|---:| | markdown files | 156 | -| markdown lines | 33,066 (+70) โš  | +| markdown lines | 33,069 (+3) โš  | | plan files | 47 | -| backlog lines | 3,336 (+6) โš  | +| backlog lines | 3,337 (+1) โš  | | lessons lines | 532 | | CLAUDE.md lines | 283 | diff --git a/docs/work/future/backlog-core.md b/docs/work/future/backlog-core.md index c42666bdf..fc4ee8eea 100644 --- a/docs/work/future/backlog-core.md +++ b/docs/work/future/backlog-core.md @@ -282,7 +282,7 @@ declared rather than for a buffer to fill, and to time out on stall rather than 1.0 ships ESP32 firmware (4 variants) + macOS arm64 + Windows x64. Still to add: -- **ESP32-P4** firmware variant โ€” **`esp32p4rev1-eth` (Ethernet-only) shipped**: in `build_esp32.py`'s `FIRMWARES`, the `deviceModels.json` catalog (Waveshare P4-NANO), and CI builds + publishes it to the web installer + releases. **`esp32p4rev1-eth-wifi` now ships too** (2026-08-19): it boots and associates on IDF v6.1-rc1, so it is out of the experimental set in the installer and carries a normal description. Its open defect is throughput, not shipping โ€” see ยง ESP32-P4 round 3, open issue 0. +- **ESP32-P4** firmware variant: **`esp32p4rev1-eth` (Ethernet-only) shipped**: in `build_esp32.py`'s `FIRMWARES`, the `deviceModels.json` catalog (Waveshare P4-NANO), and CI builds + publishes it to the web installer + releases. **`esp32p4rev1-eth-wifi` now ships too** (2026-08-19): it booted and associated on IDF v6.1-rc1, so it is out of the experimental set in the installer and carries a normal description. On IDF v6.1 it no longer associates (2026-10-12): see ยง ESP32-P4 round 3. - **ESP32-P4 v3.x silicon variant (backlog)** โ€” `esp32p4rev1-eth` is built for pre-v3 P4 (`CONFIG_ESP32P4_SELECTS_REV_LESS_V3` + `REV_MIN_0`), because the v6.1 IDF default (v3.1) refused to boot on the bench/field v1.x P4 and rev <3.0 vs >=3.0 are "huge hardware difference" (one binary can't cover both). **DONE (2026-08-19): `esp32p4rev3-eth` and `esp32p4rev3-eth-wifi` ship** (`REV_MIN_300`, which covers v3.0-v3.99), reusing the rev1 board fragment so the partition table and EMAC config stay in one place. **Still open: neither has ever been booted** โ€” both bench boards are v1.3 engineering samples, so the rev3 images are flagged experimental in the installer and need a v3 board to verify. Espressif does not recommend v0.x/v1.x for new designs, so a board bought today is v3.x and needs these. - **Retire the OTA fallback repository, once a device has another recovery route.** `FirmwareUpdateModule.h` names two: `kReleaseRepo` is tried first and `kFallbackRepo` catches a device whose firmware predates the move. It is tempting to call the second dead code after the rename, and it is not: `platform_esp32_ota.cpp:195` retries the alternate address whenever `esp_https_ota_begin` FAILS, which covers DNS, TLS, an outage and a variant whose asset is missing, rather than only a repository that has moved. So a v6.0.0 device on the happy path never reaches it and a v6.0.0 device whose first request fails still does. Removing it deletes that second chance for every board, which wants a replacement rather than a deletion: a configurable update URL, or a retry against the same address, would give a device somewhere to go. The one cost of keeping it is a wasted request on the failure path, which is already the slow path. - **Raspberry Pi** โ€” ARM64, cross-built or native. @@ -1191,6 +1191,7 @@ This is hardening, not a known bug โ€” the shipped fix is correct for the cases ### ESP32-P4 support โ€” rounds 3-4 (in progress) Rounds 1 (board + Ethernet-only) and 2 (Parlio LED driver) have landed. Remaining rounds, each its own plan + commit: +- **Round 3 regression: WiFi no longer associates on IDF v6.1 (2026-10-12).** It associated and served traffic on IDF v6.1-rc1 (below). The bench P4-NANO (v1.3, port `usbmodem5ABA0767291`, registry .139) flashed with `esp32p4rev1-eth-wifi` on the v6.1 pin, Ethernet unplugged: Improv delivered the bench SSID over USB serial, the board answered "unable to connect", and it never appeared on the network. Its log does not reach `monitor_esp32` on that USB-JTAG port, so the cause is unread. First step: read the P4's console (UART pins with an adapter) for the esp_hosted / C6 lines, then compare against the rc1 build that worked: the IDF move from rc1 to v6.1 and the esp_hosted version it pulls are the two candidates. - **Round 3 โ€” WiFi via the C6 co-processor. WORKING, BUT THE LINK IS SLOW (2026-08-19).** Boots and associates on IDF v6.1-rc1 (see round 4); the remaining defect is throughput, bench-bisected below. The P4 has no native radio (`SOC_WIFI_SUPPORTED` absent); WiFi comes from the on-board ESP32-C6 over SDIO via `esp_wifi_remote` / esp_hosted. Landed as the `esp32p4rev1-eth-wifi` firmware variant: components pulled P4-only (`rules:` gate in `idf_component.yml`), and `ensureWifiInit()` needs no hosted bring-up of its own: esp_hosted self-initialises at boot via a constructor (`ESP_SYSTEM_INIT_FN`), which sets up the SDIO transport, RPC and wifi-remote channels before `app_main`. Calling `esp_hosted_init`/`connect_to_slave` there would be worse than redundant โ€” `connect_to_slave` is a transport *reconfigure* that resets the slave and re-inits SDIO, which fails on a live link. The rest of the WiFi seam is unchanged because `esp_wifi_remote` is API-compatible. A deliberate, documented [v6.0-floor exception](../../how-to/building.md#esp-idf-version); C6 config via `CONFIG_SLAVE_IDF_TARGET_ESP32C6` + `CONFIG_ESP_HOSTED_CP_TARGET_ESP32C6` + the `CONFIG_ESP_HOSTED_P4_DEV_BOARD_FUNC_BOARD` SDIO-pin preset. **Hardware results (bench, P4-NANO, 2026-06-12):** diff --git a/docs/work/future/input-mapping-analysis.md b/docs/work/future/input-mapping-analysis.md index 62ac8cc75..359297b1a 100644 --- a/docs/work/future/input-mapping-analysis.md +++ b/docs/work/future/input-mapping-analysis.md @@ -203,7 +203,7 @@ have. A Dig-2-Go never carries it; a P4 installation does. **MoonLive can still USE it, and that is the distinction to keep.** A script cannot upload 84 KB and should never try, but it does not need to: the compiled module owns the blob and the bus, and the -script calls a builtin (`tofZone(x, y)`) exactly as it calls `beat()` or `paletteR()` without +script calls a builtin (`tofZone(x, y)`) exactly as it calls `beat()` or `paletteRGB()` without implementing a beat or a palette. The power-function pattern is what makes a heavyweight sensor scriptable. What a script cannot do is *be* the driver for such a sensor, which is the honest boundary of the scripted-sensor claim: a script suits a sensor whose init is a handful of register diff --git a/docs/work/future/moonlive-language-roadmap.md b/docs/work/future/moonlive-language-roadmap.md index 2a22186ee..aea12c29f 100644 --- a/docs/work/future/moonlive-language-roadmap.md +++ b/docs/work/future/moonlive-language-roadmap.md @@ -34,7 +34,7 @@ Five hard limits, all found by hitting them: |---|---|---| | script state | **64 bytes** shared by all members | `kCtrlBytes`, `MoonLiveBuiltins.h` | | distinct members | **16** โœ… | `kMaxCtrls`, same file | -| branch labels | **40** in the IR, **48** labels and **96** fixups in the assembler, counted across the whole script (an `if` or `for` takes up to 2) | `kIrLabels`, `kAsmLabels`, `kAsmFixups`, `MoonLiveIr.h` | +| branch labels | **96** in the IR, **255** labels and **255** fixups in the assembler, counted across the whole script (an `if` or `for` takes up to 2, and each `||`, `!` and `!=` one more) | `kIrLabels`, `kAsmLabels`, `kAsmFixups`, `MoonLiveIr.h` | | frame slots | **32**, shared by live variables, loop counters and staged call arguments โœ… | `kMaxLocals`, `MoonLiveIr.h` | | ~~numeric types~~ | ~~`uint8_t`, `uint16_t`, `int16_t`~~ โ†’ **`int`, `byte`, `bool`, `fixed`, `string`** โœ… | still no float: `fixed` is Q16.16 | | ~~builtin table~~ | ~~16, and 16 used~~ โ†’ **96** โœ… | `BuiltinTable::kMax`, raised, with an overflow assert | @@ -75,7 +75,7 @@ than "add a language feature": 1. **A builtin table of โ‰ฅ 64 entries.** โœ… *shipped*. `BuiltinTable::kMax` was **16, and the light domain registered exactly 16**: the table was FULL, `add()` returned false past the limit, and nothing checked the result, so the seventeenth builtin was dropped silently and the script - failed later with "unknown function". `kMax` is now 64, and an overflow is loud rather than + failed later with "unknown function". `kMax` is now 96, and an overflow is loud rather than silent (`MM_ASSERT_NO_BUILTIN_OVERFLOW`). 2. **A larger branch budget (`kIrLabels`, today 16).** Every `if` costs up to two labels and every `for` costs two, counted across the WHOLE script including its helper functions, and labels are @@ -272,7 +272,7 @@ A script now writes one call where it used to write three: setPaletteColor(x, y, index, brightness); ``` -`paletteR/G/B(i, bri)` shipped first and worked, but the shape was wrong: three host calls per +Three per-channel palette calls shipped first and worked, but the shape was wrong: three host calls per pixel, and, because the compiler evaluates each argument independently, three evaluations of whatever expression produced the brightness. A per-pixel radial falloff made that visible. @@ -282,11 +282,7 @@ index, so the buffer layout (`mod(x, width) + mod(y, height) * width`) stops bei every script. No engine change was needed: `line()` already proved a Call builtin can write pixels through the -per-run draw canvas, so this rode the existing seam. `paletteR/G/B` stay for a script that needs -the components rather than a pixel. - -What is still missing is the general form: a builtin RETURNING a color, so a script can hold one -in a variable and pass it on. That is #2's multi-value ABI, and #4b is what makes it worth having. +per-run draw canvas, so this rode the existing seam. `paletteRGB(i, bri)` answers the color as an `RGB` for a script that wants the value rather than a pixel (#4b). ### 4c. Insights from porting a per-pixel effect: *the cost is CALLS, not math* @@ -346,51 +342,13 @@ variables, so a new builtin should take a name a script would not: `polarA`/`pol the obvious ones. The compile-every-script test caught it immediately, which is the argument for keeping that test cheap to run. -### 4b. Predefined structs: `Coord3D` and `RGB`, together: *and they make #4 land properly* - -Planned: [MoonLive Coord3D and RGB types](../present/Plan-20261011%20-%20MoonLive%20Coord3D%20and%20RGB%20types.md), both types together as fields under one name, since the 16-bit Layer that held the color type back was reversed. - -Most of what a script manipulates is a POSITION or a COLOR, and today both are loose integers: a -coordinate is three separate values or an index the script computes by hand -(`mod(bx+dx, width) + mod(by+dy, height) * width`), and a color is three `uint8_t`s that cannot -travel together. Two predefined types would carry them: - -- **`Coord3D`**: `{x, y, z}`, the shape `setXYZ`, `addLight` and every layout already think in. -- **A color struct**: `{r, g, b}`, matching `RGB` in `core/color.h`. - -**No `HSV` struct.** The builtins already record the decision and the reason -(`MoonLiveBuiltins_light.h`): a hue wheel is how an effect picks color while IGNORING the user's -palette, and 47 of 52 compiled effects were moved off that habit. A predefined `HSV` would put it -back as the easy default. HSV stays where it earns its place, in `setPalEntryHSV`, which is -authoring a palette rather than bypassing one. - -**The color struct is `RGB`, our own name rather than FastLED's `CRGB`, and it lands with `Coord3D`.** It once waited for the 16-bit Layer the [generative-fields plan](generative-fields-analysis-top-down.md) proposed, since a script-visible color fixed at three `uint8_t` would have been the one place the channel width stopped being runtime data. That plan reversed the 16-bit Layer on the measurement (its ยง 11), so nothing holds the color type back, and the plan linked above builds both types in one step. - -Predefined rather than user-declarable structs (#10): these two are what the ENGINE already passes -around, so they need no general struct machinery, just two known layouts the compiler understands -and the builtins can take and return. - -The payoff is that #4 becomes the natural signature rather than a special case: - -```c -setColorFromPalette(pos, index, brightness); // pos is a Coord3D -``` +### 4b. Predefined structs: `Coord3D` and `RGB`, together: โœ… *shipped* -**Scope: a predefined struct is a type, not a calling convention.** One that appeared only in -builtin signatures would be a special case the language does not need. `Coord3D` is a class member, -a local, a function argument and a return value, exactly as `int`, `byte`, `bool` and `fixed` are, -which makes ยง 6 (arguments and returns) a prerequisite rather than a nicety. Two costs to settle -when it is added: whether a member is one member record or three of the eight, and hence 6 of the 64 -arena bytes; and that a local occupies one frame slot per field under today's flat allocator, so a -script holding a few coordinates reaches the 32-slot ceiling sooner (ยง 8b). +A struct is a named fixed-size array the compiler expands into scalar operations: a member, a local, a parameter and a return value, with field access, whole-struct assignment and a constructor. A struct argument fills one position per field, so every three-channel builtin takes an `RGB`. `getRGB` and `paletteRGB` answer one. The language reference holds the rest: [RGB and Coord3D](../../moonmodules/light/moonlive.md#rgb-and-coord3d). -One call, one brightness evaluation, and the index arithmetic stops being open-coded at every call -site. It also removes the `mod(...) + mod(...) * width` flattening a script writes today, which is -the buffer layout leaking into every effect. +**No `HSV` struct.** A hue wheel is how an effect picks color while ignoring the user's palette, so `HSV` stays in `setPalEntryHSV`, which authors a palette rather than bypassing one. -Worth noting the ordering: this only pays off with the multi-value call ABI from #2, and it is what -makes that ABI worth having beyond color: a `Coord3D` in and an `RGB` out is the shape most of -the power-functions surface wants. +What remains: `setColorFromPalette(pos, index, brightness)` taking a `Coord3D`, and arrays of structs (ยง 10). ### 5. Fractional arithmetic: *superseded by `fixed` in the type-system design above* @@ -421,11 +379,10 @@ with different state silently does the wrong thing. It is also what makes helper Script functions DO exist and ship: `balls.mle` calls `drawBall()`, `crosshair.mle` calls three helpers, and every layout and modifier is one. Recursion works. Three limits sit on top of them, and `aurora.mle` hit all three at once writing a `bright(v)` helper to apply one contrast window to two -layers (2026-09-04). **The return value shipped the same day**, so two remain: +layers (2026-09-04). **The return value shipped the same day, and eight parameter words with structs on 2026-10-12**, so one remains: | limit | what the compiler says | |---|---| -| more than four parameters | `a function takes at most 4 parameters` | | no forward calls: a helper must be declared ABOVE its caller | `unknown function`, with the column but not the name | A function may now RETURN a value: `int f() { return ...; }` and the call is an expression. Each @@ -469,9 +426,9 @@ So what shipped is smaller than "make the language signed" and removes more than `escape()` stays a builtin regardless: its Q13 squaring needs 64-bit intermediates, which a 32-bit script value cannot express however signed it is. -### 8. More branch labels: *probably a constant, worth measuring first* +### 8. More branch labels: โœ… *raised to 96 IR labels and 255 assembler labels and fixups, the tables on the compile-time heap* -16 is tight enough that a straightforward nested draw does not fit. The limits are now 40 IR labels and 48 assembler labels with 96 fixups, and two scripts still hit them on 2026-10-07: the StadBeest legs with a bass envelope, and the StadBeest eye with darts, blinks and a ripple, each at roughly 20 to 25 `if`s across all its functions. Both fit only after branches were rewritten as arithmetic (ยง 11). Raising `kIrLabels` costs +16 was tight enough that a straightforward nested draw did not fit. At 40 IR labels and 48 assembler labels with 96 fixups two scripts still hit them on 2026-10-07: the StadBeest legs with a bass envelope, and the StadBeest eye with darts, blinks and a ripple, each at roughly 20 to 25 `if`s across all its functions. Both fit only after branches were rewritten as arithmetic (ยง 11). Raising `kIrLabels` costs compile-time table space and nothing at run time. Measure what a realistic effect needs before picking a number: the balls port wanted ~12 and had to be folded down. @@ -558,8 +515,6 @@ Two effects written in one session on real hardware: `stadbeest-walk.mle` (a wal | limit | what it cost | wants | |---|---|---| -| no `\|\|`, `&&` or `!` | every compound condition is nested `if`s, which spends labels twice; the eye writes `pulse * music == 1` and `(r2 - lo) * (hi - r2) > 0` instead, and the Glow effect (2026-10-08) wrote `kick && !kicking` as `kick > kicking` | the three logical operators, short-circuit | -| no `abs`, `min`, `max`, `clamp` | every clamp is an `if`; the eye takes `abs` as `polarR(x, 0)` and `max(v, 0)` as `(v + polarR(v, 0)) / 2` | those four builtins, which also give back labels | | "codegen failed: assembler overflow (branch range, slot, or immediate)" | names neither the limit nor the line; finding it took a desktop bisect with `disasm.py` | the message names the table that ran out and the function it was filling | | "too many arguments to hold" | fires when the eye's `tick()` held too many live locals, so the wording sends the author to the calls | a message about live variables when that is the cause | | a failed recompile leaves the effect dark | on a live installation every edit that does not compile switches the light off until a fixed one arrives; it happened three times in the session | keep running the last program that compiled, and show the error beside it | diff --git a/docs/work/present/Plan-20260901 - Input mapping and scripted sensors (partial).md b/docs/work/present/Plan-20260901 - Input mapping and scripted sensors (partial).md index 675a9b308..5eaf07be0 100644 --- a/docs/work/present/Plan-20260901 - Input mapping and scripted sensors (partial).md +++ b/docs/work/present/Plan-20260901 - Input mapping and scripted sensors (partial).md @@ -747,7 +747,7 @@ to audio) rather than one general struct. **MoonLive is in scope for this sensor, through a power function.** A script cannot upload the blob and never should, but it does not need to: the compiled module owns the blob, the bus and the frame, -and the script reads a zone through a builtin the way it already calls `beat()` or `paletteR()` +and the script reads a zone through a builtin the way it already calls `beat()` or `paletteRGB()` without implementing either. So this step ships a builtin alongside the module: ```c diff --git a/docs/work/present/Plan-20261010 - Teensy 4.1.md b/docs/work/present/Plan-20261010 - Teensy 4.1.md index 0c55cce12..d3b455369 100644 --- a/docs/work/present/Plan-20261010 - Teensy 4.1.md +++ b/docs/work/present/Plan-20261010 - Teensy 4.1.md @@ -2,7 +2,7 @@ The README and the platform layer name Teensy as a target, but no Teensy platform exists: `src/platform` holds `esp32` and `desktop`. This plan adds `src/platform/teensy` behind the existing seam (`platform.h`), in phases that each end in something visible on the board. -The bench board is a **Teensy 4.1-NE**: no Ethernet PHY, so the PJRC MagJack kit has nothing to plug into. Ethernet comes from a **W5500 SPI module** through QNEthernet's W5500 driver, which runs lwIP on the chip's raw frames, so the W5500's own 8-socket limit does not apply. A regular Teensy 4.1 with the kit is the same code with QNEthernet's native driver, selected at build time. +The bench board is a **Teensy 4.1-NE**: no Ethernet PHY, so the PJRC MagJack kit has nothing to plug into. Ethernet comes from a **W5500 SPI module** through QNEthernet's W5500 driver, which runs lwIP on the chip's raw frames, so the W5500's own 8-socket limit does not apply. A regular Teensy 4.1 with the kit runs the same firmware on QNEthernet's native driver: both drivers are in one build, and the board's chip is found at boot, so no board needs a build of its own. โœ… **One firmware for both Ethernet chips** (phase 6 step 7, replacing an earlier pair `teensy41` and `teensy41-w5500`). `teensy41` carries QNEthernet's two drivers, the board's own PHY (a DP83825I behind PJRC's Ethernet kit) and the W5500 for the 4.1-NE, as a chip's firmware carries every Ethernet driver the chip can host. QNEthernet builds one driver, so each is compiled with its `driver` namespace renamed (per-file defines in `teensy/CMakeLists.txt`), and `src/platform/teensy/qnethernet/` holds the external driver its `qnethernet_external_driver.h` hook takes: it asks the PHY first, then the W5500, once, and forwards every call. The stack keeps lwIP's defaults, which both drivers support: no frame pad, and checksums in software. Verified on the 4.1-NE: it found the W5500, came online with its settings, and took a network update; the PHY half waits for a regular 4.1. diff --git a/docs/work/present/Plan-20261011 - Layer compositing, masks and transitions.md b/docs/work/present/Plan-20261011 - Layer compositing, masks and transitions.md index a4e091a1f..1821785eb 100644 --- a/docs/work/present/Plan-20261011 - Layer compositing, masks and transitions.md +++ b/docs/work/present/Plan-20261011 - Layer compositing, masks and transitions.md @@ -17,7 +17,7 @@ Alpha is already `lerp(backdrop, source, opacity)`, the specification's normal m ## The steps 1. โœ… **Blend modes.** Built: multiply, screen, darken, lighten, difference and exclusion, appended to `blendMode` so saved indices keep their meaning; overlay, hard-light and soft-light wait for their measured cost. Extend `BlendOp` with the separable set, multiply first, then screen, darken, lighten, difference and exclusion, and overlay, hard-light and soft-light if their cost holds. Each mode is one per-channel function with one specialized loop, chosen once per layer as today, integer math only. Opacity applies to every mode through the lerp. A multiply layer above any effect is then a mask over everything below it, soft-edged when the mask effect draws a soft edge. Tests pin each mode's formula at its corner values (0, 128, 255) and at partial opacity. -2. โœ… **Color reads for effect scripts.** Built: `getR`, `getG` and `getB` (one channel per call, as `paletteR`, `paletteG` and `paletteB` return a color), and `mulRGB`, `addRGB` and `blendRGB`. MoonLive effects can write a light but not read one. `getRGB`, plus `addRGB` (a saturating add), `mulRGB` (scale each channel by 0 to 255) and `blendRGB` (mix toward a color by an amount), let a script read what is already in the buffer. A mask effect placed after the main effect in the same layer then masks that layer only, since effects in a layer draw in order on one buffer; trails, glow and blur scripts gain the same reads. The names follow the existing `setRGB`. +2. โœ… **Color reads for effect scripts.** Built: `getRGB` (the light's color as an `RGB`, as `paletteRGB` answers a palette color), and `mulRGB`, `addRGB` and `blendRGB`. MoonLive effects can write a light but not read one. `getRGB`, plus `addRGB` (a saturating add), `mulRGB` (scale each channel by 0 to 255) and `blendRGB` (mix toward a color by an amount), let a script read what is already in the buffer. A mask effect placed after the main effect in the same layer then masks that layer only, since effects in a layer draw in order on one buffer; trails, glow and blur scripts gain the same reads. The names follow the existing `setRGB`. 3. **Transitions.** - A per-layer opacity ramp: a target and a duration, applied live, interpolated per frame. A layer fading in or out is the ramp on its opacity. - A dissolve between effects: on an effect change, the old effect keeps running in a temporary layer under the new one while the new layer's opacity ramps up; the temporary layer goes when the ramp ends. diff --git a/docs/work/present/Plan-20261011 - MoonLive Coord3D and RGB types.md b/docs/work/present/Plan-20261011 - MoonLive Coord3D and RGB types.md index 0e465820e..5a6709c35 100644 --- a/docs/work/present/Plan-20261011 - MoonLive Coord3D and RGB types.md +++ b/docs/work/present/Plan-20261011 - MoonLive Coord3D and RGB types.md @@ -6,30 +6,35 @@ Most of what a script handles is a position or a color, and today both are loose The vocabulary follows the compiled modules ([MoonLive architecture](../../explanation/architecture/moonlive.md)): `Coord3D` and `RGB` are the C++ types of the same names, and every builtin taking or returning one is a one-line call into the `draw` function that does the same for a compiled effect. -## The design: fields under one name +## The design: a struct is a named fixed-size array -The compiler treats a struct as its fields under one name: `RGB c` is three byte slots `c.r`, `c.g` and `c.b`, and `Coord3D p` is `p.x`, `p.y` and `p.z`. The backends never see a struct, so none of the five changes. +`RGB c` is `byte c[3]` and `Coord3D p` is `int p[3]`, with `r g b` and `x y z` naming elements 0, 1 and 2. A member struct reuses the member arrays MoonLive already has, packed at element width, so an `RGB` member takes 3 bytes in one member record; a local struct takes three frame slots. The backends never see a struct: every use expands into the scalar operations a script writes today. -- **A struct argument expands into its fields.** `setRGB(i, c)` is `setRGB(i, c.r, c.g, c.b)`, the existing four-argument builtin, so no overload is needed and no shipped script breaks. -- **A struct return goes through output arguments.** `getRGB(i)` writes its three fields through the by-reference arguments builtins already support; a script function returning a struct gets the same hidden output slots. -- **A struct is a full type:** a class member, a local, a parameter and a return value, with field access, assignment of a whole struct, and a constructor expression. +- **A struct argument expands into its fields.** `setRGB(i, c)` is `setRGB(i, c.r, c.g, c.b)`, so the existing builtins take structs with no overload. +- **A struct return goes through output words.** A builtin writes its fields into its argument slots, which the parser reloads; a script function returning a struct uses the script-argument block, free after the callee's entry copy. +- **A struct is a full type:** a member, a local, a parameter and a return value, with field access, whole-struct assignment, and a constructor expression `RGB(r, g, b)`. ## The steps -1. The two types in the parser and the symbol table: declarations, field access, whole-struct assignment, the constructor expression. -2. Expansion into fields for every use, builtin arguments and returns included. -3. The color builtins in struct form: `getRGB(i)` returning an `RGB`, and the existing color calls taking one through expansion. -4. The two limits a struct meets: four parameters per script function, which one `Coord3D` mostly fills, and the 32 frame slots, of which each struct takes three. -5. Tests: the compiler on both types, scripts using them on every backend, the shipped scripts' code byte-identical, and the vocabulary page. -6. One compiler trap from hpwit's new parser that applies here, while the work is in the same compiler: a variable declared before a loop, used inside it and after it, run on every backend, so the register allocator keeps it alive across the loop's back edge. His two other traps (an optimizer pass, a code-size estimate) have no counterpart in MoonLive. +1. A byte-identical harness first: every shipped script's IR, with call targets named rather than addressed, recorded before any compiler change and compared after each. +2. The two types: declarations, field access, whole-struct assignment, the constructor; members as named arrays, locals as three slots. +3. Struct arguments expanding into fields; struct returns through output words, and `paletteRGB(idx, bri)` returning an `RGB` in one call where effects now make three. +4. Script functions: parameters raised from 4 to 8 words (16 more arena bytes per running script), a struct parameter taking three, and struct returns. +5. `&&`, `||` and `!` in conditions, short-circuit, lowered to the branches `if` and `while` already use; as a value they would need a set-on-condition operation and stay out. +6. `abs`, `min`, `max` and `clamp` as common builtins. +7. The assemblers' label and fixup tables off the stack and onto the compile-time heap, with the limits raised (IR labels 40 to 96, fixups 96 to 255), since each `||`, `!` and `!=` takes a label and two shipped scripts already hit "assembler overflow". +8. A clear error at every limit a struct or operator meets. +9. Tests: the compiler on every addition, scripts using them on every backend, the IR sizing against a struct-dense script, hpwit's loop-variable trap (a variable declared before a loop, used in it and after it, run on every backend), and the vocabulary page. +10. The catalog scripts rewritten where the new forms read better, each compiled and run on the desktop, the harness then recording their new IR. ## Cost | | Flash | Memory | Hot path | |--|--|--|--| -| Compiler | The type and expansion code in the front end | None at run time | None: the emitted code is what the expanded scalar form emits today | -| A script using them | Nothing new | Three slots or member bytes per struct, as three scalars take | Unchanged | +| Compiler | Front end only, a few KB once per firmware, nothing in the five backends | Compile time: struct uses expand to three IR ops each, so the IR buffer sizing is rechecked; label tables move to the heap | None: the emitted code is what the expanded scalar form emits today | +| A script | Unchanged code for unchanged scripts | 16 bytes more arena per running script for the 8 parameter words; an `RGB` member 3 bytes, a `Coord3D` 12 | `paletteRGB` replaces three palette calls with one per light | +| Builtins | Four small common builtins and `paletteRGB` | None | A call each, as any builtin | ## Risk -A predefined type name joins the one namespace every script shares, so a script with a variable named `RGB` or `Coord3D` stops compiling; the compile-every-script test finds any such script at once. +A predefined type or builtin name joins the one namespace every script shares, so a user script with a variable named `RGB`, `Coord3D`, `abs`, `min`, `max` or `clamp` stops compiling with "that name is a type" or "that name shadows a built-in function"; no shipped script does, and the compile-every-script test finds any that would. diff --git a/esp32/main/CMakeLists.txt b/esp32/main/CMakeLists.txt index 391660095..a6c629761 100644 --- a/esp32/main/CMakeLists.txt +++ b/esp32/main/CMakeLists.txt @@ -56,6 +56,9 @@ idf_component_register( target_compile_options(${COMPONENT_LIB} PRIVATE -Wall -Wextra -Werror) +# The script compiler runs once per script load, never per frame, so it is built for size: the code it emits is the same either way. +set_source_files_properties("../../src/core/moonlive/MoonLiveCompiler.cpp" PROPERTIES COMPILE_OPTIONS "-Os") + # Xtensa: interleave literal pools into the text section (-mauto-litpools). Required by MM_RAMFUNC # (IRAM_ATTR) on TEMPLATE functions: a comdat instantiation's code lands in `.iram1.*` but its literal # pool in `.literal._Zโ€ฆ`, which the IDF linker script keeps in flash โ€” out of l32r's backward reach diff --git a/moondeck/check/check_devices.py b/moondeck/check/check_devices.py index b613f307b..b7236770f 100644 --- a/moondeck/check/check_devices.py +++ b/moondeck/check/check_devices.py @@ -212,7 +212,8 @@ def main(): # --- chip: the family the installer detects and the manifests name, and the one every firmware installs onto --- chip = e.get("chip") families = set(TARGET_TO_FAMILY.values()) - if "chip" in e and chip not in families: + # A string first: a list or an object is unhashable, and the set membership test would raise rather than report. + if "chip" in e and (not isinstance(chip, str) or chip not in families): errors.append(f"{where}: chip {chip!r} is not a known family {sorted(families)}") elif isinstance(fws, list): for fw in fws: diff --git a/mooninstaller/install-orchestrator.js b/mooninstaller/install-orchestrator.js index 4ff900f49..6a343ad68 100644 --- a/mooninstaller/install-orchestrator.js +++ b/mooninstaller/install-orchestrator.js @@ -77,6 +77,7 @@ import { isTeensySerial, rebootToBootloader, rebootedSerialPort, + otherTeensyPorts, requestHalfKay, writeImage, } from "./teensy-writer.js"; @@ -479,6 +480,8 @@ async function installTeensy({ manifest, port, device, applyDefaults, eraseBefor // A board held in its bootloader has no serial port: the bootloader is the target from the start. let halfkay = port ? null : await bootloader(true); + // Before the reboot, while the installed board still answers on `port` (or on none), so its port after the reboot is the one not here. + const others = await otherTeensyPorts(port); trackProgress("fetch-firmware"); log(`downloading ${manifest.parts[0].url}`); @@ -502,7 +505,7 @@ async function installTeensy({ manifest, port, device, applyDefaults, eraseBefor trackProgress("reboot"); let ip = "", mdns = "", defaultsApplied = false; - let serial = await rebootedSerialPort(15000); + let serial = await rebootedSerialPort(15000, others); if (!serial && uiFindBoard) { // The board was installed from its bootloader, so the installer may not read its serial port yet: one click grants it. log("no granted serial port for the board: asking for one"); diff --git a/mooninstaller/install.js b/mooninstaller/install.js index bd931e058..d9a16e16e 100644 --- a/mooninstaller/install.js +++ b/mooninstaller/install.js @@ -903,8 +903,9 @@ document.addEventListener('DOMContentLoaded', () => { } // A Teensy with no serial port (a crashed program, another USB type) is not in the port list: after a canceled pick, the status line offers its bootloader instead, which WebHID reaches once the program button is pressed. + // Every canceled pick offers it, an earlier port included, since a cancel means the board the user looks for is missing from the list. function offerTeensyBootloader() { - if (!("hid" in navigator) || pickedPort || teensyBootMode) return; + if (!("hid" in navigator) || teensyBootMode) return; const status = document.getElementById("detect-status"); const link = document.createElement("a"); link.href = "#"; diff --git a/mooninstaller/teensy-writer.js b/mooninstaller/teensy-writer.js index a4351de4c..7f98340f1 100644 --- a/mooninstaller/teensy-writer.js +++ b/mooninstaller/teensy-writer.js @@ -153,12 +153,19 @@ export async function requestTeensySerial() { return await navigator.serial.requestPort({ filters: [{ usbVendorId: PJRC_VID, usbProductId: TEENSY_SERIAL_PID }] }); } -// The board's serial port after the reboot, which re-enumerates; null when none of this browser's granted ports appears in `waitMs`. -export async function rebootedSerialPort(waitMs) { +// The granted Teensy serial ports of every board but the one being installed, noted before the install. +export async function otherTeensyPorts(installing) { + const ports = await navigator.serial.getPorts(); + return ports.filter(p => p !== installing && isTeensySerial(p.getInfo())); +} + +// The installed board's serial port after the reboot, which re-enumerates; null when none appears in `waitMs`. +// A port reports only its USB ids, which every Teensy shares, so the board is the Teensy port that is none of `others`: another granted board is never read or sent defaults. +export async function rebootedSerialPort(waitMs, others = []) { const deadline = Date.now() + waitMs; for (;;) { const ports = await navigator.serial.getPorts(); - const port = ports.find(p => isTeensySerial(p.getInfo())); + const port = ports.find(p => isTeensySerial(p.getInfo()) && !others.includes(p)); if (port) return port; if (Date.now() >= deadline) return null; await sleep(500); diff --git a/moonlive/effects/breathe.mle b/moonlive/effects/breathe.mle index 7433af660..28b8ee764 100644 --- a/moonlive/effects/breathe.mle +++ b/moonlive/effects/breathe.mle @@ -20,6 +20,6 @@ class BreatheEffect { void tick() { int bri = floorBri + div(beatsin(bpm, t, 255) * (255 - floorBri), 255); int p = hue + scale(beat(div(bpm, 2) + 1, t), drift + 1); - fill(paletteR(p, bri), paletteG(p, bri), paletteB(p, bri)); + fill(paletteRGB(p, bri)); } } diff --git a/moonlive/effects/chase.mle b/moonlive/effects/chase.mle index d65213f83..04d2718f2 100644 --- a/moonlive/effects/chase.mle +++ b/moonlive/effects/chase.mle @@ -28,7 +28,7 @@ class ChaseEffect { if (d < 0) { d = d + n; } if (d < spread) { int bri = 255 - div(d * tail, spread); - setRGB(i, paletteR(hue + d, bri), paletteG(hue + d, bri), paletteB(hue + d, bri)); + setRGB(i, paletteRGB(hue + d, bri)); } } } diff --git a/moonlive/effects/ember.mle b/moonlive/effects/ember.mle index a589e34d6..da1006a88 100644 --- a/moonlive/effects/ember.mle +++ b/moonlive/effects/ember.mle @@ -18,7 +18,7 @@ class EmberEffect { void tick() { for (int i = 0; i < 16; i = i + 1) { - if (heat[i] > cool) { heat[i] = heat[i] - cool; } else { heat[i] = 0; } + heat[i] = max(heat[i] - cool, 0); } for (int j = 0; j < 16; j = j + 1) { @@ -26,8 +26,7 @@ class EmberEffect { } for (int k = 0; k < 16; k = k + 1) { - setRGB(k, paletteR(heat[k], heat[k]), paletteG(heat[k], heat[k]), - paletteB(heat[k], heat[k])); + setRGB(k, paletteRGB(heat[k], heat[k])); } } } diff --git a/moonlive/effects/glow.mle b/moonlive/effects/glow.mle index ebeb8a9e4..aa8524853 100644 --- a/moonlive/effects/glow.mle +++ b/moonlive/effects/glow.mle @@ -25,18 +25,18 @@ class GlowEffect { void tick() { int now = div((audioBand(0) + audioBand(1) + audioBand(2) + audioBand(3)) * gain, 400); - if (now > 255) { now = 255; } + now = min(now, 255); if (now * 16 > bass) { bass = bass + div(now * 16 - bass, 2); } else { bass = bass - div((bass - now * 16) * decay + 99, 100); } avg = avg + div(now * 16 - avg, 32); int target = floorBri + div(div(bass, 16) * (255 - floorBri), 255); int kick = 0; - if (now > 40) { if (now * 16 > avg + div(avg, 2)) { kick = 1; } } - if (kick > kicking) { bri = bri + flare; } + if (now > 40 && now * 16 > avg + div(avg, 2)) { kick = 1; } + if (kick && !kicking) { bri = bri + flare; } kicking = kick; if (bri > target) { bri = bri - div((bri - target) * decay + 99, 100); } else { bri = target; } - if (bri > 255) { bri = 255; } + bri = min(bri, 255); int low = 0; int most = 0; @@ -47,6 +47,6 @@ class GlowEffect { hue = hue + div((low * 24 + div(t, 400)) * 16 - hue, 64); int p = mod(div(hue, 16), 256); - fill(paletteR(p, bri), paletteG(p, bri), paletteB(p, bri)); + fill(paletteRGB(p, bri)); } } diff --git a/moonlive/effects/pulse.mle b/moonlive/effects/pulse.mle index f56ca022c..0665013cf 100644 --- a/moonlive/effects/pulse.mle +++ b/moonlive/effects/pulse.mle @@ -19,14 +19,11 @@ class PulseEffect { } void tick() { - if (audioBeat() > 0) { + if (audioBeat()) { lit = 255; hue = hue + hueStep; } - if (lit > decay) { lit = lit - decay; } - else { lit = 0; } - - if (lit < floorBri) { lit = floorBri; } - fill(paletteR(hue, lit), paletteG(hue, lit), paletteB(hue, lit)); + lit = max(lit - decay, floorBri); + fill(paletteRGB(hue, lit)); } } diff --git a/moonlive/effects/sparkle.mle b/moonlive/effects/sparkle.mle index e43cdc383..3180588d1 100644 --- a/moonlive/effects/sparkle.mle +++ b/moonlive/effects/sparkle.mle @@ -19,9 +19,8 @@ class SparkleEffect { fade(fadeAmt); int n = width * height * depth; for (int i = 0; i < density; i = i + 1) { - int p = random16(256); - if (p > hueSpread) { p = hueSpread; } - setRGB(random16(n), paletteR(p, 255), paletteG(p, 255), paletteB(p, 255)); + int p = min(random16(256), hueSpread); + setRGB(random16(n), paletteRGB(p, 255)); } } } diff --git a/moonlive/effects/spectrum.mle b/moonlive/effects/spectrum.mle index b3192b291..106bd9376 100644 --- a/moonlive/effects/spectrum.mle +++ b/moonlive/effects/spectrum.mle @@ -22,21 +22,15 @@ class SpectrumEffect { if (width < 2) { bars = height; } for (int x = 0; x < bars; x = x + 1) { int b = div(x * 16, bars); - int mag = div(audioBand(b) * gain, 100); - if (mag > 255) { mag = 255; } - if (width < 2) { - setRGB(x, div(paletteR(b * 16, 255) * mag, 255), div(paletteG(b * 16, 255) * mag, 255), - div(paletteB(b * 16, 255) * mag, 255)); - } + int mag = min(div(audioBand(b) * gain, 100), 255); + RGB c = paletteRGB(b * 16, 255); + if (width < 2) { setRGB(x, div(c.r * mag, 255), div(c.g * mag, 255), div(c.b * mag, 255)); } int top = div(mag * height, 256); if (width < 2) { top = 0; } for (int y = 0; y < top; y = y + 1) { - setRGB((height - 1 - y) * width + x, paletteR(b * 16, 255), paletteG(b * 16, 255), - paletteB(b * 16, 255)); - } - if (peakHold > 0) { - if (top > 0) { setRGB((height - top) * width + x, 255, 255, 255); } + setRGB((height - 1 - y) * width + x, c); } + if (peakHold && top > 0) { setRGB((height - top) * width + x, 255, 255, 255); } } } } diff --git a/src/core/moonlive/MoonLiveBuiltins.h b/src/core/moonlive/MoonLiveBuiltins.h index 3f759b5d9..d737cb581 100644 --- a/src/core/moonlive/MoonLiveBuiltins.h +++ b/src/core/moonlive/MoonLiveBuiltins.h @@ -69,6 +69,26 @@ struct Builtin { uint8_t fixedArgs = 0; /// Whether the result is fixed-point. bool fixedReturn = false; + /// The struct this builtin answers with, by name, written into its first argument slots; null for a scalar or no result. + const char* retStruct = nullptr; + /// Whether the arguments may be all whole or all fixed, with the result following them, as `min` and `abs` are. + bool sameTypeArgs = false; +}; + +/// Most fields a struct type carries: three for a color or a coordinate, four for a color with white. +static constexpr uint8_t kMaxStructFields = 4; + +// Host-registered like the builtins, so the core compiler names no domain's types. +/// A struct type a script can declare: a fixed number of fields of one scalar type. +struct StructDef { + /// The type name a script writes, such as `RGB`. + const char* name = nullptr; + /// The scalar type every field has. + CtrlType type = CtrlType::Int; + /// How many fields it has. + uint8_t fieldCount = 0; + /// The field names, in storage order. + const char* fields[kMaxStructFields] = {}; }; // An overflow otherwise surfaces as "unknown function" for a builtin that plainly exists. @@ -93,6 +113,32 @@ struct BuiltinTable { uint8_t count = 0; /// Set when an `add` was dropped for lack of room. bool overflowed = false; + /// How many struct types one table defines. + static constexpr uint8_t kMaxStructs = 4; + /// The struct types a script may declare. + StructDef structs[kMaxStructs]; + /// How many struct types are registered. + uint8_t structCount = 0; + + /// Register a struct type, returning false when the table is full or the type is malformed. + bool addStruct(const StructDef& s) { + if (!s.name || s.fieldCount == 0 || s.fieldCount > kMaxStructFields) return false; + for (uint8_t f = 0; f < s.fieldCount; f++) if (!s.fields[f]) return false; // a field lookup reads every name + if (structCount >= kMaxStructs) { overflowed = true; return false; } + structs[structCount++] = s; + return true; + } + + /// The struct type registered under `name`, or null when there is none. + const StructDef* findStruct(const char* name, size_t len) const { + for (uint8_t i = 0; i < structCount; i++) { + const char* n = structs[i].name; + size_t j = 0; + for (; j < len && n[j]; j++) if (n[j] != name[j]) break; + if (j == len && n[j] == 0) return &structs[i]; + } + return nullptr; + } /// Register one builtin, returning false when the table is full. bool add(const Builtin& b) { @@ -150,7 +196,7 @@ static constexpr uint8_t kMaxCallDepth = 32; // The arena rather than the frame, since a callee copies its arguments out as its first act. /// Where a script call's arguments are passed. -static constexpr uint8_t kMaxScriptArgs = 4; // per call; a helper wanting more wants an array +static constexpr uint8_t kMaxScriptArgs = 8; // words per call, a struct taking one per field static constexpr uint8_t kScriptArgBytes = 4; // one machine word, as a frame slot is /// Where the argument block starts, aligned to four bytes because these are 32-bit slots. static constexpr uint8_t kScriptArgBase = static_cast((kDepthSlot + 1 + 3) & ~3); diff --git a/src/core/moonlive/MoonLiveBuiltins_common.h b/src/core/moonlive/MoonLiveBuiltins_common.h index 06c8c03b0..4571c730a 100644 --- a/src/core/moonlive/MoonLiveBuiltins_common.h +++ b/src/core/moonlive/MoonLiveBuiltins_common.h @@ -203,6 +203,28 @@ extern "C" inline uint32_t mm_ml_addSelect(const uintptr_t* args, uint32_t, cons return 0; } +// Signed, and each works on fixed values too, since Q16.16 keeps the order and the sign of a whole number. +/// The magnitude of a value, widened so the most negative int does not overflow. +extern "C" inline uint32_t mm_ml_abs(const uintptr_t* args, uint32_t, const uint8_t*) { + const int64_t v = signedArg(args[0]); + return static_cast(v < 0 ? -v : v); +} +/// The smaller of two values. +extern "C" inline uint32_t mm_ml_min(const uintptr_t* args, uint32_t, const uint8_t*) { + const int32_t a = signedArg(args[0]), b = signedArg(args[1]); + return static_cast(a < b ? a : b); +} +/// The larger of two values. +extern "C" inline uint32_t mm_ml_max(const uintptr_t* args, uint32_t, const uint8_t*) { + const int32_t a = signedArg(args[0]), b = signedArg(args[1]); + return static_cast(a > b ? a : b); +} +/// A value held between a low and a high bound. +extern "C" inline uint32_t mm_ml_clamp(const uintptr_t* args, uint32_t, const uint8_t*) { + const int32_t v = signedArg(args[0]), lo = signedArg(args[1]), hi = signedArg(args[2]); + return static_cast(v < lo ? lo : (v > hi ? hi : v)); +} + // Both vocabularies call this first and then add their own, so a name means one thing everywhere. /// Register the neutral builtins into whatever table asks. inline void addCommonBuiltins(BuiltinTable& t) { @@ -230,14 +252,27 @@ inline void addCommonBuiltins(BuiltinTable& t) { // addSelect declares a dropdown: a name, the member, and its option names in one quoted "a|b|c". t.add({"addSelect", 3, /*returns*/ false, BuiltinKind::Call, &mm_ml_addSelect, {}, /*byRef*/ 0x2, /*byStr*/ 0x5}); + // The arithmetic a shape needs, whole or fixed: the arguments agree and the result follows them. + for (const Builtin& b : {Builtin{"abs", 1, true, BuiltinKind::Call, &mm_ml_abs, {}}, + Builtin{"min", 2, true, BuiltinKind::Call, &mm_ml_min, {}}, + Builtin{"max", 2, true, BuiltinKind::Call, &mm_ml_max, {}}, + Builtin{"clamp", 3, true, BuiltinKind::Call, &mm_ml_clamp, {}}}) { + Builtin same = b; + same.sameTypeArgs = true; + t.add(same); + } } // Run once after a successful compile, where a compiled module's defineControls() sits. /// The entry a binding runs so the controls a script declares exist. inline constexpr const char* kEntryDefineControls = "defineControls"; -/// What a binding attaches around a defineControls run, for the sizing its own builtins ask for. -using DefineHook = void (*)(void* ctx, bool attach); +// Before the compile, so the last program's buffers are gone before the next one needs memory. +/// The moments a binding is told about: a compile starting, and the defineControls run that sizes the new program's buffers. +enum class DefinePhase : uint8_t { Compiling, Attach, Detach }; + +/// What a binding does at each of those moments, for the sizing its own builtins ask for. +using DefineHook = void (*)(void* ctx, DefinePhase phase); // Re-runnable like its compiled counterpart, since the declared list is cleared first. /// Run a script's defineControls, so the controls it declares exist. @@ -248,12 +283,12 @@ inline void runDefineControls(MoonLive& engine, DefineHook hook = nullptr, void* int32_t lo, int32_t hi, CtrlType type, const char* options) { static_cast(ctx)->addDeclaredControl(n, off, lo, hi, type, options); }, &engine)) return; - if (hook) hook(hookCtx, true); + if (hook) hook(hookCtx, DefinePhase::Attach); engine.clearDeclaredControls(); // re-runnable: rebuild rather than append // This entry point writes no pixels, but `run` refuses a null or undersized buffer. uint8_t scratch[3] = {}; engine.run(scratch, 1, 3, 0, kEntryDefineControls); - if (hook) hook(hookCtx, false); + if (hook) hook(hookCtx, DefinePhase::Detach); setAddControlSink(nullptr, nullptr); } diff --git a/src/core/moonlive/MoonLiveCompiler.cpp b/src/core/moonlive/MoonLiveCompiler.cpp index ea5231dc3..a475e305c 100644 --- a/src/core/moonlive/MoonLiveCompiler.cpp +++ b/src/core/moonlive/MoonLiveCompiler.cpp @@ -17,7 +17,7 @@ namespace { /// Every token the grammar has. enum class Tok { Ident, Number, String, Assign, LParen, RParen, LBrace, RBrace, Comma, Semicolon, Plus, Minus, Star, Slash, Percent, Less, LessEq, Greater, GreaterEq, EqEq, NotEq, - LBracket, RBracket, End, Error }; + LBracket, RBracket, Dot, AndAnd, OrOr, Not, End, Error }; /// One token at a time over the source, with no buffer of its own. struct Lexer { @@ -96,6 +96,11 @@ struct Lexer { if (c == '>' && p[1] == '=') { p += 2; kind = Tok::GreaterEq; return; } if (c == '=' && p[1] == '=') { p += 2; kind = Tok::EqEq; return; } if (c == '!' && p[1] == '=') { p += 2; kind = Tok::NotEq; return; } + if (c == '&' && p[1] == '&') { p += 2; kind = Tok::AndAnd; return; } + if (c == '|' && p[1] == '|') { p += 2; kind = Tok::OrOr; return; } + if (c == '!') { p++; kind = Tok::Not; return; } + // A field access; a number's own point is read with the number, which starts with a digit. + if (c == '.') { p++; kind = Tok::Dot; return; } if (c == '=') { p++; kind = Tok::Assign; return; } if (c == '(') { p++; kind = Tok::LParen; return; } if (c == ')') { p++; kind = Tok::RParen; return; } @@ -142,6 +147,13 @@ struct Lexer { // --- Parser to IR -------------------------------------------------------------------- // It knows the grammar and resolves call names against the injected table, owning no function name. +/// What a condition operator met where a value is wanted says. +constexpr const char* kJoinsConditions = "&&, || and ! join the conditions of an if, not values"; +/// What a whole struct where one value belongs says. +constexpr const char* kOneFieldAtATime = "a struct is read one field at a time: write name.field"; +/// What a comparison after a negated value says. +constexpr const char* kNegatesWhatFollows = "! negates what follows it: write !(a < b) to negate a comparison"; + /// Recursive descent, evaluating each expression into a virtual register and emitting IR. struct Parser { Lexer& lex; ///< the token stream being parsed @@ -155,11 +167,16 @@ struct Parser { uint8_t nameLen; ///< how long that span is uint16_t irStart; ///< the IR index its body starts at RetType ret; ///< what it declares it returns - uint8_t params; }; ///< how many arguments it declares + uint8_t params; ///< how many argument words it declares + uint8_t retStruct; }; ///< the struct it returns, as its table index plus one, or 0 FnMark fns[kMaxEntryPoints] = {}; ///< the functions parsed so far uint8_t fnCount = 0; ///< how many of them there are /// What the function being parsed returns, so a `return` is checked where it is written. RetType curRet = RetType::Void; + /// The struct it returns, when it returns one. + const StructDef* curRetStruct = nullptr; + /// Each argument word's type, so the entry copy narrows a byte field as it narrows a byte parameter. + CtrlType paramWordType[kMaxScriptArgs] = {}; VReg nextTemp = kFirstTemp; ///< the high-water mark, also vregsUsed VReg freeStack[kMaxVRegs] = {}; ///< recycled temps, so a dead vreg is reused uint8_t freeCount = 0; ///< how many are on that stack @@ -167,8 +184,9 @@ struct Parser { /// A script-local variable, such as a `for` counter, which the script writes. struct Local { const char* name; ///< its name, a span into the source size_t nameLen; ///< how long that span is - uint8_t slot; ///< the frame slot holding it - CtrlType type; }; ///< its declared type, which carries the scaling + uint8_t slot; ///< the frame slot holding it, the first field's for a struct + CtrlType type; ///< its declared type, a struct's field type + uint8_t st = 0; }; ///< its struct type, as its table index plus one, or 0 for a scalar Local locals[kMaxLocals] = {}; ///< the locals currently in scope uint8_t localCount = 0; ///< how many of them there are uint8_t slotHighWater = 0; ///< slots currently in scope @@ -180,9 +198,12 @@ struct Parser { uint16_t stringCap = 0; ///< its capacity uint16_t stringLen = 0; ///< how much of it is used DeclaredControl members[kMaxCtrls] = {}; ///< the members declared so far + uint8_t memberStruct[kMaxCtrls] = {}; ///< a member's struct type, as its table index plus one, or 0 // No AST, so the type rides alongside the register and every parse function sets it. /// Whether the value last parsed is Q16.16 rather than a plain integer. bool exprIsFixed = false; + /// Whether a same-type builtin's arguments, last parsed, were fixed, which its result follows. + bool argsFixed = false; // A literal converts by patching its Const, since its meaning is visible at the call site. /// The Const op index when the value last parsed is one integer literal, else -1. int exprLitConst = -1; @@ -244,6 +265,198 @@ struct Parser { return -1; } + // --- Structs: a host-registered type is its fields under one name, a named array --- + + // A byte rather than a pointer in every local, member and function record, since the parser lives on the compiling task's stack. + /// The struct a stored index names, or null for 0. + const StructDef* structAt(uint8_t idx) const { return idx ? &table.structs[idx - 1] : nullptr; } + /// The index a struct is stored as: its place in the table plus one, or 0 for none. + uint8_t structIndex(const StructDef* st) const { return st ? static_cast(st - table.structs + 1) : 0; } + + /// A field's index in a struct, or -1. + static int fieldIndex(const StructDef& st, const char* n, size_t len) { + for (uint8_t f = 0; f < st.fieldCount; f++) + if (std::strlen(st.fields[f]) == len && std::strncmp(st.fields[f], n, len) == 0) return f; + return -1; + } + + /// Parse `.field` after a struct's name, answering the field's index, or -1 having failed. + int parseField(const StructDef& st) { + if (!expect(Tok::Dot, kOneFieldAtATime)) return -1; + if (lex.kind != Tok::Ident) { fail("expected a field name after '.'"); return -1; } + const int f = fieldIndex(st, lex.identBeg, lex.identLen); + if (f < 0) { fail("this struct has no field of that name"); return -1; } + lex.advance(); + return f; + } + + /// The arena byte of a struct member's field. + uint8_t memberFieldOffset(int mi, int f) const { + return static_cast(members[mi].offset + f * ctrlWidth(members[mi].type)); + } + + /// Load one field of a struct member, at its own width. + VReg loadMemberField(int mi, int f) { + VReg v = alloc(); + emit({ctrlWidth(members[mi].type) == 1 ? IrOp::LoadCtrl : IrOp::LoadCtrl32, v, 0,0,0,0, + memberFieldOffset(mi, f), nullptr, {}}); + return v; + } + + // Peeks without consuming, so a caller can ask before it commits to a scalar or a struct. + /// The struct type the expression at the cursor has as a whole value, or null for a scalar. + const StructDef* structValueAt() const { + if (lex.kind != Tok::Ident) return nullptr; + Lexer probe = lex; + probe.advance(); + // A call or constructor is followed past its parentheses, so `getRGB(i).r` reads as the scalar it is. + if (probe.kind == Tok::LParen) { + // A lexer error does not move past its character, so it ends the probe as the end does. + for (int depth = 0; probe.kind != Tok::End && probe.kind != Tok::Error; ) { + if (probe.kind == Tok::LParen) depth++; + if (probe.kind == Tok::RParen && --depth == 0) { probe.advance(); break; } + probe.advance(); + } + } + if (probe.kind == Tok::Dot) return nullptr; // `c.r` is one field, a scalar + if (const StructDef* st = table.findStruct(lex.identBeg, lex.identLen)) return st; // a constructor + const int li = findLocal(lex.identBeg, lex.identLen); + if (li >= 0) return structAt(locals[li].st); + const int mi = findMember(lex.identBeg, lex.identLen); + if (mi >= 0) return structAt(memberStruct[mi]); + if (const Builtin* b = table.find(lex.identBeg, lex.identLen)) + return b->retStruct ? table.findStruct(b->retStruct, std::strlen(b->retStruct)) : nullptr; + for (uint8_t i = 0; i < fnCount; i++) + if (fns[i].nameLen == lex.identLen && std::strncmp(fns[i].name, lex.identBeg, lex.identLen) == 0) + return structAt(fns[i].retStruct); + return nullptr; + } + + /// The struct a call or constructor at the cursor answers, read through a following `.field`. + const StructDef* structCallAt() const { + if (lex.kind != Tok::Ident) return nullptr; + if (const StructDef* st = table.findStruct(lex.identBeg, lex.identLen)) return st; + if (const Builtin* b = table.find(lex.identBeg, lex.identLen)) + return b->retStruct ? table.findStruct(b->retStruct, std::strlen(b->retStruct)) : nullptr; + for (uint8_t i = 0; i < fnCount; i++) + if (fns[i].nameLen == lex.identLen && std::strncmp(fns[i].name, lex.identBeg, lex.identLen) == 0) + return structAt(fns[i].retStruct); + return nullptr; + } + + // Every source of a struct value lands in registers, one per field, so every destination stores the same way. + /// Parse a whole struct value of type `st` into one register per field. + bool parseStructValue(const StructDef& st, VReg out[kMaxStructFields], bool thenField = false) { + const StructDef* got = thenField ? structCallAt() : structValueAt(); + if (got != &st) { + fail(got ? "this is a different struct type" + : "expected a struct value: a constructor, a struct variable or a call answering one"); + return false; + } + if (table.findStruct(lex.identBeg, lex.identLen)) { // `RGB(r, g, b)`: one expression per field + lex.advance(); + if (!expect(Tok::LParen, "expected '(' after the struct type")) return false; + for (uint8_t f = 0; f < st.fieldCount; f++) { + if (f > 0 && !expect(Tok::Comma, "expected ',' between the fields")) return false; + out[f] = parseExpr(); + if (failed) return false; + if (exprIsFixed) { fail("a struct field is a whole number: write toInt(x)"); return false; } + if (exprLitConst >= 0 && !fitsInType(ir.ops[exprLitConst].imm, st.type)) + { fail("this number is out of range for that field"); return false; } + narrowToType(out[f], st.type); + } + if (lex.kind == Tok::Comma) { fail("too many fields for this struct"); return false; } + return expect(Tok::RParen, "expected ')' to close the struct"); + } + const int li = findLocal(lex.identBeg, lex.identLen); + if (li >= 0) { + lex.advance(); + for (uint8_t f = 0; f < st.fieldCount; f++) { + out[f] = alloc(); + emit({IrOp::Reload, out[f], 0,0,0,0, locals[li].slot + f, nullptr, {}}); + } + return true; + } + const int mi = findMember(lex.identBeg, lex.identLen); + if (mi >= 0) { + lex.advance(); + for (uint8_t f = 0; f < st.fieldCount; f++) out[f] = loadMemberField(mi, f); + return true; + } + if (const Builtin* b = table.find(lex.identBeg, lex.identLen)) { + // The builtin writes its fields into its first argument slots, which are reserved even past its arguments. + lex.advance(); + if (!expect(Tok::LParen, "expected '(' after the function name")) return false; + const uint8_t argBase = slotHighWater; + uint8_t n = 0; + if (!parseBuiltinArgs(b, n)) return false; + if (argBase + st.fieldCount > kMaxLocals) { fail("too many arguments to hold"); return false; } + if (slotHighWater < argBase + st.fieldCount) slotHighWater = static_cast(argBase + st.fieldCount); + if (slotHighWater > slotsUsed) slotsUsed = slotHighWater; + VReg r = alloc(); + emit({IrOp::Call, r, 0, n, 0, 0, argBase, b->fn, {}}); + freeTemp(r); + for (uint8_t f = 0; f < st.fieldCount; f++) { + out[f] = alloc(); + emit({IrOp::Reload, out[f], 0,0,0,0, argBase + f, nullptr, {}}); + } + slotHighWater = argBase; + return true; + } + // A script function answering a struct leaves its fields in the argument block, free once the callee copied its own. + for (uint8_t i = 0; i < fnCount; i++) { + if (fns[i].nameLen != lex.identLen || std::strncmp(fns[i].name, lex.identBeg, lex.identLen) != 0) continue; + lex.advance(); + if (!expect(Tok::LParen, "expected '(' after the function name")) return false; + if (!stageScriptArgs(i)) return false; + emit({IrOp::CallScript, 0, 0,0,0,0, static_cast(i), nullptr, {}}); + for (uint8_t f = 0; f < st.fieldCount; f++) { + out[f] = alloc(); + emit({IrOp::LoadCtrl32, out[f], 0,0,0,0, scriptArgOffset(f), nullptr, {}}); + } + return true; + } + fail("expected a struct value"); + return false; + } + + // Stored once all are evaluated, since a later argument may call through the same block. + /// Parse a script call's arguments through its ')' into the arena argument block, a struct taking one word per field. + bool stageScriptArgs(uint8_t i) { + const uint8_t want = fns[i].params; + VReg staged[kMaxScriptArgs] = {}; + uint8_t got = 0, args = 0; + while (lex.kind != Tok::RParen && lex.kind != Tok::End) { + if (args > 0 && !expect(Tok::Comma, "expected ',' between arguments")) return false; + VReg vals[kMaxStructFields] = {}; + uint8_t count = 1; + if (const StructDef* st = structValueAt()) { + if (!parseStructValue(*st, vals)) return false; + count = st->fieldCount; + } else { + vals[0] = parseExpr(); + if (failed) return false; + } + for (uint8_t f = 0; f < count; f++) { + if (got < want && got < kMaxScriptArgs) staged[got] = vals[f]; + else freeTemp(vals[f]); + got++; + } + args++; + } + if (!expect(Tok::RParen, "expected ')' to close the arguments")) return false; + const uint8_t staging = got < want ? got : want; + for (uint8_t a = 0; a < staging && a < kMaxScriptArgs; a++) { + emit({IrOp::StoreCtrl32, 0, staged[a], 0,0,0, scriptArgOffset(a), nullptr, {}}); + freeTemp(staged[a]); + } + if (got != want) { + fail(got < want ? "too few arguments for this function" : "too many arguments for this function"); + return false; + } + return true; + } + // The textbook tree-walk register stack, so a multi-call statement fits the register file. /// Hand out a recycled virtual register, or a fresh one. VReg alloc() { @@ -262,9 +475,10 @@ struct Parser { /// Append an IR op, failing the compile when the program is full or the vreg is out of budget. void emit(const IrInst& i) { if (!ir.push(i)) fail("script too large"); } + // A condition consumes its own && and ||, so one met here stands where a value was wanted. /// Consume the expected token, or fail with a message. bool expect(Tok t, const char* msg) { - if (lex.kind != t) { fail(msg); return false; } + if (lex.kind != t) { fail(lex.kind == Tok::AndAnd || lex.kind == Tok::OrOr ? kJoinsConditions : msg); return false; } lex.advance(); return true; } @@ -388,35 +602,125 @@ struct Parser { return lhs; } + // Explicit, since a silent conversion is 65,536 times off; a shift, not a host call. + /// Parse `toFixed(x)` or `toInt(x)`, the cursor on the keyword. + VReg parseConversion() { + const bool up = atKeyword("toFixed", 7); + lex.advance(); + if (!expect(Tok::LParen, "expected '(' after the conversion")) return 0; + VReg v = parseExpr(); + if (failed) return 0; + if (up && exprIsFixed) { fail("this value is already fixed"); return 0; } + if (!up && !exprIsFixed) { fail("this value is already a whole number"); return 0; } + // A literal is range-checked here, since `toFixed(40000)` would shift past Q16.16. + if (up && exprLitConst >= 0) { + const int32_t lit = ir.ops[exprLitConst].imm; + if (lit < -32768 || lit > 32767) + { fail("this number is out of range for a fixed value"); return 0; } + } + if (!expect(Tok::RParen, "expected ')' to close the conversion")) return 0; + VReg dst = alloc(); + emit({up ? IrOp::Shl : IrOp::Sar, dst, v, 0,0,0, 16, nullptr, {}}); + freeTemp(v); + exprIsFixed = up; + return dst; + } + + /// Parse what a name reads as a value: a system variable, a struct field, a local, a member, or a call's answer. + VReg parseNamePrimary() { + // Resolved before locals and members, so a system variable means one thing everywhere. + if (const SysVar* sv = sysvars.find(lex.identBeg, lex.identLen)) { + lex.advance(); + if (sv->kind == SysVarKind::Arg) { + VReg v = alloc(); // parked at entry, so bring it back for this read + emit({IrOp::Reload, v, 0,0,0,0, hostArgSlot(sv->where), nullptr, {}}); + return v; + } + VReg v = alloc(); + // A 4-byte load, since a one-byte read gave a 768-wide wall a width of zero. + emit({IrOp::LoadCtrl32, v, 0,0,0,0, sv->where, nullptr, {}}); + return v; + } + // A whole struct in a scalar place is read by field: `getRGB(i).r`, `RGB(1, 2, 3).g`. + if (const StructDef* st = structCallAt()) { + VReg vals[kMaxStructFields] = {}; + if (!parseStructValue(*st, vals, /*thenField*/ true)) return 0; + const int f = parseField(*st); + for (uint8_t k = 0; k < st->fieldCount; k++) if (int(k) != f) freeTemp(vals[k]); + if (f < 0) return 0; + exprIsFixed = false; + exprLitConst = -1; + return vals[f]; + } + const int li = findLocal(lex.identBeg, lex.identLen); + if (li >= 0) { + // Into an ordinary temp, so a variable holds a register only for this read. + lex.advance(); + // A struct local's field sits in the slot after the one before it. + const StructDef* lst = structAt(locals[li].st); + const int f = lst ? parseField(*lst) : 0; + if (f < 0) return 0; + VReg v = alloc(); + emit({IrOp::Reload, v, 0,0,0,0, locals[li].slot + f, nullptr, {}}); + exprIsFixed = (locals[li].type == CtrlType::Fixed); + return v; + } + // One lookup answers both, since a control is a member the UI shows. + const int mi = findMember(lex.identBeg, lex.identLen); + if (mi >= 0) return parseMemberRead(mi); + VReg out = 0; + const Builtin* called = table.find(lex.identBeg, lex.identLen); + parseCall(&out); // otherwise a call used as an expression must return a value + // The builtin's business: uvX and uvY hand back a fixed coordinate, and min follows its arguments. + exprIsFixed = called && (called->fixedReturn || (called->sameTypeArgs && argsFixed)); + exprLitConst = -1; + return out; + } + + /// Read member `mi`, the cursor past its name: a struct field, an element, or the whole value. + VReg parseMemberRead(int mi) { + lex.advance(); + if (const StructDef* mst = structAt(memberStruct[mi])) { + const int f = parseField(*mst); + if (f < 0) return 0; + exprIsFixed = false; + return loadMemberField(mi, f); + } + // An arbitrary index expression, the orthogonality the rest of the language has. + if (lex.kind == Tok::LBracket) { + if (members[mi].count == 1) { fail("this member is not an array"); return 0; } + lex.advance(); + VReg idx = parseExpr(); + if (failed) return 0; + // An index counts elements, so it is a whole number. + if (exprIsFixed) { fail("an array index is a whole number: write toInt(x)"); return 0; } + if (!expect(Tok::RBracket, "expected ']' to close an array index")) { freeTemp(idx); return 0; } + VReg v = alloc(); + // The element's type, since leaving the index's made `heat[3] * 0.5` patch the 3. + exprIsFixed = (members[mi].type == CtrlType::Fixed); + exprLitConst = -1; + emit({IrOp::LoadIdx, v, idx, 0, 0, 0, + idxPack(members[mi].offset, ctrlWidth(members[mi].type), + members[mi].count), nullptr, {}}); + freeTemp(idx); + return v; + } + if (members[mi].count > 1) { fail("an array needs an index: write name[i]"); return 0; } + VReg v = alloc(); + exprIsFixed = (members[mi].type == CtrlType::Fixed); + // One load for every scalar, since a slot already holds what its type promises. + emit({IrOp::LoadCtrl32, v, 0,0,0,0, members[mi].offset, nullptr, {}}); + return v; + } + // A bare ident reads a declared value, where one followed by `(` is a call. - /// Parse a primary: a literal, a name, a call, a conversion or a parenthesised expression. + /// Parse a primary: a literal, a name, a call, a conversion or a parenthesized expression. VReg parsePrimary() { if (failed) return 0; // Set here, not per branch, so a new kind of primary cannot forget either question. exprIsFixed = false; exprLitConst = -1; - // Explicit, since a silent conversion is 65,536 times off; a shift, not a host call. - if (lex.kind == Tok::Ident && (atKeyword("toFixed", 7) || atKeyword("toInt", 5))) { - const bool up = atKeyword("toFixed", 7); - lex.advance(); - if (!expect(Tok::LParen, "expected '(' after the conversion")) return 0; - VReg v = parseExpr(); - if (failed) return 0; - if (up && exprIsFixed) { fail("this value is already fixed"); return 0; } - if (!up && !exprIsFixed) { fail("this value is already a whole number"); return 0; } - // A literal is range-checked here, since `toFixed(40000)` would shift past Q16.16. - if (up && exprLitConst >= 0) { - const int32_t lit = ir.ops[exprLitConst].imm; - if (lit < -32768 || lit > 32767) - { fail("this number is out of range for a fixed value"); return 0; } - } - if (!expect(Tok::RParen, "expected ')' to close the conversion")) return 0; - VReg dst = alloc(); - emit({up ? IrOp::Shl : IrOp::Sar, dst, v, 0,0,0, 16, nullptr, {}}); - freeTemp(v); - exprIsFixed = up; - return dst; - } + if (lex.kind == Tok::Ident && (atKeyword("toFixed", 7) || atKeyword("toInt", 5))) return parseConversion(); if (lex.kind == Tok::LParen) { // grouping lex.advance(); VReg v = parseExpr(); @@ -469,68 +773,8 @@ struct Parser { lex.advance(); return v; } - if (lex.kind == Tok::Ident) { - // Resolved before locals and members, so a system variable means one thing everywhere. - if (const SysVar* sv = sysvars.find(lex.identBeg, lex.identLen)) { - lex.advance(); - if (sv->kind == SysVarKind::Arg) { - VReg v = alloc(); // parked at entry, so bring it back for this read - emit({IrOp::Reload, v, 0,0,0,0, hostArgSlot(sv->where), nullptr, {}}); - return v; - } - VReg v = alloc(); - // A 4-byte load, since a one-byte read gave a 768-wide wall a width of zero. - emit({IrOp::LoadCtrl32, v, 0,0,0,0, sv->where, nullptr, {}}); - return v; - } - const int li = findLocal(lex.identBeg, lex.identLen); - if (li >= 0) { - // Into an ordinary temp, so a variable holds a register only for this read. - lex.advance(); - VReg v = alloc(); - emit({IrOp::Reload, v, 0,0,0,0, locals[li].slot, nullptr, {}}); - exprIsFixed = (locals[li].type == CtrlType::Fixed); - return v; - } - // One lookup answers both, since a control is a member the UI shows. - const int mi = findMember(lex.identBeg, lex.identLen); - if (mi >= 0) { - lex.advance(); - // An arbitrary index expression, the orthogonality the rest of the language has. - if (lex.kind == Tok::LBracket) { - if (members[mi].count == 1) { fail("this member is not an array"); return 0; } - lex.advance(); - VReg idx = parseExpr(); - if (failed) return 0; - // An index counts elements, so it is a whole number. - if (exprIsFixed) { fail("an array index is a whole number: write toInt(x)"); return 0; } - if (!expect(Tok::RBracket, "expected ']' to close an array index")) { freeTemp(idx); return 0; } - VReg v = alloc(); - // The element's type, since leaving the index's made `heat[3] * 0.5` patch the 3. - exprIsFixed = (members[mi].type == CtrlType::Fixed); - exprLitConst = -1; - emit({IrOp::LoadIdx, v, idx, 0, 0, 0, - idxPack(members[mi].offset, ctrlWidth(members[mi].type), - members[mi].count), nullptr, {}}); - freeTemp(idx); - return v; - } - if (members[mi].count > 1) { fail("an array needs an index: write name[i]"); return 0; } - VReg v = alloc(); - exprIsFixed = (members[mi].type == CtrlType::Fixed); - // One load for every scalar, since a slot already holds what its type promises. - emit({IrOp::LoadCtrl32, v, 0,0,0,0, members[mi].offset, nullptr, {}}); - return v; - } - VReg out = 0; - const Builtin* called = table.find(lex.identBeg, lex.identLen); - parseCall(&out); // otherwise a call used as an expression must return a value - // The builtin's business: uvX and uvY hand back a fixed coordinate. - exprIsFixed = called && called->fixedReturn; - exprLitConst = -1; - return out; - } - fail("expected a number, a control name, or a function call"); + if (lex.kind == Tok::Ident) return parseNamePrimary(); + fail(lex.kind == Tok::Not ? kJoinsConditions : "expected a number, a control name, or a function call"); return 0; } @@ -545,29 +789,7 @@ struct Parser { if (std::strncmp(fns[i].name, lex.identBeg, lex.identLen) != 0) continue; lex.advance(); if (!expect(Tok::LParen, "expected '(' after the function name")) return; - // Stored once all are evaluated, since a later one may call through this block. - const uint8_t want = fns[i].params; - VReg staged[kMaxScriptArgs] = {}; - uint8_t got = 0; - while (lex.kind != Tok::RParen && lex.kind != Tok::End) { - if (got > 0 && !expect(Tok::Comma, "expected ',' between arguments")) return; - const VReg a = parseExpr(); - if (failed) return; - if (got < want && got < kMaxScriptArgs) staged[got] = a; - else freeTemp(a); - got++; - } - if (!expect(Tok::RParen, "expected ')' to close the arguments")) return; - const uint8_t staging = got < want ? got : want; - for (uint8_t a = 0; a < staging && a < kMaxScriptArgs; a++) { - emit({IrOp::StoreCtrl32, 0, staged[a], 0,0,0, scriptArgOffset(a), nullptr, {}}); - freeTemp(staged[a]); - } - if (got != want) { - fail(got < want ? "too few arguments for this function" - : "too many arguments for this function"); - return; - } + if (!stageScriptArgs(i)) return; // Refused for a void callee, since the script would read the register's leftovers. if (resultOut) { if (fns[i].ret == RetType::Void) { @@ -594,56 +816,136 @@ struct Parser { // Parked in consecutive frame slots, so only one argument holds a register at a time. const uint8_t argBase = slotHighWater; uint8_t n = 0; + if (!parseBuiltinArgs(fn, n)) return; + // A builtin answering a struct writes it into its first argument slots, so they stay reserved. + if (fn->retStruct) { + const StructDef* st = table.findStruct(fn->retStruct, std::strlen(fn->retStruct)); + if (st && argBase + st->fieldCount > kMaxLocals) { fail("too many arguments to hold"); return; } + if (st && slotHighWater < argBase + st->fieldCount) slotHighWater = static_cast(argBase + st->fieldCount); + if (slotHighWater > slotsUsed) slotsUsed = slotHighWater; + } + + // The arguments live in the frame, so nothing is held in a register across the call. + if (resultOut) { + if (fn->kind != BuiltinKind::Call || !fn->returns) { fail("this function does not return a value"); return; } + // `imm` carries the slot they start at and `b` how many there are. + VReg r = alloc(); + emit({IrOp::Call, r, 0, n, 0, 0, argBase, fn->fn, {}}); + *resultOut = r; + } else { + // A statement call, whose result is discarded when it has one. + if (fn->kind == BuiltinKind::Call) { + VReg r = alloc(); + emit({IrOp::Call, r, 0, n, 0, 0, argBase, fn->fn, {}}); + freeTemp(r); + } else { + // Reloaded into registers, and a wider builtin is refused rather than truncated. + if (n > 4) { fail("this function takes too many arguments to inline"); return; } + VReg a0 = 0, a1 = 0, a2 = 0, a3 = 0; + VReg* slot[4] = {&a0, &a1, &a2, &a3}; + for (uint8_t i = 0; i < n && i < 4; i++) { + *slot[i] = alloc(); + emit({IrOp::Reload, *slot[i], 0,0,0,0, static_cast(argBase + i), nullptr, {}}); + } + emit({IrOp::Inline, 0, a0, a1, a2, a3, 0, nullptr, nullptr, fn->inlineOp}); + for (uint8_t i = 0; i < n && i < 4; i++) freeTemp(*slot[i]); + } + } + // Back only to where this call started, since a nested one stages inside its parent's. + slotHighWater = argBase; + } + + // Parked in consecutive frame slots, so only one argument holds a register at a time. + /// Parse a builtin's arguments through its ')' into frame slots from the high-water mark, a struct expanding into its fields; `n` answers how many. + bool parseBuiltinArgs(const Builtin* fn, uint8_t& n) { + n = 0; + // Local, since an argument may itself be a same-type call that would overwrite the member mid-list. + bool groupFixed = false; + int groupLit = -1; if (lex.kind != Tok::RParen) { while (true) { - if (n >= fn->argc) { fail("too many arguments"); return; } + if (n >= fn->argc) { fail("too many arguments"); return false; } + // A whole struct fills as many argument positions as it has fields. + const StructDef* st = (lex.kind == Tok::Ident && !((fn->byRef >> n) & 1u)) ? structValueAt() : nullptr; + if (st) { + if (n + st->fieldCount > fn->argc) { fail("too many arguments: this struct fills more positions than remain"); return false; } + for (uint8_t f = 0; f < st->fieldCount; f++) { + const uint8_t bit = static_cast(1u << (n + f)); + if ((fn->fixedArgs | fn->byStr | fn->byRef) & bit) { + fail("a struct fills whole-number positions only, and this one is not"); return false; + } + } + VReg vals[kMaxStructFields] = {}; + if (!parseStructValue(*st, vals)) return false; + if (slotHighWater + st->fieldCount > kMaxLocals) { fail("too many arguments to hold"); return false; } + for (uint8_t f = 0; f < st->fieldCount; f++) { + emit({IrOp::Spill, 0, vals[f], 0,0,0, slotHighWater++, nullptr, {}}); + freeTemp(vals[f]); + } + n = static_cast(n + st->fieldCount); + if (lex.kind == Tok::Comma) { lex.advance(); continue; } + break; + } // A string label and a member by name, only where the builtin asks for them. VReg v = 0; const bool wantStr = (fn->byStr >> n) & 1u; if (wantStr && lex.kind != Tok::String) { - fail("this argument must be a name in quotes"); return; + fail("this argument must be a name in quotes"); return false; } // Only where wanted, or a pointer's low bits would arrive as a color index. if (!wantStr && lex.kind == Tok::String) { - fail("this argument is a number, not a name in quotes"); return; + fail("this argument is a number, not a name in quotes"); return false; } if (lex.kind == Tok::String) { // Interned into a pool outliving the compile, not a pointer into the source. const char* interned = internString(lex.identBeg, lex.identLen); - if (!interned) { fail("no room for this script's strings"); return; } + if (!interned) { fail("no room for this script's strings"); return false; } v = alloc(); emit({IrOp::ConstPtr, v, 0,0,0,0, 0, nullptr, interned, {}}); lex.advance(); } else if ((fn->byRef >> n) & 1u) { - if (lex.kind != Tok::Ident) { fail("expected the member this control is bound to"); return; } + if (lex.kind != Tok::Ident) { fail("expected the member this control is bound to"); return false; } const int mi = findMember(lex.identBeg, lex.identLen); - if (mi < 0) { fail("no member of that name is declared in this class"); return; } + if (mi < 0) { fail("no member of that name is declared in this class"); return false; } // The type must have a widget, and drive one value rather than an array. if (members[mi].type == CtrlType::Fixed || members[mi].type == CtrlType::Str) - { fail("a control binds an int, byte or bool member"); return; } + { fail("a control binds an int, byte or bool member"); return false; } if (members[mi].count > 1) - { fail("a control binds a single member, not an array"); return; } + { fail("a control binds a single member, not an array"); return false; } // Offset and type in one word, since the builtin sees values, not declarations. v = alloc(); emit({IrOp::Const, v, 0,0,0,0, members[mi].offset | (int32_t(members[mi].type) << 8), nullptr, {}}); lex.advance(); + } else if (fn->sameTypeArgs) { + // One type across the arguments, a literal adopting it, and the result following it. + v = parseExpr(); + if (failed) return false; + if (n == 0) { + groupFixed = exprIsFixed; + groupLit = exprLitConst; + } else { + bool outFixed = false; + if (!meet(groupFixed, groupLit, exprIsFixed, exprLitConst, outFixed)) return false; + groupFixed = outFixed; + groupLit = -1; + } } else { const bool wantFixed = ((fn->fixedArgs >> n) & 1u) != 0; v = parseExpr(); - if (failed) return; + if (failed) return false; // Against what the builtin declares, since a stray fixed reads 65,536 times off. if (wantFixed != exprIsFixed) { bool adopted = false; if (!wantFixed || !meet(true, -1, exprIsFixed, exprLitConst, adopted)) { fail(wantFixed ? "this argument is a fixed value: write toFixed(x)" : "this argument is a whole number: write toInt(x)"); - return; + return false; } } } - if (failed) return; - if (slotHighWater >= kMaxLocals) { fail("too many arguments to hold"); return; } + if (failed) return false; + if (slotHighWater >= kMaxLocals) { fail("too many arguments to hold"); return false; } emit({IrOp::Spill, 0, v, 0,0,0, slotHighWater++, nullptr, {}}); freeTemp(v); // its register is free again at once n++; @@ -652,38 +954,35 @@ struct Parser { } } if (slotHighWater > slotsUsed) slotsUsed = slotHighWater; - if (n != fn->argc) { fail("wrong number of arguments"); return; } - - if (!expect(Tok::RParen, "expected ')'")) return; + if (n != fn->argc) { fail("wrong number of arguments"); return false; } + argsFixed = groupFixed; // read by the caller straight after, before anything else parses + return expect(Tok::RParen, "expected ')'"); + } - // The arguments live in the frame, so nothing is held in a register across the call. - if (resultOut) { - if (fn->kind != BuiltinKind::Call || !fn->returns) { fail("this function does not return a value"); return; } - // `imm` carries the slot they start at and `b` how many there are. - VReg r = alloc(); - emit({IrOp::Call, r, 0, n, 0, 0, argBase, fn->fn, {}}); - *resultOut = r; - } else { - // A statement call, whose result is discarded when it has one. - if (fn->kind == BuiltinKind::Call) { - VReg r = alloc(); - emit({IrOp::Call, r, 0, n, 0, 0, argBase, fn->fn, {}}); - freeTemp(r); - } else { - // Reloaded into registers, and a wider builtin is refused rather than truncated. - if (n > 4) { fail("this function takes too many arguments to inline"); return; } - VReg a0 = 0, a1 = 0, a2 = 0, a3 = 0; - VReg* slot[4] = {&a0, &a1, &a2, &a3}; - for (uint8_t i = 0; i < n && i < 4; i++) { - *slot[i] = alloc(); - emit({IrOp::Reload, *slot[i], 0,0,0,0, static_cast(argBase + i), nullptr, {}}); - } - emit({IrOp::Inline, 0, a0, a1, a2, a3, 0, nullptr, nullptr, fn->inlineOp}); - for (uint8_t i = 0; i < n && i < 4; i++) freeTemp(*slot[i]); - } - } - // Back only to where this call started, since a nested one stages inside its parent's. - slotHighWater = argBase; + // Its fields pack at their own width from a 4-byte boundary, as an array's elements do, and start at zero. + /// Parse a struct member, `RGB c;`, the type name already consumed. + void parseStructDecl(const StructDef& st) { + if (lex.kind != Tok::Ident) { fail("expected a member name after the type"); return; } + const char* name = lex.identBeg; size_t nameLen = lex.identLen; + if (nameLen >= kMaxControlName) { fail("member name too long"); return; } + if (sysvars.find(name, nameLen)) { fail("name is a system variable"); return; } + if (findMember(name, nameLen) >= 0) { fail("duplicate member name"); return; } + if (table.find(name, nameLen)) { fail("member name shadows a built-in function"); return; } + if (isReservedWord(name, nameLen)) { fail("member name is a reserved word"); return; } + if (table.findStruct(name, nameLen)) { fail("that name is a type"); return; } + lex.advance(); + if (lex.kind == Tok::Assign) { fail("a struct member starts at zero: set its fields in a function"); return; } + if (!expect(Tok::Semicolon, "expected ';' after the member declaration")) return; + if (memberCount >= kMaxCtrls) { fail("too many members"); return; } + uint16_t at = memberBytes; + if ((at % 4) != 0) at = uint16_t(at + (4 - at % 4)); + const uint16_t need = uint16_t(st.fieldCount) * ctrlWidth(st.type); + if (at + need > kCtrlBytes) { fail("the class declares more member data than the arena holds"); return; } + members[memberCount] = {name, 0, 0, 0, static_cast(nameLen), st.type, + static_cast(at), st.fieldCount}; + memberStruct[memberCount] = structIndex(&st); + memberBytes = static_cast(at + need); + memberCount++; } // The leading type keyword is already consumed by the caller. @@ -699,6 +998,7 @@ struct Parser { if (table.find(name, nameLen)) { fail("member name shadows a built-in function"); return; } // A reserved word resolves first, so such a member could be declared and never read. if (isReservedWord(name, nameLen)) { fail("member name is a reserved word"); return; } + if (table.findStruct(name, nameLen)) { fail("that name is a type"); return; } lex.advance(); // The length is a literal, since the arena is sized at compile time. uint8_t count = 1; @@ -821,6 +1121,18 @@ struct Parser { return false; } + /// The struct type the current token names, or null. + const StructDef* structTypeAt() const { + return lex.kind == Tok::Ident ? table.findStruct(lex.identBeg, lex.identLen) : nullptr; + } + /// Whether a struct declaration starts here: a type name followed by a variable name. + bool structDeclAt() const { + if (!structTypeAt()) return false; + Lexer probe = lex; + probe.advance(); + return probe.kind == Tok::Ident; + } + // Matched by text rather than lexed, since the set is tiny and a length check is exact. /// Whether the current token is exactly this keyword. bool atKeyword(const char* kw, size_t len) const { @@ -994,10 +1306,12 @@ struct Parser { // A system variable may not be assigned, since the engine rewrites it before every call. /// Parse an assignment, with the name already consumed. bool parseAssignment(const char* name, size_t nameLen) { + if (lex.kind == Tok::Dot) return parseFieldAssignment(name, nameLen); // Resolved before the '=', since an index sits between the name and the operator. if (lex.kind == Tok::LBracket) { const int ai = findMember(name, nameLen); if (ai < 0) { fail("no member of that name is declared in this class"); return false; } + if (memberStruct[ai]) { fail(kOneFieldAtATime); return false; } if (members[ai].count == 1) { fail("this member is not an array"); return false; } lex.advance(); VReg idx = parseExpr(); @@ -1030,6 +1344,13 @@ struct Parser { // The destination first, so a refused target reports the name the script wrote. const int li = findLocal(name, nameLen); const int mi = li >= 0 ? -1 : findMember(name, nameLen); + // A whole struct copies field by field. + if (const StructDef* st = li >= 0 ? structAt(locals[li].st) : (mi >= 0 ? structAt(memberStruct[mi]) : nullptr)) { + VReg vals[kMaxStructFields] = {}; + if (!parseStructValue(*st, vals)) return false; + storeStruct(*st, li, mi, vals); + return expect(Tok::Semicolon, "expected ';' after an assignment"); + } if (mi >= 0 && members[mi].count > 1) { fail("assign one element: name[i] = value"); return false; @@ -1075,68 +1396,216 @@ struct Parser { return expect(Tok::Semicolon, "expected ';' after an assignment"); } + /// Store a struct value's registers into a local's slots or a member's bytes, freeing them. + void storeStruct(const StructDef& st, int li, int mi, VReg vals[kMaxStructFields]) { + for (uint8_t f = 0; f < st.fieldCount; f++) { + if (li >= 0) emit({IrOp::Spill, 0, vals[f], 0,0,0, locals[li].slot + f, nullptr, {}}); + else emit({storeOpFor(members[mi].type), 0, vals[f], 0,0,0, memberFieldOffset(mi, f), nullptr, {}}); + freeTemp(vals[f]); + } + } + + /// Parse `name.field = value;`, the name consumed and the '.' current. + bool parseFieldAssignment(const char* name, size_t nameLen) { + const int li = findLocal(name, nameLen); + const int mi = li >= 0 ? -1 : findMember(name, nameLen); + const StructDef* st = li >= 0 ? structAt(locals[li].st) : (mi >= 0 ? structAt(memberStruct[mi]) : nullptr); + if (!st) { fail(li < 0 && mi < 0 ? "not declared: add it to the class body" : "only a struct has fields"); return false; } + const int f = parseField(*st); + if (f < 0) return false; + if (!expect(Tok::Assign, "expected '=' in an assignment")) return false; + VReg v = parseExpr(); + if (failed) return false; + if (exprIsFixed) { fail("a struct field is a whole number: write toInt(x)"); return false; } + if (exprLitConst >= 0 && !fitsInType(ir.ops[exprLitConst].imm, st->type)) + { fail("this number is out of range for that field"); return false; } + if (li >= 0) { + narrowToType(v, st->type); // a byte field wraps at 255, as a byte local does + emit({IrOp::Spill, 0, v, 0,0,0, locals[li].slot + f, nullptr, {}}); + } else { + emit({storeOpFor(members[mi].type), 0, v, 0,0,0, memberFieldOffset(mi, f), nullptr, {}}); + } + freeTemp(v); + return expect(Tok::Semicolon, "expected ';' after an assignment"); + } + // A bool holds 0..255, so a multiple of 256 truncates to false: the IR has no select. /// The store op a member's type needs. static IrOp storeOpFor(CtrlType type) { return ctrlWidth(type) == 1 ? IrOp::StoreCtrl : IrOp::StoreCtrl32; } - // Six comparisons onto two signed branch ops, each emitting the negation the script wrote. - /// Parse an `if` with an optional `else`, and emit it. - bool parseIf() { - lex.advance(); // `if` - if (!expect(Tok::LParen, "expected '(' after if")) return false; - VReg a = parseExpr(); - if (failed) return false; - const Tok cmp = lex.kind; - if (cmp != Tok::Less && cmp != Tok::LessEq && cmp != Tok::Greater && - cmp != Tok::GreaterEq && cmp != Tok::EqEq && cmp != Tok::NotEq) { - freeTemp(a); - fail("expected a comparison: <, <=, >, >=, == or !="); - return false; - } - const bool aFixed = exprIsFixed; - const int aLit = exprLitConst; - lex.advance(); - VReg b = parseExpr(); - if (failed) { freeTemp(a); return false; } - // Same wall as the operators, since Q16.16 preserves order but not magnitude. - bool cmpFixed = false; - if (!meet(aFixed, aLit, exprIsFixed, exprLitConst, cmpFixed)) - { freeTemp(b); freeTemp(a); return false; } - if (!expect(Tok::RParen, "expected ')' to close the if condition")) { freeTemp(b); freeTemp(a); return false; } - if (!expect(Tok::LBrace, "expected '{': an if body is braced")) { freeTemp(b); freeTemp(a); return false; } + /// A fresh branch label, or -1 having failed when the script has used them all. + int newLabel() { + if (nextLabel >= kIrLabels) { fail("too many branches in one script"); return -1; } + return nextLabel++; + } - // Two labels at most: one to skip the then-block, one to skip the else-block. - if (nextLabel + 2 > kIrLabels) { fail("too many branches in one script"); return false; } - const uint8_t lElse = nextLabel++; + // The jump idiom, `z >= z` with z zero, since the IR has no unconditional branch. + /// Branch to a label always. + void emitJump(uint8_t label) { + VReg z = alloc(); + emit({IrOp::Const, z, 0,0,0,0, 0, nullptr, {}}); + emit({IrOp::BranchGe, 0, z, z, 0,0, label, nullptr, {}}); + freeTemp(z); + } - // Emit the branch that SKIPS the then-block, which is the NEGATION of the written test. + // Six comparisons onto two signed branch ops, each emitting the negation the script wrote. + /// Branch to `lFalse` unless `a cmp b` holds, freeing both registers. + void emitBranchUnless(Tok cmp, VReg a, VReg b, uint8_t lFalse) { switch (cmp) { // Signed, since an unsigned compare reads a negative as a huge value. - case Tok::Less: emit({IrOp::BranchGeS, 0, a, b, 0,0, lElse, nullptr, {}}); break; + case Tok::Less: emit({IrOp::BranchGeS, 0, a, b, 0,0, lFalse, nullptr, {}}); break; // !(a >= b) is a < b, which needs the strict pair below. - case Tok::GreaterEq: emitStrictLess(a, b, lElse); break; + case Tok::GreaterEq: emitStrictLess(a, b, lFalse); break; // !(a > b) is a <= b, i.e. b >= a. - case Tok::Greater: emit({IrOp::BranchGeS, 0, b, a, 0,0, lElse, nullptr, {}}); break; + case Tok::Greater: emit({IrOp::BranchGeS, 0, b, a, 0,0, lFalse, nullptr, {}}); break; // !(a <= b) is a > b, i.e. b < a. - case Tok::LessEq: emitStrictLess(b, a, lElse); break; - case Tok::EqEq: emit({IrOp::BranchNe, 0, a, b, 0,0, lElse, nullptr, {}}); break; + case Tok::LessEq: emitStrictLess(b, a, lFalse); break; + case Tok::EqEq: emit({IrOp::BranchNe, 0, a, b, 0,0, lFalse, nullptr, {}}); break; // There is no BranchEq, so this is the one case needing two branches. case Tok::NotEq: { - if (nextLabel >= kIrLabels) { freeTemp(b); freeTemp(a); fail("too many branches in one script"); return false; } + if (nextLabel >= kIrLabels) { freeTemp(b); freeTemp(a); fail("too many branches in one script"); return; } const uint8_t lBody = nextLabel++; emit({IrOp::BranchNe, 0, a, b, 0,0, lBody, nullptr, {}}); - VReg z = alloc(); - emit({IrOp::Const, z, 0,0,0,0, 0, nullptr, {}}); - emit({IrOp::BranchGe, 0, z, z, 0,0, lElse, nullptr, {}}); // z >= z: always taken - freeTemp(z); + emitJump(lFalse); emit({IrOp::Label, 0, 0,0,0,0, lBody, nullptr, {}}); break; } default: break; } freeTemp(b); freeTemp(a); + } + + /// Whether a token compares two values. + static bool isComparison(Tok t) { + return t == Tok::Less || t == Tok::LessEq || t == Tok::Greater || t == Tok::GreaterEq || + t == Tok::EqEq || t == Tok::NotEq; + } + + // Peeks without consuming, to the ')' that closes the condition or group the cursor is in. + /// Whether a `||` follows at this nesting level. + bool orFollows() const { + Lexer probe = lex; + int depth = 0; + while (probe.kind != Tok::End && probe.kind != Tok::Error) { + if (probe.kind == Tok::LParen) depth++; + else if (probe.kind == Tok::RParen) { if (depth == 0) return false; depth--; } + else if (probe.kind == Tok::OrOr && depth == 0) return true; + probe.advance(); + } + return false; + } + + // `(a + b) < c` opens with a parenthesized expression, `(a < b) && c` with a parenthesized condition. + /// Whether the '(' at the cursor opens a condition rather than an arithmetic group. + bool groupIsCondition() const { + Lexer probe = lex; + probe.advance(); // past the '(' + int depth = 0; + while (probe.kind != Tok::End && probe.kind != Tok::Error) { + if (probe.kind == Tok::LParen) depth++; + else if (probe.kind == Tok::RParen) { if (depth == 0) return false; depth--; } + else if (depth == 0 && (isComparison(probe.kind) || probe.kind == Tok::AndAnd || + probe.kind == Tok::OrOr || probe.kind == Tok::Not)) return true; + probe.advance(); + } + return false; + } + + // Jumping code, as C compiles a condition: every test branches to `lFalse` when it fails, so `&&` needs no label and `||` one per term. + /// Parse a condition, branching to `lFalse` when it is false and falling through when it is true. + bool parseCondition(uint8_t lFalse) { + if (!orFollows()) return parseConjunction(lFalse); + const int lTrue = newLabel(); + if (lTrue < 0) return false; + for (;;) { + const int lNext = newLabel(); + if (lNext < 0) return false; + if (!parseConjunction(static_cast(lNext))) return false; + emitJump(static_cast(lTrue)); // this term held: the whole condition holds + emit({IrOp::Label, 0, 0,0,0,0, lNext, nullptr, {}}); + if (!expect(Tok::OrOr, "expected '||'")) return false; + if (!orFollows()) break; + } + if (!parseConjunction(lFalse)) return false; // the last term decides + emit({IrOp::Label, 0, 0,0,0,0, lTrue, nullptr, {}}); + return true; + } + + /// Parse terms joined by `&&`, each branching to `lFalse` when it fails. + bool parseConjunction(uint8_t lFalse) { + if (!parseNegation(lFalse)) return false; + while (!failed && lex.kind == Tok::AndAnd) { + lex.advance(); + if (!parseNegation(lFalse)) return false; + } + return !failed; + } + + /// Parse a test with any number of leading `!`, each one swapping where it branches. + // As in C, `!` negates what follows it rather than a comparison after that, so `!a < b` would compare !a: refused, so it is read neither way. + bool parseNegation(uint8_t lFalse) { + if (lex.kind != Tok::Not) return parseCondPrimary(lFalse); + lex.advance(); + if (lex.kind != Tok::Not && !(lex.kind == Tok::LParen && groupIsCondition())) { + // A negated value holds when the value is zero. + VReg a = parseExpr(); + if (failed) return false; + if (isComparison(lex.kind)) { freeTemp(a); fail(kNegatesWhatFollows); return false; } + VReg z = alloc(); + emit({IrOp::Const, z, 0,0,0,0, 0, nullptr, {}}); + emitBranchUnless(Tok::EqEq, a, z, lFalse); + return !failed; + } + const int lHeld = newLabel(); // where the inner test goes when it fails, which is when this one holds + if (lHeld < 0) return false; + if (!parseNegation(static_cast(lHeld))) return false; + if (isComparison(lex.kind)) { fail(kNegatesWhatFollows); return false; } + emitJump(lFalse); // the inner test held, so this one fails + emit({IrOp::Label, 0, 0,0,0,0, lHeld, nullptr, {}}); + return true; + } + + // A bare value is true when it is not zero, as C reads one, so `if (done)` and `if (!done)` read naturally. + /// Parse one test: a parenthesized condition, a comparison, or a value. + bool parseCondPrimary(uint8_t lFalse) { + if (lex.kind == Tok::LParen && groupIsCondition()) { + lex.advance(); + if (!parseCondition(lFalse)) return false; + return expect(Tok::RParen, "expected ')' to close the condition"); + } + VReg a = parseExpr(); + if (failed) return false; + const Tok cmp = lex.kind; + if (!isComparison(cmp)) { + VReg z = alloc(); + emit({IrOp::Const, z, 0,0,0,0, 0, nullptr, {}}); + emitBranchUnless(Tok::NotEq, a, z, lFalse); + return !failed; + } + const bool aFixed = exprIsFixed; + const int aLit = exprLitConst; + lex.advance(); + VReg b = parseExpr(); + if (failed) { freeTemp(a); return false; } + // Same wall as the operators, since Q16.16 preserves order but not magnitude. + bool cmpFixed = false; + if (!meet(aFixed, aLit, exprIsFixed, exprLitConst, cmpFixed)) { freeTemp(b); freeTemp(a); return false; } + emitBranchUnless(cmp, a, b, lFalse); + return !failed; + } + + /// Parse an `if` with an optional `else`, and emit it. + bool parseIf() { + lex.advance(); // `if` + if (!expect(Tok::LParen, "expected '(' after if")) return false; + // One label to skip the then-block, taken before the condition so a single comparison emits what it always did. + if (nextLabel + 2 > kIrLabels) { fail("too many branches in one script"); return false; } + const uint8_t lElse = nextLabel++; + if (!parseCondition(lElse)) return false; + if (!expect(Tok::RParen, "expected ')' to close the if condition")) return false; + if (!expect(Tok::LBrace, "expected '{': an if body is braced")) return false; // A braced body is a scope, so a local dies at the '}' and its slot is handed back. const uint8_t thenLocal = localCount, thenSlot = slotHighWater; @@ -1207,6 +1676,7 @@ struct Parser { const size_t varLen = lex.identLen; // The same collisions a loop counter checks for, and for the same reasons. if (isReservedWord(varName, varLen)) { fail("that name is a reserved word"); return false; } + if (table.findStruct(varName, varLen)) { fail("that name is a type"); return false; } // A local named for a builtin would shadow it for the rest of the function. if (table.find(varName, varLen)) { fail("that name shadows a built-in function"); return false; } if (sysvars.find(varName, varLen)) { fail("name is a system variable"); return false; } @@ -1244,8 +1714,48 @@ struct Parser { return expect(Tok::Semicolon, "expected ';' after a variable declaration"); } + // One slot per field for the rest of the block, from a struct value or zero. + /// Parse a struct local, `RGB c = value;` or `RGB c;`. + bool parseStructLocalDecl() { + const StructDef& st = *structTypeAt(); + lex.advance(); + const char* varName = lex.identBeg; + const size_t varLen = lex.identLen; + if (isReservedWord(varName, varLen)) { fail("that name is a reserved word"); return false; } + if (table.findStruct(varName, varLen)) { fail("that name is a type"); return false; } + if (table.find(varName, varLen)) { fail("that name shadows a built-in function"); return false; } + if (sysvars.find(varName, varLen)) { fail("name is a system variable"); return false; } + if (findLocal(varName, varLen) >= 0) { fail("that name is already in use here"); return false; } + if (findMember(varName, varLen) >= 0) { fail("a member of that name is declared"); return false; } + if (localCount >= kMaxLocals || slotHighWater + st.fieldCount > kMaxLocals) { + fail("too many variables in this function: a struct takes one slot per field"); + return false; + } + lex.advance(); + VReg vals[kMaxStructFields] = {}; + if (lex.kind == Tok::Semicolon) { + for (uint8_t f = 0; f < st.fieldCount; f++) { + vals[f] = alloc(); + emit({IrOp::Const, vals[f], 0,0,0,0, 0, nullptr, {}}); + } + } else { + if (!expect(Tok::Assign, "expected '=' or ';' after the variable name")) return false; + if (!parseStructValue(st, vals)) return false; + } + const uint8_t slot = slotHighWater; + slotHighWater = static_cast(slotHighWater + st.fieldCount); + if (slotHighWater > slotsUsed) slotsUsed = slotHighWater; + for (uint8_t f = 0; f < st.fieldCount; f++) { + emit({IrOp::Spill, 0, vals[f], 0,0,0, slot + f, nullptr, {}}); + freeTemp(vals[f]); + } + locals[localCount++] = {varName, varLen, slot, st.type, structIndex(&st)}; + return expect(Tok::Semicolon, "expected ';' after a variable declaration"); + } + /// Parse one statement: a declaration, a for, an if, a return, an assignment or a call. bool parseStatement() { + if (structDeclAt()) return parseStructLocalDecl(); if (atTypeKeyword()) return parseLocalDecl(); if (atKeyword("for", 3)) return parseFor(); if (atKeyword("if", 2)) return parseIf(); @@ -1256,8 +1766,8 @@ struct Parser { const size_t nameLen = lex.identLen; Lexer save = lex; lex.advance(); - // `name =` and `name[` open an assignment, where `name(` opens a call. - if (lex.kind == Tok::Assign || lex.kind == Tok::LBracket) return parseAssignment(name, nameLen); + // `name =`, `name[` and `name.` open an assignment, where `name(` opens a call. + if (lex.kind == Tok::Assign || lex.kind == Tok::LBracket || lex.kind == Tok::Dot) return parseAssignment(name, nameLen); lex = save; parseCall(nullptr); if (failed) return false; @@ -1277,6 +1787,17 @@ struct Parser { } // A value from a void function has nowhere to go. if (curRet == RetType::Void) { fail("a void function returns no value"); return false; } + // A struct's fields go into the argument block, which the caller reads after the call. + if (curRet == RetType::Struct) { + VReg vals[kMaxStructFields] = {}; + if (!parseStructValue(*curRetStruct, vals)) return false; + for (uint8_t f = 0; f < curRetStruct->fieldCount; f++) { + emit({IrOp::StoreCtrl32, 0, vals[f], 0,0,0, scriptArgOffset(f), nullptr, {}}); + freeTemp(vals[f]); + } + emit({IrOp::Ret, 0, 0,0,0,0, 0, nullptr, {}}); + return expect(Tok::Semicolon, "expected ';'"); + } VReg v = 0; if (lex.kind == Tok::String) { if (curRet != RetType::Str) { fail("this function returns an int, not a string"); return false; } @@ -1307,14 +1828,17 @@ struct Parser { while (lex.kind != Tok::RParen && lex.kind != Tok::End) { if (params > 0 && !expect(Tok::Comma, "expected ',' between parameters")) return false; // A parameter is declared like a variable, so every type means what it means elsewhere. - if (!atTypeKeyword()) { fail("expected a parameter type"); return false; } - const CtrlType t = currentType(); + const StructDef* pst = structTypeAt(); + if (!pst && !atTypeKeyword()) { fail("expected a parameter type"); return false; } + const CtrlType t = pst ? pst->type : currentType(); + const uint8_t words = pst ? pst->fieldCount : 1; lex.advance(); if (lex.kind != Tok::Ident) { fail("expected a parameter name"); return false; } // A parameter is a local, so a name refused inside the body is refused here. if (isReservedWord(lex.identBeg, lex.identLen)) { fail("that name is a reserved word"); return false; } + if (table.findStruct(lex.identBeg, lex.identLen)) { fail("that name is a type"); return false; } if (table.find(lex.identBeg, lex.identLen)) { fail("that name shadows a built-in function"); return false; } @@ -1328,15 +1852,16 @@ struct Parser { fail("a member of that name is declared"); return false; } if (localCount >= kMaxLocals) { fail("too many locals"); return false; } - // Bounded by the arena block, since a fifth load reads past its end. - if (params >= moonlive::kMaxScriptArgs) { - fail("a function takes at most 4 parameters"); + // Bounded by the arena block, since a further load reads past its end. + if (params + words > moonlive::kMaxScriptArgs) { + fail("parameters fill at most 8 words, a struct taking one per field"); return false; } - locals[localCount++] = {lex.identBeg, lex.identLen, slotHighWater, t}; - slotHighWater++; + locals[localCount++] = {lex.identBeg, lex.identLen, slotHighWater, t, structIndex(pst)}; + for (uint8_t w = 0; w < words; w++) paramWordType[params + w] = t; + slotHighWater = static_cast(slotHighWater + words); if (slotHighWater > slotsUsed) slotsUsed = slotHighWater; - params++; + params = static_cast(params + words); lex.advance(); } if (!expect(Tok::RParen, "expected ')' to close the parameter list")) return false; @@ -1369,7 +1894,8 @@ struct Parser { if (!expect(Tok::LBrace, "expected '{' to open the class body")) return false; // Two tokens of lookahead, since a member and a typed function open with the same one. - while (!failed && atTypeKeyword()) { + while (!failed && (atTypeKeyword() || structTypeAt())) { + const StructDef* memberSt = structTypeAt(); const CtrlType ty = currentType(); Lexer save = lex; lex.advance(); // past the type @@ -1379,18 +1905,20 @@ struct Parser { return probe.kind == Tok::LParen; }(); if (isFn) { lex = save; break; } // a function: leave it for the loop below - parseDecl(ty); + if (memberSt) parseStructDecl(*memberSt); + else parseDecl(ty); } if (failed) return false; bool any = false; while (!failed && lex.kind != Tok::RBrace && lex.kind != Tok::End) { // Declared rather than implicit, so the host can rely on what a function hands back. - if (!atRetKeyword()) { - fail("a function declares what it returns: void, int or string"); + const StructDef* retSt = structTypeAt(); + if (!atRetKeyword() && !retSt) { + fail("a function returns void, int, string or a struct type"); return false; } - const RetType ret = currentRet(); + const RetType ret = retSt ? RetType::Struct : currentRet(); lex.advance(); // past the return type if (lex.kind != Tok::Ident) { fail("expected a function name after its return type"); return false; } if (fnCount >= kMaxEntryPoints) { fail("too many functions in one class"); return false; } @@ -1399,11 +1927,17 @@ struct Parser { // The name is captured before the signature, since parsing moves the lexer. const char* const fnName = lex.identBeg; const uint8_t fnNameLen = static_cast(lex.identLen); + // A builtin is found before a script function, so one of the same name would never be called. + if (isReservedWord(fnName, fnNameLen)) { fail("that name is a reserved word"); return false; } + if (table.findStruct(fnName, fnNameLen)) { fail("that name is a type"); return false; } + if (table.find(fnName, fnNameLen)) { fail("that name shadows a built-in function"); return false; } lex.advance(); // the function name uint8_t params = 0; if (!parseParams(¶ms)) return false; - fns[fnCount] = {fnName, fnNameLen, static_cast(ir.count), ret, params}; + fns[fnCount] = {fnName, fnNameLen, static_cast(ir.count), ret, params, + structIndex(retSt)}; curRet = ret; // every return below is checked against it + curRetStruct = retSt; // The IR carries the start INDEX; the lowering turns it into a byte offset. ir.fnIrStart[fnCount] = static_cast(ir.count); ir.fnCount = static_cast(fnCount + 1); @@ -1416,7 +1950,7 @@ struct Parser { const VReg v = alloc(); emit({IrOp::LoadCtrl32, v, 0,0,0,0, scriptArgOffset(a), nullptr, {}}); // Narrowed here, since the callee knows the types and serves every caller once. - narrowToType(v, locals[a].type); + narrowToType(v, paramWordType[a]); emit({IrOp::Spill, 0, v, 0,0,0, a, nullptr, {}}); freeTemp(v); } @@ -1450,7 +1984,7 @@ CompileResult compileSource(const char* source, const BuiltinTable& table, if (!source) { r.error = "no source"; return r; } if (!out || cap == 0) { r.error = "no code buffer"; return r; } - // Sized from the token count against an empirical ratio, since a worst case gets no memory. + // Sized from the token count against the shipped scripts' ratio; a denser script grows the array as it goes. const uint32_t tokens = countTokens(source); IrProgram ir; // The margin covers a program's fixed overhead, so one token cannot round down to nothing. diff --git a/src/core/moonlive/MoonLiveCompiler.h b/src/core/moonlive/MoonLiveCompiler.h index cc6deaf26..368b7386a 100644 --- a/src/core/moonlive/MoonLiveCompiler.h +++ b/src/core/moonlive/MoonLiveCompiler.h @@ -25,7 +25,7 @@ inline constexpr const char* kSpillRefused = "codegen failed: too many live val // A symbol table: a binding asks for an entry by name, so a script's role follows from what it defined. /// What a function hands back, where `void` is the default an acting entry point declares. -enum class RetType : uint8_t { Void, Int, Str }; +enum class RetType : uint8_t { Void, Int, Str, Struct }; struct EntryPoint { const char* name = nullptr; ///< into the source, or the engine's own copy after compile diff --git a/src/core/moonlive/MoonLiveIr.h b/src/core/moonlive/MoonLiveIr.h index 5e54f74a1..a2fc7c39b 100644 --- a/src/core/moonlive/MoonLiveIr.h +++ b/src/core/moonlive/MoonLiveIr.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include "core/moonlive/MoonLiveBuiltins.h" // InlineOp (a neutral opcode tag) #include "platform/platform.h" // alloc/free, since the op array is script-sized @@ -35,7 +36,7 @@ static constexpr uint8_t kMaxVRegs = 32; // A sanity bound rather than the working limit, so a runaway source fails with a diagnostic. static constexpr uint16_t kMaxIrOps = 4096; -// Measured at 0.75 ops per token and never above 0.85, so 1 keeps a real margin. +// The shipped scripts measure up to 0.91 ops per token; structs and `&&` reach 1.6, and grow the array rather than raise every script's reservation. static constexpr uint16_t kIrOpsPerToken = 1; // The op set, in three-address form: a destination plus up to three source operands. @@ -137,12 +138,12 @@ constexpr uint8_t idxCount(int32_t p) { return uint8_t((p >> 16) & 0xff); } // Two per `for`, counted program-wide, so this bounds total loops rather than nesting depth. /// Branch targets one IR program may use. -static constexpr uint8_t kIrLabels = 40; +static constexpr uint8_t kIrLabels = 96; -// THE COST IS STACK: a label is a two-byte code position, since no program passes the 16 KB code cap, so 96 of them take what 48 four-byte ones did. -/// Labels and fixups an assembler's tables hold. -static constexpr uint8_t kAsmLabels = 96; -static constexpr uint8_t kAsmFixups = 96; +// On the heap for one compile; a label is a two-byte code position and a fixup four bytes, under the 16 KB code cap. +/// Labels and fixups an assembler's tables hold, the most an eight-bit index addresses. +static constexpr uint8_t kAsmLabels = 255; +static constexpr uint8_t kAsmFixups = 255; /// Where a label is bound in the code, or kUnbound before it is. using CodePos = uint16_t; static constexpr CodePos kUnbound = 0xFFFF; @@ -212,9 +213,24 @@ struct IrProgram { return ops != nullptr; } - /// Append one op, returning false when it names a register outside the budget. + // By half, as a dynamic array grows, so the reservation can be the typical script's size and a dense one still fits. + /// Enlarge the op array, keeping its contents, up to kMaxIrOps; false at the bound or out of memory. + bool grow() { + if (!ops || cap >= kMaxIrOps) return false; + const uint32_t want = uint32_t(cap) + cap / 2 + 8; + const uint16_t n = static_cast(want > kMaxIrOps ? kMaxIrOps : want); + auto* bigger = static_cast(platform::alloc(sizeof(IrInst) * n)); + if (!bigger) return false; + std::memcpy(bigger, ops, sizeof(IrInst) * count); + platform::free(ops); + ops = bigger; + cap = n; + return true; + } + + /// Append one op, returning false when it names a register outside the budget or the array cannot grow. bool push(const IrInst& i) { - if (!ops || count >= cap) return false; + if (!ops || (count >= cap && !grow())) return false; // Opcode-specific, since not every operand field holds a vreg: a Call's `b` is a count. if (i.dst >= kMaxVRegs) return false; if (i.op == IrOp::Call) { diff --git a/src/core/moonlive/MoonLiveScript.h b/src/core/moonlive/MoonLiveScript.h index bfe11f2ea..30d0bd7f7 100644 --- a/src/core/moonlive/MoonLiveScript.h +++ b/src/core/moonlive/MoonLiveScript.h @@ -54,6 +54,8 @@ class MoonLiveScript { return false; resetPrintBudget(); + // What the last program sized goes before this compile needs memory, which a small board does not have for both. + if (defineHook_) defineHook_(defineCtx_, DefinePhase::Compiling); const char* err = nullptr; uint32_t hash = 0; if (compileScriptFile(engine_, name_, builtins, sysvars, err, &hash)) { diff --git a/src/core/moonlive/MoonLiveSpill.cpp b/src/core/moonlive/MoonLiveSpill.cpp index b93b2e88e..a325fb686 100644 --- a/src/core/moonlive/MoonLiveSpill.cpp +++ b/src/core/moonlive/MoonLiveSpill.cpp @@ -125,22 +125,22 @@ bool spillToBudget(IrProgram& ir, const RegBudget& budget, uint8_t& slotsUsed) { // --- 1. Find the loops, innermost first ---------------------------------------------------- - // Bounded by kIrLabels, since a loop needs a label. - Loop loops[kIrLabels]; + // Half of kIrLabels, since a loop takes two labels, its head and its exit. + Loop loops[kIrLabels / 2]; uint8_t loopCount = 0; { - int32_t labelAt[kIrLabels]; + int16_t labelAt[kIrLabels]; // an op index, below kMaxIrOps for (auto& p : labelAt) p = -1; for (uint16_t i = 0; i < ir.count; i++) if (ir.ops[i].op == IrOp::Label && ir.ops[i].imm >= 0 && ir.ops[i].imm < kIrLabels) - labelAt[ir.ops[i].imm] = i; + labelAt[ir.ops[i].imm] = static_cast(i); for (uint16_t i = 0; i < ir.count; i++) { const IrInst& in = ir.ops[i]; if (in.op != IrOp::BranchNe) continue; if (in.imm < 0 || in.imm >= kIrLabels) { spillDetail().guard = 5; return false; }; // an unbindable label: refuse const int32_t tgt = labelAt[in.imm]; if (tgt < 0 || static_cast(tgt) > i) continue; // a forward branch, not a loop - if (loopCount >= kIrLabels) { spillDetail().guard = 6; return false; }; + if (loopCount >= kIrLabels / 2) { spillDetail().guard = 6; return false; }; loops[loopCount++] = {static_cast(tgt), i}; } // Proper nesting is what makes innermost-first meaningful, so anything else is refused. diff --git a/src/core/moonlive/moonlive_lower.h b/src/core/moonlive/moonlive_lower.h index d25ee1a0d..eced95da2 100644 --- a/src/core/moonlive/moonlive_lower.h +++ b/src/core/moonlive/moonlive_lower.h @@ -98,10 +98,13 @@ size_t lowerWith(IrProgram& ir, uint8_t* out, size_t cap, const RegBudget* squee // Function number plus one while a guard is owed, since it is emitted after the parking. int guardPending = 0; + // The all-ones index marks an unmade IR label, since an assembler hands out at most kAsmLabels below it. + static_assert(kAsmLabels <= LabelId(~LabelId(0)), "the all-ones label index must stay free as the unmade mark"); + constexpr LabelId kUnmade = LabelId(~LabelId(0)); LabelId labels[kIrLabels]; - bool labelMade[kIrLabels] = {}; + for (auto& l : labels) l = kUnmade; auto labelFor = [&](int32_t id) -> LabelId { - if (!labelMade[id]) { labels[id] = a.newLabel(); labelMade[id] = true; } + if (labels[id] == kUnmade) labels[id] = a.newLabel(); return labels[id]; }; diff --git a/src/core/system/MoonCloudModule.h b/src/core/system/MoonCloudModule.h index e1ca24045..d26140d1f 100644 --- a/src/core/system/MoonCloudModule.h +++ b/src/core/system/MoonCloudModule.h @@ -23,7 +23,7 @@ namespace mm { /// ## The browser relay, on a board without HTTPS /// /// A board with no TLS cannot post, so its one outbound call goes into a one-slot outbox instead, a hidden control the open page reads. -/// The page posts it to the server, which accepts a cross-origin request, and presses the hidden `outbox sent` button to empty it. A report then goes out whenever someone has the page open, enough for a usage count. +/// The page posts it to the server, which accepts a cross-origin request, and writes the request's `id` to the hidden `outbox sent` to empty it. A report then goes out whenever someone has the page open, enough for a usage count. /// Two pages open at once can both post the waiting request before either empties it, a rare double the count tolerates. Neither control exists on a board with HTTPS, so the relay costs it nothing but the check. class MoonCloudModule : public MoonModule { public: @@ -47,15 +47,23 @@ class MoonCloudModule : public MoonModule { controls_.addText("outbox", outbox_, kOutboxBytes); controls_.setReadOnly(controls_.count() - 1, true); controls_.setHidden(controls_.count() - 1, true); - controls_.addButton("outbox sent"); + controls_.addControl("outbox sent", acked_, 0, INT32_MAX); controls_.setHidden(controls_.count() - 1, true); } MoonModule::defineControls(); } - /// Empty the outbox once the page has delivered it. + // A restored settings file carries the last acknowledged id, which a new request must never match. + /// Number the next request after the last one acknowledged, which the settings restored. + void setup() override { + lastId_ = acked_; + MoonModule::setup(); + } + + // Only for the waiting request's id, so a late acknowledgement of an earlier one leaves a newer one waiting. + /// Empty the outbox once the page has delivered the request waiting in it. void onControlChanged(const char* name) override { - if (outbox_ && name && std::strcmp(name, "outbox sent") == 0) { + if (outbox_ && outbox_[0] && name && std::strcmp(name, "outbox sent") == 0 && acked_ == pendingId_) { outbox_[0] = 0; notifyValuesChanged(); } @@ -73,7 +81,10 @@ class MoonCloudModule : public MoonModule { bool post(const char* path, const char* body) { if (outbox_) { if (outbox_[0]) return false; - const int n = std::snprintf(outbox_, kOutboxBytes, "{\"path\":\"%s\",\"body\":%s}", path, body); + pendingId_ = lastId_ < INT32_MAX ? lastId_ + 1 : 1; + lastId_ = pendingId_; + const int n = std::snprintf(outbox_, kOutboxBytes, "{\"id\":%ld,\"path\":\"%s\",\"body\":%s}", + static_cast(pendingId_), path, body); if (n < 0 || static_cast(n) >= kOutboxBytes) { outbox_[0] = 0; return false; } notifyValuesChanged(); // to the page on the next transport tick return true; @@ -89,6 +100,9 @@ class MoonCloudModule : public MoonModule { // One request: a usage report is well under this, and a talk message is capped at 280 characters. static constexpr uint16_t kOutboxBytes = 2048; char* outbox_ = nullptr; ///< the request waiting for the page, on a board without HTTPS only + int32_t acked_ = 0; ///< the id the page last delivered, which it writes to `outbox sent` + int32_t pendingId_ = 0; ///< the id of the request waiting in the outbox + int32_t lastId_ = 0; ///< the id given last }; diff --git a/src/light/drivers/Drivers.h b/src/light/drivers/Drivers.h index bea047cb3..afd634236 100644 --- a/src/light/drivers/Drivers.h +++ b/src/light/drivers/Drivers.h @@ -164,6 +164,7 @@ class Drivers : public MoonModule, public LightOutput { // The names live in a member array because the seam holds POINTERS, so a local would dangle. /// List every factory palette at its place in the catalog, held or not, then the user's own `.mlp` files, and publish them to the picker. void refreshLivePalettes() { + livePalettesScanned_ = true; // every scan counts, whichever caller ran it liveCount_ = 0; // The catalog first, in its own order, so a palette's number is the same on every device of one firmware and a slot or a desk can pick it. for (size_t c = 0; c < moonlive::kPaletteCatalogCount && liveCount_ < LivePalettes::kMax; c++) { @@ -499,7 +500,6 @@ class Drivers : public MoonModule, public LightOutput { void setup() override { // The filesystem module mounts first, so this scan is the one that sees every script; from here a file change rescans. refreshLivePalettes(); - livePalettesScanned_ = true; LivePalettes::set(livePtrs_, liveTags_, liveCount_, livePresent_); // The saved choice starts as picking it does, a script palette compiled, since at boot nothing reacts to the value. onControlChanged("palette"); diff --git a/src/light/moonlive/MoonLiveBuiltins_light.h b/src/light/moonlive/MoonLiveBuiltins_light.h index 5553f97ad..aacb6c652 100644 --- a/src/light/moonlive/MoonLiveBuiltins_light.h +++ b/src/light/moonlive/MoonLiveBuiltins_light.h @@ -47,16 +47,6 @@ inline uint8_t byteArg(uintptr_t a) { return v < 0 ? 0 : (v > 255 ? 255 : static_cast(v)); } -// The active palette, with no hsv() beside it: a hue wheel ignores the user's choice. -extern "C" inline uint32_t mm_light_paletteR(const uintptr_t* args, uint32_t, const uint8_t*) { - return colorFromPalette(*Palettes::active(), byteArg(args[0]), byteArg(args[1])).r; -} -extern "C" inline uint32_t mm_light_paletteG(const uintptr_t* args, uint32_t, const uint8_t*) { - return colorFromPalette(*Palettes::active(), byteArg(args[0]), byteArg(args[1])).g; -} -extern "C" inline uint32_t mm_light_paletteB(const uintptr_t* args, uint32_t, const uint8_t*) { - return colorFromPalette(*Palettes::active(), byteArg(args[0]), byteArg(args[1])).b; -} @@ -474,7 +464,7 @@ extern "C" inline uint32_t mm_light_setPaletteColorZ(const uintptr_t* args, uint return 0; } -// One call rather than the three `paletteR/G/B` cost: 1451 us against 1940 us on an S3. +// One call writing the pixel, rather than a palette read and a setRGB. /// Color one light from the active palette at a brightness, dropping out-of-range coordinates. extern "C" inline uint32_t mm_light_setPaletteColor(const uintptr_t* args, uint32_t, const uint8_t*) { const draw::Canvas& cv = drawCanvas(); @@ -595,13 +585,23 @@ extern "C" inline uint32_t mm_light_fade(const uintptr_t* args, uint32_t, const return 0; } -// The color calls are the compiled effects' own draw functions, by the index a script counts lights in. -/// Read one light's red channel; 0 past the end, one channel per call as paletteR/G/B. -extern "C" inline uint32_t mm_light_getR(const uintptr_t* args, uint32_t, const uint8_t*) { return draw::get(drawCanvas(), size_t(args[0])).r; } -/// Read one light's green channel. -extern "C" inline uint32_t mm_light_getG(const uintptr_t* args, uint32_t, const uint8_t*) { return draw::get(drawCanvas(), size_t(args[0])).g; } -/// Read one light's blue channel. -extern "C" inline uint32_t mm_light_getB(const uintptr_t* args, uint32_t, const uint8_t*) { return draw::get(drawCanvas(), size_t(args[0])).b; } +// A struct answer goes back through the call's own argument slots, where the compiler reads it after the call. +/// Read one light's color as an RGB through the compiled effects' own draw::get, black past the end. +extern "C" inline uint32_t mm_light_getRGB(const uintptr_t* args, uint32_t, const uint8_t*) { + const RGB c = draw::get(drawCanvas(), size_t(args[0])); + uintptr_t* out = const_cast(args); + out[0] = c.r; out[1] = c.g; out[2] = c.b; + return 0; +} + +// The active palette, with no hsv() beside it: a hue wheel ignores the user's choice. +/// A color from the active palette at a brightness, as an RGB. +extern "C" inline uint32_t mm_light_paletteRGB(const uintptr_t* args, uint32_t, const uint8_t*) { + const RGB c = colorFromPalette(*Palettes::active(), byteArg(args[0]), byteArg(args[1])); + uintptr_t* out = const_cast(args); + out[0] = c.r; out[1] = c.g; out[2] = c.b; + return 0; +} /// Scale one light by `k` out of 255, through draw::scalePixel. extern "C" inline uint32_t mm_light_mulRGB(const uintptr_t* args, uint32_t, const uint8_t*) { @@ -887,9 +887,10 @@ extern "C" inline uint32_t mm_light_smin(const uintptr_t* args, uint32_t, const inline void addLightColorBuiltins(BuiltinTable& t) { // setRGB writes one bounds-guarded pixel through the StoreElem inline op. t.add({"setRGB", 4, /*returns*/ false, BuiltinKind::Inline, nullptr, InlineOp::StoreElem}); - t.add({"getR", 1, /*returns*/ true, BuiltinKind::Call, &mm_light_getR, {}}); - t.add({"getG", 1, /*returns*/ true, BuiltinKind::Call, &mm_light_getG, {}}); - t.add({"getB", 1, /*returns*/ true, BuiltinKind::Call, &mm_light_getB, {}}); + // A struct answer: the light's color, one call. + Builtin getRGB{"getRGB", 1, /*returns*/ false, BuiltinKind::Call, &mm_light_getRGB, {}}; + getRGB.retStruct = "RGB"; + t.add(getRGB); t.add({"mulRGB", 2, /*returns*/ false, BuiltinKind::Call, &mm_light_mulRGB, {}}); t.add({"addRGB", 4, /*returns*/ false, BuiltinKind::Call, &mm_light_addRGB, {}}); t.add({"blendRGB", 5, /*returns*/ false, BuiltinKind::Call, &mm_light_blendRGB, {}}); @@ -901,6 +902,9 @@ inline const BuiltinTable& lightBuiltins() { // Built once, since it is 2 KB by value and constant after registration. static const BuiltinTable table = [] { BuiltinTable t; + // The types the light vocabulary speaks in, which the compiled effects use as RGB and Coord3D. + t.addStruct({"RGB", CtrlType::Byte, 3, {"r", "g", "b"}}); + t.addStruct({"Coord3D", CtrlType::Int, 3, {"x", "y", "z"}}); // The neutral half first, so a name means the same thing here as in a service. addCommonBuiltins(t); // smin stays here, since it wraps a shape helper and is not neutral. @@ -978,10 +982,10 @@ inline const BuiltinTable& lightBuiltins() { // A palette script's only output, and a no-op in every other role, where no sink is installed. t.add({"setPalEntry", 4, /*returns*/ false, BuiltinKind::Call, &mm_light_setPalEntry, {}}); t.add({"setPalEntryHSV", 4, /*returns*/ false, BuiltinKind::Call, &mm_light_setPalEntryHSV, {}}); - // One channel each, for a script that wants the value rather than a written pixel. - t.add({"paletteR", 2, /*returns*/ true, BuiltinKind::Call, &mm_light_paletteR, {}}); - t.add({"paletteG", 2, /*returns*/ true, BuiltinKind::Call, &mm_light_paletteG, {}}); - t.add({"paletteB", 2, /*returns*/ true, BuiltinKind::Call, &mm_light_paletteB, {}}); + // The color as a value rather than a written pixel, a struct answer. + Builtin paletteRGB{"paletteRGB", 2, /*returns*/ false, BuiltinKind::Call, &mm_light_paletteRGB, {}}; + paletteRGB.retStruct = "RGB"; + t.add(paletteRGB); // Caught here, since a dropped registration surfaces much later as an unknown function. MM_ASSERT_NO_BUILTIN_OVERFLOW(t); return t; diff --git a/src/light/moonlive/MoonLiveEffect.h b/src/light/moonlive/MoonLiveEffect.h index 72a4c3f90..4048fef78 100644 --- a/src/light/moonlive/MoonLiveEffect.h +++ b/src/light/moonlive/MoonLiveEffect.h @@ -167,10 +167,17 @@ class MoonLiveEffect : public EffectBase { moonlive::writeSysVarSlot(script_.engine().controlSlot(offset), value); } - // Two 16-bit planes are 96 KB on a 20-cube, so only a script that advects pays for them. - /// Attach pool(n) and trail(1) to this effect for one defineControls run, or detach them. - static void attachSizers(void* ctx, bool attach) { - if (!attach) { + // A compile frees the last script's pool and trail first, so its compile has their memory; two 16-bit planes are 96 KB on a 20-cube. + /// Free the pool and trail before a compile, and attach pool(n) and trail(1) for its defineControls run, or detach them. + static void attachSizers(void* ctx, moonlive::DefinePhase phase) { + auto* self = static_cast(ctx); + if (phase == moonlive::DefinePhase::Compiling) { + self->particles_.release(); + self->trailWanted_ = false; + self->releaseTrail(); + return; + } + if (phase == moonlive::DefinePhase::Detach) { moonlive::setPoolSizeSink(nullptr, nullptr); moonlive::setTrailSizeSink(nullptr, nullptr); return; diff --git a/src/platform/desktop/moonlive_asm_host.h b/src/platform/desktop/moonlive_asm_host.h index 1ac93b3d9..c8875ed63 100644 --- a/src/platform/desktop/moonlive_asm_host.h +++ b/src/platform/desktop/moonlive_asm_host.h @@ -180,11 +180,11 @@ class HostAssembler { #endif // A pending label reference; the kind matters, a call and a branch using different fields. enum class FixKind : uint8_t { Branch, Call }; - struct Fixup { size_t at; Label label; FixKind kind = FixKind::Branch; }; + struct Fixup { CodePos at; Label label; FixKind kind = FixKind::Branch; }; /// Bounds-checked. void addFixup(size_t at, Label label, FixKind kind = FixKind::Branch) { - if (fixupCount_ >= kMaxFixups) { overflow_ = true; return; } - fixups_[fixupCount_++] = {at, label, kind}; + if (fixupCount_ >= kMaxFixups || at >= kUnbound) { overflow_ = true; return; } + fixups_[fixupCount_++] = {static_cast(at), label, kind}; } // Heap rather than a member array, which put 2 KB on the stack and overflowed a classic ESP32. diff --git a/src/platform/esp32/moonlive_asm_riscv.cpp b/src/platform/esp32/moonlive_asm_riscv.cpp index 9effd6fce..f2da189a2 100644 --- a/src/platform/esp32/moonlive_asm_riscv.cpp +++ b/src/platform/esp32/moonlive_asm_riscv.cpp @@ -74,8 +74,8 @@ void RiscvAssembler::callLabel(Label l, Reg d, bool take) { // Record a pending branch fixup, guarding the fixed table (overflow_ rather than an OOB write). void RiscvAssembler::addFixup(size_t at, Label label, FixKind kind) { - if (fixupCount_ >= kMaxFixups) { overflow_ = true; return; } - fixups_[fixupCount_++] = {at, label, kind}; + if (fixupCount_ >= kMaxFixups || at >= kUnbound) { overflow_ = true; return; } + fixups_[fixupCount_++] = {static_cast(at), label, kind}; } // --- I/R/S-type encoders (rd/rs1/rs2 are real x-register numbers) --- diff --git a/src/platform/esp32/moonlive_asm_riscv.h b/src/platform/esp32/moonlive_asm_riscv.h index 0f9a06239..ce3a58941 100644 --- a/src/platform/esp32/moonlive_asm_riscv.h +++ b/src/platform/esp32/moonlive_asm_riscv.h @@ -163,7 +163,7 @@ class RiscvAssembler { CodePos labelPos_[kMaxLabels]; uint8_t labelCount_ = 0; - struct Fixup { size_t at; Label label; FixKind kind = FixKind::Branch; }; + struct Fixup { CodePos at; Label label; FixKind kind = FixKind::Branch; }; Fixup fixups_[kMaxFixups]; uint8_t fixupCount_ = 0; diff --git a/src/platform/esp32/moonlive_asm_xtensa.cpp b/src/platform/esp32/moonlive_asm_xtensa.cpp index 77bb01994..a3064c7f7 100644 --- a/src/platform/esp32/moonlive_asm_xtensa.cpp +++ b/src/platform/esp32/moonlive_asm_xtensa.cpp @@ -188,8 +188,8 @@ void XtensaAssembler::bind(Label l) { if (len_ >= kUnbound) overflow_ = true; el // Record a pending branch fixup, guarding the fixed table (overflow_ rather than an OOB write). void XtensaAssembler::addFixup(size_t at, Label label, FixKind kind) { - if (fixupCount_ >= kMaxFixups) { overflow_ = true; return; } - fixups_[fixupCount_++] = {at, label, kind}; + if (fixupCount_ >= kMaxFixups || at >= kUnbound) { overflow_ = true; return; } + fixups_[fixupCount_++] = {static_cast(at), label, kind}; } // A call to a function in this block, the script-to-script call. @@ -339,15 +339,17 @@ void XtensaAssembler::sarImm(Reg d, Reg a, uint8_t n) { emit3(((0x2u | (uint32_t(n) >> 4)) << 20) | 0x010000u | (uint32_t(ar(d)) << 12) | ((uint32_t(n) & 0x0fu) << 8) | (uint32_t(ar(a)) << 4)); } -// The LOGICAL right shift. srli only encodes 1..15; a shift of 16 is spelled extui aD, aA, 16, 16, which extracts the top 16 bits, between them they cover every shift the front end emits. +// The LOGICAL right shift. srli encodes 1..15; a shift of 16..31 is extui aD, aA, n, 32-n, which extracts the top 32-n bits, a width extui's four-bit field holds. void XtensaAssembler::shrImm(Reg d, Reg a, uint8_t n) { if (n >= 1 && n <= 15) { emit3(0x410000u | (uint32_t(ar(d)) << 12) | (uint32_t(n) << 8) | (uint32_t(ar(a)) << 4)); return; } - // extui's width field caps at 16, so 16 is the only wide shift it can express. Anything else has NO encoding here, and falling through to a shift-by-16 would emit a silently wrong constant, the failure mode movImm above was just fixed for. Fail the compile instead. - if (n != 16) { overflow_ = true; return; } - emit3(0xf50000u | (uint32_t(ar(d)) << 12) | (uint32_t(ar(a)) << 4)); // extui aD, aA, 16, 16 + // Zero and 32 up have no encoding, and falling through to another amount would emit a silently wrong value. + if (n < 16 || n >= 32) { overflow_ = true; return; } + // op2 = width - 1, op1 = 4 plus the shift's fifth bit, s = the shift's low nibble. + emit3((uint32_t(31 - n) << 20) | ((0x4u | (uint32_t(n) >> 4)) << 16) | (uint32_t(ar(d)) << 12) | + ((uint32_t(n) & 0x0fu) << 8) | (uint32_t(ar(a)) << 4)); } // a12: the dedicated address scratch, OUTSIDE the R0..R9 -> a2..a11 vreg map, so computing an address into it can never clobber a live virtual register. Shared by every indexed access. static constexpr uint8_t kAddrScratch = 12; // a12 diff --git a/src/platform/esp32/moonlive_asm_xtensa.h b/src/platform/esp32/moonlive_asm_xtensa.h index d5019d902..00e6c0e5e 100644 --- a/src/platform/esp32/moonlive_asm_xtensa.h +++ b/src/platform/esp32/moonlive_asm_xtensa.h @@ -156,7 +156,7 @@ class XtensaAssembler { void emit3(uint32_t w); // A pending label reference; the kind matters, a call's displacement being scaled and a jump's not. enum class FixKind : uint8_t { Jump, Call }; - struct Fixup { size_t at; Label label; FixKind kind = FixKind::Jump; }; + struct Fixup { CodePos at; Label label; FixKind kind = FixKind::Jump; }; /// Enqueue a fixup (bounds-checked). void addFixup(size_t at, Label label, FixKind kind = FixKind::Jump); diff --git a/src/platform/teensy/moonlive_asm_thumb2.cpp b/src/platform/teensy/moonlive_asm_thumb2.cpp index 9c53a8b8b..1f8f3aecf 100644 --- a/src/platform/teensy/moonlive_asm_thumb2.cpp +++ b/src/platform/teensy/moonlive_asm_thumb2.cpp @@ -65,8 +65,8 @@ Label Thumb2Assembler::newLabel() { void Thumb2Assembler::bind(Label l) { if (len_ >= kUnbound) overflow_ = true; else if (l < kMaxLabels) labelPos_[l] = static_cast(len_); } void Thumb2Assembler::addFixup(size_t at, Label label, FixKind kind) { - if (fixupCount_ >= kMaxFixups) { overflow_ = true; return; } - fixups_[fixupCount_++] = {at, label, kind}; + if (fixupCount_ >= kMaxFixups || at >= kUnbound) { overflow_ = true; return; } + fixups_[fixupCount_++] = {static_cast(at), label, kind}; } // --- encoders over machine register numbers --- diff --git a/src/platform/teensy/moonlive_asm_thumb2.h b/src/platform/teensy/moonlive_asm_thumb2.h index faaa0c156..2256a2f30 100644 --- a/src/platform/teensy/moonlive_asm_thumb2.h +++ b/src/platform/teensy/moonlive_asm_thumb2.h @@ -148,7 +148,7 @@ class Thumb2Assembler { CodePos labelPos_[kMaxLabels]; uint8_t labelCount_ = 0; - struct Fixup { size_t at; Label label; FixKind kind; }; + struct Fixup { CodePos at; Label label; FixKind kind; }; Fixup fixups_[kMaxFixups]; uint8_t fixupCount_ = 0; diff --git a/src/platform/teensy/platform_teensy_ota.cpp b/src/platform/teensy/platform_teensy_ota.cpp index 4a9ee1e98..9e68c6f6e 100644 --- a/src/platform/teensy/platform_teensy_ota.cpp +++ b/src/platform/teensy/platform_teensy_ota.cpp @@ -51,7 +51,7 @@ constexpr uint32_t kMarkPrefixLen = sizeof("MoonLight image for ") - 1; alignas(4) uint8_t g_sector[kSectorBytes]; // one sector in RAM, for the upload and for the copy uint32_t g_stagedBytes = 0; // nonzero once a checked image waits for the reboot -// Block-aligned, so erasing a block of the program never touches a staged block not yet copied. +// Block-aligned, so erasing a block of the program never touches an uncopied staged block. uint32_t stagingBase() { return (kFlashBase + static_cast(firmwareSize()) + kBlockBytes - 1) & ~(kBlockBytes - 1); } uint32_t programEnd() { return kFlashBase + static_cast(firmwarePartition()); } diff --git a/src/ui/app.js b/src/ui/app.js index b9b42205a..961284ba1 100644 --- a/src/ui/app.js +++ b/src/ui/app.js @@ -1633,9 +1633,14 @@ function watchInstall(kind) { // here), so this is the one place an XMLHttpRequest earns its keep: xhr.upload.onprogress is the // only way to know how far a browser-pushed install has got. The device-fetched installs report // through the byte counts in their status line instead, so every install has a bar. -function uploadWithProgress(route, file, ui) { +function uploadWithProgress(route, file, ui, signal) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); + // The overlay's cancel aborts through `signal`, which an XMLHttpRequest takes as a listener rather than an option. + if (signal) { + if (signal.aborted) { reject(new Error("canceled")); return; } + signal.addEventListener("abort", () => xhr.abort(), { once: true }); + } ui.watchStalls(); xhr.open("POST", route); xhr.setRequestHeader("Content-Type", "application/octet-stream"); @@ -2331,10 +2336,12 @@ function createCard(mod, depth) { // Upload an image the browser holds, behind the overlay every install shows. The device // cannot report this transfer's progress (the request stays open for it), so the bar // follows the upload itself. - const installBlob = async (blob, ui = showUpdateOverlay()) => { + const installBlob = async (blob, ui = showUpdateOverlay(), ctl = new AbortController()) => { + ui.setUpload(ctl); + if (ctl.signal.aborted) { ui.fail("Install canceled."); return; } ui.status(`Installing ${fmSize(blob.size)}โ€ฆ`); try { - await uploadWithProgress(k.uploadRoute, blob, ui); + await uploadWithProgress(k.uploadRoute, blob, ui, ctl.signal); if (k.reboots) { ui.busy("Rebootingโ€ฆ"); if (!await waitForReboot()) { ui.fail("The device did not come back within two minutes: check it, then reload."); return; } @@ -2349,23 +2356,31 @@ function createCard(mod, depth) { }; // For a device that cannot fetch a URL (no TLS): the browser downloads the image and uploads it. - // GitHub's release assets refuse a cross-origin read, so a release asset comes from its - // copy on the installer's Pages site, which allows one. + // GitHub's release assets refuse a cross-origin read, so one of our release assets comes from + // its copy on the installer's Pages site, which allows one; any other URL is fetched as given. const installViaBrowser = async (url) => { - const m = /\/releases\/download\/([^/]+)\/([^/]+)$/.exec(url); - const source = m ? `https://moonmodules.org/MoonLight/install/releases/${m[1]}/${m[2]}` : url; + let source = url; + try { + const u = new URL(url); + const m = /^\/MoonModules\/MoonLight\/releases\/download\/([^/]+)\/([^/]+)$/.exec(u.pathname); + if (u.protocol === "https:" && u.hostname === "github.com" && m) + source = `https://moonmodules.org/MoonLight/install/releases/${m[1]}/${m[2]}`; + } catch (_) { /* not a URL the page can parse: fetched as given, and its own error says so */ } const ui = showUpdateOverlay(); + // One controller for both transfers, so cancel stops the download and the upload that follows it. + const ctl = new AbortController(); + ui.setUpload(ctl); ui.status("Downloading the imageโ€ฆ"); let blob; try { - const res = await fetch(source); + const res = await fetch(source, { signal: ctl.signal }); if (!res.ok) throw new Error(`HTTP ${res.status} from ${source}`); blob = await res.blob(); } catch (err) { - ui.fail("Download failed: " + err.message); + ui.fail(ctl.signal.aborted ? "Install canceled." : "Download failed: " + err.message); return; } - await installBlob(blob, ui); + await installBlob(blob, ui, ctl); }; // Hand a URL to the device. Every install route answers 202 and works on its own task, so @@ -6455,8 +6470,8 @@ function consented(mod) { const kMoonCloudUrl = "https://mooncloud-stats.moonmodules.workers.dev"; -// A board without HTTPS leaves its MoonCloud request in the hidden `outbox` control; the device page, which has HTTPS, posts it and presses `outbox sent` to empty it. -// One request at a time, and after a failed post a pause, so an unreachable server is not asked once a second. +// A board without HTTPS leaves its MoonCloud request in the hidden `outbox` control; the device page, which has HTTPS, posts it and writes its id to `outbox sent` to empty it. +// One request at a time, and after a failed post or acknowledgement a pause, so the server is not asked once a second, nor sent the same request again at once. let moonCloudRelayBusy = false; let moonCloudRelayRetryAt = 0; async function relayMoonCloudOutbox() { @@ -6466,7 +6481,7 @@ async function relayMoonCloudOutbox() { if (!raw) return; let req; try { req = JSON.parse(raw); } catch (_) { return; } - if (typeof req.path !== "string" || !req.path.startsWith("/api/")) return; + if (typeof req.path !== "string" || !req.path.startsWith("/api/") || !Number.isInteger(req.id)) return; moonCloudRelayBusy = true; try { const res = await fetch(kMoonCloudUrl + req.path, { @@ -6475,7 +6490,7 @@ async function relayMoonCloudOutbox() { body: JSON.stringify(req.body), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); - await sendControl(cloud.name, "outbox sent", 1); + if (!await sendControl(cloud.name, "outbox sent", req.id)) throw new Error("acknowledgement refused"); } catch (_) { moonCloudRelayRetryAt = Date.now() + 30000; } finally { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index bc54db443..5d5b21432 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -203,6 +203,8 @@ add_executable(mm_tests unit/light/unit_MoonLiveParticles.cpp unit/light/unit_MoonLiveLayout.cpp unit/light/unit_MoonLiveScripts.cpp + unit/light/unit_MoonLiveScripts_ir_golden.cpp + unit/light/unit_MoonLiveStructs.cpp unit/light/unit_MoonLiveScriptResolve.cpp unit/light/unit_MoonLiveMotion.cpp unit/light/unit_Layouts_container.cpp diff --git a/test/js/installer-teensy-writer.test.mjs b/test/js/installer-teensy-writer.test.mjs index 32bee39bc..aed4ba023 100644 --- a/test/js/installer-teensy-writer.test.mjs +++ b/test/js/installer-teensy-writer.test.mjs @@ -15,6 +15,8 @@ import { halfkayReports, imageProblem, isTeensySerial, + otherTeensyPorts, + rebootedSerialPort, } from "../../mooninstaller/teensy-writer.js"; // A flat image as the build writes it: the "FCFB" configuration block first, then code, with erased gaps. @@ -81,3 +83,22 @@ test("an erase zeroes the settings' superblock pair, 8 KB at the bottom of the 1 assert.ok(offset + 1024 <= TEENSY41_CODE_BYTES, "inside what the bootloader writes"); } }); + +// Every Teensy reports the same USB ids, and the installed board's port vanishes while it reboots, so a second granted Teensy would be the only match: it must never be the one read or sent defaults. +test("after the reboot the installer finds the installed board, never another granted Teensy", async () => { + const teensy = () => ({ getInfo: () => ({ usbVendorId: 0x16C0, usbProductId: 0x0483 }) }); + const installing = teensy(), other = teensy(), esp = { getInfo: () => ({ usbVendorId: 0x10C4, usbProductId: 0xEA60 }) }; + const saved = globalThis.navigator; + try { + let granted = [installing, other, esp]; + Object.defineProperty(globalThis, "navigator", { value: { serial: { getPorts: async () => granted } }, configurable: true }); + const others = await otherTeensyPorts(installing); + assert.deepEqual(others, [other]); + granted = [other, esp]; // rebooting: only the other board answers + assert.equal(await rebootedSerialPort(0, others), null); // so nothing is taken + granted = [other, esp, installing]; // back on the bus + assert.equal(await rebootedSerialPort(0, others), installing); + } finally { + Object.defineProperty(globalThis, "navigator", { value: saved, configurable: true }); + } +}); diff --git a/test/python/test_scripts_are_cpp.py b/test/python/test_scripts_are_cpp.py index bcdd8667e..203629f62 100644 --- a/test/python/test_scripts_are_cpp.py +++ b/test/python/test_scripts_are_cpp.py @@ -52,7 +52,8 @@ def builtins(): found = [] for path in BUILTINS: text = path.read_text(encoding="utf-8") - found += re.findall(r't\.add\(\{"([A-Za-z0-9_]+)",\s*(\d+)', text) + # Both registration shapes: `t.add({"name", n, ...})`, and a `Builtin` built first so a field past the aggregate's order can be set. + found += re.findall(r'(?:t\.add\(\{|Builtin\s*\w*\s*\{)"([A-Za-z0-9_]+)",\s*(\d+)', text) assert found, "no builtins parsed: the registration shape changed" # A name can register twice (an overload by arity); keep the widest, since a call with fewer # arguments still matches a declaration with more only if defaults exist, which these lack. @@ -62,6 +63,28 @@ def builtins(): return sorted(widest.items()) +def structs(): + """Every struct type the tables register: (name, field type, field names), from `addStruct`.""" + found = [] + for path in BUILTINS: + text = path.read_text(encoding="utf-8") + for name, ctype, fields in re.findall(r'addStruct\(\{"(\w+)",\s*CtrlType::(\w+),\s*\d+,\s*\{([^}]*)\}', text): + found.append((name, "byte" if ctype == "Byte" else "int", re.findall(r'"(\w+)"', fields))) + return found + + +def struct_returns(): + """Builtins answering a struct, by name: a `Builtin x{"name", ...}` whose `x.retStruct` is set.""" + found = {} + for path in BUILTINS: + text = path.read_text(encoding="utf-8") + var_to_name = dict(re.findall(r'Builtin\s+(\w+)\s*\{"(\w+)"', text)) + for var, st in re.findall(r'(\w+)\.retStruct\s*=\s*"(\w+)"', text): + if var in var_to_name: + found[var_to_name[var]] = st + return found + + def prelude() -> str: lines = [ "#include ", @@ -71,6 +94,12 @@ def prelude() -> str: ] for v in SYSVARS: lines.append(f"inline int {v} = 0;") + # An aggregate, so `RGB(1, 2, 3)` is C++20's parenthesized initialization and `RGB c;` a declaration. + types = structs() + assert types, "no struct types parsed: the addStruct shape changed" + for name, ftype, fields in types: + lines.append(f"struct {name} {{ {ftype} {', '.join(fields)}; }};") + answers = struct_returns() # Builtins whose first argument is a NAME IN QUOTES are declared by hand below: the generated # int-only form cannot express a string, and emitting both makes the call ambiguous rather than # resolving it. @@ -79,7 +108,12 @@ def prelude() -> str: if name in byString: continue args = ", ".join(["int"] * argc) - lines.append(f"int {name}({args});") + lines.append(f"{answers.get(name, 'int')} {name}({args});") + # A struct argument fills as many positions as it has fields, so each run of positions a struct can fill gets its overload. + for st, _, fields in types: + for k in range(argc - len(fields) + 1): + parts = ["int"] * k + [st] + ["int"] * (argc - k - len(fields)) + lines.append(f"{answers.get(name, 'int')} {name}({', '.join(parts)});") # toFixed/toInt are KEYWORDS, not builtins: the compiler recognizes them inline so each costs # one shift instruction rather than a host call (MoonLiveCompiler.cpp), so the registration # table above does not carry them. diff --git a/test/scenarios/core/scenario_Control_a_palette_preset_changes_only_the_palette.json b/test/scenarios/core/scenario_Control_a_palette_preset_changes_only_the_palette.json index 72421a5b1..5d1a0f5eb 100644 --- a/test/scenarios/core/scenario_Control_a_palette_preset_changes_only_the_palette.json +++ b/test/scenarios/core/scenario_Control_a_palette_preset_changes_only_the_palette.json @@ -178,10 +178,10 @@ "p95": 324, "min": 5, "max": 462, - "n": 30, - "samples": [5, 462, 324, 5, 5, 5, 7, 5, 6, 5, 6, 6, 6, 5, 5, 13, 6, 5, 11, 8, 8, 8, 8, 8, 9, 14, 8, 8, 8, 11] + "n": 31, + "samples": [5, 462, 324, 5, 5, 5, 7, 5, 6, 5, 6, 6, 6, 5, 5, 13, 6, 5, 11, 8, 8, 8, 8, 8, 9, 14, 8, 8, 8, 11, 7] }, - "last_updated": "2026-10-10" + "last_updated": "2026-10-12" }, "esp32s3-n16r8": { "tick_us": { diff --git a/test/scenarios/light/scenario_Palettes_a_script_drives_the_effects.json b/test/scenarios/light/scenario_Palettes_a_script_drives_the_effects.json index 31dcd4de7..56a934032 100644 --- a/test/scenarios/light/scenario_Palettes_a_script_drives_the_effects.json +++ b/test/scenarios/light/scenario_Palettes_a_script_drives_the_effects.json @@ -107,9 +107,9 @@ "min": 6, "max": 74, "n": 32, - "samples": [11, 6, 74, 74, 7, 6, 6, 7, 6, 6, 6, 7, 8, 11, 7, 7, 9, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 7, 9, 71, 18] + "samples": [6, 74, 74, 7, 6, 6, 7, 6, 6, 6, 7, 8, 11, 7, 7, 9, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 7, 9, 71, 18, 7] }, - "last_updated": "2026-10-11" + "last_updated": "2026-10-12" }, "teensy41": { "tick_us": { @@ -202,9 +202,9 @@ "min": 6, "max": 115, "n": 32, - "samples": [7, 7, 115, 108, 8, 6, 6, 7, 7, 7, 7, 7, 7, 8, 6, 6, 11, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 6, 80, 17] + "samples": [7, 115, 108, 8, 6, 6, 7, 7, 7, 7, 7, 7, 8, 6, 6, 11, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 6, 80, 17, 7] }, - "last_updated": "2026-10-11" + "last_updated": "2026-10-12" }, "teensy41": { "tick_us": { diff --git a/test/unit/core/moonlive_device_codegen.inc b/test/unit/core/moonlive_device_codegen.inc index 77f5f32e2..b3b2daac6 100644 --- a/test/unit/core/moonlive_device_codegen.inc +++ b/test/unit/core/moonlive_device_codegen.inc @@ -169,46 +169,60 @@ TEST_CASE("the " MM_ISA_NAME " machine code for the shipped scripts is what it w } // Degrade, never miscompile: a script the backend cannot encode must fail cleanly. The emitters -// signal that by returning zero bytes, which compileSource turns into a diagnostic โ€” the device +// signal that by returning zero bytes, which compileSource turns into a diagnostic: the device // then shows the error and keeps rendering, instead of executing whatever the truncation produced. TEST_CASE("an unencodable script is refused, not truncated, on " MM_ISA_NAME) { bool ok = true; // A loop nest far past what any label/fixup table holds. std::string deep; - for (int i = 0; i < 40; i++) deep += "for (i" + std::to_string(i) + " = 0; i" + + for (int i = 0; i < 60; i++) deep += "for (i" + std::to_string(i) + " = 0; i" + std::to_string(i) + " < 2; i" + std::to_string(i) + " = i" + std::to_string(i) + " + 1) {"; deep += "addLight(0, 0, 0);"; - for (int i = 0; i < 40; i++) deep += "}"; + for (int i = 0; i < 60; i++) deep += "}"; const size_t n = emitLen(deep.c_str(), mm::moonlive::layoutSysVars(), ok); CHECK(n == 0); // refused CHECK_FALSE(ok); // and said so } -// The assembler is a LOCAL in lowerWith, and lowerWith runs on the render task, so its size is -// stack a 12 KB task has to have spare while a script compiles. Its two fixed tables dominate it: -// 2 bytes per label (a code position, under the 16 KB code cap) and 8 per fixup, so raising -// kAsmLabels/kAsmFixups is a stack decision wearing a capacity costume. 48 four-byte labels and 96 -// fixups put lowerWith at 1120 bytes on the classic ESP32, the largest frame on the compile chain -// (2128 bytes nested); 96 two-byte labels take the same table bytes. -// -// This bound is a TRIPWIRE, not a target: it exists so that raising the tables is a deliberate act -// with a number attached, rather than something noticed when a board bootloops. A P4 has already -// been lost to an oversized frame on this path. If this fails, either the growth is wrong, or the -// tables belong on the heap next to the code buffer, which was moved off the stack for this reason. -TEST_CASE("the " MM_ISA_NAME " assembler stays small enough to build on a render task") { - // Asserted in the tables' DEVICE bytes rather than sizeof(), because sizeof() here is the host's: - // these backends are compiled on a 64-bit machine to run their real emitter, a Fixup holds a - // size_t, and the host object overstates the device one. A label is a CodePos everywhere and a - // device Fixup is 8 bytes, so the device bytes follow from the entry counts. - // - // The device figure, measured on the classic ESP32 image rather than derived: lowerWith's frame - // is 1120 bytes with these tables at 960 bytes. Re-measure with `xtensa-esp-elf-objdump -d` and - // grep the `entry a1, N` of lowerWith after changing either. - constexpr size_t kDeviceFixupBytes = 8; - const size_t tableBytes = mm::moonlive::kAsmLabels * sizeof(mm::moonlive::CodePos) + mm::moonlive::kAsmFixups * kDeviceFixupBytes; - INFO(MM_ISA_NAME " assembler = " << sizeof(MM_ISA_ASM) << " bytes on this 64-bit host, tables " << tableBytes << " on a device"); - CHECK(tableBytes <= 1280); // 64 four-byte labels and 128 fixups, the allowance before labels shrank +// The language additions expand into the scalar operations every backend already emits, so each compiles here with nothing new in the backend. +TEST_CASE("structs, joined conditions and the arithmetic builtins compile for " MM_ISA_NAME) { + const char* src = + "class T {\n" + " RGB keep;\n" + " Coord3D at;\n" + " RGB warm(int k) { return RGB(k, k / 2, 10); }\n" + " Coord3D move(Coord3D p, Coord3D d) { return Coord3D(p.x + d.x, p.y + d.y, p.z + d.z); }\n" + " void tick() {\n" + " int k = 5;\n" + " RGB c = paletteRGB(k, 255);\n" + " at = move(at, Coord3D(1, -1, 0));\n" + " for (int i = 0; i < 8; i = i + 1) {\n" + " if (i > 2 && !(i == 5) || abs(at.x) > 100) { setRGB(i, c); } else { setRGB(i, warm(clamp(k, 0, 255))); }\n" + " k = min(k + 1, max(k, 9));\n" + " }\n" + " keep = getRGB(0);\n" + " setRGB(8, keep.r, getRGB(1).g, k);\n" + " byte b = k + 1;\n" // a byte narrows by a shift of 24, which Xtensa once could not encode + " setRGB(9, b, 0, 0);\n" + " }\n" + "}\n"; + std::vector out(mm::moonlive::codeCapFor(mm::moonlive::countTokens(src))); + static char strings[mm::moonlive::CompileResult::kStringPool]; + const auto r = mm::moonlive::compileSource(src, mm::moonlive::lightBuiltins(), mm::moonlive::effectSysVars(), + out.data(), out.size(), nullptr, MM_ISA_LOWER, strings, sizeof(strings)); + INFO(std::string(r.error)); + CHECK(r.ok); + CHECK(r.len > 0); +} + +// lowerWith builds the assembler in a heap block for the length of one compile, so its tables cost heap on the board that compiles, never a task's stack. +// A label is a two-byte code position and a fixup four bytes (position, label, kind), the same on the host as on a device, so sizeof() measures what a board allocates. +TEST_CASE("the " MM_ISA_NAME " assembler's tables fit a small heap block") { + const size_t tableBytes = mm::moonlive::kAsmLabels * sizeof(mm::moonlive::CodePos) + mm::moonlive::kAsmFixups * 4; + INFO(MM_ISA_NAME " assembler = " << sizeof(MM_ISA_ASM) << " bytes, tables " << tableBytes); + CHECK(tableBytes <= 1536); + CHECK(sizeof(MM_ISA_ASM) <= tableBytes + 128); // the tables are the assembler: a wider fixup shows here first } // A class whose functions call each other has to place each function where a call can REACH it. diff --git a/test/unit/core/unit_MoonCloudModule.cpp b/test/unit/core/unit_MoonCloudModule.cpp index f01441acd..666d15df7 100644 --- a/test/unit/core/unit_MoonCloudModule.cpp +++ b/test/unit/core/unit_MoonCloudModule.cpp @@ -19,9 +19,14 @@ std::string outbox(const mm::MoonModule& m) { const auto* c = control(m, "outbox"); return c ? static_cast(c->ptr) : ""; } +// What the page does once it has posted a request: write that request's id to `outbox sent`. +void acknowledge(mm::MoonModule& m, int32_t id) { + *static_cast(const_cast(control(m, "outbox sent")->ptr)) = id; + m.onControlChanged("outbox sent"); +} } // namespace -TEST_CASE("the relay's controls exist only on a board that relays, hidden and never saved") { +TEST_CASE("the relay's controls exist only on a board that relays, hidden, the request itself never saved") { mm::MoonCloudModule plain(false); plain.defineControls(); CHECK(control(plain, "outbox") == nullptr); @@ -38,21 +43,22 @@ TEST_CASE("the relay's controls exist only on a board that relays, hidden and ne CHECK(sent->hidden); } -TEST_CASE("a post waits in the outbox as its path and body, and the next is refused until the page empties it") { +TEST_CASE("a post waits in the outbox as its id, path and body, and the next is refused until the page empties it") { mm::MoonCloudModule cloud(true); cloud.defineControls(); + cloud.setup(); CHECK(cloud.canSend()); CHECK(cloud.relaysThroughBrowser()); CHECK(outbox(cloud).empty()); CHECK(cloud.post("/api/report", "{\"event\":\"install\"}")); - CHECK(outbox(cloud) == "{\"path\":\"/api/report\",\"body\":{\"event\":\"install\"}}"); + CHECK(outbox(cloud) == "{\"id\":1,\"path\":\"/api/report\",\"body\":{\"event\":\"install\"}}"); CHECK_FALSE(cloud.post("/api/talk", "{\"text\":\"hi\"}")); // one slot: the report still waits - CHECK(outbox(cloud) == "{\"path\":\"/api/report\",\"body\":{\"event\":\"install\"}}"); + CHECK(outbox(cloud) == "{\"id\":1,\"path\":\"/api/report\",\"body\":{\"event\":\"install\"}}"); CHECK(cloud.outboxWaiting()); - cloud.onControlChanged("outbox sent"); + acknowledge(cloud, 1); CHECK(outbox(cloud).empty()); CHECK_FALSE(cloud.outboxWaiting()); CHECK(cloud.post("/api/talk", "{\"text\":\"hi\"}")); @@ -65,3 +71,29 @@ TEST_CASE("a request too large for the outbox is refused rather than truncated") CHECK_FALSE(cloud.post("/api/talk", body.c_str())); CHECK(outbox(cloud).empty()); } + +// A second open page can acknowledge a request after the first page already did and a newer one took its place: that acknowledgement must not drop the newer request unsent. +TEST_CASE("a late acknowledgement of an earlier request leaves the newer one waiting") { + mm::MoonCloudModule cloud(true); + cloud.defineControls(); + cloud.setup(); + REQUIRE(cloud.post("/api/report", "{}")); + acknowledge(cloud, 1); + REQUIRE(cloud.post("/api/talk", "{}")); // id 2 + acknowledge(cloud, 1); // the other page, late + CHECK(cloud.outboxWaiting()); + acknowledge(cloud, 2); + CHECK_FALSE(cloud.outboxWaiting()); +} + +// The acknowledgement is a saved value, so a boot restores it: the ids count on past it, or the restored value could match and empty a request no page delivered. +TEST_CASE("after a reboot the ids count on from the last acknowledged one") { + mm::MoonCloudModule cloud(true); + cloud.defineControls(); + *static_cast(const_cast(control(cloud, "outbox sent")->ptr)) = 7; // what the settings file restored + cloud.setup(); + REQUIRE(cloud.post("/api/report", "{}")); + CHECK(outbox(cloud).rfind("{\"id\":8,", 0) == 0); + acknowledge(cloud, 7); + CHECK(cloud.outboxWaiting()); +} diff --git a/test/unit/core/unit_MoonStatsModule.cpp b/test/unit/core/unit_MoonStatsModule.cpp index e49309e1d..fafd4c872 100644 --- a/test/unit/core/unit_MoonStatsModule.cpp +++ b/test/unit/core/unit_MoonStatsModule.cpp @@ -312,7 +312,9 @@ TEST_CASE("the consent explanation is a status that follows the answer") { CHECK(std::string(stats.status()).find("Switch on") != std::string::npos); stats.setConsent(true); - CHECK(stats.status() == nullptr); // answered: nothing left to explain + // Answered: nothing left to explain, unless this build has no HTTPS client (a sanitizer build), which says it cannot send. + if (stats.canSend()) CHECK(stats.status() == nullptr); + else CHECK(std::string(stats.status() ? stats.status() : "").find("cannot send") != std::string::npos); stats.setConsent(false); REQUIRE(stats.status() != nullptr); // and it comes back if they change their mind @@ -340,7 +342,10 @@ TEST_CASE("a report handed to the page counts as sent only once the page deliver CHECK(stats->reportDue()); CHECK(cloud->outboxWaiting()); - cloud->onControlChanged("outbox sent"); // the page delivered it + // The page delivered it, and wrote the request's id back: the first request this boot is 1. + for (uint8_t i = 0; i < cloud->controls().count(); i++) + if (std::string(cloud->controls()[i].name) == "outbox sent") *static_cast(const_cast(cloud->controls()[i].ptr)) = 1; + cloud->onControlChanged("outbox sent"); stats->tick1s(); CHECK_FALSE(stats->reportDue()); } diff --git a/test/unit/core/unit_moonlive_codegen_xtensa.cpp b/test/unit/core/unit_moonlive_codegen_xtensa.cpp index 89cb50a5f..4ad58b220 100644 --- a/test/unit/core/unit_moonlive_codegen_xtensa.cpp +++ b/test/unit/core/unit_moonlive_codegen_xtensa.cpp @@ -293,6 +293,26 @@ TEST_CASE("Xtensa shlImm encodes 32-n where sarImm encodes n") { CHECK(r.bytes()[2] == 0x31); } +// A byte narrows by a shift up 24 and back, and the wide shifts had no encoding, so `byte b = x + 1;` failed on ESP32. +TEST_CASE("Xtensa shrImm reaches every shift from 16 to 31 through extui") { + using Asm = mm_xtensa_backend::mm::moonlive::XtensaAssembler; + using mm_xtensa_backend::mm::moonlive::R0; + using mm_xtensa_backend::mm::moonlive::R1; + // The bytes xtensa-esp32-elf-as emits for extui a2, a3, n, 32-n. + const struct { uint8_t n; uint8_t b[3]; } kCases[] = { + {16, {0x30, 0x20, 0xf5}}, {24, {0x30, 0x28, 0x75}}, {17, {0x30, 0x21, 0xe5}}, {31, {0x30, 0x2f, 0x05}}}; + for (const auto& c : kCases) { + CAPTURE(int(c.n)); + Asm a(64); a.shrImm(R0, R1, c.n); + REQUIRE(a.size() == 3); + CHECK(a.bytes()[0] == c.b[0]); + CHECK(a.bytes()[1] == c.b[1]); + CHECK(a.bytes()[2] == c.b[2]); + } + Asm none(64); none.shrImm(R0, R1, 32); + CHECK(none.overflowed()); +} + // The 4-byte slot access, in the NARROW forms: l32i.n/s32i.n are 2 bytes where the halfword forms are 3, so every scalar access got SMALLER as well as wider. The offset field holds offset/4, which is the encoding that would silently address four times too far if read as a byte offset, pinned against the ESP-IDF assembler's own bytes. TEST_CASE("Xtensa load32 emits the two-byte l32i.n with a scaled offset") { using Asm = mm_xtensa_backend::mm::moonlive::XtensaAssembler; diff --git a/test/unit/core/unit_moonlive_compiler.cpp b/test/unit/core/unit_moonlive_compiler.cpp index 09d464801..054b1bd11 100644 --- a/test/unit/core/unit_moonlive_compiler.cpp +++ b/test/unit/core/unit_moonlive_compiler.cpp @@ -195,19 +195,19 @@ TEST_CASE("a byte parameter wraps at 255, the way a byte local does") { } TEST_CASE("a function declaring more parameters than the block holds is refused") { - // The arena block carries kMaxScriptArgs values, and the CALL site clamps to it. The prologue did not: it emitted one LoadCtrl32 per DECLARED parameter, so a fifth read past the end of the arena allocation and a later one wrote there. Script text is user-supplied on a network-reachable device, so the declaration has to be refused where it is written. - moonlive::MoonLive five; - CHECK_FALSE(five.compile("class T {\n" - " void helper(int a, int b, int c, int d, int e) { setRGB(a, 1, 0, 0); }\n" - " void tick() { helper(0, 0, 0, 0, 0); }\n" + // The prologue copies one arena word per declared parameter, so a ninth would read and write past the arena: user-supplied text, refused where it is written. + moonlive::MoonLive nine; + CHECK_FALSE(nine.compile("class T {\n" + " void helper(int a, int b, int c, int d, int e, int f, int g, int h, int i) { setRGB(a, 1, 0, 0); }\n" + " void tick() { helper(0, 0, 0, 0, 0, 0, 0, 0, 0); }\n" "}\n", kTable, kSys)); // Exactly the maximum still compiles: the bound is the block's size, not one below it. - moonlive::MoonLive four; - CHECK(four.compile("class T {\n" - " void helper(int a, int b, int c, int d) { setRGB(a, b, c, d); }\n" - " void tick() { helper(0, 1, 2, 3); }\n" - "}\n", kTable, kSys)); + moonlive::MoonLive eight; + CHECK(eight.compile("class T {\n" + " void helper(int a, int b, int c, int d, int e, int f, int g, int h) { setRGB(a, b, c, d + e + f + g + h); }\n" + " void tick() { helper(0, 1, 2, 3, 0, 0, 0, 0); }\n" + "}\n", kTable, kSys)); } TEST_CASE("a parameter name is checked the way a local is") { @@ -1863,3 +1863,59 @@ TEST_CASE("compileSource: a register refusal returns a literal, and spillDetail CHECK(std::strcmp(r.error, moonlive::kSpillRefused) == 0); CHECK(moonlive::spillDetail().guard != 0); // and the guard that fired is recorded } + +// Each refusal names what to write instead, since a struct expands into forms the script never shows and a bare "expected ';'" would point at nothing the user wrote. +TEST_CASE("a struct or a condition used where it does not fit is refused with what to write instead") { + uint8_t out[2048]; + struct Case { const char* src; const char* err; const char* what; }; + const Case refused[] = { + {mmScript("int RGB = 3;"), "that name is a type", "a variable named after a type"}, + {mmScript("setRGB(0, 1 + getRGB(0), 0, 0);"), "a struct is read one field at a time: write name.field", "a struct answer used as one number"}, + {"class T {\n int step() { return 1; }\n void tick() { }\n}\n", "that name shadows a built-in function", "a function named after a builtin, which would never be called"}, + {mmScript("RGB c = RGB(1, 2, 3); setRGB(0, c.w, 0, 0);"), "this struct has no field of that name", "a field the type does not have"}, + {mmScript("RGB c = RGB(1, 2, 3, 4);"), "too many fields for this struct", "a constructor with a fourth field"}, + {mmScript("RGB c = RGB(1, 2, 300);"), "this number is out of range for that field", "a byte field given 300"}, + {mmScript("RGB c = RGB(1, 2, 3); setRGB(0, 1, c);"), "too many arguments: this struct fills more positions than remain", "a struct past the last position"}, + {"class T {\n void f(Coord3D a, Coord3D b, Coord3D c) { }\n void tick() { }\n}\n", + "parameters fill at most 8 words, a struct taking one per field", "three Coord3D parameters, nine words"}, + {"class T {\n RGB c = RGB(1, 2, 3);\n void tick() { }\n}\n", + "a struct member starts at zero: set its fields in a function", "a member struct with an initializer"}, + {mmScript("int both = 0; both = 1 && 2;"), "&&, || and ! join the conditions of an if, not values", "&& as a value"}, + {mmScript("setRGB(0, !1, 0, 0);"), "&&, || and ! join the conditions of an if, not values", "! as a value"}, + // C reads `!a < b` as `(!a) < b`, so a comparison after a negated value is refused rather than read the other way. + {mmScript("int a = 0; if (!a < 1) { setRGB(0, 1, 0, 0); }"), "! negates what follows it: write !(a < b) to negate a comparison", "! before a comparison"}, + {"class T {\n RGB c;\n void tick() { c[1] = 5; }\n}\n", "a struct is read one field at a time: write name.field", "a struct member written by index"}, + {mmScript("fixed f = 1.5; int y = 0; y = max(f, max(10, 20));"), "mixes whole and fixed: write toFixed(x) or toInt(x)", "a nested same-type call hiding a mixed argument"}, + }; + for (const Case& c : refused) { + INFO(c.what); + auto r = moonlive::compileSource(c.src, kTable, kSys, out, sizeof(out)); + CHECK_FALSE(r.ok); + CHECK(std::string(r.error) == std::string(c.err)); + } +} + +// A struct use expands to one operation per field, so struct copies are the densest IR: about 1.6 ops per token against a reservation of one. +TEST_CASE("a script dense in struct copies compiles, its IR growing past the reservation") { + std::string src = "class T {\n Coord3D a;\n Coord3D b;\n RGB c;\n RGB d;\n void tick() {\n"; + for (int i = 0; i < 20; i++) src += " a = b; b = a; c = d; d = c; setRGB(0, c);\n"; // about the largest shipped script's length + src += " }\n}\n"; + std::vector out(moonlive::codeCapFor(moonlive::countTokens(src.c_str()))); + auto r = moonlive::compileSource(src.c_str(), kTable, kSys, out.data(), out.size()); + INFO(std::string(r.error)); +#if MM_MOONLIVE_HAS_HOST_JIT + CHECK(r.ok); +#else + CHECK((r.ok || std::string(r.error) == moonlive::kCodegenFailed)); +#endif +} + +// A lexer error does not move past its character, so a lookahead skipping a call's parentheses stops at it; before, one typo here looped the compile forever. +TEST_CASE("an unexpected character inside a call's arguments is refused, not looped on") { + uint8_t out[2048]; + for (const char* body : {"setRGB(0, max(1, 2 @ 3), 0, 0);", "fill(abs(1 $), 0, 0);", "setRGB(0, getRGB(1 # 2).r, 0, 0);"}) { + CAPTURE(body); + auto r = moonlive::compileSource(mmScript(body), kTable, kSys, out, sizeof(out)); + CHECK_FALSE(r.ok); + } +} diff --git a/test/unit/light/MoonLiveEffectScene.h b/test/unit/light/MoonLiveEffectScene.h new file mode 100644 index 000000000..15f03dac1 --- /dev/null +++ b/test/unit/light/MoonLiveEffectScene.h @@ -0,0 +1,45 @@ +#pragma once + +/// A scripted effect on a real layer, the way the module tree wires one, for tests that run a script and read the lights it wrote. + +#include "MoonLiveScriptFixture.h" +#include "light/moonlive/MoonLiveEffect.h" +#include "core/moonlive/moonlive_emit.h" // MM_MOONLIVE_HAS_HOST_JIT +#include "light/layouts/GridLayout.h" +#include "light/layouts/Layouts.h" +#include "light/layers/Layer.h" + +namespace mm { + +/// A wired effect on a real layer, the way the module tree builds one. +struct ScriptScene { + Layouts layouts; ///< the layout container the layer reads + GridLayout grid; ///< a w x h x 1 grid + Layer layer; ///< the layer the effect draws on + MoonLiveEffect effect; ///< the scripted effect under test + /// Wire a grid of `w` by `h` lights, the effect on its layer. + ScriptScene(int w = 16, int h = 16) { + grid.width = w; grid.height = h; grid.depth = 1; + layouts.addChild(&grid); + layer.setLayouts(&layouts); + layer.setChannelsPerLight(3); + layer.addChild(&effect); + effect.defineControls(); + } + /// Write `script` to a file, point the effect at it, and compile it the way a prepare does. + void run(const char* script) { + effect.setScript(mmWriteScript(script)); + layouts.applyState(); + layer.applyState(); + } + /// How many lights carry any color, which is the coverage a stroke width decides. + int lit() { + int n = 0; + const auto* px = layer.buffer().data(); + for (nrOfLightsType i = 0; i < layer.buffer().count(); i++) + if (px[i * 3] || px[i * 3 + 1] || px[i * 3 + 2]) n++; + return n; + } +}; + +} // namespace mm diff --git a/test/unit/light/moonlive_ir_golden.inc b/test/unit/light/moonlive_ir_golden.inc new file mode 100644 index 000000000..aba5c9f24 --- /dev/null +++ b/test/unit/light/moonlive_ir_golden.inc @@ -0,0 +1,64 @@ + {"layouts/backlight.mll", 0x45cad59768d0942aull}, + {"layouts/diagonal.mll", 0x8d903da3498de21aull}, + {"layouts/grid.mll", 0x19ba4f6c02fbec77ull}, + {"layouts/lattice.mll", 0xbf932d2667cc1e2bull}, + {"layouts/lightcrafter16.mll", 0x88b21fa19926d0baull}, + {"layouts/reversed-row.mll", 0x785717443ed98909ull}, + {"layouts/ring.mll", 0xff6ea124f4984df7ull}, + {"layouts/rose.mll", 0xaaa163ddfd7f7582ull}, + {"layouts/se16.mll", 0xb3d61a600c981e77ull}, + {"layouts/sixteen-rings.mll", 0x2be3763b705091acull}, + {"layouts/two-rows.mll", 0x87d0dc459e897bf6ull}, + {"effects/aurora.mle", 0x996cd3f22e4dc197ull}, + {"effects/ballpit.mle", 0x7b3066f771f2b0b6ull}, + {"effects/balls.mle", 0xbb5164cdbc9212c2ull}, + {"effects/breathe.mle", 0xc264f7b683c94cceull}, + {"effects/chase.mle", 0xc338158f7b9f20d6ull}, + {"effects/comet-trail.mle", 0xf32055d6b321a624ull}, + {"effects/crosshair.mle", 0x1ecd2d5ba648e680ull}, + {"effects/dot.mle", 0x1e5cd842f29bd16cull}, + {"effects/ember.mle", 0xfdbae5798bfc5166ull}, + {"effects/fluid.mle", 0xae90b7a03c897817ull}, + {"effects/fountain.mle", 0x82dfa1dfc065e39full}, + {"effects/fractal.mle", 0x030ffee2720ea938ull}, + {"effects/glow.mle", 0xded139de73aad7efull}, + {"effects/gradient.mle", 0x066b6e80d9fb84cbull}, + {"effects/lines.mle", 0x61a519a99d69b4c6ull}, + {"effects/metal.mle", 0x74653c8d7f09b748ull}, + {"effects/mh-aim.mle", 0xa3b9705009ee6730ull}, + {"effects/mh-ambient.mle", 0xf5170dcf140e2204ull}, + {"effects/mh-sweep.mle", 0x9b60fb840efbcda6ull}, + {"effects/mh-troy.mle", 0xf14ce04a1093b98eull}, + {"effects/mh-wowi.mle", 0x2d5344bf6451948eull}, + {"effects/nebula.mle", 0x6248d83646fad1f3ull}, + {"effects/noise.mle", 0x59d7fc331c86f6b0ull}, + {"effects/octopus.mle", 0x547abd2e1fc5e611ull}, + {"effects/plasma.mle", 0xcf6d241a11a3f985ull}, + {"effects/pulse.mle", 0xcfa5b1d85be98998ull}, + {"effects/rain.mle", 0xaa0219b0eb71cf4eull}, + {"effects/random-pixel.mle", 0xf7fb12703b9df7e6ull}, + {"effects/ripples.mle", 0x9b9f9be80e1e0c61ull}, + {"effects/sparkle.mle", 0x6ea5f2562ef2cd4full}, + {"effects/spectrum.mle", 0xba428f684216ecbeull}, + {"effects/stadbeest-eye-orb.mle", 0xfd590905cb2a4e0eull}, + {"effects/stadbeest-frog-eye.mle", 0x9e1e030206e1a49full}, + {"effects/stadbeest-mouth.mle", 0x52e0471dda27a918ull}, + {"effects/stadbeest-orb.mle", 0xc3cc0b0853ee7d42ull}, + {"effects/stadbeest-sauron.mle", 0xd439b3dd89fa6dccull}, + {"effects/stadbeest-thrusters.mle", 0x234a1e01f7efa8b7ull}, + {"effects/stadbeest-walk.mle", 0x07253f910ca2b12aull}, + {"effects/trails.mle", 0x7df2e1c3f6a5eeb6ull}, + {"modifiers/mirror.mlm", 0x72dcbfe7e1304111ull}, + {"modifiers/shift.mlm", 0xd8b0753f0d8df134ull}, + {"modifiers/transpose.mlm", 0x4c7cb92f54e87418ull}, + {"palettes/beat-flash.mlp", 0x2bddbddeecf452d6ull}, + {"palettes/drift.mlp", 0x431d765e3fcc25abull}, + {"palettes/fire.mlp", 0x1acffb6bb6322e64ull}, + {"palettes/spectrum.mlp", 0x44d8cd5c43b0500eull}, + {"palettes/stadbeest-fire.mlp", 0x0e3db80cc7ecd8a5ull}, + {"palettes/temperature.mlp", 0x98bf482549960c5full}, + {"services/button.mls", 0x377a24ae1d49b9d1ull}, + {"services/chase.mls", 0x91984b68b9521c79ull}, + {"services/power.mls", 0x0656e0565392a427ull}, + {"services/stadbeest-autopilot.mls", 0x8bf1685b0a5ada84ull}, + {"services/sweep.mls", 0xf22d60f9fdbec0ecull}, diff --git a/test/unit/light/unit_MoonLiveDrawing.cpp b/test/unit/light/unit_MoonLiveDrawing.cpp index ba7c1c023..74aca684d 100644 --- a/test/unit/light/unit_MoonLiveDrawing.cpp +++ b/test/unit/light/unit_MoonLiveDrawing.cpp @@ -4,51 +4,17 @@ /// The scripted drawing vocabulary: `circle` and `line` as a script calls them, pinning the seam where unsigned ABI words become the light domain's signed lengths. #include "doctest.h" -#include "MoonLiveScriptFixture.h" +#include "MoonLiveEffectScene.h" #include "../core/moonlive_script_wrap.h" -#include "light/moonlive/MoonLiveEffect.h" -#include "core/moonlive/moonlive_emit.h" // MM_MOONLIVE_HAS_HOST_JIT -#include "light/layouts/GridLayout.h" -#include "light/layouts/Layouts.h" -#include "light/layers/Layer.h" using namespace mm; #if MM_MOONLIVE_HAS_HOST_JIT namespace { -/// A wired effect on a real layer, the way the module tree builds one. -struct Scene { - Layouts layouts; - GridLayout grid; - Layer layer; - MoonLiveEffect effect; - Scene(int w = 16, int h = 16) { - grid.width = w; grid.height = h; grid.depth = 1; - layouts.addChild(&grid); - layer.setLayouts(&layouts); - layer.setChannelsPerLight(3); - layer.addChild(&effect); - effect.defineControls(); - } - void run(const char* script) { - effect.setScript(mmWriteScript(script)); - layouts.applyState(); - layer.applyState(); - } - /// How many lights carry any color, which is the coverage a stroke width decides. - int lit() { - int n = 0; - const auto* px = layer.buffer().data(); - for (nrOfLightsType i = 0; i < layer.buffer().count(); i++) - if (px[i * 3] || px[i * 3 + 1] || px[i * 3 + 2]) n++; - return n; - } -}; - /// One frame of a tick() body on a 16x16 grid. int litAfter(const char* body) { - Scene s; + ScriptScene s; std::string src = std::string("class T { void tick() { fill(0, 0, 0); ") + body + " } }"; s.run(src.c_str()); s.layer.tick(); @@ -57,7 +23,7 @@ int litAfter(const char* body) { } // namespace TEST_CASE("a scripted circle draws its rim and leaves the middle dark") { - Scene s; + ScriptScene s; s.run("class T { void tick() { fill(0, 0, 0); circle(8, 8, 5, 1, 255, 0, 0); } }"); s.layer.tick(); const auto* px = s.layer.buffer().data(); @@ -99,7 +65,7 @@ TEST_CASE("a scripted circle outside the grid lights nothing") { } TEST_CASE("a scripted line reaches both of its endpoints") { - Scene s; + ScriptScene s; s.run("class T { void tick() { fill(0, 0, 0); line(2, 2, 13, 13, 0, 255, 0); } }"); s.layer.tick(); const auto* px = s.layer.buffer().data(); @@ -110,7 +76,7 @@ TEST_CASE("a scripted line reaches both of its endpoints") { // The read-modify-write calls act on what is already in the light, which is what lets a script placed after another effect mask, glow or tint it. TEST_CASE("a script dims, adds to and mixes a light that is already lit") { - Scene s; + ScriptScene s; s.run("class T { void tick() { fill(200, 100, 50); mulRGB(0, 128); addRGB(1, 100, 200, 0); blendRGB(2, 0, 0, 250, 255); mulRGB(9999, 0); } }"); s.layer.tick(); const auto* px = s.layer.buffer().data(); @@ -121,9 +87,9 @@ TEST_CASE("a script dims, adds to and mixes a light that is already lit") { } // Reading back what is there is what lets a script build on another effect rather than paint over it. -TEST_CASE("a script reads back the channels of a light") { - Scene s; - s.run("class T { void tick() { fill(0, 0, 0); setRGB(0, 10, 20, 30); setRGB(1, getB(0), getG(0), getR(0)); setRGB(2, getR(9999), 0, 0); } }"); +TEST_CASE("a script reads back the color of a light") { + ScriptScene s; + s.run("class T { void tick() { fill(0, 0, 0); setRGB(0, 10, 20, 30); RGB c = getRGB(0); setRGB(1, c.b, c.g, c.r); setRGB(2, getRGB(9999).r, 0, 0); } }"); s.layer.tick(); const auto* px = s.layer.buffer().data(); CHECK(px[3] == 30); CHECK(px[4] == 20); CHECK(px[5] == 10); // light 0's channels, read back and swapped diff --git a/test/unit/light/unit_MoonLiveParticles.cpp b/test/unit/light/unit_MoonLiveParticles.cpp index fff7f0c09..be9d498fa 100644 --- a/test/unit/light/unit_MoonLiveParticles.cpp +++ b/test/unit/light/unit_MoonLiveParticles.cpp @@ -1,18 +1,11 @@ /// @module MoonLiveParticles /// @also MoonLiveEffect, MoonLive, particles -/// A scripted particle pool: the particles live in ScratchBuffers the binding owns, OUTSIDE the script's 64-byte arena, because a Pool is eight parallel arrays and an arena-resident pool would hold about five particles. The script never names a particle; it calls whole-pool operations and the binding supplies the buffers and the frame clock. -/// -/// These pin the seam rather than the physics (unit_Particles.cpp owns the kernel's own contract): that a script can size its pool, that sizing happens ONLY on the cold path, that a failure degrades visibly, and that one script's particles cannot reach another's. +/// A scripted particle pool: its buffers live in ScratchBuffers the binding owns, outside the 64-byte arena, and a script calls whole-pool operations. #include "doctest.h" -#include "MoonLiveScriptFixture.h" +#include "MoonLiveEffectScene.h" #include "../core/moonlive_script_wrap.h" -#include "light/moonlive/MoonLiveEffect.h" -#include "core/moonlive/moonlive_emit.h" // MM_MOONLIVE_HAS_HOST_JIT -#include "light/layouts/GridLayout.h" -#include "light/layouts/Layouts.h" -#include "light/layers/Layer.h" #include "platform/platform.h" // setTestNowMs: particle physics runs on elapsed time #include #include @@ -25,25 +18,9 @@ using namespace mm; #if MM_MOONLIVE_HAS_HOST_JIT namespace { -/// A wired effect on a real layer, the way the module tree builds one. -struct Scene { - Layouts layouts; - GridLayout grid; - Layer layer; - MoonLiveEffect effect; - Scene(int w = 8, int h = 8) { - grid.width = w; grid.height = h; grid.depth = 1; - layouts.addChild(&grid); - layer.setLayouts(&layouts); - layer.setChannelsPerLight(3); - layer.addChild(&effect); - effect.defineControls(); - } - void run(const char* script) { - effect.setScript(mmWriteScript(script)); - layouts.applyState(); - layer.applyState(); - } +// The shared fixture on the 8x8 grid these particle tests are sized for. +struct Scene : ScriptScene { + Scene(int w = 8, int h = 8) : ScriptScene(w, h) {} }; } // namespace @@ -92,6 +69,32 @@ TEST_CASE("disabling a scripted effect frees its particles") { CHECK(s.effect.dynamicBytes() == 0); } +// Trying effects one after another switches one module's script; on a classic ESP32 the carried buffers left too little heap to compile a large one. +TEST_CASE("switching to a script that asks for no pool or trail frees the last script's") { + const char* bare = "class T { void tick() { fill(1, 2, 3); } }"; + const char* bareWithControls = "class T { byte n = 0; void defineControls() { addControl(\"n\", n, 0, 9); } void tick() { fill(1, 2, 3); } }"; + const char* sizing = "class T { void defineControls() { pool(64); trail(1); } void tick() { } }"; + for (const char* next : {bare, bareWithControls}) { // without a defineControls() and with one + CAPTURE(next); + Scene s; + s.run(next); + const size_t alone = s.effect.dynamicBytes(); + s.run(sizing); + REQUIRE(s.effect.dynamicBytes() > alone + 1000); + s.run(next); + CHECK(s.effect.dynamicBytes() < alone + 256); // the same program again, with nothing carried over + } +} + +// Freed as the compile starts rather than after a successful one, so a large script's compile has that memory, and a script that fails to compile holds nothing. +TEST_CASE("a script that fails to compile holds none of the last script's pool and trail") { + Scene s; + s.run("class T { void defineControls() { pool(64); trail(1); } void tick() { } }"); + REQUIRE(s.effect.dynamicBytes() > 1000); + s.run("class T { void tick() { nosuchcall(); } }"); + CHECK(s.effect.dynamicBytes() < 256); +} + // A script reaching the particle vocabulary from a layout or a modifier finds no pool installed, so the calls do nothing rather than writing through another module's buffers. TEST_CASE("a particle call from a script with no pool does nothing") { Scene s; @@ -176,7 +179,7 @@ TEST_CASE("two scripted effects each get their own particles") { CHECK(litB == 0); // the other stayed empty } -// The shipped example, driven through the real binding: a fountain reaches a steady state where sparks are emitted, fly, and die at the same rate, rather than filling the pool once and stopping. +// Through the real binding, a fountain reaches a steady state where sparks are emitted, fly and die at one rate, rather than filling the pool once. TEST_CASE("the fountain example keeps emitting once its pool has cycled") { // The SHIPPED file, staged into the test filesystem: this drives the real example rather than a copy that could drift from it. const std::filesystem::path src = std::filesystem::path(__FILE__).parent_path() @@ -201,7 +204,7 @@ TEST_CASE("the fountain example keeps emitting once its pool has cycled") { CHECK(litLate > 5); // still drawing 120 frames in } -// A spray has to look like a spray. angleEmit hashes (index, seed) into an angle and a speed, so a seed that does not move makes every frame throw the IDENTICAL set of sparks: they stack into a few fixed streams and the plume pulses instead of flowing. Emitting the same arguments twice must therefore produce different trajectories. +// angleEmit hashes (index, seed) into an angle and a speed, so a seed that stood still threw identical sparks every frame, stacked into a few fixed streams. TEST_CASE("emitting twice from the same point does not repeat the same trajectories") { Scene s(24, 24); s.run("class T {" @@ -224,7 +227,7 @@ TEST_CASE("emitting twice from the same point does not repeat the same trajector CHECK(firstCols != secondCols); // a frozen seed would make these identical } -// collide() makes particles notice each other. Dropped down the SAME column, balls without it fall straight through one another and stay in that one column; with it they shove sideways and spread. That difference is the whole feature, and it is what turns a shower into a pit. +// Dropped down one column, balls without collide() fall through one another and stay in it; with it they shove sideways and spread, which turns a shower into a pit. TEST_CASE("colliding balls spread sideways instead of falling through each other") { auto pileHeight = [](const char* collideCall) { Scene s(16, 16); diff --git a/test/unit/light/unit_MoonLiveScripts_ir_golden.cpp b/test/unit/light/unit_MoonLiveScripts_ir_golden.cpp new file mode 100644 index 000000000..64f2cc4de --- /dev/null +++ b/test/unit/light/unit_MoonLiveScripts_ir_golden.cpp @@ -0,0 +1,123 @@ +/// @module MoonLive + +/// Every shipped script compiles to the same front-end IR, which all five backends lower from, as it did before a compiler change. + +#include "doctest.h" +#include "core/moonlive/MoonLiveCompiler.h" +#include "core/moonlive/MoonLiveBuiltins_service.h" +#include "core/moonlive/MoonLiveScriptFile.h" +#include "light/moonlive/MoonLiveBuiltins_light.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace mm; +using namespace mm::moonlive; + +namespace { +// One line per shipped script; a script rewritten on purpose moves its hash, and the commit updating the line says why. +const std::map kGolden = { +#include "moonlive_ir_golden.inc" +}; + +const BuiltinTable* g_table = nullptr; +const char* g_strings = nullptr; +uint64_t g_hash = 0; + +// By name rather than table position, so a builtin added elsewhere in the table moves no script's hash. +uint64_t builtinName(HostCallFn fn) { + if (!fn) return 0; + for (uint8_t i = 0; i < g_table->count; i++) { + if (g_table->items[i].fn != fn) continue; + uint64_t h = 1469598103934665603ull; + for (const char* c = g_table->items[i].name; *c; c++) { h ^= uint8_t(*c); h *= 1099511628211ull; } + return h; + } + return 0xFFFF; // a target outside the table, hashed as one value rather than as an address +} + +uint64_t pointerOffset(const void* p) { + if (!p) return 0; + const char* c = static_cast(p); + if (g_strings && c >= g_strings && c < g_strings + CompileResult::kStringPool) return uint64_t(c - g_strings) + 1; + return 0xFFFF; +} + +// A lowering that hashes the IR instead of emitting code, and reports one byte so the compile counts as done. +size_t hashIr(IrProgram& ir, uint8_t*, size_t, const RegBudget*) { + uint64_t h = 1469598103934665603ull; + auto mix = [&h](uint64_t v) { + for (int i = 0; i < 8; i++) { h ^= (v >> (i * 8)) & 0xFF; h *= 1099511628211ull; } + }; + mix(ir.count); + mix(ir.localSlots); + for (uint16_t i = 0; i < ir.count; i++) { + const IrInst& in = ir.ops[i]; + mix(uint64_t(in.op)); + mix(in.dst); mix(in.a); mix(in.b); mix(in.c); mix(in.d); + mix(uint32_t(in.imm)); + mix(builtinName(in.callFn)); + mix(pointerOffset(in.ptr)); + mix(uint64_t(in.inlineOp)); + } + g_hash = h; + return 1; +} + +std::filesystem::path scriptRoot() { + return std::filesystem::path(__FILE__).parent_path().parent_path().parent_path().parent_path() / "moonlive"; +} + +std::string read(const std::filesystem::path& p) { + std::ifstream f(p); + std::ostringstream ss; + ss << f.rdbuf(); + return ss.str(); +} +} // namespace + +TEST_CASE("every shipped script compiles to the IR it had before the compiler changed") { + // MM_PRINT_IR_GOLDEN=1 prints the table instead of checking it, which is how it is captured. + const bool print = std::getenv("MM_PRINT_IR_GOLDEN") != nullptr; + int checked = 0; + for (const char* sub : {"layouts", "effects", "modifiers", "palettes", "services"}) { + const std::filesystem::path dir = scriptRoot() / sub; + if (!std::filesystem::exists(dir)) continue; + std::map files; // sorted, so the printed table is stable + for (const auto& e : std::filesystem::directory_iterator(dir)) + if (e.is_regular_file() && isScriptExt(e.path().extension().string().c_str())) files[e.path().filename().string()] = e.path(); + for (const auto& [name, path] : files) { + const std::string label = std::string(sub) + "/" + name; + const std::string src = read(path); + const bool isService = std::string(sub) == "services"; + const SysVarTable sys = isService ? serviceSysVars() + : std::string(sub) == "layouts" ? layoutSysVars() + : std::string(sub) == "modifiers" ? modifierSysVars() + : effectSysVars(); + const BuiltinTable table = isService ? serviceBuiltins() : lightBuiltins(); + static char strings[CompileResult::kStringPool]; + g_table = &table; + g_strings = strings; + g_hash = 0; + uint8_t code[4]; + const CompileResult r = compileSource(src.c_str(), table, sys, code, sizeof(code), nullptr, hashIr, strings, sizeof(strings)); + CAPTURE(label); + REQUIRE(r.ok); + if (print) { + std::printf(" {\"%s\", 0x%016llxull},\n", label.c_str(), static_cast(g_hash)); + } else { + const auto it = kGolden.find(label); + CHECK_MESSAGE(it != kGolden.end(), "no golden IR hash for " << label << ": capture it with MM_PRINT_IR_GOLDEN=1"); + if (it != kGolden.end()) CHECK(it->second == g_hash); + } + checked++; + } + } + CHECK(checked > 0); +} diff --git a/test/unit/light/unit_MoonLiveStructs.cpp b/test/unit/light/unit_MoonLiveStructs.cpp new file mode 100644 index 000000000..227934c27 --- /dev/null +++ b/test/unit/light/unit_MoonLiveStructs.cpp @@ -0,0 +1,211 @@ +/// @module MoonLive +/// @also MoonLiveEffect + +/// `RGB` and `Coord3D`, conditions joined by `&&`, `||` and `!`, and the arithmetic builtins, each run on this host's backend and read back from the lights. + +#include "doctest.h" +#include "MoonLiveEffectScene.h" +#include "light/moonlive/MoonLiveBuiltins_light.h" + +#include + +using namespace mm; + +#if MM_MOONLIVE_HAS_HOST_JIT + +namespace { +/// One frame of a script on a 16x16 grid, and the lights it wrote; a script that does not compile fails here with its error. +const uint8_t* frame(ScriptScene& s, const char* script) { + std::vector code(moonlive::codeCapFor(moonlive::countTokens(script))); + const auto r = moonlive::compileSource(script, moonlive::lightBuiltins(), moonlive::effectSysVars(), code.data(), code.size()); + INFO(std::string(r.error), " at ", r.errorCol); + REQUIRE(r.ok); + s.run(script); + s.layer.tick(); + return s.layer.buffer().data(); +} +} // namespace + +TEST_CASE("an RGB value is made, changed and drawn as one color") { + ScriptScene s; + const uint8_t* px = frame(s, + "class T {\n" + " RGB keep;\n" + " void tick() {\n" + " fill(0, 0, 0);\n" + " RGB c = RGB(10, 20, 30);\n" + " setRGB(0, c);\n" // a struct argument fills three positions + " c.g = 99;\n" + " setRGB(1, c.r, c.g, c.b);\n" + " keep = c;\n" // whole-struct assignment into a member + " keep.b = keep.b + 1;\n" + " setRGB(2, keep);\n" + " RGB z;\n" // a local declared without a value starts at zero + " setRGB(3, z);\n" + " }\n" + "}\n"); + CHECK(px[0] == 10); CHECK(px[1] == 20); CHECK(px[2] == 30); + CHECK(px[3] == 10); CHECK(px[4] == 99); CHECK(px[5] == 30); + CHECK(px[6] == 10); CHECK(px[7] == 99); CHECK(px[8] == 31); + CHECK(px[9] == 0); CHECK(px[10] == 0); CHECK(px[11] == 0); +} + +TEST_CASE("an RGB member keeps its value from one frame to the next") { + ScriptScene s; + s.run("class T {\n" + " RGB acc;\n" + " void tick() { acc.r = acc.r + 10; acc.b = 5; setRGB(0, acc); }\n" + "}\n"); + for (int i = 0; i < 3; i++) s.layer.tick(); + const uint8_t* px = s.layer.buffer().data(); + CHECK(px[0] == 30); // starts at zero, plus ten per frame + CHECK(px[1] == 0); + CHECK(px[2] == 5); +} + +TEST_CASE("a Coord3D holds negative and large whole numbers") { + ScriptScene s; + const uint8_t* px = frame(s, + "class T {\n" + " Coord3D far;\n" + " void tick() {\n" + " fill(0, 0, 0);\n" + " Coord3D p = Coord3D(-5, 7, -300);\n" + " far = p;\n" + " far.x = far.x * 100;\n" + " setRGB(0, abs(p.x), p.y, 0);\n" + " if (p.z < -200) { setRGB(1, 1, 2, 3); }\n" + " if (far.x == -500) { setRGB(2, 4, 5, 6); }\n" + " }\n" + "}\n"); + CHECK(px[0] == 5); CHECK(px[1] == 7); + CHECK(px[3] == 1); CHECK(px[4] == 2); CHECK(px[5] == 3); + CHECK(px[6] == 4); CHECK(px[7] == 5); CHECK(px[8] == 6); +} + +TEST_CASE("a light's color and a palette color read back as one RGB") { + ScriptScene s; + const uint8_t* px = frame(s, + "class T {\n" + " void tick() {\n" + " fill(0, 0, 0);\n" + " setRGB(0, 10, 20, 30);\n" + " setRGB(1, getRGB(0));\n" // a struct answer passed straight on + " setRGB(2, getRGB(0).g, 0, 0);\n" // one field of it + " RGB c = paletteRGB(100, 200);\n" + " setRGB(3, c);\n" + " RGB past = getRGB(9999);\n" // past the end reads as black + " setRGB(5, past);\n" + " }\n" + "}\n"); + CHECK(px[3] == 10); CHECK(px[4] == 20); CHECK(px[5] == 30); + CHECK(px[6] == 20); + const RGB want = colorFromPalette(*Palettes::active(), 100, 200); // what a compiled effect reads + CHECK(px[9] == want.r); CHECK(px[10] == want.g); CHECK(px[11] == want.b); + CHECK(px[15] == 0); CHECK(px[16] == 0); CHECK(px[17] == 0); +} + +TEST_CASE("a script function takes and answers a struct") { + ScriptScene s; + const uint8_t* px = frame(s, + "class T {\n" + " RGB warm(int k) { return RGB(k, k / 2, 10); }\n" + " void paint(int i, RGB c, int boost) { setRGB(i, c.r, c.g + boost, c.b); }\n" + " Coord3D move(Coord3D p, Coord3D d) { return Coord3D(p.x + d.x, p.y + d.y, p.z + d.z); }\n" + " void tick() {\n" + " fill(0, 0, 0);\n" + " paint(0, warm(200), 1);\n" + " Coord3D q = move(Coord3D(1, 2, 3), Coord3D(10, 20, 30));\n" + " setRGB(1, q.x, q.y, q.z);\n" + " setRGB(2, warm(60).g, 0, 0);\n" + " }\n" + "}\n"); + CHECK(px[0] == 200); CHECK(px[1] == 101); CHECK(px[2] == 10); + CHECK(px[3] == 11); CHECK(px[4] == 22); CHECK(px[5] == 33); + CHECK(px[6] == 30); +} + +TEST_CASE("conditions join with && and ||, negate with !, and stop at the first answer") { + ScriptScene s; + const uint8_t* px = frame(s, + "class T {\n" + " int hits = 0;\n" + " int touch() { hits = hits + 1; return 1; }\n" + " void tick() {\n" + " fill(0, 0, 0);\n" + " hits = 0;\n" + " if (1 < 2 && 2 < 3) { setRGB(0, 1, 0, 0); }\n" + " if (1 < 2 && 3 < 2) { setRGB(1, 1, 0, 0); }\n" + " if (3 < 2 || 1 < 2) { setRGB(2, 1, 0, 0); }\n" + " if (!(3 < 2)) { setRGB(3, 1, 0, 0); }\n" + " if (0 || 1 && 1) { setRGB(4, 1, 0, 0); }\n" // && binds tighter than || + " if ((0 || 1) && 0) { setRGB(5, 1, 0, 0); }\n" // parentheses group a condition + " if (0 && touch()) { }\n" // never asked + " if (1 || touch()) { }\n" // never asked + " if (touch() && touch()) { setRGB(6, hits, 0, 0); }\n" + " if (7) { setRGB(7, 1, 0, 0); }\n" // a bare value is true when it is not zero + " if (!0 && !touch() || 1) { setRGB(8, hits, 0, 0); }\n" + " }\n" + "}\n"); + CHECK(px[0] == 1); + CHECK(px[3] == 0); + CHECK(px[6] == 1); + CHECK(px[9] == 1); + CHECK(px[12] == 1); + CHECK(px[15] == 0); + CHECK(px[18] == 2); // only the last condition's two calls ran + CHECK(px[21] == 1); + CHECK(px[24] == 3); +} + +TEST_CASE("abs, min, max and clamp work on whole and fixed values") { + ScriptScene s; + const uint8_t* px = frame(s, + "class T {\n" + " void tick() {\n" + " fill(0, 0, 0);\n" + " setRGB(0, abs(-7), min(3, -2) + 10, max(-1, 4));\n" + " setRGB(1, clamp(300, 0, 255), clamp(-5, 0, 255), clamp(40, 0, 255));\n" + " fixed f = -1.5;\n" + " setRGB(2, toInt(abs(f) * toFixed(4)), toInt(max(f, toFixed(2))), toInt(min(f, toFixed(2)) + toFixed(3)));\n" + " }\n" + "}\n"); + CHECK(px[0] == 7); CHECK(px[1] == 8); CHECK(px[2] == 4); + CHECK(px[3] == 255); CHECK(px[4] == 0); CHECK(px[5] == 40); + CHECK(px[6] == 6); CHECK(px[7] == 2); CHECK(px[8] == 1); // the fixed results stay fixed: 6.0, 2.0 and 1.5 +} + +// A same-type call as another one's argument: the outer call's literal must still adopt the fixed type of the inner result, or max(1, 0.5) reads its 1 as 1/65536. +TEST_CASE("a same-type call nested in another keeps the outer call's literal in the right type") { + ScriptScene s; + const uint8_t* px = frame(s, + "class T {\n" + " void tick() {\n" + " fill(0, 0, 0);\n" + " fixed f = 0.5;\n" + " fixed g = 0.25;\n" + " setRGB(0, toInt(max(1, min(f, g)) * toFixed(10)), toInt(clamp(min(f, g), 0, 2) * toFixed(100)), 0);\n" + " }\n" + "}\n"); + CHECK(px[0] == 10); // max(1.0, 0.25) * 10 + CHECK(px[1] == 25); // clamp(0.25, 0.0, 2.0) * 100 +} + +// hpwit's parser freed a variable declared before a loop at the loop, so a read after it saw whatever reused the register. +TEST_CASE("a variable declared before a loop, changed in it, reads right after it") { + ScriptScene s; + const uint8_t* px = frame(s, + "class T {\n" + " void tick() {\n" + " fill(0, 0, 0);\n" + " int k = 5;\n" + " RGB c = RGB(1, 2, 3);\n" + " for (int i = 0; i < 4; i = i + 1) { setRGB(i, k, c.g, 0); k = k + 1; c.g = c.g + 1; }\n" + " setRGB(10, k, c.g, c.b);\n" + " }\n" + "}\n"); + for (int i = 0; i < 4; i++) { CHECK(px[i * 3] == 5 + i); CHECK(px[i * 3 + 1] == 2 + i); } + CHECK(px[30] == 9); CHECK(px[31] == 6); CHECK(px[32] == 3); +} + +#endif // MM_MOONLIVE_HAS_HOST_JIT From 7f47be4a9db9415374e8ee5308f2b51af0ab527f Mon Sep 17 00:00:00 2001 From: ewowi Date: Mon, 12 Oct 2026 16:20:34 +0200 Subject: [PATCH 07/12] Include where the builtins register from a braced list GCC builds stopped at the arithmetic builtins' registration, which ranges over a braced list without the header that declares it; clang includes it transitively, so the desktop build passed and the sanitizer jobs failed to compile. Core - MoonLiveBuiltins_common.h includes ; verified with build_desktop --gcc --tests. Co-Authored-By: Claude Opus 5.5 --- src/core/moonlive/MoonLiveBuiltins_common.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/moonlive/MoonLiveBuiltins_common.h b/src/core/moonlive/MoonLiveBuiltins_common.h index 4571c730a..64861966e 100644 --- a/src/core/moonlive/MoonLiveBuiltins_common.h +++ b/src/core/moonlive/MoonLiveBuiltins_common.h @@ -10,6 +10,7 @@ #include #include #include +#include // the same-type builtins register from a braced list /// @defgroup moonlive_builtins_common MoonLive neutral builtins /// @{ From e13ae42e55db1fb4c7b4eff3ed16bae9658cd5c7 Mon Sep 17 00:00:00 2001 From: ewowi Date: Tue, 13 Oct 2026 01:06:33 +0200 Subject: [PATCH 08/12] Let a File Manager upload run without the install overlay's stall watch The merge of #136 broke every File Manager upload: uploadWithProgress now calls the install overlay's watchStalls, and the File Manager's drop upload passes a ui with progress only, so the call threw before the request opened. The stall watch is now called only where the caller has one. Checks: test_host --js, 251 of 251 pass, including the three SD-upload tests CI failed on. UI - uploadWithProgress calls ui.watchStalls only when the ui provides it Co-Authored-By: Claude Opus 5.5 --- src/ui/app.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ui/app.js b/src/ui/app.js index 6abfefded..369db4d2a 100644 --- a/src/ui/app.js +++ b/src/ui/app.js @@ -1641,7 +1641,8 @@ function uploadWithProgress(route, file, ui, signal) { if (signal.aborted) { reject(new Error("canceled")); return; } signal.addEventListener("abort", () => xhr.abort(), { once: true }); } - ui.watchStalls(); + // The install overlay watches for stalls; the File Manager's drop upload shows only progress. + if (ui.watchStalls) ui.watchStalls(); xhr.open("POST", route); xhr.setRequestHeader("Content-Type", "application/octet-stream"); xhr.upload.addEventListener("progress", (e) => { From abcd08bac3f5498c868997df5498a10f207077e6 Mon Sep 17 00:00:00 2001 From: ewowi Date: Tue, 13 Oct 2026 12:59:19 +0200 Subject: [PATCH 09/12] Send each client only the state it views, streamed in fragments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The web interface subscribes to the card it shows, and the device streams that view as small WebSocket fragments, so a page load, a board switch and the per-second updates cost a fraction of the whole tree and no board needs it in one memory block. The Teensy 4.1 reads and writes its built-in microSD socket, and every ESP32-S3 build reads its flash over four lines (QIO), 16% less tick time on the testbench S3. KPI: 256lights | Desktop:2301KB | tick:7/8/8/2/8/7/272/7/7/11us(FPS:142857/125000/125000/500000/125000/142857/3676/142857/142857/90909) | ESP32:2245KB | src:323(83578) | test:238(54897) | functions-over:354 Commit: src +1122/-606 lines | test +720/-71 lines | code findings 1629 -> 1641 (blocking call on the render path +1, complex function +12, deeply nested +2, duplicated block -10, large file +1, large stack frame -3, long function +2, long parameter list +1, module type in the UI +6) | duplicated lines 3732 -> 3672 (-60) Core - /ws state by subscription: a client sends {"subscribe":[...]} naming modules by name or type and gets those in full and an outline of the rest; a connect sends nothing until it subscribes, a subscribe resyncs that client alone after patching the others, and {"resync":n} asks for the view again, answered with the id. - The full state streams per client as RFC 6455 fragments from one resumable tree cursor, which also writes /api/state and one module's JSON; patches are collected once and sliced per client; Ping and Close are answered between frames. - Progress controls declare a unit (ProgressUnit: bytes or KiB); the File Manager gauge is `flash` and the SD gauge `card`. - Teensy 4.1 SD socket: SD and SdFat in their own flash-resident library, /sd routed through the filesystem seam, presence probed only between transfers, the destination removed before a rename only on the card. - ESP32-S3: QIO flash and 80 MHz PSRAM on every S3 variant, a new esp32s3-n32r16 variant (octal flash, 32 MB table), and System shows flashMode and the PSRAM clock in expert mode. UI - The page subscribes to the root it renders plus the types it reads beside it, keeps the previous cards until an outline's controls arrive, and fetches the whole tree only for diagnostics, the target picker and a restore. - A render deferred mid-edit runs when focus leaves or a selection clears; gauges size their label as B, KB, MB or GB. - Catalog: ESP32-S3 N32R16V Dev and Waveshare ESP32-S3-RGB-Matrix; MoonHub75's microphone set to local audio. Scripts/MoonDeck - check_docgen counts tracked files only, so a generated header no longer moves its ratchet; build_esp32 registers esp32s3-n32r16 and drops its em-dashes. Tests - unit_HttpServerModule_subscribe: fragments at any size equal the whole, outline vs subscribed, per-client patches, loopback streaming, Ping, Close, resync id, a tree changed mid-stream, held patches, a client that never subscribes; ui-subscribe and ui-progress-label JS tests, including every type the page names being subscribed or explained. Docs/CI - Plan "State by subscription, streamed" with measured traffic and the questions answered; MIGRATING entry for /ws; ui.md, system.md, services.md, firmware-variants.md; backlog: Teensy uploads at 20 KB/s, a dropped uplink message, LEDs flashing white then a reset, Drivers' list sizes; CLAUDE.md: the gates run once per commit. - check_code: the committed report predates this branch's 396a1110 and 48fe8d4a, which added 14 findings; against HEAD this change is -2 (complex function -2, duplicated block -6), with module type in the UI +5 for the five types the page always subscribes to and large file +1 for platform_teensy.cpp at 1053 lines. - check_esp32_built: stale on all eight variants, core changed after the last ESP32 builds; the rebuild is the product owner's call. Reviews - ๐Ÿ‘พ LittleFS lost atomic replace through remove-before-rename โ†’ done, card only. - ๐Ÿ‘พ subscription names truncated at 24 characters โ†’ done, stored as FNV-1a hashes. - ๐Ÿ‘พ per-client state about 3 KB, pong buffers โ†’ done, reads wait for a frame boundary instead. - ๐Ÿ‘พ third baseline walk to count modules โ†’ done, counted in the first. - ๐Ÿ‘พ deferred render missed a cleared selection โ†’ done, selectionchange. - ๐Ÿ‘พ fragment allocation failure silent โ†’ done, logged once per run. - ๐Ÿ‘พ four behaviors untested โ†’ done, four tests; the tree-change test found a cursor bug (a module vanishing mid-object closed its parent's brackets), fixed. - ๐Ÿ‘พ wrapped paragraphs in new comments โ†’ done, HttpServerModule.h rewritten to one-line paragraphs and cut by 20 lines. - ๐Ÿ‘พ misplaced and stale comments in app.js โ†’ done. - ๐Ÿ‘พ teensy_sd split teensy_libs' definition โ†’ done. - ๐Ÿ‘พ 1-byte Close echoed, unfinished control frames accepted โ†’ done, per RFC 6455 ยง5.5. - ๐Ÿ‘พ a mid-stream subscribe switched the view halfway โ†’ done, each stream copies its subscription. - ๐Ÿ‘พ cursor depth cap unexplained โ†’ done, reason on kMaxDepth. - ๐Ÿ‘พ test port shared with the SD tests โ†’ done, 18092. - ๐Ÿ‘พ three gauge renames missing from MIGRATING โ†’ done. - ๐Ÿ‘พ backoff, escaped em-dashes, past tense โ†’ done (six escapes, not three). - ๐Ÿ‘พ refetchState doc and timeout entry โ†’ done. - ๐Ÿ‘พ Teensy socket pins in two places โ†’ accepted, cross-referenced: the platform must refuse other wiring. - ๐Ÿ‘พ complexity of the new streaming code โ†’ done, Extract Function on six functions. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 2 + .../esp32-s3-n32r16-waveshare-rgb-matrix.png | Bin 0 -> 392049 bytes docs/contributing/documentation-standards.md | 4 +- docs/moonmodules/core/services.md | 4 +- docs/moonmodules/core/system.md | 4 +- docs/moonmodules/core/ui.md | 11 +- docs/reference/MIGRATING.md | 10 + .../reference/hardware/esp32-s31-coreboard.md | 5 +- docs/reference/hardware/firmware-variants.md | 1 + docs/reference/metrics/code.md | 134 ++-- docs/reference/metrics/docgen.md | 355 +++++---- docs/reference/metrics/prose.md | 12 +- docs/reference/metrics/repo-health.json | 156 +++- docs/reference/metrics/repo-health.md | 140 ++-- docs/work/future/backlog-core.md | 24 + docs/work/future/backlog-light.md | 7 + ...61013 - State by subscription, streamed.md | 202 +++++ esp32/partitions/ota_32mb.csv | 13 + esp32/sdkconfig.defaults.esp32s3-n16r8 | 5 +- esp32/sdkconfig.defaults.esp32s3-n32r16 | 20 + esp32/sdkconfig.defaults.esp32s3-n8r8 | 6 +- esp32/sdkconfig.defaults.esp32s3-zero | 5 + moondeck/build/build_esp32.py | 107 +-- moondeck/build/setup_teensy.py | 5 +- moondeck/check/check_docgen.py | 108 ++- moondeck/docs/screenshot_modules.py | 2 + mooninstaller/deviceModels.json | 114 ++- mooninstaller/firmwares.json | 30 +- src/core/module/Control.cpp | 6 +- src/core/module/Control.h | 12 +- src/core/services/SdCardService.cpp | 4 +- src/core/services/SdCardService.h | 19 +- src/core/system/FileManagerModule.cpp | 32 +- src/core/system/HttpServerModule.cpp | 729 ++++++++++++------ src/core/system/HttpServerModule.h | 344 ++++++--- src/core/system/SystemModule.h | 9 +- src/core/util/JsonSink.h | 8 +- src/module_types.cpp | 21 +- src/platform/desktop/platform_config.h | 3 + src/platform/desktop/platform_desktop.cpp | 34 +- src/platform/esp32/platform_config.h | 2 + src/platform/esp32/platform_esp32.cpp | 33 +- src/platform/esp32/platform_esp32_fs.cpp | 22 +- src/platform/platform.h | 10 +- src/platform/teensy/platform_config.h | 8 +- src/platform/teensy/platform_teensy.cpp | 214 +++-- src/ui/app.js | 218 +++--- teensy/CMakeLists.txt | 9 +- test/CMakeLists.txt | 1 + test/js/ui-progress-label.test.mjs | 22 + test/js/ui-render-guard.test.mjs | 20 +- test/js/ui-selected-root.test.mjs | 34 +- test/js/ui-subscribe.test.mjs | 120 +++ test/python/test_check_docgen.py | 30 +- test/unit/core/http_test_client.h | 21 + .../unit/core/unit_HttpServerModule_apply.cpp | 24 +- .../unit_HttpServerModule_connections.cpp | 20 +- .../core/unit_HttpServerModule_subscribe.cpp | 427 ++++++++++ test/unit/core/unit_MoonModule.cpp | 14 +- test/unit/core/unit_PartitionTables.cpp | 1 + test/unit/core/unit_SdCardService.cpp | 57 +- 61 files changed, 2858 insertions(+), 1156 deletions(-) create mode 100644 docs/assets/deviceModels/esp32-s3-n32r16-waveshare-rgb-matrix.png create mode 100644 docs/work/present/Plan-20261013 - State by subscription, streamed.md create mode 100644 esp32/partitions/ota_32mb.csv create mode 100644 esp32/sdkconfig.defaults.esp32s3-n32r16 create mode 100644 test/js/ui-progress-label.test.mjs create mode 100644 test/js/ui-subscribe.test.mjs create mode 100644 test/unit/core/http_test_client.h create mode 100644 test/unit/core/unit_HttpServerModule_subscribe.cpp diff --git a/CLAUDE.md b/CLAUDE.md index a06360c39..54b696baf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -179,6 +179,8 @@ On "run pre-commit": run the checks whose trigger the diff matches, report one l **A failure is something to report, not to fix and re-run.** A second run needs the words again, as much after a failure or a fix as at any other time. +**The gates run once per commit.** A run costs minutes, so code that changes during or after one does not restart it: ask the product owner whether to continue with the results so far or restart. A cosmetic change can ride on the run it followed, since the next commit's gates check it again. + ```mermaid flowchart LR pc{"๐Ÿง‘ run pre-commit
PO says the words,
once per request
"} diff --git a/docs/assets/deviceModels/esp32-s3-n32r16-waveshare-rgb-matrix.png b/docs/assets/deviceModels/esp32-s3-n32r16-waveshare-rgb-matrix.png new file mode 100644 index 0000000000000000000000000000000000000000..831d9931e1d1cecad8fd5fbb920afdfc08ed77a2 GIT binary patch literal 392049 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

*wo8sD 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�-#_ExlmJed^bDgyr5eKWLLC#!TH3 zy98TGiP;de^<|kEEK$r-`-Bx>gD|0%e>IuB1>^=?^g1#g4E#+i)QdNsAJ+!6ciml8 z%2z!m4UOIJuTmjBm_vnn)`Nxz1t^bL{m%6+@+>K)nA>M_wC6Ij^^Uyy^uzsW@D1;U z7Q-fk+$n}OZE+xlH*@Px2FyZh=re~TmX4Y07_$}@&SOZ%cZ*0Cjc-ZBu)jDOx_H0X zDj$=)q%9|5V^Emdd(R zJ)_`LKJ2@gHqx?#g;R-<3*@ff;ZxpyhY8627<6YpRmo5!XS&vW7%0DlJoN__$icmt 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

imDefemyAL?ehycK>LaR{DDr%_4~dk^kfxs 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%0DuO? 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�C!O~~g`wfY9`sx*YvnEK(30*l_=Ll@tmfvyB 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 literal 0 HcmV?d00001 diff --git a/docs/contributing/documentation-standards.md b/docs/contributing/documentation-standards.md index 545c6828d..104208e97 100644 --- a/docs/contributing/documentation-standards.md +++ b/docs/contributing/documentation-standards.md @@ -256,7 +256,7 @@ A card is read across a row; a member comment is read beside the thing it descri | One line of a file or class lead | 400 characters | | A whole `///` run, lead or class comment | 2500 characters | | A sentence in a comment | 1 line, never wrapped | -| A paragraph in a comment | 1 line while it fits the line cap | +| A paragraph in a comment | 1 line, cut to fit the line cap | | Every public member | carries one | **A lead's line is wider, and a run is capped as a block.** A lead is the summary that opens a header or a class. One of its lines carries what the whole thing is for: what it is, which hardware it runs on, where the shared body lives. Held to the member cap the driver headers each lost about a third of that, and what went was content. The run budget is what stops a lead sprawling instead. It also closes the cheapest way to satisfy a line cap: splitting one long line into two shorter ones leaves the text identical and every per-line rule passing. @@ -269,7 +269,7 @@ The first five cut and the last adds, deliberately: the result is a short line o **The appendix budget counts a SECTION, not the whole appendix.** A single total punishes a file for having several distinct topics. The cheapest way to satisfy one is to delete a section rather than tighten the prose. Ten lines per `## ` section asks each one to be disciplined and lets a file carry as many as it genuinely has. That is what an implementation file needs: the platform backends each hold a handful of separately diagnosed findings, and one shared budget could only force them out of the tree. A fenced block does not count, the same exemption the no-wrap rule makes. A protocol listing is as long as the thing it describes, and counting its lines would ask an author to delete wire format to fit a prose budget. -**No hard wrap in a comment, for the reason markdown gives.** Let the editor soft-wrap, so a one-word edit is a one-word diff rather than a reflowed paragraph. The one-line budget already forbids this on a header's member or code comment. So the rule bites where a block is allowed to be multi-line. That is the class comment, the `@moreinfo` appendix, and any multi-line run in an implementation file. A list item, a heading, a table row and a fenced block are structure rather than a wrapped sentence, and each is left alone. This one holds everywhere, because its reason is the diff rather than the page. A split sentence reflows every line it spans when a word changes, which costs a reviewer the same in either file. A paragraph goes the same way: its sentences share one line while that line fits the cap. Markdown joins them into one paragraph anyway, so the split only spends a line. Past the cap, each sentence keeps a line of its own. +**No hard wrap in a comment, for the reason markdown gives.** Let the editor soft-wrap, so a one-word edit is a one-word diff rather than a reflowed paragraph. The one-line budget already forbids this on a header's member or code comment. So the rule bites where a block is allowed to be multi-line. That is the class comment, the `@moreinfo` appendix, and any multi-line run in an implementation file. A list item, a heading, a table row and a fenced block are structure rather than a wrapped sentence, and each is left alone. This one holds everywhere, because its reason is the diff rather than the page. A split sentence reflows every line it spans when a word changes, which costs a reviewer the same in either file. A paragraph goes the same way: the sentences of one topic are one paragraph, and a paragraph is one line. Markdown joins them anyway, and a reader takes separate lines as separate topics. Past the cap the paragraph is cut until it fits, never split into sentence lines or line-sized chunks. **Use `//` sparingly.** A comment restating what the code does is a naming failure, and the fix is a better name rather than a better sentence. What survives is the WHY a reader cannot recover from the code. diff --git a/docs/moonmodules/core/services.md b/docs/moonmodules/core/services.md index 302112507..162f52c04 100644 --- a/docs/moonmodules/core/services.md +++ b/docs/moonmodules/core/services.md @@ -175,7 +175,7 @@ A board's microSD slot, where one is wired: its wiring, a usage gauge and a form - `bus`: under `Custom`, the SD bus or the SD spec's SPI mode. - `clkPin` โ€ฆ `pwrPin`: the pins under `Custom`; โˆ’1 unused, `d1`-`d3` unset is 1-bit. - `format`: a button, press twice; erases and reformats the card as FAT32 in place. -- `card (KiB)`: read-only usage bar, used / total KiB, so a card past 4 GiB still reports correctly. +- `card`: read-only usage bar, counted in KiB so a card past 4 GiB still reports correctly. Detail: [technical](moxygen/SdCardService.md) @@ -344,7 +344,7 @@ The status line reports setup state ("set pin to receive" / "ready"), the learn ## SD Card, details -The presets: `P4 SDMMC` is the slot every P4 board with one wires, and a P4 opens on it. `Classic SDMMC (1-bit)` is the classic's own SD host pins, which the Olimex ESP32-Gateway wires, and `Classic SPI` the QuinLED Dig-Octa's. A 4 MB image carries no SD at all, so the 4 MB Olimex has none. SPI mode maps `clkPin` to SCK, `cmdPin` to MOSI, `d0Pin` to MISO and `d3Pin` to CS. Over SPI, reads hold but a write over about 50 KB can stop the card until a power cycle, so a card mounted that way says so on its status. +The presets: `P4 SDMMC` is the slot every P4 board with one wires, and a P4 opens on it. `Classic SDMMC (1-bit)` is the classic's own SD host pins, which the Olimex ESP32-Gateway wires, and `Classic SPI` the QuinLED Dig-Octa's. A 4 MB image carries no SD at all, so the 4 MB Olimex has none. `Teensy 4.1 SD socket` is the Teensy 4.1's built-in slot on its own SDIO host, and a Teensy opens on it; it mounts FAT and exFAT, and its format runs in the call, pausing the render for its few seconds, since the Teensy has no task to hand it to. SPI mode maps `clkPin` to SCK, `cmdPin` to MOSI, `d0Pin` to MISO and `d3Pin` to CS. Over SPI, reads hold but a write over about 50 KB can stop the card until a power cycle, so a card mounted that way says so on its status. Mounts live, in `prepare()` rather than `setup()`: editing a pin unmounts and remounts on the new wiring, no reboot needed, the same contract the [I2C bus](system.md#i2c-bus)'s `sda`/`scl` have. A pulled card is noticed within a second. A missing or unreadable card shows its own status instead of retrying, since a mount attempt on an empty slot stalls the render thread: insert the card, then switch the module off and on, edit a pin, or reboot. diff --git a/docs/moonmodules/core/system.md b/docs/moonmodules/core/system.md index 8a1c7f781..b7b697eff 100644 --- a/docs/moonmodules/core/system.md +++ b/docs/moonmodules/core/system.md @@ -17,7 +17,7 @@ The device's identity and vitals: name (behind mDNS `.local`, the SoftAP S - `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 rate: `uptime`, `fps`, `tickTimeUs`. -- read-only memory: `heap`, `psram`, `maxBlock`, `flash`. +- read-only memory: `heap`, `psram`, `maxBlock`, `flash`; `psramType` and `flashMode` in expert mode. - read-only identity: `mac`, `chip`, `cpu`, `sdk`, `bootReason`. - `factory reset`, pressed twice: back to freshly installed, scripts kept. @@ -240,7 +240,7 @@ Browse and manage the device filesystem: a folder tree with an inline text edito 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). +- `flash`: 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) diff --git a/docs/moonmodules/core/ui.md b/docs/moonmodules/core/ui.md index 322a44b60..49733c4cb 100644 --- a/docs/moonmodules/core/ui.md +++ b/docs/moonmodules/core/ui.md @@ -48,7 +48,7 @@ Fixed top, 44 px, left-to-right: 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 - exponential backoff (500 ms โ†’ 5 s) on close, and a page reload covers the rare half-dead socket. + exponential backoff from 200 ms to a 5 s ceiling on close, and a page reload covers the rare half-dead socket. 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']`. @@ -67,7 +67,7 @@ A left column listing the root modules, one entry per top-level MoonModule. - **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 whole 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). Filtering per root server-side buys little: the full tree arrives once on connect and each second after that carries only what changed, built through a streaming sink, so tree size is not a buffer-limit concern. +The page subscribes to what it shows: the selected root, plus by type the modules it reads outside that root (`ALWAYS_SUBSCRIBED`: System for the status bar and mode, FirmwareUpdate for the update badge, MoonCloud for the relay, PreviewDriver for the frame rate, MidiService for the MIDI bridge). The device answers with those modules in full and an outline of the rest, names and shape without controls, which is all the nav reads. Opening another root sends a new subscription, and its cards appear when the answer arrives; until then the previous cards stay. The control-surface target picker offers every module's controls, so assign mode fetches the whole tree from `/api/state`. ## Module card @@ -120,7 +120,7 @@ Auto-rendered by `controls[].type`, adding a MoonModule with these types needs n | `display` (read-only) | static text | WS push updates in place | n/a | | `display-int` (read-only int + unit) | formatted text (`-58 dBm`) | unit suffix set device-side at `addReadOnlyInt` time, carried in the descriptor's `aux` slot | n/a | | `time` (read-only seconds) | formatted text (`1d 4h 27m 13s`) | WS push updates | n/a | -| `progress` | bar + numeric `X / max` | WS push updates | n/a | +| `progress` | bar + `used / total`, sized B, KB, MB or GB | WS push updates | n/a | | `ipv4` | text input (dotted-quad) | server validates (`parseDottedQuad`), 400 on malformed; stored as 4 octets device-side | n/a | | `button` | clickable button | sends value = 1 on click | none | @@ -177,7 +177,7 @@ The light-domain renderer (WebGL point cloud, the `0x03`/`0x02` frame format, or ## State updates, the no-rebuild contract -When a WS state message arrives, the full tree on connect or a patch after it, for each module in the payload: +When a WS state message arrives, a full state answering a subscription or a patch after it, for each module in the payload: - If `controls.deviceName` exists, update the status-bar device name and `document.title`. - For each `controls[key]`: look up the matching ``/``/``/`