Fix Meshtext metadata lifecycle and radio2 readiness refresh
Delay automatic GPS-free NodeInfo until a quiet TX gap, invalidate stale metadata after durable node renames, and refresh the profile when same-tuple publication releases its busy guard. Cover the real Companion methods and document the behavior.
This commit is contained in:
1 parent
8687b027d6
commit
bc1b414af0
8 files changed
+220
-14
No files matched your search
@@ -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 <channel slot>` 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.
|
||||
|
||||
|
||||
@@ -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 <channel slot>` still explicitly queues **one** standard
|
||||
`NODEINFO_APP` broadcast on the selected configured MT channel. The slot is
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<int>(mesh::mt::DeliverySlots)) {
|
||||
|
||||
@@ -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 <!8hex|default> (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 <channel slot> (one manual advert; no automatic adverts)\r\n"
|
||||
" send mt.nodeinfo <channel slot> (GPS-free; also automatic after first chat)\r\n"
|
||||
" set mt.preset <longfast|shortturbo|...> (saved manual radio2)\r\n"
|
||||
" set mt.channel <slot> <mt-name> <32/64 hex digits|AQ==>\r\n"
|
||||
" get mt.channel <slot> / delete mt.channel <slot>\r\n"
|
||||
|
||||
@@ -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";
|
||||
|
||||
Vendored
+167
-7
@@ -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<uint8_t>(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<uint8_t>(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");
|
||||
}
|
||||
@@ -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()")
|
||||
|
||||
Reference in new issue
Block a user