#include "ota_update.h" #include "config.h" #include #include #include #include #include #include #include #include namespace OtaUpdate { namespace { // Gitea API URL constructed from config constants at runtime String releasesApiUrl() { return String("https://") + Config::GITEA_API_HOST + Config::GITEA_API_PATH; } bool parseVersion(const char* s, int& major, int& minor, int& patch) { major = minor = patch = 0; if (!s || !s[0]) return false; if (s[0] == 'v' || s[0] == 'V') s++; return sscanf(s, "%d.%d.%d", &major, &minor, &patch) == 3; } // Numeric version comparison (not string-based, so e.g. "3.10.0" > "3.9.0"). int compareVersions(const char* a, const char* b) { int aMaj, aMin, aPat, bMaj, bMin, bPat; if (!parseVersion(a, aMaj, aMin, aPat) || !parseVersion(b, bMaj, bMin, bPat)) return 0; if (aMaj != bMaj) return aMaj - bMaj; if (aMin != bMin) return aMin - bMin; return aPat - bPat; } void addAuthHeader(HTTPClient& http) { http.addHeader("User-Agent", "EiswolfsFlightradarCYD-OTA/1.0"); if (Config::GITEA_API_TOKEN[0]) { String auth = "token "; auth += Config::GITEA_API_TOKEN; http.addHeader("Authorization", auth); } } } CheckInfo checkForUpdate() { CheckInfo info; String url = releasesApiUrl(); Serial.printf("[OTA] Checking for update: url=%s freeHeap=%u RSSI=%ddBm\n", url.c_str(), (unsigned)ESP.getFreeHeap(), WiFi.RSSI()); WiFiClientSecure client; client.setInsecure(); client.setTimeout(8000); HTTPClient http; http.setTimeout(8000); if (!http.begin(client, url)) { Serial.println("[OTA] Check failed: http.begin() returned false."); return info; } addAuthHeader(http); int code = http.GET(); if (code != HTTP_CODE_OK) { if (code < 0) { Serial.printf("[OTA] Check failed: HTTP error=%d (%s)\n", code, HTTPClient::errorToString(code).c_str()); } else { Serial.printf("[OTA] Check failed: HTTP status=%d\n", code); } http.end(); return info; } String body = http.getString(); http.end(); JsonDocument doc; DeserializationError jsonErr = deserializeJson(doc, body); if (jsonErr) { Serial.printf("[OTA] Check failed: JSON error (%s)\n", jsonErr.c_str()); return info; } const char* tag = doc["tag_name"] | ""; if (!tag[0]) { Serial.println("[OTA] Check failed: no tag_name in release JSON."); return info; } strncpy(info.latestVersion, (tag[0] == 'v' || tag[0] == 'V') ? tag + 1 : tag, sizeof(info.latestVersion) - 1); // Construct download URL from the tag name using Gitea's standard // release asset pattern. browser_download_url from the API contains // the internal IP (http://192.168.0.37:3000/...) which the ESP32 // cannot use — it needs the external proxy URL. String dlUrl = String("https://git.rasmusbendtsen.dk/rasmus/flyradar/releases/download/") + String(tag) + "/firmware.bin"; strncpy(info.downloadUrl, dlUrl.c_str(), sizeof(info.downloadUrl) - 1); Serial.printf("[OTA] Download URL: %s\n", info.downloadUrl); int cmp = compareVersions(info.latestVersion, Config::APP_VERSION); info.result = (cmp > 0) ? CheckResult::UpdateAvailable : CheckResult::UpToDate; Serial.printf("[OTA] Check successful: installed=v%s latest=v%s -> %s\n", Config::APP_VERSION, info.latestVersion, info.result == CheckResult::UpdateAvailable ? "Update available" : "already up-to-date"); return info; } bool performUpdate(const char* url, void (*onProgress)(uint8_t percent)) { Serial.printf("[OTA] Starting: url=%s freeHeap=%u RSSI=%ddBm\n", url, (unsigned)ESP.getFreeHeap(), WiFi.RSSI()); WiFiClientSecure client; client.setInsecure(); client.setTimeout(15000); // Use HTTPClient overload so we can add the Authorization header // (required for private repos even during download). HTTPClient http; if (!http.begin(client, url)) { Serial.println("[OTA] Failed: http.begin() returned false for download URL."); return false; } addAuthHeader(http); httpUpdate.rebootOnUpdate(false); httpUpdate.onProgress([onProgress](int cur, int total) { if (onProgress && total > 0) { onProgress((uint8_t)((cur * 100) / total)); } static int8_t lastLoggedPercent = -1; if (total > 0) { int8_t pct = (int8_t)((cur * 100) / total); if (pct != lastLoggedPercent && pct % 10 == 0) { lastLoggedPercent = pct; Serial.printf("[OTA] Progress: %d%% (%d/%d bytes) freeHeap=%u\n", pct, cur, total, (unsigned)ESP.getFreeHeap()); } } }); t_httpUpdate_return result = httpUpdate.update(http); http.end(); if (result != HTTP_UPDATE_OK) { Serial.printf("[OTA] Failed: result=%d error=%d (%s) freeHeap=%u\n", (int)result, httpUpdate.getLastError(), httpUpdate.getLastErrorString().c_str(), (unsigned)ESP.getFreeHeap()); } else { Serial.println("[OTA] Successfully downloaded and flashed."); } return result == HTTP_UPDATE_OK; } }