Honor retry settings across firmware roles

This commit is contained in:
mikecarper
2026-07-13 15:22:36 -07:00
parent 4f839d3477
commit b8f438d6ea
11 changed files with 356 additions and 27 deletions
+21 -6
View File
@@ -1199,6 +1199,10 @@ region save
Direct retry resends direct-routed packets when the downstream echo is not heard. It applies to direct messages, ACK packets, multipart packets carrying ACK payloads, and TRACE packets.
The shared state, count, base, and step controls work on repeater, room-server,
and sensor firmware. Recent-repeater/SNR controls are repeater-only because the
other roles do not keep the repeater reachability table they require.
#### View or change direct retry state
**Usage:**
- `get direct.retry`
@@ -1231,7 +1235,7 @@ set direct.retry off
**Default:** `on`
**Note:** When enabled, the recent repeater table is the direct retry eligibility
**Note:** This command is repeater-only. When enabled, the recent repeater table is the direct retry eligibility
gate. Prefixes missing from the table are assumed reachable; prefixes in the
table below the active SNR gate are blocked.
@@ -1274,6 +1278,10 @@ set retry.preset mobile
Flood retry resends flood-routed packets when the same packet is not heard from
another qualifying repeater.
The count, path, and advert controls work on repeater, room-server, and sensor
firmware. Flood forwarding must also be enabled for retries to run. Prefix,
ignore, bridge, and bucket controls are repeater-only.
#### View or change flood retry count
**Usage:**
- `get flood.retry.count`
@@ -1282,7 +1290,7 @@ another qualifying repeater.
**Parameters:**
- `count`: Base retry attempts after the original send, from `0` to `15`. `0` disables flood retry.
**Note:** Actual attempts are capped at `15`. Path count 0 flood retries use `count * 2`; path count 1 retries use `count * 1.5`, rounded up; path count 2 and higher use the configured base count.
**Note:** Actual attempts are capped at `15`. Path count 0 flood retries use `count * 2`; path count 1 retries use `count * 1.5`, rounded up; path count 2 and higher use the configured base count. Setting `count` to `0` immediately removes queued and future flood retries; a packet already transmitting is allowed to finish.
**Defaults:**
- `infra`: `1`
@@ -1462,8 +1470,8 @@ set direct.retry.count 15
- The failed echo window includes a packet-length add-on. TRACE and
ANON_REQ/type 7 packets keep the existing 4x line-time add-on. TXT_MSG/type 2
packets use 7x. Other direct retry packets use 6x.
- Non-repeater firmware uses the same packet-type add-ons with the shared
fixed base retry timing.
- Room-server and sensor firmware use this configured base with the same
packet-type add-ons.
- For non-TRACE direct paths shorter than 6 remaining hops, the effective wait is scaled by `hops / 6`.
- Non-TRACE direct paths with 6 or more remaining hops use the configured value unchanged.
- TRACE retries shorter than 16 remaining hops use `hops / 16`; 16 or more remaining hops use the configured value unchanged.
@@ -1495,8 +1503,8 @@ set direct.retry.base 500
- This is added after the failed echo window. TRACE and ANON_REQ/type 7 packets
keep the existing 4x packet-length add-on. TXT_MSG/type 2 packets use 7x.
Other direct retry packets use 6x.
- Non-repeater firmware uses the same packet-type add-ons with the shared
fixed retry step.
- Room-server and sensor firmware use this configured step with the same
packet-type add-ons.
- For non-TRACE direct paths shorter than 6 remaining hops, that computed delay is scaled by `hops / 6`.
- Non-TRACE direct paths with 6 or more remaining hops use the computed delay unchanged.
- TRACE retries shorter than 16 remaining hops use `hops / 16`; 16 or more remaining hops use the computed delay unchanged.
@@ -1526,6 +1534,7 @@ set direct.retry.step 250
**Default:** `5.00` with the `rooftop` preset
**Notes:**
- This command is repeater-only.
- Unknown repeaters are still retried.
- Known repeaters below the receive floor plus this margin are skipped.
- Failed attempts lower the recent repeater SNR estimate by `0.25 dB`.
@@ -1545,6 +1554,7 @@ set direct.retry.margin 10
**Usage:**
- `get direct.retry.cr`
- `set direct.retry.cr off`
- `set direct.retry.cr on` (room-server and sensor)
- `set direct.retry.cr <cr4_min>,<cr5_min>,<cr7_min>,<cr8_max>`
**Parameters:**
@@ -1561,6 +1571,8 @@ set direct.retry.margin 10
- Repeater retry attempts escalate from the adaptive starting CR. CR4 starts as CR4, CR5, CR7, CR7, then CR8. CR5 starts as CR5, CR7, CR7, then CR8. CR7 gets two attempts, then CR8.
- Repeater adaptive CR selection intentionally skips CR6.
- Non-repeater retry packets start at the current radio CR and follow the same escalation pattern, clamped at CR8. With the normal CR5 radio setting this is CR5, CR7, CR7, then CR8.
- Room-server and sensor firmware accept `on` or `off`; numeric SNR thresholds
are repeater-only because those roles do not keep recent-repeater SNR data.
- `off` disables per-packet retry CR overrides and uses the current radio CR.
- Direct path retry packets sent at CR4 or CR5 temporarily use a shorter 16-symbol preamble, then restore the radio's default preamble.
- Unknown repeaters start at `+3.00 dB` for adaptive CR selection.
@@ -1571,6 +1583,7 @@ set direct.retry.margin 10
```
get direct.retry.cr
set direct.retry.cr off
set direct.retry.cr on
set direct.retry.cr 10.0,7.5,2.5,2.5
set direct.retry.cr 12.0,8.0,4.0,1.0
set direct.retry.cr 8.0,5.0,1.5,0
@@ -1612,6 +1625,8 @@ set direct.retry.cr 20.0,12.0,6.0,2.0
- `snr_db`: Optional SNR in dB. If omitted or invalid, defaults to `3.0`.
- `page`: 1-based result page.
**Note:** These commands are repeater-only.
**Output order:**
- `get recent.repeater` lists 3-byte prefixes first, then 2-byte prefixes, then 1-byte prefixes.
- Within each prefix length, entries are sorted from highest SNR to lowest SNR.
+5
View File
@@ -918,6 +918,11 @@ uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
return getRNG()->nextInt(0, 5*t + 1);
}
void MyMesh::onRetryConfigChanged() {
if (!_prefs.direct_retry_enabled) cancelAllDirectRetries();
if (_prefs.disable_fwd || _prefs.flood_retry_attempts == 0) cancelAllFloodRetries();
}
bool MyMesh::extractDirectRetryPrefix(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const {
if (packet == NULL || !packet->isRouteDirect() || packet->getPathHashCount() == 0) {
return false;
+3
View File
@@ -326,6 +326,9 @@ protected:
uint32_t getRetransmitDelay(const mesh::Packet* packet) override;
uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override;
bool supportsBasicRetryConfig() const override { return true; }
bool supportsAdvancedRetryConfig() const override { return true; }
void onRetryConfigChanged() override;
uint8_t getDefaultTxCodingRate() const override { return active_cr; }
bool allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const override;
bool maybeShortCircuitDirect(mesh::Packet* packet) override;
+65
View File
@@ -307,6 +307,71 @@ uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
return getRNG()->nextInt(0, 5*t + 1);
}
bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash,
uint8_t next_hop_hash_len) const {
(void)packet;
(void)next_hop_hash;
(void)next_hop_hash_len;
return _prefs.direct_retry_enabled != 0;
}
void MyMesh::configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* original,
uint8_t retry_attempt) {
if (!_prefs.direct_retry_cr_enabled) {
if (retry != NULL) retry->tx_cr = 0;
return;
}
mesh::Mesh::configureDirectRetryPacket(retry, original, retry_attempt);
}
uint32_t MyMesh::getDirectRetryEchoDelay(const mesh::Packet* packet) const {
uint32_t base_ms = constrain((uint32_t)_prefs.direct_retry_base_ms, (uint32_t)10, (uint32_t)5000);
return base_ms + getDirectRetryPacketAirtimeDelay(packet);
}
uint8_t MyMesh::getDirectRetryMaxAttempts(const mesh::Packet* packet) const {
if (packet != NULL && packet->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) return 21;
return constrain(_prefs.direct_retry_attempts, 1, 15);
}
uint32_t MyMesh::getDirectRetryAttemptDelay(const mesh::Packet* packet, uint8_t attempt_idx) {
uint32_t step_ms = constrain((uint32_t)_prefs.direct_retry_step_ms, (uint32_t)0, (uint32_t)5000);
return getDirectRetransmitDelay(packet) + getDirectRetryEchoDelay(packet)
+ ((uint32_t)attempt_idx * step_ms);
}
bool MyMesh::allowFloodRetry(const mesh::Packet* packet) const {
if (_prefs.disable_fwd || _prefs.flood_retry_attempts == 0) return false;
return packet == NULL || packet->getPayloadType() != PAYLOAD_TYPE_ADVERT
|| _prefs.flood_retry_advert_enabled;
}
uint8_t MyMesh::getFloodRetryMaxPathLength(const mesh::Packet* packet) const {
(void)packet;
return _prefs.flood_retry_max_path == FLOOD_RETRY_PATH_GATE_DISABLED
? FLOOD_RETRY_PATH_GATE_DISABLED
: constrain(_prefs.flood_retry_max_path, 0, 63);
}
uint8_t MyMesh::getFloodRetryMaxAttempts(const mesh::Packet* packet) const {
if (_prefs.disable_fwd) return 0;
uint8_t attempts = constrain(_prefs.flood_retry_attempts, 0, 15);
uint16_t scaled_attempts = attempts;
uint8_t hops = packet != NULL ? packet->getPathHashCount() : 0;
if (hops == 0) {
scaled_attempts = (uint16_t)attempts * 2U;
} else if (hops == 1) {
scaled_attempts = (((uint16_t)attempts * 3U) + 1U) / 2U;
}
return scaled_attempts > 15 ? 15 : (uint8_t)scaled_attempts;
}
void MyMesh::onRetryConfigChanged() {
if (!_prefs.direct_retry_enabled) cancelAllDirectRetries();
if (_prefs.disable_fwd || _prefs.flood_retry_attempts == 0) cancelAllFloodRetries();
}
bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
if (_prefs.disable_fwd) return false;
if (packet->isRouteFlood()) {
+13
View File
@@ -163,6 +163,19 @@ protected:
const char* getLogDateTime() override;
uint32_t getRetransmitDelay(const mesh::Packet* packet) override;
uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override;
bool allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash,
uint8_t next_hop_hash_len) const override;
void configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* original,
uint8_t retry_attempt) override;
uint32_t getDirectRetryEchoDelay(const mesh::Packet* packet) const override;
uint8_t getDirectRetryMaxAttempts(const mesh::Packet* packet) const override;
uint32_t getDirectRetryAttemptDelay(const mesh::Packet* packet, uint8_t attempt_idx) override;
bool allowFloodRetry(const mesh::Packet* packet) const override;
uint8_t getFloodRetryMaxPathLength(const mesh::Packet* packet) const override;
uint8_t getFloodRetryMaxAttempts(const mesh::Packet* packet) const override;
bool supportsBasicRetryConfig() const override { return true; }
void onRetryConfigChanged() override;
int getInterferenceThreshold() const override {
return _prefs.interference_threshold;
+66
View File
@@ -364,6 +364,72 @@ uint32_t SensorMesh::getDirectRetransmitDelay(const mesh::Packet* packet) {
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
return getRNG()->nextInt(0, 6)*t;
}
bool SensorMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash,
uint8_t next_hop_hash_len) const {
(void)packet;
(void)next_hop_hash;
(void)next_hop_hash_len;
return _prefs.direct_retry_enabled != 0;
}
void SensorMesh::configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* original,
uint8_t retry_attempt) {
if (!_prefs.direct_retry_cr_enabled) {
if (retry != NULL) retry->tx_cr = 0;
return;
}
mesh::Mesh::configureDirectRetryPacket(retry, original, retry_attempt);
}
uint32_t SensorMesh::getDirectRetryEchoDelay(const mesh::Packet* packet) const {
uint32_t base_ms = constrain((uint32_t)_prefs.direct_retry_base_ms, (uint32_t)10, (uint32_t)5000);
return base_ms + getDirectRetryPacketAirtimeDelay(packet);
}
uint8_t SensorMesh::getDirectRetryMaxAttempts(const mesh::Packet* packet) const {
if (packet != NULL && packet->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) return 21;
return constrain(_prefs.direct_retry_attempts, 1, 15);
}
uint32_t SensorMesh::getDirectRetryAttemptDelay(const mesh::Packet* packet, uint8_t attempt_idx) {
uint32_t step_ms = constrain((uint32_t)_prefs.direct_retry_step_ms, (uint32_t)0, (uint32_t)5000);
return getDirectRetransmitDelay(packet) + getDirectRetryEchoDelay(packet)
+ ((uint32_t)attempt_idx * step_ms);
}
bool SensorMesh::allowFloodRetry(const mesh::Packet* packet) const {
if (_prefs.disable_fwd || _prefs.flood_retry_attempts == 0) return false;
return packet == NULL || packet->getPayloadType() != PAYLOAD_TYPE_ADVERT
|| _prefs.flood_retry_advert_enabled;
}
uint8_t SensorMesh::getFloodRetryMaxPathLength(const mesh::Packet* packet) const {
(void)packet;
return _prefs.flood_retry_max_path == FLOOD_RETRY_PATH_GATE_DISABLED
? FLOOD_RETRY_PATH_GATE_DISABLED
: constrain(_prefs.flood_retry_max_path, 0, 63);
}
uint8_t SensorMesh::getFloodRetryMaxAttempts(const mesh::Packet* packet) const {
if (_prefs.disable_fwd) return 0;
uint8_t attempts = constrain(_prefs.flood_retry_attempts, 0, 15);
uint16_t scaled_attempts = attempts;
uint8_t hops = packet != NULL ? packet->getPathHashCount() : 0;
if (hops == 0) {
scaled_attempts = (uint16_t)attempts * 2U;
} else if (hops == 1) {
scaled_attempts = (((uint16_t)attempts * 3U) + 1U) / 2U;
}
return scaled_attempts > 15 ? 15 : (uint8_t)scaled_attempts;
}
void SensorMesh::onRetryConfigChanged() {
if (!_prefs.direct_retry_enabled) cancelAllDirectRetries();
if (_prefs.disable_fwd || _prefs.flood_retry_attempts == 0) cancelAllFloodRetries();
}
int SensorMesh::getInterferenceThreshold() const {
return _prefs.interference_threshold;
}
+13
View File
@@ -126,6 +126,19 @@ protected:
int calcRxDelay(float score, uint32_t air_time) const override;
uint32_t getRetransmitDelay(const mesh::Packet* packet) override;
uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override;
bool allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash,
uint8_t next_hop_hash_len) const override;
void configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* original,
uint8_t retry_attempt) override;
uint32_t getDirectRetryEchoDelay(const mesh::Packet* packet) const override;
uint8_t getDirectRetryMaxAttempts(const mesh::Packet* packet) const override;
uint32_t getDirectRetryAttemptDelay(const mesh::Packet* packet, uint8_t attempt_idx) override;
bool allowFloodRetry(const mesh::Packet* packet) const override;
uint8_t getFloodRetryMaxPathLength(const mesh::Packet* packet) const override;
uint8_t getFloodRetryMaxAttempts(const mesh::Packet* packet) const override;
bool supportsBasicRetryConfig() const override { return true; }
void onRetryConfigChanged() override;
int getInterferenceThreshold() const override;
bool getCADEnabled() const override;
int getAGCResetInterval() const override;
+88 -16
View File
@@ -140,6 +140,8 @@ uint8_t Mesh::getOtaHopLimit() const { return ota::ota_ctx().manager.max_hops();
#endif
void Mesh::begin() {
_active_direct_retry_count = 0;
_active_flood_retry_count = 0;
_waiting_direct_retry_count = 0;
_waiting_flood_retry_count = 0;
_next_direct_retry_timeout = 0;
@@ -866,6 +868,9 @@ void Mesh::clearDirectRetrySlot(int idx) {
&& _waiting_direct_retry_count > 0) {
_waiting_direct_retry_count--;
}
if (_direct_retries[idx].active && _active_direct_retry_count > 0) {
_active_direct_retry_count--;
}
_direct_retries[idx].packet = NULL;
_direct_retries[idx].trigger_packet = NULL;
_direct_retries[idx].retry_started_at = 0;
@@ -905,10 +910,12 @@ void Mesh::rebuildNextDirectRetryTimeout() {
}
bool Mesh::usePassiveChannelCheck(const Packet* packet) const {
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
if (_direct_retries[i].active && _direct_retries[i].queued
&& _direct_retries[i].packet == packet) {
return true;
if (_active_direct_retry_count != 0) {
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
if (_direct_retries[i].active && _direct_retries[i].queued
&& _direct_retries[i].packet == packet) {
return true;
}
}
}
@@ -917,10 +924,12 @@ bool Mesh::usePassiveChannelCheck(const Packet* packet) const {
// CAD here too, since restarting RX can hide that echo. The initial flood
// has trigger_packet set but queued=false, so ordinary flood forwarding
// continues to use the normal CAD check.
for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) {
if (_flood_retries[i].active && _flood_retries[i].queued
&& _flood_retries[i].packet == packet) {
return true;
if (_active_flood_retry_count != 0) {
for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) {
if (_flood_retries[i].active && _flood_retries[i].queued
&& _flood_retries[i].packet == packet) {
return true;
}
}
}
@@ -954,6 +963,8 @@ void Mesh::calculateDirectRetryKey(const Packet* packet, uint8_t* dest_key) cons
}
bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) {
if (_active_direct_retry_count == 0) return false;
uint8_t recv_key[MAX_HASH_SIZE];
calculateDirectRetryKey(packet, recv_key);
@@ -1016,6 +1027,8 @@ bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) {
}
void Mesh::armDirectRetryOnSendComplete(const Packet* packet) {
if (_active_direct_retry_count == 0) return;
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
if (!_direct_retries[i].active) {
continue;
@@ -1136,6 +1149,8 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) {
}
void Mesh::clearPendingDirectRetryOnSendFail(const Packet* packet) {
if (_active_direct_retry_count == 0) return;
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
if (!_direct_retries[i].active) {
continue;
@@ -1339,6 +1354,7 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority, bool
_direct_retries[slot_idx].waiting_final_echo = false;
_direct_retries[slot_idx].queued = false;
_direct_retries[slot_idx].active = true;
_active_direct_retry_count++;
}
void Mesh::clearFloodRetrySlot(int idx) {
@@ -1346,6 +1362,9 @@ void Mesh::clearFloodRetrySlot(int idx) {
&& _flood_retries[idx].waiting_final_echo
&& _flood_retries[idx].retry_at == _next_flood_retry_timeout;
if (_flood_retries[idx].active) {
if (_active_flood_retry_count > 0) {
_active_flood_retry_count--;
}
if (_flood_retries[idx].waiting_final_echo && _waiting_flood_retry_count > 0) {
_waiting_flood_retry_count--;
}
@@ -1386,8 +1405,52 @@ void Mesh::rebuildNextFloodRetryTimeout() {
if (!found) _next_flood_retry_timeout = 0;
}
void Mesh::cancelAllDirectRetries() {
if (_active_direct_retry_count == 0) return;
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
if (!_direct_retries[i].active) continue;
Packet* retry = _direct_retries[i].queued ? _direct_retries[i].packet : NULL;
if (retry != NULL && retry != getOutboundInFlight()) {
for (int j = 0; j < _mgr->getOutboundTotal(); j++) {
if (_mgr->getOutboundByIdx(j) != retry) continue;
Packet* pending = _mgr->removeOutboundByIdx(j);
if (pending != NULL) {
_direct_retries[i].packet = NULL;
releasePacket(pending);
}
break;
}
}
clearDirectRetrySlot(i);
}
}
void Mesh::cancelAllFloodRetries() {
if (_active_flood_retry_count == 0) return;
for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) {
if (!_flood_retries[i].active) continue;
Packet* retry = _flood_retries[i].queued ? _flood_retries[i].packet : NULL;
if (retry != NULL && retry != getOutboundInFlight()) {
for (int j = 0; j < _mgr->getOutboundTotal(); j++) {
if (_mgr->getOutboundByIdx(j) != retry) continue;
Packet* pending = _mgr->removeOutboundByIdx(j);
if (pending != NULL) {
_flood_retries[i].packet = NULL;
releasePacket(pending);
}
break;
}
}
clearFloodRetrySlot(i);
}
}
bool Mesh::cancelActiveRetries(const uint8_t retry_key[MAX_HASH_SIZE]) {
if (retry_key == NULL) {
if (retry_key == NULL || (_active_direct_retry_count == 0 && _active_flood_retry_count == 0)) {
return false;
}
@@ -1444,7 +1507,7 @@ bool Mesh::cancelActiveRetries(const uint8_t retry_key[MAX_HASH_SIZE]) {
}
bool Mesh::hasActiveRetries(const uint8_t retry_key[MAX_HASH_SIZE]) const {
if (retry_key == NULL) {
if (retry_key == NULL || (_active_direct_retry_count == 0 && _active_flood_retry_count == 0)) {
return false;
}
@@ -1468,6 +1531,8 @@ bool Mesh::isFloodRetryEchoTarget(const Packet* packet, uint8_t progress_marker)
}
bool Mesh::cancelFloodRetryOnEcho(const Packet* packet) {
if (_active_flood_retry_count == 0) return false;
uint8_t recv_key[MAX_HASH_SIZE];
packet->calculatePacketHash(recv_key);
@@ -1507,6 +1572,8 @@ bool Mesh::cancelFloodRetryOnEcho(const Packet* packet) {
}
void Mesh::armFloodRetryOnSendComplete(const Packet* packet) {
if (_active_flood_retry_count == 0) return;
for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) {
if (!_flood_retries[i].active) {
continue;
@@ -1604,6 +1671,8 @@ void Mesh::armFloodRetryOnSendComplete(const Packet* packet) {
}
void Mesh::clearPendingFloodRetryOnSendFail(const Packet* packet) {
if (_active_flood_retry_count == 0) return;
for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) {
if (!_flood_retries[i].active) {
continue;
@@ -1627,7 +1696,14 @@ void Mesh::clearPendingFloodRetryOnSendFail(const Packet* packet) {
}
void Mesh::maybeScheduleFloodRetry(const Packet* packet, uint8_t priority) {
if (packet == NULL || !packet->isRouteFlood() || hasFloodRetryTargetPrefix(packet)) {
if (packet == NULL || !packet->isRouteFlood()) {
return;
}
// Check the inexpensive global kill switch before prefix/path eligibility.
// This makes flood.retry.count=0 a genuinely cheap disabled state.
uint8_t max_attempts = getFloodRetryMaxAttempts(packet);
if (max_attempts == 0 || hasFloodRetryTargetPrefix(packet)) {
return;
}
@@ -1636,11 +1712,6 @@ void Mesh::maybeScheduleFloodRetry(const Packet* packet, uint8_t priority) {
return;
}
uint8_t max_attempts = getFloodRetryMaxAttempts(packet);
if (max_attempts == 0) {
return;
}
uint8_t retry_key[MAX_HASH_SIZE];
packet->calculatePacketHash(retry_key);
for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) {
@@ -1680,6 +1751,7 @@ void Mesh::maybeScheduleFloodRetry(const Packet* packet, uint8_t priority) {
_flood_retries[slot_idx].waiting_final_echo = false;
_flood_retries[slot_idx].queued = false;
_flood_retries[slot_idx].active = true;
_active_flood_retry_count++;
}
Packet* Mesh::createAdvert(const LocalIdentity& id, const uint8_t* app_data, size_t app_data_len) {
+7
View File
@@ -92,6 +92,8 @@ class Mesh : public Dispatcher {
MeshTables* _tables;
DirectRetryEntry _direct_retries[MAX_DIRECT_RETRY_SLOTS];
FloodRetryEntry _flood_retries[MAX_FLOOD_RETRY_SLOTS];
uint8_t _active_direct_retry_count;
uint8_t _active_flood_retry_count;
uint8_t _waiting_direct_retry_count;
uint8_t _waiting_flood_retry_count;
unsigned long _next_direct_retry_timeout;
@@ -202,6 +204,11 @@ protected:
*/
bool cancelActiveRetries(const uint8_t retry_key[MAX_HASH_SIZE]);
// Cancel every queued/future retry of one route type. A packet already on
// air is allowed to finish, but no later retry is armed from it.
void cancelAllDirectRetries();
void cancelAllFloodRetries();
/**
* \returns true while a direct or flood retry sequence owns this packet payload hash.
*/
+69 -5
View File
@@ -146,6 +146,32 @@ static bool looksUnsignedInteger(const char* s) {
return saw_digit;
}
static bool configKeyMatches(const char* config, const char* key) {
if (config == NULL || key == NULL) return false;
size_t key_len = strlen(key);
return strncmp(config, key, key_len) == 0
&& (config[key_len] == 0 || config[key_len] == ' ' || config[key_len] == '.');
}
static bool isAdvancedRetryConfig(const char* config) {
return configKeyMatches(config, "direct.retry.heard")
|| configKeyMatches(config, "direct.retry.margin")
|| configKeyMatches(config, "flood.retry.prefixes")
|| configKeyMatches(config, "flood.retry.ignore")
|| configKeyMatches(config, "flood.retry.bridge")
|| configKeyMatches(config, "flood.retry.bucket")
|| configKeyMatches(config, "recent.repeater")
|| configKeyMatches(config, "recent.repeaters");
}
static bool isBasicRetryConfig(const char* config) {
return configKeyMatches(config, "retry.preset")
|| configKeyMatches(config, "direct.retry")
|| configKeyMatches(config, "flood.retry.count")
|| configKeyMatches(config, "flood.retry.path")
|| configKeyMatches(config, "flood.retry.advert");
}
static bool parseUint8Strict(const char* value, uint8_t min_value, uint8_t max_value, uint8_t& result) {
if (value == NULL || *value == 0) {
return false;
@@ -1818,8 +1844,12 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
_callbacks->clearStats();
strcpy(reply, "(OK - stats reset)");
} else if (memcmp(command, "clear recent.repeater", 21) == 0 && (command[21] == 0 || command[21] == ' ')) {
_callbacks->clearRecentRepeaters();
strcpy(reply, "OK");
if (_callbacks->supportsAdvancedRetryConfig()) {
_callbacks->clearRecentRepeaters();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, unsupported on this role");
}
} else if (memcmp(command, "get ", 4) == 0) {
handleGetCmd(sender_timestamp, command, reply);
} else if (memcmp(command, "set ", 4) == 0) {
@@ -2066,6 +2096,14 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
const char* config = &command[4];
// Observer/MQTT/WiFi/timezone/alert/SNMP commands live in CommonCLI_Observer.cpp.
if (handleObserverSetCmd(sender_timestamp, config, reply)) return;
if (isAdvancedRetryConfig(config) && !_callbacks->supportsAdvancedRetryConfig()) {
strcpy(reply, "Error, unsupported on this role");
return;
}
if (isBasicRetryConfig(config) && !_callbacks->supportsBasicRetryConfig()) {
strcpy(reply, "Error, retry configuration unsupported on this role");
return;
}
if (memcmp(config, "dutycycle ", 10) == 0) {
float dc = atof(&config[10]);
if (dc < 1 || dc > 100) {
@@ -2160,9 +2198,14 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
strcpy(reply, "Error, bad chars");
}
} else if (memcmp(config, "repeat ", 7) == 0) {
_prefs->disable_fwd = memcmp(&config[7], "off", 3) == 0;
savePrefs();
strcpy(reply, _prefs->disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON");
if (strcmp(&config[7], "on") == 0 || strcmp(&config[7], "off") == 0) {
_prefs->disable_fwd = strcmp(&config[7], "off") == 0;
savePrefs();
_callbacks->onRetryConfigChanged();
strcpy(reply, _prefs->disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON");
} else {
strcpy(reply, "Error, must be on or off");
}
} else if (memcmp(config, "radio.rxgain ", 13) == 0) {
bool enabled = memcmp(&config[13], "on", 2) == 0;
if (_callbacks->setRxBoostedGain(enabled)) {
@@ -2411,10 +2454,12 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
if (strcmp(&config[13], "on") == 0) {
_prefs->direct_retry_enabled = 1;
savePrefs();
_callbacks->onRetryConfigChanged();
strcpy(reply, "OK");
} else if (strcmp(&config[13], "off") == 0) {
_prefs->direct_retry_enabled = 0;
savePrefs();
_callbacks->onRetryConfigChanged();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, must be on or off");
@@ -2558,6 +2603,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
_prefs->flood_retry_attempts = (uint8_t)attempts;
_prefs->retry_preset = RETRY_PRESET_CUSTOM;
savePrefs();
_callbacks->onRetryConfigChanged();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, must be 0-15");
@@ -2638,6 +2684,14 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
_prefs->direct_retry_cr_enabled = 0;
savePrefs();
strcpy(reply, "OK");
} else if (!_callbacks->supportsAdvancedRetryConfig()) {
if (strcmp(&config[16], "on") == 0) {
_prefs->direct_retry_cr_enabled = 1;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, use on or off on this role");
}
} else {
strcpy(tmp, &config[16]);
const char *parts[4];
@@ -2837,6 +2891,14 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
const char* config = &command[4];
// Observer/MQTT/WiFi/timezone/alert/SNMP commands live in CommonCLI_Observer.cpp.
if (handleObserverGetCmd(sender_timestamp, config, reply)) return;
if (isAdvancedRetryConfig(config) && !_callbacks->supportsAdvancedRetryConfig()) {
strcpy(reply, "Error, unsupported on this role");
return;
}
if (isBasicRetryConfig(config) && !_callbacks->supportsBasicRetryConfig()) {
strcpy(reply, "Error, retry configuration unsupported on this role");
return;
}
int recent_page = 1;
if (memcmp(config, "dutycycle", 9) == 0) {
float dc = 100.0f / (_prefs->airtime_factor + 1.0f);
@@ -2988,6 +3050,8 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
} else if (memcmp(config, "direct.retry.cr", 15) == 0) {
if (!_prefs->direct_retry_cr_enabled) {
strcpy(reply, "> off");
} else if (!_callbacks->supportsAdvancedRetryConfig()) {
strcpy(reply, "> on");
} else {
char cr4[12], cr5[12], cr7[12], cr8[12];
formatSnrDbX4(cr4, sizeof(cr4), _prefs->direct_retry_cr4_snr_x4);
+6
View File
@@ -447,6 +447,12 @@ public:
}
virtual void clearRecentRepeaters() {
}
// Basic retry controls are portable across roles (enable/count/timing,
// flood count/path/advert, and the current-CR retry progression). Advanced
// controls require the repeater's recent-SNR and bridge-routing tables.
virtual bool supportsBasicRetryConfig() const { return false; }
virtual bool supportsAdvancedRetryConfig() const { return false; }
virtual void onRetryConfigChanged() { }
virtual mesh::LocalIdentity& getSelfId() = 0;
virtual void saveIdentity(const mesh::LocalIdentity& new_id) = 0;
virtual void clearStats() = 0;