v2.4.0: Night dimming, aircraft watchlist, and nearest-airport lookup

This commit is contained in:
Eiswolf-BG
2026-08-05 20:27:48 +02:00
parent 79f2598b0b
commit 1ee04c0055
26 changed files with 1034 additions and 48 deletions

146
CLAUDE.md Normal file
View File

@@ -0,0 +1,146 @@
# Eiswolfs Flightradar (CYD) — Projektkontext für Claude Code
Lies diese Datei ZUERST, bevor du irgendwas am Code änderst. Sie enthält die
Dinge, die man wissen muss, um nicht dieselben Fehler nochmal zu machen, die
in der Entwicklung schon mal aufgetreten und behoben wurden.
## Was ist das Projekt?
Ein Live-ADS-B-Flugradar auf einem ESP32 "Cheap Yellow Display" (CYD,
ESP32-2432S028), 240x320 Touch-TFT. Zeigt Flugzeuge in der Nähe auf einem
rotierenden Radarschirm, mit Detail-Panel (Modell, Höhe, Speed, Route,
Squawk), Näherungs-LED-Alarm, WLAN-Verwaltung (bis zu 3 Netzwerke),
Standort-Presets (auch für fremde Orte weltweit), Airline-Filter, Flugbuch
mit Statistik, und Menüs/Splash-Screen mit einer dezenten twinkelnden
Sterne-Animation.
Aktueller Stand: **v2.3.2**. Öffentliches Repo:
https://github.com/Eiswolf-BG/eiswolfs-flightradar-CYD
## Wer nutzt das?
Alex — kompletter Anfänger bei Arduino/Embedded-Entwicklung, arbeitet mit
VS Code + PlatformIO auf einem Mac. Bitte auf Deutsch antworten. Erklärungen
gerne etwas ausführlicher, nicht von Fachbegriffen ausgehen, die als bekannt
vorausgesetzt werden.
## Tech-Stack
- PlatformIO, `platform = espressif32`, `board = esp32dev`, `framework = arduino`
- TFT_eSPI (Display), XPT2046_Touchscreen (Touch), ArduinoJson, TinyGPSPlus
- Dual-Core-Design: Netzwerk (WLAN, ADS-B-Polling) auf Core 0, Display/Touch
auf Core 1 — die UI blockiert nie durch Netzwerk-Requests.
- Daten: [adsb.fi](https://adsb.fi) (Flugzeugpositionen), [hexdb.io](https://hexdb.io)
(Modell-Lookups), [ip-api.com](https://ip-api.com) (IP-Geolocation)
## ⚠️ WICHTIGSTE FALLE: Der eigene Font ist grundlinien-verankert
Die eingebauten TFT_eSPI-Fonts (GLCD, Font2 etc.) sind reines ASCII. Für
Umlaute/Akzente (Deutsch, Türkisch, Französisch, Spanisch, Italienisch)
gibt's einen **selbst generierten Font** (`src/ui_font.h`, via
`tft.setFreeFont(&UiFont11pt)` global in `main.cpp::setup()` aktiviert, gilt
danach für JEDEN `print()`/`drawString()`-Aufruf in der ganzen App).
**Der entscheidende Unterschied zu den eingebauten Fonts:** Bei
`setCursor(x, y); print(...)` ist `y` bei unserem Font die **Grundlinie**
(Baseline), NICHT die obere Kante wie beim alten GLCD-Font. Der Text wächst
von `y` aus nach OBEN (um den Ascent, ca. 9px bei Size 1, ca. 16-18px bei
Size 2), nicht nach unten.
**Das hat in der Vergangenheit zu folgenden Bugs geführt (alle behoben, aber
Vorsicht bei neuem Code!):**
- Zu kleine y-Werte (z.B. `setCursor(10, 2)`) → Text ragt oben aus dem
Bildschirm/Container heraus oder wird abgeschnitten. **Faustregel:
y sollte bei Size 1 nie kleiner als ~14 sein, bei Size 2 nie kleiner als
~24-26 (abhängig vom Container).**
- Eingabefelder/Boxen: Baseline muss nahe der UNTERKANTE der Box liegen,
nicht in der Mitte wie man's vom alten Font gewohnt wäre.
- Zwei Textgrößen kurz hintereinander in einem eng bemessenen Layout
(Label Size 1 direkt über Wert Size 2) sind fehleranfällig — im
Statistik-Screen deshalb bewusst auf EINHEITLICHE Größe (nur Farbe
unterscheidet Label/Wert) umgestellt, das ist robuster.
- `drawString()` mit `setTextDatum(MC_DATUM)` (zentriert) ist NICHT
betroffen — TFT_eSPI rechnet die Zentrierung selbst korrekt aus, egal ob
Baseline- oder Top-verankert. Nur rohes `setCursor()`+`print()` ist die
Gefahrenzone.
- Langer, mehrzeiliger Text (z.B. Erklärtexte) NIE mit TFT_eSPI's
eingebautem Auto-Wrap verlassen — das bricht mitten im Wort ab. Stattdessen
den `layoutWrapped()`-Helper verwenden (in `location_presets_screen.cpp`
und `wifi_manage_screen.cpp` je einmal implementiert, macht wortweisen
Umbruch anhand echter Pixel-Breite plus optionales Scrollen).
Falls neue Screens/Textstellen dazukommen: lieber einmal mehr testen (idealerweise
mit Foto vom echten Display), bevor der Code als fertig gilt.
## i18n (6 Sprachen)
- `src/i18n.h`: `enum class StringId` — jeder feste UI-Text hat eine ID.
- `src/i18n_en.h`, `i18n_de.h`, `i18n_fr.h`, `i18n_tr.h`, `i18n_es.h`,
`i18n_it.h`: je ein `static const char* const[]`-Array, in **exakt
derselben Reihenfolge** wie das Enum.
- Jede Datei hat am Ende einen `static_assert`, der die Array-Größe gegen
`StringId::COUNT` prüft — **wenn der Build wegen eines fehlschlagenden
static_assert bricht, fehlt in mindestens einer Sprachdatei ein Eintrag
oder es ist einer zu viel.** Neue StringId → in ALLEN 6 Dateien an
derselben Position ergänzen, sonst verschiebt sich die Zuordnung.
- Eigennamen der Sprachen (`I18n::languageName()`) sind separat in
`i18n.cpp` hinterlegt, mit korrekten landessprachlichen Sonderzeichen
(z.B. "Français", "Türkçe", "Español").
## UI-Konventionen (bitte einhalten für neue Screens)
- **Farbschema:** Schwarzer Hintergrund, grüner Rahmen/Text
(`TFT_BLACK`/`TFT_GREEN`), aktive/ausgewählte Einträge invertiert (grün
gefüllt, schwarzer Text). Destruktive Aktionen (Abbrechen/Löschen) in Rot
(`TFT_RED`).
- Jeder Screen hat i.d.R. eine lokale `struct Rect` mit `contains(x,y)` und
eine `drawButton()`-Hilfsfunktion (Copy-Paste-Muster aus den bestehenden
Screens, kein gemeinsames Rect/Button-Modul — das ist bewusst so, um
jeden Screen unabhängig lauffähig zu halten).
- **Sterne-Animation** (`src/menu_stars.h/.cpp`): Läuft im Hintergrund auf
JEDEM schwarzen Menü-/Splash-Screen. Neue Screens sollten
`MenuStars::reset()` einmal beim Betreten aufrufen und
`MenuStars::update(tft)` in jeder Warte-/Idle-Schleife (Loop läuft sonst
ungenutzt, da die Funktion sich intern selbst auf ~60ms drosselt).
- Warteschleifen-Pattern für Touch-Eingabe:
```cpp
TouchInput::Point tap;
while (true) {
if (TouchInput::wasTapped(tap)) break;
MenuStars::update(tft);
delay(20);
}
```
- Menüstruktur: Hauptmenü → 4 Kategorien (Land/Region, WLAN/Netzwerk,
System, Flugoptionen) → jeweils Unterseiten. WLAN/Netzwerk hat aktuell
KEIN eigenes Untermenü mehr, springt direkt in die Netzwerk-Verwaltung.
- Viele Screens mit Text-Eingabe (Airline-Code, Koordinaten, WLAN-Passwort)
haben einen "?"-Info-Button oben rechts, der einen scrollbaren
Erklär-Screen öffnet (Muster: `location_presets_screen.cpp` und
`wifi_manage_screen.cpp`, jeweils eigene `layoutWrapped()`-Kopie).
## Sonstige feste Werte (Config::…)
- Näherungsalarm-Radius: 3 km
- Notfall-Squawks: 7500 (Entführung), 7600 (Funkausfall), 7700 (Notfall)
- Radar-Reichweiten: 10/25/50/100 km
- ADS-B-Abruf-Intervall: alle 8 Sekunden
- Höhen-Farbcodierung: Grün <10.000ft, Gelb 10-30.000ft, Rot >30.000ft
- Max. 3 WLAN-Netzwerke, max. 3 Standort-Presets, max. 10 gefilterte Airlines
## Code-Stil
- Kommentare durchgehend auf Deutsch, ohne Umlaute in Kommentaren selbst
unüblich (ae/oe/ue sind hier ok, das ist nur ein Stil-Ding für
Kommentare, NICHT für die UI-Texte in den i18n-Dateien — dort echte
Umlaute verwenden, siehe oben).
- Build-Check nach JEDER Änderung: `pio run` im Projektverzeichnis, auf
Warnungen UND Errors prüfen (nicht nur "compiles").
- Immer least-invasive Änderungen bevorzugen — bestehende Funktionen/Namen
nicht ohne Grund umbenennen.
## Bekannte offene Punkte / mögliche nächste Schritte
Keine akuten offenen Bugs bekannt (Stand v2.3.2). Mögliche Ideen für später,
falls der Nutzer danach fragt: weitere Menüs mit Info-Buttons versehen,
ggf. weitere Sprachen, ggf. Export/Teilen der Logbuch-Daten.

115
src/aircraft_watchlist.cpp Normal file
View File

@@ -0,0 +1,115 @@
#include "aircraft_watchlist.h"
#include "config.h"
#include "sd_mutex.h"
#include "sd_storage.h"
#include <SD.h>
#include <cstring>
#include <cctype>
namespace AircraftWatchlist {
namespace {
constexpr const char* WATCHED_FILE = "/Flightradar_cyd/watched_aircraft.txt";
char watched[MAX_WATCHED][9] = {{0}};
uint8_t watchedCount = 0;
// Ueberspringt fuehrende Leerzeichen, uebernimmt bis zu 8 Zeichen und
// bricht bei einem Leerzeichen ab (ADS-B-Rufzeichen haben oft Padding),
// alles in Grossbuchstaben.
void normalize(const char* callsign, char* out) {
int j = 0;
int i = 0;
while (callsign[i] == ' ') i++;
for (; j < 8 && callsign[i] && callsign[i] != ' '; i++, j++) {
out[j] = (char)toupper((unsigned char)callsign[i]);
}
out[j] = '\0';
}
void saveToSd() {
if (!SdStorage::isMounted()) return;
SdMutex::Guard guard;
File f = SD.open(WATCHED_FILE, FILE_WRITE);
if (!f) return;
for (uint8_t i = 0; i < watchedCount; i++) {
f.println(watched[i]);
}
f.close();
}
void loadFromSd() {
watchedCount = 0;
if (!SdStorage::isMounted()) return;
SdMutex::Guard guard;
if (!SD.exists(WATCHED_FILE)) return;
File f = SD.open(WATCHED_FILE, FILE_READ);
if (!f) return;
while (f.available() && watchedCount < MAX_WATCHED) {
String line = f.readStringUntil('\n');
line.trim();
if (line.length() == 0) continue;
strncpy(watched[watchedCount], line.c_str(), 8);
watched[watchedCount][8] = 0;
watchedCount++;
}
f.close();
}
}
void init() {
loadFromSd();
}
uint8_t count() { return watchedCount; }
String callsignAt(uint8_t index) {
if (index >= watchedCount) return String();
return String(watched[index]);
}
bool addWatched(const char* callsign) {
if (watchedCount >= MAX_WATCHED) return false;
if (!callsign || !callsign[0]) return false;
char normalized[9] = {0};
normalize(callsign, normalized);
if (!normalized[0]) return false;
for (uint8_t j = 0; j < watchedCount; j++) {
if (strcmp(watched[j], normalized) == 0) return true;
}
strncpy(watched[watchedCount], normalized, 8);
watched[watchedCount][8] = 0;
watchedCount++;
saveToSd();
return true;
}
void removeWatched(uint8_t index) {
if (index >= watchedCount) return;
for (uint8_t i = index; i < watchedCount - 1; i++) {
strncpy(watched[i], watched[i + 1], 9);
}
watchedCount--;
watched[watchedCount][0] = 0;
saveToSd();
}
bool isWatched(const char* callsign) {
if (watchedCount == 0) return false;
char normalized[9];
normalize(callsign, normalized);
if (!normalized[0]) return false;
for (uint8_t i = 0; i < watchedCount; i++) {
if (strcmp(watched[i], normalized) == 0) return true;
}
return false;
}
}

16
src/aircraft_watchlist.h Normal file
View File

@@ -0,0 +1,16 @@
#pragma once
#include <Arduino.h>
namespace AircraftWatchlist {
constexpr uint8_t MAX_WATCHED = 5;
void init();
uint8_t count();
String callsignAt(uint8_t index);
bool addWatched(const char* callsign);
void removeWatched(uint8_t index);
bool isWatched(const char* callsign);
}

View File

@@ -0,0 +1,320 @@
#include "aircraft_watchlist_screen.h"
#include "aircraft_watchlist.h"
#include "touch_input.h"
#include "menu_stars.h"
#include "config.h"
#include "i18n.h"
namespace AircraftWatchlistScreen {
namespace {
struct Rect {
int16_t x, y, w, h;
bool contains(int16_t px, int16_t py) const {
return px >= x && px < x + w && py >= y && py < y + h;
}
};
void drawButton(TFT_eSPI& tft, const Rect& r, const String& label, bool danger = false) {
uint16_t accent = danger ? TFT_RED : TFT_GREEN;
tft.fillRoundRect(r.x, r.y, r.w, r.h, 4, TFT_BLACK);
tft.drawRoundRect(r.x, r.y, r.w, r.h, 4, accent);
tft.setTextDatum(MC_DATUM);
tft.setTextColor(accent, TFT_BLACK);
tft.drawString(label, r.x + r.w / 2, r.y + r.h / 2);
tft.setTextDatum(TL_DATUM);
}
// Vier Tastaturreihen (Ziffern oben, dann QWERTYUIOP/ASDFGHJKL/ZXCVBNM),
// da Rufzeichen wie "DLH441" auch Zahlen enthalten. Buffer 8 Zeichen +
// Nullterminierung (volles Rufzeichen statt nur 3-stelligem ICAO-Praefix).
String runCallsignKeypad(TFT_eSPI& tft) {
MenuStars::reset();
constexpr const char* DIGITS = "1234567890";
constexpr const char* ROW1 = "QWERTYUIOP";
constexpr const char* ROW2 = "ASDFGHJKL";
constexpr const char* ROW3 = "ZXCVBNM";
char buf[9] = {0};
uint8_t len = 0;
constexpr int16_t KEY_H = 30;
constexpr int16_t KEY_GAP = 3;
constexpr int16_t ROW0_Y = 78;
auto layoutRow = [&](const char* row, int16_t y, Rect* outRects, uint8_t n) {
int16_t usableW = Config::SCREEN_WIDTH - 8;
int16_t keyW = (usableW - (n - 1) * KEY_GAP) / n;
int16_t x = 4;
for (uint8_t i = 0; i < n; i++) {
outRects[i] = {x, y, keyW, KEY_H};
x += keyW + KEY_GAP;
}
};
Rect digitRects[10], row1Rects[10], row2Rects[9], row3Rects[7];
layoutRow(DIGITS, ROW0_Y, digitRects, 10);
layoutRow(ROW1, ROW0_Y + (KEY_H + KEY_GAP), row1Rects, 10);
layoutRow(ROW2, ROW0_Y + 2 * (KEY_H + KEY_GAP), row2Rects, 9);
layoutRow(ROW3, ROW0_Y + 3 * (KEY_H + KEY_GAP), row3Rects, 7);
Rect backspaceBtn = {4, (int16_t)(ROW0_Y + 4 * (KEY_H + KEY_GAP)), 100, KEY_H};
Rect cancelBtn = {(int16_t)(Config::SCREEN_WIDTH - 100), (int16_t)(ROW0_Y + 4 * (KEY_H + KEY_GAP)), 96, KEY_H};
Rect confirmBtn = {4, (int16_t)(ROW0_Y + 5 * (KEY_H + KEY_GAP)), (int16_t)(Config::SCREEN_WIDTH - 8), KEY_H};
bool done = false;
bool confirmed = false;
auto redraw = [&]() {
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 14);
tft.println(I18n::t(StringId::WATCHLIST_ADD_TITLE));
tft.fillRect(8, 40, Config::SCREEN_WIDTH - 16, 34, TFT_BLACK);
tft.drawRect(8, 40, Config::SCREEN_WIDTH - 16, 34, TFT_GREEN);
tft.setTextSize(2);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(14, 66);
tft.print(buf);
tft.setTextSize(1);
for (uint8_t i = 0; i < 10; i++) drawButton(tft, digitRects[i], String(DIGITS[i]));
for (uint8_t i = 0; i < 10; i++) drawButton(tft, row1Rects[i], String(ROW1[i]));
for (uint8_t i = 0; i < 9; i++) drawButton(tft, row2Rects[i], String(ROW2[i]));
for (uint8_t i = 0; i < 7; i++) drawButton(tft, row3Rects[i], String(ROW3[i]));
drawButton(tft, backspaceBtn, "<-");
drawButton(tft, cancelBtn, I18n::t(StringId::CANCEL), true);
drawButton(tft, confirmBtn, I18n::t(StringId::ADD));
};
redraw();
while (!done) {
TouchInput::Point tap;
if (!TouchInput::wasTapped(tap)) { MenuStars::update(tft); delay(20); continue; }
bool handled = false;
for (uint8_t i = 0; i < 10 && !handled; i++) {
if (digitRects[i].contains(tap.x, tap.y) && len < 8) { buf[len++] = DIGITS[i]; handled = true; }
}
for (uint8_t i = 0; i < 10 && !handled; i++) {
if (row1Rects[i].contains(tap.x, tap.y) && len < 8) { buf[len++] = ROW1[i]; handled = true; }
}
for (uint8_t i = 0; i < 9 && !handled; i++) {
if (row2Rects[i].contains(tap.x, tap.y) && len < 8) { buf[len++] = ROW2[i]; handled = true; }
}
for (uint8_t i = 0; i < 7 && !handled; i++) {
if (row3Rects[i].contains(tap.x, tap.y) && len < 8) { buf[len++] = ROW3[i]; handled = true; }
}
if (!handled && backspaceBtn.contains(tap.x, tap.y)) {
if (len > 0) { len--; buf[len] = 0; }
handled = true;
}
if (!handled && cancelBtn.contains(tap.x, tap.y)) {
done = true;
confirmed = false;
handled = true;
}
if (!handled && confirmBtn.contains(tap.x, tap.y) && len > 0) {
done = true;
confirmed = true;
handled = true;
}
if (handled) redraw();
}
return confirmed ? String(buf) : String();
}
int16_t layoutWrapped(TFT_eSPI& tft, int16_t x, int16_t startY, int16_t maxWidth,
int16_t lineHeight, const String& text, int16_t scrollY,
int16_t viewTop, int16_t viewBottom, bool draw) {
int16_t y = startY;
int32_t start = 0;
int32_t len = text.length();
while (start < len) {
while (start < len && text[start] == ' ') start++;
if (start >= len) break;
String line = text.substring(start, len);
while (tft.textWidth(line) > maxWidth) {
int32_t lastSpace = line.lastIndexOf(' ');
if (lastSpace <= 0) break;
line = line.substring(0, lastSpace);
}
if (draw) {
int16_t screenY = y - scrollY;
if (screenY >= viewTop && screenY <= viewBottom) {
tft.setCursor(x, screenY);
tft.print(line);
}
}
y += lineHeight;
start += line.length();
}
return y;
}
void runInfoScreen(TFT_eSPI& tft) {
MenuStars::reset();
constexpr int16_t textMaxWidth = Config::SCREEN_WIDTH - 20;
constexpr int16_t LINE_H = 16;
constexpr int16_t VIEW_TOP = 36;
constexpr int16_t VIEW_BOTTOM = Config::SCREEN_HEIGHT - 60;
int16_t totalH = VIEW_TOP;
totalH = layoutWrapped(tft, 10, totalH, textMaxWidth, LINE_H, I18n::t(StringId::WATCHLIST_INFO_PARA1), 0, 0, 0, false);
totalH += 8;
totalH = layoutWrapped(tft, 10, totalH, textMaxWidth, LINE_H, I18n::t(StringId::WATCHLIST_INFO_PARA2), 0, 0, 0, false);
int16_t maxScroll = totalH - VIEW_BOTTOM;
if (maxScroll < 0) maxScroll = 0;
bool scrollable = maxScroll > 0;
int16_t scrollY = 0;
Rect backBtn = scrollable
? Rect{10, (int16_t)(Config::SCREEN_HEIGHT - 50), 130, 40}
: Rect{10, (int16_t)(Config::SCREEN_HEIGHT - 50), (int16_t)(Config::SCREEN_WIDTH - 20), 40};
Rect upBtn = {146, (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;
auto redraw = [&]() {
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 14);
tft.println(I18n::t(StringId::WATCHLIST_INFO_TITLE));
tft.setTextColor(TFT_GREEN, TFT_BLACK);
int16_t y = VIEW_TOP;
y = layoutWrapped(tft, 10, y, textMaxWidth, LINE_H, I18n::t(StringId::WATCHLIST_INFO_PARA1), scrollY, VIEW_TOP, VIEW_BOTTOM, true);
y += 8;
layoutWrapped(tft, 10, y, textMaxWidth, LINE_H, I18n::t(StringId::WATCHLIST_INFO_PARA2), scrollY, VIEW_TOP, VIEW_BOTTOM, true);
drawButton(tft, backBtn, I18n::t(StringId::BACK));
if (scrollable) {
drawButton(tft, upBtn, "^");
drawButton(tft, downBtn, "v");
}
};
redraw();
while (true) {
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) {
scrollY -= SCROLL_STEP;
if (scrollY < 0) scrollY = 0;
redraw();
} else if (scrollable && downBtn.contains(tap.x, tap.y) && scrollY < maxScroll) {
scrollY += SCROLL_STEP;
if (scrollY > maxScroll) scrollY = maxScroll;
redraw();
}
}
MenuStars::update(tft);
delay(20);
}
}
}
void run(TFT_eSPI& tft) {
constexpr int16_t ROW_H = 32;
constexpr int16_t ROW_GAP = 6;
constexpr int16_t REMOVE_BTN_W = 60;
bool done = false;
MenuStars::reset();
while (!done) {
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 14);
tft.println(I18n::t(StringId::WATCHLIST_TITLE));
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 26);
tft.println(I18n::t(StringId::WATCHLIST_DESC1));
tft.setCursor(10, 38);
tft.println(I18n::t(StringId::WATCHLIST_DESC2));
Rect infoBtn = {(int16_t)(Config::SCREEN_WIDTH - 40), 2, 30, 24};
drawButton(tft, infoBtn, "?");
uint8_t count = AircraftWatchlist::count();
int16_t y = 56;
Rect rowRects[AircraftWatchlist::MAX_WATCHED];
Rect removeRects[AircraftWatchlist::MAX_WATCHED];
if (count == 0) {
tft.setTextColor(TFT_DARKGREY, TFT_BLACK);
tft.setCursor(10, y + 14);
tft.println(I18n::t(StringId::WATCHLIST_EMPTY));
y += ROW_H;
}
for (uint8_t i = 0; i < count; i++) {
Rect rowRect = {10, y, (int16_t)(Config::SCREEN_WIDTH - 20 - REMOVE_BTN_W - 6), ROW_H};
Rect removeRect = {(int16_t)(Config::SCREEN_WIDTH - 10 - REMOVE_BTN_W), y, REMOVE_BTN_W, ROW_H};
rowRects[i] = rowRect;
removeRects[i] = removeRect;
tft.fillRoundRect(rowRect.x, rowRect.y, rowRect.w, rowRect.h, 4, TFT_BLACK);
tft.drawRoundRect(rowRect.x, rowRect.y, rowRect.w, rowRect.h, 4, TFT_GREEN);
tft.setTextDatum(MC_DATUM);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.drawString(AircraftWatchlist::callsignAt(i), rowRect.x + rowRect.w / 2, rowRect.y + rowRect.h / 2);
tft.setTextDatum(TL_DATUM);
drawButton(tft, removeRect, "X", true);
y += ROW_H + ROW_GAP;
}
Rect addBtn = {10, y, (int16_t)(Config::SCREEN_WIDTH - 20), 40};
bool canAdd = count < AircraftWatchlist::MAX_WATCHED;
if (canAdd) {
drawButton(tft, addBtn, I18n::t(StringId::WATCHLIST_ADD));
y += 40 + 10;
}
Rect backBtn = {10, (int16_t)(Config::SCREEN_HEIGHT - 50), (int16_t)(Config::SCREEN_WIDTH - 20), 40};
drawButton(tft, backBtn, I18n::t(StringId::BACK));
TouchInput::Point tap;
while (true) {
if (TouchInput::wasTapped(tap)) break;
MenuStars::update(tft);
delay(20);
}
bool handled = false;
if (infoBtn.contains(tap.x, tap.y)) {
runInfoScreen(tft);
handled = true;
}
for (uint8_t i = 0; i < count && !handled; i++) {
if (removeRects[i].contains(tap.x, tap.y)) {
AircraftWatchlist::removeWatched(i);
handled = true;
}
}
if (!handled && canAdd && addBtn.contains(tap.x, tap.y)) {
String callsign = runCallsignKeypad(tft);
if (callsign.length() > 0) {
AircraftWatchlist::addWatched(callsign.c_str());
}
handled = true;
}
if (!handled && backBtn.contains(tap.x, tap.y)) {
done = true;
}
}
}
}

View File

@@ -0,0 +1,7 @@
#pragma once
#include <Arduino.h>
#include <TFT_eSPI.h>
namespace AircraftWatchlistScreen {
void run(TFT_eSPI& tft);
}

51
src/airport_lookup.cpp Normal file
View File

@@ -0,0 +1,51 @@
#include "airport_lookup.h"
#include "sd_storage.h"
#include "config.h"
#include "radar_math.h"
#include <SD.h>
namespace AirportLookup {
Nearest findNearest(double lat, double lon) {
Nearest result;
if (!SdStorage::isMounted()) return result;
File f = SD.open(Config::SD_AIRPORTS_CSV);
if (!f) return result;
f.readStringUntil('\n');
float bestDistanceKm = 0;
while (f.available()) {
String line = f.readStringUntil('\n');
line.trim();
if (line.length() == 0) continue;
int c1 = line.indexOf(',');
if (c1 < 0) continue;
int c2 = line.indexOf(',', c1 + 1);
if (c2 < 0) continue;
int c3 = line.indexOf(',', c2 + 1);
if (c3 < 0) continue;
String icao = line.substring(0, c1);
String name = line.substring(c1 + 1, c2);
double aptLat = line.substring(c2 + 1, c3).toDouble();
double aptLon = line.substring(c3 + 1).toDouble();
float distanceKm = RadarMath::toPolar(lat, lon, aptLat, aptLon).distanceKm;
if (!result.found || distanceKm < bestDistanceKm) {
bestDistanceKm = distanceKm;
result.found = true;
result.distanceKm = distanceKm;
strncpy(result.icao, icao.c_str(), sizeof(result.icao) - 1);
strncpy(result.name, name.c_str(), sizeof(result.name) - 1);
}
}
f.close();
return result;
}
}

12
src/airport_lookup.h Normal file
View File

@@ -0,0 +1,12 @@
#pragma once
#include <Arduino.h>
namespace AirportLookup {
struct Nearest {
bool found = false;
char icao[5] = {0};
char name[32] = {0};
float distanceKm = 0;
};
Nearest findNearest(double lat, double lon);
}

View File

@@ -31,6 +31,7 @@ namespace Config {
constexpr const char* SD_ROOT_DIR = "/Flightradar_cyd"; constexpr const char* SD_ROOT_DIR = "/Flightradar_cyd";
constexpr const char* SD_AIRLINES_CSV = "/Flightradar_cyd/airlines.csv"; constexpr const char* SD_AIRLINES_CSV = "/Flightradar_cyd/airlines.csv";
constexpr const char* SD_AIRCRAFT_TYPES_CSV = "/Flightradar_cyd/aircraft_types.csv"; constexpr const char* SD_AIRCRAFT_TYPES_CSV = "/Flightradar_cyd/aircraft_types.csv";
constexpr const char* SD_AIRPORTS_CSV = "/Flightradar_cyd/airports.csv";
constexpr const char* SD_LOG_DIR = "/Flightradar_cyd/logs"; constexpr const char* SD_LOG_DIR = "/Flightradar_cyd/logs";
constexpr const char* SD_SCREENSHOT_DIR = "/Flightradar_cyd/screenshots"; constexpr const char* SD_SCREENSHOT_DIR = "/Flightradar_cyd/screenshots";
constexpr const char* SD_SETTINGS_FILE = "/Flightradar_cyd/config.txt"; constexpr const char* SD_SETTINGS_FILE = "/Flightradar_cyd/config.txt";

View File

@@ -56,6 +56,7 @@ enum class StringId : uint8_t {
MENU_MANAGE_WIFI, MENU_MANAGE_WIFI,
MENU_LOCATION_PRESETS, MENU_LOCATION_PRESETS,
MENU_SCREEN_TIMEOUT_PREFIX, MENU_SCREEN_TIMEOUT_PREFIX,
MENU_NIGHT_DIMMING,
MENU_STATISTICS, MENU_STATISTICS,
MENU_LOGBOOK_FILES, MENU_LOGBOOK_FILES,
MENU_MORE_SETTINGS, MENU_MORE_SETTINGS,
@@ -65,6 +66,8 @@ enum class StringId : uint8_t {
MENU_FLIGHT_LOGBOOK, MENU_FLIGHT_LOGBOOK,
MENU_HIDE_GROUND, MENU_HIDE_GROUND,
MENU_AIRLINE_FILTER, MENU_AIRLINE_FILTER,
MENU_WATCHLIST,
MENU_WATCHLIST_ALERT,
MENU_BACKUP, MENU_BACKUP,
MENU_RESTORE, MENU_RESTORE,
MENU_BACKUP_SAVED, MENU_BACKUP_SAVED,
@@ -80,6 +83,16 @@ enum class StringId : uint8_t {
AIRLINE_FILTER_ADD, AIRLINE_FILTER_ADD,
AIRLINE_ADD_TITLE, AIRLINE_ADD_TITLE,
WATCHLIST_TITLE,
WATCHLIST_DESC1,
WATCHLIST_DESC2,
WATCHLIST_ADD,
WATCHLIST_ADD_TITLE,
WATCHLIST_EMPTY,
WATCHLIST_INFO_TITLE,
WATCHLIST_INFO_PARA1,
WATCHLIST_INFO_PARA2,
LOCATION_TITLE, LOCATION_TITLE,
LOCATION_AUTO, LOCATION_AUTO,
LOCATION_PRESET, LOCATION_PRESET,
@@ -87,10 +100,12 @@ enum class StringId : uint8_t {
LOCATION_ADD, LOCATION_ADD,
LOCATION_LAT_PROMPT, LOCATION_LAT_PROMPT,
LOCATION_LON_PROMPT, LOCATION_LON_PROMPT,
LOCATION_NEAREST_AIRPORT_PREFIX,
LOCATION_INFO_TITLE, LOCATION_INFO_TITLE,
LOCATION_INFO_PARA1, LOCATION_INFO_PARA1,
LOCATION_INFO_PARA2, LOCATION_INFO_PARA2,
LOCATION_INFO_PARA3, LOCATION_INFO_PARA3,
LOCATION_INFO_PARA4,
STATS_TITLE, STATS_TITLE,
STATS_TODAY, STATS_TODAY,

View File

@@ -56,6 +56,7 @@ static const char* const I18N_DE[] = {
"WLAN-Netzwerke verwalten", "WLAN-Netzwerke verwalten",
"Standort-Presets", "Standort-Presets",
"Bildschirm-Timeout: ", "Bildschirm-Timeout: ",
"Nachtdimmung (22-6 Uhr): ",
"Statistik", "Statistik",
"Logbuch-Dateien", "Logbuch-Dateien",
"Weitere Einstellungen >", "Weitere Einstellungen >",
@@ -65,6 +66,8 @@ static const char* const I18N_DE[] = {
"Flugbuch: ", "Flugbuch: ",
"Bodenfahrzeuge ausblenden: ", "Bodenfahrzeuge ausblenden: ",
"Airline-Filter", "Airline-Filter",
"Beobachtungsliste",
"Beobachtungs-Alarm: ",
"Einstellungen sichern", "Einstellungen sichern",
"Einstellungen wiederherstellen", "Einstellungen wiederherstellen",
"Backup gespeichert.", "Backup gespeichert.",
@@ -80,6 +83,16 @@ static const char* const I18N_DE[] = {
"Airline hinzufügen", "Airline hinzufügen",
"Airline hinzufügen (ICAO-Code)", "Airline hinzufügen (ICAO-Code)",
"Beobachtete Flugzeuge",
"Blaue LED blinkt, sobald eines",
"dieser Flugzeuge sichtbar ist.",
"Flugzeug hinzufügen",
"Flugzeug hinzufügen (Rufzeichen)",
"Noch keine Flugzeuge beobachtet.",
"Wie die Beobachtungsliste funktioniert",
"Trage das exakte Rufzeichen eines bestimmten Flugs ein (z.B. DLH441) - keinen Airline-Code wie beim Airline-Filter. Bis zu 5 Flugzeuge lassen sich gleichzeitig beobachten.",
"Sobald ein beobachtetes Flugzeug irgendwo im Radar auftaucht, bekommt es einen cyanfarbenen Ring und die LED blinkt blau - praktisch, um einen bestimmten Flug zu erkennen, z.B. wenn jemand Bekanntes ankommt.",
"Standort-Presets", "Standort-Presets",
"Automatisch (GPS/IP)", "Automatisch (GPS/IP)",
"Preset", "Preset",
@@ -87,10 +100,12 @@ static const char* const I18N_DE[] = {
"Preset hinzufügen", "Preset hinzufügen",
"Breitengrad (z.B. 48.6833)", "Breitengrad (z.B. 48.6833)",
"Längengrad (z.B. 9.8333)", "Längengrad (z.B. 9.8333)",
"Nächster Flughafen: ",
"Wie Presets funktionieren", "Wie Presets funktionieren",
"Nur EIN Standort ist zur selben Zeit aktiv - entweder Auto oder genau ein Preset. Tippe einen Eintrag an, um ihn zu aktivieren.", "Nur EIN Standort ist zur selben Zeit aktiv - entweder Auto oder genau ein Preset. Tippe einen Eintrag an, um ihn zu aktivieren.",
"Du kannst auch die Koordinaten eines beliebigen anderen Ortes eintragen - z.B. Mailand, New York oder Tokio - und siehst DORT den aktuellen Flugverkehr an, egal wo dein Gerät tatsächlich steht!", "Du kannst auch die Koordinaten eines beliebigen anderen Ortes eintragen - z.B. Mailand, New York oder Tokio - und siehst DORT den aktuellen Flugverkehr an, egal wo dein Gerät tatsächlich steht!",
"Praktisch auch für mehr Genauigkeit als die automatische IP-Standortbestimmung, z.B. wenn das Gerät dauerhaft an einem festen Ort steht.", "Praktisch auch für mehr Genauigkeit als die automatische IP-Standortbestimmung, z.B. wenn das Gerät dauerhaft an einem festen Ort steht.",
"Dieser Screen zeigt außerdem den nächstgelegenen Flughafen zum jeweils aktiven Standort an - praktisch, um genau zu sehen, wo ein fremder Preset eigentlich liegt.",
"Statistik", "Statistik",
"Heute geloggte Flugzeuge", "Heute geloggte Flugzeuge",

View File

@@ -56,6 +56,7 @@ static const char* const I18N_EN[] = {
"Manage WiFi networks", "Manage WiFi networks",
"Location presets", "Location presets",
"Screen timeout: ", "Screen timeout: ",
"Night dimming (10pm-6am): ",
"Statistics", "Statistics",
"Logbook files", "Logbook files",
"More settings >", "More settings >",
@@ -65,6 +66,8 @@ static const char* const I18N_EN[] = {
"Flight logbook: ", "Flight logbook: ",
"Hide ground vehicles: ", "Hide ground vehicles: ",
"Airline filter", "Airline filter",
"Watchlist",
"Watchlist alert: ",
"Backup settings", "Backup settings",
"Restore settings", "Restore settings",
"Backup saved.", "Backup saved.",
@@ -80,6 +83,16 @@ static const char* const I18N_EN[] = {
"Add airline", "Add airline",
"Add airline (ICAO code)", "Add airline (ICAO code)",
"Watched aircraft",
"Get notified (blue LED) when any",
"of these aircraft appears.",
"Add aircraft",
"Add aircraft (callsign)",
"No aircraft watched yet.",
"How the watchlist works",
"Enter the exact callsign of a specific flight (e.g. DLH441) - not an airline code like the Airline Filter. Up to 5 aircraft can be tracked at once.",
"As soon as a watched aircraft appears anywhere on the radar, it gets a cyan ring and the LED blinks blue - great for spotting a specific flight, e.g. when someone you know is arriving.",
"Location presets", "Location presets",
"Auto (GPS/IP)", "Auto (GPS/IP)",
"Preset", "Preset",
@@ -87,11 +100,13 @@ static const char* const I18N_EN[] = {
"Add preset", "Add preset",
"Latitude (e.g. 48.6833)", "Latitude (e.g. 48.6833)",
"Longitude (e.g. 9.8333)", "Longitude (e.g. 9.8333)",
"Nearest airport: ",
"How presets work", "How presets work",
"Only ONE location is active at a time - either Auto or exactly one preset. Tap an entry to activate it.", "Only ONE location is active at a time - either Auto or exactly one preset. Tap an entry to activate it.",
"You can also enter the coordinates of any other place - e.g. Milan, New York or Tokyo - and see the live air traffic THERE, no matter where your device actually is!", "You can also enter the coordinates of any other place - e.g. Milan, New York or Tokyo - and see the live air traffic THERE, no matter where your device actually is!",
"Also handy for more precision than automatic IP-based location, e.g. when the device stays in one fixed spot.", "Also handy for more precision than automatic IP-based location, e.g. when the device stays in one fixed spot.",
"This screen also shows the nearest airport to whichever location is currently active - handy to see exactly where a foreign preset points to.",
"Statistics", "Statistics",
"Aircraft logged today", "Aircraft logged today",

View File

@@ -56,6 +56,7 @@ static const char* const I18N_ES[] = {
"Gestionar redes WiFi", "Gestionar redes WiFi",
"Ubicaciones guardadas", "Ubicaciones guardadas",
"Apagado de pantalla: ", "Apagado de pantalla: ",
"Atenuación nocturna (22-6h): ",
"Estadísticas", "Estadísticas",
"Archivos del registro", "Archivos del registro",
"Más ajustes >", "Más ajustes >",
@@ -65,6 +66,8 @@ static const char* const I18N_ES[] = {
"Registro de vuelos: ", "Registro de vuelos: ",
"Ocultar vehículos terrestres: ", "Ocultar vehículos terrestres: ",
"Filtro de aerolíneas", "Filtro de aerolíneas",
"Lista de seguimiento",
"Alerta de seguimiento: ",
"Copia de seguridad", "Copia de seguridad",
"Restaurar ajustes", "Restaurar ajustes",
"Copia guardada.", "Copia guardada.",
@@ -80,6 +83,16 @@ static const char* const I18N_ES[] = {
"Añadir aerolínea", "Añadir aerolínea",
"Añadir aerolínea (código OACI)", "Añadir aerolínea (código OACI)",
"Aviones vigilados",
"El LED azul parpadea cuando",
"aparece uno de estos aviones.",
"Añadir avión",
"Añadir avión (indicativo)",
"Aún no hay aviones vigilados.",
"Cómo funciona la lista de seguimiento",
"Introduzca el indicativo exacto de un vuelo especifico (ej. DLH441) - no un codigo de aerolinea como en el Filtro de aerolineas. Se pueden seguir hasta 5 aviones a la vez.",
"En cuanto aparece un avion vigilado en cualquier parte del radar, recibe un anillo cian y el LED parpadea en azul - genial para detectar un vuelo especifico, por ejemplo cuando llega alguien conocido.",
"Ubicaciones guardadas", "Ubicaciones guardadas",
"Auto (GPS/IP)", "Auto (GPS/IP)",
"Ubicación", "Ubicación",
@@ -87,10 +100,12 @@ static const char* const I18N_ES[] = {
"Añadir ubicación", "Añadir ubicación",
"Latitud (ej. 48.6833)", "Latitud (ej. 48.6833)",
"Longitud (ej. 9.8333)", "Longitud (ej. 9.8333)",
"Aeropuerto más cercano: ",
"Como funcionan los presets", "Como funcionan los presets",
"Solo UNA ubicacion esta activa a la vez - Auto o exactamente un preset. Toque una entrada para activarla.", "Solo UNA ubicacion esta activa a la vez - Auto o exactamente un preset. Toque una entrada para activarla.",
"Tambien puede introducir las coordenadas de cualquier otro lugar - ej. Milan, Nueva York o Tokio - y ver el trafico aereo en tiempo real ALLI, sin importar donde este su dispositivo!", "Tambien puede introducir las coordenadas de cualquier otro lugar - ej. Milan, Nueva York o Tokio - y ver el trafico aereo en tiempo real ALLI, sin importar donde este su dispositivo!",
"Tambien es util para mas precision que la geolocalizacion automatica por IP, por ejemplo si el dispositivo permanece siempre en un lugar fijo.", "Tambien es util para mas precision que la geolocalizacion automatica por IP, por ejemplo si el dispositivo permanece siempre en un lugar fijo.",
"Esta pantalla también muestra el aeropuerto más cercano a la ubicación activa - útil para ver exactamente dónde apunta un preset lejano.",
"Estadísticas", "Estadísticas",
"Aviones registrados hoy", "Aviones registrados hoy",

View File

@@ -56,6 +56,7 @@ static const char* const I18N_FR[] = {
"Gérer les réseaux WiFi", "Gérer les réseaux WiFi",
"Positions enregistrées", "Positions enregistrées",
"Extinction écran : ", "Extinction écran : ",
"Assombrissement nuit (22h-6h) : ",
"Statistiques", "Statistiques",
"Fichiers du journal", "Fichiers du journal",
"Plus de réglages >", "Plus de réglages >",
@@ -65,6 +66,8 @@ static const char* const I18N_FR[] = {
"Journal de vol : ", "Journal de vol : ",
"Masquer véhicules au sol : ", "Masquer véhicules au sol : ",
"Filtre compagnies", "Filtre compagnies",
"Liste de surveillance",
"Alerte de surveillance : ",
"Sauvegarder réglages", "Sauvegarder réglages",
"Restaurer réglages", "Restaurer réglages",
"Sauvegarde enregistrée.", "Sauvegarde enregistrée.",
@@ -80,6 +83,16 @@ static const char* const I18N_FR[] = {
"Ajouter une compagnie", "Ajouter une compagnie",
"Ajouter (code OACI)", "Ajouter (code OACI)",
"Avions surveillés",
"La LED bleue clignote des qu'un",
"de ces avions apparait.",
"Ajouter un avion",
"Ajouter un avion (indicatif)",
"Aucun avion surveille pour l'instant.",
"Comment fonctionne la liste de surveillance",
"Entrez l'indicatif exact d'un vol precis (ex. DLH441) - pas un code compagnie comme pour le filtre. Jusqu'a 5 avions peuvent etre suivis a la fois.",
"Des qu'un avion surveille apparait quelque part sur le radar, il recoit un anneau cyan et la LED clignote en bleu - pratique pour reperer un vol precis, par exemple l'arrivee de quelqu'un que vous connaissez.",
"Positions enregistrées", "Positions enregistrées",
"Auto (GPS/IP)", "Auto (GPS/IP)",
"Position", "Position",
@@ -87,10 +100,12 @@ static const char* const I18N_FR[] = {
"Ajouter une position", "Ajouter une position",
"Latitude (ex. 48.6833)", "Latitude (ex. 48.6833)",
"Longitude (ex. 9.8333)", "Longitude (ex. 9.8333)",
"Aéroport le plus proche : ",
"Comment marchent les presets", "Comment marchent les presets",
"Un SEUL lieu est actif a la fois - soit Auto, soit un preset precis. Touchez une entree pour l'activer.", "Un SEUL lieu est actif a la fois - soit Auto, soit un preset precis. Touchez une entree pour l'activer.",
"Vous pouvez aussi entrer les coordonnees de n'importe quel autre lieu - ex. Milan, New York ou Tokyo - et voir le trafic aerien LA-BAS, peu importe ou se trouve votre appareil !", "Vous pouvez aussi entrer les coordonnees de n'importe quel autre lieu - ex. Milan, New York ou Tokyo - et voir le trafic aerien LA-BAS, peu importe ou se trouve votre appareil !",
"Pratique aussi pour plus de precision que la geolocalisation IP automatique, par exemple si l'appareil reste toujours au meme endroit.", "Pratique aussi pour plus de precision que la geolocalisation IP automatique, par exemple si l'appareil reste toujours au meme endroit.",
"Cet écran affiche aussi l'aéroport le plus proche de l'emplacement actif - pratique pour voir exactement où pointe un preset lointain.",
"Statistiques", "Statistiques",
"Avions enregistrés aujourd'hui", "Avions enregistrés aujourd'hui",

View File

@@ -56,6 +56,7 @@ static const char* const I18N_IT[] = {
"Gestisci reti WiFi", "Gestisci reti WiFi",
"Posizioni salvate", "Posizioni salvate",
"Spegnimento schermo: ", "Spegnimento schermo: ",
"Attenuazione notte (22-6): ",
"Statistiche", "Statistiche",
"File del registro", "File del registro",
"Altre impostazioni >", "Altre impostazioni >",
@@ -65,6 +66,8 @@ static const char* const I18N_IT[] = {
"Diario di volo: ", "Diario di volo: ",
"Nascondi veicoli a terra: ", "Nascondi veicoli a terra: ",
"Filtro compagnie", "Filtro compagnie",
"Lista di controllo",
"Allarme di controllo: ",
"Backup impostazioni", "Backup impostazioni",
"Ripristina impostazioni", "Ripristina impostazioni",
"Backup salvato.", "Backup salvato.",
@@ -80,6 +83,16 @@ static const char* const I18N_IT[] = {
"Aggiungi compagnia", "Aggiungi compagnia",
"Aggiungi compagnia (codice ICAO)", "Aggiungi compagnia (codice ICAO)",
"Aerei monitorati",
"Il LED blu lampeggia quando",
"appare uno di questi aerei.",
"Aggiungi aereo",
"Aggiungi aereo (nominativo)",
"Ancora nessun aereo monitorato.",
"Come funziona la lista di controllo",
"Inserisci il nominativo esatto di un volo specifico (es. DLH441) - non un codice compagnia come nel Filtro compagnie. Puoi monitorare fino a 5 aerei contemporaneamente.",
"Non appena un aereo monitorato appare ovunque sul radar, riceve un anello ciano e il LED lampeggia in blu - utile per individuare un volo specifico, ad esempio l'arrivo di una persona conosciuta.",
"Posizioni salvate", "Posizioni salvate",
"Auto (GPS/IP)", "Auto (GPS/IP)",
"Posizione", "Posizione",
@@ -87,10 +100,12 @@ static const char* const I18N_IT[] = {
"Aggiungi posizione", "Aggiungi posizione",
"Latitudine (es. 48.6833)", "Latitudine (es. 48.6833)",
"Longitudine (es. 9.8333)", "Longitudine (es. 9.8333)",
"Aeroporto più vicino: ",
"Come funzionano i preset", "Come funzionano i preset",
"Solo UNA posizione e attiva alla volta - Auto oppure esattamente un preset. Tocca una voce per attivarla.", "Solo UNA posizione e attiva alla volta - Auto oppure esattamente un preset. Tocca una voce per attivarla.",
"Puoi anche inserire le coordinate di qualsiasi altro luogo - es. Milano, New York o Tokyo - e vedere il traffico aereo in tempo reale LI, indipendentemente da dove si trovi il dispositivo!", "Puoi anche inserire le coordinate di qualsiasi altro luogo - es. Milano, New York o Tokyo - e vedere il traffico aereo in tempo reale LI, indipendentemente da dove si trovi il dispositivo!",
"Utile anche per maggiore precisione rispetto alla geolocalizzazione IP automatica, ad esempio se il dispositivo resta sempre in un punto fisso.", "Utile anche per maggiore precisione rispetto alla geolocalizzazione IP automatica, ad esempio se il dispositivo resta sempre in un punto fisso.",
"Questa schermata mostra anche l'aeroporto più vicino alla posizione attiva - utile per vedere esattamente dove punta un preset lontano.",
"Statistiche", "Statistiche",
"Aerei registrati oggi", "Aerei registrati oggi",

View File

@@ -56,6 +56,7 @@ static const char* const I18N_TR[] = {
"WiFi ağlarını yönet", "WiFi ağlarını yönet",
"Konum ön ayarları", "Konum ön ayarları",
"Ekran zaman aşımı: ", "Ekran zaman aşımı: ",
"Gece kısma (22-06): ",
"İstatistikler", "İstatistikler",
"Kayıt defteri dosyaları", "Kayıt defteri dosyaları",
"Diğer ayarlar >", "Diğer ayarlar >",
@@ -65,6 +66,8 @@ static const char* const I18N_TR[] = {
"Uçuş kayıt defteri: ", "Uçuş kayıt defteri: ",
"Kara araçlarını gizle: ", "Kara araçlarını gizle: ",
"Havayolu filtresi", "Havayolu filtresi",
"İzleme listesi",
"İzleme uyarısı: ",
"Ayarları yedekle", "Ayarları yedekle",
"Ayarları geri yükle", "Ayarları geri yükle",
"Yedekleme kaydedildi.", "Yedekleme kaydedildi.",
@@ -80,6 +83,16 @@ static const char* const I18N_TR[] = {
"Havayolu ekle", "Havayolu ekle",
"Havayolu ekle (ICAO kodu)", "Havayolu ekle (ICAO kodu)",
"İzlenen uçaklar",
"Bu uçaklardan biri gorundugunde",
"mavi LED yanip soner.",
"Uçak ekle",
"Uçak ekle (çağrı işareti)",
"Henüz izlenen uçak yok.",
"İzleme listesi nasıl çalışır",
"Belirli bir ucusun tam cagri isaretini girin (orn. DLH441) - Havayolu Filtresi'ndeki gibi bir havayolu kodu degil. Ayni anda 5 adete kadar ucak izlenebilir.",
"Izlenen bir ucak radarda herhangi bir yerde gorundugunde, cevresinde camgobegi bir halka belirir ve LED mavi yanip soner - taniidik birinin varisi gibi belirli bir ucusu fark etmek icin harika.",
"Konum ön ayarları", "Konum ön ayarları",
"Otomatik (GPS/IP)", "Otomatik (GPS/IP)",
"Ön ayar", "Ön ayar",
@@ -87,10 +100,12 @@ static const char* const I18N_TR[] = {
"Ön ayar ekle", "Ön ayar ekle",
"Enlem (örn. 48.6833)", "Enlem (örn. 48.6833)",
"Boylam (örn. 9.8333)", "Boylam (örn. 9.8333)",
"En yakın havalimanı: ",
"On ayarlar nasil calisir", "On ayarlar nasil calisir",
"Aynı anda SADECE bir konum aktiftir - Otomatik veya tam olarak bir on ayar. Aktif etmek icin bir girise dokunun.", "Aynı anda SADECE bir konum aktiftir - Otomatik veya tam olarak bir on ayar. Aktif etmek icin bir girise dokunun.",
"Ayrica baska herhangi bir yerin koordinatlarini da girebilirsiniz - orn. Milano, New York veya Tokyo - ve cihazinizin gercek konumundan bagimsiz olarak ORADAKI canli hava trafigini gorebilirsiniz!", "Ayrica baska herhangi bir yerin koordinatlarini da girebilirsiniz - orn. Milano, New York veya Tokyo - ve cihazinizin gercek konumundan bagimsiz olarak ORADAKI canli hava trafigini gorebilirsiniz!",
"Cihaz surekli sabit bir yerde kaliyorsa, otomatik IP tabanli konumdan daha hassas olmasi icin de kullanislidir.", "Cihaz surekli sabit bir yerde kaliyorsa, otomatik IP tabanli konumdan daha hassas olmasi icin de kullanislidir.",
"Bu ekran ayrıca o an aktif olan konuma en yakın havalimanını da gösterir - uzak bir on ayarın tam olarak nereye işaret ettiğini görmek için kullanışlıdır.",
"İstatistikler", "İstatistikler",
"Bugün kaydedilen uçaklar", "Bugün kaydedilen uçaklar",

View File

@@ -68,16 +68,18 @@ bool update(Mode mode, uint32_t nowMs) {
} }
uint32_t interval = (mode == Mode::EmergencyRed) ? RED_BLINK_INTERVAL_MS : GREEN_BLINK_INTERVAL_MS; uint32_t interval = (mode == Mode::EmergencyRed) ? RED_BLINK_INTERVAL_MS : GREEN_BLINK_INTERVAL_MS;
uint8_t pin = (mode == Mode::EmergencyRed) ? PIN_RED : PIN_GREEN; uint8_t pin = (mode == Mode::EmergencyRed) ? PIN_RED
: (mode == Mode::WatchlistBlue) ? PIN_BLUE
: PIN_GREEN;
if (nowMs - lastToggleMs >= interval) { if (nowMs - lastToggleMs >= interval) {
lastToggleMs = nowMs; lastToggleMs = nowMs;
blinkState = !blinkState; blinkState = !blinkState;
} }
digitalWrite(pin, blinkState ? LOW : HIGH); digitalWrite(PIN_RED, (pin == PIN_RED && blinkState) ? LOW : HIGH);
digitalWrite(mode == Mode::EmergencyRed ? PIN_GREEN : PIN_RED, HIGH); digitalWrite(PIN_GREEN, (pin == PIN_GREEN && blinkState) ? LOW : HIGH);
digitalWrite(PIN_BLUE, HIGH); digitalWrite(PIN_BLUE, (pin == PIN_BLUE && blinkState) ? LOW : HIGH);
return blinkState; return blinkState;
} }

View File

@@ -9,6 +9,7 @@ namespace LedAlert {
enum class Mode { enum class Mode {
Off, Off,
ProximityGreen, ProximityGreen,
WatchlistBlue,
EmergencyRed, EmergencyRed,
}; };

View File

@@ -1,5 +1,7 @@
#include "location_presets_screen.h" #include "location_presets_screen.h"
#include "location_presets.h" #include "location_presets.h"
#include "location_manager.h"
#include "airport_lookup.h"
#include "touch_input.h" #include "touch_input.h"
#include "menu_stars.h" #include "menu_stars.h"
#include "config.h" #include "config.h"
@@ -170,8 +172,7 @@ namespace {
constexpr int16_t textMaxWidth = Config::SCREEN_WIDTH - 20; constexpr int16_t textMaxWidth = Config::SCREEN_WIDTH - 20;
constexpr int16_t LINE_H = 16; constexpr int16_t LINE_H = 16;
constexpr int16_t VIEW_TOP = 40; constexpr int16_t VIEW_TOP = 36;
Rect backBtn = {10, (int16_t)(Config::SCREEN_HEIGHT - 50), (int16_t)(Config::SCREEN_WIDTH - 20), 40};
constexpr int16_t VIEW_BOTTOM = Config::SCREEN_HEIGHT - 60; constexpr int16_t VIEW_BOTTOM = Config::SCREEN_HEIGHT - 60;
int16_t totalH = VIEW_TOP; int16_t totalH = VIEW_TOP;
@@ -180,14 +181,19 @@ namespace {
totalH = layoutWrapped(tft, 10, totalH, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA2), 0, 0, 0, false); totalH = layoutWrapped(tft, 10, totalH, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA2), 0, 0, 0, false);
totalH += 8; totalH += 8;
totalH = layoutWrapped(tft, 10, totalH, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA3), 0, 0, 0, false); totalH = layoutWrapped(tft, 10, totalH, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA3), 0, 0, 0, false);
totalH += 8;
totalH = layoutWrapped(tft, 10, totalH, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA4), 0, 0, 0, false);
int16_t maxScroll = totalH - VIEW_BOTTOM; int16_t maxScroll = totalH - VIEW_BOTTOM;
if (maxScroll < 0) maxScroll = 0; if (maxScroll < 0) maxScroll = 0;
bool scrollable = maxScroll > 0; bool scrollable = maxScroll > 0;
int16_t scrollY = 0; int16_t scrollY = 0;
Rect upBtn = {(int16_t)(Config::SCREEN_WIDTH - 34), 4, 30, 24}; Rect backBtn = scrollable
Rect downBtn = {(int16_t)(Config::SCREEN_WIDTH - 34), 30, 30, 24}; ? Rect{10, (int16_t)(Config::SCREEN_HEIGHT - 50), 130, 40}
: Rect{10, (int16_t)(Config::SCREEN_HEIGHT - 50), (int16_t)(Config::SCREEN_WIDTH - 20), 40};
Rect upBtn = {146, (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;
auto redraw = [&]() { auto redraw = [&]() {
@@ -196,20 +202,21 @@ namespace {
tft.setCursor(10, 14); tft.setCursor(10, 14);
tft.println(I18n::t(StringId::LOCATION_INFO_TITLE)); tft.println(I18n::t(StringId::LOCATION_INFO_TITLE));
if (scrollable) {
drawButton(tft, upBtn, "^");
drawButton(tft, downBtn, "v");
}
tft.setTextColor(TFT_GREEN, TFT_BLACK); tft.setTextColor(TFT_GREEN, TFT_BLACK);
int16_t y = VIEW_TOP; int16_t y = VIEW_TOP;
y = layoutWrapped(tft, 10, y, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA1), scrollY, VIEW_TOP, VIEW_BOTTOM, true); y = layoutWrapped(tft, 10, y, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA1), scrollY, VIEW_TOP, VIEW_BOTTOM, true);
y += 8; y += 8;
y = layoutWrapped(tft, 10, y, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA2), scrollY, VIEW_TOP, VIEW_BOTTOM, true); y = layoutWrapped(tft, 10, y, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA2), scrollY, VIEW_TOP, VIEW_BOTTOM, true);
y += 8; y += 8;
layoutWrapped(tft, 10, y, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA3), scrollY, VIEW_TOP, VIEW_BOTTOM, true); y = layoutWrapped(tft, 10, y, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA3), scrollY, VIEW_TOP, VIEW_BOTTOM, true);
y += 8;
layoutWrapped(tft, 10, y, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA4), scrollY, VIEW_TOP, VIEW_BOTTOM, true);
drawButton(tft, backBtn, I18n::t(StringId::BACK)); drawButton(tft, backBtn, I18n::t(StringId::BACK));
if (scrollable) {
drawButton(tft, upBtn, "^");
drawButton(tft, downBtn, "v");
}
}; };
redraw(); redraw();
@@ -296,6 +303,25 @@ void run(TFT_eSPI& tft) {
drawButton(tft, addBtn, I18n::t(StringId::LOCATION_ADD)); drawButton(tft, addBtn, I18n::t(StringId::LOCATION_ADD));
} }
{
double activeLat = 0, activeLon = 0;
if (active < 0) {
LocationManager::getHomeLocation(activeLat, activeLon);
} else {
LocationPresets::getLatLon((uint8_t)active, activeLat, activeLon);
}
AirportLookup::Nearest nearest = AirportLookup::findNearest(activeLat, activeLon);
if (nearest.found) {
char buf[48];
snprintf(buf, sizeof(buf), "%s %s (%.0f km)", nearest.icao, nearest.name, nearest.distanceKm);
String line = String(I18n::t(StringId::LOCATION_NEAREST_AIRPORT_PREFIX)) + buf;
line = truncateForWidth(tft, line, (int16_t)(Config::SCREEN_WIDTH - 20));
tft.setTextColor(TFT_DARKGREEN, TFT_BLACK);
tft.setCursor(10, (int16_t)(Config::SCREEN_HEIGHT - 60));
tft.print(line);
}
}
Rect backBtn = {10, (int16_t)(Config::SCREEN_HEIGHT - 50), (int16_t)(Config::SCREEN_WIDTH - 20), 40}; Rect backBtn = {10, (int16_t)(Config::SCREEN_HEIGHT - 50), (int16_t)(Config::SCREEN_WIDTH - 20), 40};
drawButton(tft, backBtn, I18n::t(StringId::BACK)); drawButton(tft, backBtn, I18n::t(StringId::BACK));

View File

@@ -11,6 +11,7 @@
#include "aircraft_table.h" #include "aircraft_table.h"
#include "airline_lookup.h" #include "airline_lookup.h"
#include "airline_filter.h" #include "airline_filter.h"
#include "aircraft_watchlist.h"
#include "sd_storage.h" #include "sd_storage.h"
#include "wifi_manager.h" #include "wifi_manager.h"
#include "location_manager.h" #include "location_manager.h"
@@ -49,10 +50,12 @@ bool wasEmergency = false;
bool bannerBlinkOn = false; bool bannerBlinkOn = false;
constexpr uint8_t BACKLIGHT_FULL = 255; constexpr uint8_t BACKLIGHT_FULL = 255;
constexpr uint8_t BACKLIGHT_NIGHT_DIM = 90;
constexpr uint8_t BACKLIGHT_DIMMED = 12; constexpr uint8_t BACKLIGHT_DIMMED = 12;
constexpr uint8_t BACKLIGHT_PWM_CHANNEL = 0; constexpr uint8_t BACKLIGHT_PWM_CHANNEL = 0;
uint32_t lastInteractionMs = 0; uint32_t lastInteractionMs = 0;
bool screenDimmed = false; bool screenDimmed = false;
bool nightDimActive = false;
struct Rect { struct Rect {
int16_t x, y, w, h; int16_t x, y, w, h;
@@ -146,6 +149,33 @@ void updateStatusLine() {
updateWifiIcon(); updateWifiIcon();
} }
// Prueft, ob die aktuelle Lokalzeit im Nachtdimm-Fenster (22:00-06:00) liegt.
// Ueber Mitternacht hinweg gerechnet. Solange die Uhrzeit noch nicht per NTP
// synchronisiert ist, wird false zurueckgegeben statt zu raten (gleiche
// Pruefung wie in updateStatusLine()).
bool isNightDimHours() {
time_t now = time(nullptr);
if (now <= 8 * 3600 * 2) return false;
struct tm tmNow;
localtime_r(&now, &tmNow);
int hour = tmNow.tm_hour;
return (hour >= 22 || hour < 6);
}
// Sanfte Nachtdimmung des Backlights zwischen 22:00 und 06:00 Uhr, sofern in
// den Einstellungen aktiviert. Der Inaktivitaets-Timeout (screenDimmed) hat
// Vorrang und wird hier nicht ueberschrieben.
void updateNightDimming() {
if (screenDimmed) return;
bool shouldDim = SettingsStore::nightDimmingEnabled() && isNightDimHours();
if (shouldDim != nightDimActive) {
ledcWrite(BACKLIGHT_PWM_CHANNEL, shouldDim ? BACKLIGHT_NIGHT_DIM : BACKLIGHT_FULL);
nightDimActive = shouldDim;
}
}
void takeScreenshotWithFeedback() { void takeScreenshotWithFeedback() {
LedAlert::flashWhite(); LedAlert::flashWhite();
@@ -240,6 +270,7 @@ void setup() {
WifiMgr::init(); WifiMgr::init();
LocationPresets::init(); LocationPresets::init();
AirlineFilter::init(); AirlineFilter::init();
AircraftWatchlist::init();
SplashScreen::begin(tft); SplashScreen::begin(tft);
SplashScreen::setStatusLine(tft, 0, I18n::t(StringId::SPLASH_SD_OK), TFT_WHITE); SplashScreen::setStatusLine(tft, 0, I18n::t(StringId::SPLASH_SD_OK), TFT_WHITE);
@@ -314,7 +345,9 @@ void loop() {
lastInteractionMs = millis(); lastInteractionMs = millis();
if (screenDimmed) { if (screenDimmed) {
ledcWrite(BACKLIGHT_PWM_CHANNEL, BACKLIGHT_FULL); bool shouldNightDim = SettingsStore::nightDimmingEnabled() && isNightDimHours();
ledcWrite(BACKLIGHT_PWM_CHANNEL, shouldNightDim ? BACKLIGHT_NIGHT_DIM : BACKLIGHT_FULL);
nightDimActive = shouldNightDim;
screenDimmed = false; screenDimmed = false;
tapped = false; tapped = false;
} }
@@ -360,6 +393,7 @@ void loop() {
if (nowMs - lastStatusLineMs >= STATUS_LINE_UPDATE_MS) { if (nowMs - lastStatusLineMs >= STATUS_LINE_UPDATE_MS) {
lastStatusLineMs = nowMs; lastStatusLineMs = nowMs;
updateStatusLine(); updateStatusLine();
updateNightDimming();
} }
uint8_t timeoutMin = SettingsStore::screenTimeoutMinutes(); uint8_t timeoutMin = SettingsStore::screenTimeoutMinutes();

View File

@@ -6,6 +6,7 @@
#include "logbook_files_screen.h" #include "logbook_files_screen.h"
#include "location_presets_screen.h" #include "location_presets_screen.h"
#include "airline_filter_screen.h" #include "airline_filter_screen.h"
#include "aircraft_watchlist_screen.h"
#include "language_screen.h" #include "language_screen.h"
#include "units_screen.h" #include "units_screen.h"
#include "settings_backup.h" #include "settings_backup.h"
@@ -24,8 +25,8 @@ namespace {
} }
}; };
constexpr int16_t ROW_H = 24; constexpr int16_t ROW_H = 22;
constexpr int16_t ROW_GAP = 2; constexpr int16_t ROW_GAP = 1;
constexpr int16_t ROW_START_Y = 18; constexpr int16_t ROW_START_Y = 18;
Rect rowRect(uint8_t index) { Rect rowRect(uint8_t index) {
@@ -152,12 +153,13 @@ void run(TFT_eSPI& tft) {
tft.setCursor(10, 14); tft.setCursor(10, 14);
tft.println(I18n::t(StringId::MENU_CATEGORY_SYSTEM)); tft.println(I18n::t(StringId::MENU_CATEGORY_SYSTEM));
Rect calibBtn = rowRect(0); Rect calibBtn = rowRect(0);
Rect invertBtn = rowRect(1); Rect invertBtn = rowRect(1);
Rect timeoutBtn = rowRect(2); Rect timeoutBtn = rowRect(2);
Rect backupBtn = rowRect(3); Rect nightDimBtn = rowRect(3);
Rect restoreBtn = rowRect(4); Rect backupBtn = rowRect(4);
Rect backBtn = rowRect(5); Rect restoreBtn = rowRect(5);
Rect backBtn = rowRect(6);
drawButton(tft, calibBtn, I18n::t(StringId::MENU_CALIBRATE)); drawButton(tft, calibBtn, I18n::t(StringId::MENU_CALIBRATE));
@@ -167,6 +169,7 @@ void run(TFT_eSPI& tft) {
drawButton(tft, invertBtn, invertLabel); drawButton(tft, invertBtn, invertLabel);
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, backupBtn, I18n::t(StringId::MENU_BACKUP)); drawButton(tft, backupBtn, I18n::t(StringId::MENU_BACKUP));
drawButton(tft, restoreBtn, I18n::t(StringId::MENU_RESTORE)); drawButton(tft, restoreBtn, I18n::t(StringId::MENU_RESTORE));
drawButton(tft, backBtn, I18n::t(StringId::BACK_ARROW)); drawButton(tft, backBtn, I18n::t(StringId::BACK_ARROW));
@@ -188,6 +191,8 @@ void run(TFT_eSPI& tft) {
uint8_t current = SettingsStore::screenTimeoutMinutes(); uint8_t current = SettingsStore::screenTimeoutMinutes();
uint8_t next = (current >= 10) ? 0 : (current + 1); uint8_t next = (current >= 10) ? 0 : (current + 1);
SettingsStore::setScreenTimeoutMinutes(next); SettingsStore::setScreenTimeoutMinutes(next);
} else if (nightDimBtn.contains(tap.x, tap.y)) {
SettingsStore::setNightDimmingEnabled(!SettingsStore::nightDimmingEnabled());
} else if (backupBtn.contains(tap.x, tap.y)) { } else if (backupBtn.contains(tap.x, tap.y)) {
bool ok = SettingsBackup::backup(); bool ok = SettingsBackup::backup();
showBriefMessage(tft, I18n::t(ok ? StringId::MENU_BACKUP_SAVED : StringId::MENU_BACKUP_FAILED), showBriefMessage(tft, I18n::t(ok ? StringId::MENU_BACKUP_SAVED : StringId::MENU_BACKUP_FAILED),
@@ -215,8 +220,10 @@ void run(TFT_eSPI& tft) {
Rect emergencyBtn = rowRect(5); Rect emergencyBtn = rowRect(5);
Rect locationBtn = rowRect(6); Rect locationBtn = rowRect(6);
Rect airlineBtn = rowRect(7); Rect airlineBtn = rowRect(7);
Rect groundBtn = rowRect(8); Rect watchlistBtn = rowRect(8);
Rect backBtn = rowRect(9); Rect watchlistAlertBtn = rowRect(9);
Rect groundBtn = rowRect(10);
Rect backBtn = rowRect(11);
drawButton(tft, statsBtn, I18n::t(StringId::MENU_STATISTICS)); drawButton(tft, statsBtn, I18n::t(StringId::MENU_STATISTICS));
drawButton(tft, logFilesBtn, I18n::t(StringId::MENU_LOGBOOK_FILES)); drawButton(tft, logFilesBtn, I18n::t(StringId::MENU_LOGBOOK_FILES));
@@ -226,6 +233,8 @@ void run(TFT_eSPI& tft) {
drawButton(tft, emergencyBtn, I18n::t(StringId::MENU_EMERGENCY_ALERT) + onOff(SettingsStore::emergencyAlertEnabled())); drawButton(tft, emergencyBtn, I18n::t(StringId::MENU_EMERGENCY_ALERT) + onOff(SettingsStore::emergencyAlertEnabled()));
drawButton(tft, locationBtn, I18n::t(StringId::MENU_LOCATION_PRESETS)); drawButton(tft, locationBtn, I18n::t(StringId::MENU_LOCATION_PRESETS));
drawButton(tft, airlineBtn, I18n::t(StringId::MENU_AIRLINE_FILTER)); drawButton(tft, airlineBtn, I18n::t(StringId::MENU_AIRLINE_FILTER));
drawButton(tft, watchlistBtn, I18n::t(StringId::MENU_WATCHLIST));
drawButton(tft, watchlistAlertBtn, I18n::t(StringId::MENU_WATCHLIST_ALERT) + onOff(SettingsStore::watchlistAlertEnabled()));
drawButton(tft, groundBtn, I18n::t(StringId::MENU_HIDE_GROUND) + onOff(SettingsStore::hideGroundVehicles())); drawButton(tft, groundBtn, I18n::t(StringId::MENU_HIDE_GROUND) + onOff(SettingsStore::hideGroundVehicles()));
drawButton(tft, backBtn, I18n::t(StringId::BACK_ARROW)); drawButton(tft, backBtn, I18n::t(StringId::BACK_ARROW));
@@ -252,6 +261,10 @@ void run(TFT_eSPI& tft) {
LocationPresetsScreen::run(tft); LocationPresetsScreen::run(tft);
} else if (airlineBtn.contains(tap.x, tap.y)) { } else if (airlineBtn.contains(tap.x, tap.y)) {
AirlineFilterScreen::run(tft); AirlineFilterScreen::run(tft);
} else if (watchlistBtn.contains(tap.x, tap.y)) {
AircraftWatchlistScreen::run(tft);
} else if (watchlistAlertBtn.contains(tap.x, tap.y)) {
SettingsStore::setWatchlistAlertEnabled(!SettingsStore::watchlistAlertEnabled());
} else if (groundBtn.contains(tap.x, tap.y)) { } else if (groundBtn.contains(tap.x, tap.y)) {
SettingsStore::setHideGroundVehicles(!SettingsStore::hideGroundVehicles()); SettingsStore::setHideGroundVehicles(!SettingsStore::hideGroundVehicles());
} else if (backBtn.contains(tap.x, tap.y)) { } else if (backBtn.contains(tap.x, tap.y)) {

View File

@@ -11,6 +11,7 @@
#include "location_manager.h" #include "location_manager.h"
#include "world_map.h" #include "world_map.h"
#include "airline_filter.h" #include "airline_filter.h"
#include "aircraft_watchlist.h"
#include "i18n.h" #include "i18n.h"
#include <math.h> #include <math.h>
@@ -56,6 +57,7 @@ namespace {
float headingDeg; float headingDeg;
float distanceKm; float distanceKm;
bool isEmergency; bool isEmergency;
bool isWatched;
}; };
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];
@@ -487,6 +489,7 @@ void render(TFT_eSPI& tft, int16_t top) {
uint16_t color = colorForAltitude(a.altBaroFt); uint16_t color = colorForAltitude(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);
bool isWatched = SettingsStore::watchlistAlertEnabled() && AircraftWatchlist::isWatched(a.callsign);
TrailEntry* trail = findOrCreateTrail(a.hex); TrailEntry* trail = findOrCreateTrail(a.hex);
uint16_t trailColor = dimColorForAltitude(a.altBaroFt); uint16_t trailColor = dimColorForAltitude(a.altBaroFt);
@@ -503,6 +506,8 @@ void render(TFT_eSPI& tft, int16_t top) {
if (isEmergency) { if (isEmergency) {
tft.drawCircle(pt.x, pt.y, 12, TFT_RED); tft.drawCircle(pt.x, pt.y, 12, TFT_RED);
} else if (isWatched) {
tft.drawCircle(pt.x, pt.y, 12, TFT_CYAN);
} }
tft.setTextColor(color); tft.setTextColor(color);
@@ -518,6 +523,7 @@ void render(TFT_eSPI& tft, int16_t top) {
hitPoints[i].headingDeg = a.headingDeg; hitPoints[i].headingDeg = a.headingDeg;
hitPoints[i].distanceKm = a.distanceKm; hitPoints[i].distanceKm = a.distanceKm;
hitPoints[i].isEmergency = isEmergency; hitPoints[i].isEmergency = isEmergency;
hitPoints[i].isWatched = isWatched;
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);
} }
@@ -602,6 +608,8 @@ void tick(TFT_eSPI& tft, int16_t top, uint32_t deltaMs) {
if (hp.isEmergency) { if (hp.isEmergency) {
tft.drawCircle(hp.x, hp.y, 12, TFT_RED); tft.drawCircle(hp.x, hp.y, 12, TFT_RED);
} else if (hp.isWatched) {
tft.drawCircle(hp.x, hp.y, 12, TFT_CYAN);
} }
tft.setTextColor(hp.color); tft.setTextColor(hp.color);
@@ -671,22 +679,26 @@ bool handleTap(int16_t x, int16_t y, int16_t top) {
void updateProximityAlert(uint32_t nowMs) { void updateProximityAlert(uint32_t nowMs) {
bool anyClose = false; bool anyClose = false;
bool anyEmergency = false; bool anyEmergency = false;
bool anyWatched = false;
bool proximityOn = SettingsStore::proximityAlertEnabled(); bool proximityOn = SettingsStore::proximityAlertEnabled();
bool emergencyOn = SettingsStore::emergencyAlertEnabled(); bool emergencyOn = SettingsStore::emergencyAlertEnabled();
bool watchlistOn = SettingsStore::watchlistAlertEnabled();
if (proximityOn || emergencyOn) { if (proximityOn || emergencyOn || watchlistOn) {
AircraftTable::lock(); AircraftTable::lock();
Aircraft* table = AircraftTable::raw(); Aircraft* table = AircraftTable::raw();
for (uint8_t i = 0; i < AircraftTable::capacity(); i++) { for (uint8_t i = 0; i < AircraftTable::capacity(); i++) {
if (!table[i].valid) continue; if (!table[i].valid) continue;
if (proximityOn && table[i].distanceKm <= Config::LED_ALERT_RADIUS_KM) anyClose = true; if (proximityOn && table[i].distanceKm <= Config::LED_ALERT_RADIUS_KM) anyClose = true;
if (emergencyOn && isEmergencySquawk(table[i].squawk)) anyEmergency = true; if (emergencyOn && isEmergencySquawk(table[i].squawk)) anyEmergency = true;
if (watchlistOn && AircraftWatchlist::isWatched(table[i].callsign)) anyWatched = true;
} }
AircraftTable::unlock(); AircraftTable::unlock();
} }
LedAlert::Mode mode = anyEmergency ? LedAlert::Mode::EmergencyRed LedAlert::Mode mode = anyEmergency ? LedAlert::Mode::EmergencyRed
: anyWatched ? LedAlert::Mode::WatchlistBlue
: anyClose ? LedAlert::Mode::ProximityGreen : anyClose ? LedAlert::Mode::ProximityGreen
: LedAlert::Mode::Off; : LedAlert::Mode::Off;

View File

@@ -50,6 +50,43 @@ namespace {
"CRJ9,90\n" "CRJ9,90\n"
"DH8D,78\n"; "DH8D,78\n";
const char* kDefaultAirportsCsv =
"icao,name,lat,lon\n"
"EDDF,Frankfurt,50.0379,8.5622\n"
"EDDM,Muenchen,48.3538,11.7861\n"
"EDDB,Berlin Brandenburg,52.3667,13.5033\n"
"EDDL,Duesseldorf,51.2895,6.7668\n"
"EDDH,Hamburg,53.6304,9.9882\n"
"EDDS,Stuttgart,48.6900,9.2219\n"
"LFPG,Paris CDG,49.0097,2.5479\n"
"EGLL,London Heathrow,51.4700,-0.4543\n"
"LEMD,Madrid Barajas,40.4936,-3.5668\n"
"LIRF,Roma Fiumicino,41.8003,12.2389\n"
"EHAM,Amsterdam Schiphol,52.3086,4.7639\n"
"LSZH,Zuerich,47.4647,8.5492\n"
"LOWW,Wien,48.1103,16.5697\n"
"EKCH,Kopenhagen,55.6180,12.6560\n"
"ESSA,Stockholm Arlanda,59.6519,17.9186\n"
"LTFM,Istanbul,41.2753,28.7519\n"
"OMDB,Dubai,25.2532,55.3657\n"
"OTHH,Doha Hamad,25.2609,51.6138\n"
"KJFK,New York JFK,40.6413,-73.7781\n"
"KLAX,Los Angeles,33.9416,-118.4085\n"
"KORD,Chicago O'Hare,41.9742,-87.9073\n"
"CYYZ,Toronto Pearson,43.6777,-79.6248\n"
"RJTT,Tokyo Haneda,35.5494,139.7798\n"
"RJAA,Tokyo Narita,35.7647,140.3864\n"
"ZBAA,Beijing Capital,40.0801,116.5846\n"
"VHHH,Hong Kong,22.3080,113.9185\n"
"WSSS,Singapore Changi,1.3644,103.9915\n"
"YSSY,Sydney,-33.9399,151.1753\n"
"FAOR,Johannesburg OR Tambo,-26.1392,28.2460\n"
"SBGR,Sao Paulo Guarulhos,-23.4356,-46.4731\n"
"OMAA,Abu Dhabi,24.4330,54.6511\n"
"LGAV,Athen,37.9364,23.9445\n"
"LPPT,Lissabon,38.7756,-9.1354\n"
"EPWA,Warschau Chopin,52.1657,20.9671\n";
bool ensureDir(const char* path) { bool ensureDir(const char* path) {
if (SD.exists(path)) return true; if (SD.exists(path)) return true;
return SD.mkdir(path); return SD.mkdir(path);
@@ -86,6 +123,7 @@ void seedDefaultDataFiles() {
SdMutex::Guard guard; SdMutex::Guard guard;
writeIfAbsent(Config::SD_AIRLINES_CSV, kDefaultAirlinesCsv); writeIfAbsent(Config::SD_AIRLINES_CSV, kDefaultAirlinesCsv);
writeIfAbsent(Config::SD_AIRCRAFT_TYPES_CSV, kDefaultAircraftTypesCsv); writeIfAbsent(Config::SD_AIRCRAFT_TYPES_CSV, kDefaultAircraftTypesCsv);
writeIfAbsent(Config::SD_AIRPORTS_CSV, kDefaultAirportsCsv);
} }
void logEvent(const char* csvLine) { void logEvent(const char* csvLine) {

View File

@@ -10,9 +10,11 @@ namespace {
bool inverted = true; // Dieses Board braucht invertDisplay(true) fuer korrekte Farben (siehe main.cpp) bool inverted = true; // Dieses Board braucht invertDisplay(true) fuer korrekte Farben (siehe main.cpp)
bool emergencyAlertOn = true; bool emergencyAlertOn = true;
bool proximityAlertOn = true; bool proximityAlertOn = true;
bool watchlistAlertOn = true;
bool flightLogbookOn = true; bool flightLogbookOn = true;
bool ledHeartbeatOn = true; bool ledHeartbeatOn = true;
uint8_t screenTimeoutMin = 0; uint8_t screenTimeoutMin = 0;
bool nightDimmingOn = true;
bool hideGroundVehiclesOn = true; bool hideGroundVehiclesOn = true;
uint8_t languageIdx = 0; uint8_t languageIdx = 0;
uint8_t unitsModeVal = 0; uint8_t unitsModeVal = 0;
@@ -29,6 +31,8 @@ namespace {
emergencyAlertOn = (value.toInt() != 0); emergencyAlertOn = (value.toInt() != 0);
} else if (key == "proximity_alert") { } else if (key == "proximity_alert") {
proximityAlertOn = (value.toInt() != 0); proximityAlertOn = (value.toInt() != 0);
} else if (key == "watchlist_alert") {
watchlistAlertOn = (value.toInt() != 0);
} else if (key == "flight_logbook") { } else if (key == "flight_logbook") {
flightLogbookOn = (value.toInt() != 0); flightLogbookOn = (value.toInt() != 0);
} else if (key == "led_heartbeat") { } else if (key == "led_heartbeat") {
@@ -36,6 +40,8 @@ namespace {
} else if (key == "screen_timeout_min") { } else if (key == "screen_timeout_min") {
int v = value.toInt(); int v = value.toInt();
if (v >= 0 && v <= 10) screenTimeoutMin = (uint8_t)v; if (v >= 0 && v <= 10) screenTimeoutMin = (uint8_t)v;
} else if (key == "night_dimming") {
nightDimmingOn = (value.toInt() != 0);
} else if (key == "hide_ground_vehicles") { } else if (key == "hide_ground_vehicles") {
hideGroundVehiclesOn = (value.toInt() != 0); hideGroundVehiclesOn = (value.toInt() != 0);
} else if (key == "language") { } else if (key == "language") {
@@ -82,9 +88,11 @@ void save() {
f.printf("invert=%d\n", inverted ? 1 : 0); f.printf("invert=%d\n", inverted ? 1 : 0);
f.printf("emergency_alert=%d\n", emergencyAlertOn ? 1 : 0); f.printf("emergency_alert=%d\n", emergencyAlertOn ? 1 : 0);
f.printf("proximity_alert=%d\n", proximityAlertOn ? 1 : 0); f.printf("proximity_alert=%d\n", proximityAlertOn ? 1 : 0);
f.printf("watchlist_alert=%d\n", watchlistAlertOn ? 1 : 0);
f.printf("flight_logbook=%d\n", flightLogbookOn ? 1 : 0); f.printf("flight_logbook=%d\n", flightLogbookOn ? 1 : 0);
f.printf("led_heartbeat=%d\n", ledHeartbeatOn ? 1 : 0); f.printf("led_heartbeat=%d\n", ledHeartbeatOn ? 1 : 0);
f.printf("screen_timeout_min=%d\n", screenTimeoutMin); f.printf("screen_timeout_min=%d\n", screenTimeoutMin);
f.printf("night_dimming=%d\n", nightDimmingOn ? 1 : 0);
f.printf("hide_ground_vehicles=%d\n", hideGroundVehiclesOn ? 1 : 0); f.printf("hide_ground_vehicles=%d\n", hideGroundVehiclesOn ? 1 : 0);
f.printf("language=%d\n", languageIdx); f.printf("language=%d\n", languageIdx);
f.printf("units_mode=%d\n", unitsModeVal); f.printf("units_mode=%d\n", unitsModeVal);
@@ -121,6 +129,13 @@ void setProximityAlertEnabled(bool on) {
save(); save();
} }
bool watchlistAlertEnabled() { return watchlistAlertOn; }
void setWatchlistAlertEnabled(bool on) {
watchlistAlertOn = on;
save();
}
bool flightLogbookEnabled() { return flightLogbookOn; } bool flightLogbookEnabled() { return flightLogbookOn; }
void setFlightLogbookEnabled(bool on) { void setFlightLogbookEnabled(bool on) {
@@ -144,6 +159,13 @@ void setScreenTimeoutMinutes(uint8_t minutes) {
} }
} }
bool nightDimmingEnabled() { return nightDimmingOn; }
void setNightDimmingEnabled(bool on) {
nightDimmingOn = on;
save();
}
bool hideGroundVehicles() { return hideGroundVehiclesOn; } bool hideGroundVehicles() { return hideGroundVehiclesOn; }
void setHideGroundVehicles(bool on) { void setHideGroundVehicles(bool on) {

View File

@@ -17,6 +17,9 @@ namespace SettingsStore {
bool proximityAlertEnabled(); bool proximityAlertEnabled();
void setProximityAlertEnabled(bool on); void setProximityAlertEnabled(bool on);
bool watchlistAlertEnabled();
void setWatchlistAlertEnabled(bool on);
bool flightLogbookEnabled(); bool flightLogbookEnabled();
void setFlightLogbookEnabled(bool on); void setFlightLogbookEnabled(bool on);
@@ -26,6 +29,9 @@ namespace SettingsStore {
uint8_t screenTimeoutMinutes(); uint8_t screenTimeoutMinutes();
void setScreenTimeoutMinutes(uint8_t minutes); void setScreenTimeoutMinutes(uint8_t minutes);
bool nightDimmingEnabled();
void setNightDimmingEnabled(bool on);
bool hideGroundVehicles(); bool hideGroundVehicles();
void setHideGroundVehicles(bool on); void setHideGroundVehicles(bool on);

View File

@@ -1,5 +1,6 @@
#include "splash_screen.h" #include "splash_screen.h"
#include "menu_stars.h" #include "menu_stars.h"
#include <math.h>
namespace SplashScreen { namespace SplashScreen {
@@ -20,17 +21,74 @@ namespace {
tft.drawFastVLine(cx, cy - 80, 160, dim); tft.drawFastVLine(cx, cy - 80, 160, dim);
} }
void drawMiniJet(TFT_eSPI& tft, int16_t x, int16_t y, uint16_t color) { // Dreht einen Punkt (px, py) im lokalen Koordinatensystem der kleinen
tft.drawLine(x, y - 10, x, y + 12, color); // Deko-Symbole um angleDeg und verschiebt ihn nach (x, y).
tft.drawLine(x - 12, y + 2, x + 12, y + 2, color); void rotatePoint(int16_t x, int16_t y, float sinA, float cosA,
tft.drawLine(x - 5, y + 10, x + 5, y + 10, color); float px, float py, int16_t& outX, int16_t& outY) {
outX = x + (int16_t)lroundf(px * cosA - py * sinA);
outY = y + (int16_t)lroundf(px * sinA + py * cosA);
} }
void drawMiniHeli(TFT_eSPI& tft, int16_t x, int16_t y, uint16_t color) { // Kleines Flugzeug-Silhouette aus gefuellten Dreiecken (Rumpf, Tragflaechen,
tft.drawLine(x - 8, y - 6, x + 8, y - 6, color); // Leitwerk), analog zum grossen Flugzeug in der Mitte, aber schlichter und
tft.drawLine(x, y - 6, x, y + 4, color); // in beliebiger Richtung (angleDeg, 0 = Nase zeigt nach oben).
tft.drawCircle(x, y + 2, 4, color); void drawMiniJet(TFT_eSPI& tft, int16_t x, int16_t y, uint16_t color, float angleDeg) {
tft.drawLine(x - 4, y + 7, x + 4, y + 7, color); float rad = angleDeg * (PI / 180.0f);
float s = sinf(rad), c = cosf(rad);
int16_t nx, ny, flx, fly, frx, fry;
rotatePoint(x, y, s, c, 0, -11, nx, ny);
rotatePoint(x, y, s, c, -3, 11, flx, fly);
rotatePoint(x, y, s, c, 3, 11, frx, fry);
tft.fillTriangle(nx, ny, flx, fly, frx, fry, color);
int16_t wlx, wly, wrx, wry, wcx, wcy;
rotatePoint(x, y, s, c, -13, 3, wlx, wly);
rotatePoint(x, y, s, c, 13, 3, wrx, wry);
rotatePoint(x, y, s, c, 0, -3, wcx, wcy);
tft.fillTriangle(wlx, wly, wrx, wry, wcx, wcy, color);
int16_t tlx, tly, trx, try_, ttx, tty;
rotatePoint(x, y, s, c, -5, 8, tlx, tly);
rotatePoint(x, y, s, c, 5, 8, trx, try_);
rotatePoint(x, y, s, c, 0, 13, ttx, tty);
tft.fillTriangle(tlx, tly, trx, try_, ttx, tty, color);
}
// Kleiner Helikopter: Rotorkreis mit Blaettern, gefuellter Rumpf, Heckausleger
// mit Leitwerk. angleDeg bestimmt die Flugrichtung (0 = Nase nach oben).
void drawMiniHeli(TFT_eSPI& tft, int16_t x, int16_t y, uint16_t color, float angleDeg) {
float rad = angleDeg * (PI / 180.0f);
float s = sinf(rad), c = cosf(rad);
int16_t rcx, rcy;
rotatePoint(x, y, s, c, 0, -7, rcx, rcy);
tft.drawCircle(rcx, rcy, 6, color);
int16_t b1x, b1y, b2x, b2y;
rotatePoint(x, y, s, c, -7, -7, b1x, b1y);
rotatePoint(x, y, s, c, 7, -7, b2x, b2y);
tft.drawLine(b1x, b1y, b2x, b2y, color);
rotatePoint(x, y, s, c, 0, -13, b1x, b1y);
rotatePoint(x, y, s, c, 0, -1, b2x, b2y);
tft.drawLine(b1x, b1y, b2x, b2y, color);
int16_t cx1, cy1, cx2, cy2, cx3, cy3;
rotatePoint(x, y, s, c, 0, -6, cx1, cy1);
rotatePoint(x, y, s, c, -4, 4, cx2, cy2);
rotatePoint(x, y, s, c, 4, 4, cx3, cy3);
tft.fillTriangle(cx1, cy1, cx2, cy2, cx3, cy3, color);
int16_t tbx1, tby1, tbx2, tby2;
rotatePoint(x, y, s, c, 0, 4, tbx1, tby1);
rotatePoint(x, y, s, c, 0, 12, tbx2, tby2);
tft.drawLine(tbx1, tby1, tbx2, tby2, color);
int16_t fx1, fy1, fx2, fy2, fx3, fy3;
rotatePoint(x, y, s, c, -3, 10, fx1, fy1);
rotatePoint(x, y, s, c, 3, 10, fx2, fy2);
rotatePoint(x, y, s, c, 0, 15, fx3, fy3);
tft.fillTriangle(fx1, fy1, fx2, fy2, fx3, fy3, color);
} }
void drawAirplane(TFT_eSPI& tft, int16_t cx) { void drawAirplane(TFT_eSPI& tft, int16_t cx) {
@@ -55,10 +113,10 @@ void begin(TFT_eSPI& tft) {
drawRadarReticle(tft, cx, 174); drawRadarReticle(tft, cx, 174);
uint16_t dimGreen = 0x0320; uint16_t dimGreen = 0x0320;
drawMiniJet(tft, cx - 45, 135, dimGreen); drawMiniJet(tft, cx - 45, 135, dimGreen, 25.0f);
drawMiniJet(tft, cx + 50, 160, dimGreen); drawMiniJet(tft, cx + 50, 160, dimGreen, 160.0f);
drawMiniHeli(tft, cx - 35, 215, dimGreen); drawMiniHeli(tft, cx - 35, 215, dimGreen, 250.0f);
drawMiniHeli(tft, cx + 45, 115, dimGreen); drawMiniHeli(tft, cx + 45, 115, dimGreen, 100.0f);
drawAirplane(tft, cx); drawAirplane(tft, cx);

View File

@@ -61,8 +61,7 @@ namespace {
constexpr int16_t textMaxWidth = Config::SCREEN_WIDTH - 20; constexpr int16_t textMaxWidth = Config::SCREEN_WIDTH - 20;
constexpr int16_t LINE_H = 16; constexpr int16_t LINE_H = 16;
constexpr int16_t VIEW_TOP = 40; constexpr int16_t VIEW_TOP = 36;
Rect backBtn = {10, (int16_t)(Config::SCREEN_HEIGHT - 50), (int16_t)(Config::SCREEN_WIDTH - 20), 40};
constexpr int16_t VIEW_BOTTOM = Config::SCREEN_HEIGHT - 60; constexpr int16_t VIEW_BOTTOM = Config::SCREEN_HEIGHT - 60;
int16_t totalH = VIEW_TOP; int16_t totalH = VIEW_TOP;
@@ -77,8 +76,11 @@ namespace {
bool scrollable = maxScroll > 0; bool scrollable = maxScroll > 0;
int16_t scrollY = 0; int16_t scrollY = 0;
Rect upBtn = {(int16_t)(Config::SCREEN_WIDTH - 34), 4, 30, 24}; Rect backBtn = scrollable
Rect downBtn = {(int16_t)(Config::SCREEN_WIDTH - 34), 30, 30, 24}; ? Rect{10, (int16_t)(Config::SCREEN_HEIGHT - 50), 130, 40}
: Rect{10, (int16_t)(Config::SCREEN_HEIGHT - 50), (int16_t)(Config::SCREEN_WIDTH - 20), 40};
Rect upBtn = {146, (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;
auto redraw = [&]() { auto redraw = [&]() {
@@ -87,11 +89,6 @@ namespace {
tft.setCursor(10, 14); tft.setCursor(10, 14);
tft.println(I18n::t(StringId::WIFI_INFO_TITLE)); tft.println(I18n::t(StringId::WIFI_INFO_TITLE));
if (scrollable) {
drawButton(tft, upBtn, "^");
drawButton(tft, downBtn, "v");
}
tft.setTextColor(TFT_GREEN, TFT_BLACK); tft.setTextColor(TFT_GREEN, TFT_BLACK);
int16_t y = VIEW_TOP; int16_t y = VIEW_TOP;
y = layoutWrapped(tft, 10, y, textMaxWidth, LINE_H, I18n::t(StringId::WIFI_INFO_PARA1), scrollY, VIEW_TOP, VIEW_BOTTOM, true); y = layoutWrapped(tft, 10, y, textMaxWidth, LINE_H, I18n::t(StringId::WIFI_INFO_PARA1), scrollY, VIEW_TOP, VIEW_BOTTOM, true);
@@ -101,6 +98,10 @@ namespace {
layoutWrapped(tft, 10, y, textMaxWidth, LINE_H, I18n::t(StringId::WIFI_INFO_PARA3), scrollY, VIEW_TOP, VIEW_BOTTOM, true); layoutWrapped(tft, 10, y, textMaxWidth, LINE_H, I18n::t(StringId::WIFI_INFO_PARA3), scrollY, VIEW_TOP, VIEW_BOTTOM, true);
drawButton(tft, backBtn, I18n::t(StringId::BACK)); drawButton(tft, backBtn, I18n::t(StringId::BACK));
if (scrollable) {
drawButton(tft, upBtn, "^");
drawButton(tft, downBtn, "v");
}
}; };
redraw(); redraw();