From e4f6d348e98b99b02c82f9162060163a2404a3f4 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 30287d21275d2d66d4fb62fe5faacb6694598169 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 f7d26dc694108f0bc5df000c4f870405c11f11ab 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. 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 b2645c12c76b9e313ac76ad626f3ef5292341bc0 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 07a44daa6a1540b6845af7867502c888e82f4933 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 0ea376fe6776af0a2113c1dacea8109a93372822 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 1dfb6d7a122a207a48fe37ae1d8595e2334da8a2 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..dc87a322f 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() && ((millis() - 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; + } }