Add flight route lookup, helicopter marker, and QR code for WebUI

Detail panel now shows origin/destination via adsbdb.com callsign
lookup. Rotorcraft (ADS-B category A7) get a distinct rotor-cross
marker instead of a heading arrowhead, same rationale as the existing
ground-vehicle marker. WebUI info screen gets a QR button (when WiFi
is connected) that renders the local WebUI URL as a scannable code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Eiswolf-BG
2026-08-12 16:52:22 +02:00
parent 467d30d8cd
commit 32f8e873a9
14 changed files with 186 additions and 14 deletions

View File

@@ -57,7 +57,12 @@ namespace {
return L;
}
constexpr int16_t DETAIL_PANEL_H = 264;
// +22px gegenueber frueher (264) fuer die neue Route-Zeile (siehe
// drawDetailPanel()) - beeinflusst NUR die Groesse des Detail-Panel-
// Overlays, nicht das normale Radar-Layout (computeLayout() oben
// richtet sich ausschliesslich nach infoBarHeight(), nicht nach
// DETAIL_PANEL_H).
constexpr int16_t DETAIL_PANEL_H = 286;
struct HitPoint {
int16_t x, y;
@@ -70,6 +75,7 @@ namespace {
bool isEmergency;
bool isWatched;
bool isGroundVehicle;
bool isRotorcraft;
};
constexpr uint8_t MAX_HIT_POINTS = Config::MAX_TRACKED_AIRCRAFT;
HitPoint hitPoints[MAX_HIT_POINTS];
@@ -333,6 +339,19 @@ namespace {
gfx.fillRect((int16_t)(x - HALF), (int16_t)(y - HALF), (int16_t)(2 * HALF), (int16_t)(2 * HALF), color);
}
// Eigener Marker fuer Hubschrauber (ADS-B-Emitter-Kategorie "A7" =
// Rotorcraft) - ein gefuellter Kreis mit durchgehendem Rotorkreuz statt
// Pfeilkopf, da Hubschrauber im Schwebeflug keinen aussagekraeftigen
// "nach vorne"-Kurs wie ein Flugzeug haben (gleiche Ueberlegung wie beim
// Bodenfahrzeug-Marker, der aus demselben Grund ebenfalls ohne
// Kurslinie auskommt).
void drawHelicopterMarker(TFT_eSPI& gfx, int16_t x, int16_t y, uint16_t color) {
constexpr int16_t ROTOR_LEN = 8;
gfx.fillCircle(x, y, 4, color);
gfx.drawLine((int16_t)(x - ROTOR_LEN), y, (int16_t)(x + ROTOR_LEN), y, color);
gfx.drawLine(x, (int16_t)(y - ROTOR_LEN), x, (int16_t)(y + ROTOR_LEN), color);
}
void printLineTruncated(TFT_eSPI& gfx, int16_t x, int16_t y, int16_t maxWidth, const String& text) {
String s = text;
if (gfx.textWidth(s) > maxWidth) {
@@ -507,7 +526,7 @@ namespace {
bool valid = false;
char hex[7] = {0};
String callsignText;
LineMarquee airline, model, type, alt, speed, climb, distHeading, squawk, seats;
LineMarquee airline, model, type, route, alt, speed, climb, distHeading, squawk, seats;
};
PanelState lastPanel;
@@ -580,6 +599,7 @@ namespace {
advanceAndDrawMarqueeLine(gfx, lastPanel.airline);
advanceAndDrawMarqueeLine(gfx, lastPanel.model);
advanceAndDrawMarqueeLine(gfx, lastPanel.type);
advanceAndDrawMarqueeLine(gfx, lastPanel.route);
advanceAndDrawMarqueeLine(gfx, lastPanel.alt);
advanceAndDrawMarqueeLine(gfx, lastPanel.speed);
advanceAndDrawMarqueeLine(gfx, lastPanel.climb);
@@ -633,6 +653,24 @@ namespace {
updateMarqueeLine(gfx, y, LINE_H, textMaxWidth, TFT_GREEN, lastPanel.type, typeLine, forceFull);
y += LINE_H;
// Start-/Zielflughafen (ICAO-Code) ueber AircraftDetails::get() -
// wird zusammen mit dem Modell abgefragt (siehe aircraft_details.cpp),
// aber ueber das Rufzeichen statt den Hex-Code aufgeloest. Ohne
// Rufzeichen (z.B. manche Sichtflug-Maschinen) bleibt die Route
// grundsaetzlich unbekannt, kein Abruf noetig.
String routeLine;
if (!a.callsign[0]) {
routeLine = String(I18n::t(StringId::DETAIL_ROUTE)) + I18n::t(StringId::DETAIL_UNKNOWN);
} else if (details.loading) {
routeLine = String(I18n::t(StringId::DETAIL_ROUTE)) + I18n::t(StringId::DETAIL_LOADING_DOTS);
} else if (details.routeOrigin[0] && details.routeDest[0]) {
routeLine = String(I18n::t(StringId::DETAIL_ROUTE)) + details.routeOrigin + " -> " + details.routeDest;
} else {
routeLine = String(I18n::t(StringId::DETAIL_ROUTE)) + I18n::t(StringId::DETAIL_UNKNOWN);
}
updateMarqueeLine(gfx, y, LINE_H, textMaxWidth, TFT_GREEN, lastPanel.route, routeLine, forceFull);
y += LINE_H;
char buf[48];
snprintf(buf, sizeof(buf), "%s%.0fm / %.0fft", I18n::t(StringId::DETAIL_ALT),
Units::feetToMeters((float)a.altBaroFt), (float)a.altBaroFt);
@@ -860,6 +898,7 @@ void render(TFT_eSPI& tft, int16_t top) {
// oben) - eigene Farbe/Marker statt der hoehenbasierten Flugzeug-
// Darstellung, siehe drawGroundVehicleMarker()/colorForGroundVehicle().
bool isGroundVehicle = a.category[0] == 'C';
bool isRotorcraft = a.category[0] == 'A' && a.category[1] == '7';
uint16_t color = isGroundVehicle ? colorForGroundVehicle(tft) : colorForAltitude(tft, a.altBaroFt);
bool isSelected = selectedHex[0] && strcmp(a.hex, selectedHex) == 0;
bool isEmergency = SettingsStore::emergencyAlertEnabled() && isEmergencySquawk(a.squawk);
@@ -873,6 +912,8 @@ void render(TFT_eSPI& tft, int16_t top) {
}
if (isGroundVehicle) {
drawGroundVehicleMarker(tft, pt.x, pt.y, color);
} else if (isRotorcraft) {
drawHelicopterMarker(tft, pt.x, pt.y, color);
} else {
drawAircraftMarker(tft, pt.x, pt.y, a.headingDeg, color);
}
@@ -898,6 +939,7 @@ void render(TFT_eSPI& tft, int16_t top) {
hitPoints[i].isEmergency = isEmergency;
hitPoints[i].isWatched = isWatched;
hitPoints[i].isGroundVehicle = isGroundVehicle;
hitPoints[i].isRotorcraft = isRotorcraft;
strncpy(hitPoints[i].hex, a.hex, sizeof(hitPoints[i].hex) - 1);
strncpy(hitPoints[i].callsign, a.callsign, sizeof(hitPoints[i].callsign) - 1);
}
@@ -1007,6 +1049,8 @@ void tick(TFT_eSPI& tft, int16_t top, uint32_t deltaMs) {
// beruehren.
if (hp.isGroundVehicle) {
drawGroundVehicleMarker(tft, hp.x, hp.y, TFT_BLACK);
} else if (hp.isRotorcraft) {
drawHelicopterMarker(tft, hp.x, hp.y, TFT_BLACK);
} else {
drawAircraftMarker(tft, hp.x, hp.y, hp.headingDeg, TFT_BLACK);
}
@@ -1023,6 +1067,8 @@ void tick(TFT_eSPI& tft, int16_t top, uint32_t deltaMs) {
if (hp.isGroundVehicle) {
drawGroundVehicleMarker(tft, hp.x, hp.y, hp.color);
} else if (hp.isRotorcraft) {
drawHelicopterMarker(tft, hp.x, hp.y, hp.color);
} else {
drawAircraftMarker(tft, hp.x, hp.y, hp.headingDeg, hp.color);
}
@@ -1108,7 +1154,7 @@ bool handleTap(int16_t x, int16_t y, int16_t top) {
int16_t dy = y - hitPoints[i].y;
if (dx * dx + dy * dy <= 12 * 12) {
strncpy(selectedHex, hitPoints[i].hex, sizeof(selectedHex) - 1);
AircraftDetails::request(hitPoints[i].hex);
AircraftDetails::request(hitPoints[i].hex, hitPoints[i].callsign);
return true;
}
}
@@ -1129,7 +1175,7 @@ bool handleTap(int16_t x, int16_t y, int16_t top) {
int16_t dy = y - hitPoints[i].y;
if (dx * dx + dy * dy <= 12 * 12) {
strncpy(selectedHex, hitPoints[i].hex, sizeof(selectedHex) - 1);
AircraftDetails::request(hitPoints[i].hex);
AircraftDetails::request(hitPoints[i].hex, hitPoints[i].callsign);
return true;
}
}
@@ -1152,9 +1198,9 @@ bool handleTap(int16_t x, int16_t y, int16_t top) {
return true;
}
void selectAircraft(const char* hex) {
void selectAircraft(const char* hex, const char* callsign) {
strncpy(selectedHex, hex, sizeof(selectedHex) - 1);
AircraftDetails::request(hex);
AircraftDetails::request(hex, callsign);
// Erzwingt einen kompletten Neuaufbau des Detail-Panels beim naechsten
// render() - wichtig, falls der Bildschirm zwischenzeitlich von einem
// anderen Screen (z.B. der Flugzeugliste) ueberschrieben wurde, sonst