diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 02d1fc25f..5f9175336 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -3604,7 +3604,7 @@ void setup() { FILESYSTEM* regulator_prefs_fs = store.getPrimaryFS(); #if defined(NRF52_PLATFORM) if (!mesh::storage::internalPrimaryFilesystemReady(primary_fs_boot) - && !store.isVolatilePrimaryFS()) regulator_prefs_fs = nullptr; + || store.isVolatilePrimaryFS()) regulator_prefs_fs = nullptr; #endif if (!mesh::power::beginRegulatorSettings(board, radio_driver, regulator_prefs_fs)) { MESH_DEBUG_PRINTLN("Regulator selection could not be applied"); diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 8e67c1d55..207a775a9 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -204,7 +204,11 @@ void setup() { #else #error "need to define filesystem" #endif - if (!mesh::power::beginRegulatorSettings(board, radio_driver, fs)) { + FILESYSTEM* regulator_prefs_fs = fs; +#if defined(NRF52_PLATFORM) + if (volatile_primary_fs) regulator_prefs_fs = nullptr; +#endif + if (!mesh::power::beginRegulatorSettings(board, radio_driver, regulator_prefs_fs)) { MESH_DEBUG_PRINTLN("Regulator selection could not be applied"); } mesh::hilStartupTrace("fs_ready"); diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index 6b9800cb7..f837dca26 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -138,7 +138,11 @@ void setup() { #else #error "need to define filesystem" #endif - if (!mesh::power::beginRegulatorSettings(board, radio_driver, fs)) { + FILESYSTEM* regulator_prefs_fs = fs; +#if defined(NRF52_PLATFORM) + if (volatile_primary_fs) regulator_prefs_fs = nullptr; +#endif + if (!mesh::power::beginRegulatorSettings(board, radio_driver, regulator_prefs_fs)) { MESH_DEBUG_PRINTLN("Regulator selection could not be applied"); } #ifdef DISPLAY_CLASS diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index d1fb64563..56ea058b6 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -163,7 +163,11 @@ void setup() { #else #error "need to define filesystem" #endif - if (!mesh::power::beginRegulatorSettings(board, radio_driver, fs)) { + FILESYSTEM* regulator_prefs_fs = fs; +#if defined(NRF52_PLATFORM) + if (volatile_primary_fs) regulator_prefs_fs = nullptr; +#endif + if (!mesh::power::beginRegulatorSettings(board, radio_driver, regulator_prefs_fs)) { MESH_DEBUG_PRINTLN("Regulator selection could not be applied"); } #ifdef DISPLAY_CLASS diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index 52a9f5c23..170c52447 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -33,6 +33,7 @@ static bool ota_ble_started = false; // Intentional reboot paths share the recovery policy's clean-reset marker. // Keep this optional for standalone board/library builds without the policy. extern "C" void meshRegulatorCleanReset() __attribute__((weak)); +extern "C" void meshRegulatorResumeRecovery() __attribute__((weak)); namespace { volatile mesh::power::RecoveryRecord regulator_recovery_record @@ -313,9 +314,15 @@ void NRF52Board::feedWatchdog(bool enabled) { // setting is turned off, deliberately stop reloading it; the resulting // watchdog reset is the only software-only way to return it to the stopped // state. On that next boot the disabled preference prevents it starting. - if (!enabled) return; - const bool running = NRF_WDT->RUNSTATUS != 0; + if (!enabled) { + if (running && !watchdog_disable_reset_pending) { + watchdog_disable_reset_pending = true; + if (meshRegulatorCleanReset) meshRegulatorCleanReset(); + } + return; + } + if (!running) { // Keep running during CPU sleep: the flash-driver failure this protects // against sleeps in sd_app_evt_wait(). Pause while halted so breakpoints @@ -329,6 +336,12 @@ void NRF52Board::feedWatchdog(bool enabled) { } reloadWatchdogChannels(); + if (watchdog_disable_reset_pending) { + watchdog_disable_reset_pending = false; + // Cancel a planned watchdog-off reset with fresh recovery state. Feed + // first so the callback's battery sampling cannot race an imminent expiry. + if (meshRegulatorResumeRecovery) meshRegulatorResumeRecovery(); + } #else (void)enabled; #endif diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index 1dc58b97e..9972b2b99 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -37,7 +37,8 @@ protected: uint8_t startup_reason; char *ota_name; bool cpu_dcdc_supported = false; - mesh::power::RegulatorMode cpu_regulator_mode = mesh::power::RegulatorMode::Ldo; + mesh::power::RegulatorMode cpu_regulator_mode = mesh::power::RegulatorMode::Default; + bool watchdog_disable_reset_pending = false; #ifdef NRF52_POWER_MANAGEMENT void pwrmgtInit() { initPowerMgr(); } diff --git a/src/helpers/RegulatorSettings.cpp b/src/helpers/RegulatorSettings.cpp index 4db18adc8..7b8fb80ef 100644 --- a/src/helpers/RegulatorSettings.cpp +++ b/src/helpers/RegulatorSettings.cpp @@ -3,6 +3,9 @@ #include "NRF52VoltageRules.h" #include "NRF52VoltagePolicy.h" #include "PersistentStoreFormat.h" +#if defined(NRF52_PLATFORM) +#include "AtomicFileWriter.h" +#endif #include #include #include @@ -51,6 +54,22 @@ bool readFile(const char* name, uint8_t (&data)[12]) { } bool save(RegulatorMode mcu, RegulatorMode radio, uint8_t fallback) { if (!settings_fs) return false; +#if defined(NRF52_PLATFORM) + uint8_t data[12], checked[12]; + encode(mcu, radio, fallback, data); + // The shared LittleFS writer already stages, flushes, checks size/CRC32C, + // atomically publishes and verifies the public file. Reuse it rather than + // linking a second replacement transaction into flash-constrained targets. + mesh::AtomicFileWriter file(settings_fs, path); + if (!file || file.write(data, sizeof(data)) != sizeof(data) || !file.commit()) return false; + // Retain the format validation and exact-byte comparison as well as the + // writer's independent transaction checksum. Legacy .bak files remain + // readable at boot and are retired only after this verified publication. + if (!readFile(path, checked) || memcmp(data, checked, sizeof(data)) != 0) return false; + settings_fs->remove(backup); + saved_fallback = fallback; + return true; +#else settings_fs->remove(temporary); if (settings_fs->exists(temporary)) return false; #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) @@ -78,6 +97,7 @@ bool save(RegulatorMode mcu, RegulatorMode radio, uint8_t fallback) { settings_fs->remove(backup); saved_fallback = fallback; return true; +#endif } uint8_t readBatteryPercent(bool& valid) { @@ -256,9 +276,25 @@ void markRegulatorCleanReset() { } } +void resumeRegulatorRecovery() { + if (!initialized) return; + bool valid = false; + const uint8_t percent = readBatteryPercent(valid); + const uint32_t now = millis(); + const uint8_t active_dcdc = + (activeRegulatorMode(true) == RegulatorMode::Dcdc ? RECOVERY_MCU : 0) + | (activeRegulatorMode(false) == RegulatorMode::Dcdc ? RECOVERY_RADIO : 0); + recovery().arm(active_dcdc, now, percent, valid); + last_service = last_battery = now; +} + }} // namespace mesh::power extern "C" void meshRegulatorCleanReset() { // Intentional resets can happen before the settings/filesystem is ready. mesh::power::markRegulatorCleanReset(); } + +extern "C" void meshRegulatorResumeRecovery() { + mesh::power::resumeRegulatorRecovery(); +} diff --git a/src/helpers/RegulatorSettings.h b/src/helpers/RegulatorSettings.h index 6aae490e9..71f5ab547 100644 --- a/src/helpers/RegulatorSettings.h +++ b/src/helpers/RegulatorSettings.h @@ -21,3 +21,4 @@ bool regulatorRecoveryAvailable(); }} extern "C" void meshRegulatorCleanReset(); +extern "C" void meshRegulatorResumeRecovery(); diff --git a/test/fixtures/regulator_settings_native.cpp b/test/fixtures/regulator_settings_native.cpp index 687fa331a..7ec639336 100644 --- a/test/fixtures/regulator_settings_native.cpp +++ b/test/fixtures/regulator_settings_native.cpp @@ -34,6 +34,13 @@ uint8_t configuredBatteryPercent(uint16_t mv) { #if !defined(NRF52_PLATFORM) #include #endif +#if defined(NRF52_PLATFORM) +// Compile the production writer implementation, including independent CRC32C +// validation and the public-file verification after an attempted rename. +#define ATOMIC_FILE_WRITER_IMPLEMENTATION +#include +#undef ATOMIC_FILE_WRITER_IMPLEMENTATION +#endif #include using namespace mesh::power; static void retainedClear() { @@ -157,12 +164,19 @@ int main(int argc,char** argv) { 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 defined(NRF52_PLATFORM) + if(failure==2)fs.fail_rename_from="/regulator_v1.tmp"; + if(failure==3)fs.fail_read_path="/regulator_v1.tmp"; + if(failure==4)fs.corrupt_write=true; +#else 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;} +#endif 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(); + fs.fail_write=fs.fail_open=fs.fail_restore=fs.corrupt_write=false; + fs.fail_rename_from.clear();fs.fail_read_path.clear(); fresh(board,radio,fs,ResetKind::Cold,true); assert(requestedRegulatorMode(true)==RegulatorMode::Ldo); assert(requestedRegulatorMode(false)==RegulatorMode::Default); @@ -222,7 +236,7 @@ int main(int argc,char** argv) { 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); + assert(radio.active==RegulatorMode::Dcdc && fs.files.empty()); } else if(test=="failed_radio") { fresh(board,radio,fs,ResetKind::Cold,true); for(unsigned n=1;n<=3;++n) { diff --git a/test/test_nrf52_regulator.py b/test/test_nrf52_regulator.py index 6492b95a5..0040bc9e1 100644 --- a/test/test_nrf52_regulator.py +++ b/test/test_nrf52_regulator.py @@ -28,6 +28,128 @@ def run_native(source): class Nrf52RegulatorTests(unittest.TestCase): + def test_watchdog_off_marks_once_and_cancellation_rearms_after_reload(self): + source = BOARD.read_text() + methods = "\n".join(body(source, signature) for signature in ( + "void NRF52Board::feedWatchdog(bool enabled)", + "void NRF52Board::serviceWatchdog()", + )) + reload = body(source, "static void reloadWatchdogChannels()") + fixture = r''' +#include +#include +#include "helpers/RegulatorRecovery.h" +using namespace mesh::power; +constexpr uint32_t WDT_RR_RR_Reload = 0x6e524635; +constexpr unsigned WDT_CONFIG_SLEEP_Run = 1, WDT_CONFIG_SLEEP_Pos = 0; +constexpr unsigned WDT_CONFIG_HALT_Pause = 0, WDT_CONFIG_HALT_Pos = 1; +constexpr unsigned WDT_RREN_RR0_Msk = 1; +static unsigned reloads, clean_marks, resumes; +static uint32_t now = 1000; +static uint8_t fresh_percent = 75; +struct ReloadRegister { + uint32_t value = 0; + void operator=(uint32_t next) { assert(next == WDT_RR_RR_Reload); value = next; ++reloads; } +}; +struct Watchdog { + uint32_t RUNSTATUS = 0, RREN = 0, CONFIG = 0, CRV = 0, TASKS_START = 0; + ReloadRegister RR[8]; +} watchdog; +#define NRF_WDT (&watchdog) +static volatile RecoveryRecord retained{}; +static RecoveryPolicy policy(retained); +static void (*meshRegulatorCleanReset)(); +static void (*meshRegulatorResumeRecovery)(); +void markClean() { ++clean_marks; policy.markCleanReset(); } +void resumeRecovery() { + // The callback can sample the ADC only after the nearly-expired hardware + // watchdog has been reloaded. Both bootloader-owned enabled channels matter. + assert(watchdog.RR[0].value == WDT_RR_RR_Reload); + assert(watchdog.RR[3].value == WDT_RR_RR_Reload); + ++resumes; + policy.arm(RECOVERY_DOMAINS, now, fresh_percent, true); +} +struct NRF52Board { + bool watchdog_disable_reset_pending = false; + void feedWatchdog(bool); + void serviceWatchdog(); +}; +#if NRF52_WATCHDOG_TIMEOUT_SECONDS > 0 +@RELOAD@ +#endif +@METHODS@ +int main() { + constexpr bool callbacks = @CALLBACKS@; + meshRegulatorCleanReset = callbacks ? markClean : nullptr; + meshRegulatorResumeRecovery = callbacks ? resumeRecovery : nullptr; + policy.begin(ResetKind::Cold, 0); + policy.arm(RECOVERY_DOMAINS, 1, 75, true); + for (unsigned crash = 0; crash < 2; ++crash) { + policy.begin(ResetKind::Crash, 0); + policy.arm(RECOVERY_DOMAINS, 1, 75, true); + } + assert(policy.crashCount(RECOVERY_MCU) == 2); + NRF52Board board; + board.feedWatchdog(false); + board.feedWatchdog(false); + assert(clean_marks == 0 && resumes == 0 && reloads == 0); + assert(policy.crashCount(RECOVERY_MCU) == 2); // Never-started WDT is neutral. + board.feedWatchdog(true); +#if NRF52_WATCHDOG_TIMEOUT_SECONDS == 0 + watchdog.RUNSTATUS = 1; + board.feedWatchdog(false); + board.feedWatchdog(true); + board.serviceWatchdog(); + assert(clean_marks == 0 && resumes == 0 && reloads == 0); + assert(policy.crashCount(RECOVERY_MCU) == 2); + return 0; +#else + assert(watchdog.TASKS_START == 1 && watchdog.RREN == 1); + assert(watchdog.CRV == NRF52_WATCHDOG_TIMEOUT_SECONDS * 32768UL); + assert(reloads == 1 && clean_marks == 0 && resumes == 0); + assert(policy.crashCount(RECOVERY_MCU) == 2); // Normal feeding keeps streak. + watchdog.RUNSTATUS = 1; + watchdog.RREN = 9; // A bootloader can leave more than RR0 enabled. + const unsigned before_off = reloads; + board.feedWatchdog(false); + board.feedWatchdog(false); + assert(reloads == before_off && clean_marks == unsigned(callbacks)); + assert(board.watchdog_disable_reset_pending); + if (callbacks) { + RecoveryPolicy planned_reset(retained); + planned_reset.begin(ResetKind::Crash, 0); + assert(planned_reset.crashCount(RECOVERY_MCU) == 0 && planned_reset.fallbackMask() == 0); + } + // Keep the existing explicit service hook: it feeds already-running WDT + // channels without starting one or changing a deliberate-off transition. + board.serviceWatchdog(); + assert(reloads == before_off + 2 && resumes == 0); + watchdog.RR[0].value = watchdog.RR[3].value = 0; + board.feedWatchdog(true); + assert(!board.watchdog_disable_reset_pending); + assert(reloads == before_off + 4 && resumes == unsigned(callbacks)); + board.feedWatchdog(true); + assert(clean_marks == unsigned(callbacks) && resumes == unsigned(callbacks)); + if (callbacks) { + RecoveryPolicy real_crash(retained); + real_crash.begin(ResetKind::Crash, 0); + assert(real_crash.crashCount(RECOVERY_MCU) == 1); + assert(real_crash.crashCount(RECOVERY_RADIO) == 1 && real_crash.fallbackMask() == 0); + } + // A subsequent real off transition is marked once again. + board.feedWatchdog(false); + board.feedWatchdog(false); + assert(clean_marks == 2 * unsigned(callbacks)); + assert(resumes == unsigned(callbacks)); +#endif +} +'''.replace("@RELOAD@", reload).replace("@METHODS@", methods) + for timeout in (0, 60): + for callbacks in (False, True): + with self.subTest(timeout=timeout, callbacks=callbacks): + run_native(("#define NRF52_WATCHDOG_TIMEOUT_SECONDS %d\n" % timeout) + + fixture.replace("@CALLBACKS@", str(callbacks).lower())) + 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 ( @@ -36,6 +158,8 @@ class Nrf52RegulatorTests(unittest.TestCase): "mesh::power::RegulatorMode NRF52Board::activeMcuRegulatorMode() const", "bool NRF52Board::applyMcuRegulator(mesh::power::RegulatorMode mode)", )) + initial_mode = (ROOT / "src/helpers/NRF52Board.h").read_text().split( + "cpu_regulator_mode = ", 1)[1].split(";", 1)[0] run_native(r''' #include #include @@ -62,7 +186,7 @@ uint32_t sd_power_dcdc_mode_set(uint8_t mode) { } struct NRF52Board { bool cpu_dcdc_supported = false; - RegulatorMode cpu_regulator_mode = RegulatorMode::Ldo; + RegulatorMode cpu_regulator_mode = @INITIAL_MODE@; uint8_t mcuRegulatorModes() const; RegulatorMode defaultMcuRegulatorMode() const; RegulatorMode activeMcuRegulatorMode() const; @@ -81,7 +205,13 @@ int main() { board.cpu_dcdc_supported = true; assert(board.mcuRegulatorModes() == 3); assert(board.defaultMcuRegulatorMode() == RegulatorMode::Dcdc); + assert(board.activeMcuRegulatorMode() == RegulatorMode::Default); assert(!board.applyMcuRegulator(static_cast(255))); + softdevice = 1; + setter_result = 4; + assert(!board.applyMcuRegulator(RegulatorMode::Ldo)); + assert(board.activeMcuRegulatorMode() == RegulatorMode::Default); + setter_calls = 0; for (softdevice = 0; softdevice < 2; ++softdevice) { power.DCDCEN = 77; query_result = 5; @@ -104,7 +234,7 @@ int main() { setter_calls = 0; } } -'''.replace("@METHODS@", methods)) +'''.replace("@METHODS@", methods).replace("@INITIAL_MODE@", initial_mode)) def test_reset_classifier_excludes_wakes_and_intentional_resets(self): method = body(BOARD.read_text(), "ResetKind platformResetKind()") diff --git a/test/test_regulator_settings.py b/test/test_regulator_settings.py index 4bf6f48f1..f21393688 100644 --- a/test/test_regulator_settings.py +++ b/test/test_regulator_settings.py @@ -29,18 +29,24 @@ struct File { size_t read(uint8_t*,size_t); size_t write(const uint8_t*,size_t); void close() {} + void flush() {} + bool isDirectory() const { return false; } }; struct MockFS { std::map> files; - bool fail_open=false,fail_write=false,fail_restore=false; - std::string fail_rename_from; + bool fail_open=false,fail_write=false,fail_restore=false,corrupt_write=false; + std::string fail_rename_from,fail_read_path; 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; + if(!files.count(from))return false; +#if !defined(NRF52_PLATFORM) + if(files.count(to))return false; +#endif + // Adafruit LittleFS atomically replaces an existing destination. files[to]=files[from];files.erase(from);return true; } File open(const char* name,const char* mode,bool=false) { @@ -54,14 +60,16 @@ inline bool File::open(const char* key,int flags) { } 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; + if(!valid || fs->fail_read_path==name)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; + auto& data=fs->files[name];data.insert(data.end(),bytes,bytes+length); + if(fs->corrupt_write && !data.empty())data.back()^=1; + return length; } #define FILESYSTEM MockFS #define FILE_O_READ 0 @@ -222,6 +230,11 @@ class RegulatorSettingsTests(unittest.TestCase): self.assertEqual(setup.count(hook),1) self.assertLess(setup.index('board.begin()'),setup.index(hook)) self.assertLess(setup.index(hook),setup.index('radio_init()')) + self.assertIn('beginRegulatorSettings(board, radio_driver, regulator_prefs_fs)',setup) + if role == 'companion_radio': + self.assertIn('store.isVolatilePrimaryFS()',setup) + else: + self.assertIn('if (volatile_primary_fs) regulator_prefs_fs = nullptr',setup) if role == 'simple_repeater': self.assertEqual(loop.count('serviceCommandInterfaces()'),1) loop = method(source,'serviceCommandInterfaces() {')