From 8e35f477c055727d3a6e93b119bcff5ccf337d3f Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 19 Oct 2026 18:54:14 -0700 Subject: [PATCH] Repair failed profile retunes before receiving packets --- .github/workflows/run-unit-tests.yml | 1 + src/helpers/radiolib/RadioLibWrappers.cpp | 61 +++++++++++++++----- src/helpers/radiolib/RadioLibWrappers.h | 3 + test/fixtures/radio_receive_retry.cpp | 13 +++++ test/test_beacon_legacy_lora_protocol.py | 6 ++ test/test_beacon_radio.py | 39 +++++++++++-- test/test_carrier_wave.py | 5 +- test/test_meshtext_transport.py | 1 + test/test_radio_profile_repair.py | 70 +++++++++++++++++++++++ test/test_radio_profile_scan.py | 1 + test/test_radio_receive_contract.py | 1 + test/test_radio_receive_retry.py | 6 ++ test/test_room_sensor_radio_restore.py | 3 + 13 files changed, 189 insertions(+), 21 deletions(-) create mode 100644 test/test_radio_profile_repair.py diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index 0932e33b9..e1f7443b9 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -590,6 +590,7 @@ jobs: python3 -B test/test_radio_reply_mutation.py python3 -B test/test_radio_profile_scan.py python3 -B test/test_radio_profile_clock.py -v + python3 -B test/test_radio_profile_repair.py -v python3 -B test/test_radio_profile_runtime_recommendation.py -v python3 -B test/test_radio_profile_idle_dispatcher.py -v python3 -B test/test_radio_rxps_rejection.py -v diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 4fc4be3ee..1261ef414 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -315,6 +315,21 @@ uint8_t RadioLibWrapper::beginReconfigure() { #endif if (_cw_active) return 2; const uint8_t base_state = state; + if (_profile_repair_required) { + // The last transaction failed after changing the chip. Its old FIFO and + // activity flags have no trustworthy modulation/profile ownership. + if (base_state == STATE_TX_WAIT || isChipBusy()) return 2; + claimRadioInterrupt(); + _rx_hold_continuous = false; + if (_radio->standby() != RADIOLIB_ERR_NONE) { + state = STATE_IDLE; + return 3; + } + _rx_ps_armed = false; + claimRadioInterrupt(); + state = STATE_IDLE; + return 1; // A repair must verify RX re-entry even if software was idle. + } // On SX126x/LR11xx duty-cycle RX, BUSY may remain asserted during the sleep // side of the cycle. Do not issue an IRQ/preamble query over SPI then; due // scheduled work will retry as soon as the next safe listen window opens. @@ -324,10 +339,13 @@ uint8_t RadioLibWrapper::beginReconfigure() { } const bool resume_rx = base_state == STATE_RX; - if (_rx_ps_armed) { - stopReceiveDutyCycle(); - } else if (resume_rx) { - _radio->standby(); + if (_rx_ps_armed || resume_rx) { + if (_radio->standby() != RADIOLIB_ERR_NONE) { + state = STATE_IDLE; + _profile_refresh_required = _profile_repair_required = true; + return 3; + } + _rx_ps_armed = false; } // An RX completion during standby still owns its FIFO and old profile. // Defer reconfiguration until recvRaw consumes it, rather than rearming and @@ -368,6 +386,7 @@ bool RadioLibWrapper::restoreAfterDeepInit() { restored = applyRxBoostedGainMode(_cur_rx_boosted_gain) && restored; } recalibrateNoiseFloor(); + if (restored) _profile_repair_required = false; return restored; } @@ -378,7 +397,8 @@ mesh::RadioParamApplyResult RadioLibWrapper::trySetParams(float freq, float bw, } uint8_t resume_rx = beginReconfigure(); - if (resume_rx > 1) return mesh::RadioParamApplyResult::BUSY; + if (resume_rx > 1) return resume_rx == 2 + ? mesh::RadioParamApplyResult::BUSY : mesh::RadioParamApplyResult::FAILED; const bool had_previous_params = _params_valid; const float previous_freq = _cur_freq; @@ -418,6 +438,7 @@ mesh::RadioParamApplyResult RadioLibWrapper::trySetParams(float freq, float bw, _rx_ps_continuous_fallback = rxPowerSavingUsesContinuousFallback( _rx_ps_rx_us, _rx_ps_sleep_us); } + _profile_repair_required = false; endReconfigure(resume_rx); success = !resume_rx || isInRecvMode(); } @@ -453,8 +474,12 @@ mesh::RadioParamApplyResult RadioLibWrapper::trySetParams(float freq, float bw, #endif if (!restored) restored = restoreAfterDeepInit(); - if (restored) endReconfigure(resume_rx); - if (!restored || (resume_rx && !isInRecvMode())) _profile_refresh_required = true; + if (restored) { + _profile_repair_required = false; + endReconfigure(resume_rx); + } + if (!restored || (resume_rx && !isInRecvMode())) + _profile_refresh_required = _profile_repair_required = true; if (!restored) { MESH_DEBUG_PRINTLN("RadioLibWrapper: failed to restore radio parameters after apply failure"); @@ -518,7 +543,7 @@ mesh::RadioParamApplyResult RadioLibWrapper::tuneProfile(uint8_t profile) { if (profile > 1 || (profile == 1 && !_profiles.enabled())) return mesh::RadioParamApplyResult::FAILED; const auto& p = _profiles.params(profile); const uint16_t preamble = profilePreamble(profile); - if (!_profile_refresh_required && _active_profile == profile && _profile_generation == _profiles.generation[profile] + if (!_profile_refresh_required && !_profile_repair_required && _active_profile == profile && _profile_generation == _profiles.generation[profile] && _physical_preamble == preamble) return mesh::RadioParamApplyResult::APPLIED; if (!validateProfile(p)) return mesh::RadioParamApplyResult::FAILED; const bool dual_profile = _profiles.enabled(); @@ -526,8 +551,8 @@ mesh::RadioParamApplyResult RadioLibWrapper::tuneProfile(uint8_t profile) { beginProfileRetune(isInRecvMode() && !_rx_ps_armed && !_rx_ps_enabled); const uint8_t resume = beginReconfigure(); if (resume > 1) { - endProfileRetune(true); // guard deferred before changing hardware - return mesh::RadioParamApplyResult::BUSY; + endProfileRetune(resume == 2); // Only a BUSY guard deferred before changing hardware. + return resume == 2 ? mesh::RadioParamApplyResult::BUSY : mesh::RadioParamApplyResult::FAILED; } const float old_freq = _cur_freq, old_bw = _cur_bw; const uint8_t old_sf = _cur_sf, old_cr = _cur_cr, old_profile = _active_profile; @@ -553,6 +578,7 @@ mesh::RadioParamApplyResult RadioLibWrapper::tuneProfile(uint8_t profile) { _active_profile = profile; _profile_generation = _profiles.generation[profile]; _profile_refresh_required = false; + _profile_repair_required = false; endReconfigure(resume); applied = !resume || isInRecvMode(); } @@ -579,8 +605,12 @@ mesh::RadioParamApplyResult RadioLibWrapper::tuneProfile(uint8_t profile) { restored = restored && applyProtocol(old_protocol); #endif if (!restored) restored = restoreAfterDeepInit(); - if (restored) endReconfigure(resume); - if (!restored || (resume && !isInRecvMode())) _profile_refresh_required = true; + if (restored) { + _profile_repair_required = false; + endReconfigure(resume); + } + if (!restored || (resume && !isInRecvMode())) + _profile_refresh_required = _profile_repair_required = true; } _profile_visit_stamp = profileTimestamp(dual_profile); // A profile retune re-enters RX on a different tuple. Do not read its @@ -669,7 +699,7 @@ void RadioLibWrapper::cacheBeaconRestoreParams() { mesh::RadioParamApplyResult RadioLibWrapper::prepareBeaconTransmit( const mesh::RadioProfileParams& p, int8_t power) { using Result = mesh::RadioParamApplyResult; - if (_beacon_owned || _cw_active) return Result::BUSY; + if (_beacon_owned || _cw_active || _profile_repair_required) return Result::BUSY; #if defined(MESH_HIL_TX_FREQ_MHZ) _hil_beacon_tx_frequency_mhz = 0; if (!mesh::hilTransmitFrequencyAllowed(p.freq)) return Result::FAILED; @@ -753,6 +783,7 @@ mesh::RadioParamApplyResult RadioLibWrapper::restoreBeaconReceive() { } claimRadioInterrupt(); if (restored) { + _profile_repair_required = false; // Full normal tuple/protocol/power/IRQ restore succeeded. state = STATE_IDLE; startRecv(); restored = isInRecvMode(); @@ -2035,7 +2066,7 @@ void RadioLibWrapper::startRecv() { #if MESH_ENABLE_TX_BEACONS if (_beacon_owned && !_beacon_restoring) return; #endif - if (_cw_active) return; + if (_cw_active || _profile_repair_required) return; int err = startReceiveMode(); if (err == RADIOLIB_ERR_NONE) { // A very short frame may complete while startReceiveMode() returns. @@ -2190,7 +2221,7 @@ int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) { #if MESH_ENABLE_TX_BEACONS if (_beacon_owned) return 0; #endif - if (_cw_active || state == STATE_TX_WAIT) return 0; + if (_cw_active || state == STATE_TX_WAIT || _profile_repair_required) return 0; int len = 0; #if defined(USE_LR2021) bool receiver_stopped = false; diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 7b9929f57..e34ba4f49 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -114,6 +114,9 @@ protected: bool _profile_rxps_suspended = false; bool _profile_saved_rxps = false; bool _profile_refresh_required = false; + // A failed parameter transaction left the physical tuple untrusted. Unlike + // an ordinary refresh, its FIFO and activity flags cannot identify a packet. + bool _profile_repair_required = false; #if defined(NRF52_PLATFORM) // Published by the scanner before the final sleep predicates. The late // query uses integers only: floating-point work can leave a stale FPU event. diff --git a/test/fixtures/radio_receive_retry.cpp b/test/fixtures/radio_receive_retry.cpp index f3adf8164..17f557aa1 100644 --- a/test/fixtures/radio_receive_retry.cpp +++ b/test/fixtures/radio_receive_retry.cpp @@ -351,6 +351,19 @@ static void recoveryOwnershipAdmission() { int main(int argc, char** argv) { if (argc == 2) { + if (!strcmp(argv[1], "profile-repair")) { + RadioLibWrapper w; uint8_t bytes[8]; + w._profile_repair_required=true; w.events.record(); w.radio.packet_length=1; + const auto arms=w.arms; + assert(w.recvRaw(bytes,sizeof(bytes))==0); + w.startRecv(); + assert(w.radio.fifo_reads==0 && w.arms==arms && w.hasPendingRadioInterrupt()); + // A trusted configuration refresh does not revoke old FIFO ownership. + w._profile_repair_required=false; + assert(w.recvRaw(bytes,sizeof(bytes))==1 && w.radio.fifo_reads==1); + std::puts("PASS: untrusted profile RX waits for repair"); + return 0; + } assert(!strcmp(argv[1], "recovery")); recoveryOwnershipAdmission(); return 0; diff --git a/test/test_beacon_legacy_lora_protocol.py b/test/test_beacon_legacy_lora_protocol.py index abf598adb..109d5a44e 100644 --- a/test/test_beacon_legacy_lora_protocol.py +++ b/test/test_beacon_legacy_lora_protocol.py @@ -76,6 +76,7 @@ struct RadioLibWrapper { PhysicalLayer* _radio; mesh::MainBoard* _board; bool _params_valid = true, _rx_ps_armed = true, _dbm_valid = false, _rx_boosted_gain_valid = false; + bool _profile_repair_required = false; int state = 1; float _cur_freq = 906.875f, _cur_bw = 250; uint8_t _cur_sf = 11, _cur_cr = 5; @@ -136,14 +137,19 @@ static void failures_and_recovery() { assert(chip.sync == 0x12 && chip.crc && chip.explicit_header && !chip.iq); assert(chip.cached_crc && !chip.cached_implicit && !chip.cached_iq); wrapper._physical_protocol = Protocol::Meshtext; + wrapper._profile_repair_required = true; assert(wrapper.restoreAfterDeepInit()); + assert(!wrapper._profile_repair_required); assert(chip.sync == 0x2b && chip.crc && chip.explicit_header && !chip.iq); assert(!wrapper._rx_ps_armed && wrapper.state == STATE_IDLE); chip.fail_at = fail; + wrapper._profile_repair_required = true; assert(!wrapper.restoreAfterDeepInit()); + assert(wrapper._profile_repair_required); chip.fail_at = 0; wrapper._physical_protocol = Protocol::MeshCore; assert(wrapper.restoreAfterDeepInit()); + assert(!wrapper._profile_repair_required); assert(chip.sync == 0x12 && chip.crc && chip.explicit_header && !chip.iq); } } diff --git a/test/test_beacon_radio.py b/test/test_beacon_radio.py index bb6f28322..42c2451d8 100644 --- a/test/test_beacon_radio.py +++ b/test/test_beacon_radio.py @@ -75,7 +75,7 @@ struct RadioLibWrapper { uint32_t _beacon_restore_at = 0, _beacon_cad_timeout = 0; uint8_t state = STATE_RX, _active_profile = 0; uint32_t _profile_generation = 1, _profile_visit_stamp = 0; - bool _profile_refresh_required = false; + bool _profile_refresh_required = false, _profile_repair_required = false; uint16_t _physical_preamble = 32; mesh::RadioProfileProtocol _physical_protocol = mesh::RadioProfileProtocol::MeshCore; float _cur_freq = 906, _cur_bw = 62.5; @@ -219,29 +219,56 @@ int main() { assert(w.recalibrations == 0 && w.deep_calls == 0); ++cases; } - for (unsigned reason = 0; reason < 5; ++reason) { + for (unsigned reason = 0; reason < 6; ++reason) { RadioLibWrapper w; if (reason == 0) w.pending = true; if (reason == 1) w.packet = true; if (reason == 2) w.busy = true; if (reason == 3) w._cw_active = true; if (reason == 4) w.calibrating = true; + if (reason == 5) w._profile_repair_required = true; assert(w.prepareBeaconTransmit(beacon(), 10) == Result::BUSY); assert(!w._beacon_owned && w.applications == 0 && w.power_sets == 0); ++cases; } - for (unsigned failure = 0; failure < 4; ++failure) { + for (unsigned failure = 0; failure < 3; ++failure) { RadioLibWrapper w; if (failure == 0) w.modem.param_fail = true; if (failure == 1) w.protocol_fail = true; if (failure == 2) w.power_fail = true; - if (failure == 3) w.modem.standby_fail = true; assert(w.prepareBeaconTransmit(beacon(true), 10) == Result::FAILED && w._beacon_owned); w.repair_on_deep = true; assert(w.restoreBeaconReceive() == Result::APPLIED && !w._beacon_owned); assert(w.modem.freq == 906 && w.modem.protocol == mesh::RadioProfileProtocol::MeshCore && w.modem.power == 20); ++cases; } + { + RadioLibWrapper w; + w.modem.standby_fail = true; + assert(w.prepareBeaconTransmit(beacon(true), 10) == Result::BUSY); + // Refused initial standby never admits a beacon lease or mutates PA/params. + assert(!w._beacon_owned && w._profile_repair_required && w.state == STATE_IDLE); + assert(w.applications == 0 && w.power_sets == 0 && w.protocols == 0); + assert(w.modem.standbys == 1 && !w.board.before && !w.arms); + w.startRecv(); + assert(w.arms == 0 && w.state == STATE_IDLE); + assert(w.prepareBeaconTransmit(beacon(), 10) == Result::BUSY && w.modem.standbys == 1); + w.repair_on_deep = true; + assert(w.restoreAfterDeepInit() && !w._profile_repair_required); + assert(w.prepareBeaconTransmit(beacon(true), 10) == Result::APPLIED && w._beacon_owned); + assert(w.restoreBeaconReceive() == Result::APPLIED && !w._beacon_owned); + ++cases; + } + { + RadioLibWrapper w; + assert(w.prepareBeaconTransmit(beacon(true), 10) == Result::APPLIED); + w._profile_repair_required = true; + w.deep_ok = false; // A skipped first RX arm cannot be hidden by deep recovery. + assert(w.restoreBeaconReceive() == Result::APPLIED && !w._beacon_owned); + assert(!w._profile_repair_required && w.state == STATE_RX && w.arms == 1); + assert(w.deep_calls == 0); + ++cases; + } for (unsigned changed = 0; changed < 2; ++changed) { RadioLibWrapper w; w._profiles.secondary.params = w._profiles.primary; @@ -386,6 +413,10 @@ class BeaconRadioTest(unittest.TestCase): def test_ownership_and_snapshot_negative_controls(self): source = self.source() mutations = [ + ("if (_beacon_owned || _cw_active || _profile_repair_required) return Result::BUSY;", + "if (_beacon_owned || _cw_active) return Result::BUSY;"), + ("_profile_repair_required = false; // Full normal tuple/protocol/power/IRQ restore succeeded.", + "/* omit repair retirement before ordinary RX */"), ("if (_beacon_owned && !_beacon_restoring) return 2;", "/* remove ownership guard */"), ("if (_beacon_owned && !_beacon_restoring) return;", "/* permit RX on beacon tuple */"), ("bool applied = _radio->standby() == RADIOLIB_ERR_NONE", diff --git a/test/test_carrier_wave.py b/test/test_carrier_wave.py index a6a07fd11..484bdabc9 100644 --- a/test/test_carrier_wave.py +++ b/test/test_carrier_wave.py @@ -117,6 +117,7 @@ struct RadioLibWrapper : mesh::Radio { uint8_t state = STATE_RX; RadioInterruptEvents events; bool hasPendingRadioInterrupt() const { return events.pending(); } + bool claimRadioInterrupt() { return events.claim(); } unsigned register_failures=0; bool registerInterruptAction() { if (register_failures) { --register_failures; return false; } @@ -148,7 +149,7 @@ struct RadioLibWrapper : mesh::Radio { if (rx_gain_apply_ok) physical_rx_gain=enabled; return rx_gain_apply_ok; } - bool _nf_calib_active=true, _profile_refresh_required=false; + bool _nf_calib_active=true, _profile_refresh_required=false, _profile_repair_required=false; unsigned long _wd_observe_until=0; bool packet=false, busy=false, reset_ok=false, supported=true, deep_supported=true; unsigned rtc_stops=0, tunes=0, rx_starts=0, calibrations=0, resets=0; @@ -270,7 +271,7 @@ int main() { assert(w.setCarrierWave(profile,1250)==Result::APPLIED); assert(w.frequency==(profile ? 911.5f : 909.5f)); assert(w.chip.carrier && !w.chip.callback && w.board.before==1 && w.board.awake); - assert(w.rtc_stops==1 && !w._rx_ps_armed && !w._nf_calib_active); + assert(w.chip.standby_calls==2 && !w._rx_ps_armed && !w._nf_calib_active); assert(w.beginReconfigure()==2); advance(1249); assert(w.serviceCarrierWave() && w.chip.carrier); advance(1); assert(w.serviceCarrierWave() && !w.chip.carrier); diff --git a/test/test_meshtext_transport.py b/test/test_meshtext_transport.py index e75417244..3c26e3000 100644 --- a/test/test_meshtext_transport.py +++ b/test/test_meshtext_transport.py @@ -315,6 +315,7 @@ struct CustomSX1262: SX1262 { }; struct RadioLibWrapper { CustomSX1262 chip;CustomSX1262* _radio=&chip; + bool _profile_repair_required=false; bool supported=true; bool supportsMeshtext() const { return supported; } bool _rx_ps_armed=true,_params_valid=true,_dbm_valid=false,_rx_boosted_gain_valid=false; diff --git a/test/test_radio_profile_repair.py b/test/test_radio_profile_repair.py new file mode 100644 index 000000000..4bfa5ef22 --- /dev/null +++ b/test/test_radio_profile_repair.py @@ -0,0 +1,70 @@ +"""Execute failed transaction recovery without erasing fork feature ownership.""" +from pathlib import Path +import os +import subprocess +import tempfile +import unittest +from cpp_source import body +from test_radio_profile_scan import HARNESS, ROOT + + +class ProfileRepairTests(unittest.TestCase): + def test_failed_apply_rollback_standby_and_trusted_refresh(self): + source = (ROOT / 'src/helpers/radiolib/RadioLibWrappers.cpp').read_text() + methods = '\n'.join(body(source, signature) for signature in ( + 'uint8_t RadioLibWrapper::beginReconfigure(', + 'void RadioLibWrapper::endReconfigure(', + 'mesh::RadioParamApplyResult RadioLibWrapper::tuneProfile(', + 'mesh::RadioParamApplyResult RadioLibWrapper::trySetParams(')) + harness = HARNESS[:HARNESS.index('int main() {')] + harness = harness.replace('bool standbyXOSC=false; int standby() { return 0; }', + 'bool standbyXOSC=false; int result=0; int standby() { return result; }') + harness = harness.replace(' bool _cw_active=false;', + ' bool _cw_active=false, _beacon_owned=false, _beacon_restoring=false, fail_all=false;') + harness = harness.replace('return !fail || f==909.5f;', 'return !fail_all && (!fail || f==909.5f);') + harness = harness.replace('bool restoreAfterDeepInit() { return true; }', + 'bool restoreAfterDeepInit() { return false; }') + harness = harness.replace(' void startRecv() {', + ' void startRecv() { if (_profile_repair_required) return;') + harness += r''' +int main() { + using Result=mesh::RadioParamApplyResult; + RadioLibWrapper w; w._rx_ps_armed=w._rx_ps_enabled=false; + auto second=w._profiles.primary;second.freq=910.5; + w._profiles.secondary.params=second;w._profiles.secondary.mode=mesh::RadioProfileMode::RxTx; + w.fail_all=true; + assert(w.tuneProfile(1)==Result::FAILED && w._profile_repair_required); + assert(w._active_profile==0 && w._profile_refresh_required); + w.fail_all=false;w.packet=true;w.events.record(); + assert(w.tuneProfile(0)==Result::APPLIED); // Untrusted stale activity cannot block repair. + assert(!w._profile_repair_required && w.state==STATE_RX && !w.hasPendingRadioInterrupt()); + w._profile_refresh_required=true;w.events.record(); + const auto applies=w.applies; + assert(w.tuneProfile(0)==Result::BUSY && w.applies==applies && w.hasPendingRadioInterrupt()); + w.events.claim();w.packet=false;w._profile_repair_required=true; + w._beacon_owned=true;assert(w.beginReconfigure()==2);w._beacon_owned=false; + w._cw_active=true;assert(w.beginReconfigure()==2);w._cw_active=false; + w.state=STATE_TX_WAIT;assert(w.beginReconfigure()==2);w.state=STATE_IDLE; + w.busy=true;assert(w.beginReconfigure()==2);w.busy=false; + w.chip.result=-7;assert(w.tuneProfile(0)==Result::FAILED && w._profile_repair_required); + w.chip.result=0;assert(w.tuneProfile(0)==Result::APPLIED && w.state==STATE_RX); + w.fail_all=true; + assert(w.trySetParams(911.5,125,8,5)==Result::FAILED && w._profile_repair_required); + w.fail_all=false;assert(w.tuneProfile(0)==Result::APPLIED && !w._profile_repair_required); +} +''' + harness = harness.replace('@METHODS@', methods) + with tempfile.TemporaryDirectory(prefix='profile-repair-') as folder: + cpp, exe = Path(folder)/'test.cpp', Path(folder)/'test.exe' + (Path(folder)/'Arduino.h').write_text('#pragma once\n#include \n#include \n') + cpp.write_text(harness) + flags=[] if os.name=='nt' else ['-fsanitize=address,undefined','-fno-pie','-no-pie'] + built=subprocess.run([os.environ.get('CXX','g++'),'-std=c++17',*flags, + '-DMESH_ENABLE_TX_BEACONS=1','-I',folder,'-I',str(ROOT/'src'),str(cpp),'-o',str(exe)], + capture_output=True,text=True,timeout=60) + self.assertEqual(built.returncode,0,built.stderr) + ran=subprocess.run([str(exe)],capture_output=True,text=True,timeout=10) + self.assertEqual(ran.returncode,0,ran.stderr) + + +if __name__=='__main__': unittest.main() diff --git a/test/test_radio_profile_scan.py b/test/test_radio_profile_scan.py index 3724ba4a2..8a9956034 100644 --- a/test/test_radio_profile_scan.py +++ b/test/test_radio_profile_scan.py @@ -40,6 +40,7 @@ struct RadioLibWrapper { Chip chip; Chip* _radio = &chip; mesh::RadioProfiles _profiles; bool _params_valid=true, packet=false, busy=false, fail=false; + bool _profile_repair_required=false, _rx_hold_continuous=false; bool _rx_ps_enabled=true, _rx_ps_armed=true, _rx_ps_continuous_fallback=false; bool _profile_saved_rxps=false, _profile_rxps_suspended=false; bool _profile_standby_held=false, _saved_standby_xosc=false; diff --git a/test/test_radio_receive_contract.py b/test/test_radio_receive_contract.py index 9bc26fa9b..43f791a70 100644 --- a/test/test_radio_receive_contract.py +++ b/test/test_radio_receive_contract.py @@ -65,6 +65,7 @@ struct RadioLibWrapper { bool hasPendingRadioInterrupt() const { return events.pending(); } bool claimRadioInterrupt() { return events.claim(); } bool _cw_active = false; + bool _profile_repair_required = false; bool serviceCarrierWave() { return false; } // dedicated CW harness owns this path mesh::RadioProfiles _profiles; uint32_t _profile_visit_stamp = 0; diff --git a/test/test_radio_receive_retry.py b/test/test_radio_receive_retry.py index 5b12c0ad7..6530769e2 100644 --- a/test/test_radio_receive_retry.py +++ b/test/test_radio_receive_retry.py @@ -185,6 +185,12 @@ class RadioReceiveRetryTests(unittest.TestCase): self.assertIn('PASS: receive retry lifecycle', result.stdout) print(result.stdout, end='') + def test_failed_profile_transaction_blocks_fifo_delivery_and_rx_rearm(self): + with tempfile.TemporaryDirectory(prefix='meshcore-profile-repair-rx-') as folder: + result = self.run_probe(self.build(folder), 'profile-repair') + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn('PASS: untrusted profile RX waits for repair', result.stdout) + def test_old_hot_poll_and_pending_calibration_veto_negative_controls(self): for mutation in ('unpaced', 'pending_veto', 'agc_fast'): with self.subTest(mutation=mutation), tempfile.TemporaryDirectory( diff --git a/test/test_room_sensor_radio_restore.py b/test/test_room_sensor_radio_restore.py index dcef4c5ee..094497711 100644 --- a/test/test_room_sensor_radio_restore.py +++ b/test/test_room_sensor_radio_restore.py @@ -45,6 +45,8 @@ struct Driver { uint8_t state = STATE_IDLE; bool receiving = false, pending_irq = false, busy = false, supported = true; bool _rx_ps_armed = false, _cw_active = false; + bool _profile_repair_required = false, _profile_refresh_required = false; + bool _rx_hold_continuous = false; bool _dbm_valid = true, _rx_boosted_gain_valid = false; int8_t _cur_dbm = 7; bool _cur_rx_boosted_gain = false; @@ -53,6 +55,7 @@ struct Driver { std::deque gain_results; Chip* _radio = &chip; bool hasPendingRadioInterrupt() { return pending_irq; } + bool claimRadioInterrupt() { const bool pending = pending_irq; pending_irq = false; return pending; } bool isChipBusy() { return busy; } bool isReceivingPacket() { return receiving; } void stopReceiveDutyCycle() { _rx_ps_armed = false; }