Consolidate legacy pattern ids into meteor, particles, sparkle, chase, and colour_cycle with n6/mode style selection; add pattern_modes helper, self-contained tests/all.py, and preset mode alias on wire. Co-authored-by: Cursor <cursoragent@cursor.com>
7.1 KiB
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.
End-to-end control
- The controller sends a v1 JSON object (ESP-NOW, serial bridge, or one line per message over TCP WebSocket in Wi-Fi mode).
controller_messages.process_data()parses it and applies fields in a fixed order (seesrc/controller_messages.py):device_config— name, LED count, colour order, startup mode; may reloadpresets.jsonand re-select the previous preset.b— global output brightness (0–255), stored in settings and inpresets.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 bysettings["name"]).default— update saved default preset whentargetsincludes this device.manifest— pattern OTA: fetch pattern.pyfiles andreload_patterns().save— persist presets and/or settings when combined with the relevant fields.
- The main loop calls
presets.tick()so the active pattern generator advances one frame per iteration.
Presets
-
Class:
src/preset.py—Presetholds the pattern configuration. -
Short keys (what the driver uses internally after
apply_presetsnormalisation):Key Meaning Default pPattern id (string), must match a registered pattern "off"cColours as RGB tuples (after colour-order conversion) [(255,255,255)]dDelay (ms); meaning is pattern-specific 100bPreset brightness 0–255 (combined with global presets.b)127aAuto: continuous animation; false= manual / beat-stepped where supportedTruebgBackground colour (hex string or RGB tuple on device) (0,0,0)n1–n6Pattern-specific integers 0Long aliases from the controller (
pattern,colors,delay,brightness,auto,background) are converted inPreset.edit(). -
Persistence:
presets.jsonon flash;MAX_PRESETS= 32 (exceptions for auto-created"on"/"off"). -
Activation:
Presets.select(preset_name, step=None)loads the preset, looks uppreset.pin the pattern registry, and setsgenerator = patterns[preset.p](preset), then runs onetick()so the first frame appears.
Brightness
- Global:
presets.bfrom message{"v":"1","b":…}scales every output channel. - Per preset:
preset.b; combined inPresets.apply_brightness(colour, preset.b)as
effective = round(preset_channel * presets.b / 255)with preset level applied first conceptually (apply_brightnesstakes the preset’sbas the override for that colour).
Pattern registry
Built in Presets.reload_patterns() (src/presets.py):
-
Built-ins:
"off"and"on"— methods on thePresetsinstance (not separate files). -
Dynamic modules: Every
patterns/*.pyon flash (except__init__.py), imported aspatterns.<basename>. The loader takes the first class in the module that definesrun, instantiates it withPresets(self)(the driver / NeoPixel wrapper), and registers: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 listingfileswithname,urlorcode) — seeapply_patterns_ota()incontroller_messages.py. - HTTP:
POST /patterns/uploadon the device (src/main.py) with a safe.pyfilename; 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 callingtick(); the generator runs continuously (subject to internalyieldtiming /utime).a: false(manual): Intended for patterns that advance once per explicitselect(or per beat routing from the controller). The driver does not callselect()again when editing a manual preset-only push — manual steps are driven by incomingselectmessages.
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,
tickcompletes immediately). - Parameters: ignores preset colours for the strip; optional
presetargument unused for pixels.
on
- Registration: built-in method
Presets.on. - Behaviour: solid fill with
preset.c[0](or white if no colours), viaapply_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 (mustyieldeach 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 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) |