Add saved regulator modes and battery-gated crash recovery

This commit is contained in:
mikecarper committed 2026-10-18 21:10:58 -07:00
1 parent 515bb57cdf
commit c4a1d485ff
70 files changed
+5335 -2472

No files matched your search

+5
View File
@@ -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
View File
@@ -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");
}
+1
View File
@@ -1,3 +1,4 @@
extern "C" void meshRegulatorCleanReset() __attribute__((weak));
#include <helpers/ota/MotaContainer.h>
#include <helpers/ota/OtaByteIO.h>
+1
View File
@@ -1,3 +1,4 @@
extern "C" void meshRegulatorCleanReset() __attribute__((weak));
#include <algorithm>
#include <cstdint>
#include <cstdio>
+268
View File
@@ -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);
}
+12
View File
@@ -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
+28
View File
@@ -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("),
+5 -1
View File
@@ -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(",
+3
View File
@@ -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")
+1 -1
View File
@@ -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")
+63
View File
@@ -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(
+22 -19
View File
@@ -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):
+43 -11
View File
@@ -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)",
+243
View File
@@ -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()
+7 -3
View File
@@ -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)
+183
View File
@@ -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()
+5
View File
@@ -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>
+269
View File
@@ -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()
+237
View File
@@ -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()
+9 -6
View File
@@ -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);
}
}
'''
+1
View File
@@ -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
+189
View File
@@ -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()