Add direct retry controls

This commit is contained in:
mikecarper committed 2026-06-15 17:24:22 -07:00
1 parent 7d8bb1d319
commit 765ec1447b
12 files changed
+649 -416

No files matched your search

+45 -87
View File
@@ -35,6 +35,7 @@ void Mesh::begin() {
_direct_retries[i].retry_at = 0;
_direct_retries[i].retry_delay = 0;
_direct_retries[i].retry_attempts_sent = 0;
_direct_retries[i].next_hop_hash_len = 0;
_direct_retries[i].priority = 0;
_direct_retries[i].progress_marker = 0;
_direct_retries[i].expect_path_growth = false;
@@ -63,6 +64,7 @@ void Mesh::loop() {
: (uint32_t)(_ms->getMillis() - _direct_retries[i].retry_started_at);
onDirectRetryEvent("failed_all_tries", _direct_retries[i].packet, elapsed_millis, _direct_retries[i].retry_attempts_sent);
onDirectRetryEvent("failure", _direct_retries[i].packet, elapsed_millis, _direct_retries[i].retry_attempts_sent);
onDirectRetryFailed(_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len);
clearDirectRetrySlot(i);
continue;
}
@@ -183,12 +185,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
}
}
if (canDecodeDirectPayloadForSelf(pkt)) {
// Some path sources include the final node hash, and some packets are
// heard before all planned hops are consumed. Only stop forwarding once
// this node proves it can decrypt the payload.
removePathPrefix(pkt, pkt->getPathHashCount());
} else if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) || maybeShortCircuitDirect(pkt)) {
if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) || maybeShortCircuitDirect(pkt)) {
if (allowPacketForward(pkt)) {
if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) {
return forwardMultipartDirect(pkt);
@@ -479,13 +476,10 @@ DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt) {
void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) {
if (!packet->isMarkedDoNotRetransmit()) {
uint32_t crc;
memcpy(&crc, packet->payload, 4);
uint8_t extra = getExtraAckTransmitCount();
while (extra > 0) {
delay_millis += getDirectRetransmitDelay(packet) + 300;
auto a1 = createMultiAck(crc, extra);
auto a1 = createMultiAck(packet->payload, packet->payload_len, extra);
if (a1) {
a1->path_len = Packet::copyPath(a1->path, packet->path, packet->path_len);
a1->header &= ~PH_ROUTE_MASK;
@@ -496,7 +490,7 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) {
extra--;
}
auto a2 = createAck(crc);
auto a2 = createAck(packet->payload, packet->payload_len);
if (a2) {
a2->path_len = Packet::copyPath(a2->path, packet->path, packet->path_len);
a2->header &= ~PH_ROUTE_MASK;
@@ -508,9 +502,6 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) {
}
void Mesh::clearDirectRetrySlot(int idx) {
if (_direct_retries[idx].waiting_final_echo && _direct_retries[idx].packet != NULL) {
releasePacket(_direct_retries[idx].packet);
}
_direct_retries[idx].packet = NULL;
_direct_retries[idx].trigger_packet = NULL;
_direct_retries[idx].retry_started_at = 0;
@@ -518,6 +509,8 @@ void Mesh::clearDirectRetrySlot(int idx) {
_direct_retries[idx].retry_at = 0;
_direct_retries[idx].retry_delay = 0;
_direct_retries[idx].retry_attempts_sent = 0;
memset(_direct_retries[idx].next_hop_hash, 0, sizeof(_direct_retries[idx].next_hop_hash));
_direct_retries[idx].next_hop_hash_len = 0;
_direct_retries[idx].priority = 0;
_direct_retries[idx].progress_marker = 0;
_direct_retries[idx].expect_path_growth = false;
@@ -558,6 +551,7 @@ bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) {
}
int8_t echo_snr_x4 = packet->_snr;
onDirectRetrySucceeded(_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len, echo_snr_x4);
if (_direct_retries[i].queued || _direct_retries[i].waiting_final_echo) {
if (_direct_retries[i].packet != NULL) {
// Success quality comes from the received downstream echo, not the original upstream RX.
@@ -620,16 +614,9 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) {
max_attempts = DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX;
}
if (_direct_retries[i].retry_attempts_sent >= max_attempts) {
Packet* final_wait = obtainNewPacket();
if (final_wait == NULL) {
onDirectRetryEvent("dropped_no_packet", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent);
onDirectRetryEvent("failure", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent);
clearDirectRetrySlot(i);
continue;
}
*final_wait = *packet;
_direct_retries[i].packet = final_wait;
// Dispatcher releases the retry packet after this hook. Keep only retry metadata
// for the final echo window so pool exhaustion cannot force a premature failure.
_direct_retries[i].packet = NULL;
_direct_retries[i].retry_at = futureMillis(_direct_retries[i].retry_delay);
_direct_retries[i].waiting_final_echo = true;
_direct_retries[i].queued = false;
@@ -783,62 +770,6 @@ bool Mesh::getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_h
}
}
bool Mesh::canDecodeDirectPayloadForSelf(const Packet* packet) {
if (packet == NULL || !packet->isRouteDirect() || packet->getPathHashCount() == 0 || packet->payload_len < 1) {
return false;
}
switch (packet->getPayloadType()) {
case PAYLOAD_TYPE_PATH:
case PAYLOAD_TYPE_REQ:
case PAYLOAD_TYPE_RESPONSE:
case PAYLOAD_TYPE_TXT_MSG: {
if (packet->payload_len < 2) {
return false;
}
int i = 0;
uint8_t dest_hash = packet->payload[i++];
uint8_t src_hash = packet->payload[i++];
if (i + CIPHER_MAC_SIZE >= packet->payload_len || !self_id.isHashMatch(&dest_hash)) {
return false;
}
int num = searchPeersByHash(&src_hash);
for (int j = 0; j < num; j++) {
uint8_t secret[PUB_KEY_SIZE];
getPeerSharedSecret(secret, j);
uint8_t data[MAX_PACKET_PAYLOAD];
if (Utils::MACThenDecrypt(secret, data, &packet->payload[i], packet->payload_len - i) > 0) {
return true;
}
}
return false;
}
case PAYLOAD_TYPE_ANON_REQ: {
int i = 0;
uint8_t dest_hash = packet->payload[i++];
if (i + PUB_KEY_SIZE + CIPHER_MAC_SIZE >= packet->payload_len || !self_id.isHashMatch(&dest_hash)) {
return false;
}
Identity sender(&packet->payload[i]);
i += PUB_KEY_SIZE;
uint8_t secret[PUB_KEY_SIZE];
self_id.calcSharedSecret(secret, sender);
uint8_t data[MAX_PACKET_PAYLOAD];
return Utils::MACThenDecrypt(secret, data, &packet->payload[i], packet->payload_len - i) > 0;
}
default:
return false;
}
}
void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) {
const uint8_t* next_hop_hash;
uint8_t next_hop_hash_len;
@@ -849,6 +780,18 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) {
return;
}
uint8_t retry_key[MAX_HASH_SIZE];
calculateDirectRetryKey(packet, retry_key);
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
if (_direct_retries[i].active
&& memcmp(retry_key, _direct_retries[i].retry_key, MAX_HASH_SIZE) == 0
&& _direct_retries[i].progress_marker == progress_marker
&& _direct_retries[i].expect_path_growth == expect_path_growth) {
return;
}
}
int slot_idx = -1;
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
if (!_direct_retries[i].active) {
@@ -864,7 +807,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 = getDirectRetryAttemptDelay(packet, 0);
calculateDirectRetryKey(packet, _direct_retries[slot_idx].retry_key);
memcpy(_direct_retries[slot_idx].retry_key, retry_key, MAX_HASH_SIZE);
_direct_retries[slot_idx].packet = NULL;
_direct_retries[slot_idx].trigger_packet = const_cast<Packet*>(packet);
_direct_retries[slot_idx].retry_started_at = 0;
@@ -872,6 +815,9 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) {
_direct_retries[slot_idx].retry_at = 0;
_direct_retries[slot_idx].retry_delay = retry_delay;
_direct_retries[slot_idx].retry_attempts_sent = 0;
memset(_direct_retries[slot_idx].next_hop_hash, 0, sizeof(_direct_retries[slot_idx].next_hop_hash));
memcpy(_direct_retries[slot_idx].next_hop_hash, next_hop_hash, next_hop_hash_len);
_direct_retries[slot_idx].next_hop_hash_len = next_hop_hash_len;
_direct_retries[slot_idx].priority = priority;
_direct_retries[slot_idx].progress_marker = progress_marker;
_direct_retries[slot_idx].expect_path_growth = expect_path_growth;
@@ -1036,7 +982,9 @@ Packet* Mesh::createGroupDatagram(uint8_t type, const GroupChannel& channel, con
return packet;
}
Packet* Mesh::createAck(uint32_t ack_crc) {
Packet* Mesh::createAck(const uint8_t* ack_hash, uint8_t ack_len) {
if (ack_len > sizeof(Packet::payload)) return NULL;
Packet* packet = obtainNewPacket();
if (packet == NULL) {
MESH_DEBUG_PRINTLN("%s Mesh::createAck(): error, packet pool empty", getLogDateTime());
@@ -1044,13 +992,19 @@ Packet* Mesh::createAck(uint32_t ack_crc) {
}
packet->header = (PAYLOAD_TYPE_ACK << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later
memcpy(packet->payload, &ack_crc, 4);
packet->payload_len = 4;
memcpy(packet->payload, ack_hash, ack_len);
packet->payload_len = ack_len;
return packet;
}
Packet* Mesh::createMultiAck(uint32_t ack_crc, uint8_t remaining) {
Packet* Mesh::createAck(uint32_t ack_crc) {
return createAck((const uint8_t*)&ack_crc, 4);
}
Packet* Mesh::createMultiAck(const uint8_t* ack_hash, uint8_t ack_len, uint8_t remaining) {
if (ack_len + 1 > sizeof(Packet::payload)) return NULL;
Packet* packet = obtainNewPacket();
if (packet == NULL) {
MESH_DEBUG_PRINTLN("%s Mesh::createMultiAck(): error, packet pool empty", getLogDateTime());
@@ -1059,12 +1013,16 @@ Packet* Mesh::createMultiAck(uint32_t ack_crc, uint8_t remaining) {
packet->header = (PAYLOAD_TYPE_MULTIPART << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later
packet->payload[0] = (remaining << 4) | PAYLOAD_TYPE_ACK;
memcpy(&packet->payload[1], &ack_crc, 4);
packet->payload_len = 5;
memcpy(&packet->payload[1], ack_hash, ack_len);
packet->payload_len = ack_len + 1;
return packet;
}
Packet* Mesh::createMultiAck(uint32_t ack_crc, uint8_t remaining) {
return createMultiAck((const uint8_t*)&ack_crc, 4, remaining);
}
Packet* Mesh::createRawData(const uint8_t* data, size_t len) {
if (len > sizeof(Packet::payload)) return NULL; // invalid arg
+18 -1
View File
@@ -29,6 +29,10 @@ public:
* and provides virtual methods for sub-classes on handling incoming, and also preparing outbound Packets.
*/
class Mesh : public Dispatcher {
RNG* _rng;
RTCClock* _rtc;
MeshTables* _tables;
struct DirectRetryEntry {
Packet* packet;
Packet* trigger_packet;
@@ -38,6 +42,8 @@ class Mesh : public Dispatcher {
uint32_t retry_delay;
uint8_t retry_attempts_sent;
uint8_t retry_key[MAX_HASH_SIZE];
uint8_t next_hop_hash[MAX_HASH_SIZE];
uint8_t next_hop_hash_len;
uint8_t priority;
uint8_t progress_marker;
bool expect_path_growth;
@@ -58,7 +64,6 @@ class Mesh : public Dispatcher {
void clearPendingDirectRetryOnSendFail(const Packet* packet);
bool getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_hash, uint8_t& next_hop_hash_len,
uint8_t& progress_marker, bool& expect_path_growth) const;
bool canDecodeDirectPayloadForSelf(const Packet* packet);
void maybeScheduleDirectRetry(const Packet* packet, uint8_t priority);
//void routeRecvAcks(Packet* packet, uint32_t delay_millis);
DispatcherAction forwardMultipartDirect(Packet* pkt);
@@ -133,6 +138,16 @@ protected:
*/
virtual void onDirectRetryEvent(const char* event, const Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) { }
/**
* \brief Optional hook for link-quality feedback when all direct-retry attempts fail.
*/
virtual void onDirectRetryFailed(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) { }
/**
* \brief Optional hook for link-quality feedback when a direct-retry echo is heard.
*/
virtual void onDirectRetrySucceeded(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len, int8_t snr_x4) { }
/**
* \brief Optional hook to set local-only transmit options on a retry packet before it is queued.
*/
@@ -253,7 +268,9 @@ public:
Packet* createDatagram(uint8_t type, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t len);
Packet* createAnonDatagram(uint8_t type, const LocalIdentity& sender, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t data_len);
Packet* createGroupDatagram(uint8_t type, const GroupChannel& channel, const uint8_t* data, size_t data_len);
Packet* createAck(const uint8_t* ack_hash, uint8_t ack_len);
Packet* createAck(uint32_t ack_crc);
Packet* createMultiAck(const uint8_t* ack_hash, uint8_t ack_len, uint8_t remaining);
Packet* createMultiAck(uint32_t ack_crc, uint8_t remaining);
Packet* createPathReturn(const uint8_t* dest_hash, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len);
Packet* createPathReturn(const Identity& dest, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len);
+1 -12
View File
@@ -1,7 +1,5 @@
#include "ClientACL.h"
static const uint8_t CONTACT_RECORD_VERSION_ALT_PATH = 1;
static File openWrite(FILESYSTEM* _fs, const char* filename) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
_fs->remove(filename);
@@ -30,7 +28,6 @@ void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) {
uint8_t unused[2];
memset(&c, 0, sizeof(c));
c.alt_path_len = OUT_PATH_UNKNOWN;
bool success = (file.read(pub_key, 32) == 32);
success = success && (file.read((uint8_t *) &c.permissions, 1) == 1);
@@ -39,10 +36,6 @@ void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) {
success = success && (file.read((uint8_t *)&c.out_path_len, 1) == 1);
success = success && (file.read(c.out_path, 64) == 64);
success = success && (file.read(c.shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE); // will be recalculated below
if (success && unused[0] >= CONTACT_RECORD_VERSION_ALT_PATH) {
success = success && (file.read((uint8_t *)&c.alt_path_len, 1) == 1);
success = success && (file.read(c.alt_path, 64) == 64);
}
if (!success) break; // EOF
@@ -64,8 +57,7 @@ void ClientACL::save(FILESYSTEM* fs, bool (*filter)(ClientInfo*)) {
File file = openWrite(_fs, "/s_contacts");
if (file) {
uint8_t unused[2];
unused[0] = CONTACT_RECORD_VERSION_ALT_PATH;
unused[1] = 0;
memset(unused, 0, sizeof(unused));
for (int i = 0; i < num_clients; i++) {
auto c = &clients[i];
@@ -78,8 +70,6 @@ void ClientACL::save(FILESYSTEM* fs, bool (*filter)(ClientInfo*)) {
success = success && (file.write((uint8_t *)&c->out_path_len, 1) == 1);
success = success && (file.write(c->out_path, 64) == 64);
success = success && (file.write(c->shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE);
success = success && (file.write((uint8_t *)&c->alt_path_len, 1) == 1);
success = success && (file.write(c->alt_path, 64) == 64);
if (!success) break; // write failed
}
@@ -125,7 +115,6 @@ ClientInfo* ClientACL::putClient(const mesh::Identity& id, uint8_t init_perms) {
c->permissions = init_perms;
c->id = id;
c->out_path_len = OUT_PATH_UNKNOWN;
c->alt_path_len = OUT_PATH_UNKNOWN;
return c;
}
+1 -4
View File
@@ -10,16 +10,13 @@
#define PERM_ACL_READ_WRITE 2
#define PERM_ACL_ADMIN 3
#define OUT_PATH_FORCE_FLOOD 0xFE
#define OUT_PATH_UNKNOWN 0xFF
#define OUT_PATH_UNKNOWN 0xFF
struct ClientInfo {
mesh::Identity id;
uint8_t permissions;
uint8_t out_path_len;
uint8_t out_path[MAX_PATH_SIZE];
uint8_t alt_path_len;
uint8_t alt_path[MAX_PATH_SIZE];
uint8_t shared_secret[PUB_KEY_SIZE];
uint32_t last_timestamp; // by THEIR clock (transient)
uint32_t last_activity; // by OUR clock (transient)
+275 -2
View File
@@ -27,6 +27,113 @@ static bool isValidName(const char *n) {
return true;
}
static bool looksNumeric(const char* s) {
if (s == NULL) return false;
while (*s == ' ') s++;
if (*s == '-' || *s == '+') s++;
bool saw_digit = false;
while (*s) {
if (*s >= '0' && *s <= '9') {
saw_digit = true;
} else if (*s != '.') {
break;
}
s++;
}
return saw_digit;
}
static int16_t parseSnrDbX4(const char* s) {
float db = atof(s);
return (int16_t)(db * 4.0f + (db >= 0.0f ? 0.5f : -0.5f));
}
static void formatSnrDbX4(char* dest, size_t dest_len, int16_t snr_x4) {
int16_t v = snr_x4;
const char* sign = "";
if (v < 0) {
sign = "-";
v = -v;
}
snprintf(dest, dest_len, "%s%d.%02d", sign, v / 4, (v % 4) * 25);
}
static const char* retryPresetName(uint8_t preset) {
switch (preset) {
case RETRY_PRESET_INFRA: return "infra";
case RETRY_PRESET_ROOFTOP: return "rooftop";
case RETRY_PRESET_MOBILE: return "mobile";
default: return "custom";
}
}
static void markDirectRetryPrefsValid(NodePrefs* prefs) {
prefs->direct_retry_prefs_magic[0] = DIRECT_RETRY_PREFS_MAGIC_0;
prefs->direct_retry_prefs_magic[1] = DIRECT_RETRY_PREFS_MAGIC_1;
}
static void applyDirectRetryPreset(NodePrefs* prefs, uint8_t preset) {
prefs->retry_preset = preset;
if (preset == RETRY_PRESET_INFRA) {
prefs->direct_retry_attempts = DIRECT_RETRY_INFRA_COUNT;
prefs->direct_retry_base_ms = DIRECT_RETRY_INFRA_BASE_MS;
prefs->direct_retry_step_ms = DIRECT_RETRY_INFRA_STEP_MS;
prefs->direct_retry_snr_margin_x4 = DIRECT_RETRY_INFRA_MARGIN_X4;
} else if (preset == RETRY_PRESET_MOBILE) {
prefs->direct_retry_attempts = DIRECT_RETRY_MOBILE_COUNT;
prefs->direct_retry_base_ms = DIRECT_RETRY_MOBILE_BASE_MS;
prefs->direct_retry_step_ms = DIRECT_RETRY_MOBILE_STEP_MS;
prefs->direct_retry_snr_margin_x4 = DIRECT_RETRY_MOBILE_MARGIN_X4;
} else {
prefs->retry_preset = RETRY_PRESET_ROOFTOP;
prefs->direct_retry_attempts = DIRECT_RETRY_ROOFTOP_COUNT;
prefs->direct_retry_base_ms = DIRECT_RETRY_ROOFTOP_BASE_MS;
prefs->direct_retry_step_ms = DIRECT_RETRY_ROOFTOP_STEP_MS;
prefs->direct_retry_snr_margin_x4 = DIRECT_RETRY_ROOFTOP_MARGIN_X4;
}
markDirectRetryPrefsValid(prefs);
}
static void setDefaultDirectRetryPrefs(NodePrefs* prefs) {
applyDirectRetryPreset(prefs, RETRY_PRESET_ROOFTOP);
prefs->direct_retry_cr_enabled = 1;
prefs->direct_retry_cr4_snr_x4 = DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT;
prefs->direct_retry_cr5_snr_x4 = DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT;
prefs->direct_retry_cr7_snr_x4 = DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT;
prefs->direct_retry_cr8_snr_x4 = DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT;
markDirectRetryPrefsValid(prefs);
}
static bool directRetryPrefsValid(const NodePrefs* prefs) {
return prefs->direct_retry_prefs_magic[0] == DIRECT_RETRY_PREFS_MAGIC_0
&& prefs->direct_retry_prefs_magic[1] == DIRECT_RETRY_PREFS_MAGIC_1;
}
static bool parseRetryPreset(const char* s, uint8_t& preset) {
if (strcmp(s, "infra") == 0 || strcmp(s, "0") == 0) {
preset = RETRY_PRESET_INFRA;
return true;
}
if (strcmp(s, "rooftop") == 0 || strcmp(s, "1") == 0) {
preset = RETRY_PRESET_ROOFTOP;
return true;
}
if (strcmp(s, "mobile") == 0 || strcmp(s, "2") == 0) {
preset = RETRY_PRESET_MOBILE;
return true;
}
return false;
}
static bool parseHashPrefix(const char* text, uint8_t* prefix, uint8_t& prefix_len) {
size_t hex_len = strlen(text);
if (hex_len == 0 || (hex_len & 1) || hex_len > MAX_HASH_SIZE * 2) {
return false;
}
prefix_len = hex_len / 2;
return mesh::Utils::fromHex(prefix, prefix_len, text);
}
void CommonCLI::loadPrefs(FILESYSTEM* fs) {
if (fs->exists("/com_prefs")) {
loadPrefsInt(fs, "/com_prefs"); // new filename
@@ -93,7 +200,18 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
file.read((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292
file.read((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 293
file.read((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294
// next: 295
file.read((uint8_t *)&_prefs->retry_preset, sizeof(_prefs->retry_preset)); // 295
file.read((uint8_t *)&_prefs->direct_retry_attempts, sizeof(_prefs->direct_retry_attempts)); // 296
file.read((uint8_t *)&_prefs->direct_retry_base_ms, sizeof(_prefs->direct_retry_base_ms)); // 297
file.read((uint8_t *)&_prefs->direct_retry_step_ms, sizeof(_prefs->direct_retry_step_ms)); // 299
file.read((uint8_t *)&_prefs->direct_retry_snr_margin_x4, sizeof(_prefs->direct_retry_snr_margin_x4)); // 301
file.read((uint8_t *)&_prefs->direct_retry_cr4_snr_x4, sizeof(_prefs->direct_retry_cr4_snr_x4)); // 303
file.read((uint8_t *)&_prefs->direct_retry_cr5_snr_x4, sizeof(_prefs->direct_retry_cr5_snr_x4)); // 304
file.read((uint8_t *)&_prefs->direct_retry_cr7_snr_x4, sizeof(_prefs->direct_retry_cr7_snr_x4)); // 305
file.read((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 306
file.read((uint8_t *)&_prefs->direct_retry_cr_enabled, sizeof(_prefs->direct_retry_cr_enabled)); // 307
file.read((uint8_t *)&_prefs->direct_retry_prefs_magic, sizeof(_prefs->direct_retry_prefs_magic)); // 308
// next: 310
// sanitise bad pref values
_prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f);
@@ -125,6 +243,17 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
_prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean
_prefs->radio_fem_rxgain = constrain(_prefs->radio_fem_rxgain, 0, 1); // boolean
_prefs->cad_enabled = constrain(_prefs->cad_enabled, 0, 1); // boolean
if (!directRetryPrefsValid(_prefs)) {
setDefaultDirectRetryPrefs(_prefs);
}
if (_prefs->retry_preset > RETRY_PRESET_MOBILE && _prefs->retry_preset != RETRY_PRESET_CUSTOM) {
_prefs->retry_preset = RETRY_PRESET_CUSTOM;
}
_prefs->direct_retry_attempts = constrain(_prefs->direct_retry_attempts, 1, 15);
_prefs->direct_retry_base_ms = constrain(_prefs->direct_retry_base_ms, 10, 5000);
_prefs->direct_retry_step_ms = constrain(_prefs->direct_retry_step_ms, 0, 5000);
_prefs->direct_retry_snr_margin_x4 = constrain(_prefs->direct_retry_snr_margin_x4, 0, 160);
_prefs->direct_retry_cr_enabled = constrain(_prefs->direct_retry_cr_enabled, 0, 1);
file.close();
}
@@ -190,7 +319,19 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
file.write((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292
file.write((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 293
file.write((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294
// next: 295
markDirectRetryPrefsValid(_prefs);
file.write((uint8_t *)&_prefs->retry_preset, sizeof(_prefs->retry_preset)); // 295
file.write((uint8_t *)&_prefs->direct_retry_attempts, sizeof(_prefs->direct_retry_attempts)); // 296
file.write((uint8_t *)&_prefs->direct_retry_base_ms, sizeof(_prefs->direct_retry_base_ms)); // 297
file.write((uint8_t *)&_prefs->direct_retry_step_ms, sizeof(_prefs->direct_retry_step_ms)); // 299
file.write((uint8_t *)&_prefs->direct_retry_snr_margin_x4, sizeof(_prefs->direct_retry_snr_margin_x4)); // 301
file.write((uint8_t *)&_prefs->direct_retry_cr4_snr_x4, sizeof(_prefs->direct_retry_cr4_snr_x4)); // 303
file.write((uint8_t *)&_prefs->direct_retry_cr5_snr_x4, sizeof(_prefs->direct_retry_cr5_snr_x4)); // 304
file.write((uint8_t *)&_prefs->direct_retry_cr7_snr_x4, sizeof(_prefs->direct_retry_cr7_snr_x4)); // 305
file.write((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 306
file.write((uint8_t *)&_prefs->direct_retry_cr_enabled, sizeof(_prefs->direct_retry_cr_enabled)); // 307
file.write((uint8_t *)&_prefs->direct_retry_prefs_magic, sizeof(_prefs->direct_retry_prefs_magic)); // 308
// next: 310
file.close();
}
@@ -301,6 +442,9 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
} else if (memcmp(command, "clear stats", 11) == 0) {
_callbacks->clearStats();
strcpy(reply, "(OK - stats reset)");
} else if (memcmp(command, "clear recent.repeater", 21) == 0 && (command[21] == 0 || command[21] == ' ')) {
_callbacks->clearRecentRepeaters();
strcpy(reply, "OK");
} else if (memcmp(command, "get ", 4) == 0) {
handleGetCmd(sender_timestamp, command, reply);
} else if (memcmp(command, "set ", 4) == 0) {
@@ -680,6 +824,104 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
} else {
strcpy(reply, "Error, must be 0-2");
}
} else if (memcmp(config, "retry.preset ", 13) == 0) {
uint8_t preset;
if (parseRetryPreset(&config[13], preset)) {
applyDirectRetryPreset(_prefs, preset);
savePrefs();
sprintf(reply, "OK - %s", retryPresetName(_prefs->retry_preset));
} else {
strcpy(reply, "Error, must be infra, rooftop, or mobile");
}
} else if (memcmp(config, "direct.retry.margin ", 20) == 0) {
if (!looksNumeric(&config[20])) {
strcpy(reply, "Error, must be 0-40 dB");
} else {
int16_t margin_x4 = parseSnrDbX4(&config[20]);
if (margin_x4 >= 0 && margin_x4 <= 160) {
_prefs->direct_retry_snr_margin_x4 = (uint16_t)margin_x4;
_prefs->retry_preset = RETRY_PRESET_CUSTOM;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, must be 0-40 dB");
}
}
} else if (memcmp(config, "direct.retry.count ", 19) == 0) {
int attempts = _atoi(&config[19]);
if (attempts >= 1 && attempts <= 15) {
_prefs->direct_retry_attempts = (uint8_t)attempts;
_prefs->retry_preset = RETRY_PRESET_CUSTOM;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, must be 1-15");
}
} else if (memcmp(config, "direct.retry.base ", 18) == 0) {
int base_ms = _atoi(&config[18]);
if (base_ms >= 10 && base_ms <= 5000) {
_prefs->direct_retry_base_ms = (uint16_t)base_ms;
_prefs->retry_preset = RETRY_PRESET_CUSTOM;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, must be 10-5000 ms");
}
} else if (memcmp(config, "direct.retry.step ", 18) == 0) {
int step_ms = _atoi(&config[18]);
if (step_ms >= 0 && step_ms <= 5000) {
_prefs->direct_retry_step_ms = (uint16_t)step_ms;
_prefs->retry_preset = RETRY_PRESET_CUSTOM;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, must be 0-5000 ms");
}
} else if (memcmp(config, "direct.retry.cr ", 16) == 0) {
if (memcmp(&config[16], "off", 3) == 0) {
_prefs->direct_retry_cr_enabled = 0;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(tmp, &config[16]);
const char *parts[4];
int num = mesh::Utils::parseTextParts(tmp, parts, 4, ',');
if (num == 4 && looksNumeric(parts[0]) && looksNumeric(parts[1]) && looksNumeric(parts[2]) && looksNumeric(parts[3])) {
int16_t cr4 = parseSnrDbX4(parts[0]);
int16_t cr5 = parseSnrDbX4(parts[1]);
int16_t cr7 = parseSnrDbX4(parts[2]);
int16_t cr8 = parseSnrDbX4(parts[3]);
if (cr4 >= -128 && cr4 <= 127 && cr5 >= -128 && cr5 <= 127 && cr7 >= -128 && cr7 <= 127 && cr8 >= -128 && cr8 <= 127) {
_prefs->direct_retry_cr4_snr_x4 = (int8_t)cr4;
_prefs->direct_retry_cr5_snr_x4 = (int8_t)cr5;
_prefs->direct_retry_cr7_snr_x4 = (int8_t)cr7;
_prefs->direct_retry_cr8_snr_x4 = (int8_t)cr8;
_prefs->direct_retry_cr_enabled = 1;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, SNR must fit -32.00..31.75 dB");
}
} else {
strcpy(reply, "Error, use CR4,CR5,CR7,CR8 SNRs or off");
}
}
} else if (memcmp(config, "recent.repeater ", 16) == 0) {
strcpy(tmp, &config[16]);
const char *parts[2];
int num = mesh::Utils::parseTextParts(tmp, parts, 2, ' ');
uint8_t prefix[MAX_HASH_SIZE];
uint8_t prefix_len = 0;
int16_t snr_x4 = num > 1 && looksNumeric(parts[1]) ? parseSnrDbX4(parts[1]) : 0;
if (num != 2 || !parseHashPrefix(parts[0], prefix, prefix_len)) {
strcpy(reply, "Error, use: set recent.repeater <hex> <snr_db>");
} else if (snr_x4 < -128 || snr_x4 > 127) {
strcpy(reply, "Error, SNR must fit -32.00..31.75 dB");
} else if (_callbacks->setRecentRepeater(prefix, prefix_len, (int8_t)snr_x4)) {
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, table rejected prefix");
}
} else if (memcmp(config, "owner.info ", 11) == 0) {
config += 11;
char *dp = _prefs->owner_info;
@@ -862,6 +1104,37 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
sprintf(reply, "> %d", (uint32_t)_prefs->flood_max);
} else if (memcmp(config, "direct.txdelay", 14) == 0) {
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor));
} else if (memcmp(config, "retry.preset", 12) == 0) {
sprintf(reply, "> %s", retryPresetName(_prefs->retry_preset));
} else if (memcmp(config, "direct.retry.margin", 19) == 0) {
char margin[12];
formatSnrDbX4(margin, sizeof(margin), _prefs->direct_retry_snr_margin_x4);
sprintf(reply, "> %s", margin);
} else if (memcmp(config, "direct.retry.count", 18) == 0) {
sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_attempts);
} else if (memcmp(config, "direct.retry.base", 17) == 0) {
sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_base_ms);
} else if (memcmp(config, "direct.retry.step", 17) == 0) {
sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_step_ms);
} else if (memcmp(config, "direct.retry.cr", 15) == 0) {
if (!_prefs->direct_retry_cr_enabled) {
strcpy(reply, "> off");
} else {
char cr4[12], cr5[12], cr7[12], cr8[12];
formatSnrDbX4(cr4, sizeof(cr4), _prefs->direct_retry_cr4_snr_x4);
formatSnrDbX4(cr5, sizeof(cr5), _prefs->direct_retry_cr5_snr_x4);
formatSnrDbX4(cr7, sizeof(cr7), _prefs->direct_retry_cr7_snr_x4);
formatSnrDbX4(cr8, sizeof(cr8), _prefs->direct_retry_cr8_snr_x4);
sprintf(reply, "> %s,%s,%s,%s", cr4, cr5, cr7, cr8);
}
} else if (memcmp(config, "recent.repeater", 15) == 0) {
int page = 1;
const char* cursor = &config[15];
while (*cursor == ' ') cursor++;
if (memcmp(cursor, "page ", 5) == 0) cursor += 5;
if (*cursor) page = _atoi(cursor);
if (page < 1) page = 1;
_callbacks->formatRecentRepeatersReply(reply, page);
} else if (memcmp(config, "owner.info", 10) == 0) {
auto start = reply;
*reply++ = '>';
+51
View File
@@ -19,6 +19,34 @@
#define LOOP_DETECT_MODERATE 2
#define LOOP_DETECT_STRICT 3
#define RETRY_PRESET_INFRA 0
#define RETRY_PRESET_ROOFTOP 1
#define RETRY_PRESET_MOBILE 2
#define RETRY_PRESET_CUSTOM 0xFF
#define DIRECT_RETRY_INFRA_BASE_MS 275
#define DIRECT_RETRY_INFRA_COUNT 4
#define DIRECT_RETRY_INFRA_STEP_MS 150
#define DIRECT_RETRY_INFRA_MARGIN_X4 60
#define DIRECT_RETRY_ROOFTOP_BASE_MS 175
#define DIRECT_RETRY_ROOFTOP_COUNT 15
#define DIRECT_RETRY_ROOFTOP_STEP_MS 100
#define DIRECT_RETRY_ROOFTOP_MARGIN_X4 20
#define DIRECT_RETRY_MOBILE_BASE_MS 175
#define DIRECT_RETRY_MOBILE_COUNT 15
#define DIRECT_RETRY_MOBILE_STEP_MS 50
#define DIRECT_RETRY_MOBILE_MARGIN_X4 0
#define DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT 40
#define DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT 30
#define DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT 10
#define DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT 10
#define DIRECT_RETRY_PREFS_MAGIC_0 0xD1
#define DIRECT_RETRY_PREFS_MAGIC_1 0x52
struct NodePrefs { // persisted to file
float airtime_factor;
char node_name[32];
@@ -65,6 +93,17 @@ struct NodePrefs { // persisted to file
uint8_t path_hash_mode; // which path mode to use when sending
uint8_t loop_detect;
uint8_t cad_enabled; // hardware Channel Activity Detection before TX (boolean)
uint8_t retry_preset;
uint8_t direct_retry_attempts;
uint16_t direct_retry_base_ms;
uint16_t direct_retry_step_ms;
uint16_t direct_retry_snr_margin_x4;
int8_t direct_retry_cr4_snr_x4;
int8_t direct_retry_cr5_snr_x4;
int8_t direct_retry_cr7_snr_x4;
int8_t direct_retry_cr8_snr_x4;
uint8_t direct_retry_cr_enabled;
uint8_t direct_retry_prefs_magic[2];
};
class CommonCLICallbacks {
@@ -88,6 +127,18 @@ public:
virtual void formatStatsReply(char *reply) = 0;
virtual void formatRadioStatsReply(char *reply) = 0;
virtual void formatPacketStatsReply(char *reply) = 0;
virtual void formatRecentRepeatersReply(char *reply, int page) {
(void)page;
if (reply != NULL) reply[0] = 0;
}
virtual bool setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4) {
(void)prefix;
(void)prefix_len;
(void)snr_x4;
return false;
}
virtual void clearRecentRepeaters() {
}
virtual mesh::LocalIdentity& getSelfId() = 0;
virtual void saveIdentity(const mesh::LocalIdentity& new_id) = 0;
virtual void clearStats() = 0;
+22 -86
View File
@@ -9,8 +9,7 @@
#include <FS.h>
#endif
#define MAX_PACKET_HASHES 128
#define MAX_PACKET_ACKS 64
#define MAX_PACKET_HASHES (128+32)
#ifndef MAX_RECENT_REPEATERS
// Platform defaults. Can be overridden with -D MAX_RECENT_REPEATERS=<n>.
#if defined(ESP32) || defined(ESP32_PLATFORM)
@@ -39,8 +38,6 @@ public:
private:
uint8_t _hashes[MAX_PACKET_HASHES*MAX_HASH_SIZE];
int _next_idx;
uint32_t _acks[MAX_PACKET_ACKS];
int _next_ack_idx;
uint32_t _direct_dups, _flood_dups;
RecentRepeaterInfo _recent_repeaters[MAX_RECENT_REPEATERS];
int _next_recent_repeater_idx;
@@ -48,20 +45,6 @@ private:
RecentRepeaterAllowFn _recent_repeater_allow_fn;
void* _recent_repeater_allow_ctx;
bool hasSeenAck(uint32_t ack) const {
for (int i = 0; i < MAX_PACKET_ACKS; i++) {
if (ack == _acks[i]) {
return true;
}
}
return false;
}
void storeAck(uint32_t ack) {
_acks[_next_ack_idx] = ack;
_next_ack_idx = (_next_ack_idx + 1) % MAX_PACKET_ACKS;
}
bool hasSeenHash(const uint8_t* hash) const {
const uint8_t* sp = _hashes;
for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) {
@@ -148,8 +131,6 @@ public:
SimpleMeshTables() {
memset(_hashes, 0, sizeof(_hashes));
_next_idx = 0;
memset(_acks, 0, sizeof(_acks));
_next_ack_idx = 0;
_direct_dups = _flood_dups = 0;
memset(_recent_repeaters, 0, sizeof(_recent_repeaters));
_next_recent_repeater_idx = 0;
@@ -162,8 +143,6 @@ public:
void restoreFrom(File f) {
f.read(_hashes, sizeof(_hashes));
f.read((uint8_t *) &_next_idx, sizeof(_next_idx));
f.read((uint8_t *) &_acks[0], sizeof(_acks));
f.read((uint8_t *) &_next_ack_idx, sizeof(_next_ack_idx));
// Recent repeater entries are intentionally not restored across boots.
// This avoids struct-layout migration issues and keeps stale path quality
// stats from persisting indefinitely.
@@ -173,31 +152,10 @@ public:
void saveTo(File f) {
f.write(_hashes, sizeof(_hashes));
f.write((const uint8_t *) &_next_idx, sizeof(_next_idx));
f.write((const uint8_t *) &_acks[0], sizeof(_acks));
f.write((const uint8_t *) &_next_ack_idx, sizeof(_next_ack_idx));
}
#endif
bool hasSeen(const mesh::Packet* packet) override {
if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) {
recordRecentRepeater(packet);
uint32_t ack;
memcpy(&ack, packet->payload, 4);
if (hasSeenAck(ack)) {
if (packet->isRouteDirect()) {
_direct_dups++; // keep some stats
} else {
_flood_dups++;
}
return true;
}
storeAck(ack);
return false;
}
uint8_t hash[MAX_HASH_SIZE];
packet->calculatePacketHash(hash);
@@ -217,15 +175,6 @@ public:
void markSent(const mesh::Packet* packet) override {
// Outbound packets must be marked as already-sent without teaching the recent-heard cache about ourselves.
if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) {
uint32_t ack;
memcpy(&ack, packet->payload, 4);
if (!hasSeenAck(ack)) {
storeAck(ack);
}
return;
}
uint8_t hash[MAX_HASH_SIZE];
packet->calculatePacketHash(hash);
if (!hasSeenHash(hash)) {
@@ -234,25 +183,14 @@ public:
}
void clear(const mesh::Packet* packet) override {
if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) {
uint32_t ack;
memcpy(&ack, packet->payload, 4);
for (int i = 0; i < MAX_PACKET_ACKS; i++) {
if (ack == _acks[i]) {
_acks[i] = 0;
break;
}
}
} else {
uint8_t hash[MAX_HASH_SIZE];
packet->calculatePacketHash(hash);
uint8_t hash[MAX_HASH_SIZE];
packet->calculatePacketHash(hash);
uint8_t* sp = _hashes;
for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) {
if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) {
memset(sp, 0, MAX_HASH_SIZE);
break;
}
uint8_t* sp = _hashes;
for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) {
if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) {
memset(sp, 0, MAX_HASH_SIZE);
break;
}
}
}
@@ -282,17 +220,13 @@ public:
return false;
}
// Keep one slot for overlapping prefixes so 1/2/3-byte paths share the same entry.
// Keep exact prefixes distinct so a 1-byte path prefix does not collapse
// independent 2/3-byte repeaters that share the same first byte.
for (int i = 0; i < MAX_RECENT_REPEATERS; i++) {
RecentRepeaterInfo& existing = _recent_repeaters[i];
if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) {
if (existing.prefix_len != prefix_len || memcmp(existing.prefix, prefix, prefix_len) != 0) {
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 (snr_locked) {
existing.snr_x4 = snr_x4;
existing.snr_locked = 1;
@@ -352,14 +286,9 @@ public:
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)) {
if (existing.prefix_len != prefix_len || memcmp(existing.prefix, prefix, prefix_len) != 0) {
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) {
@@ -432,18 +361,25 @@ public:
return NULL;
}
// Search newest-to-oldest and allow 1/2/3-byte prefixes to overlap-match.
// Prefer exact matches. If none exists, fall back to the newest longest
// overlapping prefix so coarse learned prefixes can still inform CR.
const RecentRepeaterInfo* best = NULL;
for (int i = 0; i < MAX_RECENT_REPEATERS; i++) {
int idx = (_next_recent_repeater_idx - 1 - i + MAX_RECENT_REPEATERS) % MAX_RECENT_REPEATERS;
const RecentRepeaterInfo* info = &_recent_repeaters[idx];
if (info->prefix_len == 0) {
continue;
}
if (prefixesOverlap(info->prefix, info->prefix_len, hash, hash_len)) {
if (info->prefix_len == hash_len && memcmp(info->prefix, hash, hash_len) == 0) {
return info;
}
if (prefixesOverlap(info->prefix, info->prefix_len, hash, hash_len)) {
if (best == NULL || info->prefix_len > best->prefix_len) {
best = info;
}
}
}
return NULL;
return best;
}
void clearRecentRepeaters() {
memset(_recent_repeaters, 0, sizeof(_recent_repeaters));