Files
led-controller/docs/espnow-architecture.md
Jimmy 4fc3f46866 feat(espnow): Pi bridge client, binary wire, and espnow-sender firmware
Replace serial/Wi-Fi driver transport paths with WebSocket bridge client,
binary espnow_wire delivery, device announce registry, and restructured
espnow-sender (AP + broadcast passthrough). Includes docs and tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 22:44:44 +12:00

5.6 KiB
Raw Blame History

ESP-NOW transport architecture

This document describes how led-controller, the bridge ESP32, and led-driver devices work together. Wire-level byte layouts are in espnow-binary-protocol.md.

On the wire: binary only (no JSON) for ESP-NOW and Pi↔bridge WebSocket. The Pi web UI and db/*.json still use JSON internally.

System overview

Three-node ESP-NOW architecture

Component Firmware / path Role
led-controller Raspberry Pi, src/ Web app; WebSocket client to bridge; device registry; builds binary commands
Bridge espnow-sender/ WebSocket server /ws; relays binary ↔ ESP-NOW; max 20 peers (LRU)
led-driver led-driver/ submodule Boot ANNOUNCE broadcast; applies GROUPS, CMD, GROUP_CMD

Configure the Pi in settings.json:

{
  "bridge_ws_url": "ws://192.168.4.1/ws",
  "wifi_channel": 6
}

Connect the Pi to the bridge access point (SSID = bridge name in /settings.json, default IP 192.168.4.1). All nodes must use the same 2.4 GHz channel (Pi sends BRIDGE_CH on connect; bridge updates AP + ESP-NOW STA).


Boot and registration

Boot and registration sequence

  1. Driver powers on and sends ANNOUNCE to broadcast MAC ff:ff:ff:ff:ff:ff.
  2. Bridge receives it and forwards a WebSocket uplink frame to the Pi (peer MAC + packet).
  3. Pi upserts the device in db/device.json (key = 12-char hex MAC).
  4. Pi scans db/group.json and builds a GROUPS packet.
  5. Pi sends GROUPS unicast to that MAC via the bridge.
  6. Driver stores group ids in RAM for GROUP_CMD filtering.

If the Pi or bridge is not up yet, the driver re-sends ANNOUNCE periodically until GROUPS arrives.


Sending presets and commands

Command delivery flow

  1. UI or API triggers a send (e.g. POST /presets/send).
  2. Pi builds one or more CMD packets (v2 binary envelope, chunked to ≤250 bytes).
  3. Each packet is wrapped in a WebSocket downlink frame (unicast MAC or broadcast).
  4. Bridge forwards on ESP-NOW.
  5. Driver parses and applies (presets, select, brightness, device_config, etc.).

For a group, Pi may send GROUP_CMD on broadcast once per chunk; only drivers that belong to that group apply the payload.


Packet layers

Packet layer stack

Layer A — WebSocket frame (Pi ↔ bridge)

Offset Size Field
0 1 flags — bit0 = broadcast (ff:ff:…); peer ignored if set
1 6 peer — destination MAC (raw bytes)
7 Full ESP-NOW packet (layer B)

Uplink (bridge → Pi): same layout; flags = 0, peer = sender.

Ack (bridge → Pi after downlink): 1 byte — 0x01 ok, 0x00 error.

Layer B — ESP-NOW packet (on air)

Offset Size Field
0 1 Magic 0x4C ('L')
1 1 Message type
2 Body (≤248 bytes so total ≤250)

Message types

Type Value Direction Purpose
ANNOUNCE 0x01 Driver → broadcast Boot settings
GROUPS 0x02 Pi → driver Group membership
CMD 0x03 Pi → driver Command (v2 envelope)
GROUP_CMD 0x04 Pi → broadcast Command scoped to one group
BRIDGE_CH 0x10 Pi → bridge Set STA channel 111

Layer C — v2 command envelope (inside CMD / GROUP_CMD)

Used for presets, select, default, brightness. No JSON.

Byte Field
0 Version 2
1 Brightness wire 0127 (→ 0255); 128255 = unchanged
2 lp — presets section length
3 ls — select section length
4 ld — default section length
5… Presets blob (lp bytes)
Select blob (ls bytes)
Default blob (ld bytes)

Optional trailing 0x01 after the envelope in CMD means save (persist to flash).

Implementation: src/util/binary_envelope.py, src/util/espnow_wire.py.


Message body reference

ANNOUNCE (0x01)

Sender MAC comes from ESP-NOW headers, not the body.

name_len (u8) | name (utf-8) | num_leds (u16 LE) | color_order (u8) | startup_mode (u8) | brightness (u8) | device_type (u8)
color_order startup_mode
0=rgb, 1=rbg, 2=grb, 3=gbr, 4=brg, 5=bgr 0=default, 1=last, 2=off

GROUPS (0x02)

count (u8) | repeat: id_len (u8) | group_id (utf-8)

Group ids match keys in db/group.json (e.g. "5", "18").

GROUP_CMD (0x04)

group_id_len (u8) | group_id (utf-8) | v2 envelope | [optional 0x01 save]

Driver applies only if group_id is in its stored list.


Size limits and chunking

  • 250 bytes max per ESP-NOW datagram.
  • Large preset libraries → multiple CMD packets from the Pi.
  • Bridge stores at most 20 peer MACs; oldest peer evicted (LRU) when full.

Topic Location
Byte-level spec espnow-binary-protocol.md
Pi wire codec src/util/espnow_wire.py
Pi bridge client src/models/bridge_ws_client.py
Bridge firmware espnow-sender/main.py
Driver ESP-NOW led-driver/src/espnow_transport.py