Repository navigation
Add the BLE GATT mesh-peer transport - #12010
Draft
jamesarich wants to merge 13 commits into
Draft
jamesarich wants to merge 13 commits into
jamesarich wants to merge 13 commits into
Conversation
Contributor
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
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. Comment |
Contributor
⚡ Try this PR in the Web FlasherNote Building this pull request… the flash button, badges and supported-board |
Contributor
|
jamesarich
added a commit
that referenced
this pull request
Oct 1, 2026
It rebuilds the host and controller for two connections and three controller activities (the advertisement and both links); every other ESP32 env keeps one connection and two activities. board_level = pr so CI and MemBrowse measure it while #12010 is a draft, like rak4631_gattpeer.
jamesarich
force-pushed
the
feat/ble-gatt-mesh-peer
branch
from
October 1, 2026 21:58
d4fe5ad to
1d53f35
Compare
jamesarich
force-pushed
the
feat/ble-gatt-mesh-peer
branch
from
October 1, 2026 23:14
1d53f35 to
ea8c4ac
Compare
jamesarich
changed the base branch from
feat/transport-registry
to
feat/ble-phone-api-pairing
October 1, 2026 23:14
This was referenced Oct 2, 2026
jamesarich
added a commit
that referenced
this pull request
Oct 2, 2026
… mesh-peer It rebuilds the host and controller for two connections and three controller activities (the advertisement and both links); every other ESP32 env keeps one connection and two activities. It inherits heltec-v3's board_level = pr, so CI and MemBrowse measure it while #12010 is a draft, as they do rak4631_blemesh.
jamesarich
force-pushed
the
feat/ble-gatt-mesh-peer
branch
from
October 2, 2026 02:10
ea8c4ac to
f5a69e7
Compare
jamesarich
force-pushed
the
feat/ble-phone-api-pairing
branch
from
October 3, 2026 22:36
177f75f to
5b04d3a
Compare
jamesarich
added a commit
that referenced
this pull request
Oct 3, 2026
… mesh-peer It rebuilds the host and controller for two connections and three controller activities (the advertisement and both links); every other ESP32 env keeps one connection and two activities. It inherits heltec-v3's board_level = pr, so CI and MemBrowse measure it while #12010 is a draft, as they do rak4631_blemesh.
jamesarich
force-pushed
the
feat/ble-gatt-mesh-peer
branch
from
October 3, 2026 22:36
f5a69e7 to
fb5bfb2
Compare
jamesarich
force-pushed
the
feat/ble-phone-api-pairing
branch
from
October 3, 2026 23:48
5b04d3a to
caa6e59
Compare
jamesarich
added a commit
that referenced
this pull request
Oct 3, 2026
… mesh-peer It rebuilds the host and controller for two connections and three controller activities (the advertisement and both links); every other ESP32 env keeps one connection and two activities. It inherits heltec-v3's board_level = pr, so CI and MemBrowse measure it while #12010 is a draft, as they do rak4631_blemesh.
jamesarich
force-pushed
the
feat/ble-gatt-mesh-peer
branch
2 times, most recently
from
October 7, 2026 18:04
a57d844 to
8fcc4ee
Compare
jamesarich
force-pushed
the
feat/ble-phone-api-pairing
branch
from
October 7, 2026 18:04
caa6e59 to
67a0942
Compare
The GATT mesh-peer transport gates on ProtocolFlags.BLE_GATT_PEER and tags its arrivals TransportMechanism.TRANSPORT_BLE_GATT, neither of which exists on protobufs master yet. This pins the submodule at the protobufs branch that adds them (feat/ble-mesh-protocol-flags), alongside BLE_BROADCAST and TRANSPORT_BLE_ADV for the advertisement transport that follows. Nothing under src/mesh/generated/ changes here: the update_protobufs workflow regenerates it once the protobufs PR merges, and a local bin/regen-protos.sh run supplies the headers until then.
… a mesh peer A phone can already reach a node's phone API over BLE, but it cannot be a node on the mesh through that link: the phone API carries commands and FromRadio state, not mesh frames. This adds a second GATT service, the mesh-peer service, on which a client library (node-transport-ble-gatt) exchanges whole encrypted MeshPackets with the node - the SIG Mesh "GATT proxy" role. The node stays the GATT server; the phone is a central that subscribes to the mesh characteristic and writes fragments to it. BLEGattMeshHandler is the platform-neutral half and holds everything the client library speaks, so the native suite covers it: the fragment framing ([version][id][index][total][payload], one ATT value per fragment), bounded reassembly (per-peer in-flight limit, per-table peer limit, expiry, so a hostile writer cannot fill memory), the ingress guards shared with the UDP transport (no sender, our own node number, out-of-range hop counts, local-only flags cleared), a relay exclusion so a packet is never written back to the peer that delivered it unless that peer originated it (the echo is its only implicit ack on a point-to-point bearer), the HELLO link-control frame that names a node on its GATT links so a node reached both ways can elect one link, and a liveness probe for a link this node dialled. No platform here dials yet, so the probe and shed hooks keep their base-class defaults; the dial-out half is a later change. It registers as a PostEncode transport, gated on ProtocolFlags.BLE_GATT_PEER, and tags its arrivals TRANSPORT_BLE_GATT. NRF52BLEGattMesh is the Bluefruit half: the service and characteristic, CCCD tracking (a link becomes a mesh peer when it subscribes), a write queue from Bluefruit's callback task to the main-task pump, per-connection MTU chunking, and re-arming the connectable advertisement while a peripheral slot is free, which Bluefruit only does by itself once no link is left. Bluefruit.begin(2, 0): two peripheral links, the phone and one mesh peer, no central. NRF52Bluetooth has to stop assuming one link. The phone is whichever link uses the phone API, not the most recent connect, so notePhoneLink() learns it from the first phone-API access and only that link's drop closes the phone session; fromNum and logRadio notify that handle explicitly rather than Bluefruit's default. The mesh-peer UUID rides in the scan response in place of the TX power entry, since S140 has one advertising set. The nanopb enums come from the protobufs branch this submodule now points at; nothing under src/mesh/generated/ is committed, the regen workflow supplies it once that PR merges (bin/regen-protos.sh locally until then). Native tests in test_ble_gatt_mesh, built by the portduino env's new HAS_BLE_GATT_MESH flag: framing and its bounds, reassembly, every ingress guard, per-peer fragmentation, the relay exclusion and its originator exception, the bounded TX ring and busy-stack retry, the greeting, the election, and the probe timings.
rak4631_blemesh is the BLE-mesh opt-in build for the board: rak4631 with HAS_BLE_GATT_MESH=1 and nothing else, so no central link and the stock linker script. network.enabled_protocols selects the bearer at runtime, so one opt-in env per board carries every bearer the build has. It inherits rak4631's board_level, so CI and MemBrowse build it with the PR matrix. Whether two peripheral links fit the SoftDevice RAM origin that script reserves is a question for the boot log: Bluefruit.begin() logs "SoftDevice RAM too small" and records a critical error if they do not.
Measured on a RAK4631: with HAS_BLE_GATT_MESH and the stock linker script, Bluefruit.begin(2, 0) fails with "SoftDevice RAM too small" and the node boots with no Bluetooth at all. The SoftDevice sizes its RAM by links, not by role, so the second peripheral link crosses the shared script's 0x20004000 origin the same way the scanning central does. The re-based script moves here, to the first rung that needs it; the advertisement bearer uses the same file.
ESP32BLEGattMesh is the NimBLE half of BLEGattMeshHandler, peripheral only. One legacy connectable advertisement serves the phone and the mesh peers, with the mesh-peer service UUID in its scan response as on the nRF52, and a link becomes a mesh peer when it subscribes to the mesh characteristic. Notifies go to one connection at a time so a relay skips the peer it came from. With the GATT peer protocol on, NimbleBluetooth registers every link from onConnect, re-arms the advertisement while a connection slot is free, and identifies the phone by phone-API use instead of by pairing, so a mesh peer dropping no longer ends the phone's session. Security starts on demand rather than on connect, so a mesh peer is never pushed into pairing; the phone API keeps its attribute protection. deinit() disconnects every link before freeing the server. All of it is compiled only with HAS_BLE_GATT_MESH and inert unless the BLE_GATT_PEER protocol bit is set.
… mesh-peer It rebuilds the host and controller for two connections and three controller activities (the advertisement and both links); every other ESP32 env keeps one connection and two activities. It inherits heltec-v3's board_level = pr, so CI and MemBrowse measure it while #12010 is a draft, as they do rak4631_blemesh.
…tral cannot crash it Same HCI command as feat/ble-adv-mesh, scoped to HAS_BLE_GATT_MESH so stock builds are unchanged.
A radio arrival has via_mqtt from the header flags, and ignore_mqtt and the MQTT uplink's loop guard read it, so clearing it on a GATT arrival let an MQTT-origin packet relayed over GATT be uplinked again. tx_after and priority are still cleared, since the LoRa header carries neither.
deliverToRouter() carried its own copy of the ingress guards; it now calls MeshTransportBase::sanitizeIngress(), the rule set UDP and MQTT use. onSend() encodes stripForTransmit()'s copy, so a GATT peer no longer receives this node's rx_rssi/rx_snr, priority, tx_after or transport_mechanism. The five copied ingress tests move to test_transport_ingress, which now runs every rule for the GATT bearer as well. test_ble_gatt_mesh keeps the decode-failure case, which is GATT's own, and gains one for the egress strip.
BLE_GATT_PEER rides on the phone-API Bluetooth stack. On an ESP32 that stack starts only when bluetooth.enabled is set and Wi-Fi is not in use; on an nRF52 with bluetooth.enabled false, startDisabled() registers the service but never advertises it. Either way the flag did nothing and nothing said so, while main() started the pump regardless. setBluetoothEnable()'s gate becomes bluetoothBlockedReason(), the same expression, which now also names the condition that failed. At boot a set flag that cannot take effect logs one warning naming it, and the pump is not started. The GATT bit is read through BLEGattMeshHandler::protocolEnabled() and main() asks isEnabled().
ESP32BLEGattMesh and NRF52BLEGattMesh each carried the same link table, the same RX ring and the same disconnect-marker rule, and the copies had started to disagree: the nRF52 pushRx logged a full queue while onWrite still held the lock, where the ESP32 copy returned and let its caller log after releasing it. BLEGattMeshLinks holds the one copy. It never logs, so both platforms now log after the lock, and the nRF52 drop warning moves out from under it. Each platform keeps its own lock, its stack calls and its link count. The ring is platform-neutral now, so test_ble_gatt_mesh pins the marker that must land in a full ring and the mesh-peer report on disconnect. The platform static hooks read the GATT bit through protocolEnabled().
jamesarich
force-pushed
the
feat/ble-gatt-mesh-peer
branch
from
October 11, 2026 16:05
8fcc4ee to
98cbeb2
Compare
jamesarich
force-pushed
the
feat/ble-phone-api-pairing
branch
from
October 11, 2026 16:06
67a0942 to
37ff2ec
Compare
A connectable advertisement stops once every peripheral link is taken. The rak4631_blemesh build has two, and nothing stopped two mesh peers from holding both, so the RAK4631 stopped advertising and no phone could reach its phone API until a peer left. heltec-v3_blemesh has the same two NimBLE links and the same exposure. Mesh peers other than the phone's own link may now hold at most all but one link. A mesh subscribe that would take the last one is dropped at the CCCD write, which frees the link and brings the advertisement back. The phone is the link that uses the phone API, so a phone that is also a mesh peer over one link is never counted against the cap.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Draft so MemBrowse and the PR matrix measure what the GATT mesh-peer transport costs an nRF52 and an ESP32-S3. A phone or another node connects as a mesh peer over GATT and exchanges whole mesh packets, beside the phone API link.
What changed
BLEGattMeshHandlercarries mesh frames over a mesh-peer GATT service, withNRF52BLEGattMeshandESP32BLEGattMeshas the platform halves.tx_afterandpriority, which the LoRa header does not carry, and keepsvia_mqtt, which it does.rak4631_blemeshandheltec-v3_blemeshare the opt-in BLE mesh envs, one per board. Add the BLE advertisement mesh transport #12011 adds the advertisement mesh to the same envs, andBLE_GATT_PEERinenabled_protocolsturns GATT on at runtime.rak4631_blemeshusesnrf52840_s140_v6_blemesh.ld, because a second peripheral link raises the SoftDevice's RAM requirement past the shared script's origin.heltec-v3_blemeshraises NimBLE to 2 connections and 3 controller activities for that env only.protobufspoints atfeat/ble-mesh-protocol-flagsfor theTRANSPORT_BLE_GATTandBLE_GATT_PEERenums, with the headers regenerated.board_level = prfrom their parents, so the PR matrix and MemBrowse build them.Testing
On a RAK4631 (OTAFIX 2.5) against stock
rak4631at the fork point: static RAM +3.3 KB, flash +6.9 KB, free heap 15.8 KB lower with one GATT peer linked (a Linux node-kmp peer, 25 min, link held throughout).On a Heltec V3 (
heltec-v3_blemesh, default PIN mode, WiFi off) against a Linux node-kmp central: linked with no pairing prompt, a PKI DM was acked with a valid ack proof in 180 ms, and a RAK4631's LoRa text was relayed to the node-kmp peer over GATT. Static RAM +3.2 KB, flash +8.8 KB against stockheltec-v3.test_ble_gatt_meshandtest_transport_registrypass. Not tested: iOS and Android peers, free heap with two links on ESP32.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
protobufspointer can move fromfeat/ble-mesh-protocol-flagsto protobufsmaster.