Developer Guide — API Reference¶
This document is the reference for the HTTP REST API and WebSocket protocol exposed by a running projectMM instance. Architecture and design rationale live in architecture.md. See user-guide/getting-started.md for how to connect.
Base URL¶
| Platform | HTTP base | WebSocket |
|---|---|---|
| ESP32 in AP mode | http://4.3.2.1 (port 80) |
ws://4.3.2.1:81 |
| ESP32 after STA joins | http://<device-ip> (port 80) |
ws://<device-ip>:81 |
| PC / rPi | http://localhost:80 |
ws://localhost:81 |
REST API¶
GET /¶
Returns the embedded web frontend (HTML/CSS/JS). Open in any browser.
Response: 200 text/html
GET /api/modules¶
Returns the full module list: identity, schema (controls with current values), parent/child relationships, and per-module timing.
Response: 200 application/json
[
{
"id": "producer1",
"type": "EffectsLayer",
"name": "EffectsLayer",
"category": "layout",
"parent_id": "",
"permanent": false,
"controls": [
{ "key": "width", "type": "slider", "value": 16, "min": 1, "max": 256 },
{ "key": "height", "type": "slider", "value": 16, "min": 1, "max": 256 }
],
"timing": { "avg": 0.01, "min": 0.0, "max": 0.1 }
},
{
"id": "sine1",
"type": "SineEffectModule",
"name": "SineEffectModule",
"category": "effect",
"parent_id": "producer1",
"permanent": false,
"controls": [
{ "key": "frequency", "type": "slider", "value": 1, "min": 0, "max": 8 },
{ "key": "amplitude", "type": "slider", "value": 255, "min": 0, "max": 255 },
{ "key": "enabled", "type": "toggle", "value": true }
],
"timing": { "avg": 0.05, "min": 0.04, "max": 0.12 }
}
]
Control type values:
| type | Value | Notes |
|---|---|---|
slider |
number | Has min, max, optionally step |
toggle |
boolean | Rendered as a checkbox |
select |
string | Has options array |
Notes:
- controls always includes the enabled toggle (added by StatefulModule after each module's own setup()).
- timing values are in milliseconds.
- permanent: true means the module cannot be removed via the REST API.
GET /api/types¶
Returns the list of registered module type names. Used by the frontend's "add module" picker.
Response: 200 application/json
["EffectsLayer", "DriverLayer", "SineEffectModule", "BrightnessModifierModule",
"PreviewModule", "SystemStatusModule", "Network", "WifiStaModule",
"WifiApModule", "EthernetModule"]
POST /api/modules¶
Adds a new module at runtime. The change is persisted to state/modulemanager.json immediately.
Request body:
{
"type": "SineEffectModule",
"id": "sine2",
"parent_id": "producer1",
"props": { "frequency": 2.0 },
"inputs": { "layer": "producer1" },
"core": 1
}
| Field | Required | Notes |
|---|---|---|
type |
yes | Must be a registered type (see GET /api/types) |
id |
yes | Must be unique across all modules |
parent_id |
no | Id of the parent module; empty string for top-level |
props |
no | Construction-time parameters injected via setProps() |
inputs |
no | Data-flow wiring: key → source module id |
core |
no | CPU core (1 or 0). Default: 1 |
Response:
| Status | Body | Meaning |
|---|---|---|
201 |
{"ok": true} |
Module added and running |
400 |
{"ok": false, "error": "type and id required"} |
Missing required fields or bad JSON |
409 |
{"ok": false, "error": "id exists, unknown type, or invalid parent"} |
Id already in use, type not registered, or parent id unknown |
DELETE /api/modules/<id>¶
Removes a module at runtime. The change is persisted to state/modulemanager.json immediately.
Path parameter: id — the id of the module to remove.
Response:
| Status | Body | Meaning |
|---|---|---|
200 |
{"ok": true} |
Module removed |
400 |
{"ok": false, "error": "id required"} |
Empty id in path |
403 |
{"ok": false, "error": "permanent module"} |
Module declared itself permanent |
404 |
{"ok": false, "error": "not found"} |
No module with that id |
409 |
{"ok": false, "error": "module has children — remove children first"} |
Parent still has children |
POST /api/control¶
Sets a control value on a running module. The change is written to the module's field immediately and debounced to disk.
Request body:
{
"id": "sine1",
"key": "frequency",
"value": 3
}
| Field | Notes |
|---|---|
id |
Module id |
key |
Control key as declared in addControl() |
value |
New value. Type must match the control's declared type (number for slider, boolean for toggle, string for select) |
Response:
| Status | Body | Meaning |
|---|---|---|
200 |
{"ok": true} |
Control updated |
400 |
{"ok": false, "error": "bad json"} |
Malformed request body |
404 |
{"ok": false, "error": "not found"} |
Module id or key not found |
WebSocket Protocol¶
The WebSocket server listens on port 81 (separate from the HTTP server on port 80).
The frontend connects to ws://<host>:81/ws.
State push (server → client)¶
The server broadcasts a state message after each scheduler tick, at most at ~50 Hz (20 ms interval). The message is a JSON text frame containing the same structure as GET /api/modules but only the live state fields (controls with current values, timing). The frontend re-renders on each push.
[
{ "id": "sine1", "controls": [
{ "key": "frequency", "value": 2 },
{ "key": "amplitude", "value": 200 },
{ "key": "enabled", "value": true }
],
"timing": { "avg": 0.05, "min": 0.04, "max": 0.12 }
}
]
Pixel push (server → client)¶
The server broadcasts a binary WebSocket frame containing the current pixel buffer after each tick, at most at ~50 Hz. The frontend uploads this directly to a WebGL texture.
Frame layout:
| Offset | Size | Content |
|---|---|---|
| 0 | 1 byte | Frame type: 0x01 = pixel frame |
| 1 | 2 bytes | Width (little-endian uint16) |
| 3 | 2 bytes | Height (little-endian uint16) |
| 5 | width × height × 3 bytes | RGB pixels, row-major |
At 16×16 the frame is 773 bytes. At 128×128 it is 49,157 bytes — well within a single WebSocket frame.
Heartbeat (client → server)¶
The frontend sends a "ping" text frame every 25 seconds to prevent idle-timeout disconnection on mobile carriers and NAT firewalls. The server ignores this message (no "pong" is sent back).
Notes on Sensitive Controls¶
Controls declared with sensitive: true (e.g. WiFi passwords) are included in GET /api/modules and GET /api/types only as schema metadata — the current value is never broadcast over WebSocket or returned in the REST response. Use POST /api/control to set them.
Test coverage¶
→ WebSocket — getControlValues, state snapshot protocol, getStateJson, pixel snapshot, runtime add/remove reflected in live state.
→ HTTP Server — REST endpoint correctness, module add/remove/update lifecycle over HTTP.
→ REST and WebSocket Integration — end-to-end integration: module add, control update, pixel snapshot, WebSocket push verified together.