494 lines
25 KiB
Python
494 lines
25 KiB
Python
"""Execute Sensor tracker sleep gates, real wake arming and radio handoff."""
|
|
from pathlib import Path
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
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]
|
|
|
|
DEADLINES = r'''
|
|
#include <Arduino.h>
|
|
#include <cassert>
|
|
#include <helpers/PowerManagementUtils.h>
|
|
#include <helpers/PowerSavingDiagnostics.h>
|
|
#define SENSOR_READ_INTERVAL_SECS 60
|
|
bool logging=false;
|
|
namespace mesh {bool isUsbLoggingWatchdogArmed(){return logging;}}
|
|
struct Board {bool usb=false,ota=false,test=false;
|
|
bool isUsbDataConnected()const{return usb;}
|
|
bool isOTAUpdateRunning()const{return ota;}
|
|
bool isRadioTestActive()const{return test;}
|
|
};
|
|
struct Radio {bool watchdog=false,calibration=false;
|
|
bool isWatchdogObserving()const{return watchdog;}
|
|
bool isCalibratingNoiseFloor()const{return calibration;}
|
|
} radio_driver;
|
|
struct Cli {bool gpio=false;bool hasActiveUserGpioTimer()const{return gpio;}};
|
|
struct Manager {int queued=0;int getOutboundTotal()const{return queued;}};
|
|
struct Clock {uint32_t now=100;uint32_t getCurrentTime()const{return now;}};
|
|
class SensorMesh {
|
|
public:
|
|
Board board;Cli _cli;Manager manager;Manager* _mgr=&manager;Clock rtc;
|
|
#if MESH_ENABLE_SENSOR_TRACKER
|
|
struct {bool enabled=true;} tracker;
|
|
bool tracker_pending=false,tracker_acquiring=false,tracker_storage_fault=false;
|
|
unsigned long tracker_awake_until=0;uint32_t tracker_next_check=1900;
|
|
#endif
|
|
bool dual=false,ota=false,temp=false,outbound=false;
|
|
bool queue_due=false,retry_due=false,queue_delay_set=false,retry_delay_set=false;
|
|
uint32_t queue_delay=0,retry_delay=0;
|
|
unsigned long next_flood_advert=0,next_local_advert=0,dirty_contacts_expiry=0;
|
|
unsigned long radio_apply_retry_at=0,set_radio_at=0,revert_radio_at=0;
|
|
bool saved_radio_apply_pending=false,temp_radio_applied=false;
|
|
uint32_t last_read_time=100;
|
|
struct Alert {unsigned long send_expiry=0;} alert;
|
|
Alert* alert_tasks[1]={&alert};int num_alert_tasks=0;
|
|
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;}
|
|
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 getNextNoiseFloorWakeDelay(uint32_t& out)const{out=0;return radio_driver.calibration;}
|
|
bool isMillisTimerDue(unsigned long timestamp)const;
|
|
uint32_t limitSleepToMillisTimer(unsigned long timestamp,uint32_t sleep_secs)const;
|
|
bool hasPendingWork()const;
|
|
mesh::powerdiag::Mask pendingWorkReason()const;
|
|
uint32_t getPowerSaveSleepMilliseconds(uint32_t max_ms)const;
|
|
uint32_t getPowerSaveSleepSeconds(uint32_t max_secs)const;
|
|
#if MESH_ENABLE_SENSOR_TRACKER
|
|
bool trackerNeedsRadio()const;
|
|
#endif
|
|
};
|
|
@METHODS@
|
|
int main(){
|
|
now_ms=1000;SensorMesh s;
|
|
assert(!s.hasPendingWork()&&s.getPowerSaveSleepSeconds(30)==30);
|
|
#if MESH_ENABLE_SENSOR_TRACKER
|
|
assert(!s.trackerNeedsRadio());
|
|
s.tracker_next_check=107;assert(s.getPowerSaveSleepSeconds(30)==7);
|
|
s.tracker_next_check=100;assert(s.hasPendingWork()&&s.trackerNeedsRadio());
|
|
assert(s.getPowerSaveSleepSeconds(30)==0);s.tracker_next_check=1900;
|
|
for(bool* pending:{&s.tracker_pending,&s.tracker_acquiring}){
|
|
*pending=true;assert(s.hasPendingWork()&&s.trackerNeedsRadio());
|
|
assert(s.getPowerSaveSleepSeconds(30)==0);*pending=false;
|
|
}
|
|
s.tracker_awake_until=1001;assert(s.hasPendingWork()&&s.trackerNeedsRadio());
|
|
++now_ms;assert(!s.hasPendingWork()&&!s.trackerNeedsRadio());
|
|
s.tracker_awake_until=0;s.tracker_storage_fault=true;s.tracker_next_check=0;
|
|
assert(!s.hasPendingWork()&&!s.trackerNeedsRadio());
|
|
s.tracker_storage_fault=false;s.tracker_next_check=1900;
|
|
// All standard radio/console/update owners veto RF sleep.
|
|
for(bool* busy:{&s.board.usb,&s.board.ota,&s.board.test,&logging,&s.temp,&s.ota,
|
|
&s.dual,&s._cli.gpio,&s.outbound}){
|
|
*busy=true;assert(s.trackerNeedsRadio());*busy=false;
|
|
}
|
|
s.manager.queued=1;assert(s.trackerNeedsRadio());s.manager.queued=0;
|
|
s.set_radio_at=1;assert(s.trackerNeedsRadio());s.set_radio_at=0;
|
|
s.revert_radio_at=1;assert(s.trackerNeedsRadio());s.revert_radio_at=0;
|
|
s.saved_radio_apply_pending=true;assert(s.trackerNeedsRadio());
|
|
s.saved_radio_apply_pending=false;
|
|
#endif
|
|
for(bool* due:{&s.dual,&s.ota,&s._cli.gpio,&radio_driver.watchdog,
|
|
&radio_driver.calibration,&s.queue_due,&s.retry_due}){
|
|
*due=true;assert(s.hasPendingWork()&&s.getPowerSaveSleepSeconds(30)==0);*due=false;
|
|
}
|
|
#if defined(NRF52_PLATFORM) || defined(RP2040_PLATFORM) || defined(STM32_PLATFORM)
|
|
constexpr unsigned queue_idle=3,retry_idle=2;
|
|
#else
|
|
constexpr unsigned queue_idle=2,retry_idle=1;
|
|
#endif
|
|
s.queue_delay_set=true;s.queue_delay=2500;assert(s.getPowerSaveSleepSeconds(30)==queue_idle);
|
|
#if MESH_ENABLE_SENSOR_TRACKER
|
|
assert(s.trackerNeedsRadio());
|
|
#endif
|
|
s.queue_delay_set=false;s.retry_delay_set=true;s.retry_delay=1500;
|
|
assert(s.getPowerSaveSleepSeconds(30)==retry_idle);s.retry_delay_set=false;
|
|
s.dirty_contacts_expiry=now_ms+1001;assert(s.getPowerSaveSleepSeconds(30)==retry_idle);
|
|
s.dirty_contacts_expiry=0;s.last_read_time=45;
|
|
assert(s.getPowerSaveSleepSeconds(30)==5);s.last_read_time=40;
|
|
assert(s.hasPendingWork()&&s.getPowerSaveSleepSeconds(30)==0);
|
|
}
|
|
'''
|
|
|
|
TIMER = r'''
|
|
#include <Arduino.h>
|
|
#include <cassert>
|
|
#include <helpers/PowerSavingDiagnostics.h>
|
|
#include <helpers/PowerManagementUtils.h>
|
|
using TimerHandle_t=void*;
|
|
using TickType_t=uint32_t;
|
|
bool allocate=true,queue_ok=true,active=false;
|
|
TickType_t timer_now=0,expiry=0;
|
|
unsigned period_ms=0,begin_calls=0,queue_calls=0;
|
|
constexpr int pdPASS=1,pdFALSE=0;
|
|
void taskENTER_CRITICAL(){}
|
|
void taskEXIT_CRITICAL(){}
|
|
int xTimerIsTimerActive(TimerHandle_t){return active;}
|
|
TickType_t xTimerGetExpiryTime(TimerHandle_t){assert(active);return expiry;}
|
|
TickType_t xTaskGetTickCount(){return timer_now;}
|
|
uint32_t pdMS_TO_TICKS(uint32_t value){return value/10;}
|
|
int xTimerChangePeriod(TimerHandle_t handle,uint32_t ticks,int wait){
|
|
assert(handle&&wait==0);++queue_calls;
|
|
if(!queue_ok)return 0;
|
|
period_ms=ticks*10;expiry=timer_now+ticks;active=true;return pdPASS;
|
|
}
|
|
class SoftwareTimer {
|
|
TimerHandle_t handle=nullptr;
|
|
public:
|
|
TimerHandle_t getHandle(){return handle;}
|
|
void begin(uint32_t ms,void(*callback)(TimerHandle_t),void* id,bool repeat){
|
|
assert(ms==1000&&callback&&id==nullptr&&!repeat);++begin_calls;
|
|
if(allocate)handle=this;
|
|
}
|
|
};
|
|
static SoftwareTimer tracker_wake_timer;
|
|
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;}
|
|
} the_mesh;
|
|
struct Board {unsigned sleeps=0,last=0;
|
|
void sleep(unsigned value){++sleeps;last=value;}
|
|
void sleepMilliseconds(unsigned value){++sleeps;last=value;}
|
|
} board;
|
|
static const unsigned long POWERSAVING_FIRST_SLEEP_SECS=120;
|
|
void sleepGate(bool can_power_save){
|
|
@GATE@
|
|
}
|
|
int main(){
|
|
#if defined(NRF52_PLATFORM) && MESH_ENABLE_SENSOR_TRACKER
|
|
allocate=false;sleepGate(true);assert(board.sleeps==0&&delay_count==1&&queue_calls==0);
|
|
allocate=true;queue_ok=false;sleepGate(true);
|
|
assert(board.sleeps==0&&delay_count==2&&queue_calls==1);
|
|
queue_ok=true;sleepGate(true);assert(board.sleeps==1&&board.last==0&&period_ms==30000);
|
|
assert(!armTrackerSleepWake(0)&&!armTrackerSleepWake(31));
|
|
unsigned calls=queue_calls;sleepGate(false);assert(queue_calls==calls);
|
|
the_mesh.sleep_secs=0;sleepGate(true);assert(queue_calls==calls&&board.sleeps==1);
|
|
the_mesh.sleep_secs=7;sleepGate(true);assert(period_ms==7000&&board.sleeps==2);
|
|
the_mesh.tracker=false;queue_ok=false;sleepGate(true);assert(board.sleeps==3);
|
|
#elif defined(NRF52_PLATFORM)
|
|
sleepGate(true);assert(board.sleeps==1&&board.last==0);
|
|
the_mesh.sleep_secs=0;sleepGate(true);assert(board.sleeps==1);
|
|
#else
|
|
now_ms=119999;sleepGate(true);assert(board.sleeps==0);
|
|
now_ms=120000;sleepGate(true);assert(board.sleeps==1&&board.last==30000);
|
|
the_mesh.sleep_secs=0;sleepGate(true);assert(board.sleeps==1);
|
|
#endif
|
|
}
|
|
'''
|
|
|
|
RADIO = r'''
|
|
#include <Arduino.h>
|
|
#include <cassert>
|
|
#define RADIOLIB_ERR_NONE 0
|
|
#define STATE_IDLE 0
|
|
#define STATE_TX_WAIT 3
|
|
struct PhysicalLayer {
|
|
int standby_result=0,sleep_result=0;unsigned standbys=0,sleeps=0;
|
|
int standby(){++standbys;return standby_result;}
|
|
int sleep(){++sleeps;return sleep_result;}
|
|
};
|
|
class RadioLibWrapper {
|
|
public:
|
|
PhysicalLayer layer;PhysicalLayer* _radio=&layer;
|
|
bool _cw_active=false,pending=false,_rx_ps_armed=true,_rx_hold_continuous=true;
|
|
unsigned state=5;
|
|
bool isPacketPendingOrReceiving(){return pending;}
|
|
bool setTrackerSleep(bool sleeping);
|
|
};
|
|
@RADIO@
|
|
struct Ms {uint32_t getMillis(){return millis();}};
|
|
struct DispatcherRadio {unsigned begins=0;void begin(){++begins;}bool isInRecvMode(){return true;}};
|
|
struct Liveness {unsigned begins=0;uint32_t at=0;void begin(uint32_t now){++begins;at=now;}};
|
|
class Dispatcher {
|
|
public:
|
|
bool radio_available=true,dispatcher_started=true,radio_nonrx_timer_armed=true;
|
|
bool prev_isrecv_mode=false,nonrx_soft_recovery_attempted=true;
|
|
struct { unsigned resets=0; void reset(){++resets;} } agc_reset;
|
|
uint32_t radio_nonrx_start=0,next_floor_calib_time=0;
|
|
uint32_t rx_watchdog_window_start=0;
|
|
Ms ms;Ms* _ms=&ms;DispatcherRadio radio;DispatcherRadio* _radio=&radio;Liveness radio_liveness;
|
|
void setRadioAvailable(bool available);
|
|
};
|
|
#define MESH_DEBUG_PRINTLN(...) ((void)0)
|
|
@DISPATCHER@
|
|
int main(){
|
|
RadioLibWrapper r;r._cw_active=true;assert(!r.setTrackerSleep(true)&&r.layer.standbys==0);
|
|
r._cw_active=false;r.state=STATE_TX_WAIT;
|
|
assert(!r.setTrackerSleep(true)&&r.layer.standbys==0);r.state=5;r.pending=true;
|
|
assert(!r.setTrackerSleep(true)&&r.layer.standbys==0);r.pending=false;
|
|
r.layer.standby_result=1;assert(!r.setTrackerSleep(true)&&r.layer.sleeps==0);
|
|
r.layer.standby_result=0;r.layer.sleep_result=1;assert(!r.setTrackerSleep(true));
|
|
r.layer.sleep_result=0;assert(r.setTrackerSleep(true)&&r.state==STATE_IDLE);
|
|
assert(!r._rx_ps_armed&&!r._rx_hold_continuous);
|
|
unsigned sleeps=r.layer.sleeps;assert(r.setTrackerSleep(false)&&r.layer.sleeps==sleeps);
|
|
Dispatcher d;now_ms=12345;d.setRadioAvailable(false);
|
|
assert(!d.radio_available&&!d.radio_nonrx_timer_armed&&d.radio.begins==0);
|
|
d.setRadioAvailable(true);assert(d.radio_available&&d.radio.begins==1&&d.prev_isrecv_mode);
|
|
assert(d.radio_nonrx_start==12345&&d.next_floor_calib_time==12345);
|
|
assert(d.agc_reset.resets==1);
|
|
#ifdef RADIO_LIVENESS_SOFT_ONLY
|
|
assert(d.rx_watchdog_window_start==12345);
|
|
#else
|
|
assert(d.radio_liveness.begins==1&&d.radio_liveness.at==12345&&!d.nonrx_soft_recovery_attempted);
|
|
#endif
|
|
d.setRadioAvailable(true);assert(d.radio.begins==1);
|
|
d.setRadioAvailable(false);d.dispatcher_started=false;d.setRadioAvailable(true);
|
|
assert(d.radio.begins==1);
|
|
}
|
|
'''
|
|
|
|
|
|
class SensorTrackerSleepTest(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.compiler = shutil.which("g++") or shutil.which("clang++")
|
|
if cls.compiler is None:
|
|
raise unittest.SkipTest("a host C++17 compiler is required")
|
|
|
|
def compile_run(self, source, defines=()):
|
|
with tempfile.TemporaryDirectory(prefix="meshcore-tracker-sleep-") as directory:
|
|
path = Path(directory)
|
|
(path / "Arduino.h").write_text(ARDUINO, encoding="ascii")
|
|
cpp, binary = path / "test.cpp", path / "test"
|
|
cpp.write_text(source, encoding="ascii")
|
|
result = subprocess.run([
|
|
self.compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
|
"-Wno-unused-parameter", "-Wno-unused-function", "-Wno-unused-variable",
|
|
*(["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
|
|
"-fno-pie", "-no-pie"] if sys.platform.startswith("linux") else []),
|
|
*[f"-D{define}" for define in defines], "-I", str(path), "-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=10)
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
|
|
def test_actual_deadlines_and_radio_owners_feature_on_and_off(self):
|
|
sensor = (ROOT / "examples/simple_sensor/SensorMesh.cpp").read_text()
|
|
tracker = (ROOT / "examples/simple_sensor/Tracker.cpp").read_text()
|
|
methods = "\n".join(extract_braced(sensor, name) for name in (
|
|
"bool SensorMesh::isMillisTimerDue(", "uint32_t SensorMesh::limitSleepToMillisTimer(",
|
|
"bool SensorMesh::hasPendingWork(", "mesh::powerdiag::Mask SensorMesh::pendingWorkReason(",
|
|
"uint32_t SensorMesh::getPowerSaveSleepMilliseconds(",
|
|
"uint32_t SensorMesh::getPowerSaveSleepSeconds("))
|
|
methods += "\n#if MESH_ENABLE_SENSOR_TRACKER\n" + extract_braced(
|
|
tracker, "bool SensorMesh::trackerNeedsRadio(") + "\n#endif\n"
|
|
for enabled in (0, 1):
|
|
for platform in ((), ("NRF52_PLATFORM=1",)):
|
|
with self.subTest(enabled=enabled, platform=platform):
|
|
self.compile_run(DEADLINES.replace("@METHODS@", methods),
|
|
(f"MESH_ENABLE_SENSOR_TRACKER={enabled}", *platform))
|
|
|
|
def test_actual_main_timer_allocation_queue_failure_and_platform_paths(self):
|
|
main = (ROOT / "examples/simple_sensor/main.cpp").read_text()
|
|
source = TIMER.replace("@ARM@", extract_braced(
|
|
main, "static bool armTrackerSleepWake(")).replace(
|
|
"@GATE@", extract_braced(main, "if (can_power_save)"))
|
|
for defines in (("NRF52_PLATFORM=1", "MESH_ENABLE_SENSOR_TRACKER=1"),
|
|
("NRF52_PLATFORM=1", "MESH_ENABLE_SENSOR_TRACKER=0"),
|
|
("ESP32_PLATFORM=1", "MESH_ENABLE_SENSOR_TRACKER=1"),
|
|
("RP2040_PLATFORM=1", "MESH_ENABLE_SENSOR_TRACKER=1")):
|
|
with self.subTest(defines=defines):
|
|
self.compile_run(source, defines)
|
|
|
|
def test_actual_wake_timer_reuse_rearm_failures_wrap_and_late_irqs(self):
|
|
main = (ROOT / "examples/simple_sensor/main.cpp").read_text(encoding="ascii")
|
|
arm = body(main, "static bool armTrackerSleepWake(")
|
|
fixture = (ROOT / "test/fixtures/sensor_tracker_wake_timer.cpp").read_text(encoding="ascii")
|
|
self.compile_run(fixture.replace("@ARM@", arm))
|
|
|
|
def test_negative_control_reposting_timer_command_prevents_idle_sleep(self):
|
|
main = (ROOT / "examples/simple_sensor/main.cpp").read_text(encoding="ascii")
|
|
arm = body(main, "static bool armTrackerSleepWake(")
|
|
start = arm.index(" // Reposting an unchanged timer command")
|
|
end = arm.index(" // Dormant/expired timers", start)
|
|
old = arm[:start] + arm[end:]
|
|
fixture = (ROOT / "test/fixtures/sensor_tracker_wake_timer.cpp").read_text(encoding="ascii")
|
|
with self.assertRaisesRegex(AssertionError,
|
|
"idle timer poll must not requeue and relatch scheduler event"):
|
|
self.compile_run(fixture.replace("@ARM@", old))
|
|
|
|
def test_wake_timer_has_no_dependency_when_tracker_or_platform_is_disabled(self):
|
|
main = (ROOT / "examples/simple_sensor/main.cpp").read_text(encoding="ascii")
|
|
start = main.index("#if defined(NRF52_PLATFORM) && MESH_ENABLE_SENSOR_TRACKER\n")
|
|
end = main.index("\n#endif", start) + len("\n#endif")
|
|
source = main[start:end] + "\nint main() {}\n"
|
|
for defines in (("NRF52_PLATFORM=1", "MESH_ENABLE_SENSOR_TRACKER=0"),
|
|
("ESP32_PLATFORM=1", "MESH_ENABLE_SENSOR_TRACKER=1")):
|
|
with self.subTest(defines=defines):
|
|
self.compile_run(source, defines)
|
|
|
|
def test_actual_radio_warm_sleep_and_dispatcher_watchdog_reanchor(self):
|
|
radio = (ROOT / "src/helpers/radiolib/RadioLibWrappers.cpp").read_text()
|
|
dispatcher = (ROOT / "src/Dispatcher.cpp").read_text()
|
|
source = RADIO.replace("@RADIO@", extract_braced(
|
|
radio, "bool RadioLibWrapper::setTrackerSleep(")).replace(
|
|
"@DISPATCHER@", extract_braced(dispatcher, "void Dispatcher::setRadioAvailable("))
|
|
for defines in ((), ("RADIO_LIVENESS_SOFT_ONLY=1",)):
|
|
with self.subTest(defines=defines):
|
|
self.compile_run(source, defines)
|
|
|
|
def test_feature_defaults_and_overrides(self):
|
|
header = (ROOT / "examples/simple_sensor/SensorMesh.h").read_text()
|
|
config = re.search(r"#ifndef MESH_ENABLE_SENSOR_TRACKER[\s\S]*?#endif\n#endif", header).group()
|
|
source = config + "\nstatic_assert(MESH_ENABLE_SENSOR_TRACKER == EXPECTED);int main(){}\n"
|
|
for defines in (("NRF52_PLATFORM=1", "ENV_INCLUDE_GPS=1", "EXPECTED=1"),
|
|
("ESP32_PLATFORM=1", "ENV_INCLUDE_GPS=1", "EXPECTED=1"),
|
|
("RP2040_PLATFORM=1", "ENV_INCLUDE_GPS=1", "EXPECTED=1"),
|
|
("STM32_PLATFORM=1", "ENV_INCLUDE_GPS=1", "EXPECTED=0"),
|
|
("NRF52_PLATFORM=1", "ENV_INCLUDE_GPS=0", "EXPECTED=0"),
|
|
("STM32_PLATFORM=1", "ENV_INCLUDE_GPS=1", "MESH_ENABLE_SENSOR_TRACKER=1", "EXPECTED=1")):
|
|
with self.subTest(defines=defines):
|
|
self.compile_run(source, defines)
|
|
|
|
def test_actual_foreground_loop_without_tracker_has_no_tracker_dependencies(self):
|
|
sensor = (ROOT / "examples/simple_sensor/SensorMesh.cpp").read_text(encoding="ascii")
|
|
loop = extract_braced(sensor, "void SensorMesh::loop()")
|
|
boundary = "\n if (next_flood_advert &&"
|
|
self.assertEqual(loop.count(boundary), 1)
|
|
loop = loop[:loop.index(boundary)] + "}\n"
|
|
policy = (ROOT / "src/helpers/MorseLoopPolicy.h").read_text(encoding="ascii")
|
|
source = r'''
|
|
#include <Arduino.h>
|
|
#include <cassert>
|
|
namespace mesh {
|
|
struct Radio {
|
|
bool tx=false,rx=false;
|
|
bool isMorseTxActive()const{return tx;}
|
|
bool isMorseRxActive()const{return rx;}
|
|
};
|
|
struct Mesh {unsigned base_loops=0;void loop(){++base_loops;}};
|
|
@MORSE@
|
|
}
|
|
struct SensorMesh:mesh::Mesh {
|
|
mesh::Radio radio;mesh::Radio* _radio=&radio;
|
|
struct Service {unsigned loops=0;void loop(){++loops;}} _cli,_clock_sync;
|
|
void loop();
|
|
};
|
|
@LOOP@
|
|
int main(){
|
|
SensorMesh mesh;
|
|
mesh.loop();assert(mesh._cli.loops==1&&mesh.base_loops==1&&mesh._clock_sync.loops==1);
|
|
mesh.radio.tx=true;mesh.loop();
|
|
assert(mesh._cli.loops==2&&mesh.base_loops==2&&mesh._clock_sync.loops==1);
|
|
mesh.radio.tx=false;mesh.radio.rx=true;mesh.loop();
|
|
assert(mesh._cli.loops==3&&mesh.base_loops==3&&mesh._clock_sync.loops==1);
|
|
mesh.radio.rx=false;mesh.loop();
|
|
assert(mesh._cli.loops==4&&mesh.base_loops==4&&mesh._clock_sync.loops==2);
|
|
}
|
|
'''
|
|
source = source.replace("@MORSE@", extract_braced(
|
|
policy, "inline bool morseTimingActive(")).replace("@LOOP@", loop)
|
|
self.compile_run(source, ("MESH_ENABLE_SENSOR_TRACKER=0",))
|
|
|
|
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(", "mesh::powerdiag::Mask SensorMesh::pendingWorkReason(",
|
|
"uint32_t SensorMesh::getPowerSaveSleepMilliseconds(",
|
|
"uint32_t SensorMesh::getPowerSaveSleepSeconds("))
|
|
methods += "\n" + "\n".join(body(tracker, signature) for signature in (
|
|
"bool SensorMesh::trackerNeedsRadio(", "void SensorMesh::wakeTrackerRadio("))
|
|
# 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()
|