diff --git a/docs/power_saving_diagnostics.md b/docs/power_saving_diagnostics.md index b37f4c753..49ac84eb8 100644 --- a/docs/power_saving_diagnostics.md +++ b/docs/power_saving_diagnostics.md @@ -81,6 +81,10 @@ AGC resets still request recalibration. Dual-profile scanning keeps its existing timing and calibration policy. ESP32 timed sleep uses a millisecond timer capped at the next sample deadline. +Repeater, Room and Sensor also preserve millisecond queue, retry, RS232 standby, +scheduled-radio and maintenance deadlines, so the last fraction of a second +does not become a busy-wait tail. A future sub-second deadline is not reported +as blocked work; a deadline that is already due still prevents sleep. nRF52, RP2040 and STM32 retain their interrupt-idle wake paths. Other sleep guards, including USB/logging, packet queues and bridges, still apply. These changes have host scheduling tests and firmware build coverage; their diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 61694ed27..e3d9a3e47 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -5148,82 +5148,83 @@ bool MyMesh::hasScheduledRadioWorkDue() const { || millisHasNowPassed(next_scheduled_radio_check_at)); } -uint32_t MyMesh::limitSleepToMillisTimer(unsigned long timestamp, uint32_t sleep_secs) const { - if (!timestamp || sleep_secs == 0) { - return sleep_secs; +uint32_t MyMesh::limitSleepToMillisTimer(unsigned long timestamp, uint32_t sleep_ms) const { + if (!timestamp || sleep_ms == 0) { + return sleep_ms; } const uint32_t now = millis(); if ((int32_t)(now - (uint32_t)timestamp) >= 0) { return 0; } 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; + return remaining_ms < sleep_ms ? remaining_ms : sleep_ms; } -uint32_t MyMesh::limitSleepToScheduledRadioWork(uint32_t sleep_secs) const { - sleep_secs = limitSleepToMillisTimer(next_scheduled_radio_check_at, sleep_secs); +uint32_t MyMesh::limitSleepToScheduledRadioWork(uint32_t sleep_ms) const { + sleep_ms = limitSleepToMillisTimer(next_scheduled_radio_check_at, sleep_ms); const uint64_t current_uptime_millis = uptime_millis + (uint32_t)(millis() - last_millis); for (int i = 0; i < MAX_SCHEDULED_RADIO_SETTINGS; i++) { const ScheduledRadioSetting& setting = scheduled_radio_settings[i]; if (!setting.active || !setting.temporary) continue; - const uint32_t hard_end_seconds = - mesh::TempRadioLeaseDeadline::secondsUntil( + const uint32_t hard_end_ms = + mesh::TempRadioLeaseDeadline::millisecondsUntil( current_uptime_millis, setting.hard_end_uptime_millis); - if (hard_end_seconds < sleep_secs) sleep_secs = hard_end_seconds; + if (hard_end_ms < sleep_ms) sleep_ms = hard_end_ms; } if (scheduled_radio_retry_at && !millisHasNowPassed(scheduled_radio_retry_at)) { - sleep_secs = limitSleepToMillisTimer(scheduled_radio_retry_at, sleep_secs); + sleep_ms = limitSleepToMillisTimer(scheduled_radio_retry_at, sleep_ms); } - return sleep_secs; + return sleep_ms; } uint32_t MyMesh::getPowerSaveSleepSeconds(uint32_t max_secs) const { + return mesh::power::powerSaveSleepSeconds(getPowerSaveSleepMilliseconds( + mesh::power::secondsToSleepMilliseconds(max_secs))); +} + +uint32_t MyMesh::getPowerSaveSleepMilliseconds(uint32_t max_ms) const { using mesh::powerdiag::mask; using mesh::powerdiag::Reason; - const mesh::powerdiag::Mask pending = max_secs ? pendingWorkReason() : mask(Reason::NoWake); + const mesh::powerdiag::Mask pending = max_ms ? pendingWorkReason() : mask(Reason::NoWake); if (pending) { if (mesh::powerdiag::enabled()) mesh::powerdiag::decision(pending, millis()); return 0; } mesh::powerdiag::Mask blocked = 0; - uint32_t sleep_secs = max_secs; + uint32_t sleep_ms = max_ms; uint32_t queue_delay_ms; if (getNextQueueWakeDelay(queue_delay_ms)) { - uint32_t queue_delay_secs = mesh::power::powerSaveSleepSeconds(queue_delay_ms); - if (queue_delay_secs < sleep_secs) sleep_secs = queue_delay_secs; - if (queue_delay_secs == 0) blocked |= mask(Reason::Queue); + if (queue_delay_ms < sleep_ms) sleep_ms = queue_delay_ms; + if (queue_delay_ms == 0) blocked |= mask(Reason::Queue); } uint32_t retry_delay_ms; if (getNextRetryWakeDelay(retry_delay_ms)) { - uint32_t retry_delay_secs = mesh::power::powerSaveSleepSeconds(retry_delay_ms); - if (retry_delay_secs < sleep_secs) sleep_secs = retry_delay_secs; - if (retry_delay_secs == 0) blocked |= mask(Reason::Retry); + if (retry_delay_ms < sleep_ms) sleep_ms = retry_delay_ms; + if (retry_delay_ms == 0) blocked |= mask(Reason::Retry); } - const uint32_t before_scheduled = sleep_secs; - sleep_secs = limitSleepToMillisTimer(next_flood_advert, sleep_secs); - sleep_secs = limitSleepToMillisTimer(next_local_advert, sleep_secs); - sleep_secs = limitSleepToMillisTimer(dirty_contacts_expiry, sleep_secs); - sleep_secs = limitSleepToMillisTimer(next_recent_repeater_sweep, sleep_secs); + const uint32_t before_scheduled = sleep_ms; + sleep_ms = limitSleepToMillisTimer(next_flood_advert, sleep_ms); + sleep_ms = limitSleepToMillisTimer(next_local_advert, sleep_ms); + sleep_ms = limitSleepToMillisTimer(dirty_contacts_expiry, sleep_ms); + sleep_ms = limitSleepToMillisTimer(next_recent_repeater_sweep, sleep_ms); if (_prefs.battery_alert_enabled) { - sleep_secs = limitSleepToMillisTimer(next_battery_alert_check, sleep_secs); + sleep_ms = limitSleepToMillisTimer(next_battery_alert_check, sleep_ms); } - sleep_secs = limitSleepToScheduledRadioWork(sleep_secs); - if (before_scheduled > 0 && sleep_secs == 0) blocked |= mask(Reason::Scheduled); + sleep_ms = limitSleepToScheduledRadioWork(sleep_ms); + if (before_scheduled > 0 && sleep_ms == 0) blocked |= mask(Reason::Scheduled); #if defined(WITH_RS232_BRIDGE) if (bridge && rs232BridgeEnabled()) { - // Timed sleepers must not overshoot; interrupt-idle boards can still - // sleep with a positive sub-second remainder and wake on their clock IRQ. - const uint32_t bridge_sleep_secs = mesh::power::powerSaveSleepSeconds( - bridge->powerWakeDelayMs(millis())); - if (bridge_sleep_secs < sleep_secs) sleep_secs = bridge_sleep_secs; - if (bridge_sleep_secs == 0) blocked |= mask(Reason::BridgeWake); + // Preserve the exact rendezvous deadline; the board selects timed sleep + // or interrupt idle without rounding away its sub-second remainder. + const uint32_t bridge_sleep_ms = bridge->powerWakeDelayMs(millis()); + if (bridge_sleep_ms < sleep_ms) sleep_ms = bridge_sleep_ms; + if (bridge_sleep_ms == 0) blocked |= mask(Reason::BridgeWake); } #endif if (blocked && mesh::powerdiag::enabled()) mesh::powerdiag::decision(blocked, millis()); - return sleep_secs; + return sleep_ms; } void MyMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins, uint16_t preamble) { diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 5e32f5859..89e44b5d6 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -942,8 +942,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks void formatClockSyncTable(char* reply, size_t reply_len) const; void formatClockSyncSampleDetail(int index, char* reply, size_t reply_len) const; bool hasScheduledRadioWorkDue() const; - uint32_t limitSleepToMillisTimer(unsigned long timestamp, uint32_t sleep_secs) const; - uint32_t limitSleepToScheduledRadioWork(uint32_t sleep_secs) const; + uint32_t limitSleepToMillisTimer(unsigned long timestamp, uint32_t sleep_ms) const; + uint32_t limitSleepToScheduledRadioWork(uint32_t sleep_ms) const; bool hasStartedScheduledTempRadio() const; int findFreeScheduledRadioSlot() const; int countScheduledRadioSettings(bool temporary) const; @@ -1236,6 +1236,7 @@ public: #endif uint32_t getPowerSaveSleepSeconds(uint32_t max_secs) const; + uint32_t getPowerSaveSleepMilliseconds(uint32_t max_ms) const; #if defined(WITH_BRIDGE) diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index d40e80538..22b1b8ee4 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -646,29 +646,29 @@ void loop() { can_power_save = can_power_save && sleep_guard(!user_btn.needsPolling(), mesh::powerdiag::Reason::Button); #endif if (can_power_save) { - uint32_t sleep_secs = the_mesh.getPowerSaveSleepSeconds(30); + uint32_t sleep_ms = the_mesh.getPowerSaveSleepMilliseconds(30000); uint32_t noise_floor_delay_ms = 0; const bool noise_floor_wait = the_mesh.getNextNoiseFloorWakeDelay(noise_floor_delay_ms); + if (noise_floor_wait && noise_floor_delay_ms < sleep_ms) sleep_ms = noise_floor_delay_ms; if (noise_floor_wait && noise_floor_delay_ms == 0) { if (mesh::powerdiag::enabled()) mesh::powerdiag::decision(mesh::powerdiag::mask(mesh::powerdiag::Reason::Calibration), millis()); - sleep_secs = 0; + sleep_ms = 0; } #ifdef HAS_EXTERNAL_WATCHDOG - if (sleep_secs > 0) external_watchdog.feed(); + if (sleep_ms > 0) external_watchdog.feed(); #endif #if defined(NRF52_PLATFORM) - if (sleep_secs > 0) { + if (sleep_ms > 0) { if (mesh::powerdiag::enabled()) mesh::powerdiag::decision(0, millis()); - if (noise_floor_wait) board.sleepMilliseconds(mesh::power::limitSleepMilliseconds(sleep_secs, noise_floor_delay_ms)); + if (noise_floor_wait) board.sleepMilliseconds(sleep_ms); else board.sleep(0); // nrf ignores seconds param, sleeps whenever possible } #else - if (sleep_secs > 0 && the_mesh.millisHasNowPassed(POWERSAVING_FIRSTSLEEP_SECS * 1000)) { // To check if it is time to sleep + if (sleep_ms > 0 && the_mesh.millisHasNowPassed(POWERSAVING_FIRSTSLEEP_SECS * 1000)) { // To check if it is time to sleep if (mesh::powerdiag::enabled()) mesh::powerdiag::decision(0, millis()); - if (noise_floor_wait) board.sleepMilliseconds(mesh::power::limitSleepMilliseconds(sleep_secs, noise_floor_delay_ms)); - else board.sleep(sleep_secs); // Wake for scheduled jobs or a LoRa packet. - } else if (sleep_secs > 0 && mesh::powerdiag::enabled()) { + board.sleepMilliseconds(sleep_ms); // Wake for scheduled jobs or a LoRa packet. + } else if (sleep_ms > 0 && mesh::powerdiag::enabled()) { mesh::powerdiag::decision(mesh::powerdiag::mask(mesh::powerdiag::Reason::Startup), millis()); } #endif diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 2ba5b20b9..c42a07375 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -4186,59 +4186,61 @@ bool MyMesh::isMillisTimerDue(unsigned long timestamp) const { return timestamp && millisHasNowPassed(timestamp); } -uint32_t MyMesh::limitSleepToMillisTimer(unsigned long timestamp, uint32_t sleep_secs) const { - if (!timestamp || sleep_secs == 0) { - return sleep_secs; +uint32_t MyMesh::limitSleepToMillisTimer(unsigned long timestamp, uint32_t sleep_ms) const { + if (!timestamp || sleep_ms == 0) { + return sleep_ms; } const uint32_t now = millis(); if ((int32_t)(now - (uint32_t)timestamp) >= 0) { return 0; } 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; + return remaining_ms < sleep_ms ? remaining_ms : sleep_ms; } uint32_t MyMesh::getPowerSaveSleepSeconds(uint32_t max_secs) const { + return mesh::power::powerSaveSleepSeconds(getPowerSaveSleepMilliseconds( + mesh::power::secondsToSleepMilliseconds(max_secs))); +} + +uint32_t MyMesh::getPowerSaveSleepMilliseconds(uint32_t max_ms) const { using mesh::powerdiag::mask; using mesh::powerdiag::Reason; - const mesh::powerdiag::Mask pending = max_secs ? pendingWorkReason() : mask(Reason::NoWake); + const mesh::powerdiag::Mask pending = max_ms ? pendingWorkReason() : mask(Reason::NoWake); if (pending) { if (mesh::powerdiag::enabled()) mesh::powerdiag::decision(pending, millis()); return 0; } mesh::powerdiag::Mask blocked = 0; - uint32_t sleep_secs = max_secs; + uint32_t sleep_ms = max_ms; uint32_t queue_delay_ms; if (getNextQueueWakeDelay(queue_delay_ms)) { - uint32_t queue_delay_secs = mesh::power::powerSaveSleepSeconds(queue_delay_ms); - if (queue_delay_secs < sleep_secs) sleep_secs = queue_delay_secs; - if (queue_delay_secs == 0) blocked |= mask(Reason::Queue); + if (queue_delay_ms < sleep_ms) sleep_ms = queue_delay_ms; + if (queue_delay_ms == 0) blocked |= mask(Reason::Queue); } uint32_t retry_delay_ms; if (getNextRetryWakeDelay(retry_delay_ms)) { - uint32_t retry_delay_secs = mesh::power::powerSaveSleepSeconds(retry_delay_ms); - if (retry_delay_secs < sleep_secs) sleep_secs = retry_delay_secs; - if (retry_delay_secs == 0) blocked |= mask(Reason::Retry); + if (retry_delay_ms < sleep_ms) sleep_ms = retry_delay_ms; + if (retry_delay_ms == 0) blocked |= mask(Reason::Retry); } - const uint32_t before_scheduled = sleep_secs; + const uint32_t before_scheduled = sleep_ms; if (acl.getNumClients() > 0) { - sleep_secs = limitSleepToMillisTimer(next_push, sleep_secs); + sleep_ms = limitSleepToMillisTimer(next_push, sleep_ms); } - sleep_secs = limitSleepToMillisTimer(next_flood_advert, sleep_secs); - sleep_secs = limitSleepToMillisTimer(next_local_advert, sleep_secs); + sleep_ms = limitSleepToMillisTimer(next_flood_advert, sleep_ms); + sleep_ms = limitSleepToMillisTimer(next_local_advert, sleep_ms); const bool radio_apply_backoff = radio_apply_retry_at && !millisHasNowPassed(radio_apply_retry_at); if (radio_apply_backoff) { - sleep_secs = limitSleepToMillisTimer(radio_apply_retry_at, sleep_secs); + sleep_ms = limitSleepToMillisTimer(radio_apply_retry_at, sleep_ms); } else { - sleep_secs = limitSleepToMillisTimer(set_radio_at, sleep_secs); - sleep_secs = limitSleepToMillisTimer(revert_radio_at, sleep_secs); + sleep_ms = limitSleepToMillisTimer(set_radio_at, sleep_ms); + sleep_ms = limitSleepToMillisTimer(revert_radio_at, sleep_ms); } - sleep_secs = limitSleepToMillisTimer(dirty_contacts_expiry, sleep_secs); - if (before_scheduled > 0 && sleep_secs == 0) blocked |= mask(Reason::Scheduled); + sleep_ms = limitSleepToMillisTimer(dirty_contacts_expiry, sleep_ms); + if (before_scheduled > 0 && sleep_ms == 0) blocked |= mask(Reason::Scheduled); if (blocked && mesh::powerdiag::enabled()) mesh::powerdiag::decision(blocked, millis()); - return sleep_secs; + return sleep_ms; } // To check if there is pending work diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index fbdad0604..f1c1799ee 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -1041,6 +1041,7 @@ public: void buildStatsJson(char* buf, size_t buf_size) override; #endif uint32_t getPowerSaveSleepSeconds(uint32_t max_secs) const; + uint32_t getPowerSaveSleepMilliseconds(uint32_t max_ms) const; // To check if there is pending work bool hasPendingWork() const; @@ -1048,5 +1049,5 @@ public: private: bool isMillisTimerDue(unsigned long timestamp) const; - uint32_t limitSleepToMillisTimer(unsigned long timestamp, uint32_t sleep_secs) const; + uint32_t limitSleepToMillisTimer(unsigned long timestamp, uint32_t sleep_ms) const; }; diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index 66d45fb13..f68b89527 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -418,29 +418,29 @@ void loop() { can_power_save = can_power_save && sleep_guard(!user_btn.needsPolling(), mesh::powerdiag::Reason::Button); #endif if (can_power_save) { - uint32_t sleep_secs = the_mesh.getPowerSaveSleepSeconds(30); + uint32_t sleep_ms = the_mesh.getPowerSaveSleepMilliseconds(30000); uint32_t noise_floor_delay_ms = 0; const bool noise_floor_wait = the_mesh.getNextNoiseFloorWakeDelay(noise_floor_delay_ms); + if (noise_floor_wait && noise_floor_delay_ms < sleep_ms) sleep_ms = noise_floor_delay_ms; if (noise_floor_wait && noise_floor_delay_ms == 0) { if (mesh::powerdiag::enabled()) mesh::powerdiag::decision(mesh::powerdiag::mask(mesh::powerdiag::Reason::Calibration), millis()); - sleep_secs = 0; + sleep_ms = 0; } #ifdef HAS_EXTERNAL_WATCHDOG - if (sleep_secs > 0) external_watchdog.feed(); + if (sleep_ms > 0) external_watchdog.feed(); #endif #if defined(NRF52_PLATFORM) - if (sleep_secs > 0) { + if (sleep_ms > 0) { if (mesh::powerdiag::enabled()) mesh::powerdiag::decision(0, millis()); - if (noise_floor_wait) board.sleepMilliseconds(mesh::power::limitSleepMilliseconds(sleep_secs, noise_floor_delay_ms)); + if (noise_floor_wait) board.sleepMilliseconds(sleep_ms); else board.sleep(0); // nrf ignores seconds param, sleeps whenever possible } #else - if (sleep_secs > 0 && the_mesh.millisHasNowPassed(POWERSAVING_FIRSTSLEEP_SECS * 1000)) { // To check if it is time to sleep + if (sleep_ms > 0 && the_mesh.millisHasNowPassed(POWERSAVING_FIRSTSLEEP_SECS * 1000)) { // To check if it is time to sleep if (mesh::powerdiag::enabled()) mesh::powerdiag::decision(0, millis()); - if (noise_floor_wait) board.sleepMilliseconds(mesh::power::limitSleepMilliseconds(sleep_secs, noise_floor_delay_ms)); - else board.sleep(sleep_secs); // Wake for scheduled jobs or a LoRa packet. - } else if (sleep_secs > 0 && mesh::powerdiag::enabled()) { + board.sleepMilliseconds(sleep_ms); // Wake for scheduled jobs or a LoRa packet. + } else if (sleep_ms > 0 && mesh::powerdiag::enabled()) { mesh::powerdiag::decision(mesh::powerdiag::mask(mesh::powerdiag::Reason::Startup), millis()); } #endif diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 363b7cd33..c001e6af5 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -1702,16 +1702,15 @@ bool SensorMesh::isMillisTimerDue(unsigned long timestamp) const { } uint32_t SensorMesh::limitSleepToMillisTimer(unsigned long timestamp, - uint32_t sleep_secs) const { - if (!timestamp || sleep_secs == 0) return sleep_secs; + uint32_t sleep_ms) const { + if (!timestamp || sleep_ms == 0) return sleep_ms; const uint32_t now = millis(); if ((int32_t)(now - (uint32_t)timestamp) >= 0) return 0; // 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; + return remaining_ms < sleep_ms ? remaining_ms : sleep_ms; } bool SensorMesh::hasPendingWork() const { @@ -1753,16 +1752,21 @@ mesh::powerdiag::Mask SensorMesh::pendingWorkReason() const { } uint32_t SensorMesh::getPowerSaveSleepSeconds(uint32_t max_secs) const { + return mesh::power::powerSaveSleepSeconds(getPowerSaveSleepMilliseconds( + mesh::power::secondsToSleepMilliseconds(max_secs))); +} + +uint32_t SensorMesh::getPowerSaveSleepMilliseconds(uint32_t max_ms) const { using mesh::powerdiag::mask; using mesh::powerdiag::Reason; - const mesh::powerdiag::Mask pending = max_secs ? pendingWorkReason() : mask(Reason::NoWake); + const mesh::powerdiag::Mask pending = max_ms ? pendingWorkReason() : mask(Reason::NoWake); if (pending) { if (mesh::powerdiag::enabled()) mesh::powerdiag::decision(pending, millis()); return 0; } mesh::powerdiag::Mask blocked = 0; - uint32_t sleep_secs = max_secs; + uint32_t sleep_ms = max_ms; #if MESH_ENABLE_SENSOR_TRACKER if (tracker.enabled && !tracker_storage_fault) { const uint32_t now = getRTCClock()->getCurrentTime(); @@ -1770,56 +1774,53 @@ uint32_t SensorMesh::getPowerSaveSleepSeconds(uint32_t max_secs) const { if (mesh::powerdiag::enabled()) mesh::powerdiag::decision(mask(Reason::Tracker), millis()); return 0; } - const uint32_t until_check = tracker_next_check - now; - if (until_check < sleep_secs) sleep_secs = until_check; + const uint32_t until_check = mesh::power::secondsToSleepMilliseconds(tracker_next_check - now); + if (until_check < sleep_ms) sleep_ms = until_check; } #endif uint32_t wake_delay_ms; if (getNextQueueWakeDelay(wake_delay_ms)) { - uint32_t wake_delay_secs = mesh::power::powerSaveSleepSeconds(wake_delay_ms); - if (wake_delay_secs < sleep_secs) sleep_secs = wake_delay_secs; - if (wake_delay_secs == 0) blocked |= mask(Reason::Queue); + if (wake_delay_ms < sleep_ms) sleep_ms = wake_delay_ms; + if (wake_delay_ms == 0) blocked |= mask(Reason::Queue); } #if MESH_ENABLE_SENSOR_TRACKER && MESH_ENABLE_TX_BEACONS // The normal queue query omits RF maintenance while radio_available is // false. A tracker's deliberate RF sleep must not hide its next beacon. if (tracker.enabled && getNextBeaconWakeDelay(wake_delay_ms)) { - uint32_t wake_delay_secs = mesh::power::powerSaveSleepSeconds(wake_delay_ms); - if (wake_delay_secs < sleep_secs) sleep_secs = wake_delay_secs; - if (wake_delay_secs == 0) blocked |= mask(Reason::Queue); + if (wake_delay_ms < sleep_ms) sleep_ms = wake_delay_ms; + if (wake_delay_ms == 0) blocked |= mask(Reason::Queue); } #endif if (getNextRetryWakeDelay(wake_delay_ms)) { - uint32_t wake_delay_secs = mesh::power::powerSaveSleepSeconds(wake_delay_ms); - if (wake_delay_secs < sleep_secs) sleep_secs = wake_delay_secs; - if (wake_delay_secs == 0) blocked |= mask(Reason::Retry); + if (wake_delay_ms < sleep_ms) sleep_ms = wake_delay_ms; + if (wake_delay_ms == 0) blocked |= mask(Reason::Retry); } - const uint32_t before_scheduled = sleep_secs; - sleep_secs = limitSleepToMillisTimer(next_flood_advert, sleep_secs); - sleep_secs = limitSleepToMillisTimer(next_local_advert, sleep_secs); - sleep_secs = limitSleepToMillisTimer(dirty_contacts_expiry, sleep_secs); + const uint32_t before_scheduled = sleep_ms; + sleep_ms = limitSleepToMillisTimer(next_flood_advert, sleep_ms); + sleep_ms = limitSleepToMillisTimer(next_local_advert, sleep_ms); + sleep_ms = limitSleepToMillisTimer(dirty_contacts_expiry, sleep_ms); if (num_alert_tasks > 0) { - sleep_secs = limitSleepToMillisTimer(alert_tasks[0]->send_expiry, - sleep_secs); + sleep_ms = limitSleepToMillisTimer(alert_tasks[0]->send_expiry, + sleep_ms); } const bool radio_apply_backoff = radio_apply_retry_at && !millisHasNowPassed(radio_apply_retry_at); if (radio_apply_backoff) { - sleep_secs = limitSleepToMillisTimer(radio_apply_retry_at, sleep_secs); + sleep_ms = limitSleepToMillisTimer(radio_apply_retry_at, sleep_ms); } else { - sleep_secs = limitSleepToMillisTimer(set_radio_at, sleep_secs); - sleep_secs = limitSleepToMillisTimer(revert_radio_at, sleep_secs); + sleep_ms = limitSleepToMillisTimer(set_radio_at, sleep_ms); + sleep_ms = limitSleepToMillisTimer(revert_radio_at, sleep_ms); } uint32_t now_secs = getRTCClock()->getCurrentTime(); uint32_t sensor_due = last_read_time + SENSOR_READ_INTERVAL_SECS; if (now_secs < sensor_due) { - uint32_t sensor_delay = sensor_due - now_secs; - if (sensor_delay < sleep_secs) sleep_secs = sensor_delay; + uint32_t sensor_delay = mesh::power::secondsToSleepMilliseconds(sensor_due - now_secs); + if (sensor_delay < sleep_ms) sleep_ms = sensor_delay; } - if (before_scheduled > 0 && sleep_secs == 0) blocked |= mask(Reason::Scheduled); + if (before_scheduled > 0 && sleep_ms == 0) blocked |= mask(Reason::Scheduled); if (blocked && mesh::powerdiag::enabled()) mesh::powerdiag::decision(blocked, millis()); - return sleep_secs; + return sleep_ms; } diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index c19a612a2..99ab81c0e 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -87,6 +87,7 @@ public: const char* getNodeName() { return _prefs.node_name; } NodePrefs* getNodePrefs() { return &_prefs; } uint32_t getPowerSaveSleepSeconds(uint32_t max_secs) const; + uint32_t getPowerSaveSleepMilliseconds(uint32_t max_ms) const; bool isTrackerModeEnabled() const { #if MESH_ENABLE_SENSOR_TRACKER return tracker.enabled; @@ -299,7 +300,7 @@ private: bool applySavedRadioParams(); bool isMillisTimerDue(unsigned long timestamp) const; uint32_t limitSleepToMillisTimer(unsigned long timestamp, - uint32_t sleep_secs) const; + uint32_t sleep_ms) const; bool hasPendingWork() const; mesh::powerdiag::Mask pendingWorkReason() const; bool region_load_active; diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index c48ea311a..5bf5d91c5 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -384,23 +384,24 @@ void loop() { can_power_save = can_power_save && sleep_guard(!user_btn.needsPolling(), mesh::powerdiag::Reason::Button); #endif if (can_power_save) { - uint32_t sleep_secs = the_mesh.getPowerSaveSleepSeconds(30); + uint32_t sleep_ms = the_mesh.getPowerSaveSleepMilliseconds(30000); uint32_t noise_floor_delay_ms = 0; const bool noise_floor_wait = the_mesh.getNextNoiseFloorWakeDelay(noise_floor_delay_ms); + if (noise_floor_wait && noise_floor_delay_ms < sleep_ms) sleep_ms = noise_floor_delay_ms; if (noise_floor_wait && noise_floor_delay_ms == 0) { if (mesh::powerdiag::enabled()) mesh::powerdiag::decision(mesh::powerdiag::mask(mesh::powerdiag::Reason::Calibration), millis()); - sleep_secs = 0; + sleep_ms = 0; } #ifdef HAS_EXTERNAL_WATCHDOG - if (sleep_secs > 0) external_watchdog.feed(); + if (sleep_ms > 0) external_watchdog.feed(); #endif #if defined(NRF52_PLATFORM) - if (sleep_secs > 0) { + if (sleep_ms > 0) { #if MESH_ENABLE_SENSOR_TRACKER - if (!the_mesh.isTrackerModeEnabled() || armTrackerSleepWake(sleep_secs)) { + if (!the_mesh.isTrackerModeEnabled() || armTrackerSleepWake(mesh::power::powerSaveSleepSeconds(sleep_ms))) { if (mesh::powerdiag::enabled()) mesh::powerdiag::decision(0, millis()); - if (noise_floor_wait) board.sleepMilliseconds(mesh::power::limitSleepMilliseconds(sleep_secs, noise_floor_delay_ms)); + if (noise_floor_wait) board.sleepMilliseconds(sleep_ms); else board.sleep(0); } else { @@ -410,18 +411,17 @@ void loop() { } #else if (mesh::powerdiag::enabled()) mesh::powerdiag::decision(0, millis()); - if (noise_floor_wait) board.sleepMilliseconds(mesh::power::limitSleepMilliseconds(sleep_secs, noise_floor_delay_ms)); + if (noise_floor_wait) board.sleepMilliseconds(sleep_ms); else board.sleep(0); #endif } #else - if (sleep_secs > 0 + if (sleep_ms > 0 && the_mesh.millisHasNowPassed( POWERSAVING_FIRST_SLEEP_SECS * 1000UL)) { if (mesh::powerdiag::enabled()) mesh::powerdiag::decision(0, millis()); - if (noise_floor_wait) board.sleepMilliseconds(mesh::power::limitSleepMilliseconds(sleep_secs, noise_floor_delay_ms)); - else board.sleep(sleep_secs); - } else if (sleep_secs > 0 && mesh::powerdiag::enabled()) { + board.sleepMilliseconds(sleep_ms); + } else if (sleep_ms > 0 && mesh::powerdiag::enabled()) { mesh::powerdiag::decision(mesh::powerdiag::mask(mesh::powerdiag::Reason::Startup), millis()); } #endif diff --git a/src/helpers/PowerManagementUtils.h b/src/helpers/PowerManagementUtils.h index 8ccdc4c14..12debf3cf 100644 --- a/src/helpers/PowerManagementUtils.h +++ b/src/helpers/PowerManagementUtils.h @@ -5,6 +5,11 @@ namespace mesh { namespace power { +inline uint32_t secondsToSleepMilliseconds(uint32_t seconds) { + const uint64_t millisecs = static_cast(seconds) * 1000ULL; + return millisecs > UINT32_MAX ? UINT32_MAX : static_cast(millisecs); +} + 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) diff --git a/src/helpers/TempRadioLeaseDeadline.h b/src/helpers/TempRadioLeaseDeadline.h index 2125876e8..da757fcd2 100644 --- a/src/helpers/TempRadioLeaseDeadline.h +++ b/src/helpers/TempRadioLeaseDeadline.h @@ -25,6 +25,14 @@ public: && now_uptime_millis >= hard_end_uptime_millis; } + static uint32_t millisecondsUntil(uint64_t now_uptime_millis, + uint64_t hard_end_uptime_millis) { + if (hard_end_uptime_millis == 0) return UINT32_MAX; + if (now_uptime_millis >= hard_end_uptime_millis) return 0; + const uint64_t remaining = hard_end_uptime_millis - now_uptime_millis; + return remaining > UINT32_MAX ? UINT32_MAX : static_cast(remaining); + } + static uint32_t secondsUntil(uint64_t now_uptime_millis, uint64_t hard_end_uptime_millis) { if (hard_end_uptime_millis == 0) return UINT32_MAX; diff --git a/test/fixtures/morse_main_loop.cpp b/test/fixtures/morse_main_loop.cpp index 148245c6c..818a807a4 100644 --- a/test/fixtures/morse_main_loop.cpp +++ b/test/fixtures/morse_main_loop.cpp @@ -53,10 +53,10 @@ struct Mesh { struct Prefs { bool powersaving_enabled=true,system_watchdog_enabled=true; uint32_t reboot_interval=0; } prefs; unsigned calls=0; bool noise_floor_wait=false,work_pending=false; - uint32_t noise_floor_delay_ms=50; + uint32_t noise_floor_delay_ms=50,sleep_budget_ms=1000; void loop() { ++calls; } Prefs* getNodePrefs() { return &prefs; } - uint32_t getPowerSaveSleepSeconds(unsigned) { return work_pending ? 0 : 1; } + uint32_t getPowerSaveSleepMilliseconds(unsigned) { return work_pending ? 0 : sleep_budget_ms; } bool getNextNoiseFloorWakeDelay(uint32_t& ms) const { ms=noise_floor_delay_ms; return noise_floor_wait; } @@ -142,8 +142,9 @@ int main() { // bounded MCU idle in every actual role loop. Due work and other vetoes win. the_mesh.noise_floor_wait=true; auto before_noise_sleep=board.sleeps; + const auto before_noise_ms_sleep=board.millisecond_sleeps; loop(); - assert(board.sleeps==before_noise_sleep+1 && board.millisecond_sleeps==1); + assert(board.sleeps==before_noise_sleep+1 && board.millisecond_sleeps==before_noise_ms_sleep+1); #if ROLE_COMPANION assert(board.last_sleep_ms==10); // Preserve the shorter Companion cadence. #else @@ -161,4 +162,21 @@ int main() { the_mesh.work_pending=false; usb_watchdog_armed=true; loop(); assert(board.sleeps==before_noise_sleep+2); + usb_watchdog_armed=false; +#if !ROLE_COMPANION + // The role's earlier queue/bridge/timer deadline wins over calibration. + the_mesh.sleep_budget_ms=17; + loop(); + assert(board.last_sleep_ms==17); + the_mesh.noise_floor_wait=false; + for (uint32_t remaining : {1U,50U,999U,1001U,29999U}) { + const auto before=board.sleeps; + the_mesh.sleep_budget_ms=remaining; + loop(); + assert(board.sleeps==before+1); +#if !defined(NRF52_PLATFORM) + assert(board.last_sleep_ms==remaining); // No whole-second busy tail. +#endif + } +#endif } diff --git a/test/fixtures/official_web_console/firmware.cpp b/test/fixtures/official_web_console/firmware.cpp index e1c92640e..651eafd7b 100644 --- a/test/fixtures/official_web_console/firmware.cpp +++ b/test/fixtures/official_web_console/firmware.cpp @@ -56,7 +56,7 @@ struct MyMesh { bool hasPendingSerialOutput() { return false; } bool isDualRadioActive() { return dual_radio_active; } Prefs* getNodePrefs() { return _prefs; } - unsigned getPowerSaveSleepSeconds(unsigned limit) { return limit; } + unsigned getPowerSaveSleepMilliseconds(unsigned limit) { return limit; } bool millisHasNowPassed(unsigned deadline) { return millis() > deadline; } } the_mesh; static ESP32Board board; diff --git a/test/fixtures/sensor_tracker_radio_lease.cpp b/test/fixtures/sensor_tracker_radio_lease.cpp index ead9aee54..7f3c72c9e 100644 --- a/test/fixtures/sensor_tracker_radio_lease.cpp +++ b/test/fixtures/sensor_tracker_radio_lease.cpp @@ -145,6 +145,7 @@ public: uint32_t limitSleepToMillisTimer(unsigned long, uint32_t) const; bool hasPendingWork() const; mesh::powerdiag::Mask pendingWorkReason() const; + uint32_t getPowerSaveSleepMilliseconds(uint32_t) const; uint32_t getPowerSaveSleepSeconds(uint32_t) const; void loopBeforeService(); void sleepIfIdle() { @SLEEP_BLOCK@ } diff --git a/test/test_companion_power_diagnostics.py b/test/test_companion_power_diagnostics.py index 7fc6fa47e..1b3c5321e 100644 --- a/test/test_companion_power_diagnostics.py +++ b/test/test_companion_power_diagnostics.py @@ -36,6 +36,7 @@ static int esp_light_sleep_start() { ++wait_calls; clock_us += 500; return sleep static unsigned pdMS_TO_TICKS(unsigned ms) { return ms; } static void vTaskDelay(unsigned ms) { assert(ms == 10); ++delay_calls; clock_us += 10000; } static void attempt() { + const uint32_t idle_slice_ms = 10; @BLOCK@ } int main() { diff --git a/test/test_esp32_full_power_lifecycle.py b/test/test_esp32_full_power_lifecycle.py index 280f0c759..a35091797 100644 --- a/test/test_esp32_full_power_lifecycle.py +++ b/test/test_esp32_full_power_lifecycle.py @@ -240,6 +240,7 @@ struct MyMesh { void suspendUnconfiguredSetupBridges(); void serviceIdleWiFi(); bool startWebConfigImpl(bool force_ap, char* reply, bool automatic_setup); bool startWebConfig(bool force_ap, char* reply); bool stopWebConfig(char* reply); + uint32_t getPowerSaveSleepMilliseconds(uint32_t) const; bool hasPendingWork() const; uint32_t getPowerSaveSleepSeconds(uint32_t) const; mesh::powerdiag::Mask pendingWorkReason() const; @INLINE_METHODS@ @@ -391,6 +392,7 @@ class ESP32FullPowerLifecycleTest(unittest.TestCase): "bool MyMesh::startWebConfig(", "bool MyMesh::startWebConfigImpl(", "bool MyMesh::stopWebConfig(", "bool MyMesh::hasPendingWork() const", "mesh::powerdiag::Mask MyMesh::pendingWorkReason() const", + "uint32_t MyMesh::getPowerSaveSleepMilliseconds(", "uint32_t MyMesh::getPowerSaveSleepSeconds(")) defaults_start = source.index(" // bridge defaults") defaults = source[defaults_start:source.index(" _prefs.bridge_delay", defaults_start)] diff --git a/test/test_esp32_full_sleep.py b/test/test_esp32_full_sleep.py index 0a59dfdaa..6def0c9e8 100644 --- a/test/test_esp32_full_sleep.py +++ b/test/test_esp32_full_sleep.py @@ -145,6 +145,7 @@ struct Mesh { return pending ? mesh::powerdiag::mask(mesh::powerdiag::Reason::Mesh) : 0; } bool isDualRadioActive() const { return dual; } + bool getNextNoiseFloorWakeDelay(uint32_t&) const { return false; } mesh::Radio* getProfileRadio() { return &timing_radio; } } the_mesh; struct Interfaces { bool pending = false; bool hasPendingIO() const { return pending; } } interface_manager; diff --git a/test/test_logging_sleep_contract.py b/test/test_logging_sleep_contract.py index 9171d2de3..bfb144586 100644 --- a/test/test_logging_sleep_contract.py +++ b/test/test_logging_sleep_contract.py @@ -68,6 +68,7 @@ struct MyMesh { uint32_t limitSleepToScheduledRadioWork(uint32_t secs) const { return secs; } bool hasPendingWork() const; mesh::powerdiag::Mask pendingWorkReason() const; + uint32_t getPowerSaveSleepMilliseconds(uint32_t) const; uint32_t getPowerSaveSleepSeconds(uint32_t) const; }; @METHODS@ @@ -151,6 +152,7 @@ class LoggingSleepContractTest(unittest.TestCase): methods = "\n".join(method(source, signature) for signature in ( "bool MyMesh::hasPendingWork() const", "mesh::powerdiag::Mask MyMesh::pendingWorkReason() const", + "uint32_t MyMesh::getPowerSaveSleepMilliseconds(uint32_t max_ms) const", "uint32_t MyMesh::getPowerSaveSleepSeconds(uint32_t max_secs) const", )) cpp = Path(temp) / f"{role}.cpp" diff --git a/test/test_official_web_console.py b/test/test_official_web_console.py index 3ef6bc603..928491efa 100644 --- a/test/test_official_web_console.py +++ b/test/test_official_web_console.py @@ -98,7 +98,7 @@ struct Radio { static mesh::Radio radio_driver; '''.replace("@MORSE_HELPER@", morse_helper) fixture = (ROOT / "test/fixtures/official_web_console/firmware.cpp").read_text() - sleep_getter = " unsigned getPowerSaveSleepSeconds(unsigned limit) { return limit; }" + sleep_getter = " unsigned getPowerSaveSleepMilliseconds(unsigned limit) { return limit; }" assert fixture.count(sleep_getter) == 1, "Missing sleep-deadline peripheral boundary" fixture = fixture.replace(sleep_getter, sleep_getter + "\n" + " bool getNextNoiseFloorWakeDelay(uint32_t&) const { return false; }") diff --git a/test/test_power_saving_deadlines.py b/test/test_power_saving_deadlines.py index 16e3ae4f1..ec36bf905 100644 --- a/test/test_power_saving_deadlines.py +++ b/test/test_power_saving_deadlines.py @@ -14,6 +14,7 @@ HARNESS = r''' #include #include #include +#include #include #define SENSOR_READ_INTERVAL_SECS 60 static uint32_t now_ms = 1000; @@ -44,12 +45,27 @@ struct Node { } uint32_t limitSleepToScheduledRadioWork(uint32_t seconds) const { return seconds; } uint32_t limitSleepToMillisTimer(unsigned long, uint32_t) const; + uint32_t getPowerSaveSleepMilliseconds(uint32_t) const; uint32_t getPowerSaveSleepSeconds(uint32_t) const; }; @METHODS@ int main() { Node node; + using Lease = mesh::TempRadioLeaseDeadline; + assert(Lease::millisecondsUntil(10, 0) == UINT32_MAX); + assert(Lease::millisecondsUntil(100, 100) == 0); + assert(Lease::millisecondsUntil(101, 100) == 0); + assert(Lease::millisecondsUntil(100, uint64_t(UINT32_MAX) + 101) == UINT32_MAX); + for (uint64_t now : {1000ULL, 0x100000001ULL, UINT64_MAX - 100000ULL}) { + for (uint32_t delay : {1U, 50U, 999U, 1001U, 30000U}) + assert(Lease::millisecondsUntil(now, now + delay) == delay); + } assert(mesh::power::powerSaveSleepSeconds(0) == 0); + assert(mesh::power::secondsToSleepMilliseconds(0) == 0); + assert(mesh::power::secondsToSleepMilliseconds(30) == 30000); + assert(mesh::power::secondsToSleepMilliseconds(4294967) == 4294967000U); + assert(mesh::power::secondsToSleepMilliseconds(4294968) == UINT32_MAX); + assert(mesh::power::secondsToSleepMilliseconds(UINT32_MAX) == UINT32_MAX); assert(mesh::power::limitSleepMilliseconds(0, 50) == 0); assert(mesh::power::limitSleepMilliseconds(30, 0) == 0); assert(mesh::power::limitSleepMilliseconds(30, 50) == 50); @@ -63,7 +79,9 @@ int main() { #endif assert(node.getPowerSaveSleepSeconds(30) == 30); assert(node.getPowerSaveSleepSeconds(0) == 0); - assert(node.limitSleepToMillisTimer(0, 30) == 30); // Disabled timer sentinel. + assert(node.getPowerSaveSleepMilliseconds(30000) == 30000); + assert(node.getPowerSaveSleepMilliseconds(0) == 0); + assert(node.limitSleepToMillisTimer(0, 30000) == 30000); // Disabled timer sentinel. assert(node.limitSleepToMillisTimer(1001, 0) == 0); node.pending = true; assert(node.getPowerSaveSleepSeconds(30) == 0); @@ -82,27 +100,33 @@ int main() { const uint32_t idle_seconds = whole_seconds; #endif const uint32_t expected = idle_seconds < 30 ? idle_seconds : 30; + const uint32_t expected_ms = ms < 30000 ? ms : 30000; node.queue = true; node.queue_ms = ms; assert(node.getPowerSaveSleepSeconds(30) == expected); + assert(node.getPowerSaveSleepMilliseconds(30000) == expected_ms); node.queue = false; node.retry = true; node.retry_ms = ms; assert(node.getPowerSaveSleepSeconds(30) == expected); + assert(node.getPowerSaveSleepMilliseconds(30000) == expected_ms); 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); + assert(node.limitSleepToMillisTimer(deadline, 30000) == expected_ms); node.next_flood_advert = deadline; assert(node.getPowerSaveSleepSeconds(30) == expected); + assert(node.getPowerSaveSleepMilliseconds(30000) == expected_ms); 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; + assert(node.getPowerSaveSleepMilliseconds(30000) == 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); + assert(node.getPowerSaveSleepMilliseconds(30000) == 999); #if EXPECT_INTERRUPT_IDLE assert(node.getPowerSaveSleepSeconds(30) == 1); #else @@ -116,6 +140,68 @@ int main() { class PowerSavingDeadlineTests(unittest.TestCase): + def test_actual_role_loops_preserve_subsecond_sleep_budgets(self): + from cpp_source import body + harness = r''' +#include +#include +#include +#include +#include +#define POWERSAVING_FIRSTSLEEP_SECS 1 +#define POWERSAVING_FIRST_SLEEP_SECS 1 +static uint32_t millis() { return 100000; } +struct Mesh { + uint32_t budget = 30000, noise_delay = 50; + bool noise = false; + uint32_t getPowerSaveSleepMilliseconds(uint32_t limit) const { + assert(limit == 30000); return budget; + } + bool getNextNoiseFloorWakeDelay(uint32_t& out) const { + out = noise_delay; return noise; + } + bool millisHasNowPassed(uint32_t) const { return true; } +} the_mesh; +struct Board { + unsigned calls = 0; + uint32_t last_ms = 0; + void sleep(uint32_t seconds) { ++calls; last_ms = seconds * 1000; } + void sleepMilliseconds(uint32_t ms) { ++calls; last_ms = ms; } +} board; +void idle(bool can_power_save) { @GATE@ } +int main() { + for (uint32_t budget : {0U, 1U, 50U, 999U, 1001U, 29999U, 30000U}) { + the_mesh.budget = budget; + for (bool noise : {false, true}) { + the_mesh.noise = noise; + const auto before = board.calls; + idle(true); + assert(board.calls == before + (budget != 0)); + if (budget) assert(board.last_ms == (noise && budget > 50 ? 50 : budget)); + const auto after = board.calls; + idle(false); assert(board.calls == after); + } + } + the_mesh.noise_delay = 0; + const auto before = board.calls; + idle(true); assert(board.calls == before); +} +''' + with tempfile.TemporaryDirectory(prefix='meshcore-role-ms-idle-') as directory: + cpp, exe = Path(directory) / 'test.cpp', Path(directory) / 'test' + for role in ('simple_repeater', 'simple_room_server', 'simple_sensor'): + with self.subTest(role=role): + source = (ROOT / 'examples' / role / 'main.cpp').read_text() + gate = body(source, ' if (can_power_save) {') + cpp.write_text(harness.replace('@GATE@', gate), encoding='ascii') + built = subprocess.run([ + os.environ.get('CXX', 'g++'), '-std=c++17', '-Wall', '-Wextra', '-Werror', + '-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) + def test_real_board_millisecond_idle_preserves_port_wake_contract(self): source = (ROOT / 'src/MeshCore.h').read_text(encoding='ascii') implementation = method(source, 'virtual void sleepMilliseconds(uint32_t millisecs)') @@ -172,6 +258,7 @@ int main() { source = (ROOT / filename).read_text() methods = '\n'.join(method(source, signature) for signature in ( f'uint32_t {name}::limitSleepToMillisTimer(', + f'uint32_t {name}::getPowerSaveSleepMilliseconds(', f'uint32_t {name}::getPowerSaveSleepSeconds(')) methods = methods.replace(name + '::', 'Node::') cpp, exe = work / (role + '.cpp'), work / role diff --git a/test/test_power_saving_role_diagnostics.py b/test/test_power_saving_role_diagnostics.py index f0063b4f9..9b973e3f2 100644 --- a/test/test_power_saving_role_diagnostics.py +++ b/test/test_power_saving_role_diagnostics.py @@ -157,6 +157,7 @@ int main() { methods = "\n".join(extract_braced(source, signature) for signature in ( "bool MyMesh::hasPendingWork() const", "mesh::powerdiag::Mask MyMesh::pendingWorkReason() const", + "uint32_t MyMesh::getPowerSaveSleepMilliseconds(uint32_t max_ms) const", "uint32_t MyMesh::getPowerSaveSleepSeconds(uint32_t max_secs) const", )) code = r''' @@ -210,6 +211,7 @@ struct MyMesh { uint32_t limitSleepToScheduledRadioWork(uint32_t value) const { return value; } bool hasPendingWork() const; mesh::powerdiag::Mask pendingWorkReason() const; + uint32_t getPowerSaveSleepMilliseconds(uint32_t max_ms) const; uint32_t getPowerSaveSleepSeconds(uint32_t max_secs) const; }; @METHODS@ @@ -240,8 +242,9 @@ int main() { assert(node.pendingWorkReason() == mask(Reason::BridgeWake)); node.uart.wake_ms = 500; assert(!node.hasPendingWork()); - assert(node.getPowerSaveSleepSeconds(30) == 0); // Whole-second bridge rendezvous floor. - if (diagnostics) assert(snapshot(millis()).current == mask(Reason::BridgeWake)); + reset(millis()); + assert(node.getPowerSaveSleepMilliseconds(30000) == 500); + if (diagnostics) assert(snapshot(millis()).current == 0); node.uart.wake_ms = 60000; check(node.radio_driver.watchdog, Reason::RadioWatchdog); check(node.radio_driver.calibration, Reason::Calibration); @@ -253,10 +256,18 @@ int main() { check(node.queue, Reason::Queue); check(node.retry, Reason::Retry); check(node.scheduled, Reason::Scheduled); node.queue_delay_set = true; node.queue_delay = 500; - assert(!node.hasPendingWork() && node.getPowerSaveSleepSeconds(30) == 0); + reset(millis()); + assert(!node.hasPendingWork() && node.getPowerSaveSleepMilliseconds(30000) == 500); + if (diagnostics) assert(snapshot(millis()).current == 0); + node.queue_delay = 0; + assert(node.getPowerSaveSleepMilliseconds(30000) == 0); if (diagnostics) assert(snapshot(millis()).current == mask(Reason::Queue)); node.queue_delay_set = false; node.retry_delay_set = true; node.retry_delay = 500; - assert(node.getPowerSaveSleepSeconds(30) == 0); + reset(millis()); + assert(node.getPowerSaveSleepMilliseconds(30000) == 500); + if (diagnostics) assert(snapshot(millis()).current == 0); + node.retry_delay = 0; + assert(node.getPowerSaveSleepMilliseconds(30000) == 0); if (diagnostics) assert(snapshot(millis()).current == mask(Reason::Retry)); node.retry_delay_set = false; node.dirty_contacts_expiry = millis(); assert(node.pendingWorkReason() == mask(Reason::Scheduled)); diff --git a/test/test_rs232_heartbeat_owner.py b/test/test_rs232_heartbeat_owner.py index 455d65b82..e27f84491 100644 --- a/test/test_rs232_heartbeat_owner.py +++ b/test/test_rs232_heartbeat_owner.py @@ -166,6 +166,7 @@ struct MyMesh : Callbacks { uint32_t limitSleepToScheduledRadioWork(uint32_t secs) const { return secs; } bool hasPendingWork() const; mesh::powerdiag::Mask pendingWorkReason() const; + uint32_t getPowerSaveSleepMilliseconds(uint32_t) const; uint32_t getPowerSaveSleepSeconds(uint32_t) const; @OWNER@ ~MyMesh() override { assert(setRs232BridgeState(false)); } @@ -225,6 +226,7 @@ int main() { const uint32_t expected = remainder / 1000U; #endif assert(node.getPowerSaveSleepSeconds(30) == expected); + assert(node.getPowerSaveSleepMilliseconds(30000) == remainder); } node.bridge->wake_delay = 0; assert(node.hasPendingWork()); #ifdef WITH_MQTT_BRIDGE @@ -309,6 +311,7 @@ class RS232HeartbeatOwnerTests(unittest.TestCase): sleep = "\n".join(extract_braced(implementation, signature) for signature in ( "bool MyMesh::hasPendingWork() const", "mesh::powerdiag::Mask MyMesh::pendingWorkReason() const", + "uint32_t MyMesh::getPowerSaveSleepMilliseconds(uint32_t max_ms) const", "uint32_t MyMesh::getPowerSaveSleepSeconds(uint32_t max_secs) const", )) source = HARNESS.replace("@OWNER@", owner).replace("@SLEEP@", sleep) diff --git a/test/test_sensor_tracker_sleep.py b/test/test_sensor_tracker_sleep.py index 849418292..9a9f12855 100644 --- a/test/test_sensor_tracker_sleep.py +++ b/test/test_sensor_tracker_sleep.py @@ -67,6 +67,7 @@ public: uint32_t limitSleepToMillisTimer(unsigned long timestamp,uint32_t sleep_secs)const; bool hasPendingWork()const; mesh::powerdiag::Mask pendingWorkReason()const; + uint32_t getPowerSaveSleepMilliseconds(uint32_t max_ms)const; uint32_t getPowerSaveSleepSeconds(uint32_t max_secs)const; #if MESH_ENABLE_SENSOR_TRACKER bool trackerNeedsRadio()const; @@ -159,7 +160,7 @@ static void trackerWakeCallback(TimerHandle_t){} @ARM@ struct Mesh {bool tracker=true;uint32_t sleep_secs=30; bool isTrackerModeEnabled()const{return tracker;} - uint32_t getPowerSaveSleepSeconds(uint32_t max)const{assert(max==30);return sleep_secs;} + uint32_t getPowerSaveSleepMilliseconds(uint32_t max)const{assert(max==30000);return sleep_secs*1000;} bool getNextNoiseFloorWakeDelay(uint32_t&)const{return false;} bool millisHasNowPassed(uint32_t deadline)const{return (int32_t)(millis()-deadline)>=0;} } the_mesh; @@ -187,7 +188,7 @@ int main(){ the_mesh.sleep_secs=0;sleepGate(true);assert(board.sleeps==1); #else now_ms=119999;sleepGate(true);assert(board.sleeps==0); - now_ms=120000;sleepGate(true);assert(board.sleeps==1&&board.last==30); + now_ms=120000;sleepGate(true);assert(board.sleeps==1&&board.last==30000); the_mesh.sleep_secs=0;sleepGate(true);assert(board.sleeps==1); #endif } @@ -286,6 +287,7 @@ class SensorTrackerSleepTest(unittest.TestCase): methods = "\n".join(extract_braced(sensor, name) for name in ( "bool SensorMesh::isMillisTimerDue(", "uint32_t SensorMesh::limitSleepToMillisTimer(", "bool SensorMesh::hasPendingWork(", "mesh::powerdiag::Mask SensorMesh::pendingWorkReason(", + "uint32_t SensorMesh::getPowerSaveSleepMilliseconds(", "uint32_t SensorMesh::getPowerSaveSleepSeconds(")) methods += "\n#if MESH_ENABLE_SENSOR_TRACKER\n" + extract_braced( tracker, "bool SensorMesh::trackerNeedsRadio(") + "\n#endif\n" @@ -418,6 +420,7 @@ int main(){ methods = "\n".join(body(sensor, signature) for signature in ( "bool SensorMesh::isMillisTimerDue(", "uint32_t SensorMesh::limitSleepToMillisTimer(", "bool SensorMesh::hasPendingWork(", "mesh::powerdiag::Mask SensorMesh::pendingWorkReason(", + "uint32_t SensorMesh::getPowerSaveSleepMilliseconds(", "uint32_t SensorMesh::getPowerSaveSleepSeconds(")) methods += "\n" + "\n".join(body(tracker, signature) for signature in ( "bool SensorMesh::trackerNeedsRadio(", "void SensorMesh::wakeTrackerRadio("))