diff --git a/README.md b/README.md index 0318b3f4f..8286e1be2 100644 --- a/README.md +++ b/README.md @@ -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`** @@ -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): @@ -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 diff --git a/docs/images/nm-tv-154.jpg b/docs/images/nm-tv-154.jpg new file mode 100644 index 000000000..efc9314c4 Binary files /dev/null and b/docs/images/nm-tv-154.jpg differ diff --git a/include/config.h b/include/config.h index 6555c0342..facf2a4de 100644 --- a/include/config.h +++ b/include/config.h @@ -22,6 +22,9 @@ 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; @@ -29,12 +32,21 @@ 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; @@ -42,7 +54,12 @@ 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; diff --git a/include/hardware/display_bus_policy.h b/include/hardware/display_bus_policy.h new file mode 100644 index 000000000..78fcb2704 --- /dev/null +++ b/include/hardware/display_bus_policy.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +namespace hardware::display { + +constexpr uint8_t spiModeForBoard(bool is_nm_tv_154) { + return is_nm_tv_154 ? 3 : 0; +} + +} // namespace hardware::display diff --git a/include/hardware/lgfx_config.hpp b/include/hardware/lgfx_config.hpp index 4f1484b6a..afe443980 100644 --- a/include/hardware/lgfx_config.hpp +++ b/include/hardware/lgfx_config.hpp @@ -4,17 +4,31 @@ #include #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(config::kDisplayPinSclk); cfg.pin_mosi = static_cast(config::kDisplayPinMosi); @@ -29,8 +43,26 @@ class LGFX : public lgfx::LGFX_Device { cfg.pin_rst = static_cast(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); } }; diff --git a/include/hardware/nm_tv_154_pins.h b/include/hardware/nm_tv_154_pins.h new file mode 100644 index 000000000..4631f0718 --- /dev/null +++ b/include/hardware/nm_tv_154_pins.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +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 diff --git a/include/services/time_settings.h b/include/services/time_settings.h new file mode 100644 index 000000000..38508a205 --- /dev/null +++ b/include/services/time_settings.h @@ -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 \ No newline at end of file diff --git a/include/ui/nm_tv_154_policy.h b/include/ui/nm_tv_154_policy.h new file mode 100644 index 000000000..bb979c4ba --- /dev/null +++ b/include/ui/nm_tv_154_policy.h @@ -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 \ No newline at end of file diff --git a/include/ui/radar_display.h b/include/ui/radar_display.h index 5ab3bb292..a5930c9ed 100644 --- a/include/ui/radar_display.h +++ b/include/ui/radar_display.h @@ -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 diff --git a/include/ui/radar_render_policy.h b/include/ui/radar_render_policy.h new file mode 100644 index 000000000..49ac3ed10 --- /dev/null +++ b/include/ui/radar_render_policy.h @@ -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 diff --git a/include/ui/square_status.h b/include/ui/square_status.h new file mode 100644 index 000000000..51cd8d282 --- /dev/null +++ b/include/ui/square_status.h @@ -0,0 +1,56 @@ +#pragma once + +#include + +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 diff --git a/partitions/plane_radar.csv b/partitions/plane_radar.csv index 57fff6c93..169d3c523 100644 --- a/partitions/plane_radar.csv +++ b/partitions/plane_radar.csv @@ -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, diff --git a/platformio.ini b/platformio.ini index c81ce535f..c190546d5 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,10 +1,13 @@ +[platformio] +default_envs = nm-tv-154 + ; ESP32-C3 Super Mini + 1.28" round GC9A01 (240×240) ; Board in Arduino IDE: "ESP32C3 Dev Module" or "MakerGO ESP32 C3 SuperMini" - [env:supermini] platform = espressif32@6.5.0 board = esp32-c3-devkitm-1 framework = arduino +build_unflags = -std=gnu++11 monitor_speed = 115200 board_build.partitions = partitions/plane_radar.csv extra_scripts = post:scripts/merge_firmware.py @@ -24,3 +27,42 @@ lib_deps = lovyan03/LovyanGFX@^1.2.7 tzapu/WiFiManager@^2.0.17 bblanchon/ArduinoJson@^7.4.2 + +[env:nm-tv-154] +platform = espressif32@6.5.0 +board = esp32dev +framework = arduino +build_unflags = -std=gnu++11 +monitor_speed = 115200 +board_build.partitions = partitions/plane_radar.csv +extra_scripts = post:scripts/merge_firmware.py + +; Anti-aliased VLW font (Noto Sans Bold 15, from TFT_eSPI smooth-font examples) +board_build.embed_files = data/ui_font.vlw + +; USB serial over USB-C (Super Mini native USB) +build_flags = + -std=gnu++17 + -DWM_NODEBUG + -DWM_MDNS + -DBOARD_NM_TV_154=1 + -DUSER_SETUP_LOADED=1 + -DST7789_DRIVER=1 + -DLOAD_GLCD=1 + -DCGRAM_OFFSET + -DUSE_HSPI_PORT + -DTFT_WIDTH=240 + -DTFT_HEIGHT=240 + -DTFT_MOSI=13 + -DTFT_SCLK=14 + -DTFT_CS=15 + -DTFT_DC=2 + -DTFT_RST=-1 + -DTFT_MISO=-1 + -DSPI_FREQUENCY=40000000 + -DSPI_READ_FREQUENCY=20000000 + +lib_deps = + lovyan03/LovyanGFX@^1.2.7 + tzapu/WiFiManager@^2.0.17 + bblanchon/ArduinoJson@^7.4.2 diff --git a/scripts/merge_firmware.py b/scripts/merge_firmware.py index 2dfcd9661..45c0c8068 100644 --- a/scripts/merge_firmware.py +++ b/scripts/merge_firmware.py @@ -7,6 +7,10 @@ from os.path import join +def bootloader_offset_for_mcu(mcu): + return "0x1000" if mcu in ("esp32", "esp32s2") else "0x0" + + def merge_firmware(source, target, env): build_dir = env.subst("$BUILD_DIR") progname = env.subst("${PROGNAME}") @@ -15,6 +19,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") + bootloader_offset = bootloader_offset_for_mcu(mcu) flash_size = env.BoardConfig().get("upload.flash_size", "4MB") bootloader = join(build_dir, "bootloader.bin") @@ -44,7 +49,7 @@ def merge_firmware(source, target, env): "80m", "--flash_size", flash_size, - "0x0", + bootloader_offset, bootloader, "0x8000", partitions, diff --git a/src/hardware/display.cpp b/src/hardware/display.cpp index 42f82524d..e9dcf7fcc 100644 --- a/src/hardware/display.cpp +++ b/src/hardware/display.cpp @@ -1,13 +1,34 @@ #include "hardware/display.h" +#include + #include "hardware/display_font.h" +#ifdef BOARD_NM_TV_154 +#include "hardware/nm_tv_154_pins.h" +#endif LGFX tft; void displayInit() { +#ifdef BOARD_NM_TV_154 + pinMode(hardware::nm_tv_154::kLcdPowerPin, OUTPUT); + digitalWrite(hardware::nm_tv_154::kLcdPowerPin, + hardware::nm_tv_154::kLcdPowerEnabledLevel ? HIGH : LOW); + delay(50); +#endif tft.init(); tft.setRotation(0); tft.setBrightness(255); tft.setTextWrap(false); +#ifdef BOARD_NM_TV_154 + Serial.println( + "display: ST7789 240x240 mem=240x320 offset=(0,0) rotation=0 " + "order=BGR invert=ON spi=MODE3 pwr=GPIO21/LOW bl=GPIO19/LOW"); + tft.fillRect(0, 0, 80, 240, 0xF800); + tft.fillRect(80, 0, 80, 240, 0x07E0); + tft.fillRect(160, 0, 80, 240, 0x001F); + delay(750); + tft.fillScreen(0x0000); +#endif displayFontInit(); } diff --git a/src/main.cpp b/src/main.cpp index 797aecc09..2f388f9e0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,14 +5,20 @@ #include #include +#include + #include "config.h" #include "hardware/display.h" #include "services/adsb_client.h" #include "services/radar_location.h" +#include "services/time_settings.h" #include "services/wifi_setup.h" #include "ui/radar_display.h" #include "ui/radar_range.h" #include "ui/status_screens.h" +#ifdef BOARD_NM_TV_154 +#include "ui/nm_tv_154_policy.h" +#endif namespace { @@ -20,12 +26,31 @@ bool g_radar_visible = false; unsigned long g_wifi_down_since = 0; unsigned long g_last_reconnect_ms = 0; unsigned long g_last_adsb_fetch_ms = 0; +#ifdef BOARD_NM_TV_154 +constexpr uint16_t kTouchPressedThreshold = 90; +constexpr unsigned long kSquareStatusRefreshIntervalMs = 1000UL; +unsigned long g_last_square_status_refresh_ms = 0; +bool g_clock_started = false; +ui::nm_tv_154::TouchRangeState g_touch_range_state; +#endif + +void startLocalClock() { +#ifdef BOARD_NM_TV_154 + if (g_clock_started || WiFi.status() != WL_CONNECTED) { + return; + } + configTime(0, 0, config::kNtpServer); + g_clock_started = true; + Serial.printf("Clock sync: %s\n", config::kNtpServer); +#endif +} void showRadarIfConnected() { if (WiFi.status() != WL_CONNECTED) { g_radar_visible = false; return; } + startLocalClock(); ui::radarDisplayDraw(); g_radar_visible = true; } @@ -42,20 +67,47 @@ void onRangeTap() { } } +void handleNmTv154RangeTouch() { +#ifdef BOARD_NM_TV_154 + const uint16_t raw = touchRead(T9); + const bool is_down = raw < kTouchPressedThreshold; + const bool was_down = g_touch_range_state.was_down; + g_touch_range_state = ui::nm_tv_154::nextTouchRangeState(g_touch_range_state, + is_down); + if (is_down && !was_down) { + Serial.printf("[touch] pressed, raw=%u\n", static_cast(raw)); + } + if (g_touch_range_state.range_tap) { + Serial.println("[touch] tap -> range"); + onRangeTap(); + } +#endif +} + +void pollNetwork() { + wifiLoop(); + handleNmTv154RangeTouch(); +} + void handleBootButton() { bootButtonPollLongPress(); if (bootButtonConsumeTap()) { onRangeTap(); } + handleNmTv154RangeTouch(); } void fetchAndDrawAircraft() { const float fetch_km = ui::radar::fetchRadiusKm(); if (!services::adsb::fetchUpdate(services::location::lat(), services::location::lon(), fetch_km)) { +#ifdef BOARD_NM_TV_154 + ui::radarDisplayRefreshAircraft(); +#endif handleBootButton(); return; } + ui::radarDisplayMarkDataUpdated(millis()); ui::radarDisplayRefreshAircraft(); handleBootButton(); } @@ -74,8 +126,13 @@ void setup() { statusScreenPortal(); } services::location::init(); + services::time_settings::init(); ui::radar::rangeInit(); - services::adsb::setPollFn(wifiLoop); + services::adsb::setPollFn(pollNetwork); +#ifdef BOARD_NM_TV_154 + Serial.printf("[touch] ready, raw=%u\n", + static_cast(touchRead(T9))); +#endif if (wifiSetupConnect()) { showRadarIfConnected(); @@ -88,6 +145,9 @@ void loop() { if (WiFi.status() != WL_CONNECTED) { if (g_radar_visible) { +#ifdef BOARD_NM_TV_154 + ui::radarDisplayRefreshAircraft(); +#endif Serial.println("WiFi lost — will reconnect"); g_radar_visible = false; } @@ -109,9 +169,18 @@ void loop() { g_wifi_down_since = 0; if (!g_radar_visible) { showRadarIfConnected(); - } else if (millis() - g_last_adsb_fetch_ms >= config::kAdsbFetchIntervalMs) { - g_last_adsb_fetch_ms = millis(); - fetchAndDrawAircraft(); + } else { + const unsigned long now_ms = millis(); + if (now_ms - g_last_adsb_fetch_ms >= config::kAdsbFetchIntervalMs) { + g_last_adsb_fetch_ms = now_ms; + fetchAndDrawAircraft(); +#ifdef BOARD_NM_TV_154 + } else if (now_ms - g_last_square_status_refresh_ms >= + kSquareStatusRefreshIntervalMs) { + g_last_square_status_refresh_ms = now_ms; + ui::radarDisplayRefreshStatus(); +#endif + } } } diff --git a/src/services/time_settings.cpp b/src/services/time_settings.cpp new file mode 100644 index 000000000..cca85d47f --- /dev/null +++ b/src/services/time_settings.cpp @@ -0,0 +1,106 @@ +#include "services/time_settings.h" + +#include + +#include +#include +#include + +#include "config.h" + +namespace services::time_settings { +namespace { + +constexpr char kPrefsNamespace[] = "clock"; +constexpr char kManualTimezoneKey[] = "manualTz"; +constexpr char kTimezoneKey[] = "timezone"; +constexpr char kClock24HourKey[] = "clock24"; +constexpr size_t kTimezoneMaxLen = 63; + +bool s_manual_timezone = false; +bool s_clock_24_hour = true; +char s_timezone[kTimezoneMaxLen + 1] = {}; + +bool checkboxChecked(const char* value) { + return value != nullptr && + (strcmp(value, "T") == 0 || strcmp(value, "on") == 0); +} + +bool validTimezone(const char* value) { + if (value == nullptr || value[0] == '\0' || + strnlen(value, kTimezoneMaxLen + 1) > kTimezoneMaxLen) { + return false; + } + for (const char* cursor = value; *cursor != '\0'; ++cursor) { + const unsigned char character = static_cast(*cursor); + if (!(std::isalnum(character) || strchr("_+-,./<>", *cursor) != nullptr)) { + return false; + } + } + return true; +} + +void applyTimezone() { + setenv("TZ", s_timezone, 1); + tzset(); +} + +void setAutomaticTimezone() { + s_manual_timezone = false; + strncpy(s_timezone, config::kLocalTimeZone, sizeof(s_timezone) - 1); + s_timezone[sizeof(s_timezone) - 1] = '\0'; +} + +} // namespace + +void init() { + setAutomaticTimezone(); + Preferences prefs; + if (prefs.begin(kPrefsNamespace, true)) { + s_clock_24_hour = prefs.getBool(kClock24HourKey, true); + if (prefs.getBool(kManualTimezoneKey, false)) { + const String saved_timezone = prefs.getString(kTimezoneKey, ""); + if (validTimezone(saved_timezone.c_str())) { + s_manual_timezone = true; + saved_timezone.toCharArray(s_timezone, sizeof(s_timezone)); + } + } + prefs.end(); + } + applyTimezone(); +} + +const char* timeZone() { return s_timezone; } + +bool usesManualTimeZone() { return s_manual_timezone; } + +bool uses24HourClock() { return s_clock_24_hour; } + +void saveFromPortal(const char* manual_timezone_value, const char* timezone_value, + const char* clock_24h_value) { + const bool wants_manual_timezone = checkboxChecked(manual_timezone_value); + if (wants_manual_timezone && validTimezone(timezone_value)) { + s_manual_timezone = true; + strncpy(s_timezone, timezone_value, sizeof(s_timezone) - 1); + s_timezone[sizeof(s_timezone) - 1] = '\0'; + } else { + setAutomaticTimezone(); + if (wants_manual_timezone) { + Serial.println("Invalid manual timezone; using automatic timezone"); + } + } + s_clock_24_hour = checkboxChecked(clock_24h_value); + + Preferences prefs; + if (prefs.begin(kPrefsNamespace, false)) { + prefs.putBool(kManualTimezoneKey, s_manual_timezone); + prefs.putString(kTimezoneKey, s_timezone); + prefs.putBool(kClock24HourKey, s_clock_24_hour); + prefs.end(); + } + applyTimezone(); + Serial.printf("Clock settings: timezone=%s, format=%s\n", s_timezone, + s_clock_24_hour ? "24h" : "12h"); +} + +} // namespace services::time_settings \ No newline at end of file diff --git a/src/services/wifi_setup.cpp b/src/services/wifi_setup.cpp index b4c24c873..704fa4170 100644 --- a/src/services/wifi_setup.cpp +++ b/src/services/wifi_setup.cpp @@ -15,6 +15,7 @@ #include "config.h" #include "services/radar_location.h" +#include "services/time_settings.h" #include "ui/radar_range.h" #include "ui/status_screens.h" @@ -61,6 +62,8 @@ constexpr char kPrefsForcePortalKey[] = "portal"; bool s_force_config_portal = false; WiFiManager s_wm; bool s_wm_configured = false; +bool s_has_portal_log = false; +unsigned long s_last_portal_log_ms = 0; void ensureWifiManager(); void startLanWebPortal(); @@ -71,11 +74,50 @@ constexpr int kCoordParamLen = 20; constexpr char kCoordInputAttrs[] = " type=\"number\" step=\"0.000001\""; +constexpr char kCitySelectorHtml[] = R"html( + + +

Offline presets fill latitude and longitude only. Search by city, then save; you can always enter coordinates manually below.

+ +)html"; + +constexpr char kTimeSettingsHintHtml[] = R"html( +

Automatic timezone uses the default radar location. Enable manual timezone only when you need an override; enter a POSIX timezone such as CST-8 for China or EST5EDT,M3.2.0,M11.1.0 for US Eastern time.

+ +)html"; + +WiFiManagerParameter s_param_city_selector(kCitySelectorHtml); + WiFiManagerParameter s_param_lat("radar_lat", "Latitude (deg)", "0", kCoordParamLen, kCoordInputAttrs); WiFiManagerParameter s_param_lon("radar_lon", "Longitude (deg)", "0", kCoordParamLen, kCoordInputAttrs); +char s_manual_timezone_attrs[32] = "type=\"checkbox\""; +WiFiManagerParameter s_param_manual_timezone("manual_tz", "Use manual timezone", "T", 2, + s_manual_timezone_attrs, WFM_LABEL_AFTER); +WiFiManagerParameter s_param_timezone("time_zone", "Manual POSIX timezone", "", + 64, "placeholder=\"CST-8\""); +WiFiManagerParameter s_param_time_settings_hint(kTimeSettingsHintHtml); +char s_clock_24h_attrs[32] = "type=\"checkbox\" checked"; +WiFiManagerParameter s_param_clock_24h("clock_24h", "Use 24-hour time", "T", 2, + s_clock_24h_attrs, WFM_LABEL_AFTER); + char s_miles_checkbox_attrs[32] = "type=\"checkbox\""; WiFiManagerParameter s_param_miles("use_miles", "Display distances in miles", "T", 2, s_miles_checkbox_attrs, WFM_LABEL_AFTER); @@ -91,6 +133,17 @@ void refreshPortalParamDefaults() { snprintf(lon_buf, sizeof(lon_buf), "%.6f", services::location::lon()); s_param_lat.setValue(lat_buf, kCoordParamLen); s_param_lon.setValue(lon_buf, kCoordParamLen); + snprintf(s_manual_timezone_attrs, sizeof(s_manual_timezone_attrs), + "type=\"checkbox\"%s", + services::time_settings::usesManualTimeZone() ? " checked" : ""); + s_param_manual_timezone.setValue("T", 2); + s_param_timezone.setValue(services::time_settings::usesManualTimeZone() + ? services::time_settings::timeZone() + : "", + 64); + snprintf(s_clock_24h_attrs, sizeof(s_clock_24h_attrs), "type=\"checkbox\"%s", + services::time_settings::uses24HourClock() ? " checked" : ""); + s_param_clock_24h.setValue("T", 2); snprintf(s_miles_checkbox_attrs, sizeof(s_miles_checkbox_attrs), "type=\"checkbox\"%s", ui::radar::useMiles() ? " checked" : ""); s_param_miles.setValue("T", 2); @@ -104,19 +157,38 @@ void onPortalParamsSaved() { s_param_lon.getValue())) { Serial.println("Invalid lat/lon in portal — keeping previous location"); } + services::time_settings::saveFromPortal(s_param_manual_timezone.getValue(), + s_param_timezone.getValue(), + s_param_clock_24h.getValue()); ui::radar::saveMilesFromPortal(s_param_miles.getValue()); ui::radar::saveRunwaysFromPortal(s_param_runways.getValue()); } void attachPortalParams(WiFiManager& wm) { refreshPortalParamDefaults(); + wm.addParameter(&s_param_city_selector); wm.addParameter(&s_param_lat); wm.addParameter(&s_param_lon); + wm.addParameter(&s_param_manual_timezone); + wm.addParameter(&s_param_timezone); + wm.addParameter(&s_param_time_settings_hint); + wm.addParameter(&s_param_clock_24h); wm.addParameter(&s_param_miles); wm.addParameter(&s_param_runways); wm.setSaveParamsCallback(onPortalParamsSaved); } +void logPortalLifecycle(const char* event) { + constexpr unsigned long kPortalLogMinIntervalMs = 1000UL; + const unsigned long now_ms = millis(); + if (s_has_portal_log && now_ms - s_last_portal_log_ms < kPortalLogMinIntervalMs) { + return; + } + s_has_portal_log = true; + s_last_portal_log_ms = now_ms; + Serial.printf("Portal: %s\n", event); +} + void markForceConfigPortal() { s_force_config_portal = true; Preferences prefs; @@ -195,6 +267,7 @@ void resetWifiCredentials() { } void onConfigPortalApStarted(WiFiManager*) { + logPortalLifecycle("setup AP started"); WiFi.setTxPower(WIFI_POWER_8_5dBm); statusScreenPortal(); #ifdef WM_MDNS @@ -243,6 +316,7 @@ void startLanWebPortal() { } #endif s_wm.startWebPortal(); + logPortalLifecycle("LAN portal started"); Serial.printf("LAN config: http://%s.local or http://%s\n", config::kPortalHostname, WiFi.localIP().toString().c_str()); } @@ -251,6 +325,7 @@ void stopLanWebPortal() { if (!s_wm.getWebPortalActive()) { return; } + logPortalLifecycle("LAN portal stopped"); s_wm.stopWebPortal(); #ifdef WM_MDNS MDNS.end(); @@ -337,6 +412,7 @@ bool openConfigPortal() { statusScreenPortal(); s_wm.setConfigPortalBlocking(false); s_wm.startConfigPortal(config::kPortalApName); + logPortalLifecycle("setup portal requested"); while (s_wm.getConfigPortalActive()) { bootButtonPollLongPress(); if (s_wm.process()) { @@ -344,6 +420,7 @@ bool openConfigPortal() { } delay(10); } + logPortalLifecycle("setup portal stopped"); return wifiLinkUp(); } diff --git a/src/ui/radar_display.cpp b/src/ui/radar_display.cpp index b0d333997..08c6453bc 100644 --- a/src/ui/radar_display.cpp +++ b/src/ui/radar_display.cpp @@ -4,18 +4,30 @@ #include #include +#include #include +#include + +#ifdef BOARD_NM_TV_154 +#include +#endif #include "config.h" #include "hardware/display.h" #include "hardware/display_font.h" #include "services/adsb_client.h" #include "services/radar_location.h" +#include "services/time_settings.h" #include "ui/radar_range.h" +#include "ui/radar_render_policy.h" #include "ui/radar_theme.h" #include "ui/runway_overlay.h" +#ifdef BOARD_NM_TV_154 +#include "ui/square_status.h" +#include "ui/nm_tv_154_policy.h" +#endif -namespace fonts = lgfx::v1::fonts; +namespace radar_fonts = lgfx::v1::fonts; namespace ui { namespace radar { @@ -40,10 +52,13 @@ bool s_cardinal_use_vlw = false; bool s_scale_use_vlw = false; float s_cardinal_vlw_size = 0.56f; float s_scale_vlw_size = 0.50f; +bool s_corner_value_use_vlw = false; +float s_corner_value_vlw_size = 0.9f; float s_tag_vlw_size = 0.56f; -const lgfx::GFXfont* s_cardinal_gfx = &fonts::FreeSansBold12pt7b; -const lgfx::GFXfont* s_scale_gfx = &fonts::FreeSansBold9pt7b; -const lgfx::GFXfont* s_tag_gfx = &fonts::FreeSansBold12pt7b; +const lgfx::GFXfont* s_cardinal_gfx = &radar_fonts::FreeSansBold12pt7b; +const lgfx::GFXfont* s_scale_gfx = &radar_fonts::FreeSansBold9pt7b; +const lgfx::GFXfont* s_corner_value_gfx = &radar_fonts::FreeSansBold18pt7b; +const lgfx::GFXfont* s_tag_gfx = &radar_fonts::FreeSansBold12pt7b; bool s_tag_label_metrics_ready = false; bool s_tag_use_vlw = false; @@ -55,6 +70,16 @@ lgfx::LovyanGFX* s_draw = &tft; LGFX_Sprite s_frame(&tft); bool s_frame_ready = false; +#ifdef BOARD_NM_TV_154 +bool s_has_data_update = false; +unsigned long s_last_data_update_ms = 0; +uint16_t s_corner_green = 0; +uint16_t s_corner_amber = 0; +uint16_t s_corner_cyan = 0; +uint16_t s_corner_red = 0; +uint16_t s_corner_muted = 0; +#endif + class DrawScope { public: explicit DrawScope(lgfx::LovyanGFX& gfx) : prev_(s_draw) { s_draw = &gfx; } @@ -114,6 +139,7 @@ void initLabelMetrics() { } const int cardinal_target = radar::kCardinalLabelHeightPx; + constexpr int kCornerValueTargetHeightPx = 24; if (displayFontIsSmooth()) { s_cardinal_use_vlw = true; @@ -122,19 +148,25 @@ void initLabelMetrics() { const int scale_target = cardinal_h - radar::kScaleBelowCardinalPx; s_scale_use_vlw = true; s_scale_vlw_size = findVlwSizeForHeight(scale_target); + s_corner_value_use_vlw = true; + s_corner_value_vlw_size = findVlwSizeForHeight(kCornerValueTargetHeightPx); } else { - const lgfx::GFXfont* cardinal_candidates[] = {&fonts::FreeSansBold12pt7b, - &fonts::FreeSansBold9pt7b}; + const lgfx::GFXfont* cardinal_candidates[] = { + &radar_fonts::FreeSansBold12pt7b, + &radar_fonts::FreeSansBold9pt7b}; s_cardinal_gfx = pickGfxFontClosest(cardinal_target, cardinal_candidates, 2); s_cardinal_use_vlw = false; const int cardinal_h = measureGfxHeight(*s_cardinal_gfx); const int scale_target = cardinal_h - radar::kScaleBelowCardinalPx; - const lgfx::GFXfont* scale_candidates[] = {&fonts::FreeSansBold9pt7b, - &fonts::FreeSansBold12pt7b}; + const lgfx::GFXfont* scale_candidates[] = { + &radar_fonts::FreeSansBold9pt7b, + &radar_fonts::FreeSansBold12pt7b}; s_scale_gfx = pickGfxFontClosest(scale_target, scale_candidates, 2); s_scale_use_vlw = false; + s_corner_value_gfx = &radar_fonts::FreeSansBold18pt7b; + s_corner_value_use_vlw = false; } applyScaleStyle(); @@ -165,8 +197,9 @@ void initTagLabelMetrics() { s_tag_use_vlw = true; s_tag_vlw_size = findVlwSizeForHeight(target); } else { - const lgfx::GFXfont* tag_candidates[] = {&fonts::FreeSansBold12pt7b, - &fonts::FreeSansBold9pt7b}; + const lgfx::GFXfont* tag_candidates[] = { + &radar_fonts::FreeSansBold12pt7b, + &radar_fonts::FreeSansBold9pt7b}; s_tag_gfx = pickGfxFontClosest(target, tag_candidates, 2); s_tag_use_vlw = false; } @@ -174,6 +207,13 @@ void initTagLabelMetrics() { s_tag_label_metrics_ready = true; } +uint16_t logicalColor565(uint8_t red, uint8_t green, uint8_t blue) { + if (config::kDisplayRgbOrder) { + return tft.color565(blue, green, red); + } + return tft.color565(red, green, blue); +} + void initPalette() { radar::kColorBackground = tft.color565(radar::kBgR, radar::kBgG, radar::kBgB); radar::kColorGrid = tft.color565(radar::kGridR, radar::kGridG, radar::kGridB); @@ -197,6 +237,13 @@ void initPalette() { tft.color565(radar::kRunwayR, radar::kRunwayG, radar::kRunwayB); radar::kColorRunwayLabel = tft.color565(radar::kRunwayLabelR, radar::kRunwayLabelG, radar::kRunwayLabelB); +#ifdef BOARD_NM_TV_154 + s_corner_green = logicalColor565(54, 220, 110); + s_corner_amber = logicalColor565(255, 190, 70); + s_corner_cyan = logicalColor565(65, 205, 235); + s_corner_red = logicalColor565(255, 75, 75); + s_corner_muted = logicalColor565(95, 125, 145); +#endif } constexpr float kKmPerDeg = 111.0f; @@ -559,6 +606,14 @@ void applyScaleStyle() { } } +void applyCornerValueStyle() { + if (s_corner_value_use_vlw) { + displayFontSetSmoothSize(*s_draw, s_corner_value_vlw_size); + } else { + displayFontSetBitmap(*s_draw, s_corner_value_gfx); + } +} + void drawCardinalLabel(const char* text, int x, int y, textdatum_t datum) { applyCardinalStyle(); s_draw->setTextDatum(datum); @@ -636,6 +691,106 @@ void drawScaleLabel(int cx, int cy, int outer_radius) { scaleLabelAnchorX(cx, outer_radius), cy); } +#ifdef BOARD_NM_TV_154 +void drawCornerTime(const nm_tv_154::CornerTelemetryLayout& layout, + int value_height, int right, bool clear_value_area) { + char value[12]; + tm local_time = {}; + uint16_t time_color = s_corner_muted; + if (getLocalTime(&local_time, 0)) { + strftime(value, sizeof(value), + services::time_settings::uses24HourClock() ? "%H:%M" : "%I:%M %p", + &local_time); + time_color = s_corner_cyan; + } else { + snprintf(value, sizeof(value), "--"); + } + + applyCornerValueStyle(); + if (clear_value_area) { + const int max_width = s_draw->textWidth("88:88 PM"); + s_draw->fillRect(right - max_width, layout.bottom_value_y - value_height, + max_width + 1, value_height + 1, radar::kColorBackground); + } + s_draw->setTextDatum(textdatum_t::bottom_right); + s_draw->setTextColor(time_color, radar::kColorBackground); + s_draw->drawString(value, right, layout.bottom_value_y); +} +#endif + +void drawCornerTelemetry() { +#ifdef BOARD_NM_TV_154 + constexpr int kEdge = 5; + constexpr int kValueGap = 2; + const int kRight = radar::kSize - kEdge; + + const bool wifi_connected = WiFi.status() == WL_CONNECTED; + + s_draw->setFont(&radar_fonts::Font0); + s_draw->setTextSize(1); + const int label_height = s_draw->fontHeight(); + applyCornerValueStyle(); + const int value_height = s_draw->fontHeight(); + const nm_tv_154::CornerTelemetryLayout layout = + nm_tv_154::cornerTelemetryLayout(radar::kSize, kEdge, label_height, + value_height, kValueGap); + + s_draw->setFont(&radar_fonts::Font0); + s_draw->setTextSize(1); + s_draw->setTextColor(s_corner_muted); + s_draw->setTextDatum(textdatum_t::top_left); + s_draw->drawString("WIFI", kEdge, layout.top_label_y); + + const uint8_t bars = + square::wifiBars(wifi_connected, wifi_connected ? WiFi.RSSI() : -100); + if (wifi_connected) { + constexpr int kBarWidth = 3; + constexpr int kBarGap = 2; + constexpr int kBarHeights[] = {3, 5, 8, 11}; + const int bar_baseline = layout.top_value_y + value_height; + for (uint8_t i = 0; i < 4; ++i) { + const int x = kEdge + i * (kBarWidth + kBarGap); + const int height = kBarHeights[i]; + const uint16_t color = i < bars ? s_corner_green : s_corner_muted; + s_draw->fillRect(x, bar_baseline - height + 1, kBarWidth, height, color); + } + } else { + s_draw->drawLine(kEdge, layout.top_value_y, kEdge + 15, + layout.top_value_y + 13, + s_corner_red); + s_draw->drawLine(kEdge + 15, layout.top_value_y, kEdge, + layout.top_value_y + 13, + s_corner_red); + } + + char value[12]; + applyCornerValueStyle(); + s_draw->setTextDatum(textdatum_t::top_right); + s_draw->setTextColor(s_corner_green); + snprintf(value, sizeof(value), "%u", + static_cast(services::adsb::aircraftCount())); + s_draw->drawString(value, kRight, layout.top_value_y); + + s_draw->setFont(&radar_fonts::Font0); + s_draw->setTextSize(1); + s_draw->setTextColor(s_corner_muted); + s_draw->setTextDatum(textdatum_t::top_right); + s_draw->drawString("AIR", kRight, layout.top_label_y); + s_draw->setTextDatum(textdatum_t::top_left); + s_draw->drawString("RANGE", kEdge, layout.bottom_label_y); + s_draw->setTextDatum(textdatum_t::top_right); + s_draw->drawString("TIME", kRight, layout.bottom_label_y); + + applyCornerValueStyle(); + s_draw->setTextDatum(textdatum_t::bottom_left); + s_draw->setTextColor(s_corner_amber); + radar::formatCurrentRing3Label(value, sizeof(value)); + s_draw->drawString(value, kEdge, layout.bottom_value_y); + + drawCornerTime(layout, value_height, kRight, false); +#endif +} + template void drawStaticGrid(Gfx& gfx) { initLabelMetrics(); @@ -652,11 +807,23 @@ void drawStaticGrid(Gfx& gfx) { runway::drawLargeAirportRunways(gfx); drawCenterDot(cx, cy); drawCardinalLabels(); +#ifndef BOARD_NM_TV_154 drawScaleLabel(cx, cy, grid_r); +#endif gfx.setTextDatum(textdatum_t::top_left); } bool ensureFrameSprite() { +#ifdef BOARD_NM_TV_154 + constexpr bool kFrameSpriteEnabled = + radar::frameSpriteEnabledForBoard(true); +#else + constexpr bool kFrameSpriteEnabled = + radar::frameSpriteEnabledForBoard(false); +#endif + if (!kFrameSpriteEnabled) { + return false; + } if (s_frame_ready) { return true; } @@ -676,6 +843,7 @@ void renderFrame() { drawStaticGrid(s_frame); // opens its own DrawScope(s_frame) { const DrawScope scope(s_frame); + drawCornerTelemetry(); drawAircraft(); } s_frame.pushSprite(0, 0); @@ -696,6 +864,7 @@ void radarDisplayDraw() { // Fallback when the sprite can't be allocated: draw straight to the panel. const DrawScope scope(tft); drawStaticGrid(tft); + drawCornerTelemetry(); drawAircraft(); tft.setTextDatum(textdatum_t::top_left); } @@ -711,4 +880,35 @@ void radarDisplayRefreshAircraft() { radarDisplayDraw(); } +void radarDisplayRefreshStatus() { +#ifdef BOARD_NM_TV_154 + initPalette(); + initLabelMetrics(); + const DrawScope scope(tft); + constexpr int kEdge = 5; + constexpr int kValueGap = 2; + const int right = radar::kSize - kEdge; + + tft.setFont(&radar_fonts::Font0); + tft.setTextSize(1); + const int label_height = tft.fontHeight(); + applyCornerValueStyle(); + const int value_height = tft.fontHeight(); + const nm_tv_154::CornerTelemetryLayout layout = + nm_tv_154::cornerTelemetryLayout(radar::kSize, kEdge, label_height, + value_height, kValueGap); + drawCornerTime(layout, value_height, right, true); + tft.setTextDatum(textdatum_t::top_left); +#endif +} + +void radarDisplayMarkDataUpdated(unsigned long now_ms) { +#ifdef BOARD_NM_TV_154 + s_last_data_update_ms = now_ms; + s_has_data_update = true; +#else + (void)now_ms; +#endif +} + } // namespace ui diff --git a/src/ui/runway_overlay.cpp b/src/ui/runway_overlay.cpp index 8f4b63127..114a131a3 100644 --- a/src/ui/runway_overlay.cpp +++ b/src/ui/runway_overlay.cpp @@ -11,7 +11,7 @@ #include "ui/radar_range.h" #include "ui/radar_theme.h" -namespace fonts = lgfx::v1::fonts; +namespace runway_fonts = lgfx::v1::fonts; namespace ui::runway { namespace { @@ -25,7 +25,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 = &runway_fonts::FreeSansBold12pt7b; int measureVlwHeight(lgfx::LGFXBase& gfx, float size) { gfx.setTextSize(size); @@ -56,7 +56,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 = &runway_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..60dcb9a69 100644 --- a/src/ui/status_screens.cpp +++ b/src/ui/status_screens.cpp @@ -11,7 +11,7 @@ #include "hardware/display.h" #include "hardware/display_font.h" -namespace fonts = lgfx::v1::fonts; +namespace status_fonts = lgfx::v1::fonts; namespace { @@ -38,13 +38,13 @@ float s_spinner_angle_deg = -90.0f; SpinnerDot s_spinner_dots[kSpinnerDotCount]; bool s_connecting_text_drawn = false; -constexpr auto& kGfxTitle = fonts::FreeSans18pt7b; -constexpr auto& kGfxBody = fonts::FreeSans12pt7b; -constexpr auto& kGfxDetail = fonts::Font2; -constexpr auto& kPortalGfxTitle = fonts::FreeSansBold18pt7b; -constexpr auto& kPortalGfxBody = fonts::FreeSansBold12pt7b; -constexpr auto& kPortalGfxEmphasis = fonts::FreeSansBold18pt7b; -constexpr auto& kConnectingGfxDetail = fonts::FreeSans9pt7b; +constexpr auto& kGfxTitle = status_fonts::FreeSans18pt7b; +constexpr auto& kGfxBody = status_fonts::FreeSans12pt7b; +constexpr auto& kGfxDetail = status_fonts::Font2; +constexpr auto& kPortalGfxTitle = status_fonts::FreeSansBold18pt7b; +constexpr auto& kPortalGfxBody = status_fonts::FreeSansBold12pt7b; +constexpr auto& kPortalGfxEmphasis = status_fonts::FreeSansBold18pt7b; +constexpr auto& kConnectingGfxDetail = status_fonts::FreeSans9pt7b; struct TextLine { const char* text;