diff --git a/src/helpers/RegulatorSettings.cpp b/src/helpers/RegulatorSettings.cpp index 1fba0d97c..39448df31 100644 --- a/src/helpers/RegulatorSettings.cpp +++ b/src/helpers/RegulatorSettings.cpp @@ -38,6 +38,7 @@ void markRegulatorCleanReset() {} void resumeRegulatorRecovery() {} }} // namespace mesh::power #else +#include "MorseLoopPolicy.h" namespace mesh { namespace power { namespace { MainBoard* main_board = nullptr; @@ -231,14 +232,18 @@ void serviceRegulatorSettings() { (activeRegulatorMode(true) == RegulatorMode::Dcdc ? RECOVERY_MCU : 0) | (activeRegulatorMode(false) == RegulatorMode::Dcdc ? RECOVERY_RADIO : 0); recovery().updateActiveDomains(active_dcdc); - if (uint32_t(now - last_battery) >= 30000) { + // ADC sampling deliberately spaces three readings, and persistence can + // block on flash. Keep both out of keyed Morse TX/RX timing; heartbeats + // remain live without refreshing a deferred sample or retry deadline. + const bool defer_io = mesh::morseTimingActive(main_radio); + if (!defer_io && uint32_t(now - last_battery) >= 30000) { bool valid = false; const uint8_t percent = readBatteryPercent(valid); last_battery = millis(); recovery().updateBattery(last_battery, percent, valid); } recovery().heartbeat(millis()); - if (!fallback_saved && uint32_t(now - last_save) >= 30000) { + if (!defer_io && !fallback_saved && uint32_t(now - last_save) >= 30000) { last_save = now; fallback_saved = save(requested[0], requested[1], recovery().fallbackMask()); } diff --git a/test/fixtures/regulator_settings_native.cpp b/test/fixtures/regulator_settings_native.cpp index 78aabc117..5f3814477 100644 --- a/test/fixtures/regulator_settings_native.cpp +++ b/test/fixtures/regulator_settings_native.cpp @@ -269,6 +269,59 @@ int main(int argc,char** argv) { 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=="morse_battery") { + for(bool receive:{false,true}) { + fs=MockFS();fresh(board,radio,fs,ResetKind::Cold,true); + radio.morse_tx=!receive;radio.morse_rx=receive; + const auto measured=last_battery; + const unsigned reads=board.reads; + advance(30000); + assert(mock_now==measured+30000 && board.reads==reads); + assert(last_battery==measured); + assert(platformRecoveryRecord().heartbeatAt==mock_now); + assert(platformRecoveryRecord().activeDcdcMask==RECOVERY_DOMAINS); + radio.active=RegulatorMode::Ldo; + advance(1000); + assert(platformRecoveryRecord().activeDcdcMask==RECOVERY_MCU); + radio.active=RegulatorMode::Dcdc; + advance(30000); + assert(mock_now==measured+61000 && board.reads==reads); + assert(last_battery==measured); + assert(platformRecoveryRecord().heartbeatAt==mock_now); + assert(platformRecoveryRecord().activeDcdcMask==RECOVERY_DOMAINS); + fresh(board,radio,fs,ResetKind::Crash);counts(0,0); + + radio.morse_tx=!receive;radio.morse_rx=receive; + const unsigned resumed_reads=board.reads; + advance(30000); + radio.morse_tx=radio.morse_rx=false; + // An unkeyed carrier does not defer optional work. + radio.carrier_wave=true; + const uint32_t before=mock_now; + advance(1000); + assert(mock_now==before+1010 && board.reads==resumed_reads+3); + assert(last_battery==mock_now); + } + } else if(test=="morse_save") { + for(bool receive:{false,true}) { + fs=MockFS();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(!fallback_saved); + const auto measured=last_battery,save_deadline=last_save; + const unsigned reads=board.reads,writes=fs.writes; + fs.fail_write=false;radio.morse_tx=!receive;radio.morse_rx=receive; + advance(30000); + assert(mock_now==measured+30000 && board.reads==reads && fs.writes==writes); + assert(last_battery==measured && last_save==save_deadline && !fallback_saved); + assert(platformRecoveryRecord().heartbeatAt==mock_now); + radio.morse_tx=radio.morse_rx=false; + advance(1000); + assert(board.reads==reads+3 && fs.writes>writes && fallback_saved); + assert(last_battery==mock_now && last_save==mock_now-10); + fresh(board,radio,fs,ResetKind::Cold,true); + assert(board.active==RegulatorMode::Ldo && radio.active==RegulatorMode::Ldo); + } } 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);} diff --git a/test/test_regulator_settings.py b/test/test_regulator_settings.py index 886d79825..9119a89f0 100644 --- a/test/test_regulator_settings.py +++ b/test/test_regulator_settings.py @@ -86,7 +86,7 @@ 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 samples;unsigned sample_index=0; + std::vector samples;unsigned sample_index=0,reads=0; uint8_t mcuRegulatorModes() const { return modes; } power::RegulatorMode defaultMcuRegulatorMode() const { return default_mode; } power::RegulatorMode activeMcuRegulatorMode() const { return active; } @@ -96,6 +96,7 @@ struct MainBoard { dcdc_applied|=active==power::RegulatorMode::Dcdc;return true; } uint16_t getBattMilliVolts() { + ++reads; return samples.empty()?mv:samples[sample_index++%samples.size()]; } void resetHardware() { active=power::RegulatorMode::Default;sample_index=0; } @@ -104,6 +105,10 @@ 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; + bool morse_tx=false,morse_rx=false,carrier_wave=false; + bool isMorseTxActive() const { return morse_tx; } + bool isMorseRxActive() const { return morse_rx; } + bool isCarrierWaveActive() const { return carrier_wave; } uint8_t regulatorModes() const { return modes; } power::RegulatorMode defaultRegulatorMode() const { return default_mode; } power::RegulatorMode activeRegulatorMode() const { return active; } @@ -115,7 +120,10 @@ struct Radio { ++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; } + void resetHardware() { + active=boot=power::RegulatorMode::Default;locked=false; + morse_tx=morse_rx=carrier_wave=false; + } }; } ''' @@ -239,6 +247,12 @@ class RegulatorSettingsTests(unittest.TestCase): def test_failed_fallback_save_retries_and_then_survives_power_loss(self): self.scenario('retry') + def test_morse_defers_battery_io_without_refreshing_stale_samples_or_stopping_heartbeats(self): + self.scenario('morse_battery') + + def test_morse_defers_flash_retry_and_resumes_overdue_io_after_timing_ends(self): + self.scenario('morse_save') + def test_missing_primary_store_rejects_persistence_without_live_mode_changes(self): self.scenario('no_store')