Fix LR2021 side detectors and W12 shutdown

This commit is contained in:
mikecarper
2026-08-05 17:33:20 -07:00
parent 24c580f90a
commit 6ddbaf3274
10 changed files with 353 additions and 49 deletions
+27 -1
View File
@@ -4,6 +4,9 @@
#include <helpers/CLICommandUtils.h>
#include <helpers/ClockSyncUtils.h>
#include <helpers/FloodFilterPolicy.h>
#if defined(USE_LR2021)
#include <helpers/radiolib/LR2021SideDetectorConfig.h>
#endif
#include <helpers/radiolib/RXPowerSaving.h>
#include <helpers/RxReservePacketManager.h>
#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() {
+31 -15
View File
@@ -1,6 +1,7 @@
#include <Arduino.h>
#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]);
+2 -2
View File
@@ -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;
+33
View File
@@ -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();
+97 -29
View File
@@ -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<float>(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;
};
@@ -0,0 +1,89 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#include <string.h>
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<uint16_t>(value * 10U + static_cast<uint8_t>(*cursor - '0'));
if (value > 12U) return false;
cursor++;
digits++;
}
if (digits == 0 || value < 5U) return false;
side_sfs[num++] = static_cast<uint8_t>(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<uint32_t>(((sf - 5U) >> 1U) << 1U);
}
return static_cast<float>(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<float>(uint32_t(1) << sf) / bandwidth_khz >= 16.0f;
}
} // namespace lr2021
} // namespace mesh
+4 -1
View File
@@ -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) {
+1
View File
@@ -88,6 +88,7 @@ protected:
const float bw = _params_valid ? _cur_bw : static_cast<float>(LORA_BW);
return mesh::calculateCadScanTimeoutMillis(sf, bw);
}
int16_t performChannelScanWithTimeout(unsigned long timeout_ms);
virtual int startReceiveMode();
virtual void stopReceiveDutyCycle();
virtual bool isPacketReady();
+59
View File
@@ -1,6 +1,7 @@
#include <gtest/gtest.h>
#include <helpers/radiolib/CadTiming.h>
#include <helpers/radiolib/LR2021SideDetectorConfig.h>
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();
+10 -1
View File
@@ -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() {