Commit Graph
4 Commits
Author SHA1 Message Date
James Rich 7e7248f6b3 Join an ESP32 advertisement the controller splits across scan reports
An HCI LE Extended Advertising Report carries at most 229 bytes of data, so
the controller hands a longer advertisement up as several reports from the
same address and SID, each but the last INCOMPLETE. NimBLE passes them
through one by one. handleExtendedAdvertisement() dropped every INCOMPLETE
report on the belief that nothing chains on send, then parsed the COMPLETE
tail as if it were the whole advertisement, so an ESP32 heard no packet over
221 bytes. That belief was wrong twice: a 251-byte advertisement is more than
the 245 one AUX_ADV_IND holds, so the Link Layer chains it, and a single PDU
over 229 bytes is split at HCI anyway.

BLEMeshHandler::onScanReport() now joins reports keyed on (address type,
address, SID): INCOMPLETE starts or extends a buffer, COMPLETE finishes it
and delivers once, TRUNCATED discards it. Memory is bounded at
BLE_MESH_REASSEMBLY_SLOTS buffers of BLE_MESH_ADV_TOTAL_MAX; a further
advertiser evicts the stalest, anything longer than a mesh advertisement is
skipped to its last report, and HCI marks no first report, so a buffer idle
for BLE_MESH_REASSEMBLY_TIMEOUT_MS is abandoned rather than prefixed to the
next advertisement.

The AD parser moves into BLEMeshHandler beside it, shared with nRF52. The
nRF52 scan path needs no join: S140 does not support report_incomplete_evts,
assembles a chained advertisement into the 255-byte scan buffer itself, and
reports it COMPLETE, TRUNCATED or MISSED, of which only COMPLETE was ever
parsed.

The comments that called 251 bytes one unfragmented PDU now say what it is:
the most one HCI Set Extended Advertising Data command carries.
2026-10-12 19:34:28 -05:00
James Rich 9c4fdca9f0 Give the legacy BLE-mesh advertiser its burst in milliseconds
NimBLE's ble_gap_adv_start takes duration_ms in milliseconds; only the
extended ble_gap_ext_adv_start counts its duration in 10 ms units. BLE_MESH_ADV_EVENTS * 10
asked for 30 ms, about one event at the 30 ms interval rather than three.
bleMeshBurstMs() converts events and interval to milliseconds, with one
interval of slack for the advDelay each event may add: 120 ms for 3 events at
48 x 0.625 ms.

No env builds this branch today: every _blemesh env sets
BLE_MESH_USE_EXT_ADV, so the fix is covered by the native test of the helper,
not by a build.
2026-10-12 19:34:28 -05:00
James Rich f87dcdbe5c Drop the BLE advertisement peer table, which nothing read
ESP32BLEMesh and NRF52BLEMesh each kept peers[] and peerCount, updated on
every heard burst and pruned on a five-minute timeout, and nothing read them
on either platform beyond a debug line counting new entries. Routing and
dedup never consult it: the advertisement bearer is connectionless and keys
nothing on the sender's address. The table, updatePeer(), pruneStale(),
BLE_MESH_MAX_PEERS and BLE_MESH_PEER_TIMEOUT_MS go, and so does the scan
address the ESP32 handler passed along only to fill it.
2026-10-12 19:34:28 -05:00
James Rich 0e4d67c657 Add the BLE advertisement mesh bearer for ESP32 and nRF52
A second broadcast transport beside LoRa, carried in BLE extended
advertisements: each node advertises a mesh packet as manufacturer data
and scans for its peers' bursts, with no connection between them. It is
registered with the transport registry as TRANSPORT_BLE_ADV and enabled
by ProtocolFlags.BLE_BROADCAST in network.enabled_protocols. Off by
default and compiled only into envs that opt in, so nothing changes for
an existing build.

BLEMeshHandler holds the platform-independent half: framing, the ingress
guards mirrored from UdpMulticastHandler, the transmit ring and the peer
table. ESP32BLEMesh and NRF52BLEMesh drive the two BLE stacks. The nRF52
side polls nrf52BluetoothReady rather than being called, because
NRF52Bluetooth::setup() can run before main() has constructed the
handler. A scan needs a central link, which the shared linker script's
RAM origin cannot afford, so the nRF52 central is behind
BLE_MESH_NRF52_CENTRAL for the env that ships the re-based script.

On ESP32 the transport needs CONFIG_BT_NIMBLE_EXT_ADV, and IDF's NimBLE
compiles the legacy advertising API out once that is set: every
ble_gap_adv_* call returns BLE_HS_ENOTSUP, and the Arduino wrapper has
no extended equivalent. So under BLE_MESH_USE_EXT_ADV the phone API's
own advertisement moves to ext-adv instance 0 as a legacy ADV_IND,
identical on air to what the wrapper produced, chained to the wrapper's
private GAP handler so its subscription bookkeeping still sees the phone
link. The mesh burst takes instance 1.

Two ESP32 changes ride along. A one-time clear of the NimBLE bond store
under HAS_BLE_MESH, because NimBLE was seen to fail to come up after
flipping between builds with different BLE configs, which is this
feature's hazard. And on the S3 and C3 the default PHY is pinned to 1M
for every build of those targets: the controller asserts in its remote
PHY-update handler (esp-idf#15311) when an Apple central moves the link
to 2M, and ble_gap_set_default_le_phy() is compiled out of the prebuilt
host, so the HCI command is sent directly once the host has synced.

test_ble_mesh covers the handler on native, which is why portduino
defines HAS_BLE_MESH: nothing is instantiated there at runtime. The
suite constructs a NodeDB for the self-echo check, so it declares the
default pref writes in test/state-manifest.tsv like every suite that does.

The generated nanopb headers are not part of this change. The
update_protobufs workflow supplies them once the protobufs PR carrying
the enums merges.
2026-10-12 19:34:28 -05:00