Skip to content

Commit 84bc3fa

Browse files
committed
Implement final hop ACK handling for direct packets to allow cancellable resends
1 parent 31f577e commit 84bc3fa

5 files changed

Lines changed: 74 additions & 4 deletions

File tree

src/Dispatcher.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,13 @@ bool Dispatcher::resendPacket(mesh::Packet *packet) {
439439
// prepare error correction via potential retransmit:
440440
// re-send only direct routed packets that carry at least one relay hash, so that a
441441
// downstream relay's forward can be overheard to cancel this re-send.
442-
if (packet->isRouteDirect() && packet->getPathHashCount() > 0 && packet->sending_attempts < getMaxResendAttempts()) {
442+
// The final relay hop (path empty after removeSelfFromPath, flagged via
443+
// final_hop_ack_resend) is the exception: there is no downstream forward to overhear, but
444+
// the destination ACKs receipt. Allow exactly one resend there (sending_attempts == 0),
445+
// cancellable by the returning ACK (see Mesh::cancelPendingFinalHopResend).
446+
if (packet->isRouteDirect() && packet->sending_attempts < getMaxResendAttempts() &&
447+
(packet->getPathHashCount() > 0 ||
448+
(packet->final_hop_ack_resend && packet->sending_attempts == 0))) {
443449
packet->sending_attempts++;
444450

445451
MESH_DEBUG_PRINTLN("Dispatcher::resendPacket %s attempt=%d", packet->getHashHex(),

src/Mesh.cpp

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,20 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
128128
}
129129

130130
// For direct packets with no relay hashes (zero-hop, addressed straight to us),
131-
// no further path-based processing applies
132-
if (pkt->getPathHashCount() == 0) goto direct_path_done;
131+
// no further path-based processing applies.
132+
// A destination's receipt-ACK for a TXT_MSG we delivered as the final relay hop arrives
133+
// exactly this way: zero-path direct, because the destination ACKs its immediate
134+
// neighbour (it has no usable multi-hop return path to the originator, so the ACK only
135+
// travels one hop). The ACK-relay branch below is gated on is_next_hop, which requires
136+
// getPathHashCount() > 0 — unreachable for such an ACK. Cancel any pending final-hop
137+
// resend here, before the early-exit, so the resend does not fire after a delivery the
138+
// destination has already acknowledged.
139+
if (pkt->getPathHashCount() == 0) {
140+
if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) {
141+
cancelPendingFinalHopResend();
142+
}
143+
goto direct_path_done;
144+
}
133145

134146
// check for 'early received' ACK
135147
if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) {
@@ -145,6 +157,12 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
145157
if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) {
146158
return forwardMultipartDirect(pkt);
147159
} else if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) {
160+
// This ACK is addressed back along the return path with us as the next hop, i.e. it
161+
// transits back through the very relay that delivered the original message. That is
162+
// proof the destination received it: cancel any pending final-hop resend so we do not
163+
// needlessly retransmit to the destination.
164+
cancelPendingFinalHopResend();
165+
148166
if (!_tables->wasSeen(pkt)) { // don't retransmit!
149167
_tables->markSeen(pkt);
150168
removeSelfFromPath(pkt);
@@ -155,12 +173,25 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
155173

156174
if (!_tables->wasSeen(pkt)) {
157175
_tables->markSeen(pkt);
176+
177+
// Detect the final relay hop: only our own hash remained in the path, so after
178+
// removeSelfFromPath() the destination is reached implicitly (it lives in the payload
179+
// dest_hash, not as a relay hash) and getPathHashCount() becomes 0. The normal resend
180+
// guard (getPathHashCount() > 0) would then suppress any retransmit, leaving a lost
181+
// final-hop TX unrecoverable at the relay layer. TXT_MSG destinations ACK receipt, so
182+
// mark this packet to allow exactly one ACK-cancellable resend (see resendPacket() and
183+
// cancelPendingFinalHopResend()): the resend self-cancels on the returning ACK, so the
184+
// destination is not flooded.
185+
if (pkt->getPathHashCount() == 1 && pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) {
186+
pkt->final_hop_ack_resend = true;
187+
}
188+
158189
removeSelfFromPath(pkt);
159190

160191
MESH_DEBUG_PRINTLN("Mesh::onRecvPacket(): prepare to repeat packet %s", pkt->getHashHex());
161192

162193
uint32_t d = getDirectRetransmitDelay(pkt);
163-
return ACTION_RETRANSMIT_DELAYED(0, d); // Routed traffic is HIGHEST priority
194+
return ACTION_RETRANSMIT_DELAYED(0, d); // Routed traffic is HIGHEST priority
164195
}
165196
}
166197
return ACTION_RELEASE; // this node is NOT the next hop (OR this packet has already been forwarded), so discard.
@@ -399,6 +430,34 @@ void Mesh::removeSelfFromPath(Packet* pkt) {
399430
}
400431
}
401432

433+
void Mesh::cancelPendingFinalHopResend() {
434+
// The destination has ACKed receipt. ACKs are produced in delivery order and (for the
435+
// direct zero-path ACKs a companion sends) travel only one hop back to us, so the OLDEST
436+
// pending final-hop resend corresponds to the message this ACK acknowledges (FIFO). Cancel
437+
// exactly one — the earliest still-queued resend.
438+
//
439+
// The sending_attempts > 0 guard is essential: it selects only actual resends (the attempt
440+
// counter is bumped in resendPacket() before re-queueing) and never the original final-hop
441+
// forward, which is also flagged final_hop_ack_resend while it sits in the queue waiting to
442+
// be TXed. Without it, an ACK arriving in that brief pre-TX window would cancel the delivery
443+
// itself. (send_queue.add() appends, so index 0 is the oldest entry.)
444+
//
445+
// An in-flight resend (the 'outbound' packet, already on-air) is deliberately NOT touched:
446+
// aborting a mid-TX send risks radio state, and the duplicate it delivers is harmless (the
447+
// destination dedups via wasSeen). Only queued, not-yet-sent resends are cancelled.
448+
for (int i = 0; i < _mgr->getOutboundTotal(); i++) {
449+
Packet* queued = _mgr->getOutboundByIdx(i);
450+
if (queued && queued->final_hop_ack_resend && queued->sending_attempts > 0 &&
451+
queued->isRouteDirect() && queued->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) {
452+
MESH_DEBUG_PRINTLN("%s Mesh::cancelPendingFinalHopResend(): ACK heard, canceling oldest "
453+
"final-hop resend (queued) %s", getLogDateTime(), queued->getHashHex());
454+
Packet* removed = _mgr->removeOutboundByIdx(i);
455+
if (removed) _mgr->free(removed);
456+
return;
457+
}
458+
}
459+
}
460+
402461
DispatcherAction Mesh::routeRecvPacket(Packet* packet) {
403462
uint8_t n = packet->getPathHashCount();
404463
if (packet->isRouteFlood() && !packet->isMarkedDoNotRetransmit()

src/Mesh.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class Mesh : public Dispatcher {
3131

3232
void removeSelfFromPath(Packet* packet);
3333
void routeDirectRecvAcks(Packet* packet, uint32_t delay_millis);
34+
void cancelPendingFinalHopResend(); // drop a queued final-hop resend when the destination's ACK transits back
3435
//void routeRecvAcks(Packet* packet, uint32_t delay_millis);
3536
DispatcherAction forwardMultipartDirect(Packet* pkt);
3637

src/Packet.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Packet::Packet() {
1414
memcpy(hash, ZERO_HASH, MAX_HASH_SIZE);
1515
memset(hash_hex, 0, sizeof(hash_hex));
1616
sending_attempts = 0;
17+
final_hop_ack_resend = false;
1718
}
1819

1920
bool Packet::isValidPathLen(uint8_t path_len) {

src/Packet.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ class Packet {
5252
uint8_t hash[MAX_HASH_SIZE];
5353
mutable char hash_hex[MAX_HASH_SIZE * 2 + 1];
5454
uint8_t sending_attempts;
55+
bool final_hop_ack_resend; // runtime-only: set when this node forwards the final relay hop
56+
// of an ACK-producing direct packet (e.g. TXT_MSG). Allows exactly
57+
// one ACK-cancellable resend from resendPacket(); not wire-serialized.
5558

5659
/**
5760
* \brief calculate the hash of payload + type

0 commit comments

Comments
 (0)