Skip to content

rak4631: implement button power-off and fix immediate wakeup (closes #1411) - #3705

Open
komacke wants to merge 3 commits into
meshcore-dev:mainfrom
komacke:fix-1411-r1-button-power
Open

komacke wants to merge 3 commits into
meshcore-dev:mainfrom
komacke:fix-1411-r1-button-power

Conversation

@komacke

@komacke komacke commented Oct 28, 2026

Copy link
Copy Markdown

Summary

This PR implements hardware button power management for RAK4631 / Muziworks R1 hardware, enabling deep sleep power-down via long-press, and fixes the immediate wakeup from SYSTEMOFF (superseding #2701).

Background & Root Cause

  • Issue Configure Muziworks R1 button for power down #1411: The Muziworks R1 (built on RAK4631 + WisBlock base with a button wired to WB_A1 / Pin 31) had no hardware button power-off monitoring in rak4631.
  • Immediate Wakeup: Entering SYSTEMOFF previously triggered an immediate reboot (RESETREAS=0x00010000, GPIO DETECT) because:
    1. RadioLib interrupts attached on DIO1 and BUSY fired as the SX1262 power rail collapsed.
    2. Pin transitions during button release and peripheral shutdown remained latched in the nRF52 LATCH registers.

Key Changes

  1. 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 confirmation (leaving the blue LED for BLE status) and initiates deep sleep upon button release.
  2. Debounce & Settle Window: Wait for stable button release before power-down, and enforce a 50ms settle window before arming NRF_GPIO_PIN_SENSE_LOW.
  3. Radio & Peripheral Isolation: Call detachInterrupt() and reset SX1262 pins to default prior to pulling SX126X_POWER_EN low. Disable SENSE on all non-button pins.
  4. BLE Disconnect: Gracefully disconnect active BLE connections (Bluefruit.disconnect()) and stop advertising before entering sleep.
  5. Clear LATCH Registers: Write 0xFFFFFFFF to NRF_P0->LATCH and NRF_P1->LATCH right before entering SYSTEMOFF.
  6. Zero Impact on Other Variants: Changes are strictly confined to variants/rak4631/. Other variants and newer Muziworks hardware (muzi_base, muziworks_r1_neo) remain untouched. All RAK4631 build targets (BLE, USB, Repeater, Room Server, Terminal Chat) compile cleanly.

Hardware Testing

  • Verified on a physical Muziworks R1 unit (RAK4631 + WisBlock base with hardware button on WB_A1 / Pin 31).
  • Confirmed normal BLE connection and operation.
  • Confirmed 3-second button hold illuminates the green LED, releases cleanly, and disconnects BLE.
  • Confirmed sustained deep sleep with all LEDs completely off (no spurious wakeups).
  • Confirmed clean wake via reset button back to operational state.

Closes #1411
Supersedes #2701

…eshcore-dev#1411)

Add hardware button power management for RAK4631 and Muziworks R1 builds,
and resolve immediate wakeup from SYSTEMOFF sleep (supersedes meshcore-dev#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 meshcore-dev#1411
Supersedes meshcore-dev#2701
@StarWhiz

StarWhiz commented Nov 9, 2026

Copy link
Copy Markdown

Tested this PR on a RAK19007 base + RAK4631 core with an SSD1306 OLED, built from RAK_4631_companion_radio_ble. Thanks for the work on this. The immediate-wakeup fix works for me, and Hibernate from the on-screen menu powers off cleanly.

Issue on boards with a display: the 3-second long-press and the CLI/app poweroff command seemed to freeze the device. The green LED lit, then the screen stayed on showing its last frame and nothing responded. The board had actually entered SYSTEMOFF: a single button press woke it normally. The OLED stays powered from 3V3 during SYSTEMOFF and keeps showing whatever was last drawn.

Hibernate didn't have this problem because UITask::shutdown() clears the display before calling board.powerOff(). The long-press in RAK4631Board::loop() and the poweroff command (MyMesh.cpp / CommonCLI.cpp) call board.powerOff() directly, so the panel is never blanked. With poweroff it looked like it worked only when the screen had already timed out.

Fix I tested: send the SSD1306 display-off and charge-pump-off commands in initiateShutdown(), guarded by DISPLAY_CLASS so builds without a display are unaffected:

@@ -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

With this change, the long-press, poweroff and Hibernate all leave the screen dark, and a button press wakes the board as expected.

A cleaner long-term option might be to route the long-press and poweroff through the UI's shutdown path, so every display type is handled. I went with the board-level fix because it's small and self-contained.

Note: the diagnosis and patch were AI-assisted (Claude). I built, flashed and tested the result on my own hardware.

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 <StarWhiz@users.noreply.github.com>
@komacke

komacke commented Nov 9, 2026

Copy link
Copy Markdown
Author

@StarWhiz I've added your patch and made a commit. It does not affect my non-display device. Would you double check your device and report back? I'm hopeful the powers that be will accept this. :)

@jirogit

jirogit commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

Heads-up: this overlaps with the RAK4631 part of #2701 (powerOff() override + clearing DIO1 SENSE before SYSTEMOFF). Your PR is a superset of that, so I’ll drop the RAK4631 commits from #2701 and keep it T114-only to avoid conflicts.

@StarWhiz

StarWhiz commented Nov 12, 2026 •

Copy link
Copy Markdown

@komacke Thanks for adding the OLED patch. I worked with Claude again and re-tested. I found that I was wrong in my earlier comment: on my board the device never actually reached SYSTEMOFF. It hung in initiateShutdown(). The OLED patch only made the hang look like a power-off. USB stayed enumerated, and only the reset button brought it back. Sorry for the noise. Here's what's really going on, with a fix that Claude and I now verified below...

Hardware: RAK19007 + RAK4631 + SSD1306, RAK_4631_companion_radio_ble. A side button in the case is wired to A1, so the board uses PIN_USER_BTN_ANA=31 (P0.31 / AIN7).

Cause: analogRead() in the Adafruit core (wiring_analog_nRF52.c) turns the SAADC off after each sample, but it leaves CH[0].PSELP/PSELN selecting the AIN pin. While that selection stays, the pin's digital input reads 0, even though the line is at about 3.3 V. isButtonPressed() polls with analogRead(), so by the time initiateShutdown() runs, the settle loop (while ... if (digitalRead(btn) == LOW) settle_start = millis();) never exits. Even if it did, SENSE-LOW couldn't detect a press. The R1 reads its button digitally, which is presumably why you don't see this there.

Evidence (serial log from a temporary diagnostic build, button released):

SHUTDIAG: 4 settle loop start: digital=0 PIN_CNF=0x0000000C
SHUTDIAG: 4a SAADC ENABLE=0 CH0.PSELP=8 CH0.PSELN=8 (AIN7 = 8)
SHUTDIAG: 4b after clearing SAADC pin selection: digital=1
SHUTDIAG: 5 settle loop done
WAKEDIAG: arduino_pin=31 nrf_pin=P0.31 PIN_CNF=0x0003000C DETECTMODE=0

Without step 4b, the log stayed at digital=0 for more than 2 minutes. At runtime the released button reads about 3765/4095 on analogRead(31) (about 3.3 V) and 0 when pressed.

Fix: release the SAADC channel inputs before the button pin is used digitally. This applies only when the button is analog:

@@ -132,4 +132,14 @@ void RAK4631Board::initiateShutdown(uint8_t reason) {
 
 #if defined(BOARD_BUTTON_PIN)
+#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
   pinMode(BOARD_BUTTON_PIN, INPUT_PULLUP);

Tested on top of fe55808d: the 3-second long-press, the poweroff command and on-screen Hibernate now all blank the display, actually enter SYSTEMOFF (USB disconnects), and wake on a single press of the side button.

Possibly worth considering: a timeout on the settle loop and on the final while (digitalRead(btn) == LOW) wait. That way a stuck input falls through to SYSTEMOFF instead of hanging with the radio and BLE already off.

AI-assisted (Claude): diagnosis, diagnostic build and patch. I built, flashed and tested everything on my own hardware.

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 <StarWhiz@users.noreply.github.com>
@komacke

komacke commented Nov 12, 2026

Copy link
Copy Markdown
Author

@StarWhiz Thanks for being thorough! I had not noticed that the USB mapping did not go away. I was in the process of doing a long power-off test and forgot about it. it is draining slowly but clearly it's going to drain more slowing now. :)

Your patch and suggested settle loop timeout also added.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Configure Muziworks R1 button for power down

3 participants