v2.4.0: Night dimming, aircraft watchlist, and nearest-airport lookup
This commit is contained in:
146
CLAUDE.md
Normal file
146
CLAUDE.md
Normal 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
115
src/aircraft_watchlist.cpp
Normal 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
16
src/aircraft_watchlist.h
Normal 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);
|
||||||
|
}
|
||||||
320
src/aircraft_watchlist_screen.cpp
Normal file
320
src/aircraft_watchlist_screen.cpp
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
7
src/aircraft_watchlist_screen.h
Normal file
7
src/aircraft_watchlist_screen.h
Normal 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
51
src/airport_lookup.cpp
Normal 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
12
src/airport_lookup.h
Normal 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);
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
|
|||||||
15
src/i18n.h
15
src/i18n.h
@@ -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,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ namespace LedAlert {
|
|||||||
enum class Mode {
|
enum class Mode {
|
||||||
Off,
|
Off,
|
||||||
ProximityGreen,
|
ProximityGreen,
|
||||||
|
WatchlistBlue,
|
||||||
EmergencyRed,
|
EmergencyRed,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|
||||||
|
|||||||
36
src/main.cpp
36
src/main.cpp
@@ -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();
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user