Files
MeshCore/test/test_radio_regulator_modes.py
T
mikecarper 8a8c80ee6a 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.
2026-10-26 15:55:36 -07:00

202 lines
8.9 KiB
Python

"""Execute boot selection, regulator errors, Morse resets and radio recovery."""
from pathlib import Path
import unittest
from test_morse_tx import compile_run
from test_radio_receive_contract import method
from radio_regulator_harness import regulator_members
ROOT = Path(__file__).resolve().parents[1]
HARNESS = r'''
#include <cassert>
#include <cstdint>
#include <cstddef>
#include <initializer_list>
#include <helpers/radiolib/RadioRegulatorState.h>
#define RADIOLIB_ERR_NONE 0
#define RADIOLIB_ERR_UNKNOWN -16
#define RADIOLIB_SX126X_SYNC_WORD_PRIVATE 0x12
#define RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE 0x12
#define RADIOLIB_LR2021_LORA_SYNC_WORD_PRIVATE 0x12
#define LORA_FREQ 909.5f
#define LORA_BW 500.0f
#define LORA_SF 5
#define LORA_CR 5
#define LORA_TX_POWER 2
#define MESH_DEBUG_PRINTLN(...) ((void)0)
struct SPIClass {};
struct Module {};
namespace mesh {
struct Logger { void print(const char*) {} void println(int) {} };
inline Logger& usbLoggingPort() { static Logger logger;return logger; }
struct MorseRx { static constexpr float ReceiverBitRateKbps=4.8f; };
namespace lr2021 { inline bool isHighBand(float f) { return f>1900; } }
}
struct Native {
int fail=0,spi=0,regulator_writes=0,modem_resets=0;
bool useRegulatorLDO=true,standbyXOSC=true;
float tcxoVoltage=1.6f;
uint8_t syncWordLength=4,preambleDetLength=4,gainModeLf=1,gainModeHf=3;
int step(int phase) { ++spi;return fail==phase ? -100-phase : 0; }
int nativeReset() { ++modem_resets;useRegulatorLDO=true;return step(1); }
int16_t begin(float,float,uint8_t,uint8_t,uint8_t,int8_t,uint16_t,float) {
return nativeReset();
}
int16_t begin(float,float,uint8_t,uint8_t,uint8_t,int8_t,uint16_t,float,bool ldo) {
assert(!standbyXOSC);
int result=nativeReset();
if(!result) { ++regulator_writes;result=step(2);if(!result)useRegulatorLDO=ldo; }
return result;
}
virtual int16_t beginFSK(float,float,float,float,int8_t,uint16_t,float,bool ldo) {
int result=nativeReset();
if(!result) { ++regulator_writes;result=step(2);if(!result)useRegulatorLDO=ldo; }
return result;
}
int16_t beginGFSK(float,float,float,float,int8_t,uint16_t,float) { return nativeReset(); }
int16_t setRegulatorDCDC() {
++regulator_writes;int result=step(2);if(!result)useRegulatorLDO=false;return result;
}
int16_t setRegulatorLDO() {
++regulator_writes;int result=step(2);if(!result)useRegulatorLDO=true;return result;
}
int16_t applyMeshCoreTcxoDelay() { return step(3); }
int16_t setCRC(int) { return step(4); }
int16_t setRxBoostedGainMode(int) { return step(5); }
int16_t setSyncWord(uint8_t*,size_t) { return step(6); }
int16_t explicitHeader() { return step(7); }
int16_t setDioAsRfSwitch(uint8_t,uint8_t,uint8_t,uint8_t,uint8_t,uint8_t,uint8_t,uint8_t) { return step(8); }
struct Profile { void invalidate() {} };
struct RfSwitch {
template<class Callback> int restore(Callback callback) {
const uint8_t modes[]={0,0,0,0,0,0,0};return callback(0,modes);
}
};
};
using @CHIP@=Native;
class Custom:public @CHIP@ {
@POLICY@
bool _coldStandby=false,_tcxoWakePending=false,_profilePacketDirty=true,_rx_boosted=false;
Profile _profileSwitch;
RfSwitch _morse_rf_switch;
public:
int restoreRfSwitchTable() { return 0; }
int restoreXoscTrim() { return 0; }
@INIT@
@MORSE@
int init() { return @INIT_CALL@; }
int morse() { return @MORSE_CALL@; }
};
@WRAPPER@
int main() {
using mesh::power::RegulatorMode;
constexpr bool supported_dcdc=@DCDC@;
for (auto requested:{RegulatorMode::Default,RegulatorMode::Ldo,RegulatorMode::Dcdc}) {
Custom radio;Wrapper wrapper{&radio};
assert(wrapper.activeRegulatorMode()==RegulatorMode::Default && radio.spi==0);
assert(!wrapper.configureBootRegulator(static_cast<RegulatorMode>(3)));
assert(wrapper.regulatorModes()==radio.regulatorModes());
const bool supported=requested!=RegulatorMode::Dcdc || supported_dcdc;
assert(wrapper.configureBootRegulator(requested)==supported);
assert(radio.spi==0); // boot configuration cannot send commands mid-RX/TX
if(!supported) continue;
const auto expected=requested==RegulatorMode::Default ? wrapper.defaultRegulatorMode() : requested;
assert(radio.init()==0 && wrapper.activeRegulatorMode()==expected);
assert(radio.useRegulatorLDO==(expected==RegulatorMode::Ldo));
assert(!wrapper.configureBootRegulator(RegulatorMode::Default));
// Both transitions reset the native modem; the saved choice survives each.
assert(radio.morse()==0 && wrapper.activeRegulatorMode()==expected);
assert(radio.useRegulatorLDO==(expected==RegulatorMode::Ldo));
assert(radio.init()==0 && wrapper.activeRegulatorMode()==expected);
assert(radio.useRegulatorLDO==(expected==RegulatorMode::Ldo));
for(int fault:{1,2}) {
if(fault==2 && @SKIP_LDO_WRITE@) continue;
radio.fail=fault;
assert(radio.morse()!=0);
assert(wrapper.activeRegulatorMode()==RegulatorMode::Default);
assert(!wrapper.configureBootRegulator(RegulatorMode::Ldo));
radio.fail=0;
assert(radio.init()==0 && wrapper.activeRegulatorMode()==expected);
assert(radio.useRegulatorLDO==(expected==RegulatorMode::Ldo));
radio.fail=fault;
assert(radio.init()!=0 && wrapper.activeRegulatorMode()==RegulatorMode::Default);
radio.fail=0;
assert(radio.init()==0 && wrapper.activeRegulatorMode()==expected);
}
}
// A failed first init must lock the boot policy as firmly as a successful one.
Custom failed;
assert(failed.configureBootRegulator(RegulatorMode::Ldo));failed.fail=1;
assert(failed.init()!=0 && !failed.configureBootRegulator(RegulatorMode::Default));
}
'''
class RadioRegulatorTests(unittest.TestCase):
def test_thinknode_m4_recipe_uses_lr1110_capture_cost_before_target_headers(self):
recipe = (ROOT / 'variants/thinknode_m4/platformio.ini').read_text()
family_flags = ['-DUSE_LR1110=1'] if ' -D USE_LR1110' in recipe else []
compile_run(self, r'''
#include <helpers/radiolib/RXPowerSaving.h>
#include <cassert>
int main() {
// CommonCLI includes its RXPS helpers before target.h can select a wrapper.
// The board recipe must therefore select the LR11x0 eight-symbol cost.
assert(rxPowerSavingDefaultCaptureCost() == 8.0f);
uint32_t family_rx=0, family_sleep=0, explicit_rx=0, explicit_sleep=0;
assert(calcRxPowerSavingLevel(9,8,62.5f,32,&family_rx,&family_sleep));
assert(calcRxPowerSavingLevel(9,8,62.5f,32,&explicit_rx,&explicit_sleep,8.0f));
assert(family_rx==explicit_rx && family_sleep==explicit_sleep);
}
''', flags=family_flags)
def exercise(self, chip, flags=(), dcdc=True):
source = (ROOT / f'src/helpers/radiolib/Custom{chip}.h').read_text()
if chip == 'LR2021':
initialize = method(source, 'bool std_init(')
init_call = 'std_init() ? 0 : -1'
else:
initialize = method(source, 'int16_t begin(float')
init_call = 'begin()'
if chip == 'SX1262':
morse = method(source, 'int16_t beginFSK(float')
morse_call = 'beginFSK()'
else:
morse = method(source, 'int16_t beginMorseReceive(')
morse_call = 'beginMorseReceive(902.1f)'
wrapper_source = (ROOT / f'src/helpers/radiolib/Custom{chip}Wrapper.h').read_text()
delegates = '\n'.join(method(wrapper_source, signature).replace(' override', '')
for signature in ('uint8_t regulatorModes(',
'mesh::power::RegulatorMode defaultRegulatorMode(',
'bool configureBootRegulator(', 'mesh::power::RegulatorMode activeRegulatorMode('))
wrapper = f'using Custom{chip}=Custom;\nstruct Wrapper {{ Custom* _radio;\n{delegates}\n}};'
code = HARNESS.replace('@WRAPPER@', wrapper)
for marker, value in {'@CHIP@': chip, '@POLICY@': regulator_members(source),
'@INIT@': initialize, '@MORSE@': morse, '@INIT_CALL@': init_call,
'@MORSE_CALL@': morse_call, '@DCDC@': str(dcdc).lower(),
'@SKIP_LDO_WRITE@': str(chip == 'LR2021' and not dcdc).lower()}.items():
code = code.replace(marker, value)
compile_run(self, code, flags=('-Wno-unused-parameter', *flags))
def test_sx1262_boot_selection_morse_failures_and_recovery(self):
self.exercise('SX1262')
def test_sx1262_forced_ldo_recipe_cannot_enable_missing_dcdc_network(self):
self.exercise('SX1262', ('-DSX126X_USE_REGULATOR_LDO=1',), dcdc=False)
def test_lr1110_boot_selection_morse_failures_and_recovery(self):
self.exercise('LR1110')
def test_lr1121_boot_selection_morse_failures_and_recovery(self):
self.exercise('LR1121')
def test_lr2021_verified_layout_boot_selection_morse_failures_and_recovery(self):
self.exercise('LR2021', ('-DLR2021_USE_DCDC=1',))
def test_lr2021_unverified_layout_cannot_enable_simo(self):
self.exercise('LR2021', ('-DLR2021_USE_DCDC=0',), dcdc=False)
if __name__ == '__main__':
unittest.main()