diff --git a/docs/cli_commands.md b/docs/cli_commands.md index c06f5e12b3..9c816cf0f6 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -950,6 +950,239 @@ region save - Enables flooding for all child regions automatically - Useful for global networks with specific regional rules +--- +### Direct Retry + +Direct retry resends direct-routed packets when the downstream echo is not heard. It applies to direct messages and TRACE packets. It does not change ACK handling. + +#### View or change direct retry state +**Usage:** +- `get direct.retry` +- `set direct.retry ` + +**Parameters:** +- `state`: `on`|`off` + +**Default:** `on` + +**Examples:** +``` +get direct.retry +set direct.retry on +set direct.retry off +``` + +--- + +#### View or apply a direct retry preset +**Usage:** +- `get retry.preset` +- `set retry.preset ` + +**Parameters:** +- `preset`: `infra`|`rooftop`|`mobile` + +**Notes:** +- `infra`: fewer, slower retries for stable fixed infrastructure. +- `rooftop`: default long retry window for weak rooftop links. +- `mobile`: long retry count with shorter spacing for moving or changing links. +- Changing `direct.retry.count`, `direct.retry.base`, `direct.retry.step`, or `direct.retry.margin` makes the preset report as `custom`. + +**Examples:** +``` +get retry.preset +set retry.preset infra +set retry.preset rooftop +set retry.preset mobile +``` + +--- + +#### View or change direct retry count +**Usage:** +- `get direct.retry.count` +- `set direct.retry.count ` + +**Parameters:** +- `count`: Maximum retry attempts after the original send, from `1` to `15`. + +**Default:** `15` with the `rooftop` preset + +**Examples:** +``` +get direct.retry.count +set direct.retry.count 1 +set direct.retry.count 4 +set direct.retry.count 15 +``` + +--- + +#### View or change direct retry base delay +**Usage:** +- `get direct.retry.base` +- `set direct.retry.base ` + +**Parameters:** +- `ms`: First retry wait in milliseconds, from `10` to `5000`. + +**Default:** `175` with the `rooftop` preset + +**Explanation:** +- The first retry waits `base` milliseconds after the failed echo window. +- Larger values reduce channel pressure and give slow repeaters more time. +- Smaller values recover faster but create tighter retry bursts. + +**Examples:** +``` +get direct.retry.base +set direct.retry.base 175 +set direct.retry.base 275 +set direct.retry.base 500 +``` + +--- + +#### View or change direct retry step delay +**Usage:** +- `get direct.retry.step` +- `set direct.retry.step ` + +**Parameters:** +- `ms`: Extra milliseconds added for each subsequent retry, from `0` to `5000`. + +**Default:** `100` with the `rooftop` preset + +**Explanation:** +- Retry delay is `base + attempt_index * step`. +- With `base=175` and `step=100`, retries wait about `175`, `275`, `375`, `475` ms, and so on. +- `step=0` keeps every retry at the same delay. +- Larger steps spread retries over time and are safer on busy channels. + +**Examples:** +``` +get direct.retry.step +set direct.retry.step 0 +set direct.retry.step 50 +set direct.retry.step 100 +set direct.retry.step 250 +``` + +--- + +#### View or change direct retry SNR margin +**Usage:** +- `get direct.retry.margin` +- `set direct.retry.margin ` + +**Parameters:** +- `snr_db`: Extra SNR margin above the SF receive floor, from `0` to `40`. + +**Default:** `5.00` with the `rooftop` preset + +**Notes:** +- 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`. + +**Examples:** +``` +get direct.retry.margin +set direct.retry.margin 0 +set direct.retry.margin 2.5 +set direct.retry.margin 5 +set direct.retry.margin 10 +``` + +--- + +#### View or change adaptive direct retry coding rate +**Usage:** +- `get direct.retry.cr` +- `set direct.retry.cr off` +- `set direct.retry.cr ,,,` + +**Parameters:** +- `cr4_min`: Minimum SNR in dB to retry at CR4. +- `cr5_min`: Minimum SNR in dB to retry at CR5. +- `cr7_min`: Minimum SNR in dB to retry at CR7. +- `cr8_max`: Maximum SNR in dB that forces CR8. + +**Default:** `10.00,7.50,2.50,2.50` + +**Explanation:** +- Higher SNR uses faster coding rates. +- Lower SNR uses more robust coding rates. +- CR6 is intentionally skipped. +- `off` disables per-packet retry CR overrides and uses the current radio CR. +- Unknown repeaters start at `+3.00 dB` for adaptive CR selection. +- A failed unknown repeater is seeded at `+2.75 dB`. +- Each later failure lowers the SNR estimate by `0.25 dB`. + +**Examples:** +``` +get direct.retry.cr +set direct.retry.cr off +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 +set direct.retry.cr 6.0,3.0,0,-2.0 +set direct.retry.cr 20.0,12.0,6.0,2.0 +set direct.retry.cr 4.0,2.0,0,-4.0 +``` + +**Example profiles:** +- Conservative weak-link profile: +``` +set direct.retry.cr 12.0,8.0,4.0,1.0 +``` +- Balanced rooftop profile: +``` +set direct.retry.cr 10.0,7.5,2.5,2.5 +``` +- Faster strong-link profile: +``` +set direct.retry.cr 6.0,3.0,0,-2.0 +``` +- Very cautious noisy-link profile: +``` +set direct.retry.cr 20.0,12.0,6.0,2.0 +``` + +--- + +#### View, seed, or clear the recent repeater table +**Usage:** +- `get recent.repeater` +- `get recent.repeater ` +- `set recent.repeater [snr_db]` +- `clear recent.repeater` + +**Parameters:** +- `prefix`: Repeater path-hash prefix as hex. +- `snr_db`: Optional SNR in dB. If omitted or invalid, defaults to `3.0`. +- `page`: 1-based result page. + +**SNR details:** +- Recent repeater SNR is stored internally in quarter-dB units. +- Heard repeater samples update an existing table entry with a weighted blend: `75%` existing SNR and `25%` new heard SNR, rounded up. +- Direct retry success also feeds the heard echo SNR back into the same weighted table. +- Direct retry failure is not weighted: each final echo-timeout failure lowers that repeater's SNR by `0.25 dB`. +- Unknown repeaters start at `+3.00 dB` for adaptive CR selection. +- If an unknown repeater fails, it is seeded into the table at `+2.75 dB`. +- `set recent.repeater [snr_db]` seeds a missing prefix or adds another weighted sample for an existing prefix. +- Successful `set recent.repeater` replies include the stored prefix and SNR, for example `OK - set A1B2C3 at 3.0 SNR`. + +**Examples:** +``` +get recent.repeater +get recent.repeater 2 +set recent.repeater A1B2C3 8.5 +set recent.repeater 71CE82 -3.25 +set recent.repeater A1B2C3 +clear recent.repeater +``` + --- ### GPS (When GPS support is compiled in) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 5cc3a9a11e..4261edd995 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -549,6 +549,203 @@ uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { return getRNG()->nextInt(0, 5*t + 1); } +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; + } + prefix_len = packet->getPathHashSize(); + memcpy(prefix, packet->path, prefix_len); + return true; +} + +int8_t MyMesh::getDirectRetryMinSNRX4() const { + switch (active_sf) { + case 7: return -30; + case 8: return -40; + case 9: return -50; + case 10: return -60; + case 11: return -70; + case 12: return -80; + default: return -60; + } +} + +uint8_t MyMesh::getDirectRetryCodingRateForSNR(int8_t snr_x4) const { + if (!_prefs.direct_retry_cr_enabled) return 0; + if (snr_x4 >= _prefs.direct_retry_cr4_snr_x4) return 4; + if (snr_x4 >= _prefs.direct_retry_cr5_snr_x4) return 5; + if (snr_x4 >= _prefs.direct_retry_cr7_snr_x4) return 7; + if (snr_x4 <= _prefs.direct_retry_cr8_snr_x4) return 8; + return 7; +} + +uint8_t MyMesh::getDirectRetryConfiguredMaxAttempts() const { + return constrain(_prefs.direct_retry_attempts, 1, 15); +} + +uint32_t MyMesh::getDirectRetryAttemptStepMillis() const { + return _prefs.direct_retry_step_ms; +} + +bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const { + (void)packet; + if (!_prefs.direct_retry_enabled) { + return false; + } + if (next_hop_hash == NULL || next_hop_hash_len == 0) { + return true; + } + const SimpleMeshTables* tables = static_cast(getTables()); + const SimpleMeshTables::RecentRepeaterInfo* repeater = tables != NULL + ? tables->findRecentRepeaterByHash(next_hop_hash, next_hop_hash_len) + : NULL; + + if (repeater == NULL) { + // Retry unknown repeaters too. If they fail, onDirectRetryFailed() seeds the + // recent-repeater table below the +3.00 dB starting point. + return true; + } + int16_t retry_floor_x4 = (int16_t)getDirectRetryMinSNRX4() + (int16_t)_prefs.direct_retry_snr_margin_x4; + return (int16_t)repeater->snr_x4 >= retry_floor_x4; +} + +void MyMesh::configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* original, uint8_t retry_attempt) { + (void)retry_attempt; + int8_t snr_x4 = 12; // unknown repeaters start at +3.00 dB + const SimpleMeshTables* tables = static_cast(getTables()); + if (tables != NULL) { + uint8_t prefix[MAX_HASH_SIZE]; + uint8_t prefix_len = 0; + if (extractDirectRetryPrefix(original, prefix, prefix_len)) { + const SimpleMeshTables::RecentRepeaterInfo* repeater = tables->findRecentRepeaterByHash(prefix, prefix_len); + if (repeater != NULL) { + snr_x4 = repeater->snr_x4; + } + } + } + + retry->tx_cr = getDirectRetryCodingRateForSNR(snr_x4); +} + +uint32_t MyMesh::getDirectRetryEchoDelay(const mesh::Packet* packet) const { + (void)packet; + return 200; +} + +uint8_t MyMesh::getDirectRetryMaxAttempts(const mesh::Packet* packet) const { + (void)packet; + return getDirectRetryConfiguredMaxAttempts(); +} + +uint32_t MyMesh::getDirectRetryAttemptDelay(const mesh::Packet* packet, uint8_t attempt_idx) { + (void)packet; + return _prefs.direct_retry_base_ms + ((uint32_t)attempt_idx * getDirectRetryAttemptStepMillis()); +} + +void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) { +#if MESH_DEBUG + MESH_DEBUG_PRINTLN("direct retry %s attempt=%u delay=%lu type=%u route=%s", + event ? event : "?", + (uint32_t)retry_attempt, + (unsigned long)delay_millis, + packet ? (uint32_t)packet->getPayloadType() : 0, + packet && packet->isRouteDirect() ? "D" : "F"); +#endif + if (_logging) { + File f = openAppend(PACKET_LOG_FILE); + if (f) { + f.print(getLogDateTime()); + f.printf(": direct retry %s attempt=%u delay=%lu type=%u route=%s\n", + event ? event : "?", + (uint32_t)retry_attempt, + (unsigned long)delay_millis, + packet ? (uint32_t)packet->getPayloadType() : 0, + packet && packet->isRouteDirect() ? "D" : "F"); + f.close(); + } + } +} + +void MyMesh::onDirectRetryFailed(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) { + if (next_hop_hash == NULL || next_hop_hash_len == 0) { + return; + } + + SimpleMeshTables* tables = static_cast(getTables()); + if (tables != NULL) { + if (!tables->decrementRecentRepeaterSnrX4(next_hop_hash, next_hop_hash_len, 1)) { + tables->setRecentRepeater(next_hop_hash, next_hop_hash_len, 11, false, true); + } + } +} + +void MyMesh::onDirectRetrySucceeded(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len, int8_t snr_x4) { + if (next_hop_hash == NULL || next_hop_hash_len == 0) { + return; + } + + SimpleMeshTables* tables = static_cast(getTables()); + if (tables != NULL) { + tables->setRecentRepeater(next_hop_hash, next_hop_hash_len, snr_x4, false, true); + } +} + +static void formatLocalSnrX4(char* dest, size_t dest_len, int16_t snr_x4) { + int16_t v = snr_x4; + const char* sign = ""; + if (v < 0) { + sign = "-"; + v = -v; + } + snprintf(dest, dest_len, "%s%d.%02d", sign, v / 4, (v % 4) * 25); +} + +void MyMesh::formatRecentRepeatersReply(char *reply, int page) { + const SimpleMeshTables* tables = static_cast(getTables()); + if (tables == NULL) { + strcpy(reply, "Error: unsupported"); + return; + } + int count = tables->getRecentRepeaterCount(); + if (count <= 0) { + strcpy(reply, "> -none-"); + return; + } + + const int page_size = 4; + int pages = (count + page_size - 1) / page_size; + if (page < 1) page = 1; + if (page > pages) page = pages; + + int len = snprintf(reply, 160, "> %d/%d ", page, pages); + int start = (page - 1) * page_size; + for (int i = 0; i < page_size && len < 150; i++) { + const SimpleMeshTables::RecentRepeaterInfo* info = tables->getRecentRepeaterNewestByIdx(start + i); + if (info == NULL) break; + char prefix[MAX_ROUTE_HASH_BYTES * 2 + 1]; + char snr[12]; + mesh::Utils::toHex(prefix, info->prefix, info->prefix_len); + prefix[info->prefix_len * 2] = 0; + formatLocalSnrX4(snr, sizeof(snr), info->snr_x4); + len += snprintf(&reply[len], 160 - len, "%s%s,%s", + i == 0 ? "" : " ", + prefix, + snr); + } +} + +bool MyMesh::setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4) { + SimpleMeshTables* tables = static_cast(getTables()); + return tables != NULL && tables->setRecentRepeater(prefix, prefix_len, snr_x4, false, true); +} + +void MyMesh::clearRecentRepeaters() { + SimpleMeshTables* tables = static_cast(getTables()); + if (tables != NULL) { + tables->clearRecentRepeaters(); + } +} + bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) { // just try to determine region for packet (apply later in allowPacketForward()) if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) { @@ -865,6 +1062,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc next_local_advert = next_flood_advert = 0; dirty_contacts_expiry = 0; set_radio_at = revert_radio_at = 0; + active_sf = 0; + active_cr = 0; _logging = false; region_load_active = false; @@ -894,6 +1093,19 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.flood_max_advert = 8; _prefs.interference_threshold = 0; // disabled _prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on') + _prefs.retry_preset = RETRY_PRESET_ROOFTOP; + _prefs.direct_retry_attempts = DIRECT_RETRY_ROOFTOP_COUNT; + _prefs.direct_retry_base_ms = DIRECT_RETRY_ROOFTOP_BASE_MS; + _prefs.direct_retry_step_ms = DIRECT_RETRY_ROOFTOP_STEP_MS; + _prefs.direct_retry_snr_margin_x4 = DIRECT_RETRY_ROOFTOP_MARGIN_X4; + _prefs.direct_retry_cr4_snr_x4 = DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT; + _prefs.direct_retry_cr5_snr_x4 = DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT; + _prefs.direct_retry_cr7_snr_x4 = DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT; + _prefs.direct_retry_cr8_snr_x4 = DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT; + _prefs.direct_retry_enabled = 1; + _prefs.direct_retry_cr_enabled = 1; + _prefs.direct_retry_prefs_magic[0] = DIRECT_RETRY_PREFS_MAGIC_0; + _prefs.direct_retry_prefs_magic[1] = DIRECT_RETRY_PREFS_MAGIC_1; // bridge defaults _prefs.bridge_enabled = 1; // enabled @@ -962,6 +1174,8 @@ void MyMesh::begin(FILESYSTEM *fs) { #endif radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + active_sf = _prefs.sf; + active_cr = _prefs.cr; radio_driver.setTxPower(_prefs.tx_power_dbm); radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); @@ -1289,12 +1503,16 @@ void MyMesh::loop() { if (set_radio_at && millisHasNowPassed(set_radio_at)) { // apply pending (temporary) radio params set_radio_at = 0; // clear timer radio_driver.setParams(pending_freq, pending_bw, pending_sf, pending_cr); + active_sf = pending_sf; + active_cr = pending_cr; MESH_DEBUG_PRINTLN("Temp radio params"); } if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig revert_radio_at = 0; // clear timer radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + active_sf = _prefs.sf; + active_cr = _prefs.cr; MESH_DEBUG_PRINTLN("Radio params restored"); } diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 24c4b1f2a7..fddbed3e51 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -111,7 +111,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { float pending_freq; float pending_bw; uint8_t pending_sf; + uint8_t active_sf; // live SF, including temporary radio overrides uint8_t pending_cr; + uint8_t active_cr; // live CR, including temporary radio overrides int matching_peer_indexes[MAX_CLIENTS]; #if defined(WITH_RS232_BRIDGE) RS232Bridge bridge; @@ -119,6 +121,11 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { ESPNowBridge bridge; #endif + bool extractDirectRetryPrefix(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const; + int8_t getDirectRetryMinSNRX4() const; + uint8_t getDirectRetryCodingRateForSNR(int8_t snr_x4) const; + uint8_t getDirectRetryConfiguredMaxAttempts() const; + uint32_t getDirectRetryAttemptStepMillis() const; void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood); uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data); @@ -146,6 +153,15 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint32_t getRetransmitDelay(const mesh::Packet* packet) override; uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) 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; + 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; + void onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) override; + void onDirectRetryFailed(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) override; + void onDirectRetrySucceeded(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len, int8_t snr_x4) override; int getInterferenceThreshold() const override { return _prefs.interference_threshold; @@ -217,6 +233,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { void formatStatsReply(char *reply) override; void formatRadioStatsReply(char *reply) override; void formatPacketStatsReply(char *reply) override; + void formatRecentRepeatersReply(char *reply, int page) override; + bool setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4) override; + void clearRecentRepeaters() override; void startRegionsLoad() override; bool saveRegions() override; void onDefaultRegionChanged(const RegionEntry* r) override; diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index c0610b7f8a..63112f85d8 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -52,6 +52,17 @@ void Dispatcher::updateTxBudget() { } } +void Dispatcher::restoreOutboundTxOverrides() { + if (outbound_restore_cr != 0) { + _radio->setCodingRate(outbound_restore_cr); + outbound_restore_cr = 0; + } + if (outbound_restore_preamble_len != 0) { + _radio->setPreambleLength(outbound_restore_preamble_len); + outbound_restore_preamble_len = 0; + } +} + int Dispatcher::calcRxDelay(float score, uint32_t air_time) const { return (int) ((pow(10, 0.85f - score) - 1.0) * air_time); } @@ -106,7 +117,9 @@ void Dispatcher::loop() { } _radio->onSendFinished(); + restoreOutboundTxOverrides(); logTx(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); + onSendComplete(outbound); if (outbound->isRouteFlood()) { n_sent_flood++; } else { @@ -118,7 +131,9 @@ void Dispatcher::loop() { MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): WARNING: outbound packed send timed out!", getLogDateTime()); _radio->onSendFinished(); + restoreOutboundTxOverrides(); logTxFail(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); + onSendFail(outbound); releasePacket(outbound); // return to pool outbound = NULL; @@ -149,6 +164,7 @@ void Dispatcher::loop() { bool Dispatcher::tryParsePacket(Packet* pkt, const uint8_t* raw, int len) { int i = 0; + pkt->tx_cr = 0; pkt->header = raw[i++]; if (pkt->getPayloadVer() > PAYLOAD_VER_1) { MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): unsupported packet version", getLogDateTime()); @@ -268,7 +284,10 @@ void Dispatcher::processRecvPacket(Packet* pkt) { uint8_t priority = (action >> 24) - 1; uint32_t _delay = action & 0xFFFFFF; - _mgr->queueOutbound(pkt, priority, futureMillis(_delay)); + if (!queueOutboundPacket(pkt, priority, _delay)) { + onSendFail(pkt); + releasePacket(pkt); + } } } @@ -319,18 +338,43 @@ void Dispatcher::checkSend() { if (len + outbound->payload_len > MAX_TRANS_UNIT) { MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): FATAL: Invalid packet queued... too long, len=%d", getLogDateTime(), len + outbound->payload_len); - _mgr->free(outbound); + onSendFail(outbound); + releasePacket(outbound); outbound = NULL; } else { memcpy(&raw[len], outbound->payload, outbound->payload_len); len += outbound->payload_len; uint32_t max_airtime = _radio->getEstAirtimeFor(len)*3/2; + outbound_restore_cr = 0; + outbound_restore_preamble_len = 0; + uint8_t default_cr = getDefaultTxCodingRate(); + if (outbound->tx_cr >= 4 && outbound->tx_cr <= 8 && default_cr >= 4 && default_cr <= 8 + && outbound->tx_cr != default_cr) { + if (_radio->setCodingRate(outbound->tx_cr)) { + outbound_restore_cr = default_cr; + max_airtime = _radio->getEstAirtimeFor(len)*3/2; + } else { + MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): WARN: failed to set packet CR%d", getLogDateTime(), (uint32_t)outbound->tx_cr); + } + } + bool has_direct_path = outbound->getPathHashCount() > 0 + || (outbound->getPayloadType() == PAYLOAD_TYPE_TRACE && outbound->payload_len > 9); + if (outbound->isRouteDirect() && has_direct_path + && (outbound->tx_cr == 4 || outbound->tx_cr == 5)) { + uint16_t default_preamble_len = _radio->getDefaultPreambleLength(); + if (default_preamble_len > 16 && _radio->setPreambleLength(16)) { + outbound_restore_preamble_len = default_preamble_len; + max_airtime = _radio->getEstAirtimeFor(len)*3/2; + } + } outbound_start = _ms->getMillis(); bool success = _radio->startSendRaw(raw, len); if (!success) { MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): ERROR: send start failed!", getLogDateTime()); + restoreOutboundTxOverrides(); logTxFail(outbound, outbound->getRawLength()); + onSendFail(outbound); releasePacket(outbound); // return to pool outbound = NULL; @@ -360,6 +404,7 @@ Packet* Dispatcher::obtainNewPacket() { } else { pkt->payload_len = pkt->path_len = 0; pkt->_snr = 0; + pkt->tx_cr = 0; } return pkt; } @@ -368,13 +413,21 @@ void Dispatcher::releasePacket(Packet* packet) { _mgr->free(packet); } -void Dispatcher::sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis) { +bool Dispatcher::queueOutboundPacket(Packet* packet, uint8_t priority, uint32_t delay_millis) { if (!Packet::isValidPathLen(packet->path_len) || packet->payload_len > MAX_PACKET_PAYLOAD) { MESH_DEBUG_PRINTLN("%s Dispatcher::sendPacket(): ERROR: invalid packet... path_len=%d, payload_len=%d", getLogDateTime(), (uint32_t) packet->path_len, (uint32_t) packet->payload_len); - _mgr->free(packet); - } else { - _mgr->queueOutbound(packet, priority, futureMillis(delay_millis)); + return false; + } + return _mgr->queueOutbound(packet, priority, futureMillis(delay_millis)); +} + +bool Dispatcher::sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis) { + if (!queueOutboundPacket(packet, priority, delay_millis)) { + onSendFail(packet); + releasePacket(packet); + return false; } + return true; } // Utility function -- handles the case where millis() wraps around back to zero @@ -387,4 +440,4 @@ unsigned long Dispatcher::futureMillis(int millis_from_now) const { return _ms->getMillis() + millis_from_now; } -} \ No newline at end of file +} diff --git a/src/Dispatcher.h b/src/Dispatcher.h index aad6cba3ec..cb86bb4b7e 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -46,6 +46,14 @@ class Radio { */ virtual bool startSendRaw(const uint8_t* bytes, int len) = 0; + /** + * \brief Sets LoRa coding rate for subsequent transmits/receives. + * \returns true if the radio accepted the coding rate. + */ + virtual bool setCodingRate(uint8_t cr) { return false; } + virtual uint16_t getDefaultPreambleLength() const { return 0; } + virtual bool setPreambleLength(uint16_t len) { return false; } + /** * \returns true if the previous 'startSendRaw()' completed successfully. */ @@ -89,7 +97,7 @@ class PacketManager { virtual Packet* allocNew() = 0; virtual void free(Packet* packet) = 0; - virtual void queueOutbound(Packet* packet, uint8_t priority, uint32_t scheduled_for) = 0; + virtual bool queueOutbound(Packet* packet, uint8_t priority, uint32_t scheduled_for) = 0; virtual Packet* getNextOutbound(uint32_t now) = 0; // by priority virtual int getOutboundCount(uint32_t now) const = 0; virtual int getOutboundTotal() const = 0; @@ -118,6 +126,8 @@ typedef uint32_t DispatcherAction; class Dispatcher { Packet* outbound; // current outbound packet unsigned long outbound_expiry, outbound_start, total_air_time, rx_air_time; + uint16_t outbound_restore_preamble_len; + uint8_t outbound_restore_cr; unsigned long next_tx_time; unsigned long cad_busy_start; unsigned long radio_nonrx_start; @@ -130,6 +140,7 @@ class Dispatcher { unsigned long duty_cycle_window_ms; void processRecvPacket(Packet* pkt); + void restoreOutboundTxOverrides(); void updateTxBudget(); protected: @@ -142,6 +153,8 @@ class Dispatcher { : _radio(&radio), _ms(&ms), _mgr(&mgr) { outbound = NULL; + outbound_restore_preamble_len = 0; + outbound_restore_cr = 0; total_air_time = rx_air_time = 0; next_tx_time = ms.getMillis(); cad_busy_start = 0; @@ -161,16 +174,21 @@ class Dispatcher { virtual void logRx(Packet* packet, int len, float score) { } // hooks for custom logging virtual void logTx(Packet* packet, int len) { } virtual void logTxFail(Packet* packet, int len) { } + virtual void onSendComplete(Packet* packet) { } + virtual void onSendFail(Packet* packet) { } virtual const char* getLogDateTime() { return ""; } virtual float getAirtimeBudgetFactor() const; virtual int calcRxDelay(float score, uint32_t air_time) const; virtual uint32_t getCADFailRetryDelay() const; virtual uint32_t getCADFailMaxDuration() const; + virtual uint8_t getDefaultTxCodingRate() const { return 0; } virtual int getInterferenceThreshold() const { return 0; } // disabled by default virtual bool getCADEnabled() const { return false; } // hardware CAD disabled by default virtual int getAGCResetInterval() const { return 0; } // disabled by default virtual unsigned long getDutyCycleWindowMs() const { return 3600000; } + const Packet* getOutboundInFlight() const { return outbound; } + bool queueOutboundPacket(Packet* packet, uint8_t priority, uint32_t delay_millis); public: void begin(); @@ -178,7 +196,7 @@ class Dispatcher { Packet* obtainNewPacket(); void releasePacket(Packet* packet); - void sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis=0); + bool sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis=0); unsigned long getTotalAirTime() const { return total_air_time; } unsigned long getReceiveAirTime() const {return rx_air_time; } @@ -196,9 +214,8 @@ class Dispatcher { bool millisHasNowPassed(unsigned long timestamp) const; unsigned long futureMillis(int millis_from_now) const; - bool tryParsePacket(Packet* pkt, const uint8_t* raw, int len); - private: + bool tryParsePacket(Packet* pkt, const uint8_t* raw, int len); void checkRecv(); void checkSend(); }; diff --git a/src/Mesh.cpp b/src/Mesh.cpp index e9b92262ce..a4b2750dfa 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -3,12 +3,83 @@ namespace mesh { +static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_DEFAULT = 15; +static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX = 15; + +static uint8_t decodeTraceHashSize(uint8_t flags, uint8_t route_bytes) { + uint8_t code = flags & 0x03; + uint8_t size_pow2 = (uint8_t)(1U << code); // legacy TRACE interpretation + uint8_t size_linear = (uint8_t)(code + 1U); // packed-size interpretation (1..4) + + bool pow2_ok = size_pow2 > 0 && (route_bytes % size_pow2) == 0; + bool linear_ok = size_linear > 0 && (route_bytes % size_linear) == 0; + + if (pow2_ok && !linear_ok) { + return size_pow2; + } + if (linear_ok && !pow2_ok) { + return size_linear; + } + if (pow2_ok) { + return size_pow2; + } + return size_linear; +} + void Mesh::begin() { + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + _direct_retries[i].packet = NULL; + _direct_retries[i].trigger_packet = NULL; + _direct_retries[i].retry_started_at = 0; + _direct_retries[i].echo_wait_started_at = 0; + _direct_retries[i].retry_at = 0; + _direct_retries[i].retry_delay = 0; + _direct_retries[i].retry_attempts_sent = 0; + _direct_retries[i].next_hop_hash_len = 0; + _direct_retries[i].priority = 0; + _direct_retries[i].progress_marker = 0; + _direct_retries[i].expect_path_growth = false; + _direct_retries[i].waiting_final_echo = false; + _direct_retries[i].queued = false; + _direct_retries[i].active = false; + } Dispatcher::begin(); } void Mesh::loop() { Dispatcher::loop(); + + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + if (!_direct_retries[i].active) { + continue; + } + + if (_direct_retries[i].waiting_final_echo) { + if (!millisHasNowPassed(_direct_retries[i].retry_at)) { + continue; + } + + uint32_t elapsed_millis = _direct_retries[i].retry_started_at == 0 + ? 0 + : (uint32_t)(_ms->getMillis() - _direct_retries[i].retry_started_at); + onDirectRetryEvent("failed_all_tries", _direct_retries[i].packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); + onDirectRetryEvent("failure", _direct_retries[i].packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); + onDirectRetryFailed(_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len); + clearDirectRetrySlot(i); + continue; + } + + if (!_direct_retries[i].queued || !millisHasNowPassed(_direct_retries[i].retry_at)) { + continue; + } + + if (!isDirectRetryQueued(_direct_retries[i].packet)) { + if (_direct_retries[i].packet == getOutboundInFlight()) { + continue; // currently transmitting; keep slot until onSendComplete/onSendFail emits event + } + clearDirectRetrySlot(i); + } + } } bool Mesh::allowPacketForward(const mesh::Packet* packet) { @@ -22,10 +93,33 @@ uint32_t Mesh::getRetransmitDelay(const mesh::Packet* packet) { uint32_t Mesh::getDirectRetransmitDelay(const Packet* packet) { return 0; // by default, no delay } +bool Mesh::allowDirectRetry(const Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const { + return false; +} +uint32_t Mesh::getDirectRetryEchoDelay(const Packet* packet) const { + // Keep the base fallback aligned with the repeater's minimum retry wait. + return 200; +} +uint8_t Mesh::getDirectRetryMaxAttempts(const Packet* packet) const { + return DIRECT_RETRY_MAX_ATTEMPTS_DEFAULT; +} +uint32_t Mesh::getDirectRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx) { + uint32_t base = getDirectRetryEchoDelay(packet); + // Keep the historical linear spacing while allowing the base wait to vary by platform/profile. + return base + ((uint32_t)attempt_idx * 100UL); +} uint8_t Mesh::getExtraAckTransmitCount() const { return 0; } +void Mesh::onSendComplete(Packet* packet) { + armDirectRetryOnSendComplete(packet); +} + +void Mesh::onSendFail(Packet* packet) { + clearPendingDirectRetryOnSendFail(packet); +} + uint32_t Mesh::getCADFailRetryDelay() const { return _rng->nextInt(1, 4)*120; } @@ -39,6 +133,10 @@ int Mesh::searchChannelsByHash(const uint8_t* hash, GroupChannel channels[], int } DispatcherAction Mesh::onRecvPacket(Packet* pkt) { + if (pkt->isRouteDirect()) { + cancelDirectRetryOnEcho(pkt); + } + if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_TRACE) { if (pkt->path_len < MAX_PATH_SIZE) { uint8_t i = 0; @@ -47,19 +145,21 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { uint32_t auth_code; memcpy(&auth_code, &pkt->payload[i], 4); i += 4; uint8_t flags = pkt->payload[i++]; - uint8_t path_sz = flags & 0x03; // NEW v1.11+: lower 2 bits is path hash size - uint8_t len = pkt->payload_len - i; + uint8_t hash_size = decodeTraceHashSize(flags, len); // path_len*entry_size can exceed 255 (path_len up to 63, entry_size up to 8); // a uint8_t offset would wrap and steer the isHashMatch() read to the wrong place. - uint16_t offset = (uint16_t)pkt->path_len << path_sz; + uint16_t offset = (uint16_t)pkt->path_len * (uint16_t)hash_size; if (offset >= len) { // TRACE has reached end of given path onTraceRecv(pkt, trace_tag, auth_code, flags, pkt->path, &pkt->payload[i], len); - } else if (self_id.isHashMatch(&pkt->payload[i + offset], 1 << path_sz) && allowPacketForward(pkt) && !_tables->hasSeen(pkt)) { + } else if (hash_size > 0 && offset + hash_size <= len + && self_id.isHashMatch(&pkt->payload[i + offset], hash_size) + && allowPacketForward(pkt) && !_tables->hasSeen(pkt)) { // append SNR (Not hash!) pkt->path[pkt->path_len++] = (int8_t) (pkt->getSNR()*4); uint32_t d = getDirectRetransmitDelay(pkt); + maybeScheduleDirectRetry(pkt, 5); return ACTION_RETRANSMIT_DELAYED(5, d); // schedule with priority 5 (for now), maybe make configurable? } } @@ -85,25 +185,30 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { } } - if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) && allowPacketForward(pkt)) { - if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) { - return forwardMultipartDirect(pkt); - } else if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { - if (!_tables->hasSeen(pkt)) { // don't retransmit! - removeSelfFromPath(pkt); - routeDirectRecvAcks(pkt, 0); + if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize())) { + if (allowPacketForward(pkt)) { + if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) { + return forwardMultipartDirect(pkt); + } else if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { + if (!_tables->hasSeen(pkt)) { // don't retransmit! + removePathPrefix(pkt, 1); + routeDirectRecvAcks(pkt, 0); + } + return ACTION_RELEASE; } - return ACTION_RELEASE; - } - if (!_tables->hasSeen(pkt)) { - removeSelfFromPath(pkt); + if (!_tables->hasSeen(pkt)) { + removePathPrefix(pkt, 1); - uint32_t d = getDirectRetransmitDelay(pkt); - return ACTION_RETRANSMIT_DELAYED(0, d); // Routed traffic is HIGHEST priority + uint32_t d = getDirectRetransmitDelay(pkt); + maybeScheduleDirectRetry(pkt, 0); + return ACTION_RETRANSMIT_DELAYED(0, d); // Routed traffic is HIGHEST priority + } } } - return ACTION_RELEASE; // this node is NOT the next hop (OR this packet has already been forwarded), so discard. + if (pkt->getPathHashCount() > 0) { + return ACTION_RELEASE; // this node is NOT the next hop (OR this packet has already been forwarded), so discard. + } } if (pkt->isRouteFlood() && filterRecvFloodPacket(pkt)) return ACTION_RELEASE; @@ -321,13 +426,16 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { return action; } -void Mesh::removeSelfFromPath(Packet* pkt) { - // remove our hash from 'path' - pkt->setPathHashCount(pkt->getPathHashCount() - 1); // decrement the count +void Mesh::removePathPrefix(Packet* pkt, uint8_t prefix_count) { + uint8_t hash_count = pkt->getPathHashCount(); + if (prefix_count == 0 || hash_count == 0) return; + if (prefix_count > hash_count) prefix_count = hash_count; + pkt->setPathHashCount(hash_count - prefix_count); uint8_t sz = pkt->getPathHashSize(); - for (int k = 0; k < pkt->getPathHashCount()*sz; k += sz) { // shuffle path by 1 'entry' - memcpy(&pkt->path[k], &pkt->path[k + sz], sz); + uint8_t prefix_bytes = prefix_count * sz; + for (int k = 0; k < pkt->getPathHashCount()*sz; k += sz) { + memmove(&pkt->path[k], &pkt->path[k + prefix_bytes], sz); } } @@ -358,7 +466,7 @@ DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt) { memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len); if (!_tables->hasSeen(&tmp)) { // don't retransmit! - removeSelfFromPath(&tmp); + removePathPrefix(&tmp, 1); routeDirectRecvAcks(&tmp, ((uint32_t)remaining + 1) * 300); // expect multipart ACKs 300ms apart (x2) } } @@ -375,6 +483,7 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) { a1->path_len = Packet::copyPath(a1->path, packet->path, packet->path_len); a1->header &= ~PH_ROUTE_MASK; a1->header |= ROUTE_TYPE_DIRECT; + maybeScheduleDirectRetry(a1, 0); sendPacket(a1, 0, delay_millis); } extra--; @@ -385,11 +494,337 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) { a2->path_len = Packet::copyPath(a2->path, packet->path, packet->path_len); a2->header &= ~PH_ROUTE_MASK; a2->header |= ROUTE_TYPE_DIRECT; + maybeScheduleDirectRetry(a2, 0); sendPacket(a2, 0, delay_millis); } } } +void Mesh::clearDirectRetrySlot(int idx) { + _direct_retries[idx].packet = NULL; + _direct_retries[idx].trigger_packet = NULL; + _direct_retries[idx].retry_started_at = 0; + _direct_retries[idx].echo_wait_started_at = 0; + _direct_retries[idx].retry_at = 0; + _direct_retries[idx].retry_delay = 0; + _direct_retries[idx].retry_attempts_sent = 0; + memset(_direct_retries[idx].next_hop_hash, 0, sizeof(_direct_retries[idx].next_hop_hash)); + _direct_retries[idx].next_hop_hash_len = 0; + _direct_retries[idx].priority = 0; + _direct_retries[idx].progress_marker = 0; + _direct_retries[idx].expect_path_growth = false; + _direct_retries[idx].waiting_final_echo = false; + _direct_retries[idx].queued = false; + _direct_retries[idx].active = false; +} + +bool Mesh::isDirectRetryQueued(const Packet* packet) const { + for (int i = 0; i < _mgr->getOutboundTotal(); i++) { + if (_mgr->getOutboundByIdx(i) == packet) { + return true; + } + } + return false; +} + +void Mesh::calculateDirectRetryKey(const Packet* packet, uint8_t* dest_key) const { + uint8_t type = packet->getPayloadType(); + Utils::sha256(dest_key, MAX_HASH_SIZE, &type, 1, packet->payload, packet->payload_len); +} + +bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) { + uint8_t recv_key[MAX_HASH_SIZE]; + calculateDirectRetryKey(packet, recv_key); + + bool cleared = false; + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + if (!_direct_retries[i].active || memcmp(recv_key, _direct_retries[i].retry_key, MAX_HASH_SIZE) != 0) { + continue; + } + + bool is_echo = _direct_retries[i].expect_path_growth + ? packet->path_len > _direct_retries[i].progress_marker + : packet->getPathHashCount() < _direct_retries[i].progress_marker; + if (!is_echo) { + continue; + } + + int8_t echo_snr_x4 = packet->_snr; + onDirectRetrySucceeded(_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len, echo_snr_x4); + if (_direct_retries[i].queued || _direct_retries[i].waiting_final_echo) { + if (_direct_retries[i].packet != NULL) { + // Success quality comes from the received downstream echo, not the original upstream RX. + _direct_retries[i].packet->_snr = echo_snr_x4; + } + uint32_t echo_millis = _direct_retries[i].echo_wait_started_at == 0 + ? 0 + : (uint32_t)(_ms->getMillis() - _direct_retries[i].echo_wait_started_at); + uint8_t retry_attempt = _direct_retries[i].waiting_final_echo + ? _direct_retries[i].retry_attempts_sent + : _direct_retries[i].retry_attempts_sent + 1; + onDirectRetryEvent("good", _direct_retries[i].packet, echo_millis, retry_attempt); + if (_direct_retries[i].queued) { + for (int j = 0; j < _mgr->getOutboundTotal(); j++) { + if (_mgr->getOutboundByIdx(j) == _direct_retries[i].packet) { + Packet* pending = _mgr->removeOutboundByIdx(j); + if (pending) { + releasePacket(pending); + } + break; + } + } + } + clearDirectRetrySlot(i); + } else { + if (_direct_retries[i].trigger_packet != NULL) { + _direct_retries[i].trigger_packet->_snr = echo_snr_x4; + } + uint32_t echo_millis = _direct_retries[i].echo_wait_started_at == 0 + ? 0 + : (uint32_t)(_ms->getMillis() - _direct_retries[i].echo_wait_started_at); + onDirectRetryEvent("good", _direct_retries[i].trigger_packet, echo_millis, _direct_retries[i].retry_attempts_sent + 1); + clearDirectRetrySlot(i); + } + cleared = true; + } + + return cleared; +} + +void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + if (!_direct_retries[i].active) { + continue; + } + + if (_direct_retries[i].queued) { + if (_direct_retries[i].packet == packet) { + // The retry packet itself just finished transmitting; Dispatcher will release it after this hook. + uint32_t elapsed_millis = _direct_retries[i].retry_started_at == 0 + ? 0 + : (uint32_t)(_ms->getMillis() - _direct_retries[i].retry_started_at); + onDirectRetryEvent("resent", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent + 1); + _direct_retries[i].echo_wait_started_at = _ms->getMillis(); + _direct_retries[i].retry_attempts_sent++; + uint8_t max_attempts = getDirectRetryMaxAttempts(packet); + if (max_attempts < 1) { + max_attempts = 1; + } else if (max_attempts > DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX) { + max_attempts = DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX; + } + if (_direct_retries[i].retry_attempts_sent >= max_attempts) { + // Dispatcher releases the retry packet after this hook. Keep only retry metadata + // for the final echo window so pool exhaustion cannot force a premature failure. + _direct_retries[i].packet = NULL; + _direct_retries[i].retry_at = futureMillis(_direct_retries[i].retry_delay); + _direct_retries[i].waiting_final_echo = true; + _direct_retries[i].queued = false; + continue; + } + + Packet* retry = obtainNewPacket(); + if (retry == NULL) { + onDirectRetryEvent("dropped_no_packet", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent + 1); + onDirectRetryEvent("failure", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent + 1); + clearDirectRetrySlot(i); + continue; + } + + *retry = *packet; + retry->tx_cr = 0; + uint8_t retry_attempt = _direct_retries[i].retry_attempts_sent + 1; + configureDirectRetryPacket(retry, packet, retry_attempt); + uint32_t retry_delay = getDirectRetryAttemptDelay(packet, _direct_retries[i].retry_attempts_sent); + if (queueOutboundPacket(retry, _direct_retries[i].priority, retry_delay)) { + _direct_retries[i].packet = retry; + _direct_retries[i].retry_delay = retry_delay; + _direct_retries[i].retry_at = futureMillis(retry_delay); + _direct_retries[i].waiting_final_echo = false; + onDirectRetryEvent("queued", retry, retry_delay, retry_attempt); + } else { + onDirectRetryEvent("dropped_queue_full", retry, retry_delay, retry_attempt); + onDirectRetryEvent("failure", retry, elapsed_millis, retry_attempt); + releasePacket(retry); + clearDirectRetrySlot(i); + } + } + continue; + } + + if (_direct_retries[i].trigger_packet != packet) { + continue; + } + + // Allocate the retry packet only after TX-complete so busy repeaters do not reserve pool slots early. + Packet* retry = obtainNewPacket(); + if (retry == NULL) { + onDirectRetryEvent("dropped_no_packet", packet, _direct_retries[i].retry_delay, 1); + onDirectRetryEvent("failure", packet, 0, 1); + clearDirectRetrySlot(i); + continue; + } + + *retry = *packet; + retry->tx_cr = 0; + configureDirectRetryPacket(retry, packet, 1); + + // Start the echo wait only after the initial direct transmission actually completed. + if (queueOutboundPacket(retry, _direct_retries[i].priority, _direct_retries[i].retry_delay)) { + unsigned long now = _ms->getMillis(); + _direct_retries[i].packet = retry; + _direct_retries[i].trigger_packet = NULL; + _direct_retries[i].queued = true; + _direct_retries[i].waiting_final_echo = false; + _direct_retries[i].retry_at = futureMillis(_direct_retries[i].retry_delay); + _direct_retries[i].retry_started_at = now; + _direct_retries[i].echo_wait_started_at = now; + onDirectRetryEvent("queued", retry, _direct_retries[i].retry_delay, 1); + } else { + onDirectRetryEvent("dropped_queue_full", retry, _direct_retries[i].retry_delay, 1); + onDirectRetryEvent("failure", retry, 0, 1); + releasePacket(retry); + clearDirectRetrySlot(i); + } + } +} + +void Mesh::clearPendingDirectRetryOnSendFail(const Packet* packet) { + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + if (!_direct_retries[i].active) { + continue; + } + + if (_direct_retries[i].queued) { + if (_direct_retries[i].packet == packet) { + // The queued retry itself failed; Dispatcher will release it after this hook. + onDirectRetryEvent("dropped_send_fail", packet, 0, _direct_retries[i].retry_attempts_sent + 1); + onDirectRetryEvent("failure", packet, 0, _direct_retries[i].retry_attempts_sent + 1); + clearDirectRetrySlot(i); + } + continue; + } + + if (_direct_retries[i].trigger_packet == packet) { + onDirectRetryEvent("dropped_send_fail", packet, 0, 1); + onDirectRetryEvent("failure", packet, 0, 1); + clearDirectRetrySlot(i); + } + } +} + +bool Mesh::getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_hash, uint8_t& next_hop_hash_len, + uint8_t& progress_marker, bool& expect_path_growth) const { + switch (packet->getPayloadType()) { + case PAYLOAD_TYPE_ACK: + case PAYLOAD_TYPE_PATH: + case PAYLOAD_TYPE_REQ: + case PAYLOAD_TYPE_RESPONSE: + case PAYLOAD_TYPE_TXT_MSG: + case PAYLOAD_TYPE_ANON_REQ: + // Allow retries even when only one downstream hop remains so fixed direct paths + // (e.g. remote admin/login over 2-hop chains) use the same retry policy. + if (packet->getPathHashCount() == 0) { + return false; + } + next_hop_hash = packet->path; + next_hop_hash_len = packet->getPathHashSize(); + progress_marker = packet->getPathHashCount(); + expect_path_growth = false; + return true; + + case PAYLOAD_TYPE_MULTIPART: + if (packet->payload_len < 1 || (packet->payload[0] & 0x0F) != PAYLOAD_TYPE_ACK || packet->getPathHashCount() == 0) { + return false; + } + next_hop_hash = packet->path; + next_hop_hash_len = packet->getPathHashSize(); + progress_marker = packet->getPathHashCount(); + expect_path_growth = false; + return true; + + case PAYLOAD_TYPE_TRACE: { + if (packet->payload_len < 9) { + return false; + } + + uint8_t route_bytes = packet->payload_len - 9; + uint8_t hash_size = decodeTraceHashSize(packet->payload[8], route_bytes); + uint16_t offset = (uint16_t)packet->path_len * (uint16_t)hash_size; + if (offset + hash_size > route_bytes) { + return false; + } + if (offset + (2 * hash_size) > route_bytes) { + return false; // no downstream repeater means there will be no forward echo to overhear. + } + + next_hop_hash = &packet->payload[9 + offset]; + next_hop_hash_len = hash_size; + progress_marker = packet->path_len; + expect_path_growth = true; + return true; + } + + default: + return false; + } +} + +void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { + const uint8_t* next_hop_hash; + uint8_t next_hop_hash_len; + uint8_t progress_marker; + bool expect_path_growth; + if (!getDirectRetryTarget(packet, next_hop_hash, next_hop_hash_len, progress_marker, expect_path_growth) + || !allowDirectRetry(packet, next_hop_hash, next_hop_hash_len)) { + return; + } + + uint8_t retry_key[MAX_HASH_SIZE]; + calculateDirectRetryKey(packet, retry_key); + + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + if (_direct_retries[i].active + && memcmp(retry_key, _direct_retries[i].retry_key, MAX_HASH_SIZE) == 0 + && _direct_retries[i].progress_marker == progress_marker + && _direct_retries[i].expect_path_growth == expect_path_growth) { + return; + } + } + + int slot_idx = -1; + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + if (!_direct_retries[i].active) { + slot_idx = i; + break; + } + } + if (slot_idx < 0) { + onDirectRetryEvent("dropped_no_slot", packet, 0, 0); + onDirectRetryEvent("failure", packet, 0, 0); + return; + } + + // Only store retry metadata here; allocate the retry packet after the initial TX really completes. + uint32_t retry_delay = getDirectRetryAttemptDelay(packet, 0); + memcpy(_direct_retries[slot_idx].retry_key, retry_key, MAX_HASH_SIZE); + _direct_retries[slot_idx].packet = NULL; + _direct_retries[slot_idx].trigger_packet = const_cast(packet); + _direct_retries[slot_idx].retry_started_at = 0; + _direct_retries[slot_idx].echo_wait_started_at = 0; + _direct_retries[slot_idx].retry_at = 0; + _direct_retries[slot_idx].retry_delay = retry_delay; + _direct_retries[slot_idx].retry_attempts_sent = 0; + memset(_direct_retries[slot_idx].next_hop_hash, 0, sizeof(_direct_retries[slot_idx].next_hop_hash)); + memcpy(_direct_retries[slot_idx].next_hop_hash, next_hop_hash, next_hop_hash_len); + _direct_retries[slot_idx].next_hop_hash_len = next_hop_hash_len; + _direct_retries[slot_idx].priority = priority; + _direct_retries[slot_idx].progress_marker = progress_marker; + _direct_retries[slot_idx].expect_path_growth = expect_path_growth; + _direct_retries[slot_idx].waiting_final_echo = false; + _direct_retries[slot_idx].queued = false; + _direct_retries[slot_idx].active = true; +} + Packet* Mesh::createAdvert(const LocalIdentity& id, const uint8_t* app_data, size_t app_data_len) { if (app_data_len > MAX_ADVERT_DATA_SIZE) return NULL; @@ -546,7 +981,9 @@ Packet* Mesh::createGroupDatagram(uint8_t type, const GroupChannel& channel, con return packet; } -Packet* Mesh::createAck(const uint8_t* ack, uint8_t len) { +Packet* Mesh::createAck(const uint8_t* ack_hash, uint8_t ack_len) { + if (ack_len > sizeof(Packet::payload)) return NULL; + Packet* packet = obtainNewPacket(); if (packet == NULL) { MESH_DEBUG_PRINTLN("%s Mesh::createAck(): error, packet pool empty", getLogDateTime()); @@ -554,13 +991,19 @@ Packet* Mesh::createAck(const uint8_t* ack, uint8_t len) { } packet->header = (PAYLOAD_TYPE_ACK << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later - memcpy(packet->payload, ack, len); - packet->payload_len = len; + memcpy(packet->payload, ack_hash, ack_len); + packet->payload_len = ack_len; return packet; } -Packet* Mesh::createMultiAck(const uint8_t* ack, uint8_t len, uint8_t remaining) { +Packet* Mesh::createAck(uint32_t ack_crc) { + return createAck((const uint8_t*)&ack_crc, 4); +} + +Packet* Mesh::createMultiAck(const uint8_t* ack_hash, uint8_t ack_len, uint8_t remaining) { + if (ack_len + 1 > sizeof(Packet::payload)) return NULL; + Packet* packet = obtainNewPacket(); if (packet == NULL) { MESH_DEBUG_PRINTLN("%s Mesh::createMultiAck(): error, packet pool empty", getLogDateTime()); @@ -569,12 +1012,16 @@ Packet* Mesh::createMultiAck(const uint8_t* ack, uint8_t len, uint8_t remaining) packet->header = (PAYLOAD_TYPE_MULTIPART << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later packet->payload[0] = (remaining << 4) | PAYLOAD_TYPE_ACK; - memcpy(&packet->payload[1], ack, len); - packet->payload_len = 1 + len; + memcpy(&packet->payload[1], ack_hash, ack_len); + packet->payload_len = ack_len + 1; return packet; } +Packet* Mesh::createMultiAck(uint32_t ack_crc, uint8_t remaining) { + return createMultiAck((const uint8_t*)&ack_crc, 4, remaining); +} + Packet* Mesh::createRawData(const uint8_t* data, size_t len) { if (len > sizeof(Packet::payload)) return NULL; // invalid arg @@ -637,7 +1084,7 @@ void Mesh::sendFlood(Packet* packet, uint32_t delay_millis, uint8_t path_hash_si packet->header |= ROUTE_TYPE_FLOOD; packet->setPathHashSizeAndCount(path_hash_size, 0); - _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us + _tables->markSent(packet); // mark this packet as already sent in case it is rebroadcast back to us uint8_t pri; if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { @@ -666,7 +1113,7 @@ void Mesh::sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_m packet->transport_codes[1] = transport_codes[1]; packet->setPathHashSizeAndCount(path_hash_size, 0); - _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us + _tables->markSent(packet); // mark this packet as already sent in case it is rebroadcast back to us uint8_t pri; if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { @@ -699,7 +1146,8 @@ void Mesh::sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uin pri = 0; } } - _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us + _tables->markSent(packet); // mark this packet as already sent in case it is rebroadcast back to us + maybeScheduleDirectRetry(packet, pri); sendPacket(packet, pri, delay_millis); } @@ -709,7 +1157,7 @@ void Mesh::sendZeroHop(Packet* packet, uint32_t delay_millis) { packet->path_len = 0; // path_len of zero means Zero Hop - _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us + _tables->markSent(packet); // mark this packet as already sent in case it is rebroadcast back to us sendPacket(packet, 0, delay_millis); } @@ -722,9 +1170,9 @@ void Mesh::sendZeroHop(Packet* packet, uint16_t* transport_codes, uint32_t delay packet->path_len = 0; // path_len of zero means Zero Hop - _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us + _tables->markSent(packet); // mark this packet as already sent in case it is rebroadcast back to us sendPacket(packet, 0, delay_millis); } -} \ No newline at end of file +} diff --git a/src/Mesh.h b/src/Mesh.h index 2302d6b5b7..b342aed2c4 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -4,6 +4,10 @@ namespace mesh { +#ifndef MAX_DIRECT_RETRY_SLOTS + #define MAX_DIRECT_RETRY_SLOTS 6 +#endif + class GroupChannel { public: uint8_t hash[PATH_HASH_SIZE]; @@ -16,6 +20,7 @@ class GroupChannel { class MeshTables { public: virtual bool hasSeen(const Packet* packet) = 0; + virtual void markSent(const Packet* packet) = 0; virtual void clear(const Packet* packet) = 0; // remove this packet hash from table }; @@ -28,13 +33,45 @@ class Mesh : public Dispatcher { RNG* _rng; MeshTables* _tables; - void removeSelfFromPath(Packet* packet); + struct DirectRetryEntry { + Packet* packet; + Packet* trigger_packet; + unsigned long retry_started_at; + unsigned long echo_wait_started_at; + unsigned long retry_at; + uint32_t retry_delay; + uint8_t retry_attempts_sent; + uint8_t retry_key[MAX_HASH_SIZE]; + uint8_t next_hop_hash[MAX_HASH_SIZE]; + uint8_t next_hop_hash_len; + uint8_t priority; + uint8_t progress_marker; + bool expect_path_growth; + bool waiting_final_echo; + bool queued; + bool active; + }; + + DirectRetryEntry _direct_retries[MAX_DIRECT_RETRY_SLOTS]; + + void removePathPrefix(Packet* packet, uint8_t prefix_count); void routeDirectRecvAcks(Packet* packet, uint32_t delay_millis); + void clearDirectRetrySlot(int idx); + bool isDirectRetryQueued(const Packet* packet) const; + void calculateDirectRetryKey(const Packet* packet, uint8_t* dest_key) const; + bool cancelDirectRetryOnEcho(const Packet* packet); + void armDirectRetryOnSendComplete(const Packet* packet); + void clearPendingDirectRetryOnSendFail(const Packet* packet); + bool getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_hash, uint8_t& next_hop_hash_len, + uint8_t& progress_marker, bool& expect_path_growth) const; + void maybeScheduleDirectRetry(const Packet* packet, uint8_t priority); //void routeRecvAcks(Packet* packet, uint32_t delay_millis); DispatcherAction forwardMultipartDirect(Packet* pkt); protected: DispatcherAction onRecvPacket(Packet* pkt) override; + void onSendComplete(Packet* packet) override; + void onSendFail(Packet* packet) override; virtual uint32_t getCADFailRetryDelay() const override; @@ -65,11 +102,52 @@ class Mesh : public Dispatcher { */ virtual uint32_t getDirectRetransmitDelay(const Packet* packet); + /** + * \brief Decide whether a DIRECT packet should get one delayed retry if the next hop echo is not overheard. + * Sub-classes can use recent repeater or other link-quality data to opt in selectively. + */ + virtual bool allowDirectRetry(const Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const; + + /** + * \returns milliseconds to wait for the next-hop echo before queueing one retry of the DIRECT packet. + */ + virtual uint32_t getDirectRetryEchoDelay(const Packet* packet) const; + + /** + * \returns maximum number of retry transmissions after the initial direct TX. + */ + virtual uint8_t getDirectRetryMaxAttempts(const Packet* packet) const; + + /** + * \returns delay before a specific retry attempt, where attempt_idx=0 is the first retry. + */ + virtual uint32_t getDirectRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx); + /** * \returns number of extra (Direct) ACK transmissions wanted. */ virtual uint8_t getExtraAckTransmitCount() const; + /** + * \brief Optional hook for logging direct-retry lifecycle events. + */ + virtual void onDirectRetryEvent(const char* event, const Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) { } + + /** + * \brief Optional hook for link-quality feedback when all direct-retry attempts fail. + */ + virtual void onDirectRetryFailed(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) { } + + /** + * \brief Optional hook for link-quality feedback when a direct-retry echo is heard. + */ + virtual void onDirectRetrySucceeded(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len, int8_t snr_x4) { } + + /** + * \brief Optional hook to set local-only transmit options on a retry packet before it is queued. + */ + virtual void configureDirectRetryPacket(Packet* retry, const Packet* original, uint8_t retry_attempt) { } + /** * \brief Perform search of local DB of peers/contacts. * \returns Number of peers with matching hash @@ -185,10 +263,10 @@ class Mesh : public Dispatcher { Packet* createDatagram(uint8_t type, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t len); Packet* createAnonDatagram(uint8_t type, const LocalIdentity& sender, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t data_len); Packet* createGroupDatagram(uint8_t type, const GroupChannel& channel, const uint8_t* data, size_t data_len); - Packet* createAck(const uint8_t* ack, uint8_t len); - Packet* createAck(uint32_t ack_crc) { return createAck((uint8_t *) &ack_crc, 4); } - Packet* createMultiAck(const uint8_t* ack, uint8_t len, uint8_t remaining); - Packet* createMultiAck(uint32_t ack_crc, uint8_t remaining) { return createMultiAck((uint8_t *)&ack_crc, 4, remaining); } + Packet* createAck(const uint8_t* ack_hash, uint8_t ack_len); + Packet* createAck(uint32_t ack_crc); + Packet* createMultiAck(const uint8_t* ack_hash, uint8_t ack_len, uint8_t remaining); + Packet* createMultiAck(uint32_t ack_crc, uint8_t remaining); Packet* createPathReturn(const uint8_t* dest_hash, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len); Packet* createPathReturn(const Identity& dest, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len); Packet* createRawData(const uint8_t* data, size_t len); diff --git a/src/Packet.cpp b/src/Packet.cpp index aad3e2f48e..a542a5a27e 100644 --- a/src/Packet.cpp +++ b/src/Packet.cpp @@ -8,6 +8,7 @@ Packet::Packet() { header = 0; path_len = 0; payload_len = 0; + tx_cr = 0; } bool Packet::isValidPathLen(uint8_t path_len) { @@ -64,6 +65,7 @@ uint8_t Packet::writeTo(uint8_t dest[]) const { bool Packet::readFrom(const uint8_t src[], uint8_t len) { uint8_t i = 0; + tx_cr = 0; header = src[i++]; if (hasTransportCodes()) { memcpy(&transport_codes[0], &src[i], 2); i += 2; @@ -84,4 +86,5 @@ bool Packet::readFrom(const uint8_t src[], uint8_t len) { return true; // success } -} \ No newline at end of file +} + diff --git a/src/Packet.h b/src/Packet.h index 0886a06c4e..2943c03770 100644 --- a/src/Packet.h +++ b/src/Packet.h @@ -49,6 +49,7 @@ class Packet { uint8_t path[MAX_PATH_SIZE]; uint8_t payload[MAX_PACKET_PAYLOAD]; int8_t _snr; + uint8_t tx_cr; // volatile local-only TX coding-rate override; not serialized /** * \brief calculate the hash of payload + type diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 82e5374352..4c17922353 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -9,6 +9,8 @@ #define BRIDGE_MAX_BAUD 115200 #endif +#define RECENT_REPEATER_PREFIX_MAX_BYTES 3 + // Believe it or not, this std C function is busted on some platforms! static uint32_t _atoi(const char* sp) { uint32_t n = 0; @@ -27,6 +29,122 @@ static bool isValidName(const char *n) { return true; } +static bool looksNumeric(const char* s) { + if (s == NULL) return false; + while (*s == ' ') s++; + if (*s == '-' || *s == '+') s++; + bool saw_digit = false; + while (*s) { + if (*s >= '0' && *s <= '9') { + saw_digit = true; + } else if (*s != '.') { + break; + } + s++; + } + return saw_digit; +} + +static int16_t parseSnrDbX4(const char* s) { + float db = atof(s); + return (int16_t)(db * 4.0f + (db >= 0.0f ? 0.5f : -0.5f)); +} + +static void formatSnrDbX4(char* dest, size_t dest_len, int16_t snr_x4) { + int16_t v = snr_x4; + const char* sign = ""; + if (v < 0) { + sign = "-"; + v = -v; + } + snprintf(dest, dest_len, "%s%d.%02d", sign, v / 4, (v % 4) * 25); +} + +static const char* retryPresetName(uint8_t preset) { + switch (preset) { + case RETRY_PRESET_INFRA: return "infra"; + case RETRY_PRESET_ROOFTOP: return "rooftop"; + case RETRY_PRESET_MOBILE: return "mobile"; + default: return "custom"; + } +} + +static void markDirectRetryPrefsValid(NodePrefs* prefs) { + prefs->direct_retry_prefs_magic[0] = DIRECT_RETRY_PREFS_MAGIC_0; + prefs->direct_retry_prefs_magic[1] = DIRECT_RETRY_PREFS_MAGIC_1; +} + +static void applyDirectRetryPreset(NodePrefs* prefs, uint8_t preset) { + prefs->retry_preset = preset; + if (preset == RETRY_PRESET_INFRA) { + prefs->direct_retry_attempts = DIRECT_RETRY_INFRA_COUNT; + prefs->direct_retry_base_ms = DIRECT_RETRY_INFRA_BASE_MS; + prefs->direct_retry_step_ms = DIRECT_RETRY_INFRA_STEP_MS; + prefs->direct_retry_snr_margin_x4 = DIRECT_RETRY_INFRA_MARGIN_X4; + } else if (preset == RETRY_PRESET_MOBILE) { + prefs->direct_retry_attempts = DIRECT_RETRY_MOBILE_COUNT; + prefs->direct_retry_base_ms = DIRECT_RETRY_MOBILE_BASE_MS; + prefs->direct_retry_step_ms = DIRECT_RETRY_MOBILE_STEP_MS; + prefs->direct_retry_snr_margin_x4 = DIRECT_RETRY_MOBILE_MARGIN_X4; + } else { + prefs->retry_preset = RETRY_PRESET_ROOFTOP; + prefs->direct_retry_attempts = DIRECT_RETRY_ROOFTOP_COUNT; + prefs->direct_retry_base_ms = DIRECT_RETRY_ROOFTOP_BASE_MS; + prefs->direct_retry_step_ms = DIRECT_RETRY_ROOFTOP_STEP_MS; + prefs->direct_retry_snr_margin_x4 = DIRECT_RETRY_ROOFTOP_MARGIN_X4; + } + markDirectRetryPrefsValid(prefs); +} + +static void setDefaultDirectRetryPrefs(NodePrefs* prefs) { + applyDirectRetryPreset(prefs, RETRY_PRESET_ROOFTOP); + prefs->direct_retry_cr_enabled = 1; + prefs->direct_retry_cr4_snr_x4 = DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT; + prefs->direct_retry_cr5_snr_x4 = DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT; + prefs->direct_retry_cr7_snr_x4 = DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT; + prefs->direct_retry_cr8_snr_x4 = DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT; + prefs->direct_retry_enabled = 1; + markDirectRetryPrefsValid(prefs); +} + +static bool directRetryPrefsValid(const NodePrefs* prefs) { + return prefs->direct_retry_prefs_magic[0] == DIRECT_RETRY_PREFS_MAGIC_0 + && prefs->direct_retry_prefs_magic[1] == DIRECT_RETRY_PREFS_MAGIC_1; +} + +static bool parseRetryPreset(const char* s, uint8_t& preset) { + if (strcmp(s, "infra") == 0 || strcmp(s, "0") == 0) { + preset = RETRY_PRESET_INFRA; + return true; + } + if (strcmp(s, "rooftop") == 0 || strcmp(s, "1") == 0) { + preset = RETRY_PRESET_ROOFTOP; + return true; + } + if (strcmp(s, "mobile") == 0 || strcmp(s, "2") == 0) { + preset = RETRY_PRESET_MOBILE; + return true; + } + return false; +} + +static bool parseHashPrefix(const char* text, uint8_t* prefix, uint8_t& prefix_len) { + size_t hex_len = strlen(text); + if (hex_len == 0 || (hex_len & 1) || hex_len > RECENT_REPEATER_PREFIX_MAX_BYTES * 2) { + return false; + } + prefix_len = hex_len / 2; + return mesh::Utils::fromHex(prefix, prefix_len, text); +} + +static void formatSnrDbX4Short(char* dest, size_t dest_len, int16_t snr_x4) { + formatSnrDbX4(dest, dest_len, snr_x4); + size_t len = strlen(dest); + if (len > 3 && dest[len - 1] == '0') { + dest[len - 1] = 0; + } +} + void CommonCLI::loadPrefs(FILESYSTEM* fs) { if (fs->exists("/com_prefs")) { loadPrefsInt(fs, "/com_prefs"); // new filename @@ -93,7 +211,19 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292 file.read((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 293 file.read((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294 - // next: 295 + file.read((uint8_t *)&_prefs->retry_preset, sizeof(_prefs->retry_preset)); // 295 + file.read((uint8_t *)&_prefs->direct_retry_attempts, sizeof(_prefs->direct_retry_attempts)); // 296 + file.read((uint8_t *)&_prefs->direct_retry_base_ms, sizeof(_prefs->direct_retry_base_ms)); // 297 + file.read((uint8_t *)&_prefs->direct_retry_step_ms, sizeof(_prefs->direct_retry_step_ms)); // 299 + file.read((uint8_t *)&_prefs->direct_retry_snr_margin_x4, sizeof(_prefs->direct_retry_snr_margin_x4)); // 301 + file.read((uint8_t *)&_prefs->direct_retry_cr4_snr_x4, sizeof(_prefs->direct_retry_cr4_snr_x4)); // 303 + file.read((uint8_t *)&_prefs->direct_retry_cr5_snr_x4, sizeof(_prefs->direct_retry_cr5_snr_x4)); // 304 + file.read((uint8_t *)&_prefs->direct_retry_cr7_snr_x4, sizeof(_prefs->direct_retry_cr7_snr_x4)); // 305 + file.read((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 306 + file.read((uint8_t *)&_prefs->direct_retry_enabled, sizeof(_prefs->direct_retry_enabled)); // 307 + file.read((uint8_t *)&_prefs->direct_retry_cr_enabled, sizeof(_prefs->direct_retry_cr_enabled)); // 308 + file.read((uint8_t *)&_prefs->direct_retry_prefs_magic, sizeof(_prefs->direct_retry_prefs_magic)); // 309 + // next: 311 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -125,6 +255,18 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { _prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean _prefs->radio_fem_rxgain = constrain(_prefs->radio_fem_rxgain, 0, 1); // boolean _prefs->cad_enabled = constrain(_prefs->cad_enabled, 0, 1); // boolean + if (!directRetryPrefsValid(_prefs)) { + setDefaultDirectRetryPrefs(_prefs); + } + if (_prefs->retry_preset > RETRY_PRESET_MOBILE && _prefs->retry_preset != RETRY_PRESET_CUSTOM) { + _prefs->retry_preset = RETRY_PRESET_CUSTOM; + } + _prefs->direct_retry_attempts = constrain(_prefs->direct_retry_attempts, 1, 15); + _prefs->direct_retry_base_ms = constrain(_prefs->direct_retry_base_ms, 10, 5000); + _prefs->direct_retry_step_ms = constrain(_prefs->direct_retry_step_ms, 0, 5000); + _prefs->direct_retry_snr_margin_x4 = constrain(_prefs->direct_retry_snr_margin_x4, 0, 160); + _prefs->direct_retry_enabled = constrain(_prefs->direct_retry_enabled, 0, 1); + _prefs->direct_retry_cr_enabled = constrain(_prefs->direct_retry_cr_enabled, 0, 1); file.close(); } @@ -190,7 +332,20 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292 file.write((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 293 file.write((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294 - // next: 295 + markDirectRetryPrefsValid(_prefs); + file.write((uint8_t *)&_prefs->retry_preset, sizeof(_prefs->retry_preset)); // 295 + file.write((uint8_t *)&_prefs->direct_retry_attempts, sizeof(_prefs->direct_retry_attempts)); // 296 + file.write((uint8_t *)&_prefs->direct_retry_base_ms, sizeof(_prefs->direct_retry_base_ms)); // 297 + file.write((uint8_t *)&_prefs->direct_retry_step_ms, sizeof(_prefs->direct_retry_step_ms)); // 299 + file.write((uint8_t *)&_prefs->direct_retry_snr_margin_x4, sizeof(_prefs->direct_retry_snr_margin_x4)); // 301 + file.write((uint8_t *)&_prefs->direct_retry_cr4_snr_x4, sizeof(_prefs->direct_retry_cr4_snr_x4)); // 303 + file.write((uint8_t *)&_prefs->direct_retry_cr5_snr_x4, sizeof(_prefs->direct_retry_cr5_snr_x4)); // 304 + file.write((uint8_t *)&_prefs->direct_retry_cr7_snr_x4, sizeof(_prefs->direct_retry_cr7_snr_x4)); // 305 + file.write((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 306 + file.write((uint8_t *)&_prefs->direct_retry_enabled, sizeof(_prefs->direct_retry_enabled)); // 307 + file.write((uint8_t *)&_prefs->direct_retry_cr_enabled, sizeof(_prefs->direct_retry_cr_enabled)); // 308 + file.write((uint8_t *)&_prefs->direct_retry_prefs_magic, sizeof(_prefs->direct_retry_prefs_magic)); // 309 + // next: 311 file.close(); } @@ -301,6 +456,9 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else if (memcmp(command, "clear stats", 11) == 0) { _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"); } else if (memcmp(command, "get ", 4) == 0) { handleGetCmd(sender_timestamp, command, reply); } else if (memcmp(command, "set ", 4) == 0) { @@ -680,6 +838,124 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else { strcpy(reply, "Error, must be 0-2"); } + } else if (memcmp(config, "retry.preset ", 13) == 0) { + uint8_t preset; + if (parseRetryPreset(&config[13], preset)) { + applyDirectRetryPreset(_prefs, preset); + savePrefs(); + sprintf(reply, "OK - %s", retryPresetName(_prefs->retry_preset)); + } else { + strcpy(reply, "Error, must be infra, rooftop, or mobile"); + } + } else if (memcmp(config, "direct.retry ", 13) == 0) { + if (memcmp(&config[13], "on", 2) == 0) { + _prefs->direct_retry_enabled = 1; + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(&config[13], "off", 3) == 0) { + _prefs->direct_retry_enabled = 0; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be on or off"); + } + } else if (memcmp(config, "direct.retry.margin ", 20) == 0) { + if (!looksNumeric(&config[20])) { + strcpy(reply, "Error, must be 0-40 dB"); + } else { + int16_t margin_x4 = parseSnrDbX4(&config[20]); + if (margin_x4 >= 0 && margin_x4 <= 160) { + _prefs->direct_retry_snr_margin_x4 = (uint16_t)margin_x4; + _prefs->retry_preset = RETRY_PRESET_CUSTOM; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be 0-40 dB"); + } + } + } else if (memcmp(config, "direct.retry.count ", 19) == 0) { + int attempts = _atoi(&config[19]); + if (attempts >= 1 && attempts <= 15) { + _prefs->direct_retry_attempts = (uint8_t)attempts; + _prefs->retry_preset = RETRY_PRESET_CUSTOM; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be 1-15"); + } + } else if (memcmp(config, "direct.retry.base ", 18) == 0) { + int base_ms = _atoi(&config[18]); + if (base_ms >= 10 && base_ms <= 5000) { + _prefs->direct_retry_base_ms = (uint16_t)base_ms; + _prefs->retry_preset = RETRY_PRESET_CUSTOM; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be 10-5000 ms"); + } + } else if (memcmp(config, "direct.retry.step ", 18) == 0) { + int step_ms = _atoi(&config[18]); + if (step_ms >= 0 && step_ms <= 5000) { + _prefs->direct_retry_step_ms = (uint16_t)step_ms; + _prefs->retry_preset = RETRY_PRESET_CUSTOM; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be 0-5000 ms"); + } + } else if (memcmp(config, "direct.retry.cr ", 16) == 0) { + if (memcmp(&config[16], "off", 3) == 0) { + _prefs->direct_retry_cr_enabled = 0; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(tmp, &config[16]); + const char *parts[4]; + int num = mesh::Utils::parseTextParts(tmp, parts, 4, ','); + if (num == 4 && looksNumeric(parts[0]) && looksNumeric(parts[1]) && looksNumeric(parts[2]) && looksNumeric(parts[3])) { + int16_t cr4 = parseSnrDbX4(parts[0]); + int16_t cr5 = parseSnrDbX4(parts[1]); + int16_t cr7 = parseSnrDbX4(parts[2]); + int16_t cr8 = parseSnrDbX4(parts[3]); + if (cr4 >= -128 && cr4 <= 127 && cr5 >= -128 && cr5 <= 127 && cr7 >= -128 && cr7 <= 127 && cr8 >= -128 && cr8 <= 127) { + _prefs->direct_retry_cr4_snr_x4 = (int8_t)cr4; + _prefs->direct_retry_cr5_snr_x4 = (int8_t)cr5; + _prefs->direct_retry_cr7_snr_x4 = (int8_t)cr7; + _prefs->direct_retry_cr8_snr_x4 = (int8_t)cr8; + _prefs->direct_retry_cr_enabled = 1; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, SNR must fit -32.00..31.75 dB"); + } + } else { + strcpy(reply, "Error, use CR4,CR5,CR7,CR8 SNRs or off"); + } + } + } else if (memcmp(config, "recent.repeater ", 16) == 0) { + strcpy(tmp, &config[16]); + const char *parts[2]; + int num = mesh::Utils::parseTextParts(tmp, parts, 2, ' '); + uint8_t prefix[MAX_HASH_SIZE]; + uint8_t prefix_len = 0; + int16_t snr_x4 = 12; // default to +3.0 dB when omitted or invalid + if (num > 1 && looksNumeric(parts[1])) { + snr_x4 = parseSnrDbX4(parts[1]); + } + if (num < 1 || !parseHashPrefix(parts[0], prefix, prefix_len)) { + strcpy(reply, "Error, use: set recent.repeater [snr_db]"); + } else if (snr_x4 < -128 || snr_x4 > 127) { + strcpy(reply, "Error, SNR must fit -32.00..31.75 dB"); + } else if (_callbacks->setRecentRepeater(prefix, prefix_len, (int8_t)snr_x4)) { + char prefix_hex[RECENT_REPEATER_PREFIX_MAX_BYTES * 2 + 1]; + char snr[12]; + mesh::Utils::toHex(prefix_hex, prefix, prefix_len); + prefix_hex[prefix_len * 2] = 0; + formatSnrDbX4Short(snr, sizeof(snr), snr_x4); + sprintf(reply, "OK - set %s at %s SNR", prefix_hex, snr); + } else { + strcpy(reply, "Error, table rejected prefix"); + } } else if (memcmp(config, "owner.info ", 11) == 0) { config += 11; char *dp = _prefs->owner_info; @@ -862,6 +1138,38 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep sprintf(reply, "> %d", (uint32_t)_prefs->flood_max); } else if (memcmp(config, "direct.txdelay", 14) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor)); + } else if (memcmp(config, "retry.preset", 12) == 0) { + sprintf(reply, "> %s", retryPresetName(_prefs->retry_preset)); + } else if (memcmp(config, "direct.retry", 12) == 0 && (config[12] == 0 || config[12] == ' ')) { + sprintf(reply, "> %s", _prefs->direct_retry_enabled ? "on" : "off"); + } else if (memcmp(config, "direct.retry.margin", 19) == 0) { + char margin[12]; + formatSnrDbX4(margin, sizeof(margin), _prefs->direct_retry_snr_margin_x4); + sprintf(reply, "> %s", margin); + } else if (memcmp(config, "direct.retry.count", 18) == 0) { + sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_attempts); + } else if (memcmp(config, "direct.retry.base", 17) == 0) { + sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_base_ms); + } else if (memcmp(config, "direct.retry.step", 17) == 0) { + sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_step_ms); + } else if (memcmp(config, "direct.retry.cr", 15) == 0) { + if (!_prefs->direct_retry_cr_enabled) { + strcpy(reply, "> off"); + } else { + char cr4[12], cr5[12], cr7[12], cr8[12]; + formatSnrDbX4(cr4, sizeof(cr4), _prefs->direct_retry_cr4_snr_x4); + formatSnrDbX4(cr5, sizeof(cr5), _prefs->direct_retry_cr5_snr_x4); + formatSnrDbX4(cr7, sizeof(cr7), _prefs->direct_retry_cr7_snr_x4); + formatSnrDbX4(cr8, sizeof(cr8), _prefs->direct_retry_cr8_snr_x4); + sprintf(reply, "> %s,%s,%s,%s", cr4, cr5, cr7, cr8); + } + } else if (memcmp(config, "recent.repeater", 15) == 0) { + int page = 1; + const char* cursor = &config[15]; + while (*cursor == ' ') cursor++; + if (*cursor) page = _atoi(cursor); + if (page < 1) page = 1; + _callbacks->formatRecentRepeatersReply(reply, page); } else if (memcmp(config, "owner.info", 10) == 0) { auto start = reply; *reply++ = '>'; diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 10cb00c776..76a7cf4623 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -19,6 +19,34 @@ #define LOOP_DETECT_MODERATE 2 #define LOOP_DETECT_STRICT 3 +#define RETRY_PRESET_INFRA 0 +#define RETRY_PRESET_ROOFTOP 1 +#define RETRY_PRESET_MOBILE 2 +#define RETRY_PRESET_CUSTOM 0xFF + +#define DIRECT_RETRY_INFRA_BASE_MS 275 +#define DIRECT_RETRY_INFRA_COUNT 4 +#define DIRECT_RETRY_INFRA_STEP_MS 150 +#define DIRECT_RETRY_INFRA_MARGIN_X4 60 + +#define DIRECT_RETRY_ROOFTOP_BASE_MS 175 +#define DIRECT_RETRY_ROOFTOP_COUNT 15 +#define DIRECT_RETRY_ROOFTOP_STEP_MS 100 +#define DIRECT_RETRY_ROOFTOP_MARGIN_X4 20 + +#define DIRECT_RETRY_MOBILE_BASE_MS 175 +#define DIRECT_RETRY_MOBILE_COUNT 15 +#define DIRECT_RETRY_MOBILE_STEP_MS 50 +#define DIRECT_RETRY_MOBILE_MARGIN_X4 0 + +#define DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT 40 +#define DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT 30 +#define DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT 10 +#define DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT 10 + +#define DIRECT_RETRY_PREFS_MAGIC_0 0xD1 +#define DIRECT_RETRY_PREFS_MAGIC_1 0x52 + struct NodePrefs { // persisted to file float airtime_factor; char node_name[32]; @@ -65,6 +93,18 @@ struct NodePrefs { // persisted to file uint8_t path_hash_mode; // which path mode to use when sending uint8_t loop_detect; uint8_t cad_enabled; // hardware Channel Activity Detection before TX (boolean) + uint8_t retry_preset; + uint8_t direct_retry_attempts; + uint16_t direct_retry_base_ms; + uint16_t direct_retry_step_ms; + uint16_t direct_retry_snr_margin_x4; + int8_t direct_retry_cr4_snr_x4; + int8_t direct_retry_cr5_snr_x4; + int8_t direct_retry_cr7_snr_x4; + int8_t direct_retry_cr8_snr_x4; + uint8_t direct_retry_enabled; + uint8_t direct_retry_cr_enabled; + uint8_t direct_retry_prefs_magic[2]; }; class CommonCLICallbacks { @@ -88,6 +128,18 @@ class CommonCLICallbacks { virtual void formatStatsReply(char *reply) = 0; virtual void formatRadioStatsReply(char *reply) = 0; virtual void formatPacketStatsReply(char *reply) = 0; + virtual void formatRecentRepeatersReply(char *reply, int page) { + (void)page; + if (reply != NULL) reply[0] = 0; + } + virtual bool setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4) { + (void)prefix; + (void)prefix_len; + (void)snr_x4; + return false; + } + virtual void clearRecentRepeaters() { + } virtual mesh::LocalIdentity& getSelfId() = 0; virtual void saveIdentity(const mesh::LocalIdentity& new_id) = 0; virtual void clearStats() = 0; diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index 0b79cfb422..f1d5273361 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -1,29 +1,153 @@ #pragma once #include +#if ARDUINO + #include +#endif #ifdef ESP32 #include #endif #define MAX_PACKET_HASHES (128+32) +#ifndef MAX_RECENT_REPEATERS + // Platform defaults. Can be overridden with -D MAX_RECENT_REPEATERS=. + #if defined(ESP32) || defined(ESP32_PLATFORM) + #define MAX_RECENT_REPEATERS 2048 + #elif defined(NRF52_PLATFORM) + #define MAX_RECENT_REPEATERS 512 + #else + #define MAX_RECENT_REPEATERS 64 + #endif +#endif +#define MAX_ROUTE_HASH_BYTES 3 class SimpleMeshTables : public mesh::MeshTables { +public: + typedef bool (*RecentRepeaterAllowFn)(const uint8_t* prefix, uint8_t prefix_len, void* ctx); + + struct RecentRepeaterInfo { + // Identity and link quality for a next-hop path prefix. + uint8_t prefix[MAX_ROUTE_HASH_BYTES]; + uint8_t prefix_len; + int8_t snr_x4; + uint8_t snr_locked; + uint32_t last_heard_millis; + }; + +private: uint8_t _hashes[MAX_PACKET_HASHES*MAX_HASH_SIZE]; int _next_idx; uint32_t _direct_dups, _flood_dups; + RecentRepeaterInfo _recent_repeaters[MAX_RECENT_REPEATERS]; + int _next_recent_repeater_idx; + int8_t _recent_repeater_min_snr_x4; + RecentRepeaterAllowFn _recent_repeater_allow_fn; + void* _recent_repeater_allow_ctx; + + bool hasSeenHash(const uint8_t* hash) const { + const uint8_t* sp = _hashes; + for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) { + if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) { + return true; + } + } + return false; + } + + void storeHash(const uint8_t* hash) { + memcpy(&_hashes[_next_idx*MAX_HASH_SIZE], hash, MAX_HASH_SIZE); + _next_idx = (_next_idx + 1) % MAX_PACKET_HASHES; + } + + bool prefixesOverlap(const uint8_t* a, uint8_t a_len, const uint8_t* b, uint8_t b_len) const { + uint8_t n = a_len < b_len ? a_len : b_len; + return n > 0 && memcmp(a, b, n) == 0; + } + + int8_t weightedSnrX4RoundUp(int8_t curr_snr_x4, int8_t new_snr_x4) const { + // Keep existing SNR heavier than a single new sample: 75% existing + 25% new. + int32_t weighted_sum = ((int32_t)curr_snr_x4 * 3) + (int32_t)new_snr_x4; + int32_t blended = weighted_sum / 4; // truncates toward zero + // "Round up" means ceil(), which only differs from truncation for positive remainders. + if (weighted_sum > 0 && (weighted_sum % 4) != 0) { + blended++; + } + if (blended > 127) { + blended = 127; + } else if (blended < -128) { + blended = -128; + } + return (int8_t)blended; + } + + bool extractRecentRepeater(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const { + // Learn repeater prefixes only from packet shapes that expose a trustworthy repeater ID. + // For flood traffic, the last path entry is the repeater we directly heard. + if (packet->isRouteFlood() && packet->getPathHashCount() > 0) { + prefix_len = packet->getPathHashSize(); + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + + const uint8_t* last_hop = &packet->path[(packet->getPathHashCount() - 1) * packet->getPathHashSize()]; + memcpy(prefix, last_hop, prefix_len); + return true; + } + + // If there is no flood path to inspect, fall back to payload-derived identities. + if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT && packet->payload_len >= PUB_KEY_SIZE) { + memcpy(prefix, packet->payload, MAX_ROUTE_HASH_BYTES); + prefix_len = MAX_ROUTE_HASH_BYTES; + return true; + } + + if (packet->getPayloadType() == PAYLOAD_TYPE_CONTROL + && packet->isRouteDirect() + && packet->getPathHashCount() == 0 + && packet->payload_len >= 6 + MAX_ROUTE_HASH_BYTES + && (packet->payload[0] & 0xF0) == 0x90) { + memcpy(prefix, &packet->payload[6], MAX_ROUTE_HASH_BYTES); + prefix_len = MAX_ROUTE_HASH_BYTES; + return true; + } + + return false; + } + + void recordRecentRepeater(const mesh::Packet* packet) { + uint8_t prefix[MAX_ROUTE_HASH_BYTES] = {0}; + uint8_t prefix_len = 0; + if (!extractRecentRepeater(packet, prefix, prefix_len) || prefix_len == 0) { + return; + } + if (packet->_snr < _recent_repeater_min_snr_x4) { + return; + } + setRecentRepeater(prefix, prefix_len, packet->_snr); + } public: SimpleMeshTables() { memset(_hashes, 0, sizeof(_hashes)); _next_idx = 0; _direct_dups = _flood_dups = 0; + memset(_recent_repeaters, 0, sizeof(_recent_repeaters)); + _next_recent_repeater_idx = 0; + _recent_repeater_min_snr_x4 = -128; + _recent_repeater_allow_fn = NULL; + _recent_repeater_allow_ctx = NULL; } #ifdef ESP32 void restoreFrom(File f) { f.read(_hashes, sizeof(_hashes)); f.read((uint8_t *) &_next_idx, sizeof(_next_idx)); + // Recent repeater entries are intentionally not restored across boots. + // This avoids struct-layout migration issues and keeps stale path quality + // stats from persisting indefinitely. + memset(_recent_repeaters, 0, sizeof(_recent_repeaters)); + _next_recent_repeater_idx = 0; } void saveTo(File f) { f.write(_hashes, sizeof(_hashes)); @@ -35,23 +159,29 @@ class SimpleMeshTables : public mesh::MeshTables { uint8_t hash[MAX_HASH_SIZE]; packet->calculatePacketHash(hash); - const uint8_t* sp = _hashes; - for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) { - if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) { - if (packet->isRouteDirect()) { - _direct_dups++; // keep some stats - } else { - _flood_dups++; - } - return true; + if (hasSeenHash(hash)) { + if (packet->isRouteDirect()) { + _direct_dups++; // keep some stats + } else { + _flood_dups++; } + return true; } - memcpy(&_hashes[_next_idx*MAX_HASH_SIZE], hash, MAX_HASH_SIZE); - _next_idx = (_next_idx + 1) % MAX_PACKET_HASHES; // cyclic table + storeHash(hash); + recordRecentRepeater(packet); return false; } + void markSent(const mesh::Packet* packet) override { + // Outbound packets must be marked as already-sent without teaching the recent-heard cache about ourselves. + uint8_t hash[MAX_HASH_SIZE]; + packet->calculatePacketHash(hash); + if (!hasSeenHash(hash)) { + storeHash(hash); + } + } + void clear(const mesh::Packet* packet) override { uint8_t hash[MAX_HASH_SIZE]; packet->calculatePacketHash(hash); @@ -68,5 +198,193 @@ class SimpleMeshTables : public mesh::MeshTables { uint32_t getNumDirectDups() const { return _direct_dups; } uint32_t getNumFloodDups() const { return _flood_dups; } + void setRecentRepeaterMinSNRX4(int8_t min_snr_x4) { + _recent_repeater_min_snr_x4 = min_snr_x4; + } + void setRecentRepeaterAllowFilter(RecentRepeaterAllowFn fn, void* ctx) { + _recent_repeater_allow_fn = fn; + _recent_repeater_allow_ctx = ctx; + } + bool setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4, bool snr_locked = false, + bool bypass_allow_filter = false) { + if (prefix == NULL || prefix_len == 0) { + return false; + } + + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + + if (!bypass_allow_filter && _recent_repeater_allow_fn != NULL + && !_recent_repeater_allow_fn(prefix, prefix_len, _recent_repeater_allow_ctx)) { + return false; + } + + // Keep exact prefixes distinct so a 1-byte path prefix does not collapse + // independent 2/3-byte repeaters that share the same first byte. + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + RecentRepeaterInfo& existing = _recent_repeaters[i]; + if (existing.prefix_len != prefix_len || memcmp(existing.prefix, prefix, prefix_len) != 0) { + continue; + } + if (snr_locked) { + existing.snr_x4 = snr_x4; + existing.snr_locked = 1; + } else if (!existing.snr_locked) { + existing.snr_x4 = weightedSnrX4RoundUp(existing.snr_x4, snr_x4); + } +#if ARDUINO + existing.last_heard_millis = millis(); +#else + existing.last_heard_millis = 0; +#endif + return true; + } + + int slot_idx = -1; + // Prefer empty slots first while preserving newest-order iteration. + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + int idx = (_next_recent_repeater_idx + i) % MAX_RECENT_REPEATERS; + if (_recent_repeaters[idx].prefix_len == 0) { + slot_idx = idx; + break; + } + } + if (slot_idx < 0) { + // Table is full: evict the weakest observed SNR entry. + slot_idx = 0; + int8_t min_snr_x4 = _recent_repeaters[0].snr_x4; + for (int i = 1; i < MAX_RECENT_REPEATERS; i++) { + if (_recent_repeaters[i].snr_x4 < min_snr_x4) { + min_snr_x4 = _recent_repeaters[i].snr_x4; + slot_idx = i; + } + } + } + + RecentRepeaterInfo& slot = _recent_repeaters[slot_idx]; + memset(slot.prefix, 0, sizeof(slot.prefix)); + memcpy(slot.prefix, prefix, prefix_len); + slot.prefix_len = prefix_len; + slot.snr_x4 = snr_x4; + slot.snr_locked = snr_locked ? 1 : 0; +#if ARDUINO + slot.last_heard_millis = millis(); +#else + slot.last_heard_millis = 0; +#endif + _next_recent_repeater_idx = (slot_idx + 1) % MAX_RECENT_REPEATERS; + return true; + } + bool decrementRecentRepeaterSnrX4(const uint8_t* prefix, uint8_t prefix_len, uint8_t amount_x4 = 1) { + if (prefix == NULL || prefix_len == 0 || amount_x4 == 0) { + return false; + } + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + RecentRepeaterInfo& existing = _recent_repeaters[i]; + if (existing.prefix_len != prefix_len || memcmp(existing.prefix, prefix, prefix_len) != 0) { + continue; + } + if (!existing.snr_locked) { + int16_t lowered = (int16_t)existing.snr_x4 - (int16_t)amount_x4; + if (lowered < -128) { + lowered = -128; + } + existing.snr_x4 = (int8_t)lowered; + } + return true; + } + return false; + } + const RecentRepeaterInfo* getLatestRecentRepeater() const { + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + int idx = (_next_recent_repeater_idx - 1 - i + MAX_RECENT_REPEATERS) % MAX_RECENT_REPEATERS; + const RecentRepeaterInfo* info = &_recent_repeaters[idx]; + if (info->prefix_len > 0) { + return info; + } + } + return NULL; + } + int getRecentRepeaterCount() const { + int count = 0; + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + if (_recent_repeaters[i].prefix_len > 0) { + count++; + } + } + return count; + } + const RecentRepeaterInfo* getRecentRepeaterNewestByIdx(int idx_wanted) const { + if (idx_wanted < 0) { + return NULL; + } + int idx_seen = 0; + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + int idx = (_next_recent_repeater_idx - 1 - i + MAX_RECENT_REPEATERS) % MAX_RECENT_REPEATERS; + const RecentRepeaterInfo* info = &_recent_repeaters[idx]; + if (info->prefix_len == 0) { + continue; + } + if (idx_seen == idx_wanted) { + return info; + } + idx_seen++; + } + return NULL; + } + const RecentRepeaterInfo* getRecentRepeaterOldestByIdx(int idx_wanted) const { + if (idx_wanted < 0) { + return NULL; + } + int idx_seen = 0; + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + int idx = (_next_recent_repeater_idx + i) % MAX_RECENT_REPEATERS; + const RecentRepeaterInfo* info = &_recent_repeaters[idx]; + if (info->prefix_len == 0) { + continue; + } + if (idx_seen == idx_wanted) { + return info; + } + idx_seen++; + } + return NULL; + } + + const RecentRepeaterInfo* findRecentRepeaterByHash(const uint8_t* hash, uint8_t hash_len) const { + if (hash == NULL || hash_len == 0) { + return NULL; + } + + // Prefer exact matches. If none exists, fall back to the newest longest + // overlapping prefix so coarse learned prefixes can still inform CR. + const RecentRepeaterInfo* best = NULL; + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + int idx = (_next_recent_repeater_idx - 1 - i + MAX_RECENT_REPEATERS) % MAX_RECENT_REPEATERS; + const RecentRepeaterInfo* info = &_recent_repeaters[idx]; + if (info->prefix_len == 0) { + continue; + } + if (info->prefix_len == hash_len && memcmp(info->prefix, hash, hash_len) == 0) { + return info; + } + if (prefixesOverlap(info->prefix, info->prefix_len, hash, hash_len)) { + if (best == NULL || info->prefix_len > best->prefix_len) { + best = info; + } + } + } + return best; + } + void clearRecentRepeaters() { + memset(_recent_repeaters, 0, sizeof(_recent_repeaters)); + _next_recent_repeater_idx = 0; + } + void resetStats() { _direct_dups = _flood_dups = 0; } }; diff --git a/src/helpers/StaticPoolPacketManager.cpp b/src/helpers/StaticPoolPacketManager.cpp index b8926df0cc..fc2bb059fa 100644 --- a/src/helpers/StaticPoolPacketManager.cpp +++ b/src/helpers/StaticPoolPacketManager.cpp @@ -83,11 +83,12 @@ void StaticPoolPacketManager::free(mesh::Packet* packet) { unused.add(packet, 0, 0); } -void StaticPoolPacketManager::queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) { +bool StaticPoolPacketManager::queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) { if (!send_queue.add(packet, priority, scheduled_for)) { MESH_DEBUG_PRINTLN("queueOutbound: send queue full, dropping packet"); - free(packet); + return false; } + return true; } mesh::Packet* StaticPoolPacketManager::getNextOutbound(uint32_t now) { diff --git a/src/helpers/StaticPoolPacketManager.h b/src/helpers/StaticPoolPacketManager.h index 59715b4e01..350e85d29d 100644 --- a/src/helpers/StaticPoolPacketManager.h +++ b/src/helpers/StaticPoolPacketManager.h @@ -26,7 +26,7 @@ class StaticPoolPacketManager : public mesh::PacketManager { mesh::Packet* allocNew() override; void free(mesh::Packet* packet) override; - void queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) override; + bool queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) override; mesh::Packet* getNextOutbound(uint32_t now) override; int getOutboundCount(uint32_t now) const override; int getOutboundTotal() const override; @@ -35,4 +35,4 @@ class StaticPoolPacketManager : public mesh::PacketManager { mesh::Packet* removeOutboundByIdx(int i) override; void queueInbound(mesh::Packet* packet, uint32_t scheduled_for) override; mesh::Packet* getNextInbound(uint32_t now) override; -}; \ No newline at end of file +}; diff --git a/src/helpers/radiolib/CustomLLCC68Wrapper.h b/src/helpers/radiolib/CustomLLCC68Wrapper.h index 8861f76d24..620ef8640b 100644 --- a/src/helpers/radiolib/CustomLLCC68Wrapper.h +++ b/src/helpers/radiolib/CustomLLCC68Wrapper.h @@ -16,6 +16,10 @@ class CustomLLCC68Wrapper : public RadioLibWrapper { updatePreamble(sf); } + bool setCodingRate(uint8_t cr) override { + return ((CustomLLCC68 *)_radio)->setCodingRate(cr) == RADIOLIB_ERR_NONE; + } + bool isReceivingPacket() override { return ((CustomLLCC68 *)_radio)->isReceiving(); } diff --git a/src/helpers/radiolib/CustomLR1110Wrapper.h b/src/helpers/radiolib/CustomLR1110Wrapper.h index 13efd25b57..4d30f515ed 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -16,6 +16,10 @@ class CustomLR1110Wrapper : public RadioLibWrapper { updatePreamble(sf); } + bool setCodingRate(uint8_t cr) override { + return ((CustomLR1110 *)_radio)->setCodingRate(cr) == RADIOLIB_ERR_NONE; + } + void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio, ((CustomLR1110 *)_radio)->getFreqMHz()); } bool isReceivingPacket() override { return ((CustomLR1110 *)_radio)->isReceiving(); diff --git a/src/helpers/radiolib/CustomSTM32WLxWrapper.h b/src/helpers/radiolib/CustomSTM32WLxWrapper.h index 97bf6820d6..45e275a3fd 100644 --- a/src/helpers/radiolib/CustomSTM32WLxWrapper.h +++ b/src/helpers/radiolib/CustomSTM32WLxWrapper.h @@ -17,6 +17,10 @@ class CustomSTM32WLxWrapper : public RadioLibWrapper { updatePreamble(sf); } + bool setCodingRate(uint8_t cr) override { + return ((CustomSTM32WLx *)_radio)->setCodingRate(cr) == RADIOLIB_ERR_NONE; + } + bool isReceivingPacket() override { return ((CustomSTM32WLx *)_radio)->isReceiving(); } diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index cc7bb2238b..66f7adbfba 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -20,6 +20,10 @@ class CustomSX1262Wrapper : public RadioLibWrapper { updatePreamble(sf); } + bool setCodingRate(uint8_t cr) override { + return ((CustomSX1262 *)_radio)->setCodingRate(cr) == RADIOLIB_ERR_NONE; + } + bool isReceivingPacket() override { return ((CustomSX1262 *)_radio)->isReceiving(); } diff --git a/src/helpers/radiolib/CustomSX1268Wrapper.h b/src/helpers/radiolib/CustomSX1268Wrapper.h index 9ddea78f3f..b87e1cd645 100644 --- a/src/helpers/radiolib/CustomSX1268Wrapper.h +++ b/src/helpers/radiolib/CustomSX1268Wrapper.h @@ -20,6 +20,10 @@ class CustomSX1268Wrapper : public RadioLibWrapper { updatePreamble(sf); } + bool setCodingRate(uint8_t cr) override { + return ((CustomSX1268 *)_radio)->setCodingRate(cr) == RADIOLIB_ERR_NONE; + } + bool isReceivingPacket() override { return ((CustomSX1268 *)_radio)->isReceiving(); } diff --git a/src/helpers/radiolib/CustomSX1276Wrapper.h b/src/helpers/radiolib/CustomSX1276Wrapper.h index 9d75ce12a1..cb073f91bc 100644 --- a/src/helpers/radiolib/CustomSX1276Wrapper.h +++ b/src/helpers/radiolib/CustomSX1276Wrapper.h @@ -19,6 +19,10 @@ class CustomSX1276Wrapper : public RadioLibWrapper { updatePreamble(sf); } + bool setCodingRate(uint8_t cr) override { + return ((CustomSX1276 *)_radio)->setCodingRate(cr) == RADIOLIB_ERR_NONE; + } + bool isReceivingPacket() override { return ((CustomSX1276 *)_radio)->isReceiving(); } diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 9943bcab77..0774023421 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -40,6 +40,8 @@ class RadioLibWrapper : public mesh::Radio { } virtual void setParams(float freq, float bw, uint8_t sf, uint8_t cr) = 0; + uint16_t getDefaultPreambleLength() const override { return preambleLengthForSF(_preamble_sf); } + bool setPreambleLength(uint16_t len) override { return _radio->setPreambleLength(len) == RADIOLIB_ERR_NONE; } uint32_t getRngSeed(); void setTxPower(int8_t dbm);