# REST API `go run . serve` (optional `-port` / `-ble`) starts two HTTP servers on the same `managedHost` (UART or BLE): | Base URL | Flag | Used by | |----------|------|---------| | `http://localhost:8080` | `-addr` (default `:8080`) | Web dashboard + automation on the UI routes | | `http://localhost:8081` | `-api-addr` (default `:8081`, `""` disables) | External programs; subset of routes + service info | WebSocket streaming (accel/tap push): [`API_WEBSOCKET.md`](API_WEBSOCKET.md). All JSON responses use `Content-Type: application/json`. On UART errors many routes return **503** with `"error"` in the body. --- ## External API (`:8081`) ### Service info ```http GET / GET /api/v1/ ``` ```json { "name": "powerpod-external-api", "version": "1", "serial_port": "/dev/ttyUSB0", "transport": "uart", "endpoint": "/dev/ttyUSB0", "connected": true, "websocket": "/ws", "default_interval_ms": 16, "min_interval_ms": 1, "max_interval_ms": 10000, "tap_display_min_ms": 2000, "description": "..." } ``` ### Battery ```http GET /api/battery?all_clients=true GET /api/battery?client_id=16 POST /api/battery Content-Type: application/json ``` POST body: ```json {"all_clients": true} {"client_id": 0} {"client_id": 16} ``` Response: ```json { "success": true, "samples": [ { "client_id": 16, "lipo1": {"valid": true, "voltage_mv": 3850, "percent": 71}, "lipo2": {"valid": false}, "age_ms": 1200 } ] } ``` Slaves push battery to the master every **30 s**; these routes read the master cache. WebSocket equivalent: `get_battery` on `ws://localhost:8081/ws` (reply type `battery_status`). ### LED ring ```http POST /api/led-ring Content-Type: application/json ``` Body: ```json {"mode":"color","client_id":16,"r":255,"g":0,"b":0,"intensity":128} {"mode":"digit","client_id":0,"digit":3,"r":0,"g":255,"b":0} {"mode":"find-me","all_clients":true,"slaves_only":true} ``` | `mode` | Notes | |--------|--------| | `clear` | Turn off | | `color` | Full ring RGB + `intensity` | | `progress` | `progress` 0–100 | | `digit` | `digit` 0–10 | | `blink` | `blink_ms`, `blink_count` | | `find-me` | Locate pod | | `battery-low` | First 4 LEDs red at ~10 % for 5 s (UV indicator test) | Use `client_id` (`0` = master) or `all_clients` (+ optional `slaves_only`) for broadcast. Response: `success`, `slaves_updated`, optional `error`. WebSocket: `set_led_ring` with the same fields plus `"type":"set_led_ring"` → `led_ring_status`. --- ## Dashboard API (`:8080`) Used by the web UI; safe for scripts that drive the same features. ### Host link (UART / BLE) ```http GET /api/host → {"transport":"none|uart|ble","endpoint":"…","connected":false,"ble_name":""} GET /api/serial/ports → {"ports":["/dev/ttyUSB0",…]} POST /api/host/connect Content-Type: application/json {"transport":"uart","port":"/dev/ttyUSB0"} {"transport":"ble","address":"AA:BB:…","name":"Powerpod"} POST /api/host/disconnect POST /api/ble/scan {"timeout_s":5,"name_filter":"Powerpod"} → {"devices":[{"address":"…","name":"Powerpod","rssi":-55}]} ``` `name_filter` is optional substring match (case-insensitive); omit to list all LE advertisements. Active BLE is disconnected before scan (BlueZ adapter). Dashboard WebSocket state includes `host_connected`, `host_transport`, `host_endpoint` (aliases `uart_connected` / `serial_port` kept). **OTA** (`POST /api/ota`) returns **400** unless a host (UART or BLE) is connected. ### BLE latency tests (BLE host only) ```http POST /api/ble-rtt {"count":50,"warmup":5,"settle_s":0} → {"success":true,"count":50,"warmup":5,"stats":{"n":50,"min_ms":…,"median_ms":…,"mean_ms":…,"max_ms":…}} POST /api/ble-stream {"count":50,"period_ms":8,"tol_ms":2,"timeout_s":15,"client_id":0} → {"success":true,"received":50,"requested":50,"window_ok":true,"dt_esp_ms":{…},"dr_pc_ms":{…},…} ``` Pauses dashboard/API polling while running. Requires active BLE host (`DIP8=1`). ### Live stream (host `CACHE_STATUS` poll ~8 ms; BLE uses `BLE_STREAM_TICK`) ```http GET /api/live-stream PUT /api/live-stream Content-Type: application/json {"enable": true} ``` ```json {"enabled": true, "success": true} ``` Enables fast UART polling for dashboard accel/tap display. Per-slave accel still requires accel-stream (below). ### Accel stream (firmware ESP-NOW, per slave) ```http GET /api/clients/16/accel-stream PUT /api/clients/16/accel-stream Content-Type: application/json {"enable": true} ``` ```json {"enabled": true, "client_id": 16, "success": true} ``` All slaves: ```http POST /api/accel-stream Content-Type: application/json {"write": true, "enable": true, "all_clients": true} ``` Polling on the host runs only while at least one slave has streaming enabled (here or via external WebSocket / dashboard). ### Tap notify (firmware; does not start host tap polling) ```http GET /api/clients/16/tap-notify PUT /api/clients/16/tap-notify Content-Type: application/json {"single": true, "double_tap": false, "triple": false} ``` ```json { "client_id": 16, "success": true, "slaves_updated": 1, "single": true, "double_tap": false, "triple": false } ``` All slaves: ```http POST /api/tap-notify Content-Type: application/json {"single": true, "double_tap": false, "triple": false, "all_clients": true} ``` Host tap display / external `set_tap_stream` is separate. ### Tap snapshot (one-shot, via `CACHE_STATUS`) ```http GET /api/tap-snapshot?client_id=16 ``` Reads the combined cache (`CACHE_STATUS`); optional `client_id` filters pending tap events. Pending taps are consumed on read. ```json { "events": [ {"client_id": 16, "kind": "single", "age_ms": 4} ] } ``` ### Deadzone ```http GET /api/deadzone?client_id=0 POST /api/deadzone Content-Type: application/json {"write": true, "deadzone": 128, "client_id": 0} ``` With `all_clients` + `slaves_only`: push to ESP-NOW slaves only (master BMA456 unchanged). ```json {"deadzone": 128, "client_id": 0, "success": true, "slaves_updated": 2} ``` ### Tap config (BMA456 multitap) ```http GET /api/tap-config?client_id=0 POST /api/tap-config Content-Type: application/json { "write": true, "client_id": 0, "tap_sens_thres": 5, "max_gest_dur": 100, "tap_shock_dur": 6, "quite_time_after_gest": 60, "wait_for_timeout": 0, "axis_sel": 2, "enable_single": true, "enable_double": true, "enable_triple": true } ``` Same `client_id` / `all_clients` / `slaves_only` semantics as deadzone. Values are applied to the sensor and persisted in NVS on each target node. ```json { "client_id": 0, "success": true, "slaves_updated": 0, "tap_sens_thres": 5, "max_gest_dur": 100, "tap_shock_dur": 6, "quite_time_after_gest": 60, "wait_for_timeout": 0, "axis_sel": 2, "enable_single": true, "enable_double": true, "enable_triple": true } ``` ### Unicast test ```http POST /api/unicast-test Content-Type: application/json {"client_id": 16, "seq": 42} ``` ### Echo ping (ESP-NOW round-trip latency) ```http POST /api/echo-ping Content-Type: application/json {"client_id": 16} ``` `client_id` must be a registered slave id (`> 0`). The host sends a microsecond timestamp; the master forwards it over ESP-NOW and the slave echoes it back unchanged. **Flow:** Host → UART → `cmd_espnow_echo_ping` → `ESPNOW_ECHO_PING` (with `master_time_us` from `esp_timer_get_time()`) → Slave → `ESPNOW_ECHO_PONG` → Master `recv_cb` → UART response. **Response fields:** | Field | Unit | Meaning | |-------|------|---------| | `success` | — | `true` if pong received within 500 ms | | `client_id` | — | Echo of request | | `timestamp_us` | µs (Unix) | Echoed host timestamp; must match request on success | | `rtt_ms` | ms | **Host-side** round-trip: goTool send → UART response (full chain incl. USB serial) | | `esp_rtt_us` | µs | **Master-side** ESP-NOW only: `esp_timer_get_time()` delta from ping send to pong recv | `esp_rtt_us` is the raw firmware value (microseconds, not converted). The web dashboard displays it converted to milliseconds for readability (`esp_rtt_us / 1000`, 3 decimal places). The success banner stays visible for at least 5 seconds. ```json { "success": true, "client_id": 16, "timestamp_us": 1717654321123456, "rtt_ms": 49.729, "esp_rtt_us": 18234 } ``` On failure (`success: false`), `esp_rtt_us` is omitted; `rtt_ms` still reflects the host round-trip. HTTP 503 if UART exchange fails (e.g. timeout, client not in registry). **CLI:** ```bash go run . -port /dev/ttyUSB0 echo-ping -client 16 ``` Example output: ``` echo ping: success=true client_id=16 rtt_ms=49.729 esp_rtt_us=18234 ``` ### Find me ```http POST /api/find-me Content-Type: application/json {"client_id": 16} ``` `client_id` `0` = master LED ring. ### Restart ```http POST /api/restart Content-Type: application/json {"client_id": 16} ``` ### Log level (master ESP-IDF console) Runtime get/set of the **global** log filter on the master (`esp_log_level_set("*", …)`). Output goes to **UART0** (USB debug, 115200), not the host protocol UART (GPIO 2/3, 921600). ```http GET /api/log-level POST /api/log-level Content-Type: application/json {"write": true, "level": 3} ``` | `level` | Meaning | |---------|---------| | 0 | None (no log output) | | 1 | Error | | 2 | Warn | | 3 | Info | | 4 | Debug | | 5 | Verbose | ```json {"success": true, "level": 3} ``` **CLI:** ```bash go run . -port /dev/ttyUSB0 log-level go run . -port /dev/ttyUSB0 log-level -set -level 3 ``` Dashboard: Master card → dropdown **ESP Log-Level** with **Lesen** / **Setzen**. ### Input send mode (slave accel/tap ESP-NOW TX path) Debug A/B for how slaves transmit `ACCEL_SAMPLE` / `TAP_EVENT`. Default on slaves is `best_effort` (0). Master remembers the last mode for GET; write pushes via ESP-NOW. ```http GET /api/input-send-mode PUT /api/input-send-mode Content-Type: application/json {"mode": 2, "all_clients": true} ``` | `mode` | Path | |--------|------| | 0 | `best_effort` (fire-and-forget, quiet) | | 1 | `fast` (fire-and-forget, logs failures) | | 2 | `wait` (send-done + retries) | | 3 | `reliable` (OTA-style retries; can stall high-rate accel) | ```json {"success": true, "mode": 2, "mode_name": "wait", "slaves_updated": 2} ``` **CLI:** ```bash go run . -port /dev/ttyUSB0 input-send-mode go run . -port /dev/ttyUSB0 input-send-mode -set -mode 2 -all ``` Dashboard: ESP-NOW Slaves toolbar → **Input-Send** dropdown + **Setzen** / **Lesen**. ### OTA (master host upload, UART or BLE) ```http POST /api/ota Content-Type: multipart/form-data ``` Form field **`firmware`**: binary image, max **2 MiB**. Requires connected host (`transport` uart or ble). BLE needs ATT MTU ≥ 219 for 200 B chunks. ```json {"success": true, "bytes_written": 123456, "target_slot": 1} ``` Firmware distributes to slaves over ESP-NOW after `OTA_END`. Progress also appears on dashboard WebSocket as `ota_progress` messages. CLI equivalent: ```bash go run . -port /dev/ttyUSB0 ota build/powerpod.bin go run . -ble ota build/powerpod.bin ``` ### LED ring and battery Same as external API: - `POST /api/led-ring` - `GET` / `POST` `/api/battery` --- ## Dashboard vs external | Feature | Dashboard `:8080` | External `:8081` | |---------|-------------------|------------------| | Client list | Via dashboard WebSocket state / CLI `clients` | WebSocket `list_clients` | | Accel/tap **push stream** | WebSocket state when live-stream on | WebSocket `set_stream` / `set_tap_stream` | | Accel stream enable | REST `PUT .../accel-stream` | WebSocket `set_accel_stream` | | Tap notify | REST `PUT .../tap-notify` | WebSocket `set_tap_notify` | | LED / battery | REST | REST + WebSocket on `:8081` | --- ## UI mapping | UI action | REST / CLI | |-----------|------------| | Nur Master deadzone | `POST /api/deadzone` `client_id: 0` or CLI `deadzone -set -client 0` | | Einzelner Slave | `client_id: ` | | Alle Slaves deadzone | `all_clients` + `slaves_only` on POST | | Master tap config | `GET` / `POST /api/tap-config` `client_id: 0` or CLI `tap-config` | | Tap-Cfg alle Slaves | `POST /api/tap-config` with `all_clients` + `slaves_only` | | Unicast test | `POST /api/unicast-test` | | Echo ping | `POST /api/echo-ping` (per-slave latency; UI shows Host ms + ESP ms) | | Master log level | `GET` / `POST /api/log-level` or CLI `log-level` | | Input-Send mode | `GET` / `PUT /api/input-send-mode` or CLI `input-send-mode` | | Tap notify S/D/T | `PUT /api/clients/{id}/tap-notify` | | Tap receive (UI) | Live stream + tap notify; see WebSocket doc for external API |