Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 35 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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).

Expand Down Expand Up @@ -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` |

Expand All @@ -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
Expand Down Expand Up @@ -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 |
|---------|----------|
Expand All @@ -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

Expand Down
18 changes: 8 additions & 10 deletions include/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,26 +30,24 @@ 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;
constexpr double kDefaultRadarLon = 4.9041;

/** Poll adsb.fi (API public limit: 1 req/s). */
constexpr unsigned long kAdsbFetchIntervalMs = 3000;
/** Poll interval for adsb.fi (seconds). Configurable in the portal; the API
* public limit is 1 req/s, so the minimum is 3 s. */
constexpr uint8_t kAdsbFetchIntervalDefaultSec = 3;
constexpr uint8_t kAdsbFetchIntervalMinSec = 3;
constexpr uint8_t kAdsbFetchIntervalMaxSec = 30;
/** Legacy scale unused — fetch uses radar::fetchRadiusKm() to screen edge. */
constexpr float kAdsbFetchRadiusScale = 1.0f;
/** false = hide aircraft with alt_baro "ground"; true = show them too. */
Expand Down
23 changes: 23 additions & 0 deletions include/data/airlines.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#pragma once

// Airline lookup: maps a flight callsign's 3-letter ICAO airline designator
// (e.g. "BAW" in "BAW123") to the airline's IATA acronym and full name.
// Curated list of common/major airlines; unknown or non-airline (GA, military,
// registration) callsigns resolve to nullptr.
namespace data::airlines {

struct Airline {
const char* icao; // 3-letter ICAO designator (callsign prefix)
const char* iata; // 2-letter IATA code / acronym (may be "")
const char* name; // full airline name, e.g. "British Airways"
const char* short_name; // friendly short name, e.g. "Virgin", "American"
};

/**
* Resolve a flight callsign (e.g. "BAW123") to an airline, or nullptr if the
* callsign does not look like an airline flight (needs 3 letters + a digit) or
* the ICAO code is not in the table.
*/
const Airline* forCallsign(const char* callsign);

} // namespace data::airlines
66 changes: 66 additions & 0 deletions include/hardware/board.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#pragma once

#include <cstdint>

// 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
// Capacitive touch (CST816, I2C). touch_sda < 0 means no touch on this board.
int touch_sda;
int touch_scl;
int touch_int; // -1 if not wired
int touch_rst; // -1 if not wired
};

inline constexpr DisplayPins kBoards[kBoardCount] = {
// kSuperMini: ESP32-C3 Super Mini with a separately wired GC9A01 (no touch).
{/*sclk*/ 4, /*mosi*/ 3, /*dc*/ 10, /*cs*/ 1, /*rst*/ 0, /*bl*/ -1,
/*rgb_order*/ true,
/*touch sda*/ -1, /*scl*/ -1, /*int*/ -1, /*rst*/ -1},
// kEsp32_2424S012: Sunton/JCZN integrated round board (BL on GPIO3, BGR,
// CST816 touch on I2C).
{/*sclk*/ 6, /*mosi*/ 7, /*dc*/ 2, /*cs*/ 10, /*rst*/ -1, /*bl*/ 3,
/*rgb_order*/ false,
/*touch sda*/ 4, /*scl*/ 5, /*int*/ 0, /*rst*/ 1},
};

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<uint8_t>(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
41 changes: 33 additions & 8 deletions include/hardware/lgfx_config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,58 @@
#include <LovyanGFX.hpp>

#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;
lgfx::Touch_CST816S _touch;

public:
LGFX() {
applyBoard(hardware::board::pins(hardware::board::compileDefault()));
setPanel(&_panel);
}

/** (Re)configure SPI bus, panel and touch 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<int>(config::kDisplayPinSclk);
cfg.pin_mosi = static_cast<int>(config::kDisplayPinMosi);
cfg.pin_sclk = p.pin_sclk;
cfg.pin_mosi = p.pin_mosi;
cfg.pin_miso = -1;
cfg.pin_dc = static_cast<int>(config::kDisplayPinDc);
cfg.pin_dc = p.pin_dc;
cfg.dma_channel = SPI_DMA_CH_AUTO; // non-blocking frame pushes
_bus.config(cfg);
_panel.setBus(&_bus);
}
{
auto cfg = _panel.config();
cfg.pin_cs = static_cast<int>(config::kDisplayPinCs);
cfg.pin_rst = static_cast<int>(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);
if (p.touch_sda >= 0) {
auto cfg = _touch.config();
cfg.i2c_port = 0;
cfg.i2c_addr = 0x15; // CST816 default address
cfg.pin_sda = p.touch_sda;
cfg.pin_scl = p.touch_scl;
cfg.pin_int = p.touch_int;
cfg.pin_rst = p.touch_rst;
cfg.freq = 400000;
cfg.x_min = 0;
cfg.x_max = config::kDisplayWidth - 1;
cfg.y_min = 0;
cfg.y_max = config::kDisplayHeight - 1;
cfg.offset_rotation = 0;
_touch.config(cfg);
_panel.setTouch(&_touch);
}
}
};
9 changes: 7 additions & 2 deletions include/services/adsb_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

#include <cstddef>

#include "data/airlines.h"

namespace services::adsb {

struct Aircraft {
Expand All @@ -10,9 +12,12 @@ struct Aircraft {
float nose_deg;
float track_deg;
float gs_knots;
char callsign[9];
char type[5];
char callsign[9]; // flight/callsign, e.g. "BAW123"
char type[5]; // aircraft type code, e.g. "A320"
char alt[12];
// Resolved airline (nullptr if the callsign is not a known airline flight).
// Provides the IATA acronym, full name, and friendly short name.
const data::airlines::Airline* airline;
};

constexpr size_t kMaxAircraft = 64;
Expand Down
19 changes: 19 additions & 0 deletions include/services/route.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#pragma once

// Flight route lookup: maps a callsign to its departure/arrival airports via the
// adsbdb.com public API. Looked up on demand (when a flight dialog opens), not
// per aircraft. Routes exist only for scheduled flights.
namespace services::route {

struct RouteInfo {
bool valid;
char origin_code[5]; // IATA (falls back to ICAO)
char origin_city[24]; // municipality, may be empty
char dest_code[5];
char dest_city[24];
};

/** Look up the route for a callsign. Returns true and fills out on success. */
bool lookup(const char* callsign, RouteInfo* out);

} // namespace services::route
19 changes: 19 additions & 0 deletions include/ui/radar_display.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#pragma once

#include "services/route.h"

namespace ui {

/** Draw the static sonar/radar grid (black disc, green overlay, labels). */
Expand All @@ -8,4 +10,21 @@ void radarDisplayDraw();
/** Redraw aircraft only (blits cached grid; no full-screen clear). */
void radarDisplayRefreshAircraft();

/** Advance and draw the rotating radar sweep. Call frequently from loop(). */
void radarDisplayAnimate();

/**
* Find the aircraft nearest to a screen point (from the last radar draw).
* Returns its index into services::adsb::aircraftList(), or -1 if none is
* within tap range.
*/
int radarDisplayHitTest(int x, int y);

/**
* Blank the screen and draw the flight details dialog for an aircraft.
* route may be nullptr (or invalid) if no departure/arrival info is available.
*/
void radarDisplayDrawDialog(int aircraft_index,
const services::route::RouteInfo* route);

} // namespace ui
Loading