Files
led-controller/docs/API.md
Jimmy cfdd6de291 docs(espnow): update docs and tests for p2p merge
Align API, architecture, and help with devices envelope transport,
bridge wifi/serial settings, and MAC-keyed device registry. Fix
endpoint tests for envelope identify payloads; remove obsolete p2p.py.
Bump led-tool for --serial-usb bridge provisioning.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 21:10:06 +12:00

20 KiB
Raw Blame History

LED Controller API

This document covers:

  1. HTTP and WebSocket exposed by the Raspberry Pi app (src/main.py) — profiles, zones, presets, transport send, pattern OTA helpers, and related resources.
  2. LED driver JSON — the compact v1 message format. ESP-NOW traffic is wrapped in a devices envelope (dv map keyed by MAC) on the Pi ↔ bridge link (WebSocket or USB serial); drivers receive compact per-device bodies (≤250 bytes). Wi-Fi drivers still accept single JSON text messages over an outbound WebSocket (same logical fields).

Default HTTP listen address: 0.0.0.0. Port defaults to 80; override with the PORT environment variable (see pipenv run run).

ESP-NOW bridge: Set bridge_transport to wifi (default) or serial. WiFi mode uses bridge_ws_url (e.g. ws://192.168.4.1/ws) after joining the bridge AP; serial mode uses bridge_serial_port and bridge_serial_baudrate (default 921600). Saved bridge profiles and connect helpers live under /settings/wifi/* (see below). Architecture: espnow-architecture.md.

Wi-Fi drivers (optional): UDP on port 8766 is the discovery channel: each drivers JSON hello (device_name, MAC, optional type) creates or updates that device in db/device.json (keyed by MAC); the Pi echoes the datagram. After a valid hello with v: "1", the Pi also opens an outbound WebSocket to that IP (wifi_driver_ws_port, default 80; wifi_driver_ws_path, default /ws) for v1 commands; presets are not pushed automatically on connect (use Send Presets / profile apply). The Pi may send periodic UDP hello nudges to known WiFi device IPs when the WebSocket is down (wifi_driver_hello_interval_s in settings).

All JSON APIs use Content-Type: application/json for bodies and responses unless noted.


UI behavior notes

The main UI has two modes controlled by the mode toggle:

  • Run mode: optimized for operation (zone/preset selection and profile apply).
  • Edit mode: shows editing/management controls (zones, presets, patterns, colour palette, send presets, profile management actions, Devices registry for LED driver names/MACs, and related tools).

Profiles are available in both modes, but behavior differs:

  • Run mode: profile apply only.
  • Edit mode: profile create/clone/delete/apply.

POST /presets/send is wired to the Send Presets UI action, which is exposed in Edit mode.


Session and scoping

Several routes use @with_session: the server stores a current profile in the session (cookie). Endpoints that scope data to “the current profile” (notably /presets) only return or mutate presets whose profile_id matches that session value.

Profiles are selected with POST /profiles/<id>/apply, which sets current_profile in the session.


Static pages and assets

Method Path Description
GET / Main UI (templates/index.html)
GET /settings/page Standalone settings page (templates/settings.html)
GET /favicon.ico Empty response (204)
GET /static/<path> Static files under src/static/

WebSocket: /ws

Connect to ws://<host>:<port>/ws.

  • Send JSON: the object is forwarded through the ESP-NOW bridge as a devices envelope (or legacy MAC-prefixed / binary payload). Optional key to: 12-character hex MAC address; if present it is removed from the object and the payload is sent to that peer; otherwise the default destination from settings is used. Example envelope: msg.json.
  • Send non-JSON text: forwarded as raw bytes with the default address.
  • On send failure, the server may reply with {"error": "Send failed"}.

Wi-Fi devices are not targeted by /ws directly; use POST /presets/send, device routes, or POST /patterns/<name>/send as appropriate.


HTTP API by resource

Below, <id> values are string identifiers used by the JSON stores. Device ids are 12-character lowercase hex MACs (no colons); other resources typically use numeric string ids.

Settings — /settings

Method Path Description
GET /settings Full settings object (from settings.json / Settings model).
PUT /settings/settings Merge keys into settings and save. Returns {"message": "Settings updated successfully"}.
GET /settings/wifi/ap Saved WiFi AP fields: saved_ssid, saved_password, saved_channel, active (Pi: active is always false).
POST /settings/wifi/ap Body: ssid (required), password, channel (111). Persists AP-related settings.
GET /settings/page Serves templates/settings.html.

Bridge — /settings/wifi

Pi-side bridge configuration (ESP-NOW path to drivers). Mounted from controllers/wifi_bridge.py.

Method Path Description
GET /settings/wifi/interfaces List WiFi interfaces via NetworkManager (nmcli).
GET /settings/wifi/scan?device=<ifname> Scan SSIDs on the given interface.
GET /settings/wifi/bridges Bridge state: bridge_transport, bridge_ws_url, bridge_connected, wifi_interface, saved bridges profiles, serial port/baud.
PUT /settings/wifi/bridges Merge bridge settings and/or replace the bridges profile list.
DELETE /settings/wifi/bridges/<id> Remove a saved bridge profile.
POST /settings/wifi/bridges/<id>/connect Connect using a saved profile (transport: wifi or serial).
POST /settings/wifi/connect Join a bridge AP and open its WebSocket. Body: device, ssid, optional password, ap_ip (default 192.168.4.1), ws_port, label, save_profile.
POST /settings/wifi/serial/connect Open the bridge over USB serial. Body: port, optional baudrate, label, save_profile.

Devices — /devices

Registry in db/device.json: storage key <id> is the device MAC (12 lowercase hex characters, no colons). Each record includes:

Field Description
id Same as the storage key (12-char hex MAC).
name Shown in the UI; matched when building zone select lists.
type led (only value today; extensible).
transport espnow (default) or wifi.
address For espnow: same as id (MAC). For wifi: IP or hostname used for outbound WebSocket / OTA.
connected Response-only on GET list/detail: always null today (ESP-NOW has no live session flag on the Pi).
default_pattern, zones Optional. Legacy tabs may still appear in old files and is migrated away on load.

Drivers also self-register on ESP-NOW ANNOUNCE (bridge uplink) or WiFi UDP hello; manual POST /devices is optional.

Method Path Description
GET /devices Map of device id → device object (includes connected).
GET /devices/<id> One device, 404 if missing.
POST /devices Create. Body: name (required), type (default led), transport (default espnow), optional address, mac (required for WiFi when address is set), default_pattern, zones. Returns { "<id>": { ... } }, 201.
PUT /devices/<id> Partial update. name cannot be cleared. id in the body is ignored. type / transport validated; address normalised for the resulting transport.
DELETE /devices/<id> Remove device.
POST /devices/<id>/identify ESP-NOW: sends a short red blink preset (__identify, 10 Hz) via the bridge, then off after ~2 s. Not persisted on the Pi.
POST /groups/<id>/identify Same identify blink for every device in the group (broadcast envelope; drivers filter by group membership).

Profiles — /profiles

Method Path Description
GET /profiles {"profiles": {...}, "current_profile_id": "<id>"}. Ensures a default current profile when possible.
GET /profiles/current {"id": "...", "profile": {...}}
GET /profiles/<id> Single profile. If <id> is current, same as /profiles/current.
POST /profiles Create profile. Body may include name and other fields. Optional seed_dj_zone (request-only) seeds a DJ zone + presets. New profiles always get a populated default zone. Returns { "<id>": { ... } } with status 201.
POST /profiles/<id>/apply Sets session current profile to <id>.
POST /profiles/<id>/clone Clone profile (zones, palettes, presets). Body may include name.
PUT /profiles/current Update the current profile (from session).
PUT /profiles/<id> Update profile by id.
DELETE /profiles/<id> Delete profile.

Presets — /presets

Scoped to current profile in session (see above).

Method Path Description
GET /presets Map of preset id → preset object for the current profile only.
GET /presets/<id> One preset, 404 if missing or wrong profile.
POST /presets Create preset; server assigns id and sets profile_id. Body fields stored on the preset. Returns { "<id>": { ... } }, 201.
PUT /presets/<id> Update preset (must belong to current profile).
DELETE /presets/<id> Delete preset.
POST /presets/send Push presets to the LED driver over the configured transport (see below).

POST /presets/send body:

{
  "preset_ids": ["1", "2"],
  "save": true,
  "default": "1",
  "destination_mac": "aabbccddeeff"
}
  • preset_ids (or ids): non-empty list of preset ids to include.
  • save: if true, the outgoing message includes "save": true so the driver may persist presets (default true).
  • default: optional preset id string; forwarded as top-level "default" in the driver message (startup selection on device).
  • destination_mac (or to): optional 12-character hex MAC for unicast; omitted uses the transport default (e.g. broadcast).

Response on success includes presets_sent, messages_sent (chunking splits payloads so each JSON string stays ≤ 240 bytes).

Stored preset records can include:

  • colors: resolved hex colours for editor/display.
  • palette_refs: optional array of palette indexes parallel to colors. If a slot contains an integer index, the colour is linked to the current profile palette at that index.

Zones — /zones

Method Path Description
GET /zones zones (map of zone id → zone object), zone_order, current_zone_id, profile_id for the session-backed profile.
GET /zones/current Current zone from cookie/session.
POST /zones Create zone; optional JSON name, names, presets; can append to current profiles zone list.
GET /zones/<id> Zone JSON.
PUT /zones/<id> Update zone.
DELETE /zones/<id> Delete zone; can delete current to remove the active zone; updates profile zone list.
POST /zones/<id>/set-current Sets current_zone cookie.
POST /zones/<id>/clone Clone zone into current profile.

Palettes — /palettes

Method Path Description
GET /palettes Map of id → colour list.
GET /palettes/<id> {"colors": [...], "id": "<id>"}
POST /palettes Body may include colors. Returns palette object with id, 201.
PUT /palettes/<id> Update colours (name ignored).
DELETE /palettes/<id> Delete palette.

Groups — /groups

Method Path Description
GET /groups All groups.
GET /groups/<id> One group.
POST /groups Create; optional name and fields.
PUT /groups/<id> Update.
DELETE /groups/<id> Delete.

Scenes — /scenes

Method Path Description
GET /scenes All scenes.
GET /scenes/<id> One scene.
POST /scenes Create (body JSON stored on scene).
PUT /scenes/<id> Update.
DELETE /scenes/<id> Delete.

Sequences — /sequences

Method Path Description
GET /sequences All sequences.
GET /sequences/<id> One sequence.
POST /sequences Create; may use group_name, presets in body.
PUT /sequences/<id> Update.
DELETE /sequences/<id> Delete.

Patterns — /patterns

Pattern metadata lives in db/pattern.json; driver source files live under led-driver/src/patterns/. Several routes expose a runtime map (metadata merged with on-disk .py names so new files appear in menus).

Method Path Description
GET /patterns Runtime pattern map (object keyed by pattern id).
GET /patterns/definitions Same runtime map (intended for UI “definitions” clients).
GET /patterns/ota/manifest JSON {"files":[{"name":"blink.py","url":"http://<Host>/patterns/ota/file/blink.py"},...]} for OTA pulls. Requires Host header.
GET /patterns/ota/file/<name> Raw .py source for one driver pattern (name must be a safe filename, e.g. rainbow.py).
POST /patterns/<name>/send Push a manifest JSON line to Wi-Fi devices so they pull one pattern file over HTTP. Body may include device_id to target one device; otherwise all Wi-Fi devices with an address are tried. <name> may be with or without .py.
POST /patterns/upload Body JSON: name, code, optional overwrite (default true). Writes led-driver/src/patterns/<name>.py.
POST /patterns/driver Body JSON: name (identifier), code, optional metadata (min_delay, max_delay, max_colors, n1n8, overwrite). Creates/updates both the .py file and db/pattern.json via the Pattern model.
GET /patterns/<id> One pattern record from the Pattern model (metadata only).
POST /patterns Create (name, optional data).
PUT /patterns/<id> Update.
DELETE /patterns/<id> Delete.

Devices — pattern OTA push

Method Path Description
POST /devices/<id>/patterns/push Wi-Fi only. Asks the driver at address to pull pattern files from this server. Optional body manifest: either a URL string pointing at a manifest JSON document, or a manifest object (same shape as in driver messages). If omitted, a default manifest is built from the request Host header.

LED driver message format (transport / ESP-NOW / Wi-Fi)

Messages are JSON objects. The Pi build_message() helper (src/util/espnow_message.py) produces per-device bodies; build_devices_envelope() (src/util/bridge_envelope.py) wraps them for the bridge WebSocket or USB serial link. Wi-Fi drivers accept the same logical body as a single JSON text message over the outbound WebSocket.

Devices envelope (Pi → bridge)

On the bridge link, traffic uses a top-level dv map (long name devices still accepted on receive):

{
  "v": "1",
  "dv": {
    "e8:f6:0a:16:ea:10": {
      "p": { "2": { "p": "on", "c": ["#FFFFFF"], "a": true } },
      "s": ["2", 0],
      "g": ["5"],
      "sg": false,
      "sv": true
    }
  }
}

See espnow-architecture.md for routing (sg, broadcast MAC ff:ff:ff:ff:ff:ff, and group filtering).

Per-device body fields (inside dv or Wi-Fi WebSocket)

Short wire keys are used on the bridge and over ESP-NOW (long names still accepted on receive):

{
  "v": "1",
  "p": { },
  "s": ["preset_id", 0],
  "sv": true,
  "df": "preset_id",
  "b": 255,
  "g": ["5"],
  "sg": false
}
Short Long Meaning
p presets Map of preset id → preset object (see below).
s select ["preset_id"] or ["preset_id", step] — routing is by MAC envelope / group membership, not by device name.
sv save If true, driver may persist presets to flash.
df default Startup default preset id.
g groups Group ids for membership updates or broadcast filtering.
sg set_groups If true, replace stored group list before applying the body.
  • v (required): Must be "1" or the driver ignores the message.
  • b: Optional global brightness 0255 (driver applies this in addition to per-preset brightness).

Preset object (wire / driver keys)

On the wire, presets use short keys (saves space in the ≤240-byte chunks):

Key Meaning Notes
p Pattern id off, on, blink, rainbow, pulse, transition, chase, circle
c Colours Array of "#RRGGBB" hex strings; converted to RGB on device
d Delay ms Default 100
b Preset brightness 0255; combined with global b on the device
a Auto true: run continuously; false: one step/cycle per “beat”
n1n6 Pattern parameters See below

The HTTP apps POST /presets/send path builds this from stored presets via build_preset_dict() (long names like pattern / colors in the DB are translated to p / c / …).

Pattern-specific parameters (n1n6)

Rainbow

  • n1: Step increment on the colour wheel per update (default 1).

Pulse

  • n1: Attack (fade in) ms
  • n2: Hold ms
  • n3: Decay (fade out) ms
  • d: Off time between pulses ms

Transition

  • d: Transition duration ms

Chase

  • n1: LEDs with first colour
  • n2: LEDs with second colour
  • n3: Movement on even steps (may be negative)
  • n4: Movement on odd steps (may be negative)

Circle

  • n1: Head speed (LEDs/s)
  • n2: Max length
  • n3: Tail speed (LEDs/s)
  • n4: Min length

Select messages

{
  "select": {
    "device_name": ["preset_id"],
    "other_device": ["preset_id", 10]
  }
}
  • One element: select preset; step behavior follows driver rules (reset on off, etc.).
  • Two elements: explicit step for sync.

Beat and sync behavior

  • Sending select again with the same preset name acts as a beat (advances manual patterns / restarts generators per driver logic).
  • Choosing off resets step as a sync point; then selecting a pattern aligns step 0 across devices unless a step is passed explicitly.

Example (compact preset map)

{
  "v": "1",
  "save": true,
  "presets": {
    "1": {
      "name": "Red blink",
      "p": "blink",
      "c": ["#FF0000"],
      "d": 200,
      "b": 255,
      "a": true,
      "n1": 0, "n2": 0, "n3": 0, "n4": 0, "n5": 0, "n6": 0
    }
  },
  "select": {
    "living-room": ["1"]
  }
}

Processing summary (driver)

  1. Reject if v != "1".
  2. Apply optional top-level b (global brightness).
  3. For each entry in presets, normalize colours and upsert preset by id.
  4. If this devices name appears in select, run selection (optional step).
  5. If default is set, store startup preset id.
  6. If save is set, persist presets.

Error handling (HTTP)

Controllers typically return JSON with an error string and 4xx/5xx status codes. Invalid JSON bodies often yield {"error": "Invalid JSON"}.


Notes

  • Human-readable preset fields (pattern, colors, delay, …) are fine in the web app / database; the send path converts them to p / c / d for the driver.
  • For a copy of the older long-key reference, see led-driver/docs/API.md in this repo (conceptually the same behavior; wire format prefers short keys).