diff --git a/zephcore/helpers/ui-button/ui_task.c b/zephcore/helpers/ui-button/ui_task.c index 90f8263..709b73a 100644 --- a/zephcore/helpers/ui-button/ui_task.c +++ b/zephcore/helpers/ui-button/ui_task.c @@ -495,10 +495,7 @@ static void action_deep_sleep(void) #ifdef CONFIG_POWEROFF LOG_INF("deep sleep: shutting down..."); - /* 1. Stop LED heartbeat and msg indicator */ - ui_set_heartbeat_led(false); - - /* 2. Play shutdown melody (blocking wait) */ + /* Shutdown melody — blocking wait is fine on this terminal path. */ #ifdef CONFIG_ZEPHCORE_UI_BUZZER buzzer_play(MELODY_SHUTDOWN); while (buzzer_is_playing()) { @@ -507,78 +504,10 @@ static void action_deep_sleep(void) buzzer_stop(); #endif - /* 3. Turn display off */ -#ifdef CONFIG_ZEPHCORE_UI_DISPLAY - mc_display_off(); -#endif - - /* 4. Drive power-hungry enable pins LOW. - * - * nRF52840 GPIO output latches persist across System OFF and reset. - * If GPS_EN is HIGH, the GPS module stays powered during "sleep." - * Drive known power-enable GPIOs LOW via Zephyr's safe GPIO API. - * - * DO NOT touch BLE (bt_conn_disconnect / bt_le_adv_stop) — that - * corrupts BLE controller state and prevents clean reboot. - * DO NOT blank all GPIOs — that bricked the device previously. */ - gps_power_off_for_shutdown(); - mesh_disable_power_regulators(); - - /* 5. Hold the LoRa radio in hardware reset. - * - * sys_poweroff() bypasses device PM — the SX126x hardware duty cycle - * would otherwise keep cycling autonomously (drawing mA) while the - * SoC is in System OFF (~1µA). Drive RESET low; nRF52 GPIO output - * latches persist across System OFF so the chip stays in reset (0µA). - * On wakeup, the driver's sx126x_init() re-asserts then releases reset. */ -#if DT_NODE_EXISTS(DT_ALIAS(lora0)) && DT_NODE_HAS_PROP(DT_ALIAS(lora0), reset_gpios) - { - static const struct gpio_dt_spec lora_reset = - GPIO_DT_SPEC_GET(DT_ALIAS(lora0), reset_gpios); - gpio_pin_configure_dt(&lora_reset, GPIO_OUTPUT_ACTIVE); - } -#endif - - /* 6. Configure GPIO SENSE for button wakeup, then enter System OFF. - * - * The nRF GPIO driver does not implement GPIO_INT_WAKEUP, so the - * wakeup-source DTS property has no effect on nRF52. We must set - * SENSE bits directly via the nRF HAL. sys_poweroff() goes straight - * to nrf_power_system_off() with no device PM suspend, so these bits - * persist into System OFF and trigger a reset on next button press. - * - * Wait for button release first: if we enter System OFF while the - * button is still held (long-press shutdown), DETECT is already - * asserted and the chip cannot enter System OFF cleanly. */ -#if defined(CONFIG_SOC_FAMILY_NORDIC_NRF) && DT_NODE_EXISTS(DT_ALIAS(sw0)) - { -#define _SW0_NODE DT_ALIAS(sw0) -#define _SW0_PORT DT_PROP(DT_GPIO_CTLR(_SW0_NODE, gpios), port) -#define _SW0_PIN DT_GPIO_PIN(_SW0_NODE, gpios) -#define _SW0_FLAGS DT_GPIO_FLAGS(_SW0_NODE, gpios) - - static const struct gpio_dt_spec sw0 = - GPIO_DT_SPEC_GET(DT_ALIAS(sw0), gpios); - gpio_pin_configure_dt(&sw0, GPIO_INPUT); - - int64_t deadline = k_uptime_get() + 5000; - while (gpio_pin_get_dt(&sw0) && k_uptime_get() < deadline) { - k_sleep(K_MSEC(10)); - } - - nrf_gpio_cfg_sense_input( - NRF_GPIO_PIN_MAP(_SW0_PORT, _SW0_PIN), - (_SW0_FLAGS & GPIO_PULL_UP) ? NRF_GPIO_PIN_PULLUP : - (_SW0_FLAGS & GPIO_PULL_DOWN) ? NRF_GPIO_PIN_PULLDOWN : - NRF_GPIO_PIN_NOPULL, - (_SW0_FLAGS & GPIO_ACTIVE_LOW) ? NRF_GPIO_PIN_SENSE_LOW - : NRF_GPIO_PIN_SENSE_HIGH); -#undef _SW0_NODE -#undef _SW0_PORT -#undef _SW0_PIN -#undef _SW0_FLAGS - } -#endif /* CONFIG_SOC_FAMILY_NORDIC_NRF && sw0 */ + /* Shared peripheral teardown + SENSE config for sw0 wake. + * Single source of truth in helpers/ui/ui_common.c so the joystick + * UI variant ends up in the same low-power state. */ + ui_prepare_for_system_off(); LOG_INF("deep sleep: entering System OFF"); sys_poweroff(); diff --git a/zephcore/helpers/ui-joystick/joystick_ui_task.cpp b/zephcore/helpers/ui-joystick/joystick_ui_task.cpp index 3231b39..e8dfd1a 100644 --- a/zephcore/helpers/ui-joystick/joystick_ui_task.cpp +++ b/zephcore/helpers/ui-joystick/joystick_ui_task.cpp @@ -1394,16 +1394,23 @@ void JoystickUITask::shutdown(bool restart) LOG_INF("joystick UI: shutdown (restart=%d)", restart); #ifdef CONFIG_ZEPHCORE_UI_BUZZER buzzer_play("shutdown:d=16,o=5,b=120:c,p,a4,p,f4"); - /* Brief wait for melody */ + /* Brief wait for melody — terminal path either way. */ k_sleep(K_MSEC(400)); #endif - _display.turnOff(); + if (restart) { + _display.turnOff(); sys_reboot(SYS_REBOOT_COLD); } else { #ifdef CONFIG_POWEROFF + /* Full peripheral teardown + SENSE config for sw0 wake. Shared + * helper turns off display, GPS, regulators, holds LoRa in reset + * and arms the button SENSE so the user can actually wake the + * device. Same code path as the button UI's action_deep_sleep. */ + ui_prepare_for_system_off(); sys_poweroff(); #else + _display.turnOff(); sys_reboot(SYS_REBOOT_COLD); #endif } diff --git a/zephcore/helpers/ui/ui_common.c b/zephcore/helpers/ui/ui_common.c index 828b8c4..89d59f0 100644 --- a/zephcore/helpers/ui/ui_common.c +++ b/zephcore/helpers/ui/ui_common.c @@ -13,8 +13,20 @@ #include "buzzer.h" #endif +#ifdef CONFIG_ZEPHCORE_UI_DISPLAY +#include "display.h" +#endif + +#include /* gps_power_off_for_shutdown */ +#include "ui_mesh_actions.h" /* mesh_disable_power_regulators (weak) */ + #include #include + +#if defined(CONFIG_SOC_FAMILY_NORDIC_NRF) +#include +#endif + #include LOG_MODULE_REGISTER(ui_led, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL); @@ -234,6 +246,86 @@ void ui_invalidate_battery_cache(void) s_batt_last_read_ms = 0; } +/* ========== System OFF preparation ========== + * Shared by both UI variants. Caller is responsible for any shutdown chime + * and the final sys_poweroff() call; this just leaves the SoC + peripherals + * in the lowest-power state with a wakeup source armed. + * + * On nRF52 the SoC enters System OFF (~1 µA) but GPIO output latches and + * SENSE bits persist across the transition — so we must explicitly: + * - hold every power-enable GPIO LOW so external chips don't keep drawing + * - hold the LoRa radio in HW reset (its internal duty cycle would + * otherwise keep cycling autonomously, drawing mA) + * - configure SENSE on sw0 so a button press wakes the chip (the nRF GPIO + * driver doesn't honour the DT wakeup-source property, so the dtsi + * marker is inert without this) + * + * Non-nRF platforms skip the SENSE block; they rely on Zephyr's wakeup-source + * DT property which is honoured by their respective GPIO drivers. + */ +void ui_prepare_for_system_off(void) +{ + /* 1. Stop heartbeat LED cycle (cancels both works). */ + ui_set_heartbeat_led(false); + + /* 2. Display off — content stays visible on EPD, blanks on OLED. */ +#ifdef CONFIG_ZEPHCORE_UI_DISPLAY + mc_display_off(); +#endif + + /* 3. Drive power-enable GPIOs LOW so peripherals don't keep drawing. + * Do NOT touch BLE here — that corrupts controller state and prevents + * clean reboot on wake. */ + gps_power_off_for_shutdown(); + mesh_disable_power_regulators(); /* weak-stubbed on non-companion roles */ + + /* 4. Hold LoRa radio in HW reset. + * SX126x/LR11xx duty-cycle mode would otherwise keep the radio cycling + * autonomously (mA) while the SoC is in System OFF. nRF52 output + * latches persist across System OFF → chip stays in reset (~0 µA). */ +#if DT_NODE_EXISTS(DT_ALIAS(lora0)) && DT_NODE_HAS_PROP(DT_ALIAS(lora0), reset_gpios) + { + static const struct gpio_dt_spec lora_reset = + GPIO_DT_SPEC_GET(DT_ALIAS(lora0), reset_gpios); + gpio_pin_configure_dt(&lora_reset, GPIO_OUTPUT_ACTIVE); + } +#endif + + /* 5. Configure GPIO SENSE for sw0 button wakeup, after waiting for the + * user to release any held button (otherwise DETECT is already asserted + * when we enter System OFF and the chip never sleeps cleanly). + * nRF only — other platforms rely on the DT wakeup-source property. */ +#if defined(CONFIG_SOC_FAMILY_NORDIC_NRF) && DT_NODE_EXISTS(DT_ALIAS(sw0)) + { +#define _SW0_NODE DT_ALIAS(sw0) +#define _SW0_PORT DT_PROP(DT_GPIO_CTLR(_SW0_NODE, gpios), port) +#define _SW0_PIN DT_GPIO_PIN(_SW0_NODE, gpios) +#define _SW0_FLAGS DT_GPIO_FLAGS(_SW0_NODE, gpios) + + static const struct gpio_dt_spec sw0 = + GPIO_DT_SPEC_GET(DT_ALIAS(sw0), gpios); + gpio_pin_configure_dt(&sw0, GPIO_INPUT); + + int64_t deadline = k_uptime_get() + 5000; + while (gpio_pin_get_dt(&sw0) && k_uptime_get() < deadline) { + k_sleep(K_MSEC(10)); + } + + nrf_gpio_cfg_sense_input( + NRF_GPIO_PIN_MAP(_SW0_PORT, _SW0_PIN), + (_SW0_FLAGS & GPIO_PULL_UP) ? NRF_GPIO_PIN_PULLUP : + (_SW0_FLAGS & GPIO_PULL_DOWN) ? NRF_GPIO_PIN_PULLDOWN : + NRF_GPIO_PIN_NOPULL, + (_SW0_FLAGS & GPIO_ACTIVE_LOW) ? NRF_GPIO_PIN_SENSE_LOW + : NRF_GPIO_PIN_SENSE_HIGH); +#undef _SW0_NODE +#undef _SW0_PORT +#undef _SW0_PIN +#undef _SW0_FLAGS + } +#endif /* CONFIG_SOC_FAMILY_NORDIC_NRF && sw0 */ +} + /* ========== Shared splash logo ========== * 128×13 ZephCore wordmark, MSB-first row-major (Adafruit XBM/drawBitmap * format). Used by both UI variants' splash renders via mc_display_xbm(). */ diff --git a/zephcore/helpers/ui/ui_task.h b/zephcore/helpers/ui/ui_task.h index 58a3b85..cd6e05a 100644 --- a/zephcore/helpers/ui/ui_task.h +++ b/zephcore/helpers/ui/ui_task.h @@ -182,6 +182,17 @@ void ui_set_battery_provider(uint16_t (*provider)(void)); */ void ui_refresh_battery(void); +/** + * Prepare the device for sys_poweroff(): stop heartbeat LED, blank the + * display, power off GPS + sensor regulators, hold LoRa in HW reset, + * configure SENSE on sw0 (nRF only) for button wakeup. + * + * Caller is responsible for any shutdown chime BEFORE this call and the + * final sys_poweroff() AFTER. Both UI variants share this so the System + * OFF state is consistent regardless of which UI design is compiled in. + */ +void ui_prepare_for_system_off(void); + /** * Drop the battery-refresh freshness timestamp. The next * ui_refresh_battery() call is guaranteed to sample the ADC.