This commit is contained in:
@@ -15,29 +15,62 @@ namespace {
|
||||
char pendingCallsign[9] = {0};
|
||||
bool hasPending = false;
|
||||
|
||||
char cachedHex[7] = {0};
|
||||
Info cached;
|
||||
constexpr uint8_t CACHE_SIZE = 4;
|
||||
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() {
|
||||
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;
|
||||
http.setTimeout(5000);
|
||||
if (!http.begin(client, url)) return false;
|
||||
http.setTimeout(timeoutMs);
|
||||
if (!http.begin(persistentClient, url)) return false;
|
||||
http.setReuse(true);
|
||||
int code = http.GET();
|
||||
bool ok = (code == HTTP_CODE_OK);
|
||||
if (ok) outBody = http.getString();
|
||||
http.end();
|
||||
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) {
|
||||
ensureMutex();
|
||||
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(pendingCallsign, callsign ? callsign : "", sizeof(pendingCallsign) - 1);
|
||||
hasPending = true;
|
||||
@@ -49,8 +82,9 @@ Info get(const char* hex) {
|
||||
ensureMutex();
|
||||
xSemaphoreTake(mutex, portMAX_DELAY);
|
||||
Info out;
|
||||
if (strcmp(cachedHex, hex) == 0) {
|
||||
out = cached;
|
||||
int slot = findCacheSlot(hex);
|
||||
if (slot >= 0) {
|
||||
out = cache[slot].info;
|
||||
} else if (strcmp(pendingHex, hex) == 0 && hasPending) {
|
||||
out.loading = true;
|
||||
}
|
||||
@@ -77,15 +111,10 @@ void update() {
|
||||
|
||||
Info result;
|
||||
|
||||
WiFiClientSecure client;
|
||||
client.setInsecure();
|
||||
|
||||
// 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);
|
||||
// Model lookup — try hexdb.io first, fallback to adsbdb.com.
|
||||
// Timeout reduced to 2s per source (was 3-4s) since we chain fallbacks.
|
||||
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;
|
||||
DeserializationError err = deserializeJson(doc, body);
|
||||
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]) {
|
||||
client.setTimeout(4000);
|
||||
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;
|
||||
DeserializationError err2 = deserializeJson(doc2, body2);
|
||||
if (!err2) {
|
||||
@@ -120,25 +145,7 @@ void update() {
|
||||
}
|
||||
}
|
||||
|
||||
// Flight route (origin/destination airport) - now via a chain of THREE
|
||||
// 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.
|
||||
// Route lookup — via chain of three sources, 2s timeout each.
|
||||
String trimmedCallsign = String(callsign);
|
||||
trimmedCallsign.trim();
|
||||
trimmedCallsign.toUpperCase();
|
||||
@@ -152,11 +159,9 @@ void update() {
|
||||
};
|
||||
|
||||
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)) {
|
||||
String folder = trimmedCallsign.substring(0, 2);
|
||||
if (httpGetString(String("https://vrs-standing-data.adsb.lol/routes/") + folder + "/" + trimmedCallsign + ".json", body3, 2000)) {
|
||||
JsonDocument doc3;
|
||||
if (!deserializeJson(doc3, body3)) {
|
||||
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]) {
|
||||
client.setTimeout(3000);
|
||||
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;
|
||||
if (!deserializeJson(doc4, body4)) {
|
||||
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])) {
|
||||
client.setTimeout(4000);
|
||||
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;
|
||||
if (!deserializeJson(doc5, body5)) {
|
||||
const char* originIcao = doc5["response"]["flightroute"]["origin"]["icao_code"] | "";
|
||||
@@ -196,10 +197,9 @@ void update() {
|
||||
}
|
||||
|
||||
xSemaphoreTake(mutex, portMAX_DELAY);
|
||||
strncpy(cachedHex, hex, sizeof(cachedHex) - 1);
|
||||
cached = result;
|
||||
storeInCache(hex, result);
|
||||
hasPending = false;
|
||||
xSemaphoreGive(mutex);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user