Refine direct retry SNR handling and recent repeater controls
This commit is contained in:
1 parent
d1d50967cb
commit
399939f41b
7 files changed
+538
-178
No files matched your search
+12
-16
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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; }
|
||||
};
|
||||
Reference in new issue
Block a user