Keep power-saving sleep within queued work deadlines

This commit is contained in:
mikecarper committed 2026-10-14 00:20:04 -07:00
1 parent 6b12f4c0a4
commit 32e920250a
10 files changed
+193 -26

No files matched your search

+1
View File
@@ -265,6 +265,7 @@ jobs:
python3 -B test/test_sensor_tracker_client.py -v
python3 -B test/test_sensor_tracker_gps.py -v
python3 -B test/test_sensor_tracker_sleep.py -v
python3 -B test/test_power_saving_deadlines.py -v
- name: Verify Companion contact and channel TX routing
run: |
+7 -6
View File
@@ -42,6 +42,7 @@
#include <helpers/FloodFilterPolicy.h>
#include <helpers/FloodRuleCLI.h>
#include <helpers/LazyPersistence.h>
#include <helpers/PowerManagementUtils.h>
#include <helpers/RegionNameUtils.h>
#if MESH_PACKET_LOGGING
#include <helpers/SerialPacketLog.h>
@@ -5149,12 +5150,12 @@ uint32_t MyMesh::limitSleepToMillisTimer(unsigned long timestamp, uint32_t sleep
if (!timestamp || sleep_secs == 0) {
return sleep_secs;
}
unsigned long now = millis();
if ((long)(now - timestamp) >= 0) {
const uint32_t now = millis();
if ((int32_t)(now - (uint32_t)timestamp) >= 0) {
return 0;
}
unsigned long remaining_ms = timestamp - now;
uint32_t remaining_secs = (remaining_ms + 999UL) / 1000UL;
const uint32_t remaining_ms = (uint32_t)timestamp - now;
uint32_t remaining_secs = mesh::power::powerSaveSleepSeconds(remaining_ms);
return remaining_secs < sleep_secs ? remaining_secs : sleep_secs;
}
@@ -5184,12 +5185,12 @@ uint32_t MyMesh::getPowerSaveSleepSeconds(uint32_t max_secs) const {
uint32_t sleep_secs = max_secs;
uint32_t queue_delay_ms;
if (getNextQueueWakeDelay(queue_delay_ms)) {
uint32_t queue_delay_secs = (queue_delay_ms + 999UL) / 1000UL;
uint32_t queue_delay_secs = mesh::power::powerSaveSleepSeconds(queue_delay_ms);
if (queue_delay_secs < sleep_secs) sleep_secs = queue_delay_secs;
}
uint32_t retry_delay_ms;
if (getNextRetryWakeDelay(retry_delay_ms)) {
uint32_t retry_delay_secs = (retry_delay_ms + 999UL) / 1000UL;
uint32_t retry_delay_secs = mesh::power::powerSaveSleepSeconds(retry_delay_ms);
if (retry_delay_secs < sleep_secs) sleep_secs = retry_delay_secs;
}
sleep_secs = limitSleepToMillisTimer(next_flood_advert, sleep_secs);
+7 -6
View File
@@ -32,6 +32,7 @@
#include <helpers/RoomClientPathCommand.h>
#include <helpers/RoomCatchUp.h>
#include <helpers/LazyPersistence.h>
#include <helpers/PowerManagementUtils.h>
#if MESH_PACKET_LOGGING
#include <helpers/SerialPacketLog.h>
#endif
@@ -4185,12 +4186,12 @@ uint32_t MyMesh::limitSleepToMillisTimer(unsigned long timestamp, uint32_t sleep
if (!timestamp || sleep_secs == 0) {
return sleep_secs;
}
unsigned long now = millis();
if ((long)(now - timestamp) >= 0) {
const uint32_t now = millis();
if ((int32_t)(now - (uint32_t)timestamp) >= 0) {
return 0;
}
unsigned long remaining_ms = timestamp - now;
uint32_t remaining_secs = (remaining_ms + 999UL) / 1000UL;
const uint32_t remaining_ms = (uint32_t)timestamp - now;
uint32_t remaining_secs = mesh::power::powerSaveSleepSeconds(remaining_ms);
return remaining_secs < sleep_secs ? remaining_secs : sleep_secs;
}
@@ -4202,12 +4203,12 @@ uint32_t MyMesh::getPowerSaveSleepSeconds(uint32_t max_secs) const {
uint32_t sleep_secs = max_secs;
uint32_t queue_delay_ms;
if (getNextQueueWakeDelay(queue_delay_ms)) {
uint32_t queue_delay_secs = (queue_delay_ms + 999UL) / 1000UL;
uint32_t queue_delay_secs = mesh::power::powerSaveSleepSeconds(queue_delay_ms);
if (queue_delay_secs < sleep_secs) sleep_secs = queue_delay_secs;
}
uint32_t retry_delay_ms;
if (getNextRetryWakeDelay(retry_delay_ms)) {
uint32_t retry_delay_secs = (retry_delay_ms + 999UL) / 1000UL;
uint32_t retry_delay_secs = mesh::power::powerSaveSleepSeconds(retry_delay_ms);
if (retry_delay_secs < sleep_secs) sleep_secs = retry_delay_secs;
}
if (acl.getNumClients() > 0) {
+8 -5
View File
@@ -5,6 +5,7 @@
#include <helpers/ClientLoginPersistence.h>
#include <helpers/ClientPathPersistence.h>
#include <helpers/LazyPersistence.h>
#include <helpers/PowerManagementUtils.h>
#include <helpers/RoutingPolicy.h>
#include <helpers/radiolib/RXPowerSaving.h>
#include <helpers/sensors/LPPDataHelpers.h>
@@ -1676,10 +1677,12 @@ uint32_t SensorMesh::limitSleepToMillisTimer(unsigned long timestamp,
uint32_t sleep_secs) const {
if (!timestamp || sleep_secs == 0) return sleep_secs;
unsigned long now = millis();
if ((long)(now - timestamp) >= 0) return 0;
const uint32_t now = millis();
if ((int32_t)(now - (uint32_t)timestamp) >= 0) return 0;
uint32_t remaining_secs = (timestamp - now + 999UL) / 1000UL;
// Keep the millis arithmetic 32-bit, including on native test hosts.
const uint32_t remaining_ms = (uint32_t)timestamp - now;
uint32_t remaining_secs = mesh::power::powerSaveSleepSeconds(remaining_ms);
return remaining_secs < sleep_secs ? remaining_secs : sleep_secs;
}
@@ -1725,11 +1728,11 @@ uint32_t SensorMesh::getPowerSaveSleepSeconds(uint32_t max_secs) const {
#endif
uint32_t wake_delay_ms;
if (getNextQueueWakeDelay(wake_delay_ms)) {
uint32_t wake_delay_secs = (wake_delay_ms + 999UL) / 1000UL;
uint32_t wake_delay_secs = mesh::power::powerSaveSleepSeconds(wake_delay_ms);
if (wake_delay_secs < sleep_secs) sleep_secs = wake_delay_secs;
}
if (getNextRetryWakeDelay(wake_delay_ms)) {
uint32_t wake_delay_secs = (wake_delay_ms + 999UL) / 1000UL;
uint32_t wake_delay_secs = mesh::power::powerSaveSleepSeconds(wake_delay_ms);
if (wake_delay_secs < sleep_secs) sleep_secs = wake_delay_secs;
}
+12
View File
@@ -5,6 +5,18 @@
namespace mesh {
namespace power {
inline uint32_t powerSaveSleepSeconds(uint32_t delay_ms) {
const uint32_t seconds = delay_ms / 1000UL;
#if defined(NRF52_PLATFORM) || defined(RP2040_PLATFORM) || defined(STM32_PLATFORM)
// These boards ignore seconds and idle until an interrupt. Keep sub-second
// idle eligible; periodic clock interrupts service the remaining deadline.
return seconds + (delay_ms % 1000UL != 0);
#else
// Timed sleepers must wake before a deadline, not round past its last fraction.
return seconds;
#endif
}
inline uint16_t medianVoltage(uint16_t a, uint16_t b, uint16_t c) {
if (a > b) { uint16_t t = a; a = b; b = t; }
if (b > c) { uint16_t t = b; b = c; c = t; }
+1
View File
@@ -24,6 +24,7 @@ FIXTURE = r'''
#include <cstring>
#include <new>
#include <helpers/WebConfigBatch.h>
#include <helpers/PowerManagementUtils.h>
#include <helpers/WiFiNetworkPowerPolicy.h>
#include <helpers/WirelessControl.h>
uint32_t now = 0;
+2 -1
View File
@@ -26,6 +26,7 @@ HARNESS = r'''
#include <cstdint>
#include <stdexcept>
#include <iostream>
#include <helpers/PowerManagementUtils.h>
#define WITH_BRIDGE 1
struct AbstractBridge { virtual bool isRunning() const = 0; };
struct MQTTBridge : AbstractBridge {
@@ -153,7 +154,7 @@ class LoggingSleepContractTest(unittest.TestCase):
.replace("@METHODS@", methods))
built = subprocess.run([
os.environ.get("CXX", "c++"), "-std=c++17", "-Wall", "-Wextra",
str(cpp), "-o", str(binary),
"-I", str(ROOT / "src"), str(cpp), "-o", str(binary),
], capture_output=True, text=True)
self.assertEqual(built.returncode, 0, built.stderr)
result = subprocess.run([str(binary)], capture_output=True, text=True)
+139
View File
@@ -0,0 +1,139 @@
"""Run real role deadlines while preserving timed sleep and interrupt idle."""
from pathlib import Path
import os
import subprocess
import tempfile
import unittest
from test_radio_receive_contract import method
ROOT = Path(__file__).resolve().parents[1]
HARNESS = r'''
#include <cassert>
#include <cstdint>
#include <initializer_list>
#include <helpers/PowerManagementUtils.h>
#define SENSOR_READ_INTERVAL_SECS 60
static uint32_t now_ms = 1000;
uint32_t millis() { return now_ms; }
struct Node {
bool pending = false, queue = false, retry = false;
uint32_t queue_ms = 0, retry_ms = 0;
struct { bool battery_alert_enabled = false; } _prefs;
struct { int getNumClients() const { return 0; } } acl;
unsigned long next_flood_advert = 0, next_local_advert = 0;
unsigned long dirty_contacts_expiry = 0, next_recent_repeater_sweep = 0;
unsigned long next_battery_alert_check = 0, next_push = 0;
unsigned long radio_apply_retry_at = 0, set_radio_at = 0, revert_radio_at = 0;
uint32_t last_read_time = 100;
struct Alert { unsigned long send_expiry = 0; } alert;
Alert* alert_tasks[1] = {&alert};
int num_alert_tasks = 0;
struct Clock { uint32_t getCurrentTime() const { return 100; } } clock;
const Clock* getRTCClock() const { return &clock; }
bool hasPendingWork() const { return pending; }
bool getNextQueueWakeDelay(uint32_t& out) const { out = queue_ms; return queue; }
bool getNextRetryWakeDelay(uint32_t& out) const { out = retry_ms; return retry; }
bool millisHasNowPassed(unsigned long at) const {
return int32_t(now_ms - uint32_t(at)) > 0;
}
uint32_t limitSleepToScheduledRadioWork(uint32_t seconds) const { return seconds; }
uint32_t limitSleepToMillisTimer(unsigned long, uint32_t) const;
uint32_t getPowerSaveSleepSeconds(uint32_t) const;
};
@METHODS@
int main() {
Node node;
assert(mesh::power::powerSaveSleepSeconds(0) == 0);
#if EXPECT_INTERRUPT_IDLE
assert(mesh::power::powerSaveSleepSeconds(UINT32_MAX) == 4294968U);
#else
assert(mesh::power::powerSaveSleepSeconds(UINT32_MAX) == 4294967U);
#endif
assert(node.getPowerSaveSleepSeconds(30) == 30);
assert(node.getPowerSaveSleepSeconds(0) == 0);
assert(node.limitSleepToMillisTimer(0, 30) == 30); // Disabled timer sentinel.
assert(node.limitSleepToMillisTimer(1001, 0) == 0);
node.pending = true;
assert(node.getPowerSaveSleepSeconds(30) == 0);
node.pending = false;
for (uint32_t now : {1000U, UINT32_MAX - 15U}) {
now_ms = now;
// Timed sleep ends before the deadline. Event-driven boards instead permit
// interrupt idle for every positive remainder, including across rollover.
for (uint32_t ms : {0U, 1U, 999U, 1000U, 1001U, 1999U, 2000U,
2500U, 29999U, 30000U, 30001U, 60000U}) {
const uint32_t whole_seconds = ms / 1000;
#if EXPECT_INTERRUPT_IDLE
// Event-driven boards ignore seconds and wake at the next IRQ.
const uint32_t idle_seconds = whole_seconds + (ms % 1000 != 0);
#else
const uint32_t idle_seconds = whole_seconds;
#endif
const uint32_t expected = idle_seconds < 30 ? idle_seconds : 30;
node.queue = true; node.queue_ms = ms;
assert(node.getPowerSaveSleepSeconds(30) == expected);
node.queue = false; node.retry = true; node.retry_ms = ms;
assert(node.getPowerSaveSleepSeconds(30) == expected);
node.retry = false;
const uint32_t deadline = now + ms;
assert(deadline != 0); // Zero has a separate intentional sentinel meaning.
assert(node.limitSleepToMillisTimer(deadline, 30) == expected);
node.next_flood_advert = deadline;
assert(node.getPowerSaveSleepSeconds(30) == expected);
node.next_flood_advert = 0;
}
assert(node.limitSleepToMillisTimer(uint32_t(now - 1), 30) == 0);
node.queue = true; node.queue_ms = 2500;
node.retry = true; node.retry_ms = 1001;
#if EXPECT_INTERRUPT_IDLE
assert(node.getPowerSaveSleepSeconds(30) == 2); // Interrupt idle is permitted.
#else
assert(node.getPowerSaveSleepSeconds(30) == 1); // Earliest timed owner wins.
#endif
node.dirty_contacts_expiry = uint32_t(now + 999);
#if EXPECT_INTERRUPT_IDLE
assert(node.getPowerSaveSleepSeconds(30) == 1);
#else
assert(node.getPowerSaveSleepSeconds(30) == 0);
#endif
node.dirty_contacts_expiry = 0;
node.queue = node.retry = false;
}
}
'''
class PowerSavingDeadlineTests(unittest.TestCase):
def test_real_role_deadlines_preserve_timed_and_interrupt_idle(self):
with tempfile.TemporaryDirectory(prefix='meshcore-sleep-deadlines-') as directory:
work = Path(directory)
for role, filename, name in (
('repeater', 'examples/simple_repeater/MyMesh.cpp', 'MyMesh'),
('room', 'examples/simple_room_server/MyMesh.cpp', 'MyMesh'),
('sensor', 'examples/simple_sensor/SensorMesh.cpp', 'SensorMesh')):
for platform in ('', 'NRF52_PLATFORM', 'RP2040_PLATFORM', 'STM32_PLATFORM'):
with self.subTest(role=role, platform=platform or 'timed'):
source = (ROOT / filename).read_text()
methods = '\n'.join(method(source, signature) for signature in (
f'uint32_t {name}::limitSleepToMillisTimer(',
f'uint32_t {name}::getPowerSaveSleepSeconds('))
methods = methods.replace(name + '::', 'Node::')
cpp, exe = work / (role + '.cpp'), work / role
cpp.write_text(HARNESS.replace('@METHODS@', methods))
sanitizers = [] if os.name == 'nt' else [
'-fsanitize=address,undefined', '-fno-sanitize-recover=all',
'-fno-pie', '-no-pie']
built = subprocess.run([os.environ.get('CXX', 'g++'), '-std=c++17',
'-Wall', '-Wextra', '-Werror', *sanitizers,
'-DEXPECT_INTERRUPT_IDLE=' + ('1' if platform else '0'),
*(['-D' + platform + '=1'] if platform else []), '-I', str(ROOT / 'src'),
str(cpp), '-o', str(exe)], capture_output=True, text=True)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
ran = subprocess.run([str(exe)], capture_output=True, text=True)
self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr)
if __name__ == '__main__':
unittest.main()
+2 -1
View File
@@ -18,6 +18,7 @@ HARNESS = r'''
#include <cstring>
#include <new>
#include <limits>
#include <helpers/PowerManagementUtils.h>
#define WITH_BRIDGE 1
#define WITH_RS232_BRIDGE Serial2
#define WITH_RS232_BRIDGE_UART 2
@@ -310,7 +311,7 @@ class RS232HeartbeatOwnerTests(unittest.TestCase):
flags.append("-DWITH_MQTT_BRIDGE=1")
built = subprocess.run([compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
*flags, str(cpp), "-o", str(binary)], capture_output=True, text=True, timeout=60)
*flags, "-I", str(ROOT / "src"), str(cpp), "-o", str(binary)], capture_output=True, text=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
checked = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
+14 -7
View File
@@ -15,6 +15,7 @@ ROOT = Path(__file__).resolve().parents[1]
DEADLINES = r'''
#include <Arduino.h>
#include <cassert>
#include <helpers/PowerManagementUtils.h>
#define SENSOR_READ_INTERVAL_SECS 60
bool logging=false;
namespace mesh {bool isUsbLoggingWatchdogArmed(){return logging;}}
@@ -98,13 +99,18 @@ int main(){
&radio_driver.calibration,&s.queue_due,&s.retry_due}){
*due=true;assert(s.hasPendingWork()&&s.getPowerSaveSleepSeconds(30)==0);*due=false;
}
s.queue_delay_set=true;s.queue_delay=2500;assert(s.getPowerSaveSleepSeconds(30)==3);
#if defined(NRF52_PLATFORM) || defined(RP2040_PLATFORM) || defined(STM32_PLATFORM)
constexpr unsigned queue_idle=3,retry_idle=2;
#else
constexpr unsigned queue_idle=2,retry_idle=1;
#endif
s.queue_delay_set=true;s.queue_delay=2500;assert(s.getPowerSaveSleepSeconds(30)==queue_idle);
#if MESH_ENABLE_SENSOR_TRACKER
assert(s.trackerNeedsRadio());
#endif
s.queue_delay_set=false;s.retry_delay_set=true;s.retry_delay=1500;
assert(s.getPowerSaveSleepSeconds(30)==2);s.retry_delay_set=false;
s.dirty_contacts_expiry=now_ms+1001;assert(s.getPowerSaveSleepSeconds(30)==2);
assert(s.getPowerSaveSleepSeconds(30)==retry_idle);s.retry_delay_set=false;
s.dirty_contacts_expiry=now_ms+1001;assert(s.getPowerSaveSleepSeconds(30)==retry_idle);
s.dirty_contacts_expiry=0;s.last_read_time=45;
assert(s.getPowerSaveSleepSeconds(30)==5);s.last_read_time=40;
assert(s.hasPendingWork()&&s.getPowerSaveSleepSeconds(30)==0);
@@ -245,7 +251,7 @@ class SensorTrackerSleepTest(unittest.TestCase):
"-Wno-unused-parameter", "-Wno-unused-function", "-Wno-unused-variable",
*(["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
"-fno-pie", "-no-pie"] if sys.platform.startswith("linux") else []),
*[f"-D{define}" for define in defines], "-I", str(path),
*[f"-D{define}" for define in defines], "-I", str(path), "-I", str(ROOT / "src"),
str(cpp), "-o", str(binary),
], capture_output=True, text=True, timeout=60)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
@@ -261,9 +267,10 @@ class SensorTrackerSleepTest(unittest.TestCase):
methods += "\n#if MESH_ENABLE_SENSOR_TRACKER\n" + extract_braced(
tracker, "bool SensorMesh::trackerNeedsRadio(") + "\n#endif\n"
for enabled in (0, 1):
with self.subTest(enabled=enabled):
self.compile_run(DEADLINES.replace("@METHODS@", methods),
(f"MESH_ENABLE_SENSOR_TRACKER={enabled}",))
for platform in ((), ("NRF52_PLATFORM=1",)):
with self.subTest(enabled=enabled, platform=platform):
self.compile_run(DEADLINES.replace("@METHODS@", methods),
(f"MESH_ENABLE_SENSOR_TRACKER={enabled}", *platform))
def test_actual_main_timer_allocation_queue_failure_and_platform_paths(self):
main = (ROOT / "examples/simple_sensor/main.cpp").read_text()