mirror of
https://github.com/meshtastic/firmware.git
synced 2026-11-07 23:04:49 -05:00
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.
272 lines
8.1 KiB
C++
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, ¶ms, &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
|