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.
|
||||
Reference in New Issue
Block a user