mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-20 12:41:58 +00:00
350 lines
14 KiB
Python
350 lines
14 KiB
Python
"""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 <cassert>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#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 <cassert>
|
|
#include <cstdint>
|
|
#include <cstddef>
|
|
#include <string>
|
|
@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 <cassert>
|
|
#include <cstdint>
|
|
@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 <cassert>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <initializer_list>
|
|
@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<<sf)/bandwidthKhz)>=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<<sf)/bw)>=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()
|