Unify cache polling on CACHE_STATUS and split API docs.

Replace separate accel/tap snapshot UART commands with one clients[] response
that omits unsubscribed fields; remove snapshot handlers and CLI commands.
Add goTool/docs for WebSocket streams and REST; tap-snapshot REST uses CACHE_STATUS.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
simon 2026-05-29 21:23:09 +02:00
parent a85d48320e
commit 31e539052a
25 changed files with 1243 additions and 1269 deletions

View File

@ -25,10 +25,8 @@ go run . -port /dev/ttyUSB0 clients
| `version` | `0x03` | Prints `version` and `git_hash` from firmware | | `version` | `0x03` | Prints `version` and `git_hash` from firmware |
| `clients` | `0x04` | Lists slaves registered on the master via ESP-NOW | | `clients` | `0x04` | Lists slaves registered on the master via ESP-NOW |
| `deadzone` | `0x06` | Get/set accelerometer deadzone LSB (`-set`, `-value`, `-client`, `-all`) | | `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-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 | | `cache-status` | `0x1d` | Subscribed accel + tap cache (`CACHE_STATUS`); one UART round-trip for 16 ms polling |
| `cache-status` | `0x1d` | Combined accel + tap cache (`CACHE_STATUS`); one UART round-trip for 16 ms polling |
| `unicast-test` | `0x07` | Sends ESP-NOW unicast test to one slave (`-client`, `-seq`) | | `unicast-test` | `0x07` | Sends ESP-NOW unicast test to one slave (`-client`, `-seq`) |
| `test` | — | Run an automated scenario (JSON configs under `testdata/`) | | `test` | — | Run an automated scenario (JSON configs under `testdata/`) |
| `serve` | — | Web dashboard at `http://localhost:8080` (WebSocket live updates) | | `serve` | — | Web dashboard at `http://localhost:8080` (WebSocket live updates) |
@ -80,257 +78,28 @@ Open [http://localhost:8080](http://localhost:8080) — shows master firmware in
Enable notify first, then turn receive on to see events. Same split as the external WebSocket API (`set_tap_notify` vs `set_tap_stream`). 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`, `tap_display_min_ms`) |
| `WebSocket /ws` | Per-connection accel/tap receive + interval; slave ESP-NOW accel/tap control |
**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).
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`).
**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 `CACHE_STATUS` (tap slice) 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,"tap_display_min_ms":2000,"note":"set_tap_notify configures slave S/D/T only; set_tap_stream enables tap polling/push","commands":["list_clients","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"]}
```
**List registered slaves** (UART `CLIENT_INFO`; use before per-slave `set_accel_stream` / `set_tap_notify`):
```json
{"type":"list_clients"}
```
Reply:
```json
{"type":"client_list","success":true,"clients":[{"id":16,"mac":"aa:bb:cc:dd:ee:10","version":1,"available":true,"used":true,"last_ping":1234,"last_success_ping":1200,"accel_stream":false,"tap_notify_single":false,"tap_notify_double":false,"tap_notify_triple":false}]}
```
**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}
```
**LED ring** (same JSON fields as `POST /api/led-ring`):
```json
{"type":"set_led_ring","mode":"color","client_id":16,"r":255,"g":0,"b":0,"intensity":200}
{"type":"set_led_ring","mode":"digit","all_clients":true,"slaves_only":true,"digit":3,"g":255}
```
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
{"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": "list_clients"}))
clients = json.loads(await ws.recv())["clients"]
for c in clients:
if not c.get("available"):
continue
await ws.send(json.dumps({"type": "set_accel_stream", "client_id": c["id"], "enable": True}))
print(await ws.recv()) # accel_stream_status
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())
```
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. 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: ### HTTP / WebSocket API
| UI action | CLI equivalent | `serve` also listens on **`:8081`** for external programs (`-api-addr`, empty to disable). Same UART as the dashboard.
|-----------|------------------|
| 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/PUT /api/live-stream`, `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`, `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). | Doc | Content |
|-----|---------|
**LED ring** (`POST /api/led-ring` and WebSocket `set_led_ring` on `:8081`): | **[docs/API_WEBSOCKET.md](docs/API_WEBSOCKET.md)** | `ws://…:8081/ws` commands, **`accel` / `tap` push stream** format, dashboard `ws://…:8080/ws` |
| **[docs/API_REST.md](docs/API_REST.md)** | REST on `:8080` (dashboard) and `:8081` (battery, LED, service info) |
```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}
```
Modes: `clear`, `color` (full ring), `progress` (0100), `digit` (010 symbols), `blink`, `find-me`. Use `client_id` (0 = master), or `all_clients` (+ optional `slaves_only`) for broadcast.
**Battery** (`GET/POST /api/battery`, WebSocket `get_battery` on `:8081`):
```json
{"all_clients":true}
{"client_id":0}
{"client_id":16}
```
Response: `samples[]` with `client_id`, `lipo1`/`lipo2` (`valid`, `voltage_mv`, `percent`), `age_ms`. Slaves push to the master every **30 s**; UART reads the cache (fast). Dashboard polls with `all_clients`.
**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}`.
**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}`.
**Live stream** (dashboard: host-side `CACHE_STATUS` poll ~16 ms; per-slave accel via `accel-stream`):
```http
PUT /api/live-stream
Content-Type: application/json
{"enable": true}
→ {"enabled":true,"success":true}
```
One-shot read: `GET /api/tap-snapshot?client_id=16``{"events":[{"client_id":16,"kind":"single","age_ms":4}]}`.
CLI: CLI:
```bash ```bash
go run . -port /dev/ttyUSB0 tap-notify -client 16 -set -single go run . -port /dev/ttyUSB0 tap-notify -client 16 -set -single
go run . -port /dev/ttyUSB0 tap -client 16 go run . -port /dev/ttyUSB0 cache-status
``` ```
| UI / API | Behaviour | | UI / API | Behaviour |
|----------|-----------| |----------|-----------|
| Firmware OTA card | Same as `ota` CLI; WebSocket `ota_progress` with `step` `master` (UART) then `slaves` (ESP-NOW) | | Firmware OTA card | Same as `ota` CLI; dashboard WebSocket `ota_progress` ([REST doc](docs/API_REST.md)) |
| `POST /api/ota` | Upload `.bin` to master only — slaves are updated by firmware over ESP-NOW after `OTA_END` | | `POST /api/ota` | Upload `.bin` to master — slaves updated by firmware over ESP-NOW after `OTA_END` |
```bash ```bash
go run . -port /dev/ttyUSB0 ota build/powerpod.bin go run . -port /dev/ttyUSB0 ota build/powerpod.bin

View File

@ -506,8 +506,9 @@ func runAccelStreamer(link *managedSerial, hub *accelStreamHub, dash *wsHub, ctl
} }
if wantAccel { if wantAccel {
clients := make([]AccelClientSample, 0, len(cache.GetAccel())) samples := accelSamplesFromCacheStatus(cache)
for _, s := range cache.GetAccel() { clients := make([]AccelClientSample, 0, len(samples))
for _, s := range samples {
clients = append(clients, AccelClientSample{ clients = append(clients, AccelClientSample{
ClientID: s.GetClientId(), ClientID: s.GetClientId(),
Valid: s.GetValid(), Valid: s.GetValid(),
@ -525,8 +526,9 @@ func runAccelStreamer(link *managedSerial, hub *accelStreamHub, dash *wsHub, ctl
}) })
} }
if wantTap { if wantTap {
fresh := make([]TapClientEvent, 0, len(cache.GetTaps())) events := tapEventsFromCacheStatus(cache)
for _, e := range cache.GetTaps() { fresh := make([]TapClientEvent, 0, len(events))
for _, e := range events {
if !e.GetValid() { if !e.GetValid() {
continue continue
} }
@ -537,13 +539,13 @@ func runAccelStreamer(link *managedSerial, hub *accelStreamHub, dash *wsHub, ctl
AgeMs: e.GetAgeMs(), AgeMs: e.GetAgeMs(),
}) })
} }
events := hub.ingestTapEvents(fresh) visible := hub.ingestTapEvents(fresh)
if len(events) > 0 { if len(visible) > 0 {
hub.deliverTap(TapStreamMessage{ hub.deliverTap(TapStreamMessage{
Type: "tap", Type: "tap",
T: now, T: now,
Success: true, Success: true,
Events: events, Events: visible,
}) })
} }
} }

View File

@ -199,14 +199,14 @@ func serveTapSnapshotGet(w http.ResponseWriter, r *http.Request, link *managedSe
writeJSON(w, http.StatusBadRequest, tapSnapshotAPIResponse{Error: err.Error()}) writeJSON(w, http.StatusBadRequest, tapSnapshotAPIResponse{Error: err.Error()})
return return
} }
resp, err := link.readTapSnapshotPoll(clientID) cache, err := link.readCacheStatusPoll()
if err != nil { if err != nil {
writeJSON(w, http.StatusServiceUnavailable, tapSnapshotAPIResponse{Error: err.Error()}) writeJSON(w, http.StatusServiceUnavailable, tapSnapshotAPIResponse{Error: err.Error()})
return return
} }
out := tapSnapshotAPIResponse{Events: make([]tapEventView, 0, len(resp.GetEvents()))} out := tapSnapshotAPIResponse{Events: make([]tapEventView, 0)}
for _, e := range resp.GetEvents() { for _, e := range tapEventsFromCacheStatus(cache) {
if !e.GetValid() { if clientID != 0 && e.GetClientId() != clientID {
continue continue
} }
out.Events = append(out.Events, tapEventView{ out.Events = append(out.Events, tapEventView{

49
goTool/cache_status.go Normal file
View File

@ -0,0 +1,49 @@
package main
import "powerpod/gotool/pb"
// accelSamplesFromCacheStatus maps combined CACHE_STATUS entries to AccelSample
// (for dashboard / WebSocket accel push).
func accelSamplesFromCacheStatus(r *pb.CacheStatusResponse) []*pb.AccelSample {
if r == nil {
return nil
}
out := make([]*pb.AccelSample, 0, len(r.GetClients()))
for _, c := range r.GetClients() {
if c.GetAccel() == nil {
continue
}
a := c.GetAccel()
out = append(out, &pb.AccelSample{
ClientId: c.GetClientId(),
Valid: a.GetValid(),
X: a.GetX(),
Y: a.GetY(),
Z: a.GetZ(),
AgeMs: a.GetAgeMs(),
})
}
return out
}
// tapEventsFromCacheStatus maps combined CACHE_STATUS entries to TapEvent
// (only clients with a consumed pending tap).
func tapEventsFromCacheStatus(r *pb.CacheStatusResponse) []*pb.TapEvent {
if r == nil {
return nil
}
out := make([]*pb.TapEvent, 0, len(r.GetClients()))
for _, c := range r.GetClients() {
if c.GetTap() == nil {
continue
}
t := c.GetTap()
out = append(out, &pb.TapEvent{
ClientId: c.GetClientId(),
Valid: true,
Kind: t.GetKind(),
AgeMs: t.GetAgeMs(),
})
}
return out
}

View File

@ -40,25 +40,6 @@ func (m *managedSerial) listClientsPoll() ([]*pb.ClientInfo, error) {
return decodeClientsPayload(payload) return decodeClientsPayload(payload)
} }
func (m *managedSerial) readAccelSnapshotPoll(clientID uint32) (*pb.AccelSnapshotResponse, error) {
msg := &pb.UartMessage{
Type: pb.MessageType_ACCEL_SNAPSHOT,
Payload: &pb.UartMessage_AccelSnapshotRequest{
AccelSnapshotRequest: &pb.AccelSnapshotRequest{ClientId: clientID},
},
}
body, err := proto.Marshal(msg)
if err != nil {
return nil, fmt.Errorf("encode: %w", err)
}
payload := append([]byte{byte(pb.MessageType_ACCEL_SNAPSHOT)}, body...)
respPayload, err := m.exchangePayloadPoll(payload, "ACCEL_SNAPSHOT")
if err != nil {
return nil, err
}
return decodeAccelSnapshotPayload(respPayload)
}
func decodeBatteryStatusPayload(payload []byte) (*pb.BatteryStatusResponse, error) { func decodeBatteryStatusPayload(payload []byte) (*pb.BatteryStatusResponse, error) {
if len(payload) < 2 { if len(payload) < 2 {
return nil, fmt.Errorf("short battery response") return nil, fmt.Errorf("short battery response")
@ -222,43 +203,6 @@ func decodeCacheStatusPayload(payload []byte) (*pb.CacheStatusResponse, error) {
return r, nil return r, nil
} }
func (m *managedSerial) readTapSnapshotPoll(clientID uint32) (*pb.TapSnapshotResponse, error) {
msg := &pb.UartMessage{
Type: pb.MessageType_TAP_SNAPSHOT,
Payload: &pb.UartMessage_TapSnapshotRequest{
TapSnapshotRequest: &pb.TapSnapshotRequest{ClientId: clientID},
},
}
body, err := proto.Marshal(msg)
if err != nil {
return nil, fmt.Errorf("encode: %w", err)
}
payload := append([]byte{byte(pb.MessageType_TAP_SNAPSHOT)}, body...)
respPayload, err := m.exchangePayloadPoll(payload, "TAP_SNAPSHOT")
if err != nil {
return nil, err
}
return decodeTapSnapshotPayload(respPayload)
}
func decodeTapSnapshotPayload(payload []byte) (*pb.TapSnapshotResponse, error) {
if len(payload) < 1 {
return nil, fmt.Errorf("empty response payload")
}
var msg pb.UartMessage
if err := proto.Unmarshal(payload[1:], &msg); err != nil {
return nil, fmt.Errorf("decode: %w", err)
}
if msg.GetType() != pb.MessageType_TAP_SNAPSHOT {
return nil, fmt.Errorf("unexpected type %v", msg.GetType())
}
r := msg.GetTapSnapshotResponse()
if r == nil {
return nil, fmt.Errorf("missing tap_snapshot_response")
}
return r, nil
}
func (m *managedSerial) accelStreamVia( func (m *managedSerial) accelStreamVia(
portFn func(func(*serialPort) error) error, portFn func(func(*serialPort) error) error,
req *pb.AccelStreamRequest, req *pb.AccelStreamRequest,
@ -333,43 +277,6 @@ func decodeClientsPayload(payload []byte) ([]*pb.ClientInfo, error) {
return info.GetClients(), nil return info.GetClients(), nil
} }
func decodeAccelSnapshotPayload(payload []byte) (*pb.AccelSnapshotResponse, error) {
if len(payload) < 1 {
return nil, fmt.Errorf("empty response payload")
}
var msg pb.UartMessage
if err := proto.Unmarshal(payload[1:], &msg); err != nil {
return nil, fmt.Errorf("decode: %w", err)
}
if msg.GetType() != pb.MessageType_ACCEL_SNAPSHOT {
return nil, fmt.Errorf("unexpected type %v", msg.GetType())
}
r := msg.GetAccelSnapshotResponse()
if r == nil {
return nil, fmt.Errorf("missing accel_snapshot_response")
}
return r, nil
}
func (s *serialPort) readAccelSnapshot(clientID uint32) (*pb.AccelSnapshotResponse, error) {
msg := &pb.UartMessage{
Type: pb.MessageType_ACCEL_SNAPSHOT,
Payload: &pb.UartMessage_AccelSnapshotRequest{
AccelSnapshotRequest: &pb.AccelSnapshotRequest{ClientId: clientID},
},
}
body, err := proto.Marshal(msg)
if err != nil {
return nil, fmt.Errorf("encode: %w", err)
}
payload := append([]byte{byte(pb.MessageType_ACCEL_SNAPSHOT)}, body...)
respPayload, err := s.exchangePayload(payload, "ACCEL_SNAPSHOT")
if err != nil {
return nil, err
}
return decodeAccelSnapshotPayload(respPayload)
}
func (s *serialPort) getVersion() (*pb.VersionResponse, error) { func (s *serialPort) getVersion() (*pb.VersionResponse, error) {
payload, err := s.exchange(byte(pb.MessageType_VERSION), "VERSION") payload, err := s.exchange(byte(pb.MessageType_VERSION), "VERSION")
if err != nil { if err != nil {
@ -448,25 +355,6 @@ func (s *serialPort) readCacheStatus() (*pb.CacheStatusResponse, error) {
return decodeCacheStatusPayload(payload) return decodeCacheStatusPayload(payload)
} }
func (s *serialPort) readTapSnapshot(clientID uint32) (*pb.TapSnapshotResponse, error) {
msg := &pb.UartMessage{
Type: pb.MessageType_TAP_SNAPSHOT,
Payload: &pb.UartMessage_TapSnapshotRequest{
TapSnapshotRequest: &pb.TapSnapshotRequest{ClientId: clientID},
},
}
body, err := proto.Marshal(msg)
if err != nil {
return nil, fmt.Errorf("encode: %w", err)
}
payload := append([]byte{byte(pb.MessageType_TAP_SNAPSHOT)}, body...)
respPayload, err := s.exchangePayload(payload, "TAP_SNAPSHOT")
if err != nil {
return nil, err
}
return decodeTapSnapshotPayload(respPayload)
}
func (s *serialPort) accelDeadzone(req *pb.AccelDeadzoneRequest) (*pb.AccelDeadzoneResponse, error) { func (s *serialPort) accelDeadzone(req *pb.AccelDeadzoneRequest) (*pb.AccelDeadzoneResponse, error) {
msg := &pb.UartMessage{ msg := &pb.UartMessage{
Type: pb.MessageType_ACCEL_DEADZONE, Type: pb.MessageType_ACCEL_DEADZONE,

View File

@ -1,30 +0,0 @@
package main
import (
"fmt"
)
func runAccel(sp *serialPort) error {
return runAccelSnapshot(sp, 0)
}
func runAccelSnapshot(sp *serialPort, clientID uint32) error {
r, err := sp.readAccelSnapshot(clientID)
if err != nil {
return err
}
samples := r.GetSamples()
if len(samples) == 0 {
fmt.Println("no accel samples (no slaves or no ESP-NOW stream yet)")
return nil
}
for _, s := range samples {
if !s.GetValid() {
fmt.Printf("client %d: no sample yet\n", s.GetClientId())
continue
}
fmt.Printf("client %d: x=%d y=%d z=%d (age %d ms, raw LSB ±2g)\n",
s.GetClientId(), s.GetX(), s.GetY(), s.GetZ(), s.GetAgeMs())
}
return nil
}

View File

@ -9,26 +9,24 @@ func runCacheStatus(sp *serialPort) error {
if err != nil { if err != nil {
return err return err
} }
accel := r.GetAccel() clients := r.GetClients()
if len(accel) == 0 { if len(clients) == 0 {
fmt.Println("accel: (none — no slaves with accel stream enabled)") fmt.Println("(no slaves with accel stream or tap notify enabled)")
return nil
}
for _, c := range clients {
id := c.GetClientId()
if a := c.GetAccel(); a != nil {
if !a.GetValid() {
fmt.Printf("client %d accel: no sample yet\n", id)
} else { } else {
for _, s := range accel { fmt.Printf("client %d accel: x=%d y=%d z=%d (age %d ms)\n",
if !s.GetValid() { id, a.GetX(), a.GetY(), a.GetZ(), a.GetAgeMs())
fmt.Printf("accel client %d: no sample yet\n", s.GetClientId())
continue
}
fmt.Printf("accel client %d: x=%d y=%d z=%d (age %d ms)\n",
s.GetClientId(), s.GetX(), s.GetY(), s.GetZ(), s.GetAgeMs())
} }
} }
taps := r.GetTaps() if t := c.GetTap(); t != nil {
if len(taps) == 0 { fmt.Printf("client %d tap: %s (age %d ms)\n",
fmt.Println("tap: (none pending)") id, tapKindLabel(t.GetKind()), t.GetAgeMs())
} else {
for _, e := range taps {
fmt.Printf("tap client %d: %s (age %d ms)\n",
e.GetClientId(), tapKindLabel(e.GetKind()), e.GetAgeMs())
} }
} }
return nil return nil

View File

@ -44,32 +44,6 @@ func runTapNotify(sp *serialPort, args []string) error {
return nil return nil
} }
func runTapSnapshot(sp *serialPort, args []string) error {
fs := flag.NewFlagSet("tap", flag.ExitOnError)
clientID := fs.Uint("client", 0, "client id (0 = all slaves with tap notify)")
if err := fs.Parse(args); err != nil {
return err
}
return runTapSnapshotForClient(sp, uint32(*clientID))
}
func runTapSnapshotForClient(sp *serialPort, clientID uint32) error {
r, err := sp.readTapSnapshot(clientID)
if err != nil {
return err
}
events := r.GetEvents()
if len(events) == 0 {
fmt.Println("no tap events (none pending or older than 16 ms)")
return nil
}
for _, e := range events {
fmt.Printf("client %d: %s (age %d ms)\n",
e.GetClientId(), tapKindLabel(e.GetKind()), e.GetAgeMs())
}
return nil
}
func tapKindLabel(k pb.TapKind) string { func tapKindLabel(k pb.TapKind) string {
switch k { switch k {
case pb.TapKind_TAP_SINGLE: case pb.TapKind_TAP_SINGLE:

View File

@ -643,8 +643,8 @@ func runCacheStatusDashboardPoller(link *managedSerial, hub *wsHub, interval tim
if err != nil { if err != nil {
continue continue
} }
hub.mergeAccel(cache.GetAccel()) hub.mergeAccel(accelSamplesFromCacheStatus(cache))
hub.mergeTap(cache.GetTaps()) hub.mergeTap(tapEventsFromCacheStatus(cache))
} }
} }
} }

284
goTool/docs/API_REST.md Normal file
View File

@ -0,0 +1,284 @@
# REST API
`go run . -port /dev/ttyUSB0 serve` starts two HTTP servers on the same UART link:
| 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",
"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` 0100 |
| `digit` | `digit` 010 |
| `blink` | `blink_ms`, `blink_count` |
| `find-me` | Locate pod |
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.
### Live stream (host `CACHE_STATUS` poll ~16 ms)
```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}
```
### Unicast test
```http
POST /api/unicast-test
Content-Type: application/json
{"client_id": 16, "seq": 42}
```
### 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}
```
### OTA (master UART upload)
```http
POST /api/ota
Content-Type: multipart/form-data
```
Form field **`firmware`**: binary image, max **2 MiB**.
```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: `go run . -port /dev/ttyUSB0 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: <id>` |
| Alle Slaves deadzone | `all_clients` + `slaves_only` on POST |
| Unicast test | `POST /api/unicast-test` |
| Tap notify S/D/T | `PUT /api/clients/{id}/tap-notify` |
| Tap receive (UI) | Live stream + tap notify; see WebSocket doc for external API |

View File

@ -0,0 +1,348 @@
# WebSocket API
`go run . -port /dev/ttyUSB0 serve` exposes two WebSocket endpoints. They share the same UART link but serve different purposes.
| URL | Port (default) | Role |
|-----|----------------|------|
| `ws://localhost:8080/ws` | Dashboard (`-addr`) | Server → client only: full `DashboardState` JSON (~2 s poll + live-stream accel/tap) |
| `ws://localhost:8081/ws` | External API (`-api-addr`) | Request/response commands + optional **accel** / **tap** push streams |
Disable the external server with `-api-addr ""`.
CLI overview and UART commands: [`../README.md`](../README.md). HTTP endpoints: [`API_REST.md`](API_REST.md).
---
## External API (`:8081/ws`)
### Connection flow
1. Connect → server sends **`hello`** (receive off; lists available commands).
2. Send JSON commands → server replies with a matching `*_status` or `client_list` message (one reply per command).
3. After `set_stream` / `set_tap_stream` with `enable: true`, the server may send **`accel`** and/or **`tap`** messages **without** a prior command (push stream).
Commands and stream pushes are multiplexed on one socket. While streaming, always parse `type` and branch (status vs sample vs error).
### Two layers (accel and tap)
| Layer | Commands | Effect |
|-------|----------|--------|
| **Firmware (ESP-NOW)** | `set_accel_stream`, `set_tap_notify` | Per `client_id`: slave sends accel or tap kinds to the master |
| **This connection (host)** | `set_stream`, `set_tap_stream` | Whether **you** receive push JSON and at what rate (`interval_ms`, 1 ms … 10 s) |
- **Accel UART polling** runs only if at least one connection has `receive_accel: true` **and** at least one slave streams accel (`set_accel_stream` or dashboard).
- **Tap UART polling** runs only if at least one connection has `receive_tap: true` (`set_tap_stream`). `set_tap_notify` alone does **not** poll.
Typical sequence:
1. `list_clients` → slave IDs
2. Per slave: `set_accel_stream` / `set_tap_notify` as needed
3. `set_stream` and/or `set_tap_stream` with `"enable": true`
4. Read push messages in a loop
There is **no per-slave filter** on push messages: each `accel` contains all cached slaves; each `tap` contains all visible events. Filter by `client_id` in your app.
---
## Push stream messages
These are the samples you get after enabling receive. Interval is per WebSocket connection; the server UART poll uses the **minimum** `interval_ms` among all subscribers that want accel or tap.
### `accel` (type `"accel"`)
Sent only when `set_stream` has `enable: true`, a slave streams accel, and the poll tick fires for this connection.
**Success** — all slaves with a cache entry on the master (not only those with `valid: true`):
```json
{
"type": "accel",
"t": 1716900123456789012,
"success": true,
"clients": [
{
"client_id": 16,
"valid": true,
"x": 12,
"y": -34,
"z": 16384,
"age_ms": 8
},
{
"client_id": 42,
"valid": false
}
]
}
```
| Field | Meaning |
|-------|---------|
| `t` | Unix timestamp in **nanoseconds** when the host read the cache |
| `success` | `true` if `CACHE_STATUS` succeeded |
| `clients[]` | One entry per slave slot in the master cache |
| `client_id` | ESP-NOW client id (same as `list_clients`) |
| `valid` | `false` if no sample yet or stale; omit `x`/`y`/`z` when false |
| `x`, `y`, `z` | Raw accelerometer LSB (BMA456, ±2 g scale on the pod) |
| `age_ms` | Milliseconds since the master received this sample |
**Failure** (e.g. UART busy):
```json
{
"type": "accel",
"t": 1716900123456789012,
"success": false,
"error": "uart busy"
}
```
No `clients` array on failure.
### `tap` (type `"tap"`)
Sent only when `set_tap_stream` has `enable: true` and there is at least one event to show.
Events appear when the master cache reports a new tap. Each event stays in push payloads for **`tap_display_min_ms`** (2000 ms, also in `hello`) after the API first saw it, even if the hardware age grows.
**Success**:
```json
{
"type": "tap",
"t": 1716900123456789012,
"success": true,
"events": [
{
"client_id": 16,
"valid": true,
"kind": "single",
"age_ms": 3,
"shown_at_ms": 1717000000123
}
]
}
```
| Field | Meaning |
|-------|---------|
| `t` | Unix timestamp in **nanoseconds** (poll time) |
| `events[]` | All taps currently “on screen” for the API |
| `client_id` | Slave that tapped |
| `kind` | `"single"`, `"double"`, or `"triple"` |
| `age_ms` | Age in the master cache when read |
| `shown_at_ms` | Unix **milliseconds** when this host first included the event |
If no events are visible, **no** `tap` message is sent on that tick (unlike accel, which can send empty `clients` only on success with cache data).
**Failure**:
```json
{
"type": "tap",
"t": 1716900123456789012,
"success": false,
"error": "uart busy"
}
```
---
## Commands (request → response)
Send one JSON object per message. Field `type` selects the command.
### `hello` (server → client, on connect)
```json
{
"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": [
"list_clients",
"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"
]
}
```
### `list_clients`
Request: `{"type":"list_clients"}`
Response `client_list`:
```json
{
"type": "client_list",
"success": true,
"clients": [
{
"id": 16,
"mac": "aa:bb:cc:dd:ee:10",
"version": 1,
"available": true,
"used": true,
"last_ping": 1234,
"last_success_ping": 1200,
"accel_stream": false,
"tap_notify_single": false,
"tap_notify_double": false,
"tap_notify_triple": false
}
]
}
```
### `set_stream` / `get_stream` (receive accel on this connection)
```json
{"type":"set_stream","enable":true,"interval_ms":32}
{"type":"get_stream"}
```
Response `stream_status`:
```json
{"type":"stream_status","receive_accel":true,"interval_ms":32,"success":true}
```
### `set_accel_stream` / `get_accel_stream` (firmware, per slave)
`client_id` required (> 0).
```json
{"type":"set_accel_stream","client_id":16,"enable":true}
{"type":"get_accel_stream","client_id":16}
```
Response `accel_stream_status`:
```json
{"type":"accel_stream_status","client_id":16,"enabled":true,"success":true}
```
### `set_tap_stream` / `get_tap_stream` (receive tap on this connection)
```json
{"type":"set_tap_stream","enable":true,"interval_ms":16}
{"type":"get_tap_stream"}
```
Response `tap_stream_status`:
```json
{"type":"tap_stream_status","receive_tap":true,"interval_ms":16,"success":true}
```
### `set_tap_notify` / `get_tap_notify` (firmware, per slave)
Per client: `single`, `double_tap`, `triple` required on set.
```json
{"type":"set_tap_notify","client_id":16,"single":true,"double_tap":false,"triple":false}
```
Broadcast: `"all_clients": true` with the three booleans.
Response `tap_notify_status`:
```json
{
"type": "tap_notify_status",
"client_id": 16,
"success": true,
"single": true,
"double_tap": false,
"triple": false
}
```
### `set_led_ring`
Same JSON body as [`POST /api/led-ring`](API_REST.md#led-ring) with `"type":"set_led_ring"` added. Reply: `led_ring_status`.
### `get_battery`
Body: `{"type":"get_battery","all_clients":true}` or `"client_id":16`. Default if omitted: all clients.
Reply: `battery_status` with `samples[]` (see REST doc).
---
## Examples
### Accel stream
```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": "list_clients"}))
clients = json.loads(await ws.recv())["clients"]
for c in clients:
if not c.get("available"):
continue
await ws.send(json.dumps({
"type": "set_accel_stream", "client_id": c["id"], "enable": True
}))
await ws.recv() # accel_stream_status
await ws.send(json.dumps({"type": "set_stream", "enable": True, "interval_ms": 16}))
await ws.recv() # stream_status
while True:
msg = json.loads(await ws.recv())
if msg.get("type") != "accel":
continue
if not msg.get("success"):
print("error:", msg.get("error"))
continue
for c in msg.get("clients", []):
if c.get("valid"):
print(c["client_id"], c["x"], c["y"], c["z"], "age", c.get("age_ms"))
asyncio.run(main())
```
### Tap stream
```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
}))
await ws.recv() # tap_notify_status
await ws.send(json.dumps({"type": "set_tap_stream", "enable": True, "interval_ms": 16}))
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())
```
---
## Dashboard WebSocket (`:8080/ws`)
Read-only from the browsers perspective: the server pushes JSON whenever state changes. Clients do not send commands on this socket (messages are ignored).
Payload shape: `DashboardState``updated_at`, `serial_port`, `uart_connected`, `live_stream`, `master`, `clients[]` (id, mac, accel, tap notify flags, battery, etc.). Accel/tap samples appear here when **Live stream** is enabled in the UI (`PUT /api/live-stream`).
During OTA, additional messages with `"type":"ota_progress"` may appear on the same socket.
Configure slaves via REST on `:8080` ([`API_REST.md`](API_REST.md)), not via this WebSocket.

View File

@ -17,9 +17,7 @@ func usage() {
fmt.Fprintf(os.Stderr, " clients registered ESP-NOW slaves on the master\n") fmt.Fprintf(os.Stderr, " clients registered ESP-NOW slaves on the master\n")
fmt.Fprintf(os.Stderr, " deadzone get/set accelerometer deadzone (LSB)\n") fmt.Fprintf(os.Stderr, " deadzone get/set accelerometer deadzone (LSB)\n")
fmt.Fprintf(os.Stderr, " tap-notify get/set which tap kinds notify via ESP-NOW\n") fmt.Fprintf(os.Stderr, " tap-notify get/set which tap kinds notify via ESP-NOW\n")
fmt.Fprintf(os.Stderr, " tap read cached tap events from master\n") fmt.Fprintf(os.Stderr, " cache-status subscribed accel + tap cache (one UART round-trip)\n")
fmt.Fprintf(os.Stderr, " accel read cached slave accel snapshot from master\n")
fmt.Fprintf(os.Stderr, " cache-status combined accel + tap cache (one UART round-trip)\n")
fmt.Fprintf(os.Stderr, " unicast-test send ESP-NOW unicast test to one slave\n") fmt.Fprintf(os.Stderr, " unicast-test send ESP-NOW unicast test to one slave\n")
fmt.Fprintf(os.Stderr, " test run automated scenario (see testdata/)\n") fmt.Fprintf(os.Stderr, " test run automated scenario (see testdata/)\n")
fmt.Fprintf(os.Stderr, " serve web dashboard (Bootstrap + WebSocket)\n") fmt.Fprintf(os.Stderr, " serve web dashboard (Bootstrap + WebSocket)\n")
@ -54,7 +52,7 @@ func main() {
os.Exit(2) os.Exit(2)
} }
runErr = runServe(*portName, *baud, flag.Args()[1:]) runErr = runServe(*portName, *baud, flag.Args()[1:])
case "version", "clients", "client-info", "deadzone", "accel-deadzone", "tap-notify", "tap_notify", "tap", "accel", "accel-read", "accel_read", "cache-status", "cache_status", "unicast-test", "unicast_test", "led-ring", "led_ring", "find-me", "find_me", "restart", "ota", "ota-progress", "ota_progress": case "version", "clients", "client-info", "deadzone", "accel-deadzone", "tap-notify", "tap_notify", "cache-status", "cache_status", "unicast-test", "unicast_test", "led-ring", "led_ring", "find-me", "find_me", "restart", "ota", "ota-progress", "ota_progress":
if *portName == "" { if *portName == "" {
fmt.Fprintf(os.Stderr, "command %q requires -port\n\n", cmd) fmt.Fprintf(os.Stderr, "command %q requires -port\n\n", cmd)
usage() usage()
@ -74,10 +72,6 @@ func main() {
runErr = runDeadzone(sp, flag.Args()[1:]) runErr = runDeadzone(sp, flag.Args()[1:])
case "tap-notify", "tap_notify": case "tap-notify", "tap_notify":
runErr = runTapNotify(sp, flag.Args()[1:]) runErr = runTapNotify(sp, flag.Args()[1:])
case "tap":
runErr = runTapSnapshot(sp, flag.Args()[1:])
case "accel", "accel-read", "accel_read":
runErr = runAccel(sp)
case "cache-status", "cache_status": case "cache-status", "cache_status":
runErr = runCacheStatus(sp) runErr = runCacheStatus(sp)
case "unicast-test", "unicast_test": case "unicast-test", "unicast_test":

File diff suppressed because it is too large Load Diff

View File

@ -18,10 +18,8 @@ idf_component_register(
"cmd/cmd_version.c" "cmd/cmd_version.c"
"cmd/cmd_client_info.c" "cmd/cmd_client_info.c"
"cmd/cmd_accel_deadzone.c" "cmd/cmd_accel_deadzone.c"
"cmd/cmd_accel_snapshot.c"
"cmd/cmd_accel_stream.c" "cmd/cmd_accel_stream.c"
"cmd/cmd_tap_notify.c" "cmd/cmd_tap_notify.c"
"cmd/cmd_tap_snapshot.c"
"cmd/cmd_cache_status.c" "cmd/cmd_cache_status.c"
"cmd/cmd_espnow_unicast_test.c" "cmd/cmd_espnow_unicast_test.c"
"cmd/cmd_espnow_find_me.c" "cmd/cmd_espnow_find_me.c"

View File

@ -222,10 +222,9 @@ Host and master speak nanopb-encoded `UartMessage` inside UART frames (byte 0 =
| 21 | `OTA_SLAVE_PROGRESS` | Implemented (`cmd/cmd_ota_slave_progress.c`) — query per-slave ESP-NOW OTA progress | | 21 | `OTA_SLAVE_PROGRESS` | Implemented (`cmd/cmd_ota_slave_progress.c`) — query per-slave ESP-NOW OTA progress |
| 22 | `FIND_ME` | Implemented (`cmd/cmd_espnow_find_me.c`) — `client_id=0` local ring, `>0` ESP-NOW to slave | | 22 | `FIND_ME` | Implemented (`cmd/cmd_espnow_find_me.c`) — `client_id=0` local ring, `>0` ESP-NOW to slave |
| 23 | `RESTART` | Implemented (`cmd/cmd_restart.c`) — `client_id=0` reboot master, `>0` ESP-NOW reboot slave | | 23 | `RESTART` | Implemented (`cmd/cmd_restart.c`) — `client_id=0` reboot master, `>0` ESP-NOW reboot slave |
| 24 | `ACCEL_SNAPSHOT` | Implemented (`cmd/cmd_accel_snapshot.c`) — cached slave accel from ESP-NOW stream | | 25 | `ACCEL_STREAM` | Implemented — enable/disable slave ESP-NOW accel stream to master |
| 27 | `TAP_NOTIFY` | Implemented (`cmd/cmd_tap_notify.c`) — get/set which tap kinds notify via ESP-NOW | | 27 | `TAP_NOTIFY` | Implemented (`cmd/cmd_tap_notify.c`) — get/set which tap kinds notify via ESP-NOW |
| 28 | `TAP_SNAPSHOT` | Implemented (`cmd/cmd_tap_snapshot.c`) — consume cached tap events from master registry | | 29 | `CACHE_STATUS` | Implemented (`cmd/cmd_cache_status.c`) — subscribed accel + tap cache (one UART round-trip) |
| 29 | `CACHE_STATUS` | Implemented (`cmd/cmd_cache_status.c`) — combined accel + tap cache in one UART round-trip |
Regenerate C code: Regenerate C code:
@ -319,29 +318,6 @@ Sets the **software** deadzone used by `bosch456.c` when logging accel (see [BMA
**Response:** `accel_deadzone_response` with applied `deadzone`, `success`, and `slaves_updated` (ESP-NOW count). **Response:** `accel_deadzone_response` with applied `deadzone`, `success`, and `slaves_updated` (ESP-NOW count).
### ACCEL_SNAPSHOT command
Read **cached** accelerometer samples on the **master** (one entry per registered slave). Slaves send `ESPNOW_ACCEL_SAMPLE` to the master every **16 ms** (`esp_now_comm.c`); the master stores the latest value per client in `client_registry.c`.
**Request:** framed `18` (`0x18`) + optional `accel_snapshot_request` (`client_id`: `0` = all slaves, `>0` = one id).
**Response:** `accel_snapshot_response.samples[]`:
| Field | Meaning |
|-------|---------|
| `client_id` | Slave id (registry) |
| `valid` | At least one ESP-NOW sample received since boot |
| `x`, `y`, `z` | Raw BMA456 LSB (±2g) |
| `age_ms` | Ms since last sample from that slave |
Host:
```bash
go run . -port /dev/ttyUSB0 accel
```
External API (`serve -api-addr :8081`) polls this command every 16 ms and streams JSON over WebSocket.
### TAP_NOTIFY command ### TAP_NOTIFY command
Configure which BMA456 tap kinds a **slave** forwards to the master over ESP-NOW. The slave only sends `ESPNOW_TAP_EVENT` when the matching notify flag is enabled (set locally on the slave via ESP-NOW). Configure which BMA456 tap kinds a **slave** forwards to the master over ESP-NOW. The slave only sends `ESPNOW_TAP_EVENT` when the matching notify flag is enabled (set locally on the slave via ESP-NOW).
@ -364,41 +340,30 @@ go run . -port /dev/ttyUSB0 tap-notify -client 16 -set -single
go run . -port /dev/ttyUSB0 tap-notify -client 16 go run . -port /dev/ttyUSB0 tap-notify -client 16
``` ```
### TAP_SNAPSHOT command
Read **cached** tap events on the **master** (one pending event per slave). Slaves send `ESPNOW_TAP_EVENT` on tap; the master stores the latest value per client in `client_registry.c` for up to **16 ms** (`CLIENT_REGISTRY_TAP_MAX_AGE_MS`). Each snapshot **consumes** fresh events (cleared after read).
### CACHE_STATUS command ### CACHE_STATUS command
Fast combined poll for host tools at **16 ms** or faster: one UART frame, no request body (command id `0x1d` only). Read **cached** accel and/or tap data on the **master** in one UART round-trip. Slaves send `ESPNOW_ACCEL_SAMPLE` every **16 ms** when streaming; tap events arrive via `ESPNOW_TAP_EVENT` and are held up to **16 ms** (`CLIENT_REGISTRY_TAP_MAX_AGE_MS`). Pending taps are **consumed** on read (like the former `TAP_SNAPSHOT`).
**Response:** `cache_status_response` with: **Request:** framed `1d` (`0x1d`) only — no body (`CacheStatusRequest` empty).
- `accel[]` — same as `ACCEL_SNAPSHOT`, only clients with `accel_stream_enabled` **Response:** `cache_status_response.clients[]` — one entry per slave with `accel_stream_enabled` and/or any tap-notify flag:
- `taps[]` — same as `TAP_SNAPSHOT`, only clients with any tap notify flag; pending taps are consumed
The master walks `client_registry` once per request (`cmd/cmd_cache_status.c`). Prefer this over separate `ACCEL_SNAPSHOT` + `TAP_SNAPSHOT` when polling both streams. | Field | When present |
|-------|----------------|
| `client_id` | Always (for listed slaves) |
| `accel` | Slave has accel stream on (`valid`, `x`/`y`/`z`, `age_ms` when sample fresh) |
| `tap` | Tap notify on **and** a pending tap was consumed (`kind`, `age_ms`) |
Only slaves with at least one tap-notify flag enabled are included. Unsubscribed submessages are omitted on the wire (proto3 defaults). The master walks `client_registry` once per request (`cmd/cmd_cache_status.c`).
**Request:** framed `1c` (`0x1c`) + optional `tap_snapshot_request` (`client_id`: `0` = all, `>0` = one id). Host tools poll this at **16 ms** when live-stream / WebSocket receive is enabled. Tap events stay visible for **2 s** in the UI/API after first sight.
**Response:** `tap_snapshot_response.events[]`:
| Field | Meaning |
|-------|---------|
| `client_id` | Slave id (registry) |
| `valid` | Fresh tap available (≤16 ms) |
| `kind` | `TAP_SINGLE`, `TAP_DOUBLE`, or `TAP_TRIPLE` |
| `age_ms` | Ms since tap was received on master |
Host tools poll this only when **receive** is enabled (dashboard tap column, WebSocket `set_tap_stream`). They keep events visible for **2 s** in the UI/API after first sight.
```bash ```bash
go run . -port /dev/ttyUSB0 tap go run . -port /dev/ttyUSB0 cache-status
go run . -port /dev/ttyUSB0 tap -client 16
``` ```
External API (`serve -api-addr :8081`) uses the same command for WebSocket `accel` / `tap` push.
### ESPNOW_UNICAST_TEST command ### ESPNOW_UNICAST_TEST command
Minimal master→slave ESP-NOW unicast check (no BMA456). Use this before debugging `ACCEL_DEADZONE` unicast. Minimal master→slave ESP-NOW unicast check (no BMA456). Use this before debugging `ACCEL_DEADZONE` unicast.
@ -564,10 +529,8 @@ Target: ESP32-S3. Close serial monitor on the UART adapter port before running `
| `cmd/cmd_client_info.c/h` | CLIENT_INFO handler | | `cmd/cmd_client_info.c/h` | CLIENT_INFO handler |
| `client_registry.c/h` | Registered slave table | | `client_registry.c/h` | Registered slave table |
| `bosch456.c/h` | BMA456H I2C driver, accel poll, on-demand read, tap INT, deadzone filter | | `bosch456.c/h` | BMA456H I2C driver, accel poll, on-demand read, tap INT, deadzone filter |
| `cmd/cmd_accel_snapshot.c` | UART `ACCEL_SNAPSHOT` — cached slave accel |
| `cmd/cmd_tap_notify.c` | UART `TAP_NOTIFY` — ESP-NOW tap notify config | | `cmd/cmd_tap_notify.c` | UART `TAP_NOTIFY` — ESP-NOW tap notify config |
| `cmd/cmd_tap_snapshot.c` | UART `TAP_SNAPSHOT` — consume cached tap events | | `cmd/cmd_cache_status.c` | UART `CACHE_STATUS` — subscribed accel + tap cache poll |
| `cmd/cmd_cache_status.c` | UART `CACHE_STATUS` — combined accel + tap cache poll |
| `board_input.c/h` | Taster GPIO12, LiPo ADC on GPIO1 / GPIO12 | | `board_input.c/h` | Taster GPIO12, LiPo ADC on GPIO1 / GPIO12 |
| `pod_settings.c/h` | NVS persistence (accel deadzone, …) | | `pod_settings.c/h` | NVS persistence (accel deadzone, …) |
| `led_ring.c/h` | LED ring (digit display, progress bar) | | `led_ring.c/h` | LED ring (digit display, progress bar) |

View File

@ -1,68 +0,0 @@
#include "client_registry.h"
#include "cmd_accel_snapshot.h"
#include "uart_cmd.h"
static const char *TAG = "[ACCEL_SNAP]";
static void fill_accel_snapshot(alox_AccelSnapshotResponse *out,
uint32_t filter_client_id) {
if (out == NULL) {
return;
}
out->samples_count = 0;
size_t count = client_registry_count();
for (size_t i = 0; i < count; i++) {
const client_info_t *client = client_registry_at(i);
if (client == NULL) {
continue;
}
if (filter_client_id != 0 && client->id != filter_client_id) {
continue;
}
if (!client->accel_stream_enabled) {
continue;
}
if (out->samples_count >=
sizeof(out->samples) / sizeof(out->samples[0])) {
break;
}
alox_AccelSample *sample = &out->samples[out->samples_count++];
sample->client_id = client->id;
sample->valid = client->accel_valid;
sample->x = client->accel_x;
sample->y = client->accel_y;
sample->z = client->accel_z;
if (client->accel_valid) {
sample->age_ms = client_registry_ms_since(client->accel_updated_at);
}
}
}
static void handle_accel_snapshot(const uint8_t *data, size_t len) {
uint32_t filter_client_id = 0;
if (len > 0) {
alox_UartMessage req;
if (uart_cmd_decode(data, len, &req) == ESP_OK) {
alox_AccelSnapshotRequest *snap_req = UART_CMD_REQ(
&req, alox_UartMessage_accel_snapshot_request_tag, accel_snapshot_request);
if (snap_req != NULL) {
filter_client_id = snap_req->client_id;
}
}
}
alox_UartMessage response;
uart_cmd_init_response(&response, alox_MessageType_ACCEL_SNAPSHOT,
alox_UartMessage_accel_snapshot_response_tag);
fill_accel_snapshot(&response.payload.accel_snapshot_response, filter_client_id);
uart_cmd_send(&response, TAG);
}
void cmd_accel_snapshot_register(void) {
uart_cmd_register(alox_MessageType_ACCEL_SNAPSHOT, handle_accel_snapshot);
}

View File

@ -1,6 +0,0 @@
#ifndef CMD_ACCEL_SNAPSHOT_H
#define CMD_ACCEL_SNAPSHOT_H
void cmd_accel_snapshot_register(void);
#endif

View File

@ -28,8 +28,7 @@ static void fill_cache_status(alox_CacheStatusResponse *out) {
return; return;
} }
out->accel_count = 0; out->clients_count = 0;
out->taps_count = 0;
size_t count = client_registry_count(); size_t count = client_registry_count();
for (size_t i = 0; i < count; i++) { for (size_t i = 0; i < count; i++) {
@ -38,31 +37,41 @@ static void fill_cache_status(alox_CacheStatusResponse *out) {
continue; continue;
} }
if (client->accel_stream_enabled && const bool want_accel = client->accel_stream_enabled;
out->accel_count < sizeof(out->accel) / sizeof(out->accel[0])) { const bool want_tap = tap_notify_any(client);
alox_AccelSample *sample = &out->accel[out->accel_count++]; if (!want_accel && !want_tap) {
sample->client_id = client->id; continue;
sample->valid = client->accel_valid; }
sample->x = client->accel_x; if (out->clients_count >=
sample->y = client->accel_y; sizeof(out->clients) / sizeof(out->clients[0])) {
sample->z = client->accel_z; break;
}
alox_CacheClientStatus *entry = &out->clients[out->clients_count++];
entry->client_id = client->id;
entry->has_accel = false;
entry->has_tap = false;
if (want_accel) {
entry->has_accel = true;
entry->accel.valid = client->accel_valid;
if (client->accel_valid) { if (client->accel_valid) {
sample->age_ms = client_registry_ms_since(client->accel_updated_at); entry->accel.x = client->accel_x;
entry->accel.y = client->accel_y;
entry->accel.z = client->accel_z;
entry->accel.age_ms =
client_registry_ms_since(client->accel_updated_at);
} }
} }
if (tap_notify_any(client) && if (want_tap) {
out->taps_count < sizeof(out->taps) / sizeof(out->taps[0])) {
uint32_t kind = 0; uint32_t kind = 0;
uint32_t age_ms = 0; uint32_t age_ms = 0;
if (!client_registry_take_tap(client->id, &kind, &age_ms)) { if (client_registry_take_tap(client->id, &kind, &age_ms)) {
continue; entry->has_tap = true;
entry->tap.kind = tap_kind_from_registry(kind);
entry->tap.age_ms = age_ms;
} }
alox_TapEvent *event = &out->taps[out->taps_count++];
event->client_id = client->id;
event->valid = true;
event->kind = tap_kind_from_registry(kind);
event->age_ms = age_ms;
} }
} }
} }

View File

@ -48,16 +48,12 @@ static const char *message_type_name(uint16_t id) {
return "FIND_ME"; return "FIND_ME";
case alox_MessageType_RESTART: case alox_MessageType_RESTART:
return "RESTART"; return "RESTART";
case alox_MessageType_ACCEL_SNAPSHOT:
return "ACCEL_SNAPSHOT";
case alox_MessageType_ACCEL_STREAM: case alox_MessageType_ACCEL_STREAM:
return "ACCEL_STREAM"; return "ACCEL_STREAM";
case alox_MessageType_BATTERY_STATUS: case alox_MessageType_BATTERY_STATUS:
return "BATTERY_STATUS"; return "BATTERY_STATUS";
case alox_MessageType_TAP_NOTIFY: case alox_MessageType_TAP_NOTIFY:
return "TAP_NOTIFY"; return "TAP_NOTIFY";
case alox_MessageType_TAP_SNAPSHOT:
return "TAP_SNAPSHOT";
case alox_MessageType_CACHE_STATUS: case alox_MessageType_CACHE_STATUS:
return "CACHE_STATUS"; return "CACHE_STATUS";
default: default:

View File

@ -1,88 +0,0 @@
#include "client_registry.h"
#include "cmd_tap_snapshot.h"
#include "uart_cmd.h"
static const char *TAG = "[TAP_SNAP]";
static alox_TapKind tap_kind_from_registry(uint32_t kind) {
switch (kind) {
case 1:
return alox_TapKind_TAP_SINGLE;
case 2:
return alox_TapKind_TAP_DOUBLE;
case 3:
return alox_TapKind_TAP_TRIPLE;
default:
return alox_TapKind_TAP_NONE;
}
}
static bool tap_notify_any(const client_info_t *client) {
return client != NULL &&
(client->tap_notify_single || client->tap_notify_double ||
client->tap_notify_triple);
}
static void fill_tap_snapshot(alox_TapSnapshotResponse *out,
uint32_t filter_client_id) {
if (out == NULL) {
return;
}
out->events_count = 0;
size_t count = client_registry_count();
for (size_t i = 0; i < count; i++) {
const client_info_t *client = client_registry_at(i);
if (client == NULL) {
continue;
}
if (filter_client_id != 0 && client->id != filter_client_id) {
continue;
}
if (!tap_notify_any(client)) {
continue;
}
if (out->events_count >= sizeof(out->events) / sizeof(out->events[0])) {
break;
}
uint32_t kind = 0;
uint32_t age_ms = 0;
if (!client_registry_take_tap(client->id, &kind, &age_ms)) {
continue;
}
alox_TapEvent *event = &out->events[out->events_count++];
event->client_id = client->id;
event->valid = true;
event->kind = tap_kind_from_registry(kind);
event->age_ms = age_ms;
}
}
static void handle_tap_snapshot(const uint8_t *data, size_t len) {
uint32_t filter_client_id = 0;
if (len > 0) {
alox_UartMessage req;
if (uart_cmd_decode(data, len, &req) == ESP_OK) {
alox_TapSnapshotRequest *snap_req = UART_CMD_REQ(
&req, alox_UartMessage_tap_snapshot_request_tag, tap_snapshot_request);
if (snap_req != NULL) {
filter_client_id = snap_req->client_id;
}
}
}
alox_UartMessage response;
uart_cmd_init_response(&response, alox_MessageType_TAP_SNAPSHOT,
alox_UartMessage_tap_snapshot_response_tag);
fill_tap_snapshot(&response.payload.tap_snapshot_response, filter_client_id);
uart_cmd_send(&response, TAG);
}
void cmd_tap_snapshot_register(void) {
uart_cmd_register(alox_MessageType_TAP_SNAPSHOT, handle_tap_snapshot);
}

View File

@ -1,6 +0,0 @@
#ifndef CMD_TAP_SNAPSHOT_H
#define CMD_TAP_SNAPSHOT_H
void cmd_tap_snapshot_register(void);
#endif

View File

@ -1,10 +1,8 @@
#include "app_config.h" #include "app_config.h"
#include "cmd_handler.h" #include "cmd_handler.h"
#include "cmd_accel_deadzone.h" #include "cmd_accel_deadzone.h"
#include "cmd_accel_snapshot.h"
#include "cmd_accel_stream.h" #include "cmd_accel_stream.h"
#include "cmd_tap_notify.h" #include "cmd_tap_notify.h"
#include "cmd_tap_snapshot.h"
#include "cmd_cache_status.h" #include "cmd_cache_status.h"
#include "cmd_espnow_unicast_test.h" #include "cmd_espnow_unicast_test.h"
#include "cmd_espnow_find_me.h" #include "cmd_espnow_find_me.h"
@ -183,10 +181,8 @@ void app_main(void) {
cmd_version_register(); cmd_version_register();
cmd_client_info_register(); cmd_client_info_register();
cmd_accel_deadzone_register(); cmd_accel_deadzone_register();
cmd_accel_snapshot_register();
cmd_accel_stream_register(); cmd_accel_stream_register();
cmd_tap_notify_register(); cmd_tap_notify_register();
cmd_tap_snapshot_register();
cmd_cache_status_register(); cmd_cache_status_register();
cmd_espnow_unicast_test_register(); cmd_espnow_unicast_test_register();
cmd_espnow_find_me_register(); cmd_espnow_find_me_register();

View File

@ -54,33 +54,30 @@ PB_BIND(alox_BatterySample, alox_BatterySample, AUTO)
PB_BIND(alox_BatteryStatusResponse, alox_BatteryStatusResponse, 2) PB_BIND(alox_BatteryStatusResponse, alox_BatteryStatusResponse, 2)
PB_BIND(alox_AccelSnapshotRequest, alox_AccelSnapshotRequest, AUTO)
PB_BIND(alox_AccelSample, alox_AccelSample, AUTO) PB_BIND(alox_AccelSample, alox_AccelSample, AUTO)
PB_BIND(alox_AccelSnapshotResponse, alox_AccelSnapshotResponse, 2)
PB_BIND(alox_TapNotifyRequest, alox_TapNotifyRequest, AUTO) PB_BIND(alox_TapNotifyRequest, alox_TapNotifyRequest, AUTO)
PB_BIND(alox_TapNotifyResponse, alox_TapNotifyResponse, AUTO) PB_BIND(alox_TapNotifyResponse, alox_TapNotifyResponse, AUTO)
PB_BIND(alox_TapSnapshotRequest, alox_TapSnapshotRequest, AUTO)
PB_BIND(alox_TapEvent, alox_TapEvent, AUTO) PB_BIND(alox_TapEvent, alox_TapEvent, AUTO)
PB_BIND(alox_TapSnapshotResponse, alox_TapSnapshotResponse, 2)
PB_BIND(alox_CacheStatusRequest, alox_CacheStatusRequest, AUTO) PB_BIND(alox_CacheStatusRequest, alox_CacheStatusRequest, AUTO)
PB_BIND(alox_CacheClientAccel, alox_CacheClientAccel, AUTO)
PB_BIND(alox_CacheClientTap, alox_CacheClientTap, AUTO)
PB_BIND(alox_CacheClientStatus, alox_CacheClientStatus, AUTO)
PB_BIND(alox_CacheStatusResponse, alox_CacheStatusResponse, 2) PB_BIND(alox_CacheStatusResponse, alox_CacheStatusResponse, 2)

View File

@ -28,11 +28,9 @@ typedef enum _alox_MessageType {
alox_MessageType_OTA_SLAVE_PROGRESS = 21, alox_MessageType_OTA_SLAVE_PROGRESS = 21,
alox_MessageType_FIND_ME = 22, alox_MessageType_FIND_ME = 22,
alox_MessageType_RESTART = 23, alox_MessageType_RESTART = 23,
alox_MessageType_ACCEL_SNAPSHOT = 24,
alox_MessageType_ACCEL_STREAM = 25, alox_MessageType_ACCEL_STREAM = 25,
alox_MessageType_BATTERY_STATUS = 26, alox_MessageType_BATTERY_STATUS = 26,
alox_MessageType_TAP_NOTIFY = 27, alox_MessageType_TAP_NOTIFY = 27,
alox_MessageType_TAP_SNAPSHOT = 28,
/* * Combined cached accel + tap poll (one UART round-trip, ~16 ms cadence). */ /* * Combined cached accel + tap poll (one UART round-trip, ~16 ms cadence). */
alox_MessageType_CACHE_STATUS = 29 alox_MessageType_CACHE_STATUS = 29
} alox_MessageType; } alox_MessageType;
@ -154,12 +152,7 @@ typedef struct _alox_BatteryStatusResponse {
alox_BatterySample samples[17]; alox_BatterySample samples[17];
} alox_BatteryStatusResponse; } alox_BatteryStatusResponse;
/* Host → master: read cached accel samples from slaves (only while stream enabled). /* * Legacy host-side sample shape (dashboard helpers); use CACHE_STATUS on the wire. */
client_id 0 = all registered slaves; otherwise one slave. */
typedef struct _alox_AccelSnapshotRequest {
uint32_t client_id;
} alox_AccelSnapshotRequest;
typedef struct _alox_AccelSample { typedef struct _alox_AccelSample {
uint32_t client_id; uint32_t client_id;
bool valid; bool valid;
@ -170,11 +163,6 @@ typedef struct _alox_AccelSample {
uint32_t age_ms; uint32_t age_ms;
} alox_AccelSample; } alox_AccelSample;
typedef struct _alox_AccelSnapshotResponse {
pb_size_t samples_count;
alox_AccelSample samples[16];
} alox_AccelSnapshotResponse;
/* * Host → master: enable/disable tap ESP-NOW notify per slave (single/double/triple). */ /* * Host → master: enable/disable tap ESP-NOW notify per slave (single/double/triple). */
typedef struct _alox_TapNotifyRequest { typedef struct _alox_TapNotifyRequest {
bool write; bool write;
@ -194,11 +182,7 @@ typedef struct _alox_TapNotifyResponse {
bool triple; bool triple;
} alox_TapNotifyResponse; } alox_TapNotifyResponse;
/* * Host → master: read cached tap events (discarded after reply or when age > 16 ms). */ /* * Legacy tap event shape (dashboard helpers); use CACHE_STATUS on the wire. */
typedef struct _alox_TapSnapshotRequest {
uint32_t client_id;
} alox_TapSnapshotRequest;
typedef struct _alox_TapEvent { typedef struct _alox_TapEvent {
uint32_t client_id; uint32_t client_id;
bool valid; bool valid;
@ -206,23 +190,39 @@ typedef struct _alox_TapEvent {
uint32_t age_ms; uint32_t age_ms;
} alox_TapEvent; } alox_TapEvent;
typedef struct _alox_TapSnapshotResponse {
pb_size_t events_count;
alox_TapEvent events[16];
} alox_TapSnapshotResponse;
/* * Host → master: one-shot read of subscribed cached slave data (no request body). */ /* * Host → master: one-shot read of subscribed cached slave data (no request body). */
typedef struct _alox_CacheStatusRequest { typedef struct _alox_CacheStatusRequest {
char dummy_field; char dummy_field;
} alox_CacheStatusRequest; } alox_CacheStatusRequest;
/* * Accel slice inside CACHE_STATUS (no client_id — use parent CacheClientStatus). */
typedef struct _alox_CacheClientAccel {
bool valid;
int32_t x;
int32_t y;
int32_t z;
uint32_t age_ms;
} alox_CacheClientAccel;
/* * Tap slice inside CACHE_STATUS; only present when a pending tap was consumed. */
typedef struct _alox_CacheClientTap {
alox_TapKind kind;
uint32_t age_ms;
} alox_CacheClientTap;
/* * One slave with accel and/or tap notify enabled; only subscribed fields are set. */
typedef struct _alox_CacheClientStatus {
uint32_t client_id;
bool has_accel;
alox_CacheClientAccel accel;
bool has_tap;
alox_CacheClientTap tap;
} alox_CacheClientStatus;
typedef struct _alox_CacheStatusResponse { typedef struct _alox_CacheStatusResponse {
/* * Slaves with accel_stream_enabled. */ /* * Slaves with accel_stream and/or tap notify; omitted fields are not subscribed. */
pb_size_t accel_count; pb_size_t clients_count;
alox_AccelSample accel[16]; alox_CacheClientStatus clients[16];
/* * Slaves with any tap notify flag; pending taps are consumed (like TAP_SNAPSHOT). */
pb_size_t taps_count;
alox_TapEvent taps[16];
} alox_CacheStatusResponse; } alox_CacheStatusResponse;
typedef struct _alox_EspNowUnicastTestRequest { typedef struct _alox_EspNowUnicastTestRequest {
@ -363,16 +363,12 @@ typedef struct _alox_UartMessage {
alox_EspNowFindMeResponse espnow_find_me_response; alox_EspNowFindMeResponse espnow_find_me_response;
alox_RestartRequest restart_request; alox_RestartRequest restart_request;
alox_RestartResponse restart_response; alox_RestartResponse restart_response;
alox_AccelSnapshotRequest accel_snapshot_request;
alox_AccelSnapshotResponse accel_snapshot_response;
alox_AccelStreamRequest accel_stream_request; alox_AccelStreamRequest accel_stream_request;
alox_AccelStreamResponse accel_stream_response; alox_AccelStreamResponse accel_stream_response;
alox_BatteryStatusRequest battery_status_request; alox_BatteryStatusRequest battery_status_request;
alox_BatteryStatusResponse battery_status_response; alox_BatteryStatusResponse battery_status_response;
alox_TapNotifyRequest tap_notify_request; alox_TapNotifyRequest tap_notify_request;
alox_TapNotifyResponse tap_notify_response; alox_TapNotifyResponse tap_notify_response;
alox_TapSnapshotRequest tap_snapshot_request;
alox_TapSnapshotResponse tap_snapshot_response;
alox_CacheStatusRequest cache_status_request; alox_CacheStatusRequest cache_status_request;
alox_CacheStatusResponse cache_status_response; alox_CacheStatusResponse cache_status_response;
} payload; } payload;
@ -410,15 +406,15 @@ extern "C" {
#define alox_TapEvent_kind_ENUMTYPE alox_TapKind #define alox_TapEvent_kind_ENUMTYPE alox_TapKind
#define alox_CacheClientTap_kind_ENUMTYPE alox_TapKind
@ -453,16 +449,15 @@ extern "C" {
#define alox_LipoReading_init_default {0, 0} #define alox_LipoReading_init_default {0, 0}
#define alox_BatterySample_init_default {0, false, alox_LipoReading_init_default, false, alox_LipoReading_init_default, 0} #define alox_BatterySample_init_default {0, false, alox_LipoReading_init_default, false, alox_LipoReading_init_default, 0}
#define alox_BatteryStatusResponse_init_default {0, 0, {alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default}} #define alox_BatteryStatusResponse_init_default {0, 0, {alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default}}
#define alox_AccelSnapshotRequest_init_default {0}
#define alox_AccelSample_init_default {0, 0, 0, 0, 0, 0} #define alox_AccelSample_init_default {0, 0, 0, 0, 0, 0}
#define alox_AccelSnapshotResponse_init_default {0, {alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default}}
#define alox_TapNotifyRequest_init_default {0, 0, 0, 0, 0, 0} #define alox_TapNotifyRequest_init_default {0, 0, 0, 0, 0, 0}
#define alox_TapNotifyResponse_init_default {0, 0, 0, 0, 0, 0} #define alox_TapNotifyResponse_init_default {0, 0, 0, 0, 0, 0}
#define alox_TapSnapshotRequest_init_default {0}
#define alox_TapEvent_init_default {0, 0, _alox_TapKind_MIN, 0} #define alox_TapEvent_init_default {0, 0, _alox_TapKind_MIN, 0}
#define alox_TapSnapshotResponse_init_default {0, {alox_TapEvent_init_default, alox_TapEvent_init_default, alox_TapEvent_init_default, alox_TapEvent_init_default, alox_TapEvent_init_default, alox_TapEvent_init_default, alox_TapEvent_init_default, alox_TapEvent_init_default, alox_TapEvent_init_default, alox_TapEvent_init_default, alox_TapEvent_init_default, alox_TapEvent_init_default, alox_TapEvent_init_default, alox_TapEvent_init_default, alox_TapEvent_init_default, alox_TapEvent_init_default}}
#define alox_CacheStatusRequest_init_default {0} #define alox_CacheStatusRequest_init_default {0}
#define alox_CacheStatusResponse_init_default {0, {alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default}, 0, {alox_TapEvent_init_default, alox_TapEvent_init_default, alox_TapEvent_init_default, alox_TapEvent_init_default, alox_TapEvent_init_default, alox_TapEvent_init_default, alox_TapEvent_init_default, alox_TapEvent_init_default, alox_TapEvent_init_default, alox_TapEvent_init_default, alox_TapEvent_init_default, alox_TapEvent_init_default, alox_TapEvent_init_default, alox_TapEvent_init_default, alox_TapEvent_init_default, alox_TapEvent_init_default}} #define alox_CacheClientAccel_init_default {0, 0, 0, 0, 0}
#define alox_CacheClientTap_init_default {_alox_TapKind_MIN, 0}
#define alox_CacheClientStatus_init_default {0, false, alox_CacheClientAccel_init_default, false, alox_CacheClientTap_init_default}
#define alox_CacheStatusResponse_init_default {0, {alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default}}
#define alox_EspNowUnicastTestRequest_init_default {0, 0} #define alox_EspNowUnicastTestRequest_init_default {0, 0}
#define alox_EspNowUnicastTestResponse_init_default {0, 0} #define alox_EspNowUnicastTestResponse_init_default {0, 0}
#define alox_LedRingProgressRequest_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define alox_LedRingProgressRequest_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
@ -494,16 +489,15 @@ extern "C" {
#define alox_LipoReading_init_zero {0, 0} #define alox_LipoReading_init_zero {0, 0}
#define alox_BatterySample_init_zero {0, false, alox_LipoReading_init_zero, false, alox_LipoReading_init_zero, 0} #define alox_BatterySample_init_zero {0, false, alox_LipoReading_init_zero, false, alox_LipoReading_init_zero, 0}
#define alox_BatteryStatusResponse_init_zero {0, 0, {alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero}} #define alox_BatteryStatusResponse_init_zero {0, 0, {alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero}}
#define alox_AccelSnapshotRequest_init_zero {0}
#define alox_AccelSample_init_zero {0, 0, 0, 0, 0, 0} #define alox_AccelSample_init_zero {0, 0, 0, 0, 0, 0}
#define alox_AccelSnapshotResponse_init_zero {0, {alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero}}
#define alox_TapNotifyRequest_init_zero {0, 0, 0, 0, 0, 0} #define alox_TapNotifyRequest_init_zero {0, 0, 0, 0, 0, 0}
#define alox_TapNotifyResponse_init_zero {0, 0, 0, 0, 0, 0} #define alox_TapNotifyResponse_init_zero {0, 0, 0, 0, 0, 0}
#define alox_TapSnapshotRequest_init_zero {0}
#define alox_TapEvent_init_zero {0, 0, _alox_TapKind_MIN, 0} #define alox_TapEvent_init_zero {0, 0, _alox_TapKind_MIN, 0}
#define alox_TapSnapshotResponse_init_zero {0, {alox_TapEvent_init_zero, alox_TapEvent_init_zero, alox_TapEvent_init_zero, alox_TapEvent_init_zero, alox_TapEvent_init_zero, alox_TapEvent_init_zero, alox_TapEvent_init_zero, alox_TapEvent_init_zero, alox_TapEvent_init_zero, alox_TapEvent_init_zero, alox_TapEvent_init_zero, alox_TapEvent_init_zero, alox_TapEvent_init_zero, alox_TapEvent_init_zero, alox_TapEvent_init_zero, alox_TapEvent_init_zero}}
#define alox_CacheStatusRequest_init_zero {0} #define alox_CacheStatusRequest_init_zero {0}
#define alox_CacheStatusResponse_init_zero {0, {alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero}, 0, {alox_TapEvent_init_zero, alox_TapEvent_init_zero, alox_TapEvent_init_zero, alox_TapEvent_init_zero, alox_TapEvent_init_zero, alox_TapEvent_init_zero, alox_TapEvent_init_zero, alox_TapEvent_init_zero, alox_TapEvent_init_zero, alox_TapEvent_init_zero, alox_TapEvent_init_zero, alox_TapEvent_init_zero, alox_TapEvent_init_zero, alox_TapEvent_init_zero, alox_TapEvent_init_zero, alox_TapEvent_init_zero}} #define alox_CacheClientAccel_init_zero {0, 0, 0, 0, 0}
#define alox_CacheClientTap_init_zero {_alox_TapKind_MIN, 0}
#define alox_CacheClientStatus_init_zero {0, false, alox_CacheClientAccel_init_zero, false, alox_CacheClientTap_init_zero}
#define alox_CacheStatusResponse_init_zero {0, {alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero}}
#define alox_EspNowUnicastTestRequest_init_zero {0, 0} #define alox_EspNowUnicastTestRequest_init_zero {0, 0}
#define alox_EspNowUnicastTestResponse_init_zero {0, 0} #define alox_EspNowUnicastTestResponse_init_zero {0, 0}
#define alox_LedRingProgressRequest_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define alox_LedRingProgressRequest_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
@ -568,14 +562,12 @@ extern "C" {
#define alox_BatterySample_age_ms_tag 4 #define alox_BatterySample_age_ms_tag 4
#define alox_BatteryStatusResponse_success_tag 1 #define alox_BatteryStatusResponse_success_tag 1
#define alox_BatteryStatusResponse_samples_tag 2 #define alox_BatteryStatusResponse_samples_tag 2
#define alox_AccelSnapshotRequest_client_id_tag 1
#define alox_AccelSample_client_id_tag 1 #define alox_AccelSample_client_id_tag 1
#define alox_AccelSample_valid_tag 2 #define alox_AccelSample_valid_tag 2
#define alox_AccelSample_x_tag 3 #define alox_AccelSample_x_tag 3
#define alox_AccelSample_y_tag 4 #define alox_AccelSample_y_tag 4
#define alox_AccelSample_z_tag 5 #define alox_AccelSample_z_tag 5
#define alox_AccelSample_age_ms_tag 6 #define alox_AccelSample_age_ms_tag 6
#define alox_AccelSnapshotResponse_samples_tag 1
#define alox_TapNotifyRequest_write_tag 1 #define alox_TapNotifyRequest_write_tag 1
#define alox_TapNotifyRequest_client_id_tag 2 #define alox_TapNotifyRequest_client_id_tag 2
#define alox_TapNotifyRequest_all_clients_tag 3 #define alox_TapNotifyRequest_all_clients_tag 3
@ -588,14 +580,21 @@ extern "C" {
#define alox_TapNotifyResponse_single_tag 4 #define alox_TapNotifyResponse_single_tag 4
#define alox_TapNotifyResponse_double_tap_tag 5 #define alox_TapNotifyResponse_double_tap_tag 5
#define alox_TapNotifyResponse_triple_tag 6 #define alox_TapNotifyResponse_triple_tag 6
#define alox_TapSnapshotRequest_client_id_tag 1
#define alox_TapEvent_client_id_tag 1 #define alox_TapEvent_client_id_tag 1
#define alox_TapEvent_valid_tag 2 #define alox_TapEvent_valid_tag 2
#define alox_TapEvent_kind_tag 3 #define alox_TapEvent_kind_tag 3
#define alox_TapEvent_age_ms_tag 4 #define alox_TapEvent_age_ms_tag 4
#define alox_TapSnapshotResponse_events_tag 1 #define alox_CacheClientAccel_valid_tag 1
#define alox_CacheStatusResponse_accel_tag 1 #define alox_CacheClientAccel_x_tag 2
#define alox_CacheStatusResponse_taps_tag 2 #define alox_CacheClientAccel_y_tag 3
#define alox_CacheClientAccel_z_tag 4
#define alox_CacheClientAccel_age_ms_tag 5
#define alox_CacheClientTap_kind_tag 1
#define alox_CacheClientTap_age_ms_tag 2
#define alox_CacheClientStatus_client_id_tag 1
#define alox_CacheClientStatus_accel_tag 2
#define alox_CacheClientStatus_tap_tag 3
#define alox_CacheStatusResponse_clients_tag 1
#define alox_EspNowUnicastTestRequest_client_id_tag 1 #define alox_EspNowUnicastTestRequest_client_id_tag 1
#define alox_EspNowUnicastTestRequest_seq_tag 2 #define alox_EspNowUnicastTestRequest_seq_tag 2
#define alox_EspNowUnicastTestResponse_success_tag 1 #define alox_EspNowUnicastTestResponse_success_tag 1
@ -664,16 +663,12 @@ extern "C" {
#define alox_UartMessage_espnow_find_me_response_tag 20 #define alox_UartMessage_espnow_find_me_response_tag 20
#define alox_UartMessage_restart_request_tag 21 #define alox_UartMessage_restart_request_tag 21
#define alox_UartMessage_restart_response_tag 22 #define alox_UartMessage_restart_response_tag 22
#define alox_UartMessage_accel_snapshot_request_tag 23
#define alox_UartMessage_accel_snapshot_response_tag 24
#define alox_UartMessage_accel_stream_request_tag 25 #define alox_UartMessage_accel_stream_request_tag 25
#define alox_UartMessage_accel_stream_response_tag 26 #define alox_UartMessage_accel_stream_response_tag 26
#define alox_UartMessage_battery_status_request_tag 27 #define alox_UartMessage_battery_status_request_tag 27
#define alox_UartMessage_battery_status_response_tag 28 #define alox_UartMessage_battery_status_response_tag 28
#define alox_UartMessage_tap_notify_request_tag 29 #define alox_UartMessage_tap_notify_request_tag 29
#define alox_UartMessage_tap_notify_response_tag 30 #define alox_UartMessage_tap_notify_response_tag 30
#define alox_UartMessage_tap_snapshot_request_tag 31
#define alox_UartMessage_tap_snapshot_response_tag 32
#define alox_UartMessage_cache_status_request_tag 33 #define alox_UartMessage_cache_status_request_tag 33
#define alox_UartMessage_cache_status_response_tag 34 #define alox_UartMessage_cache_status_response_tag 34
@ -701,16 +696,12 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,espnow_find_me_request,payload.espno
X(a, STATIC, ONEOF, MESSAGE, (payload,espnow_find_me_response,payload.espnow_find_me_response), 20) \ X(a, STATIC, ONEOF, MESSAGE, (payload,espnow_find_me_response,payload.espnow_find_me_response), 20) \
X(a, STATIC, ONEOF, MESSAGE, (payload,restart_request,payload.restart_request), 21) \ X(a, STATIC, ONEOF, MESSAGE, (payload,restart_request,payload.restart_request), 21) \
X(a, STATIC, ONEOF, MESSAGE, (payload,restart_response,payload.restart_response), 22) \ X(a, STATIC, ONEOF, MESSAGE, (payload,restart_response,payload.restart_response), 22) \
X(a, STATIC, ONEOF, MESSAGE, (payload,accel_snapshot_request,payload.accel_snapshot_request), 23) \
X(a, STATIC, ONEOF, MESSAGE, (payload,accel_snapshot_response,payload.accel_snapshot_response), 24) \
X(a, STATIC, ONEOF, MESSAGE, (payload,accel_stream_request,payload.accel_stream_request), 25) \ X(a, STATIC, ONEOF, MESSAGE, (payload,accel_stream_request,payload.accel_stream_request), 25) \
X(a, STATIC, ONEOF, MESSAGE, (payload,accel_stream_response,payload.accel_stream_response), 26) \ X(a, STATIC, ONEOF, MESSAGE, (payload,accel_stream_response,payload.accel_stream_response), 26) \
X(a, STATIC, ONEOF, MESSAGE, (payload,battery_status_request,payload.battery_status_request), 27) \ X(a, STATIC, ONEOF, MESSAGE, (payload,battery_status_request,payload.battery_status_request), 27) \
X(a, STATIC, ONEOF, MESSAGE, (payload,battery_status_response,payload.battery_status_response), 28) \ X(a, STATIC, ONEOF, MESSAGE, (payload,battery_status_response,payload.battery_status_response), 28) \
X(a, STATIC, ONEOF, MESSAGE, (payload,tap_notify_request,payload.tap_notify_request), 29) \ X(a, STATIC, ONEOF, MESSAGE, (payload,tap_notify_request,payload.tap_notify_request), 29) \
X(a, STATIC, ONEOF, MESSAGE, (payload,tap_notify_response,payload.tap_notify_response), 30) \ X(a, STATIC, ONEOF, MESSAGE, (payload,tap_notify_response,payload.tap_notify_response), 30) \
X(a, STATIC, ONEOF, MESSAGE, (payload,tap_snapshot_request,payload.tap_snapshot_request), 31) \
X(a, STATIC, ONEOF, MESSAGE, (payload,tap_snapshot_response,payload.tap_snapshot_response), 32) \
X(a, STATIC, ONEOF, MESSAGE, (payload,cache_status_request,payload.cache_status_request), 33) \ X(a, STATIC, ONEOF, MESSAGE, (payload,cache_status_request,payload.cache_status_request), 33) \
X(a, STATIC, ONEOF, MESSAGE, (payload,cache_status_response,payload.cache_status_response), 34) X(a, STATIC, ONEOF, MESSAGE, (payload,cache_status_response,payload.cache_status_response), 34)
#define alox_UartMessage_CALLBACK NULL #define alox_UartMessage_CALLBACK NULL
@ -736,16 +727,12 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,cache_status_response,payload.cache_
#define alox_UartMessage_payload_espnow_find_me_response_MSGTYPE alox_EspNowFindMeResponse #define alox_UartMessage_payload_espnow_find_me_response_MSGTYPE alox_EspNowFindMeResponse
#define alox_UartMessage_payload_restart_request_MSGTYPE alox_RestartRequest #define alox_UartMessage_payload_restart_request_MSGTYPE alox_RestartRequest
#define alox_UartMessage_payload_restart_response_MSGTYPE alox_RestartResponse #define alox_UartMessage_payload_restart_response_MSGTYPE alox_RestartResponse
#define alox_UartMessage_payload_accel_snapshot_request_MSGTYPE alox_AccelSnapshotRequest
#define alox_UartMessage_payload_accel_snapshot_response_MSGTYPE alox_AccelSnapshotResponse
#define alox_UartMessage_payload_accel_stream_request_MSGTYPE alox_AccelStreamRequest #define alox_UartMessage_payload_accel_stream_request_MSGTYPE alox_AccelStreamRequest
#define alox_UartMessage_payload_accel_stream_response_MSGTYPE alox_AccelStreamResponse #define alox_UartMessage_payload_accel_stream_response_MSGTYPE alox_AccelStreamResponse
#define alox_UartMessage_payload_battery_status_request_MSGTYPE alox_BatteryStatusRequest #define alox_UartMessage_payload_battery_status_request_MSGTYPE alox_BatteryStatusRequest
#define alox_UartMessage_payload_battery_status_response_MSGTYPE alox_BatteryStatusResponse #define alox_UartMessage_payload_battery_status_response_MSGTYPE alox_BatteryStatusResponse
#define alox_UartMessage_payload_tap_notify_request_MSGTYPE alox_TapNotifyRequest #define alox_UartMessage_payload_tap_notify_request_MSGTYPE alox_TapNotifyRequest
#define alox_UartMessage_payload_tap_notify_response_MSGTYPE alox_TapNotifyResponse #define alox_UartMessage_payload_tap_notify_response_MSGTYPE alox_TapNotifyResponse
#define alox_UartMessage_payload_tap_snapshot_request_MSGTYPE alox_TapSnapshotRequest
#define alox_UartMessage_payload_tap_snapshot_response_MSGTYPE alox_TapSnapshotResponse
#define alox_UartMessage_payload_cache_status_request_MSGTYPE alox_CacheStatusRequest #define alox_UartMessage_payload_cache_status_request_MSGTYPE alox_CacheStatusRequest
#define alox_UartMessage_payload_cache_status_response_MSGTYPE alox_CacheStatusResponse #define alox_UartMessage_payload_cache_status_response_MSGTYPE alox_CacheStatusResponse
@ -862,11 +849,6 @@ X(a, STATIC, REPEATED, MESSAGE, samples, 2)
#define alox_BatteryStatusResponse_DEFAULT NULL #define alox_BatteryStatusResponse_DEFAULT NULL
#define alox_BatteryStatusResponse_samples_MSGTYPE alox_BatterySample #define alox_BatteryStatusResponse_samples_MSGTYPE alox_BatterySample
#define alox_AccelSnapshotRequest_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, client_id, 1)
#define alox_AccelSnapshotRequest_CALLBACK NULL
#define alox_AccelSnapshotRequest_DEFAULT NULL
#define alox_AccelSample_FIELDLIST(X, a) \ #define alox_AccelSample_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, client_id, 1) \ X(a, STATIC, SINGULAR, UINT32, client_id, 1) \
X(a, STATIC, SINGULAR, BOOL, valid, 2) \ X(a, STATIC, SINGULAR, BOOL, valid, 2) \
@ -877,12 +859,6 @@ X(a, STATIC, SINGULAR, UINT32, age_ms, 6)
#define alox_AccelSample_CALLBACK NULL #define alox_AccelSample_CALLBACK NULL
#define alox_AccelSample_DEFAULT NULL #define alox_AccelSample_DEFAULT NULL
#define alox_AccelSnapshotResponse_FIELDLIST(X, a) \
X(a, STATIC, REPEATED, MESSAGE, samples, 1)
#define alox_AccelSnapshotResponse_CALLBACK NULL
#define alox_AccelSnapshotResponse_DEFAULT NULL
#define alox_AccelSnapshotResponse_samples_MSGTYPE alox_AccelSample
#define alox_TapNotifyRequest_FIELDLIST(X, a) \ #define alox_TapNotifyRequest_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, BOOL, write, 1) \ X(a, STATIC, SINGULAR, BOOL, write, 1) \
X(a, STATIC, SINGULAR, UINT32, client_id, 2) \ X(a, STATIC, SINGULAR, UINT32, client_id, 2) \
@ -903,11 +879,6 @@ X(a, STATIC, SINGULAR, BOOL, triple, 6)
#define alox_TapNotifyResponse_CALLBACK NULL #define alox_TapNotifyResponse_CALLBACK NULL
#define alox_TapNotifyResponse_DEFAULT NULL #define alox_TapNotifyResponse_DEFAULT NULL
#define alox_TapSnapshotRequest_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, client_id, 1)
#define alox_TapSnapshotRequest_CALLBACK NULL
#define alox_TapSnapshotRequest_DEFAULT NULL
#define alox_TapEvent_FIELDLIST(X, a) \ #define alox_TapEvent_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, client_id, 1) \ X(a, STATIC, SINGULAR, UINT32, client_id, 1) \
X(a, STATIC, SINGULAR, BOOL, valid, 2) \ X(a, STATIC, SINGULAR, BOOL, valid, 2) \
@ -916,24 +887,40 @@ X(a, STATIC, SINGULAR, UINT32, age_ms, 4)
#define alox_TapEvent_CALLBACK NULL #define alox_TapEvent_CALLBACK NULL
#define alox_TapEvent_DEFAULT NULL #define alox_TapEvent_DEFAULT NULL
#define alox_TapSnapshotResponse_FIELDLIST(X, a) \
X(a, STATIC, REPEATED, MESSAGE, events, 1)
#define alox_TapSnapshotResponse_CALLBACK NULL
#define alox_TapSnapshotResponse_DEFAULT NULL
#define alox_TapSnapshotResponse_events_MSGTYPE alox_TapEvent
#define alox_CacheStatusRequest_FIELDLIST(X, a) \ #define alox_CacheStatusRequest_FIELDLIST(X, a) \
#define alox_CacheStatusRequest_CALLBACK NULL #define alox_CacheStatusRequest_CALLBACK NULL
#define alox_CacheStatusRequest_DEFAULT NULL #define alox_CacheStatusRequest_DEFAULT NULL
#define alox_CacheClientAccel_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, BOOL, valid, 1) \
X(a, STATIC, SINGULAR, SINT32, x, 2) \
X(a, STATIC, SINGULAR, SINT32, y, 3) \
X(a, STATIC, SINGULAR, SINT32, z, 4) \
X(a, STATIC, SINGULAR, UINT32, age_ms, 5)
#define alox_CacheClientAccel_CALLBACK NULL
#define alox_CacheClientAccel_DEFAULT NULL
#define alox_CacheClientTap_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UENUM, kind, 1) \
X(a, STATIC, SINGULAR, UINT32, age_ms, 2)
#define alox_CacheClientTap_CALLBACK NULL
#define alox_CacheClientTap_DEFAULT NULL
#define alox_CacheClientStatus_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, client_id, 1) \
X(a, STATIC, OPTIONAL, MESSAGE, accel, 2) \
X(a, STATIC, OPTIONAL, MESSAGE, tap, 3)
#define alox_CacheClientStatus_CALLBACK NULL
#define alox_CacheClientStatus_DEFAULT NULL
#define alox_CacheClientStatus_accel_MSGTYPE alox_CacheClientAccel
#define alox_CacheClientStatus_tap_MSGTYPE alox_CacheClientTap
#define alox_CacheStatusResponse_FIELDLIST(X, a) \ #define alox_CacheStatusResponse_FIELDLIST(X, a) \
X(a, STATIC, REPEATED, MESSAGE, accel, 1) \ X(a, STATIC, REPEATED, MESSAGE, clients, 1)
X(a, STATIC, REPEATED, MESSAGE, taps, 2)
#define alox_CacheStatusResponse_CALLBACK NULL #define alox_CacheStatusResponse_CALLBACK NULL
#define alox_CacheStatusResponse_DEFAULT NULL #define alox_CacheStatusResponse_DEFAULT NULL
#define alox_CacheStatusResponse_accel_MSGTYPE alox_AccelSample #define alox_CacheStatusResponse_clients_MSGTYPE alox_CacheClientStatus
#define alox_CacheStatusResponse_taps_MSGTYPE alox_TapEvent
#define alox_EspNowUnicastTestRequest_FIELDLIST(X, a) \ #define alox_EspNowUnicastTestRequest_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, client_id, 1) \ X(a, STATIC, SINGULAR, UINT32, client_id, 1) \
@ -1059,15 +1046,14 @@ extern const pb_msgdesc_t alox_BatteryStatusRequest_msg;
extern const pb_msgdesc_t alox_LipoReading_msg; extern const pb_msgdesc_t alox_LipoReading_msg;
extern const pb_msgdesc_t alox_BatterySample_msg; extern const pb_msgdesc_t alox_BatterySample_msg;
extern const pb_msgdesc_t alox_BatteryStatusResponse_msg; extern const pb_msgdesc_t alox_BatteryStatusResponse_msg;
extern const pb_msgdesc_t alox_AccelSnapshotRequest_msg;
extern const pb_msgdesc_t alox_AccelSample_msg; extern const pb_msgdesc_t alox_AccelSample_msg;
extern const pb_msgdesc_t alox_AccelSnapshotResponse_msg;
extern const pb_msgdesc_t alox_TapNotifyRequest_msg; extern const pb_msgdesc_t alox_TapNotifyRequest_msg;
extern const pb_msgdesc_t alox_TapNotifyResponse_msg; extern const pb_msgdesc_t alox_TapNotifyResponse_msg;
extern const pb_msgdesc_t alox_TapSnapshotRequest_msg;
extern const pb_msgdesc_t alox_TapEvent_msg; extern const pb_msgdesc_t alox_TapEvent_msg;
extern const pb_msgdesc_t alox_TapSnapshotResponse_msg;
extern const pb_msgdesc_t alox_CacheStatusRequest_msg; extern const pb_msgdesc_t alox_CacheStatusRequest_msg;
extern const pb_msgdesc_t alox_CacheClientAccel_msg;
extern const pb_msgdesc_t alox_CacheClientTap_msg;
extern const pb_msgdesc_t alox_CacheClientStatus_msg;
extern const pb_msgdesc_t alox_CacheStatusResponse_msg; extern const pb_msgdesc_t alox_CacheStatusResponse_msg;
extern const pb_msgdesc_t alox_EspNowUnicastTestRequest_msg; extern const pb_msgdesc_t alox_EspNowUnicastTestRequest_msg;
extern const pb_msgdesc_t alox_EspNowUnicastTestResponse_msg; extern const pb_msgdesc_t alox_EspNowUnicastTestResponse_msg;
@ -1102,15 +1088,14 @@ extern const pb_msgdesc_t alox_OtaSlaveProgressResponse_msg;
#define alox_LipoReading_fields &alox_LipoReading_msg #define alox_LipoReading_fields &alox_LipoReading_msg
#define alox_BatterySample_fields &alox_BatterySample_msg #define alox_BatterySample_fields &alox_BatterySample_msg
#define alox_BatteryStatusResponse_fields &alox_BatteryStatusResponse_msg #define alox_BatteryStatusResponse_fields &alox_BatteryStatusResponse_msg
#define alox_AccelSnapshotRequest_fields &alox_AccelSnapshotRequest_msg
#define alox_AccelSample_fields &alox_AccelSample_msg #define alox_AccelSample_fields &alox_AccelSample_msg
#define alox_AccelSnapshotResponse_fields &alox_AccelSnapshotResponse_msg
#define alox_TapNotifyRequest_fields &alox_TapNotifyRequest_msg #define alox_TapNotifyRequest_fields &alox_TapNotifyRequest_msg
#define alox_TapNotifyResponse_fields &alox_TapNotifyResponse_msg #define alox_TapNotifyResponse_fields &alox_TapNotifyResponse_msg
#define alox_TapSnapshotRequest_fields &alox_TapSnapshotRequest_msg
#define alox_TapEvent_fields &alox_TapEvent_msg #define alox_TapEvent_fields &alox_TapEvent_msg
#define alox_TapSnapshotResponse_fields &alox_TapSnapshotResponse_msg
#define alox_CacheStatusRequest_fields &alox_CacheStatusRequest_msg #define alox_CacheStatusRequest_fields &alox_CacheStatusRequest_msg
#define alox_CacheClientAccel_fields &alox_CacheClientAccel_msg
#define alox_CacheClientTap_fields &alox_CacheClientTap_msg
#define alox_CacheClientStatus_fields &alox_CacheClientStatus_msg
#define alox_CacheStatusResponse_fields &alox_CacheStatusResponse_msg #define alox_CacheStatusResponse_fields &alox_CacheStatusResponse_msg
#define alox_EspNowUnicastTestRequest_fields &alox_EspNowUnicastTestRequest_msg #define alox_EspNowUnicastTestRequest_fields &alox_EspNowUnicastTestRequest_msg
#define alox_EspNowUnicastTestResponse_fields &alox_EspNowUnicastTestResponse_msg #define alox_EspNowUnicastTestResponse_fields &alox_EspNowUnicastTestResponse_msg
@ -1139,16 +1124,17 @@ extern const pb_msgdesc_t alox_OtaSlaveProgressResponse_msg;
#define alox_AccelDeadzoneRequest_size 16 #define alox_AccelDeadzoneRequest_size 16
#define alox_AccelDeadzoneResponse_size 20 #define alox_AccelDeadzoneResponse_size 20
#define alox_AccelSample_size 32 #define alox_AccelSample_size 32
#define alox_AccelSnapshotRequest_size 6
#define alox_AccelSnapshotResponse_size 544
#define alox_AccelStreamRequest_size 12 #define alox_AccelStreamRequest_size 12
#define alox_AccelStreamResponse_size 16 #define alox_AccelStreamResponse_size 16
#define alox_Ack_size 0 #define alox_Ack_size 0
#define alox_BatterySample_size 32 #define alox_BatterySample_size 32
#define alox_BatteryStatusRequest_size 8 #define alox_BatteryStatusRequest_size 8
#define alox_BatteryStatusResponse_size 580 #define alox_BatteryStatusResponse_size 580
#define alox_CacheClientAccel_size 26
#define alox_CacheClientStatus_size 44
#define alox_CacheClientTap_size 8
#define alox_CacheStatusRequest_size 0 #define alox_CacheStatusRequest_size 0
#define alox_CacheStatusResponse_size 832 #define alox_CacheStatusResponse_size 736
#define alox_ClientInput_size 22 #define alox_ClientInput_size 22
#define alox_EspNowFindMeRequest_size 6 #define alox_EspNowFindMeRequest_size 6
#define alox_EspNowFindMeResponse_size 8 #define alox_EspNowFindMeResponse_size 8
@ -1169,8 +1155,6 @@ extern const pb_msgdesc_t alox_OtaSlaveProgressResponse_msg;
#define alox_TapEvent_size 16 #define alox_TapEvent_size 16
#define alox_TapNotifyRequest_size 16 #define alox_TapNotifyRequest_size 16
#define alox_TapNotifyResponse_size 20 #define alox_TapNotifyResponse_size 20
#define alox_TapSnapshotRequest_size 6
#define alox_TapSnapshotResponse_size 288
#ifdef __cplusplus #ifdef __cplusplus
} /* extern "C" */ } /* extern "C" */

View File

@ -22,11 +22,11 @@ enum MessageType {
OTA_SLAVE_PROGRESS = 21; OTA_SLAVE_PROGRESS = 21;
FIND_ME = 22; FIND_ME = 22;
RESTART = 23; RESTART = 23;
ACCEL_SNAPSHOT = 24; reserved 24;
ACCEL_STREAM = 25; ACCEL_STREAM = 25;
BATTERY_STATUS = 26; BATTERY_STATUS = 26;
TAP_NOTIFY = 27; TAP_NOTIFY = 27;
TAP_SNAPSHOT = 28; reserved 28;
/** Combined cached accel + tap poll (one UART round-trip, ~16 ms cadence). */ /** Combined cached accel + tap poll (one UART round-trip, ~16 ms cadence). */
CACHE_STATUS = 29; CACHE_STATUS = 29;
} }
@ -55,16 +55,12 @@ message UartMessage {
EspNowFindMeResponse espnow_find_me_response = 20; EspNowFindMeResponse espnow_find_me_response = 20;
RestartRequest restart_request = 21; RestartRequest restart_request = 21;
RestartResponse restart_response = 22; RestartResponse restart_response = 22;
AccelSnapshotRequest accel_snapshot_request = 23;
AccelSnapshotResponse accel_snapshot_response = 24;
AccelStreamRequest accel_stream_request = 25; AccelStreamRequest accel_stream_request = 25;
AccelStreamResponse accel_stream_response = 26; AccelStreamResponse accel_stream_response = 26;
BatteryStatusRequest battery_status_request = 27; BatteryStatusRequest battery_status_request = 27;
BatteryStatusResponse battery_status_response = 28; BatteryStatusResponse battery_status_response = 28;
TapNotifyRequest tap_notify_request = 29; TapNotifyRequest tap_notify_request = 29;
TapNotifyResponse tap_notify_response = 30; TapNotifyResponse tap_notify_response = 30;
TapSnapshotRequest tap_snapshot_request = 31;
TapSnapshotResponse tap_snapshot_response = 32;
CacheStatusRequest cache_status_request = 33; CacheStatusRequest cache_status_request = 33;
CacheStatusResponse cache_status_response = 34; CacheStatusResponse cache_status_response = 34;
} }
@ -174,12 +170,7 @@ message BatteryStatusResponse {
repeated BatterySample samples = 2 [(nanopb).max_count = 17]; repeated BatterySample samples = 2 [(nanopb).max_count = 17];
} }
// Host master: read cached accel samples from slaves (only while stream enabled). /** Legacy host-side sample shape (dashboard helpers); use CACHE_STATUS on the wire. */
// client_id 0 = all registered slaves; otherwise one slave.
message AccelSnapshotRequest {
uint32 client_id = 1;
}
message AccelSample { message AccelSample {
uint32 client_id = 1; uint32 client_id = 1;
bool valid = 2; bool valid = 2;
@ -190,10 +181,6 @@ message AccelSample {
uint32 age_ms = 6; uint32 age_ms = 6;
} }
message AccelSnapshotResponse {
repeated AccelSample samples = 1 [(nanopb).max_count = 16];
}
/** Host → master: enable/disable tap ESP-NOW notify per slave (single/double/triple). */ /** Host → master: enable/disable tap ESP-NOW notify per slave (single/double/triple). */
message TapNotifyRequest { message TapNotifyRequest {
bool write = 1; bool write = 1;
@ -220,11 +207,7 @@ enum TapKind {
TAP_TRIPLE = 3; TAP_TRIPLE = 3;
} }
/** Host → master: read cached tap events (discarded after reply or when age > 16 ms). */ /** Legacy tap event shape (dashboard helpers); use CACHE_STATUS on the wire. */
message TapSnapshotRequest {
uint32 client_id = 1;
}
message TapEvent { message TapEvent {
uint32 client_id = 1; uint32 client_id = 1;
bool valid = 2; bool valid = 2;
@ -232,18 +215,34 @@ message TapEvent {
uint32 age_ms = 4; uint32 age_ms = 4;
} }
message TapSnapshotResponse {
repeated TapEvent events = 1 [(nanopb).max_count = 16];
}
/** Host → master: one-shot read of subscribed cached slave data (no request body). */ /** Host → master: one-shot read of subscribed cached slave data (no request body). */
message CacheStatusRequest {} message CacheStatusRequest {}
/** Accel slice inside CACHE_STATUS (no client_id — use parent CacheClientStatus). */
message CacheClientAccel {
bool valid = 1;
sint32 x = 2;
sint32 y = 3;
sint32 z = 4;
uint32 age_ms = 5;
}
/** Tap slice inside CACHE_STATUS; only present when a pending tap was consumed. */
message CacheClientTap {
TapKind kind = 1;
uint32 age_ms = 2;
}
/** One slave with accel and/or tap notify enabled; only subscribed fields are set. */
message CacheClientStatus {
uint32 client_id = 1;
CacheClientAccel accel = 2;
CacheClientTap tap = 3;
}
message CacheStatusResponse { message CacheStatusResponse {
/** Slaves with accel_stream_enabled. */ /** Slaves with accel_stream and/or tap notify; omitted fields are not subscribed. */
repeated AccelSample accel = 1 [(nanopb).max_count = 16]; repeated CacheClientStatus clients = 1 [(nanopb).max_count = 16];
/** Slaves with any tap notify flag; pending taps are consumed (like TAP_SNAPSHOT). */
repeated TapEvent taps = 2 [(nanopb).max_count = 16];
} }
message EspNowUnicastTestRequest { message EspNowUnicastTestRequest {