Support LR1121 RX duty-cycle power saving

This commit is contained in:
mikecarper committed 2026-10-27 14:34:55 -07:00
1 parent eab29a12cd
commit ff1ccacb2a
11 files changed
+2053 -23

No files matched your search

+1
View File
@@ -431,6 +431,7 @@ jobs:
python3 -B test/test_upstream_20261015_wifi_boot.py -v
python3 -B test/test_wio_tracker_l2_gps_power.py -v
python3 -B test/test_lr1121_power_merge.py -v
python3 -B test/test_lr1121_rxps.py -v
python3 -B test/test_lr2021_power.py -v
python3 -B test/test_lr2021_tx_prearm.py -v
python3 -B test/test_lr2021_primary_apply_wait.py -v
+11 -2
View File
@@ -2106,7 +2106,7 @@ intervals and the 24-hour watchdog policy. An explicitly saved normal-mode
available. Returning to slower settings recalculates from the saved RXPS
minimum.
With the measured RXPS model and the default 1600 us TCXO startup delay
With the measured SX126x/LR1110 RXPS model and the default 1600 us TCXO startup delay
(2600 us total transition), a requested minimum level 1 gives:
| SF | BW (kHz) | Wire preamble | Effective timing preamble | Effective RXPS level | RX / sleep |
@@ -2279,6 +2279,13 @@ Station G2/G3 targets default to `off`.
- `state`: `on` or `off`.
**Notes:**
- LR1121 RXPS is supported on Waveshare ESP32-S3 LR1121 and Muzi Base Duo
builds, using the same commands. Their existing 5000 us TCXO startup delay
is preserved, giving a 6000 us sleep/wake transition. Fast SF/BW tuples may
use continuous receive; `get radio.rxps.status` reports the actual mode.
- While radio2 profile scanning is enabled, RXPS is suspended so neither
profile sleeps through its listening window. The requested RXPS setting
is retained and restored after returning to single-profile receive.
- `get rxps.wd` reports the radio watchdogs' soft and hard recovery counts.
See [receive calibration and recovery](test-results/radio_receive_calibration.md) for
noise sampling, CAD handling, and the continuous-RX mode check.
@@ -2310,7 +2317,9 @@ Station G2/G3 targets default to `off`.
- Named experimental profiles accept `preamble 32`; numeric levels keep the
existing automatic-preamble selection. Manual timings bypass the level model.
- `MC_TCXO_DELAY_US` defaults to 1600 us. Initialization applies it and checks
TCXO/calibration errors. A board can override it for its oscillator.
TCXO/calibration errors on the qualified SX126x/LR1110 paths. A board can
override it for its oscillator. LR1121 retains RadioLib's 5000 us setting;
it does not inherit the LR1110 hardware timing qualification.
- Fresh Cascade-profile builds start with RXPS on at level `8` and a 16-symbol preamble. Saved operator settings still take precedence after an upgrade.
- Level-based settings automatically recalculate their timings when the spreading factor or bandwidth changes. Custom `<rx_us> <sleep_us>` timings remain fixed.
- `get radio.rxps` keeps the legacy on/off, RX, and sleep reply. The new
@@ -0,0 +1,77 @@
<!-- meshcore-hosted-doc-link:start -->
<p class="meshcore-hosted-doc-link"><a href="https://mikecarper.github.io/MeshCore/test-results/lr1121_rxps_2026-10-27/">View this page on MeshCore Docs</a>.</p>
<!-- meshcore-hosted-doc-link:end -->
# LR1121 RX power saving, 2026-10-27
RXPS support is implemented for Waveshare ESP32-S3 LR1121 and Muzi Base Duo.
Existing saved settings and commands apply to Repeater, Room Server and
Companion roles. No board was flashed during this work; RF reception and
current savings have not been physically measured.
```text
set radio.rxps balanced
get radio.rxps.config
get radio.rxps.status
set radio.rxps off
```
Status distinguishes requested power saving from actual armed or continuous
receive. Radio2 scanning suspends RXPS while retaining the requested setting;
returning to the primary profile restores it. Fast tuples can require
continuous receive. Experimental levels 9/10 retain their existing caveats.
## Driver changes
The LR1121 uses the same `0x0214` duty-cycle command and 32768 Hz, 24-bit
periods as the existing LR11x0 implementation. Arming enters Standby RC,
configures the LF clock, stages packet/IRQ parameters, activates the RX RF
path and issues the native command. See [Semtech's LR1121 user manual,
section 7.2.6](https://storage.googleapis.com/cloud-storage-web/public/t_form3/rzsyE6ulSlqjnJrpxSmZqD5It5wRyeCyyxP6I2Y3.pdf).
The implementation uses 64-bit timer conversion, rejects zero/sub-tick RX
periods, preserves the extended-preamble guard and restores LF-clock behavior
when leaving duty cycle. Pinned RadioLib's public implementation uses 32-bit
conversion, which overflows above about 131 ms; its staging also needs the
explicit RF-path activation. [RadioLib implementation](https://github.com/jgromes/RadioLib/blob/2325a38a89dd0f0e402c19f67fe910111da1e246/src/modules/LR11x0/LR11x0.cpp).
BUSY-aware activity polling and cached LoRa airtime avoid ordinary SPI reads
while asleep. Header errors remain visible until the receive owner clears
the stale buffer and rearms. The shared receive path retains packet-metadata
ownership, automatic fallback/retries and watchdog behavior. Waveshare now
registers its full-radio recovery callback, restoring the board's TCXO and
DIO RF-switch table after a reset.
LR1121 retains RadioLib's existing **5000 us TCXO startup setting**, giving
a 6000 us transition. It does not inherit the LR1110's shorter qualified
oscillator timing. Both board recipes select the eight-symbol LR11x0 capture
cost before CLI/default calculations. PA selection, regulator mode, band
configuration and boosted gain keep their existing paths.
## Checks
All **68 affected host tests** pass across 13 suites. Three new tests execute
the actual driver/wrapper methods in five ASan/UBSan binaries: timer bounds,
command ordering and failures, clock restoration, sleeping BUSY, cached
airtime, header recovery, profile suspension and reset callbacks. The extended
activity-epoch tests retain success/failure and removed-reset negative controls.
| Packaged firmware | RAM admission | Capability checks |
| --- | --- | ---: |
| Waveshare Full Repeater | Pass | 15/15 |
| Waveshare Full Companion | Pass | 26/26 |
| Muzi Duo Full Companion | Pass | 16/16 |
| Muzi Duo LoRa-OTA Repeater, Full sensors | Pass | 7/7 |
| Muzi Duo LoRa-OTA Repeater, Reduced sensors | Pass | 5/5 |
All packages pass their size and update-path checks. PlatformIO ran one
process at a time. The [machine-readable evidence](lr1121_rxps_2026-10-27.safe.json)
contains source hashes, exact commands, artifact hashes and memory reports.
After integrating the concurrent Companion trace-handling update at
`eab29a12c`, Muzi Full Companion was packaged again at `b34a82acb` and passed
all 16 capability checks, RAM admission and UF2 address checks. Its available
internal RAM remains 89,292 bytes against 59,200 required. The RXPS source
hashes still match the original five packages above.
These software checks do not establish LR1121 packet-delivery margins,
measured power reduction or watchdog recovery on physical hardware.
File diff suppressed because it is too large. Load diff
+105 -11
View File
@@ -3,6 +3,7 @@
#include <RadioLib.h>
#include "RadioRegulatorState.h"
#include "MeshCore.h"
#include "RXPowerSaving.h"
#include "LR11x0MorseRfSwitch.h"
#include "../MorseRx.h"
@@ -15,6 +16,7 @@ class CustomLR1121 : public LR1121 {
uint32_t _activityAt = 0;
bool _headerSeen = false;
bool _rx_boosted = false;
bool _lfClockRestoreNeeded = false;
LR11x0MorseRfSwitch _morse_rf_switch;
int8_t _txPower = 0;
bool _txPowerSet = false;
@@ -72,17 +74,32 @@ class CustomLR1121 : public LR1121 {
return _regulator.finishInitialization(state);
}
// Airtime is a cached LoRa calculation. GetPacketType would wake a
// sleeping duty-cycle receiver merely to estimate an outgoing packet.
RadioLibTime_t getTimeOnAir(size_t len) override {
return getToA(len, ModemType_t::RADIOLIB_MODEM_LORA);
}
int16_t recoverReceivePath() {
_activityAt = 0;
_headerSeen = false;
int16_t state = standby();
int16_t next = clearRxBuffer();
if (state == RADIOLIB_ERR_NONE) state = next;
next = clearIrqState(RADIOLIB_LR11X0_IRQ_ALL);
if (state == RADIOLIB_ERR_NONE) state = next;
return state;
}
size_t getPacketLength(bool update) override {
size_t len = LR1121::getPacketLength(update);
if (len == 0 && getIrqStatus() & RADIOLIB_LR11X0_IRQ_HEADER_ERR) {
// we've just received a corrupted packet
// this may have triggered a bug causing subsequent packets to be shifted
// call standby() to return radio to known-good state
// recvRaw will call startReceive() to restart rx
MESH_DEBUG_PRINTLN("LR1121: got header err, calling standby()");
standby();
// A header-error event can retain the previous packet's nonzero length.
// Recover from the IRQ itself rather than delivering those stale bytes.
if (getIrqStatus() & RADIOLIB_LR11X0_IRQ_HEADER_ERR) {
MESH_DEBUG_PRINTLN("LR1121: header error, resetting RX path");
recoverReceivePath();
return 0;
}
return len;
return LR1121::getPacketLength(update);
}
using LR1121::setOutputPower;
@@ -113,6 +130,74 @@ class CustomLR1121 : public LR1121 {
}
float getFreqMHz() const { return freqMHz; }
uint32_t getTcxoDelay() const { return tcxoDelay; }
bool needsLfClockRestore() const { return _lfClockRestoreNeeded; }
int16_t restoreLfClock() {
if (!_lfClockRestoreNeeded) return RADIOLIB_ERR_NONE;
// Retain the LF RC clock: DIO11 can also be an IRQ/RF-switch signal.
const int16_t state = configLfClock(
RADIOLIB_LR11X0_LF_BUSY_RELEASE_DISABLED | RADIOLIB_LR11X0_LF_CLK_RC);
if (state == RADIOLIB_ERR_NONE) _lfClockRestoreNeeded = false;
return state;
}
int16_t startReceiveDutyCycle(uint32_t rxPeriod, uint32_t sleepPeriod,
RadioLibIrqFlags_t irqFlags = RADIOLIB_IRQ_RX_DEFAULT_FLAGS,
RadioLibIrqFlags_t irqMask = RADIOLIB_IRQ_RX_DEFAULT_MASK,
uint32_t* effectiveRxPeriod = nullptr) {
// Reject zero/sub-tick inputs before the preamble guard can enlarge
// them: one 32.768 kHz RTC tick requires at least 31 microseconds.
if (rxPeriod < 31UL) return RADIOLIB_ERR_INVALID_RX_PERIOD;
const uint32_t symbolPeriod = (uint32_t)(((1000.0f * (float)(1UL << spreadingFactor)) /
bandwidthKhz) + 0.999f);
const uint32_t transitionTime = tcxoDelay + 1000UL;
if (sleepPeriod <= transitionTime) return RADIOLIB_ERR_INVALID_SLEEP_PERIOD;
const uint32_t programmedSleepPeriod = sleepPeriod - transitionTime;
// Use the existing LR11x0 extended-preamble guard. Preamble detection
// extends the receive timeout to 2*rx + programmed sleep.
const uint64_t requiredExtendedPeriod =
((uint64_t)preambleLengthLoRa + 11ULL) * symbolPeriod + 1000ULL;
if (2ULL * rxPeriod + programmedSleepPeriod < requiredExtendedPeriod) {
rxPeriod = (uint32_t)((requiredExtendedPeriod - programmedSleepPeriod + 1ULL) / 2ULL);
}
if (effectiveRxPeriod) *effectiveRxPeriod = rxPeriod;
// LR11xx uses 32.768 kHz RTC ticks. Native RadioLib 7.7.1 overflows
// its 32-bit multiplication for periods above roughly 131 ms.
const uint32_t rxPeriodRaw = (uint32_t)(((uint64_t)rxPeriod * 32768UL) / 1000000UL);
const uint32_t sleepPeriodRaw =
(uint32_t)(((uint64_t)programmedSleepPeriod * 32768UL) / 1000000UL);
if ((rxPeriodRaw & 0xFF000000) || rxPeriodRaw == 0) return RADIOLIB_ERR_INVALID_RX_PERIOD;
if ((sleepPeriodRaw & 0xFF000000) || sleepPeriodRaw == 0) return RADIOLIB_ERR_INVALID_SLEEP_PERIOD;
// SetRxDutyCycle requires Standby RC and a configured RTC source.
int16_t state = standby(RADIOLIB_LR11X0_STANDBY_RC);
RADIOLIB_ASSERT(state);
_lfClockRestoreNeeded = true;
state = configLfClock(
RADIOLIB_LR11X0_LF_CLK_RC | RADIOLIB_LR11X0_LF_BUSY_RELEASE_ENABLED);
RADIOLIB_ASSERT(state);
RadioModeConfig_t cfg = {
.receive = {
.timeout = RADIOLIB_LR11X0_RX_TIMEOUT_INF,
.irqFlags = irqFlags,
.irqMask = irqMask,
.len = 0,
}
};
state = stageMode(RADIOLIB_RADIO_MODE_RX, &cfg);
RADIOLIB_ASSERT(state);
// stageMode prepares packet/IRQ state; launch the RX RF path too.
mod->setRfSwitchState(Module::MODE_RX);
state = setRxDutyCycle(rxPeriodRaw, sleepPeriodRaw, RADIOLIB_LR11X0_RX_DUTY_CYCLE_MODE_RX);
if (state == RADIOLIB_ERR_NONE) {
_activityAt = 0;
_headerSeen = false;
}
return state;
}
int16_t setRxBoostedGainMode(bool en) {
_rx_boosted = en;
@@ -123,7 +208,9 @@ class CustomLR1121 : public LR1121 {
int16_t startReceive() override {
// include the PREAMBLE_DETECTED irq bit in reported flags.
const int16_t status = LR1121::startReceive(RADIOLIB_LR11X0_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0);
const int16_t status = LR1121::startReceive(RADIOLIB_LR11X0_RX_TIMEOUT_INF,
RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED),
RADIOLIB_IRQ_RX_DEFAULT_MASK | (1UL << RADIOLIB_IRQ_HEADER_ERR), 0);
if (status == RADIOLIB_ERR_NONE) {
_activityAt = 0;
_headerSeen = false;
@@ -131,14 +218,21 @@ class CustomLR1121 : public LR1121 {
return status;
}
bool isChipBusy() {
const uint32_t busy = mod->getGpio();
return busy != RADIOLIB_NC && mod->hal->digitalRead(busy);
}
bool isReceiving() {
if (isChipBusy()) return false;
uint32_t irq = getIrqStatus();
bool preamble = irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED; // bit 4
bool header = irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID; // bit 5
bool hdrErr = irq & RADIOLIB_LR11X0_IRQ_HEADER_ERR; // bit 6
uint32_t now = millis();
if (hdrErr) {
clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED | RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID | RADIOLIB_LR11X0_IRQ_HEADER_ERR);
// The receive wrapper owns recovery. Clearing this during polling
// would hide the error before stale RX-buffer length is rejected.
_activityAt = 0;
_headerSeen = false;
return false;
@@ -1,5 +1,9 @@
#pragma once
#ifndef USE_LR1121
#define USE_LR1121
#endif
#include "CustomLR1121.h"
#include "RadioLibWrappers.h"
#include "LR11x0Reset.h"
@@ -8,6 +12,10 @@ class CustomLR1121Wrapper : public RadioLibWrapper {
using DeepInitCallback = bool (*)();
DeepInitCallback _deep_init = nullptr;
public:
static constexpr bool SupportsRxPowerSaving = true;
uint32_t rxPowerSavingTransitionUs() const override {
return static_cast<CustomLR1121*>(_radio)->getTcxoDelay() + 1000UL;
}
uint8_t regulatorModes() const override { return static_cast<CustomLR1121*>(_radio)->regulatorModes(); }
mesh::power::RegulatorMode defaultRegulatorMode() const override {
return static_cast<CustomLR1121*>(_radio)->defaultRegulatorMode();
@@ -116,6 +124,9 @@ public:
bool isReceivingPacket() override {
return ((CustomLR1121 *)_radio)->isReceiving();
}
bool isChipBusy() override {
return static_cast<CustomLR1121*>(_radio)->isChipBusy();
}
float getCurrentRSSI() override {
float rssi = NAN;
return ((CustomLR1121 *)_radio)->getRssiInst(&rssi) == RADIOLIB_ERR_NONE ? rssi : NAN;
@@ -128,12 +139,47 @@ public:
uint32_t getEstAirtimeFor(int len_bytes) override {
auto airtime = RadioLibWrapper::getEstAirtimeFor(len_bytes);
if (airtime == 0) return 0;
return airtime < 200 ? 200 : airtime; // at least 200 millis
}
uint8_t getSpreadingFactor() const override { return ((CustomLR1121 *)_radio)->getSpreadingFactor(); }
bool supportsRxPowerSaving() const override { return SupportsRxPowerSaving; }
void onReceiveProcessed() override { finishReceiveProcessing(); }
protected:
int startReceiveMode() override { return startReceivePowerSaving(); }
bool isPacketReady() override {
if (_radio->checkIrq(RADIOLIB_IRQ_HEADER_ERR) > 0) {
const int16_t error = static_cast<CustomLR1121*>(_radio)->recoverReceivePath();
if (error != RADIOLIB_ERR_NONE) {
MESH_DEBUG_PRINTLN("CustomLR1121Wrapper: RX recovery error (%d)", error);
}
_rx_ps_armed = false;
_rx_hold_continuous = false;
n_recv_errors++;
return false;
}
return RadioLibWrapper::isPacketReady();
}
int16_t armDutyCycle(RadioLibIrqFlags_t irq_flags, RadioLibIrqFlags_t irq_mask,
uint32_t* effective_rx_us, uint32_t* effective_sleep_us) override {
*effective_sleep_us = _rx_ps_sleep_us;
return static_cast<CustomLR1121*>(_radio)->startReceiveDutyCycle(
_rx_ps_rx_us, _rx_ps_sleep_us, irq_flags, irq_mask, effective_rx_us);
}
int16_t stopDutyCycleHardware() override {
const int16_t standby_error = _radio->standby();
const int16_t clock_error = static_cast<CustomLR1121*>(_radio)->restoreLfClock();
return standby_error != RADIOLIB_ERR_NONE ? standby_error : clock_error;
}
int16_t startContinuousReceive() override {
auto* radio = static_cast<CustomLR1121*>(_radio);
if (!radio->needsLfClockRestore()) return _radio->startReceive();
int16_t error = _radio->standby();
if (error == RADIOLIB_ERR_NONE) error = radio->restoreLfClock();
return error == RADIOLIB_ERR_NONE ? _radio->startReceive() : error;
}
bool radioDeepInit() override {
return _deep_init && prepareRadioHardReset() && _deep_init()
&& _board->finishRadioHardReset();
+14 -6
View File
@@ -226,17 +226,25 @@ inline bool rxPowerSavingUsesContinuousFallback(uint32_t rx_us, uint32_t sleep_u
}
inline uint32_t rxPowerSavingDefaultTransitionUs() {
#if defined(SX126X_DIO3_TCXO_VOLTAGE)
return SX126X_DIO3_TCXO_VOLTAGE > 0.0f ? MC_TCXO_DELAY_US + 1000UL : 1000UL;
#elif defined(LR11X0_DIO3_TCXO_VOLTAGE)
return LR11X0_DIO3_TCXO_VOLTAGE > 0.0f ? MC_TCXO_DELAY_US + 1000UL : 1000UL;
#if defined(USE_LR1121)
// Retain RadioLib's existing 5 ms LR1121 TCXO startup time. The shorter
// LR1110 setting has not been qualified for these boards. Driver arming
// uses its actual cached delay; CLI/default calculations must agree.
const uint32_t tcxo_delay_us = 5000UL;
#else
return MC_TCXO_DELAY_US + 1000UL;
const uint32_t tcxo_delay_us = MC_TCXO_DELAY_US;
#endif
#if defined(SX126X_DIO3_TCXO_VOLTAGE)
return SX126X_DIO3_TCXO_VOLTAGE > 0.0f ? tcxo_delay_us + 1000UL : 1000UL;
#elif defined(LR11X0_DIO3_TCXO_VOLTAGE)
return LR11X0_DIO3_TCXO_VOLTAGE > 0.0f ? tcxo_delay_us + 1000UL : 1000UL;
#else
return tcxo_delay_us + 1000UL;
#endif
}
inline float rxPowerSavingDefaultCaptureCost() {
#ifdef USE_LR1110
#if defined(USE_LR1110) || defined(USE_LR1121)
return RX_POWERSAVING_CAPTURE_COST_SYMBOLS_LR11X0;
#else
return RX_POWERSAVING_CAPTURE_COST_SYMBOLS;
+573
View File
@@ -0,0 +1,573 @@
"""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()
+4 -4
View File
@@ -220,8 +220,8 @@ int main(){
++now_ms;assert(!chip.isReceiving()&&!chip._headerSeen);
chip.irq=ACTIVITY_HEADER|ACTIVITY_HEADER_ERROR;
assert(!chip.isReceiving()&&!chip._headerSeen&&chip._activityAt==0);
#if MESH_RADIO_PROFILE_CHIP == 1110
// LR1110 deliberately leaves its error IRQ for wrapper-owned recovery.
#if MESH_RADIO_PROFILE_CHIP == 1110 || MESH_RADIO_PROFILE_CHIP == 1121
// LR11x0 deliberately leaves its error IRQ for wrapper-owned recovery.
assert(chip.irq&ACTIVITY_HEADER_ERROR);
#else
assert(!(chip.irq&ACTIVITY_HEADER_ERROR));
@@ -261,7 +261,7 @@ int main(){
assert(chip._headerSeen&&chip._activityAt==37);
chip.duty_failure=0;
assert(chip.startReceiveDutyCycle(100000,100000)==0&&!chip._headerSeen&&chip._activityAt==0);
#if MESH_RADIO_PROFILE_CHIP == 1110
#if MESH_RADIO_PROFILE_CHIP == 1110 || MESH_RADIO_PROFILE_CHIP == 1121
// Bounds and every preparatory command can reject duty-cycle entry. None
// acknowledged a new RX epoch, so none may retire the old local activity.
chip._headerSeen=true;chip._activityAt=37;
@@ -357,7 +357,7 @@ class RadioRxActivityEpochTests(unittest.TestCase):
compile_run(build_source(family)+FAST_CHECKS,['-DESP32_PLATFORM=1','-DRADIO_CLASS='+family])
def test_actual_duty_cycle_entry_resets_only_after_acknowledged_entry(self):
for family in ('CustomSX1262','CustomLR1110'):
for family in ('CustomSX1262','CustomLR1110','CustomLR1121'):
for remove in (False,True):
with self.subTest(family=family,removed_reset=remove):
source=build_source(family,remove_reset=remove,include_duty=True)
@@ -20,6 +20,7 @@ build_flags = ${esp32_base.build_flags}
-D P_LORA_MOSI=45
-D RADIO_CLASS=CustomLR1121
-D WRAPPER_CLASS=CustomLR1121Wrapper
-D USE_LR1121
-D LR11X0_DIO3_TCXO_VOLTAGE=1.8
-D LR1121_FORCE_HP_PA ; RFO_LP_LF is not fitted (R4 NC)
-D RX_BOOSTED_GAIN=true
@@ -56,6 +56,8 @@ bool radio_init() {
radio.setRxBoostedGainMode(RX_BOOSTED_GAIN);
#endif
// Full RX recovery must restore this board's TCXO and DIO RF-switch table.
radio_driver.setDeepInitCallback(radio_init);
return true;
}