feat: ESP32-S3 OTA stack — partitions, Gitea checker, self-update, UART IO, display, Orin serial trigger (6 beads) #731
183
esp32s3/balance/main/ota_self.c
Normal file
183
esp32s3/balance/main/ota_self.c
Normal file
@ -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 <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
34
esp32s3/balance/main/ota_self.h
Normal file
34
esp32s3/balance/main/ota_self.h
Normal file
@ -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 <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#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);
|
||||
Loading…
x
Reference in New Issue
Block a user