Add BMA456 tap detection with ESP-NOW notify and host snapshot API.
Slaves forward configured tap kinds to the master; goTool exposes CLI, dashboard, REST, and WebSocket with separate notify vs receive and 2s display cache. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+111
-7
@@ -26,6 +26,8 @@ go run . -port /dev/ttyUSB0 clients
|
||||
| `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` |
|
||||
| `tap-notify` | `0x1b` | Get/set which tap kinds (single/double/triple) notify via ESP-NOW (`-set`, `-client`, `-all`, `-single`, `-double`, `-triple`) |
|
||||
| `tap` | `0x1c` | Cached tap snapshot from master (`TAP_SNAPSHOT`); events ≤16 ms old |
|
||||
| `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) |
|
||||
@@ -68,26 +70,42 @@ 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`.
|
||||
|
||||
**Tap (dashboard):** two independent controls per slave:
|
||||
|
||||
| Column | Meaning |
|
||||
|--------|---------|
|
||||
| Tap-Notify (S/D/T) | Which tap kinds the **slave** sends to the master over ESP-NOW (UART `TAP_NOTIFY`) — does **not** poll UART |
|
||||
| Tap (An/Aus) | Host **receive**: poll master tap cache (~16 ms) and show last tap for **≥2 s** |
|
||||
|
||||
Enable notify first, then turn receive on to see events. Same split as the external WebSocket API (`set_tap_notify` vs `set_tap_stream`).
|
||||
|
||||
### 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 |
|
||||
| `GET /` or `GET /api/v1/` | JSON service info (`default_interval_ms`, min/max, `serial_port`, `tap_display_min_ms`) |
|
||||
| `WebSocket /ws` | Per-connection accel/tap receive + interval; slave ESP-NOW accel/tap control |
|
||||
|
||||
Two layers:
|
||||
**Accel** — 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`).
|
||||
Accel 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`):
|
||||
**Tap** — also two layers (notify alone does **not** poll UART):
|
||||
|
||||
1. **`set_tap_notify`** — firmware: which tap kinds (single/double/triple) the slave sends to the master over ESP-NOW.
|
||||
2. **`set_tap_stream`** — this WebSocket connection: poll `TAP_SNAPSHOT` and push `tap` JSON. Events stay visible for **`tap_display_min_ms`** (2000 ms) after first sight.
|
||||
|
||||
Tap polling runs only when at least one connection has `receive_tap: true` (via `set_tap_stream`).
|
||||
|
||||
**Hello** (on connect; accel/tap receive off until `set_stream` / `set_tap_stream`):
|
||||
|
||||
```json
|
||||
{"type":"hello","serial_port":"/dev/ttyUSB0","interval_ms":16,"commands":["set_stream","get_stream","set_accel_stream","get_accel_stream","set_led_ring","get_battery"]}
|
||||
{"type":"hello","serial_port":"/dev/ttyUSB0","interval_ms":16,"tap_display_min_ms":2000,"note":"set_tap_notify configures slave S/D/T only; set_tap_stream enables tap polling/push","commands":["set_stream","get_stream","set_accel_stream","get_accel_stream","set_tap_stream","get_tap_stream","set_tap_notify","get_tap_notify","set_led_ring","get_battery"]}
|
||||
```
|
||||
|
||||
**Receive accel on this connection** (optional `interval_ms`, default from `-accel-interval`):
|
||||
@@ -125,6 +143,38 @@ Reply:
|
||||
|
||||
Reply: `{"type":"led_ring_status","success":true,"slaves_updated":2,...}`
|
||||
|
||||
**Tap notify** (slave ESP-NOW config; per `client_id`, or `all_clients`):
|
||||
|
||||
```json
|
||||
{"type":"set_tap_notify","client_id":16,"single":true,"double_tap":false,"triple":false}
|
||||
{"type":"get_tap_notify","client_id":16}
|
||||
```
|
||||
|
||||
Reply:
|
||||
|
||||
```json
|
||||
{"type":"tap_notify_status","client_id":16,"success":true,"single":true,"double_tap":false,"triple":false}
|
||||
```
|
||||
|
||||
**Receive tap on this connection** (optional `interval_ms`; default from `-accel-interval`):
|
||||
|
||||
```json
|
||||
{"type":"set_tap_stream","enable":true,"interval_ms":16}
|
||||
{"type":"get_tap_stream"}
|
||||
```
|
||||
|
||||
Reply:
|
||||
|
||||
```json
|
||||
{"type":"tap_stream_status","receive_tap":true,"interval_ms":16,"success":true}
|
||||
```
|
||||
|
||||
**Tap events** (only to connections with `receive_tap: true`; each event shown ≥2 s):
|
||||
|
||||
```json
|
||||
{"type":"tap","port":"/dev/ttyUSB0","success":true,"events":[{"client_id":16,"kind":"single","age_ms":3,"shown_at_ms":1717000000123}]}
|
||||
```
|
||||
|
||||
**Accel** (only to connections with `receive_accel: true`, and only while slaves stream):
|
||||
|
||||
```json
|
||||
@@ -156,6 +206,28 @@ async def main():
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
Tap example (notify first, then enable stream on this connection):
|
||||
|
||||
```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_tap_notify", "client_id": 16,
|
||||
"single": True, "double_tap": False, "triple": False}))
|
||||
print(await ws.recv()) # tap_notify_status
|
||||
await ws.send(json.dumps({"type": "set_tap_stream", "enable": True, "interval_ms": 16}))
|
||||
print(await ws.recv()) # tap_stream_status
|
||||
while True:
|
||||
msg = json.loads(await ws.recv())
|
||||
if msg.get("type") == "tap" and msg.get("events"):
|
||||
for e in msg["events"]:
|
||||
print(e["client_id"], e["kind"], "age", e.get("age_ms"))
|
||||
|
||||
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:
|
||||
@@ -167,7 +239,7 @@ The dashboard can configure nodes using the same UART commands as the CLI:
|
||||
| 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`), `GET/POST /api/battery`, `POST /api/led-ring`, `POST /api/unicast-test`, `POST /api/find-me`, `POST /api/restart`, `POST /api/ota` (multipart field `firmware`, max 2 MiB).
|
||||
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`), `GET/PUT /api/clients/{id}/tap-notify`, `PUT /api/clients/{id}/tap-receive`, `GET/POST /api/tap-notify`, `GET /api/tap-snapshot`, `GET/POST /api/battery`, `POST /api/led-ring`, `POST /api/unicast-test`, `POST /api/find-me`, `POST /api/restart`, `POST /api/ota` (multipart field `firmware`, max 2 MiB).
|
||||
|
||||
**LED ring** (`POST /api/led-ring` and WebSocket `set_led_ring` on `:8081`):
|
||||
|
||||
@@ -203,6 +275,38 @@ Content-Type: application/json
|
||||
|
||||
Enable all slaves: `POST /api/accel-stream` with `{"write":true,"enable":true,"all_clients":true}`.
|
||||
|
||||
**Tap notify per slave** (slave → master ESP-NOW; does not start host polling):
|
||||
|
||||
```http
|
||||
GET /api/clients/16/tap-notify
|
||||
→ {"client_id":16,"success":true,"single":false,"double_tap":false,"triple":false}
|
||||
|
||||
PUT /api/clients/16/tap-notify
|
||||
Content-Type: application/json
|
||||
{"single": true, "double_tap": false, "triple": false}
|
||||
→ {"client_id":16,"success":true,"slaves_updated":1,"single":true,"double_tap":false,"triple":false}
|
||||
```
|
||||
|
||||
All slaves: `POST /api/tap-notify` with `{"single":true,"double_tap":false,"triple":false,"all_clients":true}`.
|
||||
|
||||
**Tap receive** (host-side; dashboard polls `TAP_SNAPSHOT` while enabled):
|
||||
|
||||
```http
|
||||
PUT /api/clients/16/tap-receive
|
||||
Content-Type: application/json
|
||||
{"enable": true}
|
||||
→ {"client_id":16,"enabled":true,"success":true}
|
||||
```
|
||||
|
||||
One-shot read (no receive flag): `GET /api/tap-snapshot?client_id=16` → `{"events":[{"client_id":16,"kind":"single","age_ms":4}]}`.
|
||||
|
||||
CLI:
|
||||
|
||||
```bash
|
||||
go run . -port /dev/ttyUSB0 tap-notify -client 16 -set -single
|
||||
go run . -port /dev/ttyUSB0 tap -client 16
|
||||
```
|
||||
|
||||
| UI / API | Behaviour |
|
||||
|----------|-----------|
|
||||
| Firmware OTA card | Same as `ota` CLI; WebSocket `ota_progress` with `step` `master` (UART) then `slaves` (ESP-NOW) |
|
||||
|
||||
Reference in New Issue
Block a user