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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .vale.ini
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,14 @@ MoonLight.Weasel = error
MoonLight.NegatedHeading = error
MoonLight.SelfReference = error

# The rules page must QUOTE the things it bans (an em-dash, "analyse", "this page", "really"), so
# the four rules whose examples it carries stay off here. The rest are strict.
# The rules page must QUOTE the things it bans (an em-dash, "analyse", "this page", "really", "today"), so
# the five rules whose examples it carries stay off here. The rest are strict.
[docs/contributing/documentation-standards.md]
BasedOnStyles = MoonLight
MoonLight.EmDash = NO
MoonLight.Spelling = NO
MoonLight.SelfReference = NO
MoonLight.Weasel = NO
MoonLight.Temporal = NO
MoonLight.SentenceLength = error
MoonLight.NegatedHeading = error
14 changes: 14 additions & 0 deletions .vale/styles/MoonLight/Temporal.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# documentation-standards.md: "Present tense only." These words date a sentence: they compare the system with a moment, which is history, so the sentence describes what exists without them.
extends: existence
message: "'%s' dates the sentence: describe what exists."
level: error
ignorecase: true
tokens:
- today
- currently
- at the moment
- at present
- for now
- no longer
- anymore
- not yet
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ flowchart LR
pc --> diff
always["<b>always</b><br/>💀 check_specs · 💀 check_prose <i>· records, writes to the tree</i>"]
md["<b>.md</b><br/>build_docs --strict 🐢<br/>💀 check_docgen · 🛸 test_host --python <i>(catalog pages)</i><br/>check_taglines <i>(front pages only)</i>"]
code["<b>src/ or test/</b><br/>💀 check_code 🐢 · build_desktop 🐢<br/>🛸 test_desktop 🐢 · run_scenario 🐢<br/>💀 check_docgen <i>(the headers it covers)</i><br/>💀 check_esp32_built <i>(not src/platform/desktop)</i><br/>💀 build_desktop --no-jit 🐢 <i>(MoonLive only)</i><br/>💀 collect_kpi 🐢 <i>· records, writes to the tree</i>"]
code["<b>src/ or test/</b><br/>💀 check_code · build_desktop 🐢<br/>🛸 test_desktop 🐢 · run_scenario 🐢<br/>💀 check_docgen <i>(the headers it covers)</i><br/>💀 check_esp32_built <i>(not src/platform/desktop)</i><br/>💀 build_desktop --no-jit 🐢 <i>(MoonLive only)</i><br/>💀 collect_kpi 🐢 <i>· records, writes to the tree</i>"]
web["<b>src/ui or mooninstaller/</b><br/>🛸 test_host --js · 💀 check_devices"]
py["<b>moondeck/ or moonlive/</b><br/>🛸 test_host --python · 💀 check_firmwares"]
board["<b>the provisioning path,<br/>with a board attached</b><br/>🛸 improv_smoke_test<br/><i>recommended, say so when skipped</i>"]
Expand Down
6 changes: 6 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ else()
# and that count may only fall, while each site stays a judgment (a driver may have to wait
# on hardware), so failing the build on them would block every other gate.
add_compile_options(-Wfunction-effects -Wno-error=function-effects)
# Each file's findings are kept beside its object, so the code report reads the whole tree after an incremental build instead of a clean one.
find_package(Python3 COMPONENTS Interpreter QUIET)
if(Python3_Interpreter_FOUND AND NOT CMAKE_CXX_COMPILER_LAUNCHER)
set(CMAKE_CXX_COMPILER_LAUNCHER "${Python3_EXECUTABLE};${CMAKE_SOURCE_DIR}/moondeck/build/diag_launcher.py")
endif()
endif()
endif()

Expand Down Expand Up @@ -126,6 +131,7 @@ find_program(UV_EXECUTABLE NAMES uv REQUIRED
add_library(mm_core STATIC
src/core/module/Control.cpp
src/core/util/JsonUtil.cpp
src/core/util/parse.cpp
src/core/util/ScratchBuffer.cpp
src/core/system/HttpServerModule.cpp
src/core/system/FilesystemModule.cpp
Expand Down
File renamed without changes
Binary file added docs/assets/deviceModels/esp32-p4-guition-m3.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/contributing/documentation-standards.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ Two scales below a page. **A module** has exactly one reference page written and
- **No sentence whose job is tone.** Every sentence carries a fact the reader needs. Three shapes to cut: a second person used for effect rather than instruction, a flourish before any information arrives, and a rhetorical question the page then answers. Vale checks spelling, sentence length and weasel words, so this one is the writer's judgement and the reviewer's check.
- **Follow the [principles](../CLAUDE.md#principles).** Three bear on documentation directly:
- **Minimalism**: every fact has one home; history lives in git.
- **Present tense only.** "No X anymore" narrates a removal, which is history. Describe the path that exists today.
- **Present tense only.** "No X anymore" narrates a removal, which is history. Describe the path that exists. Vale flags the words that date a sentence ("today", "currently", "no longer", "not yet").
- **Positive form only.** "Not", "never", "neither", "without", "un-" and "non-" are the alarm bells: a negation says everything a thing is not, which is no shape at all. A real constraint stays ("the DMA cannot read PSRAM at shift clock"); a bare absence goes.
- **Industry standards**: the textbook name for a thing, so a reader recognizes it without being taught our vocabulary. A bespoke choice carries its one-line reason where it appears.
- **Continuous improvement**: a doc describing what the code no longer does is a defect. Fix it in the change that opened the file, not in a sweep.
Expand Down
2 changes: 1 addition & 1 deletion docs/explanation/architecture/mooncore.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ A **service** is a MoonModule (role `ModuleRole::Service`) that bridges to the o

The defining line is the **data relationship, not the connector**: *does the module consume the light output buffer?* If yes it's a **driver** (ArtNet, DMX, SPI-LED all consume the buffer, differing only in transport; a DMX sender uses a UART/RS-485 transport but is a driver because it sends the rendered buffer). If no, it's a **service**.

Services are **user-add/deletable children of the `Services` container**, the core-domain twin of the light pipeline's `Effects`/`Drivers`: a top-level container holding user-added children of one role. The firmware is identical whether or not the hardware is wired. The user adds the module when they solder a gyro on and removes it later, reusing the generic child add, replace, delete and persistence machinery; `Services` declares `acceptsChildRoles("service")`. Fixed device infrastructure lives under **System** instead, wired by code rather than user-added: identity, network, and the inspection tools Tasks and I2cScan. That is the System/Services split. Direction is per-module, not a role: a service may read (gyro), write (relay), or both, so one `Service` role spans the category. Each is a header-only or `.h`+`.cpp` core module under `src/core/`, reaches hardware only through a domain-neutral platform primitive (`platform::i2c*`, `platform::audioMic*`, …), and gets a spec in `docs/moonmodules/core/services.md` (enforced by `check_specs.py`). Most poll in `tick20ms` or `tick1s`. The exception is a service whose data an effect consumes *every frame*: [AudioService](../../moonmodules/core/moxygen/AudioService.md) reads + analyses its I²S microphone in `tick()` because the audio effects react per render tick, and its per-tick cost (one FFT) is part of the render budget. Automatic bus-probe detection is out of scope; the manual path is the foundation.
Services are **user-add/deletable children of the `Services` container**, the core-domain twin of the light pipeline's `Effects`/`Drivers`: a top-level container holding user-added children of one role. The firmware is identical whether or not the hardware is wired. The user adds the module when they solder a gyro on and removes it later, reusing the generic child add, replace, delete and persistence machinery; `Services` declares `acceptsChildRoles("service")`. Fixed device infrastructure lives under **System** instead, wired by code rather than user-added: identity, network, the inspection tool Tasks, and the board's I2C bus. That is the System/Services split. Direction is per-module, not a role: a service may read (gyro), write (relay), or both, so one `Service` role spans the category. Each is a header-only or `.h`+`.cpp` core module under `src/core/`, reaches hardware only through a domain-neutral platform primitive (`platform::i2c*`, `platform::audioMic*`, …), and gets a spec in `docs/moonmodules/core/services.md` (enforced by `check_specs.py`). Most poll in `tick20ms` or `tick1s`. The exception is a service whose data an effect consumes *every frame*: [AudioService](../../moonmodules/core/moxygen/AudioService.md) reads and analyzes its I²S microphone in `tick()`, because the audio effects react per render tick. Its per-tick cost, one FFT, is part of the render budget. Automatic bus-probe detection is out of scope; the manual path is the foundation.

**An effect reads a service's data** through the shared-struct pull pattern from [Data exchange](moonmodule.md#data-exchange-between-modules), with no new mechanism. The service owns a small POD struct overwritten in place each poll or tick, and the consuming effect holds a `const` pointer to it. The first concrete case is audio: AudioService produces an `AudioFrame` (level + 16-band spectrum + peak) that [AudioSpectrumEffect](../../moonmodules/light/effects.md) and the other audio effects consume. It reaches the frame through a static `AudioService::latestFrame()` rather than a boot-time setter. That variation exists because an audio effect can be added through the UI *after* boot and must still find the one live mic (a setter only wired the boot instance). The active mic registers itself in `setup()` and clears the pointer in `release()`, so add/remove in any order returns either the live frame or a static silent one, never null. A service that only *displays* its readings (the gyro today) skips the consumer side entirely.

Expand Down
4 changes: 3 additions & 1 deletion docs/moonmodules/core/services.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ A user-added Service: the audio source the audio-reactive effects consume. `mode
- `mode`: Local audio, Receive network or Simulate, each showing only its own controls below.
- `micMode`: (Local, I²S targets) `I2S` for a three-wire part, `PDM` for a two-wire one.
- `sckPin` / `wsPin` / `sdPin`: (Local, I²S targets) the bus GPIOs, unset until entered.
- `mclkPin`: (Local, I²S targets) the master clock a line-in ADC may need; unset for a plain mic.
- `mclkPin`: (Local, I²S targets) the master clock an ADC or codec needs; unset for a plain mic.
- `codec`: (Local, I²S targets) `ES8311` for a mic behind that codec, else `none`.
- `codecAddr`: (with a codec) its address on the [I2C bus](system.md#i2c-bus), in hex (`0x18`).
- `device`: (Local, desktop) the OS capture input, `default` following the system setting.
- `sampleRate`: (Local) mic/ADC sample rate.
- `levels`: (Local) who sets the display window: `manual`'s two sliders, or `automatic`, a learner.
Expand Down
18 changes: 8 additions & 10 deletions docs/moonmodules/core/system.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

### System

The device's identity and vitals: name (behind mDNS `<name>.local`, the SoftAP SSID, the DHCP hostname), uptime, heap and the tick it renders at. Its fixed inspection children (Tasks, I2C scan) hang beneath it.
The device's identity and vitals: name (behind mDNS `<name>.local`, the SoftAP SSID, the DHCP hostname), uptime, heap and the tick it renders at. Its fixed children (Tasks, the I2C bus, Pins) hang beneath it.

<img src="../../assets/core/SystemModule.png" width="300" alt="System module controls">

Expand Down Expand Up @@ -240,21 +240,19 @@

Detail: [technical](moxygen/FileManagerModule.md)

<a id="i2c-scan"></a>
<a id="i2c-bus"></a>

### I2C scan
### I2C bus

A fixed System module (wired-by-code, always present) that probes the I²C bus on a button press and reports the addresses found, a hardware bring-up tool. The bus pins default to unused (−1), so a board without an I²C device claims no GPIO for it; a board with a bus sets its pins via the catalog, or you type them for an ad-hoc scan. Passive until the scan button is pressed.
A fixed System module (wired-by-code, always present) that owns the board's I²C bus: it opens the bus on its pins, every device on the bus attaches to it, and a scan probes it. A device names only its own address, such as AudioService's `codecAddr`; the wires are stated here, once per board. The pins default to unused (−1), so a board without an I²C device claims no GPIO.

Check warning on line 247 in docs/moonmodules/core/system.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [MoonLight.SentenceLength] Sentence runs past 30 words; it is probably two thoughts joined by a comma. Raw Output: {"message":"[MoonLight.SentenceLength] Sentence runs past 30 words; it is probably two thoughts joined by a comma.","location":{"path":"docs/moonmodules/core/system.md","range":{"start":{"line":247,"column":1},"end":{"line":247,"column":2}}},"severity":"INFO","code":{"value":"MoonLight.SentenceLength"}}

<img src="../../assets/core/I2cScanModule.png" width="300" alt="I2C scan module controls">
<img src="../../assets/core/I2cBusModule.png" width="300" alt="I2C bus module controls">

- `sda` / `scl`: the bus GPIOs, defaulting to −1 for unused.

A board with a fixed bus injects its own through the catalog, or you type the pins for an ad-hoc scan. The classic Arduino-ESP32 pair is 21/22.
- `sda` / `scl`: the bus GPIOs, −1 for unused; a board's catalog entry sets them.
- `scan`: a button; press to probe the bus now.
- read-only: `result` (addresses found).
- read-only: `result`, each address found, named by the module driving it: `0x18 Audio (ES8311)`.

Detail: [technical](moxygen/I2cScanModule.md)
Detail: [technical](moxygen/I2cBusModule.md)

<a id="tasks"></a>

Expand Down
11 changes: 6 additions & 5 deletions docs/moonmodules/light/drivers.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@
- `pins`: data GPIO list: `18,17,16`, or ranges like `20-23`, mixed freely. Empty idles until set.
- `ledsPerPin`: lights per strand. Blank splits evenly, one number to all, a list per strand.
- `peripheral`: the DMA peripheral, filtered to what the chip supports. It divides the card.
- `doubleBuffer`, `pinExpander`, `latchPin`, bus pins: shown per peripheral.
- 🎚️ `doubleBuffer`: encode the next frame while this one clocks out, where supported.
- `pinExpander`, `latchPin`, bus pins: shown per peripheral.
- read-only `frameTime`: what one frame costs on the wire, the headroom the pipeline has left.
- 🔧 `loopbackTest`: a TX to RX self-test, verdict in the status field.
- 🔧 `loopbackStrand` / `loopbackIntrusive`: which strand it tests, and whether it may disturb output.
Expand All @@ -90,11 +91,11 @@
- `clk lat oe`: shift clock, latch, output enable.
- `peripheral`: `LCD_CAM` or `Parlio`, where the chip has both and the frame fits.
- `scanRate`: 1/8, 1/16 or 1/32, **read off the panel**, not calculated.
- `bitDepth` (2 to 4): color precision against refresh and memory.
- `bitDepth` (2 to 6, default 6): color levels against refresh and memory.
- `clockEdge`: `rising` or `falling`. Every pixel one column over means the other one.
- `refresh`: the **measured** rate. The number to report if a panel flickers.

**New, and not yet run on a wall we own.** Built from the panel's documented behavior with its encoder pinned by [host tests](../../reference/tests/unit-tests.md#hub75driver), which is not hardware verification. Reports welcome: `refresh` plus your geometry is what makes one useful.
**Run on one panel.** A [MoonHub75](https://moonmodules.org/projects/hardware/#moonhub75-pcb) drives a 64x64 1/32-scan panel at 6-bit and 1,508 Hz. Larger walls and other panel families wait on reports: `refresh` plus your geometry makes one useful.

Detail: [technical](moxygen/Hub75Driver.md) · encoder: [Hub75Slots](moxygen/Hub75Slots.md)

Expand Down Expand Up @@ -415,6 +416,6 @@

**How a HUB75 panel works, in two facts.** It is *scanned*, not addressed: two rows light at once, the upper half-panel through R1/G1/B1 and the lower through R2/G2/B2, selected by a row address on A/B/C (and D/E on finer panels). The controller walks every scan row in turn and persistence of vision does the rest, so a 64-row panel has 32 scan rows and row `r` drives panel rows `r` and `r + 32` together.

And brightness is *time*, not amplitude. A HUB75 pixel is a switch, on or off, so intensity comes from binary coded modulation: bit plane `p` is displayed for 2^p time units, and a value lights its planes for a total proportional to itself. That weighting is the peripheral's output-enable window, and it is not built yet, which is why the depth cap below exists. Either way the encoder stores each plane once. Emitting plane `p` 2^p times is the obvious reading and it is wrong: 255 passes at 8-bit is 1,060,800 bytes for a single 64x64 panel, where storing once is 33,280.
And brightness is *time*, not amplitude. A HUB75 pixel is a switch, on or off, so intensity comes from binary coded modulation: bit plane `p` is displayed for 2^p time units, and a value lights its planes for a total proportional to itself. That weighting is the output-enable window, which the encoder sets per plane. Either way the encoder stores each plane once. Emitting plane `p` 2^p times is the obvious reading and it is wrong: 255 passes at 8-bit is 1,060,800 bytes for a single 64x64 panel, where storing once is 33,280.

Check warning on line 419 in docs/moonmodules/light/drivers.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [MoonLight.SentenceLength] Sentence runs past 30 words; it is probably two thoughts joined by a comma. Raw Output: {"message":"[MoonLight.SentenceLength] Sentence runs past 30 words; it is probably two thoughts joined by a comma.","location":{"path":"docs/moonmodules/light/drivers.md","range":{"start":{"line":419,"column":367},"end":{"line":419,"column":375}}},"severity":"INFO","code":{"value":"MoonLight.SentenceLength"}}

Check warning on line 419 in docs/moonmodules/light/drivers.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [MoonLight.SentenceLength] Sentence runs past 30 words; it is probably two thoughts joined by a comma. Raw Output: {"message":"[MoonLight.SentenceLength] Sentence runs past 30 words; it is probably two thoughts joined by a comma.","location":{"path":"docs/moonmodules/light/drivers.md","range":{"start":{"line":419,"column":42},"end":{"line":419,"column":43}}},"severity":"INFO","code":{"value":"MoonLight.SentenceLength"}}

**What depth costs, and why it stops at 4.** Every bit plane is a full scan, so depth costs refresh and memory linearly, and each slot on the wire is 2 bytes because the address, latch and output-enable lines sit above bit 7 of a 16-bit word. One 64x64 panel at 1/32 scan is 16,640 bytes a frame at 4-bit. The planes are emitted once each rather than weighted for 2^p time, so bit 3 lights as long as bit 0 and a fifth plane would buy nothing the eye can find. The weighting is [backlogged](https://github.com/MoonModules/MoonLight/blob/main/docs/work/future/backlog-light.md), and the cap lifts with it.
**What depth costs, and why brightness is time.** Every bit plane is a full scan, so depth costs refresh and memory linearly, and each slot on the wire is 2 bytes because the address, latch and output-enable lines sit above bit 7 of a 16-bit word. One 64x64 panel at 1/32 scan is 16,384 bytes a frame at 4-bit and 24,576 at 6-bit. A panel's chips only switch a column on or off, so each plane is lit twice as long as the one below and brightness shortens every plane's lit time instead of scaling the colors, which keeps every level at any brightness. The price is top brightness: full white lights about a third of the time at 6-bit. Repeating the upper planes instead, for full brightness and 8-bit depth, is [backlogged](https://github.com/MoonModules/MoonLight/blob/main/docs/work/future/backlog-light.md).

Check warning on line 421 in docs/moonmodules/light/drivers.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [MoonLight.SentenceLength] Sentence runs past 30 words; it is probably two thoughts joined by a comma. Raw Output: {"message":"[MoonLight.SentenceLength] Sentence runs past 30 words; it is probably two thoughts joined by a comma.","location":{"path":"docs/moonmodules/light/drivers.md","range":{"start":{"line":421,"column":332},"end":{"line":421,"column":333}}},"severity":"INFO","code":{"value":"MoonLight.SentenceLength"}}

Check warning on line 421 in docs/moonmodules/light/drivers.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [MoonLight.SentenceLength] Sentence runs past 30 words; it is probably two thoughts joined by a comma. Raw Output: {"message":"[MoonLight.SentenceLength] Sentence runs past 30 words; it is probably two thoughts joined by a comma.","location":{"path":"docs/moonmodules/light/drivers.md","range":{"start":{"line":421,"column":51},"end":{"line":421,"column":56}}},"severity":"INFO","code":{"value":"MoonLight.SentenceLength"}}
1 change: 1 addition & 0 deletions docs/moonmodules/light/supporting.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ The container of driver modules, owning the shared buffer and the per-light outp
- `on`: master power, on by default.

Off scales the output to black while preserving `brightness`, so switching on restores the level. Every consumer drives it: the UI, infrared, the WLED app, Home Assistant and MQTT.
- 🎚️ `relayPins`: GPIOs switching the LED power supply, closed while `on` and brightness > 0.
- `brightness`: global output brightness, multiplied with each driver's `localBrightness`.
- `palette`: what effects sample from, built in or [scripted](moonlive.md); settable by name too.
- `multicore`: run the output stage on the second core.
Expand Down
14 changes: 14 additions & 0 deletions docs/reference/MIGRATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,20 @@ For a device still on old firmware (no Backup button yet), the [installer page](

## Unreleased

### An audio codec is the device model's setting

**Action: on an ESP32-S31 CoreBoard, apply its device model again, or set the I2C bus to SDA 51 / SCL 50 under System and AudioService's `codec` to `ES8311` with `mclkPin` 52.**

The codec in front of a microphone is now AudioService's runtime configuration, where the S31 firmware used to assume it. One P4 firmware so serves every P4 board, with or without a codec.
A board configured before this change has no codec set, and its microphone stays silent until it does.

### The I2C scan is the board's I2C bus

**Action: *nothing* for a restored backup, which carries the pins over; on a running device, set `sda` and `scl` again under System, I2cBus.**

The scan module became the owner of the board's I2C bus, which every device on it shares. A codec now names only its address, and the wires are stated once.
A device's saved scan pins sit under the old module type and are not read at boot.

### The access point always broadcasts its name and follows the radio's channel

**Action: *nothing*.**
Expand Down
Loading
Loading