Skip to content

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

Merged
caveman99 merged 2 commits into
developfrom
fix/local-admin-reply-to-phone
Jul 24, 2026
Merged

Deliver locally-generated replies addressed to us to the phone#11185
caveman99 merged 2 commits into
developfrom
fix/local-admin-reply-to-phone

Conversation

@thebentern

@thebentern thebentern commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

The bug

Config get/set from a client times out on every device running current develop. The client
connects fine and downloads config at connect, but opening a config screen or changing a
setting (e.g. Device Role) times out.

Reported by an alpha tester on a Seeed Xiao nRF52840 Kit against 2.8.0.ef1aedd, working on
2.8.0.5ded0ec. It is not board- or app-specific — both Android 2.8.0 and 2.7.14 behave the
same, and the mechanism is platform-independent.

Regression

d6b12ea3f (#10967, packet authenticity policies) changed Router::sendLocal's isToUs
branch:

-        enqueueReceivedMessage(p);
-        return ERRNO_OK;
+        handleReceived(p, src);
+        return ERRNO_SHOULD_RELEASE;

Preserving the RxSource is the right call — the queue round-trip used to relabel local
packets RX_SRC_RADIO, which is exactly the confusion the new policy gates need gone. But
module replies are sent 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). So those replies now re-enter callModules as RX_SRC_LOCAL,
where the loopback gate skips every module with loopbackOk == false:

https://github.com/meshtastic/firmware/blob/develop/src/mesh/MeshModule.cpp#L121-L124

That includes RoutingModule, whose promiscuous sniff is the only code that moves a received
packet into toPhoneQueue:

https://github.com/meshtastic/firmware/blob/develop/src/modules/RoutingModule.cpp#L35-L38

The reply is released without ever being queued.

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 — which is why a
client can connect and download config but times out on every config screen and every setter.
Testers who saw a timeout may find the setting actually took effect.

The fix

Deliver the phone's copy from sendToMesh() for a local packet addressed to us. The loopback
gate is doing its job keeping the packet out of module re-dispatch; the phone copy is the part
that went missing.

Two alternatives I ruled out:

  • loopbackOk = true on RoutingModule — would also echo every locally-generated broadcast
    (position, nodeinfo, telemetry) back to the phone through the promiscuous sniff.
  • Relabeling module replies RX_SRC_RADIO in sendLocal — re-muddles the exact local/remote
    distinction feat(security): enforce packet authenticity policies #10967 introduced.

Also stops 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.

Tests

Two regression tests in test/test_mesh_module:

  • test_localReplyToSelf_isDeliveredToPhone — a reply addressed to ourselves reaches the
    phone queue exactly once, with a success QueueStatus and no radio traffic.
  • test_phoneRequest_replyReachesPhone — full loop through the real router: a from == 0
    want_response request produces a module reply that reaches the phone queue with the right
    request_id, and the request itself does not echo back.

Both fail on unpatched develop (empty phone queue) and pass with the fix.

Validation

Native suites with the fix applied: native-macos 577 cases, 573 passed + 4
environment-conditional ignores, 0 failed
; Docker 751/751 passed.

End-to-end on the portduino simulator over the TCP PhoneAPI (same client path as BLE), as a
minimal pair at the reporter's exact commit ef1aedd56, with only MeshService.cpp differing:

unpatched ef1aedd56 ef1aedd56 + this fix
--set device.role CLIENT_MUTE hangs >180 s (killed) completes in 0.6 s, persists across reboot
--get-ringtone (admin round-trip) hangs >180 s (killed) completes in 0.4 s
--get device.role works works

--get device.role passing on both is itself diagnostic — the Python CLI serves it from the
want_config cache rather than an admin round-trip, which is the same reason a client can
connect while every config screen times out.

Summary by CodeRabbit

  • Bug Fixes
    • Ensured locally addressed module replies are consistently delivered to the phone callback/queue.
    • Updated queue status forwarding so local delivery failures are no longer reported incorrectly.
    • Prevented phone-originated requests/replies from being echoed back or sent to the radio.
  • Tests
    • Added regression tests covering local reply-to-phone delivery and phone request response routing.
    • Improved test teardown to properly drain pending phone deliveries and statuses.

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.
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8a00ff03-28ad-4a6f-8a30-59af26c44a19

📥 Commits

Reviewing files that changed from the base of the PR and between f77c61a and 6efce75.

📒 Files selected for processing (2)
  • src/mesh/MeshService.cpp
  • test/test_mesh_module/test_main.cpp
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/mesh/MeshService.cpp
  • test/test_mesh_module/test_main.cpp

📝 Walkthrough

Walkthrough

MeshService::sendToMesh now forwards eligible locally generated packets to the phone and reports successful local consumption. Mesh module tests cover self-addressed replies, phone-originated requests, cleanup, and registration.

Changes

Local delivery behavior

Layer / File(s) Summary
Local delivery and queue status handling
src/mesh/MeshService.cpp
Locally addressed packets from RX_SRC_LOCAL force phone delivery, and ERRNO_SHOULD_RELEASE is reported as ERRNO_OK for successful local consumption.
Regression coverage for phone delivery
test/test_mesh_module/test_main.cpp
Tests verify self-addressed and phone-originated replies reach the phone queue without radio transmission; cleanup drains phone deliveries and registers both regressions.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested labels: mesh, module

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main fix: routing local self-addressed replies to the phone.
Description check ✅ Passed The description covers bug, regression, fix, tests, and validation, with only the template attestations missing.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/local-admin-reply-to-phone

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thebentern thebentern added the bugfix Pull request that fixes bugs label Jul 24, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/mesh/MeshService.cpp (1)

327-331: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Keep the added comments to at most two lines.

  • src/mesh/MeshService.cpp#L327-L331: condense the loopback rationale to two lines.
  • test/test_mesh_module/test_main.cpp#L476-L483: shorten the regression rationale and test description to one or two lines each.

As per coding guidelines, “Keep comments minimal—one or two lines maximum.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mesh/MeshService.cpp` around lines 327 - 331, Condense the loopback
rationale comment in src/mesh/MeshService.cpp lines 327-331 to no more than two
lines while preserving its purpose. Also shorten the regression rationale and
test description in test/test_mesh_module/test_main.cpp lines 476-483 to one or
two lines each; make comment-only changes and keep the existing test behavior
unchanged.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/test_mesh_module/test_main.cpp`:
- Around line 538-542: Update the QueueStatus verification in the test around
mockService->getQueueStatusForPhone() to count the records consumed and assert
that exactly two statuses were emitted, while retaining the ERRNO_OK checks for
each record and releasing each through releaseQueueStatusToPool.

---

Nitpick comments:
In `@src/mesh/MeshService.cpp`:
- Around line 327-331: Condense the loopback rationale comment in
src/mesh/MeshService.cpp lines 327-331 to no more than two lines while
preserving its purpose. Also shorten the regression rationale and test
description in test/test_mesh_module/test_main.cpp lines 476-483 to one or two
lines each; make comment-only changes and keep the existing test behavior
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 23074f58-44da-4634-aa5a-3dcf84db42f9

📥 Commits

Reviewing files that changed from the base of the PR and between 6908d27 and f77c61a.

📒 Files selected for processing (2)
  • src/mesh/MeshService.cpp
  • test/test_mesh_module/test_main.cpp

Comment thread test/test_mesh_module/test_main.cpp Outdated
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.
@thebentern

Copy link
Copy Markdown
Contributor Author

Both addressed in 6efce75.

Comments condensed to the one-or-two-line house style — the rationale belongs in the commit message and PR body, which carry it.

The QueueStatus point was a real weakness, thanks: the while loop asserted ERRNO_OK per record but would have passed just as happily on an empty queue. It now counts and requires exactly two, one for the request and one for the reply — verified empirically rather than assumed. Re-confirmed both tests still fail on unpatched develop ("Expected Non-NULL") and pass with the fix, 21/21 for the suite.

@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

⚡ Try this PR in the Web Flasher

Flash this PR in the Web Flasher

firmware commit boards expires

Warning

This is an automated, unreviewed CI test build. Back up your device configuration
before flashing, and only flash devices you are able to recover.

Supported boards built by this PR (30)
Device Board Platform
Crowpanel Adv 3.5 TFT elecrow-adv-35-tft esp32-s3
Heltec HT62 heltec-ht62-esp32c3-sx1262 esp32-c3
Heltec Mesh Node 096 heltec-mesh-node-t096 nrf52840
Heltec Mesh Node T1 heltec-mesh-node-t1 nrf52840
Heltec Mesh Node T114 heltec-mesh-node-t114 nrf52840
Heltec V3 heltec-v3 esp32-s3
Heltec V4 heltec-v4 esp32-s3
Meshnology W10 meshnology_w10 esp32-s3
Raspberry Pi Pico pico rp2040
Raspberry Pi Pico W picow rp2040
RAK WisMesh Pocket V3 rak_wismesh_pocket nrf52840
RAK WisMesh Pod rak_wismesh_pod nrf52840
RAK WisMesh Repeater Mini V2 rak_wismesh_repeater_mini nrf52840
RAK WisMesh Tag rak_wismeshtag nrf52840
RAK WisBlock 11200 rak11200 esp32
RAK WisBlock 11310 rak11310 rp2040
RAK3312 rak3312 esp32-s3
RAK WisBlock 4631 rak4631 nrf52840
Seeed SenseCAP Mesh-Tracker-X1 seeed_mesh_tracker_X1 nrf52840
Seeed Wio Tracker L1 seeed_wio_tracker_L1 nrf52840
Seeed Xiao NRF52840 Kit seeed_xiao_nrf52840_kit nrf52840
Seeed Xiao ESP32-S3 seeed-xiao-s3 esp32-s3
Station G2 station-g2 esp32-s3
Station G3 station-g3 esp32-s3
LILYGO T-Deck t-deck-tft esp32-s3
LILYGO T-Echo t-echo nrf52840
LILYGO T-Echo Plus t-echo-plus nrf52840
LILYGO T-Impulse Plus t-impulse-plus nrf52840
LilyGo T3-C6 tlora-c6 esp32-c6
Seeed SenseCAP T1000-E tracker-t1000-e nrf52840

Build artifacts expire on 2026-08-23. Updated for 6efce75.

@Ixitxachitl Ixitxachitl mentioned this pull request Jul 24, 2026
8 tasks
@caveman99
caveman99 added this pull request to the merge queue Jul 24, 2026

@mverch67 mverch67 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested with ThinkNode M9 and the bug from 11094 is fixed.

Merged via the queue into develop with commit 01d1873 Jul 24, 2026
105 checks passed
thebentern added a commit that referenced this pull request Jul 24, 2026
…FS (#11190)

Since #10967 made Router::sendLocal handle self-addressed packets
synchronously, the entire phone-API chain for a BLE client runs inline in
the Bluefruit characteristic write callback: toRadioWriteCb ->
PhoneAPI::handleToRadio -> admin set-config -> radio reconfigure ->
NodeDB::saveToDisk. That callback executes on the Bluefruit BLE FreeRTOS
task, whose stock stack is 5 KB (CFG_BLE_TASK_STACKSIZE = 256*5 words) -
not the Arduino loop task that #10944 already raised to 8 KB. The loop-task
fix therefore protects the wrong task for BLE-originated writes.

On a Seeed Wio Tracker L1 the 5 KB stack overflows during pairing
first-sync, resetting the device mid-LittleFS-write, every single time.
Repeated mid-write resets tear the LittleFS metadata, lfs_assert fires on
the next boot, and the corruption handler formats the whole filesystem:
region, channels, module config, and the node's keypair are all lost
(critical fault #13, new node identity on next region set). Reproduced
end-to-end tonight on stock develop 6908d27; with this change the same
device pairs, serves config screens, and survives back-to-back
config.proto saves over BLE.

Raise the BLE task to the same 2048 words (8 KB) as LOOP_STACK_SZ, for the
same reason. bluefruit.cpp's #ifndef guard makes the -D take effect with no
framework patch. Costs 3 KB of RAM on nrf52840 targets only.

Credit where due: Ixitxachitl independently established in #11155 testing
that the save-path crash persists after #11185 and that re-queueing
sendLocal (moving the pipeline back to the Router thread) makes it go away
- which corroborates this diagnosis from the other direction. This commit
is the minimal capacity-side fix; #11155's relocation of the pipeline off
the BLE task remains the right architectural follow-up, and this guard
stays correct even after it lands.

Likely also explains #10905 (L1 display-thread crash when a client
requests full configuration) and the 2.8 field reports of idle nodes
losing region and keys after a BLE session.
@thebentern
thebentern deleted the fix/local-admin-reply-to-phone branch July 24, 2026 11:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Pull request that fixes bugs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants