Make controller input best-effort and harden OTA rollback.
Accel/tap use fire-and-forget ESP-NOW with a 10 ms FreeRTOS-safe cadence, OTA recovers from seq gaps and auto-reboots after success, and bootloader app rollback confirms after a stable boot. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -109,7 +109,7 @@ sequenceDiagram
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Host UART vs BLE:** Nur einer aktiv (DIP8). Gleiches Framing (`0xAA`…`0xCC`) und dieselbe `cmd_queue`. BLE: NUS-ähnliche GATT-Char, Write=RX, Notify=TX. `gotool serve` verbindet UART oder BLE über Dashboard/REST/WS (`managedHost`); **OTA** über UART oder BLE (ATT MTU ≥ ~219). Bei BLE-Host und aktivem Accel-Stream pusht `ble_stream` periodisch `BLE_STREAM_TICK` (8–16 ms; pausiert während OTA) mit `esp_timestamp_us` (Live-Pfad bevorzugt Tick statt CACHE_STATUS-Poll).
|
||||
**Host UART vs BLE:** Nur einer aktiv (DIP8). Gleiches Framing (`0xAA`…`0xCC`) und dieselbe `cmd_queue`. BLE: NUS-ähnliche GATT-Char, Write=RX, Notify=TX. `gotool serve` verbindet UART oder BLE über Dashboard/REST/WS (`managedHost`); **OTA** über UART oder BLE (ATT MTU ≥ ~219). Bei BLE-Host und aktivem Accel-Stream pusht `ble_stream` periodisch `BLE_STREAM_TICK` (10–16 ms, Default 10; pausiert während OTA) mit `esp_timestamp_us` (Live-Pfad bevorzugt Tick statt CACHE_STATUS-Poll).
|
||||
|
||||
|
||||
---
|
||||
@@ -315,7 +315,7 @@ Alle Master-Sends laufen über `send_message()` / `send_message_ex()`:
|
||||
|
||||
| Nachricht | Task / Trigger | Master-Ziel |
|
||||
|-----------|----------------|-------------|
|
||||
| `ESPNOW_ACCEL_SAMPLE` | `slave_accel_stream_task` 16 ms | `client_registry_update_accel()` |
|
||||
| `ESPNOW_ACCEL_SAMPLE` | `slave_accel_stream_task` 10 ms (best-effort) | `client_registry_update_accel()` |
|
||||
| `ESPNOW_TAP_EVENT` | BMA456-IRQ → `on_bma456_tap` | `client_registry_update_tap()` |
|
||||
| `ESPNOW_BATTERY_REPORT` | Heartbeat + nach Join | `client_registry_update_battery()` |
|
||||
| `ESPNOW_SLAVE_INFO` / `HEARTBEAT` | `slave_tx_task` / Heartbeat-Task | `client_registry_heartbeat()` |
|
||||
@@ -325,7 +325,7 @@ Alle Master-Sends laufen über `send_message()` / `send_message_ex()`:
|
||||
|
||||
### 6.5 OTA über ESP-NOW
|
||||
|
||||
Nach erfolgreichem UART-OTA auf dem Master (oder `OTA_START_ESPNOW`): `ota_espnow.c` liest gestagte Partition, sendet `OTA_START` / `PAYLOAD` (≤200 B, `send_message_ex` mit ACK-Semaphore) / `OTA_END` an alle **available** Slaves.
|
||||
Nach erfolgreichem UART-OTA auf dem Master (oder `OTA_START_ESPNOW`): `ota_espnow.c` liest gestagte Partition, sendet `OTA_START` / `PAYLOAD` (≤200 B, `send_message_ex` mit ACK-Semaphore) / `OTA_END` an alle **available** Slaves. Anschließend Reboot; Bootloader führt neues Image als `PENDING_VERIFY`, `ota_confirm` bestätigt nach stabilem Boot.
|
||||
|
||||
Gemeinsamer Flash-Puffer: `ota_uart.c` (4 KiB Blockgröße).
|
||||
|
||||
@@ -348,7 +348,7 @@ flowchart TB
|
||||
|-------|-------------------|-------------------|
|
||||
| Presence, Version | HEARTBEAT / SLAVE_INFO | `CLIENT_INFO` |
|
||||
| Accel-Stream-Samples | `ESPNOW_ACCEL_SAMPLE` | `CACHE_STATUS` |
|
||||
| Tap-Events (16 ms Cache) | `ESPNOW_TAP_EVENT` | `CACHE_STATUS` |
|
||||
| Tap-Events (10 ms Cache) | `ESPNOW_TAP_EVENT` | `CACHE_STATUS` |
|
||||
| Batterie | `ESPNOW_BATTERY_REPORT` + Master-ADC | `BATTERY_STATUS` |
|
||||
| Konfig-Flags | Handler + ESP-NOW | `CLIENT_INFO`, diverse GET |
|
||||
|
||||
@@ -366,7 +366,7 @@ flowchart TB
|
||||
| `espnow_mon` | Master | 1 s Timeout + Master-Battery |
|
||||
| `espnow_stx` | Slave | Queue: SLAVE_INFO, Battery nach Join |
|
||||
| `espnow_hb` | Slave | 1 s Heartbeat + 30 s Battery |
|
||||
| `espnow_accel` | Slave | 16 ms Accel wenn Stream an |
|
||||
| `espnow_accel` | Slave | 10 ms Accel best-effort wenn Stream an |
|
||||
| `bma456_poll` | Beide | 10 Hz (`bosch456.c`) |
|
||||
| `vTaskLedRing` | Beide | LED-Ring-Befehle |
|
||||
|
||||
@@ -391,6 +391,7 @@ flowchart TB
|
||||
| LED | `main/led_ring.c` | 95 LEDs, Digit-Maps |
|
||||
| OTA UART | `main/ota_uart.c`, `cmd/cmd_ota.c` | A/B-Partition Upload |
|
||||
| OTA ESP-NOW | `main/ota_espnow.c` | Verteilung an Slaves |
|
||||
| OTA Confirm | `main/ota_confirm.c` | Nach ~8 s `mark_app_valid` (Rollback) |
|
||||
| Einstellungen NVS | `main/pod_settings.c` | Accel-Deadzone persistent |
|
||||
| Board | `main/board_input.c` | Taster, LiPo-ADC |
|
||||
| Protobuf Schemas | `main/proto/*.proto` | Vertrag UART / ESP-NOW |
|
||||
|
||||
@@ -55,7 +55,7 @@ Zielbild für den Host: Befehle an den Master senden; der Master steuert Slaves
|
||||
| LiPo ADC 1 | 1 | `board_input.c` |
|
||||
| LiPo ADC 2 | 11 | `board_input.c` |
|
||||
|
||||
**Host-Protokoll (UART oder BLE):** Gleiches Framing (`0xAA`…`0xCC`) + nanopb. **UART:** `UART_NUM_1`, **921600** Baud, 8N1. **BLE:** Gerät `Powerpod`, NUS-UUIDs `6e400001`/`6e400002` (Write=RX, Notify=TX). goTool CLI: `-port …` oder `-ble` / `-ble-name Powerpod`. **`gotool serve`** startet ohne Pflicht-Gerät; Dashboard/REST/WS verbinden UART oder BLE dynamisch (`-port`/`-ble` nur optionale Vorbelegung). **OTA** über UART oder BLE (DIP8); BLE braucht ATT MTU ≥ ~219 für 200-B-Chunks. Bei BLE + Accel-Stream: periodische `BLE_STREAM_TICK`-Notifies (`period_ms` 8–16; während OTA pausiert).
|
||||
**Host-Protokoll (UART oder BLE):** Gleiches Framing (`0xAA`…`0xCC`) + nanopb. **UART:** `UART_NUM_1`, **921600** Baud, 8N1. **BLE:** Gerät `Powerpod`, NUS-UUIDs `6e400001`/`6e400002` (Write=RX, Notify=TX). goTool CLI: `-port …` oder `-ble` / `-ble-name Powerpod`. **`gotool serve`** startet ohne Pflicht-Gerät; Dashboard/REST/WS verbinden UART oder BLE dynamisch (`-port`/`-ble` nur optionale Vorbelegung). **OTA** über UART oder BLE (DIP8); BLE braucht ATT MTU ≥ ~219 für 200-B-Chunks. Bei BLE + Accel-Stream: periodische `BLE_STREAM_TICK`-Notifies (`period_ms` 10–16; während OTA pausiert).
|
||||
|
||||
**ESP-IDF-Log (Debug):** `esp_log_*` geht auf **UART0** (Standard-Konsole, typisch USB am Dev-Board, **115200** Baud, `CONFIG_ESP_CONSOLE_UART_NUM=0`). Das ist **getrennt** vom Host-UART1 — goTool liest keine `esp_log`-Ausgabe. Ohne angeschlossenes Debug-Kabel werden aktivierte Logs trotzdem formatiert und an UART0 gesendet (CPU-Overhead bleibt); nur `ESP_LOG_NONE` / Level-Filter vermeiden die Arbeit.
|
||||
|
||||
@@ -177,7 +177,7 @@ UART_CMD_REQ(&uart_msg, alox_UartMessage_accel_deadzone_request_tag, accel_deadz
|
||||
| `ESPNOW_HEARTBEAT` | S→M | `heartbeat` | Keepalive 1 s |
|
||||
| `ESPNOW_SET_ACCEL_DEADZONE` | M→S | `accel_deadzone` | Deadzone LSB |
|
||||
| `ESPNOW_SET_TAP_CONFIG` | M→S | `tap_config` | Multitap-Parameter + NVS |
|
||||
| `ESPNOW_SET_ACCEL_STREAM` | M→S | `accel_stream` | Stream ~16 ms |
|
||||
| `ESPNOW_SET_ACCEL_STREAM` | M→S | `accel_stream` | Stream ~10 ms (best-effort) |
|
||||
| `ESPNOW_ACCEL_SAMPLE` | S→M | `accel_sample` | x/y/z Roh-LSB |
|
||||
| `ESPNOW_SET_TAP_NOTIFY` | M→S | `tap_notify` | Tap-Arten filtern |
|
||||
| `ESPNOW_TAP_EVENT` | S→M | `tap_event` | kind 1/2/3 |
|
||||
@@ -199,7 +199,7 @@ UART_CMD_REQ(&uart_msg, alox_UartMessage_accel_deadzone_request_tag, accel_deadz
|
||||
| Heartbeat | 1000 ms |
|
||||
| Client-Timeout (Master) | 3 × Heartbeat = 3 s → `available=false` |
|
||||
| Master-Verlust (Slave) | 5 s ohne Discover |
|
||||
| Accel-Stream | 16 ms |
|
||||
| Accel-Stream | 10 ms (best-effort, kein ACK-Wait; = FreeRTOS-Tick) |
|
||||
| Batterie-Report | 30 s (+ einmal 150 ms nach Join) |
|
||||
| Echo-Ping Timeout (Master) | 500 ms (`ESPNOW_ECHO_PING_TIMEOUT_MS`) |
|
||||
|
||||
@@ -260,9 +260,9 @@ Nur auf dem **Master** registriert (`powerpod.c`). IDs aus `MessageType` in `uar
|
||||
|
||||
### 7.4 ACCEL_STREAM (25)
|
||||
|
||||
- Optional `period_ms` (8–16): BLE-Host-Push-Intervall für `BLE_STREAM_TICK` (UART ignoriert; Default 16).
|
||||
- Optional `period_ms` (10–16): BLE-Host-Push-Intervall für `BLE_STREAM_TICK` (UART ignoriert; Default 10).
|
||||
- Master setzt `client_registry_set_accel_stream` und sendet `ESPNOW_SET_ACCEL_STREAM`.
|
||||
- Slave-Task `slave_accel_stream_task` sendet `ESPNOW_ACCEL_SAMPLE` alle 16 ms.
|
||||
- Slave-Task `slave_accel_stream_task` sendet `ESPNOW_ACCEL_SAMPLE` alle 10 ms (best-effort / `send_best_effort`).
|
||||
|
||||
### 7.5 TAP_NOTIFY (27)
|
||||
|
||||
@@ -371,6 +371,7 @@ Implementierung: `ota_uart.c`, Steuerung `cmd/cmd_ota.c`. Gleiches Protokoll üb
|
||||
- Inaktive Partition: `esp_ota_get_next_update_partition()`.
|
||||
- LED-Ring zeigt Fortschritt (blau Schreiben, grün Verteilung).
|
||||
- BLE: ATT MTU ≥ ~219 (preferred 247); `BLE_STREAM_TICK` pausiert während `ota_session_busy()`.
|
||||
- **App-Rollback** (`CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE`): neues Image bootet als `PENDING_VERIFY`. `ota_confirm` markiert nach ~8 s stabilem Lauf `mark_app_valid`; Crash/Reset davor → Bootloader rollt auf die vorherige App zurück. Während `PENDING_VERIFY` ist ein neues `OTA_START` blockiert.
|
||||
|
||||
### 8.2 ESP-NOW-OTA (Master → Slaves)
|
||||
|
||||
@@ -378,10 +379,10 @@ Implementierung: `ota_uart.c`, Steuerung `cmd/cmd_ota.c`. Gleiches Protokoll üb
|
||||
|
||||
1. `ESPNOW_OTA_START` + `total_size` → Slave `ota_espnow_slave_on_start`
|
||||
2. `ESPNOW_OTA_PAYLOAD` bis 200 B → 4 KiB Buffer auf Slave
|
||||
3. `ESPNOW_OTA_END` → Boot-Partition setzen
|
||||
3. `ESPNOW_OTA_END` → Boot-Partition setzen + automatischer Reboot
|
||||
4. Slave antwortet `ESPNOW_OTA_STATUS` (mit `send_message_ex` + Semaphore auf Master)
|
||||
|
||||
Nach Erfolg: **alle Knoten neu starten**.
|
||||
Nach Erfolg: Master und Slaves starten automatisch neu (Rollback-Confirm siehe 8.1).
|
||||
|
||||
### 8.3 OTA_SLAVE_PROGRESS (21)
|
||||
|
||||
|
||||
+2
-2
@@ -27,7 +27,7 @@ go run . -port /dev/ttyUSB0 clients
|
||||
| `deadzone` | `0x06` | Get/set accelerometer deadzone LSB (`-set`, `-value`, `-client`, `-all`) |
|
||||
| `tap-config` | `0x20` | Get/set BMA456 tap detection params (`-set`, `-client`, `-all`, `-sens`, `-max-gest`, `-shock`, `-quiet`, `-wait`, `-axis`, `-single`, `-double`, `-triple`) |
|
||||
| `tap-notify` | `0x1b` | Get/set which tap kinds (single/double/triple) notify via ESP-NOW (`-set`, `-client`, `-all`, `-single`, `-double`, `-triple`) |
|
||||
| `cache-status` | `0x1d` | Subscribed accel + tap cache (`CACHE_STATUS`); one UART round-trip for 16 ms polling |
|
||||
| `cache-status` | `0x1d` | Subscribed accel + tap cache (`CACHE_STATUS`); one UART round-trip for ~8 ms polling |
|
||||
| `unicast-test` | `0x07` | Sends ESP-NOW unicast test to one slave (`-client`, `-seq`) |
|
||||
| `echo-ping` | `0x1e` | ESP-NOW echo round-trip to one slave (`-client`); prints `rtt_ms` (host UART chain) and `esp_rtt_us` (master ESP-NOW, raw µs) |
|
||||
| `test` | — | Run an automated scenario (JSON configs under `testdata/`) |
|
||||
@@ -79,7 +79,7 @@ Open [http://localhost:8080](http://localhost:8080) — Host-Verbindung panel (U
|
||||
| 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** |
|
||||
| Tap (An/Aus) | Host **receive**: poll master tap cache (~8 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`).
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ func serveBleStreamTest(w http.ResponseWriter, r *http.Request, link *managedHos
|
||||
return
|
||||
}
|
||||
count := 50
|
||||
periodMs := uint32(16)
|
||||
periodMs := uint32(10)
|
||||
tolMs := 2.0
|
||||
timeoutS := 15.0
|
||||
var clientID uint32
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defaultAccelStreamInterval = 16 * time.Millisecond
|
||||
defaultAccelStreamInterval = 10 * time.Millisecond
|
||||
defaultPreFetchMs = 2
|
||||
minAPIStreamInterval = 1 * time.Millisecond
|
||||
maxAPIStreamInterval = 10 * time.Second
|
||||
|
||||
@@ -79,8 +79,8 @@ func measureBleStream(h hostConn, clientID, periodMs uint32, count int, tolMs, t
|
||||
PeriodMs: periodMs,
|
||||
TolMs: tolMs,
|
||||
}
|
||||
if periodMs < 8 || periodMs > 16 {
|
||||
out.Error = "period_ms must be 8..16"
|
||||
if periodMs < 10 || periodMs > 16 {
|
||||
out.Error = "period_ms must be 10..16"
|
||||
return out
|
||||
}
|
||||
if count < 2 {
|
||||
|
||||
@@ -13,7 +13,7 @@ func runAccelStreamCmd(h hostConn, args []string) error {
|
||||
all := fs.Bool("all", false, "all registered clients")
|
||||
enable := fs.Bool("enable", true, "enable stream")
|
||||
disable := fs.Bool("disable", false, "disable stream")
|
||||
period := fs.Uint("period", 0, "BLE push period_ms 8–16 (0=leave/default)")
|
||||
period := fs.Uint("period", 0, "BLE push period_ms 10–16 (0=leave/default)")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
func runBleStream(h hostConn, args []string) error {
|
||||
fs := flag.NewFlagSet("ble-stream", flag.ExitOnError)
|
||||
clientID := fs.Uint("client", 0, "slave client id (0 = all_clients)")
|
||||
periodMs := fs.Uint("period", 16, "BLE push period_ms (8–16)")
|
||||
periodMs := fs.Uint("period", 10, "BLE push period_ms (10–16)")
|
||||
count := fs.Int("count", 50, "number of BleStreamTick samples")
|
||||
tolMs := fs.Float64("tol", 2.0, "tolerance ms around period for window_ok")
|
||||
duration := fs.Float64("timeout", 15.0, "max seconds waiting for ticks")
|
||||
|
||||
+2
-2
@@ -75,7 +75,7 @@ type DashboardState struct {
|
||||
UARTConnected bool `json:"uart_connected"`
|
||||
SerialOK bool `json:"serial_ok"`
|
||||
SerialError string `json:"serial_error,omitempty"`
|
||||
/** Host: fast CACHE_STATUS poll (~16 ms) for accel + tap. */
|
||||
/** Host: fast CACHE_STATUS poll (~8 ms) for accel + tap. */
|
||||
LiveStream bool `json:"live_stream"`
|
||||
/** BLE live path: inter-tick stats from BleStreamTick.esp_timestamp_us. */
|
||||
BleTick *BleTickView `json:"ble_tick,omitempty"`
|
||||
@@ -489,7 +489,7 @@ func (h *wsHub) snapshotClients() []ClientView {
|
||||
return out
|
||||
}
|
||||
|
||||
// patchLiveStream toggles host CACHE_STATUS polling (~16 ms).
|
||||
// patchLiveStream toggles host CACHE_STATUS polling (~8 ms).
|
||||
func (h *wsHub) patchLiveStream(enabled bool) {
|
||||
h.mu.Lock()
|
||||
h.liveStream = enabled
|
||||
|
||||
@@ -148,13 +148,13 @@ POST /api/ble-rtt
|
||||
→ {"success":true,"count":50,"warmup":5,"stats":{"n":50,"min_ms":…,"median_ms":…,"mean_ms":…,"max_ms":…}}
|
||||
|
||||
POST /api/ble-stream
|
||||
{"count":50,"period_ms":16,"tol_ms":2,"timeout_s":15,"client_id":0}
|
||||
{"count":50,"period_ms":8,"tol_ms":2,"timeout_s":15,"client_id":0}
|
||||
→ {"success":true,"received":50,"requested":50,"window_ok":true,"dt_esp_ms":{…},"dr_pc_ms":{…},…}
|
||||
```
|
||||
|
||||
Pauses dashboard/API polling while running. Requires active BLE host (`DIP8=1`).
|
||||
|
||||
### Live stream (host `CACHE_STATUS` poll ~16 ms; BLE uses `BLE_STREAM_TICK`)
|
||||
### Live stream (host `CACHE_STATUS` poll ~8 ms; BLE uses `BLE_STREAM_TICK`)
|
||||
|
||||
```http
|
||||
GET /api/live-stream
|
||||
|
||||
@@ -21,7 +21,7 @@ const (
|
||||
otaDistQueryTimeout = 2 * time.Second
|
||||
otaDistEmitMinInterval = 150 * time.Millisecond
|
||||
// Pace host chunks so the master UART RX ring is not overrun (~20 frames/block).
|
||||
otaHostChunkPaceUART = 3 * time.Millisecond
|
||||
otaHostChunkPaceUART = 5 * time.Millisecond
|
||||
// Slightly slower for BLE WriteWithoutResponse flow control.
|
||||
otaHostChunkPaceBLE = 6 * time.Millisecond
|
||||
otaBlockMaxRetries = 3
|
||||
@@ -389,7 +389,7 @@ func runOTAUnlocked(t otaTransport, firmware []byte, onProgress otaProgressFn) e
|
||||
mergeSlaveProgressResponse(prog, slaveBytes, slaveDetails)
|
||||
}
|
||||
notify("done", "", 100,
|
||||
fmt.Sprintf("Fertig — %d bytes, Boot-Slot %d. Master und Slaves neu starten.",
|
||||
fmt.Sprintf("Fertig — %d bytes, Boot-Slot %d. Geräte starten neu auf die neue Firmware.",
|
||||
st.GetBytesWritten(), st.GetTargetSlot()),
|
||||
OTAProgress{
|
||||
Bytes: st.GetBytesWritten(), Slot: st.GetTargetSlot(),
|
||||
|
||||
@@ -39,6 +39,7 @@ idf_component_register(
|
||||
"ota_uart.c"
|
||||
"ota_espnow.c"
|
||||
"ota_session.c"
|
||||
"ota_confirm.c"
|
||||
"client_registry.c"
|
||||
"esp_now_comm.c"
|
||||
"esp_now_core.c"
|
||||
|
||||
+4
-4
@@ -124,7 +124,7 @@ Schema: `proto/esp_now_messages.proto`. Encode/decode: `esp_now_proto.c`. The ES
|
||||
| `ESPNOW_UNICAST_TEST` | Master → slave | `EspNowUnicastTest` (`seq`) |
|
||||
| `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_ACCEL_SAMPLE` | Slave → master | `EspNowAccelSample` (`slave_id`, `x`, `y`, `z` raw LSB) — ~every 8 ms, best-effort |
|
||||
| `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` |
|
||||
@@ -376,7 +376,7 @@ go run . -port /dev/ttyUSB0 tap-notify -client 16
|
||||
|
||||
### CACHE_STATUS command
|
||||
|
||||
Read **cached** accel and/or tap data on the **master** in one UART round-trip. Slaves send `ESPNOW_ACCEL_SAMPLE` every **16 ms** when streaming; tap events arrive via `ESPNOW_TAP_EVENT` and are held up to **16 ms** (`CLIENT_REGISTRY_TAP_MAX_AGE_MS`). Pending taps are **consumed** on read (like the former `TAP_SNAPSHOT`).
|
||||
Read **cached** accel and/or tap data on the **master** in one UART round-trip. Slaves send `ESPNOW_ACCEL_SAMPLE` every **8 ms** (best-effort) when streaming; tap events arrive via `ESPNOW_TAP_EVENT` and are held up to **8 ms** (`CLIENT_REGISTRY_TAP_MAX_AGE_MS`). Pending taps are **consumed** on read (like the former `TAP_SNAPSHOT`).
|
||||
|
||||
**Request:** framed `1d` (`0x1d`) only — no body (`CacheStatusRequest` empty).
|
||||
|
||||
@@ -390,7 +390,7 @@ Read **cached** accel and/or tap data on the **master** in one UART round-trip.
|
||||
|
||||
Unsubscribed submessages are omitted on the wire (proto3 defaults). The master walks `client_registry` once per request (`cmd/cmd_cache_status.c`).
|
||||
|
||||
Host tools poll this at **16 ms** when live-stream / WebSocket receive is enabled. Tap events stay visible for **2 s** in the UI/API after first sight.
|
||||
Host tools poll this at **~8 ms** when live-stream / WebSocket receive is enabled. Tap events stay visible for **2 s** in the UI/API after first sight.
|
||||
|
||||
```bash
|
||||
go run . -port /dev/ttyUSB0 cache-status
|
||||
@@ -534,7 +534,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) |
|
||||
| `client_registry_set_tap_notify()` / `client_registry_take_tap()` | Tap notify flags + short-lived tap cache (8 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).
|
||||
|
||||
|
||||
+8
-3
@@ -15,9 +15,10 @@
|
||||
|
||||
static const char *TAG = "[BLE_STREAM]";
|
||||
|
||||
#define BLE_STREAM_PERIOD_MIN_MS 8u
|
||||
/* Min matches FreeRTOS tick (HZ=100 → 10 ms); 8 ms would delay 0 ticks. */
|
||||
#define BLE_STREAM_PERIOD_MIN_MS 10u
|
||||
#define BLE_STREAM_PERIOD_MAX_MS 16u
|
||||
#define BLE_STREAM_PERIOD_DEFAULT_MS 16u
|
||||
#define BLE_STREAM_PERIOD_DEFAULT_MS 10u
|
||||
|
||||
static uint32_t s_period_ms = BLE_STREAM_PERIOD_DEFAULT_MS;
|
||||
static TaskHandle_t s_task;
|
||||
@@ -164,7 +165,11 @@ static void ble_stream_task(void *param) {
|
||||
}
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(period));
|
||||
TickType_t delay_ticks = pdMS_TO_TICKS(period);
|
||||
if (delay_ticks < 1) {
|
||||
delay_ticks = 1;
|
||||
}
|
||||
vTaskDelay(delay_ticks);
|
||||
}
|
||||
|
||||
s_task = NULL;
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/** Clamp and store BLE push period (8–16 ms). */
|
||||
/** Clamp and store BLE push period (10–16 ms; min = FreeRTOS tick). */
|
||||
void ble_stream_set_period_ms(uint32_t period_ms);
|
||||
|
||||
uint32_t ble_stream_period_ms(void);
|
||||
|
||||
@@ -53,7 +53,7 @@ typedef struct {
|
||||
|
||||
#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
|
||||
#define CLIENT_REGISTRY_TAP_MAX_AGE_MS 10u
|
||||
|
||||
void client_registry_init(void);
|
||||
|
||||
|
||||
@@ -111,8 +111,13 @@ void vCmdDispatcherTask(void *param) {
|
||||
bool handled = false;
|
||||
for (int i = 0; i < handler_count; i++) {
|
||||
if (handlers[i].msg_id == msg.msg_id) {
|
||||
ESP_LOGI(TAG, "trigger command %s (0x%02x)", message_type_name(msg.msg_id),
|
||||
(unsigned)msg.msg_id);
|
||||
if (msg.msg_id == (uint16_t)alox_MessageType_OTA_PAYLOAD) {
|
||||
ESP_LOGD(TAG, "trigger command %s (0x%02x)",
|
||||
message_type_name(msg.msg_id), (unsigned)msg.msg_id);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "trigger command %s (0x%02x)",
|
||||
message_type_name(msg.msg_id), (unsigned)msg.msg_id);
|
||||
}
|
||||
handlers[i].callback(msg.payload, msg.len);
|
||||
handled = true;
|
||||
break;
|
||||
|
||||
+7
-1
@@ -2,6 +2,7 @@
|
||||
#include "led_ring.h"
|
||||
#include "ota_espnow.h"
|
||||
#include "ota_uart.h"
|
||||
#include "pod_reboot.h"
|
||||
#include "uart_cmd.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
@@ -179,8 +180,10 @@ static void handle_ota_payload(const uint8_t *data, size_t len) {
|
||||
|
||||
ota_feed_result_t r = ota_uart_feed_chunk(req_ptr->seq, req_ptr->data.bytes,
|
||||
req_ptr->data.size);
|
||||
/* Gap: keep session; host block-retries from the last ACK boundary. */
|
||||
if (r == OTA_FEED_SEQ_GAP) {
|
||||
send_ota_failed(16);
|
||||
ESP_LOGW(TAG, "OTA_PAYLOAD seq gap (got %lu) — waiting for host retry",
|
||||
(unsigned long)req_ptr->seq);
|
||||
return;
|
||||
}
|
||||
if (r == OTA_FEED_SEQ_DUP) {
|
||||
@@ -257,6 +260,8 @@ static void ota_distribute_task(void *param) {
|
||||
|
||||
led_ring_ota_success();
|
||||
free(job);
|
||||
/* Host already got SUCCESS; reboot onto the staged image. */
|
||||
pod_schedule_restart();
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
@@ -332,6 +337,7 @@ static void ota_start_espnow_task(void *param) {
|
||||
response.payload.ota_status.error = 0;
|
||||
uart_cmd_send(&response, TAG);
|
||||
led_ring_ota_success();
|
||||
pod_schedule_restart();
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
|
||||
+22
-7
@@ -204,10 +204,11 @@ esp_err_t esp_now_core_send_reliable(const uint8_t *dest_mac,
|
||||
ESPNOW_SEND_RETRY_DELAY_MS, true);
|
||||
}
|
||||
|
||||
esp_err_t esp_now_core_send_fast(const uint8_t *dest_mac,
|
||||
const alox_EspNowMessage *msg) {
|
||||
static esp_err_t send_fast_internal(const uint8_t *dest_mac,
|
||||
const alox_EspNowMessage *msg,
|
||||
bool quiet) {
|
||||
if (s_send_lock == NULL ||
|
||||
xSemaphoreTake(s_send_lock, pdMS_TO_TICKS(5000)) != pdTRUE) {
|
||||
xSemaphoreTake(s_send_lock, pdMS_TO_TICKS(5)) != pdTRUE) {
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
@@ -216,14 +217,18 @@ esp_err_t esp_now_core_send_fast(const uint8_t *dest_mac,
|
||||
|
||||
esp_err_t err = esp_now_proto_encode(msg, buf, sizeof(buf), &len);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "encode failed");
|
||||
if (!quiet) {
|
||||
ESP_LOGW(TAG, "encode failed");
|
||||
}
|
||||
xSemaphoreGive(s_send_lock);
|
||||
return err;
|
||||
}
|
||||
|
||||
if (len > ESP_NOW_MAX_DATA_LEN) {
|
||||
ESP_LOGW(TAG, "encoded len %u > ESP-NOW max %u", (unsigned)len,
|
||||
(unsigned)ESP_NOW_MAX_DATA_LEN);
|
||||
if (!quiet) {
|
||||
ESP_LOGW(TAG, "encoded len %u > ESP-NOW max %u", (unsigned)len,
|
||||
(unsigned)ESP_NOW_MAX_DATA_LEN);
|
||||
}
|
||||
xSemaphoreGive(s_send_lock);
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
@@ -234,7 +239,7 @@ esp_err_t esp_now_core_send_fast(const uint8_t *dest_mac,
|
||||
}
|
||||
|
||||
err = esp_now_send(dest_mac, buf, len);
|
||||
if (err != ESP_OK) {
|
||||
if (err != ESP_OK && !quiet) {
|
||||
ESP_LOGW(TAG, "send type=%u failed: %s", (unsigned)msg->type,
|
||||
esp_err_to_name(err));
|
||||
}
|
||||
@@ -242,6 +247,16 @@ esp_err_t esp_now_core_send_fast(const uint8_t *dest_mac,
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t esp_now_core_send_fast(const uint8_t *dest_mac,
|
||||
const alox_EspNowMessage *msg) {
|
||||
return send_fast_internal(dest_mac, msg, false);
|
||||
}
|
||||
|
||||
esp_err_t esp_now_core_send_best_effort(const uint8_t *dest_mac,
|
||||
const alox_EspNowMessage *msg) {
|
||||
return send_fast_internal(dest_mac, msg, true);
|
||||
}
|
||||
|
||||
esp_err_t esp_now_core_send(const uint8_t *dest_mac,
|
||||
const alox_EspNowMessage *msg) {
|
||||
return esp_now_core_send_wait(dest_mac, msg);
|
||||
|
||||
@@ -26,6 +26,12 @@ esp_err_t esp_now_core_send(const uint8_t *dest_mac,
|
||||
/** Like send but does not wait for esp_now send-done (lower latency). */
|
||||
esp_err_t esp_now_core_send_fast(const uint8_t *dest_mac,
|
||||
const alox_EspNowMessage *msg);
|
||||
/**
|
||||
* Fire-and-forget without failure logs — for high-rate gaming input
|
||||
* (accel/tap) where drops are expected under load.
|
||||
*/
|
||||
esp_err_t esp_now_core_send_best_effort(const uint8_t *dest_mac,
|
||||
const alox_EspNowMessage *msg);
|
||||
esp_err_t esp_now_core_send_wait(const uint8_t *dest_mac,
|
||||
const alox_EspNowMessage *msg);
|
||||
/** OTA payloads: wait for send-done with extra NO_MEM backoff (queue drain). */
|
||||
|
||||
+15
-5
@@ -23,7 +23,12 @@
|
||||
#define SLAVE_MASTER_LOST_MS (ESPNOW_HEARTBEAT_INTERVAL_MS * 5)
|
||||
/** While master or slave OTA is in progress (discover may be sparse). */
|
||||
#define SLAVE_MASTER_OTA_GRACE_MS 300000u
|
||||
#define ESPNOW_ACCEL_INTERVAL_MS 16
|
||||
/**
|
||||
* Best-effort gaming input cadence (loss OK; send_best_effort).
|
||||
* Must be >= one FreeRTOS tick (CONFIG_FREERTOS_HZ=100 → 10 ms); shorter
|
||||
* values become 0 ticks and assert in vTaskDelayUntil.
|
||||
*/
|
||||
#define ESPNOW_ACCEL_INTERVAL_MS 10
|
||||
#define ESPNOW_BATTERY_INTERVAL_MS 30000
|
||||
#define SLAVE_BATTERY_AFTER_JOIN_MS 150
|
||||
|
||||
@@ -165,7 +170,8 @@ static esp_err_t send_accel_sample(const uint8_t *dest_mac, uint32_t slave_id,
|
||||
msg.payload.accel_sample.x = x;
|
||||
msg.payload.accel_sample.y = y;
|
||||
msg.payload.accel_sample.z = z;
|
||||
return esp_now_core_send(dest_mac, &msg);
|
||||
/* Fire-and-forget: controller input prefers low latency over delivery. */
|
||||
return esp_now_core_send_best_effort(dest_mac, &msg);
|
||||
}
|
||||
|
||||
static esp_err_t send_tap_event(const uint8_t *dest_mac, uint32_t slave_id,
|
||||
@@ -175,7 +181,7 @@ static esp_err_t send_tap_event(const uint8_t *dest_mac, uint32_t slave_id,
|
||||
msg.which_payload = alox_EspNowMessage_tap_event_tag;
|
||||
msg.payload.tap_event.slave_id = slave_id;
|
||||
msg.payload.tap_event.kind = kind;
|
||||
return esp_now_core_send(dest_mac, &msg);
|
||||
return esp_now_core_send_best_effort(dest_mac, &msg);
|
||||
}
|
||||
|
||||
static esp_err_t send_battery_report(const uint8_t *dest_mac,
|
||||
@@ -579,11 +585,15 @@ static void check_master_timeout(void) {
|
||||
static void accel_stream_task(void *param) {
|
||||
(void)param;
|
||||
|
||||
ESP_LOGI(TAG, "accel stream task (interval %u ms)",
|
||||
ESP_LOGI(TAG, "accel stream task (interval %u ms, best-effort)",
|
||||
(unsigned)ESPNOW_ACCEL_INTERVAL_MS);
|
||||
|
||||
TickType_t last_wake = xTaskGetTickCount();
|
||||
const TickType_t period_ticks = pdMS_TO_TICKS(ESPNOW_ACCEL_INTERVAL_MS);
|
||||
/* pdMS_TO_TICKS can be 0 when ms < tick period — DelayUntil asserts. */
|
||||
const TickType_t delay_ticks = period_ticks > 0 ? period_ticks : 1;
|
||||
while (1) {
|
||||
vTaskDelay(pdMS_TO_TICKS(ESPNOW_ACCEL_INTERVAL_MS));
|
||||
vTaskDelayUntil(&last_wake, delay_ticks);
|
||||
if (!s_joined || s_assigned_id == 0 || !s_accel_stream_enabled ||
|
||||
!bma456_is_ready()) {
|
||||
continue;
|
||||
|
||||
+11
-3
@@ -2,6 +2,7 @@
|
||||
#include "cmd_handler.h"
|
||||
#include "esp_log.h"
|
||||
#include "uart.h"
|
||||
#include "uart_messages.pb.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -87,9 +88,16 @@ void host_rx_bytes(const uint8_t *data, size_t len) {
|
||||
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
if (parse_uart_byte(data[i], &s_rx_packet)) {
|
||||
ESP_LOGI(TAG, "received message cmd=0x%02x len=%u",
|
||||
s_rx_packet.len > 0 ? s_rx_packet.payload[0] : 0,
|
||||
(unsigned)s_rx_packet.len);
|
||||
uint8_t cmd =
|
||||
s_rx_packet.len > 0 ? s_rx_packet.payload[0] : 0;
|
||||
/* OTA payloads are high-rate; avoid INFO spam starving the dispatcher. */
|
||||
if (cmd == (uint8_t)alox_MessageType_OTA_PAYLOAD) {
|
||||
ESP_LOGD(TAG, "received message cmd=0x%02x len=%u", cmd,
|
||||
(unsigned)s_rx_packet.len);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "received message cmd=0x%02x len=%u", cmd,
|
||||
(unsigned)s_rx_packet.len);
|
||||
}
|
||||
host_enqueue_packet(s_rx_packet.payload, s_rx_packet.len);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
#include "ota_confirm.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_ota_ops.h"
|
||||
#include "esp_partition.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
static const char *TAG = "[OTA_CONFIRM]";
|
||||
|
||||
/** Long enough for early crash loops (e.g. assert in a worker task) to reboot
|
||||
* before we cancel rollback; short enough that a follow-up OTA is not blocked
|
||||
* for long (esp_ota_begin fails while PENDING_VERIFY). */
|
||||
#define OTA_CONFIRM_DELAY_MS 8000u
|
||||
|
||||
static void ota_confirm_task(void *param) {
|
||||
(void)param;
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(OTA_CONFIRM_DELAY_MS));
|
||||
|
||||
const esp_partition_t *running = esp_ota_get_running_partition();
|
||||
if (running == NULL) {
|
||||
ESP_LOGW(TAG, "no running partition");
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
esp_ota_img_states_t state = ESP_OTA_IMG_UNDEFINED;
|
||||
esp_err_t err = esp_ota_get_state_partition(running, &state);
|
||||
if (err != ESP_OK) {
|
||||
/* otadata missing / non-OTA slot — nothing to confirm. */
|
||||
ESP_LOGI(TAG, "ota state unavailable (%s) — skip confirm",
|
||||
esp_err_to_name(err));
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state != ESP_OTA_IMG_PENDING_VERIFY) {
|
||||
ESP_LOGI(TAG, "running %s state=%d — no pending verify", running->label,
|
||||
(int)state);
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ota_mark_app_valid_cancel_rollback();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "mark_app_valid failed: %s", esp_err_to_name(err));
|
||||
} else {
|
||||
ESP_LOGI(TAG, "app confirmed valid (%s) — rollback cancelled",
|
||||
running->label);
|
||||
}
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
void ota_confirm_schedule(void) {
|
||||
if (xTaskCreate(ota_confirm_task, "ota_confirm", 3072, NULL, 3, NULL) !=
|
||||
pdPASS) {
|
||||
ESP_LOGE(TAG, "failed to create confirm task — attempting immediate mark");
|
||||
(void)esp_ota_mark_app_valid_cancel_rollback();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#ifndef OTA_CONFIRM_H
|
||||
#define OTA_CONFIRM_H
|
||||
|
||||
/**
|
||||
* After OTA with BOOTLOADER_APP_ROLLBACK_ENABLE, the new image boots as
|
||||
* ESP_OTA_IMG_PENDING_VERIFY. If we crash/reset before confirming, the
|
||||
* bootloader rolls back to the previous app on the next boot.
|
||||
*
|
||||
* Call once from app_main after init: schedules a delayed confirm so early
|
||||
* panics (task asserts, etc.) still trigger rollback.
|
||||
*/
|
||||
void ota_confirm_schedule(void);
|
||||
|
||||
#endif
|
||||
+18
-7
@@ -10,6 +10,7 @@
|
||||
#include "freertos/event_groups.h"
|
||||
#include "freertos/idf_additions.h"
|
||||
#include "ota_uart.h"
|
||||
#include "pod_reboot.h"
|
||||
#include <string.h>
|
||||
|
||||
static const char *TAG = "[OTA_ESPNOW]";
|
||||
@@ -186,6 +187,10 @@ static bool any_target_failed(void) {
|
||||
/**
|
||||
* Wait until all want_bits are set, or OTA_EG_FAIL_BIT (slave FAILED).
|
||||
* Returns true only when every target bit is set; false on fail or timeout.
|
||||
*
|
||||
* Wait only for *still missing* bits. Waiting on the full mask with "any bit"
|
||||
* busy-spins after the first slave ACK (already-set bits make WaitBits return
|
||||
* immediately) and starves IDLE → task_wdt.
|
||||
*/
|
||||
static bool wait_target_bits(uint32_t want_bits, uint32_t timeout_ms) {
|
||||
if (s_eg == NULL || want_bits == 0) {
|
||||
@@ -194,7 +199,6 @@ static bool wait_target_bits(uint32_t want_bits, uint32_t timeout_ms) {
|
||||
|
||||
const TickType_t start = xTaskGetTickCount();
|
||||
const TickType_t timeout_ticks = pdMS_TO_TICKS(timeout_ms);
|
||||
const EventBits_t wait_mask = want_bits | OTA_EG_FAIL_BIT;
|
||||
|
||||
while (true) {
|
||||
TickType_t elapsed = xTaskGetTickCount() - start;
|
||||
@@ -203,16 +207,19 @@ static bool wait_target_bits(uint32_t want_bits, uint32_t timeout_ms) {
|
||||
}
|
||||
TickType_t remain = timeout_ticks - elapsed;
|
||||
|
||||
EventBits_t got =
|
||||
xEventGroupWaitBits(s_eg, wait_mask, pdFALSE, pdFALSE, remain);
|
||||
if (got & OTA_EG_FAIL_BIT) {
|
||||
EventBits_t have = xEventGroupGetBits(s_eg);
|
||||
if (have & OTA_EG_FAIL_BIT) {
|
||||
xEventGroupClearBits(s_eg, OTA_EG_FAIL_BIT);
|
||||
return false;
|
||||
}
|
||||
if ((got & want_bits) == want_bits) {
|
||||
if ((have & want_bits) == want_bits) {
|
||||
xEventGroupClearBits(s_eg, want_bits);
|
||||
return true;
|
||||
}
|
||||
|
||||
EventBits_t missing = want_bits & ~have;
|
||||
(void)xEventGroupWaitBits(s_eg, missing | OTA_EG_FAIL_BIT, pdFALSE,
|
||||
pdFALSE, remain);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,9 +321,10 @@ static void process_slave_payload(const uint8_t master_mac[6],
|
||||
|
||||
ota_feed_result_t r = ota_uart_feed_chunk(payload->seq, payload->data.bytes,
|
||||
payload->data.size);
|
||||
/* Gap: keep session; master block-retries from the last ACK boundary. */
|
||||
if (r == OTA_FEED_SEQ_GAP) {
|
||||
led_ring_ota_failed();
|
||||
send_slave_status(master_mac, OTA_ST_FAILED, ota_uart_bytes_written(), 16);
|
||||
ESP_LOGW(TAG, "ESP-NOW OTA seq gap (got %lu) — waiting for retry",
|
||||
(unsigned long)payload->seq);
|
||||
return;
|
||||
}
|
||||
if (r == OTA_FEED_SEQ_DUP) {
|
||||
@@ -371,6 +379,9 @@ static void process_slave_end(const uint8_t master_mac[6]) {
|
||||
led_ring_ota_success();
|
||||
ESP_LOGI(TAG, "slave OTA success (%lu bytes), reboot to run",
|
||||
(unsigned long)written);
|
||||
/* Boot partition already set in ota_uart_finish(true); must reboot or we
|
||||
* keep running the old image while the master expects the new one. */
|
||||
pod_schedule_restart();
|
||||
}
|
||||
|
||||
static void ota_slave_work_task(void *param) {
|
||||
|
||||
+7
-1
@@ -97,7 +97,13 @@ int ota_uart_prepare(uint32_t total_size) {
|
||||
esp_ota_handle_t handle;
|
||||
esp_err_t err = esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &handle);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_ota_begin failed: %s", esp_err_to_name(err));
|
||||
if (err == ESP_ERR_OTA_ROLLBACK_INVALID_STATE) {
|
||||
ESP_LOGE(TAG,
|
||||
"esp_ota_begin blocked: running app still PENDING_VERIFY "
|
||||
"(wait for ota_confirm ~8 s after boot, or let rollback finish)");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "esp_ota_begin failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -45,7 +45,9 @@ void ota_uart_abort(void);
|
||||
|
||||
/**
|
||||
* Append up to 200 bytes with strict seq checking (0, 1, 2, …).
|
||||
* Duplicates (seq < expected) return OTA_FEED_SEQ_DUP; gaps return OTA_FEED_SEQ_GAP.
|
||||
* Duplicates (seq < expected) return OTA_FEED_SEQ_DUP.
|
||||
* Gaps (seq > expected) return OTA_FEED_SEQ_GAP without consuming — caller
|
||||
* should keep the session so the host can retry the current flash block.
|
||||
*/
|
||||
ota_feed_result_t ota_uart_feed_chunk(uint32_t seq, const uint8_t *data, size_t len);
|
||||
|
||||
|
||||
+1
-5
@@ -355,10 +355,6 @@ esp_err_t pod_settings_load_id_map(pod_id_map_t *out) {
|
||||
(unsigned)len);
|
||||
return ESP_OK;
|
||||
}
|
||||
if (blob.count > POD_ID_MAP_MAX) {
|
||||
ESP_LOGW(TAG, "id_map count %u invalid", (unsigned)blob.count);
|
||||
return ESP_OK;
|
||||
}
|
||||
if (blob.next_id < POD_CLIENT_ID_MIN) {
|
||||
blob.next_id = (uint8_t)POD_CLIENT_ID_MIN;
|
||||
}
|
||||
@@ -375,7 +371,7 @@ esp_err_t pod_settings_load_id_map(pod_id_map_t *out) {
|
||||
}
|
||||
|
||||
esp_err_t pod_settings_save_id_map(const pod_id_map_t *map) {
|
||||
if (map == NULL || map->count > POD_ID_MAP_MAX) {
|
||||
if (map == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (map->next_id < POD_CLIENT_ID_MIN) {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "cmd_version.h"
|
||||
#include "cmd_ota.h"
|
||||
#include "cmd_ota_slave_progress.h"
|
||||
#include "ota_confirm.h"
|
||||
#include "cmd_led_ring.h"
|
||||
#include "cmd_battery.h"
|
||||
#include "cmd_set_log_level.h"
|
||||
@@ -236,6 +237,10 @@ void app_main(void) {
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "LED ring: UART LED_RING commands only (no local demo loop)");
|
||||
|
||||
/* After OTA: PENDING_VERIFY until confirm; crash before this → rollback. */
|
||||
ota_confirm_schedule();
|
||||
|
||||
while (1) {
|
||||
vTaskDelay(portMAX_DELAY);
|
||||
}
|
||||
|
||||
@@ -99,13 +99,13 @@ typedef struct _alox_EspNowAccelDeadzone {
|
||||
uint32_t client_id; /* 0 = all slaves; otherwise only matching slave_id applies */
|
||||
} alox_EspNowAccelDeadzone;
|
||||
|
||||
/* * Master → slave: enable/disable periodic accel ESP-NOW stream (~16 ms). */
|
||||
/* * Master → slave: enable/disable periodic accel ESP-NOW stream (~10 ms). */
|
||||
typedef struct _alox_EspNowAccelStream {
|
||||
bool enable;
|
||||
uint32_t client_id;
|
||||
} alox_EspNowAccelStream;
|
||||
|
||||
/* * Slave → master: latest BMA456 sample (sent ~every 16 ms). */
|
||||
/* * Slave → master: latest BMA456 sample (best-effort, ~every 10 ms). */
|
||||
typedef struct _alox_EspNowAccelSample {
|
||||
uint32_t slave_id;
|
||||
int32_t x;
|
||||
|
||||
@@ -90,13 +90,13 @@ message EspNowAccelDeadzone {
|
||||
uint32 client_id = 2; // 0 = all slaves; otherwise only matching slave_id applies
|
||||
}
|
||||
|
||||
/** Master → slave: enable/disable periodic accel ESP-NOW stream (~16 ms). */
|
||||
/** Master → slave: enable/disable periodic accel ESP-NOW stream (~10 ms). */
|
||||
message EspNowAccelStream {
|
||||
bool enable = 1;
|
||||
uint32 client_id = 2;
|
||||
}
|
||||
|
||||
/** Slave → master: latest BMA456 sample (sent ~every 16 ms). */
|
||||
/** Slave → master: latest BMA456 sample (best-effort, ~every 10 ms). */
|
||||
message EspNowAccelSample {
|
||||
uint32 slave_id = 1;
|
||||
sint32 x = 2;
|
||||
|
||||
@@ -31,7 +31,7 @@ typedef enum _alox_MessageType {
|
||||
alox_MessageType_ACCEL_STREAM = 25,
|
||||
alox_MessageType_BATTERY_STATUS = 26,
|
||||
alox_MessageType_TAP_NOTIFY = 27,
|
||||
/* * Combined cached accel + tap poll (one UART round-trip, ~16 ms cadence). */
|
||||
/* * Combined cached accel + tap poll (one UART round-trip, ~10 ms cadence). */
|
||||
alox_MessageType_CACHE_STATUS = 29,
|
||||
/* * Host → master → slave → master: timestamp echo round-trip (latency test). */
|
||||
alox_MessageType_ESPNOW_ECHO_PING = 30,
|
||||
@@ -118,9 +118,9 @@ typedef struct _alox_AccelDeadzoneResponse {
|
||||
uint32_t slaves_updated;
|
||||
} alox_AccelDeadzoneResponse;
|
||||
|
||||
/* Host → master: enable/disable slave accel ESP-NOW stream (~16 ms per slave).
|
||||
/* Host → master: enable/disable slave accel ESP-NOW stream (~10 ms per slave, best-effort).
|
||||
write=false: read; write=true: apply. client_id 0 invalid for write (use >0 or all_clients).
|
||||
period_ms (8–16): BLE host push interval for BleStreamTick; ignored on UART (default 16). */
|
||||
period_ms (10–16): BLE host push interval for BleStreamTick; ignored on UART (default 10). */
|
||||
typedef struct _alox_AccelStreamRequest {
|
||||
bool write;
|
||||
bool enable;
|
||||
|
||||
@@ -27,7 +27,7 @@ enum MessageType {
|
||||
BATTERY_STATUS = 26;
|
||||
TAP_NOTIFY = 27;
|
||||
reserved 28;
|
||||
/** Combined cached accel + tap poll (one UART round-trip, ~16 ms cadence). */
|
||||
/** Combined cached accel + tap poll (one UART round-trip, ~10 ms cadence). */
|
||||
CACHE_STATUS = 29;
|
||||
/** Host → master → slave → master: timestamp echo round-trip (latency test). */
|
||||
ESPNOW_ECHO_PING = 30;
|
||||
@@ -149,9 +149,9 @@ message AccelDeadzoneResponse {
|
||||
uint32 slaves_updated = 4;
|
||||
}
|
||||
|
||||
// Host → master: enable/disable slave accel ESP-NOW stream (~16 ms per slave).
|
||||
// Host → master: enable/disable slave accel ESP-NOW stream (~10 ms per slave, best-effort).
|
||||
// write=false: read; write=true: apply. client_id 0 invalid for write (use >0 or all_clients).
|
||||
// period_ms (8–16): BLE host push interval for BleStreamTick; ignored on UART (default 16).
|
||||
// period_ms (10–16): BLE host push interval for BleStreamTick; ignored on UART (default 10).
|
||||
message AccelStreamRequest {
|
||||
bool write = 1;
|
||||
bool enable = 2;
|
||||
|
||||
@@ -421,7 +421,8 @@ CONFIG_BOOTLOADER_PROJECT_VER=1
|
||||
#
|
||||
# Application Rollback
|
||||
#
|
||||
# CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE is not set
|
||||
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y
|
||||
# CONFIG_BOOTLOADER_APP_ANTI_ROLLBACK is not set
|
||||
# end of Application Rollback
|
||||
|
||||
#
|
||||
@@ -2529,7 +2530,8 @@ CONFIG_WIFI_PROV_STA_ALL_CHANNEL_SCAN=y
|
||||
# Deprecated options for backward compatibility
|
||||
# CONFIG_APP_BUILD_TYPE_ELF_RAM is not set
|
||||
# CONFIG_NO_BLOBS is not set
|
||||
# CONFIG_APP_ROLLBACK_ENABLE is not set
|
||||
CONFIG_APP_ROLLBACK_ENABLE=y
|
||||
# CONFIG_APP_ANTI_ROLLBACK is not set
|
||||
# CONFIG_LOG_BOOTLOADER_LEVEL_NONE is not set
|
||||
# CONFIG_LOG_BOOTLOADER_LEVEL_ERROR is not set
|
||||
# CONFIG_LOG_BOOTLOADER_LEVEL_WARN is not set
|
||||
|
||||
@@ -6,3 +6,6 @@ CONFIG_ESP_COEX_SW_COEXIST_ENABLE=y
|
||||
# Pin stacks to different cores (WiFi stays on core 0)
|
||||
CONFIG_BT_NIMBLE_PINNED_TO_CORE_1=y
|
||||
CONFIG_ESP_WIFI_TASK_PINNED_TO_CORE_0=y
|
||||
|
||||
# OTA A/B: new image boots PENDING_VERIFY; crash/reset before confirm → rollback
|
||||
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y
|
||||
|
||||
Reference in New Issue
Block a user