From 972db16635cf05a36f87e87e95258334a565dca3 Mon Sep 17 00:00:00 2001 From: sl-firmware Date: Fri, 17 Apr 2026 22:16:11 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20UART=20OTA=20protocol=20Balance?= =?UTF-8?q?=E2=86=92IO=20board,=201=20KB=20chunk=20+=20ACK=20(bd-21hv)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Balance side (uart_ota.c): downloads io-firmware.bin from Gitea to RAM, computes SHA256, then streams to IO over UART1 (GPIO17/18, 460800 baud) as OTA_BEGIN/OTA_DATA/OTA_END frames with CRC8 + per-chunk ACK/retry (×3). IO side (uart_ota_recv.c): receives frames, writes to inactive OTA partition via esp_ota_write, verifies SHA256 on OTA_END, sets boot partition, reboots. IO board main.c + CMakeLists.txt scaffold included. Co-Authored-By: Claude Sonnet 4.6 --- esp32s3/balance/main/uart_ota.c | 241 ++++++++++++++++++++++++++++++++ esp32s3/balance/main/uart_ota.h | 64 +++++++++ esp32s3/io/main/CMakeLists.txt | 4 + esp32s3/io/main/main.c | 42 ++++++ esp32s3/io/main/uart_ota_recv.c | 210 ++++++++++++++++++++++++++++ esp32s3/io/main/uart_ota_recv.h | 20 +++ 6 files changed, 581 insertions(+) create mode 100644 esp32s3/balance/main/uart_ota.c create mode 100644 esp32s3/balance/main/uart_ota.h create mode 100644 esp32s3/io/main/CMakeLists.txt create mode 100644 esp32s3/io/main/main.c create mode 100644 esp32s3/io/main/uart_ota_recv.c create mode 100644 esp32s3/io/main/uart_ota_recv.h diff --git a/esp32s3/balance/main/uart_ota.c b/esp32s3/balance/main/uart_ota.c new file mode 100644 index 0000000..acbf34c --- /dev/null +++ b/esp32s3/balance/main/uart_ota.c @@ -0,0 +1,241 @@ +/* uart_ota.c — UART OTA sender: Balance→IO board (bd-21hv) + * + * Downloads io-firmware.bin from Gitea, then sends to IO board via UART1. + * IO board must update itself BEFORE Balance self-update (per spec). + */ + +#include "uart_ota.h" +#include "gitea_ota.h" +#include "esp_log.h" +#include "esp_http_client.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "mbedtls/sha256.h" +#include +#include + +static const char *TAG = "uart_ota"; + +volatile uart_ota_send_state_t g_uart_ota_state = UART_OTA_S_IDLE; +volatile uint8_t g_uart_ota_progress = 0; + +/* ── CRC8-SMBUS ── */ +static uint8_t crc8(const uint8_t *d, uint16_t len) +{ + uint8_t crc = 0; + for (uint16_t i = 0; i < len; i++) { + crc ^= d[i]; + for (uint8_t b = 0; b < 8; b++) + crc = (crc & 0x80u) ? (uint8_t)((crc << 1u) ^ 0x07u) : (uint8_t)(crc << 1u); + } + return crc; +} + +/* ── Build and send one UART OTA frame ── */ +static void send_frame(uint8_t type, uint16_t seq, + const uint8_t *payload, uint16_t plen) +{ + /* [TYPE:1][SEQ:2 BE][LEN:2 BE][PAYLOAD][CRC8:1] */ + uint8_t hdr[5]; + hdr[0] = type; + hdr[1] = (uint8_t)(seq >> 8u); + hdr[2] = (uint8_t)(seq); + hdr[3] = (uint8_t)(plen >> 8u); + hdr[4] = (uint8_t)(plen); + + /* CRC over hdr + payload */ + uint8_t crc_buf[5 + OTA_UART_CHUNK_SIZE]; + memcpy(crc_buf, hdr, 5); + if (plen > 0 && payload) memcpy(crc_buf + 5, payload, plen); + uint8_t crc = crc8(crc_buf, (uint16_t)(5 + plen)); + + uart_write_bytes(UART_OTA_PORT, (char *)hdr, 5); + if (plen > 0 && payload) + uart_write_bytes(UART_OTA_PORT, (char *)payload, plen); + uart_write_bytes(UART_OTA_PORT, (char *)&crc, 1); +} + +/* ── Wait for ACK/NACK from IO board ── */ +static bool wait_ack(uint16_t expected_seq) +{ + /* Response frame: [TYPE:1][SEQ:2][LEN:2][PAYLOAD][CRC:1] */ + uint8_t buf[16]; + int timeout = OTA_UART_ACK_TIMEOUT_MS; + int got = 0; + + while (timeout > 0 && got < 6) { + int r = uart_read_bytes(UART_OTA_PORT, buf + got, 1, pdMS_TO_TICKS(50)); + if (r > 0) got++; + else timeout -= 50; + } + + if (got < 3) return false; + + uint8_t type = buf[0]; + uint16_t seq = (uint16_t)((buf[1] << 8u) | buf[2]); + + if (type == UART_OTA_ACK && seq == expected_seq) return true; + if (type == UART_OTA_NACK) { + uint8_t err = (got >= 6) ? buf[5] : 0; + ESP_LOGW(TAG, "NACK seq=%u err=%u", seq, err); + } + return false; +} + +/* ── Download firmware to RAM buffer (max 1.75 MB) ── */ +static uint8_t *download_io_firmware(uint32_t *out_size) +{ + const char *url = g_io_update.download_url; + ESP_LOGI(TAG, "downloading IO fw: %s", url); + + esp_http_client_config_t cfg = { + .url = url, .timeout_ms = 30000, + .skip_cert_common_name_check = true, + }; + esp_http_client_handle_t client = esp_http_client_init(&cfg); + if (esp_http_client_open(client, 0) != ESP_OK) { + esp_http_client_cleanup(client); + return NULL; + } + + int content_len = esp_http_client_fetch_headers(client); + if (content_len <= 0 || content_len > (int)(0x1B0000)) { + ESP_LOGE(TAG, "bad content-length: %d", content_len); + esp_http_client_cleanup(client); + return NULL; + } + + uint8_t *buf = malloc(content_len); + if (!buf) { + ESP_LOGE(TAG, "malloc %d failed", content_len); + esp_http_client_cleanup(client); + return NULL; + } + + int total = 0, rd; + while ((rd = esp_http_client_read(client, (char *)buf + total, + content_len - total)) > 0) { + total += rd; + g_uart_ota_progress = (uint8_t)((total * 50) / content_len); /* 0-50% for download */ + } + esp_http_client_cleanup(client); + + if (total != content_len) { + free(buf); + return NULL; + } + *out_size = (uint32_t)total; + return buf; +} + +/* ── UART OTA send task ── */ +static void uart_ota_task(void *arg) +{ + g_uart_ota_state = UART_OTA_S_DOWNLOADING; + g_uart_ota_progress = 0; + + uint32_t fw_size = 0; + uint8_t *fw = download_io_firmware(&fw_size); + if (!fw) { + ESP_LOGE(TAG, "download failed"); + g_uart_ota_state = UART_OTA_S_FAILED; + vTaskDelete(NULL); + return; + } + + /* Compute SHA256 of downloaded firmware */ + uint8_t digest[32]; + mbedtls_sha256_context sha; + mbedtls_sha256_init(&sha); + mbedtls_sha256_starts(&sha, 0); + mbedtls_sha256_update(&sha, fw, fw_size); + mbedtls_sha256_finish(&sha, digest); + mbedtls_sha256_free(&sha); + + g_uart_ota_state = UART_OTA_S_SENDING; + + /* Send OTA_BEGIN: uint32 size + uint8[32] sha256 */ + uint8_t begin_payload[36]; + begin_payload[0] = (uint8_t)(fw_size >> 24u); + begin_payload[1] = (uint8_t)(fw_size >> 16u); + begin_payload[2] = (uint8_t)(fw_size >> 8u); + begin_payload[3] = (uint8_t)(fw_size); + memcpy(&begin_payload[4], digest, 32); + + for (int retry = 0; retry < OTA_UART_MAX_RETRIES; retry++) { + send_frame(UART_OTA_BEGIN, 0, begin_payload, 36); + if (wait_ack(0)) goto send_data; + ESP_LOGW(TAG, "BEGIN retry %d", retry); + } + ESP_LOGE(TAG, "BEGIN failed"); + free(fw); + g_uart_ota_state = UART_OTA_S_FAILED; + vTaskDelete(NULL); + return; + +send_data: { + uint32_t offset = 0; + uint16_t seq = 1; + while (offset < fw_size) { + uint16_t chunk = (uint16_t)((fw_size - offset) < OTA_UART_CHUNK_SIZE + ? (fw_size - offset) : OTA_UART_CHUNK_SIZE); + bool acked = false; + for (int retry = 0; retry < OTA_UART_MAX_RETRIES; retry++) { + send_frame(UART_OTA_DATA, seq, fw + offset, chunk); + if (wait_ack(seq)) { acked = true; break; } + ESP_LOGW(TAG, "DATA seq=%u retry=%d", seq, retry); + } + if (!acked) { + ESP_LOGE(TAG, "DATA seq=%u failed", seq); + send_frame(UART_OTA_ABORT, seq, NULL, 0); + free(fw); + g_uart_ota_state = UART_OTA_S_FAILED; + vTaskDelete(NULL); + return; + } + offset += chunk; + seq++; + /* 50-100% for sending phase */ + g_uart_ota_progress = (uint8_t)(50u + (offset * 50u) / fw_size); + } + + /* Send OTA_END */ + for (int retry = 0; retry < OTA_UART_MAX_RETRIES; retry++) { + send_frame(UART_OTA_END, seq, NULL, 0); + if (wait_ack(seq)) break; + } + } + + free(fw); + g_uart_ota_progress = 100; + g_uart_ota_state = UART_OTA_S_DONE; + ESP_LOGI(TAG, "IO OTA complete — %lu bytes sent", (unsigned long)fw_size); + vTaskDelete(NULL); +} + +bool uart_ota_trigger(void) +{ + if (!g_io_update.available) { + ESP_LOGW(TAG, "no IO update available"); + return false; + } + if (g_uart_ota_state != UART_OTA_S_IDLE) { + ESP_LOGW(TAG, "UART OTA busy (state=%d)", g_uart_ota_state); + return false; + } + /* Init UART1 for OTA */ + uart_config_t ucfg = { + .baud_rate = UART_OTA_BAUD, + .data_bits = UART_DATA_8_BITS, + .parity = UART_PARITY_DISABLE, + .stop_bits = UART_STOP_BITS_1, + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, + }; + uart_param_config(UART_OTA_PORT, &ucfg); + uart_set_pin(UART_OTA_PORT, UART_OTA_TX_GPIO, UART_OTA_RX_GPIO, + UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); + uart_driver_install(UART_OTA_PORT, 2048, 0, 0, NULL, 0); + + xTaskCreate(uart_ota_task, "uart_ota", 16384, NULL, 4, NULL); + return true; +} diff --git a/esp32s3/balance/main/uart_ota.h b/esp32s3/balance/main/uart_ota.h new file mode 100644 index 0000000..58dda86 --- /dev/null +++ b/esp32s3/balance/main/uart_ota.h @@ -0,0 +1,64 @@ +#pragma once +/* uart_ota.h — UART OTA protocol for Balance→IO firmware update (bd-21hv) + * + * Balance downloads io-firmware.bin from Gitea, then streams it to the IO + * board over UART1 (GPIO17/18, 460800 baud) in 1 KB chunks with ACK. + * + * Protocol frame format (both directions): + * [TYPE:1][SEQ:2 BE][LEN:2 BE][PAYLOAD:LEN][CRC8:1] + * CRC8-SMBUS over TYPE+SEQ+LEN+PAYLOAD. + * + * Balance→IO: + * OTA_BEGIN (0xC0) payload: uint32 total_size BE + uint8[32] sha256 + * OTA_DATA (0xC1) payload: uint8[] chunk (up to 1024 bytes) + * OTA_END (0xC2) no payload + * OTA_ABORT (0xC3) no payload + * + * IO→Balance: + * OTA_ACK (0xC4) payload: uint16 acked_seq BE + * OTA_NACK (0xC5) payload: uint16 failed_seq BE + uint8 err_code + * OTA_STATUS (0xC6) payload: uint8 state + uint8 progress% + */ + +#include +#include + +/* UART for Balance→IO OTA */ +#include "driver/uart.h" +#define UART_OTA_PORT UART_NUM_1 +#define UART_OTA_BAUD 460800 +#define UART_OTA_TX_GPIO 17 +#define UART_OTA_RX_GPIO 18 + +#define OTA_UART_CHUNK_SIZE 1024 +#define OTA_UART_ACK_TIMEOUT_MS 3000 +#define OTA_UART_MAX_RETRIES 3 + +/* Frame type bytes */ +#define UART_OTA_BEGIN 0xC0u +#define UART_OTA_DATA 0xC1u +#define UART_OTA_END 0xC2u +#define UART_OTA_ABORT 0xC3u +#define UART_OTA_ACK 0xC4u +#define UART_OTA_NACK 0xC5u +#define UART_OTA_STATUS 0xC6u + +/* NACK error codes */ +#define OTA_ERR_BAD_CRC 0x01u +#define OTA_ERR_WRITE 0x02u +#define OTA_ERR_SIZE 0x03u + +typedef enum { + UART_OTA_S_IDLE = 0, + UART_OTA_S_DOWNLOADING, /* downloading from Gitea */ + UART_OTA_S_SENDING, /* sending to IO board */ + UART_OTA_S_DONE, + UART_OTA_S_FAILED, +} uart_ota_send_state_t; + +extern volatile uart_ota_send_state_t g_uart_ota_state; +extern volatile uint8_t g_uart_ota_progress; + +/* Trigger IO firmware update. Uses g_io_update (from gitea_ota). + * Downloads bin, then streams via UART. Returns false if busy or no update. */ +bool uart_ota_trigger(void); diff --git a/esp32s3/io/main/CMakeLists.txt b/esp32s3/io/main/CMakeLists.txt new file mode 100644 index 0000000..46381b1 --- /dev/null +++ b/esp32s3/io/main/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register( + SRCS "main.c" "uart_ota_recv.c" + INCLUDE_DIRS "." +) diff --git a/esp32s3/io/main/main.c b/esp32s3/io/main/main.c new file mode 100644 index 0000000..05864d9 --- /dev/null +++ b/esp32s3/io/main/main.c @@ -0,0 +1,42 @@ +/* main.c — ESP32-S3 IO board app_main */ + +#include "uart_ota_recv.h" +#include "config.h" +#include "esp_log.h" +#include "esp_ota_ops.h" +#include "driver/uart.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +static const char *TAG = "io_main"; + +static void uart_init(void) +{ + uart_config_t cfg = { + .baud_rate = IO_UART_BAUD, + .data_bits = UART_DATA_8_BITS, + .parity = UART_PARITY_DISABLE, + .stop_bits = UART_STOP_BITS_1, + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, + }; + uart_param_config(IO_UART_PORT, &cfg); + uart_set_pin(IO_UART_PORT, IO_UART_TX_GPIO, IO_UART_RX_GPIO, + UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); + uart_driver_install(IO_UART_PORT, 4096, 0, 0, NULL, 0); +} + +void app_main(void) +{ + ESP_LOGI(TAG, "ESP32-S3 IO v%s starting", IO_FW_VERSION); + + /* Mark running image valid (OTA rollback support) */ + esp_ota_mark_app_valid_cancel_rollback(); + + uart_init(); + uart_ota_recv_init(); + + /* IO board main loop placeholder — RC/motor/sensor tasks added in later beads */ + while (1) { + vTaskDelay(pdMS_TO_TICKS(1000)); + } +} diff --git a/esp32s3/io/main/uart_ota_recv.c b/esp32s3/io/main/uart_ota_recv.c new file mode 100644 index 0000000..7a6f297 --- /dev/null +++ b/esp32s3/io/main/uart_ota_recv.c @@ -0,0 +1,210 @@ +/* uart_ota_recv.c — IO board OTA receiver (bd-21hv) + * + * Listens on UART0 for OTA frames from Balance board. + * Writes incoming chunks to the inactive OTA partition, verifies SHA256, + * then reboots into new firmware. + */ + +#include "uart_ota_recv.h" +#include "config.h" +#include "esp_log.h" +#include "esp_ota_ops.h" +#include "driver/uart.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "mbedtls/sha256.h" +#include + +static const char *TAG = "io_ota"; + +volatile io_ota_state_t g_io_ota_state = IO_OTA_IDLE; +volatile uint8_t g_io_ota_progress = 0; + +/* Frame type bytes (same as uart_ota.h sender side) */ +#define OTA_BEGIN 0xC0u +#define OTA_DATA 0xC1u +#define OTA_END 0xC2u +#define OTA_ABORT 0xC3u +#define OTA_ACK 0xC4u +#define OTA_NACK 0xC5u + +#define CHUNK_MAX 1024 + +static uint8_t crc8(const uint8_t *d, uint16_t len) +{ + uint8_t crc = 0; + for (uint16_t i = 0; i < len; i++) { + crc ^= d[i]; + for (uint8_t b = 0; b < 8; b++) + crc = (crc & 0x80u) ? (uint8_t)((crc << 1u) ^ 0x07u) : (uint8_t)(crc << 1u); + } + return crc; +} + +static void send_ack(uint16_t seq) +{ + uint8_t frame[6]; + frame[0] = OTA_ACK; + frame[1] = (uint8_t)(seq >> 8u); + frame[2] = (uint8_t)(seq); + frame[3] = 0; frame[4] = 0; /* LEN=0 */ + uint8_t crc = crc8(frame, 5); + frame[5] = crc; + uart_write_bytes(IO_UART_PORT, (char *)frame, 6); +} + +static void send_nack(uint16_t seq, uint8_t err) +{ + uint8_t frame[8]; + frame[0] = OTA_NACK; + frame[1] = (uint8_t)(seq >> 8u); + frame[2] = (uint8_t)(seq); + frame[3] = 0; frame[4] = 1; /* LEN=1 */ + frame[5] = err; + uint8_t crc = crc8(frame, 6); + frame[6] = crc; + uart_write_bytes(IO_UART_PORT, (char *)frame, 7); +} + +/* Read exact n bytes with timeout */ +static bool uart_read_exact(uint8_t *buf, int n, int timeout_ms) +{ + int got = 0; + while (got < n && timeout_ms > 0) { + int r = uart_read_bytes(IO_UART_PORT, buf + got, n - got, + pdMS_TO_TICKS(50)); + if (r > 0) got += r; + else timeout_ms -= 50; + } + return got == n; +} + +static void ota_recv_task(void *arg) +{ + esp_ota_handle_t handle = 0; + const esp_partition_t *ota_part = esp_ota_get_next_update_partition(NULL); + mbedtls_sha256_context sha; + mbedtls_sha256_init(&sha); + uint32_t expected_size = 0; + uint8_t expected_digest[32] = {0}; + uint32_t received = 0; + bool ota_started = false; + static uint8_t payload[CHUNK_MAX]; + + for (;;) { + /* Read frame header: TYPE(1) + SEQ(2) + LEN(2) = 5 bytes */ + uint8_t hdr[5]; + if (!uart_read_exact(hdr, 5, 5000)) continue; + + uint8_t type = hdr[0]; + uint16_t seq = (uint16_t)((hdr[1] << 8u) | hdr[2]); + uint16_t plen = (uint16_t)((hdr[3] << 8u) | hdr[4]); + + if (plen > CHUNK_MAX + 36) { + ESP_LOGW(TAG, "oversized frame plen=%u", plen); + continue; + } + + /* Read payload + CRC */ + if (plen > 0 && !uart_read_exact(payload, plen, 2000)) continue; + uint8_t crc_rx; + if (!uart_read_exact(&crc_rx, 1, 500)) continue; + + /* Verify CRC over hdr+payload */ + uint8_t crc_buf[5 + CHUNK_MAX + 36]; + memcpy(crc_buf, hdr, 5); + if (plen > 0) memcpy(crc_buf + 5, payload, plen); + uint8_t expected_crc = crc8(crc_buf, (uint16_t)(5 + plen)); + if (crc_rx != expected_crc) { + ESP_LOGW(TAG, "CRC fail seq=%u", seq); + send_nack(seq, 0x01u); /* OTA_ERR_BAD_CRC */ + continue; + } + + switch (type) { + case OTA_BEGIN: + if (plen < 36) { send_nack(seq, 0x03u); break; } + expected_size = ((uint32_t)payload[0] << 24u) | + ((uint32_t)payload[1] << 16u) | + ((uint32_t)payload[2] << 8u) | + (uint32_t)payload[3]; + memcpy(expected_digest, &payload[4], 32); + + if (!ota_part || esp_ota_begin(ota_part, OTA_WITH_SEQUENTIAL_WRITES, + &handle) != ESP_OK) { + send_nack(seq, 0x02u); + break; + } + mbedtls_sha256_starts(&sha, 0); + received = 0; + ota_started = true; + g_io_ota_state = IO_OTA_RECEIVING; + g_io_ota_progress = 0; + ESP_LOGI(TAG, "OTA begin: %lu bytes", (unsigned long)expected_size); + send_ack(seq); + break; + + case OTA_DATA: + if (!ota_started) { send_nack(seq, 0x02u); break; } + if (esp_ota_write(handle, payload, plen) != ESP_OK) { + send_nack(seq, 0x02u); + esp_ota_abort(handle); + ota_started = false; + g_io_ota_state = IO_OTA_FAILED; + break; + } + mbedtls_sha256_update(&sha, payload, plen); + received += plen; + if (expected_size > 0) + g_io_ota_progress = (uint8_t)((received * 100u) / expected_size); + send_ack(seq); + break; + + case OTA_END: { + if (!ota_started) { send_nack(seq, 0x02u); break; } + g_io_ota_state = IO_OTA_VERIFYING; + + uint8_t digest[32]; + mbedtls_sha256_finish(&sha, digest); + if (memcmp(digest, expected_digest, 32) != 0) { + ESP_LOGE(TAG, "SHA256 mismatch"); + esp_ota_abort(handle); + send_nack(seq, 0x01u); + g_io_ota_state = IO_OTA_FAILED; + break; + } + + if (esp_ota_end(handle) != ESP_OK || + esp_ota_set_boot_partition(ota_part) != ESP_OK) { + send_nack(seq, 0x02u); + g_io_ota_state = IO_OTA_FAILED; + break; + } + + g_io_ota_state = IO_OTA_REBOOTING; + g_io_ota_progress = 100; + ESP_LOGI(TAG, "OTA done — rebooting"); + send_ack(seq); + vTaskDelay(pdMS_TO_TICKS(500)); + esp_restart(); + break; + } + + case OTA_ABORT: + if (ota_started) { esp_ota_abort(handle); ota_started = false; } + g_io_ota_state = IO_OTA_IDLE; + ESP_LOGW(TAG, "OTA aborted"); + break; + + default: + break; + } + } +} + +void uart_ota_recv_init(void) +{ + /* UART0 already initialized for inter-board comms; just create the task */ + xTaskCreate(ota_recv_task, "io_ota_recv", 8192, NULL, 6, NULL); + ESP_LOGI(TAG, "OTA receiver task started"); +} diff --git a/esp32s3/io/main/uart_ota_recv.h b/esp32s3/io/main/uart_ota_recv.h new file mode 100644 index 0000000..d7cba70 --- /dev/null +++ b/esp32s3/io/main/uart_ota_recv.h @@ -0,0 +1,20 @@ +#pragma once +/* uart_ota_recv.h — IO board: receives OTA firmware from Balance (bd-21hv) */ + +#include +#include + +typedef enum { + IO_OTA_IDLE = 0, + IO_OTA_RECEIVING, + IO_OTA_VERIFYING, + IO_OTA_APPLYING, + IO_OTA_REBOOTING, + IO_OTA_FAILED, +} io_ota_state_t; + +extern volatile io_ota_state_t g_io_ota_state; +extern volatile uint8_t g_io_ota_progress; + +/* Start listening for OTA frames on UART0 */ +void uart_ota_recv_init(void);