Files
led-driver/docs/patterns.md
Jimmy 55a97ac51c feat(patterns): merge pattern styles and add mode support
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>
2026-05-16 21:14:54 +12:00

7.1 KiB
Raw Blame History

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

  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.
    • bglobal output brightness (0255), 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.pyPreset 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 0255 (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)
    n1n6 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 presets 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.<basename>. The loader takes the first class in the module that defines run, instantiates it with Presets(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 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: Radiaterun(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.

Those pattern ids are valid on the wire and in led-controller db/pattern.json, but they are not all present in this repositorys src/patterns/ tree. On a real device they normally appear as additional patterns/*.py files delivered by OTA or upload. For the intended n1n6 semantics on the wire, use API.md Pattern-Specific Parameters; the implementation must match that contract in each modules 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)