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:
@@ -12,6 +12,7 @@ namespace {
|
||||
SemaphoreHandle_t mutex = nullptr;
|
||||
|
||||
char pendingHex[7] = {0};
|
||||
char pendingCallsign[9] = {0};
|
||||
bool hasPending = false;
|
||||
|
||||
char cachedHex[7] = {0};
|
||||
@@ -33,11 +34,12 @@ namespace {
|
||||
}
|
||||
}
|
||||
|
||||
void request(const char* hex) {
|
||||
void request(const char* hex, const char* callsign) {
|
||||
ensureMutex();
|
||||
xSemaphoreTake(mutex, portMAX_DELAY);
|
||||
if (strcmp(cachedHex, hex) != 0 && strcmp(pendingHex, hex) != 0) {
|
||||
strncpy(pendingHex, hex, sizeof(pendingHex) - 1);
|
||||
strncpy(pendingCallsign, callsign ? callsign : "", sizeof(pendingCallsign) - 1);
|
||||
hasPending = true;
|
||||
}
|
||||
xSemaphoreGive(mutex);
|
||||
@@ -60,11 +62,13 @@ void update() {
|
||||
ensureMutex();
|
||||
|
||||
char hex[7] = {0};
|
||||
char callsign[9] = {0};
|
||||
bool doWork = false;
|
||||
|
||||
xSemaphoreTake(mutex, portMAX_DELAY);
|
||||
if (hasPending) {
|
||||
strncpy(hex, pendingHex, sizeof(hex) - 1);
|
||||
strncpy(callsign, pendingCallsign, sizeof(callsign) - 1);
|
||||
doWork = true;
|
||||
}
|
||||
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);
|
||||
strncpy(cachedHex, hex, sizeof(cachedHex) - 1);
|
||||
cached = result;
|
||||
@@ -123,4 +149,4 @@ void update() {
|
||||
xSemaphoreGive(mutex);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,19 @@ namespace AircraftDetails {
|
||||
struct Info {
|
||||
bool loading = false;
|
||||
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
|
||||
// fetched for this aircraft (if not already done).
|
||||
void request(const char* hex);
|
||||
// fetched for this aircraft (if not already done). callsign may be
|
||||
// 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)
|
||||
// state for 'hex'.
|
||||
@@ -24,4 +32,4 @@ namespace AircraftDetails {
|
||||
// (blocking HTTPS call, but that's fine - runs in the background and
|
||||
// only briefly delays the next ADS-B poll).
|
||||
void update();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +214,7 @@ bool run(TFT_eSPI& tft) {
|
||||
} else {
|
||||
for (uint8_t i = 0; i < visibleRowCount; i++) {
|
||||
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;
|
||||
break;
|
||||
}
|
||||
|
||||
13
src/i18n.h
13
src/i18n.h
@@ -303,6 +303,19 @@ enum class StringId : uint8_t {
|
||||
// instead of waiting it out.
|
||||
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
|
||||
};
|
||||
|
||||
|
||||
@@ -238,6 +238,8 @@ static const char* const I18N_DE[] = {
|
||||
"Lösche alle Daten...",
|
||||
"Zurücksetzen fehlgeschlagen.",
|
||||
"Tippen zum Überspringen",
|
||||
"Route: ",
|
||||
"Mit dem Handy scannen:",
|
||||
};
|
||||
|
||||
static_assert(sizeof(I18N_DE) / sizeof(I18N_DE[0]) == (size_t)StringId::COUNT,
|
||||
|
||||
@@ -239,6 +239,8 @@ static const char* const I18N_EN[] = {
|
||||
"Deleting all data...",
|
||||
"Reset failed.",
|
||||
"Tap to skip",
|
||||
"Route: ",
|
||||
"Scan with your phone:",
|
||||
};
|
||||
|
||||
static_assert(sizeof(I18N_EN) / sizeof(I18N_EN[0]) == (size_t)StringId::COUNT,
|
||||
|
||||
@@ -238,6 +238,8 @@ static const char* const I18N_ES[] = {
|
||||
"Eliminando todos los datos...",
|
||||
"Error al reiniciar.",
|
||||
"Toca para saltar",
|
||||
"Ruta: ",
|
||||
"Escanea con el móvil:",
|
||||
};
|
||||
|
||||
static_assert(sizeof(I18N_ES) / sizeof(I18N_ES[0]) == (size_t)StringId::COUNT,
|
||||
|
||||
@@ -238,6 +238,8 @@ static const char* const I18N_FR[] = {
|
||||
"Suppression de toutes les données...",
|
||||
"Échec de la réinitialisation.",
|
||||
"Toucher pour passer",
|
||||
"Trajet : ",
|
||||
"Scanner avec le téléphone :",
|
||||
};
|
||||
|
||||
static_assert(sizeof(I18N_FR) / sizeof(I18N_FR[0]) == (size_t)StringId::COUNT,
|
||||
|
||||
@@ -238,6 +238,8 @@ static const char* const I18N_IT[] = {
|
||||
"Eliminazione di tutti i dati...",
|
||||
"Reimpostazione fallita.",
|
||||
"Tocca per saltare",
|
||||
"Rotta: ",
|
||||
"Scansiona con il telefono:",
|
||||
};
|
||||
|
||||
static_assert(sizeof(I18N_IT) / sizeof(I18N_IT[0]) == (size_t)StringId::COUNT,
|
||||
|
||||
@@ -238,6 +238,8 @@ static const char* const I18N_TR[] = {
|
||||
"Tüm veriler siliniyor...",
|
||||
"Sıfırlama başarısız.",
|
||||
"Atlamak için dokun",
|
||||
"Rota: ",
|
||||
"Telefonla tarayın:",
|
||||
};
|
||||
|
||||
static_assert(sizeof(I18N_TR) / sizeof(I18N_TR[0]) == (size_t)StringId::COUNT,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace RadarScreen {
|
||||
// aus, nicht per Antippen auf dem Radar) - damit beim naechsten render()
|
||||
// sofort das Detail-Panel fuer dieses Flugzeug erscheint, so als haette
|
||||
// man es direkt im Radar angetippt.
|
||||
void selectAircraft(const char* hex);
|
||||
void selectAircraft(const char* hex, const char* callsign);
|
||||
|
||||
struct EmergencyInfo {
|
||||
bool active = false;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "config.h"
|
||||
#include "i18n.h"
|
||||
#include <WiFi.h>
|
||||
#include <qrcode.h>
|
||||
|
||||
namespace WebUiScreen {
|
||||
|
||||
@@ -53,6 +54,63 @@ namespace {
|
||||
}
|
||||
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) {
|
||||
@@ -90,6 +148,8 @@ void run(TFT_eSPI& tft) {
|
||||
Rect downBtn = {190, (int16_t)(Config::SCREEN_HEIGHT - 50), 38, 40};
|
||||
constexpr int16_t SCROLL_STEP = 48;
|
||||
|
||||
Rect qrBtn = {(int16_t)(Config::SCREEN_WIDTH - 40), 2, 30, 24};
|
||||
|
||||
auto redraw = [&]() {
|
||||
tft.fillScreen(TFT_BLACK);
|
||||
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
||||
@@ -117,6 +177,9 @@ void run(TFT_eSPI& tft) {
|
||||
drawButton(tft, upBtn, "^");
|
||||
drawButton(tft, downBtn, "v");
|
||||
}
|
||||
if (wifiConnected) {
|
||||
drawButton(tft, qrBtn, "QR");
|
||||
}
|
||||
};
|
||||
|
||||
redraw();
|
||||
@@ -125,7 +188,10 @@ void run(TFT_eSPI& tft) {
|
||||
TouchInput::Point tap;
|
||||
if (TouchInput::wasTapped(tap)) {
|
||||
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;
|
||||
if (scrollY < 0) scrollY = 0;
|
||||
redraw();
|
||||
|
||||
Reference in New Issue
Block a user