Limit Meshtext messages to 15 total attempts
Stop on a matching forwarded echo and preserve lower saved retry settings. Keep scheduled beacons on their separate single-send path.
This commit is contained in:
1 parent
51adb8fe1e
commit
99eb24736f
8 files changed
+80
-16
No files matched your search
@@ -23,9 +23,10 @@ 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. 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.
|
||||
MT messages default to 15 total attempts (the first send plus up to 14 retries)
|
||||
unless a matching repeat is heard. The Companion's `flood.retry.count` can lower
|
||||
that budget; `0` or `mt.hops 0` sends once. NodeInfo keeps its metadata cap and
|
||||
`flood.retry.advert` gate. Scheduled beacons remain fire and forget.
|
||||
|
||||
See [carrier-wave RF checks](carrier_wave.md) for `cw on [seconds]`,
|
||||
`cw2 on [seconds]`, and stopping a carrier with `off`.
|
||||
@@ -5156,7 +5157,8 @@ 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.
|
||||
`flood.retry.count` also limits retries of the node's own MT channel text,
|
||||
with a cap of 15 total attempts (the original plus 14 retries).
|
||||
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,
|
||||
@@ -5166,6 +5168,7 @@ 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.
|
||||
Scheduled beacons always send once per occurrence.
|
||||
|
||||
**Search terms:** flood tx retries, flood retry attempts, flood retransmissions, broadcast retries.
|
||||
|
||||
|
||||
@@ -813,9 +813,11 @@ 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
|
||||
MT channel messages default to **15 total attempts** without a heard repeat:
|
||||
the initial send and up to 14 retries. They use the Companion's saved
|
||||
`flood.retry.count`, capped at 14 extra transmissions for MT text. Lower saved
|
||||
counts retain their extra-retry meaning; `0` disables retries. MeshCore's retry
|
||||
count meaning is unchanged. 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
|
||||
@@ -826,9 +828,8 @@ 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.
|
||||
matching heard repeats, exhausted retry sequences, pending work and the shared
|
||||
configured retry `count`. Use `set flood.retry.count` 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
|
||||
|
||||
@@ -64,6 +64,8 @@ or Meshtext NodeInfo. Meshtext beacons request no ACK and have no forwarding
|
||||
budget. MeshCore Public uses one locally transmitted flood; other MeshCore
|
||||
nodes may relay that flood according to their own policies.
|
||||
|
||||
Beacons are **fire and forget**, independent of `flood.retry.count` and MT
|
||||
message retries. They do not wait for an echo or resend when none is heard.
|
||||
The transmitter briefly borrows the physical radio, sends once, and restores
|
||||
its normal receive settings, power, and RX power saving. It does not occupy,
|
||||
replace, or persist changes to `radio2`, including a temporary second profile.
|
||||
|
||||
@@ -460,7 +460,8 @@ uint8_t MyMesh::meshtextRetryLimit(const mesh::mt::PendingFrame& frame) const {
|
||||
// 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;
|
||||
return frame.nodeinfo ? (_prefs.flood_retry_advert_enabled && count ? 1 : 0)
|
||||
: mesh::mt::messageRetryLimit(count);
|
||||
}
|
||||
|
||||
uint32_t MyMesh::meshtextRetryDelay(const mesh::mt::PendingFrame& frame) {
|
||||
|
||||
@@ -43,4 +43,11 @@ struct PendingFrame {
|
||||
static constexpr unsigned DeliverySlots = 4;
|
||||
static constexpr unsigned ChatDeliverySlots = DeliverySlots - 1;
|
||||
|
||||
// Text messages get at most 15 transmissions, including the original send.
|
||||
// Lower saved flood retry counts still mean additional transmissions.
|
||||
static constexpr uint8_t MaxMessageAttempts = 15;
|
||||
constexpr uint8_t messageRetryLimit(uint8_t count) {
|
||||
return count < MaxMessageAttempts ? count : MaxMessageAttempts - 1;
|
||||
}
|
||||
|
||||
} } // namespace mesh::mt
|
||||
Vendored
+39
-4
@@ -99,7 +99,7 @@ public:
|
||||
mesh::RadioProfileCLI _radio_profiles;
|
||||
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_attempts=@DEFAULT_FLOOD_RETRIES@,flood_retry_max_path=1;
|
||||
uint8_t flood_retry_group_max_path=0xff,flood_retry_advert_enabled=1;
|
||||
} _prefs;
|
||||
mesh::LocalIdentity self_id;
|
||||
@@ -1016,6 +1016,40 @@ static void automaticNodeinfoQuiet() {
|
||||
failure.completeForeign(); failure.advance(250); assert(failure.foreign.size()==3);
|
||||
puts("PASS auto-nodeinfo-quiet");
|
||||
}
|
||||
static void defaultTries() {
|
||||
for (bool echo_on_last : {false, true}) {
|
||||
Preferences::reset(); MyMesh node; active(node); announce(node);
|
||||
assert(node._prefs.flood_retry_attempts==15);
|
||||
char reply[256];
|
||||
assert(node.handleMeshtextCommand("get mt.retry",reply,sizeof(reply)));
|
||||
assert(strstr(reply,"count=15"));
|
||||
const auto before=node.foreign.size();
|
||||
assert(node.sendMeshtextText(node.stored[0],"fifteen tries",13));
|
||||
const auto original=node.foreign.back();
|
||||
const auto next_id=node._mt_packet_id;
|
||||
node.completeForeign();
|
||||
for (unsigned attempt=1;attempt<15;++attempt) {
|
||||
auto* frame=pendingChat(node); assert(frame);
|
||||
advanceTo(node,frame->due);
|
||||
assert(node.foreign_pending && node.foreign.back()==original);
|
||||
assert(node._mt_packet_id==next_id);
|
||||
node.completeForeign();
|
||||
}
|
||||
assert(node.foreign.size()==before+15 && node._mt_retried==14);
|
||||
auto* frame=pendingChat(node); assert(frame); // Final echo window remains.
|
||||
if (echo_on_last) {
|
||||
const auto echo=repeated(original);
|
||||
assert(receiveFrame(node,echo.data(),echo.size()));
|
||||
assert(node._mt_heard==1 && node._mt_exhausted==0);
|
||||
} else {
|
||||
advanceTo(node,frame->due);
|
||||
assert(node._mt_exhausted==1 && node._mt_heard==0);
|
||||
}
|
||||
node.advance(3600000);
|
||||
assert(!pendingChat(node) && !node.foreign_pending && node.foreign.size()==before+15);
|
||||
}
|
||||
puts("PASS default-tries");
|
||||
}
|
||||
static void heardRetries() {
|
||||
Preferences::reset(); MyMesh node; active(node); announce(node);
|
||||
node._prefs.flood_retry_attempts=2;
|
||||
@@ -1040,16 +1074,16 @@ static void heardRetries() {
|
||||
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) {
|
||||
for(unsigned i=0;i<14;++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);
|
||||
assert(capped._mt_retried==14 && capped.foreign.size()==16);
|
||||
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);
|
||||
capped.advance(3600000); assert(capped.foreign.size()==16);
|
||||
|
||||
Preferences::reset(); MyMesh disabled; active(disabled); announce(disabled);
|
||||
disabled._prefs.flood_retry_attempts=0;
|
||||
@@ -1620,6 +1654,7 @@ int main(int argc,char** argv) {
|
||||
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],"default-tries"))defaultTries();
|
||||
else if(!strcmp(argv[1],"strict-echo"))strictEcho();
|
||||
else if(!strcmp(argv[1],"delivery-lifecycle"))deliveryLifecycle();
|
||||
else if(!strcmp(argv[1],"delivery-contention"))deliveryContention();
|
||||
|
||||
@@ -212,6 +212,13 @@ static void basic() {
|
||||
assert(f.radio.p.generation[0] == saved.generation[0] && f.radio.p.generation[1] == saved.generation[1]);
|
||||
assert(!f.d.getNumSentFlood() && !f.d.getNumSentDirect() && !f.d.mc_sent);
|
||||
assert(f.d.getTotalAirTime() == 21);
|
||||
// A silent channel never turns this beacon into a chat retry. Its raw
|
||||
// owner is retired after TX, even while message retries are enabled.
|
||||
for (unsigned minute = 0; minute < 15; ++minute) {
|
||||
f.tick(60000);
|
||||
assert(!f.d.hasOutbound() && !f.d.hasBeaconRadioFrame());
|
||||
assert(f.radio.beacon_sends == 1 && f.radio.normal_sends == 0);
|
||||
}
|
||||
}
|
||||
static void preparation() {
|
||||
{ Fixture f; f.radio.prepare = RadioParamApplyResult::BUSY;
|
||||
@@ -629,7 +636,7 @@ class BeaconDispatcherTest(unittest.TestCase):
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertIn("PASS", result.stdout)
|
||||
|
||||
def test_one_tx_uses_private_tuple_preserves_radio2_and_accounts_airtime(self):
|
||||
def test_one_tx_without_echo_preserves_radio2_and_accounts_airtime(self):
|
||||
self.run_case("basic")
|
||||
|
||||
def test_prepare_busy_is_bounded_and_owned_failures_restore_exclusively(self):
|
||||
|
||||
@@ -82,6 +82,11 @@ class CompanionMeshtextTest(unittest.TestCase):
|
||||
fixture = (ROOT / "test/fixtures/radio_profiles/cli_test.cpp").read_text(encoding="utf-8")
|
||||
fixture = fixture[:fixture.index("static void permanentScheduleOrder()")]
|
||||
text = (ROOT / "test/fixtures/companion_meshtext.cpp").read_text(encoding="utf-8")
|
||||
prefs = (ROOT / "examples/companion_radio/NodePrefs.h").read_text(encoding="utf-8")
|
||||
default_retries = re.search(r"uint8_t\s+flood_retry_attempts\s*=\s*(\d+)\s*;", prefs)
|
||||
if default_retries is None:
|
||||
raise AssertionError("Companion retry default missing from production preferences")
|
||||
text = text.replace("@DEFAULT_FLOOD_RETRIES@", default_retries.group(1))
|
||||
radio_source = (ROOT / "src/helpers/radiolib/RadioLibWrappers.cpp").read_text(encoding="utf-8")
|
||||
airtime = body(radio_source, "uint32_t RadioLibWrapper::getProfileAirtime(")
|
||||
airtime = "uint32_t actualProfileAirtime(uint8_t profile, int len, uint8_t cr=0) " + airtime[airtime.index("{"):]
|
||||
@@ -250,6 +255,9 @@ class CompanionMeshtextTest(unittest.TestCase):
|
||||
def test_heard_repeat_retries_identical_packets_bounded_by_existing_settings(self):
|
||||
self.run_case("heard-retries")
|
||||
|
||||
def test_default_fifteen_total_attempts_and_last_attempt_echo(self):
|
||||
self.run_case("default-tries")
|
||||
|
||||
def test_only_exact_forwarded_echo_on_the_current_profile_stops_retries(self):
|
||||
self.run_case("strict-echo")
|
||||
|
||||
|
||||
Reference in new issue
Block a user