Files
flyradar/AGENTS.md
2026-08-16 14:33:27 +02:00

267 lines
13 KiB
Markdown

# 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.