From 6ddbaf3274f8ca4ea8b72e15cc06799c374ea74b Mon Sep 17 00:00:00 2001 From: mikecarper Date: Wed, 5 Aug 2026 17:33:20 -0700 Subject: [PATCH] Fix LR2021 side detectors and W12 shutdown --- examples/simple_repeater/MyMesh.cpp | 28 +++- src/helpers/CommonCLI.cpp | 46 ++++--- src/helpers/CommonCLI.h | 4 +- src/helpers/radiolib/CustomLR2021.h | 33 +++++ src/helpers/radiolib/CustomLR2021Wrapper.h | 126 ++++++++++++++---- .../radiolib/LR2021SideDetectorConfig.h | 89 +++++++++++++ src/helpers/radiolib/RadioLibWrappers.cpp | 5 +- src/helpers/radiolib/RadioLibWrappers.h | 1 + test/test_cad_timing/test_cad_timing.cpp | 59 ++++++++ .../meshnology_w12/MeshnologyW12Board.cpp | 11 +- 10 files changed, 353 insertions(+), 49 deletions(-) create mode 100644 src/helpers/radiolib/LR2021SideDetectorConfig.h diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index d552fe8e..b0200517 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -4,6 +4,9 @@ #include #include #include +#if defined(USE_LR2021) +#include +#endif #include #include #ifdef WITH_WEBCONFIG @@ -3418,7 +3421,30 @@ bool MyMesh::applyRadioParams(float freq, float bw, uint8_t sf, uint8_t cr) { } bool MyMesh::applySavedRadioParams() { - return applyRadioParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); +#if defined(USE_LR2021) + uint8_t extra_sf_count = 0; + const bool extra_sf_valid = mesh::lr2021::storedSideDetectorCount( + _prefs.extra_sf, extra_sf_count) + && mesh::lr2021::validateSideDetectorSFs( + _prefs.extra_sf, extra_sf_count, _prefs.sf, _prefs.bw); + if (!extra_sf_valid) { + // A permanent radio-profile change can make the old detector list invalid. + // Clear the live/cache state before applying the new primary modulation so + // the radio is not trapped retrying an impossible combination forever. + if (!radio_driver.configSideDetectors(nullptr, 0, _prefs.bw)) return false; + memset(_prefs.extra_sf, 0, sizeof(_prefs.extra_sf)); + extra_sf_count = 0; + savePrefs(); + } +#endif + + if (!applyRadioParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr)) return false; + +#if defined(USE_LR2021) + return radio_driver.configSideDetectors(_prefs.extra_sf, extra_sf_count, _prefs.bw); +#else + return true; +#endif } void MyMesh::queueSavedRadioApply() { diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 5399cdc0..c93a0754 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -1,6 +1,7 @@ #include #include "CommonCLI.h" #include "CLICommandUtils.h" +#include "radiolib/LR2021SideDetectorConfig.h" #include "TxtDataHelpers.h" #include "AdvertDataHelpers.h" #include "AlertReporter.h" // for alertReporterBannedChannelMatch() @@ -872,6 +873,7 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) { // The hardware main-loop watchdog is opt-out. Older preference files do not // contain its appended byte, so they safely inherit the enabled default. _prefs->system_watchdog_enabled = 1; + memset(_prefs->extra_sf, 0, sizeof(_prefs->extra_sf)); #ifdef WITH_MQTT_BRIDGE bool node_prefs_needs_migration = false; @@ -1068,6 +1070,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // Build-profile defaults - overwritten below when the saved field is present. _prefs->radio_fem_rxgain = 1; _prefs->cad_enabled = DEFAULT_CAD_ENABLED; + memset(_prefs->extra_sf, 0, sizeof(_prefs->extra_sf)); _prefs->rx_powersaving_enabled = 0; _prefs->rx_ps_rx_us = RX_POWERSAVING_DEFAULT_RX_US; _prefs->rx_ps_sleep_us = RX_POWERSAVING_DEFAULT_SLEEP_US; @@ -1347,6 +1350,12 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->system_watchdog_enabled, sizeof(_prefs->system_watchdog_enabled)); } + if (file.available() >= (int)sizeof(_prefs->extra_sf)) { + file.read((uint8_t *)_prefs->extra_sf, sizeof(_prefs->extra_sf)); + } else if (file.available() > 0) { + // Never accept a torn append as a partial detector list. + _com_prefs_needs_upgrade = true; + } } // Sanitize non-finite values before constrain(), whose comparisons leave @@ -1383,6 +1392,13 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { _prefs->bw = isValidLoRaBandwidth(_prefs->bw) ? _prefs->bw : defaultLoRaBandwidth(); _prefs->sf = constrain(_prefs->sf, 5, 12); _prefs->cr = constrain(_prefs->cr, 5, 8); + uint8_t extra_sf_count = 0; + if (!mesh::lr2021::storedSideDetectorCount(_prefs->extra_sf, extra_sf_count) + || !mesh::lr2021::validateSideDetectorSFs( + _prefs->extra_sf, extra_sf_count, _prefs->sf, _prefs->bw)) { + memset(_prefs->extra_sf, 0, sizeof(_prefs->extra_sf)); + _com_prefs_needs_upgrade = true; + } _prefs->tx_power_dbm = constrain(_prefs->tx_power_dbm, -9, 30); _prefs->multi_acks = constrain(_prefs->multi_acks, 0, 1); _prefs->adc_multiplier = constrain(_prefs->adc_multiplier, 0.0f, 10.0f); @@ -1608,6 +1624,7 @@ static bool writeCommonPrefsImage(Writer& writer, NodePrefs* prefs) { WRITE_COMMON_PREFS(&prefs->flood_retry_group_max_path); // 853 WRITE_COMMON_PREFS(&prefs->rx_watchdog_enabled); // 854 WRITE_COMMON_PREFS(&prefs->system_watchdog_enabled); // 855 + WRITE_COMMON_PREFS(&prefs->extra_sf); // 856 #undef WRITE_COMMON_PREFS_BYTES #undef WRITE_COMMON_PREFS @@ -1753,7 +1770,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs, bool save_mqtt) { sizeof(_prefs->rx_watchdog_enabled)); // 854 file.write((uint8_t *)&_prefs->system_watchdog_enabled, sizeof(_prefs->system_watchdog_enabled)); // 855 - // next: 856 + file.write((uint8_t *)_prefs->extra_sf, sizeof(_prefs->extra_sf)); // 856 + // next: 860 #if defined(NRF52_PLATFORM) if (!file.commit()) { @@ -4327,22 +4345,20 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep sprintf(reply, "OK - reboot.interval set to %d", _prefs->reboot_interval); } #if defined(USE_LR2021) - } else if (memcmp(config, "extra.sf ", 9) == 0) { - strcpy(tmp, &config[9]); - const char *parts[4]; - uint8_t sideDetSFs[4]; - int num = mesh::Utils::parseTextParts(tmp, parts, 4); - if (num > 3) { - sprintf(reply, "Invalid extra SF config"); + } else if (strcmp(config, "extra.sf") == 0 || memcmp(config, "extra.sf ", 9) == 0) { + uint8_t sideDetSFs[mesh::lr2021::STORED_SIDE_DETECTOR_BYTES] = {}; + uint8_t num = 0; + const char* value = config[8] == '\0' ? "" : &config[9]; + if (strcmp(value, "none") == 0 || strcmp(value, "off") == 0) value = ""; + if (!mesh::lr2021::parseSideDetectorSFList(value, sideDetSFs, num) + || !mesh::lr2021::validateSideDetectorSFs( + sideDetSFs, num, _prefs->sf, _prefs->bw)) { + strcpy(reply, "Invalid extra SF config"); } else { - for (int i = 0; i < num; i++) { - sideDetSFs[i] = atoi(parts[i]); - } - sideDetSFs[num] = 0; if (_callbacks->configSideDetectors(sideDetSFs, num, _prefs->bw)) { - for (int i = 0; i <= num; i++) _prefs->extra_sf[i] = sideDetSFs[i]; + memcpy(_prefs->extra_sf, sideDetSFs, sizeof(_prefs->extra_sf)); savePrefs(); - strcpy(reply, "OK - extra SFs set"); + strcpy(reply, num == 0 ? "OK - extra SFs cleared" : "OK - extra SFs set"); } else { strcpy(reply, "Invalid extra SF config"); } @@ -4770,7 +4786,7 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep } else { sprintf(reply, "> %d", (uint8_t)_prefs->reboot_interval); } - } else if (memcmp(config, "extra.sf", 8) == 0) { + } else if (strcmp(config, "extra.sf") == 0) { char* tmp = reply; for (int i = 0; i < 3 && _prefs->extra_sf[i] != 0; i++) { tmp += sprintf(tmp, "%s%d", (i == 0) ? "" : ",", _prefs->extra_sf[i]); diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 02207519..3745b99d 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -163,8 +163,8 @@ struct NodePrefs { // persisted to file uint8_t path_hash_mode; // which path mode to use when sending uint8_t loop_detect; uint8_t cad_enabled; // hardware Channel Activity Detection before TX (boolean) - // LR2021 side-detector SFs are runtime-only in upstream/dev. Keep the field - // here without shifting the established /com_prefs binary layout. + // LR2021 side-detector SFs are appended at /com_prefs offset 856. Their + // in-memory placement here does not shift the established binary layout. uint8_t extra_sf[4] = {}; uint8_t retry_preset; uint8_t direct_retry_attempts; diff --git a/src/helpers/radiolib/CustomLR2021.h b/src/helpers/radiolib/CustomLR2021.h index 446ff50e..82483825 100644 --- a/src/helpers/radiolib/CustomLR2021.h +++ b/src/helpers/radiolib/CustomLR2021.h @@ -5,6 +5,14 @@ class CustomLR2021 : public LR2021 { bool _rx_boosted = false; + uint32_t _rf_switch_pins[Module::RFSWITCH_MAX_PINS] = {}; + const Module::RfSwitchMode_t* _rf_switch_table = nullptr; + + void restoreRfSwitchTable() { + if (_rf_switch_table != nullptr) { + LR2021::setRfSwitchTable(_rf_switch_pins, _rf_switch_table); + } + } public: CustomLR2021(Module *mod) : LR2021(mod) { irqDioNum = LR2021_IRQ_DIO; } @@ -58,9 +66,33 @@ class CustomLR2021 : public LR2021 { setRxBoostedGainMode(LR2021_RX_BOOSTED_GAIN); #endif + // LR2021::begin() performs a hardware reset. Preserve the board-specific + // DIO/FEM routing across watchdog and SPI-timeout recovery resets. + restoreRfSwitchTable(); + return true; // success } + void setRfSwitchTable(const uint32_t (&pins)[Module::RFSWITCH_MAX_PINS], + const Module::RfSwitchMode_t table[]) { + for (size_t i = 0; i < Module::RFSWITCH_MAX_PINS; i++) { + _rf_switch_pins[i] = pins[i]; + } + _rf_switch_table = table; + LR2021::setRfSwitchTable(pins, table); + } + + int16_t setSideDetector(const LR2021LoRaSideDetector_t* cfg, size_t numDetectors) { + if (numDetectors > 0 && cfg == nullptr) return RADIOLIB_ERR_INVALID_SIDE_DETECT; + if (numDetectors == 0) { + // RadioLib 7.7.1 returns success without issuing this command for a + // zero-length list. The LR2021 command encodes detector count in its + // payload length, so the empty command is the actual disable operation. + return setLoRaSideDetConfig(nullptr, 0); + } + return LR2021::setSideDetector(cfg, numDetectors); + } + float getFreqMHz() const { return freqMHz; } int16_t setRxBoostedGainMode(uint8_t level) { @@ -70,6 +102,7 @@ class CustomLR2021 : public LR2021 { } bool getRxBoostedGainMode() const { return _rx_boosted; } + float getBandwidthKhz() const { return bandwidthKhz; } bool isChipBusy() { uint32_t busy = this->mod->getGpio(); diff --git a/src/helpers/radiolib/CustomLR2021Wrapper.h b/src/helpers/radiolib/CustomLR2021Wrapper.h index f381bc7b..29d67730 100644 --- a/src/helpers/radiolib/CustomLR2021Wrapper.h +++ b/src/helpers/radiolib/CustomLR2021Wrapper.h @@ -1,6 +1,7 @@ #pragma once #include "CustomLR2021.h" +#include "LR2021SideDetectorConfig.h" #include "RadioLibWrappers.h" #ifndef USE_LR2021 @@ -24,7 +25,7 @@ protected: && ((CustomLR2021 *)_radio)->setBandwidth(bw) == RADIOLIB_ERR_NONE && ((CustomLR2021 *)_radio)->setCodingRate(cr) == RADIOLIB_ERR_NONE && updatePreamble(sf) - && applySideDetectorConfig(); + && applySideDetectorConfig(sf, bw); } public: @@ -33,37 +34,36 @@ public: } bool configSideDetectors(const uint8_t* sideDetSFs, uint8_t num, float bw) override { - if (num > 3 || (num > 0 && sideDetSFs == nullptr)) return false; - - LR2021LoRaSideDetector_t tmp[3]; - uint8_t sf = getSpreadingFactor(); - - if (sf >= 10 && num > 1) { return false; } // only 1 side detector allowed when primary SF >= 10 - for (int i = 0; i < num; i++) { - if (sideDetSFs[i] > 12 || sideDetSFs[i] < 5) { return false; } // must be valid SF - if (sideDetSFs[i] <= sf) { return false; } // must be greater than the primary SF - if (sideDetSFs[i] > sf + 4) { return false; } // span must not be > 4 - - tmp[i].sf = sideDetSFs[i]; - float tSym = calcTsym(tmp[i].sf, bw); - if (tSym >= 16.0f) { - tmp[i].ldro = true; - } else { - tmp[i].ldro = false; - } - tmp[i].invertIQ = false; - tmp[i].syncWord = RADIOLIB_LR2021_LORA_SYNC_WORD_PRIVATE; + const uint8_t primary_sf = getSpreadingFactor(); + float active_bw = _params_valid ? _cur_bw : ((CustomLR2021 *)_radio)->getBandwidthKhz(); + if (active_bw <= 0.0f) active_bw = bw; + if (!mesh::lr2021::validateSideDetectorSFs(sideDetSFs, num, primary_sf, active_bw)) { + return false; } + LR2021LoRaSideDetector_t tmp[mesh::lr2021::MAX_SIDE_DETECTORS] = {}; + buildSideDetectorConfig(sideDetSFs, num, active_bw, tmp); + uint8_t resume_rx = beginReconfigure(); if (resume_rx > 1) return false; - int16_t status = ((CustomLR2021 *)_radio)->setSideDetector(tmp, num); + int16_t status = ((CustomLR2021 *)_radio)->setSideDetector(num > 0 ? tmp : nullptr, num); MESH_DEBUG_PRINTLN("setSideDetector() returned %d", status); if (status == RADIOLIB_ERR_NONE) { - for (int i = 0; i < num; i++) { _sideDet[i] = tmp[i]; } + for (uint8_t i = 0; i < mesh::lr2021::MAX_SIDE_DETECTORS; i++) { + _sideDetSFs[i] = i < num ? sideDetSFs[i] : 0; + } _numSideDet = num; + } else { + // Setting side detectors writes the detector bytes and sync words in two + // separate commands. If the second command fails, restore the cached + // configuration so a rejected CLI change cannot leave partial hardware state. + bool restored = applySideDetectorConfig(primary_sf, active_bw); + if (!restored) restored = restoreAfterDeepInit(); + if (!restored) { + MESH_DEBUG_PRINTLN("LR2021: failed to restore side detectors after config error"); + } } endReconfigure(resume_rx); @@ -71,13 +71,81 @@ public: } protected: - bool applySideDetectorConfig() { - return ((CustomLR2021 *)_radio)->setSideDetector(_sideDet, _numSideDet) - == RADIOLIB_ERR_NONE; + static void buildSideDetectorConfig(const uint8_t* sideDetSFs, size_t num, float bw, + LR2021LoRaSideDetector_t* config) { + for (size_t i = 0; i < num; i++) { + config[i].sf = sideDetSFs[i]; + config[i].ldro = mesh::lr2021::sideDetectorLDRO(sideDetSFs[i], bw); + config[i].invertIQ = false; + config[i].syncWord = RADIOLIB_LR2021_LORA_SYNC_WORD_PRIVATE; + } } - float calcTsym(uint8_t sf, float bw) { - return (float)(uint32_t(1) << sf) / bw; + bool applySideDetectorConfig(uint8_t primary_sf, float bw) { + if (!mesh::lr2021::validateSideDetectorSFs(_sideDetSFs, _numSideDet, primary_sf, bw)) { + return false; + } + LR2021LoRaSideDetector_t config[mesh::lr2021::MAX_SIDE_DETECTORS] = {}; + buildSideDetectorConfig(_sideDetSFs, _numSideDet, bw, config); + return ((CustomLR2021 *)_radio)->setSideDetector(_numSideDet > 0 ? config : nullptr, + _numSideDet) == RADIOLIB_ERR_NONE; + } + + int16_t performChannelScan() override { + if (_numSideDet == 0) return RadioLibWrapper::performChannelScan(); + + CustomLR2021* radio = (CustomLR2021 *)_radio; + const uint8_t rx_primary_sf = getSpreadingFactor(); + float bw = _params_valid ? _cur_bw : radio->getBandwidthKhz(); + if (bw <= 0.0f) bw = static_cast(LORA_BW); + + int16_t status = radio->standby(); + if (status != RADIOLIB_ERR_NONE) return status; + + // RadioLib's LR2021 multi-SF CAD support is unfinished and CAD requires + // the inverse primary/side-SF ordering from RX. Scan every configured SF + // through the supported primary detector instead, then restore RX atomically. + status = radio->setSideDetector(nullptr, 0); + if (status != RADIOLIB_ERR_NONE) { + bool restored = applySideDetectorConfig(rx_primary_sf, bw); + if (!restored) restored = restoreAfterDeepInit(); + return restored ? status : RADIOLIB_ERR_UNKNOWN; + } + + uint8_t scan_sfs[mesh::lr2021::STORED_SIDE_DETECTOR_BYTES] = {}; + scan_sfs[0] = rx_primary_sf; + for (size_t i = 0; i < _numSideDet; i++) scan_sfs[i + 1] = _sideDetSFs[i]; + + int16_t scan_result = RADIOLIB_CHANNEL_FREE; + for (size_t i = 0; i <= _numSideDet; i++) { + if (i > 0) { + status = radio->standby(); + if (status != RADIOLIB_ERR_NONE) { + scan_result = status; + break; + } + } + status = radio->setSpreadingFactor(scan_sfs[i]); + if (status != RADIOLIB_ERR_NONE) { + scan_result = status; + break; + } + scan_result = performChannelScanWithTimeout( + mesh::calculateCadScanTimeoutMillis(scan_sfs[i], bw)); + // Each individual CAD can finish inside the helper's one-second service + // cadence while the combined multi-SF pass still exceeds it. + _board->serviceWatchdog(); + if (scan_result != RADIOLIB_CHANNEL_FREE) break; + } + + bool restored = radio->standby() == RADIOLIB_ERR_NONE + && radio->setSpreadingFactor(rx_primary_sf) == RADIOLIB_ERR_NONE + && applySideDetectorConfig(rx_primary_sf, bw); + if (!restored && !restoreAfterDeepInit()) { + MESH_DEBUG_PRINTLN("LR2021: failed to restore RX side detectors after CAD"); + return RADIOLIB_ERR_UNKNOWN; + } + return scan_result; } public: @@ -126,7 +194,7 @@ protected: == RADIOLIB_ERR_NONE; } - LR2021LoRaSideDetector_t _sideDet[3]; + uint8_t _sideDetSFs[mesh::lr2021::MAX_SIDE_DETECTORS] = {}; size_t _numSideDet = 0; }; diff --git a/src/helpers/radiolib/LR2021SideDetectorConfig.h b/src/helpers/radiolib/LR2021SideDetectorConfig.h new file mode 100644 index 00000000..acb30db7 --- /dev/null +++ b/src/helpers/radiolib/LR2021SideDetectorConfig.h @@ -0,0 +1,89 @@ +#pragma once + +#include +#include +#include + +namespace mesh { +namespace lr2021 { + +static constexpr uint8_t MAX_SIDE_DETECTORS = 3; +static constexpr uint8_t STORED_SIDE_DETECTOR_BYTES = MAX_SIDE_DETECTORS + 1; + +// Parse the CLI's comma-separated SF list without copying it into CommonCLI's +// shared scratch buffer. An empty string deliberately means "disable". +inline bool parseSideDetectorSFList(const char* text, + uint8_t (&side_sfs)[STORED_SIDE_DETECTOR_BYTES], + uint8_t& num) { + memset(side_sfs, 0, sizeof(side_sfs)); + num = 0; + if (text == nullptr) return false; + if (*text == '\0') return true; + + const char* cursor = text; + while (*cursor != '\0') { + if (num >= MAX_SIDE_DETECTORS) return false; + + uint16_t value = 0; + uint8_t digits = 0; + while (*cursor >= '0' && *cursor <= '9') { + value = static_cast(value * 10U + static_cast(*cursor - '0')); + if (value > 12U) return false; + cursor++; + digits++; + } + if (digits == 0 || value < 5U) return false; + side_sfs[num++] = static_cast(value); + + if (*cursor == '\0') break; + if (*cursor != ',') return false; + cursor++; + if (*cursor == '\0') return false; // reject a trailing comma + } + + side_sfs[num] = 0; + return true; +} + +inline bool storedSideDetectorCount( + const uint8_t (&stored)[STORED_SIDE_DETECTOR_BYTES], uint8_t& num) { + for (uint8_t i = 0; i < STORED_SIDE_DETECTOR_BYTES; i++) { + if (stored[i] == 0) { + num = i; + return i <= MAX_SIDE_DETECTORS; + } + } + num = 0; + return false; +} + +inline bool validateSideDetectorSFs(const uint8_t* side_sfs, uint8_t num, + uint8_t primary_sf, float bandwidth_khz) { + if (num > MAX_SIDE_DETECTORS || (num > 0 && side_sfs == nullptr)) return false; + if (num == 0) return true; + if (primary_sf < 5 || primary_sf > 12 || bandwidth_khz <= 0.0f) return false; + + // These limits mirror LR2021::setSideDetector(). A primary SF of 10 or + // greater permits two side detectors, not one. + if ((primary_sf >= 10 || bandwidth_khz > 500.0f) && num > 2) return false; + + uint32_t detector_factor_sum = 0; + for (uint8_t i = 0; i < num; i++) { + const uint8_t sf = side_sfs[i]; + if (sf < 5 || sf > 12 || sf <= primary_sf || sf > primary_sf + 4) return false; + for (uint8_t j = 0; j < i; j++) { + if (side_sfs[j] == sf) return false; + } + detector_factor_sum += 10U + static_cast(((sf - 5U) >> 1U) << 1U); + } + + return static_cast(detector_factor_sum) * bandwidth_khz < 32000.0f; +} + +inline bool sideDetectorLDRO(uint8_t sf, float bandwidth_khz) { + if (sf < 5 || sf > 12 || bandwidth_khz <= 0.0f) return false; + return static_cast(uint32_t(1) << sf) / bandwidth_khz >= 16.0f; +} + +} // namespace lr2021 +} // namespace mesh diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 2fbf25b9..af56c2d0 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -675,13 +675,16 @@ void RadioLibWrapper::onSendFinished() { } int16_t RadioLibWrapper::performChannelScan() { + return performChannelScanWithTimeout(cadScanTimeoutMillis()); +} + +int16_t RadioLibWrapper::performChannelScanWithTimeout(unsigned long timeout_ms) { // RadioLib's blocking scanChannel() waits forever if the radio never raises // its CAD-done IRQ. Keep CAD bounded so a missing IRQ cannot starve the main // loop until the MCU watchdog resets the node. int16_t result = _radio->startChannelScan(); if (result != RADIOLIB_ERR_NONE) return result; - const unsigned long timeout_ms = cadScanTimeoutMillis(); const unsigned long started = millis(); unsigned long last_watchdog_service = started; while (millis() - started < timeout_ms) { diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 7869ad9d..8074749e 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -88,6 +88,7 @@ protected: const float bw = _params_valid ? _cur_bw : static_cast(LORA_BW); return mesh::calculateCadScanTimeoutMillis(sf, bw); } + int16_t performChannelScanWithTimeout(unsigned long timeout_ms); virtual int startReceiveMode(); virtual void stopReceiveDutyCycle(); virtual bool isPacketReady(); diff --git a/test/test_cad_timing/test_cad_timing.cpp b/test/test_cad_timing/test_cad_timing.cpp index e38ab598..46237033 100644 --- a/test/test_cad_timing/test_cad_timing.cpp +++ b/test/test_cad_timing/test_cad_timing.cpp @@ -1,6 +1,7 @@ #include #include +#include TEST(CadTiming, UsesShortDeadlineForCascadeProfile) { EXPECT_EQ(mesh::calculateCadScanTimeoutMillis(7, 62.5f), 100UL); @@ -17,6 +18,64 @@ TEST(CadTiming, BoundsInvalidAndExtremeInputs) { EXPECT_EQ(mesh::calculateCadScanTimeoutMillis(12, 1.0f), 3500UL); } +TEST(LR2021SideDetectors, ParsesStrictBoundedListsAndDisable) { + uint8_t side_sfs[mesh::lr2021::STORED_SIDE_DETECTOR_BYTES] = {99, 99, 99, 99}; + uint8_t num = 99; + + EXPECT_TRUE(mesh::lr2021::parseSideDetectorSFList("", side_sfs, num)); + EXPECT_EQ(0, num); + EXPECT_EQ(0, side_sfs[0]); + EXPECT_TRUE(mesh::lr2021::validateSideDetectorSFs(nullptr, 0, 0, 0.0f)); + EXPECT_FALSE(mesh::lr2021::validateSideDetectorSFs(nullptr, 1, 8, 62.5f)); + + EXPECT_TRUE(mesh::lr2021::parseSideDetectorSFList("9,10,12", side_sfs, num)); + EXPECT_EQ(3, num); + EXPECT_EQ(9, side_sfs[0]); + EXPECT_EQ(10, side_sfs[1]); + EXPECT_EQ(12, side_sfs[2]); + EXPECT_EQ(0, side_sfs[3]); + + EXPECT_FALSE(mesh::lr2021::parseSideDetectorSFList("9,10,11,12", side_sfs, num)); + EXPECT_FALSE(mesh::lr2021::parseSideDetectorSFList("9,", side_sfs, num)); + EXPECT_FALSE(mesh::lr2021::parseSideDetectorSFList("9junk", side_sfs, num)); + EXPECT_FALSE(mesh::lr2021::parseSideDetectorSFList( + "999999999999999999999999999999999999999999999999999999", side_sfs, num)); +} + +TEST(LR2021SideDetectors, AcceptsTwoDetectorsAtPrimarySf10) { + const uint8_t valid[] = {11, 12}; + EXPECT_TRUE(mesh::lr2021::validateSideDetectorSFs(valid, 2, 10, 62.5f)); + + const uint8_t too_many[] = {10, 11, 12}; + EXPECT_FALSE(mesh::lr2021::validateSideDetectorSFs(too_many, 3, 9, 812.5f)); +} + +TEST(LR2021SideDetectors, RejectsInvalidRxRelationships) { + const uint8_t duplicate[] = {9, 9}; + EXPECT_FALSE(mesh::lr2021::validateSideDetectorSFs(duplicate, 2, 8, 62.5f)); + + const uint8_t below_primary[] = {8}; + EXPECT_FALSE(mesh::lr2021::validateSideDetectorSFs(below_primary, 1, 8, 62.5f)); + + const uint8_t excessive_span[] = {12}; + EXPECT_FALSE(mesh::lr2021::validateSideDetectorSFs(excessive_span, 1, 7, 62.5f)); +} + +TEST(LR2021SideDetectors, RecomputesLdroWhenBandwidthChanges) { + EXPECT_TRUE(mesh::lr2021::sideDetectorLDRO(10, 62.5f)); + EXPECT_FALSE(mesh::lr2021::sideDetectorLDRO(10, 125.0f)); +} + +TEST(LR2021SideDetectors, RequiresStoredTerminator) { + uint8_t count = 0; + const uint8_t valid[] = {9, 10, 11, 0}; + EXPECT_TRUE(mesh::lr2021::storedSideDetectorCount(valid, count)); + EXPECT_EQ(3, count); + + const uint8_t torn[] = {9, 10, 11, 12}; + EXPECT_FALSE(mesh::lr2021::storedSideDetectorCount(torn, count)); +} + int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); diff --git a/variants/meshnology_w12/MeshnologyW12Board.cpp b/variants/meshnology_w12/MeshnologyW12Board.cpp index 7e9ae32c..e63ba5e1 100644 --- a/variants/meshnology_w12/MeshnologyW12Board.cpp +++ b/variants/meshnology_w12/MeshnologyW12Board.cpp @@ -3,6 +3,8 @@ void MeshnologyW12Board::begin() { ESP32Board::begin(); + rtc_gpio_hold_dis((gpio_num_t)P_LORA_LF_PA_POWER); + rtc_gpio_hold_dis((gpio_num_t)P_LORA_HF_PA_POWER); pinMode(PIN_ADC_CTRL, OUTPUT); digitalWrite(PIN_ADC_CTRL, LOW); // Disable battery sense until required @@ -59,7 +61,14 @@ void MeshnologyW12Board::begin() { } void MeshnologyW12Board::powerOff() { - enterDeepSleep(0); + // A shutdown must not retain the packet-wake deep-sleep behavior above. + // Disable both external FEM rails, then use the base shutdown path so the + // display, GPS, radio, serial buffers, and stale wake sources are handled. + digitalWrite(P_LORA_LF_PA_POWER, LOW); + digitalWrite(P_LORA_HF_PA_POWER, LOW); + rtc_gpio_hold_en((gpio_num_t)P_LORA_LF_PA_POWER); + rtc_gpio_hold_en((gpio_num_t)P_LORA_HF_PA_POWER); + ESP32Board::powerOff(); } uint16_t MeshnologyW12Board::getBattMilliVolts() {