Files
MeshCore/test/test_lr2021_tx_prearm.py
T

479 lines
20 KiB
Python

"""Exercise the W12 TX warm-up using real driver methods and board routing.
The native radio mock models the distinction between host RF-switch GPIOs and
LR2021 DIO masks. No PlatformIO or RF hardware is needed. Failed SPI responses
may still apply a mask, so cleanup must not assume an error left a pin unchanged.
"""
from pathlib import Path
import os
import re
import shutil
import subprocess
import tempfile
import unittest
from radio_regulator_harness import regulator_members
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].replace(" override", "")
HARNESS = r'''
#include <algorithm>
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
#include <helpers/radiolib/LR2021Band.h>
#include <helpers/radiolib/RadioRegulatorState.h>
@POLICY@
#define LR2021_TX_BUSY_TIMEOUT_MS 4UL
#define LR2021_USE_DCDC 0
#define RADIOLIB_ERR_NONE 0
#define RADIOLIB_ERR_UNKNOWN -16
#define RADIOLIB_ERR_UNSUPPORTED -99
#define RADIOLIB_ERR_SPI_CMD_TIMEOUT -705
#define RADIOLIB_NC 0xffffffffU
#define RFSWITCH_PIN_FLAG (1UL << 31)
#define RADIOLIB_LRXXXX_DIOx_VAL(pin) ((pin) & ~RFSWITCH_PIN_FLAG)
#define RADIOLIB_LR2021_DIO5 (RFSWITCH_PIN_FLAG | 0)
#define RADIOLIB_LR2021_DIO6 (RFSWITCH_PIN_FLAG | 1)
#define RADIOLIB_LR2021_DIO9 (RFSWITCH_PIN_FLAG | 4)
#define RADIOLIB_LR2021_DIO10 (RFSWITCH_PIN_FLAG | 5)
#define RADIOLIB_LR2021_DIO11 (RFSWITCH_PIN_FLAG | 6)
#define RADIOLIB_LR2021_DIO_SLEEP_PULL_UP 2
#define RADIOLIB_LR2021_DIO_SLEEP_PULL_AUTO 3
#define RADIOLIB_LR2021_DIO_FUNCTION_RF_SWITCH 0x20
#define RADIOLIB_LR2021_TX_TIMEOUT_NONE 0
#define RADIOLIB_LR2021_TX_TEST_MODE_CW 0
#define RADIOLIB_LR2021_LORA_SYNC_WORD_PRIVATE 0x12
#define RADIOLIB_LR2021_RX_TIMEOUT_INF 0xffffff
#define RADIOLIB_IRQ_RX_DEFAULT_FLAGS 0
#define RADIOLIB_IRQ_RX_DEFAULT_MASK 0
#define RADIOLIB_IRQ_PREAMBLE_DETECTED 5
#define RADIOLIB_RADIO_MODE_NONE 0
#define RADIOLIB_RADIO_MODE_RX 1
#define RADIOLIB_RADIO_MODE_TX 2
#define LOW 0
#define HIGH 1
#define END_OF_MODE_TABLE {0, {0, 0, 0, 0, 0}}
#define LORA_FREQ 909.5f
#define LORA_BW 500.0f
#define LORA_SF 5
#define LORA_CR 5
#define LORA_TX_POWER 2
using RadioLibTime_t = uint32_t;
struct SPIClass {};
struct Logger { void print(const char*) {} void println(int) {} };
namespace mesh { Logger& usbLoggingPort() { static Logger logger;return logger; } }
struct Hal {
uint32_t GpioLevelHigh=HIGH;
uint32_t now=0;
bool busy=false;
int delays=0;
uint32_t millis() { return now; }
int digitalRead(uint32_t) { return busy; }
void yield() { ++now; }
void delay(uint32_t ms) { assert(ms==LR2021_TX_PREARM_MS);now+=ms;++delays; }
};
struct Module {
static constexpr size_t RFSWITCH_MAX_PINS=5;
// These are RadioLib's actual Module::OpMode_t values, not synthetic modes.
enum OpMode_t { MODE_END_OF_TABLE=0, MODE_IDLE, MODE_RX, MODE_TX };
struct RfSwitchMode_t { uint8_t mode;uint32_t values[RFSWITCH_MAX_PINS]; };
Hal storage;Hal* hal=&storage;
uint8_t host_switch=0;
uint32_t getGpio() { return 13; }
// DIO-controlled RF switches are NOT host GPIOs. The chip mode selects them.
void setRfSwitchState(uint8_t mode) { host_switch=mode; }
};
class LR2021 {
public:
// Exact RadioLib LR2021::OpMode_t declaration (aliases plus HF extensions).
enum OpMode_t {
MODE_END_OF_TABLE=Module::MODE_END_OF_TABLE,
MODE_STBY=Module::MODE_IDLE,
MODE_RX=Module::MODE_RX,
MODE_TX=Module::MODE_TX,
MODE_RX_HF,
MODE_TX_HF,
};
Module* mod;
float freqMHz=909.5f;
uint8_t stagedMode=RADIOLIB_RADIO_MODE_NONE,chip_mode=MODE_STBY;
uint32_t rxTimeout=0xffffff;
uint8_t masks[7]={};
bool levels[7]={},latch_masks_on_mode=false;
int fail_dio=0,fail_count=0,fail_standby_at=0;
bool fail_arm=false,fail_disarm=false,fail_tx=false,fail_standby=false;
bool busy_after_tx=false,failed_standby_not_applied=false;
int tx_calls=0,rx_calls=0,sleep_calls=0,standby_calls=0,function_calls=0;
int previous_tx_delays=0;
std::vector<int> writes,clear_attempts;
explicit LR2021(Module* module):mod(module) {}
virtual ~LR2021()=default;
virtual int16_t launchMode() { return -99; }
virtual int16_t startReceive() { return -99; }
bool level(uint8_t dio) const { return levels[dio-5]; }
void applyMode(uint8_t mode) {
chip_mode=mode;
for(size_t i=0;i<7;++i) levels[i]=(masks[i] & (1U << (mode-1)))!=0;
}
bool offInStandby() const {
for(uint8_t dio:{9,10,11}) if(masks[dio-5]&1) return false;
return true;
}
virtual int16_t standby() {
++standby_calls;
const bool failed=fail_standby || standby_calls==fail_standby_at;
if(!failed || !failed_standby_not_applied) applyMode(MODE_STBY);
return failed ? -71 : 0;
}
virtual int16_t sleep() { ++sleep_calls;applyMode(MODE_STBY);return 0; }
int16_t setTx(uint32_t timeout) {
assert(timeout==0);++tx_calls;
#if LR2021_TX_PREARM_MS > 0
assert(chip_mode==MODE_STBY);
if(!mesh::lr2021::isHighBand(freqMHz)) {
assert(level(9) && level(10) && level(11));
assert(mod->hal->delays==previous_tx_delays+1);
assert(mod->hal->now>=LR2021_TX_PREARM_MS);
}
#endif
previous_tx_delays=mod->hal->delays;
// A failed result does not prove that the TX command was not applied.
applyMode(mesh::lr2021::isHighBand(freqMHz) ? MODE_TX_HF : MODE_TX);
mod->hal->busy=busy_after_tx;
return fail_tx ? -72 : 0;
}
int16_t setTxTestMode(uint8_t mode) { assert(mode==0);return setTx(0); }
int16_t setRx(uint32_t timeout) {
assert(timeout==rxTimeout);++rx_calls;
#if LR2021_TX_PREARM_MS > 0
assert(offInStandby());
#endif
applyMode(mesh::lr2021::isHighBand(freqMHz) ? MODE_RX_HF : MODE_RX);
return 0;
}
int16_t setDioRfSwitchConfig(uint8_t dio,uint8_t mask) {
assert(dio>=5 && dio<=11);writes.push_back(dio);
const bool arming=(mask&1)!=0;
if(!arming && (dio>=9)) clear_attempts.push_back(dio);
masks[dio-5]=mask;
if(!latch_masks_on_mode) levels[dio-5]=(mask & (1U << (chip_mode-1)))!=0;
// Restoring STBY bits must never momentarily deassert an active TX FEM.
if(chip_mode==MODE_TX && dio>=9) assert(level(dio));
const bool match=(arming && fail_arm) || (!arming && fail_disarm);
if(dio==fail_dio && match && fail_count) {
if(fail_count>0) --fail_count;
return -73;
}
return 0;
}
int16_t setDioFunction(uint8_t dio,uint8_t function,uint8_t pull) {
assert(chip_mode==MODE_STBY); // Semtech permits this in Standby RC only.
assert(dio>=5 && dio<=11 && function==0x20 && (pull==2 || pull==3));
++function_calls;return 0;
}
void setRfSwitchTable(const uint32_t (&pins)[5],const Module::RfSwitchMode_t* table) {
for(size_t i=0;i<5;++i) {
uint8_t mask=0;
for(size_t j=0;table[j].mode;++j)
if(table[j].values[i]==HIGH) mask|=1U<<(table[j].mode-1);
masks[RADIOLIB_LRXXXX_DIOx_VAL(pins[i])]=mask;
}
applyMode(chip_mode);
}
int16_t begin(float,float,uint8_t,uint8_t,uint8_t,int8_t,uint16_t,float) {
std::fill(masks,masks+7,0);applyMode(MODE_STBY);return 0;
}
int16_t setRegulatorDCDC() { assert(false); return -1; }
int16_t setCRC(uint8_t) { return 0; }
int16_t explicitHeader() { return 0; }
int16_t clearRxFifo() { return 0; }
int16_t startReceive(uint32_t timeout,uint32_t,uint32_t,int) {
rxTimeout=timeout;stagedMode=RADIOLIB_RADIO_MODE_RX;return launchMode();
}
};
class CustomLR2021:public LR2021 {
@REGULATOR@
public:
using LR2021::LR2021;
uint32_t _rf_switch_pins[Module::RFSWITCH_MAX_PINS]={};
const Module::RfSwitchMode_t* _rf_switch_table=nullptr;
#if LR2021_TX_PREARM_MS > 0
bool _tx_standby_armed=false;
@MASK@
@PREPARE@
@FINISH@
@STANDBY@
@SLEEP@
#endif
@RESTORE@
@TABLE@
@INIT@
@RX_MODE@
@TX_MODE@
@LAUNCH@
@MORSE@
@RECEIVE@
};
@BOARD_PINS@
@BOARD_TABLE@
static void configure(CustomLR2021& radio) {
radio.setRfSwitchTable(rfswitch_dios,rfswitch_table);
assert(radio.offInStandby());
radio.writes.clear();radio.clear_attempts.clear();
}
static void noBias(CustomLR2021& radio) {
assert(radio.offInStandby());
if(radio.chip_mode==LR2021::MODE_STBY)
assert(!radio.level(9) && !radio.level(10) && !radio.level(11));
#if LR2021_TX_PREARM_MS > 0
assert(!radio._tx_standby_armed);
#endif
}
static int packet(CustomLR2021& radio) {
radio.stagedMode=RADIOLIB_RADIO_MODE_TX;
const int result=radio.launchMode();
assert(radio.stagedMode==RADIOLIB_RADIO_MODE_NONE);
return result;
}
int main(int argc,char** argv) {
assert(argc==2);std::string test=argv[1];
Module module;CustomLR2021 radio(&module);configure(radio);
if(test.rfind("latched_",0)==0) {
radio.latch_masks_on_mode=true;test=test.substr(8);
}
if(test=="packet" || test=="cw") {
radio.chip_mode=LR2021::MODE_RX;
assert((test=="packet" ? packet(radio) : radio.transmitMorseCarrier())==0);
assert(radio.tx_calls==1 && radio.chip_mode==LR2021::MODE_TX);
noBias(radio);
#if LR2021_TX_PREARM_MS > 0
assert(radio.writes==std::vector<int>({9,10,11,9,10,11}));
assert(module.hal->delays==1 && radio.standby_calls==2);
#else
assert(radio.writes.empty() && module.hal->delays==0 && radio.standby_calls==0);
#endif
assert(radio.function_calls==0);
assert(radio.standby()==0);noBias(radio);
assert(radio.startReceive()==0 && radio.chip_mode==LR2021::MODE_RX);
assert(!radio.level(9) && radio.level(11));
} else if(test=="hf") {
radio.freqMHz=2440;
assert(packet(radio)==0 && radio.chip_mode==LR2021::MODE_TX_HF);
assert(radio.writes.empty() && module.hal->delays==0);noBias(radio);
assert(radio.mod->host_switch==LR2021::MODE_TX_HF);
assert(radio.level(5) && !radio.level(6) && !radio.level(9) && !radio.level(11));
radio.stagedMode=RADIOLIB_RADIO_MODE_RX;
assert(radio.launchMode()==0 && radio.mod->host_switch==LR2021::MODE_RX_HF);
assert(!radio.level(5) && radio.level(6) && !radio.level(11));
}
#if LR2021_TX_PREARM_MS > 0
else if(test=="tx_fail" || test=="cw_fail" || test=="busy") {
radio.fail_tx=(test!="busy");radio.busy_after_tx=(test=="busy");
assert((test=="cw_fail" ? radio.transmitMorseCarrier() : packet(radio))==
(test=="busy" ? RADIOLIB_ERR_SPI_CMD_TIMEOUT : -72));
assert(radio.tx_calls==1 && radio.chip_mode==LR2021::MODE_STBY);
assert(radio.clear_attempts==std::vector<int>({9,10,11}));noBias(radio);
} else if(test=="standby_fail" || test=="second_standby_fail") {
if(test=="standby_fail") radio.fail_standby=true;
else radio.fail_standby_at=2;
assert(packet(radio)==(radio.fail_standby ? RADIOLIB_ERR_SPI_CMD_TIMEOUT : -71));
assert(radio.tx_calls==0 && module.hal->delays==0);noBias(radio);
if(radio.fail_standby) assert(radio.writes.empty());
else assert(radio.clear_attempts==std::vector<int>({9,10,11}));
} else if(test.rfind("arm_fail",0)==0 || test.rfind("disarm_fail",0)==0) {
const bool arm=test.rfind("arm_fail",0)==0;
radio.fail_dio=std::stoi(test.substr(arm?8:11));
radio.fail_arm=arm;radio.fail_disarm=!arm;radio.fail_count=1;
assert(packet(radio)==(arm ? -73 : RADIOLIB_ERR_SPI_CMD_TIMEOUT));
assert(radio.tx_calls==(arm?0:1));noBias(radio);
assert(radio.writes.size()>=(arm?6U:9U));
// A failed response still requires every later pin to be restored.
assert(radio.clear_attempts[0]==9 && radio.clear_attempts[1]==10 && radio.clear_attempts[2]==11);
assert(radio.chip_mode==LR2021::MODE_STBY);
} else if(test=="persistent_cleanup") {
radio.fail_dio=10;radio.fail_disarm=true;radio.fail_count=-1;
assert(packet(radio)==RADIOLIB_ERR_SPI_CMD_TIMEOUT);
assert(radio._tx_standby_armed && radio.clear_attempts.size()==6);
assert(radio.clear_attempts==std::vector<int>({9,10,11,9,10,11}));
radio.stagedMode=RADIOLIB_RADIO_MODE_RX;
assert(radio.launchMode()==RADIOLIB_ERR_SPI_CMD_TIMEOUT && radio.rx_calls==0);
assert(radio.stagedMode==RADIOLIB_RADIO_MODE_NONE && radio._tx_standby_armed);
assert(radio.sleep()==RADIOLIB_ERR_SPI_CMD_TIMEOUT && radio.sleep_calls==0);
radio.fail_count=0;assert(radio.sleep()==0 && radio.sleep_calls==1);noBias(radio);
} else if(test=="standby" || test=="sleep" || test=="receive" || test=="reset") {
assert(radio.prepareTxRfSwitch()==0 && radio._tx_standby_armed);
assert(!radio.offInStandby());
if(test=="standby") assert(radio.standby()==0);
if(test=="sleep") assert(radio.sleep()==0 && radio.sleep_calls==1);
if(test=="receive") {
assert(radio.startReceive()==0 && radio.rx_calls==1);
assert(radio.chip_mode==LR2021::MODE_RX && !radio.level(9));
}
if(test=="reset") assert(radio.std_init(nullptr));
noBias(radio);
assert(radio.clear_attempts.size()>=3);
} else if(test=="dirty_first_standby_fail" || test=="dirty_final_standby_fail") {
// A mask can be written successfully while the current DIO output is
// unchanged until a mode command. A failed mode command may not execute.
radio.latch_masks_on_mode=true;
assert(radio.prepareTxRfSwitch()==0 && radio._tx_standby_armed);
assert(radio.standby_calls==2 && radio.level(9) && radio.level(10) && radio.level(11));
radio.failed_standby_not_applied=true;
radio.fail_standby_at=(test=="dirty_first_standby_fail" ? 3 : 4);
assert(radio.standby()==RADIOLIB_ERR_SPI_CMD_TIMEOUT);
assert(radio._tx_standby_armed && radio.offInStandby());
assert(radio.level(9) && radio.level(10) && radio.level(11));
assert(radio.clear_attempts==std::vector<int>({9,10,11}));
assert(radio.tx_calls==0 && radio.rx_calls==0 && radio.sleep_calls==0);
radio.fail_standby_at=0;
assert(radio.standby()==0);noBias(radio);
assert(radio.clear_attempts==std::vector<int>({9,10,11,9,10,11}));
} else if(test=="repeat") {
for(float frequency:{909.5f,2440.0f,909.5f,2440.0f,909.5f}) {
radio.freqMHz=frequency;
for(bool cw:{false,true}) {
assert((cw ? radio.transmitMorseCarrier() : packet(radio))==0);
noBias(radio);
assert(radio.standby()==0);noBias(radio);
assert(radio.startReceive()==0);noBias(radio);
assert(radio.chip_mode==(frequency>1500 ? LR2021::MODE_RX_HF : LR2021::MODE_RX));
}
}
assert(radio.tx_calls==10 && module.hal->delays==6);
assert(radio.function_calls==0);
}
#endif
else assert(false);
}
'''
class LR2021TxPrearmTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
if not compiler:
raise AssertionError("a host C++17 compiler is required")
cls.temp = tempfile.TemporaryDirectory(prefix="meshcore-lr2021-prearm-")
cls.addClassCleanup(cls.temp.cleanup)
cls.binaries = {}
driver = (ROOT / "src/helpers/radiolib/CustomLR2021.h").read_text()
board = (ROOT / "variants/meshnology_w12/target.cpp").read_text()
source = HARNESS.replace("@REGULATOR@", regulator_members(driver))
for marker, signature in {
"@MASK@": "int16_t setTxStandbyMask(",
"@PREPARE@": "int16_t prepareTxRfSwitch(",
"@FINISH@": "int16_t finishTxRfSwitch(",
"@STANDBY@": "int16_t standby()",
"@SLEEP@": "int16_t sleep()",
"@RESTORE@": "int16_t restoreRfSwitchTable(",
"@TABLE@": "void setRfSwitchTable(",
"@INIT@": "bool std_init(",
"@RX_MODE@": "uint8_t rfSwitchRxMode(",
"@TX_MODE@": "uint8_t rfSwitchTxMode(",
"@LAUNCH@": "int16_t launchMode(",
"@MORSE@": "int16_t transmitMorseCarrier(",
"@RECEIVE@": "int16_t startReceive()",
}.items():
source = source.replace(marker, method(driver, signature))
source = source.replace("@POLICY@", re.search(
r"#ifndef LR2021_TX_PREARM_MS\b.*?#endif", driver, re.S).group(0))
for marker, pattern in {
"@BOARD_PINS@": r"const uint32_t rfswitch_dios\[\].*?\n};",
"@BOARD_TABLE@": r"static const Module::RfSwitchMode_t rfswitch_table\[\].*?\n};",
}.items():
source = source.replace(marker, re.search(pattern, board, re.S).group(0))
for name, flags in {"w12": ["-DLR2021_TX_PREARM_MS=1"], "default": [],
"disabled": ["-DLR2021_TX_PREARM_MS=0"]}.items():
cpp = Path(cls.temp.name) / (name + ".cpp")
binary = Path(cls.temp.name) / (name + (".exe" if os.name == "nt" else ""))
cpp.write_text(source, encoding="utf-8")
sanitizers = [] if os.name == "nt" else ["-fsanitize=address,undefined",
"-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]
built = subprocess.run([compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-Wno-unused-parameter", *sanitizers, *flags, "-I", str(ROOT / "src"),
str(cpp), "-o", str(binary)], capture_output=True, text=True, timeout=40)
if built.returncode:
raise AssertionError(built.stdout + built.stderr)
cls.binaries[name] = binary
def run_case(self, case, build="w12"):
ran = subprocess.run([str(self.binaries[build]), case], capture_output=True,
text=True, timeout=10)
self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr)
def test_packet_settles_pa_without_retaining_standby_bias(self):
self.run_case("packet")
def test_morse_cw_uses_same_preparation_and_cleanup(self):
self.run_case("cw")
def test_high_band_does_not_arm_low_frequency_pa(self):
self.run_case("hf")
def test_failed_packet_cw_and_busy_timeout_abort_and_disarm(self):
for case in ["tx_fail", "cw_fail", "busy", "standby_fail", "second_standby_fail"]:
with self.subTest(case=case):
self.run_case(case)
def test_every_arm_and_cleanup_pin_failure_still_cleans_later_pins(self):
for phase in ["arm_fail", "disarm_fail"]:
for dio in [9, 10, 11]:
with self.subTest(phase=phase, dio=dio):
self.run_case(phase + str(dio))
def test_persistent_cleanup_error_blocks_receive_and_sleep_until_recovery(self):
self.run_case("persistent_cleanup")
def test_standby_sleep_receive_and_actual_reset_restore_normal_table(self):
for case in ["standby", "sleep", "receive", "reset"]:
with self.subTest(case=case):
self.run_case(case)
def test_default_and_explicit_optout_keep_existing_tx_behavior(self):
for build in ["default", "disabled"]:
for case in ["packet", "cw", "hf"]:
with self.subTest(build=build, case=case):
self.run_case(case, build)
def test_prearm_and_cleanup_with_mode_latched_dio_outputs(self):
for case in ["packet", "cw", "tx_fail", "cw_fail", "busy", "second_standby_fail",
"persistent_cleanup", "standby", "sleep", "receive", "reset"]:
with self.subTest(case=case):
self.run_case("latched_" + case)
def test_repeated_packet_and_cw_across_lf_hf_switches_on_same_instance(self):
for case in ["repeat", "latched_repeat"]:
with self.subTest(case=case):
self.run_case(case)
def test_dirty_cleanup_keeps_retry_flag_until_physical_standby_is_confirmed(self):
for case in ["dirty_first_standby_fail", "dirty_final_standby_fail"]:
with self.subTest(case=case):
self.run_case(case)
def test_only_w12_enables_prearm_and_normal_standby_remains_off(self):
opted_in = []
for recipe in sorted((ROOT / "variants").glob("*/platformio.ini")):
if re.search(r"(?m)^\s*-D\s+LR2021_TX_PREARM_MS=[1-9]\d*\b", recipe.read_text()):
opted_in.append(recipe.parent.name)
self.assertEqual(opted_in, ["meshnology_w12"])
if __name__ == "__main__":
unittest.main()