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

+57 -5
View File
@@ -173,6 +173,7 @@ void CommonCLI::loop() {
}
#endif
if (!mesh::morseTimingActive(_callbacks->getProfileRadio())) {
servicePendingTxPower();
_radio_profiles.loop();
loopManagement();
#if MESH_ENABLE_TX_BEACONS
@@ -197,6 +198,55 @@ bool CommonCLI::hasActiveUserGpioTimer() const {
#endif
}
void CommonCLI::servicePendingTxPower() {
if (!_tx_power_pending || (_tx_power_retry_at
&& static_cast<int32_t>(millis() - _tx_power_retry_at) < 0)) return;
// A pending normal setting must not become a power-meter sweep if a CW
// test started after the setting was accepted.
const mesh::Radio* radio = _callbacks->getProfileRadio();
if (radio && radio->isCarrierWaveActive()) return;
const auto result = _callbacks->trySetTxPower(_prefs->tx_power_dbm);
if (result == mesh::RadioParamApplyResult::APPLIED) _tx_power_pending = false;
_tx_power_retry_at = result == mesh::RadioParamApplyResult::FAILED
? millis() + 1000 : 0;
if (result == mesh::RadioParamApplyResult::FAILED && !_tx_power_retry_at)
_tx_power_retry_at = 1;
}
void CommonCLI::applyAndSaveTxPower(int8_t power, char* reply) {
using Result = mesh::RadioParamApplyResult;
const int8_t previous = _prefs->tx_power_dbm;
const bool previous_pending = _tx_power_pending;
const uint32_t previous_retry_at = _tx_power_retry_at;
const auto result = _callbacks->trySetTxPower(power);
if (result == Result::FAILED) {
// A failed SPI write may have changed the hardware partially. Reassert
// the saved desired power without accepting or saving the rejected one.
_tx_power_pending = true;
_tx_power_retry_at = 0;
mesh::cli::formatError(reply, 160, "TX power rejected");
return;
}
_prefs->tx_power_dbm = power;
if (trySavePrefs()) {
_tx_power_pending = result == Result::BUSY;
_tx_power_retry_at = 0;
strcpy(reply, _tx_power_pending ? "OK - saved; pending" : "OK");
return;
}
_prefs->tx_power_dbm = previous;
_tx_power_pending = previous_pending;
_tx_power_retry_at = previous_retry_at;
if (result == Result::APPLIED) {
const auto restored = _callbacks->trySetTxPower(previous);
_tx_power_pending = restored != Result::APPLIED;
_tx_power_retry_at = restored == Result::FAILED ? millis() + 1000 : 0;
if (restored == Result::FAILED && !_tx_power_retry_at) _tx_power_retry_at = 1;
}
mesh::cli::formatError(reply, 160, "TX save unconfirmed; reboot may differ");
}
// Old fork firmware persisted the (since removed) NodePrefs MQTT fields to /com_prefs
// as a zero-filled gap between owner_info (which ends at offset 290) and a trailing
// observer block (rx_boosted_gain, flood_max_*, snmp/watchdog/alert settings).
@@ -5322,12 +5372,8 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
} else if (!mesh::isLoRaTxPowerValid(power, _prefs->freq)) {
sprintf(reply, "Error: TX power range is %d to %d dBm",
(int)minimum, (int)maximum);
} else if (!_callbacks->setTxPower(static_cast<int8_t>(power))) {
mesh::cli::formatError(reply, 160, "radio busy or TX power rejected");
} else {
_prefs->tx_power_dbm = static_cast<int8_t>(power);
savePrefs();
strcpy(reply, "OK");
applyAndSaveTxPower(static_cast<int8_t>(power), reply);
}
} else if (memcmp(config, "freq ", 5) == 0) {
float freq = 0.0f;
@@ -6045,6 +6091,12 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
} else {
strcpy(reply, "> strict");
}
#if defined(MESH_HIL_TX_FREQ_MHZ)
} else if (configKeyEquals(config, "tx.status")) {
sprintf(reply, "> desired=%d,apply=%s", (int)_prefs->tx_power_dbm,
!_tx_power_pending && _callbacks->isTxPowerApplied(_prefs->tx_power_dbm)
? "applied" : "pending");
#endif
} else if (configKeyEquals(config, "tx")) {
sprintf(reply, "> %d", (int32_t) _prefs->tx_power_dbm);
} else if (configKeyEquals(config, "freq")) {
+12
View File
@@ -564,6 +564,13 @@ public:
virtual void eraseLogFile() = 0;
virtual void dumpLogFile() = 0;
virtual bool setTxPower(int8_t power_dbm) = 0;
virtual mesh::RadioParamApplyResult trySetTxPower(int8_t power_dbm) {
return setTxPower(power_dbm) ? mesh::RadioParamApplyResult::APPLIED
: mesh::RadioParamApplyResult::FAILED;
}
#if defined(MESH_HIL_TX_FREQ_MHZ)
virtual bool isTxPowerApplied(int8_t) const { return false; }
#endif
virtual void formatNeighborsReply(char *reply) = 0;
// A zero-length prefix clears the whole table; pubkey may be null then.
// Nonempty prefixes must contain 1..PUB_KEY_SIZE bytes.
@@ -941,6 +948,8 @@ class CommonCLI {
bool _com_prefs_needs_upgrade = false; // old-format legacy prefs detected; rewrite once after load
bool _common_save_result_known = false;
bool _common_save_succeeded = false;
bool _tx_power_pending = false;
uint32_t _tx_power_retry_at = 0;
uint32_t _prefs_save_failures = 0;
mesh::RadioProfileCLI _radio_profiles;
#if MESH_ENABLE_TX_BEACONS
@@ -961,6 +970,7 @@ class CommonCLI {
PrefsSaveRouting::Scope scope = PrefsSaveRouting::Scope::Common);
bool trySavePrefs(
PrefsSaveRouting::Scope scope = PrefsSaveRouting::Scope::Common);
void applyAndSaveTxPower(int8_t power, char* reply);
bool applyAndSaveAdcMultiplier(float multiplier, char* reply, size_t capacity);
bool saveObserverPrefs();
void loadPrefsInt(FILESYSTEM* _fs, const char* filename);
@@ -1051,6 +1061,8 @@ public:
#endif
uint8_t batteryPercent(uint16_t mv) const;
bool hasActiveUserGpioTimer() const;
bool hasPendingTxPower() const { return _tx_power_pending; }
void servicePendingTxPower();
void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
mesh::MainBoard* getBoard() { return _board; }
uint8_t buildAdvertData(uint8_t node_type, uint8_t* app_data);
+4
View File
@@ -202,6 +202,10 @@ void CommonRadioPrefs::formatRxPowerSavingStatus(char* reply, size_t reply_size)
RXPowerSavingCLI::get(config, status, sf, bw, reply, reply_size, wire_preamble);
}
#if defined(NRF52_PLATFORM)
// Keep shared radio parsing out of the Companion's larger command dispatcher.
__attribute__((noinline))
#endif
bool CommonRadioPrefs::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) {
(void)sender_timestamp;
if (command == nullptr || reply == nullptr) return false;
+15 -1
View File
@@ -68,9 +68,23 @@ public:
bool isCalibratingNoiseFloor() const { return false; }
uint32_t intID();
bool isTxPowerValid(int8_t dbm) const {
// The SDK accepts 8..84 quarter-dBm units; the public API uses whole dBm.
if (dbm < 2 || dbm > 21) return false;
#ifdef MIN_LORA_TX_POWER
if (dbm < MIN_LORA_TX_POWER) return false;
#endif
#ifdef MAX_LORA_TX_POWER
if (dbm > MAX_LORA_TX_POWER) return false;
#endif
return true;
}
bool isTxPowerApplied(int8_t dbm) const {
return initialized_ && tx_power_dbm_ == dbm;
}
bool setTxPower(int8_t dbm);
mesh::RadioParamApplyResult trySetTxPower(int8_t dbm) {
return setTxPower(dbm) ? mesh::RadioParamApplyResult::APPLIED
return isTxPowerValid(dbm) && setTxPower(dbm) ? mesh::RadioParamApplyResult::APPLIED
: mesh::RadioParamApplyResult::FAILED;
}
};
File diff suppressed because it is too large. Load diff
@@ -139,12 +139,10 @@ public:
}
protected:
#if MESH_ENABLE_TX_BEACONS
bool checkBeaconOutputPower(float frequency_mhz, int8_t dbm) const override {
(void)frequency_mhz;
return dbm >= _min_dbm && dbm <= _max_dbm;
}
#endif
// Emit a gain-control code. Override for a PA driven by PWM or an external
// DAC instead of the MCU's own.
virtual void writeGainControl(uint8_t code) {
+33 -2
View File
@@ -324,8 +324,33 @@ bool RadioLibWrapper::setTxPower(int8_t dbm) {
return trySetTxPower(dbm) == mesh::RadioParamApplyResult::APPLIED;
}
bool RadioLibWrapper::isTxPowerValid(int8_t dbm) const {
#ifdef MIN_LORA_TX_POWER
if (dbm < MIN_LORA_TX_POWER) return false;
#endif
#ifdef MAX_LORA_TX_POWER
if (dbm > MAX_LORA_TX_POWER) return false;
#endif
#if MESH_RADIO_PROFILE_CHIP == 1121 || MESH_RADIO_PROFILE_CHIP == 2021
// These drivers intentionally clip desired power to the physically selected
// band's PA limit. A temporary/radio2 band must not reject saved intent.
return true;
#else
return checkBeaconOutputPower(_cur_freq, dbm);
#endif
}
bool RadioLibWrapper::isTxPowerApplied(int8_t dbm) const {
#if MESH_ENABLE_TX_BEACONS
if (_beacon_owned) return false;
#endif
return !_cw_active && _dbm_valid && _dbm_applied && _cur_dbm == dbm
&& !_profile_repair_required;
}
mesh::RadioParamApplyResult RadioLibWrapper::trySetTxPower(int8_t dbm) {
using Result = mesh::RadioParamApplyResult;
if (!isTxPowerValid(dbm)) return Result::FAILED;
#if MESH_ENABLE_TX_BEACONS
if (_beacon_owned) return Result::BUSY;
#endif
@@ -348,7 +373,8 @@ mesh::RadioParamApplyResult RadioLibWrapper::trySetTxPower(int8_t dbm) {
// Permit meter sweeps through the board's normal power/PA calibration.
// SX127x's power setter enters standby, so re-key without resetting the
// modem or extending the original CW deadline.
if (applyCachedTxPower(dbm) != RADIOLIB_ERR_NONE) {
_dbm_applied = applyCachedTxPower(dbm) == RADIOLIB_ERR_NONE;
if (!_dbm_applied) {
stopCarrierWave();
return Result::FAILED;
}
@@ -882,6 +908,7 @@ bool RadioLibWrapper::validateBeaconPower(float frequency_mhz, int8_t power) con
return isfinite(frequency_mhz) && mesh::isLoRaTxPowerValid(power, frequency_mhz)
&& checkBeaconOutputPower(frequency_mhz, power);
}
#endif
bool RadioLibWrapper::checkBeaconOutputPower(float frequency_mhz, int8_t power) const {
(void)frequency_mhz;
@@ -911,6 +938,7 @@ bool RadioLibWrapper::checkBeaconOutputPower(float frequency_mhz, int8_t power)
#endif
}
#if MESH_ENABLE_TX_BEACONS
uint32_t RadioLibWrapper::getBeaconAirtime(const mesh::RadioProfileParams& p, int length) {
if (length <= 0 || length > MAX_TRANS_UNIT || !validateProfile(p)) return 0;
const uint16_t preamble = p.protocol == mesh::RadioProfileProtocol::Meshtext
@@ -967,6 +995,7 @@ mesh::RadioParamApplyResult RadioLibWrapper::prepareBeaconTransmit(
// an exclusive lease until the ordinary receiver has been restored.
_beacon_restore_profile = _active_profile;
_beacon_owned = true;
_dbm_applied = false;
_beacon_restore_at = 0;
_beacon_cad_timeout = cadScanTimeoutMillis(p.sf, p.bw);
_physical_preamble = p.protocol == mesh::RadioProfileProtocol::Meshtext
@@ -1019,7 +1048,7 @@ mesh::RadioParamApplyResult RadioLibWrapper::restoreBeaconReceive() {
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE
restored = restored && applyProtocol(p.protocol);
#endif
restored = restored && applyCachedTxPower(_cur_dbm) == RADIOLIB_ERR_NONE;
restored = _dbm_applied = restored && applyCachedTxPower(_cur_dbm) == RADIOLIB_ERR_NONE;
endProfileRetune(restored);
if (restored) {
// No RX ran on the beacon tuple, so any remaining event belongs to its
@@ -2163,6 +2192,7 @@ bool RadioLibWrapper::stopCarrierWave() {
if (_morse_rx_owned && _morse_rx) _morse_rx->stop(static_cast<uint32_t>(millis()));
#endif
_cw_stopping = true;
_dbm_applied = false;
// Deassert external TX even if SPI fails. SX127x must also rebuild its LoRa
// modem after FSK direct mode before normal packet work can resume.
const int16_t status = _radio->finishTransmit();
@@ -2184,6 +2214,7 @@ bool RadioLibWrapper::stopCarrierWave() {
restored = status == RADIOLIB_ERR_NONE || standby_status == RADIOLIB_ERR_NONE
? restoreCarrierWaveModem() : restoreAfterDeepInit();
}
_dbm_applied = restored && _dbm_valid;
if (!restored) {
_cw_retry_at = static_cast<uint32_t>(millis()) + 100;
return false; // hold normal work until stopping the carrier is confirmed
+2 -2
View File
@@ -355,11 +355,9 @@ protected:
virtual int16_t applyCachedTxPower(int8_t dbm) {
return _radio->setOutputPower(dbm);
}
#if MESH_ENABLE_TX_BEACONS
// External amplifiers override this in antenna dBm. The public capability
// still checks the board's explicit limits before consulting this hook.
virtual bool checkBeaconOutputPower(float frequency_mhz, int8_t dbm) const;
#endif
virtual bool applyRxBoostedGainMode(bool) { return false; }
#if MESH_ENABLE_CARRIER_TOOLS || !defined(STM32_PLATFORM)
virtual int16_t enterCarrierWave() { return _radio->transmitDirect(); }
@@ -574,6 +572,8 @@ public:
bool setParams(float freq, float bw, uint8_t sf, uint8_t cr,
const uint32_t* rx_ps_timings = NULL);
uint32_t getRngSeed();
bool isTxPowerValid(int8_t dbm) const;
bool isTxPowerApplied(int8_t dbm) const;
bool setTxPower(int8_t dbm);
mesh::RadioParamApplyResult trySetTxPower(int8_t dbm);
+2
View File
@@ -75,6 +75,8 @@ public:
== mesh::RadioParamApplyResult::APPLIED;
}
void powerOff() {}
bool isTxPowerValid(int8_t /*dbm*/) const { return true; }
bool isTxPowerApplied(int8_t /*dbm*/) const { return true; }
bool setTxPower(int8_t /*dbm*/) { return true; }
mesh::RadioParamApplyResult trySetTxPower(int8_t /*dbm*/) {
return mesh::RadioParamApplyResult::APPLIED;