Adds gitea_ota_check_task on Balance board: fetches Gitea releases API every 30 min and on boot, filters by esp32-balance/ and esp32-io/ tag prefixes, compares semver against embedded FW version, stores update info (version string, download URL, SHA256) in g_balance_update / g_io_update. WiFi credentials read from NVS namespace "wifi"; falls back to compile-time DEFAULT_WIFI_SSID/PASS if NVS is empty. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
286 lines
8.9 KiB
C
286 lines
8.9 KiB
C
/* 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 <string.h>
|
|
#include <stdio.h>
|
|
|
|
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");
|
|
}
|