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>
209 lines
8.1 KiB
Markdown
209 lines
8.1 KiB
Markdown
# goTool
|
||
|
||
Host-side UART client for the Powerpod **master** ESP.
|
||
|
||
Full system documentation (roles, ESP-NOW, framing, protobuf): [`../main/README.md`](../main/README.md).
|
||
|
||
## Usage
|
||
|
||
```bash
|
||
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](../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.
|
||
|
||
```bash
|
||
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`](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).
|
||
|
||
```bash
|
||
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](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:
|
||
|
||
1. **`set_stream`** — this WebSocket connection: whether to receive `accel` JSON and at what poll rate (1 ms … 10 s per client; server UART poll uses the minimum among active subscribers).
|
||
2. **`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`):
|
||
|
||
```json
|
||
{"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`):
|
||
|
||
```json
|
||
{"type":"set_stream","enable":true,"interval_ms":32}
|
||
{"type":"get_stream"}
|
||
```
|
||
|
||
Reply:
|
||
|
||
```json
|
||
{"type":"stream_status","receive_accel":true,"interval_ms":32,"success":true}
|
||
```
|
||
|
||
**Slave ESP-NOW stream** (per `client_id`):
|
||
|
||
```json
|
||
{"type":"set_accel_stream","client_id":16,"enable":true}
|
||
{"type":"get_accel_stream","client_id":16}
|
||
```
|
||
|
||
Reply:
|
||
|
||
```json
|
||
{"type":"accel_stream_status","client_id":16,"enabled":true,"success":true}
|
||
```
|
||
|
||
**Accel** (only to connections with `receive_accel: true`, and only while slaves stream):
|
||
|
||
```json
|
||
{"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):
|
||
|
||
```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):
|
||
|
||
```http
|
||
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` |
|
||
|
||
```bash
|
||
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.
|
||
|
||
```bash
|
||
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`):
|
||
|
||
```bash
|
||
make gotool-proto # Go: goTool/pb/uart_messages.pb.go
|
||
make proto_generate # C: main/proto/*.pb.h, *.pb.c
|
||
```
|