# Patterns and presets on the LED driver This document describes **how patterns are wired**, how **presets** map to patterns, and what each **shipped pattern** expects. For the JSON wire format (`v`: `"1"`, `presets`, `select`, short keys `p` / `c` / `b`, etc.), see [API.md](API.md). ## End-to-end control 1. The controller sends a **v1 JSON** object (ESP-NOW, serial bridge, or one line per message over TCP WebSocket in Wi-Fi mode). 2. `controller_messages.process_data()` parses it and applies fields in a fixed order (see `src/controller_messages.py`): - `device_config` — name, LED count, colour order, startup mode; may reload `presets.json` and re-select the previous preset. - `b` — **global** output brightness (0–255), stored in settings and in `presets.b`. - `presets` — merge definitions into the in-memory preset table (`Presets.edit()` per id). - `clear_presets` — optional wipe of all presets. - `select` — pick the active preset (and optional step) for **this** device (matched by `settings["name"]`). - `default` — update saved default preset when `targets` includes this device. - `manifest` — pattern OTA: fetch pattern `.py` files and `reload_patterns()`. - `save` — persist presets and/or settings when combined with the relevant fields. 3. The main loop calls `presets.tick()` so the active pattern **generator** advances one frame per iteration. ## Presets - **Class:** `src/preset.py` — `Preset` holds the pattern configuration. - **Short keys** (what the driver uses internally after `apply_presets` normalisation): | Key | Meaning | Default | |-----|---------|--------| | `p` | Pattern id (string), must match a registered pattern | `"off"` | | `c` | Colours as RGB tuples (after colour-order conversion) | `[(255,255,255)]` | | `d` | Delay (ms); meaning is pattern-specific | `100` | | `b` | Preset brightness 0–255 (combined with global `presets.b`) | `127` | | `a` | Auto: continuous animation; `false` = manual / beat-stepped where supported | `True` | | `bg` | Background colour (hex string or RGB tuple on device) | `(0,0,0)` | | `n1`–`n6` | Pattern-specific integers | `0` | Long aliases from the controller (`pattern`, `colors`, `delay`, `brightness`, `auto`, `background`) are converted in `Preset.edit()`. - **Persistence:** `presets.json` on flash; **`MAX_PRESETS` = 32** (exceptions for auto-created `"on"` / `"off"`). - **Activation:** `Presets.select(preset_name, step=None)` loads the preset, looks up **`preset.p`** in the pattern registry, and sets `generator = patterns[preset.p](preset)`, then runs one `tick()` so the first frame appears. ## Brightness - **Global:** `presets.b` from message `{"v":"1","b":…}` scales every output channel. - **Per preset:** `preset.b`; combined in `Presets.apply_brightness(colour, preset.b)` as `effective = round(preset_channel * presets.b / 255)` with preset level applied first conceptually (`apply_brightness` takes the preset’s `b` as the override for that colour). ## Pattern registry Built in `Presets.reload_patterns()` (`src/presets.py`): 1. **Built-ins:** `"off"` and `"on"` — methods on the `Presets` instance (not separate files). 2. **Dynamic modules:** Every `patterns/*.py` on flash (except `__init__.py`), imported as `patterns.`. The loader takes the **first class** in the module that defines **`run`**, instantiates it with `Presets(self)` (the driver / NeoPixel wrapper), and registers: ```text patterns[basename] = PatternClass(driver).run ``` So the **`p` field must equal the file basename without `.py`** (e.g. file `radiate.py` ⇒ pattern `"radiate"`). ### Adding or updating patterns on device - **OTA:** v1 message with `"manifest"` (URL or inline JSON listing `files` with `name`, `url` or `code`) — see `apply_patterns_ota()` in `controller_messages.py`. - **HTTP:** `POST /patterns/upload` on the device (`src/main.py`) with a safe `.py` filename; optional reload of the registry. After new files land in `patterns/`, call `presets.reload_patterns()` (done automatically by OTA and upload when configured). ## Auto vs manual (`a`) - **`a: true` (auto):** The main loop keeps calling `tick()`; the generator runs continuously (subject to internal `yield` timing / `utime`). - **`a: false` (manual):** Intended for patterns that advance **once per explicit `select`** (or per beat routing from the controller). The driver does **not** call `select()` again when editing a manual preset-only push — manual steps are driven by incoming `select` messages. Special case in `Presets.select()`: for **manual chase**, if the same preset is re-selected mid-generator, pending frames may be flushed so step indices stay aligned with beats. ## Built-in patterns ### `off` - **Registration:** built-in method `Presets.off`. - **Behaviour:** fills the strip with black (after generator setup, `tick` completes immediately). - **Parameters:** ignores preset colours for the strip; optional `preset` argument unused for pixels. ### `on` - **Registration:** built-in method `Presets.on`. - **Behaviour:** solid fill with `preset.c[0]` (or white if no colours), via `apply_brightness(..., preset.b)`. - **Parameters:** `c`, `b`; `d` / `n*` not used. ## Dynamic pattern: `radiate` - **File:** `src/patterns/radiate.py` - **Class:** `Radiate` — `run(self, preset)` is a **generator** (must `yield` each frame). - **Pattern key:** `p` = `"radiate"` Concept: repeating **nodes** along the strip every **`n1`** LEDs; from each node a lit region expands outward then contracts (timed by **`n2`** / **`n3`**). In **auto**, a new pulse train starts every **`d`** ms and the active colour index advances. In **manual**, a **single** out-and-back cycle runs, then the generator ends (next colour on the next `select`). | Field | Role | |-------|------| | `n1` | Node spacing in LEDs (`>= 1`; half-spacing used for symmetry) | | `n2` | Outbound travel time (ms), `>= 1` | | `n3` | Return travel time (ms), `>= 1` | | `d` | Auto only: interval (ms) between re-triggers; `>= 1` | | `c` | Colour list; cycles per retrigger / per manual cycle | | `bg` | Off state / gap colour (via `preset.background_or`) | | `b` | Preset brightness | | `a` | `true` = repeating pulses on a timer; `false` = one shot per select | Debug: if `presets.debug` is true (from settings), periodic logs print timing and lit LED counts. ## Other pattern names (`blink`, `rainbow`, `pulse`, …) Those pattern **ids** are valid on the **wire** and in **led-controller** `db/pattern.json`, but they are **not** all present in this repository’s `src/patterns/` tree. On a real device they normally appear as **additional** `patterns/*.py` files delivered by OTA or upload. For the intended **`n1`–`n6`** semantics on the wire, use [API.md](API.md) **Pattern-Specific Parameters**; the implementation must match that contract in each module’s `run(preset)` generator. ## Quick reference: files | File | Role | |------|------| | `src/preset.py` | Preset field model and aliases | | `src/presets.py` | Registry, `select`, `tick`, `off` / `on`, dynamic load | | `src/controller_messages.py` | Parse v1 JSON, apply presets/select/brightness/OTA | | `src/patterns/*.py` | One pattern module per dynamic id (basename = `p`) |