Files
MeshCore/src/helpers/nrf52/SerialBLEInterface.cpp
T

1490 lines
60 KiB
C++

#include "SerialBLEInterface.h"
#include "../BluetoothMac.h"
#include "../CompanionFrameQueue.h"
#include "SoftDeviceState.h"
#include "SleepCycleState.h"
#include "BleDebugTrace.h"
#include <stdio.h>
#include <string.h>
#include "ble_gap.h"
#include "ble_hci.h"
#include <utility/bonding.h>
#if defined(MESH_NRF52_BLE_TRACE) && MESH_NRF52_BLE_TRACE
extern "C" {
__attribute__((used, externally_visible)) MeshBleTraceBuffer mesh_ble_trace;
}
#endif
// Magic numbers came from actual testing
#define BLE_HEALTH_CHECK_INTERVAL 10000 // Advertising watchdog check every 10 seconds
#define BLE_RETRY_THROTTLE_MS 250 // Throttle retries to 250ms when queue buildup detected
#define BLE_BOND_PERSIST_TIMEOUT_MS 15000 // Bound first-pair transition recovery
#define BLE_COMPANION_START_TIMEOUT_MS 15000 // Release a secured link with no app data
// Connection parameters (units: interval=1.25ms, timeout=10ms)
#define BLE_MIN_CONN_INTERVAL 12 // 15ms
#define BLE_MAX_CONN_INTERVAL 24 // 30ms
#define BLE_SLAVE_LATENCY 4
#define BLE_CONN_SUP_TIMEOUT 200 // 2000ms
// Advertising parameters
#define BLE_ADV_INTERVAL_MIN 32 // 20ms (units: 0.625ms)
#define BLE_ADV_INTERVAL_MAX 244 // 152.5ms (units: 0.625ms)
#define BLE_ADV_FAST_TIMEOUT 30 // seconds
static SerialBLEInterface* instance = nullptr;
#if defined(MESH_NRF52_BLE_TRACE) && MESH_NRF52_BLE_TRACE
void SerialBLEInterface::formatDebugState(char* reply, size_t capacity) {
snprintf(reply, capacity,
"> enabled=%u,ready=%u,handle=%u,secured=%u,adv=%u,conn=%u,data=%u,txq=%u,rxq=%u,recovery=%u,notify=%u,mtu=%u",
(unsigned)_isEnabled, (unsigned)_begin_ready, (unsigned)_conn_handle,
(unsigned)_isDeviceConnected, (unsigned)Bluefruit.Advertising.isRunning(),
(unsigned)Bluefruit.connected(),
(unsigned)_companionDataSeen.load(std::memory_order_acquire),
(unsigned)send_queue_len, (unsigned)recv_queue.size(),
(unsigned)_tx_disconnect_recovery.pending(),
(unsigned)(_conn_handle != BLE_CONN_HANDLE_INVALID && bleuart.notifyEnabled(_conn_handle)),
(unsigned)_debug_mtu.load());
}
void meshBleTraceState(char* reply, size_t capacity) {
if (instance) instance->formatDebugState(reply, capacity);
else snprintf(reply, capacity, "> Bluetooth not initialized");
}
#endif
static bool formatDeviceName(char* dest, size_t capacity,
const char* prefix, const char* name) {
if (dest == nullptr || capacity == 0 || prefix == nullptr || name == nullptr) {
return false;
}
char resolved_name[32];
const char* suffix = name;
if (strcmp(name, "@@MAC") == 0) {
ble_gap_addr_t addr;
if (sd_ble_gap_addr_get(&addr) == NRF_SUCCESS) {
snprintf(resolved_name, sizeof(resolved_name),
"%02X%02X%02X%02X%02X%02X",
addr.addr[5], addr.addr[4], addr.addr[3], addr.addr[2],
addr.addr[1], addr.addr[0]);
suffix = resolved_name;
}
}
const int length = snprintf(dest, capacity, "%s%s", prefix, suffix);
return length >= 0 && length < (int)capacity;
}
#if defined(COMPANION_RADIO_FULL) && COMPANION_RADIO_FULL
// Cache the small SoftDevice system-attribute image for warm reconnects.
// Bluefruit also persists it in the peer's bond record for cold boot; losing
// that record leaves clients that retain their subscription unable to receive
// replies after reboot. SoftDeviceSvcCompat.h protects its SDK queries under LTO.
static constexpr uint16_t CCCD_RAM_CACHE_MAX = 128;
static constexpr uint32_t CCCD_SYS_ATTR_FLAGS =
BLE_GATTS_SYS_ATTR_FLAG_SYS_SRVCS | BLE_GATTS_SYS_ATTR_FLAG_USR_SRVCS;
struct CccdRamCache {
uint8_t attrs[CCCD_RAM_CACHE_MAX] = {};
uint16_t length = 0;
ble_gap_addr_t peer = {};
std::atomic<bool> valid{false};
};
static CccdRamCache cccd_ram_cache;
static bool cccd_written_this_connection = false;
static bool cccdPeersMatch(const ble_gap_addr_t& a,
const ble_gap_addr_t& b) {
if (a.addr_type == b.addr_type
&& memcmp(a.addr, b.addr, sizeof(a.addr)) == 0) return true;
// iPhones may reconnect from a different resolvable private address. The
// stored bond maps both addresses to the same stable peer identity.
ble_gap_addr_t a_lookup = a;
ble_gap_addr_t b_lookup = b;
bond_keys_t a_keys;
bond_keys_t b_keys;
return bond_load_keys(BLE_GAP_ROLE_PERIPH, &a_lookup, &a_keys)
&& bond_load_keys(BLE_GAP_ROLE_PERIPH, &b_lookup, &b_keys)
&& a_keys.peer_id.id_addr_info.addr_type
== b_keys.peer_id.id_addr_info.addr_type
&& memcmp(a_keys.peer_id.id_addr_info.addr,
b_keys.peer_id.id_addr_info.addr,
sizeof(a_keys.peer_id.id_addr_info.addr)) == 0;
}
bool mesh_nrf52_restore_ram_cccd(uint16_t handle,
const ble_gap_addr_t* peer) {
if (!cccd_ram_cache.valid.load(std::memory_order_acquire)
|| cccd_written_this_connection || !peer
|| !cccdPeersMatch(cccd_ram_cache.peer, *peer)) {
BLE_DEBUG_PRINTLN("CCCD RAM not restored at initialization: valid=%u written=%u",
(unsigned)cccd_ram_cache.valid.load(std::memory_order_relaxed),
(unsigned)cccd_written_this_connection);
return false;
}
// The first sys_attr_set on a connection must contain the saved CCCD.
// Bluefruit's no-file fallback initializes everything to off otherwise.
const uint32_t status = sd_ble_gatts_sys_attr_set(
handle, cccd_ram_cache.attrs, cccd_ram_cache.length,
CCCD_SYS_ATTR_FLAGS);
meshBleTrace(0xf041, handle, status, cccd_ram_cache.length);
if (status != NRF_SUCCESS) {
BLE_DEBUG_PRINTLN("CCCD RAM initialization failed: %lu",
(unsigned long)status);
cccd_ram_cache.valid.store(false, std::memory_order_release);
return false;
}
BLE_DEBUG_PRINTLN("CCCD RAM initialized: len=%u",
(unsigned)cccd_ram_cache.length);
return true;
}
static void captureCccdInRam(uint16_t handle, const ble_gap_addr_t& peer) {
cccd_ram_cache.valid.store(false, std::memory_order_release);
uint16_t length = sizeof(cccd_ram_cache.attrs);
const uint32_t status = sd_ble_gatts_sys_attr_get(
handle, cccd_ram_cache.attrs, &length, CCCD_SYS_ATTR_FLAGS);
// Retain a local reload barrier in addition to the global SVC ABI guard.
// The original SDK wrapper does not declare writes through this pointer.
__asm__ __volatile__("" ::: "memory");
meshBleTrace(0xf042, handle, status, length);
if (status != NRF_SUCCESS || length == 0
|| length > CCCD_RAM_CACHE_MAX) {
BLE_DEBUG_PRINTLN("CCCD RAM capture: status=%lu len=%u",
(unsigned long)status, (unsigned)length);
return;
}
cccd_ram_cache.length = length;
cccd_ram_cache.peer = peer;
cccd_ram_cache.valid.store(true, std::memory_order_release);
BLE_DEBUG_PRINTLN("CCCD RAM captured: len=%u", (unsigned)length);
}
static void captureCccdDeferred(uint16_t handle, ble_gap_addr_t* peer) {
if (peer) captureCccdInRam(handle, *peer);
}
#endif
static bool isBondAuthenticationFailure(uint8_t reason) {
return reason == BLE_HCI_AUTHENTICATION_FAILURE ||
reason == BLE_HCI_STATUS_CODE_PIN_OR_KEY_MISSING ||
reason == BLE_HCI_CONN_TERMINATED_DUE_TO_MIC_FAILURE;
}
void SerialBLEInterface::onConnect(uint16_t connection_handle) {
meshBleTrace(0xf001, connection_handle);
#if defined(MESH_NRF52_BLE_TRACE) && MESH_NRF52_BLE_TRACE
mesh_ble_trace.connects.fetch_add(1);
#endif
BLE_DEBUG_PRINTLN("SerialBLEInterface: connected handle=0x%04X", connection_handle);
if (instance) {
instance->_pairingRequestPending.store(false, std::memory_order_release);
instance->_conn_handle = connection_handle;
instance->_isDeviceConnected = false;
instance->_security_timer.start(millis());
instance->_companion_start_timer.cancel();
instance->_companionDataSeen.store(false, std::memory_order_release);
instance->clearBuffers();
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
instance->setMotaStreamActive(false);
#endif
}
}
void SerialBLEInterface::onDisconnect(uint16_t connection_handle, uint8_t reason) {
meshBleTrace(0xf002, connection_handle, reason);
#if defined(MESH_NRF52_BLE_TRACE) && MESH_NRF52_BLE_TRACE
mesh_ble_trace.disconnects.fetch_add(1);
mesh_ble_trace.last_reason.store(reason);
mesh_ble_trace.last_disconnect_ms.store(millis());
#endif
BLE_DEBUG_PRINTLN("SerialBLEInterface: disconnected handle=0x%04X reason=%u", connection_handle, reason);
if (instance) {
if (instance->_conn_handle == connection_handle) {
instance->_pairingRequestPending.store(false, std::memory_order_release);
instance->_conn_handle = BLE_CONN_HANDLE_INVALID;
instance->_isDeviceConnected = false;
instance->_security_timer.cancel();
instance->_companion_start_timer.cancel();
instance->clearBuffers();
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
instance->setMotaStreamActive(false);
#endif
}
}
}
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
void SerialBLEInterface::onMotaResponse(
uint16_t conn_handle, BLECharacteristic* characteristic,
uint8_t* data, uint16_t length) {
if (!instance || characteristic != &instance->_mota_response
|| instance->_conn_handle != conn_handle || !instance->isConnected()
|| !instance->_mota_stream.isActive()) {
return;
}
if (length == 0 || length > mesh::ota::BLE_MOTA_RESPONSE_MAX
|| !instance->_mota_stream.pushRx(data, length)) {
// A response that cannot fit intact would make the byte stream ambiguous.
// Disable the link so the current transaction times out and the main loop
// detaches it instead of consuming a partial or injected frame.
instance->setMotaStreamActive(false);
} else {
instance->_companionDataSeen.store(true, std::memory_order_release);
instance->_companion_start_timer.cancel();
}
}
size_t SerialBLEInterface::sendMotaRequest(void* context,
const uint8_t* data,
size_t length) {
SerialBLEInterface* self = static_cast<SerialBLEInterface*>(context);
if (!self || !data || length == 0
|| length > mesh::ota::BLE_MOTA_REQUEST_MAX
|| !self->isMotaChannelReady() || !self->_mota_stream.isActive()) {
return 0;
}
return self->_mota_request.notify(self->_conn_handle, data, length)
? length : 0;
}
#endif
void SerialBLEInterface::onSecured(uint16_t connection_handle) {
meshBleTrace(0xf003, connection_handle);
BLE_DEBUG_PRINTLN("SerialBLEInterface: onSecured handle=0x%04X", connection_handle);
if (instance) {
if (instance->isValidConnection(connection_handle, true)) {
BLEConnection* conn = Bluefruit.Connection(connection_handle);
if (conn == nullptr || !conn->secured()) {
BLE_DEBUG_PRINTLN("SerialBLEInterface: security update did not secure the link");
instance->_isDeviceConnected = false;
instance->_security_timer.cancel();
if (conn != nullptr && conn->bonded()) {
instance->removeStoredBondForPeer("unsecured link");
}
if (instance->_bonded_only) {
instance->requestBondedOnlyRecovery("unsecured bonded-only link");
}
instance->disconnect();
return;
}
instance->_isDeviceConnected = true;
instance->recv_queue.secure(connection_handle);
#if defined(MESH_NRF52_BLE_TRACE) && MESH_NRF52_BLE_TRACE
mesh_ble_trace.secured.fetch_add(1);
meshBleTrace(0xf006, connection_handle, conn->bonded(),
instance->bleuart.notifyEnabled(connection_handle));
#endif
if (conn->bonded()) {
instance->noteSuccessfulConnection(conn->getPeerAddr());
} else {
BLE_DEBUG_PRINTLN(
"SerialBLEInterface: secured connection was not bonded");
}
instance->_security_timer.cancel();
if (!instance->_companionDataSeen.load(std::memory_order_acquire)) {
instance->_companion_start_timer.start(millis());
}
// Connection interval units: 1.25ms, supervision timeout units: 10ms
// Apple: "The product will not read or use the parameters in the Peripheral Preferred Connection Parameters characteristic."
// So we explicitly set it here to make Android & Apple match
ble_gap_conn_params_t conn_params;
conn_params.min_conn_interval = BLE_MIN_CONN_INTERVAL;
conn_params.max_conn_interval = BLE_MAX_CONN_INTERVAL;
conn_params.slave_latency = BLE_SLAVE_LATENCY;
conn_params.conn_sup_timeout = BLE_CONN_SUP_TIMEOUT;
uint32_t err_code = sd_ble_gap_conn_param_update(connection_handle, &conn_params);
meshBleTrace(0xf011, connection_handle, err_code,
BLE_SLAVE_LATENCY | (BLE_CONN_SUP_TIMEOUT << 16));
if (err_code == NRF_SUCCESS) {
BLE_DEBUG_PRINTLN("Connection parameter update requested: %u-%ums interval, latency=%u, %ums timeout",
conn_params.min_conn_interval * 5 / 4, // convert to ms (1.25ms units)
conn_params.max_conn_interval * 5 / 4,
conn_params.slave_latency,
conn_params.conn_sup_timeout * 10); // convert to ms (10ms units)
} else {
BLE_DEBUG_PRINTLN("Failed to request connection parameter update: %lu", err_code);
}
} else {
BLE_DEBUG_PRINTLN("onSecured: ignoring stale/duplicate callback");
}
}
}
bool SerialBLEInterface::onPairingPasskey(uint16_t connection_handle, uint8_t const passkey[6], bool match_request) {
meshBleTrace(0xf004, connection_handle, match_request);
(void)passkey;
BLE_DEBUG_PRINTLN("SerialBLEInterface: pairing passkey request match=%d", match_request);
if (instance && instance->isValidConnection(connection_handle)) {
instance->_pairingRequestPending.store(true, std::memory_order_release);
}
return true;
}
void SerialBLEInterface::onPairingComplete(uint16_t connection_handle, uint8_t auth_status) {
meshBleTrace(0xf005, connection_handle, auth_status);
BLE_DEBUG_PRINTLN("SerialBLEInterface: pairing complete handle=0x%04X status=%u", connection_handle, auth_status);
if (instance) {
if (instance->isValidConnection(connection_handle)) {
if (auth_status == BLE_GAP_SEC_STATUS_SUCCESS) {
BLE_DEBUG_PRINTLN("SerialBLEInterface: pairing successful");
BLEConnection* conn = Bluefruit.Connection(connection_handle);
if (conn != nullptr && conn->secured()) {
// Bluefruit may invoke onSecured before it marks a newly paired
// connection as bonded. Record the completed pairing here as well;
// resolveSuccessfulPeer() will keep the event pending until the
// deferred bond write can be loaded.
instance->noteSuccessfulConnection(conn->getPeerAddr());
}
} else {
BLE_DEBUG_PRINTLN("SerialBLEInterface: pairing failed, clearing stale bond and disconnecting");
if (instance->_bonded_only) {
instance->requestBondedOnlyRecovery("pairing failure");
}
BLEConnection* conn = Bluefruit.Connection(connection_handle);
if (conn != nullptr && conn->bonded()) {
instance->removeStoredBondForPeer("pairing failure");
}
instance->_isDeviceConnected = false;
instance->_security_timer.cancel();
instance->disconnect();
}
} else {
BLE_DEBUG_PRINTLN("onPairingComplete: ignoring stale callback");
}
}
}
void SerialBLEInterface::onBLEEvent(ble_evt_t* evt) {
#if defined(MESH_NRF52_BLE_TRACE) && MESH_NRF52_BLE_TRACE
uint32_t detail = 0, extra = 0;
const ble_gap_conn_params_t* params = nullptr;
if (evt->header.evt_id == BLE_GAP_EVT_CONNECTED) {
params = &evt->evt.gap_evt.params.connected.conn_params;
} else if (evt->header.evt_id == BLE_GAP_EVT_CONN_PARAM_UPDATE) {
params = &evt->evt.gap_evt.params.conn_param_update.conn_params;
} else if (evt->header.evt_id == BLE_GAP_EVT_DISCONNECTED) {
detail = evt->evt.gap_evt.params.disconnected.reason;
} else if (evt->header.evt_id == BLE_GAP_EVT_ADV_SET_TERMINATED) {
detail = evt->evt.gap_evt.params.adv_set_terminated.reason;
}
if (evt->header.evt_id == BLE_GAP_EVT_AUTH_STATUS) {
const auto& auth = evt->evt.gap_evt.params.auth_status;
detail = auth.auth_status;
extra = auth.error_src | (auth.bonded << 8) | (auth.lesc << 9);
} else if (evt->header.evt_id == BLE_GAP_EVT_CONN_SEC_UPDATE) {
const auto& sec = evt->evt.gap_evt.params.conn_sec_update.conn_sec;
detail = sec.sec_mode.sm | (sec.sec_mode.lv << 8);
extra = sec.encr_key_size;
} else if (evt->header.evt_id == BLE_GAP_EVT_TIMEOUT) {
detail = evt->evt.gap_evt.params.timeout.src;
} else if (evt->header.evt_id == BLE_GATTS_EVT_WRITE) {
const auto& write = evt->evt.gatts_evt.params.write;
detail = write.handle | (write.len << 16);
extra = write.op;
if (write.uuid.type == BLE_UUID_TYPE_BLE
&& write.uuid.uuid == BLE_UUID_DESCRIPTOR_CLIENT_CHAR_CONFIG) {
extra |= 0x100;
// Only a descriptor's public subscription bits; never characteristic data.
if (write.len == 2) extra |= (write.data[0] | (write.data[1] << 8)) << 16;
}
} else if (evt->header.evt_id == BLE_GATTS_EVT_EXCHANGE_MTU_REQUEST) {
detail = evt->evt.gatts_evt.params.exchange_mtu_request.client_rx_mtu;
} else if (evt->header.evt_id == BLE_GATTS_EVT_HVN_TX_COMPLETE) {
detail = evt->evt.gatts_evt.params.hvn_tx_complete.count;
} else if (evt->header.evt_id == BLE_GATTS_EVT_TIMEOUT) {
detail = evt->evt.gatts_evt.params.timeout.src;
} else if (evt->header.evt_id == BLE_GAP_EVT_PHY_UPDATE) {
const auto& phy = evt->evt.gap_evt.params.phy_update;
detail = phy.status;
extra = phy.tx_phy | (phy.rx_phy << 8);
} else if (evt->header.evt_id == BLE_GAP_EVT_DATA_LENGTH_UPDATE) {
const auto& data = evt->evt.gap_evt.params.data_length_update.effective_params;
detail = data.max_tx_octets | (data.max_rx_octets << 16);
extra = data.max_tx_time_us | (data.max_rx_time_us << 16);
}
if (params) {
detail = params->min_conn_interval | (params->max_conn_interval << 16);
extra = params->slave_latency | (params->conn_sup_timeout << 16);
}
meshBleTrace(evt->header.evt_id, evt->evt.common_evt.conn_handle, detail, extra);
#else
meshBleTrace(evt->header.evt_id, evt->evt.common_evt.conn_handle);
#endif
if (!instance) return;
#if defined(MESH_NRF52_BLE_TRACE) && MESH_NRF52_BLE_TRACE
if (evt->header.evt_id == BLE_GAP_EVT_CONNECTED
|| evt->header.evt_id == BLE_GATTS_EVT_EXCHANGE_MTU_REQUEST) {
BLEConnection* conn = Bluefruit.Connection(evt->evt.common_evt.conn_handle);
instance->_debug_mtu.store(conn ? conn->getMtu() : 0);
} else if (evt->header.evt_id == BLE_GAP_EVT_DISCONNECTED) {
instance->_debug_mtu.store(0);
}
#endif
if (evt->header.evt_id == BLE_GAP_EVT_CONNECTED) {
ble_gap_evt_connected_t const* connected = &evt->evt.gap_evt.params.connected;
if (connected->role == BLE_GAP_ROLE_PERIPH) {
// Observe the physical session in the BLE worker before its deferred
// onConnect callback. A disable/re-enable between the event and that
// callback must not allow the old link to reopen the RX queue.
instance->recv_queue.connect(evt->evt.gap_evt.conn_handle);
instance->_peer_address = connected->peer_addr;
instance->_peer_address_valid = true;
instance->_bond_removed_for_connection = false;
#if defined(COMPANION_RADIO_FULL) && COMPANION_RADIO_FULL
cccd_written_this_connection = false;
BLE_DEBUG_PRINTLN("CCCD RAM connect: cached=%u",
(unsigned)cccd_ram_cache.valid.load(std::memory_order_relaxed));
#endif
}
} else if (evt->header.evt_id == BLE_GAP_EVT_CONN_SEC_UPDATE) {
uint16_t conn_handle = evt->evt.gap_evt.conn_handle;
BLEConnection* conn = Bluefruit.Connection(conn_handle);
if (conn != nullptr && conn->connected() && !conn->secured()) {
BLE_DEBUG_PRINTLN("CONN_SEC_UPDATE: link is not secured, bonded=%d", conn->bonded());
if (conn->bonded()) {
instance->removeStoredBondForPeer("failed bond encryption");
}
if (instance->_bonded_only) {
instance->requestBondedOnlyRecovery("failed bond encryption");
}
instance->_isDeviceConnected = false;
instance->_security_timer.cancel();
sd_ble_gap_disconnect(conn_handle, BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);
}
#if defined(COMPANION_RADIO_FULL) && COMPANION_RADIO_FULL
} else if (evt->header.evt_id == BLE_GATTS_EVT_WRITE) {
const ble_gatts_evt_write_t& write = evt->evt.gatts_evt.params.write;
if (write.uuid.type == BLE_UUID_TYPE_BLE
&& write.uuid.uuid == BLE_UUID_DESCRIPTOR_CLIENT_CHAR_CONFIG
&& instance->_peer_address_valid) {
cccd_written_this_connection = true;
BLE_DEBUG_PRINTLN("CCCD RAM write: len=%u value=%u",
(unsigned)write.len,
(unsigned)(write.len ? write.data[0] : 0));
// Mirror Bluefruit's deferred CCCD save timing for the warm RAM cache.
// Reading during the BLE event is too early: sys_attr_get succeeds but
// returns the pre-write CCCD even though notifyEnabled() is already on.
cccd_ram_cache.valid.store(false, std::memory_order_release);
if (!ada_callback(&instance->_peer_address,
sizeof(instance->_peer_address), captureCccdDeferred,
evt->evt.gatts_evt.conn_handle,
&instance->_peer_address)) {
BLE_DEBUG_PRINTLN("CCCD RAM capture could not be queued");
}
}
#endif
} else if (evt->header.evt_id == BLE_GATTS_EVT_SYS_ATTR_MISSING) {
BLE_DEBUG_PRINTLN("CCCD RAM system attributes missing");
} else if (evt->header.evt_id == BLE_GAP_EVT_DISCONNECTED) {
instance->recv_queue.disconnect(evt->evt.gap_evt.conn_handle);
ble_gap_evt_disconnected_t const* disconnected = &evt->evt.gap_evt.params.disconnected;
if (isBondAuthenticationFailure(disconnected->reason)) {
instance->removeStoredBondForPeer("authentication disconnect");
if (instance->_bonded_only) {
instance->requestBondedOnlyRecovery(
"bond authentication disconnect");
}
}
#if defined(COMPANION_RADIO_FULL) && COMPANION_RADIO_FULL
cccd_written_this_connection = false;
#endif
instance->_peer_address_valid = false;
} else if (evt->header.evt_id == BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST) {
uint16_t conn_handle = evt->evt.gap_evt.conn_handle;
if (instance->isValidConnection(conn_handle)) {
BLE_DEBUG_PRINTLN("CONN_PARAM_UPDATE_REQUEST: handle=0x%04X, min_interval=%u, max_interval=%u, latency=%u, timeout=%u",
conn_handle,
evt->evt.gap_evt.params.conn_param_update_request.conn_params.min_conn_interval,
evt->evt.gap_evt.params.conn_param_update_request.conn_params.max_conn_interval,
evt->evt.gap_evt.params.conn_param_update_request.conn_params.slave_latency,
evt->evt.gap_evt.params.conn_param_update_request.conn_params.conn_sup_timeout);
uint32_t err_code = sd_ble_gap_conn_param_update(conn_handle, NULL);
if (err_code == NRF_SUCCESS) {
BLE_DEBUG_PRINTLN("Accepted CONN_PARAM_UPDATE_REQUEST (using PPCP)");
} else {
BLE_DEBUG_PRINTLN("ERROR: Failed to accept CONN_PARAM_UPDATE_REQUEST: 0x%08X", err_code);
}
} else {
BLE_DEBUG_PRINTLN("CONN_PARAM_UPDATE_REQUEST: ignoring stale callback for handle=0x%04X", conn_handle);
}
}
}
bool SerialBLEInterface::removeStoredBondForPeer(const char* cause) {
if (!_peer_address_valid || _bond_removed_for_connection) {
return false;
}
ble_gap_addr_t peer_address = _peer_address;
bond_keys_t bond_keys;
if (!bond_load_keys(BLE_GAP_ROLE_PERIPH, &peer_address, &bond_keys)) {
BLE_DEBUG_PRINTLN("SerialBLEInterface: no stored peer bond to clear (%s)", cause);
return false;
}
bond_remove_key(BLE_GAP_ROLE_PERIPH, &bond_keys.peer_id.id_addr_info);
_bond_removed_for_connection = true;
BLE_DEBUG_PRINTLN("SerialBLEInterface: cleared stale peer bond (%s)", cause);
return true;
}
void SerialBLEInterface::noteSuccessfulConnection(
const ble_gap_addr_t& peer_address) {
if (_stealth_pair_once) {
_advertisingSuppressed.store(true, std::memory_order_release);
Bluefruit.Advertising.restartOnDisconnect(false);
}
if (!_successfulConnectionPending.load(std::memory_order_acquire)) {
_successful_peer_address = peer_address;
_successfulConnectionStarted.store(
(uint32_t)millis(), std::memory_order_relaxed);
_successfulConnectionPending.store(true, std::memory_order_release);
}
}
bool SerialBLEInterface::resolveSuccessfulPeer(
mesh::companion::BluetoothPeerIdentity& peer) const {
ble_gap_addr_t lookup = _successful_peer_address;
bond_keys_t keys;
if (!bond_load_keys(BLE_GAP_ROLE_PERIPH, &lookup, &keys)) {
return false;
}
const ble_gap_addr_t& identity = keys.peer_id.id_addr_info;
if (identity.addr_type == BLE_GAP_ADDR_TYPE_PUBLIC) {
peer.type = mesh::companion::BLUETOOTH_PEER_ADDRESS_PUBLIC;
} else if (identity.addr_type == BLE_GAP_ADDR_TYPE_RANDOM_STATIC) {
peer.type = mesh::companion::BLUETOOTH_PEER_ADDRESS_RANDOM;
} else {
return false;
}
for (size_t i = 0; i < mesh::companion::BLUETOOTH_MAC_BYTES; i++) {
peer.address[i] = identity.addr[
mesh::companion::BLUETOOTH_MAC_BYTES - 1 - i];
}
return mesh::companion::isValidBluetoothPeerIdentity(peer);
}
bool SerialBLEInterface::configureBondedOnlyAdvertising(
const mesh::companion::BluetoothPeerIdentity& peer,
bool require_stored_bond) {
if (!mesh::companion::isValidBluetoothPeerIdentity(peer)) return false;
ble_gap_addr_t native_peer = {};
native_peer.addr_type =
peer.type == mesh::companion::BLUETOOTH_PEER_ADDRESS_PUBLIC
? BLE_GAP_ADDR_TYPE_PUBLIC
: BLE_GAP_ADDR_TYPE_RANDOM_STATIC;
for (size_t i = 0; i < mesh::companion::BLUETOOTH_MAC_BYTES; i++) {
native_peer.addr[i] = peer.address[
mesh::companion::BLUETOOTH_MAC_BYTES - 1 - i];
}
ble_gap_addr_t lookup = native_peer;
bond_keys_t keys;
if (!bond_load_keys(BLE_GAP_ROLE_PERIPH, &lookup, &keys)
|| keys.peer_id.id_addr_info.addr_type != native_peer.addr_type
|| memcmp(keys.peer_id.id_addr_info.addr, native_peer.addr,
sizeof(native_peer.addr)) != 0) {
if (require_stored_bond) {
requestBondedOnlyRecovery("saved peer bond is unavailable");
}
return false;
}
if (isAdvertising() && !Bluefruit.Advertising.stop()) {
requestBondedOnlyRecovery("general advertising could not stop");
return false;
}
// A bonded phone may reconnect from a resolvable private address. Loading
// its identity key lets the SoftDevice put a current RPA in TargetA instead
// of directing advertisements only to the phone's static identity address.
const ble_gap_id_key_t* peer_identities[] = {&keys.peer_id};
const uint32_t identity_error = sd_ble_gap_device_identities_set(
peer_identities, nullptr, 1);
if (identity_error != NRF_SUCCESS) {
BLE_DEBUG_PRINTLN(
"SerialBLEInterface: peer identity-list setup failed: %lu",
(unsigned long)identity_error);
requestBondedOnlyRecovery("peer identity-list setup failed");
return false;
}
// Legacy directed advertisements do not carry an advertising or scan
// response payload. Leaving the normal name/service payload configured can
// make the SoftDevice reject the directed advertising parameters.
Bluefruit.Advertising.clearData();
Bluefruit.ScanResponse.clearData();
Bluefruit.Advertising.setType(
BLE_GAP_ADV_TYPE_CONNECTABLE_NONSCANNABLE_DIRECTED);
Bluefruit.Advertising.setPeerAddress(native_peer);
Bluefruit.Advertising.restartOnDisconnect(true);
_bonded_only = true;
_stealth_pair_once = false;
_advertisingSuppressed.store(false, std::memory_order_release);
if (_isEnabled && _conn_handle == BLE_CONN_HANDLE_INVALID
&& !startAdvertising("directed advertising failed to start")) {
return false;
}
return true;
}
void SerialBLEInterface::requestBondedOnlyRecovery(const char* cause) {
BLE_DEBUG_PRINTLN("SerialBLEInterface: stealth recovery required (%s)",
cause);
_bonded_only = true;
_stealth_pair_once = false;
_advertisingSuppressed.store(true, std::memory_order_release);
_bondedOnlyRecoveryPending.store(true, std::memory_order_release);
Bluefruit.Advertising.restartOnDisconnect(false);
if (isAdvertising()) Bluefruit.Advertising.stop();
}
bool SerialBLEInterface::advertisingAllowed() const {
return _isEnabled
&& !_advertisingSuppressed.load(std::memory_order_acquire)
&& !_bondedOnlyRecoveryPending.load(std::memory_order_acquire);
}
bool SerialBLEInterface::startAdvertising(const char* failure_cause) {
if (!advertisingAllowed()) return false;
const bool started = Bluefruit.Advertising.start(0);
meshBleTrace(0xf012, 0xffff, started, _bonded_only);
if (started) return true;
if (_bonded_only) requestBondedOnlyRecovery(failure_cause);
return false;
}
bool SerialBLEInterface::begin(const char* prefix, const char* name,
uint32_t pin_code,
const uint8_t* custom_address,
bool clear_bonds, bool stealth_pair_once,
const mesh::companion::BluetoothPeerIdentity*
bonded_only_peer) {
// Bluefruit cannot safely reinitialize a partly started SoftDevice: doing
// so leaks worker tasks, FIFOs and GATT registrations on every retry. Keep
// USB/UI usable after a failed start; recovery requires a corrected build
// or configuration and a reboot.
if (_begin_attempted) return _begin_ready;
_begin_attempted = true;
_begin_failure[0] = 0;
instance = this;
meshBleTrace(0xf000, 0xffff, NRF_POWER->RESETREAS, BLE_TX_POWER);
_successfulConnectionPending.store(false, std::memory_order_release);
_successfulConnectionStarted.store(0, std::memory_order_relaxed);
_companionDataSeen.store(false, std::memory_order_release);
_companion_start_timer.cancel();
_bondedOnlyRecoveryPending.store(false, std::memory_order_release);
_advertisingSuppressed.store(false, std::memory_order_release);
_stealth_pair_once = stealth_pair_once;
_bonded_only = false;
_bonded_only_configure_pending = false;
_pending_bonded_peer = mesh::companion::BluetoothPeerIdentity();
char charpin[20];
snprintf(charpin, sizeof(charpin), "%lu", (unsigned long)pin_code);
#ifdef DISABLE_BLE_LED
// Opt out of Bluefruit's automatic LED control without changing the normal
// board/library default or the application's own LED handling.
Bluefruit.autoConnLed(false);
#endif
// Configure individual connection values rather than the all-or-nothing
// Bluefruit preset. Defaults are identical to BANDWIDTH_MAX; constrained
// board profiles can lower event/buffer reservations without shrinking the
// Companion's negotiated 247-byte MTU.
Bluefruit.configPrphConn(COMPANION_BLE_PRPH_MTU,
COMPANION_BLE_PRPH_EVENT_LENGTH,
COMPANION_BLE_PRPH_HVN_QUEUE,
COMPANION_BLE_PRPH_WRCMD_QUEUE);
// The pinned nRF52 core is built with LTO. Its internal xTaskCreate() calls
// are resolved before the application's linker wrappers, so a wrapper-based
// task observation incorrectly reports that the BLE/SOC workers never
// started. Bluefruit.begin() is the reliable startup result available to
// this application.
// A partial/failed begin must not leave the current loop's cached
// SoftDevice state eligible for bare-metal sleep.
mesh_nrf52::sleepCycleState().invalidate();
if (!Bluefruit.begin()) {
uint8_t softdevice_enabled = 0;
const uint32_t softdevice_status =
mesh_nrf52::softdeviceIsEnabled(softdevice_enabled);
ble_gap_addr_t address = {};
const uint32_t gap_status = softdevice_enabled
? sd_ble_gap_addr_get(&address) : NRF_ERROR_INVALID_STATE;
snprintf(_begin_failure, sizeof(_begin_failure),
"Bluefruit begin failed (SD %lu/%u, GAP %lu)",
(unsigned long)softdevice_status,
(unsigned)softdevice_enabled,
(unsigned long)gap_status);
instance = nullptr;
mesh::usbLoggingPort().println(
_begin_failure);
return false;
}
if (clear_bonds) Bluefruit.Periph.clearBonds();
if (custom_address != nullptr) {
if (!mesh::companion::isValidBluetoothMac(custom_address)) {
strncpy(_begin_failure, "custom Bluetooth MAC is invalid",
sizeof(_begin_failure) - 1);
_begin_failure[sizeof(_begin_failure) - 1] = 0;
instance = nullptr;
BLE_DEBUG_PRINTLN("Custom Bluetooth MAC is invalid");
return false;
}
ble_gap_addr_t address = {};
address.addr_type = BLE_GAP_ADDR_TYPE_RANDOM_STATIC;
for (size_t i = 0; i < mesh::companion::BLUETOOTH_MAC_BYTES; i++) {
address.addr[i] = custom_address[
mesh::companion::BLUETOOTH_MAC_BYTES - 1 - i];
}
const uint32_t address_error = sd_ble_gap_addr_set(&address);
if (address_error != NRF_SUCCESS) {
snprintf(_begin_failure, sizeof(_begin_failure),
"custom Bluetooth MAC rejected (%lu)",
(unsigned long)address_error);
instance = nullptr;
BLE_DEBUG_PRINTLN("Custom Bluetooth MAC failed: %lu", address_error);
return false;
}
}
char dev_name[sizeof(_active_name)];
if (!formatDeviceName(dev_name, sizeof(dev_name), prefix, name)) {
strncpy(_begin_failure, "Bluetooth device name is too long",
sizeof(_begin_failure) - 1);
_begin_failure[sizeof(_begin_failure) - 1] = 0;
instance = nullptr;
BLE_DEBUG_PRINTLN("Bluetooth device name is too long");
return false;
}
// Connection interval units: 1.25ms, supervision timeout units: 10ms
ble_gap_conn_params_t ppcp_params;
ppcp_params.min_conn_interval = BLE_MIN_CONN_INTERVAL;
ppcp_params.max_conn_interval = BLE_MAX_CONN_INTERVAL;
ppcp_params.slave_latency = BLE_SLAVE_LATENCY;
ppcp_params.conn_sup_timeout = BLE_CONN_SUP_TIMEOUT;
uint32_t err_code = sd_ble_gap_ppcp_set(&ppcp_params);
meshBleTrace(0xf010, 0xffff, err_code);
if (err_code == NRF_SUCCESS) {
BLE_DEBUG_PRINTLN("PPCP set: %u-%ums interval, latency=%u, %ums timeout",
ppcp_params.min_conn_interval * 5 / 4, // convert to ms (1.25ms units)
ppcp_params.max_conn_interval * 5 / 4,
ppcp_params.slave_latency,
ppcp_params.conn_sup_timeout * 10); // convert to ms (10ms units)
} else {
BLE_DEBUG_PRINTLN("Failed to set PPCP: %lu", err_code);
}
Bluefruit.setTxPower(BLE_TX_POWER);
Bluefruit.setName(dev_name);
memcpy(_active_name, dev_name, strlen(dev_name) + 1);
Bluefruit.Security.setMITM(true);
Bluefruit.Security.setPIN(charpin);
Bluefruit.Security.setIOCaps(true, false, false);
Bluefruit.Security.setPairPasskeyCallback(onPairingPasskey);
Bluefruit.Security.setPairCompleteCallback(onPairingComplete);
Bluefruit.Periph.setConnectCallback(onConnect);
Bluefruit.Periph.setDisconnectCallback(onDisconnect);
Bluefruit.Security.setSecuredCallback(onSecured);
Bluefruit.setEventCallback(onBLEEvent);
bleuart.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM);
if (bleuart.begin() != ERROR_NONE) {
strncpy(_begin_failure, "Bluetooth UART service registration failed",
sizeof(_begin_failure) - 1);
_begin_failure[sizeof(_begin_failure) - 1] = 0;
instance = nullptr;
BLE_DEBUG_PRINTLN("Bluetooth UART service begin failed");
return false;
}
bleuart.setRxCallback(onBleUartRX);
#if defined(MESH_NRF52_BLE_TRACE) && MESH_NRF52_BLE_TRACE
bleuart.setNotifyCallback(onBleUartNotify);
bleuart.setRxOverflowCallback(onBleUartOverflow);
#endif
// Register the legacy DFU service before the optional mOTA service. A
// fielded SoftDevice has a fixed GATT table: reserve the ordinary Companion
// and firmware-recovery transports first, then use whatever remains for
// the larger mOTA extension. Do not attempt a new service after an mOTA
// registration failure because Bluefruit cannot remove attributes that did
// fit from its active table.
#if COMPANION_FEATURE_BLE_DFU
bledfu.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM);
if (bledfu.begin() != ERROR_NONE) {
mesh::usbLoggingPort().println(
"Bluetooth DFU service unavailable; normal Companion Bluetooth continues");
}
#endif
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
_mota_stream.setSender(sendMotaRequest, this);
_mota_stream.setActive(false);
_mota_available = false;
_mota_service.setPermission(SECMODE_ENC_WITH_MITM,
SECMODE_ENC_WITH_MITM);
if (_mota_service.begin() == ERROR_NONE) {
_mota_request.setProperties(CHR_PROPS_NOTIFY);
_mota_request.setPermission(SECMODE_ENC_WITH_MITM,
SECMODE_NO_ACCESS);
_mota_request.setMaxLen(mesh::ota::BLE_MOTA_REQUEST_MAX);
_mota_request.setUserDescriptor("mOTA device request");
if (_mota_request.begin() == ERROR_NONE) {
_mota_response.setProperties(CHR_PROPS_WRITE);
_mota_response.setPermission(SECMODE_NO_ACCESS,
SECMODE_ENC_WITH_MITM);
_mota_response.setMaxLen(mesh::ota::BLE_MOTA_RESPONSE_MAX);
_mota_response.setUserDescriptor("mOTA host response");
_mota_response.setWriteCallback(onMotaResponse);
_mota_available = _mota_response.begin() == ERROR_NONE;
}
}
if (!_mota_available) {
// GATT attributes cannot be removed after Bluefruit starts, so do not
// retry here. Continuing with the already-registered UART service keeps
// the phone and USB recovery paths available; a corrected image can add
// the optional mOTA service on the next reboot.
mesh::usbLoggingPort().println(
"Bluetooth mOTA unavailable; normal Companion Bluetooth continues");
}
#endif
Bluefruit.Advertising.setType(
BLE_GAP_ADV_TYPE_CONNECTABLE_SCANNABLE_UNDIRECTED);
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
Bluefruit.Advertising.addTxPower();
Bluefruit.Advertising.addService(bleuart);
Bluefruit.ScanResponse.addName();
Bluefruit.Advertising.setInterval(BLE_ADV_INTERVAL_MIN, BLE_ADV_INTERVAL_MAX);
Bluefruit.Advertising.setFastTimeout(BLE_ADV_FAST_TIMEOUT);
Bluefruit.Advertising.restartOnDisconnect(true);
if (bonded_only_peer != nullptr) {
configureBondedOnlyAdvertising(*bonded_only_peer, true);
}
_begin_ready = true;
return true;
}
bool SerialBLEInterface::refreshName(const char* prefix, const char* name) {
if (!_begin_ready || _conn_handle != BLE_CONN_HANDLE_INVALID) return false;
char desired[sizeof(_active_name)];
if (!formatDeviceName(desired, sizeof(desired), prefix, name)) return false;
if (strcmp(desired, _active_name) == 0) return true;
const bool was_advertising = isAdvertising();
if (was_advertising && !Bluefruit.Advertising.stop()) return false;
Bluefruit.setName(desired);
char actual[sizeof(_active_name)];
const size_t actual_len = Bluefruit.getName(actual, sizeof(actual));
if (actual_len != strlen(desired)
|| memcmp(actual, desired, actual_len) != 0) {
if (was_advertising) startAdvertising("advertising failed after name refresh");
return false;
}
// Directed bonded-only advertisements have no payload to refresh.
if (!_bonded_only) {
Bluefruit.ScanResponse.clearData();
Bluefruit.ScanResponse.addName();
}
memcpy(_active_name, desired, strlen(desired) + 1);
if (was_advertising) startAdvertising("advertising failed after name refresh");
return true;
}
bool SerialBLEInterface::takeSuccessfulConnection(
mesh::companion::BluetoothPeerIdentity* peer) {
if (!_successfulConnectionPending.load(std::memory_order_acquire)) {
return false;
}
if (peer != nullptr && !resolveSuccessfulPeer(*peer)) {
// Bond persistence is deferred by Bluefruit. Leave the event pending and
// let the main loop retry after that callback has written the keys. If the
// deferred write failed, reopen pairing instead of remaining hidden until
// somebody manually power-cycles the node.
const uint32_t started = _successfulConnectionStarted.load(
std::memory_order_relaxed);
if (_stealth_pair_once
&& (uint32_t)((uint32_t)millis() - started)
>= BLE_BOND_PERSIST_TIMEOUT_MS) {
_successfulConnectionPending.store(false, std::memory_order_release);
requestBondedOnlyRecovery("new peer bond did not persist");
}
return false;
}
const bool pending = _successfulConnectionPending.exchange(
false, std::memory_order_acq_rel);
if (pending) {
_successfulConnectionStarted.store(0, std::memory_order_relaxed);
}
return pending;
}
bool SerialBLEInterface::enableBondedOnlyAdvertising(
const mesh::companion::BluetoothPeerIdentity& peer) {
if (!mesh::companion::isValidBluetoothPeerIdentity(peer)) return false;
// The SoftDevice may reject identity-list changes while a connection is
// using that list. Preserve the newly paired session and finish switching
// to directed advertising from the main loop after it disconnects.
if (_conn_handle != BLE_CONN_HANDLE_INVALID) {
_pending_bonded_peer = peer;
_bonded_only_configure_pending = true;
_bonded_only = true;
_stealth_pair_once = false;
_advertisingSuppressed.store(true, std::memory_order_release);
Bluefruit.Advertising.restartOnDisconnect(false);
return true;
}
return configureBondedOnlyAdvertising(peer, false);
}
void SerialBLEInterface::serviceBondedOnlyTransition() {
if (!_bonded_only_configure_pending
|| _conn_handle != BLE_CONN_HANDLE_INVALID) {
return;
}
const mesh::companion::BluetoothPeerIdentity peer = _pending_bonded_peer;
_bonded_only_configure_pending = false;
configureBondedOnlyAdvertising(peer, true);
}
void SerialBLEInterface::cancelStealthPairingTransition() {
if (!_stealth_pair_once) return;
_stealth_pair_once = false;
_advertisingSuppressed.store(false, std::memory_order_release);
Bluefruit.Advertising.setType(
BLE_GAP_ADV_TYPE_CONNECTABLE_SCANNABLE_UNDIRECTED);
Bluefruit.Advertising.restartOnDisconnect(true);
if (_isEnabled && _conn_handle == BLE_CONN_HANDLE_INVALID
&& !isAdvertising()) {
startAdvertising("advertising failed after stealth cancellation");
}
}
void SerialBLEInterface::clearBuffers() {
_tx_reset_pending.store(true, std::memory_order_release);
recv_queue.reset([this]() { bleuart.flush(); });
}
void SerialBLEInterface::resetTxState() {
send_queue_len = 0;
_last_retry_attempt = 0;
_tx_stall_watchdog.reset();
_tx_disconnect_recovery.complete();
_tx_recovery_handle = BLE_CONN_HANDLE_INVALID;
_tx_recovery_generation = 0;
}
bool SerialBLEInterface::servicePendingTxReset() {
if (!_tx_reset_pending.exchange(false, std::memory_order_acq_rel)) return false;
resetTxState();
return true;
}
void SerialBLEInterface::shiftSendQueueLeft() {
if (send_queue_len > 0) {
send_queue_len--;
for (uint8_t i = 0; i < send_queue_len; i++) {
send_queue[i] = send_queue[i + 1];
}
}
}
size_t SerialBLEInterface::writeBleUartFrame(const Frame& frame) {
const uint16_t handle = _conn_handle;
uint32_t generation;
if (!recv_queue.snapshot(handle, generation)
|| generation != _send_queue_generation) return 0;
BLEConnection* conn = Bluefruit.Connection(handle);
if (conn == nullptr || !conn->connected() ||
!bleuart.notifyEnabled(handle)) {
return 0;
}
const uint16_t mtu = conn->getMtu();
if (mtu <= 3) return 0;
#if defined(MESH_NRF52_BLE_TRACE) && MESH_NRF52_BLE_TRACE
// Record only public framing metadata, never characteristic payload data.
meshBleTrace(0xf036, handle, frame.buf[0] | (frame.len << 8), mtu);
#endif
// BLEUart::write() reports either the full requested length or zero, even
// when its internal multi-notification loop queued an earlier fragment
// before a later fragment failed. Submit one ATT payload at a time so a
// non-zero return accurately tells us that part of the protocol frame is
// already on the stream and must never be followed by a whole-frame retry.
return mesh::writeBleFrameInChunks(
frame.buf, frame.len, mtu - 3,
[this, handle, generation](const uint8_t* data, size_t len) -> size_t {
// Never continue an old frame on a replacement connection, including
// when SoftDevice reuses the same numeric handle after a reconnect.
// FIFO/SDK calls remain outside the RX metadata critical section.
if (!recv_queue.current(handle, generation)) return 0;
const size_t written = bleuart.write(handle, data, len);
return recv_queue.current(handle, generation) ? written : 0;
});
}
void SerialBLEInterface::serviceTxRecovery(uint32_t now) {
if (!_tx_disconnect_recovery.pending()) return;
if (servicePendingTxReset()) return;
const uint16_t handle = _tx_recovery_handle;
const uint32_t generation = _tx_recovery_generation;
if (handle == BLE_CONN_HANDLE_INVALID || _conn_handle != handle
|| recv_queue.generation() != generation) {
resetTxState();
return;
}
BLEConnection* conn = Bluefruit.Connection(handle);
if (conn == nullptr || !conn->connected()) {
BLE_DEBUG_PRINTLN("SerialBLEInterface: stalled TX link is already closed");
{
RxQueueGuard guard;
if (_conn_handle != handle || recv_queue.generation() != generation) return;
_conn_handle = BLE_CONN_HANDLE_INVALID;
_isDeviceConnected = false;
_peer_address_valid = false;
_security_timer.cancel();
_companion_start_timer.cancel();
resetTxState();
}
if (_conn_handle == BLE_CONN_HANDLE_INVALID && advertisingAllowed() && !isAdvertising()) {
startAdvertising("advertising failed after TX recovery");
}
return;
}
if (!_tx_disconnect_recovery.shouldAttempt(now)) return;
if (servicePendingTxReset() || _conn_handle != handle
|| recv_queue.generation() != generation) return;
const uint32_t result = sd_ble_gap_disconnect(
handle, BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);
meshBleTrace(0xf013, handle, result, 1);
if (result == NRF_SUCCESS) {
BLE_DEBUG_PRINTLN("SerialBLEInterface: stalled TX disconnect requested");
} else if (result == NRF_ERROR_INVALID_STATE) {
BLE_DEBUG_PRINTLN("SerialBLEInterface: stalled TX disconnect already in progress");
} else {
BLE_DEBUG_PRINTLN(
"SerialBLEInterface: stalled TX disconnect failed, err=0x%08lX; will retry",
(unsigned long)result);
}
}
void SerialBLEInterface::recoverStalledTx(const char* cause,
uint16_t expected_handle,
uint32_t expected_generation) {
if (expected_handle == BLE_CONN_HANDLE_INVALID) {
expected_handle = _conn_handle;
if (!recv_queue.snapshot(expected_handle, expected_generation)) return;
}
meshBleTrace(0xf020, expected_handle, send_queue_len, recv_queue.size());
if (_tx_disconnect_recovery.pending()) return;
BLE_DEBUG_PRINTLN("SerialBLEInterface: %s; forcing reconnect", cause);
// Keep the physical connection state intact until the SoftDevice confirms
// disconnection, but make isConnected() false through the recovery state so
// no more companion frames enter this damaged stream.
{
RxQueueGuard guard;
if (_tx_reset_pending.load(std::memory_order_acquire)
|| !recv_queue.current(expected_handle, expected_generation)) return;
send_queue_len = 0;
_last_retry_attempt = 0;
_tx_stall_watchdog.reset();
_companion_start_timer.cancel();
recv_queue.disconnect(expected_handle);
_tx_recovery_handle = expected_handle;
_tx_recovery_generation = recv_queue.generation();
_tx_disconnect_recovery.begin();
}
// Invalidate RX immediately, but let normal connection cleanup flush its
// FIFO. An old recovery must not flush a replacement link after yielding.
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
setMotaStreamActive(false);
#endif
serviceTxRecovery((uint32_t)millis());
}
bool SerialBLEInterface::isValidConnection(uint16_t handle, bool requireWaitingForSecurity) const {
if (_conn_handle != handle) {
return false;
}
BLEConnection* conn = Bluefruit.Connection(handle);
if (conn == nullptr || !conn->connected()) {
return false;
}
if (requireWaitingForSecurity && _isDeviceConnected) {
return false;
}
return true;
}
bool SerialBLEInterface::isAdvertising() const {
return Bluefruit.Advertising.isRunning();
}
void SerialBLEInterface::enable() {
if (_isEnabled) return;
recv_queue.setEnabled(true);
_pairingRequestPending.store(false, std::memory_order_release);
_isEnabled = true;
clearBuffers();
servicePendingTxReset();
_last_health_check = millis();
if (advertisingAllowed()) {
Bluefruit.Advertising.restartOnDisconnect(true);
startAdvertising("advertising failed while enabling Bluetooth");
}
}
void SerialBLEInterface::disconnect() {
if (_conn_handle != BLE_CONN_HANDLE_INVALID) {
recv_queue.disconnect(_conn_handle);
const uint32_t result = sd_ble_gap_disconnect(_conn_handle, BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);
meshBleTrace(0xf013, _conn_handle, result, 0);
(void)result;
}
}
void SerialBLEInterface::disable() {
recv_queue.setEnabled(false);
_tx_reset_pending.store(true, std::memory_order_release);
_isEnabled = false;
_pairingRequestPending.store(false, std::memory_order_release);
BLE_DEBUG_PRINTLN("SerialBLEInterface: disable");
Bluefruit.Advertising.restartOnDisconnect(false);
Bluefruit.Advertising.stop();
disconnect();
_security_timer.cancel();
_companion_start_timer.cancel();
_last_health_check = 0;
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
setMotaStreamActive(false);
#endif
}
void SerialBLEInterface::loop() {
const uint32_t now = (uint32_t)millis();
#if defined(MESH_NRF52_BLE_TRACE) && MESH_NRF52_BLE_TRACE
if (_debug_last_loop_ms) meshBleTraceMax(mesh_ble_trace.max_loop_gap_ms,
now - _debug_last_loop_ms);
_debug_last_loop_ms = now;
#endif
serviceBondedOnlyTransition();
servicePendingTxReset();
if (_tx_disconnect_recovery.pending()) {
serviceTxRecovery(now);
return;
}
if (!_isEnabled) return;
if (_conn_handle != BLE_CONN_HANDLE_INVALID) {
if (_security_timer.expired(now)) {
// Keep the two-minute PIN-entry window, but do not let an unfinished
// security exchange occupy the only BLE connection indefinitely.
BLE_DEBUG_PRINTLN("SerialBLEInterface: security setup timed out");
meshBleTrace(0xf021, _conn_handle);
_security_timer.cancel();
disconnect();
}
if (_companionDataSeen.load(std::memory_order_acquire)) {
_companion_start_timer.cancel();
} else if (_isDeviceConnected && _companion_start_timer.expired(
now, BLE_COMPANION_START_TIMEOUT_MS)) {
// iOS can report a secured BLE link while the app never establishes its
// UART session. Release that occupied link so the app can reconnect.
meshBleTrace(0xf022, _conn_handle);
recoverStalledTx("no Companion data after secured BLE connection");
}
return;
}
if (advertisingAllowed()
&& now - _last_health_check >= BLE_HEALTH_CHECK_INTERVAL) {
_last_health_check = now;
if (!isAdvertising()) {
BLE_DEBUG_PRINTLN("SerialBLEInterface: advertising watchdog restarting");
meshBleTrace(0xf023);
startAdvertising("advertising watchdog restart failed");
}
}
}
bool SerialBLEInterface::hasRequiredReplyCapacity() const {
if (_tx_reset_pending.load(std::memory_order_acquire)) return true;
if (send_queue_len < FRAME_QUEUE_SIZE) return true;
// Required responses may evict an unsent packet log, never another required
// reply. Match enqueueCompanionFrame without growing either bounded queue.
for (uint8_t i = 0; i < send_queue_len; ++i) {
if (!mesh::companionFrameRequiresDelivery(send_queue[i].buf, send_queue[i].len))
return true;
}
return false;
}
size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) {
if (len > MAX_FRAME_SIZE) {
BLE_DEBUG_PRINTLN("writeFrame(), frame too big, len=%u", (unsigned)len);
return 0;
}
servicePendingTxReset();
const uint16_t handle = _conn_handle;
uint32_t generation;
if (!recv_queue.snapshot(handle, generation)) return 0;
if (send_queue_len && _send_queue_generation != generation) resetTxState();
_send_queue_generation = generation;
bool connected = isConnected();
if (connected && len > 0) {
if (!mesh::enqueueCompanionFrame(send_queue, send_queue_len, FRAME_QUEUE_SIZE,
src, len)) {
meshBleTrace(0xf033, _conn_handle, send_queue_len, len);
BLE_DEBUG_PRINTLN("writeFrame(), send_queue is full!");
return 0;
}
if (servicePendingTxReset() || !recv_queue.current(handle, generation)) {
resetTxState();
return 0;
}
return len;
}
return 0;
}
size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) {
const uint32_t check_now = (uint32_t)millis();
serviceBondedOnlyTransition();
servicePendingTxReset();
if (_tx_disconnect_recovery.pending()) {
serviceTxRecovery(check_now);
return 0;
}
// Restored bonded subscriptions can be enabled while the client is still
// discovering services and negotiating its MTU. An unsolicited push at the
// default MTU can exhaust the notification slots partway through a frame.
// The independent UART RX callback (or explicit mOTA activation) establishes
// application readiness. Keep queued frames and their TX watchdog idle until
// then; the secured-link no-data timer still bounds an abandoned connection.
if (!_companionDataSeen.load(std::memory_order_acquire)) return 0;
if (send_queue_len > 0) {
if (!isConnected()) {
BLE_DEBUG_PRINTLN("writeBytes: connection invalid, clearing send queue");
send_queue_len = 0;
_last_retry_attempt = 0;
_tx_stall_watchdog.reset();
} else {
uint32_t now = check_now;
bool throttle_active = (_last_retry_attempt > 0 && (now - _last_retry_attempt) < BLE_RETRY_THROTTLE_MS);
if (!throttle_active) {
const uint16_t handle = _conn_handle;
const uint32_t generation = _send_queue_generation;
Frame frame_to_send = send_queue[0];
const bool delivery_required = mesh::companionFrameRequiresDelivery(
frame_to_send.buf, frame_to_send.len);
size_t written = writeBleUartFrame(frame_to_send);
// A callback may reset or replace the link while the SDK waits for a
// notification slot. Discard only the old TX state; do not recover or
// dequeue against the replacement session using the old write result.
if (servicePendingTxReset() || !recv_queue.current(handle, generation)) {
resetTxState();
return 0;
}
meshBleTrace(0xf031, _conn_handle, frame_to_send.len, written);
#if defined(MESH_NRF52_BLE_TRACE) && MESH_NRF52_BLE_TRACE
if (written) mesh_ble_trace.tx.fetch_add(1);
#endif
if (written == frame_to_send.len) {
BLE_DEBUG_PRINTLN("writeBytes: sz=%u, hdr=%u", (unsigned)frame_to_send.len, (unsigned)frame_to_send.buf[0]);
_last_retry_attempt = 0;
_tx_stall_watchdog.reset();
shiftSendQueueLeft();
} else if (written > 0) {
BLE_DEBUG_PRINTLN("writeBytes: partial write, sent=%u of %u",
(unsigned)written,
(unsigned)frame_to_send.len);
// The app cannot recover framing after receiving only part of one
// protocol frame. Reconnect instead of following it with another
// frame on the same BLE UART stream.
meshBleTrace(0xf025, _conn_handle, written, frame_to_send.len);
recoverStalledTx("partial BLE UART frame", handle, generation);
return 0;
} else {
if (!isConnected()) {
BLE_DEBUG_PRINTLN("writeBytes failed: connection lost, dropping frame");
_last_retry_attempt = 0;
_tx_stall_watchdog.reset();
shiftSendQueueLeft();
} else {
BLE_DEBUG_PRINTLN("writeBytes failed (buffer full), keeping frame for retry");
_last_retry_attempt = now;
if (delivery_required) {
if (_tx_stall_watchdog.noteBlocked((uint32_t)now)) {
meshBleTrace(0xf024, _conn_handle);
recoverStalledTx("command reply blocked for 10 seconds", handle, generation);
return 0;
}
} else {
// Best-effort pushes do not make an otherwise healthy but idle
// app reconnect. A later response is inserted ahead of them and
// starts its own bounded watchdog window.
_tx_stall_watchdog.reset();
}
}
}
}
}
} else {
_tx_stall_watchdog.reset();
}
// TX/recovery above must run even when full. Retain RX until a required
// command reply can be admitted instead of silently losing that reply.
if (hasRequiredReplyCapacity()) {
const size_t len = recv_queue.pop(dest);
if (len) {
BLE_DEBUG_PRINTLN("readBytes: sz=%u, hdr=%u", (unsigned)len, (unsigned)dest[0]);
}
return len;
}
return 0;
}
void SerialBLEInterface::onBleUartRX(uint16_t conn_handle) {
SerialBLEInterface* self = instance;
if (!self) return;
uint32_t generation;
if (!self->recv_queue.snapshot(conn_handle, generation)
|| !self->isConnected()) return;
meshBleTrace(0xf030, conn_handle, self->bleuart.available(), true);
while (self->recv_queue.current(conn_handle, generation)) {
const int avail = self->bleuart.available();
if (avail <= 0) return;
if (avail > MAX_FRAME_SIZE) {
BLE_DEBUG_PRINTLN("onBleUartRX: WARN: BLE RX overflow, avail=%d, draining all", avail);
self->bleuart.flush();
return;
}
// BLEUart::read() is an immediate FIFO read. Stream::readBytes() may wait
// for bytes flushed by a simultaneous reset. Stage outside the critical
// section, then reject the frame if its connection generation changed.
Frame frame = {};
const int read_len = self->bleuart.read(frame.buf, avail);
if (read_len != avail) return;
frame.len = read_len;
RxQueue::PushResult result;
{
RxQueueGuard guard;
result = self->recv_queue.push(conn_handle, generation, frame);
if (result == RxQueue::PushResult::Accepted) {
self->_companionDataSeen.store(true, std::memory_order_release);
self->_companion_start_timer.cancel();
}
}
if (result == RxQueue::PushResult::Stale) return;
if (result == RxQueue::PushResult::Full) {
self->bleuart.flush();
meshBleTrace(0xf034, conn_handle, FRAME_QUEUE_SIZE);
BLE_DEBUG_PRINTLN("onBleUartRX: recv queue full, dropping data");
return;
}
#if defined(MESH_NRF52_BLE_TRACE) && MESH_NRF52_BLE_TRACE
mesh_ble_trace.rx.fetch_add(1);
#endif
}
}
#if defined(MESH_NRF52_BLE_TRACE) && MESH_NRF52_BLE_TRACE
void SerialBLEInterface::onBleUartNotify(uint16_t handle, bool enabled) {
meshBleTrace(0xf040, handle, enabled);
}
void SerialBLEInterface::onBleUartOverflow(uint16_t handle, uint16_t leftover) {
meshBleTrace(0xf035, handle, leftover);
}
#endif
bool SerialBLEInterface::isConnected() const {
uint32_t generation;
return _isEnabled && (_tx_reset_pending.load(std::memory_order_acquire)
|| !_tx_disconnect_recovery.pending()) && _isDeviceConnected &&
recv_queue.snapshot(_conn_handle, generation) &&
Bluefruit.connected() > 0;
}
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
bool SerialBLEInterface::isMotaChannelReady() {
return _mota_available && isConnected()
&& _conn_handle != BLE_CONN_HANDLE_INVALID
&& _mota_request.notifyEnabled(_conn_handle);
}
uint16_t SerialBLEInterface::motaResponseCapacity() {
if (!isMotaChannelReady() || !_mota_stream.isActive()) return 0;
BLEConnection* connection = Bluefruit.Connection(_conn_handle);
if (!connection || !connection->connected()) return 0;
const uint16_t mtu = connection->getMtu();
if (mtu < 23) return 0;
const uint16_t capacity = (uint16_t)(mtu - 3u);
return capacity < mesh::ota::BLE_MOTA_RESPONSE_MAX
? capacity : mesh::ota::BLE_MOTA_RESPONSE_MAX;
}
#endif
bool SerialBLEInterface::isReadBusy() const {
return recv_queue.size() > 0;
}
bool SerialBLEInterface::isWriteBusy() const {
return !_tx_reset_pending.load(std::memory_order_acquire)
&& send_queue_len >= (FRAME_QUEUE_SIZE * 2 / 3);
}
bool SerialBLEInterface::hasPendingIO() const {
return recv_queue.size() > 0
|| (!_tx_reset_pending.load(std::memory_order_acquire) && send_queue_len > 0);
}