Files
MeshCore/test/test_lr_meshtext_wire.py
T
mikecarper a87b2a33a3 Fix LR2021 TempRadio with incompatible extra-SF detectors
Temporarily disable incompatible cached side detectors while applying a temporary primary tuple, preserving their saved configuration for normal restoration. Use primary-only CAD while those detectors are suspended. Keep compatible temporary detectors and real hardware errors intact.

Report pending TempRadio starts instead of off, preserve live-tuple status during replacements and future schedules, and include the two-second delay in the accepted reply. Share primary validation to retain STM32 Mini flash capacity without dropping features.

Validated production CLI status, scheduler and persistence cases; LR2021 detector suspension, restoration, CAD and hardware-error paths; reply handoff, room/sensor recovery, Morse and OTA automation regressions. W12 repeater, room-server and sensor builds pass. Canonical STM32 Mini Repeater passes flash and runtime RAM admission with 436 bytes of flash remaining. Physical W12 RF testing remains unverified.
2026-10-26 00:58:57 -07:00

291 lines
13 KiB
Python

"""Exercise production LR11xx/LR2021 Meshtext packet and restoration methods."""
from pathlib import Path
import os
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
def method(source, signature):
start = source.index(signature)
end = source.index("{", start) + 1
depth = 1
while depth:
depth += (source[end] == "{") - (source[end] == "}")
end += 1
return source[start:end]
COMMON = r'''
#include <cassert>
#include <cstdint>
#include <cstdio>
#include <initializer_list>
#include <RadioProfiles.h>
#include <helpers/radiolib/LR1110ProfileSwitchState.h>
#define MESH_ENABLE_TX_BEACONS 1
#define MESH_ENABLE_MESHTEXT_WIRE 1
#define RADIOLIB_ERR_NONE 0
#define RADIOLIB_LR2021_LORA_SYNC_WORD_PRIVATE 0x12
struct Wire {
unsigned calls = 0, fail = 0;
uint8_t sync = 0x12;
uint16_t preamble = 16;
bool crc = false, explicit_header = false, inverted = true;
int16_t next() { return ++calls == fail ? -705 : 0; }
int16_t setSyncWord(uint8_t value) { sync = value; return next(); }
int16_t setCRC(uint8_t value) { assert(value == 2); crc = value > 0; return next(); }
int16_t explicitHeader() { explicit_header = true; return next(); }
int16_t invertIQ(bool value) { inverted = value; return next(); }
};
'''
class LRMeshtextWireTest(unittest.TestCase):
def run_cpp(self, code):
with tempfile.TemporaryDirectory(prefix="meshcore-lr-meshtext-") as folder:
source, binary = Path(folder) / "test.cpp", Path(folder) / "test"
source.write_text(code, encoding="ascii")
result = subprocess.run(
["g++", "-std=c++17", "-Wall", "-Wextra", "-Werror", "-I", str(ROOT / "src"),
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
str(source), "-o", str(binary)], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
env = dict(os.environ, ASAN_OPTIONS="detect_leaks=1:halt_on_error=1",
UBSAN_OPTIONS="halt_on_error=1:print_stacktrace=1")
result = subprocess.run([str(binary)], capture_output=True, text=True, env=env)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
return result.stdout
def test_actual_three_lr_families_protocol_errors_and_restore(self):
for family in ("LR1110", "LR1121", "LR2021"):
with self.subTest(family=family):
driver = (ROOT / f"src/helpers/radiolib/Custom{family}.h").read_text()
wrapper = (ROOT / f"src/helpers/radiolib/Custom{family}Wrapper.h").read_text()
protocol = method(driver, "int16_t setProfileProtocol(")
apply = method(wrapper, "bool applyProtocol(").replace(" override", "")
support = method(wrapper, "bool supportsMeshtext()").replace(" override", "")
code = COMMON + r'''
struct @FAMILY@ : Wire {};
struct Custom@FAMILY@ : @FAMILY@ {
LR1110ProfileSwitchState _profileSwitch{true};
bool _profilePacketDirty = true;
@PROTOCOL@
};
struct Wrapper {
Custom@FAMILY@ chip;
Custom@FAMILY@* _radio = &chip;
@SUPPORT@
@APPLY@
};
int main() {
using Protocol = mesh::RadioProfileProtocol;
Wrapper invalid;
assert(!invalid.applyProtocol(static_cast<Protocol>(255)) && invalid.chip.calls == 0);
unsigned scenarios = 1;
for (Protocol protocol : {Protocol::MeshCore, Protocol::Meshtext}) {
Wrapper wrapper;
assert(wrapper.supportsMeshtext());
wrapper.chip._profileSwitch.rxValid = wrapper.chip._profileSwitch.active = true;
wrapper.chip._profileSwitch.modulationValid = wrapper.chip._profileSwitch.standbyReady = true;
wrapper.chip._profileSwitch.open = true;
assert(wrapper.applyProtocol(protocol));
assert(wrapper.chip.sync == (protocol == Protocol::Meshtext ? 0x2b : 0x12));
assert(wrapper.chip.crc && wrapper.chip.explicit_header && !wrapper.chip.inverted);
assert(wrapper.chip.preamble == 16 && wrapper.chip.calls == 4);
@FAST_SUCCESS@
wrapper.chip.preamble = 32;
assert(wrapper.applyProtocol(Protocol::MeshCore));
assert(wrapper.chip.sync == 0x12 && wrapper.chip.crc && wrapper.chip.explicit_header);
assert(!wrapper.chip.inverted && wrapper.chip.preamble == 32);
++scenarios;
}
for (unsigned failure = 1; failure <= 4; ++failure) {
Wrapper wrapper;
wrapper.chip._profileSwitch.rxValid = wrapper.chip._profileSwitch.active = true;
wrapper.chip._profileSwitch.modulationValid = wrapper.chip._profileSwitch.standbyReady = true;
wrapper.chip._profileSwitch.open = true;
wrapper.chip.fail = failure;
assert(!wrapper.applyProtocol(Protocol::Meshtext));
assert(wrapper.chip.calls == failure); // no later setters after an error
@FAST_FAILURE@
wrapper.chip.fail = 0;
assert(wrapper.applyProtocol(Protocol::MeshCore));
assert(wrapper.chip.sync == 0x12 && wrapper.chip.crc && wrapper.chip.explicit_header);
assert(!wrapper.chip.inverted);
++scenarios;
}
std::printf("PASS: @FAMILY@ %u actual protocol cases\n", scenarios);
}
'''
fast = "assert(!wrapper.chip._profileSwitch.rxValid && !wrapper.chip._profileSwitch.modulationValid);\n"
fast += "assert(!wrapper.chip._profileSwitch.canResumeFast() && !wrapper.chip._profilePacketDirty);"
code = (code.replace("@FAMILY@", family).replace("@PROTOCOL@", protocol)
.replace("@APPLY@", apply).replace("@SUPPORT@", support)
.replace("@FAST_SUCCESS@", fast if family == "LR1110" else "")
.replace("@FAST_FAILURE@", fast if family == "LR1110" else ""))
self.assertIn("7 actual protocol cases", self.run_cpp(code))
def test_lr2021_transient_side_detectors_and_physical_power_band(self):
source = (ROOT / "src/helpers/radiolib/CustomLR2021Wrapper.h").read_text()
build = method(source, "static void buildSideDetectorConfig(")
side = method(source, "bool applySideDetectorConfig(")
scan = method(source, "int16_t performChannelScan()").replace(" override", "")
power = method(source, "int16_t applyCachedTxPower(").replace(" override", "")
code = COMMON + r'''
#define USE_LR2021 1
#define LORA_BW 125
#define RADIOLIB_ERR_UNKNOWN -1
#define RADIOLIB_CHANNEL_FREE -2
#define MESH_DEBUG_PRINTLN(...) ((void)0)
#include <helpers/radiolib/LR2021SideDetectorConfig.h>
#include <helpers/radiolib/RadioPowerLimits.h>
struct LR2021LoRaSideDetector_t {
uint8_t sf = 0;
bool ldro = false, invertIQ = true;
uint8_t syncWord = 0;
};
struct CustomLR2021 {
unsigned calls = 0;
size_t count = 0;
bool fail = false;
float frequency = 906;
float bandwidth = 125;
uint8_t sf = 7;
unsigned sf_changes = 0;
int8_t power = 0;
LR2021LoRaSideDetector_t values[3];
int16_t setSideDetector(const LR2021LoRaSideDetector_t* config, size_t size) {
++calls; count = size;
if (size) for (size_t i = 0; i < size; ++i) values[i] = config[i];
return fail ? -705 : 0;
}
float getFreqMHz() const { return frequency; }
int16_t setOutputPower(int8_t value) { power = value; return 0; }
float getBandwidthKhz() const { return bandwidth; }
int16_t standby() { return 0; }
int16_t setSpreadingFactor(uint8_t value) { sf = value; ++sf_changes; return 0; }
};
struct RadioLibWrapper {
unsigned primary_scans = 0;
int16_t performChannelScan() { ++primary_scans; return RADIOLIB_CHANNEL_FREE; }
};
struct Board {
unsigned watchdog_services = 0;
void serviceWatchdog() { ++watchdog_services; }
};
struct Wrapper : RadioLibWrapper {
CustomLR2021 chip;
CustomLR2021* _radio = &chip;
Board board;
Board* _board = &board;
mesh::RadioProfiles _profiles;
bool beacon_owned = false, _beacon_restoring = false;
bool _params_valid = true;
float _cur_bw = 125;
uint8_t _sideDetSFs[3] = {8, 9, 10};
size_t _numSideDet = 3;
uint8_t scanned_sfs[4] = {};
unsigned multi_scans = 0;
uint8_t getSpreadingFactor() const { return chip.sf; }
int16_t performChannelScanWithTimeout(uint32_t timeout) {
assert(timeout > 0 && multi_scans < 4);
scanned_sfs[multi_scans++] = chip.sf;
return RADIOLIB_CHANNEL_FREE;
}
uint32_t cadScanTimeoutMillis(uint8_t sf, float bw) {
assert(sf >= 5 && sf <= 12 && bw > 0);
return 100;
}
bool restoreAfterDeepInit() { assert(false); return false; }
bool isBeaconRadioOwned() const { return beacon_owned; }
@BUILD@
@SIDE@
@SCAN@
@POWER@
};
int main() {
Wrapper wrapper;
// Valid normal RX set: SF7, 125 kHz with SF8/9/10 side detectors.
assert(wrapper.applySideDetectorConfig(7, 125) && wrapper.chip.count == 3);
// Same saved set is illegal at LongFast/SF11; disable, not reject or mutate.
wrapper.beacon_owned = true;
assert(wrapper.applySideDetectorConfig(11, 250) && wrapper.chip.count == 0);
assert(wrapper._numSideDet == 3 && wrapper._sideDetSFs[0] == 8);
// Cleanup repairs normal RX before relinquishing exclusive ownership.
wrapper._beacon_restoring = true;
assert(wrapper.applySideDetectorConfig(7, 125) && wrapper.chip.count == 3);
for (size_t i = 0; i < 3; ++i) {
assert(wrapper.chip.values[i].sf == 8 + i);
assert(!wrapper.chip.values[i].invertIQ && wrapper.chip.values[i].syncWord == 0x12);
}
wrapper._beacon_restoring = false;
wrapper.chip.fail = true;
assert(!wrapper.applySideDetectorConfig(11, 250));
wrapper.chip.fail = false;
wrapper._profiles.secondary.mode = mesh::RadioProfileMode::RxTx;
wrapper._beacon_restoring = true;
assert(wrapper.applySideDetectorConfig(7, 125) && wrapper.chip.count == 0);
// An off-channel TX keeps the wrapper's normal RX cache. Clamp using the
// driver's actual band, matching the PA chosen by its setOutputPower().
wrapper.chip.frequency = 2440;
assert(wrapper.applyCachedTxPower(22) == 0 && wrapper.chip.power == 12);
wrapper.chip.frequency = 906;
assert(wrapper.applyCachedTxPower(22) == 0 && wrapper.chip.power == 22);
// Temporary SF11 cannot use the saved SF8/9/10 list. Keep the saved list,
// disable its hardware detectors, and scan only SF11 while it is suspended.
Wrapper temporary;
temporary._profiles.primary_temporary = true;
temporary.chip.sf = 11;
assert(temporary.applySideDetectorConfig(11, 250) && temporary.chip.count == 0);
assert(temporary._numSideDet == 3 && temporary._sideDetSFs[0] == 8);
assert(temporary.performChannelScan() == RADIOLIB_CHANNEL_FREE);
assert(temporary.primary_scans == 1 && temporary.multi_scans == 0);
assert(temporary.chip.sf == 11 && temporary.chip.sf_changes == 0);
// A failed disable must remain a real apply failure, even during TempRadio.
temporary.chip.fail = true;
assert(!temporary.applySideDetectorConfig(11, 250));
assert(temporary._numSideDet == 3 && temporary._sideDetSFs[2] == 10);
temporary.chip.fail = false;
temporary._profiles.primary_temporary = false;
assert(!temporary.applySideDetectorConfig(11, 250)); // permanent validation remains strict
// Expiry or cancellation restores the complete saved detector list and CAD.
temporary.chip.sf = 7;
assert(temporary.applySideDetectorConfig(7, 125) && temporary.chip.count == 3);
assert(temporary.performChannelScan() == RADIOLIB_CHANNEL_FREE);
assert(temporary.multi_scans == 4 && temporary.chip.sf == 7);
for (unsigned i = 0; i < 4; ++i) assert(temporary.scanned_sfs[i] == 7 + i);
// The reported SF7/62.5 tuple also conflicts with SF12 from a valid saved
// SF9 setup: its detector gap exceeds four. Resume all extras on normal SF9.
Wrapper lower;
lower._sideDetSFs[0] = 10; lower._sideDetSFs[1] = 11; lower._sideDetSFs[2] = 12;
lower._profiles.primary_temporary = true;
lower._cur_bw = 62.5;
assert(lower.applySideDetectorConfig(7, 62.5) && lower.chip.count == 0);
assert(lower.performChannelScan() == RADIOLIB_CHANNEL_FREE && lower.primary_scans == 1);
lower._profiles.primary_temporary = false;
lower.chip.sf = 9;
assert(lower.applySideDetectorConfig(9, 62.5) && lower.chip.count == 3);
assert(lower._numSideDet == 3 && lower._sideDetSFs[2] == 12);
// A compatible temporary tuple keeps multi-SF reception and CAD available.
Wrapper compatible;
compatible._profiles.primary_temporary = true;
assert(compatible.applySideDetectorConfig(7, 125) && compatible.chip.count == 3);
assert(compatible.performChannelScan() == RADIOLIB_CHANNEL_FREE);
assert(compatible.primary_scans == 0 && compatible.multi_scans == 4);
std::puts("PASS: LR2021 independent side detectors and physical power band");
}
'''
code = (code.replace("@BUILD@", build).replace("@SIDE@", side)
.replace("@SCAN@", scan).replace("@POWER@", power))
self.assertIn("PASS:", self.run_cpp(code))
if __name__ == "__main__":
unittest.main()