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
35 changes: 33 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

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.

The RockBase IoT Team adds support for **NM-TV-154**: **ESP32** + **1.54″ square ST7789** display (240×240).

![NM-TV-154](docs/images/nm-tv-154.jpg)

## What it does

1. **Wi‑Fi setup** (if needed) — captive portal on AP **`PlaneRadar-Setup`**
Expand All @@ -22,6 +26,18 @@ After Wi‑Fi is saved, the device reconnects automatically; the radar runs in t

During setup you can also hold BOOT at power-on to force a credential reset (same as the long press).

### NM-TV-154 touch control

On **NM-TV-154**, the onboard capacitive touch key **T9 / GPIO32** can also cycle the same range presets (**5 → 10 → 15 → 25 km**).
Validated board signals:

| Signal | Value |
|--------|-------|
| Display | ST7789, 240×240 |
| LCD power | GPIO **21** (`LOW = enabled`) |
| Backlight | GPIO **19** (`active LOW`) |
| Touch | **T9 / GPIO32** |

## Wi‑Fi setup portal

**First-time setup** (no saved Wi‑Fi):
Expand Down Expand Up @@ -158,13 +174,28 @@ pio run -t upload
pio device monitor
```

- PlatformIO env: **`supermini`**
- PlatformIO envs: **`supermini`**, **`nm-tv-154`**
- Current default env in `platformio.ini`: **`nm-tv-154`**
- Serial: **115200** baud
- USB CDC on boot enabled in `platformio.ini` for the Super Mini

### NM-TV-154 build

```bash
pio run -e nm-tv-154
pio run -e nm-tv-154 -t upload
```

For split-image flashing on **NM-TV-154** (**ESP32**), the standard offsets are:

- `bootloader.bin` → **0x1000**
- `partitions.bin` → **0x8000**
- `boot_app0.bin` → **0xE000**
- `firmware.bin` → **0x10000**

### Web-flashable release image

Single `.bin` for [esptool-js](https://espressif.github.io/esptool-js/) and similar tools (ESP32-C3, 4 MB, flash at **0x0**):
Single `.bin` for [esptool-js](https://espressif.github.io/esptool-js/) and similar tools (ESP32-C3, 4 MB, flash at **0x0**). For flashing NM-TV-154, can use [RockBase IoT WebFlasher](https://flash.rockbaseiot.com) one-click flash, choose Project: **ESP32-Plane-Radar**, Device: **NM-TV-154**.:

```bash
chmod +x scripts/merge-firmware.sh # once
Expand Down
Binary file added docs/images/nm-tv-154.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 22 additions & 5 deletions include/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,27 +22,44 @@ constexpr unsigned long kWifiConnectingFrameMs = 50;
constexpr unsigned long kWifiDownGraceMs = 4000;
/** Minimum interval between background reconnect tries. */
constexpr unsigned long kWifiReconnectIntervalMs = 15000;
/** Amsterdam local time, matching the default radar location; includes DST. */
constexpr char kLocalTimeZone[] = "CET-1CEST,M3.5.0,M10.5.0/3";
constexpr char kNtpServer[] = "pool.ntp.org";

// --- BOOT button (ESP32-C3 Super Mini, active LOW) ---
constexpr gpio_num_t kBootPin = GPIO_NUM_9;
constexpr unsigned long kBootResetHoldMs = 3000UL;
/** Ignore BOOT taps shorter than this (debounce). */
constexpr unsigned long kBootTapMinMs = 40UL;

// --- Display: 240x240 SPI panel ---
#ifdef BOARD_NM_TV_154
constexpr int kDisplayPinRst = -1;
constexpr int kDisplayPinCs = 15;
constexpr int kDisplayPinDc = 2;
constexpr int kDisplayPinMosi = 13;
constexpr int kDisplayPinSclk = 14;
#else
// --- 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
constexpr int kDisplayPinRst = GPIO_NUM_0;
constexpr int kDisplayPinCs = GPIO_NUM_1;
constexpr int kDisplayPinDc = GPIO_NUM_10;
constexpr int kDisplayPinMosi = GPIO_NUM_3; // display SDA
constexpr int kDisplayPinSclk = GPIO_NUM_4; // display SCL
#endif

constexpr int kDisplayWidth = 240;
constexpr int kDisplayHeight = 240;

constexpr uint32_t kDisplaySpiWriteHz = 40000000;
// GC9A01 modules often need invert + BGR for correct black/green output
constexpr bool kDisplayInvert = true;
#ifdef BOARD_NM_TV_154
// TFT_eSPI's ST7789 + CGRAM_OFFSET default selects BGR (MADCTL bit set).
constexpr bool kDisplayRgbOrder = false;
#else
constexpr bool kDisplayRgbOrder = true;
#endif

// --- Radar center defaults (overridden via WiFi setup portal) ---
constexpr double kDefaultRadarLat = 52.3676;
Expand Down
11 changes: 11 additions & 0 deletions include/hardware/display_bus_policy.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#pragma once

#include <cstdint>

namespace hardware::display {

constexpr uint8_t spiModeForBoard(bool is_nm_tv_154) {
return is_nm_tv_154 ? 3 : 0;
}

} // namespace hardware::display
34 changes: 33 additions & 1 deletion include/hardware/lgfx_config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,31 @@
#include <LovyanGFX.hpp>

#include "config.h"
#include "hardware/display_bus_policy.h"
#ifdef BOARD_NM_TV_154
#include "hardware/nm_tv_154_pins.h"
#endif

/** LovyanGFX device: GC9A01 on SPI. Pin values come from config.h. */
/** LovyanGFX device for the selected 240x240 SPI panel. */
class LGFX : public lgfx::LGFX_Device {
lgfx::Bus_SPI _bus;
#ifdef BOARD_NM_TV_154
lgfx::Panel_ST7789 _panel;
lgfx::Light_PWM _light;
#else
lgfx::Panel_GC9A01 _panel;
#endif

public:
LGFX() {
{
auto cfg = _bus.config();
cfg.spi_host = SPI2_HOST;
#ifdef BOARD_NM_TV_154
cfg.spi_mode = hardware::display::spiModeForBoard(true);
#else
cfg.spi_mode = hardware::display::spiModeForBoard(false);
#endif
cfg.freq_write = config::kDisplaySpiWriteHz;
cfg.pin_sclk = static_cast<int>(config::kDisplayPinSclk);
cfg.pin_mosi = static_cast<int>(config::kDisplayPinMosi);
Expand All @@ -29,8 +43,26 @@ class LGFX : public lgfx::LGFX_Device {
cfg.pin_rst = static_cast<int>(config::kDisplayPinRst);
cfg.invert = config::kDisplayInvert;
cfg.rgb_order = config::kDisplayRgbOrder;
#ifdef BOARD_NM_TV_154
cfg.panel_width = config::kDisplayWidth;
cfg.panel_height = config::kDisplayHeight;
cfg.memory_width = config::kDisplayWidth;
cfg.memory_height = 320;
cfg.offset_x = 0;
// TFT_eSPI's 240x240 CGRAM_OFFSET mapping starts at row 0 in rotation 0.
cfg.offset_y = 0;
#endif
_panel.config(cfg);
}
#ifdef BOARD_NM_TV_154
{
auto cfg = _light.config();
cfg.pin_bl = hardware::nm_tv_154::kLcdBacklightPin;
cfg.invert = hardware::nm_tv_154::kLcdBacklightActiveLow;
_light.config(cfg);
_panel.setLight(&_light);
}
#endif
setPanel(&_panel);
}
};
13 changes: 13 additions & 0 deletions include/hardware/nm_tv_154_pins.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#pragma once

#include <cstdint>

namespace hardware::nm_tv_154 {

constexpr uint8_t kLcdPowerPin = 21;
constexpr bool kLcdPowerEnabledLevel = false;
constexpr uint8_t kLcdBacklightPin = 19;
constexpr bool kLcdBacklightActiveLow = true;
constexpr uint8_t kTouchGpio = 32; // ESP32 touch channel T9.

} // namespace hardware::nm_tv_154
17 changes: 17 additions & 0 deletions include/services/time_settings.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#pragma once

namespace services::time_settings {

/** Load persisted clock display settings; call once during boot. */
void init();
/** Active POSIX timezone string, either the default or a saved manual value. */
const char* timeZone();
/** True when the portal's manual timezone option is enabled. */
bool usesManualTimeZone();
/** True for HH:MM; false for 12-hour time with AM/PM. */
bool uses24HourClock();
/** Persist portal values and apply the selected timezone immediately. */
void saveFromPortal(const char* manual_timezone_value, const char* timezone_value,
const char* clock_24h_value);

} // namespace services::time_settings
58 changes: 58 additions & 0 deletions include/ui/nm_tv_154_policy.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#pragma once

namespace ui::nm_tv_154 {

struct CornerTelemetryLayout {
int top_label_y;
int top_value_y;
int bottom_label_y;
int bottom_value_y;
};

constexpr CornerTelemetryLayout cornerTelemetryLayout(int display_size, int inset,
int label_height,
int value_height, int gap) {
return {inset, inset + label_height + gap,
display_size - inset - value_height - gap - label_height,
display_size - inset};
}

constexpr bool cornerTelemetryLayoutFits(int display_size, int inset,
int label_height, int value_height,
int gap) {
if (display_size <= 0 || inset < 0 || label_height <= 0 || value_height <= 0 ||
gap < 0) {
return false;
}

const CornerTelemetryLayout layout =
cornerTelemetryLayout(display_size, inset, label_height, value_height, gap);
return layout.top_label_y >= inset &&
layout.top_label_y + label_height + gap <= layout.top_value_y &&
layout.top_value_y + value_height + gap <= layout.bottom_label_y &&
layout.bottom_label_y + label_height + gap <=
layout.bottom_value_y - value_height &&
layout.bottom_value_y <= display_size - inset;
}

struct TouchRangeState {
bool was_down = false;
bool range_tap = false;
};

constexpr TouchRangeState nextTouchRangeState(TouchRangeState state, bool is_down) {
state.range_tap = false;
if (is_down) {
state.was_down = true;
return state;
}
if (!state.was_down) {
return state;
}

state.was_down = false;
state.range_tap = true;
return state;
}

} // namespace ui::nm_tv_154
6 changes: 6 additions & 0 deletions include/ui/radar_display.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,10 @@ void radarDisplayDraw();
/** Redraw aircraft only (blits cached grid; no full-screen clear). */
void radarDisplayRefreshAircraft();

/** Refresh only the NM-TV-154 update-age value without redrawing the radar. */
void radarDisplayRefreshStatus();

/** Record the time of the most recent successful ADS-B response. */
void radarDisplayMarkDataUpdated(unsigned long now_ms);

} // namespace ui
9 changes: 9 additions & 0 deletions include/ui/radar_render_policy.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#pragma once

namespace ui::radar {

constexpr bool frameSpriteEnabledForBoard(bool is_nm_tv_154) {
return !is_nm_tv_154;
}

} // namespace ui::radar
56 changes: 56 additions & 0 deletions include/ui/square_status.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#pragma once

#include <cstdint>

namespace ui::square {

enum class UpdateFreshness : uint8_t {
fresh,
stale,
unavailable,
};

constexpr uint8_t wifiBars(bool connected, int rssi) {
if (!connected) {
return 0;
}
if (rssi >= -55) {
return 4;
}
if (rssi >= -67) {
return 3;
}
if (rssi >= -75) {
return 2;
}
return 1;
}

constexpr unsigned long elapsedMs(unsigned long now_ms,
unsigned long then_ms) {
return now_ms - then_ms;
}

constexpr unsigned long updateAgeSeconds(unsigned long now_ms,
unsigned long last_update_ms) {
return elapsedMs(now_ms, last_update_ms) / 1000UL;
}

constexpr UpdateFreshness updateFreshness(bool connected, bool has_update,
unsigned long now_ms,
unsigned long last_update_ms) {
if (!connected || !has_update) {
return UpdateFreshness::unavailable;
}

const unsigned long age_ms = elapsedMs(now_ms, last_update_ms);
if (age_ms < 10000UL) {
return UpdateFreshness::fresh;
}
if (age_ms < 30000UL) {
return UpdateFreshness::stale;
}
return UpdateFreshness::unavailable;
}

} // namespace ui::square
2 changes: 1 addition & 1 deletion partitions/plane_radar.csv
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 4 MB flash single large app (no OTA slot). Plane Radar + runway dataset.
# 4 MB flash - single large app (no OTA slot). Plane Radar + runway dataset.
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
Expand Down
Loading