diff --git a/boards/onx3248g035.json b/boards/onx3248g035.json new file mode 100644 index 000000000..09c0fe7a0 --- /dev/null +++ b/boards/onx3248g035.json @@ -0,0 +1,48 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_opi", + "partitions": "default_16MB.csv" + }, + "core": "esp32", + "extra_flags": [ + "-DARDUINO_ONX3248G035", + "-DBOARD_HAS_PSRAM", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [ + [ + "0x303A", + "0x1001" + ] + ], + "mcu": "esp32s3", + "variant": "esp32_s3r8n16" + }, + "connectivity": [ + "bluetooth", + "wifi" + ], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": [ + "arduino", + "espidf" + ], + "name": "OpenNextion ONX3248G035 (ESP32-S3R8, 16MB Flash)", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "require_upload_port": true, + "speed": 460800 + }, + "url": "https://github.com/OpenNextion", + "vendor": "OpenNextion" +} diff --git a/include/config.h b/include/config.h index 6555c0342..d26bdd5fe 100644 --- a/include/config.h +++ b/include/config.h @@ -6,6 +6,12 @@ namespace config { +#if defined(BOARD_ONX3248G035) +constexpr bool kBoardOnx3248g035 = true; +#else +constexpr bool kBoardOnx3248g035 = false; +#endif + // --- Wi-Fi portal --- constexpr char kPortalApName[] = "PlaneRadar-Setup"; constexpr char kPortalIp[] = "192.168.4.1"; @@ -23,26 +29,46 @@ constexpr unsigned long kWifiDownGraceMs = 4000; /** Minimum interval between background reconnect tries. */ constexpr unsigned long kWifiReconnectIntervalMs = 15000; -// --- BOOT button (ESP32-C3 Super Mini, active LOW) --- -constexpr gpio_num_t kBootPin = GPIO_NUM_9; +// --- User input (active LOW when present) --- +constexpr bool kHasBootButton = true; +constexpr bool kBootTapChangesRange = true; +constexpr gpio_num_t kBootPin = + kBoardOnx3248g035 ? GPIO_NUM_0 : GPIO_NUM_9; 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) --- -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 +// --- Display --- +constexpr gpio_num_t kDisplayPinRst = + kBoardOnx3248g035 ? GPIO_NUM_NC : GPIO_NUM_0; +constexpr gpio_num_t kDisplayPinCs = + kBoardOnx3248g035 ? GPIO_NUM_2 : GPIO_NUM_1; +constexpr gpio_num_t kDisplayPinDc = + kBoardOnx3248g035 ? GPIO_NUM_3 : GPIO_NUM_10; +constexpr gpio_num_t kDisplayPinMosi = + kBoardOnx3248g035 ? GPIO_NUM_1 : GPIO_NUM_3; +constexpr gpio_num_t kDisplayPinSclk = + kBoardOnx3248g035 ? GPIO_NUM_5 : GPIO_NUM_4; +constexpr gpio_num_t kDisplayPinBl = + kBoardOnx3248g035 ? GPIO_NUM_6 : GPIO_NUM_NC; + +constexpr int kDisplayWidth = kBoardOnx3248g035 ? 320 : 240; +constexpr int kDisplayHeight = kBoardOnx3248g035 ? 480 : 240; -constexpr int kDisplayWidth = 240; -constexpr int kDisplayHeight = 240; +constexpr int kRadarViewportSize = kBoardOnx3248g035 ? kDisplayWidth : + (kDisplayWidth < 240 ? kDisplayWidth : 240); +constexpr int kRadarViewportX = (kDisplayWidth - kRadarViewportSize) / 2; +constexpr int kRadarViewportY = + kBoardOnx3248g035 ? 0 : (kDisplayHeight - kRadarViewportSize) / 2; +constexpr bool kFrameSpriteUsePsram = kBoardOnx3248g035; +constexpr bool kRadarInfoPanelEnabled = kBoardOnx3248g035; +constexpr int kRadarInfoPanelY = 330; -constexpr uint32_t kDisplaySpiWriteHz = 40000000; -// GC9A01 modules often need invert + BGR for correct black/green output -constexpr bool kDisplayInvert = true; +constexpr uint32_t kDisplaySpiWriteHz = + kBoardOnx3248g035 ? 40000000 : 40000000; +constexpr bool kDisplayInvert = kBoardOnx3248g035 ? false : true; constexpr bool kDisplayRgbOrder = true; +constexpr uint8_t kDisplayRotation = 0; // --- Radar center defaults (overridden via WiFi setup portal) --- constexpr double kDefaultRadarLat = 52.3676; diff --git a/include/hardware/board_support.h b/include/hardware/board_support.h new file mode 100644 index 000000000..17ca7d4a7 --- /dev/null +++ b/include/hardware/board_support.h @@ -0,0 +1,3 @@ +#pragma once + +void boardInitBeforeDisplay(); diff --git a/include/hardware/display.h b/include/hardware/display.h index 6191f1776..dd46ef849 100644 --- a/include/hardware/display.h +++ b/include/hardware/display.h @@ -5,3 +5,4 @@ extern LGFX tft; void displayInit(); +void displayRadarBackground(); diff --git a/include/hardware/lgfx_config.hpp b/include/hardware/lgfx_config.hpp index 4f1484b6a..27944e4d1 100644 --- a/include/hardware/lgfx_config.hpp +++ b/include/hardware/lgfx_config.hpp @@ -5,10 +5,14 @@ #include "config.h" -/** LovyanGFX device: GC9A01 on SPI. Pin values come from config.h. */ +/** LovyanGFX device. Panel selection and pins come from config.h build flags. */ class LGFX : public lgfx::LGFX_Device { lgfx::Bus_SPI _bus; +#if defined(BOARD_ONX3248G035) + lgfx::Panel_ST7796 _panel; +#else lgfx::Panel_GC9A01 _panel; +#endif public: LGFX() { @@ -27,8 +31,21 @@ class LGFX : public lgfx::LGFX_Device { auto cfg = _panel.config(); cfg.pin_cs = static_cast(config::kDisplayPinCs); cfg.pin_rst = static_cast(config::kDisplayPinRst); + cfg.pin_busy = -1; + cfg.panel_width = config::kDisplayWidth; + cfg.panel_height = config::kDisplayHeight; + cfg.memory_width = config::kDisplayWidth; + cfg.memory_height = config::kDisplayHeight; + cfg.offset_x = 0; + cfg.offset_y = 0; + cfg.offset_rotation = 0; + cfg.dummy_read_pixel = 8; + cfg.dummy_read_bits = 1; + cfg.readable = false; cfg.invert = config::kDisplayInvert; cfg.rgb_order = config::kDisplayRgbOrder; + cfg.dlen_16bit = false; + cfg.bus_shared = false; _panel.config(cfg); } setPanel(&_panel); diff --git a/include/ui/radar_display.h b/include/ui/radar_display.h index 5ab3bb292..e11fd2cc4 100644 --- a/include/ui/radar_display.h +++ b/include/ui/radar_display.h @@ -8,4 +8,7 @@ void radarDisplayDraw(); /** Redraw aircraft only (blits cached grid; no full-screen clear). */ void radarDisplayRefreshAircraft(); +/** Refresh dynamic info panel without refetching ADS-B data. */ +void radarDisplayRefreshInfo(); + } // namespace ui diff --git a/include/ui/radar_theme.h b/include/ui/radar_theme.h index 2d763a638..22b526eb8 100644 --- a/include/ui/radar_theme.h +++ b/include/ui/radar_theme.h @@ -2,65 +2,79 @@ #include +#include "config.h" + namespace ui::radar { -constexpr int kSize = 240; -constexpr int kCenterX = kSize / 2; -constexpr int kCenterY = kSize / 2; +constexpr int kSize = config::kRadarViewportSize; +constexpr int kBaseSize = 240; +constexpr int kOffsetX = config::kRadarViewportX; +constexpr int kOffsetY = config::kRadarViewportY; +constexpr int kRight = kOffsetX + kSize - 1; +constexpr int kBottom = kOffsetY + kSize - 1; +constexpr int kCenterX = kOffsetX + kSize / 2; +constexpr int kCenterY = kOffsetY + kSize / 2; + +// Keep the original 240×240 radar geometry unchanged on existing boards. +// Larger rectangular boards scale these pixel constants from the 240 px baseline. +constexpr int scalePx(int px) { return (px * kSize + kBaseSize / 2) / kBaseSize; } +constexpr float scalePx(float px) { + return px * static_cast(kSize) / static_cast(kBaseSize); +} /** Outermost grid ring (inside edge labels). */ -constexpr int kGridOuterRadius = 107; +constexpr int kGridOuterRadius = scalePx(107); /** N: offset from top edge (top_center, negative = up). */ -constexpr int kCardinalNorthOffsetY = -1; +constexpr int kCardinalNorthOffsetY = scalePx(-1); /** S: offset from bottom edge (bottom_center, positive = down). */ -constexpr int kCardinalSouthOffsetY = 3; +constexpr int kCardinalSouthOffsetY = scalePx(3); /** Gap between scale label right edge and outer ring on the east spoke (px). */ -constexpr int kScaleGapFromOuterRing = 6; +constexpr int kScaleGapFromOuterRing = scalePx(6); /** Target cap height (px) for N/S/E/W. */ -constexpr int kCardinalLabelHeightPx = 14; +constexpr int kCardinalLabelHeightPx = scalePx(14); /** Scale label is this many px shorter than cardinals. */ -constexpr int kScaleBelowCardinalPx = 3; +constexpr int kScaleBelowCardinalPx = scalePx(3); constexpr int kRingCount = 4; /** Shared grid stroke: drawWideLine half-width (~2 px total); rings use the same px count. */ -constexpr float kGridStrokeHalfWidth = 1.0f; +constexpr float kGridStrokeHalfWidth = scalePx(1.0f); -constexpr int kCenterDotRadius = 2; +constexpr int kCenterDotRadius = scalePx(2); /** Filled aircraft symbol (nose triangle). */ -constexpr int kAircraftNoseLenPx = 8; -constexpr int kAircraftTailLenPx = 3; -constexpr int kAircraftTailHalfPx = 4; +constexpr int kAircraftNoseLenPx = scalePx(8); +constexpr int kAircraftTailLenPx = scalePx(3); +constexpr int kAircraftTailHalfPx = scalePx(4); /** 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; +constexpr int kAircraftSpeedLineMinPx = scalePx(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. */ constexpr float kAircraftTrackLengthScale = 1.5f / 5.0f; /** drawWideLine half-width for speed vectors (~2 px total). */ -constexpr float kAircraftTrackLineHalfWidth = 1.0f; +constexpr float kAircraftTrackLineHalfWidth = scalePx(1.0f); -constexpr float kRunwayLineWidthPx = 2.0f; +constexpr float kRunwayLineWidthPx = scalePx(2.0f); constexpr float kRunwayLineHalfWidth = kRunwayLineWidthPx * 0.5f; constexpr int kRunwayLabelHeightPx = kCardinalLabelHeightPx; -constexpr int kRunwayLabelGapPx = 3; +constexpr int kRunwayLabelGapPx = scalePx(3); /** Gap from triangle edge to tag block (px). */ -constexpr int kAircraftLabelGapPx = 1; +constexpr int kAircraftLabelGapPx = scalePx(1); /** Keep symbol centroid inside outer ring by at least this inset (px). */ constexpr int kAircraftInsideRingInsetPx = kAircraftNoseLenPx + kAircraftTailHalfPx + 1; /** Beyond-ring traffic: bearing cues on screen rim (correct direction, fixed radius). */ -constexpr int kBeyondRingDotRadiusPx = 4; -constexpr int kBeyondRingScreenMarginPx = 2; +constexpr int kBeyondRingDotRadiusPx = scalePx(4); +constexpr int kBeyondRingScreenMarginPx = scalePx(2); /** Target cap height (px) for aircraft tags (bold, slightly above scale label). */ -constexpr int kAircraftTagLabelHeightPx = 13; +constexpr int kAircraftTagLabelHeightPx = scalePx(13); /** RGB565 palette targets (applied in initPalette). */ constexpr uint8_t kBgR = 4; diff --git a/platformio.ini b/platformio.ini index c81ce535f..21afc7cc7 100644 --- a/platformio.ini +++ b/platformio.ini @@ -24,3 +24,21 @@ lib_deps = lovyan03/LovyanGFX@^1.2.7 tzapu/WiFiManager@^2.0.17 bblanchon/ArduinoJson@^7.4.2 + +[env:onx3248g035] +platform = espressif32@6.5.0 +board = onx3248g035 +framework = arduino +monitor_speed = 115200 +board_build.partitions = partitions/plane_radar.csv +extra_scripts = post:scripts/merge_firmware.py +board_build.embed_files = data/ui_font.vlw +build_flags = + -std=gnu++17 + -DWM_NODEBUG + -DWM_MDNS + -DBOARD_ONX3248G035 +lib_deps = + lovyan03/LovyanGFX@^1.2.7 + tzapu/WiFiManager@^2.0.17 + bblanchon/ArduinoJson@^7.4.2 diff --git a/src/hardware/board_support.cpp b/src/hardware/board_support.cpp new file mode 100644 index 000000000..c4c9810b6 --- /dev/null +++ b/src/hardware/board_support.cpp @@ -0,0 +1,73 @@ +#include "hardware/board_support.h" + +#include +#include + +#include "config.h" + +namespace { + +#if defined(BOARD_ONX3248G035) + +constexpr uint8_t kPcf8574Addr = 0x38; +constexpr int kPcfExioLcdRst = 6; +constexpr int kPcfExioSdCs = 7; +constexpr int kBoardI2cSda = 8; +constexpr int kBoardI2cScl = 7; + +uint8_t s_pcf_shadow = 0xFF; +bool s_pcf_ready = false; + +bool pcfWrite(uint8_t value) { + Wire.beginTransmission(kPcf8574Addr); + Wire.write(value); + const uint8_t error = Wire.endTransmission(); + if (error != 0) { + Serial.printf("board: PCF8574 write failed (%u)\n", error); + return false; + } + s_pcf_shadow = value; + return true; +} + +bool pcfSetOutput(int bit, bool high) { + const uint8_t mask = static_cast(1u << bit); + const uint8_t next = high ? (s_pcf_shadow | mask) : (s_pcf_shadow & ~mask); + return pcfWrite(next); +} + +void initOnx3248g035IoExpander() { + if (s_pcf_ready) { + return; + } + + Wire.begin(kBoardI2cSda, kBoardI2cScl); + Wire.setClock(400000); + + s_pcf_ready = pcfWrite(0xFF); + if (!s_pcf_ready) { + Serial.println("board: continuing without PCF8574 init"); + return; + } + + pcfSetOutput(kPcfExioSdCs, true); + pcfSetOutput(kPcfExioLcdRst, false); + delay(200); + pcfSetOutput(kPcfExioLcdRst, true); + delay(200); +} + +#endif + +} // namespace + +void boardInitBeforeDisplay() { + if (config::kDisplayPinBl != GPIO_NUM_NC) { + pinMode(config::kDisplayPinBl, OUTPUT); + digitalWrite(config::kDisplayPinBl, LOW); + } + +#if defined(BOARD_ONX3248G035) + initOnx3248g035IoExpander(); +#endif +} diff --git a/src/hardware/display.cpp b/src/hardware/display.cpp index 42f82524d..15f03a1db 100644 --- a/src/hardware/display.cpp +++ b/src/hardware/display.cpp @@ -1,13 +1,23 @@ #include "hardware/display.h" +#include + +#include "config.h" +#include "hardware/board_support.h" #include "hardware/display_font.h" LGFX tft; void displayInit() { + boardInitBeforeDisplay(); tft.init(); - tft.setRotation(0); + tft.setRotation(config::kDisplayRotation); + if (config::kDisplayPinBl != GPIO_NUM_NC) { + digitalWrite(config::kDisplayPinBl, HIGH); + } tft.setBrightness(255); tft.setTextWrap(false); displayFontInit(); } + +void displayRadarBackground() { tft.fillScreen(config::kColorBlack); } diff --git a/src/main.cpp b/src/main.cpp index 797aecc09..91bba96d5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -20,6 +20,7 @@ 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; +unsigned long g_last_info_refresh_ms = 0; void showRadarIfConnected() { if (WiFi.status() != WL_CONNECTED) { @@ -109,9 +110,17 @@ 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 = millis(); + if (now - g_last_adsb_fetch_ms >= config::kAdsbFetchIntervalMs) { + fetchAndDrawAircraft(); + const unsigned long after_fetch_ms = millis(); + g_last_adsb_fetch_ms = after_fetch_ms; + g_last_info_refresh_ms = after_fetch_ms; + } else if (now - g_last_info_refresh_ms >= 1000UL) { + g_last_info_refresh_ms = now; + ui::radarDisplayRefreshInfo(); + } } } diff --git a/src/ui/radar_display.cpp b/src/ui/radar_display.cpp index b0d333997..aed2bf991 100644 --- a/src/ui/radar_display.cpp +++ b/src/ui/radar_display.cpp @@ -2,8 +2,11 @@ #include +#include + #include #include +#include #include #include "config.h" @@ -15,7 +18,7 @@ #include "ui/radar_theme.h" #include "ui/runway_overlay.h" -namespace fonts = lgfx::v1::fonts; +namespace lgfx_fonts = lgfx::v1::fonts; namespace ui { namespace radar { @@ -30,6 +33,7 @@ uint16_t kColorTagType = 0x5DFF; uint16_t kColorTagAltitude = 0xFFE0; uint16_t kColorRunway = 0x4D5F; uint16_t kColorRunwayLabel = 0x7DFF; +uint16_t kColorInfoMuted = 0xC618; } // namespace radar @@ -38,14 +42,18 @@ namespace { bool s_label_metrics_ready = false; bool s_cardinal_use_vlw = false; bool s_scale_use_vlw = false; +bool s_info_use_vlw = false; float s_cardinal_vlw_size = 0.56f; float s_scale_vlw_size = 0.50f; 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; +float s_info_vlw_size = 0.34f; +const lgfx::GFXfont* s_cardinal_gfx = &lgfx_fonts::FreeSansBold12pt7b; +const lgfx::GFXfont* s_scale_gfx = &lgfx_fonts::FreeSansBold9pt7b; +const lgfx::GFXfont* s_tag_gfx = &lgfx_fonts::FreeSansBold12pt7b; +const lgfx::GFXfont* s_info_gfx = &lgfx_fonts::FreeSansBold9pt7b; bool s_tag_label_metrics_ready = false; +bool s_info_metrics_ready = false; bool s_tag_use_vlw = false; int s_scale_label_max_w = 0; @@ -54,6 +62,7 @@ int s_scale_label_h = 0; lgfx::LovyanGFX* s_draw = &tft; LGFX_Sprite s_frame(&tft); bool s_frame_ready = false; +unsigned long s_last_adsb_update_ms = 0; class DrawScope { public: @@ -123,16 +132,16 @@ void initLabelMetrics() { s_scale_use_vlw = true; s_scale_vlw_size = findVlwSizeForHeight(scale_target); } else { - const lgfx::GFXfont* cardinal_candidates[] = {&fonts::FreeSansBold12pt7b, - &fonts::FreeSansBold9pt7b}; + const lgfx::GFXfont* cardinal_candidates[] = {&lgfx_fonts::FreeSansBold12pt7b, + &lgfx_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[] = {&lgfx_fonts::FreeSansBold9pt7b, + &lgfx_fonts::FreeSansBold12pt7b}; s_scale_gfx = pickGfxFontClosest(scale_target, scale_candidates, 2); s_scale_use_vlw = false; } @@ -165,8 +174,8 @@ 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[] = {&lgfx_fonts::FreeSansBold12pt7b, + &lgfx_fonts::FreeSansBold9pt7b}; s_tag_gfx = pickGfxFontClosest(target, tag_candidates, 2); s_tag_use_vlw = false; } @@ -259,7 +268,7 @@ bool beyondRingEdgeDotFromLatLon(float lat, float lon, int* out_x, int* out_y) { const int cx = radar::kCenterX; const int cy = radar::kCenterY; - const int rim_r = radar::kCenterX - radar::kBeyondRingScreenMarginPx; + const int rim_r = radar::kSize / 2 - radar::kBeyondRingScreenMarginPx; const float angle_rad = atan2f(dx_km, dy_km); *out_x = cx + static_cast(lroundf(sinf(angle_rad) * rim_r)); @@ -418,14 +427,14 @@ void drawAircraftTag(int x, int y, const services::adsb::Aircraft& plane) { int anchor_x = 0; if (tag_on_right) { anchor_x = x + symbol_half + radar::kAircraftLabelGapPx; - anchor_x = std::min(anchor_x, radar::kSize - block_w - 1); + anchor_x = std::min(anchor_x, radar::kRight - block_w); s_draw->setTextDatum(textdatum_t::top_left); } else { anchor_x = x - symbol_half - radar::kAircraftLabelGapPx; - anchor_x = std::max(anchor_x, block_w + 1); + anchor_x = std::max(anchor_x, radar::kOffsetX + block_w + 1); s_draw->setTextDatum(textdatum_t::top_right); } - ly = std::max(1, std::min(ly, radar::kSize - block_h - 1)); + ly = std::max(radar::kOffsetY + 1, std::min(ly, radar::kBottom - block_h)); if (plane.callsign[0] != '\0') { s_draw->setTextColor(radar::kColorLabel, radar::kColorBackground); @@ -543,6 +552,193 @@ void drawAircraft() { } } +void initInfoMetrics() { + if (s_info_metrics_ready) { + return; + } + + if (displayFontIsSmooth()) { + s_info_use_vlw = true; + s_info_vlw_size = findVlwSizeForHeight(13); + } else { + const lgfx::GFXfont* info_candidates[] = {&lgfx_fonts::FreeSansBold9pt7b, + &lgfx_fonts::FreeSansBold12pt7b}; + s_info_gfx = pickGfxFontClosest(13, info_candidates, 2); + s_info_use_vlw = false; + } + + s_info_metrics_ready = true; +} + +void applyInfoStyle() { + if (s_info_use_vlw) { + displayFontSetSmoothSize(*s_draw, s_info_vlw_size); + } else { + displayFontSetBitmap(*s_draw, s_info_gfx); + } +} + +const char* bearingLabel(float dx_km, float dy_km) { + constexpr const char* kLabels[] = {"N", "NE", "E", "SE", "S", "SW", "W", "NW"}; + float deg = atan2f(dx_km, dy_km) * 57.2957795f; + if (deg < 0.0f) { + deg += 360.0f; + } + const int sector = static_cast(floorf((deg + 22.5f) / 45.0f)) % 8; + return kLabels[sector]; +} + +struct NearestAircraftInfo { + const services::adsb::Aircraft* aircraft = nullptr; + float km = 0.0f; + const char* bearing = ""; +}; + +size_t findNearestAircraft(NearestAircraftInfo* nearest, size_t max_count) { + const size_t count = services::adsb::aircraftCount(); + const services::adsb::Aircraft* planes = services::adsb::aircraftList(); + size_t found = 0; + + for (size_t i = 0; i < count; ++i) { + float dx_km = 0.0f; + float dy_km = 0.0f; + float dist_km = 0.0f; + offsetKmFromCenter(planes[i].lat, planes[i].lon, &dx_km, &dy_km, &dist_km); + + if (found == max_count && dist_km >= nearest[found - 1].km) { + continue; + } + + const size_t limit = (found < max_count) ? found : max_count - 1; + size_t insert_at = limit; + while (insert_at > 0 && dist_km < nearest[insert_at - 1].km) { + nearest[insert_at] = nearest[insert_at - 1]; + --insert_at; + } + + nearest[insert_at].aircraft = &planes[i]; + nearest[insert_at].km = dist_km; + nearest[insert_at].bearing = bearingLabel(dx_km, dy_km); + if (found < max_count) { + ++found; + } + } + + return found; +} + +void formatDistance(char* out, size_t out_len, float km) { + if (radar::useMiles()) { + snprintf(out, out_len, "%.1fmi", km / 1.609344f); + return; + } + snprintf(out, out_len, "%.1fkm", km); +} + +void drawInfoPanel() { + if (!config::kRadarInfoPanelEnabled) { + return; + } + + initInfoMetrics(); + applyInfoStyle(); + + constexpr int card_w = 280; + constexpr int card_h = 126; + const int card_x = (config::kDisplayWidth - card_w) / 2; + const int card_y = config::kRadarInfoPanelY; + constexpr int pad_x = 12; + constexpr int pad_y = 9; + constexpr int label_gap = 14; + const int line_h = s_draw->fontHeight() + 4; + const int aircraft_line_h = line_h + 2; + + s_draw->fillRoundRect(card_x, card_y, card_w, card_h, 8, + radar::kColorBackground); + s_draw->drawRoundRect(card_x, card_y, card_w, card_h, 8, + radar::kColorGrid); + + char range_label[12]; + radar::formatCurrentRing3Label(range_label, sizeof(range_label)); + + char updated_label[16]; + if (s_last_adsb_update_ms == 0) { + snprintf(updated_label, sizeof(updated_label), "--"); + } else { + unsigned long age_s = (millis() - s_last_adsb_update_ms) / 1000UL; + if (age_s == 0) { + age_s = 1; + } + snprintf(updated_label, sizeof(updated_label), "%lus", age_s); + } + + const int col_w = (card_w - pad_x * 2) / 3; + const int col_y = card_y + pad_y; + const int value_y = col_y + label_gap; + const int col0 = card_x + pad_x + col_w / 2; + const int col1 = col0 + col_w; + const int col2 = col1 + col_w; + + s_draw->setTextDatum(textdatum_t::top_center); + s_draw->setTextColor(radar::kColorGrid, radar::kColorBackground); + s_draw->drawString("RANGE", col0, col_y); + s_draw->drawString("ACFT", col1, col_y); + s_draw->drawString("AGE", col2, col_y); + + s_draw->setTextColor(radar::kColorLabel, radar::kColorBackground); + s_draw->drawString(range_label, col0, value_y); + char aircraft_label[8]; + snprintf(aircraft_label, sizeof(aircraft_label), "%u", + static_cast(services::adsb::aircraftCount())); + s_draw->setTextColor(radar::kColorAircraft, radar::kColorBackground); + s_draw->drawString(aircraft_label, col1, value_y); + s_draw->setTextColor(radar::kColorInfoMuted, radar::kColorBackground); + s_draw->drawString(updated_label, col2, value_y); + + const int text_x = card_x + pad_x; + int y = card_y + 50; + char line[96]; + s_draw->setTextDatum(textdatum_t::top_left); + s_draw->setTextColor(radar::kColorGrid, radar::kColorBackground); + s_draw->drawString("NEAREST", text_x, y); + + y += line_h; + NearestAircraftInfo nearest[3]; + const size_t nearest_count = findNearestAircraft(nearest, 3); + if (nearest_count == 0) { + s_draw->setTextColor(radar::kColorLabel, radar::kColorBackground); + s_draw->drawString("none", text_x, y); + return; + } + + for (size_t i = 0; i < nearest_count; ++i) { + const services::adsb::Aircraft* aircraft = nearest[i].aircraft; + char dist_label[16]; + formatDistance(dist_label, sizeof(dist_label), nearest[i].km); + const char* ident = aircraft->callsign[0] != '\0' ? aircraft->callsign : "unknown"; + const char* type = aircraft->type[0] != '\0' ? aircraft->type : "--"; + const char* alt = aircraft->alt[0] != '\0' ? aircraft->alt : "--"; + int cursor_x = text_x; + + s_draw->setTextColor(radar::kColorLabel, radar::kColorBackground); + s_draw->drawString(ident, cursor_x, y); + cursor_x += s_draw->textWidth(ident) + 8; + + s_draw->setTextColor(radar::kColorTagType, radar::kColorBackground); + s_draw->drawString(type, cursor_x, y); + cursor_x += s_draw->textWidth(type) + 8; + + snprintf(line, sizeof(line), "%s %s", dist_label, nearest[i].bearing); + s_draw->setTextColor(radar::kColorLabel, radar::kColorBackground); + s_draw->drawString(line, cursor_x, y); + cursor_x += s_draw->textWidth(line) + 8; + + s_draw->setTextColor(radar::kColorTagAltitude, radar::kColorBackground); + s_draw->drawString(alt, cursor_x, y); + y += aircraft_line_h; + } +} + void applyCardinalStyle() { if (s_cardinal_use_vlw) { displayFontSetSmoothSize(*s_draw, s_cardinal_vlw_size); @@ -616,13 +812,12 @@ void drawCenterDot(int cx, int cy) { void drawCardinalLabels() { const int cx = radar::kCenterX; const int cy = radar::kCenterY; - const int edge = radar::kSize - 1; - - drawCardinalLabel("N", cx, radar::kCardinalNorthOffsetY, textdatum_t::top_center); - drawCardinalLabel("S", cx, edge + radar::kCardinalSouthOffsetY, + drawCardinalLabel("N", cx, radar::kOffsetY + radar::kCardinalNorthOffsetY, + textdatum_t::top_center); + drawCardinalLabel("S", cx, radar::kBottom + radar::kCardinalSouthOffsetY, textdatum_t::bottom_center); - drawCardinalLabel("W", 0, cy, textdatum_t::middle_left); - drawCardinalLabel("E", edge, cy, textdatum_t::middle_right); + drawCardinalLabel("W", radar::kOffsetX, cy, textdatum_t::middle_left); + drawCardinalLabel("E", radar::kRight, cy, textdatum_t::middle_right); } int scaleLabelAnchorX(int cx, int outer_radius) { @@ -661,7 +856,8 @@ bool ensureFrameSprite() { return true; } s_frame.setColorDepth(16); - if (!s_frame.createSprite(radar::kSize, radar::kSize)) { + s_frame.setPsram(config::kFrameSpriteUsePsram); + if (!s_frame.createSprite(config::kDisplayWidth, config::kDisplayHeight)) { Serial.println("radar: frame sprite alloc failed"); return false; } @@ -677,6 +873,7 @@ void renderFrame() { { const DrawScope scope(s_frame); drawAircraft(); + drawInfoPanel(); } s_frame.pushSprite(0, 0); tft.setTextDatum(textdatum_t::top_left); @@ -687,6 +884,7 @@ void renderFrame() { void radarDisplayDraw() { initPalette(); initLabelMetrics(); + displayRadarBackground(); if (ensureFrameSprite()) { renderFrame(); @@ -697,11 +895,13 @@ void radarDisplayDraw() { const DrawScope scope(tft); drawStaticGrid(tft); drawAircraft(); + drawInfoPanel(); tft.setTextDatum(textdatum_t::top_left); } void radarDisplayRefreshAircraft() { initPalette(); + s_last_adsb_update_ms = millis(); if (ensureFrameSprite()) { renderFrame(); @@ -711,4 +911,23 @@ void radarDisplayRefreshAircraft() { radarDisplayDraw(); } +void radarDisplayRefreshInfo() { + if (!config::kRadarInfoPanelEnabled) { + return; + } + + initPalette(); + if (ensureFrameSprite()) { + const DrawScope scope(s_frame); + drawInfoPanel(); + s_frame.pushSprite(0, 0); + tft.setTextDatum(textdatum_t::top_left); + return; + } + + const DrawScope scope(tft); + drawInfoPanel(); + tft.setTextDatum(textdatum_t::top_left); +} + } // namespace ui diff --git a/src/ui/radar_range.cpp b/src/ui/radar_range.cpp index d8bf002e2..bb12c5735 100644 --- a/src/ui/radar_range.cpp +++ b/src/ui/radar_range.cpp @@ -85,7 +85,7 @@ uint8_t rangeIndex() { return s_range_index; } float fetchRadiusKm() { const float outer_km = rangeCurrent().outer_km; const float screen_r_px = - static_cast(kCenterX - kBeyondRingScreenMarginPx); + static_cast(kSize / 2 - kBeyondRingScreenMarginPx); return outer_km * (screen_r_px / static_cast(kGridOuterRadius)); } diff --git a/src/ui/runway_overlay.cpp b/src/ui/runway_overlay.cpp index 8f4b63127..5546d4405 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 lgfx_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 = &lgfx_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 = &lgfx_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..de16c714b 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 lgfx_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 = lgfx_fonts::FreeSans18pt7b; +constexpr auto& kGfxBody = lgfx_fonts::FreeSans12pt7b; +constexpr auto& kGfxDetail = lgfx_fonts::Font2; +constexpr auto& kPortalGfxTitle = lgfx_fonts::FreeSansBold18pt7b; +constexpr auto& kPortalGfxBody = lgfx_fonts::FreeSansBold12pt7b; +constexpr auto& kPortalGfxEmphasis = lgfx_fonts::FreeSansBold18pt7b; +constexpr auto& kConnectingGfxDetail = lgfx_fonts::FreeSans9pt7b; struct TextLine { const char* text;