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:
2026-11-01 22:16:28 +01:00
co-authored by Cursor
parent ae8ef40820
commit 178ab46cec
35 changed files with 236 additions and 79 deletions
+6 -5
View File
@@ -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 |
+8 -7
View File
@@ -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
View File
@@ -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`).
+1 -1
View File
@@ -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
+1 -1
View File
@@ -16,7 +16,7 @@ import (
)
const (
defaultAccelStreamInterval = 16 * time.Millisecond
defaultAccelStreamInterval = 10 * time.Millisecond
defaultPreFetchMs = 2
minAPIStreamInterval = 1 * time.Millisecond
maxAPIStreamInterval = 10 * time.Second
+2 -2
View File
@@ -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 {
+1 -1
View File
@@ -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
}
+1 -1
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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(),
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+1 -1
View File
@@ -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);
+7 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+6
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
}
+60
View File
@@ -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();
}
}
+14
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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) {
+5
View File
@@ -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);
}
+2 -2
View File
@@ -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;
+2 -2
View File
@@ -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;
+3 -3
View File
@@ -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;
+3 -3
View File
@@ -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;
+4 -2
View File
@@ -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
+3
View File
@@ -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