Add saved regulator modes and battery-gated crash recovery
This commit is contained in:
1 parent
515bb57cdf
commit
c4a1d485ff
70 files changed
+5335
-2472
No files matched your search
Vendored
+5
@@ -5,6 +5,7 @@
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
#include <helpers/radiolib/LR2021Band.h>
|
||||
#include <helpers/radiolib/RadioRegulatorState.h>
|
||||
#define RADIOLIB_ERR_NONE 0
|
||||
#define RADIOLIB_ERR_UNKNOWN -16
|
||||
#define RADIOLIB_NC 0xffffffffU
|
||||
@@ -74,6 +75,7 @@ struct Native {
|
||||
}
|
||||
int16_t applyMeshCoreTcxoDelay() { return call(2); }
|
||||
int16_t setRegulatorDCDC() { return call(3); }
|
||||
int16_t setRegulatorLDO() { return call(3); }
|
||||
int16_t setDioAsRfSwitch(uint8_t mask,uint8_t a,uint8_t b,uint8_t c,uint8_t d,uint8_t e,uint8_t f,uint8_t g) {
|
||||
enable=mask; const uint8_t values[]={a,b,c,d,e,f,g}; memcpy(modes,values,sizeof(modes));
|
||||
return call(4);
|
||||
@@ -112,17 +114,20 @@ using LR1110=Native; using LR1121=Native;
|
||||
struct LR2021 : Native { enum { MODE_RX=3,MODE_RX_HF=6,MODE_END_OF_TABLE=0 }; };
|
||||
using SX1276=Native;
|
||||
struct LR1110Probe : LR1110 {
|
||||
@LR1110_REGULATOR@
|
||||
struct State { unsigned invalidations=0; void invalidate() { ++invalidations; } } _profileSwitch;
|
||||
bool _profilePacketDirty=true,_rx_boosted=true;
|
||||
LR11x0MorseRfSwitch _morse_rf_switch;
|
||||
@LR1110@
|
||||
};
|
||||
struct LR1121Probe : LR1121 {
|
||||
@LR1121_REGULATOR@
|
||||
bool _rx_boosted=true;
|
||||
LR11x0MorseRfSwitch _morse_rf_switch;
|
||||
@LR1121@
|
||||
};
|
||||
struct LR2021Probe : LR2021 {
|
||||
@LR2021_REGULATOR@
|
||||
uint8_t gainModeLf=1,gainModeHf=3;
|
||||
uint32_t _rf_switch_pins[Module::RFSWITCH_MAX_PINS]={0x80,0x81,RADIOLIB_NC};
|
||||
const Module::RfSwitchMode_t* _rf_switch_table=nullptr;
|
||||
|
||||
+14
-3
@@ -14,6 +14,7 @@ MockNrfUsbd mock_usbd;
|
||||
static bool mounted = true, dtr = true, dfu = false;
|
||||
static bool attached = true, armed_in = false;
|
||||
static unsigned detaches = 0, attaches = 0;
|
||||
static unsigned clean_reboots = 0;
|
||||
static uint32_t baud = 115200;
|
||||
static std::string fifo, host;
|
||||
static std::function<void()> during_write;
|
||||
@@ -47,7 +48,11 @@ uint32_t tud_cdc_n_write(uint8_t n, const void* data, uint32_t size) {
|
||||
uint32_t tud_cdc_n_write_flush(uint8_t n) { assert(n == 0); return 0; }
|
||||
uint32_t tud_cdc_n_write_clear(uint8_t n) { assert(n == 0); fifo.clear(); return 0; }
|
||||
void tud_cdc_get_line_coding(cdc_line_coding_t* coding) { coding->bit_rate = baud; }
|
||||
void TinyUSB_Port_EnterDFU() { dfu = true; }
|
||||
extern "C" void meshRegulatorCleanReset() { ++clean_reboots; }
|
||||
void TinyUSB_Port_EnterDFU() {
|
||||
assert(clean_reboots == 1); // Mark the actual reset before entering the core.
|
||||
dfu = true;
|
||||
}
|
||||
|
||||
static void service() {
|
||||
mesh::serviceUsbLoggingPort();
|
||||
@@ -169,8 +174,14 @@ int main() {
|
||||
drain();
|
||||
assert(host.empty()); // old armed packet cannot leak into a new handle
|
||||
|
||||
baud = 1200; dtr = false;
|
||||
assert(clean_reboots == 0); // Normal host closes/reconnects remain armed.
|
||||
baud = 1200;
|
||||
tud_cdc_line_coding_cb(0, nullptr);
|
||||
tud_cdc_line_state_cb(0, true, false);
|
||||
tud_cdc_line_state_cb(1, false, false);
|
||||
assert(clean_reboots == 0 && !dfu); // Neither coding nor CDC1 requests DFU.
|
||||
dtr = false;
|
||||
tud_cdc_line_state_cb(0, false, false);
|
||||
assert(dfu); // Existing bootloader entry is preserved.
|
||||
assert(dfu && clean_reboots == 1); // Existing bootloader entry is preserved.
|
||||
puts("native nRF52 console transport passed");
|
||||
}
|
||||
Vendored
+1
@@ -1,3 +1,4 @@
|
||||
extern "C" void meshRegulatorCleanReset() __attribute__((weak));
|
||||
|
||||
#include <helpers/ota/MotaContainer.h>
|
||||
#include <helpers/ota/OtaByteIO.h>
|
||||
|
||||
Vendored
+1
@@ -1,3 +1,4 @@
|
||||
extern "C" void meshRegulatorCleanReset() __attribute__((weak));
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <helpers/RegulatorMode.h>
|
||||
#include <helpers/RegulatorRecovery.h>
|
||||
#include <helpers/IdentityStore.h>
|
||||
#include <Dispatcher.h>
|
||||
uint32_t mock_now=0;
|
||||
#if defined(ESP32)
|
||||
#include <esp_system.h>
|
||||
int mock_reset_reason=ESP_RST_POWERON;
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
bool mock_watchdog_enabled=false,mock_watchdog_any=false;
|
||||
#else
|
||||
namespace mesh { namespace power {
|
||||
volatile RecoveryRecord test_retained;
|
||||
ResetKind test_reset=ResetKind::Cold;
|
||||
volatile RecoveryRecord& platformRecoveryRecord() { return test_retained; }
|
||||
ResetKind platformResetKind() { return test_reset; }
|
||||
bool platformRecoverySupported() { return true; }
|
||||
unsigned configured_percent_calls=0;
|
||||
uint8_t configuredBatteryPercent(uint16_t mv) {
|
||||
++configured_percent_calls;
|
||||
return mv<=3000 ? 0 : mv>=4200 ? 100 : uint8_t((mv-3000)*100/1200);
|
||||
}
|
||||
}}
|
||||
#endif
|
||||
#if !defined(NRF52_PLATFORM)
|
||||
#include <helpers/RegulatorPlatform.cpp>
|
||||
#endif
|
||||
#include <helpers/RegulatorSettings.cpp>
|
||||
using namespace mesh::power;
|
||||
static void retainedClear() {
|
||||
volatile uint8_t* p=reinterpret_cast<volatile uint8_t*>(&platformRecoveryRecord());
|
||||
for(size_t i=0;i<sizeof(RecoveryRecord);++i)p[i]=0;
|
||||
}
|
||||
static void resetKind(ResetKind kind) {
|
||||
#if defined(ESP32)
|
||||
mock_reset_reason=kind==ResetKind::Crash ? ESP_RST_PANIC : kind==ResetKind::Intentional
|
||||
? ESP_RST_SW : kind==ResetKind::Wake ? ESP_RST_DEEPSLEEP : kind==ResetKind::Cold
|
||||
? ESP_RST_POWERON : ESP_RST_BROWNOUT;
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
mock_watchdog_enabled=kind==ResetKind::Crash;
|
||||
mock_watchdog_any=kind==ResetKind::Crash || kind==ResetKind::Intentional;
|
||||
#else
|
||||
test_reset=kind;
|
||||
#endif
|
||||
}
|
||||
static void resetGlobals(bool cold=false) {
|
||||
mesh::power::main_board=nullptr;mesh::power::main_radio=nullptr;
|
||||
mesh::power::settings_fs=nullptr;
|
||||
mesh::power::requested[0]=mesh::power::requested[1]=RegulatorMode::Default;
|
||||
mesh::power::saved_fallback=0;mesh::power::initialized=false;
|
||||
mesh::power::fallback_saved=true;
|
||||
mesh::power::last_service=mesh::power::last_battery=mesh::power::last_save=0;
|
||||
mock_now=0;
|
||||
if(cold)retainedClear();
|
||||
}
|
||||
static void fresh(mesh::MainBoard& board,mesh::Radio& radio,MockFS& fs,
|
||||
ResetKind kind=ResetKind::Cold,bool cold=false) {
|
||||
resetGlobals(cold);resetKind(kind);board.resetHardware();radio.resetHardware();
|
||||
assert(beginRegulatorSettings(board,radio,&fs));
|
||||
assert(radio.active==RegulatorMode::Default); // selecting touches no active radio
|
||||
radio.initialize();
|
||||
}
|
||||
static std::string command(const char* text) {
|
||||
char reply[160];memset(reply,0xa5,sizeof(reply));
|
||||
assert(handleRegulatorCommand(text,reply,sizeof(reply)));
|
||||
assert(memchr(reply,0,sizeof(reply)));
|
||||
return reply;
|
||||
}
|
||||
static void advance(uint32_t ms) { mock_now+=ms;serviceRegulatorSettings(); }
|
||||
static void counts(unsigned cpu,unsigned rf) {
|
||||
assert(recovery().crashCount(RECOVERY_MCU)==cpu);
|
||||
assert(recovery().crashCount(RECOVERY_RADIO)==rf);
|
||||
}
|
||||
int main(int argc,char** argv) {
|
||||
assert(argc==2);const std::string test=argv[1];
|
||||
mesh::MainBoard board;mesh::Radio radio;MockFS fs;
|
||||
if(test=="select") {
|
||||
fresh(board,radio,fs,ResetKind::Cold,true);
|
||||
assert(board.active==RegulatorMode::Dcdc && radio.active==RegulatorMode::Dcdc);
|
||||
const unsigned applies=board.applies,configs=radio.configures,inits=radio.inits;
|
||||
assert(command("set mcu.regulator ldo").find("reboot required")!=std::string::npos);
|
||||
assert(command("set radio.regulator ldo").find("reboot required")!=std::string::npos);
|
||||
assert(requestedRegulatorMode(true)==RegulatorMode::Ldo);
|
||||
assert(requestedRegulatorMode(false)==RegulatorMode::Ldo);
|
||||
assert(board.active==RegulatorMode::Dcdc && radio.active==RegulatorMode::Dcdc);
|
||||
assert(board.applies==applies && radio.configures==configs && radio.inits==inits);
|
||||
advance(1000);
|
||||
assert(board.active==RegulatorMode::Dcdc && radio.active==RegulatorMode::Dcdc);
|
||||
fresh(board,radio,fs,ResetKind::Cold,true);
|
||||
assert(board.active==RegulatorMode::Ldo && radio.active==RegulatorMode::Ldo);
|
||||
} else if(test=="fallback") {
|
||||
fresh(board,radio,fs,ResetKind::Cold,true);
|
||||
for(unsigned n=1;n<=3;++n) {
|
||||
advance(1000);fresh(board,radio,fs,ResetKind::Crash);counts(n,n);
|
||||
assert(regulatorFallbackActive(true)==(n==3));
|
||||
assert(regulatorFallbackActive(false)==(n==3));
|
||||
}
|
||||
assert(board.active==RegulatorMode::Ldo && radio.active==RegulatorMode::Ldo);
|
||||
assert(fs.files.count("/regulator_v1"));
|
||||
fresh(board,radio,fs,ResetKind::Cold,true);counts(0,0);
|
||||
assert(board.active==RegulatorMode::Ldo && radio.active==RegulatorMode::Ldo);
|
||||
assert(command("get radio.regulator").find("fallback ldo")!=std::string::npos);
|
||||
} else if(test=="domains") {
|
||||
fresh(board,radio,fs,ResetKind::Cold,true);
|
||||
command("set mcu.regulator ldo");fresh(board,radio,fs,ResetKind::Intentional);
|
||||
for(unsigned n=1;n<=3;++n) { advance(1000);fresh(board,radio,fs,ResetKind::Crash);counts(0,n); }
|
||||
assert(!regulatorFallbackActive(true) && regulatorFallbackActive(false));
|
||||
const auto saved=fs.files["/regulator_v1"];
|
||||
command("set mcu.regulator dcdc");
|
||||
assert(regulatorFallbackActive(false) && !regulatorFallbackActive(true));
|
||||
assert(requestedRegulatorMode(false)==RegulatorMode::Default);
|
||||
fresh(board,radio,fs,ResetKind::Intentional);
|
||||
assert(board.active==RegulatorMode::Dcdc && radio.active==RegulatorMode::Ldo);
|
||||
command("set radio.regulator auto");
|
||||
assert(!regulatorFallbackActive(false));
|
||||
fresh(board,radio,fs,ResetKind::Intentional);
|
||||
assert(radio.active==RegulatorMode::Dcdc);
|
||||
} else if(test=="battery") {
|
||||
for(unsigned mv:{3588u,0u,1999u,5501u}) {
|
||||
fs.files.clear();board.mv=mv;
|
||||
fresh(board,radio,fs,ResetKind::Cold,true);
|
||||
for(unsigned n=0;n<4;++n) {advance(1000);fresh(board,radio,fs,ResetKind::Crash);counts(0,0);}
|
||||
assert(!regulatorFallbackActive(true) && !regulatorFallbackActive(false));
|
||||
}
|
||||
board.mv=3600;fs.files.clear();fresh(board,radio,fs,ResetKind::Cold,true);
|
||||
// Heartbeat alone cannot make a 60-second-old voltage sample fresh.
|
||||
recovery().heartbeat(mock_now+REGULATOR_BATTERY_FRESH_MS+1);
|
||||
fresh(board,radio,fs,ResetKind::Crash);counts(0,0);
|
||||
// A median rejects one high outlier rather than guessing from the maximum.
|
||||
board.samples={3588,5000,3588};fresh(board,radio,fs,ResetKind::Cold,true);
|
||||
advance(1000);fresh(board,radio,fs,ResetKind::Crash);counts(0,0);
|
||||
} else if(test=="clean") {
|
||||
fresh(board,radio,fs,ResetKind::Cold,true);
|
||||
for(unsigned n=1;n<=2;++n) {advance(1000);fresh(board,radio,fs,ResetKind::Crash);counts(n,n);}
|
||||
meshRegulatorCleanReset();fresh(board,radio,fs,ResetKind::Crash);counts(0,0);
|
||||
// A clean-reset marker cannot discard a learned fallback that has not yet
|
||||
// reached flash because a write failed.
|
||||
for(unsigned n=1;n<=2;++n) {advance(1000);fresh(board,radio,fs,ResetKind::Crash);counts(n,n);}
|
||||
fs.fail_write=true;advance(1000);fresh(board,radio,fs,ResetKind::Crash);
|
||||
assert(regulatorFallbackActive(true) && regulatorFallbackActive(false));
|
||||
meshRegulatorCleanReset();
|
||||
resetGlobals();meshRegulatorCleanReset(); // early reboot before filesystem setup
|
||||
fs.fail_write=false;fresh(board,radio,fs,ResetKind::Crash);
|
||||
assert(regulatorFallbackActive(true) && regulatorFallbackActive(false));
|
||||
} else if(test=="atomic") {
|
||||
for(unsigned failure=0;failure<5;++failure) {
|
||||
fs=MockFS();fresh(board,radio,fs,ResetKind::Cold,true);
|
||||
command("set mcu.regulator ldo");const auto old=fs.files["/regulator_v1"];
|
||||
if(failure==0)fs.fail_write=true;
|
||||
if(failure==1)fs.fail_open=true;
|
||||
if(failure==2)fs.fail_rename_from="/regulator_v1";
|
||||
if(failure==3)fs.fail_rename_from="/regulator_v1.tmp";
|
||||
if(failure==4) {fs.fail_rename_from="/regulator_v1.tmp";fs.fail_restore=true;}
|
||||
assert(command("set radio.regulator ldo").find("save failed")!=std::string::npos);
|
||||
assert(requestedRegulatorMode(false)==RegulatorMode::Default);
|
||||
fs.fail_write=fs.fail_open=fs.fail_restore=false;fs.fail_rename_from.clear();
|
||||
fresh(board,radio,fs,ResetKind::Cold,true);
|
||||
assert(requestedRegulatorMode(true)==RegulatorMode::Ldo);
|
||||
assert(requestedRegulatorMode(false)==RegulatorMode::Default);
|
||||
assert(fs.files["/regulator_v1"]==old);
|
||||
}
|
||||
} else if(test=="corruption") {
|
||||
fresh(board,radio,fs,ResetKind::Cold,true);command("set mcu.regulator ldo");
|
||||
const auto good=fs.files["/regulator_v1"];
|
||||
for(size_t byte=0;byte<good.size();++byte) {
|
||||
fs.files["/regulator_v1"]=good;fs.files["/regulator_v1"][byte]^=1;
|
||||
fresh(board,radio,fs,ResetKind::Cold,true);
|
||||
assert(requestedRegulatorMode(true)==RegulatorMode::Default);
|
||||
assert(board.active==RegulatorMode::Dcdc);
|
||||
}
|
||||
fs.files["/regulator_v1"]=good;fs.files["/regulator_v1"].push_back(0);
|
||||
fresh(board,radio,fs,ResetKind::Cold,true);assert(requestedRegulatorMode(true)==RegulatorMode::Default);
|
||||
fs.files["/regulator_v1"]=good;fs.files["/regulator_v1"].pop_back();
|
||||
fs.files["/regulator_v1.bak"]=good;
|
||||
fresh(board,radio,fs,ResetKind::Cold,true);assert(board.active==RegulatorMode::Ldo);
|
||||
} else if(test=="unsupported") {
|
||||
board.modes=radio.modes=REGULATOR_LDO;board.default_mode=radio.default_mode=RegulatorMode::Ldo;
|
||||
fresh(board,radio,fs,ResetKind::Cold,true);
|
||||
assert(command("set mcu.regulator dcdc").find("unsupported")!=std::string::npos);
|
||||
assert(command("set radio.regulator dcdc").find("unsupported")!=std::string::npos);
|
||||
assert(board.active==RegulatorMode::Ldo && radio.active==RegulatorMode::Ldo);
|
||||
assert(!board.dcdc_applied && !radio.dcdc_applied);
|
||||
// A previously saved choice from a DC/DC-capable recipe cannot activate
|
||||
// absent circuitry if that file is encountered by an LDO-only recipe.
|
||||
board.modes=radio.modes=3;
|
||||
command("set mcu.regulator dcdc");command("set radio.regulator dcdc");
|
||||
board.modes=radio.modes=REGULATOR_LDO;
|
||||
board.dcdc_applied=radio.dcdc_applied=false;
|
||||
resetGlobals(true);resetKind(ResetKind::Cold);board.resetHardware();radio.resetHardware();
|
||||
(void)beginRegulatorSettings(board,radio,&fs);
|
||||
assert(!board.dcdc_applied && !radio.dcdc_applied);
|
||||
assert(board.active!=RegulatorMode::Dcdc && radio.active!=RegulatorMode::Dcdc);
|
||||
board.modes=radio.modes=0;
|
||||
assert(command("get mcu.regulator").find("unsupported")!=std::string::npos);
|
||||
assert(command("get radio.regulator").find("unsupported")!=std::string::npos);
|
||||
} else if(test=="bounds") {
|
||||
fresh(board,radio,fs,ResetKind::Cold,true);
|
||||
for(const char* text:{"get mcu.regulatorx","set radio.regulatorx ldo","get radio.regulator.extra","get mcu.regulato","set radio.regulator\tldo","other"}) {
|
||||
char reply[4]={'x','x','x','x'};assert(!handleRegulatorCommand(text,reply,sizeof(reply)));
|
||||
assert(reply[0]=='x' && reply[3]=='x');
|
||||
}
|
||||
for(const char* text:{"get mcu.regulator arg","set radio.regulator", "set radio.regulator dcdc extra","set mcu.regulator LDO"})
|
||||
assert(command(text).find("Error:")==0);
|
||||
for(size_t size:{0u,1u,2u,8u,32u}) {
|
||||
char guarded[40];memset(guarded,0x5a,sizeof(guarded));
|
||||
assert(handleRegulatorCommand("get radio.regulator",guarded+3,size));
|
||||
for(size_t i=0;i<3;++i)assert(guarded[i]==0x5a);
|
||||
for(size_t i=size+3;i<sizeof(guarded);++i)assert(guarded[i]==0x5a);
|
||||
if(size)assert(memchr(guarded+3,0,size));
|
||||
}
|
||||
} else if(test=="no_store") {
|
||||
resetGlobals(true);resetKind(ResetKind::Cold);
|
||||
assert(beginRegulatorSettings(board,radio,nullptr));radio.initialize();
|
||||
assert(command("set radio.regulator ldo").find("save failed")!=std::string::npos);
|
||||
assert(requestedRegulatorMode(false)==RegulatorMode::Default);
|
||||
assert(radio.active==RegulatorMode::Dcdc);
|
||||
} else if(test=="failed_radio") {
|
||||
fresh(board,radio,fs,ResetKind::Cold,true);
|
||||
for(unsigned n=1;n<=3;++n) {
|
||||
radio.active=RegulatorMode::Default; // radio initialization never succeeded
|
||||
advance(1000);fresh(board,radio,fs,ResetKind::Crash);counts(n,0);
|
||||
}
|
||||
assert(regulatorFallbackActive(true) && !regulatorFallbackActive(false));
|
||||
} else if(test=="platform_unsupported") {
|
||||
assert(!platformRecoverySupported());
|
||||
fresh(board,radio,fs,ResetKind::Cold,true);
|
||||
for(unsigned n=0;n<4;++n){advance(1000);fresh(board,radio,fs,ResetKind::Crash);counts(0,0);}
|
||||
assert(!regulatorRecoveryAvailable());
|
||||
assert(!regulatorFallbackActive(true) && !regulatorFallbackActive(false));
|
||||
} else if(test=="platform") {
|
||||
#if defined(ESP32)
|
||||
for(int reason:{ESP_RST_PANIC,ESP_RST_INT_WDT,ESP_RST_TASK_WDT,ESP_RST_WDT}) {
|
||||
mock_reset_reason=reason;assert(platformResetKind()==ResetKind::Crash);
|
||||
}
|
||||
mock_reset_reason=ESP_RST_SW;assert(platformResetKind()==ResetKind::Intentional);
|
||||
mock_reset_reason=ESP_RST_DEEPSLEEP;assert(platformResetKind()==ResetKind::Wake);
|
||||
for(int reason:{ESP_RST_POWERON,ESP_RST_EXT}) {mock_reset_reason=reason;assert(platformResetKind()==ResetKind::Cold);}
|
||||
mock_reset_reason=ESP_RST_BROWNOUT;assert(platformResetKind()==ResetKind::Unknown);
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
mock_watchdog_any=mock_watchdog_enabled=false;assert(platformResetKind()==ResetKind::Cold);
|
||||
mock_watchdog_any=true;assert(platformResetKind()==ResetKind::Intentional);
|
||||
mock_watchdog_enabled=true;assert(platformResetKind()==ResetKind::Crash);
|
||||
#endif
|
||||
assert(&platformRecoveryRecord()==&platformRecoveryRecord());
|
||||
} else if(test=="healthy") {
|
||||
fresh(board,radio,fs,ResetKind::Cold,true);advance(1000);fresh(board,radio,fs,ResetKind::Crash);counts(1,1);
|
||||
for(unsigned n=0;n<301;++n)advance(1000);
|
||||
fresh(board,radio,fs,ResetKind::Crash);counts(1,1);
|
||||
} else if(test=="retry") {
|
||||
fresh(board,radio,fs,ResetKind::Cold,true);
|
||||
for(unsigned n=1;n<=2;++n){advance(1000);fresh(board,radio,fs,ResetKind::Crash);counts(n,n);}
|
||||
fs.fail_write=true;advance(1000);fresh(board,radio,fs,ResetKind::Crash);
|
||||
assert(command("get radio.regulator").find("fallback not saved")!=std::string::npos);
|
||||
fs.fail_write=false;advance(30000);
|
||||
assert(command("get radio.regulator").find("fallback not saved")==std::string::npos);
|
||||
fresh(board,radio,fs,ResetKind::Cold,true);
|
||||
assert(board.active==RegulatorMode::Ldo && radio.active==RegulatorMode::Ldo);
|
||||
} else assert(false);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
"""Extract radio regulator declarations for native production-method harnesses."""
|
||||
from test_radio_receive_contract import method
|
||||
|
||||
|
||||
def regulator_members(source):
|
||||
start = source.index('{', source.index('class Custom')) + 1
|
||||
end = source.index(' uint32_t _preambleMillis', start)
|
||||
fields = source[start:end]
|
||||
methods = '\n'.join(method(source, signature) for signature in (
|
||||
'uint8_t regulatorModes(', 'mesh::power::RegulatorMode defaultRegulatorMode(',
|
||||
'bool configureBootRegulator(', 'mesh::power::RegulatorMode activeRegulatorMode('))
|
||||
return fields + '\n public:\n' + methods
|
||||
@@ -10,6 +10,7 @@ import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from radio_regulator_harness import regulator_members
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
@@ -32,6 +33,7 @@ HARNESS = r'''
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <helpers/radiolib/LR2021Band.h>
|
||||
#include <helpers/radiolib/RadioRegulatorState.h>
|
||||
@POLICY@
|
||||
constexpr bool ExpectDcdc = @EXPECT_DCDC@;
|
||||
static_assert((LR2021_USE_DCDC != 0)==ExpectDcdc,"unexpected board regulator default");
|
||||
@@ -103,6 +105,9 @@ class LR2021 {
|
||||
int16_t setRegulatorDCDC() {
|
||||
++regulator_calls;const int16_t result=step(2);if(!result)regulator_on=true;return result;
|
||||
}
|
||||
int16_t setRegulatorLDO() {
|
||||
++regulator_calls;const int16_t result=step(2);if(!result)regulator_on=false;return result;
|
||||
}
|
||||
int16_t setCRC(uint8_t value) { assert(value==0 || value==2);return step(3); }
|
||||
int16_t explicitHeader() { return step(4); }
|
||||
int16_t setRxBoostedGainMode(uint8_t value) { gain=value;return step(5); }
|
||||
@@ -117,6 +122,7 @@ class LR2021 {
|
||||
int16_t setSyncWord(uint8_t*,size_t length) { assert(length==0);return step(8); }
|
||||
};
|
||||
class CustomLR2021:public LR2021 {
|
||||
@REGULATOR@
|
||||
uint32_t _rf_switch_pins[Module::RFSWITCH_MAX_PINS]={};
|
||||
const Module::RfSwitchMode_t* _rf_switch_table=nullptr;
|
||||
@ROUTING@
|
||||
@@ -154,6 +160,27 @@ static void routed(const CustomLR2021& radio) {
|
||||
assert(radio.pulls[0]==1 && radio.pulls[1]==0);
|
||||
}
|
||||
int main() {
|
||||
using mesh::power::RegulatorMode;
|
||||
Module selection_module;
|
||||
CustomLR2021 selected(&selection_module);configure(selected);
|
||||
assert(selected.activeRegulatorMode()==RegulatorMode::Default && selected.calls.empty());
|
||||
assert(!selected.configureBootRegulator(static_cast<RegulatorMode>(3)));
|
||||
assert(selected.configureBootRegulator(RegulatorMode::Dcdc)==ExpectDcdc);
|
||||
assert(selected.configureBootRegulator(RegulatorMode::Ldo));
|
||||
assert(selected.calls.empty()); // selecting never touches SPI
|
||||
assert(selected.std_init() && !selected.regulator_on);
|
||||
assert(selected.activeRegulatorMode()==RegulatorMode::Ldo);
|
||||
assert(!selected.configureBootRegulator(RegulatorMode::Default));
|
||||
assert(selected.beginMorseReceive(2440.0f)==0 && !selected.regulator_on);
|
||||
assert(selected.activeRegulatorMode()==RegulatorMode::Ldo);
|
||||
assert(selected.std_init() && !selected.regulator_on); // recovery stays LDO
|
||||
selected.fail=ExpectDcdc?2:1;
|
||||
assert(!selected.std_init());
|
||||
assert(selected.activeRegulatorMode()==RegulatorMode::Default);
|
||||
assert(!selected.configureBootRegulator(RegulatorMode::Dcdc));
|
||||
selected.fail=0;
|
||||
assert(selected.std_init() && !selected.regulator_on);
|
||||
assert(selected.activeRegulatorMode()==RegulatorMode::Ldo);
|
||||
// Only verified/opted-in layouts reapply DC/DC after a native reset.
|
||||
Module module;CustomLR2021 radio(&module);configure(radio);SPIClass spi;
|
||||
assert(radio.std_init(&spi) && spi.started && radio.regulator_on==ExpectDcdc);
|
||||
@@ -225,6 +252,7 @@ class LR2021PowerTest(unittest.TestCase):
|
||||
self.assertIsNotNone(policy, "LR2021 board policy must remain explicit and default-safe")
|
||||
for marker, body in {
|
||||
"@POLICY@": policy.group(0),
|
||||
"@REGULATOR@": regulator_members(driver),
|
||||
"@EXPECT_DCDC@": "true" if "-DLR2021_USE_DCDC=1" in flags else "false",
|
||||
"@ROUTING@": method(driver, "int16_t restoreRfSwitchTable("),
|
||||
"@TABLE@": method(driver, "void setRfSwitchTable("),
|
||||
|
||||
@@ -11,6 +11,7 @@ import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from radio_regulator_harness import regulator_members
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
@@ -34,6 +35,7 @@ HARNESS = r'''
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <helpers/radiolib/LR2021Band.h>
|
||||
#include <helpers/radiolib/RadioRegulatorState.h>
|
||||
@POLICY@
|
||||
#define LR2021_TX_BUSY_TIMEOUT_MS 4UL
|
||||
#define LR2021_USE_DCDC 0
|
||||
@@ -195,6 +197,7 @@ public:
|
||||
int16_t begin(float,float,uint8_t,uint8_t,uint8_t,int8_t,uint16_t,float) {
|
||||
std::fill(masks,masks+7,0);applyMode(MODE_STBY);return 0;
|
||||
}
|
||||
int16_t setRegulatorDCDC() { assert(false); return -1; }
|
||||
int16_t setCRC(uint8_t) { return 0; }
|
||||
int16_t explicitHeader() { return 0; }
|
||||
int16_t clearRxFifo() { return 0; }
|
||||
@@ -203,6 +206,7 @@ public:
|
||||
}
|
||||
};
|
||||
class CustomLR2021:public LR2021 {
|
||||
@REGULATOR@
|
||||
public:
|
||||
using LR2021::LR2021;
|
||||
uint32_t _rf_switch_pins[Module::RFSWITCH_MAX_PINS]={};
|
||||
@@ -369,7 +373,7 @@ class LR2021TxPrearmTest(unittest.TestCase):
|
||||
cls.binaries = {}
|
||||
driver = (ROOT / "src/helpers/radiolib/CustomLR2021.h").read_text()
|
||||
board = (ROOT / "variants/meshnology_w12/target.cpp").read_text()
|
||||
source = HARNESS
|
||||
source = HARNESS.replace("@REGULATOR@", regulator_members(driver))
|
||||
for marker, signature in {
|
||||
"@MASK@": "int16_t setTxStandbyMask(",
|
||||
"@PREPARE@": "int16_t prepareTxRfSwitch(",
|
||||
|
||||
@@ -6,6 +6,7 @@ from test_morse_tx import compile_run, portable_radio_header
|
||||
from test_carrier_wave import production_harness
|
||||
from test_radio_receive_contract import method
|
||||
from morse_rx_floor_step_experiment import legacy_estimator
|
||||
from radio_regulator_harness import regulator_members
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
@@ -302,6 +303,8 @@ int main() {
|
||||
for chip in ("LR1110", "LR1121", "LR2021", "SX1276"):
|
||||
source = (ROOT / f"src/helpers/radiolib/Custom{chip}.h").read_text(encoding="utf-8")
|
||||
code = code.replace(f"@{chip}@", method(source, "int16_t beginMorseReceive("))
|
||||
if chip != "SX1276":
|
||||
code = code.replace(f"@{chip}_REGULATOR@", regulator_members(source))
|
||||
if chip == "LR2021":
|
||||
code = code.replace("@LR2021_RESTORE@", method(source, "int16_t restoreRfSwitchTable("))
|
||||
sx1262 = (ROOT / "src/helpers/radiolib/CustomSX1262.h").read_text(encoding="utf-8")
|
||||
|
||||
@@ -15,7 +15,7 @@ SANITIZERS = ["-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno
|
||||
def portable_radio_header():
|
||||
"""Use the exact Radio interface without unrelated packet/crypto mocks."""
|
||||
dispatcher = (ROOT / "src/Dispatcher.h").read_text(encoding="utf-8")
|
||||
return ("#pragma once\n#include <RadioProfiles.h>\n#include <helpers/MorseTx.h>\n#include <helpers/MorseRx.h>\nnamespace mesh {\n"
|
||||
return ("#pragma once\n#include <RadioProfiles.h>\n#include <helpers/MorseTx.h>\n#include <helpers/MorseRx.h>\n#include <helpers/RegulatorMode.h>\nnamespace mesh {\n"
|
||||
+ method(dispatcher, "enum class RadioParamApplyResult") + ";\n"
|
||||
+ method(dispatcher, "class Radio {") + ";\n}\n")
|
||||
|
||||
|
||||
@@ -77,8 +77,71 @@ class DfuSourcePatchTest(unittest.TestCase):
|
||||
self.assertLess(fixed.index("SCB_ICSR_PENDSVCLR_Msk"),
|
||||
fixed.index("sd_softdevice_vector_table_base_set"))
|
||||
self.assertLess(fixed.index("sd_softdevice_vector_table_base_set"),
|
||||
fixed.index("if (meshRegulatorCleanReset)"))
|
||||
self.assertLess(fixed.index("if (meshRegulatorCleanReset)"),
|
||||
fixed.index("mesh_nrf52_dfu_jump(NRF_UICR"))
|
||||
|
||||
def test_only_a_successful_dfu_handoff_marks_clean_with_optional_policy(self):
|
||||
compiler = shutil.which("c++") or shutil.which("g++")
|
||||
if not compiler:
|
||||
self.skipTest("host C++ compiler required for DFU clean-reset regression")
|
||||
code = r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
@DECLARATION@
|
||||
static unsigned clean_marks, jumps, vector_calls;
|
||||
static bool reject_vector;
|
||||
struct Uicr { uint32_t NRFFW[1] = {0xF4000}; } uicr;
|
||||
#define NRF_UICR (&uicr)
|
||||
#define VERIFY_STATUS(expression, result) do { if ((expression) != 0) return result; } while (0)
|
||||
uint32_t sd_softdevice_vector_table_base_set(uint32_t address) {
|
||||
assert(address == 0xF4000);
|
||||
++vector_calls;
|
||||
return reject_vector ? 1 : 0;
|
||||
}
|
||||
void mesh_nrf52_dfu_jump(uint32_t address) {
|
||||
assert(address == 0xF4000);
|
||||
assert(clean_marks == EXPECTED_MARKS);
|
||||
++jumps;
|
||||
}
|
||||
void attemptDfu(bool requested) {
|
||||
if (!requested) return; // Advertising or an aborted update is not a reboot.
|
||||
VERIFY_STATUS(sd_softdevice_vector_table_base_set(NRF_UICR->NRFFW[0]), );
|
||||
@JUMP@
|
||||
}
|
||||
#if WITH_POLICY
|
||||
extern "C" void meshRegulatorCleanReset() {
|
||||
assert(jumps == 0 && vector_calls == 2 && !reject_vector);
|
||||
++clean_marks;
|
||||
}
|
||||
#endif
|
||||
int main() {
|
||||
attemptDfu(false);
|
||||
assert(clean_marks == 0 && vector_calls == 0 && jumps == 0);
|
||||
reject_vector = true;
|
||||
attemptDfu(true);
|
||||
assert(clean_marks == 0 && vector_calls == 1 && jumps == 0);
|
||||
reject_vector = false;
|
||||
attemptDfu(true);
|
||||
assert(clean_marks == EXPECTED_MARKS && vector_calls == 2 && jumps == 1);
|
||||
}
|
||||
'''.replace("@DECLARATION@", patch.CLEAN_RESET_DECLARATION).replace("@JUMP@", patch.JUMP)
|
||||
with tempfile.TemporaryDirectory(prefix="meshcore-dfu-clean-reset-") as directory:
|
||||
cpp = Path(directory) / "fixture.cpp"
|
||||
cpp.write_text(code)
|
||||
for policy in (False, True):
|
||||
with self.subTest(policy=policy):
|
||||
binary = Path(directory) / f"test-{int(policy)}"
|
||||
built = subprocess.run([
|
||||
compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
||||
f"-DWITH_POLICY={int(policy)}", f"-DEXPECTED_MARKS={int(policy)}",
|
||||
str(cpp), "-o", str(binary),
|
||||
], capture_output=True, text=True, timeout=30)
|
||||
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
|
||||
tested = subprocess.run([str(binary)], capture_output=True,
|
||||
text=True, timeout=10)
|
||||
self.assertEqual(tested.returncode, 0, tested.stdout + tested.stderr)
|
||||
|
||||
def test_unknown_or_partially_patched_framework_fails_closed(self):
|
||||
for original in ("unrecognized framework", self.fixture().replace(
|
||||
patch.OLD_JUMP, patch.JUMP), self.fixture().replace(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run real board startup methods against counted nRF52 regulator SDK calls."""
|
||||
"""Board startup approves DC/DC hardware and defers activation to saved policy."""
|
||||
import configparser
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -58,25 +58,28 @@ void sd_power_dcdc_mode_set(int mode) {
|
||||
assert(base_calls == 1 && softdevice && mode == NRF_POWER_DCDC_ENABLE);
|
||||
++sdk_calls;
|
||||
}
|
||||
void requireDcdc() {
|
||||
void requireDeferredDcdc() {
|
||||
assert(base_calls == 1);
|
||||
assert(softdevice ? sdk_calls == 1 : power.DCDCEN == 1);
|
||||
assert(sdk_calls == 0 && power.DCDCEN == 0);
|
||||
}
|
||||
void pinMode(int, int) { requireDcdc(); }
|
||||
void digitalWrite(int, int) { requireDcdc(); }
|
||||
void pinMode(int, int) { requireDeferredDcdc(); }
|
||||
void digitalWrite(int, int) { requireDeferredDcdc(); }
|
||||
void delay(unsigned) {}
|
||||
void meshSolarStart() { requireDcdc(); }
|
||||
void meshSolarStart() { requireDeferredDcdc(); }
|
||||
struct Bus {
|
||||
void setPins(int, int) { requireDcdc(); }
|
||||
void begin() { requireDcdc(); }
|
||||
void setPins(int, int) { requireDeferredDcdc(); }
|
||||
void begin() { requireDeferredDcdc(); }
|
||||
} Wire;
|
||||
struct Uart { void begin(unsigned) { requireDcdc(); } } Serial;
|
||||
struct Uart { void begin(unsigned) { requireDeferredDcdc(); } } Serial;
|
||||
struct Peripheral {
|
||||
void begin() { requireDcdc(); }
|
||||
void init() { requireDcdc(); }
|
||||
void begin() { requireDeferredDcdc(); }
|
||||
void init() { requireDeferredDcdc(); }
|
||||
};
|
||||
struct Config {} power_config;
|
||||
struct NRF52Board { void begin() { ++base_calls; } };
|
||||
struct NRF52Board {
|
||||
bool cpu_dcdc_supported = false;
|
||||
void begin() { ++base_calls; }
|
||||
};
|
||||
struct NRF52BoardDCDC : NRF52Board { void begin(); };
|
||||
struct @BOARD@ : NRF52BoardDCDC {
|
||||
Peripheral periph_power, loRaFEMControl;
|
||||
@@ -84,7 +87,7 @@ struct @BOARD@ : NRF52BoardDCDC {
|
||||
void begin();
|
||||
void checkBootVoltage(const Config*) {
|
||||
++voltage_checks;
|
||||
requireDcdc();
|
||||
requireDeferredDcdc();
|
||||
}
|
||||
};
|
||||
@METHODS@
|
||||
@@ -94,8 +97,8 @@ int main() {
|
||||
@BOARD@ board;
|
||||
board.begin();
|
||||
assert(base_calls == 1 && voltage_checks == @VOLTAGE_CHECKS@);
|
||||
assert(sdk_calls == softdevice);
|
||||
assert(power.DCDCEN == (softdevice ? 0U : 1U));
|
||||
assert(board.cpu_dcdc_supported);
|
||||
assert(sdk_calls == 0 && power.DCDCEN == 0);
|
||||
}
|
||||
}
|
||||
'''
|
||||
@@ -122,14 +125,14 @@ class Nrf52BoardRegulatorTests(unittest.TestCase):
|
||||
result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
|
||||
def test_rak4631_startup_uses_dcdc_before_voltage_check(self):
|
||||
# Cover boot before BLE starts and an already-active SoftDevice. Also
|
||||
# prove an obsolete downstream flag cannot silently force LDO again.
|
||||
def test_rak4631_defers_dcdc_until_saved_policy_is_loaded(self):
|
||||
# Board startup must not force either saved mode before policy loads,
|
||||
# regardless of SoftDevice state or obsolete downstream flags.
|
||||
for flags in ((), ('-DRAK4631_COMPANION_FORCE_LDO=1',)):
|
||||
with self.subTest(flags=flags):
|
||||
self.run_startup('variants/rak4631/RAK4631Board.cpp', 'RAK4631Board', flags)
|
||||
|
||||
def test_verified_boards_initialize_dcdc_before_peripherals(self):
|
||||
def test_verified_boards_defer_dcdc_until_peripherals_and_policy_are_ready(self):
|
||||
for filename, board, has_voltage_check in VERIFIED_BOARDS:
|
||||
for power_management in (False, True):
|
||||
with self.subTest(board=board, power_management=power_management):
|
||||
|
||||
@@ -307,9 +307,10 @@ class Nrf52InternalBootloaderSelectorTest(unittest.TestCase):
|
||||
|
||||
class Nrf52HybridLinkerContractTest(unittest.TestCase):
|
||||
def test_real_linker_markers_do_not_confuse_late_data_with_reservation(self):
|
||||
compiler, linker, nm = (shutil.which(name) for name in ("cc", "ld", "nm"))
|
||||
if not all((compiler, linker, nm)):
|
||||
self.skipTest("native cc, ld and nm are required for linker fixtures")
|
||||
compiler, linker, nm, readelf = (
|
||||
shutil.which(name) for name in ("cc", "ld", "nm", "readelf"))
|
||||
if not all((compiler, linker, nm, readelf)):
|
||||
self.skipTest("native cc, ld, nm and readelf are required for linker fixtures")
|
||||
# A large framework registration prefix moves .data beyond the auth
|
||||
# ABI even in an unreserved map. Only the explicit region markers can
|
||||
# distinguish it from a genuinely reserved receiver layout.
|
||||
@@ -318,6 +319,8 @@ class Nrf52HybridLinkerContractTest(unittest.TestCase):
|
||||
.long 0
|
||||
.section .persistent_data,"aw"
|
||||
.long 0
|
||||
.section .regulator_recovery,"aw"
|
||||
.space 64
|
||||
.section .svc_data,"aw"
|
||||
.space 128
|
||||
.section .fs_data,"aw"
|
||||
@@ -329,14 +332,27 @@ class Nrf52HybridLinkerContractTest(unittest.TestCase):
|
||||
SECTIONS {
|
||||
.data : { __data_start__ = .; *(.data) } > RAM
|
||||
}
|
||||
__HeapBase = __data_start__ + SIZEOF(.data);
|
||||
__HeapLimit = ORIGIN(RAM) + LENGTH(RAM);
|
||||
"""
|
||||
with tempfile.TemporaryDirectory(prefix="meshcore-sd-auth-linker-") as directory:
|
||||
work = Path(directory)
|
||||
(work / "fixture.S").write_text(assembly, encoding="ascii")
|
||||
(work / "nrf52_common.ld").write_text(common, encoding="ascii")
|
||||
(work / "boards").mkdir()
|
||||
shutil.copyfile(ROOT / "boards/nrf52_heap_reserve.ld",
|
||||
work / "boards/nrf52_heap_reserve.ld")
|
||||
subprocess.run([compiler, "-c", str(work / "fixture.S"), "-o", str(work / "fixture.o")],
|
||||
check=True, capture_output=True, text=True, timeout=30)
|
||||
for suffix in ("v6", "v7", "v6_sd_ota", "v6_mota64", "v7_mota64"):
|
||||
(work / "overflow.S").write_text(
|
||||
assembly.replace('.section .regulator_recovery,"aw"\n.space 64',
|
||||
'.section .regulator_recovery,"aw"\n.space 65'),
|
||||
encoding="ascii")
|
||||
subprocess.run([compiler, "-c", str(work / "overflow.S"),
|
||||
"-o", str(work / "overflow.o")],
|
||||
check=True, capture_output=True, text=True, timeout=30)
|
||||
for suffix in ("v6", "v7", "v6_extrafs", "v7_extrafs",
|
||||
"v7_xiao_bootloader_ota", "v6_sd_ota", "v6_mota64", "v7_mota64"):
|
||||
with self.subTest(suffix=suffix):
|
||||
source = (ROOT / "boards" / f"nrf52840_s140_{suffix}.ld").read_text()
|
||||
# Native tools suffice: omit only the ARM C library group;
|
||||
@@ -349,13 +365,25 @@ SECTIONS {
|
||||
output = subprocess.check_output([nm, str(work / "fixture.elf")], text=True)
|
||||
symbols = {name: int(value, 16) for value, _kind, name in
|
||||
(line.split() for line in output.splitlines())}
|
||||
self.assertGreaterEqual(symbols["__data_start__"], 0x20006050)
|
||||
if suffix in ("v6", "v7"):
|
||||
self.assertGreaterEqual(symbols["__data_start__"], 0x20006090)
|
||||
self.assertEqual(symbols["__regulator_recovery_start__"], 0x20006050)
|
||||
self.assertEqual(symbols["__regulator_recovery_end__"], 0x20006090)
|
||||
section_info = subprocess.check_output(
|
||||
[readelf, "-SW", str(work / "fixture.elf")],
|
||||
text=True)
|
||||
self.assertRegex(section_info,
|
||||
r"\.regulator_recovery\s+NOBITS\s+0*20006050\s+\S+\s+0*40\s")
|
||||
if suffix in ("v6", "v7", "v6_extrafs", "v7_extrafs", "v7_xiao_bootloader_ota"):
|
||||
self.assertNotIn("__mota_sd_auth_start__", symbols)
|
||||
self.assertNotIn("__mota_sd_auth_end__", symbols)
|
||||
else:
|
||||
self.assertEqual(symbols["__mota_sd_auth_start__"], 0x20006008)
|
||||
self.assertEqual(symbols["__mota_sd_auth_end__"], 0x20006050)
|
||||
overflow = subprocess.run(
|
||||
[linker, "-T", "fixture.ld", "overflow.o", "-o", "overflow.elf"],
|
||||
cwd=work, text=True, capture_output=True, timeout=30)
|
||||
self.assertNotEqual(overflow.returncode, 0,
|
||||
"65-byte recovery record must fail the production link")
|
||||
|
||||
def test_sd_authorization_has_explicit_reserved_boundaries(self):
|
||||
for suffix in ("v6_sd_ota", "v6_mota64", "v7_mota64"):
|
||||
@@ -363,13 +391,14 @@ SECTIONS {
|
||||
path = ROOT / "boards" / f"nrf52840_s140_{suffix}.ld"
|
||||
regions = memory_regions(path)
|
||||
source = path.read_text(encoding="utf-8")
|
||||
self.assertEqual(regions["RAM"][0], 0x20006050)
|
||||
self.assertEqual(regions["RAM"][0], 0x20006090)
|
||||
self.assertEqual(regions["REGULATOR_RECOVERY_RAM"], (0x20006050, 64))
|
||||
self.assertEqual(regions["PERSISTENT_RAM"], (0x20006000, 80))
|
||||
self.assertIn("__mota_sd_auth_start__ = ", source)
|
||||
self.assertIn("__mota_sd_auth_end__ = ", source)
|
||||
self.assertIn("ASSERT(ORIGIN(RAM) >= __mota_sd_auth_end__,", source)
|
||||
if suffix == "v6_sd_ota":
|
||||
self.assertEqual(regions["RAM"][1], 0x20040000 - 0x20006050)
|
||||
self.assertEqual(regions["RAM"][1], 0x20040000 - 0x20006090)
|
||||
self.assertIn("ASSERT(__mota_sd_auth_start__ == 0x20006008,", source)
|
||||
self.assertIn("ASSERT(__mota_sd_auth_end__ == 0x20006050,", source)
|
||||
else:
|
||||
@@ -398,13 +427,15 @@ SECTIONS {
|
||||
self.assertEqual(hybrid["FLASH"], (flash_start, 0xED000 - flash_start))
|
||||
self.assertEqual(hybrid["PERSISTENT_RAM"], (0x20006000, 8 + auth_len))
|
||||
self.assertEqual(auth_start, 0x20006000 + 8)
|
||||
self.assertEqual(hybrid["RAM"][0], auth_start + auth_len)
|
||||
self.assertEqual(hybrid["REGULATOR_RECOVERY_RAM"], (auth_start + auth_len, 64))
|
||||
self.assertEqual(normal["REGULATOR_RECOVERY_RAM"], hybrid["REGULATOR_RECOVERY_RAM"])
|
||||
self.assertEqual(hybrid["RAM"][0], auth_start + auth_len + 64)
|
||||
self.assertEqual(hybrid["RAM"][0] + hybrid["RAM"][1], arena_start)
|
||||
self.assertEqual(hybrid["MOTA_RAM"], (arena_start, arena_len))
|
||||
self.assertEqual(arena_start + arena_len, 0x20040000)
|
||||
self.assertEqual(
|
||||
normal["RAM"][1] - hybrid["RAM"][1],
|
||||
arena_len + auth_len,
|
||||
arena_len,
|
||||
)
|
||||
|
||||
def test_linkers_export_and_assert_every_fixed_handoff_boundary(self):
|
||||
@@ -416,7 +447,8 @@ SECTIONS {
|
||||
"ASSERT(__mota_hybrid_handoff_start__ == 0x20006008,",
|
||||
"ASSERT(__mota_hybrid_handoff_end__ == 0x20006050,",
|
||||
"ASSERT(__mota_hybrid_handoff_end__ - __mota_hybrid_handoff_start__ == 72,",
|
||||
"ASSERT(ORIGIN(RAM) == __mota_hybrid_handoff_end__,",
|
||||
"ASSERT(ORIGIN(REGULATOR_RECOVERY_RAM) == __mota_hybrid_handoff_end__,",
|
||||
"ASSERT(ORIGIN(RAM) == __regulator_recovery_end__,",
|
||||
"ASSERT(ORIGIN(RAM) + LENGTH(RAM) == ORIGIN(MOTA_RAM),",
|
||||
"ASSERT(ORIGIN(MOTA_RAM) + LENGTH(MOTA_RAM) == 0x20040000,",
|
||||
".mota_ram (NOLOAD)",
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execute the production nRF52 CPU regulator and reset classification paths."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from cpp_source import body
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
BOARD = ROOT / "src/helpers/NRF52Board.cpp"
|
||||
|
||||
|
||||
def run_native(source):
|
||||
with tempfile.TemporaryDirectory(prefix="meshcore-nrf52-regulator-") as directory:
|
||||
cpp, binary = Path(directory) / "test.cpp", Path(directory) / "test"
|
||||
cpp.write_text(source, encoding="ascii")
|
||||
compiled = subprocess.run(
|
||||
[os.environ.get("CXX", "c++"), "-std=c++17", "-Wall", "-Wextra",
|
||||
"-Werror", "-I", str(ROOT / "src"), str(cpp), "-o", str(binary)],
|
||||
capture_output=True, text=True, timeout=60)
|
||||
if compiled.returncode:
|
||||
raise AssertionError(compiled.stdout + compiled.stderr)
|
||||
result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
|
||||
if result.returncode:
|
||||
raise AssertionError(result.stdout + result.stderr)
|
||||
|
||||
|
||||
class Nrf52RegulatorTests(unittest.TestCase):
|
||||
def test_saved_cpu_mode_uses_checked_sdk_and_rejects_unsupported_boards(self):
|
||||
source = BOARD.read_text()
|
||||
methods = "\n".join(body(source, signature) for signature in (
|
||||
"uint8_t NRF52Board::mcuRegulatorModes() const",
|
||||
"mesh::power::RegulatorMode NRF52Board::defaultMcuRegulatorMode() const",
|
||||
"mesh::power::RegulatorMode NRF52Board::activeMcuRegulatorMode() const",
|
||||
"bool NRF52Board::applyMcuRegulator(mesh::power::RegulatorMode mode)",
|
||||
))
|
||||
run_native(r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include "helpers/RegulatorMode.h"
|
||||
using mesh::power::RegulatorMode;
|
||||
constexpr uint32_t NRF_SUCCESS = 0;
|
||||
constexpr uint8_t NRF_POWER_DCDC_DISABLE = 0, NRF_POWER_DCDC_ENABLE = 1;
|
||||
static uint32_t query_result, setter_result;
|
||||
static uint8_t softdevice, requested;
|
||||
static unsigned setter_calls;
|
||||
struct Power { unsigned DCDCEN; } power;
|
||||
#define NRF_POWER (&power)
|
||||
namespace mesh_nrf52 {
|
||||
uint32_t softdeviceIsEnabled(uint8_t& enabled) {
|
||||
enabled = softdevice;
|
||||
return query_result;
|
||||
}
|
||||
}
|
||||
uint32_t sd_power_dcdc_mode_set(uint8_t mode) {
|
||||
assert(softdevice);
|
||||
requested = mode;
|
||||
++setter_calls;
|
||||
return setter_result;
|
||||
}
|
||||
struct NRF52Board {
|
||||
bool cpu_dcdc_supported = false;
|
||||
RegulatorMode cpu_regulator_mode = RegulatorMode::Ldo;
|
||||
uint8_t mcuRegulatorModes() const;
|
||||
RegulatorMode defaultMcuRegulatorMode() const;
|
||||
RegulatorMode activeMcuRegulatorMode() const;
|
||||
bool applyMcuRegulator(RegulatorMode);
|
||||
};
|
||||
@METHODS@
|
||||
int main() {
|
||||
NRF52Board board;
|
||||
power.DCDCEN = 77;
|
||||
assert(board.mcuRegulatorModes() == 0);
|
||||
assert(board.defaultMcuRegulatorMode() == RegulatorMode::Default);
|
||||
assert(!board.applyMcuRegulator(RegulatorMode::Dcdc));
|
||||
assert(!board.applyMcuRegulator(RegulatorMode::Ldo));
|
||||
assert(!board.applyMcuRegulator(RegulatorMode::Default));
|
||||
assert(power.DCDCEN == 77 && setter_calls == 0);
|
||||
board.cpu_dcdc_supported = true;
|
||||
assert(board.mcuRegulatorModes() == 3);
|
||||
assert(board.defaultMcuRegulatorMode() == RegulatorMode::Dcdc);
|
||||
assert(!board.applyMcuRegulator(static_cast<RegulatorMode>(255)));
|
||||
for (softdevice = 0; softdevice < 2; ++softdevice) {
|
||||
power.DCDCEN = 77;
|
||||
query_result = 5;
|
||||
assert(!board.applyMcuRegulator(RegulatorMode::Dcdc));
|
||||
assert(power.DCDCEN == 77 && setter_calls == 0);
|
||||
query_result = 0;
|
||||
setter_result = 0;
|
||||
assert(board.applyMcuRegulator(RegulatorMode::Default));
|
||||
assert(board.activeMcuRegulatorMode() == RegulatorMode::Dcdc);
|
||||
assert(softdevice ? requested == 1 : power.DCDCEN == 1);
|
||||
assert(board.applyMcuRegulator(RegulatorMode::Ldo));
|
||||
assert(board.activeMcuRegulatorMode() == RegulatorMode::Ldo);
|
||||
assert(softdevice ? requested == 0 : power.DCDCEN == 0);
|
||||
if (softdevice) {
|
||||
setter_result = 4;
|
||||
assert(!board.applyMcuRegulator(RegulatorMode::Dcdc));
|
||||
assert(board.activeMcuRegulatorMode() == RegulatorMode::Ldo);
|
||||
assert(power.DCDCEN == 77);
|
||||
}
|
||||
setter_calls = 0;
|
||||
}
|
||||
}
|
||||
'''.replace("@METHODS@", methods))
|
||||
|
||||
def test_reset_classifier_excludes_wakes_and_intentional_resets(self):
|
||||
method = body(BOARD.read_text(), "ResetKind platformResetKind()")
|
||||
run_native(r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include "helpers/RegulatorRecovery.h"
|
||||
using mesh::power::ResetKind;
|
||||
constexpr uint32_t NRF_SUCCESS = 0;
|
||||
#define POWER_RESETREAS_RESETPIN_Msk 1U
|
||||
#define POWER_RESETREAS_DOG_Msk 2U
|
||||
#define POWER_RESETREAS_SREQ_Msk 4U
|
||||
#define POWER_RESETREAS_LOCKUP_Msk 8U
|
||||
#define POWER_RESETREAS_OFF_Msk 16U
|
||||
#define POWER_RESETREAS_LPCOMP_Msk 32U
|
||||
#define POWER_RESETREAS_VBUS_Msk 64U
|
||||
#define POWER_RESETREAS_DIF_Msk 128U
|
||||
static uint32_t regulator_reset_reason;
|
||||
static uint8_t regulator_shutdown_reason;
|
||||
static uint32_t query_result = 3, clear_result = 7;
|
||||
static unsigned query_calls, clear_calls;
|
||||
struct Power { uint32_t RESETREAS = 0; } power;
|
||||
#define NRF_POWER (&power)
|
||||
namespace mesh_nrf52 {
|
||||
uint32_t softdeviceIsEnabled(uint8_t& enabled) {
|
||||
++query_calls;
|
||||
enabled = 1;
|
||||
return query_result;
|
||||
}
|
||||
}
|
||||
uint32_t sd_power_reset_reason_clr(uint32_t bits) {
|
||||
assert(bits == 0xFFFFFFFFUL);
|
||||
++clear_calls;
|
||||
return clear_result;
|
||||
}
|
||||
@METHOD@
|
||||
int main() {
|
||||
assert(platformResetKind() == ResetKind::Unknown);
|
||||
assert(query_calls == 1 && clear_calls == 0 && power.RESETREAS == 0);
|
||||
query_result = 0;
|
||||
assert(platformResetKind() == ResetKind::Unknown);
|
||||
assert(clear_calls == 1 && power.RESETREAS == 0);
|
||||
clear_result = 0;
|
||||
regulator_reset_reason = 0;
|
||||
assert(platformResetKind() == ResetKind::Cold);
|
||||
assert(clear_calls == 2);
|
||||
for (uint32_t reason : {2U, 4U, 8U, 2U | 4U}) {
|
||||
regulator_reset_reason = reason;
|
||||
assert(platformResetKind() == ResetKind::Crash);
|
||||
}
|
||||
for (uint32_t reason : {16U, 32U, 64U, 16U | 2U}) {
|
||||
regulator_reset_reason = reason;
|
||||
assert(platformResetKind() == ResetKind::Wake);
|
||||
}
|
||||
for (uint32_t reason : {1U, 128U, 1U | 2U}) {
|
||||
regulator_reset_reason = reason;
|
||||
assert(platformResetKind() == ResetKind::Intentional);
|
||||
}
|
||||
regulator_reset_reason = 8U;
|
||||
for (uint8_t shutdown : {0x4C, 0x55, 0x42}) {
|
||||
regulator_shutdown_reason = shutdown;
|
||||
assert(platformResetKind() == ResetKind::Intentional);
|
||||
}
|
||||
regulator_shutdown_reason = 0;
|
||||
regulator_reset_reason = 0x80000000UL;
|
||||
assert(platformResetKind() == ResetKind::Unknown);
|
||||
assert(clear_calls == 2);
|
||||
}
|
||||
'''.replace("@METHOD@", method).replace("#include <cstdint>",
|
||||
"#include <cstdint>\n#include <initializer_list>"))
|
||||
|
||||
def test_reset_acknowledgement_without_softdevice_uses_direct_register(self):
|
||||
method = body(BOARD.read_text(), "ResetKind platformResetKind()")
|
||||
run_native(r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include "helpers/RegulatorRecovery.h"
|
||||
using mesh::power::ResetKind;
|
||||
constexpr uint32_t NRF_SUCCESS = 0;
|
||||
#define POWER_RESETREAS_RESETPIN_Msk 1U
|
||||
#define POWER_RESETREAS_DOG_Msk 2U
|
||||
#define POWER_RESETREAS_SREQ_Msk 4U
|
||||
#define POWER_RESETREAS_LOCKUP_Msk 8U
|
||||
static uint32_t regulator_reset_reason;
|
||||
static uint8_t regulator_shutdown_reason;
|
||||
static unsigned query_calls, clear_calls;
|
||||
struct Power { uint32_t RESETREAS = 0; } power;
|
||||
#define NRF_POWER (&power)
|
||||
namespace mesh_nrf52 {
|
||||
uint32_t softdeviceIsEnabled(uint8_t& enabled) {
|
||||
++query_calls;
|
||||
enabled = 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
uint32_t sd_power_reset_reason_clr(uint32_t) {
|
||||
++clear_calls;
|
||||
return 9;
|
||||
}
|
||||
@METHOD@
|
||||
int main() {
|
||||
assert(platformResetKind() == ResetKind::Cold);
|
||||
assert(query_calls == 1 && clear_calls == 0);
|
||||
assert(power.RESETREAS == 0xFFFFFFFFUL);
|
||||
regulator_reset_reason = POWER_RESETREAS_DOG_Msk;
|
||||
assert(platformResetKind() == ResetKind::Crash);
|
||||
assert(query_calls == 1 && clear_calls == 0);
|
||||
}
|
||||
'''.replace("@METHOD@", method))
|
||||
|
||||
def test_clean_reboot_marker_is_optional_and_precedes_reset(self):
|
||||
method = body(BOARD.read_text(), "void NRF52Board::reboot()")
|
||||
run_native(r'''
|
||||
#include <cassert>
|
||||
static unsigned resets, clean_marks;
|
||||
static void (*meshRegulatorCleanReset)();
|
||||
void markClean() { assert(resets == 1); ++clean_marks; }
|
||||
void NVIC_SystemReset() {
|
||||
assert(resets == 0 || clean_marks == 1);
|
||||
++resets;
|
||||
}
|
||||
struct NRF52Board { void reboot(); };
|
||||
@METHOD@
|
||||
int main() {
|
||||
NRF52Board board;
|
||||
board.reboot();
|
||||
assert(resets == 1 && clean_marks == 0);
|
||||
meshRegulatorCleanReset = markClean;
|
||||
board.reboot();
|
||||
assert(resets == 2 && clean_marks == 1);
|
||||
}
|
||||
'''.replace("@METHOD@", method))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -5,6 +5,7 @@ import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from test_radio_receive_contract import method
|
||||
from radio_regulator_harness import regulator_members
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
HARNESS = r'''
|
||||
@@ -447,7 +448,7 @@ class ProfileScanTest(unittest.TestCase):
|
||||
(Path(folder)/'Arduino.h').write_text('#pragma once\n#include <cstdint>\n#include <cmath>\n')
|
||||
cpp.write_text(HARNESS.replace('@METHODS@',methods))
|
||||
result=subprocess.run([os.environ.get('CXX','g++'),'-std=c++17','-Wall','-Wextra',
|
||||
'-I',folder,'-I',str(ROOT/'src'),str(cpp),'-o',str(exe)],capture_output=True,text=True)
|
||||
'-I',folder,'-I',str(ROOT/'src'),'-I',str(ROOT/'src'),str(cpp),'-o',str(exe)],capture_output=True,text=True)
|
||||
self.assertEqual(result.returncode,0,result.stderr)
|
||||
result=subprocess.run([str(exe)],capture_output=True,text=True)
|
||||
self.assertEqual(result.returncode,0,result.stderr)
|
||||
@@ -458,6 +459,8 @@ class ProfileScanTest(unittest.TestCase):
|
||||
harness=r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <helpers/radiolib/RadioRegulatorState.h>
|
||||
#define MESH_DEBUG_PRINTLN(...) ((void)0)
|
||||
#define RADIOLIB_SX126X_SYNC_WORD_PRIVATE 0x12
|
||||
#define RADIOLIB_ERR_NONE 0
|
||||
struct SX1262 {
|
||||
@@ -470,6 +473,7 @@ struct SX1262 {
|
||||
}
|
||||
};
|
||||
struct CustomSX1262: SX1262 {
|
||||
@REGULATOR@
|
||||
bool _coldStandby=false;
|
||||
bool _tcxoWakePending=true;
|
||||
struct { bool invalidated=false; void invalidate() { invalidated=true; } } _profileSwitch;
|
||||
@@ -496,9 +500,9 @@ int main() {
|
||||
'''
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
cpp=Path(folder)/'test.cpp'; exe=Path(folder)/'test.exe'
|
||||
cpp.write_text('#include <initializer_list>\n'+harness.replace('@BEGIN@',begin))
|
||||
cpp.write_text('#include <initializer_list>\n'+harness.replace('@BEGIN@',begin).replace('@REGULATOR@',regulator_members(source)))
|
||||
result=subprocess.run([os.environ.get('CXX','g++'),'-std=c++17','-Wall','-Wextra',
|
||||
str(cpp),'-o',str(exe)],capture_output=True,text=True)
|
||||
'-I',str(ROOT/'src'),str(cpp),'-o',str(exe)],capture_output=True,text=True)
|
||||
self.assertEqual(result.returncode,0,result.stderr)
|
||||
result=subprocess.run([str(exe)],capture_output=True,text=True)
|
||||
self.assertEqual(result.returncode,0,result.stderr)
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
"""Execute boot selection, regulator errors, Morse resets and radio recovery."""
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
from test_morse_tx import compile_run
|
||||
from test_radio_receive_contract import method
|
||||
from radio_regulator_harness import regulator_members
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
HARNESS = r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <initializer_list>
|
||||
#include <helpers/radiolib/RadioRegulatorState.h>
|
||||
#define RADIOLIB_ERR_NONE 0
|
||||
#define RADIOLIB_ERR_UNKNOWN -16
|
||||
#define RADIOLIB_SX126X_SYNC_WORD_PRIVATE 0x12
|
||||
#define RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE 0x12
|
||||
#define RADIOLIB_LR2021_LORA_SYNC_WORD_PRIVATE 0x12
|
||||
#define LORA_FREQ 909.5f
|
||||
#define LORA_BW 500.0f
|
||||
#define LORA_SF 5
|
||||
#define LORA_CR 5
|
||||
#define LORA_TX_POWER 2
|
||||
#define MESH_DEBUG_PRINTLN(...) ((void)0)
|
||||
struct SPIClass {};
|
||||
struct Module {};
|
||||
namespace mesh {
|
||||
struct Logger { void print(const char*) {} void println(int) {} };
|
||||
inline Logger& usbLoggingPort() { static Logger logger;return logger; }
|
||||
struct MorseRx { static constexpr float ReceiverBitRateKbps=4.8f; };
|
||||
namespace lr2021 { inline bool isHighBand(float f) { return f>1900; } }
|
||||
}
|
||||
struct Native {
|
||||
int fail=0,spi=0,regulator_writes=0,modem_resets=0;
|
||||
bool useRegulatorLDO=true,standbyXOSC=true;
|
||||
float tcxoVoltage=1.6f;
|
||||
uint8_t syncWordLength=4,preambleDetLength=4,gainModeLf=1,gainModeHf=3;
|
||||
int step(int phase) { ++spi;return fail==phase ? -100-phase : 0; }
|
||||
int nativeReset() { ++modem_resets;useRegulatorLDO=true;return step(1); }
|
||||
int16_t begin(float,float,uint8_t,uint8_t,uint8_t,int8_t,uint16_t,float) {
|
||||
return nativeReset();
|
||||
}
|
||||
int16_t begin(float,float,uint8_t,uint8_t,uint8_t,int8_t,uint16_t,float,bool ldo) {
|
||||
assert(!standbyXOSC);
|
||||
int result=nativeReset();
|
||||
if(!result) { ++regulator_writes;result=step(2);if(!result)useRegulatorLDO=ldo; }
|
||||
return result;
|
||||
}
|
||||
virtual int16_t beginFSK(float,float,float,float,int8_t,uint16_t,float,bool ldo) {
|
||||
int result=nativeReset();
|
||||
if(!result) { ++regulator_writes;result=step(2);if(!result)useRegulatorLDO=ldo; }
|
||||
return result;
|
||||
}
|
||||
int16_t beginGFSK(float,float,float,float,int8_t,uint16_t,float) { return nativeReset(); }
|
||||
int16_t setRegulatorDCDC() {
|
||||
++regulator_writes;int result=step(2);if(!result)useRegulatorLDO=false;return result;
|
||||
}
|
||||
int16_t setRegulatorLDO() {
|
||||
++regulator_writes;int result=step(2);if(!result)useRegulatorLDO=true;return result;
|
||||
}
|
||||
int16_t applyMeshCoreTcxoDelay() { return step(3); }
|
||||
int16_t setCRC(int) { return step(4); }
|
||||
int16_t setRxBoostedGainMode(int) { return step(5); }
|
||||
int16_t setSyncWord(uint8_t*,size_t) { return step(6); }
|
||||
int16_t explicitHeader() { return step(7); }
|
||||
int16_t setDioAsRfSwitch(uint8_t,uint8_t,uint8_t,uint8_t,uint8_t,uint8_t,uint8_t,uint8_t) { return step(8); }
|
||||
struct Profile { void invalidate() {} };
|
||||
struct RfSwitch {
|
||||
template<class Callback> int restore(Callback callback) {
|
||||
const uint8_t modes[]={0,0,0,0,0,0,0};return callback(0,modes);
|
||||
}
|
||||
};
|
||||
};
|
||||
using @CHIP@=Native;
|
||||
class Custom:public @CHIP@ {
|
||||
@POLICY@
|
||||
bool _coldStandby=false,_tcxoWakePending=false,_profilePacketDirty=true,_rx_boosted=false;
|
||||
Profile _profileSwitch;
|
||||
RfSwitch _morse_rf_switch;
|
||||
public:
|
||||
int restoreRfSwitchTable() { return 0; }
|
||||
@INIT@
|
||||
@MORSE@
|
||||
int init() { return @INIT_CALL@; }
|
||||
int morse() { return @MORSE_CALL@; }
|
||||
};
|
||||
@WRAPPER@
|
||||
int main() {
|
||||
using mesh::power::RegulatorMode;
|
||||
constexpr bool supported_dcdc=@DCDC@;
|
||||
for (auto requested:{RegulatorMode::Default,RegulatorMode::Ldo,RegulatorMode::Dcdc}) {
|
||||
Custom radio;Wrapper wrapper{&radio};
|
||||
assert(wrapper.activeRegulatorMode()==RegulatorMode::Default && radio.spi==0);
|
||||
assert(!wrapper.configureBootRegulator(static_cast<RegulatorMode>(3)));
|
||||
assert(wrapper.regulatorModes()==radio.regulatorModes());
|
||||
const bool supported=requested!=RegulatorMode::Dcdc || supported_dcdc;
|
||||
assert(wrapper.configureBootRegulator(requested)==supported);
|
||||
assert(radio.spi==0); // boot configuration cannot send commands mid-RX/TX
|
||||
if(!supported) continue;
|
||||
const auto expected=requested==RegulatorMode::Default ? wrapper.defaultRegulatorMode() : requested;
|
||||
assert(radio.init()==0 && wrapper.activeRegulatorMode()==expected);
|
||||
assert(radio.useRegulatorLDO==(expected==RegulatorMode::Ldo));
|
||||
assert(!wrapper.configureBootRegulator(RegulatorMode::Default));
|
||||
// Both transitions reset the native modem; the saved choice survives each.
|
||||
assert(radio.morse()==0 && wrapper.activeRegulatorMode()==expected);
|
||||
assert(radio.useRegulatorLDO==(expected==RegulatorMode::Ldo));
|
||||
assert(radio.init()==0 && wrapper.activeRegulatorMode()==expected);
|
||||
assert(radio.useRegulatorLDO==(expected==RegulatorMode::Ldo));
|
||||
for(int fault:{1,2}) {
|
||||
if(fault==2 && @SKIP_LDO_WRITE@) continue;
|
||||
radio.fail=fault;
|
||||
assert(radio.morse()!=0);
|
||||
assert(wrapper.activeRegulatorMode()==RegulatorMode::Default);
|
||||
assert(!wrapper.configureBootRegulator(RegulatorMode::Ldo));
|
||||
radio.fail=0;
|
||||
assert(radio.init()==0 && wrapper.activeRegulatorMode()==expected);
|
||||
assert(radio.useRegulatorLDO==(expected==RegulatorMode::Ldo));
|
||||
radio.fail=fault;
|
||||
assert(radio.init()!=0 && wrapper.activeRegulatorMode()==RegulatorMode::Default);
|
||||
radio.fail=0;
|
||||
assert(radio.init()==0 && wrapper.activeRegulatorMode()==expected);
|
||||
}
|
||||
}
|
||||
// A failed first init must lock the boot policy as firmly as a successful one.
|
||||
Custom failed;
|
||||
assert(failed.configureBootRegulator(RegulatorMode::Ldo));failed.fail=1;
|
||||
assert(failed.init()!=0 && !failed.configureBootRegulator(RegulatorMode::Default));
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class RadioRegulatorTests(unittest.TestCase):
|
||||
def exercise(self, chip, flags=(), dcdc=True):
|
||||
source = (ROOT / f'src/helpers/radiolib/Custom{chip}.h').read_text()
|
||||
if chip == 'LR2021':
|
||||
initialize = method(source, 'bool std_init(')
|
||||
init_call = 'std_init() ? 0 : -1'
|
||||
else:
|
||||
initialize = method(source, 'int16_t begin(')
|
||||
init_call = 'begin()'
|
||||
if chip == 'SX1262':
|
||||
morse = method(source, 'int16_t beginFSK(')
|
||||
morse_call = 'beginFSK()'
|
||||
else:
|
||||
morse = method(source, 'int16_t beginMorseReceive(')
|
||||
morse_call = 'beginMorseReceive(902.1f)'
|
||||
wrapper_source = (ROOT / f'src/helpers/radiolib/Custom{chip}Wrapper.h').read_text()
|
||||
delegates = '\n'.join(method(wrapper_source, signature).replace(' override', '')
|
||||
for signature in ('uint8_t regulatorModes(',
|
||||
'mesh::power::RegulatorMode defaultRegulatorMode(',
|
||||
'bool configureBootRegulator(', 'mesh::power::RegulatorMode activeRegulatorMode('))
|
||||
wrapper = f'using Custom{chip}=Custom;\nstruct Wrapper {{ Custom* _radio;\n{delegates}\n}};'
|
||||
code = HARNESS.replace('@WRAPPER@', wrapper)
|
||||
for marker, value in {'@CHIP@': chip, '@POLICY@': regulator_members(source),
|
||||
'@INIT@': initialize, '@MORSE@': morse, '@INIT_CALL@': init_call,
|
||||
'@MORSE_CALL@': morse_call, '@DCDC@': str(dcdc).lower(),
|
||||
'@SKIP_LDO_WRITE@': str(chip == 'LR2021' and not dcdc).lower()}.items():
|
||||
code = code.replace(marker, value)
|
||||
compile_run(self, code, flags=('-Wno-unused-parameter', *flags))
|
||||
|
||||
def test_sx1262_boot_selection_morse_failures_and_recovery(self):
|
||||
self.exercise('SX1262')
|
||||
|
||||
def test_sx1262_forced_ldo_recipe_cannot_enable_missing_dcdc_network(self):
|
||||
self.exercise('SX1262', ('-DSX126X_USE_REGULATOR_LDO=1',), dcdc=False)
|
||||
|
||||
def test_lr1110_boot_selection_morse_failures_and_recovery(self):
|
||||
self.exercise('LR1110')
|
||||
|
||||
def test_lr1121_boot_selection_morse_failures_and_recovery(self):
|
||||
self.exercise('LR1121')
|
||||
|
||||
def test_lr2021_verified_layout_boot_selection_morse_failures_and_recovery(self):
|
||||
self.exercise('LR2021', ('-DLR2021_USE_DCDC=1',))
|
||||
|
||||
def test_lr2021_unverified_layout_cannot_enable_simo(self):
|
||||
self.exercise('LR2021', ('-DLR2021_USE_DCDC=0',), dcdc=False)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -42,6 +42,7 @@ class RakSdReceiverTest(unittest.TestCase):
|
||||
"void ota_reboot_to_apply() { // public:",
|
||||
))
|
||||
run_cpp(r'''
|
||||
extern "C" void meshRegulatorCleanReset() __attribute__((weak));
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include "src/helpers/ota/OtaBlInfo.h"
|
||||
@@ -127,6 +128,7 @@ int main() {
|
||||
end = cli.index("#elif defined(OTA_QSPI_STORE)", error)
|
||||
gate = cli[cli.index("\n", start) + 1:end]
|
||||
run_cpp(r'''
|
||||
extern "C" void meshRegulatorCleanReset() __attribute__((weak));
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
@@ -169,6 +171,7 @@ int main() {
|
||||
self.assertNotIn("ota_acquire_context", body)
|
||||
self.assertNotIn("ota_ctx()", body)
|
||||
run_cpp(r'''
|
||||
extern "C" void meshRegulatorCleanReset() __attribute__((weak));
|
||||
#include <cassert>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
@@ -216,6 +219,7 @@ int main() {
|
||||
# Full Companion uses the separate stable SD archive, not an adaptive
|
||||
# receiver. Compile the same production status block in both roles.
|
||||
run_cpp(r'''
|
||||
extern "C" void meshRegulatorCleanReset() __attribute__((weak));
|
||||
#include <cassert>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
@@ -304,6 +308,7 @@ int main() {
|
||||
|
||||
def test_exact_supplement_preserves_legacy_application_and_update_views(self):
|
||||
run_cpp(r'''
|
||||
extern "C" void meshRegulatorCleanReset() __attribute__((weak));
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <initializer_list>
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
"""Exercise the production retained-RAM regulator recovery policy on the host."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_serial_wifi_sessions import SANITIZER_FLAGS
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
HARNESS = r'''
|
||||
#include <helpers/RegulatorRecovery.h>
|
||||
#include <cassert>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <type_traits>
|
||||
using namespace mesh::power;
|
||||
|
||||
static_assert(std::is_trivial<RecoveryRecord>::value, "retention must be POD");
|
||||
static_assert(std::is_standard_layout<RecoveryRecord>::value, "retention layout");
|
||||
|
||||
void initial(RecoveryRecord& r, uint8_t domains = RECOVERY_DOMAINS,
|
||||
uint8_t percent = 50, bool valid = true, uint32_t now = 0) {
|
||||
RecoveryPolicy p(r);
|
||||
p.begin(ResetKind::Cold, 0);
|
||||
p.arm(domains, now, percent, valid);
|
||||
}
|
||||
void counted(RecoveryRecord& r, unsigned crashes, uint8_t domains = RECOVERY_DOMAINS) {
|
||||
initial(r, domains);
|
||||
for (unsigned i = 0; i < crashes; ++i) {
|
||||
RecoveryPolicy boot(r);
|
||||
boot.begin(ResetKind::Crash, 0);
|
||||
boot.arm(domains, 0, 50, true);
|
||||
}
|
||||
}
|
||||
void assertCounts(const RecoveryPolicy& p, unsigned mcu, unsigned radio, unsigned fallback = 0) {
|
||||
assert(p.crashCount(RECOVERY_MCU) == mcu);
|
||||
assert(p.crashCount(RECOVERY_RADIO) == radio);
|
||||
assert(p.fallbackMask() == fallback);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
assert(argc == 2);
|
||||
const int scenario = std::atoi(argv[1]);
|
||||
RecoveryRecord r{};
|
||||
if (scenario == 0) {
|
||||
initial(r);
|
||||
for (unsigned n = 1; n <= 3; ++n) {
|
||||
RecoveryPolicy boot(r); boot.begin(ResetKind::Crash, 0);
|
||||
assertCounts(boot, n, n, n == 3 ? RECOVERY_DOMAINS : 0);
|
||||
boot.arm(RECOVERY_DOMAINS, 10, 50, true);
|
||||
}
|
||||
// A learned LDO fallback never flips back to DC-DC after more resets.
|
||||
for (unsigned n = 0; n < 4; ++n) {
|
||||
RecoveryPolicy boot(r); boot.begin(ResetKind::Crash, 0);
|
||||
assertCounts(boot, 0, 0, RECOVERY_DOMAINS);
|
||||
boot.arm(RECOVERY_DOMAINS, 10, 100, true);
|
||||
}
|
||||
} else if (scenario == 1) {
|
||||
// Battery threshold is inclusive at 50%; a low sample breaks a streak.
|
||||
counted(r, 2);
|
||||
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); p.clearDomain(RECOVERY_DOMAINS);
|
||||
p.arm(RECOVERY_DOMAINS, 0, 49, true); }
|
||||
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 0, 0);
|
||||
p.arm(RECOVERY_DOMAINS, 0, 50, true); }
|
||||
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 1, 1); }
|
||||
for (unsigned percent : {0u, 49u, 101u, 255u}) {
|
||||
initial(r, RECOVERY_DOMAINS, percent, true);
|
||||
RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 0, 0);
|
||||
}
|
||||
} else if (scenario == 2) {
|
||||
// USB voltage, a missing battery, or an unknown percentage is not evidence.
|
||||
for (unsigned percent : {0u, 50u, 100u, 255u}) {
|
||||
initial(r, RECOVERY_DOMAINS, percent, false);
|
||||
RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 0, 0);
|
||||
}
|
||||
initial(r);
|
||||
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 1, 1);
|
||||
p.arm(RECOVERY_DOMAINS, 0, 90, true); p.updateBattery(100, 90, false);
|
||||
p.heartbeat(200); }
|
||||
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 0, 0); }
|
||||
} else if (scenario == 3) {
|
||||
for (uint32_t age : {59999u, 60000u, 60001u, 299999u}) {
|
||||
initial(r, RECOVERY_DOMAINS, 50, true, 1000);
|
||||
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); p.arm(RECOVERY_DOMAINS, 1000, 50, true);
|
||||
p.heartbeat(1000 + age); }
|
||||
RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0);
|
||||
assertCounts(p, age <= 60000 ? 2 : 0, age <= 60000 ? 2 : 0);
|
||||
}
|
||||
// A fresh new sample, rather than an old value re-used by a heartbeat,
|
||||
// restores eligibility even when battery measurements are infrequent.
|
||||
initial(r);
|
||||
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); p.arm(RECOVERY_DOMAINS, 0, 50, true);
|
||||
p.heartbeat(70000); p.updateBattery(70000, 51, true); p.heartbeat(70001); }
|
||||
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 2, 2); }
|
||||
} else if (scenario == 4) {
|
||||
for (ResetKind reset : {ResetKind::Cold, ResetKind::Intentional, ResetKind::Wake,
|
||||
ResetKind::Unknown}) {
|
||||
counted(r, 2);
|
||||
RecoveryPolicy p(r); p.begin(reset, 0); assertCounts(p, 0, 0);
|
||||
p.arm(RECOVERY_DOMAINS, 0, 50, true);
|
||||
RecoveryPolicy next(r); next.begin(ResetKind::Crash, 0); assertCounts(next, 1, 1);
|
||||
}
|
||||
} else if (scenario == 5) {
|
||||
counted(r, 2);
|
||||
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); p.clearDomain(RECOVERY_DOMAINS);
|
||||
p.arm(RECOVERY_DOMAINS, 0, 90, true); p.markCleanReset(); }
|
||||
// A clean marker beats even a hardware reset cause labelled as a crash.
|
||||
RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 0, 0);
|
||||
} else if (scenario == 6) {
|
||||
counted(r, 2);
|
||||
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 3, 3, RECOVERY_DOMAINS); }
|
||||
// Sticky fallback survives power loss, sleep, software reset, unknown cause.
|
||||
for (ResetKind reset : {ResetKind::Cold, ResetKind::Intentional, ResetKind::Wake,
|
||||
ResetKind::Unknown, ResetKind::Crash}) {
|
||||
RecoveryPolicy p(r); p.begin(reset, 0); assertCounts(p, 0, 0, RECOVERY_DOMAINS);
|
||||
}
|
||||
// With retention lost, the flash-persisted fallback still applies.
|
||||
std::memset(&r, 0xff, sizeof(r));
|
||||
RecoveryPolicy p(r); p.begin(ResetKind::Cold, RECOVERY_RADIO | 0x80);
|
||||
assertCounts(p, 0, 0, RECOVERY_RADIO);
|
||||
} else if (scenario == 7) {
|
||||
// Every single corrupted bit of a valid 32-byte record is fail-closed.
|
||||
initial(r);
|
||||
const RecoveryRecord valid = r;
|
||||
for (size_t byte = 0; byte < sizeof(r); ++byte) {
|
||||
for (unsigned bit = 0; bit < 8; ++bit) {
|
||||
r = valid;
|
||||
reinterpret_cast<unsigned char*>(&r)[byte] ^= 1u << bit;
|
||||
RecoveryPolicy p(r); p.begin(ResetKind::Crash, RECOVERY_RADIO);
|
||||
assertCounts(p, 0, 0, RECOVERY_RADIO);
|
||||
}
|
||||
}
|
||||
// Uninitialised memory and an interrupted commit cannot create a streak.
|
||||
for (unsigned fill : {0u, 0x55u, 0xaau, 0xffu}) {
|
||||
std::memset(&r, fill, sizeof(r));
|
||||
RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 0, 0);
|
||||
}
|
||||
r = valid; r.magic = 0;
|
||||
RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 0, 0);
|
||||
} else if (scenario == 8) {
|
||||
counted(r, 1);
|
||||
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 2, 2);
|
||||
p.arm(RECOVERY_DOMAINS, 123, 90, true);
|
||||
p.tick(123 + 299999, 90, true); assertCounts(p, 2, 2);
|
||||
p.tick(123 + 300000, 90, true); assertCounts(p, 0, 0);
|
||||
p.tick(123 + 301000, 90, true); assertCounts(p, 0, 0); }
|
||||
// Healthy operation breaks the streak; a later crash starts a new one.
|
||||
RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 1, 1);
|
||||
} else if (scenario == 9) {
|
||||
// Both the stability window and sample age work across millis() rollover.
|
||||
const uint32_t start = 0xffff0000u;
|
||||
initial(r, RECOVERY_DOMAINS, 90, true, start);
|
||||
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); p.arm(RECOVERY_DOMAINS, start, 90, true);
|
||||
p.tick(start + 299999u, 90, true); assertCounts(p, 1, 1);
|
||||
p.tick(start + 300000u, 90, true); assertCounts(p, 0, 0); }
|
||||
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 1, 1); }
|
||||
for (uint32_t age : {60000u, 60001u}) {
|
||||
initial(r, RECOVERY_DOMAINS, 90, true, 0xfffffff0u);
|
||||
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0);
|
||||
p.arm(RECOVERY_DOMAINS, 0xfffffff0u, 90, true); p.heartbeat(0xfffffff0u + age); }
|
||||
RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0);
|
||||
assertCounts(p, age <= 60000 ? 2 : 0, age <= 60000 ? 2 : 0);
|
||||
}
|
||||
} else if (scenario == 10) {
|
||||
// A radio DC-DC streak does not accuse a processor already using LDO.
|
||||
counted(r, 2, RECOVERY_RADIO);
|
||||
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 0, 3, RECOVERY_RADIO);
|
||||
p.arm(RECOVERY_MCU, 0, 90, true); }
|
||||
for (unsigned n = 1; n <= 3; ++n) {
|
||||
RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0);
|
||||
assertCounts(p, n, 0, RECOVERY_RADIO | (n == 3 ? RECOVERY_MCU : 0));
|
||||
if (n < 3) p.arm(RECOVERY_MCU, 0, 90, true);
|
||||
}
|
||||
} else if (scenario == 11) {
|
||||
// Disabling one domain breaks only its consecutive-crash streak.
|
||||
counted(r, 1);
|
||||
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 2, 2);
|
||||
p.arm(RECOVERY_RADIO, 0, 90, true); }
|
||||
RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 0, 3, RECOVERY_RADIO);
|
||||
assert(p.crashCount(RECOVERY_DOMAINS) == 0 && p.crashCount(0x80) == 0);
|
||||
} else if (scenario == 12) {
|
||||
counted(r, 2);
|
||||
RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 3, 3, RECOVERY_DOMAINS);
|
||||
p.clearDomain(RECOVERY_RADIO | 0x80); assertCounts(p, 3, 0, RECOVERY_MCU);
|
||||
p.arm(RECOVERY_RADIO, 0, 90, true);
|
||||
RecoveryPolicy next(r); next.begin(ResetKind::Crash, RECOVERY_MCU);
|
||||
assertCounts(next, 0, 1, RECOVERY_MCU);
|
||||
next.clearDomain(RECOVERY_MCU); assertCounts(next, 0, 1);
|
||||
next.clearDomain(0); assertCounts(next, 0, 1);
|
||||
} else if (scenario == 13) {
|
||||
initial(r);
|
||||
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 1, 1); }
|
||||
// Crash before the next arm() cannot blame a regulator not yet enabled.
|
||||
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 0, 0);
|
||||
p.arm(0, 0, 90, true); }
|
||||
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 0, 0); }
|
||||
} else if (scenario == 14) {
|
||||
// Independently persisted and retained fallbacks are merged, never toggled.
|
||||
counted(r, 2, RECOVERY_MCU);
|
||||
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, RECOVERY_RADIO);
|
||||
assertCounts(p, 3, 0, RECOVERY_DOMAINS); }
|
||||
RecoveryPolicy p(r); p.begin(ResetKind::Cold, RECOVERY_MCU);
|
||||
assertCounts(p, 0, 0, RECOVERY_DOMAINS);
|
||||
} else if (scenario == 15) {
|
||||
initial(r);
|
||||
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); p.arm(RECOVERY_DOMAINS, 100, 90, true);
|
||||
p.updateBattery(101, 90, true); }
|
||||
// A sample apparently newer than the last heartbeat is conservative until
|
||||
// that heartbeat catches up; subtraction cannot turn it into a fresh age.
|
||||
{ RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 0, 0); }
|
||||
} else assert(false);
|
||||
std::cout << "PASS: regulator recovery " << scenario << '\n';
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class RegulatorRecoveryTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
|
||||
if compiler is None:
|
||||
raise unittest.SkipTest("a host C++11 compiler is required")
|
||||
cls.folder = tempfile.TemporaryDirectory(prefix="regulator-recovery-")
|
||||
cls.addClassCleanup(cls.folder.cleanup)
|
||||
work = Path(cls.folder.name)
|
||||
source = work / "test.cpp"
|
||||
source.write_text(HARNESS, encoding="utf-8")
|
||||
cls.binary = work / "test.exe"
|
||||
built = subprocess.run(
|
||||
[compiler, "-std=c++11", "-Wall", "-Wextra", "-Werror", *SANITIZER_FLAGS,
|
||||
"-I" + str(ROOT / "src"), str(source), "-o", str(cls.binary)],
|
||||
capture_output=True, text=True, timeout=60,
|
||||
)
|
||||
if built.returncode:
|
||||
raise AssertionError(built.stdout + built.stderr)
|
||||
|
||||
def test_retained_state_machine(self):
|
||||
scenarios = (
|
||||
"third crash latches fallback and never reverses it",
|
||||
"50 percent threshold and invalid percentage",
|
||||
"unknown battery and USB-only readings do not count",
|
||||
"fresh sample boundary and heartbeat timestamp separation",
|
||||
"cold intentional wake and unknown causes break streak",
|
||||
"clean reset marker defeats misleading reset reason",
|
||||
"sticky fallback survives normal resets and retention loss",
|
||||
"every record bit corruption fails closed",
|
||||
"five minute stability clears the streak",
|
||||
"stability and freshness survive timer rollover",
|
||||
"radio and MCU learn fallback independently",
|
||||
"inactive regulator does not accrue crashes",
|
||||
"manual domain change clears only its fallback and count",
|
||||
"a boot is disarmed until the actual modes are selected",
|
||||
"saved and retained fallback masks merge",
|
||||
"future sample is ineligible until heartbeat catches up",
|
||||
)
|
||||
for number, scenario in enumerate(scenarios):
|
||||
with self.subTest(scenario=scenario):
|
||||
checked = subprocess.run([str(self.binary), str(number)],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
|
||||
self.assertIn(f"PASS: regulator recovery {number}", checked.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,237 @@
|
||||
"""Execute the real regulator settings and retained-platform implementations."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from test_radio_receive_contract import method
|
||||
from test_serial_wifi_sessions import SANITIZER_FLAGS
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
FILESYSTEM = r'''
|
||||
#pragma once
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
struct MockFS;
|
||||
struct File {
|
||||
MockFS* fs=nullptr;std::string name;size_t cursor=0;bool valid=false;
|
||||
File()=default;explicit File(MockFS& fs):fs(&fs) {}
|
||||
File(MockFS* fs,const std::string& name,bool valid):fs(fs),name(name),valid(valid) {}
|
||||
bool open(const char*,int);
|
||||
explicit operator bool() const { return valid; }
|
||||
size_t size() const;
|
||||
size_t read(uint8_t*,size_t);
|
||||
size_t write(const uint8_t*,size_t);
|
||||
void close() {}
|
||||
};
|
||||
struct MockFS {
|
||||
std::map<std::string,std::vector<uint8_t>> files;
|
||||
bool fail_open=false,fail_write=false,fail_restore=false;
|
||||
std::string fail_rename_from;
|
||||
unsigned writes=0,renames=0;
|
||||
bool exists(const char* name) const { return files.count(name); }
|
||||
bool remove(const char* name) { return files.erase(name)!=0; }
|
||||
bool rename(const char* from,const char* to) {
|
||||
++renames;
|
||||
if(fail_rename_from==from || (fail_restore && std::string(from)=="/regulator_v1.bak"))return false;
|
||||
if(!files.count(from)||files.count(to))return false;
|
||||
files[to]=files[from];files.erase(from);return true;
|
||||
}
|
||||
File open(const char* name,const char* mode,bool=false) {
|
||||
if(fail_open)return File();
|
||||
if(*mode=='w'){ files[name].clear();return File(this,name,true); }
|
||||
return File(this,name,exists(name));
|
||||
}
|
||||
};
|
||||
inline bool File::open(const char* key,int flags) {
|
||||
File f=fs->open(key,flags?"w":"r");*this=f;return valid;
|
||||
}
|
||||
inline size_t File::size() const { return valid ? fs->files[name].size() : 0; }
|
||||
inline size_t File::read(uint8_t* bytes,size_t length) {
|
||||
if(!valid)return 0;
|
||||
const auto& data=fs->files[name];length=std::min(length,data.size()-cursor);
|
||||
memcpy(bytes,data.data()+cursor,length);cursor+=length;return length;
|
||||
}
|
||||
inline size_t File::write(const uint8_t* bytes,size_t length) {
|
||||
if(!valid)return 0;
|
||||
++fs->writes;if(fs->fail_write)length/=2;
|
||||
auto& data=fs->files[name];data.insert(data.end(),bytes,bytes+length);return length;
|
||||
}
|
||||
#define FILESYSTEM MockFS
|
||||
#define FILE_O_READ 0
|
||||
#define FILE_O_WRITE 1
|
||||
'''
|
||||
|
||||
DISPATCHER = r'''
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include <helpers/RegulatorMode.h>
|
||||
namespace mesh {
|
||||
struct MainBoard {
|
||||
uint8_t modes=3;power::RegulatorMode default_mode=power::RegulatorMode::Dcdc;
|
||||
power::RegulatorMode active=power::RegulatorMode::Default;
|
||||
unsigned applies=0;bool dcdc_applied=false;uint16_t mv=3600;
|
||||
std::vector<uint16_t> samples;unsigned sample_index=0;
|
||||
uint8_t mcuRegulatorModes() const { return modes; }
|
||||
power::RegulatorMode defaultMcuRegulatorMode() const { return default_mode; }
|
||||
power::RegulatorMode activeMcuRegulatorMode() const { return active; }
|
||||
bool applyMcuRegulator(power::RegulatorMode mode) {
|
||||
++applies;if(!power::regulatorModeSupported(modes,mode))return false;
|
||||
active=mode==power::RegulatorMode::Default ? default_mode : mode;
|
||||
dcdc_applied|=active==power::RegulatorMode::Dcdc;return true;
|
||||
}
|
||||
uint16_t getBattMilliVolts() {
|
||||
return samples.empty()?mv:samples[sample_index++%samples.size()];
|
||||
}
|
||||
void resetHardware() { active=power::RegulatorMode::Default;sample_index=0; }
|
||||
};
|
||||
struct Radio {
|
||||
uint8_t modes=3;power::RegulatorMode default_mode=power::RegulatorMode::Dcdc;
|
||||
power::RegulatorMode active=power::RegulatorMode::Default,boot=power::RegulatorMode::Default;
|
||||
unsigned configures=0,inits=0;bool locked=false,dcdc_applied=false;
|
||||
uint8_t regulatorModes() const { return modes; }
|
||||
power::RegulatorMode defaultRegulatorMode() const { return default_mode; }
|
||||
power::RegulatorMode activeRegulatorMode() const { return active; }
|
||||
bool configureBootRegulator(power::RegulatorMode mode) {
|
||||
++configures;if(locked||!power::regulatorModeSupported(modes,mode))return false;
|
||||
boot=mode;return true;
|
||||
}
|
||||
void initialize() {
|
||||
++inits;locked=true;active=boot==power::RegulatorMode::Default?default_mode:boot;
|
||||
dcdc_applied|=active==power::RegulatorMode::Dcdc;
|
||||
}
|
||||
void resetHardware() { active=boot=power::RegulatorMode::Default;locked=false; }
|
||||
};
|
||||
}
|
||||
'''
|
||||
|
||||
ESP_SYSTEM = r'''
|
||||
#pragma once
|
||||
extern int mock_reset_reason;
|
||||
enum { ESP_RST_UNKNOWN=0,ESP_RST_POWERON,ESP_RST_EXT,ESP_RST_SW,ESP_RST_PANIC,
|
||||
ESP_RST_INT_WDT,ESP_RST_TASK_WDT,ESP_RST_WDT,ESP_RST_DEEPSLEEP,ESP_RST_BROWNOUT };
|
||||
inline int esp_reset_reason() { return mock_reset_reason; }
|
||||
'''
|
||||
|
||||
|
||||
class RegulatorSettingsTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
compiler = os.environ.get('CXX') or shutil.which('g++') or shutil.which('clang++')
|
||||
if not compiler:
|
||||
raise unittest.SkipTest('native C++ compiler unavailable')
|
||||
cls.temp = tempfile.TemporaryDirectory(prefix='meshcore-regulator-settings-')
|
||||
cls.addClassCleanup(cls.temp.cleanup)
|
||||
folder = Path(cls.temp.name)
|
||||
headers = {
|
||||
'helpers/IdentityStore.h': FILESYSTEM,
|
||||
'Dispatcher.h': DISPATCHER,
|
||||
'Arduino.h': '#pragma once\n#include <cstdint>\nextern uint32_t mock_now;\ninline uint32_t millis(){return mock_now;}\ninline void delay(unsigned ms){mock_now+=ms;}\n',
|
||||
'esp_system.h': ESP_SYSTEM,
|
||||
'esp_attr.h': '#pragma once\n#define RTC_NOINIT_ATTR\n',
|
||||
'soc/soc_caps.h': '#pragma once\n#ifndef SOC_RTC_SLOW_MEM_SUPPORTED\n#define SOC_RTC_SLOW_MEM_SUPPORTED 1\n#endif\n#define SOC_RTC_FAST_MEM_SUPPORTED 0\n',
|
||||
'hardware/watchdog.h': '#pragma once\nextern bool mock_watchdog_enabled,mock_watchdog_any;\ninline bool watchdog_enable_caused_reboot(){return mock_watchdog_enabled;}\ninline bool watchdog_caused_reboot(){return mock_watchdog_any;}\n',
|
||||
}
|
||||
for name, content in headers.items():
|
||||
path = folder / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content)
|
||||
cls.binaries = {}
|
||||
for platform, flags in {'esp32': ['-DESP32=1'],
|
||||
'rp2040': ['-DRP2040_PLATFORM=1'],
|
||||
'nrf52': ['-DNRF52_PLATFORM=1','-DNRF52_POWER_MANAGEMENT=1'],
|
||||
'no_retention': ['-DESP32=1','-DSOC_RTC_SLOW_MEM_SUPPORTED=0']}.items():
|
||||
binary = folder / platform
|
||||
result = subprocess.run([compiler, '-std=c++17','-Wall','-Wextra','-Werror',
|
||||
*SANITIZER_FLAGS,*flags,'-I',str(folder),'-I',str(ROOT/'src'),
|
||||
str(ROOT/'test/fixtures/regulator_settings_native.cpp'),'-o',str(binary)],
|
||||
text=True,capture_output=True,timeout=45)
|
||||
if result.returncode:
|
||||
raise AssertionError(result.stdout+result.stderr)
|
||||
cls.binaries[platform] = binary
|
||||
|
||||
def scenario(self, scenario):
|
||||
for platform, binary in self.binaries.items():
|
||||
if platform == 'no_retention':
|
||||
continue
|
||||
with self.subTest(platform=platform, scenario=scenario):
|
||||
result = subprocess.run([str(binary),scenario],text=True,capture_output=True,timeout=10)
|
||||
self.assertEqual(result.returncode,0,result.stdout+result.stderr)
|
||||
|
||||
def test_selections_save_both_domains_without_altering_active_hardware(self):
|
||||
self.scenario('select')
|
||||
|
||||
def test_three_crashes_at_fifty_percent_latch_both_domains_across_cold_boot(self):
|
||||
self.scenario('fallback')
|
||||
|
||||
def test_domain_choices_and_fallbacks_are_independent(self):
|
||||
self.scenario('domains')
|
||||
|
||||
def test_below_fifty_missing_stale_and_outlier_battery_readings_do_not_count(self):
|
||||
self.scenario('battery')
|
||||
|
||||
def test_intentional_reset_breaks_streak_and_preserves_unsaved_fallback(self):
|
||||
self.scenario('clean')
|
||||
|
||||
def test_short_writes_open_and_rename_failures_recover_last_committed_settings(self):
|
||||
self.scenario('atomic')
|
||||
|
||||
def test_crc_length_validation_and_backup_recovery(self):
|
||||
self.scenario('corruption')
|
||||
|
||||
def test_unsupported_dcdc_is_rejected_without_touching_either_regulator(self):
|
||||
self.scenario('unsupported')
|
||||
|
||||
def test_command_prefix_boundaries_syntax_and_bounded_reply(self):
|
||||
self.scenario('bounds')
|
||||
|
||||
def test_actual_esp_and_pico_reset_classification(self):
|
||||
self.scenario('platform')
|
||||
|
||||
def test_five_healthy_minutes_break_old_crash_streak(self):
|
||||
self.scenario('healthy')
|
||||
|
||||
def test_failed_fallback_save_retries_and_then_survives_power_loss(self):
|
||||
self.scenario('retry')
|
||||
|
||||
def test_missing_primary_store_rejects_persistence_without_live_mode_changes(self):
|
||||
self.scenario('no_store')
|
||||
|
||||
def test_failed_radio_initialization_is_not_counted_as_running_dcdc(self):
|
||||
self.scenario('failed_radio')
|
||||
|
||||
def test_missing_rtc_retention_disables_automatic_recovery(self):
|
||||
result = subprocess.run([str(self.binaries['no_retention']),'platform_unsupported'],
|
||||
text=True,capture_output=True,timeout=10)
|
||||
self.assertEqual(result.returncode,0,result.stdout+result.stderr)
|
||||
|
||||
def test_all_four_roles_initialize_once_before_first_radio_and_service_loop(self):
|
||||
for role in ('companion_radio','simple_repeater','simple_room_server','simple_sensor'):
|
||||
with self.subTest(role=role):
|
||||
source = (ROOT/f'examples/{role}/main.cpp').read_text()
|
||||
setup = method(source,'\nvoid setup(')
|
||||
loop = method(source,'\nvoid loop(')
|
||||
hook = 'mesh::power::beginRegulatorSettings(board, radio_driver, '
|
||||
self.assertEqual(setup.count(hook),1)
|
||||
self.assertLess(setup.index('board.begin()'),setup.index(hook))
|
||||
self.assertLess(setup.index(hook),setup.index('radio_init()'))
|
||||
if role == 'simple_repeater':
|
||||
self.assertEqual(loop.count('serviceCommandInterfaces()'),1)
|
||||
loop = method(source,'serviceCommandInterfaces() {')
|
||||
self.assertEqual(loop.count('mesh::power::serviceRegulatorSettings()'),1)
|
||||
self.assertLess(loop.index('mesh::power::serviceRegulatorSettings()'),loop.index('board.loop()'))
|
||||
cli = (ROOT/'src/helpers/CommonCLI.cpp').read_text()
|
||||
self.assertIn('handleRegulatorCommand(command, reply, 157)',cli)
|
||||
other = (ROOT/'examples/kiss_modem/main.cpp').read_text()
|
||||
self.assertNotIn('beginRegulatorSettings(',other)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execute T096 startup and the real DC/DC initializer with counted SDK I/O."""
|
||||
"""T096 board startup must defer DC/DC until saved regulator policy is loaded."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
@@ -12,7 +12,7 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class T096RegulatorTests(unittest.TestCase):
|
||||
def test_startup_selects_dcdc_before_voltage_check_with_or_without_softdevice(self):
|
||||
def test_startup_defers_dcdc_until_after_battery_hardware_is_ready(self):
|
||||
methods = body((ROOT / 'src/helpers/NRF52Board.cpp').read_text(),
|
||||
'void NRF52BoardDCDC::begin()')
|
||||
methods += body((ROOT / 'variants/heltec_t096/T096Board.cpp').read_text(),
|
||||
@@ -42,14 +42,17 @@ void delay(unsigned) {}
|
||||
struct Bus { void setPins(int, int) {} void begin() {} } Wire;
|
||||
struct Peripheral { void begin() {} void init() {} };
|
||||
struct Config {} power_config;
|
||||
struct NRF52Board { void begin() { ++base_calls; } };
|
||||
struct NRF52Board {
|
||||
bool cpu_dcdc_supported = false;
|
||||
void begin() { ++base_calls; }
|
||||
};
|
||||
struct NRF52BoardDCDC : NRF52Board { void begin(); };
|
||||
struct T096Board : NRF52BoardDCDC {
|
||||
Peripheral periph_power, loRaFEMControl;
|
||||
void begin();
|
||||
void checkBootVoltage(const Config*) {
|
||||
++voltage_checks;
|
||||
assert(softdevice ? sdk_calls == 1 : power.DCDCEN == 1);
|
||||
assert(sdk_calls == 0 && power.DCDCEN == 0);
|
||||
}
|
||||
};
|
||||
@METHODS@
|
||||
@@ -59,8 +62,8 @@ int main() {
|
||||
T096Board board;
|
||||
board.begin();
|
||||
assert(base_calls == 1 && voltage_checks == 1);
|
||||
assert(sdk_calls == softdevice);
|
||||
assert(power.DCDCEN == (softdevice ? 0U : 1U));
|
||||
assert(board.cpu_dcdc_supported);
|
||||
assert(sdk_calls == 0 && power.DCDCEN == 0);
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
@@ -222,6 +222,7 @@ int main() {
|
||||
'''
|
||||
|
||||
HANDOFF_SETUP = r'''
|
||||
extern "C" void meshRegulatorCleanReset() __attribute__((weak));
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#define OTA_TOWER_AUTO_STORE 1
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
"""Exercise regulator JSON, authenticated batch reboot and shipped browser UI."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
from test_replay_reset_integration import extract_braced
|
||||
import test_webconfig_lost_reply as batch_fixture
|
||||
import test_webconfig_recovery_auth as auth_fixture
|
||||
import test_webconfig_ui_runtime as browser_fixture
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class WebConfigRegulatorTest(unittest.TestCase):
|
||||
compile_run = auth_fixture.WebConfigRecoveryAuthTest.compile_run
|
||||
setup_values = browser_fixture.WebConfigUiRuntimeTest.setup_values
|
||||
run_page = browser_fixture.WebConfigUiRuntimeTest.run_page
|
||||
|
||||
def test_actual_snapshot_and_authenticated_reboot_batch(self):
|
||||
# Reuse the actual HTTP/config parser fixture, rather than a duplicate
|
||||
# batch model. The unrelated Lost-reply test main is replaced below.
|
||||
source = batch_fixture.LostReplyWebConfigTest.fixture(self)
|
||||
source = source[:source.index("String writeBody(")]
|
||||
server = (ROOT / "src/helpers/esp32/WebConfigServer.cpp").read_text()
|
||||
snapshot = extract_braced(server, "static void webConfigRegulatorsSnapshot(")
|
||||
self.compile_run(source + r'''
|
||||
#include <helpers/RegulatorMode.h>
|
||||
namespace mesh { namespace power {
|
||||
uint8_t modes[2] = {0, 3};
|
||||
RegulatorMode requested[2] = {RegulatorMode::Default, RegulatorMode::Dcdc};
|
||||
RegulatorMode active[2] = {RegulatorMode::Default, RegulatorMode::Ldo};
|
||||
bool fallback[2] = {false, true}, recovery = true;
|
||||
const char* regulatorModeName(RegulatorMode mode) {
|
||||
return mode == RegulatorMode::Dcdc ? "dcdc" : mode == RegulatorMode::Ldo ? "ldo" : "auto";
|
||||
}
|
||||
uint8_t configuredRegulatorModes(bool mcu) { return modes[mcu ? 0 : 1]; }
|
||||
RegulatorMode requestedRegulatorMode(bool mcu) { return requested[mcu ? 0 : 1]; }
|
||||
RegulatorMode activeRegulatorMode(bool mcu) { return active[mcu ? 0 : 1]; }
|
||||
const char* activeRegulatorModeName(bool mcu) {
|
||||
auto mode = activeRegulatorMode(mcu);
|
||||
return mode == RegulatorMode::Default ? "unknown" : regulatorModeName(mode);
|
||||
}
|
||||
bool regulatorFallbackActive(bool mcu) { return fallback[mcu ? 0 : 1]; }
|
||||
bool regulatorRecoveryAvailable() { return recovery; }
|
||||
}}
|
||||
''' + snapshot + r'''
|
||||
int main() {
|
||||
using namespace mesh::power;
|
||||
JsonDocument doc;
|
||||
webConfigRegulatorsSnapshot(doc.to<JsonObject>());
|
||||
assert(doc["recovery"].as<bool>());
|
||||
assert(doc["mcu"]["modes"].as<int>() == 0);
|
||||
assert(strcmp(doc["mcu"]["active"].as<const char*>(), "unknown") == 0);
|
||||
assert(doc["radio"]["modes"].as<int>() == 3);
|
||||
assert(strcmp(doc["radio"]["requested"].as<const char*>(), "dcdc") == 0);
|
||||
assert(strcmp(doc["radio"]["active"].as<const char*>(), "ldo") == 0);
|
||||
assert(doc["radio"]["fallback"].as<bool>());
|
||||
modes[0] = 3; modes[1] = 2; recovery = false;
|
||||
requested[0] = active[0] = RegulatorMode::Dcdc;
|
||||
webConfigRegulatorsSnapshot(doc.to<JsonObject>());
|
||||
assert(!doc["recovery"].as<bool>() && doc["mcu"]["modes"].as<int>() == 3);
|
||||
assert(doc["radio"]["modes"].as<int>() == 2);
|
||||
assert(strcmp(doc["mcu"]["active"].as<const char*>(), "dcdc") == 0);
|
||||
assert(mesh::esp32::managementPassword::publish("password"));
|
||||
for (const char* key : {"mcu.regulator", "radio.regulator"}) {
|
||||
assert(wcIsAllowedSetKey(key) && wcSetKeyRequiresReboot(key));
|
||||
WebConfigServer server;
|
||||
String body = String("{\"reqid\":\"0123456789abcdef\",\"reboot\":false,\"set\":{\"") + key + "\":\"ldo\"}}";
|
||||
AsyncWebServerRequest unauthenticated(body); unauthenticated.header.cookies.clear();
|
||||
server.handleConfigPost(&unauthenticated);
|
||||
assert(unauthenticated.status == 401 && server._batch_count == 0);
|
||||
AsyncWebServerRequest save(body); server.handleConfigPost(&save);
|
||||
assert(save.status == 202 && server._batch_count == 1 && server._batch_reboot);
|
||||
assert(String(server._batch[0].cmd) == String("set ") + key + " ldo");
|
||||
assert(String(server._batch[0].key) == key);
|
||||
AsyncWebServerRequest replay(body); server.handleConfigPost(&replay);
|
||||
assert(replay.status == 202 && server._batch_count == 1 && server._batch_reboot);
|
||||
}
|
||||
for (const char* key : {"mcu.regulator.extra", "radio.regulator.extra", "mcu.regulator\nreboot",
|
||||
"MCU.regulator", "radio.regulator ", "regulator"}) {
|
||||
assert(!wcIsAllowedSetKey(key) && !wcSetKeyRequiresReboot(key));
|
||||
}
|
||||
assert(!wcSetKeyRequiresReboot(nullptr));
|
||||
}
|
||||
''')
|
||||
|
||||
def test_ldo_only_radio_hides_dc_dc_and_unsupported_processor(self):
|
||||
status, config = self.setup_values()
|
||||
status["mode"] = "lan"
|
||||
config["regulators"] = {
|
||||
"recovery": False,
|
||||
"mcu": {"modes": 0},
|
||||
"radio": {"modes": 2, "requested": "ldo", "active": "ldo", "fallback": False},
|
||||
}
|
||||
prelude = r'''<script>
|
||||
(function(){
|
||||
var status=%s,config=%s;
|
||||
window.fetch=function(path){var value=path==="/api/status"?status:config;
|
||||
return Promise.resolve({ok:true,status:200,json:function(){return Promise.resolve(value)}})};
|
||||
window.addEventListener("load",function(){setTimeout(function(){
|
||||
var body=document.body,radio=document.querySelector('[data-k="radio.regulator"]');
|
||||
var dcdc=radio.querySelector('[value="dcdc"]');
|
||||
body.setAttribute("data-test-regulator-visible",!document.getElementById("regulator-settings").classList.contains("hide"));
|
||||
body.setAttribute("data-test-mcu-hidden",document.getElementById("mcu-regulator-setting").classList.contains("hide"));
|
||||
body.setAttribute("data-test-mcu-disabled",document.querySelector('[data-k="mcu.regulator"]').disabled);
|
||||
body.setAttribute("data-test-dcdc-unavailable",dcdc.hidden&&dcdc.disabled);
|
||||
body.setAttribute("data-test-radio-value",radio.value);
|
||||
body.setAttribute("data-test-radio-status",document.getElementById("radio-regulator-status").textContent);
|
||||
body.setAttribute("data-test-recovery-unavailable",document.getElementById("regulator-recovery-status").textContent.indexOf("unavailable")>=0);
|
||||
},400)});
|
||||
})();</script>''' % (json.dumps(status), json.dumps(config))
|
||||
dom = self.run_page(prelude)
|
||||
for check in ("regulator-visible", "mcu-hidden", "mcu-disabled", "dcdc-unavailable", "recovery-unavailable"):
|
||||
self.assertIn(f'data-test-{check}="true"', dom)
|
||||
self.assertIn('data-test-radio-value="ldo"', dom)
|
||||
self.assertIn('data-test-radio-status="Active: LDO. Automatic fallback: off."', dom)
|
||||
|
||||
def test_clear_fallback_and_mode_change_use_existing_save_and_reboot_flow(self):
|
||||
status, config = self.setup_values()
|
||||
status["mode"] = "lan"
|
||||
config["regulators"] = {
|
||||
"recovery": True,
|
||||
"mcu": {"modes": 3, "requested": "dcdc", "active": "dcdc", "fallback": False},
|
||||
"radio": {"modes": 3, "requested": "auto", "active": "ldo", "fallback": True},
|
||||
}
|
||||
prelude = r'''<script>
|
||||
(function(){
|
||||
var status=%s,config=%s,submitted=null,posts=0;
|
||||
function response(value,code){return {ok:true,status:code||200,json:function(){return Promise.resolve(value)}}}
|
||||
window.fetch=function(path,options){
|
||||
if(path==="/api/status")return Promise.resolve(response(status));
|
||||
if(path==="/api/config"&&options&&options.method==="POST"){
|
||||
posts++;submitted=JSON.parse(options.body);return Promise.resolve(response({state:"pending"},202));
|
||||
}
|
||||
if(path.indexOf("/api/config/result")===0)return Promise.resolve(response({state:"done",reqid:submitted.reqid,all_ok:true,reboot:true,
|
||||
results:[{key:"mcu.regulator",reply:"OK - MCU regulator ldo saved; reboot required"},{key:"radio.regulator",reply:"OK - radio regulator auto saved; reboot required"}]}));
|
||||
return Promise.resolve(response(config));
|
||||
};
|
||||
window.addEventListener("load",function(){
|
||||
setTimeout(function(){
|
||||
var body=document.body,mcu=document.querySelector('[data-k="mcu.regulator"]');
|
||||
body.setAttribute("data-test-mcu-loaded",mcu.value);
|
||||
body.setAttribute("data-test-fallback-status",document.getElementById("radio-regulator-status").textContent);
|
||||
body.setAttribute("data-test-clear-visible",!document.getElementById("radio-regulator-clear").classList.contains("hide"));
|
||||
document.getElementById("radio-regulator-clear").click();
|
||||
body.setAttribute("data-test-unchanged-mode-resubmitted",st.dirty["radio.regulator"]);
|
||||
body.setAttribute("data-test-posts-before-save",posts);
|
||||
mcu.value="ldo";mcu.dispatchEvent(new Event("input",{bubbles:true}));appSave();
|
||||
},400);
|
||||
setTimeout(function(){
|
||||
var body=document.body;
|
||||
body.setAttribute("data-test-posts",posts);
|
||||
body.setAttribute("data-test-post-mcu",submitted&&submitted.set["mcu.regulator"]);
|
||||
body.setAttribute("data-test-post-radio",submitted&&submitted.set["radio.regulator"]);
|
||||
body.setAttribute("data-test-post-reboot",submitted&&submitted.reboot);
|
||||
body.setAttribute("data-test-reboot-visible",document.getElementById("reboot-ov").classList.contains("show"));
|
||||
body.setAttribute("data-test-regulator-reboot-message",document.getElementById("reboot-msg").textContent.indexOf("power regulator mode")>=0);
|
||||
},900);
|
||||
});
|
||||
})();</script>''' % (json.dumps(status), json.dumps(config))
|
||||
dom = self.run_page(prelude)
|
||||
for key, value in (("mcu-loaded", "dcdc"), ("clear-visible", "true"),
|
||||
("unchanged-mode-resubmitted", "auto"), ("posts-before-save", "0"),
|
||||
("posts", "1"), ("post-mcu", "ldo"), ("post-radio", "auto"),
|
||||
("post-reboot", "true"), ("reboot-visible", "true"),
|
||||
("regulator-reboot-message", "true")):
|
||||
self.assertIn(f'data-test-{key}="{value}"', dom)
|
||||
self.assertIn('data-test-fallback-status="Active: LDO. Automatic fallback: on (LDO)."', dom)
|
||||
|
||||
def test_older_firmware_without_regulator_metadata_hides_the_card(self):
|
||||
status, config = self.setup_values()
|
||||
status["mode"] = "lan"
|
||||
config.pop("regulators", None)
|
||||
prelude = r'''<script>
|
||||
(function(){
|
||||
var status=%s,config=%s;
|
||||
window.fetch=function(path){return Promise.resolve({ok:true,status:200,json:function(){return Promise.resolve(path==="/api/status"?status:config)}})};
|
||||
window.addEventListener("load",function(){setTimeout(function(){
|
||||
document.body.setAttribute("data-test-regulator-hidden",document.getElementById("regulator-settings").classList.contains("hide"));
|
||||
document.body.setAttribute("data-test-radio-disabled",document.querySelector('[data-k="radio.regulator"]').disabled);
|
||||
},400)});
|
||||
})();</script>''' % (json.dumps(status), json.dumps(config))
|
||||
dom = self.run_page(prelude)
|
||||
self.assertIn('data-test-regulator-hidden="true"', dom)
|
||||
self.assertIn('data-test-radio-disabled="true"', dom)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user