diff --git a/.gitignore b/.gitignore index 10833326b..9da3cc86a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ .pio release/ +.venv-pio/ .vscode/.browse.c_cpp.db* .vscode/c_cpp_properties.json .vscode/launch.json .vscode/ipch + +# Local Wi-Fi / location overrides (copy from include/secrets.h.example) +include/secrets.h diff --git a/docs/FORK_AND_UPSTREAM.md b/docs/FORK_AND_UPSTREAM.md new file mode 100644 index 000000000..aef74f1f0 --- /dev/null +++ b/docs/FORK_AND_UPSTREAM.md @@ -0,0 +1,47 @@ +# Fork vs upstream contribution + +Upstream: [MatixYo/ESP32-Plane-Radar](https://github.com/MatixYo/ESP32-Plane-Radar) (MIT). + +## Recommended split + +| Track | Purpose | +|-------|---------| +| **Upstream PR** | Generic `esp32-tft-round` port: pins, polled buttons, CH343 notes, display fixes — **no secrets**, default env stays `supermini` | +| **Your fork** | Bench branch with `include/secrets.h` (gitignored) for fixed Wi-Fi / location | + +Contributing the board port upstream helps other HackerBox #0107 / 88267 owners. Keep personal network settings in `secrets.h` only on your fork. + +## Prepare an upstream PR + +1. Ensure `include/secrets.h` is **not** tracked (`git status` clean of secrets). +2. Default `platformio.ini` → `default_envs = supermini`. +3. Commit: + - `include/board/esp32_tft_round.h` (pins only) + - `include/board/supermini.h` (if shared button abstractions changed) + - `include/config.h`, WiFi/button/display fixes + - `docs/PORT_ESP32_TFT_ROUND.md` + - `scripts/flash-esp32-tft.sh`, `esptool.cfg` + - `include/secrets.h.example` (template only) +4. Open PR to `MatixYo/ESP32-Plane-Radar` with title e.g. **Add ESP32-TFT round (GC9A01 integrated) board profile**. + +## Fork for bench use + +```bash +gh repo fork MatixYo/ESP32-Plane-Radar --clone +cd ESP32-Plane-Radar +cp include/secrets.h.example include/secrets.h +# edit secrets.h +pio run -e esp32-tft-round -t upload --upload-port /dev/cu.wchusbserial* +``` + +Add your fork as `origin`, keep `upstream` pointing at MatixYo; rebase bench branches on upstream `main` when merging PR feedback. + +## Secrets policy + +| File | Commit? | +|------|---------| +| `include/secrets.h.example` | Yes — placeholders only | +| `include/secrets.h` | **Never** — in `.gitignore` | +| `include/board/*.h` | Yes — hardware only, no SSIDs/passwords | + +If secrets were ever committed, rotate Wi-Fi password and `git filter-repo` / BFG before pushing public. diff --git a/docs/PORT_ESP32_P4_WAVESHARE_4C.md b/docs/PORT_ESP32_P4_WAVESHARE_4C.md new file mode 100644 index 000000000..3bd22980c --- /dev/null +++ b/docs/PORT_ESP32_P4_WAVESHARE_4C.md @@ -0,0 +1,88 @@ +# ESP32-Plane-Radar — Waveshare ESP32-P4 4C port + +Target: [Waveshare ESP32-P4-WIFI6-Touch-LCD-4C](https://www.waveshare.com/wiki/ESP32-P4-WIFI6-Touch-LCD-4C) — 720×720 round MIPI DSI, GT911 touch, ESP32-C6 Wi-Fi 6 coprocessor. + +| | | +|---|---| +| MCU | ESP32-P4NRW32 | +| Display | JD9365, 720×720, MIPI DSI 2-lane | +| Touch | GT911 / GT9271 (I2C SDA **GPIO7**, SCL **GPIO8**) | +| Wi-Fi | ESP32-C6 via esp_hosted (pre-flashed on board) | +| PlatformIO env | **`esp32-p4-waveshare-4c`** | +| Pin map | `include/board/esp32_p4_waveshare_4c.h` | + +## Prerequisites + +- **Python 3.10+** for pioarduino (macOS system Python 3.9 is too old). Use a venv: + +```bash +python3.12 -m venv .venv-pio +.venv-pio/bin/pip install platformio +.venv-pio/bin/pio run -e esp32-p4-waveshare-4c +``` + +- **pioarduino** platform (configured in `platformio.ini`; first build downloads the SDK) +- Arduino-esp32 **3.2+** with ESP32-P4 support (via pioarduino) +- LovyanGFX **develop** branch (MIPI DSI / `Bus_DSI`) + +## Local Wi-Fi (required for this port) + +Hardcoded STA credentials via gitignored `include/secrets.h`: + +```bash +cp include/secrets.h.example include/secrets.h +# kWifiSkipPortal = true, set SSID/password +``` + +Never commit `secrets.h`. + +## Build & flash + +```bash +pio run -e esp32-p4-waveshare-4c -t upload +pio device monitor -b 115200 +``` + +Or: + +```bash +chmod +x scripts/flash-esp32-p4-waveshare-4c.sh +./scripts/flash-esp32-p4-waveshare-4c.sh +``` + +Use the **USB TO UART** Type-C port for flashing. Hold **BOOT** while resetting if upload fails. + +## Controls + +| Input | Effect | +|-------|--------| +| **Pinch out** (two fingers spread) | Larger range (5 → 10 → 15 → 25 km) | +| **Pinch in** (two fingers close) | Smaller range | +| **BOOT (GPIO35) short tap** | Cycle range (fallback) | +| **BOOT long press** | Disabled (hardcoded Wi-Fi) | + +## Display init + +JD9365 vendor init sequence from [xiaozhi-esp32 Waveshare P4 board](https://github.com/78/xiaozhi-esp32/tree/main/boards/waveshare/esp32-p4-wifi6-touch-lcd), converted to LovyanGFX `Panel_DSI` lists: + +```bash +python3 scripts/convert_jd9365_init.py # regenerates include/hardware/panel_jd9365_waveshare_4c.hpp +``` + +## Merged binary + +```bash +pio run -e esp32-p4-waveshare-4c -t merge +# .pio/build/esp32-p4-waveshare-4c/firmware-merged.bin @ 0x0 +``` + +## Troubleshooting + +| Symptom | Check | +|---------|--------| +| Blank display | Backlight GPIO26 PWM; MIPI LDO channel 3 @ 2.5 V | +| Wi-Fi never connects | C6 coprocessor powered; serial for esp_hosted errors | +| No pinch response | Serial for `touch: GT911 @ 0x..`; I2C 7/8 | +| Sprite alloc failed | 32 MB PSRAM; `BOARD_HAS_PSRAM` in build flags | + +See also [FORK_AND_UPSTREAM.md](FORK_AND_UPSTREAM.md). diff --git a/docs/PORT_ESP32_TFT_ROUND.md b/docs/PORT_ESP32_TFT_ROUND.md new file mode 100644 index 000000000..2e4859d1d --- /dev/null +++ b/docs/PORT_ESP32_TFT_ROUND.md @@ -0,0 +1,65 @@ +# ESP32-Plane-Radar — board port notes + +Upstream targets **ESP32-C3 Super Mini + wired GC9A01** (`env:supermini`). + +## ESP32-TFT round (HackerBox #0107 / 88267) + +Parts-catalog slug: `ESP32_Round_240x240_IPS_Module` + +**Kit:** [HackerBox #0107 — Dock](https://www.instructables.com/HackerBox-0107-Dock/) (round ESP32 + GC9A01 module, USB dock, RTL8812BU Wi‑Fi stick). + +| | | +|---|---| +| MCU | ESP32-D0WD-V3 | +| Flash | 16 MB | +| USB | WCH CH343 (`1A86:55D3`) | +| Display | GC9A01 240×240 round (integrated SPI) | +| PlatformIO env | **`esp32-tft-round`** | + +Pin map: `include/board/esp32_tft_round.h` (hardware only — no credentials). + +### Local dev credentials (optional) + +```bash +cp include/secrets.h.example include/secrets.h +# Edit secrets.h — gitignored; never commit +``` + +See [FORK_AND_UPSTREAM.md](FORK_AND_UPSTREAM.md). + +### Power before USB (HackerBox module) + +1. Plug USB — green LED only, display off. +2. Press the **middle button** — red LED on. +3. CH343 enumerates only when powered on. + +Side keys: GPIO **34** (range tap), GPIO **35** (unused in firmware — floats). + +### macOS CH343 driver (Apple Silicon) + +Use WCH **CH34xVCPDriver.dmg** + Driver Extension → port `/dev/cu.wchusbserial*`. Apple `usbmodem` often fails esptool writes. + +Display pins (Setup200 integrated PCB): MOSI **15**, SCLK **14**, CS **5**, DC **27**, RST **33**, BL **22**, invert **true**, rgb_order **false**. + +### Build & flash + +```bash +pio run -e esp32-tft-round -t upload --upload-port /dev/cu.wchusbserialXXXX +./scripts/flash-esp32-tft.sh /dev/cu.wchusbserialXXXX +pio device monitor -b 115200 +``` + +Upload uses `--no-stub` (required for CH343). If upload fails, hold GPIO **34** or BOOT0 through flash. + +### Controls (esp32-tft-round) + +- **Short tap GPIO 34** — cycle range (5 → 10 → 15 → 25 km), polled + debounced +- **GPIO 35** — not used (floating pin caused ghost events) +- **Wi-Fi setup** — captive portal unless `secrets.h` sets `kWifiSkipPortal` + +### Merged binary + +```bash +pio run -e esp32-tft-round -t merge +# .pio/build/esp32-tft-round/firmware-merged.bin @ 0x0 +``` diff --git a/esptool.cfg b/esptool.cfg new file mode 100644 index 000000000..c9f1cf2d8 --- /dev/null +++ b/esptool.cfg @@ -0,0 +1,9 @@ +[esptool] +timeout = 10 +mem_end_rom_timeout = 2 +serial_write_timeout = 30 +write_block_attempts = 5 +connect_attempts = 15 +reset_delay = 0.2 +# Enter download mode and leave GPIO0 (BOOT) low +custom_reset_sequence = D0|R1|W0.15|D1|R0|W0.1|D1 diff --git a/include/board/README.md b/include/board/README.md new file mode 100644 index 000000000..c3ad73289 --- /dev/null +++ b/include/board/README.md @@ -0,0 +1,25 @@ +# Board profiles + +Plane Radar selects hardware via PlatformIO `build_flags`: + +| Environment | Board header | Target | +|-------------|--------------|--------| +| `supermini` | `include/board/supermini.h` | ESP32-C3 Super Mini + wired GC9A01 (upstream default) | +| `esp32-tft-round` | `include/board/esp32_tft_round.h` | HackerBox #0107 / ESP32-TFT 88267 integrated round module | +| `esp32-p4-waveshare-4c` | `include/board/esp32_p4_waveshare_4c.h` | Waveshare ESP32-P4 4″ 720×720 MIPI DSI round touch | + +Add `-DPLANE_RADAR_BOARD_ESP32_TFT_ROUND` only for the classic ESP32 round env. +Add `-DPLANE_RADAR_BOARD_ESP32_P4_WAVESHARE_4C` for the Waveshare P4 4C env. + +## Local Wi-Fi / location (optional) + +For development without the captive portal: + +```bash +cp include/secrets.h.example include/secrets.h +# edit include/secrets.h — file is gitignored +``` + +Never commit `include/secrets.h`. Upstream PRs must not include credentials. + +See [docs/FORK_AND_UPSTREAM.md](../docs/FORK_AND_UPSTREAM.md). diff --git a/include/board/esp32_p4_waveshare_4c.h b/include/board/esp32_p4_waveshare_4c.h new file mode 100644 index 000000000..ad4cde283 --- /dev/null +++ b/include/board/esp32_p4_waveshare_4c.h @@ -0,0 +1,64 @@ +#pragma once + +/** Waveshare ESP32-P4-WIFI6-Touch-LCD-4C — 720×720 round MIPI DSI (JD9365). */ +#include + +namespace board { + +constexpr gpio_num_t kBootPin = GPIO_NUM_35; +constexpr bool kBootPinInternalPullup = true; +constexpr gpio_num_t kWifiResetPin = GPIO_NUM_NC; +constexpr bool kWifiResetPinInternalPullup = false; +constexpr bool kBootUsePolledTap = false; +constexpr gpio_num_t kRangePin2 = GPIO_NUM_NC; +constexpr bool kButtonActiveLow = true; +constexpr unsigned long kBootResetHoldMs = 5000UL; +constexpr uint8_t kBootLowStreakRequired = 1; +constexpr uint8_t kButtonStableSamplesRequired = 1; +constexpr unsigned long kBootTapMinMs = 120UL; +constexpr unsigned long kButtonTapCooldownMs = 600UL; +constexpr uint8_t kButtonReadSamples = 1; +constexpr uint8_t kButtonReadThreshold = 1; + +/** Hardcoded Wi-Fi — no portal long-press wipe. */ +constexpr bool kEnableWifiLongPressReset = false; + +constexpr gpio_num_t kSdCardPinCs = GPIO_NUM_NC; + +/** MIPI DSI panel reset / backlight (xiaozhi Waveshare P4 config). */ +constexpr gpio_num_t kDisplayPinRst = GPIO_NUM_27; +constexpr gpio_num_t kDisplayPinBl = GPIO_NUM_26; +constexpr bool kDisplayBacklightInvert = true; + +/** Not used on DSI path. */ +constexpr gpio_num_t kDisplayPinCs = GPIO_NUM_NC; +constexpr gpio_num_t kDisplayPinDc = GPIO_NUM_NC; +constexpr gpio_num_t kDisplayPinMosi = GPIO_NUM_NC; +constexpr gpio_num_t kDisplayPinSclk = GPIO_NUM_NC; +constexpr uint32_t kDisplaySpiWriteHz = 0; +constexpr bool kDisplayInvert = false; +constexpr bool kDisplayRgbOrder = true; + +constexpr int kDisplayWidth = 720; +constexpr int kDisplayHeight = 720; + +/** MIPI DSI PHY / timing (Waveshare 4C). */ +constexpr uint16_t kMipiLaneMbps = 1500; +constexpr uint8_t kMipiLaneNum = 2; +constexpr uint8_t kMipiLdoChan = 3; +constexpr uint32_t kMipiLdoVoltageMv = 2500; +constexpr uint16_t kDpiClockMhz = 46; +constexpr uint16_t kDpiHsyncPulse = 20; +constexpr uint16_t kDpiHsyncBackPorch = 20; +constexpr uint16_t kDpiHsyncFrontPorch = 40; +constexpr uint16_t kDpiVsyncPulse = 4; +constexpr uint16_t kDpiVsyncBackPorch = 12; +constexpr uint16_t kDpiVsyncFrontPorch = 24; + +/** GT911 touch on shared codec I2C bus. */ +constexpr gpio_num_t kTouchPinSda = GPIO_NUM_7; +constexpr gpio_num_t kTouchPinScl = GPIO_NUM_8; +constexpr uint8_t kTouchI2cAddrPrimary = 0x5D; +constexpr uint8_t kTouchI2cAddrBackup = 0x14; + +} // namespace board diff --git a/include/board/esp32_tft_round.h b/include/board/esp32_tft_round.h new file mode 100644 index 000000000..a4e2f5e2a --- /dev/null +++ b/include/board/esp32_tft_round.h @@ -0,0 +1,46 @@ +#pragma once + +/** HackerBox #0107 / ESP32-TFT 88267 — ESP32-D0WD + integrated GC9A01 round panel. + * Pin map matches official kit TFT_eSPI Setup200_GC9A01 (Instructables HackerBox 0107). */ +#include +#include + +namespace board { + +constexpr gpio_num_t kBootPin = GPIO_NUM_34; +constexpr bool kBootPinInternalPullup = false; +constexpr gpio_num_t kWifiResetPin = GPIO_NUM_35; +constexpr bool kWifiResetPinInternalPullup = false; +/** GPIO 34–39 have no internal pull-up; poll in loop with heavy debounce. */ +constexpr bool kBootUsePolledTap = true; +/** Only GPIO 34 cycles range; GPIO 35 floats on this PCB. */ +constexpr gpio_num_t kRangePin2 = GPIO_NUM_NC; +constexpr bool kButtonActiveLow = true; +constexpr unsigned long kBootResetHoldMs = 5000UL; +constexpr uint8_t kBootLowStreakRequired = 8; +constexpr uint8_t kButtonStableSamplesRequired = 10; +constexpr unsigned long kBootTapMinMs = 120UL; +constexpr unsigned long kButtonTapCooldownMs = 600UL; +constexpr uint8_t kButtonReadSamples = 5; +constexpr uint8_t kButtonReadThreshold = 4; + +/** GPIO 35 floats — disable long-press Wi-Fi wipe unless external pull-ups added. */ +constexpr bool kEnableWifiLongPressReset = false; + +/** microSD (if populated) — not used by Plane Radar; do not drive GPIO 5 (display CS). */ +constexpr gpio_num_t kSdCardPinCs = GPIO_NUM_NC; + +constexpr gpio_num_t kDisplayPinMosi = GPIO_NUM_15; +constexpr gpio_num_t kDisplayPinSclk = GPIO_NUM_14; +constexpr gpio_num_t kDisplayPinCs = GPIO_NUM_5; +constexpr gpio_num_t kDisplayPinDc = GPIO_NUM_27; +constexpr gpio_num_t kDisplayPinRst = GPIO_NUM_33; +constexpr gpio_num_t kDisplayPinBl = GPIO_NUM_22; + +constexpr spi_host_device_t kDisplaySpiHost = SPI2_HOST; + +constexpr uint32_t kDisplaySpiWriteHz = 27000000; +constexpr bool kDisplayInvert = true; +constexpr bool kDisplayRgbOrder = false; + +} // namespace board diff --git a/include/board/supermini.h b/include/board/supermini.h new file mode 100644 index 000000000..c1ba28bc0 --- /dev/null +++ b/include/board/supermini.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +namespace board { + +constexpr gpio_num_t kBootPin = GPIO_NUM_9; +constexpr bool kBootPinInternalPullup = true; +constexpr gpio_num_t kWifiResetPin = GPIO_NUM_NC; +constexpr bool kWifiResetPinInternalPullup = false; +constexpr bool kBootUsePolledTap = false; +constexpr gpio_num_t kRangePin2 = GPIO_NUM_NC; +constexpr bool kButtonActiveLow = true; +constexpr unsigned long kBootResetHoldMs = 3000UL; +constexpr uint8_t kBootLowStreakRequired = 1; +constexpr uint8_t kButtonStableSamplesRequired = 1; +constexpr unsigned long kBootTapMinMs = 40UL; +constexpr unsigned long kButtonTapCooldownMs = 0UL; +constexpr uint8_t kButtonReadSamples = 1; +constexpr uint8_t kButtonReadThreshold = 1; + +constexpr bool kEnableWifiLongPressReset = true; + +constexpr gpio_num_t kSdCardPinCs = GPIO_NUM_NC; + +constexpr gpio_num_t kDisplayPinRst = GPIO_NUM_0; +constexpr gpio_num_t kDisplayPinCs = GPIO_NUM_1; +constexpr gpio_num_t kDisplayPinDc = GPIO_NUM_10; +constexpr gpio_num_t kDisplayPinMosi = GPIO_NUM_3; +constexpr gpio_num_t kDisplayPinSclk = GPIO_NUM_4; +constexpr gpio_num_t kDisplayPinBl = GPIO_NUM_NC; + +constexpr spi_host_device_t kDisplaySpiHost = SPI2_HOST; + +constexpr uint32_t kDisplaySpiWriteHz = 40000000; +constexpr bool kDisplayInvert = true; +constexpr bool kDisplayRgbOrder = true; + +} // namespace board diff --git a/include/config.h b/include/config.h index 6555c0342..841147cbd 100644 --- a/include/config.h +++ b/include/config.h @@ -4,8 +4,49 @@ #include +#if defined(PLANE_RADAR_BOARD_ESP32_P4_WAVESHARE_4C) +#include "board/esp32_p4_waveshare_4c.h" +#elif defined(PLANE_RADAR_BOARD_ESP32_TFT_ROUND) +#include "board/esp32_tft_round.h" +#else +#include "board/supermini.h" +#endif + +#if __has_include("secrets.h") +#include "secrets.h" +#endif + namespace config { +constexpr char kFirmwareVersion[] = "1.2.0"; + +namespace defaults { +constexpr double kRadarLat = 52.3676; +constexpr double kRadarLon = 4.9041; +} // namespace defaults + +#if defined(PLANE_RADAR_SECRETS_ACTIVE) && PLANE_RADAR_SECRETS_ACTIVE +constexpr bool kWifiSkipPortal = plane_radar_secrets::kWifiSkipPortal; +constexpr const char* kWifiSsid = plane_radar_secrets::kWifiSsid; +constexpr const char* kWifiPass = plane_radar_secrets::kWifiPass; +constexpr double kDefaultRadarLat = + plane_radar_secrets::kOverrideDefaultLocation + ? plane_radar_secrets::kDefaultRadarLat + : defaults::kRadarLat; +constexpr double kDefaultRadarLon = + plane_radar_secrets::kOverrideDefaultLocation + ? plane_radar_secrets::kDefaultRadarLon + : defaults::kRadarLon; +#else +constexpr bool kWifiSkipPortal = false; +constexpr const char* kWifiSsid = ""; +constexpr const char* kWifiPass = ""; +constexpr double kDefaultRadarLat = defaults::kRadarLat; +constexpr double kDefaultRadarLon = defaults::kRadarLon; +#endif + +constexpr bool kEnableWifiLongPressReset = board::kEnableWifiLongPressReset; + // --- Wi-Fi portal --- constexpr char kPortalApName[] = "PlaneRadar-Setup"; constexpr char kPortalIp[] = "192.168.4.1"; @@ -23,30 +64,62 @@ constexpr unsigned long kWifiDownGraceMs = 4000; /** Minimum interval between background reconnect tries. */ constexpr unsigned long kWifiReconnectIntervalMs = 15000; -// --- BOOT button (ESP32-C3 Super Mini, active LOW) --- -constexpr gpio_num_t kBootPin = GPIO_NUM_9; -constexpr unsigned long kBootResetHoldMs = 3000UL; +// --- User button (board profile in include/board/*.h) --- +constexpr gpio_num_t kBootPin = board::kBootPin; +constexpr bool kBootPinInternalPullup = board::kBootPinInternalPullup; +constexpr gpio_num_t kWifiResetPin = board::kWifiResetPin; +constexpr bool kWifiResetPinInternalPullup = board::kWifiResetPinInternalPullup; +constexpr bool kBootUsePolledTap = board::kBootUsePolledTap; +constexpr gpio_num_t kRangePin2 = board::kRangePin2; +constexpr bool kButtonActiveLow = board::kButtonActiveLow; +constexpr unsigned long kBootResetHoldMs = board::kBootResetHoldMs; +constexpr uint8_t kBootLowStreakRequired = board::kBootLowStreakRequired; +constexpr uint8_t kWifiBootConnectAttempts = 1; +constexpr uint8_t kButtonStableSamplesRequired = board::kButtonStableSamplesRequired; +constexpr unsigned long kButtonTapCooldownMs = board::kButtonTapCooldownMs; +constexpr uint8_t kButtonReadSamples = board::kButtonReadSamples; +constexpr uint8_t kButtonReadThreshold = board::kButtonReadThreshold; /** Ignore BOOT taps shorter than this (debounce). */ -constexpr unsigned long kBootTapMinMs = 40UL; +constexpr unsigned long kBootTapMinMs = board::kBootTapMinMs; -// --- Display: GC9A01 1.28" round 240×240 (SPI) --- -constexpr gpio_num_t kDisplayPinRst = GPIO_NUM_0; -constexpr gpio_num_t kDisplayPinCs = GPIO_NUM_1; -constexpr gpio_num_t kDisplayPinDc = GPIO_NUM_10; -constexpr gpio_num_t kDisplayPinMosi = GPIO_NUM_3; // display SDA -constexpr gpio_num_t kDisplayPinSclk = GPIO_NUM_4; // display SCL +// --- Display --- +constexpr gpio_num_t kDisplayPinRst = board::kDisplayPinRst; +constexpr gpio_num_t kDisplayPinBl = board::kDisplayPinBl; +#if defined(PLANE_RADAR_BOARD_ESP32_P4_WAVESHARE_4C) +constexpr gpio_num_t kDisplayPinCs = board::kDisplayPinCs; +constexpr gpio_num_t kDisplayPinDc = board::kDisplayPinDc; +constexpr gpio_num_t kDisplayPinMosi = board::kDisplayPinMosi; +constexpr gpio_num_t kDisplayPinSclk = board::kDisplayPinSclk; +constexpr uint32_t kDisplaySpiWriteHz = board::kDisplaySpiWriteHz; +constexpr bool kDisplayInvert = board::kDisplayInvert; +constexpr bool kDisplayRgbOrder = board::kDisplayRgbOrder; +#else +#include +constexpr gpio_num_t kDisplayPinCs = board::kDisplayPinCs; +constexpr gpio_num_t kDisplayPinDc = board::kDisplayPinDc; +constexpr gpio_num_t kDisplayPinMosi = board::kDisplayPinMosi; +constexpr gpio_num_t kDisplayPinSclk = board::kDisplayPinSclk; +constexpr spi_host_device_t kDisplaySpiHost = board::kDisplaySpiHost; +constexpr uint32_t kDisplaySpiWriteHz = board::kDisplaySpiWriteHz; +constexpr bool kDisplayInvert = board::kDisplayInvert; +constexpr bool kDisplayRgbOrder = board::kDisplayRgbOrder; +#endif + +constexpr int kDisplayBaseSize = 240; +#if defined(PLANE_RADAR_BOARD_ESP32_P4_WAVESHARE_4C) +constexpr int kDisplayWidth = board::kDisplayWidth; +constexpr int kDisplayHeight = board::kDisplayHeight; +#else constexpr int kDisplayWidth = 240; constexpr int kDisplayHeight = 240; +#endif -constexpr uint32_t kDisplaySpiWriteHz = 40000000; -// GC9A01 modules often need invert + BGR for correct black/green output -constexpr bool kDisplayInvert = true; -constexpr bool kDisplayRgbOrder = true; - -// --- Radar center defaults (overridden via WiFi setup portal) --- -constexpr double kDefaultRadarLat = 52.3676; -constexpr double kDefaultRadarLon = 4.9041; +#if defined(PLANE_RADAR_BOARD_ESP32_P4_WAVESHARE_4C) +constexpr bool kDisplayBacklightInvert = board::kDisplayBacklightInvert; +constexpr gpio_num_t kTouchPinSda = board::kTouchPinSda; +constexpr gpio_num_t kTouchPinScl = board::kTouchPinScl; +#endif /** Poll adsb.fi (API public limit: 1 req/s). */ constexpr unsigned long kAdsbFetchIntervalMs = 3000; diff --git a/include/hardware/display.h b/include/hardware/display.h index 6191f1776..ea837f976 100644 --- a/include/hardware/display.h +++ b/include/hardware/display.h @@ -5,3 +5,5 @@ extern LGFX tft; void displayInit(); +/** Turn panel backlight on after init/first draw (keeps boot garbage hidden). */ +void displayBacklightOn(); diff --git a/include/hardware/lgfx_config.hpp b/include/hardware/lgfx_config.hpp index 4f1484b6a..24c6d20d9 100644 --- a/include/hardware/lgfx_config.hpp +++ b/include/hardware/lgfx_config.hpp @@ -1,36 +1,7 @@ #pragma once -#define LGFX_USE_V1 -#include - -#include "config.h" - -/** LovyanGFX device: GC9A01 on SPI. Pin values come from config.h. */ -class LGFX : public lgfx::LGFX_Device { - lgfx::Bus_SPI _bus; - lgfx::Panel_GC9A01 _panel; - -public: - LGFX() { - { - auto cfg = _bus.config(); - cfg.spi_host = SPI2_HOST; - cfg.freq_write = config::kDisplaySpiWriteHz; - cfg.pin_sclk = static_cast(config::kDisplayPinSclk); - cfg.pin_mosi = static_cast(config::kDisplayPinMosi); - cfg.pin_miso = -1; - cfg.pin_dc = static_cast(config::kDisplayPinDc); - _bus.config(cfg); - _panel.setBus(&_bus); - } - { - auto cfg = _panel.config(); - cfg.pin_cs = static_cast(config::kDisplayPinCs); - cfg.pin_rst = static_cast(config::kDisplayPinRst); - cfg.invert = config::kDisplayInvert; - cfg.rgb_order = config::kDisplayRgbOrder; - _panel.config(cfg); - } - setPanel(&_panel); - } -}; +#if defined(PLANE_RADAR_BOARD_ESP32_P4_WAVESHARE_4C) +#include "hardware/lgfx_config_dsi.hpp" +#else +#include "hardware/lgfx_config_spi.hpp" +#endif diff --git a/include/hardware/lgfx_config_dsi.hpp b/include/hardware/lgfx_config_dsi.hpp new file mode 100644 index 000000000..9ff21632f --- /dev/null +++ b/include/hardware/lgfx_config_dsi.hpp @@ -0,0 +1,49 @@ +#pragma once + +#define LGFX_USE_V1 +#include + +#include "config.h" +#include "hardware/panel_jd9365_waveshare_4c.hpp" + +/** LovyanGFX device: JD9365 on MIPI DSI (Waveshare ESP32-P4 4C). */ +class LGFX : public lgfx::LGFX_Device { + lgfx::Bus_DSI _bus; + lgfx::Panel_JD9365_Waveshare4C _panel; + + public: + LGFX() { + { + auto cfg = _bus.config(); + cfg.lane_mbps = board::kMipiLaneMbps; + cfg.lane_num = board::kMipiLaneNum; + cfg.ldo_chan_id = board::kMipiLdoChan; + cfg.ldo_voltage_mv = board::kMipiLdoVoltageMv; + _bus.config(cfg); + _panel.setBus(&_bus); + } + { + auto cfg = _panel.config(); + cfg.panel_width = board::kDisplayWidth; + cfg.panel_height = board::kDisplayHeight; + cfg.memory_width = board::kDisplayWidth; + cfg.memory_height = board::kDisplayHeight; + cfg.pin_rst = static_cast(board::kDisplayPinRst); + cfg.invert = board::kDisplayInvert; + cfg.rgb_order = board::kDisplayRgbOrder; + _panel.config(cfg); + } + { + auto detail = _panel.config_detail(); + detail.dpi_freq_mhz = board::kDpiClockMhz; + detail.hsync_pulse_width = board::kDpiHsyncPulse; + detail.hsync_back_porch = board::kDpiHsyncBackPorch; + detail.hsync_front_porch = board::kDpiHsyncFrontPorch; + detail.vsync_pulse_width = board::kDpiVsyncPulse; + detail.vsync_back_porch = board::kDpiVsyncBackPorch; + detail.vsync_front_porch = board::kDpiVsyncFrontPorch; + _panel.config_detail(detail); + } + setPanel(&_panel); + } +}; diff --git a/include/hardware/lgfx_config_spi.hpp b/include/hardware/lgfx_config_spi.hpp new file mode 100644 index 000000000..77297734c --- /dev/null +++ b/include/hardware/lgfx_config_spi.hpp @@ -0,0 +1,36 @@ +#pragma once + +#define LGFX_USE_V1 +#include + +#include "config.h" + +/** LovyanGFX device: GC9A01 on SPI. Pin values come from config.h. */ +class LGFX : public lgfx::LGFX_Device { + lgfx::Bus_SPI _bus; + lgfx::Panel_GC9A01 _panel; + + public: + LGFX() { + { + auto cfg = _bus.config(); + cfg.spi_host = config::kDisplaySpiHost; + cfg.freq_write = config::kDisplaySpiWriteHz; + cfg.pin_sclk = static_cast(config::kDisplayPinSclk); + cfg.pin_mosi = static_cast(config::kDisplayPinMosi); + cfg.pin_miso = -1; + cfg.pin_dc = static_cast(config::kDisplayPinDc); + _bus.config(cfg); + _panel.setBus(&_bus); + } + { + auto cfg = _panel.config(); + cfg.pin_cs = static_cast(config::kDisplayPinCs); + cfg.pin_rst = static_cast(config::kDisplayPinRst); + cfg.invert = config::kDisplayInvert; + cfg.rgb_order = config::kDisplayRgbOrder; + _panel.config(cfg); + } + setPanel(&_panel); + } +}; diff --git a/include/hardware/panel_jd9365_waveshare_4c.hpp b/include/hardware/panel_jd9365_waveshare_4c.hpp new file mode 100644 index 000000000..298cb1559 --- /dev/null +++ b/include/hardware/panel_jd9365_waveshare_4c.hpp @@ -0,0 +1,245 @@ +#pragma once + +#include +#if SOC_MIPI_DSI_SUPPORTED + +namespace lgfx { +inline namespace v1 { + +/** JD9365 init for Waveshare ESP32-P4-WIFI6-Touch-LCD-4C (720x720). */ +struct Panel_JD9365_Waveshare4C : public Panel_DSI { + protected: + const uint8_t* getInitParams(size_t listno) const override { + static constexpr uint8_t list0[] = { + // len(cmd+params), cmd, params... + 0x02, 0xE0, 0x00, + 0x02, 0xE1, 0x93, + 0x02, 0xE2, 0x65, + 0x02, 0xE3, 0xF8, + 0x02, 0x80, 0x01, + 0x02, 0xE0, 0x01, + 0x02, 0x00, 0x00, + 0x02, 0x01, 0x41, + 0x02, 0x03, 0x10, + 0x02, 0x04, 0x44, + 0x02, 0x17, 0x00, + 0x02, 0x18, 0xD0, + 0x02, 0x19, 0x00, + 0x02, 0x1A, 0x00, + 0x02, 0x1B, 0xD0, + 0x02, 0x1C, 0x00, + 0x02, 0x24, 0xFE, + 0x02, 0x35, 0x26, + 0x02, 0x37, 0x09, + 0x02, 0x38, 0x04, + 0x02, 0x39, 0x08, + 0x02, 0x3A, 0x0A, + 0x02, 0x3C, 0x78, + 0x02, 0x3D, 0xFF, + 0x02, 0x3E, 0xFF, + 0x02, 0x3F, 0xFF, + 0x02, 0x40, 0x04, + 0x02, 0x41, 0x64, + 0x02, 0x42, 0xC7, + 0x02, 0x43, 0x18, + 0x02, 0x44, 0x0B, + 0x02, 0x45, 0x14, + 0x02, 0x55, 0x02, + 0x02, 0x57, 0x49, + 0x02, 0x59, 0x0A, + 0x02, 0x5A, 0x1B, + 0x02, 0x5B, 0x19, + 0x02, 0x5D, 0x7F, + 0x02, 0x5E, 0x56, + 0x02, 0x5F, 0x43, + 0x02, 0x60, 0x37, + 0x02, 0x61, 0x33, + 0x02, 0x62, 0x25, + 0x02, 0x63, 0x2A, + 0x02, 0x64, 0x16, + 0x02, 0x65, 0x30, + 0x02, 0x66, 0x2F, + 0x02, 0x67, 0x32, + 0x02, 0x68, 0x53, + 0x02, 0x69, 0x43, + 0x02, 0x6A, 0x4C, + 0x02, 0x6B, 0x40, + 0x02, 0x6C, 0x3D, + 0x02, 0x6D, 0x31, + 0x02, 0x6E, 0x20, + 0x02, 0x6F, 0x0F, + 0x02, 0x70, 0x7F, + 0x02, 0x71, 0x56, + 0x02, 0x72, 0x43, + 0x02, 0x73, 0x37, + 0x02, 0x74, 0x33, + 0x02, 0x75, 0x25, + 0x02, 0x76, 0x2A, + 0x02, 0x77, 0x16, + 0x02, 0x78, 0x30, + 0x02, 0x79, 0x2F, + 0x02, 0x7A, 0x32, + 0x02, 0x7B, 0x53, + 0x02, 0x7C, 0x43, + 0x02, 0x7D, 0x4C, + 0x02, 0x7E, 0x40, + 0x02, 0x7F, 0x3D, + 0x02, 0x80, 0x31, + 0x02, 0x81, 0x20, + 0x02, 0x82, 0x0F, + 0x02, 0xE0, 0x02, + 0x02, 0x00, 0x5F, + 0x02, 0x01, 0x5F, + 0x02, 0x02, 0x5E, + 0x02, 0x03, 0x5E, + 0x02, 0x04, 0x50, + 0x02, 0x05, 0x48, + 0x02, 0x06, 0x48, + 0x02, 0x07, 0x4A, + 0x02, 0x08, 0x4A, + 0x02, 0x09, 0x44, + 0x02, 0x0A, 0x44, + 0x02, 0x0B, 0x46, + 0x02, 0x0C, 0x46, + 0x02, 0x0D, 0x5F, + 0x02, 0x0E, 0x5F, + 0x02, 0x0F, 0x57, + 0x02, 0x10, 0x57, + 0x02, 0x11, 0x77, + 0x02, 0x12, 0x77, + 0x02, 0x13, 0x40, + 0x02, 0x14, 0x42, + 0x02, 0x15, 0x5F, + 0x02, 0x16, 0x5F, + 0x02, 0x17, 0x5F, + 0x02, 0x18, 0x5E, + 0x02, 0x19, 0x5E, + 0x02, 0x1A, 0x50, + 0x02, 0x1B, 0x49, + 0x02, 0x1C, 0x49, + 0x02, 0x1D, 0x4B, + 0x02, 0x1E, 0x4B, + 0x02, 0x1F, 0x45, + 0x02, 0x20, 0x45, + 0x02, 0x21, 0x47, + 0x02, 0x22, 0x47, + 0x02, 0x23, 0x5F, + 0x02, 0x24, 0x5F, + 0x02, 0x25, 0x57, + 0x02, 0x26, 0x57, + 0x02, 0x27, 0x77, + 0x02, 0x28, 0x77, + 0x02, 0x29, 0x41, + 0x02, 0x2A, 0x43, + 0x02, 0x2B, 0x5F, + 0x02, 0x2C, 0x1E, + 0x02, 0x2D, 0x1E, + 0x02, 0x2E, 0x1F, + 0x02, 0x2F, 0x1F, + 0x02, 0x30, 0x10, + 0x02, 0x31, 0x07, + 0x02, 0x32, 0x07, + 0x02, 0x33, 0x05, + 0x02, 0x34, 0x05, + 0x02, 0x35, 0x0B, + 0x02, 0x36, 0x0B, + 0x02, 0x37, 0x09, + 0x02, 0x38, 0x09, + 0x02, 0x39, 0x1F, + 0x02, 0x3A, 0x1F, + 0x02, 0x3B, 0x17, + 0x02, 0x3C, 0x17, + 0x02, 0x3D, 0x17, + 0x02, 0x3E, 0x17, + 0x02, 0x3F, 0x03, + 0x02, 0x40, 0x01, + 0x02, 0x41, 0x1F, + 0x02, 0x42, 0x1E, + 0x02, 0x43, 0x1E, + 0x02, 0x44, 0x1F, + 0x02, 0x45, 0x1F, + 0x02, 0x46, 0x10, + 0x02, 0x47, 0x06, + 0x02, 0x48, 0x06, + 0x02, 0x49, 0x04, + 0x02, 0x4A, 0x04, + 0x02, 0x4B, 0x0A, + 0x02, 0x4C, 0x0A, + 0x02, 0x4D, 0x08, + 0x02, 0x4E, 0x08, + 0x02, 0x4F, 0x1F, + 0x02, 0x50, 0x1F, + 0x02, 0x51, 0x17, + 0x02, 0x52, 0x17, + 0x02, 0x53, 0x17, + 0x02, 0x54, 0x17, + 0x02, 0x55, 0x02, + 0x02, 0x56, 0x00, + 0x02, 0x57, 0x1F, + 0x02, 0xE0, 0x02, + 0x02, 0x58, 0x40, + 0x02, 0x59, 0x00, + 0x02, 0x5A, 0x00, + 0x02, 0x5B, 0x30, + 0x02, 0x5C, 0x01, + 0x02, 0x5D, 0x30, + 0x02, 0x5E, 0x01, + 0x02, 0x5F, 0x02, + 0x02, 0x60, 0x30, + 0x02, 0x61, 0x03, + 0x02, 0x62, 0x04, + 0x02, 0x63, 0x04, + 0x02, 0x64, 0xA6, + 0x02, 0x65, 0x43, + 0x02, 0x66, 0x30, + 0x02, 0x67, 0x73, + 0x02, 0x68, 0x05, + 0x02, 0x69, 0x04, + 0x02, 0x6A, 0x7F, + 0x02, 0x6B, 0x08, + 0x02, 0x6C, 0x00, + 0x02, 0x6D, 0x04, + 0x02, 0x6E, 0x04, + 0x02, 0x6F, 0x88, + 0x02, 0x75, 0xD9, + 0x02, 0x76, 0x00, + 0x02, 0x77, 0x33, + 0x02, 0x78, 0x43, + 0x02, 0xE0, 0x00, + 0x02, 0x11, 0x00, + 0, // end + }; + + static constexpr uint8_t list1[] = { + // len(cmd+params), cmd, params... + 0x02, 0x29, 0x00, + 0, // end + }; + + static constexpr uint8_t list2[] = { + // len(cmd+params), cmd, params... + 0x02, 0x35, 0x00, + 0, // end + }; + + switch (listno) { + case 0: return list0; + case 1: return list1; + case 2: return list2; + default: return nullptr; + } + } + + size_t getInitDelay(size_t listno) const override { + switch (listno) { + case 0: return 120; + case 1: return 20; + default: return 0; + } + } +}; + +} // namespace v1 +} // namespace lgfx + +#endif diff --git a/include/hardware/touch_gesture.h b/include/hardware/touch_gesture.h new file mode 100644 index 000000000..6fe9ef560 --- /dev/null +++ b/include/hardware/touch_gesture.h @@ -0,0 +1,15 @@ +#pragma once + +/** GT911 touch gestures (Waveshare ESP32-P4 4C). */ + +void touchGestureInit(); + +struct TouchGestureEvent { + /** +1 zoom out (larger range), -1 zoom in, 0 none. */ + int pinch_zoom = 0; + /** Triple single-finger tap toggles day/night palette. */ + bool color_mode_toggle = false; +}; + +/** One GT911 read per call — pinch and tap do not compete for the buffer. */ +TouchGestureEvent touchGesturePoll(); diff --git a/include/secrets.h.example b/include/secrets.h.example new file mode 100644 index 000000000..8c817de74 --- /dev/null +++ b/include/secrets.h.example @@ -0,0 +1,21 @@ +#pragma once + +/** Optional local overrides — copy to secrets.h (gitignored) for bench/dev builds. + * + * cp include/secrets.h.example include/secrets.h + * Edit secrets.h with your Wi-Fi and location. Never commit secrets.h. + */ +#define PLANE_RADAR_SECRETS_ACTIVE 1 + +namespace plane_radar_secrets { + +inline constexpr bool kWifiSkipPortal = false; +inline constexpr char kWifiSsid[] = "your-wifi-ssid"; +inline constexpr char kWifiPass[] = "your-wifi-password"; + +/** When true, overwrites NVS default location on boot (dev only). */ +inline constexpr bool kOverrideDefaultLocation = false; +inline constexpr double kDefaultRadarLat = 52.3676; +inline constexpr double kDefaultRadarLon = 4.9041; + +} // namespace plane_radar_secrets diff --git a/include/services/wifi_setup.h b/include/services/wifi_setup.h index b2489d17f..74918d905 100644 --- a/include/services/wifi_setup.h +++ b/include/services/wifi_setup.h @@ -14,5 +14,9 @@ bool wifiBootButtonPressed(); void bootButtonInit(); /** Latched short tap (survives blocking HTTP/display work). */ bool bootButtonConsumeTap(); +/** Call after setup WiFi flow so long-press reset cannot interrupt the portal. */ +void bootButtonEnableWifiReset(); +/** True while the captive setup portal is active. */ +bool wifiConfigPortalActive(); /** Call each loop iteration; triggers WiFi reset on long hold. */ void bootButtonPollLongPress(); diff --git a/include/ui/radar_color_mode.h b/include/ui/radar_color_mode.h new file mode 100644 index 000000000..a5e8ae13f --- /dev/null +++ b/include/ui/radar_color_mode.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +namespace ui::radar { + +enum ColorMode : uint8_t { kColorModeNight = 0, kColorModeDay = 1 }; + +/** Load saved color mode from flash. Call once at boot (via rangeInit). */ +void colorModeInit(); + +ColorMode colorMode(); +bool isDayMode(); + +/** Toggle night/day, save to flash, log to serial. */ +void colorModeToggle(); + +} // namespace ui::radar diff --git a/include/ui/radar_geo.h b/include/ui/radar_geo.h new file mode 100644 index 000000000..972e93c66 --- /dev/null +++ b/include/ui/radar_geo.h @@ -0,0 +1,15 @@ +#pragma once + +namespace ui::radar::geo { + +/** Flat-earth km offset from radar center (same model as aircraft). */ +void offsetKmFromCenter(float lat, float lon, float* dx_km, float* dy_km, + float* dist_km); + +/** Pixels per km for the active range preset. */ +float pxPerKm(); + +/** Map lat/lon to screen using current radar center and range zoom. */ +void latLonToScreen(float lat, float lon, int* out_x, int* out_y); + +} // namespace ui::radar::geo diff --git a/include/ui/radar_range.h b/include/ui/radar_range.h index a95c53275..2a4f306b9 100644 --- a/include/ui/radar_range.h +++ b/include/ui/radar_range.h @@ -38,6 +38,8 @@ constexpr size_t kRangePresetCount = void rangeInit(); /** Cycle preset and save to flash. */ void rangeNext(); +/** Previous preset (pinch-in / zoom in). */ +void rangePrev(); const RangePreset& rangeCurrent(); uint8_t rangeIndex(); /** ADSB fetch radius (km): scaled to screen edge so beyond-ring dots have data. */ diff --git a/include/ui/radar_runways.h b/include/ui/radar_runways.h new file mode 100644 index 000000000..6142e142a --- /dev/null +++ b/include/ui/radar_runways.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +namespace ui::radar { + +/** Draw local airport runways on the radar grid (geographic scale). */ +void drawRunways(lgfx::LGFXBase& gfx); + +} // namespace ui::radar diff --git a/include/ui/radar_theme.h b/include/ui/radar_theme.h index 2d763a638..fb5887334 100644 --- a/include/ui/radar_theme.h +++ b/include/ui/radar_theme.h @@ -2,73 +2,88 @@ #include +#include "config.h" + namespace ui::radar { -constexpr int kSize = 240; +inline constexpr int scalePx(int v) { + return v * config::kDisplayWidth / config::kDisplayBaseSize; +} + +inline constexpr float scalePxF(float v) { + return v * static_cast(config::kDisplayWidth) / + static_cast(config::kDisplayBaseSize); +} + +constexpr int kSize = config::kDisplayWidth; constexpr int kCenterX = kSize / 2; constexpr int kCenterY = kSize / 2; /** Outermost grid ring (inside edge labels). */ -constexpr int kGridOuterRadius = 107; +constexpr int kGridOuterRadius = scalePx(107); /** N: offset from top edge (top_center, negative = up). */ -constexpr int kCardinalNorthOffsetY = -1; +constexpr int kCardinalNorthOffsetY = scalePx(-1); /** S: offset from bottom edge (bottom_center, positive = down). */ -constexpr int kCardinalSouthOffsetY = 3; +constexpr int kCardinalSouthOffsetY = scalePx(3); /** Gap between scale label right edge and outer ring on the east spoke (px). */ -constexpr int kScaleGapFromOuterRing = 6; +constexpr int kScaleGapFromOuterRing = scalePx(6); /** Target cap height (px) for N/S/E/W. */ -constexpr int kCardinalLabelHeightPx = 14; +constexpr int kCardinalLabelHeightPx = scalePx(14); /** Scale label is this many px shorter than cardinals. */ -constexpr int kScaleBelowCardinalPx = 3; +constexpr int kScaleBelowCardinalPx = scalePx(3); constexpr int kRingCount = 4; /** Shared grid stroke: drawWideLine half-width (~2 px total); rings use the same px count. */ -constexpr float kGridStrokeHalfWidth = 1.0f; +constexpr float kGridStrokeHalfWidth = scalePxF(1.0f); -constexpr int kCenterDotRadius = 2; +constexpr int kCenterDotRadius = scalePx(2); /** Filled aircraft symbol (nose triangle). */ -constexpr int kAircraftNoseLenPx = 8; -constexpr int kAircraftTailLenPx = 3; -constexpr int kAircraftTailHalfPx = 4; +constexpr int kAircraftNoseLenPx = scalePx(8); +constexpr int kAircraftTailLenPx = scalePx(3); +constexpr int kAircraftTailHalfPx = scalePx(4); /** Track vector: ground distance covered in this many seconds at current gs. */ constexpr float kAircraftTrackHorizonSec = 60.0f; /** Minimum visible vector when gs > 0 (px). */ -constexpr int kAircraftSpeedLineMinPx = 2; +constexpr int kAircraftSpeedLineMinPx = scalePx(2); /** Track line length uses this outer_km, not the active range preset. */ constexpr float kAircraftTrackRefOuterKm = 13.3f; /** Shorter than full 60 s horizon at ref scale; ×1.5 length boost applied. */ constexpr float kAircraftTrackLengthScale = 1.5f / 5.0f; /** drawWideLine half-width for speed vectors (~2 px total). */ -constexpr float kAircraftTrackLineHalfWidth = 1.0f; +constexpr float kAircraftTrackLineHalfWidth = scalePxF(1.0f); -constexpr float kRunwayLineWidthPx = 2.0f; +constexpr float kRunwayLineWidthPx = scalePxF(2.0f); constexpr float kRunwayLineHalfWidth = kRunwayLineWidthPx * 0.5f; constexpr int kRunwayLabelHeightPx = kCardinalLabelHeightPx; -constexpr int kRunwayLabelGapPx = 3; +constexpr int kRunwayLabelGapPx = scalePx(3); /** Gap from triangle edge to tag block (px). */ -constexpr int kAircraftLabelGapPx = 1; +constexpr int kAircraftLabelGapPx = scalePx(1); /** Keep symbol centroid inside outer ring by at least this inset (px). */ constexpr int kAircraftInsideRingInsetPx = - kAircraftNoseLenPx + kAircraftTailHalfPx + 1; + kAircraftNoseLenPx + kAircraftTailHalfPx + scalePx(1); /** Beyond-ring traffic: bearing cues on screen rim (correct direction, fixed radius). */ -constexpr int kBeyondRingDotRadiusPx = 4; -constexpr int kBeyondRingScreenMarginPx = 2; +constexpr int kBeyondRingDotRadiusPx = scalePx(4); +constexpr int kBeyondRingScreenMarginPx = scalePx(2); /** Target cap height (px) for aircraft tags (bold, slightly above scale label). */ -constexpr int kAircraftTagLabelHeightPx = 13; +constexpr int kAircraftTagLabelHeightPx = scalePx(13); /** RGB565 palette targets (applied in initPalette). */ +// Night — classic sonar (default). constexpr uint8_t kBgR = 4; constexpr uint8_t kBgG = 10; constexpr uint8_t kBgB = 28; constexpr uint8_t kGridR = 16; constexpr uint8_t kGridG = 100; constexpr uint8_t kGridB = 32; +constexpr uint8_t kLabelR = 255; +constexpr uint8_t kLabelG = 255; +constexpr uint8_t kLabelB = 255; constexpr uint8_t kAircraftR = 255; constexpr uint8_t kAircraftG = 0; constexpr uint8_t kAircraftB = 0; @@ -84,11 +99,39 @@ constexpr uint8_t kTagAltB = 255; constexpr uint8_t kRunwayR = 56; constexpr uint8_t kRunwayG = 150; constexpr uint8_t kRunwayB = 170; -/** Lighter teal for ICAO labels (vs runway lines). */ constexpr uint8_t kRunwayLabelR = 110; constexpr uint8_t kRunwayLabelG = 210; constexpr uint8_t kRunwayLabelB = 230; +// Day — light background, dark grid and labels. +constexpr uint8_t kDayBgR = 245; +constexpr uint8_t kDayBgG = 248; +constexpr uint8_t kDayBgB = 252; +constexpr uint8_t kDayGridR = 24; +constexpr uint8_t kDayGridG = 130; +constexpr uint8_t kDayGridB = 56; +constexpr uint8_t kDayLabelR = 24; +constexpr uint8_t kDayLabelG = 28; +constexpr uint8_t kDayLabelB = 36; +constexpr uint8_t kDayAircraftR = 210; +constexpr uint8_t kDayAircraftG = 0; +constexpr uint8_t kDayAircraftB = 0; +constexpr uint8_t kDayTrackR = 130; +constexpr uint8_t kDayTrackG = 0; +constexpr uint8_t kDayTrackB = 170; +constexpr uint8_t kDayTagTypeR = 170; +constexpr uint8_t kDayTagTypeG = 90; +constexpr uint8_t kDayTagTypeB = 0; +constexpr uint8_t kDayTagAltR = 0; +constexpr uint8_t kDayTagAltG = 90; +constexpr uint8_t kDayTagAltB = 150; +constexpr uint8_t kDayRunwayR = 36; +constexpr uint8_t kDayRunwayG = 105; +constexpr uint8_t kDayRunwayB = 118; +constexpr uint8_t kDayRunwayLabelR = 18; +constexpr uint8_t kDayRunwayLabelG = 95; +constexpr uint8_t kDayRunwayLabelB = 108; + extern uint16_t kColorBackground; extern uint16_t kColorGrid; extern uint16_t kColorLabel; diff --git a/platformio.ini b/platformio.ini index c81ce535f..ad39a9762 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,6 +1,9 @@ ; ESP32-C3 Super Mini + 1.28" round GC9A01 (240×240) ; Board in Arduino IDE: "ESP32C3 Dev Module" or "MakerGO ESP32 C3 SuperMini" +[platformio] +default_envs = supermini + [env:supermini] platform = espressif32@6.5.0 board = esp32-c3-devkitm-1 @@ -24,3 +27,57 @@ lib_deps = lovyan03/LovyanGFX@^1.2.7 tzapu/WiFiManager@^2.0.17 bblanchon/ArduinoJson@^7.4.2 + +; Classic ESP32-TFT 88267 — ESP32-D0WD + integrated GC9A01 round (parts-catalog bench ID 2026-06-02) +; USB: WCH CH343 (1A86:55D3). Flash: 16 MB. + +[env:esp32-tft-round] +platform = espressif32@6.5.0 +board = esp32dev +framework = arduino +monitor_speed = 115200 +upload_speed = 115200 +upload_resetmethod = nodemcu +board_build.flash_size = 16MB +extra_scripts = post:scripts/merge_firmware.py +board_build.embed_files = data/ui_font.vlw + +upload_flags = + --no-stub + +build_flags = + -DPLANE_RADAR_BOARD_ESP32_TFT_ROUND + -DWM_NODEBUG + -DWM_MDNS + -DCONFIG_ARDUINO_LOOP_STACK_SIZE=16384 + +lib_deps = + lovyan03/LovyanGFX@^1.2.7 + tzapu/WiFiManager@^2.0.17 + bblanchon/ArduinoJson@^7.4.2 + +; Waveshare ESP32-P4-WIFI6-Touch-LCD-4C — 720×720 MIPI DSI (JD9365), ESP32-C6 Wi-Fi 6 +; Toolchain: pioarduino (official platform-espressif32 lacks ESP32-P4) + +[env:esp32-p4-waveshare-4c] +platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip +board = esp32-p4-evboard +framework = arduino +monitor_speed = 115200 +upload_speed = 921600 +board_build.flash_size = 32MB +board_build.partitions = partitions/plane_radar.csv +extra_scripts = post:scripts/merge_firmware.py +board_build.embed_files = data/ui_font.vlw + +build_flags = + -DPLANE_RADAR_BOARD_ESP32_P4_WAVESHARE_4C + -DWM_NODEBUG + -DBOARD_HAS_PSRAM + -DCONFIG_ARDUINO_LOOP_STACK_SIZE=16384 + -DCONFIG_ESP32_PHY_MAX_WIFI_TX_POWER=20 + +lib_deps = + https://github.com/lovyan03/LovyanGFX.git#develop + tzapu/WiFiManager@^2.0.17 + bblanchon/ArduinoJson@^7.4.2 diff --git a/scripts/convert_jd9365_init.py b/scripts/convert_jd9365_init.py new file mode 100644 index 000000000..2acf09d83 --- /dev/null +++ b/scripts/convert_jd9365_init.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""Convert xiaozhi jd9365_lcd_init_cmd_t tables to LovyanGFX Panel_DSI getInitParams lists.""" + +import re +import sys +from pathlib import Path + +CMD_RE = re.compile( + r"\{0x([0-9A-Fa-f]+),\s*\(uint8_t\[\]\)\{(.*?)\},\s*(\d+),\s*(\d+)\}" +) + + +def parse_cmds(text: str) -> list[tuple[int, list[int], int]]: + cmds = [] + for m in CMD_RE.finditer(text): + cmd = int(m.group(1), 16) + params_str = m.group(2).strip() + params = [] + if params_str: + for p in params_str.split(","): + p = p.strip() + if p.startswith("0x"): + params.append(int(p, 16)) + elif p: + params.append(int(p)) + delay = int(m.group(4)) + cmds.append((cmd, params, delay)) + return cmds + + +def split_lists(cmds: list[tuple[int, list[int], int]]) -> list[tuple[list[tuple[int, list[int]]], int]]: + lists: list[tuple[list[tuple[int, list[int]]], int]] = [] + current: list[tuple[int, list[int]]] = [] + trailing_delay = 0 + for cmd, params, delay in cmds: + current.append((cmd, params)) + if delay > 0: + lists.append((current, delay)) + current = [] + trailing_delay = 0 + else: + trailing_delay = 0 + if current: + lists.append((current, trailing_delay)) + return lists + + +def emit_header(lists) -> str: + lines = [ + "#pragma once", + "", + "#include ", + "#if SOC_MIPI_DSI_SUPPORTED", + "", + "namespace lgfx {", + "inline namespace v1 {", + "", + "/** JD9365 init for Waveshare ESP32-P4-WIFI6-Touch-LCD-4C (720x720). */", + "struct Panel_JD9365_Waveshare4C : public Panel_DSI {", + " protected:", + " const uint8_t* getInitParams(size_t listno) const override {", + ] + for i, (cmds, _) in enumerate(lists): + lines.append(f" static constexpr uint8_t list{i}[] = {{") + lines.append(" // len(cmd+params), cmd, params...") + for cmd, params in cmds: + total_len = 1 + len(params) + byte_str = ", ".join(f"0x{b:02X}" for b in [total_len, cmd] + params) + lines.append(f" {byte_str},") + lines.append(" 0, // end") + lines.append(" };") + lines.append("") + lines.append(" switch (listno) {") + for i in range(len(lists)): + lines.append(f" case {i}: return list{i};") + lines.append(" default: return nullptr;") + lines.append(" }") + lines.append(" }") + lines.append("") + lines.append(" size_t getInitDelay(size_t listno) const override {") + lines.append(" switch (listno) {") + for i, (_, delay) in enumerate(lists): + if delay: + lines.append(f" case {i}: return {delay};") + lines.append(" default: return 0;") + lines.append(" }") + lines.append(" }") + lines.append("};") + lines.append("") + lines.append("} // namespace v1") + lines.append("} // namespace lgfx") + lines.append("") + lines.append("#endif") + lines.append("") + return "\n".join(lines) + + +def main() -> int: + src = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("/tmp/jd9365_4c_init.txt") + dst = ( + Path(sys.argv[2]) + if len(sys.argv) > 2 + else Path(__file__).resolve().parent.parent + / "include/hardware/panel_jd9365_waveshare_4c.hpp" + ) + text = src.read_text() + cmds = parse_cmds(text) + if not cmds: + print("No commands parsed", file=sys.stderr) + return 1 + lists = split_lists(cmds) + dst.write_text(emit_header(lists)) + print(f"Wrote {len(cmds)} cmds in {len(lists)} lists -> {dst}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/flash-esp32-p4-waveshare-4c.sh b/scripts/flash-esp32-p4-waveshare-4c.sh new file mode 100755 index 000000000..333f03023 --- /dev/null +++ b/scripts/flash-esp32-p4-waveshare-4c.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Flash Waveshare ESP32-P4-WIFI6-Touch-LCD-4C (720×720 MIPI DSI). +# Usage: ./scripts/flash-esp32-p4-waveshare-4c.sh [PORT] +set -euo pipefail +cd "$(dirname "$0")/.." + +ENV=esp32-p4-waveshare-4c +PIO="pio" +if [[ -x .venv-pio/bin/pio ]]; then + PIO=".venv-pio/bin/pio" +fi + +PORT="${1:-}" +if [[ -z "$PORT" ]]; then + PORT="$(ls /dev/cu.usbserial* /dev/cu.wchusbserial* /dev/cu.usbmodem* 2>/dev/null | head -1 || true)" +fi +if [[ -z "$PORT" ]]; then + echo "No serial port found. Connect USB TO UART Type-C and retry." >&2 + exit 1 +fi + +echo "Port: $PORT" +echo "Env: $ENV (pioarduino / ESP32-P4 + LovyanGFX DSI)" +echo "PIO: $PIO" + +"$PIO" run -e "$ENV" -t upload --upload-port "$PORT" +echo "Done. Monitor: $PIO device monitor -p $PORT -b 115200" diff --git a/scripts/flash-esp32-tft.sh b/scripts/flash-esp32-tft.sh new file mode 100755 index 000000000..140bcf960 --- /dev/null +++ b/scripts/flash-esp32-tft.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Flash ESP32-TFT round module (CH343 USB, 16 MB merged image). +# Usage: ./scripts/flash-esp32-tft.sh [PORT] +set -euo pipefail +cd "$(dirname "$0")/.." + +PORT="${1:-}" +if [[ -z "$PORT" ]]; then + PORT="$(ls /dev/cu.wchusbserial* /dev/cu.usbmodem* 2>/dev/null | head -1 || true)" +fi +if [[ -z "$PORT" ]]; then + echo "No serial port found. Power on module (middle button) and replug USB." >&2 + exit 1 +fi + +echo "Port: $PORT" +echo "HackerBox 0107: press the MIDDLE power button first (red LED) so CH343 enumerates." +echo "macOS Apple Silicon: WCH CH34xVCPDriver.dmg + Driver Extension, then /dev/cu.wchusbserial*" + +if pio run -e esp32-tft-round -t upload --upload-port "$PORT"; then + echo "Done. Monitor: pio device monitor -p $PORT -b 115200" + exit 0 +fi + +echo "PlatformIO upload failed; trying merged binary at 0x0..." >&2 +pio run -e esp32-tft-round -t merge +MERGED=".pio/build/esp32-tft-round/firmware-merged.bin" +ESPTOOL_DIR="$(pio pkg exec -p espressif32 --where tool-esptoolpy 2>/dev/null | head -1)" +if [[ -z "$ESPTOOL_DIR" || ! -f "$ESPTOOL_DIR/esptool.py" ]]; then + echo "Could not locate esptool.py from PlatformIO espressif32 package." >&2 + exit 1 +fi +python3 "$ESPTOOL_DIR/esptool.py" \ + --chip esp32 --port "$PORT" --baud 115200 \ + --before default_reset --after hard_reset \ + --no-stub write_flash --flash_mode dio --flash_freq 40m --flash_size 16MB \ + 0x0 "$MERGED" +echo "Done. Monitor: pio device monitor -p $PORT -b 115200" diff --git a/scripts/merge_firmware.py b/scripts/merge_firmware.py index 2dfcd9661..dce4acb60 100644 --- a/scripts/merge_firmware.py +++ b/scripts/merge_firmware.py @@ -15,7 +15,7 @@ def merge_firmware(source, target, env): boot_app0 = join(framework_dir, "tools", "partitions", "boot_app0.bin") merged = join(build_dir, "firmware-merged.bin") mcu = env.BoardConfig().get("build.mcu", "esp32c3") - flash_size = env.BoardConfig().get("upload.flash_size", "4MB") + flash_size = env.GetProjectOption("board_build.flash_size") or env.BoardConfig().get("upload.flash_size", "4MB") bootloader = join(build_dir, "bootloader.bin") partitions = join(build_dir, "partitions.bin") diff --git a/src/hardware/display.cpp b/src/hardware/display.cpp index 42f82524d..853cebf75 100644 --- a/src/hardware/display.cpp +++ b/src/hardware/display.cpp @@ -1,13 +1,92 @@ #include "hardware/display.h" +#include "config.h" #include "hardware/display_font.h" +#if defined(PLANE_RADAR_BOARD_ESP32_P4_WAVESHARE_4C) +#include +#endif + LGFX tft; +#if defined(PLANE_RADAR_BOARD_ESP32_P4_WAVESHARE_4C) +namespace { +constexpr ledc_channel_t kBacklightChannel = LEDC_CHANNEL_0; +constexpr uint32_t kBacklightPwmHz = 5000; +bool s_backlight_pwm_ready = false; + +void initBacklightPwm() { + if (config::kDisplayPinBl == GPIO_NUM_NC || s_backlight_pwm_ready) { + return; + } + ledc_timer_config_t timer_cfg = {}; + timer_cfg.speed_mode = LEDC_LOW_SPEED_MODE; + timer_cfg.timer_num = LEDC_TIMER_0; + timer_cfg.duty_resolution = LEDC_TIMER_8_BIT; + timer_cfg.freq_hz = kBacklightPwmHz; + timer_cfg.clk_cfg = LEDC_AUTO_CLK; + ledc_timer_config(&timer_cfg); + + ledc_channel_config_t ch_cfg = {}; + ch_cfg.gpio_num = config::kDisplayPinBl; + ch_cfg.speed_mode = LEDC_LOW_SPEED_MODE; + ch_cfg.channel = kBacklightChannel; + ch_cfg.timer_sel = LEDC_TIMER_0; + ch_cfg.intr_type = LEDC_INTR_DISABLE; + ch_cfg.duty = 0; + ch_cfg.hpoint = 0; + ledc_channel_config(&ch_cfg); + s_backlight_pwm_ready = true; +} + +void setBacklightDuty(uint8_t brightness) { + if (!s_backlight_pwm_ready) { + return; + } + const uint32_t duty = config::kDisplayBacklightInvert + ? (255U - brightness) + : static_cast(brightness); + ledc_set_duty(LEDC_LOW_SPEED_MODE, kBacklightChannel, duty); + ledc_update_duty(LEDC_LOW_SPEED_MODE, kBacklightChannel); +} +} // namespace +#endif + +static void deselectSharedSpiDevices() { +#if defined(PLANE_RADAR_BOARD_ESP32_TFT_ROUND) + if (board::kSdCardPinCs != GPIO_NUM_NC && + board::kSdCardPinCs != board::kDisplayPinCs) { + pinMode(static_cast(board::kSdCardPinCs), OUTPUT); + digitalWrite(static_cast(board::kSdCardPinCs), HIGH); + } +#endif +} + void displayInit() { + deselectSharedSpiDevices(); +#if defined(PLANE_RADAR_BOARD_ESP32_P4_WAVESHARE_4C) + initBacklightPwm(); + setBacklightDuty(0); +#else + if (config::kDisplayPinBl != GPIO_NUM_NC) { + pinMode(static_cast(config::kDisplayPinBl), OUTPUT); + digitalWrite(static_cast(config::kDisplayPinBl), LOW); + } +#endif tft.init(); tft.setRotation(0); tft.setBrightness(255); tft.setTextWrap(false); displayFontInit(); + tft.fillScreen(config::kColorBlack); +} + +void displayBacklightOn() { +#if defined(PLANE_RADAR_BOARD_ESP32_P4_WAVESHARE_4C) + setBacklightDuty(255); +#else + if (config::kDisplayPinBl != GPIO_NUM_NC) { + digitalWrite(static_cast(config::kDisplayPinBl), HIGH); + } +#endif } diff --git a/src/hardware/touch_gesture.cpp b/src/hardware/touch_gesture.cpp new file mode 100644 index 000000000..a9f7aef73 --- /dev/null +++ b/src/hardware/touch_gesture.cpp @@ -0,0 +1,283 @@ +#include "hardware/touch_gesture.h" + +#include "config.h" + +#if defined(PLANE_RADAR_BOARD_ESP32_P4_WAVESHARE_4C) + +#include +#include + +#include + +namespace { + +constexpr uint16_t kGt911RegStatus = 0x814E; +constexpr uint16_t kGt911RegPoint1 = 0x8150; +constexpr uint32_t kI2cHz = 400000UL; +constexpr int kPinchDeltaThresholdPx = 40; +constexpr unsigned long kPinchCooldownMs = 600UL; + +constexpr unsigned long kTapMaxMs = 400UL; +constexpr unsigned long kInterTapMaxMs = 600UL; +constexpr unsigned long kTripleTapCooldownMs = 700UL; +constexpr int kTapMoveThresholdPx = 40; + +uint8_t s_touch_addr = board::kTouchI2cAddrPrimary; +bool s_ready = false; + +bool s_pinch_active = false; +float s_last_span_px = 0.0f; +unsigned long s_last_zoom_ms = 0; + +bool s_touch_down = false; +unsigned long s_down_ms = 0; +int s_down_x = 0; +int s_down_y = 0; +int s_last_x = 0; +int s_last_y = 0; +uint8_t s_tap_count = 0; +unsigned long s_last_tap_ms = 0; +unsigned long s_last_triple_ms = 0; + +bool i2cWriteReg16(uint16_t reg, const uint8_t* data, size_t len) { + Wire.beginTransmission(s_touch_addr); + Wire.write(static_cast(reg >> 8)); + Wire.write(static_cast(reg & 0xFF)); + for (size_t i = 0; i < len; ++i) { + Wire.write(data[i]); + } + return Wire.endTransmission() == 0; +} + +bool i2cReadReg16(uint16_t reg, uint8_t* data, size_t len) { + Wire.beginTransmission(s_touch_addr); + Wire.write(static_cast(reg >> 8)); + Wire.write(static_cast(reg & 0xFF)); + if (Wire.endTransmission(false) != 0) { + return false; + } + const int got = Wire.requestFrom(static_cast(s_touch_addr), + static_cast(len)); + if (got != static_cast(len)) { + return false; + } + for (size_t i = 0; i < len; ++i) { + data[i] = static_cast(Wire.read()); + } + return true; +} + +bool probeTouchAddr(uint8_t addr) { + Wire.beginTransmission(addr); + return Wire.endTransmission() == 0; +} + +void clearTouchStatus() { + const uint8_t clear = 0; + i2cWriteReg16(kGt911RegStatus, &clear, 1); +} + +/** + * One GT911 buffer read. had_event is true when the controller latched new data + * (0x80). When false, callers should not treat count==0 as finger lift. + */ +bool readTouchFrame(bool& had_event, uint8_t& count, int& x0, int& y0, int& x1, + int& y1) { + uint8_t status = 0; + if (!i2cReadReg16(kGt911RegStatus, &status, 1)) { + return false; + } + + had_event = (status & 0x80) != 0; + count = status & 0x0F; + x0 = 0; + y0 = 0; + x1 = 0; + y1 = 0; + + if (!had_event) { + return true; + } + + if (count >= 2) { + uint8_t raw[16] = {}; + if (!i2cReadReg16(kGt911RegPoint1, raw, sizeof(raw))) { + clearTouchStatus(); + return false; + } + x0 = static_cast(raw[1]) | (static_cast(raw[2]) << 8); + y0 = static_cast(raw[3]) | (static_cast(raw[4]) << 8); + x1 = static_cast(raw[9]) | (static_cast(raw[10]) << 8); + y1 = static_cast(raw[11]) | (static_cast(raw[12]) << 8); + } else if (count == 1) { + uint8_t raw[8] = {}; + if (!i2cReadReg16(kGt911RegPoint1, raw, sizeof(raw))) { + clearTouchStatus(); + return false; + } + x0 = static_cast(raw[1]) | (static_cast(raw[2]) << 8); + y0 = static_cast(raw[3]) | (static_cast(raw[4]) << 8); + } + + clearTouchStatus(); + return true; +} + +float spanPx(int x0, int y0, int x1, int y1) { + const float dx = static_cast(x1 - x0); + const float dy = static_cast(y1 - y0); + return std::sqrt(dx * dx + dy * dy); +} + +void registerTap(unsigned long now) { + if (s_tap_count > 0 && (now - s_last_tap_ms) > kInterTapMaxMs) { + s_tap_count = 0; + } + s_tap_count++; + s_last_tap_ms = now; +} + +bool consumeTripleTap(unsigned long now) { + if (s_tap_count < 3) { + return false; + } + s_tap_count = 0; + if (now - s_last_triple_ms < kTripleTapCooldownMs) { + return false; + } + s_last_triple_ms = now; + return true; +} + +int pollPinchFromFrame(bool had_event, uint8_t count, int x0, int y0, int x1, + int y1) { + if (!had_event) { + return 0; + } + + if (count < 2) { + s_pinch_active = false; + s_last_span_px = 0.0f; + return 0; + } + + s_touch_down = false; + s_tap_count = 0; + + const float span = spanPx(x0, y0, x1, y1); + if (!s_pinch_active) { + s_pinch_active = true; + s_last_span_px = span; + return 0; + } + + const float delta = span - s_last_span_px; + s_last_span_px = span; + + const unsigned long now = millis(); + if (now - s_last_zoom_ms < kPinchCooldownMs) { + return 0; + } + if (std::abs(delta) < static_cast(kPinchDeltaThresholdPx)) { + return 0; + } + + s_last_zoom_ms = now; + return (delta > 0.0f) ? 1 : -1; +} + +bool pollTripleTapFromFrame(bool had_event, uint8_t count, int x0, int y0) { + if (!had_event) { + return false; + } + + if (count >= 2) { + return false; + } + + const unsigned long now = millis(); + + if (count == 1) { + if (!s_touch_down) { + s_touch_down = true; + s_down_ms = now; + s_down_x = x0; + s_down_y = y0; + } + s_last_x = x0; + s_last_y = y0; + return false; + } + + if (!s_touch_down) { + return false; + } + + s_touch_down = false; + const unsigned long held = now - s_down_ms; + if (held > kTapMaxMs) { + return false; + } + + const int dx = s_last_x - s_down_x; + const int dy = s_last_y - s_down_y; + if ((dx * dx + dy * dy) > (kTapMoveThresholdPx * kTapMoveThresholdPx)) { + return false; + } + + registerTap(now); + return consumeTripleTap(now); +} + +} // namespace + +void touchGestureInit() { + Wire.begin(static_cast(config::kTouchPinSda), + static_cast(config::kTouchPinScl)); + Wire.setClock(kI2cHz); + if (probeTouchAddr(board::kTouchI2cAddrPrimary)) { + s_touch_addr = board::kTouchI2cAddrPrimary; + } else if (probeTouchAddr(board::kTouchI2cAddrBackup)) { + s_touch_addr = board::kTouchI2cAddrBackup; + } else { + Serial.println("touch: GT911 not found on I2C"); + return; + } + s_ready = true; + Serial.printf("touch: GT911 @ 0x%02X (pinch zoom, triple-tap day mode)\n", + s_touch_addr); +} + +TouchGestureEvent touchGesturePoll() { + TouchGestureEvent ev; + if (!s_ready) { + return ev; + } + + bool had_event = false; + uint8_t count = 0; + int x0 = 0; + int y0 = 0; + int x1 = 0; + int y1 = 0; + if (!readTouchFrame(had_event, count, x0, y0, x1, y1)) { + s_pinch_active = false; + s_last_span_px = 0.0f; + return ev; + } + + ev.pinch_zoom = pollPinchFromFrame(had_event, count, x0, y0, x1, y1); + if (ev.pinch_zoom != 0) { + return ev; + } + ev.color_mode_toggle = pollTripleTapFromFrame(had_event, count, x0, y0); + return ev; +} + +#else + +void touchGestureInit() {} + +TouchGestureEvent touchGesturePoll() { return {}; } + +#endif diff --git a/src/idf_component.yml b/src/idf_component.yml new file mode 100644 index 000000000..d752765ff --- /dev/null +++ b/src/idf_component.yml @@ -0,0 +1,2 @@ +dependencies: + idf: '>=5.1' diff --git a/src/main.cpp b/src/main.cpp index 797aecc09..41bf8c484 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,15 +1,18 @@ /** - * Plane Radar — WiFi setup, then radar UI on the round GC9A01 display. + * Plane Radar — WiFi setup, then radar UI on the round display. */ #include #include +#include #include "config.h" #include "hardware/display.h" +#include "hardware/touch_gesture.h" #include "services/adsb_client.h" #include "services/radar_location.h" #include "services/wifi_setup.h" +#include "ui/radar_color_mode.h" #include "ui/radar_display.h" #include "ui/radar_range.h" #include "ui/status_screens.h" @@ -30,8 +33,7 @@ void showRadarIfConnected() { g_radar_visible = true; } -void onRangeTap() { - ui::radar::rangeNext(); +void onRangeChanged() { char range_label[12]; ui::radar::formatCurrentRing3Label(range_label, sizeof(range_label)); Serial.printf("Range: %s (outer ~%.0f km)\n", range_label, @@ -42,6 +44,11 @@ void onRangeTap() { } } +void onRangeTap() { + ui::radar::rangeNext(); + onRangeChanged(); +} + void handleBootButton() { bootButtonPollLongPress(); if (bootButtonConsumeTap()) { @@ -49,15 +56,39 @@ void handleBootButton() { } } +void onColorModeChanged() { + if (g_radar_visible && WiFi.status() == WL_CONNECTED) { + ui::radarDisplayDraw(); + } +} + +void handleTouchGestures() { + const TouchGestureEvent touch = touchGesturePoll(); + if (touch.color_mode_toggle) { + ui::radar::colorModeToggle(); + onColorModeChanged(); + return; + } + if (touch.pinch_zoom > 0) { + ui::radar::rangeNext(); + onRangeChanged(); + } else if (touch.pinch_zoom < 0) { + ui::radar::rangePrev(); + onRangeChanged(); + } +} + void fetchAndDrawAircraft() { const float fetch_km = ui::radar::fetchRadiusKm(); if (!services::adsb::fetchUpdate(services::location::lat(), services::location::lon(), fetch_km)) { handleBootButton(); + handleTouchGestures(); return; } ui::radarDisplayRefreshAircraft(); handleBootButton(); + handleTouchGestures(); } } // namespace @@ -66,10 +97,13 @@ void setup() { Serial.begin(115200); delay(500); Serial.println(); - Serial.println("Plane Radar"); + Serial.printf("Plane Radar v%s\n", config::kFirmwareVersion); + Serial.printf("Reset reason: %d\n", static_cast(esp_reset_reason())); bootButtonInit(); displayInit(); + displayBacklightOn(); + touchGestureInit(); if (wifiShowsSetupScreenOnBoot()) { statusScreenPortal(); } @@ -79,11 +113,16 @@ void setup() { if (wifiSetupConnect()) { showRadarIfConnected(); + if (g_radar_visible) { + fetchAndDrawAircraft(); + } } + bootButtonEnableWifiReset(); } void loop() { handleBootButton(); + handleTouchGestures(); wifiLoop(); if (WiFi.status() != WL_CONNECTED) { diff --git a/src/services/adsb_client.cpp b/src/services/adsb_client.cpp index 84be21bb7..713fec741 100644 --- a/src/services/adsb_client.cpp +++ b/src/services/adsb_client.cpp @@ -1,6 +1,7 @@ #include "services/adsb_client.h" #include +#include #include #include @@ -197,6 +198,24 @@ void fillTagFields(Aircraft* ac, const JsonObject& plane) { formatAltitudeTag(plane, ac->alt, sizeof(ac->alt)); } +void buildAircraftJsonFilter(JsonDocument& filter) { + JsonObject ac0 = filter["ac"][0].to(); + ac0["lat"] = true; + ac0["lon"] = true; + ac0["flight"] = true; + ac0["hex"] = true; + ac0["t"] = true; + ac0["alt_baro"] = true; + ac0["alt_geom"] = true; + ac0["true_heading"] = true; + ac0["mag_heading"] = true; + ac0["track"] = true; + ac0["dir"] = true; + ac0["gs"] = true; + ac0["tas"] = true; + ac0["ias"] = true; +} + } // namespace void setPollFn(PollFn fn) { s_poll_fn = fn; } diff --git a/src/services/radar_location.cpp b/src/services/radar_location.cpp index a8c9c273b..81f784bf6 100644 --- a/src/services/radar_location.cpp +++ b/src/services/radar_location.cpp @@ -47,6 +47,15 @@ void persist(double lat, double lon) { } // namespace void init() { +#if defined(PLANE_RADAR_SECRETS_ACTIVE) && PLANE_RADAR_SECRETS_ACTIVE + if (plane_radar_secrets::kOverrideDefaultLocation) { + persist(config::kDefaultRadarLat, config::kDefaultRadarLon); + Serial.printf("Radar location (secrets override): %.8f, %.8f\n", s_lat, + s_lon); + return; + } +#endif + Preferences prefs; prefs.begin(kPrefsNamespace, true); if (prefs.isKey(kKeyLat) && prefs.isKey(kKeyLon)) { diff --git a/src/services/wifi_setup.cpp b/src/services/wifi_setup.cpp index 2fa8b7233..5dafd7f3c 100644 --- a/src/services/wifi_setup.cpp +++ b/src/services/wifi_setup.cpp @@ -18,12 +18,127 @@ #include "ui/radar_range.h" #include "ui/status_screens.h" +void wifiResetCredentialsAndReboot(); + portMUX_TYPE s_boot_mux = portMUX_INITIALIZER_UNLOCKED; volatile bool s_boot_tap_pending = false; volatile bool s_boot_is_down = false; volatile unsigned long s_boot_down_ms = 0; bool s_long_press_handled = false; bool s_boot_interrupt_attached = false; +bool s_config_portal_active = false; +bool s_allow_boot_wifi_reset = false; +uint8_t s_boot_low_streak = 0; +bool s_pending_range_tap = false; +unsigned long s_last_range_tap_ms = 0; + +struct PolledButtonState { + gpio_num_t pin; + bool down; + uint8_t stable_count; + unsigned long down_ms; + bool long_handled; + bool allow_long_reset; + bool allow_range_tap; +}; + +PolledButtonState s_range_btn_1{}; +PolledButtonState s_range_btn_2{}; +bool s_polled_buttons_ready = false; + +bool wifiConfigPortalActive() { return s_config_portal_active; } + +bool userButtonPressed(gpio_num_t pin) { + uint8_t active_votes = 0; + for (uint8_t i = 0; i < config::kButtonReadSamples; ++i) { + const int level = digitalRead(pin); + const bool active = + config::kButtonActiveLow ? (level == LOW) : (level == HIGH); + if (active) { + ++active_votes; + } + } + return active_votes >= config::kButtonReadThreshold; +} + +void initPolledButtonStates() { + if (s_polled_buttons_ready) { + return; + } + s_range_btn_1 = {config::kBootPin, false, 0, 0, false, false, true}; + s_range_btn_2 = {config::kWifiResetPin, false, 0, 0, false, + config::kEnableWifiLongPressReset, false}; + s_polled_buttons_ready = true; +} + +bool pollPolledButton(PolledButtonState& btn) { + if (btn.pin == GPIO_NUM_NC) { + return false; + } + + const bool pressed = userButtonPressed(btn.pin); + const unsigned long now = millis(); + + if (pressed) { + if (btn.stable_count < config::kButtonStableSamplesRequired) { + ++btn.stable_count; + } + } else if (btn.stable_count > 0) { + --btn.stable_count; + } + + const bool stable_pressed = + btn.stable_count >= config::kButtonStableSamplesRequired; + const bool stable_released = btn.stable_count == 0; + + if (stable_pressed) { + if (!btn.down) { + btn.down = true; + btn.down_ms = now; + btn.long_handled = false; + } else if (config::kEnableWifiLongPressReset && btn.allow_long_reset && + s_allow_boot_wifi_reset && + !s_config_portal_active && !btn.long_handled && + now - btn.down_ms >= config::kBootResetHoldMs) { + btn.long_handled = true; + Serial.println("WiFi reset key held — restarting"); + wifiResetCredentialsAndReboot(); + } + return false; + } + + if (!btn.down || !stable_released) { + return false; + } + + btn.down = false; + if (btn.long_handled || !btn.allow_range_tap) { + return false; + } + + const unsigned long held = now - btn.down_ms; + if (held < config::kBootTapMinMs || held >= config::kBootResetHoldMs) { + return false; + } + + if (config::kButtonTapCooldownMs > 0 && + now - s_last_range_tap_ms < config::kButtonTapCooldownMs) { + return false; + } + + s_last_range_tap_ms = now; + return true; +} + +void pollPolledRangeButtons() { + initPolledButtonStates(); + if (pollPolledButton(s_range_btn_1)) { + s_pending_range_tap = true; + } + if (config::kEnableWifiLongPressReset) { + pollPolledButton(s_range_btn_2); + } +} void IRAM_ATTR onBootButtonIsr() { const bool down = digitalRead(config::kBootPin) == LOW; @@ -43,7 +158,16 @@ void IRAM_ATTR onBootButtonIsr() { } void initBootButton() { - pinMode(config::kBootPin, INPUT_PULLUP); + pinMode(config::kBootPin, + config::kBootPinInternalPullup ? INPUT_PULLUP : INPUT); + if (config::kWifiResetPin != GPIO_NUM_NC) { + pinMode(config::kWifiResetPin, + config::kWifiResetPinInternalPullup ? INPUT_PULLUP : INPUT); + } + if (config::kBootUsePolledTap) { + initPolledButtonStates(); + return; + } if (s_boot_interrupt_attached) { return; } @@ -127,6 +251,16 @@ void markForceConfigPortal() { prefs.end(); } +void clearForceConfigPortal() { + s_force_config_portal = false; + Preferences prefs; + if (!prefs.begin(kWifiPrefsNamespace, false)) { + return; + } + prefs.remove(kPrefsForcePortalKey); + prefs.end(); +} + bool consumeForceConfigPortal() { if (s_force_config_portal) { s_force_config_portal = false; @@ -277,14 +411,14 @@ bool waitForLinkWithUi(const char* ssid_for_ui, unsigned long attempt_ms) { if (wifiLinkUp()) { return true; } - bootButtonPollLongPress(); statusScreenConnectingTick(); delay(config::kWifiConnectingFrameMs); } return wifiLinkUp(); } -bool tryConnectWithUi(const String& ssid, const String& pass, bool show_ui) { +bool tryConnectWithUi(const String& ssid, const String& pass, bool show_ui, + uint8_t max_attempts) { if (wifiLinkUp()) { return true; } @@ -294,10 +428,9 @@ bool tryConnectWithUi(const String& ssid, const String& pass, bool show_ui) { statusScreenConnectingBegin(ui_ssid); } - for (uint8_t attempt = 1; attempt <= config::kWifiConnectAttempts; ++attempt) { + for (uint8_t attempt = 1; attempt <= max_attempts; ++attempt) { if (attempt > 1) { - Serial.printf("WiFi connect retry %u/%u\n", attempt, - config::kWifiConnectAttempts); + Serial.printf("WiFi connect retry %u/%u\n", attempt, max_attempts); WiFi.disconnect(true); WiFi.mode(WIFI_OFF); delay(400); @@ -313,7 +446,8 @@ bool tryConnectWithUi(const String& ssid, const String& pass, bool show_ui) { return false; } -bool connectSavedNetwork(bool show_ui) { +bool connectSavedNetwork(bool show_ui, + uint8_t max_attempts = config::kWifiConnectAttempts) { if (!storedWifiCredentials()) { return false; } @@ -324,7 +458,16 @@ bool connectSavedNetwork(bool show_ui) { return false; } const String pass = s_wm.getWiFiPass(); - return tryConnectWithUi(ssid, pass, show_ui); + return tryConnectWithUi(ssid, pass, show_ui, max_attempts); +} + +bool connectConfiguredNetwork(bool show_ui, + uint8_t max_attempts = config::kWifiConnectAttempts) { + if (config::kWifiSkipPortal) { + return tryConnectWithUi(config::kWifiSsid, config::kWifiPass, show_ui, + max_attempts); + } + return connectSavedNetwork(show_ui, max_attempts); } bool openConfigPortal() { @@ -348,6 +491,9 @@ bool openConfigPortal() { } // namespace bool wifiShowsSetupScreenOnBoot() { + if (config::kWifiSkipPortal) { + return false; + } if (s_force_config_portal) { return true; } @@ -364,9 +510,22 @@ bool wifiBootButtonPressed() { return digitalRead(config::kBootPin) == LOW; } +bool wifiResetButtonPressed() { + if (config::kWifiResetPin != GPIO_NUM_NC) { + return digitalRead(config::kWifiResetPin) == LOW; + } + return wifiBootButtonPressed(); +} + void bootButtonInit() { initBootButton(); } bool bootButtonConsumeTap() { + if (config::kBootUsePolledTap) { + const bool tap = s_pending_range_tap; + s_pending_range_tap = false; + return tap; + } + portENTER_CRITICAL(&s_boot_mux); const bool tap = s_boot_tap_pending; if (tap) { @@ -376,8 +535,31 @@ bool bootButtonConsumeTap() { return tap; } +void bootButtonEnableWifiReset() { + if (config::kEnableWifiLongPressReset) { + s_allow_boot_wifi_reset = true; + } +} + void bootButtonPollLongPress() { - if (wifiBootButtonPressed()) { + if (config::kBootUsePolledTap) { + pollPolledRangeButtons(); + return; + } + + if (!config::kEnableWifiLongPressReset || !s_allow_boot_wifi_reset || + s_config_portal_active) { + s_boot_low_streak = 0; + s_long_press_handled = false; + return; + } + + if (wifiResetButtonPressed()) { + if (s_boot_low_streak < config::kBootLowStreakRequired) { + ++s_boot_low_streak; + return; + } + portENTER_CRITICAL(&s_boot_mux); if (!s_boot_is_down) { s_boot_is_down = true; @@ -389,10 +571,11 @@ void bootButtonPollLongPress() { if (!s_long_press_handled && millis() - down_ms >= config::kBootResetHoldMs) { s_long_press_handled = true; - Serial.println("BOOT held — resetting WiFi"); + Serial.println("WiFi reset key held — clearing credentials"); wifiResetCredentialsAndReboot(); } } else { + s_boot_low_streak = 0; portENTER_CRITICAL(&s_boot_mux); s_boot_is_down = false; portEXIT_CRITICAL(&s_boot_mux); @@ -401,7 +584,11 @@ void bootButtonPollLongPress() { } void wifiResetCredentialsAndReboot() { - resetWifiCredentials(); + if (!config::kWifiSkipPortal) { + resetWifiCredentials(); + } else { + Serial.println("Restarting (hardcoded Wi-Fi unchanged)"); + } statusScreenWifiReset(); delay(800); esp_restart(); @@ -410,7 +597,7 @@ void wifiResetCredentialsAndReboot() { bool wifiReconnect() { initBootButton(); Serial.println("WiFi reconnecting..."); - return connectSavedNetwork(true); + return connectConfiguredNetwork(true); } void wifiLoop() { @@ -432,6 +619,31 @@ bool wifiSetupConnect() { initBootButton(); ensureWifiManager(); + WiFi.setAutoReconnect(false); + + if (config::kWifiSkipPortal) { + clearForceConfigPortal(); + Serial.printf("Connecting to %s...\n", config::kWifiSsid); + + if (wifiLinkUp()) { + WiFi.setAutoReconnect(true); + Serial.printf("Connected: %s IP %s\n", WiFi.SSID().c_str(), + WiFi.localIP().toString().c_str()); + return true; + } + + if (connectConfiguredNetwork(true, config::kWifiConnectAttempts)) { + WiFi.setAutoReconnect(true); + Serial.printf("Connected: %s IP %s\n", WiFi.SSID().c_str(), + WiFi.localIP().toString().c_str()); + return true; + } + + Serial.println("WiFi connection failed"); + statusScreenConnectFailed(); + return false; + } + const bool force_portal = consumeForceConfigPortal(); WiFi.setAutoReconnect(false); @@ -445,6 +657,7 @@ bool wifiSetupConnect() { Serial.println("Opening WiFi setup portal (after reset)"); if (openConfigPortal() && wifiLinkUp()) { WiFi.setAutoReconnect(true); + clearForceConfigPortal(); Serial.printf("Connected: %s IP %s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str()); return true; @@ -458,13 +671,16 @@ bool wifiSetupConnect() { if (wifiLinkUp()) { WiFi.setAutoReconnect(true); + clearForceConfigPortal(); Serial.printf("Connected: %s IP %s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str()); return true; } - if (storedWifiCredentials() && connectSavedNetwork(true)) { + if (storedWifiCredentials() && + connectSavedNetwork(true, config::kWifiBootConnectAttempts)) { WiFi.setAutoReconnect(true); + clearForceConfigPortal(); Serial.printf("Connected: %s IP %s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str()); return true; @@ -472,12 +688,14 @@ bool wifiSetupConnect() { if (storedWifiCredentials()) { Serial.println("Saved WiFi could not connect — opening setup portal"); + eraseWifiCredentials(); } else { Serial.println("No saved WiFi — opening setup portal"); } if (openConfigPortal() && wifiLinkUp()) { WiFi.setAutoReconnect(true); + clearForceConfigPortal(); Serial.printf("Connected: %s IP %s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str()); return true; diff --git a/src/ui/radar_color_mode.cpp b/src/ui/radar_color_mode.cpp new file mode 100644 index 000000000..63d9aca05 --- /dev/null +++ b/src/ui/radar_color_mode.cpp @@ -0,0 +1,46 @@ +#include "ui/radar_color_mode.h" + +#include + +namespace ui::radar { + +namespace { + +constexpr char kPrefsNamespace[] = "planeradar"; +constexpr char kPrefsColorModeKey[] = "colorMode"; + +Preferences s_prefs; +ColorMode s_color_mode = kColorModeNight; + +void saveColorMode() { + if (!s_prefs.begin(kPrefsNamespace, false)) { + return; + } + s_prefs.putUChar(kPrefsColorModeKey, + static_cast(s_color_mode)); + s_prefs.end(); +} + +} // namespace + +void colorModeInit() { + if (!s_prefs.begin(kPrefsNamespace, true)) { + return; + } + const uint8_t saved = s_prefs.getUChar(kPrefsColorModeKey, 0); + s_color_mode = + (saved == kColorModeDay) ? kColorModeDay : kColorModeNight; + s_prefs.end(); +} + +ColorMode colorMode() { return s_color_mode; } + +bool isDayMode() { return s_color_mode == kColorModeDay; } + +void colorModeToggle() { + s_color_mode = isDayMode() ? kColorModeNight : kColorModeDay; + saveColorMode(); + Serial.printf("Color mode: %s\n", isDayMode() ? "day" : "night"); +} + +} // namespace ui::radar diff --git a/src/ui/radar_display.cpp b/src/ui/radar_display.cpp index b0d333997..66ee4e063 100644 --- a/src/ui/radar_display.cpp +++ b/src/ui/radar_display.cpp @@ -11,12 +11,13 @@ #include "hardware/display_font.h" #include "services/adsb_client.h" #include "services/radar_location.h" +#include "ui/radar_color_mode.h" +#include "ui/radar_geo.h" #include "ui/radar_range.h" +#include "ui/radar_runways.h" #include "ui/radar_theme.h" #include "ui/runway_overlay.h" -namespace fonts = lgfx::v1::fonts; - namespace ui { namespace radar { @@ -175,39 +176,59 @@ void initTagLabelMetrics() { } void initPalette() { - radar::kColorBackground = tft.color565(radar::kBgR, radar::kBgG, radar::kBgB); - radar::kColorGrid = tft.color565(radar::kGridR, radar::kGridG, radar::kGridB); - radar::kColorLabel = tft.color565(255, 255, 255); - radar::kColorCenter = tft.color565(255, 255, 255); + const bool day = radar::isDayMode(); + const uint8_t bg_r = day ? radar::kDayBgR : radar::kBgR; + const uint8_t bg_g = day ? radar::kDayBgG : radar::kBgG; + const uint8_t bg_b = day ? radar::kDayBgB : radar::kBgB; + const uint8_t grid_r = day ? radar::kDayGridR : radar::kGridR; + const uint8_t grid_g = day ? radar::kDayGridG : radar::kGridG; + const uint8_t grid_b = day ? radar::kDayGridB : radar::kGridB; + const uint8_t label_r = day ? radar::kDayLabelR : radar::kLabelR; + const uint8_t label_g = day ? radar::kDayLabelG : radar::kLabelG; + const uint8_t label_b = day ? radar::kDayLabelB : radar::kLabelB; + const uint8_t ac_r = day ? radar::kDayAircraftR : radar::kAircraftR; + const uint8_t ac_g = day ? radar::kDayAircraftG : radar::kAircraftG; + const uint8_t ac_b = day ? radar::kDayAircraftB : radar::kAircraftB; + const uint8_t tr_r = day ? radar::kDayTrackR : radar::kTrackR; + const uint8_t tr_g = day ? radar::kDayTrackG : radar::kTrackG; + const uint8_t tr_b = day ? radar::kDayTrackB : radar::kTrackB; + const uint8_t tt_r = day ? radar::kDayTagTypeR : radar::kTagTypeR; + const uint8_t tt_g = day ? radar::kDayTagTypeG : radar::kTagTypeG; + const uint8_t tt_b = day ? radar::kDayTagTypeB : radar::kTagTypeB; + const uint8_t ta_r = day ? radar::kDayTagAltR : radar::kTagAltR; + const uint8_t ta_g = day ? radar::kDayTagAltG : radar::kTagAltG; + const uint8_t ta_b = day ? radar::kDayTagAltB : radar::kTagAltB; + const uint8_t rw_r = day ? radar::kDayRunwayR : radar::kRunwayR; + const uint8_t rw_g = day ? radar::kDayRunwayG : radar::kRunwayG; + const uint8_t rw_b = day ? radar::kDayRunwayB : radar::kRunwayB; + const uint8_t rwl_r = day ? radar::kDayRunwayLabelR : radar::kRunwayLabelR; + const uint8_t rwl_g = day ? radar::kDayRunwayLabelG : radar::kRunwayLabelG; + const uint8_t rwl_b = day ? radar::kDayRunwayLabelB : radar::kRunwayLabelB; + + radar::kColorBackground = tft.color565(bg_r, bg_g, bg_b); + radar::kColorGrid = tft.color565(grid_r, grid_g, grid_b); + radar::kColorLabel = tft.color565(label_r, label_g, label_b); + radar::kColorCenter = tft.color565(label_r, label_g, label_b); // GC9A01 BGR panel: swap R/B in color565 so logical red renders red on screen. if (config::kDisplayRgbOrder) { - radar::kColorAircraft = - tft.color565(radar::kAircraftB, radar::kAircraftG, radar::kAircraftR); + radar::kColorAircraft = tft.color565(ac_b, ac_g, ac_r); } else { - radar::kColorAircraft = - tft.color565(radar::kAircraftR, radar::kAircraftG, radar::kAircraftB); + radar::kColorAircraft = tft.color565(ac_r, ac_g, ac_b); } - radar::kColorTrackVector = - tft.color565(radar::kTrackR, radar::kTrackG, radar::kTrackB); - radar::kColorTagType = - tft.color565(radar::kTagTypeR, radar::kTagTypeG, radar::kTagTypeB); - radar::kColorTagAltitude = - tft.color565(radar::kTagAltR, radar::kTagAltG, radar::kTagAltB); - radar::kColorRunway = - tft.color565(radar::kRunwayR, radar::kRunwayG, radar::kRunwayB); - radar::kColorRunwayLabel = tft.color565(radar::kRunwayLabelR, radar::kRunwayLabelG, - radar::kRunwayLabelB); + radar::kColorTrackVector = tft.color565(tr_r, tr_g, tr_b); + radar::kColorTagType = tft.color565(tt_r, tt_g, tt_b); + radar::kColorTagAltitude = tft.color565(ta_r, ta_g, ta_b); + radar::kColorRunway = tft.color565(rw_r, rw_g, rw_b); + radar::kColorRunwayLabel = tft.color565(rwl_r, rwl_g, rwl_b); } -constexpr float kKmPerDeg = 111.0f; - void offsetKmFromCenter(float lat, float lon, float* dx_km, float* dy_km, float* dist_km) { - *dx_km = - static_cast(lon - services::location::lon()) * kKmPerDeg; - *dy_km = - static_cast(lat - services::location::lat()) * kKmPerDeg; - *dist_km = sqrtf((*dx_km) * (*dx_km) + (*dy_km) * (*dy_km)); + radar::geo::offsetKmFromCenter(lat, lon, dx_km, dy_km, dist_km); +} + +void latLonToScreen(float lat, float lon, int* out_x, int* out_y) { + radar::geo::latLonToScreen(lat, lon, out_x, out_y); } float innerRingMaxKm() { @@ -217,20 +238,6 @@ float innerRingMaxKm() { static_cast(radar::kGridOuterRadius)); } -/** Flat lat/lon as x/y: 1° ≈ 111 km, north = screen up. */ -void latLonToScreen(float lat, float lon, int* out_x, int* out_y) { - const float outer_km = radar::rangeCurrent().outer_km; - const float px_per_km = static_cast(radar::kGridOuterRadius) / outer_km; - - float dx_km = 0.0f; - float dy_km = 0.0f; - float dist_km = 0.0f; - offsetKmFromCenter(lat, lon, &dx_km, &dy_km, &dist_km); - - *out_x = radar::kCenterX + static_cast(lroundf(dx_km * px_per_km)); - *out_y = radar::kCenterY - static_cast(lroundf(dy_km * px_per_km)); -} - bool isInsideOuterRingKm(float dist_km) { return dist_km <= innerRingMaxKm(); } int distSqFromCenter(int x, int y) { @@ -648,8 +655,8 @@ void drawStaticGrid(Gfx& gfx) { gfx.fillScreen(radar::kColorBackground); drawRings(cx, cy, grid_r); drawCrosshairs(cx, cy, grid_r, radar::kColorGrid); - initPalette(); runway::drawLargeAirportRunways(gfx); + radar::drawRunways(gfx); drawCenterDot(cx, cy); drawCardinalLabels(); drawScaleLabel(cx, cy, grid_r); diff --git a/src/ui/radar_geo.cpp b/src/ui/radar_geo.cpp new file mode 100644 index 000000000..641792d8f --- /dev/null +++ b/src/ui/radar_geo.cpp @@ -0,0 +1,43 @@ +#include "ui/radar_geo.h" + +#include + +#include "services/radar_location.h" +#include "ui/radar_range.h" +#include "ui/radar_theme.h" + +namespace ui::radar::geo { + +namespace { + +constexpr float kKmPerDeg = 111.0f; + +} // namespace + +void offsetKmFromCenter(float lat, float lon, float* dx_km, float* dy_km, + float* dist_km) { + *dx_km = + static_cast(lon - services::location::lon()) * kKmPerDeg; + *dy_km = + static_cast(lat - services::location::lat()) * kKmPerDeg; + *dist_km = sqrtf((*dx_km) * (*dx_km) + (*dy_km) * (*dy_km)); +} + +float pxPerKm() { + return static_cast(radar::kGridOuterRadius) / + radar::rangeCurrent().outer_km; +} + +void latLonToScreen(float lat, float lon, int* out_x, int* out_y) { + const float px_per_km = pxPerKm(); + + float dx_km = 0.0f; + float dy_km = 0.0f; + float dist_km = 0.0f; + offsetKmFromCenter(lat, lon, &dx_km, &dy_km, &dist_km); + + *out_x = radar::kCenterX + static_cast(lroundf(dx_km * px_per_km)); + *out_y = radar::kCenterY - static_cast(lroundf(dy_km * px_per_km)); +} + +} // namespace ui::radar::geo diff --git a/src/ui/radar_range.cpp b/src/ui/radar_range.cpp index d8bf002e2..67ef814ab 100644 --- a/src/ui/radar_range.cpp +++ b/src/ui/radar_range.cpp @@ -1,5 +1,6 @@ #include "ui/radar_range.h" +#include "ui/radar_color_mode.h" #include "ui/radar_theme.h" #include @@ -71,6 +72,7 @@ void rangeInit() { s_use_miles = s_prefs.getBool(kPrefsMilesKey, false); s_show_runways = s_prefs.getBool(kPrefsRunwaysKey, true); s_prefs.end(); + colorModeInit(); } void rangeNext() { @@ -78,6 +80,12 @@ void rangeNext() { saveRangeIndex(); } +void rangePrev() { + s_range_index = static_cast( + (s_range_index + kRangePresetCount - 1) % kRangePresetCount); + saveRangeIndex(); +} + const RangePreset& rangeCurrent() { return kRangePresets[s_range_index]; } uint8_t rangeIndex() { return s_range_index; } diff --git a/src/ui/radar_runways.cpp b/src/ui/radar_runways.cpp new file mode 100644 index 000000000..f34b4c9ab --- /dev/null +++ b/src/ui/radar_runways.cpp @@ -0,0 +1,58 @@ +#include "ui/radar_runways.h" + +#include "ui/radar_geo.h" +#include "ui/radar_range.h" +#include "ui/radar_theme.h" + +namespace ui::radar { + +namespace { + +/** Runway segment from FAA published threshold coordinates (AirNav). */ +struct RunwaySegment { + double lat1; + double lon1; + double lat2; + double lon2; +}; + +/** KSFF Felts Field, KGEG Spokane Intl, KSKA Fairchild AFB. */ +constexpr RunwaySegment kLocalRunways[] = { + // Felts — 04L/22R + {47.6792003, -117.3313870, 47.6864162, -117.3165778}, + // Felts — 04R/22L + {47.6810402, -117.3241463, 47.6852893, -117.3154213}, + // GEG — 03/21 + {47.6100808, -117.5500799, 47.6312169, -117.5182659}, + // GEG — 08/26 + {47.6169635, -117.5532678, 47.6167678, -117.5200290}, + // Fairchild — 05/23 + {47.6075250, -117.6816833, 47.6225833, -117.6299195}, +}; + +void drawRunwaySegment(lgfx::LGFXBase& gfx, const RunwaySegment& rwy) { + int x1 = 0; + int y1 = 0; + int x2 = 0; + int y2 = 0; + geo::latLonToScreen(static_cast(rwy.lat1), + static_cast(rwy.lon1), &x1, &y1); + geo::latLonToScreen(static_cast(rwy.lat2), + static_cast(rwy.lon2), &x2, &y2); + + gfx.drawWideLine(x1, y1, x2, y2, radar::kRunwayLineHalfWidth, + radar::kColorRunway); +} + +} // namespace + +void drawRunways(lgfx::LGFXBase& gfx) { + if (!radar::showRunways()) { + return; + } + for (const RunwaySegment& rwy : kLocalRunways) { + drawRunwaySegment(gfx, rwy); + } +} + +} // namespace ui::radar diff --git a/src/ui/runway_overlay.cpp b/src/ui/runway_overlay.cpp index 8f4b63127..6fb6a9421 100644 --- a/src/ui/runway_overlay.cpp +++ b/src/ui/runway_overlay.cpp @@ -11,8 +11,6 @@ #include "ui/radar_range.h" #include "ui/radar_theme.h" -namespace fonts = lgfx::v1::fonts; - namespace ui::runway { namespace { @@ -25,7 +23,7 @@ bool s_label_pending[data::large_airports::kAirportCount]; bool s_runway_label_ready = false; bool s_runway_label_use_vlw = false; float s_runway_label_vlw_size = 0.38f; -const lgfx::GFXfont* s_runway_label_gfx = &fonts::FreeSansBold12pt7b; +const lgfx::GFXfont* s_runway_label_gfx = &lgfx::v1::fonts::FreeSansBold12pt7b; int measureVlwHeight(lgfx::LGFXBase& gfx, float size) { gfx.setTextSize(size); @@ -56,7 +54,7 @@ void initRunwayLabelStyle(lgfx::LGFXBase& gfx) { s_runway_label_use_vlw = true; s_runway_label_vlw_size = findVlwSizeForHeight(gfx, target); } else { - s_runway_label_gfx = &fonts::FreeSansBold12pt7b; + s_runway_label_gfx = &lgfx::v1::fonts::FreeSansBold12pt7b; s_runway_label_use_vlw = false; } s_runway_label_ready = true; diff --git a/src/ui/status_screens.cpp b/src/ui/status_screens.cpp index c9be33fe8..21fa27206 100644 --- a/src/ui/status_screens.cpp +++ b/src/ui/status_screens.cpp @@ -11,8 +11,6 @@ #include "hardware/display.h" #include "hardware/display_font.h" -namespace fonts = lgfx::v1::fonts; - namespace { constexpr int kLineGap = 6; @@ -226,8 +224,8 @@ void statusScreenConnectFailed() { {"Could not connect", 1.15f, &kGfxTitle}, {"Check Wi-Fi password", 1.0f, &kGfxBody}, {"and signal strength.", 1.0f, &kGfxBody}, - {"Hold BOOT 3 sec", 1.0f, &kGfxBody}, - {"to reset Wi-Fi", 1.0f, &kGfxBody}, + {"Hold side key 2", 1.0f, &kGfxBody}, + {"5 sec to reset Wi-Fi", 1.0f, &kGfxBody}, }; drawTextBlock(config::kColorYellow, config::kTextOnYellow, lines, sizeof(lines) / sizeof(lines[0]));