Separate tracker RF leases from timed maintenance wakes
This commit is contained in:
1 parent
9bb63527ce
commit
92b9a1cc64
6 files changed
+433
-1
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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");
|
||||
}
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user