Files
firmware/src/platform/esp32/ESP32BLEMesh.cpp
T
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

272 lines
8.1 KiB
C++

#include "configuration.h"
#if HAS_BLE_MESH && defined(ARCH_ESP32)
#include "ESP32BLEMesh.h"
#include "main.h"
#include "mesh/Router.h"
#include "nimble/NimbleBluetooth.h"
#include "host/ble_gap.h"
#include "host/ble_hs.h"
#include "host/ble_hs_adv.h"
#include "nimble/hci_common.h"
#if BLE_MESH_USE_EXT_ADV
static_assert(BLE_MESH_ADV_TOTAL_MAX <= BLE_HCI_MAX_EXT_ADV_DATA_LEN,
"advertisement budget must fit one NimBLE LE Set Extended Advertising Data command");
#endif
void ESP32BLEMesh::start()
{
if (isRunning) {
LOG_DEBUG("BLE mesh already running");
return;
}
isRunning = true;
LOG_INFO("BLE mesh started (waiting for Bluetooth ready)");
}
bool ESP32BLEMesh::platformReady()
{
// Polled, not a callback: construction order against NimbleBluetooth is not fixed.
// isActive(), not ble_hs_synced() - the host syncs before NimbleBluetooth::setup() registers its
// service, and starting a scan in that window races the stack's own GAP configuration.
return nimbleBluetooth && nimbleBluetooth->isActive();
}
void ESP32BLEMesh::onBluetoothReady()
{
if (!isRunning)
return;
startScanning();
LOG_INFO("BLE mesh Bluetooth ready, scanning");
}
void ESP32BLEMesh::stop()
{
if (!isRunning)
return;
platformEndAdvertising();
stopScanning();
isRunning = false;
LOG_INFO("BLE mesh stopped");
}
#if BLE_MESH_USE_EXT_ADV
bool ESP32BLEMesh::configureAdvInstance()
{
if (advInstanceConfigured)
return true;
struct ble_gap_ext_adv_params params = {};
// legacy_pdu must stay 0, or the payload is back to the 31-byte limit, which cannot hold a mesh
// frame at all.
params.connectable = 0;
params.scannable = 0;
params.directed = 0;
params.high_duty_directed = 0;
params.legacy_pdu = 0;
params.anonymous = 0;
params.include_tx_power = 0;
params.scan_req_notif = 0;
params.itvl_min = BLE_MESH_ADV_INTERVAL;
params.itvl_max = BLE_MESH_ADV_INTERVAL;
params.channel_map = 0; // all channels
params.own_addr_type = BLE_OWN_ADDR_PUBLIC;
params.primary_phy = BLE_HCI_LE_PHY_1M;
params.secondary_phy = BLE_HCI_LE_PHY_1M;
params.tx_power = 127; // controller picks its maximum
params.sid = 0;
int8_t selectedTxPower = 0;
int rc = ble_gap_ext_adv_configure(BLE_MESH_ADV_INSTANCE, &params, &selectedTxPower, onGapEvent, this);
if (rc != 0) {
LOG_WARN("BLE mesh ext adv configure failed: %d", rc);
return false;
}
// Left configured: only the data changes between frames, so reconfiguring per packet is a GAP
// round trip for nothing.
advInstanceConfigured = true;
return true;
}
#endif
bool ESP32BLEMesh::platformBeginAdvertising(const uint8_t *adv, size_t len)
{
#if BLE_MESH_USE_EXT_ADV
if (!configureAdvInstance())
return false;
struct os_mbuf *advData = os_msys_get_pkthdr(len, 0);
if (!advData) {
LOG_WARN("BLE mesh: failed to allocate mbuf");
return false;
}
if (os_mbuf_append(advData, adv, len) != 0) {
os_mbuf_free_chain(advData);
return false;
}
// set_data takes ownership on success and frees on failure - do not touch advData after this.
int rc = ble_gap_ext_adv_set_data(BLE_MESH_ADV_INSTANCE, advData);
if (rc != 0) {
LOG_WARN("BLE mesh ext adv set data failed: %d", rc);
return false;
}
// Bounded by max_events, not by duration: duration is the middle argument and is in 10ms units,
// not a repeat count.
rc = ble_gap_ext_adv_start(BLE_MESH_ADV_INSTANCE, 0, BLE_MESH_ADV_EVENTS);
if (rc != 0) {
LOG_WARN("BLE mesh ext adv start failed: %d", rc);
return false;
}
return true;
#else
// Legacy advertising (BLE 4.2, 31 bytes total) cannot hold a mesh frame. This path exists to
// keep the build uniform across ESP32 parts, not because classic ESP32 can join a BLE mesh.
if (len > 31) {
LOG_DEBUG("BLE mesh: %u bytes exceeds legacy advertising capacity, not sent", (unsigned)len);
return false;
}
struct ble_gap_adv_params legacyParams = {};
legacyParams.conn_mode = BLE_GAP_CONN_MODE_NON;
legacyParams.disc_mode = BLE_GAP_DISC_MODE_GEN;
legacyParams.itvl_min = BLE_MESH_ADV_INTERVAL;
legacyParams.itvl_max = BLE_MESH_ADV_INTERVAL;
legacyParams.channel_map = 0;
if (ble_gap_adv_set_data(adv, len) != 0)
return false;
// The legacy API bounds a burst by milliseconds, not events, so convert the extended path's count.
return ble_gap_adv_start(BLE_OWN_ADDR_PUBLIC, NULL, bleMeshBurstMs(BLE_MESH_ADV_EVENTS, BLE_MESH_ADV_INTERVAL), &legacyParams,
onGapEvent, this) == 0;
#endif
}
bool ESP32BLEMesh::platformAdvertisingActive()
{
#if BLE_MESH_USE_EXT_ADV
return ble_gap_ext_adv_active(BLE_MESH_ADV_INSTANCE) != 0;
#else
return ble_gap_adv_active() != 0;
#endif
}
void ESP32BLEMesh::platformEndAdvertising()
{
#if BLE_MESH_USE_EXT_ADV
// Stop but do NOT remove: the instance stays configured for the next frame.
ble_gap_ext_adv_stop(BLE_MESH_ADV_INSTANCE);
#else
ble_gap_adv_stop();
#endif
}
void ESP32BLEMesh::startScanning()
{
if (!platformReady())
return;
#ifdef BLE_MESH_TX_ONLY
LOG_INFO("BLE mesh: TX-only build, not scanning");
return;
#endif
#if BLE_MESH_USE_EXT_ADV
struct ble_gap_ext_disc_params uncodedParams = {};
uncodedParams.itvl = BLE_MESH_SCAN_INTERVAL;
uncodedParams.window = BLE_MESH_SCAN_WINDOW;
uncodedParams.passive = 1; // never scan-request; the payload is all in the advertisement
// filter_duplicates MUST stay 0: the controller de-duplicates on advertiser address, not on
// payload, so every frame after a neighbour's first would be filtered away.
int rc = ble_gap_ext_disc(BLE_OWN_ADDR_PUBLIC, 0 /* duration: forever */, 0 /* period */, 0 /* filter_duplicates */,
BLE_HCI_SCAN_FILT_NO_WL, 0 /* limited */, &uncodedParams, NULL, onGapEvent, this);
#else
struct ble_gap_disc_params scanParams = {};
scanParams.passive = 1;
scanParams.itvl = BLE_MESH_SCAN_INTERVAL;
scanParams.window = BLE_MESH_SCAN_WINDOW;
scanParams.filter_duplicates = 0; // see above
scanParams.limited = 0;
scanParams.filter_policy = BLE_HCI_SCAN_FILT_NO_WL;
int rc = ble_gap_disc(BLE_OWN_ADDR_PUBLIC, BLE_HS_FOREVER, &scanParams, onGapEvent, this);
#endif
if (rc == 0) {
LOG_DEBUG("BLE mesh scanning started");
} else if (rc == BLE_HS_EALREADY) {
LOG_DEBUG("BLE mesh scanning already active");
} else {
LOG_WARN("BLE mesh scan start failed: %d", rc);
}
}
void ESP32BLEMesh::stopScanning()
{
if (!platformReady())
return;
ble_gap_disc_cancel();
}
int ESP32BLEMesh::onGapEvent(struct ble_gap_event *event, void *arg)
{
auto *self = static_cast<ESP32BLEMesh *>(arg);
if (!self)
return 0;
switch (event->type) {
case BLE_GAP_EVENT_DISC:
self->handleAdvertisement(&event->disc);
break;
#if BLE_MESH_USE_EXT_ADV
case BLE_GAP_EVENT_EXT_DISC:
self->handleExtendedAdvertisement(&event->ext_disc);
break;
#endif
case BLE_GAP_EVENT_DISC_COMPLETE:
if (self->isRunning) {
self->startScanning();
}
break;
default:
break;
}
return 0;
}
void ESP32BLEMesh::handleAdvertisement(const struct ble_gap_disc_desc *desc)
{
if (!isRunning || !desc)
return;
handleAdvertisementData(desc->data, desc->length_data, desc->rssi);
}
#if BLE_MESH_USE_EXT_ADV
void ESP32BLEMesh::handleExtendedAdvertisement(const struct ble_gap_ext_disc_desc *desc)
{
if (!isRunning || !desc)
return;
ScanReportStatus status = ScanReportStatus::Truncated;
if (desc->data_status == BLE_GAP_EXT_ADV_DATA_STATUS_COMPLETE)
status = ScanReportStatus::Complete;
else if (desc->data_status == BLE_GAP_EXT_ADV_DATA_STATUS_INCOMPLETE)
status = ScanReportStatus::Incomplete;
onScanReport(desc->addr.type, desc->addr.val, desc->sid, status, desc->data, desc->length_data, desc->rssi);
}
#endif
#endif // HAS_BLE_MESH && ARCH_ESP32