Files
MeshCore/test/test_lr1121_rxps.py
T

574 lines
26 KiB
Python

"""Execute LR1121 RXPS production methods with faultable RadioLib doubles."""
from pathlib import Path
import os
import re
import shutil
import subprocess
import tempfile
import unittest
from cpp_source import body
ROOT = Path(__file__).resolve().parents[1]
HARNESS = r'''
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <initializer_list>
#include <string>
#include <helpers/radiolib/RXPowerSaving.h>
#include <helpers/radiolib/RadioRegulatorState.h>
#define MESH_DEBUG_PRINTLN(...) ((void)0)
#define RADIOLIB_ERR_NONE 0
#define RADIOLIB_ERR_UNSUPPORTED -2
#define RADIOLIB_ERR_INVALID_RX_PERIOD -3
#define RADIOLIB_ERR_INVALID_SLEEP_PERIOD -4
#define RADIOLIB_ERR_UNKNOWN -5
#define RADIOLIB_NC 0xffffffffUL
#define RADIOLIB_LR11X0_STANDBY_RC 0
#define RADIOLIB_LR11X0_LF_CLK_RC 0
#define RADIOLIB_LR11X0_LF_BUSY_RELEASE_DISABLED 0
#define RADIOLIB_LR11X0_LF_BUSY_RELEASE_ENABLED 16
#define RADIOLIB_LR11X0_RX_DUTY_CYCLE_MODE_RX 1
#define RADIOLIB_LR11X0_RX_TIMEOUT_INF 0xffffff
#define RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE 0x12
#define RADIOLIB_LR11X0_IRQ_ALL 0xffffffffUL
#define RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED (1UL << 4)
#define RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID (1UL << 5)
#define RADIOLIB_LR11X0_IRQ_HEADER_ERR (1UL << 6)
#define RADIOLIB_IRQ_RX_DONE 2
#define RADIOLIB_IRQ_PREAMBLE_DETECTED 4
#define RADIOLIB_IRQ_HEADER_ERR 6
#define RADIOLIB_IRQ_RX_DEFAULT_FLAGS (1UL << RADIOLIB_IRQ_RX_DONE)
#define RADIOLIB_IRQ_RX_DEFAULT_MASK (1UL << RADIOLIB_IRQ_RX_DONE)
#define RADIOLIB_RADIO_MODE_RX 1
#define RADIOLIB_ASSERT(state) do { if ((state) != 0) return (state); } while (0)
#define STATE_IDLE 0
#define STATE_RX 1
#define STATE_TX_WAIT 3
using RadioLibTime_t = uint32_t;
using RadioLibIrqFlags_t = uint32_t;
enum class ModemType_t { RADIOLIB_MODEM_LORA };
struct RadioModeConfig_t {
struct { uint32_t timeout, irqFlags, irqMask; size_t len; } receive;
};
static uint32_t now_ms = 100;
static unsigned delays;
uint32_t millis() { return now_ms; }
void delay(uint32_t value) { assert(value == 50); ++delays; }
struct Hal {
bool busy = false;
unsigned reads = 0;
bool digitalRead(uint32_t pin) { assert(pin == 9); ++reads; return busy; }
};
struct Module {
enum { MODE_RX = 1 };
Hal hal_value;
Hal* hal = &hal_value;
std::string* trace = nullptr;
uint32_t gpio = 9;
uint32_t getGpio() { return gpio; }
void setRfSwitchState(unsigned mode) { assert(mode == MODE_RX && trace); *trace += 'W'; }
};
class LR1121 {
public:
virtual ~LR1121() = default;
Module module;
Module* mod = &module;
float tcxoVoltage = 1.8f;
uint32_t tcxoDelay = 5000;
uint8_t spreadingFactor = 8;
float bandwidthKhz = 62.5f;
uint16_t preambleLengthLoRa = 32;
std::string trace;
int16_t standby_error = 0, lf_error = 0, stage_error = 0, duty_error = 0;
int16_t buffer_error = 0, irq_error = 0, continuous_error = 0;
int16_t tcxo_error = 0, calibration_error = 0, begin_error = 0;
uint32_t programmed_rx = 0, programmed_sleep = 0, irq = 0;
uint32_t continuous_flags = 0, continuous_mask = 0;
unsigned stage_calls = 0, duty_calls = 0, irq_reads = 0, irq_clears = 0;
unsigned base_length_calls = 0, modem_queries = 0, toa_calls = 0;
unsigned tcxo_calls = 0, calibration_calls = 0;
uint32_t tcxo_programmed = 0;
size_t stale_length = 17;
RadioModeConfig_t staged = {};
LR1121() { module.trace = &trace; }
int16_t standby(uint8_t mode = RADIOLIB_LR11X0_STANDBY_RC) {
assert(mode == RADIOLIB_LR11X0_STANDBY_RC);
trace += 'S'; return standby_error;
}
int16_t configLfClock(uint8_t setup) {
assert(setup == (RADIOLIB_LR11X0_LF_CLK_RC | RADIOLIB_LR11X0_LF_BUSY_RELEASE_ENABLED)
|| setup == (RADIOLIB_LR11X0_LF_CLK_RC | RADIOLIB_LR11X0_LF_BUSY_RELEASE_DISABLED));
trace += setup & RADIOLIB_LR11X0_LF_BUSY_RELEASE_ENABLED ? 'L' : 'R';
return lf_error;
}
int16_t stageMode(int mode, RadioModeConfig_t* config) {
assert(mode == RADIOLIB_RADIO_MODE_RX);
trace += 'G'; ++stage_calls; staged = *config; return stage_error;
}
int16_t setRxDutyCycle(uint32_t rx, uint32_t sleep, uint8_t mode) {
assert(mode == RADIOLIB_LR11X0_RX_DUTY_CYCLE_MODE_RX);
trace += 'D'; ++duty_calls; programmed_rx = rx; programmed_sleep = sleep;
return duty_error;
}
int16_t clearRxBuffer() { trace += 'B'; stale_length = 0; return buffer_error; }
int16_t clearIrqState(uint32_t bits) {
trace += 'C'; ++irq_clears; irq &= ~bits; return irq_error;
}
uint32_t getIrqStatus() { ++irq_reads; return irq; }
int16_t checkIrq(unsigned bit) { return (getIrqStatus() & (1UL << bit)) ? 1 : 0; }
virtual size_t getPacketLength(bool) { ++base_length_calls; return stale_length; }
virtual int16_t startReceive() { return startReceive(0, 0, 0, 0); }
int16_t startReceive(uint32_t, uint32_t flags, uint32_t mask, size_t) {
trace += 'X'; continuous_flags = flags; continuous_mask = mask;
return continuous_error;
}
virtual RadioLibTime_t getTimeOnAir(size_t) { ++modem_queries; return 0xffffffffUL; }
RadioLibTime_t getToA(size_t length, ModemType_t modem) {
assert(modem == ModemType_t::RADIOLIB_MODEM_LORA);
++toa_calls; return uint32_t(length * 900);
}
int16_t setTCXO(float voltage, uint32_t value) {
assert(voltage == tcxoVoltage); trace += 'T'; ++tcxo_calls;
tcxo_programmed = value;
if (!tcxo_error) tcxoDelay = value;
return tcxo_error;
}
int16_t calibrate(uint8_t mask) {
assert(mask == 0x3f); trace += 'A'; ++calibration_calls;
return calibration_error;
}
int16_t begin(float, float, uint8_t, uint8_t, uint8_t, int8_t, uint16_t, float voltage) {
trace += 'I'; tcxoVoltage = voltage; tcxoDelay = voltage > 0 ? 5000 : 0;
return begin_error;
}
int16_t setRegulatorLDO() { trace += 'O'; return 0; }
int16_t setRegulatorDCDC() { trace += 'V'; return 0; }
};
class CustomLR1121 : public LR1121 {
public:
mesh::power::RadioRegulatorState _regulator{
mesh::power::REGULATOR_DCDC | mesh::power::REGULATOR_LDO,
mesh::power::RegulatorMode::Dcdc};
uint32_t _preambleMillis = 66, _maxPayloadMillis = 3934, _activityAt = 0;
bool _headerSeen = false, _lfClockRestoreNeeded = false;
@DRIVER_METHODS@
};
namespace mesh { enum class RadioParamApplyResult : uint8_t { APPLIED, BUSY, FAILED }; }
struct Board {
bool finish_ok = true;
unsigned finish_calls = 0;
bool finishRadioHardReset() { ++finish_calls; return finish_ok; }
};
struct RadioLibWrapper {
virtual ~RadioLibWrapper() = default;
static constexpr bool SupportsRxPowerSaving = false;
CustomLR1121* _radio;
Board board;
Board* _board = &board;
bool prepare_ok = true, hard_reset_path = true;
unsigned prepare_calls = 0;
bool _rx_ps_enabled = true, _rx_ps_armed = true, _rx_hold_continuous = true;
bool _rx_ps_continuous_fallback = false, _nf_calib_active = false;
bool _profile_rxps_suspended = false, _profile_saved_rxps = false;
bool _profile_repair_required = false, _cw_active = false, _beacon_owned = false;
bool _loop_event_pending = false, interrupt_pending = false;
uint8_t _irq_probe_failures = 0, state = STATE_RX;
uint32_t _rx_ps_rx_us = 78000, _rx_ps_sleep_us = 26851;
int16_t _rx_ps_last_error = 0;
unsigned n_recv_errors = 0, reconfigure_calls = 0, rearm_calls = 0;
RxPowerSavingArmRetryState _rx_ps_arm_retry;
explicit RadioLibWrapper(CustomLR1121& value) : _radio(&value) {}
virtual bool supportsRxPowerSaving() const { return false; }
virtual uint32_t rxPowerSavingTransitionUs() const { return 0; }
virtual bool isChipBusy() { return false; }
virtual bool isReceivingPacket() { return false; }
virtual int startReceiveMode() { return -1; }
virtual int16_t armDutyCycle(RadioLibIrqFlags_t, RadioLibIrqFlags_t, uint32_t*, uint32_t*) { return -1; }
virtual int16_t stopDutyCycleHardware() { return -1; }
virtual int16_t startContinuousReceive() { return -1; }
virtual void onReceiveProcessed() {}
virtual bool radioDeepInit() { return false; }
virtual bool supportsRadioDeepInit() const { return false; }
bool prepareRadioHardReset() { ++prepare_calls; return prepare_ok; }
bool supportsRadioHardResetPath() const { return hard_reset_path; }
bool hasPendingRadioInterrupt() const { return interrupt_pending; }
uint8_t beginReconfigure() { ++reconfigure_calls; return 1; }
void endReconfigure(uint8_t) { ++reconfigure_calls; }
void startRecv() { ++rearm_calls; }
int startReceivePowerSaving() { return 77; }
@BASE_METHODS@
};
struct CustomLR1121Wrapper : RadioLibWrapper {
using DeepInitCallback = bool (*)();
DeepInitCallback _deep_init = nullptr;
explicit CustomLR1121Wrapper(CustomLR1121& value) : RadioLibWrapper(value) {}
@CAPABILITY@
@WRAPPER_METHODS@
};
static bool reinit_ok = true;
static unsigned reinit_calls = 0;
static bool board_reinit() { ++reinit_calls; return reinit_ok; }
static void arm(CustomLR1121& radio) {
uint32_t effective = 0;
assert(radio.startReceiveDutyCycle(1000000, 500000, 0x35, 0x46, &effective) == 0);
assert(effective == 1000000 && radio.programmed_rx == 32768);
assert(radio.programmed_sleep == uint32_t((500000ULL - 6000) * 32768 / 1000000));
assert(radio.trace == "SLGWD" && radio.needsLfClockRestore());
assert(radio.staged.receive.timeout == RADIOLIB_LR11X0_RX_TIMEOUT_INF);
assert(radio.staged.receive.irqFlags == 0x35 && radio.staged.receive.irqMask == 0x46);
}
int main() {
static_assert(CustomLR1121Wrapper::SupportsRxPowerSaving, "LR1121 advertises RXPS");
{
CustomLR1121 radio;
arm(radio);
radio.trace.clear();
assert(radio.restoreLfClock() == 0 && !radio.needsLfClockRestore());
assert(radio.trace == "R");
assert(radio.restoreLfClock() == 0 && radio.trace == "R");
}
// The maximum CLI periods must survive the native 32768 Hz conversion.
{
CustomLR1121 radio;
uint32_t effective = 0;
assert(radio.startReceiveDutyCycle(30000000, 30000000, 1, 2, &effective) == 0);
assert(effective == 30000000 && radio.programmed_rx == 983040);
assert(radio.programmed_sleep == uint32_t((30000000ULL - 6000) * 32768 / 1000000));
}
// Invalid periods fail before changing hardware or underflowing the sleep.
for (uint32_t sleep : {0U, 5999U, 6000U, 6001U}) {
CustomLR1121 radio;
assert(radio.startReceiveDutyCycle(78000, sleep, 1, 2, nullptr)
== RADIOLIB_ERR_INVALID_SLEEP_PERIOD);
assert(radio.trace.empty() && !radio.needsLfClockRestore());
}
{
CustomLR1121 radio;
assert(radio.startReceiveDutyCycle(1000000, 6031, 1, 2, nullptr) == 0);
assert(radio.programmed_sleep == 1);
}
{
CustomLR1121 radio;
assert(radio.startReceiveDutyCycle(0, 1000000, 1, 2, nullptr)
== RADIOLIB_ERR_INVALID_RX_PERIOD);
assert(radio.trace.empty());
for (uint32_t rx : {0U, 30U}) {
assert(radio.startReceiveDutyCycle(rx, 60000, 1, 2, nullptr)
== RADIOLIB_ERR_INVALID_RX_PERIOD);
assert(radio.trace.empty());
}
assert(radio.startReceiveDutyCycle(512000000, 1000000, 1, 2, nullptr)
== RADIOLIB_ERR_INVALID_RX_PERIOD);
assert(radio.trace.empty());
assert(radio.startReceiveDutyCycle(1000000, 512006000, 1, 2, nullptr)
== RADIOLIB_ERR_INVALID_SLEEP_PERIOD);
assert(radio.trace.empty());
uint32_t effective = 0;
assert(radio.startReceiveDutyCycle(31, 30000, 1, 2, &effective) == 0);
assert(effective == 76564); // SF8/BW62.5: 43 symbols plus 1 ms, less sleep.
}
// Stop at the first failed command and remember LF restoration after any
// attempt to enable BUSY release, including a failing configuration command.
for (unsigned fault = 0; fault < 4; ++fault) {
CustomLR1121 radio;
if (fault == 0) radio.standby_error = -71;
if (fault == 1) radio.lf_error = -72;
if (fault == 2) radio.stage_error = -73;
if (fault == 3) radio.duty_error = -74;
assert(radio.startReceiveDutyCycle(1000000, 500000, 1, 2, nullptr) == -71 - int(fault));
const char* expected[] = {"S", "SL", "SLG", "SLGWD"};
assert(radio.trace == expected[fault]);
assert(radio.needsLfClockRestore() == (fault != 0));
}
{
CustomLR1121 radio;
radio._lfClockRestoreNeeded = true; radio.lf_error = -75;
assert(radio.restoreLfClock() == -75 && radio.needsLfClockRestore());
radio.lf_error = 0;
assert(radio.restoreLfClock() == 0 && !radio.needsLfClockRestore());
}
for (unsigned fault = 0; fault < 3; ++fault) {
CustomLR1121 radio;
radio._activityAt = 90; radio._headerSeen = true;
if (fault == 0) radio.standby_error = -81;
if (fault == 1) radio.buffer_error = -82;
if (fault == 2) radio.irq_error = -83;
assert(radio.recoverReceivePath() == -81 - int(fault));
assert(radio.trace == "SBC" && !radio._activityAt && !radio._headerSeen);
}
// Driver activity polling does no SPI in a duty-cycle sleep window, and
// leaves HEADER_ERR visible to the receive owner when awake.
{
CustomLR1121 radio;
radio.module.hal->busy = true;
radio.irq = RADIOLIB_LR11X0_IRQ_HEADER_ERR;
assert(!radio.isReceiving() && radio.irq_reads == 0 && radio.irq_clears == 0);
radio.module.hal->busy = false;
assert(!radio.isReceiving() && radio.irq == RADIOLIB_LR11X0_IRQ_HEADER_ERR);
assert(radio.irq_clears == 0);
assert(radio.getPacketLength(true) == 0 && radio.trace == "SBC");
assert(radio.base_length_calls == 0 && radio.stale_length == 0 && radio.irq == 0);
radio.module.gpio = RADIOLIB_NC;
const unsigned reads = radio.module.hal->reads;
assert(!radio.isChipBusy() && radio.module.hal->reads == reads);
}
{
CustomLR1121 radio;
radio.module.hal->busy = true;
assert(radio.getTimeOnAir(32) == 28800 && radio.toa_calls == 1 && radio.modem_queries == 0);
radio._activityAt = 99; radio._headerSeen = true;
assert(radio.startReceive() == 0 && !radio._activityAt && !radio._headerSeen);
assert(radio.continuous_flags & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED);
assert(radio.continuous_mask & RADIOLIB_LR11X0_IRQ_HEADER_ERR);
}
{
CustomLR1121 radio;
assert(radio.begin() == 0 && radio.getTcxoDelay() == 5000);
assert(radio.trace == "IV" && radio.tcxo_calls == 0 && delays == 0);
radio.trace.clear();
assert(radio.begin(434,125,9,7,0x12,10,8,0.0f) == 0);
assert(radio.getTcxoDelay() == 0 && radio.trace == "IV" && radio.tcxo_calls == 0);
CustomLR1121Wrapper xo_wrapper(radio);
assert(xo_wrapper.rxPowerSavingTransitionUs() == 1000);
radio.trace.clear();
assert(radio.begin() == 0 && radio.getTcxoDelay() == 5000 && radio.trace == "IV");
}
{
CustomLR1121 radio;
CustomLR1121Wrapper wrapper(radio);
assert(wrapper.supportsRxPowerSaving() && wrapper.rxPowerSavingTransitionUs() == 6000);
assert(wrapper.startReceiveMode() == 77);
uint32_t effective_rx = 0, effective_sleep = 0;
assert(wrapper.armDutyCycle(0x31,0x42,&effective_rx,&effective_sleep) == 0);
assert(effective_rx >= wrapper._rx_ps_rx_us && effective_sleep == wrapper._rx_ps_sleep_us);
assert(radio.staged.receive.irqFlags == 0x31 && radio.staged.receive.irqMask == 0x42);
radio.trace.clear();
assert(wrapper.stopDutyCycleHardware() == 0 && radio.trace == "SR");
assert(!radio.needsLfClockRestore());
radio._lfClockRestoreNeeded = true; radio.trace.clear(); radio.lf_error = -78;
assert(wrapper.startContinuousReceive() == -78 && radio.trace == "SR");
assert(radio.needsLfClockRestore());
radio.trace.clear(); radio.lf_error = 0;
assert(wrapper.startContinuousReceive() == 0 && radio.trace == "SRX");
assert(!radio.needsLfClockRestore());
radio.trace.clear();
assert(wrapper.startContinuousReceive() == 0 && radio.trace == "X");
radio._lfClockRestoreNeeded = true; radio.standby_error = -79; radio.trace.clear();
assert(wrapper.startContinuousReceive() == -79 && radio.trace == "S");
assert(radio.needsLfClockRestore());
radio.lf_error = -80; radio.trace.clear();
assert(wrapper.stopDutyCycleHardware() == -79 && radio.trace == "SR");
assert(radio.needsLfClockRestore());
radio.standby_error = 0; radio.lf_error = 0; radio.trace.clear();
assert(wrapper.stopDutyCycleHardware() == 0 && radio.trace == "SR");
assert(!radio.needsLfClockRestore());
}
{
CustomLR1121 radio;
CustomLR1121Wrapper wrapper(radio);
radio.module.hal->busy = true;
radio.irq = RADIOLIB_LR11X0_IRQ_HEADER_ERR;
assert(!wrapper.isReceivingPacket() && radio.irq_reads == 0);
radio.module.hal->busy = false;
assert(!wrapper.isPacketReady() && radio.trace == "SBC");
assert(wrapper.n_recv_errors == 1 && !wrapper._rx_ps_armed && !wrapper._rx_hold_continuous);
assert(!wrapper.isPacketReady()); // A non-RxDone IRQ cannot replay stale FIFO data.
radio.irq = 1UL << RADIOLIB_IRQ_RX_DONE;
assert(wrapper.isPacketReady());
}
// The driver's completion callback must retain continuous RX while another
// packet owns it, then hand control back to the generic duty-cycle rearm.
{
CustomLR1121 radio;
CustomLR1121Wrapper wrapper(radio);
wrapper._rx_ps_armed = false; wrapper.interrupt_pending = true;
wrapper.onReceiveProcessed();
assert(wrapper._rx_hold_continuous && wrapper.rearm_calls == 0);
wrapper.interrupt_pending = false;
wrapper.onReceiveProcessed();
assert(!wrapper._rx_hold_continuous && wrapper.rearm_calls == 1 && wrapper.state == STATE_IDLE);
}
// Dual-profile scanning keeps RX continuous while changing only the saved
// requested RXPS setting. Existing generic scan teardown restores that intent.
{
CustomLR1121 radio;
CustomLR1121Wrapper wrapper(radio);
wrapper._profile_rxps_suspended = true; wrapper._rx_ps_enabled = false;
assert(wrapper.trySetRxPowerSaving(true,100000,50000) == mesh::RadioParamApplyResult::APPLIED);
assert(wrapper._profile_saved_rxps && !wrapper._rx_ps_enabled && wrapper.reconfigure_calls == 0);
assert(wrapper._rx_ps_rx_us == 100000 && wrapper._rx_ps_sleep_us == 50000);
assert(wrapper.trySetRxPowerSaving(false,100000,50000) == mesh::RadioParamApplyResult::APPLIED);
assert(!wrapper._profile_saved_rxps && radio.trace.empty());
wrapper._beacon_owned = true;
assert(wrapper.trySetRxPowerSaving(true,100000,50000) == mesh::RadioParamApplyResult::BUSY);
assert(!wrapper._profile_saved_rxps);
}
{
CustomLR1121 radio;
CustomLR1121Wrapper wrapper(radio);
assert(!wrapper.supportsRadioDeepInit() && !wrapper.radioDeepInit());
assert(wrapper.prepare_calls == 0 && wrapper.board.finish_calls == 0 && reinit_calls == 0);
wrapper.setDeepInitCallback(board_reinit);
assert(wrapper.supportsRadioDeepInit());
wrapper.hard_reset_path = false;
assert(!wrapper.supportsRadioDeepInit());
wrapper.hard_reset_path = true;
wrapper.prepare_ok = false;
assert(!wrapper.radioDeepInit() && wrapper.prepare_calls == 1);
assert(wrapper.board.finish_calls == 0 && reinit_calls == 0);
wrapper.prepare_ok = true; reinit_ok = false;
assert(!wrapper.radioDeepInit() && wrapper.prepare_calls == 2 && reinit_calls == 1);
assert(wrapper.board.finish_calls == 0);
reinit_ok = true; wrapper.board.finish_ok = false;
assert(!wrapper.radioDeepInit() && wrapper.prepare_calls == 3 && reinit_calls == 2);
assert(wrapper.board.finish_calls == 1);
wrapper.board.finish_ok = true;
assert(wrapper.radioDeepInit() && wrapper.prepare_calls == 4 && reinit_calls == 3);
assert(wrapper.board.finish_calls == 2);
}
}
'''
class LR1121RxpsTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
if not cls.compiler:
raise unittest.SkipTest("C++ compiler unavailable")
def compile_and_run(self, source, flags=()):
with tempfile.TemporaryDirectory(prefix="lr1121-rxps-") as directory:
folder = Path(directory)
cpp, binary = folder / "probe.cpp", folder / "probe"
cpp.write_text(source, encoding="ascii")
command = [self.compiler, "-std=gnu++17", "-O1", "-g", "-Wall", "-Wextra",
"-Werror", "-I", str(ROOT / "src"), *flags]
if os.name != "nt":
command += ["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
"-fno-pie", "-no-pie"]
built = subprocess.run([*command, str(cpp), "-o", str(binary)],
capture_output=True, text=True)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
result = subprocess.run([str(binary)], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
def test_driver_and_wrapper_hardware_lifecycle(self):
driver = (ROOT / "src/helpers/radiolib/CustomLR1121.h").read_text()
wrapper = (ROOT / "src/helpers/radiolib/CustomLR1121Wrapper.h").read_text()
generic = (ROOT / "src/helpers/radiolib/RadioLibWrappers.cpp").read_text()
driver_methods = "\n".join(body(driver, signature) for signature in (
"uint32_t getTcxoDelay()", "bool needsLfClockRestore()",
"int16_t restoreLfClock()", "int16_t startReceiveDutyCycle(",
"RadioLibTime_t getTimeOnAir(",
"bool isChipBusy()", "int16_t recoverReceivePath()",
"size_t getPacketLength(", "int16_t startReceive()", "bool isReceiving()",
"int16_t begin(float",
))
wrapper_methods = "\n".join(body(wrapper, signature) for signature in (
"uint32_t rxPowerSavingTransitionUs()", "bool supportsRxPowerSaving()",
"bool isChipBusy()", "bool isReceivingPacket()", "int startReceiveMode()",
"bool isPacketReady()", "int16_t armDutyCycle(", "int16_t stopDutyCycleHardware()",
"int16_t startContinuousReceive()", "void onReceiveProcessed()",
"bool radioDeepInit()", "bool supportsRadioDeepInit()",
"void setDeepInitCallback(",
))
capability = re.search(r"static constexpr bool SupportsRxPowerSaving\s*=\s*true;", wrapper)
self.assertIsNotNone(capability)
base_methods = "\n".join(body(generic, signature).replace("RadioLibWrapper::", "")
for signature in (
"bool RadioLibWrapper::isPacketReady()",
"void RadioLibWrapper::finishReceiveProcessing()",
"mesh::RadioParamApplyResult RadioLibWrapper::trySetRxPowerSaving(",
))
base_methods = base_methods.replace("bool isPacketReady()", "virtual bool isPacketReady()", 1)
source = (HARNESS.replace("@DRIVER_METHODS@", driver_methods)
.replace("@WRAPPER_METHODS@", wrapper_methods)
.replace("@BASE_METHODS@", base_methods)
.replace("@CAPABILITY@", capability.group(0)))
self.compile_and_run(source, ["-DMESH_ENABLE_TX_BEACONS=1", "-DUSE_LR1121"])
def test_board_recipes_select_lr11x0_math_before_target_headers(self):
recipes = [
("variants/waveshare_esp32s3_lr1121/platformio.ini", "Waveshare_S3_LR1121"),
("variants/muzi_base/platformio.ini", "muzi_base_duo"),
]
source = r'''
#include <cassert>
// Intentionally included before any target/driver header: recipe flags must
// select family-specific math in CommonCLI and role constructors too.
#include <helpers/radiolib/RXPowerSaving.h>
int main() {
assert(rxPowerSavingDefaultCaptureCost() == 8.0f);
assert(rxPowerSavingDefaultTransitionUs() == EXPECT_TRANSITION);
uint32_t rx = 0, sleep = 0, explicit_rx = 0, explicit_sleep = 0;
assert(calcRxPowerSavingLevel(6,8,62.5f,32,&rx,&sleep));
assert(calcRxPowerSavingLevel(6,8,62.5f,32,&explicit_rx,&explicit_sleep,
8.0f,EXPECT_TRANSITION));
assert(rx == explicit_rx && sleep == explicit_sleep);
#if defined(USE_LR1121) && EXPECT_TRANSITION == 6000
assert(!calcRxPowerSavingLevel(6,8,812.5f,16,&rx,&sleep));
uint8_t effective_level = 0, effective_preamble = 0;
assert(recalcRxPowerSavingFromLevel(6,8,812.5f,16,&rx,&sleep,
&effective_level,&effective_preamble));
// Preserve the existing policy of promoting to the actual longer wire
// preamble when a short configured preamble cannot cover TCXO startup.
assert(!rxPowerSavingUsesContinuousFallback(rx,sleep));
assert(effective_level >= 6 && effective_preamble > 16);
assert(recalcRxPowerSavingFromLevel(6,5,812.5f,16,&rx,&sleep));
assert(rxPowerSavingUsesContinuousFallback(rx,sleep));
assert(recalcRxPowerSavingFromLevel(6,10,812.5f,16,&rx,&sleep));
assert(!rxPowerSavingUsesContinuousFallback(rx,sleep));
#endif
}
'''
for relative, section in recipes:
text = (ROOT / relative).read_text()
section_text = text.split("[" + section + "]", 1)[1].split("\n[", 1)[0]
self.assertRegex(section_text, r"(?m)^\s*-D\s+USE_LR1121\b", relative)
voltage = re.search(r"-D\s+LR11X0_DIO3_TCXO_VOLTAGE=([0-9.]+)", section_text)
self.assertIsNotNone(voltage)
with self.subTest(recipe=relative):
self.compile_and_run(source, ["-DUSE_LR1121",
"-DEXPECT_TRANSITION=6000",
"-DLR11X0_DIO3_TCXO_VOLTAGE=" + voltage.group(1)])
with self.subTest(family="LR1110"):
self.compile_and_run(source, ["-DUSE_LR1110", "-DEXPECT_TRANSITION=2600",
"-DLR11X0_DIO3_TCXO_VOLTAGE=1.8"])
with self.subTest(oscillator="LR1121 XO"):
self.compile_and_run(source, ["-DUSE_LR1121", "-DEXPECT_TRANSITION=1000",
"-DLR11X0_DIO3_TCXO_VOLTAGE=0.0"])
def test_boards_register_deep_init_after_restoring_rf_switch_and_gain(self):
for relative in ("variants/waveshare_esp32s3_lr1121/target.cpp",
"variants/muzi_base/target.cpp"):
with self.subTest(board=relative):
initialization = body((ROOT / relative).read_text(), "bool radio_init()")
callback = initialization.index("radio_driver.setDeepInitCallback(radio_init)")
switch = initialization.index("radio.setRfSwitchTable(")
gain = initialization.index("radio.setRxBoostedGainMode(")
self.assertLess(initialization.index("radio.begin("), switch)
self.assertLess(switch, gain)
self.assertLess(gain, callback)
self.assertLess(callback, initialization.index("return true", callback))
if __name__ == "__main__":
unittest.main()