3 Commits

Author SHA1 Message Date
fb884acc27 faster response times
Some checks failed
Build Release / build (push) Failing after 1m18s
2026-08-16 16:55:32 +02:00
3648538cbc eco mode menu toggle
Some checks failed
Build Release / build (push) Failing after 1m31s
2026-08-16 16:24:02 +02:00
a02e280f42 ota update 2026-08-16 14:52:23 +02:00
12 changed files with 154 additions and 103 deletions

View File

@@ -0,0 +1,45 @@
name: Build Release
on:
push:
tags: ['v*']
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install PlatformIO
run: pip install platformio
- name: Build firmware
run: pio run
- name: Upload firmware.bin to release
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
set -e
API="http://192.168.0.37:3000/api/v1/repos/rasmus/flyradar"
TAG="${GITHUB_REF_NAME}"
echo "Fetching release for tag $TAG ..."
RELEASE_JSON=$(curl -s -H "Authorization: token $GITEA_TOKEN" \
"$API/releases/tags/$TAG")
RELEASE_ID=$(echo "$RELEASE_JSON" | jq -r '.id')
if [ "$RELEASE_ID" = "null" ] || [ -z "$RELEASE_ID" ]; then
echo "Error: could not find release for tag $TAG"
echo "Response: $RELEASE_JSON"
exit 1
fi
echo "Uploading to release ID $RELEASE_ID ..."
curl -s -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary @.pio/build/esp32dev/firmware.bin \
"$API/releases/$RELEASE_ID/assets?name=firmware.bin"
echo ""
echo "Upload complete."

View File

@@ -15,29 +15,62 @@ namespace {
char pendingCallsign[9] = {0}; char pendingCallsign[9] = {0};
bool hasPending = false; bool hasPending = false;
char cachedHex[7] = {0}; constexpr uint8_t CACHE_SIZE = 4;
Info cached; struct CacheEntry {
char hex[7] = {0};
Info info;
};
CacheEntry cache[CACHE_SIZE];
uint8_t nextCacheSlot = 0;
// Persistent client — avoids repeated TLS handshakes (~0.5-1s each).
WiFiClientSecure persistentClient;
bool clientConfigured = false;
void ensureMutex() { void ensureMutex() {
if (mutex == nullptr) mutex = xSemaphoreCreateMutex(); if (mutex == nullptr) mutex = xSemaphoreCreateMutex();
} }
bool httpGetString(WiFiClientSecure& client, const String& url, String& outBody) { bool httpGetString(const String& url, String& outBody, uint32_t timeoutMs) {
if (!clientConfigured) {
persistentClient.setInsecure();
clientConfigured = true;
}
persistentClient.setTimeout(timeoutMs);
HTTPClient http; HTTPClient http;
http.setTimeout(5000); http.setTimeout(timeoutMs);
if (!http.begin(client, url)) return false; if (!http.begin(persistentClient, url)) return false;
http.setReuse(true);
int code = http.GET(); int code = http.GET();
bool ok = (code == HTTP_CODE_OK); bool ok = (code == HTTP_CODE_OK);
if (ok) outBody = http.getString(); if (ok) outBody = http.getString();
http.end(); http.end();
return ok; return ok;
} }
int findCacheSlot(const char* hex) {
for (uint8_t i = 0; i < CACHE_SIZE; i++) {
if (strcmp(cache[i].hex, hex) == 0) return i;
}
return -1;
}
void storeInCache(const char* hex, const Info& info) {
int slot = findCacheSlot(hex);
if (slot < 0) {
slot = nextCacheSlot;
nextCacheSlot = (nextCacheSlot + 1) % CACHE_SIZE;
}
strncpy(cache[slot].hex, hex, sizeof(cache[slot].hex) - 1);
cache[slot].info = info;
}
} }
void request(const char* hex, const char* callsign) { void request(const char* hex, const char* callsign) {
ensureMutex(); ensureMutex();
xSemaphoreTake(mutex, portMAX_DELAY); xSemaphoreTake(mutex, portMAX_DELAY);
if (strcmp(cachedHex, hex) != 0 && strcmp(pendingHex, hex) != 0) { if (findCacheSlot(hex) < 0 && strcmp(pendingHex, hex) != 0) {
strncpy(pendingHex, hex, sizeof(pendingHex) - 1); strncpy(pendingHex, hex, sizeof(pendingHex) - 1);
strncpy(pendingCallsign, callsign ? callsign : "", sizeof(pendingCallsign) - 1); strncpy(pendingCallsign, callsign ? callsign : "", sizeof(pendingCallsign) - 1);
hasPending = true; hasPending = true;
@@ -49,8 +82,9 @@ Info get(const char* hex) {
ensureMutex(); ensureMutex();
xSemaphoreTake(mutex, portMAX_DELAY); xSemaphoreTake(mutex, portMAX_DELAY);
Info out; Info out;
if (strcmp(cachedHex, hex) == 0) { int slot = findCacheSlot(hex);
out = cached; if (slot >= 0) {
out = cache[slot].info;
} else if (strcmp(pendingHex, hex) == 0 && hasPending) { } else if (strcmp(pendingHex, hex) == 0 && hasPending) {
out.loading = true; out.loading = true;
} }
@@ -77,15 +111,10 @@ void update() {
Info result; Info result;
WiFiClientSecure client; // Model lookup — try hexdb.io first, fallback to adsbdb.com.
client.setInsecure(); // Timeout reduced to 2s per source (was 3-4s) since we chain fallbacks.
// Try hexdb.io first (previous source) - but with a shorter
// timeout (3s instead of 5s) so a complete service outage does not
// block the ADS-B query unnecessarily long before the fallback kicks in.
client.setTimeout(3000);
String body; String body;
if (httpGetString(client, String("https://hexdb.io/api/v1/aircraft/") + hex, body)) { if (httpGetString(String("https://hexdb.io/api/v1/aircraft/") + hex, body, 2000)) {
JsonDocument doc; JsonDocument doc;
DeserializationError err = deserializeJson(doc, body); DeserializationError err = deserializeJson(doc, body);
if (!err) { if (!err) {
@@ -99,13 +128,9 @@ void update() {
} }
} }
// Fallback: hexdb.io was unreachable/did not deliver a model -
// try adsbdb.com as a second, independent source (different API format,
// but equivalent in content: manufacturer + type via the hex code).
if (!result.model[0]) { if (!result.model[0]) {
client.setTimeout(4000);
String body2; String body2;
if (httpGetString(client, String("https://api.adsbdb.com/v0/aircraft/") + hex, body2)) { if (httpGetString(String("https://api.adsbdb.com/v0/aircraft/") + hex, body2, 2000)) {
JsonDocument doc2; JsonDocument doc2;
DeserializationError err2 = deserializeJson(doc2, body2); DeserializationError err2 = deserializeJson(doc2, body2);
if (!err2) { if (!err2) {
@@ -120,25 +145,7 @@ void update() {
} }
} }
// Flight route (origin/destination airport) - now via a chain of THREE // Route lookup — via chain of three sources, 2s timeout each.
// independent free sources instead of just one, sorted descending by
// observed hit rate in testing. Previously adsbdb.com alone returned
// "unknown" in about 80% of cases (see Alex' feedback) - the
// three sources draw from different, overlapping but
// not identical community databases, therefore much better
// overall coverage through chaining:
// 1. VRS-Standing-Data-Mirror (adsb.lol) - hourly updated
// mirror of the Virtual Radar Server project, by far the most
// reliable source in testing. Path = first 2 characters of the
// (UPPERCASE - the service is case-sensitive) callsign
// as folder, returns "airport_codes":"ORIG-DEST" (ICAO).
// 2. hexdb.io - own route endpoint (different URL from the
// aircraft endpoint above), returns "route":"ORIG-DEST".
// 3. adsbdb.com callsign endpoint - previous single source, remains
// as last fallback since it occasionally has data that the
// other two do not.
// Only try if a callsign is known at all -
// VFR aircraft without a callsign do not have an evaluable route anyway.
String trimmedCallsign = String(callsign); String trimmedCallsign = String(callsign);
trimmedCallsign.trim(); trimmedCallsign.trim();
trimmedCallsign.toUpperCase(); trimmedCallsign.toUpperCase();
@@ -152,11 +159,9 @@ void update() {
}; };
if (trimmedCallsign.length() >= 2) { if (trimmedCallsign.length() >= 2) {
// 1. VRS-Standing-Data-Mirror.
client.setTimeout(4000);
String folder = trimmedCallsign.substring(0, 2);
String body3; String body3;
if (httpGetString(client, String("https://vrs-standing-data.adsb.lol/routes/") + folder + "/" + trimmedCallsign + ".json", body3)) { String folder = trimmedCallsign.substring(0, 2);
if (httpGetString(String("https://vrs-standing-data.adsb.lol/routes/") + folder + "/" + trimmedCallsign + ".json", body3, 2000)) {
JsonDocument doc3; JsonDocument doc3;
if (!deserializeJson(doc3, body3)) { if (!deserializeJson(doc3, body3)) {
const char* codes = doc3["airport_codes"] | ""; const char* codes = doc3["airport_codes"] | "";
@@ -164,11 +169,9 @@ void update() {
} }
} }
// 2. hexdb.io route endpoint, if source 1 did not deliver anything.
if (!result.routeOrigin[0] || !result.routeDest[0]) { if (!result.routeOrigin[0] || !result.routeDest[0]) {
client.setTimeout(3000);
String body4; String body4;
if (httpGetString(client, String("https://hexdb.io/api/v1/route/icao/") + trimmedCallsign, body4)) { if (httpGetString(String("https://hexdb.io/api/v1/route/icao/") + trimmedCallsign, body4, 2000)) {
JsonDocument doc4; JsonDocument doc4;
if (!deserializeJson(doc4, body4)) { if (!deserializeJson(doc4, body4)) {
const char* route = doc4["route"] | ""; const char* route = doc4["route"] | "";
@@ -178,11 +181,9 @@ void update() {
} }
} }
// 3. adsbdb.com callsign endpoint as last fallback.
if (trimmedCallsign.length() > 0 && (!result.routeOrigin[0] || !result.routeDest[0])) { if (trimmedCallsign.length() > 0 && (!result.routeOrigin[0] || !result.routeDest[0])) {
client.setTimeout(4000);
String body5; String body5;
if (httpGetString(client, String("https://api.adsbdb.com/v0/callsign/") + trimmedCallsign, body5)) { if (httpGetString(String("https://api.adsbdb.com/v0/callsign/") + trimmedCallsign, body5, 2000)) {
JsonDocument doc5; JsonDocument doc5;
if (!deserializeJson(doc5, body5)) { if (!deserializeJson(doc5, body5)) {
const char* originIcao = doc5["response"]["flightroute"]["origin"]["icao_code"] | ""; const char* originIcao = doc5["response"]["flightroute"]["origin"]["icao_code"] | "";
@@ -196,8 +197,7 @@ void update() {
} }
xSemaphoreTake(mutex, portMAX_DELAY); xSemaphoreTake(mutex, portMAX_DELAY);
strncpy(cachedHex, hex, sizeof(cachedHex) - 1); storeInCache(hex, result);
cached = result;
hasPending = false; hasPending = false;
xSemaphoreGive(mutex); xSemaphoreGive(mutex);
} }

View File

@@ -37,7 +37,7 @@ namespace Config {
// require a meaningful User-Agent instead of the HTTPClient // require a meaningful User-Agent instead of the HTTPClient
// default, see https://operations.osmfoundation.org/policies/nominatim/. // default, see https://operations.osmfoundation.org/policies/nominatim/.
constexpr const char* NOMINATIM_HOST = "nominatim.openstreetmap.org"; constexpr const char* NOMINATIM_HOST = "nominatim.openstreetmap.org";
constexpr const char* NOMINATIM_USER_AGENT = "EiswolfsFlightradarCYD (github.com/Eiswolf-BG/eiswolfs-flightradar-CYD)"; constexpr const char* NOMINATIM_USER_AGENT = "EiswolfsFlightradarCYD (git.rasmusbendtsen.dk/rasmus/flyradar)";
struct GpsPinPair { uint8_t rx; uint8_t tx; const char* label; }; struct GpsPinPair { uint8_t rx; uint8_t tx; const char* label; };
constexpr GpsPinPair GPS_PIN_CANDIDATES[] = { constexpr GpsPinPair GPS_PIN_CANDIDATES[] = {
@@ -116,4 +116,12 @@ constexpr float RANGE_STEPS_KM[] = {10.0f, 25.0f, 50.0f, 100.0f};
constexpr uint8_t EMERGENCY_SQUAWK_COUNT = 3; constexpr uint8_t EMERGENCY_SQUAWK_COUNT = 3;
constexpr uint8_t MAX_WIFI_NETWORKS = 3; constexpr uint8_t MAX_WIFI_NETWORKS = 3;
// Gitea OTA update (Menu > System > "Check for update") - checks
// the latest release in a private Gitea repository and downloads
// firmware.bin from there. Uses the external proxy URL since the
// ESP32 reaches Gitea via https://git.rasmusbendtsen.dk (Pangolin).
constexpr const char* GITEA_API_HOST = "git.rasmusbendtsen.dk";
constexpr const char* GITEA_API_PATH = "/api/v1/repos/rasmus/flyradar/releases/latest";
constexpr const char* GITEA_API_TOKEN = "YOUR_GITEA_TOKEN_HERE"; // set this!
} }

View File

@@ -687,6 +687,7 @@ void run(TFT_eSPI& tft) {
drawButton(tft, brightnessBtn, brightnessLabel(SettingsStore::brightnessPercent())); drawButton(tft, brightnessBtn, brightnessLabel(SettingsStore::brightnessPercent()));
drawButton(tft, timeoutBtn, screenTimeoutLabel(SettingsStore::screenTimeoutMinutes())); drawButton(tft, timeoutBtn, screenTimeoutLabel(SettingsStore::screenTimeoutMinutes()));
drawButton(tft, nightDimBtn, I18n::t(StringId::MENU_NIGHT_DIMMING) + onOff(SettingsStore::nightDimmingEnabled())); drawButton(tft, nightDimBtn, I18n::t(StringId::MENU_NIGHT_DIMMING) + onOff(SettingsStore::nightDimmingEnabled()));
drawButton(tft, ecoModeBtn, I18n::t(StringId::MENU_ECO_MODE) + onOff(SettingsStore::ecoModeEnabled()));
drawButton(tft, webuiBtn, I18n::t(StringId::MENU_LOGBOOK_WEBUI)); drawButton(tft, webuiBtn, I18n::t(StringId::MENU_LOGBOOK_WEBUI));
drawButton(tft, backupResetBtn, I18n::t(StringId::MENU_BACKUP_RESET)); drawButton(tft, backupResetBtn, I18n::t(StringId::MENU_BACKUP_RESET));
drawButton(tft, checkUpdateBtn, I18n::t(StringId::MENU_CHECK_UPDATE)); drawButton(tft, checkUpdateBtn, I18n::t(StringId::MENU_CHECK_UPDATE));

View File

@@ -12,13 +12,10 @@
namespace OtaUpdate { namespace OtaUpdate {
namespace { namespace {
constexpr const char* RELEASES_API_URL = // Gitea API URL constructed from config constants at runtime
"https://api.github.com/repos/Eiswolf-BG/eiswolfs-flightradar-CYD/releases/latest"; String releasesApiUrl() {
// GitHub requires a meaningful User-Agent for API requests return String("https://") + Config::GITEA_API_HOST + Config::GITEA_API_PATH;
// (otherwise HTTP 403) - same principle as Config::NOMINATIM_USER_AGENT }
// for address search.
constexpr const char* USER_AGENT =
"EiswolfsFlightradarCYD-OTA/1.0 (+https://github.com/Eiswolf-BG/eiswolfs-flightradar-CYD)";
bool parseVersion(const char* s, int& major, int& minor, int& patch) { bool parseVersion(const char* s, int& major, int& minor, int& patch) {
major = minor = patch = 0; major = minor = patch = 0;
@@ -27,10 +24,7 @@ namespace {
return sscanf(s, "%d.%d.%d", &major, &minor, &patch) == 3; 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. // Numeric version comparison (not string-based, so e.g. "3.10.0" > "3.9.0").
// 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 compareVersions(const char* a, const char* b) {
int aMaj, aMin, aPat, bMaj, bMin, bPat; int aMaj, aMin, aPat, bMaj, bMin, bPat;
if (!parseVersion(a, aMaj, aMin, aPat) || !parseVersion(b, bMaj, bMin, bPat)) return 0; if (!parseVersion(a, aMaj, aMin, aPat) || !parseVersion(b, bMaj, bMin, bPat)) return 0;
@@ -38,12 +32,22 @@ namespace {
if (aMin != bMin) return aMin - bMin; if (aMin != bMin) return aMin - bMin;
return aPat - bPat; 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 checkForUpdate() {
CheckInfo info; CheckInfo info;
String url = releasesApiUrl();
Serial.printf("[OTA] Checking for update: url=%s freeHeap=%u RSSI=%ddBm\n", RELEASES_API_URL, Serial.printf("[OTA] Checking for update: url=%s freeHeap=%u RSSI=%ddBm\n", url.c_str(),
(unsigned)ESP.getFreeHeap(), WiFi.RSSI()); (unsigned)ESP.getFreeHeap(), WiFi.RSSI());
WiFiClientSecure client; WiFiClientSecure client;
@@ -52,11 +56,11 @@ CheckInfo checkForUpdate() {
HTTPClient http; HTTPClient http;
http.setTimeout(8000); http.setTimeout(8000);
if (!http.begin(client, RELEASES_API_URL)) { if (!http.begin(client, url)) {
Serial.println("[OTA] Check failed: http.begin() returned false."); Serial.println("[OTA] Check failed: http.begin() returned false.");
return info; return info;
} }
http.addHeader("User-Agent", USER_AGENT); addAuthHeader(http);
int code = http.GET(); int code = http.GET();
if (code != HTTP_CODE_OK) { if (code != HTTP_CODE_OK) {
@@ -88,12 +92,15 @@ CheckInfo checkForUpdate() {
strncpy(info.latestVersion, (tag[0] == 'v' || tag[0] == 'V') ? tag + 1 : tag, strncpy(info.latestVersion, (tag[0] == 'v' || tag[0] == 'V') ? tag + 1 : tag,
sizeof(info.latestVersion) - 1); sizeof(info.latestVersion) - 1);
// Use browser_download_url from the API response — this contains the
// external proxy URL (https://git.rasmusbendtsen.dk/...) which the
// ESP32 can reach through the proxy.
JsonArray assets = doc["assets"]; JsonArray assets = doc["assets"];
for (JsonObject asset : assets) { for (JsonObject asset : assets) {
const char* name = asset["name"] | ""; const char* name = asset["name"] | "";
if (strcmp(name, "firmware.bin") == 0) { if (strcmp(name, "firmware.bin") == 0) {
const char* url = asset["browser_download_url"] | ""; const char* dlUrl = asset["browser_download_url"] | "";
strncpy(info.downloadUrl, url, sizeof(info.downloadUrl) - 1); strncpy(info.downloadUrl, dlUrl, sizeof(info.downloadUrl) - 1);
break; break;
} }
} }
@@ -101,7 +108,7 @@ CheckInfo checkForUpdate() {
if (!info.downloadUrl[0]) { if (!info.downloadUrl[0]) {
Serial.printf("[OTA] Check failed: Release v%s has no firmware.bin attachment.\n", Serial.printf("[OTA] Check failed: Release v%s has no firmware.bin attachment.\n",
info.latestVersion); info.latestVersion);
return info; // Release without firmware.bin attachment return info;
} }
int cmp = compareVersions(info.latestVersion, Config::APP_VERSION); int cmp = compareVersions(info.latestVersion, Config::APP_VERSION);
@@ -113,11 +120,6 @@ Serial.printf("[OTA] Check successful: installed=v%s latest=v%s -> %s\n", Config
} }
bool performUpdate(const char* url, void (*onProgress)(uint8_t percent)) { 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, Serial.printf("[OTA] Starting: url=%s freeHeap=%u RSSI=%ddBm\n", url,
(unsigned)ESP.getFreeHeap(), WiFi.RSSI()); (unsigned)ESP.getFreeHeap(), WiFi.RSSI());
@@ -125,26 +127,20 @@ bool performUpdate(const char* url, void (*onProgress)(uint8_t percent)) {
client.setInsecure(); client.setInsecure();
client.setTimeout(15000); client.setTimeout(15000);
// IMPORTANT: GitHub's "browser_download_url" for release assets is NOT // Use HTTPClient overload so we can add the Authorization header
// a direct download link, but first returns an HTTP 301/302 redirect // (required for private repos even during download).
// to a signed objects.githubusercontent.com URL. HTTPUpdate does NOT HTTPClient http;
// follow redirects by default (HTTPC_DISABLE_FOLLOW_REDIRECTS is the if (!http.begin(client, url)) {
// default) - without this line the download aborts with HTTP_UPDATE_FAILED Serial.println("[OTA] Failed: http.begin() returned false for download URL.");
// because only the redirect response arrives instead of the .bin file. See return false;
// e.g. espressif/arduino-esp32#3020. HTTPC_STRICT_FOLLOW_REDIRECTS }
// suffices since we only use GET. addAuthHeader(http);
httpUpdate.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
// We show a brief success message ourselves after successful installation,
// before the device reboots - see
// menu_screen.cpp::runOtaUpdateScreen().
httpUpdate.rebootOnUpdate(false); httpUpdate.rebootOnUpdate(false);
httpUpdate.onProgress([onProgress](int cur, int total) { httpUpdate.onProgress([onProgress](int cur, int total) {
if (onProgress && total > 0) { if (onProgress && total > 0) {
onProgress((uint8_t)((cur * 100) / total)); 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; static int8_t lastLoggedPercent = -1;
if (total > 0) { if (total > 0) {
int8_t pct = (int8_t)((cur * 100) / total); int8_t pct = (int8_t)((cur * 100) / total);
@@ -156,7 +152,8 @@ bool performUpdate(const char* url, void (*onProgress)(uint8_t percent)) {
} }
}); });
t_httpUpdate_return result = httpUpdate.update(client, url); t_httpUpdate_return result = httpUpdate.update(http);
http.end();
if (result != HTTP_UPDATE_OK) { if (result != HTTP_UPDATE_OK) {
Serial.printf("[OTA] Failed: result=%d error=%d (%s) freeHeap=%u\n", (int)result, Serial.printf("[OTA] Failed: result=%d error=%d (%s) freeHeap=%u\n", (int)result,

View File

@@ -2,7 +2,7 @@
#include <Arduino.h> #include <Arduino.h>
// Firmware update over WiFi (Menu > System > "Check for update") - // Firmware update over WiFi (Menu > System > "Check for update") -
// checks against the latest version in the GitHub repository (Releases API) and // checks against the latest version in a private Gitea repository (Releases API) and
// installs it on the device directly upon confirmation, without cable/ // installs it on the device directly upon confirmation, without cable/
// web flasher. Blocking, synchronous HTTPS calls - only executed on // web flasher. Blocking, synchronous HTTPS calls - only executed on
// explicit button press (no background polling), analogous to // explicit button press (no background polling), analogous to
@@ -23,7 +23,7 @@ namespace OtaUpdate {
char downloadUrl[192] = {0}; char downloadUrl[192] = {0};
}; };
// Queries the GitHub Releases API for the latest release, compares // Queries the Gitea Releases API for the latest release, compares
// its version number (tag name, "v" prefix is ignored) against // its version number (tag name, "v" prefix is ignored) against
// Config::APP_VERSION and looks for the "firmware.bin" attachment in the release. // Config::APP_VERSION and looks for the "firmware.bin" attachment in the release.
// CheckResult::Error on any error along the way (no WiFi, timeout, // CheckResult::Error on any error along the way (no WiFi, timeout,