Add per-slave ESP-NOW OTA progress over UART and fix dashboard updates.

Expose OTA_SLAVE_PROGRESS on the master, track per-slave state during
distribution, run ESP-NOW OTA in a background task so the host can poll
while slaves update, and show master/slave progress in the dashboard
with table layout and faster WebSocket refresh during uploads.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-19 21:07:46 +02:00
co-authored by Cursor
parent 5a948a5c8c
commit a0f4a81a55
23 changed files with 1467 additions and 146 deletions
+124 -3
View File
@@ -36,10 +36,90 @@ typedef struct {
uint8_t mac[OTA_MAX_TARGETS][6];
uint32_t id[OTA_MAX_TARGETS];
uint32_t expected_bytes;
uint32_t total_bytes;
ota_espnow_progress_cbs_t progress;
} ota_dist_t;
static ota_dist_t s_dist;
typedef struct {
uint32_t client_id;
uint32_t bytes_written;
uint32_t status;
uint32_t error;
} ota_prog_entry_t;
static struct {
bool active;
uint32_t total_bytes;
uint32_t aggregate_bytes;
uint8_t count;
ota_prog_entry_t entries[OTA_MAX_TARGETS];
} s_prog;
static void prog_begin(uint32_t total_bytes) {
s_prog.active = true;
s_prog.total_bytes = total_bytes;
s_prog.aggregate_bytes = 0;
s_prog.count = s_dist.count;
for (uint8_t i = 0; i < s_dist.count; i++) {
s_prog.entries[i].client_id = s_dist.id[i];
s_prog.entries[i].bytes_written = 0;
s_prog.entries[i].status = OTA_ST_PREPARING;
s_prog.entries[i].error = 0;
}
}
static void prog_end(void) { s_prog.active = false; }
static void prog_set_aggregate(uint32_t bytes_done) {
s_prog.aggregate_bytes = bytes_done;
}
static void prog_update_idx(int idx, uint32_t status, uint32_t bytes,
uint32_t error) {
if (idx < 0 || idx >= (int)s_prog.count) {
return;
}
ota_prog_entry_t *e = &s_prog.entries[idx];
e->status = status;
if (bytes > e->bytes_written) {
e->bytes_written = bytes;
}
if (error != 0) {
e->error = error;
}
}
void ota_espnow_progress_query(uint32_t filter_client_id,
alox_OtaSlaveProgressResponse *out) {
if (out == NULL) {
return;
}
*out = (alox_OtaSlaveProgressResponse)alox_OtaSlaveProgressResponse_init_zero;
out->active = s_prog.active;
out->total_bytes = s_prog.total_bytes;
out->aggregate_bytes = s_prog.aggregate_bytes;
out->slave_count = s_prog.count;
for (uint8_t i = 0; i < s_prog.count; i++) {
const ota_prog_entry_t *e = &s_prog.entries[i];
if (filter_client_id != 0 && e->client_id != filter_client_id) {
continue;
}
if (out->slaves_count >=
sizeof(out->slaves) / sizeof(out->slaves[0])) {
break;
}
alox_OtaSlaveProgressEntry *dst = &out->slaves[out->slaves_count++];
dst->client_id = e->client_id;
dst->bytes_written = e->bytes_written;
dst->total_bytes = s_prog.total_bytes;
dst->status = e->status;
dst->error = e->error;
}
}
static int find_target_index(const uint8_t mac[6]) {
for (uint8_t i = 0; i < s_dist.count; i++) {
if (memcmp(s_dist.mac[i], mac, 6) == 0) {
@@ -179,9 +259,15 @@ void ota_espnow_master_on_status(const uint8_t slave_mac[6],
switch (status->status) {
case OTA_ST_READY:
prog_update_idx(idx, OTA_ST_READY, 0, 0);
xEventGroupSetBits(s_eg, bit);
break;
case OTA_ST_BLOCK_ACK:
prog_update_idx(idx, OTA_ST_BLOCK_ACK, status->bytes_written, 0);
if (s_dist.progress.per_slave != NULL) {
s_dist.progress.per_slave(s_dist.id[idx], status->bytes_written,
s_dist.total_bytes);
}
if (status->bytes_written >= s_dist.expected_bytes) {
xEventGroupSetBits(s_eg, bit);
} else {
@@ -191,9 +277,12 @@ void ota_espnow_master_on_status(const uint8_t slave_mac[6],
}
break;
case OTA_ST_SUCCESS:
prog_update_idx(idx, OTA_ST_SUCCESS, status->bytes_written, 0);
xEventGroupSetBits(s_eg, bit);
break;
case OTA_ST_FAILED:
prog_update_idx(idx, OTA_ST_FAILED, status->bytes_written,
status->error);
ESP_LOGW(TAG, "slave %lu OTA failed (err=%lu)",
(unsigned long)s_dist.id[idx], (unsigned long)status->error);
break;
@@ -220,7 +309,8 @@ static size_t collect_targets(void) {
}
static esp_err_t distribute_image(const esp_partition_t *partition,
uint32_t size) {
uint32_t size,
const ota_espnow_progress_cbs_t *progress) {
if (s_eg == NULL) {
s_eg = xEventGroupCreate();
if (s_eg == NULL) {
@@ -228,6 +318,13 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
}
}
memset(&s_dist.progress, 0, sizeof(s_dist.progress));
if (progress != NULL) {
s_dist.progress = *progress;
}
s_dist.total_bytes = size;
prog_begin(size);
ESP_LOGI(TAG, "distributing %lu bytes from %s to %u slave(s)",
(unsigned long)size, partition->label, (unsigned)s_dist.count);
@@ -240,15 +337,22 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
if (err != ESP_OK) {
ESP_LOGW(TAG, "OTA_START to slave %lu failed",
(unsigned long)s_dist.id[i]);
prog_end();
return err;
}
}
if (!wait_target_bits(target_mask, OTA_PREPARE_TIMEOUT_MS)) {
ESP_LOGE(TAG, "timeout waiting for slave OTA ready");
prog_end();
return ESP_ERR_TIMEOUT;
}
prog_set_aggregate(0);
if (s_dist.progress.aggregate != NULL) {
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;
@@ -263,6 +367,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
if (err != ESP_OK) {
ESP_LOGE(TAG, "partition read @%lu failed: %s", (unsigned long)offset,
esp_err_to_name(err));
prog_end();
return err;
}
@@ -277,6 +382,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
err = esp_now_comm_send_ota_payload(s_dist.mac[i], seq,
block_buf + sent, chunk);
if (err != ESP_OK) {
prog_end();
return err;
}
}
@@ -293,6 +399,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
if (!wait_target_bits(target_mask, OTA_BLOCK_TIMEOUT_MS)) {
ESP_LOGE(TAG, "timeout block ack @%lu bytes",
(unsigned long)s_dist.expected_bytes);
prog_end();
return ESP_ERR_TIMEOUT;
}
ESP_LOGI(TAG, "block ack @%lu/%lu (%lu%%)",
@@ -303,34 +410,48 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
(unsigned long)block_len);
}
offset += block_len;
prog_set_aggregate(offset);
if (s_dist.progress.aggregate != NULL) {
s_dist.progress.aggregate(offset, size, s_dist.count);
}
}
xEventGroupClearBits(s_eg, target_mask);
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) {
prog_end();
return err;
}
}
if (!wait_target_bits(target_mask, OTA_END_TIMEOUT_MS)) {
ESP_LOGE(TAG, "timeout waiting for slave OTA success");
prog_end();
return ESP_ERR_TIMEOUT;
}
prog_set_aggregate(size);
prog_end();
ESP_LOGI(TAG, "ESP-NOW OTA complete for %u slave(s)", (unsigned)s_dist.count);
return ESP_OK;
}
esp_err_t ota_espnow_distribute(const esp_partition_t *partition, uint32_t size) {
esp_err_t ota_espnow_distribute(const esp_partition_t *partition, uint32_t size,
const ota_espnow_progress_cbs_t *progress) {
if (partition == NULL || size == 0) {
return ESP_ERR_INVALID_ARG;
}
if (collect_targets() == 0) {
ESP_LOGI(TAG, "no available slaves — skip ESP-NOW OTA");
memset(&s_prog, 0, sizeof(s_prog));
s_prog.total_bytes = size;
if (progress != NULL && progress->aggregate != NULL) {
progress->aggregate(size, size, 0);
}
return ESP_OK;
}
return distribute_image(partition, size);
return distribute_image(partition, size, progress);
}