on device wifi
All checks were successful
ESP32 Build & Release / build (push) Successful in 6m41s

This commit is contained in:
2026-09-12 13:09:47 +02:00
parent 80eab1752c
commit d691febfe4
13 changed files with 1340 additions and 224 deletions

View File

@@ -14,26 +14,21 @@
#include "esp_log.h"
#include "esp_system.h"
#include "esp_ota_ops.h"
#include "esp_partition.h"
#include "esp_https_ota.h"
#include "esp_http_client.h"
#include "esp_crt_bundle.h"
#include "esp_app_desc.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "cJSON.h"
#include "ota_manager.h"
static const char *TAG = "ota";
#ifndef CONFIG_MTG_GITEA_URL
#define CONFIG_MTG_GITEA_URL "https://gitea.com"
#endif
#ifndef CONFIG_MTG_GITEA_REPO_OWNER
#define CONFIG_MTG_GITEA_REPO_OWNER "user"
#endif
#ifndef CONFIG_MTG_GITEA_REPO_NAME
#define CONFIG_MTG_GITEA_REPO_NAME "espMTG"
#endif
#define GITEA_RELEASES_PAGE_URL "https://git.rasmusbendtsen.dk/rasmus/MTGcompanion/releases"
#define GITEA_RELEASE_API_URL "https://git.rasmusbendtsen.dk/api/v1/repos/rasmus/MTGcompanion/releases/latest"
#define GITEA_FIRMWARE_FALLBACK_URL "https://git.rasmusbendtsen.dk/rasmus/MTGcompanion/releases/download/latest/mtg-rfid-companion.bin"
#define GITEA_API_BUF_SIZE (8192)
@@ -41,6 +36,8 @@ static SemaphoreHandle_t s_ota_mux = NULL;
static ota_status_t s_status = OTA_STATUS_IDLE;
static char s_message[128] = "Idle";
static ota_release_info_t s_release = {0};
static char s_running_sha256[65] = {0};
static char s_running_elf_sha256[65] = {0};
static size_t s_bytes_read = 0;
static size_t s_total_bytes = 0;
static volatile bool s_cancel_requested = false;
@@ -77,9 +74,20 @@ esp_err_t ota_manager_init(void)
}
}
/* Compute running image SHA-256 */
uint8_t sha_bytes[32];
if (esp_partition_get_sha256(running, sha_bytes) == ESP_OK) {
for (int i = 0; i < 32; i++) {
snprintf(s_running_sha256 + (i * 2), 3, "%02x", sha_bytes[i]);
}
}
/* Compute running ELF SHA-256 */
esp_app_get_elf_sha256(s_running_elf_sha256, sizeof(s_running_elf_sha256));
const esp_app_desc_t *app_desc = esp_app_get_description();
ESP_LOGI(TAG, "Running firmware '%s' version '%s' compiled on %s %s on partition '%s'",
app_desc->project_name, app_desc->version, app_desc->date, app_desc->time,
ESP_LOGI(TAG, "Running firmware: ver='%s', SHA256=%.8s..., ELF=%.8s... on partition '%s'",
app_desc->version, s_running_sha256, s_running_elf_sha256,
running ? running->label : "unknown");
return ESP_OK;
@@ -90,6 +98,11 @@ const char *ota_manager_get_current_version(void)
return esp_app_get_description()->version;
}
const char *ota_manager_get_running_sha256(void)
{
return s_running_sha256;
}
ota_status_t ota_manager_get_status(void)
{
ota_status_t st;
@@ -137,35 +150,44 @@ void ota_manager_reset(void)
}
}
/* Semver parsing: compares "v1.2.3" or "1.2.3" */
static bool parse_semver(const char *ver, int *maj, int *min, int *patch)
static void extract_hash_from_body(const char *body, char *out_sha256, size_t sha_sz, char *out_commit, size_t com_sz)
{
if (!ver) return false;
while (*ver && !isdigit((unsigned char)*ver)) {
ver++;
if (out_sha256 && sha_sz > 0) out_sha256[0] = '\0';
if (out_commit && com_sz > 0) out_commit[0] = '\0';
if (!body) return;
/* Look for "SHA256:" or "Image-SHA256:" or "File-SHA256:" */
const char *p = strstr(body, "SHA256:");
if (!p) p = strstr(body, "sha256:");
if (!p) p = strstr(body, "SHA-256:");
if (p) {
p = strchr(p, ':');
if (p) {
p++;
while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') p++;
size_t idx = 0;
while (isxdigit((unsigned char)*p) && idx < sha_sz - 1) {
out_sha256[idx++] = (char)tolower((unsigned char)*p++);
}
out_sha256[idx] = '\0';
}
}
*maj = 0; *min = 0; *patch = 0;
int n = sscanf(ver, "%d.%d.%d", maj, min, patch);
return (n >= 1);
}
static bool check_is_newer(const char *remote, const char *local)
{
int r_maj = 0, r_min = 0, r_patch = 0;
int l_maj = 0, l_min = 0, l_patch = 0;
bool r_ok = parse_semver(remote, &r_maj, &r_min, &r_patch);
bool l_ok = parse_semver(local, &l_maj, &l_min, &l_patch);
if (r_ok && l_ok) {
if (r_maj > l_maj) return true;
if (r_maj < l_maj) return false;
if (r_min > l_min) return true;
if (r_min < l_min) return false;
return (r_patch > l_patch);
/* Look for "Commit:" */
const char *c = strstr(body, "Commit:");
if (!c) c = strstr(body, "commit:");
if (c) {
c = strchr(c, ':');
if (c) {
c++;
while (*c == ' ' || *c == '\t' || *c == '\r' || *c == '\n') c++;
size_t idx = 0;
while (isxdigit((unsigned char)*c) && idx < com_sz - 1) {
out_commit[idx++] = (char)tolower((unsigned char)*c++);
}
out_commit[idx] = '\0';
}
}
/* Fallback to string comparison */
return (strcmp(remote, local) != 0);
}
typedef struct {
@@ -191,11 +213,7 @@ static void ota_check_task(void *arg)
{
set_state(OTA_STATUS_CHECKING, "Connecting to Gitea...");
char api_url[256];
snprintf(api_url, sizeof(api_url), "%s/api/v1/repos/%s/%s/releases/latest",
CONFIG_MTG_GITEA_URL, CONFIG_MTG_GITEA_REPO_OWNER, CONFIG_MTG_GITEA_REPO_NAME);
ESP_LOGI(TAG, "Querying Gitea release API: %s", api_url);
ESP_LOGI(TAG, "Querying Gitea release API: %s", GITEA_RELEASE_API_URL);
http_rx_buf_t rx = {
.buf = malloc(GITEA_API_BUF_SIZE),
@@ -212,7 +230,7 @@ static void ota_check_task(void *arg)
rx.buf[0] = '\0';
esp_http_client_config_t cfg = {
.url = api_url,
.url = GITEA_RELEASE_API_URL,
.transport_type = HTTP_TRANSPORT_OVER_SSL,
.timeout_ms = 10000,
.event_handler = http_rx_event_handler,
@@ -268,8 +286,21 @@ static void ota_check_task(void *arg)
cJSON *tag_item = cJSON_GetObjectItem(root, "tag_name");
cJSON *title_item = cJSON_GetObjectItem(root, "name");
const char *tag_str = (tag_item && tag_item->valuestring) ? tag_item->valuestring : "unknown";
cJSON *body_item = cJSON_GetObjectItem(root, "body");
cJSON *commit_item = cJSON_GetObjectItem(root, "target_commitish");
const char *tag_str = (tag_item && tag_item->valuestring) ? tag_item->valuestring : "latest";
const char *title_str = (title_item && title_item->valuestring) ? title_item->valuestring : tag_str;
const char *body_str = (body_item && body_item->valuestring) ? body_item->valuestring : "";
const char *target_commit = (commit_item && commit_item->valuestring) ? commit_item->valuestring : "";
char remote_sha256[68] = {0};
char remote_commit[68] = {0};
extract_hash_from_body(body_str, remote_sha256, sizeof(remote_sha256), remote_commit, sizeof(remote_commit));
if (remote_commit[0] == '\0' && strlen(target_commit) >= 7) {
strncpy(remote_commit, target_commit, sizeof(remote_commit) - 1);
}
if (s_ota_mux) xSemaphoreTake(s_ota_mux, portMAX_DELAY);
strncpy(s_release.tag_name, tag_str, sizeof(s_release.tag_name) - 1);
@@ -277,6 +308,21 @@ static void ota_check_task(void *arg)
s_release.download_url[0] = '\0';
s_release.binary_size = 0;
/* Preferred remote hash: SHA256 if found, else Commit hash */
if (strlen(remote_sha256) > 0) {
strncpy(s_release.remote_hash, remote_sha256, sizeof(s_release.remote_hash) - 1);
} else if (strlen(remote_commit) > 0) {
strncpy(s_release.remote_hash, remote_commit, sizeof(s_release.remote_hash) - 1);
} else {
strncpy(s_release.remote_hash, tag_str, sizeof(s_release.remote_hash) - 1);
}
if (strlen(s_running_sha256) > 0) {
strncpy(s_release.local_hash, s_running_sha256, sizeof(s_release.local_hash) - 1);
} else {
strncpy(s_release.local_hash, esp_app_get_description()->version, sizeof(s_release.local_hash) - 1);
}
/* Search assets for mtg-rfid-companion.bin or *.bin */
cJSON *assets = cJSON_GetObjectItem(root, "assets");
if (assets && cJSON_IsArray(assets)) {
@@ -290,16 +336,11 @@ static void ota_check_task(void *arg)
if (aname && aname->valuestring && aurl && aurl->valuestring) {
if (strstr(aname->valuestring, ".bin") != NULL) {
const char *url_str = aurl->valuestring;
if (url_str[0] == '/') {
snprintf(s_release.download_url, sizeof(s_release.download_url),
"%s%s", CONFIG_MTG_GITEA_URL, url_str);
} else {
strncpy(s_release.download_url, url_str, sizeof(s_release.download_url) - 1);
}
strncpy(s_release.download_url, url_str, sizeof(s_release.download_url) - 1);
if (asize && asize->valuedouble > 0) {
s_release.binary_size = (size_t)asize->valuedouble;
}
if (strstr(aname->valuestring, "mtg-rfid-companion") != NULL) {
if (strcmp(aname->valuestring, "mtg-rfid-companion.bin") == 0) {
break; /* Exact match, stop looking */
}
}
@@ -309,29 +350,58 @@ static void ota_check_task(void *arg)
/* Fallback URL if not in assets list */
if (s_release.download_url[0] == '\0') {
snprintf(s_release.download_url, sizeof(s_release.download_url),
"%s/%s/%s/releases/download/%s/mtg-rfid-companion.bin",
CONFIG_MTG_GITEA_URL, CONFIG_MTG_GITEA_REPO_OWNER, CONFIG_MTG_GITEA_REPO_NAME,
s_release.tag_name);
strncpy(s_release.download_url, GITEA_FIRMWARE_FALLBACK_URL, sizeof(s_release.download_url) - 1);
}
const char *current_ver = ota_manager_get_current_version();
s_release.is_newer = check_is_newer(s_release.tag_name, current_ver);
/* Verify if an update is available by checking hashes */
bool is_up_to_date = false;
const esp_app_desc_t *running_app = esp_app_get_description();
/* 1. Match by SHA-256 (image hash) */
if (strlen(remote_sha256) > 0 && strlen(s_running_sha256) > 0) {
if (strcasecmp(remote_sha256, s_running_sha256) == 0) {
is_up_to_date = true;
}
}
/* 2. Match by commit hash prefix */
if (!is_up_to_date && strlen(remote_commit) >= 7) {
if (strncasecmp(remote_commit, running_app->version, 7) == 0 ||
strncasecmp(running_app->version, remote_commit, 7) == 0) {
is_up_to_date = true;
}
}
/* 3. Match by NVS last installed hash */
nvs_handle_t nvs_h;
if (!is_up_to_date && nvs_open("ota_store", NVS_READONLY, &nvs_h) == ESP_OK) {
char nvs_val[68] = {0};
size_t nvs_sz = sizeof(nvs_val);
if (nvs_get_str(nvs_h, "last_hash", nvs_val, &nvs_sz) == ESP_OK) {
if (strlen(remote_sha256) > 0 && strcasecmp(nvs_val, remote_sha256) == 0) {
is_up_to_date = true;
} else if (strlen(remote_commit) > 0 && strncasecmp(nvs_val, remote_commit, 7) == 0) {
is_up_to_date = true;
}
}
nvs_close(nvs_h);
}
s_release.is_newer = !is_up_to_date;
if (s_ota_mux) xSemaphoreGive(s_ota_mux);
ESP_LOGI(TAG, "Discovered release '%s' (%s), download: %s, is_newer: %d (current: %s)",
s_release.tag_name, s_release.release_title, s_release.download_url,
s_release.is_newer, current_ver);
ESP_LOGI(TAG, "Gitea check: remote_hash='%.8s' (SHA256: '%.8s', Commit: '%.8s'), local_sha256='%.8s', ver='%s' => is_newer=%d",
s_release.remote_hash, remote_sha256, remote_commit, s_running_sha256, running_app->version, s_release.is_newer);
cJSON_Delete(root);
if (s_release.is_newer) {
char msg[128];
snprintf(msg, sizeof(msg), "New firmware %s available!", s_release.tag_name);
snprintf(msg, sizeof(msg), "Update available (hash: %.8s)", s_release.remote_hash);
set_state(OTA_STATUS_UPDATE_AVAILABLE, msg);
} else {
char msg[128];
snprintf(msg, sizeof(msg), "Firmware %s is up to date", current_ver);
snprintf(msg, sizeof(msg), "Firmware %.8s is up to date", s_running_sha256[0] ? s_running_sha256 : running_app->version);
set_state(OTA_STATUS_UP_TO_DATE, msg);
}
@@ -371,6 +441,20 @@ static void ota_download_task(void *arg)
return;
}
/* Pre-check: verify incoming image ELF SHA-256 against currently running */
esp_app_desc_t new_app_info;
if (esp_https_ota_get_img_desc(ota_handle, &new_app_info) == ESP_OK) {
const esp_app_desc_t *running = esp_app_get_description();
if (memcmp(new_app_info.app_elf_sha256, running->app_elf_sha256, sizeof(new_app_info.app_elf_sha256)) == 0) {
ESP_LOGW(TAG, "Incoming image has identical ELF SHA-256 as running app - aborting flash write");
esp_https_ota_abort(ota_handle);
set_state(OTA_STATUS_UP_TO_DATE, "Firmware is already up to date");
s_ota_task_handle = NULL;
vTaskDelete(NULL);
return;
}
}
while (1) {
if (s_cancel_requested) {
ESP_LOGW(TAG, "OTA canceled by user");
@@ -398,6 +482,16 @@ static void ota_download_task(void *arg)
if (esp_https_ota_is_complete_data_received(ota_handle)) {
esp_err_t finish_err = esp_https_ota_finish(ota_handle);
if (finish_err == ESP_OK) {
/* Store installed release hash in NVS */
nvs_handle_t nvs_h;
if (nvs_open("ota_store", NVS_READWRITE, &nvs_h) == ESP_OK) {
if (s_release.remote_hash[0] != '\0') {
nvs_set_str(nvs_h, "last_hash", s_release.remote_hash);
nvs_commit(nvs_h);
}
nvs_close(nvs_h);
}
ESP_LOGI(TAG, "OTA upgrade successful! Rebooting in 2 seconds...");
set_state(OTA_STATUS_SUCCESS_REBOOTING, "Update complete! Rebooting...");
vTaskDelay(pdMS_TO_TICKS(2000));