Skip to content

Fix/call modules - #11155

Closed
Ixitxachitl wants to merge 13 commits into
meshtastic:developfrom
Ixitxachitl:fix/callModules
Closed

Fix/call modules#11155
Ixitxachitl wants to merge 13 commits into
meshtastic:developfrom
Ixitxachitl:fix/callModules

Conversation

@Ixitxachitl

Copy link
Copy Markdown
Contributor

Summary

Router::sendLocal() was calling handleReceived() synchronously for locally-addressed packets. When a module (e.g. an admin/module-config handler) generates a reply from inside MeshModule::callModules() and sends it via service->sendToMesh(), that synchronous call re-entered callModules() from within itself, which caused reply packets to phone requests to be dropped instead of delivered.

This PR restores queuing of locally-addressed packets through fromRadioQueue (avoiding the re-entrancy), while fixing the underlying reason queuing had been avoided before: queuing used to erase the packet's RxSource, making a local phone/module-generated packet indistinguishable from already-decoded remote ingress once it was dequeued.

Changes

  • Router::fromRadioQueue is now a TypedQueue<QueuedFromRadio> (packet + RxSource pair) instead of a PointerQueue<meshtastic_MeshPacket>, so origin is preserved across the queue.
  • Router::enqueueReceivedMessage() and Router::perhapsHandleReceived() now take/forward an RxSource (defaulting to RX_SRC_RADIO for existing radio/MQTT/UDP callers).
  • Router::sendLocal() queues locally-addressed packets via enqueueReceivedMessage(p, src) instead of calling handleReceived() in-line, avoiding re-entrant callModules() calls while still replaying the packet with its true origin.
  • MeshModule::callModules() now explicitly passes RX_SRC_LOCAL (and isToUs(currentReply)) when sending a reply packet, so it reaches the phone instead of being silently dropped by the loopback guard.

Fixes reply packets (e.g. admin/module-config responses to the phone) being silently dropped.

🤝 Attestations

  • I have tested that my proposed changes behave as described.
  • I have tested that my proposed changes do not cause any obvious regressions on the following devices:
    • Heltec (Lora32) V3
    • LilyGo T-Deck
    • LilyGo T-Beam
    • RAK WisBlock 4631
    • Seeed Studio T-1000E tracker card
    • Other (please specify below)

Tested on a Seeed WIO Tracker L1. Have not been able to test on the other listed hardware — help testing on those devices would be appreciated.

@github-actions

github-actions Bot commented Jul 22, 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-21. Updated for cbbe131.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6a6cceb8-d32d-4564-a3ae-330da5dfdec5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@caveman99 caveman99 added the bugfix Pull request that fixes bugs label Jul 23, 2026

@caveman99 caveman99 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Overall agreement, please see comments inline.

Comment thread src/mesh/Router.cpp
// Note: we avoid calling shouldFilterReceived if we are supposed to ignore certain nodes - because some overrides might
// cache/learn of the existence of nodes (i.e. FloodRouter) that they should not
handleReceived(p);
handleReceived(p, src);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

locally-addressed packets now traverse perhapsHandleReceived() which means they are dedup ed and checked against the ignore-list. They are also added to the PacketHistory. Confirm a module emitting rapid to-phone-only packets can't get deduped.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You're right, locally-generated replies were falling through the ignore-list/PacketHistory/MQTT/pre-hop checks meant for radio ingress. handleReceived() already special-cases RX_SRC_LOCAL internally (e.g. it only applies the routing-auth cache when src == RX_SRC_RADIO), so I've made perhapsHandleReceived() skip straight to handleReceived() when src == RX_SRC_LOCAL, bypassing the filter pipeline entirely for local packets, same as the pre-PR behavior, just routed through the queue instead of called inline. Pushed in a2c03e5.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Follow-up: that guard was too narrow. RX_SRC_USER (a phone/serial packet addressed to the local node, e.g. a config request) hits the same perhapsHandleReceived() path and wasn't exempted, so it still traversed the ignore-list/dedup/pre-hop/routing-auth filters meant only for radio ingress — the same risk you flagged, just on the request side instead of the reply side. In the pre-PR code RX_SRC_USER never went through perhapsHandleReceived() at all (synchronous handleReceived() call), so this was a regression from queuing it. Widened the check from src == RX_SRC_LOCAL to src != RX_SRC_RADIO to cover both trusted non-radio sources. Pushed.

Comment thread src/mesh/Router.cpp
// within itself. The queue carries src through so the packet is still replayed with its
// true origin instead of defaulting to RX_SRC_RADIO.
enqueueReceivedMessage(p, src);
return ERRNO_OK;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

return code changed from ERRNO_SHOULD_RELEASE to ERRNO_OK (the queue now owns/frees the packet). Every other sendLocal caller must be verified to no longer release on ERRNO_OK or else we have another double-free/leak.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Checked all three sendLocal() callers:

  • MQTT.cpp:127 — if (router->sendLocal(pAck) == ERRNO_SHOULD_RELEASE) packetPool.release(pAck);
  • RoutingModule.cpp:60 — same pattern
  • MeshService.cpp:348 — same pattern

All three only release when the result is ERRNO_SHOULD_RELEASE, never on ERRNO_OK. Since the local/queued path now returns ERRNO_OK, none of them double-release, the queue owns the packet and it's freed once in perhapsHandleReceived() after processing. No double-free/leak.

@Ixitxachitl

Copy link
Copy Markdown
Contributor Author

This also fixes the same config-get/set phone-timeout regression that #11185 targets (both trace back to #10967 making sendLocal()'s isToUs branch call handleReceived() synchronously) — MeshModule.cpp's existing service->sendToMesh(currentReply, RX_SRC_LOCAL, isToUs(currentReply)) already forces phone delivery at the call site that matters, so no equivalent to #11185's MeshService.cpp change is needed here. Unlike #11185, this approach doesn't reintroduce the synchronous re-entry into callModules(), which I confirmed overflows the stack on nRF52840 (crash on save, e.g. Wio Tracker L1) — #11185 alone still has that crash since it keeps the synchronous handleReceived() call.

Rebasing onto current develop will need two test updates that aren't new regressions, just stale coupling to the old synchronous contract: test_packet_signing's test_C8 asserts the old ERRNO_SHOULD_RELEASE return, and test_mqtt's MockRouter::enqueueReceivedMessage override needs the widened RxSource parameter to compile. Happy to push both.

@caveman99

Copy link
Copy Markdown
Member

i opted for approving #11185 since it's the most surgical approach of fixing the regression. Regarding the stack overflow, that'as actually easier to fix now #11185 decouples phone delivery from handleReceived entirely. Add a depth counter to the router. if it is not zero, it's calld from within callModules and you enqueue a copy of the packet with its RxSource into a small local queue, then return ERRNO_SHOULD_RELEASE. Depth 0 has no change to today.

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.
LN4CY pushed a commit to LN4CY/meshtastic-firmware that referenced this pull request Jul 25, 2026
…eshtastic#11191)

Prevents an nRF52 task-stack overflow on config save. Replaces draft meshtastic#11155.
madeofstown pushed a commit to madeofstown/meshtastic-firmware that referenced this pull request Jul 26, 2026
…eshtastic#11191)

Prevents an nRF52 task-stack overflow on config save. Replaces draft meshtastic#11155.
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.

2 participants