Add BMA456 tap detection with ESP-NOW notify and host snapshot API.
Slaves forward configured tap kinds to the master; goTool exposes CLI, dashboard, REST, and WebSocket with separate notify vs receive and 2s display cache. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
3cb0b5bbe9
commit
a8d4d42920
118
goTool/README.md
118
goTool/README.md
@ -26,6 +26,8 @@ go run . -port /dev/ttyUSB0 clients
|
||||
| `clients` | `0x04` | Lists slaves registered on the master via ESP-NOW |
|
||||
| `deadzone` | `0x06` | Get/set accelerometer deadzone LSB (`-set`, `-value`, `-client`, `-all`) |
|
||||
| `accel` | `0x18` | Cached slave accel snapshot from master (`ACCEL_SNAPSHOT`); alias `accel-read` |
|
||||
| `tap-notify` | `0x1b` | Get/set which tap kinds (single/double/triple) notify via ESP-NOW (`-set`, `-client`, `-all`, `-single`, `-double`, `-triple`) |
|
||||
| `tap` | `0x1c` | Cached tap snapshot from master (`TAP_SNAPSHOT`); events ≤16 ms old |
|
||||
| `unicast-test` | `0x07` | Sends ESP-NOW unicast test to one slave (`-client`, `-seq`) |
|
||||
| `test` | — | Run an automated scenario (JSON configs under `testdata/`) |
|
||||
| `serve` | — | Web dashboard at `http://localhost:8080` (WebSocket live updates) |
|
||||
@ -68,26 +70,42 @@ make gotool-serve PORT=/dev/ttyUSB0
|
||||
|
||||
Open [http://localhost:8080](http://localhost:8080) — shows master firmware info and the ESP-NOW client table from `CLIENT_INFO`.
|
||||
|
||||
**Tap (dashboard):** two independent controls per slave:
|
||||
|
||||
| Column | Meaning |
|
||||
|--------|---------|
|
||||
| Tap-Notify (S/D/T) | Which tap kinds the **slave** sends to the master over ESP-NOW (UART `TAP_NOTIFY`) — does **not** poll UART |
|
||||
| Tap (An/Aus) | Host **receive**: poll master tap cache (~16 ms) and show last tap for **≥2 s** |
|
||||
|
||||
Enable notify first, then turn receive on to see events. Same split as the external WebSocket API (`set_tap_notify` vs `set_tap_stream`).
|
||||
|
||||
### External API (second HTTP server)
|
||||
|
||||
`serve` starts a separate listener (default **`:8081`**, disable with `-api-addr ""`) for external programs. It shares the same UART connection as the dashboard.
|
||||
|
||||
| Endpoint | Description |
|
||||
|----------|-------------|
|
||||
| `GET /` or `GET /api/v1/` | JSON service info (`default_interval_ms`, min/max, `serial_port`) |
|
||||
| `WebSocket /ws` | Per-connection accel receive + interval; slave ESP-NOW stream control |
|
||||
| `GET /` or `GET /api/v1/` | JSON service info (`default_interval_ms`, min/max, `serial_port`, `tap_display_min_ms`) |
|
||||
| `WebSocket /ws` | Per-connection accel/tap receive + interval; slave ESP-NOW accel/tap control |
|
||||
|
||||
Two layers:
|
||||
**Accel** — two layers:
|
||||
|
||||
1. **`set_stream`** — this WebSocket connection: whether to receive `accel` JSON and at what poll rate (1 ms … 10 s per client; server UART poll uses the minimum among active subscribers).
|
||||
2. **`set_accel_stream`** — firmware: whether a slave sends accel to the master over ESP-NOW (16 ms on the pod).
|
||||
|
||||
Polling runs only when at least one connection has `receive_accel: true` **and** at least one slave streams (via `set_accel_stream` or dashboard `:8080`).
|
||||
Accel polling runs only when at least one connection has `receive_accel: true` **and** at least one slave streams (via `set_accel_stream` or dashboard `:8080`).
|
||||
|
||||
**Hello** (on connect; accel is off until `set_stream`):
|
||||
**Tap** — also two layers (notify alone does **not** poll UART):
|
||||
|
||||
1. **`set_tap_notify`** — firmware: which tap kinds (single/double/triple) the slave sends to the master over ESP-NOW.
|
||||
2. **`set_tap_stream`** — this WebSocket connection: poll `TAP_SNAPSHOT` and push `tap` JSON. Events stay visible for **`tap_display_min_ms`** (2000 ms) after first sight.
|
||||
|
||||
Tap polling runs only when at least one connection has `receive_tap: true` (via `set_tap_stream`).
|
||||
|
||||
**Hello** (on connect; accel/tap receive off until `set_stream` / `set_tap_stream`):
|
||||
|
||||
```json
|
||||
{"type":"hello","serial_port":"/dev/ttyUSB0","interval_ms":16,"commands":["set_stream","get_stream","set_accel_stream","get_accel_stream","set_led_ring","get_battery"]}
|
||||
{"type":"hello","serial_port":"/dev/ttyUSB0","interval_ms":16,"tap_display_min_ms":2000,"note":"set_tap_notify configures slave S/D/T only; set_tap_stream enables tap polling/push","commands":["set_stream","get_stream","set_accel_stream","get_accel_stream","set_tap_stream","get_tap_stream","set_tap_notify","get_tap_notify","set_led_ring","get_battery"]}
|
||||
```
|
||||
|
||||
**Receive accel on this connection** (optional `interval_ms`, default from `-accel-interval`):
|
||||
@ -125,6 +143,38 @@ Reply:
|
||||
|
||||
Reply: `{"type":"led_ring_status","success":true,"slaves_updated":2,...}`
|
||||
|
||||
**Tap notify** (slave ESP-NOW config; per `client_id`, or `all_clients`):
|
||||
|
||||
```json
|
||||
{"type":"set_tap_notify","client_id":16,"single":true,"double_tap":false,"triple":false}
|
||||
{"type":"get_tap_notify","client_id":16}
|
||||
```
|
||||
|
||||
Reply:
|
||||
|
||||
```json
|
||||
{"type":"tap_notify_status","client_id":16,"success":true,"single":true,"double_tap":false,"triple":false}
|
||||
```
|
||||
|
||||
**Receive tap on this connection** (optional `interval_ms`; default from `-accel-interval`):
|
||||
|
||||
```json
|
||||
{"type":"set_tap_stream","enable":true,"interval_ms":16}
|
||||
{"type":"get_tap_stream"}
|
||||
```
|
||||
|
||||
Reply:
|
||||
|
||||
```json
|
||||
{"type":"tap_stream_status","receive_tap":true,"interval_ms":16,"success":true}
|
||||
```
|
||||
|
||||
**Tap events** (only to connections with `receive_tap: true`; each event shown ≥2 s):
|
||||
|
||||
```json
|
||||
{"type":"tap","port":"/dev/ttyUSB0","success":true,"events":[{"client_id":16,"kind":"single","age_ms":3,"shown_at_ms":1717000000123}]}
|
||||
```
|
||||
|
||||
**Accel** (only to connections with `receive_accel: true`, and only while slaves stream):
|
||||
|
||||
```json
|
||||
@ -156,6 +206,28 @@ async def main():
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
Tap example (notify first, then enable stream on this connection):
|
||||
|
||||
```python
|
||||
import asyncio, json, websockets
|
||||
|
||||
async def main():
|
||||
async with websockets.connect("ws://127.0.0.1:8081/ws") as ws:
|
||||
print(await ws.recv()) # hello
|
||||
await ws.send(json.dumps({"type": "set_tap_notify", "client_id": 16,
|
||||
"single": True, "double_tap": False, "triple": False}))
|
||||
print(await ws.recv()) # tap_notify_status
|
||||
await ws.send(json.dumps({"type": "set_tap_stream", "enable": True, "interval_ms": 16}))
|
||||
print(await ws.recv()) # tap_stream_status
|
||||
while True:
|
||||
msg = json.loads(await ws.recv())
|
||||
if msg.get("type") == "tap" and msg.get("events"):
|
||||
for e in msg["events"]:
|
||||
print(e["client_id"], e["kind"], "age", e.get("age_ms"))
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
If the UART device is unplugged or the port disappears, `serve` keeps running and retries on each poll interval; the UI shows **UART off** until the port is available again.
|
||||
|
||||
The dashboard can configure nodes using the same UART commands as the CLI:
|
||||
@ -167,7 +239,7 @@ The dashboard can configure nodes using the same UART commands as the CLI:
|
||||
| Alle Slaves | per-slave ESP-NOW (Master bleibt unverändert; CLI `-all` setzt auch den Master) |
|
||||
| Unicast test | `unicast-test -client ID` |
|
||||
|
||||
HTTP API (used by the web UI): `GET/POST /api/deadzone`, `GET/PUT /api/clients/{id}/accel-stream`, `POST /api/accel-stream` (legacy / `all_clients`), `GET/POST /api/battery`, `POST /api/led-ring`, `POST /api/unicast-test`, `POST /api/find-me`, `POST /api/restart`, `POST /api/ota` (multipart field `firmware`, max 2 MiB).
|
||||
HTTP API (used by the web UI): `GET/POST /api/deadzone`, `GET/PUT /api/clients/{id}/accel-stream`, `POST /api/accel-stream` (legacy / `all_clients`), `GET/PUT /api/clients/{id}/tap-notify`, `PUT /api/clients/{id}/tap-receive`, `GET/POST /api/tap-notify`, `GET /api/tap-snapshot`, `GET/POST /api/battery`, `POST /api/led-ring`, `POST /api/unicast-test`, `POST /api/find-me`, `POST /api/restart`, `POST /api/ota` (multipart field `firmware`, max 2 MiB).
|
||||
|
||||
**LED ring** (`POST /api/led-ring` and WebSocket `set_led_ring` on `:8081`):
|
||||
|
||||
@ -203,6 +275,38 @@ Content-Type: application/json
|
||||
|
||||
Enable all slaves: `POST /api/accel-stream` with `{"write":true,"enable":true,"all_clients":true}`.
|
||||
|
||||
**Tap notify per slave** (slave → master ESP-NOW; does not start host polling):
|
||||
|
||||
```http
|
||||
GET /api/clients/16/tap-notify
|
||||
→ {"client_id":16,"success":true,"single":false,"double_tap":false,"triple":false}
|
||||
|
||||
PUT /api/clients/16/tap-notify
|
||||
Content-Type: application/json
|
||||
{"single": true, "double_tap": false, "triple": false}
|
||||
→ {"client_id":16,"success":true,"slaves_updated":1,"single":true,"double_tap":false,"triple":false}
|
||||
```
|
||||
|
||||
All slaves: `POST /api/tap-notify` with `{"single":true,"double_tap":false,"triple":false,"all_clients":true}`.
|
||||
|
||||
**Tap receive** (host-side; dashboard polls `TAP_SNAPSHOT` while enabled):
|
||||
|
||||
```http
|
||||
PUT /api/clients/16/tap-receive
|
||||
Content-Type: application/json
|
||||
{"enable": true}
|
||||
→ {"client_id":16,"enabled":true,"success":true}
|
||||
```
|
||||
|
||||
One-shot read (no receive flag): `GET /api/tap-snapshot?client_id=16` → `{"events":[{"client_id":16,"kind":"single","age_ms":4}]}`.
|
||||
|
||||
CLI:
|
||||
|
||||
```bash
|
||||
go run . -port /dev/ttyUSB0 tap-notify -client 16 -set -single
|
||||
go run . -port /dev/ttyUSB0 tap -client 16
|
||||
```
|
||||
|
||||
| UI / API | Behaviour |
|
||||
|----------|-----------|
|
||||
| Firmware OTA card | Same as `ota` CLI; WebSocket `ota_progress` with `step` `master` (UART) then `slaves` (ESP-NOW) |
|
||||
|
||||
@ -67,8 +67,9 @@ type otaAPIResponse struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func mountServeAPI(mux *http.ServeMux, link *managedSerial, hub *wsHub, streamCtl *accelStreamCtl) {
|
||||
func mountServeAPI(mux *http.ServeMux, link *managedSerial, hub *wsHub, streamCtl *accelStreamCtl, tapCtl *tapNotifyCtl) {
|
||||
mountAccelStreamAPI(mux, link, hub, streamCtl)
|
||||
mountTapAPI(mux, link, hub, tapCtl)
|
||||
mountLedRingAPI(mux, link)
|
||||
mountBatteryAPI(mux, link)
|
||||
mux.HandleFunc("/api/deadzone", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@ -17,6 +17,8 @@ const (
|
||||
defaultAccelStreamInterval = 16 * time.Millisecond
|
||||
minAPIStreamInterval = 1 * time.Millisecond
|
||||
maxAPIStreamInterval = 10 * time.Second
|
||||
// How long tap events stay in API push/cache after first sight (matches dashboard).
|
||||
apiTapDisplayMinMs = 2000
|
||||
)
|
||||
|
||||
// AccelClientSample is one slave's cached accel on the master.
|
||||
@ -29,12 +31,14 @@ type AccelClientSample struct {
|
||||
AgeMs uint32 `json:"age_ms,omitempty"`
|
||||
}
|
||||
|
||||
// AccelStreamMessage is sent to external WebSocket clients.
|
||||
// AccelStreamMessage is sent to external WebSocket clients (hello + accel samples).
|
||||
type AccelStreamMessage struct {
|
||||
Type string `json:"type"` // "hello" | "accel"
|
||||
Serial string `json:"serial_port,omitempty"`
|
||||
IntervalMs int `json:"interval_ms,omitempty"`
|
||||
TapDisplayMinMs int `json:"tap_display_min_ms,omitempty"`
|
||||
Commands []string `json:"commands,omitempty"`
|
||||
Note string `json:"note,omitempty"`
|
||||
|
||||
T int64 `json:"t,omitempty"` // Unix nanoseconds
|
||||
Success bool `json:"success,omitempty"`
|
||||
@ -61,11 +65,54 @@ type AccelStreamStatusMessage struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// TapClientEvent is one tap visible to API clients (fresh or within tap_display_min_ms).
|
||||
type TapClientEvent struct {
|
||||
ClientID uint32 `json:"client_id"`
|
||||
Valid bool `json:"valid"`
|
||||
Kind string `json:"kind,omitempty"` // single | double | triple
|
||||
AgeMs uint32 `json:"age_ms,omitempty"`
|
||||
ShownAtMs int64 `json:"shown_at_ms,omitempty"` // Unix ms when API first saw this tap
|
||||
}
|
||||
|
||||
// TapStreamMessage is pushed to external WebSocket clients when receive_tap is on.
|
||||
type TapStreamMessage struct {
|
||||
Type string `json:"type"` // "tap"
|
||||
T int64 `json:"t,omitempty"`
|
||||
Success bool `json:"success,omitempty"`
|
||||
Events []TapClientEvent `json:"events,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// TapStreamStatusMessage is the reply to set_tap_stream / get_tap_stream (this connection).
|
||||
type TapStreamStatusMessage struct {
|
||||
Type string `json:"type"` // "tap_stream_status"
|
||||
ReceiveTap bool `json:"receive_tap"`
|
||||
IntervalMs int `json:"interval_ms"`
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// TapNotifyStatusMessage is the reply to set_tap_notify / get_tap_notify (slave).
|
||||
type TapNotifyStatusMessage struct {
|
||||
Type string `json:"type"` // "tap_notify_status"
|
||||
ClientID uint32 `json:"client_id"`
|
||||
Single bool `json:"single"`
|
||||
DoubleTap bool `json:"double_tap"`
|
||||
Triple bool `json:"triple"`
|
||||
Success bool `json:"success"`
|
||||
SlavesUpdated uint32 `json:"slaves_updated,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type accelWSCommand struct {
|
||||
Type string `json:"type"`
|
||||
ClientID uint32 `json:"client_id"`
|
||||
Enable *bool `json:"enable"`
|
||||
IntervalMs *int `json:"interval_ms"`
|
||||
Single *bool `json:"single"`
|
||||
DoubleTap *bool `json:"double_tap"`
|
||||
Triple *bool `json:"triple"`
|
||||
AllClients bool `json:"all_clients"`
|
||||
}
|
||||
|
||||
type APIInfoResponse struct {
|
||||
@ -76,14 +123,22 @@ type APIInfoResponse struct {
|
||||
DefaultIntervalMs int `json:"default_interval_ms"`
|
||||
MinIntervalMs int `json:"min_interval_ms"`
|
||||
MaxIntervalMs int `json:"max_interval_ms"`
|
||||
TapDisplayMinMs int `json:"tap_display_min_ms"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type cachedTapEvent struct {
|
||||
kind string
|
||||
shownAt time.Time
|
||||
}
|
||||
|
||||
type wsSubscriber struct {
|
||||
conn *websocket.Conn
|
||||
receiveAccel bool
|
||||
receiveTap bool
|
||||
interval time.Duration
|
||||
lastSent time.Time
|
||||
lastAccelSent time.Time
|
||||
lastTapSent time.Time
|
||||
}
|
||||
|
||||
type accelStreamHub struct {
|
||||
@ -91,6 +146,7 @@ type accelStreamHub struct {
|
||||
clients map[*websocket.Conn]*wsSubscriber
|
||||
defaultInterval time.Duration
|
||||
configChanged chan struct{}
|
||||
recentTaps map[uint32]cachedTapEvent
|
||||
}
|
||||
|
||||
func newAccelStreamHub(defaultInterval time.Duration) *accelStreamHub {
|
||||
@ -132,8 +188,11 @@ func (h *accelStreamHub) register(conn *websocket.Conn, portName string) *wsSubs
|
||||
Type: "hello",
|
||||
Serial: portName,
|
||||
IntervalMs: int(h.defaultInterval / time.Millisecond),
|
||||
TapDisplayMinMs: apiTapDisplayMinMs,
|
||||
Note: "set_tap_notify configures slave S/D/T only; set_tap_stream enables tap polling/push",
|
||||
Commands: []string{
|
||||
"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",
|
||||
},
|
||||
}
|
||||
@ -146,6 +205,16 @@ func (h *accelStreamHub) register(conn *websocket.Conn, portName string) *wsSubs
|
||||
func (h *accelStreamHub) unregister(conn *websocket.Conn) {
|
||||
h.mu.Lock()
|
||||
delete(h.clients, conn)
|
||||
anyTap := false
|
||||
for _, sub := range h.clients {
|
||||
if sub.receiveTap {
|
||||
anyTap = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !anyTap {
|
||||
h.recentTaps = nil
|
||||
}
|
||||
h.mu.Unlock()
|
||||
h.notifyConfigChanged()
|
||||
}
|
||||
@ -161,12 +230,23 @@ func (h *accelStreamHub) anyWantsAccel() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *accelStreamHub) anyWantsTap() bool {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
for _, sub := range h.clients {
|
||||
if sub.receiveTap {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *accelStreamHub) minWantedInterval() time.Duration {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
var min time.Duration
|
||||
for _, sub := range h.clients {
|
||||
if !sub.receiveAccel {
|
||||
if !sub.receiveAccel && !sub.receiveTap {
|
||||
continue
|
||||
}
|
||||
if min == 0 || sub.interval < min {
|
||||
@ -208,6 +288,78 @@ func (h *accelStreamHub) getStream(sub *wsSubscriber) StreamStatusMessage {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *accelStreamHub) setTapStream(sub *wsSubscriber, enable bool, intervalMs *int) TapStreamStatusMessage {
|
||||
h.mu.Lock()
|
||||
sub.receiveTap = enable
|
||||
if !enable {
|
||||
h.recentTaps = nil
|
||||
}
|
||||
if intervalMs != nil {
|
||||
sub.interval = clampAPIInterval(time.Duration(*intervalMs) * time.Millisecond)
|
||||
}
|
||||
ms := int(sub.interval / time.Millisecond)
|
||||
h.mu.Unlock()
|
||||
h.notifyConfigChanged()
|
||||
|
||||
return TapStreamStatusMessage{
|
||||
Type: "tap_stream_status",
|
||||
ReceiveTap: enable,
|
||||
IntervalMs: ms,
|
||||
Success: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *accelStreamHub) getTapStream(sub *wsSubscriber) TapStreamStatusMessage {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
return TapStreamStatusMessage{
|
||||
Type: "tap_stream_status",
|
||||
ReceiveTap: sub.receiveTap,
|
||||
IntervalMs: int(sub.interval / time.Millisecond),
|
||||
Success: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *accelStreamHub) ingestTapEvents(incoming []TapClientEvent) []TapClientEvent {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
if h.recentTaps == nil {
|
||||
h.recentTaps = make(map[uint32]cachedTapEvent)
|
||||
}
|
||||
for _, e := range incoming {
|
||||
if !e.Valid || e.Kind == "" {
|
||||
continue
|
||||
}
|
||||
h.recentTaps[e.ClientID] = cachedTapEvent{kind: e.Kind, shownAt: now}
|
||||
}
|
||||
return h.activeTapEventsLocked(now)
|
||||
}
|
||||
|
||||
func (h *accelStreamHub) activeTapEventsLocked(now time.Time) []TapClientEvent {
|
||||
if len(h.recentTaps) == 0 {
|
||||
return nil
|
||||
}
|
||||
cutoff := now.Add(-apiTapDisplayMinMs * time.Millisecond)
|
||||
out := make([]TapClientEvent, 0, len(h.recentTaps))
|
||||
for id, ev := range h.recentTaps {
|
||||
if ev.shownAt.Before(cutoff) {
|
||||
delete(h.recentTaps, id)
|
||||
continue
|
||||
}
|
||||
shownAtMs := ev.shownAt.UnixMilli()
|
||||
out = append(out, TapClientEvent{
|
||||
ClientID: id,
|
||||
Valid: true,
|
||||
Kind: ev.kind,
|
||||
AgeMs: uint32(now.Sub(ev.shownAt).Milliseconds()),
|
||||
ShownAtMs: shownAtMs,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (h *accelStreamHub) deliver(msg AccelStreamMessage) {
|
||||
data, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
@ -221,10 +373,10 @@ func (h *accelStreamHub) deliver(msg AccelStreamMessage) {
|
||||
if !sub.receiveAccel {
|
||||
continue
|
||||
}
|
||||
if !sub.lastSent.IsZero() && now.Sub(sub.lastSent) < sub.interval {
|
||||
if !sub.lastAccelSent.IsZero() && now.Sub(sub.lastAccelSent) < sub.interval {
|
||||
continue
|
||||
}
|
||||
sub.lastSent = now
|
||||
sub.lastAccelSent = now
|
||||
if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
|
||||
delete(h.clients, conn)
|
||||
_ = conn.Close()
|
||||
@ -232,7 +384,31 @@ func (h *accelStreamHub) deliver(msg AccelStreamMessage) {
|
||||
}
|
||||
}
|
||||
|
||||
func runAccelStreamer(link *managedSerial, hub *accelStreamHub, dash *wsHub, ctl *accelStreamCtl, stop <-chan struct{}) {
|
||||
func (h *accelStreamHub) deliverTap(msg TapStreamMessage) {
|
||||
data, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
for conn, sub := range h.clients {
|
||||
if !sub.receiveTap {
|
||||
continue
|
||||
}
|
||||
if !sub.lastTapSent.IsZero() && now.Sub(sub.lastTapSent) < sub.interval {
|
||||
continue
|
||||
}
|
||||
sub.lastTapSent = now
|
||||
if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
|
||||
delete(h.clients, conn)
|
||||
_ = conn.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runAccelStreamer(link *managedSerial, hub *accelStreamHub, dash *wsHub, ctl *accelStreamCtl, tapCtl *tapNotifyCtl, stop <-chan struct{}) {
|
||||
var ticker *time.Ticker
|
||||
var tick <-chan time.Time
|
||||
|
||||
@ -258,13 +434,8 @@ func runAccelStreamer(link *managedSerial, hub *accelStreamHub, dash *wsHub, ctl
|
||||
case <-hub.configChanged:
|
||||
resetTicker()
|
||||
case <-tick:
|
||||
if !hub.anyWantsAccel() {
|
||||
continue
|
||||
}
|
||||
if !accelStreamPollingActive(dash, ctl) {
|
||||
continue
|
||||
}
|
||||
now := time.Now().UnixNano()
|
||||
if hub.anyWantsAccel() && accelStreamPollingActive(dash, ctl) {
|
||||
resp, err := link.readAccelSnapshotPoll(0)
|
||||
if errors.Is(err, errUARTBusy) {
|
||||
hub.deliver(AccelStreamMessage{
|
||||
@ -273,17 +444,14 @@ func runAccelStreamer(link *managedSerial, hub *accelStreamHub, dash *wsHub, ctl
|
||||
Success: false,
|
||||
Error: "uart busy",
|
||||
})
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
} else if err != nil {
|
||||
hub.deliver(AccelStreamMessage{
|
||||
Type: "accel",
|
||||
T: now,
|
||||
Success: false,
|
||||
Error: err.Error(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
clients := make([]AccelClientSample, 0, len(resp.GetSamples()))
|
||||
for _, s := range resp.GetSamples() {
|
||||
clients = append(clients, AccelClientSample{
|
||||
@ -303,6 +471,50 @@ func runAccelStreamer(link *managedSerial, hub *accelStreamHub, dash *wsHub, ctl
|
||||
})
|
||||
}
|
||||
}
|
||||
if hub.anyWantsTap() {
|
||||
nowMs := time.Now().UnixNano()
|
||||
resp, err := link.readTapSnapshotPoll(0)
|
||||
if errors.Is(err, errUARTBusy) {
|
||||
hub.deliverTap(TapStreamMessage{
|
||||
Type: "tap",
|
||||
T: nowMs,
|
||||
Success: false,
|
||||
Error: "uart busy",
|
||||
})
|
||||
} else if err != nil {
|
||||
hub.deliverTap(TapStreamMessage{
|
||||
Type: "tap",
|
||||
T: nowMs,
|
||||
Success: false,
|
||||
Error: err.Error(),
|
||||
})
|
||||
} else {
|
||||
fresh := make([]TapClientEvent, 0, len(resp.GetEvents()))
|
||||
for _, e := range resp.GetEvents() {
|
||||
if !e.GetValid() {
|
||||
continue
|
||||
}
|
||||
fresh = append(fresh, TapClientEvent{
|
||||
ClientID: e.GetClientId(),
|
||||
Valid: true,
|
||||
Kind: tapKindLabelPB(e.GetKind()),
|
||||
AgeMs: e.GetAgeMs(),
|
||||
})
|
||||
}
|
||||
events := hub.ingestTapEvents(fresh)
|
||||
if len(events) == 0 {
|
||||
continue
|
||||
}
|
||||
hub.deliverTap(TapStreamMessage{
|
||||
Type: "tap",
|
||||
T: nowMs,
|
||||
Success: true,
|
||||
Events: events,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func accelStreamPollingActive(dash *wsHub, ctl *accelStreamCtl) bool {
|
||||
@ -354,7 +566,63 @@ func writeAccelStreamStatus(conn *websocket.Conn, out accelStreamAPIResponse) {
|
||||
_ = conn.WriteMessage(websocket.TextMessage, data)
|
||||
}
|
||||
|
||||
func handleAccelWSCommand(conn *websocket.Conn, sub *wsSubscriber, data []byte, link *managedSerial, dash *wsHub, ctl *accelStreamCtl, hub *accelStreamHub) {
|
||||
func writeTapStreamStatus(conn *websocket.Conn, msg TapStreamStatusMessage) {
|
||||
data, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = conn.WriteMessage(websocket.TextMessage, data)
|
||||
}
|
||||
|
||||
func writeTapNotifyStatus(conn *websocket.Conn, out tapNotifyAPIResponse) {
|
||||
msg := TapNotifyStatusMessage{
|
||||
Type: "tap_notify_status",
|
||||
ClientID: out.ClientID,
|
||||
Single: out.Single,
|
||||
DoubleTap: out.DoubleTap,
|
||||
Triple: out.Triple,
|
||||
Success: out.Success,
|
||||
SlavesUpdated: out.SlavesUpdated,
|
||||
Error: out.Error,
|
||||
}
|
||||
data, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = conn.WriteMessage(websocket.TextMessage, data)
|
||||
}
|
||||
|
||||
func applyTapNotifyClientWS(link *managedSerial, dash *wsHub, tapCtl *tapNotifyCtl, clientID uint32, single, doubleTap, triple bool) tapNotifyAPIResponse {
|
||||
resp, err := link.TapNotify(&pb.TapNotifyRequest{
|
||||
Write: true,
|
||||
ClientId: clientID,
|
||||
Single: single,
|
||||
DoubleTap: doubleTap,
|
||||
Triple: triple,
|
||||
})
|
||||
if err != nil {
|
||||
return tapNotifyAPIResponse{ClientID: clientID, Error: err.Error()}
|
||||
}
|
||||
out := tapNotifyAPIResponse{
|
||||
ClientID: resp.GetClientId(),
|
||||
Success: resp.GetSuccess(),
|
||||
SlavesUpdated: resp.GetSlavesUpdated(),
|
||||
Single: resp.GetSingle(),
|
||||
DoubleTap: resp.GetDoubleTap(),
|
||||
Triple: resp.GetTriple(),
|
||||
}
|
||||
if resp.GetSuccess() {
|
||||
if tapCtl != nil {
|
||||
tapCtl.Set(clientID, single, doubleTap, triple)
|
||||
}
|
||||
if dash != nil {
|
||||
dash.patchClientTapNotify(clientID, single, doubleTap, triple)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func handleAccelWSCommand(conn *websocket.Conn, sub *wsSubscriber, data []byte, link *managedSerial, dash *wsHub, ctl *accelStreamCtl, tapCtl *tapNotifyCtl, hub *accelStreamHub) {
|
||||
var cmd accelWSCommand
|
||||
if err := json.Unmarshal(data, &cmd); err != nil {
|
||||
writeStreamStatus(conn, StreamStatusMessage{Type: "stream_status", Error: "invalid JSON"})
|
||||
@ -414,6 +682,79 @@ func handleAccelWSCommand(conn *websocket.Conn, sub *wsSubscriber, data []byte,
|
||||
Success: resp.GetSuccess(),
|
||||
})
|
||||
|
||||
case "set_tap_stream":
|
||||
if cmd.Enable == nil {
|
||||
writeTapStreamStatus(conn, TapStreamStatusMessage{
|
||||
Type: "tap_stream_status",
|
||||
Error: "enable required",
|
||||
})
|
||||
return
|
||||
}
|
||||
writeTapStreamStatus(conn, hub.setTapStream(sub, *cmd.Enable, cmd.IntervalMs))
|
||||
|
||||
case "get_tap_stream":
|
||||
writeTapStreamStatus(conn, hub.getTapStream(sub))
|
||||
|
||||
case "set_tap_notify":
|
||||
if cmd.AllClients {
|
||||
if cmd.Single == nil || cmd.DoubleTap == nil || cmd.Triple == nil {
|
||||
writeTapNotifyStatus(conn, tapNotifyAPIResponse{Error: "single, double_tap, triple required"})
|
||||
return
|
||||
}
|
||||
updated, err := applyTapNotifyAll(link, dash, tapCtl, *cmd.Single, *cmd.DoubleTap, *cmd.Triple)
|
||||
if err != nil {
|
||||
writeTapNotifyStatus(conn, tapNotifyAPIResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
writeTapNotifyStatus(conn, tapNotifyAPIResponse{
|
||||
Success: updated > 0,
|
||||
SlavesUpdated: updated,
|
||||
Single: *cmd.Single,
|
||||
DoubleTap: *cmd.DoubleTap,
|
||||
Triple: *cmd.Triple,
|
||||
})
|
||||
return
|
||||
}
|
||||
if cmd.ClientID == 0 {
|
||||
writeTapNotifyStatus(conn, tapNotifyAPIResponse{Error: "client_id required"})
|
||||
return
|
||||
}
|
||||
if cmd.Single == nil || cmd.DoubleTap == nil || cmd.Triple == nil {
|
||||
writeTapNotifyStatus(conn, tapNotifyAPIResponse{
|
||||
ClientID: cmd.ClientID,
|
||||
Error: "single, double_tap, triple required",
|
||||
})
|
||||
return
|
||||
}
|
||||
writeTapNotifyStatus(conn, applyTapNotifyClientWS(link, dash, tapCtl, cmd.ClientID, *cmd.Single, *cmd.DoubleTap, *cmd.Triple))
|
||||
|
||||
case "get_tap_notify":
|
||||
if cmd.ClientID == 0 {
|
||||
writeTapNotifyStatus(conn, tapNotifyAPIResponse{Error: "client_id required"})
|
||||
return
|
||||
}
|
||||
resp, err := link.TapNotifyPoll(&pb.TapNotifyRequest{
|
||||
Write: false,
|
||||
ClientId: cmd.ClientID,
|
||||
})
|
||||
if err != nil {
|
||||
writeTapNotifyStatus(conn, tapNotifyAPIResponse{
|
||||
ClientID: cmd.ClientID,
|
||||
Error: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
if tapCtl != nil {
|
||||
tapCtl.Set(cmd.ClientID, resp.GetSingle(), resp.GetDoubleTap(), resp.GetTriple())
|
||||
}
|
||||
writeTapNotifyStatus(conn, tapNotifyAPIResponse{
|
||||
ClientID: cmd.ClientID,
|
||||
Success: resp.GetSuccess(),
|
||||
Single: resp.GetSingle(),
|
||||
DoubleTap: resp.GetDoubleTap(),
|
||||
Triple: resp.GetTriple(),
|
||||
})
|
||||
|
||||
case "set_led_ring":
|
||||
var body ledRingAPIRequest
|
||||
if err := json.Unmarshal(data, &body); err != nil {
|
||||
@ -440,12 +781,12 @@ func handleAccelWSCommand(conn *websocket.Conn, sub *wsSubscriber, data []byte,
|
||||
default:
|
||||
writeStreamStatus(conn, StreamStatusMessage{
|
||||
Type: "stream_status",
|
||||
Error: "unknown type (set_stream, get_stream, set_accel_stream, get_accel_stream, set_led_ring, get_battery)",
|
||||
Error: "unknown type (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)",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func serveExternalWS(conn *websocket.Conn, link *managedSerial, dash *wsHub, ctl *accelStreamCtl, portName string, hub *accelStreamHub) {
|
||||
func serveExternalWS(conn *websocket.Conn, link *managedSerial, dash *wsHub, ctl *accelStreamCtl, tapCtl *tapNotifyCtl, portName string, hub *accelStreamHub) {
|
||||
sub := hub.register(conn, portName)
|
||||
defer hub.unregister(conn)
|
||||
defer conn.Close()
|
||||
@ -455,11 +796,11 @@ func serveExternalWS(conn *websocket.Conn, link *managedSerial, dash *wsHub, ctl
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
handleAccelWSCommand(conn, sub, data, link, dash, ctl, hub)
|
||||
handleAccelWSCommand(conn, sub, data, link, dash, ctl, tapCtl, hub)
|
||||
}
|
||||
}
|
||||
|
||||
func mountExternalAPI(mux *http.ServeMux, portName string, defaultInterval time.Duration, hub *accelStreamHub, link *managedSerial, dash *wsHub, ctl *accelStreamCtl) {
|
||||
func mountExternalAPI(mux *http.ServeMux, portName string, defaultInterval time.Duration, hub *accelStreamHub, link *managedSerial, dash *wsHub, ctl *accelStreamCtl, tapCtl *tapNotifyCtl) {
|
||||
defMs := int(defaultInterval / time.Millisecond)
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" && r.URL.Path != "/api/v1" && r.URL.Path != "/api/v1/" {
|
||||
@ -478,7 +819,8 @@ func mountExternalAPI(mux *http.ServeMux, portName string, defaultInterval time.
|
||||
DefaultIntervalMs: defMs,
|
||||
MinIntervalMs: int(minAPIStreamInterval / time.Millisecond),
|
||||
MaxIntervalMs: int(maxAPIStreamInterval / time.Millisecond),
|
||||
Description: "WebSocket: accel stream + set_led_ring (modes: clear, color, progress, digit, blink, find-me)",
|
||||
TapDisplayMinMs: apiTapDisplayMinMs,
|
||||
Description: "WebSocket: set_accel_stream + set_stream for accel; set_tap_notify (slave S/D/T) then set_tap_stream for tap events (shown ≥2s)",
|
||||
})
|
||||
})
|
||||
|
||||
@ -488,22 +830,22 @@ func mountExternalAPI(mux *http.ServeMux, portName string, defaultInterval time.
|
||||
log.Printf("api websocket upgrade: %v", err)
|
||||
return
|
||||
}
|
||||
serveExternalWS(conn, link, dash, ctl, portName, hub)
|
||||
serveExternalWS(conn, link, dash, ctl, tapCtl, portName, hub)
|
||||
})
|
||||
}
|
||||
|
||||
func runAPIServer(portName string, link *managedSerial, addr string, defaultInterval time.Duration, dash *wsHub, ctl *accelStreamCtl, stop <-chan struct{}) *http.Server {
|
||||
func runAPIServer(portName string, link *managedSerial, addr string, defaultInterval time.Duration, dash *wsHub, ctl *accelStreamCtl, tapCtl *tapNotifyCtl, stop <-chan struct{}) *http.Server {
|
||||
hub := newAccelStreamHub(defaultInterval)
|
||||
go runAccelStreamer(link, hub, dash, ctl, stop)
|
||||
go runAccelStreamer(link, hub, dash, ctl, tapCtl, stop)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mountExternalAPI(mux, portName, defaultInterval, hub, link, dash, ctl)
|
||||
mountExternalAPI(mux, portName, defaultInterval, hub, link, dash, ctl, tapCtl)
|
||||
mountLedRingAPI(mux, link)
|
||||
mountBatteryAPI(mux, link)
|
||||
|
||||
srv := &http.Server{Addr: addr, Handler: mux}
|
||||
go func() {
|
||||
log.Printf("external API http://localhost%s WebSocket ws://localhost%s/ws (default accel interval %s, per-client via set_stream)",
|
||||
log.Printf("external API http://localhost%s WebSocket ws://localhost%s/ws (default stream interval %s, per-client via set_stream / set_tap_stream)",
|
||||
addr, addr, defaultInterval.String())
|
||||
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Printf("external API server: %v", err)
|
||||
|
||||
253
goTool/api_tap.go
Normal file
253
goTool/api_tap.go
Normal file
@ -0,0 +1,253 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"powerpod/gotool/pb"
|
||||
)
|
||||
|
||||
type tapNotifyAPIRequest struct {
|
||||
Write bool `json:"write"`
|
||||
ClientID uint32 `json:"client_id"`
|
||||
AllClients bool `json:"all_clients"`
|
||||
Single bool `json:"single"`
|
||||
DoubleTap bool `json:"double_tap"`
|
||||
Triple bool `json:"triple"`
|
||||
}
|
||||
|
||||
type tapNotifyAPIResponse struct {
|
||||
ClientID uint32 `json:"client_id"`
|
||||
Success bool `json:"success"`
|
||||
SlavesUpdated uint32 `json:"slaves_updated"`
|
||||
Single bool `json:"single"`
|
||||
DoubleTap bool `json:"double_tap"`
|
||||
Triple bool `json:"triple"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type tapSnapshotAPIResponse struct {
|
||||
Events []tapEventView `json:"events"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type tapEventView struct {
|
||||
ClientID uint32 `json:"client_id"`
|
||||
Kind string `json:"kind"`
|
||||
AgeMs uint32 `json:"age_ms"`
|
||||
}
|
||||
|
||||
type tapReceiveAPIResponse struct {
|
||||
ClientID uint32 `json:"client_id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func mountTapAPI(mux *http.ServeMux, link *managedSerial, hub *wsHub, tapCtl *tapNotifyCtl) {
|
||||
mux.HandleFunc("GET /api/clients/{clientID}/tap-notify", func(w http.ResponseWriter, r *http.Request) {
|
||||
clientID, err := parsePathClientID(r)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, tapNotifyAPIResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
serveTapNotifyGet(w, clientID, link)
|
||||
})
|
||||
mux.HandleFunc("PUT /api/clients/{clientID}/tap-notify", func(w http.ResponseWriter, r *http.Request) {
|
||||
clientID, err := parsePathClientID(r)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, tapNotifyAPIResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
serveClientTapNotifyPut(w, r, clientID, link, hub, tapCtl)
|
||||
})
|
||||
mux.HandleFunc("PUT /api/clients/{clientID}/tap-receive", func(w http.ResponseWriter, r *http.Request) {
|
||||
clientID, err := parsePathClientID(r)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, tapReceiveAPIResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
serveClientTapReceivePut(w, r, clientID, hub)
|
||||
})
|
||||
|
||||
mux.HandleFunc("/api/tap-notify", func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
serveTapNotifyGetQuery(w, r, link)
|
||||
case http.MethodPost:
|
||||
serveTapNotifyPost(w, r, link, hub, tapCtl)
|
||||
default:
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
})
|
||||
|
||||
mux.HandleFunc("/api/tap-snapshot", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
serveTapSnapshotGet(w, r, link)
|
||||
})
|
||||
}
|
||||
|
||||
func applyTapNotifyClient(link *managedSerial, hub *wsHub, tapCtl *tapNotifyCtl, clientID uint32, single, doubleTap, triple bool) tapNotifyAPIResponse {
|
||||
return applyTapNotifyClientWS(link, hub, tapCtl, clientID, single, doubleTap, triple)
|
||||
}
|
||||
|
||||
func serveTapNotifyGet(w http.ResponseWriter, clientID uint32, link *managedSerial) {
|
||||
resp, err := link.TapNotifyPoll(&pb.TapNotifyRequest{
|
||||
Write: false,
|
||||
ClientId: clientID,
|
||||
})
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusServiceUnavailable, tapNotifyAPIResponse{
|
||||
ClientID: clientID,
|
||||
Error: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, tapNotifyAPIResponse{
|
||||
ClientID: resp.GetClientId(),
|
||||
Success: resp.GetSuccess(),
|
||||
Single: resp.GetSingle(),
|
||||
DoubleTap: resp.GetDoubleTap(),
|
||||
Triple: resp.GetTriple(),
|
||||
})
|
||||
}
|
||||
|
||||
func serveTapNotifyGetQuery(w http.ResponseWriter, r *http.Request, link *managedSerial) {
|
||||
clientID, err := parseUintQuery(r, "client_id", 0)
|
||||
if err != nil || clientID == 0 {
|
||||
writeJSON(w, http.StatusBadRequest, tapNotifyAPIResponse{Error: "client_id required"})
|
||||
return
|
||||
}
|
||||
serveTapNotifyGet(w, clientID, link)
|
||||
}
|
||||
|
||||
type clientTapNotifyBody struct {
|
||||
Single bool `json:"single"`
|
||||
DoubleTap bool `json:"double_tap"`
|
||||
Triple bool `json:"triple"`
|
||||
}
|
||||
|
||||
func serveClientTapNotifyPut(w http.ResponseWriter, r *http.Request, clientID uint32, link *managedSerial, hub *wsHub, tapCtl *tapNotifyCtl) {
|
||||
var body clientTapNotifyBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, tapNotifyAPIResponse{Error: "invalid JSON"})
|
||||
return
|
||||
}
|
||||
out := applyTapNotifyClient(link, hub, tapCtl, clientID, body.Single, body.DoubleTap, body.Triple)
|
||||
status := http.StatusOK
|
||||
if out.Error != "" || !out.Success {
|
||||
status = http.StatusServiceUnavailable
|
||||
}
|
||||
writeJSON(w, status, out)
|
||||
}
|
||||
|
||||
func serveTapNotifyPost(w http.ResponseWriter, r *http.Request, link *managedSerial, hub *wsHub, tapCtl *tapNotifyCtl) {
|
||||
var body tapNotifyAPIRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, tapNotifyAPIResponse{Error: "invalid JSON"})
|
||||
return
|
||||
}
|
||||
|
||||
if body.AllClients {
|
||||
updated, err := applyTapNotifyAll(link, hub, tapCtl, body.Single, body.DoubleTap, body.Triple)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusServiceUnavailable, tapNotifyAPIResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, tapNotifyAPIResponse{
|
||||
Success: updated > 0,
|
||||
SlavesUpdated: updated,
|
||||
Single: body.Single,
|
||||
DoubleTap: body.DoubleTap,
|
||||
Triple: body.Triple,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if body.ClientID == 0 {
|
||||
writeJSON(w, http.StatusBadRequest, tapNotifyAPIResponse{Error: "client_id required"})
|
||||
return
|
||||
}
|
||||
|
||||
out := applyTapNotifyClient(link, hub, tapCtl, body.ClientID, body.Single, body.DoubleTap, body.Triple)
|
||||
status := http.StatusOK
|
||||
if out.Error != "" || !out.Success {
|
||||
status = http.StatusServiceUnavailable
|
||||
}
|
||||
writeJSON(w, status, out)
|
||||
}
|
||||
|
||||
func serveClientTapReceivePut(w http.ResponseWriter, r *http.Request, clientID uint32, hub *wsHub) {
|
||||
var body struct {
|
||||
Enable bool `json:"enable"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, tapReceiveAPIResponse{Error: "invalid JSON"})
|
||||
return
|
||||
}
|
||||
if hub != nil {
|
||||
hub.patchClientTapReceive(clientID, body.Enable)
|
||||
}
|
||||
writeJSON(w, http.StatusOK, tapReceiveAPIResponse{
|
||||
ClientID: clientID,
|
||||
Enabled: body.Enable,
|
||||
Success: true,
|
||||
})
|
||||
}
|
||||
|
||||
func applyTapNotifyAll(link *managedSerial, hub *wsHub, tapCtl *tapNotifyCtl, single, doubleTap, triple bool) (uint32, error) {
|
||||
resp, err := link.TapNotify(&pb.TapNotifyRequest{
|
||||
Write: true,
|
||||
AllClients: true,
|
||||
Single: single,
|
||||
DoubleTap: doubleTap,
|
||||
Triple: triple,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !resp.GetSuccess() {
|
||||
return 0, fmt.Errorf("tap notify not applied to any slave")
|
||||
}
|
||||
if hub != nil || tapCtl != nil {
|
||||
clients, _ := link.listClientsPoll()
|
||||
for _, c := range clients {
|
||||
if tapCtl != nil {
|
||||
tapCtl.Set(c.GetId(), single, doubleTap, triple)
|
||||
}
|
||||
if hub != nil {
|
||||
hub.patchClientTapNotify(c.GetId(), single, doubleTap, triple)
|
||||
}
|
||||
}
|
||||
}
|
||||
return resp.GetSlavesUpdated(), nil
|
||||
}
|
||||
|
||||
func serveTapSnapshotGet(w http.ResponseWriter, r *http.Request, link *managedSerial) {
|
||||
clientID, err := parseUintQuery(r, "client_id", 0)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, tapSnapshotAPIResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
resp, err := link.readTapSnapshotPoll(clientID)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusServiceUnavailable, tapSnapshotAPIResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
out := tapSnapshotAPIResponse{Events: make([]tapEventView, 0, len(resp.GetEvents()))}
|
||||
for _, e := range resp.GetEvents() {
|
||||
if !e.GetValid() {
|
||||
continue
|
||||
}
|
||||
out.Events = append(out.Events, tapEventView{
|
||||
ClientID: e.GetClientId(),
|
||||
Kind: tapKindLabel(e.GetKind()),
|
||||
AgeMs: e.GetAgeMs(),
|
||||
})
|
||||
}
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
@ -171,6 +171,64 @@ func (m *managedSerial) GetAccelStream(clientID uint32) (bool, error) {
|
||||
return resp.GetEnabled(), nil
|
||||
}
|
||||
|
||||
func (m *managedSerial) TapNotify(req *pb.TapNotifyRequest) (*pb.TapNotifyResponse, error) {
|
||||
return m.tapNotifyVia(m.withPort, req)
|
||||
}
|
||||
|
||||
func (m *managedSerial) TapNotifyPoll(req *pb.TapNotifyRequest) (*pb.TapNotifyResponse, error) {
|
||||
return m.tapNotifyVia(m.withPortPoll, req)
|
||||
}
|
||||
|
||||
func (m *managedSerial) tapNotifyVia(
|
||||
portFn func(func(*serialPort) error) error,
|
||||
req *pb.TapNotifyRequest,
|
||||
) (*pb.TapNotifyResponse, error) {
|
||||
var resp *pb.TapNotifyResponse
|
||||
err := portFn(func(sp *serialPort) error {
|
||||
var e error
|
||||
resp, e = sp.TapNotify(req)
|
||||
return e
|
||||
})
|
||||
return resp, err
|
||||
}
|
||||
|
||||
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(
|
||||
portFn func(func(*serialPort) error) error,
|
||||
req *pb.AccelStreamRequest,
|
||||
@ -325,6 +383,52 @@ func (s *serialPort) AccelStream(req *pb.AccelStreamRequest) (*pb.AccelStreamRes
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (s *serialPort) TapNotify(req *pb.TapNotifyRequest) (*pb.TapNotifyResponse, error) {
|
||||
msg := &pb.UartMessage{
|
||||
Type: pb.MessageType_TAP_NOTIFY,
|
||||
Payload: &pb.UartMessage_TapNotifyRequest{
|
||||
TapNotifyRequest: req,
|
||||
},
|
||||
}
|
||||
body, err := proto.Marshal(msg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("encode: %w", err)
|
||||
}
|
||||
payload := append([]byte{byte(pb.MessageType_TAP_NOTIFY)}, body...)
|
||||
respPayload, err := s.exchangePayload(payload, "TAP_NOTIFY")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var respMsg pb.UartMessage
|
||||
if err := proto.Unmarshal(respPayload[1:], &respMsg); err != nil {
|
||||
return nil, fmt.Errorf("decode: %w", err)
|
||||
}
|
||||
r := respMsg.GetTapNotifyResponse()
|
||||
if r == nil {
|
||||
return nil, fmt.Errorf("missing tap_notify_response")
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
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) {
|
||||
msg := &pb.UartMessage{
|
||||
Type: pb.MessageType_ACCEL_DEADZONE,
|
||||
|
||||
@ -18,9 +18,17 @@ func runClients(sp *serialPort) error {
|
||||
fmt.Printf("clients (%d):\n", len(clients))
|
||||
for i, c := range clients {
|
||||
mac := hex.EncodeToString(c.GetMac())
|
||||
fmt.Printf(" [%d] id=%d mac=%s ver=%d available=%v used=%v last_ping=%d last_success_ping=%d\n",
|
||||
fmt.Printf(" [%d] id=%d mac=%s ver=%d available=%v used=%v last_ping=%d last_success_ping=%d tap=%s/%s/%s\n",
|
||||
i, c.GetId(), mac, c.GetVersion(), c.GetAvailable(), c.GetUsed(),
|
||||
c.GetLastPing(), c.GetLastSuccessPing())
|
||||
c.GetLastPing(), c.GetLastSuccessPing(),
|
||||
boolFlag(c.GetTapNotifySingle()), boolFlag(c.GetTapNotifyDouble()), boolFlag(c.GetTapNotifyTriple()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func boolFlag(v bool) string {
|
||||
if v {
|
||||
return "on"
|
||||
}
|
||||
return "off"
|
||||
}
|
||||
|
||||
@ -38,20 +38,22 @@ func runServe(portName string, baud int, args []string) error {
|
||||
|
||||
hub := newWSHub()
|
||||
streamCtl := newAccelStreamCtl()
|
||||
tapCtl := newTapNotifyCtl()
|
||||
stop := make(chan struct{})
|
||||
defer close(stop)
|
||||
go runPoller(link, portName, hub, streamCtl, *interval, stop)
|
||||
go runPoller(link, portName, hub, streamCtl, tapCtl, *interval, stop)
|
||||
go runBatteryPoller(link, hub, 5*time.Second, stop)
|
||||
go runAccelDashboardPoller(link, hub, *accelInterval, stop)
|
||||
go runTapDashboardPoller(link, hub, *accelInterval, stop)
|
||||
|
||||
var apiSrv *http.Server
|
||||
if *apiAddr != "" {
|
||||
apiSrv = runAPIServer(portName, link, *apiAddr, *accelInterval, hub, streamCtl, stop)
|
||||
apiSrv = runAPIServer(portName, link, *apiAddr, *accelInterval, hub, streamCtl, tapCtl, stop)
|
||||
defer shutdownAPIServer(apiSrv)
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mountServeAPI(mux, link, hub, streamCtl)
|
||||
mountServeAPI(mux, link, hub, streamCtl, tapCtl)
|
||||
mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := wsUpgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
|
||||
84
goTool/cmd_tap.go
Normal file
84
goTool/cmd_tap.go
Normal file
@ -0,0 +1,84 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"powerpod/gotool/pb"
|
||||
)
|
||||
|
||||
func runTapNotify(sp *serialPort, args []string) error {
|
||||
fs := flag.NewFlagSet("tap-notify", flag.ExitOnError)
|
||||
write := fs.Bool("set", false, "write tap notify flags (default: read)")
|
||||
clientID := fs.Uint("client", 0, "client id (>0 required for read/set one slave)")
|
||||
all := fs.Bool("all", false, "apply to all registered slaves (with -set)")
|
||||
single := fs.Bool("single", false, "notify on single tap")
|
||||
doubleTap := fs.Bool("double", false, "notify on double tap")
|
||||
triple := fs.Bool("triple", false, "notify on triple tap")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !*write && (*all || *clientID == 0) {
|
||||
return fmt.Errorf("read requires -client <id>")
|
||||
}
|
||||
if *write && !*all && *clientID == 0 {
|
||||
return fmt.Errorf("set requires -client <id> or -all")
|
||||
}
|
||||
|
||||
r, err := sp.TapNotify(&pb.TapNotifyRequest{
|
||||
Write: *write,
|
||||
ClientId: uint32(*clientID),
|
||||
AllClients: *all,
|
||||
Single: *single,
|
||||
DoubleTap: *doubleTap,
|
||||
Triple: *triple,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("client_id=%d success=%v slaves_updated=%d single=%v double=%v triple=%v\n",
|
||||
r.GetClientId(), r.GetSuccess(), r.GetSlavesUpdated(),
|
||||
r.GetSingle(), r.GetDoubleTap(), r.GetTriple())
|
||||
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 {
|
||||
switch k {
|
||||
case pb.TapKind_TAP_SINGLE:
|
||||
return "single"
|
||||
case pb.TapKind_TAP_DOUBLE:
|
||||
return "double"
|
||||
case pb.TapKind_TAP_TRIPLE:
|
||||
return "triple"
|
||||
default:
|
||||
return "none"
|
||||
}
|
||||
}
|
||||
@ -41,6 +41,13 @@ type ClientView struct {
|
||||
AccelZ int32 `json:"accel_z"`
|
||||
AccelAgeMs uint32 `json:"accel_age_ms"`
|
||||
AccelStream bool `json:"accel_stream"`
|
||||
TapNotifySingle bool `json:"tap_notify_single"`
|
||||
TapNotifyDouble bool `json:"tap_notify_double"`
|
||||
TapNotifyTriple bool `json:"tap_notify_triple"`
|
||||
/** Host-side: poll master tap cache for this slave (~16 ms). */
|
||||
TapReceive bool `json:"tap_receive"`
|
||||
LastTap string `json:"last_tap,omitempty"`
|
||||
LastTapAt int64 `json:"last_tap_at,omitempty"`
|
||||
Lipo1 lipoReadingJSON `json:"lipo1"`
|
||||
Lipo2 lipoReadingJSON `json:"lipo2"`
|
||||
BatteryAgeMs uint32 `json:"battery_age_ms,omitempty"`
|
||||
@ -71,6 +78,8 @@ func (h *wsHub) setState(st DashboardState) {
|
||||
prev := h.state
|
||||
st.Clients = preserveClientAccel(st.Clients, prev.Clients)
|
||||
st.Clients = preserveClientBattery(st.Clients, prev.Clients)
|
||||
st.Clients = preserveClientTapReceive(st.Clients, prev.Clients)
|
||||
st.Clients = preserveClientTap(st.Clients, prev.Clients)
|
||||
if !st.Master.Lipo1.Valid && !st.Master.Lipo2.Valid {
|
||||
if prev.Master.Lipo1.Valid || prev.Master.Lipo2.Valid {
|
||||
st.Master.Lipo1 = prev.Master.Lipo1
|
||||
@ -206,6 +215,124 @@ func anyClientAccelStream(clients []ClientView) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func anyClientTapNotify(clients []ClientView) bool {
|
||||
for _, c := range clients {
|
||||
if c.TapNotifySingle || c.TapNotifyDouble || c.TapNotifyTriple {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func anyClientTapReceive(clients []ClientView) bool {
|
||||
for _, c := range clients {
|
||||
if c.TapReceive {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func preserveClientTapReceive(newClients, oldClients []ClientView) []ClientView {
|
||||
if len(oldClients) == 0 {
|
||||
return newClients
|
||||
}
|
||||
oldByID := make(map[uint32]ClientView, len(oldClients))
|
||||
for _, c := range oldClients {
|
||||
oldByID[c.ID] = c
|
||||
}
|
||||
out := make([]ClientView, len(newClients))
|
||||
for i, c := range newClients {
|
||||
out[i] = c
|
||||
prev, ok := oldByID[c.ID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
out[i].TapReceive = prev.TapReceive
|
||||
if !prev.TapReceive {
|
||||
continue
|
||||
}
|
||||
if c.LastTap == "" && prev.LastTap != "" {
|
||||
cutoff := time.Now().Add(-clientTapDisplayMinMs * time.Millisecond).UnixMilli()
|
||||
if prev.LastTapAt >= cutoff {
|
||||
out[i].LastTap = prev.LastTap
|
||||
out[i].LastTapAt = prev.LastTapAt
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func tapKindLabelPB(k pb.TapKind) string {
|
||||
switch k {
|
||||
case pb.TapKind_TAP_SINGLE:
|
||||
return "single"
|
||||
case pb.TapKind_TAP_DOUBLE:
|
||||
return "double"
|
||||
case pb.TapKind_TAP_TRIPLE:
|
||||
return "triple"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func applyTapEvents(clients []ClientView, events []*pb.TapEvent) []ClientView {
|
||||
if len(events) == 0 {
|
||||
return clients
|
||||
}
|
||||
byID := make(map[uint32]*pb.TapEvent, len(events))
|
||||
for _, e := range events {
|
||||
if e.GetValid() {
|
||||
byID[e.GetClientId()] = e
|
||||
}
|
||||
}
|
||||
if len(byID) == 0 {
|
||||
return clients
|
||||
}
|
||||
now := time.Now().UnixMilli()
|
||||
out := make([]ClientView, len(clients))
|
||||
for i, c := range clients {
|
||||
out[i] = c
|
||||
if !c.TapReceive {
|
||||
continue
|
||||
}
|
||||
e, ok := byID[c.ID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
out[i].LastTap = tapKindLabelPB(e.GetKind())
|
||||
out[i].LastTapAt = now
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const clientTapDisplayMinMs = 2000
|
||||
|
||||
func preserveClientTap(newClients, oldClients []ClientView) []ClientView {
|
||||
if len(oldClients) == 0 {
|
||||
return newClients
|
||||
}
|
||||
oldByID := make(map[uint32]ClientView, len(oldClients))
|
||||
for _, c := range oldClients {
|
||||
oldByID[c.ID] = c
|
||||
}
|
||||
cutoff := time.Now().Add(-clientTapDisplayMinMs * time.Millisecond).UnixMilli()
|
||||
out := make([]ClientView, len(newClients))
|
||||
for i, c := range newClients {
|
||||
out[i] = c
|
||||
if c.LastTap != "" {
|
||||
continue
|
||||
}
|
||||
prev, ok := oldByID[c.ID]
|
||||
if !ok || prev.LastTap == "" || prev.LastTapAt < cutoff {
|
||||
continue
|
||||
}
|
||||
out[i].LastTap = prev.LastTap
|
||||
out[i].LastTapAt = prev.LastTapAt
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// patchClientAccelStream updates stream flag immediately (e.g. after REST) and pushes WS.
|
||||
func (h *wsHub) patchClientAccelStream(clientID uint32, enabled bool) {
|
||||
h.mu.Lock()
|
||||
@ -246,6 +373,51 @@ func (h *wsHub) anyAccelStreamEnabled() bool {
|
||||
return anyClientAccelStream(h.state.Clients)
|
||||
}
|
||||
|
||||
func (h *wsHub) anyTapNotifyEnabled() bool {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
return anyClientTapNotify(h.state.Clients)
|
||||
}
|
||||
|
||||
func (h *wsHub) anyTapReceiveEnabled() bool {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
return anyClientTapReceive(h.state.Clients)
|
||||
}
|
||||
|
||||
// patchClientTapNotify updates tap notify flags immediately (e.g. after REST) and pushes WS.
|
||||
func (h *wsHub) patchClientTapNotify(clientID uint32, single, doubleTap, triple bool) {
|
||||
h.mu.Lock()
|
||||
for i := range h.state.Clients {
|
||||
if h.state.Clients[i].ID != clientID {
|
||||
continue
|
||||
}
|
||||
h.state.Clients[i].TapNotifySingle = single
|
||||
h.state.Clients[i].TapNotifyDouble = doubleTap
|
||||
h.state.Clients[i].TapNotifyTriple = triple
|
||||
if !single && !doubleTap && !triple {
|
||||
h.state.Clients[i].LastTap = ""
|
||||
h.state.Clients[i].LastTapAt = 0
|
||||
}
|
||||
break
|
||||
}
|
||||
st := h.state
|
||||
st.UpdatedAt = time.Now().Format(time.RFC3339)
|
||||
conns := make([]*websocket.Conn, 0, len(h.clients))
|
||||
for c := range h.clients {
|
||||
conns = append(conns, c)
|
||||
}
|
||||
h.mu.Unlock()
|
||||
|
||||
data, err := json.Marshal(st)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, c := range conns {
|
||||
_ = c.WriteMessage(websocket.TextMessage, data)
|
||||
}
|
||||
}
|
||||
|
||||
// mergeAccel updates cached accel on clients and pushes state to dashboard WebSockets.
|
||||
func (h *wsHub) mergeAccel(samples []*pb.AccelSample) {
|
||||
h.mu.Lock()
|
||||
@ -268,6 +440,60 @@ func (h *wsHub) mergeAccel(samples []*pb.AccelSample) {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *wsHub) patchClientTapReceive(clientID uint32, enabled bool) {
|
||||
h.mu.Lock()
|
||||
for i := range h.state.Clients {
|
||||
if h.state.Clients[i].ID != clientID {
|
||||
continue
|
||||
}
|
||||
h.state.Clients[i].TapReceive = enabled
|
||||
if !enabled {
|
||||
h.state.Clients[i].LastTap = ""
|
||||
h.state.Clients[i].LastTapAt = 0
|
||||
}
|
||||
break
|
||||
}
|
||||
st := h.state
|
||||
st.UpdatedAt = time.Now().Format(time.RFC3339)
|
||||
conns := make([]*websocket.Conn, 0, len(h.clients))
|
||||
for c := range h.clients {
|
||||
conns = append(conns, c)
|
||||
}
|
||||
h.mu.Unlock()
|
||||
|
||||
data, err := json.Marshal(st)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, c := range conns {
|
||||
_ = c.WriteMessage(websocket.TextMessage, data)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *wsHub) mergeTap(events []*pb.TapEvent) {
|
||||
if len(events) == 0 {
|
||||
return
|
||||
}
|
||||
h.mu.Lock()
|
||||
st := h.state
|
||||
st.Clients = applyTapEvents(st.Clients, events)
|
||||
st.UpdatedAt = time.Now().Format(time.RFC3339)
|
||||
h.state = st
|
||||
conns := make([]*websocket.Conn, 0, len(h.clients))
|
||||
for c := range h.clients {
|
||||
conns = append(conns, c)
|
||||
}
|
||||
h.mu.Unlock()
|
||||
|
||||
data, err := json.Marshal(st)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, c := range conns {
|
||||
_ = c.WriteMessage(websocket.TextMessage, data)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *wsHub) broadcastRaw(v any) {
|
||||
h.mu.RLock()
|
||||
conns := make([]*websocket.Conn, 0, len(h.clients))
|
||||
@ -285,7 +511,7 @@ func (h *wsHub) broadcastRaw(v any) {
|
||||
}
|
||||
}
|
||||
|
||||
func pollDashboard(link *managedSerial, portName string, last *DashboardState, streamCtl *accelStreamCtl) DashboardState {
|
||||
func pollDashboard(link *managedSerial, portName string, last *DashboardState, streamCtl *accelStreamCtl, tapCtl *tapNotifyCtl) DashboardState {
|
||||
st := DashboardState{
|
||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
||||
SerialPort: portName,
|
||||
@ -332,6 +558,9 @@ func pollDashboard(link *managedSerial, portName string, last *DashboardState, s
|
||||
LastPing: c.GetLastPing(),
|
||||
LastSuccessPing: c.GetLastSuccessPing(),
|
||||
AccelStream: c.GetAccelStreamEnabled(),
|
||||
TapNotifySingle: c.GetTapNotifySingle(),
|
||||
TapNotifyDouble: c.GetTapNotifyDouble(),
|
||||
TapNotifyTriple: c.GetTapNotifyTriple(),
|
||||
}
|
||||
st.Clients = append(st.Clients, cv)
|
||||
}
|
||||
@ -358,6 +587,9 @@ func pollDashboard(link *managedSerial, portName string, last *DashboardState, s
|
||||
if streamCtl != nil {
|
||||
streamCtl.SyncFromClients(st.Clients)
|
||||
}
|
||||
if tapCtl != nil {
|
||||
tapCtl.SyncFromClients(st.Clients)
|
||||
}
|
||||
return st
|
||||
}
|
||||
|
||||
@ -436,6 +668,27 @@ func runAccelDashboardPoller(link *managedSerial, hub *wsHub, interval time.Dura
|
||||
}
|
||||
}
|
||||
|
||||
func runTapDashboardPoller(link *managedSerial, hub *wsHub, interval time.Duration, stop <-chan struct{}) {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
case <-ticker.C:
|
||||
if hub.clientCount() == 0 || !hub.anyTapReceiveEnabled() {
|
||||
continue
|
||||
}
|
||||
snap, err := link.readTapSnapshotPoll(0)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
hub.mergeTap(snap.GetEvents())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *wsHub) clientCount() int {
|
||||
h.mu.RLock()
|
||||
n := len(h.clients)
|
||||
@ -490,15 +743,15 @@ func formatMAC(mac []byte) string {
|
||||
return hex.EncodeToString(mac)
|
||||
}
|
||||
|
||||
func runPoller(link *managedSerial, portName string, hub *wsHub, streamCtl *accelStreamCtl, interval time.Duration, stop <-chan struct{}) {
|
||||
// streamCtl kept for external API; dashboard uses hub.state AccelStream flags.
|
||||
func runPoller(link *managedSerial, portName string, hub *wsHub, streamCtl *accelStreamCtl, tapCtl *tapNotifyCtl, interval time.Duration, stop <-chan struct{}) {
|
||||
// streamCtl / tapCtl kept for external API; dashboard uses hub.state flags.
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
uartUp := false
|
||||
var lastGood DashboardState
|
||||
publish := func() {
|
||||
st := pollDashboard(link, portName, &lastGood, streamCtl)
|
||||
st := pollDashboard(link, portName, &lastGood, streamCtl, tapCtl)
|
||||
if st.UARTConnected && st.SerialOK {
|
||||
lastGood = st
|
||||
}
|
||||
|
||||
@ -16,6 +16,8 @@ func usage() {
|
||||
fmt.Fprintf(os.Stderr, " version firmware version and git hash\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, " 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, " accel read cached slave accel snapshot from master\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")
|
||||
@ -51,7 +53,7 @@ func main() {
|
||||
os.Exit(2)
|
||||
}
|
||||
runErr = runServe(*portName, *baud, flag.Args()[1:])
|
||||
case "version", "clients", "client-info", "deadzone", "accel-deadzone", "accel", "accel-read", "accel_read", "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", "tap", "accel", "accel-read", "accel_read", "unicast-test", "unicast_test", "led-ring", "led_ring", "find-me", "find_me", "restart", "ota", "ota-progress", "ota_progress":
|
||||
if *portName == "" {
|
||||
fmt.Fprintf(os.Stderr, "command %q requires -port\n\n", cmd)
|
||||
usage()
|
||||
@ -69,6 +71,10 @@ func main() {
|
||||
runErr = runClients(sp)
|
||||
case "deadzone", "accel-deadzone":
|
||||
runErr = runDeadzone(sp, flag.Args()[1:])
|
||||
case "tap-notify", "tap_notify":
|
||||
runErr = runTapNotify(sp, flag.Args()[1:])
|
||||
case "tap":
|
||||
runErr = runTapSnapshot(sp, flag.Args()[1:])
|
||||
case "accel", "accel-read", "accel_read":
|
||||
runErr = runAccel(sp)
|
||||
case "unicast-test", "unicast_test":
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
50
goTool/tap_notify_ctl.go
Normal file
50
goTool/tap_notify_ctl.go
Normal file
@ -0,0 +1,50 @@
|
||||
package main
|
||||
|
||||
import "sync"
|
||||
|
||||
// tapNotifyCtl tracks which slaves have tap notify enabled (mirrors firmware / dashboard).
|
||||
type tapNotifyCtl struct {
|
||||
mu sync.Mutex
|
||||
flags map[uint32]tapNotifyFlags
|
||||
}
|
||||
|
||||
type tapNotifyFlags struct {
|
||||
single bool
|
||||
doubleTap bool
|
||||
triple bool
|
||||
}
|
||||
|
||||
func newTapNotifyCtl() *tapNotifyCtl {
|
||||
return &tapNotifyCtl{flags: make(map[uint32]tapNotifyFlags)}
|
||||
}
|
||||
|
||||
func (c *tapNotifyCtl) Set(clientID uint32, single, doubleTap, triple bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if !single && !doubleTap && !triple {
|
||||
delete(c.flags, clientID)
|
||||
return
|
||||
}
|
||||
c.flags[clientID] = tapNotifyFlags{single: single, doubleTap: doubleTap, triple: triple}
|
||||
}
|
||||
|
||||
func (c *tapNotifyCtl) Any() bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return len(c.flags) > 0
|
||||
}
|
||||
|
||||
func (c *tapNotifyCtl) SyncFromClients(clients []ClientView) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.flags = make(map[uint32]tapNotifyFlags)
|
||||
for _, cl := range clients {
|
||||
if cl.TapNotifySingle || cl.TapNotifyDouble || cl.TapNotifyTriple {
|
||||
c.flags[cl.ID] = tapNotifyFlags{
|
||||
single: cl.TapNotifySingle,
|
||||
doubleTap: cl.TapNotifyDouble,
|
||||
triple: cl.TapNotifyTriple,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -62,6 +62,25 @@
|
||||
}
|
||||
.accel-stale { color: var(--pp-text-muted); }
|
||||
|
||||
.tap-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.15rem;
|
||||
font-size: 0.72rem;
|
||||
color: var(--pp-text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tap-toggle input { margin: 0; }
|
||||
.tap-hit {
|
||||
color: #ffd166;
|
||||
font-weight: 600;
|
||||
animation: tap-flash 2s ease-out;
|
||||
}
|
||||
@keyframes tap-flash {
|
||||
from { color: #fff; transform: scale(1.08); }
|
||||
to { color: #ffd166; transform: scale(1); }
|
||||
}
|
||||
|
||||
.pp-table {
|
||||
--bs-table-color: var(--pp-text);
|
||||
--bs-table-bg: transparent;
|
||||
@ -271,9 +290,21 @@
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted small px-3 pt-2 mb-0">
|
||||
Accel-Stream pro Slave per „Stream an“ aktivieren (~16 ms ESP-NOW). Ohne Aktivierung keine Werte.
|
||||
Accel-Stream pro Slave per „Stream an“ aktivieren (~16 ms ESP-NOW). Tap-Notify (S/D/T)
|
||||
konfiguriert den Slave; „Empfang an“ startet das Abfragen von Tap-Events (~16 ms).
|
||||
</p>
|
||||
<div class="card-body p-0 pt-2">
|
||||
<div class="px-3 pb-2 d-flex flex-wrap gap-2 align-items-center">
|
||||
<span class="text-muted small">Tap alle Slaves:</span>
|
||||
<label class="tap-toggle"><input type="checkbox" x-model="allTapSingle" :disabled="busy"> S</label>
|
||||
<label class="tap-toggle"><input type="checkbox" x-model="allTapDouble" :disabled="busy"> D</label>
|
||||
<label class="tap-toggle"><input type="checkbox" x-model="allTapTriple" :disabled="busy"> T</label>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||
@click="setTapNotifyAll(allTapSingle, allTapDouble, allTapTriple)"
|
||||
:disabled="busy || !state.uart_connected">
|
||||
Tap setzen
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body p-0 pt-1">
|
||||
<div class="table-responsive">
|
||||
<table class="table pp-table table-hover">
|
||||
<thead>
|
||||
@ -286,12 +317,14 @@
|
||||
<th>Accel (LSB)</th>
|
||||
<th>Akku</th>
|
||||
<th>Stream</th>
|
||||
<th>Tap-Notify</th>
|
||||
<th>Tap</th>
|
||||
<th>Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-if="!(state.clients || []).length">
|
||||
<tr><td colspan="9" class="text-muted text-center py-4">No clients</td></tr>
|
||||
<tr><td colspan="11" class="text-muted text-center py-4">No clients</td></tr>
|
||||
</template>
|
||||
<template x-for="c in (state.clients || [])" :key="c.id + c.mac">
|
||||
<tr>
|
||||
@ -319,6 +352,40 @@
|
||||
:disabled="busy || !state.uart_connected || !c.available"
|
||||
x-text="c.accel_stream ? 'Aus' : 'An'"></button>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-wrap gap-1 align-items-center">
|
||||
<label class="tap-toggle" title="Single tap">
|
||||
<input type="checkbox"
|
||||
:checked="c.tap_notify_single"
|
||||
@change="setTapNotify(c.id, $event.target.checked, c.tap_notify_double, c.tap_notify_triple)"
|
||||
:disabled="busy || !state.uart_connected || !c.available"> S
|
||||
</label>
|
||||
<label class="tap-toggle" title="Double tap">
|
||||
<input type="checkbox"
|
||||
:checked="c.tap_notify_double"
|
||||
@change="setTapNotify(c.id, c.tap_notify_single, $event.target.checked, c.tap_notify_triple)"
|
||||
:disabled="busy || !state.uart_connected || !c.available"> D
|
||||
</label>
|
||||
<label class="tap-toggle" title="Triple tap">
|
||||
<input type="checkbox"
|
||||
:checked="c.tap_notify_triple"
|
||||
@change="setTapNotify(c.id, c.tap_notify_single, c.tap_notify_double, $event.target.checked)"
|
||||
:disabled="busy || !state.uart_connected || !c.available"> T
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-wrap gap-1 align-items-center">
|
||||
<button type="button"
|
||||
class="btn btn-sm"
|
||||
:class="c.tap_receive ? 'btn-warning' : 'btn-outline-success'"
|
||||
@click="setTapReceive(c.id, !c.tap_receive)"
|
||||
:disabled="busy || !state.uart_connected || !c.available || !tapNotifyAny(c)"
|
||||
x-text="c.tap_receive ? 'Aus' : 'An'"
|
||||
title="Tap-Events vom Master abfragen"></button>
|
||||
<span :class="tapCellClass(c)" x-text="formatLastTap(c)" :title="tapTitle(c)"></span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-wrap gap-1 align-items-center">
|
||||
<input type="number" class="form-control form-control-sm dz-input"
|
||||
@ -556,7 +623,13 @@
|
||||
wsConnected: false,
|
||||
masterDz: 100,
|
||||
allDz: 100,
|
||||
allTapSingle: false,
|
||||
allTapDouble: false,
|
||||
allTapTriple: false,
|
||||
slaveDz: {},
|
||||
TAP_DISPLAY_MS: 2000,
|
||||
tapDisplay: {},
|
||||
_tapClock: 0,
|
||||
otaFile: null,
|
||||
ota: {
|
||||
active: false, phase: '', step: '', percent: 0,
|
||||
@ -584,6 +657,8 @@
|
||||
const url = proto + '//' + location.host + '/ws';
|
||||
if (this._batteryTimer) clearInterval(this._batteryTimer);
|
||||
this._batteryTimer = setInterval(() => this.refreshBattery(), 5000);
|
||||
if (this._tapTimer) clearInterval(this._tapTimer);
|
||||
this._tapTimer = setInterval(() => { this._tapClock++; }, 250);
|
||||
const connect = () => {
|
||||
this.ws = new WebSocket(url);
|
||||
this.ws.onopen = () => {
|
||||
@ -608,6 +683,7 @@
|
||||
const prev = this.state;
|
||||
this.state = msg;
|
||||
this.preserveBatteryInState(prev, this.state);
|
||||
this.syncTapDisplay(msg.clients || []);
|
||||
if (msg.master?.deadzone != null) {
|
||||
this.masterDz = msg.master.deadzone;
|
||||
}
|
||||
@ -718,6 +794,56 @@
|
||||
if (c.accel_age_ms != null && c.accel_age_ms > 200) return 'accel-stale';
|
||||
return '';
|
||||
},
|
||||
tapNotifyAny(c) {
|
||||
return !!(c?.tap_notify_single || c?.tap_notify_double || c?.tap_notify_triple);
|
||||
},
|
||||
syncTapDisplay(clients) {
|
||||
const now = Date.now();
|
||||
for (const c of clients) {
|
||||
if (!c?.last_tap || !c?.last_tap_at) continue;
|
||||
const prev = this.tapDisplay[c.id];
|
||||
if (!prev || c.last_tap_at >= prev.shownAt) {
|
||||
this.tapDisplay[c.id] = { kind: c.last_tap, shownAt: c.last_tap_at };
|
||||
}
|
||||
}
|
||||
for (const id of Object.keys(this.tapDisplay)) {
|
||||
if (now - this.tapDisplay[id].shownAt > this.TAP_DISPLAY_MS + 500) {
|
||||
delete this.tapDisplay[id];
|
||||
}
|
||||
}
|
||||
},
|
||||
activeTapDisplay(c) {
|
||||
void this._tapClock;
|
||||
const d = this.tapDisplay[c?.id];
|
||||
if (!d) return null;
|
||||
if (Date.now() - d.shownAt >= this.TAP_DISPLAY_MS) return null;
|
||||
return d;
|
||||
},
|
||||
formatLastTap(c) {
|
||||
if (!c?.tap_receive) return '—';
|
||||
if (!this.tapNotifyAny(c)) return '—';
|
||||
const labels = { single: 'Single', double: 'Double', triple: 'Triple' };
|
||||
const d = this.activeTapDisplay(c);
|
||||
if (d) return labels[d.kind] || d.kind;
|
||||
if (!c?.last_tap) return '…';
|
||||
return labels[c.last_tap] || c.last_tap;
|
||||
},
|
||||
tapTitle(c) {
|
||||
if (!c?.tap_receive) return 'Tap-Empfang aus — „An“ klicken';
|
||||
if (!this.tapNotifyAny(c)) return 'Tap-Notify nicht konfiguriert (S/D/T)';
|
||||
const d = this.activeTapDisplay(c);
|
||||
if (!d && !c?.last_tap) return 'Warte auf Tap-Event…';
|
||||
const kind = d?.kind || c?.last_tap;
|
||||
const at = d?.shownAt || c?.last_tap_at;
|
||||
const age = at ? (Date.now() - at) + ' ms her' : '';
|
||||
return `Letzter Tap: ${kind}${age ? ' · ' + age : ''}`;
|
||||
},
|
||||
tapCellClass(c) {
|
||||
if (this.activeTapDisplay(c)) return 'tap-hit';
|
||||
if (!c?.last_tap || !c?.last_tap_at) return 'text-muted';
|
||||
if (Date.now() - c.last_tap_at < this.TAP_DISPLAY_MS) return 'tap-hit';
|
||||
return '';
|
||||
},
|
||||
formatSize(n) {
|
||||
if (n == null) return '';
|
||||
if (n < 1024) return n + ' B';
|
||||
@ -992,6 +1118,104 @@
|
||||
this.busy = false;
|
||||
}
|
||||
},
|
||||
patchClientTapNotify(clientId, single, doubleTap, triple) {
|
||||
const clients = (this.state.clients || []).map((c) => {
|
||||
if (c.id !== clientId) return c;
|
||||
const next = {
|
||||
...c,
|
||||
tap_notify_single: single,
|
||||
tap_notify_double: doubleTap,
|
||||
tap_notify_triple: triple
|
||||
};
|
||||
if (!single && !doubleTap && !triple) {
|
||||
next.last_tap = '';
|
||||
next.last_tap_at = 0;
|
||||
delete this.tapDisplay[c.id];
|
||||
}
|
||||
return next;
|
||||
});
|
||||
this.state = { ...this.state, clients };
|
||||
},
|
||||
async setTapNotify(clientId, single, doubleTap, triple) {
|
||||
this.busy = true;
|
||||
try {
|
||||
const r = await fetch(`/api/clients/${clientId}/tap-notify`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ single, double_tap: doubleTap, triple })
|
||||
});
|
||||
const data = await r.json();
|
||||
if (!r.ok || !data.success) {
|
||||
this.flash(data.error || `Tap-Notify Slave ${clientId} fehlgeschlagen`, false);
|
||||
return;
|
||||
}
|
||||
this.patchClientTapNotify(clientId, !!data.single, !!data.double_tap, !!data.triple);
|
||||
const on = [data.single && 'S', data.double_tap && 'D', data.triple && 'T'].filter(Boolean).join('/') || 'aus';
|
||||
this.flash(`Slave ${clientId}: Tap-Notify ${on}`, true);
|
||||
} catch (e) {
|
||||
this.flash(String(e), false);
|
||||
} finally {
|
||||
this.busy = false;
|
||||
}
|
||||
},
|
||||
async setTapNotifyAll(single, doubleTap, triple) {
|
||||
this.busy = true;
|
||||
try {
|
||||
const r = await fetch('/api/tap-notify', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ all_clients: true, single, double_tap: doubleTap, triple })
|
||||
});
|
||||
const data = await r.json();
|
||||
if (!r.ok || !data.success) {
|
||||
this.flash(data.error || 'Tap-Notify für alle Slaves fehlgeschlagen', false);
|
||||
return;
|
||||
}
|
||||
for (const c of (this.state.clients || [])) {
|
||||
this.patchClientTapNotify(c.id, !!single, !!doubleTap, !!triple);
|
||||
}
|
||||
const on = [single && 'S', doubleTap && 'D', triple && 'T'].filter(Boolean).join('/') || 'aus';
|
||||
this.flash(`Alle Slaves: Tap-Notify ${on} (${data.slaves_updated} aktualisiert)`, true);
|
||||
} catch (e) {
|
||||
this.flash(String(e), false);
|
||||
} finally {
|
||||
this.busy = false;
|
||||
}
|
||||
},
|
||||
patchClientTapReceive(clientId, enabled) {
|
||||
const clients = (this.state.clients || []).map((c) => {
|
||||
if (c.id !== clientId) return c;
|
||||
const next = { ...c, tap_receive: enabled };
|
||||
if (!enabled) {
|
||||
next.last_tap = '';
|
||||
next.last_tap_at = 0;
|
||||
delete this.tapDisplay[clientId];
|
||||
}
|
||||
return next;
|
||||
});
|
||||
this.state = { ...this.state, clients };
|
||||
},
|
||||
async setTapReceive(clientId, enable) {
|
||||
this.busy = true;
|
||||
try {
|
||||
const r = await fetch(`/api/clients/${clientId}/tap-receive`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ enable })
|
||||
});
|
||||
const data = await r.json();
|
||||
if (!r.ok || !data.success) {
|
||||
this.flash(data.error || `Tap-Empfang Slave ${clientId} fehlgeschlagen`, false);
|
||||
return;
|
||||
}
|
||||
this.patchClientTapReceive(clientId, !!data.enabled);
|
||||
this.flash(`Slave ${clientId}: Tap-Empfang ${data.enabled ? 'an' : 'aus'}`, true);
|
||||
} catch (e) {
|
||||
this.flash(String(e), false);
|
||||
} finally {
|
||||
this.busy = false;
|
||||
}
|
||||
},
|
||||
async setDeadzoneAll(deadzone) {
|
||||
if (deadzone == null || deadzone < 0) {
|
||||
this.flash('Ungültiger Deadzone-Wert', false);
|
||||
|
||||
@ -20,6 +20,8 @@ idf_component_register(
|
||||
"cmd/cmd_accel_deadzone.c"
|
||||
"cmd/cmd_accel_snapshot.c"
|
||||
"cmd/cmd_accel_stream.c"
|
||||
"cmd/cmd_tap_notify.c"
|
||||
"cmd/cmd_tap_snapshot.c"
|
||||
"cmd/cmd_espnow_unicast_test.c"
|
||||
"cmd/cmd_espnow_find_me.c"
|
||||
"cmd/cmd_restart.c"
|
||||
|
||||
@ -116,6 +116,8 @@ Schema: `proto/esp_now_messages.proto`. Encode/decode: `esp_now_proto.c`. The ES
|
||||
| `ESPNOW_FIND_ME` | Master → slave | `EspNowFindMe` (`client_id` filter) — LED locate sequence |
|
||||
| `ESPNOW_RESTART` | Master → slave | `EspNowRestart` (`client_id` filter) — reboot slave |
|
||||
| `ESPNOW_ACCEL_SAMPLE` | Slave → master | `EspNowAccelSample` (`slave_id`, `x`, `y`, `z` raw LSB) — ~every 16 ms |
|
||||
| `ESPNOW_SET_TAP_NOTIFY` | Master → slave | `EspNowTapNotify` (`client_id`, `single`, `double_tap`, `triple`) — which tap kinds to forward |
|
||||
| `ESPNOW_TAP_EVENT` | Slave → master | `EspNowTapEvent` (`client_id`, `kind`) — on BMA456 tap interrupt if notify enabled |
|
||||
| `ESPNOW_BATTERY_REPORT` | Slave → master | `EspNowBatteryReport` (`client_id`, `lipo1/2` mV) — ~every 30 s; cached in `client_registry` |
|
||||
| `ESPNOW_OTA_START` | Master → slave (unicast) | `EspNowOtaStart` (`total_size`) |
|
||||
| `ESPNOW_OTA_PAYLOAD` | Master → slave | `EspNowOtaPayload` (`seq`, up to 200 B `data`) |
|
||||
@ -221,6 +223,8 @@ Host and master speak nanopb-encoded `UartMessage` inside UART frames (byte 0 =
|
||||
| 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 |
|
||||
| 24 | `ACCEL_SNAPSHOT` | Implemented (`cmd/cmd_accel_snapshot.c`) — cached slave accel from ESP-NOW stream |
|
||||
| 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 |
|
||||
|
||||
Regenerate C code:
|
||||
|
||||
@ -337,6 +341,52 @@ 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
|
||||
|
||||
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).
|
||||
|
||||
**Request:** framed `1b` (`0x1b`) + `tap_notify_request`:
|
||||
|
||||
| Field | Meaning |
|
||||
|-------|---------|
|
||||
| `write` | `false` = read, `true` = write |
|
||||
| `single`, `double_tap`, `triple` | Which tap kinds to notify (write) |
|
||||
| `client_id` | Slave id (read/write one slave) |
|
||||
| `all_clients` | Master: ESP-NOW unicast to every registered slave |
|
||||
|
||||
**Response:** `tap_notify_response` (`client_id`, `success`, `slaves_updated`, `single`, `double_tap`, `triple`).
|
||||
|
||||
Notify flags are mirrored in `ClientInfo` (`tap_notify_single/double/triple`) for the dashboard.
|
||||
|
||||
```bash
|
||||
go run . -port /dev/ttyUSB0 tap-notify -client 16 -set -single
|
||||
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).
|
||||
|
||||
Only slaves with at least one tap-notify flag enabled are included.
|
||||
|
||||
**Request:** framed `1c` (`0x1c`) + optional `tap_snapshot_request` (`client_id`: `0` = all, `>0` = one id).
|
||||
|
||||
**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
|
||||
go run . -port /dev/ttyUSB0 tap
|
||||
go run . -port /dev/ttyUSB0 tap -client 16
|
||||
```
|
||||
|
||||
### ESPNOW_UNICAST_TEST command
|
||||
|
||||
Minimal master→slave ESP-NOW unicast check (no BMA456). Use this before debugging `ACCEL_DEADZONE` unicast.
|
||||
@ -424,7 +474,7 @@ go run . -port /dev/ttyUSB0 led-ring -mode digit -digit 5 -all
|
||||
|
||||
**Response:** payload `04` + nanopb `UartMessage` with `client_info_response.clients` — one `ClientInfo` per registered slave (from ESP-NOW `SLAVE_INFO`).
|
||||
|
||||
Fields per client: `id`, `mac`, `version`, `available`, `used`, `last_ping`, `last_success_ping` — **milliseconds since** the last packet / last successful heartbeat (computed when `CLIENT_INFO` is answered; typically 0–1000 while the slave is heartbeating every 1 s).
|
||||
Fields per client: `id`, `mac`, `version`, `available`, `used`, `last_ping`, `last_success_ping`, `tap_notify_single`, `tap_notify_double`, `tap_notify_triple` — **milliseconds since** the last packet / last successful heartbeat (computed when `CLIENT_INFO` is answered; typically 0–1000 while the slave is heartbeating every 1 s).
|
||||
|
||||
## Client registry
|
||||
|
||||
@ -435,6 +485,7 @@ Fields per client: `id`, `mac`, `version`, `available`, `used`, `last_ping`, `la
|
||||
| `client_registry_heartbeat(mac, id, version, …)` | Same as upsert for heartbeats; reactivates inactive clients |
|
||||
| `client_registry_check_timeouts(timeout_ms)` | Mark stale clients inactive (master monitor task) |
|
||||
| `client_registry_count()` / `client_registry_at(i)` | Iterate for UART encoding |
|
||||
| `client_registry_set_tap_notify()` / `client_registry_take_tap()` | Tap notify flags + short-lived tap cache (16 ms) |
|
||||
|
||||
Slaves register when the master receives `SLAVE_INFO` on the matching network; `HEARTBEAT` keeps them marked available. The registry **MAC is always the ESP-NOW source address** (`recv_info.src_addr`), not the optional `mac` bytes in the protobuf (used only on the wire for debugging).
|
||||
|
||||
@ -502,6 +553,8 @@ Target: ESP32-S3. Close serial monitor on the UART adapter port before running `
|
||||
| `client_registry.c/h` | Registered slave table |
|
||||
| `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_snapshot.c` | UART `TAP_SNAPSHOT` — consume cached tap events |
|
||||
| `board_input.c/h` | Taster GPIO12, LiPo ADC on GPIO1 / GPIO12 |
|
||||
| `pod_settings.c/h` | NVS persistence (accel deadzone, …) |
|
||||
| `led_ring.c/h` | LED ring (digit display, progress bar) |
|
||||
|
||||
@ -36,6 +36,8 @@ static bool s_have_last_sample;
|
||||
|
||||
static volatile bool s_int_pending;
|
||||
static SemaphoreHandle_t s_accel_mutex;
|
||||
static bma456_tap_handler_t s_tap_handler;
|
||||
static void *s_tap_handler_ctx;
|
||||
|
||||
static esp_err_t check_bma4(const char *api_name, int8_t rslt);
|
||||
|
||||
@ -123,6 +125,11 @@ void bma456_set_accel_deadzone(uint32_t deadzone_lsb) {
|
||||
|
||||
uint32_t bma456_get_accel_deadzone(void) { return s_accel_deadzone; }
|
||||
|
||||
void bma456_set_tap_handler(bma456_tap_handler_t handler, void *ctx) {
|
||||
s_tap_handler = handler;
|
||||
s_tap_handler_ctx = ctx;
|
||||
}
|
||||
|
||||
esp_err_t bma456_read_accel(int16_t *x, int16_t *y, int16_t *z) {
|
||||
if (!s_bma456_ready || x == NULL || y == NULL || z == NULL) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
@ -183,10 +190,19 @@ static void handle_tap_interrupt(void) {
|
||||
|
||||
if (tap_out.single_tap) {
|
||||
ESP_LOGI(TAG, "tap: single");
|
||||
if (s_tap_handler != NULL) {
|
||||
s_tap_handler(BMA456_TAP_SINGLE, s_tap_handler_ctx);
|
||||
}
|
||||
} else if (tap_out.double_tap) {
|
||||
ESP_LOGI(TAG, "tap: double");
|
||||
if (s_tap_handler != NULL) {
|
||||
s_tap_handler(BMA456_TAP_DOUBLE, s_tap_handler_ctx);
|
||||
}
|
||||
} else if (tap_out.triple_tap) {
|
||||
ESP_LOGI(TAG, "tap: triple");
|
||||
if (s_tap_handler != NULL) {
|
||||
s_tap_handler(BMA456_TAP_TRIPLE, s_tap_handler_ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -35,6 +35,18 @@ uint32_t bma456_get_accel_deadzone(void);
|
||||
/** Log accel when any axis moved more than deadzone since last reported sample. */
|
||||
void bma456_report_accel_if_changed(int16_t x, int16_t y, int16_t z);
|
||||
|
||||
/** Tap kinds from BMA456H multitap output. */
|
||||
typedef enum {
|
||||
BMA456_TAP_SINGLE = 1,
|
||||
BMA456_TAP_DOUBLE = 2,
|
||||
BMA456_TAP_TRIPLE = 3,
|
||||
} bma456_tap_kind_t;
|
||||
|
||||
typedef void (*bma456_tap_handler_t)(bma456_tap_kind_t kind, void *ctx);
|
||||
|
||||
/** Optional callback invoked from sensor task on tap interrupt (may be NULL). */
|
||||
void bma456_set_tap_handler(bma456_tap_handler_t handler, void *ctx);
|
||||
|
||||
/** On-demand read of current accel XYZ (raw LSB). Returns ESP_ERR_INVALID_STATE if sensor not ready. */
|
||||
esp_err_t bma456_read_accel(int16_t *x, int16_t *y, int16_t *z);
|
||||
|
||||
|
||||
@ -257,6 +257,31 @@ static void clear_client_accel(client_slot_t *slot) {
|
||||
slot->info.accel_updated_at = 0;
|
||||
}
|
||||
|
||||
static void clear_client_tap(client_slot_t *slot) {
|
||||
if (slot == NULL) {
|
||||
return;
|
||||
}
|
||||
slot->info.tap_valid = false;
|
||||
slot->info.tap_kind = 0;
|
||||
slot->info.tap_updated_at = 0;
|
||||
}
|
||||
|
||||
static bool tap_kind_enabled(const client_info_t *info, uint32_t kind) {
|
||||
if (info == NULL) {
|
||||
return false;
|
||||
}
|
||||
switch (kind) {
|
||||
case 1:
|
||||
return info->tap_notify_single;
|
||||
case 2:
|
||||
return info->tap_notify_double;
|
||||
case 3:
|
||||
return info->tap_notify_triple;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t client_registry_set_accel_stream(uint32_t client_id, bool enabled) {
|
||||
for (size_t i = 0; i < CLIENT_REGISTRY_MAX; i++) {
|
||||
if (!s_clients[i].active || s_clients[i].info.id != client_id) {
|
||||
@ -325,6 +350,124 @@ esp_err_t client_registry_update_accel(const uint8_t mac[CLIENT_MAC_LEN],
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t client_registry_set_tap_notify(uint32_t client_id, bool single,
|
||||
bool double_tap, bool triple) {
|
||||
for (size_t i = 0; i < CLIENT_REGISTRY_MAX; i++) {
|
||||
if (!s_clients[i].active || s_clients[i].info.id != client_id) {
|
||||
continue;
|
||||
}
|
||||
s_clients[i].info.tap_notify_single = single;
|
||||
s_clients[i].info.tap_notify_double = double_tap;
|
||||
s_clients[i].info.tap_notify_triple = triple;
|
||||
if (!single && !double_tap && !triple) {
|
||||
clear_client_tap(&s_clients[i]);
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
esp_err_t client_registry_get_tap_notify(uint32_t client_id, bool *single_out,
|
||||
bool *double_tap_out,
|
||||
bool *triple_out) {
|
||||
if (single_out == NULL || double_tap_out == NULL || triple_out == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
const client_info_t *info = client_registry_find_by_id(client_id);
|
||||
if (info == NULL) {
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
*single_out = info->tap_notify_single;
|
||||
*double_tap_out = info->tap_notify_double;
|
||||
*triple_out = info->tap_notify_triple;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
size_t client_registry_set_tap_notify_all(bool single, bool double_tap,
|
||||
bool triple) {
|
||||
size_t n = 0;
|
||||
for (size_t i = 0; i < CLIENT_REGISTRY_MAX; i++) {
|
||||
if (!s_clients[i].active) {
|
||||
continue;
|
||||
}
|
||||
s_clients[i].info.tap_notify_single = single;
|
||||
s_clients[i].info.tap_notify_double = double_tap;
|
||||
s_clients[i].info.tap_notify_triple = triple;
|
||||
if (!single && !double_tap && !triple) {
|
||||
clear_client_tap(&s_clients[i]);
|
||||
}
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
esp_err_t client_registry_update_tap(const uint8_t mac[CLIENT_MAC_LEN],
|
||||
uint32_t slave_id, uint32_t kind) {
|
||||
if (mac == NULL || kind < 1 || kind > 3) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
client_slot_t *slot = find_slot(mac);
|
||||
if (slot == NULL) {
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
if (slot->info.id != slave_id) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (!tap_kind_enabled(&slot->info, kind)) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
slot->info.tap_kind = kind;
|
||||
slot->info.tap_valid = true;
|
||||
slot->info.tap_updated_at = now_ms();
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void client_registry_expire_tap(client_info_t *info) {
|
||||
if (info == NULL || !info->tap_valid) {
|
||||
return;
|
||||
}
|
||||
if (client_registry_ms_since(info->tap_updated_at) >
|
||||
CLIENT_REGISTRY_TAP_MAX_AGE_MS) {
|
||||
info->tap_valid = false;
|
||||
info->tap_kind = 0;
|
||||
info->tap_updated_at = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void client_registry_clear_tap(client_info_t *info) {
|
||||
if (info == NULL) {
|
||||
return;
|
||||
}
|
||||
info->tap_valid = false;
|
||||
info->tap_kind = 0;
|
||||
info->tap_updated_at = 0;
|
||||
}
|
||||
|
||||
bool client_registry_take_tap(uint32_t client_id, uint32_t *kind_out,
|
||||
uint32_t *age_ms_out) {
|
||||
for (size_t i = 0; i < CLIENT_REGISTRY_MAX; i++) {
|
||||
if (!s_clients[i].active || s_clients[i].info.id != client_id) {
|
||||
continue;
|
||||
}
|
||||
client_info_t *info = &s_clients[i].info;
|
||||
client_registry_expire_tap(info);
|
||||
if (!info->tap_valid) {
|
||||
return false;
|
||||
}
|
||||
if (kind_out != NULL) {
|
||||
*kind_out = info->tap_kind;
|
||||
}
|
||||
if (age_ms_out != NULL) {
|
||||
*age_ms_out = client_registry_ms_since(info->tap_updated_at);
|
||||
}
|
||||
client_registry_clear_tap(info);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void client_registry_set_master_battery(const board_lipo_reading_t *reading) {
|
||||
if (reading == NULL) {
|
||||
return;
|
||||
|
||||
@ -30,6 +30,14 @@ typedef struct {
|
||||
uint32_t accel_updated_at;
|
||||
/** Host-enabled ESP-NOW accel stream to master. */
|
||||
bool accel_stream_enabled;
|
||||
/** Host-enabled ESP-NOW tap notify flags. */
|
||||
bool tap_notify_single;
|
||||
bool tap_notify_double;
|
||||
bool tap_notify_triple;
|
||||
/** Latest tap from slave ESP-NOW (master only, short-lived cache). */
|
||||
bool tap_valid;
|
||||
uint32_t tap_kind;
|
||||
uint32_t tap_updated_at;
|
||||
/** Latest LiPo ADC from slave ESP-NOW battery report (~30 s). */
|
||||
bool lipo1_valid;
|
||||
bool lipo2_valid;
|
||||
@ -39,6 +47,8 @@ typedef struct {
|
||||
} client_info_t;
|
||||
|
||||
#define CLIENT_REGISTRY_DEFAULT_ACCEL_DEADZONE 100u
|
||||
/** Tap events older than this are discarded (matches accel stream interval). */
|
||||
#define CLIENT_REGISTRY_TAP_MAX_AGE_MS 16u
|
||||
|
||||
void client_registry_init(void);
|
||||
|
||||
@ -87,6 +97,31 @@ esp_err_t client_registry_set_accel_stream(uint32_t client_id, bool enabled);
|
||||
esp_err_t client_registry_get_accel_stream(uint32_t client_id, bool *enabled_out);
|
||||
size_t client_registry_set_accel_stream_all(bool enabled);
|
||||
|
||||
esp_err_t client_registry_set_tap_notify(uint32_t client_id, bool single,
|
||||
bool double_tap, bool triple);
|
||||
esp_err_t client_registry_get_tap_notify(uint32_t client_id, bool *single_out,
|
||||
bool *double_tap_out,
|
||||
bool *triple_out);
|
||||
size_t client_registry_set_tap_notify_all(bool single, bool double_tap,
|
||||
bool triple);
|
||||
|
||||
/** Store tap event from slave (matched by sender MAC). kind: 1=single, 2=double, 3=triple. */
|
||||
esp_err_t client_registry_update_tap(const uint8_t mac[CLIENT_MAC_LEN],
|
||||
uint32_t slave_id, uint32_t kind);
|
||||
|
||||
/** Drop cached tap if older than CLIENT_REGISTRY_TAP_MAX_AGE_MS. */
|
||||
void client_registry_expire_tap(client_info_t *info);
|
||||
|
||||
/** Clear cached tap after UART snapshot or expiry. */
|
||||
void client_registry_clear_tap(client_info_t *info);
|
||||
|
||||
/**
|
||||
* If client has a fresh tap (age <= CLIENT_REGISTRY_TAP_MAX_AGE_MS), copy it out
|
||||
* and clear the cache. Returns true when an event was returned.
|
||||
*/
|
||||
bool client_registry_take_tap(uint32_t client_id, uint32_t *kind_out,
|
||||
uint32_t *age_ms_out);
|
||||
|
||||
/** Master local LiPo (client_id 0 in UART battery responses). */
|
||||
void client_registry_set_master_battery(const board_lipo_reading_t *reading);
|
||||
bool client_registry_get_master_battery(board_lipo_reading_t *reading_out,
|
||||
|
||||
@ -28,6 +28,9 @@ static bool encode_clients_list(pb_ostream_t *stream, const pb_field_t *field,
|
||||
client_registry_ms_since(client->last_success_ping_at);
|
||||
proto.version = client->version;
|
||||
proto.accel_stream_enabled = client->accel_stream_enabled;
|
||||
proto.tap_notify_single = client->tap_notify_single;
|
||||
proto.tap_notify_double = client->tap_notify_double;
|
||||
proto.tap_notify_triple = client->tap_notify_triple;
|
||||
proto.mac.funcs.encode = uart_cmd_encode_bytes;
|
||||
proto.mac.arg = &mac;
|
||||
|
||||
|
||||
@ -54,6 +54,10 @@ static const char *message_type_name(uint16_t id) {
|
||||
return "ACCEL_STREAM";
|
||||
case alox_MessageType_BATTERY_STATUS:
|
||||
return "BATTERY_STATUS";
|
||||
case alox_MessageType_TAP_NOTIFY:
|
||||
return "TAP_NOTIFY";
|
||||
case alox_MessageType_TAP_SNAPSHOT:
|
||||
return "TAP_SNAPSHOT";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
110
main/cmd/cmd_tap_notify.c
Normal file
110
main/cmd/cmd_tap_notify.c
Normal file
@ -0,0 +1,110 @@
|
||||
#include "client_registry.h"
|
||||
#include "cmd_tap_notify.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_now_comm.h"
|
||||
#include "uart_cmd.h"
|
||||
|
||||
static const char *TAG = "[TAP_NOTIFY]";
|
||||
|
||||
static void reply(uint32_t client_id, bool success, uint32_t slaves_updated,
|
||||
bool single, bool double_tap, bool triple) {
|
||||
alox_UartMessage response;
|
||||
uart_cmd_init_response(&response, alox_MessageType_TAP_NOTIFY,
|
||||
alox_UartMessage_tap_notify_response_tag);
|
||||
response.payload.tap_notify_response.client_id = client_id;
|
||||
response.payload.tap_notify_response.success = success;
|
||||
response.payload.tap_notify_response.slaves_updated = slaves_updated;
|
||||
response.payload.tap_notify_response.single = single;
|
||||
response.payload.tap_notify_response.double_tap = double_tap;
|
||||
response.payload.tap_notify_response.triple = triple;
|
||||
uart_cmd_send(&response, TAG);
|
||||
}
|
||||
|
||||
static esp_err_t push_tap_notify_to_slave(const client_info_t *client,
|
||||
bool single, bool double_tap,
|
||||
bool triple) {
|
||||
if (client == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
esp_err_t err =
|
||||
client_registry_set_tap_notify(client->id, single, double_tap, triple);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
return esp_now_comm_send_tap_notify(client->mac, client->id, single,
|
||||
double_tap, triple);
|
||||
}
|
||||
|
||||
static void handle_tap_notify(const uint8_t *data, size_t len) {
|
||||
alox_UartMessage uart_msg;
|
||||
alox_TapNotifyRequest req = alox_TapNotifyRequest_init_zero;
|
||||
|
||||
if (uart_cmd_decode(data, len, &uart_msg) == ESP_OK) {
|
||||
const alox_TapNotifyRequest *req_ptr = UART_CMD_REQ(
|
||||
&uart_msg, alox_UartMessage_tap_notify_request_tag, tap_notify_request);
|
||||
if (req_ptr != NULL) {
|
||||
req = *req_ptr;
|
||||
}
|
||||
}
|
||||
|
||||
if (req.write) {
|
||||
if (req.all_clients) {
|
||||
size_t n = client_registry_set_tap_notify_all(req.single, req.double_tap,
|
||||
req.triple);
|
||||
uint32_t sent = 0;
|
||||
|
||||
for (size_t i = 0; i < client_registry_count(); i++) {
|
||||
const client_info_t *client = client_registry_at(i);
|
||||
if (client == NULL) {
|
||||
continue;
|
||||
}
|
||||
if (esp_now_comm_send_tap_notify(client->mac, client->id, req.single,
|
||||
req.double_tap,
|
||||
req.triple) == ESP_OK) {
|
||||
sent++;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "tap notify single=%d double=%d triple=%d for %u/%u slaves",
|
||||
req.single, req.double_tap, req.triple, (unsigned)sent,
|
||||
(unsigned)n);
|
||||
reply(0, sent > 0, sent, req.single, req.double_tap, req.triple);
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.client_id == 0) {
|
||||
ESP_LOGW(TAG, "client_id required (or all_clients)");
|
||||
reply(0, false, 0, req.single, req.double_tap, req.triple);
|
||||
return;
|
||||
}
|
||||
|
||||
const client_info_t *client = client_registry_find_by_id(req.client_id);
|
||||
if (client == NULL) {
|
||||
ESP_LOGW(TAG, "client id %lu not found", (unsigned long)req.client_id);
|
||||
reply(req.client_id, false, 0, req.single, req.double_tap, req.triple);
|
||||
return;
|
||||
}
|
||||
|
||||
esp_err_t err =
|
||||
push_tap_notify_to_slave(client, req.single, req.double_tap, req.triple);
|
||||
reply(req.client_id, err == ESP_OK, err == ESP_OK ? 1u : 0u, req.single,
|
||||
req.double_tap, req.triple);
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.all_clients || req.client_id == 0) {
|
||||
reply(0, false, 0, false, false, false);
|
||||
return;
|
||||
}
|
||||
|
||||
bool single = false;
|
||||
bool double_tap = false;
|
||||
bool triple = false;
|
||||
esp_err_t err = client_registry_get_tap_notify(req.client_id, &single,
|
||||
&double_tap, &triple);
|
||||
reply(req.client_id, err == ESP_OK, 0, single, double_tap, triple);
|
||||
}
|
||||
|
||||
void cmd_tap_notify_register(void) {
|
||||
uart_cmd_register(alox_MessageType_TAP_NOTIFY, handle_tap_notify);
|
||||
}
|
||||
6
main/cmd/cmd_tap_notify.h
Normal file
6
main/cmd/cmd_tap_notify.h
Normal file
@ -0,0 +1,6 @@
|
||||
#ifndef CMD_TAP_NOTIFY_H
|
||||
#define CMD_TAP_NOTIFY_H
|
||||
|
||||
void cmd_tap_notify_register(void);
|
||||
|
||||
#endif
|
||||
88
main/cmd/cmd_tap_snapshot.c
Normal file
88
main/cmd/cmd_tap_snapshot.c
Normal file
@ -0,0 +1,88 @@
|
||||
#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);
|
||||
}
|
||||
6
main/cmd/cmd_tap_snapshot.h
Normal file
6
main/cmd/cmd_tap_snapshot.h
Normal file
@ -0,0 +1,6 @@
|
||||
#ifndef CMD_TAP_SNAPSHOT_H
|
||||
#define CMD_TAP_SNAPSHOT_H
|
||||
|
||||
void cmd_tap_snapshot_register(void);
|
||||
|
||||
#endif
|
||||
@ -43,6 +43,9 @@ static uint8_t s_wifi_channel;
|
||||
static uint8_t s_own_mac[ESP_NOW_ETH_ALEN];
|
||||
static bool s_slave_joined;
|
||||
static bool s_accel_stream_enabled;
|
||||
static bool s_tap_notify_single;
|
||||
static bool s_tap_notify_double;
|
||||
static bool s_tap_notify_triple;
|
||||
static uint8_t s_master_mac[ESP_NOW_ETH_ALEN];
|
||||
static uint32_t s_last_discover_ms;
|
||||
|
||||
@ -138,6 +141,28 @@ static esp_err_t send_accel_sample(const uint8_t *dest_mac, uint32_t slave_id,
|
||||
return send_message(dest_mac, &msg);
|
||||
}
|
||||
|
||||
static esp_err_t send_tap_event(const uint8_t *dest_mac, uint32_t slave_id,
|
||||
uint32_t kind) {
|
||||
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
|
||||
msg.type = alox_EspNowMessageType_ESPNOW_TAP_EVENT;
|
||||
msg.which_payload = alox_EspNowMessage_tap_event_tag;
|
||||
msg.payload.tap_event.slave_id = slave_id;
|
||||
msg.payload.tap_event.kind = kind;
|
||||
return send_message(dest_mac, &msg);
|
||||
}
|
||||
|
||||
static esp_err_t send_tap_notify(const uint8_t *dest_mac, uint32_t client_id,
|
||||
bool single, bool double_tap, bool triple) {
|
||||
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
|
||||
msg.type = alox_EspNowMessageType_ESPNOW_SET_TAP_NOTIFY;
|
||||
msg.which_payload = alox_EspNowMessage_tap_notify_tag;
|
||||
msg.payload.tap_notify.client_id = client_id;
|
||||
msg.payload.tap_notify.single = single;
|
||||
msg.payload.tap_notify.double_tap = double_tap;
|
||||
msg.payload.tap_notify.triple = triple;
|
||||
return send_message(dest_mac, &msg);
|
||||
}
|
||||
|
||||
static esp_err_t send_message_ex(const uint8_t *dest_mac,
|
||||
const alox_EspNowMessage *msg, bool wait_done) {
|
||||
uint8_t buf[ESPNOW_PB_MAX_SIZE];
|
||||
@ -455,6 +480,29 @@ esp_err_t esp_now_comm_send_accel_stream(const uint8_t mac[CLIENT_MAC_LEN],
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t esp_now_comm_send_tap_notify(const uint8_t mac[CLIENT_MAC_LEN],
|
||||
uint32_t client_id, bool single,
|
||||
bool double_tap, bool triple) {
|
||||
if (mac == NULL || !s_config.master) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
char mac_str[18];
|
||||
mac_to_str(mac, mac_str, sizeof(mac_str));
|
||||
esp_err_t err =
|
||||
send_tap_notify(mac, client_id, single, double_tap, triple);
|
||||
if (err == ESP_OK) {
|
||||
ESP_LOGI(TAG,
|
||||
"unicast SET_TAP_NOTIFY to %s: single=%d double=%d triple=%d "
|
||||
"client_id=%lu",
|
||||
mac_str, single, double_tap, triple, (unsigned long)client_id);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "unicast SET_TAP_NOTIFY to %s failed: %s", mac_str,
|
||||
esp_err_to_name(err));
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t esp_now_comm_send_accel_deadzone(const uint8_t mac[CLIENT_MAC_LEN],
|
||||
uint32_t client_id, uint32_t deadzone) {
|
||||
if (mac == NULL || !s_config.master) {
|
||||
@ -705,6 +753,30 @@ static void handle_slave_accel_stream(const uint8_t *master_mac,
|
||||
cfg->enable ? "on" : "off", mac_str, (unsigned long)my_id);
|
||||
}
|
||||
|
||||
static void handle_slave_tap_notify(const uint8_t *master_mac,
|
||||
const alox_EspNowTapNotify *cfg) {
|
||||
uint32_t my_id = s_own_mac[5];
|
||||
|
||||
if (cfg->client_id != 0 && cfg->client_id != my_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_slave_joined && !mac_equal(master_mac, s_master_mac)) {
|
||||
return;
|
||||
}
|
||||
|
||||
s_tap_notify_single = cfg->single;
|
||||
s_tap_notify_double = cfg->double_tap;
|
||||
s_tap_notify_triple = cfg->triple;
|
||||
|
||||
char mac_str[18];
|
||||
mac_to_str(master_mac, mac_str, sizeof(mac_str));
|
||||
ESP_LOGI(TAG,
|
||||
"tap notify single=%d double=%d triple=%d from master %s (id=%lu)",
|
||||
cfg->single, cfg->double_tap, cfg->triple, mac_str,
|
||||
(unsigned long)my_id);
|
||||
}
|
||||
|
||||
static void handle_slave_accel_deadzone(const uint8_t *master_mac,
|
||||
const alox_EspNowAccelDeadzone *cfg) {
|
||||
uint32_t my_id = s_own_mac[5];
|
||||
@ -749,6 +821,24 @@ static void handle_master_accel_sample(const uint8_t mac[CLIENT_MAC_LEN],
|
||||
}
|
||||
}
|
||||
|
||||
static void handle_master_tap_event(const uint8_t mac[CLIENT_MAC_LEN],
|
||||
const alox_EspNowTapEvent *event) {
|
||||
if (event == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
esp_err_t err =
|
||||
client_registry_update_tap(mac, event->slave_id, event->kind);
|
||||
if (err == ESP_ERR_NOT_FOUND) {
|
||||
return;
|
||||
}
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "tap event id=%lu kind=%lu rejected from %02x:…:%02x",
|
||||
(unsigned long)event->slave_id, (unsigned long)event->kind, mac[0],
|
||||
mac[5]);
|
||||
}
|
||||
}
|
||||
|
||||
static void handle_client_presence(const alox_EspNowSlavePresence *presence,
|
||||
const uint8_t mac[CLIENT_MAC_LEN]) {
|
||||
if (presence->network != s_config.network) {
|
||||
@ -855,6 +945,34 @@ static void slave_accel_stream_task(void *param) {
|
||||
}
|
||||
}
|
||||
|
||||
static void on_bma456_tap(bma456_tap_kind_t kind, void *ctx) {
|
||||
(void)ctx;
|
||||
|
||||
if (!s_slave_joined) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool enabled = false;
|
||||
switch (kind) {
|
||||
case BMA456_TAP_SINGLE:
|
||||
enabled = s_tap_notify_single;
|
||||
break;
|
||||
case BMA456_TAP_DOUBLE:
|
||||
enabled = s_tap_notify_double;
|
||||
break;
|
||||
case BMA456_TAP_TRIPLE:
|
||||
enabled = s_tap_notify_triple;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
(void)send_tap_event(s_master_mac, s_own_mac[5], (uint32_t)kind);
|
||||
}
|
||||
|
||||
static void slave_heartbeat_task(void *param) {
|
||||
(void)param;
|
||||
uint32_t last_battery_ms = 0;
|
||||
@ -942,6 +1060,12 @@ static void espnow_recv_cb(const esp_now_recv_info_t *info, const uint8_t *data,
|
||||
}
|
||||
handle_slave_accel_stream(info->src_addr, &msg.payload.accel_stream);
|
||||
break;
|
||||
case alox_EspNowMessage_tap_notify_tag:
|
||||
if (!s_slave_joined || !mac_equal(info->src_addr, s_master_mac)) {
|
||||
break;
|
||||
}
|
||||
handle_slave_tap_notify(info->src_addr, &msg.payload.tap_notify);
|
||||
break;
|
||||
case alox_EspNowMessage_battery_query_tag:
|
||||
if (!s_slave_joined || !mac_equal(info->src_addr, s_master_mac)) {
|
||||
break;
|
||||
@ -1007,6 +1131,12 @@ static void espnow_recv_cb(const esp_now_recv_info_t *info, const uint8_t *data,
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.which_payload == alox_EspNowMessage_tap_event_tag) {
|
||||
ensure_peer(info->src_addr);
|
||||
handle_master_tap_event(info->src_addr, &msg.payload.tap_event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.which_payload == alox_EspNowMessage_battery_report_tag) {
|
||||
ensure_peer(info->src_addr);
|
||||
handle_master_battery_report(info->src_addr, &msg.payload.battery_report);
|
||||
@ -1133,6 +1263,7 @@ esp_err_t esp_now_comm_init(const app_config_t *config) {
|
||||
ESP_LOGE(TAG, "failed to create accel stream task");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
bma456_set_tap_handler(on_bma456_tap, NULL);
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
|
||||
@ -13,6 +13,11 @@ esp_err_t esp_now_comm_init(const app_config_t *config);
|
||||
esp_err_t esp_now_comm_send_accel_stream(const uint8_t mac[CLIENT_MAC_LEN],
|
||||
uint32_t client_id, bool enable);
|
||||
|
||||
/** Master: configure tap notify flags on one slave. */
|
||||
esp_err_t esp_now_comm_send_tap_notify(const uint8_t mac[CLIENT_MAC_LEN],
|
||||
uint32_t client_id, bool single,
|
||||
bool double_tap, bool triple);
|
||||
|
||||
/** Master: unicast accel deadzone to one slave (client_id is echoed for filtering). */
|
||||
esp_err_t esp_now_comm_send_accel_deadzone(const uint8_t mac[CLIENT_MAC_LEN],
|
||||
uint32_t client_id, uint32_t deadzone);
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
#include "cmd_accel_deadzone.h"
|
||||
#include "cmd_accel_snapshot.h"
|
||||
#include "cmd_accel_stream.h"
|
||||
#include "cmd_tap_notify.h"
|
||||
#include "cmd_tap_snapshot.h"
|
||||
#include "cmd_espnow_unicast_test.h"
|
||||
#include "cmd_espnow_find_me.h"
|
||||
#include "cmd_restart.h"
|
||||
@ -182,6 +184,8 @@ void app_main(void) {
|
||||
cmd_accel_deadzone_register();
|
||||
cmd_accel_snapshot_register();
|
||||
cmd_accel_stream_register();
|
||||
cmd_tap_notify_register();
|
||||
cmd_tap_snapshot_register();
|
||||
cmd_espnow_unicast_test_register();
|
||||
cmd_espnow_find_me_register();
|
||||
cmd_restart_register();
|
||||
|
||||
@ -33,6 +33,12 @@ PB_BIND(alox_EspNowAccelSample, alox_EspNowAccelSample, AUTO)
|
||||
PB_BIND(alox_EspNowBatteryQuery, alox_EspNowBatteryQuery, AUTO)
|
||||
|
||||
|
||||
PB_BIND(alox_EspNowTapNotify, alox_EspNowTapNotify, AUTO)
|
||||
|
||||
|
||||
PB_BIND(alox_EspNowTapEvent, alox_EspNowTapEvent, AUTO)
|
||||
|
||||
|
||||
PB_BIND(alox_EspNowBatteryReport, alox_EspNowBatteryReport, AUTO)
|
||||
|
||||
|
||||
|
||||
@ -27,7 +27,9 @@ typedef enum _alox_EspNowMessageType {
|
||||
alox_EspNowMessageType_ESPNOW_SET_ACCEL_STREAM = 13,
|
||||
alox_EspNowMessageType_ESPNOW_LED_RING = 14,
|
||||
alox_EspNowMessageType_ESPNOW_BATTERY_QUERY = 15,
|
||||
alox_EspNowMessageType_ESPNOW_BATTERY_REPORT = 16
|
||||
alox_EspNowMessageType_ESPNOW_BATTERY_REPORT = 16,
|
||||
alox_EspNowMessageType_ESPNOW_SET_TAP_NOTIFY = 17,
|
||||
alox_EspNowMessageType_ESPNOW_TAP_EVENT = 18
|
||||
} alox_EspNowMessageType;
|
||||
|
||||
/* Struct definitions */
|
||||
@ -83,6 +85,21 @@ typedef struct _alox_EspNowBatteryQuery {
|
||||
uint32_t client_id;
|
||||
} alox_EspNowBatteryQuery;
|
||||
|
||||
/* * Master → slave: which tap kinds should be reported via ESP-NOW. */
|
||||
typedef struct _alox_EspNowTapNotify {
|
||||
uint32_t client_id;
|
||||
bool single;
|
||||
bool double_tap;
|
||||
bool triple;
|
||||
} alox_EspNowTapNotify;
|
||||
|
||||
/* * Slave → master: tap detected on BMA456 (event, not periodic). */
|
||||
typedef struct _alox_EspNowTapEvent {
|
||||
uint32_t slave_id;
|
||||
/* * 1=single, 2=double, 3=triple */
|
||||
uint32_t kind;
|
||||
} alox_EspNowTapEvent;
|
||||
|
||||
/* * Slave → master: LiPo voltages (periodic ~30 s and on query). */
|
||||
typedef struct _alox_EspNowBatteryReport {
|
||||
uint32_t client_id;
|
||||
@ -150,6 +167,8 @@ typedef struct _alox_EspNowMessage {
|
||||
alox_EspNowLedRing led_ring;
|
||||
alox_EspNowBatteryQuery battery_query;
|
||||
alox_EspNowBatteryReport battery_report;
|
||||
alox_EspNowTapNotify tap_notify;
|
||||
alox_EspNowTapEvent tap_event;
|
||||
} payload;
|
||||
} alox_EspNowMessage;
|
||||
|
||||
@ -160,8 +179,10 @@ extern "C" {
|
||||
|
||||
/* Helper constants for enums */
|
||||
#define _alox_EspNowMessageType_MIN alox_EspNowMessageType_ESPNOW_UNKNOWN
|
||||
#define _alox_EspNowMessageType_MAX alox_EspNowMessageType_ESPNOW_BATTERY_REPORT
|
||||
#define _alox_EspNowMessageType_ARRAYSIZE ((alox_EspNowMessageType)(alox_EspNowMessageType_ESPNOW_BATTERY_REPORT+1))
|
||||
#define _alox_EspNowMessageType_MAX alox_EspNowMessageType_ESPNOW_TAP_EVENT
|
||||
#define _alox_EspNowMessageType_ARRAYSIZE ((alox_EspNowMessageType)(alox_EspNowMessageType_ESPNOW_TAP_EVENT+1))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -191,6 +212,8 @@ extern "C" {
|
||||
#define alox_EspNowAccelStream_init_default {0, 0}
|
||||
#define alox_EspNowAccelSample_init_default {0, 0, 0, 0}
|
||||
#define alox_EspNowBatteryQuery_init_default {0}
|
||||
#define alox_EspNowTapNotify_init_default {0, 0, 0, 0}
|
||||
#define alox_EspNowTapEvent_init_default {0, 0}
|
||||
#define alox_EspNowBatteryReport_init_default {0, 0, 0, 0, 0}
|
||||
#define alox_EspNowLedRing_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
#define alox_EspNowOtaStart_init_default {0}
|
||||
@ -207,6 +230,8 @@ extern "C" {
|
||||
#define alox_EspNowAccelStream_init_zero {0, 0}
|
||||
#define alox_EspNowAccelSample_init_zero {0, 0, 0, 0}
|
||||
#define alox_EspNowBatteryQuery_init_zero {0}
|
||||
#define alox_EspNowTapNotify_init_zero {0, 0, 0, 0}
|
||||
#define alox_EspNowTapEvent_init_zero {0, 0}
|
||||
#define alox_EspNowBatteryReport_init_zero {0, 0, 0, 0, 0}
|
||||
#define alox_EspNowLedRing_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
#define alox_EspNowOtaStart_init_zero {0}
|
||||
@ -235,6 +260,12 @@ extern "C" {
|
||||
#define alox_EspNowAccelSample_y_tag 3
|
||||
#define alox_EspNowAccelSample_z_tag 4
|
||||
#define alox_EspNowBatteryQuery_client_id_tag 1
|
||||
#define alox_EspNowTapNotify_client_id_tag 1
|
||||
#define alox_EspNowTapNotify_single_tag 2
|
||||
#define alox_EspNowTapNotify_double_tap_tag 3
|
||||
#define alox_EspNowTapNotify_triple_tag 4
|
||||
#define alox_EspNowTapEvent_slave_id_tag 1
|
||||
#define alox_EspNowTapEvent_kind_tag 2
|
||||
#define alox_EspNowBatteryReport_client_id_tag 1
|
||||
#define alox_EspNowBatteryReport_lipo1_valid_tag 2
|
||||
#define alox_EspNowBatteryReport_lipo2_valid_tag 3
|
||||
@ -273,6 +304,8 @@ extern "C" {
|
||||
#define alox_EspNowMessage_led_ring_tag 15
|
||||
#define alox_EspNowMessage_battery_query_tag 16
|
||||
#define alox_EspNowMessage_battery_report_tag 17
|
||||
#define alox_EspNowMessage_tap_notify_tag 18
|
||||
#define alox_EspNowMessage_tap_event_tag 19
|
||||
|
||||
/* Struct field encoding specification for nanopb */
|
||||
#define alox_EspNowUnicastTest_FIELDLIST(X, a) \
|
||||
@ -330,6 +363,20 @@ X(a, STATIC, SINGULAR, UINT32, client_id, 1)
|
||||
#define alox_EspNowBatteryQuery_CALLBACK NULL
|
||||
#define alox_EspNowBatteryQuery_DEFAULT NULL
|
||||
|
||||
#define alox_EspNowTapNotify_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UINT32, client_id, 1) \
|
||||
X(a, STATIC, SINGULAR, BOOL, single, 2) \
|
||||
X(a, STATIC, SINGULAR, BOOL, double_tap, 3) \
|
||||
X(a, STATIC, SINGULAR, BOOL, triple, 4)
|
||||
#define alox_EspNowTapNotify_CALLBACK NULL
|
||||
#define alox_EspNowTapNotify_DEFAULT NULL
|
||||
|
||||
#define alox_EspNowTapEvent_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UINT32, slave_id, 1) \
|
||||
X(a, STATIC, SINGULAR, UINT32, kind, 2)
|
||||
#define alox_EspNowTapEvent_CALLBACK NULL
|
||||
#define alox_EspNowTapEvent_DEFAULT NULL
|
||||
|
||||
#define alox_EspNowBatteryReport_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UINT32, client_id, 1) \
|
||||
X(a, STATIC, SINGULAR, BOOL, lipo1_valid, 2) \
|
||||
@ -393,7 +440,9 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,accel_sample,payload.accel_sample),
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload,accel_stream,payload.accel_stream), 14) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload,led_ring,payload.led_ring), 15) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload,battery_query,payload.battery_query), 16) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload,battery_report,payload.battery_report), 17)
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload,battery_report,payload.battery_report), 17) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload,tap_notify,payload.tap_notify), 18) \
|
||||
X(a, STATIC, ONEOF, MESSAGE, (payload,tap_event,payload.tap_event), 19)
|
||||
#define alox_EspNowMessage_CALLBACK NULL
|
||||
#define alox_EspNowMessage_DEFAULT NULL
|
||||
#define alox_EspNowMessage_payload_discover_MSGTYPE alox_EspNowDiscover
|
||||
@ -412,6 +461,8 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,battery_report,payload.battery_repor
|
||||
#define alox_EspNowMessage_payload_led_ring_MSGTYPE alox_EspNowLedRing
|
||||
#define alox_EspNowMessage_payload_battery_query_MSGTYPE alox_EspNowBatteryQuery
|
||||
#define alox_EspNowMessage_payload_battery_report_MSGTYPE alox_EspNowBatteryReport
|
||||
#define alox_EspNowMessage_payload_tap_notify_MSGTYPE alox_EspNowTapNotify
|
||||
#define alox_EspNowMessage_payload_tap_event_MSGTYPE alox_EspNowTapEvent
|
||||
|
||||
extern const pb_msgdesc_t alox_EspNowUnicastTest_msg;
|
||||
extern const pb_msgdesc_t alox_EspNowFindMe_msg;
|
||||
@ -422,6 +473,8 @@ extern const pb_msgdesc_t alox_EspNowAccelDeadzone_msg;
|
||||
extern const pb_msgdesc_t alox_EspNowAccelStream_msg;
|
||||
extern const pb_msgdesc_t alox_EspNowAccelSample_msg;
|
||||
extern const pb_msgdesc_t alox_EspNowBatteryQuery_msg;
|
||||
extern const pb_msgdesc_t alox_EspNowTapNotify_msg;
|
||||
extern const pb_msgdesc_t alox_EspNowTapEvent_msg;
|
||||
extern const pb_msgdesc_t alox_EspNowBatteryReport_msg;
|
||||
extern const pb_msgdesc_t alox_EspNowLedRing_msg;
|
||||
extern const pb_msgdesc_t alox_EspNowOtaStart_msg;
|
||||
@ -440,6 +493,8 @@ extern const pb_msgdesc_t alox_EspNowMessage_msg;
|
||||
#define alox_EspNowAccelStream_fields &alox_EspNowAccelStream_msg
|
||||
#define alox_EspNowAccelSample_fields &alox_EspNowAccelSample_msg
|
||||
#define alox_EspNowBatteryQuery_fields &alox_EspNowBatteryQuery_msg
|
||||
#define alox_EspNowTapNotify_fields &alox_EspNowTapNotify_msg
|
||||
#define alox_EspNowTapEvent_fields &alox_EspNowTapEvent_msg
|
||||
#define alox_EspNowBatteryReport_fields &alox_EspNowBatteryReport_msg
|
||||
#define alox_EspNowLedRing_fields &alox_EspNowLedRing_msg
|
||||
#define alox_EspNowOtaStart_fields &alox_EspNowOtaStart_msg
|
||||
@ -465,6 +520,8 @@ extern const pb_msgdesc_t alox_EspNowMessage_msg;
|
||||
#define alox_EspNowOtaStart_size 6
|
||||
#define alox_EspNowOtaStatus_size 18
|
||||
#define alox_EspNowRestart_size 6
|
||||
#define alox_EspNowTapEvent_size 12
|
||||
#define alox_EspNowTapNotify_size 12
|
||||
#define alox_EspNowUnicastTest_size 6
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@ -22,6 +22,8 @@ enum EspNowMessageType {
|
||||
ESPNOW_LED_RING = 14;
|
||||
ESPNOW_BATTERY_QUERY = 15;
|
||||
ESPNOW_BATTERY_REPORT = 16;
|
||||
ESPNOW_SET_TAP_NOTIFY = 17;
|
||||
ESPNOW_TAP_EVENT = 18;
|
||||
}
|
||||
|
||||
message EspNowUnicastTest {
|
||||
@ -76,6 +78,21 @@ message EspNowBatteryQuery {
|
||||
uint32 client_id = 1;
|
||||
}
|
||||
|
||||
/** Master → slave: which tap kinds should be reported via ESP-NOW. */
|
||||
message EspNowTapNotify {
|
||||
uint32 client_id = 1;
|
||||
bool single = 2;
|
||||
bool double_tap = 3;
|
||||
bool triple = 4;
|
||||
}
|
||||
|
||||
/** Slave → master: tap detected on BMA456 (event, not periodic). */
|
||||
message EspNowTapEvent {
|
||||
uint32 slave_id = 1;
|
||||
/** 1=single, 2=double, 3=triple */
|
||||
uint32 kind = 2;
|
||||
}
|
||||
|
||||
/** Slave → master: LiPo voltages (periodic ~30 s and on query). */
|
||||
message EspNowBatteryReport {
|
||||
uint32 client_id = 1;
|
||||
@ -139,5 +156,7 @@ message EspNowMessage {
|
||||
EspNowLedRing led_ring = 15;
|
||||
EspNowBatteryQuery battery_query = 16;
|
||||
EspNowBatteryReport battery_report = 17;
|
||||
EspNowTapNotify tap_notify = 18;
|
||||
EspNowTapEvent tap_event = 19;
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,6 +63,21 @@ PB_BIND(alox_AccelSample, alox_AccelSample, AUTO)
|
||||
PB_BIND(alox_AccelSnapshotResponse, alox_AccelSnapshotResponse, 2)
|
||||
|
||||
|
||||
PB_BIND(alox_TapNotifyRequest, alox_TapNotifyRequest, 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_TapSnapshotResponse, alox_TapSnapshotResponse, 2)
|
||||
|
||||
|
||||
PB_BIND(alox_EspNowUnicastTestRequest, alox_EspNowUnicastTestRequest, AUTO)
|
||||
|
||||
|
||||
@ -111,3 +126,5 @@ PB_BIND(alox_OtaSlaveProgressResponse, alox_OtaSlaveProgressResponse, 2)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -30,9 +30,18 @@ typedef enum _alox_MessageType {
|
||||
alox_MessageType_RESTART = 23,
|
||||
alox_MessageType_ACCEL_SNAPSHOT = 24,
|
||||
alox_MessageType_ACCEL_STREAM = 25,
|
||||
alox_MessageType_BATTERY_STATUS = 26
|
||||
alox_MessageType_BATTERY_STATUS = 26,
|
||||
alox_MessageType_TAP_NOTIFY = 27,
|
||||
alox_MessageType_TAP_SNAPSHOT = 28
|
||||
} alox_MessageType;
|
||||
|
||||
typedef enum _alox_TapKind {
|
||||
alox_TapKind_TAP_NONE = 0,
|
||||
alox_TapKind_TAP_SINGLE = 1,
|
||||
alox_TapKind_TAP_DOUBLE = 2,
|
||||
alox_TapKind_TAP_TRIPLE = 3
|
||||
} alox_TapKind;
|
||||
|
||||
/* Struct definitions */
|
||||
typedef struct _alox_Ack {
|
||||
char dummy_field;
|
||||
@ -59,6 +68,10 @@ typedef struct _alox_ClientInfo {
|
||||
uint32_t version;
|
||||
/* * Master: ESP-NOW accel stream enabled for this slave. */
|
||||
bool accel_stream_enabled;
|
||||
/* * Master: ESP-NOW tap notify flags for this slave. */
|
||||
bool tap_notify_single;
|
||||
bool tap_notify_double;
|
||||
bool tap_notify_triple;
|
||||
} alox_ClientInfo;
|
||||
|
||||
typedef struct _alox_ClientInfoResponse {
|
||||
@ -160,6 +173,42 @@ typedef struct _alox_AccelSnapshotResponse {
|
||||
alox_AccelSample samples[16];
|
||||
} alox_AccelSnapshotResponse;
|
||||
|
||||
/* * Host → master: enable/disable tap ESP-NOW notify per slave (single/double/triple). */
|
||||
typedef struct _alox_TapNotifyRequest {
|
||||
bool write;
|
||||
uint32_t client_id;
|
||||
bool all_clients;
|
||||
bool single;
|
||||
bool double_tap;
|
||||
bool triple;
|
||||
} alox_TapNotifyRequest;
|
||||
|
||||
typedef struct _alox_TapNotifyResponse {
|
||||
uint32_t client_id;
|
||||
bool success;
|
||||
uint32_t slaves_updated;
|
||||
bool single;
|
||||
bool double_tap;
|
||||
bool triple;
|
||||
} alox_TapNotifyResponse;
|
||||
|
||||
/* * Host → master: read cached tap events (discarded after reply or when age > 16 ms). */
|
||||
typedef struct _alox_TapSnapshotRequest {
|
||||
uint32_t client_id;
|
||||
} alox_TapSnapshotRequest;
|
||||
|
||||
typedef struct _alox_TapEvent {
|
||||
uint32_t client_id;
|
||||
bool valid;
|
||||
alox_TapKind kind;
|
||||
uint32_t age_ms;
|
||||
} alox_TapEvent;
|
||||
|
||||
typedef struct _alox_TapSnapshotResponse {
|
||||
pb_size_t events_count;
|
||||
alox_TapEvent events[16];
|
||||
} alox_TapSnapshotResponse;
|
||||
|
||||
typedef struct _alox_EspNowUnicastTestRequest {
|
||||
uint32_t client_id;
|
||||
uint32_t seq;
|
||||
@ -304,6 +353,10 @@ typedef struct _alox_UartMessage {
|
||||
alox_AccelStreamResponse accel_stream_response;
|
||||
alox_BatteryStatusRequest battery_status_request;
|
||||
alox_BatteryStatusResponse battery_status_response;
|
||||
alox_TapNotifyRequest tap_notify_request;
|
||||
alox_TapNotifyResponse tap_notify_response;
|
||||
alox_TapSnapshotRequest tap_snapshot_request;
|
||||
alox_TapSnapshotResponse tap_snapshot_response;
|
||||
} payload;
|
||||
} alox_UartMessage;
|
||||
|
||||
@ -314,8 +367,12 @@ extern "C" {
|
||||
|
||||
/* Helper constants for enums */
|
||||
#define _alox_MessageType_MIN alox_MessageType_UNKNOWN
|
||||
#define _alox_MessageType_MAX alox_MessageType_BATTERY_STATUS
|
||||
#define _alox_MessageType_ARRAYSIZE ((alox_MessageType)(alox_MessageType_BATTERY_STATUS+1))
|
||||
#define _alox_MessageType_MAX alox_MessageType_TAP_SNAPSHOT
|
||||
#define _alox_MessageType_ARRAYSIZE ((alox_MessageType)(alox_MessageType_TAP_SNAPSHOT+1))
|
||||
|
||||
#define _alox_TapKind_MIN alox_TapKind_TAP_NONE
|
||||
#define _alox_TapKind_MAX alox_TapKind_TAP_TRIPLE
|
||||
#define _alox_TapKind_ARRAYSIZE ((alox_TapKind)(alox_TapKind_TAP_TRIPLE+1))
|
||||
|
||||
#define alox_UartMessage_type_ENUMTYPE alox_MessageType
|
||||
|
||||
@ -338,6 +395,12 @@ extern "C" {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#define alox_TapEvent_kind_ENUMTYPE alox_TapKind
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -358,7 +421,7 @@ extern "C" {
|
||||
#define alox_Ack_init_default {0}
|
||||
#define alox_EchoPayload_init_default {{{NULL}, NULL}}
|
||||
#define alox_VersionResponse_init_default {0, {{NULL}, NULL}, {{NULL}, NULL}}
|
||||
#define alox_ClientInfo_init_default {0, 0, 0, {{NULL}, NULL}, 0, 0, 0, 0}
|
||||
#define alox_ClientInfo_init_default {0, 0, 0, {{NULL}, NULL}, 0, 0, 0, 0, 0, 0, 0}
|
||||
#define alox_ClientInfoResponse_init_default {{{NULL}, NULL}}
|
||||
#define alox_ClientInput_init_default {0, 0, 0, 0}
|
||||
#define alox_ClientInputResponse_init_default {{{NULL}, NULL}}
|
||||
@ -373,6 +436,11 @@ extern "C" {
|
||||
#define alox_AccelSnapshotRequest_init_default {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_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_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_EspNowUnicastTestRequest_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}
|
||||
@ -392,7 +460,7 @@ extern "C" {
|
||||
#define alox_Ack_init_zero {0}
|
||||
#define alox_EchoPayload_init_zero {{{NULL}, NULL}}
|
||||
#define alox_VersionResponse_init_zero {0, {{NULL}, NULL}, {{NULL}, NULL}}
|
||||
#define alox_ClientInfo_init_zero {0, 0, 0, {{NULL}, NULL}, 0, 0, 0, 0}
|
||||
#define alox_ClientInfo_init_zero {0, 0, 0, {{NULL}, NULL}, 0, 0, 0, 0, 0, 0, 0}
|
||||
#define alox_ClientInfoResponse_init_zero {{{NULL}, NULL}}
|
||||
#define alox_ClientInput_init_zero {0, 0, 0, 0}
|
||||
#define alox_ClientInputResponse_init_zero {{{NULL}, NULL}}
|
||||
@ -407,6 +475,11 @@ extern "C" {
|
||||
#define alox_AccelSnapshotRequest_init_zero {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_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_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_EspNowUnicastTestRequest_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}
|
||||
@ -436,6 +509,9 @@ extern "C" {
|
||||
#define alox_ClientInfo_last_success_ping_tag 6
|
||||
#define alox_ClientInfo_version_tag 7
|
||||
#define alox_ClientInfo_accel_stream_enabled_tag 8
|
||||
#define alox_ClientInfo_tap_notify_single_tag 9
|
||||
#define alox_ClientInfo_tap_notify_double_tag 10
|
||||
#define alox_ClientInfo_tap_notify_triple_tag 11
|
||||
#define alox_ClientInfoResponse_clients_tag 1
|
||||
#define alox_ClientInput_id_tag 1
|
||||
#define alox_ClientInput_lage_x_tag 2
|
||||
@ -476,6 +552,24 @@ extern "C" {
|
||||
#define alox_AccelSample_z_tag 5
|
||||
#define alox_AccelSample_age_ms_tag 6
|
||||
#define alox_AccelSnapshotResponse_samples_tag 1
|
||||
#define alox_TapNotifyRequest_write_tag 1
|
||||
#define alox_TapNotifyRequest_client_id_tag 2
|
||||
#define alox_TapNotifyRequest_all_clients_tag 3
|
||||
#define alox_TapNotifyRequest_single_tag 4
|
||||
#define alox_TapNotifyRequest_double_tap_tag 5
|
||||
#define alox_TapNotifyRequest_triple_tag 6
|
||||
#define alox_TapNotifyResponse_client_id_tag 1
|
||||
#define alox_TapNotifyResponse_success_tag 2
|
||||
#define alox_TapNotifyResponse_slaves_updated_tag 3
|
||||
#define alox_TapNotifyResponse_single_tag 4
|
||||
#define alox_TapNotifyResponse_double_tap_tag 5
|
||||
#define alox_TapNotifyResponse_triple_tag 6
|
||||
#define alox_TapSnapshotRequest_client_id_tag 1
|
||||
#define alox_TapEvent_client_id_tag 1
|
||||
#define alox_TapEvent_valid_tag 2
|
||||
#define alox_TapEvent_kind_tag 3
|
||||
#define alox_TapEvent_age_ms_tag 4
|
||||
#define alox_TapSnapshotResponse_events_tag 1
|
||||
#define alox_EspNowUnicastTestRequest_client_id_tag 1
|
||||
#define alox_EspNowUnicastTestRequest_seq_tag 2
|
||||
#define alox_EspNowUnicastTestResponse_success_tag 1
|
||||
@ -550,6 +644,10 @@ extern "C" {
|
||||
#define alox_UartMessage_accel_stream_response_tag 26
|
||||
#define alox_UartMessage_battery_status_request_tag 27
|
||||
#define alox_UartMessage_battery_status_response_tag 28
|
||||
#define alox_UartMessage_tap_notify_request_tag 29
|
||||
#define alox_UartMessage_tap_notify_response_tag 30
|
||||
#define alox_UartMessage_tap_snapshot_request_tag 31
|
||||
#define alox_UartMessage_tap_snapshot_response_tag 32
|
||||
|
||||
/* Struct field encoding specification for nanopb */
|
||||
#define alox_UartMessage_FIELDLIST(X, a) \
|
||||
@ -580,7 +678,11 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,accel_snapshot_response,payload.acce
|
||||
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,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_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)
|
||||
#define alox_UartMessage_CALLBACK NULL
|
||||
#define alox_UartMessage_DEFAULT NULL
|
||||
#define alox_UartMessage_payload_ack_payload_MSGTYPE alox_Ack
|
||||
@ -610,6 +712,10 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,battery_status_response,payload.batt
|
||||
#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_response_MSGTYPE alox_BatteryStatusResponse
|
||||
#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_snapshot_request_MSGTYPE alox_TapSnapshotRequest
|
||||
#define alox_UartMessage_payload_tap_snapshot_response_MSGTYPE alox_TapSnapshotResponse
|
||||
|
||||
#define alox_Ack_FIELDLIST(X, a) \
|
||||
|
||||
@ -636,7 +742,10 @@ X(a, CALLBACK, SINGULAR, BYTES, mac, 4) \
|
||||
X(a, STATIC, SINGULAR, UINT32, last_ping, 5) \
|
||||
X(a, STATIC, SINGULAR, UINT32, last_success_ping, 6) \
|
||||
X(a, STATIC, SINGULAR, UINT32, version, 7) \
|
||||
X(a, STATIC, SINGULAR, BOOL, accel_stream_enabled, 8)
|
||||
X(a, STATIC, SINGULAR, BOOL, accel_stream_enabled, 8) \
|
||||
X(a, STATIC, SINGULAR, BOOL, tap_notify_single, 9) \
|
||||
X(a, STATIC, SINGULAR, BOOL, tap_notify_double, 10) \
|
||||
X(a, STATIC, SINGULAR, BOOL, tap_notify_triple, 11)
|
||||
#define alox_ClientInfo_CALLBACK pb_default_field_callback
|
||||
#define alox_ClientInfo_DEFAULT NULL
|
||||
|
||||
@ -742,6 +851,45 @@ X(a, STATIC, REPEATED, MESSAGE, samples, 1)
|
||||
#define alox_AccelSnapshotResponse_DEFAULT NULL
|
||||
#define alox_AccelSnapshotResponse_samples_MSGTYPE alox_AccelSample
|
||||
|
||||
#define alox_TapNotifyRequest_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, BOOL, write, 1) \
|
||||
X(a, STATIC, SINGULAR, UINT32, client_id, 2) \
|
||||
X(a, STATIC, SINGULAR, BOOL, all_clients, 3) \
|
||||
X(a, STATIC, SINGULAR, BOOL, single, 4) \
|
||||
X(a, STATIC, SINGULAR, BOOL, double_tap, 5) \
|
||||
X(a, STATIC, SINGULAR, BOOL, triple, 6)
|
||||
#define alox_TapNotifyRequest_CALLBACK NULL
|
||||
#define alox_TapNotifyRequest_DEFAULT NULL
|
||||
|
||||
#define alox_TapNotifyResponse_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UINT32, client_id, 1) \
|
||||
X(a, STATIC, SINGULAR, BOOL, success, 2) \
|
||||
X(a, STATIC, SINGULAR, UINT32, slaves_updated, 3) \
|
||||
X(a, STATIC, SINGULAR, BOOL, single, 4) \
|
||||
X(a, STATIC, SINGULAR, BOOL, double_tap, 5) \
|
||||
X(a, STATIC, SINGULAR, BOOL, triple, 6)
|
||||
#define alox_TapNotifyResponse_CALLBACK 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) \
|
||||
X(a, STATIC, SINGULAR, UINT32, client_id, 1) \
|
||||
X(a, STATIC, SINGULAR, BOOL, valid, 2) \
|
||||
X(a, STATIC, SINGULAR, UENUM, kind, 3) \
|
||||
X(a, STATIC, SINGULAR, UINT32, age_ms, 4)
|
||||
#define alox_TapEvent_CALLBACK 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_EspNowUnicastTestRequest_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UINT32, client_id, 1) \
|
||||
X(a, STATIC, SINGULAR, UINT32, seq, 2)
|
||||
@ -869,6 +1017,11 @@ 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_AccelSnapshotResponse_msg;
|
||||
extern const pb_msgdesc_t alox_TapNotifyRequest_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_TapSnapshotResponse_msg;
|
||||
extern const pb_msgdesc_t alox_EspNowUnicastTestRequest_msg;
|
||||
extern const pb_msgdesc_t alox_EspNowUnicastTestResponse_msg;
|
||||
extern const pb_msgdesc_t alox_LedRingProgressRequest_msg;
|
||||
@ -905,6 +1058,11 @@ extern const pb_msgdesc_t alox_OtaSlaveProgressResponse_msg;
|
||||
#define alox_AccelSnapshotRequest_fields &alox_AccelSnapshotRequest_msg
|
||||
#define alox_AccelSample_fields &alox_AccelSample_msg
|
||||
#define alox_AccelSnapshotResponse_fields &alox_AccelSnapshotResponse_msg
|
||||
#define alox_TapNotifyRequest_fields &alox_TapNotifyRequest_msg
|
||||
#define alox_TapNotifyResponse_fields &alox_TapNotifyResponse_msg
|
||||
#define alox_TapSnapshotRequest_fields &alox_TapSnapshotRequest_msg
|
||||
#define alox_TapEvent_fields &alox_TapEvent_msg
|
||||
#define alox_TapSnapshotResponse_fields &alox_TapSnapshotResponse_msg
|
||||
#define alox_EspNowUnicastTestRequest_fields &alox_EspNowUnicastTestRequest_msg
|
||||
#define alox_EspNowUnicastTestResponse_fields &alox_EspNowUnicastTestResponse_msg
|
||||
#define alox_LedRingProgressRequest_fields &alox_LedRingProgressRequest_msg
|
||||
@ -957,6 +1115,11 @@ extern const pb_msgdesc_t alox_OtaSlaveProgressResponse_msg;
|
||||
#define alox_OtaStatusPayload_size 24
|
||||
#define alox_RestartRequest_size 6
|
||||
#define alox_RestartResponse_size 8
|
||||
#define alox_TapEvent_size 16
|
||||
#define alox_TapNotifyRequest_size 16
|
||||
#define alox_TapNotifyResponse_size 20
|
||||
#define alox_TapSnapshotRequest_size 6
|
||||
#define alox_TapSnapshotResponse_size 288
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
|
||||
@ -25,6 +25,8 @@ enum MessageType {
|
||||
ACCEL_SNAPSHOT = 24;
|
||||
ACCEL_STREAM = 25;
|
||||
BATTERY_STATUS = 26;
|
||||
TAP_NOTIFY = 27;
|
||||
TAP_SNAPSHOT = 28;
|
||||
}
|
||||
|
||||
message UartMessage {
|
||||
@ -57,6 +59,10 @@ message UartMessage {
|
||||
AccelStreamResponse accel_stream_response = 26;
|
||||
BatteryStatusRequest battery_status_request = 27;
|
||||
BatteryStatusResponse battery_status_response = 28;
|
||||
TapNotifyRequest tap_notify_request = 29;
|
||||
TapNotifyResponse tap_notify_response = 30;
|
||||
TapSnapshotRequest tap_snapshot_request = 31;
|
||||
TapSnapshotResponse tap_snapshot_response = 32;
|
||||
}
|
||||
}
|
||||
|
||||
@ -83,6 +89,10 @@ message ClientInfo {
|
||||
uint32 version = 7;
|
||||
/** Master: ESP-NOW accel stream enabled for this slave. */
|
||||
bool accel_stream_enabled = 8;
|
||||
/** Master: ESP-NOW tap notify flags for this slave. */
|
||||
bool tap_notify_single = 9;
|
||||
bool tap_notify_double = 10;
|
||||
bool tap_notify_triple = 11;
|
||||
}
|
||||
|
||||
message ClientInfoResponse {
|
||||
@ -180,6 +190,48 @@ message AccelSnapshotResponse {
|
||||
repeated AccelSample samples = 1 [(nanopb).max_count = 16];
|
||||
}
|
||||
|
||||
/** Host → master: enable/disable tap ESP-NOW notify per slave (single/double/triple). */
|
||||
message TapNotifyRequest {
|
||||
bool write = 1;
|
||||
uint32 client_id = 2;
|
||||
bool all_clients = 3;
|
||||
bool single = 4;
|
||||
bool double_tap = 5;
|
||||
bool triple = 6;
|
||||
}
|
||||
|
||||
message TapNotifyResponse {
|
||||
uint32 client_id = 1;
|
||||
bool success = 2;
|
||||
uint32 slaves_updated = 3;
|
||||
bool single = 4;
|
||||
bool double_tap = 5;
|
||||
bool triple = 6;
|
||||
}
|
||||
|
||||
enum TapKind {
|
||||
TAP_NONE = 0;
|
||||
TAP_SINGLE = 1;
|
||||
TAP_DOUBLE = 2;
|
||||
TAP_TRIPLE = 3;
|
||||
}
|
||||
|
||||
/** Host → master: read cached tap events (discarded after reply or when age > 16 ms). */
|
||||
message TapSnapshotRequest {
|
||||
uint32 client_id = 1;
|
||||
}
|
||||
|
||||
message TapEvent {
|
||||
uint32 client_id = 1;
|
||||
bool valid = 2;
|
||||
TapKind kind = 3;
|
||||
uint32 age_ms = 4;
|
||||
}
|
||||
|
||||
message TapSnapshotResponse {
|
||||
repeated TapEvent events = 1 [(nanopb).max_count = 16];
|
||||
}
|
||||
|
||||
message EspNowUnicastTestRequest {
|
||||
uint32 client_id = 1;
|
||||
uint32 seq = 2;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user