/* 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)); } }