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

This commit is contained in:
mikecarper committed 2026-06-04 15:09:37 -07:00
1 parent 2af7b75d70
commit dbc4dda0ca
7 files changed
+236 -13

No files matched your search

+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;
}
}