diff --git a/docs/cli_commands.md b/docs/cli_commands.md index b6c87a7f..9b4723e3 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -118,24 +118,20 @@ This document provides an overview of CLI commands that can be sent to MeshCore ### Get or set recent repeater fallback prefix/SNR **Usage:** - `get recent.repeater` -- `get recent.repeater all` -- `get recent.repeater all ` -- `get recent.repeater first ` -- `get recent.repeater first ` -- `get recent.repeater last ` -- `get recent.repeater last ` -- `set recent.repeater ` +- `get recent.repeater ` +- `get recent.repeater page ` +- `set recent.repeater ` **Parameters:** -- `prefix_hex`: 1-3 bytes of next-hop prefix (hex) +- `prefix_hex_6`: Exactly 3 bytes of next-hop prefix in hex (6 chars) - `snr_db`: SNR in dB (supports decimals; stored at x4 precision) -- `count`: number of entries to print -- `offset`: zero-based row offset into the selected order +- `page`: 1-based page number **Notes:** - `set` is rejected when the prefix already exists in neighbors. -- `all` prints oldest to newest; `first` prints the oldest N; `last` prints the newest N. -- Over LoRa remote CLI, replies are packet-size limited; use `offset` to page through all rows. +- Rows are shown newest-first. +- Serial CLI prints all rows (no paging). +- Over LoRa remote CLI, page size is fixed at `4` rows; choose page with `get recent.repeater `. --- @@ -536,7 +532,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `state`: `on`|`off` -**Default:** `off` +**Default:** `on` **Note:** When enabled, a repeater can use recently-heard non-duplicate repeater prefixes as a fallback for direct retry eligibility when no suitable neighbor entry is available. @@ -548,9 +544,9 @@ This document provides an overview of CLI commands that can be sent to MeshCore - `set direct.retry.margin ` **Parameters:** -- `value`: Margin in dB above the SF-specific receive floor (minimum `0`, default `5`) +- `value`: Margin in dB above the SF-specific receive floor (minimum `0`, maximum `40`, quarter-dB precision, default `2.5`) -**Default:** `5` +**Default:** `2.5` **Note:** The retry gate uses the active SF floor of `SF5=-2.5`, `SF6=-5`, `SF7=-7.5`, `SF8=-10`, `SF9=-12.5`, `SF10=-15`, `SF11=-17.5`, `SF12=-20`, then adds this margin. @@ -564,7 +560,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `value`: Retry attempts after initial TX (`1`-`15`) -**Default:** `3` +**Default:** `15` --- diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 907ce2c8..29b126fc 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -86,6 +86,64 @@ bool MyMesh::allowRecentRepeaterPrefixStore(const uint8_t* prefix, uint8_t prefi return self->findNeighbourByHash(prefix, prefix_len) == NULL; } +static void formatRecentRepeaterPrefix(const SimpleMeshTables::RecentRepeaterInfo* info, char* out, size_t out_len) { + if (out == NULL || out_len == 0) { + return; + } + out[0] = 0; + if (info == NULL) { + return; + } + + uint8_t prefix_len = info->prefix_len; + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + if (prefix_len > 0) { + mesh::Utils::toHex(out, info->prefix, prefix_len); + } + + size_t used = strlen(out); + const size_t target_len = MAX_ROUTE_HASH_BYTES * 2; + while (used < target_len && used + 1 < out_len) { + out[used++] = ' '; + } + out[used] = 0; +} + +static void formatRecentRepeaterSnrX4(int8_t snr_x4, char* out, size_t out_len) { + if (out == NULL || out_len == 0) { + return; + } + + const char* snr_text = StrHelper::ftoa(((float)snr_x4) / 4.0f); + if (snr_text[0] == '-') { + snprintf(out, out_len, "%s", snr_text); + } else { + snprintf(out, out_len, " %s", snr_text); + } +} + +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 MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float snr) { #if MAX_NEIGHBOURS // check if neighbours enabled // find existing neighbour, else use least recently updated @@ -456,6 +514,30 @@ void MyMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, ui bool MyMesh::allowPacketForward(const mesh::Packet *packet) { if (_prefs.disable_fwd) return false; + + if (packet->isRouteDirect() && packet->getPayloadType() == PAYLOAD_TYPE_TRACE && packet->payload_len >= 9) { + auto* tables = (SimpleMeshTables *)getTables(); + 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; + uint8_t sf = constrain(active_sf, (uint8_t)5, (uint8_t)12); + int16_t fallback_snr_x4 = direct_retry_floor_x4[sf - 5] + 40; // fixed +10 dB above SF floor + + // A successful TRACE forward reveals the downstream next-hop hash. Seed/update the recent table immediately. + if (hash_size > 0 && offset + (2U * hash_size) <= route_bytes) { + uint8_t prefix_len = hash_size; + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + const uint8_t* next_hop_prefix = &packet->payload[9 + offset + hash_size]; + const auto* existing = tables->findRecentRepeaterByHash(next_hop_prefix, prefix_len); + // This point only proves we can forward TO next_hop; packet->_snr is upstream RX and not a + // trustworthy metric for next_hop. Seed with existing table value or fallback only. + int8_t trace_snr_x4 = (existing != NULL) ? existing->snr_x4 : (int8_t)constrain(fallback_snr_x4, -128, 127); + tables->setRecentRepeater(next_hop_prefix, prefix_len, trace_snr_x4, false, true); + } + } + if (packet->isRouteFlood() && packet->getPathHashCount() >= _prefs.flood_max) return false; if (packet->isRouteFlood() && recv_pkt_region == NULL) { MESH_DEBUG_PRINTLN("allowPacketForward: unknown transport code, or wildcard not allowed for FLOOD packet"); @@ -564,23 +646,102 @@ void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, u return; } - MESH_DEBUG_PRINTLN("%s direct retry %s (type=%d, route=%s, payload_len=%d, delay=%lu)", + uint8_t prefix[MAX_ROUTE_HASH_BYTES] = {0}; + uint8_t prefix_len = 0; + bool has_prefix = extractDirectRetryPrefix(packet, prefix, prefix_len); + auto* tables = (SimpleMeshTables *)getTables(); + const auto* existing = has_prefix ? tables->findRecentRepeaterByHash(prefix, prefix_len) : NULL; + char next_hop_hex[(MAX_ROUTE_HASH_BYTES * 2) + 1] = {0}; + if (has_prefix && prefix_len > 0) { + mesh::Utils::toHex(next_hop_hex, prefix, prefix_len); + } + const char* next_hop = (has_prefix && prefix_len > 0) ? next_hop_hex : "unknown"; + // Direct-retry events are TX-side and usually have no trustworthy RX SNR. + // Cap event SNR at fixed SF floor + 10 dB so trace-start retries can't inflate table SNR. + uint8_t sf = constrain(active_sf, (uint8_t)5, (uint8_t)12); + int16_t fallback_snr_x4_raw = direct_retry_floor_x4[sf - 5] + 40; + int8_t fallback_snr_x4 = (int8_t)constrain(fallback_snr_x4_raw, -128, 127); + bool is_success_event = (strcmp(event, "good") == 0 || strcmp(event, "canceled_echo") == 0); + int8_t retry_event_snr_x4; + const char* snr_src; + if (is_success_event && packet->_snr != 0) { + // On success, Mesh.cpp injects echo RX SNR for TRACE retries. + retry_event_snr_x4 = packet->_snr; + snr_src = "packet"; + } else if (existing != NULL) { + retry_event_snr_x4 = existing->snr_x4; + snr_src = "table"; + } else { + retry_event_snr_x4 = fallback_snr_x4; + snr_src = "fallback"; + } + char snr_used_text[12]; + char snr_pkt_text[12]; + char snr_table_text[12]; + snprintf(snr_used_text, sizeof(snr_used_text), "%s", StrHelper::ftoa(((float)retry_event_snr_x4) / 4.0f)); + snprintf(snr_pkt_text, sizeof(snr_pkt_text), "%s", StrHelper::ftoa(((float)packet->_snr) / 4.0f)); + if (existing != NULL) { + snprintf(snr_table_text, sizeof(snr_table_text), "%s", StrHelper::ftoa(((float)existing->snr_x4) / 4.0f)); + } else { + snprintf(snr_table_text, sizeof(snr_table_text), "na"); + } + + if (has_prefix && is_success_event) { + // Refresh SNR only on successful echo/progress events, not on queued/resent bookkeeping. + tables->setRecentRepeater(prefix, prefix_len, retry_event_snr_x4, false, true); + } + + if (strcmp(event, "resent") == 0) { + if (has_prefix) { + // Retry stats should be visible even when the prefix was never learned into recent.repeater. + tables->incrementRecentRepeaterRetryCount(prefix, prefix_len, true, retry_event_snr_x4, true); + } + } else if (strcmp(event, "failed_all_tries") == 0) { + if (has_prefix) { + // A failed_all_tries event means all retry attempts for this packet failed. + // Count failures by retry-attempts so fail% reflects failed retries, not just failed sessions. + uint8_t give_up_retries = getDirectRetryMaxAttempts(packet); + uint8_t failed_retries = give_up_retries; + if (failed_retries < 1) { + failed_retries = 1; + } + for (uint8_t i = 0; i < failed_retries; i++) { + tables->incrementRecentRepeaterFailCount(prefix, prefix_len, true, retry_event_snr_x4, true); + } + if (failed_retries >= give_up_retries && give_up_retries > 0) { + // If all configured retry attempts still fail, slightly degrade stored path quality. + tables->decrementRecentRepeaterSnrX4(prefix, prefix_len, 1); + } + } + } + + MESH_DEBUG_PRINTLN("%s direct retry %s (type=%d, route=%s, payload_len=%d, next_hop=%s, snr=%s, snr_src=%s, pkt_snr=%s, table_snr=%s, delay=%lu)", getLogDateTime(), event, (uint32_t)packet->getPayloadType(), packet->isRouteDirect() ? "D" : "F", (uint32_t)packet->payload_len, + next_hop, + snr_used_text, + snr_src, + snr_pkt_text, + snr_table_text, (unsigned long)delay_millis); if (_logging) { File f = openAppend(PACKET_LOG_FILE); if (f) { f.print(getLogDateTime()); - f.printf(": DIRECT RETRY %s (type=%d, route=%s, payload_len=%d, delay=%lu)\n", + f.printf(": DIRECT RETRY %s (type=%d, route=%s, payload_len=%d, next_hop=%s, snr=%s, snr_src=%s, pkt_snr=%s, table_snr=%s, delay=%lu)\n", event, (uint32_t)packet->getPayloadType(), packet->isRouteDirect() ? "D" : "F", (uint32_t)packet->payload_len, + next_hop, + snr_used_text, + snr_src, + snr_pkt_text, + snr_table_text, (unsigned long)delay_millis); f.close(); } @@ -603,28 +764,59 @@ uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { int8_t MyMesh::getDirectRetryMinSNRX4() const { // Use the live SF so `tempradio` changes immediately affect the retry threshold. uint8_t sf = constrain(active_sf, (uint8_t)5, (uint8_t)12); - int16_t threshold = direct_retry_floor_x4[sf - 5] + ((int16_t)_prefs.direct_retry_snr_margin_db * 4); + int16_t threshold = direct_retry_floor_x4[sf - 5] + (int16_t)_prefs.direct_retry_snr_margin_db; return (int8_t)constrain(threshold, -128, 127); } +bool MyMesh::extractDirectRetryPrefix(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const { + if (packet == NULL || prefix == NULL) { + return false; + } + + // TRACE direct routes encode repeater hashes in payload; packet->path carries SNR trail bytes. + if (packet->isRouteDirect() && packet->getPayloadType() == PAYLOAD_TYPE_TRACE && packet->payload_len >= 9) { + uint8_t route_bytes = packet->payload_len - 9; + uint8_t hash_size = decodeTraceHashSize(packet->payload[8], route_bytes); + uint8_t offset = packet->path_len * hash_size; + if (hash_size > 0 && offset + hash_size <= route_bytes) { + prefix_len = hash_size; + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + memcpy(prefix, &packet->payload[9 + offset], prefix_len); + return true; + } + } + + if (packet->isRouteDirect() && packet->getPathHashCount() > 0) { + prefix_len = packet->getPathHashSize(); + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + if (prefix_len == 0) { + return false; + } + memcpy(prefix, packet->path, prefix_len); + return true; + } + + return false; +} bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const { if (_prefs.disable_fwd) { return false; } int8_t min_snr_x4 = getDirectRetryMinSNRX4(); + if (_prefs.direct_retry_recent_enabled) { + // Prefer the 64-entry recent-prefix cache first, then fall back to neighbours. + const auto* recent = ((const SimpleMeshTables *)getTables())->findRecentRepeaterByHash(next_hop_hash, next_hop_hash_len); + if (recent != NULL && recent->snr_x4 >= min_snr_x4) { + return true; + } + } + const NeighbourInfo* neighbour = findNeighbourByHash(next_hop_hash, next_hop_hash_len); - // Prefer the explicit neighbor table first; it is the strongest signal that this hop is still reachable. - if (neighbour != NULL && neighbour->snr >= min_snr_x4) { - return true; - } - - if (!_prefs.direct_retry_recent_enabled) { - return false; - } - - // If no neighbor entry exists, fall back to the recent-heard repeater cache keyed by the same path prefix. - const auto* recent = ((const SimpleMeshTables *)getTables())->findRecentRepeaterByHash(next_hop_hash, next_hop_hash_len); - return recent != NULL && recent->snr_x4 >= min_snr_x4; + return neighbour != NULL && neighbour->snr >= min_snr_x4; } uint32_t MyMesh::getDirectRetryEchoDelay(const mesh::Packet* packet) const { uint32_t base_wait_millis = constrain((uint32_t)_prefs.direct_retry_base_ms, (uint32_t)10, (uint32_t)5000); @@ -973,9 +1165,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0; _prefs.tx_delay_factor = 0.5f; // was 0.25f _prefs.direct_tx_delay_factor = 0.3f; // was 0.2 - _prefs.direct_retry_recent_enabled = 0; - _prefs.direct_retry_snr_margin_db = 5; - _prefs.direct_retry_attempts = 3; + _prefs.direct_retry_recent_enabled = 1; + _prefs.direct_retry_snr_margin_db = 10; // 2.5 dB stored in x4 units + _prefs.direct_retry_attempts = 15; _prefs.direct_retry_base_ms = 200; StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name)); _prefs.node_lat = ADVERT_LAT; @@ -1354,9 +1546,11 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply reply[0] = 0; } else if (strncmp(command, "get recent.repeater", 19) == 0 || strncmp(command, "set recent.repeater", 19) == 0 + || strncmp(command, "clear recent.repeater", 21) == 0 || strncmp(command, "recent.repeater", 15) == 0) { bool is_get = false; bool is_set = false; + bool is_clear = false; const char* sub = command; if (strncmp(command, "get recent.repeater", 19) == 0) { @@ -1365,38 +1559,55 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } else if (strncmp(command, "set recent.repeater", 19) == 0) { is_set = true; sub = command + 19; + } else if (strncmp(command, "clear recent.repeater", 21) == 0) { + is_clear = true; + sub = command + 21; } else { sub = command + 15; // legacy command format } while (*sub == ' ') sub++; auto* tables = (SimpleMeshTables*)getTables(); + if (!is_get && !is_set && !is_clear && strncmp(sub, "clear", 5) == 0 && (sub[5] == 0 || sub[5] == ' ')) { + is_clear = true; + sub += 5; + while (*sub == ' ') sub++; + } - bool is_all = (strcmp(sub, "all") == 0 || strncmp(sub, "all ", 4) == 0); - bool is_first = (strncmp(sub, "first ", 6) == 0); - bool is_last = (strncmp(sub, "last ", 5) == 0); - - if (is_set || (!is_get && *sub != 0 && !is_all && !is_first && !is_last)) { + if (is_clear) { + if (*sub != 0) { + strcpy(reply, "Err - usage: clear recent.repeater"); + } else { + tables->clearRecentRepeaters(); + strcpy(reply, "OK"); + } + } else if (is_set) { char* params = (char*) sub; char* arg_snr = strchr(params, ' '); if (arg_snr == NULL) { - strcpy(reply, "Err - usage: set recent.repeater "); + strcpy(reply, "Err - usage: set recent.repeater "); } else { *arg_snr++ = 0; while (*arg_snr == ' ') arg_snr++; if (*arg_snr == 0) { - strcpy(reply, "Err - usage: set recent.repeater "); + strcpy(reply, "Err - usage: set recent.repeater "); } else { - int hex_len = strlen(params); - int prefix_len = hex_len / 2; uint8_t prefix[MAX_ROUTE_HASH_BYTES] = {0}; - if ((hex_len % 2) != 0 || prefix_len <= 0 || prefix_len > MAX_ROUTE_HASH_BYTES || !mesh::Utils::fromHex(prefix, prefix_len, params)) { - strcpy(reply, "Err - prefix must be 1-3 bytes hex"); + int hex_len = strlen(params); + if (hex_len != (MAX_ROUTE_HASH_BYTES * 2) || !mesh::Utils::fromHex(prefix, MAX_ROUTE_HASH_BYTES, params)) { + strcpy(reply, "Err - prefix must be exactly 3 bytes hex (6 chars)"); } else { - float snr_db = strtof(arg_snr, nullptr); + char* end_snr = NULL; + float snr_db = strtof(arg_snr, &end_snr); + while (end_snr != NULL && *end_snr == ' ') end_snr++; + if (end_snr == arg_snr || (end_snr != NULL && *end_snr != 0)) { + strcpy(reply, "Err - snr must be numeric"); + return; + } + int snr_x4 = (int)(snr_db * 4.0f + (snr_db >= 0.0f ? 0.5f : -0.5f)); snr_x4 = constrain(snr_x4, -128, 127); - if (tables->setRecentRepeater(prefix, (uint8_t)prefix_len, (int8_t)snr_x4)) { + if (tables->setRecentRepeater(prefix, MAX_ROUTE_HASH_BYTES, (int8_t)snr_x4, true)) { strcpy(reply, "OK"); } else { strcpy(reply, "Err - prefix is already in neighbors"); @@ -1404,120 +1615,82 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } } } - } else if (*sub == 0) { - const auto* info = tables->getLatestRecentRepeater(); - if (info == NULL) { - strcpy(reply, "> none"); - } else { - char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; - mesh::Utils::toHex(hex, info->prefix, info->prefix_len); - sprintf(reply, "> %s,%s", hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); - } - } else if (is_all || is_first || is_last) { + } else { int total = tables->getRecentRepeaterCount(); if (total <= 0) { strcpy(reply, "> none"); } else { - bool newest_first = false; - int limit = total; - int offset = 0; - const char* mode = "all"; - - if (is_first || is_last) { - const char* nstr = sub + (is_first ? 6 : 5); - while (*nstr == ' ') nstr++; - if (*nstr == 0) { - strcpy(reply, "Err - usage: get recent.repeater first|last [offset]"); - return; - } - - char* end_ptr = NULL; - long parsed_count = strtol(nstr, &end_ptr, 10); - while (end_ptr != NULL && *end_ptr == ' ') end_ptr++; - if (end_ptr == NULL || parsed_count <= 0) { - strcpy(reply, "Err - count must be > 0"); - return; - } - - if (*end_ptr != 0) { - char* end_ptr2 = NULL; - long parsed_offset = strtol(end_ptr, &end_ptr2, 10); - while (end_ptr2 != NULL && *end_ptr2 == ' ') end_ptr2++; - if (end_ptr2 == NULL || *end_ptr2 != 0 || parsed_offset < 0) { - strcpy(reply, "Err - offset must be >= 0"); - return; - } - offset = (int)parsed_offset; - } - - limit = (int)parsed_count; - if (is_last) { - newest_first = true; - mode = "last"; - } else { - mode = "first"; - } - } else if (strncmp(sub, "all ", 4) == 0) { - const char* arg = sub + 4; - while (*arg == ' ') arg++; - if (*arg == 0) { - strcpy(reply, "Err - usage: get recent.repeater all "); - return; - } - - char* end_ptr = NULL; - long parsed_a = strtol(arg, &end_ptr, 10); - while (end_ptr != NULL && *end_ptr == ' ') end_ptr++; - if (end_ptr == NULL || parsed_a <= 0) { - strcpy(reply, "Err - count must be > 0"); - return; - } - - char* end_ptr2 = NULL; - long parsed_b = strtol(end_ptr, &end_ptr2, 10); - while (end_ptr2 != NULL && *end_ptr2 == ' ') end_ptr2++; - if (end_ptr2 == NULL || *end_ptr2 != 0 || parsed_b < 0) { - strcpy(reply, "Err - usage: get recent.repeater all "); - return; - } - limit = (int)parsed_a; - offset = (int)parsed_b; - } - - if (offset >= total) { - sprintf(reply, "> none (%s off=%d/%d)", mode, offset, total); - return; - } - - int available = total - offset; - if (limit > available) { - limit = available; - } - if (sender_timestamp == 0) { - Serial.printf("Recent repeater table (%s %d/%d, off=%d):\n", mode, limit, total, offset); - for (int i = 0; i < limit; i++) { - int idx = offset + i; - const auto* info = newest_first ? tables->getRecentRepeaterNewestByIdx(idx) : tables->getRecentRepeaterOldestByIdx(idx); + // Serial CLI: print all entries (no paging). + Serial.printf("Recent repeater table (newest first, total=%d):\n", total); + for (int i = 0; i < total; i++) { + const auto* info = tables->getRecentRepeaterNewestByIdx(i); if (info == NULL) { continue; } + char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; - mesh::Utils::toHex(hex, info->prefix, info->prefix_len); - Serial.printf("%02d: %s,%s\n", idx + 1, hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); + formatRecentRepeaterPrefix(info, hex, sizeof(hex)); + char snr_text[12]; + formatRecentRepeaterSnrX4(info->snr_x4, snr_text, sizeof(snr_text)); + uint32_t fail_pct_x10 = 0; + if (info->retry_count > 0) { + fail_pct_x10 = (((uint32_t)info->fail_count * 1000UL) + (info->retry_count / 2U)) / (uint32_t)info->retry_count; + } + Serial.printf("%03d: %s,%s,fp=%lu.%01lu%%,r=%u,f=%u%s\n", + i + 1, + hex, + snr_text, + (unsigned long)(fail_pct_x10 / 10U), + (unsigned long)(fail_pct_x10 % 10U), + (uint32_t)info->retry_count, + (uint32_t)info->fail_count, + info->snr_locked ? ",l" : ""); } - sprintf(reply, "> %s off=%d n=%d/%d", mode, offset, limit, total); + sprintf(reply, "> n=%d/%d", total, total); } else { - // Remote CLI replies are packet-bound, so include as many rows as fit. - int written = snprintf(reply, 160, "> %s off=%d n=%d/%d", mode, offset, limit, total); + // Remote CLI: page by fixed size to fit packet-limited reply payload. + long page_num = 1; + const long page_size = 4; + const char* arg = sub; + + if (strncmp(arg, "page ", 5) == 0) { + arg += 5; + while (*arg == ' ') arg++; + } + + if (*arg != 0) { + char* end_ptr = NULL; + page_num = strtol(arg, &end_ptr, 10); + while (end_ptr != NULL && *end_ptr == ' ') end_ptr++; + if (end_ptr == NULL || page_num <= 0 || (end_ptr != NULL && *end_ptr != 0)) { + strcpy(reply, "Err - usage: get recent.repeater [page]"); + return; + } + } + + int total_pages = (total + (int)page_size - 1) / (int)page_size; + if (page_num > total_pages) { + sprintf(reply, "> none (page=%ld/%d)", page_num, total_pages); + return; + } + + int offset = ((int)page_num - 1) * (int)page_size; + int limit = total - offset; + if (limit > (int)page_size) { + limit = (int)page_size; + } + + int written = snprintf(reply, 160, "> page=%ld/%d n=%d/%d", page_num, total_pages, limit, total); bool truncated = false; if (written < 0) { reply[0] = 0; written = 0; } + for (int i = 0; i < limit; i++) { int idx = offset + i; - const auto* info = newest_first ? tables->getRecentRepeaterNewestByIdx(idx) : tables->getRecentRepeaterOldestByIdx(idx); + const auto* info = tables->getRecentRepeaterNewestByIdx(idx); if (info == NULL) { continue; } @@ -1525,9 +1698,20 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply truncated = true; break; } + char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; - mesh::Utils::toHex(hex, info->prefix, info->prefix_len); - int n = snprintf(reply + written, 160 - written, "\n%02d:%s,%s", idx + 1, hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); + formatRecentRepeaterPrefix(info, hex, sizeof(hex)); + char snr_text[12]; + formatRecentRepeaterSnrX4(info->snr_x4, snr_text, sizeof(snr_text)); + int n = snprintf(reply + written, + 160 - written, + "\n%03d:%s,%s,r=%u,f=%u%s", + idx + 1, + hex, + snr_text, + (uint32_t)info->retry_count, + (uint32_t)info->fail_count, + info->snr_locked ? ",l" : ""); if (n < 0 || n >= (160 - written)) { truncated = true; break; @@ -1535,12 +1719,10 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply written += n; } if (truncated && written < 156) { - snprintf(reply + written, 160 - written, "\n... use offset"); + snprintf(reply + written, 160 - written, "\n... next page"); } } } - } else { - strcpy(reply, "Err - usage: get recent.repeater [all|all |first [offset]|last [offset]]"); } } else if (memcmp(command, "discover.neighbors", 18) == 0) { const char* sub = command + 18; diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 00a8a31b..b2626e60 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -123,6 +123,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { #endif const NeighbourInfo* findNeighbourByHash(const uint8_t* hash, uint8_t hash_len) const; + bool extractDirectRetryPrefix(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const; static bool allowRecentRepeaterPrefixStore(const uint8_t* prefix, uint8_t prefix_len, void* ctx); int8_t getDirectRetryMinSNRX4() const; void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 47fc6e8d..f07484d6 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -3,7 +3,7 @@ namespace mesh { -static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_DEFAULT = 3; +static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_DEFAULT = 15; static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX = 15; void Mesh::begin() { @@ -491,6 +491,12 @@ bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) { } if (_direct_retries[i].queued) { + if (_direct_retries[i].expect_path_growth + && _direct_retries[i].packet != NULL + && _direct_retries[i].progress_marker < packet->path_len) { + // For retry-good quality, use the received echo packet SNR (return-link quality). + _direct_retries[i].packet->_snr = packet->_snr; + } for (int j = 0; j < _mgr->getOutboundTotal(); j++) { if (_mgr->getOutboundByIdx(j) == _direct_retries[i].packet) { Packet* pending = _mgr->removeOutboundByIdx(j); @@ -504,6 +510,12 @@ bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) { onDirectRetryEvent("good", _direct_retries[i].packet, 0); clearDirectRetrySlot(i); } else { + if (_direct_retries[i].expect_path_growth + && _direct_retries[i].trigger_packet != NULL + && _direct_retries[i].progress_marker < packet->path_len) { + // For retry-good quality, use the received echo packet SNR (return-link quality). + _direct_retries[i].trigger_packet->_snr = packet->_snr; + } onDirectRetryEvent("canceled_echo", _direct_retries[i].trigger_packet, 0); onDirectRetryEvent("good", _direct_retries[i].trigger_packet, 0); clearDirectRetrySlot(i); @@ -532,6 +544,7 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { max_attempts = DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX; } if (_direct_retries[i].retry_attempts_sent >= max_attempts) { + onDirectRetryEvent("failed_all_tries", packet, 0); onDirectRetryEvent("failure", packet, 0); clearDirectRetrySlot(i); continue; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 02d27830..9460a28f 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -11,12 +11,13 @@ // These bytes used to be reserved/unused in persisted prefs, so keep a marker before trusting them. #define DIRECT_RETRY_PREFS_MAGIC_0 0xD4 #define DIRECT_RETRY_PREFS_MAGIC_1 0x52 -#define DIRECT_RETRY_RECENT_DEFAULT 0 -#define DIRECT_RETRY_SNR_MARGIN_DB_DEFAULT 5 -#define DIRECT_RETRY_SNR_MARGIN_DB_MAX 40 +#define DIRECT_RETRY_RECENT_DEFAULT 1 +#define DIRECT_RETRY_SNR_MARGIN_DB_DEFAULT_X4 10 +#define DIRECT_RETRY_SNR_MARGIN_DB_MAX 40 +#define DIRECT_RETRY_SNR_MARGIN_X4_MAX (DIRECT_RETRY_SNR_MARGIN_DB_MAX * 4) #define DIRECT_RETRY_TIMING_MAGIC_0 0xD5 #define DIRECT_RETRY_TIMING_MAGIC_1 0x54 -#define DIRECT_RETRY_COUNT_DEFAULT 3 +#define DIRECT_RETRY_COUNT_DEFAULT 15 #define DIRECT_RETRY_COUNT_MIN 1 #define DIRECT_RETRY_COUNT_MAX 15 #define DIRECT_RETRY_BASE_MS_DEFAULT 200 @@ -33,6 +34,15 @@ static uint32_t _atoi(const char* sp) { return n; } +static uint8_t directRetryMarginDbToX4(float margin_db) { + int32_t scaled_x4 = (int32_t)((margin_db * 4.0f) + 0.5f); // nearest 0.25 dB + return (uint8_t)constrain(scaled_x4, 0, DIRECT_RETRY_SNR_MARGIN_X4_MAX); +} + +static float directRetryMarginX4ToDb(uint8_t margin_x4) { + return ((float)margin_x4) / 4.0f; +} + static bool isValidName(const char *n) { while (*n) { if (*n == '[' || *n == ']' || *n == '\\' || *n == ':' || *n == ',' || *n == '?' || *n == '*') return false; @@ -127,10 +137,10 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { if (_prefs->direct_retry_prefs_magic[0] != DIRECT_RETRY_PREFS_MAGIC_0 || _prefs->direct_retry_prefs_magic[1] != DIRECT_RETRY_PREFS_MAGIC_1) { _prefs->direct_retry_recent_enabled = DIRECT_RETRY_RECENT_DEFAULT; - _prefs->direct_retry_snr_margin_db = DIRECT_RETRY_SNR_MARGIN_DB_DEFAULT; + _prefs->direct_retry_snr_margin_db = DIRECT_RETRY_SNR_MARGIN_DB_DEFAULT_X4; } else { _prefs->direct_retry_recent_enabled = constrain(_prefs->direct_retry_recent_enabled, 0, 1); - _prefs->direct_retry_snr_margin_db = constrain(_prefs->direct_retry_snr_margin_db, 0, DIRECT_RETRY_SNR_MARGIN_DB_MAX); + _prefs->direct_retry_snr_margin_db = constrain(_prefs->direct_retry_snr_margin_db, 0, DIRECT_RETRY_SNR_MARGIN_X4_MAX); } if (_prefs->direct_retry_timing_magic[0] != DIRECT_RETRY_TIMING_MAGIC_0 || _prefs->direct_retry_timing_magic[1] != DIRECT_RETRY_TIMING_MAGIC_1) { @@ -385,7 +395,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else if (memcmp(config, "direct.retry.heard", 18) == 0) { sprintf(reply, "> %s", _prefs->direct_retry_recent_enabled ? "on" : "off"); } else if (memcmp(config, "direct.retry.margin", 19) == 0) { - sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_snr_margin_db); + sprintf(reply, "> %s", StrHelper::ftoa(directRetryMarginX4ToDb(_prefs->direct_retry_snr_margin_db))); } 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) { @@ -652,9 +662,9 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re strcpy(reply, "Error, must be on or off"); } } else if (memcmp(config, "direct.retry.margin ", 20) == 0) { - int db = atoi(&config[20]); + float db = atof(&config[20]); if (db >= 0 && db <= DIRECT_RETRY_SNR_MARGIN_DB_MAX) { - _prefs->direct_retry_snr_margin_db = (uint8_t)db; + _prefs->direct_retry_snr_margin_db = directRetryMarginDbToX4(db); savePrefs(); strcpy(reply, "OK"); } else { @@ -1113,6 +1123,45 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else { strcpy(reply, "Error, cannot be negative"); } + } else if (memcmp(config, "direct.retry.heard ", 19) == 0) { + if (memcmp(&config[19], "on", 2) == 0) { + _prefs->direct_retry_recent_enabled = 1; + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(&config[19], "off", 3) == 0) { + _prefs->direct_retry_recent_enabled = 0; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be on or off"); + } + } else if (memcmp(config, "direct.retry.margin ", 20) == 0) { + float db = atof(&config[20]); + if (db >= 0 && db <= DIRECT_RETRY_SNR_MARGIN_DB_MAX) { + _prefs->direct_retry_snr_margin_db = directRetryMarginDbToX4(db); + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error, min 0 and max %d", DIRECT_RETRY_SNR_MARGIN_DB_MAX); + } + } else if (memcmp(config, "direct.retry.count ", 19) == 0) { + int count = atoi(&config[19]); + if (count >= DIRECT_RETRY_COUNT_MIN && count <= DIRECT_RETRY_COUNT_MAX) { + _prefs->direct_retry_attempts = (uint8_t)count; + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error, min %d and max %d", DIRECT_RETRY_COUNT_MIN, DIRECT_RETRY_COUNT_MAX); + } + } else if (memcmp(config, "direct.retry.base ", 18) == 0) { + int delay_ms = atoi(&config[18]); + if (delay_ms >= DIRECT_RETRY_BASE_MS_MIN && delay_ms <= DIRECT_RETRY_BASE_MS_MAX) { + _prefs->direct_retry_base_ms = (uint16_t)delay_ms; + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error, min %d and max %d", DIRECT_RETRY_BASE_MS_MIN, DIRECT_RETRY_BASE_MS_MAX); + } } else if (memcmp(config, "owner.info ", 11) == 0) { config += 11; char *dp = _prefs->owner_info; @@ -1282,6 +1331,14 @@ 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, "direct.retry.heard", 18) == 0) { + sprintf(reply, "> %s", _prefs->direct_retry_recent_enabled ? "on" : "off"); + } else if (memcmp(config, "direct.retry.margin", 19) == 0) { + sprintf(reply, "> %s", StrHelper::ftoa(directRetryMarginX4ToDb(_prefs->direct_retry_snr_margin_db))); + } 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, "owner.info", 10) == 0) { *reply++ = '>'; *reply++ = ' '; diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 03b1fb64..ddc92ff1 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -34,7 +34,7 @@ struct NodePrefs { // persisted to file char guest_password[16]; float direct_tx_delay_factor; uint8_t direct_retry_recent_enabled; - uint8_t direct_retry_snr_margin_db; + uint8_t direct_retry_snr_margin_db; // stored in quarter-dB units (x4) uint8_t direct_retry_prefs_magic[2]; uint8_t sf; uint8_t cr; diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index effea219..ac6d01b5 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -8,7 +8,14 @@ #define MAX_PACKET_HASHES 128 #define MAX_PACKET_ACKS 64 -#define MAX_RECENT_REPEATERS 64 +#ifndef MAX_RECENT_REPEATERS + // Two defaults. Can be overridden with -D MAX_RECENT_REPEATERS=. + #if defined(ESP32) + #define MAX_RECENT_REPEATERS 512 + #else + #define MAX_RECENT_REPEATERS 64 + #endif +#endif #define MAX_ROUTE_HASH_BYTES 3 class SimpleMeshTables : public mesh::MeshTables { @@ -17,9 +24,12 @@ public: struct RecentRepeaterInfo { // Just enough identity to match a next-hop path prefix plus the SNR that heard it. + uint16_t retry_count; + uint16_t fail_count; uint8_t prefix[MAX_ROUTE_HASH_BYTES]; uint8_t prefix_len; int8_t snr_x4; + uint8_t snr_locked; }; private: @@ -68,19 +78,20 @@ private: return n > 0 && memcmp(a, b, n) == 0; } - int8_t avgSnrX4RoundUp(int8_t curr_snr_x4, int8_t new_snr_x4) const { - int16_t sum = (int16_t)curr_snr_x4 + (int16_t)new_snr_x4; - int16_t avg = sum / 2; // truncates toward zero - // "Round up" means ceil(), which only differs from truncation for positive odd sums. - if (sum > 0 && (sum & 1)) { - avg++; + 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. + int16_t weighted_sum = ((int16_t)curr_snr_x4 * 3) + (int16_t)new_snr_x4; + int16_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 (avg > 127) { - avg = 127; - } else if (avg < -128) { - avg = -128; + if (blended > 127) { + blended = 127; + } else if (blended < -128) { + blended = -128; } - return (int8_t)avg; + return (int8_t)blended; } bool extractRecentRepeater(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const { @@ -249,7 +260,8 @@ public: _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 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; } @@ -258,7 +270,8 @@ public: prefix_len = MAX_ROUTE_HASH_BYTES; } - if (_recent_repeater_allow_fn != NULL && !_recent_repeater_allow_fn(prefix, prefix_len, _recent_repeater_allow_ctx)) { + if (!bypass_allow_filter && _recent_repeater_allow_fn != NULL + && !_recent_repeater_allow_fn(prefix, prefix_len, _recent_repeater_allow_ctx)) { return false; } @@ -273,19 +286,160 @@ public: memcpy(existing.prefix, prefix, prefix_len); existing.prefix_len = prefix_len; } - existing.snr_x4 = avgSnrX4RoundUp(existing.snr_x4, snr_x4); + 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); + } return true; } - // Ring buffer is enough here; retry fallback only needs a recent prefix->SNR observation. - RecentRepeaterInfo& slot = _recent_repeaters[_next_recent_repeater_idx]; + 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; - _next_recent_repeater_idx = (_next_recent_repeater_idx + 1) % MAX_RECENT_REPEATERS; + slot.retry_count = 0; + slot.fail_count = 0; + slot.snr_locked = snr_locked ? 1 : 0; + _next_recent_repeater_idx = (slot_idx + 1) % MAX_RECENT_REPEATERS; return true; } + bool incrementRecentRepeaterRetryCount(const uint8_t* prefix, uint8_t prefix_len, + bool create_if_missing = false, int8_t seed_snr_x4 = 0, + 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; + } + + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + RecentRepeaterInfo& existing = _recent_repeaters[i]; + if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) { + continue; + } + if (prefix_len > existing.prefix_len) { + memset(existing.prefix, 0, sizeof(existing.prefix)); + memcpy(existing.prefix, prefix, prefix_len); + existing.prefix_len = prefix_len; + } + if (existing.retry_count < 0xFFFF) { + existing.retry_count++; + } + return true; + } + + if (!create_if_missing || !setRecentRepeater(prefix, prefix_len, seed_snr_x4, false, bypass_allow_filter)) { + return false; + } + + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + RecentRepeaterInfo& existing = _recent_repeaters[i]; + if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) { + continue; + } + if (existing.retry_count < 0xFFFF) { + existing.retry_count++; + } + return true; + } + return false; + } + bool incrementRecentRepeaterFailCount(const uint8_t* prefix, uint8_t prefix_len, + bool create_if_missing = false, int8_t seed_snr_x4 = 0, + 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; + } + + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + RecentRepeaterInfo& existing = _recent_repeaters[i]; + if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) { + continue; + } + if (prefix_len > existing.prefix_len) { + memset(existing.prefix, 0, sizeof(existing.prefix)); + memcpy(existing.prefix, prefix, prefix_len); + existing.prefix_len = prefix_len; + } + if (existing.fail_count < 0xFFFF) { + existing.fail_count++; + } + return true; + } + + if (!create_if_missing || !setRecentRepeater(prefix, prefix_len, seed_snr_x4, false, bypass_allow_filter)) { + return false; + } + + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + RecentRepeaterInfo& existing = _recent_repeaters[i]; + if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) { + continue; + } + if (existing.fail_count < 0xFFFF) { + existing.fail_count++; + } + return true; + } + return false; + } + 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 == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) { + continue; + } + if (prefix_len > existing.prefix_len) { + memset(existing.prefix, 0, sizeof(existing.prefix)); + memcpy(existing.prefix, prefix, prefix_len); + existing.prefix_len = prefix_len; + } + 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; @@ -360,6 +514,10 @@ public: } return NULL; } + void clearRecentRepeaters() { + memset(_recent_repeaters, 0, sizeof(_recent_repeaters)); + _next_recent_repeater_idx = 0; + } void resetStats() { _direct_dups = _flood_dups = 0; } };