diff --git a/docs/cli_commands.md b/docs/cli_commands.md index b60779b59..74cddf274 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -11,7 +11,9 @@ For supported Full Companions, see [Meshtext channel chat](radio_profiles.md#mes for `mt-` channels and the explicit `send mt.nodeinfo ` command. Chat remains plain text. A separate GPS-free NodeInfo is sent after the first successful chat transmission per used channel/identity per boot; the explicit -command can also request one. MT heard-repeat retries use the Companion's +command can also request one. Automatic metadata waits for a 250 ms gap after +the latest successful MT transmission; explicit requests bypass that gap. +MT heard-repeat retries use the Companion's `flood.retry.count`, with the metadata cap and `flood.retry.advert` gate for NodeInfo. diff --git a/docs/radio_profiles.md b/docs/radio_profiles.md index 997c8ff7b..c440fee26 100644 --- a/docs/radio_profiles.md +++ b/docs/radio_profiles.md @@ -521,10 +521,15 @@ MeshCore node name, add a label to chat text or transmit anything. After the first chat packet actually finishes transmitting on a configured MT channel, the Companion queues a separate, GPS-free `NODEINFO_APP` broadcast. +Automatic metadata waits for a 250 ms gap after the most recent successful MT +transmission, including retries, so a stock peer can re-arm its receiver. +Explicit `send mt.nodeinfo` does not wait for this automatic-metadata gap. This happens once per used channel and sender identity per boot, not for every chat message. A rejected or failed chat send does not trigger it. There is no NodeInfo transmission just from booting, receiving traffic, changing settings or joining a channel, and there is no periodic NodeInfo timer. +Changing the node name cancels old-name metadata retries; the next new +successful chat announces the updated name without sending from the rename. `send mt.nodeinfo ` still explicitly queues **one** standard `NODEINFO_APP` broadcast on the selected configured MT channel. The slot is diff --git a/docs/terminal_chat_cli.md b/docs/terminal_chat_cli.md index a8062a79e..028ae9933 100644 --- a/docs/terminal_chat_cli.md +++ b/docs/terminal_chat_cli.md @@ -63,6 +63,7 @@ channel 0 Hello Use a configured `mt-` channel slot. Chat carries only the entered text. After the first successful over-air chat transmission per used channel/identity per boot, the firmware queues a separate standard name/ID NodeInfo advertisement. +Automatic metadata waits for a 250 ms quiet TX gap so the peer can resume RX. `send mt.nodeinfo` can also request one explicitly. Neither contains GPS or coordinates. No NodeInfo is sent just from booting, configuration, receiving traffic or a timer. The long name comes from the existing node name; diff --git a/examples/companion_radio/MeshtextCompanion.cpp b/examples/companion_radio/MeshtextCompanion.cpp index 4a969a09a..825dc3d23 100644 --- a/examples/companion_radio/MeshtextCompanion.cpp +++ b/examples/companion_radio/MeshtextCompanion.cpp @@ -13,6 +13,7 @@ namespace { constexpr size_t LegacySettingsSize = 16; constexpr size_t SettingsSize = 40; +constexpr uint32_t AutoNodeInfoQuietMillis = 250; bool validShortName(const char* name) { if (!name) return false; const size_t length = strnlen(name, 17); @@ -321,12 +322,16 @@ void MyMesh::refreshMeshtextProfile() { const uint32_t primary_generation = current_profiles ? current_profiles->generation[0] : 0; const uint32_t secondary_generation = current_profiles ? current_profiles->generation[1] : 0; const uint32_t intent_generation = _radio_profiles.secondaryIntentGeneration(); + const bool secondary_ready = _radio_profiles.secondaryRuntimeAvailable(); if (!_mt_profile_dirty && primary_generation == _mt_primary_generation && secondary_generation == _mt_secondary_generation && intent_generation == _mt_intent_generation - && _mt_last_radio_ready == _radio_available && _mt_primary_frequency == _prefs.freq) return; + && _mt_last_radio_ready == _radio_available && _mt_primary_frequency == _prefs.freq + && _mt_last_secondary_ready == secondary_ready) return; // Hashing names and scanning the channel table every radio loop would add // blind time to the dual-profile scanner. Recompute only on configuration - // or lease/generation edges; RX/TX still derive a fresh bounded wire copy. + // or lease/generation/readiness edges. Publishing an unchanged saved tuple + // can release a busy guard without changing either RF generation. + // RX/TX still derive a fresh bounded wire copy. struct CacheUpdate { MyMesh& owner; ~CacheUpdate() { @@ -335,6 +340,7 @@ void MyMesh::refreshMeshtextProfile() { owner._mt_secondary_generation = p ? p->generation[1] : 0; owner._mt_intent_generation = owner._radio_profiles.secondaryIntentGeneration(); owner._mt_last_radio_ready = owner._radio_available; + owner._mt_last_secondary_ready = owner._radio_profiles.secondaryRuntimeAvailable(); owner._mt_primary_frequency = owner._prefs.freq; owner._mt_profile_dirty = false; } @@ -508,7 +514,11 @@ void MyMesh::loopMeshtextDelivery() { if (!usable || hasForeignRadioFrame()) return; // Announce only channels which actually completed a user text TX. Never // turn boot, received traffic, a timer, or a configuration edit into TX. - for (int i = 0; i < MAX_GROUP_CHANNELS; ++i) { + // Stock peers need time to re-arm RX after chat/rebroadcast processing. + // Every successful MT TX restarts this gap; ordinary chat retries remain + // eligible below while the automatic announcement is still only intent. + const bool metadata_quiet = uint32_t(now - _mt_last_tx_completed_at) >= AutoNodeInfoQuietMillis; + for (int i = 0; metadata_quiet && i < MAX_GROUP_CHANNELS; ++i) { if (!_mt_nodeinfo_pending[i]) continue; ChannelDetails channel; uint8_t fingerprint[8]; @@ -639,6 +649,7 @@ void MyMesh::onForeignRadioSendComplete(bool success) { return; } #endif + if (success) _mt_last_tx_completed_at = millis(); const int slot = _mt_tx_slot; _mt_tx_slot = -1; if (slot < 0 || slot >= static_cast(mesh::mt::DeliverySlots)) { diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index e60c34b43..5c6bd4284 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -8079,7 +8079,20 @@ bool MyMesh::saveAdvertName(const char* name) { char previous[sizeof(_prefs.node_name)]; memcpy(previous, _prefs.node_name, sizeof(previous)); StrHelper::strncpy(_prefs.node_name, name, sizeof(_prefs.node_name)); - if (savePrefs()) return true; + if (savePrefs()) { +#if MESH_ENABLE_MESHTEXT_CHAT + if (strncmp(previous, _prefs.node_name, sizeof(previous)) != 0) { + // NodeInfo contains the global advert name; retained ciphertext must + // not advertise the previous name after a successful rename. Preserve + // independent text retries, and wait for a new successful chat before + // announcing again rather than transmitting from a settings edit. + for (auto& frame : _mt_delivery) if (frame.nodeinfo) frame.cancelled = true; + memset(_mt_nodeinfo_announced, 0, sizeof(_mt_nodeinfo_announced)); + memset(_mt_nodeinfo_pending, 0, sizeof(_mt_nodeinfo_pending)); + } +#endif + return true; + } memcpy(_prefs.node_name, previous, sizeof(previous)); return false; } @@ -10154,7 +10167,7 @@ void MyMesh::handleTerminalCommand(char* command, bool reader_prompt, " set mt.hops <0..7> (saved; default 7)\r\n" " set mt.node (saved; use a unique ID)\r\n" " set mt.shortname <1..4 UTF-8 characters|default> (saved; NodeInfo uses 4 bytes)\r\n" - " send mt.nodeinfo (one manual advert; no automatic adverts)\r\n" + " send mt.nodeinfo (GPS-free; also automatic after first chat)\r\n" " set mt.preset (saved manual radio2)\r\n" " set mt.channel <32/64 hex digits|AQ==>\r\n" " get mt.channel / delete mt.channel \r\n" diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 95d704220..d28e4838f 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -768,10 +768,12 @@ private: bool _mt_nodeinfo_announced[MAX_GROUP_CHANNELS] = {}; bool _mt_nodeinfo_pending[MAX_GROUP_CHANNELS] = {}; uint32_t _mt_nodeinfo_generation = 0; + uint32_t _mt_last_tx_completed_at = 0; bool _mt_delivery_config_dirty = false; uint32_t _mt_retried = 0, _mt_heard = 0, _mt_exhausted = 0; bool _mt_settings_valid = true, _mt_active = false; bool _mt_profile_dirty = true, _mt_last_radio_ready = false; + bool _mt_last_secondary_ready = false; uint32_t _mt_primary_generation = 0, _mt_secondary_generation = 0, _mt_intent_generation = 0; float _mt_primary_frequency = 0; const char* _mt_status = "no Meshtext channels"; diff --git a/test/fixtures/companion_meshtext.cpp b/test/fixtures/companion_meshtext.cpp index 4b1cf271a..c3fec8ecf 100644 --- a/test/fixtures/companion_meshtext.cpp +++ b/test/fixtures/companion_meshtext.cpp @@ -99,6 +99,7 @@ public: uint8_t _mt_nodeinfo_fingerprint[MAX_GROUP_CHANNELS][8]={}; bool _mt_nodeinfo_announced[MAX_GROUP_CHANNELS]={},_mt_nodeinfo_pending[MAX_GROUP_CHANNELS]={}; uint32_t _mt_nodeinfo_generation=0,_mt_retried=0,_mt_heard=0,_mt_exhausted=0; + uint32_t _mt_last_tx_completed_at=0; struct Random { uint32_t jitter=0; uint32_t nextInt(uint32_t low,uint32_t high) { @@ -106,6 +107,7 @@ public: } } random; bool _mt_settings_valid=true,_mt_active=false,_mt_profile_dirty=true,_mt_last_radio_ready=false; + bool _mt_last_secondary_ready=false; bool _mt_delivery_config_dirty=true; uint32_t _mt_primary_generation=0,_mt_secondary_generation=0,_mt_intent_generation=0; float _mt_primary_frequency=0; const char* _mt_status="no Meshtext channels"; @@ -174,6 +176,8 @@ public: offline.emplace_back(bytes,bytes+size); return true; } bool saveChannels() { return save_result; } + bool savePrefs() { return save_result; } + bool saveAdvertName(const char*); void writeErrFrame(uint8_t value) { error=value; } void writeOKFrame() { ok=true; } bool sendGroupMessage(uint32_t,mesh::GroupChannel&,const char*,const char*,int) { ++meshcore_sends; return true; } @@ -889,7 +893,7 @@ static void automaticNodeinfo() { const auto text=node.foreign.back(); node.advance(60000); assert(node.foreign.size()==1); // Admission is not RF success. node.completeForeign(); assert(node.foreign.size()==1); - node.advance(1); + node.advance(250); assert(node.foreign.size()==2 && node.foreign.back()==std::vector(expected,expected+sizeof(expected))); // Exact independent protobuf/AES bytes prove only User/NodeInfo was sent: // no position packet, coordinates, MAC, public key or custom name envelope. @@ -898,7 +902,7 @@ static void automaticNodeinfo() { node.completeForeign(); node.advance(100000); assert(node.foreign.size()==3); node.commit(1,channel("mt-Team",32)); assert(node.sendMeshtextText(node.stored[1],"private",7)); - node.completeForeign(); node.advance(1); assert(node.foreign.size()==5); + node.completeForeign(); node.advance(250); assert(node.foreign.size()==5); auto private_info=node.foreign.back(); mesh::mt::DecodedText private_decoded; assert(private_info!=text && !mesh::mt::decodeText(private_info.data(),private_info.size(), @@ -912,7 +916,7 @@ static void automaticNodeinfo() { assert(failure.sendMeshtextText(failure.stored[0],"failed",6)); failure.completeForeign(false); failure.advance(100000); assert(failure.foreign.size()==1); assert(failure.sendMeshtextText(failure.stored[0],"success",7)); - failure.completeForeign(); failure.advance(1); assert(failure.foreign.size()==3); + failure.completeForeign(); failure.advance(250); assert(failure.foreign.size()==3); failure.completeForeign(); Preferences::reset(); MyMesh manual; active(manual); announce(manual); @@ -921,6 +925,59 @@ static void automaticNodeinfo() { manual.completeForeign(); manual.advance(100000); assert(manual.foreign.size()==2); puts("PASS auto-nodeinfo"); } +static void automaticNodeinfoQuiet() { + Preferences::reset(); MyMesh node; active(node); + node._prefs.flood_retry_attempts=0; + assert(node.sendMeshtextText(node.stored[0],"first",5)); node.completeForeign(); + assert(node._mt_nodeinfo_pending[0] && !node._mt_last_tx_completed_at); + node.advance(0); assert(node.foreign.size()==1 && !node.foreign_pending); + node.advance(249); assert(node.foreign.size()==1 && node._mt_nodeinfo_pending[0]); + node.advance(1); assert(node.foreign.size()==2 && node.foreign_pending); + node.completeForeign(); + + // Retrying chats are not blocked by the metadata quiet gap. Every actual + // completion restarts that gap, so the announcement follows the burst. + Preferences::reset(); MyMesh retries; active(retries); + retries._prefs.flood_retry_attempts=2; + assert(retries.sendMeshtextText(retries.stored[0],"retry",5)); + const auto original=retries.foreign.back(); retries.completeForeign(); + auto* pending=pendingChat(retries); assert(pending && pending->due==210); + for(unsigned attempt=0;attempt<2;++attempt) { + advanceTo(retries,pending->due); + assert(retries.foreign_pending && retries.foreign.back()==original + && retries._mt_nodeinfo_pending[0]); + const auto prior=retries._mt_last_tx_completed_at; + retries.completeForeign(); + assert(retries._mt_last_tx_completed_at==millis() + && retries._mt_last_tx_completed_at!=prior); + pending=pendingChat(retries); assert(pending); + } + const auto count=retries.foreign.size(); + retries.advance(249); assert(retries.foreign.size()==count && !retries.foreign_pending); + retries.advance(1); assert(retries.foreign.size()==count+1 + && retries.foreign.back()!=original && retries.foreign_pending); + retries.completeForeign(); + + Preferences::reset(); MyMesh wrap; active(wrap); wrap._prefs.flood_retry_attempts=0; + g_mock_millis=UINT32_MAX-100; + assert(wrap.sendMeshtextText(wrap.stored[0],"wrap",4)); wrap.completeForeign(); + wrap.advance(249); assert(wrap.foreign.size()==1 && !wrap.foreign_pending); + wrap.advance(1); assert(wrap.foreign.size()==2 && wrap.foreign_pending); + wrap.completeForeign(); + + // A failed TX does not arm metadata or move the successful-completion + // timestamp. Explicit manual NodeInfo still admits immediately, by choice. + Preferences::reset(); MyMesh failure; active(failure); failure._prefs.flood_retry_attempts=0; + failure.advance(1000); + assert(failure.sendMeshtextText(failure.stored[0],"failed",6)); failure.completeForeign(false); + assert(!failure._mt_last_tx_completed_at && !failure._mt_nodeinfo_pending[0]); + failure.advance(1000); assert(failure.foreign.size()==1); + assert(failure.sendMeshtextText(failure.stored[0],"success",7)); failure.completeForeign(); + assert(failure._mt_last_tx_completed_at==millis()); + assert(failure.sendMeshtextNodeInfo(failure.stored[0]) && failure.foreign.size()==3); + failure.completeForeign(); failure.advance(250); assert(failure.foreign.size()==3); + puts("PASS auto-nodeinfo-quiet"); +} static void heardRetries() { Preferences::reset(); MyMesh node; active(node); announce(node); node._prefs.flood_retry_attempts=2; @@ -1103,12 +1160,12 @@ static void announcementLifecycle() { assert(node.handleMeshtextCommand("set mt.shortname NEW",reply,sizeof(reply)) && !strncmp(reply,"OK",2)); node.advance(100000); assert(node.foreign.size()==before_alias+1); // Name changes do not send alone. assert(node.sendMeshtextText(node.stored[0],"new name",8)); - node.completeForeign(); node.advance(1); assert(node.foreign.size()==before_alias+3); + node.completeForeign(); node.advance(250); assert(node.foreign.size()==before_alias+3); node.completeForeign(); auto changed=channel("#mt-longfast",32); changed.channel.secret[0]^=0x40; node.commit(0,changed); node.advance(1); const auto before_key=node.foreign.size(); assert(node.sendMeshtextText(node.stored[0],"new key",7)); - node.completeForeign(); node.advance(1); assert(node.foreign.size()==before_key+2); + node.completeForeign(); node.advance(250); assert(node.foreign.size()==before_key+2); node.completeForeign(); for(bool fail_admission:{false,true}) { @@ -1116,7 +1173,7 @@ static void announcementLifecycle() { failure._prefs.flood_retry_attempts=0; assert(failure.sendMeshtextText(failure.stored[0],"first",5)); failure.completeForeign(); const auto first_count=failure.foreign.size(); - failure.admit_foreign=!fail_admission; failure.advance(1); + failure.admit_foreign=!fail_admission; failure.advance(250); if(!fail_admission) { assert(failure.foreign_pending); failure.completeForeign(false); } @@ -1126,11 +1183,111 @@ static void announcementLifecycle() { assert(failure.foreign.size()==first_count+(fail_admission?0:1)); failure.admit_foreign=true; assert(failure.sendMeshtextText(failure.stored[0],"another",7)); failure.completeForeign(); - failure.advance(1); assert(failure.foreign_pending); + failure.advance(250); assert(failure.foreign_pending); failure.completeForeign(); } puts("PASS announcement-lifecycle"); } +static void advertNameDelivery() { + // A failed save or unchanged effective name must preserve the announced + // state and the exact retained packet awaiting its one metadata retry. + Preferences::reset(); MyMesh node; active(node); + assert(node.sendMeshtextNodeInfo(node.stored[0])); + const auto original=node.foreign.back(); node.completeForeign(); + auto& metadata=node._mt_delivery[mesh::mt::ChatDeliverySlots]; + assert(metadata.length && node._mt_nodeinfo_announced[0]); + const auto old_due=metadata.due; + node.save_result=false; + assert(!node.saveAdvertName("Rejected") && !strcmp(node._prefs.node_name,"Test")); + assert(metadata.length && !metadata.cancelled && node._mt_nodeinfo_announced[0]); + node.save_result=true; + assert(node.saveAdvertName("Test")); + assert(metadata.length && !metadata.cancelled && metadata.due==old_due + && node._mt_nodeinfo_announced[0]); + assert(node.saveAdvertName("Renamed") && !strcmp(node._prefs.node_name,"Renamed")); + assert(metadata.cancelled && !node._mt_nodeinfo_announced[0] && !node._mt_nodeinfo_pending[0]); + advanceTo(node,old_due); node.advance(100000); + assert(!metadata.length && node.foreign.size()==1 && !node.foreign_pending + && !node._mt_retried && !node._mt_failed); + // No transmission just from renaming. The next new chat advertises the + // updated name with independently generated standard User/AES bytes. + node._prefs.flood_retry_attempts=0; + assert(node.sendMeshtextText(node.stored[0],"new name",8)); + node.completeForeign(); node.advance(250); + const uint8_t expected[]=@NODEINFO_RENAMED@; + assert(node.foreign.size()==3 && node.foreign.back()!=original + && node.foreign.back()==std::vector(expected,expected+sizeof(expected))); + node.completeForeign(); node.advance(100000); assert(node.foreign.size()==3); + + // A queued old-name frame is cancelled. One which is already physically + // transmitting may finish, but cannot re-arm metadata or its old retry. + for(bool sending:{false,true}) { + Preferences::reset(); MyMesh busy; active(busy); + assert(busy.sendMeshtextNodeInfo(busy.stored[0])); + busy.foreign_sending=sending; + assert(busy.saveAdvertName("Renamed")); busy.advance(1); + assert(busy.foreign_pending==sending); + if(sending) busy.completeForeign(); + busy.advance(100000); + assert(busy.foreign.size()==1 && !busy._mt_nodeinfo_announced[0] + && !busy._mt_nodeinfo_pending[0] && !busy._mt_failed && !busy._mt_retried); + } + + // Clearing a deferred first-use intent does not turn the edit into TX, and + // old text ciphertext remains independently eligible for its own retry. + Preferences::reset(); MyMesh chat; active(chat); + assert(chat.sendMeshtextText(chat.stored[0],"text",4)); + const auto text=chat.foreign.back(); chat.completeForeign(); + auto* pending=pendingChat(chat); assert(pending && chat._mt_nodeinfo_pending[0]); + assert(chat.saveAdvertName("Renamed") && !pending->cancelled + && !chat._mt_nodeinfo_pending[0]); + chat.advance(1); assert(chat.foreign.size()==1); + advanceTo(chat,pending->due); + assert(chat.foreign.size()==2 && chat.foreign.back()==text); + chat.completeForeign(); chat.advance(1); + assert(!chat._mt_nodeinfo_pending[0] && chat.foreign.size()==2); + auto echo=repeated(text); assert(receiveFrame(chat,echo.data(),echo.size())); + chat.advance(100000); assert(chat.foreign.size()==2); + puts("PASS advert-name-delivery"); +} +static void readinessCache() { + // A delayed same-tuple publication releases its busy state without changing + // either RF generation. An MT channel installed during that delay must not + // remain permanently stuck behind a cached busy result. + for(const char* command:{"set radio2 909.5,500,5,5,rxtx,32","set tempradio2 off"}) { + Preferences::reset(); MyMesh node; node.beginMeshtext(); + node.cli("set radio2 909.5,500,5,5,rxtx,32"); node.advance(2001); + const auto generation=node.radio.p.generation[1]; + const auto initial=node.radio.p.secondary; + assert(initial.params.protocol==mesh::RadioProfileProtocol::MeshCore); + node.cli(command); node.commit(0,channel("mt-labtest",16)); + assert(!node._mt_active && !node._radio_profiles.secondaryRuntimeAvailable()); + node.advance(1999); + assert(!node._mt_active && node.radio.p.generation[1]==generation); + node.advance(1); + assert(node._radio_profiles.secondaryRuntimeAvailable() && node._mt_active + && node.radio.p.secondary.params.protocol==mesh::RadioProfileProtocol::Meshtext + && node.radio.p.secondary.params.preamble==16); + assertUntouched(node); // Readiness/configuration alone still never transmits. + const auto active_generation=node.radio.p.generation[1]; + node.advance(1000); + assert(node._mt_active && node.radio.p.generation[1]==active_generation); + + // Readiness caching must remain read-only and preserve external ownership: + // do not force the overlay back when another runtime owner changed radio2. + auto external=initial; external.params.freq=910.5f; + node.radio.p.setSecondary(external,false); + const auto external_generation=node.radio.p.generation[1]; + node.advance(1); + assert(!node._mt_active && !node._radio_profiles.secondaryRuntimeAvailable()); + node.commit(0,channel("mt-other",16)); node.advance(100000); + assert(!node._mt_active && node.radio.p.generation[1]==external_generation + && node.radio.p.secondary.params==external.params + && node.radio.p.secondary.mode==external.mode); + assertUntouched(node); + } + puts("PASS readiness-cache"); +} int main(int argc,char** argv) { assert(argc==2); if(!strcmp(argv[1],"keys"))keys(); @@ -1147,11 +1304,14 @@ int main(int argc,char** argv) { else if(!strcmp(argv[1],"node"))nodeOverride(); else if(!strcmp(argv[1],"nodeinfo"))nodeinfo(); else if(!strcmp(argv[1],"auto-nodeinfo"))automaticNodeinfo(); + else if(!strcmp(argv[1],"auto-nodeinfo-quiet"))automaticNodeinfoQuiet(); else if(!strcmp(argv[1],"heard-retries"))heardRetries(); else if(!strcmp(argv[1],"strict-echo"))strictEcho(); else if(!strcmp(argv[1],"delivery-lifecycle"))deliveryLifecycle(); else if(!strcmp(argv[1],"delivery-contention"))deliveryContention(); else if(!strcmp(argv[1],"delivery-timing"))deliveryTiming(); else if(!strcmp(argv[1],"announcement-lifecycle"))announcementLifecycle(); + else if(!strcmp(argv[1],"advert-name-delivery"))advertNameDelivery(); + else if(!strcmp(argv[1],"readiness-cache"))readinessCache(); else assert(!"unknown test case"); } diff --git a/test/test_companion_meshtext.py b/test/test_companion_meshtext.py index a643aff91..d82a1a2b1 100644 --- a/test/test_companion_meshtext.py +++ b/test/test_companion_meshtext.py @@ -56,7 +56,8 @@ class CompanionMeshtextTest(unittest.TestCase): mt = (ROOT / "examples/companion_radio/MeshtextCompanion.cpp").read_text(encoding="utf-8") mt = mt[mt.index("namespace {"):].rsplit("#endif", 1)[0] signatures = ("bool MyMesh::isAliasRadio2Defined(", "bool MyMesh::resolveChannelAlias(", - "bool MyMesh::getChannel(", "bool MyMesh::setChannel(", "bool MyMesh::queueChannelText(") + "bool MyMesh::getChannel(", "bool MyMesh::setChannel(", "bool MyMesh::queueChannelText(", + "bool MyMesh::saveAdvertName(") methods = "\n".join(body(source, signature) for signature in signatures) utils_source = (ROOT / "src/Utils.cpp").read_text(encoding="utf-8") # CLI keys use the production strict hexadecimal parser as well. @@ -104,6 +105,8 @@ class CompanionMeshtextTest(unittest.TestCase): ("%04x" % (default_node & 0xFFFF)).encode()), "@NODEINFO_AFTER_CHAT@": nodeinfo_frame(default_node, 0x12345679, b"Test", ("%04x" % (default_node & 0xFFFF)).encode()), + "@NODEINFO_RENAMED@": nodeinfo_frame(default_node, 0x1234567A, b"Renamed", + ("%04x" % (default_node & 0xFFFF)).encode()), "@NODEINFO_NAMED@": nodeinfo_frame(0x12345678, 0x12345679, b"Mike Carper", b"MIKE"), "@NODEINFO_UTF8@": nodeinfo_frame(0x12345678, 0x1234567B, b"A" * 22, "\U0001f600".encode()), "@NODEINFO_PRIVATE@": nodeinfo_frame(0x12345678, 0x1234567C, b"A" * 22, @@ -213,6 +216,9 @@ class CompanionMeshtextTest(unittest.TestCase): def test_first_successful_chat_announces_once_per_channel_with_no_position(self): self.run_case("auto-nodeinfo") + def test_automatic_nodeinfo_waits_for_quiet_tx_gap_with_retry_refresh_and_wrap(self): + self.run_case("auto-nodeinfo-quiet") + def test_heard_repeat_retries_identical_packets_bounded_by_existing_settings(self): self.run_case("heard-retries") @@ -231,6 +237,12 @@ class CompanionMeshtextTest(unittest.TestCase): def test_announcement_alias_settings_key_changes_and_failure_do_not_storm(self): self.run_case("announcement-lifecycle") + def test_advert_name_change_cancels_only_stale_metadata_and_waits_for_new_chat(self): + self.run_case("advert-name-delivery") + + def test_same_tuple_publish_busy_release_refreshes_without_stealing_external_owner(self): + self.run_case("readiness-cache") + def test_delivery_service_is_wired_before_radio_processing_and_paused_for_morse(self): source = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text(encoding="utf-8") loop = body(source, "void MyMesh::loop()")