Compare commits
2 Commits
4bf43d8a5e
...
5a948a5c8c
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a948a5c8c | |||
| 9b7bda8551 |
3
Makefile
3
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
|
||||
|
||||
|
||||
@ -27,7 +27,7 @@ go run . -port /dev/ttyUSB0 clients
|
||||
| `unicast-test` | `0x07` | Sends ESP-NOW unicast test to one slave (`-client`, `-seq`) |
|
||||
| `test` | — | Run an automated scenario (JSON configs under `testdata/`) |
|
||||
| `serve` | — | Web dashboard at `http://localhost:8080` (WebSocket live updates) |
|
||||
| `ota` | 16–19 | UART firmware upload to inactive OTA slot (200 B chunks, 4 KiB flash blocks) |
|
||||
| `ota` | 16–19 | UART firmware upload to master; firmware then pushes to slaves via ESP-NOW |
|
||||
|
||||
`clients` requires slaves to have responded to master discover broadcasts first.
|
||||
|
||||
@ -68,13 +68,18 @@ The dashboard can configure nodes using the same UART commands as the CLI:
|
||||
| Alle Slaves | per-slave ESP-NOW (Master bleibt unverändert; CLI `-all` setzt auch den Master) |
|
||||
| Unicast test | `unicast-test -client ID` |
|
||||
|
||||
HTTP API (used by the web UI): `GET/POST /api/deadzone`, `POST /api/unicast-test`.
|
||||
HTTP API (used by the web UI): `GET/POST /api/deadzone`, `POST /api/unicast-test`, `POST /api/ota` (multipart field `firmware`, max 2 MiB).
|
||||
|
||||
| UI / API | Behaviour |
|
||||
|----------|-----------|
|
||||
| Firmware OTA card | Same as `ota` CLI; progress via WebSocket `ota_progress` events |
|
||||
| `POST /api/ota` | Upload `.bin` to master only — slaves are updated by firmware over ESP-NOW after `OTA_END` |
|
||||
|
||||
```bash
|
||||
go run . -port /dev/ttyUSB0 ota build/powerpod.bin
|
||||
```
|
||||
|
||||
Waits for **ready** after start (~30 s erase), sends 200-byte `OTA_PAYLOAD` frames, reads **block_ack** every 4 KiB, then `OTA_END` and **success**.
|
||||
Waits for **ready** after start (~30 s erase), sends 200-byte `OTA_PAYLOAD` frames, reads **block_ack** every 4 KiB, then `OTA_END`. The master then distributes to all available slaves (no extra host traffic); **success** is reported only when that finishes. Allow several minutes for large images. Reboot master and slaves to boot the new firmware.
|
||||
|
||||
```bash
|
||||
go run . -port /dev/ttyUSB0 unicast-test -client 16 -seq 42
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -110,6 +110,24 @@ 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) |
|
||||
|
||||
### ESP-NOW OTA (master → slaves)
|
||||
|
||||
Triggered automatically after a successful UART `OTA_END` on the master (or manually via UART `OTA_START_ESPNOW` if an image is already **staged**). Implementation: `ota_espnow.c`.
|
||||
|
||||
| Step | Master → slave | Slave → master |
|
||||
|------|----------------|----------------|
|
||||
| 1 | `ESPNOW_OTA_START` + `total_size` | `ESPNOW_OTA_STATUS` preparing, then **ready** |
|
||||
| 2 | `ESPNOW_OTA_PAYLOAD` (**≤200 B**, shared `seq`) | **block_ack** after each **4096 B** written to flash |
|
||||
| 3 | `ESPNOW_OTA_END` | **success** or **failed** (+ `bytes_written`) |
|
||||
|
||||
Master reads the staged partition with `esp_partition_read` (same image just written via UART). Only **available** registry slaves are updated. The last transfer block may be **under 4096 B** — no block_ack is waited for that block; slaves flush the remainder on `ESPNOW_OTA_END`.
|
||||
|
||||
Status codes match UART `OtaStatusPayload` (`1`…`5`). After success, master and slaves have the boot partition set — **reboot all nodes** to run the new firmware.
|
||||
|
||||
`EspNowSlavePresence`: `network`, `mac` (6 bytes), `version`, `slave_id`, `available`, `used`.
|
||||
|
||||
@ -186,9 +204,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:
|
||||
|
||||
@ -220,15 +238,21 @@ At boot, firmware logs the running partition and OTA slot index (A/B).
|
||||
|
||||
### UART OTA (A/B)
|
||||
|
||||
Master only. Inactive app partition is selected with `esp_ota_get_next_update_partition()`; `esp_ota_begin` erases it (can take ~30 s — host should wait).
|
||||
**UART upload is master-only.** Slaves receive the same image afterwards via [ESP-NOW OTA](#esp-now-ota-master--slaves).
|
||||
|
||||
| Step | Host → device | Device → host |
|
||||
|------|----------------|---------------|
|
||||
Inactive app partition is selected with `esp_ota_get_next_update_partition()`; `esp_ota_begin` erases it (can take ~30 s — host should wait).
|
||||
|
||||
| Step | Host → master | Master → host |
|
||||
|------|---------------|---------------|
|
||||
| 1 | `OTA_START` + `total_size` | `OTA_STATUS` preparing, then **ready** (+ `target_slot` 0/1) |
|
||||
| 2 | `OTA_PAYLOAD` chunks (**≤200 B**, `seq` optional) | `OTA_STATUS` **block_ack** only after each **4096 B** written to flash |
|
||||
| 3 | `OTA_END` | `OTA_STATUS` **success** or **failed** (+ `bytes_written`) |
|
||||
| 3 | `OTA_END` | Stages image, runs ESP-NOW OTA to all available slaves, sets boot partition, then `OTA_STATUS` **success** or **failed** |
|
||||
|
||||
Implementation: `ota_uart.c` (4 KiB buffer, `esp_ota_write`), `cmd_ota.c`.
|
||||
`OTA_END` can take a long time on the wire (slave flash + ESP-NOW); the host should use a generous read timeout.
|
||||
|
||||
`OTA_START_ESPNOW` (type `20`): re-run ESP-NOW distribution from the last staged image without a new UART upload (no-op if nothing staged).
|
||||
|
||||
Implementation: `ota_uart.c` (4 KiB buffer, `esp_ota_write`), `ota_espnow.c`, `cmd_ota.c`.
|
||||
|
||||
Host upload:
|
||||
|
||||
@ -334,7 +358,10 @@ Target: ESP32-S3. Close serial monitor on the UART adapter port before running `
|
||||
| `powerpod.c` | `app_main`, DIP/network config, init order |
|
||||
| `powerpod.h` | Pin defines |
|
||||
| `app_config.h` | `app_config_t` |
|
||||
| `esp_now_comm.c/h` | WiFi, ESP-NOW, discover / slave info |
|
||||
| `esp_now_comm.c/h` | WiFi, ESP-NOW, discover / slave info / OTA send |
|
||||
| `ota_uart.c/h` | Shared 4 KiB OTA flash buffer (UART + ESP-NOW) |
|
||||
| `ota_espnow.c/h` | Master: distribute staged image to slaves |
|
||||
| `cmd_ota.c/h` | UART OTA command handlers (master only) |
|
||||
| `uart.c/h` | Framed UART RX/TX |
|
||||
| `uart_proto.c/h` | Encode/send `UartMessage` |
|
||||
| `cmd_handler.c/h` | Command queue and dispatch |
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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 <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
336
main/ota_espnow.c
Normal file
336
main/ota_espnow.c
Normal file
@ -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 <string.h>
|
||||
|
||||
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);
|
||||
}
|
||||
23
main/ota_espnow.h
Normal file
23
main/ota_espnow.h
Normal file
@ -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 <stdint.h>
|
||||
|
||||
/** 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
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
#define OTA_UART_H
|
||||
|
||||
#include "esp_err.h"
|
||||
#include "esp_partition.h"
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user