Skip to content

Conversation

@fschrempf
Copy link
Contributor

In order to reduce the power consumption of repeaters in general, this introduces a new CLI parameter idle.interval. The default value is zero which results in unchanged behavior. Setting it to a non-zero value causes the processing loop to be halted for the given amount of seconds.

During the idle interval the CLI and the UI (if available) will be unresponsive. The RF module will be kept in RX mode and upon receiving a packet, the interrupt will cause the processing loop to continue immediately before going back to idle after all outgoing packets have been transferred.

On a RAK4631 repeater this can reduce the power consumption during RX mode from around 12 mA to around 7.5 mA.

This can be extended later to use CPU sleep modes (if the platform allows it) to reduce the power consumption further similar to what is proposed in #1107.

I will let this run on my test repeater for some days and then remove the draft status of this PR if everything looks good.

Feedback, tests, reviews and questions welcome!

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 <frieder@fris.de>
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 <frieder@fris.de>
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 <frieder@fris.de>
There is no reason to not use the reset pin as the RAK4630/31 module
has it connected internally.

Signed-off-by: Frieder Schrempf <frieder@fris.de>
This adds a method getTotalOutboundCount() to the Dispatcher class to
return the number of queued outbound packets.

Signed-off-by: Frieder Schrempf <frieder@fris.de>
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 <frieder@fris.de>
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 <frieder@fris.de>
@IoTThinks
Copy link

I intended to do the same before.
But it is quite hard to power down NRF52 and be waken up by RX events. The power consumption will be around 6mA.
NRF52 does not handle Rising High well.
Your PR looks complex. May be due to this issue.

So I ended up just do some power saving trick to keep the power down to 8.5mA with few code changes only.
The MCU is still running.

Hope we can push the power down more for NRF52

@fschrempf
Copy link
Contributor Author

fschrempf commented Dec 17, 2025

But it is quite hard to power down NRF52 and be waken up by RX events.

Yes, there is no real sleep mode for NRF52 so either put the CPU in idle to save power or shut it down completely. The latter requires full reinit at wakeup.

The power consumption will be around 6mA.

In which case? With the MCU shut down and only the RF module running in RX mode? That would be in that ballpark, yes.

NRF52 does not handle Rising High well.

Sorry, I don't get what you mean here.

Your PR looks complex. May be due to this issue.

Actually it doesn't look complex to me at all. It's pretty straight forward. Instead of letting the CPU run continuously, it stops it and resumes it after the idle interval is over or a packet is received. What exactly looks complex to you?

So I ended up just do some power saving trick to keep the power down to 8.5mA with few code changes only.

What "trick" would that be? Do you mean waitForEvent() here? It doesn't work for me. I'm still seeing around 14 mA @ 3.3V with your PowerSaving07 branch.

And if it would work, it would do the same thing: put the CPU in idle, right? My approach does it in a platform-agnostic way, which is better IMHO.

@ngavars
Copy link
Contributor

ngavars commented Dec 17, 2025

This feature has been long overdue. I tested it a bit with Heltec t114 (no screen) repeater. With stock FW I am seeing around 12 mA idle current on my cheap usb power meter. It is actually quite good - it used to be around 17 mA with stock FW not so long ago.

Then I flashed FW from this branch and immediately went for "set idle.interval 10000". Now my cheapo usb meter shows current at 0 mA, which occasionally jumps briefly to 10 mA while idling. If I send a message from my companion then the repeater wakes, repeats and then goes back to idling. Now, the 0mA is probably just an artifact of my usb meter. I will try some additional tests tomorrow, but what I see so far - the power consumption at idle is noticeably lower and repeater still seems to be repeating messages and responding to admin commands etc

What is the intended reasonable idle interval? Something like 1 to 3 seconds?

@IoTThinks
Copy link

May be I will add the CLI for esp32 based boards.

Should we share the same CLI to enable/disable power saving to both esp32 and nrf52?

Like set powersave 1?

In your PR, you set the idle period? How about wake up period?

@IoTThinks
Copy link

@ngavars You have to measure the current at the battery cable by power meter.

A usbc, it needs to turn on unneccessary components like uart chip, led...

@fschrempf
Copy link
Contributor Author

May be I will add the CLI for esp32 based boards.
Should we share the same CLI to enable/disable power saving to both esp32 and nrf52?

In my opinion we should use the same approach for ESP32 and NRF52 altogether. You should be able to take my generic implementation and simply add the board.sleep() implementation for ESP32 to be called before the loop is halted.

Like set powersave 1?

What would that be good for? A single parameter that sets the length of the sleep/idle interval is enough. If set to zero (default) there is no change compared to the current implementation in the main branch. Repeater admins can then decide if they want to save power by increasing the interval.

In your PR, you set the idle period? How about wake up period?

The idle interval is the time the CPU is idling/sleeping. The wake interval is hardcoded to five seconds or three minutes after reboot or after CLI activity. I don't think this needs to be a parameter for the user to change.

@fschrempf
Copy link
Contributor Author

What is the intended reasonable idle interval? Something like 1 to 3 seconds?

The idle.interval setting is in seconds, so 10000 will give you around 2.8 hours. I'm currently running my test repeater with 1800 (30 minutes), but I'm not yet sure what would be a good value. If you have the value set very high, the automatic adverts of the repeater might be delayed accordingly (until the next wakeup happens). Apart from that I currently don't see any other downsides.

@fschrempf
Copy link
Contributor Author

@ngavars Thanks for testing by the way!

Copy link

@mtlynch mtlynch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just a user sharing my thoughts and have no authority in this project.

rtc_clock.tick();

if (the_mesh.getIdleInterval() && (millis() - active_timestamp > active_time_ms) &&
!the_mesh.getTotalOutboundCount()) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest reformatting this so that each clause is on its own line. Also, I think it's helpful practice to make operator precedence explicit rather than rely on the reader to remember C++ precedence rules, so suggest:

if (the_mesh.getIdleInterval() &&
      ((millis() - active_timestamp) > active_time_ms) &&
      !the_mesh.getTotalOutboundCount()) {
    radio_driver.blockTaskUntilRXEvent(the_mesh.getIdleInterval() * 1000);
    ...
  }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will try to improve the readibilty. Though this is what clang-format suggested using the project's configuration.

static char command[160];

#define ACTIVE_TIME_MS_INUSE 3 * 60 * 1000; // 3 minutes
#define ACTIVE_TIME_MS_IDLE 5 * 1000; // 5 seconds
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to use #define here instead of constexpr? We get more compile-time checks with constexpr.

constexpr unsigned long ACTIVE_TIME_MS_INUSE = 3 * 60 * 1000; // 3 minutes
constexpr unsigned long ACTIVE_TIME_MS_IDLE  = 5 * 1000;      // 5 seconds

If we do use #define, we need to remove the trailing semicolon or it becomes part of the macro, which is not what we want (i.e., unsigned long active_time_ms = ACTIVE_TIME_MS_INUSE; expands to unsigned long active_time_ms = 3 * 60 * 1000;; with two trailing semicolons.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I will change that. Actually I thought about it and then forgot. And semicolons are bogus of course.

uint8_t advert_loc_policy;
uint32_t discovery_mod_timestamp;
float adc_multiplier;
uint32_t idle_interval; // in seconds
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we make this idle_interval_seconds and not need the comment?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could and I thought about it. In the end I decided to follow the style of the existing variables.

Packet* obtainNewPacket();
void releasePacket(Packet* packet);
void sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis=0);
uint32_t getTotalOutboundCount() {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is exposing more data than the API client currently needs. Could it simplify to bool hasOutboundPackets() { ... }

That simplifies the callsite to:

if (the_mesh.getIdleInterval() &&
      ((millis() - active_timestamp) > active_time_ms) &&
      the_mesh.hasOutboundPacket()) {
    radio_driver.blockTaskUntilRXEvent(the_mesh.getIdleInterval() * 1000);
    ...
  }

It's easier to reason about negating a bool than negating a uint32_t.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, yes.

@fschrempf
Copy link
Contributor Author

I'm just a user sharing my thoughts and have no authority in this project.

Thanks for you review anyway! Very much appreciated!

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.

4 participants