From dc10fd2cbe2f9b150a45a4ddac21344ee68bfaac Mon Sep 17 00:00:00 2001 From: Frieder Schrempf Date: Tue, 16 Dec 2025 20:36:37 +0100 Subject: [PATCH 01/10] variants: RAK4631: Enable RF module reset pin There is no reason to not use the reset pin as the RAK4630/31 module has it connected internally. Signed-off-by: Frieder Schrempf --- variants/rak4631/RAK4631Board.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/rak4631/RAK4631Board.h b/variants/rak4631/RAK4631Board.h index a181256b0..343f4ebff 100644 --- a/variants/rak4631/RAK4631Board.h +++ b/variants/rak4631/RAK4631Board.h @@ -7,7 +7,7 @@ // LoRa radio module pins for RAK4631 #define P_LORA_DIO_1 47 #define P_LORA_NSS 42 -#define P_LORA_RESET RADIOLIB_NC // 38 +#define P_LORA_RESET 38 #define P_LORA_BUSY 46 #define P_LORA_SCLK 43 #define P_LORA_MISO 45 From 980427a1320d5a87c041a8b98394242391572e45 Mon Sep 17 00:00:00 2001 From: Frieder Schrempf Date: Fri, 2 Jan 2026 11:57:36 +0100 Subject: [PATCH 02/10] simple_repeater: Use constexpr variables for powersaving timings This makes the code easier to read and allows for easier changing of the hardcoded values. Signed-off-by: Frieder Schrempf --- examples/simple_repeater/main.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 8c745613e..c4ffc16a3 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -20,8 +20,12 @@ void halt() { static char command[160]; // For power saving +constexpr unsigned long ACTIVE_TIME_SEC_INUSE = 2 * 60; // 2 minutes +constexpr unsigned long ACTIVE_TIME_SEC_IDLE = 5; // 5 seconds +constexpr unsigned long IDLE_PERIOD_SEC = 30 * 60; // 30 minutes + unsigned long lastActive = 0; // mark last active time -unsigned long nextSleepinSecs = 120; // next sleep in seconds. The first sleep (if enabled) is after 2 minutes from boot +unsigned long nextSleepinSecs = ACTIVE_TIME_SEC_INUSE; // next sleep in seconds void setup() { Serial.begin(115200); @@ -125,14 +129,14 @@ void loop() { #endif rtc_clock.tick(); - if (the_mesh.getNodePrefs()->powersaving_enabled && // To check if power saving is enabled + if (the_mesh.getNodePrefs()->powersaving_enabled && the_mesh.millisHasNowPassed(lastActive + nextSleepinSecs * 1000)) { // To check if it is time to sleep if (!the_mesh.hasPendingWork()) { // No pending work. Safe to sleep - board.sleep(1800); // To sleep. Wake up after 30 minutes or when receiving a LoRa packet + board.sleep(IDLE_PERIOD_SEC); // To sleep. Wake up after 30 minutes or when receiving a LoRa packet lastActive = millis(); - nextSleepinSecs = 5; // Default: To work for 5s and sleep again + nextSleepinSecs = ACTIVE_TIME_SEC_IDLE; // Default: To work for 5s and sleep again } else { - nextSleepinSecs += 5; // When there is pending work, to work another 5s + nextSleepinSecs += ACTIVE_TIME_SEC_IDLE; // When there is pending work, to work another 5s } } } From c1e8ee64b425cc815ebac2d92ad89a9e68ed72b5 Mon Sep 17 00:00:00 2001 From: Frieder Schrempf Date: Fri, 2 Jan 2026 12:00:28 +0100 Subject: [PATCH 03/10] simple_repeater: Extend active interval on serial CLI activity When a CLI command is issued through the serial interface, extend the timeout for going to sleep to give the user more time for issuing more commands. Signed-off-by: Frieder Schrempf --- examples/simple_repeater/main.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index c4ffc16a3..f5d00be20 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -114,6 +114,8 @@ void loop() { Serial.print('\n'); command[len - 1] = 0; // replace newline with C string null terminator char reply[160]; + lastActive = millis(); + nextSleepinSecs = ACTIVE_TIME_SEC_INUSE; the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! if (reply[0]) { Serial.print(" -> "); Serial.println(reply); From 4865bd3dc036154baf4b30b34f1ce69ed4f0e4bd Mon Sep 17 00:00:00 2001 From: Frieder Schrempf Date: Fri, 9 Jan 2026 11:47:14 +0100 Subject: [PATCH 04/10] Add guard for board.sleep() to prevent sleep during critical tasks This can be used to prevent sleeping during critical tasks like OTA update. Signed-off-by: Frieder Schrempf --- examples/simple_repeater/main.cpp | 2 +- src/MeshCore.h | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index f5d00be20..e09bc84ba 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -134,7 +134,7 @@ void loop() { if (the_mesh.getNodePrefs()->powersaving_enabled && the_mesh.millisHasNowPassed(lastActive + nextSleepinSecs * 1000)) { // To check if it is time to sleep if (!the_mesh.hasPendingWork()) { // No pending work. Safe to sleep - board.sleep(IDLE_PERIOD_SEC); // To sleep. Wake up after 30 minutes or when receiving a LoRa packet + board.enterSleep(IDLE_PERIOD_SEC); // To sleep. Wake up after 30 minutes or when receiving a LoRa packet lastActive = millis(); nextSleepinSecs = ACTIVE_TIME_SEC_IDLE; // Default: To work for 5s and sleep again } else { diff --git a/src/MeshCore.h b/src/MeshCore.h index 718660d3b..59c292c4f 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -41,6 +41,10 @@ namespace mesh { #define BD_STARTUP_RX_PACKET 1 class MainBoard { +protected: + volatile bool prevent_sleep = false; + virtual void sleep(uint32_t secs) { /* no op */ } + public: virtual uint16_t getBattMilliVolts() = 0; virtual float getMCUTemperature() { return NAN; } @@ -51,7 +55,13 @@ class MainBoard { virtual void onAfterTransmit() { } virtual void reboot() = 0; virtual void powerOff() { /* no op */ } - virtual void sleep(uint32_t secs) { /* no op */ } + virtual void enterSleep(uint32_t secs) { + if (prevent_sleep) return; + + MESH_DEBUG_PRINTLN("Entering sleep mode, wakeup scheduled in %u seconds", secs); + sleep(secs); + } + virtual void preventSleep() { prevent_sleep = true; } virtual uint32_t getGpio() { return 0; } virtual void setGpio(uint32_t values) {} virtual uint8_t getStartupReason() const = 0; From 84f954b88d8c68f052cc5508019f3edc806c0448 Mon Sep 17 00:00:00 2001 From: Frieder Schrempf Date: Fri, 9 Jan 2026 11:50:32 +0100 Subject: [PATCH 05/10] Prevent sleep during OTA update In order to not interrupt the OTA update, prevent any sleep requests after the "start ota" command has been activated. Signed-off-by: Frieder Schrempf --- src/helpers/NRF52Board.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index c0d58314e..5a149c8d4 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -55,6 +55,7 @@ float NRF52Board::getMCUTemperature() { } bool NRF52BoardOTA::startOTAUpdate(const char *id, char reply[]) { + preventSleep(); // Config the peripheral connection with maximum bandwidth // more SRAM required by SoftDevice // Note: All config***() function must be called before begin() From 5d37a372b7c74661955f9b82f428c78c94083e77 Mon Sep 17 00:00:00 2001 From: Frieder Schrempf Date: Fri, 9 Jan 2026 14:55:58 +0100 Subject: [PATCH 06/10] simple_repeater: Include ongoing RX processing in hasPendingWork() If the radio driver state machine is not in receive mode, this means that processing of a packet is still in progress and we are not in an idle state. Signed-off-by: Frieder Schrempf --- examples/simple_repeater/MyMesh.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index d926148d6..e737aff19 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1197,5 +1197,5 @@ void MyMesh::loop() { // To check if there is pending work bool MyMesh::hasPendingWork() const { - return _mgr->getOutboundCount(0xFFFFFFFF) > 0; + return (_mgr->getOutboundCount(0xFFFFFFFF) > 0) || !radio_driver.isInRecvMode(); } From 148f6961023d7394135fde79262d4129ee0f4bc3 Mon Sep 17 00:00:00 2001 From: Frieder Schrempf Date: Fri, 9 Jan 2026 11:58:17 +0100 Subject: [PATCH 07/10] RadioLibWrappers.cpp: Rename ISR and apply clang-format fixes The ISR will be used for other purposes than just setting a flag, rename it accordingly. Signed-off-by: Frieder Schrempf --- src/helpers/radiolib/RadioLibWrappers.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index e34078211..6167b31fc 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -15,21 +15,21 @@ static volatile uint8_t state = STATE_IDLE; // this function is called when a complete packet // is transmitted by the module -static +static #if defined(ESP8266) || defined(ESP32) - ICACHE_RAM_ATTR + ICACHE_RAM_ATTR #endif -void setFlag(void) { + void radioISR(void) { // we sent a packet, set the flag state |= STATE_INT_READY; } void RadioLibWrapper::begin() { - _radio->setPacketReceivedAction(setFlag); // this is also SentComplete interrupt + _radio->setPacketReceivedAction(radioISR); // this is also SentComplete interrupt state = STATE_IDLE; if (_board->getStartupReason() == BD_STARTUP_RX_PACKET) { // received a LoRa packet (while in deep sleep) - setFlag(); // LoRa packet is already received + radioISR(); // LoRa packet is already received } _noise_floor = 0; From 72a7335434d8e5c8e675ddcd7d1f9fd3a8a931eb Mon Sep 17 00:00:00 2001 From: Frieder Schrempf Date: Mon, 12 Jan 2026 20:11:54 +0100 Subject: [PATCH 08/10] RadioLibWrappers.cpp: Fix STATE_TX_WAIT bitmask STATE_TX_WAIT is meant to be a single bit but is defined as two bits, therefore overlapping with STATE_RX. This causes incorrect results when the code uses the bits to handle the state machine. Fix this. Signed-off-by: Frieder Schrempf --- src/helpers/radiolib/RadioLibWrappers.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 6167b31fc..84e6d7e95 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -4,7 +4,7 @@ #define STATE_IDLE 0 #define STATE_RX 1 -#define STATE_TX_WAIT 3 +#define STATE_TX_WAIT 2 #define STATE_TX_DONE 4 #define STATE_INT_READY 16 From a6ce03dae8f7516e989052bc1ca1a7ee7e9951c1 Mon Sep 17 00:00:00 2001 From: Frieder Schrempf Date: Fri, 9 Jan 2026 12:04:35 +0100 Subject: [PATCH 09/10] Add callback for board-specific actions on radio RX interrupt In some cases it is useful to let the board driver know when the radio issues an RX interrupt. This can be used to wake up the board from a low power state asynchronously for example. Signed-off-by: Frieder Schrempf --- src/MeshCore.h | 1 + src/helpers/radiolib/RadioLibWrappers.cpp | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/MeshCore.h b/src/MeshCore.h index 59c292c4f..ea103aeba 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -53,6 +53,7 @@ class MainBoard { virtual const char* getManufacturerName() const = 0; virtual void onBeforeTransmit() { } virtual void onAfterTransmit() { } + virtual void onRXInterrupt() {} virtual void reboot() = 0; virtual void powerOff() { /* no op */ } virtual void enterSleep(uint32_t secs) { diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 84e6d7e95..c8748d67e 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -12,6 +12,7 @@ #define SAMPLING_THRESHOLD 14 static volatile uint8_t state = STATE_IDLE; +static mesh::MainBoard *board; // this function is called when a complete packet // is transmitted by the module @@ -22,9 +23,12 @@ static void radioISR(void) { // we sent a packet, set the flag state |= STATE_INT_READY; + + if (state & STATE_RX) board->onRXInterrupt(); } void RadioLibWrapper::begin() { + board = _board; _radio->setPacketReceivedAction(radioISR); // this is also SentComplete interrupt state = STATE_IDLE; From 1ae1d7b96757bc76acc796b78c9aa002500dc649 Mon Sep 17 00:00:00 2001 From: Frieder Schrempf Date: Fri, 2 Jan 2026 12:03:34 +0100 Subject: [PATCH 10/10] Support sleep for NRF52 boards by suspending the main task This uses a FreeRTOS semaphore to block the main loop task and resume it on RX radio IRQ or timeout. Using the Arduino core functions suspendLoop() and resumeLoop() can lead to deadlocks which can only be avoided by disabling IRQs and possibly missing incoming packets. Signed-off-by: Frieder Schrempf --- src/helpers/NRF52Board.cpp | 12 ++++++++++++ src/helpers/NRF52Board.h | 5 +++++ 2 files changed, 17 insertions(+) diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index 5a149c8d4..34cadb024 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -19,6 +19,18 @@ static void disconnect_callback(uint16_t conn_handle, uint8_t reason) { void NRF52Board::begin() { startup_reason = BD_STARTUP_NORMAL; + rx_wait_sem = xSemaphoreCreateBinary(); + xSemaphoreGive(rx_wait_sem); + xSemaphoreTake(rx_wait_sem, portMAX_DELAY); +} + +void NRF52Board::sleep(uint32_t secs) { + MESH_DEBUG_PRINTLN("Suspending loop for powersaving, set wakeup timer in %u seconds", secs); + xSemaphoreTake(rx_wait_sem, pdMS_TO_TICKS(secs * 1000)); +} + +void NRF52Board::onRXInterrupt() { + xSemaphoreGive(rx_wait_sem); } void NRF52BoardDCDC::begin() { diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index 0d6c0a431..651adadad 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -6,6 +6,9 @@ #if defined(NRF52_PLATFORM) class NRF52Board : public mesh::MainBoard { +private: + SemaphoreHandle_t rx_wait_sem; + protected: uint8_t startup_reason; @@ -14,6 +17,8 @@ class NRF52Board : public mesh::MainBoard { virtual uint8_t getStartupReason() const override { return startup_reason; } virtual float getMCUTemperature() override; virtual void reboot() override { NVIC_SystemReset(); } + virtual void sleep(uint32_t secs) override; + virtual void onRXInterrupt() override; }; /*