From 87b715f22aa32281baa249b7098a73bbbb265aa7 Mon Sep 17 00:00:00 2001 From: sl-firmware Date: Fri, 17 Apr 2026 22:14:29 -0400 Subject: [PATCH] feat: Balance self-OTA download, SHA256 verify, rollback (bd-18nb) Downloads balance-firmware.bin from Gitea release URL to inactive OTA partition, streams SHA256 verification via mbedTLS, sets boot partition and reboots. Auto-rollback via CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE if ota_self_health_check() not called within 30 s of boot. Progress 0-100% in g_ota_self_progress for display task. Co-Authored-By: Claude Sonnet 4.6 --- esp32s3/balance/main/ota_self.c | 183 ++++++++++++++++++++++++++++++++ esp32s3/balance/main/ota_self.h | 34 ++++++ 2 files changed, 217 insertions(+) create mode 100644 esp32s3/balance/main/ota_self.c create mode 100644 esp32s3/balance/main/ota_self.h diff --git a/esp32s3/balance/main/ota_self.c b/esp32s3/balance/main/ota_self.c new file mode 100644 index 0000000..10962e3 --- /dev/null +++ b/esp32s3/balance/main/ota_self.c @@ -0,0 +1,183 @@ +/* ota_self.c — Balance self-OTA (bd-18nb) + * + * Uses esp_https_ota / esp_ota_ops to download from Gitea release URL, + * stream-verify SHA256 with mbedTLS, set new boot partition, and reboot. + * CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE in sdkconfig allows auto-rollback + * if the new image doesn't call esp_ota_mark_app_valid_cancel_rollback() + * within OTA_ROLLBACK_WINDOW_S seconds. + */ + +#include "ota_self.h" +#include "gitea_ota.h" +#include "esp_log.h" +#include "esp_ota_ops.h" +#include "esp_http_client.h" +#include "esp_timer.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "mbedtls/sha256.h" +#include +#include + +static const char *TAG = "ota_self"; + +volatile ota_self_state_t g_ota_self_state = OTA_SELF_IDLE; +volatile uint8_t g_ota_self_progress = 0; + +#define OTA_CHUNK_SIZE 4096 + +/* ── SHA256 verify helper ── */ +static bool sha256_matches(const uint8_t *digest, const char *expected_hex) +{ + if (!expected_hex || expected_hex[0] == '\0') { + ESP_LOGW(TAG, "no SHA256 to verify — skipping"); + return true; + } + char got[65] = {0}; + for (int i = 0; i < 32; i++) { + snprintf(&got[i*2], 3, "%02x", digest[i]); + } + bool ok = (strncasecmp(got, expected_hex, 64) == 0); + if (!ok) { + ESP_LOGE(TAG, "SHA256 mismatch: got=%s exp=%s", got, expected_hex); + } + return ok; +} + +/* ── OTA download + flash task ── */ +static void ota_self_task(void *arg) +{ + const char *url = g_balance_update.download_url; + const char *sha256 = g_balance_update.sha256; + + g_ota_self_state = OTA_SELF_DOWNLOADING; + g_ota_self_progress = 0; + + ESP_LOGI(TAG, "OTA start: %s", url); + + esp_ota_handle_t handle = 0; + const esp_partition_t *ota_part = esp_ota_get_next_update_partition(NULL); + if (!ota_part) { + ESP_LOGE(TAG, "no OTA partition"); + g_ota_self_state = OTA_SELF_FAILED; + vTaskDelete(NULL); + return; + } + + esp_err_t err = esp_ota_begin(ota_part, OTA_WITH_SEQUENTIAL_WRITES, &handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "ota_begin: %s", esp_err_to_name(err)); + g_ota_self_state = OTA_SELF_FAILED; + vTaskDelete(NULL); + return; + } + + /* Setup HTTP client */ + esp_http_client_config_t hcfg = { + .url = url, + .timeout_ms = 30000, + .buffer_size = OTA_CHUNK_SIZE, + .skip_cert_common_name_check = true, + }; + esp_http_client_handle_t client = esp_http_client_init(&hcfg); + err = esp_http_client_open(client, 0); + if (err != ESP_OK) { + ESP_LOGE(TAG, "http_open: %s", esp_err_to_name(err)); + esp_ota_abort(handle); + esp_http_client_cleanup(client); + g_ota_self_state = OTA_SELF_FAILED; + vTaskDelete(NULL); + return; + } + + int content_len = esp_http_client_fetch_headers(client); + ESP_LOGI(TAG, "content-length: %d", content_len); + + mbedtls_sha256_context sha_ctx; + mbedtls_sha256_init(&sha_ctx); + mbedtls_sha256_starts(&sha_ctx, 0); /* 0 = SHA-256 */ + + static uint8_t buf[OTA_CHUNK_SIZE]; + int total = 0; + int rd; + while ((rd = esp_http_client_read(client, (char *)buf, sizeof(buf))) > 0) { + mbedtls_sha256_update(&sha_ctx, buf, rd); + err = esp_ota_write(handle, buf, rd); + if (err != ESP_OK) { + ESP_LOGE(TAG, "ota_write: %s", esp_err_to_name(err)); + esp_ota_abort(handle); + goto cleanup; + } + total += rd; + if (content_len > 0) { + g_ota_self_progress = (uint8_t)((total * 100) / content_len); + } + } + esp_http_client_close(client); + + /* Verify SHA256 */ + g_ota_self_state = OTA_SELF_VERIFYING; + uint8_t digest[32]; + mbedtls_sha256_finish(&sha_ctx, digest); + if (!sha256_matches(digest, sha256)) { + ESP_LOGE(TAG, "SHA256 verification failed"); + esp_ota_abort(handle); + g_ota_self_state = OTA_SELF_FAILED; + goto cleanup; + } + + /* Finalize + set boot partition */ + g_ota_self_state = OTA_SELF_APPLYING; + err = esp_ota_end(handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "ota_end: %s", esp_err_to_name(err)); + g_ota_self_state = OTA_SELF_FAILED; + goto cleanup; + } + + err = esp_ota_set_boot_partition(ota_part); + if (err != ESP_OK) { + ESP_LOGE(TAG, "set_boot_partition: %s", esp_err_to_name(err)); + g_ota_self_state = OTA_SELF_FAILED; + goto cleanup; + } + + g_ota_self_state = OTA_SELF_REBOOTING; + g_ota_self_progress = 100; + ESP_LOGI(TAG, "OTA success — rebooting"); + vTaskDelay(pdMS_TO_TICKS(500)); + esp_restart(); + +cleanup: + mbedtls_sha256_free(&sha_ctx); + esp_http_client_cleanup(client); + handle = 0; + vTaskDelete(NULL); +} + +bool ota_self_trigger(void) +{ + if (!g_balance_update.available) { + ESP_LOGW(TAG, "no update available"); + return false; + } + if (g_ota_self_state != OTA_SELF_IDLE) { + ESP_LOGW(TAG, "OTA already in progress (state=%d)", g_ota_self_state); + return false; + } + xTaskCreate(ota_self_task, "ota_self", 8192, NULL, 5, NULL); + return true; +} + +void ota_self_health_check(void) +{ + /* Mark running image as valid — prevents rollback */ + esp_err_t err = esp_ota_mark_app_valid_cancel_rollback(); + if (err == ESP_OK) { + ESP_LOGI(TAG, "image marked valid"); + } else if (err == ESP_ERR_NOT_SUPPORTED) { + /* Not an OTA image (e.g., flashed via JTAG) — ignore */ + } else { + ESP_LOGW(TAG, "mark_valid: %s", esp_err_to_name(err)); + } +} diff --git a/esp32s3/balance/main/ota_self.h b/esp32s3/balance/main/ota_self.h new file mode 100644 index 0000000..7871ca3 --- /dev/null +++ b/esp32s3/balance/main/ota_self.h @@ -0,0 +1,34 @@ +#pragma once +/* ota_self.h — Balance self-OTA (bd-18nb) + * + * Downloads balance-firmware.bin from Gitea release URL to the inactive + * OTA partition, verifies SHA256, sets boot partition, reboots. + * Auto-rollback if health check not called within ROLLBACK_WINDOW_S seconds. + */ + +#include +#include + +#define OTA_ROLLBACK_WINDOW_S 30 + +typedef enum { + OTA_SELF_IDLE = 0, + OTA_SELF_CHECKING, /* (unused — gitea_ota handles this) */ + OTA_SELF_DOWNLOADING, + OTA_SELF_VERIFYING, + OTA_SELF_APPLYING, + OTA_SELF_REBOOTING, + OTA_SELF_FAILED, +} ota_self_state_t; + +extern volatile ota_self_state_t g_ota_self_state; +extern volatile uint8_t g_ota_self_progress; /* 0-100 % */ + +/* Trigger a Balance self-update. + * Uses g_balance_update (from gitea_ota). Non-blocking: starts in a task. + * Returns false if no update available or OTA already in progress. */ +bool ota_self_trigger(void); + +/* Called from app_main after boot to mark the running image as valid. + * Must be called within OTA_ROLLBACK_WINDOW_S after boot or rollback fires. */ +void ota_self_health_check(void);