Files
firmware/src/platform/nrf52/NRF52BLEMesh.cpp
T
2026-09-14 16:20:07 -04:00

304 lines
9.6 KiB
C++

#include "configuration.h"
#if HAS_BLE_MESH && defined(ARCH_NRF52)
#include "NRF52BLEMesh.h"
#include "NRF52Bluetooth.h"
#include "main.h"
#include "mesh/Router.h"
#include <bluefruit.h>
static_assert(BLE_MESH_ADV_TOTAL_MAX <= BLE_GAP_ADV_SET_DATA_SIZE_EXTENDED_MAX_SUPPORTED,
"advertisement budget must fit the SoftDevice's extended adv data limit");
NRF52BLEMesh *NRF52BLEMesh::instance = nullptr;
static uint8_t bleMeshScanBuffer[BLE_GAP_SCAN_BUFFER_EXTENDED_MAX_SUPPORTED];
static ble_data_t bleMeshScanReportData = {.p_data = bleMeshScanBuffer, .len = sizeof(bleMeshScanBuffer)};
static ble_gap_scan_params_t bleMeshScanParams = {
.extended = 1,
.report_incomplete_evts = 0,
.active = 0,
.filter_policy = BLE_GAP_SCAN_FP_ACCEPT_ALL,
.scan_phys = BLE_GAP_PHY_1MBPS,
.interval = BLE_MESH_SCAN_INTERVAL,
.window = BLE_MESH_SCAN_WINDOW,
.timeout = 0,
.channel_mask = {0, 0, 0, 0, 0},
};
void NRF52BLEMesh::start()
{
if (isRunning) {
LOG_DEBUG("BLE mesh already running");
return;
}
pairedCount();
instance = this;
memset(peers, 0, sizeof(peers));
peerCount = 0;
isRunning = true;
LOG_INFO("BLE mesh started (waiting for Bluetooth ready)");
}
bool NRF52BLEMesh::platformReady()
{
return nrf52BluetoothReady;
}
void NRF52BLEMesh::onBluetoothReady()
{
if (!isRunning)
return;
Bluefruit.setEventCallback(onBleEvent);
startScanning();
LOG_INFO("BLE mesh Bluetooth ready, scanning");
}
void NRF52BLEMesh::stop()
{
if (!isRunning)
return;
cancelPairing();
platformEndAdvertising();
stopScanning();
isRunning = false;
LOG_INFO("BLE mesh stopped");
}
bool NRF52BLEMesh::platformBeginAdvertising(const uint8_t *adv, size_t len)
{
if (len > sizeof(advBuf))
return false;
// Copy into our own storage: sd_ble_gap_adv_set_configure retains the pointer rather than
// copying, so the caller's buffer must not be the one the SoftDevice reads from.
memcpy(advBuf, adv, len);
advBufLen = (uint8_t)len;
ble_gap_adv_data_t gapAdvData = {};
gapAdvData.adv_data.p_data = advBuf;
gapAdvData.adv_data.len = advBufLen;
gapAdvData.scan_rsp_data.p_data = NULL;
gapAdvData.scan_rsp_data.len = 0;
ble_gap_adv_params_t advParams = {};
advParams.properties.type = BLE_GAP_ADV_TYPE_EXTENDED_NONCONNECTABLE_NONSCANNABLE_UNDIRECTED;
advParams.p_peer_addr = NULL;
advParams.filter_policy = BLE_GAP_ADV_FP_ANY;
advParams.interval = BLE_MESH_ADV_INTERVAL;
advParams.duration = 0; // bounded by max_adv_evts, not by time
advParams.primary_phy = BLE_GAP_PHY_1MBPS;
advParams.secondary_phy = BLE_GAP_PHY_1MBPS;
advParams.max_adv_evts = BLE_MESH_ADV_EVENTS;
if (!ownsDedicatedSet && advHandle == BLE_GAP_ADV_SET_HANDLE_NOT_SET) {
// First attempt: ask the SoftDevice for a set of our own.
uint8_t handle = BLE_GAP_ADV_SET_HANDLE_NOT_SET;
uint32_t err = sd_ble_gap_adv_set_configure(&handle, &gapAdvData, &advParams);
if (err == NRF_SUCCESS) {
advHandle = handle;
ownsDedicatedSet = true;
LOG_INFO("BLE mesh using dedicated adv set %u", advHandle);
} else {
// No spare set. Share handle 0 with the phone advertisement, which means suspending it
// for the length of each burst and restoring it afterwards.
LOG_WARN("BLE mesh: no spare adv set (0x%x), sharing the phone's", err);
advHandle = 0;
ownsDedicatedSet = false;
}
}
if (!ownsDedicatedSet) {
Bluefruit.Advertising.stop();
uint32_t err = sd_ble_gap_adv_set_configure(&advHandle, &gapAdvData, &advParams);
if (err != NRF_SUCCESS) {
LOG_WARN("BLE mesh adv configure failed: 0x%x", err);
if (nrf52Bluetooth)
nrf52Bluetooth->resumeAdvertising();
return false;
}
} else {
uint32_t err = sd_ble_gap_adv_set_configure(&advHandle, &gapAdvData, &advParams);
if (err != NRF_SUCCESS) {
LOG_WARN("BLE mesh adv reconfigure failed: 0x%x", err);
return false;
}
}
uint32_t err = sd_ble_gap_adv_start(advHandle, BLE_CONN_CFG_TAG_DEFAULT);
if (err != NRF_SUCCESS) {
LOG_WARN("BLE mesh adv start failed: 0x%x", err);
if (!ownsDedicatedSet && nrf52Bluetooth)
nrf52Bluetooth->resumeAdvertising();
return false;
}
advActive = true;
LOG_DEBUG("BLE mesh adv burst: handle %u len %u %s", advHandle, advBufLen, ownsDedicatedSet ? "dedicated" : "shared");
return true;
}
bool NRF52BLEMesh::platformAdvertisingActive()
{
// Cleared by BLE_GAP_EVT_ADV_SET_TERMINATED once max_adv_evts have gone out.
return advActive;
}
void NRF52BLEMesh::platformEndAdvertising()
{
if (advHandle != BLE_GAP_ADV_SET_HANDLE_NOT_SET) {
uint32_t err = sd_ble_gap_adv_stop(advHandle);
if (err != NRF_SUCCESS && err != NRF_ERROR_INVALID_STATE)
LOG_WARN("BLE mesh adv stop failed: 0x%x", err);
}
advActive = false;
// Hand the shared set back to the phone.
if (!ownsDedicatedSet && nrf52Bluetooth)
nrf52Bluetooth->resumeAdvertising();
}
void NRF52BLEMesh::startScanning()
{
if (!nrf52BluetoothReady)
return;
bleMeshScanReportData.len = sizeof(bleMeshScanBuffer);
bleMeshScanParams.interval = BLE_MESH_SCAN_INTERVAL;
bleMeshScanParams.window = BLE_MESH_SCAN_WINDOW;
uint32_t err = sd_ble_gap_scan_start(&bleMeshScanParams, &bleMeshScanReportData);
if (err == NRF_SUCCESS) {
LOG_DEBUG("BLE mesh scanning started");
} else if (err == NRF_ERROR_INVALID_STATE) {
LOG_DEBUG("BLE mesh scanning already active");
} else {
// NRF_ERROR_NOT_SUPPORTED / INVALID_STATE here usually means the central role is not
// enabled: Bluefruit.begin() defaults to zero central links, and scanning needs one.
LOG_WARN("BLE mesh scan start failed: 0x%x", err);
}
}
void NRF52BLEMesh::stopScanning()
{
uint32_t err = sd_ble_gap_scan_stop();
if (err != NRF_SUCCESS && err != NRF_ERROR_INVALID_STATE) {
LOG_WARN("BLE mesh scan stop failed: 0x%x", err);
}
}
void NRF52BLEMesh::onBleEvent(ble_evt_t *event)
{
if (!instance || !instance->isRunning || !event)
return;
switch (event->header.evt_id) {
case BLE_GAP_EVT_ADV_REPORT: {
ble_gap_evt_adv_report_t *report = &event->evt.gap_evt.params.adv_report;
if (report->type.status == BLE_GAP_ADV_DATA_STATUS_COMPLETE) {
instance->handleScanResult(report);
}
// The SoftDevice pauses scanning after each report; hand the buffer back to resume.
bleMeshScanReportData.len = sizeof(bleMeshScanBuffer);
uint32_t err = sd_ble_gap_scan_start(NULL, &bleMeshScanReportData);
if (err != NRF_SUCCESS && err != NRF_ERROR_INVALID_STATE) {
LOG_WARN("BLE mesh scan resume failed: 0x%x", err);
}
break;
}
case BLE_GAP_EVT_ADV_SET_TERMINATED: {
const ble_gap_evt_adv_set_terminated_t &t = event->evt.gap_evt.params.adv_set_terminated;
LOG_DEBUG("BLE mesh adv set %u terminated: reason %u after %u events (ours=%d)", t.adv_handle, t.reason,
t.num_completed_adv_events, t.adv_handle == instance->advHandle);
if (t.adv_handle == instance->advHandle)
instance->advActive = false;
break;
}
case BLE_GAP_EVT_TIMEOUT:
if (event->evt.gap_evt.params.timeout.src == BLE_GAP_TIMEOUT_SRC_SCAN) {
instance->startScanning();
}
break;
default:
break;
}
}
void NRF52BLEMesh::handleScanResult(ble_gap_evt_adv_report_t *report)
{
if (!isRunning || !report->data.p_data)
return;
const uint8_t *data = report->data.p_data;
uint16_t len = report->data.len;
uint16_t offset = 0;
while (offset + 1 < len) {
uint8_t adLen = data[offset];
if (adLen == 0 || offset + adLen >= len)
break;
uint8_t adType = data[offset + 1];
if (adType == BLE_GAP_AD_TYPE_MANUFACTURER_SPECIFIC_DATA && adLen >= 4) {
uint16_t companyId = data[offset + 2] | (data[offset + 3] << 8);
if (companyId == BLE_MESH_COMPANY_ID && data[offset + 4] == BLE_MESH_PROTOCOL_VERSION) {
const uint8_t *payload = &data[offset + 5];
size_t payloadLen = adLen - 4;
updatePeer(report->peer_addr, report->rssi);
deliverToRouter(payload, payloadLen, report->rssi);
return;
}
}
offset += adLen + 1;
}
}
void NRF52BLEMesh::updatePeer(const ble_gap_addr_t &addr, int8_t rssi)
{
uint32_t now = millis();
for (uint8_t i = 0; i < peerCount; i++) {
if (memcmp(&peers[i].addr, &addr, sizeof(ble_gap_addr_t)) == 0) {
peers[i].rssi = rssi;
peers[i].lastSeenMs = now;
return;
}
}
if (peerCount >= BLE_MESH_MAX_PEERS)
pruneStale();
if (peerCount < BLE_MESH_MAX_PEERS) {
peers[peerCount].addr = addr;
peers[peerCount].rssi = rssi;
peers[peerCount].lastSeenMs = now;
peers[peerCount].nodeNum = 0; // unknown until we decode a packet from them
peerCount++;
LOG_DEBUG("BLE mesh new peer (%u total)", peerCount);
}
}
void NRF52BLEMesh::pruneStale()
{
uint32_t now = millis();
uint8_t writeIdx = 0;
for (uint8_t i = 0; i < peerCount; i++) {
if (now - peers[i].lastSeenMs < BLE_MESH_PEER_TIMEOUT_MS) {
if (writeIdx != i)
peers[writeIdx] = peers[i];
writeIdx++;
}
}
peerCount = writeIdx;
}
#endif // HAS_BLE_MESH && ARCH_NRF52