267 lines
13 KiB
Markdown
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. |