From dcd8a03c5b419d15eaf435b10e2f86864e843d07 Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Fri, 20 Feb 2026 22:40:56 +0100 Subject: [PATCH] Add Station G2 --- zephcore/CMakeLists.txt | 26 +++- zephcore/boards/common/repeater.conf | 26 +++- zephcore/boards/esp32/station_g2/Kconfig | 3 + .../esp32/station_g2/Kconfig.station_g2 | 3 + zephcore/boards/esp32/station_g2/board.cmake | 7 + zephcore/boards/esp32/station_g2/board.conf | 21 +++ .../boards/esp32/station_g2/board.overlay | 119 +++++++++++++++++ zephcore/boards/esp32/station_g2/board.yml | 6 + .../esp32/station_g2/station_g2-pinctrl.dtsi | 51 ++++++++ .../esp32/station_g2/station_g2_procpu.dts | 89 +++++++++++++ .../station_g2/station_g2_procpu_defconfig | 12 ++ zephcore/helpers/ui/ui_mesh_actions_stubs.c | 25 ++++ zephcore/helpers/ui/ui_pages.c | 120 +++++++++++++++--- zephcore/helpers/ui/ui_pages.h | 1 + zephcore/helpers/ui/ui_task.c | 20 ++- zephcore/src/main_repeater.cpp | 45 ++++++- 16 files changed, 542 insertions(+), 32 deletions(-) create mode 100644 zephcore/boards/esp32/station_g2/Kconfig create mode 100644 zephcore/boards/esp32/station_g2/Kconfig.station_g2 create mode 100644 zephcore/boards/esp32/station_g2/board.cmake create mode 100644 zephcore/boards/esp32/station_g2/board.conf create mode 100644 zephcore/boards/esp32/station_g2/board.overlay create mode 100644 zephcore/boards/esp32/station_g2/board.yml create mode 100644 zephcore/boards/esp32/station_g2/station_g2-pinctrl.dtsi create mode 100644 zephcore/boards/esp32/station_g2/station_g2_procpu.dts create mode 100644 zephcore/boards/esp32/station_g2/station_g2_procpu_defconfig create mode 100644 zephcore/helpers/ui/ui_mesh_actions_stubs.c diff --git a/zephcore/CMakeLists.txt b/zephcore/CMakeLists.txt index 15edc21..a9df1ee 100644 --- a/zephcore/CMakeLists.txt +++ b/zephcore/CMakeLists.txt @@ -84,12 +84,18 @@ if(BOARD MATCHES ".*nrf52.*" OR BOARD MATCHES "rak4631" OR BOARD MATCHES "wio_tr set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/nrf52_common.conf") elseif(DEFINED BOARD_QUALIFIERS AND BOARD_QUALIFIERS MATCHES ".*nrf52.*") set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/nrf52_common.conf") -elseif(BOARD MATCHES ".*esp32.*") +elseif(BOARD MATCHES ".*esp32.*" OR BOARD MATCHES "station_g2") + set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/esp32_common.conf") +elseif(DEFINED BOARD_QUALIFIERS AND BOARD_QUALIFIERS MATCHES ".*esp32.*") set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/esp32_common.conf") elseif(BOARD MATCHES ".*nrf54l.*") set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/nrf54l_common.conf") +elseif(DEFINED BOARD_QUALIFIERS AND BOARD_QUALIFIERS MATCHES ".*nrf54l.*") + set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/nrf54l_common.conf") elseif(BOARD MATCHES ".*mg24.*" OR BOARD MATCHES ".*efr32.*") set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/mg24_common.conf") +elseif(DEFINED BOARD_QUALIFIERS AND BOARD_QUALIFIERS MATCHES ".*mg24.*") + set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/mg24_common.conf") else() set(ZEPHCORE_PLATFORM_CONF "") endif() @@ -137,11 +143,12 @@ else() endif() endif() -# Append to EXTRA_CONF_FILE (preserves any user-specified extras) -# Note: repeater.conf is passed via EXTRA_CONF_FILE on the command line when building repeater role +# Append to EXTRA_CONF_FILE — auto-generated configs first, user extras last. +# User-specified extras (repeater.conf, prod.conf) MUST come after the auto chain +# so they can override settings (e.g. CONFIG_BT=n in repeater.conf). if(ZEPHCORE_CONF_FILES) if(EXTRA_CONF_FILE) - set(EXTRA_CONF_FILE "${EXTRA_CONF_FILE};${ZEPHCORE_CONF_FILES}" CACHE STRING "" FORCE) + set(EXTRA_CONF_FILE "${ZEPHCORE_CONF_FILES};${EXTRA_CONF_FILE}" CACHE STRING "" FORCE) else() set(EXTRA_CONF_FILE "${ZEPHCORE_CONF_FILES}" CACHE STRING "" FORCE) endif() @@ -260,8 +267,9 @@ if(CONFIG_ZEPHCORE_ROLE_REPEATER) helpers/TransportKeyStore.cpp helpers/CommonCLI.cpp ) - # USB CDC with 1200 baud touch detection (when not using auto-init) - if(NOT CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT) + # USB CDC with 1200 baud touch detection (nRF boards with USB CDC ACM only). + # ESP32 boards use usb_serial (no UDC/CDC ACM), so skip custom USB init. + if(NOT CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT AND (CONFIG_USB_CDC_ACM OR CONFIG_USBD_CDC_ACM_CLASS)) target_sources(app PRIVATE adapters/usb/ZephyrRepeaterUSB.cpp ) @@ -321,6 +329,12 @@ if(CONFIG_ZEPHCORE_UI_BUTTONS OR CONFIG_ZEPHCORE_UI_BUZZER OR CONFIG_ZEPHCORE_UI target_include_directories(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/helpers/ui ) + # Repeater builds need weak stubs for companion-only UI mesh actions + if(CONFIG_ZEPHCORE_ROLE_REPEATER) + target_sources(app PRIVATE + helpers/ui/ui_mesh_actions_stubs.c + ) + endif() endif() if(CONFIG_ZEPHCORE_EASTER_EGG_DOOM) diff --git a/zephcore/boards/common/repeater.conf b/zephcore/boards/common/repeater.conf index 42fe28b..e31612e 100644 --- a/zephcore/boards/common/repeater.conf +++ b/zephcore/boards/common/repeater.conf @@ -2,6 +2,28 @@ # # Build with: -DEXTRA_CONF_FILE="boards/common/repeater.conf" # -# Disable Zephyr's auto USB init - we do it ourselves with 1200 baud touch detection -# for entering UF2 bootloader mode (like Arduino's behavior). +# Repeaters use USB serial CLI for configuration — no BLE, no bonding, no NUS. +# This overrides the BLE settings in zephcore_common.conf. + +# ========== Role Selection ========== +CONFIG_ZEPHCORE_ROLE_REPEATER=y + +# ========== Disable BLE entirely ========== +# Repeaters have zero BLE functionality — all config via USB serial CLI. +# Saves ~100KB flash and ~30KB RAM on nRF52. +CONFIG_BT=n + +# ========== Entropy (required when BT is disabled) ========== +# BT normally pulls in the entropy subsystem. Without BT, we need it explicitly +# for Ed25519 key generation, LoRa random delays, etc. +CONFIG_ENTROPY_GENERATOR=y + +# ========== Display ========== +# Fast sleep — repeater display is rarely needed, save power. +# Only user button wakes it (no LoRa RX wake, no flashing). +CONFIG_ZEPHCORE_UI_DISPLAY_AUTO_OFF_MS=5000 + +# ========== USB CDC ========== +# Custom USB init with 1200 baud touch detection for UF2 bootloader (nRF52 only). +# ESP32 boards use usb_serial — this setting is ignored there. CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT=n diff --git a/zephcore/boards/esp32/station_g2/Kconfig b/zephcore/boards/esp32/station_g2/Kconfig new file mode 100644 index 0000000..959ef82 --- /dev/null +++ b/zephcore/boards/esp32/station_g2/Kconfig @@ -0,0 +1,3 @@ +config HEAP_MEM_POOL_ADD_SIZE_BOARD + int + default 4096 diff --git a/zephcore/boards/esp32/station_g2/Kconfig.station_g2 b/zephcore/boards/esp32/station_g2/Kconfig.station_g2 new file mode 100644 index 0000000..257b71b --- /dev/null +++ b/zephcore/boards/esp32/station_g2/Kconfig.station_g2 @@ -0,0 +1,3 @@ +config BOARD_STATION_G2 + select SOC_ESP32S3_WROOM_N16R8 + select SOC_ESP32S3_PROCPU if BOARD_STATION_G2_ESP32S3_PROCPU diff --git a/zephcore/boards/esp32/station_g2/board.cmake b/zephcore/boards/esp32/station_g2/board.cmake new file mode 100644 index 0000000..f3c3ac1 --- /dev/null +++ b/zephcore/boards/esp32/station_g2/board.cmake @@ -0,0 +1,7 @@ +if(NOT "${OPENOCD}" MATCHES "^${ESPRESSIF_TOOLCHAIN_PATH}/.*") + set(OPENOCD OPENOCD-NOTFOUND) +endif() +find_program(OPENOCD openocd PATHS ${ESPRESSIF_TOOLCHAIN_PATH}/openocd-esp32/bin NO_DEFAULT_PATH) + +include(${ZEPHYR_BASE}/boards/common/esp32.board.cmake) +include(${ZEPHYR_BASE}/boards/common/openocd.board.cmake) diff --git a/zephcore/boards/esp32/station_g2/board.conf b/zephcore/boards/esp32/station_g2/board.conf new file mode 100644 index 0000000..871741f --- /dev/null +++ b/zephcore/boards/esp32/station_g2/board.conf @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: Apache-2.0 +# BQ Station G2 — ZephCore board configuration +# +# Hardware: +# ESP32-S3 (16MB flash, 8MB PSRAM) +# SX1262 + PA on SPI2 (TX power 7 dBm → ~27 dBm with PA) +# SH1106 128x64 OLED on I2C0 +# GPS on UART1 +# User button on GPIO38 +# +# Include order: prj.conf → zephcore_common.conf → esp32_common.conf → board.conf + +# Board identification +CONFIG_ZEPHCORE_BOARD_NAME="Station G2" +CONFIG_BT_DIS_MODEL_NUMBER_STR="BQ Station G2" + +# Radio type — SX1262 via Zephyr native LoRa driver +CONFIG_ZEPHCORE_RADIO_NATIVE=y + +# Flash size — 16MB (override Zephyr default) +CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y diff --git a/zephcore/boards/esp32/station_g2/board.overlay b/zephcore/boards/esp32/station_g2/board.overlay new file mode 100644 index 0000000..d248836 --- /dev/null +++ b/zephcore/boards/esp32/station_g2/board.overlay @@ -0,0 +1,119 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * BQ Station G2 — ZephCore overlay + * + * Adds LoRa radio, display, GPS, button, and filesystem on top of the + * base board DTS. Pin mapping from Arduino MeshCore variants/station_g2. + * + * LoRa SPI2 pins: SCLK=12, MOSI=13, MISO=14, NSS=11 + * LoRa control: DIO1=48, RESET=21, BUSY=47 + * Display I2C0: SDA=5, SCL=6 (SH1106 @ 0x3C) + * GPS UART1: TX=7, RX=15 + * Button: GPIO38 + */ + +/ { + aliases { + lora0 = &lora; + sw0 = &user_button; + }; + + chosen { + zephyr,settings-partition = &storage_partition; + zephyr,display = &oled; + }; + + buttons: buttons { + compatible = "gpio-keys"; + user_button: user_button { + gpios = <&gpio1 6 GPIO_ACTIVE_HIGH>; /* GPIO38 = gpio1 pin 6 */ + label = "User Button"; + zephyr,code = ; + }; + }; +}; + +/* SX1262 + PA on SPI2 */ +&spi2 { + cs-gpios = <&gpio0 11 GPIO_ACTIVE_LOW>; /* NSS = GPIO11 */ + + lora: lora@0 { + compatible = "semtech,sx1262"; + reg = <0>; + spi-max-frequency = <8000000>; + + reset-gpios = <&gpio0 21 GPIO_ACTIVE_LOW>; /* RESET = GPIO21 */ + busy-gpios = <&gpio1 15 GPIO_ACTIVE_HIGH>; /* BUSY = GPIO47 = gpio1 pin 15 */ + dio1-gpios = <&gpio1 16 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>; /* DIO1 = GPIO48 = gpio1 pin 16 */ + + /* DIO2 controls RF switch for TX */ + dio2-tx-enable; + + /* TCXO 1.8 V via DIO3 (value 2 = 1.8 V in Zephyr SX126x driver) */ + dio3-tcxo-voltage = <2>; + tcxo-power-startup-delay-ms = <10>; + + /* + * RX boosted mode DISABLED — Station G2 wiki warns against it: + * "Impact of LoRa node dense areas/high noise environments on RF performance" + * https://wiki.uniteng.com/en/meshtastic/station-g2 + */ + }; +}; + +/* SH1106 128x64 OLED on I2C0 */ +&i2c0 { + oled: sh1106@3c { + compatible = "sinowealth,sh1106"; + reg = <0x3c>; + width = <128>; + height = <64>; + segment-offset = <2>; + page-offset = <0>; + display-offset = <0>; + multiplex-ratio = <63>; + prechargep = <0x22>; + segment-remap; + com-invdir; + }; + + #include "../../common/sensors-i2c.dtsi" +}; + +/* GPS on UART1 (already enabled in DTS with 9600 baud) */ +&uart1 { + gnss: gnss-nmea-generic { + compatible = "gnss-nmea-generic"; + }; +}; + +/* + * Flash partitions — repartition default ESP32-S3 16 MB layout. + * Default storage_partition is 192 KB at 0xFB0000. + * Delete unused partitions (lpcore, scratch, coredump) to reclaim space. + * Split into: NVS 8 KB (BLE bonds) + LittleFS 376 KB (DataStore). + */ +/delete-node/ &slot0_lpcore_partition; +/delete-node/ &slot1_lpcore_partition; +/delete-node/ &storage_partition; +/delete-node/ &scratch_partition; +/delete-node/ &coredump_partition; + +&flash0 { + partitions { + /* NVS 8 KB for BLE bonds + settings */ + storage_partition: partition@fa0000 { + label = "storage"; + reg = <0xFA0000 0x2000>; + }; + + /* LittleFS 376 KB for DataStore (identity, contacts, channels, etc.) */ + lfs_partition: partition@fa2000 { + label = "lfs"; + reg = <0xFA2000 0x5E000>; + }; + }; +}; + +/* LittleFS auto-mount — standard /lfs mount point */ +#include "../../common/filesystem.dtsi" diff --git a/zephcore/boards/esp32/station_g2/board.yml b/zephcore/boards/esp32/station_g2/board.yml new file mode 100644 index 0000000..3a6db9d --- /dev/null +++ b/zephcore/boards/esp32/station_g2/board.yml @@ -0,0 +1,6 @@ +board: + name: station_g2 + full_name: BQ Station G2 + vendor: bq + socs: + - name: esp32s3 diff --git a/zephcore/boards/esp32/station_g2/station_g2-pinctrl.dtsi b/zephcore/boards/esp32/station_g2/station_g2-pinctrl.dtsi new file mode 100644 index 0000000..7b5c9cd --- /dev/null +++ b/zephcore/boards/esp32/station_g2/station_g2-pinctrl.dtsi @@ -0,0 +1,51 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * BQ Station G2 — Pin Control Definitions + * + * Pin mapping (from Arduino MeshCore variants/station_g2): + * LoRa SPI2: SCLK=GPIO12, MOSI=GPIO13, MISO=GPIO14, NSS=GPIO11 + * I2C0: SDA=GPIO5, SCL=GPIO6 (SH1106 OLED + sensors) + * UART1: TX=GPIO7, RX=GPIO15 (GPS) + * Console: USB Serial (native ESP32-S3 USB) + */ + +#include +#include +#include + +&pinctrl { + /* SPI2 — LoRa SX1262 (CS handled via cs-gpios, not pinctrl) */ + spim2_default: spim2_default { + group1 { + pinmux = , + ; + }; + group2 { + pinmux = ; + output-low; + }; + }; + + /* I2C0 — SH1106 OLED display + optional I2C sensors */ + i2c0_default: i2c0_default { + group1 { + pinmux = , + ; + bias-pull-up; + drive-open-drain; + output-high; + }; + }; + + /* UART1 — GPS module */ + uart1_default: uart1_default { + group1 { + pinmux = ; + output-high; + }; + group2 { + pinmux = ; + bias-pull-up; + }; + }; +}; diff --git a/zephcore/boards/esp32/station_g2/station_g2_procpu.dts b/zephcore/boards/esp32/station_g2/station_g2_procpu.dts new file mode 100644 index 0000000..f5a8090 --- /dev/null +++ b/zephcore/boards/esp32/station_g2/station_g2_procpu.dts @@ -0,0 +1,89 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * BQ Station G2 — ESP32-S3, 16MB flash, 8MB PSRAM + * + * Hardware: + * MCU: ESP32-S3 (Xtensa LX7, dual-core @ 80 MHz) + * Flash: 16 MB (QIO) + * PSRAM: 8 MB (OPI) + * Radio: SX1262 + PA (~27 dBm output) on SPI2 + * Display: SH1106 128x64 OLED on I2C0 + * GPS: NMEA on UART1 + * Button: GPIO38 + * Console: Native USB Serial + */ + +/dts-v1/; + +#include +#include "station_g2-pinctrl.dtsi" +#include +#include + +/ { + model = "BQ Station G2"; + compatible = "bq,station-g2"; + + chosen { + zephyr,sram = &sram1; + zephyr,console = &usb_serial; + zephyr,shell-uart = &usb_serial; + zephyr,flash = &flash0; + zephyr,code-partition = &slot0_partition; + zephyr,bt-hci = &esp32_bt_hci; + }; + + aliases { + uart-0 = &uart0; + i2c-0 = &i2c0; + watchdog0 = &wdt0; + }; +}; + +/* Reduce CPU clock from 240 MHz to 80 MHz — plenty for mesh node, saves power */ +&cpu0 { + clock-frequency = ; +}; + +&cpu1 { + clock-frequency = ; +}; + +&usb_serial { + status = "okay"; +}; + +&i2c0 { + status = "okay"; + clock-frequency = ; + pinctrl-0 = <&i2c0_default>; + pinctrl-names = "default"; +}; + +&spi2 { + status = "okay"; + #address-cells = <1>; + #size-cells = <0>; + pinctrl-0 = <&spim2_default>; + pinctrl-names = "default"; +}; + +&uart1 { + status = "okay"; + current-speed = <9600>; + pinctrl-0 = <&uart1_default>; + pinctrl-names = "default"; +}; + +&gpio0 { status = "okay"; }; +&gpio1 { status = "okay"; }; +&wdt0 { status = "okay"; }; +&trng0 { status = "okay"; }; + +&esp32_bt_hci { + status = "okay"; +}; + +&wifi { + status = "okay"; +}; diff --git a/zephcore/boards/esp32/station_g2/station_g2_procpu_defconfig b/zephcore/boards/esp32/station_g2/station_g2_procpu_defconfig new file mode 100644 index 0000000..dbbe208 --- /dev/null +++ b/zephcore/boards/esp32/station_g2/station_g2_procpu_defconfig @@ -0,0 +1,12 @@ +# BQ Station G2 — minimal Zephyr defconfig (procpu) +CONFIG_CONSOLE=y +CONFIG_SERIAL=y +CONFIG_UART_CONSOLE=y +CONFIG_GPIO=y +CONFIG_CLOCK_CONTROL=y +CONFIG_PINCTRL=y +CONFIG_SPI=y +CONFIG_I2C=y +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y +CONFIG_NVS=y diff --git a/zephcore/helpers/ui/ui_mesh_actions_stubs.c b/zephcore/helpers/ui/ui_mesh_actions_stubs.c new file mode 100644 index 0000000..6746e20 --- /dev/null +++ b/zephcore/helpers/ui/ui_mesh_actions_stubs.c @@ -0,0 +1,25 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Weak stubs for companion-only UI mesh actions. + * + * Repeater builds with a display compile ui_task.c which references these + * functions. The real implementations live in ui_mesh_actions.cpp and are + * linked only for companion builds. These weak stubs allow repeater+display + * builds to link cleanly — button actions that require BLE/GPS/companion + * features simply do nothing. + */ + +#include +#include +#include + +__attribute__((weak)) void mesh_send_flood_advert(void) {} +__attribute__((weak)) void mesh_send_zerohop_advert(void) {} +__attribute__((weak)) void mesh_gps_set_enabled(bool enable) { ARG_UNUSED(enable); } +__attribute__((weak)) void mesh_ble_set_enabled(bool enable) { ARG_UNUSED(enable); } +__attribute__((weak)) void mesh_set_buzzer_quiet(bool quiet) { ARG_UNUSED(quiet); } +__attribute__((weak)) void mesh_set_offgrid_mode(bool enable) { ARG_UNUSED(enable); } +__attribute__((weak)) void mesh_disable_power_regulators(void) {} +__attribute__((weak)) void mesh_reboot_to_ota_dfu(void) {} +__attribute__((weak)) void mesh_handle_ui_actions(void) {} +__attribute__((weak)) void mesh_housekeeping_ui_refresh(void) {} diff --git a/zephcore/helpers/ui/ui_pages.c b/zephcore/helpers/ui/ui_pages.c index 621e321..327bf47 100644 --- a/zephcore/helpers/ui/ui_pages.c +++ b/zephcore/helpers/ui/ui_pages.c @@ -145,6 +145,38 @@ void utf8_to_latin1(char *dst, const char *src, size_t dst_size) /* ========== Global State ========== */ static struct ui_state state; +/* ========== Active Pages (role-dependent) ========== */ +/* Repeater: minimal pages — status, radio, shutdown. + * Companion: full page set matching Arduino UI. + * Navigation walks this array instead of the raw enum. */ + +#ifdef ZEPHCORE_REPEATER +static const enum ui_page active_pages[] = { + UI_PAGE_STATUS, + UI_PAGE_RADIO, + UI_PAGE_SHUTDOWN, +}; +#else +static const enum ui_page active_pages[] = { + UI_PAGE_MESSAGES, + UI_PAGE_RECENT, + UI_PAGE_RADIO, + UI_PAGE_BLUETOOTH, + UI_PAGE_ADVERT, + UI_PAGE_GPS, + UI_PAGE_BUZZER, + UI_PAGE_SENSORS, + UI_PAGE_OFFGRID, + UI_PAGE_DFU, + UI_PAGE_SHUTDOWN, +}; +#endif + +#define ACTIVE_PAGE_COUNT ((int)(sizeof(active_pages) / sizeof(active_pages[0]))) + +/* Current index into active_pages[] */ +static int current_page_idx; + /* ========== Helper: Battery Percentage from mV ========== */ static uint8_t calc_battery_pct(uint16_t mv) @@ -213,7 +245,7 @@ static void render_page_indicator(void) /* Centered dots matching Arduino layout: * Each dot spaced 10px apart, centered on screen. * Current page = filled 3x3 block, others = single pixel. */ - int total = UI_PAGE_COUNT; + int total = ACTIVE_PAGE_COUNT; int dot_spacing = 10; int total_width = (total - 1) * dot_spacing; int start_x = (DISP_W - total_width) / 2; @@ -221,7 +253,7 @@ static void render_page_indicator(void) for (int i = 0; i < total; i++) { int x = start_x + i * dot_spacing; - if (i == (int)state.current_page) { + if (i == current_page_idx) { /* Current page: filled 3x3 block */ mc_display_fill_rect(x - 1, DOTS_Y, 3, 3); } else { @@ -582,6 +614,51 @@ static void render_shutdown(void) } } +static void render_status(void) +{ + char buf[28]; + int y = CONTENT_Y; + + /* Role label */ + draw_centered(y, "REPEATER"); + y += LINE_H; + + /* Uptime */ + uint32_t up_s = (uint32_t)(k_uptime_get() / 1000); + uint32_t days = up_s / 86400; + uint32_t hours = (up_s % 86400) / 3600; + uint32_t mins = (up_s % 3600) / 60; + + snprintf(buf, sizeof(buf), "Up: %ud %uh %um", days, hours, mins); + mc_display_text(0, y, buf, false); + y += LINE_H; + + /* Clock — only if RTC has been synced (after Jan 1 2025) */ + if (state.rtc_epoch > 1735689600) { + uint32_t day_sec = state.rtc_epoch % 86400; + uint8_t hh = day_sec / 3600; + uint8_t mm = (day_sec % 3600) / 60; + uint8_t ss = day_sec % 60; + + snprintf(buf, sizeof(buf), "Time: %02u:%02u:%02u UTC", hh, mm, ss); + mc_display_text(0, y, buf, false); + } else { + mc_display_text(0, y, "Time: not synced", false); + } + y += LINE_H; + + /* Battery */ + if (state.battery_mv > 0) { + uint8_t pct = state.battery_pct; + + if (pct == 0) { + pct = calc_battery_pct(state.battery_mv); + } + snprintf(buf, sizeof(buf), "Batt: %u%% (%umV)", pct, state.battery_mv); + mc_display_text(0, y, buf, false); + } +} + /* ========== Page render dispatch ========== */ typedef void (*page_render_fn)(void); @@ -598,6 +675,7 @@ static const page_render_fn renderers[] = { [UI_PAGE_OFFGRID] = render_offgrid, [UI_PAGE_DFU] = render_dfu, [UI_PAGE_SHUTDOWN] = render_shutdown, + [UI_PAGE_STATUS] = render_status, }; /* ========== Public API ========== */ @@ -613,8 +691,10 @@ void ui_pages_render(void) render_top_bar(); render_page_indicator(); - if (state.current_page < UI_PAGE_COUNT && renderers[state.current_page]) { - renderers[state.current_page](); + enum ui_page page = active_pages[current_page_idx]; + + if (page < UI_PAGE_COUNT && renderers[page]) { + renderers[page](); } mc_display_finalize(); @@ -648,35 +728,41 @@ void ui_pages_render_splash(void) void ui_pages_next(void) { state.shutdown_confirm_time = 0; /* Reset confirmation on navigate */ - int page = (int)state.current_page + 1; - - if (page >= UI_PAGE_COUNT) { - page = 0; + current_page_idx++; + if (current_page_idx >= ACTIVE_PAGE_COUNT) { + current_page_idx = 0; } - state.current_page = (enum ui_page)page; + state.current_page = active_pages[current_page_idx]; } void ui_pages_prev(void) { state.shutdown_confirm_time = 0; /* Reset confirmation on navigate */ - int page = (int)state.current_page - 1; - - if (page < 0) { - page = UI_PAGE_COUNT - 1; + current_page_idx--; + if (current_page_idx < 0) { + current_page_idx = ACTIVE_PAGE_COUNT - 1; } - state.current_page = (enum ui_page)page; + state.current_page = active_pages[current_page_idx]; } void ui_pages_set(enum ui_page page) { - if (page < UI_PAGE_COUNT) { - state.current_page = page; + /* Find page in active list, fall back to first active page */ + for (int i = 0; i < ACTIVE_PAGE_COUNT; i++) { + if (active_pages[i] == page) { + current_page_idx = i; + state.current_page = page; + return; + } } + /* Page not in active list — go to first active page */ + current_page_idx = 0; + state.current_page = active_pages[0]; } enum ui_page ui_pages_current(void) { - return state.current_page; + return active_pages[current_page_idx]; } void ui_pages_set_node_name(const char *name) diff --git a/zephcore/helpers/ui/ui_pages.h b/zephcore/helpers/ui/ui_pages.h index 6770916..9f3c72b 100644 --- a/zephcore/helpers/ui/ui_pages.h +++ b/zephcore/helpers/ui/ui_pages.h @@ -31,6 +31,7 @@ enum ui_page { UI_PAGE_OFFGRID, /* Offgrid mode (client repeat) toggle */ UI_PAGE_DFU, /* BLE DFU bootloader entry */ UI_PAGE_SHUTDOWN, /* Hibernate / power off */ + UI_PAGE_STATUS, /* Repeater status (uptime, time, packets) */ UI_PAGE_COUNT }; diff --git a/zephcore/helpers/ui/ui_task.c b/zephcore/helpers/ui/ui_task.c index 40bf280..77182f4 100644 --- a/zephcore/helpers/ui/ui_task.c +++ b/zephcore/helpers/ui/ui_task.c @@ -213,8 +213,12 @@ static void splash_work_handler(struct k_work *work) splash_active = false; #ifdef CONFIG_ZEPHCORE_UI_DISPLAY - /* Transition from splash to home page */ + /* Transition from splash to home page (first active page for this role) */ +#ifdef ZEPHCORE_REPEATER + ui_pages_set(UI_PAGE_STATUS); +#else ui_pages_set(UI_PAGE_MESSAGES); +#endif k_work_reschedule(&render_work, K_NO_WAIT); #endif } @@ -611,7 +615,11 @@ static void ui_input_cb(struct input_event *evt, void *user_data) k_work_cancel_delayable(&splash_work); splash_active = false; #ifdef CONFIG_ZEPHCORE_UI_DISPLAY +#ifdef ZEPHCORE_REPEATER + ui_pages_set(UI_PAGE_STATUS); +#else ui_pages_set(UI_PAGE_MESSAGES); +#endif #endif schedule_render(); return; @@ -718,6 +726,9 @@ int ui_init(void) splash_active = true; ui_pages_render_splash(); + /* Start auto-off timer so display sleeps after timeout */ + mc_display_reset_auto_off(); + /* Schedule transition to home page */ k_work_reschedule(&splash_work, K_MSEC(SPLASH_DURATION_MS)); } else if (ret == -ENODEV) { @@ -817,14 +828,17 @@ void ui_notify(enum ui_event event) break; } - /* Wake display on notifications — but skip for incoming messages - * when a phone is connected (phone handles its own notifications). */ + /* Wake display on notifications. + * Repeater: never wake display on events — only user button wakes it. + * Companion: wake unless phone is connected (phone handles its own). */ #ifdef CONFIG_ZEPHCORE_UI_DISPLAY +#ifndef ZEPHCORE_REPEATER if (!(is_msg_event && get_state()->ble_connected)) { mc_display_on(); schedule_render(); } #endif +#endif } void ui_set_msg_count(uint16_t count) diff --git a/zephcore/src/main_repeater.cpp b/zephcore/src/main_repeater.cpp index cff1141..2ab3c16 100644 --- a/zephcore/src/main_repeater.cpp +++ b/zephcore/src/main_repeater.cpp @@ -20,8 +20,19 @@ LOG_MODULE_REGISTER(zephcore_repeater_main, CONFIG_ZEPHCORE_LORA_LOG_LEVEL); #include #include #include +#include #include "oled_power.h" +/* BLE controller assert handler — BT is compiled even for repeater (via zephcore_common.conf) */ +#if IS_ENABLED(CONFIG_BT_CTLR_ASSERT_HANDLER) +extern "C" void bt_ctlr_assert_handle(char *file, uint32_t line) +{ + LOG_ERR("!!! BLE CONTROLLER ASSERT: %s:%u !!!", file ? file : "?", line); + k_sleep(K_MSEC(100)); + sys_reboot(SYS_REBOOT_COLD); +} +#endif + /* USB CDC with 1200 baud touch detection (when not using auto-init) */ #if !IS_ENABLED(CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT) #include @@ -32,6 +43,9 @@ LOG_MODULE_REGISTER(zephcore_repeater_main, CONFIG_ZEPHCORE_LORA_LOG_LEVEL); #include #include +/* UI subsystem (display, buttons, buzzer) */ +#include "ui_task.h" + /* Radio + mesh includes (shared header selects LR1110 or SX126x) */ #include @@ -240,6 +254,12 @@ static void repeater_event_loop(void) repeater_mesh_ptr->loop(); } #endif + + /* Update display clock on housekeeping tick (if display is on) */ + if (events & MESH_EVENT_HOUSEKEEPING) { + ui_set_clock(rtc_clock.getCurrentTime()); + ui_refresh_display(); + } } } @@ -279,8 +299,10 @@ int main(void) gps_set_repeater_mode(true); } - /* Put OLED display to sleep */ - oled_sleep(); + /* Initialize UI (display + buttons). Shows splash screen, then auto- + * transitions to STATUS page. Display sleeps after auto-off timeout; + * user button is the only wake source for repeater. */ + ui_init(); /* Log environment sensor availability */ if (env_sensors_available()) { @@ -337,16 +359,31 @@ int main(void) /* Apply RX boost setting from prefs (overrides radio default if user changed it) */ lora_radio.setRxBoost(prefs->rx_boost != 0); + /* Feed initial UI state from loaded prefs */ + ui_set_node_name(prefs->node_name); + ui_set_radio_params( + (uint32_t)(prefs->freq * 1000000.0f), /* MHz → Hz */ + prefs->sf, + (uint16_t)(prefs->bw * 10.0f), /* kHz → 0.1 kHz */ + prefs->cr, + prefs->tx_power_dbm, + lora_radio.getNoiseFloor()); + /* Send initial zero-hop advertisement after boot */ LOG_INF("Sending initial advertisement..."); repeater_mesh.sendSelfAdvertisement(500, false); /* 500ms delay, zero-hop */ #endif - /* Initialize USB CDC for CLI with 1200 baud touch detection */ -#if !IS_ENABLED(CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT) + /* Initialize USB serial for CLI */ +#if !IS_ENABLED(CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT) && DT_HAS_COMPAT_STATUS_OKAY(zephyr_cdc_acm_uart) zephcore_usbd_init(); #endif +#if DT_HAS_COMPAT_STATUS_OKAY(zephyr_cdc_acm_uart) usb_dev = DEVICE_DT_GET_ONE(zephyr_cdc_acm_uart); +#else + /* No CDC ACM (e.g. ESP32 usb_serial) — use chosen console UART */ + usb_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_console)); +#endif if (device_is_ready(usb_dev)) { LOG_INF("USB CDC device ready: %s", usb_dev->name); ring_buf_init(&usb_ring_buf, sizeof(usb_ring_buf_data), usb_ring_buf_data);