Deliver locally-generated replies addressed to us to the phone (#11185)

* Deliver locally-generated replies addressed to us to the phone

Config get/set from the phone times out on every device: the client sends an
admin request, the node handles it, and the response is silently dropped
before it reaches the phone queue.

#10967 changed Router::sendLocal's isToUs branch from enqueueReceivedMessage()
to handleReceived(p, src), so a local packet keeps its RxSource instead of
being relabeled RX_SRC_RADIO by the queue round-trip. That is the right call
for the new policy gates, but module replies go out through
MeshService::sendToMesh() with the default RX_SRC_LOCAL, and a reply to a
phone-originated request is addressed to our own node (setReplyTo resolves
from == 0 to ourNodeNum). Those replies now re-enter callModules as
RX_SRC_LOCAL, where the loopback gate skips every module whose loopbackOk is
false - including RoutingModule, whose promiscuous sniff is the only path that
moves a received packet into toPhoneQueue. The reply is released, never sent.

Requests still work, because the phone's own packets arrive as RX_SRC_USER and
pass the gate, so a set_config is applied and only its acknowledgement is lost.
That is why a client can connect and download config but times out on every
config screen and every setter.

Deliver the phone's copy from sendToMesh() instead: for a local packet
addressed to us, the loopback gate is doing its job in keeping the packet away
from module re-dispatch, and the phone copy is exactly what is missing. Setting
loopbackOk on RoutingModule would instead echo every locally-generated
broadcast back to the phone, and relabeling replies RX_SRC_RADIO would undo the
origin separation #10967 added.

Also stop reporting ERRNO_SHOULD_RELEASE (35) to the phone in the QueueStatus
for these packets. It means "caller frees", not a send failure, and the same
hunk changed it from the 0 the phone used to see.

* Address review: trim comments, assert the QueueStatus count

Condense the added comments to the one-or-two-line house style; the rationale
lives in the commit message and PR.

The reply test drained QueueStatus records in a while loop, which would have
passed just as happily on an empty queue. Count them and require both the
request's and the reply's.
This commit is contained in:
Ben Meadors
2026-07-24 09:02:51 +00:00
committed by GitHub
parent 4c17dda3e6
commit 01d1873bfc
2 changed files with 80 additions and 1 deletions
+8 -1
View File
@@ -324,13 +324,20 @@ void MeshService::sendToMesh(meshtastic_MeshPacket *p, RxSource src, bool ccToPh
uint32_t mesh_packet_id = p->id;
nodeDB->updateFrom(*p); // update our local DB for this packet (because phone might have sent position packets etc...)
// callModules' loopback gate keeps RX_SRC_LOCAL packets from RoutingModule, the only module
// that forwards to the phone, so deliver our own reply's copy here or the client never sees it.
const bool localDelivery = isToUs(p);
if (src == RX_SRC_LOCAL && localDelivery)
ccToPhone = true;
// Note: We might return !OK if our fifo was full, at that point the only option we have is to drop it
ErrorCode res = router->sendLocal(p, src);
/* NOTE(pboldin): Prepare and send QueueStatus message to the phone as a
* high-priority message. */
meshtastic_QueueStatus qs = router->getQueueStatus();
ErrorCode r = sendQueueStatusToPhone(qs, res, mesh_packet_id);
// SHOULD_RELEASE means "caller frees", not a send failure, so don't report it as one.
ErrorCode r = sendQueueStatusToPhone(qs, (res == ERRNO_SHOULD_RELEASE && localDelivery) ? ERRNO_OK : res, mesh_packet_id);
if (r != ERRNO_OK) {
LOG_DEBUG("Can't send status to phone");
}