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
+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) {