Skip to content

Route the non-LoRa egress taps through a transport registry - #11945

Draft
jamesarich wants to merge 2 commits into
developfrom
feat/transport-registry
Draft

jamesarich wants to merge 2 commits into
developfrom
feat/transport-registry

Conversation

@jamesarich

@jamesarich jamesarich commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Router::send() names its non-LoRa outputs one by one: a hardcoded mqtt->onSend() inside the decoded-tag block and a udpHandler->onSend() at the end, each behind its own null check and enabled_protocols gate. Every new bearer means editing that funnel again, and two BLE bearers are on their way.

What changed

MeshTransportBase is a self-registering base in MeshModule's idiom: a transport constructs into one of two static lists and Router::send() fans out to whichever list matches the packet state. PostEncode gets the final encrypted packet and gates on each transport's own isEnabled(); UdpMulticastHandler moves there. UDP and MQTT ingress now share one rule set, sanitizeIngress(): drop a local origin or an out-of-range hop count, stamp the medium, and clear what the LoRa header does not carry, which UDP had never cleared for tx_after, priority or the public_key bytes. UDP egress goes through stripForTransmit(), so a UDP frame no longer carries this node's rx_rssi/rx_snr, priority, tx_after or transport_mechanism. PreEncode fires inside the decoded-tag block with the decoded copy and the channel index, which is what MQTT needs; a stateless MQTTTransport adapter forwards to the singleton and keeps every MQTT-side gate where it was, including the old null check. The two lists never cross, so MQTT can never be handed relayed traffic.

FloodingRouter's overhear cancel keeps LoRa on its own queue and asks the registry for any other medium, strictly same-medium. A transport that emits inline has no queue and declines by default.

No new bearer is in this PR; nothing is enabled that was not enabled before.

Testing

test_transport_registry (new, 8 tests): enabled transports are called in order, a return value never suppresses the next, a disabled one is skipped, an empty registry is a no-op, PreEncode receives both copies, and the two hook points are disjoint. test_transport_ingress (new, 8 tests) pins the ingress rule set and stripForTransmit(). Both pass, 16/16 in Docker at 77e33615a. heltec-v3 builds. No device testing: the MQTT path is the same calls behind a different dispatch, and the UDP ingress and egress changes are covered by native tests only.

Merge order

meshtastic/protobufs#1111, then #11945, #11946, #12010 and #12011, each based on the one before it. #12010 also waits for #1111 so its protobufs pointer can move from feat/ble-mesh-protocol-flags to protobufs master.

@coderabbitai

coderabbitai Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@github-actions

Copy link
Copy Markdown
Contributor

⚡ Try this PR in the Web Flasher

Note

Building this pull request… the flash button, badges and supported-board
list will appear here automatically once CI finishes.

@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

MemBrowse Memory Report

axiometa-genesis-mini

  • dram0_0_seg: .dram0.bss +16 B (+0.0%, 171,080 B / 341,760 B, total: 50% used)
  • drom0_0_seg: .flash.rodata +68 B (+0.0%, 4,460,988 B / 33,554,400 B, total: 13% used)
  • iram0_2_seg: .flash.text +812 B (+0.0%, 1,824,052 B / 8,388,576 B, total: 22% used)

heltec-ht62-esp32c3-sx1262

  • drom0_0_seg: .flash.rodata +84 B (+0.0%, 2,121,543 B / 8,388,576 B, total: 25% used)
  • iram0_2_seg: .flash.text +888 B (+0.1%, 1,711,790 B / 8,388,576 B, total: 20% used)

heltec-mesh-node-t114

  • FLASH: .text +832 B (+0.1%, 719,076 B / 802,816 B, total: 90% used)
  • RAM: .bss +8 B (+0.0%, 105,036 B / 245,760 B, total: 43% used)

heltec-v3

  • dram0_0_seg: .dram0.bss +16 B (+0.0%, 192,696 B / 341,760 B, total: 56% used)
  • drom0_0_seg: .flash.rodata +68 B (+0.0%, 4,451,000 B / 33,554,400 B, total: 13% used)
  • iram0_2_seg: .flash.text +1,020 B (+0.1%, 1,796,468 B / 8,388,576 B, total: 21% used)

heltec-vision-master-e213-inkhud

  • drom0_0_seg: .flash.rodata +64 B (+0.0%, 4,455,720 B / 33,554,400 B, total: 13% used)
  • iram0_2_seg: .flash.text +1,044 B (+0.1%, 1,783,988 B / 8,388,576 B, total: 21% used)

pico2w

  • FLASH: .ARM.exidx +80 B, .ARM.extab +8 B, .rodata +56 B, .text +752 B (+0.1%, 1,216,940 B / 3,665,920 B, total: 33% used)
  • RAM: .bss +8 B (+0.0%, 151,160 B / 524,288 B, total: 29% used)

picow

  • FLASH: .ARM.exidx +72 B, .ARM.extab +12 B, .rodata +56 B, .text +768 B (+0.1%, 1,242,340 B / 1,568,768 B, total: 79% used)
  • RAM: .bss +8 B (+0.0%, 148,848 B / 262,144 B, total: 57% used)

rak4631

  • FLASH: .text +896 B (+0.1%, 712,700 B / 802,816 B, total: 89% used)
  • RAM: .bss +8 B (+0.0%, 96,652 B / 245,760 B, total: 39% used)

seeed-sensecap-indicator-tft

  • dram0_0_seg: .dram0.bss +80 B (+0.0%, 193,068 B / 341,760 B, total: 56% used)
  • drom0_0_seg: .flash.rodata -20 B (-0.0%, 7,590,164 B / 33,554,400 B, total: 23% used)
  • iram0_2_seg: .flash.text +488 B (+0.0%, 2,363,248 B / 8,388,576 B, total: 28% used)

seeed_wio_tracker_L1

  • FLASH: .text +848 B (+0.1%, 709,212 B / 798,720 B, total: 89% used)
  • RAM: .bss +8 B (+0.0%, 104,892 B / 245,760 B, total: 43% used)

t-deck-tft

  • dram0_0_seg: .dram0.bss -8 B (-0.0%, 186,888 B / 341,760 B, total: 55% used)
  • drom0_0_seg: .flash.rodata +40 B (+0.0%, 7,736,156 B / 33,554,400 B, total: 23% used)
  • iram0_2_seg: .flash.text +1,512 B (+0.1%, 2,438,844 B / 8,388,576 B, total: 29% used)

t-echo-plus

  • FLASH: .text +912 B (+0.1%, 720,020 B / 802,816 B, total: 90% used)
  • RAM: .bss +8 B (+0.0%, 87,804 B / 245,760 B, total: 36% used)

thinknode_m7

  • drom0_0_seg: .flash.rodata +84 B (+0.0%, 4,596,344 B / 33,554,400 B, total: 14% used)
  • iram0_2_seg: .flash.text +880 B (+0.1%, 1,669,832 B / 8,388,576 B, total: 20% used)

tlora-c6

  • irom_seg: .flash.rodata +72 B, .flash.text +1,064 B (+0.0%, 4,163,973 B / 16,777,184 B, total: 25% used)

tracker-t1000-e

  • FLASH: .text +832 B (+0.2%, 504,460 B / 798,720 B, total: 63% used)
  • RAM: .bss +8 B (+0.0%, 86,316 B / 245,760 B, total: 35% used)

xiao_nrf54l15_lr2021

  • FLASH: .text +976 B (+0.2%, 578,856 B / 1,306,624 B, total: 44% used)
  • RAM: .bss +8 B (+0.0%, 68,876 B / 243,584 B, total: 28% used)
    No memory changes detected for:
  • rak3172
  • wio-e5

Router::send() named its non-LoRa outputs one by one: a hardcoded
mqtt->onSend() inside the decoded-tag block and a udpHandler->onSend() at
the end, each behind its own null check and enabled_protocols gate. A new
bearer meant editing the funnel again.

MeshTransportBase is a self-registering base in MeshModule's idiom: a
transport constructs into one of two static lists and Router::send() fans
out to whichever list matches the packet state. PostEncode gets the final
encrypted packet and gates on each transport's own isEnabled();
UdpMulticastHandler moves there unchanged in behaviour. PreEncode fires
inside the decoded-tag block with the decoded copy and the channel index,
which is what MQTT needs; a stateless MQTTTransport adapter forwards to the
singleton and keeps every MQTT-side gate where it was, including the old
null check. The two lists never cross: a PreEncode transport is invisible to
the post-encode fan-out, so MQTT can never be handed relayed traffic.

FloodingRouter's overhear cancel keeps LoRa on its own queue and asks the
registry for any other medium, strictly same-medium: an overhear on one
medium says nothing about who heard this node on another. A transport that
emits inline has no queue and declines by default.

Native tests in test_transport_registry: every enabled transport is called
in order, a return value never suppresses the next, a disabled one is
skipped, an empty registry is a no-op, PreEncode receives both copies, and
the two hook points are disjoint.
…ncode on isEnabled()

Each bearer carried its own copy of the ingress guards, and the copies drifted:
UDP never cleared tx_after or priority, and never zeroed the public_key bytes.
MeshTransportBase::sanitizeIngress() now holds the one rule set - drop a local
origin or an out-of-range hop count, stamp the medium, clear what the LoRa
header does not carry (tx_after, priority, pki_encrypted, public_key, rx_*). The
key bytes are zeroed as well as the size because Router::send reads them
without consulting the size. UDP and MQTT ingress call it; MQTT keeps its
gateway-echo implicit ack ahead of it.

stripForTransmit() is the egress half, and UDP now uses it, so a UDP frame no
longer carries this node's rx_rssi/rx_snr, priority, tx_after or
transport_mechanism. A third-party multicast listener reading those from our
frames will see defaults.

callTransportsPreEncode() now consults isEnabled() like callTransports(), so a
new PreEncode transport needs no gate added to Router::send. MQTT's adapter
reports moduleConfig.mqtt.enabled and forwards only our own originations, which
drops its two PostEncode stubs and the Router-side condition; onSend() is a
default no-op. isEnabled() is public, so the three UDP start sites ask the
handler instead of re-reading enabled_protocols.

test_transport_ingress runs every ingress rule once per bearer (UDP through
UdpMulticastHandler::decodeIngress, MQTT directly; test_mqtt covers its
envelope path) and pins the strip. test_transport_registry gains the disabled
PreEncode case.
@jamesarich
jamesarich force-pushed the feat/transport-registry branch from efb1c0d to 77e3361 Compare October 11, 2026 16:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants