From fb09b2b7a1786e2acdc34766ccb7218d2e53025f Mon Sep 17 00:00:00 2001 From: Rasmus Date: Sun, 16 Aug 2026 14:33:27 +0200 Subject: [PATCH] localization --- AGENTS.md | 267 +++++++++++++++++++ CLAUDE.md | 276 -------------------- src/about_screen.cpp | 6 +- src/about_screen.h | 4 +- src/address_search_screen.cpp | 137 +++++----- src/aircraft_details.cpp | 55 ++-- src/aircraft_list_screen.cpp | 38 +-- src/aircraft_list_screen.h | 12 +- src/aircraft_table.cpp | 13 +- src/aircraft_table.h | 14 +- src/aircraft_watchlist.cpp | 13 +- src/aircraft_watchlist_screen.cpp | 32 +-- src/airline_filter.cpp | 8 +- src/brightness_screen.cpp | 6 +- src/brightness_screen.h | 6 +- src/calibration_screen.h | 6 +- src/config.h | 60 ++--- src/first_run_location_screen.cpp | 19 +- src/first_run_location_screen.h | 7 +- src/flight_logbook.cpp | 78 +++--- src/flight_logbook.h | 47 ++-- src/i18n.h | 166 ++++++------ src/led_alert.h | 14 +- src/location_manager.h | 11 +- src/location_presets.cpp | 13 +- src/location_presets.h | 5 +- src/location_presets_screen.cpp | 165 ++++++------ src/logbook_files_screen.cpp | 12 +- src/logbook_files_screen.h | 4 +- src/main.cpp | 244 +++++++++--------- src/menu_screen.cpp | 351 +++++++++++++------------ src/menu_screen.h | 6 +- src/menu_stars.h | 18 +- src/net_task.cpp | 7 +- src/net_task.h | 14 +- src/ota_update.cpp | 77 +++--- src/ota_update.h | 50 ++-- src/radar_screen.cpp | 413 +++++++++++++++--------------- src/radar_screen.h | 8 +- src/sd_mutex.h | 21 +- src/sd_storage.cpp | 9 +- src/sd_storage.h | 12 +- src/settings_backup.cpp | 8 +- src/settings_backup.h | 23 +- src/settings_store.cpp | 17 +- src/settings_store.h | 30 +-- src/splash_screen.cpp | 14 +- src/splash_screen.h | 6 +- src/stats_history_screen.cpp | 6 +- src/stats_screen.cpp | 6 +- src/stats_screen.h | 6 +- src/sun_times.cpp | 31 ++- src/sun_times.h | 28 +- src/timeout_screen.cpp | 56 ++-- src/timeout_screen.h | 12 +- src/touch_input.cpp | 10 +- src/touch_input.h | 16 +- src/ui_font.h | 6 +- src/weather.cpp | 21 +- src/weather.h | 30 +-- src/web_export_server.cpp | 188 +++++++------- src/webui_screen.cpp | 38 ++- src/webui_screen.h | 8 +- src/wifi_manage_screen.h | 8 +- src/wifi_setup_screen.h | 8 +- 65 files changed, 1621 insertions(+), 1679 deletions(-) create mode 100644 AGENTS.md delete mode 100644 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..27081f1 --- /dev/null +++ b/AGENTS.md @@ -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` + (``) 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 "" - - 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. \ No newline at end of file diff --git a/src/about_screen.cpp b/src/about_screen.cpp index ff724d3..4c8fb79 100644 --- a/src/about_screen.cpp +++ b/src/about_screen.cpp @@ -32,9 +32,9 @@ void run(TFT_eSPI& tft) { tft.fillScreen(TFT_BLACK); tft.setTextColor(TFT_GREEN, TFT_BLACK); - // Projektname bewusst hart codiert statt ueber i18n - genau wie - // schon im Splash-Screen (splash_screen.cpp), da ein Eigenname - // ohnehin nicht uebersetzt wird. +// Project name intentionally hardcoded instead of via i18n - just like + // in the splash screen (splash_screen.cpp), since a proper noun + // is not translated anyway. tft.setCursor(10, 14); tft.println("Leo's Flightradar"); diff --git a/src/about_screen.h b/src/about_screen.h index d1595d1..73d90fc 100644 --- a/src/about_screen.h +++ b/src/about_screen.h @@ -2,8 +2,8 @@ #include #include -// Einfacher Info-Screen: Projektname, Kurzbeschreibung, Copyright-Jahr und -// die aktuelle Versionsnummer (Config::APP_VERSION). +// Simple info screen: project name, short description, copyright year and +// the current version number (Config::APP_VERSION). namespace AboutScreen { void run(TFT_eSPI& tft); } diff --git a/src/address_search_screen.cpp b/src/address_search_screen.cpp index 0e79f40..e929840 100644 --- a/src/address_search_screen.cpp +++ b/src/address_search_screen.cpp @@ -34,16 +34,12 @@ namespace { tft.setTextDatum(TL_DATUM); } - // Lokale Kopie (siehe Konvention in location_presets_screen.cpp - jeder - // Screen haelt seine eigenen kleinen Helfer statt eines gemeinsamen - // Moduls). Ohne Scroll-Unterstuetzung - hier immer nur kurze, - // vorab abgeschnittene Texte (siehe runConfirmScreen/showErrorRetry). - // Lokale Kopie (siehe Konvention in location_presets_screen.cpp - jeder - // Screen haelt seine eigenen kleinen Helfer statt eines gemeinsamen - // Moduls). Optionale Scroll-Unterstuetzung (scrollY/viewTop/viewBottom/ - // draw) fuer runConfirmScreen() unten - bei den bestehenden Aufrufstellen - // (showErrorRetry, Kopfzeilen) bleiben die Defaults aktiv, es wird also - // wie bisher immer alles auf einmal gezeichnet. +// Local copy (see convention in location_presets_screen.cpp - each + // screen keeps its own small helpers instead of a shared + // module). Optional scroll support (scrollY/viewTop/viewBottom/ + // draw) for runConfirmScreen() below - for the existing call sites + // (showErrorRetry, headers) the defaults remain active, so everything + // is still drawn at once as before. int16_t layoutWrapped(TFT_eSPI& tft, int16_t x, int16_t startY, int16_t maxWidth, int16_t lineHeight, const String& text, int16_t scrollY = 0, int16_t viewTop = -32000, int16_t viewBottom = 32000, @@ -73,8 +69,8 @@ namespace { return y; } - // ---- UTF-8-bewusste Puffer-Helfer (fuer die Sonderzeichen-Tasten wie - // "Ä", die als 2-Byte-UTF-8-Sequenz eingefuegt werden muessen) ---- +// ---- UTF-8-aware buffer helpers (for special character keys like + // "Ä", which must be inserted as 2-byte UTF-8 sequences) ---- void appendUtf8(char* buf, uint8_t& len, uint8_t cap, const char* s) { size_t sl = strlen(s); if (len + sl >= cap) return; @@ -83,9 +79,8 @@ namespace { buf[len] = 0; } - // Entfernt beim Loeschen eine ganze UTF-8-Zeichensequenz (nicht nur ein - // einzelnes Byte) - sonst bliebe bei Sonderzeichen ein kaputtes - // halbes Byte im Puffer stehen. +// When deleting, removes a whole UTF-8 character sequence (not just a + // single byte) - otherwise a broken half-byte would remain in the buffer. void backspaceUtf8(char* buf, uint8_t& len) { if (len == 0) return; len--; @@ -93,9 +88,9 @@ namespace { buf[len] = 0; } - // Zeigt nur so viel vom ENDE des Puffers, wie in maxWidth passt - der - // Nutzer tippt am Ende weiter, das sichtbare Fenster soll dem folgen - // (wie bei einem gewoehnlichen einzeiligen Texteingabefeld). +// Shows only as much of the END of the buffer as fits in maxWidth - the + // user types at the end, the visible window should follow + // (like a normal single-line text input field). String visibleTail(TFT_eSPI& tft, const char* buf, int16_t maxWidth) { String s(buf); while (s.length() > 0 && tft.textWidth(s) > maxWidth) { @@ -106,26 +101,26 @@ namespace { return s; } - // ---- Tastatur-Layout ---- + // ---- Keyboard Layout ---- constexpr const char* DIGITS = "1234567890"; constexpr const char* ROW1 = "QWERTYUIOP"; constexpr const char* ROW2 = "ASDFGHJKL"; - // Um Komma und Bindestrich erweitert (7->9 Tasten) - Adressen wie eine - // Hausnummer "45/3" oder "Strasse 12, Ort" waren sonst gar nicht - // eintippbar, da die Tastatur bisher keinerlei Satzzeichen enthielt. +// Extended with comma and hyphen (7->9 keys) - addresses like + // a house number "45/3" or "Street 12, City" were not + // typeable at all since the keyboard previously contained no punctuation. constexpr const char* ROW3 = "ZXCVBNM,-"; - // Sonderzeichen-Seite: gegenueber der ersten Version wurden die seltenen - // Â/Ë/Î/Û gegen die fuer Adressen wichtigen Satzzeichen "/", ".", "'", "-" - // getauscht (Hausnummern wie "45/3", Abkuerzungen wie "Str.", Apostroph- - // Namen). +// Special characters page: compared to the first version, the rare + // Â/Ë/Î/Û were swapped for the punctuation important for addresses + // "/", ".", "'", "-" (house numbers like "45/3", abbreviations like "Str.", apostrophe + // names). constexpr const char* SPEC0[6] = {"À", "Á", "Ä", "Ç", "É", "È"}; constexpr const char* SPEC1[6] = {"Ê", "Í", "Ñ", "Ó", "Ò", "Ô"}; constexpr const char* SPEC2[6] = {"Ö", "Ù", "Ú", "Ü", "ß", "Ğ"}; constexpr const char* SPEC3[6] = {"İ", "Ş", "/", ".", "'", "-"}; - // Gibt die eingegebene Adresse zurueck, oder einen leeren String, wenn - // der Nutzer abgebrochen hat. +// Returns the entered address, or an empty string if + // the user cancelled. String runAddressKeyboard(TFT_eSPI& tft) { MenuStars::reset(); constexpr uint8_t CAP = 64; @@ -137,12 +132,12 @@ namespace { constexpr int16_t KEY_GAP = 3; constexpr int16_t FIELD_H = 34; - // Kopfbereich (Titel + Format-Hinweis) einmal zeichnen, um seine - // tatsaechliche Hoehe per getCursorY() zu messen (variiert je nach - // Sprache/Uebersetzungslaenge) - Eingabefeld und Tastatur-Start - // werden daraus abgeleitet statt wie zuvor eine feste Pixelposition - // (78) anzunehmen. redraw() unten zeichnet denselben Kopf bei jedem - // Aufruf identisch neu. +// Head area (title + format hint) draw once to measure its + // actual height via getCursorY() (varies by + // language/translation length) - input field and keyboard start + // are derived from it instead of assuming a fixed pixel position + // (78) as before. redraw() below draws the same header identically on every + // call. tft.fillScreen(TFT_BLACK); tft.setTextColor(TFT_GREEN, TFT_BLACK); tft.setCursor(10, 14); @@ -293,10 +288,10 @@ namespace { return searched ? String(buf) : String(); } - // Namens-Tastatur fuer den finalen Schritt (Preset-Name vergeben) - - // absichtlich eine eigene, einfache ASCII-Tastatur (keine - // Sonderzeichen noetig fuer einen Preset-Namen wie "Zuhause"), - // spiegelt runPresetNameKeypad() aus location_presets_screen.cpp. +// Name keypad for the final step (naming the preset) - + // intentionally a separate, simple ASCII keyboard (no + // special characters needed for a preset name like "Home"), + // mirrors runPresetNameKeypad() from location_presets_screen.cpp. String runNameKeypad(TFT_eSPI& tft) { MenuStars::reset(); constexpr const char* NDIGITS = "1234567890"; @@ -311,9 +306,9 @@ namespace { constexpr int16_t KEY_GAP = 3; constexpr int16_t FIELD_H = 34; - // Kopfbereich (Titel + Namens-Hinweis) einmal zeichnen, um seine - // tatsaechliche Hoehe per getCursorY() zu messen - selbes Muster wie - // in runAddressKeyboard() oben. +// Head area (title + name hint) draw once to measure its + // actual height via getCursorY() - same pattern as + // in runAddressKeyboard() above. tft.fillScreen(TFT_BLACK); tft.setTextColor(TFT_GREEN, TFT_BLACK); tft.setCursor(10, 14); @@ -406,9 +401,9 @@ namespace { return String(buf); } - // Prozentkodiert einen UTF-8-String fuer die Nominatim-Query (jedes - // Roh-Byte einzeln - funktioniert damit auch fuer die mehrbytigen - // Sonderzeichen). +// Percent-encodes a UTF-8 string for the Nominatim query (each + // raw byte individually - works for multi-byte + // special characters too). String urlEncode(const String& s) { String out; char hex[4]; @@ -426,18 +421,18 @@ namespace { return out; } - // ISO-639-1-Codes in derselben Reihenfolge wie I18n::TABLES (siehe - // i18n.cpp) - fuer staerker lokalisierte display_name-Ergebnisse - // (Nominatim accept-language-Parameter). +// ISO-639-1 codes in the same order as I18n::TABLES (see + // i18n.cpp) - for more localized display_name results + // (Nominatim accept-language parameter). constexpr const char* NOMINATIM_LANG_CODES[6] = {"en", "de", "fr", "tr", "es", "it"}; enum class GeocodeResult { Ok, NoResults, NetworkError }; - // Kostenloser, anmeldefreier Geokodierungs-Dienst (OpenStreetMap - // Nominatim, siehe Config::NOMINATIM_HOST). Blockierender Aufruf, - // ausgeloest durch Nutzer-Interaktion (Tap auf "Suchen") - laeuft im - // Vordergrund auf Core 1, exakt wie der bestehende Flugzeug-Detail- - // Abruf in aircraft_details.cpp. +// Free, no-registration geocoding service (OpenStreetMap + // Nominatim, see Config::NOMINATIM_HOST). Blocking call, + // triggered by user interaction (tap on "Search") - runs in the + // foreground on Core 1, exactly like the existing aircraft detail + // fetch in aircraft_details.cpp. GeocodeResult geocode(const String& query, double& lat, double& lon, String& displayName) { if (WiFi.status() != WL_CONNECTED) return GeocodeResult::NetworkError; @@ -461,9 +456,9 @@ namespace { return GeocodeResult::NetworkError; } - // Body erst komplett einsammeln statt direkt aus http.getStream() - // zu parsen - siehe weather.cpp, gleicher Grund (Chunked-Transfer- - // Encoding fuehrte dort sonst zu ArduinoJson-"InvalidInput"). +// Collect entire body first instead of parsing directly from http.getStream() + // - see weather.cpp, same reason (chunked transfer + // encoding caused ArduinoJson "InvalidInput" there otherwise). String body = http.getString(); http.end(); @@ -486,8 +481,8 @@ namespace { return GeocodeResult::Ok; } - // Fehler-/Kein-Ergebnis-Screen mit "Erneut versuchen"/"Abbrechen". - // Gibt true zurueck, wenn der Nutzer es erneut versuchen will. +// Error/no-result screen with "Try again"/"Cancel". + // Returns true if the user wants to try again. bool showErrorRetry(TFT_eSPI& tft, StringId msgId) { MenuStars::reset(); tft.fillScreen(TFT_BLACK); @@ -507,9 +502,9 @@ namespace { } } - // Bestaetigungs-Screen mit dem von Nominatim gefundenen Ort. Gibt 1 - // zurueck ("verwenden"), 0 fuer "erneut versuchen" (zurueck zur - // Adress-Tastatur). +// Confirmation screen with the location found by Nominatim. Returns 1 + // ("use it"), 0 for "try again" (back to + // address keyboard). int runConfirmScreen(TFT_eSPI& tft, const String& displayName) { MenuStars::reset(); @@ -520,14 +515,14 @@ namespace { constexpr int16_t USE_Y = (int16_t)(Config::SCREEN_HEIGHT - 50); constexpr int16_t VIEW_BOTTOM = (int16_t)(TRY_AGAIN_Y - 10); - // Volle Adresse OHNE Kuerzung anzeigen (frueher wurde bei > 150 - // Zeichen mit "..." abgeschnitten) - stattdessen wird der Absatz bei - // Bedarf vertikal scrollbar, exakt dasselbe Muster wie beim "Wie - // funktionieren Presets"-Infoscreen (location_presets_screen.cpp - // ::runInfoScreen). Bewusst KEIN Marquee hier: das ist ein - // mehrzeiliger, wortumgebrochener Absatz, keine einzelne Zeile, die - // in der Breite nicht passt - horizontales Scrollen waere hier die - // falsche Loesung. +// Show full address WITHOUT truncation (previously cut at > 150 + // characters with "...") - instead, the paragraph becomes + // vertically scrollable if needed, exactly the same pattern as the "How + // do presets work" info screen (location_presets_screen.cpp + // ::runInfoScreen). Deliberately NOT a marquee here: this is a + // multi-line, word-wrapped paragraph, not a single line that + // does not fit in width - horizontal scrolling would be the + // wrong solution here. int16_t totalH = layoutWrapped(tft, 10, VIEW_TOP, textMaxWidth, LINE_H, displayName, 0, 0, 0, false); int16_t maxScroll = (int16_t)(totalH - VIEW_BOTTOM); if (maxScroll < 0) maxScroll = 0; @@ -611,9 +606,9 @@ bool run(TFT_eSPI& tft) { String name = runNameKeypad(tft); if (!LocationPresets::addPreset(lat, lon, name)) return false; - // Neu angelegtes Preset sofort aktivieren - sonst blieb z.B. der - // automatische IP-Standort aktiv, obwohl gerade extra eine genauere - // Adresse eingegeben wurde (siehe Alex' Feedback). +// Newly created preset is immediately activated - otherwise e.g. the + // automatic IP location would remain active, even though a more accurate + // address was just entered (see Alex' feedback). LocationPresets::setActiveIndex((int8_t)(LocationPresets::count() - 1)); return true; } diff --git a/src/aircraft_details.cpp b/src/aircraft_details.cpp index 2cf12d7..ae93ba5 100644 --- a/src/aircraft_details.cpp +++ b/src/aircraft_details.cpp @@ -80,9 +80,9 @@ void update() { WiFiClientSecure client; client.setInsecure(); - // hexdb.io zuerst versuchen (bisherige Quelle) - aber mit kuerzerem - // Timeout (3s statt 5s), damit ein kompletter Ausfall des Dienstes die - // ADS-B-Abfrage nicht unnoetig lange blockiert, bevor der Fallback greift. +// Try hexdb.io first (previous source) - but with a shorter + // timeout (3s instead of 5s) so a complete service outage does not + // block the ADS-B query unnecessarily long before the fallback kicks in. client.setTimeout(3000); String body; if (httpGetString(client, String("https://hexdb.io/api/v1/aircraft/") + hex, body)) { @@ -99,9 +99,9 @@ void update() { } } - // Fallback: hexdb.io war nicht erreichbar/lieferte kein Modell - - // adsbdb.com als zweite, unabhaengige Quelle versuchen (andere API-Form, - // aber inhaltlich aequivalent: Hersteller + Typ ueber den Hex-Code). +// Fallback: hexdb.io was unreachable/did not deliver a model - + // try adsbdb.com as a second, independent source (different API format, + // but equivalent in content: manufacturer + type via the hex code). if (!result.model[0]) { client.setTimeout(4000); String body2; @@ -120,26 +120,25 @@ void update() { } } - // Flugroute (Start-/Zielflughafen) - jetzt ueber eine Kette aus DREI - // unabhaengigen kostenlosen Quellen statt nur einer, absteigend nach in - // Tests beobachteter Trefferquote sortiert. Vorher lieferte adsbdb.com - // allein in ca. 80% der Faelle "unknown" (siehe Alex' Feedback) - die - // drei Quellen speisen sich aus unterschiedlichen, ueberlappenden aber - // nicht identischen Community-Datenbanken, daher deutlich bessere - // Gesamtabdeckung durch Verketten: - // 1. VRS-Standing-Data-Mirror (adsb.lol) - stuendlich aktualisierter - // Spiegel des Virtual-Radar-Server-Projekts, in Tests die mit - // Abstand zuverlaessigste Quelle. Pfad = erste 2 Zeichen des - // (GROSSGESCHRIEBENEN - der Dienst ist case-sensitiv) Rufzeichens - // als Ordner, liefert "airport_codes":"ORIG-DEST" (ICAO). - // 2. hexdb.io - eigener Route-Endpunkt (andere URL als der - // Aircraft-Endpunkt weiter oben), liefert "route":"ORIG-DEST". - // 3. adsbdb.com Callsign-Endpunkt - bisherige einzige Quelle, bleibt - // als letzter Fallback, da sie gelegentlich Daten hat, die die - // anderen beiden nicht haben. - // Nur versuchen, wenn ueberhaupt ein Rufzeichen bekannt ist - - // Sichtflug-Maschinen ohne Callsign haben ohnehin keine darueber - // auswertbare Route. +// Flight route (origin/destination airport) - now via a chain of THREE + // independent free sources instead of just one, sorted descending by + // observed hit rate in testing. Previously adsbdb.com alone returned + // "unknown" in about 80% of cases (see Alex' feedback) - the + // three sources draw from different, overlapping but + // not identical community databases, therefore much better + // overall coverage through chaining: + // 1. VRS-Standing-Data-Mirror (adsb.lol) - hourly updated + // mirror of the Virtual Radar Server project, by far the most + // reliable source in testing. Path = first 2 characters of the + // (UPPERCASE - the service is case-sensitive) callsign + // as folder, returns "airport_codes":"ORIG-DEST" (ICAO). + // 2. hexdb.io - own route endpoint (different URL from the + // aircraft endpoint above), returns "route":"ORIG-DEST". + // 3. adsbdb.com callsign endpoint - previous single source, remains + // as last fallback since it occasionally has data that the + // other two do not. + // Only try if a callsign is known at all - + // VFR aircraft without a callsign do not have an evaluable route anyway. String trimmedCallsign = String(callsign); trimmedCallsign.trim(); trimmedCallsign.toUpperCase(); @@ -165,7 +164,7 @@ void update() { } } - // 2. hexdb.io Route-Endpunkt, falls Quelle 1 nichts geliefert hat. + // 2. hexdb.io route endpoint, if source 1 did not deliver anything. if (!result.routeOrigin[0] || !result.routeDest[0]) { client.setTimeout(3000); String body4; @@ -179,7 +178,7 @@ void update() { } } - // 3. adsbdb.com Callsign-Endpunkt als letzter Fallback. + // 3. adsbdb.com callsign endpoint as last fallback. if (trimmedCallsign.length() > 0 && (!result.routeOrigin[0] || !result.routeDest[0])) { client.setTimeout(4000); String body5; diff --git a/src/aircraft_list_screen.cpp b/src/aircraft_list_screen.cpp index 41ef1da..6dd0969 100644 --- a/src/aircraft_list_screen.cpp +++ b/src/aircraft_list_screen.cpp @@ -33,10 +33,10 @@ namespace { tft.setTextDatum(TL_DATUM); } - // Gleiche Hoehen-Farblogik wie RadarScreen (dort intern, hier bewusst - // dupliziert - siehe CLAUDE.md-Konvention "jeder Screen unabhaengig"). - // Auf die Nachtdimmung wird hier verzichtet: diese Liste ist ein Menue- - // Screen wie jeder andere, kein Dauerbild wie das Radar. +// Same altitude color logic as RadarScreen (duplicated there internally, here deliberately + // duplicated - see CLAUDE.md convention "each screen independently runnable"). + // Night dimming is omitted here: this list is a menu + // screen like any other, not a persistent image like the radar. uint16_t colorForAltitude(int32_t altFt) { if (altFt < Config::COLOR_LOW_ALT_THRESHOLD_FT) return TFT_GREEN; if (altFt < Config::COLOR_MID_ALT_THRESHOLD_FT) return TFT_YELLOW; @@ -52,8 +52,8 @@ namespace { } enum class SortMode : uint8_t { Distance, Altitude, Callsign }; - // Bleibt bis zum naechsten Neustart erhalten (kein SD-Speichern noetig - - // das waere fuer eine reine Anzeige-Praeferenz unnoetiger Aufwand). +// Persists until the next restart (no SD storage needed - + // that would be unnecessary effort for a pure display preference). SortMode sortMode = SortMode::Distance; const char* sortModeLabel() { @@ -72,7 +72,7 @@ bool run(TFT_eSPI& tft) { constexpr int16_t LIST_BOTTOM = Config::SCREEN_HEIGHT - 56; constexpr uint8_t ROWS_VISIBLE = (LIST_BOTTOM - LIST_TOP) / (ROW_H + ROW_GAP); - uint8_t scrollTop = 0; // Index des ersten sichtbaren Eintrags in der sortierten Liste + uint8_t scrollTop = 0; // Index of the first visible entry in the sorted list bool selected = false; bool done = false; MenuStars::reset(); @@ -82,9 +82,9 @@ bool run(TFT_eSPI& tft) { uint8_t count = 0; float rangeKm = Config::RANGE_STEPS_KM[SettingsStore::rangeIndex()]; - // Gleiche Filter wie das Radar (Reichweite, Bodenfahrzeuge, - // Airline-Filter), damit die Liste genau die Flugzeuge zeigt, die - // gerade auch als Punkte auf dem Radar zu sehen sind. +// Same filters as the radar (range, ground vehicles, + // airline filter), so the list shows exactly the aircraft + // that are currently also visible as dots on the radar. AircraftTable::lock(); Aircraft* table = AircraftTable::raw(); for (uint8_t i = 0; i < AircraftTable::capacity(); i++) { @@ -126,9 +126,9 @@ bool run(TFT_eSPI& tft) { Rect rowRects[ROWS_VISIBLE]; uint8_t visibleRowCount = 0; - // Einheiten-Einstellung (Menue > Einheiten) einmal vor der Schleife - // lesen statt pro Zeile - vorher zeigten Hoehe/Distanz hier immer - // fest ft/km, auch bei Imperial eingestellt. +// Read unit setting (Menu > Units) once before the loop + // instead of per row - previously altitude/distance always showed + // fixed ft/km here, even when imperial was set. bool listMetric = LocationManager::useMetricUnits(); if (count == 0) { @@ -147,12 +147,12 @@ bool run(TFT_eSPI& tft) { uint16_t borderColor = emergency ? TFT_RED : (watched ? TFT_CYAN : TFT_GREEN); tft.drawRoundRect(r.x, r.y, r.w, r.h, 4, borderColor); - // WICHTIG (siehe CLAUDE.md-Falle "Font ist Baseline-verankert"): - // Alle drei Textfelder ueber drawString() mit einem MITTIGEN - // Datum auf die Zeilenmitte zentrieren, statt Baseline- und - // Top-verankerte Aufrufe zu mischen - genau das hatte vorher - // dazu gefuehrt, dass Hoehe/Distanz unten aus der Box liefen, - // waehrend das Rufzeichen (Baseline-verankert) korrekt sass. +// IMPORTANT (see CLAUDE.md gotcha "Font is baseline-anchored"): + // All three text fields via drawString() with a MIDDLE + // datum centered on the row's midpoint, instead of mixing baseline- and + // top-anchored calls - exactly that had previously caused + // altitude/distance to run out of the bottom of the box, + // while the callsign (baseline-anchored) was positioned correctly. int16_t midY = r.y + r.h / 2; const char* label = a.callsign[0] ? a.callsign : a.hex; diff --git a/src/aircraft_list_screen.h b/src/aircraft_list_screen.h index f423c63..019d2fa 100644 --- a/src/aircraft_list_screen.h +++ b/src/aircraft_list_screen.h @@ -2,12 +2,12 @@ #include #include -// Sortierbare Liste aller aktuell erkannten Flugzeuge (gleiche Filter wie -// das Radar: Reichweite, Bodenfahrzeuge, Airline-Filter). Antippen eines -// Eintrags springt zurueck zum Radar mit direkt geoeffnetem Detail-Panel. +// Sortable list of all currently detected aircraft (same filters as +// the radar: range, ground vehicles, airline filter). Tapping an +// entry jumps back to the radar with the detail panel directly open. namespace AircraftListScreen { - // Rueckgabewert true = der Nutzer hat ein Flugzeug angetippt (Aufrufer - // soll bis zum Radar zurueckspringen, nicht nur die eigene Seite - // schliessen). false = nur "Zurueck" gedrueckt, normal weiter im Menue. +// Return value true = the user tapped an aircraft (caller + // should jump back to the radar, not just close its own page). + // false = only "Back" pressed, continue normally in menu. bool run(TFT_eSPI& tft); } diff --git a/src/aircraft_table.cpp b/src/aircraft_table.cpp index b3eb258..5f60611 100644 --- a/src/aircraft_table.cpp +++ b/src/aircraft_table.cpp @@ -4,13 +4,12 @@ #include #include -// WICHTIG: Diese Datei ruft absichtlich KEIN AirlineLookup::resolve() mehr auf! -// postFetchUpdate() wird vom NetTask auf Core 0 aufgerufen. AirlineLookup -// braucht SD-Kartenzugriff, und die SD-Karte wurde in setup() auf Core 1 -// initialisiert - Zugriff von Core 0 aus fuehrte zu einem Haenger (Task -// Watchdog auf IDLE0). Die Aufloesung der Airline-Namen passiert deshalb -// jetzt in main.cpp/renderAircraftList() auf Core 1 (demselben Core, der -// die SD-Karte urspruenglich initialisiert hat). +// IMPORTANT: This file intentionally no longer calls AirlineLookup::resolve()! +// postFetchUpdate() is called from NetTask on Core 0. AirlineLookup +// needs SD card access, and the SD card was initialized in setup() on Core 1 +// - access from Core 0 caused a hang (Task Watchdog on IDLE0). The +// resolution of airline names now happens in main.cpp/renderAircraftList() +// on Core 1 (the same core that originally initialized the SD card). namespace AircraftTable { diff --git a/src/aircraft_table.h b/src/aircraft_table.h index 04bccec..a38dfaf 100644 --- a/src/aircraft_table.h +++ b/src/aircraft_table.h @@ -11,15 +11,15 @@ namespace AircraftTable { uint8_t validCount(); void postFetchUpdate(double homeLat, double homeLon); - // Wird bei jedem postFetchUpdate() erhoeht. Damit koennen andere Teile des - // Programms (z.B. der Render-Loop) erkennen, ob sich die Daten seit dem - // letzten Mal ueberhaupt geaendert haben, statt stumpf auf Zeit zu pollen - - // das vermeidet unnoetiges (und flackerndes) Neuzeichnen. +// Incremented on every postFetchUpdate(). This lets other parts of the + // program (e.g. the render loop) detect whether the data has + // changed at all since the last time, instead of blindly polling by time - + // this avoids unnecessary (and flickering) redrawing. uint32_t version(); - // Schuetzt den Zugriff auf raw()/validCount()/postFetchUpdate() zwischen - // dem Netzwerk-Task (Core 0, schreibt) und dem Render-Loop (Core 1, liest). - // Aufrufer muss lock() vor und unlock() nach jedem Zugriff aufrufen. +// Protects access to raw()/validCount()/postFetchUpdate() between + // the network task (Core 0, writes) and the render loop (Core 1, reads). + // Caller must call lock() before and unlock() after every access. void lock(); void unlock(); diff --git a/src/aircraft_watchlist.cpp b/src/aircraft_watchlist.cpp index 74a23f6..4253ae8 100644 --- a/src/aircraft_watchlist.cpp +++ b/src/aircraft_watchlist.cpp @@ -16,19 +16,18 @@ namespace { char watched[MAX_WATCHED][9] = {{0}}; uint8_t watchedCount = 0; - // Schuetzt watched[]/watchedCount - urspruenglich nur von Core 1 (Menue- - // Screens, Radar) verwendet, seit der WebUI-Listenverwaltung (siehe - // web_export_server.cpp) aber auch von Core 0 (NetTask) aus erreichbar. - // Gleiches Muster wie AircraftDetails::mutex. +// Protects watched[]/watchedCount - originally only used from Core 1 (menu + // screens, radar), but since the WebUI list management (see + // web_export_server.cpp) also reachable from Core 0 (NetTask). + // Same pattern as AircraftDetails::mutex. SemaphoreHandle_t mutex = nullptr; void ensureMutex() { if (mutex == nullptr) mutex = xSemaphoreCreateMutex(); } - // Ueberspringt fuehrende Leerzeichen, uebernimmt bis zu 8 Zeichen und - // bricht bei einem Leerzeichen ab (ADS-B-Rufzeichen haben oft Padding), - // alles in Grossbuchstaben. +// Skips leading spaces, copies up to 8 characters and stops at a space + // (ADS-B callsigns often have padding), all in uppercase. void normalize(const char* callsign, char* out) { int j = 0; int i = 0; diff --git a/src/aircraft_watchlist_screen.cpp b/src/aircraft_watchlist_screen.cpp index 2e2e7fb..7a0f6c8 100644 --- a/src/aircraft_watchlist_screen.cpp +++ b/src/aircraft_watchlist_screen.cpp @@ -25,9 +25,9 @@ namespace { tft.setTextDatum(TL_DATUM); } - // Vier Tastaturreihen (Ziffern oben, dann QWERTYUIOP/ASDFGHJKL/ZXCVBNM), - // da Rufzeichen wie "DLH441" auch Zahlen enthalten. Buffer 8 Zeichen + - // Nullterminierung (volles Rufzeichen statt nur 3-stelligem ICAO-Praefix). +// Four keyboard rows (digits on top, then QWERTYUIOP/ASDFGHJKL/ZXCVBNM), + // since callsigns like "DLH441" also contain numbers. Buffer 8 characters + + // null terminator (full callsign instead of just 3-letter ICAO prefix). String runCallsignKeypad(TFT_eSPI& tft) { MenuStars::reset(); constexpr const char* DIGITS = "1234567890"; @@ -164,10 +164,10 @@ namespace { constexpr int16_t textMaxWidth = Config::SCREEN_WIDTH - 20; constexpr int16_t LINE_H = 16; - // VIEW_TOP haengt davon ab, wie viele Zeilen der Titel tatsaechlich - // braucht (je nach Sprache 1 oder 2 Zeilen) - vorher "trocken" - // (draw=false) berechnen, damit der Fliesstext nicht in eine - // zweizeilige Titelueberschrift hineinlaeuft. +// VIEW_TOP depends on how many lines the title actually needs + // (1 or 2 lines depending on language) - calculate "dry" first + // (draw=false) so the body text does not run into a + // two-line title heading. int16_t titleEndY = layoutWrapped(tft, 10, 14, textMaxWidth, LINE_H, I18n::t(StringId::WATCHLIST_INFO_TITLE), 0, 0, 0, false); const int16_t VIEW_TOP = titleEndY + 4; constexpr int16_t VIEW_BOTTOM = Config::SCREEN_HEIGHT - 60; @@ -192,10 +192,10 @@ namespace { auto redraw = [&]() { tft.fillScreen(TFT_BLACK); tft.setTextColor(TFT_GREEN, TFT_BLACK); - // Titel kann laenger als eine Zeile sein - ueber denselben - // layoutWrapped()-Mechanismus wie den Fliesstext zeichnen, statt - // println() (das keinen wortweisen Umbruch macht und mitten im - // Wort abschneidet/umbricht). +// Title can be longer than one line - draw via the same + // layoutWrapped() mechanism as the body text, instead of + // println() (which does not do word-wise wrapping and + // cuts/wraps mid-word). layoutWrapped(tft, 10, 14, textMaxWidth, LINE_H, I18n::t(StringId::WATCHLIST_INFO_TITLE), 0, 0, Config::SCREEN_HEIGHT, true); tft.setTextColor(TFT_GREEN, TFT_BLACK); @@ -243,11 +243,11 @@ void run(TFT_eSPI& tft) { while (!done) { tft.fillScreen(TFT_BLACK); - // Kleiner "?"-Info-Button oben rechts, gleiches Muster wie bei den - // Standort-Presets/dem WLAN-Manager. ZUERST zeichnen, damit Titel/ - // Beschreibung bewusst unterhalb davon beginnen (der Titel + zwei - // Beschreibungszeilen liefen vorher UNTER dem Button durch und - // wurden dadurch teilweise verdeckt/abgeschnitten). +// Small "?" info button top right, same pattern as with the location + // presets/WiFi manager. Draw FIRST so that title/description + // consciously start below it (the title + two description lines + // previously ran UNDER the button and were partially + // hidden/clipped). Rect infoBtn = {(int16_t)(Config::SCREEN_WIDTH - 40), 2, 30, 24}; drawButton(tft, infoBtn, "?"); diff --git a/src/airline_filter.cpp b/src/airline_filter.cpp index a7eb087..b04c89d 100644 --- a/src/airline_filter.cpp +++ b/src/airline_filter.cpp @@ -16,10 +16,10 @@ namespace { char hidden[MAX_HIDDEN][4] = {{0}}; uint8_t hiddenCount = 0; - // Schuetzt hidden[]/hiddenCount - urspruenglich nur von Core 1 (Menue- - // Screens) verwendet, seit der WebUI-Listenverwaltung (siehe - // web_export_server.cpp) aber auch von Core 0 (NetTask) aus erreichbar. - // Gleiches Muster wie AircraftDetails::mutex. +// Protects hidden[]/hiddenCount - originally only used from Core 1 (menu + // screens), but since the WebUI list management (see + // web_export_server.cpp) also reachable from Core 0 (NetTask). + // Same pattern as AircraftDetails::mutex. SemaphoreHandle_t mutex = nullptr; void ensureMutex() { diff --git a/src/brightness_screen.cpp b/src/brightness_screen.cpp index b697907..bf2b391 100644 --- a/src/brightness_screen.cpp +++ b/src/brightness_screen.cpp @@ -24,9 +24,9 @@ namespace { tft.setTextDatum(TL_DATUM); } - // Gleicher PWM-Kanal wie in main.cpp (dort in setup() mit ledcSetup() - // initialisiert) - hier bewusst als eigene Konstante dupliziert statt - // geteilt, siehe CLAUDE.md-Konvention ("jeder Screen unabhaengig"). +// Same PWM channel as in main.cpp (initialized there in setup() with + // ledcSetup()) - intentionally duplicated here as its own constant + // instead of shared, see CLAUDE.md convention ("each screen independent"). constexpr uint8_t BACKLIGHT_PWM_CHANNEL = 0; void applyLive(uint8_t percent) { diff --git a/src/brightness_screen.h b/src/brightness_screen.h index e36b82e..3cd43ff 100644 --- a/src/brightness_screen.h +++ b/src/brightness_screen.h @@ -2,9 +2,9 @@ #include #include -// Display-Helligkeit einstellen (10-100% in 10%-Schritten). Wirkt sich -// sofort live auf die Hintergrundbeleuchtung aus, damit man die Aenderung -// beim Antippen direkt sieht. +// Display brightness setting (10-100% in 10% steps). Affects the +// backlight live immediately so you can see the change +// directly when tapping. namespace BrightnessScreen { void run(TFT_eSPI& tft); } diff --git a/src/calibration_screen.h b/src/calibration_screen.h index e3e891b..6cb8de4 100644 --- a/src/calibration_screen.h +++ b/src/calibration_screen.h @@ -3,8 +3,8 @@ #include namespace CalibrationScreen { - // Blockierend: zeigt 4 Kreise (Ecken), wartet auf Antippen jeweils in - // Reihenfolge, berechnet die Kalibrierung und speichert sie auf der - // SD-Karte (via TouchInput::saveCalibration()). +// Blocking: shows 4 circles (corners), waits for tapping each in + // sequence, calculates the calibration and saves it on the + // SD card (via TouchInput::saveCalibration()). void run(TFT_eSPI& tft); } \ No newline at end of file diff --git a/src/config.h b/src/config.h index 3f99bce..6b5d440 100644 --- a/src/config.h +++ b/src/config.h @@ -2,40 +2,40 @@ #include namespace Config { - // Wird bei jedem Versions-Release von Karl aktualisiert (siehe - // CLAUDE.md-Workflow "Standard-Workflow: Push & Release") - erscheint - // im Info-Screen (Menue > System > Info) und muss zum jeweiligen - // Git-Tag passen. +// Updated by Karl on every version release (see + // CLAUDE.md workflow "Standard Workflow: Push & Release") - shown + // in the info screen (Menu > System > Info) and must match the + // corresponding Git tag. constexpr const char* APP_VERSION = "3.6.3"; - // Display-Helligkeit (Menue > System > Helligkeit), in Prozent. - // MIN bewusst nicht 0 - ein komplett dunkles Display koennte sonst wie - // ein Defekt wirken statt wie eine Einstellung. +// Display brightness (Menu > System > Brightness), in percent. + // MIN intentionally not 0 - a completely dark display could otherwise + // look like a defect rather than a setting. constexpr uint8_t BRIGHTNESS_MIN_PERCENT = 10; constexpr uint8_t BRIGHTNESS_MAX_PERCENT = 100; constexpr uint8_t BRIGHTNESS_STEP_PERCENT = 10; - // Bildschirm-Timeout (Menue > System > Bildschirm-Timeout), in Minuten, - // per Schieberegler einstellbar (siehe timeout_screen.cpp) - danach - // folgt "Nie" (kein Timeout) als eigene Endposition. Vorher nur per - // wiederholtem Antippen 0-10 durchklickbar (0 = Nie), was bei z.B. 10 - // Minuten zehn einzelne Tipps brauchte. +// Screen timeout (Menu > System > Screen Timeout), in minutes, + // adjustable via slider (see timeout_screen.cpp) - followed by + // "Never" (no timeout) as a separate end position. Previously only + // clickable 0-10 by repeated tapping (0 = Never), which needed e.g. ten + // individual taps for 10 minutes. constexpr uint8_t SCREEN_TIMEOUT_MIN_MINUTES = 1; constexpr uint8_t SCREEN_TIMEOUT_MAX_MINUTES = 15; - // Nachtmodus (22-6 Uhr) dimmt relativ zur jeweils eingestellten normalen - // Helligkeit, nicht auf einen festen Absolutwert - sonst waere der - // Dimm-Effekt bei niedrig eingestellter Normalhelligkeit wirkungslos - // oder wuerde das Display nachts sogar heller machen als tagsueber. +// Night mode (10pm-6am) dims relative to the currently set normal + // brightness, not to a fixed absolute value - otherwise the + // dimming effect would be useless at low normal brightness + // or would even make the display brighter at night than during the day. constexpr uint8_t NIGHT_DIM_REDUCTION_PERCENT = 40; constexpr const char* IP_GEO_HOST = "ip-api.com"; constexpr const char* IP_GEO_PATH = "/json/?fields=status,lat,lon,offset,countryCode"; - // Adresssuche (AddressSearchScreen) - kostenloser, anmeldefreier - // Geokodierungs-Dienst (OpenStreetMap Nominatim). Deren Nutzungsregeln - // verlangen einen aussagekraeftigen User-Agent statt des HTTPClient- - // Standardwerts, siehe https://operations.osmfoundation.org/policies/nominatim/. +// Address search (AddressSearchScreen) - free, no-registration + // geocoding service (OpenStreetMap Nominatim). Their usage policies + // require a meaningful User-Agent instead of the HTTPClient + // default, see https://operations.osmfoundation.org/policies/nominatim/. constexpr const char* NOMINATIM_HOST = "nominatim.openstreetmap.org"; constexpr const char* NOMINATIM_USER_AGENT = "EiswolfsFlightradarCYD (github.com/Eiswolf-BG/eiswolfs-flightradar-CYD)"; @@ -50,9 +50,9 @@ constexpr float RANGE_STEPS_KM[] = {10.0f, 25.0f, 50.0f, 100.0f}; constexpr uint8_t RANGE_STEP_COUNT = 4; constexpr uint8_t DEFAULT_RANGE_INDEX = 1; - // Eco-Mode: reduziert die ADS-B-Abfragefrequenz auf dieses Intervall - // (statt FETCH_INTERVAL_MS), wenn der Bildschirm durch Inaktivitaets- - // Timeout abgeschaltet ist (siehe main.cpp). + // Eco mode: reduces the ADS-B fetch frequency to this interval + // (instead of FETCH_INTERVAL_MS), when the screen is turned off by + // inactivity timeout (see main.cpp). constexpr uint32_t ECO_FETCH_INTERVAL_MS = 60000; constexpr uint8_t ALT_FILTER_COUNT = 3; @@ -63,11 +63,11 @@ constexpr float RANGE_STEPS_KM[] = {10.0f, 25.0f, 50.0f, 100.0f}; constexpr uint32_t FETCH_INTERVAL_MS = 8000; constexpr uint32_t HTTP_TIMEOUT_MS = 6000; - // Wetter-Icon im Header (siehe weather.cpp) - deutlich seltener - // abgefragt als die ADS-B-Daten, das Wetter aendert sich nicht - // minuetlich und die kostenlose Open-Meteo-API soll nicht unnoetig oft - // belastet werden. - constexpr uint32_t WEATHER_FETCH_INTERVAL_MS = 600000; // 10 Minuten +// Weather icon in the header (see weather.cpp) - queried much less + // frequently than ADS-B data, weather does not change + // every minute and the free Open-Meteo API should not be needlessly + // stressed. + constexpr uint32_t WEATHER_FETCH_INTERVAL_MS = 600000; // 10 minutes constexpr float DEFAULT_PROXIMITY_ALERT_KM = 8.0f; @@ -97,8 +97,8 @@ constexpr float RANGE_STEPS_KM[] = {10.0f, 25.0f, 50.0f, 100.0f}; constexpr uint8_t TOUCH_MISO_PIN = 39; constexpr uint8_t TOUCH_IRQ_PIN = 36; -// Display-Rotation: 0 = normal (USB-Anschluss unten), 2 = 180 Grad - // gedreht (USB oben) - fuer Montage mit Ladebuchse oben. +// Display rotation: 0 = normal (USB connector at bottom), 2 = 180 degrees + // rotated (USB at top) - for mounting with charging port on top. constexpr uint8_t DISPLAY_ROTATION = 2; constexpr int16_t SCREEN_WIDTH = 240; diff --git a/src/first_run_location_screen.cpp b/src/first_run_location_screen.cpp index 4c8ddf0..38ff2cd 100644 --- a/src/first_run_location_screen.cpp +++ b/src/first_run_location_screen.cpp @@ -24,7 +24,7 @@ namespace { tft.setTextDatum(TL_DATUM); } - // Lokale Kopie, siehe Konvention in location_presets_screen.cpp. + // Local copy, see convention in location_presets_screen.cpp. int16_t layoutWrapped(TFT_eSPI& tft, int16_t x, int16_t startY, int16_t maxWidth, int16_t lineHeight, const String& text) { int16_t y = startY; @@ -59,12 +59,11 @@ void run(TFT_eSPI& tft) { int16_t textEndY = layoutWrapped(tft, 10, 40, (int16_t)(Config::SCREEN_WIDTH - 20), 18, I18n::t(StringId::FIRST_RUN_LOCATION_BODY)); - // Buttons folgen direkt unter dem Text (mit etwas Abstand) statt an - // einer fest einprogrammierten Position - eine laengere Uebersetzung - // hat sonst den Text bis unter/hinter die Buttons laufen lassen - // (auf Deutsch beobachtet: Text unlesbar hinter "Adresse eingeben"/ - // "Ueberspringen"). Nach unten hin trotzdem an den Bildschirmrand - // geklemmt, falls ein Text ausnahmsweise doch sehr lang waere. +// Buttons follow directly below the text (with some spacing) instead of + // a hardcoded position - a longer translation would otherwise cause the + // text to run below/behind the buttons (observed in German: text + // unreadable behind "Enter address"/"Skip"). Clamped to the bottom of + // the screen in case a text happens to be very long. constexpr int16_t BTN_H = 40; constexpr int16_t BTN_GAP = 8; int16_t setY = textEndY + 14; @@ -82,9 +81,9 @@ void run(TFT_eSPI& tft) { if (setBtn.contains(tap.x, tap.y)) { if (AddressSearchScreen::run(tft)) { - // Direkt aktivieren - das ist der ganze Sinn dieses - // Screens (sofort beim ersten Start den praezisen - // Standort nutzen, statt weiter auf Auto/IP zu bleiben). +// Activate immediately - that is the whole point of this + // screen (use the precise location right on first start, + // instead of staying on auto/IP). uint8_t idx = LocationPresets::count(); if (idx > 0) LocationPresets::setActiveIndex((int8_t)(idx - 1)); } diff --git a/src/first_run_location_screen.h b/src/first_run_location_screen.h index 88dd99d..33ad3d0 100644 --- a/src/first_run_location_screen.h +++ b/src/first_run_location_screen.h @@ -3,9 +3,8 @@ #include namespace FirstRunLocationScreen { - // Einmaliger Hinweis-Screen beim allerersten Start (nach der - // Sprachauswahl) - erklaert den Genauigkeitsvorteil eines per Adresse - // gesetzten Standorts, mit direktem Einstieg in die Adresssuche. - // Ueberspringbar. +// One-time info screen on very first start (after language selection) + // - explains the accuracy advantage of a location set by address, + // with direct entry into the address search. Skippable. void run(TFT_eSPI& tft); } diff --git a/src/flight_logbook.cpp b/src/flight_logbook.cpp index 1003584..02e54f5 100644 --- a/src/flight_logbook.cpp +++ b/src/flight_logbook.cpp @@ -15,10 +15,10 @@ namespace { char seenHex[MAX_SEEN][7]; uint16_t seenCount = 0; - // Datei der aktuell laufenden Aufzeichnungs-Sitzung (ohne ".csv"), z.B. - // "2026-08-06" fuer die erste Sitzung eines Tages oder "2026-08-06_2" - // fuer ein erneutes Einschalten am selben Tag. Leer = noch nicht - // aufgeloest (Uhrzeit noch nicht synchronisiert oder Flugbuch aus). +// File of the currently active recording session (without ".csv"), e.g. + // "2026-08-06" for the first session of a day or "2026-08-06_2" + // for powering on again on the same day. Empty = not yet + // resolved (clock not yet synced or flight logbook off). char currentSessionFile[16] = {0}; void formatDateFromEpoch(uint32_t epoch, char* out, size_t outSize) { @@ -28,12 +28,12 @@ namespace { snprintf(out, outSize, "%04d-%02d-%02d", tmv.tm_year + 1900, tmv.tm_mon + 1, tmv.tm_mday); } - // Findet fuer den gegebenen Aktivierungszeitpunkt eine noch nicht - // existierende Logbuch-Datei: ".csv" fuer die erste Sitzung - // eines Tages, "_2.csv", "_3.csv" usw. fuer erneutes Einschalten - // am selben Tag - so bekommt jede Sitzung ihre eigene, im - // Logbuch-Dateien-Screen einzeln loeschbare Datei, statt in eine - // bestehende hineinzuschreiben. +// Finds a logbook file that does not yet exist for the given activation + // time: ".csv" for the first session of a day, + // "_2.csv", "_3.csv" etc. for powering on again the same day - + // this way each session gets its own file, individually deletable + // in the logbook files screen, instead of appending to + // an existing one. void resolveSessionFilename(uint32_t epoch, char* out, size_t outSize) { char dateStr[11]; formatDateFromEpoch(epoch, dateStr, sizeof(dateStr)); @@ -57,8 +57,8 @@ namespace { } } - // Sehr unwahrscheinlicher Fall (>50 Sitzungen an einem Tag): letzten - // Kandidaten weiterverwenden statt endlos zu suchen. +// Very unlikely case (>50 sessions in one day): reuse the last + // candidate instead of searching indefinitely. strncpy(out, dateStr, outSize - 1); out[outSize - 1] = 0; } @@ -92,7 +92,7 @@ namespace { if (buf[i] == '\n') lines++; } blocksRead++; - if (blocksRead % 8 == 0) delay(1); + if (blocksRead % 8 == 0) delay(1); // yield to prevent watchdog timeout during bulk SD reads } return lines; } @@ -141,17 +141,17 @@ namespace { } } blocksRead++; - if (blocksRead % 8 == 0) delay(1); + if (blocksRead % 8 == 0) delay(1); // yield during bulk SD reads } f.close(); } - // Loest die Datei der aktuellen Sitzung auf (einmalig pro Sitzung) bzw. - // uebernimmt sie nach einem Neustart erneut aus den Einstellungen - - // nur aufrufen, wenn das Flugbuch gerade eingeschaltet ist. +// Resolves the current session file (once per session) or + // adopts it again from settings after a restart - + // only call when the flight logbook is currently enabled. void ensureSessionFile() { time_t now = time(nullptr); - if (now <= 8 * 3600 * 2) return; // Uhrzeit noch nicht synchronisiert + if (now <= 8 * 3600 * 2) return; // epoch still below reasonable minimum = RTC not yet synced String persisted = SettingsStore::flightLogbookSessionFile(); if (persisted.length() > 0) { @@ -163,10 +163,10 @@ namespace { return; } - // Keine Sitzungsdatei hinterlegt - entweder frisches Einschalten - // (menu_screen.cpp loescht den Eintrag beim Umschalten bewusst) oder - // Migration von einer alten Firmware ohne Sitzungslogik. In beiden - // Faellen jetzt eine neue, garantiert einzigartige Datei anlegen. +// No session file stored - either a fresh power-on + // (menu_screen.cpp deliberately clears the entry when toggling) or + // migration from an older firmware without session logic. In both + // cases, create a new, guaranteed unique file now. uint32_t enabledAt = SettingsStore::flightLogbookEnabledAtEpoch(); if (enabledAt == 0) enabledAt = (uint32_t)now; resolveSessionFilename(enabledAt, currentSessionFile, sizeof(currentSessionFile)); @@ -204,17 +204,17 @@ void init() { void update() { if (!SettingsStore::flightLogbookEnabled()) return; - // 24h-Sicherheitsabschaltung: verhindert, dass ein unbemerkt aktives - // Flugbuch die SD-Karte nach und nach vollschreibt (siehe - // Bestaetigungsdialog beim Einschalten in menu_screen.cpp). Nur pruefen, - // wenn die Uhrzeit schon synchronisiert ist. +// 24h safety shutoff: prevents an unnoticed active + // flight logbook from filling up the SD card over time (see + // confirmation dialog when enabling in menu_screen.cpp). Only check + // when the clock has already synced. time_t nowCheck = time(nullptr); if (nowCheck > 8 * 3600 * 2) { uint32_t enabledAt = SettingsStore::flightLogbookEnabledAtEpoch(); if (enabledAt == 0) { - // Migrations-Fall: Flugbuch war schon vor diesem Update aktiv - // (alte Einstellungsdatei ohne Zeitstempel) - Startzeitpunkt - // jetzt setzen, damit die 24h-Grenze trotzdem sicher greift. +// Migration case: flight logbook was already active before this update + // (old settings file without timestamp) - set start time + // now so that the 24h limit still works safely. SettingsStore::setFlightLogbookEnabledAtEpoch((uint32_t)nowCheck); } else if ((uint32_t)nowCheck >= enabledAt && (uint32_t)nowCheck - enabledAt >= 24UL * 3600UL) { @@ -274,7 +274,7 @@ TopAltitude todayMaxAltitude() { TopAltitude result; SdMutex::Guard guard; - if (currentSessionFile[0] == 0) return result; // noch keine Sitzungsdatei bekannt + if (currentSessionFile[0] == 0) return result; // no session file known yet char filename[64]; logFilename(filename, sizeof(filename)); @@ -287,10 +287,10 @@ TopAltitude todayMaxAltitude() { uint32_t lineIdx = 0; while (f.available()) { String line = f.readStringUntil('\n'); - if (firstLine) { firstLine = false; continue; } // CSV-Header ueberspringen + if (firstLine) { firstLine = false; continue; } // skip CSV header if (line.length() == 0) continue; - // Spalten: timestamp,hex,callsign,reg,type,distance_km,altitude_ft + // Columns: timestamp,hex,callsign,reg,type,distance_km,altitude_ft int commaIdx[6]; int found = 0; int searchFrom = 0; @@ -301,7 +301,7 @@ TopAltitude todayMaxAltitude() { searchFrom = idx + 1; found++; } - if (found < 6) continue; // unvollstaendige/kaputte Zeile ueberspringen + if (found < 6) continue; // skip incomplete/corrupt line String callsign = line.substring(commaIdx[1] + 1, commaIdx[2]); String altStr = line.substring(commaIdx[5] + 1); @@ -383,9 +383,9 @@ uint8_t listDays(DayEntry* out, uint8_t maxEntries) { } uint8_t listDaySummaries(DayEntry* out, uint8_t maxEntries) { - // Scannt grosszuegiger als maxEntries, damit auch bei vielen einzelnen - // Sitzungs-Dateien pro Tag noch korrekt pro Kalendertag aufsummiert - // wird, bevor auf die angeforderte Anzahl Tage begrenzt wird. +// Scans more generously than maxEntries, so that even with many individual + // session files per day it still correctly sums per calendar day + // before limiting to the requested number of days. constexpr uint8_t MAX_RAW_SCAN = 90; static DayEntry raw[MAX_RAW_SCAN]; uint8_t rawCount = listDays(raw, MAX_RAW_SCAN); @@ -421,9 +421,9 @@ bool deleteFile(const char* label) { bool ok = SD.remove(path); if (ok && strcmp(label, currentSessionFile) == 0) { - // Die gerade aktive Sitzungsdatei wurde geloescht - Dopplungs-Liste - // zuruecksetzen, damit neue Sichtungen wieder korrekt in die (beim - // naechsten Schreibvorgang neu angelegte) Datei geloggt werden. +// The currently active session file was deleted - reset the + // deduplication list so that new sightings are correctly logged + // into the (on the next write newly created) file. seenCount = 0; } return ok; diff --git a/src/flight_logbook.h b/src/flight_logbook.h index 5bdf52f..134741a 100644 --- a/src/flight_logbook.h +++ b/src/flight_logbook.h @@ -14,44 +14,43 @@ namespace FlightLogbook { int32_t altitudeFt = 0; }; - // Sucht in der Datei der aktuellen Sitzung den Eintrag mit der hoechsten - // geloggten Flughoehe (jeweils die Hoehe BEIM ERSTEN Sichten, nicht der - // aktuelle Wert) und gibt dessen Rufzeichen + Hoehe zurueck. found=false, - // wenn noch nichts geloggt wurde oder die Datei fehlt. +// Searches the current session file for the entry with the highest + // logged altitude (each being the altitude AT FIRST SIGHTING, not the + // current value) and returns its callsign + altitude. found=false, + // if nothing has been logged yet or the file is missing. TopAltitude todayMaxAltitude(); void computeAllTimeStats(uint32_t& totalAircraft, uint16_t& totalDays); struct DayEntry { - // Nicht mehr zwingend nur ein Kalenderdatum: bei mehrfachem - // Ein-/Ausschalten am selben Tag bekommt jede Sitzung eine eigene - // Datei mit Suffix (z.B. "2026-08-06_2") - siehe - // resolveSessionFilename() in flight_logbook.cpp. Puffer - // entsprechend groesser als ein reines "YYYY-MM-DD". +// No longer necessarily just a calendar date: when powering on/off + // multiple times on the same day, each session gets its own + // file with a suffix (e.g. "2026-08-06_2") - see + // resolveSessionFilename() in flight_logbook.cpp. Buffer + // accordingly larger than a plain "YYYY-MM-DD". char date[16] = {0}; uint32_t count = 0; }; - // Eine Zeile pro Logbuch-DATEI (also ggf. mehrere pro Kalendertag, wenn - // das Flugbuch mehrfach am selben Tag ein-/ausgeschaltet wurde). Fuer - // den Logbuch-Dateien-Screen gedacht, wo jede Datei einzeln geloescht - // werden kann. +// One line per logbook FILE (so possibly multiple per calendar day, if + // the flight logbook was toggled on/off multiple times on the same day). Intended + // for the logbook files screen, where each file can be individually deleted. uint8_t listDays(DayEntry* out, uint8_t maxEntries); - // Wie listDays(), fasst aber alle Sitzungs-Dateien desselben - // Kalendertags zu einem Eintrag zusammen (Summe der Anzahl) - fuer den - // 7-Tage-Verlauf im Statistik-Bildschirm, der weiterhin pro Tag statt - // pro einzelner Sitzung zaehlen soll. +// Like listDays(), but merges all session files of the same + // calendar day into one entry (sum of counts) - for the + // 7-day history in the statistics screen, which should still count per day + // rather than per individual session. uint8_t listDaySummaries(DayEntry* out, uint8_t maxEntries); - // Loescht eine einzelne Logbuch-Datei (Label wie von listDays() - // zurueckgegeben, ohne ".csv"). Fuer die einzelnen Loesch-Buttons im - // Logbuch-Dateien-Screen gedacht. Wird gerade die aktive Sitzungsdatei - // geloescht, faengt die Aufzeichnung sauber neu in derselben Datei an. +// Deletes a single logbook file (label as returned by listDays(), + // without ".csv"). Intended for the individual delete buttons in the + // logbook files screen. If the currently active session file is + // deleted, logging restarts cleanly in the same file. bool deleteFile(const char* label); - // Loescht ALLE Logbuch-CSV-Dateien auf der SD-Karte unwiderruflich und - // setzt die "heute schon gesehen"-Liste zurueck. Fuer den Reset-Button - // im Statistik-Bildschirm gedacht. +// Deletes ALL logbook CSV files on the SD card irreversibly and + // resets the "already seen today" list. Intended for the reset button + // in the statistics screen. void resetAllData(); } diff --git a/src/i18n.h b/src/i18n.h index 10f387c..d0401c4 100644 --- a/src/i18n.h +++ b/src/i18n.h @@ -188,43 +188,42 @@ enum class StringId : uint8_t { MENU_BRIGHTNESS_PREFIX, BRIGHTNESS_TITLE, - // Grosse Warn-Ueberlage beim Einschalten des Flugbuchs (Menue > - // Flugoptionen > Flugbuch), erklaert die 24h-Sicherheitsabschaltung. +// Large warning overlay when enabling the flight logbook (Menu > + // Flight Options > Flight Logbook), explains the 24h safety shutoff. MENU_LOGBOOK_WARNING_TITLE, MENU_LOGBOOK_WARNING_BODY, - // Preset-Name beim Anlegen (optional), "Presets voll"-Hinweis beim - // Antippen der Naechster-Flughafen-Laufschrift, sowie ein zusaetzlicher - // Info-Absatz dazu (Menue > Flugoptionen > Standort-Presets > "?"). +// Preset name when creating (optional), "Presets full" hint when + // tapping the nearest-airport marquee, and an additional + // info paragraph about it (Menu > Flight Options > Location Presets > "?"). LOCATION_NAME_PROMPT, LOCATION_NAME_SKIP, LOCATION_PRESETS_FULL, LOCATION_INFO_PARA5, - // Neuer, direkt im System-Menue erreichbarer Punkt "Logbuch/WebUI" - // (statt nur ueber das versteckte "?" im Logbuch-Dateien-Screen) - - // zeigt IP + Erklaerung der Weboberflaeche (Flugbuch - // ansehen/herunterladen/loeschen). Die urspruenglich hier mit - // enthaltene Screenshot-Verwaltung wurde wieder entfernt, siehe - // Entfernung von SCREENSHOT_SAVED_PREFIX/SCREENSHOT_FAILED oben - das - // Bildschirm-Auslesen (SPI-Readback) funktioniert auf diesem - // CYD-Board hardwareseitig nicht (TFT_MISO nicht angebunden). +// New item directly reachable in the System menu "Logbook/WebUI" + // (instead of only through the hidden "?" in the logbook files screen) - + // shows IP + explanation of the web interface (view logbook + // /download/delete). The originally included screenshot + // management was removed again, see + // removal of SCREENSHOT_SAVED_PREFIX/SCREENSHOT_FAILED above - the + // screen readback (SPI-readback) does not work on this + // CYD board at the hardware level (TFT_MISO not connected). MENU_LOGBOOK_WEBUI, WEBUI_TITLE, WEBUI_INFO_PARA1, WEBUI_INFO_PARA2, - // Kleines Info-Fenster, das beim Antippen des Wetter-Icons im Header - // erscheint (siehe main.cpp/showWeatherInfo()) - erklaert, dass das - // Wetter zum aktuell aktiven Standort (bzw. aktivem Standort-Preset) - // gehoert. +// Small info window that appears when tapping the weather icon in the header + // (see main.cpp/showWeatherInfo()) - explains that the + // weather belongs to the currently active location (or active location preset). WEATHER_INFO_TITLE, WEATHER_INFO_BODY, - // Adresssuche (AddressSearchScreen) - Standort per Adresseingabe statt - // manueller Koordinaten, per kostenlosem Nominatim-Geokodierungsdienst. - // Erreichbar sowohl beim Ersteinrichten (nach der Sprachauswahl) als - // auch jederzeit ueber Standort-Presets > "+". +// Address search (AddressSearchScreen) - location by address instead + // of manual coordinates, via the free Nominatim geocoding service. + // Reachable both during first-time setup (after language selection) and + // at any time via Location Presets > "+". LOCATION_ADD_CHOICE_TITLE, LOCATION_ADD_BY_COORDS, LOCATION_ADD_BY_ADDRESS, @@ -237,60 +236,60 @@ enum class StringId : uint8_t { ADDRESS_SEARCH_TRY_AGAIN, ADDRESS_SEARCH_CANCEL, - // Grosser Hinweis-Screen beim allerersten Start (nur einmalig, nach der - // Sprachauswahl) - erklaert den Genauigkeitsvorteil eines per Adresse - // gesetzten Standorts gegenueber der automatischen IP-Standort- - // bestimmung, mit direktem Einstieg in die Adresssuche. Ueberspringbar. +// Large info screen on very first start (only once, after language + // selection) - explains the accuracy advantage of a location set via + // address compared to automatic IP geolocation, + // with direct entry into the address search. Skippable. FIRST_RUN_LOCATION_TITLE, FIRST_RUN_LOCATION_BODY, FIRST_RUN_LOCATION_SET_BTN, FIRST_RUN_LOCATION_SKIP_BTN, - // Zusaetzlicher Info-Absatz im Standort-Presets-Screen (Menue > - // Flugoptionen > Standort-Presets > "?") - weist auf den "Per Adresse - // suchen"-Weg beim "+"-Button hin (deutlich genauer als die - // automatische IP-Standortbestimmung). Ergaenzt die knappe Erwaehnung - // in LOCATION_INFO_PARA3 um denselben anschaulichen Vergleich wie im - // Ersteinrichtungs-Screen (FIRST_RUN_LOCATION_BODY) und im README. +// Additional info paragraph in the location presets screen (Menu > + // Flight Options > Location Presets > "?") - points to the "Search by + // address" option on the "+" button (significantly more accurate than + // automatic IP geolocation). Complements the brief mention + // in LOCATION_INFO_PARA3 with the same illustrative comparison as in the + // first-run setup screen (FIRST_RUN_LOCATION_BODY) and in the README. LOCATION_INFO_PARA6, - // Format-Hinweis unter dem Titel der Adresssuche (AddressSearchScreen) - // - ein Beispiel im lokalen Format (Strasse Hausnr., PLZ Ort), da das - // einzelne Freitextfeld sonst ohne jeden Hinweis war und Nutzer nicht - // wussten, was/in welcher Reihenfolge sie eingeben sollen. +// Format hint below the title of the address search (AddressSearchScreen) + // - an example in local format (Street No., Postal Code City), since the + // single free-text field had no hint at all and users did not + // know what / in which order to enter. ADDRESS_SEARCH_HINT, - // Letzter Screen der Ersteinrichtung (nach der Standort-Adresssuche, - // main.cpp) - bestaetigt den Abschluss der Einrichtung, nennt den - // SD-Karten-Ordner, in dem alle Daten liegen, und weist darauf hin, - // dass sich das WLAN ab dem naechsten Start automatisch verbindet. +// Last screen of the first-time setup (after location address search, + // main.cpp) - confirms the completion of the setup, mentions the + // SD card folder where all data is stored, and points out + // that WiFi will connect automatically from the next start onward. FIRST_RUN_COMPLETE_TITLE, FIRST_RUN_COMPLETE_BODY1, FIRST_RUN_COMPLETE_BODY2, - // Hinweis mit Beispielnamen unter dem Titel der Namens-Tastatur - // (location_presets_screen.cpp::runPresetNameKeypad UND - // address_search_screen.cpp::runNameKeypad, beide spiegeln sich) - - // ohne jeden Hinweis wussten Einsteiger nicht, was fuer ein Name hier - // gemeint ist (Preset-Name wie "Zuhause", nicht z.B. ein Ort/Adresse). +// Hint with example name below the title of the name keypad + // (location_presets_screen.cpp::runPresetNameKeypad AND + // address_search_screen.cpp::runNameKeypad, both mirror each other) - + // without any hint, beginners did not know what kind of name + // is meant here (preset name like "Home", not e.g. a city/address). LOCATION_NAME_HINT, - // Legenden-Eintrag auf dem Radar-Screen (radar_screen.cpp::drawLegend) - // fuer die blauen Quadrat-Marker der Bodenfahrzeuge - nur sichtbar, - // wenn "Bodenfahrzeuge ausblenden" AUS ist (Flugoptionen), da die - // Fahrzeuge dann als eigene Legenden-Zeile unter den drei - // Hoehen-Farbstufen erscheinen. +// Legend entry on the radar screen (radar_screen.cpp::drawLegend) + // for the blue square markers of ground vehicles - only visible + // when "Hide ground vehicles" is OFF (Flight Options), since the + // vehicles then appear as their own legend line below the three + // altitude color levels. LEGEND_GROUND_VEHICLE, - // Neue "Sicherung & Reset"-Unterseite im System-Menue (fasst die - // bisher einzeln im System-Menue stehenden Backup/Restore-Buttons - // zusammen und ergaenzt einen dritten Punkt "Einstellungen - // zuruecksetzen" - ein kompletter Werksreset fuer Entwickler/Tester, - // der den gesamten Flightradar-Ordner von der SD-Karte loescht und - // neu startet, siehe settings_backup.cpp::factoryReset()). Titel und - // Button-Label teilen sich MENU_BACKUP_RESET, genau wie - // MENU_CATEGORY_SYSTEM sowohl fuer den Menue-Button als auch den - // Bildschirmtitel der System-Seite verwendet wird. +// New "Backup & Reset" subsection in the System menu (combines the + // previously individually placed Backup/Restore buttons in the System menu + // and adds a third item "Reset + // settings" - a complete factory reset for developers/testers, + // which deletes the entire Flightradar folder from the SD card and + // restarts, see settings_backup.cpp::factoryReset()). Title and + // button label share MENU_BACKUP_RESET, just like + // MENU_CATEGORY_SYSTEM is used both for the menu button and the + // screen title of the System page. MENU_BACKUP_RESET, MENU_FACTORY_RESET, MENU_FACTORY_RESET_WARNING_BODY, @@ -303,26 +302,25 @@ enum class StringId : uint8_t { // instead of waiting it out. FIRST_RUN_COMPLETE_TAP_TO_SKIP, - // Zeilenpraefix im Radar-Detailpanel (radar_screen.cpp::drawDetailPanel) - // fuer die Flugroute (Start- -> Zielflughafen, ICAO-Code), abgefragt - // ueber AircraftDetails per Rufzeichen. Faellt wie MODEL/TYPE/SQUAWK auf - // DETAIL_UNKNOWN zurueck, wenn kein Rufzeichen bekannt ist oder keine - // Route gefunden wurde. +// Line prefix in the radar detail panel (radar_screen.cpp::drawDetailPanel) + // for the flight route (departure -> destination airport, ICAO code), queried + // via AircraftDetails by callsign. Falls back like MODEL/TYPE/SQUAWK to + // DETAIL_UNKNOWN if no callsign is known or no + // route was found. DETAIL_ROUTE, - // Hinweiszeile ueber dem QR-Code auf dem neuen QR-Unterscreen der - // Logbuch/WebUI-Seite (webui_screen.cpp::runQrScreen) - der WEBUI_TITLE- - // String wird fuer die Kopfzeile wiederverwendet, kein eigener Titel - // noetig. +// Hint line above the QR code on the new QR sub-screen of the + // Logbook/WebUI page (webui_screen.cpp::runQrScreen) - the WEBUI_TITLE + // string is reused for the header line, no separate title needed. WEBUI_QR_HINT, - // Ruhebildschirm bei Inaktivitaets-Timeout (Menue > System > - // "Ruhebildschirm", siehe SettingsStore::screensaverEnabled() und +// Screensaver during inactivity timeout (Menu > System > + // "Screensaver", see SettingsStore::screensaverEnabled() and // main.cpp). MENU_SCREENSAVER, - // OTA-Firmware-Update ueber WLAN (Menue > System > "Nach Update - // suchen", siehe menu_screen.cpp::runOtaUpdateScreen() und +// OTA firmware update over WiFi (Menu > System > "Check for + // update", see menu_screen.cpp::runOtaUpdateScreen() and // ota_update.h/.cpp). MENU_CHECK_UPDATE, OTA_CHECKING, @@ -334,23 +332,23 @@ enum class StringId : uint8_t { OTA_UPDATE_FAILED, OTA_UPDATE_SUCCESS, - // Erfolgs-/Fehler-Ergebnis nach Download+Flash wird jetzt als - // dauerhafter Info-Screen mit explizitem Button angezeigt (statt - // automatischem Neustart bzw. kurzer, automatisch verschwindender - // Meldung) - der Nutzer muss bei einer sicherheitsrelevanten Aktion wie - // einem Firmware-Update IMMER aktiv informiert werden und selbst - // bestaetigen, siehe menu_screen.cpp::infoScreen(). +// Success/error result after download+flash is now shown as + // a permanent info screen with explicit button (instead of + // automatic restart or a brief, auto-dismissing + // message) - the user MUST be actively informed about a security-relevant + // action like a firmware update and must + // confirm it themselves, see menu_screen.cpp::infoScreen(). OTA_SUCCESS_BODY, OTA_RESTART_BUTTON, OTA_FAILED_BODY, - // Eigener Bildschirm-Timeout-Screen (Menue > System > Bildschirm- - // Timeout, siehe timeout_screen.cpp) mit Schieberegler statt des - // vorherigen Durchklickens per wiederholtem Antippen - der - // Ruhebildschirm-Umschalter (vorher eigene Zeile im System-Menue, - // MENU_SCREENSAVER) zieht mit auf diesen Screen um, da er inhaltlich - // eng mit dem Timeout zusammenhaengt und hier genug Platz fuer eine - // kurze Erklaerung ist. +// Dedicated screen timeout screen (Menu > System > Screen + // Timeout, see timeout_screen.cpp) with slider instead of the + // previous clicking through by repeated tapping - the + // screensaver toggle (previously its own line in the System menu, + // MENU_SCREENSAVER) moves to this screen as well, since it is + // closely related to the timeout and there is enough space for a + // brief explanation here. TIMEOUT_SCREEN_TITLE, TIMEOUT_SCREENSAVER_DESC, diff --git a/src/led_alert.h b/src/led_alert.h index 319ae89..8c34753 100644 --- a/src/led_alert.h +++ b/src/led_alert.h @@ -1,9 +1,9 @@ #pragma once #include -// Steuert die diskrete RGB-LED auf der Rueckseite des CYD (kein Lautsprecher -// vorhanden, daher ersetzt die LED den akustischen Alarm vom Cardputer- -// Projekt). Pins: Rot=GPIO4, Gruen=GPIO16, Blau=GPIO17, active-low (LOW = an). +// Controls the discrete RGB LED on the back of the CYD (no speaker +// present, therefore the LED replaces the acoustic alarm from the Cardputer +// project). Pins: Red=GPIO4, Green=GPIO16, Blue=GPIO17, active-low (LOW = on). namespace LedAlert { enum class Mode { @@ -18,9 +18,9 @@ namespace LedAlert { void pulseHeartbeat(uint32_t nowMs); - // Blockierendes, kurzes weisses Aufblitzen (alle 3 Farbkanaele an) als - // sofortige Bestaetigung fuer eine einmalige Nutzeraktion (z.B. "Cam"- - // Button getroffen). Bewusst blockierend (delay), da nur aus dem - // Haupt-Loop bei einem Tap aufgerufen, nicht aus der NetTask-Schleife. +// Blocking, short white flash (all 3 color channels on) as + // immediate confirmation for a one-time user action (e.g. "Cam" + // button hit). Deliberately blocking (delay), since only called from the + // main loop on a tap, not from the NetTask loop. void flashWhite(uint32_t durationMs = 150); } \ No newline at end of file diff --git a/src/location_manager.h b/src/location_manager.h index 0a4323c..c86760d 100644 --- a/src/location_manager.h +++ b/src/location_manager.h @@ -20,14 +20,13 @@ namespace LocationManager { bool hasGpsFix(); - // UTC-Offset in Sekunden (inkl. evtl. Sommerzeit), ermittelt bei der - // IP-Geolocation-Abfrage. 0/false, falls noch nicht bekannt. +// UTC offset in seconds (including possible DST), determined during the + // IP geolocation query. 0/false, if not yet known. bool hasUtcOffset(); int32_t utcOffsetSeconds(); - // Ob die Region (per IP-Geolocation-Laendercode) metrische Einheiten - // nutzt (Meter/km) statt Fuss/Meilen. Default true (metrisch), bis die - // IP-Abfrage etwas anderes ermittelt hat - nur die USA nutzen aktuell - // eine Ausnahme. +// Whether the region (per IP geolocation country code) uses metric units + // (meters/km) instead of feet/miles. Default true (metric), until the + // IP query determines otherwise - currently only the USA is an exception. bool useMetricUnits(); } \ No newline at end of file diff --git a/src/location_presets.cpp b/src/location_presets.cpp index dbbf37b..36fda58 100644 --- a/src/location_presets.cpp +++ b/src/location_presets.cpp @@ -13,10 +13,9 @@ namespace { struct Preset { double lat = 0; double lon = 0; - // Optionaler, vom Nutzer vergebener Name (z.B. "Zuhause") oder vom - // naechstgelegenen Flughafen uebernommen - leer, wenn keiner gesetzt - // wurde. 16 Zeichen + Nullterminierung reichen fuer eine gut lesbare - // Zeile in der Preset-Liste. +// Optional user-assigned name (e.g. "Home") or taken from the nearest + // airport - empty if none was set. 16 characters + null terminator + // are sufficient for a well-readable line in the preset list. char name[17] = {0}; }; @@ -65,9 +64,9 @@ namespace { if (comma1 < 0) continue; if (presetCount >= MAX_PRESETS) break; - // Der Name ist ein optionales drittes Feld - aeltere, - // gespeicherte Presets (vor diesem Feature) haben nur - // "lat,lon" ohne zweites Komma, dann bleibt der Name leer. +// The name is an optional third field - older, + // saved presets (before this feature) only have + // "lat,lon" without a second comma, then the name stays empty. int comma2 = line.indexOf(',', comma1 + 1); presets[presetCount].lat = line.substring(0, comma1).toDouble(); if (comma2 < 0) { diff --git a/src/location_presets.h b/src/location_presets.h index 4aaa637..f2764f5 100644 --- a/src/location_presets.h +++ b/src/location_presets.h @@ -8,9 +8,8 @@ namespace LocationPresets { uint8_t count(); void getLatLon(uint8_t index, double& lat, double& lon); - // Vom Nutzer vergebener Name (z.B. "Zuhause") - leerer String, wenn - // keiner gesetzt wurde (dann zeigt der Screen stattdessen die - // Koordinaten an). +// User-assigned name (e.g. "Home") - empty string if none was set + // (the screen will show the coordinates instead). String getName(uint8_t index); bool addPreset(double lat, double lon, const String& name = String()); diff --git a/src/location_presets_screen.cpp b/src/location_presets_screen.cpp index 5513f7e..3a93892 100644 --- a/src/location_presets_screen.cpp +++ b/src/location_presets_screen.cpp @@ -116,11 +116,11 @@ namespace { return confirmed ? String(buf) : String(); } - // Vier Tastaturreihen wie bei der Rufzeichen-Eingabe (siehe - // aircraft_watchlist_screen.cpp::runCallsignKeypad), aber mit Leertaste - // (Ortsnamen enthalten oft Leerzeichen, z.B. "Bei Oma") und OHNE - // Eingabepflicht - der Name ist optional, "Ohne Namen" bestaetigt mit - // leerem Puffer statt die Eingabe abzubrechen. +// Four keyboard rows like for callsign input (see + // aircraft_watchlist_screen.cpp::runCallsignKeypad), but with space bar + // (location names often contain spaces, e.g. "Bei Oma") and WITHOUT + // mandatory input - the name is optional, "Without name" confirms with + // empty buffer instead of cancelling input. String runPresetNameKeypad(TFT_eSPI& tft) { MenuStars::reset(); constexpr const char* DIGITS = "1234567890"; @@ -135,9 +135,9 @@ namespace { constexpr int16_t KEY_GAP = 3; constexpr int16_t FIELD_H = 34; - // Kopfbereich (Titel + Namens-Hinweis) einmal zeichnen, um seine - // tatsaechliche Hoehe per getCursorY() zu messen - selbes Muster wie - // in address_search_screen.cpp::runAddressKeyboard(). +// Draw header (title + name hint) once to measure its + // actual height via getCursorY() - same pattern as + // in address_search_screen.cpp::runAddressKeyboard(). tft.fillScreen(TFT_BLACK); tft.setTextColor(TFT_GREEN, TFT_BLACK); tft.setCursor(10, 14); @@ -258,17 +258,16 @@ namespace { String name = runPresetNameKeypad(tft); if (!LocationPresets::addPreset(lat, lon, name)) return false; - // Neu angelegtes Preset sofort aktivieren, gleiches Verhalten wie - // beim Anlegen per Adresssuche (siehe address_search_screen.cpp) - - // sonst blieb z.B. der automatische IP-Standort aktiv, obwohl gerade - // extra ein neuer Standort eingegeben wurde. +// Activate newly created preset immediately, same behavior as + // when creating via address search (see address_search_screen.cpp) - + // otherwise the automatic IP location would remain active even though + // a new location was just explicitly entered. LocationPresets::setActiveIndex((int8_t)(LocationPresets::count() - 1)); return true; } - // Erster Schritt beim Antippen von "+": manuelle Koordinaten oder - // Adresssuche (AddressSearchScreen, kuemmert sich dort bereits selbst - // um Namensvergabe + Speichern als Preset). +// First step when tapping "+": manual coordinates or + // address search (AddressSearchScreen handles naming + saving as preset itself). bool addPresetFlow(TFT_eSPI& tft) { MenuStars::reset(); tft.fillScreen(TFT_BLACK); @@ -293,8 +292,8 @@ namespace { } } - // Kurze Meldung unten am Bildschirmrand, z.B. wenn beim Antippen des - // Naechster-Flughafen-Textes bereits alle 3 Preset-Slots belegt sind. +// Brief message at the bottom of the screen, e.g. when tapping the + // nearest airport text but all 3 preset slots are already occupied. void showBriefMessage(TFT_eSPI& tft, const String& msg, uint16_t color) { tft.fillRect(0, Config::SCREEN_HEIGHT - 18, Config::SCREEN_WIDTH, 18, TFT_BLACK); tft.setTextColor(color, TFT_BLACK); @@ -334,50 +333,49 @@ namespace { return y; } - // Laufschrift fuer den "Naechster Flughafen"-Text: statt ihn mit "..." - // abzuschneiden, scrollt er horizontal durch, falls er nicht in die - // verfuegbare Breite passt. BEWUSST OHNE tft.setViewport() (das hatte - // im Zusammenspiel mit unserem eigenen Font dazu gefuehrt, dass der Text - // komplett unsichtbar wurde) - stattdessen wird bei jedem Schritt einfach - // der Text-Ausschnitt neu berechnet, der garantiert in die Breite passt. +// Marquee text for the "Nearest Airport" text: instead of truncating with "..." + // it scrolls horizontally if it does not fit the available width. + // DELIBERATELY WITHOUT tft.setViewport() (which had caused the text to become + // completely invisible when used with our custom font) - instead, at each step + // the text portion that is guaranteed to fit the width is simply recalculated. struct Marquee { String text; - String ring; // text + Luecke, doppelt aneinandergehaengt + String ring; // text + gap, doubled end-to-end bool needsScroll = false; int32_t charOffset = 0; uint32_t lastStepMs = 0; }; Marquee airportMarquee; - // Etwas kuerzeres Intervall als frueher fuer einen ruhigeren Lauf. - // WICHTIG: der Cursor wird beim Zeichnen IMMER exakt auf x gesetzt - // (nie davor/danach verschoben) - ein Versuch mit pixelgenauer - // Sub-Zeichen-Verschiebung (Cursor links von x) hat zu einem - // fehlerhaft gezeichneten Zeichen ausserhalb des geloeschten Bereichs - // gefuehrt (sichtbar als gruener Kasten links vom Text). - constexpr uint32_t MARQUEE_STEP_MS = 200; // alle 200ms ein Zeichen weiter +// Slightly shorter interval than before for a smoother run. + // IMPORTANT: the cursor is ALWAYS set exactly to x when drawing + // (never shifted before/after) - an attempt with pixel-precise + // sub-character shifting (cursor left of x) led to a + // incorrectly drawn character outside the erased area + // (visible as a green box left of the text). + constexpr uint32_t MARQUEE_STEP_MS = 200; // advance one character every 200ms - // Einmal aufrufen, wenn sich der anzuzeigende Text aendert (z.B. weil ein - // anderer Standort aktiviert wurde) - merkt sich Text + Ringpuffer und - // setzt den Scroll-Fortschritt zurueck. +// Call once when the displayed text changes (e.g. because a + // different location was activated) - stores text + ring buffer and + // resets the scroll progress. void setupMarquee(TFT_eSPI& tft, const String& text, int16_t viewportW) { - // WICHTIG: textWidth() haengt von der aktuell gesetzten Textgroesse - // ab. Der Marquee wird in Size 2 gezeichnet (siehe drawMarquee), - // also muss hier ebenfalls Size 2 aktiv sein, sonst wird die - // Breite falsch (zu schmal) gemessen und der Text ragt beim - // Zeichnen ueber den verfuegbaren Platz hinaus. +// IMPORTANT: textWidth() depends on the currently set text size. + // The marquee is drawn in Size 2 (see drawMarquee), + // so Size 2 must also be active here, otherwise the + // width will be measured incorrectly (too narrow) and the text will + // protrude beyond the available space when drawn. tft.setTextSize(2); airportMarquee.text = text; airportMarquee.needsScroll = tft.textWidth(text) > viewportW; tft.setTextSize(1); - String withGap = text + " "; // 3 Leerzeichen Luecke vor der Wiederholung + String withGap = text + " "; // 3 spaces gap before the repeat airportMarquee.ring = withGap + withGap; airportMarquee.charOffset = 0; airportMarquee.lastStepMs = millis(); } - // Liefert den laengsten Ausschnitt ab startIdx, der noch in maxWidth - // passt - OHNE "..." anzuhaengen (im Gegensatz zu truncateForWidth). +// Returns the longest substring from startIdx that still fits in maxWidth + // WITHOUT appending "..." (unlike truncateForWidth). String marqueeWindow(TFT_eSPI& tft, const String& src, int32_t startIdx, int16_t maxWidth) { String s = src.substring(startIdx); while (s.length() > 1 && tft.textWidth(s) > maxWidth) { @@ -386,25 +384,25 @@ namespace { return s; } - // Bei jedem Aufruf (auch in der Warteschleife) neu zeichnen - wenn der - // Text nicht scrollen muss, wird er einfach normal (fest) angezeigt. +// Redraw on every call (also in the wait loop) - if the + // text does not need to scroll, it is simply displayed normally (static). void drawMarquee(TFT_eSPI& tft, int16_t x, int16_t y, int16_t w, int16_t h) { if (airportMarquee.text.length() == 0) return; - // Der geloeschte Bereich muss zur tatsaechlichen Texthoehe bei - // Size 2 passen (~16px), nicht zur alten Size-1-Hoehe - sonst - // bleiben oben Reste vom vorherigen Frame stehen (sichtbar als - // Strich ueber dem Text). Etwas grosszuegiger nach oben (start - // bei y-20) als vorher, da ein einzelner Frame-Ausreisser sonst - // noch sichtbar blieb. +// The erased area must match the actual text height at + // Size 2 (~16px), not the old Size 1 height - otherwise + // remnants from the previous frame remain at the top (visible as + // a line above the text). Slightly more generous upward (start + // at y-20) than before, since a single frame outlier would otherwise + // remain visible. constexpr int16_t MARQUEE_CLEAR_TOP = 20; constexpr int16_t MARQUEE_CLEAR_H = 26; tft.fillRect(x, y - MARQUEE_CLEAR_TOP, w, MARQUEE_CLEAR_H, TFT_BLACK); tft.setTextColor(TFT_DARKGREEN, TFT_BLACK); - // Groessere Schrift fuer den Nearest-Airport-Marquee (auf Wunsch - // vergroessert von Size 1 auf Size 2) - nach dem Zeichnen wieder auf - // Size 1 zurueckstellen, damit nachfolgender Code (z.B. der - // "Zurueck"-Button) nicht versehentlich auch vergroessert wird. +// Larger font for the Nearest Airport marquee (enlarged + // from Size 1 to Size 2 on request) - reset back to + // Size 1 after drawing so that subsequent code (e.g. the + // "Back" button) is not accidentally enlarged as well. tft.setTextSize(2); tft.setCursor(x, y); @@ -418,8 +416,8 @@ namespace { if (now - airportMarquee.lastStepMs >= MARQUEE_STEP_MS) { airportMarquee.lastStepMs = now; airportMarquee.charOffset++; - // Zurueck an den Anfang, sobald der erste (nicht doppelte) - // Text+Luecke-Block durchgelaufen ist. +// Back to the beginning once the first (non-duplicate) + // text+gap block has completed. int32_t singleLen = (int32_t)airportMarquee.text.length() + 3; if (airportMarquee.charOffset >= singleLen) airportMarquee.charOffset = 0; } @@ -428,11 +426,11 @@ namespace { tft.setTextSize(1); } - // Laufschrift-Zustand pro Preset-Zeile, analog zu airportMarquee oben, - // aber als Array (eine Instanz je Zeile) und LINKSBUENDIG gezeichnet - // statt zentriert - zentriertes Scrollen wuerde bei jedem Schritt - // sichtbar hin- und herspringen, da sich die Textbreite laufend - // aendert. Ersetzt die bisherige truncateForWidth()-Kuerzung mit "...". +// Marquee state per preset row, analogous to airportMarquee above, + // but as an array (one instance per row) and LEFT-ALIGNED + // instead of centered - centered scrolling would visibly jump + // back and forth at each step since the text width constantly + // changes. Replaces the previous truncateForWidth() shortening with "...". struct RowMarquee { String text; String ring; @@ -442,8 +440,8 @@ namespace { }; RowMarquee rowMarquees[LocationPresets::MAX_PRESETS]; - // Wie setupMarquee() oben, aber ohne Textgroessen-Umschaltung (die - // Preset-Zeilen nutzen durchgehend Size 1). +// Like setupMarquee() above, but without text size switching (the + // preset rows consistently use Size 1). void setupRowMarquee(TFT_eSPI& tft, RowMarquee& m, const String& text, int16_t maxWidth) { m.text = text; m.needsScroll = tft.textWidth(text) > maxWidth; @@ -453,12 +451,11 @@ namespace { m.lastStepMs = millis(); } - // Zeichnet eine Preset-Zeile im selben Rahmen-/Fuellstil wie - // drawButton(), aber mit linksbuendiger Laufschrift statt zentriertem, - // ggf. abgeschnittenem Text. Wird sowohl beim ersten Bildschirmaufbau - // als auch (nur fuer Zeilen mit needsScroll) bei jedem Tick in der - // Warteschleife erneut aufgerufen, um den naechsten Scroll-Schritt zu - // zeichnen. +// Draws a preset row in the same frame/fill style as + // drawButton(), but with left-aligned marquee text instead of centered, + // potentially truncated text. Called both on initial screen build + // and (only for rows with needsScroll) on each tick in the + // wait loop to draw the next scroll step. void drawRowMarquee(TFT_eSPI& tft, const Rect& r, RowMarquee& m, bool active) { uint16_t bg = active ? TFT_GREEN : TFT_BLACK; uint16_t fg = active ? TFT_BLACK : TFT_GREEN; @@ -611,19 +608,19 @@ void run(TFT_eSPI& tft) { String presetName = LocationPresets::getName(i); String label; if (presetName.length() > 0) { - // Benannte Presets (manuell vergeben oder vom - // Flughafen-Antippen uebernommen) zeigen den Namen statt - // der Koordinaten - besser lesbar in der Uebersicht. +// Named presets (manually assigned or taken over from + // tapping an airport) show the name instead of + // the coordinates - more readable in the overview. label = (active == (int8_t)i ? "> " : "") + presetName; } else { char coords[24]; snprintf(coords, sizeof(coords), "%d: %.2f, %.2f", i + 1, lat, lon); label = (active == (int8_t)i ? "> " : "") + presetWord + " " + coords; } - // Laufschrift statt "..."-Kuerzung fuer lange Preset-Namen - // bzw. Koordinaten, die nicht in die Zeilenbreite passen - // (siehe RowMarquee oben) - kurze Labels bleiben unveraendert - // zentriert wie zuvor. +// Marquee instead of "..." truncation for long preset names + // or coordinates that do not fit the row width + // (see RowMarquee above) - short labels remain unchanged + // centered as before. if (tft.textWidth(label) <= rowRect.w - 10) { rowMarquees[i].needsScroll = false; drawButton(tft, rowRect, label, active == (int8_t)i); @@ -653,10 +650,10 @@ void run(TFT_eSPI& tft) { constexpr int16_t AIRPORT_LINE_X = 10; constexpr int16_t AIRPORT_LINE_W = Config::SCREEN_WIDTH - 20; int16_t airportLineY = (int16_t)(Config::SCREEN_HEIGHT - 60); - // Deckt denselben Bereich ab, den drawMarquee() bei jedem Frame - // loescht (siehe MARQUEE_CLEAR_TOP/-H) - dient sowohl als Tap-Zone - // als auch als sichtbarer Rahmen, der anzeigt, dass man hier - // antippen kann, um den Flughafen als neuen Preset zu uebernehmen. +// Covers the same area that drawMarquee() clears on every frame + // (see MARQUEE_CLEAR_TOP/-H) - serves both as a tap zone + // and as a visible frame indicating that you can + // tap here to adopt the airport as a new preset. Rect airportRect = {(int16_t)(AIRPORT_LINE_X - 4), (int16_t)(airportLineY - 20), (int16_t)(AIRPORT_LINE_W + 8), 26}; AirportLookup::Nearest nearest; @@ -669,8 +666,8 @@ void run(TFT_eSPI& tft) { } nearest = AirportLookup::findNearest(activeLat, activeLon); if (nearest.found) { - // Respektiert jetzt die Einheiten-Einstellung (Menue > - // Einheiten) - vorher immer "(XX km)", auch bei Imperial. +// Now respects the units setting (Menu > + // Units) - previously always "(XX km)", even in imperial. char buf[48]; if (LocationManager::useMetricUnits()) { snprintf(buf, sizeof(buf), "%s %s (%.0f km)", nearest.icao, nearest.name, nearest.distanceKm); @@ -726,8 +723,8 @@ void run(TFT_eSPI& tft) { if (!handled && nearest.found && airportRect.contains(tap.x, tap.y)) { if (canAdd) { if (LocationPresets::addPreset(nearest.lat, nearest.lon, nearest.name)) { - // Gleiches Verhalten wie bei den anderen beiden Wegen, - // ein Preset anzulegen - sofort aktivieren. +// Same behavior as with the other two ways + // of creating a preset - activate immediately. LocationPresets::setActiveIndex((int8_t)(LocationPresets::count() - 1)); } } else { diff --git a/src/logbook_files_screen.cpp b/src/logbook_files_screen.cpp index 5655301..c9a99e5 100644 --- a/src/logbook_files_screen.cpp +++ b/src/logbook_files_screen.cpp @@ -29,8 +29,8 @@ namespace { constexpr uint8_t MAX_DAYS_QUERIED = 31; constexpr uint8_t VISIBLE_ROWS = 10; - // Erste Inhaltszeile eine Zeile tiefer als frueher (30) ansetzen, damit - // sie nicht mit dem "?"-Info-Button oben rechts (y=2..26) kollidiert. +// Start the first content line one line lower than before (30), so that + // it does not collide with the "?" info button at top right (y=2..26). constexpr int16_t ROWS_START_Y = 50; int16_t layoutWrapped(TFT_eSPI& tft, int16_t x, int16_t startY, int16_t maxWidth, @@ -197,10 +197,10 @@ void run(TFT_eSPI& tft) { tft.setCursor(110, y); tft.print(String(days[i].count) + I18n::t(StringId::LOGFILES_AIRCRAFT_SUFFIX)); - // Jede Datei einzeln loeschbar (selber Stil wie das rote 'X' - // bei den WLAN-Netzwerken) - keine Bestaetigung noetig, das - // globale "Flugbuch zuruecksetzen" im Statistik-Screen - // bleibt fuer den Alles-loeschen-Fall. +// Each file individually deletable (same style as the red 'X' + // for WiFi networks) - no confirmation needed, the + // global "Reset flight logbook" in the statistics screen + // remains for deleting everything. Rect delBtn = {(int16_t)(Config::SCREEN_WIDTH - 34), (int16_t)(y - 15), 30, 18}; drawButton(tft, delBtn, "X", true); deleteRects[deleteCount++] = delBtn; diff --git a/src/logbook_files_screen.h b/src/logbook_files_screen.h index 54d9a56..d59f9bc 100644 --- a/src/logbook_files_screen.h +++ b/src/logbook_files_screen.h @@ -3,7 +3,7 @@ #include namespace LogbookFilesScreen { - // Blockierend: listet die Logbuch-Dateien (ein Tag pro Zeile, mit Anzahl - // Eintraegen) auf. Kehrt zurueck, sobald "Back" angetippt wird. +// Blocking: lists the logbook files (one day per line, with number + // of entries). Returns as soon as "Back" is tapped. void run(TFT_eSPI& tft); } \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index ff836a9..5a6343b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -61,26 +61,26 @@ uint32_t lastInteractionMs = 0; bool screenDimmed = false; bool nightDimActive = false; -// Ruhebildschirm (Menue > System > Ruhebildschirm, siehe SettingsStore:: -// screensaverEnabled()) - true, waehrend statt des komplett dunklen -// Backlights ein gedimmter Sternenhimmel mit Uhrzeit angezeigt wird. -// Getrennt von screenDimmed, da NICHT jeder Timeout automatisch ein -// Ruhebildschirm ist (Default bleibt: Backlight komplett aus) - siehe -// loop() weiter unten. +// Screensaver (Menu > System > Screensaver, see SettingsStore:: +// screensaverEnabled()) - true while a dimmed starry sky with clock is shown +// instead of the completely dark backlight. +// Separate from screenDimmed, since NOT every timeout automatically becomes a +// screensaver (default remains: backlight completely off) - see +// loop() below. bool screensaverShowing = false; uint32_t lastScreensaverClockMs = 0; -// Wandelt die Nutzer-Helligkeit (Menue > System > Helligkeit, 10-100%) in -// einen PWM-Wert 0-255 um - ersetzt das bisher fest verdrahtete -// BACKLIGHT_FULL als "normale" Helligkeit ueberall unten. +// Converts user brightness (Menu > System > Brightness, 10-100%) into +// a PWM value 0-255 - replaces the previously hardwired +// BACKLIGHT_FULL as the "normal" brightness everywhere below. uint8_t normalBacklightPwm() { return (uint8_t)((uint16_t)SettingsStore::brightnessPercent() * 255 / 100); } -// Nachtmodus-Helligkeit RELATIV zur normalen Helligkeit (siehe -// Config::NIGHT_DIM_REDUCTION_PERCENT) statt eines festen Absolutwerts - -// so bleibt der Dimm-Effekt bei JEDER eingestellten Normalhelligkeit -// spuerbar, auch bei schon niedrig eingestellter Helligkeit. +// Night mode brightness RELATIVE to normal brightness (see +// Config::NIGHT_DIM_REDUCTION_PERCENT) instead of a fixed absolute value - +// so the dimming effect remains noticeable at EVERY set normal brightness, +// even when already set low. uint8_t nightDimBacklightPwm() { uint32_t normal = normalBacklightPwm(); return (uint8_t)(normal * (100 - Config::NIGHT_DIM_REDUCTION_PERCENT) / 100); @@ -95,9 +95,9 @@ struct Rect { Rect menuBtn = {Config::SCREEN_WIDTH - 90, 3, 54, 22}; -// Platz, an dem frueher der Cam-Button war (siehe entfernte Screenshot- -// Funktion) - zeigt jetzt stattdessen ein kleines Wetter-Icon fuer den -// aktuell aktiven Standort (siehe weather.cpp/Weather::update()). +// Space where the camera button used to be (see removed screenshot +// function) - now shows a small weather icon for the +// currently active location (see weather.cpp/Weather::update()). Rect weatherIconRect = {(int16_t)(menuBtn.x - 46), 3, 42, 22}; void drawMenuButton() { @@ -109,8 +109,8 @@ void drawMenuButton() { tft.setTextDatum(TL_DATUM); } -// Kleine, mit einfachen TFT_eSPI-Grundformen gezeichnete Wolke (kein -// Bild/Font noetig) - Basis fuer die Regen/Schnee/Gewitter-Varianten. +// Small cloud drawn with simple TFT_eSPI basic shapes (no +// image/font needed) - base for the rain/snow/thunderstorm variants. void drawCloudShape(int16_t cx, int16_t cy, uint16_t color) { tft.fillCircle(cx - 5, cy + 1, 3, color); tft.fillCircle(cx - 1, cy - 2, 4, color); @@ -118,7 +118,7 @@ void drawCloudShape(int16_t cx, int16_t cy, uint16_t color) { tft.fillRect(cx - 8, cy, 13, 3, color); } -// Sonne mit ein paar Strahlen ringsum, ebenfalls nur aus Grundformen. +// Sun with a few rays around, also only from basic shapes. void drawSunShape(int16_t cx, int16_t cy, int16_t r, uint16_t color) { tft.fillCircle(cx, cy, r, color); for (uint8_t i = 0; i < 8; i++) { @@ -131,10 +131,10 @@ void drawSunShape(int16_t cx, int16_t cy, int16_t r, uint16_t color) { } } -// Zeichnet das Wetter-Icon passend zur zuletzt abgefragten Wetterlage -// (Weather::current()) - bei Weather::Condition::Unknown (noch keine -// erfolgreiche Abfrage, z.B. kurz nach dem Booten) bleibt die Flaeche -// einfach leer, statt einen Platzhalter anzuzeigen. +// Draws the weather icon matching the last queried weather condition +// (Weather::current()) - if Weather::Condition::Unknown (no successful +// query yet, e.g. shortly after boot) the area stays +// empty instead of showing a placeholder. void drawWeatherIcon() { int16_t cx = weatherIconRect.x + weatherIconRect.w / 2; int16_t cy = weatherIconRect.y + weatherIconRect.h / 2; @@ -176,11 +176,11 @@ void drawWeatherIcon() { } } -// Einfacher Zeilenumbruch fuer Fliesstext, analog zu layoutWrapped() in -// menu_screen.cpp/webui_screen.cpp (siehe "keine geteilten Module"-Konvention -// - jede Screen-Datei hat ihre eigene kleine Kopie). Ohne Scroll-Unterstuetzung, -// da der Infotext des Wetter-Popups kurz genug ist, um immer komplett in die -// verfuegbare Boxhoehe zu passen. +// Simple word wrapping for body text, analogous to layoutWrapped() in +// menu_screen.cpp/webui_screen.cpp (see "no shared module" convention +// - each screen file has its own small copy). No scroll support, +// since the weather popup info text is short enough to always fit completely in the +// available box height. int16_t weatherInfoLayoutWrapped(TFT_eSPI& tftRef, int16_t x, int16_t startY, int16_t maxWidth, int16_t lineHeight, const String& text) { int16_t y = startY; @@ -214,12 +214,12 @@ void weatherInfoDrawButton(TFT_eSPI& tftRef, const Rect& r, const String& label) tftRef.setTextDatum(TL_DATUM); } -// Kleines Info-Fenster beim Antippen des Wetter-Icons im Header - erklaert, -// dass das angezeigte Wetter immer zum aktuell aktiven Standort (bzw. -// aktivem Standort-Preset, siehe LocationManager::getHomeLocation()) -// gehoert. Gleicher geboxter Overlay-Stil wie confirmLogbookEnable() in -// menu_screen.cpp, hier aber ohne Warnfarbe und nur mit einem -// Zurueck-Button, da rein informativ (keine Bestaetigung noetig). +// Small info window when tapping the weather icon in the header - explains +// that the displayed weather always belongs to the currently active location (or +// active location preset, see LocationManager::getHomeLocation()). +// Same boxed overlay style as confirmLogbookEnable() in +// menu_screen.cpp, but here without warning color and only a +// Back button, since purely informational (no confirmation needed). void showWeatherInfo(TFT_eSPI& tftRef) { constexpr int16_t BOX_X = 4; constexpr int16_t BOX_Y = 4; @@ -228,7 +228,7 @@ void showWeatherInfo(TFT_eSPI& tftRef) { constexpr int16_t TEXT_MAX_WIDTH = BOX_W - 20; constexpr int16_t LINE_H = 16; constexpr int16_t TITLE_Y = BOX_Y + 16; - // Eine Leerzeile Abstand zwischen Titel und Fliesstext. + // One blank line gap between title and body text. constexpr int16_t VIEW_TOP = TITLE_Y + 12 + LINE_H; constexpr int16_t BTN_H = 36; @@ -292,13 +292,13 @@ void drawWifiIcon(int16_t rightX, int16_t rowTop, int16_t rowH, int8_t rssi) { } } -// Grosse, zentrierte Uhrzeit fuer den Ruhebildschirm (siehe -// screensaverShowing oben) - eigene, groessere Variante der kleinen -// Kopfzeilen-Uhr aus updateStatusLine(), da diese bewusst kompakt gehalten -// ist. Loescht bei jedem Aufruf nur den eigenen schmalen Streifen in der -// Bildschirmmitte (nicht den ganzen Bildschirm), damit die Sternenanimation -// darum herum ungestoert weiterlaeuft. Zeichnet nichts, solange die Uhrzeit -// noch nicht per NTP synchronisiert ist (gleiche Pruefung wie +// Large, centered clock for the screensaver (see +// screensaverShowing above) - own larger variant of the small +// header clock from updateStatusLine(), since that one is deliberately kept compact. +// Only clears its own narrow strip in the +// middle of the screen (not the whole screen) so the star animation +// around it continues undisturbed. Draws nothing as long as the time +// has not been synchronized via NTP yet (same check as // updateStatusLine()/isNightDimHours()). void drawScreensaverClock() { time_t now = time(nullptr); @@ -306,15 +306,8 @@ void drawScreensaverClock() { struct tm tmNow; localtime_r(&now, &tmNow); - char timeBuf[9]; - if (LocationManager::useMetricUnits()) { - snprintf(timeBuf, sizeof(timeBuf), "%02d:%02d", tmNow.tm_hour, tmNow.tm_min); - } else { - int hour12 = tmNow.tm_hour % 12; - if (hour12 == 0) hour12 = 12; - snprintf(timeBuf, sizeof(timeBuf), "%d:%02d%s", hour12, tmNow.tm_min, - tmNow.tm_hour < 12 ? "AM" : "PM"); - } +char timeBuf[9]; + snprintf(timeBuf, sizeof(timeBuf), "%02d:%02d", tmNow.tm_hour, tmNow.tm_min); constexpr int16_t CLOCK_BAND_H = 40; int16_t cy = Config::SCREEN_HEIGHT / 2; @@ -354,23 +347,23 @@ void drawHeader() { void updateStatusLine() { if (wasEmergency) return; - // Die Uhrzeit nutzt den global gesetzten 11pt-Font (setFreeFont() in - // setup()) ueber setCursor()+print() - bei GFXFF-Fonts ist das - // baseline-verankert, der Text waechst also nach OBEN (siehe - // CLAUDE.md-Hinweis zu diesem Pitfall). Die Ziffern-Glyphen sind laut - // Font-Metrik 8px hoch und reichen damit bis y=HEADER_TITLE_H-6 - also - // OBERHALB des schmalen STATUS_LINE_H-Bereichs, der hier bisher allein - // geloescht wurde. Dadurch blieben alte Ziffern-Reste stehen und - // ueberlagerten sich mit den neuen (am sichtbarsten bei der letzten - // Minutenziffer, die sich am haeufigsten aendert). Deshalb hier gezielt - // NUR unter der Uhrzeit einen hoeheren Bereich loeschen - nicht die - // ganze Zeile, sonst wuerde das jede Sekunde in den Menu-Button - // hineinschneiden, der bis y=25 reicht. - // Auf 80px verbreitert (vorher 50) - das 12h-Format mit AM/PM (siehe - // unten) ist mit bis zu 7 Zeichen ("12:59PM") laenger als das feste - // 5-Zeichen-24h-Format ("23:12") und wurde sonst nicht vollstaendig - // geloescht (Ziffernreste blieben stehen). Der Bereich rechts daneben - // war hier ohnehin leer, daher unkritisch. +// The clock uses the globally set 11pt font (setFreeFont() in + // setup()) via setCursor()+print() - with GFXFF fonts this is + // baseline-anchored, so the text grows UPWARD (see + // CLAUDE.md note about this pitfall). The digit glyphs are + // 8px high according to font metrics, thus reaching up to y=HEADER_TITLE_H-6 - i.e. + // ABOVE the narrow STATUS_LINE_H area that was previously cleared alone. + // This left old digit remnants standing and + // overlapped with the new ones (most visible on the last + // minute digit, which changes most frequently). Therefore, selectively + // clear a taller area ONLY under the clock - not the + // whole line, otherwise it would cut into the Menu button + // every second, which extends to y=25. + // Widened to 80px (was 50) - the 12h format with AM/PM (see + // below) is up to 7 characters ("12:59PM") longer than the fixed + // 5-character 24h format ("23:12") and would otherwise not be completely + // cleared (digit remnants remained). The area to the right + // was already empty here, so uncritical. constexpr int16_t CLOCK_CLEAR_W = 80; constexpr int16_t CLOCK_CLEAR_TOP = HEADER_TITLE_H - 10; tft.fillRect(0, CLOCK_CLEAR_TOP, CLOCK_CLEAR_W, CONTENT_TOP - CLOCK_CLEAR_TOP, TFT_BLACK); @@ -382,9 +375,9 @@ void updateStatusLine() { struct tm tmNow; localtime_r(&now, &tmNow); char timeBuf[9]; - // 12h mit AM/PM bei Imperial (in den USA ueblich), 24h bei - // Metrisch - dieselbe Einheiten-Einstellung (Menue > Einheiten), - // die sonst Distanz/Hoehe steuert. Vorher immer fest 24h. +// 12h with AM/PM in Imperial (common in the US), 24h in + // Metric - same unit setting (Menu > Units) + // that otherwise controls distance/altitude. Previously always fixed 24h. if (LocationManager::useMetricUnits()) { snprintf(timeBuf, sizeof(timeBuf), "%02d:%02d", tmNow.tm_hour, tmNow.tm_min); } else { @@ -399,9 +392,9 @@ void updateStatusLine() { updateWifiIcon(); - // Icon nur bei tatsaechlicher Aenderung neu zeichnen (Weather::update() - // laeuft im Hintergrund auf Core 0 und aktualisiert typischerweise nur - // alle paar Minuten) - vermeidet unnoetiges Neuzeichnen jede Sekunde. +// Only redraw the icon when the condition actually changes (Weather::update() + // runs in the background on Core 0 and typically only updates + // every few minutes) - avoids unnecessary redrawing every second. Weather::Condition weatherNow = Weather::current(); if (weatherNow != lastRenderedWeather) { lastRenderedWeather = weatherNow; @@ -409,14 +402,14 @@ void updateStatusLine() { } } -// Prueft, ob gerade Nacht ist (fuer die Nachtdimmung). Nutzt den echten -// Sonnenauf-/untergang am aktiven Standort (siehe sun_times.h), NICHT mehr -// ein festes 22:00-06:00-Fenster - im Sommer war das vorher oft noch hell -// draussen, wenn schon gedimmt wurde, und im Winter blieb es nach 6 Uhr noch -// lange dunkel, ohne dass gedimmt wurde. Solange Standort oder Uhrzeit noch -// nicht bekannt sind (z.B. kurz nach dem Start, bevor NTP/GPS/IP-Geolocation -// fertig sind), faellt die Funktion auf das alte feste Fenster zurueck, -// damit die Nachtdimmung nicht komplett ausfaellt. +// Checks whether it is currently night (for night dimming). Uses the real +// sunrise/sunset at the active location (see sun_times.h), NOT anymore +// a fixed 22:00-06:00 window - in summer it was often still light +// outside when dimming already kicked in, and in winter it stayed +// dark long after 6am without dimming being active. As long as location or time are +// not yet known (e.g. shortly after startup, before NTP/GPS/IP-geolocation +// are ready), the function falls back to the old fixed window, +// so that night dimming does not fail completely. bool isNightDimHours() { time_t now = time(nullptr); if (now <= 8 * 3600 * 2) return false; @@ -437,15 +430,15 @@ bool isNightDimHours() { } } - // Fallback: Standort noch unbekannt. + // Fallback: location not yet known. int hour = tmNow.tm_hour; return (hour >= 22 || hour < 6); } -// Sanfte Nachtdimmung des Backlights zwischen Sonnenuntergang und -// Sonnenaufgang (siehe isNightDimHours()), sofern in den Einstellungen -// aktiviert. Der Inaktivitaets-Timeout (screenDimmed) hat Vorrang und wird -// hier nicht ueberschrieben. +// Gentle night dimming of the backlight between sunset and +// sunrise (see isNightDimHours()), if enabled in +// settings. The inactivity timeout (screenDimmed) takes priority and is +// not overridden here. void updateNightDimming() { if (screenDimmed) return; @@ -538,10 +531,10 @@ void setup() { SettingsStore::load(); tft.invertDisplay(SettingsStore::displayInverted()); - // Der erste ledcWrite() oben (vor SettingsStore::load()) kannte die - // gespeicherte Helligkeit noch nicht und hat den Default (100%) - // angewendet - hier mit dem jetzt geladenen Wert korrigieren, gleiches - // Nachziehen wie bei invertDisplay() direkt drueber. +// The first ledcWrite() above (before SettingsStore::load()) did not yet know the + // saved brightness and used the default (100%) + // - correct it here with the now loaded value, same + // follow-up as with invertDisplay() directly above. ledcWrite(BACKLIGHT_PWM_CHANNEL, normalBacklightPwm()); WifiMgr::init(); @@ -550,19 +543,18 @@ void setup() { AircraftWatchlist::init(); SplashScreen::begin(tft); - // Sofort einen ersten Sternen-Frame zeichnen, statt erst auf die naechste - // Warteschleife (WLAN/Standort) zu warten - sonst blieb der Sternenhimmel - // bei schnellem Boot (gespeichertes WLAN, schnelle Standortermittlung) - // fast die ganze Splash-Anzeige ueber unsichtbar und "poppte" erst kurz - // vor dem Radarscreen auf. +// Draw a first star frame immediately, instead of waiting for the next + // wait loop (WiFi/location) - otherwise the starry sky remained + // almost invisible during the entire splash display on fast boot (saved WiFi, quick location lookup) + // and only "popped up" shortly before the radar screen. MenuStars::update(tft); SplashScreen::setStatusLine(tft, 0, I18n::t(StringId::SPLASH_SD_OK), TFT_WHITE); - // Willkommens-Screen laeuft nur beim allerersten Start (isFirstRun) und - // bewusst VOR der Touch-Kalibrierung - der erste Eindruck, bevor man - // ueberhaupt zum Kalibrieren aufgefordert wird. Englisch hart kodiert, - // da die Sprachauswahl (FirstRunLanguageScreen) erst danach kommt, - // siehe first_run_welcome_screen.cpp. +// Welcome screen only runs on the very first start (isFirstRun) and + // deliberately BEFORE touch calibration - first impression before + // even being prompted to calibrate. Hardcoded in English, + // since the language selection (FirstRunLanguageScreen) only comes after, + // see first_run_welcome_screen.cpp. if (isFirstRun) { // Creating the folder structure, seeding default data files, AND // showing the loading indicator during those (noticeably slow) SD @@ -599,14 +591,14 @@ void setup() { if (isFirstRun) { FirstRunLanguageScreen::run(tft); - // Erst NACH der Sprachauswahl (Texte erscheinen dann gleich in der - // richtigen Sprache) und NACH dem WLAN-Verbindungsversuch weiter - // oben (die Adresssuche braucht eine Internetverbindung) - - // ueberspringbar, siehe first_run_location_screen.cpp. +// Only AFTER language selection (texts then appear in the + // correct language immediately) and AFTER the WiFi connection attempt + // above (address search needs an internet connection) - + // skippable, see first_run_location_screen.cpp. FirstRunLocationScreen::run(tft); - // Abschluss-Screen: bestaetigt das Ende der Ersteinrichtung, nennt - // den SD-Ordner mit allen gespeicherten Daten und weist auf den - // automatischen WLAN-Verbindungsaufbau ab dem naechsten Start hin. +// Completion screen: confirms the end of initial setup, names + // the SD folder with all saved data and points to the + // automatic WiFi connection from the next start onward. FirstRunCompleteScreen::run(tft); SplashScreen::begin(tft); MenuStars::update(tft); @@ -618,13 +610,13 @@ void setup() { SplashScreen::setStatusLine(tft, 2, I18n::t(StringId::SPLASH_GETTING_LOCATION)); LocationManager::init(); uint32_t locStart = millis(); - // Bewusst NICHT nur warten, solange noch GAR KEINE Position bekannt ist: - // ab dem zweiten Boot ist meistens schon eine gespeicherte Position da - // (Source::Persisted, siehe LocationManager::init()), wodurch diese - // Schleife sofort uebersprungen wurde und requestIpLookupIfNeeded() nie - // wieder lief. Genau die liefert aber auch die UTC-Zeitzonenverschiebung - // (inkl. Sommer-/Winterzeit) - ohne sie blieb die Uhr dauerhaft auf UTC - // stehen (in Mitteleuropa je nach Jahreszeit 1-2h falsch). +// Deliberately NOT only waiting while NO position is known at all: + // from the second boot onward there is usually already a saved position + // (Source::Persisted, see LocationManager::init()), which would cause this + // loop to be skipped immediately and requestIpLookupIfNeeded() to never + // run again. But requestIpLookupIfNeeded() also provides the UTC timezone offset + // (including DST) - without it the clock would permanently stay on UTC + // (in Central Europe 1-2h off depending on the season). while ((LocationManager::currentSource() == LocationManager::Source::None || !LocationManager::hasUtcOffset()) && millis() - locStart < 8000) { @@ -634,9 +626,9 @@ void setup() { } SplashScreen::setStatusLine(tft, 2, I18n::t(StringId::SPLASH_READY)); - // Sobald die IP-Geolocation einen UTC-Offset geliefert hat, die - // Zeitzone entsprechend setzen - Zeitstempel (Flugbuch, Log-Dateinamen) - // zeigen dann die ECHTE Ortszeit statt UTC, automatisch weltweit richtig. +// Once IP geolocation has provided a UTC offset, set the + // timezone accordingly - timestamps (flight log, log file names) + // then show the REAL local time instead of UTC, automatically correct worldwide. if (LocationManager::hasUtcOffset()) { configTime(LocationManager::utcOffsetSeconds(), 0, "pool.ntp.org", "time.nist.gov"); } @@ -697,14 +689,14 @@ if (menuBtn.contains(tap.x, tap.y)) { } } - // Waehrend der Ruhebildschirm aktiv ist (screensaverShowing), duerfen - // Radar-Rendering/Sweep/Statuszeile den Bildschirm NICHT mehr - // ueberschreiben - sonst wuerde der naechste Aircraft-Update-Zyklus - // (oder der Sweep-Tick) den gedimmten Sternenhimmel jederzeit wieder mit - // dem vollen Radarbild uebermalen, waehrend gleichzeitig - // drawScreensaverClock() im Sekundentakt seinen Streifen drueberzeichnet - // - das Ergebnis war sichtbares Geflacker zwischen Radarbild und Uhr - // statt eines ruhigen Sternenhimmels. +// While the screensaver is active (screensaverShowing), radar + // rendering/sweep/status line must NOT overwrite the display anymore + // - otherwise the next aircraft update cycle + // (or the sweep tick) would paint over the dimmed starry sky at any time with + // the full radar image, while at the same time + // drawScreensaverClock() draws its strip over it every second + // - the result was visible flickering between radar image and clock + // instead of a calm starry sky. if (!screensaverShowing) { if (forceRedraw || millis() - lastPollMs >= POLL_INTERVAL_MS) { lastPollMs = millis(); diff --git a/src/menu_screen.cpp b/src/menu_screen.cpp index 5355840..62ee77a 100644 --- a/src/menu_screen.cpp +++ b/src/menu_screen.cpp @@ -35,18 +35,17 @@ namespace { } }; - // ROW_GAP/ROW_START_Y bleiben unveraendert (werden von flightRowRect() - // weiter unten mitbenutzt, siehe FLIGHT_ROW_H). + // ROW_GAP/ROW_START_Y remain unchanged (also used by flightRowRect() + // below, see FLIGHT_ROW_H). constexpr int16_t ROW_GAP = 1; constexpr int16_t ROW_START_Y = 18; - // Region-Unterseite (Sprache/Einheiten/Zurueck, nur 3 Eintraege): - // Zeilenhoehe/-abstand werden aus der tatsaechlich verfuegbaren - // Bildschirmflaeche errechnet (gleiches Muster wie bei SYSTEM_ROW_H - // weiter unten), statt die kleine, fuer volle Seiten (z.B. - // Flugoptionen: 14 Eintraege) gedachte feste Hoehe (vorher 22px) zu - // benutzen - die liess bei nur 3 Eintraegen fast den ganzen Bildschirm - // leer und machte die Buttons winzig und schwer zu treffen. + // Region sub-page (Language/Units/Back, only 3 entries): row + // height/spacing calculated from actual available screen area (same + // pattern as SYSTEM_ROW_H below), instead of the small fixed height + // (formerly 22px) intended for full pages (e.g. Flight Options: 14 + // entries) - that left nearly the whole screen empty with only 3 + // entries and made buttons tiny and hard to hit. constexpr uint8_t REGION_ROW_COUNT = 3; constexpr int16_t REGION_ROW_GAP = 10; constexpr int16_t REGION_END_Y = Config::SCREEN_HEIGHT - 10; @@ -67,33 +66,31 @@ namespace { (int16_t)(Config::SCREEN_WIDTH - 20), CAT_ROW_H}; } - // Eigene, etwas kompaktere Zeilenhoehe NUR fuer die Flugoptionen-Seite: - // die normale rowRect()-Hoehe (22px) liess bei 13 Eintraegen schon keinen - // Platz mehr fuer einen weiteren - andere Seiten (Region/System) bleiben - // bei der normalen Hoehe unveraendert, um dort nichts zu verschieben. + // Custom, slightly more compact row height ONLY for the Flight Options + // page: the normal rowRect() height (22px) already left no room for + // another entry with 13 entries - other pages (Region/System) keep + // their normal height unchanged to avoid shifting anything there. constexpr int16_t FLIGHT_ROW_H = 19; Rect flightRowRect(uint8_t index) { return {10, (int16_t)(ROW_START_Y + index * (FLIGHT_ROW_H + ROW_GAP)), (int16_t)(Config::SCREEN_WIDTH - 20), FLIGHT_ROW_H}; } - // Eigene, groesser berechnete Zeilenhoehe NUR fuer die System-Seite: mit - // nur 10 Eintraegen (vs. z.B. 14 auf der Flugoptionen-Seite) liess die - // normale rowRect()-Hoehe (22px) unten viel ungenutzten Platz frei. Die - // Hoehe wird hier stattdessen aus der tatsaechlich verfuegbaren - // Bildschirmflaeche errechnet, sodass die Buttons den Platz bis knapp - // ueber den Bildschirmrand ausfuellen - andere Seiten bleiben bei ihren - // eigenen, unveraenderten Zeilenhoehen. - // War 10, bevor "Sicherung"/"Wiederherstellen" in die neue - // "Sicherung & Reset"-Unterseite ausgelagert wurden (siehe - // BACKUP_RESET_ROW_COUNT unten) und dort durch einen einzelnen - // Ordner-Button ersetzt wurden: -2 (Backup/Restore raus) +1 (neuer - // Ordner-Button) = 9. Danach +1 fuer den neuen "Nach Update - // suchen"-Punkt (OTA-Update, siehe ota_update.h) = 10. Der - // Ruhebildschirm-Umschalter, der zwischenzeitlich hier eine eigene - // Zeile hatte, ist in den neuen Bildschirm-Timeout-Screen umgezogen - // (siehe timeout_screen.cpp) - Zeilenzahl bleibt dadurch bei 10. - constexpr uint8_t SYSTEM_ROW_COUNT = 11; // +1 fuer Eco-Mode + // Custom, larger calculated row height ONLY for the System page: with + // only 10 entries (vs. e.g. 14 on the Flight Options page), the + // normal rowRect() height (22px) left a lot of unused space at the + // bottom. Height is instead calculated from actual available screen + // area so buttons fill the space down to just above the screen + // edge - other pages keep their own unchanged row heights. + // Was 10 before "Backup"/"Restore" were moved to the new + // "Backup & Reset" sub-page (see BACKUP_RESET_ROW_COUNT below) and + // replaced there by a single folder button: -2 (Backup/Restore + // removed) +1 (new folder button) = 9. Then +1 for the new + // "Check for update" item (OTA update, see ota_update.h) = 10. The + // screensaver toggle, which temporarily had its own row here, moved + // to the new screen timeout screen (see timeout_screen.cpp) - row + // count therefore stays at 10. + constexpr uint8_t SYSTEM_ROW_COUNT = 11; // +1 for Eco-Mode constexpr int16_t SYSTEM_ROW_GAP = 4; constexpr int16_t SYSTEM_START_Y = 18; constexpr int16_t SYSTEM_END_Y = Config::SCREEN_HEIGHT - 10; @@ -104,11 +101,11 @@ namespace { (int16_t)(Config::SCREEN_WIDTH - 20), SYSTEM_ROW_H}; } - // Eigene Zeilenhoehe fuer die neue "Sicherung & Reset"-Unterseite (4 - // Eintraege: Sichern/Wiederherstellen/Zuruecksetzen/Zurueck) - gleiches - // "aus verfuegbarem Platz errechnen"-Muster wie bei SYSTEM_ROW_H. - // ROW_START_Y wird mitbenutzt (siehe oben, gemeinsam mit rowRect()/ - // flightRowRect()), nur GAP/COUNT/H sind eigene Werte. + // Custom row height for the new "Backup & Reset" sub-page (4 + // entries: Backup/Restore/Factory Reset/Back) - same "calculate from + // available space" pattern as SYSTEM_ROW_H. + // ROW_START_Y is reused (see above, shared with rowRect()/ + // flightRowRect()), only GAP/COUNT/H are custom values. constexpr uint8_t BACKUP_RESET_ROW_COUNT = 4; constexpr int16_t BACKUP_RESET_ROW_GAP = 10; constexpr int16_t BACKUP_RESET_END_Y = Config::SCREEN_HEIGHT - 10; @@ -134,14 +131,15 @@ namespace { String onOff(bool on) { return I18n::t(on ? StringId::ON : StringId::OFF); } - // Fortschrittspunkte-Anzeige waehrend SettingsBackup::backup()/restore() - // laufen (siehe Aufrufe unten in Page::BackupReset) - diese sind - // synchrone, SD-lastige Vorgaenge, die spuerbar dauern koennen und den - // Button vorher wie eingefroren wirken liessen. SettingsBackup ruft den - // hier uebergebenen Funktionszeiger vor jedem der beiden Kopiervorgaenge - // (erst Einstellungen, dann WLAN) auf. Namespace-globale Zeiger/Variablen - // statt Lambda-Capture, da ein einfacher C-Funktionszeiger uebergeben - // werden muss (kein std::function im Projekt). + // Progress dots display while SettingsBackup::backup()/restore() + // run (see calls below in Page::BackupReset) - these are + // synchronous, SD-intensive operations that can take noticeable + // time and previously made the button appear frozen. + // SettingsBackup calls the function pointer passed here before each + // of the two copy operations (first settings, then WiFi). + // Namespace-global pointers/variables instead of lambda capture, + // because a plain C function pointer must be passed (no + // std::function in the project). TFT_eSPI* progressTft = nullptr; Rect progressBtnRect; String progressLabel; @@ -174,11 +172,11 @@ namespace { delay(1200); } - // Wie die einfache layoutWrapped()-Variante, aber mit optionalem - // Scroll-Offset und Sichtfenster (scrollY/viewTop/viewBottom) - Zeilen - // ausserhalb des Sichtfensters werden uebersprungen. Mit draw=false wird - // nur die Gesamthoehe berechnet, ohne etwas zu zeichnen (fuer die - // Scroll-Bedarfspruefung vorab). + // Like the simple layoutWrapped() variant, but with optional scroll + // offset and viewport (scrollY/viewTop/viewBottom) - lines outside + // the viewport are skipped. With draw=false, only the total height + // is calculated without drawing anything (for pre-checking scroll + // need). int16_t layoutWrapped(TFT_eSPI& tft, int16_t x, int16_t startY, int16_t maxWidth, int16_t lineHeight, const String& text, int16_t scrollY, int16_t viewTop, int16_t viewBottom, bool draw) { @@ -209,10 +207,10 @@ namespace { return y; } - // Zerlegt einen Titel in bis zu maxLines Zeilen nach dem gleichen - // Wortumbruch-Prinzip wie layoutWrapped() - ein Titel wird aber nie - // gescrollt, sondern muss immer komplett sichtbar sein, daher eine - // eigene, einfachere Variante ohne Scroll-Unterstuetzung. + // Splits a title into up to maxLines lines using the same word-wrapping + // principle as layoutWrapped() - but a title is never scrolled and must + // always be fully visible, hence a separate, simpler variant without + // scroll support. int wrapTitleLines(TFT_eSPI& tft, const String& text, int16_t maxWidth, String* outLines, int maxLines) { int count = 0; int32_t start = 0; @@ -240,29 +238,27 @@ namespace { return count; } - // Warn-Ueberlage, die praktisch den kompletten Bildschirm einnimmt (nur - // ein paar Pixel Rand) - urspruenglich nur fuers Einschalten des - // Flugbuchs gebaut (erklaert, warum es sich nach 24h automatisch - // wieder abschaltet), jetzt generisch mit uebergebenen Titel-/Text- - // StringIds, damit sie auch fuer die "Einstellungen zuruecksetzen"- - // Bestaetigung (Werksreset) wiederverwendet werden kann - beides sind - // seltene, potenziell folgenreiche Aktionen, die dieselbe deutliche - // Warnung verdienen. "Achtung!!!" (titleId) steht ganz oben, mit einer - // Leerzeile Abstand zum Fliesstext (bodyId) darunter; der Text - // scrollt bei Bedarf (laengere Uebersetzungen) ueber eigene Pfeil- - // Buttons, OK/Zurueck bleiben dabei immer unten fix und - // kollisionsfrei sichtbar. Sternchen laufen im Hintergrund mit, wie auf - // allen anderen Menue-Screens (nur der Radar-Screen selbst spart sich - // das wegen der CPU-Last durch Abfragen/Zeichnen). Gibt true zurueck, - // wenn "OK" angetippt wurde, false bei "Zurueck". + // Warning overlay that covers nearly the entire screen (just a few + // pixels margin) - originally built only for enabling the flight + // logbook (explains why it auto-disables after 24h), now generic + // with passed-in title/text to also be reusable for the "Reset + // settings" confirmation (factory reset) - both are rare, potentially + // consequential actions that deserve the same clear warning. + // "Attention!!!" (titleId) appears at the top, with a blank line + // before the body text (bodyId) below; the text scrolls if needed + // (longer translations) via dedicated arrow buttons, OK/Back always + // stay fixed at the bottom and collision-free visible. Stars run in + // the background, as on all other menu screens (only the radar + // screen skips them due to CPU load from polling/drawing). Returns + // true if "OK" was tapped, false for "Back". // - // title/body stehen VOR dem Aufruf per I18n::t() fest (statt StringIds - // entgegenzunehmen), damit auch dynamisch zusammengesetzte Texte (z.B. - // mit eingefuegter Versionsnummer bei OTA) moeglich sind. accentColor - // (Default TFT_RED fuer die beiden bestehenden, wirklich destruktiven - // Aufrufer) faerbt Rahmen und Titel - der OTA-Aufrufer uebergibt - // TFT_GREEN, da ein Update zwar bestaetigt werden sollte, aber keine - // "gefaehrliche" Loesch-Aktion wie Werksreset/Flugbuch ist. + // title/body are fixed BEFORE the call via I18n::t() (instead of + // accepting StringIds), so dynamically composed texts (e.g. with + // inserted version number for OTA) are possible. accentColor + // (default TFT_RED for the two existing, truly destructive callers) + // colors frame and title - the OTA caller passes TFT_GREEN, since an + // update should be confirmed but is not a "dangerous" deletion + // action like factory reset/flight logbook. bool confirmWarningScreen(TFT_eSPI& tft, const String& title, const String& body, uint16_t accentColor = TFT_RED) { constexpr int16_t BOX_X = 4; constexpr int16_t BOX_Y = 4; @@ -272,11 +268,11 @@ namespace { constexpr int16_t LINE_H = 16; constexpr int16_t TITLE_Y = BOX_Y + 16; - // Titel-Text vorab in so viele Zeilen umbrechen, wie bei Größe 2 - // (oder bei zu langem Text Größe 1) nötig sind - siehe - // wrapTitleLines() oben. VIEW_TOP hängt dadurch von der - // tatsächlichen Zeilenzahl des Titels ab, ist also kein constexpr - // mehr wie vorher (wo immer nur eine Titelzeile angenommen wurde). + // Wrap title text into as many lines as needed at size 2 (or size 1 + // if text is too long) - see wrapTitleLines() above. VIEW_TOP + // therefore depends on the actual number of title lines, so it is + // no longer constexpr as before (where only one title line was + // assumed). tft.setTextSize(2); uint8_t titleTextSize = 2; if (tft.textWidth(title) > TEXT_MAX_WIDTH) { @@ -286,13 +282,13 @@ namespace { constexpr int MAX_TITLE_LINES = 3; String titleLines[MAX_TITLE_LINES]; int titleLineCount = wrapTitleLines(tft, title, TEXT_MAX_WIDTH, titleLines, MAX_TITLE_LINES); - // Fließtext wird immer bei Größe 1 vermessen/gezeichnet (siehe - // layoutWrapped()-Aufrufe unten) - Größe hier zurücksetzen, falls - // obiger Titel-Breitentest sie auf 2 stehen gelassen hat, sonst - // würde die gleich folgende totalH-Berechnung (vor dem ersten - // redraw()) mit falscher (zu breiter) Schriftgröße rechnen. + // Body text is always measured/drawn at size 1 (see + // layoutWrapped() calls below) - reset size here in case the + // title width check above left it at 2, otherwise the + // immediately following totalH calculation (before the first + // redraw()) would use the wrong (too wide) font size. tft.setTextSize(1); - // Eine Leerzeile Abstand zwischen Titel und Fließtext. + // One blank line between title and body text. int16_t VIEW_TOP = (int16_t)(TITLE_Y + titleLineCount * LINE_H + 12); constexpr int16_t BTN_H = 36; @@ -303,9 +299,9 @@ namespace { constexpr int16_t SCROLL_ROW_H = 28; constexpr int16_t SCROLL_ROW_GAP = 8; - // Ohne Scroll-Pfeile verfuegbare Texthoehe zuerst pruefen - nur wenn - // der Text da nicht reinpasst, wird zusaetzlich Platz fuer die - // Pfeile reserviert (mehr Text-Platz bei kurzen Uebersetzungen). + // First check available text height without scroll arrows - only if + // the text does not fit, additional space for arrows is reserved + // (more text space for short translations). constexpr int16_t VIEW_BOTTOM_NO_SCROLL = OK_Y - 8; constexpr int16_t VIEW_BOTTOM_SCROLL = VIEW_BOTTOM_NO_SCROLL - SCROLL_ROW_H - SCROLL_ROW_GAP; @@ -332,13 +328,12 @@ namespace { tft.setTextDatum(MC_DATUM); tft.setTextColor(accentColor, TFT_BLACK); - // Statische Titel ("Achtung!!!") sind kurz genug fuer Groesse 2, - // aber der OTA-Aufrufer baut den Titel dynamisch mit - // Versionsnummer zusammen (z.B. "Update verfuegbar: v3.5.0") - - // das passt bei Groesse 2 nicht mehr in die Box und lief vorher - // links/rechts ueber den Bildschirmrand hinaus. Deshalb hier - // die Breite bei Groesse 2 pruefen und bei Bedarf auf Groesse 1 - // zurueckfallen, statt eine feste Groesse anzunehmen. + // Static titles ("Attention!!!") are short enough for size 2, but + // the OTA caller builds the title dynamically with a version + // number (e.g. "Update available: v3.5.0") - this no longer + // fits in the box at size 2 and previously overflowed left/right + // beyond the screen edge. Hence check width at size 2 and fall + // back to size 1 if needed, instead of assuming a fixed size. tft.setTextSize(titleTextSize); for (int i = 0; i < titleLineCount; i++) { tft.drawString(titleLines[i], BOX_X + BOX_W / 2, (int16_t)(TITLE_Y + i * LINE_H)); @@ -379,16 +374,15 @@ namespace { } } - // Einfacher Info-Screen mit nur EINEM Button (kein Abbrechen) - fuer - // Endzustaende, bei denen es nichts mehr zu entscheiden gibt, nur zu - // bestaetigen (z.B. Ergebnis eines OTA-Updates). Anders als - // showBriefMessage() (kurze Meldung unten am Bildschirmrand, - // verschwindet nach 1,2s automatisch von selbst) bleibt dieser Screen - // stehen, bis aktiv bestaetigt wird - wichtig bei sicherheitsrelevanten - // Meldungen wie einem fehlgeschlagenen oder erfolgreichen Firmware- - // Update, die der Nutzer auf keinen Fall verpassen darf. Gleicher - // Kasten-/Scroll-Aufbau wie confirmWarningScreen(), nur mit einem - // einzigen, ueber die volle Breite gehenden Button statt OK/Zurueck. + // Simple info screen with only ONE button (no cancel) - for + // terminal states where there is nothing left to decide, only to + // acknowledge (e.g. result of an OTA update). Unlike + // showBriefMessage() (short message at the bottom of the screen, + // auto-disappears after 1.2s), this screen stays until actively + // acknowledged - important for safety-relevant messages like a + // failed or successful firmware update that the user must not miss. + // Same box/scroll layout as confirmWarningScreen(), but with a + // single full-width button instead of OK/Back. void infoScreen(TFT_eSPI& tft, const String& title, const String& body, uint16_t accentColor, const String& buttonLabel) { constexpr int16_t BOX_X = 4; constexpr int16_t BOX_Y = 4; @@ -398,11 +392,11 @@ namespace { constexpr int16_t LINE_H = 16; constexpr int16_t TITLE_Y = BOX_Y + 16; - // Titel-Text vorab in so viele Zeilen umbrechen, wie bei Größe 2 - // (oder bei zu langem Text Größe 1) nötig sind - siehe - // wrapTitleLines() oben. VIEW_TOP hängt dadurch von der - // tatsächlichen Zeilenzahl des Titels ab, ist also kein constexpr - // mehr wie vorher (wo immer nur eine Titelzeile angenommen wurde). + // Wrap title text into as many lines as needed at size 2 (or size 1 + // if text is too long) - see wrapTitleLines() above. VIEW_TOP + // therefore depends on the actual number of title lines, so it is + // no longer constexpr as before (where only one title line was + // assumed). tft.setTextSize(2); uint8_t titleTextSize = 2; if (tft.textWidth(title) > TEXT_MAX_WIDTH) { @@ -412,13 +406,13 @@ namespace { constexpr int MAX_TITLE_LINES = 3; String titleLines[MAX_TITLE_LINES]; int titleLineCount = wrapTitleLines(tft, title, TEXT_MAX_WIDTH, titleLines, MAX_TITLE_LINES); - // Fließtext wird immer bei Größe 1 vermessen/gezeichnet (siehe - // layoutWrapped()-Aufrufe unten) - Größe hier zurücksetzen, falls - // obiger Titel-Breitentest sie auf 2 stehen gelassen hat, sonst - // würde die gleich folgende totalH-Berechnung (vor dem ersten - // redraw()) mit falscher (zu breiter) Schriftgröße rechnen. + // Body text is always measured/drawn at size 1 (see + // layoutWrapped() calls below) - reset size here in case the + // title width check above left it at 2, otherwise the + // immediately following totalH calculation (before the first + // redraw()) would use the wrong (too wide) font size. tft.setTextSize(1); - // Eine Leerzeile Abstand zwischen Titel und Fließtext. + // One blank line between title and body text. int16_t VIEW_TOP = (int16_t)(TITLE_Y + titleLineCount * LINE_H + 12); constexpr int16_t BTN_H = 40; @@ -490,22 +484,21 @@ namespace { } } - // Fortschrittsanzeige waehrend OtaUpdate::performUpdate() laeuft - - // gleiches Namespace-globale-Zeiger-Prinzip wie progressTft oben (siehe - // Settings-Backup-Fortschrittspunkte), da OtaUpdate::performUpdate() - // ebenfalls einen einfachen C-Funktionszeiger erwartet, keine - // Lambda-Capture erlaubt. + // Progress display while OtaUpdate::performUpdate() runs - same + // namespace-global pointer principle as progressTft above (see + // Settings-Backup progress dots), since OtaUpdate::performUpdate() + // also expects a plain C function pointer, no lambda capture + // allowed. TFT_eSPI* otaProgressTft = nullptr; void drawOtaProgress(uint8_t percent) { if (!otaProgressTft) return; TFT_eSPI& t = *otaProgressTft; - // Zwei Zeilen statt einer langen: der Praefix-Text - // (OTA_INSTALLING_PREFIX) ist in manchen Sprachen zu lang, um - // zusammen mit der Prozentzahl auf einer Zeile bei lesbarer - // Schriftgroesse zu passen (lief vorher links/rechts ueber den - // Bildschirmrand hinaus). Jetzt: Beschriftung klein oben, Prozent - // gross darunter. + // Two lines instead of one long one: the prefix text + // (OTA_INSTALLING_PREFIX) is too long in some languages to fit + // together with the percentage on one line at a readable font size + // (previously overflowed left/right beyond the screen edge). + // Now: label small at top, percentage large below. constexpr int16_t BAND_H = 60; int16_t cy = Config::SCREEN_HEIGHT / 2; t.fillRect(0, (int16_t)(cy - BAND_H / 2), Config::SCREEN_WIDTH, BAND_H, TFT_BLACK); @@ -523,18 +516,17 @@ namespace { t.setTextDatum(TL_DATUM); } - // Kompletter Ablauf fuer "Nach Update suchen" (System-Menue) - Pruefung - // gegen GitHub-Releases, bei verfuegbarem Update explizite Bestaetigung - // (confirmWarningScreen() mit gruenem statt rotem Akzent - ein Update - // ist keine destruktive Aktion wie Werksreset, verdient aber trotzdem - // eine bewusste Bestaetigung, da WLAN/Strom waehrend des Vorgangs nicht - // unterbrochen werden sollten), danach Fortschrittsanzeige waehrend - // Download+Flash. WICHTIG: startet NICHT mehr automatisch neu und - // springt bei einem Fehler auch nicht einfach stillschweigend zurueck - // ins Menue - jedes Ergebnis (Erfolg wie Fehler) wird ueber infoScreen() - // als eigener, stehenbleibender Screen angezeigt, den der Nutzer aktiv - // bestaetigen muss. Bei Erfolg startet erst ein expliziter Tap auf - // "Jetzt neu starten" tatsaechlich neu. + // Complete flow for "Check for update" (System menu) - checks + // against GitHub releases, explicit confirmation when update is + // available (confirmWarningScreen() with green instead of red + // accent - an update is not a destructive action like factory reset, + // but still deserves conscious confirmation since WiFi/power should + // not be interrupted during the process), then progress display + // during download+flash. IMPORTANT: no longer auto-restarts and no + // longer silently jumps back to the menu on error - every result + // (success as well as failure) is shown via infoScreen() as its own + // persistent screen that the user must actively acknowledge. On + // success, only an explicit tap on "Restart now" actually restarts. void runOtaUpdateScreen(TFT_eSPI& tft) { MenuStars::reset(); tft.fillScreen(TFT_BLACK); @@ -570,18 +562,18 @@ namespace { otaProgressTft = nullptr; if (ok) { - // Bewusst KEIN automatischer Neustart mehr - der Nutzer - // bestaetigt aktiv per Button, damit er den Erfolg auch wirklich - // mitbekommt (vorher lief die Meldung nur 1,5s an, dann - // Neustart - leicht zu verpassen). + // Deliberately NO automatic restart anymore - the user + // actively confirms via button so they actually notice the + // success (previously the message showed for only 1.5s, + // then restart - easy to miss). infoScreen(tft, I18n::t(StringId::OTA_UPDATE_SUCCESS), I18n::t(StringId::OTA_SUCCESS_BODY), TFT_GREEN, I18n::t(StringId::OTA_RESTART_BUTTON)); ESP.restart(); } else { - // Bewusst ein stehenbleibender Info-Screen statt der alten - // showBriefMessage() (1,2s, dann automatisch zurueck ins Menue) - // - ein fehlgeschlagenes Firmware-Update ist keine - // Nebensaechlichkeit, die man verpassen darf. + // Deliberately a persistent info screen instead of the old + // showBriefMessage() (1.2s, then automatically back to menu) + // - a failed firmware update is not a minor detail that one + // can afford to miss. infoScreen(tft, I18n::t(StringId::OTA_UPDATE_FAILED), I18n::t(StringId::OTA_FAILED_BODY), TFT_RED, I18n::t(StringId::OK)); } @@ -673,12 +665,12 @@ void run(TFT_eSPI& tft) { Rect timeoutBtn = systemRowRect(3); Rect nightDimBtn = systemRowRect(4); Rect webuiBtn = systemRowRect(6); - // "Sicherung & Reset" (Backup/Restore/Werksreset, siehe - // Page::BackupReset unten) und "Nach Update suchen" stehen - // bewusst direkt ueber "Info", das seinerseits als letzter - // Punkt direkt ueber dem Zurueck-Button steht - so bleiben - // beide Positionen stabil, egal wie viele weitere Punkte davor - // noch dazukommen. + // "Backup & Reset" (Backup/Restore/Factory Reset, see + // Page::BackupReset below) and "Check for update" are + // deliberately placed directly above "About", which in turn + // is the last item directly above the Back button - this + // keeps both positions stable regardless of how many further + // items come before them. Rect ecoModeBtn = systemRowRect(5); Rect backupResetBtn = systemRowRect(7); Rect checkUpdateBtn = systemRowRect(8); @@ -717,12 +709,12 @@ void run(TFT_eSPI& tft) { } else if (brightnessBtn.contains(tap.x, tap.y)) { BrightnessScreen::run(tft); } else if (timeoutBtn.contains(tap.x, tap.y)) { - // Vorher: Durchklicken per wiederholtem Antippen (0-10, ein - // Tipp pro Minute - bei z.B. 10 Minuten also zehn Tipps). - // Jetzt: eigener Screen mit Schieberegler, siehe - // timeout_screen.cpp - dort lebt jetzt auch der - // Ruhebildschirm-Umschalter (inhaltlich eng verwandt, und - // dort ist Platz fuer eine kurze Erklaerung). + // Previously: cycling through values by repeated tapping + // (0-10, one tap per minute - e.g. 10 taps for 10 minutes). + // Now: dedicated screen with a slider, see + // timeout_screen.cpp - the screensaver toggle now also + // lives there (closely related in content, and there is + // room for a brief explanation). TimeoutScreen::run(tft); } else if (nightDimBtn.contains(tap.x, tap.y)) { SettingsStore::setNightDimmingEnabled(!SettingsStore::nightDimmingEnabled()); @@ -752,9 +744,9 @@ void run(TFT_eSPI& tft) { drawButton(tft, backupBtn, I18n::t(StringId::MENU_BACKUP)); drawButton(tft, restoreBtn, I18n::t(StringId::MENU_RESTORE)); - // Danger-Akzent (rot) - deutlich von Sichern/Wiederherstellen - // abgesetzt, da diese Aktion (nach Bestaetigung) ALLE Daten - // unwiderruflich loescht, siehe confirmWarningScreen() unten. + // Danger accent (red) - clearly distinguished from Backup/Restore + // since this action (after confirmation) irreversibly DELETES + // ALL data, see confirmWarningScreen() below. drawButton(tft, resetBtn, I18n::t(StringId::MENU_FACTORY_RESET), false, true); drawButton(tft, backBtn, I18n::t(StringId::BACK_ARROW)); @@ -794,10 +786,9 @@ void run(TFT_eSPI& tft) { tft.drawString(I18n::t(StringId::MENU_FACTORY_RESET_DELETING), Config::SCREEN_WIDTH / 2, Config::SCREEN_HEIGHT / 2); tft.setTextDatum(TL_DATUM); - // Erfolgsfall: factoryReset() startet das Geraet neu und - // kehrt nie zurueck - dieser Code danach laeuft nur im - // (seltenen) Fehlerfall (SD nicht eingehaengt) ueberhaupt - // weiter. + // On success: factoryReset() restarts the device and never + // returns - this code after it only continues in the + // (rare) error case (SD not mounted). bool ok = SettingsBackup::factoryReset(); if (!ok) { showBriefMessage(tft, I18n::t(StringId::MENU_FACTORY_RESET_FAILED), TFT_RED); @@ -812,13 +803,13 @@ void run(TFT_eSPI& tft) { tft.setCursor(10, 14); tft.println(I18n::t(StringId::MENU_CATEGORY_FLIGHT)); - // Reine An/Aus-Schalter (Heartbeat, Notfall-Alarm, Naeherungs-LED, - // Flugbuch, Beobachtungsalarm, Bodenfahrzeuge-Filter) stehen - // bewusst gemeinsam am Ende der Liste, direkt ueber dem - // Zurueck-Button - zuerst die LED-gesteuerten Alarme (Heartbeat, - // Notfall, Naeherung; siehe LedAlert::Mode) zusammenhaengend, - // danach die beiden nicht-LED-basierten Schalter Flugbuch und - // Beobachtungsalarm sowie zuletzt der Bodenfahrzeuge-Filter. + // Pure on/off toggles (Heartbeat, Emergency Alert, Proximity LED, + // Flight Logbook, Watchlist Alert, Ground Vehicles Filter) are + // deliberately grouped together at the end of the list, + // directly above the Back button - first the LED-driven alerts + // (Heartbeat, Emergency, Proximity; see LedAlert::Mode) + // together, then the two non-LED-based toggles Flight Logbook + // and Watchlist Alert, and finally the Ground Vehicles Filter. Rect aircraftListBtn = flightRowRect(0); Rect statsBtn = flightRowRect(1); Rect statsHistoryBtn = flightRowRect(2); @@ -860,9 +851,9 @@ drawButton(tft, watchlistBtn, I18n::t(StringId::MENU_WATCHLIST)); if (aircraftListBtn.contains(tap.x, tap.y)) { if (AircraftListScreen::run(tft)) { - // Ein Flugzeug wurde in der Liste ausgewaehlt - direkt bis - // zum Radar zurueckspringen (mit offenem Detail-Panel), - // statt in der Flugoptionen-Seite stehen zu bleiben. + // An aircraft was selected in the list - jump directly back + // to the radar (with detail panel open), instead of + // staying on the Flight Options page. done = true; } } else if (statsBtn.contains(tap.x, tap.y)) { @@ -887,7 +878,7 @@ drawButton(tft, watchlistBtn, I18n::t(StringId::MENU_WATCHLIST)); SettingsStore::setProximityAlertEnabled(!SettingsStore::proximityAlertEnabled()); } else if (logbookBtn.contains(tap.x, tap.y)) { if (SettingsStore::flightLogbookEnabled()) { - // Ausschalten ist immer unbedenklich - keine Bestaetigung noetig. + // Turning off is always harmless - no confirmation needed. SettingsStore::setFlightLogbookEnabled(false); SettingsStore::setFlightLogbookEnabledAtEpoch(0); SettingsStore::setFlightLogbookSessionFile(""); @@ -895,9 +886,9 @@ drawButton(tft, watchlistBtn, I18n::t(StringId::MENU_WATCHLIST)); I18n::t(StringId::MENU_LOGBOOK_WARNING_BODY))) { SettingsStore::setFlightLogbookEnabled(true); SettingsStore::setFlightLogbookEnabledAtEpoch((uint32_t)time(nullptr)); - // Leerer Eintrag erzwingt eine frische Sitzungsdatei beim - // naechsten FlightLogbook::update() statt eine evtl. noch - // vorhandene alte Datei weiterzuschreiben. + // Empty entry forces a fresh session file on the next + // FlightLogbook::update() instead of continuing to + // write to a possibly existing old file. SettingsStore::setFlightLogbookSessionFile(""); } } else if (watchlistAlertBtn.contains(tap.x, tap.y)) { diff --git a/src/menu_screen.h b/src/menu_screen.h index 503ae81..f0405e9 100644 --- a/src/menu_screen.h +++ b/src/menu_screen.h @@ -3,8 +3,8 @@ #include namespace MenuScreen { - // Blockierend: einfaches Menue (Kalibrierung, Anzeige-Invertierung, - // WLAN-Verwaltung, Alarm-Toggles, Statistik, Logbuch-Dateien). - // Kehrt zurueck, sobald "Zurueck" angetippt wird. +// Blocking: simple menu (calibration, display inversion, + // WiFi management, alert toggles, statistics, logbook files). + // Returns as soon as "Back" is tapped. void run(TFT_eSPI& tft); } \ No newline at end of file diff --git a/src/menu_stars.h b/src/menu_stars.h index e61a833..9bc666c 100644 --- a/src/menu_stars.h +++ b/src/menu_stars.h @@ -1,17 +1,17 @@ #pragma once #include -// Gemeinsames Sterne-Twinkle-Modul fuer ALLE Menue-/Untermenue-Bildschirme -// (schwarzer Hintergrund) sowie den Splash-Screen - dieselbe Optik wie im -// Flugzeug-Detail-Panel des Radars. +// Common star twinkle module for ALL menu/submenu screens +// (black background) as well as the splash screen - same look as in the +// aircraft detail panel of the radar. namespace MenuStars { - // Verteilt die Sterne neu ueber den ganzen Bildschirm - einmal beim - // Betreten eines neuen Screens aufrufen (NICHT bei jedem Redraw - // innerhalb desselben Screens, sonst "springt" die Animation staendig). +// Redistributes the stars across the entire screen - call once when + // entering a new screen (NOT on every redraw within the same + // screen, otherwise the animation keeps "jumping"). void reset(); - // In der Warte-/Idle-Schleife eines Screens aufrufen (bei jedem - // Schleifendurchlauf ist ok - die Funktion drosselt sich intern selbst - // auf ca. alle 60ms, um das Display nicht unnoetig oft anzusprechen). +// Call in the wait/idle loop of a screen (every + // loop iteration is fine - the function internally throttles itself + // to roughly every 60ms to avoid needlessly addressing the display). void update(TFT_eSPI& tft); } \ No newline at end of file diff --git a/src/net_task.cpp b/src/net_task.cpp index e0b8c46..65453f3 100644 --- a/src/net_task.cpp +++ b/src/net_task.cpp @@ -66,10 +66,9 @@ namespace { FlightLogbook::update(); - // Kurzer gruener LED-Blitz als "Herzschlag" - zeigt, - // dass gerade eine Abfrage gelaufen ist. Wird von - // LedAlert::update() automatisch ignoriert, solange - // ein Naeherungs-/Notfall-Alarm aktiv ist. +// Short green LED flash as "heartbeat" - indicates that a fetch + // just ran. Automatically ignored by LedAlert::update() as long as + // a proximity/emergency alert is active. if (SettingsStore::ledHeartbeatEnabled()) { LedAlert::pulseHeartbeat(millis()); } diff --git a/src/net_task.h b/src/net_task.h index 7b89fc6..6ef8457 100644 --- a/src/net_task.h +++ b/src/net_task.h @@ -1,17 +1,17 @@ #pragma once #include -// Hintergrund-Task fuer WLAN-Status, Standortbestimmung und ADS-B-Abfragen. -// Laeuft auf Core 0, damit Core 1 (Rendering + Touch im main-Loop) nie durch -// Netzwerk-Wartezeiten (WLAN, HTTPS) blockiert wird. +// Background task for WiFi status, geolocation and ADS-B queries. +// Runs on Core 0 so that Core 1 (rendering + touch in main loop) is never +// blocked by network wait times (WiFi, HTTPS). namespace NetTask { void begin(); void pause(); void resume(); - // Eco-Mode: aendert das ADS-B-Abfrage-Intervall zur Laufzeit. - // Wird von main.cpp beim Bildschirm-Timeout (screenDimmed) auf - // Config::ECO_FETCH_INTERVAL_MS und beim Aufwachen wieder auf - // Config::FETCH_INTERVAL_MS gesetzt. + // Eco mode: changes the ADS-B fetch interval at runtime. + // Set by main.cpp on screen timeout (screenDimmed) to + // Config::ECO_FETCH_INTERVAL_MS and on wakeup back to + // Config::FETCH_INTERVAL_MS. void setFetchInterval(uint32_t ms); } \ No newline at end of file diff --git a/src/ota_update.cpp b/src/ota_update.cpp index 297bf9c..78b9bf7 100644 --- a/src/ota_update.cpp +++ b/src/ota_update.cpp @@ -14,9 +14,9 @@ namespace OtaUpdate { namespace { constexpr const char* RELEASES_API_URL = "https://api.github.com/repos/Eiswolf-BG/eiswolfs-flightradar-CYD/releases/latest"; - // GitHub verlangt bei API-Anfragen einen aussagekraeftigen User-Agent - // (sonst HTTP 403) - gleiches Prinzip wie Config::NOMINATIM_USER_AGENT - // fuer die Adresssuche. +// GitHub requires a meaningful User-Agent for API requests + // (otherwise HTTP 403) - same principle as Config::NOMINATIM_USER_AGENT + // for address search. constexpr const char* USER_AGENT = "EiswolfsFlightradarCYD-OTA/1.0 (+https://github.com/Eiswolf-BG/eiswolfs-flightradar-CYD)"; @@ -27,10 +27,10 @@ namespace { return sscanf(s, "%d.%d.%d", &major, &minor, &patch) == 3; } - // > 0 wenn a neuer als b, 0 wenn gleich, < 0 wenn a aelter als b. - // Bewusst eine echte numerische Versionsvergleich statt eines simplen - // String-Vergleichs (der wuerde z.B. "3.10.0" faelschlich als "kleiner" - // als "3.9.0" einordnen). +// > 0 if a is newer than b, 0 if equal, < 0 if a is older than b. + // Deliberately a real numeric version comparison instead of a simple + // string comparison (which would e.g. wrongly rank "3.10.0" as "less" + // than "3.9.0"). int compareVersions(const char* a, const char* b) { int aMaj, aMin, aPat, bMaj, bMin, bPat; if (!parseVersion(a, aMaj, aMin, aPat) || !parseVersion(b, bMaj, bMin, bPat)) return 0; @@ -43,7 +43,7 @@ namespace { CheckInfo checkForUpdate() { CheckInfo info; - Serial.printf("[OTA] Pruefe auf Update: url=%s freeHeap=%u RSSI=%ddBm\n", RELEASES_API_URL, + Serial.printf("[OTA] Checking for update: url=%s freeHeap=%u RSSI=%ddBm\n", RELEASES_API_URL, (unsigned)ESP.getFreeHeap(), WiFi.RSSI()); WiFiClientSecure client; @@ -53,7 +53,7 @@ CheckInfo checkForUpdate() { HTTPClient http; http.setTimeout(8000); if (!http.begin(client, RELEASES_API_URL)) { - Serial.println("[OTA] Pruefung fehlgeschlagen: http.begin() lieferte false."); + Serial.println("[OTA] Check failed: http.begin() returned false."); return info; } http.addHeader("User-Agent", USER_AGENT); @@ -61,10 +61,10 @@ CheckInfo checkForUpdate() { int code = http.GET(); if (code != HTTP_CODE_OK) { if (code < 0) { - Serial.printf("[OTA] Pruefung fehlgeschlagen: HTTP-Fehler=%d (%s)\n", code, + Serial.printf("[OTA] Check failed: HTTP error=%d (%s)\n", code, HTTPClient::errorToString(code).c_str()); } else { - Serial.printf("[OTA] Pruefung fehlgeschlagen: HTTP-Status=%d\n", code); + Serial.printf("[OTA] Check failed: HTTP status=%d\n", code); } http.end(); return info; @@ -75,13 +75,13 @@ CheckInfo checkForUpdate() { JsonDocument doc; DeserializationError jsonErr = deserializeJson(doc, body); if (jsonErr) { - Serial.printf("[OTA] Pruefung fehlgeschlagen: JSON-Fehler (%s)\n", jsonErr.c_str()); + Serial.printf("[OTA] Check failed: JSON error (%s)\n", jsonErr.c_str()); return info; } const char* tag = doc["tag_name"] | ""; if (!tag[0]) { - Serial.println("[OTA] Pruefung fehlgeschlagen: kein tag_name im Release-JSON."); + Serial.println("[OTA] Check failed: no tag_name in release JSON."); return info; } @@ -99,59 +99,58 @@ CheckInfo checkForUpdate() { } if (!info.downloadUrl[0]) { - Serial.printf("[OTA] Pruefung fehlgeschlagen: Release v%s hat keinen firmware.bin-Anhang.\n", + Serial.printf("[OTA] Check failed: Release v%s has no firmware.bin attachment.\n", info.latestVersion); - return info; // Release ohne firmware.bin-Anhang + return info; // Release without firmware.bin attachment } int cmp = compareVersions(info.latestVersion, Config::APP_VERSION); info.result = (cmp > 0) ? CheckResult::UpdateAvailable : CheckResult::UpToDate; - Serial.printf("[OTA] Pruefung erfolgreich: installiert=v%s neuestes=v%s -> %s\n", Config::APP_VERSION, +Serial.printf("[OTA] Check successful: installed=v%s latest=v%s -> %s\n", Config::APP_VERSION, info.latestVersion, - info.result == CheckResult::UpdateAvailable ? "Update verfuegbar" : "bereits aktuell"); + info.result == CheckResult::UpdateAvailable ? "Update available" : "already up-to-date"); return info; } bool performUpdate(const char* url, void (*onProgress)(uint8_t percent)) { - // Diagnose-Logging (nur ueber USB-Seriell sichtbar, kein Einfluss auf - // die UI) - vorher wurde bei einem Fehlschlag nur ein simples "true/ - // false" nach aussen gegeben, ohne den eigentlichen Grund (Timeout, - // TLS-Fehler, HTTP-Statuscode...) festzuhalten. Damit laesst sich ein - // fehlgeschlagener OTA-Versuch am Seriell-Monitor nachvollziehen, statt - // erneut raten zu muessen. - Serial.printf("[OTA] Start: url=%s freeHeap=%u RSSI=%ddBm\n", url, +// Diagnostic logging (only visible over USB serial, no impact on + // the UI) - previously a failed update only returned a simple "true/ + // false" externally, without recording the actual reason (timeout, + // TLS error, HTTP status code...). This allows tracing a failed + // OTA attempt on the serial monitor instead of having to guess again. + Serial.printf("[OTA] Starting: url=%s freeHeap=%u RSSI=%ddBm\n", url, (unsigned)ESP.getFreeHeap(), WiFi.RSSI()); WiFiClientSecure client; client.setInsecure(); client.setTimeout(15000); - // WICHTIG: GitHubs "browser_download_url" fuer Release-Assets ist KEIN - // direkter Download-Link, sondern liefert erst ein HTTP 301/302-Redirect - // auf eine signierte objects.githubusercontent.com-URL. HTTPUpdate folgt - // Redirects standardmaessig NICHT (HTTPC_DISABLE_FOLLOW_REDIRECTS ist der - // Default) - ohne diese Zeile bricht der Download mit HTTP_UPDATE_FAILED - // ab, weil statt der .bin-Datei nur die Redirect-Antwort ankommt. Siehe - // z.B. espressif/arduino-esp32#3020. HTTPC_STRICT_FOLLOW_REDIRECTS - // reicht, da wir nur GET verwenden. +// IMPORTANT: GitHub's "browser_download_url" for release assets is NOT + // a direct download link, but first returns an HTTP 301/302 redirect + // to a signed objects.githubusercontent.com URL. HTTPUpdate does NOT + // follow redirects by default (HTTPC_DISABLE_FOLLOW_REDIRECTS is the + // default) - without this line the download aborts with HTTP_UPDATE_FAILED + // because only the redirect response arrives instead of the .bin file. See + // e.g. espressif/arduino-esp32#3020. HTTPC_STRICT_FOLLOW_REDIRECTS + // suffices since we only use GET. httpUpdate.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); - // Wir zeigen nach erfolgreicher Installation selbst noch eine kurze - // Erfolgsmeldung an, bevor das Geraet neu startet - siehe +// We show a brief success message ourselves after successful installation, + // before the device reboots - see // menu_screen.cpp::runOtaUpdateScreen(). httpUpdate.rebootOnUpdate(false); httpUpdate.onProgress([onProgress](int cur, int total) { if (onProgress && total > 0) { onProgress((uint8_t)((cur * 100) / total)); } - // Nur gelegentlich loggen (alle ~10%), sonst quillt der Seriell- - // Monitor bei grossen Dateien mit hunderten Zeilen ueber. +// Only log occasionally (every ~10%), otherwise the serial + // monitor overflows with hundreds of lines for large files. static int8_t lastLoggedPercent = -1; if (total > 0) { int8_t pct = (int8_t)((cur * 100) / total); if (pct != lastLoggedPercent && pct % 10 == 0) { lastLoggedPercent = pct; - Serial.printf("[OTA] Fortschritt: %d%% (%d/%d Bytes) freeHeap=%u\n", pct, cur, total, + Serial.printf("[OTA] Progress: %d%% (%d/%d bytes) freeHeap=%u\n", pct, cur, total, (unsigned)ESP.getFreeHeap()); } } @@ -160,11 +159,11 @@ bool performUpdate(const char* url, void (*onProgress)(uint8_t percent)) { t_httpUpdate_return result = httpUpdate.update(client, url); if (result != HTTP_UPDATE_OK) { - Serial.printf("[OTA] Fehlgeschlagen: result=%d error=%d (%s) freeHeap=%u\n", (int)result, +Serial.printf("[OTA] Failed: result=%d error=%d (%s) freeHeap=%u\n", (int)result, httpUpdate.getLastError(), httpUpdate.getLastErrorString().c_str(), (unsigned)ESP.getFreeHeap()); } else { - Serial.println("[OTA] Erfolgreich heruntergeladen und geflasht."); + Serial.println("[OTA] Successfully downloaded and flashed."); } return result == HTTP_UPDATE_OK; diff --git a/src/ota_update.h b/src/ota_update.h index 48b0a4f..b4a2303 100644 --- a/src/ota_update.h +++ b/src/ota_update.h @@ -1,42 +1,42 @@ #pragma once #include -// Firmware-Update ueber WLAN (Menue > System > "Nach Update suchen") - -// prueft gegen die neueste Version im GitHub-Repository (Releases-API) und -// installiert sie bei Bestaetigung direkt auf dem Geraet, ohne Kabel/ -// Web-Flasher. Blockierende, synchrone HTTPS-Aufrufe - werden nur bei -// explizitem Tastendruck ausgefuehrt (kein Hintergrund-Polling), analog zu -// AircraftDetails' Modell-/Routen-Abfragen, nur eben auf Core 1 statt -// Core 0, da hier direkt auf Nutzer-Interaktion reagiert wird. +// Firmware update over WiFi (Menu > System > "Check for update") - +// checks against the latest version in the GitHub repository (Releases API) and +// installs it on the device directly upon confirmation, without cable/ +// web flasher. Blocking, synchronous HTTPS calls - only executed on +// explicit button press (no background polling), analogous to +// AircraftDetails' model/route queries, just on Core 1 instead of +// Core 0, since it reacts directly to user interaction. namespace OtaUpdate { enum class CheckResult { Error, UpToDate, UpdateAvailable }; struct CheckInfo { CheckResult result = CheckResult::Error; - // Neueste verfuegbare Version OHNE "v"-Praefix (z.B. "3.5.0") - - // sowohl bei UpToDate als auch bei UpdateAvailable gesetzt, damit - // der aufrufende Screen sie in beiden Faellen anzeigen kann. +// Latest available version WITHOUT "v" prefix (e.g. "3.5.0") - + // set for both UpToDate and UpdateAvailable, so + // the calling screen can display it in both cases. char latestVersion[16] = {0}; - // Direkter Download-Link zur firmware.bin des Releases - nur - // gesetzt, wenn result == UpdateAvailable. +// Direct download link to the firmware.bin of the release - only + // set when result == UpdateAvailable. char downloadUrl[192] = {0}; }; - // Fragt die GitHub-Releases-API nach dem neuesten Release ab, vergleicht - // dessen Versionsnummer (Tag-Name, "v"-Praefix wird ignoriert) gegen - // Config::APP_VERSION und sucht im Release den Anhang "firmware.bin". - // CheckResult::Error bei jedem Fehler unterwegs (kein WLAN, Zeitueber- - // schreitung, unerwartetes JSON, kein firmware.bin im Release). +// Queries the GitHub Releases API for the latest release, compares + // its version number (tag name, "v" prefix is ignored) against + // Config::APP_VERSION and looks for the "firmware.bin" attachment in the release. + // CheckResult::Error on any error along the way (no WiFi, timeout, + // unexpected JSON, no firmware.bin in the release). CheckInfo checkForUpdate(); - // Laedt die firmware.bin von 'url' herunter und flasht sie (ueber die - // Standard-Arduino-HTTPUpdate-Bibliothek). Ruft bei Erfolg BEWUSST - // NICHT selbst ESP.restart() auf - der Aufrufer zeigt zuerst eine - // kurze Erfolgsmeldung an und startet danach selbst neu. onProgress - // wird waehrend des Downloads wiederholt mit 0-100 (Prozent) aufgerufen - // (darf nullptr sein). Gibt false zurueck, wenn Download oder Flash- - // Vorgang fehlschlagen - das Geraet laeuft in dem Fall unveraendert mit - // der bisherigen Firmware weiter. +// Downloads the firmware.bin from 'url' and flashes it (via the + // standard Arduino HTTPUpdate library). Intentionally does NOT call + // ESP.restart() itself on success - the caller shows a + // brief success message first and then restarts itself. onProgress + // is called repeatedly during download with 0-100 (percent) + // (may be nullptr). Returns false if download or flash + // process fails - the device continues running unchanged with + // the previous firmware in that case. bool performUpdate(const char* url, void (*onProgress)(uint8_t percent)); } diff --git a/src/radar_screen.cpp b/src/radar_screen.cpp index 0765b6c..351a3e6 100644 --- a/src/radar_screen.cpp +++ b/src/radar_screen.cpp @@ -41,11 +41,11 @@ int16_t cx, cy, radius; }; constexpr int16_t INFO_BAR_H = 64; - // Zusaetzliche Hoehe fuer die zweite Legenden-Zeile (Bodenfahrzeug- - // Marker-Erklaerung), nur reserviert, wenn Bodenfahrzeuge tatsaechlich - // angezeigt werden (SettingsStore::hideGroundVehicles() == false) - - // ansonsten bleibt die Infoleiste genau so hoch wie bisher und der - // Radarkreis behaelt seine gewohnte Groesse. +// Additional height for the second legend row (ground vehicle + // marker explanation), only reserved when ground vehicles are actually + // displayed (SettingsStore::hideGroundVehicles() == false) - + // otherwise the info bar stays exactly as tall as before and the + // radar circle keeps its usual size. constexpr int16_t INFO_BAR_GROUND_ROW_H = 18; int16_t infoBarHeight() { @@ -69,10 +69,10 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64 return L; } - // +22px gegenueber frueher (264) fuer die neue Route-Zeile (siehe - // drawDetailPanel()) - beeinflusst NUR die Groesse des Detail-Panel- - // Overlays, nicht das normale Radar-Layout (computeLayout() oben - // richtet sich ausschliesslich nach infoBarHeight(), nicht nach +// +22px compared to the previous value (264) for the new route line (see + // drawDetailPanel()) - only affects the size of the detail panel + // overlay, not the normal radar layout (computeLayout() above + // only depends on infoBarHeight(), not // DETAIL_PANEL_H). constexpr int16_t DETAIL_PANEL_H = 286; @@ -103,15 +103,15 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64 bool ledBlinkOn = true; -// "Leerer Himmel"-Timer: merkt sich, wann zuletzt mindestens ein - // Flugzeug sichtbar war (nach allen Filtern) - namespace-weit statt - // lokal in render(), da tick() (siehe dort) den Wert bei jedem Tick - // (alle 80ms) braucht, um den Sekundenzaehler fluessig hochzuzaehlen, - // statt nur alle paar Sekunden bei einem render()-Aufruf. +// "Empty Sky" timer: remembers when at least one + // aircraft was last visible (after all filters) - namespace-wide instead of + // local to render(), since tick() (see there) needs the value on every tick + // (every 80ms) to count up the seconds smoothly, + // instead of only every few seconds on a render() call. uint32_t lastAircraftSeenMs = millis(); - // Wird von der Detail-Panel-Laufschrift (LineMarquee in drawDetailPanel) - // fuer das Scrollen von Routentext verwendet. +// Used by the detail panel marquee (LineMarquee in drawDetailPanel) + // for scrolling route text. constexpr uint32_t INFO_MARQUEE_STEP_MS = 200; String infoMarqueeWindow(TFT_eSPI& tft, const String& src, int32_t startIdx, int16_t maxWidth) { @@ -133,16 +133,16 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64 float prevSweepAngleDeg = -1.0f; constexpr float SWEEP_DEGREES_PER_SEC = 45.0f; - // Gleiche Logik wie main.cpp::isNightDimHours() - Nacht = zwischen - // Sonnenuntergang und Sonnenaufgang am aktiven Standort (siehe - // sun_times.h), mit Rueckfall auf ein festes 22:00-06:00-Fenster, - // solange Standort/Uhrzeit noch nicht bekannt sind. Hier bewusst - // dupliziert statt geteilt, siehe CLAUDE.md-Konvention ("jeder Screen - // unabhaengig lauffaehig, kein gemeinsames Modul fuer solche - // Kleinigkeiten"). +// Same logic as main.cpp::isNightDimHours() - night = between + // sunset and sunrise at the active location (see + // sun_times.h), with fallback to a fixed 22:00-06:00 window + // as long as location/time are not yet known. Deliberately + // duplicated instead of shared, see CLAUDE.md convention ("each screen + // independently runnable, no shared module for such + // small items"). bool isNightHours() { time_t now = time(nullptr); - if (now <= 8 * 3600 * 2) return false; // Uhrzeit noch nicht per NTP synchronisiert + if (now <= 8 * 3600 * 2) return false; // time not yet synced via NTP struct tm tmNow; localtime_r(&now, &tmNow); @@ -164,22 +164,23 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64 return (hour >= 22 || hour < 6); } - // Nur aktiv, wenn der Nutzer die Nachtdimmung (Menue > System) eingeschaltet - // hat UND wir gerade im Nachtfenster sind - dieselbe Einstellung, die sonst - // nur die Hintergrundbeleuchtung dimmt, dimmt jetzt zusaetzlich auch die - // Radar-Farben (Flugzeug-Marker + Sweep-Linie), damit das Display nachts - // insgesamt weniger blendet. +// Only active when the user has enabled night dimming (Menu > System) + // AND we are currently in the night window - the same setting that otherwise + // only dims the backlight, now additionally also dims the + // radar colors (aircraft markers + sweep line), so the display + // is less glaring overall at night. bool nightDimActiveNow() { return SettingsStore::nightDimmingEnabled() && isNightHours(); } - // Farbe je nach Flughoehe - gedaempft (dunkleres Gruen/Oliv) waehrend der - // Nachtdimmung. +// Color by altitude - dimmed (darker green/olive) during + // night dimming. // - // Die rote Hoehenstufe wird bewusst NICHT gedaempft: der bisherige - // Dimm-Ton (160,30,0) sah auf dem Display eher orange-braun statt rot - // aus und war dadurch nicht mehr klar von der gelben Stufe zu - // unterscheiden. Rot bleibt deshalb Tag und Nacht der reine TFT_RED-Ton. + // The red altitude level is deliberately NOT dimmed: the previous + // dimmed tone (160,30,0) looked orange-brown instead of red + // on the display and was therefore no longer clearly distinguishable + // from the yellow level. Red therefore remains the pure TFT_RED tone + // day and night. uint16_t colorForAltitude(TFT_eSPI& gfx, int32_t altFt) { bool dim = nightDimActiveNow(); if (altFt < Config::COLOR_LOW_ALT_THRESHOLD_FT) @@ -193,34 +194,34 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64 return nightDimActiveNow() ? gfx.color565(0, 110, 0) : TFT_GREEN; } - // Feste Blau-Farbe fuer Bodenfahrzeug-Marker (ADS-B-Emitter-Kategorie - // "C*", z.B. Flughafen-Fahrzeuge) - bewusst unabhaengig von - // colorForAltitude(), da Bodenfahrzeuge praktisch immer auf 0ft stehen - // und sonst dieselbe Farbe wie niedrig fliegende Flugzeuge haetten, - // was die optische Unterscheidung (Quadrat vs. Kreis) unnoetig - // erschweren wuerde. Bei Nachtdimmung gedaempft, gleiches Muster wie +// Fixed blue color for ground vehicle markers (ADS-B emitter category + // "C*", e.g. airport vehicles) - deliberately independent of + // colorForAltitude(), since ground vehicles are practically always at 0ft + // and would otherwise have the same color as low-flying aircraft, + // which would unnecessarily complicate the visual distinction + // (square vs. circle). Dimmed during night dimming, same pattern as // colorForAltitude(). uint16_t colorForGroundVehicle(TFT_eSPI& gfx) { return nightDimActiveNow() ? gfx.color565(0, 0, 160) : TFT_BLUE; } - // Zeigt die PEILUNG (nicht zu verwechseln mit der Flugrichtung/heading) - // zum aktuell ausgewaehlten Flugzeug: eine duenne, gepunktete Linie vom - // Radar-Zentrum zum Kreisrand in Richtung bearingDeg, plus die Gradzahl - // direkt ausserhalb des Rings. Hilft dabei, das Flugzeug tatsaechlich am - // Himmel zu finden ("in diese Richtung schauen"). Nur fuer den Moment - // des Zeichnens relevant - beim naechsten Sweep-Tick/Redraw wird sie vom - // normalen Hintergrund-Redraw automatisch mit geloescht und neu gesetzt. +// Shows the BEARING (not to be confused with the heading) + // to the currently selected aircraft: a thin, dotted line from the + // radar center to the circle edge in the direction of bearingDeg, plus the degree number + // directly outside the ring. Helps to actually find the aircraft + // in the sky ("look in this direction"). Only relevant for the moment + // of drawing - on the next sweep tick/redraw it is automatically + // erased and redrawn by the normal background redraw. void drawBearingIndicator(TFT_eSPI& gfx, const Layout& L, float bearingDeg) { double rad = bearingDeg * PI / 180.0; double s = sin(rad), c = cos(rad); - // Gepunktete Linie: kurze Segmente statt einer durchgezogenen Linie, - // damit sie sich optisch von den Kompass-Kreuzlinien und Ringen - // unterscheidet und nicht wie ein staendiges UI-Element wirkt. +// Dotted line: short segments instead of a solid line, + // so it visually differs from the compass cross lines and rings + // and does not look like a permanent UI element. constexpr uint8_t DOT_COUNT = 10; for (uint8_t i = 0; i < DOT_COUNT; i++) { - if (i % 2 != 0) continue; // nur jedes zweite Segment zeichnen + if (i % 2 != 0) continue; // only draw every second segment float t0 = (float)i / DOT_COUNT; float t1 = (float)(i + 1) / DOT_COUNT; int16_t x0 = L.cx + (int16_t)lround(t0 * L.radius * s); @@ -230,12 +231,12 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64 gfx.drawLine(x0, y0, x1, y1, TFT_WHITE); } - // Gradzahl knapp ausserhalb des Rings, an der Stelle wo die Peilung - // den Kreisrand durchstoesst. Der Radius-Kreis nutzt fast die volle - // Bildschirmbreite (siehe computeLayout(): nur 6px Rand) - ein Label - // ausserhalb des Rings wuerde bei Ost/West-Peilungen ueber den - // sichtbaren Bereich hinausragen. Deshalb: Label-Position an die - // Bildschirmgrenzen clampen statt stur dem Winkel zu folgen. +// Bearing degree just outside the ring, at the point where the bearing + // line intersects the circle edge. The radius circle uses almost the full + // screen width (see computeLayout(): only 6px margin) - a label + // outside the ring would extend beyond the visible area + // for east/west bearings. Therefore: clamp label position to + // screen boundaries instead of blindly following the angle. int16_t labelX = L.cx + (int16_t)lround((L.radius + 10) * s); int16_t labelY = L.cy - (int16_t)lround((L.radius + 10) * c); labelX = constrain(labelX, (int16_t)14, (int16_t)(Config::SCREEN_WIDTH - 14)); @@ -248,11 +249,11 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64 gfx.setTextDatum(TL_DATUM); } - // kurzer Kursstrich in Flugrichtung (headingDeg) MIT Pfeilspitze am Ende - - // eine reine Linie ohne Spitze war nicht eindeutig lesbar (nicht erkennbar, - // welches Ende "vorne" ist). Die Spitze besteht aus zwei kurzen Strichen, - // die knapp vor der Linienspitze schraeg nach aussen abzweigen (klassisches - // Chevron-/Pfeilkopf-Symbol, wie bei ATC-Radardarstellungen ueblich). +// Short course line in flight direction (headingDeg) WITH arrowhead at the end - + // a plain line without a tip was not clearly readable (not recognizable + // which end is "front"). The tip consists of two short strokes + // that branch diagonally outward just before the line tip (classic + // chevron/arrowhead symbol, as typical in ATC radar displays). void drawAircraftMarker(TFT_eSPI& gfx, int16_t x, int16_t y, float headingDeg, uint16_t color) { gfx.fillCircle(x, y, 5, color); @@ -263,9 +264,9 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64 int16_t tipY = y + dy; gfx.drawLine(x, y, tipX, tipY, color); - // Pfeilspitze: zwei kurze Striche von der Spitze aus, je 150 Grad - // zur Kurslinie zurueckgeklappt (also leicht "nach hinten" zeigend, - // wie bei einem Pfeilkopf "^" ueblich). +// Arrowhead: two short strokes from the tip, each folded back 150 degrees + // to the course line (i.e. slightly pointing "backward", + // as is common for an arrowhead "^"). constexpr double WING_ANGLE_RAD = 150.0 * PI / 180.0; constexpr int16_t WING_LEN = 4; double wing1 = rad + WING_ANGLE_RAD; @@ -278,21 +279,21 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64 gfx.drawLine(tipX, tipY, w2x, w2y, color); } - // Eigener Marker fuer Bodenfahrzeuge (ADS-B-Kategorie "C*") - ein - // gefuelltes Quadrat statt Kreis+Pfeilkopf, damit sie auf dem Radar - // klar von echten Flugzeugen zu unterscheiden sind (Heading/Kurs ist - // bei Bodenfahrzeugen ausserdem meist nicht aussagekraeftig). +// Separate marker for ground vehicles (ADS-B category "C*") - a + // filled square instead of circle+arrowhead, so they are clearly + // distinguishable from real aircraft on the radar (heading/course is + // also usually not meaningful for ground vehicles). void drawGroundVehicleMarker(TFT_eSPI& gfx, int16_t x, int16_t y, uint16_t color) { constexpr int16_t HALF = 4; gfx.fillRect((int16_t)(x - HALF), (int16_t)(y - HALF), (int16_t)(2 * HALF), (int16_t)(2 * HALF), color); } - // Eigener Marker fuer Hubschrauber (ADS-B-Emitter-Kategorie "A7" = - // Rotorcraft) - ein gefuellter Kreis mit durchgehendem Rotorkreuz statt - // Pfeilkopf, da Hubschrauber im Schwebeflug keinen aussagekraeftigen - // "nach vorne"-Kurs wie ein Flugzeug haben (gleiche Ueberlegung wie beim - // Bodenfahrzeug-Marker, der aus demselben Grund ebenfalls ohne - // Kurslinie auskommt). +// Separate marker for helicopters (ADS-B emitter category "A7" = + // Rotorcraft) - a filled circle with a solid rotor cross instead of + // an arrowhead, since helicopters in hover have no meaningful + // "forward" course like a fixed-wing aircraft (same reasoning as the + // ground vehicle marker, which for the same reason also + // does without a course line). void drawHelicopterMarker(TFT_eSPI& gfx, int16_t x, int16_t y, uint16_t color) { constexpr int16_t ROTOR_LEN = 8; gfx.fillCircle(x, y, 4, color); @@ -300,14 +301,14 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64 gfx.drawLine(x, (int16_t)(y - ROTOR_LEN), x, (int16_t)(y + ROTOR_LEN), color); } - // Eigener Marker fuer "Heavy"-Flugzeuge (ADS-B-Emitter-Kategorie "A5" - - // Flugzeuge ueber 136t Starthoechstgewicht, z.B. A380/B747/B777/A330). - // Gleicher Aufbau wie drawAircraftMarker() (Kreis + Kurslinie mit - // Pfeilkopf), aber mit groesserem Kreis und einem zusaetzlichen duennen - // Aussenring - auf den ersten Blick als "groesser/schwerer" erkennbar, - // ohne eine komplett neue Formsprache einzufuehren. Die Farbe bleibt wie - // gewohnt die Hoehenfarbe (colorForAltitude()) - die Form transportiert - // "Heavy", kein Alarm-/Auffaelligkeitszustand. +// Separate marker for "Heavy" aircraft (ADS-B emitter category "A5" - + // aircraft over 136t maximum takeoff weight, e.g. A380/B747/B777/A330). + // Same structure as drawAircraftMarker() (circle + course line with + // arrowhead), but with a larger circle and an additional thin + // outer ring - recognizable at first glance as "larger/heavier", + // without introducing a completely new shape language. The color remains + // the usual altitude color (colorForAltitude()) - the shape conveys + // "Heavy", not an alarm/conspicuity state. void drawHeavyMarker(TFT_eSPI& gfx, int16_t x, int16_t y, float headingDeg, uint16_t color) { gfx.fillCircle(x, y, 6, color); gfx.drawCircle(x, y, 8, color); @@ -368,12 +369,12 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64 snprintf(highLabel, sizeof(highLabel), ">30k ft"); } - // Dieselbe colorForAltitude()-Funktion wie fuer die Flugzeug-Marker - // selbst verwenden (mit einer typischen Hoehe je Band) - sonst zeigt - // die Legende bei aktiver Nachtdimmung (Sonnenunter- bis -aufgang) die HELLEN Farben, - // waehrend die Marker/Beschriftungen auf dem Radar bereits gedaempft - // sind. Das fuehrte dazu, dass z.B. ein rotes Flugzeug nachts eher - // dunkelorange wirkte, obwohl die Legende noch reines Rot zeigte. +// Use the same colorForAltitude() function as for the aircraft markers + // themselves (with a typical altitude per band) - otherwise the + // legend would show the BRIGHT colors during active night dimming (sunset to sunrise), + // while the markers/labels on the radar are already dimmed. + // This caused e.g. a red aircraft to appear dark orange at night, + // even though the legend still showed pure red. struct { uint16_t color; const char* label; } items[3] = { {colorForAltitude(gfx, 0), lowLabel}, {colorForAltitude(gfx, Config::COLOR_LOW_ALT_THRESHOLD_FT), midLabel}, @@ -382,15 +383,15 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64 int16_t segW = Config::SCREEN_WIDTH / 3; gfx.setTextColor(TFT_WHITE, TFT_BLACK); - // Aussenspalten (gruen/rot) bleiben auf ihrer festen 1/3-Raster- - // Position (x0 = i*segW+6). Die mittlere (gelbe) Spalte wurde - // bisher genauso starr positioniert, sass dadurch aber optisch zu - // weit rechts: ihr Label ("3000-9100") ist deutlich laenger als - // die Aussenlabels, wodurch rechts kaum noch Luft zum roten - // Eintrag blieb, waehrend links viel Freiraum zum kurzen gruenen - // Label uebrig war. Jetzt wird die gelbe Spalte stattdessen mittig - // in die Luecke zwischen Ende des gruenen Textes und Anfang des - // roten Punkts gesetzt. +// Outer columns (green/red) stay at their fixed 1/3 grid + // position (x0 = i*segW+6). The middle (yellow) column was + // previously positioned just as rigidly, but visually sat too + // far right: its label ("3000-9100") is noticeably longer than + // the outer labels, leaving hardly any space on the right to the + // red entry, while lots of empty space remained on the left next to the short green + // label. Now the yellow column is instead placed + // centered in the gap between the end of the green text and the start of the + // red dot. int16_t x0Low = 0 * segW + 6; int16_t x0High = 2 * segW + 6; @@ -412,10 +413,10 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64 gfx.setCursor(x0Mid + 7, y); gfx.print(items[1].label); - // Zweite Legenden-Zeile fuer die blauen Bodenfahrzeug-Quadrate - nur - // wenn sie ueberhaupt sichtbar sind (Flugoptionen > - // "Bodenfahrzeuge ausblenden" == aus). infoBarHeight() reserviert - // den dafuer noetigen zusaetzlichen Platz nur in diesem Fall, siehe +// Second legend row for the blue ground vehicle squares - only + // when they are actually visible (Flight Options > + // "Hide ground vehicles" == off). infoBarHeight() reserves + // additional space for this only in that case, see // computeLayout(). if (!SettingsStore::hideGroundVehicles()) { int16_t gy = (int16_t)(y + INFO_BAR_GROUND_ROW_H); @@ -469,10 +470,10 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64 gfx.drawString("S", L.cx, L.cy + L.radius - 10); gfx.drawString("E", L.cx + L.radius - 10, L.cy); gfx.drawString("W", L.cx - L.radius + 10, L.cy); - // Ring-Beschriftungen (Zwischenabstaende) respektieren jetzt die - // Einheiten-Einstellung (Menue > Einheiten) - vorher immer in km, - // auch wenn Imperial (nm) eingestellt war. Gleiches Umrechnungs- - // Muster wie beim Range-Button unten und der Legende oben. +// Ring labels (intermediate distances) now respect the + // unit setting (Menu > Units) - previously always in km, + // even when Imperial (nm) was set. Same conversion + // pattern as the range button below and the legend above. bool metric = LocationManager::useMetricUnits(); float displayRange = metric ? rangeKm : Units::kmToNm(rangeKm); char ringLabel[8]; @@ -483,14 +484,14 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64 gfx.setTextDatum(TL_DATUM); } - // Laufschrift-Zustand fuer EINE Detail-Panel-Zeile - gleiches - // Grundprinzip wie InfoMarquee oben, aber das Detail-Panel hat bis zu - // neun solcher Zeilen gleichzeitig (Airline, Modell, Typ, Hoehe, - // Geschw., Steig-/Sinkrate, Distanz/Peilung, Squawk, Sitzplaetze), - // deshalb ein eigener Zustand pro Zeile statt einer einzigen globalen - // Instanz. y/h/maxWidth/fg merken sich die Zeilen-Geometrie, damit - // tickDetailPanelMarquees() (siehe unten) ohne Zusatzparameter weiss, - // wo/wie jede Zeile neu zu zeichnen ist. +// Marquee state for ONE detail panel line - same basic + // principle as InfoMarquee above, but the detail panel has up to + // nine such lines simultaneously (airline, model, type, altitude, + // speed, climb/sink rate, distance/bearing, squawk, seats), + // hence a separate state per line instead of a single global + // instance. y/h/maxWidth/fg remember the line geometry so that + // tickDetailPanelMarquees() (see below) knows where/how to + // redraw each line without extra parameters. struct LineMarquee { String text; String ring; @@ -509,22 +510,22 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64 }; PanelState lastPanel; - // Zeichnet eine Detail-Panel-Zeile NEU, wenn sich ihr Text geaendert - // hat (oder das Panel komplett neu aufgebaut wird) - merkt sich dabei - // auch, ob der Text zu breit fuer die verfuegbare Breite ist - // (needsScroll) und baut bei Bedarf den Ring-Puffer fuer die - // Laufschrift auf (gleiches Muster wie infoMarqueeWindow() oben). - // Passt zu breite Zeilen ("Modell: ...", "Distanz: ...") NICHT mehr - // mit "..." ab, sondern laesst sie horizontal durchscrollen - siehe - // tickDetailPanelMarquees() weiter unten fuer den Teil, der das - // tatsaechliche Weiterscrollen zwischen zwei Datenaktualisierungen - // uebernimmt (render() liefert i.d.R. nur alle ~300ms neue Werte, - // das waere fuer eine fluessige Laufschrift viel zu selten). +// Redraws a detail panel line when its text has changed + // (or the panel is being rebuilt) - remembers whether + // the text is too wide for the available width + // (needsScroll) and builds the ring buffer for the + // marquee if needed (same pattern as infoMarqueeWindow() above). + // Lines that are too wide ("Model: ...", "Distance: ...") are NO + // LONGER abbreviated with "..." but scrolled horizontally - see + // tickDetailPanelMarquees() below for the part that + // actually advances the scrolling between data updates + // (render() typically only supplies new values every ~300ms, + // which would be far too rare for smooth scrolling). void updateMarqueeLine(TFT_eSPI& gfx, int16_t y, int16_t h, int16_t maxWidth, uint16_t fg, LineMarquee& m, const String& newText, bool forceFull) { - // Geometrie/Farbe IMMER aktualisieren (auch ohne Textaenderung) - - // tickDetailPanelMarquees() braucht diese Werte, um beim naechsten - // Scroll-Schritt an der richtigen Stelle neu zu zeichnen. +// Always update geometry/color (even without text change) - + // tickDetailPanelMarquees() needs these values to redraw + // at the correct position on the next scroll step. m.y = y; m.h = h; m.maxWidth = maxWidth; @@ -534,7 +535,7 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64 m.text = newText; m.needsScroll = gfx.textWidth(newText) > maxWidth; - String withGap = newText + " "; // 3 Leerzeichen Luecke vor der Wiederholung + String withGap = newText + " "; // 3 space gap before repetition m.ring = withGap + withGap; m.charOffset = 0; m.lastStepMs = millis(); @@ -545,11 +546,11 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64 gfx.print(m.needsScroll ? infoMarqueeWindow(gfx, m.ring, 0, maxWidth) : newText); } - // Laesst eine einzelne Detail-Panel-Zeile weiterscrollen, falls sie zu - // breit ist (needsScroll) und genug Zeit seit dem letzten Schritt - // vergangen ist - sonst passiert nichts (kein unnoetiges Neuzeichnen - // fuer Zeilen, die ohnehin komplett passen). Wird von - // tickDetailPanelMarquees() fuer alle neun Zeilen aufgerufen. +// Advances a single detail panel line if it is too + // wide (needsScroll) and enough time has passed since the last step + // - otherwise nothing happens (no unnecessary redrawing + // for lines that fit completely). Called by + // tickDetailPanelMarquees() for all nine lines. void advanceAndDrawMarqueeLine(TFT_eSPI& gfx, LineMarquee& m) { if (!m.needsScroll) return; @@ -567,12 +568,12 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64 gfx.print(infoMarqueeWindow(gfx, m.ring, m.charOffset, m.maxWidth)); } - // Laesst alle Detail-Panel-Zeilen weiterscrollen - waehrend das Panel - // offen ist, ruft tick() (alle ~80ms) das hier auf, DAZWISCHEN also - // viel oefter als render() (das nur bei neuen Flugzeug-Daten, - // ~alle 300ms, feuert) neue Werte liefert. Ohne diesen zusaetzlichen - // Aufruf wuerde eine lange Zeile bei jedem render()-Aufruf zwar einen - // Schritt weiterspringen, aber nicht fluessig durchlaufen. +// Lets all detail panel lines scroll - while the panel + // is open, tick() (every ~80ms) calls this, in BETWEEN much + // more often than render() (which only fires on new aircraft data, + // every ~300ms). Without this additional + // call, a long line would jump one step on each render() call + // but would not scroll smoothly. void tickDetailPanelMarquees(TFT_eSPI& gfx) { if (!lastPanel.valid) return; advanceAndDrawMarqueeLine(gfx, lastPanel.airline); @@ -632,11 +633,11 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64 updateMarqueeLine(gfx, y, LINE_H, textMaxWidth, TFT_GREEN, lastPanel.type, typeLine, forceFull); y += LINE_H; - // Start-/Zielflughafen (ICAO-Code) ueber AircraftDetails::get() - - // wird zusammen mit dem Modell abgefragt (siehe aircraft_details.cpp), - // aber ueber das Rufzeichen statt den Hex-Code aufgeloest. Ohne - // Rufzeichen (z.B. manche Sichtflug-Maschinen) bleibt die Route - // grundsaetzlich unbekannt, kein Abruf noetig. +// Origin/destination airport (ICAO code) via AircraftDetails::get() - + // is queried together with the model (see aircraft_details.cpp), + // but resolved via the callsign instead of the hex code. Without + // a callsign (e.g. some VFR aircraft) the route remains + // fundamentally unknown, no lookup needed. String routeLine; if (!a.callsign[0]) { routeLine = String(I18n::t(StringId::DETAIL_ROUTE)) + I18n::t(StringId::DETAIL_UNKNOWN); @@ -674,11 +675,11 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64 updateMarqueeLine(gfx, y, LINE_H, textMaxWidth, TFT_GREEN, lastPanel.climb, climbLine, forceFull); y += LINE_H; - // Zusaetzlich zu km/nm auch Meilen (mi) - nm allein war fuer - // Laien-Nutzer aus Ländern mit imperialen Einheiten (v.a. USA) - // nicht selbsterklaerend, "Meilen" sind dort die gebraeuchlichere - // Alltags-Distanzeinheit (nm bleibt trotzdem stehen, da es zur - // Geschwindigkeit in kt passt: 1 kt = 1 nm/h). +// In addition to km/nm also miles (mi) - nm alone was + // not self-explanatory for lay users from countries with + // imperial units (especially the US), "miles" is the more common + // everyday distance unit there (nm still remains since it matches + // speed in kt: 1 kt = 1 nm/h). snprintf(buf, sizeof(buf), "%s%.0fkm / %.0fnm / %.0fmi %s%.0f", I18n::t(StringId::DETAIL_DIST), a.distanceKm, Units::kmToNm(a.distanceKm), Units::kmToMi(a.distanceKm), I18n::t(StringId::DETAIL_HDG), a.headingDeg); @@ -738,15 +739,15 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64 } } - // Hintergrund-Sterne AUSSERHALB des Radarkreises (auf Wunsch: dasselbe - // dekorative Sternenfunkeln wie im Detail-Panel/Menue, aber ohne mit - // dem staendig neu gezeichneten Kreisinhalt zu kollidieren - genau DAS - // wuerde "ruckeln" verursachen, da Sweep-Linie/Blips/Ringe innerhalb - // des Kreises bei jedem tick() sowieso neu gezeichnet werden und dabei - // die Sterne dort staendig wieder verschlucken wuerden). Ausserhalb - // des Kreises bleibt der Hintergrund dauerhaft schwarz und wird von - // sonst niemandem angefasst - dort koennen die Sterne frei vor sich - // hin twinkeln, voellig unabhaengig von Sweep-Linie/Blip-Redraws. +// Background stars OUTSIDE the radar circle (requested: the same + // decorative star twinkling as in the detail panel/menu, but without + // colliding with the constantly redrawn circle content - THAT would + // cause stuttering, since the sweep line/blips/rings within the + // circle are redrawn on every tick() anyway and would constantly + // swallow the stars there). Outside the circle the background remains + // permanently black and is touched by no one else - the stars can + // freely twinkle there, completely independent of sweep line/blip + // redraws. constexpr uint8_t BG_STAR_COUNT = 20; struct BgStar { int16_t x, y; @@ -756,15 +757,15 @@ L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 72), (int16_t)(L.infoTop + 4), 64 BgStar bgStars[BG_STAR_COUNT]; bool bgStarsInitialized = false; - // Platziert jeden Stern per Rejection-Sampling irgendwo im Bereich - // [top..L.infoTop) x [0..SCREEN_WIDTH), aber nur wenn der Punkt - // ausserhalb von Radius+MARGIN um den Kreismittelpunkt liegt (MARGIN - // haelt zusaetzlich Abstand zum Ring und zum "N"-Kompasslabel knapp - // ausserhalb des Kreises). Findet ein Versuch nach 25 Anlaeufen keinen - // passenden Punkt (praktisch nie, da links/rechts vom Kreis immer - // reichlich Platz ist), wird einfach der letzte Versuchspunkt - // uebernommen - rein kosmetisches Feature, kein Grund fuer eine - // Endlosschleife. +// Places each star via rejection sampling somewhere in the area + // [top..L.infoTop) x [0..SCREEN_WIDTH), but only if the point + // is outside radius+MARGIN around the circle center (MARGIN + // additionally keeps distance from the ring and the "N" compass label + // just outside the circle). If an attempt after 25 tries finds no + // suitable point (practically never, since there is always plenty + // of space left/right of the circle), the last attempted point is + // simply used - purely cosmetic feature, no reason for an + // infinite loop. void initBgStarsIfNeeded(const Layout& L, int16_t top) { if (bgStarsInitialized) return; randomSeed((uint32_t)esp_random()); @@ -855,7 +856,7 @@ void render(TFT_eSPI& tft, int16_t top) { bool selectionStillPresent = false; Aircraft selected{}; - uint8_t visibleCount = 0; // nach allen Filtern (Reichweite, Bodenfahrzeuge, Airline-Filter) + uint8_t visibleCount = 0; // after all filters (range, ground vehicles, airline filter) for (uint8_t i = 0; i < MAX_HIT_POINTS; i++) hitPoints[i].valid = false; @@ -876,14 +877,14 @@ if (AirlineFilter::isHidden(a.callsign)) continue; RadarMath::PolarCoord polar{a.distanceKm, a.bearingDeg}; RadarMath::ScreenPoint pt = RadarMath::toScreen(polar, L.cx, L.cy, L.radius, rangeKm); - // ADS-B-Emitter-Kategorie "C*" = Bodenfahrzeug (nur ueberhaupt - // sichtbar, wenn "Bodenfahrzeuge ausblenden" aus ist, siehe Filter - // oben) - eigene Farbe/Marker statt der hoehenbasierten Flugzeug- - // Darstellung, siehe drawGroundVehicleMarker()/colorForGroundVehicle(). +// ADS-B emitter category "C*" = ground vehicle (only ever + // visible if "Hide ground vehicles" is off, see filter + // above) - separate color/marker instead of altitude-based + // rendering, see drawGroundVehicleMarker()/colorForGroundVehicle(). bool isGroundVehicle = a.category[0] == 'C'; bool isRotorcraft = a.category[0] == 'A' && a.category[1] == '7'; - // ADS-B-Emitter-Kategorie "A5" = "Heavy" - eigene Markerform (siehe - // drawHeavyMarker()), unabhaengig von jeglicher Ring-Markierung. +// ADS-B emitter category "A5" = "Heavy" - separate marker shape (see + // drawHeavyMarker()), independent of any ring marking. bool isHeavy = a.category[0] == 'A' && a.category[1] == '5'; uint16_t color = isGroundVehicle ? colorForGroundVehicle(tft) : colorForAltitude(tft, a.altBaroFt); bool isSelected = selectedHex[0] && strcmp(a.hex, selectedHex) == 0; @@ -947,7 +948,7 @@ if (AirlineFilter::isHidden(a.callsign)) continue; lastPanel.valid = false; int16_t infoTop = L.infoTop; tft.drawFastHLine(0, infoTop, Config::SCREEN_WIDTH, TFT_DARKGREY); - // Info-Bar Button-Leiste: [?] [S] [L] [50km] + // Info bar button row: [?] [S] [L] [50km] char rangeLabel[8]; bool rangeMetric = LocationManager::useMetricUnits(); if (rangeMetric) { @@ -973,11 +974,11 @@ void tick(TFT_eSPI& tft, int16_t top, uint32_t deltaMs) { if (selectedHex[0]) { tft.startWrite(); updateDetailPanelStars(tft); - // Laesst zu breite Detail-Panel-Zeilen (z.B. "Modell: ..." oder - // "Distanz: ...") weiterscrollen, statt wie zuvor mit "..." starr - // abgeschnitten zu bleiben - render() liefert neue Werte nur alle - // ~300ms, das reicht fuer eine fluessige Laufschrift nicht aus, - // deshalb hier zusaetzlich im 80ms-Tick weiterschieben. +// Lets overly wide detail panel lines (e.g. "Model: ..." or + // "Distance: ...") scroll, instead of being statically truncated + // with "..." - render() only supplies new values every + // ~300ms, which is not enough for smooth scrolling, + // so it is additionally advanced here in the 80ms tick. tickDetailPanelMarquees(tft); tft.endWrite(); return; @@ -988,13 +989,13 @@ void tick(TFT_eSPI& tft, int16_t top, uint32_t deltaMs) { tft.startWrite(); - // Twinkeln ausserhalb des Radarkreises - laeuft im selben 80ms-Takt wie - // die Sweep-Linie, damit es fluessig wirkt. render() (bei jedem Aircraft- - // Update, alle ~300ms) loescht den kompletten Inhaltsbereich per fillRect - // und zeichnet ihn neu, OHNE die Sterne erneut zu setzen - das ist - // bewusst so (wie beim Detail-Panel-Sternenfeld auch): die naechste - // tick()-Runde (spaetestens 80ms spaeter) laesst sie einfach wieder - // aufblitzen, das ist zu kurz, um als Ruckeln wahrgenommen zu werden. +// Twinkling outside the radar circle - runs at the same 80ms rate as + // the sweep line so it looks smooth. render() (on every aircraft + // update, every ~300ms) clears the entire content area with fillRect + // and redraws it WITHOUT re-setting the stars - this is + // intentional (just like the detail panel star field): the next + // tick() round (at most 80ms later) will simply make them + // twinkle again, which is too brief to be perceived as stuttering. updateBgStars(tft, L, top); if (prevSweepAngleDeg >= 0.0f) { @@ -1015,16 +1016,16 @@ void tick(TFT_eSPI& tft, int16_t top, uint32_t deltaMs) { bool inAlertRange = hp.distanceKm <= Config::LED_ALERT_RADIUS_KM; if (inAlertRange && !ledBlinkOn) { - // Blink-Aus-Phase: frueher wurde hier ein pauschales 40x30px - // schwarzes Rechteck gezeichnet, um den Marker zu "verstecken" - - // das hat dabei aber auch die Radar-Ringe/Kompasslinien darunter - // ueberdeckt (bei jeder Hoehenfarbe gleichermassen, da diese - // Blink-Logik rein distanzbasiert ist). Stattdessen werden hier - // jetzt exakt dieselben Formen, die beim normalen Zeichnen weiter - // unten entstehen (Marker, Notfall-/Beobachtungs-Ring, Label), - // einfach in Schwarz nachgezeichnet - das macht sie pixelgenau - // wieder unsichtbar, ohne irgendetwas ausserhalb dieser Formen zu - // beruehren. +// Blink-off phase: previously a blanket 40x30px + // black rectangle was drawn to "hide" the marker - + // but that also covered the radar rings/compass lines + // underneath (equally for all altitude colors, since this + // blink logic is purely distance-based). Instead, exactly + // the same shapes that are drawn during normal rendering + // below (marker, emergency/watchlist ring, label) are + // now drawn in black - this makes them pixel-perfectly + // invisible again without touching anything outside + // those shapes. if (hp.isGroundVehicle) { drawGroundVehicleMarker(tft, hp.x, hp.y, TFT_BLACK); } else if (hp.isRotorcraft) { @@ -1068,9 +1069,9 @@ void tick(TFT_eSPI& tft, int16_t top, uint32_t deltaMs) { tft.setTextDatum(TL_DATUM); } - // Info-Zeile unten (Tap-Hinweis bzw. "Leerer Himmel"-Timer) als -// Laufende Aktualisierung der letzten Sichtung fuer den "Leerer - // Himmel"-Zaehler. +// Info line at bottom (tap hint or "Empty Sky" timer) as +// continuous update of the last sighting for the "Empty Sky" + // counter. uint8_t visibleCountNow = 0; for (uint8_t i = 0; i < MAX_HIT_POINTS; i++) { if (hitPoints[i].valid) visibleCountNow++; @@ -1539,10 +1540,10 @@ void runHelp(TFT_eSPI& tft, int16_t top) { void selectAircraft(const char* hex, const char* callsign) { strncpy(selectedHex, hex, sizeof(selectedHex) - 1); AircraftDetails::request(hex, callsign); - // Erzwingt einen kompletten Neuaufbau des Detail-Panels beim naechsten - // render() - wichtig, falls der Bildschirm zwischenzeitlich von einem - // anderen Screen (z.B. der Flugzeugliste) ueberschrieben wurde, sonst - // wuerden unveraenderte Zeilen faelschlich als "schon da" uebersprungen. +// Forces a complete rebuild of the detail panel on the next + // render() - important if the screen was overwritten by another + // screen (e.g. the aircraft list), otherwise unchanged lines would + // be incorrectly skipped as "already there". lastPanel.valid = false; } diff --git a/src/radar_screen.h b/src/radar_screen.h index e5a49e7..70a0183 100644 --- a/src/radar_screen.h +++ b/src/radar_screen.h @@ -21,10 +21,10 @@ void render(TFT_eSPI& tft, int16_t top); // Shows a full-screen usage guide popup triggered by the "?" button. void runHelp(TFT_eSPI& tft, int16_t top); - // Waehlt ein Flugzeug programmgesteuert aus (z.B. von der Flugzeugliste - // aus, nicht per Antippen auf dem Radar) - damit beim naechsten render() - // sofort das Detail-Panel fuer dieses Flugzeug erscheint, so als haette - // man es direkt im Radar angetippt. +// Selects an aircraft programmatically (e.g. from the aircraft list, + // not by tapping on the radar) - so that the next render() immediately + // shows the detail panel for this aircraft, as if it were tapped directly + // on the radar. void selectAircraft(const char* hex, const char* callsign); struct EmergencyInfo { diff --git a/src/sd_mutex.h b/src/sd_mutex.h index f791cad..d050ca0 100644 --- a/src/sd_mutex.h +++ b/src/sd_mutex.h @@ -1,22 +1,21 @@ #pragma once #include -// Gemeinsames Lock fuer ALLE SD-Kartenzugriffe im gesamten Projekt. Die -// SD-Bibliothek ist nicht thread-sicher: ohne dieses Lock koennen -// gleichzeitige Zugriffe von NetTask (Core 0, z.B. Flugbuch-Eintrag -// schreiben alle 8s) und dem Haupt-Loop (Core 1, z.B. Einstellungen im -// Menue speichern) das Geraet einfrieren, wenn sie zeitlich zusammentreffen. -// Rekursiv, damit verschachtelte Aufrufe (z.B. FlightLogbook::update() ruft -// intern ensureCurrentDate() auf, das seinerseits auch sperrt) nicht -// blockieren. +// Common lock for ALL SD card access in the entire project. The +// SD library is not thread-safe: without this lock, +// concurrent access by NetTask (Core 0, e.g. writing a logbook entry +// every 8s) and the main loop (Core 1, e.g. saving settings in the +// menu) can freeze the device if they coincide in time. +// Recursive, so that nested calls (e.g. FlightLogbook::update() calls +// ensureCurrentDate() internally, which also locks) do not +// block. namespace SdMutex { void init(); void lock(); void unlock(); - // RAII-Helfer: sperrt im Konstruktor, gibt im Destruktor frei - so kann - // man das Lock nicht versehentlich vergessen freizugeben (z.B. bei - // einem fruehen "return"). +// RAII helper: locks in constructor, unlocks in destructor - so you cannot + // accidentally forget to release the lock (e.g. on an early "return"). class Guard { public: Guard() { lock(); } diff --git a/src/sd_storage.cpp b/src/sd_storage.cpp index 530d904..297de00 100644 --- a/src/sd_storage.cpp +++ b/src/sd_storage.cpp @@ -153,11 +153,10 @@ bool deleteDirectoryRecursive(const char* path) { return false; } - // Gleiches vorsichtiges "entry.name() koennte relativ ODER absolut - // sein"-Muster wie in flight_logbook.cpp::resetAllData() - anders als - // dort aber mit echter Rekursion in Unterordner (statt sie zu - // ueberspringen), da der Flightradar-Ordner welche enthaelt (logs/, - // screenshots/). +// Same cautious "entry.name() could be relative OR absolute" pattern as + // in flight_logbook.cpp::resetAllData() - but unlike there, with actual + // recursion into subdirectories (instead of skipping them), since the + // Flightradar folder contains them (logs/, screenshots/). File entry = dir.openNextFile(); while (entry) { bool isDir = entry.isDirectory(); diff --git a/src/sd_storage.h b/src/sd_storage.h index 8dbda4c..c78d8d9 100644 --- a/src/sd_storage.h +++ b/src/sd_storage.h @@ -16,12 +16,12 @@ namespace SdStorage { void seedDefaultDataFiles(); void logEvent(const char* csvLine); - // Loescht rekursiv einen kompletten Ordner (alle Dateien und - // Unterordner) inklusive des Ordners selbst. Fuer den Menuepunkt - // "Einstellungen zuruecksetzen" (settings_backup.cpp::factoryReset()) - - // der einzige aktuelle Aufrufer, der damit den kompletten - // Flightradar-Ordner von der SD-Karte entfernt. Gibt false zurueck, - // wenn der Pfad nicht existiert/kein Ordner ist. +// Recursively deletes a complete folder (all files and + // subfolders) including the folder itself. For the menu item + // "Reset settings" (settings_backup.cpp::factoryReset()) - + // the only current caller, which removes the complete + // Flightradar folder from the SD card. Returns false + // if the path does not exist / is not a folder. bool deleteDirectoryRecursive(const char* path); } \ No newline at end of file diff --git a/src/settings_backup.cpp b/src/settings_backup.cpp index ff9818a..417100c 100644 --- a/src/settings_backup.cpp +++ b/src/settings_backup.cpp @@ -66,9 +66,9 @@ bool factoryReset() { bool ok; { - // Guard-Block bewusst vor dem Neustart wieder verlassen (statt - // ueber die Funktion hinweg zu halten) - reine Vorsicht, auch wenn - // ESP.restart() den Chip ohnehin sofort zuruecksetzt. +// Deliberately leave the guard block before the restart (instead of + // holding it across the function call) - purely precautionary, even + // though ESP.restart() resets the chip immediately anyway. SdMutex::Guard guard; ok = SdStorage::deleteDirectoryRecursive(Config::SD_ROOT_DIR); } @@ -76,7 +76,7 @@ bool factoryReset() { if (ok) { delay(200); ESP.restart(); - // Wird nie erreicht - ESP.restart() kehrt nicht zurueck. + // Never reached - ESP.restart() does not return. } return ok; } diff --git a/src/settings_backup.h b/src/settings_backup.h index 56017f0..2194076 100644 --- a/src/settings_backup.h +++ b/src/settings_backup.h @@ -2,21 +2,20 @@ #include namespace SettingsBackup { - // onStep (falls angegeben) wird bei jedem der beiden Kopiervorgaenge - // (erst Einstellungen, dann WLAN-Zugangsdaten) direkt VOR dem - // jeweiligen Kopieren aufgerufen - der aufrufende Screen - // (menu_screen.cpp) nutzt das, um waehrend des SD-bedingt spuerbar - // langsamen Sicherns/Wiederherstellens Fortschrittspunkte auf dem - // Button anzuzeigen, statt dass der Button eingefroren wirkt. +// onStep (if provided) is called before each of the two copy operations + // (first settings, then WiFi credentials) - the calling screen + // (menu_screen.cpp) uses this to show progress dots on the button during + // the SD-card-related noticeably slow backup/restore, instead of the + // button appearing frozen. bool backup(void (*onStep)() = nullptr); bool restore(void (*onStep)() = nullptr); bool hasBackup(); - // Loescht den kompletten Flightradar-Ordner von der SD-Karte und - // startet das Geraet neu - beim naechsten Boot laeuft dadurch wieder - // die komplette Ersteinrichtung (siehe main.cpp: isFirstRun-Erkennung - // ueber Config::SD_SETTINGS_FILE). Kehrt nur im Fehlerfall zurueck - // (z.B. SD nicht eingehaengt) - im Erfolgsfall startet das Geraet - // neu, bevor die Funktion "zurueckkehrt" (ESP.restart()). +// Deletes the entire Flightradar folder from the SD card and + // restarts the device - on the next boot, the complete first-time + // setup will run again (see main.cpp: isFirstRun detection via + // Config::SD_SETTINGS_FILE). Only returns on error (e.g. SD not mounted) + // - on success, the device restarts before the function "returns" + // (ESP.restart()). bool factoryReset(); } \ No newline at end of file diff --git a/src/settings_store.cpp b/src/settings_store.cpp index 077a87f..d4d5ddb 100644 --- a/src/settings_store.cpp +++ b/src/settings_store.cpp @@ -9,14 +9,14 @@ namespace SettingsStore { namespace { uint8_t rangeIdx = Config::DEFAULT_RANGE_INDEX; uint8_t altFilterIdx = Config::DEFAULT_ALT_FILTER_INDEX; - bool inverted = true; // Dieses Board braucht invertDisplay(true) fuer korrekte Farben (siehe main.cpp) + bool inverted = true; // This board needs invertDisplay(true) for correct colors (see main.cpp) uint8_t brightnessPct = Config::BRIGHTNESS_MAX_PERCENT; bool emergencyAlertOn = true; bool proximityAlertOn = true; bool watchlistAlertOn = true; - // MUSS bei einer frischen Installation aus sein - sonst schreibt sich - // die SD-Karte unbemerkt voll (siehe Bestaetigungsdialog beim - // Einschalten in menu_screen.cpp + 24h-Auto-Aus in flight_logbook.cpp). +// MUST be off on a fresh installation - otherwise the SD card fills up + // unnoticed (see confirmation dialog at power-on in menu_screen.cpp + // + 24h auto-off in flight_logbook.cpp). bool flightLogbookOn = false; uint32_t logbookEnabledAtEpoch = 0; char logbookSessionFile[16] = {0}; @@ -107,11 +107,10 @@ void load() { } f.close(); - // Sicherheitsregel: das Flugbuch darf nach einem Neustart nur dann - // aktiv bleiben, wenn auch ein gueltiger Einschalt-Zeitstempel - // vorhanden ist - kein Zeitstempel bedeutet garantiert AUS, egal was in - // "flight_logbook" steht (verhindert unbemerktes Weiterlaufen z.B. nach - // einem Firmware-Update oder einer manuell bearbeiteten Datei). +// Safety rule: flight logbook may only stay active after a restart if + // a valid power-on timestamp is present - no timestamp means guaranteed + // OFF, regardless of what is in "flight_logbook" (prevents unnoticed + // continued operation e.g. after a firmware update or manually edited file). if (flightLogbookOn && logbookEnabledAtEpoch == 0) { flightLogbookOn = false; } diff --git a/src/settings_store.h b/src/settings_store.h index ea65833..4a20cfb 100644 --- a/src/settings_store.h +++ b/src/settings_store.h @@ -14,7 +14,7 @@ uint8_t rangeIndex(); bool displayInverted(); void setDisplayInverted(bool inverted); - // Display-Helligkeit in Prozent (Config::BRIGHTNESS_MIN_PERCENT..MAX_PERCENT). + // Display brightness in percent (Config::BRIGHTNESS_MIN_PERCENT..MAX_PERCENT). uint8_t brightnessPercent(); void setBrightnessPercent(uint8_t percent); @@ -30,17 +30,17 @@ uint8_t rangeIndex(); bool flightLogbookEnabled(); void setFlightLogbookEnabled(bool on); - // Unix-Zeitstempel (Sekunden), zu dem das Flugbuch zuletzt eingeschaltet - // wurde. 0 = unbekannt/nicht gesetzt. FlightLogbook::update() nutzt dies, - // um die Aufzeichnung nach genau 24 Stunden automatisch wieder - // auszuschalten (SD-Karten-Schutz, siehe Bestaetigungsdialog im Menue). +// Unix timestamp (seconds) when the flight logbook was last enabled. + // 0 = unknown/not set. FlightLogbook::update() uses this + // to automatically turn off recording after exactly 24 hours + // (SD card protection, see confirmation dialog in the menu). uint32_t flightLogbookEnabledAtEpoch(); void setFlightLogbookEnabledAtEpoch(uint32_t epoch); - // Dateiname (ohne ".csv", z.B. "2026-08-06" oder "2026-08-06_2") der - // aktuell laufenden Flugbuch-Sitzung. "" = keine Sitzungsdatei - // hinterlegt (Flugbuch aus, oder naechste Aktivierung soll eine neue - // Datei anlegen). Siehe FlightLogbook::ensureSessionFile(). +// Filename (without ".csv", e.g. "2026-08-06" or "2026-08-06_2") of the + // currently running flight logbook session. "" = no session file + // stored (logbook off, or next activation should create a new + // file). See FlightLogbook::ensureSessionFile(). String flightLogbookSessionFile(); void setFlightLogbookSessionFile(const String& label); @@ -53,9 +53,9 @@ uint8_t rangeIndex(); bool nightDimmingEnabled(); void setNightDimmingEnabled(bool on); - // Ruhebildschirm bei Inaktivitaets-Timeout (siehe main.cpp) - AUS per - // Default, damit sich am bisherigen Verhalten (Backlight komplett aus) - // nichts aendert, wer es nicht explizit einschaltet. +// Screensaver during inactivity timeout (see main.cpp) - OFF by + // default, so the previous behavior (backlight completely off) + // does not change unless explicitly enabled. bool screensaverEnabled(); void setScreensaverEnabled(bool on); @@ -65,12 +65,12 @@ bool hideGroundVehicles(); bool ecoModeEnabled(); void setEcoModeEnabled(bool on); - // Sprache der Benutzeroberflaeche: 0=EN,1=DE,2=FR,3=TR,4=ES,5=IT. + // UI language: 0=EN,1=DE,2=FR,3=TR,4=ES,5=IT. uint8_t language(); void setLanguage(uint8_t lang); - // Einheiten-Modus: 0=Auto (per IP-Standort geschaetzt), 1=Metrisch - // erzwingen, 2=Imperial (Fuss/Knoten/Meilen) erzwingen. +// Units mode: 0=Auto (estimated by IP location), 1=Metric + // forced, 2=Imperial (feet/knots/miles) forced. uint8_t unitsMode(); void setUnitsMode(uint8_t mode); } \ No newline at end of file diff --git a/src/splash_screen.cpp b/src/splash_screen.cpp index 5095987..d223f3e 100644 --- a/src/splash_screen.cpp +++ b/src/splash_screen.cpp @@ -21,17 +21,17 @@ namespace { tft.drawFastVLine(cx, cy - 80, 160, dim); } - // Dreht einen Punkt (px, py) im lokalen Koordinatensystem der kleinen - // Deko-Symbole um angleDeg und verschiebt ihn nach (x, y). +// Rotates a point (px, py) in the local coordinate system of the small + // decorative symbols by angleDeg and translates it to (x, y). void rotatePoint(int16_t x, int16_t y, float sinA, float cosA, float px, float py, int16_t& outX, int16_t& outY) { outX = x + (int16_t)lroundf(px * cosA - py * sinA); outY = y + (int16_t)lroundf(px * sinA + py * cosA); } - // Kleines Flugzeug-Silhouette aus gefuellten Dreiecken (Rumpf, Tragflaechen, - // Leitwerk), analog zum grossen Flugzeug in der Mitte, aber schlichter und - // in beliebiger Richtung (angleDeg, 0 = Nase zeigt nach oben). +// Small airplane silhouette made of filled triangles (fuselage, wings, + // tail), analogous to the large airplane in the center, but simpler and + // in any direction (angleDeg, 0 = nose pointing up). void drawMiniJet(TFT_eSPI& tft, int16_t x, int16_t y, uint16_t color, float angleDeg) { float rad = angleDeg * (PI / 180.0f); float s = sinf(rad), c = cosf(rad); @@ -55,8 +55,8 @@ namespace { tft.fillTriangle(tlx, tly, trx, try_, ttx, tty, color); } - // Kleiner Helikopter: Rotorkreis mit Blaettern, gefuellter Rumpf, Heckausleger - // mit Leitwerk. angleDeg bestimmt die Flugrichtung (0 = Nase nach oben). +// Small helicopter: rotor circle with blades, filled fuselage, tail boom + // with tail fin. angleDeg determines flight direction (0 = nose up). void drawMiniHeli(TFT_eSPI& tft, int16_t x, int16_t y, uint16_t color, float angleDeg) { float rad = angleDeg * (PI / 180.0f); float s = sinf(rad), c = cosf(rad); diff --git a/src/splash_screen.h b/src/splash_screen.h index c882d82..04bc8b6 100644 --- a/src/splash_screen.h +++ b/src/splash_screen.h @@ -7,8 +7,8 @@ namespace SplashScreen { void setStatusLine(TFT_eSPI& tft, uint8_t slot, const String& text, uint16_t color = TFT_WHITE); - // Blockiert, bis seit begin() mindestens MIN_DISPLAY_MS vergangen sind. - // Direkt vor dem Verlassen des Splash-Screens aufrufen. Zeichnet waehrend - // des Wartens dieselbe Sterne-Animation wie die Menues. +// Blocks until at least MIN_DISPLAY_MS have passed since begin(). + // Call right before leaving the splash screen. Draws the same + // star animation as the menus while waiting. void waitRemaining(TFT_eSPI& tft); } \ No newline at end of file diff --git a/src/stats_history_screen.cpp b/src/stats_history_screen.cpp index b66b54c..7c7ba9f 100644 --- a/src/stats_history_screen.cpp +++ b/src/stats_history_screen.cpp @@ -136,9 +136,9 @@ void run(TFT_eSPI& tft) { tft.print(I18n::t(StringId::LOADING)); FlightLogbook::DayEntry days[MAX_DAYS_QUERIED]; - // listDaySummaries() statt listDays(): mehrere Sitzungs-Dateien am - // selben Kalendertag (z.B. nach erneutem Einschalten) sollen hier - // weiterhin als EIN Balken pro Tag zaehlen statt als mehrere. +// Using listDaySummaries() instead of listDays(): multiple session files + // on the same calendar day (e.g., after restarting) should still count + // as ONE bar per day instead of multiple. uint8_t count = FlightLogbook::listDaySummaries(days, MAX_DAYS_QUERIED); uint8_t barCount = (count > MAX_BARS) ? MAX_BARS : count; diff --git a/src/stats_screen.cpp b/src/stats_screen.cpp index c902350..8cd4d19 100644 --- a/src/stats_screen.cpp +++ b/src/stats_screen.cpp @@ -43,7 +43,7 @@ namespace { constexpr int16_t ROW3_Y = 106; constexpr int16_t ROW4_Y = 140; constexpr int16_t UPTIME_Y = 174; - constexpr int16_t TOP_ALT_Y = 194; // zweizeilig wie drawStatRow (Label + Wert) + constexpr int16_t TOP_ALT_Y = 194; // two lines like drawStatRow (label + value) } void run(TFT_eSPI& tft) { @@ -100,8 +100,8 @@ void run(TFT_eSPI& tft) { if (topAlt.found) { String csign = topAlt.callsign[0] ? String(topAlt.callsign) : "?"; - // Respektiert jetzt die Einheiten-Einstellung (Menue > Einheiten) - // - vorher immer "(XXXX ft)", auch bei Metrisch eingestellt. +// Now respects the units setting (Menu > Units) + // - previously always "(XXXX ft)", even when metric was set. char altBuf[24]; if (LocationManager::useMetricUnits()) { snprintf(altBuf, sizeof(altBuf), "%s (%ldm)", csign.c_str(), (long)Units::feetToMeters((float)topAlt.altitudeFt)); diff --git a/src/stats_screen.h b/src/stats_screen.h index 57861d6..adae472 100644 --- a/src/stats_screen.h +++ b/src/stats_screen.h @@ -3,8 +3,8 @@ #include namespace StatsScreen { - // Blockierend: zeigt einfache Statistiken aus dem Flight Logbook - // (heute gesehen, insgesamt gesehen, Anzahl Tage geloggt). Kehrt zurueck, - // sobald "Back" angetippt wird. +// Blocking: shows simple statistics from the Flight Logbook + // (seen today, total seen, number of days logged). Returns as soon + // as "Back" is tapped. void run(TFT_eSPI& tft); } \ No newline at end of file diff --git a/src/sun_times.cpp b/src/sun_times.cpp index 5de549d..e9fbca8 100644 --- a/src/sun_times.cpp +++ b/src/sun_times.cpp @@ -15,11 +15,10 @@ namespace { return n; } - // Klassischer "Sunrise/Sunset Algorithm" (Almanac for Computers, 1990, - // weit verbreitete Formel). sunrise=true fuer Sonnenaufgang, false fuer - // Sonnenuntergang. Gibt false zurueck, wenn die Sonne an diesem Tag/ - // Breitengrad ueberhaupt nicht auf-/untergeht (Polartag/-nacht) - dann - // ist outHour unveraendert. +// Classic "Sunrise/Sunset Algorithm" (Almanac for Computers, 1990, + // widely used formula). sunrise=true for sunrise, false for sunset. + // Returns false if the sun does not rise/set at all on this day/latitude + // (polar day/night) - then outHour is unchanged. bool calc(double lat, double lon, int N, int32_t utcOffsetSeconds, bool sunrise, float& outHour) { double lngHour = lon / 15.0; double t = sunrise ? (N + ((6.0 - lngHour) / 24.0)) : (N + ((18.0 - lngHour) / 24.0)); @@ -34,9 +33,9 @@ namespace { RA = fmod(RA, 360.0); if (RA < 0) RA += 360.0; - // RA muss im selben 90-Grad-Quadranten wie L liegen (atan() liefert - // nur Werte im Bereich -90..90 Grad, muss also entsprechend - // "zurueckgeklappt" werden). +// RA must be in the same 90-degree quadrant as L (atan() only returns + // values in the range -90..90 degrees, so it must be "folded back" + // accordingly). double lQuadrant = floor(L / 90.0) * 90.0; double raQuadrant = floor(RA / 90.0) * 90.0; RA = RA + (lQuadrant - raQuadrant); @@ -45,10 +44,10 @@ namespace { double sinDec = 0.39782 * sin(L * DEG2RAD); double cosDec = cos(asin(sinDec)); - constexpr double ZENITH = 90.833; // offizieller Zenit inkl. atm. Refraktion + constexpr double ZENITH = 90.833; // official zenith including atmospheric refraction double cosH = (cos(ZENITH * DEG2RAD) - (sinDec * sin(lat * DEG2RAD))) / (cosDec * cos(lat * DEG2RAD)); - if (cosH > 1.0 || cosH < -1.0) return false; // Polartag/-nacht + if (cosH > 1.0 || cosH < -1.0) return false; // polar day/night double H = sunrise ? (360.0 - RAD2DEG * acos(cosH)) : (RAD2DEG * acos(cosH)); H = H / 15.0; @@ -80,12 +79,12 @@ Result compute(double lat, double lon, int year, int month, int day, int32_t utc bool setOk = calc(lat, lon, N, utcOffsetSeconds, false, sunset); if (!riseOk || !setOk) { - // Polartag (Sonne geht nicht unter) vs. Polarnacht (Sonne geht nicht - // auf): grobe Naeherung ueber "hohe Sonne im Sommerhalbjahr der - // jeweiligen Hemisphaere = Polartag, sonst Polarnacht" - reicht fuer - // eine Nachtdimmung an den seltenen Orten/Tagen, an denen das - // ueberhaupt vorkommt, voellig aus. - bool northernSummerHalf = (N > 79 && N < 265); // grob Fruehlings-/Herbst-Tagundnachtgleiche +// Polar day (sun does not set) vs. polar night (sun does not rise): + // rough approximation via "high sun in the summer half of the + // respective hemisphere = polar day, otherwise polar night" - + // completely sufficient for night dimming on the rare locations/days + // where this occurs at all. + bool northernSummerHalf = (N > 79 && N < 265); // roughly spring/autumn equinox bool polarDay = (lat > 0) == northernSummerHalf; r.valid = true; r.alwaysDay = polarDay; diff --git a/src/sun_times.h b/src/sun_times.h index 9f5d159..6d708e6 100644 --- a/src/sun_times.h +++ b/src/sun_times.h @@ -1,24 +1,24 @@ #pragma once #include -// Berechnet Sonnenauf-/untergang (lokale Zeit) fuer einen gegebenen Ort und -// Tag - Grundlage fuer die standortbasierte Nachtdimmung (siehe main.cpp:: -// isNightDimHours()), die das vorherige feste 22:00-06:00-Fenster ersetzt. +// Calculates sunrise/sunset (local time) for a given location and +// day - basis for the location-based night dimming (see main.cpp:: +// isNightDimHours()), which replaces the previous fixed 22:00-06:00 window. namespace SunTimes { struct Result { - bool valid = false; // false = Standort noch unbekannt (lat/lon == 0,0) - bool alwaysDay = false; // Polartag: Sonne geht heute an diesem Ort nicht unter - bool alwaysNight = false; // Polarnacht: Sonne geht heute an diesem Ort nicht auf - float sunriseHour = 0; // Lokale Dezimalstunde (0-24), nur gueltig wenn weder - float sunsetHour = 0; // alwaysDay noch alwaysNight gesetzt ist. +bool valid = false; // false = location still unknown (lat/lon == 0,0) + bool alwaysDay = false; // polar day: sun does not set today at this location + bool alwaysNight = false; // polar night: sun does not rise today at this location + float sunriseHour = 0; // local decimal hour (0-24), only valid if neither + float sunsetHour = 0; // alwaysDay nor alwaysNight is set. }; - // year/month/day sind lokale Kalenderdatumswerte (z.B. aus localtime_r()), - // utcOffsetSeconds die bekannte Zeitzonenverschiebung (siehe - // LocationManager::utcOffsetSeconds()). Nutzt den klassischen "Almanac"- - // Sonnenauf-/untergangsalgorithmus (Sonnenzenit 90.833 Grad, beruecksichtigt - // atmosphaerische Refraktion) - Genauigkeit liegt typischerweise im Bereich - // weniger Minuten, fuer eine sanfte Backlight-Dimmung mehr als ausreichend. +// year/month/day are local calendar date values (e.g. from localtime_r()), + // utcOffsetSeconds is the known timezone offset (see + // LocationManager::utcOffsetSeconds()). Uses the classic "Almanac" + // sunrise/sunset algorithm (solar zenith 90.833 degrees, accounting for + // atmospheric refraction) - accuracy is typically within a few minutes, + // more than sufficient for smooth backlight dimming. Result compute(double lat, double lon, int year, int month, int day, int32_t utcOffsetSeconds); } diff --git a/src/timeout_screen.cpp b/src/timeout_screen.cpp index 43aed45..93eafbd 100644 --- a/src/timeout_screen.cpp +++ b/src/timeout_screen.cpp @@ -26,11 +26,11 @@ namespace { String onOff(bool on) { return I18n::t(on ? StringId::ON : StringId::OFF); } - // Einfacher Zeilenumbruch-Helfer, gleiches Prinzip wie in - // menu_screen.cpp (dort mit zusaetzlicher Scroll-Unterstuetzung, hier - // bewusst ohne - der Beschreibungstext ist kurz genug, dass Scrollen - // nicht noetig ist). draw=false liefert nur die Gesamthoehe, ohne etwas - // zu zeichnen (fuer die vorab-Platzberechnung des Zurueck-Buttons). +// Simple line wrap helper, same principle as in + // menu_screen.cpp (with additional scroll support there, here + // deliberately without - the description text is short enough that scrolling + // is not needed). draw=false returns only the total height, without drawing + // anything (for the advance space calculation of the Back button). int16_t layoutWrapped(TFT_eSPI& tft, int16_t x, int16_t startY, int16_t maxWidth, int16_t lineHeight, const String& text, bool draw) { int16_t y = startY; @@ -57,10 +57,10 @@ namespace { return (int16_t)(y - startY); } - // Schieberegler-Positionen: 1..Config::SCREEN_TIMEOUT_MAX_MINUTES - // Minuten (je eine Position pro Minute) plus eine zusaetzliche - // Endposition ganz rechts fuer "Nie" (kein Timeout) - intern weiterhin - // als 0 gespeichert, wie schon vor diesem Screen. +// Slider positions: 1..Config::SCREEN_TIMEOUT_MAX_MINUTES + // minutes (one position per minute) plus one additional + // end position at the far right for "Never" (no timeout) - internally stored + // as 0, as before this screen. constexpr uint8_t STEP_COUNT = Config::SCREEN_TIMEOUT_MAX_MINUTES + 1; uint8_t indexFromMinutes(uint8_t minutes) { @@ -91,18 +91,18 @@ void run(TFT_eSPI& tft) { constexpr int16_t TRACK_W = Config::SCREEN_WIDTH - 2 * TRACK_X; constexpr int16_t TRACK_H = 6; constexpr int16_t THUMB_R = 12; - // Grosszuegige vertikale Trefferzone rund um den duennen Regler-Strich - - // sonst muesste man beim Ziehen extrem praezise genau auf die paar - // Pixel der Linie treffen. +// Generous vertical hit zone around the thin slider track - + // otherwise you would have to hit the few pixels of the line + // extremely precisely when dragging. constexpr int16_t TRACK_HIT_Y_MIN = TRACK_Y - 24; constexpr int16_t TRACK_HIT_Y_MAX = TRACK_Y + 24; Rect screensaverBtn = {10, 135, (int16_t)(Config::SCREEN_WIDTH - 20), 36}; - // Beschreibungstext-Hoehe (haengt von der jeweils eingestellten Sprache - // ab) einmalig vorab messen, damit der Zurueck-Button IMMER direkt - // darunter landet - nie mit dem Text ueberlappend und nie unnoetig viel - // Leerraum lassend. Gleiches Prinzip wie bei confirmWarningScreen()/ +// Description text height (depends on the selected language) + // measured once in advance, so the Back button ALWAYS lands directly + // below it - never overlapping the text and never leaving unnecessarily + // much empty space. Same principle as confirmWarningScreen()/ // infoScreen() in menu_screen.cpp. constexpr int16_t DESC_Y = 185; constexpr int16_t DESC_LINE_H = 14; @@ -110,9 +110,9 @@ void run(TFT_eSPI& tft) { int16_t descH = layoutWrapped(tft, 10, DESC_Y, DESC_MAX_WIDTH, DESC_LINE_H, I18n::t(StringId::TIMEOUT_SCREENSAVER_DESC), false); int16_t backY = (int16_t)(DESC_Y + descH + 10); - // Sicherheitsnetz, falls eine Uebersetzung doch mal laenger ausfaellt, - // als der verfuegbare Platz hergibt - der Zurueck-Button bleibt so - // IMMER erreichbar, statt vom Bildschirmrand abgeschnitten zu werden. +// Safety net, if a translation ever turns out longer + // than the available space allows - the Back button stays + // ALWAYS reachable, instead of being cut off at the screen edge. constexpr int16_t BACK_Y_MAX = Config::SCREEN_HEIGHT - 46; if (backY > BACK_Y_MAX) backY = BACK_Y_MAX; Rect backBtn = {10, backY, (int16_t)(Config::SCREEN_WIDTH - 20), 36}; @@ -174,19 +174,19 @@ void run(TFT_eSPI& tft) { } dragging = true; } else if (dragging && !live.touched) { - // Loslassen: jetzt erst dauerhaft speichern (nicht bei jeder - // Zwischenposition waehrend des Ziehens selbst, um unnoetig - // viele SD-Kartenschreibvorgaenge zu vermeiden). +// Release: now save persistently (not during every + // intermediate slider position while dragging, to avoid unnecessary + // SD card writes). SettingsStore::setScreenTimeoutMinutes(minutesFromIndex(index)); dragging = false; } - // wasTapped() IMMER aufrufen (auch waehrend des Ziehens), damit ihr - // interner Loslassen-Erkennungszustand synchron bleibt - nur die - // AUSWERTUNG des Ergebnisses wird waehrend des Ziehens unterdrueckt. - // Wuerde man den Aufruf selbst waehrend des Ziehens auslassen, - // koennte beim Loslassen faelschlich ein "Tap" mit einer veralteten - // Position (von VOR Beginn des Ziehens) gemeldet werden. +// ALWAYS call wasTapped() (even while dragging), so its + // internal release detection state stays in sync - only the + // EVALUATION of the result is suppressed while dragging. + // If the call itself were omitted while dragging, + // releasing could incorrectly report a "Tap" with a stale + // position (from BEFORE the drag started). TouchInput::Point tap; bool tapped = TouchInput::wasTapped(tap); if (!dragging && tapped) { diff --git a/src/timeout_screen.h b/src/timeout_screen.h index 8e6a0a9..9e67ebb 100644 --- a/src/timeout_screen.h +++ b/src/timeout_screen.h @@ -2,12 +2,12 @@ #include #include -// Bildschirm-Timeout einstellen (Menue > System > Bildschirm-Timeout) - -// Schieberegler von Config::SCREEN_TIMEOUT_MIN_MINUTES bis -// Config::SCREEN_TIMEOUT_MAX_MINUTES, danach "Nie" als eigene Endposition. -// Der Ruhebildschirm-Umschalter (siehe SettingsStore::screensaverEnabled()) -// lebt ebenfalls hier, direkt unter dem Regler, da er inhaltlich eng mit -// dem Timeout zusammenhaengt (bestimmt nur, WAS beim Timeout passiert). +// Screen timeout setting (Menu > System > Screen Timeout) - +// Slider from Config::SCREEN_TIMEOUT_MIN_MINUTES to +// Config::SCREEN_TIMEOUT_MAX_MINUTES, then "Never" as a separate end position. +// The screensaver toggle (see SettingsStore::screensaverEnabled()) +// also lives here, directly below the slider, since it is closely related to +// the timeout (only determines WHAT happens on timeout). namespace TimeoutScreen { void run(TFT_eSPI& tft); } diff --git a/src/touch_input.cpp b/src/touch_input.cpp index 94c4b27..b83047b 100644 --- a/src/touch_input.cpp +++ b/src/touch_input.cpp @@ -10,11 +10,11 @@ namespace { SPIClass touchSpi(VSPI); XPT2046_Touchscreen touch(Config::TOUCH_CS_PIN, Config::TOUCH_IRQ_PIN); - constexpr int16_t MARGIN_PX = 24; // Abstand der Kalibrierungspunkte vom Rand + constexpr int16_t MARGIN_PX = 24; // Distance of calibration points from the edge - // Rohe Min/Max-Werte aus der Kalibrierung. Bis zur ersten Kalibrierung - // grosszuegige Standardwerte, damit die Kalibrierungs-Routine selbst schon - // grob nutzbare (wenn auch ungenaue) Koordinaten bekommt. +// Raw min/max values from calibration. Until first calibration, + // generous default values so the calibration routine itself already gets + // roughly usable (though inaccurate) coordinates. int16_t calXmin = 200; int16_t calXmax = 3900; int16_t calYmin = 200; @@ -116,7 +116,7 @@ bool wasTapped(Point& outPoint) { bool fired = false; if (!cur.touched && lastTouched) { - // Loslassen erkannt -> Tap an der zuletzt bekannten Position melden. + // Release detected -> report tap at last known position. outPoint = lastRaw; fired = true; } diff --git a/src/touch_input.h b/src/touch_input.h index a599e96..d52a7fe 100644 --- a/src/touch_input.h +++ b/src/touch_input.h @@ -11,22 +11,22 @@ namespace TouchInput { void begin(); - // Laedt calibration.txt von der SD-Karte. Gibt false zurueck, wenn keine - // vorhanden oder ungueltig ist. +// Loads calibration.txt from the SD card. Returns false if none exists + // or it is invalid. bool loadCalibration(); void setCalibration(int16_t rawXmin, int16_t rawXmax, int16_t rawYmin, int16_t rawYmax); void saveCalibration(); bool hasCalibration(); - // Aktueller Touch-Zustand, roh (unkalibriert), z.B. fuer die - // Kalibrierungs-Routine selbst. +// Current touch state, raw (uncalibrated), e.g. for the calibration + // routine itself. Point rawPoint(); - // Aktueller Touch-Zustand, auf Bildschirmkoordinaten (0..SCREEN_WIDTH-1 / - // 0..SCREEN_HEIGHT-1) umgerechnet und geclampt. +// Current touch state, converted to screen coordinates (0..SCREEN_WIDTH-1 / + // 0..SCREEN_HEIGHT-1) and clamped. Point mappedPoint(); - // Liefert genau einmal pro physischer Beruehrung "true" (beim Loslassen), - // mitsamt der zuletzt bekannten Position. Fuer Buttons/Tastatur gedacht. +// Returns "true" exactly once per physical touch (on release), along with + // the last known position. Intended for buttons/keyboard use. bool wasTapped(Point& outPoint); } \ No newline at end of file diff --git a/src/ui_font.h b/src/ui_font.h index 095c517..53aec62 100644 --- a/src/ui_font.h +++ b/src/ui_font.h @@ -1,7 +1,7 @@ #pragma once -// Automatisch generierter Font (DejaVu Sans Mono, 11px) fuer TFT_eSPI GFXFF. -// Deckt Basis-ASCII + Umlaute/Akzente fuer DE/TR/FR/ES/IT ab (U+0020-U+015F). -// NICHT von Hand bearbeiten - siehe Kommentar in i18n.h fuer den Generator-Hinweis. +// Auto-generated font (DejaVu Sans Mono, 11px) for TFT_eSPI GFXFF. +// Covers basic ASCII + umlauts/accents for DE/TR/FR/ES/IT (U+0020-U+015F). +// DO NOT edit manually - see comment in i18n.h for the generator hint. #include const uint8_t UiFont11ptBitmaps[] PROGMEM = { diff --git a/src/weather.cpp b/src/weather.cpp index bf25265..85692a6 100644 --- a/src/weather.cpp +++ b/src/weather.cpp @@ -19,9 +19,9 @@ namespace { double lastLon = 0; bool hasLastLocation = false; - // Ordnet den WMO-Wettercode von Open-Meteo (Feld "weathercode", siehe - // https://open-meteo.com/en/docs) einer der wenigen Icon-Kategorien zu, - // die main.cpp zeichnen kann. +// Maps the WMO weather code from Open-Meteo (field "weathercode", see + // https://open-meteo.com/en/docs) to one of the few icon categories + // that main.cpp can draw. Condition conditionFromWmoCode(int code) { if (code == 0) return Condition::Clear; if (code == 1 || code == 2) return Condition::PartlyCloudy; @@ -56,10 +56,10 @@ namespace { return; } - // Body erst komplett als String einsammeln (getString() kuemmert - // sich zuverlaessig um Chunked-Transfer-Encoding), statt direkt aus - // http.getStream() zu parsen - Letzteres scheiterte bei Open-Meteo - // zuverlaessig mit einem ArduinoJson-"InvalidInput"-Fehler. +// Collect body as a complete string first (getString() reliably handles + // chunked transfer encoding) instead of parsing directly from + // http.getStream() - the latter reliably failed with Open-Meteo + // with an ArduinoJson "InvalidInput" error. String body = http.getString(); http.end(); @@ -79,10 +79,9 @@ void update() { LocationManager::getHomeLocation(lat, lon); if (lat == 0 && lon == 0) return; - // Deutliche Standort-Aenderung (z.B. anderes Standort-Preset aktiviert) - // - sofort neu abfragen statt bis zum naechsten regulaeren Intervall zu - // warten, damit das Icon nicht minutenlang das Wetter des alten - // Standorts zeigt. +// Significant location change (e.g., another location preset activated) + // - query immediately instead of waiting for the next regular interval, + // so the icon does not show the old location's weather for minutes. bool locationChanged = !hasLastLocation || fabs(lat - lastLat) > 0.01 || fabs(lon - lastLon) > 0.01; diff --git a/src/weather.h b/src/weather.h index 453056e..5c4e4c2 100644 --- a/src/weather.h +++ b/src/weather.h @@ -1,15 +1,15 @@ #pragma once #include -// Sehr einfache Wetteranzeige (Icon im Header, dort wo frueher der -// Cam-Button war) - fragt periodisch die aktuelle Wetterlage fuer den -// gerade aktiven Standort ab (also inkl. aktivem Standort-Preset, siehe -// LocationManager::getHomeLocation() - wenn dort z.B. Mailand ausgewaehlt -// ist, zeigt das Icon das Wetter in Mailand). Nutzt die kostenlose -// Open-Meteo-API (kein API-Key noetig). +// Very simple weather display (icon in the header, where the camera button +// used to be) - periodically queries the current weather for the currently +// active location (including active location preset, see +// LocationManager::getHomeLocation() - if e.g. Milan is selected there, +// the icon shows the weather in Milan). Uses the free Open-Meteo API (no +// API key needed). namespace Weather { enum class Condition { - Unknown, // noch keine erfolgreiche Abfrage - Icon zeigt nichts an + Unknown, // no successful query yet - icon shows nothing Clear, PartlyCloudy, Cloudy, @@ -18,16 +18,14 @@ namespace Weather { Thunderstorm }; - // Muss regelmaessig aus dem NetTask (Core 0) aufgerufen werden - kuemmert - // sich intern um das Abfrage-Intervall (Config::WEATHER_FETCH_INTERVAL_MS) - // und erkennt eine Standort-Aenderung (z.B. anderes aktives Preset), um - // dann sofort neu abzufragen statt bis zum naechsten reguleren Intervall - // zu warten. +// Must be called regularly from NetTask (Core 0) - handles the query + // interval internally (Config::WEATHER_FETCH_INTERVAL_MS) and detects + // location changes (e.g. different active preset) to immediately re-query + // instead of waiting for the next regular interval. void update(); - // Aktuell bekannte Wetterlage, threadsicher genug fuer diesen Zweck (ein - // einzelnes uint8-artiges Enum, das nur vom NetTask geschrieben und vom - // UI-Thread gelesen wird - kein Lock noetig, ein kurzfristig veraltetes - // Lesen ist unkritisch). +// Currently known weather condition, thread-safe enough for this purpose (a + // single uint8-like enum, only written by NetTask and read by the UI thread + // - no lock needed, a briefly stale read is harmless). Condition current(); } diff --git a/src/web_export_server.cpp b/src/web_export_server.cpp index 64947b3..887bd9b 100644 --- a/src/web_export_server.cpp +++ b/src/web_export_server.cpp @@ -19,9 +19,9 @@ namespace { WebServer server(80); - // Nur reine Dateinamen/Labels aus Formularfeldern akzeptieren - kein - // "/" und kein ".." - damit ueber die WebUI kein Ausbruch aus dem - // jeweiligen SD-Verzeichnis moeglich ist (Pfad-Traversal). +// Only accept plain filenames/labels from form fields - no + // "/" and no ".." - so that no escape from the respective + // SD directory is possible via the WebUI (path traversal). bool isSafeName(const String& name) { if (name.length() == 0 || name.length() > 40) return false; if (name.indexOf('/') >= 0 || name.indexOf('\\') >= 0) return false; @@ -29,14 +29,14 @@ namespace { return true; } - // Absichtliche Duplikate der gleichnamigen (lokalen/statischen) Funktionen - // aus radar_screen.cpp - dort nicht exportiert, und nach der im Projekt - // etablierten Konvention "jeder Screen/jedes Modul dupliziert seine - // eigenen kleinen Helfer statt eines gemeinsamen Moduls" (siehe z.B. - // timeout_screen.cpp) bewusst hier erneut definiert statt radar_screen.cpp - // umzubauen. Bei Aenderungen an der Logik in radar_screen.cpp bitte diese - // Kopie hier synchron halten, damit das WebUI-Radar dieselben Ringe/ - // Markierungen zeigt wie das Geraete-Display. +// Intentional duplicates of the same-named (local/static) functions + // from radar_screen.cpp - not exported there, and following the project's + // established convention "each screen/module duplicates its own small + // helpers instead of a shared module" (see e.g. timeout_screen.cpp), + // deliberately redefined here instead of refactoring radar_screen.cpp. + // If the logic in radar_screen.cpp changes, please keep this copy + // in sync so that the WebUI radar shows the same rings/markers as the + // device display. bool isEmergencySquawkWeb(const char* squawk) { if (!squawk[0]) return false; for (uint8_t i = 0; i < Config::EMERGENCY_SQUAWK_COUNT; i++) { @@ -45,15 +45,15 @@ namespace { return false; } - // "Auffaellig" (oranger Ring) beschraenkt sich aktuell auf gar nichts - - // der Teil ueber Militaer-/Regierungs-Rufzeichen-Praefixe - // (Config::NOTABLE_CALLSIGN_PREFIXES) ist NICHT umgesetzt, weil diese - // Konstante nirgends im Projekt existiert (auch nicht in - // radar_screen.cpp) - das zugehoerige Feature wurde bisher nie - // tatsaechlich spezifiziert/implementiert. "notable" wird deshalb unten - // in handleRadarJson() fest auf false gesetzt. Sobald es eine echte - // Praefixliste gibt, hier eine isNotableCallsignWeb()-Funktion analog zu - // isEmergencySquawkWeb() ergaenzen. +// "Notable" (orange ring) is currently limited to nothing at all - + // the part about military/government callsign prefixes + // (Config::NOTABLE_CALLSIGN_PREFIXES) is NOT implemented, because this + // constant does not exist anywhere in the project (not even in + // radar_screen.cpp) - the associated feature has never actually been + // specified/implemented. "notable" is therefore hardcoded to false + // below in handleRadarJson(). As soon as there is a real prefix list, + // add an isNotableCallsignWeb() function analogous to + // isEmergencySquawkWeb() here. bool isHeavyCategoryWeb(const char* category) { return category[0] == 'A' && category[1] == '5'; } @@ -73,10 +73,9 @@ namespace { html += "button{background:#0a0f0d;color:#ff3b3b;border:1px solid #ff3b3b;border-radius:4px;padding:3px 10px;font-family:inherit;cursor:pointer;}"; html += "button:hover{background:#ff3b3b;color:#0a0f0d;}"; html += ".dl{color:#39ff14;text-decoration:none;border:1px solid #39ff14;border-radius:4px;padding:3px 10px;margin-right:6px;display:inline-block;}"; - // Gruen statt Rot fuer "Hinzufuegen"-Buttons - die roten button{}-Regeln - // oben bleiben fuer alle destruktiven "Entfernen/Loeschen"-Buttons - // unveraendert, .addbtn ist ausschliesslich fuer die neuen Listen- - // Formulare (siehe handleLists()) gedacht. +// Green instead of red for "Add" buttons - the red button{} rules + // above stay unchanged for all destructive "Remove/Delete" buttons. + // .addbtn is exclusively for the new list forms (see handleLists()). html += ".addbtn{background:#0a0f0d;color:#39ff14;border:1px solid #39ff14;border-radius:4px;padding:3px 10px;font-family:inherit;cursor:pointer;}"; html += ".addbtn:hover{background:#39ff14;color:#0a0f0d;}"; html += "input[type=text]{background:#0a0f0d;color:#39ff14;border:1px solid #39ff14;border-radius:4px;padding:5px 8px;font-family:inherit;}"; @@ -92,19 +91,19 @@ namespace { return html; } - // Live-Radar-Ansicht fuer die Startseite: ein , das per JavaScript - // alle paar Sekunden /radar.json abruft und die Flugzeuge polar (Peilung/ - // Distanz, genau wie auf dem Geraete-Display) zeichnet. Bewusst per - // fetch()-Polling statt WebSocket/SSE gehalten - deutlich weniger Code - // und Speicherbedarf auf dem ESP32, und fuer eine gelegentlich vom Handy - // aus aufgerufene Seite voellig ausreichend. +// Live radar view for the start page: a that uses JavaScript + // to fetch /radar.json every few seconds and draw the aircraft polar + // (bearing/distance, exactly like on the device display). Intentionally + // kept with fetch()-polling instead of WebSocket/SSE - significantly less + // code and memory usage on the ESP32, and perfectly adequate for a page + // occasionally opened from a phone. // - // Der Reichweiten-Waehler () is purely client-side/per page view - + // it does NOT change the device setting (SettingsStore:: + // rangeIndex()), but is sent as a "range_km" query parameter to + // /radar.json (see handleRadarJson()) and allows independent zooming + // in/out on the phone without affecting the device display. Default is + // the current device range. void appendRadarSection(String& html) { float deviceRangeKm = Config::RANGE_STEPS_KM[SettingsStore::rangeIndex()]; @@ -131,10 +130,10 @@ namespace { html += "var markers=[];"; html += "var selectedHex=null;"; - // Hintergrund-Sterne AUSSERHALB des Radarkreises - gleiches Prinzip - // wie updateBgStars()/initBgStarsIfNeeded() in radar_screen.cpp - // (Rejection-Sampling, damit kein Stern innerhalb des Kreises - // landet), hier per Canvas/JS statt TFT_eSPI nachgebaut. +// Background stars OUTSIDE the radar circle - same principle + // as updateBgStars()/initBgStarsIfNeeded() in radar_screen.cpp + // (rejection sampling, so no star lands inside the circle), + // replicated here via Canvas/JS instead of TFT_eSPI. html += "var stars=[];"; html += "(function(){var minDistSq=(R+6)*(R+6);for(var i=0;i<24;i++){var x,y,tries=0;"; html += "do{x=4+Math.random()*(W-8);y=4+Math.random()*(H-8);tries++;}"; @@ -155,9 +154,9 @@ namespace { html += "for(var ring=1;ring<=3;ring++){var r=R*ring/3;ctx.beginPath();ctx.arc(cx,cy,r,0,Math.PI*2);ctx.stroke();"; html += "ctx.fillText(Math.round(data.range_km*ring/3)+' km',cx+4,cy-r+10);}"; html += "ctx.strokeStyle='#12261a';ctx.beginPath();ctx.moveTo(cx-R,cy);ctx.lineTo(cx+R,cy);ctx.moveTo(cx,cy-R);ctx.lineTo(cx,cy+R);ctx.stroke();"; - // Alle vier Himmelsrichtungen (N/E/S/W), genau wie - // drawStaticBackground() in radar_screen.cpp - vorher stand hier nur - // "N", was auf Nachfrage ergaenzt wurde. +// All four cardinal directions (N/E/S/W), exactly like + // drawStaticBackground() in radar_screen.cpp - previously only + // "N" was shown here, which was expanded upon request. html += "ctx.fillStyle='#39ff14';ctx.textAlign='center';ctx.fillText('N',cx,cy-R-8);"; html += "ctx.fillText('S',cx,cy+R+16);"; html += "ctx.textAlign='left';ctx.fillText('E',cx+R+4,cy+3);"; @@ -180,26 +179,26 @@ namespace { html += "if(a.emergency){ctx.strokeStyle='#ff3b3b';ctx.beginPath();ctx.arc(x,y,9,0,Math.PI*2);ctx.stroke();}"; html += "else if(a.watched){ctx.strokeStyle='#00e5ff';ctx.beginPath();ctx.arc(x,y,9,0,Math.PI*2);ctx.stroke();}"; html += "else if(a.notable){ctx.strokeStyle='#ff9f1a';ctx.beginPath();ctx.arc(x,y,9,0,Math.PI*2);ctx.stroke();}"; - // Ausgewaehltes Flugzeug (per Klick/Tap, siehe unten) bekommt einen - // weissen Auswahlring, gleiches Prinzip wie isSelected auf dem - // Geraete-Display (radar_screen.cpp render()). +// Selected aircraft (by click/tap, see below) gets a + // white selection ring, same principle as isSelected on the + // device display (radar_screen.cpp render()). html += "if(a.hex===selectedHex){ctx.strokeStyle='#ffffff';ctx.beginPath();ctx.arc(x,y,11,0,Math.PI*2);ctx.stroke();}"; html += "ctx.fillStyle=color;ctx.textAlign='center';ctx.fillText(a.callsign,x,y-8);"; html += "markers.push({x:x,y:y,a:a});"; html += "});"; html += "status.textContent=(data.aircraft||[]).length+' aircraft \\u00b7 range '+data.range_km+' km';"; - // Falls das ausgewaehlte Flugzeug in dieser Aktualisierung nicht - // mehr vorkommt (z.B. aus der Reichweite geflogen), Infobox wieder - // schliessen statt veraltete Daten stehen zu lassen. +// If the selected aircraft no longer appears in this update + // (e.g. flew out of range), close the info box instead of + // showing stale data. html += "if(selectedHex){var found=markers.filter(function(m){return m.a.hex===selectedHex;})[0];"; html += "if(found){showInfo(found.a);}else{selectedHex=null;hideInfo();}}"; html += "}"; - // Info-Panel fuer ein angetipptes Flugzeug - bewusst eine eigene, - // stehenbleibende Box (kein Tooltip/Popup, das beim naechsten - // Neuzeichnen einfach verschwindet), mit explizitem Schliessen-Link, - // gleiches Grundprinzip wie infoScreen() am Geraet: der Nutzer soll - // aktiv entscheiden, wann die Info wieder verschwindet. +// Info panel for a tapped aircraft - intentionally a dedicated, + // persistent box (not a tooltip/popup that disappears on the next + // redraw), with an explicit close link, same basic principle as + // infoScreen() on the device: the user should actively decide when + // the info disappears. html += "function fmtNum(n,d){return (typeof n==='number')?n.toFixed(d):'?';}"; html += "function showInfo(a){"; html += "var lines=[];"; @@ -233,11 +232,11 @@ namespace { html += "}"; html += "poll();"; html += "setInterval(poll,3000);"; - // Schnellerer, rein lokaler Redraw-Takt (alle 150ms, ohne Netzwerk- - // Anfrage) nur fuer das Sternenfunkeln + die Auswahlmarkierung - - // gleiches Grundprinzip wie tick() vs. render() auf dem - // Geraete-Display: Flugzeugpositionen aktualisieren sich weiterhin - // nur alle 3s per poll(), die Sterne twinkeln aber fluessig dazwischen. +// Faster, purely local redraw interval (every 150ms, without network + // request) only for star twinkling + the selection marker - + // same basic principle as tick() vs. render() on the + // device display: aircraft positions still update + // only every 3s via poll(), but stars twinkle smoothly in between. html += "setInterval(function(){draw(lastData);},150);"; html += "})();"; } @@ -297,12 +296,12 @@ namespace { if (line.length() == 0) continue; server.sendContent(String(days[i].date) + "," + line + "\n"); - // Wichtig: Ohne regelmaessiges Abgeben der CPU haengt dieser - // Task (Core 0, Prioritaet 1) die Idle-Task aus, die den - // Task-Watchdog fuettert. Bei groesseren Logbuechern fuehrt - // das nach ca. 5s ohne Yield zu einem Watchdog-Reset - - // genau der Reboot mitten im CSV-Download, den der Nutzer - // beobachtet hat. delay(1) erzwingt einen Kontextwechsel. +// Important: Without regularly yielding the CPU, this + // task (Core 0, priority 1) starves the idle task that feeds + // the task watchdog. With larger logbooks, this causes a + // watchdog reset after approx. 5s without yield - + // exactly the reboot during CSV download that the user + // observed. delay(1) forces a context switch. if (++linesSent % 10 == 0) { delay(1); } @@ -348,14 +347,14 @@ namespace { server.send(303); } - // Verwaltung von Airline-Filter und Beobachtungsliste per Browser - - // beide Backend-Module (AirlineFilter/AircraftWatchlist) sind seit - // dieser Erweiterung mutex-geschuetzt (siehe dort), da sie jetzt sowohl - // von Core 1 (Radar-/Menue-Screens) als auch von hier aus - Core 0, - // WebExportServer laeuft innerhalb von NetTask - aufgerufen werden. - // Praktisch vor allem fuer laengere Eingaben (Rufzeichen, ICAO-Codes), - // die sich per Handy-Tastatur deutlich bequemer eintippen lassen als - // ueber die kleine Bildschirmtastatur des Geraets. +// Management of airline filter and watchlist via browser - + // both backend modules (AirlineFilter/AircraftWatchlist) are now + // mutex-protected (see there), since they are called both + // from Core 1 (radar/menu screens) and from here - Core 0, + // WebExportServer runs within NetTask. + // Practically most useful for longer inputs (callsigns, ICAO codes) + // that are much easier to type on a phone keyboard than + // via the device's small on-screen keyboard. void handleLists() { String html = htmlHeader("Leos Flightradar - Lists"); @@ -449,19 +448,18 @@ namespace { server.send(303); } - // Datenquelle fuer das Live-Radar auf der Startseite (siehe - // appendRadarSection()). Wendet dieselben Filter/Prioritaeten an wie das - // Geraete-Display (render() in radar_screen.cpp): Reichweite, "Boden- - // fahrzeuge ausblenden", Airline-Filter, sowie Notfall/Beobachtungsliste/ - // "auffaellig" als sich gegenseitig ausschliessende Ring-Markierungen in - // genau dieser Prioritaet. +// Data source for the live radar on the start page (see + // appendRadarSection()). Applies the same filters/priorities as the + // device display (render() in radar_screen.cpp): range, "hide ground + // vehicles", airline filter, and emergency/watchlist/"notable" as + // mutually exclusive ring markers in exactly this priority. void handleRadarJson() { float rangeKm = Config::RANGE_STEPS_KM[SettingsStore::rangeIndex()]; - // Erlaubt der Web-Ansicht ein eigenes, unabhaengiges Zoomen (siehe - // Reichweiten-Waehler in appendRadarSection()), OHNE die Geraete- - // Einstellung zu veraendern. Nur einen der bekannten - // Config::RANGE_STEPS_KM-Werte akzeptieren - ein fehlender oder - // nicht erkannter Parameter faellt auf die Geraete-Reichweite zurueck. +// Allows the web view its own independent zooming (see + // range selector in appendRadarSection()), WITHOUT changing the + // device setting. Only accept one of the known + // Config::RANGE_STEPS_KM values - a missing or + // unknown parameter falls back to the device range. if (server.hasArg("range_km")) { float requested = server.arg("range_km").toFloat(); for (uint8_t i = 0; i < Config::RANGE_STEP_COUNT; i++) { @@ -494,13 +492,13 @@ namespace { bool isHeavy = isHeavyCategoryWeb(a.category); bool isEmergency = emergencyOn && isEmergencySquawkWeb(a.squawk); bool isWatched = watchOn && AircraftWatchlist::isWatched(a.callsign); - // "notable" (oranger Ring) ist fuer auffaellige Rufzeichen - // (Militaer/Regierung) reserviert - Heavy-Flugzeuge bekommen - // stattdessen die eigene Markerform (siehe "heavy" oben). Es - // gibt aktuell aber keine Militaer-/Regierungs-Praefixliste im - // Projekt (auch nicht am Geraete-Display, siehe - // radar_screen.cpp) - "notable" bleibt daher bis auf Weiteres - // immer false. +// "notable" (orange ring) is reserved for conspicuous callsigns + // (military/government) - heavy aircraft instead get their + // own marker shape (see "heavy" above). However, there is + // currently no military/government prefix list in the + // project (not even on the device display, see + // radar_screen.cpp) - "notable" therefore remains + // always false for now. bool isNotable = false; JsonObject o = arr.add(); @@ -516,10 +514,10 @@ namespace { o["emergency"] = isEmergency; o["watched"] = isWatched; o["notable"] = isNotable; - // Zusaetzliche Felder nur fuer das Info-Panel bei Klick/Tap auf - // ein Flugzeug (siehe showInfo() in appendRadarSection()) - beide - // stehen bereits verlustfrei im Aircraft-Snapshot, kein - // zusaetzlicher Netzwerk-/SD-Zugriff noetig. +// Additional fields only for the info panel on click/tap on + // an aircraft (see showInfo() in appendRadarSection()) - both + // are already losslessly in the Aircraft snapshot, no + // additional network/SD access needed. o["speed_kt"] = a.groundSpeedKt; o["squawk"] = a.squawk; } diff --git a/src/webui_screen.cpp b/src/webui_screen.cpp index a8e2689..a11d8b6 100644 --- a/src/webui_screen.cpp +++ b/src/webui_screen.cpp @@ -63,13 +63,12 @@ void run(TFT_eSPI& tft) { constexpr int16_t LINE_H = 16; constexpr int16_t VIEW_TOP = 36; - // Wenn WLAN verbunden ist, bekommt der Absatztext nur noch ein kleines - // (bei Bedarf scrollbares) Fenster, weil darunter fest der QR-Code samt - // IP-Adresse Platz braucht (siehe Alex' Feedback: der vorherige separate - // "QR"-Button oben rechts sah seltsam aus - jetzt wird der Code direkt - // hier gezeigt, mittig, mit der IP-Adresse mittig darunter). Ohne WLAN - // gibt es keinen QR-Code, der Text bekommt dann wie vorher die volle - // Bildschirmhoehe. +// When WiFi is connected, the paragraph text gets only a small + // (scrollable if needed) window, because the QR code plus IP address + // need fixed space below (see Alex' feedback: the previous separate + // "QR" button at top right looked strange - now the code is shown + // directly here, centered, with the IP address centered below). Without + // WiFi there is no QR code, the text gets full screen height as before. constexpr int16_t TEXT_VIEW_BOTTOM_CONNECTED = 84; constexpr int16_t TEXT_VIEW_BOTTOM_DISCONNECTED = Config::SCREEN_HEIGHT - 60; @@ -77,16 +76,15 @@ void run(TFT_eSPI& tft) { String urlLine = wifiConnected ? ("http://" + WiFi.localIP().toString() + "/") : String(); int16_t viewBottom = wifiConnected ? TEXT_VIEW_BOTTOM_CONNECTED : TEXT_VIEW_BOTTOM_DISCONNECTED; - // QR-Code einmalig erzeugen (die URL aendert sich waehrend dieser - // Bildschirm offen ist nicht). Version 4 (33x33 Module) reicht mit - // deutlicher Reserve fuer eine "http:///"-URL (max. rund 24 - // Zeichen) - ECC_LOW statt eines hoeheren Fehlerkorrektur-Levels, um die - // Module moeglichst gross (und damit leicht scannbar) darstellen zu - // koennen. +// Generate QR code once (URL doesn't change while this screen + // is open). Version 4 (33x33 modules) is sufficient with plenty of + // headroom for a "http:///" URL (max. ~24 chars) - ECC_LOW + // instead of a higher error correction level, to make the modules as + // large (and thus easily scannable) as possible. constexpr uint8_t QR_VERSION = 4; constexpr int16_t QR_SIZE_MODULES = 33; // Version 4: 4*4+17 = 33 constexpr int16_t QR_BLOCK = 4; - constexpr int16_t QR_QUIET = 2; // Ruhezone in Modulen rundherum, Scanner brauchen etwas Rand + constexpr int16_t QR_QUIET = 2; // Quiet zone in modules around it, scanners need some margin constexpr int16_t QR_PIXEL_SIZE = (QR_SIZE_MODULES + 2 * QR_QUIET) * QR_BLOCK; constexpr int16_t QR_X = (Config::SCREEN_WIDTH - QR_PIXEL_SIZE) / 2; constexpr int16_t QR_GAP = 6; @@ -99,9 +97,9 @@ void run(TFT_eSPI& tft) { qrcode_initText(&qrcode, qrData, QR_VERSION, ECC_LOW, urlLine.c_str()); } - // Gesamthoehe des Absatztextes vorab berechnen (draw=false), um zu - // wissen, ob Scroll-Pfeile gebraucht werden - muss exakt zur - // Zeichenreihenfolge in redraw() unten passen. +// Pre-calculate total height of paragraph text (draw=false) to know + // whether scroll arrows are needed - must exactly match the drawing + // order in redraw() below. int16_t totalH = VIEW_TOP; totalH = layoutWrapped(tft, 10, totalH, textMaxWidth, LINE_H, I18n::t(StringId::WEBUI_INFO_PARA1), 0, 0, 0, false); if (!wifiConnected) { @@ -136,9 +134,9 @@ void run(TFT_eSPI& tft) { } if (wifiConnected) { - // QR-Code + IP-Adresse stehen fest unterhalb des (ggf. - // scrollbaren) Textes, unabhaengig von scrollY - beides mittig - // zentriert, direkt ueber dem Zurueck-Button. +// QR code + IP address are fixed below the (possibly scrollable) + // text, independent of scrollY - both centered, directly above + // the back button. tft.fillRect(QR_X, QR_Y, QR_PIXEL_SIZE, QR_PIXEL_SIZE, TFT_WHITE); for (uint8_t my = 0; my < qrcode.size; my++) { for (uint8_t mx = 0; mx < qrcode.size; mx++) { diff --git a/src/webui_screen.h b/src/webui_screen.h index fc441ee..71a7437 100644 --- a/src/webui_screen.h +++ b/src/webui_screen.h @@ -3,9 +3,9 @@ #include namespace WebUiScreen { - // Blockierend: zeigt die WLAN-IP und eine Erklaerung der kleinen - // eingebauten Webseite (Flugbuch ansehen/herunterladen/loeschen, - // Screenshots ansehen/herunterladen/loeschen). Kehrt zurueck, sobald - // "Zurueck" angetippt wurde. +// Blocking: shows the WiFi IP and an explanation of the small + // built-in web page (view logbook / download / delete, + // view screenshots / download / delete). Returns as soon as + // "Back" is tapped. void run(TFT_eSPI& tft); } diff --git a/src/wifi_manage_screen.h b/src/wifi_manage_screen.h index ed2b08a..89744ba 100644 --- a/src/wifi_manage_screen.h +++ b/src/wifi_manage_screen.h @@ -3,9 +3,9 @@ #include namespace WifiManageScreen { - // Blockierend: zeigt die bis zu 3 gespeicherten WLAN-Netzwerke, erlaubt - // Loeschen einzelner Eintraege und (falls noch Platz ist) das Hinzufuegen - // eines neuen ueber den Scan+Passwort-Bildschirm. Kehrt zurueck, sobald - // "Back" angetippt wird. +// Blocking: shows the up to 3 saved WiFi networks, allows + // deleting individual entries and (if there is still space) adding + // a new one via the scan+password screen. Returns as soon as + // "Back" is tapped. void run(TFT_eSPI& tft); } \ No newline at end of file diff --git a/src/wifi_setup_screen.h b/src/wifi_setup_screen.h index 1b52082..3512cd3 100644 --- a/src/wifi_setup_screen.h +++ b/src/wifi_setup_screen.h @@ -3,9 +3,9 @@ #include namespace WifiSetupScreen { - // Blockierend: WLAN-Netzwerke suchen, per Touch auswaehlen, Passwort - // ueber Bildschirmtastatur eingeben (Klartext, keine Sterne), verbinden. - // Speichert bei Erfolg die Zugangsdaten via WifiMgr auf der SD-Karte. - // Rueckgabe: true = verbunden, false = abgebrochen/uebersprungen. +// Blocking: scan WiFi networks, select via touch, enter password + // via on-screen keyboard (plain text, no asterisks), connect. + // Saves credentials via WifiMgr on the SD card on success. + // Returns: true = connected, false = cancelled/skipped. bool run(TFT_eSPI& tft); } \ No newline at end of file