Add flood retry controls

This commit is contained in:
mikecarper committed 2026-05-07 11:48:29 -07:00
1 parent 674beb0e47
commit d4febe7867
12 files changed
+1204 -20

No files matched your search

+293
View File
@@ -854,6 +854,180 @@ uint8_t MyMesh::getDirectRetryConfiguredMaxAttempts() const {
uint32_t MyMesh::getDirectRetryAttemptStepMillis() const {
return constrain((uint32_t)_prefs.direct_retry_step_ms, (uint32_t)0, (uint32_t)5000);
}
bool MyMesh::hasFloodRetryPrefixes() const {
for (int i = 0; i < FLOOD_RETRY_PREFIX_SLOTS; i++) {
const uint8_t* configured = _prefs.flood_retry_prefixes[i];
if (configured[0] != 0 || configured[1] != 0 || configured[2] != 0) {
return true;
}
}
return false;
}
bool MyMesh::floodRetryLastHopMatches(const mesh::Packet* packet) const {
if (packet == NULL || packet->getPathHashCount() == 0) {
return false;
}
uint8_t hash_size = packet->getPathHashSize();
if (hash_size == 0 || hash_size > MAX_ROUTE_HASH_BYTES) {
return false;
}
const uint8_t* heard_prefix = &packet->path[(packet->getPathHashCount() - 1) * hash_size];
for (int i = 0; i < FLOOD_RETRY_PREFIX_SLOTS; i++) {
const uint8_t* configured = _prefs.flood_retry_prefixes[i];
if ((configured[0] != 0 || configured[1] != 0 || configured[2] != 0)
&& memcmp(configured, heard_prefix, hash_size) == 0) {
return true;
}
}
return false;
}
bool MyMesh::floodRetryPrefixMatches(const mesh::Packet* packet) const {
if (packet == NULL || packet->getPathHashCount() == 0) {
return false;
}
uint8_t hash_size = packet->getPathHashSize();
if (hash_size == 0 || hash_size > MAX_ROUTE_HASH_BYTES) {
return false;
}
const uint8_t* path = packet->path;
for (int hop = 0; hop < packet->getPathHashCount(); hop++) {
for (int i = 0; i < FLOOD_RETRY_PREFIX_SLOTS; i++) {
const uint8_t* configured = _prefs.flood_retry_prefixes[i];
if ((configured[0] != 0 || configured[1] != 0 || configured[2] != 0)
&& memcmp(configured, path, hash_size) == 0) {
return true;
}
}
path += hash_size;
}
return false;
}
bool MyMesh::floodRetryPrefixFresh(const uint8_t* prefix, uint8_t prefix_len) const {
const auto* recent = ((const SimpleMeshTables *)getTables())->findRecentRepeaterByHash(prefix, prefix_len);
if (recent == NULL || recent->last_heard_millis == 0) {
return false;
}
return (uint32_t)(millis() - recent->last_heard_millis) <= 3600000UL;
}
int MyMesh::floodRetryBucketForPrefix(const uint8_t* prefix, uint8_t prefix_len, bool require_fresh) const {
if (prefix == NULL || prefix_len == 0 || prefix_len > MAX_ROUTE_HASH_BYTES) {
return -1;
}
if (require_fresh && !floodRetryPrefixFresh(prefix, prefix_len)) {
return -1;
}
for (int bucket = 0; bucket < FLOOD_RETRY_BRIDGE_BUCKETS; bucket++) {
for (int i = 0; i < FLOOD_RETRY_BUCKET_PREFIXES; i++) {
const uint8_t* configured = _prefs.flood_retry_bridge_buckets[bucket][i];
if ((configured[0] != 0 || configured[1] != 0 || configured[2] != 0)
&& memcmp(configured, prefix, prefix_len) == 0) {
return bucket;
}
}
}
return -1;
}
int MyMesh::floodRetrySourceBucket(const mesh::Packet* packet) const {
if (packet == NULL || packet->getPathHashCount() < 2) {
return -1;
}
uint8_t hash_size = packet->getPathHashSize();
if (hash_size == 0 || hash_size > MAX_ROUTE_HASH_BYTES) {
return -1;
}
const uint8_t* source_prefix = &packet->path[(packet->getPathHashCount() - 2) * hash_size];
return floodRetryBucketForPrefix(source_prefix, hash_size, true);
}
uint8_t MyMesh::floodRetryBridgeTargetMask(uint8_t source_bucket) const {
uint8_t mask = 0;
for (int bucket = 0; bucket < FLOOD_RETRY_BRIDGE_BUCKETS; bucket++) {
if (bucket == source_bucket) {
continue;
}
for (int i = 0; i < FLOOD_RETRY_BUCKET_PREFIXES; i++) {
const uint8_t* configured = _prefs.flood_retry_bridge_buckets[bucket][i];
if ((configured[0] != 0 || configured[1] != 0 || configured[2] != 0)
&& floodRetryPrefixFresh(configured, FLOOD_RETRY_PREFIX_LEN)) {
mask |= (uint8_t)(1U << bucket);
break;
}
}
}
return mask;
}
uint8_t MyMesh::floodRetryBridgeHeardMask(const mesh::Packet* packet, uint8_t source_bucket) const {
if (packet == NULL || packet->getPathHashCount() == 0) {
return 0;
}
uint8_t hash_size = packet->getPathHashSize();
if (hash_size == 0 || hash_size > MAX_ROUTE_HASH_BYTES) {
return 0;
}
uint8_t mask = 0;
const uint8_t* path = packet->path;
for (int hop = 0; hop < packet->getPathHashCount(); hop++) {
int bucket = floodRetryBucketForPrefix(path, hash_size, true);
if (bucket >= 0 && bucket != source_bucket) {
mask |= (uint8_t)(1U << bucket);
}
path += hash_size;
}
return mask;
}
MyMesh::FloodRetryBridgeState* MyMesh::floodRetryBridgeStateFor(const mesh::Packet* packet, bool create) const {
if (packet == NULL) {
return NULL;
}
uint8_t key[MAX_HASH_SIZE];
packet->calculatePacketHash(key);
FloodRetryBridgeState* free_slot = NULL;
for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) {
if (flood_retry_bridge_states[i].active
&& memcmp(flood_retry_bridge_states[i].key, key, MAX_HASH_SIZE) == 0) {
return &flood_retry_bridge_states[i];
}
if (!flood_retry_bridge_states[i].active && free_slot == NULL) {
free_slot = &flood_retry_bridge_states[i];
}
}
if (!create) {
return NULL;
}
if (free_slot == NULL) {
return NULL;
}
int source_bucket = floodRetrySourceBucket(packet);
if (source_bucket < 0) {
return NULL;
}
uint8_t target_mask = floodRetryBridgeTargetMask((uint8_t)source_bucket);
if (target_mask == 0) {
return NULL;
}
uint8_t heard_mask = floodRetryBridgeHeardMask(packet, (uint8_t)source_bucket) & target_mask;
if ((heard_mask & target_mask) == target_mask) {
return NULL;
}
memset(free_slot, 0, sizeof(*free_slot));
memcpy(free_slot->key, key, sizeof(free_slot->key));
free_slot->source_bucket = (uint8_t)source_bucket;
free_slot->target_mask = target_mask;
free_slot->heard_mask = heard_mask;
free_slot->active = true;
return free_slot;
}
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);
if (packet == NULL) {
@@ -871,6 +1045,121 @@ uint32_t MyMesh::getDirectRetryEchoDelay(const mesh::Packet* packet) const {
uint32_t scaled_wait_millis = (uint32_t) ((((float) bits) * 4.0f) / kbps);
return base_wait_millis + scaled_wait_millis;
}
bool MyMesh::allowFloodRetry(const mesh::Packet* packet) const {
if (_prefs.disable_fwd || constrain(_prefs.flood_retry_attempts, (uint8_t)0, (uint8_t)3) == 0) {
return false;
}
if (!_prefs.flood_retry_bridge_enabled) {
return true;
}
FloodRetryBridgeState* state = floodRetryBridgeStateFor(packet, true);
if (state == NULL) {
return false;
}
if ((state->heard_mask & state->target_mask) == state->target_mask) {
state->active = false;
return false;
}
return true;
}
void MyMesh::clearFloodRetryBridgeState(const mesh::Packet* packet) {
FloodRetryBridgeState* state = floodRetryBridgeStateFor(packet, false);
if (state != NULL) {
state->active = false;
}
}
void MyMesh::onFloodRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) {
if (event == NULL || packet == NULL) {
return;
}
const char* time_label = "time_ms";
if (strcmp(event, "queued") == 0 || strcmp(event, "dropped_queue_full") == 0) {
time_label = "wait_ms";
} else if (strcmp(event, "resent") == 0 || strcmp(event, "failed_all_tries") == 0
|| strcmp(event, "failure") == 0 || strncmp(event, "dropped_", 8) == 0) {
time_label = "elapsed_ms";
} else if (strcmp(event, "good") == 0) {
time_label = "echo_ms";
}
MESH_DEBUG_PRINTLN("%s flood retry %s (retry=%u, type=%d, route=%s, payload_len=%d, hop=%u, %s=%lu)",
getLogDateTime(),
event,
(unsigned int)retry_attempt,
(uint32_t)packet->getPayloadType(),
packet->isRouteDirect() ? "D" : "F",
(uint32_t)packet->payload_len,
(unsigned int)packet->getPathHashCount(),
time_label,
(unsigned long)delay_millis);
if (_logging) {
File f = openAppend(PACKET_LOG_FILE);
if (f) {
f.print(getLogDateTime());
f.printf(": FLOOD RETRY %s (retry=%u, type=%d, route=%s, payload_len=%d, hop=%u, %s=%lu)\n",
event,
(unsigned int)retry_attempt,
(uint32_t)packet->getPayloadType(),
packet->isRouteDirect() ? "D" : "F",
(uint32_t)packet->payload_len,
(unsigned int)packet->getPathHashCount(),
time_label,
(unsigned long)delay_millis);
f.close();
}
}
if (_prefs.flood_retry_bridge_enabled
&& (strcmp(event, "failure") == 0 || strcmp(event, "failed_all_tries") == 0
|| strncmp(event, "dropped_", 8) == 0)) {
clearFloodRetryBridgeState(packet);
}
}
bool MyMesh::hasFloodRetryTargetPrefix(const mesh::Packet* packet) const {
if (_prefs.flood_retry_bridge_enabled) {
return false;
}
return floodRetryPrefixMatches(packet);
}
uint8_t MyMesh::getFloodRetryMaxPathLength(const mesh::Packet* packet) const {
uint8_t gate = _prefs.flood_retry_path_gate;
if (gate == FLOOD_RETRY_PATH_GATE_DISABLED) {
return FLOOD_RETRY_PATH_GATE_DISABLED;
}
return gate <= 63 ? gate : 2;
}
uint8_t MyMesh::getFloodRetryMaxAttempts(const mesh::Packet* packet) const {
if (_prefs.disable_fwd) {
return 0;
}
return constrain(_prefs.flood_retry_attempts, (uint8_t)0, (uint8_t)3);
}
bool MyMesh::isFloodRetryEchoTarget(const mesh::Packet* packet, uint8_t progress_marker) const {
if (packet == NULL || !packet->isRouteFlood()) {
return false;
}
if (_prefs.flood_retry_bridge_enabled) {
FloodRetryBridgeState* state = floodRetryBridgeStateFor(packet, false);
if (state == NULL) {
return false;
}
state->heard_mask |= floodRetryBridgeHeardMask(packet, state->source_bucket) & state->target_mask;
bool complete = (state->heard_mask & state->target_mask) == state->target_mask;
if (complete) {
state->active = false;
}
return complete;
}
if (hasFloodRetryPrefixes()) {
return floodRetryLastHopMatches(packet);
}
if (packet->getPathHashCount() <= progress_marker) {
return false;
}
return true;
}
uint8_t MyMesh::getDirectRetryMaxAttempts(const mesh::Packet* packet) const {
uint8_t configured_attempts = getDirectRetryConfiguredMaxAttempts();
uint8_t total_hops = 0;
@@ -1222,6 +1511,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
set_radio_at = revert_radio_at = 0;
_logging = false;
region_load_active = false;
memset(flood_retry_bridge_states, 0, sizeof(flood_retry_bridge_states));
#if MAX_NEIGHBOURS
memset(neighbours, 0, sizeof(neighbours));
@@ -1239,6 +1529,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.direct_retry_base_ms = DIRECT_RETRY_ROOFTOP_BASE_MS;
_prefs.direct_retry_step_ms = DIRECT_RETRY_ROOFTOP_STEP_MS;
_prefs.direct_retry_preset = DIRECT_RETRY_PRESET_ROOFTOP;
_prefs.flood_retry_attempts = 3;
_prefs.flood_retry_path_gate = 2;
_prefs.flood_retry_bridge_enabled = 0;
StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name));
_prefs.node_lat = ADVERT_LAT;
_prefs.node_lon = ADVERT_LON;
+24
View File
@@ -99,6 +99,14 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
RegionEntry* recv_pkt_region;
TransportKey default_scope;
RateLimiter discover_limiter, anon_limiter;
struct FloodRetryBridgeState {
uint8_t key[MAX_HASH_SIZE];
uint8_t source_bucket;
uint8_t target_mask;
uint8_t heard_mask;
bool active;
};
mutable FloodRetryBridgeState flood_retry_bridge_states[MAX_FLOOD_RETRY_SLOTS];
uint32_t pending_discover_tag;
unsigned long pending_discover_until;
bool region_load_active;
@@ -127,6 +135,16 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
uint8_t getDirectRetryPreset() const;
uint8_t getDirectRetryConfiguredMaxAttempts() const;
uint32_t getDirectRetryAttemptStepMillis() const;
bool hasFloodRetryPrefixes() const;
bool floodRetryPrefixMatches(const mesh::Packet* packet) const;
bool floodRetryLastHopMatches(const mesh::Packet* packet) const;
bool floodRetryPrefixFresh(const uint8_t* prefix, uint8_t prefix_len) const;
int floodRetryBucketForPrefix(const uint8_t* prefix, uint8_t prefix_len, bool require_fresh) const;
int floodRetrySourceBucket(const mesh::Packet* packet) const;
uint8_t floodRetryBridgeTargetMask(uint8_t source_bucket) const;
uint8_t floodRetryBridgeHeardMask(const mesh::Packet* packet, uint8_t source_bucket) const;
FloodRetryBridgeState* floodRetryBridgeStateFor(const mesh::Packet* packet, bool create) const;
void clearFloodRetryBridgeState(const mesh::Packet* packet);
void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr);
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood);
uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data);
@@ -159,6 +177,12 @@ protected:
uint8_t getDirectRetryMaxAttempts(const mesh::Packet* packet) const override;
uint32_t getDirectRetryAttemptDelay(const mesh::Packet* packet, uint8_t attempt_idx) override;
void onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) override;
bool allowFloodRetry(const mesh::Packet* packet) const override;
void onFloodRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) override;
bool hasFloodRetryTargetPrefix(const mesh::Packet* packet) const override;
uint8_t getFloodRetryMaxPathLength(const mesh::Packet* packet) const override;
uint8_t getFloodRetryMaxAttempts(const mesh::Packet* packet) const override;
bool isFloodRetryEchoTarget(const mesh::Packet* packet, uint8_t progress_marker) const override;
int getInterferenceThreshold() const override {
return _prefs.interference_threshold;