Sleep between noise-floor samples and reduce idle refreshes
This commit is contained in:
1 parent
50428f0fb7
commit
dd7a10f9aa
38 files changed
+955
-121
No files matched your search
@@ -374,6 +374,7 @@ jobs:
|
||||
python3 -B test/test_mqtt_prefs_commit.py -v
|
||||
python3 -B test/test_identity_and_settings_recovery.py -v
|
||||
python3 -B test/test_contact_cache.py -v
|
||||
python3 -B test/test_one_key_dm_shared_queue.py -v
|
||||
|
||||
- name: Verify Ethernet RF harness framing, TX lock, isolation and safe serial ownership
|
||||
run: |
|
||||
@@ -438,6 +439,7 @@ jobs:
|
||||
run: |
|
||||
python3 -B test/test_dispatcher_startup_watchdog.py -v
|
||||
python3 -B test/test_radio_receive_contract.py
|
||||
python3 -B test/test_noise_floor_sleep.py -v
|
||||
python3 -B test/test_radio_receive_retry.py -v
|
||||
python3 -B test/test_adaptive_agc_roles.py -v
|
||||
python3 - <<'PY'
|
||||
|
||||
@@ -31,7 +31,9 @@ after off starts a fresh window. Collection defaults off at every boot.
|
||||
- `bridge`, `bridge.cleanup`, `bridge.wake`: active UART/network bridge,
|
||||
shutdown work or an overdue standby wake. A suspended RS232 owner is not
|
||||
diagnosed as running just because its saved enabled flag is on.
|
||||
- `calibration`, `radio.watchdog`: radio observation/calibration work.
|
||||
- `calibration`: noise-floor work is due now. The CPU can idle between sample
|
||||
deadlines even while the radio remains in continuous RX for calibration.
|
||||
- `radio.watchdog`: radio observation/recovery work.
|
||||
- `queue`, `retry`, `scheduled`, `mesh`, `ota`, `radio2`: pending role work.
|
||||
- `wireless`, `network`, `serial`, `button`, `tracker`, `morse`: a matching
|
||||
existing service guard is active.
|
||||
@@ -60,6 +62,27 @@ than the 32-bit `micros()` wrap (about 71 minutes); the normal wait paths here
|
||||
are much shorter. Keep gaps between policy observations below the `millis()`
|
||||
wrap (about 49 days).
|
||||
|
||||
## Noise-floor sampling and sleep
|
||||
|
||||
Startup still collects a complete baseline promptly: 64 accepted samples,
|
||||
at least 50 ms apart, after the radio frontend has settled. Collection keeps
|
||||
the radio in continuous RX when needed, but the CPU may sleep between reads.
|
||||
Pending packet interrupts retain their normal receive owner. Rejected readings
|
||||
retry at 50 ms intervals, and the existing 30-second collection timeout bounds
|
||||
an attempt on a busy channel.
|
||||
|
||||
After a successful baseline, ordinary single-profile refresh waits five
|
||||
minutes. A rejected rise or failed attempt retains the last usable floor and
|
||||
retries after one minute. Gain, tuning and AGC changes still request immediate
|
||||
recalibration, so a configured short AGC interval can refresh more frequently.
|
||||
Dual-profile scanning keeps its existing timing and calibration policy.
|
||||
|
||||
ESP32 timed sleep uses a millisecond timer capped at the next sample deadline.
|
||||
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
|
||||
power benefit requires a new matched hardware/current measurement.
|
||||
|
||||
See the [CLI reference](cli_commands.md#diagnose-what-prevents-power-saving)
|
||||
for all page names and commands.
|
||||
|
||||
|
||||
@@ -25,8 +25,10 @@ The ordinary Wio Tracker L1 BLE build reproduced a runtime RAM failure with
|
||||
48,964 bytes available against 59,968 required. Enabling the existing cache
|
||||
for high-contact nRF52 profiles recovered 30,200 bytes: the linked build has
|
||||
79,164 bytes available and a 19,196-byte margin against the unchanged budget.
|
||||
Its 350 contacts, 40 channels and configured 256-frame offline/held-DM pool
|
||||
are retained. This is a linked-capacity result, not a physical load/soak test.
|
||||
It keeps 350 contacts, 40 channels and 256 normal offline frames. As on the
|
||||
cached RAK profiles, up to 15 held one-key DMs temporarily use queue slots;
|
||||
their storage is shared instead of reserving a second pool. This is a
|
||||
linked-capacity result, not a physical load/soak test.
|
||||
|
||||
## Paths and persistence
|
||||
|
||||
|
||||
@@ -11602,7 +11602,9 @@ mesh::powerdiag::Mask MyMesh::pendingWorkReason() const {
|
||||
#endif
|
||||
if (isDualRadioActive()) return mask(Reason::DualRadio);
|
||||
if (_radio_available && radio_driver.isWatchdogObserving()) return mask(Reason::RadioWatchdog);
|
||||
if (_radio_available && radio_driver.isCalibratingNoiseFloor()) return mask(Reason::Calibration);
|
||||
uint32_t noise_floor_delay_ms;
|
||||
if (getNextNoiseFloorWakeDelay(noise_floor_delay_ms) && noise_floor_delay_ms == 0)
|
||||
return mask(Reason::Calibration);
|
||||
#if defined(NRF52_PLATFORM)
|
||||
// nRF52 sleep is event-driven, so a future lazy-write deadline has no
|
||||
// guaranteed interrupt to wake the loop when it becomes due. Keep polling
|
||||
|
||||
@@ -4248,11 +4248,18 @@ void loop() {
|
||||
// retains a two-minute wake interval after button activity.
|
||||
if (!sleep_reason && user_btn.needsPolling()) sleep_reason = mask(Reason::Button);
|
||||
#endif
|
||||
uint32_t noise_floor_delay_ms = 0;
|
||||
const bool noise_floor_wait = !sleep_reason
|
||||
&& the_mesh.getNextNoiseFloorWakeDelay(noise_floor_delay_ms);
|
||||
if (noise_floor_wait && noise_floor_delay_ms == 0) sleep_reason = mask(Reason::Calibration);
|
||||
if (mesh::powerdiag::enabled()) mesh::powerdiag::decision(sleep_reason, millis());
|
||||
const bool can_sleep = sleep_reason == 0;
|
||||
if (can_sleep) {
|
||||
const uint32_t idle_slice_ms = noise_floor_wait && noise_floor_delay_ms < 10
|
||||
? noise_floor_delay_ms : 10;
|
||||
#if defined(NRF52_PLATFORM)
|
||||
board.sleep(0); // nrf ignores seconds param, sleeps whenever possible
|
||||
if (noise_floor_wait) board.sleepMilliseconds(idle_slice_ms);
|
||||
else board.sleep(0); // nrf ignores seconds param, sleeps whenever possible
|
||||
#elif defined(ESP32_PLATFORM)
|
||||
#if defined(ENABLE_USB_INTERFACE) \
|
||||
&& defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT \
|
||||
@@ -4266,7 +4273,7 @@ void loop() {
|
||||
if (mesh::powerdiag::enabled()) mesh::powerdiag::boardVeto(Reason::Board, millis());
|
||||
sleep_failed = true;
|
||||
} else {
|
||||
esp_err_t error = esp_sleep_enable_timer_wakeup(10000ULL);
|
||||
esp_err_t error = esp_sleep_enable_timer_wakeup(static_cast<uint64_t>(idle_slice_ms) * 1000ULL);
|
||||
if (error == ESP_OK) {
|
||||
if (mesh::powerdiag::enabled()) mesh::powerdiag::sleepBegin(micros());
|
||||
error = esp_light_sleep_start();
|
||||
@@ -4278,19 +4285,20 @@ void loop() {
|
||||
}
|
||||
}
|
||||
if (sleep_failed) {
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
vTaskDelay(pdMS_TO_TICKS(idle_slice_ms));
|
||||
}
|
||||
#elif COMPANION_IDF_PM_AVAILABLE
|
||||
// Yield long enough for ESP-IDF automatic light sleep to enter when no
|
||||
// driver holds a power-management lock.
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
vTaskDelay(pdMS_TO_TICKS(idle_slice_ms));
|
||||
#else
|
||||
// Connected transports need their own modem sleep and must retain the
|
||||
// normal FreeRTOS idle behavior.
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
vTaskDelay(pdMS_TO_TICKS(idle_slice_ms));
|
||||
#endif
|
||||
#elif defined(RP2040_PLATFORM) || defined(STM32_PLATFORM)
|
||||
board.sleep(0); // event-driven idle; interrupts wake the main loop
|
||||
if (noise_floor_wait) board.sleepMilliseconds(idle_slice_ms);
|
||||
else board.sleep(0); // event-driven idle; interrupts wake the main loop
|
||||
#endif
|
||||
}
|
||||
#if defined(ESP32_PLATFORM)
|
||||
|
||||
@@ -13976,7 +13976,9 @@ mesh::powerdiag::Mask MyMesh::pendingWorkReason() const {
|
||||
#endif
|
||||
#endif
|
||||
if (radio_driver.isWatchdogObserving()) return mask(Reason::RadioWatchdog);
|
||||
if (radio_driver.isCalibratingNoiseFloor()) return mask(Reason::Calibration);
|
||||
uint32_t noise_floor_delay_ms;
|
||||
if (getNextNoiseFloorWakeDelay(noise_floor_delay_ms) && noise_floor_delay_ms == 0)
|
||||
return mask(Reason::Calibration);
|
||||
if (hasQueuedWorkDue()) return mask(Reason::Queue);
|
||||
if (hasRetryWorkDue()) return mask(Reason::Retry);
|
||||
if (isMillisTimerDue(next_flood_advert) || isMillisTimerDue(next_local_advert)) return mask(Reason::Scheduled);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <helpers/UsbLoggingWatchdog.h>
|
||||
#include <helpers/UsbLoggingClientActivity.h>
|
||||
#include <helpers/MorseLoopPolicy.h>
|
||||
#include <helpers/PowerManagementUtils.h>
|
||||
#include <helpers/HilStartupTrace.h>
|
||||
#if MESH_PACKET_LOGGING
|
||||
#include <helpers/SerialPacketLog.h>
|
||||
@@ -646,18 +647,27 @@ void loop() {
|
||||
#endif
|
||||
if (can_power_save) {
|
||||
uint32_t sleep_secs = the_mesh.getPowerSaveSleepSeconds(30);
|
||||
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 == 0) {
|
||||
if (mesh::powerdiag::enabled())
|
||||
mesh::powerdiag::decision(mesh::powerdiag::mask(mesh::powerdiag::Reason::Calibration), millis());
|
||||
sleep_secs = 0;
|
||||
}
|
||||
#ifdef HAS_EXTERNAL_WATCHDOG
|
||||
if (sleep_secs > 0) external_watchdog.feed();
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM)
|
||||
if (sleep_secs > 0) {
|
||||
if (mesh::powerdiag::enabled()) mesh::powerdiag::decision(0, millis());
|
||||
board.sleep(0); // nrf ignores seconds param, sleeps whenever possible
|
||||
if (noise_floor_wait) board.sleepMilliseconds(mesh::power::limitSleepMilliseconds(sleep_secs, noise_floor_delay_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 (mesh::powerdiag::enabled()) mesh::powerdiag::decision(0, millis());
|
||||
board.sleep(sleep_secs); // Sleep. Wake up for scheduled jobs or when receiving a LoRa packet
|
||||
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()) {
|
||||
mesh::powerdiag::decision(mesh::powerdiag::mask(mesh::powerdiag::Reason::Startup), millis());
|
||||
}
|
||||
|
||||
@@ -4321,7 +4321,9 @@ mesh::powerdiag::Mask MyMesh::pendingWorkReason() const {
|
||||
#endif
|
||||
#endif
|
||||
if (radio_driver.isWatchdogObserving()) return mask(Reason::RadioWatchdog);
|
||||
if (radio_driver.isCalibratingNoiseFloor()) return mask(Reason::Calibration);
|
||||
uint32_t noise_floor_delay_ms;
|
||||
if (getNextNoiseFloorWakeDelay(noise_floor_delay_ms) && noise_floor_delay_ms == 0)
|
||||
return mask(Reason::Calibration);
|
||||
if (hasQueuedWorkDue()) return mask(Reason::Queue);
|
||||
if (hasRetryWorkDue()) return mask(Reason::Retry);
|
||||
if (acl.getNumClients() > 0 && isMillisTimerDue(next_push)) return mask(Reason::Scheduled);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <helpers/UsbLoggingWatchdog.h>
|
||||
#include <helpers/UsbLoggingClientActivity.h>
|
||||
#include <helpers/MorseLoopPolicy.h>
|
||||
#include <helpers/PowerManagementUtils.h>
|
||||
#if MESH_PACKET_LOGGING
|
||||
#include <helpers/SerialPacketLog.h>
|
||||
#endif
|
||||
@@ -418,18 +419,27 @@ void loop() {
|
||||
#endif
|
||||
if (can_power_save) {
|
||||
uint32_t sleep_secs = the_mesh.getPowerSaveSleepSeconds(30);
|
||||
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 == 0) {
|
||||
if (mesh::powerdiag::enabled())
|
||||
mesh::powerdiag::decision(mesh::powerdiag::mask(mesh::powerdiag::Reason::Calibration), millis());
|
||||
sleep_secs = 0;
|
||||
}
|
||||
#ifdef HAS_EXTERNAL_WATCHDOG
|
||||
if (sleep_secs > 0) external_watchdog.feed();
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM)
|
||||
if (sleep_secs > 0) {
|
||||
if (mesh::powerdiag::enabled()) mesh::powerdiag::decision(0, millis());
|
||||
board.sleep(0); // nrf ignores seconds param, sleeps whenever possible
|
||||
if (noise_floor_wait) board.sleepMilliseconds(mesh::power::limitSleepMilliseconds(sleep_secs, noise_floor_delay_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 (mesh::powerdiag::enabled()) mesh::powerdiag::decision(0, millis());
|
||||
board.sleep(sleep_secs); // Sleep. Wake up for scheduled jobs or when receiving a LoRa packet
|
||||
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()) {
|
||||
mesh::powerdiag::decision(mesh::powerdiag::mask(mesh::powerdiag::Reason::Startup), millis());
|
||||
}
|
||||
|
||||
@@ -1730,7 +1730,9 @@ mesh::powerdiag::Mask SensorMesh::pendingWorkReason() const {
|
||||
if (hasPendingOtaApply()) return mask(Reason::Ota);
|
||||
if (_cli.hasActiveUserGpioTimer()) return mask(Reason::Scheduled);
|
||||
if (radio_driver.isWatchdogObserving()) return mask(Reason::RadioWatchdog);
|
||||
if (radio_driver.isCalibratingNoiseFloor()) return mask(Reason::Calibration);
|
||||
uint32_t noise_floor_delay_ms;
|
||||
if (getNextNoiseFloorWakeDelay(noise_floor_delay_ms) && noise_floor_delay_ms == 0)
|
||||
return mask(Reason::Calibration);
|
||||
if (hasQueuedWorkDue()) return mask(Reason::Queue);
|
||||
if (hasRetryWorkDue()) return mask(Reason::Retry);
|
||||
if (isMillisTimerDue(next_flood_advert)
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <helpers/UsbLoggingWatchdog.h>
|
||||
#include <helpers/UsbLoggingClientActivity.h>
|
||||
#include <helpers/MorseLoopPolicy.h>
|
||||
#include <helpers/PowerManagementUtils.h>
|
||||
#if defined(NRF52_PLATFORM) && MESH_ENABLE_SENSOR_TRACKER
|
||||
#include <utility/SoftwareTimer.h>
|
||||
static SoftwareTimer tracker_wake_timer;
|
||||
@@ -384,6 +385,13 @@ void loop() {
|
||||
#endif
|
||||
if (can_power_save) {
|
||||
uint32_t sleep_secs = the_mesh.getPowerSaveSleepSeconds(30);
|
||||
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 == 0) {
|
||||
if (mesh::powerdiag::enabled())
|
||||
mesh::powerdiag::decision(mesh::powerdiag::mask(mesh::powerdiag::Reason::Calibration), millis());
|
||||
sleep_secs = 0;
|
||||
}
|
||||
#ifdef HAS_EXTERNAL_WATCHDOG
|
||||
if (sleep_secs > 0) external_watchdog.feed();
|
||||
#endif
|
||||
@@ -392,7 +400,8 @@ void loop() {
|
||||
#if MESH_ENABLE_SENSOR_TRACKER
|
||||
if (!the_mesh.isTrackerModeEnabled() || armTrackerSleepWake(sleep_secs)) {
|
||||
if (mesh::powerdiag::enabled()) mesh::powerdiag::decision(0, millis());
|
||||
board.sleep(0);
|
||||
if (noise_floor_wait) board.sleepMilliseconds(mesh::power::limitSleepMilliseconds(sleep_secs, noise_floor_delay_ms));
|
||||
else board.sleep(0);
|
||||
}
|
||||
else {
|
||||
if (mesh::powerdiag::enabled())
|
||||
@@ -401,7 +410,8 @@ void loop() {
|
||||
}
|
||||
#else
|
||||
if (mesh::powerdiag::enabled()) mesh::powerdiag::decision(0, millis());
|
||||
board.sleep(0);
|
||||
if (noise_floor_wait) board.sleepMilliseconds(mesh::power::limitSleepMilliseconds(sleep_secs, noise_floor_delay_ms));
|
||||
else board.sleep(0);
|
||||
#endif
|
||||
}
|
||||
#else
|
||||
@@ -409,7 +419,8 @@ void loop() {
|
||||
&& the_mesh.millisHasNowPassed(
|
||||
POWERSAVING_FIRST_SLEEP_SECS * 1000UL)) {
|
||||
if (mesh::powerdiag::enabled()) mesh::powerdiag::decision(0, millis());
|
||||
board.sleep(sleep_secs);
|
||||
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()) {
|
||||
mesh::powerdiag::decision(mesh::powerdiag::mask(mesh::powerdiag::Reason::Startup), millis());
|
||||
}
|
||||
|
||||
@@ -549,6 +549,11 @@ void Dispatcher::logPacketLine(const char* direction, const Packet* packet,
|
||||
}
|
||||
#endif
|
||||
|
||||
bool Dispatcher::getNextNoiseFloorWakeDelay(uint32_t& delay_millis) const {
|
||||
return radio_available
|
||||
&& _radio->getNoiseFloorWakeDelay(_ms->getMillis(), delay_millis);
|
||||
}
|
||||
|
||||
bool Dispatcher::getNextQueueWakeDelay(uint32_t& delay_millis) const {
|
||||
if (isDualRadioActive()) { delay_millis = 0; return true; }
|
||||
#if MESH_ENABLE_FOREIGN_RADIO
|
||||
|
||||
@@ -215,6 +215,15 @@ public:
|
||||
return getNoiseFloorCalibrationSecondsRemaining();
|
||||
}
|
||||
virtual bool isCalibratingNoiseFloor() const { return false; }
|
||||
// MCU service deadline only: RF calibration can continue in continuous RX
|
||||
// while the CPU idles between readings. Unknown implementations retain the
|
||||
// conservative whole-batch veto rather than inventing a safe sample gap.
|
||||
virtual bool getNoiseFloorWakeDelay(uint32_t now, uint32_t& delay_ms) const {
|
||||
(void)now;
|
||||
if (!isCalibratingNoiseFloor()) return false;
|
||||
delay_ms = 0;
|
||||
return true;
|
||||
}
|
||||
// A failed receive start may need a later retry without keeping the MCU
|
||||
// awake. Transports without a deferred receive retry retain their cadence.
|
||||
virtual bool getReceiveRetryDelay(uint32_t now, uint32_t& delay_ms) const {
|
||||
@@ -615,6 +624,9 @@ protected:
|
||||
uint32_t getTransmitAirtime(const Packet* packet, int wire_length) const;
|
||||
|
||||
public:
|
||||
// Kept separate from whole-second queue sleep policy: a 50 ms calibration
|
||||
// deadline needs a bounded millisecond wake, not a zero-second queue veto.
|
||||
bool getNextNoiseFloorWakeDelay(uint32_t& delay_millis) const;
|
||||
void begin();
|
||||
void loop();
|
||||
#if MESH_ENABLE_TX_BEACONS
|
||||
|
||||
@@ -179,6 +179,18 @@ public:
|
||||
#if defined(ARDUINO)
|
||||
if (powerdiag::enabled()) powerdiag::sleepEnd(micros());
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
// Bounded MCU idle without changing the radio's receive mode. Interrupt-idle
|
||||
// ports keep their existing periodic-clock wake contract; timed sleepers
|
||||
// override this to provide a real sub-second timer. An unknown timed port
|
||||
// must not round a short deadline up and sleep past it.
|
||||
virtual void sleepMilliseconds(uint32_t millisecs) {
|
||||
if (millisecs == 0) return;
|
||||
#if defined(NRF52_PLATFORM) || defined(RP2040_PLATFORM) || defined(STM32_PLATFORM)
|
||||
sleep(0);
|
||||
#else
|
||||
if (millisecs >= 1000UL) sleep(millisecs / 1000UL);
|
||||
#endif
|
||||
}
|
||||
virtual uint32_t getGpio() { return 0; }
|
||||
|
||||
@@ -128,6 +128,15 @@ public:
|
||||
}
|
||||
|
||||
void sleep(uint32_t secs) override {
|
||||
sleepMicroseconds(static_cast<uint64_t>(secs) * 1000000ULL);
|
||||
}
|
||||
|
||||
void sleepMilliseconds(uint32_t millisecs) override {
|
||||
if (millisecs != 0)
|
||||
sleepMicroseconds(static_cast<uint64_t>(millisecs) * 1000ULL, true);
|
||||
}
|
||||
|
||||
void sleepMicroseconds(uint64_t wake_us, bool require_timer = false) {
|
||||
using mesh::powerdiag::Reason;
|
||||
const bool diagnostics = mesh::powerdiag::enabled();
|
||||
// Native USB loses its connection in light sleep. An enumerated host
|
||||
@@ -167,8 +176,14 @@ public:
|
||||
// Otherwise its real elapsed time would be mislabelled as blocked time.
|
||||
esp_err_t setup_error = ESP_OK;
|
||||
// Configure timer wakeup
|
||||
if (secs > 0) {
|
||||
const esp_err_t error = esp_sleep_enable_timer_wakeup(secs * 1000000ULL); // Wake up periodically to do scheduled jobs
|
||||
if (wake_us > 0) {
|
||||
const esp_err_t error = esp_sleep_enable_timer_wakeup(wake_us);
|
||||
// A calibration slice must not inherit an old, longer timer after a
|
||||
// setup failure. Keep the legacy seconds API's policy unchanged.
|
||||
if (require_timer && error != ESP_OK) {
|
||||
if (diagnostics) mesh::powerdiag::boardError(error, millis());
|
||||
return;
|
||||
}
|
||||
if (diagnostics && error != ESP_OK) setup_error = error;
|
||||
}
|
||||
|
||||
@@ -181,7 +196,7 @@ public:
|
||||
const int button_gpio = -1;
|
||||
#endif
|
||||
if (!radio_wakeup && button_gpio < 0) {
|
||||
if (secs > 0) {
|
||||
if (wake_us > 0) {
|
||||
if (diagnostics) mesh::powerdiag::sleepBegin(micros());
|
||||
const esp_err_t error = esp_light_sleep_start();
|
||||
if (diagnostics) {
|
||||
|
||||
@@ -17,6 +17,11 @@ inline uint32_t powerSaveSleepSeconds(uint32_t delay_ms) {
|
||||
#endif
|
||||
}
|
||||
|
||||
inline uint32_t limitSleepMilliseconds(uint32_t max_seconds, uint32_t deadline_ms) {
|
||||
const uint64_t maximum_ms = static_cast<uint64_t>(max_seconds) * 1000ULL;
|
||||
return deadline_ms < maximum_ms ? deadline_ms : static_cast<uint32_t>(maximum_ms);
|
||||
}
|
||||
|
||||
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; }
|
||||
|
||||
@@ -31,7 +31,12 @@ public:
|
||||
}
|
||||
bool complete() const { return _count == SAMPLE_COUNT; }
|
||||
bool ready(uint32_t now) const {
|
||||
return !complete() && (_count == 0 || uint32_t(now - _last_sample_at) >= SAMPLE_INTERVAL_MS);
|
||||
return !complete() && sampleDelay(now) == 0;
|
||||
}
|
||||
uint32_t sampleDelay(uint32_t now) const {
|
||||
if (_count == 0 || complete()) return 0;
|
||||
const uint32_t elapsed = now - _last_sample_at;
|
||||
return elapsed >= SAMPLE_INTERVAL_MS ? 0 : SAMPLE_INTERVAL_MS - elapsed;
|
||||
}
|
||||
uint8_t heldBlocks() const { return _held_blocks; }
|
||||
|
||||
|
||||
@@ -23,10 +23,11 @@ extern "C" caddr_t _sbrk(int increment);
|
||||
|
||||
// Bounded noise-floor calibration in continuous RX, including RXPS refreshes.
|
||||
// Requests are coalesced so retries cannot repeatedly force continuous RX.
|
||||
#define NF_CALIB_INTERVAL_MS 2000UL // continuous-RX refresh / forced-window cooldown
|
||||
// A quiet block needs about 3.2 s of continuous RX. Do not reopen one after
|
||||
// only 2 s of duty cycling: routine maintenance would defeat RX power saving.
|
||||
#define NF_CALIB_INTERVAL_MS (60UL * 1000UL)
|
||||
#define NF_RXPS_REFRESH_INTERVAL_MS (60UL * 1000UL)
|
||||
// A successful baseline need not hold the radio in continuous RX every minute.
|
||||
// Rejected rise/failed attempts retain the shorter retry, not this lazy cadence.
|
||||
#define NF_LAZY_REFRESH_INTERVAL_MS (5UL * 60UL * 1000UL)
|
||||
#define NF_CALIB_TIMEOUT_MS NoiseFloorEstimator::WINDOW_TIMEOUT_MS
|
||||
#define NF_CONTINUOUS_TIMEOUT_MS NoiseFloorEstimator::WINDOW_TIMEOUT_MS
|
||||
#define NF_CALIB_SETTLE_MS RadioLibWrapper::NoiseFloorSettleMillis
|
||||
@@ -232,6 +233,7 @@ void RadioLibWrapper::begin() {
|
||||
_floor_estimator.reset(true);
|
||||
_secondary_floor_estimator.reset(true);
|
||||
_nf_calib_active = false;
|
||||
_nf_window_armed = _nf_lazy_refresh = _nf_refresh_attempted = false;
|
||||
_nf_last_calib = 0;
|
||||
_nf_sample_from = 0;
|
||||
_nf_refresh_requested = true; // establish one baseline after startup
|
||||
@@ -784,6 +786,7 @@ void RadioLibWrapper::serviceProfileScan() {
|
||||
_nf_refresh_requested = true;
|
||||
_nf_last_calib = 0;
|
||||
_nf_calib_deadline = 0;
|
||||
_nf_window_armed = _nf_lazy_refresh = _nf_refresh_attempted = false;
|
||||
_floor_estimator.reset(true);
|
||||
_secondary_floor_estimator.reset(true);
|
||||
_profile_refresh_required = true; // refresh side detectors even if the tuple is unchanged
|
||||
@@ -824,6 +827,7 @@ void RadioLibWrapper::serviceProfileScan() {
|
||||
_nf_refresh_requested = true;
|
||||
_nf_last_calib = 0;
|
||||
_nf_calib_deadline = 0;
|
||||
_nf_window_armed = _nf_lazy_refresh = _nf_refresh_attempted = false;
|
||||
_floor_estimator.reset(true);
|
||||
_secondary_floor_estimator.reset(true);
|
||||
}
|
||||
@@ -970,13 +974,7 @@ void RadioLibWrapper::triggerNoiseFloorCalibrate(int threshold) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (_rx_ps_enabled && !_rx_ps_continuous_fallback) {
|
||||
requestPeriodicNoiseFloorRefresh();
|
||||
} else {
|
||||
// RX is already continuous. Keep the original maintenance-tick behavior;
|
||||
// only passive retry probes apply the existing two-second age check.
|
||||
requestNoiseFloorRefresh();
|
||||
}
|
||||
requestPeriodicNoiseFloorRefresh();
|
||||
}
|
||||
|
||||
void RadioLibWrapper::recalibrateNoiseFloor() {
|
||||
@@ -986,29 +984,34 @@ void RadioLibWrapper::recalibrateNoiseFloor() {
|
||||
_noise_floor_secondary_valid = false;
|
||||
_nf_refresh_requested = true;
|
||||
_nf_last_calib = 0;
|
||||
_nf_lazy_refresh = _nf_refresh_attempted = false;
|
||||
_floor_estimator.reset(true);
|
||||
_secondary_floor_estimator.reset(true);
|
||||
|
||||
const unsigned long now = millis();
|
||||
_nf_sample_from = now + NF_CALIB_SETTLE_MS;
|
||||
_nf_sample_from = uint32_t(now) + (_rx_ps_armed ? 0 : NF_CALIB_SETTLE_MS);
|
||||
if (_nf_calib_active) {
|
||||
// Keep an already-open continuous-RX calibration window active, but restart
|
||||
// its sample and timeout bounds around the newly selected gain path.
|
||||
_nf_calib_deadline = now + NF_CALIB_TIMEOUT_MS;
|
||||
_nf_window_armed = true;
|
||||
} else {
|
||||
// Continuous RX starts its deadline in loop(); RX power saving opens a
|
||||
// bounded continuous-RX window in noiseFloorCalibCheck().
|
||||
_nf_calib_deadline = 0;
|
||||
_nf_window_armed = false;
|
||||
}
|
||||
}
|
||||
|
||||
void RadioLibWrapper::requestPeriodicNoiseFloorRefresh() {
|
||||
const uint32_t interval = _rx_ps_enabled && !_rx_ps_continuous_fallback
|
||||
? NF_RXPS_REFRESH_INTERVAL_MS : NF_CALIB_INTERVAL_MS;
|
||||
const uint32_t interval = _nf_lazy_refresh ? NF_LAZY_REFRESH_INTERVAL_MS
|
||||
: (_rx_ps_enabled && !_rx_ps_continuous_fallback
|
||||
? NF_RXPS_REFRESH_INTERVAL_MS : NF_CALIB_INTERVAL_MS);
|
||||
// Startup must not wait for a maintenance tick. Subsequent attempts, even
|
||||
// failed ones, need a real rest interval. Forced RF-path/AGC recalibration
|
||||
// bypasses this routine policy and retains its existing prompt behavior.
|
||||
if (_profiles.enabled() || (!_noise_floor_valid && _nf_last_calib == 0)
|
||||
if (_profiles.enabled()
|
||||
|| (!_noise_floor_valid && _nf_last_calib == 0 && !_nf_refresh_attempted)
|
||||
|| static_cast<uint32_t>(millis() - _nf_last_calib) >= interval) {
|
||||
requestNoiseFloorRefresh();
|
||||
}
|
||||
@@ -1017,9 +1020,12 @@ void RadioLibWrapper::requestPeriodicNoiseFloorRefresh() {
|
||||
void RadioLibWrapper::requestNoiseFloorRefresh() {
|
||||
if (_nf_refresh_requested) return;
|
||||
_nf_refresh_requested = true;
|
||||
_nf_lazy_refresh = false;
|
||||
_floor_estimator.reset();
|
||||
_secondary_floor_estimator.reset();
|
||||
_nf_calib_deadline = 0; // starts when continuous RX is actually available
|
||||
_nf_window_armed = false;
|
||||
_nf_sample_from = uint32_t(millis()) + (_rx_ps_armed ? 0 : NF_CALIB_SETTLE_MS);
|
||||
}
|
||||
|
||||
void RadioLibWrapper::doResetAGC() {
|
||||
@@ -1189,27 +1195,32 @@ bool RadioLibWrapper::isPacketPendingOrReceiving() {
|
||||
}
|
||||
|
||||
void RadioLibWrapper::noiseFloorCalibCheck(unsigned long now) {
|
||||
const uint32_t now_ms = static_cast<uint32_t>(now);
|
||||
if (_nf_calib_active) {
|
||||
if (!_rx_ps_enabled
|
||||
|| ((long)(now - _nf_calib_deadline) >= 0
|
||||
|| (static_cast<int32_t>(now_ms - uint32_t(_nf_calib_deadline)) >= 0
|
||||
&& !_floor_estimator.complete())) {
|
||||
// powersaving turned off mid-window, or the batch couldn't complete
|
||||
// (busy channel) - keep the previous floor
|
||||
endNoiseFloorCalib(now);
|
||||
} else if (_rx_ps_armed && !isPacketPendingOrReceiving()) {
|
||||
} else if (_rx_ps_armed
|
||||
&& static_cast<int32_t>(now_ms - uint32_t(_nf_sample_from)) >= 0) {
|
||||
// A packet can delay the transition from RXPS to continuous RX. Retry
|
||||
// once the radio is idle without discarding a pending interrupt.
|
||||
requestRestartRecv();
|
||||
// at a bounded cadence without discarding a pending interrupt.
|
||||
_nf_sample_from = now_ms + NoiseFloorEstimator::SAMPLE_INTERVAL_MS;
|
||||
if (!isPacketPendingOrReceiving()) requestRestartRecv();
|
||||
}
|
||||
} else if (_nf_refresh_requested && _rx_ps_enabled && _rx_ps_armed && state == STATE_RX
|
||||
&& ((!_noise_floor_valid && _nf_last_calib == 0)
|
||||
|| now - _nf_last_calib >= NF_CALIB_INTERVAL_MS)
|
||||
&& !isPacketPendingOrReceiving()) {
|
||||
&& static_cast<int32_t>(now_ms - uint32_t(_nf_sample_from)) >= 0) {
|
||||
// never interrupt an ongoing reception to calibrate (a TX in flight is
|
||||
// already excluded by state == STATE_RX); retries next loop iteration
|
||||
// already excluded by state == STATE_RX). A requested forced baseline is
|
||||
// prompt; routine cooldown was enforced before creating the request.
|
||||
_nf_sample_from = now_ms + NoiseFloorEstimator::SAMPLE_INTERVAL_MS;
|
||||
if (isPacketPendingOrReceiving()) return;
|
||||
_nf_calib_active = true;
|
||||
_nf_calib_deadline = now + NF_CALIB_TIMEOUT_MS;
|
||||
_nf_sample_from = now + NF_CALIB_SETTLE_MS;
|
||||
_nf_calib_deadline = now_ms + NF_CALIB_TIMEOUT_MS;
|
||||
_nf_window_armed = true;
|
||||
_nf_sample_from = now_ms + NF_CALIB_SETTLE_MS;
|
||||
_floor_estimator.reset();
|
||||
if (!isPacketPendingOrReceiving()) {
|
||||
requestRestartRecv(); // startReceiveMode() selects continuous RX
|
||||
@@ -1222,6 +1233,8 @@ void RadioLibWrapper::endNoiseFloorCalib(unsigned long now) {
|
||||
_nf_refresh_requested = false;
|
||||
_nf_last_calib = now;
|
||||
_nf_calib_deadline = 0;
|
||||
_nf_window_armed = false;
|
||||
_nf_refresh_attempted = true;
|
||||
// force a receive re-arm back into duty-cycle mode, but don't clobber a
|
||||
// completed-but-unread packet or an in-flight TX (recvRaw()/onSendFinished()
|
||||
// will re-arm right after those anyway; same guard style as setRxPowerSaving)
|
||||
@@ -1802,14 +1815,16 @@ void RadioLibWrapper::loop() {
|
||||
}
|
||||
if (_nf_refresh_requested
|
||||
&& (!_rx_ps_enabled || _rx_ps_continuous_fallback)
|
||||
&& _nf_calib_deadline == 0
|
||||
&& !_nf_window_armed
|
||||
&& state == STATE_RX) {
|
||||
// Measure the awake-time bound from actual continuous RX, not from begin()
|
||||
// or a request made while the radio is idle, transmitting, or starting up.
|
||||
_nf_calib_deadline = now + NF_CONTINUOUS_TIMEOUT_MS;
|
||||
_nf_calib_deadline = uint32_t(now) + NF_CONTINUOUS_TIMEOUT_MS;
|
||||
_nf_window_armed = true;
|
||||
}
|
||||
if (_nf_refresh_requested && _floor_estimator.complete()) {
|
||||
if (_floor_estimator.publish(_noise_floor_centi_dbm, _noise_floor_valid)) {
|
||||
_nf_lazy_refresh = _floor_estimator.publish(_noise_floor_centi_dbm, _noise_floor_valid);
|
||||
if (_nf_lazy_refresh) {
|
||||
_noise_floor_valid = true;
|
||||
// Retain the whole-dB API/wire format and the fractional display value.
|
||||
_noise_floor = (_noise_floor_centi_dbm - 50) / 100;
|
||||
@@ -1817,6 +1832,8 @@ void RadioLibWrapper::loop() {
|
||||
_nf_refresh_requested = false;
|
||||
_nf_last_calib = now;
|
||||
_nf_calib_deadline = 0;
|
||||
_nf_window_armed = false;
|
||||
_nf_refresh_attempted = true;
|
||||
|
||||
MESH_DEBUG_PRINTLN("RadioLibWrapper: noise_floor = %d", (int)_noise_floor);
|
||||
|
||||
@@ -1828,14 +1845,17 @@ void RadioLibWrapper::loop() {
|
||||
|
||||
if (_nf_refresh_requested
|
||||
&& (!_rx_ps_enabled || _rx_ps_continuous_fallback)
|
||||
&& _nf_calib_deadline != 0
|
||||
&& (long)(now - _nf_calib_deadline) >= 0) {
|
||||
&& _nf_window_armed
|
||||
&& static_cast<int32_t>(uint32_t(now) - uint32_t(_nf_calib_deadline)) >= 0) {
|
||||
// A continuously busy channel can reject every candidate sample. Do not
|
||||
// keep the MCU awake indefinitely; retain the previous floor and let the
|
||||
// next scheduled calibration request retry an invalid startup baseline.
|
||||
_nf_refresh_requested = false;
|
||||
_nf_last_calib = now;
|
||||
_nf_calib_deadline = 0;
|
||||
_nf_window_armed = false;
|
||||
_nf_lazy_refresh = false;
|
||||
_nf_refresh_attempted = true;
|
||||
_floor_estimator.reset();
|
||||
}
|
||||
|
||||
@@ -1848,12 +1868,12 @@ void RadioLibWrapper::loop() {
|
||||
// ~-127 dBm garbage).
|
||||
if (!_rx_ps_armed
|
||||
&& _floor_estimator.ready(static_cast<uint32_t>(now))
|
||||
&& !(_nf_sample_from != 0 && (long)(now - _nf_sample_from) < 0)
|
||||
&& !isChipBusy() && !isPacketPendingOrReceiving()) {
|
||||
&& static_cast<int32_t>(uint32_t(now) - uint32_t(_nf_sample_from)) >= 0) {
|
||||
// Silent RX loss can leave software in STATE_RX while standby RSSI is
|
||||
// meaningless. Reject that observation before it can lower the floor.
|
||||
// Space rejected attempts too; an empty block must not hammer SPI.
|
||||
_nf_sample_from = now + NoiseFloorEstimator::SAMPLE_INTERVAL_MS;
|
||||
_nf_sample_from = uint32_t(now) + NoiseFloorEstimator::SAMPLE_INTERVAL_MS;
|
||||
if (isChipBusy() || isPacketPendingOrReceiving()) return;
|
||||
if (readReceiveMode() == 0) {
|
||||
_floor_estimator.reset(true);
|
||||
return;
|
||||
@@ -1882,7 +1902,8 @@ void RadioLibWrapper::startRecv() {
|
||||
state = STATE_RX;
|
||||
// RSSI is unsettled after every RX entry, including CAD/TX completion.
|
||||
// Keep the partial block, but wait for the frontend before its next sample.
|
||||
_nf_sample_from = millis() + NF_CALIB_SETTLE_MS;
|
||||
_nf_sample_from = uint32_t(millis())
|
||||
+ (_rx_ps_armed ? 0 : NF_CALIB_SETTLE_MS);
|
||||
_startrx_fails = 0;
|
||||
_rx_retry_pending = false;
|
||||
_rx_retry_delay_ms = 0;
|
||||
@@ -1968,6 +1989,34 @@ bool RadioLibWrapper::getReceiveRetryDelay(uint32_t now, uint32_t& delay_ms) con
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RadioLibWrapper::getNoiseFloorWakeDelay(uint32_t now, uint32_t& delay_ms) const {
|
||||
#if MESH_ENABLE_TX_BEACONS
|
||||
if (_beacon_owned) return false;
|
||||
#endif
|
||||
// Dual-profile scanning and foreign owners retain their existing cadence.
|
||||
// Failed RX startup has its separate retry deadline, not an active batch.
|
||||
if (_cw_active || _profiles.enabled() || state != STATE_RX
|
||||
|| (!_nf_refresh_requested && !_nf_calib_active)) return false;
|
||||
if (_floor_estimator.complete()
|
||||
|| (!_nf_window_armed && !_rx_ps_armed)) {
|
||||
delay_ms = 0; // Publish or establish the continuous-RX timeout first.
|
||||
return true;
|
||||
}
|
||||
|
||||
const int32_t until_attempt = static_cast<int32_t>(uint32_t(_nf_sample_from) - now);
|
||||
delay_ms = until_attempt > 0 ? uint32_t(until_attempt) : 0;
|
||||
if (!_rx_ps_armed) {
|
||||
const uint32_t until_sample = _floor_estimator.sampleDelay(now);
|
||||
if (until_sample > delay_ms) delay_ms = until_sample;
|
||||
}
|
||||
if (_nf_window_armed) {
|
||||
const int32_t until_timeout = static_cast<int32_t>(uint32_t(_nf_calib_deadline) - now);
|
||||
const uint32_t timeout = until_timeout > 0 ? uint32_t(until_timeout) : 0;
|
||||
if (timeout < delay_ms) delay_ms = timeout;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// RX PowerSaving
|
||||
bool RadioLibWrapper::setRxPowerSaving(bool enabled, uint32_t rx_us, uint32_t sleep_us) {
|
||||
#if MESH_ENABLE_TX_BEACONS
|
||||
|
||||
@@ -171,6 +171,12 @@ protected:
|
||||
// refresh briefly drops to continuous RX, publishes an average, then re-arms
|
||||
// the duty cycle.
|
||||
bool _nf_calib_active;
|
||||
// Explicit arming distinguishes a legitimate wrapped deadline of zero.
|
||||
bool _nf_window_armed = false;
|
||||
// Only an accepted full block earns the lazy routine cadence. Held rises
|
||||
// and failed/timeout attempts retain a one-minute retry without hot polling.
|
||||
bool _nf_lazy_refresh = false;
|
||||
bool _nf_refresh_attempted = false;
|
||||
unsigned long _nf_last_calib; // millis of last completed/attempted window
|
||||
unsigned long _nf_calib_deadline; // abort window if the batch can't complete
|
||||
unsigned long _nf_sample_from; // no samples before this (RX entry settle)
|
||||
@@ -532,14 +538,15 @@ public:
|
||||
// true while the watchdog is actively watching for BUSY transitions; used by
|
||||
// the app's hasPendingWork() to keep the MCU out of light sleep for the window
|
||||
bool isWatchdogObserving() const { return _wd_observe_until != 0; }
|
||||
// true while a noise-floor batch needs prompt loop service; the app's
|
||||
// hasPendingWork() keeps the MCU awake for the bounded spaced-sample window.
|
||||
// Full-batch RF ownership for UI and recovery guards. CPU idle between
|
||||
// readings is governed separately by getNoiseFloorWakeDelay().
|
||||
bool isCalibratingNoiseFloor() const override {
|
||||
return isInRecvMode() && !_rx_ps_armed && (_nf_calib_active
|
||||
|| (_nf_refresh_requested
|
||||
&& (!_rx_ps_enabled || _rx_ps_continuous_fallback)));
|
||||
}
|
||||
bool getReceiveRetryDelay(uint32_t now, uint32_t& delay_ms) const override;
|
||||
bool getNoiseFloorWakeDelay(uint32_t now, uint32_t& delay_ms) const override;
|
||||
void resetStats() { n_recv = n_sent = n_recv_errors = 0; }
|
||||
|
||||
uint8_t getRadioState() const override;
|
||||
|
||||
Vendored
+35
-2
@@ -1,5 +1,6 @@
|
||||
#include <helpers/MorseLoopPolicy.h>
|
||||
#include <helpers/PowerSavingDiagnostics.h>
|
||||
#include <helpers/PowerManagementUtils.h>
|
||||
#include <cstdarg>
|
||||
static unsigned usb_services=0,command_services=0,transport_services=0,delays=0;
|
||||
static bool usb_watchdog_armed=false,wireless_pending=false;
|
||||
@@ -30,10 +31,12 @@ Control& control() { return controller; }
|
||||
}
|
||||
struct Board {
|
||||
unsigned calls=0,watchdogs=0,sleeps=0,fans=0,reboots=0,preparations=0;
|
||||
unsigned millisecond_sleeps=0,last_sleep_ms=0;
|
||||
void prepareSleepCycle() { ++preparations; }
|
||||
void loop() { ++calls; }
|
||||
void feedWatchdog(bool=true) { ++watchdogs; }
|
||||
void sleep(uint32_t) { ++sleeps; }
|
||||
void sleepMilliseconds(uint32_t ms) { ++sleeps; ++millisecond_sleeps; last_sleep_ms=ms; }
|
||||
bool isUsbDataConnected() { return false; }
|
||||
bool isUsbHostConnected() { return false; }
|
||||
void reboot() { ++reboots; }
|
||||
@@ -49,13 +52,20 @@ bool display_ready=true;
|
||||
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;
|
||||
void loop() { ++calls; }
|
||||
Prefs* getNodePrefs() { return &prefs; }
|
||||
uint32_t getPowerSaveSleepSeconds(unsigned) { return 1; }
|
||||
uint32_t getPowerSaveSleepSeconds(unsigned) { return work_pending ? 0 : 1; }
|
||||
bool getNextNoiseFloorWakeDelay(uint32_t& ms) const {
|
||||
ms=noise_floor_delay_ms; return noise_floor_wait;
|
||||
}
|
||||
bool millisHasNowPassed(uint32_t) { return true; }
|
||||
bool isTrackerModeEnabled() { return false; }
|
||||
bool hasPendingWork() { return false; }
|
||||
mesh::powerdiag::Mask pendingWorkReason() const { return 0; }
|
||||
mesh::powerdiag::Mask pendingWorkReason() const {
|
||||
return work_pending ? mesh::powerdiag::mask(mesh::powerdiag::Reason::Queue) : 0;
|
||||
}
|
||||
void handleCommand(uint32_t,char*,char* reply) { reply[0]=0; ++command_services; }
|
||||
void handleUsbCommand(char*,char* reply) { reply[0]=0; ++command_services; }
|
||||
} the_mesh;
|
||||
@@ -128,4 +138,27 @@ int main() {
|
||||
assert(board.sleeps==sleeps_before_hold);
|
||||
wireless_pending=false;
|
||||
#endif
|
||||
// Calibration remains RF work, but its future sample deadline permits
|
||||
// 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;
|
||||
loop();
|
||||
assert(board.sleeps==before_noise_sleep+1 && board.millisecond_sleeps==1);
|
||||
#if ROLE_COMPANION
|
||||
assert(board.last_sleep_ms==10); // Preserve the shorter Companion cadence.
|
||||
#else
|
||||
assert(board.last_sleep_ms==50);
|
||||
#endif
|
||||
the_mesh.noise_floor_delay_ms=1;
|
||||
loop();
|
||||
assert(board.sleeps==before_noise_sleep+2 && board.last_sleep_ms==1);
|
||||
the_mesh.noise_floor_delay_ms=0;
|
||||
loop();
|
||||
assert(board.sleeps==before_noise_sleep+2); // Sample/transition now due.
|
||||
the_mesh.noise_floor_delay_ms=50; the_mesh.work_pending=true;
|
||||
loop();
|
||||
assert(board.sleeps==before_noise_sleep+2); // Never widen another owner's veto.
|
||||
the_mesh.work_pending=false; usb_watchdog_armed=true;
|
||||
loop();
|
||||
assert(board.sleeps==before_noise_sleep+2);
|
||||
}
|
||||
Vendored
+189
@@ -0,0 +1,189 @@
|
||||
|
||||
static const char* scenario;
|
||||
#define CHECK(expression, diagnostic) do { if (!(expression)) { \
|
||||
std::fprintf(stderr, "FAIL %s: %s (line %d)\n", scenario, diagnostic, __LINE__); \
|
||||
return 1; } } while (0)
|
||||
|
||||
static void prepare(RadioLibWrapper& w, unsigned mode, uint32_t start = 100) {
|
||||
now_ms = start;
|
||||
w._rx_ps_enabled = mode != 0;
|
||||
w._rx_ps_continuous_fallback = mode == 2;
|
||||
w._rx_ps_armed = mode == 1;
|
||||
w._noise_floor_valid = false;
|
||||
w._nf_refresh_requested = true;
|
||||
w._nf_last_calib = 0;
|
||||
w.startRecv();
|
||||
w.loop();
|
||||
if (w.state == STATE_IDLE) w.startRecv();
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc != 2) return 2;
|
||||
scenario = argv[1];
|
||||
if (!std::strcmp(scenario, "quiet")) {
|
||||
for (unsigned mode : {0U, 1U, 2U}) {
|
||||
RadioLibWrapper w;
|
||||
prepare(w, mode);
|
||||
const uint32_t start = now_ms;
|
||||
uint32_t eligible_ms = 0;
|
||||
unsigned iterations = 0;
|
||||
while (w._nf_refresh_requested || w._nf_calib_active) {
|
||||
CHECK(++iterations < 200, "deadline service must remain bounded");
|
||||
uint32_t wait = UINT32_MAX;
|
||||
CHECK(w.getNoiseFloorWakeDelay(now_ms, wait), "active batch lost wake deadline");
|
||||
CHECK(wait > 0 || w._floor_estimator.complete(),
|
||||
"pending batch must permit idle between samples");
|
||||
CHECK(wait <= NoiseFloorEstimator::SAMPLE_INTERVAL_MS,
|
||||
"sample deadline exceeded original spacing");
|
||||
eligible_ms += wait;
|
||||
now_ms += wait;
|
||||
w.loop();
|
||||
if (w.state == STATE_IDLE) w.startRecv();
|
||||
}
|
||||
CHECK(w.reads == NoiseFloorEstimator::SAMPLE_COUNT,
|
||||
"idle scheduling changed accepted sample count");
|
||||
CHECK(w._noise_floor_valid && w._noise_floor_centi_dbm == -10000,
|
||||
"idle scheduling changed published estimator value");
|
||||
CHECK(w._rx_ps_armed == (mode == 1), "calibration did not restore receive policy");
|
||||
CHECK(eligible_ms >= 3150 && uint32_t(now_ms - start) < 3300,
|
||||
"quiet collection must retain original quality and bounded cadence");
|
||||
}
|
||||
} else if (!std::strcmp(scenario, "rollover")) {
|
||||
for (uint32_t start : {100U, UINT32_MAX - 3U, uint32_t(0U - NF_CALIB_SETTLE_MS)}) {
|
||||
RadioLibWrapper w;
|
||||
prepare(w, 0, start);
|
||||
uint32_t wait = 0;
|
||||
CHECK(w.reads == 0, "RX settling must precede every first sample");
|
||||
CHECK(w.getNoiseFloorWakeDelay(now_ms, wait) && wait == NF_CALIB_SETTLE_MS,
|
||||
"settle deadline must survive rollover, including timestamp zero");
|
||||
now_ms += wait - 1;
|
||||
w.loop();
|
||||
CHECK(w.reads == 0 && w.getNoiseFloorWakeDelay(now_ms, wait) && wait == 1,
|
||||
"sample accepted before settle deadline");
|
||||
++now_ms;
|
||||
CHECK(w.getNoiseFloorWakeDelay(now_ms, wait) && wait == 0,
|
||||
"exact sample boundary must request service");
|
||||
w.loop();
|
||||
CHECK(w.reads == 1 && w.getNoiseFloorWakeDelay(now_ms, wait) && wait == 50,
|
||||
"next sample spacing must survive rollover");
|
||||
now_ms += 49;
|
||||
w.loop();
|
||||
CHECK(w.reads == 1, "early sample changed original sample spacing");
|
||||
++now_ms;
|
||||
w.loop();
|
||||
CHECK(w.reads == 2, "next sample deadline not serviced");
|
||||
}
|
||||
} else if (!std::strcmp(scenario, "queries")) {
|
||||
RadioLibWrapper w;
|
||||
prepare(w, 1);
|
||||
uint32_t wait = 0;
|
||||
const unsigned reads = w.reads, modes = w.mode_reads;
|
||||
const unsigned busy = w.busy_probes, packets = w.packet_probes;
|
||||
const auto count = w._floor_estimator.count();
|
||||
for (unsigned i = 0; i < 100; ++i)
|
||||
CHECK(w.getNoiseFloorWakeDelay(now_ms, wait) && wait > 0,
|
||||
"query must preserve pending sample deadline");
|
||||
CHECK(w.reads == reads && w.mode_reads == modes && w.busy_probes == busy
|
||||
&& w.packet_probes == packets && w._floor_estimator.count() == count,
|
||||
"deadline query must not access hardware or mutate samples");
|
||||
w.events.record();
|
||||
CHECK(w.getNoiseFloorWakeDelay(now_ms, wait) && w.hasPendingRadioInterrupt(),
|
||||
"deadline query consumed radio IRQ");
|
||||
now_ms += NF_CALIB_SETTLE_MS;
|
||||
w.loop();
|
||||
CHECK(w.hasPendingRadioInterrupt() && w.reads == reads,
|
||||
"pending packet IRQ must retain ownership during sampling service");
|
||||
w.claimRadioInterrupt();
|
||||
now_ms += NoiseFloorEstimator::SAMPLE_INTERVAL_MS;
|
||||
w.inject_mode_irq = true;
|
||||
w.loop();
|
||||
CHECK(w.hasPendingRadioInterrupt() && w.reads == reads,
|
||||
"IRQ during status probe must prevent RSSI read and remain pending");
|
||||
} else if (!std::strcmp(scenario, "rejected")) {
|
||||
for (bool busy : {false, true}) for (uint32_t start : {100U, UINT32_MAX - 17U}) {
|
||||
RadioLibWrapper w;
|
||||
prepare(w, 1, start);
|
||||
w._noise_floor_valid = true;
|
||||
w.packet = !busy;
|
||||
w.busy = busy;
|
||||
unsigned iterations = 0;
|
||||
while (w._nf_refresh_requested || w._nf_calib_active) {
|
||||
CHECK(++iterations < 1000, "rejected-sample retries must not hot poll");
|
||||
uint32_t wait = 0;
|
||||
CHECK(w.getNoiseFloorWakeDelay(now_ms, wait), "rejected sample lost timeout wake");
|
||||
CHECK(wait > 0, "rejected sample must permit bounded idle before retry");
|
||||
now_ms += wait;
|
||||
w.loop();
|
||||
if (w.state == STATE_IDLE) w.startRecv();
|
||||
}
|
||||
CHECK(w.reads == 0 && w._noise_floor_centi_dbm == -10500,
|
||||
"rejected reads must retain published floor");
|
||||
CHECK(uint32_t(now_ms - start) <= NoiseFloorEstimator::WINDOW_TIMEOUT_MS + 50,
|
||||
"idle must not extend calibration timeout");
|
||||
}
|
||||
} else if (!std::strcmp(scenario, "lazy")) {
|
||||
for (unsigned path : {0U, 1U, 2U}) for (bool accepted : {false, true})
|
||||
for (uint32_t start : {1000U, UINT32_MAX - 3149U}) {
|
||||
RadioLibWrapper w;
|
||||
now_ms = start;
|
||||
for (unsigned i = 0; i < NoiseFloorEstimator::SAMPLE_COUNT; ++i) {
|
||||
now_ms = start + i * NoiseFloorEstimator::SAMPLE_INTERVAL_MS;
|
||||
CHECK(w._floor_estimator.add(accepted ? -105 : -70, now_ms),
|
||||
"policy fixture must use original complete spaced blocks");
|
||||
}
|
||||
w.loop();
|
||||
CHECK(!w._nf_refresh_requested && w._nf_refresh_attempted,
|
||||
"complete publish attempt must retire calibration");
|
||||
CHECK(w._nf_lazy_refresh == accepted && w._noise_floor_valid,
|
||||
"held rise must not earn slow refresh merely because old floor is valid");
|
||||
const uint32_t finish = now_ms;
|
||||
const uint32_t interval = accepted ? NF_LAZY_REFRESH_INTERVAL_MS : NF_CALIB_INTERVAL_MS;
|
||||
for (uint32_t age : {1U, interval - 1}) {
|
||||
now_ms = finish + age;
|
||||
if (path == 0) w.triggerNoiseFloorCalibrate(7);
|
||||
else if (path == 1) w.isReceivingPassive(7);
|
||||
else w.requestPeriodicNoiseFloorRefresh();
|
||||
CHECK(!w._nf_refresh_requested, "routine caller ignored lazy/retry cooldown");
|
||||
}
|
||||
now_ms = finish + interval;
|
||||
if (path == 0) w.triggerNoiseFloorCalibrate(7);
|
||||
else if (path == 1) w.isReceivingPassive(7);
|
||||
else w.requestPeriodicNoiseFloorRefresh();
|
||||
CHECK(w._nf_refresh_requested, "routine refresh missed its selected cadence");
|
||||
w._nf_refresh_requested = false;
|
||||
w._nf_lazy_refresh = true;
|
||||
w._nf_last_calib = now_ms;
|
||||
w.recalibrateNoiseFloor();
|
||||
CHECK(w._nf_refresh_requested && !w._nf_lazy_refresh && !w._noise_floor_valid,
|
||||
"forced tuning/gain baseline must bypass lazy routine cadence");
|
||||
}
|
||||
} else if (!std::strcmp(scenario, "timeout-zero")) {
|
||||
RadioLibWrapper w;
|
||||
// A legitimate 30-second timeout lands exactly on uint32_t zero.
|
||||
const uint32_t start = uint32_t(0U - NoiseFloorEstimator::WINDOW_TIMEOUT_MS);
|
||||
prepare(w, 0, start);
|
||||
w.packet = true;
|
||||
w._noise_floor_valid = false;
|
||||
CHECK(w._nf_window_armed && uint32_t(w._nf_calib_deadline) == 0,
|
||||
"wrapped timeout must retain explicit armed state");
|
||||
now_ms = UINT32_MAX;
|
||||
w.loop();
|
||||
uint32_t wait = 0;
|
||||
CHECK(w._nf_refresh_requested && w.getNoiseFloorWakeDelay(now_ms, wait) && wait == 1,
|
||||
"last timeout millisecond must remain idle eligible");
|
||||
now_ms = 0;
|
||||
CHECK(w.getNoiseFloorWakeDelay(now_ms, wait) && wait == 0,
|
||||
"wrapped timeout must request exact-boundary service");
|
||||
w.loop();
|
||||
CHECK(!w._nf_refresh_requested && w._nf_refresh_attempted && w._nf_last_calib == 0,
|
||||
"timeout at timestamp zero must retire failed attempt");
|
||||
w.packet = false;
|
||||
now_ms = NF_CALIB_INTERVAL_MS - 1;
|
||||
w.requestPeriodicNoiseFloorRefresh();
|
||||
CHECK(!w._nf_refresh_requested, "timestamp-zero failure must not masquerade as startup");
|
||||
++now_ms;
|
||||
w.requestPeriodicNoiseFloorRefresh();
|
||||
CHECK(w._nf_refresh_requested, "timestamp-zero failure missed bounded retry");
|
||||
} else return 2;
|
||||
std::printf("PASS: %s\n", scenario);
|
||||
}
|
||||
+30
-1
@@ -20,6 +20,35 @@ static uint32_t retryDelay(const RadioLibWrapper& w) {
|
||||
return delay;
|
||||
}
|
||||
|
||||
static void noiseFloorDeadlines() {
|
||||
for (unsigned mode : {0U, 1U, 2U}) {
|
||||
RadioLibWrapper w; RetryClock clock; StaticPoolPacketManager manager{4};
|
||||
RetryDispatcher d(w, clock, manager); d.interval = 0;
|
||||
now_ms = 1000; d.begin(); selectMode(w, mode);
|
||||
d.loop(); d.loop();
|
||||
uint32_t wait = 0;
|
||||
assert(d.getNextNoiseFloorWakeDelay(wait) && wait == NF_CALIB_SETTLE_MS);
|
||||
// A subsecond sample is not a whole-second queue veto on timed sleepers.
|
||||
uint32_t queue = UINT32_MAX;
|
||||
assert(!d.getNextQueueWakeDelay(queue) && !d.hasQueuedWorkDue());
|
||||
const unsigned reads = w.reads, probes = w.mode_reads;
|
||||
const unsigned busy = w.busy_probes, packets = w.packet_probes;
|
||||
for (unsigned i = 0; i < 100; ++i)
|
||||
assert(d.getNextNoiseFloorWakeDelay(wait) && wait == NF_CALIB_SETTLE_MS);
|
||||
assert(w.reads == reads && w.mode_reads == probes && w.busy_probes == busy
|
||||
&& w.packet_probes == packets);
|
||||
now_ms += wait; d.loop();
|
||||
assert(w.reads == 1 && d.getNextNoiseFloorWakeDelay(wait) && wait == 50);
|
||||
w.events.record();
|
||||
assert(d.getNextNoiseFloorWakeDelay(wait) && w.hasPendingRadioInterrupt());
|
||||
w.radio.packet_length = 1;
|
||||
d.loop();
|
||||
assert(w.n_recv == 1 && !w.hasPendingRadioInterrupt());
|
||||
assert(!d.hasQueuedWorkDue());
|
||||
}
|
||||
std::puts("PASS: actual Dispatcher noise-floor deadlines (3 modes)");
|
||||
}
|
||||
|
||||
static void persistentFailure() {
|
||||
uint8_t bytes[8];
|
||||
for (unsigned mode : {0U, 1U, 2U}) {
|
||||
@@ -327,7 +356,7 @@ int main(int argc, char** argv) {
|
||||
return 0;
|
||||
}
|
||||
assert(argc == 1);
|
||||
persistentFailure(); rampAndRollover(); ownersAndImmediateRearms();
|
||||
noiseFloorDeadlines(); persistentFailure(); rampAndRollover(); ownersAndImmediateRearms();
|
||||
pendingSetupAndTx(); retainedOutboundWake(); busyAfterFailure();
|
||||
deferredCalibrationRestore();
|
||||
std::puts("PASS: receive retry lifecycle");
|
||||
|
||||
@@ -271,6 +271,7 @@ struct Mesh {
|
||||
mesh::powerdiag::Mask pendingWorkReason() const {
|
||||
return pending ? mesh::powerdiag::mask(mesh::powerdiag::Reason::Mesh) : 0;
|
||||
}
|
||||
bool getNextNoiseFloorWakeDelay(uint32_t&) const { return false; }
|
||||
} the_mesh;
|
||||
struct Board { bool usb; bool isUsbHostConnected() const { return usb; } } board;
|
||||
struct Ethernet { bool active; bool hardwareActive() const { return active; } } ethernet_interface;
|
||||
|
||||
@@ -273,6 +273,7 @@ struct MyMesh {
|
||||
bool hasPendingOtaApply() const { return false; }
|
||||
bool hasPendingReqs() const { return _delayed_replies.hasRequest(); }
|
||||
bool hasQueuedWorkDue() const { return false; }
|
||||
bool getNextNoiseFloorWakeDelay(uint32_t&) const { return false; }
|
||||
bool hasRetryWorkDue() const { return false; }
|
||||
bool hasPendingWork() const;
|
||||
mesh::powerdiag::Mask pendingWorkReason() const;
|
||||
|
||||
@@ -228,6 +228,7 @@ struct MyMesh {
|
||||
bool isMillisTimerDue(unsigned long) const { return false; }
|
||||
bool getNextQueueWakeDelay(uint32_t&) const { return false; }
|
||||
bool getNextRetryWakeDelay(uint32_t&) const { return false; }
|
||||
bool getNextNoiseFloorWakeDelay(uint32_t&) const { return false; }
|
||||
bool isDualRadioActive() const { return false; }
|
||||
uint32_t limitSleepToMillisTimer(unsigned long, uint32_t secs) const { return secs; }
|
||||
uint32_t limitSleepToScheduledRadioWork(uint32_t secs) const { return secs; }
|
||||
|
||||
@@ -269,7 +269,10 @@ class FEMBoard:public ESP32Board { void shutdownPeripherals() override {
|
||||
int main(){FEMBoard board; board.enterDeepSleep(5); assert(slept);}
|
||||
'''.replace('@SLEEP@',sleep),s3=1)
|
||||
header=(ROOT/'src/helpers/ESP32Board.h').read_text()
|
||||
light=body(header,'void sleep(uint32_t secs) override')
|
||||
light='\n'.join(body(header, signature) for signature in (
|
||||
'void sleep(uint32_t secs) override',
|
||||
'void sleepMilliseconds(uint32_t millisecs) override',
|
||||
'void sleepMicroseconds(uint64_t wake_us, bool require_timer = false)'))
|
||||
self.assertNotIn('commitTerminalDeepSleepHolds',light)
|
||||
for folder in (ROOT/'src',ROOT/'variants'):
|
||||
for path in folder.rglob('*'):
|
||||
|
||||
@@ -70,6 +70,7 @@ constexpr int GPIO_INTR_POSEDGE = 1;
|
||||
constexpr int GPIO_INTR_DISABLE = 0;
|
||||
static unsigned sleep_calls = 0;
|
||||
static uint64_t timer_us = 0;
|
||||
static int timer_error = ESP_OK;
|
||||
static bool radio_irq_high = false;
|
||||
static int critical_depth = 0;
|
||||
static int wake_pin = -1;
|
||||
@@ -80,7 +81,11 @@ static bool button_pressed = false;
|
||||
static bool press_during_sleep = false;
|
||||
static unsigned yield_ms = 0;
|
||||
static void delay(unsigned ms) { yield_ms += ms; }
|
||||
static int esp_sleep_enable_timer_wakeup(uint64_t us) { timer_us = us; return ESP_OK; }
|
||||
static int esp_sleep_enable_timer_wakeup(uint64_t us) {
|
||||
if (timer_error != ESP_OK) return timer_error;
|
||||
timer_us = us;
|
||||
return ESP_OK;
|
||||
}
|
||||
static int esp_light_sleep_start() {
|
||||
++sleep_calls;
|
||||
if (press_during_sleep) {
|
||||
@@ -116,6 +121,7 @@ struct MainBoard {
|
||||
bool radio_test_active = false;
|
||||
bool isRadioTestActive() const { return radio_test_active; }
|
||||
virtual void sleep(uint32_t) = 0;
|
||||
virtual void sleepMilliseconds(uint32_t) = 0;
|
||||
virtual bool isUsbDataConnected() = 0;
|
||||
virtual bool isUsbHostConnected() = 0;
|
||||
};
|
||||
@@ -257,6 +263,13 @@ def board_method(signature: str) -> str:
|
||||
return source[start:end]
|
||||
|
||||
|
||||
def board_sleep_methods() -> str:
|
||||
return "\n".join(board_method(signature) for signature in (
|
||||
"void sleep(uint32_t secs) override",
|
||||
"void sleepMilliseconds(uint32_t millisecs) override",
|
||||
"void sleepMicroseconds(uint64_t wake_us, bool require_timer = false)"))
|
||||
|
||||
|
||||
def v4_profile_config():
|
||||
config = configparser.ConfigParser(
|
||||
interpolation=None, inline_comment_prefixes=("#", ";"))
|
||||
@@ -370,6 +383,7 @@ static int esp_light_sleep_start() {
|
||||
#define portEXIT_CRITICAL(mux) (--critical_depth)
|
||||
struct MainBoard {
|
||||
virtual void sleep(uint32_t) = 0;
|
||||
virtual void sleepMilliseconds(uint32_t) = 0;
|
||||
virtual bool isUsbDataConnected() = 0;
|
||||
virtual bool isUsbHostConnected() = 0;
|
||||
bool isRadioTestActive() const { return false; }
|
||||
@@ -509,8 +523,7 @@ class Esp32UsbSleepTest(unittest.TestCase):
|
||||
|
||||
def test_v4_sleep_button_poller_and_real_ui_wake_the_screen(self):
|
||||
config = v4_profile_config()
|
||||
methods = "\n".join(board_method(signature) for signature in (
|
||||
"void sleep(uint32_t secs) override",
|
||||
methods = board_sleep_methods() + "\n" + "\n".join(board_method(signature) for signature in (
|
||||
"bool isUsbDataConnected() override",
|
||||
"bool isUsbHostConnected() override", "bool isUsbSleepHeld()"))
|
||||
compiler = os.environ.get("CXX", "c++")
|
||||
@@ -593,8 +606,8 @@ class Esp32UsbSleepTest(unittest.TestCase):
|
||||
self.assertIn(expected_error, result.stderr)
|
||||
|
||||
def test_raw_uart_driver_preserves_fresh_console_indefinitely_until_end(self):
|
||||
methods = "\n".join(board_method(signature) for signature in (
|
||||
"void sleep(uint32_t secs) override", "bool isUsbDataConnected() override",
|
||||
methods = board_sleep_methods() + "\n" + "\n".join(board_method(signature) for signature in (
|
||||
"bool isUsbDataConnected() override",
|
||||
"bool isUsbHostConnected() override", "bool isUsbSleepHeld()"))
|
||||
harness = HARNESS.split("int main()", 1)[0] + r'''
|
||||
int main() {
|
||||
@@ -639,8 +652,7 @@ int main() {
|
||||
def test_native_host_survives_terminal_close_and_reconnect(self):
|
||||
compiler = os.environ.get("CXX", "c++")
|
||||
self.assertIsNotNone(shutil.which(compiler), "a C++17 compiler is required")
|
||||
methods = "\n".join(board_method(signature) for signature in (
|
||||
"void sleep(uint32_t secs) override",
|
||||
methods = board_sleep_methods() + "\n" + "\n".join(board_method(signature) for signature in (
|
||||
"bool isUsbDataConnected() override",
|
||||
"bool isUsbHostConnected() override",
|
||||
"bool isUsbSleepHeld()",
|
||||
@@ -669,8 +681,7 @@ int main() {
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
||||
def test_native_host_reboot_grace_without_logging(self):
|
||||
methods = "\n".join(board_method(signature) for signature in (
|
||||
"void sleep(uint32_t secs) override",
|
||||
methods = board_sleep_methods() + "\n" + "\n".join(board_method(signature) for signature in (
|
||||
"bool isUsbDataConnected() override",
|
||||
"bool isUsbHostConnected() override",
|
||||
"bool isUsbSleepHeld()",
|
||||
@@ -778,6 +789,118 @@ int main() {
|
||||
result = subprocess.run([str(binary)], capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
||||
def test_millisecond_sleep_preserves_precision_and_all_common_guards(self):
|
||||
methods = board_sleep_methods() + "\n" + "\n".join(board_method(signature) for signature in (
|
||||
"bool isUsbDataConnected() override",
|
||||
"bool isUsbHostConnected() override", "bool isUsbSleepHeld()"))
|
||||
harness = HARNESS.split("int main()", 1)[0] + r'''
|
||||
int main() {
|
||||
try {
|
||||
ESP32Board board;
|
||||
MainBoard& polymorphic = board;
|
||||
attachHost(true);
|
||||
const unsigned before_zero = yield_ms;
|
||||
polymorphic.sleepMilliseconds(0);
|
||||
require(sleep_calls == 0 && timer_us == 0 && wake_pin == -1
|
||||
&& critical_depth == 0 && yield_ms == before_zero,
|
||||
"zero milliseconds entered sleep or touched peripherals");
|
||||
polymorphic.sleepMilliseconds(125);
|
||||
require(sleep_calls == 0 && timer_us == 0 && critical_depth == 0,
|
||||
"millisecond path bypassed the USB/UART console guard");
|
||||
attachHost(false);
|
||||
Serial.end();
|
||||
mock_millis += MESH_ESP32_USB_HOST_LOSS_SLEEP_GRACE_MS;
|
||||
polymorphic.sleepMilliseconds(1);
|
||||
require(sleep_calls == 1 && timer_us == 1000ULL,
|
||||
"one millisecond did not arm an exact subsecond wake timer");
|
||||
polymorphic.sleepMilliseconds(999);
|
||||
require(sleep_calls == 2 && timer_us == 999000ULL,
|
||||
"millisecond timer was rounded to seconds");
|
||||
polymorphic.sleepMilliseconds(UINT32_MAX);
|
||||
require(sleep_calls == 3 && timer_us == 4294967295000ULL,
|
||||
"millisecond conversion overflowed before widening");
|
||||
board.sleep(UINT32_MAX);
|
||||
require(sleep_calls == 4 && timer_us == 4294967295000000ULL,
|
||||
"existing seconds API lost precision or overflowed");
|
||||
timer_error = 17;
|
||||
polymorphic.sleepMilliseconds(125);
|
||||
require(sleep_calls == 4 && timer_us == 4294967295000000ULL
|
||||
&& critical_depth == 0,
|
||||
"failed millisecond timer setup reused an older long timer");
|
||||
timer_error = ESP_OK;
|
||||
const unsigned before_guards = sleep_calls;
|
||||
board.inhibit_sleep = true;
|
||||
polymorphic.sleepMilliseconds(125);
|
||||
require(sleep_calls == before_guards && critical_depth == 0,
|
||||
"millisecond path bypassed explicit sleep inhibition");
|
||||
board.inhibit_sleep = false;
|
||||
board.radio_test_active = true;
|
||||
polymorphic.sleepMilliseconds(125);
|
||||
require(sleep_calls == before_guards && critical_depth == 0,
|
||||
"millisecond path bypassed radio test ownership");
|
||||
board.radio_test_active = false;
|
||||
radio_irq_high = true;
|
||||
polymorphic.sleepMilliseconds(125);
|
||||
require(sleep_calls == before_guards && critical_depth == 0,
|
||||
"millisecond path slept over a pending radio IRQ");
|
||||
radio_irq_high = false;
|
||||
#if MOMENTARY_BUTTON_WAKE_FROM_SLEEP
|
||||
button_pressed = true;
|
||||
polymorphic.sleepMilliseconds(125);
|
||||
require(sleep_calls == before_guards && critical_depth == 0,
|
||||
"millisecond path slept over a pending button press");
|
||||
button_pressed = false;
|
||||
#endif
|
||||
mesh::logging_enabled = true;
|
||||
polymorphic.sleepMilliseconds(125);
|
||||
#if MESH_USB_LOGGING_AVAILABLE
|
||||
require(sleep_calls == before_guards,
|
||||
"millisecond path bypassed the USB logging veto");
|
||||
#else
|
||||
require(sleep_calls == before_guards + 1,
|
||||
"compiled-out logging blocked millisecond sleep");
|
||||
#endif
|
||||
mesh::logging_enabled = false;
|
||||
board.irq = static_cast<uint32_t>(-1);
|
||||
const unsigned before_timer = sleep_calls;
|
||||
polymorphic.sleepMilliseconds(125);
|
||||
require(sleep_calls == before_timer + 1 && timer_us == 125000ULL
|
||||
&& critical_depth == 0,
|
||||
"millisecond timer-only wake failed or left interrupts masked");
|
||||
const unsigned after_timer = sleep_calls;
|
||||
const uint64_t last_timer = timer_us;
|
||||
polymorphic.sleepMilliseconds(0);
|
||||
require(sleep_calls == after_timer && timer_us == last_timer,
|
||||
"zero milliseconds reused a previously configured wake source");
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n'; return 1;
|
||||
}
|
||||
}
|
||||
'''
|
||||
compiler = os.environ.get("CXX", "c++")
|
||||
self.assertIsNotNone(shutil.which(compiler), "a C++17 compiler is required")
|
||||
with tempfile.TemporaryDirectory(prefix="meshcore-ms-sleep-") as directory:
|
||||
cpp = Path(directory) / "sleep.cpp"
|
||||
cpp.write_text(harness.replace("@METHODS@", methods), encoding="ascii")
|
||||
for mode, cdc, button, logging in ((0, 1, 1, 1), (1, 1, 0, 1),
|
||||
(0, 0, 0, 1), (0, 1, 0, 0)):
|
||||
with self.subTest(usb_mode=mode, cdc=cdc, button=button, logging=logging):
|
||||
binary = Path(directory) / f"sleep-{mode}-{cdc}-{button}-{logging}"
|
||||
result = subprocess.run([
|
||||
compiler, "-std=c++17", "-Wall", "-Wextra",
|
||||
f"-DARDUINO_USB_MODE={mode}",
|
||||
f"-DARDUINO_USB_CDC_ON_BOOT={cdc}",
|
||||
f"-DMESH_ESP32_USB_CONSOLE_COOPERATIVE={int(bool(cdc))}",
|
||||
f"-DMOMENTARY_BUTTON_WAKE_FROM_SLEEP={button}",
|
||||
f"-DMESH_USB_LOGGING_AVAILABLE={logging}", "-DPIN_USER_BTN=38",
|
||||
"-DCONFIG_TINYUSB_ENABLED=1", "-I", str(ROOT / "src"),
|
||||
str(cpp), "-o", str(binary)], capture_output=True, text=True,
|
||||
timeout=60)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
result = subprocess.run([str(binary)], capture_output=True, text=True,
|
||||
timeout=10)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
||||
def test_g3_enables_gesture_polling_after_gpio_wake(self):
|
||||
profile = (ROOT / "variants/station_g3_esp32/platformio.ini").read_text()
|
||||
def section(name):
|
||||
|
||||
@@ -61,6 +61,7 @@ struct MyMesh {
|
||||
bool millisHasNowPassed(unsigned long) const { return false; }
|
||||
bool getNextQueueWakeDelay(uint32_t&) const { return false; }
|
||||
bool getNextRetryWakeDelay(uint32_t&) const { return false; }
|
||||
bool getNextNoiseFloorWakeDelay(uint32_t&) const { return false; }
|
||||
bool dual_radio = false;
|
||||
bool isDualRadioActive() const { return dual_radio; }
|
||||
uint32_t limitSleepToMillisTimer(unsigned long, uint32_t secs) const { return secs; }
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execute real floor deadlines/transitions with counted host I/O adapters.
|
||||
|
||||
These tests prove scheduling and IRQ-ownership contracts, not actual current
|
||||
consumption, RTOS sleep residency, or RF sample quality on untested hardware.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_radio_receive_contract import ROOT, method, transition_harness
|
||||
|
||||
|
||||
class NoiseFloorSleepTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.directory = tempfile.TemporaryDirectory(prefix='meshcore-floor-sleep-')
|
||||
cls.addClassCleanup(cls.directory.cleanup)
|
||||
work = Path(cls.directory.name)
|
||||
source = (ROOT / 'src/helpers/radiolib/RadioLibWrappers.cpp').read_text()
|
||||
header = (ROOT / 'src/helpers/radiolib/RadioLibWrappers.h').read_text()
|
||||
getter = method(source, 'bool RadioLibWrapper::getNoiseFloorWakeDelay(')
|
||||
harness = transition_harness(source).split('struct LegacyRadio {', 1)[0]
|
||||
harness = harness.replace(getter, '', 1)
|
||||
harness = harness.replace('#include <cassert>', '#include <cassert>\n#include <cstring>')
|
||||
harness = harness.replace(' bool tryResetAGC();',
|
||||
' bool isInRecvMode() const { return state == STATE_RX; }\n'
|
||||
+ method(header, 'bool isCalibratingNoiseFloor() const').replace(' override', '')
|
||||
+ '\n bool tryResetAGC();')
|
||||
harness = harness.replace('bool isChipBusy() { return busy; }',
|
||||
'unsigned busy_probes = 0, packet_probes = 0;\n'
|
||||
' bool isChipBusy() { ++busy_probes; return busy; }')
|
||||
harness = harness.replace('bool isReceivingPacket() { return packet; }',
|
||||
'bool isReceivingPacket() { ++packet_probes; return packet; }')
|
||||
fixture = (ROOT / 'test/fixtures/noise_floor_sleep.cpp').read_text()
|
||||
variants = {
|
||||
'production': getter,
|
||||
'old_batch_veto': getter[:getter.index('{') + 1] + '''
|
||||
(void)now;
|
||||
if (!isCalibratingNoiseFloor()) return false;
|
||||
delay_ms = 0;
|
||||
return true;
|
||||
}
|
||||
''',
|
||||
}
|
||||
cls.binaries = {}
|
||||
sanitize = [] if not sys.platform.startswith('linux') else [
|
||||
'-fsanitize=address,undefined', '-fno-sanitize-recover=all',
|
||||
'-fno-omit-frame-pointer', '-fno-pie', '-no-pie']
|
||||
for family in ('sx1262', 'other'):
|
||||
for name, implementation in variants.items():
|
||||
filename = name + '_' + family
|
||||
cpp, binary = work / (filename + '.cpp'), work / filename
|
||||
cpp.write_text((harness + implementation + fixture).replace('\u2014', '-'),
|
||||
encoding='ascii')
|
||||
built = subprocess.run([os.environ.get('CXX', 'g++'), '-std=c++17',
|
||||
'-O1', '-g', '-Wall', '-Wextra', '-Werror',
|
||||
*(['-DUSE_SX1262=1'] if family == 'sx1262' else []),
|
||||
*sanitize, '-I', str(ROOT / 'src'), str(cpp), '-o', str(binary)],
|
||||
capture_output=True, text=True, timeout=60)
|
||||
if built.returncode:
|
||||
raise AssertionError(built.stdout + built.stderr)
|
||||
cls.binaries[name, family] = binary
|
||||
|
||||
def run_case(self, scenario, variant='production', failure=None):
|
||||
for family in ('sx1262', 'other'):
|
||||
with self.subTest(family=family):
|
||||
result = subprocess.run([str(self.binaries[variant, family]), scenario],
|
||||
capture_output=True, text=True, timeout=20)
|
||||
if failure:
|
||||
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertIn(failure, result.stderr)
|
||||
else:
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertIn('PASS: ' + scenario, result.stdout)
|
||||
|
||||
def test_quiet_batches_allow_idle_between_all_original_samples(self):
|
||||
self.run_case('quiet')
|
||||
|
||||
def test_deadlines_and_settling_survive_rollover_including_zero(self):
|
||||
self.run_case('rollover')
|
||||
|
||||
def test_deadline_queries_have_no_io_or_irq_consumption(self):
|
||||
self.run_case('queries')
|
||||
|
||||
def test_rejected_samples_can_idle_and_keep_timeout_bound(self):
|
||||
self.run_case('rejected')
|
||||
|
||||
def test_success_becomes_lazy_but_held_rises_and_timeouts_retry_sooner(self):
|
||||
self.run_case('lazy')
|
||||
|
||||
def test_timeout_timestamp_zero_is_not_an_unarmed_or_startup_sentinel(self):
|
||||
self.run_case('timeout-zero')
|
||||
|
||||
def test_old_whole_batch_sleep_veto_negative_control(self):
|
||||
self.run_case('quiet', 'old_batch_veto', 'pending batch must permit idle')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -15,7 +15,7 @@ import tempfile
|
||||
import unittest
|
||||
import urllib.request
|
||||
|
||||
from test_esp32_usb_sleep import HARNESS, board_method
|
||||
from test_esp32_usb_sleep import HARNESS, board_method, board_sleep_methods
|
||||
from test_hwcdc_tx_backport import body
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
@@ -56,8 +56,8 @@ def firmware_harness(*, lose_sleep_guard=False, lose_crlf=False,
|
||||
" millis(), MESH_ESP32_USB_HOST_LOSS_SLEEP_GRACE_MS);")
|
||||
assert guard in sleep_held, "Missing native USB negative-control boundary"
|
||||
sleep_held = sleep_held.replace(guard, "return false;")
|
||||
methods = "\n".join(board_method(signature) for signature in (
|
||||
"void sleep(uint32_t secs) override", "bool isUsbDataConnected() override",
|
||||
methods = board_sleep_methods() + "\n" + "\n".join(board_method(signature) for signature in (
|
||||
"bool isUsbDataConnected() override",
|
||||
"bool isUsbHostConnected() override")) + "\n" + sleep_held
|
||||
if lose_raw_uart_guard:
|
||||
expression = "keep_awake = keep_awake || hold(static_cast<bool>(Serial), Reason::Serial);"
|
||||
@@ -98,6 +98,11 @@ 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; }"
|
||||
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; }")
|
||||
source += "\n#include <helpers/PowerManagementUtils.h>\n"
|
||||
return source + morse_radio + (fixture.replace("@SETTER@", setter).replace("@GETTER@", getter)
|
||||
.replace("@COMMAND_PUMP@", pump).replace("@POWER_LOOP@", power_loop))
|
||||
|
||||
|
||||
@@ -50,6 +50,12 @@ struct Node {
|
||||
int main() {
|
||||
Node node;
|
||||
assert(mesh::power::powerSaveSleepSeconds(0) == 0);
|
||||
assert(mesh::power::limitSleepMilliseconds(0, 50) == 0);
|
||||
assert(mesh::power::limitSleepMilliseconds(30, 0) == 0);
|
||||
assert(mesh::power::limitSleepMilliseconds(30, 50) == 50);
|
||||
assert(mesh::power::limitSleepMilliseconds(1, 1001) == 1000);
|
||||
assert(mesh::power::limitSleepMilliseconds(UINT32_MAX, 50) == 50);
|
||||
assert(mesh::power::limitSleepMilliseconds(UINT32_MAX, UINT32_MAX) == UINT32_MAX);
|
||||
#if EXPECT_INTERRUPT_IDLE
|
||||
assert(mesh::power::powerSaveSleepSeconds(UINT32_MAX) == 4294968U);
|
||||
#else
|
||||
@@ -110,6 +116,50 @@ int main() {
|
||||
|
||||
|
||||
class PowerSavingDeadlineTests(unittest.TestCase):
|
||||
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)')
|
||||
code = r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
struct Board {
|
||||
unsigned calls = 0;
|
||||
uint32_t last = 0;
|
||||
virtual void sleep(uint32_t secs) { ++calls; last = secs; }
|
||||
@METHOD@
|
||||
};
|
||||
int main() {
|
||||
Board board;
|
||||
board.sleepMilliseconds(0);
|
||||
assert(board.calls == 0);
|
||||
for (uint32_t ms : {1U, 50U, 999U, 1000U, 1001U, UINT32_MAX}) {
|
||||
const auto before = board.calls;
|
||||
board.sleepMilliseconds(ms);
|
||||
#if EXPECT_INTERRUPT_IDLE
|
||||
assert(board.calls == before + 1 && board.last == 0);
|
||||
#else
|
||||
assert(board.calls == before + (ms >= 1000));
|
||||
if (ms >= 1000) assert(board.last == ms / 1000);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
'''.replace('@METHOD@', implementation).replace(
|
||||
'#include <cstdint>', '#include <cstdint>\n#include <initializer_list>')
|
||||
with tempfile.TemporaryDirectory(prefix='meshcore-board-ms-idle-') as directory:
|
||||
cpp, exe = Path(directory) / 'test.cpp', Path(directory) / 'test'
|
||||
cpp.write_text(code, encoding='ascii')
|
||||
for platform in ('', 'NRF52_PLATFORM', 'RP2040_PLATFORM', 'STM32_PLATFORM'):
|
||||
with self.subTest(platform=platform or 'unknown-timed'):
|
||||
built = subprocess.run([
|
||||
os.environ.get('CXX', 'g++'), '-std=c++17', '-Wall', '-Wextra', '-Werror',
|
||||
'-DEXPECT_INTERRUPT_IDLE=' + ('1' if platform else '0'),
|
||||
*(['-D' + platform + '=1'] if platform else []),
|
||||
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_role_deadlines_preserve_timed_and_interrupt_idle(self):
|
||||
with tempfile.TemporaryDirectory(prefix='meshcore-sleep-deadlines-') as directory:
|
||||
work = Path(directory)
|
||||
|
||||
@@ -28,6 +28,55 @@ class PowerSavingRoleDiagnosticsTests(unittest.TestCase):
|
||||
result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=20)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
|
||||
def test_actual_radio_default_and_public_dispatcher_deadline_forwarder(self):
|
||||
header = (ROOT / 'src/Dispatcher.h').read_text(encoding='ascii')
|
||||
source = (ROOT / 'src/Dispatcher.cpp').read_text(encoding='ascii')
|
||||
conservative = extract_braced(header, 'virtual bool getNoiseFloorWakeDelay(')
|
||||
forwarder = extract_braced(source, 'bool Dispatcher::getNextNoiseFloorWakeDelay(')
|
||||
self.execute(r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
struct Radio {
|
||||
bool calibrating = false;
|
||||
bool isCalibratingNoiseFloor() const { return calibrating; }
|
||||
@DEFAULT@
|
||||
};
|
||||
struct SpacedRadio : Radio {
|
||||
mutable unsigned reads = 0;
|
||||
uint32_t delay = 50;
|
||||
bool getNoiseFloorWakeDelay(uint32_t now, uint32_t& out) const override {
|
||||
assert(now == 123); ++reads; out = delay; return true;
|
||||
}
|
||||
};
|
||||
struct Clock {
|
||||
unsigned reads = 0;
|
||||
uint32_t getMillis() { ++reads; return 123; }
|
||||
};
|
||||
struct Dispatcher {
|
||||
bool radio_available = true;
|
||||
Radio* _radio;
|
||||
Clock* _ms;
|
||||
bool getNextNoiseFloorWakeDelay(uint32_t&) const;
|
||||
};
|
||||
@FORWARDER@
|
||||
int main() {
|
||||
Radio legacy;
|
||||
Clock clock;
|
||||
Dispatcher node{true, &legacy, &clock};
|
||||
uint32_t delay = 71;
|
||||
assert(!node.getNextNoiseFloorWakeDelay(delay) && delay == 71);
|
||||
legacy.calibrating = true;
|
||||
assert(node.getNextNoiseFloorWakeDelay(delay) && delay == 0);
|
||||
SpacedRadio spaced;
|
||||
node._radio = &spaced;
|
||||
assert(node.getNextNoiseFloorWakeDelay(delay) && delay == 50 && spaced.reads == 1);
|
||||
node.radio_available = false;
|
||||
const auto before = clock.reads;
|
||||
assert(!node.getNextNoiseFloorWakeDelay(delay));
|
||||
assert(clock.reads == before && spaced.reads == 1);
|
||||
}
|
||||
'''.replace('@DEFAULT@', conservative).replace('@FORWARDER@', forwarder))
|
||||
|
||||
def test_actual_role_guards_preserve_short_circuit_and_ignore_charger_only(self):
|
||||
for role in ("simple_repeater", "simple_room_server", "simple_sensor"):
|
||||
with self.subTest(role=role):
|
||||
@@ -128,7 +177,7 @@ struct MyMesh {
|
||||
bool dual = false, ota = false, queue = false, retry = false, scheduled = false;
|
||||
bool bridge_enabled = true;
|
||||
bool queue_delay_set = false, retry_delay_set = false;
|
||||
uint32_t queue_delay = 0, retry_delay = 0;
|
||||
uint32_t queue_delay = 0, retry_delay = 0, calibration_delay = 0;
|
||||
struct { bool busy_value = false; bool busy() const { return busy_value; } } _local_cli_output;
|
||||
struct { bool pending = false; } deferred_cli_command;
|
||||
struct { bool gpio = false; bool hasActiveUserGpioTimer() const { return gpio; } } _cli;
|
||||
@@ -152,6 +201,9 @@ struct MyMesh {
|
||||
bool isMillisTimerDue(unsigned long value) const { return value && value <= millis(); }
|
||||
bool getNextQueueWakeDelay(uint32_t& value) const { value = queue_delay; return queue_delay_set; }
|
||||
bool getNextRetryWakeDelay(uint32_t& value) const { value = retry_delay; return retry_delay_set; }
|
||||
bool getNextNoiseFloorWakeDelay(uint32_t& value) const {
|
||||
value = calibration_delay; return radio_driver.calibration;
|
||||
}
|
||||
uint32_t limitSleepToMillisTimer(unsigned long value, uint32_t seconds) const {
|
||||
return value && value <= millis() ? 0 : seconds;
|
||||
}
|
||||
@@ -193,6 +245,11 @@ int main() {
|
||||
node.uart.wake_ms = 60000;
|
||||
check(node.radio_driver.watchdog, Reason::RadioWatchdog);
|
||||
check(node.radio_driver.calibration, Reason::Calibration);
|
||||
node.radio_driver.calibration = true; node.calibration_delay = 50;
|
||||
assert(!node.hasPendingWork() && node.getPowerSaveSleepSeconds(30) == 30);
|
||||
node.calibration_delay = 0;
|
||||
assert(node.pendingWorkReason() == mask(Reason::Calibration));
|
||||
node.radio_driver.calibration = false;
|
||||
check(node.queue, Reason::Queue); check(node.retry, Reason::Retry);
|
||||
check(node.scheduled, Reason::Scheduled);
|
||||
node.queue_delay_set = true; node.queue_delay = 500;
|
||||
@@ -278,7 +335,11 @@ int main() {
|
||||
|
||||
def test_esp32_real_veto_and_sdk_failure_timing_does_not_invent_blocked_residency(self):
|
||||
source = (ROOT / "src/helpers/ESP32Board.h").read_text(encoding="ascii")
|
||||
method = extract_braced(source, "void sleep(uint32_t secs) override")
|
||||
method = "\n".join(extract_braced(source, signature) for signature in (
|
||||
"void sleep(uint32_t secs) override",
|
||||
"void sleepMilliseconds(uint32_t millisecs) override",
|
||||
"void sleepMicroseconds(uint64_t wake_us, bool require_timer = false)",
|
||||
))
|
||||
code = r'''
|
||||
#include <cassert>
|
||||
#include <helpers/PowerSavingDiagnostics.h>
|
||||
@@ -297,7 +358,8 @@ static bool radio_pending = false;
|
||||
static uint32_t millis() { return now_us / 1000; }
|
||||
static uint32_t micros() { return now_us; }
|
||||
static void delay(unsigned ms) { yields += ms; }
|
||||
static esp_err_t esp_sleep_enable_timer_wakeup(uint64_t) { return timer_error; }
|
||||
static uint64_t timer_us = 0;
|
||||
static esp_err_t esp_sleep_enable_timer_wakeup(uint64_t value) { timer_us = value; return timer_error; }
|
||||
static esp_err_t esp_sleep_enable_gpio_wakeup() { return gpio_error; }
|
||||
static esp_err_t gpio_wakeup_enable(gpio_num_t, int) { return pin_error; }
|
||||
static void gpio_wakeup_disable(gpio_num_t) {}
|
||||
@@ -310,7 +372,10 @@ namespace mesh {
|
||||
static bool logging = false;
|
||||
static bool isUsbLoggingEnabled() { return logging; }
|
||||
}
|
||||
struct MainBoard { virtual void sleep(uint32_t) = 0; };
|
||||
struct MainBoard {
|
||||
virtual void sleep(uint32_t) = 0;
|
||||
virtual void sleepMilliseconds(uint32_t) = 0;
|
||||
};
|
||||
struct ESP32Board : MainBoard {
|
||||
bool inhibit_sleep = false, usb = false, test = false;
|
||||
uint32_t irq = 14;
|
||||
@@ -324,16 +389,27 @@ int main() {
|
||||
ESP32Board board;
|
||||
board.sleep(30);
|
||||
assert(sdk_calls == 1 && snapshot(millis()).calls == 0);
|
||||
assert(timer_us == 30000000ULL);
|
||||
board.sleepMilliseconds(50);
|
||||
assert(sdk_calls == 2 && timer_us == 50000ULL);
|
||||
board.sleepMilliseconds(0);
|
||||
assert(sdk_calls == 2 && timer_us == 50000ULL);
|
||||
setEnabled(true, millis()); reset(millis()); decision(0, millis());
|
||||
timer_error = 29; gpio_error = 31; pin_error = 33;
|
||||
board.sleep(30); // Diagnostics may observe failures, never repair/change policy.
|
||||
auto value = snapshot(millis());
|
||||
assert(sdk_calls == 2 && value.calls == 1 && value.returns == 1 && value.errors == 1);
|
||||
assert(sdk_calls == 3 && value.calls == 1 && value.returns == 1 && value.errors == 1);
|
||||
assert(value.detail == 29); // First setup failure, without changing later SDK calls.
|
||||
assert(value.last_call_us == 7000 && value.eligible_ms == 7);
|
||||
assert(value.blocked_ms[static_cast<size_t>(Reason::Board)] == 0);
|
||||
assert(critical_depth == 0);
|
||||
reset(millis()); decision(0, millis());
|
||||
const auto before_failed_slice = sdk_calls;
|
||||
board.sleepMilliseconds(50);
|
||||
value = snapshot(millis());
|
||||
assert(sdk_calls == before_failed_slice && value.calls == 0);
|
||||
assert(value.errors == 1 && value.detail == 29 && critical_depth == 0);
|
||||
reset(millis()); decision(0, millis());
|
||||
timer_error = gpio_error = pin_error = 0; sleep_error = 41;
|
||||
board.sleep(30);
|
||||
value = snapshot(millis());
|
||||
|
||||
@@ -43,6 +43,7 @@ struct RadioLibWrapper {
|
||||
bool _profile_saved_rxps=false, _profile_rxps_suspended=false;
|
||||
bool _profile_standby_held=false, _saved_standby_xosc=false;
|
||||
bool _nf_calib_active=false, _nf_refresh_requested=false, _noise_floor_valid=true, _noise_floor_secondary_valid=true, _profile_refresh_required=false;
|
||||
bool _nf_window_armed=false, _nf_lazy_refresh=false, _nf_refresh_attempted=false;
|
||||
unsigned long _nf_last_calib=0, _nf_calib_deadline=0, _nf_sample_from=0;
|
||||
NoiseFloorEstimator _floor_estimator;
|
||||
NoiseFloorEstimator _secondary_floor_estimator;
|
||||
@@ -105,6 +106,20 @@ struct RadioLibWrapper {
|
||||
@METHODS@
|
||||
int main() {
|
||||
using Result=mesh::RadioParamApplyResult;
|
||||
{
|
||||
RadioLibWrapper w;
|
||||
w._nf_window_armed=w._nf_lazy_refresh=w._nf_refresh_attempted=true;
|
||||
w.enable();
|
||||
assert(!w._nf_window_armed && !w._nf_lazy_refresh && !w._nf_refresh_attempted);
|
||||
w._nf_window_armed=w._nf_lazy_refresh=w._nf_refresh_attempted=true;
|
||||
mesh::RadioProfileConfig replacement;
|
||||
replacement.params=w._profiles.primary;
|
||||
replacement.params.freq=911.5;
|
||||
replacement.mode=mesh::RadioProfileMode::RxTx;
|
||||
w._profiles.setSecondary(replacement,false);
|
||||
w.serviceProfileScan();
|
||||
assert(!w._nf_window_armed && !w._nf_lazy_refresh && !w._nf_refresh_attempted);
|
||||
}
|
||||
{
|
||||
RadioLibWrapper w;
|
||||
w.enable();
|
||||
|
||||
@@ -78,6 +78,7 @@ struct RadioLibWrapper {
|
||||
bool _rx_hold_continuous = false;
|
||||
bool _nf_calib_active = false, _nf_refresh_requested = true, _noise_floor_valid = true,
|
||||
_noise_floor_secondary_valid = true;
|
||||
bool _nf_window_armed = false, _nf_lazy_refresh = false, _nf_refresh_attempted = false;
|
||||
bool _cad_enabled = true, packet = false, busy = false, inject_arm_irq = false;
|
||||
bool _rx_boosted_gain_valid = false, _cur_rx_boosted_gain = false;
|
||||
bool _wd_last_busy = false;
|
||||
@@ -146,6 +147,7 @@ struct RadioLibWrapper {
|
||||
void recalibrateNoiseFloor();
|
||||
bool resetAGCIfIdle();
|
||||
bool tryResetAGC();
|
||||
bool getNoiseFloorWakeDelay(uint32_t, uint32_t&) const;
|
||||
void resetAGC();
|
||||
@RECOVERY_GETTER@
|
||||
};
|
||||
@@ -158,9 +160,7 @@ struct LegacyRadio {
|
||||
@LEGACY_RECOVERY_GETTER@
|
||||
};
|
||||
|
||||
// Exercise both public callers and the shared policy itself. Continuous and
|
||||
// fallback Dispatcher triggers intentionally retain their unconditional legacy
|
||||
// request; only passive polling and the helper use the two-second age gate.
|
||||
// Public Dispatcher/passive callers share the same bounded routine policy.
|
||||
static void routineRequest(RadioLibWrapper& w, unsigned path, int margin = 7) {
|
||||
if (path == 0) w.triggerNoiseFloorCalibrate(margin);
|
||||
else if (path == 1) {
|
||||
@@ -178,7 +178,8 @@ static void selectMode(RadioLibWrapper& w, unsigned mode) {
|
||||
|
||||
static void periodicRefreshContracts() {
|
||||
static_assert(NF_RXPS_REFRESH_INTERVAL_MS == 60000UL, "Routine RXPS refresh must rest one minute");
|
||||
static_assert(NF_CALIB_INTERVAL_MS == 2000UL, "Continuous passive refresh retains its two-second cadence");
|
||||
static_assert(NF_CALIB_INTERVAL_MS == 60000UL, "Failed routine refresh must rest one minute");
|
||||
static_assert(NF_LAZY_REFRESH_INTERVAL_MS == 300000UL, "Successful floor refresh becomes lazy");
|
||||
unsigned cases = 0;
|
||||
for (unsigned path : {0U, 1U, 2U}) for (unsigned mode : {0U, 1U, 2U})
|
||||
for (uint32_t last : {10000U, 0xfffffff0U}) for (bool valid : {false, true})
|
||||
@@ -189,7 +190,7 @@ static void periodicRefreshContracts() {
|
||||
now_ms = last + age;
|
||||
routineRequest(w, path);
|
||||
const uint32_t interval = mode == 1 ? NF_RXPS_REFRESH_INTERVAL_MS : NF_CALIB_INTERVAL_MS;
|
||||
const bool expected = (path == 0 && mode != 1) || age >= interval;
|
||||
const bool expected = age >= interval;
|
||||
assert(w._nf_refresh_requested == expected);
|
||||
assert(w._threshold == (path == 0 ? 7 : 42)); // threshold changes never wait
|
||||
assert(w.arms == 0 && w.stops == 0 && w.radio.scans == 0);
|
||||
@@ -213,12 +214,10 @@ static void periodicRefreshContracts() {
|
||||
RadioLibWrapper w; selectMode(w, from);
|
||||
w._nf_last_calib = 10000; w._nf_refresh_requested = false;
|
||||
now_ms = 20000; selectMode(w, to); routineRequest(w, path);
|
||||
assert(w._nf_refresh_requested == (to != 1));
|
||||
if (to == 1) {
|
||||
now_ms = 10000 + NF_RXPS_REFRESH_INTERVAL_MS - 1; routineRequest(w, path);
|
||||
assert(!w._nf_refresh_requested);
|
||||
++now_ms; routineRequest(w, path); assert(w._nf_refresh_requested);
|
||||
}
|
||||
assert(!w._nf_refresh_requested);
|
||||
now_ms = 10000 + NF_RXPS_REFRESH_INTERVAL_MS - 1; routineRequest(w, path);
|
||||
assert(!w._nf_refresh_requested);
|
||||
++now_ms; routineRequest(w, path); assert(w._nf_refresh_requested);
|
||||
++cases;
|
||||
}
|
||||
// A periodic request may join, but must not reset or extend, a partial block.
|
||||
@@ -291,7 +290,7 @@ static void periodicRefreshContracts() {
|
||||
w.loop(); assert(w._nf_calib_active);
|
||||
++cases;
|
||||
}
|
||||
// A failed initial block gets the same one-minute rest as a valid block.
|
||||
// A failed initial block rests one minute; an accepted baseline rests five.
|
||||
// Quiet and busy windows are exercised through the actual loop/re-arm path.
|
||||
for (bool busy : {false, true}) for (unsigned path : {0U, 1U}) {
|
||||
RadioLibWrapper w; selectMode(w, 1); w._rx_ps_armed = true;
|
||||
@@ -313,7 +312,8 @@ static void periodicRefreshContracts() {
|
||||
assert(w._noise_floor_valid == !busy);
|
||||
if (busy) assert(w.reads == 0 && w._noise_floor_centi_dbm == -10500);
|
||||
// A quiet passive caller reads RSSI, but those reads do not open windows.
|
||||
for (; uint32_t(now_ms - finished) < NF_RXPS_REFRESH_INTERVAL_MS; ++now_ms) {
|
||||
const uint32_t interval = busy ? NF_RXPS_REFRESH_INTERVAL_MS : NF_LAZY_REFRESH_INTERVAL_MS;
|
||||
for (; uint32_t(now_ms - finished) < interval; ++now_ms) {
|
||||
if (uint32_t(now_ms - finished) % NF_CALIB_INTERVAL_MS == 0) {
|
||||
if (path == 0) w.triggerNoiseFloorCalibrate(7);
|
||||
else routineRequest(w, path);
|
||||
@@ -421,7 +421,7 @@ static void agcAdmissionContracts() {
|
||||
assert(w._noise_floor == -105 && w._noise_floor_centi_dbm == -10500);
|
||||
assert(w._noise_floor_secondary_centi_dbm == -10600);
|
||||
assert(w._floor_estimator.count() == 0 && w._secondary_floor_estimator.count() == 0);
|
||||
assert(w._nf_sample_from == now_ms + NF_CALIB_SETTLE_MS);
|
||||
assert(w._nf_sample_from == now_ms + (w._rx_ps_armed ? 0 : NF_CALIB_SETTLE_MS));
|
||||
assert(w._nf_calib_active == active_window);
|
||||
assert(w._nf_calib_deadline == (active_window ? now_ms + NF_CALIB_TIMEOUT_MS : 0));
|
||||
assert(w.getRecoveryCount() == 5); // AGC attempts do not inflate watchdog recovery counters
|
||||
@@ -680,6 +680,39 @@ int main() {
|
||||
}
|
||||
'''
|
||||
|
||||
def transition_harness(source=None):
|
||||
"""Build the shared actual-method harness, including its counted I/O adapters."""
|
||||
if source is None:
|
||||
source = (ROOT / 'src/helpers/radiolib/RadioLibWrappers.cpp').read_text()
|
||||
header = (ROOT / 'src/helpers/radiolib/RadioLibWrappers.h').read_text()
|
||||
dispatcher = (ROOT / 'src/Dispatcher.h').read_text()
|
||||
rxps = (ROOT / 'src/helpers/radiolib/RXPowerSaving.h').read_text()
|
||||
names = [('int16_t', 'performChannelScanWithTimeout'), ('bool', 'isPacketPendingOrReceiving'),
|
||||
('bool', 'isChannelActive'), ('bool', 'isReceivingPassive'),
|
||||
('bool', 'setRxPowerSaving'), ('bool', 'setRxBoostedGainMode'),
|
||||
('void', 'startRecv'), ('void', 'checkReceiveMode'),
|
||||
('void', 'loop'), ('void', 'triggerNoiseFloorCalibrate'),
|
||||
('void', 'requestRestartRecv'), ('void', 'noiseFloorCalibCheck'),
|
||||
('void', 'endNoiseFloorCalib'), ('void', 'requestNoiseFloorRefresh'),
|
||||
('void', 'requestPeriodicNoiseFloorRefresh'), ('void', 'recalibrateNoiseFloor'),
|
||||
('bool', 'resetAGCIfIdle'), ('bool', 'tryResetAGC'), ('void', 'resetAGC'),
|
||||
('bool', 'getNoiseFloorWakeDelay')]
|
||||
methods = method(rxps, 'inline bool rxPowerSavingUsesContinuousFallback(') + '\n'
|
||||
methods += '\n'.join(method(source, f'{kind} RadioLibWrapper::{name}(')
|
||||
for kind, name in names)
|
||||
constants = '\n'.join(re.findall(r'^#define NF_\w+\s+[^\n]+', source, re.MULTILINE))
|
||||
constants += '\n' + re.search(
|
||||
r'^#define RX_POWERSAVING_CONTINUOUS_FALLBACK_US\s+[^\n]+', rxps,
|
||||
re.MULTILINE).group(0)
|
||||
settle_start = header.index('#if defined(USE_SX1262) || defined(USE_SX1268) || defined(USE_LLCC68)')
|
||||
settle = header[settle_start:header.index('#endif', settle_start) + len('#endif')]
|
||||
return (HARNESS.replace('@METHODS@', methods).replace('@NF_CONSTANTS@', constants)
|
||||
.replace('@SETTLE_CONSTANT@', settle)
|
||||
.replace('@RECOVERY_GETTER@', method(header, 'uint32_t getRecoveryCount() const').replace('override', ''))
|
||||
.replace('@LEGACY_TRY_RESET@', method(dispatcher, 'virtual bool tryResetAGC('))
|
||||
.replace('@LEGACY_RECOVERY_GETTER@', method(dispatcher, 'virtual uint32_t getRecoveryCount(')))
|
||||
|
||||
|
||||
class RadioReceiveContractTest(unittest.TestCase):
|
||||
def test_preamble_margin_and_receive_deadlines(self):
|
||||
radio_source = (ROOT / 'src/helpers/radiolib/RadioLibWrappers.cpp').read_text()
|
||||
@@ -903,32 +936,7 @@ int main() {
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
||||
def run_production_transitions(self, source=None, expect_failure=False, phase_failure=False):
|
||||
if source is None:
|
||||
source = (ROOT / 'src/helpers/radiolib/RadioLibWrappers.cpp').read_text()
|
||||
header = (ROOT / 'src/helpers/radiolib/RadioLibWrappers.h').read_text()
|
||||
dispatcher = (ROOT / 'src/Dispatcher.h').read_text()
|
||||
rxps = (ROOT / 'src/helpers/radiolib/RXPowerSaving.h').read_text()
|
||||
names = [('int16_t','performChannelScanWithTimeout'), ('bool','isPacketPendingOrReceiving'),
|
||||
('bool','isChannelActive'), ('bool','isReceivingPassive'),
|
||||
('bool','setRxPowerSaving'), ('bool','setRxBoostedGainMode'),
|
||||
('void','startRecv'), ('void','checkReceiveMode'),
|
||||
('void','loop'), ('void','triggerNoiseFloorCalibrate'), ('void','requestRestartRecv'), ('void','noiseFloorCalibCheck'),
|
||||
('void','endNoiseFloorCalib'), ('void','requestNoiseFloorRefresh'),
|
||||
('void','requestPeriodicNoiseFloorRefresh'),
|
||||
('void','recalibrateNoiseFloor'), ('bool','resetAGCIfIdle'),
|
||||
('bool','tryResetAGC'), ('void','resetAGC')]
|
||||
methods = method(rxps, 'inline bool rxPowerSavingUsesContinuousFallback(') + '\n'
|
||||
methods += '\n'.join(method(source, f'{kind} RadioLibWrapper::{name}(') for kind,name in names)
|
||||
constants = '\n'.join(re.findall(r'^#define NF_\w+\s+[^\n]+', source, re.MULTILINE))
|
||||
constants += '\n' + re.search(
|
||||
r'^#define RX_POWERSAVING_CONTINUOUS_FALLBACK_US\s+[^\n]+', rxps, re.MULTILINE).group(0)
|
||||
settle_start = header.index('#if defined(USE_SX1262) || defined(USE_SX1268) || defined(USE_LLCC68)')
|
||||
settle = header[settle_start:header.index('#endif', settle_start) + len('#endif')]
|
||||
harness = (HARNESS.replace('@METHODS@', methods).replace('@NF_CONSTANTS@', constants)
|
||||
.replace('@SETTLE_CONSTANT@', settle)
|
||||
.replace('@RECOVERY_GETTER@', method(header, 'uint32_t getRecoveryCount() const').replace('override', ''))
|
||||
.replace('@LEGACY_TRY_RESET@', method(dispatcher, 'virtual bool tryResetAGC('))
|
||||
.replace('@LEGACY_RECOVERY_GETTER@', method(dispatcher, 'virtual uint32_t getRecoveryCount(')))
|
||||
harness = transition_harness(source)
|
||||
with tempfile.TemporaryDirectory(prefix='meshcore-radio-transitions-') as tmp:
|
||||
cpp = Path(tmp) / 'test.cpp'
|
||||
binary = Path(tmp) / 'test'
|
||||
|
||||
@@ -51,6 +51,7 @@ class RadioReceiveRetryTests(unittest.TestCase):
|
||||
('bool', 'tryResetAGC'), ('void', 'resetAGC'), ('int', 'recvRaw'),
|
||||
('void', 'finishReceiveProcessing'),
|
||||
('void', 'onSendFinished'), ('void', 'begin'),
|
||||
('bool', 'getNoiseFloorWakeDelay'),
|
||||
('bool', 'getReceiveRetryDelay'), ('void', 'rxPsWatchdogCheck')]
|
||||
methods = method(rxps, 'inline bool rxPowerSavingUsesContinuousFallback(') + '\n'
|
||||
methods += '\n'.join(method(source, f'{kind} RadioLibWrapper::{name}(')
|
||||
|
||||
@@ -128,11 +128,14 @@ int main(int argc, char** argv) {
|
||||
"accepted deferred timings not retained");
|
||||
CHECK(w.rx_starts == starts && w.chip.standbys == standbys && w.applies == applies
|
||||
&& w._nf_refresh_requested == refresh, "deferred setting touched live scan I/O/calibration");
|
||||
w._nf_window_armed = w._nf_lazy_refresh = w._nf_refresh_attempted = true;
|
||||
w._profiles.setSecondary({}, false);
|
||||
w.serviceProfileScan();
|
||||
CHECK(!w._profile_rxps_suspended && w.state == STATE_RX
|
||||
&& w._rx_ps_enabled == requested && w._rx_ps_armed == requested,
|
||||
"accepted deferred policy not restored after scan");
|
||||
CHECK(!w._nf_window_armed && !w._nf_lazy_refresh && !w._nf_refresh_attempted,
|
||||
"scan exit must invalidate stale single-profile calibration policy/deadline");
|
||||
}
|
||||
} else if (!std::strcmp(scenario, "unsupported-disable")) {
|
||||
for (bool scan : {false, true}) {
|
||||
|
||||
@@ -160,6 +160,7 @@ struct MyMesh : Callbacks {
|
||||
bool isMillisTimerDue(unsigned long) const { return false; }
|
||||
bool getNextQueueWakeDelay(uint32_t&) const { return false; }
|
||||
bool getNextRetryWakeDelay(uint32_t&) const { return false; }
|
||||
bool getNextNoiseFloorWakeDelay(uint32_t&) const { return false; }
|
||||
uint32_t limitSleepToMillisTimer(unsigned long, uint32_t secs) const { return secs; }
|
||||
uint32_t limitSleepToScheduledRadioWork(uint32_t secs) const { return secs; }
|
||||
bool hasPendingWork() const;
|
||||
|
||||
@@ -62,6 +62,7 @@ public:
|
||||
bool getNextQueueWakeDelay(uint32_t& out)const{out=queue_delay;return queue_delay_set;}
|
||||
bool hasPendingRadioWork()const{return queue_delay_set;}
|
||||
bool getNextRetryWakeDelay(uint32_t& out)const{out=retry_delay;return retry_delay_set;}
|
||||
bool getNextNoiseFloorWakeDelay(uint32_t& out)const{out=0;return radio_driver.calibration;}
|
||||
bool isMillisTimerDue(unsigned long timestamp)const;
|
||||
uint32_t limitSleepToMillisTimer(unsigned long timestamp,uint32_t sleep_secs)const;
|
||||
bool hasPendingWork()const;
|
||||
@@ -126,6 +127,7 @@ TIMER = r'''
|
||||
#include <Arduino.h>
|
||||
#include <cassert>
|
||||
#include <helpers/PowerSavingDiagnostics.h>
|
||||
#include <helpers/PowerManagementUtils.h>
|
||||
using TimerHandle_t=void*;
|
||||
using TickType_t=uint32_t;
|
||||
bool allocate=true,queue_ok=true,active=false;
|
||||
@@ -158,9 +160,13 @@ static void trackerWakeCallback(TimerHandle_t){}
|
||||
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;}
|
||||
bool getNextNoiseFloorWakeDelay(uint32_t&)const{return false;}
|
||||
bool millisHasNowPassed(uint32_t deadline)const{return (int32_t)(millis()-deadline)>=0;}
|
||||
} the_mesh;
|
||||
struct Board {unsigned sleeps=0,last=0;void sleep(unsigned value){++sleeps;last=value;}} board;
|
||||
struct Board {unsigned sleeps=0,last=0;
|
||||
void sleep(unsigned value){++sleeps;last=value;}
|
||||
void sleepMilliseconds(unsigned value){++sleeps;last=value;}
|
||||
} board;
|
||||
static const unsigned long POWERSAVING_FIRST_SLEEP_SECS=120;
|
||||
void sleepGate(bool can_power_save){
|
||||
@GATE@
|
||||
|
||||
Reference in new issue
Block a user