diff --git a/Makefile b/Makefile index b93eab8..22668e1 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,8 @@ proto_generate_uart: -I ../../libs/nanopb/generator/proto uart_messages.proto proto_generate_espnow: - python libs/nanopb/generator/nanopb_generator.py main/proto/esp_now_messages.proto + cd main/proto && python ../../libs/nanopb/generator/nanopb_generator.py \ + -I ../../libs/nanopb/generator/proto esp_now_messages.proto proto_generate: proto_generate_uart proto_generate_espnow diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 96c8c72..6611d13 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -21,6 +21,7 @@ idf_component_register( "cmd_espnow_unicast_test.c" "cmd_ota.c" "ota_uart.c" + "ota_espnow.c" "client_registry.c" "esp_now_comm.c" "esp_now_proto.c" diff --git a/main/README.md b/main/README.md index 406e3dc..30a6b97 100644 --- a/main/README.md +++ b/main/README.md @@ -110,6 +110,12 @@ Schema: `proto/esp_now_messages.proto`. Encode/decode: `esp_now_proto.c`. The ES | `ESPNOW_SLAVE_INFO` | Slave → master | `EspNowSlavePresence` | | `ESPNOW_HEARTBEAT` | Slave → master | `EspNowSlavePresence` (same fields) | | `ESPNOW_SET_ACCEL_DEADZONE` | Master → slave | `EspNowAccelDeadzone` (`deadzone` LSB) | +| `ESPNOW_OTA_START` | Master → slave (unicast) | `EspNowOtaStart` (`total_size`) | +| `ESPNOW_OTA_PAYLOAD` | Master → slave | `EspNowOtaPayload` (`seq`, up to 200 B `data`) | +| `ESPNOW_OTA_END` | Master → slave | `EspNowOtaEnd` | +| `ESPNOW_OTA_STATUS` | Slave → master | `EspNowOtaStatus` (same status codes as UART OTA) | + +After a successful UART OTA on the master, `ota_espnow.c` reads the staged image from flash in **4 KiB** blocks (sent as **200 B** ESP-NOW chunks) and waits for **block_ack** from every available slave before continuing. Slaves use the same `ota_uart` 4 KiB buffer as UART OTA. `EspNowSlavePresence`: `network`, `mac` (6 bytes), `version`, `slave_id`, `available`, `used`. @@ -186,9 +192,9 @@ Host and master speak nanopb-encoded `UartMessage` inside UART frames (byte 0 = | 6 | `ACCEL_DEADZONE` | Implemented (`cmd_accel_deadzone.c`) — get/set accel filter LSB | | 16 | `OTA_START` | Implemented (`cmd_ota.c`) — begin UART OTA on inactive slot | | 17 | `OTA_PAYLOAD` | Implemented — up to 200 B per frame; device buffers 4 KiB | -| 18 | `OTA_END` | Implemented — flush, `esp_ota_end`, set boot partition | +| 18 | `OTA_END` | Implemented — flush, `esp_ota_end`, push image to slaves via ESP-NOW, set boot | | 19 | `OTA_STATUS` | Device → host (prepare/ready/block ACK/success/failed) | -| 20 | `OTA_START_ESPNOW` | Planned | +| 20 | `OTA_START_ESPNOW` | Implemented — re-distribute staged image to slaves only | Regenerate C code: diff --git a/main/cmd_ota.c b/main/cmd_ota.c index 7d50400..2e9e4eb 100644 --- a/main/cmd_ota.c +++ b/main/cmd_ota.c @@ -1,4 +1,5 @@ #include "cmd_ota.h" +#include "ota_espnow.h" #include "ota_uart.h" #include "uart_cmd.h" #include "esp_log.h" @@ -10,7 +11,6 @@ static const char *TAG = "[OTA_CMD]"; #define OTA_PREPARE_STACK 8192 #define OTA_PREPARE_PRIO 5 - static void send_ota_status(ota_uart_status_t status, uint32_t err_code) { alox_UartMessage response; uart_cmd_init_response(&response, alox_MessageType_OTA_STATUS, @@ -128,22 +128,35 @@ static void handle_ota_payload(const uint8_t *data, size_t len) { } } -static void handle_ota_end(const uint8_t *data, size_t len) { - (void)data; - (void)len; - - if (!ota_uart_is_active()) { - send_ota_status(OTA_UART_ST_FAILED, 20); - return; - } - +static esp_err_t finish_master_ota_and_distribute(void) { uint32_t written = ota_uart_bytes_written(); int slot = ota_uart_target_slot(); bool success = false; - esp_err_t err = ota_uart_finish(&success); + esp_err_t err = ota_uart_finish(false, &success); if (err != ESP_OK || !success) { send_ota_status(OTA_UART_ST_FAILED, (uint32_t)err); - return; + return err; + } + + const esp_partition_t *part = NULL; + uint32_t image_size = 0; + if (!ota_uart_get_staged_image(&part, &image_size)) { + send_ota_status(OTA_UART_ST_FAILED, 30); + return ESP_ERR_INVALID_STATE; + } + + err = ota_espnow_distribute(part, image_size); + if (err != ESP_OK) { + ESP_LOGE(TAG, "slave OTA distribution failed: %s", esp_err_to_name(err)); + ota_uart_clear_staged(); + send_ota_status(OTA_UART_ST_FAILED, 31); + return err; + } + + err = ota_uart_apply_boot(); + if (err != ESP_OK) { + send_ota_status(OTA_UART_ST_FAILED, (uint32_t)err); + return err; } alox_UartMessage response; @@ -154,10 +167,61 @@ static void handle_ota_end(const uint8_t *data, size_t len) { response.payload.ota_status.target_slot = slot >= 0 ? (uint32_t)slot : 0; response.payload.ota_status.error = 0; uart_cmd_send(&response, TAG); + return ESP_OK; +} + +static void handle_ota_end(const uint8_t *data, size_t len) { + (void)data; + (void)len; + + if (!ota_uart_is_active()) { + send_ota_status(OTA_UART_ST_FAILED, 20); + return; + } + + (void)finish_master_ota_and_distribute(); +} + +static void handle_ota_start_espnow(const uint8_t *data, size_t len) { + (void)data; + (void)len; + + if (ota_uart_is_active()) { + send_ota_status(OTA_UART_ST_FAILED, 40); + return; + } + + const esp_partition_t *part = NULL; + uint32_t image_size = 0; + if (!ota_uart_get_staged_image(&part, &image_size)) { + send_ota_status(OTA_UART_ST_FAILED, 41); + return; + } + + esp_err_t err = ota_espnow_distribute(part, image_size); + if (err != ESP_OK) { + send_ota_status(OTA_UART_ST_FAILED, 42); + return; + } + + err = ota_uart_apply_boot(); + if (err != ESP_OK) { + send_ota_status(OTA_UART_ST_FAILED, (uint32_t)err); + return; + } + + alox_UartMessage response; + uart_cmd_init_response(&response, alox_MessageType_OTA_STATUS, + alox_UartMessage_ota_status_tag); + response.payload.ota_status.status = (uint32_t)OTA_UART_ST_SUCCESS; + response.payload.ota_status.bytes_written = image_size; + response.payload.ota_status.error = 0; + uart_cmd_send(&response, TAG); } void cmd_ota_register(void) { uart_cmd_register(alox_MessageType_OTA_START, handle_ota_start); uart_cmd_register(alox_MessageType_OTA_PAYLOAD, handle_ota_payload); uart_cmd_register(alox_MessageType_OTA_END, handle_ota_end); + uart_cmd_register(alox_MessageType_OTA_START_ESPNOW, handle_ota_start_espnow); } diff --git a/main/esp_now_comm.c b/main/esp_now_comm.c index 5a242a7..93c9da9 100644 --- a/main/esp_now_comm.c +++ b/main/esp_now_comm.c @@ -1,6 +1,7 @@ #include "bosch456.h" #include "client_registry.h" #include "esp_now_comm.h" +#include "ota_espnow.h" #include "esp_now_proto.h" #include "esp_err.h" #include "esp_event.h" @@ -12,6 +13,7 @@ #include "freertos/FreeRTOS.h" #include "freertos/idf_additions.h" #include "nvs_flash.h" +#include "ota_uart.h" #include #include @@ -38,6 +40,9 @@ static bool s_slave_joined; static uint8_t s_master_mac[ESP_NOW_ETH_ALEN]; static uint32_t s_last_discover_ms; +static SemaphoreHandle_t s_send_done; +static bool s_send_cb_ready; + static uint32_t now_ms(void) { return (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS); } @@ -78,6 +83,9 @@ static esp_err_t ensure_peer(const uint8_t *mac) { static esp_err_t ensure_broadcast_peer(void) { return ensure_peer(ESPNOW_BCAST); } +static esp_err_t send_message_ex(const uint8_t *dest_mac, + const alox_EspNowMessage *msg, bool wait_done); + static void fill_presence(alox_EspNowSlavePresence *presence) { presence->network = s_config.network; presence->version = POWERPOD_FW_VERSION; @@ -87,8 +95,22 @@ static void fill_presence(alox_EspNowSlavePresence *presence) { esp_now_proto_setup_presence_encode(presence, s_own_mac); } +static void espnow_send_done_cb(const esp_now_send_info_t *tx_info, + esp_now_send_status_t status) { + (void)tx_info; + (void)status; + if (s_send_done != NULL) { + xSemaphoreGive(s_send_done); + } +} + static esp_err_t send_message(const uint8_t *dest_mac, const alox_EspNowMessage *msg) { + return send_message_ex(dest_mac, msg, false); +} + +static esp_err_t send_message_ex(const uint8_t *dest_mac, + const alox_EspNowMessage *msg, bool wait_done) { uint8_t buf[ESPNOW_PB_MAX_SIZE]; size_t len = 0; @@ -98,14 +120,31 @@ static esp_err_t send_message(const uint8_t *dest_mac, return err; } + if (len > ESP_NOW_MAX_DATA_LEN) { + ESP_LOGW(TAG, "encoded len %u > ESP-NOW max %u", (unsigned)len, + (unsigned)ESP_NOW_MAX_DATA_LEN); + return ESP_ERR_INVALID_SIZE; + } + if (ensure_peer(dest_mac) != ESP_OK) { return ESP_FAIL; } + if (wait_done && s_send_cb_ready && s_send_done != NULL) { + xSemaphoreTake(s_send_done, 0); + } + err = esp_now_send(dest_mac, buf, len); if (err != ESP_OK) { ESP_LOGW(TAG, "send type=%u failed: %s", (unsigned)msg->type, esp_err_to_name(err)); + return err; + } + + if (wait_done && s_send_cb_ready && s_send_done != NULL) { + if (xSemaphoreTake(s_send_done, pdMS_TO_TICKS(50)) != pdTRUE) { + ESP_LOGW(TAG, "send type=%u done timeout", (unsigned)msg->type); + } } return err; } @@ -132,6 +171,96 @@ static esp_err_t send_unicast_test(const uint8_t *dest_mac, uint32_t seq) { return send_message(dest_mac, &msg); } +static esp_err_t send_ota_start(const uint8_t *dest_mac, uint32_t total_size) { + alox_EspNowMessage msg = alox_EspNowMessage_init_zero; + + msg.type = alox_EspNowMessageType_ESPNOW_OTA_START; + msg.which_payload = alox_EspNowMessage_ota_start_tag; + msg.payload.ota_start.total_size = total_size; + + return send_message_ex(dest_mac, &msg, true); +} + +static esp_err_t send_ota_payload(const uint8_t *dest_mac, uint32_t seq, + const uint8_t *data, size_t len) { + if (data == NULL || len == 0 || len > OTA_UART_HOST_CHUNK_SIZE) { + return ESP_ERR_INVALID_ARG; + } + + alox_EspNowMessage msg = alox_EspNowMessage_init_zero; + + msg.type = alox_EspNowMessageType_ESPNOW_OTA_PAYLOAD; + msg.which_payload = alox_EspNowMessage_ota_payload_tag; + msg.payload.ota_payload.seq = seq; + msg.payload.ota_payload.data.size = len; + memcpy(msg.payload.ota_payload.data.bytes, data, len); + + return send_message_ex(dest_mac, &msg, true); +} + +static esp_err_t send_ota_end(const uint8_t *dest_mac) { + alox_EspNowMessage msg = alox_EspNowMessage_init_zero; + + msg.type = alox_EspNowMessageType_ESPNOW_OTA_END; + msg.which_payload = alox_EspNowMessage_ota_end_tag; + + return send_message_ex(dest_mac, &msg, true); +} + +static esp_err_t send_ota_status(const uint8_t *dest_mac, uint32_t status, + uint32_t bytes_written, uint32_t error) { + alox_EspNowMessage msg = alox_EspNowMessage_init_zero; + + msg.type = alox_EspNowMessageType_ESPNOW_OTA_STATUS; + msg.which_payload = alox_EspNowMessage_ota_status_tag; + msg.payload.ota_status.status = status; + msg.payload.ota_status.bytes_written = bytes_written; + msg.payload.ota_status.error = error; + + return send_message_ex(dest_mac, &msg, true); +} + +esp_err_t esp_now_comm_send_ota_start(const uint8_t mac[CLIENT_MAC_LEN], + uint32_t total_size) { + if (mac == NULL || !s_config.master) { + return ESP_ERR_INVALID_STATE; + } + return send_ota_start(mac, total_size); +} + +esp_err_t esp_now_comm_send_ota_payload(const uint8_t mac[CLIENT_MAC_LEN], + uint32_t seq, const uint8_t *data, + size_t len) { + if (mac == NULL || !s_config.master) { + return ESP_ERR_INVALID_STATE; + } + return send_ota_payload(mac, seq, data, len); +} + +esp_err_t esp_now_comm_send_ota_end(const uint8_t mac[CLIENT_MAC_LEN]) { + if (mac == NULL || !s_config.master) { + return ESP_ERR_INVALID_STATE; + } + return send_ota_end(mac); +} + +esp_err_t esp_now_comm_send_ota_status(const uint8_t master_mac[CLIENT_MAC_LEN], + uint32_t status, uint32_t bytes_written, + uint32_t error) { + if (master_mac == NULL || s_config.master) { + return ESP_ERR_INVALID_STATE; + } + return send_ota_status(master_mac, status, bytes_written, error); +} + +bool esp_now_comm_get_master_mac(uint8_t mac_out[CLIENT_MAC_LEN]) { + if (mac_out == NULL || s_config.master || !s_slave_joined) { + return false; + } + memcpy(mac_out, s_master_mac, CLIENT_MAC_LEN); + return true; +} + esp_err_t esp_now_comm_send_unicast_test(const uint8_t mac[CLIENT_MAC_LEN], uint32_t seq) { if (mac == NULL || !s_config.master) { @@ -363,6 +492,20 @@ static void espnow_recv_cb(const esp_now_recv_info_t *info, const uint8_t *data, case alox_EspNowMessage_accel_deadzone_tag: handle_slave_accel_deadzone(info->src_addr, &msg.payload.accel_deadzone); break; + case alox_EspNowMessage_ota_start_tag: + case alox_EspNowMessage_ota_payload_tag: + case alox_EspNowMessage_ota_end_tag: + if (!s_slave_joined || !mac_equal(info->src_addr, s_master_mac)) { + break; + } + if (msg.which_payload == alox_EspNowMessage_ota_start_tag) { + ota_espnow_slave_on_start(info->src_addr, &msg.payload.ota_start); + } else if (msg.which_payload == alox_EspNowMessage_ota_payload_tag) { + ota_espnow_slave_on_payload(info->src_addr, &msg.payload.ota_payload); + } else { + ota_espnow_slave_on_end(info->src_addr); + } + break; default: ESP_LOGW(TAG, "slave: unhandled ESP-NOW which=%u type=%u", msg.which_payload, (unsigned)msg.type); @@ -378,6 +521,12 @@ static void espnow_recv_cb(const esp_now_recv_info_t *info, const uint8_t *data, return; } + if (msg.which_payload == alox_EspNowMessage_ota_status_tag) { + ensure_peer(info->src_addr); + ota_espnow_master_on_status(info->src_addr, &msg.payload.ota_status); + return; + } + const alox_EspNowSlavePresence *presence = esp_now_proto_get_presence(&msg); if (presence != NULL) { /* Registry key is the ESP-NOW sender MAC, not the optional protobuf mac field. */ @@ -463,6 +612,14 @@ esp_err_t esp_now_comm_init(const app_config_t *config) { ESP_ERROR_CHECK(esp_now_init()); ESP_ERROR_CHECK(esp_now_register_recv_cb(espnow_recv_cb)); + s_send_done = xSemaphoreCreateBinary(); + if (s_send_done != NULL && + esp_now_register_send_cb(espnow_send_done_cb) == ESP_OK) { + s_send_cb_ready = true; + } else { + ESP_LOGW(TAG, "ESP-NOW send-done callback unavailable (OTA may drop packets)"); + } + if (config->master) { ESP_ERROR_CHECK(ensure_broadcast_peer()); if (xTaskCreate(master_discover_task, "espnow_disc", 4096, NULL, 4, diff --git a/main/esp_now_comm.h b/main/esp_now_comm.h index 00bd7a5..9b68a94 100644 --- a/main/esp_now_comm.h +++ b/main/esp_now_comm.h @@ -15,4 +15,20 @@ esp_err_t esp_now_comm_send_accel_deadzone(const uint8_t mac[CLIENT_MAC_LEN], esp_err_t esp_now_comm_send_unicast_test(const uint8_t mac[CLIENT_MAC_LEN], uint32_t seq); +/** Master → slave OTA (unicast). */ +esp_err_t esp_now_comm_send_ota_start(const uint8_t mac[CLIENT_MAC_LEN], + uint32_t total_size); +esp_err_t esp_now_comm_send_ota_payload(const uint8_t mac[CLIENT_MAC_LEN], + uint32_t seq, const uint8_t *data, + size_t len); +esp_err_t esp_now_comm_send_ota_end(const uint8_t mac[CLIENT_MAC_LEN]); + +/** Slave → master OTA status. */ +esp_err_t esp_now_comm_send_ota_status(const uint8_t master_mac[CLIENT_MAC_LEN], + uint32_t status, uint32_t bytes_written, + uint32_t error); + +/** Slave: MAC of joined master (false if not joined). */ +bool esp_now_comm_get_master_mac(uint8_t mac_out[CLIENT_MAC_LEN]); + #endif diff --git a/main/ota_espnow.c b/main/ota_espnow.c new file mode 100644 index 0000000..8477dcf --- /dev/null +++ b/main/ota_espnow.c @@ -0,0 +1,336 @@ +#include "ota_espnow.h" +#include "app_config.h" +#include "client_registry.h" +#include "esp_log.h" +#include "esp_now_comm.h" +#include "esp_now_messages.pb.h" +#include "esp_partition.h" +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "freertos/idf_additions.h" +#include "ota_uart.h" +#include + +static const char *TAG = "[OTA_ESPNOW]"; + +#define OTA_ESPNOW_PREPARE_STACK 8192 +#define OTA_ESPNOW_PREPARE_PRIO 5 + +#define OTA_PREPARE_TIMEOUT_MS 120000u +#define OTA_BLOCK_TIMEOUT_MS 30000u +#define OTA_END_TIMEOUT_MS 60000u +#define OTA_PAYLOAD_DELAY_MS 3 + +#define OTA_ST_PREPARING 1u +#define OTA_ST_READY 2u +#define OTA_ST_BLOCK_ACK 3u +#define OTA_ST_SUCCESS 4u +#define OTA_ST_FAILED 5u + +#define OTA_MAX_TARGETS CLIENT_REGISTRY_MAX + +static EventGroupHandle_t s_eg; + +typedef struct { + uint8_t count; + uint8_t mac[OTA_MAX_TARGETS][6]; + uint32_t id[OTA_MAX_TARGETS]; + uint32_t expected_bytes; +} ota_dist_t; + +static ota_dist_t s_dist; + +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) { + return (int)i; + } + } + return -1; +} + +static uint32_t all_target_bits(void) { + if (s_dist.count == 0 || s_dist.count > 31) { + return 0; + } + return (1u << s_dist.count) - 1u; +} + +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; +} + +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; + } + + 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); + 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()) { + 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); + } +} + +void ota_espnow_slave_on_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; + } + + if (!ota_uart_is_active()) { + ESP_LOGW(TAG, "OTA_PAYLOAD seq=%lu but no active session", + (unsigned long)payload->seq); + send_slave_status(master_mac, OTA_ST_FAILED, 0, 12); + return; + } + + if (payload->seq == 0) { + ESP_LOGI(TAG, "ESP-NOW OTA payloads started"); + } + + ota_feed_result_t r = + ota_uart_feed(payload->data.bytes, payload->data.size); + if (r == OTA_FEED_ERROR) { + send_slave_status(master_mac, OTA_ST_FAILED, ota_uart_bytes_written(), 13); + return; + } + if (r == OTA_FEED_BLOCK_WRITTEN) { + uint32_t written = ota_uart_bytes_written(); + ESP_LOGI(TAG, "block written %lu bytes -> ack master", (unsigned long)written); + send_slave_status(master_mac, OTA_ST_BLOCK_ACK, written, 0); + } +} + +void ota_espnow_slave_on_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); + return; + } + + uint32_t written = ota_uart_bytes_written(); + bool success = false; + esp_err_t err = ota_uart_finish(true, &success); + if (err != ESP_OK || !success) { + send_slave_status(master_mac, OTA_ST_FAILED, written, (uint32_t)err); + return; + } + + send_slave_status(master_mac, OTA_ST_SUCCESS, written, 0); + ESP_LOGI(TAG, "slave OTA success (%lu bytes), reboot to run", + (unsigned long)written); +} + +void ota_espnow_master_on_status(const uint8_t slave_mac[6], + const alox_EspNowOtaStatus *status) { + if (status == NULL || s_eg == NULL) { + return; + } + + int idx = find_target_index(slave_mac); + if (idx < 0) { + return; + } + + uint32_t bit = (1u << (unsigned)idx); + + switch (status->status) { + case OTA_ST_READY: + xEventGroupSetBits(s_eg, bit); + break; + case OTA_ST_BLOCK_ACK: + if (status->bytes_written >= s_dist.expected_bytes) { + xEventGroupSetBits(s_eg, bit); + } else { + ESP_LOGW(TAG, "slave %lu block ack early (%lu < %lu)", + (unsigned long)s_dist.id[idx], (unsigned long)status->bytes_written, + (unsigned long)s_dist.expected_bytes); + } + break; + case OTA_ST_SUCCESS: + xEventGroupSetBits(s_eg, bit); + break; + case OTA_ST_FAILED: + ESP_LOGW(TAG, "slave %lu OTA failed (err=%lu)", + (unsigned long)s_dist.id[idx], (unsigned long)status->error); + break; + default: + break; + } +} + +static size_t collect_targets(void) { + memset(&s_dist, 0, sizeof(s_dist)); + + size_t n = client_registry_count(); + for (size_t i = 0; i < n && s_dist.count < OTA_MAX_TARGETS; i++) { + const client_info_t *c = client_registry_at(i); + if (c == NULL || !c->available) { + continue; + } + uint8_t slot = s_dist.count; + memcpy(s_dist.mac[slot], c->mac, 6); + s_dist.id[slot] = c->id; + s_dist.count++; + } + return s_dist.count; +} + +static esp_err_t distribute_image(const esp_partition_t *partition, + uint32_t size) { + if (s_eg == NULL) { + s_eg = xEventGroupCreate(); + if (s_eg == NULL) { + return ESP_ERR_NO_MEM; + } + } + + ESP_LOGI(TAG, "distributing %lu bytes from %s to %u slave(s)", + (unsigned long)size, partition->label, (unsigned)s_dist.count); + + uint32_t target_mask = all_target_bits(); + esp_err_t err; + + xEventGroupClearBits(s_eg, target_mask); + 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) { + ESP_LOGW(TAG, "OTA_START to slave %lu failed", + (unsigned long)s_dist.id[i]); + return err; + } + } + + if (!wait_target_bits(target_mask, OTA_PREPARE_TIMEOUT_MS)) { + ESP_LOGE(TAG, "timeout waiting for slave OTA ready"); + return ESP_ERR_TIMEOUT; + } + + uint8_t block_buf[OTA_UART_FLASH_BLOCK_SIZE]; + uint32_t offset = 0; + uint32_t seq = 0; + + while (offset < size) { + uint32_t block_len = size - offset; + if (block_len > OTA_UART_FLASH_BLOCK_SIZE) { + block_len = OTA_UART_FLASH_BLOCK_SIZE; + } + + err = esp_partition_read(partition, offset, block_buf, block_len); + if (err != ESP_OK) { + ESP_LOGE(TAG, "partition read @%lu failed: %s", (unsigned long)offset, + esp_err_to_name(err)); + return err; + } + + uint32_t sent = 0; + while (sent < block_len) { + uint32_t chunk = block_len - sent; + if (chunk > OTA_UART_HOST_CHUNK_SIZE) { + chunk = OTA_UART_HOST_CHUNK_SIZE; + } + + for (uint8_t i = 0; i < s_dist.count; i++) { + err = esp_now_comm_send_ota_payload(s_dist.mac[i], seq, + block_buf + sent, chunk); + if (err != ESP_OK) { + return err; + } + } + seq++; + sent += chunk; + vTaskDelay(pdMS_TO_TICKS(OTA_PAYLOAD_DELAY_MS)); + } + + const bool full_block = (block_len >= OTA_UART_FLASH_BLOCK_SIZE); + s_dist.expected_bytes = offset + block_len; + + if (full_block) { + xEventGroupClearBits(s_eg, target_mask); + if (!wait_target_bits(target_mask, OTA_BLOCK_TIMEOUT_MS)) { + ESP_LOGE(TAG, "timeout block ack @%lu bytes", + (unsigned long)s_dist.expected_bytes); + return ESP_ERR_TIMEOUT; + } + ESP_LOGI(TAG, "block ack @%lu/%lu (%lu%%)", + (unsigned long)s_dist.expected_bytes, (unsigned long)size, + (unsigned long)(s_dist.expected_bytes * 100 / size)); + } else { + ESP_LOGI(TAG, "final partial block %lu bytes (flush on OTA_END)", + (unsigned long)block_len); + } + offset += block_len; + } + + 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) { + return err; + } + } + + if (!wait_target_bits(target_mask, OTA_END_TIMEOUT_MS)) { + ESP_LOGE(TAG, "timeout waiting for slave OTA success"); + return ESP_ERR_TIMEOUT; + } + + 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) { + if (partition == NULL || size == 0) { + return ESP_ERR_INVALID_ARG; + } + + if (collect_targets() == 0) { + ESP_LOGI(TAG, "no available slaves — skip ESP-NOW OTA"); + return ESP_OK; + } + + return distribute_image(partition, size); +} diff --git a/main/ota_espnow.h b/main/ota_espnow.h new file mode 100644 index 0000000..e896814 --- /dev/null +++ b/main/ota_espnow.h @@ -0,0 +1,23 @@ +#ifndef OTA_ESPNOW_H +#define OTA_ESPNOW_H + +#include "esp_err.h" +#include "esp_now_messages.pb.h" +#include "esp_partition.h" +#include + +/** Master: read staged image from partition and push to all available slaves. */ +esp_err_t ota_espnow_distribute(const esp_partition_t *partition, uint32_t size); + +/** Master: handle slave → master OTA status / block ACK. */ +void ota_espnow_master_on_status(const uint8_t slave_mac[6], + const alox_EspNowOtaStatus *status); + +/** Slave: handle master → slave OTA commands. */ +void ota_espnow_slave_on_start(const uint8_t master_mac[6], + const alox_EspNowOtaStart *start); +void ota_espnow_slave_on_payload(const uint8_t master_mac[6], + const alox_EspNowOtaPayload *payload); +void ota_espnow_slave_on_end(const uint8_t master_mac[6]); + +#endif diff --git a/main/ota_uart.c b/main/ota_uart.c index a6c2635..9a27927 100644 --- a/main/ota_uart.c +++ b/main/ota_uart.c @@ -19,6 +19,13 @@ typedef struct { static ota_uart_state_t s_ota; +static struct { + bool valid; + const esp_partition_t *partition; + uint32_t size; + int target_slot; +} s_staged; + static int partition_slot(const esp_partition_t *part) { if (part == NULL) { return -1; @@ -144,7 +151,38 @@ ota_feed_result_t ota_uart_feed(const uint8_t *data, size_t len) { uint32_t ota_uart_bytes_written(void) { return s_ota.written; } -esp_err_t ota_uart_finish(bool *success_out) { +bool ota_uart_get_staged_image(const esp_partition_t **partition_out, + uint32_t *size_out) { + if (!s_staged.valid || s_staged.partition == NULL) { + return false; + } + if (partition_out != NULL) { + *partition_out = s_staged.partition; + } + if (size_out != NULL) { + *size_out = s_staged.size; + } + return true; +} + +void ota_uart_clear_staged(void) { memset(&s_staged, 0, sizeof(s_staged)); } + +esp_err_t ota_uart_apply_boot(void) { + if (!s_staged.valid || s_staged.partition == NULL) { + return ESP_ERR_INVALID_STATE; + } + esp_err_t err = esp_ota_set_boot_partition(s_staged.partition); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ota_set_boot_partition failed: %s", esp_err_to_name(err)); + return err; + } + ESP_LOGI(TAG, "boot partition set to %s (slot %d)", + s_staged.partition->label, s_staged.target_slot); + ota_uart_clear_staged(); + return ESP_OK; +} + +esp_err_t ota_uart_finish(bool set_boot, bool *success_out) { if (success_out != NULL) { *success_out = false; } @@ -165,20 +203,27 @@ esp_err_t ota_uart_finish(bool *success_out) { return err; } - err = esp_ota_set_boot_partition(s_ota.update_partition); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_ota_set_boot_partition failed: %s", esp_err_to_name(err)); - memset(&s_ota, 0, sizeof(s_ota)); - return err; - } + ota_uart_clear_staged(); + s_staged.valid = true; + s_staged.partition = s_ota.update_partition; + s_staged.size = s_ota.written; + s_staged.target_slot = s_ota.target_slot; - ESP_LOGI(TAG, "OTA complete: %lu bytes written to %s (slot %d), reboot to run", - (unsigned long)s_ota.written, s_ota.update_partition->label, - s_ota.target_slot); + ESP_LOGI(TAG, "OTA image staged: %lu bytes on %s (slot %d)", + (unsigned long)s_staged.size, s_staged.partition->label, + s_staged.target_slot); + + memset(&s_ota, 0, sizeof(s_ota)); + + if (set_boot) { + err = ota_uart_apply_boot(); + if (err != ESP_OK) { + return err; + } + } if (success_out != NULL) { *success_out = true; } - memset(&s_ota, 0, sizeof(s_ota)); return ESP_OK; } diff --git a/main/ota_uart.h b/main/ota_uart.h index df55d91..66754bf 100644 --- a/main/ota_uart.h +++ b/main/ota_uart.h @@ -2,6 +2,7 @@ #define OTA_UART_H #include "esp_err.h" +#include "esp_partition.h" #include #include #include @@ -39,7 +40,19 @@ ota_feed_result_t ota_uart_feed(const uint8_t *data, size_t len); uint32_t ota_uart_bytes_written(void); -/** Flush remainder, esp_ota_end, set boot partition on success. */ -esp_err_t ota_uart_finish(bool *success_out); +/** + * Flush remainder and esp_ota_end. When set_boot is false, the staged image + * remains readable via ota_uart_get_staged_image() until ota_uart_apply_boot(). + */ +esp_err_t ota_uart_finish(bool set_boot, bool *success_out); + +/** Staged partition after ota_uart_finish(false, …); valid until apply_boot or abort. */ +bool ota_uart_get_staged_image(const esp_partition_t **partition_out, + uint32_t *size_out); + +/** Set boot partition to the last staged image (after slave distribution). */ +esp_err_t ota_uart_apply_boot(void); + +void ota_uart_clear_staged(void); #endif diff --git a/main/proto/esp_now_messages.pb.c b/main/proto/esp_now_messages.pb.c index 9390c7f..3af93e2 100644 --- a/main/proto/esp_now_messages.pb.c +++ b/main/proto/esp_now_messages.pb.c @@ -18,6 +18,18 @@ PB_BIND(alox_EspNowSlavePresence, alox_EspNowSlavePresence, AUTO) PB_BIND(alox_EspNowAccelDeadzone, alox_EspNowAccelDeadzone, AUTO) +PB_BIND(alox_EspNowOtaStart, alox_EspNowOtaStart, AUTO) + + +PB_BIND(alox_EspNowOtaPayload, alox_EspNowOtaPayload, AUTO) + + +PB_BIND(alox_EspNowOtaEnd, alox_EspNowOtaEnd, AUTO) + + +PB_BIND(alox_EspNowOtaStatus, alox_EspNowOtaStatus, AUTO) + + PB_BIND(alox_EspNowMessage, alox_EspNowMessage, AUTO) diff --git a/main/proto/esp_now_messages.pb.h b/main/proto/esp_now_messages.pb.h index 6f028a4..8d54848 100644 --- a/main/proto/esp_now_messages.pb.h +++ b/main/proto/esp_now_messages.pb.h @@ -16,7 +16,11 @@ typedef enum _alox_EspNowMessageType { alox_EspNowMessageType_ESPNOW_SLAVE_INFO = 2, alox_EspNowMessageType_ESPNOW_HEARTBEAT = 3, alox_EspNowMessageType_ESPNOW_SET_ACCEL_DEADZONE = 4, - alox_EspNowMessageType_ESPNOW_UNICAST_TEST = 5 + alox_EspNowMessageType_ESPNOW_UNICAST_TEST = 5, + alox_EspNowMessageType_ESPNOW_OTA_START = 6, + alox_EspNowMessageType_ESPNOW_OTA_PAYLOAD = 7, + alox_EspNowMessageType_ESPNOW_OTA_END = 8, + alox_EspNowMessageType_ESPNOW_OTA_STATUS = 9 } alox_EspNowMessageType; /* Struct definitions */ @@ -42,6 +46,30 @@ typedef struct _alox_EspNowAccelDeadzone { uint32_t client_id; /* 0 = all slaves; otherwise only matching slave_id applies */ } alox_EspNowAccelDeadzone; +/* Master → slave: begin OTA (erase inactive slot; slave replies ESPNOW_OTA_STATUS). */ +typedef struct _alox_EspNowOtaStart { + uint32_t total_size; +} alox_EspNowOtaStart; + +typedef PB_BYTES_ARRAY_T(200) alox_EspNowOtaPayload_data_t; +/* Master → slave: firmware chunk (up to 200 bytes); slave buffers 4 KiB before flash write. */ +typedef struct _alox_EspNowOtaPayload { + uint32_t seq; + alox_EspNowOtaPayload_data_t data; +} alox_EspNowOtaPayload; + +/* Master → slave: finalize OTA (flush, esp_ota_end, set boot partition). */ +typedef struct _alox_EspNowOtaEnd { + char dummy_field; +} alox_EspNowOtaEnd; + +/* Slave → master: status / block ACK (same status codes as UART OtaStatusPayload). */ +typedef struct _alox_EspNowOtaStatus { + uint32_t status; + uint32_t bytes_written; + uint32_t error; +} alox_EspNowOtaStatus; + typedef struct _alox_EspNowMessage { alox_EspNowMessageType type; pb_size_t which_payload; @@ -51,6 +79,10 @@ typedef struct _alox_EspNowMessage { alox_EspNowSlavePresence heartbeat; alox_EspNowAccelDeadzone accel_deadzone; alox_EspNowUnicastTest unicast_test; + alox_EspNowOtaStart ota_start; + alox_EspNowOtaPayload ota_payload; + alox_EspNowOtaEnd ota_end; + alox_EspNowOtaStatus ota_status; } payload; } alox_EspNowMessage; @@ -61,8 +93,12 @@ extern "C" { /* Helper constants for enums */ #define _alox_EspNowMessageType_MIN alox_EspNowMessageType_ESPNOW_UNKNOWN -#define _alox_EspNowMessageType_MAX alox_EspNowMessageType_ESPNOW_UNICAST_TEST -#define _alox_EspNowMessageType_ARRAYSIZE ((alox_EspNowMessageType)(alox_EspNowMessageType_ESPNOW_UNICAST_TEST+1)) +#define _alox_EspNowMessageType_MAX alox_EspNowMessageType_ESPNOW_OTA_STATUS +#define _alox_EspNowMessageType_ARRAYSIZE ((alox_EspNowMessageType)(alox_EspNowMessageType_ESPNOW_OTA_STATUS+1)) + + + + @@ -76,11 +112,19 @@ extern "C" { #define alox_EspNowDiscover_init_default {0} #define alox_EspNowSlavePresence_init_default {0, {{NULL}, NULL}, 0, 0, 0, 0} #define alox_EspNowAccelDeadzone_init_default {0, 0} +#define alox_EspNowOtaStart_init_default {0} +#define alox_EspNowOtaPayload_init_default {0, {0, {0}}} +#define alox_EspNowOtaEnd_init_default {0} +#define alox_EspNowOtaStatus_init_default {0, 0, 0} #define alox_EspNowMessage_init_default {_alox_EspNowMessageType_MIN, 0, {alox_EspNowDiscover_init_default}} #define alox_EspNowUnicastTest_init_zero {0} #define alox_EspNowDiscover_init_zero {0} #define alox_EspNowSlavePresence_init_zero {0, {{NULL}, NULL}, 0, 0, 0, 0} #define alox_EspNowAccelDeadzone_init_zero {0, 0} +#define alox_EspNowOtaStart_init_zero {0} +#define alox_EspNowOtaPayload_init_zero {0, {0, {0}}} +#define alox_EspNowOtaEnd_init_zero {0} +#define alox_EspNowOtaStatus_init_zero {0, 0, 0} #define alox_EspNowMessage_init_zero {_alox_EspNowMessageType_MIN, 0, {alox_EspNowDiscover_init_zero}} /* Field tags (for use in manual encoding/decoding) */ @@ -94,12 +138,22 @@ extern "C" { #define alox_EspNowSlavePresence_used_tag 6 #define alox_EspNowAccelDeadzone_deadzone_tag 1 #define alox_EspNowAccelDeadzone_client_id_tag 2 +#define alox_EspNowOtaStart_total_size_tag 1 +#define alox_EspNowOtaPayload_seq_tag 1 +#define alox_EspNowOtaPayload_data_tag 2 +#define alox_EspNowOtaStatus_status_tag 1 +#define alox_EspNowOtaStatus_bytes_written_tag 2 +#define alox_EspNowOtaStatus_error_tag 3 #define alox_EspNowMessage_type_tag 1 #define alox_EspNowMessage_discover_tag 2 #define alox_EspNowMessage_slave_info_tag 3 #define alox_EspNowMessage_heartbeat_tag 4 #define alox_EspNowMessage_accel_deadzone_tag 5 #define alox_EspNowMessage_unicast_test_tag 6 +#define alox_EspNowMessage_ota_start_tag 7 +#define alox_EspNowMessage_ota_payload_tag 8 +#define alox_EspNowMessage_ota_end_tag 9 +#define alox_EspNowMessage_ota_status_tag 10 /* Struct field encoding specification for nanopb */ #define alox_EspNowUnicastTest_FIELDLIST(X, a) \ @@ -128,13 +182,40 @@ X(a, STATIC, SINGULAR, UINT32, client_id, 2) #define alox_EspNowAccelDeadzone_CALLBACK NULL #define alox_EspNowAccelDeadzone_DEFAULT NULL +#define alox_EspNowOtaStart_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, total_size, 1) +#define alox_EspNowOtaStart_CALLBACK NULL +#define alox_EspNowOtaStart_DEFAULT NULL + +#define alox_EspNowOtaPayload_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, seq, 1) \ +X(a, STATIC, SINGULAR, BYTES, data, 2) +#define alox_EspNowOtaPayload_CALLBACK NULL +#define alox_EspNowOtaPayload_DEFAULT NULL + +#define alox_EspNowOtaEnd_FIELDLIST(X, a) \ + +#define alox_EspNowOtaEnd_CALLBACK NULL +#define alox_EspNowOtaEnd_DEFAULT NULL + +#define alox_EspNowOtaStatus_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, status, 1) \ +X(a, STATIC, SINGULAR, UINT32, bytes_written, 2) \ +X(a, STATIC, SINGULAR, UINT32, error, 3) +#define alox_EspNowOtaStatus_CALLBACK NULL +#define alox_EspNowOtaStatus_DEFAULT NULL + #define alox_EspNowMessage_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UENUM, type, 1) \ X(a, STATIC, ONEOF, MESSAGE, (payload,discover,payload.discover), 2) \ X(a, STATIC, ONEOF, MESSAGE, (payload,slave_info,payload.slave_info), 3) \ X(a, STATIC, ONEOF, MESSAGE, (payload,heartbeat,payload.heartbeat), 4) \ X(a, STATIC, ONEOF, MESSAGE, (payload,accel_deadzone,payload.accel_deadzone), 5) \ -X(a, STATIC, ONEOF, MESSAGE, (payload,unicast_test,payload.unicast_test), 6) +X(a, STATIC, ONEOF, MESSAGE, (payload,unicast_test,payload.unicast_test), 6) \ +X(a, STATIC, ONEOF, MESSAGE, (payload,ota_start,payload.ota_start), 7) \ +X(a, STATIC, ONEOF, MESSAGE, (payload,ota_payload,payload.ota_payload), 8) \ +X(a, STATIC, ONEOF, MESSAGE, (payload,ota_end,payload.ota_end), 9) \ +X(a, STATIC, ONEOF, MESSAGE, (payload,ota_status,payload.ota_status), 10) #define alox_EspNowMessage_CALLBACK NULL #define alox_EspNowMessage_DEFAULT NULL #define alox_EspNowMessage_payload_discover_MSGTYPE alox_EspNowDiscover @@ -142,11 +223,19 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,unicast_test,payload.unicast_test), #define alox_EspNowMessage_payload_heartbeat_MSGTYPE alox_EspNowSlavePresence #define alox_EspNowMessage_payload_accel_deadzone_MSGTYPE alox_EspNowAccelDeadzone #define alox_EspNowMessage_payload_unicast_test_MSGTYPE alox_EspNowUnicastTest +#define alox_EspNowMessage_payload_ota_start_MSGTYPE alox_EspNowOtaStart +#define alox_EspNowMessage_payload_ota_payload_MSGTYPE alox_EspNowOtaPayload +#define alox_EspNowMessage_payload_ota_end_MSGTYPE alox_EspNowOtaEnd +#define alox_EspNowMessage_payload_ota_status_MSGTYPE alox_EspNowOtaStatus extern const pb_msgdesc_t alox_EspNowUnicastTest_msg; extern const pb_msgdesc_t alox_EspNowDiscover_msg; extern const pb_msgdesc_t alox_EspNowSlavePresence_msg; extern const pb_msgdesc_t alox_EspNowAccelDeadzone_msg; +extern const pb_msgdesc_t alox_EspNowOtaStart_msg; +extern const pb_msgdesc_t alox_EspNowOtaPayload_msg; +extern const pb_msgdesc_t alox_EspNowOtaEnd_msg; +extern const pb_msgdesc_t alox_EspNowOtaStatus_msg; extern const pb_msgdesc_t alox_EspNowMessage_msg; /* Defines for backwards compatibility with code written before nanopb-0.4.0 */ @@ -154,14 +243,22 @@ extern const pb_msgdesc_t alox_EspNowMessage_msg; #define alox_EspNowDiscover_fields &alox_EspNowDiscover_msg #define alox_EspNowSlavePresence_fields &alox_EspNowSlavePresence_msg #define alox_EspNowAccelDeadzone_fields &alox_EspNowAccelDeadzone_msg +#define alox_EspNowOtaStart_fields &alox_EspNowOtaStart_msg +#define alox_EspNowOtaPayload_fields &alox_EspNowOtaPayload_msg +#define alox_EspNowOtaEnd_fields &alox_EspNowOtaEnd_msg +#define alox_EspNowOtaStatus_fields &alox_EspNowOtaStatus_msg #define alox_EspNowMessage_fields &alox_EspNowMessage_msg /* Maximum encoded size of messages (where known) */ /* alox_EspNowSlavePresence_size depends on runtime parameters */ /* alox_EspNowMessage_size depends on runtime parameters */ -#define ALOX_MAIN_PROTO_ESP_NOW_MESSAGES_PB_H_MAX_SIZE alox_EspNowAccelDeadzone_size +#define ALOX_MAIN_PROTO_ESP_NOW_MESSAGES_PB_H_MAX_SIZE alox_EspNowOtaPayload_size #define alox_EspNowAccelDeadzone_size 12 #define alox_EspNowDiscover_size 6 +#define alox_EspNowOtaEnd_size 0 +#define alox_EspNowOtaPayload_size 209 +#define alox_EspNowOtaStart_size 6 +#define alox_EspNowOtaStatus_size 18 #define alox_EspNowUnicastTest_size 6 #ifdef __cplusplus diff --git a/main/proto/esp_now_messages.proto b/main/proto/esp_now_messages.proto index 5a0466e..9e7d6db 100644 --- a/main/proto/esp_now_messages.proto +++ b/main/proto/esp_now_messages.proto @@ -1,5 +1,7 @@ syntax = "proto3"; +import "nanopb.proto"; + package alox; enum EspNowMessageType { @@ -9,6 +11,10 @@ enum EspNowMessageType { ESPNOW_HEARTBEAT = 3; ESPNOW_SET_ACCEL_DEADZONE = 4; ESPNOW_UNICAST_TEST = 5; + ESPNOW_OTA_START = 6; + ESPNOW_OTA_PAYLOAD = 7; + ESPNOW_OTA_END = 8; + ESPNOW_OTA_STATUS = 9; } message EspNowUnicastTest { @@ -33,6 +39,27 @@ message EspNowAccelDeadzone { uint32 client_id = 2; // 0 = all slaves; otherwise only matching slave_id applies } +// Master → slave: begin OTA (erase inactive slot; slave replies ESPNOW_OTA_STATUS). +message EspNowOtaStart { + uint32 total_size = 1; +} + +// Master → slave: firmware chunk (up to 200 bytes); slave buffers 4 KiB before flash write. +message EspNowOtaPayload { + uint32 seq = 1; + bytes data = 2 [(nanopb).max_size = 200]; +} + +// Master → slave: finalize OTA (flush, esp_ota_end, set boot partition). +message EspNowOtaEnd {} + +// Slave → master: status / block ACK (same status codes as UART OtaStatusPayload). +message EspNowOtaStatus { + uint32 status = 1; + uint32 bytes_written = 2; + uint32 error = 3; +} + message EspNowMessage { EspNowMessageType type = 1; oneof payload { @@ -41,5 +68,9 @@ message EspNowMessage { EspNowSlavePresence heartbeat = 4; EspNowAccelDeadzone accel_deadzone = 5; EspNowUnicastTest unicast_test = 6; + EspNowOtaStart ota_start = 7; + EspNowOtaPayload ota_payload = 8; + EspNowOtaEnd ota_end = 9; + EspNowOtaStatus ota_status = 10; } }