From 1963b247f56ca7d5733bc9e87df08d461e30175a Mon Sep 17 00:00:00 2001 From: Frieder Schrempf Date: Tue, 16 Dec 2025 20:27:48 +0100 Subject: [PATCH 1/7] EnvironmentSensorManager.cpp: Fix RAK4631 serial GPS detection Serial1 is always true. If we want to check for the presence of a GPS receiver, we need to check if any data was received. Signed-off-by: Frieder Schrempf --- src/helpers/sensors/EnvironmentSensorManager.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index 2692ec9c8..8d0e4c884 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -644,8 +644,7 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){ _location = &RAK12500_provider; return true; - } - else if(Serial1){ + } else if (Serial1.available()) { MESH_DEBUG_PRINTLN("Serial GPS init correctly and is turned on"); if(PIN_GPS_EN){ gpsResetPin = PIN_GPS_EN; From ff39abff44cdb9e2dff57f57c862adea640c9f55 Mon Sep 17 00:00:00 2001 From: Frieder Schrempf Date: Tue, 16 Dec 2025 20:28:20 +0100 Subject: [PATCH 2/7] EnvironmentSensorManager.cpp: Cleanup after failed RAK4631 GPS detection If no GPS was detected, revert the hardware to the initial state, otherwise we may see conflicts or increased power consumption on some boards. Signed-off-by: Frieder Schrempf --- src/helpers/sensors/EnvironmentSensorManager.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index 8d0e4c884..698132382 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -606,6 +606,7 @@ void EnvironmentSensorManager::rakGPSInit(){ MESH_DEBUG_PRINTLN("No GPS found"); gps_active = false; gps_detected = false; + Serial1.end(); return; } @@ -654,6 +655,8 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){ gps_detected = true; return true; } + + pinMode(ioPin, INPUT); MESH_DEBUG_PRINTLN("GPS did not init with this IO pin... try the next"); return false; } From a043cc1dbff38528e8f01ba51a9b2cec0f82099b Mon Sep 17 00:00:00 2001 From: Frieder Schrempf Date: Tue, 16 Dec 2025 20:29:43 +0100 Subject: [PATCH 3/7] Add idle.interval CLI parameter The idle.interval parameter defaults to zero (no CPU idling). Setting it to a non-zero value allows apps to idle or even use sleep modes for the given amount of seconds before continuing the processing loop. This allows to reduce the power consumption. This is added as generic CLI parameter to later be used by app code for different kind of roles such as repeaters and companions. Signed-off-by: Frieder Schrempf --- examples/simple_repeater/MyMesh.cpp | 1 + examples/simple_repeater/MyMesh.h | 1 + src/helpers/CommonCLI.cpp | 14 +++++++++++--- src/helpers/CommonCLI.h | 1 + 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 6ae6ac0a8..64fcc91b7 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -722,6 +722,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.advert_loc_policy = ADVERT_LOC_PREFS; _prefs.adc_multiplier = 0.0f; // 0.0f means use default board multiplier + _prefs.idle_interval = 0; } void MyMesh::begin(FILESYSTEM *fs) { diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index ed9f0c5fc..987851b5a 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -171,6 +171,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; } const char* getRole() override { return FIRMWARE_ROLE; } const char* getNodeName() { return _prefs.node_name; } + const uint32_t getIdleInterval() { return _prefs.idle_interval; } NodePrefs* getNodePrefs() { return &_prefs; } diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index a3de990aa..35278232a 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -70,8 +70,9 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157 file.read((uint8_t *)&_prefs->advert_loc_policy, sizeof (_prefs->advert_loc_policy)); // 161 file.read((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162 - file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 - // 170 + file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 + file.read((uint8_t *)&_prefs->idle_interval, sizeof(_prefs->idle_interval)); // 170 + // 174 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -151,7 +152,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->advert_loc_policy, sizeof(_prefs->advert_loc_policy)); // 161 file.write((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162 file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 - // 170 + file.write((uint8_t *)&_prefs->idle_interval, sizeof(_prefs->idle_interval)); // 170 + // 174 file.close(); } @@ -301,6 +303,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch sprintf(reply, "> %d", (uint32_t) _prefs->tx_power_dbm); } else if (memcmp(config, "freq", 4) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->freq)); + } else if (memcmp(config, "idle.interval", 13) == 0) { + sprintf(reply, "> %d", ((uint32_t)_prefs->idle_interval)); } else if (memcmp(config, "public.key", 10) == 0) { strcpy(reply, "> "); mesh::Utils::toHex(&reply[2], _callbacks->getSelfId().pub_key, PUB_KEY_SIZE); @@ -484,6 +488,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch _prefs->freq = atof(&config[5]); savePrefs(); strcpy(reply, "OK - reboot to apply"); + } else if (memcmp(config, "idle.interval ", 14) == 0) { + _prefs->idle_interval = atoi(&config[14]); + savePrefs(); + strcpy(reply, "OK"); #ifdef WITH_BRIDGE } else if (memcmp(config, "bridge.enabled ", 15) == 0) { _prefs->bridge_enabled = memcmp(&config[15], "on", 2) == 0; diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 068783ab1..2848baf9f 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -48,6 +48,7 @@ struct NodePrefs { // persisted to file uint8_t advert_loc_policy; uint32_t discovery_mod_timestamp; float adc_multiplier; + uint32_t idle_interval; // in seconds }; class CommonCLICallbacks { From 13e3d9aa514013729dfec97dd043aa26767b1bcb Mon Sep 17 00:00:00 2001 From: Frieder Schrempf Date: Tue, 16 Dec 2025 20:36:37 +0100 Subject: [PATCH 4/7] 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 fe08ef655c58b11fad667dde840dfa04b8d53039 Mon Sep 17 00:00:00 2001 From: Frieder Schrempf Date: Tue, 16 Dec 2025 20:37:14 +0100 Subject: [PATCH 5/7] Dispatcher: Add hasOutboundPackets() This adds a method hasOutboundPackets() to the Dispatcher class to return if any outbound packets are queued. Signed-off-by: Frieder Schrempf --- src/Dispatcher.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Dispatcher.h b/src/Dispatcher.h index 25a41d82c..013528c91 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -167,6 +167,7 @@ class Dispatcher { Packet* obtainNewPacket(); void releasePacket(Packet* packet); void sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis=0); + bool hasOutboundPackets() { return _mgr->getOutboundCount(0xFFFFFFFF); } unsigned long getTotalAirTime() const { return total_air_time; } // in milliseconds unsigned long getReceiveAirTime() const {return rx_air_time; } From 7e7f091a7daa113cef2c2c3ec93a0b362be9c589 Mon Sep 17 00:00:00 2001 From: Frieder Schrempf Date: Tue, 16 Dec 2025 20:38:07 +0100 Subject: [PATCH 6/7] RadioLibWrappers: Add blockTaskUntilRXEvent() This adds a new method blockTaskUntilRXEvent() which uses a FreeRTOS semaphore to stall the calling task until a packet has been received. Signed-off-by: Frieder Schrempf --- src/helpers/radiolib/RadioLibWrappers.cpp | 11 +++++++++++ src/helpers/radiolib/RadioLibWrappers.h | 2 ++ 2 files changed, 13 insertions(+) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 9014743a3..9e852d266 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 SemaphoreHandle_t rx_wait_sem; // this function is called when a complete packet // is transmitted by the module @@ -22,6 +23,8 @@ static void setFlag(void) { // we sent a packet, set the flag state |= STATE_INT_READY; + + if (state & STATE_RX) xSemaphoreGive(rx_wait_sem); } void RadioLibWrapper::begin() { @@ -38,6 +41,10 @@ void RadioLibWrapper::begin() { // start average out some samples _num_floor_samples = 0; _floor_sample_sum = 0; + + rx_wait_sem = xSemaphoreCreateBinary(); + xSemaphoreGive(rx_wait_sem); + xSemaphoreTake(rx_wait_sem, portMAX_DELAY); } void RadioLibWrapper::idle() { @@ -45,6 +52,10 @@ void RadioLibWrapper::idle() { state = STATE_IDLE; // need another startReceive() } +int RadioLibWrapper::blockTaskUntilRXEvent(unsigned int timeout = 5000) { + return xSemaphoreTake(rx_wait_sem, pdMS_TO_TICKS(timeout)); +} + void RadioLibWrapper::triggerNoiseFloorCalibrate(int threshold) { _threshold = threshold; if (_num_floor_samples >= NUM_NOISE_FLOOR_SAMPLES) { // ignore trigger if currently sampling diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 3c26d3727..6e907ba4b 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -36,6 +36,8 @@ class RadioLibWrapper : public mesh::Radio { return isChannelActive(); } + int blockTaskUntilRXEvent(unsigned int timeout); + virtual float getCurrentRSSI() =0; int getNoiseFloor() const override { return _noise_floor; } From 37d0fe17a1f73062b0407f88c103b81647a59505 Mon Sep 17 00:00:00 2001 From: Frieder Schrempf Date: Tue, 16 Dec 2025 20:39:18 +0100 Subject: [PATCH 7/7] simple_repeater: Allow to idle the CPU for powersaving Use the idle.interval parameter to stall the loop to save power. The loop is halted for the given amount of seconds before continuing the processing. Only receiving packets will cause an interruption of the idling. Unfortunately it's not easily possible to interrupt the idling on user input such as CLI activity or user button inputs as those things are currently not interrupt-driven. Therefore the UI and the CLI will remain unresponsive during sleep. After booting the CLI will be available for three minutes before the first sleep interval and if a CLI command is issued this period will be extended for another three minutes. On a RAK4631 repeater this can reduce the power consumption during RX mode from around 12 mA to around 7.5 mA. Signed-off-by: Frieder Schrempf --- examples/simple_repeater/main.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 7387e77e7..13163f691 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -19,6 +19,12 @@ void halt() { static char command[160]; +constexpr unsigned long ACTIVE_TIME_MS_INUSE = 3 * 60 * 1000; // 3 minutes +constexpr unsigned long ACTIVE_TIME_MS_IDLE = 5 * 1000; // 5 seconds + +unsigned long active_timestamp; +unsigned long active_time_ms = ACTIVE_TIME_MS_INUSE; + void setup() { Serial.begin(115200); delay(1000); @@ -82,6 +88,7 @@ void setup() { // send out initial Advertisement to the mesh the_mesh.sendSelfAdvertisement(16000); + active_timestamp = millis(); } void loop() { @@ -103,6 +110,8 @@ void loop() { Serial.print('\n'); command[len - 1] = 0; // replace newline with C string null terminator char reply[160]; + active_timestamp = millis(); + active_time_ms = ACTIVE_TIME_MS_INUSE; the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! if (reply[0]) { Serial.print(" -> "); Serial.println(reply); @@ -117,4 +126,11 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); + + if (the_mesh.getIdleInterval() && the_mesh.millisHasNowPassed(active_timestamp + active_time_ms) && + !the_mesh.hasOutboundPackets()) { + radio_driver.blockTaskUntilRXEvent(the_mesh.getIdleInterval() * 1000); + active_timestamp = millis(); + active_time_ms = ACTIVE_TIME_MS_IDLE; + } }