Retry 3 times with a 200ms,300ms,400ms backoff.

This commit is contained in:
mikecarper
2026-06-04 15:09:37 -07:00
parent 2af7b75d70
commit dbc4dda0ca
7 changed files with 236 additions and 13 deletions
+13
View File
@@ -98,6 +98,19 @@
---
### Get or set recent repeater fallback prefix/SNR
**Usage:**
- `recent.repeater`
- `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)
**Note:** `set` is rejected when the prefix already exists in neighbors.
---
## Statistics
### Clear Stats
+85
View File
@@ -77,6 +77,15 @@ const NeighbourInfo* MyMesh::findNeighbourByHash(const uint8_t* hash, uint8_t ha
return NULL;
}
bool MyMesh::allowRecentRepeaterPrefixStore(const uint8_t* prefix, uint8_t prefix_len, void* ctx) {
if (ctx == NULL || prefix == NULL || prefix_len == 0) {
return true;
}
const MyMesh* self = (const MyMesh*) ctx;
return self->findNeighbourByHash(prefix, prefix_len) == NULL;
}
void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float snr) {
#if MAX_NEIGHBOURS // check if neighbours enabled
// find existing neighbour, else use least recently updated
@@ -510,6 +519,34 @@ void MyMesh::logTxFail(mesh::Packet *pkt, int len) {
}
}
void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis) {
if (packet == NULL) {
return;
}
MESH_DEBUG_PRINTLN("%s direct retry %s (type=%d, route=%s, payload_len=%d, delay=%lu)",
getLogDateTime(),
event,
(uint32_t)packet->getPayloadType(),
packet->isRouteDirect() ? "D" : "F",
(uint32_t)packet->payload_len,
(unsigned long)delay_millis);
if (_logging) {
File f = openAppend(PACKET_LOG_FILE);
if (f) {
f.print(getLogDateTime());
f.printf(": DIRECT RETRY %s (type=%d, route=%s, payload_len=%d, delay=%lu)\n",
event,
(uint32_t)packet->getPayloadType(),
packet->isRouteDirect() ? "D" : "F",
(uint32_t)packet->payload_len,
(unsigned long)delay_millis);
f.close();
}
}
}
int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
if (_prefs.rx_delay_base <= 0.0f) return 0;
return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time);
@@ -880,6 +917,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
active_bw = _prefs.bw;
active_sf = _prefs.sf;
active_cr = _prefs.cr;
((SimpleMeshTables *)getTables())->setRecentRepeaterAllowFilter(&MyMesh::allowRecentRepeaterPrefixStore, this);
}
void MyMesh::begin(FILESYSTEM *fs) {
@@ -901,6 +940,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
active_bw = _prefs.bw;
active_sf = _prefs.sf;
active_cr = _prefs.cr;
((SimpleMeshTables *)getTables())->setRecentRepeaterMinSNRX4(getDirectRetryMinSNRX4());
radio_set_tx_power(_prefs.tx_power_dbm);
updateAdvertTimer();
@@ -1250,6 +1290,48 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
} else {
strcpy(reply, "Err - ??");
}
} 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 {
char* params = (char*) sub;
char* arg_snr = strchr(params, ' ');
if (arg_snr == NULL) {
strcpy(reply, "Err - usage: 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>");
} else {
int hex_len = strlen(params);
int prefix_len = hex_len / 2;
uint8_t prefix[MAX_ROUTE_HASH_BYTES] = {0};
if ((hex_len % 2) != 0 || prefix_len <= 0 || prefix_len > MAX_ROUTE_HASH_BYTES || !mesh::Utils::fromHex(prefix, prefix_len, params)) {
strcpy(reply, "Err - prefix must be 1-3 bytes hex");
} else {
float snr_db = strtof(arg_snr, nullptr);
int snr_x4 = (int)(snr_db * 4.0f + (snr_db >= 0.0f ? 0.5f : -0.5f));
snr_x4 = constrain(snr_x4, -128, 127);
if (tables->setRecentRepeater(prefix, (uint8_t)prefix_len, (int8_t)snr_x4)) {
strcpy(reply, "OK");
} else {
strcpy(reply, "Err - prefix is already in neighbors");
}
}
}
}
}
} else{
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
}
@@ -1293,6 +1375,9 @@ void MyMesh::loop() {
MESH_DEBUG_PRINTLN("Radio params restored");
}
// Keep recent-prefix learning aligned with the live retry SNR gate.
((SimpleMeshTables *)getTables())->setRecentRepeaterMinSNRX4(getDirectRetryMinSNRX4());
// is pending dirty contacts write needed?
if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) {
acl.save(_fs);
+2
View File
@@ -119,6 +119,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
#endif
const NeighbourInfo* findNeighbourByHash(const uint8_t* hash, uint8_t hash_len) const;
static bool allowRecentRepeaterPrefixStore(const uint8_t* prefix, uint8_t prefix_len, void* ctx);
int8_t getDirectRetryMinSNRX4() const;
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);
@@ -148,6 +149,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;
void onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis) override;
int getInterferenceThreshold() const override {
return _prefs.interference_threshold;
+2
View File
@@ -161,6 +161,8 @@ protected:
virtual uint32_t getCADFailMaxDuration() const;
virtual int getInterferenceThreshold() const { return 0; } // disabled by default
virtual int getAGCResetInterval() const { return 0; } // disabled by default
virtual unsigned long getDutyCycleWindowMs() const { return 3600000; }
const Packet* getOutboundInFlight() const { return outbound; }
public:
void begin();
+53 -2
View File
@@ -3,12 +3,16 @@
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 };
void Mesh::begin() {
for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) {
_direct_retries[i].packet = NULL;
_direct_retries[i].trigger_packet = NULL;
_direct_retries[i].retry_at = 0;
_direct_retries[i].retry_delay = 0;
_direct_retries[i].retry_attempts_sent = 0;
_direct_retries[i].priority = 0;
_direct_retries[i].progress_marker = 0;
_direct_retries[i].expect_path_growth = false;
@@ -27,6 +31,9 @@ void Mesh::loop() {
}
if (!isDirectRetryQueued(_direct_retries[i].packet)) {
if (_direct_retries[i].packet == getOutboundInFlight()) {
continue; // currently transmitting; keep slot until onSendComplete/onSendFail emits event
}
clearDirectRetrySlot(i);
}
}
@@ -443,6 +450,7 @@ void Mesh::clearDirectRetrySlot(int idx) {
_direct_retries[idx].trigger_packet = NULL;
_direct_retries[idx].retry_at = 0;
_direct_retries[idx].retry_delay = 0;
_direct_retries[idx].retry_attempts_sent = 0;
_direct_retries[idx].priority = 0;
_direct_retries[idx].progress_marker = 0;
_direct_retries[idx].expect_path_growth = false;
@@ -491,8 +499,12 @@ bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) {
break;
}
}
onDirectRetryEvent("canceled_echo", _direct_retries[i].packet, 0);
onDirectRetryEvent("good", _direct_retries[i].packet, 0);
clearDirectRetrySlot(i);
} else {
onDirectRetryEvent("canceled_echo", _direct_retries[i].trigger_packet, 0);
onDirectRetryEvent("good", _direct_retries[i].trigger_packet, 0);
clearDirectRetrySlot(i);
}
cleared = true;
@@ -510,7 +522,35 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) {
if (_direct_retries[i].queued) {
if (_direct_retries[i].packet == packet) {
// The retry packet itself just finished transmitting; Dispatcher will release it after this hook.
clearDirectRetrySlot(i);
onDirectRetryEvent("resent", packet, 0);
_direct_retries[i].retry_attempts_sent++;
if (_direct_retries[i].retry_attempts_sent >= DIRECT_RETRY_MAX_ATTEMPTS) {
onDirectRetryEvent("failure", packet, 0);
clearDirectRetrySlot(i);
continue;
}
Packet* retry = obtainNewPacket();
if (retry == NULL) {
onDirectRetryEvent("dropped_no_packet", packet, 0);
onDirectRetryEvent("failure", packet, 0);
clearDirectRetrySlot(i);
continue;
}
*retry = *packet;
uint32_t retry_delay = DIRECT_RETRY_BACKOFF_MS[_direct_retries[i].retry_attempts_sent];
sendPacket(retry, _direct_retries[i].priority, retry_delay);
if (isDirectRetryQueued(retry)) {
_direct_retries[i].packet = retry;
_direct_retries[i].retry_delay = retry_delay;
_direct_retries[i].retry_at = futureMillis(retry_delay);
onDirectRetryEvent("queued", retry, retry_delay);
} else {
onDirectRetryEvent("dropped_queue_full", retry, retry_delay);
onDirectRetryEvent("failure", retry, 0);
clearDirectRetrySlot(i);
}
}
continue;
}
@@ -522,6 +562,8 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) {
// Allocate the retry packet only after TX-complete so busy repeaters do not reserve pool slots early.
Packet* retry = obtainNewPacket();
if (retry == NULL) {
onDirectRetryEvent("dropped_no_packet", packet, _direct_retries[i].retry_delay);
onDirectRetryEvent("failure", packet, 0);
clearDirectRetrySlot(i);
continue;
}
@@ -535,7 +577,10 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) {
_direct_retries[i].trigger_packet = NULL;
_direct_retries[i].queued = true;
_direct_retries[i].retry_at = futureMillis(_direct_retries[i].retry_delay);
onDirectRetryEvent("queued", retry, _direct_retries[i].retry_delay);
} else {
onDirectRetryEvent("dropped_queue_full", retry, _direct_retries[i].retry_delay);
onDirectRetryEvent("failure", retry, 0);
clearDirectRetrySlot(i);
}
}
@@ -550,12 +595,16 @@ void Mesh::clearPendingDirectRetryOnSendFail(const Packet* packet) {
if (_direct_retries[i].queued) {
if (_direct_retries[i].packet == packet) {
// The queued retry itself failed; Dispatcher will release it after this hook.
onDirectRetryEvent("dropped_send_fail", packet, 0);
onDirectRetryEvent("failure", packet, 0);
clearDirectRetrySlot(i);
}
continue;
}
if (_direct_retries[i].trigger_packet == packet) {
onDirectRetryEvent("dropped_send_fail", packet, 0);
onDirectRetryEvent("failure", packet, 0);
clearDirectRetrySlot(i);
}
}
@@ -638,17 +687,19 @@ 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 = getDirectRetryEchoDelay(packet);
uint32_t retry_delay = DIRECT_RETRY_BACKOFF_MS[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);
_direct_retries[slot_idx].retry_at = 0;
_direct_retries[slot_idx].retry_delay = retry_delay;
_direct_retries[slot_idx].retry_attempts_sent = 0;
_direct_retries[slot_idx].priority = priority;
_direct_retries[slot_idx].progress_marker = progress_marker;
_direct_retries[slot_idx].expect_path_growth = expect_path_growth;
_direct_retries[slot_idx].queued = false;
_direct_retries[slot_idx].active = true;
onDirectRetryEvent("armed", packet, retry_delay);
}
Packet* Mesh::createAdvert(const LocalIdentity& id, const uint8_t* app_data, size_t app_data_len) {
+6
View File
@@ -34,6 +34,7 @@ class Mesh : public Dispatcher {
Packet* trigger_packet;
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;
@@ -111,6 +112,11 @@ protected:
*/
virtual uint8_t getExtraAckTransmitCount() const;
/**
* \brief Optional hook for logging direct-retry lifecycle events.
*/
virtual void onDirectRetryEvent(const char* event, const Packet* packet, uint32_t delay_millis) { }
/**
* \brief Perform search of local DB of peers/contacts.
* \returns Number of peers with matching hash
+75 -11
View File
@@ -13,6 +13,8 @@
class SimpleMeshTables : public mesh::MeshTables {
public:
typedef bool (*RecentRepeaterAllowFn)(const uint8_t* prefix, uint8_t prefix_len, void* ctx);
struct RecentRepeaterInfo {
// Just enough identity to match a next-hop path prefix plus the SNR that heard it.
uint8_t prefix[MAX_ROUTE_HASH_BYTES];
@@ -28,6 +30,9 @@ private:
uint32_t _direct_dups, _flood_dups;
RecentRepeaterInfo _recent_repeaters[MAX_RECENT_REPEATERS];
int _next_recent_repeater_idx;
int8_t _recent_repeater_min_snr_x4;
RecentRepeaterAllowFn _recent_repeater_allow_fn;
void* _recent_repeater_allow_ctx;
bool hasSeenAck(uint32_t ack) const {
for (int i = 0; i < MAX_PACKET_ACKS; i++) {
@@ -58,6 +63,11 @@ private:
_next_idx = (_next_idx + 1) % MAX_PACKET_HASHES;
}
bool prefixesOverlap(const uint8_t* a, uint8_t a_len, const uint8_t* b, uint8_t b_len) const {
uint8_t n = a_len < b_len ? a_len : b_len;
return n > 0 && memcmp(a, b, n) == 0;
}
bool extractRecentRepeater(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const {
// Learn repeater prefixes only from packet shapes that expose a trustworthy repeater ID.
if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT && packet->payload_len >= PUB_KEY_SIZE) {
@@ -96,14 +106,10 @@ private:
if (!extractRecentRepeater(packet, prefix, prefix_len) || prefix_len == 0) {
return;
}
// Ring buffer is enough here; retry fallback only needs a recent prefix->SNR observation.
RecentRepeaterInfo& slot = _recent_repeaters[_next_recent_repeater_idx];
memset(slot.prefix, 0, sizeof(slot.prefix));
memcpy(slot.prefix, prefix, prefix_len);
slot.prefix_len = prefix_len;
slot.snr_x4 = packet->_snr;
_next_recent_repeater_idx = (_next_recent_repeater_idx + 1) % MAX_RECENT_REPEATERS;
if (packet->_snr < _recent_repeater_min_snr_x4) {
return;
}
setRecentRepeater(prefix, prefix_len, packet->_snr);
}
public:
@@ -115,6 +121,9 @@ public:
_direct_dups = _flood_dups = 0;
memset(_recent_repeaters, 0, sizeof(_recent_repeaters));
_next_recent_repeater_idx = 0;
_recent_repeater_min_snr_x4 = -128;
_recent_repeater_allow_fn = NULL;
_recent_repeater_allow_ctx = NULL;
}
#ifdef ESP32
@@ -216,19 +225,74 @@ public:
uint32_t getNumDirectDups() const { return _direct_dups; }
uint32_t getNumFloodDups() const { return _flood_dups; }
void setRecentRepeaterMinSNRX4(int8_t min_snr_x4) {
_recent_repeater_min_snr_x4 = min_snr_x4;
}
void setRecentRepeaterAllowFilter(RecentRepeaterAllowFn fn, void* ctx) {
_recent_repeater_allow_fn = fn;
_recent_repeater_allow_ctx = ctx;
}
bool setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4) {
if (prefix == NULL || prefix_len == 0) {
return false;
}
if (prefix_len > MAX_ROUTE_HASH_BYTES) {
prefix_len = MAX_ROUTE_HASH_BYTES;
}
if (_recent_repeater_allow_fn != NULL && !_recent_repeater_allow_fn(prefix, prefix_len, _recent_repeater_allow_ctx)) {
return false;
}
// Keep one slot for overlapping prefixes so 1/2/3-byte paths share the same entry.
for (int i = 0; i < MAX_RECENT_REPEATERS; i++) {
RecentRepeaterInfo& existing = _recent_repeaters[i];
if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) {
continue;
}
if (prefix_len > existing.prefix_len) {
memset(existing.prefix, 0, sizeof(existing.prefix));
memcpy(existing.prefix, prefix, prefix_len);
existing.prefix_len = prefix_len;
}
existing.snr_x4 = snr_x4;
return true;
}
// Ring buffer is enough here; retry fallback only needs a recent prefix->SNR observation.
RecentRepeaterInfo& slot = _recent_repeaters[_next_recent_repeater_idx];
memset(slot.prefix, 0, sizeof(slot.prefix));
memcpy(slot.prefix, prefix, prefix_len);
slot.prefix_len = prefix_len;
slot.snr_x4 = snr_x4;
_next_recent_repeater_idx = (_next_recent_repeater_idx + 1) % MAX_RECENT_REPEATERS;
return true;
}
const RecentRepeaterInfo* getLatestRecentRepeater() const {
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) {
return info;
}
}
return NULL;
}
const RecentRepeaterInfo* findRecentRepeaterByHash(const uint8_t* hash, uint8_t hash_len) const {
if (hash == NULL || hash_len == 0) {
return NULL;
}
// Search newest-to-oldest so the retry gate prefers the freshest SNR sample for a prefix.
// Search newest-to-oldest and allow 1/2/3-byte prefixes to overlap-match.
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 < hash_len || info->prefix_len == 0) {
if (info->prefix_len == 0) {
continue;
}
if (memcmp(info->prefix, hash, hash_len) == 0) {
if (prefixesOverlap(info->prefix, info->prefix_len, hash, hash_len)) {
return info;
}
}