fix(lr2021): live LF↔HF reconfigure via full begin() (#11279)

* fix(lr2021): full begin() on live LF/HF reconfigure.Avoid RadioLib -706 / assert when live-switching Sub-GHz ↔ LORA_24.

* fix(lr2021): align band-hop begin() with init() robustnessMirror.  RF-switch GPIOs, SPI/TCXO retries, and log CRC/RX-gain errors.

* chore: trunk fmt LR20x0Interface bandHop line wrap

* fix(lr2021): harden live band reconfigure (companion #1)

* fix(lr2021): reject invalid freq in band-hop path select

Require requestedMHz > 0 in isLr20x0BandHop, expose lr20x0ReconfigurePathfor FullBegin vs incremental selection, and extend native radio tests.

---------

Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
This commit is contained in:
Ethac.chen
2026-08-05 10:07:14 +00:00
committed by GitHub
co-authored by Thomas Göttgens
parent 74b9f6ff02
commit 08722da3d9
3 changed files with 206 additions and 36 deletions
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <cstdint>
constexpr bool isLr20x0HighBand(float frequencyMHz)
{
return frequencyMHz > 1500.0f;
}
constexpr bool isLr20x0BandHop(float previousMHz, float requestedMHz)
{
return previousMHz > 0.0f && requestedMHz > 0.0f &&
isLr20x0HighBand(previousMHz) != isLr20x0HighBand(requestedMHz);
}
// Path taken by LR20x0Interface::reconfigure() for a frequency change.
enum class Lr20x0ReconfigurePath : uint8_t { Incremental, FullBegin };
constexpr Lr20x0ReconfigurePath lr20x0ReconfigurePath(float previousMHz, float requestedMHz)
{
return isLr20x0BandHop(previousMHz, requestedMHz) ? Lr20x0ReconfigurePath::FullBegin
: Lr20x0ReconfigurePath::Incremental;
}
+138 -36
View File
@@ -1,6 +1,7 @@
#include "configuration.h"
#if (defined(USE_LR2021) || defined(ARCH_PORTDUINO)) && RADIOLIB_EXCLUDE_LR2021 != 1
#include "LR20x0Band.h"
#include "LR20x0Interface.h"
#include "error.h"
#include "mesh/NodeDB.h"
@@ -42,6 +43,9 @@ static const Module::RfSwitchMode_t lr20x0_rfswitch_table[] = {
#define LR2021_MAX_POWER_HF 12
#endif
// Last programmed carrier; LF/HF hops use full begin() (live setOutputPower returns -706).
static float lr20x0LastFreqMHz = 0;
template <typename T>
LR20x0Interface<T>::LR20x0Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst,
RADIOLIB_PIN_TYPE busy)
@@ -98,14 +102,14 @@ template <typename T> bool LR20x0Interface<T>::init()
#ifdef LR2021_RF_SWITCH_SUBGHZ
pinMode(LR2021_RF_SWITCH_SUBGHZ, OUTPUT);
digitalWrite(LR2021_RF_SWITCH_SUBGHZ, getFreq() < 1e9 ? HIGH : LOW);
LOG_DEBUG("Set RF0 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz");
digitalWrite(LR2021_RF_SWITCH_SUBGHZ, isLr20x0HighBand(getFreq()) ? LOW : HIGH);
LOG_DEBUG("Set RF0 switch to %s", isLr20x0HighBand(getFreq()) ? "2.4GHz" : "SubGHz");
#endif
#ifdef LR2021_RF_SWITCH_2_4GHZ
pinMode(LR2021_RF_SWITCH_2_4GHZ, OUTPUT);
digitalWrite(LR2021_RF_SWITCH_2_4GHZ, getFreq() < 1e9 ? LOW : HIGH);
LOG_DEBUG("Set RF1 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz");
digitalWrite(LR2021_RF_SWITCH_2_4GHZ, isLr20x0HighBand(getFreq()) ? HIGH : LOW);
LOG_DEBUG("Set RF1 switch to %s", isLr20x0HighBand(getFreq()) ? "2.4GHz" : "SubGHz");
#endif
// Allow extra time for TCXO to stabilize after power-on
@@ -169,56 +173,154 @@ template <typename T> bool LR20x0Interface<T>::init()
if (res == RADIOLIB_ERR_NONE)
startReceive(); // start receiving
lr20x0LastFreqMHz = getFreq();
return res == RADIOLIB_ERR_NONE;
}
template <typename T> bool LR20x0Interface<T>::reconfigure()
{
RadioLibInterface::reconfigure();
bool success = RadioLibInterface::reconfigure();
// set mode to standby
setStandby();
// configure publicly accessible settings
int err = lora.setSpreadingFactor(sf);
if (err != RADIOLIB_ERR_NONE)
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
err = lora.setBandwidth(bw); // different form than LR11xx
if (err != RADIOLIB_ERR_NONE)
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
err = lora.setCodingRate(cr, cr != 7); // use long interleaving except if CR is 4/7 which doesn't support it
if (err != RADIOLIB_ERR_NONE)
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
err = lora.setSyncWord(syncWord);
assert(err == RADIOLIB_ERR_NONE);
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) { // clamp if wide freq range
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) {
limitPower(LR2021_MAX_POWER_HF);
} else {
limitPower(LR2021_MAX_POWER); // default clamp for non-wide freq range
limitPower(LR2021_MAX_POWER);
}
const float freq = getFreq();
const bool bandHop = lr20x0ReconfigurePath(lr20x0LastFreqMHz, freq) == Lr20x0ReconfigurePath::FullBegin;
if (bandHop) {
LOG_INFO("LR20x0 LF/HF band hop %.1f -> %.1f MHz, full begin()", lr20x0LastFreqMHz, freq);
setStandby();
// Match init(): external LF/HF front-end GPIOs (if board defines them).
#ifdef LR2021_RF_SWITCH_SUBGHZ
pinMode(LR2021_RF_SWITCH_SUBGHZ, OUTPUT);
digitalWrite(LR2021_RF_SWITCH_SUBGHZ, isLr20x0HighBand(freq) ? LOW : HIGH);
LOG_DEBUG("Set RF0 switch to %s", isLr20x0HighBand(freq) ? "2.4GHz" : "SubGHz");
#endif
#ifdef LR2021_RF_SWITCH_2_4GHZ
pinMode(LR2021_RF_SWITCH_2_4GHZ, OUTPUT);
digitalWrite(LR2021_RF_SWITCH_2_4GHZ, isLr20x0HighBand(freq) ? HIGH : LOW);
LOG_DEBUG("Set RF1 switch to %s", isLr20x0HighBand(freq) ? "2.4GHz" : "SubGHz");
#endif
#if ARCH_PORTDUINO
float tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000;
#elif defined(LR2021_DIO3_TCXO_VOLTAGE)
float tcxoVoltage = LR2021_DIO3_TCXO_VOLTAGE;
#elif defined(TCXO_OPTIONAL)
float tcxoVoltage = 1.6f;
#else
float tcxoVoltage = 0;
#endif
delay(10); // same TCXO settle window as init()
int res = lora.begin(freq, bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
if (res == RADIOLIB_ERR_SPI_CMD_FAILED) {
LOG_WARN("LR20x0 band-hop begin SPI_CMD_FAILED, retrying...");
delay(100);
res = lora.begin(freq, bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
}
#if defined(TCXO_OPTIONAL)
if (res != RADIOLIB_ERR_NONE && res != RADIOLIB_ERR_CHIP_NOT_FOUND && tcxoVoltage > 0) {
LOG_WARN("LR20x0 band-hop begin TCXO failed (%s%d), retry without TCXO", radioLibErr, res);
tcxoVoltage = 0;
res = lora.begin(freq, bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
}
#endif
if (res != RADIOLIB_ERR_NONE) {
LOG_ERROR("LR20x0 band-hop begin %s%d", radioLibErr, res);
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
return false;
}
lr20x0LastFreqMHz = freq;
res = lora.setCRC(2);
if (res != RADIOLIB_ERR_NONE) {
LOG_ERROR("LR20x0 band-hop setCRC %s%d", radioLibErr, res);
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
return false;
}
#ifdef LR2021_DIO_AS_RF_SWITCH
lora.setRfSwitchTable(lr20x0_rfswitch_dio_pins, lr20x0_rfswitch_table);
#elif ARCH_PORTDUINO
if (portduino_config.has_rfswitch_table)
lora.setRfSwitchTable(lr20x0_rfswitch_dio_pins, lr20x0_rfswitch_table);
#endif
res = lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain);
if (res != RADIOLIB_ERR_NONE) {
LOG_ERROR("LR20x0 band-hop setRxBoostedGainMode %s%d", radioLibErr, res);
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
return false;
}
startReceive();
return true;
}
// Same-band reconfigure (previous incremental path)
setStandby();
int err = lora.setFrequency(freq);
if (err != RADIOLIB_ERR_NONE) {
LOG_ERROR("LR20x0 setFrequency %.3f MHz %s%d", freq, radioLibErr, err);
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
success = false;
}
err = lora.setSpreadingFactor(sf);
if (err != RADIOLIB_ERR_NONE) {
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
success = false;
}
err = lora.setBandwidth(bw);
if (err != RADIOLIB_ERR_NONE) {
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
success = false;
}
err = lora.setCodingRate(cr, cr != 7);
if (err != RADIOLIB_ERR_NONE) {
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
success = false;
}
err = lora.setSyncWord(syncWord);
if (err != RADIOLIB_ERR_NONE) {
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
success = false;
}
err = lora.setPreambleLength(preambleLength);
assert(err == RADIOLIB_ERR_NONE);
err = lora.setFrequency(getFreq());
if (err != RADIOLIB_ERR_NONE)
if (err != RADIOLIB_ERR_NONE) {
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
success = false;
}
err = lora.setOutputPower(power);
assert(err == RADIOLIB_ERR_NONE);
if (err != RADIOLIB_ERR_NONE) {
LOG_ERROR("LR20x0 setOutputPower %d dBm @ %.3f MHz %s%d", power, freq, radioLibErr, err);
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
success = false;
}
// Apply RX gain mode - valid in STDBY, matches resetAGC() pattern
err = lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain);
if (err != RADIOLIB_ERR_NONE)
if (err != RADIOLIB_ERR_NONE) {
LOG_WARN("LR20x0 setRxBoostedGainMode %s%d", radioLibErr, err);
success = false;
}
startReceive(); // restart receiving
return true;
if (success) {
startReceive();
lr20x0LastFreqMHz = freq;
}
return success;
}
template <typename T> void LR20x0Interface<T>::disableInterrupt()
+45
View File
@@ -1,3 +1,4 @@
#include "LR20x0Band.h"
#include "MeshRadio.h"
#include "MeshService.h"
#include "RadioInterface.h"
@@ -9,6 +10,47 @@
static MockMeshService *mockMeshService;
static void test_lr20x0BandClassification()
{
TEST_ASSERT_FALSE(isLr20x0HighBand(906.875f));
TEST_ASSERT_FALSE(isLr20x0HighBand(1500.0f));
TEST_ASSERT_TRUE(isLr20x0HighBand(2400.0f));
TEST_ASSERT_TRUE(isLr20x0HighBand(2420.71875f));
}
static void test_lr20x0BandHopDetection()
{
TEST_ASSERT_FALSE(isLr20x0BandHop(0.0f, 2420.71875f));
TEST_ASSERT_FALSE(isLr20x0BandHop(906.875f, 915.0f));
TEST_ASSERT_FALSE(isLr20x0BandHop(2400.0f, 2420.71875f));
TEST_ASSERT_TRUE(isLr20x0BandHop(906.875f, 2420.71875f));
TEST_ASSERT_TRUE(isLr20x0BandHop(2420.71875f, 906.875f));
// Invalid requested frequency must not look like a band hop.
TEST_ASSERT_FALSE(isLr20x0BandHop(2420.71875f, 0.0f));
TEST_ASSERT_FALSE(isLr20x0BandHop(906.875f, 0.0f));
TEST_ASSERT_FALSE(isLr20x0BandHop(2420.71875f, -1.0f));
TEST_ASSERT_FALSE(isLr20x0BandHop(906.875f, -915.0f));
}
static void test_lr20x0ReconfigurePathSelection()
{
// LF -> HF and HF -> LF take full begin(); same-band stays incremental.
TEST_ASSERT_EQUAL(static_cast<int>(Lr20x0ReconfigurePath::FullBegin),
static_cast<int>(lr20x0ReconfigurePath(906.875f, 2420.71875f)));
TEST_ASSERT_EQUAL(static_cast<int>(Lr20x0ReconfigurePath::FullBegin),
static_cast<int>(lr20x0ReconfigurePath(2420.71875f, 906.875f)));
TEST_ASSERT_EQUAL(static_cast<int>(Lr20x0ReconfigurePath::Incremental),
static_cast<int>(lr20x0ReconfigurePath(906.875f, 915.0f)));
TEST_ASSERT_EQUAL(static_cast<int>(Lr20x0ReconfigurePath::Incremental),
static_cast<int>(lr20x0ReconfigurePath(2400.0f, 2420.71875f)));
TEST_ASSERT_EQUAL(static_cast<int>(Lr20x0ReconfigurePath::Incremental),
static_cast<int>(lr20x0ReconfigurePath(0.0f, 2420.71875f)));
TEST_ASSERT_EQUAL(static_cast<int>(Lr20x0ReconfigurePath::Incremental),
static_cast<int>(lr20x0ReconfigurePath(2420.71875f, 0.0f)));
TEST_ASSERT_EQUAL(static_cast<int>(Lr20x0ReconfigurePath::Incremental),
static_cast<int>(lr20x0ReconfigurePath(906.875f, -1.0f)));
}
// Test shim to expose protected radio parameters set by applyModemConfig()
class TestableRadioInterface : public RadioInterface
{
@@ -359,6 +401,9 @@ void setup()
initializeTestEnvironment();
UNITY_BEGIN();
RUN_TEST(test_lr20x0BandClassification);
RUN_TEST(test_lr20x0BandHopDetection);
RUN_TEST(test_lr20x0ReconfigurePathSelection);
RUN_TEST(test_bwCodeToKHz_specialMappings);
RUN_TEST(test_bwCodeToKHz_passthrough);
RUN_TEST(test_bwCodeToKHz_roundTrip);