Unblock LR2021 retunes and add saved crystal calibration

Repeated preamble/header activity can keep a requested primary or TempRadio
change busy indefinitely. Bound unfinished-RX admission for accepted LR2021
changes while preserving completed packets, TX, BUSY and exclusive owners.
Ordinary scans and CAD keep their existing admission rules. Expose the failing
stage and native status through get radio.apply.

Add saved radio.xtrim capacitor calibration and measured radio.freqerror in
signed Hz from the last valid LoRa packet. Preserve native trim by default,
restore explicit calibration after resets/Morse, verify saves before applying,
wait two seconds for the acknowledgement, and roll back failed changes.

Calibration motivation and prior upstream work:
https://github.com/meshcore-dev/MeshCore/issues/3365
https://github.com/recrof/MeshCore/tree/lr2021-trim
Packet-status format follows Semtech's LR20xx driver and RadioLib staging.

Tests execute production activity/admission and transaction paths with noisy
RX, completion races, protected owners, failed setters, RX restart and save
rollback. Physical W12 retuning and RF calibration still require confirmation.
This commit is contained in:
mikecarper committed 2026-10-26 15:55:36 -07:00
1 parent b00d346df7
commit 8a8c80ee6a
32 files changed
+1913 -34

No files matched your search

+1
View File
@@ -128,6 +128,7 @@ struct LR1121Probe : LR1121 {
@LR1121@
};
struct LR2021Probe : LR2021 {
int16_t restoreXoscTrim() { return 0; }
@LR2021_REGULATOR@
uint8_t gainModeLf=1,gainModeHf=3;
uint32_t _rf_switch_pins[Module::RFSWITCH_MAX_PINS]={0x80,0x81,RADIOLIB_NC};
+1 -1
View File
@@ -435,7 +435,7 @@ class BeaconRadioTest(unittest.TestCase):
"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) {", "if (false) { /* remove ownership guard */"),
("if (_beacon_owned && !_beacon_restoring) return;", "/* permit RX on beacon tuple */"),
("bool applied = _radio->standby() == RADIOLIB_ERR_NONE",
"cacheParams(p.freq, p.bw, p.sf, p.cr); bool applied = _radio->standby() == RADIOLIB_ERR_NONE"),
+249
View File
@@ -0,0 +1,249 @@
"""Exercise the production LR2021 calibration transaction and diagnostics."""
from pathlib import Path
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
def method(source, signature):
start = source.index(signature)
end = source.index("{", start) + 1
depth = 1
while depth:
depth += (source[end] == "{") - (source[end] == "}")
end += 1
return source[start:end]
class LR2021ApplyTransactionTest(unittest.TestCase):
def test_actual_apply_params_reports_each_native_failure(self):
source = (ROOT / "src/helpers/radiolib/CustomLR2021Wrapper.h").read_text()
apply = method(source, "bool applyParams(").replace(" override", "")
receive = method(source, "int startReceiveMode(").replace(" override", "")
code = r'''
#include <cassert>
#include <cstdint>
#include <helpers/radiolib/LR2021ApplyStatus.h>
#define RADIOLIB_ERR_NONE 0
#define RADIOLIB_ERR_UNKNOWN -1
#define LORA_TX_POWER 5
namespace mesh {int8_t clampLoRaTxPower(int8_t power,float){return power;}}
struct PacketMillis {uint32_t preambleMillis,payloadMillis;};
struct CustomLR2021 {
int calls=0,fail=0,updated=0;
int16_t next(){return ++calls==fail?-705:0;}
int16_t setFrequency(float){return next();}
int16_t setOutputPower(int8_t){return next();}
int16_t setSpreadingFactor(uint8_t){return next();}
int16_t setBandwidth(float){return next();}
int16_t setCodingRate(uint8_t){return next();}
int16_t setPreambleLength(uint16_t){return next();}
void setPreambleMillis(uint32_t v){assert(v==44);++updated;}
void setMaxPayloadMillis(uint32_t v){assert(v==440);++updated;}
int16_t standby(){return next();}
int16_t startReceive(){return next();}
};
struct Board {bool ready=true;bool prepareRadioFrequency(float){return ready;}};
struct Wrapper {
mesh::lr2021::ApplyStatus _apply_status;
CustomLR2021 chip;CustomLR2021* _radio=&chip;
Board board;Board* _board=&board;
bool _dbm_valid=true;int8_t _cur_dbm=5;
int16_t recordApply(mesh::lr2021::ApplyStatus::Stage s,int16_t c){return _apply_status.record(s,c);}
uint16_t preambleLengthForParams(uint8_t,float){return 32;}
bool applySideDetectorConfig(uint8_t,float){return recordApply(mesh::lr2021::ApplyStatus::Stage::SideDetectors,chip.next())==0;}
PacketMillis calcMaxPacketMillis(uint8_t,float,uint8_t,uint16_t){return {44,440};}
@APPLY@
@RECEIVE@
};
int main(){
using Stage=mesh::lr2021::ApplyStatus::Stage;
const Stage stages[]={Stage::Frequency,Stage::Power,Stage::SpreadingFactor,
Stage::Bandwidth,Stage::CodingRate,Stage::Preamble,Stage::SideDetectors};
for(int failure=1;failure<=7;++failure){
Wrapper w;w._apply_status.begin();w.chip.fail=failure;
assert(!w.applyParams(910.54f,62.5f,7,5));
assert(w._apply_status.stage==stages[failure-1]&&w._apply_status.code==-705);
assert(w.chip.calls==failure&&w.chip.updated==0);
w.chip.fail=0;assert(w.applyParams(910.525f,62.5f,7,5));
assert(w._apply_status.stage==stages[failure-1]&&w._apply_status.code==-705);
}
Wrapper rail;rail.board.ready=false;
assert(!rail.applyParams(910.54f,62.5f,7,5));
assert(rail._apply_status.stage==Stage::FrequencyRail&&rail.chip.calls==0);
Wrapper ready;assert(ready.applyParams(910.54f,62.5f,7,5));assert(ready.chip.updated==2);
for(int failure=1;failure<=2;++failure){
Wrapper rx;rx.chip.fail=failure;
assert(rx.startReceiveMode()==-705);
assert(rx._apply_status.stage==(failure==1?Stage::Standby:Stage::Receive));
assert(rx.chip.calls==failure);
}
}
'''.replace("@APPLY@", apply).replace("@RECEIVE@", receive)
self.run_cpp(code)
def test_real_trim_transaction_restores_selection_modem_and_receiver(self):
source = (ROOT / "src/helpers/radiolib/CustomLR2021Wrapper.h").read_text()
transaction = method(source, "mesh::RadioParamApplyResult trySetXoscTrim(")
cancel = method(source, "void cancelXoscTrimApply(")
code = r'''
#include <cassert>
#include <cstdint>
#include <cstring>
#include <helpers/radiolib/LR2021ApplyStatus.h>
#include <helpers/radiolib/LR2021CalibrationPolicy.h>
#include <helpers/radiolib/PrimaryRadioApplyWait.h>
#define RADIOLIB_ERR_NONE 0
#define RADIOLIB_ERR_UNKNOWN -1
#define MESH_ENABLE_CARRIER_TOOLS 1
#define MESH_ENABLE_MORSE_RX 1
#define MESH_ENABLE_MESHTEXT_WIRE 1
#define MAX_TRANS_UNIT 255
static uint32_t now_ms=100;
uint32_t millis(){return now_ms;}
namespace mesh { enum class RadioParamApplyResult { APPLIED, BUSY, FAILED }; }
struct CustomLR2021 {
bool supported=true, enabled=false;
uint8_t a=0,b=0;
int commands=0, configurations=0;
int16_t error=0;
bool getXoscTrim(uint8_t& x,uint8_t& y)const{x=a;y=b;return enabled;}
bool configureBootXoscTrim(bool e,uint8_t x,uint8_t y){
++configurations;enabled=e;a=e?x:0;b=e?y:0;return true;
}
void clearXoscTrim(){enabled=false;a=b=0;}
int16_t applyXoscTrim(uint8_t x,uint8_t y){
++commands;if(error)return error;configureBootXoscTrim(true,x,y);return 0;
}
};
struct Wrapper {
mesh::lr2021::ApplyStatus _apply_status;
mesh::PrimaryRadioApplyWait _trim_apply_wait;
uint16_t _trim_apply_key=UINT16_MAX;
mesh::RadioProfiles _profiles;
uint8_t _active_profile=0;
bool _primary_receive_preempt=false;
CustomLR2021 chip;
CustomLR2021* _radio=&chip;
bool _profile_repair_required=false,_profile_refresh_required=false;
bool _morse_owned=false,_morse_rx_owned=false,_params_valid=true;
bool _dbm_valid=true,_dbm_applied=true,_rx_boosted_gain_valid=true;
bool _rx_boosted_gain_applied=true,_cur_rx_boosted_gain=true;
float _cur_freq=910.525f,_cur_bw=62.5f;
uint8_t _cur_sf=7,_cur_cr=5,_physical_protocol=1;
int8_t _cur_dbm=5;
uint8_t admission=1;
int native_fail=0,resets=0,tuple_applies=0,rearms=0;
int fail_rearm=0;
bool rx=true,fail_reset=false;
Wrapper(){_profiles.primary.freq=910.525f;_profiles.primary.bw=62.5f;
_profiles.primary.sf=7;_profiles.primary.cr=5;}
uint16_t currentPreambleLength()const{return 32;}
uint16_t profilePreamble(uint8_t)const{return 32;}
bool supportsXoscTrim()const{return chip.supported;}
void noteReconfigureAdmission(const char*,int16_t){}
uint8_t beginReconfigure(){if(admission<=1)rx=false;return admission;}
int16_t recordApply(mesh::lr2021::ApplyStatus::Stage s,int16_t c){return _apply_status.record(s,c);}
bool applyParams(float f,float bw,uint8_t sf,uint8_t cr){
assert(f==_cur_freq&&bw==_cur_bw&&sf==_cur_sf&&cr==_cur_cr);
++tuple_applies;
if(native_fail==1){recordApply(mesh::lr2021::ApplyStatus::Stage::Frequency,-705);return false;}
return true;
}
bool applyProtocol(uint8_t p){
assert(p==_physical_protocol);
if(native_fail==2){recordApply(mesh::lr2021::ApplyStatus::Stage::Protocol,-706);return false;}
return true;
}
int16_t applyCachedTxPower(int8_t p){
assert(p==_cur_dbm);
return recordApply(mesh::lr2021::ApplyStatus::Stage::Power,native_fail==3?-707:0);
}
bool applyRxBoostedGainMode(bool b){
assert(b==_cur_rx_boosted_gain);
if(native_fail==4){recordApply(mesh::lr2021::ApplyStatus::Stage::Gain,-708);return false;}
return true;
}
void recalibrateNoiseFloor(){}
bool restoreAfterDeepInit(){++resets;_profile_repair_required=false;return !fail_reset;}
void endReconfigure(bool resume){
assert(resume);++rearms;
if(fail_rearm>0){--fail_rearm;rx=false;recordApply(mesh::lr2021::ApplyStatus::Stage::Receive,-709);}
else rx=true;
}
bool isInRecvMode()const{return rx;}
@CANCEL@
@TRANSACTION@
};
int main(){
using Result=mesh::RadioParamApplyResult;
using Stage=mesh::lr2021::ApplyStatus::Stage;
for(uint8_t admission:{2,3}){
Wrapper w;w.admission=admission;
assert(w.trySetXoscTrim(true,8,8)==(admission==2?Result::BUSY:Result::FAILED));
assert(!w.chip.enabled&&w.chip.commands==0&&w.chip.configurations==0&&w.resets==0);
assert(w._trim_apply_key==(admission==2?uint16_t(0x1208):UINT16_MAX));
assert(!w._primary_receive_preempt);
w.cancelXoscTrimApply();assert(w._trim_apply_key==UINT16_MAX);
}
for(int owner=0;owner<2;++owner){
Wrapper w;(owner?w._morse_rx_owned:w._morse_owned)=true;
assert(w.trySetXoscTrim(true,8,8)==Result::BUSY&&w.chip.commands==0&&w.rx);
}
for(int invalid:{48,255}){
Wrapper w;assert(w.trySetXoscTrim(true,invalid,8)==Result::FAILED&&w.chip.commands==0);
}
Wrapper unsupported;unsupported.chip.supported=false;
assert(unsupported.trySetXoscTrim(true,8,8)==Result::FAILED);
Wrapper success;
assert(success.trySetXoscTrim(true,8,8)==Result::APPLIED);
assert(success.chip.enabled&&success.chip.a==8&&success.chip.b==8&&success.rx);
assert(success.tuple_applies==1&&success.rearms==1&&success.resets==0);
assert(success._apply_status.stage==Stage::Applied&&success._apply_status.code==0);
assert(success._trim_apply_key==UINT16_MAX&&!success._primary_receive_preempt);
int commands=success.chip.commands;
assert(success.trySetXoscTrim(true,8,8)==Result::APPLIED&&success.chip.commands==commands);
assert(success.trySetXoscTrim(false,0,0)==Result::APPLIED);
assert(!success.chip.enabled&&success.resets==1&&success.rx);
for(bool old:{false,true})for(int failure=0;failure<=5;++failure){
Wrapper w;w.chip.configureBootXoscTrim(old,3,4);
if(failure==0)w.chip.error=-704;
else if(failure==5)w.fail_rearm=1;
else w.native_fail=failure;
assert(w.trySetXoscTrim(true,8,8)==Result::FAILED);
assert(w.chip.enabled==old&&(!old||(w.chip.a==3&&w.chip.b==4)));
assert(w.resets==1&&w.rx&&!w._profile_repair_required);
assert(w._trim_apply_key==UINT16_MAX&&!w._primary_receive_preempt);
assert(w._apply_status.code==-704-failure&&w._apply_status.failed);
}
Wrapper failed_restore;failed_restore.chip.error=-704;failed_restore.fail_reset=true;
assert(failed_restore.trySetXoscTrim(true,8,8)==Result::FAILED);
assert(failed_restore._profile_repair_required&&failed_restore._profile_refresh_required);
// First failure survives successful rollback writes; the next transaction
// resets it, and positive admission guards remain distinguishable.
mesh::lr2021::ApplyStatus status;status.begin();status.record(Stage::Frequency,-705);
status.record(Stage::Receive,0);status.applied();
assert(status.stage==Stage::Frequency&&status.code==-705);
status.begin();status.applied();assert(status.stage==Stage::Applied&&status.code==0);
}
'''.replace("@TRANSACTION@", transaction).replace("@CANCEL@", cancel)
code = code.replace("#include <cassert>", "#include <cassert>\n#include <initializer_list>")
self.run_cpp(code)
def run_cpp(self, code):
with tempfile.TemporaryDirectory(prefix="meshcore-lr2021-apply-") as directory:
cpp, binary = Path(directory) / "test.cpp", Path(directory) / "test"
cpp.write_text(code, encoding="ascii")
built = subprocess.run(["g++", "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-fsanitize=address,undefined", "-fno-sanitize-recover=all",
"-fno-pie", "-no-pie", "-I", str(ROOT / "src"),
str(cpp), "-o", str(binary)], capture_output=True, text=True)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
ran = subprocess.run([str(binary)], capture_output=True, text=True)
self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr)
if __name__ == "__main__":
unittest.main()
+360
View File
@@ -0,0 +1,360 @@
"""Execute LR2021 trim persistence, native commands and packet-offset capture."""
from pathlib import Path
import os
import shutil
import subprocess
import tempfile
import unittest
from cpp_source import body
from test_display_power import FS_STUB
ROOT = Path(__file__).resolve().parents[1]
RADIO = ROOT / "src/helpers/radiolib"
def method(source, signature):
return body(source, signature)
POLICY = r'''
#include <cassert>
#include <cstring>
#include <initializer_list>
#include <helpers/radiolib/LR2021CalibrationPolicy.h>
using namespace mesh::lr2021;
int main() {
XoscTrimSelection selection;
for (unsigned a=0;a<=47;++a) for (unsigned b=0;b<=47;++b) {
XoscTrimSelection expected{true,uint8_t(a),uint8_t(b)};
uint8_t bytes[XoscTrimRecordSize];encodeXoscTrim(expected,bytes);
assert(decodeXoscTrim(bytes,sizeof(bytes),selection));
assert(sameXoscTrim(expected,selection));
for (unsigned i=0;i<sizeof(bytes);++i) for(unsigned bit=0;bit<8;++bit) {
bytes[i]^=1U<<bit;selection={true,17,19};
assert(!decodeXoscTrim(bytes,sizeof(bytes),selection));
assert(selection.enabled && selection.a==17 && selection.b==19);
bytes[i]^=1U<<bit;
}
}
for(const char* value:{"0","47","03","0,47","47,0","default"}) {
assert(parseXoscTrim(value,selection));
if(!strcmp(value,"default")) assert(!selection.enabled);
else assert(selection.enabled);
}
for(const char* invalid:{"","48","256","-1","+3","3,48","3,",",3",
"3,4,5","3.5","3,4 ","3, 4","default ","Default","99999999999999999"}) {
selection={true,17,19};
assert(!parseXoscTrim(invalid,selection));
assert(selection.enabled && selection.a==17 && selection.b==19);
}
assert(!parseXoscTrim(nullptr,selection));
XoscTrimSelection native;uint8_t bytes[XoscTrimRecordSize];encodeXoscTrim(native,bytes);
assert(decodeXoscTrim(bytes,sizeof(bytes),selection) && !selection.enabled);
bytes[4]=1;bytes[7]^=1;
assert(!decodeXoscTrim(bytes,sizeof(bytes),selection)); // no hidden default pair
const uint8_t samples[][3]={{0,0,0},{0,0,1},{0xff,0xff,0xff},
{0x7f,0xff,0xff},{0x80,0,0},{0xff,0xfc,0x18},{0,3,0xe8}};
const int32_t expected[]={0,1,-1,8388607,-8388608,-1000,1000};
for(unsigned i=0;i<7;++i) assert(decodeFrequencyErrorHz(samples[i])==expected[i]);
}
'''
DRIVER = r'''
#include <cassert>
#include <cstdio>
#include <cstring>
#include <vector>
#include <helpers/radiolib/LR2021CalibrationPolicy.h>
#define RADIOLIB_ERR_NONE 0
#define RADIOLIB_ERR_UNKNOWN -16
#define RADIOLIB_ERR_UNSUPPORTED -25
#define RADIOLIB_ERR_PACKET_TOO_SHORT -4
#define RADIOLIB_LR2021_STANDBY_RC 0
#define RADIOLIB_LR2021_CMD_SET_XOSC_CP_TRIM 0x0131
#define RADIOLIB_LR2021_CMD_GET_LORA_PACKET_STATUS 0x022a
#define RADIOLIB_LR2021_PACKET_TYPE_NONE 0
#define RADIOLIB_LR2021_PACKET_TYPE_LORA 1
struct Hal {uint32_t now=1000;uint32_t millis(){return now;}};
struct Module {Hal clock;Hal* hal=&clock;};
class LR2021 {
public:
Module storage;Module* mod=&storage;
float freqMHz=910.525f;
int standby_result=0,command_result=0,read_result=0,type_result=0,fifo_result=0;
uint8_t modem=1,trim_a=23,trim_b=25;
uint16_t fifo=4;
int trim_commands=0,status_commands=0,reads=0,standbys=0,clears=0;
uint8_t raw_offset[3]={0xff,0xfc,0x18};
std::vector<int> sequence;
int16_t standby(uint8_t mode) {assert(mode==0);++standbys;sequence.push_back(1);return standby_result;}
int16_t SPIcommand(uint16_t command,bool write,uint8_t* data,size_t length) {
if(command==0x0131) {
assert(write && length==2);++trim_commands;sequence.push_back(2);
// A failed command can still change the chip. Wrapper rollback is required.
trim_a=data[0];trim_b=data[1];
} else {
assert(command==0x022a && !write && length==9);++status_commands;sequence.push_back(3);
memcpy(data+6,raw_offset,3);
}
return command_result;
}
int16_t getPacketType(uint8_t* type) {*type=modem;return type_result;}
int16_t getRxFifoLevel(uint16_t* size) {*size=fifo;return fifo_result;}
size_t getPacketLength() {return 4;}
int16_t clearRxFifo() {++clears;fifo=0;return 0;}
int16_t readData(uint8_t*,size_t) {++reads;sequence.push_back(4);return read_result;}
};
class CustomLR2021:public LR2021 {
mesh::lr2021::XoscTrimSelection _xosc_trim;
int32_t _last_frequency_error_hz=0;
float _last_frequency_error_mhz=0;
uint32_t _last_frequency_error_at=0;
bool _last_frequency_error_valid=false;
public:
@METHODS@
};
int main() {
CustomLR2021 radio;
uint8_t a=1,b=2;
assert(!radio.getXoscTrim(a,b) && a==1 && b==2);
#if defined(LR2021_TCXO_VOLTAGE) && LR2021_TCXO_VOLTAGE == 0
assert(radio.supportsXoscTrim());
assert(radio.restoreXoscTrim()==0 && radio.trim_commands==0);
assert(!radio.configureBootXoscTrim(true,48,0));
assert(radio.configureBootXoscTrim(true,3,4) && radio.trim_commands==0);
assert(radio.getXoscTrim(a,b) && a==3 && b==4);
assert(radio.restoreXoscTrim()==0 && radio.trim_a==3 && radio.trim_b==4);
assert(radio.sequence[0]==1 && radio.sequence[1]==2);
radio.standby_result=-71;
assert(radio.applyXoscTrim(8,9)==-71 && radio.trim_commands==1);
assert(radio.getXoscTrim(a,b) && a==3 && b==4);
radio.standby_result=0;radio.command_result=-72;
assert(radio.applyXoscTrim(8,9)==-72 && radio.trim_commands==2);
assert(radio.getXoscTrim(a,b) && a==3 && b==4);
radio.command_result=0;
assert(radio.restoreXoscTrim()==0 && radio.trim_a==3 && radio.trim_b==4);
radio.clearXoscTrim();const int before=radio.trim_commands;
assert(!radio.getXoscTrim(a,b) && radio.restoreXoscTrim()==0 && radio.trim_commands==before);
#else
assert(!radio.supportsXoscTrim());
assert(!radio.configureBootXoscTrim(true,3,4));
assert(radio.applyXoscTrim(3,4)==RADIOLIB_ERR_UNSUPPORTED && radio.trim_commands==0);
#endif
char result[160];radio.formatFrequencyError(result,sizeof(result));
assert(strstr(result,"unavailable"));
uint8_t packet[4];radio.sequence.clear();
assert(radio.readData(packet,4)==0);
assert(radio.sequence.size()==2 && radio.sequence[0]==3 && radio.sequence[1]==4);
radio.mod->hal->now+=2000;radio.freqMHz=915;
radio.formatFrequencyError(result,sizeof(result));
assert(strstr(result,"-1000 Hz") && strstr(result,"freq=910.525") && strstr(result,"age=2s"));
radio.raw_offset[0]=0;radio.raw_offset[1]=0;radio.raw_offset[2]=0;
radio.read_result=-7;
assert(radio.readData(packet,4)==-7);
radio.formatFrequencyError(result,sizeof(result));assert(strstr(result,"-1000 Hz"));
radio.read_result=0;radio.command_result=-72;
assert(radio.readData(packet,4)==0);
radio.formatFrequencyError(result,sizeof(result));assert(strstr(result,"-1000 Hz"));
radio.command_result=0;radio.modem=2;
const int status_before=radio.status_commands;
assert(radio.readData(packet,4)==0 && radio.status_commands==status_before);
radio.modem=1;radio.fifo=3;
assert(radio.readData(packet,4)==RADIOLIB_ERR_PACKET_TOO_SHORT && radio.clears==1);
radio.formatFrequencyError(result,sizeof(result));assert(strstr(result,"-1000 Hz"));
radio.fifo=4;
assert(radio.readData(packet,4)==0);
radio.formatFrequencyError(result,sizeof(result));assert(strstr(result,"+0 Hz") && strstr(result,"freq=915.000000"));
}
'''
WRAPPER = r'''
#pragma once
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <helpers/radiolib/LR2021CalibrationPolicy.h>
namespace mesh {enum class RadioParamApplyResult {APPLIED,BUSY,FAILED};}
inline uint32_t test_now=100;
inline uint32_t millis(){return test_now;}
class CustomLR2021Wrapper {
public:
bool supported=true,selected=false;
uint8_t a=0,b=0;
unsigned calls=0,boot_calls=0,cancels=0;
mesh::RadioParamApplyResult result=mesh::RadioParamApplyResult::APPLIED;
bool supportsXoscTrim() const {return supported;}
void cancelXoscTrimApply(){++cancels;}
bool getXoscTrim(uint8_t& first,uint8_t& second) const {
if(!selected)return false;
first=a;second=b;return true;
}
bool configureBootXoscTrim(bool enabled,uint8_t first,uint8_t second) {
++boot_calls;selected=enabled;a=first;b=second;return true;
}
mesh::RadioParamApplyResult trySetXoscTrim(bool enabled,uint8_t first,uint8_t second) {
++calls;
if(result==mesh::RadioParamApplyResult::APPLIED){selected=enabled;a=first;b=second;}
return result;
}
void formatFrequencyError(char* out,size_t size) const {snprintf(out,size,"> +125 Hz; age=1s");}
};
'''
SETTINGS = r'''
#include <cassert>
#include <cstring>
#include <helpers/radiolib/LR2021CalibrationSettings.h>
#include <helpers/radiolib/CustomLR2021Wrapper.h>
using namespace mesh::lr2021;
char reply[160];
static bool command(CustomLR2021Wrapper& radio,const char* text){return handleLR2021CalibrationCommand(text,reply,sizeof(reply),radio);}
int main() {
FILESYSTEM fs;CustomLR2021Wrapper radio;
assert(!loadLR2021CalibrationSettings(&fs,radio) && !radio.selected && !radio.calls);
assert(!command(radio,"get freq") && !command(radio,"get radio.xtrimmed"));
assert(command(radio,"get radio.xtrim") && !strcmp(reply,"> default"));
assert(command(radio,"get radio.freqerror") && strstr(reply,"+125 Hz"));
for(const char* bad:{"48","-1","1,48","1.5","1,2 extra",""}) {
const std::string input=std::string("set radio.xtrim ")+bad;
assert(command(radio,input.c_str()) && strstr(reply,"Error:"));
assert(!radio.calls && fs.files.empty());
}
assert(command(radio,"set radio.xtrim 3,4") && strstr(reply,"applying in 2s"));
assert(!radio.calls && !radio.selected && fs.files.count("/radio_xtrim"));
test_now+=1999;loopLR2021CalibrationSettings(radio);assert(!radio.calls && !radio.selected);
test_now+=1;loopLR2021CalibrationSettings(radio);
assert(radio.selected && radio.a==3 && radio.b==4 && radio.calls==1);
const auto original=fs.files["/radio_xtrim"];
const unsigned calls=radio.calls;
assert(command(radio,"set radio.xtrim 3,4") && strstr(reply,"unchanged") && radio.calls==calls);
for(int failure=0;failure<5;++failure) {
fs.fail_write=failure==0;fs.corrupt_write=failure==1;fs.fail_temp_read=failure==2;
fs.fail_publish=failure==3;fs.fail_open_write=failure==4;
assert(command(radio,"set radio.xtrim 8") && strstr(reply,"save failed"));
assert(radio.calls==calls && radio.a==3 && fs.files["/radio_xtrim"]==original);
}
fs.fail_write=fs.corrupt_write=fs.fail_temp_read=fs.fail_publish=fs.fail_open_write=false;
radio.result=mesh::RadioParamApplyResult::BUSY;
assert(command(radio,"set radio.xtrim 8") && strstr(reply,"applying in 2s"));
assert(radio.a==3 && radio.b==4);
assert(command(radio,"get radio.xtrim") && !strcmp(reply,"> 8,8 (pending)"));
const unsigned busy_calls=radio.calls;
test_now+=2000;
loopLR2021CalibrationSettings(radio);assert(radio.calls==busy_calls+1);
radio.result=mesh::RadioParamApplyResult::APPLIED;
loopLR2021CalibrationSettings(radio);
assert(radio.a==8 && radio.b==8);
assert(command(radio,"get radio.xtrim") && !strcmp(reply,"> 8,8"));
CustomLR2021Wrapper reboot;
assert(loadLR2021CalibrationSettings(&fs,reboot) && reboot.selected && reboot.a==8 && reboot.b==8 && !reboot.calls);
radio.result=mesh::RadioParamApplyResult::FAILED;
assert(command(radio,"set radio.xtrim 1,2") && strstr(reply,"applying in 2s"));
test_now+=2000;loopLR2021CalibrationSettings(radio);
assert(command(radio,"get radio.xtrim") && strstr(reply,"> 8,8 (failed"));
assert(loadLR2021CalibrationSettings(&fs,reboot) && reboot.a==8 && reboot.b==8);
radio.result=mesh::RadioParamApplyResult::BUSY;
assert(command(radio,"set radio.xtrim 4") && strstr(reply,"applying in 2s"));
test_now+=2000;loopLR2021CalibrationSettings(radio);
test_now+=30000;const unsigned timeout_calls=radio.calls,timeout_cancels=radio.cancels;
loopLR2021CalibrationSettings(radio);assert(radio.calls==timeout_calls+1);
assert(radio.cancels==timeout_cancels+1);
assert(command(radio,"get radio.xtrim") && strstr(reply,"> 8,8 (failed"));
assert(command(radio,"set radio.xtrim 4") && strstr(reply,"applying in 2s"));
test_now+=2000;loopLR2021CalibrationSettings(radio);
test_now+=29999;
loopLR2021CalibrationSettings(radio);
assert(command(radio,"get radio.xtrim") && !strcmp(reply,"> 4,4 (pending)"));
assert(command(radio,"set radio.xtrim 5") && strstr(reply,"applying in 2s"));
test_now+=2000;loopLR2021CalibrationSettings(radio);
test_now+=30000;const unsigned admitted_calls=radio.calls;
radio.result=mesh::RadioParamApplyResult::APPLIED;
loopLR2021CalibrationSettings(radio);assert(radio.calls==admitted_calls+1);
assert(radio.a==5 && radio.b==5);
assert(command(radio,"get radio.xtrim") && !strcmp(reply,"> 5,5"));
assert(command(radio,"set radio.xtrim default") && strstr(reply,"applying in 2s") && radio.selected);
test_now+=2000;loopLR2021CalibrationSettings(radio);assert(!radio.selected);
assert(loadLR2021CalibrationSettings(&fs,reboot) && !reboot.selected);
fs.files["/radio_xtrim.bak"]=original;fs.files["/radio_xtrim"]={0,1,2};
assert(loadLR2021CalibrationSettings(&fs,reboot) && reboot.a==3 && reboot.b==4);
fs.files["/radio_xtrim"][0]^=1;
assert(!loadLR2021CalibrationSettings(&fs,reboot) && !reboot.selected);
CustomLR2021Wrapper tcxo;tcxo.supported=false;
const auto before=fs.files;
assert(!loadLR2021CalibrationSettings(&fs,tcxo));
assert(command(tcxo,"set radio.xtrim 3") && strstr(reply,"TCXO") && fs.files==before && !tcxo.calls);
}
'''
class LR2021CalibrationTests(unittest.TestCase):
def compile_run(self, source, flags=(), stubs=None, production=None):
compiler = shutil.which(os.environ.get("CXX", "g++"))
if not compiler:
self.skipTest("C++ compiler unavailable")
with tempfile.TemporaryDirectory() as temporary:
work = Path(temporary)
for path, content in (stubs or {}).items():
target = work / path
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(content)
test = work / "test.cpp"
test.write_text(source)
binary = work / "test"
command = [compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror", "-I", str(work),
"-I", str(ROOT / "src"), *flags, str(test)]
if production:
command.append(str(production))
built = subprocess.run([*command, "-o", str(binary)], capture_output=True, text=True)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
run = subprocess.run([str(binary)], capture_output=True, text=True)
self.assertEqual(run.returncode, 0, run.stdout + run.stderr)
def test_policy(self):
self.compile_run(POLICY)
def test_driver_commands_and_measurements(self):
source = (RADIO / "CustomLR2021.h").read_text()
signatures = ["bool supportsXoscTrim() const", "bool configureBootXoscTrim(bool enabled, uint8_t a, uint8_t b)",
"bool getXoscTrim(uint8_t& a, uint8_t& b) const", "void clearXoscTrim()",
"int16_t applyXoscTrim(uint8_t a, uint8_t b)", "int16_t restoreXoscTrim()",
"int16_t readPacketFrequencyErrorHz(int32_t& value)",
"void formatFrequencyError(char* reply, size_t capacity) const",
"int16_t readData(uint8_t* data, size_t len) override"]
methods = "\n".join(method(source, signature).replace(" override", "") for signature in signatures)
for flags in [("-DLR2021_TCXO_VOLTAGE=0",), ("-DLR2021_TCXO_VOLTAGE=1",), ()]:
with self.subTest(flags=flags):
self.compile_run(DRIVER.replace("@METHODS@", methods), flags)
def test_checked_settings_and_pending_apply(self):
stubs = {"helpers/IdentityStore.h": FS_STUB.replace("/display_prefs", "/radio_xtrim"),
"helpers/radiolib/CustomLR2021Wrapper.h": WRAPPER}
# The source uses a quoted include; isolate it beside its mocked boundary.
source = (RADIO / "LR2021CalibrationSettings.cpp").read_text()
source = source.replace('#include "CustomLR2021Wrapper.h"',
'#include <helpers/radiolib/CustomLR2021Wrapper.h>')
source = source.replace('#include "LR2021CalibrationSettings.h"',
'#include <helpers/radiolib/LR2021CalibrationSettings.h>')
source = source.replace('#include "LR2021CalibrationPolicy.h"',
'#include <helpers/radiolib/LR2021CalibrationPolicy.h>')
self.compile_run(SETTINGS + "\n" + source, ("-DUSE_LR2021=1",), stubs)
def test_reset_and_morse_reapply_only_selected_trim(self):
source = (RADIO / "CustomLR2021.h").read_text()
self.assertIn("restoreXoscTrim()", body(source, "bool std_init("))
self.assertIn("restoreXoscTrim()", body(source, "int16_t beginMorseReceive("))
def test_all_cli_roles_load_before_init_and_service_pending(self):
for role in ("simple_repeater", "simple_room_server", "simple_sensor", "companion_radio"):
source = (ROOT / "examples" / role / "main.cpp").read_text()
setup = body(source, "void setup()")
with self.subTest(role=role):
self.assertLess(setup.index("loadLR2021CalibrationSettings("), setup.index("radio_init()"))
self.assertIn("loopLR2021CalibrationSettings(radio_driver)", source)
self.assertIn("#if defined(USE_LR2021)", source)
if __name__ == "__main__":
unittest.main()
+1
View File
@@ -129,6 +129,7 @@ class CustomLR2021:public LR2021 {
@ROUTING@
public:
using LR2021::LR2021;
int16_t restoreXoscTrim() { return 0; } // No calibration override in this regulator-only fixture.
@TABLE@
@INIT@
@MORSE@
+204
View File
@@ -0,0 +1,204 @@
"""Exercise actual LR2021 activity/admission/primary transaction under RX noise."""
from pathlib import Path
import os
import subprocess
import tempfile
import unittest
from test_radio_profile_scan import HARNESS
from test_radio_receive_contract import method
ROOT = Path(__file__).resolve().parents[1]
MAIN = r'''
static void noise(RadioLibWrapper& w, unsigned tick) {
w.chip.irq = (tick & 1) ? RADIOLIB_LR2021_IRQ_LORA_HEADER_VALID
: RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED;
}
int main() {
using Result = mesh::RadioParamApplyResult;
for (bool temporary : {false, true}) {
RadioLibWrapper w;
auto target = w._profiles.primary; target.freq = 910.54f;
bool applied = false;
for (unsigned tick = 0; tick < 751; ++tick) {
elapsed_us = 100000 + tick * 40000ULL; noise(w, tick);
auto result = w.trySetPrimaryParams(target, temporary);
if (result == Result::APPLIED) { applied = true; break; }
assert(result == Result::BUSY && w.applies == 0 && !w._primary_receive_preempt);
}
assert(applied && w.applies == 1 && w._cur_freq == target.freq);
assert(!w._primary_receive_preempt && w._profiles.primary_temporary == temporary);
}
// A different accepted tuple restarts the wait; invalid edits do not arm it.
{
RadioLibWrapper w;
auto target = w._profiles.primary; target.freq = 910.54f;
noise(w, 0); assert(w.trySetPrimaryParams(target, true) == Result::BUSY);
elapsed_us += 29000000; noise(w, 1);
target.freq = 911; assert(w.trySetPrimaryParams(target, true) == Result::BUSY);
assert(w.applies == 0);
}
// Even after its bound, no ordinary settings/scan/TX entry gets an exception.
{
RadioLibWrapper w;
auto target = w._profiles.primary; target.freq = 910.54f;
for (unsigned tick = 0; tick < 1500; ++tick) {
elapsed_us = 100000 + tick * 40000ULL; noise(w, tick);
assert(w.beginReconfigure() == 2);
assert(w.trySetParams(target.freq,target.bw,target.sf,target.cr) == Result::BUSY);
}
assert(w.applies == 0 && w.chip.standbys == 0);
}
// An opted-out wrapper retains its existing non-preemptive behavior.
{
RadioLibWrapper w; w.bounded = false;
auto target = w._profiles.primary; target.freq = 910.54f;
for (unsigned tick = 0; tick < 1500; ++tick) {
elapsed_us = 100000 + tick * 40000ULL; noise(w, tick);
assert(w.trySetPrimaryParams(target, true) == Result::BUSY);
}
assert(w.applies == 0 && w.chip.standbys == 0);
}
// Completion, chip BUSY and every exclusive owner remain stronger than the
// bounded command. Once each guard clears the accepted tuple still applies.
for (unsigned guard = 0; guard < 7; ++guard) {
RadioLibWrapper w;
auto target = w._profiles.primary; target.freq = 910.54f;
noise(w, 0); assert(w.trySetPrimaryParams(target, false) == Result::BUSY);
elapsed_us += 31000000; noise(w, 1);
if (guard == 0) w.events.record();
if (guard == 1) w.busy = true;
if (guard == 2) w.state = STATE_TX_WAIT;
if (guard == 3) w._cw_active = true;
if (guard == 4) w._beacon_owned = true;
if (guard == 5) w._morse_owned = true;
if (guard == 6) w._morse_rx_owned = true;
assert(w.trySetPrimaryParams(target, false) == Result::BUSY);
assert(w.applies == 0 && w.chip.standbys == 0);
w.events.claim(); w.busy = w._cw_active = w._beacon_owned = false;
w._morse_owned = w._morse_rx_owned = false; w.state = STATE_RX;
assert(w.trySetPrimaryParams(target, false) == Result::APPLIED);
}
// A completion racing the activity probe or the actual standby retains its
// old tuple/FIFO; neither settings apply nor receive rearm may consume it.
for (bool during_standby : {false,true}) {
RadioLibWrapper w;
auto target = w._profiles.primary; target.freq = 910.54f;
noise(w, 0); assert(w.trySetPrimaryParams(target, false) == Result::BUSY);
elapsed_us += 31000000; noise(w, 1);
if (during_standby) w.chip.standby_irq = &w.events;
else w.activity_irq = true;
assert(w.trySetPrimaryParams(target, false) == Result::BUSY);
assert(w.events.pending() && w.applies == 0 && w._cur_freq != target.freq);
assert(w.chip.standbys == unsigned(during_standby));
w.events.claim(); w.activity_irq = false; w.chip.standby_irq = nullptr;
assert(w.trySetPrimaryParams(target, false) == Result::APPLIED);
}
// Hardware failure and failed RX resume keep ordinary rollback semantics.
for (unsigned failure = 0; failure < 3; ++failure) {
RadioLibWrapper w;
auto original = w._profiles.primary;
auto target = original; target.freq = 910.54f;
noise(w, 0); assert(w.trySetPrimaryParams(target, true) == Result::BUSY);
elapsed_us += 31000000; noise(w, 1);
if (failure == 0) w.chip.standby_error = -706;
if (failure == 1) w.fail = true;
if (failure == 2) w.failRxStarts = 1;
assert(w.trySetPrimaryParams(target, true) == Result::FAILED);
assert(w._profiles.primary == original && !w._profiles.primary_temporary);
assert(w._cur_freq == original.freq && !w._primary_receive_preempt);
w.chip.standby_error = 0; w.fail = false;
assert(w.trySetPrimaryParams(target, true) == Result::APPLIED);
}
// The public deadline helper handles long-airtime cap and millis rollover.
{
mesh::PrimaryRadioApplyWait wait;
mesh::RadioProfileParams slow; slow.freq=910; slow.bw=7.8f; slow.sf=12; slow.cr=8;
const uint32_t start = UINT32_MAX - 10000;
assert(!wait.mayAbandonReceive(start,slow,false,slow,65528,65528,255));
assert(!wait.mayAbandonReceive(start+29999,slow,false,slow,65528,65528,255));
assert(wait.mayAbandonReceive(start+30000,slow,false,slow,65528,65528,255));
wait.clear();
assert(!wait.mayAbandonReceive(start+30001,slow,false,slow,65528,65528,255));
}
}
'''
class LR2021PrimaryApplyWaitTests(unittest.TestCase):
def test_actual_activity_admission_primary_transaction(self):
wrapper = (ROOT / 'src/helpers/radiolib/RadioLibWrappers.cpp').read_text()
native = (ROOT / 'src/helpers/radiolib/CustomLR2021.h').read_text()
signatures = ('uint8_t RadioLibWrapper::beginReconfigure(',
'void RadioLibWrapper::endReconfigure(',
'mesh::RadioParamApplyResult RadioLibWrapper::trySetParams(',
'mesh::RadioParamApplyResult RadioLibWrapper::trySetPrimaryParams(')
methods = '\n'.join(method(wrapper, signature) for signature in signatures)
template = HARNESS[:HARNESS.index('int main()')]
template = template.replace('#include <cassert>', '''#include <cassert>
#define USE_LR2021 1
#define MESH_ENABLE_TX_BEACONS 1
#define MESH_ENABLE_CARRIER_TOOLS 1
#define MESH_ENABLE_MORSE_RX 1
#define MAX_TRANS_UNIT 255
#define RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED 32
#define RADIOLIB_LR2021_IRQ_LORA_HEADER_VALID 64
#define RADIOLIB_LR2021_IRQ_LORA_HDR_CRC_ERROR 512
#include <helpers/radiolib/PrimaryRadioApplyWait.h>
''')
chip = '''struct Chip {
bool standbyXOSC=false, _headerSeen=false;
uint32_t _activityAt=0, _preambleMillis=66, _maxPayloadMillis=3934, irq=0;
unsigned standbys=0;
int standby_error=0;
RadioInterruptEvents* standby_irq=nullptr;
bool isChipBusy(){return false;}
uint32_t getIrqStatus(){return irq;}
int clearIrqFlags(uint32_t flags){irq &= ~flags; return 0;}
int standby(){++standbys;if(standby_irq)standby_irq->record();return standby_error;}
@ACTIVITY@
};'''.replace('@ACTIVITY@',method(native,'bool isReceiving()'))
template = template.replace('struct Chip { bool standbyXOSC=false; int standby() { return 0; } };',chip)
template = template.replace(' bool _cw_active=false;', ''' bool _cw_active=false;
bool _beacon_owned=false, _beacon_restoring=false, _morse_owned=false, _morse_rx_owned=false;
bool _primary_receive_preempt=false, bounded=true, activity_irq=false;
mesh::PrimaryRadioApplyWait _primary_apply_wait;
bool supportsBoundedPrimaryReconfigure() const { return bounded; }
void noteReconfigureAdmission(const char*,int16_t) {}
uint16_t currentPreambleLength()const{return _physical_preamble;}
''')
template = template.replace('bool isReceivingPacket() { return packet; }',
'bool isReceivingPacket() { if(activity_irq)events.record();return chip.isReceiving(); }')
code = template.replace('@METHODS@',methods) + MAIN
with tempfile.TemporaryDirectory() as folder:
cpp, exe = Path(folder)/'test.cpp', Path(folder)/'test'
(Path(folder)/'Arduino.h').write_text('#pragma once\n#include <cstdint>\n#include <cmath>\n')
cpp.write_text(code)
result = subprocess.run([os.environ.get('CXX','g++'),'-std=c++17','-Wall','-Wextra',
'-fsanitize=address,undefined','-fno-pie','-no-pie',
'-I',folder,'-I',str(ROOT/'src'),str(cpp),'-o',str(exe)],
capture_output=True,text=True)
self.assertEqual(result.returncode,0,result.stderr)
result = subprocess.run([str(exe)],capture_output=True,text=True)
self.assertEqual(result.returncode,0,result.stderr)
# The regression must fail with either the former unbounded
# activity gate or a removed completion guard, not just compile.
mutations = (
('isReceivingPacket() && !_primary_receive_preempt', 'isReceivingPacket()'),
('if (hasPendingRadioInterrupt())', 'if (false)'),
)
for old, replacement in mutations:
self.assertIn(old, code)
cpp.write_text(code.replace(old,replacement))
result = subprocess.run([os.environ.get('CXX','g++'),'-std=c++17',
'-I',folder,'-I',str(ROOT/'src'),str(cpp),'-o',str(exe)],
capture_output=True,text=True)
self.assertEqual(result.returncode,0,result.stderr)
result = subprocess.run([str(exe)],capture_output=True,text=True)
self.assertNotEqual(result.returncode,0,'negative control unexpectedly passed')
self.assertIn('Assertion',result.stderr)
if __name__ == '__main__':
unittest.main()
+166
View File
@@ -0,0 +1,166 @@
"""Real trim transaction and LR2021 RX-noise admission, with completed IRQ races."""
from pathlib import Path
import subprocess
import tempfile
import unittest
import test_lr2021_apply_transaction as transaction_fixture
from test_lr2021_apply_transaction import method
ROOT = Path(__file__).resolve().parents[1]
MAIN = r'''
static void noise(Wrapper& w,unsigned tick) {
w.chip.irq=(tick&1)?RADIOLIB_LR2021_IRQ_LORA_HEADER_VALID:RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED;
}
static void arm(Wrapper& w) {
now_ms=100;noise(w,0);
assert(w.trySetXoscTrim(true,8,8)==mesh::RadioParamApplyResult::BUSY);
}
int main(){
using Result=mesh::RadioParamApplyResult;
{
Wrapper w;bool applied=false;
for(unsigned tick=0;tick<751;++tick){
now_ms=100+tick*40;noise(w,tick);
const auto result=w.trySetXoscTrim(true,8,8);
if(result==Result::APPLIED){applied=true;break;}
assert(result==Result::BUSY&&!w._primary_receive_preempt&&w.chip.commands==0);
}
assert(applied&&w.chip.enabled&&w.chip.a==8&&w.chip.b==8&&w.rearms==1);
assert(!w._primary_receive_preempt&&w._trim_apply_key==UINT16_MAX);
}
// Ordinary reconfiguration never inherits the expired accepted-trim lease.
{
Wrapper w;arm(w);
for(unsigned tick=1;tick<1500;++tick){
now_ms=100+tick*40;noise(w,tick);assert(w.beginReconfigure()==2);
}
assert(w.chip.standbys==0&&w.chip.commands==0);
}
// Every protected owner and completed packet remains stronger than the bound.
for(unsigned guard=0;guard<7;++guard){
Wrapper w;arm(w);now_ms+=31000;noise(w,1);
if(guard==0)w.pending=true;
if(guard==1)w.busy=true;
if(guard==2)w.state=STATE_TX_WAIT;
if(guard==3)w._cw_active=true;
if(guard==4)w._beacon_owned=true;
if(guard==5)w._morse_owned=true;
if(guard==6)w._morse_rx_owned=true;
assert(w.trySetXoscTrim(true,8,8)==Result::BUSY);
assert(w.chip.commands==0&&w.chip.standbys==0&&!w._primary_receive_preempt);
w.pending=w.busy=w._cw_active=w._beacon_owned=w._morse_owned=w._morse_rx_owned=false;
w.state=STATE_RX;
// Morse is checked before the timer starts, so release then permit its own
// complete bound instead of borrowing another owner's elapsed time.
auto result=w.trySetXoscTrim(true,8,8);
if(result==Result::BUSY){now_ms+=31000;noise(w,0);result=w.trySetXoscTrim(true,8,8);}
assert(result==Result::APPLIED);
}
// Completion during the probe or standby preserves the old packet and trim.
for(bool during_standby:{false,true}){
Wrapper w;arm(w);now_ms+=31000;noise(w,1);
if(during_standby)w.chip.standby_irq=&w.pending;else w.activity_irq=true;
assert(w.trySetXoscTrim(true,8,8)==Result::BUSY);
assert(w.pending&&w.chip.commands==0&&!w.chip.enabled&&!w._primary_receive_preempt);
assert(w.chip.standbys==unsigned(during_standby));
w.pending=w.activity_irq=false;w.chip.standby_irq=nullptr;
assert(w.trySetXoscTrim(true,8,8)==Result::APPLIED);
}
// Replacing the requested capacitor codes must restart the admission wait.
{
Wrapper w;arm(w);now_ms+=31000;noise(w,1);
assert(w.trySetXoscTrim(true,9,10)==Result::BUSY);
assert(w.chip.commands==0);
now_ms+=31000;noise(w,0);
assert(w.trySetXoscTrim(true,9,10)==Result::APPLIED&&w.chip.a==9&&w.chip.b==10);
}
// Timeout/cancellation followed by an identical retry earns a fresh wait.
{
Wrapper w;arm(w);now_ms+=31000;noise(w,1);w.pending=true;
assert(w.trySetXoscTrim(true,8,8)==Result::BUSY);
w.cancelXoscTrimApply();w.pending=false;
assert(w.trySetXoscTrim(true,8,8)==Result::BUSY&&w.chip.commands==0);
now_ms+=31000;noise(w,0);
assert(w.trySetXoscTrim(true,8,8)==Result::APPLIED);
}
// A scope restores its caller's admission flag on both guarded and successful
// returns, rather than clearing a surrounding accepted-primary transaction.
for(bool guarded:{false,true}){
Wrapper w;w._primary_receive_preempt=true;w.pending=guarded;
assert(w.trySetXoscTrim(true,8,8)==(guarded?Result::BUSY:Result::APPLIED));
assert(w._primary_receive_preempt);
}
}
'''
class Capture:
def run_cpp(self, code):
self.code = code
class LR2021TrimApplyWaitTests(unittest.TestCase):
def test_actual_trim_activity_admission_and_completion_guards(self):
captured = Capture()
transaction_fixture.LR2021ApplyTransactionTest.test_real_trim_transaction_restores_selection_modem_and_receiver(captured)
code = captured.code[:captured.code.index('int main()')]
code = code.replace('#include <cassert>', '''#include <cassert>
#define USE_LR2021 1
#define MESH_ENABLE_TX_BEACONS 1
#define STATE_IDLE 0
#define STATE_RX 1
#define STATE_TX_WAIT 3
#define RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED 32
#define RADIOLIB_LR2021_IRQ_LORA_HEADER_VALID 64
#define RADIOLIB_LR2021_IRQ_LORA_HDR_CRC_ERROR 512
#define MESH_DEBUG_PRINTLN(...) ((void)0)
''')
native = (ROOT/'src/helpers/radiolib/CustomLR2021.h').read_text()
code = code.replace('struct CustomLR2021 {', '''struct CustomLR2021 {
uint32_t _activityAt=0,_preambleMillis=66,_maxPayloadMillis=3934,irq=0;
bool _headerSeen=false;
unsigned standbys=0;
bool* standby_irq=nullptr;
bool isChipBusy(){return false;}
uint32_t getIrqStatus(){return irq;}
int clearIrqFlags(uint32_t flags){irq &= ~flags;return 0;}
int standby(){++standbys;if(standby_irq)*standby_irq=true;return 0;}
''' + method(native,'bool isReceiving()'))
code = code.replace(' bool _primary_receive_preempt=false;', ''' bool _primary_receive_preempt=false;
bool pending=false,busy=false,activity_irq=false;
bool _cw_active=false,_beacon_owned=false,_beacon_restoring=false;
bool _rx_hold_continuous=false,_rx_ps_armed=false;
uint8_t state=STATE_RX;
bool hasPendingRadioInterrupt()const{return pending;}
bool claimRadioInterrupt(){const bool had=pending;pending=false;return had;}
bool isChipBusy()const{return busy;}
bool isReceivingPacket(){if(activity_irq)pending=true;return chip.isReceiving();}
''')
wrapper = (ROOT/'src/helpers/radiolib/RadioLibWrappers.cpp').read_text()
admission = method(wrapper,'uint8_t RadioLibWrapper::beginReconfigure()').replace('RadioLibWrapper::','')
code = code.replace(method(code,'uint8_t beginReconfigure()'),admission)
code = code.replace('else rx=true;','else {rx=true;state=STATE_RX;}')
code = code.replace('bool isInRecvMode()const{return rx;}','bool isInRecvMode()const{return state==STATE_RX;}')
code += MAIN
with tempfile.TemporaryDirectory(prefix='lr2021-trim-admission-') as folder:
cpp,exe=Path(folder)/'test.cpp',Path(folder)/'test'
for mutation in (False,True):
cpp.write_text(code.replace('isReceivingPacket() && !_primary_receive_preempt',
'isReceivingPacket()') if mutation else code)
built=subprocess.run(['g++','-std=c++17','-Wall','-Wextra','-Werror',
'-fsanitize=address,undefined','-fno-sanitize-recover=all',
'-fno-pie','-no-pie','-I',str(ROOT/'src'),str(cpp),'-o',str(exe)],
capture_output=True,text=True)
self.assertEqual(built.returncode,0,built.stderr)
ran=subprocess.run([str(exe)],capture_output=True,text=True)
if mutation:
self.assertNotEqual(ran.returncode,0,'unbounded admission negative control passed')
self.assertIn('Assertion',ran.stderr)
else:
self.assertEqual(ran.returncode,0,ran.stderr)
if __name__=='__main__':
unittest.main()
+1
View File
@@ -220,6 +220,7 @@ public:
};
class CustomLR2021:public LR2021 {
@REGULATOR@
int16_t restoreXoscTrim() { return 0; } // No calibration override in this RF-switch fixture.
public:
using LR2021::LR2021;
uint32_t _rf_switch_pins[Module::RFSWITCH_MAX_PINS]={};
+10 -2
View File
@@ -25,10 +25,17 @@ COMMON = r'''
#include <initializer_list>
#include <RadioProfiles.h>
#include <helpers/radiolib/LR1110ProfileSwitchState.h>
#include <helpers/radiolib/LR2021ApplyStatus.h>
#define MESH_ENABLE_TX_BEACONS 1
#define MESH_ENABLE_MESHTEXT_WIRE 1
#define RADIOLIB_ERR_NONE 0
#define RADIOLIB_LR2021_LORA_SYNC_WORD_PRIVATE 0x12
struct ApplyRecorder {
mesh::lr2021::ApplyStatus _apply_status;
int16_t recordApply(mesh::lr2021::ApplyStatus::Stage stage, int16_t code) {
return _apply_status.record(stage, code);
}
};
struct Wire {
unsigned calls = 0, fail = 0;
uint8_t sync = 0x12;
@@ -74,7 +81,7 @@ struct Custom@FAMILY@ : @FAMILY@ {
bool _profilePacketDirty = true;
@PROTOCOL@
};
struct Wrapper {
struct Wrapper : ApplyRecorder {
Custom@FAMILY@ chip;
Custom@FAMILY@* _radio = &chip;
@SUPPORT@
@@ -138,6 +145,7 @@ int main() {
#define USE_LR2021 1
#define LORA_BW 125
#define RADIOLIB_ERR_UNKNOWN -1
#define RADIOLIB_ERR_INVALID_SIDE_DETECT -1000
#define RADIOLIB_CHANNEL_FREE -2
#define MESH_DEBUG_PRINTLN(...) ((void)0)
#include <helpers/radiolib/LR2021SideDetectorConfig.h>
@@ -176,7 +184,7 @@ struct Board {
unsigned watchdog_services = 0;
void serviceWatchdog() { ++watchdog_services; }
};
struct Wrapper : RadioLibWrapper {
struct Wrapper : RadioLibWrapper, ApplyRecorder {
CustomLR2021 chip;
CustomLR2021* _radio = &chip;
Board board;
+12
View File
@@ -139,6 +139,16 @@ struct LR2021 {
struct Chip : LR2021 {
uint32_t _activityAt=0, _preambleMillis=66, _maxPayloadMillis=3934;
bool _headerSeen=false;
float freqMHz=910.525f, _last_frequency_error_mhz=0;
int32_t _last_frequency_error_hz=0;
uint32_t _last_frequency_error_at=0;
bool _last_frequency_error_valid=false;
struct MeasurementHal {uint32_t millis(){return ::millis();}} measurement_hal;
struct MeasurementModule {MeasurementHal* hal;} measurement_mod{&measurement_hal};
MeasurementModule* mod=&measurement_mod;
// The packet-status wire decoder has its own production-method tests. Keep
// this fixture focused on preserving the IRQ/FIFO while publishing metadata.
int16_t readPacketFrequencyErrorHz(int32_t& hz){hz=250;return 0;}
bool isChipBusy(){return false;}
@LR_METHODS@
};
@@ -153,6 +163,7 @@ struct RadioLibWrapper {
bool _loop_event_pending=false, _cw_active=false, _profile_repair_required=false, _profile_refresh_required=false;
bool _rx_ps_armed=false, _rx_hold_continuous=false, _rx_ps_enabled=false;
bool _rx_ps_continuous_fallback=false, _nf_calib_active=false;
bool _primary_receive_preempt=false;
uint32_t _rx_ps_eff_rx_us=0, _rx_ps_eff_sleep_us=0;
float _last_snr=0, _last_rssi=0;
unsigned n_recv=0,n_recv_errors=0,n_sent=0;
@@ -168,6 +179,7 @@ public:
bool registerInterruptAction();void unregisterInterruptAction();
bool hasAssertedRadioInterrupt()const;bool hasPendingRadioInterrupt()const;bool claimRadioInterrupt();
uint8_t beginReconfigure();
void noteReconfigureAdmission(const char*,int16_t){}
void requestRestartRecv();bool isPacketReady();
bool startSendRaw(const uint8_t*,int);bool isSendComplete();
int recvRaw(uint8_t*,int);
+1
View File
@@ -81,6 +81,7 @@ class Custom:public @CHIP@ {
RfSwitch _morse_rf_switch;
public:
int restoreRfSwitchTable() { return 0; }
int restoreXoscTrim() { return 0; }
@INIT@
@MORSE@
int init() { return @INIT_CALL@; }
+2
View File
@@ -49,6 +49,8 @@ struct Driver {
bool _rx_ps_armed = false, _cw_active = false;
bool _profile_repair_required = false, _profile_refresh_required = false;
bool _rx_hold_continuous = false;
bool _primary_receive_preempt = false;
void noteReconfigureAdmission(const char*, int16_t) {}
bool _dbm_valid = true, _dbm_applied = true, _rx_boosted_gain_valid = false;
bool _rx_boosted_gain_applied = false;
int8_t _cur_dbm = 7;