Slaves push BMA456 samples at 16ms when enabled; the master caches per client and exposes ACCEL_SNAPSHOT and ACCEL_STREAM over UART. goTool adds dashboard stream controls, HTTP accel-stream routes, and an external WebSocket API with per-connection receive/interval and slave stream commands. Co-authored-by: Cursor <cursoragent@cursor.com>
goTool
Host-side UART client for the Powerpod master ESP.
Full system documentation (roles, ESP-NOW, framing, protobuf): ../main/README.md.
Usage
cd goTool
go mod tidy
go run . -port /dev/ttyUSB0 version
go run . -port /dev/ttyUSB0 clients
| Flag | Default | Description |
|---|---|---|
-port |
(required) | Serial port on master UART (GPIO2/3 adapter) |
-baud |
921600 |
Must match firmware UART_BAUD_RATE |
Commands
| Command | UART payload | Description |
|---|---|---|
version |
0x03 |
Prints version and git_hash from firmware |
clients |
0x04 |
Lists slaves registered on the master via ESP-NOW |
deadzone |
0x06 |
Get/set accelerometer deadzone LSB (-set, -value, -client, -all) |
accel |
0x18 |
Cached slave accel snapshot from master (ACCEL_SNAPSHOT); alias accel-read |
unicast-test |
0x07 |
Sends ESP-NOW unicast test to one slave (-client, -seq) |
test |
— | Run an automated scenario (JSON configs under testdata/) |
serve |
— | Web dashboard at http://localhost:8080 (WebSocket live updates) |
ota |
16–19 | UART firmware upload to master; firmware then pushes to slaves via ESP-NOW |
ota-progress |
21 | Query per-slave ESP-NOW OTA progress on the master (-client N, default all) |
led-ring |
8 | LED ring: -mode clear|progress|digit|blink|find-me, … |
find-me |
22 | Locate pod (-client 0 master, >0 slave via ESP-NOW) |
restart |
23 | Reboot master or slave (-client 0 / >0) |
clients requires slaves to have responded to master discover broadcasts first.
For adding commands end-to-end (UART, ESP-NOW, CLI, dashboard), see docs/adding-a-feature.md (Find me example).
Automated tests
Bench configs (testdata/configs/) list network, MACs, and serial ports (uart.master for commands, *_console for esptool reset). Scenarios run UART commands plus optional reset steps.
go run . test -list-configs
go run . test -list-scenarios
go run . test -config example-lab -scenario smoke
go run . test -config example-lab -scenario uart_cmds
go run . test -config my-lab -scenario smoke -port /dev/ttyUSB1 -v
With a complete bench config, -port is optional for test (uses uart.master from JSON).
See testdata/README.md for the JSON schema.
Web dashboard
Polls the master over UART and pushes state to the browser via WebSocket (Alpine.js + Bootstrap 5).
go run . -port /dev/ttyUSB0 serve
go run . -port /dev/ttyUSB0 serve -addr :8080 -interval 2s
go run . -port /dev/ttyUSB0 serve -api-addr :8081 -accel-interval 16ms
make gotool-serve PORT=/dev/ttyUSB0
Open http://localhost:8080 — shows master firmware info and the ESP-NOW client table from CLIENT_INFO.
External API (second HTTP server)
serve starts a separate listener (default :8081, disable with -api-addr "") for external programs. It shares the same UART connection as the dashboard.
| Endpoint | Description |
|---|---|
GET / or GET /api/v1/ |
JSON service info (default_interval_ms, min/max, serial_port) |
WebSocket /ws |
Per-connection accel receive + interval; slave ESP-NOW stream control |
Two layers:
set_stream— this WebSocket connection: whether to receiveaccelJSON and at what poll rate (1 ms … 10 s per client; server UART poll uses the minimum among active subscribers).set_accel_stream— firmware: whether a slave sends accel to the master over ESP-NOW (16 ms on the pod).
Polling runs only when at least one connection has receive_accel: true and at least one slave streams (via set_accel_stream or dashboard :8080).
Hello (on connect; accel is off until set_stream):
{"type":"hello","serial_port":"/dev/ttyUSB0","interval_ms":16,"commands":["set_stream","get_stream","set_accel_stream","get_accel_stream"]}
Receive accel on this connection (optional interval_ms, default from -accel-interval):
{"type":"set_stream","enable":true,"interval_ms":32}
{"type":"get_stream"}
Reply:
{"type":"stream_status","receive_accel":true,"interval_ms":32,"success":true}
Slave ESP-NOW stream (per client_id):
{"type":"set_accel_stream","client_id":16,"enable":true}
{"type":"get_accel_stream","client_id":16}
Reply:
{"type":"accel_stream_status","client_id":16,"enabled":true,"success":true}
Accel (only to connections with receive_accel: true, and only while slaves stream):
{"type":"accel","t":1716900123456789012,"success":true,"clients":[{"client_id":16,"valid":true,"x":12,"y":-34,"z":16384,"age_ms":8}]}
t is Unix time in nanoseconds. Each clients[] entry is one slave's latest cached sample (raw LSB, ±2g).
Example (Python):
import asyncio, json, websockets
async def main():
async with websockets.connect("ws://127.0.0.1:8081/ws") as ws:
print(await ws.recv()) # hello
await ws.send(json.dumps({"type": "set_stream", "enable": True, "interval_ms": 16}))
print(await ws.recv()) # stream_status
await ws.send(json.dumps({"type": "set_accel_stream", "client_id": 16, "enable": True}))
print(await ws.recv()) # accel_stream_status
while True:
msg = json.loads(await ws.recv())
if msg.get("type") != "accel" or not msg.get("success"):
continue
for c in msg.get("clients", []):
if c.get("valid"):
print(c["client_id"], c["x"], c["y"], c["z"])
asyncio.run(main())
If the UART device is unplugged or the port disappears, serve keeps running and retries on each poll interval; the UI shows UART off until the port is available again.
The dashboard can configure nodes using the same UART commands as the CLI:
| UI action | CLI equivalent |
|---|---|
| Nur Master | deadzone -set -value N -client 0 |
| Einzelner Slave | deadzone -set -value N -client ID |
| Alle Slaves | per-slave ESP-NOW (Master bleibt unverändert; CLI -all setzt auch den Master) |
| Unicast test | unicast-test -client ID |
HTTP API (used by the web UI): GET/POST /api/deadzone, GET/PUT /api/clients/{id}/accel-stream, POST /api/accel-stream (legacy / all_clients), POST /api/unicast-test, POST /api/find-me, POST /api/restart, POST /api/ota (multipart field firmware, max 2 MiB).
Accel stream per slave (must be enabled before values appear; goTool polls only while at least one slave has stream on):
GET /api/clients/16/accel-stream
→ {"enabled":false,"client_id":16,"success":true}
PUT /api/clients/16/accel-stream
Content-Type: application/json
{"enable": true}
→ {"enabled":true,"client_id":16,"success":true}
Enable all slaves: POST /api/accel-stream with {"write":true,"enable":true,"all_clients":true}.
| UI / API | Behaviour |
|---|---|
| Firmware OTA card | Same as ota CLI; WebSocket ota_progress with step master (UART) then slaves (ESP-NOW) |
POST /api/ota |
Upload .bin to master only — slaves are updated by firmware over ESP-NOW after OTA_END |
go run . -port /dev/ttyUSB0 ota build/powerpod.bin
Waits for ready after start (~30 s erase), sends 200-byte OTA_PAYLOAD frames, reads block_ack every 4 KiB, then OTA_END. The master then distributes to all available slaves (no extra host traffic); success is reported only when that finishes. Allow several minutes for large images. Reboot master and slaves to boot the new firmware.
go run . -port /dev/ttyUSB0 unicast-test -client 16 -seq 42
On success the slave serial log should show UNICAST TEST OK from master … seq=42.
Example output:
clients (2):
[0] id=42 mac=aabbccddeeff ver=1 available=true used=false last_ping=250 last_success_ping=250
Regenerate protobuf
From repo root (needs protoc, protoc-gen-go, and for C also pip install protobuf):
make gotool-proto # Go: goTool/pb/uart_messages.pb.go
make proto_generate # C: main/proto/*.pb.h, *.pb.c