From 0065e2dee1b109d7fbabb2b3cc690455681435af Mon Sep 17 00:00:00 2001 From: Dave Koberstein Date: Wed, 28 Oct 2026 08:38:11 -0700 Subject: [PATCH 1/3] rak4631: implement button power-off and fix immediate wakeup (closes #1411) Add hardware button power management for RAK4631 and Muziworks R1 builds, and resolve immediate wakeup from SYSTEMOFF sleep (supersedes #2701). Key Changes: - Button Power-Down: Monitor BOARD_BUTTON_PIN (WB_A1 / PIN_USER_BTN_ANA) in RAK4631Board::loop(). A 3-second long press illuminates the green LED (PIN_LED1) as power-down confirmation (leaving the blue LED for BLE status) and initiates deep sleep upon button release. - Debounce & Release Settle: Wait for stable button release before power down, and enforce a 50ms settle window before arming SENSE LOW. - Radio Isolation: Detach RadioLib interrupts (DIO1, BUSY) and reset SX1262 pins to default prior to disabling SX126X_POWER_EN. This prevents the collapsing power rail from triggering a false GPIO DETECT interrupt. - BLE Graceful Disconnect: Disconnect active BLE connections and stop advertising before entering sleep so companion apps disconnect cleanly. - LATCH Clearing: Disable SENSE on non-button pins and clear NRF_P0/P1 LATCH registers right before entering SYSTEMOFF, preventing immediate auto-wake. Hardware Testing: - Verified on a physical Muziworks R1 unit (RAK4631 + WisBlock base with hardware button on WB_A1 / Pin 31). - Confirmed normal BLE operation, 3-second long-press detection with green LED indicator, graceful mobile app disconnect, and sustained SYSTEMOFF deep sleep with all LEDs off (no spurious or immediate wakeups). - Confirmed wake from sleep via reset button back to full operational state. Fixes https://github.com/meshcore-dev/MeshCore/issues/1411 Supersedes https://github.com/meshcore-dev/MeshCore/pull/2701 --- variants/rak4631/RAK4631Board.cpp | 187 ++++++++++++++++++++++++++++-- variants/rak4631/RAK4631Board.h | 4 + variants/rak4631/variant.h | 1 + 3 files changed, 184 insertions(+), 8 deletions(-) diff --git a/variants/rak4631/RAK4631Board.cpp b/variants/rak4631/RAK4631Board.cpp index 1b5698d0ebd..b3211918b13 100644 --- a/variants/rak4631/RAK4631Board.cpp +++ b/variants/rak4631/RAK4631Board.cpp @@ -4,6 +4,10 @@ #include "RAK4631Board.h" +#if __has_include() +#include +#endif + #ifdef ETHERNET_ENABLED // Drive WB_IO2 HIGH as early as possible using direct register access. // WB_IO2 (P1.02, Arduino pin 34) controls the WisBlock slot power switch. @@ -25,31 +29,182 @@ const PowerMgtConfig power_config = { .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK }; +#if defined(PIN_USER_BTN_ANA) && (PIN_USER_BTN_ANA >= 0) + #define BOARD_BUTTON_PIN PIN_USER_BTN_ANA +#elif defined(WB_A1) + #define BOARD_BUTTON_PIN WB_A1 +#elif defined(PIN_USER_BTN) && (PIN_USER_BTN >= 0) + #define BOARD_BUTTON_PIN PIN_USER_BTN +#endif + +static bool isButtonPressed() { +#if defined(PIN_USER_BTN_ANA) && (PIN_USER_BTN_ANA >= 0) + return analogRead(PIN_USER_BTN_ANA) < 20; +#elif defined(BOARD_BUTTON_PIN) + return digitalRead(BOARD_BUTTON_PIN) == LOW; +#else + return false; +#endif +} + +void RAK4631Board::powerOff() { +#if defined(BOARD_BUTTON_PIN) + // Turn on LED to indicate long press was recognized and signal to release + #if defined(PIN_LED1) + digitalWrite(PIN_LED1, HIGH); + #elif defined(PIN_LED2) + digitalWrite(PIN_LED2, HIGH); + #endif + + // Wait until the user physically releases the button and it remains stably released + uint32_t stable_start = millis(); + while (millis() - stable_start < 100) { + if (isButtonPressed()) { + stable_start = millis(); + } + delay(5); + } + + // Turn off LEDs before shutdown + #if defined(PIN_LED1) + digitalWrite(PIN_LED1, LOW); + #endif + #if defined(PIN_LED2) + digitalWrite(PIN_LED2, LOW); + #endif +#endif + +#ifdef NRF52_POWER_MANAGEMENT + initiateShutdown(SHUTDOWN_REASON_USER); +#else + NRF52BoardDCDC::powerOff(); +#endif +} + void RAK4631Board::initiateShutdown(uint8_t reason) { +#if __has_include() + uint8_t sd_enabled = 0; + sd_softdevice_is_enabled(&sd_enabled); + if (sd_enabled) { + Bluefruit.autoConnLed(false); + uint16_t conn_hdl = Bluefruit.connHandle(); + if (conn_hdl != BLE_CONN_HANDLE_INVALID) { + Bluefruit.disconnect(conn_hdl); + delay(50); + } + Bluefruit.Advertising.stop(); + } +#endif + + // Disconnect LoRa pins and detach RadioLib interrupts before powering down + // Otherwise, the collapsing power rail on the SX1262 triggers a false DETECT wakeup + detachInterrupt(digitalPinToInterrupt(P_LORA_DIO_1)); +#ifdef P_LORA_BUSY + detachInterrupt(digitalPinToInterrupt(P_LORA_BUSY)); + nrf_gpio_cfg_default(g_ADigitalPinMap[P_LORA_BUSY]); +#endif +#ifdef P_LORA_RESET + nrf_gpio_cfg_default(g_ADigitalPinMap[P_LORA_RESET]); +#endif +#ifdef P_LORA_NSS + nrf_gpio_cfg_default(g_ADigitalPinMap[P_LORA_NSS]); +#endif + nrf_gpio_cfg_default(g_ADigitalPinMap[P_LORA_DIO_1]); + // Disable LoRa module power before shutdown digitalWrite(SX126X_POWER_EN, LOW); +#if defined(BOARD_BUTTON_PIN) + // Ensure pullup is engaged and line is stably HIGH before arming SENSE + pinMode(BOARD_BUTTON_PIN, INPUT_PULLUP); + uint32_t settle_start = millis(); + while (millis() - settle_start < 50) { + if (digitalRead(BOARD_BUTTON_PIN) == LOW) { + settle_start = millis(); + } + delay(5); + } + + // Ensure no other pin has SENSE enabled + for (uint32_t p = 0; p < 32; p++) { + if (p != g_ADigitalPinMap[BOARD_BUTTON_PIN]) { + nrf_gpio_cfg_sense_set(p, NRF_GPIO_PIN_NOSENSE); + } + } +#if defined(NRF_P1) + for (uint32_t p = 32; p < 48; p++) { + if (p != g_ADigitalPinMap[BOARD_BUTTON_PIN]) { + nrf_gpio_cfg_sense_set(p, NRF_GPIO_PIN_NOSENSE); + } + } +#endif + + nrf_gpio_cfg_sense_input(g_ADigitalPinMap[BOARD_BUTTON_PIN], NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW); +#endif + if (reason == SHUTDOWN_REASON_LOW_VOLTAGE || reason == SHUTDOWN_REASON_BOOT_PROTECT) { configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel); } - enterSystemOff(reason); + // Record shutdown reason in GPREGRET2 + uint8_t sd_on = 0; + sd_softdevice_is_enabled(&sd_on); + if (sd_on) { + sd_power_gpregret_clr(1, 0xFF); + sd_power_gpregret_set(1, reason); + } else { + NRF_POWER->GPREGRET2 = reason; + } + + Serial.flush(); + delay(50); + + // Ensure all LEDs are explicitly powered off and disconnected right before entering sleep +#if defined(PIN_LED1) + digitalWrite(PIN_LED1, LOW); + pinMode(PIN_LED1, INPUT); + nrf_gpio_pin_clear(g_ADigitalPinMap[PIN_LED1]); + nrf_gpio_cfg_default(g_ADigitalPinMap[PIN_LED1]); +#endif +#if defined(PIN_LED2) + digitalWrite(PIN_LED2, LOW); + pinMode(PIN_LED2, INPUT); + nrf_gpio_pin_clear(g_ADigitalPinMap[PIN_LED2]); + nrf_gpio_cfg_default(g_ADigitalPinMap[PIN_LED2]); +#endif + +#if defined(BOARD_BUTTON_PIN) + // Ensure the button is unpressed before clearing latch + while (digitalRead(BOARD_BUTTON_PIN) == LOW) { + delay(10); + } +#endif + + // Clear any latched GPIO DETECT events right before sleep + NRF_P0->LATCH = 0xFFFFFFFF; +#if defined(NRF_P1) + NRF_P1->LATCH = 0xFFFFFFFF; +#endif + + if (sd_on) { + sd_power_system_off(); + } + // Force hardware system off directly + NRF_POWER->SYSTEMOFF = POWER_SYSTEMOFF_SYSTEMOFF_Enter; + while (1) { + NRF_POWER->SYSTEMOFF = POWER_SYSTEMOFF_SYSTEMOFF_Enter; + } } #endif // NRF52_POWER_MANAGEMENT void RAK4631Board::begin() { NRF52BoardDCDC::begin(); pinMode(PIN_VBAT_READ, INPUT); -#ifdef PIN_USER_BTN - pinMode(PIN_USER_BTN, INPUT_PULLUP); -#endif - -#ifdef PIN_USER_BTN_ANA - pinMode(PIN_USER_BTN_ANA, INPUT_PULLUP); +#if defined(BOARD_BUTTON_PIN) + pinMode(BOARD_BUTTON_PIN, INPUT_PULLUP); #endif - #if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL) Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL); #endif @@ -64,4 +219,20 @@ void RAK4631Board::begin() { #endif digitalWrite(SX126X_POWER_EN, HIGH); delay(10); // give sx1262 some time to power up +} + +void RAK4631Board::loop() { + NRF52BoardDCDC::loop(); + +#if defined(BOARD_BUTTON_PIN) + if (isButtonPressed()) { + if (btn_press_start == 0) { + btn_press_start = millis(); + } else if (millis() - btn_press_start >= 3000) { + powerOff(); + } + } else { + btn_press_start = 0; + } +#endif } \ No newline at end of file diff --git a/variants/rak4631/RAK4631Board.h b/variants/rak4631/RAK4631Board.h index 7e67165b197..a8e54a65696 100644 --- a/variants/rak4631/RAK4631Board.h +++ b/variants/rak4631/RAK4631Board.h @@ -10,6 +10,7 @@ class RAK4631Board : public NRF52BoardDCDC { protected: + uint32_t btn_press_start = 0; #ifdef NRF52_POWER_MANAGEMENT void initiateShutdown(uint8_t reason) override; #endif @@ -17,6 +18,7 @@ class RAK4631Board : public NRF52BoardDCDC { public: RAK4631Board() : NRF52Board("RAK4631_OTA") {} void begin(); + void loop() override; #define BATTERY_SAMPLES 8 @@ -35,4 +37,6 @@ class RAK4631Board : public NRF52BoardDCDC { const char* getManufacturerName() const override { return "RAK 4631"; } + + void powerOff() override; }; diff --git a/variants/rak4631/variant.h b/variants/rak4631/variant.h index 38cc88685c0..ba9a6aaac52 100644 --- a/variants/rak4631/variant.h +++ b/variants/rak4631/variant.h @@ -104,6 +104,7 @@ extern "C" static const uint8_t A7 = PIN_A7; #define ADC_RESOLUTION 14 +#define NRF52_POWER_MANAGEMENT // Power management boot protection threshold (millivolts) // Set to 0 to disable boot protection #define PWRMGT_VOLTAGE_BOOTLOCK 3300 // Won't boot below this voltage (mV) From fe55808da9ab51314fceb7ec732b97fd1c716175 Mon Sep 17 00:00:00 2001 From: Dave Koberstein Date: Mon, 9 Nov 2026 08:40:49 -0800 Subject: [PATCH 2/3] rak4631: blank SSD1306 OLED and disable charge pump on shutdown When entering SYSTEMOFF deep sleep from a button long-press or CLI poweroff, the 3V3 rail keeps the SSD1306 OLED powered with its last frame displayed, making the shutdown appear like a system freeze. Send display-off (0xAE) and charge-pump-off (0x8D, 0x10) commands over I2C in RAK4631Board::initiateShutdown() before entering SYSTEMOFF. Co-authored-by: StarWhiz --- variants/rak4631/RAK4631Board.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/variants/rak4631/RAK4631Board.cpp b/variants/rak4631/RAK4631Board.cpp index b3211918b13..d366def2534 100644 --- a/variants/rak4631/RAK4631Board.cpp +++ b/variants/rak4631/RAK4631Board.cpp @@ -96,6 +96,22 @@ void RAK4631Board::initiateShutdown(uint8_t reason) { } #endif +#if defined(DISPLAY_CLASS) + // The OLED stays powered from 3V3 in SYSTEMOFF and keeps showing its last frame, + // which makes a successful power-off look like a freeze. Paths such as the + // long-press and the CLI "poweroff" command bypass UITask::shutdown(), so blank + // the panel here: display off (0xAE) and charge pump off (0x8D, 0x10). + #ifndef DISPLAY_ADDRESS + #define DISPLAY_ADDRESS 0x3C + #endif + Wire.beginTransmission(DISPLAY_ADDRESS); + Wire.write(0x00); // control byte: command stream + Wire.write(0xAE); + Wire.write(0x8D); + Wire.write(0x10); + Wire.endTransmission(); +#endif + // Disconnect LoRa pins and detach RadioLib interrupts before powering down // Otherwise, the collapsing power rail on the SX1262 triggers a false DETECT wakeup detachInterrupt(digitalPinToInterrupt(P_LORA_DIO_1)); From 29ecffbcaf625580be87cc1e6e1b5d9aa65a8429 Mon Sep 17 00:00:00 2001 From: Dave Koberstein Date: Thu, 12 Nov 2026 10:15:45 -0800 Subject: [PATCH 3/3] rak4631: release SAADC channels before shutdown and add settle timeouts analogRead() on the user button pin (PIN_USER_BTN_ANA / AIN7) leaves the SAADC channel multiplexer mapped to the pin, causing its digital input buffer to read LOW even when pulled up to 3.3V. This prevented the shutdown settle loop from exiting and kept SENSE_LOW from detecting wake presses. Disconnect all SAADC channels (PSELP/PSELN = NC) before enabling digital pullup in initiateShutdown(). Additionally, add 3-second safety timeouts to both the settle and release loops as a fail-safe against stuck inputs. Tested and confirmed on hardware: verified that the USB serial mapping was not dropping before this update (confirming the hang in initiateShutdown), and now cleanly drops upon entering SYSTEMOFF, and the device wakes up properly on button press. Thanks to @StarWhiz for the diagnosis and patch. Co-authored-by: StarWhiz --- variants/rak4631/RAK4631Board.cpp | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/variants/rak4631/RAK4631Board.cpp b/variants/rak4631/RAK4631Board.cpp index d366def2534..55c1ed0d120 100644 --- a/variants/rak4631/RAK4631Board.cpp +++ b/variants/rak4631/RAK4631Board.cpp @@ -131,10 +131,21 @@ void RAK4631Board::initiateShutdown(uint8_t reason) { digitalWrite(SX126X_POWER_EN, LOW); #if defined(BOARD_BUTTON_PIN) - // Ensure pullup is engaged and line is stably HIGH before arming SENSE +#if defined(PIN_USER_BTN_ANA) && (PIN_USER_BTN_ANA >= 0) + // analogRead() disables the SAADC afterwards but leaves its channel inputs + // selecting the button's AIN pin. While selected, the pin's digital input reads 0, + // so the settle loop below never exits and SENSE could never see a press. + // Release every channel so the button pin works as a digital input again. + for (int i = 0; i < 8; i++) { + NRF_SAADC->CH[i].PSELP = SAADC_CH_PSELP_PSELP_NC; + NRF_SAADC->CH[i].PSELN = SAADC_CH_PSELN_PSELN_NC; + } +#endif + // Ensure pullup is engaged and line is stably HIGH before arming SENSE (with 3s safety timeout) pinMode(BOARD_BUTTON_PIN, INPUT_PULLUP); uint32_t settle_start = millis(); - while (millis() - settle_start < 50) { + uint32_t settle_timeout = millis(); + while (millis() - settle_start < 50 && millis() - settle_timeout < 3000) { if (digitalRead(BOARD_BUTTON_PIN) == LOW) { settle_start = millis(); } @@ -191,8 +202,9 @@ void RAK4631Board::initiateShutdown(uint8_t reason) { #endif #if defined(BOARD_BUTTON_PIN) - // Ensure the button is unpressed before clearing latch - while (digitalRead(BOARD_BUTTON_PIN) == LOW) { + // Ensure the button is unpressed before clearing latch (with 3s safety timeout) + uint32_t release_timeout = millis(); + while (digitalRead(BOARD_BUTTON_PIN) == LOW && millis() - release_timeout < 3000) { delay(10); } #endif @@ -251,4 +263,4 @@ void RAK4631Board::loop() { btn_press_start = 0; } #endif -} \ No newline at end of file +}