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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,17 +21,17 @@ namespace {
tft.drawFastVLine(cx, cy - 80, 160, dim); tft.drawFastVLine(cx, cy - 80, 160, dim);
} }
// Dreht einen Punkt (px, py) im lokalen Koordinatensystem der kleinen // Rotates a point (px, py) in the local coordinate system of the small
// Deko-Symbole um angleDeg und verschiebt ihn nach (x, y). // decorative symbols by angleDeg and translates it to (x, y).
void rotatePoint(int16_t x, int16_t y, float sinA, float cosA, void rotatePoint(int16_t x, int16_t y, float sinA, float cosA,
float px, float py, int16_t& outX, int16_t& outY) { float px, float py, int16_t& outX, int16_t& outY) {
outX = x + (int16_t)lroundf(px * cosA - py * sinA); outX = x + (int16_t)lroundf(px * cosA - py * sinA);
outY = y + (int16_t)lroundf(px * sinA + py * cosA); outY = y + (int16_t)lroundf(px * sinA + py * cosA);
} }
// Kleines Flugzeug-Silhouette aus gefuellten Dreiecken (Rumpf, Tragflaechen, // Small airplane silhouette made of filled triangles (fuselage, wings,
// Leitwerk), analog zum grossen Flugzeug in der Mitte, aber schlichter und // tail), analogous to the large airplane in the center, but simpler and
// in beliebiger Richtung (angleDeg, 0 = Nase zeigt nach oben). // in any direction (angleDeg, 0 = nose pointing up).
void drawMiniJet(TFT_eSPI& tft, int16_t x, int16_t y, uint16_t color, float angleDeg) { void drawMiniJet(TFT_eSPI& tft, int16_t x, int16_t y, uint16_t color, float angleDeg) {
float rad = angleDeg * (PI / 180.0f); float rad = angleDeg * (PI / 180.0f);
float s = sinf(rad), c = cosf(rad); float s = sinf(rad), c = cosf(rad);
@@ -55,8 +55,8 @@ namespace {
tft.fillTriangle(tlx, tly, trx, try_, ttx, tty, color); tft.fillTriangle(tlx, tly, trx, try_, ttx, tty, color);
} }
// Kleiner Helikopter: Rotorkreis mit Blaettern, gefuellter Rumpf, Heckausleger // Small helicopter: rotor circle with blades, filled fuselage, tail boom
// mit Leitwerk. angleDeg bestimmt die Flugrichtung (0 = Nase nach oben). // 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) { void drawMiniHeli(TFT_eSPI& tft, int16_t x, int16_t y, uint16_t color, float angleDeg) {
float rad = angleDeg * (PI / 180.0f); float rad = angleDeg * (PI / 180.0f);
float s = sinf(rad), c = cosf(rad); 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); 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. // Blocks until at least MIN_DISPLAY_MS have passed since begin().
// Direkt vor dem Verlassen des Splash-Screens aufrufen. Zeichnet waehrend // Call right before leaving the splash screen. Draws the same
// des Wartens dieselbe Sterne-Animation wie die Menues. // star animation as the menus while waiting.
void waitRemaining(TFT_eSPI& tft); void waitRemaining(TFT_eSPI& tft);
} }

View File

@@ -136,9 +136,9 @@ void run(TFT_eSPI& tft) {
tft.print(I18n::t(StringId::LOADING)); tft.print(I18n::t(StringId::LOADING));
FlightLogbook::DayEntry days[MAX_DAYS_QUERIED]; FlightLogbook::DayEntry days[MAX_DAYS_QUERIED];
// listDaySummaries() statt listDays(): mehrere Sitzungs-Dateien am // Using listDaySummaries() instead of listDays(): multiple session files
// selben Kalendertag (z.B. nach erneutem Einschalten) sollen hier // on the same calendar day (e.g., after restarting) should still count
// weiterhin als EIN Balken pro Tag zaehlen statt als mehrere. // as ONE bar per day instead of multiple.
uint8_t count = FlightLogbook::listDaySummaries(days, MAX_DAYS_QUERIED); uint8_t count = FlightLogbook::listDaySummaries(days, MAX_DAYS_QUERIED);
uint8_t barCount = (count > MAX_BARS) ? MAX_BARS : count; 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 ROW3_Y = 106;
constexpr int16_t ROW4_Y = 140; constexpr int16_t ROW4_Y = 140;
constexpr int16_t UPTIME_Y = 174; 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) { void run(TFT_eSPI& tft) {
@@ -100,8 +100,8 @@ void run(TFT_eSPI& tft) {
if (topAlt.found) { if (topAlt.found) {
String csign = topAlt.callsign[0] ? String(topAlt.callsign) : "?"; String csign = topAlt.callsign[0] ? String(topAlt.callsign) : "?";
// Respektiert jetzt die Einheiten-Einstellung (Menue > Einheiten) // Now respects the units setting (Menu > Units)
// - vorher immer "(XXXX ft)", auch bei Metrisch eingestellt. // - previously always "(XXXX ft)", even when metric was set.
char altBuf[24]; char altBuf[24];
if (LocationManager::useMetricUnits()) { if (LocationManager::useMetricUnits()) {
snprintf(altBuf, sizeof(altBuf), "%s (%ldm)", csign.c_str(), (long)Units::feetToMeters((float)topAlt.altitudeFt)); 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> #include <TFT_eSPI.h>
namespace StatsScreen { namespace StatsScreen {
// Blockierend: zeigt einfache Statistiken aus dem Flight Logbook // Blocking: shows simple statistics from the Flight Logbook
// (heute gesehen, insgesamt gesehen, Anzahl Tage geloggt). Kehrt zurueck, // (seen today, total seen, number of days logged). Returns as soon
// sobald "Back" angetippt wird. // as "Back" is tapped.
void run(TFT_eSPI& tft); void run(TFT_eSPI& tft);
} }

View File

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

View File

@@ -1,24 +1,24 @@
#pragma once #pragma once
#include <Arduino.h> #include <Arduino.h>
// Berechnet Sonnenauf-/untergang (lokale Zeit) fuer einen gegebenen Ort und // Calculates sunrise/sunset (local time) for a given location and
// Tag - Grundlage fuer die standortbasierte Nachtdimmung (siehe main.cpp:: // day - basis for the location-based night dimming (see main.cpp::
// isNightDimHours()), die das vorherige feste 22:00-06:00-Fenster ersetzt. // isNightDimHours()), which replaces the previous fixed 22:00-06:00 window.
namespace SunTimes { namespace SunTimes {
struct Result { struct Result {
bool valid = false; // false = Standort noch unbekannt (lat/lon == 0,0) bool valid = false; // false = location still unknown (lat/lon == 0,0)
bool alwaysDay = false; // Polartag: Sonne geht heute an diesem Ort nicht unter bool alwaysDay = false; // polar day: sun does not set today at this location
bool alwaysNight = false; // Polarnacht: Sonne geht heute an diesem Ort nicht auf bool alwaysNight = false; // polar night: sun does not rise today at this location
float sunriseHour = 0; // Lokale Dezimalstunde (0-24), nur gueltig wenn weder float sunriseHour = 0; // local decimal hour (0-24), only valid if neither
float sunsetHour = 0; // alwaysDay noch alwaysNight gesetzt ist. float sunsetHour = 0; // alwaysDay nor alwaysNight is set.
}; };
// year/month/day sind lokale Kalenderdatumswerte (z.B. aus localtime_r()), // year/month/day are local calendar date values (e.g. from localtime_r()),
// utcOffsetSeconds die bekannte Zeitzonenverschiebung (siehe // utcOffsetSeconds is the known timezone offset (see
// LocationManager::utcOffsetSeconds()). Nutzt den klassischen "Almanac"- // LocationManager::utcOffsetSeconds()). Uses the classic "Almanac"
// Sonnenauf-/untergangsalgorithmus (Sonnenzenit 90.833 Grad, beruecksichtigt // sunrise/sunset algorithm (solar zenith 90.833 degrees, accounting for
// atmosphaerische Refraktion) - Genauigkeit liegt typischerweise im Bereich // atmospheric refraction) - accuracy is typically within a few minutes,
// weniger Minuten, fuer eine sanfte Backlight-Dimmung mehr als ausreichend. // more than sufficient for smooth backlight dimming.
Result compute(double lat, double lon, int year, int month, int day, int32_t utcOffsetSeconds); 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); } String onOff(bool on) { return I18n::t(on ? StringId::ON : StringId::OFF); }
// Einfacher Zeilenumbruch-Helfer, gleiches Prinzip wie in // Simple line wrap helper, same principle as in
// menu_screen.cpp (dort mit zusaetzlicher Scroll-Unterstuetzung, hier // menu_screen.cpp (with additional scroll support there, here
// bewusst ohne - der Beschreibungstext ist kurz genug, dass Scrollen // deliberately without - the description text is short enough that scrolling
// nicht noetig ist). draw=false liefert nur die Gesamthoehe, ohne etwas // is not needed). draw=false returns only the total height, without drawing
// zu zeichnen (fuer die vorab-Platzberechnung des Zurueck-Buttons). // 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 layoutWrapped(TFT_eSPI& tft, int16_t x, int16_t startY, int16_t maxWidth,
int16_t lineHeight, const String& text, bool draw) { int16_t lineHeight, const String& text, bool draw) {
int16_t y = startY; int16_t y = startY;
@@ -57,10 +57,10 @@ namespace {
return (int16_t)(y - startY); return (int16_t)(y - startY);
} }
// Schieberegler-Positionen: 1..Config::SCREEN_TIMEOUT_MAX_MINUTES // Slider positions: 1..Config::SCREEN_TIMEOUT_MAX_MINUTES
// Minuten (je eine Position pro Minute) plus eine zusaetzliche // minutes (one position per minute) plus one additional
// Endposition ganz rechts fuer "Nie" (kein Timeout) - intern weiterhin // end position at the far right for "Never" (no timeout) - internally stored
// als 0 gespeichert, wie schon vor diesem Screen. // as 0, as before this screen.
constexpr uint8_t STEP_COUNT = Config::SCREEN_TIMEOUT_MAX_MINUTES + 1; constexpr uint8_t STEP_COUNT = Config::SCREEN_TIMEOUT_MAX_MINUTES + 1;
uint8_t indexFromMinutes(uint8_t minutes) { 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_W = Config::SCREEN_WIDTH - 2 * TRACK_X;
constexpr int16_t TRACK_H = 6; constexpr int16_t TRACK_H = 6;
constexpr int16_t THUMB_R = 12; constexpr int16_t THUMB_R = 12;
// Grosszuegige vertikale Trefferzone rund um den duennen Regler-Strich - // Generous vertical hit zone around the thin slider track -
// sonst muesste man beim Ziehen extrem praezise genau auf die paar // otherwise you would have to hit the few pixels of the line
// Pixel der Linie treffen. // extremely precisely when dragging.
constexpr int16_t TRACK_HIT_Y_MIN = TRACK_Y - 24; constexpr int16_t TRACK_HIT_Y_MIN = TRACK_Y - 24;
constexpr int16_t TRACK_HIT_Y_MAX = TRACK_Y + 24; constexpr int16_t TRACK_HIT_Y_MAX = TRACK_Y + 24;
Rect screensaverBtn = {10, 135, (int16_t)(Config::SCREEN_WIDTH - 20), 36}; Rect screensaverBtn = {10, 135, (int16_t)(Config::SCREEN_WIDTH - 20), 36};
// Beschreibungstext-Hoehe (haengt von der jeweils eingestellten Sprache // Description text height (depends on the selected language)
// ab) einmalig vorab messen, damit der Zurueck-Button IMMER direkt // measured once in advance, so the Back button ALWAYS lands directly
// darunter landet - nie mit dem Text ueberlappend und nie unnoetig viel // below it - never overlapping the text and never leaving unnecessarily
// Leerraum lassend. Gleiches Prinzip wie bei confirmWarningScreen()/ // much empty space. Same principle as confirmWarningScreen()/
// infoScreen() in menu_screen.cpp. // infoScreen() in menu_screen.cpp.
constexpr int16_t DESC_Y = 185; constexpr int16_t DESC_Y = 185;
constexpr int16_t DESC_LINE_H = 14; 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, int16_t descH = layoutWrapped(tft, 10, DESC_Y, DESC_MAX_WIDTH, DESC_LINE_H,
I18n::t(StringId::TIMEOUT_SCREENSAVER_DESC), false); I18n::t(StringId::TIMEOUT_SCREENSAVER_DESC), false);
int16_t backY = (int16_t)(DESC_Y + descH + 10); int16_t backY = (int16_t)(DESC_Y + descH + 10);
// Sicherheitsnetz, falls eine Uebersetzung doch mal laenger ausfaellt, // Safety net, if a translation ever turns out longer
// als der verfuegbare Platz hergibt - der Zurueck-Button bleibt so // than the available space allows - the Back button stays
// IMMER erreichbar, statt vom Bildschirmrand abgeschnitten zu werden. // ALWAYS reachable, instead of being cut off at the screen edge.
constexpr int16_t BACK_Y_MAX = Config::SCREEN_HEIGHT - 46; constexpr int16_t BACK_Y_MAX = Config::SCREEN_HEIGHT - 46;
if (backY > BACK_Y_MAX) backY = BACK_Y_MAX; if (backY > BACK_Y_MAX) backY = BACK_Y_MAX;
Rect backBtn = {10, backY, (int16_t)(Config::SCREEN_WIDTH - 20), 36}; Rect backBtn = {10, backY, (int16_t)(Config::SCREEN_WIDTH - 20), 36};
@@ -174,19 +174,19 @@ void run(TFT_eSPI& tft) {
} }
dragging = true; dragging = true;
} else if (dragging && !live.touched) { } else if (dragging && !live.touched) {
// Loslassen: jetzt erst dauerhaft speichern (nicht bei jeder // Release: now save persistently (not during every
// Zwischenposition waehrend des Ziehens selbst, um unnoetig // intermediate slider position while dragging, to avoid unnecessary
// viele SD-Kartenschreibvorgaenge zu vermeiden). // SD card writes).
SettingsStore::setScreenTimeoutMinutes(minutesFromIndex(index)); SettingsStore::setScreenTimeoutMinutes(minutesFromIndex(index));
dragging = false; dragging = false;
} }
// wasTapped() IMMER aufrufen (auch waehrend des Ziehens), damit ihr // ALWAYS call wasTapped() (even while dragging), so its
// interner Loslassen-Erkennungszustand synchron bleibt - nur die // internal release detection state stays in sync - only the
// AUSWERTUNG des Ergebnisses wird waehrend des Ziehens unterdrueckt. // EVALUATION of the result is suppressed while dragging.
// Wuerde man den Aufruf selbst waehrend des Ziehens auslassen, // If the call itself were omitted while dragging,
// koennte beim Loslassen faelschlich ein "Tap" mit einer veralteten // releasing could incorrectly report a "Tap" with a stale
// Position (von VOR Beginn des Ziehens) gemeldet werden. // position (from BEFORE the drag started).
TouchInput::Point tap; TouchInput::Point tap;
bool tapped = TouchInput::wasTapped(tap); bool tapped = TouchInput::wasTapped(tap);
if (!dragging && tapped) { if (!dragging && tapped) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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