Skip to content
Closed
7 changes: 6 additions & 1 deletion src/mesh/LR11x0Interface.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ template <class T> class LR11x0Interface : public RadioLibInterface
/// \return true if initialisation succeeded.
virtual bool reconfigure() override;

bool supportsLoRaBandwidth(float bandwidthKHz, bool wideBand) override
{
return supportsLr11x0LoRaBandwidth(bandwidthKHz, wideBand);
}

/// Prepare hardware for sleep. Call this _only_ for deep sleep, not needed for light sleep.
virtual bool sleep() override;

Expand Down Expand Up @@ -74,4 +79,4 @@ template <class T> class LR11x0Interface : public RadioLibInterface

uint32_t getPacketTime(uint32_t pl, bool received) override { return computePacketTime(lora, pl, received); }
};
#endif
#endif
5 changes: 5 additions & 0 deletions src/mesh/LR20x0Interface.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ template <class T> class LR20x0Interface : public RadioLibInterface
/// \return true if initialisation succeeded.
virtual bool reconfigure() override;

bool supportsLoRaBandwidth(float bandwidthKHz, bool wideBand) override
{
return supportsLr20x0LoRaBandwidth(bandwidthKHz, wideBand);
}

/// Prepare hardware for sleep. Call this _only_ for deep sleep, not needed for light sleep.
virtual bool sleep() override;

Expand Down
17 changes: 16 additions & 1 deletion src/mesh/MeshRadio.h
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,21 @@ static inline uint16_t clampBandwidthCode(uint16_t bwCode)
return bwCode;
}

static inline bool supportsSx128xLoRaBandwidth(float bandwidthKHz, bool wideBand)
Comment thread
RCGV1 marked this conversation as resolved.
Resolved
{
Comment thread
RCGV1 marked this conversation as resolved.
Resolved
return wideBand && (bandwidthKHz == 203.125f || bandwidthKHz == 406.25f || bandwidthKHz == 812.5f || bandwidthKHz == 1625.0f);
}

static inline bool supportsLr11x0LoRaBandwidth(float bandwidthKHz, bool wideBand)
{
return !wideBand || bandwidthKHz == 203.125f || bandwidthKHz == 406.25f || bandwidthKHz == 812.5f;
}

static inline bool supportsLr20x0LoRaBandwidth(float bandwidthKHz, bool wideBand)
{
return !wideBand || bandwidthKHz == 203.125f || bandwidthKHz == 406.25f || bandwidthKHz == 812.5f || bandwidthKHz == 1000.0f;
}

static inline void modemPresetToParams(meshtastic_Config_LoRaConfig_ModemPreset preset, bool wideLora, float &bwKHz, uint8_t &sf,
uint8_t &cr)
{
Expand Down Expand Up @@ -294,4 +309,4 @@ static inline float modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset
uint8_t cr = 0;
modemPresetToParams(preset, wideLora, bwKHz, sf, cr);
return bwKHz;
}
}
32 changes: 27 additions & 5 deletions src/mesh/RadioInterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ extern SPIClass SPI1;
std::unique_ptr<RadioInterface> initLoRa()
{
std::unique_ptr<RadioInterface> rIf = nullptr;
const auto loraConfigBeforeProbe = config.lora;

#if ARCH_PORTDUINO
SPISettings loraSpiSettings(portduino_config.spiSpeed, MSBFIRST, SPI_MODE0);
Expand Down Expand Up @@ -583,6 +584,7 @@ std::unique_ptr<RadioInterface> initLoRa()
new LR1120Interface(loraHal, LR1120_SPI_NSS_PIN, LR1120_IRQ_PIN, LR1120_NRESET_PIN, LR1120_BUSY_PIN));
if (!rIf->init()) {
LOG_WARN("No LR1120 radio");
config.lora = loraConfigBeforeProbe;
rIf = nullptr;
} else {
LOG_INFO("LR1120 init success");
Expand All @@ -597,6 +599,7 @@ std::unique_ptr<RadioInterface> initLoRa()
new LR1121Interface(loraHal, LR1121_SPI_NSS_PIN, LR1121_IRQ_PIN, LR1121_NRESET_PIN, LR1121_BUSY_PIN));
if (!rIf->init()) {
LOG_WARN("No LR1121 radio");
config.lora = loraConfigBeforeProbe;
rIf = nullptr;
} else {
LOG_INFO("LR1121 init success");
Expand All @@ -611,6 +614,7 @@ std::unique_ptr<RadioInterface> initLoRa()
new LR2021Interface(loraHal, LR2021_SPI_NSS_PIN, LR2021_IRQ_PIN, LR2021_NRESET_PIN, LR2021_BUSY_PIN));
if (!rIf->init()) {
LOG_WARN("No LR2021 radio");
config.lora = loraConfigBeforeProbe;
rIf = nullptr;
} else {
LOG_INFO("LR2021 init success");
Comment thread
RCGV1 marked this conversation as resolved.
Resolved
Expand Down Expand Up @@ -1085,7 +1089,7 @@ bool RadioInterface::validateConfigRegion(const meshtastic_Config_LoRaConfig &lo
* When clamp==false, returns false on first error (pure validation).
* When clamp==true, fixes invalid settings in-place and returns true.
*/
bool RadioInterface::checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraConfig, bool clamp)
bool RadioInterface::checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraConfig, bool clamp, RadioInterface *radio)
{
char err_string[160];
float check_bw;
Expand Down Expand Up @@ -1147,6 +1151,24 @@ bool RadioInterface::checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraCo
check_bw = clampBandwidthKHz(bwCodeToKHz(loraConfig.bandwidth));
}

RadioInterface *candidateRadio = radio ? radio : RadioLibInterface::instance;
if (candidateRadio && !candidateRadio->supportsLoRaBandwidth(check_bw, newRegion->wideLora)) {
const float defaultBandwidth = modemPresetToBwKHz(newRegion->getDefaultPreset(), newRegion->wideLora);
snprintf(err_string, sizeof(err_string), "Bandwidth %.3fkHz invalid for this radio in %s", check_bw, newRegion->name);
LOG_ERROR("%s", err_string);
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
sendErrorNotification(err_string);

if (!clamp)
return false;

if (loraConfig.use_preset)
loraConfig.modem_preset = newRegion->getDefaultPreset();
else
loraConfig.bandwidth = bwKHzToCode(defaultBandwidth);
check_bw = defaultBandwidth;
}

// Calculate width of slots (aka channels) based on bandwidth and any spacing or padding required by the region:
// spacing = gap between slots (0 for continuous spectrum) and at the beginning of the band
// padding = gap at the beginning and end of the slots (0 for no padding)
Expand Down Expand Up @@ -1231,15 +1253,15 @@ bool RadioInterface::checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraCo
return true;
}

bool RadioInterface::validateConfigLora(const meshtastic_Config_LoRaConfig &loraConfig)
bool RadioInterface::validateConfigLora(const meshtastic_Config_LoRaConfig &loraConfig, RadioInterface *radio)
{
auto copy = loraConfig;
return checkOrClampConfigLora(copy, false);
return checkOrClampConfigLora(copy, false, radio);
}

void RadioInterface::clampConfigLora(meshtastic_Config_LoRaConfig &loraConfig)
void RadioInterface::clampConfigLora(meshtastic_Config_LoRaConfig &loraConfig, RadioInterface *radio)
{
checkOrClampConfigLora(loraConfig, true);
checkOrClampConfigLora(loraConfig, true, radio);
}

/**
Expand Down
9 changes: 6 additions & 3 deletions src/mesh/RadioInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ class RadioInterface
/// multiband chips like the LR1121 keep the default.
virtual bool supportsSubGhz() { return true; }

/// Whether this radio accepts the exact bandwidth for the requested band.
virtual bool supportsLoRaBandwidth(float bandwidthKHz, bool wideBand) { return true; }

/// Prepare hardware for sleep. Call this _only_ for deep sleep, not needed for light sleep.
virtual bool sleep() { return true; }

Expand Down Expand Up @@ -253,7 +256,7 @@ class RadioInterface
// Whether we have a custom channel name
static bool uses_custom_channel_name;

static bool checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraConfig, bool clamp);
static bool checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraConfig, bool clamp, RadioInterface *radio = nullptr);

// Check if a candidate region is compatible and valid, with no side effects (safe for
// speculative UI checks). prospectiveLicensedOwner is for a UI flow that requires
Expand All @@ -266,10 +269,10 @@ class RadioInterface
static bool validateConfigRegion(const meshtastic_Config_LoRaConfig &loraConfig);

// Check if a candidate radio configuration is valid.
static bool validateConfigLora(const meshtastic_Config_LoRaConfig &loraConfig);
static bool validateConfigLora(const meshtastic_Config_LoRaConfig &loraConfig, RadioInterface *radio = nullptr);

// Make a candidate radio configuration valid, even if it isn't.
static void clampConfigLora(meshtastic_Config_LoRaConfig &loraConfig);
static void clampConfigLora(meshtastic_Config_LoRaConfig &loraConfig, RadioInterface *radio = nullptr);

// If preset is locked to a sibling of currentRegion among the swappable EU regions
// (EU_868/EU_866/EU_N_868), return the sibling region owning the preset, else nullptr.
Expand Down
5 changes: 5 additions & 0 deletions src/mesh/SX128xInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ template <class T> class SX128xInterface : public RadioLibInterface
/// SX128x is a 2.4 GHz-only chip; it cannot tune sub-GHz regions
virtual bool supportsSubGhz() override { return false; }

bool supportsLoRaBandwidth(float bandwidthKHz, bool wideBand) override
{
return supportsSx128xLoRaBandwidth(bandwidthKHz, wideBand);
}

/// Apply any radio provisioning changes
/// Make sure the Driver is properly configured before calling init().
/// \return true if initialisation succeeded.
Expand Down
125 changes: 123 additions & 2 deletions test/test_radio/test_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,26 @@ static MockMeshService *mockMeshService;
class TestableRadioInterface : public RadioInterface
{
public:
enum class BandwidthProfile { SX128X, LR11X0, LR20X0 };

TestableRadioInterface() : RadioInterface() {}
uint8_t getCr() const { return cr; }
uint8_t getSf() const { return sf; }
float getBw() const { return bw; }
BandwidthProfile bandwidthProfile = BandwidthProfile::SX128X;

bool supportsLoRaBandwidth(float bandwidthKHz, bool wideBand) override
{
switch (bandwidthProfile) {
case BandwidthProfile::SX128X:
return supportsSx128xLoRaBandwidth(bandwidthKHz, wideBand);
case BandwidthProfile::LR11X0:
return supportsLr11x0LoRaBandwidth(bandwidthKHz, wideBand);
case BandwidthProfile::LR20X0:
return supportsLr20x0LoRaBandwidth(bandwidthKHz, wideBand);
}
return false;
}

// Override reconfigure to call the base which invokes applyModemConfig()
bool reconfigure() override { return RadioInterface::reconfigure(); }
Expand All @@ -26,6 +42,8 @@ class TestableRadioInterface : public RadioInterface
ErrorCode send(meshtastic_MeshPacket *p) override { return ERRNO_OK; }
};

static TestableRadioInterface *testRadio;

static void test_bwCodeToKHz_specialMappings()
{
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 7.8f, bwCodeToKHz(8));
Expand Down Expand Up @@ -129,12 +147,107 @@ static void test_clampConfigLora_validPresetUnchanged()
TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, cfg.modem_preset);
}

static meshtastic_Config_LoRaConfig makeCustomBandwidth(meshtastic_Config_LoRaConfig_RegionCode region, uint16_t bandwidth)
{
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
cfg.region = region;
cfg.use_preset = false;
cfg.bandwidth = bandwidth;
cfg.spread_factor = 7;
cfg.coding_rate = 5;
return cfg;
}

static void test_validateConfigLora_rejectsSubGhzBandwidthOnLora24()
{
auto cfg = makeCustomBandwidth(meshtastic_Config_LoRaConfig_RegionCode_LORA_24, 125);

TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(cfg, testRadio));
TEST_ASSERT_EQUAL_UINT16(125, cfg.bandwidth);
}

static void test_clampConfigLora_repairsSubGhzBandwidthOnLora24()
{
auto cfg = makeCustomBandwidth(meshtastic_Config_LoRaConfig_RegionCode_LORA_24, 125);

RadioInterface::clampConfigLora(cfg, testRadio);

TEST_ASSERT_EQUAL_UINT16(800, cfg.bandwidth);
TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg, testRadio));
}

static void test_validateConfigLora_allowsSx128xWideBandwidths()
{
const uint16_t bandwidths[] = {200, 400, 800, 1600};
testRadio->bandwidthProfile = TestableRadioInterface::BandwidthProfile::SX128X;

for (auto bandwidth : bandwidths) {
auto cfg = makeCustomBandwidth(meshtastic_Config_LoRaConfig_RegionCode_LORA_24, bandwidth);
TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg, testRadio));
}
}

static void test_validateConfigLora_rejectsLr1121Wide1600()
{
auto cfg = makeCustomBandwidth(meshtastic_Config_LoRaConfig_RegionCode_LORA_24, 1600);
testRadio->bandwidthProfile = TestableRadioInterface::BandwidthProfile::LR11X0;

TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(cfg, testRadio));
}

static void test_validateConfigLora_lr2021WideBandwidths()
{
testRadio->bandwidthProfile = TestableRadioInterface::BandwidthProfile::LR20X0;
auto valid = makeCustomBandwidth(meshtastic_Config_LoRaConfig_RegionCode_LORA_24, 1000);
auto invalid = makeCustomBandwidth(meshtastic_Config_LoRaConfig_RegionCode_LORA_24, 1600);

TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(valid, testRadio));
TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(invalid, testRadio));
}

static void test_clampConfigLora_repairsLr1121TurboPreset()
{
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24;
cfg.use_preset = true;
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO;
testRadio->bandwidthProfile = TestableRadioInterface::BandwidthProfile::LR11X0;

TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(cfg, testRadio));
RadioInterface::clampConfigLora(cfg, testRadio);

TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, cfg.modem_preset);
TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg, testRadio));
}

static void test_clampConfigLora_repairsLr2021TurboPreset()
{
meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero;
cfg.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24;
cfg.use_preset = true;
cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO;
testRadio->bandwidthProfile = TestableRadioInterface::BandwidthProfile::LR20X0;

TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(cfg, testRadio));
RadioInterface::clampConfigLora(cfg, testRadio);

TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, cfg.modem_preset);
TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg, testRadio));
}

static void test_validateConfigLora_preservesSubGhzBandwidth()
{
auto cfg = makeCustomBandwidth(meshtastic_Config_LoRaConfig_RegionCode_US, 125);

TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg, testRadio));
RadioInterface::clampConfigLora(cfg, testRadio);
TEST_ASSERT_EQUAL_UINT16(125, cfg.bandwidth);
}

// -----------------------------------------------------------------------
// applyModemConfig() coding rate tests (via reconfigure)
// -----------------------------------------------------------------------

static TestableRadioInterface *testRadio;

// After fresh flash: coding_rate=0, use_preset=true, modem_preset=LONG_FAST
// CR should come from the preset (5 for LONG_FAST), not from the zero default.
static void test_applyModemConfig_freshFlashCodingRateNotZero()
Expand Down Expand Up @@ -368,6 +481,14 @@ void setup()
RUN_TEST(test_validateConfigLora_rejectsInvalidPresetForRegion);
RUN_TEST(test_clampConfigLora_invalidPresetClampedToDefault);
RUN_TEST(test_clampConfigLora_validPresetUnchanged);
RUN_TEST(test_validateConfigLora_rejectsSubGhzBandwidthOnLora24);
RUN_TEST(test_clampConfigLora_repairsSubGhzBandwidthOnLora24);
RUN_TEST(test_validateConfigLora_allowsSx128xWideBandwidths);
RUN_TEST(test_validateConfigLora_rejectsLr1121Wide1600);
RUN_TEST(test_validateConfigLora_lr2021WideBandwidths);
RUN_TEST(test_clampConfigLora_repairsLr1121TurboPreset);
RUN_TEST(test_clampConfigLora_repairsLr2021TurboPreset);
RUN_TEST(test_validateConfigLora_preservesSubGhzBandwidth);
RUN_TEST(test_applyModemConfig_freshFlashCodingRateNotZero);
RUN_TEST(test_applyModemConfig_codingRateMatchesPreset);
RUN_TEST(test_applyModemConfig_customCodingRateHigherThanPreset);
Expand Down
Loading