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 <noreply@anthropic.com>
184 lines
5.5 KiB
C
184 lines
5.5 KiB
C
/* 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));
|
|
}
|
|
}
|