mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-01 20:09:17 +00:00
Add Station G2
This commit is contained in:
+20
-6
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
config HEAP_MEM_POOL_ADD_SIZE_BOARD
|
||||
int
|
||||
default 4096
|
||||
@@ -0,0 +1,3 @@
|
||||
config BOARD_STATION_G2
|
||||
select SOC_ESP32S3_WROOM_N16R8
|
||||
select SOC_ESP32S3_PROCPU if BOARD_STATION_G2_ESP32S3_PROCPU
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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 = <INPUT_BTN_0>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
/* 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"
|
||||
@@ -0,0 +1,6 @@
|
||||
board:
|
||||
name: station_g2
|
||||
full_name: BQ Station G2
|
||||
vendor: bq
|
||||
socs:
|
||||
- name: esp32s3
|
||||
@@ -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 <zephyr/dt-bindings/pinctrl/esp-pinctrl-common.h>
|
||||
#include <zephyr/dt-bindings/pinctrl/esp32s3-pinctrl.h>
|
||||
#include <zephyr/dt-bindings/pinctrl/esp32s3-gpio-sigmap.h>
|
||||
|
||||
&pinctrl {
|
||||
/* SPI2 — LoRa SX1262 (CS handled via cs-gpios, not pinctrl) */
|
||||
spim2_default: spim2_default {
|
||||
group1 {
|
||||
pinmux = <SPIM2_MISO_GPIO14>,
|
||||
<SPIM2_SCLK_GPIO12>;
|
||||
};
|
||||
group2 {
|
||||
pinmux = <SPIM2_MOSI_GPIO13>;
|
||||
output-low;
|
||||
};
|
||||
};
|
||||
|
||||
/* I2C0 — SH1106 OLED display + optional I2C sensors */
|
||||
i2c0_default: i2c0_default {
|
||||
group1 {
|
||||
pinmux = <I2C0_SDA_GPIO5>,
|
||||
<I2C0_SCL_GPIO6>;
|
||||
bias-pull-up;
|
||||
drive-open-drain;
|
||||
output-high;
|
||||
};
|
||||
};
|
||||
|
||||
/* UART1 — GPS module */
|
||||
uart1_default: uart1_default {
|
||||
group1 {
|
||||
pinmux = <UART1_TX_GPIO7>;
|
||||
output-high;
|
||||
};
|
||||
group2 {
|
||||
pinmux = <UART1_RX_GPIO15>;
|
||||
bias-pull-up;
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -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 <espressif/esp32s3/esp32s3_wroom_n16r8.dtsi>
|
||||
#include "station_g2-pinctrl.dtsi"
|
||||
#include <espressif/partitions_0x0_default_16M.dtsi>
|
||||
#include <zephyr/dt-bindings/input/input-event-codes.h>
|
||||
|
||||
/ {
|
||||
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 = <DT_FREQ_M(80)>;
|
||||
};
|
||||
|
||||
&cpu1 {
|
||||
clock-frequency = <DT_FREQ_M(80)>;
|
||||
};
|
||||
|
||||
&usb_serial {
|
||||
status = "okay";
|
||||
};
|
||||
|
||||
&i2c0 {
|
||||
status = "okay";
|
||||
clock-frequency = <I2C_BITRATE_FAST>;
|
||||
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";
|
||||
};
|
||||
@@ -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
|
||||
@@ -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 <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <zephyr/kernel.h>
|
||||
|
||||
__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) {}
|
||||
+103
-17
@@ -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)
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -20,8 +20,19 @@ LOG_MODULE_REGISTER(zephcore_repeater_main, CONFIG_ZEPHCORE_LORA_LOG_LEVEL);
|
||||
#include <zephyr/drivers/uart.h>
|
||||
#include <zephyr/sys/ring_buffer.h>
|
||||
#include <zephyr/drivers/hwinfo.h>
|
||||
#include <zephyr/sys/reboot.h>
|
||||
#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 <ZephyrRepeaterUSB.h>
|
||||
@@ -32,6 +43,9 @@ LOG_MODULE_REGISTER(zephcore_repeater_main, CONFIG_ZEPHCORE_LORA_LOG_LEVEL);
|
||||
#include <adapters/clock/ZephyrRTCClock.h>
|
||||
#include <ZephyrSensorManager.h>
|
||||
|
||||
/* UI subsystem (display, buttons, buzzer) */
|
||||
#include "ui_task.h"
|
||||
|
||||
/* Radio + mesh includes (shared header selects LR1110 or SX126x) */
|
||||
#include <mesh/RadioIncludes.h>
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user