diff --git a/README.md b/README.md index 0318b3f4f..c9e09868b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,14 @@ **3D printed case (STL + assembly):** [MakerWorld](https://makerworld.com/en/models/2872376-esp32-plane-radar-live-ads-b-on-a-round-display#profileId-3207083) · **Firmware:** [Releases](https://github.com/MatixYo/ESP32-Plane-Radar/releases) -Firmware for an **ESP32-C3 Super Mini** and a **1.28″ round GC9A01** display (240×240). Shows a circular **ADS-B radar** around your configured location, with **WiFiManager** for first-time setup. +Firmware for a **1.28″ round GC9A01** display (240×240) on the **ESP32-C3**. Shows a circular **ADS-B radar** around your configured location, with **WiFiManager** for first-time setup. + +**Supported boards** (see [Boards](#boards)): + +- **ESP32-C3 Super Mini** + a separately wired GC9A01 panel (default) +- **Sunton/JCZN ESP32-2424S012** — integrated 1.28″ round GC9A01 + CST816 touch board + +Pick the board at build time (PlatformIO env) and/or switch it at runtime from the Wi‑Fi setup portal. ## What it does @@ -44,6 +51,7 @@ The same portal runs on the setup AP and on the device’s LAN IP while connecte | **Latitude / Longitude** | Radar center and ADS-B query position (defaults in `config.h` until set) | | **Display distances in miles** | Ring scale label in **mi** instead of **km** (e.g. `6mi` vs `10km`) | | **Show airport runways** | Major-airport runway overlay on the radar (off to hide) | +| **Board** | Display board in use (Super Mini / ESP32-2424S012); reboots to apply | After a reset, the device reboots and shows the setup screen immediately (no “Connecting” loop on stale credentials). @@ -98,7 +106,8 @@ Edit **`include/config.h`** for hardware and behavior: | Portal | `kPortalApName`, `kPortalIp`, `kPortalHostname` / `kPortalHostUrl` (mDNS; needs `-DWM_MDNS` in `platformio.ini`) | | Wi‑Fi timing | connect attempts, reconnect grace, portal timeout (`0` = no timeout) | | BOOT | `kBootPin`, `kBootResetHoldMs`, `kBootTapMinMs` | -| Display SPI | pins, `kDisplayInvert`, `kDisplayRgbOrder`, `kDisplaySpiWriteHz` | +| Display | `kDisplayInvert`, `kDisplaySpiWriteHz` in `config.h`; per-board pins and color order in `include/hardware/board.h` | +| Boards | `include/hardware/board.h` (`kBoards` table, `Board` enum); add a board by extending both | | Default location | `kDefaultRadarLat`, `kDefaultRadarLon` (until portal overrides) | | ADS-B | `kAdsbFetchIntervalMs`, `kAdsbShowGroundAircraft` | @@ -110,6 +119,7 @@ Range presets: `include/ui/radar_range.h` (`kRangePresets`). include/ config.h hardware/ + board.h — board registry (pins per board, runtime select) lgfx_config.hpp display.h display_font.h @@ -138,7 +148,16 @@ src/ services/ ``` -## Wiring (GC9A01 ↔ ESP32-C3 Super Mini) +## Boards + +The display wiring per board lives in [`include/hardware/board.h`](include/hardware/board.h). Select a board two ways: + +- **Compile time** — pick the PlatformIO env (sets the default): `supermini` or `esp32-2424s012` (see [Build](#build)). +- **Runtime** — choose the board in the Wi‑Fi setup portal (**Board** dropdown). The choice is saved to NVS and applied on the next boot, so one firmware image can run on either board. + +> On first boot the display uses the build-time default. If you flash the “wrong” env, the screen may stay blank until you pick the correct board in the portal (connect to the `PlaneRadar-Setup` AP and open the page), then it reboots. + +### ESP32-C3 Super Mini (separately wired GC9A01) | Display | ESP32-C3 | |---------|----------| @@ -151,16 +170,26 @@ src/ | SCL (SCLK) | GPIO **4** | | BOOT (user) | GPIO **9** | +### Sunton/JCZN ESP32-2424S012 (integrated) + +Integrated board — no wiring needed. Display is fixed to: SCLK **6**, MOSI **7**, DC **2**, CS **10**, RST **not wired**, backlight **GPIO 3** (active high), panel color order **BGR**. (Touch/IMU are not used by this firmware.) + ## Build ```bash -pio run -t upload +# ESP32-C3 Super Mini (default) +pio run -e supermini -t upload + +# Sunton/JCZN ESP32-2424S012 +pio run -e esp32-2424s012 -t upload + pio device monitor ``` -- PlatformIO env: **`supermini`** +- PlatformIO envs: **`supermini`** (default) and **`esp32-2424s012`** - Serial: **115200** baud -- USB CDC on boot enabled in `platformio.ini` for the Super Mini +- USB CDC on boot enabled in `platformio.ini` (native USB on both boards) +- To flash a board that is boot-looping or busy, enter download mode first (hold **BOOT**, tap **RESET** / replug) ### Web-flashable release image diff --git a/include/config.h b/include/config.h index 6555c0342..2f24fa921 100644 --- a/include/config.h +++ b/include/config.h @@ -30,19 +30,14 @@ constexpr unsigned long kBootResetHoldMs = 3000UL; constexpr unsigned long kBootTapMinMs = 40UL; // --- 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 - +// Per-board pin wiring and color order live in hardware/board.h (selectable at +// compile time via -DBOARD_ESP32_2424S012 and at runtime via the setup portal). constexpr int kDisplayWidth = 240; constexpr int kDisplayHeight = 240; constexpr uint32_t kDisplaySpiWriteHz = 40000000; -// GC9A01 modules often need invert + BGR for correct black/green output +// GC9A01 modules need color inversion on for correct output. constexpr bool kDisplayInvert = true; -constexpr bool kDisplayRgbOrder = true; // --- Radar center defaults (overridden via WiFi setup portal) --- constexpr double kDefaultRadarLat = 52.3676; diff --git a/include/hardware/board.h b/include/hardware/board.h new file mode 100644 index 000000000..605b4958b --- /dev/null +++ b/include/hardware/board.h @@ -0,0 +1,58 @@ +#pragma once + +#include + +// Board support: maps a target board to its GC9A01 display wiring. The board +// can be chosen at compile time (-DBOARD_ESP32_2424S012, see platformio.ini) +// and overridden at runtime from the Wi-Fi setup portal (persisted in NVS, +// applied on the next boot). +namespace hardware::board { + +enum class Board : uint8_t { + kSuperMini = 0, // ESP32-C3 Super Mini + separately wired GC9A01 + kEsp32_2424S012 = 1, // Sunton/JCZN integrated 1.28" round board +}; +constexpr uint8_t kBoardCount = 2; + +/** Board-dependent display wiring. Pins are GPIO numbers; -1 = not wired. */ +struct DisplayPins { + int pin_sclk; + int pin_mosi; + int pin_dc; + int pin_cs; + int pin_rst; // -1 = tied to chip reset (not wired) + int pin_backlight; // -1 = hard-wired / not GPIO-controlled + bool rgb_order; // true = RGB, false = BGR +}; + +inline constexpr DisplayPins kBoards[kBoardCount] = { + // kSuperMini: ESP32-C3 Super Mini with a separately wired GC9A01. + {/*sclk*/ 4, /*mosi*/ 3, /*dc*/ 10, /*cs*/ 1, /*rst*/ 0, /*bl*/ -1, + /*rgb_order*/ true}, + // kEsp32_2424S012: Sunton/JCZN integrated round board (BL on GPIO3, BGR). + {/*sclk*/ 6, /*mosi*/ 7, /*dc*/ 2, /*cs*/ 10, /*rst*/ -1, /*bl*/ 3, + /*rgb_order*/ false}, +}; + +constexpr Board compileDefault() { +#if defined(BOARD_ESP32_2424S012) + return Board::kEsp32_2424S012; +#else + return Board::kSuperMini; +#endif +} + +constexpr const DisplayPins& pins(Board b) { + return kBoards[static_cast(b)]; +} + +constexpr bool isValidIndex(long v) { return v >= 0 && v < kBoardCount; } + +const char* name(Board b); + +// Runtime-selected board: NVS override if set and valid, else compileDefault(). +Board active(); +const DisplayPins& activePins(); +void setActive(Board b); // persists to NVS; takes effect on next boot + +} // namespace hardware::board diff --git a/include/hardware/lgfx_config.hpp b/include/hardware/lgfx_config.hpp index 4f1484b6a..47e5d8bc5 100644 --- a/include/hardware/lgfx_config.hpp +++ b/include/hardware/lgfx_config.hpp @@ -4,33 +4,39 @@ #include #include "config.h" +#include "hardware/board.h" -/** LovyanGFX device: GC9A01 on SPI. Pin values come from config.h. */ +/** LovyanGFX device: GC9A01 on SPI. Pin values come from hardware/board.h. */ class LGFX : public lgfx::LGFX_Device { lgfx::Bus_SPI _bus; lgfx::Panel_GC9A01 _panel; public: LGFX() { + applyBoard(hardware::board::pins(hardware::board::compileDefault())); + setPanel(&_panel); + } + + /** (Re)configure SPI bus and panel for a board. Call before init(). */ + void applyBoard(const hardware::board::DisplayPins& p) { { 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_sclk = p.pin_sclk; + cfg.pin_mosi = p.pin_mosi; cfg.pin_miso = -1; - cfg.pin_dc = static_cast(config::kDisplayPinDc); + cfg.pin_dc = p.pin_dc; _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.pin_cs = p.pin_cs; + cfg.pin_rst = p.pin_rst; cfg.invert = config::kDisplayInvert; - cfg.rgb_order = config::kDisplayRgbOrder; + cfg.rgb_order = p.rgb_order; _panel.config(cfg); } - setPanel(&_panel); } }; diff --git a/platformio.ini b/platformio.ini index c81ce535f..602f019fe 100644 --- a/platformio.ini +++ b/platformio.ini @@ -24,3 +24,12 @@ lib_deps = lovyan03/LovyanGFX@^1.2.7 tzapu/WiFiManager@^2.0.17 bblanchon/ArduinoJson@^7.4.2 + +; Sunton/JCZN ESP32-2424S012 — integrated 1.28" round GC9A01 + CST816 touch. +; Same ESP32-C3 target as supermini; only the default display pinout differs. +; The board can also be switched at runtime from the Wi-Fi setup portal. +[env:esp32-2424s012] +extends = env:supermini +build_flags = + ${env:supermini.build_flags} + -DBOARD_ESP32_2424S012 diff --git a/src/hardware/board.cpp b/src/hardware/board.cpp new file mode 100644 index 000000000..ea027fef6 --- /dev/null +++ b/src/hardware/board.cpp @@ -0,0 +1,57 @@ +#include "hardware/board.h" + +#include +#include + +namespace hardware::board { + +namespace { + +constexpr char kPrefsNamespace[] = "board"; +constexpr char kKeySel[] = "sel"; + +const char* const kNames[kBoardCount] = { + "ESP32-C3 Super Mini", + "ESP32-2424S012", +}; + +bool s_loaded = false; +Board s_active = compileDefault(); + +} // namespace + +const char* name(Board b) { return kNames[static_cast(b)]; } + +Board active() { + if (s_loaded) { + return s_active; + } + Preferences prefs; + if (prefs.begin(kPrefsNamespace, true)) { + if (prefs.isKey(kKeySel)) { + const long v = prefs.getUChar(kKeySel, static_cast(compileDefault())); + if (isValidIndex(v)) { + s_active = static_cast(v); + } + } + prefs.end(); + } + s_loaded = true; + Serial.printf("[board] active: %s (%s)\n", name(s_active), + s_active == compileDefault() ? "default" : "override"); + return s_active; +} + +const DisplayPins& activePins() { return pins(active()); } + +void setActive(Board b) { + Preferences prefs; + if (prefs.begin(kPrefsNamespace, false)) { + prefs.putUChar(kKeySel, static_cast(b)); + prefs.end(); + } + s_active = b; + s_loaded = true; +} + +} // namespace hardware::board diff --git a/src/hardware/display.cpp b/src/hardware/display.cpp index 42f82524d..d14bcee2f 100644 --- a/src/hardware/display.cpp +++ b/src/hardware/display.cpp @@ -1,10 +1,27 @@ #include "hardware/display.h" +#include + +#include "hardware/board.h" #include "hardware/display_font.h" LGFX tft; void displayInit() { + // Apply the runtime-selected board (NVS override, else compile default) before + // init so a portal board change takes effect after reboot. + const hardware::board::DisplayPins& p = hardware::board::activePins(); + tft.applyBoard(p); + + // Boards that gate the backlight on a GPIO (e.g. ESP32-2424S012) need it + // driven high (active high). Driven directly rather than via LEDC so it does + // not depend on PWM channel availability. Boards with a hard-wired backlight + // use pin -1 and skip this. + if (p.pin_backlight >= 0) { + pinMode(p.pin_backlight, OUTPUT); + digitalWrite(p.pin_backlight, HIGH); + } + tft.init(); tft.setRotation(0); tft.setBrightness(255); diff --git a/src/services/adsb_client.cpp b/src/services/adsb_client.cpp index 84be21bb7..139c22fc0 100644 --- a/src/services/adsb_client.cpp +++ b/src/services/adsb_client.cpp @@ -276,6 +276,14 @@ bool fetchUpdate(double center_lat, double center_lon, float fetch_radius_km) { s_aircraft_count = n; Serial.printf("adsb: %u aircraft\n", static_cast(n)); + for (size_t i = 0; i < n; ++i) { + Serial.printf(" [%u] %s %.4f,%.4f %s trk=%.0f gs=%.0f\n", + static_cast(i), + s_aircraft[i].callsign[0] != '\0' ? s_aircraft[i].callsign + : "(no id)", + s_aircraft[i].lat, s_aircraft[i].lon, s_aircraft[i].alt, + s_aircraft[i].track_deg, s_aircraft[i].gs_knots); + } return true; } diff --git a/src/services/radar_location.cpp b/src/services/radar_location.cpp index a8c9c273b..e43930414 100644 --- a/src/services/radar_location.cpp +++ b/src/services/radar_location.cpp @@ -47,6 +47,7 @@ void persist(double lat, double lon) { } // namespace void init() { + bool loaded_from_nvs = false; Preferences prefs; prefs.begin(kPrefsNamespace, true); if (prefs.isKey(kKeyLat) && prefs.isKey(kKeyLon)) { @@ -55,9 +56,12 @@ void init() { if (validLatLon(lat, lon)) { s_lat = lat; s_lon = lon; + loaded_from_nvs = true; } } prefs.end(); + Serial.printf("[location] init: lat=%.6f lon=%.6f (%s)\n", s_lat, s_lon, + loaded_from_nvs ? "saved" : "default"); } double lat() { return s_lat; } diff --git a/src/services/wifi_setup.cpp b/src/services/wifi_setup.cpp index 2fa8b7233..427e1d469 100644 --- a/src/services/wifi_setup.cpp +++ b/src/services/wifi_setup.cpp @@ -14,6 +14,7 @@ #endif #include "config.h" +#include "hardware/board.h" #include "services/radar_location.h" #include "ui/radar_range.h" #include "ui/status_screens.h" @@ -84,6 +85,33 @@ char s_runways_checkbox_attrs[32] = "type=\"checkbox\""; WiFiManagerParameter s_param_runways("show_runways", "Show airport runways", "T", 2, s_runways_checkbox_attrs, WFM_LABEL_AFTER); +// Board selector. Custom-HTML-only param renders a "); + for (uint8_t i = 0; i < hardware::board::kBoardCount && n > 0 && + n < static_cast(sizeof(s_board_select_html)); + ++i) { + n += snprintf(s_board_select_html + n, sizeof(s_board_select_html) - n, + "", i, + i == cur ? " selected" : "", + hardware::board::name(static_cast(i))); + } + if (n > 0 && n < static_cast(sizeof(s_board_select_html))) { + snprintf(s_board_select_html + n, sizeof(s_board_select_html) - n, ""); + } +} + void refreshPortalParamDefaults() { char lat_buf[kCoordParamLen + 1]; char lon_buf[kCoordParamLen + 1]; @@ -97,6 +125,29 @@ void refreshPortalParamDefaults() { snprintf(s_runways_checkbox_attrs, sizeof(s_runways_checkbox_attrs), "type=\"checkbox\"%s", ui::radar::showRunways() ? " checked" : ""); s_param_runways.setValue("T", 2); + buildBoardSelectHtml(); +} + +void saveBoardFromPortal() { + if (!s_wm.server) { + return; + } + const String value = s_wm.server->arg("board"); + if (value.length() == 0) { + return; + } + const long sel = value.toInt(); + if (!hardware::board::isValidIndex(sel)) { + return; + } + const auto chosen = static_cast(sel); + if (chosen == hardware::board::active()) { + return; + } + hardware::board::setActive(chosen); + Serial.printf("Board set to %s — rebooting to apply\n", + hardware::board::name(chosen)); + s_reboot_at_ms = millis() + 1200; // let the HTTP response flush first } void onPortalParamsSaved() { @@ -106,6 +157,7 @@ void onPortalParamsSaved() { } ui::radar::saveMilesFromPortal(s_param_miles.getValue()); ui::radar::saveRunwaysFromPortal(s_param_runways.getValue()); + saveBoardFromPortal(); } void attachPortalParams(WiFiManager& wm) { @@ -114,6 +166,7 @@ void attachPortalParams(WiFiManager& wm) { wm.addParameter(&s_param_lon); wm.addParameter(&s_param_miles); wm.addParameter(&s_param_runways); + wm.addParameter(&s_param_board); wm.setSaveParamsCallback(onPortalParamsSaved); } @@ -414,6 +467,10 @@ bool wifiReconnect() { } void wifiLoop() { + if (s_reboot_at_ms != 0 && millis() >= s_reboot_at_ms) { + Serial.println("Rebooting to apply board change..."); + esp_restart(); + } ensureWifiManager(); if (wifiLinkUp()) { if (!s_wm.getWebPortalActive() && !s_wm.getConfigPortalActive()) { diff --git a/src/ui/radar_display.cpp b/src/ui/radar_display.cpp index b0d333997..4f1782254 100644 --- a/src/ui/radar_display.cpp +++ b/src/ui/radar_display.cpp @@ -7,6 +7,7 @@ #include #include "config.h" +#include "hardware/board.h" #include "hardware/display.h" #include "hardware/display_font.h" #include "services/adsb_client.h" @@ -180,7 +181,7 @@ void initPalette() { radar::kColorLabel = tft.color565(255, 255, 255); radar::kColorCenter = tft.color565(255, 255, 255); // GC9A01 BGR panel: swap R/B in color565 so logical red renders red on screen. - if (config::kDisplayRgbOrder) { + if (hardware::board::activePins().rgb_order) { radar::kColorAircraft = tft.color565(radar::kAircraftB, radar::kAircraftG, radar::kAircraftR); } else {