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>
This commit is contained in:
110
docs/API.md
110
docs/API.md
@@ -3,11 +3,13 @@
|
||||
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. It is sent over the **ESP-NOW bridge** (WebSocket) to ESP32 peers and as **single JSON text messages** over the **outbound WebSocket** to **Wi-Fi** drivers (same logical fields).
|
||||
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`).
|
||||
|
||||
**Serial:** UART path and baud come from settings (defaults include `serial_port` such as `/dev/ttyS0` and `serial_baudrate`). **Wi-Fi drivers:** **UDP** on port **8766** is the **discovery** channel: each driver’s 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 Wi‑Fi device IPs when the WebSocket is down (**`wifi_driver_hello_interval_s`** in settings).
|
||||
**ESP-NOW bridge:** Set **`bridge_transport`** to **`wifi`** (default) or **`serial`**. Wi‑Fi 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](espnow-architecture.md).
|
||||
|
||||
**Wi-Fi drivers (optional):** **UDP** on port **8766** is the **discovery** channel: each driver’s 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 Wi‑Fi 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.
|
||||
|
||||
@@ -52,7 +54,7 @@ Profiles are selected with **`POST /profiles/<id>/apply`**, which sets `current_
|
||||
|
||||
Connect to **`ws://<host>:<port>/ws`**.
|
||||
|
||||
- Send **JSON**: the object is forwarded through the **ESP-NOW bridge** (devices envelope or legacy MAC-prefixed 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.
|
||||
- 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](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"}`.
|
||||
|
||||
@@ -62,7 +64,7 @@ Wi-Fi devices are not targeted by `/ws` directly; use **`POST /presets/send`**,
|
||||
|
||||
## HTTP API by resource
|
||||
|
||||
Below, `<id>` values are string identifiers used by the JSON stores (numeric strings in practice).
|
||||
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`
|
||||
|
||||
@@ -74,28 +76,46 @@ Below, `<id>` values are string identifiers used by the JSON stores (numeric str
|
||||
| POST | `/settings/wifi/ap` | Body: `ssid` (required), `password`, `channel` (1–11). Persists AP-related settings. |
|
||||
| GET | `/settings/page` | Serves `templates/settings.html`. |
|
||||
|
||||
### Devices — `/devices`
|
||||
### Bridge — `/settings/wifi`
|
||||
|
||||
Registry in `db/device.json`: storage key **`<id>`** (string, e.g. `"1"`) maps to an object that always includes:
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| **`id`** | Same as the storage key (stable handle for URLs). |
|
||||
| **`name`** | Shown in the UI and used in `select` keys. |
|
||||
| **`type`** | `led` (only value today; extensible). |
|
||||
| **`transport`** | `espnow` or `wifi`. |
|
||||
| **`address`** | For **`espnow`**: optional 12-character lowercase hex MAC. For **`wifi`**: optional IP or hostname string. |
|
||||
| **`default_pattern`**, **`zones`** | Optional. Legacy **`tabs`** may still appear in old files and is migrated away on load. |
|
||||
|
||||
Existing records without `type` / `transport` / `id` are backfilled on load (`led`, `espnow`, and `id` = key).
|
||||
Pi-side bridge configuration (ESP-NOW path to drivers). Mounted from `controllers/wifi_bridge.py`.
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/devices` | Map of device id → device object. |
|
||||
| GET | `/settings/wifi/interfaces` | List Wi‑Fi 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 Wi‑Fi 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`**, **`default_pattern`**, **`zones`**. Returns `{ "<id>": { ... } }`, 201. |
|
||||
| POST | `/devices` | Create. Body: **`name`** (required), **`type`** (default `led`), **`transport`** (default `espnow`), optional **`address`**, **`mac`** (required for Wi‑Fi 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`
|
||||
|
||||
@@ -228,26 +248,56 @@ Pattern metadata lives in **`db/pattern.json`**; driver source files live under
|
||||
|
||||
## LED driver message format (transport / ESP-NOW / Wi-Fi)
|
||||
|
||||
Messages are JSON objects. The Pi **`build_message()`** helper (`src/util/espnow_message.py`) produces the same shape sent over serial and forwarded by the ESP32 bridge, and the same logical object can be sent as a **single JSON text message** to a Wi-Fi driver over the **WebSocket**.
|
||||
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.
|
||||
|
||||
### Top-level fields
|
||||
### Devices envelope (Pi → bridge)
|
||||
|
||||
On the bridge link, traffic uses a top-level **`dv`** map (long name **`devices`** still accepted on receive):
|
||||
|
||||
```json
|
||||
{
|
||||
"v": "1",
|
||||
"presets": { },
|
||||
"select": { },
|
||||
"save": true,
|
||||
"default": "preset_id",
|
||||
"b": 255
|
||||
"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](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):
|
||||
|
||||
```json
|
||||
{
|
||||
"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.
|
||||
- **`presets`**: Map of **preset id** (string) → preset object (see below). Optional **`name`** field on each value is accepted for display; the driver keys presets by map key.
|
||||
- **`select`**: Map of **device name** (as in device settings) → `[ "preset_id" ]` or `[ "preset_id", step ]`.
|
||||
- **`save`**: If present (e.g. true), the driver may persist presets to flash after applying.
|
||||
- **`default`**: Preset id string to use as startup default on the device.
|
||||
- **`b`**: Optional **global** brightness 0–255 (driver applies this in addition to per-preset brightness).
|
||||
|
||||
### Preset object (wire / driver keys)
|
||||
|
||||
Reference in New Issue
Block a user