From b8f438d6ea7f22e7996ac95015665dfd0fd26258 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 13 Jul 2026 15:22:36 -0700 Subject: [PATCH] Honor retry settings across firmware roles --- docs/cli_commands.md | 27 +++++-- examples/simple_repeater/MyMesh.cpp | 5 ++ examples/simple_repeater/MyMesh.h | 3 + examples/simple_room_server/MyMesh.cpp | 65 ++++++++++++++++ examples/simple_room_server/MyMesh.h | 13 ++++ examples/simple_sensor/SensorMesh.cpp | 66 ++++++++++++++++ examples/simple_sensor/SensorMesh.h | 13 ++++ src/Mesh.cpp | 104 +++++++++++++++++++++---- src/Mesh.h | 7 ++ src/helpers/CommonCLI.cpp | 74 ++++++++++++++++-- src/helpers/CommonCLI.h | 6 ++ 11 files changed, 356 insertions(+), 27 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 1facb639..b5932e8e 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -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 ,,,` **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. diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index d86f37a0..08fb54f9 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -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; diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index bc693110..c83c1a50 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -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; diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index bea1e402..ebc562fe 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -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()) { diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 2d5691f3..4661185c 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -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; diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 5df0480c..7e8243c9 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -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; } diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 9cf8fff5..6c5f9360 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -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; diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 451c253c..deb3b125 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -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) { diff --git a/src/Mesh.h b/src/Mesh.h index 90b651ee..5af6d4e6 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -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. */ diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 01d3e1be..6bf3fd51 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -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); diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 357dc6ad..c76fbc9b 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -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;