Fix web OTA upload and isolate OTA sessions across firmware and goTool.

Split ESP-NOW into core/master/slave modules, block non-OTA UART traffic during updates, and hold the host serial port exclusively so dashboard polling cannot interleave with firmware uploads.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-31 16:35:18 +02:00
co-authored by Cursor
parent 0cbc4d0644
commit 0eea27a876
29 changed files with 1828 additions and 1416 deletions
+160 -41
View File
@@ -35,7 +35,28 @@ static const char *TAG = "[OTA_ESPNOW]";
#define OTA_MAX_TARGETS CLIENT_REGISTRY_MAX
#define OTA_SLAVE_WORK_QUEUE_LEN 12
#define OTA_SLAVE_WORK_STACK 8192
#define OTA_SLAVE_WORK_PRIO 5
typedef enum {
OTA_SLAVE_WORK_STATUS = 1,
OTA_SLAVE_WORK_PAYLOAD,
OTA_SLAVE_WORK_END,
} ota_slave_work_op_t;
typedef struct {
ota_slave_work_op_t op;
uint8_t master_mac[6];
uint32_t status;
uint32_t bytes_written;
uint32_t error;
alox_EspNowOtaPayload payload;
} ota_slave_work_t;
static EventGroupHandle_t s_eg;
static QueueHandle_t s_slave_work_queue;
static bool s_distribution_active;
typedef struct {
uint8_t count;
@@ -152,57 +173,38 @@ static bool wait_target_bits(uint32_t want_bits, uint32_t timeout_ms) {
return (got & want_bits) == want_bits;
}
bool ota_espnow_distribution_active(void) { return s_distribution_active; }
static void send_slave_status(const uint8_t master_mac[6], uint32_t status,
uint32_t bytes_written, uint32_t error) {
esp_now_comm_send_ota_status(master_mac, status, bytes_written, error);
}
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)) {
vTaskDelete(NULL);
return;
static bool queue_slave_work(const ota_slave_work_t *work) {
if (work == NULL || s_slave_work_queue == NULL) {
return false;
}
send_slave_status(master_mac, OTA_ST_PREPARING, 0, 0);
int slot = ota_uart_prepare(total_size);
if (slot < 0) {
send_slave_status(master_mac, OTA_ST_FAILED, 0, 1);
vTaskDelete(NULL);
return;
if (xQueueSend(s_slave_work_queue, work, 0) != pdTRUE) {
ESP_LOGW(TAG, "slave OTA work queue full (op=%d)", (int)work->op);
return false;
}
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);
vTaskDelete(NULL);
return true;
}
void ota_espnow_slave_on_start(const uint8_t master_mac[6],
const alox_EspNowOtaStart *start) {
if (start == NULL || start->total_size == 0) {
return;
}
ESP_LOGI(TAG, "ESP-NOW OTA_START (%lu bytes)", (unsigned long)start->total_size);
if (ota_uart_is_active()) {
send_slave_status(master_mac, OTA_ST_FAILED, 0, 4);
return;
}
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) {
send_slave_status(master_mac, OTA_ST_FAILED, 0, 5);
}
static void queue_slave_status(const uint8_t master_mac[6], uint32_t status,
uint32_t bytes_written, uint32_t error) {
ota_slave_work_t work = {
.op = OTA_SLAVE_WORK_STATUS,
.status = status,
.bytes_written = bytes_written,
.error = error,
};
memcpy(work.master_mac, master_mac, 6);
(void)queue_slave_work(&work);
}
void ota_espnow_slave_on_payload(const uint8_t master_mac[6],
const alox_EspNowOtaPayload *payload) {
static void process_slave_payload(const uint8_t master_mac[6],
const alox_EspNowOtaPayload *payload) {
if (payload == NULL || payload->data.size == 0) {
send_slave_status(master_mac, OTA_ST_FAILED, 0, 11);
return;
@@ -246,7 +248,7 @@ void ota_espnow_slave_on_payload(const uint8_t master_mac[6],
}
}
void ota_espnow_slave_on_end(const uint8_t master_mac[6]) {
static void process_slave_end(const uint8_t master_mac[6]) {
ESP_LOGI(TAG, "ESP-NOW OTA_END");
if (!ota_uart_is_active()) {
send_slave_status(master_mac, OTA_ST_FAILED, 0, 20);
@@ -268,6 +270,113 @@ void ota_espnow_slave_on_end(const uint8_t master_mac[6]) {
(unsigned long)written);
}
static void ota_slave_work_task(void *param) {
(void)param;
ota_slave_work_t work;
while (1) {
if (xQueueReceive(s_slave_work_queue, &work, portMAX_DELAY) != pdTRUE) {
continue;
}
switch (work.op) {
case OTA_SLAVE_WORK_STATUS:
send_slave_status(work.master_mac, work.status, work.bytes_written,
work.error);
break;
case OTA_SLAVE_WORK_PAYLOAD:
process_slave_payload(work.master_mac, &work.payload);
break;
case OTA_SLAVE_WORK_END:
process_slave_end(work.master_mac);
break;
default:
break;
}
}
}
void ota_espnow_slave_init(void) {
if (s_slave_work_queue != NULL) {
return;
}
s_slave_work_queue = xQueueCreate(OTA_SLAVE_WORK_QUEUE_LEN, sizeof(ota_slave_work_t));
if (s_slave_work_queue == NULL) {
ESP_LOGE(TAG, "failed to create slave OTA work queue");
return;
}
if (xTaskCreate(ota_slave_work_task, "ota_slave_wrk", OTA_SLAVE_WORK_STACK, NULL,
OTA_SLAVE_WORK_PRIO, NULL) != pdPASS) {
ESP_LOGE(TAG, "failed to create slave OTA work task");
}
}
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)) {
vTaskDelete(NULL);
return;
}
send_slave_status(master_mac, OTA_ST_PREPARING, 0, 0);
int slot = ota_uart_prepare(total_size);
if (slot < 0) {
send_slave_status(master_mac, OTA_ST_FAILED, 0, 1);
vTaskDelete(NULL);
return;
}
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);
vTaskDelete(NULL);
}
void ota_espnow_slave_on_start(const uint8_t master_mac[6],
const alox_EspNowOtaStart *start) {
if (start == NULL || start->total_size == 0) {
return;
}
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);
return;
}
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) {
queue_slave_status(master_mac, OTA_ST_FAILED, 0, 5);
}
}
void ota_espnow_slave_on_payload(const uint8_t master_mac[6],
const alox_EspNowOtaPayload *payload) {
if (payload == NULL) {
queue_slave_status(master_mac, OTA_ST_FAILED, 0, 11);
return;
}
ota_slave_work_t work = {.op = OTA_SLAVE_WORK_PAYLOAD, .payload = *payload};
memcpy(work.master_mac, master_mac, 6);
if (!queue_slave_work(&work)) {
queue_slave_status(master_mac, OTA_ST_FAILED, 0, 14);
}
}
void ota_espnow_slave_on_end(const uint8_t master_mac[6]) {
ota_slave_work_t work = {.op = OTA_SLAVE_WORK_END};
memcpy(work.master_mac, master_mac, 6);
if (!queue_slave_work(&work)) {
queue_slave_status(master_mac, OTA_ST_FAILED, 0, 15);
}
}
void ota_espnow_master_on_status(const uint8_t slave_mac[6],
const alox_EspNowOtaStatus *status) {
if (status == NULL || s_eg == NULL) {
@@ -342,6 +451,8 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
}
}
s_distribution_active = true;
memset(&s_dist.progress, 0, sizeof(s_dist.progress));
if (progress != NULL) {
s_dist.progress = *progress;
@@ -362,6 +473,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
ESP_LOGW(TAG, "OTA_START to slave %lu failed",
(unsigned long)s_dist.id[i]);
prog_end();
s_distribution_active = false;
return err;
}
}
@@ -369,6 +481,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
if (!wait_target_bits(target_mask, OTA_PREPARE_TIMEOUT_MS)) {
ESP_LOGE(TAG, "timeout waiting for slave OTA ready");
prog_end();
s_distribution_active = false;
return ESP_ERR_TIMEOUT;
}
@@ -392,6 +505,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
ESP_LOGE(TAG, "partition read @%lu failed: %s", (unsigned long)offset,
esp_err_to_name(err));
prog_end();
s_distribution_active = false;
return err;
}
@@ -407,6 +521,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
block_buf + sent, chunk);
if (err != ESP_OK) {
prog_end();
s_distribution_active = false;
return err;
}
}
@@ -424,6 +539,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
ESP_LOGE(TAG, "timeout block ack @%lu bytes",
(unsigned long)s_dist.expected_bytes);
prog_end();
s_distribution_active = false;
return ESP_ERR_TIMEOUT;
}
ESP_LOGI(TAG, "block ack @%lu/%lu (%lu%%)",
@@ -445,6 +561,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
err = esp_now_comm_send_ota_end(s_dist.mac[i]);
if (err != ESP_OK) {
prog_end();
s_distribution_active = false;
return err;
}
}
@@ -452,11 +569,13 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
if (!wait_target_bits(target_mask, OTA_END_TIMEOUT_MS)) {
ESP_LOGE(TAG, "timeout waiting for slave OTA success");
prog_end();
s_distribution_active = false;
return ESP_ERR_TIMEOUT;
}
prog_set_aggregate(size);
prog_end();
s_distribution_active = false;
ESP_LOGI(TAG, "ESP-NOW OTA complete for %u slave(s)", (unsigned)s_dist.count);
return ESP_OK;
}