Apply busy radio settings and recover service starts automatically

Save valid busy RX, fast-switch and Companion tuple changes once, then
apply the latest accepted intent at a safe radio gap. Preserve checked-save
rollback, runtime RXEN intent, TempRadio ownership and exact remote reply
delivery. Repair failed preamble compensation without hiding uncertain saves.

Retry WebConfig, MQTT and NimBLE startup failures automatically. Preserve
explicit off choices, OTA ownership and the latest WiFi credentials through
cooperative shutdown. Add regression coverage and update command guides.

Validation: focused host transaction, rollback, ownership and lifecycle suites;
Full RAK4631, Heltec V4 R8 TFT and RC32 packages; representative room, sensor,
LR1110, LR2021, SX1276, STM32WL and ESP-NOW builds. Linked RAK crypto and OTA
workspace checks pass; its complete OTA image uses 712644/712704 bytes.
This commit is contained in:
mikecarper committed 2026-10-25 15:20:55 -07:00
1 parent fb73e3afac
commit 73c5f572eb
89 files changed
+2978 -816

No files matched your search

+127 -64
View File
@@ -1,5 +1,8 @@
#include "SensorMesh.h"
#include <helpers/ui/DisplayNightLocation.h>
#if defined(USE_LR2021)
#include <helpers/radiolib/LR2021SideDetectorConfig.h>
#endif
#include <helpers/CLICommandUtils.h>
#include <helpers/ClientACLCLI.h>
#include <helpers/ClientACLResponse.h>
@@ -1112,11 +1115,80 @@ bool SensorMesh::handleIncomingMsg(ClientInfo& from, uint32_t timestamp, uint8_t
return false;
}
void SensorMesh::servicePendingPrimaryRadioSettings(bool tx_gap) {
const bool revert_radio_due = revert_radio_at && millisHasNowPassed(revert_radio_at);
const bool radio_apply_ready = !radio_apply_retry_at || millisHasNowPassed(radio_apply_retry_at);
bool radio_apply_failed = false;
if (revert_radio_due && !temp_radio_applied) {
// Never apply a temporary channel after its window has already ended.
set_radio_at = revert_radio_at = 0;
radio_apply_retry_at = 0;
radio_apply_failures = 0;
MESH_DEBUG_PRINTLN("Temp radio params expired before apply");
} else if (revert_radio_due && !hasOutbound() && radio_apply_ready) {
if (applySavedRadioParams()) {
set_radio_at = revert_radio_at = 0;
temp_radio_applied = false;
saved_radio_apply_pending = false;
radio_apply_retry_at = 0;
radio_apply_failures = 0;
MESH_DEBUG_PRINTLN("Radio params restored");
} else {
radio_apply_failed = true;
}
} else if (set_radio_at && millisHasNowPassed(set_radio_at) && !hasOutbound()
&& radio_apply_ready) {
uint32_t rx_us = _prefs.rx_ps_rx_us;
uint32_t sleep_us = _prefs.rx_ps_sleep_us;
bool timing_ok = true;
if (_prefs.rx_powersaving_enabled && _prefs.rx_ps_level != 0) {
uint32_t preamble = _prefs.rx_ps_preamble ? _prefs.rx_ps_preamble
: rxPowerSavingPreambleForParams(pending_sf, pending_bw);
timing_ok = CommonCLI::calculateRxPowerSavingLevel(
_prefs.rx_ps_level, pending_sf, pending_bw, preamble, &rx_us, &sleep_us);
}
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 && _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;
radio_apply_retry_at = 0;
radio_apply_failures = 0;
MESH_DEBUG_PRINTLN("Temp radio params");
} else {
// A failed setParams() may have applied only a prefix of the tuple.
// Ensure expiry restores the complete saved configuration.
saved_radio_apply_pending = true;
radio_apply_failed = true;
}
}
if (saved_radio_apply_pending && !temp_radio_applied && (tx_gap || !hasOutbound())
&& radio_apply_ready && !radio_apply_failed) {
if (applySavedRadioParams()) {
saved_radio_apply_pending = false;
radio_apply_retry_at = 0;
radio_apply_failures = 0;
} else {
radio_apply_failed = true;
}
}
if (radio_apply_failed) {
radio_apply_retry_at = futureMillis(nextRadioApplyRetryDelay(radio_apply_failures));
}
}
#if !MESH_ENABLE_SENSOR_TRACKER
void SensorMesh::onSendComplete(mesh::Packet* packet) {
mesh::Mesh::onSendComplete(packet);
_cli.servicePendingTxPower();
_cli.morseReply().complete(packet);
_cli.servicePendingRxSettings();
_cli.radioProfiles().servicePendingTuning();
servicePendingPrimaryRadioSettings(true);
}
void SensorMesh::onSendFail(mesh::Packet* packet) {
@@ -1339,8 +1411,23 @@ void SensorMesh::begin(FILESYSTEM* fs) {
}
bool SensorMesh::applySavedRadioParams() {
#if defined(USE_LR2021)
uint8_t extra_sf_count = 0;
const auto* profiles = radio_driver.profiles();
const bool side_detectors_allowed = !profiles || !profiles->enabled();
if (!mesh::lr2021::storedSideDetectorCount(_prefs.extra_sf, extra_sf_count)
|| !mesh::lr2021::validateSideDetectorSFs(
_prefs.extra_sf, extra_sf_count, _prefs.sf, _prefs.bw)) {
if (side_detectors_allowed && !radio_driver.configSideDetectors(nullptr, 0, _prefs.bw)) return false;
memset(_prefs.extra_sf, 0, sizeof(_prefs.extra_sf));
extra_sf_count = 0;
savePrefs();
}
#endif
// A busy radio can accept the tuple before deferring TX power. Recovery
// is complete only after both saved settings have been accepted.
if (radio_driver.supportsRxBoostedGainMode()
&& !radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain)) return false;
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;
@@ -1350,6 +1437,10 @@ bool SensorMesh::applySavedRadioParams() {
return false;
}
active_cr = _prefs.cr;
#if defined(USE_LR2021)
if (side_detectors_allowed
&& !radio_driver.configSideDetectors(_prefs.extra_sf, extra_sf_count, _prefs.bw)) return false;
#endif
return radio_driver.setTxPower(_prefs.tx_power_dbm);
}
@@ -1479,6 +1570,40 @@ bool SensorMesh::isTxPowerApplied(int8_t power_dbm) const {
bool SensorMesh::setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) {
return radio_driver.setRxPowerSaving(enable, rx_us, sleep_us);
}
bool SensorMesh::supportsRxPowerSaving() const {
return radio_driver.supportsRxPowerSaving();
}
mesh::RadioParamApplyResult SensorMesh::trySetRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) {
return radio_driver.trySetRxPowerSaving(enable, rx_us, sleep_us);
}
bool SensorMesh::supportsRxPowerSavingRfRxDisable() const {
return radio_driver.supportsRxPowerSavingRfRxDisable();
}
mesh::RadioParamApplyResult SensorMesh::trySetRxPowerSavingRfRxDisabled(bool disabled) {
return radio_driver.trySetRxPowerSavingRfRxDisabled(disabled);
}
bool SensorMesh::isRxPowerSavingRfRxDisabled() const {
return radio_driver.isRxPowerSavingRfRxDisabled();
}
bool SensorMesh::supportsRxBoostedGain() const {
return radio_driver.supportsRxBoostedGainMode();
}
mesh::RadioParamApplyResult SensorMesh::trySetRxBoostedGain(bool enable) {
return radio_driver.trySetRxBoostedGainMode(enable);
}
#if defined(USE_LR2021)
mesh::RadioParamApplyResult SensorMesh::tryConfigSideDetectors(const uint8_t sideDetSFs[], uint8_t num, float bw) {
return radio_driver.tryConfigSideDetectors(sideDetSFs, num, bw);
}
#endif
void SensorMesh::getRxPsWatchdogCounts(uint32_t* soft, uint32_t* hard) {
*soft = radio_driver.getRxPsWatchdogSoftCount();
*hard = radio_driver.getRxPsWatchdogHardCount();
@@ -1551,69 +1676,7 @@ void SensorMesh::loop() {
updateAdvertTimer(); // schedule next local advert
}
const bool revert_radio_due = revert_radio_at && millisHasNowPassed(revert_radio_at);
const bool radio_apply_ready = !radio_apply_retry_at || millisHasNowPassed(radio_apply_retry_at);
bool radio_apply_failed = false;
if (revert_radio_due && !temp_radio_applied) {
// Never apply a temporary channel after its window has already ended.
set_radio_at = revert_radio_at = 0;
radio_apply_retry_at = 0;
radio_apply_failures = 0;
MESH_DEBUG_PRINTLN("Temp radio params expired before apply");
} else if (revert_radio_due && !hasOutbound() && radio_apply_ready) {
if (applySavedRadioParams()) {
set_radio_at = revert_radio_at = 0;
temp_radio_applied = false;
saved_radio_apply_pending = false;
radio_apply_retry_at = 0;
radio_apply_failures = 0;
MESH_DEBUG_PRINTLN("Radio params restored");
} else {
radio_apply_failed = true;
}
} else if (set_radio_at && millisHasNowPassed(set_radio_at) && !hasOutbound()
&& radio_apply_ready) {
uint32_t rx_us = _prefs.rx_ps_rx_us;
uint32_t sleep_us = _prefs.rx_ps_sleep_us;
bool timing_ok = true;
if (_prefs.rx_powersaving_enabled && _prefs.rx_ps_level != 0) {
uint32_t preamble = _prefs.rx_ps_preamble ? _prefs.rx_ps_preamble
: rxPowerSavingPreambleForParams(pending_sf, pending_bw);
timing_ok = CommonCLI::calculateRxPowerSavingLevel(
_prefs.rx_ps_level, pending_sf, pending_bw, preamble, &rx_us, &sleep_us);
}
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 && _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;
radio_apply_retry_at = 0;
radio_apply_failures = 0;
MESH_DEBUG_PRINTLN("Temp radio params");
} else {
// A failed setParams() may have applied only a prefix of the tuple.
// Ensure expiry restores the complete saved configuration.
saved_radio_apply_pending = true;
radio_apply_failed = true;
}
}
if (saved_radio_apply_pending && !temp_radio_applied && !hasOutbound()
&& radio_apply_ready && !radio_apply_failed) {
if (applySavedRadioParams()) {
saved_radio_apply_pending = false;
radio_apply_retry_at = 0;
radio_apply_failures = 0;
} else {
radio_apply_failed = true;
}
}
if (radio_apply_failed) {
radio_apply_retry_at = futureMillis(nextRadioApplyRetryDelay(radio_apply_failures));
}
servicePendingPrimaryRadioSettings();
uint32_t curr = getRTCClock()->getCurrentTime();
if (curr >= last_read_time + SENSOR_READ_INTERVAL_SECS) {
@@ -1751,7 +1814,7 @@ mesh::powerdiag::Mask SensorMesh::pendingWorkReason() const {
#endif
if (!canIdleDualRadio()) return mask(Reason::DualRadio);
if (hasPendingOtaApply()) return mask(Reason::Ota);
if (_cli.hasActiveUserGpioTimer() || _cli.hasPendingTxPower()) return mask(Reason::Scheduled);
if (_cli.hasActiveUserGpioTimer() || _cli.hasPendingRadioSettings()) return mask(Reason::Scheduled);
if (radio_driver.isWatchdogObserving()) return mask(Reason::RadioWatchdog);
uint32_t noise_floor_delay_ms;
if (getNextNoiseFloorWakeDelay(noise_floor_delay_ms) && noise_floor_delay_ms == 0)