Enable host OTA over BLE and harden ESP-NOW slave distribution.

Allow the same staged firmware upload path on BLE as UART, fix distribute stack/event-group crashes, and recover sticky slave OTA sessions instead of failing with err=4.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-01 20:42:14 +01:00
co-authored by Cursor
parent 567dab6d0a
commit ae8ef40820
16 changed files with 400 additions and 208 deletions
+4 -1
View File
@@ -6,6 +6,7 @@
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "ota_session.h"
#include "pb_encode.h"
#include "uart.h"
#include "uart_cmd.h"
@@ -146,7 +147,9 @@ static void ble_stream_task(void *param) {
period = BLE_STREAM_PERIOD_MAX_MS;
}
if (ble_host_ready() && any_accel_stream()) {
/* Pause ticks during host OTA / ESP-NOW distribute so ATT bandwidth
* stays available for OTA_STATUS notifies. */
if (ble_host_ready() && any_accel_stream() && !ota_session_busy()) {
alox_UartMessage msg = alox_UartMessage_init_zero;
uart_cmd_init_response(&msg, alox_MessageType_BLE_STREAM_TICK,
alox_UartMessage_ble_stream_tick_tag);
+2 -19
View File
@@ -1,5 +1,4 @@
#include "cmd_ota.h"
#include "app_config.h"
#include "led_ring.h"
#include "ota_espnow.h"
#include "ota_uart.h"
@@ -14,6 +13,8 @@ static const char *TAG = "[OTA_CMD]";
#define OTA_PREPARE_STACK 8192
#define OTA_PREPARE_PRIO 5
/* Block buffer is static .bss; stack is for nested EspNowMessage + encode buf +
* esp_now_send path. 4 KiB overflowed; 8 KiB matches the pre-crash budget. */
#define OTA_DIST_STACK 8192
#define OTA_DIST_PRIO 5
@@ -104,24 +105,10 @@ static void ota_prepare_task(void *param) {
vTaskDelete(NULL);
}
static bool ota_host_is_uart(void) {
const app_config_t *cfg = app_config_get();
if (cfg != NULL && cfg->host_ble) {
ESP_LOGW(TAG, "OTA requires UART host (DIP8=0); BLE host active");
return false;
}
return true;
}
static void handle_ota_start(const uint8_t *data, size_t len) {
alox_UartMessage uart_msg;
alox_OtaStartPayload req = alox_OtaStartPayload_init_zero;
if (!ota_host_is_uart()) {
send_ota_failed(20);
return;
}
if (uart_cmd_decode(data, len, &uart_msg) != ESP_OK) {
send_ota_failed( 2);
return;
@@ -349,10 +336,6 @@ static void ota_start_espnow_task(void *param) {
}
static void handle_ota_start_espnow(const uint8_t *data, size_t len) {
if (!ota_host_is_uart()) {
send_ota_failed(20);
return;
}
(void)data;
(void)len;
+105 -14
View File
@@ -28,6 +28,12 @@ static const char *TAG = "[OTA_ESPNOW]";
#define OTA_ST_SUCCESS 4u
#define OTA_ST_FAILED 5u
/**
* Event-group fail bit. FreeRTOS reserves the top 8 bits (assert if used);
* targets use bits 0..CLIENT_REGISTRY_MAX-1 (max 16).
*/
#define OTA_EG_FAIL_BIT (1u << 16)
/** ESP-NOW OTA receive on slave (blue progress bar). */
#define OTA_LED_ESPNOW_RX_R 0
#define OTA_LED_ESPNOW_RX_G 0
@@ -58,6 +64,7 @@ typedef struct {
static EventGroupHandle_t s_eg;
static QueueHandle_t s_slave_work_queue;
static bool s_distribution_active;
static TaskHandle_t s_slave_prep_task;
typedef struct {
uint8_t count;
@@ -70,6 +77,9 @@ typedef struct {
static ota_dist_t s_dist;
/** Always-reserved flash block for ESP-NOW distribute (not on task stack). */
static uint8_t s_ota_block_buf[OTA_UART_FLASH_BLOCK_SIZE];
typedef struct {
uint32_t client_id;
uint32_t bytes_written;
@@ -164,14 +174,46 @@ static uint32_t all_target_bits(void) {
return (1u << s_dist.count) - 1u;
}
static bool any_target_failed(void) {
for (uint8_t i = 0; i < s_prog.count; i++) {
if (s_prog.entries[i].status == OTA_ST_FAILED) {
return true;
}
}
return false;
}
/**
* 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.
*/
static bool wait_target_bits(uint32_t want_bits, uint32_t timeout_ms) {
if (s_eg == NULL || want_bits == 0) {
return false;
}
EventBits_t got =
xEventGroupWaitBits(s_eg, want_bits, pdTRUE, pdTRUE,
pdMS_TO_TICKS(timeout_ms));
return (got & want_bits) == want_bits;
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;
if (elapsed >= timeout_ticks) {
return false;
}
TickType_t remain = timeout_ticks - elapsed;
EventBits_t got =
xEventGroupWaitBits(s_eg, wait_mask, pdFALSE, pdFALSE, remain);
if (got & OTA_EG_FAIL_BIT) {
xEventGroupClearBits(s_eg, OTA_EG_FAIL_BIT);
return false;
}
if ((got & want_bits) == want_bits) {
xEventGroupClearBits(s_eg, want_bits);
return true;
}
}
}
static uint32_t block_ack_timeout_ms(void) {
@@ -372,11 +414,21 @@ void ota_espnow_slave_init(void) {
}
}
static void drain_slave_work_queue(void) {
if (s_slave_work_queue == NULL) {
return;
}
ota_slave_work_t junk;
while (xQueueReceive(s_slave_work_queue, &junk, 0) == pdTRUE) {
}
}
static void ota_slave_prepare_task(void *param) {
uint32_t total_size = (uint32_t)(uintptr_t)param;
uint8_t master_mac[6];
if (!esp_now_comm_get_master_mac(master_mac)) {
s_slave_prep_task = NULL;
vTaskDelete(NULL);
return;
}
@@ -386,6 +438,7 @@ static void ota_slave_prepare_task(void *param) {
int slot = ota_uart_prepare(total_size);
if (slot < 0) {
send_slave_status(master_mac, OTA_ST_FAILED, 0, 1);
s_slave_prep_task = NULL;
vTaskDelete(NULL);
return;
}
@@ -393,6 +446,7 @@ static void ota_slave_prepare_task(void *param) {
send_slave_status(master_mac, OTA_ST_READY, 0, 0);
led_ring_show_ota_progress(0, total_size, OTA_LED_ESPNOW_RX_R, OTA_LED_ESPNOW_RX_G,
OTA_LED_ESPNOW_RX_B);
s_slave_prep_task = NULL;
vTaskDelete(NULL);
}
@@ -404,14 +458,31 @@ void ota_espnow_slave_on_start(const uint8_t master_mac[6],
ESP_LOGI(TAG, "ESP-NOW OTA_START (%lu bytes)", (unsigned long)start->total_size);
if (ota_uart_is_active()) {
queue_slave_status(master_mac, OTA_ST_FAILED, 0, 4);
/* Duplicate START while prepare task still running — ignore. */
if (s_slave_prep_task != NULL) {
ESP_LOGW(TAG, "OTA prepare already in flight — ignore START");
return;
}
if (ota_uart_is_active()) {
/* Same image, nothing written yet: sticky READY after master timeout /
* duplicate START — re-ACK instead of failing with err=4. */
if (ota_uart_total_size() == start->total_size &&
ota_uart_bytes_received() == 0) {
ESP_LOGI(TAG, "OTA already prepared for %lu bytes — re-send READY",
(unsigned long)start->total_size);
queue_slave_status(master_mac, OTA_ST_READY, 0, 0);
return;
}
ESP_LOGW(TAG, "aborting sticky OTA session for new START");
ota_uart_abort();
drain_slave_work_queue();
}
if (xTaskCreate(ota_slave_prepare_task, "ota_esp_prep", OTA_ESPNOW_PREPARE_STACK,
(void *)(uintptr_t)start->total_size, OTA_ESPNOW_PREPARE_PRIO,
NULL) != pdPASS) {
&s_slave_prep_task) != pdPASS) {
s_slave_prep_task = NULL;
queue_slave_status(master_mac, OTA_ST_FAILED, 0, 5);
}
}
@@ -479,6 +550,8 @@ void ota_espnow_master_on_status(const uint8_t slave_mac[6],
status->error);
ESP_LOGW(TAG, "slave %lu OTA failed (err=%lu)",
(unsigned long)s_dist.id[idx], (unsigned long)status->error);
/* Wake wait_target_bits so distribute aborts instead of waiting for READY. */
xEventGroupSetBits(s_eg, OTA_EG_FAIL_BIT);
break;
default:
break;
@@ -527,7 +600,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
uint32_t target_mask = all_target_bits();
esp_err_t err;
xEventGroupClearBits(s_eg, target_mask);
xEventGroupClearBits(s_eg, target_mask | OTA_EG_FAIL_BIT);
for (uint8_t i = 0; i < s_dist.count; i++) {
err = esp_now_comm_send_ota_start(s_dist.mac[i], size);
if (err != ESP_OK) {
@@ -540,6 +613,12 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
}
if (!wait_target_bits(target_mask, OTA_PREPARE_TIMEOUT_MS)) {
if (any_target_failed()) {
ESP_LOGE(TAG, "slave OTA failed during prepare");
prog_end();
s_distribution_active = false;
return ESP_FAIL;
}
ESP_LOGE(TAG, "timeout waiting for slave OTA ready");
prog_end();
s_distribution_active = false;
@@ -551,7 +630,6 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
s_dist.progress.aggregate(0, size, s_dist.count);
}
uint8_t block_buf[OTA_UART_FLASH_BLOCK_SIZE];
uint32_t offset = 0;
uint32_t seq = 0;
@@ -561,7 +639,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
block_len = OTA_UART_FLASH_BLOCK_SIZE;
}
err = esp_partition_read(partition, offset, block_buf, block_len);
err = esp_partition_read(partition, offset, s_ota_block_buf, block_len);
if (err != ESP_OK) {
ESP_LOGE(TAG, "partition read @%lu failed: %s", (unsigned long)offset,
esp_err_to_name(err));
@@ -591,7 +669,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
(unsigned long)send_attempt,
(unsigned long)OTA_BLOCK_MAX_RETRIES);
}
err = send_block_payloads(block_buf, block_len, &seq);
err = send_block_payloads(s_ota_block_buf, block_len, &seq);
if (err == ESP_OK) {
block_sent = true;
break;
@@ -614,6 +692,13 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
acked = true;
break;
}
if (any_target_failed()) {
ESP_LOGE(TAG, "slave OTA failed during block @%lu",
(unsigned long)s_dist.expected_bytes);
prog_end();
s_distribution_active = false;
return ESP_FAIL;
}
log_missing_block_acks(s_dist.expected_bytes);
if (attempt >= OTA_BLOCK_MAX_RETRIES) {
break;
@@ -622,9 +707,9 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
(unsigned long)s_dist.expected_bytes,
(unsigned long)(attempt + 1),
(unsigned long)OTA_BLOCK_MAX_RETRIES);
xEventGroupClearBits(s_eg, target_mask);
xEventGroupClearBits(s_eg, target_mask | OTA_EG_FAIL_BIT);
seq = block_start_seq;
err = send_block_payloads(block_buf, block_len, &seq);
err = send_block_payloads(s_ota_block_buf, block_len, &seq);
if (err != ESP_OK) {
prog_end();
s_distribution_active = false;
@@ -653,7 +738,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
}
}
xEventGroupClearBits(s_eg, target_mask);
xEventGroupClearBits(s_eg, target_mask | OTA_EG_FAIL_BIT);
for (uint8_t i = 0; i < s_dist.count; i++) {
err = esp_now_comm_send_ota_end(s_dist.mac[i]);
if (err != ESP_OK) {
@@ -664,6 +749,12 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
}
if (!wait_target_bits(target_mask, OTA_END_TIMEOUT_MS)) {
if (any_target_failed()) {
ESP_LOGE(TAG, "slave OTA failed during end");
prog_end();
s_distribution_active = false;
return ESP_FAIL;
}
ESP_LOGE(TAG, "timeout waiting for slave OTA success");
prog_end();
s_distribution_active = false;
+1 -1
View File
@@ -407,7 +407,7 @@ message TapConfigResponse {
bool enable_triple = 12;
}
// Host → device: begin UART OTA (erase inactive OTA slot; device replies OTA_STATUS).
// Host → device: begin host OTA over UART or BLE (erase inactive OTA slot; device replies OTA_STATUS).
message OtaStartPayload {
uint32 total_size = 1;
}