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:
mikecarper committed 2026-10-25 11:57:03 -07:00
1 parent af69918094
commit 8dfdba3f6d
17 files changed
+470 -57

No files matched your search

+1 -1
View File
@@ -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;
+10 -1
View File
@@ -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) {
+6 -2
View File
@@ -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(',
+163 -14
View File
@@ -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(',
))
+6 -1
View File
@@ -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.
+1 -1
View File
@@ -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;
+175
View File
@@ -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()
+4 -2
View File
@@ -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)