Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ab9b81683b | |||
| 2949e8c110 | |||
| d80600863a | |||
| fb884acc27 | |||
| 3648538cbc | |||
| a02e280f42 |
28
.gitea/workflows/release.yml
Normal file
28
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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: Create Release and Upload Firmware
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: .pio/build/esp32dev/firmware.bin
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
#include "aircraft_details.h"
|
#include "aircraft_details.h"
|
||||||
|
#include "sd_storage.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include <SD.h>
|
||||||
#include <WiFiClientSecure.h>
|
#include <WiFiClientSecure.h>
|
||||||
#include <HTTPClient.h>
|
#include <HTTPClient.h>
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
@@ -11,36 +14,208 @@ namespace AircraftDetails {
|
|||||||
namespace {
|
namespace {
|
||||||
SemaphoreHandle_t mutex = nullptr;
|
SemaphoreHandle_t mutex = nullptr;
|
||||||
|
|
||||||
|
constexpr uint8_t RING_SIZE = 8;
|
||||||
|
struct RingEntry {
|
||||||
|
char hex[7] = {0};
|
||||||
|
Info info;
|
||||||
|
};
|
||||||
|
RingEntry ring[RING_SIZE];
|
||||||
|
uint8_t ringNext = 0;
|
||||||
|
|
||||||
char pendingHex[7] = {0};
|
char pendingHex[7] = {0};
|
||||||
char pendingCallsign[9] = {0};
|
char pendingCallsign[9] = {0};
|
||||||
bool hasPending = false;
|
|
||||||
|
|
||||||
char cachedHex[7] = {0};
|
enum class Phase { Idle, ModelSrc1, ModelSrc2, RouteSrc1, RouteSrc2, RouteSrc3, Done };
|
||||||
Info cached;
|
Phase currentPhase = Phase::Idle;
|
||||||
|
|
||||||
|
char workHex[7] = {0};
|
||||||
|
char workCallsign[9] = {0};
|
||||||
|
Info workResult;
|
||||||
|
|
||||||
|
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) {
|
int ringFind(const char* hex) {
|
||||||
|
for (uint8_t i = 0; i < RING_SIZE; i++) {
|
||||||
|
if (strcmp(ring[i].hex, hex) == 0) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ringStore(const char* hex, const Info& info) {
|
||||||
|
int slot = ringFind(hex);
|
||||||
|
if (slot < 0) {
|
||||||
|
slot = ringNext;
|
||||||
|
ringNext = (ringNext + 1) % RING_SIZE;
|
||||||
|
}
|
||||||
|
strncpy(ring[slot].hex, hex, sizeof(ring[slot].hex) - 1);
|
||||||
|
ring[slot].info = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool loadFromSd(const char* hex, Info& out) {
|
||||||
|
if (!SdStorage::isMounted()) return false;
|
||||||
|
String path = String(Config::SD_DETAIL_CACHE_DIR) + "/" + hex + ".txt";
|
||||||
|
if (!SD.exists(path)) return false;
|
||||||
|
File f = SD.open(path);
|
||||||
|
if (!f) return false;
|
||||||
|
char line[64];
|
||||||
|
while (f.available()) {
|
||||||
|
int len = f.readBytesUntil('\n', line, sizeof(line) - 1);
|
||||||
|
line[len] = '\0';
|
||||||
|
char* eq = strchr(line, '=');
|
||||||
|
if (!eq) continue;
|
||||||
|
*eq = '\0';
|
||||||
|
const char* val = eq + 1;
|
||||||
|
if (strcmp(line, "model") == 0) {
|
||||||
|
strncpy(out.model, val, sizeof(out.model) - 1);
|
||||||
|
} else if (strcmp(line, "route") == 0) {
|
||||||
|
char* dash = strchr(val, '-');
|
||||||
|
if (dash) {
|
||||||
|
*dash = '\0';
|
||||||
|
strncpy(out.routeOrigin, val, sizeof(out.routeOrigin) - 1);
|
||||||
|
strncpy(out.routeDest, dash + 1, sizeof(out.routeDest) - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.close();
|
||||||
|
return out.model[0] || out.routeOrigin[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveToSd(const char* hex, const Info& info) {
|
||||||
|
if (!SdStorage::isMounted()) return;
|
||||||
|
SD.mkdir(Config::SD_DETAIL_CACHE_DIR);
|
||||||
|
String path = String(Config::SD_DETAIL_CACHE_DIR) + "/" + hex + ".txt";
|
||||||
|
File f = SD.open(path, FILE_WRITE);
|
||||||
|
if (!f) return;
|
||||||
|
f.print("model="); f.println(info.model);
|
||||||
|
f.print("route=");
|
||||||
|
if (info.routeOrigin[0]) { f.print(info.routeOrigin); f.print("-"); f.println(info.routeDest); }
|
||||||
|
else { f.println(); }
|
||||||
|
f.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void applyRouteCodes(const String& codes) {
|
||||||
|
int dash = codes.indexOf('-');
|
||||||
|
if (dash > 0 && dash < (int)codes.length() - 1) {
|
||||||
|
strncpy(workResult.routeOrigin, codes.substring(0, dash).c_str(), sizeof(workResult.routeOrigin) - 1);
|
||||||
|
strncpy(workResult.routeDest, codes.substring(dash + 1).c_str(), sizeof(workResult.routeDest) - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool advance() {
|
||||||
|
switch (currentPhase) {
|
||||||
|
case Phase::ModelSrc1: {
|
||||||
|
String body;
|
||||||
|
if (httpGetString(String("https://hexdb.io/api/v1/aircraft/") + workHex, body, 2000)) {
|
||||||
|
JsonDocument doc;
|
||||||
|
if (!deserializeJson(doc, body)) {
|
||||||
|
const char* m = doc["Manufacturer"] | "";
|
||||||
|
const char* t = doc["Type"] | "";
|
||||||
|
if (m[0] && t[0]) snprintf(workResult.model, sizeof(workResult.model), "%s %s", m, t);
|
||||||
|
else if (t[0]) strncpy(workResult.model, t, sizeof(workResult.model) - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentPhase = workResult.model[0] ? Phase::RouteSrc1 : Phase::ModelSrc2;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case Phase::ModelSrc2: {
|
||||||
|
String body;
|
||||||
|
if (httpGetString(String("https://api.adsbdb.com/v0/aircraft/") + workHex, body, 2000)) {
|
||||||
|
JsonDocument doc;
|
||||||
|
if (!deserializeJson(doc, body)) {
|
||||||
|
const char* m = doc["response"]["aircraft"]["manufacturer"] | "";
|
||||||
|
const char* t = doc["response"]["aircraft"]["type"] | "";
|
||||||
|
if (m[0] && t[0]) snprintf(workResult.model, sizeof(workResult.model), "%s %s", m, t);
|
||||||
|
else if (t[0]) strncpy(workResult.model, t, sizeof(workResult.model) - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentPhase = Phase::RouteSrc1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case Phase::RouteSrc1: {
|
||||||
|
String cs = String(workCallsign);
|
||||||
|
cs.trim(); cs.toUpperCase();
|
||||||
|
if (cs.length() >= 2) {
|
||||||
|
String body;
|
||||||
|
String folder = cs.substring(0, 2);
|
||||||
|
if (httpGetString(String("https://vrs-standing-data.adsb.lol/routes/") + folder + "/" + cs + ".json", body, 2000)) {
|
||||||
|
JsonDocument doc;
|
||||||
|
if (!deserializeJson(doc, body)) {
|
||||||
|
applyRouteCodes(String(doc["airport_codes"] | ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentPhase = workResult.routeOrigin[0] ? Phase::Done : Phase::RouteSrc2;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case Phase::RouteSrc2: {
|
||||||
|
String cs = String(workCallsign);
|
||||||
|
cs.trim(); cs.toUpperCase();
|
||||||
|
if (cs.length() >= 2) {
|
||||||
|
String body;
|
||||||
|
if (httpGetString(String("https://hexdb.io/api/v1/route/icao/") + cs, body, 2000)) {
|
||||||
|
JsonDocument doc;
|
||||||
|
if (!deserializeJson(doc, body)) {
|
||||||
|
applyRouteCodes(String(doc["route"] | ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentPhase = workResult.routeOrigin[0] ? Phase::Done : Phase::RouteSrc3;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case Phase::RouteSrc3: {
|
||||||
|
String cs = String(workCallsign);
|
||||||
|
cs.trim(); cs.toUpperCase();
|
||||||
|
if (cs.length() > 0) {
|
||||||
|
String body;
|
||||||
|
if (httpGetString(String("https://api.adsbdb.com/v0/callsign/") + cs, body, 2000)) {
|
||||||
|
JsonDocument doc;
|
||||||
|
if (!deserializeJson(doc, body)) {
|
||||||
|
const char* o = doc["response"]["flightroute"]["origin"]["icao_code"] | "";
|
||||||
|
const char* d = doc["response"]["flightroute"]["destination"]["icao_code"] | "";
|
||||||
|
if (o[0] && d[0]) {
|
||||||
|
strncpy(workResult.routeOrigin, o, sizeof(workResult.routeOrigin) - 1);
|
||||||
|
strncpy(workResult.routeDest, d, sizeof(workResult.routeDest) - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentPhase = Phase::Done;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (currentPhase == Phase::Idle && ringFind(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;
|
|
||||||
}
|
}
|
||||||
xSemaphoreGive(mutex);
|
xSemaphoreGive(mutex);
|
||||||
}
|
}
|
||||||
@@ -49,10 +224,17 @@ 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 = ringFind(hex);
|
||||||
out = cached;
|
if (slot >= 0) {
|
||||||
} else if (strcmp(pendingHex, hex) == 0 && hasPending) {
|
out = ring[slot].info;
|
||||||
|
} else if (currentPhase != Phase::Idle && strcmp(workHex, hex) == 0) {
|
||||||
out.loading = true;
|
out.loading = true;
|
||||||
|
// Return partial results as they arrive
|
||||||
|
if (workResult.model[0]) strncpy(out.model, workResult.model, sizeof(out.model) - 1);
|
||||||
|
if (workResult.routeOrigin[0]) {
|
||||||
|
strncpy(out.routeOrigin, workResult.routeOrigin, sizeof(out.routeOrigin) - 1);
|
||||||
|
strncpy(out.routeDest, workResult.routeDest, sizeof(out.routeDest) - 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
xSemaphoreGive(mutex);
|
xSemaphoreGive(mutex);
|
||||||
return out;
|
return out;
|
||||||
@@ -60,145 +242,42 @@ Info get(const char* hex) {
|
|||||||
|
|
||||||
void update() {
|
void update() {
|
||||||
ensureMutex();
|
ensureMutex();
|
||||||
|
|
||||||
char hex[7] = {0};
|
|
||||||
char callsign[9] = {0};
|
|
||||||
bool doWork = false;
|
|
||||||
|
|
||||||
xSemaphoreTake(mutex, portMAX_DELAY);
|
xSemaphoreTake(mutex, portMAX_DELAY);
|
||||||
if (hasPending) {
|
|
||||||
strncpy(hex, pendingHex, sizeof(hex) - 1);
|
|
||||||
strncpy(callsign, pendingCallsign, sizeof(callsign) - 1);
|
|
||||||
doWork = true;
|
|
||||||
}
|
|
||||||
xSemaphoreGive(mutex);
|
|
||||||
|
|
||||||
if (!doWork) return;
|
if (currentPhase == Phase::Idle) {
|
||||||
|
if (pendingHex[0]) {
|
||||||
Info result;
|
// Check SD cache before doing network
|
||||||
|
Info cached;
|
||||||
WiFiClientSecure client;
|
if (loadFromSd(pendingHex, cached)) {
|
||||||
client.setInsecure();
|
ringStore(pendingHex, cached);
|
||||||
|
memset(pendingHex, 0, sizeof(pendingHex));
|
||||||
// Try hexdb.io first (previous source) - but with a shorter
|
memset(pendingCallsign, 0, sizeof(pendingCallsign));
|
||||||
// timeout (3s instead of 5s) so a complete service outage does not
|
xSemaphoreGive(mutex);
|
||||||
// block the ADS-B query unnecessarily long before the fallback kicks in.
|
return;
|
||||||
client.setTimeout(3000);
|
|
||||||
String body;
|
|
||||||
if (httpGetString(client, String("https://hexdb.io/api/v1/aircraft/") + hex, body)) {
|
|
||||||
JsonDocument doc;
|
|
||||||
DeserializationError err = deserializeJson(doc, body);
|
|
||||||
if (!err) {
|
|
||||||
const char* manufacturer = doc["Manufacturer"] | "";
|
|
||||||
const char* type = doc["Type"] | "";
|
|
||||||
if (manufacturer[0] && type[0]) {
|
|
||||||
snprintf(result.model, sizeof(result.model), "%s %s", manufacturer, type);
|
|
||||||
} else if (type[0]) {
|
|
||||||
strncpy(result.model, type, sizeof(result.model) - 1);
|
|
||||||
}
|
}
|
||||||
|
strncpy(workHex, pendingHex, sizeof(workHex) - 1);
|
||||||
|
strncpy(workCallsign, pendingCallsign, sizeof(workCallsign) - 1);
|
||||||
|
workResult = Info{};
|
||||||
|
memset(pendingHex, 0, sizeof(pendingHex));
|
||||||
|
memset(pendingCallsign, 0, sizeof(pendingCallsign));
|
||||||
|
currentPhase = Phase::ModelSrc1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: hexdb.io was unreachable/did not deliver a model -
|
if (currentPhase == Phase::Idle) {
|
||||||
// try adsbdb.com as a second, independent source (different API format,
|
xSemaphoreGive(mutex);
|
||||||
// but equivalent in content: manufacturer + type via the hex code).
|
return;
|
||||||
if (!result.model[0]) {
|
|
||||||
client.setTimeout(4000);
|
|
||||||
String body2;
|
|
||||||
if (httpGetString(client, String("https://api.adsbdb.com/v0/aircraft/") + hex, body2)) {
|
|
||||||
JsonDocument doc2;
|
|
||||||
DeserializationError err2 = deserializeJson(doc2, body2);
|
|
||||||
if (!err2) {
|
|
||||||
const char* manufacturer = doc2["response"]["aircraft"]["manufacturer"] | "";
|
|
||||||
const char* type = doc2["response"]["aircraft"]["type"] | "";
|
|
||||||
if (manufacturer[0] && type[0]) {
|
|
||||||
snprintf(result.model, sizeof(result.model), "%s %s", manufacturer, type);
|
|
||||||
} else if (type[0]) {
|
|
||||||
strncpy(result.model, type, sizeof(result.model) - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flight route (origin/destination airport) - now via a chain of THREE
|
// Do one HTTP request, then return
|
||||||
// independent free sources instead of just one, sorted descending by
|
bool working = advance();
|
||||||
// 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);
|
|
||||||
trimmedCallsign.trim();
|
|
||||||
trimmedCallsign.toUpperCase();
|
|
||||||
|
|
||||||
auto applyRouteCodes = [&](const String& codes) {
|
if (!working || currentPhase == Phase::Done) {
|
||||||
int dash = codes.indexOf('-');
|
saveToSd(workHex, workResult);
|
||||||
if (dash > 0 && dash < (int)codes.length() - 1) {
|
ringStore(workHex, workResult);
|
||||||
strncpy(result.routeOrigin, codes.substring(0, dash).c_str(), sizeof(result.routeOrigin) - 1);
|
currentPhase = Phase::Idle;
|
||||||
strncpy(result.routeDest, codes.substring(dash + 1).c_str(), sizeof(result.routeDest) - 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (trimmedCallsign.length() >= 2) {
|
|
||||||
// 1. VRS-Standing-Data-Mirror.
|
|
||||||
client.setTimeout(4000);
|
|
||||||
String folder = trimmedCallsign.substring(0, 2);
|
|
||||||
String body3;
|
|
||||||
if (httpGetString(client, String("https://vrs-standing-data.adsb.lol/routes/") + folder + "/" + trimmedCallsign + ".json", body3)) {
|
|
||||||
JsonDocument doc3;
|
|
||||||
if (!deserializeJson(doc3, body3)) {
|
|
||||||
const char* codes = doc3["airport_codes"] | "";
|
|
||||||
applyRouteCodes(String(codes));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. hexdb.io route endpoint, if source 1 did not deliver anything.
|
|
||||||
if (!result.routeOrigin[0] || !result.routeDest[0]) {
|
|
||||||
client.setTimeout(3000);
|
|
||||||
String body4;
|
|
||||||
if (httpGetString(client, String("https://hexdb.io/api/v1/route/icao/") + trimmedCallsign, body4)) {
|
|
||||||
JsonDocument doc4;
|
|
||||||
if (!deserializeJson(doc4, body4)) {
|
|
||||||
const char* route = doc4["route"] | "";
|
|
||||||
applyRouteCodes(String(route));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. adsbdb.com callsign endpoint as last fallback.
|
|
||||||
if (trimmedCallsign.length() > 0 && (!result.routeOrigin[0] || !result.routeDest[0])) {
|
|
||||||
client.setTimeout(4000);
|
|
||||||
String body5;
|
|
||||||
if (httpGetString(client, String("https://api.adsbdb.com/v0/callsign/") + trimmedCallsign, body5)) {
|
|
||||||
JsonDocument doc5;
|
|
||||||
if (!deserializeJson(doc5, body5)) {
|
|
||||||
const char* originIcao = doc5["response"]["flightroute"]["origin"]["icao_code"] | "";
|
|
||||||
const char* destIcao = doc5["response"]["flightroute"]["destination"]["icao_code"] | "";
|
|
||||||
if (originIcao[0] && destIcao[0]) {
|
|
||||||
strncpy(result.routeOrigin, originIcao, sizeof(result.routeOrigin) - 1);
|
|
||||||
strncpy(result.routeDest, destIcao, sizeof(result.routeDest) - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
xSemaphoreTake(mutex, portMAX_DELAY);
|
|
||||||
strncpy(cachedHex, hex, sizeof(cachedHex) - 1);
|
|
||||||
cached = result;
|
|
||||||
hasPending = false;
|
|
||||||
xSemaphoreGive(mutex);
|
xSemaphoreGive(mutex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
|
||||||
// Additional aircraft details (model) that are NOT part of the ADS-B signal
|
// Additional aircraft details (model + route) that are NOT part of
|
||||||
// and get looked up via the free hexdb.io community database by hex code -
|
// the ADS-B signal and get looked up via a chain of free community
|
||||||
// only for the currently selected aircraft (not for all of them, to keep
|
// databases (hexdb.io, adsbdb.com, adsb.lol) by hex code and callsign.
|
||||||
// network load low).
|
// Results are cached on the SD card (/Flightradar_cyd/cache/{hex}.txt)
|
||||||
|
// so each aircraft only needs to be fetched once, ever.
|
||||||
|
// Lookups are spread across multiple update() calls (state machine,
|
||||||
|
// one HTTP request per call) so Core 0 is never blocked for long.
|
||||||
namespace AircraftDetails {
|
namespace AircraftDetails {
|
||||||
|
|
||||||
struct Info {
|
struct Info {
|
||||||
|
|||||||
13
src/config.h
13
src/config.h
@@ -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[] = {
|
||||||
@@ -85,6 +85,7 @@ constexpr float RANGE_STEPS_KM[] = {10.0f, 25.0f, 50.0f, 100.0f};
|
|||||||
constexpr const char* SD_SETTINGS_FILE = "/Flightradar_cyd/config.txt";
|
constexpr const char* SD_SETTINGS_FILE = "/Flightradar_cyd/config.txt";
|
||||||
constexpr const char* SD_WIFI_CREDENTIALS_FILE = "/Flightradar_cyd/wifi.txt";
|
constexpr const char* SD_WIFI_CREDENTIALS_FILE = "/Flightradar_cyd/wifi.txt";
|
||||||
constexpr const char* SD_CALIBRATION_FILE = "/Flightradar_cyd/calibration.txt";
|
constexpr const char* SD_CALIBRATION_FILE = "/Flightradar_cyd/calibration.txt";
|
||||||
|
constexpr const char* SD_DETAIL_CACHE_DIR = "/Flightradar_cyd/cache";
|
||||||
|
|
||||||
constexpr uint8_t SD_SPI_CS_PIN = 5;
|
constexpr uint8_t SD_SPI_CS_PIN = 5;
|
||||||
constexpr uint8_t SD_SPI_MOSI_PIN = 23;
|
constexpr uint8_t SD_SPI_MOSI_PIN = 23;
|
||||||
@@ -115,5 +116,13 @@ constexpr float RANGE_STEPS_KM[] = {10.0f, 25.0f, 50.0f, 100.0f};
|
|||||||
constexpr const char* EMERGENCY_SQUAWKS[] = {"7500", "7600", "7700"};
|
constexpr const char* EMERGENCY_SQUAWKS[] = {"7500", "7600", "7700"};
|
||||||
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 = ""; // set this!
|
||||||
}
|
}
|
||||||
@@ -255,7 +255,7 @@ static const char* const I18N_DE[] = {
|
|||||||
"Das Update konnte nicht heruntergeladen oder installiert werden. Das Gerät läuft unverändert mit der bisherigen Firmware weiter. Bitte WLAN-Verbindung prüfen und es später erneut versuchen.",
|
"Das Update konnte nicht heruntergeladen oder installiert werden. Das Gerät läuft unverändert mit der bisherigen Firmware weiter. Bitte WLAN-Verbindung prüfen und es später erneut versuchen.",
|
||||||
"Bildschirm-Timeout",
|
"Bildschirm-Timeout",
|
||||||
"Nach dieser Zeit ohne Berührung schaltet sich das Display ab, um Strom zu sparen. Ist der Ruhebildschirm aktiviert, zeigt es stattdessen einen gedimmten Sternenhimmel mit Uhrzeit.",
|
"Nach dieser Zeit ohne Berührung schaltet sich das Display ab, um Strom zu sparen. Ist der Ruhebildschirm aktiviert, zeigt es stattdessen einen gedimmten Sternenhimmel mit Uhrzeit.",
|
||||||
"Eco Mode",
|
"Eco Mode ",
|
||||||
"Streckenfilter",
|
"Streckenfilter",
|
||||||
"ICAO eingeben",
|
"ICAO eingeben",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ static const char* const I18N_EN[] = {
|
|||||||
"The update could not be downloaded or installed. The device is still running the previous firmware, unchanged. Please check your WiFi connection and try again later.",
|
"The update could not be downloaded or installed. The device is still running the previous firmware, unchanged. Please check your WiFi connection and try again later.",
|
||||||
"Screen Timeout",
|
"Screen Timeout",
|
||||||
"After this time without touch, the display turns off to save power. With the screensaver on, it dims to a starfield with a clock instead.",
|
"After this time without touch, the display turns off to save power. With the screensaver on, it dims to a starfield with a clock instead.",
|
||||||
"Eco Mode",
|
"Eco Mode ",
|
||||||
"Route Filter",
|
"Route Filter",
|
||||||
"Enter ICAO",
|
"Enter ICAO",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ static const char* const I18N_ES[] = {
|
|||||||
"No se pudo descargar o instalar la actualización. El dispositivo sigue funcionando sin cambios con el firmware anterior. Comprueba tu conexión WiFi e inténtalo de nuevo más tarde.",
|
"No se pudo descargar o instalar la actualización. El dispositivo sigue funcionando sin cambios con el firmware anterior. Comprueba tu conexión WiFi e inténtalo de nuevo más tarde.",
|
||||||
"Tiempo de espera de pantalla",
|
"Tiempo de espera de pantalla",
|
||||||
"Tras este tiempo sin toques, la pantalla se apaga para ahorrar energía. Con el salvapantallas activado, muestra en su lugar un cielo estrellado atenuado con la hora.",
|
"Tras este tiempo sin toques, la pantalla se apaga para ahorrar energía. Con el salvapantallas activado, muestra en su lugar un cielo estrellado atenuado con la hora.",
|
||||||
"Eco Mode",
|
"Eco Mode ",
|
||||||
"Filtro de ruta",
|
"Filtro de ruta",
|
||||||
"Ingresa OACI",
|
"Ingresa OACI",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ static const char* const I18N_FR[] = {
|
|||||||
"La mise à jour n'a pas pu être téléchargée ou installée. L'appareil continue de fonctionner normalement avec le micrologiciel précédent. Merci de vérifier votre connexion WiFi et de réessayer plus tard.",
|
"La mise à jour n'a pas pu être téléchargée ou installée. L'appareil continue de fonctionner normalement avec le micrologiciel précédent. Merci de vérifier votre connexion WiFi et de réessayer plus tard.",
|
||||||
"Délai d'inactivité",
|
"Délai d'inactivité",
|
||||||
"Après ce délai sans contact, l'écran s'éteint pour économiser l'énergie. Avec l'écran de veille activé, il affiche à la place un ciel étoilé tamisé avec l'heure.",
|
"Après ce délai sans contact, l'écran s'éteint pour économiser l'énergie. Avec l'écran de veille activé, il affiche à la place un ciel étoilé tamisé avec l'heure.",
|
||||||
"Eco Mode",
|
"Eco Mode ",
|
||||||
"Filtre d'itinéraire",
|
"Filtre d'itinéraire",
|
||||||
"Entrer code OACI",
|
"Entrer code OACI",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ static const char* const I18N_IT[] = {
|
|||||||
"Non è stato possibile scaricare o installare l'aggiornamento. Il dispositivo continua a funzionare senza modifiche con il firmware precedente. Controlla la connessione WiFi e riprova più tardi.",
|
"Non è stato possibile scaricare o installare l'aggiornamento. Il dispositivo continua a funzionare senza modifiche con il firmware precedente. Controlla la connessione WiFi e riprova più tardi.",
|
||||||
"Timeout schermo",
|
"Timeout schermo",
|
||||||
"Dopo questo tempo senza tocchi, lo schermo si spegne per risparmiare energia. Con il salvaschermo attivo, mostra invece un cielo stellato attenuato con l'orario.",
|
"Dopo questo tempo senza tocchi, lo schermo si spegne per risparmiare energia. Con il salvaschermo attivo, mostra invece un cielo stellato attenuato con l'orario.",
|
||||||
"Eco Mode",
|
"Eco Mode ",
|
||||||
"Filtro rotta",
|
"Filtro rotta",
|
||||||
"Inserisci ICAO",
|
"Inserisci ICAO",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ static const char* const I18N_TR[] = {
|
|||||||
"Güncelleme indirilemedi veya yüklenemedi. Cihaz değişmeden önceki donanım yazılımıyla çalışmaya devam ediyor. Lütfen WiFi bağlantınızı kontrol edip daha sonra tekrar deneyin.",
|
"Güncelleme indirilemedi veya yüklenemedi. Cihaz değişmeden önceki donanım yazılımıyla çalışmaya devam ediyor. Lütfen WiFi bağlantınızı kontrol edip daha sonra tekrar deneyin.",
|
||||||
"Ekran Zaman Aşımı",
|
"Ekran Zaman Aşımı",
|
||||||
"Bu süre boyunca dokunulmazsa ekran güç tasarrufu için kapanır. Ekran koruyucu açıksa, bunun yerine saatli, sönük bir yıldızlı gökyüzü gösterir.",
|
"Bu süre boyunca dokunulmazsa ekran güç tasarrufu için kapanır. Ekran koruyucu açıksa, bunun yerine saatli, sönük bir yıldızlı gökyüzü gösterir.",
|
||||||
"Eco Mode",
|
"Eco Mode ",
|
||||||
"Rota Filtresi",
|
"Rota Filtresi",
|
||||||
"ICAO Girin",
|
"ICAO Girin",
|
||||||
};
|
};
|
||||||
|
|||||||
13
src/main.cpp
13
src/main.cpp
@@ -626,12 +626,13 @@ void setup() {
|
|||||||
}
|
}
|
||||||
SplashScreen::setStatusLine(tft, 2, I18n::t(StringId::SPLASH_READY));
|
SplashScreen::setStatusLine(tft, 2, I18n::t(StringId::SPLASH_READY));
|
||||||
|
|
||||||
// Once IP geolocation has provided a UTC offset, set the
|
// Sync time via NTP (UTC), then set timezone to Central European
|
||||||
// timezone accordingly - timestamps (flight log, log file names)
|
// Time (Copenhagen) with automatic DST handling (CET/CEST).
|
||||||
// then show the REAL local time instead of UTC, automatically correct worldwide.
|
// This makes localtime_r() return the correct local time year-round
|
||||||
if (LocationManager::hasUtcOffset()) {
|
// regardless of when the device was booted.
|
||||||
configTime(LocationManager::utcOffsetSeconds(), 0, "pool.ntp.org", "time.nist.gov");
|
configTime(0, 0, "pool.ntp.org", "time.nist.gov");
|
||||||
}
|
setenv("TZ", "CET-1CEST-2,M3.5.0/2,M10.5.0/3", 1);
|
||||||
|
tzset();
|
||||||
|
|
||||||
AircraftTable::init();
|
AircraftTable::init();
|
||||||
AirlineLookup::init();
|
AirlineLookup::init();
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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,36 +92,25 @@ 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);
|
||||||
|
|
||||||
JsonArray assets = doc["assets"];
|
// Construct download URL from the tag name using Gitea's standard
|
||||||
for (JsonObject asset : assets) {
|
// release asset pattern. browser_download_url from the API contains
|
||||||
const char* name = asset["name"] | "";
|
// the internal IP (http://192.168.0.37:3000/...) which the ESP32
|
||||||
if (strcmp(name, "firmware.bin") == 0) {
|
// cannot use — it needs the external proxy URL.
|
||||||
const char* url = asset["browser_download_url"] | "";
|
String dlUrl = String("https://git.rasmusbendtsen.dk/rasmus/flyradar/releases/download/")
|
||||||
strncpy(info.downloadUrl, url, sizeof(info.downloadUrl) - 1);
|
+ String(tag) + "/firmware.bin";
|
||||||
break;
|
strncpy(info.downloadUrl, dlUrl.c_str(), sizeof(info.downloadUrl) - 1);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!info.downloadUrl[0]) {
|
Serial.printf("[OTA] Download URL: %s\n", info.downloadUrl);
|
||||||
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);
|
int cmp = compareVersions(info.latestVersion, Config::APP_VERSION);
|
||||||
info.result = (cmp > 0) ? CheckResult::UpdateAvailable : CheckResult::UpToDate;
|
info.result = (cmp > 0) ? CheckResult::UpdateAvailable : CheckResult::UpToDate;
|
||||||
Serial.printf("[OTA] Check successful: installed=v%s latest=v%s -> %s\n", Config::APP_VERSION,
|
Serial.printf("[OTA] Check successful: installed=v%s latest=v%s -> %s\n", Config::APP_VERSION,
|
||||||
info.latestVersion,
|
info.latestVersion,
|
||||||
info.result == CheckResult::UpdateAvailable ? "Update available" : "already up-to-date");
|
info.result == CheckResult::UpdateAvailable ? "Update available" : "already up-to-date");
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 +118,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,10 +143,11 @@ 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,
|
||||||
httpUpdate.getLastError(), httpUpdate.getLastErrorString().c_str(),
|
httpUpdate.getLastError(), httpUpdate.getLastErrorString().c_str(),
|
||||||
(unsigned)ESP.getFreeHeap());
|
(unsigned)ESP.getFreeHeap());
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user