diff --git a/src/mesh/LR11x0Interface.cpp b/src/mesh/LR11x0Interface.cpp index c54ae91a6b1..c31b0932984 100644 --- a/src/mesh/LR11x0Interface.cpp +++ b/src/mesh/LR11x0Interface.cpp @@ -168,23 +168,32 @@ template bool LR11x0Interface::init() template bool LR11x0Interface::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); + if (err != RADIOLIB_ERR_NONE) { + if (shouldReportConfigErrors()) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + success = false; + } err = lora.setBandwidth(bw, wideLora() && (getFreq() > 1000.0f)); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + if (err != RADIOLIB_ERR_NONE) { + if (shouldReportConfigErrors()) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + success = false; + } 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); + if (err != RADIOLIB_ERR_NONE) { + if (shouldReportConfigErrors()) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + success = false; + } err = lora.setSyncWord(syncWord); assert(err == RADIOLIB_ERR_NONE); @@ -199,8 +208,11 @@ template bool LR11x0Interface::reconfigure() assert(err == RADIOLIB_ERR_NONE); err = lora.setFrequency(getFreq()); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + if (err != RADIOLIB_ERR_NONE) { + if (shouldReportConfigErrors()) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + success = false; + } err = lora.setOutputPower(power); assert(err == RADIOLIB_ERR_NONE); @@ -210,9 +222,10 @@ template bool LR11x0Interface::reconfigure() if (err != RADIOLIB_ERR_NONE) LOG_WARN("LR11x0 setRxBoostedGainMode %s%d", radioLibErr, err); - startReceive(); // restart receiving + if (success) + startReceive(); // restart receiving - return true; + return success; } template void LR11x0Interface::disableInterrupt() diff --git a/src/mesh/LR11x0Interface.h b/src/mesh/LR11x0Interface.h index ee8761177da..1ce30b77dfd 100644 --- a/src/mesh/LR11x0Interface.h +++ b/src/mesh/LR11x0Interface.h @@ -22,6 +22,11 @@ template 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; @@ -74,4 +79,4 @@ template class LR11x0Interface : public RadioLibInterface uint32_t getPacketTime(uint32_t pl, bool received) override { return computePacketTime(lora, pl, received); } }; -#endif \ No newline at end of file +#endif diff --git a/src/mesh/LR20x0Interface.cpp b/src/mesh/LR20x0Interface.cpp index c7d00e3ab31..9b0fbb0e116 100644 --- a/src/mesh/LR20x0Interface.cpp +++ b/src/mesh/LR20x0Interface.cpp @@ -174,23 +174,32 @@ template bool LR20x0Interface::init() template bool LR20x0Interface::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); + if (err != RADIOLIB_ERR_NONE) { + if (shouldReportConfigErrors()) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + success = false; + } err = lora.setBandwidth(bw); // different form than LR11xx - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + if (err != RADIOLIB_ERR_NONE) { + if (shouldReportConfigErrors()) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + success = false; + } 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); + if (err != RADIOLIB_ERR_NONE) { + if (shouldReportConfigErrors()) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + success = false; + } err = lora.setSyncWord(syncWord); assert(err == RADIOLIB_ERR_NONE); @@ -205,8 +214,11 @@ template bool LR20x0Interface::reconfigure() assert(err == RADIOLIB_ERR_NONE); err = lora.setFrequency(getFreq()); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + if (err != RADIOLIB_ERR_NONE) { + if (shouldReportConfigErrors()) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + success = false; + } err = lora.setOutputPower(power); assert(err == RADIOLIB_ERR_NONE); @@ -216,9 +228,10 @@ template bool LR20x0Interface::reconfigure() if (err != RADIOLIB_ERR_NONE) LOG_WARN("LR20x0 setRxBoostedGainMode %s%d", radioLibErr, err); - startReceive(); // restart receiving + if (success) + startReceive(); // restart receiving - return true; + return success; } template void LR20x0Interface::disableInterrupt() diff --git a/src/mesh/LR20x0Interface.h b/src/mesh/LR20x0Interface.h index 263c83429d2..0af959c0753 100644 --- a/src/mesh/LR20x0Interface.h +++ b/src/mesh/LR20x0Interface.h @@ -22,6 +22,11 @@ template 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; diff --git a/src/mesh/MeshRadio.h b/src/mesh/MeshRadio.h index e5b54d6a21d..8aa0d599943 100644 --- a/src/mesh/MeshRadio.h +++ b/src/mesh/MeshRadio.h @@ -200,6 +200,27 @@ static inline uint16_t clampBandwidthCode(uint16_t bwCode) return bwCode; } +static inline bool supportsSx128xLoRaBandwidth(float bandwidthKHz, bool wideBand) +{ + return wideBand && (bandwidthKHz == 203.125f || bandwidthKHz == 406.25f || bandwidthKHz == 812.5f || bandwidthKHz == 1625.0f); +} + +static inline bool supportsLr11x0LoRaBandwidth(float bandwidthKHz, bool wideBand) +{ + if (wideBand) + return bandwidthKHz == 203.125f || bandwidthKHz == 406.25f || bandwidthKHz == 812.5f; + + return bandwidthKHz == 62.5f || bandwidthKHz == 125.0f || bandwidthKHz == 250.0f || bandwidthKHz == 500.0f; +} + +static inline bool supportsLr20x0LoRaBandwidth(float bandwidthKHz, bool wideBand) +{ + (void)wideBand; + return bandwidthKHz == 31.25f || bandwidthKHz == 41.7f || bandwidthKHz == 62.5f || bandwidthKHz == 83.0f || + bandwidthKHz == 101.0f || bandwidthKHz == 125.0f || bandwidthKHz == 203.125f || bandwidthKHz == 250.0f || + bandwidthKHz == 406.25f || bandwidthKHz == 500.0f || 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) { @@ -294,4 +315,4 @@ static inline float modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset uint8_t cr = 0; modemPresetToParams(preset, wideLora, bwKHz, sf, cr); return bwKHz; -} \ No newline at end of file +} diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index 208e8603d91..0435055653c 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -373,6 +373,7 @@ extern SPIClass SPI1; std::unique_ptr initLoRa() { std::unique_ptr rIf = nullptr; + const auto loraConfigBeforeProbe = config.lora; #if ARCH_PORTDUINO SPISettings loraSpiSettings(portduino_config.spiSpeed, MSBFIRST, SPI_MODE0); @@ -581,12 +582,23 @@ std::unique_ptr initLoRa() if (!rIf) { rIf = std::unique_ptr( new LR1120Interface(loraHal, LR1120_SPI_NSS_PIN, LR1120_IRQ_PIN, LR1120_NRESET_PIN, LR1120_BUSY_PIN)); + rIf->setConfigErrorReporting(false); if (!rIf->init()) { LOG_WARN("No LR1120 radio"); + config.lora = loraConfigBeforeProbe; rIf = nullptr; } else { - LOG_INFO("LR1120 init success"); - radioType = LR1120_RADIO; + config.lora = loraConfigBeforeProbe; + rIf->setConfigErrorReporting(true); + if (!rIf->reconfigure()) { + LOG_WARN("LR1120 reconfigure failed after probe"); + rIf->disable(); + config.lora = loraConfigBeforeProbe; + rIf = nullptr; + } else { + LOG_INFO("LR1120 init success"); + radioType = LR1120_RADIO; + } } } #endif @@ -595,12 +607,23 @@ std::unique_ptr initLoRa() if (!rIf) { rIf = std::unique_ptr( new LR1121Interface(loraHal, LR1121_SPI_NSS_PIN, LR1121_IRQ_PIN, LR1121_NRESET_PIN, LR1121_BUSY_PIN)); + rIf->setConfigErrorReporting(false); if (!rIf->init()) { LOG_WARN("No LR1121 radio"); + config.lora = loraConfigBeforeProbe; rIf = nullptr; } else { - LOG_INFO("LR1121 init success"); - radioType = LR1121_RADIO; + config.lora = loraConfigBeforeProbe; + rIf->setConfigErrorReporting(true); + if (!rIf->reconfigure()) { + LOG_WARN("LR1121 reconfigure failed after probe"); + rIf->disable(); + config.lora = loraConfigBeforeProbe; + rIf = nullptr; + } else { + LOG_INFO("LR1121 init success"); + radioType = LR1121_RADIO; + } } } #endif @@ -609,12 +632,23 @@ std::unique_ptr initLoRa() if (!rIf) { rIf = std::unique_ptr( new LR2021Interface(loraHal, LR2021_SPI_NSS_PIN, LR2021_IRQ_PIN, LR2021_NRESET_PIN, LR2021_BUSY_PIN)); + rIf->setConfigErrorReporting(false); if (!rIf->init()) { LOG_WARN("No LR2021 radio"); + config.lora = loraConfigBeforeProbe; rIf = nullptr; } else { - LOG_INFO("LR2021 init success"); - radioType = LR2021_RADIO; + config.lora = loraConfigBeforeProbe; + rIf->setConfigErrorReporting(true); + if (!rIf->reconfigure()) { + LOG_WARN("LR2021 reconfigure failed after probe"); + rIf->disable(); + config.lora = loraConfigBeforeProbe; + rIf = nullptr; + } else { + LOG_INFO("LR2021 init success"); + radioType = LR2021_RADIO; + } } } #endif @@ -1085,18 +1119,21 @@ 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, + bool reportErrors) { char err_string[160]; float check_bw; const RegionInfo *newRegion = getRegion(loraConfig.region); + RadioInterface *candidateRadio = radio ? radio : RadioLibInterface::instance; + auto useWideModemParameters = [&]() { return newRegion->wideLora || (candidateRadio && !candidateRadio->supportsSubGhz()); }; const char *presetName = DisplayFormatters::getModemPresetDisplayName(loraConfig.modem_preset, false, loraConfig.use_preset); // Check preset validity (only when use_preset is true) if (loraConfig.use_preset) { - check_bw = modemPresetToBwKHz(loraConfig.modem_preset, newRegion->wideLora); + check_bw = modemPresetToBwKHz(loraConfig.modem_preset, useWideModemParameters()); bool preset_valid = newRegion->supportsPreset(loraConfig.modem_preset); if (!preset_valid) { @@ -1108,18 +1145,21 @@ bool RadioInterface::checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraCo // Validation must still fail so callers route into the clamp, but quietly: // the clamp will accept this config by swapping regions, so don't record a // critical error or alarm the user over a change that is about to succeed. - LOG_INFO("Preset %s implies region swap %s to %s, deferring to clamp", presetName, newRegion->name, - swapRegion->name); + if (reportErrors) + LOG_INFO("Preset %s implies region swap %s to %s, deferring to clamp", presetName, newRegion->name, + swapRegion->name); return false; } snprintf(err_string, sizeof(err_string), "Preset %s swaps region %s to %s", presetName, newRegion->name, swapRegion->name); - LOG_INFO("%s", err_string); - sendErrorNotification(err_string, meshtastic_LogRecord_Level_INFO); + if (reportErrors) { + LOG_INFO("%s", err_string); + sendErrorNotification(err_string, meshtastic_LogRecord_Level_INFO); + } loraConfig.region = swapRegion->code; newRegion = swapRegion; - check_bw = modemPresetToBwKHz(loraConfig.modem_preset, newRegion->wideLora); + check_bw = modemPresetToBwKHz(loraConfig.modem_preset, useWideModemParameters()); preset_valid = true; } } @@ -1131,13 +1171,15 @@ bool RadioInterface::checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraCo } else { snprintf(err_string, sizeof(err_string), "Preset %s invalid for %s", presetName, newRegion->name); } - LOG_ERROR("%s", err_string); - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - sendErrorNotification(err_string); + if (reportErrors) { + LOG_ERROR("%s", err_string); + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + sendErrorNotification(err_string); + } if (clamp) { loraConfig.modem_preset = newRegion->getDefaultPreset(); - check_bw = modemPresetToBwKHz(loraConfig.modem_preset, newRegion->wideLora); + check_bw = modemPresetToBwKHz(loraConfig.modem_preset, useWideModemParameters()); } else { return false; } @@ -1147,6 +1189,25 @@ bool RadioInterface::checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraCo check_bw = clampBandwidthKHz(bwCodeToKHz(loraConfig.bandwidth)); } + if (candidateRadio && !candidateRadio->supportsLoRaBandwidth(check_bw, useWideModemParameters())) { + const float defaultBandwidth = modemPresetToBwKHz(newRegion->getDefaultPreset(), useWideModemParameters()); + snprintf(err_string, sizeof(err_string), "Bandwidth %.3fkHz invalid for this radio in %s", check_bw, newRegion->name); + if (reportErrors) { + 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) @@ -1157,12 +1218,14 @@ bool RadioInterface::checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraCo if ((newRegion->freqEnd - newRegion->freqStart) < freqSlotWidth) { const float regionSpanKHz = (newRegion->freqEnd - newRegion->freqStart) * 1000.0f; snprintf(err_string, sizeof(err_string), "%s span %.0fkHz < requested %.0fkHz", newRegion->name, regionSpanKHz, check_bw); - LOG_ERROR("%s", err_string); - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - sendErrorNotification(err_string); + if (reportErrors) { + LOG_ERROR("%s", err_string); + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + sendErrorNotification(err_string); + } if (clamp) { - loraConfig.bandwidth = bwKHzToCode(modemPresetToBwKHz(newRegion->getDefaultPreset(), newRegion->wideLora)); + loraConfig.bandwidth = bwKHzToCode(modemPresetToBwKHz(newRegion->getDefaultPreset(), useWideModemParameters())); check_bw = bwCodeToKHz(loraConfig.bandwidth); // Recompute slot width and number of slots based on the new bandwidth @@ -1199,9 +1262,11 @@ bool RadioInterface::checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraCo if (loraConfig.channel_num > numFreqSlots) { snprintf(err_string, sizeof(err_string), "Channel number %u invalid for %s, max is %u", loraConfig.channel_num, newRegion->name, numFreqSlots); - LOG_ERROR("%s", err_string); - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - sendErrorNotification(err_string); + if (reportErrors) { + LOG_ERROR("%s", err_string); + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + sendErrorNotification(err_string); + } if (clamp) { if (uses_custom_channel_name) { // clamp to channel name hash @@ -1231,15 +1296,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, bool reportErrors) { auto copy = loraConfig; - return checkOrClampConfigLora(copy, false); + return checkOrClampConfigLora(copy, false, radio, reportErrors); } -void RadioInterface::clampConfigLora(meshtastic_Config_LoRaConfig &loraConfig) +void RadioInterface::clampConfigLora(meshtastic_Config_LoRaConfig &loraConfig, RadioInterface *radio, bool reportErrors) { - checkOrClampConfigLora(loraConfig, true); + checkOrClampConfigLora(loraConfig, true, radio, reportErrors); } /** @@ -1252,14 +1317,15 @@ void RadioInterface::applyModemConfig() // No Sync Words in LORA mode meshtastic_Config_LoRaConfig &loraConfig = config.lora; const RegionInfo *newRegion = getRegion(loraConfig.region); + const bool useWideModemParameters = newRegion->wideLora || !supportsSubGhz(); myRegion = newRegion; if (loraConfig.use_preset) { - if (!validateConfigLora(loraConfig)) { + if (!validateConfigLora(loraConfig, this, reportConfigErrors)) { loraConfig.modem_preset = newRegion->getDefaultPreset(); } uint8_t newcr; - modemPresetToParams(loraConfig.modem_preset, newRegion->wideLora, bw, sf, newcr); + modemPresetToParams(loraConfig.modem_preset, useWideModemParameters, bw, sf, newcr); // If custom CR is being used already, check if the new preset is higher if (loraConfig.coding_rate >= 5 && loraConfig.coding_rate <= 8 && loraConfig.coding_rate < newcr) { cr = newcr; @@ -1274,14 +1340,20 @@ void RadioInterface::applyModemConfig() } } else { // if not using preset, then just use the custom settings - if (validateConfigLora(loraConfig)) { + if (validateConfigLora(loraConfig, this, reportConfigErrors)) { } else { - LOG_WARN("Invalid LoRa config settings, cannot apply requested modem config - falling back to %s defaults", - newRegion->name); - clampConfigLora(loraConfig); + if (reportConfigErrors) + LOG_WARN("Invalid LoRa config settings, cannot apply requested modem config - falling back to %s defaults", + newRegion->name); + clampConfigLora(loraConfig, this, reportConfigErrors); } // Clamp at the source so numFreqSlots below can never be 0 (a bandwidth-0 config may already be persisted) bw = clampBandwidthKHz(bwCodeToKHz(loraConfig.bandwidth)); + if (!supportsLoRaBandwidth(bw, useWideModemParameters)) { + // Keep 2.4 GHz-only radios operational while an incompatible or UNSET region is being corrected. + bw = modemPresetToBwKHz(newRegion->getDefaultPreset(), useWideModemParameters); + loraConfig.bandwidth = bwKHzToCode(bw); + } sf = loraConfig.spread_factor; cr = loraConfig.coding_rate; } @@ -1353,7 +1425,7 @@ void RadioInterface::applyModemConfig() saveChannelNum(channel_num); saveFreq(freq + loraConfig.frequency_offset); - if (newRegion->wideLora) { // clamp if wide freq range + if (useWideModemParameters) { // clamp if wide freq range preambleLength = wideLoraPreambleLengthDefault; // 12 is the default for operation above 2GHz } else { preambleLength = diff --git a/src/mesh/RadioInterface.h b/src/mesh/RadioInterface.h index eb9315ac122..884f3c1ec25 100644 --- a/src/mesh/RadioInterface.h +++ b/src/mesh/RadioInterface.h @@ -154,6 +154,11 @@ 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; } + + void setConfigErrorReporting(bool enabled) { reportConfigErrors = enabled; } + /// Prepare hardware for sleep. Call this _only_ for deep sleep, not needed for light sleep. virtual bool sleep() { return true; } @@ -253,7 +258,8 @@ 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, + bool reportErrors = true); // 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 @@ -266,10 +272,12 @@ 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, + bool reportErrors = true); // 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, + bool reportErrors = true); // 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. @@ -279,6 +287,8 @@ class RadioInterface protected: int8_t power = 17; // Set by applyModemConfig() + bool shouldReportConfigErrors() const { return reportConfigErrors; } + float savedFreq; uint32_t savedChannelNum; @@ -313,6 +323,8 @@ class RadioInterface virtual int16_t getCurrentRSSI() { return 0; } private: + bool reportConfigErrors = true; + /** * Convert our modemConfig enum into wf, sf, etc... * diff --git a/src/mesh/SX128xInterface.h b/src/mesh/SX128xInterface.h index 1205087b719..27af873b7b6 100644 --- a/src/mesh/SX128xInterface.h +++ b/src/mesh/SX128xInterface.h @@ -22,6 +22,11 @@ template 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. diff --git a/test/test_radio/test_main.cpp b/test/test_radio/test_main.cpp index df244216411..c3acc5f4408 100644 --- a/test/test_radio/test_main.cpp +++ b/test/test_radio/test_main.cpp @@ -1,5 +1,6 @@ #include "MeshRadio.h" #include "MeshService.h" +#include "NodeDB.h" #include "RadioInterface.h" #include "TestUtil.h" #include @@ -13,10 +14,28 @@ 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::LR11X0; + + bool supportsSubGhz() override { return 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(); } @@ -26,6 +45,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)); @@ -129,12 +150,168 @@ 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() +{ + const uint16_t validBandwidths[] = {31, 42, 62, 83, 101, 125, 200, 250, 400, 500, 800, 1000}; + testRadio->bandwidthProfile = TestableRadioInterface::BandwidthProfile::LR20X0; + + for (auto bandwidth : validBandwidths) { + auto valid = makeCustomBandwidth(meshtastic_Config_LoRaConfig_RegionCode_LORA_24, bandwidth); + TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(valid, testRadio)); + } + + auto invalid = makeCustomBandwidth(meshtastic_Config_LoRaConfig_RegionCode_LORA_24, 1600); + TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(invalid, testRadio)); +} + +static void test_validateConfigLora_rejectsUnsupportedRadioBandwidths() +{ + testRadio->bandwidthProfile = TestableRadioInterface::BandwidthProfile::LR11X0; + const uint16_t lr1121Valid[] = {62, 125, 250, 500}; + for (auto bandwidth : lr1121Valid) { + auto cfg = makeCustomBandwidth(meshtastic_Config_LoRaConfig_RegionCode_US, bandwidth); + TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg, testRadio)); + } + + const uint16_t lr1121Invalid[] = {31, 42, 83, 101, 200, 1600}; + for (auto bandwidth : lr1121Invalid) { + auto cfg = makeCustomBandwidth(meshtastic_Config_LoRaConfig_RegionCode_US, bandwidth); + TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(cfg, testRadio)); + } + + testRadio->bandwidthProfile = TestableRadioInterface::BandwidthProfile::LR20X0; + const uint16_t lr2021Valid[] = {31, 42, 62, 83, 101, 125, 200, 250, 400, 500, 800, 1000}; + for (auto bandwidth : lr2021Valid) { + auto cfg = makeCustomBandwidth(meshtastic_Config_LoRaConfig_RegionCode_US, bandwidth); + TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg, testRadio)); + } + + const uint16_t lr2021Aliases[] = {30, 41, 82, 102, 202, 405, 1600}; + for (auto bandwidth : lr2021Aliases) { + auto cfg = makeCustomBandwidth(meshtastic_Config_LoRaConfig_RegionCode_US, bandwidth); + TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(cfg, testRadio)); + } +} + +static void test_clampConfigLora_sx128xUnsetProducesValidBandwidth() +{ + auto cfg = makeCustomBandwidth(meshtastic_Config_LoRaConfig_RegionCode_UNSET, 125); + testRadio->bandwidthProfile = TestableRadioInterface::BandwidthProfile::SX128X; + + RadioInterface::clampConfigLora(cfg, testRadio); + + TEST_ASSERT_EQUAL_UINT16(800, cfg.bandwidth); + TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg, testRadio)); +} + +static void test_reconfigure_can_suppress_speculativeProbeErrors() +{ + config.lora = makeCustomBandwidth(meshtastic_Config_LoRaConfig_RegionCode_LORA_24, 1600); + testRadio->bandwidthProfile = TestableRadioInterface::BandwidthProfile::LR11X0; + error_code = meshtastic_CriticalErrorCode_NONE; + error_address = 0; + + testRadio->setConfigErrorReporting(false); + TEST_ASSERT_TRUE(testRadio->reconfigure()); + testRadio->setConfigErrorReporting(true); + + TEST_ASSERT_EQUAL_UINT16(800, config.lora.bandwidth); + TEST_ASSERT_EQUAL(meshtastic_CriticalErrorCode_NONE, error_code); + TEST_ASSERT_EQUAL_UINT32(0, error_address); + TEST_ASSERT_EQUAL_UINT32(0, mockMeshService->notificationCount); +} + +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() @@ -153,6 +330,30 @@ static void test_applyModemConfig_freshFlashCodingRateNotZero() TEST_ASSERT_FLOAT_WITHIN(0.01f, 250.0f, testRadio->getBw()); } +static void test_applyModemConfig_sx128xUsesSafeBandwidthWithUnsetRegion() +{ + config.lora = meshtastic_Config_LoRaConfig_init_zero; + config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET; + config.lora.use_preset = true; + config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; + testRadio->bandwidthProfile = TestableRadioInterface::BandwidthProfile::SX128X; + + testRadio->reconfigure(); + + TEST_ASSERT_FLOAT_WITHIN(0.01f, 812.5f, testRadio->getBw()); +} + +static void test_applyModemConfig_sx128xRepairsCustomBandwidthWithUnsetRegion() +{ + config.lora = makeCustomBandwidth(meshtastic_Config_LoRaConfig_RegionCode_UNSET, 125); + testRadio->bandwidthProfile = TestableRadioInterface::BandwidthProfile::SX128X; + + testRadio->reconfigure(); + + TEST_ASSERT_EQUAL_UINT16(800, config.lora.bandwidth); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 812.5f, testRadio->getBw()); +} + // When coding_rate matches the preset exactly, should still use the preset value static void test_applyModemConfig_codingRateMatchesPreset() { @@ -368,7 +569,20 @@ 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_validateConfigLora_rejectsUnsupportedRadioBandwidths); + RUN_TEST(test_clampConfigLora_sx128xUnsetProducesValidBandwidth); + RUN_TEST(test_reconfigure_can_suppress_speculativeProbeErrors); + RUN_TEST(test_clampConfigLora_repairsLr1121TurboPreset); + RUN_TEST(test_clampConfigLora_repairsLr2021TurboPreset); + RUN_TEST(test_validateConfigLora_preservesSubGhzBandwidth); RUN_TEST(test_applyModemConfig_freshFlashCodingRateNotZero); + RUN_TEST(test_applyModemConfig_sx128xUsesSafeBandwidthWithUnsetRegion); + RUN_TEST(test_applyModemConfig_sx128xRepairsCustomBandwidthWithUnsetRegion); RUN_TEST(test_applyModemConfig_codingRateMatchesPreset); RUN_TEST(test_applyModemConfig_customCodingRateHigherThanPreset); RUN_TEST(test_applyModemConfig_customCodingRateLowerThanPreset);