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
@@ -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