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:
+4
-1
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user