Skip to content

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

WebSocketgetControlValues, 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.