Add ESP-NOW heartbeat, client timeout, and slave reconnect.

Slaves send HEARTBEAT every 1s; the master marks clients inactive after
3s without traffic and reactivates on reconnect. CLIENT_INFO reports
last_ping as milliseconds since the last packet, not uptime.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-18 22:39:10 +02:00
co-authored by Cursor
parent 16bfbd1091
commit 6cdca4f3ad
6 changed files with 282 additions and 85 deletions
+7 -4
View File
@@ -77,6 +77,7 @@ WiFi is brought up in STA mode (no AP association). Channel = `app_config.networ
|------|-------|-----------|---------|
| `DISCOVER` | 1 | Master → broadcast `FF:FF:FF:FF:FF:FF` | Master is searching for slaves |
| `SLAVE_INFO` | 2 | Slave → master | Slave registration |
| `HEARTBEAT` | 3 | Slave → master | Keep-alive (header only, no extra payload) |
`SLAVE_INFO` payload (after header bytes 02):
@@ -88,9 +89,9 @@ WiFi is brought up in STA mode (no AP association). Channel = `app_config.networ
| `available` | uint8 | 1 = available |
| `used` | uint8 | 0 = unused |
**Master:** task `espnow_disc` sends `DISCOVER` every **500 ms** on the configured network. Logs `slave joined id=… mac=… ver=…` when a new slave is seen (up to 16 entries).
**Master:** task `espnow_disc` sends `DISCOVER` every **500 ms** on the configured network. Logs `slave joined id=… mac=… ver=…` when a new slave is seen (up to 16 entries). Task `espnow_mon` runs every **1 s** and marks a client **inactive** (`available = false`) if no `SLAVE_INFO` or `HEARTBEAT` was received for **3 s** (three missed 1 s heartbeats). A later heartbeat sets `available` true again and logs reactivation.
**Slave:** on first matching `DISCOVER`, logs `joined network N, master …`, sends `SLAVE_INFO` once, then ignores further discovers from that master (no repeat log or reply).
**Slave:** on first matching `DISCOVER`, logs `joined network N, master …`, sends `SLAVE_INFO` once, then sends `HEARTBEAT` to the master every **1 s**. While joined, periodic discovers from the same master refresh a “master alive” timer; if no discover arrives for **5 s**, the slave treats the master as lost, clears join state, and will register again on the next discover (reconnect). Discover from a different master is ignored while already joined.
Monitor via USB-JTAG (`/dev/ttyACM0`) while using a USB-serial adapter on **GPIO2/3** (`/dev/ttyUSB0`) for UART — they are different interfaces.
@@ -188,7 +189,7 @@ Encoding: `uart_send_uart_message()` in `uart_proto.c`.
**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 boot, updated on each `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 01000 while the slave is heartbeating every 1 s).
## Client registry
@@ -196,9 +197,11 @@ Fields per client: `id`, `mac`, `version`, `available`, `used`, `last_ping`, `la
|-----|-------------|
| `client_registry_init()` | Clear all slots (called from `esp_now_comm_init`) |
| `client_registry_upsert(mac, id, version, …)` | Add or refresh client; updates ping timestamps |
| `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 |
Slaves register when the master receives `SLAVE_INFO` on the matching network.
Slaves register when the master receives `SLAVE_INFO` on the matching network; `HEARTBEAT` keeps them marked available.
## Host tool (`goTool/`)