mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-30 00:22:35 +00:00
Defer busy Companion TX-power updates without interrupting RX
A settings save could reach the power setter while RX, TX, duty cycling or a profile transition owned the radio, returning BAD_STATE after earlier settings had already saved. Distinguish busy admission from a hardware rejection and reuse the existing command route, storage and three-second deadline for power. Acknowledge only after hardware acceptance and a successful preference save. Skip hardware work for verified unchanged power. Invalidate that verification after failed writes or radio initialization, retaining rollback and exclusive beacon/Morse ownership. Keep the existing firmware memory limits and features. Cover retries, captured routes, timeout/rollover, disconnect, partial writes, failed saves and recovery. Keep ESP-NOW/simulation adapters compatible and add ESP-NOW Companion compilation to the existing sequential smoke job.
This commit is contained in:
1 parent
af69918094
commit
8dfdba3f6d
17 files changed
+470
-57
No files matched your search
@@ -201,9 +201,11 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Build Environment
|
||||
uses: ./.github/actions/setup-build-environment
|
||||
- name: Compile RP2040 contact storage and Arduino 3 ESP32-C6 room services
|
||||
- name: Compile RP2040 contacts, ESP32-C6 rooms and ESP-NOW Companion
|
||||
# One process: PlatformIO environments share this checkout's build tree.
|
||||
run: pio run -e Xiao_rp2040_companion_radio_usb -e LilyGo_Tlora_C6_room_server_
|
||||
run: >-
|
||||
pio run -e Xiao_rp2040_companion_radio_usb
|
||||
-e LilyGo_Tlora_C6_room_server_ -e Generic_ESPNOW_comp_radio_usb
|
||||
|
||||
management-reports:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -736,6 +738,7 @@ jobs:
|
||||
python3 -B test/test_radio_profile_scan.py
|
||||
python3 -B test/test_radio_profile_clock.py -v
|
||||
python3 -B test/test_radio_profile_repair.py -v
|
||||
python3 -B test/test_radio_tx_power_transaction.py -v
|
||||
python3 -B test/test_radio_profile_runtime_recommendation.py -v
|
||||
python3 -B test/test_radio_profile_idle_dispatcher.py -v
|
||||
python3 -B test/test_radio_rxps_rejection.py -v
|
||||
|
||||
@@ -246,6 +246,12 @@ catalog; bytes `0x2C`-`0x31` are parked and `0x35` is unused.
|
||||
The sections below detail the most common frames. Refer to the source named
|
||||
above for command bodies that are not expanded here.
|
||||
|
||||
Radio changes (`0x0B`, `0x0C`) can wait briefly for a safe radio window before
|
||||
replying. TX power waits up to three seconds when reception or transmission is
|
||||
busy. Its OK response confirms both hardware acceptance and a saved setting;
|
||||
timeout or disconnect discards a waiting power change. Successful settings
|
||||
commands are saved independently, so an error does not undo earlier commands.
|
||||
|
||||
### Private text when only the sender knows the recipient
|
||||
|
||||
Clients continue to send an ordinary `CMD_SEND_TXT_MSG` (`0x02`) with
|
||||
|
||||
@@ -3546,12 +3546,24 @@ void MyMesh::finishRadioParamApply(float freq, float bw, uint8_t sf,
|
||||
writeOKFrame(route);
|
||||
}
|
||||
|
||||
void MyMesh::deferRadioCommand(uint8_t kind) {
|
||||
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_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;
|
||||
@@ -3569,6 +3581,22 @@ void MyMesh::servicePendingRadioParamApply() {
|
||||
// Leave enough queue headroom for the eventual command response.
|
||||
if (_serial->isReplyRouteWriteBusy(command_radio_reply_route)) return;
|
||||
|
||||
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) {
|
||||
@@ -3585,8 +3613,7 @@ void MyMesh::servicePendingRadioParamApply() {
|
||||
} else if (result == mesh::RadioParamApplyResult::FAILED) {
|
||||
writeErrFrame(ERR_CODE_ILLEGAL_ARG, command_radio_reply_route);
|
||||
cancelPendingRadioParamApply();
|
||||
} else if (command_radio_apply_deadline == _ms->getMillis()
|
||||
|| millisHasNowPassed(command_radio_apply_deadline)) {
|
||||
} else if (expired) {
|
||||
writeErrFrame(ERR_CODE_BAD_STATE, command_radio_reply_route);
|
||||
cancelPendingRadioParamApply();
|
||||
}
|
||||
@@ -3608,7 +3635,7 @@ bool MyMesh::scheduleTempRadio(float freq, float bw, uint8_t sf, uint8_t cr,
|
||||
uint32_t timeout_mins, char* reply,
|
||||
size_t reply_size) {
|
||||
if (command_radio_apply_pending) {
|
||||
snprintf(reply, reply_size, "ERR companion radio change is already pending");
|
||||
snprintf(reply, reply_size, "ERR radio change pending");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -5648,13 +5675,13 @@ void MyMesh::execCommand(char* cmd, char* reply) {
|
||||
if (strcmp(key, "tx") == 0) {
|
||||
long parsed;
|
||||
if (!wcParseLong(value, -9, MAX_LORA_TX_POWER, parsed)) {
|
||||
snprintf(reply, 160, "Error: TX power must be -9 to %d", MAX_LORA_TX_POWER);
|
||||
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"
|
||||
: result == RadioSettingResult::RadioRejected
|
||||
: result != RadioSettingResult::SaveFailed
|
||||
? "Error: radio busy or TX power rejected"
|
||||
: "Error: TX power could not be saved");
|
||||
: "Error: TX not saved");
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -6767,18 +6794,12 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
// Capture this requester instead of relying on the mutable last-RX
|
||||
// route. This remains correct if a contact stream later unlocks or a
|
||||
// different transport supplies another command before completion.
|
||||
command_radio_reply_route = _serial->captureReplyRoute();
|
||||
if (command_radio_reply_route == NULL) {
|
||||
writeErrFrame(ERR_CODE_BAD_STATE);
|
||||
return;
|
||||
}
|
||||
command_radio_apply_pending = true;
|
||||
command_radio_freq = new_freq;
|
||||
command_radio_bw = new_bw;
|
||||
command_radio_sf = sf;
|
||||
command_radio_cr = cr;
|
||||
command_radio_repeat = repeat;
|
||||
command_radio_apply_deadline = futureMillis(COMMAND_RADIO_APPLY_TIMEOUT_MS);
|
||||
deferRadioCommand(1);
|
||||
MESH_DEBUG_PRINTLN("Deferred CMD_SET_RADIO_PARAMS while radio is busy");
|
||||
return;
|
||||
}
|
||||
@@ -6806,8 +6827,15 @@ 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);
|
||||
@@ -8451,10 +8479,15 @@ bool MyMesh::saveAdvertLocation(double latitude, double longitude) {
|
||||
return false;
|
||||
}
|
||||
|
||||
MyMesh::RadioSettingResult MyMesh::applyAndSaveTxPower(int8_t power) {
|
||||
MyMesh::RadioSettingResult MyMesh::applyAndSaveTxPower(int8_t power, bool from_pending) {
|
||||
if (command_radio_apply_pending && !from_pending) return RadioSettingResult::RadioBusy;
|
||||
const int8_t previous = _prefs.tx_power_dbm;
|
||||
if (_radio_available && !radio_driver.setTxPower(power)) {
|
||||
return RadioSettingResult::RadioRejected;
|
||||
if (_radio_available) {
|
||||
const auto result = radio_driver.trySetTxPower(power);
|
||||
if (result != mesh::RadioParamApplyResult::APPLIED) {
|
||||
return result == mesh::RadioParamApplyResult::BUSY
|
||||
? RadioSettingResult::RadioBusy : RadioSettingResult::RadioRejected;
|
||||
}
|
||||
}
|
||||
_prefs.tx_power_dbm = power;
|
||||
if (savePrefs()) return RadioSettingResult::Saved;
|
||||
@@ -10924,15 +10957,15 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
|
||||
if (!mesh::cli::parseIntegerStrict(command + 7, parsed)) {
|
||||
mesh::cli::formatError(reply, reply_capacity, "invalid TX power");
|
||||
} else if (parsed < -9 || parsed > MAX_LORA_TX_POWER) {
|
||||
snprintf(reply, reply_capacity, "Error: TX power must be -9 to %d",
|
||||
snprintf(reply, reply_capacity, "Error: TX -9..%d dBm",
|
||||
MAX_LORA_TX_POWER);
|
||||
} else {
|
||||
const int8_t requested = static_cast<int8_t>(parsed);
|
||||
const auto result = applyAndSaveTxPower(requested);
|
||||
if (result == RadioSettingResult::RadioRejected) {
|
||||
if (result == RadioSettingResult::RadioRejected || result == RadioSettingResult::RadioBusy) {
|
||||
mesh::cli::formatError(reply, reply_capacity, "radio busy or TX power rejected");
|
||||
} else if (result == RadioSettingResult::SaveFailed) {
|
||||
mesh::cli::formatError(reply, reply_capacity, "TX power could not be saved");
|
||||
mesh::cli::formatError(reply, reply_capacity, "TX not saved");
|
||||
} else {
|
||||
strcpy(reply, "OK");
|
||||
}
|
||||
|
||||
@@ -675,8 +675,8 @@ private:
|
||||
bool applyAndSaveFemRxGain(bool enabled);
|
||||
bool applyAndSaveFemTxGain(bool enabled);
|
||||
bool applyAndSaveRxBoostedGain(bool enabled);
|
||||
enum class RadioSettingResult { Saved, RadioRejected, SaveFailed };
|
||||
RadioSettingResult applyAndSaveTxPower(int8_t power);
|
||||
enum class RadioSettingResult { Saved, RadioRejected, SaveFailed, RadioBusy };
|
||||
RadioSettingResult applyAndSaveTxPower(int8_t power, bool from_pending = false);
|
||||
bool handleCadCommand(const char* command, char* reply, size_t reply_size);
|
||||
bool saveBluetoothNameOverride(const char* name);
|
||||
bool applyAndSaveBluetoothName(const char* value, char* reply,
|
||||
@@ -749,6 +749,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 cancelPendingRadioParamApply();
|
||||
void servicePendingRadioParamApply();
|
||||
void servicePendingSerialReply();
|
||||
@@ -912,7 +913,8 @@ private:
|
||||
bool _radio_available;
|
||||
unsigned long radio_apply_retry_at;
|
||||
uint8_t radio_apply_failures;
|
||||
bool command_radio_apply_pending;
|
||||
// 0: none, 1: modulation tuple, 2: TX power (stored in the repeat byte).
|
||||
uint8_t command_radio_apply_pending;
|
||||
float command_radio_freq;
|
||||
float command_radio_bw;
|
||||
uint8_t command_radio_sf;
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
#include "CompanionBluetooth.h"
|
||||
#include "CompanionWireless.h"
|
||||
#include "CompanionWiFi.h"
|
||||
#if defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
#if COMPANION_FEATURE_USB_MOTA_SOURCE || COMPANION_FEATURE_BLE_MOTA_SOURCE
|
||||
#include <helpers/ota/MotaSourceSerial.h>
|
||||
#include <helpers/ota/OtaContext.h>
|
||||
|
||||
@@ -69,6 +69,10 @@ public:
|
||||
|
||||
uint32_t intID();
|
||||
bool setTxPower(int8_t dbm);
|
||||
mesh::RadioParamApplyResult trySetTxPower(int8_t dbm) {
|
||||
return setTxPower(dbm) ? mesh::RadioParamApplyResult::APPLIED
|
||||
: mesh::RadioParamApplyResult::FAILED;
|
||||
}
|
||||
};
|
||||
|
||||
#if ESPNOW_DEBUG_LOGGING && ARDUINO
|
||||
|
||||
@@ -243,6 +243,7 @@ void RadioLibWrapper::begin() {
|
||||
#if MESH_ENABLE_TX_BEACONS
|
||||
if (_beacon_owned) return;
|
||||
#endif
|
||||
_dbm_applied = false;
|
||||
#if MESH_HIL_PROFILE_TIMING
|
||||
mesh::profiletiming::initializeClock();
|
||||
#endif
|
||||
@@ -320,8 +321,13 @@ uint32_t RadioLibWrapper::getRngSeed() {
|
||||
}
|
||||
|
||||
bool RadioLibWrapper::setTxPower(int8_t dbm) {
|
||||
return trySetTxPower(dbm) == mesh::RadioParamApplyResult::APPLIED;
|
||||
}
|
||||
|
||||
mesh::RadioParamApplyResult RadioLibWrapper::trySetTxPower(int8_t dbm) {
|
||||
using Result = mesh::RadioParamApplyResult;
|
||||
#if MESH_ENABLE_TX_BEACONS
|
||||
if (_beacon_owned) return false;
|
||||
if (_beacon_owned) return Result::BUSY;
|
||||
#endif
|
||||
#if MESH_ENABLE_CARRIER_TOOLS || MESH_ENABLE_MORSE_RX
|
||||
// A CW power-meter sweep may re-key the carrier. During Morse that would
|
||||
@@ -331,37 +337,42 @@ bool RadioLibWrapper::setTxPower(int8_t dbm) {
|
||||
#if MESH_ENABLE_MORSE_RX
|
||||
|| _morse_rx_owned
|
||||
#endif
|
||||
) && _cw_active) return false;
|
||||
) && _cw_active) return Result::BUSY;
|
||||
#if MESH_ENABLE_CARRIER_TOOLS
|
||||
if (_cw_active) {
|
||||
if (_cw_stopping) return false;
|
||||
if (_cw_stopping) return Result::BUSY;
|
||||
#if defined(MESH_HIL_TX_FREQ_MHZ)
|
||||
if (!hilPacketTransmitAllowed() || _cw_profile != _active_profile
|
||||
|| _cw_generation != _profile_generation) return false;
|
||||
|| _cw_generation != _profile_generation) return Result::FAILED;
|
||||
#endif
|
||||
// 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) {
|
||||
stopCarrierWave();
|
||||
return false;
|
||||
return Result::FAILED;
|
||||
}
|
||||
if (_radio->transmitDirect() != RADIOLIB_ERR_NONE) {
|
||||
stopCarrierWave();
|
||||
return false;
|
||||
return Result::FAILED;
|
||||
}
|
||||
_cur_dbm = dbm;
|
||||
_dbm_valid = true;
|
||||
return true;
|
||||
return Result::APPLIED;
|
||||
}
|
||||
#else
|
||||
if (_cw_active) return false;
|
||||
if (_cw_active) return Result::BUSY;
|
||||
#endif
|
||||
#endif
|
||||
// Settings screens also submit TX power when only another field changed.
|
||||
// The verified normal power needs no SPI, standby, or interruption of RX.
|
||||
if (_dbm_valid && _dbm_applied && _cur_dbm == dbm
|
||||
&& !_profile_repair_required) return Result::APPLIED;
|
||||
const uint8_t resume_rx = beginReconfigure();
|
||||
if (resume_rx > 1) return false;
|
||||
if (resume_rx > 1) return resume_rx == 2 ? Result::BUSY : Result::FAILED;
|
||||
|
||||
const int16_t status = applyCachedTxPower(dbm);
|
||||
_dbm_applied = status == RADIOLIB_ERR_NONE;
|
||||
if (status == RADIOLIB_ERR_NONE) {
|
||||
_cur_dbm = dbm;
|
||||
_dbm_valid = true;
|
||||
@@ -370,7 +381,7 @@ bool RadioLibWrapper::setTxPower(int8_t dbm) {
|
||||
(int)dbm, (int)status);
|
||||
}
|
||||
endReconfigure(resume_rx);
|
||||
return status == RADIOLIB_ERR_NONE;
|
||||
return status == RADIOLIB_ERR_NONE ? Result::APPLIED : Result::FAILED;
|
||||
}
|
||||
|
||||
uint8_t RadioLibWrapper::beginReconfigure() {
|
||||
@@ -516,6 +527,7 @@ bool RadioLibWrapper::restoreAfterDeepInit() {
|
||||
#if MESH_ENABLE_TX_BEACONS
|
||||
if (_beacon_owned) cacheBeaconRestoreParams();
|
||||
#endif
|
||||
_dbm_applied = false;
|
||||
if (!radioDeepInit()) return false;
|
||||
|
||||
_rx_ps_armed = false;
|
||||
@@ -533,7 +545,8 @@ bool RadioLibWrapper::restoreAfterDeepInit() {
|
||||
restored = restored && applyProtocol(_physical_protocol);
|
||||
#endif
|
||||
if (_dbm_valid) {
|
||||
restored = applyCachedTxPower(_cur_dbm) == RADIOLIB_ERR_NONE && restored;
|
||||
_dbm_applied = applyCachedTxPower(_cur_dbm) == RADIOLIB_ERR_NONE;
|
||||
restored = _dbm_applied && restored;
|
||||
}
|
||||
if (_rx_boosted_gain_valid) {
|
||||
restored = applyRxBoostedGainMode(_cur_rx_boosted_gain) && restored;
|
||||
|
||||
@@ -108,6 +108,7 @@ protected:
|
||||
int8_t _cur_dbm;
|
||||
bool _cur_rx_boosted_gain;
|
||||
bool _params_valid, _dbm_valid, _rx_boosted_gain_valid;
|
||||
bool _dbm_applied = false;
|
||||
mesh::RadioProfiles _profiles;
|
||||
uint8_t _active_profile = 0;
|
||||
uint16_t _physical_preamble = 0;
|
||||
@@ -574,6 +575,7 @@ public:
|
||||
const uint32_t* rx_ps_timings = NULL);
|
||||
uint32_t getRngSeed();
|
||||
bool setTxPower(int8_t dbm);
|
||||
mesh::RadioParamApplyResult trySetTxPower(int8_t dbm);
|
||||
|
||||
virtual float getCurrentRSSI() =0;
|
||||
virtual uint8_t getSpreadingFactor() const { return LORA_SF; }
|
||||
|
||||
@@ -76,6 +76,9 @@ public:
|
||||
}
|
||||
void powerOff() {}
|
||||
bool setTxPower(int8_t /*dbm*/) { return true; }
|
||||
mesh::RadioParamApplyResult trySetTxPower(int8_t /*dbm*/) {
|
||||
return mesh::RadioParamApplyResult::APPLIED;
|
||||
}
|
||||
uint32_t getRngSeed() { return _simRandom(); }
|
||||
uint32_t getPacketsRecv() const { return n_recv; }
|
||||
uint32_t getPacketsSent() const { return n_sent; }
|
||||
|
||||
@@ -75,7 +75,7 @@ struct LLCC68 : PhysicalLayer { int16_t setCRC(uint8_t length) { return setCrc(l
|
||||
struct RadioLibWrapper {
|
||||
PhysicalLayer* _radio;
|
||||
mesh::MainBoard* _board;
|
||||
bool _params_valid = true, _rx_ps_armed = true, _dbm_valid = false, _rx_boosted_gain_valid = false;
|
||||
bool _params_valid = true, _rx_ps_armed = true, _dbm_valid = false, _dbm_applied = false, _rx_boosted_gain_valid = false;
|
||||
bool _profile_repair_required = false;
|
||||
int state = 1;
|
||||
float _cur_freq = 906.875f, _cur_bw = 250;
|
||||
|
||||
@@ -81,7 +81,7 @@ struct RadioLibWrapper {
|
||||
float _cur_freq = 906, _cur_bw = 62.5;
|
||||
uint8_t _cur_sf = 7, _cur_cr = 5;
|
||||
int8_t _cur_dbm = 20;
|
||||
bool _params_valid = true, _dbm_valid = true;
|
||||
bool _params_valid = true, _dbm_valid = true, _dbm_applied = true;
|
||||
bool _cw_active = false, pending = false, packet = false, busy = false, calibrating = false;
|
||||
bool _rx_ps_enabled = false, _rx_ps_armed = false, _rx_hold_continuous = false;
|
||||
bool _rx_ps_continuous_fallback = false, _nf_calib_active = false;
|
||||
@@ -190,6 +190,15 @@ mesh::RadioProfileParams beacon(bool meshtext = false) {
|
||||
}
|
||||
int main() {
|
||||
unsigned cases = 0;
|
||||
{
|
||||
RadioLibWrapper w; w.deep_ok = false;
|
||||
assert(!w.restoreAfterDeepInit() && !w._dbm_applied);
|
||||
w.deep_ok = true; w.power_fail = true;
|
||||
assert(!w.restoreAfterDeepInit() && !w._dbm_applied);
|
||||
w.power_fail = false;
|
||||
assert(w.restoreAfterDeepInit() && w._dbm_applied);
|
||||
++cases;
|
||||
}
|
||||
for (bool dual : {false, true}) for (bool rxps : {false, true}) for (bool mt : {false, true}) {
|
||||
RadioLibWrapper w; now_ms = 100;
|
||||
if (dual) {
|
||||
|
||||
@@ -159,7 +159,7 @@ struct RadioLibWrapper : mesh::Radio {
|
||||
float frequency=909.5;
|
||||
float _cur_freq=909.5;
|
||||
int8_t _cur_dbm=17;
|
||||
bool _dbm_valid=true;
|
||||
bool _dbm_valid=true, _dbm_applied=true;
|
||||
RadioLibWrapper() {
|
||||
g_mock_millis=100; state=STATE_RX;
|
||||
_profiles.primary.freq=909.5;
|
||||
@@ -193,6 +193,7 @@ struct RadioLibWrapper : mesh::Radio {
|
||||
}
|
||||
virtual float getCurrentRSSI() { ++rssi_reads; g_mock_millis+=rssi_latency; return rssi; }
|
||||
bool setTxPower(int8_t dbm);
|
||||
mesh::RadioParamApplyResult trySetTxPower(int8_t dbm);
|
||||
@NATIVE_HOOKS@
|
||||
#if MESH_ENABLE_MORSE_RX || !defined(STM32_PLATFORM)
|
||||
@RX_HOOKS@
|
||||
@@ -230,9 +231,11 @@ struct RadioLibWrapper : mesh::Radio {
|
||||
}
|
||||
bool restoreAfterDeepInit() {
|
||||
++resets;
|
||||
_dbm_applied=false;
|
||||
if (reset_ok) {
|
||||
chip.carrier=false; chip.fsk=false; chip.stop_result=chip.standby_result=0;
|
||||
return applyCachedTxPower(_cur_dbm)==0;
|
||||
_dbm_applied=applyCachedTxPower(_cur_dbm)==0;
|
||||
return _dbm_applied;
|
||||
}
|
||||
return reset_ok;
|
||||
}
|
||||
@@ -455,6 +458,7 @@ def production_harness():
|
||||
source = (ROOT/'src/helpers/radiolib/RadioLibWrappers.cpp').read_text()
|
||||
signatures = ('uint8_t RadioLibWrapper::beginReconfigure(',
|
||||
'bool RadioLibWrapper::setTxPower(',
|
||||
'mesh::RadioParamApplyResult RadioLibWrapper::trySetTxPower(',
|
||||
'mesh::RadioParamApplyResult RadioLibWrapper::setCarrierWave(',
|
||||
'uint32_t RadioLibWrapper::carrierWaveRemainingMillis(',
|
||||
'bool RadioLibWrapper::stopCarrierWave(',
|
||||
|
||||
@@ -52,14 +52,15 @@ void recalcRxPowerSavingFromLevel(uint8_t,uint8_t sf,float bw,uint8_t,
|
||||
struct Driver {
|
||||
int8_t power=3;
|
||||
unsigned calls=0;
|
||||
std::deque<bool> accepted;
|
||||
bool setTxPower(int8_t requested) {
|
||||
std::deque<Apply> accepted;
|
||||
Apply trySetTxPower(int8_t requested) {
|
||||
++calls;
|
||||
bool ok=true;
|
||||
if (!accepted.empty()) { ok=accepted.front(); accepted.pop_front(); }
|
||||
if (ok) power=requested;
|
||||
return ok;
|
||||
Apply result=Apply::APPLIED;
|
||||
if (!accepted.empty()) { result=accepted.front(); accepted.pop_front(); }
|
||||
if (result==Apply::APPLIED) power=requested;
|
||||
return result;
|
||||
}
|
||||
bool setTxPower(int8_t requested) { return trySetTxPower(requested)==Apply::APPLIED; }
|
||||
bool supportsRxBoostedGainMode() const { return false; }
|
||||
bool setRxBoostedGainMode(bool) { return true; }
|
||||
bool supportsRxPowerSaving() const { return false; }
|
||||
@@ -74,7 +75,8 @@ struct MyMesh {
|
||||
bool storage_accepts=true, _radio_available=true, outbound=false;
|
||||
unsigned saves=0, applies=0;
|
||||
std::deque<Apply> accepted;
|
||||
bool saved_radio_apply_pending=false, command_radio_apply_pending=false;
|
||||
bool saved_radio_apply_pending=false;
|
||||
uint8_t command_radio_apply_pending=0;
|
||||
uint32_t radio_apply_retry_at=0, command_radio_apply_deadline=0;
|
||||
uint8_t radio_apply_failures=0;
|
||||
float command_radio_freq=0, command_radio_bw=0;
|
||||
@@ -100,8 +102,9 @@ struct MyMesh {
|
||||
return result;
|
||||
}
|
||||
bool applySavedRadioParams();
|
||||
RadioSettingResult applyAndSaveTxPower(int8_t);
|
||||
RadioSettingResult applyAndSaveTxPower(int8_t,bool from_pending=false);
|
||||
void finishRadioParamApply(float,float,uint8_t,uint8_t,uint8_t,BaseSerialInterface* = nullptr);
|
||||
void deferRadioCommand(uint8_t);
|
||||
void servicePendingRadioParamApply();
|
||||
void cancelPendingRadioParamApply();
|
||||
void frame(size_t len) { @FRAME_BRANCHES@ }
|
||||
@@ -122,6 +125,11 @@ struct MyMesh {
|
||||
frame(12);
|
||||
}
|
||||
void expectError(uint8_t code,BaseSerialInterface* route=nullptr) {
|
||||
if(replies.size()!=1 || replies[0].error!=code) {
|
||||
fprintf(stderr,"Expected reply %u; count=%zu first=%u pending=%u saves=%u now=%u\n",
|
||||
unsigned(code),replies.size(),replies.empty()?255u:unsigned(replies[0].error),
|
||||
unsigned(command_radio_apply_pending),saves,unsigned(now_ms));
|
||||
}
|
||||
assert(replies.size()==1 && replies[0].error==code);
|
||||
assert(replies[0].route==(route?route:&original));
|
||||
}
|
||||
@@ -133,11 +141,12 @@ struct MyMesh {
|
||||
};
|
||||
@METHODS@
|
||||
int main() {
|
||||
// Reject unavailable/busy hardware before either persisting or reporting it.
|
||||
// A real hardware rejection remains an immediate error, without a save.
|
||||
{
|
||||
MyMesh m;radio_driver.accepted={false};m.powerFrame(17);
|
||||
MyMesh m;radio_driver.accepted={Apply::FAILED};m.powerFrame(17);
|
||||
m.expectError(ERR_CODE_BAD_STATE);
|
||||
assert(m.saves==0 && m._prefs.tx_power_dbm==3 && radio_driver.power==3);
|
||||
assert(!m.command_radio_apply_pending && m.saves==0);
|
||||
assert(m._prefs.tx_power_dbm==3 && m.durable.tx_power_dbm==3 && radio_driver.power==3);
|
||||
}
|
||||
for (int value : {-10,23}) {
|
||||
MyMesh m;m.powerFrame(uint8_t(value));m.expectError(ERR_CODE_ILLEGAL_ARG);
|
||||
@@ -155,6 +164,137 @@ int main() {
|
||||
MyMesh m;m._radio_available=false;m.powerFrame(17);m.expectError(0);
|
||||
assert(m.durable.tx_power_dbm==17 && radio_driver.calls==0);
|
||||
}
|
||||
// App settings can reach power while an RX packet is in progress. Keep the
|
||||
// original requester and acknowledge only after the hardware and save pass.
|
||||
for (int power : {-9,17,22}) {
|
||||
MyMesh m;radio_driver.accepted={Apply::BUSY,Apply::BUSY,Apply::APPLIED};
|
||||
m.powerFrame(uint8_t(power));
|
||||
assert(m.replies.empty() && m.command_radio_apply_pending==2 && m.saves==0);
|
||||
assert(m.command_radio_reply_route==&m.original);
|
||||
assert(int8_t(m.command_radio_repeat)==power);
|
||||
assert(m.command_radio_apply_deadline==now_ms+COMMAND_RADIO_APPLY_TIMEOUT_MS);
|
||||
assert(m._prefs.tx_power_dbm==3 && m.durable.tx_power_dbm==3 && radio_driver.power==3);
|
||||
m.serial.current=&m.other;
|
||||
m.servicePendingRadioParamApply();
|
||||
assert(m.replies.empty() && m.command_radio_apply_pending==2 && m.saves==0);
|
||||
m.servicePendingRadioParamApply();m.expectError(0);
|
||||
assert(!m.command_radio_apply_pending && m.saves==1);
|
||||
assert(m.command_radio_reply_route==nullptr && m.command_radio_apply_deadline==0);
|
||||
assert(m._prefs.tx_power_dbm==power && m.durable.tx_power_dbm==power);
|
||||
assert(radio_driver.power==power);
|
||||
}
|
||||
// Reproduce the app's multi-command settings save: the tuple is durable
|
||||
// before the following power command encounters a temporary receive lease.
|
||||
{
|
||||
MyMesh m;m.tupleFrame();m.expectError(0);m.replies.clear();
|
||||
radio_driver.accepted={Apply::BUSY,Apply::APPLIED};m.powerFrame(17);
|
||||
assert(m.replies.empty() && m.command_radio_apply_pending==2 && m.saves==1);
|
||||
assert(m.durable.freq==920 && m.durable.tx_power_dbm==3);
|
||||
m.servicePendingRadioParamApply();m.expectError(0);
|
||||
assert(m.saves==2 && m.durable.freq==920 && m.durable.tx_power_dbm==17);
|
||||
}
|
||||
// Wait for reply-queue headroom before applying or committing the candidate.
|
||||
{
|
||||
MyMesh m;radio_driver.accepted={Apply::BUSY,Apply::APPLIED};m.powerFrame(17);
|
||||
m.serial.current=&m.other;m.serial.busy=true;
|
||||
const unsigned calls=radio_driver.calls;
|
||||
m.servicePendingRadioParamApply();
|
||||
assert(m.replies.empty() && m.saves==0 && radio_driver.calls==calls);
|
||||
assert(m.command_radio_apply_pending==2 && m.durable.tx_power_dbm==3);
|
||||
m.serial.busy=false;m.servicePendingRadioParamApply();m.expectError(0);
|
||||
assert(!m.command_radio_apply_pending && m.durable.tx_power_dbm==17);
|
||||
}
|
||||
// A second settings request cannot replace the pending candidate or route.
|
||||
for (bool tuple_request : {false,true}) {
|
||||
MyMesh m;radio_driver.accepted={Apply::BUSY,Apply::APPLIED};m.powerFrame(17);
|
||||
m.serial.current=&m.other;
|
||||
if(tuple_request)m.tupleFrame();else m.powerFrame(12);
|
||||
m.expectError(ERR_CODE_BAD_STATE,&m.other);
|
||||
assert(m.command_radio_apply_pending==2 && m.command_radio_repeat==17);
|
||||
assert(m.command_radio_reply_route==&m.original && m.saves==0 && radio_driver.calls==1);
|
||||
char reply[160]={};
|
||||
assert(m.command("set tx 12",reply) && strstr(reply,"Error"));
|
||||
assert(m.command_radio_apply_pending==2 && m.command_radio_repeat==17);
|
||||
assert(m.saves==0 && radio_driver.calls==1 && m._prefs.tx_power_dbm==3);
|
||||
m.replies.clear();m.servicePendingRadioParamApply();m.expectError(0);
|
||||
assert(m.durable.tx_power_dbm==17);
|
||||
}
|
||||
// The power request also respects a tuple already occupying the shared slot.
|
||||
{
|
||||
MyMesh m;m.accepted={Apply::BUSY,Apply::APPLIED};m.tupleFrame();
|
||||
assert(m.command_radio_apply_pending==1);
|
||||
m.serial.current=&m.other;m.powerFrame(17);m.expectError(ERR_CODE_BAD_STATE,&m.other);
|
||||
assert(m.command_radio_apply_pending==1 && radio_driver.calls==0 && m.saves==0);
|
||||
m.replies.clear();m.servicePendingRadioParamApply();m.expectError(0);
|
||||
assert(m.durable.freq==920 && m.durable.tx_power_dbm==3);
|
||||
}
|
||||
// Losing the original route cancels the unsaved power, even when another
|
||||
// transport supplies commands before the deferred operation is serviced.
|
||||
{
|
||||
MyMesh m;radio_driver.accepted={Apply::BUSY,Apply::APPLIED};m.powerFrame(17);
|
||||
m.serial.current=&m.other;m.serial.disconnected=&m.original;
|
||||
m.servicePendingRadioParamApply();
|
||||
assert(!m.command_radio_apply_pending && m.replies.empty() && m.saves==0);
|
||||
assert(!m.saved_radio_apply_pending);
|
||||
assert(m._prefs.tx_power_dbm==3 && m.durable.tx_power_dbm==3 && radio_driver.power==3);
|
||||
assert(radio_driver.calls==1);
|
||||
}
|
||||
{
|
||||
MyMesh m;m.serial.current=nullptr;radio_driver.accepted={Apply::BUSY};m.powerFrame(17);
|
||||
assert(m.replies.size()==1 && m.replies[0].error==ERR_CODE_BAD_STATE);
|
||||
assert(!m.command_radio_apply_pending && m.saves==0 && radio_driver.power==3);
|
||||
}
|
||||
// Sustained busy hardware times out once on the captured route. Check both
|
||||
// the ordinary clock and a deadline that crosses the millis rollover.
|
||||
for (uint32_t start : {100u,UINT32_MAX-1000u}) {
|
||||
MyMesh m;now_ms=start;
|
||||
radio_driver.accepted={Apply::BUSY,Apply::BUSY,Apply::BUSY};m.powerFrame(17);
|
||||
m.serial.current=&m.other;
|
||||
const uint32_t deadline=m.command_radio_apply_deadline;
|
||||
now_ms=deadline-1;m.servicePendingRadioParamApply();
|
||||
assert(m.replies.empty() && m.command_radio_apply_pending==2 && m.saves==0);
|
||||
now_ms=deadline;m.servicePendingRadioParamApply();m.expectError(ERR_CODE_BAD_STATE);
|
||||
assert(!m.command_radio_apply_pending && !m.saved_radio_apply_pending && m.saves==0);
|
||||
assert(m._prefs.tx_power_dbm==3 && m.durable.tx_power_dbm==3 && radio_driver.power==3);
|
||||
m.servicePendingRadioParamApply();assert(m.replies.size()==1);
|
||||
}
|
||||
// Expiry must not commit a late success, including when queue backpressure
|
||||
// prevented the timeout reply until after its original deadline.
|
||||
for (bool blocked_queue : {false,true}) {
|
||||
MyMesh m;radio_driver.accepted={Apply::BUSY,Apply::APPLIED};m.powerFrame(17);
|
||||
m.serial.current=&m.other;now_ms=m.command_radio_apply_deadline;
|
||||
if(blocked_queue) {
|
||||
m.serial.busy=true;m.servicePendingRadioParamApply();
|
||||
assert(m.replies.empty() && m.saves==0 && radio_driver.calls==1);
|
||||
m.serial.busy=false;++now_ms;
|
||||
}
|
||||
m.servicePendingRadioParamApply();m.expectError(ERR_CODE_BAD_STATE);
|
||||
assert(!m.command_radio_apply_pending && !m.saved_radio_apply_pending);
|
||||
assert(m.saves==0 && radio_driver.calls==1);
|
||||
assert(m.durable.tx_power_dbm==3 && radio_driver.power==3);
|
||||
}
|
||||
// A later hardware rejection must not be mistaken for another busy retry.
|
||||
{
|
||||
MyMesh m;radio_driver.accepted={Apply::BUSY,Apply::FAILED};m.powerFrame(17);
|
||||
m.serial.current=&m.other;m.servicePendingRadioParamApply();m.expectError(ERR_CODE_BAD_STATE);
|
||||
assert(!m.command_radio_apply_pending && !m.saved_radio_apply_pending && m.saves==0);
|
||||
assert(m.durable.tx_power_dbm==3 && radio_driver.power==3);
|
||||
}
|
||||
// Deferred commits retain the same storage-failure rollback as immediate
|
||||
// requests, and cannot send a later success after reporting the failure.
|
||||
for (bool rollback_busy : {false,true}) {
|
||||
MyMesh m;m.storage_accepts=false;
|
||||
radio_driver.accepted={Apply::BUSY,Apply::APPLIED,
|
||||
rollback_busy?Apply::BUSY:Apply::APPLIED,Apply::APPLIED};
|
||||
m.powerFrame(17);assert(m.replies.empty() && m.saves==0);
|
||||
m.serial.current=&m.other;m.servicePendingRadioParamApply();m.expectError(ERR_CODE_FILE_IO_ERROR);
|
||||
assert(!m.command_radio_apply_pending && m.saves==1);
|
||||
assert(m._prefs.tx_power_dbm==3 && m.durable.tx_power_dbm==3);
|
||||
assert(m.saved_radio_apply_pending==rollback_busy);
|
||||
if(rollback_busy)m.recover();
|
||||
assert(!m.saved_radio_apply_pending && radio_driver.power==3 && m.replies.size()==1);
|
||||
m.servicePendingRadioParamApply();assert(m.replies.size()==1);
|
||||
}
|
||||
// A failed commit restores RAM and the physical output-power setting.
|
||||
{
|
||||
MyMesh m;m.storage_accepts=false;m.powerFrame(17);m.expectError(ERR_CODE_FILE_IO_ERROR);
|
||||
@@ -164,7 +304,8 @@ int main() {
|
||||
// RX may become busy during flash I/O. Retain recovery through a rejected
|
||||
// rollback and through a later successful tuple restore but failed power apply.
|
||||
{
|
||||
MyMesh m;m.storage_accepts=false;radio_driver.accepted={true,false,false,true};
|
||||
MyMesh m;m.storage_accepts=false;
|
||||
radio_driver.accepted={Apply::APPLIED,Apply::BUSY,Apply::FAILED,Apply::APPLIED};
|
||||
m.powerFrame(17);m.expectError(ERR_CODE_FILE_IO_ERROR);
|
||||
assert(m._prefs.tx_power_dbm==3 && radio_driver.power==17 && m.saved_radio_apply_pending);
|
||||
m.recover();assert(m.saved_radio_apply_pending && radio_driver.power==17);
|
||||
@@ -176,7 +317,8 @@ int main() {
|
||||
}
|
||||
// ASCII and framed setters share identical hardware/durability semantics.
|
||||
{
|
||||
MyMesh m;char reply[160]={};radio_driver.accepted={false};
|
||||
MyMesh m;char reply[160]={};radio_driver.accepted={Apply::FAILED,Apply::BUSY};
|
||||
assert(m.command("set tx 17",reply) && strstr(reply,"Error"));
|
||||
assert(m.command("set tx 17",reply) && strstr(reply,"Error"));
|
||||
assert(m.saves==0 && m._prefs.tx_power_dbm==3);
|
||||
m.storage_accepts=false;
|
||||
@@ -253,7 +395,8 @@ struct ContactStore {
|
||||
struct MyMesh {
|
||||
ContactStore store;
|
||||
ContactStore* _store=&store;
|
||||
bool _radio_available=true, _iter_started=false, command_radio_apply_pending=false;
|
||||
bool _radio_available=true, _iter_started=false;
|
||||
uint8_t command_radio_apply_pending=0;
|
||||
bool saved_radio_apply_pending=false, _terminal_trace_pending=false;
|
||||
uint32_t radio_apply_retry_at=0, _scheduled_reboot_at=0;
|
||||
uint32_t emergency_client_repeat_send_at=0, dirty_contacts_expiry=0;
|
||||
@@ -283,6 +426,11 @@ struct MyMesh {
|
||||
int main() {
|
||||
MyMesh m;
|
||||
assert(!m.hasPendingWork());
|
||||
m.command_radio_apply_pending=2;
|
||||
assert(m.hasPendingWork()); // Deferred power must keep the retry loop awake.
|
||||
assert(m.pendingWorkReason()==mesh::powerdiag::mask(mesh::powerdiag::Reason::Scheduled));
|
||||
m.command_radio_apply_pending=0;
|
||||
assert(!m.hasPendingWork());
|
||||
m.store._contact_recovery=ContactStore::ContactRecoveryPending;
|
||||
assert(m.hasPendingWork()); // Storage repair still needs the outer loop.
|
||||
m.store._contact_recovery=ContactStore::ContactRecoveryLegacy;
|
||||
@@ -376,6 +524,7 @@ class CompanionRadioSettingsTransactionTests(unittest.TestCase):
|
||||
'bool MyMesh::applySavedRadioParams(',
|
||||
'MyMesh::RadioSettingResult MyMesh::applyAndSaveTxPower(',
|
||||
'void MyMesh::finishRadioParamApply(',
|
||||
'void MyMesh::deferRadioCommand(',
|
||||
'void MyMesh::cancelPendingRadioParamApply(',
|
||||
'void MyMesh::servicePendingRadioParamApply(',
|
||||
))
|
||||
|
||||
@@ -40,7 +40,7 @@ struct FakeWiFi {
|
||||
bool mode(wifi_mode_t value){if(failure==1)return false;current=value;return true;}
|
||||
} WiFi;
|
||||
namespace mesh {
|
||||
enum class RadioParamApplyResult {APPLIED};
|
||||
enum class RadioParamApplyResult {APPLIED,BUSY,FAILED};
|
||||
namespace wifi {
|
||||
constexpr uint8_t kProtocolMask=15,kLongRangeRadioOwner=1,kLongRangeBridgeOwner=2;
|
||||
void setLongRangeOwner(uint8_t,bool){}
|
||||
@@ -89,6 +89,11 @@ int main(){
|
||||
assert(radio.isEnabled()&&sdk_active&&wake_refs==1&&!WiFi.autoreconnect);
|
||||
const int started=init_calls;radio.init();assert(init_calls==started&&wake_refs==1);
|
||||
assert(radio.setTxPower(13)&&power==52);
|
||||
assert(radio.trySetTxPower(14)==mesh::RadioParamApplyResult::APPLIED&&power==56);
|
||||
failure=6;
|
||||
assert(radio.trySetTxPower(15)==mesh::RadioParamApplyResult::FAILED&&power==56);
|
||||
failure=0;
|
||||
assert(radio.trySetTxPower(13)==mesh::RadioParamApplyResult::APPLIED&&power==52);
|
||||
uint8_t raw[255]={0x15,1,2,3},out[255];
|
||||
deliver(raw,4);assert(radio.recvRaw(out,sizeof(out))==4&&!memcmp(raw,out,4));
|
||||
// Stop in the middle of a fragmented TX: do not report success or send fragment 2.
|
||||
|
||||
@@ -324,7 +324,7 @@ struct RadioLibWrapper {
|
||||
bool _profile_repair_required=false;
|
||||
bool supported=true;
|
||||
bool supportsMeshtext() const { return supported; }
|
||||
bool _rx_ps_armed=true,_params_valid=true,_dbm_valid=false,_rx_boosted_gain_valid=false;
|
||||
bool _rx_ps_armed=true,_params_valid=true,_dbm_valid=false,_dbm_applied=false,_rx_boosted_gain_valid=false;
|
||||
int state=1;uint8_t _cur_sf=11,_cur_cr=5;float _cur_freq=906.875,_cur_bw=250;
|
||||
int8_t _cur_dbm=22;bool _cur_rx_boosted_gain=false;
|
||||
mesh::RadioProfileProtocol _physical_protocol=mesh::RadioProfileProtocol::Meshtext;
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
"""Execute real TX-power admission, retry and unchanged-value behavior."""
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
from test_radio_interrupt_recovery import compile_run
|
||||
from test_radio_receive_contract import method
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
HARNESS = r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <initializer_list>
|
||||
#define MESH_DEBUG_PRINTLN(...) ((void)0)
|
||||
#define RADIOLIB_ERR_NONE 0
|
||||
#define STATE_IDLE 0
|
||||
#define STATE_RX 1
|
||||
#define STATE_TX_WAIT 3
|
||||
namespace mesh { enum class RadioParamApplyResult { APPLIED, BUSY, FAILED }; }
|
||||
using Result = mesh::RadioParamApplyResult;
|
||||
struct Chip {
|
||||
int standby_result = 0, power_result = 0, direct_result = 0;
|
||||
int8_t power = 17;
|
||||
bool write_before_failure = false;
|
||||
unsigned standbys = 0, writes = 0, keys = 0;
|
||||
int standby() { ++standbys; return standby_result; }
|
||||
int setOutputPower(int8_t requested) {
|
||||
++writes;
|
||||
if (!power_result || write_before_failure) power = requested;
|
||||
return power_result;
|
||||
}
|
||||
int transmitDirect() { ++keys; return direct_result; }
|
||||
};
|
||||
struct RadioLibWrapper {
|
||||
Chip chip;
|
||||
Chip* _radio = &chip;
|
||||
uint8_t state = STATE_RX;
|
||||
int8_t _cur_dbm = 17;
|
||||
bool _dbm_valid = true, _dbm_applied = true, _profile_repair_required = false;
|
||||
bool _profile_refresh_required = false, _rx_hold_continuous = false;
|
||||
bool _rx_ps_armed = true, _cw_active = false, _cw_stopping = false;
|
||||
bool _beacon_owned = false, _beacon_restoring = false;
|
||||
bool _morse_owned = false, _morse_rx_owned = false;
|
||||
bool pending = false, busy = false, packet = false;
|
||||
unsigned irq_reads = 0, busy_reads = 0, packet_reads = 0, claims = 0;
|
||||
unsigned resumes = 0, carrier_stops = 0;
|
||||
bool hasPendingRadioInterrupt() { ++irq_reads; return pending; }
|
||||
bool claimRadioInterrupt() { ++claims; bool value = pending; pending = false; return value; }
|
||||
bool isChipBusy() { ++busy_reads; return busy; }
|
||||
bool isReceivingPacket() { ++packet_reads; return packet; }
|
||||
int16_t applyCachedTxPower(int8_t requested) { return chip.setOutputPower(requested); }
|
||||
bool stopCarrierWave() { ++carrier_stops; _cw_active = false; return true; }
|
||||
void startRecv() { ++resumes; state = STATE_RX; }
|
||||
uint8_t beginReconfigure();
|
||||
void endReconfigure(bool);
|
||||
bool setTxPower(int8_t);
|
||||
Result trySetTxPower(int8_t);
|
||||
};
|
||||
@METHODS@
|
||||
int main() {
|
||||
// A temporary receive/TX condition does not become a hardware rejection.
|
||||
for (unsigned reason = 0; reason < 4; ++reason) {
|
||||
RadioLibWrapper radio;
|
||||
radio.pending = reason == 0;
|
||||
if (reason == 1) radio.state = STATE_TX_WAIT;
|
||||
radio.busy = reason == 2;
|
||||
radio.packet = reason == 3;
|
||||
assert(radio.trySetTxPower(10) == Result::BUSY);
|
||||
assert(!radio.setTxPower(10));
|
||||
assert(!radio.chip.writes && !radio.chip.standbys && !radio.resumes);
|
||||
assert(radio._cur_dbm == 17 && radio.chip.power == 17);
|
||||
if (reason == 0) assert(radio.pending && !radio.claims);
|
||||
radio.pending = radio.busy = radio.packet = false;
|
||||
radio.state = STATE_RX;
|
||||
assert(radio.trySetTxPower(10) == Result::APPLIED);
|
||||
assert(radio.chip.writes == 1 && radio.chip.standbys == 1 && radio.resumes == 1);
|
||||
assert(radio._cur_dbm == 10 && radio._dbm_valid && radio._dbm_applied && radio.chip.power == 10);
|
||||
}
|
||||
// App saves can resend the current power while an unread packet owns RX.
|
||||
for (unsigned reason = 0; reason < 4; ++reason) {
|
||||
RadioLibWrapper radio;
|
||||
radio.pending = reason == 0;
|
||||
if (reason == 1) radio.state = STATE_TX_WAIT;
|
||||
radio.busy = reason == 2;
|
||||
radio.packet = reason == 3;
|
||||
const auto original_state = radio.state;
|
||||
assert(radio.trySetTxPower(17) == Result::APPLIED && radio.setTxPower(17));
|
||||
assert(!radio.chip.writes && !radio.chip.standbys && !radio.resumes);
|
||||
assert(!radio.irq_reads && !radio.busy_reads && !radio.packet_reads && !radio.claims);
|
||||
assert(radio.state == original_state && radio._rx_ps_armed);
|
||||
assert(radio.pending == (reason == 0) && radio.packet == (reason == 3));
|
||||
}
|
||||
// A matching number without a valid hardware cache must still be applied.
|
||||
{
|
||||
RadioLibWrapper radio; radio._dbm_valid = false; radio.busy = true;
|
||||
assert(radio.trySetTxPower(17) == Result::BUSY && !radio.chip.writes);
|
||||
radio.busy = false;
|
||||
assert(radio.trySetTxPower(17) == Result::APPLIED && radio.chip.writes == 1);
|
||||
}
|
||||
// A failed earlier transaction cannot be hidden by the unchanged shortcut.
|
||||
{
|
||||
RadioLibWrapper radio; radio._profile_repair_required = true; radio.busy = true;
|
||||
assert(radio.trySetTxPower(17) == Result::BUSY && !radio.chip.writes);
|
||||
radio.busy = false;
|
||||
assert(radio.trySetTxPower(17) == Result::APPLIED);
|
||||
assert(radio.chip.standbys == 1 && radio.chip.writes == 1 && radio.resumes == 1);
|
||||
}
|
||||
// Beacon and Morse owners remain exclusive even for the cached power.
|
||||
for (unsigned owner = 0; owner < 4; ++owner) {
|
||||
RadioLibWrapper radio;
|
||||
radio._beacon_owned = owner == 0;
|
||||
radio._cw_active = owner != 0;
|
||||
radio._morse_owned = owner == 1;
|
||||
radio._morse_rx_owned = owner == 2;
|
||||
radio._cw_stopping = owner == 3;
|
||||
assert(radio.trySetTxPower(17) == Result::BUSY && !radio.setTxPower(17));
|
||||
assert(!radio.chip.writes && !radio.chip.standbys && !radio.chip.keys);
|
||||
assert(!radio.resumes && !radio.carrier_stops);
|
||||
}
|
||||
#if MESH_ENABLE_CARRIER_TOOLS
|
||||
{
|
||||
RadioLibWrapper radio; radio._cw_active = true;
|
||||
assert(radio.trySetTxPower(17) == Result::APPLIED);
|
||||
assert(radio.chip.writes == 1 && radio.chip.keys == 1 && !radio.resumes);
|
||||
}
|
||||
#endif
|
||||
// A failed SPI command may already have changed the physical output power.
|
||||
// The old cached request is usable again only after a verified hardware apply.
|
||||
{
|
||||
RadioLibWrapper radio;
|
||||
radio.chip.power_result = -705; radio.chip.write_before_failure = true;
|
||||
assert(radio.trySetTxPower(10) == Result::FAILED);
|
||||
assert(radio._cur_dbm == 17 && radio.chip.power == 10 && !radio._dbm_applied);
|
||||
radio.busy = true;
|
||||
assert(radio.trySetTxPower(17) == Result::BUSY && radio.chip.writes == 1);
|
||||
radio.busy = false; radio.chip.power_result = 0;
|
||||
assert(radio.trySetTxPower(17) == Result::APPLIED && radio.chip.writes == 2);
|
||||
assert(radio.chip.power == 17 && radio._dbm_applied);
|
||||
assert(radio.trySetTxPower(17) == Result::APPLIED && radio.chip.writes == 2);
|
||||
}
|
||||
// Real command and standby failures remain failures, with the requested cache intact.
|
||||
{
|
||||
RadioLibWrapper radio; radio.chip.power_result = -705;
|
||||
assert(radio.trySetTxPower(10) == Result::FAILED && !radio.setTxPower(10));
|
||||
assert(radio._cur_dbm == 17 && radio.chip.power == 17);
|
||||
assert(!radio._dbm_applied);
|
||||
assert(radio.chip.writes == 2 && radio.resumes == 2);
|
||||
}
|
||||
{
|
||||
RadioLibWrapper radio; radio.chip.standby_result = -705;
|
||||
assert(radio.trySetTxPower(10) == Result::FAILED && !radio.setTxPower(10));
|
||||
assert(!radio.chip.writes && radio._profile_repair_required && !radio.resumes);
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class RadioTxPowerTransactionTests(unittest.TestCase):
|
||||
def test_actual_busy_retry_noop_ownership_and_failures(self):
|
||||
source = (ROOT / "src/helpers/radiolib/RadioLibWrappers.cpp").read_text()
|
||||
methods = "\n".join(method(source, signature) for signature in (
|
||||
"uint8_t RadioLibWrapper::beginReconfigure(",
|
||||
"void RadioLibWrapper::endReconfigure(",
|
||||
"bool RadioLibWrapper::setTxPower(",
|
||||
"mesh::RadioParamApplyResult RadioLibWrapper::trySetTxPower("))
|
||||
for carrier_tools in (0, 1):
|
||||
with self.subTest(carrier_tools=carrier_tools):
|
||||
compile_run(HARNESS.replace("@METHODS@", methods), defines=(
|
||||
"-DMESH_ENABLE_TX_BEACONS=1", "-DMESH_ENABLE_MORSE_RX=1",
|
||||
f"-DMESH_ENABLE_CARRIER_TOOLS={carrier_tools}"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -47,7 +47,7 @@ struct Driver {
|
||||
bool _rx_ps_armed = false, _cw_active = false;
|
||||
bool _profile_repair_required = false, _profile_refresh_required = false;
|
||||
bool _rx_hold_continuous = false;
|
||||
bool _dbm_valid = true, _rx_boosted_gain_valid = false;
|
||||
bool _dbm_valid = true, _dbm_applied = true, _rx_boosted_gain_valid = false;
|
||||
int8_t _cur_dbm = 7;
|
||||
bool _cur_rx_boosted_gain = false;
|
||||
unsigned power_commands = 0, gain_commands = 0;
|
||||
@@ -75,6 +75,7 @@ struct Driver {
|
||||
uint8_t beginReconfigure();
|
||||
void endReconfigure(bool);
|
||||
bool setTxPower(int8_t);
|
||||
mesh::RadioParamApplyResult trySetTxPower(int8_t);
|
||||
} radio_driver;
|
||||
@WRAPPER_METHODS@
|
||||
struct Prefs {
|
||||
@@ -280,7 +281,8 @@ def harness(role, mutation=None):
|
||||
wrapper = (ROOT / "src/helpers/radiolib/RadioLibWrappers.cpp").read_text()
|
||||
signatures = ["uint8_t RadioLibWrapper::beginReconfigure()",
|
||||
"void RadioLibWrapper::endReconfigure(bool resume_rx)",
|
||||
"bool RadioLibWrapper::setTxPower(int8_t dbm)"]
|
||||
"bool RadioLibWrapper::setTxPower(int8_t dbm)",
|
||||
"mesh::RadioParamApplyResult RadioLibWrapper::trySetTxPower(int8_t dbm)"]
|
||||
if role == "room":
|
||||
signatures.append("bool RadioLibWrapper::setRxBoostedGainMode(bool enabled)")
|
||||
methods = "\n".join(body(wrapper, signature) for signature in signatures)
|
||||
|
||||
Reference in new issue
Block a user