From 9c2ac5aa1c7b8448f9189813120879d2b00a63fc Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 24 Apr 2026 00:08:10 -0700 Subject: [PATCH] Max retries is now a var that can be set between 1 to 15 --- docs/cli_commands.md | 39 +++++++- examples/simple_repeater/MyMesh.cpp | 136 ++++++++++++++++++++++++---- examples/simple_repeater/MyMesh.h | 1 + src/Mesh.cpp | 30 ++++-- src/Mesh.h | 10 ++ src/helpers/CommonCLI.cpp | 49 +++++++++- src/helpers/CommonCLI.h | 3 + src/helpers/SimpleMeshTables.h | 45 +++++++++ 8 files changed, 284 insertions(+), 29 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 774066b0..e798cf0e 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -117,14 +117,21 @@ This document provides an overview of CLI commands that can be sent to MeshCore ### Get or set recent repeater fallback prefix/SNR **Usage:** -- `recent.repeater` -- `recent.repeater ` +- `get recent.repeater` +- `get recent.repeater all` +- `get recent.repeater first ` +- `get recent.repeater last ` +- `set recent.repeater ` **Parameters:** - `prefix_hex`: 1-3 bytes of next-hop prefix (hex) - `snr_db`: SNR in dB (supports decimals; stored at x4 precision) +- `count`: number of entries to print -**Note:** `set` is rejected when the prefix already exists in neighbors. +**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. +- Remote CLI replies include rows too, but may truncate when the packet payload limit is reached. --- @@ -545,6 +552,32 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +#### View or change the number of direct retry attempts +**Usage:** +- `get direct.retry.count` +- `set direct.retry.count ` + +**Parameters:** +- `value`: Retry attempts after initial TX (`1`-`15`) + +**Default:** `3` + +--- + +#### View or change the base direct retry wait (milliseconds) +**Usage:** +- `get direct.retry.base` +- `set direct.retry.base ` + +**Parameters:** +- `value`: Base wait in milliseconds (`10`-`5000`) + +**Default:** `200` + +**Note:** The actual first retry wait is `base + computed_echo_wait_from_live_phy`. + +--- + #### [Experimental] View or change the processing delay for received traffic **Usage:** - `get rxdelay` diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 71b532ed..220f9704 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -627,16 +627,21 @@ bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_ho return recent != NULL && recent->snr_x4 >= 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); // Approximate LoRa line rate in kilobits/sec from the live radio params the repeater is using now. float kbps = (((float) active_sf) * active_bw * ((float) active_cr)) / ((float) (1UL << active_sf)); if (kbps <= 0.0f) { - return HALO_DIRECT_RETRY_DELAY_MIN; + return base_wait_millis; } // Wait roughly long enough for our transmission, the next hop's receive/forward window, and its echo back. uint32_t bits = ((uint32_t) packet->getRawLength()) * 8; uint32_t scaled_wait_millis = (uint32_t) ((((float) bits) * 4.0f) / kbps); - return max((uint32_t) HALO_DIRECT_RETRY_DELAY_MIN, scaled_wait_millis); + return base_wait_millis + scaled_wait_millis; +} +uint8_t MyMesh::getDirectRetryMaxAttempts(const mesh::Packet* packet) const { + (void)packet; + return constrain(_prefs.direct_retry_attempts, (uint8_t)1, (uint8_t)15); } bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) { @@ -970,6 +975,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _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_base_ms = 200; StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name)); _prefs.node_lat = ADVERT_LAT; _prefs.node_lon = ADVERT_LON; @@ -1345,29 +1352,36 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply Serial.printf("\n"); } reply[0] = 0; - } else if (memcmp(command, "recent.repeater", 15) == 0) { - const char* sub = command + 15; - while (*sub == ' ') sub++; - auto* tables = (SimpleMeshTables*)getTables(); - 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 (strncmp(command, "get recent.repeater", 19) == 0 + || strncmp(command, "set recent.repeater", 19) == 0 + || strncmp(command, "recent.repeater", 15) == 0) { + bool is_get = false; + bool is_set = false; + const char* sub = command; + + if (strncmp(command, "get recent.repeater", 19) == 0) { + is_get = true; + sub = command + 19; + } else if (strncmp(command, "set recent.repeater", 19) == 0) { + is_set = true; + sub = command + 19; } else { + sub = command + 15; // legacy command format + } + while (*sub == ' ') sub++; + + auto* tables = (SimpleMeshTables*)getTables(); + + if (is_set || (!is_get && *sub != 0 && strcmp(sub, "all") != 0 && strncmp(sub, "first ", 6) != 0 && strncmp(sub, "last ", 5) != 0)) { char* params = (char*) sub; char* arg_snr = strchr(params, ' '); if (arg_snr == NULL) { - strcpy(reply, "Err - usage: 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: recent.repeater "); + strcpy(reply, "Err - usage: set recent.repeater "); } else { int hex_len = strlen(params); int prefix_len = hex_len / 2; @@ -1386,6 +1400,94 @@ 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 (strcmp(sub, "all") == 0 || strncmp(sub, "first ", 6) == 0 || strncmp(sub, "last ", 5) == 0) { + int total = tables->getRecentRepeaterCount(); + if (total <= 0) { + strcpy(reply, "> none"); + } else { + bool newest_first = false; + int limit = total; + const char* mode = "all"; + if (strncmp(sub, "first ", 6) == 0 || strncmp(sub, "last ", 5) == 0) { + const char* nstr = sub + (sub[0] == 'f' ? 6 : 5); + while (*nstr == ' ') nstr++; + if (*nstr == 0) { + strcpy(reply, "Err - usage: get recent.repeater first|last "); + return; + } + char* end_ptr = NULL; + long parsed = strtol(nstr, &end_ptr, 10); + while (end_ptr != NULL && *end_ptr == ' ') end_ptr++; + if (end_ptr == NULL || *end_ptr != 0 || parsed <= 0) { + strcpy(reply, "Err - count must be > 0"); + return; + } + limit = (int)parsed; + if (sub[0] == 'l') { + newest_first = true; + mode = "last"; + } else { + mode = "first"; + } + } + if (limit > total) { + limit = total; + } + + if (sender_timestamp == 0) { + Serial.printf("Recent repeater table (%s %d/%d):\n", mode, limit, total); + for (int i = 0; i < limit; i++) { + const auto* info = newest_first ? tables->getRecentRepeaterNewestByIdx(i) : tables->getRecentRepeaterOldestByIdx(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", i + 1, hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); + } + sprintf(reply, "> showing %d/%d (%s)", limit, total, mode); + } else { + // Remote CLI replies are packet-bound, so include as many rows as fit. + int written = snprintf(reply, 160, "> showing %d/%d (%s)", limit, total, mode); + bool truncated = false; + if (written < 0) { + reply[0] = 0; + written = 0; + } + for (int i = 0; i < limit; i++) { + const auto* info = newest_first ? tables->getRecentRepeaterNewestByIdx(i) : tables->getRecentRepeaterOldestByIdx(i); + if (info == NULL) { + continue; + } + if (written >= 154) { + 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", i + 1, hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); + if (n < 0 || n >= (160 - written)) { + truncated = true; + break; + } + written += n; + } + if (truncated && written < 156) { + snprintf(reply + written, 160 - written, "\n..."); + } + } + } + } else { + strcpy(reply, "Err - usage: get recent.repeater [all|first |last ]"); } } 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 16566dca..00a8a31b 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -154,6 +154,7 @@ protected: uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override; bool allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const override; uint32_t getDirectRetryEchoDelay(const mesh::Packet* packet) const override; + uint8_t getDirectRetryMaxAttempts(const mesh::Packet* packet) const override; void onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis) override; int getInterferenceThreshold() const override { diff --git a/src/Mesh.cpp b/src/Mesh.cpp index b9892eed..47fc6e8d 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -3,8 +3,8 @@ namespace mesh { -static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS = 3; -static const uint32_t DIRECT_RETRY_BACKOFF_MS[DIRECT_RETRY_MAX_ATTEMPTS] = { 200, 300, 400 }; +static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_DEFAULT = 3; +static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX = 15; void Mesh::begin() { for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { @@ -57,6 +57,14 @@ 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) const { + 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; } @@ -517,7 +525,13 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { // The retry packet itself just finished transmitting; Dispatcher will release it after this hook. onDirectRetryEvent("resent", packet, 0); _direct_retries[i].retry_attempts_sent++; - if (_direct_retries[i].retry_attempts_sent >= DIRECT_RETRY_MAX_ATTEMPTS) { + 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) { onDirectRetryEvent("failure", packet, 0); clearDirectRetrySlot(i); continue; @@ -532,7 +546,7 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { } *retry = *packet; - uint32_t retry_delay = DIRECT_RETRY_BACKOFF_MS[_direct_retries[i].retry_attempts_sent]; + uint32_t retry_delay = getDirectRetryAttemptDelay(packet, _direct_retries[i].retry_attempts_sent); sendPacket(retry, _direct_retries[i].priority, retry_delay); if (isDirectRetryQueued(retry)) { _direct_retries[i].packet = retry; @@ -612,7 +626,9 @@ bool Mesh::getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_h case PAYLOAD_TYPE_RESPONSE: case PAYLOAD_TYPE_TXT_MSG: case PAYLOAD_TYPE_ANON_REQ: - if (packet->getPathHashCount() <= 1) { + // 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; @@ -622,7 +638,7 @@ bool Mesh::getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_h return true; case PAYLOAD_TYPE_MULTIPART: - if (packet->payload_len < 1 || (packet->payload[0] & 0x0F) != PAYLOAD_TYPE_ACK || packet->getPathHashCount() <= 1) { + if (packet->payload_len < 1 || (packet->payload[0] & 0x0F) != PAYLOAD_TYPE_ACK || packet->getPathHashCount() == 0) { return false; } next_hop_hash = packet->path; @@ -680,7 +696,7 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { } // Only store retry metadata here; allocate the retry packet after the initial TX really completes. - uint32_t retry_delay = DIRECT_RETRY_BACKOFF_MS[0]; + uint32_t retry_delay = getDirectRetryAttemptDelay(packet, 0); calculateDirectRetryKey(packet, _direct_retries[slot_idx].retry_key); _direct_retries[slot_idx].packet = NULL; _direct_retries[slot_idx].trigger_packet = const_cast(packet); diff --git a/src/Mesh.h b/src/Mesh.h index 4441514b..ad4d8a2f 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -107,6 +107,16 @@ protected: */ 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) const; + /** * \returns number of extra (Direct) ACK transmissions wanted. */ diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 38d4536a..02d27830 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -14,6 +14,14 @@ #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_TIMING_MAGIC_0 0xD5 +#define DIRECT_RETRY_TIMING_MAGIC_1 0x54 +#define DIRECT_RETRY_COUNT_DEFAULT 3 +#define DIRECT_RETRY_COUNT_MIN 1 +#define DIRECT_RETRY_COUNT_MAX 15 +#define DIRECT_RETRY_BASE_MS_DEFAULT 200 +#define DIRECT_RETRY_BASE_MS_MIN 10 +#define DIRECT_RETRY_BASE_MS_MAX 5000 // Believe it or not, this std C function is busted on some platforms! static uint32_t _atoi(const char* sp) { @@ -97,7 +105,10 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 - // next: 291 + file.read((uint8_t *)&_prefs->direct_retry_attempts, sizeof(_prefs->direct_retry_attempts)); // 291 + file.read((uint8_t *)&_prefs->direct_retry_base_ms, sizeof(_prefs->direct_retry_base_ms)); // 292 + file.read((uint8_t *)&_prefs->direct_retry_timing_magic[0], sizeof(_prefs->direct_retry_timing_magic)); // 294 + // next: 296 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -121,6 +132,14 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { _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); } + if (_prefs->direct_retry_timing_magic[0] != DIRECT_RETRY_TIMING_MAGIC_0 + || _prefs->direct_retry_timing_magic[1] != DIRECT_RETRY_TIMING_MAGIC_1) { + _prefs->direct_retry_attempts = DIRECT_RETRY_COUNT_DEFAULT; + _prefs->direct_retry_base_ms = DIRECT_RETRY_BASE_MS_DEFAULT; + } else { + _prefs->direct_retry_attempts = constrain(_prefs->direct_retry_attempts, DIRECT_RETRY_COUNT_MIN, DIRECT_RETRY_COUNT_MAX); + _prefs->direct_retry_base_ms = constrain(_prefs->direct_retry_base_ms, DIRECT_RETRY_BASE_MS_MIN, DIRECT_RETRY_BASE_MS_MAX); + } // sanitise bad bridge pref values _prefs->bridge_enabled = constrain(_prefs->bridge_enabled, 0, 1); @@ -201,7 +220,11 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 - // next: 291 + file.write((uint8_t *)&_prefs->direct_retry_attempts, sizeof(_prefs->direct_retry_attempts)); // 291 + file.write((uint8_t *)&_prefs->direct_retry_base_ms, sizeof(_prefs->direct_retry_base_ms)); // 292 + uint8_t retry_timing_magic[2] = { DIRECT_RETRY_TIMING_MAGIC_0, DIRECT_RETRY_TIMING_MAGIC_1 }; + file.write(retry_timing_magic, sizeof(retry_timing_magic)); // 294 + // next: 296 file.close(); } @@ -363,6 +386,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re 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); + } 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++ = ' '; @@ -633,6 +660,24 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } 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; diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 85962638..03b1fb64 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -63,6 +63,9 @@ struct NodePrefs { // persisted to file uint8_t rx_boosted_gain; // power settings uint8_t path_hash_mode; // which path mode to use when sending uint8_t loop_detect; + uint8_t direct_retry_attempts; + uint16_t direct_retry_base_ms; + uint8_t direct_retry_timing_magic[2]; }; class CommonCLICallbacks { diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index f5da272b..705869ad 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -279,6 +279,51 @@ public: } 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) {