Add two-profile LoRa operation and radio CLI regression coverage

Implement radio2, tempradio2, radioat2 and tempradioat2 across Mesh roles,
with RX-only/RX+TX operation, optional preambles and persistent cross-TX
policy. Keep temporary OTA traffic on its profile by default and maintain
independent retry ownership and backoff for each profile.

Use slow-first receive scanning with 4.8-symbol visits and automatic
preambles rounded up in steps of eight. Preserve pending RX, restore power
saving on exit, and discard work bound to changed or expired profiles.

Restore the infrastructure path.hash.mode setter and report unsupported
extra.sf settings consistently. Add CLI, scheduling, scan and retry tests,
setup documentation, and the V4/XIAO hardware validation results.

Validation: 1,422 native tests, eight KISS tests, 63 final focused profile
tests, 18 checks from the staged source, sanitizer-enabled OTA transfers,
and builds for V4 Mesh roles, Full XIAO Companion and nRF52 T1000-E.
Hardware checks cover reception, cross-TX policy, expiry, reboot and OTA
discovery while receiving main-channel adverts.
This commit is contained in:
mikecarper
2026-09-13 08:25:00 -07:00
parent b87e86e10f
commit 3b6c894ee2
46 changed files with 5187 additions and 143 deletions
+9 -6
View File
@@ -1067,8 +1067,9 @@ bool SensorMesh::applySavedRadioParams() {
uint32_t timings[2] = {_prefs.rx_ps_rx_us, _prefs.rx_ps_sleep_us};
const uint32_t* applied_timings = _prefs.rx_powersaving_enabled
&& radio_driver.supportsRxPowerSaving() ? timings : NULL;
if (!radio_driver.setParams(
_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr, applied_timings)) {
if (_cli.radioProfiles().applyPrimary(
_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr, false,
_cli.radioProfiles().primaryPreamble(), applied_timings) != mesh::RadioParamApplyResult::APPLIED) {
return false;
}
active_cr = _prefs.cr;
@@ -1101,7 +1102,8 @@ void SensorMesh::saveIdentity(const mesh::LocalIdentity& new_id) {
store.save("_main", new_id);
}
void SensorMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) {
void SensorMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins, uint16_t preamble) {
pending_preamble = preamble;
radio_apply_retry_at = 0;
radio_apply_failures = 0;
set_radio_at = futureMillis(2000); // give CLI reply some time to be sent back, before applying temp radio params
@@ -1264,8 +1266,8 @@ void SensorMesh::loop() {
uint32_t timings[2] = {rx_us, sleep_us};
const uint32_t* applied_timings = _prefs.rx_powersaving_enabled
&& radio_driver.supportsRxPowerSaving() ? timings : NULL;
if (timing_ok && radio_driver.setParams(
pending_freq, pending_bw, pending_sf, pending_cr, applied_timings)) {
if (timing_ok && _cli.radioProfiles().applyPrimary(
pending_freq, pending_bw, pending_sf, pending_cr, true, pending_preamble, applied_timings) == mesh::RadioParamApplyResult::APPLIED) {
set_radio_at = 0;
active_cr = pending_cr;
temp_radio_applied = true;
@@ -1362,7 +1364,7 @@ void SensorMesh::loop() {
}
}
#if defined(ENABLE_OTA) && OTA_DYNAMIC_CONTEXT
mesh::ota::ota_service_temp_radio_context(isTempRadioActive());
mesh::ota::ota_service_temp_radio_context(isAnyTempRadioActive());
#endif
}
@@ -1382,6 +1384,7 @@ uint32_t SensorMesh::limitSleepToMillisTimer(unsigned long timestamp,
}
bool SensorMesh::hasPendingWork() const {
if (isDualRadioActive()) return true;
if (hasPendingOtaApply()) return true;
if (_cli.hasActiveUserGpioTimer()) return true;
if (radio_driver.isWatchdogObserving()) return true;