mirror of
https://github.com/agessaman/MeshCore.git
synced 2026-08-28 02:54:05 +00:00
Add flood retry controls
This commit is contained in:
+87
-1
@@ -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 <value>`
|
||||
|
||||
**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 <value>`
|
||||
|
||||
**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 <value>`
|
||||
|
||||
**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 <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 <state>`
|
||||
|
||||
**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.<bucket>`
|
||||
- `set flood.retry.bucket <bucket> <prefixes>`
|
||||
|
||||
**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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
+19
-6
@@ -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
|
||||
|
||||
+3
-2
@@ -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; }
|
||||
|
||||
+275
-5
@@ -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*>(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;
|
||||
|
||||
|
||||
+64
@@ -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.
|
||||
*/
|
||||
|
||||
+399
-2
@@ -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> <prefixes|none>", 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> <prefixes|none>", 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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <Mesh.h>
|
||||
#if ARDUINO
|
||||
#include <Arduino.h>
|
||||
#endif
|
||||
|
||||
#ifdef ESP32
|
||||
#include <FS.h>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user