fix: improve acknowledged unicast retry reliability (#11320)

* Improve acknowledged unicast retry reliability

* Fix merged next-hop routing tests

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
This commit is contained in:
Benjamin Faershtein
2026-08-13 13:55:53 -04:00
committed by GitHub
co-authored by Ben Meadors
parent faa2c8fc52
commit a400143090
5 changed files with 196 additions and 11 deletions
+11 -4
View File
@@ -57,12 +57,18 @@ PendingPacket::PendingPacket(meshtastic_MeshPacket *p, uint8_t numRetransmission
{
packet = p;
this->numRetransmissions = numRetransmissions - 1; // We subtract one, because we assume the user just did the first send
this->initialNumRetransmissions = this->numRetransmissions;
}
/**
* Send a packet
*/
ErrorCode NextHopRouter::send(meshtastic_MeshPacket *p)
{
return sendWithNextHop(p, true);
}
ErrorCode NextHopRouter::sendWithNextHop(meshtastic_MeshPacket *p, bool trackRetransmission)
{
// Add any messages _we_ send to the seen message list (so we will ignore all retransmissions we see)
p->relay_node = nodeDB->getLastByteOfNodeNum(getNodeNum()); // First set the relayer to us
@@ -73,7 +79,8 @@ ErrorCode NextHopRouter::send(meshtastic_MeshPacket *p)
// If it's from us, ReliableRouter already handles retransmissions if want_ack is set. If a next hop is set and hop limit is
// not 0 or want_ack is set, start retransmissions
if ((!isFromUs(p) || !p->want_ack) && p->next_hop != NO_NEXT_HOP_PREFERENCE && (p->hop_limit > 0 || p->want_ack)) {
if (trackRetransmission && (!isFromUs(p) || !p->want_ack) && p->next_hop != NO_NEXT_HOP_PREFERENCE &&
(p->hop_limit > 0 || p->want_ack)) {
if (auto *copy = packetPool.allocCopy(*p))
startRetransmission(copy); // start retransmission for relayed packet
}
@@ -362,7 +369,7 @@ bool NextHopRouter::stopRetransmission(GlobalPacketId key)
auto p = old->packet;
/* Only when we already transmitted a packet via LoRa, we will cancel the packet in the Tx queue
to avoid canceling a transmission if it was ACKed super fast via MQTT */
if (old->numRetransmissions < NUM_RELIABLE_RETX - 1) {
if (old->numRetransmissions < old->initialNumRetransmissions) {
// We only cancel it if we are the original sender or if we're not a router(_late)
if (isFromUs(p) || roleAllowsCancelingFromTxQueue(p)) {
// remove the 'original' (identified by originator and packet->id) from the txqueue and free it
@@ -475,13 +482,13 @@ int32_t NextHopRouter::doRetransmissions()
}
} else {
if (auto *copy = packetPool.allocCopy(*p.packet)) {
if (NextHopRouter::send(copy) == ERRNO_SHOULD_RELEASE)
if (sendWithNextHop(copy, false) == ERRNO_SHOULD_RELEASE)
packetPool.release(copy);
}
}
#else
if (auto *copy = packetPool.allocCopy(*p.packet)) {
if (NextHopRouter::send(copy) == ERRNO_SHOULD_RELEASE)
if (sendWithNextHop(copy, false) == ERRNO_SHOULD_RELEASE)
packetPool.release(copy);
}
#endif
+12 -5
View File
@@ -39,6 +39,9 @@ struct PendingPacket {
/** Starts at NUM_RETRANSMISSIONS -1 and counts down. Once zero it will be removed from the list */
uint8_t numRetransmissions = 0;
/** Initial remaining retry count, used to detect whether a retry has fired. */
uint8_t initialNumRetransmissions = 0;
PendingPacket() {}
explicit PendingPacket(meshtastic_MeshPacket *p, uint8_t numRetransmissions);
};
@@ -77,8 +80,8 @@ class GlobalPacketIdHashFunction
Namely, in the PacketHistory, we keep track of (up to 3) relayers of a packet. When the ACK is delivered back to us via a node
that also relayed the original packet, we use that node as next hop for the destination from then on. This makes sure that only
when there’s a two-way connection, we assign a next hop. Both the ReliableRouter and NextHopRouter will do retransmissions (the
NextHopRouter only 1 time). For the final retry, if no one actually relayed the packet, it will reset the next hop in order to
fall back to the FloodingRouter again. Note that thus also intermediate hops will do a single retransmission if the intended
NextHopRouter only a small number of times). For the final retry, if no one actually relayed the packet, it will reset the next
hop in order to fall back to the FloodingRouter again. Intermediate hops also do bounded retransmissions if the intended
next-hop didn’t relay, in order to fix changes in the middle of the route.
*/
class NextHopRouter : public FloodingRouter
@@ -109,10 +112,12 @@ class NextHopRouter : public FloodingRouter
return min(d, r);
}
// The number of retransmissions intermediate nodes will do (actually 1 less than this)
constexpr static uint8_t NUM_INTERMEDIATE_RETX = 2;
// The number of retransmissions the original sender will do
// Total attempts for directed hop-level delivery, including the initial send.
constexpr static uint8_t NUM_INTERMEDIATE_RETX = 3;
// Existing reliable broadcast budget, including the initial send.
constexpr static uint8_t NUM_RELIABLE_RETX = 3;
// Total attempts for acknowledged unicast from the originating node.
constexpr static uint8_t NUM_RELIABLE_UNICAST_ATTEMPTS = 5;
// M3: bounded RAM route-health table (reuse-oldest eviction, like PacketHistory)
constexpr static uint8_t ROUTE_HEALTH_MAX = 32; // ~12B/slot -> ~384B
@@ -155,6 +160,8 @@ class NextHopRouter : public FloodingRouter
*/
PendingPacket *startRetransmission(meshtastic_MeshPacket *p, uint8_t numReTx = NUM_INTERMEDIATE_RETX);
ErrorCode sendWithNextHop(meshtastic_MeshPacket *p, bool trackRetransmission);
// Return true if we're allowed to cancel a packet in the txQueue (so we may never transmit it even once)
bool roleAllowsCancelingFromTxQueue(const meshtastic_MeshPacket *p);
+4 -2
View File
@@ -30,8 +30,10 @@ ErrorCode ReliableRouter::send(meshtastic_MeshPacket *p)
auto copy = packetPool.allocCopy(*p);
DEBUG_HEAP_AFTER("ReliableRouter::send", copy);
if (copy)
startRetransmission(copy, NUM_RELIABLE_RETX);
if (copy) {
const uint8_t totalAttempts = isBroadcast(p->to) ? NUM_RELIABLE_RETX : NUM_RELIABLE_UNICAST_ATTEMPTS;
startRetransmission(copy, totalAttempts);
}
}
/* If we have pending retransmissions, add the airtime of this packet to it, because during that time we cannot receive an