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

1102 lines
37 KiB
C++

#include "SerialBLEInterface.h"
#include "../BluetoothMac.h"
#include "../CompanionFrameQueue.h"
#include "esp_mac.h"
#include <stdlib.h>
#if defined(CONFIG_BLUEDROID_ENABLED) && !defined(MESH_USE_NIMBLE_ARDUINO)
#include "esp_gap_ble_api.h"
#endif
#if MESH_BLE_USES_NIMBLE && !defined(MESH_USE_NIMBLE_ARDUINO)
#include <host/ble_store.h>
#endif
// Arduino 3.x relies on a library constructor to mark BLE as used before
// initArduino(), and that constructor ordering is not reliable with all link
// layouts/LTO combinations. A strong override keeps the controller memory
// available until SerialBLEInterface::begin(); Full Companion then reserves
// BLE controller/host memory before starting WiFi.
extern "C" bool bleInUse(void) {
return true;
}
// See the following for generating UUIDs:
// https://www.uuidgenerator.net/
#define SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" // UART service UUID
#define CHARACTERISTIC_UUID_RX "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
#define CHARACTERISTIC_UUID_TX "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
#define ADVERT_RESTART_DELAY 1000 // millis
#define BLE_BOND_PERSIST_TIMEOUT_MS 15000
static void cleanupFailedBluetoothStart() {
#if defined(MESH_USE_NIMBLE_ARDUINO)
BLEDevice::deinit(true);
#else
BLEDevice::deinit(false);
#endif
}
#if !defined(MESH_USE_NIMBLE_ARDUINO)
static void clearStoredBluetoothBonds() {
#if MESH_BLE_USES_NIMBLE
const int result = ble_store_clear();
if (result != 0) {
BLE_DEBUG_PRINTLN("Could not clear NimBLE bonds: %d", result);
}
#else
int count = esp_ble_get_bond_device_num();
if (count <= 0) return;
esp_ble_bond_dev_t* devices = static_cast<esp_ble_bond_dev_t*>(
malloc(sizeof(esp_ble_bond_dev_t) * count));
if (devices == NULL) {
BLE_DEBUG_PRINTLN("Could not allocate Bluetooth bond list");
return;
}
if (esp_ble_get_bond_device_list(&count, devices) != ESP_OK) {
BLE_DEBUG_PRINTLN("Could not read Bluetooth bond list");
free(devices);
return;
}
for (int i = 0; i < count; i++) {
if (esp_ble_remove_bond_device(devices[i].bd_addr) != ESP_OK) {
BLE_DEBUG_PRINTLN("Could not remove Bluetooth bond %d", i);
}
}
free(devices);
#endif
}
#endif
static bool bluetoothPeersEqual(
const mesh::companion::BluetoothPeerIdentity& lhs,
const mesh::companion::BluetoothPeerIdentity& rhs) {
return lhs.type == rhs.type
&& memcmp(lhs.address, rhs.address, sizeof(lhs.address)) == 0;
}
void SerialBLEInterface::noteSuccessfulConnection(
const mesh::companion::BluetoothPeerIdentity& peer) {
if (_stealth_pair_once) {
_advertisingSuppressed.store(true, std::memory_order_release);
_adv_restart_pending = false;
}
if (!_successfulConnectionPending.load(std::memory_order_acquire)) {
_successfulPeer = peer;
_successfulConnectionStarted.store(
(uint32_t)millis(), std::memory_order_relaxed);
_successfulConnectionPending.store(true, std::memory_order_release);
}
}
#if MESH_BLE_USES_NIMBLE
static void bluetoothPeerFromNimbleAddress(
const ble_addr_t& address,
mesh::companion::BluetoothPeerIdentity& peer) {
peer.type = (address.type & 0x01u) == BLE_ADDR_PUBLIC
? mesh::companion::BLUETOOTH_PEER_ADDRESS_PUBLIC
: mesh::companion::BLUETOOTH_PEER_ADDRESS_RANDOM;
for (size_t i = 0; i < mesh::companion::BLUETOOTH_MAC_BYTES; i++) {
peer.address[i] = address.val[
mesh::companion::BLUETOOTH_MAC_BYTES - 1 - i];
}
}
static ble_addr_t nimbleAddressFromBluetoothPeer(
const mesh::companion::BluetoothPeerIdentity& peer) {
ble_addr_t address = {};
address.type =
peer.type == mesh::companion::BLUETOOTH_PEER_ADDRESS_PUBLIC
? BLE_ADDR_PUBLIC
: BLE_ADDR_RANDOM;
for (size_t i = 0; i < mesh::companion::BLUETOOTH_MAC_BYTES; i++) {
address.val[i] = peer.address[
mesh::companion::BLUETOOTH_MAC_BYTES - 1 - i];
}
return address;
}
static bool nimbleBondExistsForPeer(
const mesh::companion::BluetoothPeerIdentity& peer) {
#if defined(MESH_USE_NIMBLE_ARDUINO)
return BLEDevice::isBonded(BLEAddress(nimbleAddressFromBluetoothPeer(peer)));
#else
ble_addr_t bonded_peers[8];
int count = 0;
if (ble_store_util_bonded_peers(
bonded_peers, &count,
sizeof(bonded_peers) / sizeof(bonded_peers[0])) != 0) {
return false;
}
for (int i = 0; i < count; i++) {
mesh::companion::BluetoothPeerIdentity candidate;
bluetoothPeerFromNimbleAddress(bonded_peers[i], candidate);
if (bluetoothPeersEqual(candidate, peer)) return true;
}
return false;
#endif
}
#else
static void bluetoothPeerFromBluedroidBond(
const esp_ble_bond_dev_t& bond, uint8_t fallback_type,
mesh::companion::BluetoothPeerIdentity& peer) {
const bool has_identity =
(bond.bond_key.key_mask & ESP_LE_KEY_PID) != 0;
const uint8_t* address = has_identity
? bond.bond_key.pid_key.static_addr
: bond.bd_addr;
const uint8_t address_type = has_identity
? bond.bond_key.pid_key.addr_type
: fallback_type;
peer.type = address_type == BLE_ADDR_TYPE_PUBLIC
? mesh::companion::BLUETOOTH_PEER_ADDRESS_PUBLIC
: mesh::companion::BLUETOOTH_PEER_ADDRESS_RANDOM;
memcpy(peer.address, address, sizeof(peer.address));
}
static bool findBluedroidBondedPeer(
const mesh::companion::BluetoothPeerIdentity& requested,
mesh::companion::BluetoothPeerIdentity& resolved) {
int count = esp_ble_get_bond_device_num();
if (count <= 0) return false;
esp_ble_bond_dev_t* devices = static_cast<esp_ble_bond_dev_t*>(
malloc(sizeof(esp_ble_bond_dev_t) * count));
if (devices == NULL) return false;
if (esp_ble_get_bond_device_list(&count, devices) != ESP_OK) {
free(devices);
return false;
}
bool found = false;
for (int i = 0; i < count; i++) {
mesh::companion::BluetoothPeerIdentity candidate;
bluetoothPeerFromBluedroidBond(
devices[i],
requested.type == mesh::companion::BLUETOOTH_PEER_ADDRESS_PUBLIC
? BLE_ADDR_TYPE_PUBLIC
: BLE_ADDR_TYPE_RANDOM,
candidate);
if (bluetoothPeersEqual(candidate, requested)
|| memcmp(devices[i].bd_addr, requested.address,
sizeof(requested.address)) == 0
|| count == 1) {
resolved = candidate;
found = mesh::companion::isValidBluetoothPeerIdentity(resolved);
break;
}
}
free(devices);
return found;
}
#endif
bool SerialBLEInterface::resolveSuccessfulPeer(
mesh::companion::BluetoothPeerIdentity& peer) const {
#if MESH_BLE_USES_NIMBLE
if (!mesh::companion::isValidBluetoothPeerIdentity(_successfulPeer)
|| !nimbleBondExistsForPeer(_successfulPeer)) {
return false;
}
peer = _successfulPeer;
return true;
#else
if (!mesh::companion::isValidBluetoothPeerIdentity(_successfulPeer)) {
return false;
}
return findBluedroidBondedPeer(_successfulPeer, peer);
#endif
}
bool SerialBLEInterface::configureBondedOnlyAdvertising(
const mesh::companion::BluetoothPeerIdentity& peer,
bool require_stored_bond) {
if (!mesh::companion::isValidBluetoothPeerIdentity(peer)
|| pServer == NULL) {
return false;
}
#if MESH_BLE_USES_NIMBLE
if (require_stored_bond && !nimbleBondExistsForPeer(peer)) {
requestBondedOnlyRecovery("saved peer bond is unavailable");
return false;
}
const ble_addr_t native_peer = nimbleAddressFromBluetoothPeer(peer);
if (ble_gap_wl_set(&native_peer, 1) != 0) {
requestBondedOnlyRecovery("could not set NimBLE allowlist");
return false;
}
#else
if (require_stored_bond) {
mesh::companion::BluetoothPeerIdentity resolved;
if (!findBluedroidBondedPeer(peer, resolved)
|| !bluetoothPeersEqual(peer, resolved)) {
requestBondedOnlyRecovery("saved peer bond is unavailable");
return false;
}
}
esp_bd_addr_t native_peer;
memcpy(native_peer, peer.address, sizeof(native_peer));
const esp_ble_wl_addr_type_t address_type =
peer.type == mesh::companion::BLUETOOTH_PEER_ADDRESS_PUBLIC
? BLE_WL_ADDR_TYPE_PUBLIC
: BLE_WL_ADDR_TYPE_RANDOM;
// The Bluedroid API adds one entry at a time. Replace any controller state
// left by an older mode/session so the requested peer is genuinely the only
// address allowed to scan or connect.
if (esp_ble_gap_clear_whitelist() != ESP_OK) {
requestBondedOnlyRecovery("could not clear Bluedroid allowlist");
return false;
}
if (esp_ble_gap_update_whitelist(true, native_peer, address_type)
!= ESP_OK) {
requestBondedOnlyRecovery("could not set Bluedroid allowlist");
return false;
}
#endif
BLEAdvertising* advertising = pServer->getAdvertising();
if (advertising == NULL) {
requestBondedOnlyRecovery("Bluetooth advertising is unavailable");
return false;
}
advertising->stop();
BLEAdvertisementData minimal_advertisement;
#if defined(MESH_USE_NIMBLE_ARDUINO)
minimal_advertisement.setFlags(BLE_HS_ADV_F_BREDR_UNSUP);
advertising->enableScanResponse(false);
#else
minimal_advertisement.setFlags(ESP_BLE_ADV_FLAG_BREDR_NOT_SPT);
advertising->setScanResponse(false);
#endif
advertising->setScanFilter(true, true);
advertising->setAdvertisementData(minimal_advertisement);
_bonded_only = true;
_stealth_pair_once = false;
_advertisingSuppressed.store(false, std::memory_order_release);
_adv_restart_pending = false;
if (_isEnabled && pServer->getConnectedCount() == 0) {
advertising->start();
}
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);
_adv_restart_pending = false;
if (pServer != NULL && pServer->getAdvertising() != NULL) {
pServer->getAdvertising()->stop();
}
}
bool SerialBLEInterface::advertisingAllowed() const {
return _isEnabled
&& !_advertisingSuppressed.load(std::memory_order_acquire)
&& !_bondedOnlyRecoveryPending.load(std::memory_order_acquire);
}
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) {
_pin_code = pin_code;
_successfulConnectionPending.store(false, std::memory_order_release);
_successfulConnectionStarted.store(0, std::memory_order_relaxed);
_bondedOnlyRecoveryPending.store(false, std::memory_order_release);
_advertisingSuppressed.store(false, std::memory_order_release);
_successfulPeer = mesh::companion::BluetoothPeerIdentity();
_stealth_pair_once = stealth_pair_once;
_bonded_only = false;
if (custom_address != nullptr
&& !mesh::companion::isValidBluetoothMac(custom_address)) {
BLE_DEBUG_PRINTLN("Custom Bluetooth MAC is invalid");
return false;
}
char resolved_name[32];
const char* suffix = name;
if (strcmp(name, "@@MAC") == 0) {
uint8_t addr[8];
memset(addr, 0, sizeof(addr));
esp_efuse_mac_get_default(addr);
snprintf(resolved_name, sizeof(resolved_name),
"%02X%02X%02X%02X%02X%02X",
addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]);
suffix = resolved_name;
}
char dev_name[32+16];
const int dev_name_len = snprintf(dev_name, sizeof(dev_name), "%s%s",
prefix, suffix);
if (dev_name_len < 0 || dev_name_len >= (int)sizeof(dev_name)) {
BLE_DEBUG_PRINTLN("Bluetooth device name is too long");
return false;
}
// Create the BLE Device
#if MESH_BLE_USES_NIMBLE
if (!BLEDevice::init(dev_name)) {
BLE_DEBUG_PRINTLN("BLEDevice::init failed");
return false;
}
#else
// Arduino-ESP32 2.x Bluedroid exposes a void init(). Its first observable
// failure is a null server below, which is handled without dereferencing it.
BLEDevice::init(dev_name);
#endif
#if defined(MESH_USE_NIMBLE_ARDUINO)
if (clear_bonds && !BLEDevice::deleteAllBonds()) {
BLE_DEBUG_PRINTLN("Could not clear NimBLE bonds for the new identity");
cleanupFailedBluetoothStart();
return false;
}
#else
if (clear_bonds) clearStoredBluetoothBonds();
#endif
#if MESH_BLE_USES_NIMBLE
if (custom_address != nullptr) {
uint8_t native_address[mesh::companion::BLUETOOTH_MAC_BYTES];
for (size_t i = 0; i < mesh::companion::BLUETOOTH_MAC_BYTES; i++) {
native_address[i] = custom_address[
mesh::companion::BLUETOOTH_MAC_BYTES - 1 - i];
}
if (!BLEDevice::setOwnAddr(native_address)
|| !BLEDevice::setOwnAddrType(BLE_OWN_ADDR_RANDOM)) {
BLE_DEBUG_PRINTLN("Custom Bluetooth MAC setup failed");
cleanupFailedBluetoothStart();
return false;
}
}
#if defined(MESH_USE_NIMBLE_ARDUINO)
else if (!BLEDevice::setOwnAddrType(BLE_OWN_ADDR_PUBLIC)) {
cleanupFailedBluetoothStart();
return false;
}
#endif
#endif
#if !defined(MESH_USE_NIMBLE_ARDUINO)
BLEDevice::setSecurityCallbacks(this);
#endif
// ATT notifications consume three bytes of the negotiated MTU. Reserve
// that overhead so a MAX_FRAME_SIZE protocol frame fits without truncation.
BLEDevice::setMTU(MAX_FRAME_SIZE + 3);
#if defined(MESH_USE_NIMBLE_ARDUINO)
// NimBLE-Arduino 2.5.1 bypasses onPassKeyDisplay() for a non-default
// static passkey. Keep its callback sentinel here: our callback returns
// the actual random/saved _pin_code and also wakes the pairing display.
// This does not change the PIN used on air or weaken authentication.
BLEDevice::setSecurityPasskey(123456);
BLEDevice::setSecurityIOCap(BLE_HS_IO_DISPLAY_ONLY);
BLEDevice::setSecurityAuth(true, true, true);
#else
BLESecurity sec;
#if MESH_BLE_USES_NIMBLE
sec.setPassKey(true, pin_code);
// A passkey alone does not provide MITM protection when the controller's
// default capability is NoInputNoOutput: the peers can silently fall back
// to Just Works and create an unauthenticated bond. The Companion displays
// (or otherwise publishes) this PIN for entry on the phone/host, so declare
// the peripheral as DisplayOnly and force the passkey-entry association.
sec.setCapability(ESP_IO_CAP_OUT);
sec.setAuthenticationMode(true, true, true);
#else
sec.setStaticPIN(pin_code);
sec.setCapability(ESP_IO_CAP_OUT);
sec.setAuthenticationMode(ESP_LE_AUTH_REQ_SC_MITM_BOND);
#endif
#endif
//BLEDevice::setPower(ESP_PWR_LVL_N8);
// Create the BLE Server
pServer = BLEDevice::createServer();
if (pServer == NULL) {
BLE_DEBUG_PRINTLN("BLEDevice::createServer failed");
cleanupFailedBluetoothStart();
return false;
}
#if defined(MESH_USE_NIMBLE_ARDUINO)
// The interface is owned by the application, not by the NimBLE server.
pServer->setCallbacks(this, false);
pServer->advertiseOnDisconnect(false);
#else
pServer->setCallbacks(this);
#endif
#if !MESH_BLE_USES_NIMBLE
if (custom_address != nullptr) {
esp_bd_addr_t native_address;
memcpy(native_address, custom_address, sizeof(native_address));
// Arduino-ESP32 2.x returns void here, while newer Bluedroid wrappers
// return bool. Both configure the advertising address type and report any
// controller failure through the BLE library log.
pServer->getAdvertising()->setDeviceAddress(
native_address, BLE_ADDR_TYPE_RANDOM);
}
#endif
// Create the BLE Service
pService = pServer->createService(SERVICE_UUID);
if (pService == NULL) {
BLE_DEBUG_PRINTLN("BLEServer::createService failed");
cleanupFailedBluetoothStart();
pServer = NULL;
return false;
}
// Create a BLE Characteristic
#if defined(MESH_USE_NIMBLE_ARDUINO)
uint32_t tx_properties = NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY |
NIMBLE_PROPERTY::READ_AUTHEN;
uint32_t rx_properties = NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_AUTHEN;
#else
uint32_t tx_properties = BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_NOTIFY;
uint32_t rx_properties = BLECharacteristic::PROPERTY_WRITE;
#if MESH_BLE_USES_NIMBLE
// NimBLE ignores setAccessPermissions(). Authentication requirements must
// be part of the characteristic properties instead.
tx_properties |= BLECharacteristic::PROPERTY_READ_AUTHEN;
rx_properties |= BLECharacteristic::PROPERTY_WRITE_AUTHEN;
#endif
#endif
pTxCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID_TX, tx_properties);
if (pTxCharacteristic == NULL) {
BLE_DEBUG_PRINTLN("BLEService::createCharacteristic(TX) failed");
cleanupFailedBluetoothStart();
pServer = NULL;
pService = NULL;
return false;
}
#if !defined(MESH_USE_NIMBLE_ARDUINO)
pTxCharacteristic->setAccessPermissions(ESP_GATT_PERM_READ_ENC_MITM);
#endif
pTxCharacteristic->setCallbacks(this);
#if !MESH_BLE_USES_NIMBLE
// NimBLE creates and owns the 0x2902 descriptor automatically.
pTxDescriptor = new BLE2902();
// Make notification setup start/finish pairing before the client begins its
// short device-info request timeout. The RX and TX characteristics already
// require the same MITM-encrypted link, so this does not add a new pairing
// requirement; it only moves it earlier in the connection handshake.
pTxDescriptor->setAccessPermissions(
(esp_gatt_perm_t)(ESP_GATT_PERM_READ_ENC_MITM | ESP_GATT_PERM_WRITE_ENC_MITM));
pTxCharacteristic->addDescriptor(pTxDescriptor);
#endif
BLECharacteristic * pRxCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID_RX, rx_properties);
if (pRxCharacteristic == NULL) {
BLE_DEBUG_PRINTLN("BLEService::createCharacteristic(RX) failed");
cleanupFailedBluetoothStart();
pServer = NULL;
pService = NULL;
pTxCharacteristic = NULL;
#if !defined(MESH_USE_NIMBLE_ARDUINO)
pTxDescriptor = NULL;
#endif
return false;
}
#if !defined(MESH_USE_NIMBLE_ARDUINO)
pRxCharacteristic->setAccessPermissions(ESP_GATT_PERM_WRITE_ENC_MITM);
#endif
pRxCharacteristic->setCallbacks(this);
#if defined(MESH_USE_NIMBLE_ARDUINO)
pServer->getAdvertising()->setName(dev_name);
pServer->getAdvertising()->enableScanResponse(true);
#endif
pServer->getAdvertising()->addServiceUUID(SERVICE_UUID);
if (bonded_only_peer != nullptr) {
configureBondedOnlyAdvertising(*bonded_only_peer, true);
}
return true;
}
// -------- BLESecurityCallbacks methods
#if defined(MESH_USE_NIMBLE_ARDUINO)
uint32_t SerialBLEInterface::onPassKeyDisplay() {
_pairingRequestPending.store(true, std::memory_order_release);
return _pin_code;
}
void SerialBLEInterface::onAuthenticationComplete(NimBLEConnInfo& info) {
if (info.isEncrypted() && info.isAuthenticated() && info.isBonded()) {
deviceConnected = true;
mesh::companion::BluetoothPeerIdentity peer;
bluetoothPeerFromNimbleAddress(*info.getIdAddress().getBase(), peer);
noteSuccessfulConnection(peer);
} else {
BLEDevice::deleteBond(info.getIdAddress());
deviceConnected = false;
if (_bonded_only) requestBondedOnlyRecovery("bond authentication failed");
pServer->disconnect(info.getConnHandle());
if (advertisingAllowed()) scheduleAdvertisingRestart((uint32_t)millis());
}
}
#else
uint32_t SerialBLEInterface::onPassKeyRequest() {
BLE_DEBUG_PRINTLN("onPassKeyRequest()");
_pairingRequestPending.store(true, std::memory_order_release);
return _pin_code;
}
void SerialBLEInterface::onPassKeyNotify(uint32_t pass_key) {
BLE_DEBUG_PRINTLN("onPassKeyNotify(%u)", pass_key);
_pairingRequestPending.store(true, std::memory_order_release);
}
bool SerialBLEInterface::onConfirmPIN(uint32_t pass_key) {
BLE_DEBUG_PRINTLN("onConfirmPIN(%u)", pass_key);
_pairingRequestPending.store(true, std::memory_order_release);
return true;
}
bool SerialBLEInterface::onSecurityRequest() {
BLE_DEBUG_PRINTLN("onSecurityRequest()");
return true; // allow
}
#if MESH_BLE_USES_NIMBLE
void SerialBLEInterface::onAuthenticationComplete(ble_gap_conn_desc* desc) {
const bool success = desc != NULL && desc->sec_state.encrypted &&
desc->sec_state.authenticated
&& desc->sec_state.bonded;
if (success) {
BLE_DEBUG_PRINTLN(" - SecurityCallback - Authentication Success");
deviceConnected = true;
mesh::companion::BluetoothPeerIdentity peer;
bluetoothPeerFromNimbleAddress(desc->peer_id_addr, peer);
noteSuccessfulConnection(peer);
} else {
BLE_DEBUG_PRINTLN(" - SecurityCallback - Authentication Failure");
if (desc != NULL) {
int remove_result = ble_store_util_delete_peer(&desc->peer_id_addr);
if (remove_result == 0) {
BLE_DEBUG_PRINTLN(" - SecurityCallback - Cleared failed peer bond");
} else {
BLE_DEBUG_PRINTLN(" - SecurityCallback - No peer bond cleared, err=%d",
remove_result);
}
}
deviceConnected = false;
if (_bonded_only) {
requestBondedOnlyRecovery("bond authentication failed");
}
if (desc != NULL) {
pServer->disconnect(desc->conn_handle);
}
if (advertisingAllowed()) {
scheduleAdvertisingRestart((uint32_t)millis());
}
}
}
#else
void SerialBLEInterface::onAuthenticationComplete(esp_ble_auth_cmpl_t cmpl) {
if (cmpl.success) {
BLE_DEBUG_PRINTLN(" - SecurityCallback - Authentication Success");
deviceConnected = true;
mesh::companion::BluetoothPeerIdentity peer;
peer.type = cmpl.addr_type == BLE_ADDR_TYPE_PUBLIC
? mesh::companion::BLUETOOTH_PEER_ADDRESS_PUBLIC
: mesh::companion::BLUETOOTH_PEER_ADDRESS_RANDOM;
memcpy(peer.address, cmpl.bd_addr, sizeof(peer.address));
noteSuccessfulConnection(peer);
} else {
BLE_DEBUG_PRINTLN(" - SecurityCallback - Authentication Failure, reason=%u", (unsigned)cmpl.fail_reason);
// Firmware flashing normally preserves the Bluetooth bond database. If
// either side has forgotten or replaced its key, retaining the local key
// makes every reconnect fail until the device is fully erased. Remove only
// the peer whose authentication just failed; successful bonds are kept.
esp_err_t remove_result = esp_ble_remove_bond_device(cmpl.bd_addr);
if (remove_result == ESP_OK) {
BLE_DEBUG_PRINTLN(" - SecurityCallback - Cleared failed peer bond");
} else {
BLE_DEBUG_PRINTLN(" - SecurityCallback - No peer bond cleared, err=%d", (int)remove_result);
}
deviceConnected = false;
if (_bonded_only) {
requestBondedOnlyRecovery("bond authentication failed");
}
pServer->disconnect(pServer->getConnId());
if (advertisingAllowed()) {
scheduleAdvertisingRestart((uint32_t)millis());
}
}
}
#endif
#endif
// -------- BLEServerCallbacks methods
#if defined(MESH_USE_NIMBLE_ARDUINO)
void SerialBLEInterface::onConnect(BLEServer* pServer, NimBLEConnInfo& info) {
last_conn_id = info.getConnHandle();
deviceConnected = false;
oldDeviceConnected = false;
notifySucceeded = false;
notificationsEnabled.store(false, std::memory_order_release);
xQueueReset(recv_queue);
_tx_reset_pending.store(true, std::memory_order_release);
_adv_restart_pending = false;
}
void SerialBLEInterface::onMTUChange(uint16_t mtu, NimBLEConnInfo& info) {
(void)info;
BLE_DEBUG_PRINTLN("onMtuChanged(), mtu=%d", mtu);
}
#else
void SerialBLEInterface::onConnect(BLEServer* pServer) {
}
#if MESH_BLE_USES_NIMBLE
void SerialBLEInterface::onConnect(BLEServer* pServer, ble_gap_conn_desc* desc) {
BLE_DEBUG_PRINTLN("onConnect(), conn_id=%d, mtu=%d", desc->conn_handle,
pServer->getPeerMTU(desc->conn_handle));
last_conn_id = desc->conn_handle;
deviceConnected = false; // becomes usable only after authentication completes
oldDeviceConnected = false;
notifySucceeded = false;
notificationsEnabled.store(false, std::memory_order_release);
xQueueReset(recv_queue);
_tx_reset_pending.store(true, std::memory_order_release);
_adv_restart_pending = false;
}
void SerialBLEInterface::onMtuChanged(BLEServer* pServer,
ble_gap_conn_desc* desc,
uint16_t mtu) {
(void)pServer;
(void)desc;
BLE_DEBUG_PRINTLN("onMtuChanged(), mtu=%d", mtu);
}
#else
void SerialBLEInterface::onConnect(BLEServer* pServer, esp_ble_gatts_cb_param_t *param) {
BLE_DEBUG_PRINTLN("onConnect(), conn_id=%d, mtu=%d", param->connect.conn_id, pServer->getPeerMTU(param->connect.conn_id));
last_conn_id = param->connect.conn_id;
deviceConnected = false; // becomes usable only after authentication completes
oldDeviceConnected = false;
notifySucceeded = false;
// BLE callbacks run outside the Arduino loop. FreeRTOS owns the RX queue,
// so it is safe to reset here; defer the plain-array TX queue reset to the
// loop to avoid racing a notification completion.
xQueueReset(recv_queue);
_tx_reset_pending.store(true, std::memory_order_release);
_adv_restart_pending = false;
#if !defined(MESH_USE_NIMBLE_ARDUINO)
if (pTxDescriptor != NULL) pTxDescriptor->setNotifications(false);
#endif
}
void SerialBLEInterface::onMtuChanged(BLEServer* pServer, esp_ble_gatts_cb_param_t* param) {
BLE_DEBUG_PRINTLN("onMtuChanged(), mtu=%d", pServer->getPeerMTU(param->mtu.conn_id));
}
#endif
#endif
#if defined(MESH_USE_NIMBLE_ARDUINO)
void SerialBLEInterface::onDisconnect(BLEServer* pServer, NimBLEConnInfo& info,
int reason) {
(void)info;
(void)reason;
#else
void SerialBLEInterface::onDisconnect(BLEServer* pServer) {
#endif
BLE_DEBUG_PRINTLN("onDisconnect()");
deviceConnected = false;
notifySucceeded = false;
notificationsEnabled.store(false, std::memory_order_release);
xQueueReset(recv_queue);
_tx_reset_pending.store(true, std::memory_order_release);
#if !defined(MESH_USE_NIMBLE_ARDUINO)
if (pTxDescriptor != NULL) pTxDescriptor->setNotifications(false);
#endif
if (advertisingAllowed()) {
scheduleAdvertisingRestart((uint32_t)millis());
}
}
// -------- BLECharacteristicCallbacks methods
#if defined(MESH_USE_NIMBLE_ARDUINO)
void SerialBLEInterface::onWrite(BLECharacteristic* pCharacteristic,
NimBLEConnInfo& info) {
if (!info.isEncrypted() || !info.isAuthenticated() || !info.isBonded()) return;
#elif MESH_BLE_USES_NIMBLE
void SerialBLEInterface::onWrite(BLECharacteristic* pCharacteristic,
ble_gap_conn_desc* desc) {
(void)desc;
#else
void SerialBLEInterface::onWrite(BLECharacteristic* pCharacteristic,
esp_ble_gatts_cb_param_t* param) {
(void)param;
#endif
if (_tx_disconnect_recovery.pending()) {
BLE_DEBUG_PRINTLN("onWrite(): dropping frame while BLE reconnect is pending");
return;
}
#if defined(MESH_USE_NIMBLE_ARDUINO)
const auto value = pCharacteristic->getValue();
const uint8_t* rxValue = value.data();
const int len = value.size();
#else
uint8_t* rxValue = pCharacteristic->getData();
int len = pCharacteristic->getLength();
#endif
if (len > MAX_FRAME_SIZE) {
BLE_DEBUG_PRINTLN("ERROR: onWrite(), frame too big, len=%d", len);
} else {
Frame frame = {};
frame.len = len;
memcpy(frame.buf, rxValue, len);
if (xQueueSend(recv_queue, &frame, 0) != pdTRUE) {
BLE_DEBUG_PRINTLN("ERROR: onWrite(), recv_queue is full!");
}
}
}
#if defined(MESH_USE_NIMBLE_ARDUINO)
void SerialBLEInterface::onSubscribe(BLECharacteristic* pCharacteristic,
NimBLEConnInfo& info, uint16_t subValue) {
(void)info;
#elif MESH_BLE_USES_NIMBLE
void SerialBLEInterface::onSubscribe(BLECharacteristic* pCharacteristic,
ble_gap_conn_desc* desc,
uint16_t subValue) {
(void)desc;
#endif
#if MESH_BLE_USES_NIMBLE
if (pCharacteristic == pTxCharacteristic) {
// NimBLE uses bit 0 for notification subscriptions.
notificationsEnabled.store((subValue & 0x0001u) != 0,
std::memory_order_release);
}
}
#endif
#if !defined(MESH_USE_NIMBLE_ARDUINO)
void SerialBLEInterface::onStatus(BLECharacteristic* pCharacteristic, Status status, uint32_t code) {
(void)pCharacteristic;
notifySucceeded = status == SUCCESS_NOTIFY;
if (!notifySucceeded) {
BLE_DEBUG_PRINTLN("notify failed, status=%d, code=%u; retaining frame",
(int)status, (unsigned)code);
}
}
#endif
// ---------- public methods
bool SerialBLEInterface::takeSuccessfulConnection(
mesh::companion::BluetoothPeerIdentity* peer) {
if (!_successfulConnectionPending.load(std::memory_order_acquire)) {
return false;
}
if (peer != nullptr && !resolveSuccessfulPeer(*peer)) {
// Bond storage can finish shortly after authentication. Keep the event
// pending so the main loop can retry without reopening general access. A
// failed bond write must not leave the node unavailable indefinitely.
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) {
return configureBondedOnlyAdvertising(peer, false);
}
void SerialBLEInterface::cancelStealthPairingTransition() {
if (!_stealth_pair_once) return;
_stealth_pair_once = false;
_advertisingSuppressed.store(false, std::memory_order_release);
if (_isEnabled && pServer != NULL && pServer->getConnectedCount() == 0) {
scheduleAdvertisingRestart((uint32_t)millis());
}
}
void SerialBLEInterface::clearBuffers() {
xQueueReset(recv_queue);
send_queue_len = 0;
notifySucceeded = false;
notificationsEnabled.store(false, std::memory_order_release);
_tx_stall_watchdog.reset();
_tx_disconnect_recovery.complete();
_tx_reset_pending.store(false, std::memory_order_release);
}
void SerialBLEInterface::servicePendingTxReset() {
if (!_tx_reset_pending.exchange(false, std::memory_order_acq_rel)) return;
send_queue_len = 0;
notifySucceeded = false;
_tx_stall_watchdog.reset();
_tx_disconnect_recovery.complete();
}
void SerialBLEInterface::scheduleAdvertisingRestart(uint32_t now) {
if (!advertisingAllowed()) {
_adv_restart_pending = false;
return;
}
_adv_restart_started = now;
_adv_restart_pending = true;
}
void SerialBLEInterface::serviceTxRecovery(uint32_t now) {
if (!_tx_disconnect_recovery.pending()) return;
if (pServer == NULL || pServer->getConnectedCount() == 0) {
BLE_DEBUG_PRINTLN("SerialBLEInterface: stalled TX link is already closed");
deviceConnected = false;
clearBuffers();
if (advertisingAllowed()) scheduleAdvertisingRestart(now);
return;
}
if (!_tx_disconnect_recovery.shouldAttempt(now)) return;
// BLEServer::disconnect() does not expose the controller return code. Keep
// recovery pending and issue another bounded request until onDisconnect() or
// getConnectedCount() confirms that the link actually closed.
pServer->disconnect(last_conn_id);
BLE_DEBUG_PRINTLN("SerialBLEInterface: stalled TX disconnect requested");
}
void SerialBLEInterface::recoverStalledTx(const char* cause) {
if (_tx_disconnect_recovery.pending()) return;
BLE_DEBUG_PRINTLN("SerialBLEInterface: %s; forcing reconnect", cause);
// Preserve the controller's physical state until the disconnect callback,
// while the recovery state makes isConnected() false to callers.
xQueueReset(recv_queue);
notifySucceeded = false;
send_queue_len = 0;
_tx_stall_watchdog.reset();
_tx_disconnect_recovery.begin();
serviceTxRecovery((uint32_t)millis());
}
void SerialBLEInterface::enable() {
if (_isEnabled) return;
_isEnabled = true;
clearBuffers();
// Start the service
#if defined(MESH_USE_NIMBLE_ARDUINO)
if (!pServer->start()) {
_isEnabled = false;
return;
}
#else
pService->start();
#endif
// Start advertising
//pServer->getAdvertising()->setMinInterval(500);
//pServer->getAdvertising()->setMaxInterval(1000);
if (advertisingAllowed()) {
pServer->getAdvertising()->start();
}
_adv_restart_pending = false;
}
void SerialBLEInterface::disable() {
_isEnabled = false;
BLE_DEBUG_PRINTLN("SerialBLEInterface::disable");
pServer->getAdvertising()->stop();
pServer->disconnect(last_conn_id);
#if !defined(MESH_USE_NIMBLE_ARDUINO)
pService->stop();
#endif
oldDeviceConnected = deviceConnected = false;
clearBuffers();
_adv_restart_pending = false;
}
size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) {
servicePendingTxReset();
if (len > MAX_FRAME_SIZE) {
BLE_DEBUG_PRINTLN("writeFrame(), frame too big, len=%d", len);
return 0;
}
if (isConnected() && len > 0) {
if (!mesh::enqueueCompanionFrame(send_queue, send_queue_len, FRAME_QUEUE_SIZE,
src, len)) {
BLE_DEBUG_PRINTLN("writeFrame(), send_queue is full!");
return 0;
}
return len;
}
return 0;
}
#define BLE_WRITE_MIN_INTERVAL 60
bool SerialBLEInterface::isReadBusy() const {
return uxQueueMessagesWaiting(recv_queue) > 0;
}
bool SerialBLEInterface::isWriteBusy() const {
return !mesh::bleElapsedAtLeast((uint32_t)millis(), _last_write,
BLE_WRITE_MIN_INTERVAL);
}
size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) {
const uint32_t now = (uint32_t)millis();
servicePendingTxReset();
if (_tx_disconnect_recovery.pending()) {
serviceTxRecovery(now);
return 0;
}
if (send_queue_len > 0 // first, check send queue
&& mesh::bleElapsedAtLeast(now, _last_write,
BLE_WRITE_MIN_INTERVAL) // space the writes apart
) {
const uint16_t peer_mtu = pServer->getPeerMTU(last_conn_id);
#if MESH_BLE_USES_NIMBLE
const bool notifications_ready =
notificationsEnabled.load(std::memory_order_acquire);
#else
const bool notifications_ready =
pTxDescriptor != NULL && pTxDescriptor->getNotifications();
#endif
const bool frame_fits = peer_mtu > 3 && send_queue[0].len <= peer_mtu - 3;
const bool delivery_required = mesh::companionFrameRequiresDelivery(
send_queue[0].buf, send_queue[0].len);
// A fresh pairing can deliver the app's first command before its CCCD
// subscription or MTU exchange completes. Keep the response queued until
// both are ready instead of silently dropping/truncating device info.
if (notifications_ready && frame_fits) {
_last_write = now;
notifySucceeded = false;
pTxCharacteristic->setValue(send_queue[0].buf, send_queue[0].len);
#if defined(MESH_USE_NIMBLE_ARDUINO)
// NimBLE reports queue acceptance directly. Its status callback may run
// later; do not retain or duplicate an already accepted frame awaiting it.
notifySucceeded = pTxCharacteristic->notify(last_conn_id);
#else
pTxCharacteristic->notify();
#endif
if (notifySucceeded) {
BLE_DEBUG_PRINTLN("writeBytes: sz=%d, hdr=%d", (uint32_t)send_queue[0].len, (uint32_t) send_queue[0].buf[0]);
send_queue_len--;
for (int i = 0; i < send_queue_len; i++) { // delete top item from queue
send_queue[i] = send_queue[i + 1];
}
_tx_stall_watchdog.reset();
} else if (delivery_required
&& _tx_stall_watchdog.noteBlocked(now)) {
recoverStalledTx("command reply notification blocked for 10 seconds");
return 0;
}
} else if (delivery_required
&& _tx_stall_watchdog.noteBlocked(now)) {
recoverStalledTx("command reply waiting for notifications or MTU for 10 seconds");
return 0;
} else if (!delivery_required) {
_tx_stall_watchdog.reset();
}
} else if (send_queue_len == 0) {
_tx_stall_watchdog.reset();
}
Frame frame;
if (deviceConnected && xQueueReceive(recv_queue, &frame, 0) == pdTRUE) {
memcpy(dest, frame.buf, frame.len);
BLE_DEBUG_PRINTLN("readBytes: sz=%d, hdr=%d", (uint32_t) frame.len, (uint32_t) dest[0]);
return frame.len;
}
if (deviceConnected != oldDeviceConnected) {
if (!deviceConnected) { // disconnecting
clearBuffers();
BLE_DEBUG_PRINTLN("SerialBLEInterface -> disconnecting...");
//pServer->getAdvertising()->setMinInterval(500);
//pServer->getAdvertising()->setMaxInterval(1000);
if (advertisingAllowed()) scheduleAdvertisingRestart(now);
} else {
BLE_DEBUG_PRINTLN("SerialBLEInterface -> stopping advertising");
BLE_DEBUG_PRINTLN("SerialBLEInterface -> connecting...");
// connecting
// do stuff here on connecting
pServer->getAdvertising()->stop();
_adv_restart_pending = false;
}
oldDeviceConnected = deviceConnected;
}
if (advertisingAllowed() && _adv_restart_pending &&
mesh::bleElapsedAtLeast(now, _adv_restart_started,
ADVERT_RESTART_DELAY)) {
if (pServer->getConnectedCount() == 0) {
BLE_DEBUG_PRINTLN("SerialBLEInterface -> re-starting advertising");
pServer->getAdvertising()->start(); // re-Start advertising
_adv_restart_pending = false;
} else {
// A disconnect can take longer than the normal restart delay. Keep the
// restart armed instead of losing it while the controller still reports
// the old connection.
_adv_restart_started = now;
}
}
return 0;
}
bool SerialBLEInterface::isConnected() const {
return !_tx_disconnect_recovery.pending() && deviceConnected;
}