#include "ota_update.h" #include "config.h" #include #include #include #include #include #include #include #include namespace OtaUpdate { namespace { constexpr const char* RELEASES_API_URL = "https://git.rasmusbendtsen.dk/api/v1/repos/rasmus/flyradar/releases/latest"; // Gitea API uses Authorization: token header instead of // User-Agent. The token is set in Config::GITEA_API_TOKEN. // Gitea serves downloads directly (no redirect), so // setFollowRedirects is not needed. 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; } // > 0 if a is newer than b, 0 if equal, < 0 if a is older than b. // Deliberately a real numeric version comparison instead of a simple // string comparison (which would e.g. wrongly rank "3.10.0" as "less" // than "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; } } CheckInfo checkForUpdate() { CheckInfo info; Serial.printf("[OTA] Checking for update: url=%s freeHeap=%u RSSI=%ddBm\n", RELEASES_API_URL, (unsigned)ESP.getFreeHeap(), WiFi.RSSI()); WiFiClientSecure client; client.setInsecure(); client.setTimeout(8000); HTTPClient http; http.setTimeout(8000); if (!http.begin(client, RELEASES_API_URL)) { Serial.println("[OTA] Check failed: http.begin() returned false."); return info; } 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); } 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); JsonArray assets = doc["assets"]; for (JsonObject asset : assets) { const char* name = asset["name"] | ""; if (strcmp(name, "firmware.bin") == 0) { const char* url = asset["browser_download_url"] | ""; strncpy(info.downloadUrl, url, sizeof(info.downloadUrl) - 1); break; } } if (!info.downloadUrl[0]) { Serial.printf("[OTA] Check failed: Release v%s has no firmware.bin attachment.\n", info.latestVersion); return info; // Release without firmware.bin attachment } 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)) { // Diagnostic logging (only visible over USB serial, no impact on // the UI) - previously a failed update only returned a simple "true/ // false" externally, without recording the actual reason (timeout, // TLS error, HTTP status code...). This allows tracing a failed // OTA attempt on the serial monitor instead of having to guess again. Serial.printf("[OTA] Starting: url=%s freeHeap=%u RSSI=%ddBm\n", url, (unsigned)ESP.getFreeHeap(), WiFi.RSSI()); WiFiClientSecure client; client.setInsecure(); client.setTimeout(15000); // Gitea serves downloads directly (no redirect like GitHub's S3), // but private repos still need the Authorization header. Use the // HTTPClient overload of httpUpdate.update() so we can set it. HTTPClient http; if (!http.begin(client, url)) { Serial.println("[OTA] Failed: http.begin() returned false for download URL."); return false; } if (Config::GITEA_API_TOKEN[0]) { String auth = "token "; auth += Config::GITEA_API_TOKEN; http.addHeader("Authorization", auth); } // We show a brief success message ourselves after successful installation, // before the device reboots - see // menu_screen.cpp::runOtaUpdateScreen(). httpUpdate.rebootOnUpdate(false); httpUpdate.onProgress([onProgress](int cur, int total) { if (onProgress && total > 0) { onProgress((uint8_t)((cur * 100) / total)); } // Only log occasionally (every ~10%), otherwise the serial // monitor overflows with hundreds of lines for large files. 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; } }