From c313658b88bd008ab318128e74e3bc4b52a47162 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 26 Oct 2026 19:40:05 -0700 Subject: [PATCH] Wait for the radio from the repeater's own loop --- examples/simple_repeater/MyMesh.cpp | 2 +- src/helpers/DirectOtaAdminClient.h | 5 +++-- test/test_direct_ota_admin_client.py | 22 +++++++++++++++++----- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 32630f099..5b46651db 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -13393,7 +13393,7 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char * void MyMesh::loop() { #if MESH_ENABLE_DIRECT_OTA_ADMIN - direct_ota_admin.service(*this, millis()); + direct_ota_admin.service(*this, millis(), !hasPendingRadioWork()); #endif #if defined(WITH_WEBCONFIG) || defined(ETHERNET_ENABLED) _local_cli_output.service(); diff --git a/src/helpers/DirectOtaAdminClient.h b/src/helpers/DirectOtaAdminClient.h index eac706e0e..ab51e29d1 100644 --- a/src/helpers/DirectOtaAdminClient.h +++ b/src/helpers/DirectOtaAdminClient.h @@ -146,14 +146,15 @@ public: if (phase_ != Idle) finish("admin session expired", true); } } - void service(Mesh& mesh, uint32_t now) { + void service(Mesh& mesh, uint32_t now, bool radio_idle) { tick(now); if (phase_ == Proof && expired(now, proof_retry_at_) && proof_attempts_ < 3) { if (!queueReturnPath(mesh)) { clearSession(); finish("login return path queue failed", true); } + return; // radio_idle was sampled before this new PATH was queued } // PATH has lower queue priority than CLI. Wait until its transmission and // retries finish before admitting the proof, rather than relying on delay. - if (phase_ == ReturnPath && !mesh.hasPendingRadioWork()) { + if (phase_ == ReturnPath && radio_idle) { phase_ = Proof; if (!queueCommand(mesh, "get public.key", now)) { clearSession(); finish("login proof queue failed", true); } else proof_retry_at_ = now + 8000; diff --git a/test/test_direct_ota_admin_client.py b/test/test_direct_ota_admin_client.py index 1b17b427b..2d737c93c 100644 --- a/test/test_direct_ota_admin_client.py +++ b/test/test_direct_ota_admin_client.py @@ -80,6 +80,7 @@ struct Mesh { Packet* createPathReturn(const uint8_t*,const uint8_t*,const uint8_t*,uint8_t,uint8_t,const uint8_t*,size_t); Packet* createPathReturn(const Identity& dest,const uint8_t* secret,const uint8_t* path,uint8_t length,uint8_t type,const uint8_t* extra,size_t size){uint8_t hash[1];dest.copyHashTo(hash);return createPathReturn(hash,secret,path,length,type,extra,size);} bool sendDirect(Packet* p,const uint8_t* path,uint8_t length){assert(p&&path==nullptr&&length==0&&p->tx_radio==RADIO_TX_PRIMARY);if(type==PAYLOAD_TYPE_PATH)sent.assign(p->payload,p->payload+p->payload_len);pending=send_ok;return send_ok;} +protected: bool hasPendingRadioWork()const{return pending;} }; @PATH_BUILDER@ @@ -92,6 +93,7 @@ using Client=mesh::DirectOtaAdminClient; static uint32_t millis(){return 100;} struct UsbRole:mesh::Mesh { Client direct_ota_admin; + void loop(){ @SERVICE_CALL@ } void dispatch(uint32_t sender_timestamp,void* sender,char* command,char* reply,bool usb_origin){ @USB_GATE@ strcpy(reply,"not a peer command"); @@ -153,8 +155,8 @@ static void authenticate(Client& c,mesh::Mesh& radio,uint32_t now=100){ // by authenticated ciphertext containing empty return path + dummy payload. assert(radio.sent.size()==10&&radio.sent[0]==0x11&&radio.sent[1]==0x55); assert(radio.sent[2]==0xCA&&radio.sent[3]==0xFE&&radio.sent[4]==0&&radio.sent[5]==0xFF); - c.service(radio,now+3);assert(radio.type==PAYLOAD_TYPE_PATH); // do not overtake the PATH - radio.pending=false;c.service(radio,now+4); + c.service(radio,now+3,!radio.pending);assert(radio.type==PAYLOAD_TYPE_PATH); // do not overtake the PATH + radio.pending=false;c.service(radio,now+4,!radio.pending); assert(radio.type==PAYLOAD_TYPE_TXT_MSG&&radio.sent[4]==12); std::string proof(radio.sent.begin()+5,radio.sent.begin()+8); assert(std::string(radio.sent.begin()+8,radio.sent.end())=="get public.key"); @@ -247,10 +249,17 @@ int main(){ Client retries;discover(retries,radio,10);command(retries,radio,"ota.peer login "+key+" password",6011); mesh::Packet direct;uint8_t accepted_login[13]{};uint32_t stamp=60000;memcpy(accepted_login,&stamp,4);accepted_login[7]=3; retries.onData(radio,&direct,PAYLOAD_TYPE_RESPONSE,accepted_login,sizeof(accepted_login),6012); - radio.pending=false;retries.service(radio,6013);assert(radio.type==PAYLOAD_TYPE_TXT_MSG); - for(uint32_t now:{14013U,22014U}){retries.service(radio,now);assert(radio.type==PAYLOAD_TYPE_PATH);radio.pending=false;retries.service(radio,now+1);} - const int attempts=radio.calls;retries.service(radio,30015);assert(radio.calls==attempts); + radio.pending=false;retries.service(radio,6013,!radio.pending);assert(radio.type==PAYLOAD_TYPE_TXT_MSG); + for(uint32_t now:{14013U,22014U}){radio.pending=false;retries.service(radio,now,!radio.pending);assert(radio.type==PAYLOAD_TYPE_PATH);radio.pending=false;retries.service(radio,now+1,!radio.pending);} + const int attempts=radio.calls;retries.service(radio,30015,!radio.pending);assert(radio.calls==attempts); retries.tick(36011);assert(!retries.hasSession()); + // Execute the production MyMesh loop call with Dispatcher-like protection. + // Only the derived mesh may inspect its protected radio-work state. + UsbRole integration;discover(integration.direct_ota_admin,integration,10); + command(integration.direct_ota_admin,integration,"ota.peer login "+key+" password",6011); + integration.direct_ota_admin.onData(integration,&direct,PAYLOAD_TYPE_RESPONSE,accepted_login,sizeof(accepted_login),6012); + integration.loop();assert(integration.type==PAYLOAD_TYPE_PATH); + integration.pending=false;integration.loop();assert(integration.type==PAYLOAD_TYPE_TXT_MSG); } ''' @@ -266,6 +275,9 @@ class DirectOtaAdminClientTest(unittest.TestCase): end += 1 code = HARNESS.replace('@ECHO_LOOP@',source[start:end]) mesh_source = (ROOT/'examples/simple_repeater/MyMesh.cpp').read_text() + start = mesh_source.index('direct_ota_admin.service(') + end = mesh_source.index(';',start)+1 + code = code.replace('@SERVICE_CALL@', mesh_source[start:end]) start = mesh_source.index('if (mesh::DirectOtaAdminClient::isCommand(command))') opening = mesh_source.index('{',start) end,depth=opening+1,1