284 lines
12 KiB
C++
284 lines
12 KiB
C++
#include <helpers/MorseLoopPolicy.h>
|
|
#include <helpers/PowerSavingDiagnostics.h>
|
|
#include <helpers/PowerManagementUtils.h>
|
|
#include <cstdarg>
|
|
static unsigned usb_services=0,command_services=0,transport_services=0,delays=0;
|
|
static unsigned usb_bluetooth_priority_services=0;
|
|
static bool usb_watchdog_armed=false,wireless_pending=false;
|
|
bool profileIdlePreparationAllowed();
|
|
bool profileIdleQueryAllowed();
|
|
bool profileIdleSleepAllowed();
|
|
struct Stream {
|
|
int available() const { return 0; }
|
|
char read() const { return 0; }
|
|
void print(char) {}
|
|
void print(const char*) {}
|
|
void println(const char*) {}
|
|
void printf(const char*,...) {}
|
|
} console;
|
|
char command[180]={};
|
|
uint32_t millis() { return 100000; }
|
|
void delay(unsigned) { ++delays; }
|
|
bool usbLoggingRecoverySafe() { return true; }
|
|
namespace mesh {
|
|
namespace power {
|
|
// Regulator persistence/recovery has separate executable tests. Keep its
|
|
// lifecycle boundary visible while this fixture exercises actual role loops.
|
|
static unsigned regulator_services=0;
|
|
void serviceRegulatorSettings() { ++regulator_services; }
|
|
}
|
|
void serviceUsbLoggingPort() { ++usb_services; }
|
|
void serviceUsbTerminalPort() { ++usb_services; }
|
|
bool isUsbLoggingEnabled() { return false; }
|
|
bool isUsbLoggingWatchdogArmed() { return usb_watchdog_armed; }
|
|
bool serviceUsbLoggingWatchdog(bool(*)()) { ++usb_services; return false; }
|
|
Stream& usbConsolePort() { return console; }
|
|
void noteUsbLoggingStatsCommand(const char*) { ++usb_services; }
|
|
namespace wireless {
|
|
struct Control { void service(uint32_t) { ++transport_services; } bool pending() { return wireless_pending; } } controller;
|
|
Control& control() { return controller; }
|
|
}
|
|
}
|
|
struct Board {
|
|
unsigned calls=0,watchdogs=0,sleeps=0,fans=0,reboots=0,preparations=0;
|
|
unsigned millisecond_sleeps=0,last_sleep_ms=0,profile_preparations=0;
|
|
bool profile_prepared=false;
|
|
void prepareSleepCycle() { ++preparations; profile_prepared=false; }
|
|
void prepareProfileIdleCycle() {
|
|
assert(profileIdlePreparationAllowed());
|
|
++profile_preparations; profile_prepared=true;
|
|
}
|
|
void loop() { ++calls; }
|
|
void feedWatchdog(bool=true) { ++watchdogs; }
|
|
void sleep(uint32_t) { assert(profileIdleSleepAllowed()); ++sleeps; }
|
|
void sleepMilliseconds(uint32_t ms) {
|
|
assert(profileIdleSleepAllowed()); ++sleeps; ++millisecond_sleeps; last_sleep_ms=ms;
|
|
}
|
|
bool isUsbDataConnected() { return false; }
|
|
bool isUsbHostConnected() { return false; }
|
|
void reboot() { ++reboots; }
|
|
void updateFanControl() { ++fans; }
|
|
} board;
|
|
struct Task {
|
|
unsigned calls=0,deadline=200;
|
|
void loop() { ++calls; deadline+=100; }
|
|
void tick() { loop(); }
|
|
void feed() { loop(); }
|
|
} sensors,ui_task,rtc_clock,interface_manager,external_watchdog;
|
|
bool display_ready=true;
|
|
struct Mesh {
|
|
struct Prefs { bool powersaving_enabled=true,system_watchdog_enabled=true; uint32_t reboot_interval=0; } prefs;
|
|
unsigned calls=0;
|
|
bool noise_floor_wait=false,work_pending=false;
|
|
bool dual_radio=false,profile_idle_allowed=true;
|
|
mutable unsigned profile_idle_queries=0;
|
|
uint32_t noise_floor_delay_ms=50,sleep_budget_ms=1000;
|
|
void loop() { ++calls; }
|
|
Prefs* getNodePrefs() { return &prefs; }
|
|
uint32_t getPowerSaveSleepMilliseconds(unsigned) { return work_pending ? 0 : sleep_budget_ms; }
|
|
bool getNextNoiseFloorWakeDelay(uint32_t& ms) const {
|
|
ms=noise_floor_delay_ms; return noise_floor_wait;
|
|
}
|
|
bool millisHasNowPassed(uint32_t) { return true; }
|
|
bool isTrackerModeEnabled() { return false; }
|
|
bool isDualRadioActive() const { return dual_radio; }
|
|
bool canIdleDualRadio() const {
|
|
if (!dual_radio) return true;
|
|
assert(profileIdleQueryAllowed());
|
|
++profile_idle_queries;
|
|
return profile_idle_allowed;
|
|
}
|
|
bool hasPendingWork() { return false; }
|
|
mesh::powerdiag::Mask pendingWorkReason() const {
|
|
if (!canIdleDualRadio()) return mesh::powerdiag::mask(mesh::powerdiag::Reason::DualRadio);
|
|
return work_pending ? mesh::powerdiag::mask(mesh::powerdiag::Reason::Queue) : 0;
|
|
}
|
|
void handleCommand(uint32_t,char*,char* reply) { reply[0]=0; ++command_services; }
|
|
void handleUsbCommand(char*,char* reply) { reply[0]=0; ++command_services; }
|
|
} the_mesh;
|
|
TestRadio radio_driver;
|
|
bool profileIdlePreparationAllowed() {
|
|
return (the_mesh.prefs.powersaving_enabled || the_mesh.isTrackerModeEnabled())
|
|
&& the_mesh.isDualRadioActive() && !mesh::morseTimingActive(&radio_driver);
|
|
}
|
|
bool profileIdleQueryAllowed() {
|
|
return profileIdlePreparationAllowed() && board.profile_prepared;
|
|
}
|
|
bool profileIdleSleepAllowed() {
|
|
return !mesh::morseTimingActive(&radio_driver)
|
|
&& (!the_mesh.isDualRadioActive() || (profileIdleQueryAllowed() && the_mesh.profile_idle_allowed));
|
|
}
|
|
void serviceCommandInterfaces() { ++command_services; }
|
|
void serviceUsbLoggingOnlySession() { ++usb_services; }
|
|
void serviceUsbTerminalHostSessionReset() { ++usb_services; }
|
|
void serviceUsbAsciiSessionDefault() { ++usb_services; }
|
|
void serviceUsbLoggingReplyBarrier() { ++usb_services; }
|
|
void serviceUsbLoggingOwnership(bool) { ++usb_services; }
|
|
void expireUsbBinaryStartupProbeBeforeDispatch() { ++usb_services; }
|
|
void serviceUsbTerminal() { ++usb_services; }
|
|
void syncUsbTerminalInputEditing() { ++usb_services; }
|
|
void serviceDeferredCompanionBluetooth() { ++transport_services; }
|
|
void serviceCompanionBluetoothIdentity() { ++transport_services; }
|
|
void serviceCompanionBluetoothControl() { ++transport_services; }
|
|
void serviceCompanionBluetoothName() { ++transport_services; }
|
|
// Transport handoff has separate policy/state tests. Keep its loop boundary
|
|
// live here, including during Morse, before the authoritative sleep snapshot.
|
|
void serviceCompanionUsbBluetoothPriority() {
|
|
assert(usb_bluetooth_priority_services == board.preparations);
|
|
++usb_bluetooth_priority_services;
|
|
}
|
|
@LOOP@
|
|
int main() {
|
|
assert(!mesh::morseTimingActive(nullptr));
|
|
assert(!mesh::morseTimingActive(&radio_driver));
|
|
loop();
|
|
assert(sensors.calls==1 && ui_task.calls==1 && board.sleeps==1);
|
|
radio_driver.cw=true; // Ordinary carrier-wave diagnostics keep old scheduling.
|
|
loop();
|
|
assert(sensors.calls==2 && ui_task.calls==2 && board.sleeps==2);
|
|
for(unsigned mode=0;mode<3;++mode) {
|
|
radio_driver.active=mode==0;
|
|
radio_driver.rx_active=mode!=0;
|
|
radio_driver.rx_recovering=mode==2;
|
|
assert(mesh::morseTimingActive(&radio_driver));
|
|
const auto sensor_deadline=sensors.deadline;
|
|
const auto usb_before=usb_services;
|
|
loop();
|
|
// UI button/touch polling stays live; actual expensive redraw work is
|
|
// deferred inside the UI implementation, not by skipping input service.
|
|
assert(sensors.calls==2 && ui_task.calls==mode+3 && board.sleeps==2 && !delays);
|
|
assert(sensors.deadline==sensor_deadline);
|
|
assert(usb_services>usb_before && the_mesh.calls==mode+3 && board.watchdogs==mode+3);
|
|
assert(rtc_clock.calls==mode+3 && external_watchdog.calls>=mode+3);
|
|
#if !ROLE_SENSOR
|
|
assert(board.fans==mode+3);
|
|
#endif
|
|
}
|
|
radio_driver.active=radio_driver.rx_active=radio_driver.rx_recovering=false;
|
|
loop();
|
|
assert(sensors.calls==3 && ui_task.calls==6 && board.sleeps==3 && !delays);
|
|
assert(the_mesh.calls==6 && rtc_clock.calls==6 && board.watchdogs==6);
|
|
assert(board.preparations==6); // Prepare even when Morse vetoes actual sleep.
|
|
#if !ROLE_SENSOR
|
|
assert(board.fans==6);
|
|
#endif
|
|
#if ROLE_COMPANION
|
|
assert(interface_manager.calls==6 && transport_services==30 && board.calls==6);
|
|
#elif ROLE_REPEATER
|
|
assert(command_services==6 && transport_services==6);
|
|
#elif ROLE_ROOM
|
|
assert(transport_services==6 && board.calls==6);
|
|
#else
|
|
assert(board.calls==6);
|
|
#endif
|
|
const auto sleeps_before_hold=board.sleeps;
|
|
usb_watchdog_armed=true;
|
|
loop();
|
|
assert(board.sleeps==sleeps_before_hold);
|
|
usb_watchdog_armed=false;
|
|
#if !ROLE_SENSOR
|
|
wireless_pending=true;
|
|
loop();
|
|
assert(board.sleeps==sleeps_before_hold);
|
|
wireless_pending=false;
|
|
#endif
|
|
// Calibration remains RF work, but its future sample deadline permits
|
|
// bounded MCU idle in every actual role loop. Due work and other vetoes win.
|
|
the_mesh.noise_floor_wait=true;
|
|
auto before_noise_sleep=board.sleeps;
|
|
const auto before_noise_ms_sleep=board.millisecond_sleeps;
|
|
loop();
|
|
assert(board.sleeps==before_noise_sleep+1 && board.millisecond_sleeps==before_noise_ms_sleep+1);
|
|
#if ROLE_COMPANION
|
|
assert(board.last_sleep_ms==10); // Preserve the shorter Companion cadence.
|
|
#else
|
|
assert(board.last_sleep_ms==50);
|
|
#endif
|
|
the_mesh.noise_floor_delay_ms=1;
|
|
loop();
|
|
assert(board.sleeps==before_noise_sleep+2 && board.last_sleep_ms==1);
|
|
the_mesh.noise_floor_delay_ms=0;
|
|
loop();
|
|
assert(board.sleeps==before_noise_sleep+2); // Sample/transition now due.
|
|
the_mesh.noise_floor_delay_ms=50; the_mesh.work_pending=true;
|
|
loop();
|
|
assert(board.sleeps==before_noise_sleep+2); // Never widen another owner's veto.
|
|
the_mesh.work_pending=false; usb_watchdog_armed=true;
|
|
loop();
|
|
assert(board.sleeps==before_noise_sleep+2);
|
|
usb_watchdog_armed=false;
|
|
#if !ROLE_COMPANION
|
|
// The role's earlier queue/bridge/timer deadline wins over calibration.
|
|
the_mesh.sleep_budget_ms=17;
|
|
loop();
|
|
assert(board.last_sleep_ms==17);
|
|
the_mesh.noise_floor_wait=false;
|
|
for (uint32_t remaining : {1U,50U,999U,1001U,29999U}) {
|
|
const auto before=board.sleeps;
|
|
the_mesh.sleep_budget_ms=remaining;
|
|
loop();
|
|
assert(board.sleeps==before+1);
|
|
#if !defined(NRF52_PLATFORM)
|
|
assert(board.last_sleep_ms==remaining); // No whole-second busy tail.
|
|
#endif
|
|
}
|
|
#endif
|
|
assert(board.profile_preparations==0); // Ordinary single-radio loops do not prepare a scan idle.
|
|
// A scanner can prepare its board wake contract without admitting sleep.
|
|
// Once settled it may idle, but it must not override Morse or other owners.
|
|
the_mesh.noise_floor_wait=false;
|
|
the_mesh.work_pending=false;
|
|
the_mesh.sleep_budget_ms=1000;
|
|
the_mesh.dual_radio=true;
|
|
the_mesh.profile_idle_allowed=false;
|
|
auto profile_preparations=board.profile_preparations;
|
|
auto profile_sleeps=board.sleeps;
|
|
auto idle_queries=the_mesh.profile_idle_queries;
|
|
loop();
|
|
assert(board.profile_preparations==profile_preparations+1 && board.sleeps==profile_sleeps);
|
|
assert(the_mesh.profile_idle_queries>idle_queries);
|
|
the_mesh.profile_idle_allowed=true;
|
|
idle_queries=the_mesh.profile_idle_queries;
|
|
loop();
|
|
assert(board.profile_preparations==profile_preparations+2 && board.sleeps==profile_sleeps+1);
|
|
assert(the_mesh.profile_idle_queries>idle_queries);
|
|
profile_preparations=board.profile_preparations;
|
|
profile_sleeps=board.sleeps;
|
|
idle_queries=the_mesh.profile_idle_queries;
|
|
for (unsigned mode=0;mode<3;++mode) {
|
|
radio_driver.active=mode==0;
|
|
radio_driver.rx_active=mode!=0;
|
|
radio_driver.rx_recovering=mode==2;
|
|
const auto sensor_before=sensors.calls;
|
|
const auto ui_before=ui_task.calls;
|
|
const auto watchdog_before=board.watchdogs;
|
|
const auto usb_before=usb_services;
|
|
loop();
|
|
assert(board.profile_preparations==profile_preparations && board.sleeps==profile_sleeps);
|
|
assert(the_mesh.profile_idle_queries==idle_queries);
|
|
assert(sensors.calls==sensor_before && ui_task.calls==ui_before+1);
|
|
assert(board.watchdogs==watchdog_before+1 && usb_services>usb_before);
|
|
}
|
|
radio_driver.active=radio_driver.rx_active=radio_driver.rx_recovering=false;
|
|
the_mesh.prefs.powersaving_enabled=false;
|
|
loop();
|
|
assert(board.profile_preparations==profile_preparations && board.sleeps==profile_sleeps);
|
|
assert(the_mesh.profile_idle_queries==idle_queries);
|
|
the_mesh.prefs.powersaving_enabled=true;
|
|
usb_watchdog_armed=true;
|
|
loop();
|
|
assert(board.profile_preparations==profile_preparations+1 && board.sleeps==profile_sleeps);
|
|
usb_watchdog_armed=false;
|
|
the_mesh.dual_radio=false;
|
|
loop();
|
|
assert(board.profile_preparations==profile_preparations+1 && board.sleeps==profile_sleeps+1);
|
|
#if !ROLE_REPEATER
|
|
assert(mesh::power::regulator_services==board.calls);
|
|
#endif
|
|
#if ROLE_COMPANION
|
|
assert(usb_bluetooth_priority_services==board.preparations);
|
|
#else
|
|
assert(usb_bluetooth_priority_services==0);
|
|
#endif
|
|
}
|