Separate tracker RF leases from timed maintenance wakes

This commit is contained in:
mikecarper committed 2026-10-16 04:03:34 -07:00
1 parent 9bb63527ce
commit 92b9a1cc64
6 files changed
+433 -1

No files matched your search

+8
View File
@@ -1731,6 +1731,14 @@ uint32_t SensorMesh::getPowerSaveSleepSeconds(uint32_t max_secs) const {
uint32_t wake_delay_secs = mesh::power::powerSaveSleepSeconds(wake_delay_ms);
if (wake_delay_secs < sleep_secs) sleep_secs = wake_delay_secs;
}
#if MESH_ENABLE_SENSOR_TRACKER && MESH_ENABLE_TX_BEACONS
// The normal queue query omits RF maintenance while radio_available is
// false. A tracker's deliberate RF sleep must not hide its next beacon.
if (tracker.enabled && getNextBeaconWakeDelay(wake_delay_ms)) {
uint32_t wake_delay_secs = mesh::power::powerSaveSleepSeconds(wake_delay_ms);
if (wake_delay_secs < sleep_secs) sleep_secs = wake_delay_secs;
}
#endif
if (getNextRetryWakeDelay(wake_delay_ms)) {
uint32_t wake_delay_secs = mesh::power::powerSaveSleepSeconds(wake_delay_ms);
if (wake_delay_secs < sleep_secs) sleep_secs = wake_delay_secs;
+4 -1
View File
@@ -86,7 +86,10 @@ bool SensorMesh::trackerNeedsRadio() const {
|| _cli.hasActiveUserGpioTimer() || hasOutbound() || _mgr->getOutboundTotal() != 0)
return true;
uint32_t delay;
if (getNextQueueWakeDelay(delay) || getNextRetryWakeDelay(delay)) return true;
if (hasPendingRadioWork() || getNextRetryWakeDelay(delay)) return true;
#if MESH_ENABLE_TX_BEACONS
if (getNextBeaconWakeDelay(delay) && delay == 0) return true;
#endif
if (tracker_pending || tracker_acquiring
|| (tracker_awake_until && !millisHasNowPassed(tracker_awake_until))) return true;
return !tracker_storage_fault && getRTCClock()->getCurrentTime() >= tracker_next_check;
+21
View File
@@ -577,6 +577,27 @@ protected:
// This includes queue schedules, delayed inbound processing, airtime-budget
// waits, and short channel-busy deferrals.
bool getNextQueueWakeDelay(uint32_t& delay_millis) const;
// Timed MCU wakes are not RF leases: an idle tracker can suspend the radio
// across AGC maintenance and a future beacon, but not actual packet work.
bool hasPendingRadioWork() const {
if (hasOutbound() || outbound_restore_cr || _mgr->getOutboundTotal() != 0
|| _radio->isCarrierWaveActive() || _radio->isMorseTxActive()
|| _radio->isMorseRxActive()) return true;
uint32_t ignored;
return _mgr->getNextInboundTime(_ms->getMillis(), ignored)
|| (radio_available && _radio->getReceiveRetryDelay(_ms->getMillis(), ignored));
}
#if MESH_ENABLE_TX_BEACONS
// A deliberately suspended tracker must still wake before the beacon CLI
// consumes its one admission attempt. Other unavailable radios retain the
// existing getNextQueueWakeDelay() policy.
bool getNextBeaconWakeDelay(uint32_t& delay_millis) const {
if (!beacon_wake_enabled) return false;
const int32_t wait = int32_t(beacon_wake_at - uint32_t(_ms->getMillis()));
delay_millis = wait > 0 ? uint32_t(wait) : 0;
return true;
}
#endif
bool hasQueuedWorkDue() const {
uint32_t delay_millis;
return getNextQueueWakeDelay(delay_millis) && delay_millis == 0;
+2
View File
@@ -199,6 +199,8 @@ public:
bool hasPendingOtaApply() const { return pending_ota; }
bool isDualRadioActive() const { return dual; }
bool getNextQueueWakeDelay(uint32_t& value) const { value = 1; return queue_wake; }
bool hasPendingRadioWork() const { return queue_wake; }
bool getNextBeaconWakeDelay(uint32_t&) const { return false; }
bool getNextRetryWakeDelay(uint32_t& value) const { value = 1; return retry_wake; }
void setRadioAvailable(bool value) { available = value; }
void updateAdvertTimer() { ++advert_timer_updates; }
+310
View File
@@ -0,0 +1,310 @@
// Execute tracker RF ownership against the real Dispatcher and packet manager.
// Only the radio/clock and CLI beacon schedule are hardware-boundary adapters.
#include <Dispatcher.h>
#include <Arduino.h>
#include <helpers/StaticPoolPacketManager.h>
#include <helpers/PowerManagementUtils.h>
#include <helpers/MorseLoopPolicy.h>
#include <cassert>
#include <cstdio>
#include <cstring>
#include <initializer_list>
constexpr uint32_t SENSOR_READ_INTERVAL_SECS = 60;
namespace mesh {
bool isUsbLoggingWatchdogArmed() { return false; }
class Mesh : public Dispatcher {
public:
Mesh(Radio& radio, MillisecondClock& clock, PacketManager& manager)
: Dispatcher(radio, clock, manager) {}
void loop() { Dispatcher::loop(); }
};
}
struct Clock : mesh::MillisecondClock {
uint32_t now;
explicit Clock(uint32_t initial = 1000) : now(initial) {}
unsigned long getMillis() override { return now; }
};
struct RTC { uint32_t now = 100; uint32_t getCurrentTime() const { return now; } };
struct Board {
bool isUsbDataConnected() const { return false; }
bool isOTAUpdateRunning() const { return false; }
bool isRadioTestActive() const { return false; }
};
struct Radio : mesh::Radio {
mesh::RadioProfiles profiles_state;
bool asleep = false, sending = false, complete = false, beacon_owned = false;
bool receive_retry = false, carrier = false, morse_tx = false, morse_rx = false;
unsigned starts = 0, wakes = 0, sleeps = 0, agc = 0, beacon_sends = 0;
mesh::RadioParamApplyResult restore_cr = mesh::RadioParamApplyResult::APPLIED;
Radio() {
auto& p = profiles_state.primary;
p.freq = 906.5; p.bw = 62.5; p.sf = 7; p.cr = 5; p.preamble = 32;
}
mesh::RadioProfiles* profiles() override { return &profiles_state; }
const mesh::RadioProfiles* profiles() const override { return &profiles_state; }
void begin() override { assert(!asleep); ++starts; }
int recvRaw(uint8_t*, int) override { return 0; }
float packetScore(float, int) override { return 1; }
uint32_t getEstAirtimeFor(int) override { return 100; }
bool startSendRaw(const uint8_t*, int) override {
assert(!asleep); sending = true; complete = false;
if (beacon_owned) ++beacon_sends;
return true;
}
bool isSendComplete() override { return complete; }
void onSendFinished() override { sending = false; }
bool isInRecvMode() const override { return !asleep && !sending; }
bool isReceiving() override { return false; }
bool isCarrierWaveActive() const override { return carrier; }
bool isMorseTxActive() const override { return morse_tx; }
bool isMorseRxActive() const override { return morse_rx; }
bool setTrackerSleep(bool sleeping) override {
assert(!sending && !beacon_owned);
asleep = sleeping; sleeping ? ++sleeps : ++wakes; return true;
}
bool recoverRadio(bool) override { return true; }
bool isWatchdogObserving() const { return false; }
bool getReceiveRetryDelay(uint32_t, uint32_t& delay) const override {
delay = 2000; return receive_retry;
}
bool setCodingRate(uint8_t) override { return true; }
mesh::RadioParamApplyResult tryRestoreCodingRate(uint8_t) override { return restore_cr; }
void resetAGC() override { assert(!asleep && !beacon_owned); ++agc; }
bool tryResetAGC() override { resetAGC(); return true; }
bool validateProfile(const mesh::RadioProfileParams& params) const override {
return mesh::RadioProfiles::valid(params);
}
bool supportsMeshtext() const override { return true; }
#if MESH_ENABLE_TX_BEACONS
uint32_t getBeaconAirtime(const mesh::RadioProfileParams&, int) override { return 100; }
mesh::RadioParamApplyResult prepareBeaconTransmit(const mesh::RadioProfileParams&, int8_t) override {
beacon_owned = true; return mesh::RadioParamApplyResult::APPLIED;
}
bool isBeaconRadioOwned() const override { return beacon_owned; }
bool isBeaconChannelBusy() override { return false; }
mesh::RadioParamApplyResult restoreBeaconReceive() override {
beacon_owned = false; return mesh::RadioParamApplyResult::APPLIED;
}
#endif
};
class SensorMesh;
struct CLI {
SensorMesh* owner = nullptr;
bool scheduled = false;
unsigned attempts = 0;
bool hasActiveUserGpioTimer() const { return false; }
void loop();
};
class SensorMesh : public mesh::Mesh {
public:
Clock& clock;
Radio& radio_driver;
Board board;
RTC rtc;
CLI _cli{this};
struct { bool enabled = true; } tracker;
struct { uint8_t powersaving_enabled = 1; } prefs;
bool tracker_pending = false, tracker_acquiring = false, tracker_storage_fault = false;
bool tracker_radio_asleep = false, saved_radio_apply_pending = false, temp_radio_applied = false;
unsigned long tracker_awake_until = 0, set_radio_at = 0, revert_radio_at = 0;
unsigned long next_flood_advert = 0, next_local_advert = 0, dirty_contacts_expiry = 0;
unsigned long radio_apply_retry_at = 0;
uint32_t tracker_next_check = 1900, last_read_time = 100;
struct Alert { unsigned long send_expiry = 0; } alert;
Alert* alert_tasks[1] = {&alert};
int num_alert_tasks = 0, interval = 4000;
bool retry = false;
SensorMesh(Radio& radio, Clock& time, StaticPoolPacketManager& manager)
: mesh::Mesh(radio, time, manager), clock(time), radio_driver(radio) { begin(); }
RTC* getRTCClock() const { return const_cast<RTC*>(&rtc); }
bool millisHasNowPassed(unsigned long timestamp) const {
return int32_t(clock.now - uint32_t(timestamp)) >= 0;
}
bool isAnyTempRadioActive() const { return false; }
bool hasPendingOtaApply() const { return false; }
bool getNextRetryWakeDelay(uint32_t& delay) const { delay = 1500; return retry; }
bool hasRetryWorkDue() const { return false; }
int getAGCResetInterval() const override { return interval; }
bool useAdaptiveAGC() const override { return prefs.powersaving_enabled != 0; }
mesh::DispatcherAction onRecvPacket(mesh::Packet*) override { return ACTION_RELEASE; }
using mesh::Dispatcher::getNextQueueWakeDelay;
using mesh::Dispatcher::hasPendingRadioWork;
using mesh::Dispatcher::setRadioAvailable;
using mesh::Dispatcher::isRadioAvailable;
#if MESH_ENABLE_TX_BEACONS
using mesh::Dispatcher::getNextBeaconWakeDelay;
#endif
#if MESH_ENABLE_FOREIGN_RADIO
using mesh::Dispatcher::queueForeignRadioFrame;
#endif
bool trackerNeedsRadio() const;
void wakeTrackerRadio();
bool isMillisTimerDue(unsigned long) const;
uint32_t limitSleepToMillisTimer(unsigned long, uint32_t) const;
bool hasPendingWork() const;
uint32_t getPowerSaveSleepSeconds(uint32_t) const;
void loopBeforeService();
void sleepIfIdle() { @SLEEP_BLOCK@ }
};
@METHODS@
void CLI::loop() {
#if MESH_ENABLE_TX_BEACONS
uint32_t delay;
if (scheduled && owner->getNextBeaconWakeDelay(delay) && delay == 0) {
// BeaconManager consumes one admission attempt, even if the radio is
// unavailable. Exercise that same boundary with the real queue API.
++attempts;
const uint8_t bytes[] = {0x15, 0, 0x23};
const auto params = owner->radio_driver.profiles_state.primary;
assert(owner->queueBeaconRadioFrame(bytes, sizeof(bytes), params, 14)
&& "due beacon must wake RF before the CLI admission attempt");
owner->setBeaconWakeDelay(owner->clock.now, 86400000, true);
}
#endif
}
struct Fixture {
Clock clock;
Radio radio;
StaticPoolPacketManager manager{8};
SensorMesh sensor{radio, clock, manager};
explicit Fixture(uint32_t initial = 1000) : clock(initial) { g_mock_millis = clock.now; }
void advance(uint32_t elapsed) { clock.now += elapsed; g_mock_millis = clock.now; }
};
static mesh::Packet* packet(Fixture& f) {
auto* p = f.sensor.obtainNewPacket(); assert(p);
p->header = ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_GRP_TXT << PH_TYPE_SHIFT);
p->tx_radio = mesh::RADIO_TX_PRIMARY; p->payload_len = 1; p->payload[0] = 0x12;
return p;
}
static void agc_deadlines() {
for (bool powersaving : {false, true}) {
Fixture f; f.sensor.prefs.powersaving_enabled = powersaving;
f.sensor.mesh::Mesh::loop();
uint32_t delay = 0;
assert(f.sensor.getNextQueueWakeDelay(delay) == powersaving);
if (powersaving) assert(delay == 4000);
assert(!f.sensor.trackerNeedsRadio() && "a future AGC wake is not an RF lease");
f.advance(4001);
assert(!f.sensor.trackerNeedsRadio() && "due AGC is not an RF lease either");
f.sensor.mesh::Mesh::loop(); assert(f.radio.agc == 1);
f.sensor.sleepIfIdle();
assert(f.sensor.tracker_radio_asleep && !f.sensor.isRadioAvailable());
const auto starts = f.radio.starts;
for (unsigned pass = 0; pass < 3; ++pass) {
f.advance(60000);
assert(!f.sensor.trackerNeedsRadio());
assert(!f.sensor.getNextQueueWakeDelay(delay));
assert(f.sensor.getPowerSaveSleepSeconds(30) == 30);
f.sensor.loopBeforeService();
assert(f.sensor.tracker_radio_asleep && f.radio.starts == starts);
}
f.sensor.tracker_next_check = f.sensor.rtc.now;
f.sensor.loopBeforeService();
assert(!f.sensor.tracker_radio_asleep && f.sensor.isRadioAvailable());
assert(f.radio.starts == starts + 1 && f.radio.agc == 1);
if (powersaving) assert(f.sensor.getNextQueueWakeDelay(delay) && delay == 4000);
}
}
static void radio_leases() {
{
Fixture f; auto* p = packet(f);
assert(f.sensor.sendPacket(p, 0, 5000));
assert(f.sensor.trackerNeedsRadio());
f.sensor.sleepIfIdle(); assert(!f.sensor.tracker_radio_asleep);
}
{
Fixture f; auto* p = packet(f);
f.manager.queueInbound(p, f.clock.now + 5000);
assert(!f.manager.getOutboundTotal() && f.sensor.trackerNeedsRadio());
f.sensor.sleepIfIdle(); assert(!f.sensor.tracker_radio_asleep);
}
{
Fixture f; f.radio.receive_retry = true;
assert(f.sensor.trackerNeedsRadio());
f.sensor.sleepIfIdle(); assert(!f.sensor.tracker_radio_asleep);
f.sensor.setRadioAvailable(false); f.sensor.tracker_radio_asleep = true;
assert(!f.sensor.trackerNeedsRadio()); // Stale maintenance cannot wake dormant RF.
}
{
Fixture f; f.sensor.retry = true;
assert(f.sensor.trackerNeedsRadio());
f.sensor.sleepIfIdle(); assert(!f.sensor.tracker_radio_asleep);
}
for (unsigned owner = 0; owner < 3; ++owner) {
Fixture f;
f.radio.carrier = owner == 0; f.radio.morse_tx = owner == 1; f.radio.morse_rx = owner == 2;
assert(f.sensor.trackerNeedsRadio());
f.sensor.sleepIfIdle(); assert(!f.sensor.tracker_radio_asleep);
}
{
Fixture f; auto* p = packet(f); p->tx_cr = 6;
assert(f.sensor.sendPacket(p, 0));
f.advance(1); f.sensor.mesh::Mesh::loop();
assert(f.radio.sending && f.sensor.trackerNeedsRadio());
f.radio.complete = true; f.radio.restore_cr = mesh::RadioParamApplyResult::BUSY;
f.advance(100); f.sensor.mesh::Mesh::loop();
assert(!f.sensor.hasOutbound() && !f.manager.getOutboundTotal());
assert(f.sensor.trackerNeedsRadio() && "pending CR restoration retains the radio lease");
f.sensor.sleepIfIdle(); assert(!f.sensor.tracker_radio_asleep);
f.radio.restore_cr = mesh::RadioParamApplyResult::APPLIED;
f.advance(251); f.sensor.mesh::Mesh::loop();
assert(!f.sensor.trackerNeedsRadio());
}
#if MESH_ENABLE_FOREIGN_RADIO
{
Fixture f; const uint8_t bytes[] = {1, 2, 3};
auto& p = f.radio.profiles_state;
p.secondary.params = p.primary; p.secondary.params.protocol = mesh::RadioProfileProtocol::Meshtext;
p.secondary.mode = mesh::RadioProfileMode::RxTx;
assert(f.sensor.queueForeignRadioFrame(bytes, sizeof(bytes), 1, p.generation[1]));
assert(f.sensor.hasPendingRadioWork());
}
#endif
}
static void beacon_deadlines() {
#if MESH_ENABLE_TX_BEACONS
for (bool wrap : {false, true}) {
Fixture f(wrap ? UINT32_MAX - 1500U : 1000U);
f.sensor._cli.scheduled = true;
f.sensor.setBeaconWakeDelay(f.clock.now, 2500, true);
assert(!f.sensor.trackerNeedsRadio());
f.sensor.sleepIfIdle();
assert(f.sensor.tracker_radio_asleep && !f.sensor.isRadioAvailable());
uint32_t delay = 0;
assert(!f.sensor.getNextQueueWakeDelay(delay)); // Normal unavailable-radio policy is unchanged.
assert(f.sensor.getNextBeaconWakeDelay(delay) && delay == 2500);
#if defined(NRF52_PLATFORM) || defined(RP2040_PLATFORM) || defined(STM32_PLATFORM)
constexpr unsigned sleep = 3;
#else
constexpr unsigned sleep = 2;
#endif
assert(f.sensor.getPowerSaveSleepSeconds(30) == sleep
&& "RF suspension must not hide the next beacon MCU wake");
f.advance(2499); f.sensor.loopBeforeService();
assert(f.sensor.tracker_radio_asleep && f.sensor._cli.attempts == 0);
f.advance(1);
assert(f.sensor.getPowerSaveSleepSeconds(30) == 0);
f.sensor.loopBeforeService();
assert(!f.sensor.tracker_radio_asleep && f.sensor.isRadioAvailable());
assert(f.sensor._cli.attempts == 1 && f.radio.beacon_sends == 1);
assert(f.sensor.trackerNeedsRadio()); f.sensor.sleepIfIdle();
assert(!f.sensor.tracker_radio_asleep);
f.radio.complete = true; f.advance(100); f.sensor.loopBeforeService();
assert(!f.sensor.hasOutbound() && !f.sensor.trackerNeedsRadio());
f.sensor.sleepIfIdle(); assert(f.sensor.tracker_radio_asleep);
f.sensor.setBeaconWakeDelay(f.clock.now, 0, false);
assert(!f.sensor.getNextBeaconWakeDelay(delay));
assert(f.sensor.getPowerSaveSleepSeconds(30) == 30);
}
#endif
}
int main(int argc, char** argv) {
assert(argc == 2);
if (!strcmp(argv[1], "agc")) agc_deadlines();
else if (!strcmp(argv[1], "leases")) radio_leases();
else if (!strcmp(argv[1], "beacons")) beacon_deadlines();
else assert(false);
puts("PASS: tracker radio lease");
}
+88
View File
@@ -1,5 +1,6 @@
"""Execute Sensor tracker sleep gates, real wake arming and radio handoff."""
from pathlib import Path
import os
import re
import shutil
import subprocess
@@ -9,6 +10,7 @@ import unittest
from test_gps_upstream_adaptations import ARDUINO
from test_replay_reset_integration import extract_braced
from cpp_source import body
ROOT = Path(__file__).resolve().parents[1]
@@ -57,6 +59,7 @@ public:
bool hasQueuedWorkDue()const{return queue_due;}
bool hasRetryWorkDue()const{return retry_due;}
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 isMillisTimerDue(unsigned long timestamp)const;
uint32_t limitSleepToMillisTimer(unsigned long timestamp,uint32_t sleep_secs)const;
@@ -308,6 +311,91 @@ class SensorTrackerSleepTest(unittest.TestCase):
with self.subTest(defines=defines):
self.compile_run(source, defines)
def cross_layer_source(self, mutation=None):
sensor = (ROOT / "examples/simple_sensor/SensorMesh.cpp").read_text(encoding="ascii")
tracker = (ROOT / "examples/simple_sensor/Tracker.cpp").read_text(encoding="ascii")
if mutation == "old-radio-lease":
lease = " if (hasPendingRadioWork() || getNextRetryWakeDelay(delay)) return true;"
self.assertEqual(tracker.count(lease), 1)
tracker = tracker.replace(lease,
" if (getNextQueueWakeDelay(delay) || getNextRetryWakeDelay(delay)) return true;", 1)
elif mutation == "no-beacon-radio-wake":
due = " if (getNextBeaconWakeDelay(delay) && delay == 0) return true;"
self.assertEqual(tracker.count(due), 1)
tracker = tracker.replace(due, "", 1)
elif mutation == "no-beacon-mcu-wake":
start = "#if MESH_ENABLE_SENSOR_TRACKER && MESH_ENABLE_TX_BEACONS\n"
self.assertEqual(sensor.count(start), 1)
begin = sensor.index(start)
end = sensor.index("#endif", begin) + len("#endif")
sensor = sensor[:begin] + sensor[end:]
methods = "\n".join(body(sensor, signature) for signature in (
"bool SensorMesh::isMillisTimerDue(", "uint32_t SensorMesh::limitSleepToMillisTimer(",
"bool SensorMesh::hasPendingWork(", "uint32_t SensorMesh::getPowerSaveSleepSeconds("))
methods += "\n" + "\n".join(body(tracker, signature) for signature in (
"bool SensorMesh::trackerNeedsRadio(", "void SensorMesh::wakeTrackerRadio("))
# Execute the actual wake-before-CLI order. Application sensor work
# after Mesh::loop has separate tracker client/GPS regression coverage.
loop = body(sensor, "void SensorMesh::loop()")
end = " mesh::Mesh::loop();"
self.assertEqual(loop.count(end), 1)
methods += "\n" + loop[:loop.index(end) + len(end)].replace(
"void SensorMesh::loop()", "void SensorMesh::loopBeforeService()", 1) + "\n}\n"
source = (ROOT / "test/fixtures/sensor_tracker_radio_lease.cpp").read_text(encoding="ascii")
return source.replace("@METHODS@", methods).replace("@SLEEP_BLOCK@", body(
tracker, "if (!trackerNeedsRadio() && !radio_driver.isWatchdogObserving()"))
def run_cross_layer(self, defines, scenarios=("agc", "leases", "beacons"), mutation=None):
with tempfile.TemporaryDirectory(prefix="meshcore-tracker-radio-lease-") as directory:
path = Path(directory)
source, binary = path / "lease.cpp", path / "lease"
source.write_text(self.cross_layer_source(mutation), encoding="ascii")
result = subprocess.run([
self.compiler, "-std=c++17", "-O1", "-g", "-Wall", "-Wextra",
"-Wno-unused-parameter", "-DMESH_ENABLE_SENSOR_TRACKER=1", *defines,
*(["-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]
if sys.platform.startswith("linux") else []),
"-I" + str(ROOT / "test/mocks"), "-I" + str(ROOT / "src"),
str(ROOT / "src/Dispatcher.cpp"), str(ROOT / "src/DispatcherBeacon.cpp"),
str(ROOT / "src/Packet.cpp"), str(ROOT / "src/helpers/StaticPoolPacketManager.cpp"),
str(source), "-o", str(binary),
], capture_output=True, text=True, timeout=90)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
env = dict(os.environ, ASAN_OPTIONS="detect_leaks=0")
for scenario in scenarios:
result = subprocess.run([str(binary), scenario], capture_output=True,
text=True, env=env, timeout=20)
if mutation:
self.assertNotEqual(result.returncode, 0, "old behavior escaped cross-layer regression")
self.assertIn("Assertion", result.stderr)
self.assertIn({
"old-radio-lease": "a future AGC wake is not an RF lease",
"no-beacon-radio-wake": "due beacon must wake RF before the CLI admission attempt",
"no-beacon-mcu-wake": "RF suspension must not hide the next beacon MCU wake",
}[mutation], result.stderr)
else:
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn("PASS: tracker radio lease", result.stdout)
def test_real_dispatcher_distinguishes_rf_leases_from_mcu_deadlines(self):
for beacons, foreign, platform in ((0, 0, ()), (1, 0, ()), (1, 1, ()),
(1, 0, ("-DNRF52_PLATFORM=1",))):
with self.subTest(beacons=beacons, foreign=foreign, platform=platform):
self.run_cross_layer((f"-DMESH_ENABLE_TX_BEACONS={beacons}",
f"-DMESH_ENABLE_MESHTEXT_CHAT={foreign}", *platform))
def test_negative_control_agc_deadline_is_not_a_tracker_rf_lease(self):
self.run_cross_layer(("-DMESH_ENABLE_TX_BEACONS=0", "-DMESH_ENABLE_MESHTEXT_CHAT=0"),
("agc",), "old-radio-lease")
def test_negative_control_due_beacon_wakes_radio_before_cli(self):
self.run_cross_layer(("-DMESH_ENABLE_TX_BEACONS=1", "-DMESH_ENABLE_MESHTEXT_CHAT=0"),
("beacons",), "no-beacon-radio-wake")
def test_negative_control_suspended_radio_keeps_beacon_mcu_deadline(self):
self.run_cross_layer(("-DMESH_ENABLE_TX_BEACONS=1", "-DMESH_ENABLE_MESHTEXT_CHAT=0"),
("beacons",), "no-beacon-mcu-wake")
if __name__ == "__main__":
unittest.main()