diff --git a/docs/cli_commands.md b/docs/cli_commands.md index f9c08259..79d319ad 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -585,7 +585,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore - `rooftop` (`1`): `175 ms` base wait, `15` retries, `100 ms` added per retry, SNR gate is SF floor + `5 dB` - `mobile` (`2`): `175 ms` base wait, `15` retries, `50 ms` added per retry, SNR gate is the SF floor -**Note:** Selecting a preset copies those values into the retry settings. You can refine `direct.retry.margin`, `direct.retry.count`, `direct.retry.base`, or `direct.retry.step` afterward. Retry delay is `direct.txdelay` jitter + base wait + packet-length airtime wait + per-attempt step. +**Note:** Selecting a preset copies those values into the direct retry settings and also resets flood retry defaults. You can refine `direct.retry.margin`, `direct.retry.count`, `direct.retry.base`, `direct.retry.step`, `flood.retry.count`, or `flood.retry.path` afterward. Retry delay is `direct.txdelay` jitter + base wait + packet-length airtime wait + per-attempt step. --- @@ -754,6 +754,92 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +#### View or change the flood retry preset +**Usage:** +- `get flood.retry.preset` +- `set flood.retry.preset ` + +**Parameters:** +- `value`: `infra`|`rooftop`|`mobile` or `0`|`1`|`2` + +**Presets:** +- `infra` (`0`): `1` retry, path gate `1` +- `rooftop` (`1`): `3` retries, path gate `2` +- `mobile` (`2`): `3` retries, path gate `1` + +**Note:** This applies only the flood retry defaults. `set direct.retry.preset` also resets these flood retry defaults. + +--- + +#### View or change the number of flood retry attempts +**Usage:** +- `get flood.retry.count` +- `set flood.retry.count ` + +**Parameters:** +- `value`: Maximum retry attempts after initial flood TX (`0`-`3`) + +**Default:** `3` for `rooftop` and `mobile`, `1` for `infra` + +**Note:** `0` disables flood retry. + +--- + +#### View or change the flood retry path gate +**Usage:** +- `get flood.retry.path` +- `set flood.retry.path ` + +**Parameters:** +- `value`: Maximum flood path length eligible for retry (`0`-`63`), or `off` to disable the gate + +**Default:** `2` for `rooftop`, `1` for `infra` and `mobile` + +--- + +#### View or change flood retry target prefixes +**Usage:** +- `get flood.retry.prefixes` +- `set flood.retry.prefixes ` + +**Parameters:** +- `prefixes`: Comma-separated 3-byte hex prefixes, such as `A1B2C3,D4E5F6`; use `none` or `off` to clear + +**Default:** `none` + +**Note:** Prefixes are stored as 3 bytes. Flood retry skips packets whose path already contains a matching target prefix. When prefixes are configured, only a downstream echo from one of those target prefixes cancels a queued retry; when no prefixes are configured, any downstream echo cancels it. Matching works with 3-byte, 2-byte, or 1-byte flood paths by comparing the matching leading bytes. + +--- + +#### View or change flood retry bridge mode +**Usage:** +- `get flood.retry.bridge` +- `set flood.retry.bridge ` + +**Parameters:** +- `state`: `on` or `off` + +**Default:** `off` + +**Note:** Bridge mode uses bucket definitions instead of the single `flood.retry.prefixes` target list. If a flood comes from one fresh bucket, retry continues until every other fresh configured bucket has been heard or `flood.retry.count` is exhausted. + +--- + +#### View or change flood retry bridge buckets +**Usage:** +- `get flood.retry.bucket.` +- `set flood.retry.bucket ` + +**Parameters:** +- `bucket`: Bucket number (`1`-`6`) +- `prefixes`: Up to 8 comma-separated 3-byte hex prefixes, such as `AABBCC,223344`; use `none` or `off` to clear + +**Default:** all buckets empty + +**Note:** Prefixes are stored as 3 bytes but match 3-byte, 2-byte, and 1-byte flood paths by comparing leading bytes. Bucket prefixes are included in bridge retry logic only if they were heard in the recent repeater table within the last hour. + +--- + ### ACL #### Add, update or remove permissions for a companion diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 6b3e8d6a..404a26d6 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -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; diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 1a2f505c..80c41b02 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -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; diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index cccbd36c..a59f0be6 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -269,7 +269,10 @@ void Dispatcher::processRecvPacket(Packet* pkt) { uint8_t priority = (action >> 24) - 1; uint32_t _delay = action & 0xFFFFFF; - _mgr->queueOutbound(pkt, priority, futureMillis(_delay)); + if (!queueOutboundPacket(pkt, priority, _delay)) { + onSendFail(pkt); + releasePacket(pkt); + } } } @@ -320,7 +323,8 @@ void Dispatcher::checkSend() { if (len + outbound->payload_len > MAX_TRANS_UNIT) { MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): FATAL: Invalid packet queued... too long, len=%d", getLogDateTime(), len + outbound->payload_len); - _mgr->free(outbound); + onSendFail(outbound); + releasePacket(outbound); outbound = NULL; } else { memcpy(&raw[len], outbound->payload, outbound->payload_len); len += outbound->payload_len; @@ -332,6 +336,7 @@ void Dispatcher::checkSend() { MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): ERROR: send start failed!", getLogDateTime()); logTxFail(outbound, outbound->getRawLength()); + onSendFail(outbound); releasePacket(outbound); // return to pool outbound = NULL; @@ -369,13 +374,21 @@ void Dispatcher::releasePacket(Packet* packet) { _mgr->free(packet); } -void Dispatcher::sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis) { +bool Dispatcher::queueOutboundPacket(Packet* packet, uint8_t priority, uint32_t delay_millis) { if (!Packet::isValidPathLen(packet->path_len) || packet->payload_len > MAX_PACKET_PAYLOAD) { MESH_DEBUG_PRINTLN("%s Dispatcher::sendPacket(): ERROR: invalid packet... path_len=%d, payload_len=%d", getLogDateTime(), (uint32_t) packet->path_len, (uint32_t) packet->payload_len); - _mgr->free(packet); - } else { - _mgr->queueOutbound(packet, priority, futureMillis(delay_millis)); + return false; } + return _mgr->queueOutbound(packet, priority, futureMillis(delay_millis)); +} + +bool Dispatcher::sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis) { + if (!queueOutboundPacket(packet, priority, delay_millis)) { + onSendFail(packet); + releasePacket(packet); + return false; + } + return true; } // Utility function -- handles the case where millis() wraps around back to zero diff --git a/src/Dispatcher.h b/src/Dispatcher.h index 90ee5cdb..16a1a964 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -87,7 +87,7 @@ public: virtual Packet* allocNew() = 0; virtual void free(Packet* packet) = 0; - virtual void queueOutbound(Packet* packet, uint8_t priority, uint32_t scheduled_for) = 0; + virtual bool queueOutbound(Packet* packet, uint8_t priority, uint32_t scheduled_for) = 0; virtual Packet* getNextOutbound(uint32_t now) = 0; // by priority virtual int getOutboundCount(uint32_t now) const = 0; virtual int getOutboundTotal() const = 0; @@ -171,6 +171,7 @@ protected: virtual int getAGCResetInterval() const { return 0; } // disabled by default virtual unsigned long getDutyCycleWindowMs() const { return 3600000; } const Packet* getOutboundInFlight() const { return outbound; } + bool queueOutboundPacket(Packet* packet, uint8_t priority, uint32_t delay_millis); public: void begin(); @@ -178,7 +179,7 @@ public: Packet* obtainNewPacket(); void releasePacket(Packet* packet); - void sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis=0); + bool sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis=0); unsigned long getTotalAirTime() const { return total_air_time; } unsigned long getReceiveAirTime() const {return rx_air_time; } diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 6c9c8080..254523bc 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -5,6 +5,7 @@ namespace mesh { static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_DEFAULT = 15; static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX = 15; +static const uint8_t FLOOD_RETRY_MAX_ATTEMPTS_DEFAULT = 3; static uint8_t decodeTraceHashSize(uint8_t flags, uint8_t route_bytes) { uint8_t code = flags & 0x03; @@ -41,6 +42,18 @@ void Mesh::begin() { _direct_retries[i].queued = false; _direct_retries[i].active = false; } + for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { + _flood_retries[i].packet = NULL; + _flood_retries[i].trigger_packet = NULL; + _flood_retries[i].retry_started_at = 0; + _flood_retries[i].retry_at = 0; + _flood_retries[i].retry_delay = 0; + _flood_retries[i].retry_attempts_sent = 0; + _flood_retries[i].priority = 0; + _flood_retries[i].progress_marker = 0; + _flood_retries[i].queued = false; + _flood_retries[i].active = false; + } Dispatcher::begin(); } @@ -59,6 +72,19 @@ void Mesh::loop() { clearDirectRetrySlot(i); } } + + for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { + if (!_flood_retries[i].active || !_flood_retries[i].queued || !millisHasNowPassed(_flood_retries[i].retry_at)) { + continue; + } + + if (!isFloodRetryQueued(_flood_retries[i].packet)) { + if (_flood_retries[i].packet == getOutboundInFlight()) { + continue; + } + clearFloodRetrySlot(i); + } + } } bool Mesh::allowPacketForward(const mesh::Packet* packet) { @@ -87,16 +113,39 @@ uint32_t Mesh::getDirectRetryAttemptDelay(const Packet* packet, uint8_t attempt_ // Keep the historical linear spacing while allowing the base wait to vary by platform/profile. return base + ((uint32_t)attempt_idx * 100UL); } +bool Mesh::allowFloodRetry(const Packet* packet) const { + return true; +} +bool Mesh::hasFloodRetryTargetPrefix(const Packet* packet) const { + return false; +} +uint8_t Mesh::getFloodRetryMaxPathLength(const Packet* packet) const { + return 2; +} +uint8_t Mesh::getFloodRetryMaxAttempts(const Packet* packet) const { + return FLOOD_RETRY_MAX_ATTEMPTS_DEFAULT; +} +uint32_t Mesh::getFloodRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx) { + if (packet == NULL) { + return _radio->getEstAirtimeFor(MAX_TRANS_UNIT); + } + + uint32_t max_packet_airtime = _radio->getEstAirtimeFor(MAX_TRANS_UNIT); + uint32_t packet_airtime = _radio->getEstAirtimeFor(packet->getRawLength()); + return max_packet_airtime + (20UL * packet_airtime); +} uint8_t Mesh::getExtraAckTransmitCount() const { return 0; } void Mesh::onSendComplete(Packet* packet) { armDirectRetryOnSendComplete(packet); + armFloodRetryOnSendComplete(packet); } void Mesh::onSendFail(Packet* packet) { clearPendingDirectRetryOnSendFail(packet); + clearPendingFloodRetryOnSendFail(packet); } uint32_t Mesh::getCADFailRetryDelay() const { @@ -114,6 +163,8 @@ int Mesh::searchChannelsByHash(const uint8_t* hash, GroupChannel channels[], int DispatcherAction Mesh::onRecvPacket(Packet* pkt) { if (pkt->isRouteDirect()) { cancelDirectRetryOnEcho(pkt); + } else if (pkt->isRouteFlood()) { + cancelFloodRetryOnEcho(pkt); } if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_TRACE) { @@ -414,8 +465,10 @@ DispatcherAction Mesh::routeRecvPacket(Packet* packet) { packet->setPathHashCount(n + 1); uint32_t d = getRetransmitDelay(packet); + uint8_t priority = packet->getPathHashCount(); + maybeScheduleFloodRetry(packet, priority); // as this propagates outwards, give it lower and lower priority - return ACTION_RETRANSMIT_DELAYED(packet->getPathHashCount(), d); // give priority to closer sources, than ones further away + return ACTION_RETRANSMIT_DELAYED(priority, d); // give priority to closer sources, than ones further away } return ACTION_RELEASE; } @@ -589,8 +642,7 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { *retry = *packet; uint32_t retry_delay = getDirectRetryAttemptDelay(packet, _direct_retries[i].retry_attempts_sent); - sendPacket(retry, _direct_retries[i].priority, retry_delay); - if (isDirectRetryQueued(retry)) { + if (queueOutboundPacket(retry, _direct_retries[i].priority, retry_delay)) { _direct_retries[i].packet = retry; _direct_retries[i].retry_delay = retry_delay; _direct_retries[i].retry_at = futureMillis(retry_delay); @@ -598,6 +650,7 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { } else { onDirectRetryEvent("dropped_queue_full", retry, retry_delay, _direct_retries[i].retry_attempts_sent + 1); onDirectRetryEvent("failure", retry, elapsed_millis, _direct_retries[i].retry_attempts_sent + 1); + releasePacket(retry); clearDirectRetrySlot(i); } } @@ -620,8 +673,7 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { *retry = *packet; // Start the echo wait only after the initial direct transmission actually completed. - sendPacket(retry, _direct_retries[i].priority, _direct_retries[i].retry_delay); - if (isDirectRetryQueued(retry)) { + if (queueOutboundPacket(retry, _direct_retries[i].priority, _direct_retries[i].retry_delay)) { unsigned long now = _ms->getMillis(); _direct_retries[i].packet = retry; _direct_retries[i].trigger_packet = NULL; @@ -633,6 +685,7 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { } else { onDirectRetryEvent("dropped_queue_full", retry, _direct_retries[i].retry_delay, 1); onDirectRetryEvent("failure", retry, 0, 1); + releasePacket(retry); clearDirectRetrySlot(i); } } @@ -759,6 +812,223 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { _direct_retries[slot_idx].active = true; } +void Mesh::clearFloodRetrySlot(int idx) { + _flood_retries[idx].packet = NULL; + _flood_retries[idx].trigger_packet = NULL; + _flood_retries[idx].retry_started_at = 0; + _flood_retries[idx].retry_at = 0; + _flood_retries[idx].retry_delay = 0; + _flood_retries[idx].retry_attempts_sent = 0; + _flood_retries[idx].priority = 0; + _flood_retries[idx].progress_marker = 0; + _flood_retries[idx].queued = false; + _flood_retries[idx].active = false; +} + +bool Mesh::isFloodRetryQueued(const Packet* packet) const { + for (int i = 0; i < _mgr->getOutboundTotal(); i++) { + if (_mgr->getOutboundByIdx(i) == packet) { + return true; + } + } + return false; +} + +bool Mesh::isFloodRetryEchoTarget(const Packet* packet, uint8_t progress_marker) const { + return packet->isRouteFlood() && packet->getPathHashCount() > progress_marker; +} + +bool Mesh::cancelFloodRetryOnEcho(const Packet* packet) { + uint8_t recv_key[MAX_HASH_SIZE]; + packet->calculatePacketHash(recv_key); + + bool cleared = false; + for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { + if (!_flood_retries[i].active || memcmp(recv_key, _flood_retries[i].retry_key, MAX_HASH_SIZE) != 0) { + continue; + } + if (!isFloodRetryEchoTarget(packet, _flood_retries[i].progress_marker)) { + continue; + } + + uint32_t echo_millis = _flood_retries[i].retry_started_at == 0 + ? 0 + : (uint32_t)(_ms->getMillis() - _flood_retries[i].retry_started_at); + const Packet* event_packet = _flood_retries[i].queued ? _flood_retries[i].packet : _flood_retries[i].trigger_packet; + onFloodRetryEvent("good", event_packet, echo_millis, _flood_retries[i].retry_attempts_sent + 1); + + if (_flood_retries[i].queued) { + for (int j = 0; j < _mgr->getOutboundTotal(); j++) { + if (_mgr->getOutboundByIdx(j) == _flood_retries[i].packet) { + Packet* pending = _mgr->removeOutboundByIdx(j); + if (pending) { + releasePacket(pending); + } + break; + } + } + } + clearFloodRetrySlot(i); + cleared = true; + } + + return cleared; +} + +void Mesh::armFloodRetryOnSendComplete(const Packet* packet) { + for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { + if (!_flood_retries[i].active) { + continue; + } + + if (_flood_retries[i].queued) { + if (_flood_retries[i].packet != packet) { + continue; + } + + uint32_t elapsed_millis = _flood_retries[i].retry_started_at == 0 + ? 0 + : (uint32_t)(_ms->getMillis() - _flood_retries[i].retry_started_at); + onFloodRetryEvent("resent", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent + 1); + _flood_retries[i].retry_attempts_sent++; + + uint8_t max_attempts = getFloodRetryMaxAttempts(packet); + if (max_attempts < 1) { + max_attempts = 1; + } else if (max_attempts > FLOOD_RETRY_MAX_ATTEMPTS_DEFAULT) { + max_attempts = FLOOD_RETRY_MAX_ATTEMPTS_DEFAULT; + } + if (_flood_retries[i].retry_attempts_sent >= max_attempts) { + onFloodRetryEvent("failed_all_tries", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent); + onFloodRetryEvent("failure", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent); + clearFloodRetrySlot(i); + continue; + } + + Packet* retry = obtainNewPacket(); + if (retry == NULL) { + onFloodRetryEvent("dropped_no_packet", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent + 1); + onFloodRetryEvent("failure", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent + 1); + clearFloodRetrySlot(i); + continue; + } + + *retry = *packet; + uint32_t retry_delay = getFloodRetryAttemptDelay(packet, _flood_retries[i].retry_attempts_sent); + if (queueOutboundPacket(retry, _flood_retries[i].priority, retry_delay)) { + _flood_retries[i].packet = retry; + _flood_retries[i].retry_delay = retry_delay; + _flood_retries[i].retry_at = futureMillis(retry_delay); + _flood_retries[i].retry_started_at = _ms->getMillis(); + onFloodRetryEvent("queued", retry, retry_delay, _flood_retries[i].retry_attempts_sent + 1); + } else { + onFloodRetryEvent("dropped_queue_full", retry, retry_delay, _flood_retries[i].retry_attempts_sent + 1); + onFloodRetryEvent("failure", retry, elapsed_millis, _flood_retries[i].retry_attempts_sent + 1); + releasePacket(retry); + clearFloodRetrySlot(i); + } + continue; + } + + if (_flood_retries[i].trigger_packet != packet) { + continue; + } + + Packet* retry = obtainNewPacket(); + if (retry == NULL) { + onFloodRetryEvent("dropped_no_packet", packet, _flood_retries[i].retry_delay, 1); + onFloodRetryEvent("failure", packet, 0, 1); + clearFloodRetrySlot(i); + continue; + } + + *retry = *packet; + if (queueOutboundPacket(retry, _flood_retries[i].priority, _flood_retries[i].retry_delay)) { + unsigned long now = _ms->getMillis(); + _flood_retries[i].packet = retry; + _flood_retries[i].trigger_packet = NULL; + _flood_retries[i].queued = true; + _flood_retries[i].retry_at = futureMillis(_flood_retries[i].retry_delay); + _flood_retries[i].retry_started_at = now; + onFloodRetryEvent("queued", retry, _flood_retries[i].retry_delay, 1); + } else { + onFloodRetryEvent("dropped_queue_full", retry, _flood_retries[i].retry_delay, 1); + onFloodRetryEvent("failure", retry, 0, 1); + releasePacket(retry); + clearFloodRetrySlot(i); + } + } +} + +void Mesh::clearPendingFloodRetryOnSendFail(const Packet* packet) { + for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { + if (!_flood_retries[i].active) { + continue; + } + + if (_flood_retries[i].queued) { + if (_flood_retries[i].packet == packet) { + onFloodRetryEvent("dropped_send_fail", packet, 0, _flood_retries[i].retry_attempts_sent + 1); + onFloodRetryEvent("failure", packet, 0, _flood_retries[i].retry_attempts_sent + 1); + clearFloodRetrySlot(i); + } + continue; + } + + if (_flood_retries[i].trigger_packet == packet) { + onFloodRetryEvent("dropped_send_fail", packet, 0, 1); + onFloodRetryEvent("failure", packet, 0, 1); + clearFloodRetrySlot(i); + } + } +} + +void Mesh::maybeScheduleFloodRetry(const Packet* packet, uint8_t priority) { + if (packet == NULL || !packet->isRouteFlood() || hasFloodRetryTargetPrefix(packet)) { + return; + } + + uint8_t max_path_len = getFloodRetryMaxPathLength(packet); + if (max_path_len != FLOOD_RETRY_PATH_GATE_DISABLED && packet->getPathHashCount() > max_path_len) { + return; + } + + uint8_t max_attempts = getFloodRetryMaxAttempts(packet); + if (max_attempts == 0) { + return; + } + + int slot_idx = -1; + for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { + if (!_flood_retries[i].active) { + slot_idx = i; + break; + } + } + if (slot_idx < 0) { + onFloodRetryEvent("dropped_no_slot", packet, 0, 0); + onFloodRetryEvent("failure", packet, 0, 0); + return; + } + + if (!allowFloodRetry(packet)) { + return; + } + + uint32_t retry_delay = getFloodRetryAttemptDelay(packet, 0); + packet->calculatePacketHash(_flood_retries[slot_idx].retry_key); + _flood_retries[slot_idx].packet = NULL; + _flood_retries[slot_idx].trigger_packet = const_cast(packet); + _flood_retries[slot_idx].retry_started_at = 0; + _flood_retries[slot_idx].retry_at = 0; + _flood_retries[slot_idx].retry_delay = retry_delay; + _flood_retries[slot_idx].retry_attempts_sent = 0; + _flood_retries[slot_idx].priority = priority; + _flood_retries[slot_idx].progress_marker = packet->getPathHashCount(); + _flood_retries[slot_idx].queued = false; + _flood_retries[slot_idx].active = true; +} + Packet* Mesh::createAdvert(const LocalIdentity& id, const uint8_t* app_data, size_t app_data_len) { if (app_data_len > MAX_ADVERT_DATA_SIZE) return NULL; diff --git a/src/Mesh.h b/src/Mesh.h index 422b79ab..69f05195 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -8,6 +8,14 @@ namespace mesh { #define MAX_DIRECT_RETRY_SLOTS 6 #endif +#ifndef MAX_FLOOD_RETRY_SLOTS + #define MAX_FLOOD_RETRY_SLOTS 6 +#endif + +#ifndef FLOOD_RETRY_PATH_GATE_DISABLED + #define FLOOD_RETRY_PATH_GATE_DISABLED 0xFF +#endif + class GroupChannel { public: uint8_t hash[PATH_HASH_SIZE]; @@ -45,10 +53,25 @@ class Mesh : public Dispatcher { bool active; }; + struct FloodRetryEntry { + Packet* packet; + Packet* trigger_packet; + unsigned long retry_started_at; + unsigned long retry_at; + uint32_t retry_delay; + uint8_t retry_attempts_sent; + uint8_t retry_key[MAX_HASH_SIZE]; + uint8_t priority; + uint8_t progress_marker; + bool queued; + bool active; + }; + RTCClock* _rtc; RNG* _rng; MeshTables* _tables; DirectRetryEntry _direct_retries[MAX_DIRECT_RETRY_SLOTS]; + FloodRetryEntry _flood_retries[MAX_FLOOD_RETRY_SLOTS]; void removeSelfFromPath(Packet* packet); void routeDirectRecvAcks(Packet* packet, uint32_t delay_millis); @@ -61,6 +84,12 @@ class Mesh : public Dispatcher { 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; void maybeScheduleDirectRetry(const Packet* packet, uint8_t priority); + void clearFloodRetrySlot(int idx); + bool isFloodRetryQueued(const Packet* packet) const; + bool cancelFloodRetryOnEcho(const Packet* packet); + void armFloodRetryOnSendComplete(const Packet* packet); + void clearPendingFloodRetryOnSendFail(const Packet* packet); + void maybeScheduleFloodRetry(const Packet* packet, uint8_t priority); //void routeRecvAcks(Packet* packet, uint32_t delay_millis); DispatcherAction forwardMultipartDirect(Packet* pkt); @@ -119,6 +148,41 @@ protected: */ virtual uint32_t getDirectRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx); + /** + * \brief Decide whether a FLOOD packet should retry when no downstream echo is overheard. + */ + virtual bool allowFloodRetry(const Packet* packet) const; + + /** + * \brief Return true when this FLOOD packet already carries an application-defined target prefix. + */ + virtual bool hasFloodRetryTargetPrefix(const Packet* packet) const; + + /** + * \returns maximum flood path hash count eligible for retry, or FLOOD_RETRY_PATH_GATE_DISABLED. + */ + virtual uint8_t getFloodRetryMaxPathLength(const Packet* packet) const; + + /** + * \returns maximum number of FLOOD retry transmissions after the initial TX. + */ + virtual uint8_t getFloodRetryMaxAttempts(const Packet* packet) const; + + /** + * \brief Return true when a received FLOOD echo is enough to cancel a pending retry. + */ + virtual bool isFloodRetryEchoTarget(const Packet* packet, uint8_t progress_marker) const; + + /** + * \returns delay before a specific flood retry attempt, where attempt_idx=0 is the first retry. + */ + virtual uint32_t getFloodRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx); + + /** + * \brief Optional hook for logging flood-retry lifecycle events. + */ + virtual void onFloodRetryEvent(const char* event, const Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) { } + /** * \returns number of extra (Direct) ACK transmissions wanted. */ diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index dc206516..27b2eeb4 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -30,6 +30,10 @@ #define DIRECT_RETRY_STEP_MS_MIN 0 #define DIRECT_RETRY_STEP_MS_MAX 5000 #define DIRECT_RETRY_PRESET_DEFAULT DIRECT_RETRY_PRESET_ROOFTOP +#define FLOOD_RETRY_PREFS_MAGIC_0 0xF4 +#define FLOOD_RETRY_PREFS_MAGIC_1 0x52 +#define FLOOD_RETRY_COUNT_MIN 0 +#define FLOOD_RETRY_COUNT_MAX 3 // Believe it or not, this std C function is busted on some platforms! static uint32_t _atoi(const char* sp) { @@ -41,6 +45,30 @@ static uint32_t _atoi(const char* sp) { return n; } +static bool parseUint8Strict(const char* value, uint8_t min_value, uint8_t max_value, uint8_t& result) { + if (value == NULL || *value == 0) { + return false; + } + + uint16_t parsed = 0; + const char* sp = value; + while (*sp) { + if (*sp < '0' || *sp > '9') { + return false; + } + parsed = (uint16_t)((parsed * 10) + (*sp - '0')); + if (parsed > max_value) { + return false; + } + sp++; + } + if (parsed < min_value) { + return false; + } + result = (uint8_t)parsed; + return true; +} + static uint8_t directRetryMarginDbToX4(float margin_db) { int32_t scaled_x4 = (int32_t)((margin_db * 4.0f) + 0.5f); // nearest 0.25 dB return (uint8_t)constrain(scaled_x4, 0, DIRECT_RETRY_SNR_MARGIN_X4_MAX); @@ -97,6 +125,49 @@ static uint16_t directRetryPresetStepDefault(uint8_t preset) { } } +static uint8_t floodRetryPresetCountDefault(uint8_t preset) { + switch (directRetryPresetOrDefault(preset)) { + case DIRECT_RETRY_PRESET_INFRA: + return 1; + case DIRECT_RETRY_PRESET_MOBILE: + case DIRECT_RETRY_PRESET_ROOFTOP: + default: + return 3; + } +} + +static uint8_t floodRetryPresetPathDefault(uint8_t preset) { + switch (directRetryPresetOrDefault(preset)) { + case DIRECT_RETRY_PRESET_INFRA: + return 1; + case DIRECT_RETRY_PRESET_MOBILE: + return 1; + case DIRECT_RETRY_PRESET_ROOFTOP: + default: + return 2; + } +} + +static void applyFloodRetryPreset(NodePrefs* prefs, uint8_t preset) { + prefs->flood_retry_attempts = floodRetryPresetCountDefault(preset); + prefs->flood_retry_path_gate = floodRetryPresetPathDefault(preset); +} + +static uint8_t floodRetryEffectiveCount(const NodePrefs* prefs) { + return constrain(prefs->flood_retry_attempts, (uint8_t)FLOOD_RETRY_COUNT_MIN, (uint8_t)FLOOD_RETRY_COUNT_MAX); +} + +static uint8_t floodRetryPresetForPrefs(const NodePrefs* prefs) { + uint8_t count = floodRetryEffectiveCount(prefs); + uint8_t path_gate = prefs->flood_retry_path_gate; + for (uint8_t preset = DIRECT_RETRY_PRESET_INFRA; preset <= DIRECT_RETRY_PRESET_MOBILE; preset++) { + if (count == floodRetryPresetCountDefault(preset) && path_gate == floodRetryPresetPathDefault(preset)) { + return preset; + } + } + return directRetryPresetOrDefault(prefs->direct_retry_preset); +} + static void applyDirectRetryPreset(NodePrefs* prefs, uint8_t preset) { prefs->direct_retry_preset = directRetryPresetOrDefault(preset); switch (prefs->direct_retry_preset) { @@ -120,6 +191,138 @@ static void applyDirectRetryPreset(NodePrefs* prefs, uint8_t preset) { prefs->direct_retry_snr_margin_db = DIRECT_RETRY_ROOFTOP_MARGIN_X4; break; } + applyFloodRetryPreset(prefs, prefs->direct_retry_preset); +} + +static bool parseFloodRetryPathGate(const char* value, uint8_t& path_gate) { + if (value == NULL) { + return false; + } + if (strcmp(value, "off") == 0 || strcmp(value, "disabled") == 0 || strcmp(value, "disable") == 0) { + path_gate = FLOOD_RETRY_PATH_GATE_DISABLED; + return true; + } + return parseUint8Strict(value, 0, 63, path_gate); +} + +static void formatFloodRetryPathGate(char* dest, uint8_t path_gate) { + if (path_gate == FLOOD_RETRY_PATH_GATE_DISABLED) { + strcpy(dest, "off"); + } else { + sprintf(dest, "%u", (unsigned int)path_gate); + } +} + +static void formatFloodRetryPrefixes(char* dest, const NodePrefs* prefs) { + char* out = dest; + bool first = true; + for (int i = 0; i < FLOOD_RETRY_PREFIX_SLOTS; i++) { + const uint8_t* prefix = prefs->flood_retry_prefixes[i]; + if (prefix[0] == 0 && prefix[1] == 0 && prefix[2] == 0) { + continue; + } + if (!first) { + *out++ = ','; + } + mesh::Utils::toHex(out, prefix, FLOOD_RETRY_PREFIX_LEN); + out += FLOOD_RETRY_PREFIX_LEN * 2; + first = false; + } + *out = 0; +} + +static void formatFloodRetryBridgeBucket(char* dest, const NodePrefs* prefs, uint8_t bucket) { + char* out = dest; + bool first = true; + if (bucket >= FLOOD_RETRY_BRIDGE_BUCKETS) { + *out = 0; + return; + } + for (int i = 0; i < FLOOD_RETRY_BUCKET_PREFIXES; i++) { + const uint8_t* prefix = prefs->flood_retry_bridge_buckets[bucket][i]; + if (prefix[0] == 0 && prefix[1] == 0 && prefix[2] == 0) { + continue; + } + if (!first) { + *out++ = ','; + } + mesh::Utils::toHex(out, prefix, FLOOD_RETRY_PREFIX_LEN); + out += FLOOD_RETRY_PREFIX_LEN * 2; + first = false; + } + *out = 0; +} + +static bool parseFloodRetryPrefixes(NodePrefs* prefs, const char* value) { + uint8_t parsed[FLOOD_RETRY_PREFIX_SLOTS][FLOOD_RETRY_PREFIX_LEN]; + memset(parsed, 0, sizeof(parsed)); + if (value == NULL || value[0] == 0 || strcmp(value, "none") == 0 || strcmp(value, "off") == 0) { + memcpy(prefs->flood_retry_prefixes, parsed, sizeof(prefs->flood_retry_prefixes)); + return true; + } + + char tmp[96]; + StrHelper::strncpy(tmp, value, sizeof(tmp)); + const char* parts[FLOOD_RETRY_PREFIX_SLOTS + 1]; + int num = mesh::Utils::parseTextParts(tmp, parts, FLOOD_RETRY_PREFIX_SLOTS + 1); + if (num > FLOOD_RETRY_PREFIX_SLOTS) { + return false; + } + for (int i = 0; i < num; i++) { + if (strlen(parts[i]) != FLOOD_RETRY_PREFIX_LEN * 2) { + return false; + } + for (int j = 0; j < FLOOD_RETRY_PREFIX_LEN * 2; j++) { + if (!mesh::Utils::isHexChar(parts[i][j])) { + return false; + } + } + if (!mesh::Utils::fromHex(parsed[i], FLOOD_RETRY_PREFIX_LEN, parts[i])) { + return false; + } + const uint8_t* prefix = parsed[i]; + if (prefix[0] == 0 && prefix[1] == 0 && prefix[2] == 0) { + return false; + } + } + memcpy(prefs->flood_retry_prefixes, parsed, sizeof(prefs->flood_retry_prefixes)); + return true; +} + +static bool parseFloodRetryPrefixList(uint8_t dest[][FLOOD_RETRY_PREFIX_LEN], uint8_t max_prefixes, const char* value) { + if (max_prefixes > FLOOD_RETRY_BUCKET_PREFIXES) { + return false; + } + uint8_t parsed[FLOOD_RETRY_BUCKET_PREFIXES][FLOOD_RETRY_PREFIX_LEN]; + memset(parsed, 0, sizeof(parsed)); + if (value == NULL || value[0] == 0 || strcmp(value, "none") == 0 || strcmp(value, "off") == 0) { + memcpy(dest, parsed, max_prefixes * FLOOD_RETRY_PREFIX_LEN); + return true; + } + + char tmp[96]; + StrHelper::strncpy(tmp, value, sizeof(tmp)); + const char* parts[FLOOD_RETRY_BUCKET_PREFIXES + 1]; + int num = mesh::Utils::parseTextParts(tmp, parts, FLOOD_RETRY_BUCKET_PREFIXES + 1); + if (num > max_prefixes) { + return false; + } + for (int i = 0; i < num; i++) { + if (strlen(parts[i]) != FLOOD_RETRY_PREFIX_LEN * 2) { + return false; + } + for (int j = 0; j < FLOOD_RETRY_PREFIX_LEN * 2; j++) { + if (!mesh::Utils::isHexChar(parts[i][j])) { + return false; + } + } + if (!mesh::Utils::fromHex(parsed[i], FLOOD_RETRY_PREFIX_LEN, parts[i]) + || (parsed[i][0] == 0 && parsed[i][1] == 0 && parsed[i][2] == 0)) { + return false; + } + } + memcpy(dest, parsed, max_prefixes * FLOOD_RETRY_PREFIX_LEN); + return true; } static bool parseDirectRetryPreset(const char* value, uint8_t& preset) { @@ -227,6 +430,13 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->direct_retry_preset, sizeof(_prefs->direct_retry_preset)); // 297 size_t retry_step_read = file.read((uint8_t *)&_prefs->direct_retry_step_ms, sizeof(_prefs->direct_retry_step_ms)); // 298 + size_t flood_retry_attempts_read = file.read((uint8_t *)&_prefs->flood_retry_attempts, + sizeof(_prefs->flood_retry_attempts)); // 300 + file.read((uint8_t *)&_prefs->flood_retry_path_gate, sizeof(_prefs->flood_retry_path_gate)); // 301 + file.read((uint8_t *)&_prefs->flood_retry_prefs_magic[0], sizeof(_prefs->flood_retry_prefs_magic)); // 302 + file.read((uint8_t *)&_prefs->flood_retry_prefixes[0][0], sizeof(_prefs->flood_retry_prefixes)); // 304 + file.read((uint8_t *)&_prefs->flood_retry_bridge_enabled, sizeof(_prefs->flood_retry_bridge_enabled)); // 328 + file.read((uint8_t *)&_prefs->flood_retry_bridge_buckets[0][0][0], sizeof(_prefs->flood_retry_bridge_buckets)); // 329 // PowerSaving-only prefs stored radio_fem_rxgain at 291, before direct retry timing existed. if (radio_fem_rxgain_read != sizeof(_prefs->radio_fem_rxgain) && legacy_retry_attempts_read == sizeof(legacy_retry_attempts_or_radio_fem_rxgain) @@ -234,7 +444,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { || _prefs->direct_retry_timing_magic[1] != DIRECT_RETRY_TIMING_MAGIC_1)) { _prefs->radio_fem_rxgain = constrain(legacy_retry_attempts_or_radio_fem_rxgain, 0, 1); } - // next: 298 + // next: 473 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -288,6 +498,20 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { } else { _prefs->direct_retry_step_ms = constrain(_prefs->direct_retry_step_ms, DIRECT_RETRY_STEP_MS_MIN, DIRECT_RETRY_STEP_MS_MAX); } + if (flood_retry_attempts_read != sizeof(_prefs->flood_retry_attempts) + || _prefs->flood_retry_prefs_magic[0] != FLOOD_RETRY_PREFS_MAGIC_0 + || _prefs->flood_retry_prefs_magic[1] != FLOOD_RETRY_PREFS_MAGIC_1) { + applyFloodRetryPreset(_prefs, _prefs->direct_retry_preset); + memset(_prefs->flood_retry_prefixes, 0, sizeof(_prefs->flood_retry_prefixes)); + _prefs->flood_retry_bridge_enabled = 0; + memset(_prefs->flood_retry_bridge_buckets, 0, sizeof(_prefs->flood_retry_bridge_buckets)); + } else { + _prefs->flood_retry_attempts = constrain(_prefs->flood_retry_attempts, FLOOD_RETRY_COUNT_MIN, FLOOD_RETRY_COUNT_MAX); + if (_prefs->flood_retry_path_gate > 63 && _prefs->flood_retry_path_gate != FLOOD_RETRY_PATH_GATE_DISABLED) { + _prefs->flood_retry_path_gate = floodRetryPresetPathDefault(_prefs->direct_retry_preset); + } + _prefs->flood_retry_bridge_enabled = constrain(_prefs->flood_retry_bridge_enabled, 0, 1); + } file.close(); } @@ -360,7 +584,14 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 296 file.write((uint8_t *)&_prefs->direct_retry_preset, sizeof(_prefs->direct_retry_preset)); // 297 file.write((uint8_t *)&_prefs->direct_retry_step_ms, sizeof(_prefs->direct_retry_step_ms)); // 298 - // next: 300 + file.write((uint8_t *)&_prefs->flood_retry_attempts, sizeof(_prefs->flood_retry_attempts)); // 300 + file.write((uint8_t *)&_prefs->flood_retry_path_gate, sizeof(_prefs->flood_retry_path_gate)); // 301 + uint8_t flood_retry_magic[2] = { FLOOD_RETRY_PREFS_MAGIC_0, FLOOD_RETRY_PREFS_MAGIC_1 }; + file.write(flood_retry_magic, sizeof(flood_retry_magic)); // 302 + file.write((uint8_t *)&_prefs->flood_retry_prefixes[0][0], sizeof(_prefs->flood_retry_prefixes)); // 304 + file.write((uint8_t *)&_prefs->flood_retry_bridge_enabled, sizeof(_prefs->flood_retry_bridge_enabled)); // 328 + file.write((uint8_t *)&_prefs->flood_retry_bridge_buckets[0][0][0], sizeof(_prefs->flood_retry_bridge_buckets)); // 329 + // next: 473 file.close(); } @@ -516,6 +747,30 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re sprintf(reply, "> %s", StrHelper::ftoa(_prefs->tx_delay_factor)); } else if (memcmp(config, "flood.max", 9) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->flood_max); + } else if (memcmp(config, "flood.retry.count", 17) == 0) { + sprintf(reply, "> %d", (uint32_t)floodRetryEffectiveCount(_prefs)); + } else if (memcmp(config, "flood.retry.path", 16) == 0) { + char path_gate[8]; + formatFloodRetryPathGate(path_gate, _prefs->flood_retry_path_gate); + sprintf(reply, "> %s", path_gate); + } else if (memcmp(config, "flood.retry.prefixes", 20) == 0) { + formatFloodRetryPrefixes(tmp, _prefs); + sprintf(reply, "> %s", tmp[0] ? tmp : "none"); + } else if (memcmp(config, "flood.retry.bridge", 18) == 0) { + sprintf(reply, "> %s", _prefs->flood_retry_bridge_enabled ? "on" : "off"); + } else if (memcmp(config, "flood.retry.bucket.", 19) == 0) { + uint8_t bucket = atoi(&config[19]); + if (bucket >= 1 && bucket <= FLOOD_RETRY_BRIDGE_BUCKETS) { + formatFloodRetryBridgeBucket(tmp, _prefs, bucket - 1); + sprintf(reply, "> %s", tmp[0] ? tmp : "none"); + } else { + sprintf(reply, "Error, bucket 1-%d", FLOOD_RETRY_BRIDGE_BUCKETS); + } + } else if (memcmp(config, "flood.retry.preset", 18) == 0) { + uint8_t preset = floodRetryPresetForPrefs(_prefs); + sprintf(reply, "> %d,%s", + (uint32_t)preset, + directRetryPresetName(preset)); } else if (memcmp(config, "direct.txdelay", 14) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor)); } else if (memcmp(config, "direct.retry.heard", 18) == 0) { @@ -772,6 +1027,65 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else { strcpy(reply, "Error, max 64"); } + } else if (memcmp(config, "flood.retry.preset ", 19) == 0) { + uint8_t preset; + if (parseDirectRetryPreset(&config[19], preset)) { + applyFloodRetryPreset(_prefs, preset); + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be infra, rooftop, mobile, 0, 1, or 2"); + } + } else if (memcmp(config, "flood.retry.count ", 18) == 0) { + uint8_t count; + if (parseUint8Strict(&config[18], FLOOD_RETRY_COUNT_MIN, FLOOD_RETRY_COUNT_MAX, count)) { + _prefs->flood_retry_attempts = count; + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error, min %d and max %d", FLOOD_RETRY_COUNT_MIN, FLOOD_RETRY_COUNT_MAX); + } + } else if (memcmp(config, "flood.retry.path ", 17) == 0) { + uint8_t path_gate; + if (parseFloodRetryPathGate(&config[17], path_gate)) { + _prefs->flood_retry_path_gate = path_gate; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be 0-63 or off"); + } + } else if (memcmp(config, "flood.retry.prefixes ", 21) == 0) { + if (parseFloodRetryPrefixes(_prefs, &config[21])) { + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, use comma-separated 3-byte hex prefixes"); + } + } else if (memcmp(config, "flood.retry.bridge ", 19) == 0) { + if (memcmp(&config[19], "on", 2) == 0) { + _prefs->flood_retry_bridge_enabled = 1; + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(&config[19], "off", 3) == 0) { + _prefs->flood_retry_bridge_enabled = 0; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be on or off"); + } + } else if (memcmp(config, "flood.retry.bucket ", 19) == 0) { + const char* params = &config[19]; + uint8_t bucket = atoi(params); + const char* list = strchr(params, ' '); + if (bucket < 1 || bucket > FLOOD_RETRY_BRIDGE_BUCKETS || list == NULL || *(list + 1) == 0) { + sprintf(reply, "Error, usage: set flood.retry.bucket <1-%d> ", FLOOD_RETRY_BRIDGE_BUCKETS); + } else if (parseFloodRetryPrefixList(_prefs->flood_retry_bridge_buckets[bucket - 1], + FLOOD_RETRY_BUCKET_PREFIXES, list + 1)) { + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, use up to 8 comma-separated 3-byte hex prefixes"); + } } else if (memcmp(config, "direct.txdelay ", 15) == 0) { float f = atof(&config[15]); if (f >= 0) { @@ -1326,6 +1640,65 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else { strcpy(reply, "Error, max 64"); } + } else if (memcmp(config, "flood.retry.preset ", 19) == 0) { + uint8_t preset; + if (parseDirectRetryPreset(&config[19], preset)) { + applyFloodRetryPreset(_prefs, preset); + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be infra, rooftop, mobile, 0, 1, or 2"); + } + } else if (memcmp(config, "flood.retry.count ", 18) == 0) { + uint8_t count; + if (parseUint8Strict(&config[18], FLOOD_RETRY_COUNT_MIN, FLOOD_RETRY_COUNT_MAX, count)) { + _prefs->flood_retry_attempts = count; + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error, min %d and max %d", FLOOD_RETRY_COUNT_MIN, FLOOD_RETRY_COUNT_MAX); + } + } else if (memcmp(config, "flood.retry.path ", 17) == 0) { + uint8_t path_gate; + if (parseFloodRetryPathGate(&config[17], path_gate)) { + _prefs->flood_retry_path_gate = path_gate; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be 0-63 or off"); + } + } else if (memcmp(config, "flood.retry.prefixes ", 21) == 0) { + if (parseFloodRetryPrefixes(_prefs, &config[21])) { + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, use comma-separated 3-byte hex prefixes"); + } + } else if (memcmp(config, "flood.retry.bridge ", 19) == 0) { + if (memcmp(&config[19], "on", 2) == 0) { + _prefs->flood_retry_bridge_enabled = 1; + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(&config[19], "off", 3) == 0) { + _prefs->flood_retry_bridge_enabled = 0; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be on or off"); + } + } else if (memcmp(config, "flood.retry.bucket ", 19) == 0) { + const char* params = &config[19]; + uint8_t bucket = atoi(params); + const char* list = strchr(params, ' '); + if (bucket < 1 || bucket > FLOOD_RETRY_BRIDGE_BUCKETS || list == NULL || *(list + 1) == 0) { + sprintf(reply, "Error, usage: set flood.retry.bucket <1-%d> ", FLOOD_RETRY_BRIDGE_BUCKETS); + } else if (parseFloodRetryPrefixList(_prefs->flood_retry_bridge_buckets[bucket - 1], + FLOOD_RETRY_BUCKET_PREFIXES, list + 1)) { + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, use up to 8 comma-separated 3-byte hex prefixes"); + } } else if (memcmp(config, "direct.txdelay ", 15) == 0) { float f = atof(&config[15]); if (f >= 0) { @@ -1565,6 +1938,30 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep sprintf(reply, "> %s", StrHelper::ftoa(_prefs->tx_delay_factor)); } else if (memcmp(config, "flood.max", 9) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->flood_max); + } else if (memcmp(config, "flood.retry.count", 17) == 0) { + sprintf(reply, "> %d", (uint32_t)floodRetryEffectiveCount(_prefs)); + } else if (memcmp(config, "flood.retry.path", 16) == 0) { + char path_gate[8]; + formatFloodRetryPathGate(path_gate, _prefs->flood_retry_path_gate); + sprintf(reply, "> %s", path_gate); + } else if (memcmp(config, "flood.retry.prefixes", 20) == 0) { + formatFloodRetryPrefixes(tmp, _prefs); + sprintf(reply, "> %s", tmp[0] ? tmp : "none"); + } else if (memcmp(config, "flood.retry.bridge", 18) == 0) { + sprintf(reply, "> %s", _prefs->flood_retry_bridge_enabled ? "on" : "off"); + } else if (memcmp(config, "flood.retry.bucket.", 19) == 0) { + uint8_t bucket = atoi(&config[19]); + if (bucket >= 1 && bucket <= FLOOD_RETRY_BRIDGE_BUCKETS) { + formatFloodRetryBridgeBucket(tmp, _prefs, bucket - 1); + sprintf(reply, "> %s", tmp[0] ? tmp : "none"); + } else { + sprintf(reply, "Error, bucket 1-%d", FLOOD_RETRY_BRIDGE_BUCKETS); + } + } else if (memcmp(config, "flood.retry.preset", 18) == 0) { + uint8_t preset = floodRetryPresetForPrefs(_prefs); + sprintf(reply, "> %d,%s", + (uint32_t)preset, + directRetryPresetName(preset)); } else if (memcmp(config, "direct.txdelay", 14) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor)); } else if (memcmp(config, "direct.retry.heard", 18) == 0) { diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index ea30777a..321c3c8f 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -38,6 +38,19 @@ #define DIRECT_RETRY_MOBILE_STEP_MS 50 #define DIRECT_RETRY_MOBILE_MARGIN_X4 0 +#ifndef FLOOD_RETRY_PREFIX_SLOTS + #define FLOOD_RETRY_PREFIX_SLOTS 8 +#endif +#ifndef FLOOD_RETRY_PREFIX_LEN + #define FLOOD_RETRY_PREFIX_LEN 3 +#endif +#ifndef FLOOD_RETRY_BRIDGE_BUCKETS + #define FLOOD_RETRY_BRIDGE_BUCKETS 6 +#endif +#ifndef FLOOD_RETRY_BUCKET_PREFIXES + #define FLOOD_RETRY_BUCKET_PREFIXES 8 +#endif + struct NodePrefs { // persisted to file float airtime_factor; char node_name[32]; @@ -88,6 +101,12 @@ struct NodePrefs { // persisted to file uint8_t direct_retry_timing_magic[2]; uint8_t direct_retry_preset; uint16_t direct_retry_step_ms; + uint8_t flood_retry_attempts; + uint8_t flood_retry_path_gate; + uint8_t flood_retry_prefs_magic[2]; + uint8_t flood_retry_prefixes[FLOOD_RETRY_PREFIX_SLOTS][FLOOD_RETRY_PREFIX_LEN]; + uint8_t flood_retry_bridge_enabled; + uint8_t flood_retry_bridge_buckets[FLOOD_RETRY_BRIDGE_BUCKETS][FLOOD_RETRY_BUCKET_PREFIXES][FLOOD_RETRY_PREFIX_LEN]; }; class CommonCLICallbacks { diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index ae28acc8..ca81b144 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -1,6 +1,9 @@ #pragma once #include +#if ARDUINO + #include +#endif #ifdef ESP32 #include @@ -30,6 +33,7 @@ public: uint8_t prefix_len; int8_t snr_x4; uint8_t snr_locked; + uint32_t last_heard_millis; }; private: @@ -178,6 +182,8 @@ public: bool hasSeen(const mesh::Packet* packet) override { if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) { + recordRecentRepeater(packet); + uint32_t ack; memcpy(&ack, packet->payload, 4); @@ -295,6 +301,11 @@ public: } else if (!existing.snr_locked) { existing.snr_x4 = weightedSnrX4RoundUp(existing.snr_x4, snr_x4); } +#if ARDUINO + existing.last_heard_millis = millis(); +#else + existing.last_heard_millis = 0; +#endif return true; } @@ -325,6 +336,11 @@ public: slot.prefix_len = prefix_len; slot.snr_x4 = snr_x4; slot.snr_locked = snr_locked ? 1 : 0; +#if ARDUINO + slot.last_heard_millis = millis(); +#else + slot.last_heard_millis = 0; +#endif _next_recent_repeater_idx = (slot_idx + 1) % MAX_RECENT_REPEATERS; return true; } diff --git a/src/helpers/StaticPoolPacketManager.cpp b/src/helpers/StaticPoolPacketManager.cpp index b8926df0..fc2bb059 100644 --- a/src/helpers/StaticPoolPacketManager.cpp +++ b/src/helpers/StaticPoolPacketManager.cpp @@ -83,11 +83,12 @@ void StaticPoolPacketManager::free(mesh::Packet* packet) { unused.add(packet, 0, 0); } -void StaticPoolPacketManager::queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) { +bool StaticPoolPacketManager::queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) { if (!send_queue.add(packet, priority, scheduled_for)) { MESH_DEBUG_PRINTLN("queueOutbound: send queue full, dropping packet"); - free(packet); + return false; } + return true; } mesh::Packet* StaticPoolPacketManager::getNextOutbound(uint32_t now) { diff --git a/src/helpers/StaticPoolPacketManager.h b/src/helpers/StaticPoolPacketManager.h index 59715b4e..350e85d2 100644 --- a/src/helpers/StaticPoolPacketManager.h +++ b/src/helpers/StaticPoolPacketManager.h @@ -26,7 +26,7 @@ public: mesh::Packet* allocNew() override; void free(mesh::Packet* packet) override; - void queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) override; + bool queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) override; mesh::Packet* getNextOutbound(uint32_t now) override; int getOutboundCount(uint32_t now) const override; int getOutboundTotal() const override; @@ -35,4 +35,4 @@ public: mesh::Packet* removeOutboundByIdx(int i) override; void queueInbound(mesh::Packet* packet, uint32_t scheduled_for) override; mesh::Packet* getNextInbound(uint32_t now) override; -}; \ No newline at end of file +};