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

@@ -18,6 +18,7 @@ lib_deps =
https://github.com/PaulStoffregen/XPT2046_Touchscreen.git https://github.com/PaulStoffregen/XPT2046_Touchscreen.git
bblanchon/ArduinoJson @ ^7.0.4 bblanchon/ArduinoJson @ ^7.0.4
mikalhart/TinyGPSPlus @ ^1.1.0 mikalhart/TinyGPSPlus @ ^1.1.0
ricmoo/QRCode @ ^0.0.1
build_flags = build_flags =
-D USER_SETUP_LOADED=1 -D USER_SETUP_LOADED=1

View File

@@ -12,6 +12,7 @@ namespace {
SemaphoreHandle_t mutex = nullptr; SemaphoreHandle_t mutex = nullptr;
char pendingHex[7] = {0}; char pendingHex[7] = {0};
char pendingCallsign[9] = {0};
bool hasPending = false; bool hasPending = false;
char cachedHex[7] = {0}; char cachedHex[7] = {0};
@@ -33,11 +34,12 @@ namespace {
} }
} }
void request(const char* hex) { 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 (strcmp(cachedHex, 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);
hasPending = true; hasPending = true;
} }
xSemaphoreGive(mutex); xSemaphoreGive(mutex);
@@ -60,11 +62,13 @@ void update() {
ensureMutex(); ensureMutex();
char hex[7] = {0}; char hex[7] = {0};
char callsign[9] = {0};
bool doWork = false; bool doWork = false;
xSemaphoreTake(mutex, portMAX_DELAY); xSemaphoreTake(mutex, portMAX_DELAY);
if (hasPending) { if (hasPending) {
strncpy(hex, pendingHex, sizeof(hex) - 1); strncpy(hex, pendingHex, sizeof(hex) - 1);
strncpy(callsign, pendingCallsign, sizeof(callsign) - 1);
doWork = true; doWork = true;
} }
xSemaphoreGive(mutex); xSemaphoreGive(mutex);
@@ -116,6 +120,28 @@ void update() {
} }
} }
// Flugroute (Start-/Zielflughafen) - derselbe adsbdb.com-Dienst wie oben,
// aber ueber den Callsign-Endpunkt statt Hex-Code abgefragt (liefert
// dafuer origin/destination-Flughafendaten zurueck). Nur versuchen, wenn
// ueberhaupt ein Rufzeichen bekannt ist - Sichtflug-Maschinen ohne
// Callsign haben ohnehin keine darueber auswertbare Route.
String trimmedCallsign = String(callsign);
trimmedCallsign.trim();
if (trimmedCallsign.length() > 0) {
client.setTimeout(4000);
String body3;
if (httpGetString(client, String("https://api.adsbdb.com/v0/callsign/") + trimmedCallsign, body3)) {
JsonDocument doc3;
DeserializationError err3 = deserializeJson(doc3, body3);
if (!err3) {
const char* originIcao = doc3["response"]["flightroute"]["origin"]["icao_code"] | "";
const char* destIcao = doc3["response"]["flightroute"]["destination"]["icao_code"] | "";
strncpy(result.routeOrigin, originIcao, sizeof(result.routeOrigin) - 1);
strncpy(result.routeDest, destIcao, sizeof(result.routeDest) - 1);
}
}
}
xSemaphoreTake(mutex, portMAX_DELAY); xSemaphoreTake(mutex, portMAX_DELAY);
strncpy(cachedHex, hex, sizeof(cachedHex) - 1); strncpy(cachedHex, hex, sizeof(cachedHex) - 1);
cached = result; cached = result;

View File

@@ -10,11 +10,19 @@ namespace AircraftDetails {
struct Info { struct Info {
bool loading = false; bool loading = false;
char model[40] = {0}; // e.g. "Airbus A320 216", empty if unknown char model[40] = {0}; // e.g. "Airbus A320 216", empty if unknown
// Departure/destination airport (ICAO code, e.g. "KIAH"/"EDDF") of
// the current flight route, looked up by callsign via the same
// adsbdb.com service used for the model fallback above - empty if
// no callsign is known or no route was found for it.
char routeOrigin[8] = {0};
char routeDest[8] = {0};
}; };
// Called from Core 1 (touch selection): marks that details should be // Called from Core 1 (touch selection): marks that details should be
// fetched for this aircraft (if not already done). // fetched for this aircraft (if not already done). callsign may be
void request(const char* hex); // empty (no route lookup is attempted in that case).
void request(const char* hex, const char* callsign);
// Called from Core 1 to get the current (possibly still incomplete) // Called from Core 1 to get the current (possibly still incomplete)
// state for 'hex'. // state for 'hex'.

View File

@@ -214,7 +214,7 @@ bool run(TFT_eSPI& tft) {
} else { } else {
for (uint8_t i = 0; i < visibleRowCount; i++) { for (uint8_t i = 0; i < visibleRowCount; i++) {
if (rowRects[i].contains(tap.x, tap.y)) { if (rowRects[i].contains(tap.x, tap.y)) {
RadarScreen::selectAircraft(snapshot[scrollTop + i].hex); RadarScreen::selectAircraft(snapshot[scrollTop + i].hex, snapshot[scrollTop + i].callsign);
selected = true; selected = true;
break; break;
} }

View File

@@ -303,6 +303,19 @@ enum class StringId : uint8_t {
// instead of waiting it out. // instead of waiting it out.
FIRST_RUN_COMPLETE_TAP_TO_SKIP, FIRST_RUN_COMPLETE_TAP_TO_SKIP,
// Zeilenpraefix im Radar-Detailpanel (radar_screen.cpp::drawDetailPanel)
// fuer die Flugroute (Start- -> Zielflughafen, ICAO-Code), abgefragt
// ueber AircraftDetails per Rufzeichen. Faellt wie MODEL/TYPE/SQUAWK auf
// DETAIL_UNKNOWN zurueck, wenn kein Rufzeichen bekannt ist oder keine
// Route gefunden wurde.
DETAIL_ROUTE,
// Hinweiszeile ueber dem QR-Code auf dem neuen QR-Unterscreen der
// Logbuch/WebUI-Seite (webui_screen.cpp::runQrScreen) - der WEBUI_TITLE-
// String wird fuer die Kopfzeile wiederverwendet, kein eigener Titel
// noetig.
WEBUI_QR_HINT,
COUNT COUNT
}; };

View File

@@ -238,6 +238,8 @@ static const char* const I18N_DE[] = {
"Lösche alle Daten...", "Lösche alle Daten...",
"Zurücksetzen fehlgeschlagen.", "Zurücksetzen fehlgeschlagen.",
"Tippen zum Überspringen", "Tippen zum Überspringen",
"Route: ",
"Mit dem Handy scannen:",
}; };
static_assert(sizeof(I18N_DE) / sizeof(I18N_DE[0]) == (size_t)StringId::COUNT, static_assert(sizeof(I18N_DE) / sizeof(I18N_DE[0]) == (size_t)StringId::COUNT,

View File

@@ -239,6 +239,8 @@ static const char* const I18N_EN[] = {
"Deleting all data...", "Deleting all data...",
"Reset failed.", "Reset failed.",
"Tap to skip", "Tap to skip",
"Route: ",
"Scan with your phone:",
}; };
static_assert(sizeof(I18N_EN) / sizeof(I18N_EN[0]) == (size_t)StringId::COUNT, static_assert(sizeof(I18N_EN) / sizeof(I18N_EN[0]) == (size_t)StringId::COUNT,

View File

@@ -238,6 +238,8 @@ static const char* const I18N_ES[] = {
"Eliminando todos los datos...", "Eliminando todos los datos...",
"Error al reiniciar.", "Error al reiniciar.",
"Toca para saltar", "Toca para saltar",
"Ruta: ",
"Escanea con el móvil:",
}; };
static_assert(sizeof(I18N_ES) / sizeof(I18N_ES[0]) == (size_t)StringId::COUNT, static_assert(sizeof(I18N_ES) / sizeof(I18N_ES[0]) == (size_t)StringId::COUNT,

View File

@@ -238,6 +238,8 @@ static const char* const I18N_FR[] = {
"Suppression de toutes les données...", "Suppression de toutes les données...",
"Échec de la réinitialisation.", "Échec de la réinitialisation.",
"Toucher pour passer", "Toucher pour passer",
"Trajet : ",
"Scanner avec le téléphone :",
}; };
static_assert(sizeof(I18N_FR) / sizeof(I18N_FR[0]) == (size_t)StringId::COUNT, static_assert(sizeof(I18N_FR) / sizeof(I18N_FR[0]) == (size_t)StringId::COUNT,

View File

@@ -238,6 +238,8 @@ static const char* const I18N_IT[] = {
"Eliminazione di tutti i dati...", "Eliminazione di tutti i dati...",
"Reimpostazione fallita.", "Reimpostazione fallita.",
"Tocca per saltare", "Tocca per saltare",
"Rotta: ",
"Scansiona con il telefono:",
}; };
static_assert(sizeof(I18N_IT) / sizeof(I18N_IT[0]) == (size_t)StringId::COUNT, static_assert(sizeof(I18N_IT) / sizeof(I18N_IT[0]) == (size_t)StringId::COUNT,

View File

@@ -238,6 +238,8 @@ static const char* const I18N_TR[] = {
"Tüm veriler siliniyor...", "Tüm veriler siliniyor...",
"Sıfırlama başarısız.", "Sıfırlama başarısız.",
"Atlamak için dokun", "Atlamak için dokun",
"Rota: ",
"Telefonla tarayın:",
}; };
static_assert(sizeof(I18N_TR) / sizeof(I18N_TR[0]) == (size_t)StringId::COUNT, static_assert(sizeof(I18N_TR) / sizeof(I18N_TR[0]) == (size_t)StringId::COUNT,

View File

@@ -57,7 +57,12 @@ namespace {
return L; 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 { struct HitPoint {
int16_t x, y; int16_t x, y;
@@ -70,6 +75,7 @@ namespace {
bool isEmergency; bool isEmergency;
bool isWatched; bool isWatched;
bool isGroundVehicle; bool isGroundVehicle;
bool isRotorcraft;
}; };
constexpr uint8_t MAX_HIT_POINTS = Config::MAX_TRACKED_AIRCRAFT; constexpr uint8_t MAX_HIT_POINTS = Config::MAX_TRACKED_AIRCRAFT;
HitPoint hitPoints[MAX_HIT_POINTS]; 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); 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) { void printLineTruncated(TFT_eSPI& gfx, int16_t x, int16_t y, int16_t maxWidth, const String& text) {
String s = text; String s = text;
if (gfx.textWidth(s) > maxWidth) { if (gfx.textWidth(s) > maxWidth) {
@@ -507,7 +526,7 @@ namespace {
bool valid = false; bool valid = false;
char hex[7] = {0}; char hex[7] = {0};
String callsignText; 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; PanelState lastPanel;
@@ -580,6 +599,7 @@ namespace {
advanceAndDrawMarqueeLine(gfx, lastPanel.airline); advanceAndDrawMarqueeLine(gfx, lastPanel.airline);
advanceAndDrawMarqueeLine(gfx, lastPanel.model); advanceAndDrawMarqueeLine(gfx, lastPanel.model);
advanceAndDrawMarqueeLine(gfx, lastPanel.type); advanceAndDrawMarqueeLine(gfx, lastPanel.type);
advanceAndDrawMarqueeLine(gfx, lastPanel.route);
advanceAndDrawMarqueeLine(gfx, lastPanel.alt); advanceAndDrawMarqueeLine(gfx, lastPanel.alt);
advanceAndDrawMarqueeLine(gfx, lastPanel.speed); advanceAndDrawMarqueeLine(gfx, lastPanel.speed);
advanceAndDrawMarqueeLine(gfx, lastPanel.climb); advanceAndDrawMarqueeLine(gfx, lastPanel.climb);
@@ -633,6 +653,24 @@ namespace {
updateMarqueeLine(gfx, y, LINE_H, textMaxWidth, TFT_GREEN, lastPanel.type, typeLine, forceFull); updateMarqueeLine(gfx, y, LINE_H, textMaxWidth, TFT_GREEN, lastPanel.type, typeLine, forceFull);
y += LINE_H; 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]; char buf[48];
snprintf(buf, sizeof(buf), "%s%.0fm / %.0fft", I18n::t(StringId::DETAIL_ALT), snprintf(buf, sizeof(buf), "%s%.0fm / %.0fft", I18n::t(StringId::DETAIL_ALT),
Units::feetToMeters((float)a.altBaroFt), (float)a.altBaroFt); 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- // oben) - eigene Farbe/Marker statt der hoehenbasierten Flugzeug-
// Darstellung, siehe drawGroundVehicleMarker()/colorForGroundVehicle(). // Darstellung, siehe drawGroundVehicleMarker()/colorForGroundVehicle().
bool isGroundVehicle = a.category[0] == 'C'; 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); uint16_t color = isGroundVehicle ? colorForGroundVehicle(tft) : colorForAltitude(tft, a.altBaroFt);
bool isSelected = selectedHex[0] && strcmp(a.hex, selectedHex) == 0; bool isSelected = selectedHex[0] && strcmp(a.hex, selectedHex) == 0;
bool isEmergency = SettingsStore::emergencyAlertEnabled() && isEmergencySquawk(a.squawk); bool isEmergency = SettingsStore::emergencyAlertEnabled() && isEmergencySquawk(a.squawk);
@@ -873,6 +912,8 @@ void render(TFT_eSPI& tft, int16_t top) {
} }
if (isGroundVehicle) { if (isGroundVehicle) {
drawGroundVehicleMarker(tft, pt.x, pt.y, color); drawGroundVehicleMarker(tft, pt.x, pt.y, color);
} else if (isRotorcraft) {
drawHelicopterMarker(tft, pt.x, pt.y, color);
} else { } else {
drawAircraftMarker(tft, pt.x, pt.y, a.headingDeg, color); 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].isEmergency = isEmergency;
hitPoints[i].isWatched = isWatched; hitPoints[i].isWatched = isWatched;
hitPoints[i].isGroundVehicle = isGroundVehicle; hitPoints[i].isGroundVehicle = isGroundVehicle;
hitPoints[i].isRotorcraft = isRotorcraft;
strncpy(hitPoints[i].hex, a.hex, sizeof(hitPoints[i].hex) - 1); strncpy(hitPoints[i].hex, a.hex, sizeof(hitPoints[i].hex) - 1);
strncpy(hitPoints[i].callsign, a.callsign, sizeof(hitPoints[i].callsign) - 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. // beruehren.
if (hp.isGroundVehicle) { if (hp.isGroundVehicle) {
drawGroundVehicleMarker(tft, hp.x, hp.y, TFT_BLACK); drawGroundVehicleMarker(tft, hp.x, hp.y, TFT_BLACK);
} else if (hp.isRotorcraft) {
drawHelicopterMarker(tft, hp.x, hp.y, TFT_BLACK);
} else { } else {
drawAircraftMarker(tft, hp.x, hp.y, hp.headingDeg, TFT_BLACK); 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) { if (hp.isGroundVehicle) {
drawGroundVehicleMarker(tft, hp.x, hp.y, hp.color); drawGroundVehicleMarker(tft, hp.x, hp.y, hp.color);
} else if (hp.isRotorcraft) {
drawHelicopterMarker(tft, hp.x, hp.y, hp.color);
} else { } else {
drawAircraftMarker(tft, hp.x, hp.y, hp.headingDeg, hp.color); 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; int16_t dy = y - hitPoints[i].y;
if (dx * dx + dy * dy <= 12 * 12) { if (dx * dx + dy * dy <= 12 * 12) {
strncpy(selectedHex, hitPoints[i].hex, sizeof(selectedHex) - 1); strncpy(selectedHex, hitPoints[i].hex, sizeof(selectedHex) - 1);
AircraftDetails::request(hitPoints[i].hex); AircraftDetails::request(hitPoints[i].hex, hitPoints[i].callsign);
return true; return true;
} }
} }
@@ -1129,7 +1175,7 @@ bool handleTap(int16_t x, int16_t y, int16_t top) {
int16_t dy = y - hitPoints[i].y; int16_t dy = y - hitPoints[i].y;
if (dx * dx + dy * dy <= 12 * 12) { if (dx * dx + dy * dy <= 12 * 12) {
strncpy(selectedHex, hitPoints[i].hex, sizeof(selectedHex) - 1); strncpy(selectedHex, hitPoints[i].hex, sizeof(selectedHex) - 1);
AircraftDetails::request(hitPoints[i].hex); AircraftDetails::request(hitPoints[i].hex, hitPoints[i].callsign);
return true; return true;
} }
} }
@@ -1152,9 +1198,9 @@ bool handleTap(int16_t x, int16_t y, int16_t top) {
return true; return true;
} }
void selectAircraft(const char* hex) { void selectAircraft(const char* hex, const char* callsign) {
strncpy(selectedHex, hex, sizeof(selectedHex) - 1); strncpy(selectedHex, hex, sizeof(selectedHex) - 1);
AircraftDetails::request(hex); AircraftDetails::request(hex, callsign);
// Erzwingt einen kompletten Neuaufbau des Detail-Panels beim naechsten // Erzwingt einen kompletten Neuaufbau des Detail-Panels beim naechsten
// render() - wichtig, falls der Bildschirm zwischenzeitlich von einem // render() - wichtig, falls der Bildschirm zwischenzeitlich von einem
// anderen Screen (z.B. der Flugzeugliste) ueberschrieben wurde, sonst // anderen Screen (z.B. der Flugzeugliste) ueberschrieben wurde, sonst

View File

@@ -12,7 +12,7 @@ namespace RadarScreen {
// aus, nicht per Antippen auf dem Radar) - damit beim naechsten render() // aus, nicht per Antippen auf dem Radar) - damit beim naechsten render()
// sofort das Detail-Panel fuer dieses Flugzeug erscheint, so als haette // sofort das Detail-Panel fuer dieses Flugzeug erscheint, so als haette
// man es direkt im Radar angetippt. // man es direkt im Radar angetippt.
void selectAircraft(const char* hex); void selectAircraft(const char* hex, const char* callsign);
struct EmergencyInfo { struct EmergencyInfo {
bool active = false; bool active = false;

View File

@@ -4,6 +4,7 @@
#include "config.h" #include "config.h"
#include "i18n.h" #include "i18n.h"
#include <WiFi.h> #include <WiFi.h>
#include <qrcode.h>
namespace WebUiScreen { namespace WebUiScreen {
@@ -53,6 +54,63 @@ namespace {
} }
return y; return y;
} }
// QR-Code-Unterscreen: zeigt die WebUI-URL als QR-Code, damit man sie
// nicht mehr von Hand ins Handy tippen muss. Nutzt die ricmoo/QRCode-
// Bibliothek (kein dynamisches Allozieren, Version fest auf 4/33x33
// Module mit niedrigster Fehlerkorrektur, reicht locker fuer eine kurze
// "http://192.168.x.x/"-URL).
void runQrScreen(TFT_eSPI& tft, const String& url) {
QRCode qrcode;
uint8_t qrData[qrcode_getBufferSize(4)];
qrcode_initText(&qrcode, qrData, 4, ECC_LOW, url.c_str());
constexpr int16_t MODULE_PX = 6;
constexpr int16_t QUIET_ZONE = 4;
int16_t qrPx = qrcode.size * MODULE_PX;
int16_t boxPx = qrPx + 2 * QUIET_ZONE * MODULE_PX;
int16_t boxX = (Config::SCREEN_WIDTH - boxPx) / 2;
int16_t boxY = 30;
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 14);
tft.println(I18n::t(StringId::WEBUI_TITLE));
tft.setTextDatum(MC_DATUM);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.drawString(I18n::t(StringId::WEBUI_QR_HINT), Config::SCREEN_WIDTH / 2, boxY - 6);
tft.setTextDatum(TL_DATUM);
tft.fillRect(boxX, boxY + 10, boxPx, boxPx, TFT_WHITE);
for (uint8_t y = 0; y < qrcode.size; y++) {
for (uint8_t x = 0; x < qrcode.size; x++) {
if (qrcode_getModule(&qrcode, x, y)) {
int16_t px = boxX + QUIET_ZONE * MODULE_PX + x * MODULE_PX;
int16_t py = boxY + 10 + QUIET_ZONE * MODULE_PX + y * MODULE_PX;
tft.fillRect(px, py, MODULE_PX, MODULE_PX, TFT_BLACK);
}
}
}
int16_t urlY = boxY + 10 + boxPx + 16;
tft.setTextDatum(MC_DATUM);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.drawString(url, Config::SCREEN_WIDTH / 2, urlY);
tft.setTextDatum(TL_DATUM);
Rect backBtn = {10, (int16_t)(Config::SCREEN_HEIGHT - 50), (int16_t)(Config::SCREEN_WIDTH - 20), 40};
drawButton(tft, backBtn, I18n::t(StringId::BACK));
while (true) {
TouchInput::Point tap;
if (TouchInput::wasTapped(tap)) {
if (backBtn.contains(tap.x, tap.y)) return;
}
MenuStars::update(tft);
delay(20);
}
}
} }
void run(TFT_eSPI& tft) { void run(TFT_eSPI& tft) {
@@ -90,6 +148,8 @@ void run(TFT_eSPI& tft) {
Rect downBtn = {190, (int16_t)(Config::SCREEN_HEIGHT - 50), 38, 40}; Rect downBtn = {190, (int16_t)(Config::SCREEN_HEIGHT - 50), 38, 40};
constexpr int16_t SCROLL_STEP = 48; constexpr int16_t SCROLL_STEP = 48;
Rect qrBtn = {(int16_t)(Config::SCREEN_WIDTH - 40), 2, 30, 24};
auto redraw = [&]() { auto redraw = [&]() {
tft.fillScreen(TFT_BLACK); tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK); tft.setTextColor(TFT_GREEN, TFT_BLACK);
@@ -117,6 +177,9 @@ void run(TFT_eSPI& tft) {
drawButton(tft, upBtn, "^"); drawButton(tft, upBtn, "^");
drawButton(tft, downBtn, "v"); drawButton(tft, downBtn, "v");
} }
if (wifiConnected) {
drawButton(tft, qrBtn, "QR");
}
}; };
redraw(); redraw();
@@ -125,7 +188,10 @@ void run(TFT_eSPI& tft) {
TouchInput::Point tap; TouchInput::Point tap;
if (TouchInput::wasTapped(tap)) { if (TouchInput::wasTapped(tap)) {
if (backBtn.contains(tap.x, tap.y)) return; if (backBtn.contains(tap.x, tap.y)) return;
if (scrollable && upBtn.contains(tap.x, tap.y) && scrollY > 0) { if (wifiConnected && qrBtn.contains(tap.x, tap.y)) {
runQrScreen(tft, urlLine);
redraw();
} else if (scrollable && upBtn.contains(tap.x, tap.y) && scrollY > 0) {
scrollY -= SCROLL_STEP; scrollY -= SCROLL_STEP;
if (scrollY < 0) scrollY = 0; if (scrollY < 0) scrollY = 0;
redraw(); redraw();