Refine direct retry SNR handling and recent repeater controls

This commit is contained in:
mikecarper committed 2026-06-04 15:12:44 -07:00
1 parent d1d50967cb
commit 399939f41b
7 files changed
+538 -178

No files matched your search

+12 -16
View File
@@ -101,24 +101,20 @@
### Get or set recent repeater fallback prefix/SNR
**Usage:**
- `get recent.repeater`
- `get recent.repeater all`
- `get recent.repeater all <count> <offset>`
- `get recent.repeater first <count>`
- `get recent.repeater first <count> <offset>`
- `get recent.repeater last <count>`
- `get recent.repeater last <count> <offset>`
- `set recent.repeater <prefix_hex> <snr_db>`
- `get recent.repeater <page>`
- `get recent.repeater page <page>`
- `set recent.repeater <prefix_hex_6> <snr_db>`
**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 <page>`.
---
@@ -437,7 +433,7 @@
**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.
@@ -449,9 +445,9 @@
- `set direct.retry.margin <value>`
**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.
@@ -465,7 +461,7 @@
**Parameters:**
- `value`: Retry attempts after initial TX (`1`-`15`)
**Default:** `3`
**Default:** `15`
---
+315 -133
View File
@@ -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
@@ -430,7 +488,31 @@ bool MyMesh::isLooped(const mesh::Packet* packet, const uint8_t max_counters[])
bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
if (_prefs.disable_fwd) return false;
if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) 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");
return false;
@@ -524,23 +606,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();
}
@@ -563,28 +724,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);
@@ -888,9 +1080,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;
@@ -1299,9 +1491,11 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
}
} 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) {
@@ -1310,38 +1504,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 <prefix_hex> <snr_db>");
strcpy(reply, "Err - usage: set recent.repeater <prefix_hex_6> <snr_db>");
} else {
*arg_snr++ = 0;
while (*arg_snr == ' ') arg_snr++;
if (*arg_snr == 0) {
strcpy(reply, "Err - usage: set recent.repeater <prefix_hex> <snr_db>");
strcpy(reply, "Err - usage: set recent.repeater <prefix_hex_6> <snr_db>");
} 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");
@@ -1349,120 +1560,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 <count> [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 <count> <offset>");
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 <count> <offset>");
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;
}
@@ -1470,9 +1643,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;
@@ -1480,12 +1664,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 <count> <offset>|first <count> [offset]|last <count> [offset]]");
}
} else{
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
+1
View File
@@ -119,6 +119,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);
+14 -1
View File
@@ -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() {
@@ -498,6 +498,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);
@@ -511,6 +517,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);
@@ -539,6 +551,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;
+19 -9
View File
@@ -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);
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;
@@ -123,10 +133,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) {
@@ -368,7 +378,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
} 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) {
@@ -605,9 +615,9 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
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 {
+1 -1
View File
@@ -28,7 +28,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;
+176 -18
View File
@@ -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=<n>.
#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; }
};