"""Host-side contracts for LR1110 profile ownership and batched modulation.""" from pathlib import Path import unittest from test_radio_receive_contract import method import test_sx1262_batched_modulation as compiler ROOT = Path(__file__).resolve().parents[1] class LR1110ProfileSwitchTests(unittest.TestCase): compile_run = compiler.BatchedModulationTests.compile_run def test_fast_profile_switch_is_enabled_for_every_lr1110_target(self): source = (ROOT / "src/helpers/radiolib/CustomLR1110.h").read_text() self.assertIn("#define MC_LR1110_FAST_PROFILE_SWITCH 1", source) targets = { "t1000-e": "Nrf52BufferedRadioHal", "wio_wm1110": "Nrf52BufferedRadioHal", "thinknode_m3": "Nrf52BufferedRadioHal", "minewsemi_me25ls01": "Nrf52BufferedRadioHal", "thinknode_m7": "ESP32BufferedRadioHal", "thinknode_m9": "ESP32BufferedRadioHal", } for target, hal in targets.items(): with self.subTest(target=target): variant = ROOT / "variants" / target config = (variant / "platformio.ini").read_text() target_source = (variant / "target.cpp").read_text() self.assertIn("-D RADIO_CLASS=CustomLR1110", config) self.assertIn("-D WRAPPER_CLASS=CustomLR1110Wrapper", config) self.assertIn(f"{hal} radioHal(", target_source) self.assertIn("new Module(&radioHal,", target_source) esp32_hal = (ROOT / "src/helpers/radiolib/ESP32BufferedRadioHal.h").read_text() nrf52_hal = (ROOT / "src/helpers/radiolib/Nrf52BufferedRadioHal.h").read_text() self.assertIn("SPISettings(hz, MSBFIRST, SPI_MODE0)", esp32_hal) self.assertIn("spi->transferBytes(out, in, len)", esp32_hal) self.assertIn("SPISettings(hz, MSBFIRST, SPI_MODE0)", nrf52_hal) self.assertIn("spi->transfer(out, in, len)", nrf52_hal) for target in ("thinknode_m7", "thinknode_m9"): source = (ROOT / "variants" / target / "target.cpp").read_text() self.assertIn("ESP32BufferedRadioHal radioHal(", source) self.assertIn(", 16000000);", source) def test_esp32_buffered_hal_has_one_case_insensitive_header_name(self): names = [p.name for p in (ROOT / "src/helpers/radiolib").iterdir() if p.name.casefold() == "esp32bufferedradiohal.h"] self.assertEqual(names, ["ESP32BufferedRadioHal.h"]) def test_checked_irq_snapshot_restores_spi_width_and_propagates_error(self): source = (ROOT / "src/helpers/radiolib/CustomLR1110.h").read_text() implementation = method(source, "int16_t snapshotIrqAfterRxExit(") harness = r''' #include #include #include #include #define RADIOLIB_ERR_NONE 0 #define RADIOLIB_MODULE_SPI_WIDTH_STATUS 2 struct Module { enum { BITS_0=0 }; struct { uint8_t widths[3]={0,0,8}; } spiConfig; uint8_t response[6]={0,0,0x12,0x34,0x56,0x78}; int16_t result=0; int16_t SPItransferStream(const uint8_t*,uint8_t,bool,const uint8_t*, uint8_t* out,size_t length,bool wait) { assert(spiConfig.widths[2]==0 && length==6 && wait); std::memcpy(out,response,length);return result; } }; struct Radio { Module* mod; @METHOD@ }; int main() { Module m; Radio r{&m}; uint32_t irq=0; assert(r.snapshotIrqAfterRxExit(&irq)==0 && irq==0x12345678); assert(m.spiConfig.widths[2]==8); m.result=-7;irq=0xAABBCCDD; assert(r.snapshotIrqAfterRxExit(&irq)==-7 && irq==0xAABBCCDD); assert(m.spiConfig.widths[2]==8); } ''' self.compile_run(harness.replace("@METHOD@", implementation)) def test_fast_rx_skips_only_unchanged_packet_tuple_and_fails_closed(self): source = (ROOT / "src/helpers/radiolib/CustomLR1110.h").read_text() signatures = ( "void setProfileSwitchOptimization(", "void setProfileStandbyWarm(", "void beginProfileSwitch(", "void endProfileSwitch(", "bool profileSwitchFailed(", "int16_t standby()", "int16_t standby(uint8_t", "int16_t sleep()", "int16_t reset()", "int16_t stageMode(", "int16_t setPreambleLength(", "int16_t startReceive()", ) methods = "\n".join(method(source, signature).replace(" override", "") for signature in signatures) state = (ROOT / "src/helpers/radiolib/LR1110ProfileSwitchState.h").read_text().replace( "#pragma once", "" ) harness = r''' #include #include #include #include @STATE@ #define RADIOLIB_ERR_NONE 0 #define RADIOLIB_LR11X0_STANDBY_XOSC 0 #define RADIOLIB_LR11X0_RX_TIMEOUT_INF 0xffffff #define RADIOLIB_LR11X0_IRQ_ALL 0xffffffff #define RADIOLIB_LR11X0_MAX_PACKET_LENGTH 255 #define RADIOLIB_RADIO_MODE_NONE 0 #define RADIOLIB_IRQ_RX_DEFAULT_FLAGS 1 #define RADIOLIB_IRQ_RX_DEFAULT_MASK 2 #define RADIOLIB_IRQ_PREAMBLE_DETECTED 3 #define RADIOLIB_IRQ_HEADER_ERR 6 using RadioModeType_t=int; struct RadioModeConfig_t {}; struct Module { enum { MODE_IDLE=0, MODE_RX=1 }; std::string* trace; void setRfSwitchState(int mode) { assert(mode==MODE_RX || mode==MODE_IDLE); *trace+=mode==MODE_RX ? 'W' : 'I'; } }; struct LR1110 { std::string trace; char fail=0; size_t preambleLengthLoRa=32; uint8_t headerType=0,crcTypeLoRa=1,invertIQEnabled=0; uint32_t rxTimeout=0; int stagedMode=7; Module module{&trace}; Module* mod=&module; int16_t call(char c) { trace+=c;return fail==c ? -7:0; } int16_t standby() { return call('S'); } int16_t standby(uint8_t) { return call('U'); } int16_t standby(uint8_t mode, bool wake) { assert(mode==1 && !wake);return call('s'); } int16_t setFs() { return call('F'); } int16_t sleep() { return call('L'); } int16_t reset() { return call('Z'); } int16_t stageMode(RadioModeType_t,RadioModeConfig_t*) { return call('G'); } int16_t setPreambleLength(size_t n) { preambleLengthLoRa=n;return call('P'); } int16_t startReceive(uint32_t,uint32_t,uint32_t,size_t) { return call('N'); } int16_t clearIrqState(uint32_t flags) { assert(flags==0xffffffff);return call('C'); } int16_t setPacketParamsLoRa(size_t p,uint8_t,uint8_t len,uint8_t,uint8_t) { assert(p==preambleLengthLoRa && len==255);return call('P'); } int16_t setRx(uint32_t timeout) { assert(timeout==0xffffff);return call('R'); } }; struct Radio:LR1110 { LR1110ProfileSwitchState _profileSwitch{true}; bool _profileStandbyWarm=false,_profilePacketDirty=false; uint32_t _profileFastResumes=0; uint32_t pendingIrq=0; int16_t snapshotIrqAfterRxExit(uint32_t* irq) { const int16_t rc=call('Q'); if (rc==0) *irq=pendingIrq; return rc; } @METHODS@ }; void prime(Radio& r) { r.trace.clear();assert(r.startReceive()==0 && r.trace=="N"); assert(r._profileSwitch.rxValid);r.trace.clear(); r.setProfileStandbyWarm(true); } void hop(Radio& r,size_t preamble,const std::string& expected) { r.beginProfileSwitch(true);assert(r.standby()==0 && r.trace=="IF");r.trace.clear(); assert(r.setPreambleLength(preamble)==0 && r.trace.empty()); assert(r.startReceive()==0 && r.trace==expected); assert(r._profileSwitch.rxValid && !r._profilePacketDirty); r.endProfileSwitch(true);r.trace.clear(); } int main() { Radio r;prime(r);hop(r,32,"QWR");hop(r,48,"QPWR");hop(r,48,"QWR"); r.pendingIrq=1;hop(r,32,"QCPWR");r.pendingIrq=0; assert(r._profileFastResumes==4); r.beginProfileSwitch(true);r.fail='F'; assert(r.standby()==-7 && r.trace=="IF"); assert(r.profileSwitchFailed() && !r._profileSwitch.rxValid); r.endProfileSwitch(false);r.fail=0;prime(r); r.setProfileStandbyWarm(false); assert(!r._profileSwitch.rxValid);r.beginProfileSwitch(true); assert(r.standby()==0 && r.trace=="S");r.endProfileSwitch(true); prime(r); r.beginProfileSwitch(true);r.standby();r.trace.clear(); r.pendingIrq=1;r.fail='C';assert(r.startReceive()==-7 && r.trace=="QC"); assert(r.profileSwitchFailed() && !r._profileSwitch.rxValid); r.endProfileSwitch(false);r.fail=0;r.pendingIrq=0;prime(r);hop(r,32,"QWR"); r.beginProfileSwitch(true);r.standby();r.trace.clear(); r.fail='Q';assert(r.startReceive()==-7 && r.trace=="Q"); assert(r.profileSwitchFailed() && !r._profileSwitch.rxValid); r.endProfileSwitch(false);r.fail=0;prime(r); r.beginProfileSwitch(true);r.standby();r.trace.clear(); r.setPreambleLength(64);r.fail='P'; assert(r.startReceive()==-7 && r.trace=="QP"); assert(r.profileSwitchFailed() && !r._profileSwitch.rxValid); r.endProfileSwitch(false);r.fail=0;prime(r);hop(r,32,"QPWR"); r.beginProfileSwitch(true);r.standby();r.trace.clear(); r.fail='R';assert(r.startReceive()==-7 && r.trace=="QWR"); assert(r.profileSwitchFailed() && !r._profileSwitch.rxValid); r.endProfileSwitch(false);r.fail=0;prime(r); r.standby(uint8_t(0));assert(!r._profileSwitch.rxValid); prime(r);r.stageMode(2,nullptr);assert(!r._profileSwitch.rxValid); prime(r);r.sleep();assert(!r._profileSwitch.rxValid); prime(r);r.reset();assert(!r._profileSwitch.rxValid); } ''' self.compile_run(harness.replace("@STATE@", state).replace("@METHODS@", methods)) def test_owned_rx_context_invalidates_on_errors_and_unowned_operations(self): state = (ROOT / "src/helpers/radiolib/LR1110ProfileSwitchState.h").read_text().replace( "#pragma once", "" ) harness = r''' #include #include @STATE@ int main() { LR1110ProfileSwitchState s(true); s.begin(true, true); assert(!s.canResumeFast()); s.end(true); s.rxResult(0, true); for (int continuous = 0; continuous < 2; ++continuous) for (int warm = 0; warm < 2; ++warm) { s.begin(continuous, warm); s.standbyResult(0); assert(s.canResumeFast() == bool(continuous && warm)); s.end(true); } s.begin(true, true); assert(!s.canResumeFast()); s.standbyResult(0); assert(s.canResumeFast()); s.modulationResult(0, 8, 4, 1, 0); assert(s.matchesModulation(8, 4, 1, 0)); assert(!s.matchesModulation(9, 4, 1, 0)); s.rxResult(0, true); assert(!s.canResumeFast()); s.end(true); s.begin(true, true); s.standbyResult(-7); assert(s.failed() && !s.rxValid && !s.modulationValid); s.end(false); assert(!s.failed()); s.rxResult(0, true); s.begin(true, true); s.standbyResult(0); s.modulationResult(-8, 8, 4, 1, 0); assert(s.failed() && !s.rxValid && !s.modulationValid); s.end(false); s.rxResult(0, true); s.begin(true, true); s.standbyResult(0); s.invalidate(); assert(!s.canResumeFast() && !s.rxValid); } ''' self.compile_run(harness.replace("@STATE@", state)) def test_batched_modulation_validation_cache_and_failed_write_rollback(self): source = (ROOT / "src/helpers/radiolib/CustomLR1110.h").read_text() production = method(source, "int16_t setLoRaModulationParams(") state = (ROOT / "src/helpers/radiolib/LR1110ProfileSwitchState.h").read_text().replace( "#pragma once", "" ) harness = r''' #include #include #include #include @STATE@ #define RADIOLIB_ERR_NONE 0 #define RADIOLIB_ERR_INVALID_SPREADING_FACTOR -1 #define RADIOLIB_ERR_INVALID_CODING_RATE -2 #define RADIOLIB_ERR_INVALID_BANDWIDTH -3 #define RADIOLIB_ERR_WRONG_MODEM -4 #define RADIOLIB_LR11X0_PACKET_TYPE_NONE 0 #define RADIOLIB_LR11X0_PACKET_TYPE_LORA 1 #define RADIOLIB_LR11X0_LORA_BW_62_5 3 #define RADIOLIB_LR11X0_LORA_BW_125_0 4 #define RADIOLIB_LR11X0_LORA_BW_250_0 5 #define RADIOLIB_LR11X0_LORA_BW_500_0 6 #define RADIOLIB_LR11X0_LORA_LDRO_ENABLED 1 #define RADIOLIB_LR11X0_LORA_LDRO_DISABLED 0 struct Radio { LR1110ProfileSwitchState _profileSwitch{true}; uint8_t spreadingFactor=7, bandwidth=4, codingRate=1, ldrOptimize=0; float bandwidthKhz=125; bool ldroAuto=true; uint8_t packetType=1; int16_t result=0; unsigned queries=0, writes=0; uint8_t payload[4]={}; int16_t getPacketType(uint8_t* type) { ++queries;*type=packetType;return 0; } int16_t setModulationParamsLoRa(uint8_t sf,uint8_t bw,uint8_t cr,uint8_t ldro) { assert(spreadingFactor==sf && bandwidth==bw && codingRate==cr); ldrOptimize=ldroAuto ? ((float(1U<=16.0f) : ldro; payload[0]=sf;payload[1]=bw;payload[2]=cr;payload[3]=ldrOptimize; ++writes;return result; } @PRODUCTION@ }; int main() { Radio r; assert(r.setLoRaModulationParams(125,4,5)==RADIOLIB_ERR_INVALID_SPREADING_FACTOR); assert(r.setLoRaModulationParams(125,7,9)==RADIOLIB_ERR_INVALID_CODING_RATE); assert(r.setLoRaModulationParams(63,7,5)==RADIOLIB_ERR_INVALID_BANDWIDTH); assert(r.writes==0); r.packetType=0; assert(r.setLoRaModulationParams(125,7,5)==RADIOLIB_ERR_WRONG_MODEM); r.packetType=1; for(float bw: {62.5f,125.0f,250.0f,500.0f}) { for(uint8_t sf=5;sf<=12;++sf) { for(uint8_t cr=4;cr<=8;++cr) { assert(r.setLoRaModulationParams(bw,sf,cr)==0); assert(r.spreadingFactor==sf && r.bandwidthKhz==bw && r.codingRate==cr-4); assert(r.payload[0]==sf && r.payload[2]==cr-4); assert(r.payload[3]==((float(1U<=16.0f)); r._profileSwitch.rxResult(0,true); r._profileSwitch.begin(true,true); r._profileSwitch.standbyResult(0); const unsigned writes=r.writes; assert(r.setLoRaModulationParams(bw,sf,cr)==0 && r.writes==writes); r._profileSwitch.end(true); } } } r._profileSwitch.rxResult(0,true); r._profileSwitch.begin(true,true); r._profileSwitch.standbyResult(0); const auto oldSf=r.spreadingFactor, oldBw=r.bandwidth, oldCr=r.codingRate; const auto oldKhz=r.bandwidthKhz; const auto oldLdro=r.ldrOptimize; r.result=-99; assert(r.setLoRaModulationParams(125,8,5)==-99); assert(r.spreadingFactor==oldSf && r.bandwidth==oldBw && r.codingRate==oldCr); assert(r.bandwidthKhz==oldKhz && r.ldrOptimize==oldLdro); assert(r._profileSwitch.failed() && !r._profileSwitch.rxValid); } ''' self.compile_run(harness.replace("@STATE@", state).replace("@PRODUCTION@", production)) if __name__ == "__main__": unittest.main()