# 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` (`