Apply saved TX power automatically across firmware roles

This commit is contained in:
mikecarper committed 2026-10-25 14:08:15 -07:00
1 parent 31e5d54074
commit 65928aae95
51 files changed
+3838 -2728

No files matched your search

+60 -52
View File
@@ -1972,6 +1972,8 @@ void MyMesh::onRadioProfileCopyQueued(mesh::Packet* packet, const mesh::Packet*
void MyMesh::onSendComplete(mesh::Packet* packet) {
mesh::Mesh::onSendComplete(packet);
// Use the completed packet's idle gap before a busy queue starts another TX.
servicePendingTxPower();
_morse_reply.complete(packet);
}
@@ -3534,8 +3536,7 @@ void MyMesh::finishRadioParamApply(float freq, float bw, uint8_t sf,
return;
}
// Preserve an outstanding power rollback; the full saved-radio recovery
// checks both the tuple and TX power before clearing this latch.
// Preserve any earlier saved-radio recovery while committing the tuple.
saved_radio_apply_pending = previous_recovery_pending;
radio_apply_retry_at = 0;
radio_apply_failures = 0;
@@ -3546,24 +3547,22 @@ void MyMesh::finishRadioParamApply(float freq, float bw, uint8_t sf,
writeOKFrame(route);
}
void MyMesh::deferRadioCommand(uint8_t kind) {
void MyMesh::deferRadioCommand() {
command_radio_reply_route = _serial->captureReplyRoute();
if (command_radio_reply_route == NULL) {
writeErrFrame(ERR_CODE_BAD_STATE);
return;
}
command_radio_apply_pending = kind;
command_radio_apply_pending = true;
command_radio_apply_deadline = futureMillis(COMMAND_RADIO_APPLY_TIMEOUT_MS);
}
void MyMesh::cancelPendingRadioParamApply() {
if (!command_radio_apply_pending) return;
const bool restore_tuple = command_radio_apply_pending == 1;
command_radio_apply_pending = false;
command_radio_apply_deadline = 0;
command_radio_reply_route = NULL;
if (!restore_tuple) return;
// The requested tuple was not committed. Reassert the persisted tuple in
// case a failed hardware apply only restored part of the old configuration.
saved_radio_apply_pending = true;
@@ -3583,20 +3582,6 @@ void MyMesh::servicePendingRadioParamApply() {
const bool expired = static_cast<int32_t>(static_cast<uint32_t>(
_ms->getMillis() - command_radio_apply_deadline)) >= 0;
if (command_radio_apply_pending == 2) {
const auto result = expired ? RadioSettingResult::RadioBusy
: applyAndSaveTxPower(static_cast<int8_t>(command_radio_repeat), true);
if (result == RadioSettingResult::RadioBusy
&& !expired) return;
if (result == RadioSettingResult::Saved) writeOKFrame(command_radio_reply_route);
else writeErrFrame(result == RadioSettingResult::SaveFailed
? ERR_CODE_FILE_IO_ERROR : ERR_CODE_BAD_STATE, command_radio_reply_route);
// The checked setter owns any failed-save rollback. Retire this command
// without forcing a tuple restore or consuming a pending receive.
cancelPendingRadioParamApply();
return;
}
mesh::RadioParamApplyResult result = tryApplyRadioParams(
command_radio_freq, command_radio_bw, command_radio_sf, command_radio_cr);
if (result == mesh::RadioParamApplyResult::APPLIED) {
@@ -5687,9 +5672,10 @@ void MyMesh::execCommand(char* cmd, char* reply) {
snprintf(reply, 160, "Error: TX -9..%d dBm", MAX_LORA_TX_POWER);
} else {
const auto result = applyAndSaveTxPower(static_cast<int8_t>(parsed));
strcpy(reply, result == RadioSettingResult::Saved ? "OK"
strcpy(reply, result == RadioSettingResult::Saved
? (tx_power_apply_pending ? "OK - saved; pending" : "OK")
: result != RadioSettingResult::SaveFailed
? "Error: radio busy or TX power rejected"
? "Error: TX power rejected"
: "Error: TX not saved");
}
return;
@@ -6808,7 +6794,7 @@ void MyMesh::handleCmdFrame(size_t len) {
command_radio_sf = sf;
command_radio_cr = cr;
command_radio_repeat = repeat;
deferRadioCommand(1);
deferRadioCommand();
MESH_DEBUG_PRINTLN("Deferred CMD_SET_RADIO_PARAMS while radio is busy");
return;
}
@@ -6836,15 +6822,8 @@ void MyMesh::handleCmdFrame(size_t len) {
int8_t power = (int8_t)cmd_frame[1];
if (power < -9 || power > MAX_LORA_TX_POWER) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else if (command_radio_apply_pending) {
writeErrFrame(ERR_CODE_BAD_STATE);
} else {
const auto result = applyAndSaveTxPower(power);
if (result == RadioSettingResult::RadioBusy) {
command_radio_repeat = static_cast<uint8_t>(power);
deferRadioCommand(2);
return;
}
if (result == RadioSettingResult::Saved) writeOKFrame();
else writeErrFrame(result == RadioSettingResult::RadioRejected
? ERR_CODE_BAD_STATE : ERR_CODE_FILE_IO_ERROR);
@@ -8488,32 +8467,47 @@ bool MyMesh::saveAdvertLocation(double latitude, double longitude) {
return false;
}
MyMesh::RadioSettingResult MyMesh::applyAndSaveTxPower(int8_t power, bool from_pending) {
if (command_radio_apply_pending && !from_pending) return RadioSettingResult::RadioBusy;
MyMesh::RadioSettingResult MyMesh::applyAndSaveTxPower(int8_t power) {
if (!radio_driver.isTxPowerValid(power)) return RadioSettingResult::RadioRejected;
const int8_t previous = _prefs.tx_power_dbm;
if (_radio_available) {
const auto result = radio_driver.trySetTxPower(power);
if (result != mesh::RadioParamApplyResult::APPLIED) {
return result == mesh::RadioParamApplyResult::BUSY
? RadioSettingResult::RadioBusy : RadioSettingResult::RadioRejected;
}
const auto result = _radio_available && !command_radio_apply_pending
? radio_driver.trySetTxPower(power) : mesh::RadioParamApplyResult::BUSY;
if (result == mesh::RadioParamApplyResult::FAILED) {
// A rejected SPI transaction may have partly changed the hardware. Keep
// the saved setting authoritative and restore it without retuning RX.
tx_power_apply_pending = true;
tx_power_apply_retry_at = 0;
return RadioSettingResult::RadioRejected;
}
_prefs.tx_power_dbm = power;
if (savePrefs()) return RadioSettingResult::Saved;
if (savePrefs()) {
tx_power_apply_pending = result != mesh::RadioParamApplyResult::APPLIED;
tx_power_apply_retry_at = 0;
return RadioSettingResult::Saved;
}
_prefs.tx_power_dbm = previous;
if (_radio_available && !radio_driver.setTxPower(previous)) {
// An RX packet can arrive between the apply and its failed flash commit.
// Keep retrying the durable power rather than leaving the rejected value
// active until reboot. Recovery also restores the saved tuple, so an
// active TempRadio lease deliberately delays it until the lease ends.
saved_radio_apply_pending = true;
radio_apply_retry_at = 0;
radio_apply_failures = 0;
// A busy attempt has not changed the earlier pending target or retry time.
if (result == mesh::RadioParamApplyResult::APPLIED) {
tx_power_apply_pending = !radio_driver.setTxPower(previous);
tx_power_apply_retry_at = 0;
}
return RadioSettingResult::SaveFailed;
}
void MyMesh::servicePendingTxPower() {
if (!tx_power_apply_pending || !_radio_available || command_radio_apply_pending
|| (_radio && _radio->isCarrierWaveActive())) return;
const uint32_t now = _ms->getMillis();
if (tx_power_apply_retry_at
&& static_cast<int32_t>(now - tx_power_apply_retry_at) < 0) return;
const auto result = radio_driver.trySetTxPower(_prefs.tx_power_dbm);
if (result == mesh::RadioParamApplyResult::APPLIED) tx_power_apply_pending = false;
// Busy admission does not touch RX. Back off actual hardware failures.
tx_power_apply_retry_at = result == mesh::RadioParamApplyResult::FAILED
? (now + 1000u ? now + 1000u : 1u) : 0;
}
bool MyMesh::applyAndSaveFemRxGain(bool enabled) {
// Binary Companion setters and WebConfig also enter through this helper.
// Do not alter an external FEM while the Morse transmitter owns it.
@@ -10957,6 +10951,17 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
if (handleLocalControlCommand(command, reply, reply_capacity)) return true;
#if defined(MESH_HIL_TX_FREQ_MHZ)
// RF test builds verify active power before permitting any test TX.
if (strcmp(command, "get tx.status") == 0) {
snprintf(reply, reply_capacity, "> desired=%d,apply=%s", (int)_prefs.tx_power_dbm,
!tx_power_apply_pending && _radio_available
&& radio_driver.isTxPowerApplied(_prefs.tx_power_dbm)
? "applied" : "pending");
return true;
}
#endif
if (strncmp(command, "set tx ", 7) == 0) {
int32_t parsed = 0;
if (!mesh::cli::parseIntegerStrict(command + 7, parsed)) {
@@ -10967,12 +10972,13 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
} else {
const int8_t requested = static_cast<int8_t>(parsed);
const auto result = applyAndSaveTxPower(requested);
if (result == RadioSettingResult::RadioRejected || result == RadioSettingResult::RadioBusy) {
mesh::cli::formatError(reply, reply_capacity, "radio busy or TX power rejected");
if (result == RadioSettingResult::RadioRejected) {
mesh::cli::formatError(reply, reply_capacity, "TX power rejected");
} else if (result == RadioSettingResult::SaveFailed) {
mesh::cli::formatError(reply, reply_capacity, "TX not saved");
} else {
strcpy(reply, "OK");
snprintf(reply, reply_capacity, "%s",
tx_power_apply_pending ? "OK - saved; pending" : "OK");
}
}
return true;
@@ -11618,6 +11624,7 @@ void MyMesh::loop() {
#endif
}
servicePendingRadioParamApply();
servicePendingTxPower();
#if MESH_ENABLE_ONE_KEY_DM
if (held_dm_count != 0 || verified_pending_count != 0) releaseHeldOneKeyDMs();
#endif
@@ -11710,7 +11717,7 @@ bool MyMesh::canRecoverUsbLogging() const {
&& !_store->hasPendingAdvertWrites()
#endif
&& !hasOutbound() && !isAnyTempRadioActive() && !hasPendingOtaApply()
&& !command_radio_apply_pending && !saved_radio_apply_pending
&& !command_radio_apply_pending && !saved_radio_apply_pending && !tx_power_apply_pending
&& _scheduled_reboot_at == 0 && !hasPendingReqs()
&& !_delayed_replies.hasBinaryTrace() && sign_data == NULL
#if COMPANION_FEATURE_TEXT_TERMINAL
@@ -11755,7 +11762,8 @@ mesh::powerdiag::Mask MyMesh::pendingWorkReason() const {
if (_iter_started && _serial != NULL
&& _serial->isReplyRouteAvailable(_iter_reply_route)) return mask(Reason::SerialIO);
if (hasPendingOtaApply()) return mask(Reason::Ota);
if (command_radio_apply_pending || _scheduled_reboot_at != 0) return mask(Reason::Scheduled);
if (command_radio_apply_pending || (tx_power_apply_pending && _radio_available)
|| _scheduled_reboot_at != 0) return mask(Reason::Scheduled);
if (hasPendingReqs() || _delayed_replies.hasBinaryTrace()) return mask(Reason::Mesh);
#if COMPANION_FEATURE_TEXT_TERMINAL
if (_terminal_trace_pending) return mask(Reason::Mesh);
+7 -5
View File
@@ -675,8 +675,9 @@ private:
bool applyAndSaveFemRxGain(bool enabled);
bool applyAndSaveFemTxGain(bool enabled);
bool applyAndSaveRxBoostedGain(bool enabled);
enum class RadioSettingResult { Saved, RadioRejected, SaveFailed, RadioBusy };
RadioSettingResult applyAndSaveTxPower(int8_t power, bool from_pending = false);
enum class RadioSettingResult { Saved, RadioRejected, SaveFailed };
RadioSettingResult applyAndSaveTxPower(int8_t power);
void servicePendingTxPower();
bool handleCadCommand(const char* command, char* reply, size_t reply_size);
bool saveBluetoothNameOverride(const char* name);
bool applyAndSaveBluetoothName(const char* value, char* reply,
@@ -749,7 +750,7 @@ private:
void finishRadioParamApply(float freq, float bw, uint8_t sf, uint8_t cr,
uint8_t repeat,
BaseSerialInterface* route = nullptr);
void deferRadioCommand(uint8_t kind);
void deferRadioCommand();
void cancelPendingRadioParamApply();
void servicePendingRadioParamApply();
void servicePendingSerialReply();
@@ -911,10 +912,11 @@ private:
#endif
bool saved_radio_apply_pending;
bool _radio_available;
bool tx_power_apply_pending = false;
uint32_t tx_power_apply_retry_at = 0;
unsigned long radio_apply_retry_at;
uint8_t radio_apply_failures;
// 0: none, 1: modulation tuple, 2: TX power (stored in the repeat byte).
uint8_t command_radio_apply_pending;
bool command_radio_apply_pending;
float command_radio_freq;
float command_radio_bw;
uint8_t command_radio_sf;
+13 -1
View File
@@ -4376,6 +4376,7 @@ bool MyMesh::sendRepeatersFloodText(const char* text, const TransportKey* scope,
void MyMesh::onSendComplete(mesh::Packet* packet) {
mesh::Mesh::onSendComplete(packet);
_cli.servicePendingTxPower();
_cli.morseReply().complete(packet);
#if MESH_ENABLE_FLEET_CONTROL
if (_cli.fleetChannel()) _cli.fleetChannel()->complete(packet, _cli.radioProfiles());
@@ -5882,6 +5883,16 @@ bool MyMesh::setTxPower(int8_t power_dbm) {
return radio_driver.setTxPower(power_dbm);
}
mesh::RadioParamApplyResult MyMesh::trySetTxPower(int8_t power_dbm) {
return radio_driver.trySetTxPower(power_dbm);
}
#if defined(MESH_HIL_TX_FREQ_MHZ)
bool MyMesh::isTxPowerApplied(int8_t power_dbm) const {
return radio_driver.isTxPowerApplied(power_dbm);
}
#endif
bool MyMesh::setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) {
bool ok = radio_driver.setRxPowerSaving(enable, rx_us, sleep_us);
MESH_DEBUG_PRINTLN("RX Power Saving: %s (%lu/%lu us)%s",
@@ -14791,7 +14802,8 @@ mesh::powerdiag::Mask MyMesh::pendingWorkReason() const {
#if defined(WITH_WEBCONFIG) || defined(ETHERNET_ENABLED)
if (_local_cli_output.busy()) return mask(Reason::SerialIO);
#endif
if (deferred_cli_command.pending || pending_self_advert || _cli.hasActiveUserGpioTimer()) return mask(Reason::Scheduled);
if (deferred_cli_command.pending || pending_self_advert || _cli.hasActiveUserGpioTimer()
|| _cli.hasPendingTxPower()) return mask(Reason::Scheduled);
#if defined(WITH_BRIDGE)
const AbstractBridge* active_bridge = activeBridge();
if (active_bridge && active_bridge->isRunning()) {
+5 -1
View File
@@ -1282,6 +1282,10 @@ public:
void cancelPendingSerialOutput();
#endif
bool setTxPower(int8_t power_dbm) override;
mesh::RadioParamApplyResult trySetTxPower(int8_t power_dbm) override;
#if defined(MESH_HIL_TX_FREQ_MHZ)
bool isTxPowerApplied(int8_t power_dbm) const override;
#endif
bool setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) override;
bool supportsRxPowerSavingRfRxDisable() const override;
bool setRxPowerSavingRfRxDisabled(bool disabled) override;
@@ -1356,7 +1360,7 @@ public:
#endif
bool canRecoverUsbLogging() const {
return dirty_contacts_expiry == 0 && !hasOutbound() && !isAnyTempRadioActive()
&& !hasPendingOtaApply() && !saved_radio_apply_pending
&& !hasPendingOtaApply() && !saved_radio_apply_pending && !_cli.hasPendingTxPower()
&& !temp_radio_handoff_pending && _ota_update_at == 0;
}
#if MESH_ENABLE_FLOOD_RULE_ENGINE
+12
View File
@@ -2424,6 +2424,7 @@ void MyMesh::onGroupPacketRecv(mesh::Packet* packet) {
void MyMesh::onSendComplete(mesh::Packet* packet) {
mesh::Mesh::onSendComplete(packet);
_cli.servicePendingTxPower();
_cli.morseReply().complete(packet);
#if MESH_ENABLE_FLEET_CONTROL
if (_cli.fleetChannel()) _cli.fleetChannel()->complete(packet, _cli.radioProfiles());
@@ -3190,6 +3191,16 @@ bool MyMesh::setTxPower(int8_t power_dbm) {
return radio_driver.setTxPower(power_dbm);
}
mesh::RadioParamApplyResult MyMesh::trySetTxPower(int8_t power_dbm) {
return radio_driver.trySetTxPower(power_dbm);
}
#if defined(MESH_HIL_TX_FREQ_MHZ)
bool MyMesh::isTxPowerApplied(int8_t power_dbm) const {
return radio_driver.isTxPowerApplied(power_dbm);
}
#endif
bool MyMesh::setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) {
return radio_driver.setRxPowerSaving(enable, rx_us, sleep_us);
}
@@ -4489,6 +4500,7 @@ mesh::powerdiag::Mask MyMesh::pendingWorkReason() const {
using mesh::powerdiag::Reason;
if (!canIdleDualRadio()) return mask(Reason::DualRadio);
if (hasPendingOtaApply()) return mask(Reason::Ota);
if (_cli.hasPendingTxPower()) return mask(Reason::Scheduled);
#if defined(ESP32_PLATFORM)
if (mesh::wifi::networkPower().foregroundActive()) return mask(Reason::Network);
#endif
+5 -1
View File
@@ -620,6 +620,10 @@ public:
void cancelPendingSerialOutput();
#endif
bool setTxPower(int8_t power_dbm) override;
mesh::RadioParamApplyResult trySetTxPower(int8_t power_dbm) override;
#if defined(MESH_HIL_TX_FREQ_MHZ)
bool isTxPowerApplied(int8_t power_dbm) const override;
#endif
bool setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) override;
void recalibrateNoiseFloor() override { _radio->recalibrateNoiseFloor(); }
void getRxPsWatchdogCounts(uint32_t* soft, uint32_t* hard) override;
@@ -700,7 +704,7 @@ public:
#endif
bool canRecoverUsbLogging() const {
return dirty_contacts_expiry == 0 && !hasOutbound() && !isAnyTempRadioActive()
&& !hasPendingOtaApply() && !saved_radio_apply_pending
&& !hasPendingOtaApply() && !saved_radio_apply_pending && !_cli.hasPendingTxPower()
&& set_radio_at == 0 && _ota_update_at == 0;
}
#if MESH_ENABLE_ROOM_FLOOD_RULE_ENGINE
+12 -1
View File
@@ -1110,6 +1110,7 @@ bool SensorMesh::handleIncomingMsg(ClientInfo& from, uint32_t timestamp, uint8_t
#if !MESH_ENABLE_SENSOR_TRACKER
void SensorMesh::onSendComplete(mesh::Packet* packet) {
mesh::Mesh::onSendComplete(packet);
_cli.servicePendingTxPower();
_cli.morseReply().complete(packet);
}
@@ -1460,6 +1461,16 @@ bool SensorMesh::setTxPower(int8_t power_dbm) {
return radio_driver.setTxPower(power_dbm);
}
mesh::RadioParamApplyResult SensorMesh::trySetTxPower(int8_t power_dbm) {
return radio_driver.trySetTxPower(power_dbm);
}
#if defined(MESH_HIL_TX_FREQ_MHZ)
bool SensorMesh::isTxPowerApplied(int8_t power_dbm) const {
return radio_driver.isTxPowerApplied(power_dbm);
}
#endif
bool SensorMesh::setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) {
return radio_driver.setRxPowerSaving(enable, rx_us, sleep_us);
}
@@ -1730,7 +1741,7 @@ mesh::powerdiag::Mask SensorMesh::pendingWorkReason() const {
#endif
if (!canIdleDualRadio()) return mask(Reason::DualRadio);
if (hasPendingOtaApply()) return mask(Reason::Ota);
if (_cli.hasActiveUserGpioTimer()) return mask(Reason::Scheduled);
if (_cli.hasActiveUserGpioTimer() || _cli.hasPendingTxPower()) 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)
+5 -1
View File
@@ -73,7 +73,7 @@ public:
void loop();
bool canRecoverUsbLogging() const {
return dirty_contacts_expiry == 0 && !hasOutbound() && !isAnyTempRadioActive()
&& !hasPendingOtaApply() && !saved_radio_apply_pending
&& !hasPendingOtaApply() && !saved_radio_apply_pending && !_cli.hasPendingTxPower()
&& set_radio_at == 0;
}
void handleCommand(uint32_t sender_timestamp, char* command, char* reply,
@@ -115,6 +115,10 @@ public:
void eraseLogFile() override { }
void dumpLogFile() override { }
bool setTxPower(int8_t power_dbm) override;
mesh::RadioParamApplyResult trySetTxPower(int8_t power_dbm) override;
#if defined(MESH_HIL_TX_FREQ_MHZ)
bool isTxPowerApplied(int8_t power_dbm) const override;
#endif
bool setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) override;
void recalibrateNoiseFloor() override { _radio->recalibrateNoiseFloor(); }
void getRxPsWatchdogCounts(uint32_t* soft, uint32_t* hard) override;
+3 -1
View File
@@ -83,7 +83,8 @@ bool SensorMesh::trackerNeedsRadio() const {
|| board.isRadioTestActive() || mesh::isUsbLoggingWatchdogArmed()
|| isAnyTempRadioActive() || hasPendingOtaApply() || isDualRadioActive()
|| set_radio_at || revert_radio_at || saved_radio_apply_pending
|| _cli.hasActiveUserGpioTimer() || hasOutbound() || _mgr->getOutboundTotal() != 0)
|| _cli.hasActiveUserGpioTimer() || _cli.hasPendingTxPower()
|| hasOutbound() || _mgr->getOutboundTotal() != 0)
return true;
uint32_t delay;
if (hasPendingRadioWork() || getNextRetryWakeDelay(delay)) return true;
@@ -269,6 +270,7 @@ bool SensorMesh::handleTrackerPath(mesh::Packet* packet, uint8_t* path, uint8_t
void SensorMesh::onSendComplete(mesh::Packet* packet) {
mesh::Mesh::onSendComplete(packet);
_cli.servicePendingTxPower();
_cli.morseReply().complete(packet);
if (!tracker_pending || !packet || packet->getPayloadType() != PAYLOAD_TYPE_REQ) return;
uint8_t hash[MAX_HASH_SIZE];