diff --git a/zephcore/adapters/board/ZephyrBoard.cpp b/zephcore/adapters/board/ZephyrBoard.cpp index e08d56d..0639e68 100644 --- a/zephcore/adapters/board/ZephyrBoard.cpp +++ b/zephcore/adapters/board/ZephyrBoard.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -18,6 +19,15 @@ #define NRF52_GPREGRET 1 #endif +/* LoRa TX activity LED (optional — defined per-board via DT alias) */ +#if DT_NODE_EXISTS(DT_ALIAS(lora_tx_led)) +static const struct gpio_dt_spec tx_led = + GPIO_DT_SPEC_GET(DT_ALIAS(lora_tx_led), gpios); +#define HAS_TX_LED 1 +#else +#define HAS_TX_LED 0 +#endif + #include LOG_MODULE_REGISTER(zephcore_board, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL); @@ -55,6 +65,18 @@ static const struct device *vbat_enable_dev = NULL; #define VBAT_ADC_SAMPLES 8 #endif +/* Initialize TX LED GPIO at boot */ +#if HAS_TX_LED +static int tx_led_init(void) +{ + if (gpio_is_ready_dt(&tx_led)) { + gpio_pin_configure_dt(&tx_led, GPIO_OUTPUT_INACTIVE); + } + return 0; +} +SYS_INIT(tx_led_init, APPLICATION, 90); +#endif + namespace mesh { uint16_t ZephyrBoard::getBattMilliVolts() @@ -142,6 +164,20 @@ const char *ZephyrBoard::getManufacturerName() const return CONFIG_ZEPHCORE_BOARD_NAME; } +void ZephyrBoard::onBeforeTransmit() +{ +#if HAS_TX_LED + gpio_pin_set_dt(&tx_led, 1); +#endif +} + +void ZephyrBoard::onAfterTransmit() +{ +#if HAS_TX_LED + gpio_pin_set_dt(&tx_led, 0); +#endif +} + void ZephyrBoard::reboot() { k_msleep(50); /* Let UART/USB flush */ diff --git a/zephcore/adapters/board/ZephyrBoard.h b/zephcore/adapters/board/ZephyrBoard.h index c4c2eab..7d752b5 100644 --- a/zephcore/adapters/board/ZephyrBoard.h +++ b/zephcore/adapters/board/ZephyrBoard.h @@ -14,6 +14,8 @@ public: uint16_t getBattMilliVolts() override; float getMCUTemperature() override; const char *getManufacturerName() const override; + void onBeforeTransmit() override; + void onAfterTransmit() override; void reboot() override; void rebootToBootloader(); /* Reboot into UF2 mass storage bootloader */ bool getBootloaderVersion(char *version, size_t max_len) override; diff --git a/zephcore/boards/nrf52840/thinknode_m1/board.conf b/zephcore/boards/nrf52840/thinknode_m1/board.conf index dbb02b9..9bf019b 100644 --- a/zephcore/boards/nrf52840/thinknode_m1/board.conf +++ b/zephcore/boards/nrf52840/thinknode_m1/board.conf @@ -35,3 +35,7 @@ CONFIG_HEAP_MEM_POOL_SIZE=6144 # Shrink RTT buffer to free RAM (4096 → 1024, saves 3KB). # Only used during J-Link debugging, 1KB is Zephyr's default. CONFIG_SEGGER_RTT_BUFFER_SIZE_UP=1024 + +# RX duty cycle — safe on SX1262 (only LR1110 has mid-preamble issues). +# Extends battery life on this battery-powered board. +CONFIG_ZEPHCORE_LORA_RX_DUTY_CYCLE=y diff --git a/zephcore/boards/nrf52840/thinknode_m1/thinknode_m1.dts b/zephcore/boards/nrf52840/thinknode_m1/thinknode_m1.dts index 8c35440..38deb4a 100644 --- a/zephcore/boards/nrf52840/thinknode_m1/thinknode_m1.dts +++ b/zephcore/boards/nrf52840/thinknode_m1/thinknode_m1.dts @@ -45,6 +45,8 @@ lora0 = &lora; led0 = &led_green; led1 = &led_blue; + led2 = &lora_tx_led; + lora-tx-led = &lora_tx_led; sw0 = &user_button; watchdog0 = &wdt0; buzzer = &buzzer; @@ -64,6 +66,12 @@ gpios = <&gpio0 14 GPIO_ACTIVE_LOW>; label = "Blue LED"; }; + + /* LoRa TX activity LED (active-high, matches Arduino P_LORA_TX_LED) */ + lora_tx_led: led_2 { + gpios = <&gpio0 13 GPIO_ACTIVE_HIGH>; + label = "TX LED"; + }; /* LED_RED (P1.06) omitted — shared with display SPI MISO */ }; @@ -172,14 +180,14 @@ }; }; - /* Display power enable (P1.11) — drives the EPD's boost converter / - * frontlight. Without this HIGH, SPI works but the charge pump - * cannot flip e-ink particles (display appears dead). */ + /* Display backlight enable (P1.11) — powers the e-paper frontlight. + * The hardware knob dims brightness; this GPIO gates the circuit. + * Controlled dynamically by display.c (on with display, off on auto-off) + * so the backlight is fully off when not needed. */ disp_pwr_enable: disp-pwr-enable { compatible = "regulator-fixed"; regulator-name = "disp-pwr-enable"; enable-gpios = <&gpio1 11 GPIO_ACTIVE_HIGH>; - regulator-boot-on; }; /* ---- E-Paper display (MIPI DBI SPI) ---- @@ -201,6 +209,7 @@ reg = <0>; width = <200>; height = <200>; + rotation = <270>; busy-gpios = <&gpio0 3 GPIO_ACTIVE_HIGH>; }; }; diff --git a/zephcore/helpers/ui/display.c b/zephcore/helpers/ui/display.c index c1cbfc1..26f0338 100644 --- a/zephcore/helpers/ui/display.c +++ b/zephcore/helpers/ui/display.c @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -40,6 +41,27 @@ static uint8_t font_w; static uint8_t font_h; static bool is_epd; /* true for e-paper displays */ +/* Optional display backlight regulator (e.g. e-paper frontlight). + * Boards define a "disp_pwr_enable" regulator-fixed node to gate the + * backlight circuit. When present, backlight follows display on/off. */ +#if DT_NODE_EXISTS(DT_NODELABEL(disp_pwr_enable)) +static const struct device *backlight_reg = + DEVICE_DT_GET_OR_NULL(DT_NODELABEL(disp_pwr_enable)); +#else +static const struct device *backlight_reg; +#endif + +static inline void backlight_set(bool on) +{ + if (backlight_reg && device_is_ready(backlight_reg)) { + if (on) { + regulator_enable(backlight_reg); + } else { + regulator_disable(backlight_reg); + } + } +} + /* Auto-off work */ static struct k_work_delayable auto_off_work; @@ -50,9 +72,10 @@ static void auto_off_handler(struct k_work *work) if (doom_game_is_running()) { return; } - /* E-paper uses zero power when static — blanking wastes a full - * refresh cycle (~2s) and leaves the screen blank for no benefit. */ + /* E-paper content persists without power — blanking wastes a full + * refresh cycle (~2s) for no benefit. Just turn off the backlight. */ if (is_epd) { + backlight_set(false); return; } if (disp_on) { @@ -170,6 +193,7 @@ int mc_display_init(void) cfb_framebuffer_finalize(disp_dev); display_blanking_off(disp_dev); + backlight_set(true); disp_on = true; disp_initialized = true; @@ -211,6 +235,7 @@ void mc_display_on(void) display_blanking_off(disp_dev); disp_on = true; } + backlight_set(true); mc_display_reset_auto_off(); } @@ -223,6 +248,7 @@ void mc_display_off(void) if (disp_on) { display_blanking_on(disp_dev); + backlight_set(false); disp_on = false; } } diff --git a/zephcore/helpers/ui/ui_task.c b/zephcore/helpers/ui/ui_task.c index 48cf660..05db8b3 100644 --- a/zephcore/helpers/ui/ui_task.c +++ b/zephcore/helpers/ui/ui_task.c @@ -80,12 +80,15 @@ LOG_MODULE_REGISTER(ui_task, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL); /* Match Arduino: 4s cycle, 20ms pulse (normal) or 200ms (unread messages). * Uses led0 or led1 alias — whichever exists in the board's DTS. * Disabled when OLED display is present (display makes LED redundant). */ -#if DT_NODE_HAS_PROP(DT_ALIAS(led0), gpios) && !IS_ENABLED(CONFIG_ZEPHCORE_UI_DISPLAY) +/* Heartbeat LED — subtle pulse every 4s on led0 (or led1 fallback). + * Works alongside displays; boards that want to disable it can + * remove the led0 alias or override this with a Kconfig guard. */ +#if DT_NODE_HAS_PROP(DT_ALIAS(led0), gpios) #define LED_NODE DT_ALIAS(led0) static const struct gpio_dt_spec heartbeat_led = GPIO_DT_SPEC_GET(LED_NODE, gpios); #define HAS_HEARTBEAT_LED 1 -#elif DT_NODE_HAS_PROP(DT_ALIAS(led1), gpios) && !IS_ENABLED(CONFIG_ZEPHCORE_UI_DISPLAY) +#elif DT_NODE_HAS_PROP(DT_ALIAS(led1), gpios) #define LED_NODE DT_ALIAS(led1) static const struct gpio_dt_spec heartbeat_led = GPIO_DT_SPEC_GET(LED_NODE, gpios); @@ -1035,5 +1038,15 @@ void ui_refresh_display(void) return; } + /* EPD displays: skip periodic housekeeping renders. + * Each full e-paper refresh takes ~2s and causes visible flashing. + * All meaningful events (messages, BLE, GPS fix, button presses) + * already trigger renders via their own ui_set_*() → schedule_render(). + * Housekeeping just updates slow-changing data (clock, contact ages) + * which will appear on the next event-driven render. */ + if (mc_display_is_epd()) { + return; + } + schedule_render(); }