localization

This commit is contained in:
2026-08-16 14:33:27 +02:00
parent 9c634b819e
commit f39fdf104d
65 changed files with 1621 additions and 1679 deletions

267
AGENTS.md Normal file
View File

@@ -0,0 +1,267 @@
# Eiswolfs Flightradar (CYD) — Project Context for Claude Code
Read this file FIRST before making any code changes. It contains the
things you need to know to avoid repeating the same bugs that have been
encountered and fixed during development.
## What is this project?
A live ADS-B flight radar on an ESP32 "Cheap Yellow Display" (CYD,
ESP32-2432S028), 240x320 touch TFT. Shows nearby aircraft on a
rotating radar screen with a detail panel (model, altitude, speed, route,
squawk), proximity LED alert, WiFi management (up to 3 networks),
location presets (including remote locations worldwide), airline filter, flight logbook
with statistics, and menus/splash screen with a subtle twinkling
star animation.
Current version: **v3.6.3**. Public repo:
https://github.com/Eiswolf-BG/eiswolfs-flightradar-CYD
## Who uses this?
Alex — a complete beginner at Arduino/embedded development, working with
VS Code + PlatformIO on a Mac. Please respond in a way that is accessible;
avoid assuming familiarity with technical jargon.
## Tech Stack
- PlatformIO, `platform = espressif32`, `board = esp32dev`, `framework = arduino`
- TFT_eSPI (display), XPT2046_Touchscreen (touch), ArduinoJson, TinyGPSPlus
- Dual-core design: networking (WiFi, ADS-B polling) on Core 0, display/touch
on Core 1 — the UI never blocks on network requests.
- Data sources: [adsb.fi](https://adsb.fi) (aircraft positions), [hexdb.io](https://hexdb.io)
(model lookups), [ip-api.com](https://ip-api.com) (IP geolocation)
## ⚠️ MOST IMPORTANT GOTCHA: Custom font is baseline-anchored
The built-in TFT_eSPI fonts (GLCD, Font2 etc.) are pure ASCII. For
umlauts/accents (German, Turkish, French, Spanish, Italian)
there is a **self-generated font** (`src/ui_font.h`, activated globally
via `tft.setFreeFont(&UiFont11pt)` in `main.cpp::setup()`, which applies
to EVERY `print()`/`drawString()` call throughout the app).
**The critical difference from built-in fonts:** With
`setCursor(x, y); print(...)`, the `y` coordinate for our font is the **baseline**,
NOT the top edge as with the old GLCD font. Text grows
UPWARD from `y` (by the ascent, roughly 9px at Size 1, roughly 16-18px at
Size 2), not downward.
**This has caused the following bugs in the past (all fixed, but
be careful with new code!):**
- Values of `y` that are too small (e.g. `setCursor(10, 2)`) → text protrudes
out of or is clipped at the top of the screen/container. **Rule of thumb:
y should never be less than ~14 at Size 1, never less than
~24-26 at Size 2 (depending on the container).**
- Input fields/boxes: Baseline must be near the BOTTOM edge of the box,
not in the middle as one might be used to from the old font.
- Two text sizes in quick succession within a tight layout
(label at Size 1 directly above value at Size 2) are error-prone — in the
statistics screen this was deliberately changed to a UNIFORM size (only color
distinguishes label/value), which is more robust.
- `drawString()` with `setTextDatum(MC_DATUM)` (centered) is NOT
affected — TFT_eSPI handles centering correctly regardless of whether the
font is baseline- or top-anchored. Only raw `setCursor()`+`print()` is
the danger zone.
- Long multi-line text (e.g. explanatory text) should NEVER rely on TFT_eSPI's
built-in auto-wrap — it breaks mid-word. Instead,
use the `layoutWrapped()` helper (implemented once each in
`location_presets_screen.cpp` and `wifi_manage_screen.cpp`, handles word-wise
wrapping based on actual pixel width plus optional scrolling).
If new screens or text areas are added: test more than once (ideally
with a photo of the actual display) before considering the code done.
## i18n (6 languages)
- `src/i18n.h`: `enum class StringId` — every fixed UI text has an ID.
- `src/i18n_en.h`, `i18n_de.h`, `i18n_fr.h`, `i18n_tr.h`, `i18n_es.h`,
`i18n_it.h`: each a `static const char* const[]` array, in **exactly the
same order** as the enum.
- Each file has a `static_assert` at the end checking the array size against
`StringId::COUNT` — **if the build fails due to a failing static_assert,
at least one language file is missing an entry or has one too many.**
New StringId → add in ALL 6 files at the same position, otherwise the
mapping shifts.
- Language names themselves (`I18n::languageName()`) are stored separately in
`i18n.cpp`, with correct native special characters
(e.g. "Français", "Türkçe", "Español").
## UI Conventions (please follow for new screens)
- **Color scheme:** Black background, green frame/text
(`TFT_BLACK`/`TFT_GREEN`), active/selected entries inverted (green
fill, black text). Destructive actions (cancel/delete) in red
(`TFT_RED`).
- Each screen typically has a local `struct Rect` with `contains(x,y)` and
a `drawButton()` helper function (copy-paste pattern from existing
screens, no shared Rect/Button module — this is intentional, to keep
each screen independently runnable).
- **Star animation** (`src/menu_stars.h/.cpp`): Runs in the background on
EVERY black menu/splash screen. New screens should call
`MenuStars::reset()` once on enter and
`MenuStars::update(tft)` in every wait/idle loop (the loop otherwise
runs idle since the function internally throttles itself to ~60ms).
- Wait loop pattern for touch input:
```cpp
TouchInput::Point tap;
while (true) {
if (TouchInput::wasTapped(tap)) break;
MenuStars::update(tft);
delay(20);
}
```
- Menu structure: Main menu → 4 categories (Region & Language, WiFi/Network,
System, Flight Options) → subpages each. WiFi/Network currently has
NO submenu anymore, jumps directly into network management.
- Many screens with text input (airline code, coordinates, WiFi password)
have a "?" info button at the top right that opens a scrollable
explanation screen (pattern: `location_presets_screen.cpp` and
`wifi_manage_screen.cpp`, each with its own `layoutWrapped()` copy).
## Other fixed values (Config::...)
- Proximity alert radius: 3 km
- Emergency squawks: 7500 (hijack), 7600 (radio failure), 7700 (emergency)
- Radar ranges: 10/25/50/100 km
- ADS-B fetch interval: every 8 seconds
- Altitude color coding: Green <10,000ft, Yellow 10-30,000ft, Red >30,000ft
- Max. 3 WiFi networks, max. 3 location presets, max. 10 filtered airlines
## Code Style
- All comments in English. Use clear, concise English for implementation
notes and rationale.
- Build check after EVERY change: `pio run` in the project directory, check
for BOTH warnings AND errors (not just "compiles").
- Always prefer least-invasive changes — do not rename existing
functions/names without reason.
## Language: Project outward-facing text always in English
All outwardly visible text must ALWAYS be written in English —
regardless of which language the conversation with Alex is conducted in.
This applies to:
- `README.md`
- `index.html` (website/flasher page)
- GitHub release notes / descriptions
- Commit messages
- Any other descriptive or bugfix text that is publicly visible
(e.g. on GitHub)
Exception: The firmware UI itself remains multilingual as before
(`i18n_de/en/fr/tr/es/it.h`) — this rule only applies to the
project's outward-facing presentation (repo, release notes, website), not
the app interface on the device.
## Known open points / possible next steps
No acute open bugs known (as of v3.6.3). Possible ideas for later,
if the user asks: add info buttons to more menus, possibly more languages,
possibly export/share logbook data.
## Standard Workflow: Push & Release
IMPORTANT — when this workflow starts: The complete release workflow
(README update, commit, tag, push) may ONLY be started when Alex
EXPLICITLY asks for it (e.g. "let's push", "can we release", "run the
release workflow"). A simple "yes" in response to a follow-up question
(e.g. about a CLAUDE.md change or some other detail) is NOT a request to
start the release workflow. For small fixes/changes, ONLY build
and flash (see the "Auto-flash after every successful build" section below),
but do NOT commit/tag/push until explicitly asked.
Once the workflow is explicitly requested, automatically follow these steps
in order:
0. Determine the version number: The new version number ALWAYS comes exactly
from Alex — he names it in the push request (e.g. via Claude/the sandbox
assistant: "Alex wants to push version X.Y.Z"). Enter THAT EXACT
number into `Config::APP_VERSION` (`src/config.h`) — never
increment, guess, or derive it from the last version (not even for small
patches). This also applies to larger jumps (e.g.
2.6 -> 3.0) that Alex may deliberately and intentionally make — always
adopt the stated number 1:1 without making assumptions.
If no explicit version number was given in the push request, ask
Alex rather than guessing. Only AFTER entering the correct
number: do a clean `pio run` build, THEN the rest of the known workflow
(README, tag, index.html, bin files, commit/push).
(Here, intentionally build ONLY, do NOT flash — exception to the otherwise
applicable auto-flash rule, see the "Auto-flash after every successful
build" section below. The test device should stay on the current version
so the new release can be tested via OTA.)
1. Check whether any new/changed features have been added since the last
commit that are visible to end users (new menu items, changed behavior,
new screens) — if so, **update README.md accordingly**
(same style: emoji headings, anchor links between the "Features" list
and the deep-dive sections, short examples where useful). Purely internal
bugfixes/refactorings with no visible user impact do not need a
README entry.
2. Commit the code (meaningful commit message).
3. If it is a version jump: Create a git tag with the
version number + description of changes.
4. Check whether `index.html` (web flasher) still shows the old version
number — if so, update it. **NOT OPTIONAL, must NEVER be skipped
on any release push** — not even for small patch versions. Always treat
as a fixed double-step together with Step 1: whenever
the README (or even just the version number) is updated to a new version,
ALWAYS check and update `index.html` in the same pass.
4b. Keep the web flasher version selection current: Before overwriting the
root `.bin` files with the new build, archive the CURRENT (still old)
bootloader.bin/partitions.bin/firmware.bin into a new folder
`versions/vOLD/` (vOLD = the version number that index.html
showed before this update) — also put a `manifest.json` there
(identical content to the root manifest.json, see
`versions/v3.6.2/manifest.json` as a template). Then in the
`versions/` folder keep only the 2 newest version folders (sorted by
version number, not file date) — delete older folders.
Then update the version dropdown in `index.html`
(`<select id="versionSelect">`): 3 options — the new
current version (`value="manifest.json"`, `data-version="vNEW"`, text
"vNEW (latest)") plus the two versions now remaining in `versions/`
(`value="versions/vX.Y.Z/manifest.json"`, newest first).
5. Check whether `bootloader.bin`, `firmware.bin`, `partitions.bin` in the
project root match the current build in `.pio/build/esp32dev/` —
if not, copy them from there.
6. Commit and push all these changes (code + README + web flasher files)
together (`git push`, plus `git push origin vX.Y.Z` if a
tag was created).
7. Create a GitHub Release AND upload the `.bin` file in one step
(via the `gh` CLI, set up and authenticated since v2.7.5 — see
`gh auth status`). The release asset is simply called `firmware.bin`, no
renaming needed — most users download via the
web flasher anyway; the asset is only relevant for the few people who
need a `.bin` file directly via a CYD launcher app (the filename
doesn't matter for that):
gh release create vX.Y.Z .pio/build/esp32dev/firmware.bin \
--repo Eiswolf-BG/eiswolfs-flightradar-CYD \
--title "vX.Y.Z" \
--notes "<Release-notes-text>"
The release notes text comes from the respective push request (the same
text used for the tag message) — if no text was provided in the
push request, derive it from `git log` since the last tag, as was done
previously for the tag message.
8. Brief summary at the end: what was committed/tagged/pushed, whether
the README was updated (and if so, which sections), and the URL
of the created GitHub Release. The GitHub Release step is now fully
automatic — no manual follow-up needed from Alex anymore, except if
`gh` should ever lose authentication (then run `gh auth login` again,
see above).
## Auto-flash after every successful build
As soon as `pio run` (build) completes successfully without errors, ALWAYS
flash immediately afterward (`pio run --target upload`) without asking —
unless the user explicitly says "build only, don't flash" or similar.
Shortly after, confirm that the upload was also successful (including
the "[SUCCESS]" line at the end).
EXCEPTION: Within the Push & Release routine (see the
"Standard Workflow: Push & Release" section above), do NOT auto-flash, even
after a successful build — there, the build is intentionally run only.
Reason: Alex wants to keep the test device on the current version so the
new release can subsequently be tested via the OTA update functionality,
rather than flashing it directly via cable. For all other purposes (normal
development/testing, individual fixes), the auto-flash rule continues to
apply unchanged.

276
CLAUDE.md
View File

@@ -1,276 +0,0 @@
# 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.
## Sprache: Projekt-Außendarstellung immer Englisch
Alle nach außen sichtbaren Texte sind IMMER auf Englisch zu verfassen —
unabhängig davon, in welcher Sprache die Unterhaltung mit Alex geführt
wird. Das betrifft insbesondere:
- `README.md`
- `index.html` (Webseite/Flasher-Seite)
- GitHub-Release-Notes / -Beschreibungen
- Commit-Messages
- Jeglicher sonstiger Beschreibungs- oder Bugfix-Text, der öffentlich
sichtbar ist (z.B. auf GitHub)
Ausnahme: Die Firmware-UI selbst bleibt mehrsprachig wie gehabt
(`i18n_de/en/fr/tr/es/it.h`) — diese Regel betrifft NUR die
Projekt-Außendarstellung (Repo, Release Notes, Webseite), nicht die
App-Oberfläche auf dem Gerät. Interne Code-Kommentare bleiben ebenfalls
wie gehabt auf Deutsch (siehe „Code-Stil" oben) — diese Regel gilt nur
für nach außen sichtbare Texte.
## 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.
## Standard-Workflow: Push & Release
WICHTIG - wann dieser Workflow startet: Der komplette Release-Workflow
(README-Update, Commit, Tag, Push) darf NUR gestartet werden, wenn Alex
EXPLIZIT danach fragt (z.B. "lass pushen", "können wir releasen", "mach den
Release-Workflow"). Ein einfaches "ja" auf eine Rückfrage (z.B. zu einer
CLAUDE.md-Änderung oder einem anderen Detail) ist KEINE Aufforderung, den
Release-Workflow zu starten. Bei kleineren Fixes/Änderungen bitte NUR bauen
und flashen (siehe Abschnitt "Nach jedem erfolgreichen Build automatisch
flashen" unten), aber NICHT committen/taggen/pushen, bis ausdrücklich danach
gefragt wird.
Sobald der Workflow explizit angefordert wurde, automatisch folgende Schritte
in dieser Reihenfolge:
0. Versionsnummer festlegen: Die neue Versionsnummer kommt IMMER exakt von
Alex - er nennt sie im Push-Wunsch (z.B. über Claude/den Sandbox-
Assistenten: "Alex will auf Version X.Y.Z pushen"). Karl trägt GENAU
diese Nummer in `Config::APP_VERSION` (`src/config.h`) ein - niemals
selbst hochzählen, erraten oder von der letzten Version ableiten (auch
nicht bei kleinen Patches). Das gilt auch für größere Sprünge (z.B.
2.6 -> 3.0), die Alex bewusst und absichtlich machen kann - Karl
übernimmt in jedem Fall die genannte Nummer 1:1, ohne eigene Annahmen.
Falls im Push-Wunsch keine explizite Versionsnummer genannt wurde, bei
Alex nachfragen statt zu raten. Erst NACH dem Eintragen der korrekten
Nummer: einmal sauber `pio run` bauen, DANACH erst der Rest des
bekannten Workflows (README, Tag, index.html, Bin-Dateien, Commit/Push).
(Hier bewusst NUR bauen, NICHT flashen - Ausnahme von der sonst
geltenden Auto-Flash-Regel, siehe Abschnitt "Nach jedem erfolgreichen
Build automatisch flashen" weiter unten. Das Testgerät soll auf der
bisherigen Version bleiben, damit das neue Release per OTA getestet
werden kann.)
1. Prüfen, ob seit dem letzten Commit neue/geänderte Features hinzugekommen
sind, die für Endnutzer sichtbar sind (neue Menüpunkte, geändertes
Verhalten, neue Screens) - falls ja, **README.md entsprechend ergänzen**
(gleicher Stil: Emoji-Überschriften, Ankerlinks zwischen "Features"-Liste
und den Deep-Dive-Sektionen, kurze Beispiele wo sinnvoll). Reine interne
Bugfixes/Refactorings ohne sichtbare Nutzerauswirkung brauchen keinen
README-Eintrag.
2. Code committen (aussagekräftige Commit-Message).
3. Falls es sich um einen Versionssprung handelt: Git-Tag mit
Versionsnummer + Beschreibung der Änderungen erstellen.
4. Prüfen, ob `index.html` (Web-Flasher) noch die alte Versionsnummer zeigt -
falls ja, aktualisieren. **NICHT OPTIONAL, darf bei KEINEM Release-Push
übersprungen werden** - auch nicht bei kleinen Patch-Versionen. Immer als
fester Doppel-Schritt zusammen mit Schritt 1 (README) behandeln: wann
immer die README (oder auch nur die Versionsnummer) auf eine neue Version
aktualisiert wird, IMMER im selben Zug auch `index.html` prüfen und
synchron mitziehen.
4b. Web-Flasher-Versionsauswahl aktuell halten: Bevor die Wurzel-.bin-Dateien
mit dem neuen Build überschrieben werden, die BISHERIGEN (noch alten)
bootloader.bin/partitions.bin/firmware.bin in einen neuen Ordner
`versions/vALT/` archivieren (vALT = die Versionsnummer, die index.html
vor diesem Update zeigte) - dort auch eine `manifest.json` ablegen
(identischer Inhalt wie die Wurzel-manifest.json, siehe
`versions/v3.6.2/manifest.json` als Vorlage). Danach im Ordner
`versions/` nur die 2 neuesten Versionsordner (nach Versionsnummer
sortiert, nicht nach Datei-Datum) behalten - ältere Ordner löschen.
Anschließend in `index.html` den Versions-Dropdown
(`<select id="versionSelect">`) aktualisieren: 3 Optionen - die neue
aktuelle Version (`value="manifest.json"`, `data-version="vNEU"`, Text
"vNEU (latest)") plus die beiden jetzt in `versions/` verbliebenen
Versionen (`value="versions/vX.Y.Z/manifest.json"`, neueste zuerst).
5. Prüfen, ob `bootloader.bin`, `firmware.bin`, `partitions.bin` im
Hauptverzeichnis dem aktuellen Build in `.pio/build/esp32dev/`
entsprechen - falls nicht, von dort kopieren.
6. Alle diese Änderungen (Code + README + Web-Flasher-Dateien) zusammen
committen und pushen (`git push`, plus `git push origin vX.Y.Z` falls ein
Tag erstellt wurde).
7. GitHub Release erstellen UND die `.bin`-Datei in einem Schritt hochladen
(per `gh` CLI, seit v2.7.5 eingerichtet und authentifiziert - siehe
`gh auth status`). Das Release-Asset heißt einfach `firmware.bin`, keine
Umbenennung nötig - die meisten Nutzer laden ohnehin über den
Web-Flasher, das Asset ist nur noch für die wenigen Leute relevant, die
über eine CYD-Launcher-App direkt eine `.bin`-Datei brauchen (dafür ist
der Dateiname egal):
gh release create vX.Y.Z .pio/build/esp32dev/firmware.bin \
--repo Eiswolf-BG/eiswolfs-flightradar-CYD \
--title "vX.Y.Z" \
--notes "<Release-Notes-Text>"
Der Release-Notes-Text kommt aus dem jeweiligen Push-Wunsch (derselbe
Text, der auch für die Tag-Message verwendet wird) - falls im
Push-Wunsch kein Text mitgegeben wurde, aus dem `git log` seit dem
letzten Tag ableiten, wie bisher auch für die Tag-Message.
8. Kurze Zusammenfassung am Ende: was committet/getaggt/gepusht wurde, ob
die README aktualisiert wurde (und falls ja, welche Abschnitte), sowie
die URL des erstellten GitHub Release. Der GitHub-Release-Schritt ist
damit vollautomatisch - kein manuelles Nacharbeiten von Alex mehr
nötig, außer `gh` sollte einmal die Authentifizierung verlieren (dann
erneut `gh auth login`, siehe oben).
## Nach jedem erfolgreichen Build automatisch flashen
Sobald `pio run` (Build) erfolgreich ohne Fehler durchgelaufen ist, IMMER direkt
im Anschluss auch flashen (`pio run --target upload`), ohne extra danach zu
fragen - außer der Nutzer sagt ausdrücklich "nur bauen, nicht flashen" o.ä.
Kurz danach bestätigen, dass der Upload ebenfalls erfolgreich war (inkl.
"[SUCCESS]"-Zeile am Ende).
AUSNAHME: Innerhalb der Push & Release-Routine (siehe Abschnitt
"Standard-Workflow: Push & Release") NICHT automatisch flashen, selbst
nach erfolgreichem Build - dort wird bewusst nur gebaut. Grund: Alex
möchte das Testgerät auf der bisherigen Version belassen, um das neue
Release anschließend über die OTA-Update-Funktion zu testen, statt es
direkt per Kabel zu flashen. Für alle anderen Anlässe (normales
Entwickeln/Testen, einzelne Fixes) gilt die automatische Flash-Regel
unverändert weiter.
Bitte diese Regel jetzt in die CLAUDE.md-Datei einpflegen.

View File

@@ -32,9 +32,9 @@ void run(TFT_eSPI& tft) {
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
// Projektname bewusst hart codiert statt ueber i18n - genau wie
// schon im Splash-Screen (splash_screen.cpp), da ein Eigenname
// ohnehin nicht uebersetzt wird.
// Project name intentionally hardcoded instead of via i18n - just like
// in the splash screen (splash_screen.cpp), since a proper noun
// is not translated anyway.
tft.setCursor(10, 14);
tft.println("Leo's Flightradar");

View File

@@ -2,8 +2,8 @@
#include <Arduino.h>
#include <TFT_eSPI.h>
// Einfacher Info-Screen: Projektname, Kurzbeschreibung, Copyright-Jahr und
// die aktuelle Versionsnummer (Config::APP_VERSION).
// Simple info screen: project name, short description, copyright year and
// the current version number (Config::APP_VERSION).
namespace AboutScreen {
void run(TFT_eSPI& tft);
}

View File

@@ -34,16 +34,12 @@ namespace {
tft.setTextDatum(TL_DATUM);
}
// Lokale Kopie (siehe Konvention in location_presets_screen.cpp - jeder
// Screen haelt seine eigenen kleinen Helfer statt eines gemeinsamen
// Moduls). Ohne Scroll-Unterstuetzung - hier immer nur kurze,
// vorab abgeschnittene Texte (siehe runConfirmScreen/showErrorRetry).
// Lokale Kopie (siehe Konvention in location_presets_screen.cpp - jeder
// Screen haelt seine eigenen kleinen Helfer statt eines gemeinsamen
// Moduls). Optionale Scroll-Unterstuetzung (scrollY/viewTop/viewBottom/
// draw) fuer runConfirmScreen() unten - bei den bestehenden Aufrufstellen
// (showErrorRetry, Kopfzeilen) bleiben die Defaults aktiv, es wird also
// wie bisher immer alles auf einmal gezeichnet.
// Local copy (see convention in location_presets_screen.cpp - each
// screen keeps its own small helpers instead of a shared
// module). Optional scroll support (scrollY/viewTop/viewBottom/
// draw) for runConfirmScreen() below - for the existing call sites
// (showErrorRetry, headers) the defaults remain active, so everything
// is still drawn at once as before.
int16_t layoutWrapped(TFT_eSPI& tft, int16_t x, int16_t startY, int16_t maxWidth,
int16_t lineHeight, const String& text,
int16_t scrollY = 0, int16_t viewTop = -32000, int16_t viewBottom = 32000,
@@ -73,8 +69,8 @@ namespace {
return y;
}
// ---- UTF-8-bewusste Puffer-Helfer (fuer die Sonderzeichen-Tasten wie
// "Ä", die als 2-Byte-UTF-8-Sequenz eingefuegt werden muessen) ----
// ---- UTF-8-aware buffer helpers (for special character keys like
// "Ä", which must be inserted as 2-byte UTF-8 sequences) ----
void appendUtf8(char* buf, uint8_t& len, uint8_t cap, const char* s) {
size_t sl = strlen(s);
if (len + sl >= cap) return;
@@ -83,9 +79,8 @@ namespace {
buf[len] = 0;
}
// Entfernt beim Loeschen eine ganze UTF-8-Zeichensequenz (nicht nur ein
// einzelnes Byte) - sonst bliebe bei Sonderzeichen ein kaputtes
// halbes Byte im Puffer stehen.
// When deleting, removes a whole UTF-8 character sequence (not just a
// single byte) - otherwise a broken half-byte would remain in the buffer.
void backspaceUtf8(char* buf, uint8_t& len) {
if (len == 0) return;
len--;
@@ -93,9 +88,9 @@ namespace {
buf[len] = 0;
}
// Zeigt nur so viel vom ENDE des Puffers, wie in maxWidth passt - der
// Nutzer tippt am Ende weiter, das sichtbare Fenster soll dem folgen
// (wie bei einem gewoehnlichen einzeiligen Texteingabefeld).
// Shows only as much of the END of the buffer as fits in maxWidth - the
// user types at the end, the visible window should follow
// (like a normal single-line text input field).
String visibleTail(TFT_eSPI& tft, const char* buf, int16_t maxWidth) {
String s(buf);
while (s.length() > 0 && tft.textWidth(s) > maxWidth) {
@@ -106,26 +101,26 @@ namespace {
return s;
}
// ---- Tastatur-Layout ----
// ---- Keyboard Layout ----
constexpr const char* DIGITS = "1234567890";
constexpr const char* ROW1 = "QWERTYUIOP";
constexpr const char* ROW2 = "ASDFGHJKL";
// Um Komma und Bindestrich erweitert (7->9 Tasten) - Adressen wie eine
// Hausnummer "45/3" oder "Strasse 12, Ort" waren sonst gar nicht
// eintippbar, da die Tastatur bisher keinerlei Satzzeichen enthielt.
// Extended with comma and hyphen (7->9 keys) - addresses like
// a house number "45/3" or "Street 12, City" were not
// typeable at all since the keyboard previously contained no punctuation.
constexpr const char* ROW3 = "ZXCVBNM,-";
// Sonderzeichen-Seite: gegenueber der ersten Version wurden die seltenen
// Â/Ë/Î/Û gegen die fuer Adressen wichtigen Satzzeichen "/", ".", "'", "-"
// getauscht (Hausnummern wie "45/3", Abkuerzungen wie "Str.", Apostroph-
// Namen).
// Special characters page: compared to the first version, the rare
// Â/Ë/Î/Û were swapped for the punctuation important for addresses
// "/", ".", "'", "-" (house numbers like "45/3", abbreviations like "Str.", apostrophe
// names).
constexpr const char* SPEC0[6] = {"À", "Á", "Ä", "Ç", "É", "È"};
constexpr const char* SPEC1[6] = {"Ê", "Í", "Ñ", "Ó", "Ò", "Ô"};
constexpr const char* SPEC2[6] = {"Ö", "Ù", "Ú", "Ü", "ß", "Ğ"};
constexpr const char* SPEC3[6] = {"İ", "Ş", "/", ".", "'", "-"};
// Gibt die eingegebene Adresse zurueck, oder einen leeren String, wenn
// der Nutzer abgebrochen hat.
// Returns the entered address, or an empty string if
// the user cancelled.
String runAddressKeyboard(TFT_eSPI& tft) {
MenuStars::reset();
constexpr uint8_t CAP = 64;
@@ -137,12 +132,12 @@ namespace {
constexpr int16_t KEY_GAP = 3;
constexpr int16_t FIELD_H = 34;
// Kopfbereich (Titel + Format-Hinweis) einmal zeichnen, um seine
// tatsaechliche Hoehe per getCursorY() zu messen (variiert je nach
// Sprache/Uebersetzungslaenge) - Eingabefeld und Tastatur-Start
// werden daraus abgeleitet statt wie zuvor eine feste Pixelposition
// (78) anzunehmen. redraw() unten zeichnet denselben Kopf bei jedem
// Aufruf identisch neu.
// Head area (title + format hint) draw once to measure its
// actual height via getCursorY() (varies by
// language/translation length) - input field and keyboard start
// are derived from it instead of assuming a fixed pixel position
// (78) as before. redraw() below draws the same header identically on every
// call.
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 14);
@@ -293,10 +288,10 @@ namespace {
return searched ? String(buf) : String();
}
// Namens-Tastatur fuer den finalen Schritt (Preset-Name vergeben) -
// absichtlich eine eigene, einfache ASCII-Tastatur (keine
// Sonderzeichen noetig fuer einen Preset-Namen wie "Zuhause"),
// spiegelt runPresetNameKeypad() aus location_presets_screen.cpp.
// Name keypad for the final step (naming the preset) -
// intentionally a separate, simple ASCII keyboard (no
// special characters needed for a preset name like "Home"),
// mirrors runPresetNameKeypad() from location_presets_screen.cpp.
String runNameKeypad(TFT_eSPI& tft) {
MenuStars::reset();
constexpr const char* NDIGITS = "1234567890";
@@ -311,9 +306,9 @@ namespace {
constexpr int16_t KEY_GAP = 3;
constexpr int16_t FIELD_H = 34;
// Kopfbereich (Titel + Namens-Hinweis) einmal zeichnen, um seine
// tatsaechliche Hoehe per getCursorY() zu messen - selbes Muster wie
// in runAddressKeyboard() oben.
// Head area (title + name hint) draw once to measure its
// actual height via getCursorY() - same pattern as
// in runAddressKeyboard() above.
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 14);
@@ -406,9 +401,9 @@ namespace {
return String(buf);
}
// Prozentkodiert einen UTF-8-String fuer die Nominatim-Query (jedes
// Roh-Byte einzeln - funktioniert damit auch fuer die mehrbytigen
// Sonderzeichen).
// Percent-encodes a UTF-8 string for the Nominatim query (each
// raw byte individually - works for multi-byte
// special characters too).
String urlEncode(const String& s) {
String out;
char hex[4];
@@ -426,18 +421,18 @@ namespace {
return out;
}
// ISO-639-1-Codes in derselben Reihenfolge wie I18n::TABLES (siehe
// i18n.cpp) - fuer staerker lokalisierte display_name-Ergebnisse
// (Nominatim accept-language-Parameter).
// ISO-639-1 codes in the same order as I18n::TABLES (see
// i18n.cpp) - for more localized display_name results
// (Nominatim accept-language parameter).
constexpr const char* NOMINATIM_LANG_CODES[6] = {"en", "de", "fr", "tr", "es", "it"};
enum class GeocodeResult { Ok, NoResults, NetworkError };
// Kostenloser, anmeldefreier Geokodierungs-Dienst (OpenStreetMap
// Nominatim, siehe Config::NOMINATIM_HOST). Blockierender Aufruf,
// ausgeloest durch Nutzer-Interaktion (Tap auf "Suchen") - laeuft im
// Vordergrund auf Core 1, exakt wie der bestehende Flugzeug-Detail-
// Abruf in aircraft_details.cpp.
// Free, no-registration geocoding service (OpenStreetMap
// Nominatim, see Config::NOMINATIM_HOST). Blocking call,
// triggered by user interaction (tap on "Search") - runs in the
// foreground on Core 1, exactly like the existing aircraft detail
// fetch in aircraft_details.cpp.
GeocodeResult geocode(const String& query, double& lat, double& lon, String& displayName) {
if (WiFi.status() != WL_CONNECTED) return GeocodeResult::NetworkError;
@@ -461,9 +456,9 @@ namespace {
return GeocodeResult::NetworkError;
}
// Body erst komplett einsammeln statt direkt aus http.getStream()
// zu parsen - siehe weather.cpp, gleicher Grund (Chunked-Transfer-
// Encoding fuehrte dort sonst zu ArduinoJson-"InvalidInput").
// Collect entire body first instead of parsing directly from http.getStream()
// - see weather.cpp, same reason (chunked transfer
// encoding caused ArduinoJson "InvalidInput" there otherwise).
String body = http.getString();
http.end();
@@ -486,8 +481,8 @@ namespace {
return GeocodeResult::Ok;
}
// Fehler-/Kein-Ergebnis-Screen mit "Erneut versuchen"/"Abbrechen".
// Gibt true zurueck, wenn der Nutzer es erneut versuchen will.
// Error/no-result screen with "Try again"/"Cancel".
// Returns true if the user wants to try again.
bool showErrorRetry(TFT_eSPI& tft, StringId msgId) {
MenuStars::reset();
tft.fillScreen(TFT_BLACK);
@@ -507,9 +502,9 @@ namespace {
}
}
// Bestaetigungs-Screen mit dem von Nominatim gefundenen Ort. Gibt 1
// zurueck ("verwenden"), 0 fuer "erneut versuchen" (zurueck zur
// Adress-Tastatur).
// Confirmation screen with the location found by Nominatim. Returns 1
// ("use it"), 0 for "try again" (back to
// address keyboard).
int runConfirmScreen(TFT_eSPI& tft, const String& displayName) {
MenuStars::reset();
@@ -520,14 +515,14 @@ namespace {
constexpr int16_t USE_Y = (int16_t)(Config::SCREEN_HEIGHT - 50);
constexpr int16_t VIEW_BOTTOM = (int16_t)(TRY_AGAIN_Y - 10);
// Volle Adresse OHNE Kuerzung anzeigen (frueher wurde bei > 150
// Zeichen mit "..." abgeschnitten) - stattdessen wird der Absatz bei
// Bedarf vertikal scrollbar, exakt dasselbe Muster wie beim "Wie
// funktionieren Presets"-Infoscreen (location_presets_screen.cpp
// ::runInfoScreen). Bewusst KEIN Marquee hier: das ist ein
// mehrzeiliger, wortumgebrochener Absatz, keine einzelne Zeile, die
// in der Breite nicht passt - horizontales Scrollen waere hier die
// falsche Loesung.
// Show full address WITHOUT truncation (previously cut at > 150
// characters with "...") - instead, the paragraph becomes
// vertically scrollable if needed, exactly the same pattern as the "How
// do presets work" info screen (location_presets_screen.cpp
// ::runInfoScreen). Deliberately NOT a marquee here: this is a
// multi-line, word-wrapped paragraph, not a single line that
// does not fit in width - horizontal scrolling would be the
// wrong solution here.
int16_t totalH = layoutWrapped(tft, 10, VIEW_TOP, textMaxWidth, LINE_H, displayName, 0, 0, 0, false);
int16_t maxScroll = (int16_t)(totalH - VIEW_BOTTOM);
if (maxScroll < 0) maxScroll = 0;
@@ -611,9 +606,9 @@ bool run(TFT_eSPI& tft) {
String name = runNameKeypad(tft);
if (!LocationPresets::addPreset(lat, lon, name)) return false;
// Neu angelegtes Preset sofort aktivieren - sonst blieb z.B. der
// automatische IP-Standort aktiv, obwohl gerade extra eine genauere
// Adresse eingegeben wurde (siehe Alex' Feedback).
// Newly created preset is immediately activated - otherwise e.g. the
// automatic IP location would remain active, even though a more accurate
// address was just entered (see Alex' feedback).
LocationPresets::setActiveIndex((int8_t)(LocationPresets::count() - 1));
return true;
}

View File

@@ -80,9 +80,9 @@ void update() {
WiFiClientSecure client;
client.setInsecure();
// hexdb.io zuerst versuchen (bisherige Quelle) - aber mit kuerzerem
// Timeout (3s statt 5s), damit ein kompletter Ausfall des Dienstes die
// ADS-B-Abfrage nicht unnoetig lange blockiert, bevor der Fallback greift.
// Try hexdb.io first (previous source) - but with a shorter
// timeout (3s instead of 5s) so a complete service outage does not
// block the ADS-B query unnecessarily long before the fallback kicks in.
client.setTimeout(3000);
String body;
if (httpGetString(client, String("https://hexdb.io/api/v1/aircraft/") + hex, body)) {
@@ -99,9 +99,9 @@ void update() {
}
}
// Fallback: hexdb.io war nicht erreichbar/lieferte kein Modell -
// adsbdb.com als zweite, unabhaengige Quelle versuchen (andere API-Form,
// aber inhaltlich aequivalent: Hersteller + Typ ueber den Hex-Code).
// Fallback: hexdb.io was unreachable/did not deliver a model -
// try adsbdb.com as a second, independent source (different API format,
// but equivalent in content: manufacturer + type via the hex code).
if (!result.model[0]) {
client.setTimeout(4000);
String body2;
@@ -120,26 +120,25 @@ void update() {
}
}
// Flugroute (Start-/Zielflughafen) - jetzt ueber eine Kette aus DREI
// unabhaengigen kostenlosen Quellen statt nur einer, absteigend nach in
// Tests beobachteter Trefferquote sortiert. Vorher lieferte adsbdb.com
// allein in ca. 80% der Faelle "unknown" (siehe Alex' Feedback) - die
// drei Quellen speisen sich aus unterschiedlichen, ueberlappenden aber
// nicht identischen Community-Datenbanken, daher deutlich bessere
// Gesamtabdeckung durch Verketten:
// 1. VRS-Standing-Data-Mirror (adsb.lol) - stuendlich aktualisierter
// Spiegel des Virtual-Radar-Server-Projekts, in Tests die mit
// Abstand zuverlaessigste Quelle. Pfad = erste 2 Zeichen des
// (GROSSGESCHRIEBENEN - der Dienst ist case-sensitiv) Rufzeichens
// als Ordner, liefert "airport_codes":"ORIG-DEST" (ICAO).
// 2. hexdb.io - eigener Route-Endpunkt (andere URL als der
// Aircraft-Endpunkt weiter oben), liefert "route":"ORIG-DEST".
// 3. adsbdb.com Callsign-Endpunkt - bisherige einzige Quelle, bleibt
// als letzter Fallback, da sie gelegentlich Daten hat, die die
// anderen beiden nicht haben.
// Nur versuchen, wenn ueberhaupt ein Rufzeichen bekannt ist -
// Sichtflug-Maschinen ohne Callsign haben ohnehin keine darueber
// auswertbare Route.
// Flight route (origin/destination airport) - now via a chain of THREE
// independent free sources instead of just one, sorted descending by
// observed hit rate in testing. Previously adsbdb.com alone returned
// "unknown" in about 80% of cases (see Alex' feedback) - the
// three sources draw from different, overlapping but
// not identical community databases, therefore much better
// overall coverage through chaining:
// 1. VRS-Standing-Data-Mirror (adsb.lol) - hourly updated
// mirror of the Virtual Radar Server project, by far the most
// reliable source in testing. Path = first 2 characters of the
// (UPPERCASE - the service is case-sensitive) callsign
// as folder, returns "airport_codes":"ORIG-DEST" (ICAO).
// 2. hexdb.io - own route endpoint (different URL from the
// aircraft endpoint above), returns "route":"ORIG-DEST".
// 3. adsbdb.com callsign endpoint - previous single source, remains
// as last fallback since it occasionally has data that the
// other two do not.
// Only try if a callsign is known at all -
// VFR aircraft without a callsign do not have an evaluable route anyway.
String trimmedCallsign = String(callsign);
trimmedCallsign.trim();
trimmedCallsign.toUpperCase();
@@ -165,7 +164,7 @@ void update() {
}
}
// 2. hexdb.io Route-Endpunkt, falls Quelle 1 nichts geliefert hat.
// 2. hexdb.io route endpoint, if source 1 did not deliver anything.
if (!result.routeOrigin[0] || !result.routeDest[0]) {
client.setTimeout(3000);
String body4;
@@ -179,7 +178,7 @@ void update() {
}
}
// 3. adsbdb.com Callsign-Endpunkt als letzter Fallback.
// 3. adsbdb.com callsign endpoint as last fallback.
if (trimmedCallsign.length() > 0 && (!result.routeOrigin[0] || !result.routeDest[0])) {
client.setTimeout(4000);
String body5;

View File

@@ -33,10 +33,10 @@ namespace {
tft.setTextDatum(TL_DATUM);
}
// Gleiche Hoehen-Farblogik wie RadarScreen (dort intern, hier bewusst
// dupliziert - siehe CLAUDE.md-Konvention "jeder Screen unabhaengig").
// Auf die Nachtdimmung wird hier verzichtet: diese Liste ist ein Menue-
// Screen wie jeder andere, kein Dauerbild wie das Radar.
// Same altitude color logic as RadarScreen (duplicated there internally, here deliberately
// duplicated - see CLAUDE.md convention "each screen independently runnable").
// Night dimming is omitted here: this list is a menu
// screen like any other, not a persistent image like the radar.
uint16_t colorForAltitude(int32_t altFt) {
if (altFt < Config::COLOR_LOW_ALT_THRESHOLD_FT) return TFT_GREEN;
if (altFt < Config::COLOR_MID_ALT_THRESHOLD_FT) return TFT_YELLOW;
@@ -52,8 +52,8 @@ namespace {
}
enum class SortMode : uint8_t { Distance, Altitude, Callsign };
// Bleibt bis zum naechsten Neustart erhalten (kein SD-Speichern noetig -
// das waere fuer eine reine Anzeige-Praeferenz unnoetiger Aufwand).
// Persists until the next restart (no SD storage needed -
// that would be unnecessary effort for a pure display preference).
SortMode sortMode = SortMode::Distance;
const char* sortModeLabel() {
@@ -72,7 +72,7 @@ bool run(TFT_eSPI& tft) {
constexpr int16_t LIST_BOTTOM = Config::SCREEN_HEIGHT - 56;
constexpr uint8_t ROWS_VISIBLE = (LIST_BOTTOM - LIST_TOP) / (ROW_H + ROW_GAP);
uint8_t scrollTop = 0; // Index des ersten sichtbaren Eintrags in der sortierten Liste
uint8_t scrollTop = 0; // Index of the first visible entry in the sorted list
bool selected = false;
bool done = false;
MenuStars::reset();
@@ -82,9 +82,9 @@ bool run(TFT_eSPI& tft) {
uint8_t count = 0;
float rangeKm = Config::RANGE_STEPS_KM[SettingsStore::rangeIndex()];
// Gleiche Filter wie das Radar (Reichweite, Bodenfahrzeuge,
// Airline-Filter), damit die Liste genau die Flugzeuge zeigt, die
// gerade auch als Punkte auf dem Radar zu sehen sind.
// Same filters as the radar (range, ground vehicles,
// airline filter), so the list shows exactly the aircraft
// that are currently also visible as dots on the radar.
AircraftTable::lock();
Aircraft* table = AircraftTable::raw();
for (uint8_t i = 0; i < AircraftTable::capacity(); i++) {
@@ -126,9 +126,9 @@ bool run(TFT_eSPI& tft) {
Rect rowRects[ROWS_VISIBLE];
uint8_t visibleRowCount = 0;
// Einheiten-Einstellung (Menue > Einheiten) einmal vor der Schleife
// lesen statt pro Zeile - vorher zeigten Hoehe/Distanz hier immer
// fest ft/km, auch bei Imperial eingestellt.
// Read unit setting (Menu > Units) once before the loop
// instead of per row - previously altitude/distance always showed
// fixed ft/km here, even when imperial was set.
bool listMetric = LocationManager::useMetricUnits();
if (count == 0) {
@@ -147,12 +147,12 @@ bool run(TFT_eSPI& tft) {
uint16_t borderColor = emergency ? TFT_RED : (watched ? TFT_CYAN : TFT_GREEN);
tft.drawRoundRect(r.x, r.y, r.w, r.h, 4, borderColor);
// WICHTIG (siehe CLAUDE.md-Falle "Font ist Baseline-verankert"):
// Alle drei Textfelder ueber drawString() mit einem MITTIGEN
// Datum auf die Zeilenmitte zentrieren, statt Baseline- und
// Top-verankerte Aufrufe zu mischen - genau das hatte vorher
// dazu gefuehrt, dass Hoehe/Distanz unten aus der Box liefen,
// waehrend das Rufzeichen (Baseline-verankert) korrekt sass.
// IMPORTANT (see CLAUDE.md gotcha "Font is baseline-anchored"):
// All three text fields via drawString() with a MIDDLE
// datum centered on the row's midpoint, instead of mixing baseline- and
// top-anchored calls - exactly that had previously caused
// altitude/distance to run out of the bottom of the box,
// while the callsign (baseline-anchored) was positioned correctly.
int16_t midY = r.y + r.h / 2;
const char* label = a.callsign[0] ? a.callsign : a.hex;

View File

@@ -2,12 +2,12 @@
#include <Arduino.h>
#include <TFT_eSPI.h>
// Sortierbare Liste aller aktuell erkannten Flugzeuge (gleiche Filter wie
// das Radar: Reichweite, Bodenfahrzeuge, Airline-Filter). Antippen eines
// Eintrags springt zurueck zum Radar mit direkt geoeffnetem Detail-Panel.
// Sortable list of all currently detected aircraft (same filters as
// the radar: range, ground vehicles, airline filter). Tapping an
// entry jumps back to the radar with the detail panel directly open.
namespace AircraftListScreen {
// Rueckgabewert true = der Nutzer hat ein Flugzeug angetippt (Aufrufer
// soll bis zum Radar zurueckspringen, nicht nur die eigene Seite
// schliessen). false = nur "Zurueck" gedrueckt, normal weiter im Menue.
// Return value true = the user tapped an aircraft (caller
// should jump back to the radar, not just close its own page).
// false = only "Back" pressed, continue normally in menu.
bool run(TFT_eSPI& tft);
}

View File

@@ -4,13 +4,12 @@
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
// WICHTIG: Diese Datei ruft absichtlich KEIN AirlineLookup::resolve() mehr auf!
// postFetchUpdate() wird vom NetTask auf Core 0 aufgerufen. AirlineLookup
// braucht SD-Kartenzugriff, und die SD-Karte wurde in setup() auf Core 1
// initialisiert - Zugriff von Core 0 aus fuehrte zu einem Haenger (Task
// Watchdog auf IDLE0). Die Aufloesung der Airline-Namen passiert deshalb
// jetzt in main.cpp/renderAircraftList() auf Core 1 (demselben Core, der
// die SD-Karte urspruenglich initialisiert hat).
// IMPORTANT: This file intentionally no longer calls AirlineLookup::resolve()!
// postFetchUpdate() is called from NetTask on Core 0. AirlineLookup
// needs SD card access, and the SD card was initialized in setup() on Core 1
// - access from Core 0 caused a hang (Task Watchdog on IDLE0). The
// resolution of airline names now happens in main.cpp/renderAircraftList()
// on Core 1 (the same core that originally initialized the SD card).
namespace AircraftTable {

View File

@@ -11,15 +11,15 @@ namespace AircraftTable {
uint8_t validCount();
void postFetchUpdate(double homeLat, double homeLon);
// Wird bei jedem postFetchUpdate() erhoeht. Damit koennen andere Teile des
// Programms (z.B. der Render-Loop) erkennen, ob sich die Daten seit dem
// letzten Mal ueberhaupt geaendert haben, statt stumpf auf Zeit zu pollen -
// das vermeidet unnoetiges (und flackerndes) Neuzeichnen.
// Incremented on every postFetchUpdate(). This lets other parts of the
// program (e.g. the render loop) detect whether the data has
// changed at all since the last time, instead of blindly polling by time -
// this avoids unnecessary (and flickering) redrawing.
uint32_t version();
// Schuetzt den Zugriff auf raw()/validCount()/postFetchUpdate() zwischen
// dem Netzwerk-Task (Core 0, schreibt) und dem Render-Loop (Core 1, liest).
// Aufrufer muss lock() vor und unlock() nach jedem Zugriff aufrufen.
// Protects access to raw()/validCount()/postFetchUpdate() between
// the network task (Core 0, writes) and the render loop (Core 1, reads).
// Caller must call lock() before and unlock() after every access.
void lock();
void unlock();

View File

@@ -16,19 +16,18 @@ namespace {
char watched[MAX_WATCHED][9] = {{0}};
uint8_t watchedCount = 0;
// Schuetzt watched[]/watchedCount - urspruenglich nur von Core 1 (Menue-
// Screens, Radar) verwendet, seit der WebUI-Listenverwaltung (siehe
// web_export_server.cpp) aber auch von Core 0 (NetTask) aus erreichbar.
// Gleiches Muster wie AircraftDetails::mutex.
// Protects watched[]/watchedCount - originally only used from Core 1 (menu
// screens, radar), but since the WebUI list management (see
// web_export_server.cpp) also reachable from Core 0 (NetTask).
// Same pattern as AircraftDetails::mutex.
SemaphoreHandle_t mutex = nullptr;
void ensureMutex() {
if (mutex == nullptr) mutex = xSemaphoreCreateMutex();
}
// Ueberspringt fuehrende Leerzeichen, uebernimmt bis zu 8 Zeichen und
// bricht bei einem Leerzeichen ab (ADS-B-Rufzeichen haben oft Padding),
// alles in Grossbuchstaben.
// Skips leading spaces, copies up to 8 characters and stops at a space
// (ADS-B callsigns often have padding), all in uppercase.
void normalize(const char* callsign, char* out) {
int j = 0;
int i = 0;

View File

@@ -25,9 +25,9 @@ namespace {
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).
// Four keyboard rows (digits on top, then QWERTYUIOP/ASDFGHJKL/ZXCVBNM),
// since callsigns like "DLH441" also contain numbers. Buffer 8 characters +
// null terminator (full callsign instead of just 3-letter ICAO prefix).
String runCallsignKeypad(TFT_eSPI& tft) {
MenuStars::reset();
constexpr const char* DIGITS = "1234567890";
@@ -164,10 +164,10 @@ namespace {
constexpr int16_t textMaxWidth = Config::SCREEN_WIDTH - 20;
constexpr int16_t LINE_H = 16;
// VIEW_TOP haengt davon ab, wie viele Zeilen der Titel tatsaechlich
// braucht (je nach Sprache 1 oder 2 Zeilen) - vorher "trocken"
// (draw=false) berechnen, damit der Fliesstext nicht in eine
// zweizeilige Titelueberschrift hineinlaeuft.
// VIEW_TOP depends on how many lines the title actually needs
// (1 or 2 lines depending on language) - calculate "dry" first
// (draw=false) so the body text does not run into a
// two-line title heading.
int16_t titleEndY = layoutWrapped(tft, 10, 14, textMaxWidth, LINE_H, I18n::t(StringId::WATCHLIST_INFO_TITLE), 0, 0, 0, false);
const int16_t VIEW_TOP = titleEndY + 4;
constexpr int16_t VIEW_BOTTOM = Config::SCREEN_HEIGHT - 60;
@@ -192,10 +192,10 @@ namespace {
auto redraw = [&]() {
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
// Titel kann laenger als eine Zeile sein - ueber denselben
// layoutWrapped()-Mechanismus wie den Fliesstext zeichnen, statt
// println() (das keinen wortweisen Umbruch macht und mitten im
// Wort abschneidet/umbricht).
// Title can be longer than one line - draw via the same
// layoutWrapped() mechanism as the body text, instead of
// println() (which does not do word-wise wrapping and
// cuts/wraps mid-word).
layoutWrapped(tft, 10, 14, textMaxWidth, LINE_H, I18n::t(StringId::WATCHLIST_INFO_TITLE), 0, 0, Config::SCREEN_HEIGHT, true);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
@@ -243,11 +243,11 @@ void run(TFT_eSPI& tft) {
while (!done) {
tft.fillScreen(TFT_BLACK);
// Kleiner "?"-Info-Button oben rechts, gleiches Muster wie bei den
// Standort-Presets/dem WLAN-Manager. ZUERST zeichnen, damit Titel/
// Beschreibung bewusst unterhalb davon beginnen (der Titel + zwei
// Beschreibungszeilen liefen vorher UNTER dem Button durch und
// wurden dadurch teilweise verdeckt/abgeschnitten).
// Small "?" info button top right, same pattern as with the location
// presets/WiFi manager. Draw FIRST so that title/description
// consciously start below it (the title + two description lines
// previously ran UNDER the button and were partially
// hidden/clipped).
Rect infoBtn = {(int16_t)(Config::SCREEN_WIDTH - 40), 2, 30, 24};
drawButton(tft, infoBtn, "?");

View File

@@ -16,10 +16,10 @@ namespace {
char hidden[MAX_HIDDEN][4] = {{0}};
uint8_t hiddenCount = 0;
// Schuetzt hidden[]/hiddenCount - urspruenglich nur von Core 1 (Menue-
// Screens) verwendet, seit der WebUI-Listenverwaltung (siehe
// web_export_server.cpp) aber auch von Core 0 (NetTask) aus erreichbar.
// Gleiches Muster wie AircraftDetails::mutex.
// Protects hidden[]/hiddenCount - originally only used from Core 1 (menu
// screens), but since the WebUI list management (see
// web_export_server.cpp) also reachable from Core 0 (NetTask).
// Same pattern as AircraftDetails::mutex.
SemaphoreHandle_t mutex = nullptr;
void ensureMutex() {

View File

@@ -24,9 +24,9 @@ namespace {
tft.setTextDatum(TL_DATUM);
}
// Gleicher PWM-Kanal wie in main.cpp (dort in setup() mit ledcSetup()
// initialisiert) - hier bewusst als eigene Konstante dupliziert statt
// geteilt, siehe CLAUDE.md-Konvention ("jeder Screen unabhaengig").
// Same PWM channel as in main.cpp (initialized there in setup() with
// ledcSetup()) - intentionally duplicated here as its own constant
// instead of shared, see CLAUDE.md convention ("each screen independent").
constexpr uint8_t BACKLIGHT_PWM_CHANNEL = 0;
void applyLive(uint8_t percent) {

View File

@@ -2,9 +2,9 @@
#include <Arduino.h>
#include <TFT_eSPI.h>
// Display-Helligkeit einstellen (10-100% in 10%-Schritten). Wirkt sich
// sofort live auf die Hintergrundbeleuchtung aus, damit man die Aenderung
// beim Antippen direkt sieht.
// Display brightness setting (10-100% in 10% steps). Affects the
// backlight live immediately so you can see the change
// directly when tapping.
namespace BrightnessScreen {
void run(TFT_eSPI& tft);
}

View File

@@ -3,8 +3,8 @@
#include <TFT_eSPI.h>
namespace CalibrationScreen {
// Blockierend: zeigt 4 Kreise (Ecken), wartet auf Antippen jeweils in
// Reihenfolge, berechnet die Kalibrierung und speichert sie auf der
// SD-Karte (via TouchInput::saveCalibration()).
// Blocking: shows 4 circles (corners), waits for tapping each in
// sequence, calculates the calibration and saves it on the
// SD card (via TouchInput::saveCalibration()).
void run(TFT_eSPI& tft);
}

View File

@@ -2,40 +2,40 @@
#include <Arduino.h>
namespace Config {
// Wird bei jedem Versions-Release von Karl aktualisiert (siehe
// CLAUDE.md-Workflow "Standard-Workflow: Push & Release") - erscheint
// im Info-Screen (Menue > System > Info) und muss zum jeweiligen
// Git-Tag passen.
// Updated by Karl on every version release (see
// CLAUDE.md workflow "Standard Workflow: Push & Release") - shown
// in the info screen (Menu > System > Info) and must match the
// corresponding Git tag.
constexpr const char* APP_VERSION = "3.6.3";
// Display-Helligkeit (Menue > System > Helligkeit), in Prozent.
// MIN bewusst nicht 0 - ein komplett dunkles Display koennte sonst wie
// ein Defekt wirken statt wie eine Einstellung.
// Display brightness (Menu > System > Brightness), in percent.
// MIN intentionally not 0 - a completely dark display could otherwise
// look like a defect rather than a setting.
constexpr uint8_t BRIGHTNESS_MIN_PERCENT = 10;
constexpr uint8_t BRIGHTNESS_MAX_PERCENT = 100;
constexpr uint8_t BRIGHTNESS_STEP_PERCENT = 10;
// Bildschirm-Timeout (Menue > System > Bildschirm-Timeout), in Minuten,
// per Schieberegler einstellbar (siehe timeout_screen.cpp) - danach
// folgt "Nie" (kein Timeout) als eigene Endposition. Vorher nur per
// wiederholtem Antippen 0-10 durchklickbar (0 = Nie), was bei z.B. 10
// Minuten zehn einzelne Tipps brauchte.
// Screen timeout (Menu > System > Screen Timeout), in minutes,
// adjustable via slider (see timeout_screen.cpp) - followed by
// "Never" (no timeout) as a separate end position. Previously only
// clickable 0-10 by repeated tapping (0 = Never), which needed e.g. ten
// individual taps for 10 minutes.
constexpr uint8_t SCREEN_TIMEOUT_MIN_MINUTES = 1;
constexpr uint8_t SCREEN_TIMEOUT_MAX_MINUTES = 15;
// Nachtmodus (22-6 Uhr) dimmt relativ zur jeweils eingestellten normalen
// Helligkeit, nicht auf einen festen Absolutwert - sonst waere der
// Dimm-Effekt bei niedrig eingestellter Normalhelligkeit wirkungslos
// oder wuerde das Display nachts sogar heller machen als tagsueber.
// Night mode (10pm-6am) dims relative to the currently set normal
// brightness, not to a fixed absolute value - otherwise the
// dimming effect would be useless at low normal brightness
// or would even make the display brighter at night than during the day.
constexpr uint8_t NIGHT_DIM_REDUCTION_PERCENT = 40;
constexpr const char* IP_GEO_HOST = "ip-api.com";
constexpr const char* IP_GEO_PATH = "/json/?fields=status,lat,lon,offset,countryCode";
// Adresssuche (AddressSearchScreen) - kostenloser, anmeldefreier
// Geokodierungs-Dienst (OpenStreetMap Nominatim). Deren Nutzungsregeln
// verlangen einen aussagekraeftigen User-Agent statt des HTTPClient-
// Standardwerts, siehe https://operations.osmfoundation.org/policies/nominatim/.
// Address search (AddressSearchScreen) - free, no-registration
// geocoding service (OpenStreetMap Nominatim). Their usage policies
// require a meaningful User-Agent instead of the HTTPClient
// default, see https://operations.osmfoundation.org/policies/nominatim/.
constexpr const char* NOMINATIM_HOST = "nominatim.openstreetmap.org";
constexpr const char* NOMINATIM_USER_AGENT = "EiswolfsFlightradarCYD (github.com/Eiswolf-BG/eiswolfs-flightradar-CYD)";
@@ -50,9 +50,9 @@ constexpr float RANGE_STEPS_KM[] = {10.0f, 25.0f, 50.0f, 100.0f};
constexpr uint8_t RANGE_STEP_COUNT = 4;
constexpr uint8_t DEFAULT_RANGE_INDEX = 1;
// Eco-Mode: reduziert die ADS-B-Abfragefrequenz auf dieses Intervall
// (statt FETCH_INTERVAL_MS), wenn der Bildschirm durch Inaktivitaets-
// Timeout abgeschaltet ist (siehe main.cpp).
// Eco mode: reduces the ADS-B fetch frequency to this interval
// (instead of FETCH_INTERVAL_MS), when the screen is turned off by
// inactivity timeout (see main.cpp).
constexpr uint32_t ECO_FETCH_INTERVAL_MS = 60000;
constexpr uint8_t ALT_FILTER_COUNT = 3;
@@ -63,11 +63,11 @@ constexpr float RANGE_STEPS_KM[] = {10.0f, 25.0f, 50.0f, 100.0f};
constexpr uint32_t FETCH_INTERVAL_MS = 8000;
constexpr uint32_t HTTP_TIMEOUT_MS = 6000;
// Wetter-Icon im Header (siehe weather.cpp) - deutlich seltener
// abgefragt als die ADS-B-Daten, das Wetter aendert sich nicht
// minuetlich und die kostenlose Open-Meteo-API soll nicht unnoetig oft
// belastet werden.
constexpr uint32_t WEATHER_FETCH_INTERVAL_MS = 600000; // 10 Minuten
// Weather icon in the header (see weather.cpp) - queried much less
// frequently than ADS-B data, weather does not change
// every minute and the free Open-Meteo API should not be needlessly
// stressed.
constexpr uint32_t WEATHER_FETCH_INTERVAL_MS = 600000; // 10 minutes
constexpr float DEFAULT_PROXIMITY_ALERT_KM = 8.0f;
@@ -97,8 +97,8 @@ constexpr float RANGE_STEPS_KM[] = {10.0f, 25.0f, 50.0f, 100.0f};
constexpr uint8_t TOUCH_MISO_PIN = 39;
constexpr uint8_t TOUCH_IRQ_PIN = 36;
// Display-Rotation: 0 = normal (USB-Anschluss unten), 2 = 180 Grad
// gedreht (USB oben) - fuer Montage mit Ladebuchse oben.
// Display rotation: 0 = normal (USB connector at bottom), 2 = 180 degrees
// rotated (USB at top) - for mounting with charging port on top.
constexpr uint8_t DISPLAY_ROTATION = 2;
constexpr int16_t SCREEN_WIDTH = 240;

View File

@@ -24,7 +24,7 @@ namespace {
tft.setTextDatum(TL_DATUM);
}
// Lokale Kopie, siehe Konvention in location_presets_screen.cpp.
// Local copy, see convention in location_presets_screen.cpp.
int16_t layoutWrapped(TFT_eSPI& tft, int16_t x, int16_t startY, int16_t maxWidth,
int16_t lineHeight, const String& text) {
int16_t y = startY;
@@ -59,12 +59,11 @@ void run(TFT_eSPI& tft) {
int16_t textEndY = layoutWrapped(tft, 10, 40, (int16_t)(Config::SCREEN_WIDTH - 20), 18,
I18n::t(StringId::FIRST_RUN_LOCATION_BODY));
// Buttons folgen direkt unter dem Text (mit etwas Abstand) statt an
// einer fest einprogrammierten Position - eine laengere Uebersetzung
// hat sonst den Text bis unter/hinter die Buttons laufen lassen
// (auf Deutsch beobachtet: Text unlesbar hinter "Adresse eingeben"/
// "Ueberspringen"). Nach unten hin trotzdem an den Bildschirmrand
// geklemmt, falls ein Text ausnahmsweise doch sehr lang waere.
// Buttons follow directly below the text (with some spacing) instead of
// a hardcoded position - a longer translation would otherwise cause the
// text to run below/behind the buttons (observed in German: text
// unreadable behind "Enter address"/"Skip"). Clamped to the bottom of
// the screen in case a text happens to be very long.
constexpr int16_t BTN_H = 40;
constexpr int16_t BTN_GAP = 8;
int16_t setY = textEndY + 14;
@@ -82,9 +81,9 @@ void run(TFT_eSPI& tft) {
if (setBtn.contains(tap.x, tap.y)) {
if (AddressSearchScreen::run(tft)) {
// Direkt aktivieren - das ist der ganze Sinn dieses
// Screens (sofort beim ersten Start den praezisen
// Standort nutzen, statt weiter auf Auto/IP zu bleiben).
// Activate immediately - that is the whole point of this
// screen (use the precise location right on first start,
// instead of staying on auto/IP).
uint8_t idx = LocationPresets::count();
if (idx > 0) LocationPresets::setActiveIndex((int8_t)(idx - 1));
}

View File

@@ -3,9 +3,8 @@
#include <TFT_eSPI.h>
namespace FirstRunLocationScreen {
// Einmaliger Hinweis-Screen beim allerersten Start (nach der
// Sprachauswahl) - erklaert den Genauigkeitsvorteil eines per Adresse
// gesetzten Standorts, mit direktem Einstieg in die Adresssuche.
// Ueberspringbar.
// One-time info screen on very first start (after language selection)
// - explains the accuracy advantage of a location set by address,
// with direct entry into the address search. Skippable.
void run(TFT_eSPI& tft);
}

View File

@@ -15,10 +15,10 @@ namespace {
char seenHex[MAX_SEEN][7];
uint16_t seenCount = 0;
// Datei der aktuell laufenden Aufzeichnungs-Sitzung (ohne ".csv"), z.B.
// "2026-08-06" fuer die erste Sitzung eines Tages oder "2026-08-06_2"
// fuer ein erneutes Einschalten am selben Tag. Leer = noch nicht
// aufgeloest (Uhrzeit noch nicht synchronisiert oder Flugbuch aus).
// File of the currently active recording session (without ".csv"), e.g.
// "2026-08-06" for the first session of a day or "2026-08-06_2"
// for powering on again on the same day. Empty = not yet
// resolved (clock not yet synced or flight logbook off).
char currentSessionFile[16] = {0};
void formatDateFromEpoch(uint32_t epoch, char* out, size_t outSize) {
@@ -28,12 +28,12 @@ namespace {
snprintf(out, outSize, "%04d-%02d-%02d", tmv.tm_year + 1900, tmv.tm_mon + 1, tmv.tm_mday);
}
// Findet fuer den gegebenen Aktivierungszeitpunkt eine noch nicht
// existierende Logbuch-Datei: "<Datum>.csv" fuer die erste Sitzung
// eines Tages, "<Datum>_2.csv", "_3.csv" usw. fuer erneutes Einschalten
// am selben Tag - so bekommt jede Sitzung ihre eigene, im
// Logbuch-Dateien-Screen einzeln loeschbare Datei, statt in eine
// bestehende hineinzuschreiben.
// Finds a logbook file that does not yet exist for the given activation
// time: "<Date>.csv" for the first session of a day,
// "<Date>_2.csv", "_3.csv" etc. for powering on again the same day -
// this way each session gets its own file, individually deletable
// in the logbook files screen, instead of appending to
// an existing one.
void resolveSessionFilename(uint32_t epoch, char* out, size_t outSize) {
char dateStr[11];
formatDateFromEpoch(epoch, dateStr, sizeof(dateStr));
@@ -57,8 +57,8 @@ namespace {
}
}
// Sehr unwahrscheinlicher Fall (>50 Sitzungen an einem Tag): letzten
// Kandidaten weiterverwenden statt endlos zu suchen.
// Very unlikely case (>50 sessions in one day): reuse the last
// candidate instead of searching indefinitely.
strncpy(out, dateStr, outSize - 1);
out[outSize - 1] = 0;
}
@@ -92,7 +92,7 @@ namespace {
if (buf[i] == '\n') lines++;
}
blocksRead++;
if (blocksRead % 8 == 0) delay(1);
if (blocksRead % 8 == 0) delay(1); // yield to prevent watchdog timeout during bulk SD reads
}
return lines;
}
@@ -141,17 +141,17 @@ namespace {
}
}
blocksRead++;
if (blocksRead % 8 == 0) delay(1);
if (blocksRead % 8 == 0) delay(1); // yield during bulk SD reads
}
f.close();
}
// Loest die Datei der aktuellen Sitzung auf (einmalig pro Sitzung) bzw.
// uebernimmt sie nach einem Neustart erneut aus den Einstellungen -
// nur aufrufen, wenn das Flugbuch gerade eingeschaltet ist.
// Resolves the current session file (once per session) or
// adopts it again from settings after a restart -
// only call when the flight logbook is currently enabled.
void ensureSessionFile() {
time_t now = time(nullptr);
if (now <= 8 * 3600 * 2) return; // Uhrzeit noch nicht synchronisiert
if (now <= 8 * 3600 * 2) return; // epoch still below reasonable minimum = RTC not yet synced
String persisted = SettingsStore::flightLogbookSessionFile();
if (persisted.length() > 0) {
@@ -163,10 +163,10 @@ namespace {
return;
}
// Keine Sitzungsdatei hinterlegt - entweder frisches Einschalten
// (menu_screen.cpp loescht den Eintrag beim Umschalten bewusst) oder
// Migration von einer alten Firmware ohne Sitzungslogik. In beiden
// Faellen jetzt eine neue, garantiert einzigartige Datei anlegen.
// No session file stored - either a fresh power-on
// (menu_screen.cpp deliberately clears the entry when toggling) or
// migration from an older firmware without session logic. In both
// cases, create a new, guaranteed unique file now.
uint32_t enabledAt = SettingsStore::flightLogbookEnabledAtEpoch();
if (enabledAt == 0) enabledAt = (uint32_t)now;
resolveSessionFilename(enabledAt, currentSessionFile, sizeof(currentSessionFile));
@@ -204,17 +204,17 @@ void init() {
void update() {
if (!SettingsStore::flightLogbookEnabled()) return;
// 24h-Sicherheitsabschaltung: verhindert, dass ein unbemerkt aktives
// Flugbuch die SD-Karte nach und nach vollschreibt (siehe
// Bestaetigungsdialog beim Einschalten in menu_screen.cpp). Nur pruefen,
// wenn die Uhrzeit schon synchronisiert ist.
// 24h safety shutoff: prevents an unnoticed active
// flight logbook from filling up the SD card over time (see
// confirmation dialog when enabling in menu_screen.cpp). Only check
// when the clock has already synced.
time_t nowCheck = time(nullptr);
if (nowCheck > 8 * 3600 * 2) {
uint32_t enabledAt = SettingsStore::flightLogbookEnabledAtEpoch();
if (enabledAt == 0) {
// Migrations-Fall: Flugbuch war schon vor diesem Update aktiv
// (alte Einstellungsdatei ohne Zeitstempel) - Startzeitpunkt
// jetzt setzen, damit die 24h-Grenze trotzdem sicher greift.
// Migration case: flight logbook was already active before this update
// (old settings file without timestamp) - set start time
// now so that the 24h limit still works safely.
SettingsStore::setFlightLogbookEnabledAtEpoch((uint32_t)nowCheck);
} else if ((uint32_t)nowCheck >= enabledAt &&
(uint32_t)nowCheck - enabledAt >= 24UL * 3600UL) {
@@ -274,7 +274,7 @@ TopAltitude todayMaxAltitude() {
TopAltitude result;
SdMutex::Guard guard;
if (currentSessionFile[0] == 0) return result; // noch keine Sitzungsdatei bekannt
if (currentSessionFile[0] == 0) return result; // no session file known yet
char filename[64];
logFilename(filename, sizeof(filename));
@@ -287,10 +287,10 @@ TopAltitude todayMaxAltitude() {
uint32_t lineIdx = 0;
while (f.available()) {
String line = f.readStringUntil('\n');
if (firstLine) { firstLine = false; continue; } // CSV-Header ueberspringen
if (firstLine) { firstLine = false; continue; } // skip CSV header
if (line.length() == 0) continue;
// Spalten: timestamp,hex,callsign,reg,type,distance_km,altitude_ft
// Columns: timestamp,hex,callsign,reg,type,distance_km,altitude_ft
int commaIdx[6];
int found = 0;
int searchFrom = 0;
@@ -301,7 +301,7 @@ TopAltitude todayMaxAltitude() {
searchFrom = idx + 1;
found++;
}
if (found < 6) continue; // unvollstaendige/kaputte Zeile ueberspringen
if (found < 6) continue; // skip incomplete/corrupt line
String callsign = line.substring(commaIdx[1] + 1, commaIdx[2]);
String altStr = line.substring(commaIdx[5] + 1);
@@ -383,9 +383,9 @@ uint8_t listDays(DayEntry* out, uint8_t maxEntries) {
}
uint8_t listDaySummaries(DayEntry* out, uint8_t maxEntries) {
// Scannt grosszuegiger als maxEntries, damit auch bei vielen einzelnen
// Sitzungs-Dateien pro Tag noch korrekt pro Kalendertag aufsummiert
// wird, bevor auf die angeforderte Anzahl Tage begrenzt wird.
// Scans more generously than maxEntries, so that even with many individual
// session files per day it still correctly sums per calendar day
// before limiting to the requested number of days.
constexpr uint8_t MAX_RAW_SCAN = 90;
static DayEntry raw[MAX_RAW_SCAN];
uint8_t rawCount = listDays(raw, MAX_RAW_SCAN);
@@ -421,9 +421,9 @@ bool deleteFile(const char* label) {
bool ok = SD.remove(path);
if (ok && strcmp(label, currentSessionFile) == 0) {
// Die gerade aktive Sitzungsdatei wurde geloescht - Dopplungs-Liste
// zuruecksetzen, damit neue Sichtungen wieder korrekt in die (beim
// naechsten Schreibvorgang neu angelegte) Datei geloggt werden.
// The currently active session file was deleted - reset the
// deduplication list so that new sightings are correctly logged
// into the (on the next write newly created) file.
seenCount = 0;
}
return ok;

View File

@@ -14,44 +14,43 @@ namespace FlightLogbook {
int32_t altitudeFt = 0;
};
// Sucht in der Datei der aktuellen Sitzung den Eintrag mit der hoechsten
// geloggten Flughoehe (jeweils die Hoehe BEIM ERSTEN Sichten, nicht der
// aktuelle Wert) und gibt dessen Rufzeichen + Hoehe zurueck. found=false,
// wenn noch nichts geloggt wurde oder die Datei fehlt.
// Searches the current session file for the entry with the highest
// logged altitude (each being the altitude AT FIRST SIGHTING, not the
// current value) and returns its callsign + altitude. found=false,
// if nothing has been logged yet or the file is missing.
TopAltitude todayMaxAltitude();
void computeAllTimeStats(uint32_t& totalAircraft, uint16_t& totalDays);
struct DayEntry {
// Nicht mehr zwingend nur ein Kalenderdatum: bei mehrfachem
// Ein-/Ausschalten am selben Tag bekommt jede Sitzung eine eigene
// Datei mit Suffix (z.B. "2026-08-06_2") - siehe
// resolveSessionFilename() in flight_logbook.cpp. Puffer
// entsprechend groesser als ein reines "YYYY-MM-DD".
// No longer necessarily just a calendar date: when powering on/off
// multiple times on the same day, each session gets its own
// file with a suffix (e.g. "2026-08-06_2") - see
// resolveSessionFilename() in flight_logbook.cpp. Buffer
// accordingly larger than a plain "YYYY-MM-DD".
char date[16] = {0};
uint32_t count = 0;
};
// Eine Zeile pro Logbuch-DATEI (also ggf. mehrere pro Kalendertag, wenn
// das Flugbuch mehrfach am selben Tag ein-/ausgeschaltet wurde). Fuer
// den Logbuch-Dateien-Screen gedacht, wo jede Datei einzeln geloescht
// werden kann.
// One line per logbook FILE (so possibly multiple per calendar day, if
// the flight logbook was toggled on/off multiple times on the same day). Intended
// for the logbook files screen, where each file can be individually deleted.
uint8_t listDays(DayEntry* out, uint8_t maxEntries);
// Wie listDays(), fasst aber alle Sitzungs-Dateien desselben
// Kalendertags zu einem Eintrag zusammen (Summe der Anzahl) - fuer den
// 7-Tage-Verlauf im Statistik-Bildschirm, der weiterhin pro Tag statt
// pro einzelner Sitzung zaehlen soll.
// Like listDays(), but merges all session files of the same
// calendar day into one entry (sum of counts) - for the
// 7-day history in the statistics screen, which should still count per day
// rather than per individual session.
uint8_t listDaySummaries(DayEntry* out, uint8_t maxEntries);
// Loescht eine einzelne Logbuch-Datei (Label wie von listDays()
// zurueckgegeben, ohne ".csv"). Fuer die einzelnen Loesch-Buttons im
// Logbuch-Dateien-Screen gedacht. Wird gerade die aktive Sitzungsdatei
// geloescht, faengt die Aufzeichnung sauber neu in derselben Datei an.
// Deletes a single logbook file (label as returned by listDays(),
// without ".csv"). Intended for the individual delete buttons in the
// logbook files screen. If the currently active session file is
// deleted, logging restarts cleanly in the same file.
bool deleteFile(const char* label);
// Loescht ALLE Logbuch-CSV-Dateien auf der SD-Karte unwiderruflich und
// setzt die "heute schon gesehen"-Liste zurueck. Fuer den Reset-Button
// im Statistik-Bildschirm gedacht.
// Deletes ALL logbook CSV files on the SD card irreversibly and
// resets the "already seen today" list. Intended for the reset button
// in the statistics screen.
void resetAllData();
}

View File

@@ -188,43 +188,42 @@ enum class StringId : uint8_t {
MENU_BRIGHTNESS_PREFIX,
BRIGHTNESS_TITLE,
// Grosse Warn-Ueberlage beim Einschalten des Flugbuchs (Menue >
// Flugoptionen > Flugbuch), erklaert die 24h-Sicherheitsabschaltung.
// Large warning overlay when enabling the flight logbook (Menu >
// Flight Options > Flight Logbook), explains the 24h safety shutoff.
MENU_LOGBOOK_WARNING_TITLE,
MENU_LOGBOOK_WARNING_BODY,
// Preset-Name beim Anlegen (optional), "Presets voll"-Hinweis beim
// Antippen der Naechster-Flughafen-Laufschrift, sowie ein zusaetzlicher
// Info-Absatz dazu (Menue > Flugoptionen > Standort-Presets > "?").
// Preset name when creating (optional), "Presets full" hint when
// tapping the nearest-airport marquee, and an additional
// info paragraph about it (Menu > Flight Options > Location Presets > "?").
LOCATION_NAME_PROMPT,
LOCATION_NAME_SKIP,
LOCATION_PRESETS_FULL,
LOCATION_INFO_PARA5,
// Neuer, direkt im System-Menue erreichbarer Punkt "Logbuch/WebUI"
// (statt nur ueber das versteckte "?" im Logbuch-Dateien-Screen) -
// zeigt IP + Erklaerung der Weboberflaeche (Flugbuch
// ansehen/herunterladen/loeschen). Die urspruenglich hier mit
// enthaltene Screenshot-Verwaltung wurde wieder entfernt, siehe
// Entfernung von SCREENSHOT_SAVED_PREFIX/SCREENSHOT_FAILED oben - das
// Bildschirm-Auslesen (SPI-Readback) funktioniert auf diesem
// CYD-Board hardwareseitig nicht (TFT_MISO nicht angebunden).
// New item directly reachable in the System menu "Logbook/WebUI"
// (instead of only through the hidden "?" in the logbook files screen) -
// shows IP + explanation of the web interface (view logbook
// /download/delete). The originally included screenshot
// management was removed again, see
// removal of SCREENSHOT_SAVED_PREFIX/SCREENSHOT_FAILED above - the
// screen readback (SPI-readback) does not work on this
// CYD board at the hardware level (TFT_MISO not connected).
MENU_LOGBOOK_WEBUI,
WEBUI_TITLE,
WEBUI_INFO_PARA1,
WEBUI_INFO_PARA2,
// Kleines Info-Fenster, das beim Antippen des Wetter-Icons im Header
// erscheint (siehe main.cpp/showWeatherInfo()) - erklaert, dass das
// Wetter zum aktuell aktiven Standort (bzw. aktivem Standort-Preset)
// gehoert.
// Small info window that appears when tapping the weather icon in the header
// (see main.cpp/showWeatherInfo()) - explains that the
// weather belongs to the currently active location (or active location preset).
WEATHER_INFO_TITLE,
WEATHER_INFO_BODY,
// Adresssuche (AddressSearchScreen) - Standort per Adresseingabe statt
// manueller Koordinaten, per kostenlosem Nominatim-Geokodierungsdienst.
// Erreichbar sowohl beim Ersteinrichten (nach der Sprachauswahl) als
// auch jederzeit ueber Standort-Presets > "+".
// Address search (AddressSearchScreen) - location by address instead
// of manual coordinates, via the free Nominatim geocoding service.
// Reachable both during first-time setup (after language selection) and
// at any time via Location Presets > "+".
LOCATION_ADD_CHOICE_TITLE,
LOCATION_ADD_BY_COORDS,
LOCATION_ADD_BY_ADDRESS,
@@ -237,60 +236,60 @@ enum class StringId : uint8_t {
ADDRESS_SEARCH_TRY_AGAIN,
ADDRESS_SEARCH_CANCEL,
// Grosser Hinweis-Screen beim allerersten Start (nur einmalig, nach der
// Sprachauswahl) - erklaert den Genauigkeitsvorteil eines per Adresse
// gesetzten Standorts gegenueber der automatischen IP-Standort-
// bestimmung, mit direktem Einstieg in die Adresssuche. Ueberspringbar.
// Large info screen on very first start (only once, after language
// selection) - explains the accuracy advantage of a location set via
// address compared to automatic IP geolocation,
// with direct entry into the address search. Skippable.
FIRST_RUN_LOCATION_TITLE,
FIRST_RUN_LOCATION_BODY,
FIRST_RUN_LOCATION_SET_BTN,
FIRST_RUN_LOCATION_SKIP_BTN,
// Zusaetzlicher Info-Absatz im Standort-Presets-Screen (Menue >
// Flugoptionen > Standort-Presets > "?") - weist auf den "Per Adresse
// suchen"-Weg beim "+"-Button hin (deutlich genauer als die
// automatische IP-Standortbestimmung). Ergaenzt die knappe Erwaehnung
// in LOCATION_INFO_PARA3 um denselben anschaulichen Vergleich wie im
// Ersteinrichtungs-Screen (FIRST_RUN_LOCATION_BODY) und im README.
// Additional info paragraph in the location presets screen (Menu >
// Flight Options > Location Presets > "?") - points to the "Search by
// address" option on the "+" button (significantly more accurate than
// automatic IP geolocation). Complements the brief mention
// in LOCATION_INFO_PARA3 with the same illustrative comparison as in the
// first-run setup screen (FIRST_RUN_LOCATION_BODY) and in the README.
LOCATION_INFO_PARA6,
// Format-Hinweis unter dem Titel der Adresssuche (AddressSearchScreen)
// - ein Beispiel im lokalen Format (Strasse Hausnr., PLZ Ort), da das
// einzelne Freitextfeld sonst ohne jeden Hinweis war und Nutzer nicht
// wussten, was/in welcher Reihenfolge sie eingeben sollen.
// Format hint below the title of the address search (AddressSearchScreen)
// - an example in local format (Street No., Postal Code City), since the
// single free-text field had no hint at all and users did not
// know what / in which order to enter.
ADDRESS_SEARCH_HINT,
// Letzter Screen der Ersteinrichtung (nach der Standort-Adresssuche,
// main.cpp) - bestaetigt den Abschluss der Einrichtung, nennt den
// SD-Karten-Ordner, in dem alle Daten liegen, und weist darauf hin,
// dass sich das WLAN ab dem naechsten Start automatisch verbindet.
// Last screen of the first-time setup (after location address search,
// main.cpp) - confirms the completion of the setup, mentions the
// SD card folder where all data is stored, and points out
// that WiFi will connect automatically from the next start onward.
FIRST_RUN_COMPLETE_TITLE,
FIRST_RUN_COMPLETE_BODY1,
FIRST_RUN_COMPLETE_BODY2,
// Hinweis mit Beispielnamen unter dem Titel der Namens-Tastatur
// (location_presets_screen.cpp::runPresetNameKeypad UND
// address_search_screen.cpp::runNameKeypad, beide spiegeln sich) -
// ohne jeden Hinweis wussten Einsteiger nicht, was fuer ein Name hier
// gemeint ist (Preset-Name wie "Zuhause", nicht z.B. ein Ort/Adresse).
// Hint with example name below the title of the name keypad
// (location_presets_screen.cpp::runPresetNameKeypad AND
// address_search_screen.cpp::runNameKeypad, both mirror each other) -
// without any hint, beginners did not know what kind of name
// is meant here (preset name like "Home", not e.g. a city/address).
LOCATION_NAME_HINT,
// Legenden-Eintrag auf dem Radar-Screen (radar_screen.cpp::drawLegend)
// fuer die blauen Quadrat-Marker der Bodenfahrzeuge - nur sichtbar,
// wenn "Bodenfahrzeuge ausblenden" AUS ist (Flugoptionen), da die
// Fahrzeuge dann als eigene Legenden-Zeile unter den drei
// Hoehen-Farbstufen erscheinen.
// Legend entry on the radar screen (radar_screen.cpp::drawLegend)
// for the blue square markers of ground vehicles - only visible
// when "Hide ground vehicles" is OFF (Flight Options), since the
// vehicles then appear as their own legend line below the three
// altitude color levels.
LEGEND_GROUND_VEHICLE,
// Neue "Sicherung & Reset"-Unterseite im System-Menue (fasst die
// bisher einzeln im System-Menue stehenden Backup/Restore-Buttons
// zusammen und ergaenzt einen dritten Punkt "Einstellungen
// zuruecksetzen" - ein kompletter Werksreset fuer Entwickler/Tester,
// der den gesamten Flightradar-Ordner von der SD-Karte loescht und
// neu startet, siehe settings_backup.cpp::factoryReset()). Titel und
// Button-Label teilen sich MENU_BACKUP_RESET, genau wie
// MENU_CATEGORY_SYSTEM sowohl fuer den Menue-Button als auch den
// Bildschirmtitel der System-Seite verwendet wird.
// New "Backup & Reset" subsection in the System menu (combines the
// previously individually placed Backup/Restore buttons in the System menu
// and adds a third item "Reset
// settings" - a complete factory reset for developers/testers,
// which deletes the entire Flightradar folder from the SD card and
// restarts, see settings_backup.cpp::factoryReset()). Title and
// button label share MENU_BACKUP_RESET, just like
// MENU_CATEGORY_SYSTEM is used both for the menu button and the
// screen title of the System page.
MENU_BACKUP_RESET,
MENU_FACTORY_RESET,
MENU_FACTORY_RESET_WARNING_BODY,
@@ -303,26 +302,25 @@ enum class StringId : uint8_t {
// instead of waiting it out.
FIRST_RUN_COMPLETE_TAP_TO_SKIP,
// Zeilenpraefix im Radar-Detailpanel (radar_screen.cpp::drawDetailPanel)
// fuer die Flugroute (Start- -> Zielflughafen, ICAO-Code), abgefragt
// ueber AircraftDetails per Rufzeichen. Faellt wie MODEL/TYPE/SQUAWK auf
// DETAIL_UNKNOWN zurueck, wenn kein Rufzeichen bekannt ist oder keine
// Route gefunden wurde.
// Line prefix in the radar detail panel (radar_screen.cpp::drawDetailPanel)
// for the flight route (departure -> destination airport, ICAO code), queried
// via AircraftDetails by callsign. Falls back like MODEL/TYPE/SQUAWK to
// DETAIL_UNKNOWN if no callsign is known or no
// route was found.
DETAIL_ROUTE,
// Hinweiszeile ueber dem QR-Code auf dem neuen QR-Unterscreen der
// Logbuch/WebUI-Seite (webui_screen.cpp::runQrScreen) - der WEBUI_TITLE-
// String wird fuer die Kopfzeile wiederverwendet, kein eigener Titel
// noetig.
// Hint line above the QR code on the new QR sub-screen of the
// Logbook/WebUI page (webui_screen.cpp::runQrScreen) - the WEBUI_TITLE
// string is reused for the header line, no separate title needed.
WEBUI_QR_HINT,
// Ruhebildschirm bei Inaktivitaets-Timeout (Menue > System >
// "Ruhebildschirm", siehe SettingsStore::screensaverEnabled() und
// Screensaver during inactivity timeout (Menu > System >
// "Screensaver", see SettingsStore::screensaverEnabled() and
// main.cpp).
MENU_SCREENSAVER,
// OTA-Firmware-Update ueber WLAN (Menue > System > "Nach Update
// suchen", siehe menu_screen.cpp::runOtaUpdateScreen() und
// OTA firmware update over WiFi (Menu > System > "Check for
// update", see menu_screen.cpp::runOtaUpdateScreen() and
// ota_update.h/.cpp).
MENU_CHECK_UPDATE,
OTA_CHECKING,
@@ -334,23 +332,23 @@ enum class StringId : uint8_t {
OTA_UPDATE_FAILED,
OTA_UPDATE_SUCCESS,
// Erfolgs-/Fehler-Ergebnis nach Download+Flash wird jetzt als
// dauerhafter Info-Screen mit explizitem Button angezeigt (statt
// automatischem Neustart bzw. kurzer, automatisch verschwindender
// Meldung) - der Nutzer muss bei einer sicherheitsrelevanten Aktion wie
// einem Firmware-Update IMMER aktiv informiert werden und selbst
// bestaetigen, siehe menu_screen.cpp::infoScreen().
// Success/error result after download+flash is now shown as
// a permanent info screen with explicit button (instead of
// automatic restart or a brief, auto-dismissing
// message) - the user MUST be actively informed about a security-relevant
// action like a firmware update and must
// confirm it themselves, see menu_screen.cpp::infoScreen().
OTA_SUCCESS_BODY,
OTA_RESTART_BUTTON,
OTA_FAILED_BODY,
// Eigener Bildschirm-Timeout-Screen (Menue > System > Bildschirm-
// Timeout, siehe timeout_screen.cpp) mit Schieberegler statt des
// vorherigen Durchklickens per wiederholtem Antippen - der
// Ruhebildschirm-Umschalter (vorher eigene Zeile im System-Menue,
// MENU_SCREENSAVER) zieht mit auf diesen Screen um, da er inhaltlich
// eng mit dem Timeout zusammenhaengt und hier genug Platz fuer eine
// kurze Erklaerung ist.
// Dedicated screen timeout screen (Menu > System > Screen
// Timeout, see timeout_screen.cpp) with slider instead of the
// previous clicking through by repeated tapping - the
// screensaver toggle (previously its own line in the System menu,
// MENU_SCREENSAVER) moves to this screen as well, since it is
// closely related to the timeout and there is enough space for a
// brief explanation here.
TIMEOUT_SCREEN_TITLE,
TIMEOUT_SCREENSAVER_DESC,

View File

@@ -1,9 +1,9 @@
#pragma once
#include <Arduino.h>
// Steuert die diskrete RGB-LED auf der Rueckseite des CYD (kein Lautsprecher
// vorhanden, daher ersetzt die LED den akustischen Alarm vom Cardputer-
// Projekt). Pins: Rot=GPIO4, Gruen=GPIO16, Blau=GPIO17, active-low (LOW = an).
// Controls the discrete RGB LED on the back of the CYD (no speaker
// present, therefore the LED replaces the acoustic alarm from the Cardputer
// project). Pins: Red=GPIO4, Green=GPIO16, Blue=GPIO17, active-low (LOW = on).
namespace LedAlert {
enum class Mode {
@@ -18,9 +18,9 @@ namespace LedAlert {
void pulseHeartbeat(uint32_t nowMs);
// Blockierendes, kurzes weisses Aufblitzen (alle 3 Farbkanaele an) als
// sofortige Bestaetigung fuer eine einmalige Nutzeraktion (z.B. "Cam"-
// Button getroffen). Bewusst blockierend (delay), da nur aus dem
// Haupt-Loop bei einem Tap aufgerufen, nicht aus der NetTask-Schleife.
// Blocking, short white flash (all 3 color channels on) as
// immediate confirmation for a one-time user action (e.g. "Cam"
// button hit). Deliberately blocking (delay), since only called from the
// main loop on a tap, not from the NetTask loop.
void flashWhite(uint32_t durationMs = 150);
}

View File

@@ -20,14 +20,13 @@ namespace LocationManager {
bool hasGpsFix();
// UTC-Offset in Sekunden (inkl. evtl. Sommerzeit), ermittelt bei der
// IP-Geolocation-Abfrage. 0/false, falls noch nicht bekannt.
// UTC offset in seconds (including possible DST), determined during the
// IP geolocation query. 0/false, if not yet known.
bool hasUtcOffset();
int32_t utcOffsetSeconds();
// Ob die Region (per IP-Geolocation-Laendercode) metrische Einheiten
// nutzt (Meter/km) statt Fuss/Meilen. Default true (metrisch), bis die
// IP-Abfrage etwas anderes ermittelt hat - nur die USA nutzen aktuell
// eine Ausnahme.
// Whether the region (per IP geolocation country code) uses metric units
// (meters/km) instead of feet/miles. Default true (metric), until the
// IP query determines otherwise - currently only the USA is an exception.
bool useMetricUnits();
}

View File

@@ -13,10 +13,9 @@ namespace {
struct Preset {
double lat = 0;
double lon = 0;
// Optionaler, vom Nutzer vergebener Name (z.B. "Zuhause") oder vom
// naechstgelegenen Flughafen uebernommen - leer, wenn keiner gesetzt
// wurde. 16 Zeichen + Nullterminierung reichen fuer eine gut lesbare
// Zeile in der Preset-Liste.
// Optional user-assigned name (e.g. "Home") or taken from the nearest
// airport - empty if none was set. 16 characters + null terminator
// are sufficient for a well-readable line in the preset list.
char name[17] = {0};
};
@@ -65,9 +64,9 @@ namespace {
if (comma1 < 0) continue;
if (presetCount >= MAX_PRESETS) break;
// Der Name ist ein optionales drittes Feld - aeltere,
// gespeicherte Presets (vor diesem Feature) haben nur
// "lat,lon" ohne zweites Komma, dann bleibt der Name leer.
// The name is an optional third field - older,
// saved presets (before this feature) only have
// "lat,lon" without a second comma, then the name stays empty.
int comma2 = line.indexOf(',', comma1 + 1);
presets[presetCount].lat = line.substring(0, comma1).toDouble();
if (comma2 < 0) {

View File

@@ -8,9 +8,8 @@ namespace LocationPresets {
uint8_t count();
void getLatLon(uint8_t index, double& lat, double& lon);
// Vom Nutzer vergebener Name (z.B. "Zuhause") - leerer String, wenn
// keiner gesetzt wurde (dann zeigt der Screen stattdessen die
// Koordinaten an).
// User-assigned name (e.g. "Home") - empty string if none was set
// (the screen will show the coordinates instead).
String getName(uint8_t index);
bool addPreset(double lat, double lon, const String& name = String());

View File

@@ -116,11 +116,11 @@ namespace {
return confirmed ? String(buf) : String();
}
// Vier Tastaturreihen wie bei der Rufzeichen-Eingabe (siehe
// aircraft_watchlist_screen.cpp::runCallsignKeypad), aber mit Leertaste
// (Ortsnamen enthalten oft Leerzeichen, z.B. "Bei Oma") und OHNE
// Eingabepflicht - der Name ist optional, "Ohne Namen" bestaetigt mit
// leerem Puffer statt die Eingabe abzubrechen.
// Four keyboard rows like for callsign input (see
// aircraft_watchlist_screen.cpp::runCallsignKeypad), but with space bar
// (location names often contain spaces, e.g. "Bei Oma") and WITHOUT
// mandatory input - the name is optional, "Without name" confirms with
// empty buffer instead of cancelling input.
String runPresetNameKeypad(TFT_eSPI& tft) {
MenuStars::reset();
constexpr const char* DIGITS = "1234567890";
@@ -135,9 +135,9 @@ namespace {
constexpr int16_t KEY_GAP = 3;
constexpr int16_t FIELD_H = 34;
// Kopfbereich (Titel + Namens-Hinweis) einmal zeichnen, um seine
// tatsaechliche Hoehe per getCursorY() zu messen - selbes Muster wie
// in address_search_screen.cpp::runAddressKeyboard().
// Draw header (title + name hint) once to measure its
// actual height via getCursorY() - same pattern as
// in address_search_screen.cpp::runAddressKeyboard().
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 14);
@@ -258,17 +258,16 @@ namespace {
String name = runPresetNameKeypad(tft);
if (!LocationPresets::addPreset(lat, lon, name)) return false;
// Neu angelegtes Preset sofort aktivieren, gleiches Verhalten wie
// beim Anlegen per Adresssuche (siehe address_search_screen.cpp) -
// sonst blieb z.B. der automatische IP-Standort aktiv, obwohl gerade
// extra ein neuer Standort eingegeben wurde.
// Activate newly created preset immediately, same behavior as
// when creating via address search (see address_search_screen.cpp) -
// otherwise the automatic IP location would remain active even though
// a new location was just explicitly entered.
LocationPresets::setActiveIndex((int8_t)(LocationPresets::count() - 1));
return true;
}
// Erster Schritt beim Antippen von "+": manuelle Koordinaten oder
// Adresssuche (AddressSearchScreen, kuemmert sich dort bereits selbst
// um Namensvergabe + Speichern als Preset).
// First step when tapping "+": manual coordinates or
// address search (AddressSearchScreen handles naming + saving as preset itself).
bool addPresetFlow(TFT_eSPI& tft) {
MenuStars::reset();
tft.fillScreen(TFT_BLACK);
@@ -293,8 +292,8 @@ namespace {
}
}
// Kurze Meldung unten am Bildschirmrand, z.B. wenn beim Antippen des
// Naechster-Flughafen-Textes bereits alle 3 Preset-Slots belegt sind.
// Brief message at the bottom of the screen, e.g. when tapping the
// nearest airport text but all 3 preset slots are already occupied.
void showBriefMessage(TFT_eSPI& tft, const String& msg, uint16_t color) {
tft.fillRect(0, Config::SCREEN_HEIGHT - 18, Config::SCREEN_WIDTH, 18, TFT_BLACK);
tft.setTextColor(color, TFT_BLACK);
@@ -334,50 +333,49 @@ namespace {
return y;
}
// Laufschrift fuer den "Naechster Flughafen"-Text: statt ihn mit "..."
// abzuschneiden, scrollt er horizontal durch, falls er nicht in die
// verfuegbare Breite passt. BEWUSST OHNE tft.setViewport() (das hatte
// im Zusammenspiel mit unserem eigenen Font dazu gefuehrt, dass der Text
// komplett unsichtbar wurde) - stattdessen wird bei jedem Schritt einfach
// der Text-Ausschnitt neu berechnet, der garantiert in die Breite passt.
// Marquee text for the "Nearest Airport" text: instead of truncating with "..."
// it scrolls horizontally if it does not fit the available width.
// DELIBERATELY WITHOUT tft.setViewport() (which had caused the text to become
// completely invisible when used with our custom font) - instead, at each step
// the text portion that is guaranteed to fit the width is simply recalculated.
struct Marquee {
String text;
String ring; // text + Luecke, doppelt aneinandergehaengt
String ring; // text + gap, doubled end-to-end
bool needsScroll = false;
int32_t charOffset = 0;
uint32_t lastStepMs = 0;
};
Marquee airportMarquee;
// Etwas kuerzeres Intervall als frueher fuer einen ruhigeren Lauf.
// WICHTIG: der Cursor wird beim Zeichnen IMMER exakt auf x gesetzt
// (nie davor/danach verschoben) - ein Versuch mit pixelgenauer
// Sub-Zeichen-Verschiebung (Cursor links von x) hat zu einem
// fehlerhaft gezeichneten Zeichen ausserhalb des geloeschten Bereichs
// gefuehrt (sichtbar als gruener Kasten links vom Text).
constexpr uint32_t MARQUEE_STEP_MS = 200; // alle 200ms ein Zeichen weiter
// Slightly shorter interval than before for a smoother run.
// IMPORTANT: the cursor is ALWAYS set exactly to x when drawing
// (never shifted before/after) - an attempt with pixel-precise
// sub-character shifting (cursor left of x) led to a
// incorrectly drawn character outside the erased area
// (visible as a green box left of the text).
constexpr uint32_t MARQUEE_STEP_MS = 200; // advance one character every 200ms
// Einmal aufrufen, wenn sich der anzuzeigende Text aendert (z.B. weil ein
// anderer Standort aktiviert wurde) - merkt sich Text + Ringpuffer und
// setzt den Scroll-Fortschritt zurueck.
// Call once when the displayed text changes (e.g. because a
// different location was activated) - stores text + ring buffer and
// resets the scroll progress.
void setupMarquee(TFT_eSPI& tft, const String& text, int16_t viewportW) {
// WICHTIG: textWidth() haengt von der aktuell gesetzten Textgroesse
// ab. Der Marquee wird in Size 2 gezeichnet (siehe drawMarquee),
// also muss hier ebenfalls Size 2 aktiv sein, sonst wird die
// Breite falsch (zu schmal) gemessen und der Text ragt beim
// Zeichnen ueber den verfuegbaren Platz hinaus.
// IMPORTANT: textWidth() depends on the currently set text size.
// The marquee is drawn in Size 2 (see drawMarquee),
// so Size 2 must also be active here, otherwise the
// width will be measured incorrectly (too narrow) and the text will
// protrude beyond the available space when drawn.
tft.setTextSize(2);
airportMarquee.text = text;
airportMarquee.needsScroll = tft.textWidth(text) > viewportW;
tft.setTextSize(1);
String withGap = text + " "; // 3 Leerzeichen Luecke vor der Wiederholung
String withGap = text + " "; // 3 spaces gap before the repeat
airportMarquee.ring = withGap + withGap;
airportMarquee.charOffset = 0;
airportMarquee.lastStepMs = millis();
}
// Liefert den laengsten Ausschnitt ab startIdx, der noch in maxWidth
// passt - OHNE "..." anzuhaengen (im Gegensatz zu truncateForWidth).
// Returns the longest substring from startIdx that still fits in maxWidth
// WITHOUT appending "..." (unlike truncateForWidth).
String marqueeWindow(TFT_eSPI& tft, const String& src, int32_t startIdx, int16_t maxWidth) {
String s = src.substring(startIdx);
while (s.length() > 1 && tft.textWidth(s) > maxWidth) {
@@ -386,25 +384,25 @@ namespace {
return s;
}
// Bei jedem Aufruf (auch in der Warteschleife) neu zeichnen - wenn der
// Text nicht scrollen muss, wird er einfach normal (fest) angezeigt.
// Redraw on every call (also in the wait loop) - if the
// text does not need to scroll, it is simply displayed normally (static).
void drawMarquee(TFT_eSPI& tft, int16_t x, int16_t y, int16_t w, int16_t h) {
if (airportMarquee.text.length() == 0) return;
// Der geloeschte Bereich muss zur tatsaechlichen Texthoehe bei
// Size 2 passen (~16px), nicht zur alten Size-1-Hoehe - sonst
// bleiben oben Reste vom vorherigen Frame stehen (sichtbar als
// Strich ueber dem Text). Etwas grosszuegiger nach oben (start
// bei y-20) als vorher, da ein einzelner Frame-Ausreisser sonst
// noch sichtbar blieb.
// The erased area must match the actual text height at
// Size 2 (~16px), not the old Size 1 height - otherwise
// remnants from the previous frame remain at the top (visible as
// a line above the text). Slightly more generous upward (start
// at y-20) than before, since a single frame outlier would otherwise
// remain visible.
constexpr int16_t MARQUEE_CLEAR_TOP = 20;
constexpr int16_t MARQUEE_CLEAR_H = 26;
tft.fillRect(x, y - MARQUEE_CLEAR_TOP, w, MARQUEE_CLEAR_H, TFT_BLACK);
tft.setTextColor(TFT_DARKGREEN, TFT_BLACK);
// Groessere Schrift fuer den Nearest-Airport-Marquee (auf Wunsch
// vergroessert von Size 1 auf Size 2) - nach dem Zeichnen wieder auf
// Size 1 zurueckstellen, damit nachfolgender Code (z.B. der
// "Zurueck"-Button) nicht versehentlich auch vergroessert wird.
// Larger font for the Nearest Airport marquee (enlarged
// from Size 1 to Size 2 on request) - reset back to
// Size 1 after drawing so that subsequent code (e.g. the
// "Back" button) is not accidentally enlarged as well.
tft.setTextSize(2);
tft.setCursor(x, y);
@@ -418,8 +416,8 @@ namespace {
if (now - airportMarquee.lastStepMs >= MARQUEE_STEP_MS) {
airportMarquee.lastStepMs = now;
airportMarquee.charOffset++;
// Zurueck an den Anfang, sobald der erste (nicht doppelte)
// Text+Luecke-Block durchgelaufen ist.
// Back to the beginning once the first (non-duplicate)
// text+gap block has completed.
int32_t singleLen = (int32_t)airportMarquee.text.length() + 3;
if (airportMarquee.charOffset >= singleLen) airportMarquee.charOffset = 0;
}
@@ -428,11 +426,11 @@ namespace {
tft.setTextSize(1);
}
// Laufschrift-Zustand pro Preset-Zeile, analog zu airportMarquee oben,
// aber als Array (eine Instanz je Zeile) und LINKSBUENDIG gezeichnet
// statt zentriert - zentriertes Scrollen wuerde bei jedem Schritt
// sichtbar hin- und herspringen, da sich die Textbreite laufend
// aendert. Ersetzt die bisherige truncateForWidth()-Kuerzung mit "...".
// Marquee state per preset row, analogous to airportMarquee above,
// but as an array (one instance per row) and LEFT-ALIGNED
// instead of centered - centered scrolling would visibly jump
// back and forth at each step since the text width constantly
// changes. Replaces the previous truncateForWidth() shortening with "...".
struct RowMarquee {
String text;
String ring;
@@ -442,8 +440,8 @@ namespace {
};
RowMarquee rowMarquees[LocationPresets::MAX_PRESETS];
// Wie setupMarquee() oben, aber ohne Textgroessen-Umschaltung (die
// Preset-Zeilen nutzen durchgehend Size 1).
// Like setupMarquee() above, but without text size switching (the
// preset rows consistently use Size 1).
void setupRowMarquee(TFT_eSPI& tft, RowMarquee& m, const String& text, int16_t maxWidth) {
m.text = text;
m.needsScroll = tft.textWidth(text) > maxWidth;
@@ -453,12 +451,11 @@ namespace {
m.lastStepMs = millis();
}
// Zeichnet eine Preset-Zeile im selben Rahmen-/Fuellstil wie
// drawButton(), aber mit linksbuendiger Laufschrift statt zentriertem,
// ggf. abgeschnittenem Text. Wird sowohl beim ersten Bildschirmaufbau
// als auch (nur fuer Zeilen mit needsScroll) bei jedem Tick in der
// Warteschleife erneut aufgerufen, um den naechsten Scroll-Schritt zu
// zeichnen.
// Draws a preset row in the same frame/fill style as
// drawButton(), but with left-aligned marquee text instead of centered,
// potentially truncated text. Called both on initial screen build
// and (only for rows with needsScroll) on each tick in the
// wait loop to draw the next scroll step.
void drawRowMarquee(TFT_eSPI& tft, const Rect& r, RowMarquee& m, bool active) {
uint16_t bg = active ? TFT_GREEN : TFT_BLACK;
uint16_t fg = active ? TFT_BLACK : TFT_GREEN;
@@ -611,19 +608,19 @@ void run(TFT_eSPI& tft) {
String presetName = LocationPresets::getName(i);
String label;
if (presetName.length() > 0) {
// Benannte Presets (manuell vergeben oder vom
// Flughafen-Antippen uebernommen) zeigen den Namen statt
// der Koordinaten - besser lesbar in der Uebersicht.
// Named presets (manually assigned or taken over from
// tapping an airport) show the name instead of
// the coordinates - more readable in the overview.
label = (active == (int8_t)i ? "> " : "") + presetName;
} else {
char coords[24];
snprintf(coords, sizeof(coords), "%d: %.2f, %.2f", i + 1, lat, lon);
label = (active == (int8_t)i ? "> " : "") + presetWord + " " + coords;
}
// Laufschrift statt "..."-Kuerzung fuer lange Preset-Namen
// bzw. Koordinaten, die nicht in die Zeilenbreite passen
// (siehe RowMarquee oben) - kurze Labels bleiben unveraendert
// zentriert wie zuvor.
// Marquee instead of "..." truncation for long preset names
// or coordinates that do not fit the row width
// (see RowMarquee above) - short labels remain unchanged
// centered as before.
if (tft.textWidth(label) <= rowRect.w - 10) {
rowMarquees[i].needsScroll = false;
drawButton(tft, rowRect, label, active == (int8_t)i);
@@ -653,10 +650,10 @@ void run(TFT_eSPI& tft) {
constexpr int16_t AIRPORT_LINE_X = 10;
constexpr int16_t AIRPORT_LINE_W = Config::SCREEN_WIDTH - 20;
int16_t airportLineY = (int16_t)(Config::SCREEN_HEIGHT - 60);
// Deckt denselben Bereich ab, den drawMarquee() bei jedem Frame
// loescht (siehe MARQUEE_CLEAR_TOP/-H) - dient sowohl als Tap-Zone
// als auch als sichtbarer Rahmen, der anzeigt, dass man hier
// antippen kann, um den Flughafen als neuen Preset zu uebernehmen.
// Covers the same area that drawMarquee() clears on every frame
// (see MARQUEE_CLEAR_TOP/-H) - serves both as a tap zone
// and as a visible frame indicating that you can
// tap here to adopt the airport as a new preset.
Rect airportRect = {(int16_t)(AIRPORT_LINE_X - 4), (int16_t)(airportLineY - 20),
(int16_t)(AIRPORT_LINE_W + 8), 26};
AirportLookup::Nearest nearest;
@@ -669,8 +666,8 @@ void run(TFT_eSPI& tft) {
}
nearest = AirportLookup::findNearest(activeLat, activeLon);
if (nearest.found) {
// Respektiert jetzt die Einheiten-Einstellung (Menue >
// Einheiten) - vorher immer "(XX km)", auch bei Imperial.
// Now respects the units setting (Menu >
// Units) - previously always "(XX km)", even in imperial.
char buf[48];
if (LocationManager::useMetricUnits()) {
snprintf(buf, sizeof(buf), "%s %s (%.0f km)", nearest.icao, nearest.name, nearest.distanceKm);
@@ -726,8 +723,8 @@ void run(TFT_eSPI& tft) {
if (!handled && nearest.found && airportRect.contains(tap.x, tap.y)) {
if (canAdd) {
if (LocationPresets::addPreset(nearest.lat, nearest.lon, nearest.name)) {
// Gleiches Verhalten wie bei den anderen beiden Wegen,
// ein Preset anzulegen - sofort aktivieren.
// Same behavior as with the other two ways
// of creating a preset - activate immediately.
LocationPresets::setActiveIndex((int8_t)(LocationPresets::count() - 1));
}
} else {

View File

@@ -29,8 +29,8 @@ namespace {
constexpr uint8_t MAX_DAYS_QUERIED = 31;
constexpr uint8_t VISIBLE_ROWS = 10;
// Erste Inhaltszeile eine Zeile tiefer als frueher (30) ansetzen, damit
// sie nicht mit dem "?"-Info-Button oben rechts (y=2..26) kollidiert.
// Start the first content line one line lower than before (30), so that
// it does not collide with the "?" info button at top right (y=2..26).
constexpr int16_t ROWS_START_Y = 50;
int16_t layoutWrapped(TFT_eSPI& tft, int16_t x, int16_t startY, int16_t maxWidth,
@@ -197,10 +197,10 @@ void run(TFT_eSPI& tft) {
tft.setCursor(110, y);
tft.print(String(days[i].count) + I18n::t(StringId::LOGFILES_AIRCRAFT_SUFFIX));
// Jede Datei einzeln loeschbar (selber Stil wie das rote 'X'
// bei den WLAN-Netzwerken) - keine Bestaetigung noetig, das
// globale "Flugbuch zuruecksetzen" im Statistik-Screen
// bleibt fuer den Alles-loeschen-Fall.
// Each file individually deletable (same style as the red 'X'
// for WiFi networks) - no confirmation needed, the
// global "Reset flight logbook" in the statistics screen
// remains for deleting everything.
Rect delBtn = {(int16_t)(Config::SCREEN_WIDTH - 34), (int16_t)(y - 15), 30, 18};
drawButton(tft, delBtn, "X", true);
deleteRects[deleteCount++] = delBtn;

View File

@@ -3,7 +3,7 @@
#include <TFT_eSPI.h>
namespace LogbookFilesScreen {
// Blockierend: listet die Logbuch-Dateien (ein Tag pro Zeile, mit Anzahl
// Eintraegen) auf. Kehrt zurueck, sobald "Back" angetippt wird.
// Blocking: lists the logbook files (one day per line, with number
// of entries). Returns as soon as "Back" is tapped.
void run(TFT_eSPI& tft);
}

View File

@@ -61,26 +61,26 @@ uint32_t lastInteractionMs = 0;
bool screenDimmed = false;
bool nightDimActive = false;
// Ruhebildschirm (Menue > System > Ruhebildschirm, siehe SettingsStore::
// screensaverEnabled()) - true, waehrend statt des komplett dunklen
// Backlights ein gedimmter Sternenhimmel mit Uhrzeit angezeigt wird.
// Getrennt von screenDimmed, da NICHT jeder Timeout automatisch ein
// Ruhebildschirm ist (Default bleibt: Backlight komplett aus) - siehe
// loop() weiter unten.
// Screensaver (Menu > System > Screensaver, see SettingsStore::
// screensaverEnabled()) - true while a dimmed starry sky with clock is shown
// instead of the completely dark backlight.
// Separate from screenDimmed, since NOT every timeout automatically becomes a
// screensaver (default remains: backlight completely off) - see
// loop() below.
bool screensaverShowing = false;
uint32_t lastScreensaverClockMs = 0;
// Wandelt die Nutzer-Helligkeit (Menue > System > Helligkeit, 10-100%) in
// einen PWM-Wert 0-255 um - ersetzt das bisher fest verdrahtete
// BACKLIGHT_FULL als "normale" Helligkeit ueberall unten.
// Converts user brightness (Menu > System > Brightness, 10-100%) into
// a PWM value 0-255 - replaces the previously hardwired
// BACKLIGHT_FULL as the "normal" brightness everywhere below.
uint8_t normalBacklightPwm() {
return (uint8_t)((uint16_t)SettingsStore::brightnessPercent() * 255 / 100);
}
// Nachtmodus-Helligkeit RELATIV zur normalen Helligkeit (siehe
// Config::NIGHT_DIM_REDUCTION_PERCENT) statt eines festen Absolutwerts -
// so bleibt der Dimm-Effekt bei JEDER eingestellten Normalhelligkeit
// spuerbar, auch bei schon niedrig eingestellter Helligkeit.
// Night mode brightness RELATIVE to normal brightness (see
// Config::NIGHT_DIM_REDUCTION_PERCENT) instead of a fixed absolute value -
// so the dimming effect remains noticeable at EVERY set normal brightness,
// even when already set low.
uint8_t nightDimBacklightPwm() {
uint32_t normal = normalBacklightPwm();
return (uint8_t)(normal * (100 - Config::NIGHT_DIM_REDUCTION_PERCENT) / 100);
@@ -95,9 +95,9 @@ struct Rect {
Rect menuBtn = {Config::SCREEN_WIDTH - 90, 3, 54, 22};
// Platz, an dem frueher der Cam-Button war (siehe entfernte Screenshot-
// Funktion) - zeigt jetzt stattdessen ein kleines Wetter-Icon fuer den
// aktuell aktiven Standort (siehe weather.cpp/Weather::update()).
// Space where the camera button used to be (see removed screenshot
// function) - now shows a small weather icon for the
// currently active location (see weather.cpp/Weather::update()).
Rect weatherIconRect = {(int16_t)(menuBtn.x - 46), 3, 42, 22};
void drawMenuButton() {
@@ -109,8 +109,8 @@ void drawMenuButton() {
tft.setTextDatum(TL_DATUM);
}
// Kleine, mit einfachen TFT_eSPI-Grundformen gezeichnete Wolke (kein
// Bild/Font noetig) - Basis fuer die Regen/Schnee/Gewitter-Varianten.
// Small cloud drawn with simple TFT_eSPI basic shapes (no
// image/font needed) - base for the rain/snow/thunderstorm variants.
void drawCloudShape(int16_t cx, int16_t cy, uint16_t color) {
tft.fillCircle(cx - 5, cy + 1, 3, color);
tft.fillCircle(cx - 1, cy - 2, 4, color);
@@ -118,7 +118,7 @@ void drawCloudShape(int16_t cx, int16_t cy, uint16_t color) {
tft.fillRect(cx - 8, cy, 13, 3, color);
}
// Sonne mit ein paar Strahlen ringsum, ebenfalls nur aus Grundformen.
// Sun with a few rays around, also only from basic shapes.
void drawSunShape(int16_t cx, int16_t cy, int16_t r, uint16_t color) {
tft.fillCircle(cx, cy, r, color);
for (uint8_t i = 0; i < 8; i++) {
@@ -131,10 +131,10 @@ void drawSunShape(int16_t cx, int16_t cy, int16_t r, uint16_t color) {
}
}
// Zeichnet das Wetter-Icon passend zur zuletzt abgefragten Wetterlage
// (Weather::current()) - bei Weather::Condition::Unknown (noch keine
// erfolgreiche Abfrage, z.B. kurz nach dem Booten) bleibt die Flaeche
// einfach leer, statt einen Platzhalter anzuzeigen.
// Draws the weather icon matching the last queried weather condition
// (Weather::current()) - if Weather::Condition::Unknown (no successful
// query yet, e.g. shortly after boot) the area stays
// empty instead of showing a placeholder.
void drawWeatherIcon() {
int16_t cx = weatherIconRect.x + weatherIconRect.w / 2;
int16_t cy = weatherIconRect.y + weatherIconRect.h / 2;
@@ -176,11 +176,11 @@ void drawWeatherIcon() {
}
}
// Einfacher Zeilenumbruch fuer Fliesstext, analog zu layoutWrapped() in
// menu_screen.cpp/webui_screen.cpp (siehe "keine geteilten Module"-Konvention
// - jede Screen-Datei hat ihre eigene kleine Kopie). Ohne Scroll-Unterstuetzung,
// da der Infotext des Wetter-Popups kurz genug ist, um immer komplett in die
// verfuegbare Boxhoehe zu passen.
// Simple word wrapping for body text, analogous to layoutWrapped() in
// menu_screen.cpp/webui_screen.cpp (see "no shared module" convention
// - each screen file has its own small copy). No scroll support,
// since the weather popup info text is short enough to always fit completely in the
// available box height.
int16_t weatherInfoLayoutWrapped(TFT_eSPI& tftRef, int16_t x, int16_t startY,
int16_t maxWidth, int16_t lineHeight, const String& text) {
int16_t y = startY;
@@ -214,12 +214,12 @@ void weatherInfoDrawButton(TFT_eSPI& tftRef, const Rect& r, const String& label)
tftRef.setTextDatum(TL_DATUM);
}
// Kleines Info-Fenster beim Antippen des Wetter-Icons im Header - erklaert,
// dass das angezeigte Wetter immer zum aktuell aktiven Standort (bzw.
// aktivem Standort-Preset, siehe LocationManager::getHomeLocation())
// gehoert. Gleicher geboxter Overlay-Stil wie confirmLogbookEnable() in
// menu_screen.cpp, hier aber ohne Warnfarbe und nur mit einem
// Zurueck-Button, da rein informativ (keine Bestaetigung noetig).
// Small info window when tapping the weather icon in the header - explains
// that the displayed weather always belongs to the currently active location (or
// active location preset, see LocationManager::getHomeLocation()).
// Same boxed overlay style as confirmLogbookEnable() in
// menu_screen.cpp, but here without warning color and only a
// Back button, since purely informational (no confirmation needed).
void showWeatherInfo(TFT_eSPI& tftRef) {
constexpr int16_t BOX_X = 4;
constexpr int16_t BOX_Y = 4;
@@ -228,7 +228,7 @@ void showWeatherInfo(TFT_eSPI& tftRef) {
constexpr int16_t TEXT_MAX_WIDTH = BOX_W - 20;
constexpr int16_t LINE_H = 16;
constexpr int16_t TITLE_Y = BOX_Y + 16;
// Eine Leerzeile Abstand zwischen Titel und Fliesstext.
// One blank line gap between title and body text.
constexpr int16_t VIEW_TOP = TITLE_Y + 12 + LINE_H;
constexpr int16_t BTN_H = 36;
@@ -292,13 +292,13 @@ void drawWifiIcon(int16_t rightX, int16_t rowTop, int16_t rowH, int8_t rssi) {
}
}
// Grosse, zentrierte Uhrzeit fuer den Ruhebildschirm (siehe
// screensaverShowing oben) - eigene, groessere Variante der kleinen
// Kopfzeilen-Uhr aus updateStatusLine(), da diese bewusst kompakt gehalten
// ist. Loescht bei jedem Aufruf nur den eigenen schmalen Streifen in der
// Bildschirmmitte (nicht den ganzen Bildschirm), damit die Sternenanimation
// darum herum ungestoert weiterlaeuft. Zeichnet nichts, solange die Uhrzeit
// noch nicht per NTP synchronisiert ist (gleiche Pruefung wie
// Large, centered clock for the screensaver (see
// screensaverShowing above) - own larger variant of the small
// header clock from updateStatusLine(), since that one is deliberately kept compact.
// Only clears its own narrow strip in the
// middle of the screen (not the whole screen) so the star animation
// around it continues undisturbed. Draws nothing as long as the time
// has not been synchronized via NTP yet (same check as
// updateStatusLine()/isNightDimHours()).
void drawScreensaverClock() {
time_t now = time(nullptr);
@@ -306,15 +306,8 @@ void drawScreensaverClock() {
struct tm tmNow;
localtime_r(&now, &tmNow);
char timeBuf[9];
if (LocationManager::useMetricUnits()) {
snprintf(timeBuf, sizeof(timeBuf), "%02d:%02d", tmNow.tm_hour, tmNow.tm_min);
} else {
int hour12 = tmNow.tm_hour % 12;
if (hour12 == 0) hour12 = 12;
snprintf(timeBuf, sizeof(timeBuf), "%d:%02d%s", hour12, tmNow.tm_min,
tmNow.tm_hour < 12 ? "AM" : "PM");
}
char timeBuf[9];
snprintf(timeBuf, sizeof(timeBuf), "%02d:%02d", tmNow.tm_hour, tmNow.tm_min);
constexpr int16_t CLOCK_BAND_H = 40;
int16_t cy = Config::SCREEN_HEIGHT / 2;
@@ -354,23 +347,23 @@ void drawHeader() {
void updateStatusLine() {
if (wasEmergency) return;
// Die Uhrzeit nutzt den global gesetzten 11pt-Font (setFreeFont() in
// setup()) ueber setCursor()+print() - bei GFXFF-Fonts ist das
// baseline-verankert, der Text waechst also nach OBEN (siehe
// CLAUDE.md-Hinweis zu diesem Pitfall). Die Ziffern-Glyphen sind laut
// Font-Metrik 8px hoch und reichen damit bis y=HEADER_TITLE_H-6 - also
// OBERHALB des schmalen STATUS_LINE_H-Bereichs, der hier bisher allein
// geloescht wurde. Dadurch blieben alte Ziffern-Reste stehen und
// ueberlagerten sich mit den neuen (am sichtbarsten bei der letzten
// Minutenziffer, die sich am haeufigsten aendert). Deshalb hier gezielt
// NUR unter der Uhrzeit einen hoeheren Bereich loeschen - nicht die
// ganze Zeile, sonst wuerde das jede Sekunde in den Menu-Button
// hineinschneiden, der bis y=25 reicht.
// Auf 80px verbreitert (vorher 50) - das 12h-Format mit AM/PM (siehe
// unten) ist mit bis zu 7 Zeichen ("12:59PM") laenger als das feste
// 5-Zeichen-24h-Format ("23:12") und wurde sonst nicht vollstaendig
// geloescht (Ziffernreste blieben stehen). Der Bereich rechts daneben
// war hier ohnehin leer, daher unkritisch.
// The clock uses the globally set 11pt font (setFreeFont() in
// setup()) via setCursor()+print() - with GFXFF fonts this is
// baseline-anchored, so the text grows UPWARD (see
// CLAUDE.md note about this pitfall). The digit glyphs are
// 8px high according to font metrics, thus reaching up to y=HEADER_TITLE_H-6 - i.e.
// ABOVE the narrow STATUS_LINE_H area that was previously cleared alone.
// This left old digit remnants standing and
// overlapped with the new ones (most visible on the last
// minute digit, which changes most frequently). Therefore, selectively
// clear a taller area ONLY under the clock - not the
// whole line, otherwise it would cut into the Menu button
// every second, which extends to y=25.
// Widened to 80px (was 50) - the 12h format with AM/PM (see
// below) is up to 7 characters ("12:59PM") longer than the fixed
// 5-character 24h format ("23:12") and would otherwise not be completely
// cleared (digit remnants remained). The area to the right
// was already empty here, so uncritical.
constexpr int16_t CLOCK_CLEAR_W = 80;
constexpr int16_t CLOCK_CLEAR_TOP = HEADER_TITLE_H - 10;
tft.fillRect(0, CLOCK_CLEAR_TOP, CLOCK_CLEAR_W, CONTENT_TOP - CLOCK_CLEAR_TOP, TFT_BLACK);
@@ -382,9 +375,9 @@ void updateStatusLine() {
struct tm tmNow;
localtime_r(&now, &tmNow);
char timeBuf[9];
// 12h mit AM/PM bei Imperial (in den USA ueblich), 24h bei
// Metrisch - dieselbe Einheiten-Einstellung (Menue > Einheiten),
// die sonst Distanz/Hoehe steuert. Vorher immer fest 24h.
// 12h with AM/PM in Imperial (common in the US), 24h in
// Metric - same unit setting (Menu > Units)
// that otherwise controls distance/altitude. Previously always fixed 24h.
if (LocationManager::useMetricUnits()) {
snprintf(timeBuf, sizeof(timeBuf), "%02d:%02d", tmNow.tm_hour, tmNow.tm_min);
} else {
@@ -399,9 +392,9 @@ void updateStatusLine() {
updateWifiIcon();
// Icon nur bei tatsaechlicher Aenderung neu zeichnen (Weather::update()
// laeuft im Hintergrund auf Core 0 und aktualisiert typischerweise nur
// alle paar Minuten) - vermeidet unnoetiges Neuzeichnen jede Sekunde.
// Only redraw the icon when the condition actually changes (Weather::update()
// runs in the background on Core 0 and typically only updates
// every few minutes) - avoids unnecessary redrawing every second.
Weather::Condition weatherNow = Weather::current();
if (weatherNow != lastRenderedWeather) {
lastRenderedWeather = weatherNow;
@@ -409,14 +402,14 @@ void updateStatusLine() {
}
}
// Prueft, ob gerade Nacht ist (fuer die Nachtdimmung). Nutzt den echten
// Sonnenauf-/untergang am aktiven Standort (siehe sun_times.h), NICHT mehr
// ein festes 22:00-06:00-Fenster - im Sommer war das vorher oft noch hell
// draussen, wenn schon gedimmt wurde, und im Winter blieb es nach 6 Uhr noch
// lange dunkel, ohne dass gedimmt wurde. Solange Standort oder Uhrzeit noch
// nicht bekannt sind (z.B. kurz nach dem Start, bevor NTP/GPS/IP-Geolocation
// fertig sind), faellt die Funktion auf das alte feste Fenster zurueck,
// damit die Nachtdimmung nicht komplett ausfaellt.
// Checks whether it is currently night (for night dimming). Uses the real
// sunrise/sunset at the active location (see sun_times.h), NOT anymore
// a fixed 22:00-06:00 window - in summer it was often still light
// outside when dimming already kicked in, and in winter it stayed
// dark long after 6am without dimming being active. As long as location or time are
// not yet known (e.g. shortly after startup, before NTP/GPS/IP-geolocation
// are ready), the function falls back to the old fixed window,
// so that night dimming does not fail completely.
bool isNightDimHours() {
time_t now = time(nullptr);
if (now <= 8 * 3600 * 2) return false;
@@ -437,15 +430,15 @@ bool isNightDimHours() {
}
}
// Fallback: Standort noch unbekannt.
// Fallback: location not yet known.
int hour = tmNow.tm_hour;
return (hour >= 22 || hour < 6);
}
// Sanfte Nachtdimmung des Backlights zwischen Sonnenuntergang und
// Sonnenaufgang (siehe isNightDimHours()), sofern in den Einstellungen
// aktiviert. Der Inaktivitaets-Timeout (screenDimmed) hat Vorrang und wird
// hier nicht ueberschrieben.
// Gentle night dimming of the backlight between sunset and
// sunrise (see isNightDimHours()), if enabled in
// settings. The inactivity timeout (screenDimmed) takes priority and is
// not overridden here.
void updateNightDimming() {
if (screenDimmed) return;
@@ -538,10 +531,10 @@ void setup() {
SettingsStore::load();
tft.invertDisplay(SettingsStore::displayInverted());
// Der erste ledcWrite() oben (vor SettingsStore::load()) kannte die
// gespeicherte Helligkeit noch nicht und hat den Default (100%)
// angewendet - hier mit dem jetzt geladenen Wert korrigieren, gleiches
// Nachziehen wie bei invertDisplay() direkt drueber.
// The first ledcWrite() above (before SettingsStore::load()) did not yet know the
// saved brightness and used the default (100%)
// - correct it here with the now loaded value, same
// follow-up as with invertDisplay() directly above.
ledcWrite(BACKLIGHT_PWM_CHANNEL, normalBacklightPwm());
WifiMgr::init();
@@ -550,19 +543,18 @@ void setup() {
AircraftWatchlist::init();
SplashScreen::begin(tft);
// Sofort einen ersten Sternen-Frame zeichnen, statt erst auf die naechste
// Warteschleife (WLAN/Standort) zu warten - sonst blieb der Sternenhimmel
// bei schnellem Boot (gespeichertes WLAN, schnelle Standortermittlung)
// fast die ganze Splash-Anzeige ueber unsichtbar und "poppte" erst kurz
// vor dem Radarscreen auf.
// Draw a first star frame immediately, instead of waiting for the next
// wait loop (WiFi/location) - otherwise the starry sky remained
// almost invisible during the entire splash display on fast boot (saved WiFi, quick location lookup)
// and only "popped up" shortly before the radar screen.
MenuStars::update(tft);
SplashScreen::setStatusLine(tft, 0, I18n::t(StringId::SPLASH_SD_OK), TFT_WHITE);
// Willkommens-Screen laeuft nur beim allerersten Start (isFirstRun) und
// bewusst VOR der Touch-Kalibrierung - der erste Eindruck, bevor man
// ueberhaupt zum Kalibrieren aufgefordert wird. Englisch hart kodiert,
// da die Sprachauswahl (FirstRunLanguageScreen) erst danach kommt,
// siehe first_run_welcome_screen.cpp.
// Welcome screen only runs on the very first start (isFirstRun) and
// deliberately BEFORE touch calibration - first impression before
// even being prompted to calibrate. Hardcoded in English,
// since the language selection (FirstRunLanguageScreen) only comes after,
// see first_run_welcome_screen.cpp.
if (isFirstRun) {
// Creating the folder structure, seeding default data files, AND
// showing the loading indicator during those (noticeably slow) SD
@@ -599,14 +591,14 @@ void setup() {
if (isFirstRun) {
FirstRunLanguageScreen::run(tft);
// Erst NACH der Sprachauswahl (Texte erscheinen dann gleich in der
// richtigen Sprache) und NACH dem WLAN-Verbindungsversuch weiter
// oben (die Adresssuche braucht eine Internetverbindung) -
// ueberspringbar, siehe first_run_location_screen.cpp.
// Only AFTER language selection (texts then appear in the
// correct language immediately) and AFTER the WiFi connection attempt
// above (address search needs an internet connection) -
// skippable, see first_run_location_screen.cpp.
FirstRunLocationScreen::run(tft);
// Abschluss-Screen: bestaetigt das Ende der Ersteinrichtung, nennt
// den SD-Ordner mit allen gespeicherten Daten und weist auf den
// automatischen WLAN-Verbindungsaufbau ab dem naechsten Start hin.
// Completion screen: confirms the end of initial setup, names
// the SD folder with all saved data and points to the
// automatic WiFi connection from the next start onward.
FirstRunCompleteScreen::run(tft);
SplashScreen::begin(tft);
MenuStars::update(tft);
@@ -618,13 +610,13 @@ void setup() {
SplashScreen::setStatusLine(tft, 2, I18n::t(StringId::SPLASH_GETTING_LOCATION));
LocationManager::init();
uint32_t locStart = millis();
// Bewusst NICHT nur warten, solange noch GAR KEINE Position bekannt ist:
// ab dem zweiten Boot ist meistens schon eine gespeicherte Position da
// (Source::Persisted, siehe LocationManager::init()), wodurch diese
// Schleife sofort uebersprungen wurde und requestIpLookupIfNeeded() nie
// wieder lief. Genau die liefert aber auch die UTC-Zeitzonenverschiebung
// (inkl. Sommer-/Winterzeit) - ohne sie blieb die Uhr dauerhaft auf UTC
// stehen (in Mitteleuropa je nach Jahreszeit 1-2h falsch).
// Deliberately NOT only waiting while NO position is known at all:
// from the second boot onward there is usually already a saved position
// (Source::Persisted, see LocationManager::init()), which would cause this
// loop to be skipped immediately and requestIpLookupIfNeeded() to never
// run again. But requestIpLookupIfNeeded() also provides the UTC timezone offset
// (including DST) - without it the clock would permanently stay on UTC
// (in Central Europe 1-2h off depending on the season).
while ((LocationManager::currentSource() == LocationManager::Source::None ||
!LocationManager::hasUtcOffset()) &&
millis() - locStart < 8000) {
@@ -634,9 +626,9 @@ void setup() {
}
SplashScreen::setStatusLine(tft, 2, I18n::t(StringId::SPLASH_READY));
// Sobald die IP-Geolocation einen UTC-Offset geliefert hat, die
// Zeitzone entsprechend setzen - Zeitstempel (Flugbuch, Log-Dateinamen)
// zeigen dann die ECHTE Ortszeit statt UTC, automatisch weltweit richtig.
// Once IP geolocation has provided a UTC offset, set the
// timezone accordingly - timestamps (flight log, log file names)
// then show the REAL local time instead of UTC, automatically correct worldwide.
if (LocationManager::hasUtcOffset()) {
configTime(LocationManager::utcOffsetSeconds(), 0, "pool.ntp.org", "time.nist.gov");
}
@@ -697,14 +689,14 @@ if (menuBtn.contains(tap.x, tap.y)) {
}
}
// Waehrend der Ruhebildschirm aktiv ist (screensaverShowing), duerfen
// Radar-Rendering/Sweep/Statuszeile den Bildschirm NICHT mehr
// ueberschreiben - sonst wuerde der naechste Aircraft-Update-Zyklus
// (oder der Sweep-Tick) den gedimmten Sternenhimmel jederzeit wieder mit
// dem vollen Radarbild uebermalen, waehrend gleichzeitig
// drawScreensaverClock() im Sekundentakt seinen Streifen drueberzeichnet
// - das Ergebnis war sichtbares Geflacker zwischen Radarbild und Uhr
// statt eines ruhigen Sternenhimmels.
// While the screensaver is active (screensaverShowing), radar
// rendering/sweep/status line must NOT overwrite the display anymore
// - otherwise the next aircraft update cycle
// (or the sweep tick) would paint over the dimmed starry sky at any time with
// the full radar image, while at the same time
// drawScreensaverClock() draws its strip over it every second
// - the result was visible flickering between radar image and clock
// instead of a calm starry sky.
if (!screensaverShowing) {
if (forceRedraw || millis() - lastPollMs >= POLL_INTERVAL_MS) {
lastPollMs = millis();

View File

@@ -35,18 +35,17 @@ namespace {
}
};
// ROW_GAP/ROW_START_Y bleiben unveraendert (werden von flightRowRect()
// weiter unten mitbenutzt, siehe FLIGHT_ROW_H).
// ROW_GAP/ROW_START_Y remain unchanged (also used by flightRowRect()
// below, see FLIGHT_ROW_H).
constexpr int16_t ROW_GAP = 1;
constexpr int16_t ROW_START_Y = 18;
// Region-Unterseite (Sprache/Einheiten/Zurueck, nur 3 Eintraege):
// Zeilenhoehe/-abstand werden aus der tatsaechlich verfuegbaren
// Bildschirmflaeche errechnet (gleiches Muster wie bei SYSTEM_ROW_H
// weiter unten), statt die kleine, fuer volle Seiten (z.B.
// Flugoptionen: 14 Eintraege) gedachte feste Hoehe (vorher 22px) zu
// benutzen - die liess bei nur 3 Eintraegen fast den ganzen Bildschirm
// leer und machte die Buttons winzig und schwer zu treffen.
// Region sub-page (Language/Units/Back, only 3 entries): row
// height/spacing calculated from actual available screen area (same
// pattern as SYSTEM_ROW_H below), instead of the small fixed height
// (formerly 22px) intended for full pages (e.g. Flight Options: 14
// entries) - that left nearly the whole screen empty with only 3
// entries and made buttons tiny and hard to hit.
constexpr uint8_t REGION_ROW_COUNT = 3;
constexpr int16_t REGION_ROW_GAP = 10;
constexpr int16_t REGION_END_Y = Config::SCREEN_HEIGHT - 10;
@@ -67,33 +66,31 @@ namespace {
(int16_t)(Config::SCREEN_WIDTH - 20), CAT_ROW_H};
}
// Eigene, etwas kompaktere Zeilenhoehe NUR fuer die Flugoptionen-Seite:
// die normale rowRect()-Hoehe (22px) liess bei 13 Eintraegen schon keinen
// Platz mehr fuer einen weiteren - andere Seiten (Region/System) bleiben
// bei der normalen Hoehe unveraendert, um dort nichts zu verschieben.
// Custom, slightly more compact row height ONLY for the Flight Options
// page: the normal rowRect() height (22px) already left no room for
// another entry with 13 entries - other pages (Region/System) keep
// their normal height unchanged to avoid shifting anything there.
constexpr int16_t FLIGHT_ROW_H = 19;
Rect flightRowRect(uint8_t index) {
return {10, (int16_t)(ROW_START_Y + index * (FLIGHT_ROW_H + ROW_GAP)),
(int16_t)(Config::SCREEN_WIDTH - 20), FLIGHT_ROW_H};
}
// Eigene, groesser berechnete Zeilenhoehe NUR fuer die System-Seite: mit
// nur 10 Eintraegen (vs. z.B. 14 auf der Flugoptionen-Seite) liess die
// normale rowRect()-Hoehe (22px) unten viel ungenutzten Platz frei. Die
// Hoehe wird hier stattdessen aus der tatsaechlich verfuegbaren
// Bildschirmflaeche errechnet, sodass die Buttons den Platz bis knapp
// ueber den Bildschirmrand ausfuellen - andere Seiten bleiben bei ihren
// eigenen, unveraenderten Zeilenhoehen.
// War 10, bevor "Sicherung"/"Wiederherstellen" in die neue
// "Sicherung & Reset"-Unterseite ausgelagert wurden (siehe
// BACKUP_RESET_ROW_COUNT unten) und dort durch einen einzelnen
// Ordner-Button ersetzt wurden: -2 (Backup/Restore raus) +1 (neuer
// Ordner-Button) = 9. Danach +1 fuer den neuen "Nach Update
// suchen"-Punkt (OTA-Update, siehe ota_update.h) = 10. Der
// Ruhebildschirm-Umschalter, der zwischenzeitlich hier eine eigene
// Zeile hatte, ist in den neuen Bildschirm-Timeout-Screen umgezogen
// (siehe timeout_screen.cpp) - Zeilenzahl bleibt dadurch bei 10.
constexpr uint8_t SYSTEM_ROW_COUNT = 11; // +1 fuer Eco-Mode
// Custom, larger calculated row height ONLY for the System page: with
// only 10 entries (vs. e.g. 14 on the Flight Options page), the
// normal rowRect() height (22px) left a lot of unused space at the
// bottom. Height is instead calculated from actual available screen
// area so buttons fill the space down to just above the screen
// edge - other pages keep their own unchanged row heights.
// Was 10 before "Backup"/"Restore" were moved to the new
// "Backup & Reset" sub-page (see BACKUP_RESET_ROW_COUNT below) and
// replaced there by a single folder button: -2 (Backup/Restore
// removed) +1 (new folder button) = 9. Then +1 for the new
// "Check for update" item (OTA update, see ota_update.h) = 10. The
// screensaver toggle, which temporarily had its own row here, moved
// to the new screen timeout screen (see timeout_screen.cpp) - row
// count therefore stays at 10.
constexpr uint8_t SYSTEM_ROW_COUNT = 11; // +1 for Eco-Mode
constexpr int16_t SYSTEM_ROW_GAP = 4;
constexpr int16_t SYSTEM_START_Y = 18;
constexpr int16_t SYSTEM_END_Y = Config::SCREEN_HEIGHT - 10;
@@ -104,11 +101,11 @@ namespace {
(int16_t)(Config::SCREEN_WIDTH - 20), SYSTEM_ROW_H};
}
// Eigene Zeilenhoehe fuer die neue "Sicherung & Reset"-Unterseite (4
// Eintraege: Sichern/Wiederherstellen/Zuruecksetzen/Zurueck) - gleiches
// "aus verfuegbarem Platz errechnen"-Muster wie bei SYSTEM_ROW_H.
// ROW_START_Y wird mitbenutzt (siehe oben, gemeinsam mit rowRect()/
// flightRowRect()), nur GAP/COUNT/H sind eigene Werte.
// Custom row height for the new "Backup & Reset" sub-page (4
// entries: Backup/Restore/Factory Reset/Back) - same "calculate from
// available space" pattern as SYSTEM_ROW_H.
// ROW_START_Y is reused (see above, shared with rowRect()/
// flightRowRect()), only GAP/COUNT/H are custom values.
constexpr uint8_t BACKUP_RESET_ROW_COUNT = 4;
constexpr int16_t BACKUP_RESET_ROW_GAP = 10;
constexpr int16_t BACKUP_RESET_END_Y = Config::SCREEN_HEIGHT - 10;
@@ -134,14 +131,15 @@ namespace {
String onOff(bool on) { return I18n::t(on ? StringId::ON : StringId::OFF); }
// Fortschrittspunkte-Anzeige waehrend SettingsBackup::backup()/restore()
// laufen (siehe Aufrufe unten in Page::BackupReset) - diese sind
// synchrone, SD-lastige Vorgaenge, die spuerbar dauern koennen und den
// Button vorher wie eingefroren wirken liessen. SettingsBackup ruft den
// hier uebergebenen Funktionszeiger vor jedem der beiden Kopiervorgaenge
// (erst Einstellungen, dann WLAN) auf. Namespace-globale Zeiger/Variablen
// statt Lambda-Capture, da ein einfacher C-Funktionszeiger uebergeben
// werden muss (kein std::function im Projekt).
// Progress dots display while SettingsBackup::backup()/restore()
// run (see calls below in Page::BackupReset) - these are
// synchronous, SD-intensive operations that can take noticeable
// time and previously made the button appear frozen.
// SettingsBackup calls the function pointer passed here before each
// of the two copy operations (first settings, then WiFi).
// Namespace-global pointers/variables instead of lambda capture,
// because a plain C function pointer must be passed (no
// std::function in the project).
TFT_eSPI* progressTft = nullptr;
Rect progressBtnRect;
String progressLabel;
@@ -174,11 +172,11 @@ namespace {
delay(1200);
}
// Wie die einfache layoutWrapped()-Variante, aber mit optionalem
// Scroll-Offset und Sichtfenster (scrollY/viewTop/viewBottom) - Zeilen
// ausserhalb des Sichtfensters werden uebersprungen. Mit draw=false wird
// nur die Gesamthoehe berechnet, ohne etwas zu zeichnen (fuer die
// Scroll-Bedarfspruefung vorab).
// Like the simple layoutWrapped() variant, but with optional scroll
// offset and viewport (scrollY/viewTop/viewBottom) - lines outside
// the viewport are skipped. With draw=false, only the total height
// is calculated without drawing anything (for pre-checking scroll
// need).
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) {
@@ -209,10 +207,10 @@ namespace {
return y;
}
// Zerlegt einen Titel in bis zu maxLines Zeilen nach dem gleichen
// Wortumbruch-Prinzip wie layoutWrapped() - ein Titel wird aber nie
// gescrollt, sondern muss immer komplett sichtbar sein, daher eine
// eigene, einfachere Variante ohne Scroll-Unterstuetzung.
// Splits a title into up to maxLines lines using the same word-wrapping
// principle as layoutWrapped() - but a title is never scrolled and must
// always be fully visible, hence a separate, simpler variant without
// scroll support.
int wrapTitleLines(TFT_eSPI& tft, const String& text, int16_t maxWidth, String* outLines, int maxLines) {
int count = 0;
int32_t start = 0;
@@ -240,29 +238,27 @@ namespace {
return count;
}
// Warn-Ueberlage, die praktisch den kompletten Bildschirm einnimmt (nur
// ein paar Pixel Rand) - urspruenglich nur fuers Einschalten des
// Flugbuchs gebaut (erklaert, warum es sich nach 24h automatisch
// wieder abschaltet), jetzt generisch mit uebergebenen Titel-/Text-
// StringIds, damit sie auch fuer die "Einstellungen zuruecksetzen"-
// Bestaetigung (Werksreset) wiederverwendet werden kann - beides sind
// seltene, potenziell folgenreiche Aktionen, die dieselbe deutliche
// Warnung verdienen. "Achtung!!!" (titleId) steht ganz oben, mit einer
// Leerzeile Abstand zum Fliesstext (bodyId) darunter; der Text
// scrollt bei Bedarf (laengere Uebersetzungen) ueber eigene Pfeil-
// Buttons, OK/Zurueck bleiben dabei immer unten fix und
// kollisionsfrei sichtbar. Sternchen laufen im Hintergrund mit, wie auf
// allen anderen Menue-Screens (nur der Radar-Screen selbst spart sich
// das wegen der CPU-Last durch Abfragen/Zeichnen). Gibt true zurueck,
// wenn "OK" angetippt wurde, false bei "Zurueck".
// Warning overlay that covers nearly the entire screen (just a few
// pixels margin) - originally built only for enabling the flight
// logbook (explains why it auto-disables after 24h), now generic
// with passed-in title/text to also be reusable for the "Reset
// settings" confirmation (factory reset) - both are rare, potentially
// consequential actions that deserve the same clear warning.
// "Attention!!!" (titleId) appears at the top, with a blank line
// before the body text (bodyId) below; the text scrolls if needed
// (longer translations) via dedicated arrow buttons, OK/Back always
// stay fixed at the bottom and collision-free visible. Stars run in
// the background, as on all other menu screens (only the radar
// screen skips them due to CPU load from polling/drawing). Returns
// true if "OK" was tapped, false for "Back".
//
// title/body stehen VOR dem Aufruf per I18n::t() fest (statt StringIds
// entgegenzunehmen), damit auch dynamisch zusammengesetzte Texte (z.B.
// mit eingefuegter Versionsnummer bei OTA) moeglich sind. accentColor
// (Default TFT_RED fuer die beiden bestehenden, wirklich destruktiven
// Aufrufer) faerbt Rahmen und Titel - der OTA-Aufrufer uebergibt
// TFT_GREEN, da ein Update zwar bestaetigt werden sollte, aber keine
// "gefaehrliche" Loesch-Aktion wie Werksreset/Flugbuch ist.
// title/body are fixed BEFORE the call via I18n::t() (instead of
// accepting StringIds), so dynamically composed texts (e.g. with
// inserted version number for OTA) are possible. accentColor
// (default TFT_RED for the two existing, truly destructive callers)
// colors frame and title - the OTA caller passes TFT_GREEN, since an
// update should be confirmed but is not a "dangerous" deletion
// action like factory reset/flight logbook.
bool confirmWarningScreen(TFT_eSPI& tft, const String& title, const String& body, uint16_t accentColor = TFT_RED) {
constexpr int16_t BOX_X = 4;
constexpr int16_t BOX_Y = 4;
@@ -272,11 +268,11 @@ namespace {
constexpr int16_t LINE_H = 16;
constexpr int16_t TITLE_Y = BOX_Y + 16;
// Titel-Text vorab in so viele Zeilen umbrechen, wie bei Größe 2
// (oder bei zu langem Text Größe 1) nötig sind - siehe
// wrapTitleLines() oben. VIEW_TOP hängt dadurch von der
// tatsächlichen Zeilenzahl des Titels ab, ist also kein constexpr
// mehr wie vorher (wo immer nur eine Titelzeile angenommen wurde).
// Wrap title text into as many lines as needed at size 2 (or size 1
// if text is too long) - see wrapTitleLines() above. VIEW_TOP
// therefore depends on the actual number of title lines, so it is
// no longer constexpr as before (where only one title line was
// assumed).
tft.setTextSize(2);
uint8_t titleTextSize = 2;
if (tft.textWidth(title) > TEXT_MAX_WIDTH) {
@@ -286,13 +282,13 @@ namespace {
constexpr int MAX_TITLE_LINES = 3;
String titleLines[MAX_TITLE_LINES];
int titleLineCount = wrapTitleLines(tft, title, TEXT_MAX_WIDTH, titleLines, MAX_TITLE_LINES);
// Fließtext wird immer bei Größe 1 vermessen/gezeichnet (siehe
// layoutWrapped()-Aufrufe unten) - Größe hier zurücksetzen, falls
// obiger Titel-Breitentest sie auf 2 stehen gelassen hat, sonst
// würde die gleich folgende totalH-Berechnung (vor dem ersten
// redraw()) mit falscher (zu breiter) Schriftgröße rechnen.
// Body text is always measured/drawn at size 1 (see
// layoutWrapped() calls below) - reset size here in case the
// title width check above left it at 2, otherwise the
// immediately following totalH calculation (before the first
// redraw()) would use the wrong (too wide) font size.
tft.setTextSize(1);
// Eine Leerzeile Abstand zwischen Titel und Fließtext.
// One blank line between title and body text.
int16_t VIEW_TOP = (int16_t)(TITLE_Y + titleLineCount * LINE_H + 12);
constexpr int16_t BTN_H = 36;
@@ -303,9 +299,9 @@ namespace {
constexpr int16_t SCROLL_ROW_H = 28;
constexpr int16_t SCROLL_ROW_GAP = 8;
// Ohne Scroll-Pfeile verfuegbare Texthoehe zuerst pruefen - nur wenn
// der Text da nicht reinpasst, wird zusaetzlich Platz fuer die
// Pfeile reserviert (mehr Text-Platz bei kurzen Uebersetzungen).
// First check available text height without scroll arrows - only if
// the text does not fit, additional space for arrows is reserved
// (more text space for short translations).
constexpr int16_t VIEW_BOTTOM_NO_SCROLL = OK_Y - 8;
constexpr int16_t VIEW_BOTTOM_SCROLL = VIEW_BOTTOM_NO_SCROLL - SCROLL_ROW_H - SCROLL_ROW_GAP;
@@ -332,13 +328,12 @@ namespace {
tft.setTextDatum(MC_DATUM);
tft.setTextColor(accentColor, TFT_BLACK);
// Statische Titel ("Achtung!!!") sind kurz genug fuer Groesse 2,
// aber der OTA-Aufrufer baut den Titel dynamisch mit
// Versionsnummer zusammen (z.B. "Update verfuegbar: v3.5.0") -
// das passt bei Groesse 2 nicht mehr in die Box und lief vorher
// links/rechts ueber den Bildschirmrand hinaus. Deshalb hier
// die Breite bei Groesse 2 pruefen und bei Bedarf auf Groesse 1
// zurueckfallen, statt eine feste Groesse anzunehmen.
// Static titles ("Attention!!!") are short enough for size 2, but
// the OTA caller builds the title dynamically with a version
// number (e.g. "Update available: v3.5.0") - this no longer
// fits in the box at size 2 and previously overflowed left/right
// beyond the screen edge. Hence check width at size 2 and fall
// back to size 1 if needed, instead of assuming a fixed size.
tft.setTextSize(titleTextSize);
for (int i = 0; i < titleLineCount; i++) {
tft.drawString(titleLines[i], BOX_X + BOX_W / 2, (int16_t)(TITLE_Y + i * LINE_H));
@@ -379,16 +374,15 @@ namespace {
}
}
// Einfacher Info-Screen mit nur EINEM Button (kein Abbrechen) - fuer
// Endzustaende, bei denen es nichts mehr zu entscheiden gibt, nur zu
// bestaetigen (z.B. Ergebnis eines OTA-Updates). Anders als
// showBriefMessage() (kurze Meldung unten am Bildschirmrand,
// verschwindet nach 1,2s automatisch von selbst) bleibt dieser Screen
// stehen, bis aktiv bestaetigt wird - wichtig bei sicherheitsrelevanten
// Meldungen wie einem fehlgeschlagenen oder erfolgreichen Firmware-
// Update, die der Nutzer auf keinen Fall verpassen darf. Gleicher
// Kasten-/Scroll-Aufbau wie confirmWarningScreen(), nur mit einem
// einzigen, ueber die volle Breite gehenden Button statt OK/Zurueck.
// Simple info screen with only ONE button (no cancel) - for
// terminal states where there is nothing left to decide, only to
// acknowledge (e.g. result of an OTA update). Unlike
// showBriefMessage() (short message at the bottom of the screen,
// auto-disappears after 1.2s), this screen stays until actively
// acknowledged - important for safety-relevant messages like a
// failed or successful firmware update that the user must not miss.
// Same box/scroll layout as confirmWarningScreen(), but with a
// single full-width button instead of OK/Back.
void infoScreen(TFT_eSPI& tft, const String& title, const String& body, uint16_t accentColor, const String& buttonLabel) {
constexpr int16_t BOX_X = 4;
constexpr int16_t BOX_Y = 4;
@@ -398,11 +392,11 @@ namespace {
constexpr int16_t LINE_H = 16;
constexpr int16_t TITLE_Y = BOX_Y + 16;
// Titel-Text vorab in so viele Zeilen umbrechen, wie bei Größe 2
// (oder bei zu langem Text Größe 1) nötig sind - siehe
// wrapTitleLines() oben. VIEW_TOP hängt dadurch von der
// tatsächlichen Zeilenzahl des Titels ab, ist also kein constexpr
// mehr wie vorher (wo immer nur eine Titelzeile angenommen wurde).
// Wrap title text into as many lines as needed at size 2 (or size 1
// if text is too long) - see wrapTitleLines() above. VIEW_TOP
// therefore depends on the actual number of title lines, so it is
// no longer constexpr as before (where only one title line was
// assumed).
tft.setTextSize(2);
uint8_t titleTextSize = 2;
if (tft.textWidth(title) > TEXT_MAX_WIDTH) {
@@ -412,13 +406,13 @@ namespace {
constexpr int MAX_TITLE_LINES = 3;
String titleLines[MAX_TITLE_LINES];
int titleLineCount = wrapTitleLines(tft, title, TEXT_MAX_WIDTH, titleLines, MAX_TITLE_LINES);
// Fließtext wird immer bei Größe 1 vermessen/gezeichnet (siehe
// layoutWrapped()-Aufrufe unten) - Größe hier zurücksetzen, falls
// obiger Titel-Breitentest sie auf 2 stehen gelassen hat, sonst
// würde die gleich folgende totalH-Berechnung (vor dem ersten
// redraw()) mit falscher (zu breiter) Schriftgröße rechnen.
// Body text is always measured/drawn at size 1 (see
// layoutWrapped() calls below) - reset size here in case the
// title width check above left it at 2, otherwise the
// immediately following totalH calculation (before the first
// redraw()) would use the wrong (too wide) font size.
tft.setTextSize(1);
// Eine Leerzeile Abstand zwischen Titel und Fließtext.
// One blank line between title and body text.
int16_t VIEW_TOP = (int16_t)(TITLE_Y + titleLineCount * LINE_H + 12);
constexpr int16_t BTN_H = 40;
@@ -490,22 +484,21 @@ namespace {
}
}
// Fortschrittsanzeige waehrend OtaUpdate::performUpdate() laeuft -
// gleiches Namespace-globale-Zeiger-Prinzip wie progressTft oben (siehe
// Settings-Backup-Fortschrittspunkte), da OtaUpdate::performUpdate()
// ebenfalls einen einfachen C-Funktionszeiger erwartet, keine
// Lambda-Capture erlaubt.
// Progress display while OtaUpdate::performUpdate() runs - same
// namespace-global pointer principle as progressTft above (see
// Settings-Backup progress dots), since OtaUpdate::performUpdate()
// also expects a plain C function pointer, no lambda capture
// allowed.
TFT_eSPI* otaProgressTft = nullptr;
void drawOtaProgress(uint8_t percent) {
if (!otaProgressTft) return;
TFT_eSPI& t = *otaProgressTft;
// Zwei Zeilen statt einer langen: der Praefix-Text
// (OTA_INSTALLING_PREFIX) ist in manchen Sprachen zu lang, um
// zusammen mit der Prozentzahl auf einer Zeile bei lesbarer
// Schriftgroesse zu passen (lief vorher links/rechts ueber den
// Bildschirmrand hinaus). Jetzt: Beschriftung klein oben, Prozent
// gross darunter.
// Two lines instead of one long one: the prefix text
// (OTA_INSTALLING_PREFIX) is too long in some languages to fit
// together with the percentage on one line at a readable font size
// (previously overflowed left/right beyond the screen edge).
// Now: label small at top, percentage large below.
constexpr int16_t BAND_H = 60;
int16_t cy = Config::SCREEN_HEIGHT / 2;
t.fillRect(0, (int16_t)(cy - BAND_H / 2), Config::SCREEN_WIDTH, BAND_H, TFT_BLACK);
@@ -523,18 +516,17 @@ namespace {
t.setTextDatum(TL_DATUM);
}
// Kompletter Ablauf fuer "Nach Update suchen" (System-Menue) - Pruefung
// gegen GitHub-Releases, bei verfuegbarem Update explizite Bestaetigung
// (confirmWarningScreen() mit gruenem statt rotem Akzent - ein Update
// ist keine destruktive Aktion wie Werksreset, verdient aber trotzdem
// eine bewusste Bestaetigung, da WLAN/Strom waehrend des Vorgangs nicht
// unterbrochen werden sollten), danach Fortschrittsanzeige waehrend
// Download+Flash. WICHTIG: startet NICHT mehr automatisch neu und
// springt bei einem Fehler auch nicht einfach stillschweigend zurueck
// ins Menue - jedes Ergebnis (Erfolg wie Fehler) wird ueber infoScreen()
// als eigener, stehenbleibender Screen angezeigt, den der Nutzer aktiv
// bestaetigen muss. Bei Erfolg startet erst ein expliziter Tap auf
// "Jetzt neu starten" tatsaechlich neu.
// Complete flow for "Check for update" (System menu) - checks
// against GitHub releases, explicit confirmation when update is
// available (confirmWarningScreen() with green instead of red
// accent - an update is not a destructive action like factory reset,
// but still deserves conscious confirmation since WiFi/power should
// not be interrupted during the process), then progress display
// during download+flash. IMPORTANT: no longer auto-restarts and no
// longer silently jumps back to the menu on error - every result
// (success as well as failure) is shown via infoScreen() as its own
// persistent screen that the user must actively acknowledge. On
// success, only an explicit tap on "Restart now" actually restarts.
void runOtaUpdateScreen(TFT_eSPI& tft) {
MenuStars::reset();
tft.fillScreen(TFT_BLACK);
@@ -570,18 +562,18 @@ namespace {
otaProgressTft = nullptr;
if (ok) {
// Bewusst KEIN automatischer Neustart mehr - der Nutzer
// bestaetigt aktiv per Button, damit er den Erfolg auch wirklich
// mitbekommt (vorher lief die Meldung nur 1,5s an, dann
// Neustart - leicht zu verpassen).
// Deliberately NO automatic restart anymore - the user
// actively confirms via button so they actually notice the
// success (previously the message showed for only 1.5s,
// then restart - easy to miss).
infoScreen(tft, I18n::t(StringId::OTA_UPDATE_SUCCESS), I18n::t(StringId::OTA_SUCCESS_BODY),
TFT_GREEN, I18n::t(StringId::OTA_RESTART_BUTTON));
ESP.restart();
} else {
// Bewusst ein stehenbleibender Info-Screen statt der alten
// showBriefMessage() (1,2s, dann automatisch zurueck ins Menue)
// - ein fehlgeschlagenes Firmware-Update ist keine
// Nebensaechlichkeit, die man verpassen darf.
// Deliberately a persistent info screen instead of the old
// showBriefMessage() (1.2s, then automatically back to menu)
// - a failed firmware update is not a minor detail that one
// can afford to miss.
infoScreen(tft, I18n::t(StringId::OTA_UPDATE_FAILED), I18n::t(StringId::OTA_FAILED_BODY),
TFT_RED, I18n::t(StringId::OK));
}
@@ -673,12 +665,12 @@ void run(TFT_eSPI& tft) {
Rect timeoutBtn = systemRowRect(3);
Rect nightDimBtn = systemRowRect(4);
Rect webuiBtn = systemRowRect(6);
// "Sicherung & Reset" (Backup/Restore/Werksreset, siehe
// Page::BackupReset unten) und "Nach Update suchen" stehen
// bewusst direkt ueber "Info", das seinerseits als letzter
// Punkt direkt ueber dem Zurueck-Button steht - so bleiben
// beide Positionen stabil, egal wie viele weitere Punkte davor
// noch dazukommen.
// "Backup & Reset" (Backup/Restore/Factory Reset, see
// Page::BackupReset below) and "Check for update" are
// deliberately placed directly above "About", which in turn
// is the last item directly above the Back button - this
// keeps both positions stable regardless of how many further
// items come before them.
Rect ecoModeBtn = systemRowRect(5);
Rect backupResetBtn = systemRowRect(7);
Rect checkUpdateBtn = systemRowRect(8);
@@ -717,12 +709,12 @@ void run(TFT_eSPI& tft) {
} else if (brightnessBtn.contains(tap.x, tap.y)) {
BrightnessScreen::run(tft);
} else if (timeoutBtn.contains(tap.x, tap.y)) {
// Vorher: Durchklicken per wiederholtem Antippen (0-10, ein
// Tipp pro Minute - bei z.B. 10 Minuten also zehn Tipps).
// Jetzt: eigener Screen mit Schieberegler, siehe
// timeout_screen.cpp - dort lebt jetzt auch der
// Ruhebildschirm-Umschalter (inhaltlich eng verwandt, und
// dort ist Platz fuer eine kurze Erklaerung).
// Previously: cycling through values by repeated tapping
// (0-10, one tap per minute - e.g. 10 taps for 10 minutes).
// Now: dedicated screen with a slider, see
// timeout_screen.cpp - the screensaver toggle now also
// lives there (closely related in content, and there is
// room for a brief explanation).
TimeoutScreen::run(tft);
} else if (nightDimBtn.contains(tap.x, tap.y)) {
SettingsStore::setNightDimmingEnabled(!SettingsStore::nightDimmingEnabled());
@@ -752,9 +744,9 @@ void run(TFT_eSPI& tft) {
drawButton(tft, backupBtn, I18n::t(StringId::MENU_BACKUP));
drawButton(tft, restoreBtn, I18n::t(StringId::MENU_RESTORE));
// Danger-Akzent (rot) - deutlich von Sichern/Wiederherstellen
// abgesetzt, da diese Aktion (nach Bestaetigung) ALLE Daten
// unwiderruflich loescht, siehe confirmWarningScreen() unten.
// Danger accent (red) - clearly distinguished from Backup/Restore
// since this action (after confirmation) irreversibly DELETES
// ALL data, see confirmWarningScreen() below.
drawButton(tft, resetBtn, I18n::t(StringId::MENU_FACTORY_RESET), false, true);
drawButton(tft, backBtn, I18n::t(StringId::BACK_ARROW));
@@ -794,10 +786,9 @@ void run(TFT_eSPI& tft) {
tft.drawString(I18n::t(StringId::MENU_FACTORY_RESET_DELETING),
Config::SCREEN_WIDTH / 2, Config::SCREEN_HEIGHT / 2);
tft.setTextDatum(TL_DATUM);
// Erfolgsfall: factoryReset() startet das Geraet neu und
// kehrt nie zurueck - dieser Code danach laeuft nur im
// (seltenen) Fehlerfall (SD nicht eingehaengt) ueberhaupt
// weiter.
// On success: factoryReset() restarts the device and never
// returns - this code after it only continues in the
// (rare) error case (SD not mounted).
bool ok = SettingsBackup::factoryReset();
if (!ok) {
showBriefMessage(tft, I18n::t(StringId::MENU_FACTORY_RESET_FAILED), TFT_RED);
@@ -812,13 +803,13 @@ void run(TFT_eSPI& tft) {
tft.setCursor(10, 14);
tft.println(I18n::t(StringId::MENU_CATEGORY_FLIGHT));
// Reine An/Aus-Schalter (Heartbeat, Notfall-Alarm, Naeherungs-LED,
// Flugbuch, Beobachtungsalarm, Bodenfahrzeuge-Filter) stehen
// bewusst gemeinsam am Ende der Liste, direkt ueber dem
// Zurueck-Button - zuerst die LED-gesteuerten Alarme (Heartbeat,
// Notfall, Naeherung; siehe LedAlert::Mode) zusammenhaengend,
// danach die beiden nicht-LED-basierten Schalter Flugbuch und
// Beobachtungsalarm sowie zuletzt der Bodenfahrzeuge-Filter.
// Pure on/off toggles (Heartbeat, Emergency Alert, Proximity LED,
// Flight Logbook, Watchlist Alert, Ground Vehicles Filter) are
// deliberately grouped together at the end of the list,
// directly above the Back button - first the LED-driven alerts
// (Heartbeat, Emergency, Proximity; see LedAlert::Mode)
// together, then the two non-LED-based toggles Flight Logbook
// and Watchlist Alert, and finally the Ground Vehicles Filter.
Rect aircraftListBtn = flightRowRect(0);
Rect statsBtn = flightRowRect(1);
Rect statsHistoryBtn = flightRowRect(2);
@@ -860,9 +851,9 @@ drawButton(tft, watchlistBtn, I18n::t(StringId::MENU_WATCHLIST));
if (aircraftListBtn.contains(tap.x, tap.y)) {
if (AircraftListScreen::run(tft)) {
// Ein Flugzeug wurde in der Liste ausgewaehlt - direkt bis
// zum Radar zurueckspringen (mit offenem Detail-Panel),
// statt in der Flugoptionen-Seite stehen zu bleiben.
// An aircraft was selected in the list - jump directly back
// to the radar (with detail panel open), instead of
// staying on the Flight Options page.
done = true;
}
} else if (statsBtn.contains(tap.x, tap.y)) {
@@ -887,7 +878,7 @@ drawButton(tft, watchlistBtn, I18n::t(StringId::MENU_WATCHLIST));
SettingsStore::setProximityAlertEnabled(!SettingsStore::proximityAlertEnabled());
} else if (logbookBtn.contains(tap.x, tap.y)) {
if (SettingsStore::flightLogbookEnabled()) {
// Ausschalten ist immer unbedenklich - keine Bestaetigung noetig.
// Turning off is always harmless - no confirmation needed.
SettingsStore::setFlightLogbookEnabled(false);
SettingsStore::setFlightLogbookEnabledAtEpoch(0);
SettingsStore::setFlightLogbookSessionFile("");
@@ -895,9 +886,9 @@ drawButton(tft, watchlistBtn, I18n::t(StringId::MENU_WATCHLIST));
I18n::t(StringId::MENU_LOGBOOK_WARNING_BODY))) {
SettingsStore::setFlightLogbookEnabled(true);
SettingsStore::setFlightLogbookEnabledAtEpoch((uint32_t)time(nullptr));
// Leerer Eintrag erzwingt eine frische Sitzungsdatei beim
// naechsten FlightLogbook::update() statt eine evtl. noch
// vorhandene alte Datei weiterzuschreiben.
// Empty entry forces a fresh session file on the next
// FlightLogbook::update() instead of continuing to
// write to a possibly existing old file.
SettingsStore::setFlightLogbookSessionFile("");
}
} else if (watchlistAlertBtn.contains(tap.x, tap.y)) {

View File

@@ -3,8 +3,8 @@
#include <TFT_eSPI.h>
namespace MenuScreen {
// Blockierend: einfaches Menue (Kalibrierung, Anzeige-Invertierung,
// WLAN-Verwaltung, Alarm-Toggles, Statistik, Logbuch-Dateien).
// Kehrt zurueck, sobald "Zurueck" angetippt wird.
// Blocking: simple menu (calibration, display inversion,
// WiFi management, alert toggles, statistics, logbook files).
// Returns as soon as "Back" is tapped.
void run(TFT_eSPI& tft);
}

View File

@@ -1,17 +1,17 @@
#pragma once
#include <TFT_eSPI.h>
// Gemeinsames Sterne-Twinkle-Modul fuer ALLE Menue-/Untermenue-Bildschirme
// (schwarzer Hintergrund) sowie den Splash-Screen - dieselbe Optik wie im
// Flugzeug-Detail-Panel des Radars.
// Common star twinkle module for ALL menu/submenu screens
// (black background) as well as the splash screen - same look as in the
// aircraft detail panel of the radar.
namespace MenuStars {
// Verteilt die Sterne neu ueber den ganzen Bildschirm - einmal beim
// Betreten eines neuen Screens aufrufen (NICHT bei jedem Redraw
// innerhalb desselben Screens, sonst "springt" die Animation staendig).
// Redistributes the stars across the entire screen - call once when
// entering a new screen (NOT on every redraw within the same
// screen, otherwise the animation keeps "jumping").
void reset();
// In der Warte-/Idle-Schleife eines Screens aufrufen (bei jedem
// Schleifendurchlauf ist ok - die Funktion drosselt sich intern selbst
// auf ca. alle 60ms, um das Display nicht unnoetig oft anzusprechen).
// Call in the wait/idle loop of a screen (every
// loop iteration is fine - the function internally throttles itself
// to roughly every 60ms to avoid needlessly addressing the display).
void update(TFT_eSPI& tft);
}

View File

@@ -66,10 +66,9 @@ namespace {
FlightLogbook::update();
// Kurzer gruener LED-Blitz als "Herzschlag" - zeigt,
// dass gerade eine Abfrage gelaufen ist. Wird von
// LedAlert::update() automatisch ignoriert, solange
// ein Naeherungs-/Notfall-Alarm aktiv ist.
// Short green LED flash as "heartbeat" - indicates that a fetch
// just ran. Automatically ignored by LedAlert::update() as long as
// a proximity/emergency alert is active.
if (SettingsStore::ledHeartbeatEnabled()) {
LedAlert::pulseHeartbeat(millis());
}

View File

@@ -1,17 +1,17 @@
#pragma once
#include <Arduino.h>
// Hintergrund-Task fuer WLAN-Status, Standortbestimmung und ADS-B-Abfragen.
// Laeuft auf Core 0, damit Core 1 (Rendering + Touch im main-Loop) nie durch
// Netzwerk-Wartezeiten (WLAN, HTTPS) blockiert wird.
// Background task for WiFi status, geolocation and ADS-B queries.
// Runs on Core 0 so that Core 1 (rendering + touch in main loop) is never
// blocked by network wait times (WiFi, HTTPS).
namespace NetTask {
void begin();
void pause();
void resume();
// Eco-Mode: aendert das ADS-B-Abfrage-Intervall zur Laufzeit.
// Wird von main.cpp beim Bildschirm-Timeout (screenDimmed) auf
// Config::ECO_FETCH_INTERVAL_MS und beim Aufwachen wieder auf
// Config::FETCH_INTERVAL_MS gesetzt.
// Eco mode: changes the ADS-B fetch interval at runtime.
// Set by main.cpp on screen timeout (screenDimmed) to
// Config::ECO_FETCH_INTERVAL_MS and on wakeup back to
// Config::FETCH_INTERVAL_MS.
void setFetchInterval(uint32_t ms);
}

View File

@@ -14,9 +14,9 @@ namespace OtaUpdate {
namespace {
constexpr const char* RELEASES_API_URL =
"https://api.github.com/repos/Eiswolf-BG/eiswolfs-flightradar-CYD/releases/latest";
// GitHub verlangt bei API-Anfragen einen aussagekraeftigen User-Agent
// (sonst HTTP 403) - gleiches Prinzip wie Config::NOMINATIM_USER_AGENT
// fuer die Adresssuche.
// GitHub requires a meaningful User-Agent for API requests
// (otherwise HTTP 403) - same principle as Config::NOMINATIM_USER_AGENT
// for address search.
constexpr const char* USER_AGENT =
"EiswolfsFlightradarCYD-OTA/1.0 (+https://github.com/Eiswolf-BG/eiswolfs-flightradar-CYD)";
@@ -27,10 +27,10 @@ namespace {
return sscanf(s, "%d.%d.%d", &major, &minor, &patch) == 3;
}
// > 0 wenn a neuer als b, 0 wenn gleich, < 0 wenn a aelter als b.
// Bewusst eine echte numerische Versionsvergleich statt eines simplen
// String-Vergleichs (der wuerde z.B. "3.10.0" faelschlich als "kleiner"
// als "3.9.0" einordnen).
// > 0 if a is newer than b, 0 if equal, < 0 if a is older than b.
// Deliberately a real numeric version comparison instead of a simple
// string comparison (which would e.g. wrongly rank "3.10.0" as "less"
// than "3.9.0").
int compareVersions(const char* a, const char* b) {
int aMaj, aMin, aPat, bMaj, bMin, bPat;
if (!parseVersion(a, aMaj, aMin, aPat) || !parseVersion(b, bMaj, bMin, bPat)) return 0;
@@ -43,7 +43,7 @@ namespace {
CheckInfo checkForUpdate() {
CheckInfo info;
Serial.printf("[OTA] Pruefe auf Update: url=%s freeHeap=%u RSSI=%ddBm\n", RELEASES_API_URL,
Serial.printf("[OTA] Checking for update: url=%s freeHeap=%u RSSI=%ddBm\n", RELEASES_API_URL,
(unsigned)ESP.getFreeHeap(), WiFi.RSSI());
WiFiClientSecure client;
@@ -53,7 +53,7 @@ CheckInfo checkForUpdate() {
HTTPClient http;
http.setTimeout(8000);
if (!http.begin(client, RELEASES_API_URL)) {
Serial.println("[OTA] Pruefung fehlgeschlagen: http.begin() lieferte false.");
Serial.println("[OTA] Check failed: http.begin() returned false.");
return info;
}
http.addHeader("User-Agent", USER_AGENT);
@@ -61,10 +61,10 @@ CheckInfo checkForUpdate() {
int code = http.GET();
if (code != HTTP_CODE_OK) {
if (code < 0) {
Serial.printf("[OTA] Pruefung fehlgeschlagen: HTTP-Fehler=%d (%s)\n", code,
Serial.printf("[OTA] Check failed: HTTP error=%d (%s)\n", code,
HTTPClient::errorToString(code).c_str());
} else {
Serial.printf("[OTA] Pruefung fehlgeschlagen: HTTP-Status=%d\n", code);
Serial.printf("[OTA] Check failed: HTTP status=%d\n", code);
}
http.end();
return info;
@@ -75,13 +75,13 @@ CheckInfo checkForUpdate() {
JsonDocument doc;
DeserializationError jsonErr = deserializeJson(doc, body);
if (jsonErr) {
Serial.printf("[OTA] Pruefung fehlgeschlagen: JSON-Fehler (%s)\n", jsonErr.c_str());
Serial.printf("[OTA] Check failed: JSON error (%s)\n", jsonErr.c_str());
return info;
}
const char* tag = doc["tag_name"] | "";
if (!tag[0]) {
Serial.println("[OTA] Pruefung fehlgeschlagen: kein tag_name im Release-JSON.");
Serial.println("[OTA] Check failed: no tag_name in release JSON.");
return info;
}
@@ -99,59 +99,58 @@ CheckInfo checkForUpdate() {
}
if (!info.downloadUrl[0]) {
Serial.printf("[OTA] Pruefung fehlgeschlagen: Release v%s hat keinen firmware.bin-Anhang.\n",
Serial.printf("[OTA] Check failed: Release v%s has no firmware.bin attachment.\n",
info.latestVersion);
return info; // Release ohne firmware.bin-Anhang
return info; // Release without firmware.bin attachment
}
int cmp = compareVersions(info.latestVersion, Config::APP_VERSION);
info.result = (cmp > 0) ? CheckResult::UpdateAvailable : CheckResult::UpToDate;
Serial.printf("[OTA] Pruefung erfolgreich: installiert=v%s neuestes=v%s -> %s\n", Config::APP_VERSION,
Serial.printf("[OTA] Check successful: installed=v%s latest=v%s -> %s\n", Config::APP_VERSION,
info.latestVersion,
info.result == CheckResult::UpdateAvailable ? "Update verfuegbar" : "bereits aktuell");
info.result == CheckResult::UpdateAvailable ? "Update available" : "already up-to-date");
return info;
}
bool performUpdate(const char* url, void (*onProgress)(uint8_t percent)) {
// Diagnose-Logging (nur ueber USB-Seriell sichtbar, kein Einfluss auf
// die UI) - vorher wurde bei einem Fehlschlag nur ein simples "true/
// false" nach aussen gegeben, ohne den eigentlichen Grund (Timeout,
// TLS-Fehler, HTTP-Statuscode...) festzuhalten. Damit laesst sich ein
// fehlgeschlagener OTA-Versuch am Seriell-Monitor nachvollziehen, statt
// erneut raten zu muessen.
Serial.printf("[OTA] Start: url=%s freeHeap=%u RSSI=%ddBm\n", url,
// Diagnostic logging (only visible over USB serial, no impact on
// the UI) - previously a failed update only returned a simple "true/
// false" externally, without recording the actual reason (timeout,
// TLS error, HTTP status code...). This allows tracing a failed
// OTA attempt on the serial monitor instead of having to guess again.
Serial.printf("[OTA] Starting: url=%s freeHeap=%u RSSI=%ddBm\n", url,
(unsigned)ESP.getFreeHeap(), WiFi.RSSI());
WiFiClientSecure client;
client.setInsecure();
client.setTimeout(15000);
// WICHTIG: GitHubs "browser_download_url" fuer Release-Assets ist KEIN
// direkter Download-Link, sondern liefert erst ein HTTP 301/302-Redirect
// auf eine signierte objects.githubusercontent.com-URL. HTTPUpdate folgt
// Redirects standardmaessig NICHT (HTTPC_DISABLE_FOLLOW_REDIRECTS ist der
// Default) - ohne diese Zeile bricht der Download mit HTTP_UPDATE_FAILED
// ab, weil statt der .bin-Datei nur die Redirect-Antwort ankommt. Siehe
// z.B. espressif/arduino-esp32#3020. HTTPC_STRICT_FOLLOW_REDIRECTS
// reicht, da wir nur GET verwenden.
// IMPORTANT: GitHub's "browser_download_url" for release assets is NOT
// a direct download link, but first returns an HTTP 301/302 redirect
// to a signed objects.githubusercontent.com URL. HTTPUpdate does NOT
// follow redirects by default (HTTPC_DISABLE_FOLLOW_REDIRECTS is the
// default) - without this line the download aborts with HTTP_UPDATE_FAILED
// because only the redirect response arrives instead of the .bin file. See
// e.g. espressif/arduino-esp32#3020. HTTPC_STRICT_FOLLOW_REDIRECTS
// suffices since we only use GET.
httpUpdate.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
// Wir zeigen nach erfolgreicher Installation selbst noch eine kurze
// Erfolgsmeldung an, bevor das Geraet neu startet - siehe
// We show a brief success message ourselves after successful installation,
// before the device reboots - see
// menu_screen.cpp::runOtaUpdateScreen().
httpUpdate.rebootOnUpdate(false);
httpUpdate.onProgress([onProgress](int cur, int total) {
if (onProgress && total > 0) {
onProgress((uint8_t)((cur * 100) / total));
}
// Nur gelegentlich loggen (alle ~10%), sonst quillt der Seriell-
// Monitor bei grossen Dateien mit hunderten Zeilen ueber.
// Only log occasionally (every ~10%), otherwise the serial
// monitor overflows with hundreds of lines for large files.
static int8_t lastLoggedPercent = -1;
if (total > 0) {
int8_t pct = (int8_t)((cur * 100) / total);
if (pct != lastLoggedPercent && pct % 10 == 0) {
lastLoggedPercent = pct;
Serial.printf("[OTA] Fortschritt: %d%% (%d/%d Bytes) freeHeap=%u\n", pct, cur, total,
Serial.printf("[OTA] Progress: %d%% (%d/%d bytes) freeHeap=%u\n", pct, cur, total,
(unsigned)ESP.getFreeHeap());
}
}
@@ -160,11 +159,11 @@ bool performUpdate(const char* url, void (*onProgress)(uint8_t percent)) {
t_httpUpdate_return result = httpUpdate.update(client, url);
if (result != HTTP_UPDATE_OK) {
Serial.printf("[OTA] Fehlgeschlagen: result=%d error=%d (%s) freeHeap=%u\n", (int)result,
Serial.printf("[OTA] Failed: result=%d error=%d (%s) freeHeap=%u\n", (int)result,
httpUpdate.getLastError(), httpUpdate.getLastErrorString().c_str(),
(unsigned)ESP.getFreeHeap());
} else {
Serial.println("[OTA] Erfolgreich heruntergeladen und geflasht.");
Serial.println("[OTA] Successfully downloaded and flashed.");
}
return result == HTTP_UPDATE_OK;

View File

@@ -1,42 +1,42 @@
#pragma once
#include <Arduino.h>
// Firmware-Update ueber WLAN (Menue > System > "Nach Update suchen") -
// prueft gegen die neueste Version im GitHub-Repository (Releases-API) und
// installiert sie bei Bestaetigung direkt auf dem Geraet, ohne Kabel/
// Web-Flasher. Blockierende, synchrone HTTPS-Aufrufe - werden nur bei
// explizitem Tastendruck ausgefuehrt (kein Hintergrund-Polling), analog zu
// AircraftDetails' Modell-/Routen-Abfragen, nur eben auf Core 1 statt
// Core 0, da hier direkt auf Nutzer-Interaktion reagiert wird.
// Firmware update over WiFi (Menu > System > "Check for update") -
// checks against the latest version in the GitHub repository (Releases API) and
// installs it on the device directly upon confirmation, without cable/
// web flasher. Blocking, synchronous HTTPS calls - only executed on
// explicit button press (no background polling), analogous to
// AircraftDetails' model/route queries, just on Core 1 instead of
// Core 0, since it reacts directly to user interaction.
namespace OtaUpdate {
enum class CheckResult { Error, UpToDate, UpdateAvailable };
struct CheckInfo {
CheckResult result = CheckResult::Error;
// Neueste verfuegbare Version OHNE "v"-Praefix (z.B. "3.5.0") -
// sowohl bei UpToDate als auch bei UpdateAvailable gesetzt, damit
// der aufrufende Screen sie in beiden Faellen anzeigen kann.
// Latest available version WITHOUT "v" prefix (e.g. "3.5.0") -
// set for both UpToDate and UpdateAvailable, so
// the calling screen can display it in both cases.
char latestVersion[16] = {0};
// Direkter Download-Link zur firmware.bin des Releases - nur
// gesetzt, wenn result == UpdateAvailable.
// Direct download link to the firmware.bin of the release - only
// set when result == UpdateAvailable.
char downloadUrl[192] = {0};
};
// Fragt die GitHub-Releases-API nach dem neuesten Release ab, vergleicht
// dessen Versionsnummer (Tag-Name, "v"-Praefix wird ignoriert) gegen
// Config::APP_VERSION und sucht im Release den Anhang "firmware.bin".
// CheckResult::Error bei jedem Fehler unterwegs (kein WLAN, Zeitueber-
// schreitung, unerwartetes JSON, kein firmware.bin im Release).
// Queries the GitHub Releases API for the latest release, compares
// its version number (tag name, "v" prefix is ignored) against
// Config::APP_VERSION and looks for the "firmware.bin" attachment in the release.
// CheckResult::Error on any error along the way (no WiFi, timeout,
// unexpected JSON, no firmware.bin in the release).
CheckInfo checkForUpdate();
// Laedt die firmware.bin von 'url' herunter und flasht sie (ueber die
// Standard-Arduino-HTTPUpdate-Bibliothek). Ruft bei Erfolg BEWUSST
// NICHT selbst ESP.restart() auf - der Aufrufer zeigt zuerst eine
// kurze Erfolgsmeldung an und startet danach selbst neu. onProgress
// wird waehrend des Downloads wiederholt mit 0-100 (Prozent) aufgerufen
// (darf nullptr sein). Gibt false zurueck, wenn Download oder Flash-
// Vorgang fehlschlagen - das Geraet laeuft in dem Fall unveraendert mit
// der bisherigen Firmware weiter.
// Downloads the firmware.bin from 'url' and flashes it (via the
// standard Arduino HTTPUpdate library). Intentionally does NOT call
// ESP.restart() itself on success - the caller shows a
// brief success message first and then restarts itself. onProgress
// is called repeatedly during download with 0-100 (percent)
// (may be nullptr). Returns false if download or flash
// process fails - the device continues running unchanged with
// the previous firmware in that case.
bool performUpdate(const char* url, void (*onProgress)(uint8_t percent));
}

View File

@@ -41,11 +41,11 @@ int16_t cx, cy, radius;
};
constexpr int16_t INFO_BAR_H = 64;
// Zusaetzliche Hoehe fuer die zweite Legenden-Zeile (Bodenfahrzeug-
// Marker-Erklaerung), nur reserviert, wenn Bodenfahrzeuge tatsaechlich
// angezeigt werden (SettingsStore::hideGroundVehicles() == false) -
// ansonsten bleibt die Infoleiste genau so hoch wie bisher und der
// Radarkreis behaelt seine gewohnte Groesse.
// Additional height for the second legend row (ground vehicle
// marker explanation), only reserved when ground vehicles are actually
// displayed (SettingsStore::hideGroundVehicles() == false) -
// otherwise the info bar stays exactly as tall as before and the
// radar circle keeps its usual size.
constexpr int16_t INFO_BAR_GROUND_ROW_H = 18;
int16_t infoBarHeight() {
@@ -69,10 +69,10 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64
return L;
}
// +22px gegenueber frueher (264) fuer die neue Route-Zeile (siehe
// drawDetailPanel()) - beeinflusst NUR die Groesse des Detail-Panel-
// Overlays, nicht das normale Radar-Layout (computeLayout() oben
// richtet sich ausschliesslich nach infoBarHeight(), nicht nach
// +22px compared to the previous value (264) for the new route line (see
// drawDetailPanel()) - only affects the size of the detail panel
// overlay, not the normal radar layout (computeLayout() above
// only depends on infoBarHeight(), not
// DETAIL_PANEL_H).
constexpr int16_t DETAIL_PANEL_H = 286;
@@ -103,15 +103,15 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64
bool ledBlinkOn = true;
// "Leerer Himmel"-Timer: merkt sich, wann zuletzt mindestens ein
// Flugzeug sichtbar war (nach allen Filtern) - namespace-weit statt
// lokal in render(), da tick() (siehe dort) den Wert bei jedem Tick
// (alle 80ms) braucht, um den Sekundenzaehler fluessig hochzuzaehlen,
// statt nur alle paar Sekunden bei einem render()-Aufruf.
// "Empty Sky" timer: remembers when at least one
// aircraft was last visible (after all filters) - namespace-wide instead of
// local to render(), since tick() (see there) needs the value on every tick
// (every 80ms) to count up the seconds smoothly,
// instead of only every few seconds on a render() call.
uint32_t lastAircraftSeenMs = millis();
// Wird von der Detail-Panel-Laufschrift (LineMarquee in drawDetailPanel)
// fuer das Scrollen von Routentext verwendet.
// Used by the detail panel marquee (LineMarquee in drawDetailPanel)
// for scrolling route text.
constexpr uint32_t INFO_MARQUEE_STEP_MS = 200;
String infoMarqueeWindow(TFT_eSPI& tft, const String& src, int32_t startIdx, int16_t maxWidth) {
@@ -133,16 +133,16 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64
float prevSweepAngleDeg = -1.0f;
constexpr float SWEEP_DEGREES_PER_SEC = 45.0f;
// Gleiche Logik wie main.cpp::isNightDimHours() - Nacht = zwischen
// Sonnenuntergang und Sonnenaufgang am aktiven Standort (siehe
// sun_times.h), mit Rueckfall auf ein festes 22:00-06:00-Fenster,
// solange Standort/Uhrzeit noch nicht bekannt sind. Hier bewusst
// dupliziert statt geteilt, siehe CLAUDE.md-Konvention ("jeder Screen
// unabhaengig lauffaehig, kein gemeinsames Modul fuer solche
// Kleinigkeiten").
// Same logic as main.cpp::isNightDimHours() - night = between
// sunset and sunrise at the active location (see
// sun_times.h), with fallback to a fixed 22:00-06:00 window
// as long as location/time are not yet known. Deliberately
// duplicated instead of shared, see CLAUDE.md convention ("each screen
// independently runnable, no shared module for such
// small items").
bool isNightHours() {
time_t now = time(nullptr);
if (now <= 8 * 3600 * 2) return false; // Uhrzeit noch nicht per NTP synchronisiert
if (now <= 8 * 3600 * 2) return false; // time not yet synced via NTP
struct tm tmNow;
localtime_r(&now, &tmNow);
@@ -164,22 +164,23 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64
return (hour >= 22 || hour < 6);
}
// Nur aktiv, wenn der Nutzer die Nachtdimmung (Menue > System) eingeschaltet
// hat UND wir gerade im Nachtfenster sind - dieselbe Einstellung, die sonst
// nur die Hintergrundbeleuchtung dimmt, dimmt jetzt zusaetzlich auch die
// Radar-Farben (Flugzeug-Marker + Sweep-Linie), damit das Display nachts
// insgesamt weniger blendet.
// Only active when the user has enabled night dimming (Menu > System)
// AND we are currently in the night window - the same setting that otherwise
// only dims the backlight, now additionally also dims the
// radar colors (aircraft markers + sweep line), so the display
// is less glaring overall at night.
bool nightDimActiveNow() {
return SettingsStore::nightDimmingEnabled() && isNightHours();
}
// Farbe je nach Flughoehe - gedaempft (dunkleres Gruen/Oliv) waehrend der
// Nachtdimmung.
// Color by altitude - dimmed (darker green/olive) during
// night dimming.
//
// Die rote Hoehenstufe wird bewusst NICHT gedaempft: der bisherige
// Dimm-Ton (160,30,0) sah auf dem Display eher orange-braun statt rot
// aus und war dadurch nicht mehr klar von der gelben Stufe zu
// unterscheiden. Rot bleibt deshalb Tag und Nacht der reine TFT_RED-Ton.
// The red altitude level is deliberately NOT dimmed: the previous
// dimmed tone (160,30,0) looked orange-brown instead of red
// on the display and was therefore no longer clearly distinguishable
// from the yellow level. Red therefore remains the pure TFT_RED tone
// day and night.
uint16_t colorForAltitude(TFT_eSPI& gfx, int32_t altFt) {
bool dim = nightDimActiveNow();
if (altFt < Config::COLOR_LOW_ALT_THRESHOLD_FT)
@@ -193,34 +194,34 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64
return nightDimActiveNow() ? gfx.color565(0, 110, 0) : TFT_GREEN;
}
// Feste Blau-Farbe fuer Bodenfahrzeug-Marker (ADS-B-Emitter-Kategorie
// "C*", z.B. Flughafen-Fahrzeuge) - bewusst unabhaengig von
// colorForAltitude(), da Bodenfahrzeuge praktisch immer auf 0ft stehen
// und sonst dieselbe Farbe wie niedrig fliegende Flugzeuge haetten,
// was die optische Unterscheidung (Quadrat vs. Kreis) unnoetig
// erschweren wuerde. Bei Nachtdimmung gedaempft, gleiches Muster wie
// Fixed blue color for ground vehicle markers (ADS-B emitter category
// "C*", e.g. airport vehicles) - deliberately independent of
// colorForAltitude(), since ground vehicles are practically always at 0ft
// and would otherwise have the same color as low-flying aircraft,
// which would unnecessarily complicate the visual distinction
// (square vs. circle). Dimmed during night dimming, same pattern as
// colorForAltitude().
uint16_t colorForGroundVehicle(TFT_eSPI& gfx) {
return nightDimActiveNow() ? gfx.color565(0, 0, 160) : TFT_BLUE;
}
// Zeigt die PEILUNG (nicht zu verwechseln mit der Flugrichtung/heading)
// zum aktuell ausgewaehlten Flugzeug: eine duenne, gepunktete Linie vom
// Radar-Zentrum zum Kreisrand in Richtung bearingDeg, plus die Gradzahl
// direkt ausserhalb des Rings. Hilft dabei, das Flugzeug tatsaechlich am
// Himmel zu finden ("in diese Richtung schauen"). Nur fuer den Moment
// des Zeichnens relevant - beim naechsten Sweep-Tick/Redraw wird sie vom
// normalen Hintergrund-Redraw automatisch mit geloescht und neu gesetzt.
// Shows the BEARING (not to be confused with the heading)
// to the currently selected aircraft: a thin, dotted line from the
// radar center to the circle edge in the direction of bearingDeg, plus the degree number
// directly outside the ring. Helps to actually find the aircraft
// in the sky ("look in this direction"). Only relevant for the moment
// of drawing - on the next sweep tick/redraw it is automatically
// erased and redrawn by the normal background redraw.
void drawBearingIndicator(TFT_eSPI& gfx, const Layout& L, float bearingDeg) {
double rad = bearingDeg * PI / 180.0;
double s = sin(rad), c = cos(rad);
// Gepunktete Linie: kurze Segmente statt einer durchgezogenen Linie,
// damit sie sich optisch von den Kompass-Kreuzlinien und Ringen
// unterscheidet und nicht wie ein staendiges UI-Element wirkt.
// Dotted line: short segments instead of a solid line,
// so it visually differs from the compass cross lines and rings
// and does not look like a permanent UI element.
constexpr uint8_t DOT_COUNT = 10;
for (uint8_t i = 0; i < DOT_COUNT; i++) {
if (i % 2 != 0) continue; // nur jedes zweite Segment zeichnen
if (i % 2 != 0) continue; // only draw every second segment
float t0 = (float)i / DOT_COUNT;
float t1 = (float)(i + 1) / DOT_COUNT;
int16_t x0 = L.cx + (int16_t)lround(t0 * L.radius * s);
@@ -230,12 +231,12 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64
gfx.drawLine(x0, y0, x1, y1, TFT_WHITE);
}
// Gradzahl knapp ausserhalb des Rings, an der Stelle wo die Peilung
// den Kreisrand durchstoesst. Der Radius-Kreis nutzt fast die volle
// Bildschirmbreite (siehe computeLayout(): nur 6px Rand) - ein Label
// ausserhalb des Rings wuerde bei Ost/West-Peilungen ueber den
// sichtbaren Bereich hinausragen. Deshalb: Label-Position an die
// Bildschirmgrenzen clampen statt stur dem Winkel zu folgen.
// Bearing degree just outside the ring, at the point where the bearing
// line intersects the circle edge. The radius circle uses almost the full
// screen width (see computeLayout(): only 6px margin) - a label
// outside the ring would extend beyond the visible area
// for east/west bearings. Therefore: clamp label position to
// screen boundaries instead of blindly following the angle.
int16_t labelX = L.cx + (int16_t)lround((L.radius + 10) * s);
int16_t labelY = L.cy - (int16_t)lround((L.radius + 10) * c);
labelX = constrain(labelX, (int16_t)14, (int16_t)(Config::SCREEN_WIDTH - 14));
@@ -248,11 +249,11 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64
gfx.setTextDatum(TL_DATUM);
}
// kurzer Kursstrich in Flugrichtung (headingDeg) MIT Pfeilspitze am Ende -
// eine reine Linie ohne Spitze war nicht eindeutig lesbar (nicht erkennbar,
// welches Ende "vorne" ist). Die Spitze besteht aus zwei kurzen Strichen,
// die knapp vor der Linienspitze schraeg nach aussen abzweigen (klassisches
// Chevron-/Pfeilkopf-Symbol, wie bei ATC-Radardarstellungen ueblich).
// Short course line in flight direction (headingDeg) WITH arrowhead at the end -
// a plain line without a tip was not clearly readable (not recognizable
// which end is "front"). The tip consists of two short strokes
// that branch diagonally outward just before the line tip (classic
// chevron/arrowhead symbol, as typical in ATC radar displays).
void drawAircraftMarker(TFT_eSPI& gfx, int16_t x, int16_t y, float headingDeg, uint16_t color) {
gfx.fillCircle(x, y, 5, color);
@@ -263,9 +264,9 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64
int16_t tipY = y + dy;
gfx.drawLine(x, y, tipX, tipY, color);
// Pfeilspitze: zwei kurze Striche von der Spitze aus, je 150 Grad
// zur Kurslinie zurueckgeklappt (also leicht "nach hinten" zeigend,
// wie bei einem Pfeilkopf "^" ueblich).
// Arrowhead: two short strokes from the tip, each folded back 150 degrees
// to the course line (i.e. slightly pointing "backward",
// as is common for an arrowhead "^").
constexpr double WING_ANGLE_RAD = 150.0 * PI / 180.0;
constexpr int16_t WING_LEN = 4;
double wing1 = rad + WING_ANGLE_RAD;
@@ -278,21 +279,21 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64
gfx.drawLine(tipX, tipY, w2x, w2y, color);
}
// Eigener Marker fuer Bodenfahrzeuge (ADS-B-Kategorie "C*") - ein
// gefuelltes Quadrat statt Kreis+Pfeilkopf, damit sie auf dem Radar
// klar von echten Flugzeugen zu unterscheiden sind (Heading/Kurs ist
// bei Bodenfahrzeugen ausserdem meist nicht aussagekraeftig).
// Separate marker for ground vehicles (ADS-B category "C*") - a
// filled square instead of circle+arrowhead, so they are clearly
// distinguishable from real aircraft on the radar (heading/course is
// also usually not meaningful for ground vehicles).
void drawGroundVehicleMarker(TFT_eSPI& gfx, int16_t x, int16_t y, uint16_t color) {
constexpr int16_t HALF = 4;
gfx.fillRect((int16_t)(x - HALF), (int16_t)(y - HALF), (int16_t)(2 * HALF), (int16_t)(2 * HALF), color);
}
// Eigener Marker fuer Hubschrauber (ADS-B-Emitter-Kategorie "A7" =
// Rotorcraft) - ein gefuellter Kreis mit durchgehendem Rotorkreuz statt
// Pfeilkopf, da Hubschrauber im Schwebeflug keinen aussagekraeftigen
// "nach vorne"-Kurs wie ein Flugzeug haben (gleiche Ueberlegung wie beim
// Bodenfahrzeug-Marker, der aus demselben Grund ebenfalls ohne
// Kurslinie auskommt).
// Separate marker for helicopters (ADS-B emitter category "A7" =
// Rotorcraft) - a filled circle with a solid rotor cross instead of
// an arrowhead, since helicopters in hover have no meaningful
// "forward" course like a fixed-wing aircraft (same reasoning as the
// ground vehicle marker, which for the same reason also
// does without a course line).
void drawHelicopterMarker(TFT_eSPI& gfx, int16_t x, int16_t y, uint16_t color) {
constexpr int16_t ROTOR_LEN = 8;
gfx.fillCircle(x, y, 4, color);
@@ -300,14 +301,14 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64
gfx.drawLine(x, (int16_t)(y - ROTOR_LEN), x, (int16_t)(y + ROTOR_LEN), color);
}
// Eigener Marker fuer "Heavy"-Flugzeuge (ADS-B-Emitter-Kategorie "A5" -
// Flugzeuge ueber 136t Starthoechstgewicht, z.B. A380/B747/B777/A330).
// Gleicher Aufbau wie drawAircraftMarker() (Kreis + Kurslinie mit
// Pfeilkopf), aber mit groesserem Kreis und einem zusaetzlichen duennen
// Aussenring - auf den ersten Blick als "groesser/schwerer" erkennbar,
// ohne eine komplett neue Formsprache einzufuehren. Die Farbe bleibt wie
// gewohnt die Hoehenfarbe (colorForAltitude()) - die Form transportiert
// "Heavy", kein Alarm-/Auffaelligkeitszustand.
// Separate marker for "Heavy" aircraft (ADS-B emitter category "A5" -
// aircraft over 136t maximum takeoff weight, e.g. A380/B747/B777/A330).
// Same structure as drawAircraftMarker() (circle + course line with
// arrowhead), but with a larger circle and an additional thin
// outer ring - recognizable at first glance as "larger/heavier",
// without introducing a completely new shape language. The color remains
// the usual altitude color (colorForAltitude()) - the shape conveys
// "Heavy", not an alarm/conspicuity state.
void drawHeavyMarker(TFT_eSPI& gfx, int16_t x, int16_t y, float headingDeg, uint16_t color) {
gfx.fillCircle(x, y, 6, color);
gfx.drawCircle(x, y, 8, color);
@@ -368,12 +369,12 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64
snprintf(highLabel, sizeof(highLabel), ">30k ft");
}
// Dieselbe colorForAltitude()-Funktion wie fuer die Flugzeug-Marker
// selbst verwenden (mit einer typischen Hoehe je Band) - sonst zeigt
// die Legende bei aktiver Nachtdimmung (Sonnenunter- bis -aufgang) die HELLEN Farben,
// waehrend die Marker/Beschriftungen auf dem Radar bereits gedaempft
// sind. Das fuehrte dazu, dass z.B. ein rotes Flugzeug nachts eher
// dunkelorange wirkte, obwohl die Legende noch reines Rot zeigte.
// Use the same colorForAltitude() function as for the aircraft markers
// themselves (with a typical altitude per band) - otherwise the
// legend would show the BRIGHT colors during active night dimming (sunset to sunrise),
// while the markers/labels on the radar are already dimmed.
// This caused e.g. a red aircraft to appear dark orange at night,
// even though the legend still showed pure red.
struct { uint16_t color; const char* label; } items[3] = {
{colorForAltitude(gfx, 0), lowLabel},
{colorForAltitude(gfx, Config::COLOR_LOW_ALT_THRESHOLD_FT), midLabel},
@@ -382,15 +383,15 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64
int16_t segW = Config::SCREEN_WIDTH / 3;
gfx.setTextColor(TFT_WHITE, TFT_BLACK);
// Aussenspalten (gruen/rot) bleiben auf ihrer festen 1/3-Raster-
// Position (x0 = i*segW+6). Die mittlere (gelbe) Spalte wurde
// bisher genauso starr positioniert, sass dadurch aber optisch zu
// weit rechts: ihr Label ("3000-9100") ist deutlich laenger als
// die Aussenlabels, wodurch rechts kaum noch Luft zum roten
// Eintrag blieb, waehrend links viel Freiraum zum kurzen gruenen
// Label uebrig war. Jetzt wird die gelbe Spalte stattdessen mittig
// in die Luecke zwischen Ende des gruenen Textes und Anfang des
// roten Punkts gesetzt.
// Outer columns (green/red) stay at their fixed 1/3 grid
// position (x0 = i*segW+6). The middle (yellow) column was
// previously positioned just as rigidly, but visually sat too
// far right: its label ("3000-9100") is noticeably longer than
// the outer labels, leaving hardly any space on the right to the
// red entry, while lots of empty space remained on the left next to the short green
// label. Now the yellow column is instead placed
// centered in the gap between the end of the green text and the start of the
// red dot.
int16_t x0Low = 0 * segW + 6;
int16_t x0High = 2 * segW + 6;
@@ -412,10 +413,10 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64
gfx.setCursor(x0Mid + 7, y);
gfx.print(items[1].label);
// Zweite Legenden-Zeile fuer die blauen Bodenfahrzeug-Quadrate - nur
// wenn sie ueberhaupt sichtbar sind (Flugoptionen >
// "Bodenfahrzeuge ausblenden" == aus). infoBarHeight() reserviert
// den dafuer noetigen zusaetzlichen Platz nur in diesem Fall, siehe
// Second legend row for the blue ground vehicle squares - only
// when they are actually visible (Flight Options >
// "Hide ground vehicles" == off). infoBarHeight() reserves
// additional space for this only in that case, see
// computeLayout().
if (!SettingsStore::hideGroundVehicles()) {
int16_t gy = (int16_t)(y + INFO_BAR_GROUND_ROW_H);
@@ -469,10 +470,10 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64
gfx.drawString("S", L.cx, L.cy + L.radius - 10);
gfx.drawString("E", L.cx + L.radius - 10, L.cy);
gfx.drawString("W", L.cx - L.radius + 10, L.cy);
// Ring-Beschriftungen (Zwischenabstaende) respektieren jetzt die
// Einheiten-Einstellung (Menue > Einheiten) - vorher immer in km,
// auch wenn Imperial (nm) eingestellt war. Gleiches Umrechnungs-
// Muster wie beim Range-Button unten und der Legende oben.
// Ring labels (intermediate distances) now respect the
// unit setting (Menu > Units) - previously always in km,
// even when Imperial (nm) was set. Same conversion
// pattern as the range button below and the legend above.
bool metric = LocationManager::useMetricUnits();
float displayRange = metric ? rangeKm : Units::kmToNm(rangeKm);
char ringLabel[8];
@@ -483,14 +484,14 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64
gfx.setTextDatum(TL_DATUM);
}
// Laufschrift-Zustand fuer EINE Detail-Panel-Zeile - gleiches
// Grundprinzip wie InfoMarquee oben, aber das Detail-Panel hat bis zu
// neun solcher Zeilen gleichzeitig (Airline, Modell, Typ, Hoehe,
// Geschw., Steig-/Sinkrate, Distanz/Peilung, Squawk, Sitzplaetze),
// deshalb ein eigener Zustand pro Zeile statt einer einzigen globalen
// Instanz. y/h/maxWidth/fg merken sich die Zeilen-Geometrie, damit
// tickDetailPanelMarquees() (siehe unten) ohne Zusatzparameter weiss,
// wo/wie jede Zeile neu zu zeichnen ist.
// Marquee state for ONE detail panel line - same basic
// principle as InfoMarquee above, but the detail panel has up to
// nine such lines simultaneously (airline, model, type, altitude,
// speed, climb/sink rate, distance/bearing, squawk, seats),
// hence a separate state per line instead of a single global
// instance. y/h/maxWidth/fg remember the line geometry so that
// tickDetailPanelMarquees() (see below) knows where/how to
// redraw each line without extra parameters.
struct LineMarquee {
String text;
String ring;
@@ -509,22 +510,22 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64
};
PanelState lastPanel;
// Zeichnet eine Detail-Panel-Zeile NEU, wenn sich ihr Text geaendert
// hat (oder das Panel komplett neu aufgebaut wird) - merkt sich dabei
// auch, ob der Text zu breit fuer die verfuegbare Breite ist
// (needsScroll) und baut bei Bedarf den Ring-Puffer fuer die
// Laufschrift auf (gleiches Muster wie infoMarqueeWindow() oben).
// Passt zu breite Zeilen ("Modell: ...", "Distanz: ...") NICHT mehr
// mit "..." ab, sondern laesst sie horizontal durchscrollen - siehe
// tickDetailPanelMarquees() weiter unten fuer den Teil, der das
// tatsaechliche Weiterscrollen zwischen zwei Datenaktualisierungen
// uebernimmt (render() liefert i.d.R. nur alle ~300ms neue Werte,
// das waere fuer eine fluessige Laufschrift viel zu selten).
// Redraws a detail panel line when its text has changed
// (or the panel is being rebuilt) - remembers whether
// the text is too wide for the available width
// (needsScroll) and builds the ring buffer for the
// marquee if needed (same pattern as infoMarqueeWindow() above).
// Lines that are too wide ("Model: ...", "Distance: ...") are NO
// LONGER abbreviated with "..." but scrolled horizontally - see
// tickDetailPanelMarquees() below for the part that
// actually advances the scrolling between data updates
// (render() typically only supplies new values every ~300ms,
// which would be far too rare for smooth scrolling).
void updateMarqueeLine(TFT_eSPI& gfx, int16_t y, int16_t h, int16_t maxWidth,
uint16_t fg, LineMarquee& m, const String& newText, bool forceFull) {
// Geometrie/Farbe IMMER aktualisieren (auch ohne Textaenderung) -
// tickDetailPanelMarquees() braucht diese Werte, um beim naechsten
// Scroll-Schritt an der richtigen Stelle neu zu zeichnen.
// Always update geometry/color (even without text change) -
// tickDetailPanelMarquees() needs these values to redraw
// at the correct position on the next scroll step.
m.y = y;
m.h = h;
m.maxWidth = maxWidth;
@@ -534,7 +535,7 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64
m.text = newText;
m.needsScroll = gfx.textWidth(newText) > maxWidth;
String withGap = newText + " "; // 3 Leerzeichen Luecke vor der Wiederholung
String withGap = newText + " "; // 3 space gap before repetition
m.ring = withGap + withGap;
m.charOffset = 0;
m.lastStepMs = millis();
@@ -545,11 +546,11 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64
gfx.print(m.needsScroll ? infoMarqueeWindow(gfx, m.ring, 0, maxWidth) : newText);
}
// Laesst eine einzelne Detail-Panel-Zeile weiterscrollen, falls sie zu
// breit ist (needsScroll) und genug Zeit seit dem letzten Schritt
// vergangen ist - sonst passiert nichts (kein unnoetiges Neuzeichnen
// fuer Zeilen, die ohnehin komplett passen). Wird von
// tickDetailPanelMarquees() fuer alle neun Zeilen aufgerufen.
// Advances a single detail panel line if it is too
// wide (needsScroll) and enough time has passed since the last step
// - otherwise nothing happens (no unnecessary redrawing
// for lines that fit completely). Called by
// tickDetailPanelMarquees() for all nine lines.
void advanceAndDrawMarqueeLine(TFT_eSPI& gfx, LineMarquee& m) {
if (!m.needsScroll) return;
@@ -567,12 +568,12 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64
gfx.print(infoMarqueeWindow(gfx, m.ring, m.charOffset, m.maxWidth));
}
// Laesst alle Detail-Panel-Zeilen weiterscrollen - waehrend das Panel
// offen ist, ruft tick() (alle ~80ms) das hier auf, DAZWISCHEN also
// viel oefter als render() (das nur bei neuen Flugzeug-Daten,
// ~alle 300ms, feuert) neue Werte liefert. Ohne diesen zusaetzlichen
// Aufruf wuerde eine lange Zeile bei jedem render()-Aufruf zwar einen
// Schritt weiterspringen, aber nicht fluessig durchlaufen.
// Lets all detail panel lines scroll - while the panel
// is open, tick() (every ~80ms) calls this, in BETWEEN much
// more often than render() (which only fires on new aircraft data,
// every ~300ms). Without this additional
// call, a long line would jump one step on each render() call
// but would not scroll smoothly.
void tickDetailPanelMarquees(TFT_eSPI& gfx) {
if (!lastPanel.valid) return;
advanceAndDrawMarqueeLine(gfx, lastPanel.airline);
@@ -632,11 +633,11 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64
updateMarqueeLine(gfx, y, LINE_H, textMaxWidth, TFT_GREEN, lastPanel.type, typeLine, forceFull);
y += LINE_H;
// Start-/Zielflughafen (ICAO-Code) ueber AircraftDetails::get() -
// wird zusammen mit dem Modell abgefragt (siehe aircraft_details.cpp),
// aber ueber das Rufzeichen statt den Hex-Code aufgeloest. Ohne
// Rufzeichen (z.B. manche Sichtflug-Maschinen) bleibt die Route
// grundsaetzlich unbekannt, kein Abruf noetig.
// Origin/destination airport (ICAO code) via AircraftDetails::get() -
// is queried together with the model (see aircraft_details.cpp),
// but resolved via the callsign instead of the hex code. Without
// a callsign (e.g. some VFR aircraft) the route remains
// fundamentally unknown, no lookup needed.
String routeLine;
if (!a.callsign[0]) {
routeLine = String(I18n::t(StringId::DETAIL_ROUTE)) + I18n::t(StringId::DETAIL_UNKNOWN);
@@ -674,11 +675,11 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64
updateMarqueeLine(gfx, y, LINE_H, textMaxWidth, TFT_GREEN, lastPanel.climb, climbLine, forceFull);
y += LINE_H;
// Zusaetzlich zu km/nm auch Meilen (mi) - nm allein war fuer
// Laien-Nutzer aus Ländern mit imperialen Einheiten (v.a. USA)
// nicht selbsterklaerend, "Meilen" sind dort die gebraeuchlichere
// Alltags-Distanzeinheit (nm bleibt trotzdem stehen, da es zur
// Geschwindigkeit in kt passt: 1 kt = 1 nm/h).
// In addition to km/nm also miles (mi) - nm alone was
// not self-explanatory for lay users from countries with
// imperial units (especially the US), "miles" is the more common
// everyday distance unit there (nm still remains since it matches
// speed in kt: 1 kt = 1 nm/h).
snprintf(buf, sizeof(buf), "%s%.0fkm / %.0fnm / %.0fmi %s%.0f", I18n::t(StringId::DETAIL_DIST),
a.distanceKm, Units::kmToNm(a.distanceKm), Units::kmToMi(a.distanceKm),
I18n::t(StringId::DETAIL_HDG), a.headingDeg);
@@ -738,15 +739,15 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64
}
}
// Hintergrund-Sterne AUSSERHALB des Radarkreises (auf Wunsch: dasselbe
// dekorative Sternenfunkeln wie im Detail-Panel/Menue, aber ohne mit
// dem staendig neu gezeichneten Kreisinhalt zu kollidieren - genau DAS
// wuerde "ruckeln" verursachen, da Sweep-Linie/Blips/Ringe innerhalb
// des Kreises bei jedem tick() sowieso neu gezeichnet werden und dabei
// die Sterne dort staendig wieder verschlucken wuerden). Ausserhalb
// des Kreises bleibt der Hintergrund dauerhaft schwarz und wird von
// sonst niemandem angefasst - dort koennen die Sterne frei vor sich
// hin twinkeln, voellig unabhaengig von Sweep-Linie/Blip-Redraws.
// Background stars OUTSIDE the radar circle (requested: the same
// decorative star twinkling as in the detail panel/menu, but without
// colliding with the constantly redrawn circle content - THAT would
// cause stuttering, since the sweep line/blips/rings within the
// circle are redrawn on every tick() anyway and would constantly
// swallow the stars there). Outside the circle the background remains
// permanently black and is touched by no one else - the stars can
// freely twinkle there, completely independent of sweep line/blip
// redraws.
constexpr uint8_t BG_STAR_COUNT = 20;
struct BgStar {
int16_t x, y;
@@ -756,15 +757,15 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64
BgStar bgStars[BG_STAR_COUNT];
bool bgStarsInitialized = false;
// Platziert jeden Stern per Rejection-Sampling irgendwo im Bereich
// [top..L.infoTop) x [0..SCREEN_WIDTH), aber nur wenn der Punkt
// ausserhalb von Radius+MARGIN um den Kreismittelpunkt liegt (MARGIN
// haelt zusaetzlich Abstand zum Ring und zum "N"-Kompasslabel knapp
// ausserhalb des Kreises). Findet ein Versuch nach 25 Anlaeufen keinen
// passenden Punkt (praktisch nie, da links/rechts vom Kreis immer
// reichlich Platz ist), wird einfach der letzte Versuchspunkt
// uebernommen - rein kosmetisches Feature, kein Grund fuer eine
// Endlosschleife.
// Places each star via rejection sampling somewhere in the area
// [top..L.infoTop) x [0..SCREEN_WIDTH), but only if the point
// is outside radius+MARGIN around the circle center (MARGIN
// additionally keeps distance from the ring and the "N" compass label
// just outside the circle). If an attempt after 25 tries finds no
// suitable point (practically never, since there is always plenty
// of space left/right of the circle), the last attempted point is
// simply used - purely cosmetic feature, no reason for an
// infinite loop.
void initBgStarsIfNeeded(const Layout& L, int16_t top) {
if (bgStarsInitialized) return;
randomSeed((uint32_t)esp_random());
@@ -855,7 +856,7 @@ void render(TFT_eSPI& tft, int16_t top) {
bool selectionStillPresent = false;
Aircraft selected{};
uint8_t visibleCount = 0; // nach allen Filtern (Reichweite, Bodenfahrzeuge, Airline-Filter)
uint8_t visibleCount = 0; // after all filters (range, ground vehicles, airline filter)
for (uint8_t i = 0; i < MAX_HIT_POINTS; i++) hitPoints[i].valid = false;
@@ -876,14 +877,14 @@ if (AirlineFilter::isHidden(a.callsign)) continue;
RadarMath::PolarCoord polar{a.distanceKm, a.bearingDeg};
RadarMath::ScreenPoint pt = RadarMath::toScreen(polar, L.cx, L.cy, L.radius, rangeKm);
// ADS-B-Emitter-Kategorie "C*" = Bodenfahrzeug (nur ueberhaupt
// sichtbar, wenn "Bodenfahrzeuge ausblenden" aus ist, siehe Filter
// oben) - eigene Farbe/Marker statt der hoehenbasierten Flugzeug-
// Darstellung, siehe drawGroundVehicleMarker()/colorForGroundVehicle().
// ADS-B emitter category "C*" = ground vehicle (only ever
// visible if "Hide ground vehicles" is off, see filter
// above) - separate color/marker instead of altitude-based
// rendering, see drawGroundVehicleMarker()/colorForGroundVehicle().
bool isGroundVehicle = a.category[0] == 'C';
bool isRotorcraft = a.category[0] == 'A' && a.category[1] == '7';
// ADS-B-Emitter-Kategorie "A5" = "Heavy" - eigene Markerform (siehe
// drawHeavyMarker()), unabhaengig von jeglicher Ring-Markierung.
// ADS-B emitter category "A5" = "Heavy" - separate marker shape (see
// drawHeavyMarker()), independent of any ring marking.
bool isHeavy = a.category[0] == 'A' && a.category[1] == '5';
uint16_t color = isGroundVehicle ? colorForGroundVehicle(tft) : colorForAltitude(tft, a.altBaroFt);
bool isSelected = selectedHex[0] && strcmp(a.hex, selectedHex) == 0;
@@ -947,7 +948,7 @@ if (AirlineFilter::isHidden(a.callsign)) continue;
lastPanel.valid = false;
int16_t infoTop = L.infoTop;
tft.drawFastHLine(0, infoTop, Config::SCREEN_WIDTH, TFT_DARKGREY);
// Info-Bar Button-Leiste: [?] [S] [L] [50km]
// Info bar button row: [?] [S] [L] [50km]
char rangeLabel[8];
bool rangeMetric = LocationManager::useMetricUnits();
if (rangeMetric) {
@@ -973,11 +974,11 @@ void tick(TFT_eSPI& tft, int16_t top, uint32_t deltaMs) {
if (selectedHex[0]) {
tft.startWrite();
updateDetailPanelStars(tft);
// Laesst zu breite Detail-Panel-Zeilen (z.B. "Modell: ..." oder
// "Distanz: ...") weiterscrollen, statt wie zuvor mit "..." starr
// abgeschnitten zu bleiben - render() liefert neue Werte nur alle
// ~300ms, das reicht fuer eine fluessige Laufschrift nicht aus,
// deshalb hier zusaetzlich im 80ms-Tick weiterschieben.
// Lets overly wide detail panel lines (e.g. "Model: ..." or
// "Distance: ...") scroll, instead of being statically truncated
// with "..." - render() only supplies new values every
// ~300ms, which is not enough for smooth scrolling,
// so it is additionally advanced here in the 80ms tick.
tickDetailPanelMarquees(tft);
tft.endWrite();
return;
@@ -988,13 +989,13 @@ void tick(TFT_eSPI& tft, int16_t top, uint32_t deltaMs) {
tft.startWrite();
// Twinkeln ausserhalb des Radarkreises - laeuft im selben 80ms-Takt wie
// die Sweep-Linie, damit es fluessig wirkt. render() (bei jedem Aircraft-
// Update, alle ~300ms) loescht den kompletten Inhaltsbereich per fillRect
// und zeichnet ihn neu, OHNE die Sterne erneut zu setzen - das ist
// bewusst so (wie beim Detail-Panel-Sternenfeld auch): die naechste
// tick()-Runde (spaetestens 80ms spaeter) laesst sie einfach wieder
// aufblitzen, das ist zu kurz, um als Ruckeln wahrgenommen zu werden.
// Twinkling outside the radar circle - runs at the same 80ms rate as
// the sweep line so it looks smooth. render() (on every aircraft
// update, every ~300ms) clears the entire content area with fillRect
// and redraws it WITHOUT re-setting the stars - this is
// intentional (just like the detail panel star field): the next
// tick() round (at most 80ms later) will simply make them
// twinkle again, which is too brief to be perceived as stuttering.
updateBgStars(tft, L, top);
if (prevSweepAngleDeg >= 0.0f) {
@@ -1015,16 +1016,16 @@ void tick(TFT_eSPI& tft, int16_t top, uint32_t deltaMs) {
bool inAlertRange = hp.distanceKm <= Config::LED_ALERT_RADIUS_KM;
if (inAlertRange && !ledBlinkOn) {
// Blink-Aus-Phase: frueher wurde hier ein pauschales 40x30px
// schwarzes Rechteck gezeichnet, um den Marker zu "verstecken" -
// das hat dabei aber auch die Radar-Ringe/Kompasslinien darunter
// ueberdeckt (bei jeder Hoehenfarbe gleichermassen, da diese
// Blink-Logik rein distanzbasiert ist). Stattdessen werden hier
// jetzt exakt dieselben Formen, die beim normalen Zeichnen weiter
// unten entstehen (Marker, Notfall-/Beobachtungs-Ring, Label),
// einfach in Schwarz nachgezeichnet - das macht sie pixelgenau
// wieder unsichtbar, ohne irgendetwas ausserhalb dieser Formen zu
// beruehren.
// Blink-off phase: previously a blanket 40x30px
// black rectangle was drawn to "hide" the marker -
// but that also covered the radar rings/compass lines
// underneath (equally for all altitude colors, since this
// blink logic is purely distance-based). Instead, exactly
// the same shapes that are drawn during normal rendering
// below (marker, emergency/watchlist ring, label) are
// now drawn in black - this makes them pixel-perfectly
// invisible again without touching anything outside
// those shapes.
if (hp.isGroundVehicle) {
drawGroundVehicleMarker(tft, hp.x, hp.y, TFT_BLACK);
} else if (hp.isRotorcraft) {
@@ -1068,9 +1069,9 @@ void tick(TFT_eSPI& tft, int16_t top, uint32_t deltaMs) {
tft.setTextDatum(TL_DATUM);
}
// Info-Zeile unten (Tap-Hinweis bzw. "Leerer Himmel"-Timer) als
// Laufende Aktualisierung der letzten Sichtung fuer den "Leerer
// Himmel"-Zaehler.
// Info line at bottom (tap hint or "Empty Sky" timer) as
// continuous update of the last sighting for the "Empty Sky"
// counter.
uint8_t visibleCountNow = 0;
for (uint8_t i = 0; i < MAX_HIT_POINTS; i++) {
if (hitPoints[i].valid) visibleCountNow++;
@@ -1539,10 +1540,10 @@ void runHelp(TFT_eSPI& tft, int16_t top) {
void selectAircraft(const char* hex, const char* callsign) {
strncpy(selectedHex, hex, sizeof(selectedHex) - 1);
AircraftDetails::request(hex, callsign);
// Erzwingt einen kompletten Neuaufbau des Detail-Panels beim naechsten
// render() - wichtig, falls der Bildschirm zwischenzeitlich von einem
// anderen Screen (z.B. der Flugzeugliste) ueberschrieben wurde, sonst
// wuerden unveraenderte Zeilen faelschlich als "schon da" uebersprungen.
// Forces a complete rebuild of the detail panel on the next
// render() - important if the screen was overwritten by another
// screen (e.g. the aircraft list), otherwise unchanged lines would
// be incorrectly skipped as "already there".
lastPanel.valid = false;
}

View File

@@ -21,10 +21,10 @@ void render(TFT_eSPI& tft, int16_t top);
// Shows a full-screen usage guide popup triggered by the "?" button.
void runHelp(TFT_eSPI& tft, int16_t top);
// Waehlt ein Flugzeug programmgesteuert aus (z.B. von der Flugzeugliste
// aus, nicht per Antippen auf dem Radar) - damit beim naechsten render()
// sofort das Detail-Panel fuer dieses Flugzeug erscheint, so als haette
// man es direkt im Radar angetippt.
// Selects an aircraft programmatically (e.g. from the aircraft list,
// not by tapping on the radar) - so that the next render() immediately
// shows the detail panel for this aircraft, as if it were tapped directly
// on the radar.
void selectAircraft(const char* hex, const char* callsign);
struct EmergencyInfo {

View File

@@ -1,22 +1,21 @@
#pragma once
#include <Arduino.h>
// Gemeinsames Lock fuer ALLE SD-Kartenzugriffe im gesamten Projekt. Die
// SD-Bibliothek ist nicht thread-sicher: ohne dieses Lock koennen
// gleichzeitige Zugriffe von NetTask (Core 0, z.B. Flugbuch-Eintrag
// schreiben alle 8s) und dem Haupt-Loop (Core 1, z.B. Einstellungen im
// Menue speichern) das Geraet einfrieren, wenn sie zeitlich zusammentreffen.
// Rekursiv, damit verschachtelte Aufrufe (z.B. FlightLogbook::update() ruft
// intern ensureCurrentDate() auf, das seinerseits auch sperrt) nicht
// blockieren.
// Common lock for ALL SD card access in the entire project. The
// SD library is not thread-safe: without this lock,
// concurrent access by NetTask (Core 0, e.g. writing a logbook entry
// every 8s) and the main loop (Core 1, e.g. saving settings in the
// menu) can freeze the device if they coincide in time.
// Recursive, so that nested calls (e.g. FlightLogbook::update() calls
// ensureCurrentDate() internally, which also locks) do not
// block.
namespace SdMutex {
void init();
void lock();
void unlock();
// RAII-Helfer: sperrt im Konstruktor, gibt im Destruktor frei - so kann
// man das Lock nicht versehentlich vergessen freizugeben (z.B. bei
// einem fruehen "return").
// RAII helper: locks in constructor, unlocks in destructor - so you cannot
// accidentally forget to release the lock (e.g. on an early "return").
class Guard {
public:
Guard() { lock(); }

View File

@@ -153,11 +153,10 @@ bool deleteDirectoryRecursive(const char* path) {
return false;
}
// Gleiches vorsichtiges "entry.name() koennte relativ ODER absolut
// sein"-Muster wie in flight_logbook.cpp::resetAllData() - anders als
// dort aber mit echter Rekursion in Unterordner (statt sie zu
// ueberspringen), da der Flightradar-Ordner welche enthaelt (logs/,
// screenshots/).
// Same cautious "entry.name() could be relative OR absolute" pattern as
// in flight_logbook.cpp::resetAllData() - but unlike there, with actual
// recursion into subdirectories (instead of skipping them), since the
// Flightradar folder contains them (logs/, screenshots/).
File entry = dir.openNextFile();
while (entry) {
bool isDir = entry.isDirectory();

View File

@@ -16,12 +16,12 @@ namespace SdStorage {
void seedDefaultDataFiles();
void logEvent(const char* csvLine);
// Loescht rekursiv einen kompletten Ordner (alle Dateien und
// Unterordner) inklusive des Ordners selbst. Fuer den Menuepunkt
// "Einstellungen zuruecksetzen" (settings_backup.cpp::factoryReset()) -
// der einzige aktuelle Aufrufer, der damit den kompletten
// Flightradar-Ordner von der SD-Karte entfernt. Gibt false zurueck,
// wenn der Pfad nicht existiert/kein Ordner ist.
// Recursively deletes a complete folder (all files and
// subfolders) including the folder itself. For the menu item
// "Reset settings" (settings_backup.cpp::factoryReset()) -
// the only current caller, which removes the complete
// Flightradar folder from the SD card. Returns false
// if the path does not exist / is not a folder.
bool deleteDirectoryRecursive(const char* path);
}

View File

@@ -66,9 +66,9 @@ bool factoryReset() {
bool ok;
{
// Guard-Block bewusst vor dem Neustart wieder verlassen (statt
// ueber die Funktion hinweg zu halten) - reine Vorsicht, auch wenn
// ESP.restart() den Chip ohnehin sofort zuruecksetzt.
// Deliberately leave the guard block before the restart (instead of
// holding it across the function call) - purely precautionary, even
// though ESP.restart() resets the chip immediately anyway.
SdMutex::Guard guard;
ok = SdStorage::deleteDirectoryRecursive(Config::SD_ROOT_DIR);
}
@@ -76,7 +76,7 @@ bool factoryReset() {
if (ok) {
delay(200);
ESP.restart();
// Wird nie erreicht - ESP.restart() kehrt nicht zurueck.
// Never reached - ESP.restart() does not return.
}
return ok;
}

View File

@@ -2,21 +2,20 @@
#include <Arduino.h>
namespace SettingsBackup {
// onStep (falls angegeben) wird bei jedem der beiden Kopiervorgaenge
// (erst Einstellungen, dann WLAN-Zugangsdaten) direkt VOR dem
// jeweiligen Kopieren aufgerufen - der aufrufende Screen
// (menu_screen.cpp) nutzt das, um waehrend des SD-bedingt spuerbar
// langsamen Sicherns/Wiederherstellens Fortschrittspunkte auf dem
// Button anzuzeigen, statt dass der Button eingefroren wirkt.
// onStep (if provided) is called before each of the two copy operations
// (first settings, then WiFi credentials) - the calling screen
// (menu_screen.cpp) uses this to show progress dots on the button during
// the SD-card-related noticeably slow backup/restore, instead of the
// button appearing frozen.
bool backup(void (*onStep)() = nullptr);
bool restore(void (*onStep)() = nullptr);
bool hasBackup();
// Loescht den kompletten Flightradar-Ordner von der SD-Karte und
// startet das Geraet neu - beim naechsten Boot laeuft dadurch wieder
// die komplette Ersteinrichtung (siehe main.cpp: isFirstRun-Erkennung
// ueber Config::SD_SETTINGS_FILE). Kehrt nur im Fehlerfall zurueck
// (z.B. SD nicht eingehaengt) - im Erfolgsfall startet das Geraet
// neu, bevor die Funktion "zurueckkehrt" (ESP.restart()).
// Deletes the entire Flightradar folder from the SD card and
// restarts the device - on the next boot, the complete first-time
// setup will run again (see main.cpp: isFirstRun detection via
// Config::SD_SETTINGS_FILE). Only returns on error (e.g. SD not mounted)
// - on success, the device restarts before the function "returns"
// (ESP.restart()).
bool factoryReset();
}

View File

@@ -9,14 +9,14 @@ namespace SettingsStore {
namespace {
uint8_t rangeIdx = Config::DEFAULT_RANGE_INDEX;
uint8_t altFilterIdx = Config::DEFAULT_ALT_FILTER_INDEX;
bool inverted = true; // Dieses Board braucht invertDisplay(true) fuer korrekte Farben (siehe main.cpp)
bool inverted = true; // This board needs invertDisplay(true) for correct colors (see main.cpp)
uint8_t brightnessPct = Config::BRIGHTNESS_MAX_PERCENT;
bool emergencyAlertOn = true;
bool proximityAlertOn = true;
bool watchlistAlertOn = true;
// MUSS bei einer frischen Installation aus sein - sonst schreibt sich
// die SD-Karte unbemerkt voll (siehe Bestaetigungsdialog beim
// Einschalten in menu_screen.cpp + 24h-Auto-Aus in flight_logbook.cpp).
// MUST be off on a fresh installation - otherwise the SD card fills up
// unnoticed (see confirmation dialog at power-on in menu_screen.cpp
// + 24h auto-off in flight_logbook.cpp).
bool flightLogbookOn = false;
uint32_t logbookEnabledAtEpoch = 0;
char logbookSessionFile[16] = {0};
@@ -107,11 +107,10 @@ void load() {
}
f.close();
// Sicherheitsregel: das Flugbuch darf nach einem Neustart nur dann
// aktiv bleiben, wenn auch ein gueltiger Einschalt-Zeitstempel
// vorhanden ist - kein Zeitstempel bedeutet garantiert AUS, egal was in
// "flight_logbook" steht (verhindert unbemerktes Weiterlaufen z.B. nach
// einem Firmware-Update oder einer manuell bearbeiteten Datei).
// Safety rule: flight logbook may only stay active after a restart if
// a valid power-on timestamp is present - no timestamp means guaranteed
// OFF, regardless of what is in "flight_logbook" (prevents unnoticed
// continued operation e.g. after a firmware update or manually edited file).
if (flightLogbookOn && logbookEnabledAtEpoch == 0) {
flightLogbookOn = false;
}

View File

@@ -14,7 +14,7 @@ uint8_t rangeIndex();
bool displayInverted();
void setDisplayInverted(bool inverted);
// Display-Helligkeit in Prozent (Config::BRIGHTNESS_MIN_PERCENT..MAX_PERCENT).
// Display brightness in percent (Config::BRIGHTNESS_MIN_PERCENT..MAX_PERCENT).
uint8_t brightnessPercent();
void setBrightnessPercent(uint8_t percent);
@@ -30,17 +30,17 @@ uint8_t rangeIndex();
bool flightLogbookEnabled();
void setFlightLogbookEnabled(bool on);
// Unix-Zeitstempel (Sekunden), zu dem das Flugbuch zuletzt eingeschaltet
// wurde. 0 = unbekannt/nicht gesetzt. FlightLogbook::update() nutzt dies,
// um die Aufzeichnung nach genau 24 Stunden automatisch wieder
// auszuschalten (SD-Karten-Schutz, siehe Bestaetigungsdialog im Menue).
// Unix timestamp (seconds) when the flight logbook was last enabled.
// 0 = unknown/not set. FlightLogbook::update() uses this
// to automatically turn off recording after exactly 24 hours
// (SD card protection, see confirmation dialog in the menu).
uint32_t flightLogbookEnabledAtEpoch();
void setFlightLogbookEnabledAtEpoch(uint32_t epoch);
// Dateiname (ohne ".csv", z.B. "2026-08-06" oder "2026-08-06_2") der
// aktuell laufenden Flugbuch-Sitzung. "" = keine Sitzungsdatei
// hinterlegt (Flugbuch aus, oder naechste Aktivierung soll eine neue
// Datei anlegen). Siehe FlightLogbook::ensureSessionFile().
// Filename (without ".csv", e.g. "2026-08-06" or "2026-08-06_2") of the
// currently running flight logbook session. "" = no session file
// stored (logbook off, or next activation should create a new
// file). See FlightLogbook::ensureSessionFile().
String flightLogbookSessionFile();
void setFlightLogbookSessionFile(const String& label);
@@ -53,9 +53,9 @@ uint8_t rangeIndex();
bool nightDimmingEnabled();
void setNightDimmingEnabled(bool on);
// Ruhebildschirm bei Inaktivitaets-Timeout (siehe main.cpp) - AUS per
// Default, damit sich am bisherigen Verhalten (Backlight komplett aus)
// nichts aendert, wer es nicht explizit einschaltet.
// Screensaver during inactivity timeout (see main.cpp) - OFF by
// default, so the previous behavior (backlight completely off)
// does not change unless explicitly enabled.
bool screensaverEnabled();
void setScreensaverEnabled(bool on);
@@ -65,12 +65,12 @@ bool hideGroundVehicles();
bool ecoModeEnabled();
void setEcoModeEnabled(bool on);
// Sprache der Benutzeroberflaeche: 0=EN,1=DE,2=FR,3=TR,4=ES,5=IT.
// UI language: 0=EN,1=DE,2=FR,3=TR,4=ES,5=IT.
uint8_t language();
void setLanguage(uint8_t lang);
// Einheiten-Modus: 0=Auto (per IP-Standort geschaetzt), 1=Metrisch
// erzwingen, 2=Imperial (Fuss/Knoten/Meilen) erzwingen.
// Units mode: 0=Auto (estimated by IP location), 1=Metric
// forced, 2=Imperial (feet/knots/miles) forced.
uint8_t unitsMode();
void setUnitsMode(uint8_t mode);
}

View File

@@ -21,17 +21,17 @@ namespace {
tft.drawFastVLine(cx, cy - 80, 160, dim);
}
// Dreht einen Punkt (px, py) im lokalen Koordinatensystem der kleinen
// Deko-Symbole um angleDeg und verschiebt ihn nach (x, y).
// Rotates a point (px, py) in the local coordinate system of the small
// decorative symbols by angleDeg and translates it to (x, y).
void rotatePoint(int16_t x, int16_t y, float sinA, float cosA,
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);
}
// Kleines Flugzeug-Silhouette aus gefuellten Dreiecken (Rumpf, Tragflaechen,
// Leitwerk), analog zum grossen Flugzeug in der Mitte, aber schlichter und
// in beliebiger Richtung (angleDeg, 0 = Nase zeigt nach oben).
// Small airplane silhouette made of filled triangles (fuselage, wings,
// tail), analogous to the large airplane in the center, but simpler and
// in any direction (angleDeg, 0 = nose pointing up).
void drawMiniJet(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);
@@ -55,8 +55,8 @@ namespace {
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).
// Small helicopter: rotor circle with blades, filled fuselage, tail boom
// with tail fin. angleDeg determines flight direction (0 = nose up).
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);

View File

@@ -7,8 +7,8 @@ namespace SplashScreen {
void setStatusLine(TFT_eSPI& tft, uint8_t slot, const String& text, uint16_t color = TFT_WHITE);
// Blockiert, bis seit begin() mindestens MIN_DISPLAY_MS vergangen sind.
// Direkt vor dem Verlassen des Splash-Screens aufrufen. Zeichnet waehrend
// des Wartens dieselbe Sterne-Animation wie die Menues.
// Blocks until at least MIN_DISPLAY_MS have passed since begin().
// Call right before leaving the splash screen. Draws the same
// star animation as the menus while waiting.
void waitRemaining(TFT_eSPI& tft);
}

View File

@@ -136,9 +136,9 @@ void run(TFT_eSPI& tft) {
tft.print(I18n::t(StringId::LOADING));
FlightLogbook::DayEntry days[MAX_DAYS_QUERIED];
// listDaySummaries() statt listDays(): mehrere Sitzungs-Dateien am
// selben Kalendertag (z.B. nach erneutem Einschalten) sollen hier
// weiterhin als EIN Balken pro Tag zaehlen statt als mehrere.
// Using listDaySummaries() instead of listDays(): multiple session files
// on the same calendar day (e.g., after restarting) should still count
// as ONE bar per day instead of multiple.
uint8_t count = FlightLogbook::listDaySummaries(days, MAX_DAYS_QUERIED);
uint8_t barCount = (count > MAX_BARS) ? MAX_BARS : count;

View File

@@ -43,7 +43,7 @@ namespace {
constexpr int16_t ROW3_Y = 106;
constexpr int16_t ROW4_Y = 140;
constexpr int16_t UPTIME_Y = 174;
constexpr int16_t TOP_ALT_Y = 194; // zweizeilig wie drawStatRow (Label + Wert)
constexpr int16_t TOP_ALT_Y = 194; // two lines like drawStatRow (label + value)
}
void run(TFT_eSPI& tft) {
@@ -100,8 +100,8 @@ void run(TFT_eSPI& tft) {
if (topAlt.found) {
String csign = topAlt.callsign[0] ? String(topAlt.callsign) : "?";
// Respektiert jetzt die Einheiten-Einstellung (Menue > Einheiten)
// - vorher immer "(XXXX ft)", auch bei Metrisch eingestellt.
// Now respects the units setting (Menu > Units)
// - previously always "(XXXX ft)", even when metric was set.
char altBuf[24];
if (LocationManager::useMetricUnits()) {
snprintf(altBuf, sizeof(altBuf), "%s (%ldm)", csign.c_str(), (long)Units::feetToMeters((float)topAlt.altitudeFt));

View File

@@ -3,8 +3,8 @@
#include <TFT_eSPI.h>
namespace StatsScreen {
// Blockierend: zeigt einfache Statistiken aus dem Flight Logbook
// (heute gesehen, insgesamt gesehen, Anzahl Tage geloggt). Kehrt zurueck,
// sobald "Back" angetippt wird.
// Blocking: shows simple statistics from the Flight Logbook
// (seen today, total seen, number of days logged). Returns as soon
// as "Back" is tapped.
void run(TFT_eSPI& tft);
}

View File

@@ -15,11 +15,10 @@ namespace {
return n;
}
// Klassischer "Sunrise/Sunset Algorithm" (Almanac for Computers, 1990,
// weit verbreitete Formel). sunrise=true fuer Sonnenaufgang, false fuer
// Sonnenuntergang. Gibt false zurueck, wenn die Sonne an diesem Tag/
// Breitengrad ueberhaupt nicht auf-/untergeht (Polartag/-nacht) - dann
// ist outHour unveraendert.
// Classic "Sunrise/Sunset Algorithm" (Almanac for Computers, 1990,
// widely used formula). sunrise=true for sunrise, false for sunset.
// Returns false if the sun does not rise/set at all on this day/latitude
// (polar day/night) - then outHour is unchanged.
bool calc(double lat, double lon, int N, int32_t utcOffsetSeconds, bool sunrise, float& outHour) {
double lngHour = lon / 15.0;
double t = sunrise ? (N + ((6.0 - lngHour) / 24.0)) : (N + ((18.0 - lngHour) / 24.0));
@@ -34,9 +33,9 @@ namespace {
RA = fmod(RA, 360.0);
if (RA < 0) RA += 360.0;
// RA muss im selben 90-Grad-Quadranten wie L liegen (atan() liefert
// nur Werte im Bereich -90..90 Grad, muss also entsprechend
// "zurueckgeklappt" werden).
// RA must be in the same 90-degree quadrant as L (atan() only returns
// values in the range -90..90 degrees, so it must be "folded back"
// accordingly).
double lQuadrant = floor(L / 90.0) * 90.0;
double raQuadrant = floor(RA / 90.0) * 90.0;
RA = RA + (lQuadrant - raQuadrant);
@@ -45,10 +44,10 @@ namespace {
double sinDec = 0.39782 * sin(L * DEG2RAD);
double cosDec = cos(asin(sinDec));
constexpr double ZENITH = 90.833; // offizieller Zenit inkl. atm. Refraktion
constexpr double ZENITH = 90.833; // official zenith including atmospheric refraction
double cosH = (cos(ZENITH * DEG2RAD) - (sinDec * sin(lat * DEG2RAD))) / (cosDec * cos(lat * DEG2RAD));
if (cosH > 1.0 || cosH < -1.0) return false; // Polartag/-nacht
if (cosH > 1.0 || cosH < -1.0) return false; // polar day/night
double H = sunrise ? (360.0 - RAD2DEG * acos(cosH)) : (RAD2DEG * acos(cosH));
H = H / 15.0;
@@ -80,12 +79,12 @@ Result compute(double lat, double lon, int year, int month, int day, int32_t utc
bool setOk = calc(lat, lon, N, utcOffsetSeconds, false, sunset);
if (!riseOk || !setOk) {
// Polartag (Sonne geht nicht unter) vs. Polarnacht (Sonne geht nicht
// auf): grobe Naeherung ueber "hohe Sonne im Sommerhalbjahr der
// jeweiligen Hemisphaere = Polartag, sonst Polarnacht" - reicht fuer
// eine Nachtdimmung an den seltenen Orten/Tagen, an denen das
// ueberhaupt vorkommt, voellig aus.
bool northernSummerHalf = (N > 79 && N < 265); // grob Fruehlings-/Herbst-Tagundnachtgleiche
// Polar day (sun does not set) vs. polar night (sun does not rise):
// rough approximation via "high sun in the summer half of the
// respective hemisphere = polar day, otherwise polar night" -
// completely sufficient for night dimming on the rare locations/days
// where this occurs at all.
bool northernSummerHalf = (N > 79 && N < 265); // roughly spring/autumn equinox
bool polarDay = (lat > 0) == northernSummerHalf;
r.valid = true;
r.alwaysDay = polarDay;

View File

@@ -1,24 +1,24 @@
#pragma once
#include <Arduino.h>
// Berechnet Sonnenauf-/untergang (lokale Zeit) fuer einen gegebenen Ort und
// Tag - Grundlage fuer die standortbasierte Nachtdimmung (siehe main.cpp::
// isNightDimHours()), die das vorherige feste 22:00-06:00-Fenster ersetzt.
// Calculates sunrise/sunset (local time) for a given location and
// day - basis for the location-based night dimming (see main.cpp::
// isNightDimHours()), which replaces the previous fixed 22:00-06:00 window.
namespace SunTimes {
struct Result {
bool valid = false; // false = Standort noch unbekannt (lat/lon == 0,0)
bool alwaysDay = false; // Polartag: Sonne geht heute an diesem Ort nicht unter
bool alwaysNight = false; // Polarnacht: Sonne geht heute an diesem Ort nicht auf
float sunriseHour = 0; // Lokale Dezimalstunde (0-24), nur gueltig wenn weder
float sunsetHour = 0; // alwaysDay noch alwaysNight gesetzt ist.
bool valid = false; // false = location still unknown (lat/lon == 0,0)
bool alwaysDay = false; // polar day: sun does not set today at this location
bool alwaysNight = false; // polar night: sun does not rise today at this location
float sunriseHour = 0; // local decimal hour (0-24), only valid if neither
float sunsetHour = 0; // alwaysDay nor alwaysNight is set.
};
// year/month/day sind lokale Kalenderdatumswerte (z.B. aus localtime_r()),
// utcOffsetSeconds die bekannte Zeitzonenverschiebung (siehe
// LocationManager::utcOffsetSeconds()). Nutzt den klassischen "Almanac"-
// Sonnenauf-/untergangsalgorithmus (Sonnenzenit 90.833 Grad, beruecksichtigt
// atmosphaerische Refraktion) - Genauigkeit liegt typischerweise im Bereich
// weniger Minuten, fuer eine sanfte Backlight-Dimmung mehr als ausreichend.
// year/month/day are local calendar date values (e.g. from localtime_r()),
// utcOffsetSeconds is the known timezone offset (see
// LocationManager::utcOffsetSeconds()). Uses the classic "Almanac"
// sunrise/sunset algorithm (solar zenith 90.833 degrees, accounting for
// atmospheric refraction) - accuracy is typically within a few minutes,
// more than sufficient for smooth backlight dimming.
Result compute(double lat, double lon, int year, int month, int day, int32_t utcOffsetSeconds);
}

View File

@@ -26,11 +26,11 @@ namespace {
String onOff(bool on) { return I18n::t(on ? StringId::ON : StringId::OFF); }
// Einfacher Zeilenumbruch-Helfer, gleiches Prinzip wie in
// menu_screen.cpp (dort mit zusaetzlicher Scroll-Unterstuetzung, hier
// bewusst ohne - der Beschreibungstext ist kurz genug, dass Scrollen
// nicht noetig ist). draw=false liefert nur die Gesamthoehe, ohne etwas
// zu zeichnen (fuer die vorab-Platzberechnung des Zurueck-Buttons).
// Simple line wrap helper, same principle as in
// menu_screen.cpp (with additional scroll support there, here
// deliberately without - the description text is short enough that scrolling
// is not needed). draw=false returns only the total height, without drawing
// anything (for the advance space calculation of the Back button).
int16_t layoutWrapped(TFT_eSPI& tft, int16_t x, int16_t startY, int16_t maxWidth,
int16_t lineHeight, const String& text, bool draw) {
int16_t y = startY;
@@ -57,10 +57,10 @@ namespace {
return (int16_t)(y - startY);
}
// Schieberegler-Positionen: 1..Config::SCREEN_TIMEOUT_MAX_MINUTES
// Minuten (je eine Position pro Minute) plus eine zusaetzliche
// Endposition ganz rechts fuer "Nie" (kein Timeout) - intern weiterhin
// als 0 gespeichert, wie schon vor diesem Screen.
// Slider positions: 1..Config::SCREEN_TIMEOUT_MAX_MINUTES
// minutes (one position per minute) plus one additional
// end position at the far right for "Never" (no timeout) - internally stored
// as 0, as before this screen.
constexpr uint8_t STEP_COUNT = Config::SCREEN_TIMEOUT_MAX_MINUTES + 1;
uint8_t indexFromMinutes(uint8_t minutes) {
@@ -91,18 +91,18 @@ void run(TFT_eSPI& tft) {
constexpr int16_t TRACK_W = Config::SCREEN_WIDTH - 2 * TRACK_X;
constexpr int16_t TRACK_H = 6;
constexpr int16_t THUMB_R = 12;
// Grosszuegige vertikale Trefferzone rund um den duennen Regler-Strich -
// sonst muesste man beim Ziehen extrem praezise genau auf die paar
// Pixel der Linie treffen.
// Generous vertical hit zone around the thin slider track -
// otherwise you would have to hit the few pixels of the line
// extremely precisely when dragging.
constexpr int16_t TRACK_HIT_Y_MIN = TRACK_Y - 24;
constexpr int16_t TRACK_HIT_Y_MAX = TRACK_Y + 24;
Rect screensaverBtn = {10, 135, (int16_t)(Config::SCREEN_WIDTH - 20), 36};
// Beschreibungstext-Hoehe (haengt von der jeweils eingestellten Sprache
// ab) einmalig vorab messen, damit der Zurueck-Button IMMER direkt
// darunter landet - nie mit dem Text ueberlappend und nie unnoetig viel
// Leerraum lassend. Gleiches Prinzip wie bei confirmWarningScreen()/
// Description text height (depends on the selected language)
// measured once in advance, so the Back button ALWAYS lands directly
// below it - never overlapping the text and never leaving unnecessarily
// much empty space. Same principle as confirmWarningScreen()/
// infoScreen() in menu_screen.cpp.
constexpr int16_t DESC_Y = 185;
constexpr int16_t DESC_LINE_H = 14;
@@ -110,9 +110,9 @@ void run(TFT_eSPI& tft) {
int16_t descH = layoutWrapped(tft, 10, DESC_Y, DESC_MAX_WIDTH, DESC_LINE_H,
I18n::t(StringId::TIMEOUT_SCREENSAVER_DESC), false);
int16_t backY = (int16_t)(DESC_Y + descH + 10);
// Sicherheitsnetz, falls eine Uebersetzung doch mal laenger ausfaellt,
// als der verfuegbare Platz hergibt - der Zurueck-Button bleibt so
// IMMER erreichbar, statt vom Bildschirmrand abgeschnitten zu werden.
// Safety net, if a translation ever turns out longer
// than the available space allows - the Back button stays
// ALWAYS reachable, instead of being cut off at the screen edge.
constexpr int16_t BACK_Y_MAX = Config::SCREEN_HEIGHT - 46;
if (backY > BACK_Y_MAX) backY = BACK_Y_MAX;
Rect backBtn = {10, backY, (int16_t)(Config::SCREEN_WIDTH - 20), 36};
@@ -174,19 +174,19 @@ void run(TFT_eSPI& tft) {
}
dragging = true;
} else if (dragging && !live.touched) {
// Loslassen: jetzt erst dauerhaft speichern (nicht bei jeder
// Zwischenposition waehrend des Ziehens selbst, um unnoetig
// viele SD-Kartenschreibvorgaenge zu vermeiden).
// Release: now save persistently (not during every
// intermediate slider position while dragging, to avoid unnecessary
// SD card writes).
SettingsStore::setScreenTimeoutMinutes(minutesFromIndex(index));
dragging = false;
}
// wasTapped() IMMER aufrufen (auch waehrend des Ziehens), damit ihr
// interner Loslassen-Erkennungszustand synchron bleibt - nur die
// AUSWERTUNG des Ergebnisses wird waehrend des Ziehens unterdrueckt.
// Wuerde man den Aufruf selbst waehrend des Ziehens auslassen,
// koennte beim Loslassen faelschlich ein "Tap" mit einer veralteten
// Position (von VOR Beginn des Ziehens) gemeldet werden.
// ALWAYS call wasTapped() (even while dragging), so its
// internal release detection state stays in sync - only the
// EVALUATION of the result is suppressed while dragging.
// If the call itself were omitted while dragging,
// releasing could incorrectly report a "Tap" with a stale
// position (from BEFORE the drag started).
TouchInput::Point tap;
bool tapped = TouchInput::wasTapped(tap);
if (!dragging && tapped) {

View File

@@ -2,12 +2,12 @@
#include <Arduino.h>
#include <TFT_eSPI.h>
// Bildschirm-Timeout einstellen (Menue > System > Bildschirm-Timeout) -
// Schieberegler von Config::SCREEN_TIMEOUT_MIN_MINUTES bis
// Config::SCREEN_TIMEOUT_MAX_MINUTES, danach "Nie" als eigene Endposition.
// Der Ruhebildschirm-Umschalter (siehe SettingsStore::screensaverEnabled())
// lebt ebenfalls hier, direkt unter dem Regler, da er inhaltlich eng mit
// dem Timeout zusammenhaengt (bestimmt nur, WAS beim Timeout passiert).
// Screen timeout setting (Menu > System > Screen Timeout) -
// Slider from Config::SCREEN_TIMEOUT_MIN_MINUTES to
// Config::SCREEN_TIMEOUT_MAX_MINUTES, then "Never" as a separate end position.
// The screensaver toggle (see SettingsStore::screensaverEnabled())
// also lives here, directly below the slider, since it is closely related to
// the timeout (only determines WHAT happens on timeout).
namespace TimeoutScreen {
void run(TFT_eSPI& tft);
}

View File

@@ -10,11 +10,11 @@ namespace {
SPIClass touchSpi(VSPI);
XPT2046_Touchscreen touch(Config::TOUCH_CS_PIN, Config::TOUCH_IRQ_PIN);
constexpr int16_t MARGIN_PX = 24; // Abstand der Kalibrierungspunkte vom Rand
constexpr int16_t MARGIN_PX = 24; // Distance of calibration points from the edge
// Rohe Min/Max-Werte aus der Kalibrierung. Bis zur ersten Kalibrierung
// grosszuegige Standardwerte, damit die Kalibrierungs-Routine selbst schon
// grob nutzbare (wenn auch ungenaue) Koordinaten bekommt.
// Raw min/max values from calibration. Until first calibration,
// generous default values so the calibration routine itself already gets
// roughly usable (though inaccurate) coordinates.
int16_t calXmin = 200;
int16_t calXmax = 3900;
int16_t calYmin = 200;
@@ -116,7 +116,7 @@ bool wasTapped(Point& outPoint) {
bool fired = false;
if (!cur.touched && lastTouched) {
// Loslassen erkannt -> Tap an der zuletzt bekannten Position melden.
// Release detected -> report tap at last known position.
outPoint = lastRaw;
fired = true;
}

View File

@@ -11,22 +11,22 @@ namespace TouchInput {
void begin();
// Laedt calibration.txt von der SD-Karte. Gibt false zurueck, wenn keine
// vorhanden oder ungueltig ist.
// Loads calibration.txt from the SD card. Returns false if none exists
// or it is invalid.
bool loadCalibration();
void setCalibration(int16_t rawXmin, int16_t rawXmax, int16_t rawYmin, int16_t rawYmax);
void saveCalibration();
bool hasCalibration();
// Aktueller Touch-Zustand, roh (unkalibriert), z.B. fuer die
// Kalibrierungs-Routine selbst.
// Current touch state, raw (uncalibrated), e.g. for the calibration
// routine itself.
Point rawPoint();
// Aktueller Touch-Zustand, auf Bildschirmkoordinaten (0..SCREEN_WIDTH-1 /
// 0..SCREEN_HEIGHT-1) umgerechnet und geclampt.
// Current touch state, converted to screen coordinates (0..SCREEN_WIDTH-1 /
// 0..SCREEN_HEIGHT-1) and clamped.
Point mappedPoint();
// Liefert genau einmal pro physischer Beruehrung "true" (beim Loslassen),
// mitsamt der zuletzt bekannten Position. Fuer Buttons/Tastatur gedacht.
// Returns "true" exactly once per physical touch (on release), along with
// the last known position. Intended for buttons/keyboard use.
bool wasTapped(Point& outPoint);
}

View File

@@ -1,7 +1,7 @@
#pragma once
// Automatisch generierter Font (DejaVu Sans Mono, 11px) fuer TFT_eSPI GFXFF.
// Deckt Basis-ASCII + Umlaute/Akzente fuer DE/TR/FR/ES/IT ab (U+0020-U+015F).
// NICHT von Hand bearbeiten - siehe Kommentar in i18n.h fuer den Generator-Hinweis.
// Auto-generated font (DejaVu Sans Mono, 11px) for TFT_eSPI GFXFF.
// Covers basic ASCII + umlauts/accents for DE/TR/FR/ES/IT (U+0020-U+015F).
// DO NOT edit manually - see comment in i18n.h for the generator hint.
#include <pgmspace.h>
const uint8_t UiFont11ptBitmaps[] PROGMEM = {

View File

@@ -19,9 +19,9 @@ namespace {
double lastLon = 0;
bool hasLastLocation = false;
// Ordnet den WMO-Wettercode von Open-Meteo (Feld "weathercode", siehe
// https://open-meteo.com/en/docs) einer der wenigen Icon-Kategorien zu,
// die main.cpp zeichnen kann.
// Maps the WMO weather code from Open-Meteo (field "weathercode", see
// https://open-meteo.com/en/docs) to one of the few icon categories
// that main.cpp can draw.
Condition conditionFromWmoCode(int code) {
if (code == 0) return Condition::Clear;
if (code == 1 || code == 2) return Condition::PartlyCloudy;
@@ -56,10 +56,10 @@ namespace {
return;
}
// Body erst komplett als String einsammeln (getString() kuemmert
// sich zuverlaessig um Chunked-Transfer-Encoding), statt direkt aus
// http.getStream() zu parsen - Letzteres scheiterte bei Open-Meteo
// zuverlaessig mit einem ArduinoJson-"InvalidInput"-Fehler.
// Collect body as a complete string first (getString() reliably handles
// chunked transfer encoding) instead of parsing directly from
// http.getStream() - the latter reliably failed with Open-Meteo
// with an ArduinoJson "InvalidInput" error.
String body = http.getString();
http.end();
@@ -79,10 +79,9 @@ void update() {
LocationManager::getHomeLocation(lat, lon);
if (lat == 0 && lon == 0) return;
// Deutliche Standort-Aenderung (z.B. anderes Standort-Preset aktiviert)
// - sofort neu abfragen statt bis zum naechsten regulaeren Intervall zu
// warten, damit das Icon nicht minutenlang das Wetter des alten
// Standorts zeigt.
// Significant location change (e.g., another location preset activated)
// - query immediately instead of waiting for the next regular interval,
// so the icon does not show the old location's weather for minutes.
bool locationChanged = !hasLastLocation ||
fabs(lat - lastLat) > 0.01 || fabs(lon - lastLon) > 0.01;

View File

@@ -1,15 +1,15 @@
#pragma once
#include <Arduino.h>
// Sehr einfache Wetteranzeige (Icon im Header, dort wo frueher der
// Cam-Button war) - fragt periodisch die aktuelle Wetterlage fuer den
// gerade aktiven Standort ab (also inkl. aktivem Standort-Preset, siehe
// LocationManager::getHomeLocation() - wenn dort z.B. Mailand ausgewaehlt
// ist, zeigt das Icon das Wetter in Mailand). Nutzt die kostenlose
// Open-Meteo-API (kein API-Key noetig).
// Very simple weather display (icon in the header, where the camera button
// used to be) - periodically queries the current weather for the currently
// active location (including active location preset, see
// LocationManager::getHomeLocation() - if e.g. Milan is selected there,
// the icon shows the weather in Milan). Uses the free Open-Meteo API (no
// API key needed).
namespace Weather {
enum class Condition {
Unknown, // noch keine erfolgreiche Abfrage - Icon zeigt nichts an
Unknown, // no successful query yet - icon shows nothing
Clear,
PartlyCloudy,
Cloudy,
@@ -18,16 +18,14 @@ namespace Weather {
Thunderstorm
};
// Muss regelmaessig aus dem NetTask (Core 0) aufgerufen werden - kuemmert
// sich intern um das Abfrage-Intervall (Config::WEATHER_FETCH_INTERVAL_MS)
// und erkennt eine Standort-Aenderung (z.B. anderes aktives Preset), um
// dann sofort neu abzufragen statt bis zum naechsten reguleren Intervall
// zu warten.
// Must be called regularly from NetTask (Core 0) - handles the query
// interval internally (Config::WEATHER_FETCH_INTERVAL_MS) and detects
// location changes (e.g. different active preset) to immediately re-query
// instead of waiting for the next regular interval.
void update();
// Aktuell bekannte Wetterlage, threadsicher genug fuer diesen Zweck (ein
// einzelnes uint8-artiges Enum, das nur vom NetTask geschrieben und vom
// UI-Thread gelesen wird - kein Lock noetig, ein kurzfristig veraltetes
// Lesen ist unkritisch).
// Currently known weather condition, thread-safe enough for this purpose (a
// single uint8-like enum, only written by NetTask and read by the UI thread
// - no lock needed, a briefly stale read is harmless).
Condition current();
}

View File

@@ -19,9 +19,9 @@ namespace {
WebServer server(80);
// Nur reine Dateinamen/Labels aus Formularfeldern akzeptieren - kein
// "/" und kein ".." - damit ueber die WebUI kein Ausbruch aus dem
// jeweiligen SD-Verzeichnis moeglich ist (Pfad-Traversal).
// Only accept plain filenames/labels from form fields - no
// "/" and no ".." - so that no escape from the respective
// SD directory is possible via the WebUI (path traversal).
bool isSafeName(const String& name) {
if (name.length() == 0 || name.length() > 40) return false;
if (name.indexOf('/') >= 0 || name.indexOf('\\') >= 0) return false;
@@ -29,14 +29,14 @@ namespace {
return true;
}
// Absichtliche Duplikate der gleichnamigen (lokalen/statischen) Funktionen
// aus radar_screen.cpp - dort nicht exportiert, und nach der im Projekt
// etablierten Konvention "jeder Screen/jedes Modul dupliziert seine
// eigenen kleinen Helfer statt eines gemeinsamen Moduls" (siehe z.B.
// timeout_screen.cpp) bewusst hier erneut definiert statt radar_screen.cpp
// umzubauen. Bei Aenderungen an der Logik in radar_screen.cpp bitte diese
// Kopie hier synchron halten, damit das WebUI-Radar dieselben Ringe/
// Markierungen zeigt wie das Geraete-Display.
// Intentional duplicates of the same-named (local/static) functions
// from radar_screen.cpp - not exported there, and following the project's
// established convention "each screen/module duplicates its own small
// helpers instead of a shared module" (see e.g. timeout_screen.cpp),
// deliberately redefined here instead of refactoring radar_screen.cpp.
// If the logic in radar_screen.cpp changes, please keep this copy
// in sync so that the WebUI radar shows the same rings/markers as the
// device display.
bool isEmergencySquawkWeb(const char* squawk) {
if (!squawk[0]) return false;
for (uint8_t i = 0; i < Config::EMERGENCY_SQUAWK_COUNT; i++) {
@@ -45,15 +45,15 @@ namespace {
return false;
}
// "Auffaellig" (oranger Ring) beschraenkt sich aktuell auf gar nichts -
// der Teil ueber Militaer-/Regierungs-Rufzeichen-Praefixe
// (Config::NOTABLE_CALLSIGN_PREFIXES) ist NICHT umgesetzt, weil diese
// Konstante nirgends im Projekt existiert (auch nicht in
// radar_screen.cpp) - das zugehoerige Feature wurde bisher nie
// tatsaechlich spezifiziert/implementiert. "notable" wird deshalb unten
// in handleRadarJson() fest auf false gesetzt. Sobald es eine echte
// Praefixliste gibt, hier eine isNotableCallsignWeb()-Funktion analog zu
// isEmergencySquawkWeb() ergaenzen.
// "Notable" (orange ring) is currently limited to nothing at all -
// the part about military/government callsign prefixes
// (Config::NOTABLE_CALLSIGN_PREFIXES) is NOT implemented, because this
// constant does not exist anywhere in the project (not even in
// radar_screen.cpp) - the associated feature has never actually been
// specified/implemented. "notable" is therefore hardcoded to false
// below in handleRadarJson(). As soon as there is a real prefix list,
// add an isNotableCallsignWeb() function analogous to
// isEmergencySquawkWeb() here.
bool isHeavyCategoryWeb(const char* category) {
return category[0] == 'A' && category[1] == '5';
}
@@ -73,10 +73,9 @@ namespace {
html += "button{background:#0a0f0d;color:#ff3b3b;border:1px solid #ff3b3b;border-radius:4px;padding:3px 10px;font-family:inherit;cursor:pointer;}";
html += "button:hover{background:#ff3b3b;color:#0a0f0d;}";
html += ".dl{color:#39ff14;text-decoration:none;border:1px solid #39ff14;border-radius:4px;padding:3px 10px;margin-right:6px;display:inline-block;}";
// Gruen statt Rot fuer "Hinzufuegen"-Buttons - die roten button{}-Regeln
// oben bleiben fuer alle destruktiven "Entfernen/Loeschen"-Buttons
// unveraendert, .addbtn ist ausschliesslich fuer die neuen Listen-
// Formulare (siehe handleLists()) gedacht.
// Green instead of red for "Add" buttons - the red button{} rules
// above stay unchanged for all destructive "Remove/Delete" buttons.
// .addbtn is exclusively for the new list forms (see handleLists()).
html += ".addbtn{background:#0a0f0d;color:#39ff14;border:1px solid #39ff14;border-radius:4px;padding:3px 10px;font-family:inherit;cursor:pointer;}";
html += ".addbtn:hover{background:#39ff14;color:#0a0f0d;}";
html += "input[type=text]{background:#0a0f0d;color:#39ff14;border:1px solid #39ff14;border-radius:4px;padding:5px 8px;font-family:inherit;}";
@@ -92,19 +91,19 @@ namespace {
return html;
}
// Live-Radar-Ansicht fuer die Startseite: ein <canvas>, das per JavaScript
// alle paar Sekunden /radar.json abruft und die Flugzeuge polar (Peilung/
// Distanz, genau wie auf dem Geraete-Display) zeichnet. Bewusst per
// fetch()-Polling statt WebSocket/SSE gehalten - deutlich weniger Code
// und Speicherbedarf auf dem ESP32, und fuer eine gelegentlich vom Handy
// aus aufgerufene Seite voellig ausreichend.
// Live radar view for the start page: a <canvas> that uses JavaScript
// to fetch /radar.json every few seconds and draw the aircraft polar
// (bearing/distance, exactly like on the device display). Intentionally
// kept with fetch()-polling instead of WebSocket/SSE - significantly less
// code and memory usage on the ESP32, and perfectly adequate for a page
// occasionally opened from a phone.
//
// Der Reichweiten-Waehler (<select>) ist rein clientseitig/pro Seiten-
// aufruf - er aendert NICHT die Geraete-Einstellung (SettingsStore::
// rangeIndex()), sondern wird als "range_km"-Query-Parameter an
// /radar.json mitgeschickt (siehe handleRadarJson()) und erlaubt so ein
// unabhaengiges Herein-/Herauszoomen auf dem Handy, ohne das Geraete-
// Display zu beeinflussen. Default ist die aktuelle Geraete-Reichweite.
// The range selector (<select>) is purely client-side/per page view -
// it does NOT change the device setting (SettingsStore::
// rangeIndex()), but is sent as a "range_km" query parameter to
// /radar.json (see handleRadarJson()) and allows independent zooming
// in/out on the phone without affecting the device display. Default is
// the current device range.
void appendRadarSection(String& html) {
float deviceRangeKm = Config::RANGE_STEPS_KM[SettingsStore::rangeIndex()];
@@ -131,10 +130,10 @@ namespace {
html += "var markers=[];";
html += "var selectedHex=null;";
// Hintergrund-Sterne AUSSERHALB des Radarkreises - gleiches Prinzip
// wie updateBgStars()/initBgStarsIfNeeded() in radar_screen.cpp
// (Rejection-Sampling, damit kein Stern innerhalb des Kreises
// landet), hier per Canvas/JS statt TFT_eSPI nachgebaut.
// Background stars OUTSIDE the radar circle - same principle
// as updateBgStars()/initBgStarsIfNeeded() in radar_screen.cpp
// (rejection sampling, so no star lands inside the circle),
// replicated here via Canvas/JS instead of TFT_eSPI.
html += "var stars=[];";
html += "(function(){var minDistSq=(R+6)*(R+6);for(var i=0;i<24;i++){var x,y,tries=0;";
html += "do{x=4+Math.random()*(W-8);y=4+Math.random()*(H-8);tries++;}";
@@ -155,9 +154,9 @@ namespace {
html += "for(var ring=1;ring<=3;ring++){var r=R*ring/3;ctx.beginPath();ctx.arc(cx,cy,r,0,Math.PI*2);ctx.stroke();";
html += "ctx.fillText(Math.round(data.range_km*ring/3)+' km',cx+4,cy-r+10);}";
html += "ctx.strokeStyle='#12261a';ctx.beginPath();ctx.moveTo(cx-R,cy);ctx.lineTo(cx+R,cy);ctx.moveTo(cx,cy-R);ctx.lineTo(cx,cy+R);ctx.stroke();";
// Alle vier Himmelsrichtungen (N/E/S/W), genau wie
// drawStaticBackground() in radar_screen.cpp - vorher stand hier nur
// "N", was auf Nachfrage ergaenzt wurde.
// All four cardinal directions (N/E/S/W), exactly like
// drawStaticBackground() in radar_screen.cpp - previously only
// "N" was shown here, which was expanded upon request.
html += "ctx.fillStyle='#39ff14';ctx.textAlign='center';ctx.fillText('N',cx,cy-R-8);";
html += "ctx.fillText('S',cx,cy+R+16);";
html += "ctx.textAlign='left';ctx.fillText('E',cx+R+4,cy+3);";
@@ -180,26 +179,26 @@ namespace {
html += "if(a.emergency){ctx.strokeStyle='#ff3b3b';ctx.beginPath();ctx.arc(x,y,9,0,Math.PI*2);ctx.stroke();}";
html += "else if(a.watched){ctx.strokeStyle='#00e5ff';ctx.beginPath();ctx.arc(x,y,9,0,Math.PI*2);ctx.stroke();}";
html += "else if(a.notable){ctx.strokeStyle='#ff9f1a';ctx.beginPath();ctx.arc(x,y,9,0,Math.PI*2);ctx.stroke();}";
// Ausgewaehltes Flugzeug (per Klick/Tap, siehe unten) bekommt einen
// weissen Auswahlring, gleiches Prinzip wie isSelected auf dem
// Geraete-Display (radar_screen.cpp render()).
// Selected aircraft (by click/tap, see below) gets a
// white selection ring, same principle as isSelected on the
// device display (radar_screen.cpp render()).
html += "if(a.hex===selectedHex){ctx.strokeStyle='#ffffff';ctx.beginPath();ctx.arc(x,y,11,0,Math.PI*2);ctx.stroke();}";
html += "ctx.fillStyle=color;ctx.textAlign='center';ctx.fillText(a.callsign,x,y-8);";
html += "markers.push({x:x,y:y,a:a});";
html += "});";
html += "status.textContent=(data.aircraft||[]).length+' aircraft \\u00b7 range '+data.range_km+' km';";
// Falls das ausgewaehlte Flugzeug in dieser Aktualisierung nicht
// mehr vorkommt (z.B. aus der Reichweite geflogen), Infobox wieder
// schliessen statt veraltete Daten stehen zu lassen.
// If the selected aircraft no longer appears in this update
// (e.g. flew out of range), close the info box instead of
// showing stale data.
html += "if(selectedHex){var found=markers.filter(function(m){return m.a.hex===selectedHex;})[0];";
html += "if(found){showInfo(found.a);}else{selectedHex=null;hideInfo();}}";
html += "}";
// Info-Panel fuer ein angetipptes Flugzeug - bewusst eine eigene,
// stehenbleibende Box (kein Tooltip/Popup, das beim naechsten
// Neuzeichnen einfach verschwindet), mit explizitem Schliessen-Link,
// gleiches Grundprinzip wie infoScreen() am Geraet: der Nutzer soll
// aktiv entscheiden, wann die Info wieder verschwindet.
// Info panel for a tapped aircraft - intentionally a dedicated,
// persistent box (not a tooltip/popup that disappears on the next
// redraw), with an explicit close link, same basic principle as
// infoScreen() on the device: the user should actively decide when
// the info disappears.
html += "function fmtNum(n,d){return (typeof n==='number')?n.toFixed(d):'?';}";
html += "function showInfo(a){";
html += "var lines=[];";
@@ -233,11 +232,11 @@ namespace {
html += "}";
html += "poll();";
html += "setInterval(poll,3000);";
// Schnellerer, rein lokaler Redraw-Takt (alle 150ms, ohne Netzwerk-
// Anfrage) nur fuer das Sternenfunkeln + die Auswahlmarkierung -
// gleiches Grundprinzip wie tick() vs. render() auf dem
// Geraete-Display: Flugzeugpositionen aktualisieren sich weiterhin
// nur alle 3s per poll(), die Sterne twinkeln aber fluessig dazwischen.
// Faster, purely local redraw interval (every 150ms, without network
// request) only for star twinkling + the selection marker -
// same basic principle as tick() vs. render() on the
// device display: aircraft positions still update
// only every 3s via poll(), but stars twinkle smoothly in between.
html += "setInterval(function(){draw(lastData);},150);";
html += "})();</script>";
}
@@ -297,12 +296,12 @@ namespace {
if (line.length() == 0) continue;
server.sendContent(String(days[i].date) + "," + line + "\n");
// Wichtig: Ohne regelmaessiges Abgeben der CPU haengt dieser
// Task (Core 0, Prioritaet 1) die Idle-Task aus, die den
// Task-Watchdog fuettert. Bei groesseren Logbuechern fuehrt
// das nach ca. 5s ohne Yield zu einem Watchdog-Reset -
// genau der Reboot mitten im CSV-Download, den der Nutzer
// beobachtet hat. delay(1) erzwingt einen Kontextwechsel.
// Important: Without regularly yielding the CPU, this
// task (Core 0, priority 1) starves the idle task that feeds
// the task watchdog. With larger logbooks, this causes a
// watchdog reset after approx. 5s without yield -
// exactly the reboot during CSV download that the user
// observed. delay(1) forces a context switch.
if (++linesSent % 10 == 0) {
delay(1);
}
@@ -348,14 +347,14 @@ namespace {
server.send(303);
}
// Verwaltung von Airline-Filter und Beobachtungsliste per Browser -
// beide Backend-Module (AirlineFilter/AircraftWatchlist) sind seit
// dieser Erweiterung mutex-geschuetzt (siehe dort), da sie jetzt sowohl
// von Core 1 (Radar-/Menue-Screens) als auch von hier aus - Core 0,
// WebExportServer laeuft innerhalb von NetTask - aufgerufen werden.
// Praktisch vor allem fuer laengere Eingaben (Rufzeichen, ICAO-Codes),
// die sich per Handy-Tastatur deutlich bequemer eintippen lassen als
// ueber die kleine Bildschirmtastatur des Geraets.
// Management of airline filter and watchlist via browser -
// both backend modules (AirlineFilter/AircraftWatchlist) are now
// mutex-protected (see there), since they are called both
// from Core 1 (radar/menu screens) and from here - Core 0,
// WebExportServer runs within NetTask.
// Practically most useful for longer inputs (callsigns, ICAO codes)
// that are much easier to type on a phone keyboard than
// via the device's small on-screen keyboard.
void handleLists() {
String html = htmlHeader("Leos Flightradar - Lists");
@@ -449,19 +448,18 @@ namespace {
server.send(303);
}
// Datenquelle fuer das Live-Radar auf der Startseite (siehe
// appendRadarSection()). Wendet dieselben Filter/Prioritaeten an wie das
// Geraete-Display (render() in radar_screen.cpp): Reichweite, "Boden-
// fahrzeuge ausblenden", Airline-Filter, sowie Notfall/Beobachtungsliste/
// "auffaellig" als sich gegenseitig ausschliessende Ring-Markierungen in
// genau dieser Prioritaet.
// Data source for the live radar on the start page (see
// appendRadarSection()). Applies the same filters/priorities as the
// device display (render() in radar_screen.cpp): range, "hide ground
// vehicles", airline filter, and emergency/watchlist/"notable" as
// mutually exclusive ring markers in exactly this priority.
void handleRadarJson() {
float rangeKm = Config::RANGE_STEPS_KM[SettingsStore::rangeIndex()];
// Erlaubt der Web-Ansicht ein eigenes, unabhaengiges Zoomen (siehe
// Reichweiten-Waehler in appendRadarSection()), OHNE die Geraete-
// Einstellung zu veraendern. Nur einen der bekannten
// Config::RANGE_STEPS_KM-Werte akzeptieren - ein fehlender oder
// nicht erkannter Parameter faellt auf die Geraete-Reichweite zurueck.
// Allows the web view its own independent zooming (see
// range selector in appendRadarSection()), WITHOUT changing the
// device setting. Only accept one of the known
// Config::RANGE_STEPS_KM values - a missing or
// unknown parameter falls back to the device range.
if (server.hasArg("range_km")) {
float requested = server.arg("range_km").toFloat();
for (uint8_t i = 0; i < Config::RANGE_STEP_COUNT; i++) {
@@ -494,13 +492,13 @@ namespace {
bool isHeavy = isHeavyCategoryWeb(a.category);
bool isEmergency = emergencyOn && isEmergencySquawkWeb(a.squawk);
bool isWatched = watchOn && AircraftWatchlist::isWatched(a.callsign);
// "notable" (oranger Ring) ist fuer auffaellige Rufzeichen
// (Militaer/Regierung) reserviert - Heavy-Flugzeuge bekommen
// stattdessen die eigene Markerform (siehe "heavy" oben). Es
// gibt aktuell aber keine Militaer-/Regierungs-Praefixliste im
// Projekt (auch nicht am Geraete-Display, siehe
// radar_screen.cpp) - "notable" bleibt daher bis auf Weiteres
// immer false.
// "notable" (orange ring) is reserved for conspicuous callsigns
// (military/government) - heavy aircraft instead get their
// own marker shape (see "heavy" above). However, there is
// currently no military/government prefix list in the
// project (not even on the device display, see
// radar_screen.cpp) - "notable" therefore remains
// always false for now.
bool isNotable = false;
JsonObject o = arr.add<JsonObject>();
@@ -516,10 +514,10 @@ namespace {
o["emergency"] = isEmergency;
o["watched"] = isWatched;
o["notable"] = isNotable;
// Zusaetzliche Felder nur fuer das Info-Panel bei Klick/Tap auf
// ein Flugzeug (siehe showInfo() in appendRadarSection()) - beide
// stehen bereits verlustfrei im Aircraft-Snapshot, kein
// zusaetzlicher Netzwerk-/SD-Zugriff noetig.
// Additional fields only for the info panel on click/tap on
// an aircraft (see showInfo() in appendRadarSection()) - both
// are already losslessly in the Aircraft snapshot, no
// additional network/SD access needed.
o["speed_kt"] = a.groundSpeedKt;
o["squawk"] = a.squawk;
}

View File

@@ -63,13 +63,12 @@ void run(TFT_eSPI& tft) {
constexpr int16_t LINE_H = 16;
constexpr int16_t VIEW_TOP = 36;
// Wenn WLAN verbunden ist, bekommt der Absatztext nur noch ein kleines
// (bei Bedarf scrollbares) Fenster, weil darunter fest der QR-Code samt
// IP-Adresse Platz braucht (siehe Alex' Feedback: der vorherige separate
// "QR"-Button oben rechts sah seltsam aus - jetzt wird der Code direkt
// hier gezeigt, mittig, mit der IP-Adresse mittig darunter). Ohne WLAN
// gibt es keinen QR-Code, der Text bekommt dann wie vorher die volle
// Bildschirmhoehe.
// When WiFi is connected, the paragraph text gets only a small
// (scrollable if needed) window, because the QR code plus IP address
// need fixed space below (see Alex' feedback: the previous separate
// "QR" button at top right looked strange - now the code is shown
// directly here, centered, with the IP address centered below). Without
// WiFi there is no QR code, the text gets full screen height as before.
constexpr int16_t TEXT_VIEW_BOTTOM_CONNECTED = 84;
constexpr int16_t TEXT_VIEW_BOTTOM_DISCONNECTED = Config::SCREEN_HEIGHT - 60;
@@ -77,16 +76,15 @@ void run(TFT_eSPI& tft) {
String urlLine = wifiConnected ? ("http://" + WiFi.localIP().toString() + "/") : String();
int16_t viewBottom = wifiConnected ? TEXT_VIEW_BOTTOM_CONNECTED : TEXT_VIEW_BOTTOM_DISCONNECTED;
// QR-Code einmalig erzeugen (die URL aendert sich waehrend dieser
// Bildschirm offen ist nicht). Version 4 (33x33 Module) reicht mit
// deutlicher Reserve fuer eine "http://<lokale-IP>/"-URL (max. rund 24
// Zeichen) - ECC_LOW statt eines hoeheren Fehlerkorrektur-Levels, um die
// Module moeglichst gross (und damit leicht scannbar) darstellen zu
// koennen.
// Generate QR code once (URL doesn't change while this screen
// is open). Version 4 (33x33 modules) is sufficient with plenty of
// headroom for a "http://<local-IP>/" URL (max. ~24 chars) - ECC_LOW
// instead of a higher error correction level, to make the modules as
// large (and thus easily scannable) as possible.
constexpr uint8_t QR_VERSION = 4;
constexpr int16_t QR_SIZE_MODULES = 33; // Version 4: 4*4+17 = 33
constexpr int16_t QR_BLOCK = 4;
constexpr int16_t QR_QUIET = 2; // Ruhezone in Modulen rundherum, Scanner brauchen etwas Rand
constexpr int16_t QR_QUIET = 2; // Quiet zone in modules around it, scanners need some margin
constexpr int16_t QR_PIXEL_SIZE = (QR_SIZE_MODULES + 2 * QR_QUIET) * QR_BLOCK;
constexpr int16_t QR_X = (Config::SCREEN_WIDTH - QR_PIXEL_SIZE) / 2;
constexpr int16_t QR_GAP = 6;
@@ -99,9 +97,9 @@ void run(TFT_eSPI& tft) {
qrcode_initText(&qrcode, qrData, QR_VERSION, ECC_LOW, urlLine.c_str());
}
// Gesamthoehe des Absatztextes vorab berechnen (draw=false), um zu
// wissen, ob Scroll-Pfeile gebraucht werden - muss exakt zur
// Zeichenreihenfolge in redraw() unten passen.
// Pre-calculate total height of paragraph text (draw=false) to know
// whether scroll arrows are needed - must exactly match the drawing
// order in redraw() below.
int16_t totalH = VIEW_TOP;
totalH = layoutWrapped(tft, 10, totalH, textMaxWidth, LINE_H, I18n::t(StringId::WEBUI_INFO_PARA1), 0, 0, 0, false);
if (!wifiConnected) {
@@ -136,9 +134,9 @@ void run(TFT_eSPI& tft) {
}
if (wifiConnected) {
// QR-Code + IP-Adresse stehen fest unterhalb des (ggf.
// scrollbaren) Textes, unabhaengig von scrollY - beides mittig
// zentriert, direkt ueber dem Zurueck-Button.
// QR code + IP address are fixed below the (possibly scrollable)
// text, independent of scrollY - both centered, directly above
// the back button.
tft.fillRect(QR_X, QR_Y, QR_PIXEL_SIZE, QR_PIXEL_SIZE, TFT_WHITE);
for (uint8_t my = 0; my < qrcode.size; my++) {
for (uint8_t mx = 0; mx < qrcode.size; mx++) {

View File

@@ -3,9 +3,9 @@
#include <TFT_eSPI.h>
namespace WebUiScreen {
// Blockierend: zeigt die WLAN-IP und eine Erklaerung der kleinen
// eingebauten Webseite (Flugbuch ansehen/herunterladen/loeschen,
// Screenshots ansehen/herunterladen/loeschen). Kehrt zurueck, sobald
// "Zurueck" angetippt wurde.
// Blocking: shows the WiFi IP and an explanation of the small
// built-in web page (view logbook / download / delete,
// view screenshots / download / delete). Returns as soon as
// "Back" is tapped.
void run(TFT_eSPI& tft);
}

View File

@@ -3,9 +3,9 @@
#include <TFT_eSPI.h>
namespace WifiManageScreen {
// Blockierend: zeigt die bis zu 3 gespeicherten WLAN-Netzwerke, erlaubt
// Loeschen einzelner Eintraege und (falls noch Platz ist) das Hinzufuegen
// eines neuen ueber den Scan+Passwort-Bildschirm. Kehrt zurueck, sobald
// "Back" angetippt wird.
// Blocking: shows the up to 3 saved WiFi networks, allows
// deleting individual entries and (if there is still space) adding
// a new one via the scan+password screen. Returns as soon as
// "Back" is tapped.
void run(TFT_eSPI& tft);
}

View File

@@ -3,9 +3,9 @@
#include <TFT_eSPI.h>
namespace WifiSetupScreen {
// Blockierend: WLAN-Netzwerke suchen, per Touch auswaehlen, Passwort
// ueber Bildschirmtastatur eingeben (Klartext, keine Sterne), verbinden.
// Speichert bei Erfolg die Zugangsdaten via WifiMgr auf der SD-Karte.
// Rueckgabe: true = verbunden, false = abgebrochen/uebersprungen.
// Blocking: scan WiFi networks, select via touch, enter password
// via on-screen keyboard (plain text, no asterisks), connect.
// Saves credentials via WifiMgr on the SD card on success.
// Returns: true = connected, false = cancelled/skipped.
bool run(TFT_eSPI& tft);
}