/* gitea_ota.c — Gitea version checker (bd-3hte) * * Uses esp_http_client + cJSON to query: * GET /api/v1/repos/{repo}/releases?limit=10 * Filters releases by tag prefix, extracts version and download URLs. */ #include "gitea_ota.h" #include "version.h" #include "esp_log.h" #include "esp_wifi.h" #include "esp_event.h" #include "esp_netif.h" #include "esp_http_client.h" #include "nvs_flash.h" #include "nvs.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/event_groups.h" #include "cJSON.h" #include #include static const char *TAG = "gitea_ota"; ota_update_info_t g_balance_update = {0}; ota_update_info_t g_io_update = {0}; /* ── WiFi connection ── */ #define WIFI_CONNECTED_BIT BIT0 #define WIFI_FAIL_BIT BIT1 #define WIFI_MAX_RETRIES 5 /* Compile-time WiFi fallback (override in NVS "wifi"/"ssid","pass") */ #define DEFAULT_WIFI_SSID "saltylab" #define DEFAULT_WIFI_PASS "" static EventGroupHandle_t s_wifi_eg; static int s_wifi_retries = 0; static void wifi_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data) { if (base == WIFI_EVENT && id == WIFI_EVENT_STA_START) { esp_wifi_connect(); } else if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) { if (s_wifi_retries < WIFI_MAX_RETRIES) { esp_wifi_connect(); s_wifi_retries++; } else { xEventGroupSetBits(s_wifi_eg, WIFI_FAIL_BIT); } } else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) { s_wifi_retries = 0; xEventGroupSetBits(s_wifi_eg, WIFI_CONNECTED_BIT); } } static bool wifi_connect(void) { char ssid[64] = DEFAULT_WIFI_SSID; char pass[64] = DEFAULT_WIFI_PASS; /* Try to read credentials from NVS */ nvs_handle_t nvs; if (nvs_open("wifi", NVS_READONLY, &nvs) == ESP_OK) { size_t sz = sizeof(ssid); nvs_get_str(nvs, "ssid", ssid, &sz); sz = sizeof(pass); nvs_get_str(nvs, "pass", pass, &sz); nvs_close(nvs); } s_wifi_eg = xEventGroupCreate(); s_wifi_retries = 0; ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_event_loop_create_default()); esp_netif_create_default_wifi_sta(); wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_wifi_init(&cfg)); esp_event_handler_instance_t h1, h2; ESP_ERROR_CHECK(esp_event_handler_instance_register( WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_event_handler, NULL, &h1)); ESP_ERROR_CHECK(esp_event_handler_instance_register( IP_EVENT, IP_EVENT_STA_GOT_IP, wifi_event_handler, NULL, &h2)); wifi_config_t wcfg = {0}; strlcpy((char *)wcfg.sta.ssid, ssid, sizeof(wcfg.sta.ssid)); strlcpy((char *)wcfg.sta.password, pass, sizeof(wcfg.sta.password)); ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wcfg)); ESP_ERROR_CHECK(esp_wifi_start()); EventBits_t bits = xEventGroupWaitBits(s_wifi_eg, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdFALSE, pdFALSE, pdMS_TO_TICKS(15000)); esp_event_handler_instance_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, h2); esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, h1); vEventGroupDelete(s_wifi_eg); if (bits & WIFI_CONNECTED_BIT) { ESP_LOGI(TAG, "WiFi connected SSID=%s", ssid); return true; } ESP_LOGW(TAG, "WiFi connect failed SSID=%s", ssid); return false; } /* ── HTTP fetch into a heap buffer ── */ #define HTTP_RESP_MAX (8 * 1024) typedef struct { char *buf; int len; int cap; } http_buf_t; static esp_err_t http_event_cb(esp_http_client_event_t *evt) { http_buf_t *b = (http_buf_t *)evt->user_data; if (evt->event_id == HTTP_EVENT_ON_DATA && b) { if (b->len + evt->data_len < b->cap) { memcpy(b->buf + b->len, evt->data, evt->data_len); b->len += evt->data_len; } } return ESP_OK; } static char *http_get(const char *url) { char *buf = malloc(HTTP_RESP_MAX); if (!buf) return NULL; http_buf_t b = {.buf = buf, .len = 0, .cap = HTTP_RESP_MAX}; buf[0] = '\0'; esp_http_client_config_t cfg = { .url = url, .event_handler = http_event_cb, .user_data = &b, .timeout_ms = GITEA_API_TIMEOUT_MS, .skip_cert_common_name_check = true, }; esp_http_client_handle_t client = esp_http_client_init(&cfg); esp_err_t err = esp_http_client_perform(client); int status = esp_http_client_get_status_code(client); esp_http_client_cleanup(client); if (err != ESP_OK || status != 200) { ESP_LOGW(TAG, "HTTP GET %s → err=%d status=%d", url, err, status); free(buf); return NULL; } buf[b.len] = '\0'; return buf; } /* ── Version comparison: returns true if remote > local ── */ static bool version_newer(const char *local, const char *remote) { int la=0,lb=0,lc=0, ra=0,rb=0,rc=0; sscanf(local, "%d.%d.%d", &la, &lb, &lc); sscanf(remote, "%d.%d.%d", &ra, &rb, &rc); if (ra != la) return ra > la; if (rb != lb) return rb > lb; return rc > lc; } /* ── Parse releases JSON array, fill ota_update_info_t ── */ static void parse_releases(const char *json, const char *tag_prefix, const char *bin_asset, const char *sha_asset, const char *local_version, ota_update_info_t *out) { cJSON *arr = cJSON_Parse(json); if (!arr || !cJSON_IsArray(arr)) { ESP_LOGW(TAG, "JSON parse failed"); cJSON_Delete(arr); return; } cJSON *rel; cJSON_ArrayForEach(rel, arr) { cJSON *tag_j = cJSON_GetObjectItem(rel, "tag_name"); if (!cJSON_IsString(tag_j)) continue; const char *tag = tag_j->valuestring; if (strncmp(tag, tag_prefix, strlen(tag_prefix)) != 0) continue; /* Extract version after prefix */ const char *ver = tag + strlen(tag_prefix); if (*ver == 'v') ver++; /* strip leading 'v' */ if (!version_newer(local_version, ver)) continue; /* Found a newer release — extract asset URLs */ cJSON *assets = cJSON_GetObjectItem(rel, "assets"); if (!cJSON_IsArray(assets)) continue; out->available = false; out->download_url[0] = '\0'; out->sha256[0] = '\0'; strlcpy(out->version, ver, sizeof(out->version)); cJSON *asset; cJSON_ArrayForEach(asset, assets) { cJSON *name_j = cJSON_GetObjectItem(asset, "name"); cJSON *url_j = cJSON_GetObjectItem(asset, "browser_download_url"); if (!cJSON_IsString(name_j) || !cJSON_IsString(url_j)) continue; if (strcmp(name_j->valuestring, bin_asset) == 0) { strlcpy(out->download_url, url_j->valuestring, sizeof(out->download_url)); out->available = true; } else if (strcmp(name_j->valuestring, sha_asset) == 0) { /* Download the SHA256 asset inline */ char *sha = http_get(url_j->valuestring); if (sha) { /* sha file is just hex+newline */ size_t n = strspn(sha, "0123456789abcdefABCDEF"); if (n == 64) { memcpy(out->sha256, sha, 64); out->sha256[64] = '\0'; } free(sha); } } } if (out->available) { ESP_LOGI(TAG, "update: tag=%s ver=%s", tag, out->version); } break; /* use first matching release */ } cJSON_Delete(arr); } /* ── Main check ── */ void gitea_ota_check_now(void) { char url[512]; snprintf(url, sizeof(url), "%s/api/v1/repos/%s/releases?limit=10", GITEA_BASE_URL, GITEA_REPO); char *json = http_get(url); if (!json) { ESP_LOGW(TAG, "releases fetch failed"); return; } parse_releases(json, BALANCE_TAG_PREFIX, BALANCE_BIN_ASSET, BALANCE_SHA256_ASSET, BALANCE_FW_VERSION, &g_balance_update); parse_releases(json, IO_TAG_PREFIX, IO_BIN_ASSET, IO_SHA256_ASSET, IO_FW_VERSION, &g_io_update); free(json); } /* ── Background task ── */ static void version_check_task(void *arg) { /* Initial check immediately after WiFi up */ vTaskDelay(pdMS_TO_TICKS(2000)); gitea_ota_check_now(); for (;;) { vTaskDelay(pdMS_TO_TICKS(VERSION_CHECK_PERIOD_MS)); gitea_ota_check_now(); } } void gitea_ota_init(void) { ESP_ERROR_CHECK(nvs_flash_init()); if (!wifi_connect()) { ESP_LOGW(TAG, "WiFi unavailable — version checks disabled"); return; } xTaskCreate(version_check_task, "ver_check", 6144, NULL, 3, NULL); ESP_LOGI(TAG, "version check task started"); }