Announce MeshTalk identity after first chat and retry until heard
This commit is contained in:
1 parent
0a6658a47f
commit
8f2bbbab19
11 files changed
+730
-45
No files matched your search
+17
-1
@@ -9,7 +9,11 @@ RX-only mode, cross-transmission policy, and optional radio preambles.
|
||||
|
||||
For supported Full Companions, see [Meshtext channel chat](radio_profiles.md#meshtext-channel-chat)
|
||||
for `mt-` channels and the explicit `send mt.nodeinfo <channel slot>` command.
|
||||
Chat remains plain text; NodeInfo is sent only when that command is requested.
|
||||
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
|
||||
`flood.retry.count`, with the metadata cap and `flood.retry.advert` gate for
|
||||
NodeInfo.
|
||||
|
||||
See [carrier-wave RF checks](carrier_wave.md) for `cw on [seconds]`,
|
||||
`cw2 on [seconds]`, and stopping a carrier with `off`.
|
||||
@@ -4389,6 +4393,18 @@ The `infra`, `rooftop`, and `mobile` defaults and `retry.preset` command in this
|
||||
section apply to infrastructure roles. Companion does not expose repeater
|
||||
bridge or prefix retry filters.
|
||||
|
||||
On builds with [Meshtext channel chat](radio_profiles.md#heard-repeat-retries),
|
||||
`flood.retry.count` also limits retries of the node's own MT channel text.
|
||||
Retries reuse the original encoded packet and stop after hearing a matching
|
||||
forwarded copy with a lower remaining hop limit. This is not a recipient ACK
|
||||
or a delivery guarantee. NodeInfo permits at most one additional transmission,
|
||||
only with nonzero retry count and `flood.retry.advert on`, and waits an extra
|
||||
minute beyond its airtime-based delay. These settings never enable relaying of
|
||||
received MT traffic or apply MeshCore prefix/bridge rules to MT packets.
|
||||
`set mt.hops 0` disables heard-repeat retries, without suppressing the original
|
||||
chat or its first-use NodeInfo. `get mt.retry` reports retry/heard/exhausted
|
||||
counters, pending work and the configured count without changing settings.
|
||||
|
||||
**Search terms:** flood tx retries, flood retry attempts, flood retransmissions, broadcast retries.
|
||||
|
||||
**Usage:**
|
||||
|
||||
+54
-11
@@ -465,6 +465,7 @@ get mt.status
|
||||
get mt.region
|
||||
get mt.slot
|
||||
get mt.hops
|
||||
get mt.retry
|
||||
get mt.node
|
||||
get mt.shortname
|
||||
set mt.node !12345678
|
||||
@@ -518,21 +519,28 @@ changed. `set mt.shortname default` clears the override and uses the last four
|
||||
hexadecimal digits of the effective MT node ID. This does not change the
|
||||
MeshCore node name, add a label to chat text or transmit anything.
|
||||
|
||||
`send mt.nodeinfo <channel slot>` explicitly queues **one** standard
|
||||
After the first chat packet actually finishes transmitting on a configured MT
|
||||
channel, the Companion queues a separate, GPS-free `NODEINFO_APP` broadcast.
|
||||
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.
|
||||
|
||||
`send mt.nodeinfo <channel slot>` still explicitly queues **one** standard
|
||||
`NODEINFO_APP` broadcast on the selected configured MT channel. The slot is
|
||||
the Companion channel index, not `mt.slot` (the frequency slot). The packet
|
||||
contains the actual sender ID, the global MeshCore node name as its long name
|
||||
(UTF-8-safe truncated to 24 bytes), the effective short name, `CLIENT_MUTE`
|
||||
(no relaying), and the no-DM flag. It omits MAC, public keys and unsupported
|
||||
metadata. The usual radio2 TX-mode, pending-TX, airtime and profile guards
|
||||
apply. The reply says **queued**, not delivered or acknowledged.
|
||||
(no relaying), and the no-DM flag. Both automatic and explicit NodeInfo omit
|
||||
GPS/coordinates, MAC, public keys and unsupported metadata. The usual radio2
|
||||
TX-mode, pending-TX, airtime and profile guards apply. The reply says
|
||||
**queued**, not delivered or acknowledged.
|
||||
|
||||
There are no automatic NodeInfo advertisements: not at boot, on a timer, when
|
||||
names change, when chat is sent, or in response to received packets. Received
|
||||
NodeInfo is still ignored; this backend does not maintain a node database.
|
||||
Received NodeInfo is still ignored; this backend does not maintain a node
|
||||
database.
|
||||
|
||||
Outgoing Meshtext chat and explicitly requested NodeInfo set **OK to MQTT**
|
||||
on by default, allowing a compatible receiving gateway to upload those packets.
|
||||
Outgoing Meshtext chat and automatic or explicitly requested NodeInfo set
|
||||
**OK to MQTT** on by default, allowing a compatible receiving gateway to upload those packets.
|
||||
This does not start an MQTT client, bridge networks, request a reply, or mark
|
||||
the packet as having arrived through MQTT. The 228-byte text limit is unchanged.
|
||||
|
||||
@@ -630,12 +638,12 @@ Outgoing MT chat is plain text exactly as entered, with the sender node ID in
|
||||
the standard radio header. It contains no custom name envelope or NodeInfo.
|
||||
Incoming text uses the exact sender-ID prefix `!12345678: ` before the received
|
||||
payload when passed to the MeshCore app. Changing the shortname only changes
|
||||
future manually requested NodeInfo, not chat or the packet's sender ID;
|
||||
future NodeInfo, not chat or the packet's sender ID;
|
||||
`set mt.node` changes the actual Meshtext sender ID. Neither setting change
|
||||
emits advertisements. A successful
|
||||
send response means **queued**, not acknowledged by a remote node. This backend
|
||||
does not relay MT packets or provide ACKs, administrative commands, direct
|
||||
messages, automatic node-info adverts or a node database. MeshCore emergency forwarding
|
||||
messages, periodic node-info adverts or a node database. MeshCore emergency forwarding
|
||||
does not apply to MT traffic. Interoperability is with stock peers' PSK channel
|
||||
text using AES-CTR, not AES-CCM. AES-CTR channel traffic is not authenticated;
|
||||
successful decoding is not proof of sender identity.
|
||||
@@ -657,6 +665,41 @@ The compatible protocol's fixed short preamble also does not provide the scan-aw
|
||||
preamble described below. Do not treat this as continuous coverage of both
|
||||
networks or guaranteed message delivery.
|
||||
|
||||
#### Heard-repeat retries
|
||||
|
||||
MT channel chat uses the Companion's saved `flood.retry.count` budget: `0`
|
||||
disables retries, and `1`–`15` allows that many extra transmissions after the
|
||||
initial send. Locally originated MT traffic is hop zero for the
|
||||
`flood.retry.path` gate, so a gate of `0` still permits it; the group-data gate
|
||||
does not apply to channel text. `mt.hops 0` disables heard-repeat retries
|
||||
because peers cannot forward a packet with no remaining hop budget; the
|
||||
original chat and its first-use NodeInfo still transmit normally.
|
||||
Retries keep the exact encoded payload and
|
||||
packet ID, allowing peers to recognize duplicates. They use conservative,
|
||||
airtime-based flood spacing and the normal CAD and airtime limits rather than
|
||||
transmitting continuously.
|
||||
|
||||
`get mt.retry` is read-only diagnostics: it reports retry transmissions,
|
||||
matching heard repeats, exhausted retry sequences, pending work and the
|
||||
configured retry count. Use `set flood.retry.count`, not this getter, to change
|
||||
the budget.
|
||||
|
||||
A matching over-air copy with a lower remaining hop limit cancels the retry
|
||||
sequence. Merely receiving another message, an unchanged-hop copy or a packet
|
||||
with the same ID but different payload does not. A heard repeat indicates a
|
||||
compatible peer forwarded the packet; it does **not** confirm that a particular
|
||||
person or app received it. This does not add DM delivery receipts or ACKs.
|
||||
|
||||
NodeInfo follows the metadata retry cap: at most one extra send, only when
|
||||
`flood.retry.count` is nonzero and `flood.retry.advert` is `on`. Its wait adds
|
||||
one minute to the airtime-based delay, matching the conservative self-advert
|
||||
policy. Turning retry count off cancels retained retries. Channel, identity,
|
||||
settings or radio-profile changes invalidate retained work rather than sending
|
||||
it on a different network. The bounded pending-frame storage may reject a new
|
||||
send when full. Other people's MT packets are never forwarded by this backend;
|
||||
MeshCore repeater prefix/bridge IDs and flood-filter rules are not MT routing
|
||||
rules.
|
||||
|
||||
## Choose whether traffic crosses between profiles
|
||||
|
||||
```text
|
||||
|
||||
@@ -60,11 +60,35 @@ send mt.nodeinfo 0
|
||||
channel 0 Hello
|
||||
```
|
||||
|
||||
Use a configured `mt-` channel slot. The explicit `send mt.nodeinfo` command
|
||||
queues one standard name/ID advertisement; chat carries only the entered text.
|
||||
There are no automatic NodeInfo sends or responses. The long name comes from
|
||||
the existing node name; `get mt.shortname` shows the effective four-byte short
|
||||
name. These commands are not added to smaller or feature-disabled builds.
|
||||
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.
|
||||
`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;
|
||||
`get mt.shortname` shows the effective four-byte short name. These commands
|
||||
are not added to smaller or feature-disabled builds.
|
||||
|
||||
MT chat uses `flood.retry.count` (`0`–`15`) to retry when no matching forwarded
|
||||
copy is heard. A heard repeat stops retries, but is not a recipient delivery
|
||||
receipt. NodeInfo has at most one retry, only with a nonzero count and
|
||||
`flood.retry.advert on`, with an extra one-minute wait. For example:
|
||||
|
||||
```text
|
||||
get flood.retry.count
|
||||
set flood.retry.count 3
|
||||
set flood.retry.advert on
|
||||
get mt.retry
|
||||
```
|
||||
|
||||
`get mt.retry` shows retry/heard/exhausted counters, pending work and the
|
||||
configured count; it does not change settings. With `set mt.hops 0`, no peer
|
||||
can forward a copy, so heard-repeat retries are disabled. The original chat
|
||||
and first-use NodeInfo are unaffected.
|
||||
|
||||
This does not make the Companion relay other people's MT packets. See
|
||||
[heard-repeat retries](radio_profiles.md#heard-repeat-retries) for the airtime
|
||||
limits, exact-packet matching and cancellation rules.
|
||||
|
||||
Local means a connection directly to the node: USB, BLE or WiFi/Ethernet
|
||||
Companion protocol, a TCP terminal, or the browser CLI on the LAN. These
|
||||
|
||||
@@ -65,6 +65,16 @@ bool describe(const ChannelDetails& channel, mesh::mt::Channel& wire) {
|
||||
mesh::Utils::sha256(out, size, in, length);
|
||||
});
|
||||
}
|
||||
bool channelFingerprint(const ChannelDetails& channel, uint32_t node, uint8_t out[8]) {
|
||||
mesh::mt::Channel wire;
|
||||
if (!describe(channel, wire)) return false;
|
||||
uint8_t input[48] = {};
|
||||
memcpy(input, wire.name, sizeof(wire.name));
|
||||
memcpy(input + 12, wire.key, sizeof(wire.key));
|
||||
for (unsigned i = 0; i < 4; ++i) input[44 + i] = node >> (8 * i);
|
||||
mesh::Utils::sha256(out, 8, input, sizeof(input));
|
||||
return true;
|
||||
}
|
||||
bool nextToken(const char*& cursor, char* out, size_t capacity) {
|
||||
while (*cursor == ' ' || *cursor == '\t') ++cursor;
|
||||
size_t size = 0;
|
||||
@@ -87,6 +97,9 @@ mesh::mt::Region MyMesh::meshtextRegion() const {
|
||||
}
|
||||
|
||||
void MyMesh::refreshMeshtextIdentity() {
|
||||
for (auto& frame : _mt_delivery) frame.cancelled = true;
|
||||
memset(_mt_nodeinfo_announced, 0, sizeof(_mt_nodeinfo_announced));
|
||||
memset(_mt_nodeinfo_pending, 0, sizeof(_mt_nodeinfo_pending));
|
||||
uint8_t digest[4];
|
||||
mesh::Utils::sha256(digest, sizeof(digest), self_id.pub_key, PUB_KEY_SIZE);
|
||||
_mt_default_node = uint32_t(digest[0]) | (uint32_t(digest[1]) << 8)
|
||||
@@ -194,7 +207,13 @@ bool MyMesh::saveMeshtextSettings(mesh::mt::Region region, uint32_t slot, uint8_
|
||||
refreshMeshtextProfile();
|
||||
return false;
|
||||
}
|
||||
const bool announcement_changed = region != _mt_region || slot != _mt_slot
|
||||
|| node_override != _mt_node_override || strcmp(short_name, _mt_short_name) != 0;
|
||||
_mt_region = region; _mt_slot = slot; _mt_hop_limit = hops; _mt_settings_valid = true;
|
||||
for (auto& frame : _mt_delivery) frame.cancelled = true;
|
||||
if (announcement_changed)
|
||||
memset(_mt_nodeinfo_announced, 0, sizeof(_mt_nodeinfo_announced));
|
||||
memset(_mt_nodeinfo_pending, 0, sizeof(_mt_nodeinfo_pending));
|
||||
memcpy(_mt_short_name, image + 12, sizeof(_mt_short_name));
|
||||
_mt_node_override = node_override;
|
||||
_mt_node = node_override ? node_override : _mt_default_node;
|
||||
@@ -207,6 +226,8 @@ bool MyMesh::saveMeshtextSettings(mesh::mt::Region region, uint32_t slot, uint8_
|
||||
}
|
||||
|
||||
bool MyMesh::eraseMeshtextSettings() {
|
||||
for (auto& frame : _mt_delivery) frame.cancelled = true;
|
||||
memset(_mt_nodeinfo_pending, 0, sizeof(_mt_nodeinfo_pending));
|
||||
// The filesystem erase also removes channels/identity on the next boot.
|
||||
// Do not keep sending with their still-resident RAM copies until then.
|
||||
_mt_active = false;
|
||||
@@ -387,6 +408,136 @@ void MyMesh::refreshMeshtextProfile() {
|
||||
_mt_status = _mt_active ? "active" : "radio2 manually off, leased, scheduled or busy";
|
||||
}
|
||||
|
||||
int MyMesh::meshtextChannelIndex(const ChannelDetails& channel, uint8_t fingerprint[8]) {
|
||||
if (!channelFingerprint(channel, _mt_node, fingerprint)) return -1;
|
||||
for (int i = 0; i < MAX_GROUP_CHANNELS; ++i) {
|
||||
ChannelDetails candidate;
|
||||
uint8_t other[8];
|
||||
if (BaseChatMesh::getChannel(i, candidate)
|
||||
&& channelFingerprint(candidate, _mt_node, other)
|
||||
&& memcmp(other, fingerprint, sizeof(other)) == 0) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool MyMesh::meshtextDeliveryCurrent(const mesh::mt::PendingFrame& frame) {
|
||||
const auto* profiles = _radio->profiles();
|
||||
if (frame.cancelled || !_mt_active || !profiles || !profiles->canTransmit(1)
|
||||
|| profiles->generation[1] != frame.generation
|
||||
|| profiles->secondary.params.protocol != mesh::RadioProfileProtocol::Meshtext) return false;
|
||||
ChannelDetails channel;
|
||||
uint8_t fingerprint[8];
|
||||
return BaseChatMesh::getChannel(frame.channel, channel)
|
||||
&& channelFingerprint(channel, _mt_node, fingerprint)
|
||||
&& memcmp(frame.fingerprint, fingerprint, sizeof(fingerprint)) == 0;
|
||||
}
|
||||
|
||||
uint8_t MyMesh::meshtextRetryLimit(const mesh::mt::PendingFrame& frame) const {
|
||||
// These are local, zero-hop origins: any general path gate admits them.
|
||||
// GRP_DATA-only gates do not apply to channel text. No forwarding echo can
|
||||
// exist when mt.hops is zero, so don't force all configured duplicates.
|
||||
if (!(frame.bytes[12] & 7)) return 0;
|
||||
const uint8_t count = _prefs.flood_retry_attempts > 15 ? 15 : _prefs.flood_retry_attempts;
|
||||
return frame.nodeinfo ? (_prefs.flood_retry_advert_enabled && count ? 1 : 0) : count;
|
||||
}
|
||||
|
||||
uint32_t MyMesh::meshtextRetryDelay(const mesh::mt::PendingFrame& frame) {
|
||||
const uint64_t airtime = _radio->getProfileAirtime(1, frame.length);
|
||||
// Same conservative flood wait/jitter and one-minute metadata extension,
|
||||
// using the FOREIGN frame's real length and secondary PHY, not MC framing.
|
||||
uint64_t delay = _radio->getProfileAirtime(1, mesh::mt::MaxFrameSize)
|
||||
+ 20 * airtime + airtime * getRNG()->nextInt(0, 201) / 100;
|
||||
if (frame.nodeinfo) delay += 60000;
|
||||
if (!delay) delay = 1;
|
||||
return delay > 0x7fffffffULL ? 0x7fffffffU : static_cast<uint32_t>(delay);
|
||||
}
|
||||
|
||||
bool MyMesh::queueMeshtextDelivery(const ChannelDetails& channel, const uint8_t* bytes,
|
||||
size_t length, bool nodeinfo) {
|
||||
uint8_t fingerprint[8];
|
||||
const int index = meshtextChannelIndex(channel, fingerprint);
|
||||
if (index < 0 || !bytes || length <= mesh::mt::HeaderSize
|
||||
|| length > mesh::mt::MaxFrameSize || hasForeignRadioFrame()) return false;
|
||||
// Metadata has a reserved slot so a burst of chats awaiting echoes cannot
|
||||
// prevent the first-use announcement. Backpressure is explicit, never a
|
||||
// successful admission which silently drops retry state.
|
||||
const unsigned first = nodeinfo ? mesh::mt::ChatDeliverySlots : 0;
|
||||
const unsigned end = nodeinfo ? mesh::mt::DeliverySlots : mesh::mt::ChatDeliverySlots;
|
||||
for (unsigned i = first; i < end; ++i) {
|
||||
auto& frame = _mt_delivery[i];
|
||||
if (frame.length) continue;
|
||||
const auto* profiles = _radio->profiles();
|
||||
if (!profiles || !queueForeignRadioFrame(bytes, length, 1, profiles->generation[1])) return false;
|
||||
frame = {};
|
||||
memcpy(frame.bytes, bytes, length);
|
||||
memcpy(frame.fingerprint, fingerprint, sizeof(fingerprint));
|
||||
frame.length = length; frame.channel = index; frame.nodeinfo = nodeinfo;
|
||||
frame.generation = profiles->generation[1];
|
||||
_mt_tx_slot = i;
|
||||
if (memcmp(_mt_nodeinfo_fingerprint[index], fingerprint, sizeof(fingerprint))) {
|
||||
memcpy(_mt_nodeinfo_fingerprint[index], fingerprint, sizeof(fingerprint));
|
||||
_mt_nodeinfo_announced[index] = _mt_nodeinfo_pending[index] = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void MyMesh::loopMeshtextDelivery() {
|
||||
const auto* profiles = _radio->profiles();
|
||||
const uint32_t now = millis();
|
||||
const bool usable = _mt_active && profiles && profiles->canTransmit(1)
|
||||
&& profiles->secondary.params.protocol == mesh::RadioProfileProtocol::Meshtext;
|
||||
if (!usable || profiles->generation[1] != _mt_nodeinfo_generation) {
|
||||
memset(_mt_nodeinfo_pending, 0, sizeof(_mt_nodeinfo_pending));
|
||||
}
|
||||
for (unsigned i = 0; i < mesh::mt::DeliverySlots; ++i) {
|
||||
auto& frame = _mt_delivery[i];
|
||||
if (!frame.length) continue;
|
||||
if (!usable || profiles->generation[1] != frame.generation
|
||||
|| (_mt_delivery_config_dirty && !meshtextDeliveryCurrent(frame))) frame.cancelled = true;
|
||||
if (frame.cancelled || (frame.sent && !meshtextRetryLimit(frame))) {
|
||||
if (_mt_tx_slot == static_cast<int>(i)) {
|
||||
frame.cancelled = true;
|
||||
discardQueuedForeignRadioFrame(mesh::RadioProfileProtocol::Meshtext);
|
||||
} else frame.clear();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
_mt_delivery_config_dirty = false;
|
||||
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) {
|
||||
if (!_mt_nodeinfo_pending[i]) continue;
|
||||
ChannelDetails channel;
|
||||
uint8_t fingerprint[8];
|
||||
if (!BaseChatMesh::getChannel(i, channel)
|
||||
|| !channelFingerprint(channel, _mt_node, fingerprint)
|
||||
|| memcmp(fingerprint, _mt_nodeinfo_fingerprint[i], sizeof(fingerprint))) {
|
||||
_mt_nodeinfo_pending[i] = false;
|
||||
continue;
|
||||
}
|
||||
if (_mt_delivery[mesh::mt::ChatDeliverySlots].length) break;
|
||||
if (sendMeshtextNodeInfo(channel)) return;
|
||||
// Invalid names/admission failure must not become an unbounded automatic
|
||||
// transmit/encoding loop. Another successfully sent chat can try again.
|
||||
_mt_nodeinfo_pending[i] = false;
|
||||
}
|
||||
for (unsigned i = 0; i < mesh::mt::DeliverySlots; ++i) {
|
||||
auto& frame = _mt_delivery[i];
|
||||
if (!frame.length || !frame.sent || static_cast<int32_t>(now - frame.due) < 0) continue;
|
||||
if (!meshtextDeliveryCurrent(frame)) { frame.clear(); continue; }
|
||||
if (frame.retries >= meshtextRetryLimit(frame)) {
|
||||
++_mt_exhausted; frame.clear(); continue;
|
||||
}
|
||||
if (queueForeignRadioFrame(frame.bytes, frame.length, 1, frame.generation)) {
|
||||
++frame.retries; _mt_tx_slot = i;
|
||||
}
|
||||
return; // Queue contention doesn't consume an attempt or spin.
|
||||
}
|
||||
}
|
||||
|
||||
bool MyMesh::sendMeshtextText(const ChannelDetails& channel, const char* text, size_t length) {
|
||||
refreshMeshtextProfile();
|
||||
if (!_mt_active || !text || !length || hasForeignRadioFrame()) return false;
|
||||
@@ -396,7 +547,7 @@ bool MyMesh::sendMeshtextText(const ChannelDetails& channel, const char* text, s
|
||||
mesh::mt::Channel wire;
|
||||
if (!describe(channel, wire)) return false;
|
||||
// Stock TEXT_MESSAGE_APP contains the user's text only. Sender identity is
|
||||
// in the radio header; name metadata requires a separately requested User.
|
||||
// in the radio header; first-use name metadata is a separate User packet.
|
||||
if (length > mesh::mt::MaxTextSize) return false;
|
||||
if (!_mt_packet_id) return false; // exhaustion: do not wrap/reuse CTR nonces.
|
||||
const uint32_t id = _mt_packet_id++;
|
||||
@@ -405,7 +556,7 @@ bool MyMesh::sendMeshtextText(const ChannelDetails& channel, const char* text, s
|
||||
// This does not enable forwarding or ACK responses in this Companion.
|
||||
const size_t size = mesh::mt::encodeText(_mt_node, id, _mt_hop_limit, wire.name, wire.key,
|
||||
wire.key_length, text, length, raw);
|
||||
return size && queueForeignRadioFrame(raw, size, 1, profiles->generation[1]);
|
||||
return size && queueMeshtextDelivery(channel, raw, size, false);
|
||||
}
|
||||
|
||||
bool MyMesh::sendMeshtextNodeInfo(const ChannelDetails& channel) {
|
||||
@@ -428,8 +579,8 @@ bool MyMesh::sendMeshtextNodeInfo(const ChannelDetails& channel) {
|
||||
uint8_t raw[mesh::mt::MaxFrameSize];
|
||||
const size_t size = mesh::mt::encodeNodeInfo(_mt_node, id, _mt_hop_limit, wire.name, wire.key,
|
||||
wire.key_length, _prefs.node_name, long_length, short_name, strlen(short_name), raw);
|
||||
// Only the explicit CLI reaches here. No timer, RX reply or chat piggyback.
|
||||
return size && queueForeignRadioFrame(raw, size, 1, profiles->generation[1]);
|
||||
// Explicit CLI or first successfully transmitted chat, never a GPS packet.
|
||||
return size && queueMeshtextDelivery(channel, raw, size, true);
|
||||
}
|
||||
|
||||
bool MyMesh::onForeignRadioFrame(const uint8_t* bytes, size_t length, uint8_t profile,
|
||||
@@ -443,6 +594,17 @@ bool MyMesh::onForeignRadioFrame(const uint8_t* bytes, size_t length, uint8_t pr
|
||||
(void)rssi;
|
||||
const auto* profiles = _radio->profiles();
|
||||
if (!_mt_active || !profiles || profile != 1 || generation != profiles->generation[1]) return false;
|
||||
for (unsigned i = 0; i < mesh::mt::DeliverySlots; ++i) {
|
||||
auto& frame = _mt_delivery[i];
|
||||
if (frame.generation != generation || !frame.matchesEcho(bytes, length)
|
||||
|| !meshtextDeliveryCurrent(frame)) continue;
|
||||
if (!frame.heard) ++_mt_heard;
|
||||
frame.heard = true;
|
||||
if (_mt_tx_slot == static_cast<int>(i))
|
||||
discardQueuedForeignRadioFrame(mesh::RadioProfileProtocol::Meshtext);
|
||||
else frame.clear();
|
||||
return true; // Own echo is never displayed, forwarded or ACKed.
|
||||
}
|
||||
for (int i = 0; i < MAX_GROUP_CHANNELS; ++i) {
|
||||
ChannelDetails channel;
|
||||
mesh::mt::Channel wire;
|
||||
@@ -477,8 +639,31 @@ void MyMesh::onForeignRadioSendComplete(bool success) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (success) ++_mt_sent;
|
||||
else ++_mt_failed;
|
||||
const int slot = _mt_tx_slot;
|
||||
_mt_tx_slot = -1;
|
||||
if (slot < 0 || slot >= static_cast<int>(mesh::mt::DeliverySlots)) {
|
||||
if (success) ++_mt_sent; else ++_mt_failed;
|
||||
return; // e.g. an explicitly configured text beacon, not delivery work.
|
||||
}
|
||||
auto& frame = _mt_delivery[slot];
|
||||
if (frame.nodeinfo && !success) _mt_nodeinfo_pending[frame.channel] = false;
|
||||
if (success) {
|
||||
++_mt_sent;
|
||||
if (frame.retries) ++_mt_retried;
|
||||
}
|
||||
else if (!frame.heard && !frame.cancelled) ++_mt_failed;
|
||||
if (!success || frame.cancelled || !meshtextDeliveryCurrent(frame)) { frame.clear(); return; }
|
||||
frame.sent = true;
|
||||
const int index = frame.channel;
|
||||
if (frame.nodeinfo) {
|
||||
_mt_nodeinfo_announced[index] = true;
|
||||
_mt_nodeinfo_pending[index] = false;
|
||||
} else if (!frame.retries && !_mt_nodeinfo_announced[index]) {
|
||||
_mt_nodeinfo_pending[index] = true;
|
||||
_mt_nodeinfo_generation = frame.generation;
|
||||
}
|
||||
if (frame.heard || !meshtextRetryLimit(frame)) frame.clear();
|
||||
else frame.due = millis() + meshtextRetryDelay(frame);
|
||||
}
|
||||
|
||||
bool MyMesh::handleMeshtextCommand(const char* command, char* reply, size_t capacity) {
|
||||
@@ -495,7 +680,7 @@ bool MyMesh::handleMeshtextCommand(const char* command, char* reply, size_t capa
|
||||
} else if (!sendMeshtextNodeInfo(channel)) {
|
||||
snprintf(reply, capacity, "Error: NodeInfo not queued; check get mt.status, TX mode or pending TX");
|
||||
} else {
|
||||
snprintf(reply, capacity, "OK - NodeInfo queued on Meshtext channel %lu; no automatic adverts",
|
||||
snprintf(reply, capacity, "OK - NodeInfo queued on Meshtext channel %lu; no GPS",
|
||||
(unsigned long)index);
|
||||
}
|
||||
return true;
|
||||
@@ -575,6 +760,14 @@ bool MyMesh::handleMeshtextCommand(const char* command, char* reply, size_t capa
|
||||
return true;
|
||||
}
|
||||
const bool status = !strcmp(command, "get mt") || !strcmp(command, "get mt.status");
|
||||
if (!strcmp(command, "get mt.retry")) {
|
||||
unsigned pending = 0;
|
||||
for (const auto& frame : _mt_delivery) if (frame.length) ++pending;
|
||||
snprintf(reply, capacity, "> retry=%lu heard=%lu exhausted=%lu pending=%u count=%u advert=%s",
|
||||
(unsigned long)_mt_retried, (unsigned long)_mt_heard, (unsigned long)_mt_exhausted,
|
||||
pending, _prefs.flood_retry_attempts, _prefs.flood_retry_advert_enabled ? "on" : "off");
|
||||
return true;
|
||||
}
|
||||
const bool get_region = !strcmp(command, "get mt.region");
|
||||
const bool get_slot = !strcmp(command, "get mt.slot");
|
||||
const bool get_hops = !strcmp(command, "get mt.hops");
|
||||
@@ -643,7 +836,7 @@ bool MyMesh::handleMeshtextCommand(const char* command, char* reply, size_t capa
|
||||
_mt_node_override ? "saved override" : "identity default");
|
||||
else if (get_short_name) {
|
||||
char short_name[5]; makeWireShortName(_mt_short_name, _mt_node, short_name);
|
||||
snprintf(reply, capacity, "> %s; effective=%s (manual NodeInfo only)",
|
||||
snprintf(reply, capacity, "> %s; effective=%s (first-chat/manual NodeInfo)",
|
||||
_mt_short_name[0] ? _mt_short_name : "default", short_name);
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -1722,6 +1722,9 @@ bool MyMesh::setChannel(int channel_idx, const ChannelDetails& channel) {
|
||||
&& parseCompanionChannelAlias(channel.name).kind != CompanionChannelKind::MeshCore) return false;
|
||||
#endif
|
||||
const bool saved = BaseChatMesh::setChannel(channel_idx, channel);
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT
|
||||
if (saved) _mt_delivery_config_dirty = true;
|
||||
#endif
|
||||
#if MESH_ENABLE_ETHERNET_TUNNEL
|
||||
if (saved) ++_ethernet_channel_generation;
|
||||
#endif
|
||||
@@ -11272,6 +11275,7 @@ void MyMesh::loop() {
|
||||
#endif
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT
|
||||
refreshMeshtextProfile();
|
||||
if (!mesh::morseTimingActive(_radio)) loopMeshtextDelivery();
|
||||
#endif
|
||||
#if MESH_ENABLE_ETHERNET_TUNNEL
|
||||
if (!mesh::morseTimingActive(_radio)) loopEthernetTunnel();
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
namespace mesh { class BeaconManager; }
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT
|
||||
#include <helpers/CompanionMeshtextChannel.h>
|
||||
#include <helpers/MeshtextDelivery.h>
|
||||
#endif
|
||||
#include <helpers/CompanionNotificationPolicy.h>
|
||||
#include "NodePrefs.h"
|
||||
@@ -430,6 +431,13 @@ protected:
|
||||
bool eraseMeshtextSettings();
|
||||
bool sendMeshtextText(const ChannelDetails& channel, const char* text, size_t length);
|
||||
bool sendMeshtextNodeInfo(const ChannelDetails& channel);
|
||||
bool queueMeshtextDelivery(const ChannelDetails& channel, const uint8_t* bytes,
|
||||
size_t length, bool nodeinfo);
|
||||
int meshtextChannelIndex(const ChannelDetails& channel, uint8_t fingerprint[8]);
|
||||
bool meshtextDeliveryCurrent(const mesh::mt::PendingFrame& pending);
|
||||
uint8_t meshtextRetryLimit(const mesh::mt::PendingFrame& pending) const;
|
||||
uint32_t meshtextRetryDelay(const mesh::mt::PendingFrame& pending);
|
||||
void loopMeshtextDelivery();
|
||||
mesh::mt::Region meshtextRegion() const;
|
||||
#endif
|
||||
bool queueChannelText(uint8_t index, const uint8_t secret[32], float snr,
|
||||
@@ -754,6 +762,14 @@ private:
|
||||
uint8_t _mt_hop_limit = 7;
|
||||
uint32_t _mt_sent = 0, _mt_failed = 0, _mt_received = 0;
|
||||
mesh::mt::SeenPackets _mt_seen;
|
||||
mesh::mt::PendingFrame _mt_delivery[mesh::mt::DeliverySlots];
|
||||
int8_t _mt_tx_slot = -1;
|
||||
uint8_t _mt_nodeinfo_fingerprint[MAX_GROUP_CHANNELS][8] = {};
|
||||
bool _mt_nodeinfo_announced[MAX_GROUP_CHANNELS] = {};
|
||||
bool _mt_nodeinfo_pending[MAX_GROUP_CHANNELS] = {};
|
||||
uint32_t _mt_nodeinfo_generation = 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;
|
||||
uint32_t _mt_primary_generation = 0, _mt_secondary_generation = 0, _mt_intent_generation = 0;
|
||||
|
||||
+2
-1
@@ -498,7 +498,8 @@ bool Dispatcher::checkForeignRadioSend() {
|
||||
foreign_radio_start = _ms->getMillis();
|
||||
if (!_radio->startSendRaw(foreign_radio_bytes, foreign_radio_length)) {
|
||||
// RadioLib owns failure cleanup (including the board's FEM). Unlike
|
||||
// MeshCore reliable routing, foreign chat never retries autonomously.
|
||||
// MeshCore reliable routing, this transport never retries autonomously.
|
||||
// Companion may later submit a separately validated retained chat copy.
|
||||
finishForeignRadioFrame(false, false);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
// Bounded adaptation of MIT-licensed esp-cpp/espp wire helpers.
|
||||
// See licenses/ESPP-Meshtext-MIT.txt for the pinned source and license.
|
||||
// Meshtext is a bounded text interoperability component, not a peer firmware fork.
|
||||
// NodeInfo may only be explicitly encoded; received NodeInfo stays ignored.
|
||||
// NodeInfo is a separate GPS-free User packet; received NodeInfo stays ignored.
|
||||
namespace mesh {
|
||||
namespace mt {
|
||||
|
||||
@@ -47,7 +47,7 @@ struct DecodedText {
|
||||
|
||||
// Expanded AES-128/256 PSK only. No unencrypted-key fallback. Names are exact,
|
||||
// canonical peer channel names (<12 bytes), without local display markers.
|
||||
// Outgoing text and explicit NodeInfo permit peer MQTT upload (Data.bitfield
|
||||
// Outgoing text and NodeInfo permit peer MQTT upload (Data.bitfield
|
||||
// bit 0); this neither enables a local MQTT client nor marks traffic via_mqtt.
|
||||
// Returns 0 for invalid arguments, malformed UTF-8, embedded NUL or overflow.
|
||||
size_t encodeText(uint32_t from, uint32_t id, uint8_t hop_limit, const char* channel_name,
|
||||
@@ -59,7 +59,8 @@ size_t encodeText(uint32_t from, uint32_t id, uint8_t hop_limit, const char* cha
|
||||
// names may be empty; short names require 1..4 UTF-8 bytes (stock wire bound).
|
||||
// This can hold four ASCII characters but fewer multi-byte characters. Longer
|
||||
// names are rejected, not byte-truncated. No MAC, PSK, public key or MC identity
|
||||
// is copied. No ACK/response is requested and no automatic send is performed.
|
||||
// is copied. No GPS/position or ACK/response is requested. The caller controls
|
||||
// explicit and first-successful-chat announcements and bounded retries.
|
||||
size_t encodeNodeInfo(uint32_t from, uint32_t id, uint8_t hop_limit, const char* channel_name,
|
||||
const uint8_t* key, size_t key_len, const char* long_name, size_t long_name_len,
|
||||
const char* short_name, size_t short_name_len, uint8_t out[MaxFrameSize]);
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include "MeshtextChat.h"
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh { namespace mt {
|
||||
|
||||
// Three retained chats and one metadata slot. No heap, packet-pool entries,
|
||||
// keys, positions or forwarding state are allocated by the delivery policy.
|
||||
struct PendingFrame {
|
||||
uint8_t bytes[MaxFrameSize] = {};
|
||||
uint8_t fingerprint[8] = {};
|
||||
uint32_t generation = 0, due = 0;
|
||||
uint16_t length = 0;
|
||||
uint8_t channel = 0, retries = 0;
|
||||
bool nodeinfo = false, sent = false, heard = false, cancelled = false;
|
||||
|
||||
void clear() { length = 0; }
|
||||
bool matchesEcho(const uint8_t* raw, size_t size) const {
|
||||
// The sender/id/channel/ciphertext are immutable. Only remaining hops
|
||||
// and next-hop/relay bytes may change. A copy at the original hop limit
|
||||
// is not evidence that another node forwarded the transmission.
|
||||
return length && sent && raw && size == length && size > HeaderSize
|
||||
&& memcmp(raw, bytes, 12) == 0 && raw[13] == bytes[13]
|
||||
&& (raw[12] & 0xf8) == (bytes[12] & 0xf8)
|
||||
&& (raw[12] & 7) < (bytes[12] & 7)
|
||||
&& memcmp(raw + HeaderSize, bytes + HeaderSize, size - HeaderSize) == 0;
|
||||
}
|
||||
};
|
||||
|
||||
static constexpr unsigned DeliverySlots = 4;
|
||||
static constexpr unsigned ChatDeliverySlots = DeliverySlots - 1;
|
||||
|
||||
} } // namespace mesh::mt
|
||||
Vendored
+339
-13
@@ -3,6 +3,7 @@
|
||||
@FIXTURE@
|
||||
#include <SHA256.h>
|
||||
#include <helpers/CompanionMeshtextChannel.h>
|
||||
#include <helpers/MeshtextDelivery.h>
|
||||
#include <helpers/CompanionFrameLimits.h>
|
||||
#include <helpers/UTF8Helpers.h>
|
||||
#include <cmath>
|
||||
@@ -60,6 +61,7 @@ struct BaseChatMesh {
|
||||
struct MtRadio : Radio {
|
||||
bool supported=true;
|
||||
bool supportsMeshtext() const override { return supported; }
|
||||
uint32_t getProfileAirtime(uint8_t,int,uint8_t=0) override { return 10; }
|
||||
};
|
||||
struct Serial {
|
||||
bool connected=false;
|
||||
@@ -80,7 +82,11 @@ class MyMesh : public BaseChatMesh {
|
||||
public:
|
||||
MemoryFS fs; Clock clock; MtRadio radio; mesh::Radio* _radio=&radio;
|
||||
mesh::RadioProfileCLI _radio_profiles;
|
||||
struct {float freq=909.5; char node_name[32]="Test";} _prefs;
|
||||
struct {
|
||||
float freq=909.5; char node_name[32]="Test";
|
||||
uint8_t flood_retry_attempts=15,flood_retry_max_path=1;
|
||||
uint8_t flood_retry_group_max_path=0xff,flood_retry_advert_enabled=1;
|
||||
} _prefs;
|
||||
mesh::LocalIdentity self_id;
|
||||
mesh::mt::Region _mt_region=mesh::mt::Region::Unset;
|
||||
uint32_t _mt_slot=0,_mt_node=0,_mt_packet_id=0,_mt_sent=0,_mt_failed=0,_mt_received=0;
|
||||
@@ -88,10 +94,22 @@ public:
|
||||
uint8_t _mt_hop_limit=7;
|
||||
char _mt_short_name[17]={};
|
||||
mesh::mt::SeenPackets _mt_seen;
|
||||
mesh::mt::PendingFrame _mt_delivery[mesh::mt::DeliverySlots];
|
||||
int8_t _mt_tx_slot=-1;
|
||||
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;
|
||||
struct Random {
|
||||
uint32_t jitter=0;
|
||||
uint32_t nextInt(uint32_t low,uint32_t high) {
|
||||
assert(low==0 && high==201 && jitter<high); return jitter;
|
||||
}
|
||||
} random;
|
||||
bool _mt_settings_valid=true,_mt_active=false,_mt_profile_dirty=true,_mt_last_radio_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";
|
||||
bool _radio_available=true,foreign_pending=false,admit_foreign=true,save_result=true,ok=false;
|
||||
bool _radio_available=true,foreign_pending=false,foreign_sending=false,admit_foreign=true,save_result=true,ok=false;
|
||||
uint32_t foreign_generation=0; uint8_t foreign_profile=255;
|
||||
unsigned foreign_admissions=0,meshcore_sends=0;
|
||||
std::vector<std::vector<uint8_t>> foreign,offline;
|
||||
@@ -116,6 +134,12 @@ public:
|
||||
void refreshMeshtextProfile();
|
||||
bool sendMeshtextText(const ChannelDetails&,const char*,size_t);
|
||||
bool sendMeshtextNodeInfo(const ChannelDetails&);
|
||||
bool queueMeshtextDelivery(const ChannelDetails&,const uint8_t*,size_t,bool);
|
||||
int meshtextChannelIndex(const ChannelDetails&,uint8_t[8]);
|
||||
bool meshtextDeliveryCurrent(const mesh::mt::PendingFrame&);
|
||||
uint8_t meshtextRetryLimit(const mesh::mt::PendingFrame&) const;
|
||||
uint32_t meshtextRetryDelay(const mesh::mt::PendingFrame&);
|
||||
void loopMeshtextDelivery();
|
||||
bool onForeignRadioFrame(const uint8_t*,size_t,uint8_t,uint32_t,float,float);
|
||||
void onForeignRadioSendComplete(bool);
|
||||
bool handleMeshtextCommand(const char*,char*,size_t);
|
||||
@@ -126,7 +150,25 @@ public:
|
||||
++foreign_admissions; foreign.emplace_back(bytes,bytes+size); foreign_profile=profile;
|
||||
foreign_generation=generation; foreign_pending=true; return true;
|
||||
}
|
||||
void completeForeign(bool success=true) {
|
||||
assert(foreign_pending);
|
||||
// The real Dispatcher releases queue ownership before the application
|
||||
// completion callback. Successful RF TX is not a delivery acknowledgement.
|
||||
foreign_pending=false;
|
||||
foreign_sending=false;
|
||||
onForeignRadioSendComplete(success);
|
||||
}
|
||||
bool discardQueuedForeignRadioFrame(mesh::RadioProfileProtocol protocol) {
|
||||
assert(protocol==mesh::RadioProfileProtocol::Meshtext);
|
||||
if(!foreign_pending || foreign_sending) return false;
|
||||
completeForeign(false); return true;
|
||||
}
|
||||
void releaseForeign() {
|
||||
if(foreign_pending) completeForeign(false);
|
||||
loopMeshtextDelivery();
|
||||
}
|
||||
mesh::RTCClock* getRTCClock() { return &clock; }
|
||||
Random* getRNG() { return &random; }
|
||||
bool addToOfflineQueue(const uint8_t* bytes,int size) {
|
||||
assert(size>=0 && size<=MAX_FRAME_SIZE); if(offline.size()>=offline_capacity) return false;
|
||||
offline.emplace_back(bytes,bytes+size); return true;
|
||||
@@ -146,7 +188,10 @@ public:
|
||||
}
|
||||
return reply;
|
||||
}
|
||||
void advance(uint32_t ms) { g_mock_millis+=ms; clock.epoch+=ms/1000; _radio_profiles.loop(); refreshMeshtextProfile(); }
|
||||
void advance(uint32_t ms) {
|
||||
g_mock_millis+=ms; clock.epoch+=ms/1000;
|
||||
_radio_profiles.loop(); refreshMeshtextProfile(); loopMeshtextDelivery();
|
||||
}
|
||||
};
|
||||
@METHODS@
|
||||
void MyMesh::command(uint8_t* cmd_frame,int len) {
|
||||
@@ -183,7 +228,7 @@ static void keys() {
|
||||
auto c=channel("#mt-longfast",size); const auto before=c;
|
||||
assert(describe(c,wire) && wire.key_length==size && !memcmp(wire.key,c.channel.secret,32));
|
||||
assert(!memcmp(c.channel.secret,before.channel.secret,32));
|
||||
node.foreign_pending=false; assert(node.sendMeshtextText(c,"private",7));
|
||||
node.releaseForeign(); node.commit(0,c); assert(node.sendMeshtextText(c,"private",7));
|
||||
mesh::mt::DecodedText decoded;
|
||||
assert(mesh::mt::decodeText(node.foreign.back().data(),node.foreign.back().size(),wire.name,wire.key,size,1,decoded));
|
||||
assert(!strcmp(decoded.text,"private"));
|
||||
@@ -364,7 +409,7 @@ static void transmit() {
|
||||
assert(frame[12]==0xe7); // limit/start 7, no want-ACK or MQTT flag.
|
||||
assert(!mesh::mt::decodeText(frame.data(),frame.size(),"LongFast",mesh::mt::DefaultPsk,16,node._mt_node,decoded));
|
||||
node.command(request,12); assert(!node.ok && node.error==ERR_CODE_BAD_STATE && node.foreign.size()==1);
|
||||
node.foreign_pending=false; node.onForeignRadioSendComplete(true); node.onForeignRadioSendComplete(false);
|
||||
node.completeForeign(); node.onForeignRadioSendComplete(false);
|
||||
assert(node._mt_sent==1 && node._mt_failed==1);
|
||||
char oversized[229]; memset(oversized,'x',sizeof(oversized));
|
||||
assert(!node.sendMeshtextText(node.stored[0],oversized,sizeof(oversized)));
|
||||
@@ -538,13 +583,13 @@ static void hops() {
|
||||
assertUntouched(migrated); assertUntouched(migrated_reboot); puts("PASS hops");
|
||||
}
|
||||
static mesh::mt::DecodedText decodeOwnText(MyMesh& node,const char* text,size_t length) {
|
||||
node.foreign_pending=false;
|
||||
node.releaseForeign();
|
||||
const uint32_t expected_packet=node._mt_packet_id;
|
||||
assert(node.sendMeshtextText(node.stored[0],text,length));
|
||||
const auto& raw=node.foreign.back(); mesh::mt::DecodedText decoded;
|
||||
assert(mesh::mt::decodeText(raw.data(),raw.size(),"LongFast",mesh::mt::DefaultPsk,16,1,decoded));
|
||||
assert(decoded.from==node._mt_node && decoded.id==expected_packet && node._mt_packet_id==expected_packet+1);
|
||||
node.foreign_pending=false; return decoded;
|
||||
node.releaseForeign(); return decoded;
|
||||
}
|
||||
static void shortname() {
|
||||
Preferences::reset(); MyMesh node; active(node); char reply[256];
|
||||
@@ -742,7 +787,7 @@ static void nodeinfo() {
|
||||
assertUntouched(node);
|
||||
assert(node.handleMeshtextCommand("get mt.shortname",reply,sizeof(reply)));
|
||||
char fallback[5]; snprintf(fallback,sizeof(fallback),"%04lx",(unsigned long)(node._mt_node&0xffff));
|
||||
assert(strstr(reply,fallback) && strstr(reply,"manual NodeInfo only"));
|
||||
assert(strstr(reply,fallback) && strstr(reply,"first-chat/manual NodeInfo"));
|
||||
const uint8_t expected_default[]=@NODEINFO_DEFAULT@,expected_named[]=@NODEINFO_NAMED@,
|
||||
expected_utf8[]=@NODEINFO_UTF8@,expected_private[]=@NODEINFO_PRIVATE@;
|
||||
const auto original_settings=Preferences::blob; const auto original_writes=Preferences::writes;
|
||||
@@ -755,13 +800,13 @@ static void nodeinfo() {
|
||||
assert(node._mt_packet_id==pending_id && node.foreign.size()==pending_count);
|
||||
assert(!node.sendMeshtextText(node.stored[0],"busy",4));
|
||||
assert(!node.saveMeshtextSettings(mesh::mt::Region::US,0));
|
||||
node.foreign_pending=false;
|
||||
node.releaseForeign();
|
||||
assert(node.handleMeshtextCommand("set mt.node !12345678",reply,sizeof(reply)) && !strncmp(reply,"OK",2));
|
||||
assert(node.handleMeshtextCommand("set mt.shortname MIKE",reply,sizeof(reply)) && !strncmp(reply,"OK",2));
|
||||
strcpy(node._prefs.node_name,"Mike Carper");
|
||||
assert(node.handleMeshtextCommand("send mt.nodeinfo 0",reply,sizeof(reply)) && strstr(reply,"queued"));
|
||||
assert(node.foreign.back()==std::vector<uint8_t>(expected_named,expected_named+sizeof(expected_named)));
|
||||
node.foreign_pending=false;
|
||||
node.releaseForeign();
|
||||
auto text=decodeOwnText(node,"hello",5);
|
||||
assert(text.id==0x1234567a && !strcmp(text.text,"hello"));
|
||||
const char* emojis="\xf0\x9f\x98\x80\xf0\x9f\x98\x80\xf0\x9f\x98\x80\xf0\x9f\x98\x80";
|
||||
@@ -771,10 +816,10 @@ static void nodeinfo() {
|
||||
assert(node.handleMeshtextCommand("send mt.nodeinfo 0",reply,sizeof(reply)) && strstr(reply,"queued"));
|
||||
assert(node.foreign.back()==std::vector<uint8_t>(expected_utf8,expected_utf8+sizeof(expected_utf8)));
|
||||
assert(strlen(node._prefs.node_name)==29 && strlen(node._mt_short_name)==16); // no mutation
|
||||
node.foreign_pending=false; node.commit(1,channel("mt-Team",32));
|
||||
node.releaseForeign(); node.commit(1,channel("mt-Team",32));
|
||||
assert(node.handleMeshtextCommand("send mt.nodeinfo 1",reply,sizeof(reply)) && strstr(reply,"queued"));
|
||||
assert(node.foreign.back()==std::vector<uint8_t>(expected_private,expected_private+sizeof(expected_private)));
|
||||
node.foreign_pending=false;
|
||||
node.releaseForeign();
|
||||
const auto count=node.foreign.size(); const uint32_t packet=node._mt_packet_id;
|
||||
node.commit(2,channel("ordinary"));
|
||||
for(const char* bad:{"send mt.nodeinfo","send mt.nodeinfo ","send mt.nodeinfo -1",
|
||||
@@ -805,13 +850,287 @@ static void nodeinfo() {
|
||||
node.cli("set radio2 auto"); node.advance(2000);
|
||||
node._mt_packet_id=UINT32_MAX;
|
||||
assert(node.handleMeshtextCommand("send mt.nodeinfo 0",reply,sizeof(reply)) && strstr(reply,"queued"));
|
||||
node.foreign_pending=false;
|
||||
node.releaseForeign();
|
||||
assert(node._mt_packet_id==0 && !node.sendMeshtextText(node.stored[0],"done",4));
|
||||
assert(node.handleMeshtextCommand("send mt.nodeinfo 0",reply,sizeof(reply)) && !strncmp(reply,"Error",5));
|
||||
MyMesh reboot; reboot.stored[0]=node.stored[0]; reboot.beginMeshtext();
|
||||
assertUntouched(reboot); assert(!node.meshcore_sends);
|
||||
puts("PASS nodeinfo");
|
||||
}
|
||||
static mesh::mt::PendingFrame* pendingChat(MyMesh& node) {
|
||||
for(unsigned i=0;i<mesh::mt::ChatDeliverySlots;++i)
|
||||
if(node._mt_delivery[i].length) return &node._mt_delivery[i];
|
||||
return nullptr;
|
||||
}
|
||||
static void advanceTo(MyMesh& node,uint32_t due) {
|
||||
const int32_t delta=static_cast<int32_t>(due-millis());
|
||||
node.advance(delta>0?static_cast<uint32_t>(delta):0);
|
||||
}
|
||||
static std::vector<uint8_t> repeated(const std::vector<uint8_t>& original) {
|
||||
auto echo=original;
|
||||
assert((echo[12]&7)>0);
|
||||
echo[12]=static_cast<uint8_t>((echo[12]&0xf8)|((echo[12]&7)-1));
|
||||
echo[14]=0x42; echo[15]=0x43; // Forwarder-controlled header fields.
|
||||
return echo;
|
||||
}
|
||||
static void announce(MyMesh& node) {
|
||||
node._prefs.flood_retry_attempts=0;
|
||||
assert(node.sendMeshtextNodeInfo(node.stored[0]));
|
||||
node.completeForeign(); node.advance(0);
|
||||
assert(!node.foreign_pending);
|
||||
node._prefs.flood_retry_attempts=15;
|
||||
}
|
||||
static void automaticNodeinfo() {
|
||||
const uint8_t expected[]=@NODEINFO_AFTER_CHAT@;
|
||||
Preferences::reset(); MyMesh node; active(node);
|
||||
node._prefs.flood_retry_attempts=0;
|
||||
node.advance(3600000); assertUntouched(node);
|
||||
assert(node.sendMeshtextText(node.stored[0],"hello",5));
|
||||
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);
|
||||
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.
|
||||
node.completeForeign(); node.advance(3600000); assert(node.foreign.size()==2);
|
||||
assert(node.sendMeshtextText(node.stored[0],"again",5));
|
||||
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);
|
||||
auto private_info=node.foreign.back();
|
||||
mesh::mt::DecodedText private_decoded;
|
||||
assert(private_info!=text && !mesh::mt::decodeText(private_info.data(),private_info.size(),
|
||||
"Team",node.stored[1].channel.secret,32,1,private_decoded));
|
||||
node.completeForeign(); node.advance(3600000); assert(node.foreign.size()==5);
|
||||
assert(node.offline.empty() && !node.meshcore_sends && !node._notifications.calls);
|
||||
|
||||
Preferences::reset(); MyMesh failure; active(failure); failure._prefs.flood_retry_attempts=0;
|
||||
failure.admit_foreign=false; assert(!failure.sendMeshtextText(failure.stored[0],"rejected",8));
|
||||
failure.admit_foreign=true; failure.advance(1); assertUntouched(failure);
|
||||
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();
|
||||
|
||||
Preferences::reset(); MyMesh manual; active(manual); announce(manual);
|
||||
manual._prefs.flood_retry_attempts=0;
|
||||
assert(manual.sendMeshtextText(manual.stored[0],"already named",13));
|
||||
manual.completeForeign(); manual.advance(100000); assert(manual.foreign.size()==2);
|
||||
puts("PASS auto-nodeinfo");
|
||||
}
|
||||
static void heardRetries() {
|
||||
Preferences::reset(); MyMesh node; active(node); announce(node);
|
||||
node._prefs.flood_retry_attempts=2;
|
||||
assert(node.sendMeshtextText(node.stored[0],"retry this",10));
|
||||
const auto original=node.foreign.back(); const auto packet=node._mt_packet_id;
|
||||
const auto start=node.foreign.size();
|
||||
node.completeForeign(); auto* pending=pendingChat(node); assert(pending);
|
||||
uint32_t due=pending->due;
|
||||
node.advance(static_cast<uint32_t>(due-millis()-1)); assert(node.foreign.size()==start);
|
||||
node.advance(1); assert(node.foreign.size()==start+1 && node.foreign.back()==original);
|
||||
assert(node._mt_packet_id==packet && node._mt_retried==0);
|
||||
node.completeForeign(); assert(node._mt_retried==1); pending=pendingChat(node); assert(pending);
|
||||
advanceTo(node,pending->due);
|
||||
assert(node.foreign.size()==start+2 && node.foreign.back()==original && node._mt_retried==1);
|
||||
node.completeForeign(); assert(node._mt_retried==2); pending=pendingChat(node); assert(pending); // Final echo window remains.
|
||||
auto echo=repeated(original);
|
||||
assert(receiveFrame(node,echo.data(),echo.size()));
|
||||
assert(node._mt_heard==1 && !node._mt_exhausted && node.offline.empty());
|
||||
node.advance(3600000); assert(node.foreign.size()==start+2 && !pendingChat(node));
|
||||
|
||||
Preferences::reset(); MyMesh capped; active(capped); announce(capped);
|
||||
capped._prefs.flood_retry_attempts=15;
|
||||
assert(capped.sendMeshtextText(capped.stored[0],"bounded",7));
|
||||
const auto cap_original=capped.foreign.back(); capped.completeForeign();
|
||||
for(unsigned i=0;i<15;++i) {
|
||||
auto* current=pendingChat(capped); assert(current);
|
||||
advanceTo(capped,current->due);
|
||||
assert(capped.foreign_pending && capped.foreign.back()==cap_original);
|
||||
capped.completeForeign();
|
||||
}
|
||||
assert(capped._mt_retried==15 && capped.foreign.size()==17);
|
||||
auto* current=pendingChat(capped); assert(current); advanceTo(capped,current->due);
|
||||
assert(capped._mt_exhausted==1 && !pendingChat(capped));
|
||||
capped.advance(3600000); assert(capped.foreign.size()==17);
|
||||
|
||||
Preferences::reset(); MyMesh disabled; active(disabled); announce(disabled);
|
||||
disabled._prefs.flood_retry_attempts=0;
|
||||
assert(disabled.sendMeshtextText(disabled.stored[0],"once",4));
|
||||
disabled.completeForeign(); disabled.advance(3600000);
|
||||
assert(disabled.foreign.size()==2 && !disabled._mt_retried && !pendingChat(disabled));
|
||||
disabled._prefs.flood_retry_attempts=15; disabled._mt_hop_limit=0;
|
||||
assert(disabled.sendMeshtextText(disabled.stored[0],"no hops",7));
|
||||
disabled.completeForeign(); disabled.advance(3600000);
|
||||
assert(disabled.foreign.size()==3 && !disabled._mt_retried && !pendingChat(disabled));
|
||||
puts("PASS heard-retries");
|
||||
}
|
||||
static void strictEcho() {
|
||||
Preferences::reset(); MyMesh node; active(node); announce(node);
|
||||
assert(node.sendMeshtextText(node.stored[0],"strict echo",11));
|
||||
const auto original=node.foreign.back(); auto echo=repeated(original);
|
||||
node.completeForeign(); const auto count=node.foreign.size();
|
||||
receiveFrame(node,original.data(),original.size()); assert(!node._mt_heard);
|
||||
for(unsigned position:{0U,4U,8U,12U,13U,16U,unsigned(echo.size()-1)}) {
|
||||
auto wrong=echo; wrong[position]^=position==12?0x20:1;
|
||||
receiveFrame(node,wrong.data(),wrong.size()); assert(!node._mt_heard);
|
||||
}
|
||||
receiveFrame(node,echo.data(),echo.size()-1); assert(!node._mt_heard);
|
||||
node.onForeignRadioFrame(echo.data(),echo.size(),0,node.radio.p.generation[1],1,-80);
|
||||
node.onForeignRadioFrame(echo.data(),echo.size(),1,node.radio.p.generation[1]-1,1,-80);
|
||||
assert(!node._mt_heard && pendingChat(node));
|
||||
assert(receiveFrame(node,echo.data(),echo.size()) && node._mt_heard==1);
|
||||
receiveFrame(node,echo.data(),echo.size()); assert(node._mt_heard==1);
|
||||
node.advance(100000); assert(node.foreign.size()==count && node.offline.empty());
|
||||
puts("PASS strict-echo");
|
||||
}
|
||||
static void deliveryLifecycle() {
|
||||
for(unsigned change=0;change<7;++change) {
|
||||
Preferences::reset(); MyMesh node; active(node); announce(node);
|
||||
assert(node.sendMeshtextText(node.stored[0],"stale",5));
|
||||
node.completeForeign(); assert(pendingChat(node));
|
||||
const auto count=node.foreign.size(); char reply[256];
|
||||
switch(change) {
|
||||
case 0: ++node.radio.p.generation[1]; break;
|
||||
case 1: node.cli("set radio2 off"); break;
|
||||
case 2: node.cli("set radio2 906.875,250,11,5,rx,16"); break;
|
||||
case 3: node.commit(0,channel("#mt-longfast",32)); break;
|
||||
case 4:
|
||||
assert(node.handleMeshtextCommand("set mt.node !12345678",reply,sizeof(reply))
|
||||
&& !strncmp(reply,"OK",2)); break;
|
||||
case 5: node.commit(0,{}); break;
|
||||
case 6: node._radio_available=false; break;
|
||||
}
|
||||
node.advance(2000); node.advance(100000);
|
||||
assert(!pendingChat(node) && !node.foreign_pending && node.foreign.size()==count);
|
||||
assert(!node._mt_retried && !node._mt_exhausted);
|
||||
}
|
||||
// Configuration changes before RF completion cannot arm metadata intent.
|
||||
Preferences::reset(); MyMesh inflight; active(inflight);
|
||||
assert(inflight.sendMeshtextText(inflight.stored[0],"not sent",8));
|
||||
inflight.commit(0,channel("#mt-longfast",32)); inflight.advance(1);
|
||||
assert(!inflight.foreign_pending && inflight.foreign.size()==1);
|
||||
inflight.advance(100000); assert(inflight.foreign.size()==1);
|
||||
// Config edits and RX by themselves remain completely silent.
|
||||
inflight.self_id.pub_key[0]=43; inflight.refreshMeshtextIdentity(); inflight.advance(1);
|
||||
assert(inflight.foreign.size()==1);
|
||||
puts("PASS delivery-lifecycle");
|
||||
}
|
||||
static void deliveryContention() {
|
||||
Preferences::reset(); MyMesh node; active(node); announce(node);
|
||||
assert(node.sendMeshtextText(node.stored[0],"older",5));
|
||||
const auto original=node.foreign.back(); node.completeForeign();
|
||||
auto* pending=pendingChat(node); assert(pending); const auto due=pending->due;
|
||||
const auto count=node.foreign.size();
|
||||
node.admit_foreign=false; advanceTo(node,due);
|
||||
assert(node.foreign.size()==count && !node._mt_retried && pending->retries==0);
|
||||
node.admit_foreign=true;
|
||||
assert(node.sendMeshtextText(node.stored[0],"newer",5)); // Don't block users for an echo window.
|
||||
node.completeForeign(); node.advance(0);
|
||||
assert(node.foreign_pending && node.foreign.back()==original && !node._mt_retried);
|
||||
const auto failed=node._mt_failed; auto echo=repeated(original);
|
||||
assert(receiveFrame(node,echo.data(),echo.size())); // Arrives while a retry is queued.
|
||||
assert(!node.foreign_pending && node._mt_heard==1 && node._mt_failed==failed);
|
||||
pending=pendingChat(node); assert(pending); advanceTo(node,pending->due);
|
||||
assert(node.foreign_pending); // Newer chat's own timer still works.
|
||||
const auto newer=node.foreign.back(); node.foreign_sending=true;
|
||||
auto newer_echo=repeated(newer);
|
||||
assert(receiveFrame(node,newer_echo.data(),newer_echo.size()));
|
||||
assert(node.foreign_pending && node._mt_heard==2); // In-flight RF cannot be discarded.
|
||||
node.completeForeign(); node.advance(100000);
|
||||
assert(!node.foreign_pending && !pendingChat(node) && node._mt_failed==failed && node._mt_retried==1);
|
||||
char reply[256]; assert(node.handleMeshtextCommand("get mt.retry",reply,sizeof(reply)));
|
||||
assert(strstr(reply,"heard=2") && strstr(reply,"pending=0"));
|
||||
|
||||
Preferences::reset(); MyMesh full; active(full); announce(full);
|
||||
for(unsigned i=0;i<mesh::mt::ChatDeliverySlots;++i) {
|
||||
assert(full.sendMeshtextText(full.stored[0],"pending",7)); full.completeForeign();
|
||||
}
|
||||
const auto full_count=full.foreign.size();
|
||||
assert(!full.sendMeshtextText(full.stored[0],"backpressure",12));
|
||||
assert(full.foreign.size()==full_count && !full.foreign_pending);
|
||||
puts("PASS delivery-contention");
|
||||
}
|
||||
static void deliveryTiming() {
|
||||
Preferences::reset(); MyMesh node; active(node); announce(node);
|
||||
g_mock_millis=UINT32_MAX-100;
|
||||
node._prefs.flood_retry_max_path=0; node._prefs.flood_retry_group_max_path=0;
|
||||
assert(node.sendMeshtextText(node.stored[0],"wrap",4));
|
||||
const auto original=node.foreign.back(); node.completeForeign();
|
||||
auto* pending=pendingChat(node); assert(pending && pending->due==109);
|
||||
const auto count=node.foreign.size(); node.advance(209);
|
||||
assert(node.foreign.size()==count); node.advance(1);
|
||||
assert(node.foreign.size()==count+1 && node.foreign.back()==original);
|
||||
node.completeForeign(); auto echo=repeated(original);
|
||||
assert(receiveFrame(node,echo.data(),echo.size()) && node._mt_heard==1);
|
||||
node.advance(100000); assert(!pendingChat(node));
|
||||
|
||||
Preferences::reset(); MyMesh metadata; active(metadata); metadata.random.jitter=200;
|
||||
assert(metadata.sendMeshtextNodeInfo(metadata.stored[0]));
|
||||
const auto info=metadata.foreign.back(); const auto packet=metadata._mt_packet_id;
|
||||
metadata.completeForeign(); auto& advert=metadata._mt_delivery[mesh::mt::ChatDeliverySlots];
|
||||
assert(advert.length && advert.due-millis()==60230);
|
||||
metadata.advance(60229); assert(metadata.foreign.size()==1);
|
||||
metadata.advance(1); assert(metadata.foreign.size()==2 && metadata.foreign.back()==info);
|
||||
assert(metadata._mt_packet_id==packet && metadata._mt_retried==0);
|
||||
metadata.completeForeign(); assert(metadata._mt_retried==1); advanceTo(metadata,advert.due);
|
||||
assert(!advert.length && metadata._mt_exhausted==1);
|
||||
metadata.advance(3600000); assert(metadata.foreign.size()==2); // Metadata cap is one, not 15.
|
||||
|
||||
Preferences::reset(); MyMesh no_advert; active(no_advert);
|
||||
no_advert._prefs.flood_retry_advert_enabled=0;
|
||||
assert(no_advert.sendMeshtextNodeInfo(no_advert.stored[0]));
|
||||
no_advert.completeForeign(); no_advert.advance(3600000);
|
||||
assert(no_advert.foreign.size()==1 && !no_advert._mt_retried);
|
||||
puts("PASS delivery-timing");
|
||||
}
|
||||
static void announcementLifecycle() {
|
||||
Preferences::reset(); MyMesh node; active(node); announce(node);
|
||||
node._prefs.flood_retry_attempts=0;
|
||||
char reply[256];
|
||||
for(const char* command:{"set mt.hops 7","set mt.hops 3"}) {
|
||||
assert(node.handleMeshtextCommand(command,reply,sizeof(reply)) && !strncmp(reply,"OK",2));
|
||||
node.advance(1); const auto count=node.foreign.size();
|
||||
assert(node.sendMeshtextText(node.stored[0],"no reannounce",13));
|
||||
node.completeForeign(); node.advance(1); assert(node.foreign.size()==count+1);
|
||||
}
|
||||
node.commit(1,channel("#mt-LoNgFaSt"));
|
||||
const auto before_alias=node.foreign.size();
|
||||
assert(node.sendMeshtextText(node.stored[1],"same wire",9));
|
||||
node.completeForeign(); node.advance(1); assert(node.foreign.size()==before_alias+1);
|
||||
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();
|
||||
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();
|
||||
|
||||
for(bool fail_admission:{false,true}) {
|
||||
Preferences::reset(); MyMesh failure; active(failure);
|
||||
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);
|
||||
if(!fail_admission) {
|
||||
assert(failure.foreign_pending); failure.completeForeign(false);
|
||||
}
|
||||
const auto attempts=failure.foreign_admissions,packet=failure._mt_packet_id;
|
||||
for(unsigned i=0;i<100;++i) failure.advance(1);
|
||||
assert(!failure.foreign_pending && failure.foreign_admissions==attempts && failure._mt_packet_id==packet);
|
||||
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.completeForeign();
|
||||
}
|
||||
puts("PASS announcement-lifecycle");
|
||||
}
|
||||
int main(int argc,char** argv) {
|
||||
assert(argc==2);
|
||||
if(!strcmp(argv[1],"keys"))keys();
|
||||
@@ -827,5 +1146,12 @@ int main(int argc,char** argv) {
|
||||
else if(!strcmp(argv[1],"shortname"))shortname();
|
||||
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],"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 assert(!"unknown test case");
|
||||
}
|
||||
@@ -102,6 +102,8 @@ class CompanionMeshtextTest(unittest.TestCase):
|
||||
vectors.update({
|
||||
"@NODEINFO_DEFAULT@": nodeinfo_frame(default_node, 0x12345678, b"Test",
|
||||
("%04x" % (default_node & 0xFFFF)).encode()),
|
||||
"@NODEINFO_AFTER_CHAT@": nodeinfo_frame(default_node, 0x12345679, b"Test",
|
||||
("%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,
|
||||
@@ -200,16 +202,41 @@ class CompanionMeshtextTest(unittest.TestCase):
|
||||
self.assertEqual(imported.count("refreshMeshtextIdentity();"), 1,
|
||||
"the refresh belongs only to the successfully saved identity branch")
|
||||
|
||||
def test_manual_nodeinfo_standard_fields_shared_sequence_guards_and_no_automatic_adverts(self):
|
||||
def test_manual_nodeinfo_standard_fields_shared_sequence_guards_and_no_periodic_adverts(self):
|
||||
self.run_case("nodeinfo")
|
||||
source = (ROOT / "examples/companion_radio/MeshtextCompanion.cpp").read_text(encoding="utf-8")
|
||||
self.assertEqual(source.count("sendMeshtextNodeInfo("), 2,
|
||||
"NodeInfo has exactly one caller: the explicit CLI")
|
||||
chat = body(source, "bool MyMesh::sendMeshtextText(")
|
||||
self.assertNotIn("_mt_short_name", chat)
|
||||
self.assertNotIn("node_name", chat)
|
||||
self.assertNotIn("encodeNodeInfo", chat)
|
||||
|
||||
def test_first_successful_chat_announces_once_per_channel_with_no_position(self):
|
||||
self.run_case("auto-nodeinfo")
|
||||
|
||||
def test_heard_repeat_retries_identical_packets_bounded_by_existing_settings(self):
|
||||
self.run_case("heard-retries")
|
||||
|
||||
def test_only_exact_forwarded_echo_on_the_current_profile_stops_retries(self):
|
||||
self.run_case("strict-echo")
|
||||
|
||||
def test_profile_channel_key_identity_and_radio_changes_cancel_stale_delivery(self):
|
||||
self.run_case("delivery-lifecycle")
|
||||
|
||||
def test_busy_queue_new_chat_backpressure_and_queued_vs_inflight_echo(self):
|
||||
self.run_case("delivery-contention")
|
||||
|
||||
def test_rollover_jitter_origin_path_gates_and_advert_retry_cap(self):
|
||||
self.run_case("delivery-timing")
|
||||
|
||||
def test_announcement_alias_settings_key_changes_and_failure_do_not_storm(self):
|
||||
self.run_case("announcement-lifecycle")
|
||||
|
||||
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()")
|
||||
self.assertIn("if (!mesh::morseTimingActive(_radio)) loopMeshtextDelivery();", loop)
|
||||
self.assertLess(loop.index("loopMeshtextDelivery();"), loop.index("BaseChatMesh::loop();"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user