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
77 changes: 73 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

**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 an **ESP32-C3 Super Mini** and a round SPI display — **1.28″ GC9A01** (240×240) or **2.1″ GC9B72** (360×360). Shows a circular **ADS-B radar** around your configured location, with **WiFiManager** for first-time setup.

The radar layout is derived from the panel size at runtime, so both displays get the same design scaled to fit rather than a second set of hand-tuned constants.

## What it does

Expand Down Expand Up @@ -138,7 +140,13 @@ src/
services/
```

## Wiring (GC9A01 ↔ ESP32-C3 Super Mini)
## Wiring

`BOOT (user)` is the on-board button on **GPIO 9** in every combination below — no wire.

### GC9A01 (1.28″ round, 240×240) ↔ ESP32-C3 Super Mini

The shipping build.

| Display | ESP32-C3 |
|---------|----------|
Expand All @@ -151,16 +159,77 @@ src/
| SCL (SCLK) | GPIO **4** |
| BOOT (user) | GPIO **9** |

### GC9B72 (2.1″ round, 360×360) ↔ ESP32-C3 Super Mini

| Display | ESP32-C3 | |
|---------|----------|---|
| VCC | 3V3 | **3.3 V only** — the module has no regulator |
| GND | GND | |
| RES | GPIO **0** | |
| CS | GPIO **1** | |
| DC | GPIO **10** | |
| SDA (MOSI) | GPIO **3** | |
| SCL (SCLK) | GPIO **4** | |
| BL | GPIO **5** | backlight, on a GPIO so it can be dimmed |
| SDO (MISO) | GPIO **6** | optional — see below |
| TE | GPIO **7** | optional — see below |
| BOOT (user) | GPIO **9** | on-board button |

Only `RES`, `CS`, `DC`, `SDA` and `SCL` are required; the rest are set to `-1` in
`include/config.h` when not wired, and the firmware adapts.

**SDO (MISO).** The radar composes each frame in an off-screen sprite and pushes
it in one pass, so nothing normally reads back from the panel. It matters on the
fallback path taken when no frame sprite can be allocated, where LovyanGFX's
antialiased primitives read the panel to blend against it — with SDO unwired
those blends have nothing to read.

**TE (tearing effect).** The GC9B72 init sequence enables this output, but
LovyanGFX does not consume it — `getScanLine()` returns `-1` for every SPI panel
— so the firmware polls it directly in `displayWaitForFrameStart()` and starts
the push on the edge. A full 360×360 frame is 259,200 bytes on the wire, about
104 ms at 20 MHz, which is long enough for a seam to show without it. The wait
times out after 25 ms so a miswired line cannot stall the main loop.

**Backlight current.** The 2.1″ backlight draws several times what the 1.28″ one
does, and rides the Super Mini's 3V3 LDO alongside WiFi transmit bursts. Measure
3V3 under load before trusting it; TX power is already capped to 8.5 dBm for
related reasons.

**Board orientation.** Holding the Super Mini with USB-C up, `GPIO5` and `5V` are
the pads nearest the connector and `GPIO20`/`GPIO21` the two furthest. Published
pinouts appear in both orientations — check the silkscreen, not a diagram.

## Build

Pick the environment that matches the display fitted:

| Display | Environment | Resolution |
|---------|-------------|------------|
| 1.28″ GC9A01 | `supermini` | 240×240 |
| 2.1″ GC9B72 | `supermini_gc9b72` | 360×360 |

```bash
pio run -t upload
# 1.28" GC9A01 (default)
pio run -e supermini -t upload

# 2.1" GC9B72
pio run -e supermini_gc9b72 -t upload

pio device monitor
```

- PlatformIO env: **`supermini`**
- Serial: **115200** baud
- USB CDC on boot enabled in `platformio.ini` for the Super Mini
- The two environments differ only by `-DPLANE_RADAR_PANEL_GC9B72`, which selects
the panel driver, pins, SPI clock and colour settings in `include/config.h`
- The 360×360 frame buffer does not fit at 16 bits on an ESP32-C3, so the radar
falls back to 8-bit RGB332 (129,600 B instead of 259,200 B). The chosen depth is
printed at boot:

```
radar: frame sprite 360x360 @8bpp
```

### Web-flashable release image

Expand Down
56 changes: 55 additions & 1 deletion include/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,64 @@ constexpr unsigned long kBootResetHoldMs = 3000UL;
/** Ignore BOOT taps shorter than this (debounce). */
constexpr unsigned long kBootTapMinMs = 40UL;

// --- Display: GC9A01 1.28" round 240×240 (SPI) ---
// --- Display ---
// Two panels are supported. Define PLANE_RADAR_PANEL_GC9B72 (PlatformIO env
// `supermini_gc9b72`) for the 2.1" 360x360 panel; the default is the 1.28"
// GC9A01 the project shipped with. Wiring for both is in the README.
//
// kDisplayWidth/kDisplayHeight configure the panel driver. The UI does NOT read
// them — it asks the driver at runtime via ui::radar::initMetrics(), so a panel
// that reports a size of its own still lays out correctly.
#if defined(PLANE_RADAR_PANEL_GC9B72)

// GC9B72 2.1" round 360x360 (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
/**
* Backlight. The 2.1" backlight draws several times what the 1.28" one does, so
* it sits on a GPIO rather than being strapped to 3V3 — that way it can be
* dimmed if the 3V3 rail sags under WiFi transmit. -1 = not wired.
*/
constexpr int kDisplayPinBacklight = 5;
/**
* SDO/MISO. Wire it and the panel becomes readable, which the antialiased
* primitives need on the direct-to-panel path taken when no frame sprite fits.
* -1 = not wired.
*/
constexpr int kDisplayPinMiso = 6;
/**
* Tearing effect. The controller pulses this once per frame; waiting for it
* before pushing keeps a seam out of the picture. A full 360x360 frame is
* 259,200 B on the wire, long enough to tear without it. -1 = not wired.
*/
constexpr int kDisplayPinTe = 7;

constexpr int kDisplayWidth = 360;
constexpr int kDisplayHeight = 360;

/** The only published GC9B72 driver reports ~20 MHz tested on short leads. */
constexpr uint32_t kDisplaySpiWriteHz = 20000000;
// The GC9B72 init sequence does not set inversion itself, and this panel is
// normally black — so, unlike the GC9A01 modules, it must not be inverted.
constexpr bool kDisplayInvert = false;
constexpr bool kDisplayRgbOrder = false;

#else

// GC9A01 1.28" round 240x240 (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
/** Not wired on the GC9A01 modules — backlight is tied on at the module. */
constexpr int kDisplayPinBacklight = -1;
/** Neither is broken out on the GC9A01 modules. */
constexpr int kDisplayPinMiso = -1;
constexpr int kDisplayPinTe = -1;

constexpr int kDisplayWidth = 240;
constexpr int kDisplayHeight = 240;
Expand All @@ -44,6 +96,8 @@ constexpr uint32_t kDisplaySpiWriteHz = 40000000;
constexpr bool kDisplayInvert = true;
constexpr bool kDisplayRgbOrder = true;

#endif

// --- Radar center defaults (overridden via WiFi setup portal) ---
constexpr double kDefaultRadarLat = 52.3676;
constexpr double kDefaultRadarLon = 4.9041;
Expand Down
11 changes: 11 additions & 0 deletions include/hardware/display.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,14 @@
extern LGFX tft;

void displayInit();

/**
* Block until the panel's tearing-effect line says a new frame is starting, so
* a full-screen push lands in the vertical blanking interval instead of partway
* down a visible frame. Returns immediately when no TE pin is wired, and gives
* up after a short timeout so a miswired line cannot stall the main loop.
*
* LovyanGFX has no TE support for SPI panels -- getScanLine() returns -1 for
* all of them -- so this is polled here rather than by the driver.
*/
void displayWaitForFrameStart();
29 changes: 27 additions & 2 deletions include/hardware/lgfx_config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,21 @@

#include "config.h"

/** LovyanGFX device: GC9A01 on SPI. Pin values come from config.h. */
/**
* LovyanGFX device. Pin values and panel geometry come from config.h.
*
* Both panels derive from LovyanGFX's shared Panel_GC9xxx base, so only the
* type differs — the bus, pins and backlight setup below are identical.
* Panel_GC9B72 needs LovyanGFX >= 1.2.28.
*/
class LGFX : public lgfx::LGFX_Device {
lgfx::Bus_SPI _bus;
#if defined(PLANE_RADAR_PANEL_GC9B72)
lgfx::Panel_GC9B72 _panel;
#else
lgfx::Panel_GC9A01 _panel;
#endif
lgfx::Light_PWM _light;

public:
LGFX() {
Expand All @@ -18,7 +29,7 @@ class LGFX : public lgfx::LGFX_Device {
cfg.freq_write = config::kDisplaySpiWriteHz;
cfg.pin_sclk = static_cast<int>(config::kDisplayPinSclk);
cfg.pin_mosi = static_cast<int>(config::kDisplayPinMosi);
cfg.pin_miso = -1;
cfg.pin_miso = config::kDisplayPinMiso;
cfg.pin_dc = static_cast<int>(config::kDisplayPinDc);
_bus.config(cfg);
_panel.setBus(&_bus);
Expand All @@ -31,6 +42,20 @@ class LGFX : public lgfx::LGFX_Device {
cfg.rgb_order = config::kDisplayRgbOrder;
_panel.config(cfg);
}
// Only attach a light when a backlight pin is actually wired; on the GC9A01
// modules the backlight is tied on at the module and setBrightness is a
// no-op, which is the behaviour this project has always had.
if (config::kDisplayPinBacklight >= 0) {
auto cfg = _light.config();
cfg.pin_bl = config::kDisplayPinBacklight;
cfg.invert = false;
cfg.freq = 44100;
// The ESP32-C3 has only 6 LEDC channels (0-5), so the 7 used in most
// LovyanGFX examples (written for the 16-channel ESP32) is out of range.
cfg.pwm_channel = 0;
_light.config(cfg);
_panel.setLight(&_light);
}
setPanel(&_panel);
}
};
87 changes: 57 additions & 30 deletions include/ui/radar_theme.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,63 +4,90 @@

namespace ui::radar {

constexpr int kSize = 240;
constexpr int kCenterX = kSize / 2;
constexpr int kCenterY = kSize / 2;
/**
* Radar layout.
*
* Lengths were chosen against a 240x240 panel and are scaled at runtime by
* initMetrics() to the panel actually fitted, so they are variables rather than
* constants — the same shape the colours below have always had. Values that are
* not lengths (counts, times, km, ratios) stay constexpr.
*/

/** The panel this layout was designed on; every scaled value is a ratio of it. */
constexpr int kReferenceSize = 240;

/**
* Recompute the layout for a panel of this size. Call once, after `tft.init()`
* and before anything draws. `panel_width`/`panel_height` come from
* `tft.width()` / `tft.height()` rather than from `config.h`, so a panel that
* reports a size different from the configured one still lays out correctly.
*/
void initMetrics(int panel_width, int panel_height);

/** Scale actually applied, = min(width, height) / kReferenceSize. 1.0 on 240px. */
extern float kScale;

/** Square extent of the radar, = min(panel width, panel height). */
extern int kSize;
extern int kCenterX;
extern int kCenterY;

/** Outermost grid ring (inside edge labels). */
constexpr int kGridOuterRadius = 107;
extern int kGridOuterRadius;

/** N: offset from top edge (top_center, negative = up). */
constexpr int kCardinalNorthOffsetY = -1;
extern int kCardinalNorthOffsetY;
/** S: offset from bottom edge (bottom_center, positive = down). */
constexpr int kCardinalSouthOffsetY = 3;
extern int kCardinalSouthOffsetY;

/** Gap between scale label right edge and outer ring on the east spoke (px). */
constexpr int kScaleGapFromOuterRing = 6;
extern int kScaleGapFromOuterRing;

/** Target cap height (px) for N/S/E/W. */
constexpr int kCardinalLabelHeightPx = 14;
extern int kCardinalLabelHeightPx;
/** Scale label is this many px shorter than cardinals. */
constexpr int kScaleBelowCardinalPx = 3;
extern int kScaleBelowCardinalPx;

/** A count, not a length — the same four rings at any panel size. */
constexpr int kRingCount = 4;

/** Shared grid stroke: drawWideLine half-width (~2 px total); rings use the same px count. */
constexpr float kGridStrokeHalfWidth = 1.0f;
/** Shared grid stroke: drawWideLine half-width; rings use the same px count. */
extern float kGridStrokeHalfWidth;

constexpr int kCenterDotRadius = 2;
extern int kCenterDotRadius;

/** Filled aircraft symbol (nose triangle). */
constexpr int kAircraftNoseLenPx = 8;
constexpr int kAircraftTailLenPx = 3;
constexpr int kAircraftTailHalfPx = 4;
extern int kAircraftNoseLenPx;
extern int kAircraftTailLenPx;
extern int kAircraftTailHalfPx;

/** 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;
/** 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. */
/** Shorter than full 60 s horizon at ref scale; x1.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 kRunwayLineWidthPx = 2.0f;
constexpr float kRunwayLineHalfWidth = kRunwayLineWidthPx * 0.5f;
constexpr int kRunwayLabelHeightPx = kCardinalLabelHeightPx;
constexpr int kRunwayLabelGapPx = 3;
/** Minimum visible vector when gs > 0 (px). */
extern int kAircraftSpeedLineMinPx;
/** drawWideLine half-width for speed vectors. */
extern float kAircraftTrackLineHalfWidth;

extern float kRunwayLineWidthPx;
extern float kRunwayLineHalfWidth;
extern int kRunwayLabelHeightPx;
extern int kRunwayLabelGapPx;

/** Gap from triangle edge to tag block (px). */
constexpr int kAircraftLabelGapPx = 1;
extern int kAircraftLabelGapPx;
/** Keep symbol centroid inside outer ring by at least this inset (px). */
constexpr int kAircraftInsideRingInsetPx =
kAircraftNoseLenPx + kAircraftTailHalfPx + 1;
extern int kAircraftInsideRingInsetPx;

/** Beyond-ring traffic: bearing cues on screen rim (correct direction, fixed radius). */
constexpr int kBeyondRingDotRadiusPx = 4;
constexpr int kBeyondRingScreenMarginPx = 2;
extern int kBeyondRingDotRadiusPx;
extern int kBeyondRingScreenMarginPx;
/** Target cap height (px) for aircraft tags (bold, slightly above scale label). */
constexpr int kAircraftTagLabelHeightPx = 13;
extern int kAircraftTagLabelHeightPx;

/** RGB565 palette targets (applied in initPalette). */
constexpr uint8_t kBgR = 4;
Expand Down
Loading