Max retries is now a var that can be set between 1 to 15

This commit is contained in:
mikecarper
2026-04-24 00:08:10 -07:00
parent 577433ce47
commit 9c2ac5aa1c
8 changed files with 284 additions and 29 deletions
+36 -3
View File
@@ -117,14 +117,21 @@ This document provides an overview of CLI commands that can be sent to MeshCore
### Get or set recent repeater fallback prefix/SNR
**Usage:**
- `recent.repeater`
- `recent.repeater <prefix_hex> <snr_db>`
- `get recent.repeater`
- `get recent.repeater all`
- `get recent.repeater first <count>`
- `get recent.repeater last <count>`
- `set recent.repeater <prefix_hex> <snr_db>`
**Parameters:**
- `prefix_hex`: 1-3 bytes of next-hop prefix (hex)
- `snr_db`: SNR in dB (supports decimals; stored at x4 precision)
- `count`: number of entries to print
**Note:** `set` is rejected when the prefix already exists in neighbors.
**Notes:**
- `set` is rejected when the prefix already exists in neighbors.
- `all` prints oldest to newest; `first` prints the oldest N; `last` prints the newest N.
- Remote CLI replies include rows too, but may truncate when the packet payload limit is reached.
---
@@ -545,6 +552,32 @@ This document provides an overview of CLI commands that can be sent to MeshCore
---
#### View or change the number of direct retry attempts
**Usage:**
- `get direct.retry.count`
- `set direct.retry.count <value>`
**Parameters:**
- `value`: Retry attempts after initial TX (`1`-`15`)
**Default:** `3`
---
#### View or change the base direct retry wait (milliseconds)
**Usage:**
- `get direct.retry.base`
- `set direct.retry.base <value>`
**Parameters:**
- `value`: Base wait in milliseconds (`10`-`5000`)
**Default:** `200`
**Note:** The actual first retry wait is `base + computed_echo_wait_from_live_phy`.
---
#### [Experimental] View or change the processing delay for received traffic
**Usage:**
- `get rxdelay`
+119 -17
View File
@@ -627,16 +627,21 @@ bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_ho
return recent != NULL && recent->snr_x4 >= min_snr_x4;
}
uint32_t MyMesh::getDirectRetryEchoDelay(const mesh::Packet* packet) const {
uint32_t base_wait_millis = constrain((uint32_t)_prefs.direct_retry_base_ms, (uint32_t)10, (uint32_t)5000);
// Approximate LoRa line rate in kilobits/sec from the live radio params the repeater is using now.
float kbps = (((float) active_sf) * active_bw * ((float) active_cr)) / ((float) (1UL << active_sf));
if (kbps <= 0.0f) {
return HALO_DIRECT_RETRY_DELAY_MIN;
return base_wait_millis;
}
// Wait roughly long enough for our transmission, the next hop's receive/forward window, and its echo back.
uint32_t bits = ((uint32_t) packet->getRawLength()) * 8;
uint32_t scaled_wait_millis = (uint32_t) ((((float) bits) * 4.0f) / kbps);
return max((uint32_t) HALO_DIRECT_RETRY_DELAY_MIN, scaled_wait_millis);
return base_wait_millis + scaled_wait_millis;
}
uint8_t MyMesh::getDirectRetryMaxAttempts(const mesh::Packet* packet) const {
(void)packet;
return constrain(_prefs.direct_retry_attempts, (uint8_t)1, (uint8_t)15);
}
bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) {
@@ -970,6 +975,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.direct_tx_delay_factor = 0.3f; // was 0.2
_prefs.direct_retry_recent_enabled = 0;
_prefs.direct_retry_snr_margin_db = 5;
_prefs.direct_retry_attempts = 3;
_prefs.direct_retry_base_ms = 200;
StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name));
_prefs.node_lat = ADVERT_LAT;
_prefs.node_lon = ADVERT_LON;
@@ -1345,29 +1352,36 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
Serial.printf("\n");
}
reply[0] = 0;
} else if (memcmp(command, "recent.repeater", 15) == 0) {
const char* sub = command + 15;
while (*sub == ' ') sub++;
auto* tables = (SimpleMeshTables*)getTables();
if (*sub == 0) {
const auto* info = tables->getLatestRecentRepeater();
if (info == NULL) {
strcpy(reply, "> none");
} else {
char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1];
mesh::Utils::toHex(hex, info->prefix, info->prefix_len);
sprintf(reply, "> %s,%s", hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f));
}
} else if (strncmp(command, "get recent.repeater", 19) == 0
|| strncmp(command, "set recent.repeater", 19) == 0
|| strncmp(command, "recent.repeater", 15) == 0) {
bool is_get = false;
bool is_set = false;
const char* sub = command;
if (strncmp(command, "get recent.repeater", 19) == 0) {
is_get = true;
sub = command + 19;
} else if (strncmp(command, "set recent.repeater", 19) == 0) {
is_set = true;
sub = command + 19;
} else {
sub = command + 15; // legacy command format
}
while (*sub == ' ') sub++;
auto* tables = (SimpleMeshTables*)getTables();
if (is_set || (!is_get && *sub != 0 && strcmp(sub, "all") != 0 && strncmp(sub, "first ", 6) != 0 && strncmp(sub, "last ", 5) != 0)) {
char* params = (char*) sub;
char* arg_snr = strchr(params, ' ');
if (arg_snr == NULL) {
strcpy(reply, "Err - usage: recent.repeater <prefix_hex> <snr_db>");
strcpy(reply, "Err - usage: set recent.repeater <prefix_hex> <snr_db>");
} else {
*arg_snr++ = 0;
while (*arg_snr == ' ') arg_snr++;
if (*arg_snr == 0) {
strcpy(reply, "Err - usage: recent.repeater <prefix_hex> <snr_db>");
strcpy(reply, "Err - usage: set recent.repeater <prefix_hex> <snr_db>");
} else {
int hex_len = strlen(params);
int prefix_len = hex_len / 2;
@@ -1386,6 +1400,94 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
}
}
}
} else if (*sub == 0) {
const auto* info = tables->getLatestRecentRepeater();
if (info == NULL) {
strcpy(reply, "> none");
} else {
char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1];
mesh::Utils::toHex(hex, info->prefix, info->prefix_len);
sprintf(reply, "> %s,%s", hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f));
}
} else if (strcmp(sub, "all") == 0 || strncmp(sub, "first ", 6) == 0 || strncmp(sub, "last ", 5) == 0) {
int total = tables->getRecentRepeaterCount();
if (total <= 0) {
strcpy(reply, "> none");
} else {
bool newest_first = false;
int limit = total;
const char* mode = "all";
if (strncmp(sub, "first ", 6) == 0 || strncmp(sub, "last ", 5) == 0) {
const char* nstr = sub + (sub[0] == 'f' ? 6 : 5);
while (*nstr == ' ') nstr++;
if (*nstr == 0) {
strcpy(reply, "Err - usage: get recent.repeater first|last <count>");
return;
}
char* end_ptr = NULL;
long parsed = strtol(nstr, &end_ptr, 10);
while (end_ptr != NULL && *end_ptr == ' ') end_ptr++;
if (end_ptr == NULL || *end_ptr != 0 || parsed <= 0) {
strcpy(reply, "Err - count must be > 0");
return;
}
limit = (int)parsed;
if (sub[0] == 'l') {
newest_first = true;
mode = "last";
} else {
mode = "first";
}
}
if (limit > total) {
limit = total;
}
if (sender_timestamp == 0) {
Serial.printf("Recent repeater table (%s %d/%d):\n", mode, limit, total);
for (int i = 0; i < limit; i++) {
const auto* info = newest_first ? tables->getRecentRepeaterNewestByIdx(i) : tables->getRecentRepeaterOldestByIdx(i);
if (info == NULL) {
continue;
}
char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1];
mesh::Utils::toHex(hex, info->prefix, info->prefix_len);
Serial.printf("%02d: %s,%s\n", i + 1, hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f));
}
sprintf(reply, "> showing %d/%d (%s)", limit, total, mode);
} else {
// Remote CLI replies are packet-bound, so include as many rows as fit.
int written = snprintf(reply, 160, "> showing %d/%d (%s)", limit, total, mode);
bool truncated = false;
if (written < 0) {
reply[0] = 0;
written = 0;
}
for (int i = 0; i < limit; i++) {
const auto* info = newest_first ? tables->getRecentRepeaterNewestByIdx(i) : tables->getRecentRepeaterOldestByIdx(i);
if (info == NULL) {
continue;
}
if (written >= 154) {
truncated = true;
break;
}
char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1];
mesh::Utils::toHex(hex, info->prefix, info->prefix_len);
int n = snprintf(reply + written, 160 - written, "\n%02d:%s,%s", i + 1, hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f));
if (n < 0 || n >= (160 - written)) {
truncated = true;
break;
}
written += n;
}
if (truncated && written < 156) {
snprintf(reply + written, 160 - written, "\n...");
}
}
}
} else {
strcpy(reply, "Err - usage: get recent.repeater [all|first <count>|last <count>]");
}
} else if (memcmp(command, "discover.neighbors", 18) == 0) {
const char* sub = command + 18;
+1
View File
@@ -154,6 +154,7 @@ protected:
uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override;
bool allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const override;
uint32_t getDirectRetryEchoDelay(const mesh::Packet* packet) const override;
uint8_t getDirectRetryMaxAttempts(const mesh::Packet* packet) const override;
void onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis) override;
int getInterferenceThreshold() const override {
+23 -7
View File
@@ -3,8 +3,8 @@
namespace mesh {
static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS = 3;
static const uint32_t DIRECT_RETRY_BACKOFF_MS[DIRECT_RETRY_MAX_ATTEMPTS] = { 200, 300, 400 };
static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_DEFAULT = 3;
static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX = 15;
void Mesh::begin() {
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
@@ -57,6 +57,14 @@ uint32_t Mesh::getDirectRetryEchoDelay(const Packet* packet) const {
// Keep the base fallback aligned with the repeater's minimum retry wait.
return 200;
}
uint8_t Mesh::getDirectRetryMaxAttempts(const Packet* packet) const {
return DIRECT_RETRY_MAX_ATTEMPTS_DEFAULT;
}
uint32_t Mesh::getDirectRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx) const {
uint32_t base = getDirectRetryEchoDelay(packet);
// Keep the historical linear spacing while allowing the base wait to vary by platform/profile.
return base + ((uint32_t)attempt_idx * 100UL);
}
uint8_t Mesh::getExtraAckTransmitCount() const {
return 0;
}
@@ -517,7 +525,13 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) {
// The retry packet itself just finished transmitting; Dispatcher will release it after this hook.
onDirectRetryEvent("resent", packet, 0);
_direct_retries[i].retry_attempts_sent++;
if (_direct_retries[i].retry_attempts_sent >= DIRECT_RETRY_MAX_ATTEMPTS) {
uint8_t max_attempts = getDirectRetryMaxAttempts(packet);
if (max_attempts < 1) {
max_attempts = 1;
} else if (max_attempts > DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX) {
max_attempts = DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX;
}
if (_direct_retries[i].retry_attempts_sent >= max_attempts) {
onDirectRetryEvent("failure", packet, 0);
clearDirectRetrySlot(i);
continue;
@@ -532,7 +546,7 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) {
}
*retry = *packet;
uint32_t retry_delay = DIRECT_RETRY_BACKOFF_MS[_direct_retries[i].retry_attempts_sent];
uint32_t retry_delay = getDirectRetryAttemptDelay(packet, _direct_retries[i].retry_attempts_sent);
sendPacket(retry, _direct_retries[i].priority, retry_delay);
if (isDirectRetryQueued(retry)) {
_direct_retries[i].packet = retry;
@@ -612,7 +626,9 @@ bool Mesh::getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_h
case PAYLOAD_TYPE_RESPONSE:
case PAYLOAD_TYPE_TXT_MSG:
case PAYLOAD_TYPE_ANON_REQ:
if (packet->getPathHashCount() <= 1) {
// Allow retries even when only one downstream hop remains so fixed direct paths
// (e.g. remote admin/login over 2-hop chains) use the same retry policy.
if (packet->getPathHashCount() == 0) {
return false;
}
next_hop_hash = packet->path;
@@ -622,7 +638,7 @@ bool Mesh::getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_h
return true;
case PAYLOAD_TYPE_MULTIPART:
if (packet->payload_len < 1 || (packet->payload[0] & 0x0F) != PAYLOAD_TYPE_ACK || packet->getPathHashCount() <= 1) {
if (packet->payload_len < 1 || (packet->payload[0] & 0x0F) != PAYLOAD_TYPE_ACK || packet->getPathHashCount() == 0) {
return false;
}
next_hop_hash = packet->path;
@@ -680,7 +696,7 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) {
}
// Only store retry metadata here; allocate the retry packet after the initial TX really completes.
uint32_t retry_delay = DIRECT_RETRY_BACKOFF_MS[0];
uint32_t retry_delay = getDirectRetryAttemptDelay(packet, 0);
calculateDirectRetryKey(packet, _direct_retries[slot_idx].retry_key);
_direct_retries[slot_idx].packet = NULL;
_direct_retries[slot_idx].trigger_packet = const_cast<Packet*>(packet);
+10
View File
@@ -107,6 +107,16 @@ protected:
*/
virtual uint32_t getDirectRetryEchoDelay(const Packet* packet) const;
/**
* \returns maximum number of retry transmissions after the initial direct TX.
*/
virtual uint8_t getDirectRetryMaxAttempts(const Packet* packet) const;
/**
* \returns delay before a specific retry attempt, where attempt_idx=0 is the first retry.
*/
virtual uint32_t getDirectRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx) const;
/**
* \returns number of extra (Direct) ACK transmissions wanted.
*/
+47 -2
View File
@@ -14,6 +14,14 @@
#define DIRECT_RETRY_RECENT_DEFAULT 0
#define DIRECT_RETRY_SNR_MARGIN_DB_DEFAULT 5
#define DIRECT_RETRY_SNR_MARGIN_DB_MAX 40
#define DIRECT_RETRY_TIMING_MAGIC_0 0xD5
#define DIRECT_RETRY_TIMING_MAGIC_1 0x54
#define DIRECT_RETRY_COUNT_DEFAULT 3
#define DIRECT_RETRY_COUNT_MIN 1
#define DIRECT_RETRY_COUNT_MAX 15
#define DIRECT_RETRY_BASE_MS_DEFAULT 200
#define DIRECT_RETRY_BASE_MS_MIN 10
#define DIRECT_RETRY_BASE_MS_MAX 5000
// Believe it or not, this std C function is busted on some platforms!
static uint32_t _atoi(const char* sp) {
@@ -97,7 +105,10 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170
file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290
// next: 291
file.read((uint8_t *)&_prefs->direct_retry_attempts, sizeof(_prefs->direct_retry_attempts)); // 291
file.read((uint8_t *)&_prefs->direct_retry_base_ms, sizeof(_prefs->direct_retry_base_ms)); // 292
file.read((uint8_t *)&_prefs->direct_retry_timing_magic[0], sizeof(_prefs->direct_retry_timing_magic)); // 294
// next: 296
// sanitise bad pref values
_prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f);
@@ -121,6 +132,14 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
_prefs->direct_retry_recent_enabled = constrain(_prefs->direct_retry_recent_enabled, 0, 1);
_prefs->direct_retry_snr_margin_db = constrain(_prefs->direct_retry_snr_margin_db, 0, DIRECT_RETRY_SNR_MARGIN_DB_MAX);
}
if (_prefs->direct_retry_timing_magic[0] != DIRECT_RETRY_TIMING_MAGIC_0
|| _prefs->direct_retry_timing_magic[1] != DIRECT_RETRY_TIMING_MAGIC_1) {
_prefs->direct_retry_attempts = DIRECT_RETRY_COUNT_DEFAULT;
_prefs->direct_retry_base_ms = DIRECT_RETRY_BASE_MS_DEFAULT;
} else {
_prefs->direct_retry_attempts = constrain(_prefs->direct_retry_attempts, DIRECT_RETRY_COUNT_MIN, DIRECT_RETRY_COUNT_MAX);
_prefs->direct_retry_base_ms = constrain(_prefs->direct_retry_base_ms, DIRECT_RETRY_BASE_MS_MIN, DIRECT_RETRY_BASE_MS_MAX);
}
// sanitise bad bridge pref values
_prefs->bridge_enabled = constrain(_prefs->bridge_enabled, 0, 1);
@@ -201,7 +220,11 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170
file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290
// next: 291
file.write((uint8_t *)&_prefs->direct_retry_attempts, sizeof(_prefs->direct_retry_attempts)); // 291
file.write((uint8_t *)&_prefs->direct_retry_base_ms, sizeof(_prefs->direct_retry_base_ms)); // 292
uint8_t retry_timing_magic[2] = { DIRECT_RETRY_TIMING_MAGIC_0, DIRECT_RETRY_TIMING_MAGIC_1 };
file.write(retry_timing_magic, sizeof(retry_timing_magic)); // 294
// next: 296
file.close();
}
@@ -363,6 +386,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
sprintf(reply, "> %s", _prefs->direct_retry_recent_enabled ? "on" : "off");
} else if (memcmp(config, "direct.retry.margin", 19) == 0) {
sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_snr_margin_db);
} else if (memcmp(config, "direct.retry.count", 18) == 0) {
sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_attempts);
} else if (memcmp(config, "direct.retry.base", 17) == 0) {
sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_base_ms);
} else if (memcmp(config, "owner.info", 10) == 0) {
*reply++ = '>';
*reply++ = ' ';
@@ -633,6 +660,24 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
} else {
sprintf(reply, "Error, min 0 and max %d", DIRECT_RETRY_SNR_MARGIN_DB_MAX);
}
} else if (memcmp(config, "direct.retry.count ", 19) == 0) {
int count = atoi(&config[19]);
if (count >= DIRECT_RETRY_COUNT_MIN && count <= DIRECT_RETRY_COUNT_MAX) {
_prefs->direct_retry_attempts = (uint8_t)count;
savePrefs();
strcpy(reply, "OK");
} else {
sprintf(reply, "Error, min %d and max %d", DIRECT_RETRY_COUNT_MIN, DIRECT_RETRY_COUNT_MAX);
}
} else if (memcmp(config, "direct.retry.base ", 18) == 0) {
int delay_ms = atoi(&config[18]);
if (delay_ms >= DIRECT_RETRY_BASE_MS_MIN && delay_ms <= DIRECT_RETRY_BASE_MS_MAX) {
_prefs->direct_retry_base_ms = (uint16_t)delay_ms;
savePrefs();
strcpy(reply, "OK");
} else {
sprintf(reply, "Error, min %d and max %d", DIRECT_RETRY_BASE_MS_MIN, DIRECT_RETRY_BASE_MS_MAX);
}
} else if (memcmp(config, "owner.info ", 11) == 0) {
config += 11;
char *dp = _prefs->owner_info;
+3
View File
@@ -63,6 +63,9 @@ struct NodePrefs { // persisted to file
uint8_t rx_boosted_gain; // power settings
uint8_t path_hash_mode; // which path mode to use when sending
uint8_t loop_detect;
uint8_t direct_retry_attempts;
uint16_t direct_retry_base_ms;
uint8_t direct_retry_timing_magic[2];
};
class CommonCLICallbacks {
+45
View File
@@ -279,6 +279,51 @@ public:
}
return NULL;
}
int getRecentRepeaterCount() const {
int count = 0;
for (int i = 0; i < MAX_RECENT_REPEATERS; i++) {
if (_recent_repeaters[i].prefix_len > 0) {
count++;
}
}
return count;
}
const RecentRepeaterInfo* getRecentRepeaterNewestByIdx(int idx_wanted) const {
if (idx_wanted < 0) {
return NULL;
}
int idx_seen = 0;
for (int i = 0; i < MAX_RECENT_REPEATERS; i++) {
int idx = (_next_recent_repeater_idx - 1 - i + MAX_RECENT_REPEATERS) % MAX_RECENT_REPEATERS;
const RecentRepeaterInfo* info = &_recent_repeaters[idx];
if (info->prefix_len == 0) {
continue;
}
if (idx_seen == idx_wanted) {
return info;
}
idx_seen++;
}
return NULL;
}
const RecentRepeaterInfo* getRecentRepeaterOldestByIdx(int idx_wanted) const {
if (idx_wanted < 0) {
return NULL;
}
int idx_seen = 0;
for (int i = 0; i < MAX_RECENT_REPEATERS; i++) {
int idx = (_next_recent_repeater_idx + i) % MAX_RECENT_REPEATERS;
const RecentRepeaterInfo* info = &_recent_repeaters[idx];
if (info->prefix_len == 0) {
continue;
}
if (idx_seen == idx_wanted) {
return info;
}
idx_seen++;
}
return NULL;
}
const RecentRepeaterInfo* findRecentRepeaterByHash(const uint8_t* hash, uint8_t hash_len) const {
if (hash == NULL || hash_len == 0) {