localization
This commit is contained in:
267
AGENTS.md
Normal file
267
AGENTS.md
Normal file
@@ -0,0 +1,267 @@
|
||||
# Eiswolfs Flightradar (CYD) — Project Context for Claude Code
|
||||
|
||||
Read this file FIRST before making any code changes. It contains the
|
||||
things you need to know to avoid repeating the same bugs that have been
|
||||
encountered and fixed during development.
|
||||
|
||||
## What is this project?
|
||||
|
||||
A live ADS-B flight radar on an ESP32 "Cheap Yellow Display" (CYD,
|
||||
ESP32-2432S028), 240x320 touch TFT. Shows nearby aircraft on a
|
||||
rotating radar screen with a detail panel (model, altitude, speed, route,
|
||||
squawk), proximity LED alert, WiFi management (up to 3 networks),
|
||||
location presets (including remote locations worldwide), airline filter, flight logbook
|
||||
with statistics, and menus/splash screen with a subtle twinkling
|
||||
star animation.
|
||||
|
||||
Current version: **v3.6.3**. Public repo:
|
||||
https://github.com/Eiswolf-BG/eiswolfs-flightradar-CYD
|
||||
|
||||
## Who uses this?
|
||||
|
||||
Alex — a complete beginner at Arduino/embedded development, working with
|
||||
VS Code + PlatformIO on a Mac. Please respond in a way that is accessible;
|
||||
avoid assuming familiarity with technical jargon.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- PlatformIO, `platform = espressif32`, `board = esp32dev`, `framework = arduino`
|
||||
- TFT_eSPI (display), XPT2046_Touchscreen (touch), ArduinoJson, TinyGPSPlus
|
||||
- Dual-core design: networking (WiFi, ADS-B polling) on Core 0, display/touch
|
||||
on Core 1 — the UI never blocks on network requests.
|
||||
- Data sources: [adsb.fi](https://adsb.fi) (aircraft positions), [hexdb.io](https://hexdb.io)
|
||||
(model lookups), [ip-api.com](https://ip-api.com) (IP geolocation)
|
||||
|
||||
## ⚠️ MOST IMPORTANT GOTCHA: Custom font is baseline-anchored
|
||||
|
||||
The built-in TFT_eSPI fonts (GLCD, Font2 etc.) are pure ASCII. For
|
||||
umlauts/accents (German, Turkish, French, Spanish, Italian)
|
||||
there is a **self-generated font** (`src/ui_font.h`, activated globally
|
||||
via `tft.setFreeFont(&UiFont11pt)` in `main.cpp::setup()`, which applies
|
||||
to EVERY `print()`/`drawString()` call throughout the app).
|
||||
|
||||
**The critical difference from built-in fonts:** With
|
||||
`setCursor(x, y); print(...)`, the `y` coordinate for our font is the **baseline**,
|
||||
NOT the top edge as with the old GLCD font. Text grows
|
||||
UPWARD from `y` (by the ascent, roughly 9px at Size 1, roughly 16-18px at
|
||||
Size 2), not downward.
|
||||
|
||||
**This has caused the following bugs in the past (all fixed, but
|
||||
be careful with new code!):**
|
||||
- Values of `y` that are too small (e.g. `setCursor(10, 2)`) → text protrudes
|
||||
out of or is clipped at the top of the screen/container. **Rule of thumb:
|
||||
y should never be less than ~14 at Size 1, never less than
|
||||
~24-26 at Size 2 (depending on the container).**
|
||||
- Input fields/boxes: Baseline must be near the BOTTOM edge of the box,
|
||||
not in the middle as one might be used to from the old font.
|
||||
- Two text sizes in quick succession within a tight layout
|
||||
(label at Size 1 directly above value at Size 2) are error-prone — in the
|
||||
statistics screen this was deliberately changed to a UNIFORM size (only color
|
||||
distinguishes label/value), which is more robust.
|
||||
- `drawString()` with `setTextDatum(MC_DATUM)` (centered) is NOT
|
||||
affected — TFT_eSPI handles centering correctly regardless of whether the
|
||||
font is baseline- or top-anchored. Only raw `setCursor()`+`print()` is
|
||||
the danger zone.
|
||||
- Long multi-line text (e.g. explanatory text) should NEVER rely on TFT_eSPI's
|
||||
built-in auto-wrap — it breaks mid-word. Instead,
|
||||
use the `layoutWrapped()` helper (implemented once each in
|
||||
`location_presets_screen.cpp` and `wifi_manage_screen.cpp`, handles word-wise
|
||||
wrapping based on actual pixel width plus optional scrolling).
|
||||
|
||||
If new screens or text areas are added: test more than once (ideally
|
||||
with a photo of the actual display) before considering the code done.
|
||||
|
||||
## i18n (6 languages)
|
||||
|
||||
- `src/i18n.h`: `enum class StringId` — every fixed UI text has an ID.
|
||||
- `src/i18n_en.h`, `i18n_de.h`, `i18n_fr.h`, `i18n_tr.h`, `i18n_es.h`,
|
||||
`i18n_it.h`: each a `static const char* const[]` array, in **exactly the
|
||||
same order** as the enum.
|
||||
- Each file has a `static_assert` at the end checking the array size against
|
||||
`StringId::COUNT` — **if the build fails due to a failing static_assert,
|
||||
at least one language file is missing an entry or has one too many.**
|
||||
New StringId → add in ALL 6 files at the same position, otherwise the
|
||||
mapping shifts.
|
||||
- Language names themselves (`I18n::languageName()`) are stored separately in
|
||||
`i18n.cpp`, with correct native special characters
|
||||
(e.g. "Français", "Türkçe", "Español").
|
||||
|
||||
## UI Conventions (please follow for new screens)
|
||||
|
||||
- **Color scheme:** Black background, green frame/text
|
||||
(`TFT_BLACK`/`TFT_GREEN`), active/selected entries inverted (green
|
||||
fill, black text). Destructive actions (cancel/delete) in red
|
||||
(`TFT_RED`).
|
||||
- Each screen typically has a local `struct Rect` with `contains(x,y)` and
|
||||
a `drawButton()` helper function (copy-paste pattern from existing
|
||||
screens, no shared Rect/Button module — this is intentional, to keep
|
||||
each screen independently runnable).
|
||||
- **Star animation** (`src/menu_stars.h/.cpp`): Runs in the background on
|
||||
EVERY black menu/splash screen. New screens should call
|
||||
`MenuStars::reset()` once on enter and
|
||||
`MenuStars::update(tft)` in every wait/idle loop (the loop otherwise
|
||||
runs idle since the function internally throttles itself to ~60ms).
|
||||
- Wait loop pattern for touch input:
|
||||
```cpp
|
||||
TouchInput::Point tap;
|
||||
while (true) {
|
||||
if (TouchInput::wasTapped(tap)) break;
|
||||
MenuStars::update(tft);
|
||||
delay(20);
|
||||
}
|
||||
```
|
||||
- Menu structure: Main menu → 4 categories (Region & Language, WiFi/Network,
|
||||
System, Flight Options) → subpages each. WiFi/Network currently has
|
||||
NO submenu anymore, jumps directly into network management.
|
||||
- Many screens with text input (airline code, coordinates, WiFi password)
|
||||
have a "?" info button at the top right that opens a scrollable
|
||||
explanation screen (pattern: `location_presets_screen.cpp` and
|
||||
`wifi_manage_screen.cpp`, each with its own `layoutWrapped()` copy).
|
||||
|
||||
## Other fixed values (Config::...)
|
||||
|
||||
- Proximity alert radius: 3 km
|
||||
- Emergency squawks: 7500 (hijack), 7600 (radio failure), 7700 (emergency)
|
||||
- Radar ranges: 10/25/50/100 km
|
||||
- ADS-B fetch interval: every 8 seconds
|
||||
- Altitude color coding: Green <10,000ft, Yellow 10-30,000ft, Red >30,000ft
|
||||
- Max. 3 WiFi networks, max. 3 location presets, max. 10 filtered airlines
|
||||
|
||||
## Code Style
|
||||
|
||||
- All comments in English. Use clear, concise English for implementation
|
||||
notes and rationale.
|
||||
- Build check after EVERY change: `pio run` in the project directory, check
|
||||
for BOTH warnings AND errors (not just "compiles").
|
||||
- Always prefer least-invasive changes — do not rename existing
|
||||
functions/names without reason.
|
||||
|
||||
## Language: Project outward-facing text always in English
|
||||
|
||||
All outwardly visible text must ALWAYS be written in English —
|
||||
regardless of which language the conversation with Alex is conducted in.
|
||||
This applies to:
|
||||
- `README.md`
|
||||
- `index.html` (website/flasher page)
|
||||
- GitHub release notes / descriptions
|
||||
- Commit messages
|
||||
- Any other descriptive or bugfix text that is publicly visible
|
||||
(e.g. on GitHub)
|
||||
|
||||
Exception: The firmware UI itself remains multilingual as before
|
||||
(`i18n_de/en/fr/tr/es/it.h`) — this rule only applies to the
|
||||
project's outward-facing presentation (repo, release notes, website), not
|
||||
the app interface on the device.
|
||||
|
||||
## Known open points / possible next steps
|
||||
|
||||
No acute open bugs known (as of v3.6.3). Possible ideas for later,
|
||||
if the user asks: add info buttons to more menus, possibly more languages,
|
||||
possibly export/share logbook data.
|
||||
|
||||
## Standard Workflow: Push & Release
|
||||
|
||||
IMPORTANT — when this workflow starts: The complete release workflow
|
||||
(README update, commit, tag, push) may ONLY be started when Alex
|
||||
EXPLICITLY asks for it (e.g. "let's push", "can we release", "run the
|
||||
release workflow"). A simple "yes" in response to a follow-up question
|
||||
(e.g. about a CLAUDE.md change or some other detail) is NOT a request to
|
||||
start the release workflow. For small fixes/changes, ONLY build
|
||||
and flash (see the "Auto-flash after every successful build" section below),
|
||||
but do NOT commit/tag/push until explicitly asked.
|
||||
|
||||
Once the workflow is explicitly requested, automatically follow these steps
|
||||
in order:
|
||||
|
||||
0. Determine the version number: The new version number ALWAYS comes exactly
|
||||
from Alex — he names it in the push request (e.g. via Claude/the sandbox
|
||||
assistant: "Alex wants to push version X.Y.Z"). Enter THAT EXACT
|
||||
number into `Config::APP_VERSION` (`src/config.h`) — never
|
||||
increment, guess, or derive it from the last version (not even for small
|
||||
patches). This also applies to larger jumps (e.g.
|
||||
2.6 -> 3.0) that Alex may deliberately and intentionally make — always
|
||||
adopt the stated number 1:1 without making assumptions.
|
||||
If no explicit version number was given in the push request, ask
|
||||
Alex rather than guessing. Only AFTER entering the correct
|
||||
number: do a clean `pio run` build, THEN the rest of the known workflow
|
||||
(README, tag, index.html, bin files, commit/push).
|
||||
(Here, intentionally build ONLY, do NOT flash — exception to the otherwise
|
||||
applicable auto-flash rule, see the "Auto-flash after every successful
|
||||
build" section below. The test device should stay on the current version
|
||||
so the new release can be tested via OTA.)
|
||||
1. Check whether any new/changed features have been added since the last
|
||||
commit that are visible to end users (new menu items, changed behavior,
|
||||
new screens) — if so, **update README.md accordingly**
|
||||
(same style: emoji headings, anchor links between the "Features" list
|
||||
and the deep-dive sections, short examples where useful). Purely internal
|
||||
bugfixes/refactorings with no visible user impact do not need a
|
||||
README entry.
|
||||
2. Commit the code (meaningful commit message).
|
||||
3. If it is a version jump: Create a git tag with the
|
||||
version number + description of changes.
|
||||
4. Check whether `index.html` (web flasher) still shows the old version
|
||||
number — if so, update it. **NOT OPTIONAL, must NEVER be skipped
|
||||
on any release push** — not even for small patch versions. Always treat
|
||||
as a fixed double-step together with Step 1: whenever
|
||||
the README (or even just the version number) is updated to a new version,
|
||||
ALWAYS check and update `index.html` in the same pass.
|
||||
4b. Keep the web flasher version selection current: Before overwriting the
|
||||
root `.bin` files with the new build, archive the CURRENT (still old)
|
||||
bootloader.bin/partitions.bin/firmware.bin into a new folder
|
||||
`versions/vOLD/` (vOLD = the version number that index.html
|
||||
showed before this update) — also put a `manifest.json` there
|
||||
(identical content to the root manifest.json, see
|
||||
`versions/v3.6.2/manifest.json` as a template). Then in the
|
||||
`versions/` folder keep only the 2 newest version folders (sorted by
|
||||
version number, not file date) — delete older folders.
|
||||
Then update the version dropdown in `index.html`
|
||||
(`<select id="versionSelect">`): 3 options — the new
|
||||
current version (`value="manifest.json"`, `data-version="vNEW"`, text
|
||||
"vNEW (latest)") plus the two versions now remaining in `versions/`
|
||||
(`value="versions/vX.Y.Z/manifest.json"`, newest first).
|
||||
5. Check whether `bootloader.bin`, `firmware.bin`, `partitions.bin` in the
|
||||
project root match the current build in `.pio/build/esp32dev/` —
|
||||
if not, copy them from there.
|
||||
6. Commit and push all these changes (code + README + web flasher files)
|
||||
together (`git push`, plus `git push origin vX.Y.Z` if a
|
||||
tag was created).
|
||||
7. Create a GitHub Release AND upload the `.bin` file in one step
|
||||
(via the `gh` CLI, set up and authenticated since v2.7.5 — see
|
||||
`gh auth status`). The release asset is simply called `firmware.bin`, no
|
||||
renaming needed — most users download via the
|
||||
web flasher anyway; the asset is only relevant for the few people who
|
||||
need a `.bin` file directly via a CYD launcher app (the filename
|
||||
doesn't matter for that):
|
||||
|
||||
gh release create vX.Y.Z .pio/build/esp32dev/firmware.bin \
|
||||
--repo Eiswolf-BG/eiswolfs-flightradar-CYD \
|
||||
--title "vX.Y.Z" \
|
||||
--notes "<Release-notes-text>"
|
||||
|
||||
The release notes text comes from the respective push request (the same
|
||||
text used for the tag message) — if no text was provided in the
|
||||
push request, derive it from `git log` since the last tag, as was done
|
||||
previously for the tag message.
|
||||
8. Brief summary at the end: what was committed/tagged/pushed, whether
|
||||
the README was updated (and if so, which sections), and the URL
|
||||
of the created GitHub Release. The GitHub Release step is now fully
|
||||
automatic — no manual follow-up needed from Alex anymore, except if
|
||||
`gh` should ever lose authentication (then run `gh auth login` again,
|
||||
see above).
|
||||
|
||||
## Auto-flash after every successful build
|
||||
|
||||
As soon as `pio run` (build) completes successfully without errors, ALWAYS
|
||||
flash immediately afterward (`pio run --target upload`) without asking —
|
||||
unless the user explicitly says "build only, don't flash" or similar.
|
||||
Shortly after, confirm that the upload was also successful (including
|
||||
the "[SUCCESS]" line at the end).
|
||||
|
||||
EXCEPTION: Within the Push & Release routine (see the
|
||||
"Standard Workflow: Push & Release" section above), do NOT auto-flash, even
|
||||
after a successful build — there, the build is intentionally run only.
|
||||
Reason: Alex wants to keep the test device on the current version so the
|
||||
new release can subsequently be tested via the OTA update functionality,
|
||||
rather than flashing it directly via cable. For all other purposes (normal
|
||||
development/testing, individual fixes), the auto-flash rule continues to
|
||||
apply unchanged.
|
||||
276
CLAUDE.md
276
CLAUDE.md
@@ -1,276 +0,0 @@
|
||||
# Eiswolfs Flightradar (CYD) — Projektkontext für Claude Code
|
||||
|
||||
Lies diese Datei ZUERST, bevor du irgendwas am Code änderst. Sie enthält die
|
||||
Dinge, die man wissen muss, um nicht dieselben Fehler nochmal zu machen, die
|
||||
in der Entwicklung schon mal aufgetreten und behoben wurden.
|
||||
|
||||
## Was ist das Projekt?
|
||||
|
||||
Ein Live-ADS-B-Flugradar auf einem ESP32 "Cheap Yellow Display" (CYD,
|
||||
ESP32-2432S028), 240x320 Touch-TFT. Zeigt Flugzeuge in der Nähe auf einem
|
||||
rotierenden Radarschirm, mit Detail-Panel (Modell, Höhe, Speed, Route,
|
||||
Squawk), Näherungs-LED-Alarm, WLAN-Verwaltung (bis zu 3 Netzwerke),
|
||||
Standort-Presets (auch für fremde Orte weltweit), Airline-Filter, Flugbuch
|
||||
mit Statistik, und Menüs/Splash-Screen mit einer dezenten twinkelnden
|
||||
Sterne-Animation.
|
||||
|
||||
Aktueller Stand: **v2.3.2**. Öffentliches Repo:
|
||||
https://github.com/Eiswolf-BG/eiswolfs-flightradar-CYD
|
||||
|
||||
## Wer nutzt das?
|
||||
|
||||
Alex — kompletter Anfänger bei Arduino/Embedded-Entwicklung, arbeitet mit
|
||||
VS Code + PlatformIO auf einem Mac. Bitte auf Deutsch antworten. Erklärungen
|
||||
gerne etwas ausführlicher, nicht von Fachbegriffen ausgehen, die als bekannt
|
||||
vorausgesetzt werden.
|
||||
|
||||
## Tech-Stack
|
||||
|
||||
- PlatformIO, `platform = espressif32`, `board = esp32dev`, `framework = arduino`
|
||||
- TFT_eSPI (Display), XPT2046_Touchscreen (Touch), ArduinoJson, TinyGPSPlus
|
||||
- Dual-Core-Design: Netzwerk (WLAN, ADS-B-Polling) auf Core 0, Display/Touch
|
||||
auf Core 1 — die UI blockiert nie durch Netzwerk-Requests.
|
||||
- Daten: [adsb.fi](https://adsb.fi) (Flugzeugpositionen), [hexdb.io](https://hexdb.io)
|
||||
(Modell-Lookups), [ip-api.com](https://ip-api.com) (IP-Geolocation)
|
||||
|
||||
## ⚠️ WICHTIGSTE FALLE: Der eigene Font ist grundlinien-verankert
|
||||
|
||||
Die eingebauten TFT_eSPI-Fonts (GLCD, Font2 etc.) sind reines ASCII. Für
|
||||
Umlaute/Akzente (Deutsch, Türkisch, Französisch, Spanisch, Italienisch)
|
||||
gibt's einen **selbst generierten Font** (`src/ui_font.h`, via
|
||||
`tft.setFreeFont(&UiFont11pt)` global in `main.cpp::setup()` aktiviert, gilt
|
||||
danach für JEDEN `print()`/`drawString()`-Aufruf in der ganzen App).
|
||||
|
||||
**Der entscheidende Unterschied zu den eingebauten Fonts:** Bei
|
||||
`setCursor(x, y); print(...)` ist `y` bei unserem Font die **Grundlinie**
|
||||
(Baseline), NICHT die obere Kante wie beim alten GLCD-Font. Der Text wächst
|
||||
von `y` aus nach OBEN (um den Ascent, ca. 9px bei Size 1, ca. 16-18px bei
|
||||
Size 2), nicht nach unten.
|
||||
|
||||
**Das hat in der Vergangenheit zu folgenden Bugs geführt (alle behoben, aber
|
||||
Vorsicht bei neuem Code!):**
|
||||
- Zu kleine y-Werte (z.B. `setCursor(10, 2)`) → Text ragt oben aus dem
|
||||
Bildschirm/Container heraus oder wird abgeschnitten. **Faustregel:
|
||||
y sollte bei Size 1 nie kleiner als ~14 sein, bei Size 2 nie kleiner als
|
||||
~24-26 (abhängig vom Container).**
|
||||
- Eingabefelder/Boxen: Baseline muss nahe der UNTERKANTE der Box liegen,
|
||||
nicht in der Mitte wie man's vom alten Font gewohnt wäre.
|
||||
- Zwei Textgrößen kurz hintereinander in einem eng bemessenen Layout
|
||||
(Label Size 1 direkt über Wert Size 2) sind fehleranfällig — im
|
||||
Statistik-Screen deshalb bewusst auf EINHEITLICHE Größe (nur Farbe
|
||||
unterscheidet Label/Wert) umgestellt, das ist robuster.
|
||||
- `drawString()` mit `setTextDatum(MC_DATUM)` (zentriert) ist NICHT
|
||||
betroffen — TFT_eSPI rechnet die Zentrierung selbst korrekt aus, egal ob
|
||||
Baseline- oder Top-verankert. Nur rohes `setCursor()`+`print()` ist die
|
||||
Gefahrenzone.
|
||||
- Langer, mehrzeiliger Text (z.B. Erklärtexte) NIE mit TFT_eSPI's
|
||||
eingebautem Auto-Wrap verlassen — das bricht mitten im Wort ab. Stattdessen
|
||||
den `layoutWrapped()`-Helper verwenden (in `location_presets_screen.cpp`
|
||||
und `wifi_manage_screen.cpp` je einmal implementiert, macht wortweisen
|
||||
Umbruch anhand echter Pixel-Breite plus optionales Scrollen).
|
||||
|
||||
Falls neue Screens/Textstellen dazukommen: lieber einmal mehr testen (idealerweise
|
||||
mit Foto vom echten Display), bevor der Code als fertig gilt.
|
||||
|
||||
## i18n (6 Sprachen)
|
||||
|
||||
- `src/i18n.h`: `enum class StringId` — jeder feste UI-Text hat eine ID.
|
||||
- `src/i18n_en.h`, `i18n_de.h`, `i18n_fr.h`, `i18n_tr.h`, `i18n_es.h`,
|
||||
`i18n_it.h`: je ein `static const char* const[]`-Array, in **exakt
|
||||
derselben Reihenfolge** wie das Enum.
|
||||
- Jede Datei hat am Ende einen `static_assert`, der die Array-Größe gegen
|
||||
`StringId::COUNT` prüft — **wenn der Build wegen eines fehlschlagenden
|
||||
static_assert bricht, fehlt in mindestens einer Sprachdatei ein Eintrag
|
||||
oder es ist einer zu viel.** Neue StringId → in ALLEN 6 Dateien an
|
||||
derselben Position ergänzen, sonst verschiebt sich die Zuordnung.
|
||||
- Eigennamen der Sprachen (`I18n::languageName()`) sind separat in
|
||||
`i18n.cpp` hinterlegt, mit korrekten landessprachlichen Sonderzeichen
|
||||
(z.B. "Français", "Türkçe", "Español").
|
||||
|
||||
## UI-Konventionen (bitte einhalten für neue Screens)
|
||||
|
||||
- **Farbschema:** Schwarzer Hintergrund, grüner Rahmen/Text
|
||||
(`TFT_BLACK`/`TFT_GREEN`), aktive/ausgewählte Einträge invertiert (grün
|
||||
gefüllt, schwarzer Text). Destruktive Aktionen (Abbrechen/Löschen) in Rot
|
||||
(`TFT_RED`).
|
||||
- Jeder Screen hat i.d.R. eine lokale `struct Rect` mit `contains(x,y)` und
|
||||
eine `drawButton()`-Hilfsfunktion (Copy-Paste-Muster aus den bestehenden
|
||||
Screens, kein gemeinsames Rect/Button-Modul — das ist bewusst so, um
|
||||
jeden Screen unabhängig lauffähig zu halten).
|
||||
- **Sterne-Animation** (`src/menu_stars.h/.cpp`): Läuft im Hintergrund auf
|
||||
JEDEM schwarzen Menü-/Splash-Screen. Neue Screens sollten
|
||||
`MenuStars::reset()` einmal beim Betreten aufrufen und
|
||||
`MenuStars::update(tft)` in jeder Warte-/Idle-Schleife (Loop läuft sonst
|
||||
ungenutzt, da die Funktion sich intern selbst auf ~60ms drosselt).
|
||||
- Warteschleifen-Pattern für Touch-Eingabe:
|
||||
```cpp
|
||||
TouchInput::Point tap;
|
||||
while (true) {
|
||||
if (TouchInput::wasTapped(tap)) break;
|
||||
MenuStars::update(tft);
|
||||
delay(20);
|
||||
}
|
||||
```
|
||||
- Menüstruktur: Hauptmenü → 4 Kategorien (Land/Region, WLAN/Netzwerk,
|
||||
System, Flugoptionen) → jeweils Unterseiten. WLAN/Netzwerk hat aktuell
|
||||
KEIN eigenes Untermenü mehr, springt direkt in die Netzwerk-Verwaltung.
|
||||
- Viele Screens mit Text-Eingabe (Airline-Code, Koordinaten, WLAN-Passwort)
|
||||
haben einen "?"-Info-Button oben rechts, der einen scrollbaren
|
||||
Erklär-Screen öffnet (Muster: `location_presets_screen.cpp` und
|
||||
`wifi_manage_screen.cpp`, jeweils eigene `layoutWrapped()`-Kopie).
|
||||
|
||||
## Sonstige feste Werte (Config::…)
|
||||
|
||||
- Näherungsalarm-Radius: 3 km
|
||||
- Notfall-Squawks: 7500 (Entführung), 7600 (Funkausfall), 7700 (Notfall)
|
||||
- Radar-Reichweiten: 10/25/50/100 km
|
||||
- ADS-B-Abruf-Intervall: alle 8 Sekunden
|
||||
- Höhen-Farbcodierung: Grün <10.000ft, Gelb 10-30.000ft, Rot >30.000ft
|
||||
- Max. 3 WLAN-Netzwerke, max. 3 Standort-Presets, max. 10 gefilterte Airlines
|
||||
|
||||
## Code-Stil
|
||||
|
||||
- Kommentare durchgehend auf Deutsch, ohne Umlaute in Kommentaren selbst
|
||||
unüblich (ae/oe/ue sind hier ok, das ist nur ein Stil-Ding für
|
||||
Kommentare, NICHT für die UI-Texte in den i18n-Dateien — dort echte
|
||||
Umlaute verwenden, siehe oben).
|
||||
- Build-Check nach JEDER Änderung: `pio run` im Projektverzeichnis, auf
|
||||
Warnungen UND Errors prüfen (nicht nur "compiles").
|
||||
- Immer least-invasive Änderungen bevorzugen — bestehende Funktionen/Namen
|
||||
nicht ohne Grund umbenennen.
|
||||
|
||||
## Sprache: Projekt-Außendarstellung immer Englisch
|
||||
|
||||
Alle nach außen sichtbaren Texte sind IMMER auf Englisch zu verfassen —
|
||||
unabhängig davon, in welcher Sprache die Unterhaltung mit Alex geführt
|
||||
wird. Das betrifft insbesondere:
|
||||
- `README.md`
|
||||
- `index.html` (Webseite/Flasher-Seite)
|
||||
- GitHub-Release-Notes / -Beschreibungen
|
||||
- Commit-Messages
|
||||
- Jeglicher sonstiger Beschreibungs- oder Bugfix-Text, der öffentlich
|
||||
sichtbar ist (z.B. auf GitHub)
|
||||
|
||||
Ausnahme: Die Firmware-UI selbst bleibt mehrsprachig wie gehabt
|
||||
(`i18n_de/en/fr/tr/es/it.h`) — diese Regel betrifft NUR die
|
||||
Projekt-Außendarstellung (Repo, Release Notes, Webseite), nicht die
|
||||
App-Oberfläche auf dem Gerät. Interne Code-Kommentare bleiben ebenfalls
|
||||
wie gehabt auf Deutsch (siehe „Code-Stil" oben) — diese Regel gilt nur
|
||||
für nach außen sichtbare Texte.
|
||||
|
||||
## Bekannte offene Punkte / mögliche nächste Schritte
|
||||
|
||||
Keine akuten offenen Bugs bekannt (Stand v2.3.2). Mögliche Ideen für später,
|
||||
falls der Nutzer danach fragt: weitere Menüs mit Info-Buttons versehen,
|
||||
ggf. weitere Sprachen, ggf. Export/Teilen der Logbuch-Daten.
|
||||
## Standard-Workflow: Push & Release
|
||||
|
||||
WICHTIG - wann dieser Workflow startet: Der komplette Release-Workflow
|
||||
(README-Update, Commit, Tag, Push) darf NUR gestartet werden, wenn Alex
|
||||
EXPLIZIT danach fragt (z.B. "lass pushen", "können wir releasen", "mach den
|
||||
Release-Workflow"). Ein einfaches "ja" auf eine Rückfrage (z.B. zu einer
|
||||
CLAUDE.md-Änderung oder einem anderen Detail) ist KEINE Aufforderung, den
|
||||
Release-Workflow zu starten. Bei kleineren Fixes/Änderungen bitte NUR bauen
|
||||
und flashen (siehe Abschnitt "Nach jedem erfolgreichen Build automatisch
|
||||
flashen" unten), aber NICHT committen/taggen/pushen, bis ausdrücklich danach
|
||||
gefragt wird.
|
||||
|
||||
Sobald der Workflow explizit angefordert wurde, automatisch folgende Schritte
|
||||
in dieser Reihenfolge:
|
||||
|
||||
0. Versionsnummer festlegen: Die neue Versionsnummer kommt IMMER exakt von
|
||||
Alex - er nennt sie im Push-Wunsch (z.B. über Claude/den Sandbox-
|
||||
Assistenten: "Alex will auf Version X.Y.Z pushen"). Karl trägt GENAU
|
||||
diese Nummer in `Config::APP_VERSION` (`src/config.h`) ein - niemals
|
||||
selbst hochzählen, erraten oder von der letzten Version ableiten (auch
|
||||
nicht bei kleinen Patches). Das gilt auch für größere Sprünge (z.B.
|
||||
2.6 -> 3.0), die Alex bewusst und absichtlich machen kann - Karl
|
||||
übernimmt in jedem Fall die genannte Nummer 1:1, ohne eigene Annahmen.
|
||||
Falls im Push-Wunsch keine explizite Versionsnummer genannt wurde, bei
|
||||
Alex nachfragen statt zu raten. Erst NACH dem Eintragen der korrekten
|
||||
Nummer: einmal sauber `pio run` bauen, DANACH erst der Rest des
|
||||
bekannten Workflows (README, Tag, index.html, Bin-Dateien, Commit/Push).
|
||||
(Hier bewusst NUR bauen, NICHT flashen - Ausnahme von der sonst
|
||||
geltenden Auto-Flash-Regel, siehe Abschnitt "Nach jedem erfolgreichen
|
||||
Build automatisch flashen" weiter unten. Das Testgerät soll auf der
|
||||
bisherigen Version bleiben, damit das neue Release per OTA getestet
|
||||
werden kann.)
|
||||
1. Prüfen, ob seit dem letzten Commit neue/geänderte Features hinzugekommen
|
||||
sind, die für Endnutzer sichtbar sind (neue Menüpunkte, geändertes
|
||||
Verhalten, neue Screens) - falls ja, **README.md entsprechend ergänzen**
|
||||
(gleicher Stil: Emoji-Überschriften, Ankerlinks zwischen "Features"-Liste
|
||||
und den Deep-Dive-Sektionen, kurze Beispiele wo sinnvoll). Reine interne
|
||||
Bugfixes/Refactorings ohne sichtbare Nutzerauswirkung brauchen keinen
|
||||
README-Eintrag.
|
||||
2. Code committen (aussagekräftige Commit-Message).
|
||||
3. Falls es sich um einen Versionssprung handelt: Git-Tag mit
|
||||
Versionsnummer + Beschreibung der Änderungen erstellen.
|
||||
4. Prüfen, ob `index.html` (Web-Flasher) noch die alte Versionsnummer zeigt -
|
||||
falls ja, aktualisieren. **NICHT OPTIONAL, darf bei KEINEM Release-Push
|
||||
übersprungen werden** - auch nicht bei kleinen Patch-Versionen. Immer als
|
||||
fester Doppel-Schritt zusammen mit Schritt 1 (README) behandeln: wann
|
||||
immer die README (oder auch nur die Versionsnummer) auf eine neue Version
|
||||
aktualisiert wird, IMMER im selben Zug auch `index.html` prüfen und
|
||||
synchron mitziehen.
|
||||
4b. Web-Flasher-Versionsauswahl aktuell halten: Bevor die Wurzel-.bin-Dateien
|
||||
mit dem neuen Build überschrieben werden, die BISHERIGEN (noch alten)
|
||||
bootloader.bin/partitions.bin/firmware.bin in einen neuen Ordner
|
||||
`versions/vALT/` archivieren (vALT = die Versionsnummer, die index.html
|
||||
vor diesem Update zeigte) - dort auch eine `manifest.json` ablegen
|
||||
(identischer Inhalt wie die Wurzel-manifest.json, siehe
|
||||
`versions/v3.6.2/manifest.json` als Vorlage). Danach im Ordner
|
||||
`versions/` nur die 2 neuesten Versionsordner (nach Versionsnummer
|
||||
sortiert, nicht nach Datei-Datum) behalten - ältere Ordner löschen.
|
||||
Anschließend in `index.html` den Versions-Dropdown
|
||||
(`<select id="versionSelect">`) aktualisieren: 3 Optionen - die neue
|
||||
aktuelle Version (`value="manifest.json"`, `data-version="vNEU"`, Text
|
||||
"vNEU (latest)") plus die beiden jetzt in `versions/` verbliebenen
|
||||
Versionen (`value="versions/vX.Y.Z/manifest.json"`, neueste zuerst).
|
||||
5. Prüfen, ob `bootloader.bin`, `firmware.bin`, `partitions.bin` im
|
||||
Hauptverzeichnis dem aktuellen Build in `.pio/build/esp32dev/`
|
||||
entsprechen - falls nicht, von dort kopieren.
|
||||
6. Alle diese Änderungen (Code + README + Web-Flasher-Dateien) zusammen
|
||||
committen und pushen (`git push`, plus `git push origin vX.Y.Z` falls ein
|
||||
Tag erstellt wurde).
|
||||
7. GitHub Release erstellen UND die `.bin`-Datei in einem Schritt hochladen
|
||||
(per `gh` CLI, seit v2.7.5 eingerichtet und authentifiziert - siehe
|
||||
`gh auth status`). Das Release-Asset heißt einfach `firmware.bin`, keine
|
||||
Umbenennung nötig - die meisten Nutzer laden ohnehin über den
|
||||
Web-Flasher, das Asset ist nur noch für die wenigen Leute relevant, die
|
||||
über eine CYD-Launcher-App direkt eine `.bin`-Datei brauchen (dafür ist
|
||||
der Dateiname egal):
|
||||
|
||||
gh release create vX.Y.Z .pio/build/esp32dev/firmware.bin \
|
||||
--repo Eiswolf-BG/eiswolfs-flightradar-CYD \
|
||||
--title "vX.Y.Z" \
|
||||
--notes "<Release-Notes-Text>"
|
||||
|
||||
Der Release-Notes-Text kommt aus dem jeweiligen Push-Wunsch (derselbe
|
||||
Text, der auch für die Tag-Message verwendet wird) - falls im
|
||||
Push-Wunsch kein Text mitgegeben wurde, aus dem `git log` seit dem
|
||||
letzten Tag ableiten, wie bisher auch für die Tag-Message.
|
||||
8. Kurze Zusammenfassung am Ende: was committet/getaggt/gepusht wurde, ob
|
||||
die README aktualisiert wurde (und falls ja, welche Abschnitte), sowie
|
||||
die URL des erstellten GitHub Release. Der GitHub-Release-Schritt ist
|
||||
damit vollautomatisch - kein manuelles Nacharbeiten von Alex mehr
|
||||
nötig, außer `gh` sollte einmal die Authentifizierung verlieren (dann
|
||||
erneut `gh auth login`, siehe oben).
|
||||
|
||||
## Nach jedem erfolgreichen Build automatisch flashen
|
||||
|
||||
Sobald `pio run` (Build) erfolgreich ohne Fehler durchgelaufen ist, IMMER direkt
|
||||
im Anschluss auch flashen (`pio run --target upload`), ohne extra danach zu
|
||||
fragen - außer der Nutzer sagt ausdrücklich "nur bauen, nicht flashen" o.ä.
|
||||
Kurz danach bestätigen, dass der Upload ebenfalls erfolgreich war (inkl.
|
||||
"[SUCCESS]"-Zeile am Ende).
|
||||
|
||||
AUSNAHME: Innerhalb der Push & Release-Routine (siehe Abschnitt
|
||||
"Standard-Workflow: Push & Release") NICHT automatisch flashen, selbst
|
||||
nach erfolgreichem Build - dort wird bewusst nur gebaut. Grund: Alex
|
||||
möchte das Testgerät auf der bisherigen Version belassen, um das neue
|
||||
Release anschließend über die OTA-Update-Funktion zu testen, statt es
|
||||
direkt per Kabel zu flashen. Für alle anderen Anlässe (normales
|
||||
Entwickeln/Testen, einzelne Fixes) gilt die automatische Flash-Regel
|
||||
unverändert weiter.
|
||||
|
||||
Bitte diese Regel jetzt in die CLAUDE.md-Datei einpflegen.
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
#include <Arduino.h>
|
||||
#include <TFT_eSPI.h>
|
||||
|
||||
// Einfacher Info-Screen: Projektname, Kurzbeschreibung, Copyright-Jahr und
|
||||
// die aktuelle Versionsnummer (Config::APP_VERSION).
|
||||
// Simple info screen: project name, short description, copyright year and
|
||||
// the current version number (Config::APP_VERSION).
|
||||
namespace AboutScreen {
|
||||
void run(TFT_eSPI& tft);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
#include <Arduino.h>
|
||||
#include <TFT_eSPI.h>
|
||||
|
||||
// Sortierbare Liste aller aktuell erkannten Flugzeuge (gleiche Filter wie
|
||||
// das Radar: Reichweite, Bodenfahrzeuge, Airline-Filter). Antippen eines
|
||||
// Eintrags springt zurueck zum Radar mit direkt geoeffnetem Detail-Panel.
|
||||
// Sortable list of all currently detected aircraft (same filters as
|
||||
// the radar: range, ground vehicles, airline filter). Tapping an
|
||||
// entry jumps back to the radar with the detail panel directly open.
|
||||
namespace AircraftListScreen {
|
||||
// Rueckgabewert true = der Nutzer hat ein Flugzeug angetippt (Aufrufer
|
||||
// soll bis zum Radar zurueckspringen, nicht nur die eigene Seite
|
||||
// schliessen). false = nur "Zurueck" gedrueckt, normal weiter im Menue.
|
||||
// Return value true = the user tapped an aircraft (caller
|
||||
// should jump back to the radar, not just close its own page).
|
||||
// false = only "Back" pressed, continue normally in menu.
|
||||
bool run(TFT_eSPI& tft);
|
||||
}
|
||||
|
||||
@@ -4,13 +4,12 @@
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
|
||||
// WICHTIG: Diese Datei ruft absichtlich KEIN AirlineLookup::resolve() mehr auf!
|
||||
// postFetchUpdate() wird vom NetTask auf Core 0 aufgerufen. AirlineLookup
|
||||
// braucht SD-Kartenzugriff, und die SD-Karte wurde in setup() auf Core 1
|
||||
// initialisiert - Zugriff von Core 0 aus fuehrte zu einem Haenger (Task
|
||||
// Watchdog auf IDLE0). Die Aufloesung der Airline-Namen passiert deshalb
|
||||
// jetzt in main.cpp/renderAircraftList() auf Core 1 (demselben Core, der
|
||||
// die SD-Karte urspruenglich initialisiert hat).
|
||||
// IMPORTANT: This file intentionally no longer calls AirlineLookup::resolve()!
|
||||
// postFetchUpdate() is called from NetTask on Core 0. AirlineLookup
|
||||
// needs SD card access, and the SD card was initialized in setup() on Core 1
|
||||
// - access from Core 0 caused a hang (Task Watchdog on IDLE0). The
|
||||
// resolution of airline names now happens in main.cpp/renderAircraftList()
|
||||
// on Core 1 (the same core that originally initialized the SD card).
|
||||
|
||||
namespace AircraftTable {
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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, "?");
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
#include <Arduino.h>
|
||||
#include <TFT_eSPI.h>
|
||||
|
||||
// Display-Helligkeit einstellen (10-100% in 10%-Schritten). Wirkt sich
|
||||
// sofort live auf die Hintergrundbeleuchtung aus, damit man die Aenderung
|
||||
// beim Antippen direkt sieht.
|
||||
// Display brightness setting (10-100% in 10% steps). Affects the
|
||||
// backlight live immediately so you can see the change
|
||||
// directly when tapping.
|
||||
namespace BrightnessScreen {
|
||||
void run(TFT_eSPI& tft);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
#include <TFT_eSPI.h>
|
||||
|
||||
namespace CalibrationScreen {
|
||||
// Blockierend: zeigt 4 Kreise (Ecken), wartet auf Antippen jeweils in
|
||||
// Reihenfolge, berechnet die Kalibrierung und speichert sie auf der
|
||||
// SD-Karte (via TouchInput::saveCalibration()).
|
||||
// Blocking: shows 4 circles (corners), waits for tapping each in
|
||||
// sequence, calculates the calibration and saves it on the
|
||||
// SD card (via TouchInput::saveCalibration()).
|
||||
void run(TFT_eSPI& tft);
|
||||
}
|
||||
60
src/config.h
60
src/config.h
@@ -2,40 +2,40 @@
|
||||
#include <Arduino.h>
|
||||
|
||||
namespace Config {
|
||||
// Wird bei jedem Versions-Release von Karl aktualisiert (siehe
|
||||
// CLAUDE.md-Workflow "Standard-Workflow: Push & Release") - erscheint
|
||||
// im Info-Screen (Menue > System > Info) und muss zum jeweiligen
|
||||
// Git-Tag passen.
|
||||
// Updated by Karl on every version release (see
|
||||
// CLAUDE.md workflow "Standard Workflow: Push & Release") - shown
|
||||
// in the info screen (Menu > System > Info) and must match the
|
||||
// corresponding Git tag.
|
||||
constexpr const char* APP_VERSION = "3.6.3";
|
||||
|
||||
// Display-Helligkeit (Menue > System > Helligkeit), in Prozent.
|
||||
// MIN bewusst nicht 0 - ein komplett dunkles Display koennte sonst wie
|
||||
// ein Defekt wirken statt wie eine Einstellung.
|
||||
// Display brightness (Menu > System > Brightness), in percent.
|
||||
// MIN intentionally not 0 - a completely dark display could otherwise
|
||||
// look like a defect rather than a setting.
|
||||
constexpr uint8_t BRIGHTNESS_MIN_PERCENT = 10;
|
||||
constexpr uint8_t BRIGHTNESS_MAX_PERCENT = 100;
|
||||
constexpr uint8_t BRIGHTNESS_STEP_PERCENT = 10;
|
||||
|
||||
// Bildschirm-Timeout (Menue > System > Bildschirm-Timeout), in Minuten,
|
||||
// per Schieberegler einstellbar (siehe timeout_screen.cpp) - danach
|
||||
// folgt "Nie" (kein Timeout) als eigene Endposition. Vorher nur per
|
||||
// wiederholtem Antippen 0-10 durchklickbar (0 = Nie), was bei z.B. 10
|
||||
// Minuten zehn einzelne Tipps brauchte.
|
||||
// Screen timeout (Menu > System > Screen Timeout), in minutes,
|
||||
// adjustable via slider (see timeout_screen.cpp) - followed by
|
||||
// "Never" (no timeout) as a separate end position. Previously only
|
||||
// clickable 0-10 by repeated tapping (0 = Never), which needed e.g. ten
|
||||
// individual taps for 10 minutes.
|
||||
constexpr uint8_t SCREEN_TIMEOUT_MIN_MINUTES = 1;
|
||||
constexpr uint8_t SCREEN_TIMEOUT_MAX_MINUTES = 15;
|
||||
|
||||
// Nachtmodus (22-6 Uhr) dimmt relativ zur jeweils eingestellten normalen
|
||||
// Helligkeit, nicht auf einen festen Absolutwert - sonst waere der
|
||||
// Dimm-Effekt bei niedrig eingestellter Normalhelligkeit wirkungslos
|
||||
// oder wuerde das Display nachts sogar heller machen als tagsueber.
|
||||
// Night mode (10pm-6am) dims relative to the currently set normal
|
||||
// brightness, not to a fixed absolute value - otherwise the
|
||||
// dimming effect would be useless at low normal brightness
|
||||
// or would even make the display brighter at night than during the day.
|
||||
constexpr uint8_t NIGHT_DIM_REDUCTION_PERCENT = 40;
|
||||
|
||||
constexpr const char* IP_GEO_HOST = "ip-api.com";
|
||||
constexpr const char* IP_GEO_PATH = "/json/?fields=status,lat,lon,offset,countryCode";
|
||||
|
||||
// Adresssuche (AddressSearchScreen) - kostenloser, anmeldefreier
|
||||
// Geokodierungs-Dienst (OpenStreetMap Nominatim). Deren Nutzungsregeln
|
||||
// verlangen einen aussagekraeftigen User-Agent statt des HTTPClient-
|
||||
// Standardwerts, siehe https://operations.osmfoundation.org/policies/nominatim/.
|
||||
// Address search (AddressSearchScreen) - free, no-registration
|
||||
// geocoding service (OpenStreetMap Nominatim). Their usage policies
|
||||
// require a meaningful User-Agent instead of the HTTPClient
|
||||
// default, see https://operations.osmfoundation.org/policies/nominatim/.
|
||||
constexpr const char* NOMINATIM_HOST = "nominatim.openstreetmap.org";
|
||||
constexpr const char* NOMINATIM_USER_AGENT = "EiswolfsFlightradarCYD (github.com/Eiswolf-BG/eiswolfs-flightradar-CYD)";
|
||||
|
||||
@@ -50,9 +50,9 @@ constexpr float RANGE_STEPS_KM[] = {10.0f, 25.0f, 50.0f, 100.0f};
|
||||
constexpr uint8_t RANGE_STEP_COUNT = 4;
|
||||
constexpr uint8_t DEFAULT_RANGE_INDEX = 1;
|
||||
|
||||
// Eco-Mode: reduziert die ADS-B-Abfragefrequenz auf dieses Intervall
|
||||
// (statt FETCH_INTERVAL_MS), wenn der Bildschirm durch Inaktivitaets-
|
||||
// Timeout abgeschaltet ist (siehe main.cpp).
|
||||
// Eco mode: reduces the ADS-B fetch frequency to this interval
|
||||
// (instead of FETCH_INTERVAL_MS), when the screen is turned off by
|
||||
// inactivity timeout (see main.cpp).
|
||||
constexpr uint32_t ECO_FETCH_INTERVAL_MS = 60000;
|
||||
|
||||
constexpr uint8_t ALT_FILTER_COUNT = 3;
|
||||
@@ -63,11 +63,11 @@ constexpr float RANGE_STEPS_KM[] = {10.0f, 25.0f, 50.0f, 100.0f};
|
||||
constexpr uint32_t FETCH_INTERVAL_MS = 8000;
|
||||
constexpr uint32_t HTTP_TIMEOUT_MS = 6000;
|
||||
|
||||
// Wetter-Icon im Header (siehe weather.cpp) - deutlich seltener
|
||||
// abgefragt als die ADS-B-Daten, das Wetter aendert sich nicht
|
||||
// minuetlich und die kostenlose Open-Meteo-API soll nicht unnoetig oft
|
||||
// belastet werden.
|
||||
constexpr uint32_t WEATHER_FETCH_INTERVAL_MS = 600000; // 10 Minuten
|
||||
// Weather icon in the header (see weather.cpp) - queried much less
|
||||
// frequently than ADS-B data, weather does not change
|
||||
// every minute and the free Open-Meteo API should not be needlessly
|
||||
// stressed.
|
||||
constexpr uint32_t WEATHER_FETCH_INTERVAL_MS = 600000; // 10 minutes
|
||||
|
||||
constexpr float DEFAULT_PROXIMITY_ALERT_KM = 8.0f;
|
||||
|
||||
@@ -97,8 +97,8 @@ constexpr float RANGE_STEPS_KM[] = {10.0f, 25.0f, 50.0f, 100.0f};
|
||||
constexpr uint8_t TOUCH_MISO_PIN = 39;
|
||||
constexpr uint8_t TOUCH_IRQ_PIN = 36;
|
||||
|
||||
// Display-Rotation: 0 = normal (USB-Anschluss unten), 2 = 180 Grad
|
||||
// gedreht (USB oben) - fuer Montage mit Ladebuchse oben.
|
||||
// Display rotation: 0 = normal (USB connector at bottom), 2 = 180 degrees
|
||||
// rotated (USB at top) - for mounting with charging port on top.
|
||||
constexpr uint8_t DISPLAY_ROTATION = 2;
|
||||
|
||||
constexpr int16_t SCREEN_WIDTH = 240;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
#include <TFT_eSPI.h>
|
||||
|
||||
namespace FirstRunLocationScreen {
|
||||
// Einmaliger Hinweis-Screen beim allerersten Start (nach der
|
||||
// Sprachauswahl) - erklaert den Genauigkeitsvorteil eines per Adresse
|
||||
// gesetzten Standorts, mit direktem Einstieg in die Adresssuche.
|
||||
// Ueberspringbar.
|
||||
// One-time info screen on very first start (after language selection)
|
||||
// - explains the accuracy advantage of a location set by address,
|
||||
// with direct entry into the address search. Skippable.
|
||||
void run(TFT_eSPI& tft);
|
||||
}
|
||||
|
||||
@@ -15,10 +15,10 @@ namespace {
|
||||
char seenHex[MAX_SEEN][7];
|
||||
uint16_t seenCount = 0;
|
||||
|
||||
// Datei der aktuell laufenden Aufzeichnungs-Sitzung (ohne ".csv"), z.B.
|
||||
// "2026-08-06" fuer die erste Sitzung eines Tages oder "2026-08-06_2"
|
||||
// fuer ein erneutes Einschalten am selben Tag. Leer = noch nicht
|
||||
// aufgeloest (Uhrzeit noch nicht synchronisiert oder Flugbuch aus).
|
||||
// File of the currently active recording session (without ".csv"), e.g.
|
||||
// "2026-08-06" for the first session of a day or "2026-08-06_2"
|
||||
// for powering on again on the same day. Empty = not yet
|
||||
// resolved (clock not yet synced or flight logbook off).
|
||||
char currentSessionFile[16] = {0};
|
||||
|
||||
void formatDateFromEpoch(uint32_t epoch, char* out, size_t outSize) {
|
||||
@@ -28,12 +28,12 @@ namespace {
|
||||
snprintf(out, outSize, "%04d-%02d-%02d", tmv.tm_year + 1900, tmv.tm_mon + 1, tmv.tm_mday);
|
||||
}
|
||||
|
||||
// Findet fuer den gegebenen Aktivierungszeitpunkt eine noch nicht
|
||||
// existierende Logbuch-Datei: "<Datum>.csv" fuer die erste Sitzung
|
||||
// eines Tages, "<Datum>_2.csv", "_3.csv" usw. fuer erneutes Einschalten
|
||||
// am selben Tag - so bekommt jede Sitzung ihre eigene, im
|
||||
// Logbuch-Dateien-Screen einzeln loeschbare Datei, statt in eine
|
||||
// bestehende hineinzuschreiben.
|
||||
// Finds a logbook file that does not yet exist for the given activation
|
||||
// time: "<Date>.csv" for the first session of a day,
|
||||
// "<Date>_2.csv", "_3.csv" etc. for powering on again the same day -
|
||||
// this way each session gets its own file, individually deletable
|
||||
// in the logbook files screen, instead of appending to
|
||||
// an existing one.
|
||||
void resolveSessionFilename(uint32_t epoch, char* out, size_t outSize) {
|
||||
char dateStr[11];
|
||||
formatDateFromEpoch(epoch, dateStr, sizeof(dateStr));
|
||||
@@ -57,8 +57,8 @@ namespace {
|
||||
}
|
||||
}
|
||||
|
||||
// Sehr unwahrscheinlicher Fall (>50 Sitzungen an einem Tag): letzten
|
||||
// Kandidaten weiterverwenden statt endlos zu suchen.
|
||||
// Very unlikely case (>50 sessions in one day): reuse the last
|
||||
// candidate instead of searching indefinitely.
|
||||
strncpy(out, dateStr, outSize - 1);
|
||||
out[outSize - 1] = 0;
|
||||
}
|
||||
@@ -92,7 +92,7 @@ namespace {
|
||||
if (buf[i] == '\n') lines++;
|
||||
}
|
||||
blocksRead++;
|
||||
if (blocksRead % 8 == 0) delay(1);
|
||||
if (blocksRead % 8 == 0) delay(1); // yield to prevent watchdog timeout during bulk SD reads
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
@@ -141,17 +141,17 @@ namespace {
|
||||
}
|
||||
}
|
||||
blocksRead++;
|
||||
if (blocksRead % 8 == 0) delay(1);
|
||||
if (blocksRead % 8 == 0) delay(1); // yield during bulk SD reads
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
|
||||
// Loest die Datei der aktuellen Sitzung auf (einmalig pro Sitzung) bzw.
|
||||
// uebernimmt sie nach einem Neustart erneut aus den Einstellungen -
|
||||
// nur aufrufen, wenn das Flugbuch gerade eingeschaltet ist.
|
||||
// Resolves the current session file (once per session) or
|
||||
// adopts it again from settings after a restart -
|
||||
// only call when the flight logbook is currently enabled.
|
||||
void ensureSessionFile() {
|
||||
time_t now = time(nullptr);
|
||||
if (now <= 8 * 3600 * 2) return; // Uhrzeit noch nicht synchronisiert
|
||||
if (now <= 8 * 3600 * 2) return; // epoch still below reasonable minimum = RTC not yet synced
|
||||
|
||||
String persisted = SettingsStore::flightLogbookSessionFile();
|
||||
if (persisted.length() > 0) {
|
||||
@@ -163,10 +163,10 @@ namespace {
|
||||
return;
|
||||
}
|
||||
|
||||
// Keine Sitzungsdatei hinterlegt - entweder frisches Einschalten
|
||||
// (menu_screen.cpp loescht den Eintrag beim Umschalten bewusst) oder
|
||||
// Migration von einer alten Firmware ohne Sitzungslogik. In beiden
|
||||
// Faellen jetzt eine neue, garantiert einzigartige Datei anlegen.
|
||||
// No session file stored - either a fresh power-on
|
||||
// (menu_screen.cpp deliberately clears the entry when toggling) or
|
||||
// migration from an older firmware without session logic. In both
|
||||
// cases, create a new, guaranteed unique file now.
|
||||
uint32_t enabledAt = SettingsStore::flightLogbookEnabledAtEpoch();
|
||||
if (enabledAt == 0) enabledAt = (uint32_t)now;
|
||||
resolveSessionFilename(enabledAt, currentSessionFile, sizeof(currentSessionFile));
|
||||
@@ -204,17 +204,17 @@ void init() {
|
||||
void update() {
|
||||
if (!SettingsStore::flightLogbookEnabled()) return;
|
||||
|
||||
// 24h-Sicherheitsabschaltung: verhindert, dass ein unbemerkt aktives
|
||||
// Flugbuch die SD-Karte nach und nach vollschreibt (siehe
|
||||
// Bestaetigungsdialog beim Einschalten in menu_screen.cpp). Nur pruefen,
|
||||
// wenn die Uhrzeit schon synchronisiert ist.
|
||||
// 24h safety shutoff: prevents an unnoticed active
|
||||
// flight logbook from filling up the SD card over time (see
|
||||
// confirmation dialog when enabling in menu_screen.cpp). Only check
|
||||
// when the clock has already synced.
|
||||
time_t nowCheck = time(nullptr);
|
||||
if (nowCheck > 8 * 3600 * 2) {
|
||||
uint32_t enabledAt = SettingsStore::flightLogbookEnabledAtEpoch();
|
||||
if (enabledAt == 0) {
|
||||
// Migrations-Fall: Flugbuch war schon vor diesem Update aktiv
|
||||
// (alte Einstellungsdatei ohne Zeitstempel) - Startzeitpunkt
|
||||
// jetzt setzen, damit die 24h-Grenze trotzdem sicher greift.
|
||||
// Migration case: flight logbook was already active before this update
|
||||
// (old settings file without timestamp) - set start time
|
||||
// now so that the 24h limit still works safely.
|
||||
SettingsStore::setFlightLogbookEnabledAtEpoch((uint32_t)nowCheck);
|
||||
} else if ((uint32_t)nowCheck >= enabledAt &&
|
||||
(uint32_t)nowCheck - enabledAt >= 24UL * 3600UL) {
|
||||
@@ -274,7 +274,7 @@ TopAltitude todayMaxAltitude() {
|
||||
TopAltitude result;
|
||||
|
||||
SdMutex::Guard guard;
|
||||
if (currentSessionFile[0] == 0) return result; // noch keine Sitzungsdatei bekannt
|
||||
if (currentSessionFile[0] == 0) return result; // no session file known yet
|
||||
|
||||
char filename[64];
|
||||
logFilename(filename, sizeof(filename));
|
||||
@@ -287,10 +287,10 @@ TopAltitude todayMaxAltitude() {
|
||||
uint32_t lineIdx = 0;
|
||||
while (f.available()) {
|
||||
String line = f.readStringUntil('\n');
|
||||
if (firstLine) { firstLine = false; continue; } // CSV-Header ueberspringen
|
||||
if (firstLine) { firstLine = false; continue; } // skip CSV header
|
||||
if (line.length() == 0) continue;
|
||||
|
||||
// Spalten: timestamp,hex,callsign,reg,type,distance_km,altitude_ft
|
||||
// Columns: timestamp,hex,callsign,reg,type,distance_km,altitude_ft
|
||||
int commaIdx[6];
|
||||
int found = 0;
|
||||
int searchFrom = 0;
|
||||
@@ -301,7 +301,7 @@ TopAltitude todayMaxAltitude() {
|
||||
searchFrom = idx + 1;
|
||||
found++;
|
||||
}
|
||||
if (found < 6) continue; // unvollstaendige/kaputte Zeile ueberspringen
|
||||
if (found < 6) continue; // skip incomplete/corrupt line
|
||||
|
||||
String callsign = line.substring(commaIdx[1] + 1, commaIdx[2]);
|
||||
String altStr = line.substring(commaIdx[5] + 1);
|
||||
@@ -383,9 +383,9 @@ uint8_t listDays(DayEntry* out, uint8_t maxEntries) {
|
||||
}
|
||||
|
||||
uint8_t listDaySummaries(DayEntry* out, uint8_t maxEntries) {
|
||||
// Scannt grosszuegiger als maxEntries, damit auch bei vielen einzelnen
|
||||
// Sitzungs-Dateien pro Tag noch korrekt pro Kalendertag aufsummiert
|
||||
// wird, bevor auf die angeforderte Anzahl Tage begrenzt wird.
|
||||
// Scans more generously than maxEntries, so that even with many individual
|
||||
// session files per day it still correctly sums per calendar day
|
||||
// before limiting to the requested number of days.
|
||||
constexpr uint8_t MAX_RAW_SCAN = 90;
|
||||
static DayEntry raw[MAX_RAW_SCAN];
|
||||
uint8_t rawCount = listDays(raw, MAX_RAW_SCAN);
|
||||
@@ -421,9 +421,9 @@ bool deleteFile(const char* label) {
|
||||
|
||||
bool ok = SD.remove(path);
|
||||
if (ok && strcmp(label, currentSessionFile) == 0) {
|
||||
// Die gerade aktive Sitzungsdatei wurde geloescht - Dopplungs-Liste
|
||||
// zuruecksetzen, damit neue Sichtungen wieder korrekt in die (beim
|
||||
// naechsten Schreibvorgang neu angelegte) Datei geloggt werden.
|
||||
// The currently active session file was deleted - reset the
|
||||
// deduplication list so that new sightings are correctly logged
|
||||
// into the (on the next write newly created) file.
|
||||
seenCount = 0;
|
||||
}
|
||||
return ok;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
166
src/i18n.h
166
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,
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
|
||||
// Steuert die diskrete RGB-LED auf der Rueckseite des CYD (kein Lautsprecher
|
||||
// vorhanden, daher ersetzt die LED den akustischen Alarm vom Cardputer-
|
||||
// Projekt). Pins: Rot=GPIO4, Gruen=GPIO16, Blau=GPIO17, active-low (LOW = an).
|
||||
// Controls the discrete RGB LED on the back of the CYD (no speaker
|
||||
// present, therefore the LED replaces the acoustic alarm from the Cardputer
|
||||
// project). Pins: Red=GPIO4, Green=GPIO16, Blue=GPIO17, active-low (LOW = on).
|
||||
namespace LedAlert {
|
||||
|
||||
enum class Mode {
|
||||
@@ -18,9 +18,9 @@ namespace LedAlert {
|
||||
|
||||
void pulseHeartbeat(uint32_t nowMs);
|
||||
|
||||
// Blockierendes, kurzes weisses Aufblitzen (alle 3 Farbkanaele an) als
|
||||
// sofortige Bestaetigung fuer eine einmalige Nutzeraktion (z.B. "Cam"-
|
||||
// Button getroffen). Bewusst blockierend (delay), da nur aus dem
|
||||
// Haupt-Loop bei einem Tap aufgerufen, nicht aus der NetTask-Schleife.
|
||||
// Blocking, short white flash (all 3 color channels on) as
|
||||
// immediate confirmation for a one-time user action (e.g. "Cam"
|
||||
// button hit). Deliberately blocking (delay), since only called from the
|
||||
// main loop on a tap, not from the NetTask loop.
|
||||
void flashWhite(uint32_t durationMs = 150);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,8 +135,8 @@ 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
|
||||
// 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);
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include <TFT_eSPI.h>
|
||||
|
||||
namespace LogbookFilesScreen {
|
||||
// Blockierend: listet die Logbuch-Dateien (ein Tag pro Zeile, mit Anzahl
|
||||
// Eintraegen) auf. Kehrt zurueck, sobald "Back" angetippt wird.
|
||||
// Blocking: lists the logbook files (one day per line, with number
|
||||
// of entries). Returns as soon as "Back" is tapped.
|
||||
void run(TFT_eSPI& tft);
|
||||
}
|
||||
242
src/main.cpp
242
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()) {
|
||||
char timeBuf[9];
|
||||
snprintf(timeBuf, sizeof(timeBuf), "%02d:%02d", tmNow.tm_hour, tmNow.tm_min);
|
||||
} else {
|
||||
int hour12 = tmNow.tm_hour % 12;
|
||||
if (hour12 == 0) hour12 = 12;
|
||||
snprintf(timeBuf, sizeof(timeBuf), "%d:%02d%s", hour12, tmNow.tm_min,
|
||||
tmNow.tm_hour < 12 ? "AM" : "PM");
|
||||
}
|
||||
|
||||
constexpr int16_t CLOCK_BAND_H = 40;
|
||||
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();
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
#include <TFT_eSPI.h>
|
||||
|
||||
namespace MenuScreen {
|
||||
// Blockierend: einfaches Menue (Kalibrierung, Anzeige-Invertierung,
|
||||
// WLAN-Verwaltung, Alarm-Toggles, Statistik, Logbuch-Dateien).
|
||||
// Kehrt zurueck, sobald "Zurueck" angetippt wird.
|
||||
// Blocking: simple menu (calibration, display inversion,
|
||||
// WiFi management, alert toggles, statistics, logbook files).
|
||||
// Returns as soon as "Back" is tapped.
|
||||
void run(TFT_eSPI& tft);
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
#pragma once
|
||||
#include <TFT_eSPI.h>
|
||||
|
||||
// Gemeinsames Sterne-Twinkle-Modul fuer ALLE Menue-/Untermenue-Bildschirme
|
||||
// (schwarzer Hintergrund) sowie den Splash-Screen - dieselbe Optik wie im
|
||||
// Flugzeug-Detail-Panel des Radars.
|
||||
// Common star twinkle module for ALL menu/submenu screens
|
||||
// (black background) as well as the splash screen - same look as in the
|
||||
// aircraft detail panel of the radar.
|
||||
namespace MenuStars {
|
||||
// Verteilt die Sterne neu ueber den ganzen Bildschirm - einmal beim
|
||||
// Betreten eines neuen Screens aufrufen (NICHT bei jedem Redraw
|
||||
// innerhalb desselben Screens, sonst "springt" die Animation staendig).
|
||||
// Redistributes the stars across the entire screen - call once when
|
||||
// entering a new screen (NOT on every redraw within the same
|
||||
// screen, otherwise the animation keeps "jumping").
|
||||
void reset();
|
||||
|
||||
// In der Warte-/Idle-Schleife eines Screens aufrufen (bei jedem
|
||||
// Schleifendurchlauf ist ok - die Funktion drosselt sich intern selbst
|
||||
// auf ca. alle 60ms, um das Display nicht unnoetig oft anzusprechen).
|
||||
// Call in the wait/idle loop of a screen (every
|
||||
// loop iteration is fine - the function internally throttles itself
|
||||
// to roughly every 60ms to avoid needlessly addressing the display).
|
||||
void update(TFT_eSPI& tft);
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
|
||||
// Hintergrund-Task fuer WLAN-Status, Standortbestimmung und ADS-B-Abfragen.
|
||||
// Laeuft auf Core 0, damit Core 1 (Rendering + Touch im main-Loop) nie durch
|
||||
// Netzwerk-Wartezeiten (WLAN, HTTPS) blockiert wird.
|
||||
// Background task for WiFi status, geolocation and ADS-B queries.
|
||||
// Runs on Core 0 so that Core 1 (rendering + touch in main loop) is never
|
||||
// blocked by network wait times (WiFi, HTTPS).
|
||||
namespace NetTask {
|
||||
void begin();
|
||||
void pause();
|
||||
void resume();
|
||||
|
||||
// Eco-Mode: aendert das ADS-B-Abfrage-Intervall zur Laufzeit.
|
||||
// Wird von main.cpp beim Bildschirm-Timeout (screenDimmed) auf
|
||||
// Config::ECO_FETCH_INTERVAL_MS und beim Aufwachen wieder auf
|
||||
// Config::FETCH_INTERVAL_MS gesetzt.
|
||||
// Eco mode: changes the ADS-B fetch interval at runtime.
|
||||
// Set by main.cpp on screen timeout (screenDimmed) to
|
||||
// Config::ECO_FETCH_INTERVAL_MS and on wakeup back to
|
||||
// Config::FETCH_INTERVAL_MS.
|
||||
void setFetchInterval(uint32_t ms);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
|
||||
// Firmware-Update ueber WLAN (Menue > System > "Nach Update suchen") -
|
||||
// prueft gegen die neueste Version im GitHub-Repository (Releases-API) und
|
||||
// installiert sie bei Bestaetigung direkt auf dem Geraet, ohne Kabel/
|
||||
// Web-Flasher. Blockierende, synchrone HTTPS-Aufrufe - werden nur bei
|
||||
// explizitem Tastendruck ausgefuehrt (kein Hintergrund-Polling), analog zu
|
||||
// AircraftDetails' Modell-/Routen-Abfragen, nur eben auf Core 1 statt
|
||||
// Core 0, da hier direkt auf Nutzer-Interaktion reagiert wird.
|
||||
// Firmware update over WiFi (Menu > System > "Check for update") -
|
||||
// checks against the latest version in the GitHub repository (Releases API) and
|
||||
// installs it on the device directly upon confirmation, without cable/
|
||||
// web flasher. Blocking, synchronous HTTPS calls - only executed on
|
||||
// explicit button press (no background polling), analogous to
|
||||
// AircraftDetails' model/route queries, just on Core 1 instead of
|
||||
// Core 0, since it reacts directly to user interaction.
|
||||
namespace OtaUpdate {
|
||||
|
||||
enum class CheckResult { Error, UpToDate, UpdateAvailable };
|
||||
|
||||
struct CheckInfo {
|
||||
CheckResult result = CheckResult::Error;
|
||||
// Neueste verfuegbare Version OHNE "v"-Praefix (z.B. "3.5.0") -
|
||||
// sowohl bei UpToDate als auch bei UpdateAvailable gesetzt, damit
|
||||
// der aufrufende Screen sie in beiden Faellen anzeigen kann.
|
||||
// Latest available version WITHOUT "v" prefix (e.g. "3.5.0") -
|
||||
// set for both UpToDate and UpdateAvailable, so
|
||||
// the calling screen can display it in both cases.
|
||||
char latestVersion[16] = {0};
|
||||
// Direkter Download-Link zur firmware.bin des Releases - nur
|
||||
// gesetzt, wenn result == UpdateAvailable.
|
||||
// Direct download link to the firmware.bin of the release - only
|
||||
// set when result == UpdateAvailable.
|
||||
char downloadUrl[192] = {0};
|
||||
};
|
||||
|
||||
// Fragt die GitHub-Releases-API nach dem neuesten Release ab, vergleicht
|
||||
// dessen Versionsnummer (Tag-Name, "v"-Praefix wird ignoriert) gegen
|
||||
// Config::APP_VERSION und sucht im Release den Anhang "firmware.bin".
|
||||
// CheckResult::Error bei jedem Fehler unterwegs (kein WLAN, Zeitueber-
|
||||
// schreitung, unerwartetes JSON, kein firmware.bin im Release).
|
||||
// Queries the GitHub Releases API for the latest release, compares
|
||||
// its version number (tag name, "v" prefix is ignored) against
|
||||
// Config::APP_VERSION and looks for the "firmware.bin" attachment in the release.
|
||||
// CheckResult::Error on any error along the way (no WiFi, timeout,
|
||||
// unexpected JSON, no firmware.bin in the release).
|
||||
CheckInfo checkForUpdate();
|
||||
|
||||
// Laedt die firmware.bin von 'url' herunter und flasht sie (ueber die
|
||||
// Standard-Arduino-HTTPUpdate-Bibliothek). Ruft bei Erfolg BEWUSST
|
||||
// NICHT selbst ESP.restart() auf - der Aufrufer zeigt zuerst eine
|
||||
// kurze Erfolgsmeldung an und startet danach selbst neu. onProgress
|
||||
// wird waehrend des Downloads wiederholt mit 0-100 (Prozent) aufgerufen
|
||||
// (darf nullptr sein). Gibt false zurueck, wenn Download oder Flash-
|
||||
// Vorgang fehlschlagen - das Geraet laeuft in dem Fall unveraendert mit
|
||||
// der bisherigen Firmware weiter.
|
||||
// Downloads the firmware.bin from 'url' and flashes it (via the
|
||||
// standard Arduino HTTPUpdate library). Intentionally does NOT call
|
||||
// ESP.restart() itself on success - the caller shows a
|
||||
// brief success message first and then restarts itself. onProgress
|
||||
// is called repeatedly during download with 0-100 (percent)
|
||||
// (may be nullptr). Returns false if download or flash
|
||||
// process fails - the device continues running unchanged with
|
||||
// the previous firmware in that case.
|
||||
bool performUpdate(const char* url, void (*onProgress)(uint8_t percent));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
|
||||
// Gemeinsames Lock fuer ALLE SD-Kartenzugriffe im gesamten Projekt. Die
|
||||
// SD-Bibliothek ist nicht thread-sicher: ohne dieses Lock koennen
|
||||
// gleichzeitige Zugriffe von NetTask (Core 0, z.B. Flugbuch-Eintrag
|
||||
// schreiben alle 8s) und dem Haupt-Loop (Core 1, z.B. Einstellungen im
|
||||
// Menue speichern) das Geraet einfrieren, wenn sie zeitlich zusammentreffen.
|
||||
// Rekursiv, damit verschachtelte Aufrufe (z.B. FlightLogbook::update() ruft
|
||||
// intern ensureCurrentDate() auf, das seinerseits auch sperrt) nicht
|
||||
// blockieren.
|
||||
// Common lock for ALL SD card access in the entire project. The
|
||||
// SD library is not thread-safe: without this lock,
|
||||
// concurrent access by NetTask (Core 0, e.g. writing a logbook entry
|
||||
// every 8s) and the main loop (Core 1, e.g. saving settings in the
|
||||
// menu) can freeze the device if they coincide in time.
|
||||
// Recursive, so that nested calls (e.g. FlightLogbook::update() calls
|
||||
// ensureCurrentDate() internally, which also locks) do not
|
||||
// block.
|
||||
namespace SdMutex {
|
||||
void init();
|
||||
void lock();
|
||||
void unlock();
|
||||
|
||||
// RAII-Helfer: sperrt im Konstruktor, gibt im Destruktor frei - so kann
|
||||
// man das Lock nicht versehentlich vergessen freizugeben (z.B. bei
|
||||
// einem fruehen "return").
|
||||
// RAII helper: locks in constructor, unlocks in destructor - so you cannot
|
||||
// accidentally forget to release the lock (e.g. on an early "return").
|
||||
class Guard {
|
||||
public:
|
||||
Guard() { lock(); }
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -2,21 +2,20 @@
|
||||
#include <Arduino.h>
|
||||
|
||||
namespace SettingsBackup {
|
||||
// onStep (falls angegeben) wird bei jedem der beiden Kopiervorgaenge
|
||||
// (erst Einstellungen, dann WLAN-Zugangsdaten) direkt VOR dem
|
||||
// jeweiligen Kopieren aufgerufen - der aufrufende Screen
|
||||
// (menu_screen.cpp) nutzt das, um waehrend des SD-bedingt spuerbar
|
||||
// langsamen Sicherns/Wiederherstellens Fortschrittspunkte auf dem
|
||||
// Button anzuzeigen, statt dass der Button eingefroren wirkt.
|
||||
// onStep (if provided) is called before each of the two copy operations
|
||||
// (first settings, then WiFi credentials) - the calling screen
|
||||
// (menu_screen.cpp) uses this to show progress dots on the button during
|
||||
// the SD-card-related noticeably slow backup/restore, instead of the
|
||||
// button appearing frozen.
|
||||
bool backup(void (*onStep)() = nullptr);
|
||||
bool restore(void (*onStep)() = nullptr);
|
||||
bool hasBackup();
|
||||
|
||||
// Loescht den kompletten Flightradar-Ordner von der SD-Karte und
|
||||
// startet das Geraet neu - beim naechsten Boot laeuft dadurch wieder
|
||||
// die komplette Ersteinrichtung (siehe main.cpp: isFirstRun-Erkennung
|
||||
// ueber Config::SD_SETTINGS_FILE). Kehrt nur im Fehlerfall zurueck
|
||||
// (z.B. SD nicht eingehaengt) - im Erfolgsfall startet das Geraet
|
||||
// neu, bevor die Funktion "zurueckkehrt" (ESP.restart()).
|
||||
// Deletes the entire Flightradar folder from the SD card and
|
||||
// restarts the device - on the next boot, the complete first-time
|
||||
// setup will run again (see main.cpp: isFirstRun detection via
|
||||
// Config::SD_SETTINGS_FILE). Only returns on error (e.g. SD not mounted)
|
||||
// - on success, the device restarts before the function "returns"
|
||||
// (ESP.restart()).
|
||||
bool factoryReset();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
#include <TFT_eSPI.h>
|
||||
|
||||
namespace StatsScreen {
|
||||
// Blockierend: zeigt einfache Statistiken aus dem Flight Logbook
|
||||
// (heute gesehen, insgesamt gesehen, Anzahl Tage geloggt). Kehrt zurueck,
|
||||
// sobald "Back" angetippt wird.
|
||||
// Blocking: shows simple statistics from the Flight Logbook
|
||||
// (seen today, total seen, number of days logged). Returns as soon
|
||||
// as "Back" is tapped.
|
||||
void run(TFT_eSPI& tft);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
|
||||
// Berechnet Sonnenauf-/untergang (lokale Zeit) fuer einen gegebenen Ort und
|
||||
// Tag - Grundlage fuer die standortbasierte Nachtdimmung (siehe main.cpp::
|
||||
// isNightDimHours()), die das vorherige feste 22:00-06:00-Fenster ersetzt.
|
||||
// Calculates sunrise/sunset (local time) for a given location and
|
||||
// day - basis for the location-based night dimming (see main.cpp::
|
||||
// isNightDimHours()), which replaces the previous fixed 22:00-06:00 window.
|
||||
namespace SunTimes {
|
||||
|
||||
struct Result {
|
||||
bool valid = false; // false = Standort noch unbekannt (lat/lon == 0,0)
|
||||
bool alwaysDay = false; // Polartag: Sonne geht heute an diesem Ort nicht unter
|
||||
bool alwaysNight = false; // Polarnacht: Sonne geht heute an diesem Ort nicht auf
|
||||
float sunriseHour = 0; // Lokale Dezimalstunde (0-24), nur gueltig wenn weder
|
||||
float sunsetHour = 0; // alwaysDay noch alwaysNight gesetzt ist.
|
||||
bool valid = false; // false = location still unknown (lat/lon == 0,0)
|
||||
bool alwaysDay = false; // polar day: sun does not set today at this location
|
||||
bool alwaysNight = false; // polar night: sun does not rise today at this location
|
||||
float sunriseHour = 0; // local decimal hour (0-24), only valid if neither
|
||||
float sunsetHour = 0; // alwaysDay nor alwaysNight is set.
|
||||
};
|
||||
|
||||
// year/month/day sind lokale Kalenderdatumswerte (z.B. aus localtime_r()),
|
||||
// utcOffsetSeconds die bekannte Zeitzonenverschiebung (siehe
|
||||
// LocationManager::utcOffsetSeconds()). Nutzt den klassischen "Almanac"-
|
||||
// Sonnenauf-/untergangsalgorithmus (Sonnenzenit 90.833 Grad, beruecksichtigt
|
||||
// atmosphaerische Refraktion) - Genauigkeit liegt typischerweise im Bereich
|
||||
// weniger Minuten, fuer eine sanfte Backlight-Dimmung mehr als ausreichend.
|
||||
// year/month/day are local calendar date values (e.g. from localtime_r()),
|
||||
// utcOffsetSeconds is the known timezone offset (see
|
||||
// LocationManager::utcOffsetSeconds()). Uses the classic "Almanac"
|
||||
// sunrise/sunset algorithm (solar zenith 90.833 degrees, accounting for
|
||||
// atmospheric refraction) - accuracy is typically within a few minutes,
|
||||
// more than sufficient for smooth backlight dimming.
|
||||
Result compute(double lat, double lon, int year, int month, int day, int32_t utcOffsetSeconds);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
#include <Arduino.h>
|
||||
#include <TFT_eSPI.h>
|
||||
|
||||
// Bildschirm-Timeout einstellen (Menue > System > Bildschirm-Timeout) -
|
||||
// Schieberegler von Config::SCREEN_TIMEOUT_MIN_MINUTES bis
|
||||
// Config::SCREEN_TIMEOUT_MAX_MINUTES, danach "Nie" als eigene Endposition.
|
||||
// Der Ruhebildschirm-Umschalter (siehe SettingsStore::screensaverEnabled())
|
||||
// lebt ebenfalls hier, direkt unter dem Regler, da er inhaltlich eng mit
|
||||
// dem Timeout zusammenhaengt (bestimmt nur, WAS beim Timeout passiert).
|
||||
// Screen timeout setting (Menu > System > Screen Timeout) -
|
||||
// Slider from Config::SCREEN_TIMEOUT_MIN_MINUTES to
|
||||
// Config::SCREEN_TIMEOUT_MAX_MINUTES, then "Never" as a separate end position.
|
||||
// The screensaver toggle (see SettingsStore::screensaverEnabled())
|
||||
// also lives here, directly below the slider, since it is closely related to
|
||||
// the timeout (only determines WHAT happens on timeout).
|
||||
namespace TimeoutScreen {
|
||||
void run(TFT_eSPI& tft);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
// Automatisch generierter Font (DejaVu Sans Mono, 11px) fuer TFT_eSPI GFXFF.
|
||||
// Deckt Basis-ASCII + Umlaute/Akzente fuer DE/TR/FR/ES/IT ab (U+0020-U+015F).
|
||||
// NICHT von Hand bearbeiten - siehe Kommentar in i18n.h fuer den Generator-Hinweis.
|
||||
// Auto-generated font (DejaVu Sans Mono, 11px) for TFT_eSPI GFXFF.
|
||||
// Covers basic ASCII + umlauts/accents for DE/TR/FR/ES/IT (U+0020-U+015F).
|
||||
// DO NOT edit manually - see comment in i18n.h for the generator hint.
|
||||
#include <pgmspace.h>
|
||||
|
||||
const uint8_t UiFont11ptBitmaps[] PROGMEM = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
|
||||
// Sehr einfache Wetteranzeige (Icon im Header, dort wo frueher der
|
||||
// Cam-Button war) - fragt periodisch die aktuelle Wetterlage fuer den
|
||||
// gerade aktiven Standort ab (also inkl. aktivem Standort-Preset, siehe
|
||||
// LocationManager::getHomeLocation() - wenn dort z.B. Mailand ausgewaehlt
|
||||
// ist, zeigt das Icon das Wetter in Mailand). Nutzt die kostenlose
|
||||
// Open-Meteo-API (kein API-Key noetig).
|
||||
// Very simple weather display (icon in the header, where the camera button
|
||||
// used to be) - periodically queries the current weather for the currently
|
||||
// active location (including active location preset, see
|
||||
// LocationManager::getHomeLocation() - if e.g. Milan is selected there,
|
||||
// the icon shows the weather in Milan). Uses the free Open-Meteo API (no
|
||||
// API key needed).
|
||||
namespace Weather {
|
||||
enum class Condition {
|
||||
Unknown, // noch keine erfolgreiche Abfrage - Icon zeigt nichts an
|
||||
Unknown, // no successful query yet - icon shows nothing
|
||||
Clear,
|
||||
PartlyCloudy,
|
||||
Cloudy,
|
||||
@@ -18,16 +18,14 @@ namespace Weather {
|
||||
Thunderstorm
|
||||
};
|
||||
|
||||
// Muss regelmaessig aus dem NetTask (Core 0) aufgerufen werden - kuemmert
|
||||
// sich intern um das Abfrage-Intervall (Config::WEATHER_FETCH_INTERVAL_MS)
|
||||
// und erkennt eine Standort-Aenderung (z.B. anderes aktives Preset), um
|
||||
// dann sofort neu abzufragen statt bis zum naechsten reguleren Intervall
|
||||
// zu warten.
|
||||
// Must be called regularly from NetTask (Core 0) - handles the query
|
||||
// interval internally (Config::WEATHER_FETCH_INTERVAL_MS) and detects
|
||||
// location changes (e.g. different active preset) to immediately re-query
|
||||
// instead of waiting for the next regular interval.
|
||||
void update();
|
||||
|
||||
// Aktuell bekannte Wetterlage, threadsicher genug fuer diesen Zweck (ein
|
||||
// einzelnes uint8-artiges Enum, das nur vom NetTask geschrieben und vom
|
||||
// UI-Thread gelesen wird - kein Lock noetig, ein kurzfristig veraltetes
|
||||
// Lesen ist unkritisch).
|
||||
// Currently known weather condition, thread-safe enough for this purpose (a
|
||||
// single uint8-like enum, only written by NetTask and read by the UI thread
|
||||
// - no lock needed, a briefly stale read is harmless).
|
||||
Condition current();
|
||||
}
|
||||
|
||||
@@ -19,9 +19,9 @@ namespace {
|
||||
|
||||
WebServer server(80);
|
||||
|
||||
// Nur reine Dateinamen/Labels aus Formularfeldern akzeptieren - kein
|
||||
// "/" und kein ".." - damit ueber die WebUI kein Ausbruch aus dem
|
||||
// jeweiligen SD-Verzeichnis moeglich ist (Pfad-Traversal).
|
||||
// Only accept plain filenames/labels from form fields - no
|
||||
// "/" and no ".." - so that no escape from the respective
|
||||
// SD directory is possible via the WebUI (path traversal).
|
||||
bool isSafeName(const String& name) {
|
||||
if (name.length() == 0 || name.length() > 40) return false;
|
||||
if (name.indexOf('/') >= 0 || name.indexOf('\\') >= 0) return false;
|
||||
@@ -29,14 +29,14 @@ namespace {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Absichtliche Duplikate der gleichnamigen (lokalen/statischen) Funktionen
|
||||
// aus radar_screen.cpp - dort nicht exportiert, und nach der im Projekt
|
||||
// etablierten Konvention "jeder Screen/jedes Modul dupliziert seine
|
||||
// eigenen kleinen Helfer statt eines gemeinsamen Moduls" (siehe z.B.
|
||||
// timeout_screen.cpp) bewusst hier erneut definiert statt radar_screen.cpp
|
||||
// umzubauen. Bei Aenderungen an der Logik in radar_screen.cpp bitte diese
|
||||
// Kopie hier synchron halten, damit das WebUI-Radar dieselben Ringe/
|
||||
// Markierungen zeigt wie das Geraete-Display.
|
||||
// Intentional duplicates of the same-named (local/static) functions
|
||||
// from radar_screen.cpp - not exported there, and following the project's
|
||||
// established convention "each screen/module duplicates its own small
|
||||
// helpers instead of a shared module" (see e.g. timeout_screen.cpp),
|
||||
// deliberately redefined here instead of refactoring radar_screen.cpp.
|
||||
// If the logic in radar_screen.cpp changes, please keep this copy
|
||||
// in sync so that the WebUI radar shows the same rings/markers as the
|
||||
// device display.
|
||||
bool isEmergencySquawkWeb(const char* squawk) {
|
||||
if (!squawk[0]) return false;
|
||||
for (uint8_t i = 0; i < Config::EMERGENCY_SQUAWK_COUNT; i++) {
|
||||
@@ -45,15 +45,15 @@ namespace {
|
||||
return false;
|
||||
}
|
||||
|
||||
// "Auffaellig" (oranger Ring) beschraenkt sich aktuell auf gar nichts -
|
||||
// der Teil ueber Militaer-/Regierungs-Rufzeichen-Praefixe
|
||||
// (Config::NOTABLE_CALLSIGN_PREFIXES) ist NICHT umgesetzt, weil diese
|
||||
// Konstante nirgends im Projekt existiert (auch nicht in
|
||||
// radar_screen.cpp) - das zugehoerige Feature wurde bisher nie
|
||||
// tatsaechlich spezifiziert/implementiert. "notable" wird deshalb unten
|
||||
// in handleRadarJson() fest auf false gesetzt. Sobald es eine echte
|
||||
// Praefixliste gibt, hier eine isNotableCallsignWeb()-Funktion analog zu
|
||||
// isEmergencySquawkWeb() ergaenzen.
|
||||
// "Notable" (orange ring) is currently limited to nothing at all -
|
||||
// the part about military/government callsign prefixes
|
||||
// (Config::NOTABLE_CALLSIGN_PREFIXES) is NOT implemented, because this
|
||||
// constant does not exist anywhere in the project (not even in
|
||||
// radar_screen.cpp) - the associated feature has never actually been
|
||||
// specified/implemented. "notable" is therefore hardcoded to false
|
||||
// below in handleRadarJson(). As soon as there is a real prefix list,
|
||||
// add an isNotableCallsignWeb() function analogous to
|
||||
// isEmergencySquawkWeb() here.
|
||||
bool isHeavyCategoryWeb(const char* category) {
|
||||
return category[0] == 'A' && category[1] == '5';
|
||||
}
|
||||
@@ -73,10 +73,9 @@ namespace {
|
||||
html += "button{background:#0a0f0d;color:#ff3b3b;border:1px solid #ff3b3b;border-radius:4px;padding:3px 10px;font-family:inherit;cursor:pointer;}";
|
||||
html += "button:hover{background:#ff3b3b;color:#0a0f0d;}";
|
||||
html += ".dl{color:#39ff14;text-decoration:none;border:1px solid #39ff14;border-radius:4px;padding:3px 10px;margin-right:6px;display:inline-block;}";
|
||||
// Gruen statt Rot fuer "Hinzufuegen"-Buttons - die roten button{}-Regeln
|
||||
// oben bleiben fuer alle destruktiven "Entfernen/Loeschen"-Buttons
|
||||
// unveraendert, .addbtn ist ausschliesslich fuer die neuen Listen-
|
||||
// Formulare (siehe handleLists()) gedacht.
|
||||
// Green instead of red for "Add" buttons - the red button{} rules
|
||||
// above stay unchanged for all destructive "Remove/Delete" buttons.
|
||||
// .addbtn is exclusively for the new list forms (see handleLists()).
|
||||
html += ".addbtn{background:#0a0f0d;color:#39ff14;border:1px solid #39ff14;border-radius:4px;padding:3px 10px;font-family:inherit;cursor:pointer;}";
|
||||
html += ".addbtn:hover{background:#39ff14;color:#0a0f0d;}";
|
||||
html += "input[type=text]{background:#0a0f0d;color:#39ff14;border:1px solid #39ff14;border-radius:4px;padding:5px 8px;font-family:inherit;}";
|
||||
@@ -92,19 +91,19 @@ namespace {
|
||||
return html;
|
||||
}
|
||||
|
||||
// Live-Radar-Ansicht fuer die Startseite: ein <canvas>, das per JavaScript
|
||||
// alle paar Sekunden /radar.json abruft und die Flugzeuge polar (Peilung/
|
||||
// Distanz, genau wie auf dem Geraete-Display) zeichnet. Bewusst per
|
||||
// fetch()-Polling statt WebSocket/SSE gehalten - deutlich weniger Code
|
||||
// und Speicherbedarf auf dem ESP32, und fuer eine gelegentlich vom Handy
|
||||
// aus aufgerufene Seite voellig ausreichend.
|
||||
// Live radar view for the start page: a <canvas> that uses JavaScript
|
||||
// to fetch /radar.json every few seconds and draw the aircraft polar
|
||||
// (bearing/distance, exactly like on the device display). Intentionally
|
||||
// kept with fetch()-polling instead of WebSocket/SSE - significantly less
|
||||
// code and memory usage on the ESP32, and perfectly adequate for a page
|
||||
// occasionally opened from a phone.
|
||||
//
|
||||
// Der Reichweiten-Waehler (<select>) ist rein clientseitig/pro Seiten-
|
||||
// aufruf - er aendert NICHT die Geraete-Einstellung (SettingsStore::
|
||||
// rangeIndex()), sondern wird als "range_km"-Query-Parameter an
|
||||
// /radar.json mitgeschickt (siehe handleRadarJson()) und erlaubt so ein
|
||||
// unabhaengiges Herein-/Herauszoomen auf dem Handy, ohne das Geraete-
|
||||
// Display zu beeinflussen. Default ist die aktuelle Geraete-Reichweite.
|
||||
// The range selector (<select>) is purely client-side/per page view -
|
||||
// it does NOT change the device setting (SettingsStore::
|
||||
// rangeIndex()), but is sent as a "range_km" query parameter to
|
||||
// /radar.json (see handleRadarJson()) and allows independent zooming
|
||||
// in/out on the phone without affecting the device display. Default is
|
||||
// the current device range.
|
||||
void appendRadarSection(String& html) {
|
||||
float deviceRangeKm = Config::RANGE_STEPS_KM[SettingsStore::rangeIndex()];
|
||||
|
||||
@@ -131,10 +130,10 @@ namespace {
|
||||
html += "var markers=[];";
|
||||
html += "var selectedHex=null;";
|
||||
|
||||
// Hintergrund-Sterne AUSSERHALB des Radarkreises - gleiches Prinzip
|
||||
// wie updateBgStars()/initBgStarsIfNeeded() in radar_screen.cpp
|
||||
// (Rejection-Sampling, damit kein Stern innerhalb des Kreises
|
||||
// landet), hier per Canvas/JS statt TFT_eSPI nachgebaut.
|
||||
// Background stars OUTSIDE the radar circle - same principle
|
||||
// as updateBgStars()/initBgStarsIfNeeded() in radar_screen.cpp
|
||||
// (rejection sampling, so no star lands inside the circle),
|
||||
// replicated here via Canvas/JS instead of TFT_eSPI.
|
||||
html += "var stars=[];";
|
||||
html += "(function(){var minDistSq=(R+6)*(R+6);for(var i=0;i<24;i++){var x,y,tries=0;";
|
||||
html += "do{x=4+Math.random()*(W-8);y=4+Math.random()*(H-8);tries++;}";
|
||||
@@ -155,9 +154,9 @@ namespace {
|
||||
html += "for(var ring=1;ring<=3;ring++){var r=R*ring/3;ctx.beginPath();ctx.arc(cx,cy,r,0,Math.PI*2);ctx.stroke();";
|
||||
html += "ctx.fillText(Math.round(data.range_km*ring/3)+' km',cx+4,cy-r+10);}";
|
||||
html += "ctx.strokeStyle='#12261a';ctx.beginPath();ctx.moveTo(cx-R,cy);ctx.lineTo(cx+R,cy);ctx.moveTo(cx,cy-R);ctx.lineTo(cx,cy+R);ctx.stroke();";
|
||||
// Alle vier Himmelsrichtungen (N/E/S/W), genau wie
|
||||
// drawStaticBackground() in radar_screen.cpp - vorher stand hier nur
|
||||
// "N", was auf Nachfrage ergaenzt wurde.
|
||||
// All four cardinal directions (N/E/S/W), exactly like
|
||||
// drawStaticBackground() in radar_screen.cpp - previously only
|
||||
// "N" was shown here, which was expanded upon request.
|
||||
html += "ctx.fillStyle='#39ff14';ctx.textAlign='center';ctx.fillText('N',cx,cy-R-8);";
|
||||
html += "ctx.fillText('S',cx,cy+R+16);";
|
||||
html += "ctx.textAlign='left';ctx.fillText('E',cx+R+4,cy+3);";
|
||||
@@ -180,26 +179,26 @@ namespace {
|
||||
html += "if(a.emergency){ctx.strokeStyle='#ff3b3b';ctx.beginPath();ctx.arc(x,y,9,0,Math.PI*2);ctx.stroke();}";
|
||||
html += "else if(a.watched){ctx.strokeStyle='#00e5ff';ctx.beginPath();ctx.arc(x,y,9,0,Math.PI*2);ctx.stroke();}";
|
||||
html += "else if(a.notable){ctx.strokeStyle='#ff9f1a';ctx.beginPath();ctx.arc(x,y,9,0,Math.PI*2);ctx.stroke();}";
|
||||
// Ausgewaehltes Flugzeug (per Klick/Tap, siehe unten) bekommt einen
|
||||
// weissen Auswahlring, gleiches Prinzip wie isSelected auf dem
|
||||
// Geraete-Display (radar_screen.cpp render()).
|
||||
// Selected aircraft (by click/tap, see below) gets a
|
||||
// white selection ring, same principle as isSelected on the
|
||||
// device display (radar_screen.cpp render()).
|
||||
html += "if(a.hex===selectedHex){ctx.strokeStyle='#ffffff';ctx.beginPath();ctx.arc(x,y,11,0,Math.PI*2);ctx.stroke();}";
|
||||
html += "ctx.fillStyle=color;ctx.textAlign='center';ctx.fillText(a.callsign,x,y-8);";
|
||||
html += "markers.push({x:x,y:y,a:a});";
|
||||
html += "});";
|
||||
html += "status.textContent=(data.aircraft||[]).length+' aircraft \\u00b7 range '+data.range_km+' km';";
|
||||
// Falls das ausgewaehlte Flugzeug in dieser Aktualisierung nicht
|
||||
// mehr vorkommt (z.B. aus der Reichweite geflogen), Infobox wieder
|
||||
// schliessen statt veraltete Daten stehen zu lassen.
|
||||
// If the selected aircraft no longer appears in this update
|
||||
// (e.g. flew out of range), close the info box instead of
|
||||
// showing stale data.
|
||||
html += "if(selectedHex){var found=markers.filter(function(m){return m.a.hex===selectedHex;})[0];";
|
||||
html += "if(found){showInfo(found.a);}else{selectedHex=null;hideInfo();}}";
|
||||
html += "}";
|
||||
|
||||
// Info-Panel fuer ein angetipptes Flugzeug - bewusst eine eigene,
|
||||
// stehenbleibende Box (kein Tooltip/Popup, das beim naechsten
|
||||
// Neuzeichnen einfach verschwindet), mit explizitem Schliessen-Link,
|
||||
// gleiches Grundprinzip wie infoScreen() am Geraet: der Nutzer soll
|
||||
// aktiv entscheiden, wann die Info wieder verschwindet.
|
||||
// Info panel for a tapped aircraft - intentionally a dedicated,
|
||||
// persistent box (not a tooltip/popup that disappears on the next
|
||||
// redraw), with an explicit close link, same basic principle as
|
||||
// infoScreen() on the device: the user should actively decide when
|
||||
// the info disappears.
|
||||
html += "function fmtNum(n,d){return (typeof n==='number')?n.toFixed(d):'?';}";
|
||||
html += "function showInfo(a){";
|
||||
html += "var lines=[];";
|
||||
@@ -233,11 +232,11 @@ namespace {
|
||||
html += "}";
|
||||
html += "poll();";
|
||||
html += "setInterval(poll,3000);";
|
||||
// Schnellerer, rein lokaler Redraw-Takt (alle 150ms, ohne Netzwerk-
|
||||
// Anfrage) nur fuer das Sternenfunkeln + die Auswahlmarkierung -
|
||||
// gleiches Grundprinzip wie tick() vs. render() auf dem
|
||||
// Geraete-Display: Flugzeugpositionen aktualisieren sich weiterhin
|
||||
// nur alle 3s per poll(), die Sterne twinkeln aber fluessig dazwischen.
|
||||
// Faster, purely local redraw interval (every 150ms, without network
|
||||
// request) only for star twinkling + the selection marker -
|
||||
// same basic principle as tick() vs. render() on the
|
||||
// device display: aircraft positions still update
|
||||
// only every 3s via poll(), but stars twinkle smoothly in between.
|
||||
html += "setInterval(function(){draw(lastData);},150);";
|
||||
html += "})();</script>";
|
||||
}
|
||||
@@ -297,12 +296,12 @@ namespace {
|
||||
if (line.length() == 0) continue;
|
||||
server.sendContent(String(days[i].date) + "," + line + "\n");
|
||||
|
||||
// Wichtig: Ohne regelmaessiges Abgeben der CPU haengt dieser
|
||||
// Task (Core 0, Prioritaet 1) die Idle-Task aus, die den
|
||||
// Task-Watchdog fuettert. Bei groesseren Logbuechern fuehrt
|
||||
// das nach ca. 5s ohne Yield zu einem Watchdog-Reset -
|
||||
// genau der Reboot mitten im CSV-Download, den der Nutzer
|
||||
// beobachtet hat. delay(1) erzwingt einen Kontextwechsel.
|
||||
// Important: Without regularly yielding the CPU, this
|
||||
// task (Core 0, priority 1) starves the idle task that feeds
|
||||
// the task watchdog. With larger logbooks, this causes a
|
||||
// watchdog reset after approx. 5s without yield -
|
||||
// exactly the reboot during CSV download that the user
|
||||
// observed. delay(1) forces a context switch.
|
||||
if (++linesSent % 10 == 0) {
|
||||
delay(1);
|
||||
}
|
||||
@@ -348,14 +347,14 @@ namespace {
|
||||
server.send(303);
|
||||
}
|
||||
|
||||
// Verwaltung von Airline-Filter und Beobachtungsliste per Browser -
|
||||
// beide Backend-Module (AirlineFilter/AircraftWatchlist) sind seit
|
||||
// dieser Erweiterung mutex-geschuetzt (siehe dort), da sie jetzt sowohl
|
||||
// von Core 1 (Radar-/Menue-Screens) als auch von hier aus - Core 0,
|
||||
// WebExportServer laeuft innerhalb von NetTask - aufgerufen werden.
|
||||
// Praktisch vor allem fuer laengere Eingaben (Rufzeichen, ICAO-Codes),
|
||||
// die sich per Handy-Tastatur deutlich bequemer eintippen lassen als
|
||||
// ueber die kleine Bildschirmtastatur des Geraets.
|
||||
// Management of airline filter and watchlist via browser -
|
||||
// both backend modules (AirlineFilter/AircraftWatchlist) are now
|
||||
// mutex-protected (see there), since they are called both
|
||||
// from Core 1 (radar/menu screens) and from here - Core 0,
|
||||
// WebExportServer runs within NetTask.
|
||||
// Practically most useful for longer inputs (callsigns, ICAO codes)
|
||||
// that are much easier to type on a phone keyboard than
|
||||
// via the device's small on-screen keyboard.
|
||||
void handleLists() {
|
||||
String html = htmlHeader("Leos Flightradar - Lists");
|
||||
|
||||
@@ -449,19 +448,18 @@ namespace {
|
||||
server.send(303);
|
||||
}
|
||||
|
||||
// Datenquelle fuer das Live-Radar auf der Startseite (siehe
|
||||
// appendRadarSection()). Wendet dieselben Filter/Prioritaeten an wie das
|
||||
// Geraete-Display (render() in radar_screen.cpp): Reichweite, "Boden-
|
||||
// fahrzeuge ausblenden", Airline-Filter, sowie Notfall/Beobachtungsliste/
|
||||
// "auffaellig" als sich gegenseitig ausschliessende Ring-Markierungen in
|
||||
// genau dieser Prioritaet.
|
||||
// Data source for the live radar on the start page (see
|
||||
// appendRadarSection()). Applies the same filters/priorities as the
|
||||
// device display (render() in radar_screen.cpp): range, "hide ground
|
||||
// vehicles", airline filter, and emergency/watchlist/"notable" as
|
||||
// mutually exclusive ring markers in exactly this priority.
|
||||
void handleRadarJson() {
|
||||
float rangeKm = Config::RANGE_STEPS_KM[SettingsStore::rangeIndex()];
|
||||
// Erlaubt der Web-Ansicht ein eigenes, unabhaengiges Zoomen (siehe
|
||||
// Reichweiten-Waehler in appendRadarSection()), OHNE die Geraete-
|
||||
// Einstellung zu veraendern. Nur einen der bekannten
|
||||
// Config::RANGE_STEPS_KM-Werte akzeptieren - ein fehlender oder
|
||||
// nicht erkannter Parameter faellt auf die Geraete-Reichweite zurueck.
|
||||
// Allows the web view its own independent zooming (see
|
||||
// range selector in appendRadarSection()), WITHOUT changing the
|
||||
// device setting. Only accept one of the known
|
||||
// Config::RANGE_STEPS_KM values - a missing or
|
||||
// unknown parameter falls back to the device range.
|
||||
if (server.hasArg("range_km")) {
|
||||
float requested = server.arg("range_km").toFloat();
|
||||
for (uint8_t i = 0; i < Config::RANGE_STEP_COUNT; i++) {
|
||||
@@ -494,13 +492,13 @@ namespace {
|
||||
bool isHeavy = isHeavyCategoryWeb(a.category);
|
||||
bool isEmergency = emergencyOn && isEmergencySquawkWeb(a.squawk);
|
||||
bool isWatched = watchOn && AircraftWatchlist::isWatched(a.callsign);
|
||||
// "notable" (oranger Ring) ist fuer auffaellige Rufzeichen
|
||||
// (Militaer/Regierung) reserviert - Heavy-Flugzeuge bekommen
|
||||
// stattdessen die eigene Markerform (siehe "heavy" oben). Es
|
||||
// gibt aktuell aber keine Militaer-/Regierungs-Praefixliste im
|
||||
// Projekt (auch nicht am Geraete-Display, siehe
|
||||
// radar_screen.cpp) - "notable" bleibt daher bis auf Weiteres
|
||||
// immer false.
|
||||
// "notable" (orange ring) is reserved for conspicuous callsigns
|
||||
// (military/government) - heavy aircraft instead get their
|
||||
// own marker shape (see "heavy" above). However, there is
|
||||
// currently no military/government prefix list in the
|
||||
// project (not even on the device display, see
|
||||
// radar_screen.cpp) - "notable" therefore remains
|
||||
// always false for now.
|
||||
bool isNotable = false;
|
||||
|
||||
JsonObject o = arr.add<JsonObject>();
|
||||
@@ -516,10 +514,10 @@ namespace {
|
||||
o["emergency"] = isEmergency;
|
||||
o["watched"] = isWatched;
|
||||
o["notable"] = isNotable;
|
||||
// Zusaetzliche Felder nur fuer das Info-Panel bei Klick/Tap auf
|
||||
// ein Flugzeug (siehe showInfo() in appendRadarSection()) - beide
|
||||
// stehen bereits verlustfrei im Aircraft-Snapshot, kein
|
||||
// zusaetzlicher Netzwerk-/SD-Zugriff noetig.
|
||||
// Additional fields only for the info panel on click/tap on
|
||||
// an aircraft (see showInfo() in appendRadarSection()) - both
|
||||
// are already losslessly in the Aircraft snapshot, no
|
||||
// additional network/SD access needed.
|
||||
o["speed_kt"] = a.groundSpeedKt;
|
||||
o["squawk"] = a.squawk;
|
||||
}
|
||||
|
||||
@@ -63,13 +63,12 @@ void run(TFT_eSPI& tft) {
|
||||
constexpr int16_t LINE_H = 16;
|
||||
constexpr int16_t VIEW_TOP = 36;
|
||||
|
||||
// Wenn WLAN verbunden ist, bekommt der Absatztext nur noch ein kleines
|
||||
// (bei Bedarf scrollbares) Fenster, weil darunter fest der QR-Code samt
|
||||
// IP-Adresse Platz braucht (siehe Alex' Feedback: der vorherige separate
|
||||
// "QR"-Button oben rechts sah seltsam aus - jetzt wird der Code direkt
|
||||
// hier gezeigt, mittig, mit der IP-Adresse mittig darunter). Ohne WLAN
|
||||
// gibt es keinen QR-Code, der Text bekommt dann wie vorher die volle
|
||||
// Bildschirmhoehe.
|
||||
// When WiFi is connected, the paragraph text gets only a small
|
||||
// (scrollable if needed) window, because the QR code plus IP address
|
||||
// need fixed space below (see Alex' feedback: the previous separate
|
||||
// "QR" button at top right looked strange - now the code is shown
|
||||
// directly here, centered, with the IP address centered below). Without
|
||||
// WiFi there is no QR code, the text gets full screen height as before.
|
||||
constexpr int16_t TEXT_VIEW_BOTTOM_CONNECTED = 84;
|
||||
constexpr int16_t TEXT_VIEW_BOTTOM_DISCONNECTED = Config::SCREEN_HEIGHT - 60;
|
||||
|
||||
@@ -77,16 +76,15 @@ void run(TFT_eSPI& tft) {
|
||||
String urlLine = wifiConnected ? ("http://" + WiFi.localIP().toString() + "/") : String();
|
||||
int16_t viewBottom = wifiConnected ? TEXT_VIEW_BOTTOM_CONNECTED : TEXT_VIEW_BOTTOM_DISCONNECTED;
|
||||
|
||||
// QR-Code einmalig erzeugen (die URL aendert sich waehrend dieser
|
||||
// Bildschirm offen ist nicht). Version 4 (33x33 Module) reicht mit
|
||||
// deutlicher Reserve fuer eine "http://<lokale-IP>/"-URL (max. rund 24
|
||||
// Zeichen) - ECC_LOW statt eines hoeheren Fehlerkorrektur-Levels, um die
|
||||
// Module moeglichst gross (und damit leicht scannbar) darstellen zu
|
||||
// koennen.
|
||||
// Generate QR code once (URL doesn't change while this screen
|
||||
// is open). Version 4 (33x33 modules) is sufficient with plenty of
|
||||
// headroom for a "http://<local-IP>/" URL (max. ~24 chars) - ECC_LOW
|
||||
// instead of a higher error correction level, to make the modules as
|
||||
// large (and thus easily scannable) as possible.
|
||||
constexpr uint8_t QR_VERSION = 4;
|
||||
constexpr int16_t QR_SIZE_MODULES = 33; // Version 4: 4*4+17 = 33
|
||||
constexpr int16_t QR_BLOCK = 4;
|
||||
constexpr int16_t QR_QUIET = 2; // Ruhezone in Modulen rundherum, Scanner brauchen etwas Rand
|
||||
constexpr int16_t QR_QUIET = 2; // Quiet zone in modules around it, scanners need some margin
|
||||
constexpr int16_t QR_PIXEL_SIZE = (QR_SIZE_MODULES + 2 * QR_QUIET) * QR_BLOCK;
|
||||
constexpr int16_t QR_X = (Config::SCREEN_WIDTH - QR_PIXEL_SIZE) / 2;
|
||||
constexpr int16_t QR_GAP = 6;
|
||||
@@ -99,9 +97,9 @@ void run(TFT_eSPI& tft) {
|
||||
qrcode_initText(&qrcode, qrData, QR_VERSION, ECC_LOW, urlLine.c_str());
|
||||
}
|
||||
|
||||
// Gesamthoehe des Absatztextes vorab berechnen (draw=false), um zu
|
||||
// wissen, ob Scroll-Pfeile gebraucht werden - muss exakt zur
|
||||
// Zeichenreihenfolge in redraw() unten passen.
|
||||
// Pre-calculate total height of paragraph text (draw=false) to know
|
||||
// whether scroll arrows are needed - must exactly match the drawing
|
||||
// order in redraw() below.
|
||||
int16_t totalH = VIEW_TOP;
|
||||
totalH = layoutWrapped(tft, 10, totalH, textMaxWidth, LINE_H, I18n::t(StringId::WEBUI_INFO_PARA1), 0, 0, 0, false);
|
||||
if (!wifiConnected) {
|
||||
@@ -136,9 +134,9 @@ void run(TFT_eSPI& tft) {
|
||||
}
|
||||
|
||||
if (wifiConnected) {
|
||||
// QR-Code + IP-Adresse stehen fest unterhalb des (ggf.
|
||||
// scrollbaren) Textes, unabhaengig von scrollY - beides mittig
|
||||
// zentriert, direkt ueber dem Zurueck-Button.
|
||||
// QR code + IP address are fixed below the (possibly scrollable)
|
||||
// text, independent of scrollY - both centered, directly above
|
||||
// the back button.
|
||||
tft.fillRect(QR_X, QR_Y, QR_PIXEL_SIZE, QR_PIXEL_SIZE, TFT_WHITE);
|
||||
for (uint8_t my = 0; my < qrcode.size; my++) {
|
||||
for (uint8_t mx = 0; mx < qrcode.size; mx++) {
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
#include <TFT_eSPI.h>
|
||||
|
||||
namespace WebUiScreen {
|
||||
// Blockierend: zeigt die WLAN-IP und eine Erklaerung der kleinen
|
||||
// eingebauten Webseite (Flugbuch ansehen/herunterladen/loeschen,
|
||||
// Screenshots ansehen/herunterladen/loeschen). Kehrt zurueck, sobald
|
||||
// "Zurueck" angetippt wurde.
|
||||
// Blocking: shows the WiFi IP and an explanation of the small
|
||||
// built-in web page (view logbook / download / delete,
|
||||
// view screenshots / download / delete). Returns as soon as
|
||||
// "Back" is tapped.
|
||||
void run(TFT_eSPI& tft);
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
#include <TFT_eSPI.h>
|
||||
|
||||
namespace WifiManageScreen {
|
||||
// Blockierend: zeigt die bis zu 3 gespeicherten WLAN-Netzwerke, erlaubt
|
||||
// Loeschen einzelner Eintraege und (falls noch Platz ist) das Hinzufuegen
|
||||
// eines neuen ueber den Scan+Passwort-Bildschirm. Kehrt zurueck, sobald
|
||||
// "Back" angetippt wird.
|
||||
// Blocking: shows the up to 3 saved WiFi networks, allows
|
||||
// deleting individual entries and (if there is still space) adding
|
||||
// a new one via the scan+password screen. Returns as soon as
|
||||
// "Back" is tapped.
|
||||
void run(TFT_eSPI& tft);
|
||||
}
|
||||
@@ -3,9 +3,9 @@
|
||||
#include <TFT_eSPI.h>
|
||||
|
||||
namespace WifiSetupScreen {
|
||||
// Blockierend: WLAN-Netzwerke suchen, per Touch auswaehlen, Passwort
|
||||
// ueber Bildschirmtastatur eingeben (Klartext, keine Sterne), verbinden.
|
||||
// Speichert bei Erfolg die Zugangsdaten via WifiMgr auf der SD-Karte.
|
||||
// Rueckgabe: true = verbunden, false = abgebrochen/uebersprungen.
|
||||
// Blocking: scan WiFi networks, select via touch, enter password
|
||||
// via on-screen keyboard (plain text, no asterisks), connect.
|
||||
// Saves credentials via WifiMgr on the SD card on success.
|
||||
// Returns: true = connected, false = cancelled/skipped.
|
||||
bool run(TFT_eSPI& tft);
|
||||
}
|
||||
Reference in New Issue
Block a user