Allow safe nRF52 MCU idle between radio2 scan deadlines
This commit is contained in:
1 parent
53835c0793
commit
f1d281a766
30 files changed
+916
-24
No files matched your search
@@ -329,6 +329,7 @@ jobs:
|
||||
python3 -B test/test_companion_power_diagnostics.py -v
|
||||
python3 -B test/test_nrf52_sleep_cycle.py -v
|
||||
python3 -B test/test_nrf52_sleep_events.py -v
|
||||
python3 -B test/test_nrf52_profile_idle.py -v
|
||||
python3 -B test/test_esp32_scanner_power_yield.py -v
|
||||
python3 -B test/test_esp32_gpio_hold.py -v
|
||||
|
||||
@@ -485,6 +486,7 @@ jobs:
|
||||
python3 -B test/test_radio_profile_spiffs_missing.py -v
|
||||
python3 -B test/test_radio_reply_mutation.py
|
||||
python3 -B test/test_radio_profile_scan.py
|
||||
python3 -B test/test_radio_profile_idle_dispatcher.py -v
|
||||
python3 -B test/test_radio_rxps_rejection.py -v
|
||||
python3 -B test/test_repeater_radio_timing_integration.py
|
||||
python3 -B test/test_temp_radio_reply_delivery_contract.py
|
||||
|
||||
@@ -39,6 +39,11 @@ after off starts a fresh window. Collection defaults off at every boot.
|
||||
existing service guard is active.
|
||||
- `board`, `no.wake`: an inner board veto, SDK error or lack of a wake source.
|
||||
|
||||
`radio2` means the scanner cannot safely idle right now: for example, a short
|
||||
remaining visit, calibration, pending reception or an unvalidated platform.
|
||||
On nRF52, settled long visits can idle the MCU with power saving enabled;
|
||||
the RF receiver stays on. See [radio profiles](radio_profiles.md#mcu-power-saving-while-scanning).
|
||||
|
||||
The role reports the first actual short-circuit blocker. It does not query
|
||||
otherwise skipped hardware just to populate an exhaustive list. Board guards
|
||||
may add reasons. Per-reason durations therefore are observed guard time, not
|
||||
|
||||
@@ -803,6 +803,40 @@ An RX-only `tempradio2` also blocks locally generated OTA requests under
|
||||
for a fetching node that needs to exchange requests and data. Infrastructure
|
||||
OTA responses follow `tx.reply` independently of this crossing table.
|
||||
|
||||
## MCU power saving while scanning
|
||||
|
||||
On nRF52, `set powersaving on` also permits interrupt-driven MCU idle during a
|
||||
settled `radio2` or `tempradio2` listening visit. The RF receiver stays on.
|
||||
This includes a Meshtext LongFast visit (SF11 / 250 kHz), whose minimum visit
|
||||
is about 37.7 ms. No extra command is needed.
|
||||
|
||||
The scanner first completes its switch self-test and both noise-floor
|
||||
baselines. It polls again for pending packets, calibration, changed settings,
|
||||
retries, or short visits. All normal USB, logging, bridge, OTA, queue and role
|
||||
sleep guards still apply. An nRF52 periodic interrupt bounds each wait to
|
||||
about 0.977 ms; admission reserves another 0.488 ms before the visit deadline
|
||||
and is rechecked immediately before sleeping. If that wake source is not
|
||||
available, scanning keeps polling. Other MCU families retain polling until
|
||||
their timing is validated.
|
||||
|
||||
The integer admission gate requires at least 49 RTC2 ticks (about 1.495 ms)
|
||||
remaining after the work checks. A configured visit must also cover the loop
|
||||
work before that point. Eligibility is therefore per visit, not an SF/BW
|
||||
cutoff: a fast SF5 secondary can still idle the MCU when the primary profile
|
||||
gives it a sufficiently long listening visit.
|
||||
|
||||
For example, the RAK3401 bench with USA primary `910.525,62.5,7,5,32` and
|
||||
secondary `909.5,500,5,5,rxtx,384` had roughly 9.4/21.3 ms visits. MCU sleep
|
||||
was observed in 35% of 4265 debugger samples with power saving on, versus
|
||||
zero of 2129 with it off. This is a CPU-state spot check, not a current or
|
||||
Ethernet throughput measurement. Enabling power saving changed neither preamble.
|
||||
|
||||
Radio RX duty cycling remains suspended while scanning, then its previous
|
||||
setting is restored when radio2 stops. Meshtext's fixed 16-symbol preamble
|
||||
does not have the same sleep margin as a longer MeshCore preamble. Adding
|
||||
RF sleep inside a scan visit would introduce extra blind time, so this
|
||||
optimization saves MCU activity without shortening either listening visit.
|
||||
|
||||
## Preamble
|
||||
|
||||
Append the preamble to the end of a command:
|
||||
|
||||
@@ -11600,7 +11600,7 @@ mesh::powerdiag::Mask MyMesh::pendingWorkReason() const {
|
||||
#if COMPANION_FEATURE_NOTIFICATIONS
|
||||
if (_notifications.active()) return mask(Reason::Mesh);
|
||||
#endif
|
||||
if (isDualRadioActive()) return mask(Reason::DualRadio);
|
||||
if (!canIdleDualRadio()) return mask(Reason::DualRadio);
|
||||
if (_radio_available && radio_driver.isWatchdogObserving()) return mask(Reason::RadioWatchdog);
|
||||
uint32_t noise_floor_delay_ms;
|
||||
if (getNextNoiseFloorWakeDelay(noise_floor_delay_ms) && noise_floor_delay_ms == 0)
|
||||
|
||||
@@ -4209,6 +4209,10 @@ void loop() {
|
||||
#ifdef ESP32_PLATFORM
|
||||
serviceCompanionPowerSaving();
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM)
|
||||
if (the_mesh.getNodePrefs()->powersaving_enabled && the_mesh.isDualRadioActive()
|
||||
&& !morse_timing) board.prepareProfileIdleCycle();
|
||||
#endif
|
||||
|
||||
// USB power alone (for example, a wall charger) does not inhibit sleep.
|
||||
// Host sessions, live logging, and button activity still need service.
|
||||
@@ -4254,7 +4258,7 @@ void loop() {
|
||||
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) {
|
||||
if (can_sleep && the_mesh.canIdleDualRadio()) {
|
||||
const uint32_t idle_slice_ms = noise_floor_wait && noise_floor_delay_ms < 10
|
||||
? noise_floor_delay_ms : 10;
|
||||
#if defined(NRF52_PLATFORM)
|
||||
|
||||
@@ -13928,7 +13928,7 @@ mesh::powerdiag::Mask MyMesh::pendingWorkReason() const {
|
||||
using mesh::powerdiag::Reason;
|
||||
// Keep the original guard order and short circuit: diagnostics must not
|
||||
// probe additional hardware or change which work keeps the MCU awake.
|
||||
if (isDualRadioActive()) return mask(Reason::DualRadio);
|
||||
if (!canIdleDualRadio()) return mask(Reason::DualRadio);
|
||||
if (hasPendingOtaApply()) return mask(Reason::Ota);
|
||||
#if defined(ESP32_PLATFORM)
|
||||
if (mesh::wifi::networkPower().foregroundActive()) return mask(Reason::Network);
|
||||
|
||||
@@ -623,6 +623,10 @@ void loop() {
|
||||
|
||||
#ifdef HAS_EXTERNAL_WATCHDOG
|
||||
external_watchdog.loop();
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM)
|
||||
if (the_mesh.getNodePrefs()->powersaving_enabled && the_mesh.isDualRadioActive()
|
||||
&& !morse_timing) board.prepareProfileIdleCycle();
|
||||
#endif
|
||||
// Report the guard that really stopped this iteration, without probing
|
||||
// later guards or changing their short-circuit hardware access.
|
||||
@@ -659,7 +663,7 @@ void loop() {
|
||||
if (sleep_ms > 0) external_watchdog.feed();
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM)
|
||||
if (sleep_ms > 0) {
|
||||
if (sleep_ms > 0 && the_mesh.canIdleDualRadio()) {
|
||||
if (mesh::powerdiag::enabled()) mesh::powerdiag::decision(0, millis());
|
||||
if (noise_floor_wait) board.sleepMilliseconds(sleep_ms);
|
||||
else board.sleep(0); // nrf ignores seconds param, sleeps whenever possible
|
||||
|
||||
@@ -4287,7 +4287,7 @@ bool MyMesh::hasPendingWork() const {
|
||||
mesh::powerdiag::Mask MyMesh::pendingWorkReason() const {
|
||||
using mesh::powerdiag::mask;
|
||||
using mesh::powerdiag::Reason;
|
||||
if (isDualRadioActive()) return mask(Reason::DualRadio);
|
||||
if (!canIdleDualRadio()) return mask(Reason::DualRadio);
|
||||
if (hasPendingOtaApply()) return mask(Reason::Ota);
|
||||
#if defined(ESP32_PLATFORM)
|
||||
if (mesh::wifi::networkPower().foregroundActive()) return mask(Reason::Network);
|
||||
|
||||
@@ -400,6 +400,10 @@ void loop() {
|
||||
#endif
|
||||
#ifdef HAS_EXTERNAL_WATCHDOG
|
||||
external_watchdog.loop();
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM)
|
||||
if (the_mesh.getNodePrefs()->powersaving_enabled && the_mesh.isDualRadioActive()
|
||||
&& !morse_timing) board.prepareProfileIdleCycle();
|
||||
#endif
|
||||
const auto sleep_guard = [](bool allowed, mesh::powerdiag::Reason reason) {
|
||||
if (!allowed && mesh::powerdiag::enabled())
|
||||
@@ -431,7 +435,7 @@ void loop() {
|
||||
if (sleep_ms > 0) external_watchdog.feed();
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM)
|
||||
if (sleep_ms > 0) {
|
||||
if (sleep_ms > 0 && the_mesh.canIdleDualRadio()) {
|
||||
if (mesh::powerdiag::enabled()) mesh::powerdiag::decision(0, millis());
|
||||
if (noise_floor_wait) board.sleepMilliseconds(sleep_ms);
|
||||
else board.sleep(0); // nrf ignores seconds param, sleeps whenever possible
|
||||
|
||||
@@ -1725,7 +1725,7 @@ mesh::powerdiag::Mask SensorMesh::pendingWorkReason() const {
|
||||
|| (tracker_awake_until && !millisHasNowPassed(tracker_awake_until))
|
||||
|| (!tracker_storage_fault && getRTCClock()->getCurrentTime() >= tracker_next_check))) return mask(Reason::Tracker);
|
||||
#endif
|
||||
if (isDualRadioActive()) return mask(Reason::DualRadio);
|
||||
if (!canIdleDualRadio()) return mask(Reason::DualRadio);
|
||||
if (hasPendingOtaApply()) return mask(Reason::Ota);
|
||||
if (_cli.hasActiveUserGpioTimer()) return mask(Reason::Scheduled);
|
||||
if (radio_driver.isWatchdogObserving()) return mask(Reason::RadioWatchdog);
|
||||
|
||||
@@ -367,6 +367,10 @@ void loop() {
|
||||
#ifdef HAS_EXTERNAL_WATCHDOG
|
||||
external_watchdog.loop();
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM)
|
||||
if ((the_mesh.getNodePrefs()->powersaving_enabled || the_mesh.isTrackerModeEnabled())
|
||||
&& the_mesh.isDualRadioActive() && !morse_timing) board.prepareProfileIdleCycle();
|
||||
#endif
|
||||
|
||||
const auto sleep_guard = [](bool allowed, mesh::powerdiag::Reason reason) {
|
||||
if (!allowed && mesh::powerdiag::enabled())
|
||||
@@ -397,12 +401,15 @@ void loop() {
|
||||
if (sleep_ms > 0) external_watchdog.feed();
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM)
|
||||
if (sleep_ms > 0) {
|
||||
if (sleep_ms > 0 && the_mesh.canIdleDualRadio()) {
|
||||
#if MESH_ENABLE_SENSOR_TRACKER
|
||||
if (!the_mesh.isTrackerModeEnabled() || armTrackerSleepWake(mesh::power::powerSaveSleepSeconds(sleep_ms))) {
|
||||
if (mesh::powerdiag::enabled()) mesh::powerdiag::decision(0, millis());
|
||||
if (noise_floor_wait) board.sleepMilliseconds(sleep_ms);
|
||||
else board.sleep(0);
|
||||
// Timer setup can schedule another task or consume the scan guard.
|
||||
if (the_mesh.canIdleDualRadio()) {
|
||||
if (mesh::powerdiag::enabled()) mesh::powerdiag::decision(0, millis());
|
||||
if (noise_floor_wait) board.sleepMilliseconds(sleep_ms);
|
||||
else board.sleep(0);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (mesh::powerdiag::enabled())
|
||||
|
||||
+14
-4
@@ -555,13 +555,19 @@ bool Dispatcher::getNextNoiseFloorWakeDelay(uint32_t& delay_millis) const {
|
||||
}
|
||||
|
||||
bool Dispatcher::getNextQueueWakeDelay(uint32_t& delay_millis) const {
|
||||
if (isDualRadioActive()) { delay_millis = 0; return true; }
|
||||
const bool dual_profile = isDualRadioActive();
|
||||
uint32_t profile_delay = 0;
|
||||
if (dual_profile && (!radio_available
|
||||
|| !_radio->getProfileIdleDelay(profile_delay) || profile_delay == 0)) {
|
||||
delay_millis = 0;
|
||||
return true;
|
||||
}
|
||||
#if MESH_ENABLE_FOREIGN_RADIO
|
||||
if (hasForeignRadioFrame()) { delay_millis = 0; return true; }
|
||||
#endif
|
||||
const uint32_t now = _ms->getMillis();
|
||||
bool found = false;
|
||||
uint32_t shortest_delay = 0;
|
||||
bool found = dual_profile;
|
||||
uint32_t shortest_delay = profile_delay;
|
||||
#if MESH_ENABLE_TX_BEACONS
|
||||
if (radio_available && hasBeaconRadioFrame()) {
|
||||
// TX completion and restoration need the fast lifecycle, not timed sleep.
|
||||
@@ -570,7 +576,8 @@ bool Dispatcher::getNextQueueWakeDelay(uint32_t& delay_millis) const {
|
||||
}
|
||||
if (radio_available && beacon_wake_enabled) {
|
||||
const int32_t wait = int32_t(beacon_wake_at - now);
|
||||
shortest_delay = wait > 0 ? uint32_t(wait) : 0;
|
||||
const uint32_t beacon_delay = wait > 0 ? uint32_t(wait) : 0;
|
||||
if (!found || beacon_delay < shortest_delay) shortest_delay = beacon_delay;
|
||||
found = true;
|
||||
}
|
||||
#endif
|
||||
@@ -601,6 +608,9 @@ bool Dispatcher::getNextQueueWakeDelay(uint32_t& delay_millis) const {
|
||||
if (receive_retry && receive_retry_delay < delay_millis) {
|
||||
delay_millis = receive_retry_delay;
|
||||
}
|
||||
if (dual_profile && profile_delay < delay_millis) {
|
||||
delay_millis = profile_delay;
|
||||
}
|
||||
} else {
|
||||
// TX completion/timeout still needs the normal fast lifecycle path.
|
||||
delay_millis = 0;
|
||||
|
||||
@@ -229,6 +229,14 @@ public:
|
||||
virtual bool getReceiveRetryDelay(uint32_t now, uint32_t& delay_ms) const {
|
||||
(void)now; (void)delay_ms; return false;
|
||||
}
|
||||
// A supported profile scanner may idle the MCU while RF keeps receiving.
|
||||
// Return a conservative whole-millisecond service budget, not an RF sleep
|
||||
// permission. False (unsupported/not ready) or zero retains fast polling.
|
||||
// The caller must preserve every other work deadline and the validated
|
||||
// board wake contract, then recheck admission immediately before waiting.
|
||||
virtual bool getProfileIdleDelay(uint32_t& delay_millis) const {
|
||||
(void)delay_millis; return false;
|
||||
}
|
||||
|
||||
virtual void triggerNoiseFloorCalibrate(int threshold) { }
|
||||
|
||||
@@ -642,6 +650,12 @@ public:
|
||||
#endif
|
||||
Radio* getProfileRadio() { return _radio; }
|
||||
bool isDualRadioActive() const { return _radio->profiles() && _radio->profiles()->enabled(); }
|
||||
bool canIdleDualRadio() const {
|
||||
if (!isDualRadioActive()) return true;
|
||||
uint32_t delay_millis = 0;
|
||||
return radio_available && _radio->getProfileIdleDelay(delay_millis)
|
||||
&& delay_millis > 0;
|
||||
}
|
||||
|
||||
Packet* obtainNewPacket();
|
||||
void releasePacket(Packet* packet);
|
||||
|
||||
@@ -542,12 +542,49 @@ void NRF52Board::prepareSleepCycle() {
|
||||
state.publish(token, sd_enabled != 0, usb_status, error);
|
||||
|
||||
// FreeRTOS yields and the state-query SVC can both leave a stale event.
|
||||
// Clear it ONLY at loop entry, before servicing/checking application work.
|
||||
// A radio interrupt after this point must remain visible to the late wait.
|
||||
// Clear it only before a complete work/guard pass: at loop entry, or during
|
||||
// explicit profile-idle preparation before all role sleep predicates. A radio
|
||||
// interrupt after that pass begins must remain visible to the late wait.
|
||||
__SEV();
|
||||
__WFE();
|
||||
}
|
||||
|
||||
bool NRF52Board::canIdleRadioProfile() {
|
||||
#if defined(configTICK_RATE_HZ) && configTICK_RATE_HZ == 1024
|
||||
// RTC1 belongs to FreeRTOS. Only observe its periodic wake; never alter its
|
||||
// counter, compare channels, IRQ ownership, or tickless-idle bookkeeping.
|
||||
return __get_IPSR() == 0 && __get_PRIMASK() == 0
|
||||
&& __get_BASEPRI() == 0 && __get_FAULTMASK() == 0
|
||||
&& nrf_nvic_state.__cr_flag == 0
|
||||
&& xTaskGetSchedulerState() == taskSCHEDULER_RUNNING
|
||||
&& !(SCB->ICSR & SCB_ICSR_PENDSVSET_Msk)
|
||||
&& NRF_RTC1->PRESCALER == 31
|
||||
&& (NRF_RTC1->INTENSET & RTC_INTENSET_TICK_Msk) != 0
|
||||
&& NVIC_GetEnableIRQ(RTC1_IRQn) != 0;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void NRF52Board::prepareProfileIdleCycle() {
|
||||
if (canIdleRadioProfile()) {
|
||||
#if (__FPU_USED == 1)
|
||||
// The disabled FPU IRQ can retain an inexact exception from profile math.
|
||||
// Clear that known source before refreshing the event baseline. An enabled
|
||||
// owner's flags/IRQ belong to its handler, not this polling cleanup.
|
||||
if (NVIC_GetEnableIRQ(FPU_IRQn) == 0) {
|
||||
__set_FPSCR(__get_FPSCR() & ~0x0000009FUL);
|
||||
(void)__get_FPSCR();
|
||||
NVIC_ClearPendingIRQ(FPU_IRQn);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
// This also revalidates context and SDK state, acknowledges a real PendSV
|
||||
// when SoftDevice is on, and clears the old ARM event. All role predicates
|
||||
// must run afterward; an interrupt during those checks is never erased.
|
||||
prepareSleepCycle();
|
||||
}
|
||||
|
||||
bool NRF52Board::isUsbDataConnected() {
|
||||
#if defined(USE_TINYUSB)
|
||||
#if defined(CFG_TUD_CDC) && CFG_TUD_CDC
|
||||
|
||||
@@ -73,9 +73,20 @@ public:
|
||||
virtual bool getBootloaderVersion(char* version, size_t max_len) override;
|
||||
virtual bool startOTAUpdate(const char *id, char reply[], bool force_ap = false) override;
|
||||
virtual bool stopOTAUpdate(char reply[]) override;
|
||||
// Must precede all work servicing/checks in each application loop. The
|
||||
// authoritative SDK queries and stale-event clear are never done at sleep.
|
||||
// Normal loop-entry preparation must precede all work servicing/checks. The
|
||||
// profile-only second preparation below precedes a complete new guard pass;
|
||||
// authoritative SDK queries and event clearing never belong in sleep().
|
||||
void prepareSleepCycle();
|
||||
// Radio-profile service can set an FPU event after loop entry. This second
|
||||
// preparation belongs BEFORE the complete role sleep predicate, never after
|
||||
// any work/USB/radio checks whose interrupt wake must remain visible.
|
||||
void prepareProfileIdleCycle();
|
||||
// Pure readiness check for the pinned 1024 Hz periodic RTC1 wake contract.
|
||||
// The sketch task stays ready while waiting, so the idle task cannot suppress
|
||||
// this tick. A profile must still reserve this interval plus its own guard.
|
||||
static constexpr uint32_t ProfileIdleTickUs = 977;
|
||||
static constexpr uint32_t ProfileIdleRtcTicks = 32;
|
||||
static bool canIdleRadioProfile();
|
||||
virtual void sleep(uint32_t secs) override;
|
||||
bool isUserGpioAvailable(uint8_t pin) const override;
|
||||
bool isExternalPowered() override;
|
||||
|
||||
@@ -11,7 +11,8 @@ struct SleepCycleSnapshot {
|
||||
bool softdevice_enabled = false;
|
||||
};
|
||||
|
||||
// The application task publishes once at loop entry, before checking work.
|
||||
// The application task publishes before checking work. Profile idle can
|
||||
// re-baseline after servicing, but only before a complete new guard pass.
|
||||
// A SoftDevice startup attempt can invalidate from another task without
|
||||
// racing the publication. No SDK state is inferred from NVIC/vector state.
|
||||
class SleepCycleState {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <esp_heap_caps.h>
|
||||
#elif defined(NRF52_PLATFORM)
|
||||
#include <utility/debug.h>
|
||||
#include "../NRF52Board.h"
|
||||
#elif defined(STM32_PLATFORM)
|
||||
#include <sys/types.h>
|
||||
extern "C" caddr_t _sbrk(int increment);
|
||||
@@ -768,6 +769,9 @@ mesh::RadioParamApplyResult RadioLibWrapper::tryRestoreCodingRate(uint8_t cr) {
|
||||
}
|
||||
|
||||
void RadioLibWrapper::serviceProfileScan() {
|
||||
#if defined(NRF52_PLATFORM)
|
||||
_profile_idle_visit_ticks = 0; // Every deferred/restarted scan fails closed.
|
||||
#endif
|
||||
syncProfileClock(_profiles.enabled());
|
||||
if (_cw_active) return;
|
||||
if (!_params_valid || (_profile_retry_at && (int32_t)(millis() - _profile_retry_at) < 0)) return;
|
||||
@@ -816,6 +820,10 @@ void RadioLibWrapper::serviceProfileScan() {
|
||||
if (!_profiles.switchTestReady()) return;
|
||||
}
|
||||
uint8_t target = _active_profile;
|
||||
#if defined(NRF52_PLATFORM)
|
||||
const uint8_t visited_profile = _active_profile;
|
||||
#endif
|
||||
uint32_t visit_us = 0;
|
||||
const bool restart_scan = _profiles.enabled()
|
||||
&& (_profile_scan_generation[0] != _profiles.generation[0]
|
||||
|| _profile_scan_generation[1] != _profiles.generation[1]);
|
||||
@@ -834,7 +842,7 @@ void RadioLibWrapper::serviceProfileScan() {
|
||||
if (!_profiles.enabled()) target = 0;
|
||||
else if (restart_scan) target = _profiles.slowerProfile();
|
||||
else {
|
||||
uint32_t visit_us = _profiles.listenUs(
|
||||
visit_us = _profiles.listenUs(
|
||||
_active_profile, profilePreamble(_profiles.slowerProfile()));
|
||||
// A normal visit can be shorter than the RSSI settle interval on some
|
||||
// chips. Hold only a visit that is actually due to contribute its next
|
||||
@@ -869,6 +877,55 @@ void RadioLibWrapper::serviceProfileScan() {
|
||||
recalibrateNoiseFloor();
|
||||
endReconfigure(resume);
|
||||
}
|
||||
#if defined(NRF52_PLATFORM)
|
||||
// A real hop gets a fresh budget on the next pass. Never use the old
|
||||
// profile's visit duration with the new profile's RX timestamp.
|
||||
if (_profiles.enabled() && !restart_scan && _active_profile == visited_profile
|
||||
&& result == mesh::RadioParamApplyResult::APPLIED
|
||||
&& !isChipBusy() && !isPacketPendingOrReceiving()) {
|
||||
_profile_idle_visit_ticks = uint64_t(visit_us) * 512 / 15625;
|
||||
_profile_idle_stamp = _profile_visit_stamp;
|
||||
_profile_idle_switch_us = _profiles.switchBudgetUs();
|
||||
_profile_idle_preamble = _physical_preamble;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool RadioLibWrapper::getProfileIdleDelay(uint32_t& delay_ms) const {
|
||||
delay_ms = 0;
|
||||
#if defined(NRF52_PLATFORM)
|
||||
if (!_profiles.enabled() || !NRF52Board::canIdleRadioProfile()
|
||||
|| !_profile_idle_visit_ticks || !_params_valid || state != STATE_RX
|
||||
|| !_profile_rxps_suspended || _rx_ps_enabled || _rx_ps_armed
|
||||
|| _rx_hold_continuous || _rx_retry_pending || _profile_retry_at
|
||||
|| _profile_refresh_required || _nf_refresh_requested || _nf_calib_active
|
||||
|| !_noise_floor_valid || !_noise_floor_secondary_valid
|
||||
|| _wd_observe_until || _cw_active || _morse_owned || hasPendingRadioInterrupt()
|
||||
|| !_profiles.switchTestReady()
|
||||
|| _profile_scan_generation[0] != _profiles.generation[0]
|
||||
|| _profile_scan_generation[1] != _profiles.generation[1]
|
||||
|| _active_profile > 1 || _profile_generation != _profiles.generation[_active_profile]
|
||||
|| _profile_idle_stamp != _profile_visit_stamp
|
||||
|| _profile_idle_preamble != _physical_preamble
|
||||
|| _profile_idle_switch_us != _profiles.switchBudgetUs()) return false;
|
||||
#if MESH_ENABLE_TX_BEACONS
|
||||
if (_beacon_owned) return false;
|
||||
#endif
|
||||
#if MESH_ENABLE_MORSE_RX
|
||||
if (_morse_rx_owned) return false;
|
||||
#endif
|
||||
const uint32_t elapsed = (NRF_RTC2->COUNTER - _profile_idle_stamp) & 0x00ffffffUL;
|
||||
// RTC1 wakes the ready sketch task at 1024 Hz (32 RTC2 ticks). Retain
|
||||
// another 16 ticks (488 us) for return-to-loop work, validated on RAK3401.
|
||||
// Short visits poll; neither a new timer nor RF duty cycling is needed.
|
||||
constexpr uint32_t guard_ticks = 16;
|
||||
if (_profile_idle_visit_ticks >= 0x00800000UL || elapsed >= _profile_idle_visit_ticks
|
||||
|| _profile_idle_visit_ticks - elapsed <= 32 + guard_ticks) return false;
|
||||
delay_ms = uint64_t(_profile_idle_visit_ticks - elapsed - guard_ticks) * 15625 / 512000;
|
||||
return delay_ms != 0;
|
||||
#else
|
||||
return false; // Other platforms retain polling until their wake path is validated.
|
||||
#endif
|
||||
}
|
||||
|
||||
uint32_t RadioLibWrapper::getProfileAirtime(uint8_t profile, int len, uint8_t cr) {
|
||||
|
||||
@@ -113,6 +113,14 @@ protected:
|
||||
bool _profile_rxps_suspended = false;
|
||||
bool _profile_saved_rxps = false;
|
||||
bool _profile_refresh_required = false;
|
||||
#if defined(NRF52_PLATFORM)
|
||||
// Published by the scanner before the final sleep predicates. The late
|
||||
// query uses integers only: floating-point work can leave a stale FPU event.
|
||||
uint32_t _profile_idle_visit_ticks = 0;
|
||||
uint32_t _profile_idle_stamp = 0;
|
||||
uint32_t _profile_idle_switch_us = 0;
|
||||
uint16_t _profile_idle_preamble = 0;
|
||||
#endif
|
||||
#if defined(MESH_HIL_TX_FREQ_MHZ)
|
||||
bool hilPacketTransmitAllowed() const;
|
||||
#endif
|
||||
@@ -547,6 +555,7 @@ public:
|
||||
}
|
||||
bool getReceiveRetryDelay(uint32_t now, uint32_t& delay_ms) const override;
|
||||
bool getNoiseFloorWakeDelay(uint32_t now, uint32_t& delay_ms) const override;
|
||||
bool getProfileIdleDelay(uint32_t& delay_ms) const override;
|
||||
void resetStats() { n_recv = n_sent = n_recv_errors = 0; }
|
||||
|
||||
uint8_t getRadioState() const override;
|
||||
|
||||
Vendored
+2
@@ -89,11 +89,13 @@ public:
|
||||
mesh::powerdiag::Mask pendingWorkReason() const;
|
||||
bool canRecoverUsbLogging() const;
|
||||
bool isDualRadioActive() const{return false;}
|
||||
bool canIdleDualRadio() const{return true;}
|
||||
bool hasOutbound() const{return false;}
|
||||
bool isAnyTempRadioActive() const{return false;}
|
||||
bool hasPendingOtaApply() const{return false;}
|
||||
bool hasPendingReqs() const{return false;}
|
||||
bool hasQueuedWorkDue() const{return false;}
|
||||
bool getNextNoiseFloorWakeDelay(uint32_t&) const{return false;}
|
||||
bool hasRetryWorkDue() const{return false;}
|
||||
bool millisHasNowPassed(uint32_t when) const{return _ms->now>=when;}
|
||||
bool isContactWriteDue() const{return _store->contact_pending;}
|
||||
|
||||
@@ -243,7 +243,7 @@ int main() {
|
||||
def test_main_loop_gates_sleep_on_pending_work(self):
|
||||
main = (ROOT / "examples/companion_radio/main.cpp").read_text()
|
||||
sleep_start = main.index(" using mesh::powerdiag::Reason;")
|
||||
sleep_end = main.index("if (can_sleep)", sleep_start)
|
||||
sleep_end = main.index("if (can_sleep", sleep_start)
|
||||
sleep_gate = main[sleep_start:sleep_end]
|
||||
|
||||
self.assertIn("the_mesh.pendingWorkReason()", sleep_gate)
|
||||
@@ -258,7 +258,7 @@ int main() {
|
||||
def test_polled_rak_ethernet_holds_event_sleep_through_pending_off(self):
|
||||
main = (ROOT / "examples/companion_radio/main.cpp").read_text()
|
||||
start = main.index(" using mesh::powerdiag::Reason;")
|
||||
gate = main[start:main.index("if (can_sleep)", start)]
|
||||
gate = main[start:main.index("if (can_sleep", start)]
|
||||
fixture = r'''
|
||||
#include <cassert>
|
||||
#include <helpers/PowerSavingDiagnostics.h>
|
||||
|
||||
@@ -269,6 +269,7 @@ struct MyMesh {
|
||||
#endif
|
||||
bool millisHasNowPassed(uint32_t at) const { return int32_t(now_ms-at)>=0; }
|
||||
bool isDualRadioActive() const { return false; }
|
||||
bool canIdleDualRadio() const { return true; }
|
||||
bool isContactWriteDue() const { return false; }
|
||||
bool hasPendingOtaApply() const { return false; }
|
||||
bool hasPendingReqs() const { return _delayed_replies.hasRequest(); }
|
||||
|
||||
@@ -230,6 +230,7 @@ struct MyMesh {
|
||||
bool getNextRetryWakeDelay(uint32_t&) const { return false; }
|
||||
bool getNextNoiseFloorWakeDelay(uint32_t&) const { return false; }
|
||||
bool isDualRadioActive() const { return false; }
|
||||
bool canIdleDualRadio() const { return true; }
|
||||
uint32_t limitSleepToMillisTimer(unsigned long, uint32_t secs) const { return secs; }
|
||||
uint32_t limitSleepToScheduledRadioWork(uint32_t secs) const { return secs; }
|
||||
const AbstractBridge* activeBridge() const { return @WORKER@; }
|
||||
|
||||
@@ -145,6 +145,7 @@ struct Mesh {
|
||||
return pending ? mesh::powerdiag::mask(mesh::powerdiag::Reason::Mesh) : 0;
|
||||
}
|
||||
bool isDualRadioActive() const { return dual; }
|
||||
bool canIdleDualRadio() const { return !dual; }
|
||||
bool getNextNoiseFloorWakeDelay(uint32_t&) const { return false; }
|
||||
mesh::Radio* getProfileRadio() { return &timing_radio; }
|
||||
} the_mesh;
|
||||
|
||||
@@ -64,6 +64,7 @@ struct MyMesh {
|
||||
bool getNextNoiseFloorWakeDelay(uint32_t&) const { return false; }
|
||||
bool dual_radio = false;
|
||||
bool isDualRadioActive() const { return dual_radio; }
|
||||
bool canIdleDualRadio() const { return !dual_radio; }
|
||||
uint32_t limitSleepToMillisTimer(unsigned long, uint32_t secs) const { return secs; }
|
||||
uint32_t limitSleepToScheduledRadioWork(uint32_t secs) const { return secs; }
|
||||
bool hasPendingWork() const;
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execute the real two-phase nRF52 profile-idle preparation and tick guard."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from cpp_source import body
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class Nrf52ProfileIdleTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.temp = tempfile.TemporaryDirectory(prefix="meshcore-profile-idle-")
|
||||
cls.addClassCleanup(cls.temp.cleanup)
|
||||
source = (ROOT / "src/helpers/NRF52Board.cpp").read_text(encoding="ascii")
|
||||
methods = "\n".join(body(source, signature) for signature in (
|
||||
"void NRF52Board::prepareSleepCycle()",
|
||||
"bool NRF52Board::canIdleRadioProfile()",
|
||||
"void NRF52Board::prepareProfileIdleCycle()",
|
||||
"void NRF52Board::sleep(uint32_t secs)",
|
||||
))
|
||||
fixture = r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <initializer_list>
|
||||
#include <helpers/PowerSavingDiagnostics.h>
|
||||
#include <helpers/nrf52/SleepCycleState.h>
|
||||
#ifndef TEST_TICK_RATE
|
||||
#define TEST_TICK_RATE 1024
|
||||
#endif
|
||||
#define configTICK_RATE_HZ TEST_TICK_RATE
|
||||
#define __FPU_USED 1
|
||||
#define NRF_CRYPTOCELL 1
|
||||
constexpr uint32_t NRF_SUCCESS = 0, NRF_ERROR_INVALID_STATE = 8;
|
||||
constexpr uint32_t SCB_ICSR_PENDSVSET_Msk = 1UL << 28;
|
||||
constexpr uint32_t RTC_INTENSET_TICK_Msk = 1;
|
||||
constexpr int RTC1_IRQn = 17, FPU_IRQn = 38, CRYPTOCELL_IRQn = 42;
|
||||
constexpr int taskSCHEDULER_RUNNING = 1;
|
||||
struct SdkNvicState { volatile uint32_t __cr_flag = 0; } nrf_nvic_state;
|
||||
struct Scb { uint32_t ICSR = 0; } scb;
|
||||
static Scb* const SCB = &scb;
|
||||
struct Rtc { uint32_t PRESCALER = 31, INTENSET = RTC_INTENSET_TICK_Msk; } rtc;
|
||||
static Rtc* const NRF_RTC1 = &rtc;
|
||||
struct Power { uint32_t USBREGSTATUS = 0; } power;
|
||||
static Power* const NRF_POWER = &power;
|
||||
static bool irq_enabled[64] = {}, irq_pending[64] = {};
|
||||
static unsigned clears[64] = {};
|
||||
static uint32_t ipsr = 0, primask = 0, basepri = 0, faultmask = 0;
|
||||
static uint32_t fpscr = 0, clock_us = 10000;
|
||||
static int scheduler_state = taskSCHEDULER_RUNNING;
|
||||
static bool event = false, softdevice = false, work_pending = false;
|
||||
static bool inject_during_prepare = false;
|
||||
static unsigned app_irq = 1, acknowledged_irq = 0;
|
||||
static unsigned state_queries = 0, sdk_calls = 0, sleeps = 0, delays = 0;
|
||||
static uint32_t __get_IPSR() { return ipsr; }
|
||||
static uint32_t __get_PRIMASK() { return primask; }
|
||||
static uint32_t __get_BASEPRI() { return basepri; }
|
||||
static uint32_t __get_FAULTMASK() { return faultmask; }
|
||||
static int xTaskGetSchedulerState() { return scheduler_state; }
|
||||
static uint32_t NVIC_GetEnableIRQ(int irq) { return irq_enabled[irq]; }
|
||||
static void NVIC_ClearPendingIRQ(int irq) { irq_pending[irq] = false; ++clears[irq]; }
|
||||
static uint32_t __get_FPSCR() { return fpscr; }
|
||||
static void __set_FPSCR(uint32_t value) { fpscr = value; }
|
||||
static uint32_t millis() { return clock_us / 1000; }
|
||||
static uint32_t micros() { return clock_us; }
|
||||
static void delay(uint32_t ms) { delays += ms; }
|
||||
static void applicationIrq() { ++app_irq; event = true; work_pending = true; }
|
||||
static void taskYIELD() { ++app_irq; event = true; }
|
||||
namespace mesh_nrf52 {
|
||||
static uint32_t softdeviceIsEnabled(uint8_t& enabled) {
|
||||
++state_queries; enabled = softdevice; event = true; return NRF_SUCCESS;
|
||||
}
|
||||
}
|
||||
static uint32_t sd_power_usbregstatus_get(uint32_t* value) {
|
||||
*value = power.USBREGSTATUS; event = true; return NRF_SUCCESS;
|
||||
}
|
||||
static uint32_t sd_app_evt_wait() {
|
||||
++sdk_calls;
|
||||
const bool history = app_irq != acknowledged_irq;
|
||||
acknowledged_irq = app_irq;
|
||||
event = false;
|
||||
if (!history && !irq_pending[FPU_IRQn] && !irq_pending[CRYPTOCELL_IRQn]) {
|
||||
assert(!work_pending); ++sleeps; clock_us += 500;
|
||||
++app_irq;
|
||||
}
|
||||
return NRF_SUCCESS;
|
||||
}
|
||||
static void __SEV() { event = true; }
|
||||
static void __WFE() {
|
||||
if (inject_during_prepare) { inject_during_prepare = false; applicationIrq(); }
|
||||
if (event) event = false;
|
||||
else { assert(!work_pending); ++sleeps; clock_us += 500; }
|
||||
}
|
||||
struct NRF52Board {
|
||||
bool radio_test = false;
|
||||
bool isRadioTestActive() const { return radio_test; }
|
||||
void prepareSleepCycle();
|
||||
static bool canIdleRadioProfile();
|
||||
void prepareProfileIdleCycle();
|
||||
void sleep(uint32_t secs);
|
||||
};
|
||||
@METHODS@
|
||||
static void floatingWork() {
|
||||
fpscr = 0x60000010UL; irq_pending[FPU_IRQn] = true;
|
||||
event = true; // SEVONPEND retains the event even after NVIC pending clears.
|
||||
}
|
||||
static void ready() {
|
||||
irq_enabled[RTC1_IRQn] = true;
|
||||
assert(NRF52Board::canIdleRadioProfile() == (TEST_TICK_RATE == 1024));
|
||||
}
|
||||
static void normal(bool sdk) {
|
||||
ready(); softdevice = sdk;
|
||||
NRF52Board board;
|
||||
board.prepareSleepCycle();
|
||||
const auto initial_queries = state_queries;
|
||||
floatingWork();
|
||||
board.prepareProfileIdleCycle();
|
||||
assert(state_queries == initial_queries + 1);
|
||||
assert(!event && !irq_pending[FPU_IRQn] && (fpscr & 0x9f) == 0);
|
||||
assert((fpscr & ~0x9fUL) == 0x60000000UL);
|
||||
assert(clears[FPU_IRQn] == 1 && !work_pending);
|
||||
const auto waits_before = sdk_calls;
|
||||
assert(NRF52Board::canIdleRadioProfile()); // Final pure recheck creates no event/SVC.
|
||||
assert(!event && state_queries == initial_queries + 1 && sdk_calls == waits_before);
|
||||
board.sleep(0);
|
||||
assert(sleeps == 1 && delays == 0);
|
||||
}
|
||||
static void legacy() {
|
||||
ready(); NRF52Board board;
|
||||
board.prepareSleepCycle(); floatingWork();
|
||||
board.sleep(0);
|
||||
// The late FPU cleanup cannot remove an already-latched ARM event. This
|
||||
// negative control reproduces the measured tuple-dependent raw-WFE insomnia.
|
||||
assert(sleeps == 0 && !irq_pending[FPU_IRQn]);
|
||||
}
|
||||
static void vetoes() {
|
||||
ready();
|
||||
if (TEST_TICK_RATE != 1024) return;
|
||||
for (uint32_t* value : {&ipsr, &primask, &basepri, &faultmask}) {
|
||||
*value = 1; assert(!NRF52Board::canIdleRadioProfile()); *value = 0;
|
||||
}
|
||||
scheduler_state = 0; assert(!NRF52Board::canIdleRadioProfile());
|
||||
scheduler_state = taskSCHEDULER_RUNNING;
|
||||
nrf_nvic_state.__cr_flag = 1; assert(!NRF52Board::canIdleRadioProfile());
|
||||
nrf_nvic_state.__cr_flag = 0;
|
||||
scb.ICSR = SCB_ICSR_PENDSVSET_Msk; assert(!NRF52Board::canIdleRadioProfile());
|
||||
scb.ICSR = 0;
|
||||
rtc.PRESCALER = 0; assert(!NRF52Board::canIdleRadioProfile()); rtc.PRESCALER = 31;
|
||||
rtc.INTENSET = 0; assert(!NRF52Board::canIdleRadioProfile());
|
||||
rtc.INTENSET = RTC_INTENSET_TICK_Msk;
|
||||
irq_enabled[RTC1_IRQn] = false; assert(!NRF52Board::canIdleRadioProfile());
|
||||
irq_enabled[RTC1_IRQn] = true;
|
||||
assert(NRF52Board::canIdleRadioProfile() && state_queries == 0 && sdk_calls == 0);
|
||||
}
|
||||
static void owners() {
|
||||
ready(); NRF52Board board;
|
||||
floatingWork(); irq_enabled[FPU_IRQn] = true;
|
||||
irq_pending[7] = true;
|
||||
board.prepareProfileIdleCycle();
|
||||
assert(clears[FPU_IRQn] == 0 && irq_pending[FPU_IRQn]);
|
||||
assert(fpscr == 0x60000010UL && irq_pending[7] && clears[7] == 0);
|
||||
irq_enabled[FPU_IRQn] = false;
|
||||
nrf_nvic_state.__cr_flag = 1;
|
||||
const auto before = state_queries;
|
||||
board.prepareProfileIdleCycle();
|
||||
assert(state_queries == before && clears[FPU_IRQn] == 0 && irq_pending[FPU_IRQn]);
|
||||
assert(mesh_nrf52::sleepCycleState().lastError() == NRF_ERROR_INVALID_STATE);
|
||||
}
|
||||
static void arrivals(bool sdk, bool during_prepare) {
|
||||
ready(); softdevice = sdk; NRF52Board board;
|
||||
floatingWork(); inject_during_prepare = during_prepare;
|
||||
board.prepareProfileIdleCycle();
|
||||
if (during_prepare) {
|
||||
// Preparation may consume the event, but the ENTIRE following role
|
||||
// predicate still observes the ISR-owned work and must decline sleep.
|
||||
assert(work_pending && sleeps == 0);
|
||||
return;
|
||||
}
|
||||
assert(!work_pending && NRF52Board::canIdleRadioProfile());
|
||||
applicationIrq(); // Arrives after the role predicate's final decision.
|
||||
board.sleep(0);
|
||||
assert(work_pending && sleeps == 0 && delays == 0);
|
||||
}
|
||||
static void lateFloatingWork() {
|
||||
ready(); NRF52Board board;
|
||||
board.prepareProfileIdleCycle();
|
||||
floatingWork(); // A missed float call must degrade idle, never erase its event.
|
||||
board.sleep(0);
|
||||
assert(sleeps == 0 && delays == 0);
|
||||
}
|
||||
int main(int argc, char** argv) {
|
||||
assert(argc == 2);
|
||||
if (!std::strcmp(argv[1], "raw")) normal(false);
|
||||
else if (!std::strcmp(argv[1], "sdk")) normal(true);
|
||||
else if (!std::strcmp(argv[1], "legacy")) legacy();
|
||||
else if (!std::strcmp(argv[1], "vetoes")) vetoes();
|
||||
else if (!std::strcmp(argv[1], "owners")) owners();
|
||||
else if (!std::strcmp(argv[1], "late-raw")) arrivals(false, false);
|
||||
else if (!std::strcmp(argv[1], "late-sdk")) arrivals(true, false);
|
||||
else if (!std::strcmp(argv[1], "prepare-raw")) arrivals(false, true);
|
||||
else if (!std::strcmp(argv[1], "prepare-sdk")) arrivals(true, true);
|
||||
else if (!std::strcmp(argv[1], "late-fpu")) lateFloatingWork();
|
||||
else assert(false);
|
||||
}
|
||||
'''.replace("@METHODS@", methods)
|
||||
cls.binaries = {}
|
||||
for tick_rate in (1024, 1000):
|
||||
cpp = Path(cls.temp.name) / ("idle-%d.cpp" % tick_rate)
|
||||
binary = cpp.with_suffix("")
|
||||
cpp.write_text(fixture, encoding="ascii")
|
||||
result = subprocess.run(
|
||||
shlex.split(os.environ.get("CXX", "g++"))
|
||||
+ ["-std=c++11", "-Wall", "-Wextra", "-Werror", "-pedantic",
|
||||
"-fsanitize=address,undefined", "-fno-sanitize-recover=all",
|
||||
"-fno-pie", "-no-pie", "-DTEST_TICK_RATE=%d" % tick_rate,
|
||||
"-I", str(ROOT / "src"), str(cpp), "-o", str(binary)],
|
||||
capture_output=True, text=True, timeout=60)
|
||||
if result.returncode:
|
||||
raise RuntimeError(result.stdout + result.stderr)
|
||||
cls.binaries[tick_rate] = binary
|
||||
|
||||
def run_case(self, case, tick_rate=1024):
|
||||
result = subprocess.run([str(self.binaries[tick_rate]), case],
|
||||
capture_output=True, text=True, timeout=30)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
|
||||
def test_fpu_event_refresh_restores_raw_and_softdevice_single_wait(self):
|
||||
self.run_case("raw")
|
||||
self.run_case("sdk")
|
||||
|
||||
def test_late_existing_fpu_cleanup_reproduces_raw_insomnia(self):
|
||||
self.run_case("legacy")
|
||||
|
||||
def test_tick_context_and_pending_scheduler_guards_are_pure(self):
|
||||
self.run_case("vetoes")
|
||||
self.run_case("vetoes", tick_rate=1000)
|
||||
|
||||
def test_enabled_fpu_owner_unrelated_irq_and_sdk_critical_are_preserved(self):
|
||||
self.run_case("owners")
|
||||
|
||||
def test_work_during_preparation_survives_for_complete_role_predicate(self):
|
||||
self.run_case("prepare-raw")
|
||||
self.run_case("prepare-sdk")
|
||||
|
||||
def test_irq_after_predicate_prevents_sleep_without_late_event_clear(self):
|
||||
self.run_case("late-raw")
|
||||
self.run_case("late-sdk")
|
||||
|
||||
def test_unexpected_float_after_preparation_remains_immediate_not_double_wait(self):
|
||||
self.run_case("late-fpu")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -175,7 +175,7 @@ struct AbstractBridge {
|
||||
uint32_t powerWakeDelayMs(uint32_t) const { return wake_ms; }
|
||||
};
|
||||
struct MyMesh {
|
||||
bool dual = false, ota = false, queue = false, retry = false, scheduled = false;
|
||||
bool dual = false, profile_idle = 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, calibration_delay = 0;
|
||||
@@ -195,6 +195,7 @@ struct MyMesh {
|
||||
const AbstractBridge* activeBridge() const { return bridge; }
|
||||
bool rs232BridgeEnabled() const { return bridge_enabled; }
|
||||
bool isDualRadioActive() const { return dual; }
|
||||
bool canIdleDualRadio() const { return !dual || profile_idle; }
|
||||
bool hasPendingOtaApply() const { return ota; }
|
||||
bool hasQueuedWorkDue() const { return queue; }
|
||||
bool hasRetryWorkDue() const { return retry; }
|
||||
@@ -232,6 +233,11 @@ int main() {
|
||||
flag = false;
|
||||
};
|
||||
check(node.dual, Reason::DualRadio); check(node.ota, Reason::Ota);
|
||||
node.dual = true; node.profile_idle = true;
|
||||
assert(!node.hasPendingWork());
|
||||
check(node.ota, Reason::Ota); // An idle scanner does not bypass other role guards.
|
||||
check(node.queue, Reason::Queue); check(node.retry, Reason::Retry);
|
||||
node.dual = node.profile_idle = false;
|
||||
check(node._local_cli_output.busy_value, Reason::Serial);
|
||||
check(node.deferred_cli_command.pending, Reason::Scheduled);
|
||||
check(node.uart.running, Reason::Bridge);
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
"""Exercise production radio2 idle admission and its final role rechecks."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from cpp_source import body
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
HARNESS = r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <RadioProfiles.h>
|
||||
#define STATE_RX 1
|
||||
#define MESH_ENABLE_TX_BEACONS 1
|
||||
#define MESH_ENABLE_MORSE_RX 1
|
||||
struct Rtc { uint32_t COUNTER = 200; } rtc;
|
||||
#define NRF_RTC2 (&rtc)
|
||||
struct NRF52Board {
|
||||
static bool ready;
|
||||
static bool canIdleRadioProfile() { return ready; }
|
||||
};
|
||||
bool NRF52Board::ready = true;
|
||||
struct RadioLibWrapper {
|
||||
mesh::RadioProfiles _profiles;
|
||||
uint32_t _profile_idle_visit_ticks=1000, _profile_idle_stamp=100;
|
||||
uint32_t _profile_visit_stamp=100, _profile_idle_switch_us=1100;
|
||||
uint16_t _profile_idle_preamble=32, _physical_preamble=32;
|
||||
uint32_t _profile_generation=1, _profile_scan_generation[2]={1,1};
|
||||
uint32_t _profile_retry_at=0, _wd_observe_until=0;
|
||||
uint8_t state=STATE_RX, _active_profile=0;
|
||||
bool _params_valid=true, _profile_rxps_suspended=true;
|
||||
bool _rx_ps_enabled=false, _rx_ps_armed=false, _rx_hold_continuous=false;
|
||||
bool _rx_retry_pending=false, _profile_refresh_required=false;
|
||||
bool _nf_refresh_requested=false, _nf_calib_active=false;
|
||||
bool _noise_floor_valid=true, _noise_floor_secondary_valid=true;
|
||||
bool _cw_active=false, _morse_owned=false, _beacon_owned=false, _morse_rx_owned=false;
|
||||
bool irq=false;
|
||||
bool hasPendingRadioInterrupt() const { return irq; }
|
||||
bool getProfileIdleDelay(uint32_t&) const;
|
||||
RadioLibWrapper() {
|
||||
_profiles.secondary.mode=mesh::RadioProfileMode::RxTx;
|
||||
_profiles.switch_test_samples[0]=_profiles.switch_test_samples[1]=4;
|
||||
_profiles.switch_test_max_us[0]=_profiles.switch_test_max_us[1]=1000;
|
||||
}
|
||||
};
|
||||
@METHOD@
|
||||
int main() {
|
||||
uint32_t delay=999;
|
||||
RadioLibWrapper w;
|
||||
#if !defined(NRF52_PLATFORM)
|
||||
assert(!w.getProfileIdleDelay(delay) && delay==0);
|
||||
#else
|
||||
assert(w.getProfileIdleDelay(delay) && delay==26);
|
||||
// Exact early-wake boundary, rounding, already expired, and RTC wrap.
|
||||
for (uint32_t remaining=0; remaining<=48; ++remaining) {
|
||||
rtc.COUNTER=100+1000-remaining;
|
||||
assert(!w.getProfileIdleDelay(delay) && delay==0);
|
||||
}
|
||||
rtc.COUNTER=100+1000-49;
|
||||
assert(w.getProfileIdleDelay(delay) && delay==1);
|
||||
rtc.COUNTER=1101;
|
||||
assert(!w.getProfileIdleDelay(delay));
|
||||
w._profile_visit_stamp=w._profile_idle_stamp=0x00fffff0;
|
||||
rtc.COUNTER=0x10;
|
||||
assert(w.getProfileIdleDelay(delay) && delay==29);
|
||||
w=RadioLibWrapper(); rtc.COUNTER=200;
|
||||
w._profile_idle_visit_ticks=0x00800000;
|
||||
assert(!w.getProfileIdleDelay(delay));
|
||||
// Every software owner, cached timing proof, and asynchronous IRQ fails closed.
|
||||
auto denied=[&](auto mutate) {
|
||||
w=RadioLibWrapper(); rtc.COUNTER=200; delay=999;
|
||||
mutate(); assert(!w.getProfileIdleDelay(delay) && delay==0);
|
||||
};
|
||||
denied([&]{NRF52Board::ready=false;}); NRF52Board::ready=true;
|
||||
denied([&]{w._profiles.secondary.mode=mesh::RadioProfileMode::Off;});
|
||||
denied([&]{w._profile_idle_visit_ticks=0;});
|
||||
denied([&]{w._params_valid=false;});
|
||||
denied([&]{w.state=3;});
|
||||
denied([&]{w._profile_rxps_suspended=false;});
|
||||
denied([&]{w._rx_ps_enabled=true;});
|
||||
denied([&]{w._rx_ps_armed=true;});
|
||||
denied([&]{w._rx_hold_continuous=true;});
|
||||
denied([&]{w._rx_retry_pending=true;});
|
||||
denied([&]{w._profile_retry_at=1;});
|
||||
denied([&]{w._profile_refresh_required=true;});
|
||||
denied([&]{w._nf_refresh_requested=true;});
|
||||
denied([&]{w._nf_calib_active=true;});
|
||||
denied([&]{w._noise_floor_valid=false;});
|
||||
denied([&]{w._noise_floor_secondary_valid=false;});
|
||||
denied([&]{w._wd_observe_until=1;});
|
||||
denied([&]{w._cw_active=true;});
|
||||
denied([&]{w._morse_owned=true;});
|
||||
denied([&]{w._beacon_owned=true;});
|
||||
denied([&]{w._morse_rx_owned=true;});
|
||||
denied([&]{w.irq=true;});
|
||||
denied([&]{w._profiles.resetSwitchTest();});
|
||||
denied([&]{++w._profiles.generation[0];});
|
||||
denied([&]{++w._profiles.generation[1];});
|
||||
denied([&]{w._active_profile=2;});
|
||||
denied([&]{++w._profile_generation;});
|
||||
denied([&]{++w._profile_visit_stamp;});
|
||||
denied([&]{++w._physical_preamble;});
|
||||
denied([&]{++w._profiles.switch_test_max_us[1];});
|
||||
// A second query must reject an interrupt or elapsed deadline after admission.
|
||||
w=RadioLibWrapper();
|
||||
assert(w.getProfileIdleDelay(delay)); w.irq=true;
|
||||
assert(!w.getProfileIdleDelay(delay));
|
||||
w.irq=false; rtc.COUNTER=1099;
|
||||
assert(!w.getProfileIdleDelay(delay));
|
||||
mesh::RadioProfileParams longfast;
|
||||
longfast.bw=250; longfast.sf=11;
|
||||
longfast.protocol=mesh::RadioProfileProtocol::Meshtext;
|
||||
assert(mesh::RadioProfiles::minimumListenUs(longfast,true)==37684);
|
||||
w._profiles.secondary.params=longfast;
|
||||
assert(w._profiles.preamble(1,32)==16);
|
||||
#endif
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class ProfileIdleTests(unittest.TestCase):
|
||||
def test_scanner_cache_tracks_visits_and_invalidates_early_returns(self):
|
||||
from test_radio_profile_scan import HARNESS as scan_harness
|
||||
source = (ROOT / "src/helpers/radiolib/RadioLibWrappers.cpp").read_text()
|
||||
names = [("uint8_t", "beginReconfigure"), ("void", "endReconfigure"),
|
||||
("void", "serviceProfileScan")] + [("mesh::RadioParamApplyResult", name) for name in
|
||||
("tuneProfile", "prepareTransmitProfile", "tryRestoreCodingRate",
|
||||
"trySetParams", "trySetPrimaryParams")]
|
||||
methods = "\n".join(body(source, f"{kind} RadioLibWrapper::{name}(") for kind, name in names)
|
||||
methods += "\n" + body(source,
|
||||
"mesh::RadioParamApplyResult RadioLibWrapper::prepareTransmitProfile(uint8_t profile, bool")
|
||||
wrapper = (ROOT / "src/helpers/radiolib/CustomSX1262Wrapper.h").read_text()
|
||||
methods += "\n" + body(wrapper, "void setProfileStandbyWarm(").replace(
|
||||
"void setProfileStandbyWarm(bool enabled) override",
|
||||
"void RadioLibWrapper::setProfileStandbyWarm(bool enabled)")
|
||||
fixture = scan_harness.split("int main()", 1)[0].replace("@METHODS@", methods)
|
||||
fixture = fixture.replace(" bool _cw_active=false;", """
|
||||
uint32_t _profile_idle_visit_ticks=0, _profile_idle_stamp=0, _profile_idle_switch_us=0;
|
||||
uint16_t _profile_idle_preamble=0;
|
||||
bool _cw_active=false;
|
||||
""")
|
||||
fixture += r'''
|
||||
int main() {
|
||||
RadioLibWrapper w; w.enable();
|
||||
assert(w._profile_idle_visit_ticks==0); // restart needs a fresh pass
|
||||
w.serviceProfileScan();
|
||||
const auto ticks=w._profile_idle_visit_ticks;
|
||||
assert(ticks>0 && w._profile_idle_stamp==w._profile_visit_stamp);
|
||||
assert(ticks==uint64_t(w._profiles.listenUs(w._active_profile,
|
||||
w.profilePreamble(w._profiles.slowerProfile())))*512/15625);
|
||||
w._cw_active=true; w.serviceProfileScan();
|
||||
assert(w._profile_idle_visit_ticks==0);
|
||||
w._cw_active=false; w.serviceProfileScan(); assert(w._profile_idle_visit_ticks);
|
||||
w._profile_retry_at=millis()+1000; w.serviceProfileScan();
|
||||
assert(w._profile_idle_visit_ticks==0);
|
||||
w._profile_retry_at=0; w.serviceProfileScan(); assert(w._profile_idle_visit_ticks);
|
||||
w.busy=true; w.serviceProfileScan(); assert(w._profile_idle_visit_ticks==0);
|
||||
w.busy=false; w.packet=true; w.serviceProfileScan(); assert(w._profile_idle_visit_ticks==0);
|
||||
w.packet=false; w.events.record(); w.serviceProfileScan(); assert(w._profile_idle_visit_ticks==0);
|
||||
w.events.claim(); w.serviceProfileScan(); assert(w._profile_idle_visit_ticks);
|
||||
elapsed_us+=1000000; // hop cannot publish old visit with new timestamp
|
||||
const auto old=w._active_profile; w.serviceProfileScan();
|
||||
assert(w._active_profile!=old && w._profile_idle_visit_ticks==0);
|
||||
w.serviceProfileScan(); assert(w._profile_idle_visit_ticks);
|
||||
w._profiles.setSecondary({},false); w.serviceProfileScan();
|
||||
assert(w._profile_idle_visit_ticks==0);
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
path = Path(folder)
|
||||
(path / "Arduino.h").write_text("#pragma once\n#include <cstdint>\n#include <cmath>\n")
|
||||
(path / "test.cpp").write_text(fixture)
|
||||
result = subprocess.run([os.environ.get("CXX", "g++"), "-std=c++17",
|
||||
"-DNRF52_PLATFORM=1", "-Wall", "-Wextra", "-I", folder,
|
||||
"-I", str(ROOT / "src"), str(path / "test.cpp"), "-o", str(path / "test")],
|
||||
capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
result = subprocess.run([str(path / "test")], capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
||||
def test_integer_admission_and_unsupported_platform(self):
|
||||
source = (ROOT / "src/helpers/radiolib/RadioLibWrappers.cpp").read_text()
|
||||
method = body(source, "bool RadioLibWrapper::getProfileIdleDelay(")
|
||||
# Keep the final admission free of SPI, float work and clock mutation.
|
||||
for forbidden in ("profilePreamble(", "listenUs(", "profileTimestamp(",
|
||||
"isChipBusy(", "isPacketPendingOrReceiving("):
|
||||
self.assertNotIn(forbidden, method)
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
path = Path(folder)
|
||||
(path / "Arduino.h").write_text("#pragma once\n#include <cstdint>\n#include <cmath>\n")
|
||||
(path / "test.cpp").write_text(HARNESS.replace("@METHOD@", method))
|
||||
for defines in ([], ["-DNRF52_PLATFORM=1"]):
|
||||
result = subprocess.run([os.environ.get("CXX", "g++"), "-std=c++17",
|
||||
"-Wall", "-Wextra", "-Werror", *defines, "-I", folder,
|
||||
"-I", str(ROOT / "src"), str(path / "test.cpp"), "-o", str(path / "test")],
|
||||
capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
result = subprocess.run([str(path / "test")], capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
||||
def test_late_preparation_precedes_all_guards_and_final_recheck(self):
|
||||
for role in ("companion_radio", "simple_repeater", "simple_room_server", "simple_sensor"):
|
||||
source = (ROOT / "examples" / role / "main.cpp").read_text()
|
||||
loop = body(source, "\nvoid loop()")
|
||||
prepare = loop.index("board.prepareProfileIdleCycle()")
|
||||
predicate = loop.index("sleep_reason =" if role == "companion_radio" else "const auto sleep_guard")
|
||||
self.assertLess(prepare, predicate, role)
|
||||
final = loop.index("the_mesh.canIdleDualRadio()", predicate)
|
||||
sleep = loop.index("board.sleep", final)
|
||||
self.assertLess(final, sleep, role)
|
||||
self.assertNotIn("prepareProfileIdleCycle", loop[predicate:], role)
|
||||
if role == "simple_sensor":
|
||||
arm = loop.index("armTrackerSleepWake(", predicate)
|
||||
timer_recheck = loop.index("the_mesh.canIdleDualRadio()", arm)
|
||||
self.assertLess(timer_recheck, loop.index("board.sleep", arm))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,195 @@
|
||||
"""Execute production scanner-idle admission and Dispatcher wake deadlines."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_radio_receive_contract import method
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
HARNESS = r'''
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <initializer_list>
|
||||
#include <helpers/AGCResetScheduler.h>
|
||||
#define MESH_ENABLE_FOREIGN_RADIO 1
|
||||
#define MESH_ENABLE_TX_BEACONS 1
|
||||
struct Profiles { bool active = false; bool enabled() const { return active; } };
|
||||
struct Radio {
|
||||
Profiles profile;
|
||||
bool receive_retry = false;
|
||||
uint32_t receive_delay = 0, recovery = 0;
|
||||
Profiles* profiles() { return &profile; }
|
||||
bool getReceiveRetryDelay(uint32_t, uint32_t& out) const {
|
||||
out = receive_delay; return receive_retry;
|
||||
}
|
||||
uint32_t getRecoveryCount() const { return recovery; }
|
||||
@DEFAULT@
|
||||
};
|
||||
struct Scanner : Radio {
|
||||
bool supported = false;
|
||||
uint32_t delay = 0;
|
||||
mutable unsigned reads = 0;
|
||||
bool getProfileIdleDelay(uint32_t& out) const override {
|
||||
++reads; out = delay; return supported;
|
||||
}
|
||||
};
|
||||
struct Clock { uint32_t now = 1000; uint32_t getMillis() const { return now; } };
|
||||
struct Manager {
|
||||
bool tx = false, rx = false;
|
||||
uint32_t tx_at = 0, rx_at = 0;
|
||||
bool getNextOutboundTime(uint32_t, uint32_t& out) const { out = tx_at; return tx; }
|
||||
bool getNextInboundTime(uint32_t, uint32_t& out) const { out = rx_at; return rx; }
|
||||
};
|
||||
struct Dispatcher {
|
||||
Radio* _radio;
|
||||
Clock* _ms;
|
||||
Manager* _mgr;
|
||||
bool radio_available = true, foreign = false, beacon = false;
|
||||
bool beacon_wake_enabled = false, adaptive = false;
|
||||
uint32_t beacon_wake_at = 0, next_tx_time = 0;
|
||||
void* outbound = nullptr;
|
||||
bool outbound_radio_retry_pending = false;
|
||||
uint32_t outbound_radio_retry_at = 0, outbound_restore_retry_at = 0;
|
||||
uint8_t outbound_restore_cr = 0;
|
||||
mesh::AGCResetScheduler agc_reset;
|
||||
Dispatcher(Radio* radio, Clock* clock, Manager* manager)
|
||||
: _radio(radio), _ms(clock), _mgr(manager) {}
|
||||
bool isDualRadioActive() const { return _radio->profiles()->enabled(); }
|
||||
bool hasForeignRadioFrame() const { return foreign; }
|
||||
bool hasBeaconRadioFrame() const { return beacon; }
|
||||
bool useAdaptiveAGC() const { return adaptive; }
|
||||
@ADMISSION@
|
||||
bool getNextQueueWakeDelay(uint32_t&) const;
|
||||
};
|
||||
@DEADLINES@
|
||||
int main() {
|
||||
Radio unknown;
|
||||
uint32_t unchanged = 71;
|
||||
assert(!unknown.getProfileIdleDelay(unchanged) && unchanged == 71);
|
||||
Clock clock;
|
||||
Manager manager;
|
||||
Dispatcher legacy{&unknown, &clock, &manager};
|
||||
assert(legacy.canIdleDualRadio());
|
||||
unknown.profile.active = true;
|
||||
assert(!legacy.canIdleDualRadio());
|
||||
assert(legacy.getNextQueueWakeDelay(unchanged) && unchanged == 0);
|
||||
|
||||
for (uint32_t now : {1000U, UINT32_MAX - 15U}) {
|
||||
clock.now = now;
|
||||
manager = {};
|
||||
Scanner radio;
|
||||
Dispatcher node{&radio, &clock, &manager};
|
||||
uint32_t delay = 99;
|
||||
assert(node.canIdleDualRadio() && radio.reads == 0);
|
||||
assert(!node.getNextQueueWakeDelay(delay) && radio.reads == 0);
|
||||
radio.profile.active = true;
|
||||
radio.delay = 37;
|
||||
assert(!node.canIdleDualRadio()); // Unknown ports keep their polling contract.
|
||||
assert(node.getNextQueueWakeDelay(delay) && delay == 0);
|
||||
radio.supported = true;
|
||||
radio.delay = 0;
|
||||
assert(!node.canIdleDualRadio()); // A due/too-short visit is not an idle budget.
|
||||
assert(node.getNextQueueWakeDelay(delay) && delay == 0);
|
||||
radio.delay = 37;
|
||||
assert(node.canIdleDualRadio());
|
||||
assert(node.getNextQueueWakeDelay(delay) && delay == 37);
|
||||
node.radio_available = false;
|
||||
assert(!node.canIdleDualRadio());
|
||||
assert(node.getNextQueueWakeDelay(delay) && delay == 0);
|
||||
node.radio_available = true;
|
||||
|
||||
// Scanner admission cannot hide any previously actionable owner/deadline.
|
||||
node.foreign = true;
|
||||
assert(node.getNextQueueWakeDelay(delay) && delay == 0);
|
||||
node.foreign = false;
|
||||
node.beacon = true;
|
||||
assert(node.getNextQueueWakeDelay(delay) && delay == 0);
|
||||
node.beacon = false;
|
||||
node.beacon_wake_enabled = true;
|
||||
node.beacon_wake_at = now + 50;
|
||||
assert(node.getNextQueueWakeDelay(delay) && delay == 37);
|
||||
node.beacon_wake_at = now + 4;
|
||||
assert(node.getNextQueueWakeDelay(delay) && delay == 4);
|
||||
node.beacon_wake_at = now;
|
||||
assert(node.getNextQueueWakeDelay(delay) && delay == 0);
|
||||
node.beacon_wake_enabled = false;
|
||||
|
||||
radio.receive_retry = true; radio.receive_delay = 7;
|
||||
assert(node.getNextQueueWakeDelay(delay) && delay == 7);
|
||||
radio.receive_delay = 50;
|
||||
assert(node.getNextQueueWakeDelay(delay) && delay == 37);
|
||||
radio.receive_retry = false;
|
||||
node.outbound_restore_cr = 5;
|
||||
node.outbound_restore_retry_at = now + 2;
|
||||
assert(node.getNextQueueWakeDelay(delay) && delay == 2);
|
||||
node.outbound_restore_cr = 0;
|
||||
|
||||
manager.tx = true; manager.tx_at = now + 5;
|
||||
node.next_tx_time = now + 12;
|
||||
assert(node.getNextQueueWakeDelay(delay) && delay == 12);
|
||||
manager.tx_at = now + 50;
|
||||
assert(node.getNextQueueWakeDelay(delay) && delay == 37);
|
||||
manager.rx = true; manager.rx_at = now + 3;
|
||||
assert(node.getNextQueueWakeDelay(delay) && delay == 3);
|
||||
manager.rx_at = now;
|
||||
assert(node.getNextQueueWakeDelay(delay) && delay == 0);
|
||||
manager = {};
|
||||
|
||||
node.adaptive = true;
|
||||
node.agc_reset.configure(now, 8, true, radio.recovery);
|
||||
assert(node.getNextQueueWakeDelay(delay) && delay == 8);
|
||||
node.agc_reset.configure(now, 80, true, radio.recovery);
|
||||
assert(node.getNextQueueWakeDelay(delay) && delay == 37);
|
||||
++radio.recovery;
|
||||
assert(node.getNextQueueWakeDelay(delay) && delay == 0);
|
||||
node.adaptive = false;
|
||||
|
||||
node.outbound = &node;
|
||||
assert(node.getNextQueueWakeDelay(delay) && delay == 0);
|
||||
node.outbound_radio_retry_pending = true;
|
||||
node.outbound_radio_retry_at = now + 200;
|
||||
assert(node.getNextQueueWakeDelay(delay) && delay == 37);
|
||||
radio.delay = 400;
|
||||
assert(node.getNextQueueWakeDelay(delay) && delay == 200);
|
||||
radio.receive_retry = true; radio.receive_delay = 6;
|
||||
assert(node.getNextQueueWakeDelay(delay) && delay == 6);
|
||||
radio.delay = 3;
|
||||
assert(node.getNextQueueWakeDelay(delay) && delay == 3);
|
||||
node.outbound_radio_retry_at = now;
|
||||
assert(node.getNextQueueWakeDelay(delay) && delay == 0);
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class RadioProfileIdleDispatcherTest(unittest.TestCase):
|
||||
def test_actual_default_admission_and_all_dispatcher_deadlines(self):
|
||||
header = (ROOT / 'src/Dispatcher.h').read_text(encoding='ascii')
|
||||
source = (ROOT / 'src/Dispatcher.cpp').read_text(encoding='ascii')
|
||||
code = HARNESS.replace('@DEFAULT@', method(header, 'virtual bool getProfileIdleDelay('))
|
||||
code = code.replace('@ADMISSION@', method(header, 'bool canIdleDualRadio() const'))
|
||||
code = code.replace('@DEADLINES@', method(source, 'bool Dispatcher::getNextQueueWakeDelay('))
|
||||
with tempfile.TemporaryDirectory(prefix='meshcore-profile-idle-dispatcher-') as directory:
|
||||
cpp, binary = Path(directory) / 'test.cpp', Path(directory) / 'test'
|
||||
cpp.write_text(code, encoding='ascii')
|
||||
flags = ['-std=c++17', '-Wall', '-Wextra', '-Werror']
|
||||
if os.name != 'nt':
|
||||
flags += ['-fsanitize=address,undefined', '-fno-sanitize-recover=all',
|
||||
'-fno-pie', '-no-pie']
|
||||
result = subprocess.run([
|
||||
os.environ.get('CXX', 'c++'), *flags, '-I', str(ROOT / 'src'),
|
||||
str(cpp), '-o', str(binary),
|
||||
], capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=20)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -154,6 +154,7 @@ struct MyMesh : Callbacks {
|
||||
unsigned long dirty_contacts_expiry = 0, next_recent_repeater_sweep = 0;
|
||||
unsigned long next_battery_alert_check = 0;
|
||||
bool isDualRadioActive() const { return false; }
|
||||
bool canIdleDualRadio() const { return true; }
|
||||
bool hasPendingOtaApply() const { return false; }
|
||||
bool hasQueuedWorkDue() const { return false; }
|
||||
bool hasRetryWorkDue() const { return false; }
|
||||
|
||||
@@ -54,6 +54,7 @@ public:
|
||||
const Clock* getRTCClock()const{return &rtc;}
|
||||
bool millisHasNowPassed(unsigned long time)const{return (int32_t)(millis()-time)>=0;}
|
||||
bool isDualRadioActive()const{return dual;}
|
||||
bool canIdleDualRadio()const{return !dual;}
|
||||
bool hasPendingOtaApply()const{return ota;}
|
||||
bool isAnyTempRadioActive()const{return temp;}
|
||||
bool hasOutbound()const{return outbound;}
|
||||
@@ -160,6 +161,7 @@ static void trackerWakeCallback(TimerHandle_t){}
|
||||
@ARM@
|
||||
struct Mesh {bool tracker=true;uint32_t sleep_secs=30;
|
||||
bool isTrackerModeEnabled()const{return tracker;}
|
||||
bool canIdleDualRadio()const{return true;}
|
||||
uint32_t getPowerSaveSleepMilliseconds(uint32_t max)const{assert(max==30000);return sleep_secs*1000;}
|
||||
bool getNextNoiseFloorWakeDelay(uint32_t&)const{return false;}
|
||||
bool millisHasNowPassed(uint32_t deadline)const{return (int32_t)(millis()-deadline)>=0;}
|
||||
|
||||
Reference in new issue
Block a user