Files
pyxis/lib/ble_interface/platforms/BluedroidPlatform.cpp
torlando-tech ac6ceca9f8 Initial commit: standalone Pyxis T-Deck firmware
Split T-Deck firmware from microReticulum examples/lxmf_tdeck/ into its
own repo. microReticulum is consumed as a git submodule dependency pinned
to feat/t-deck. All include paths updated from relative symlinks to bare
includes resolved via library build flags.

Both tdeck (NimBLE) and tdeck-bluedroid environments compile successfully.
Licensed under AGPLv3.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 19:48:33 -05:00

1964 lines
67 KiB
C++

/**
* @file BluedroidPlatform.cpp
* @brief ESP-IDF Bluedroid implementation of IBLEPlatform for ESP32
*/
#include "BluedroidPlatform.h"
#if defined(ESP32) && defined(USE_BLUEDROID)
#include "Log.h"
#include "../BLETypes.h"
#include <Arduino.h>
#include <cstring>
namespace RNS { namespace BLE {
// Static singleton instance for callback routing
BluedroidPlatform* BluedroidPlatform::_instance = nullptr;
//=============================================================================
// Constructor / Destructor
//=============================================================================
BluedroidPlatform::BluedroidPlatform() {
DEBUG("BluedroidPlatform: Constructor");
if (_instance != nullptr) {
WARNING("BluedroidPlatform: Another instance exists - callbacks may misbehave");
}
_instance = this;
}
BluedroidPlatform::~BluedroidPlatform() {
DEBUG("BluedroidPlatform: Destructor");
shutdown();
if (_instance == this) {
_instance = nullptr;
}
}
//=============================================================================
// Static Callback Handlers (route to instance methods)
//=============================================================================
void BluedroidPlatform::gapEventHandler(esp_gap_ble_cb_event_t event,
esp_ble_gap_cb_param_t* param) {
if (_instance) {
_instance->handleGapEvent(event, param);
}
}
void BluedroidPlatform::gattsEventHandler(esp_gatts_cb_event_t event,
esp_gatt_if_t gatts_if,
esp_ble_gatts_cb_param_t* param) {
if (_instance) {
_instance->handleGattsEvent(event, gatts_if, param);
}
}
void BluedroidPlatform::gattcEventHandler(esp_gattc_cb_event_t event,
esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t* param) {
if (_instance) {
_instance->handleGattcEvent(event, gattc_if, param);
}
}
//=============================================================================
// Lifecycle
//=============================================================================
bool BluedroidPlatform::initialize(const PlatformConfig& config) {
if (_initialized) {
WARNING("BluedroidPlatform: Already initialized");
return true;
}
INFO("BluedroidPlatform: Initializing Bluedroid BLE stack...");
_config = config;
if (!initBluetooth()) {
ERROR("BluedroidPlatform: Failed to initialize Bluetooth");
return false;
}
_initialized = true;
INFO("BluedroidPlatform: Initialization complete");
return true;
}
bool BluedroidPlatform::initBluetooth() {
esp_err_t ret;
// Release classic BT memory if not needed (saves ~65KB)
ret = esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT);
if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) {
WARNING("BluedroidPlatform: Could not release classic BT memory: " +
std::to_string(ret));
}
// Initialize BT controller
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
ret = esp_bt_controller_init(&bt_cfg);
if (ret != ESP_OK) {
ERROR("BluedroidPlatform: Controller init failed: " + std::to_string(ret));
return false;
}
_init_state = InitState::CONTROLLER_INIT;
// Enable BT controller in BLE-only mode
ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);
if (ret != ESP_OK) {
ERROR("BluedroidPlatform: Controller enable failed: " + std::to_string(ret));
return false;
}
// Initialize Bluedroid
ret = esp_bluedroid_init();
if (ret != ESP_OK) {
ERROR("BluedroidPlatform: Bluedroid init failed: " + std::to_string(ret));
return false;
}
_init_state = InitState::BLUEDROID_INIT;
// Enable Bluedroid
ret = esp_bluedroid_enable();
if (ret != ESP_OK) {
ERROR("BluedroidPlatform: Bluedroid enable failed: " + std::to_string(ret));
return false;
}
// Register callbacks
ret = esp_ble_gap_register_callback(gapEventHandler);
if (ret != ESP_OK) {
ERROR("BluedroidPlatform: GAP callback register failed: " + std::to_string(ret));
return false;
}
ret = esp_ble_gatts_register_callback(gattsEventHandler);
if (ret != ESP_OK) {
ERROR("BluedroidPlatform: GATTS callback register failed: " + std::to_string(ret));
return false;
}
ret = esp_ble_gattc_register_callback(gattcEventHandler);
if (ret != ESP_OK) {
ERROR("BluedroidPlatform: GATTC callback register failed: " + std::to_string(ret));
return false;
}
_init_state = InitState::CALLBACKS_REGISTERED;
// Set device name
ret = esp_ble_gap_set_device_name(_config.device_name.c_str());
if (ret != ESP_OK) {
WARNING("BluedroidPlatform: Set device name failed: " + std::to_string(ret));
}
// Set local MTU
ret = esp_ble_gatt_set_local_mtu(_config.preferred_mtu);
if (ret != ESP_OK) {
WARNING("BluedroidPlatform: Set local MTU failed: " + std::to_string(ret));
}
// Register GATTS app for peripheral mode
if (_config.role == Role::PERIPHERAL || _config.role == Role::DUAL) {
ret = esp_ble_gatts_app_register(GATTS_APP_ID);
if (ret != ESP_OK) {
ERROR("BluedroidPlatform: GATTS app register failed: " + std::to_string(ret));
return false;
}
_init_state = InitState::GATTS_REGISTERING;
DEBUG("BluedroidPlatform: GATTS app registration pending...");
}
// Register GATTC app for central mode
if (_config.role == Role::CENTRAL || _config.role == Role::DUAL) {
ret = esp_ble_gattc_app_register(GATTC_APP_ID);
if (ret != ESP_OK) {
ERROR("BluedroidPlatform: GATTC app register failed: " + std::to_string(ret));
return false;
}
DEBUG("BluedroidPlatform: GATTC app registration pending...");
}
return true;
}
bool BluedroidPlatform::start() {
if (!_initialized) {
ERROR("BluedroidPlatform: Cannot start - not initialized");
return false;
}
if (_running) {
return true;
}
INFO("BluedroidPlatform: Starting BLE operations");
// Reset state to ensure clean startup
_connect_pending = false;
_connect_success = false;
_connect_error = 0;
_connect_start_time = 0;
_connect_timeout_ms = 10000;
_discovery_in_progress = 0xFFFF;
while (!_pending_discoveries.empty()) {
_pending_discoveries.pop();
}
// Start advertising if in peripheral or dual mode
if (_config.role == Role::PERIPHERAL || _config.role == Role::DUAL) {
if (_init_state == InitState::READY) {
startAdvertising();
} else {
DEBUG("BluedroidPlatform: Waiting for GATTS service ready before advertising");
}
}
_running = true;
return true;
}
void BluedroidPlatform::stop() {
if (!_running) return;
INFO("BluedroidPlatform: Stopping BLE operations");
stopScan();
stopAdvertising();
disconnectAll();
_running = false;
}
void BluedroidPlatform::loop() {
if (!_running) return;
// Process operation queue
process();
// Check scan timeout
if (_scan_state == ScanState::ACTIVE && _scan_duration_ms > 0) {
if (millis() - _scan_start_time >= _scan_duration_ms) {
stopScan();
}
}
// Check connection timeout - allows retrying other peers while BLE stack
// continues its internal connection attempt (which can take 30+ seconds)
if (_connect_pending && _connect_timeout_ms > 0) {
if (millis() - _connect_start_time >= _connect_timeout_ms) {
WARNING("BluedroidPlatform: Connection timed out to " + _pending_connect_address.toString());
_connect_pending = false;
// Note: The BLE stack may still complete the connection later,
// which will be handled normally in handleGattcConnect()
}
}
}
void BluedroidPlatform::shutdown() {
INFO("BluedroidPlatform: Shutting down");
stop();
if (_initialized) {
esp_bluedroid_disable();
esp_bluedroid_deinit();
esp_bt_controller_disable();
esp_bt_controller_deinit();
}
_initialized = false;
_init_state = InitState::UNINITIALIZED;
_gatts_if = ESP_GATT_IF_NONE;
_gattc_if = ESP_GATT_IF_NONE;
}
bool BluedroidPlatform::isRunning() const {
return _running && _init_state == InitState::READY;
}
//=============================================================================
// Scanning (Central Mode)
//=============================================================================
bool BluedroidPlatform::startScan(uint16_t duration_ms) {
if (_scan_state != ScanState::IDLE) {
DEBUG("BluedroidPlatform: Scan already in progress");
return false;
}
// Skip scanning if we've reached connection limit - saves memory from scan results
if (getConnectionCount() >= _config.max_connections) {
TRACE("BluedroidPlatform: Skipping scan - at max connections (" +
std::to_string(getConnectionCount()) + "/" +
std::to_string(_config.max_connections) + ")");
return false;
}
// In dual mode, stop advertising before scanning
if (_adv_state == AdvState::ACTIVE) {
DEBUG("BluedroidPlatform: Stopping advertising for scan");
stopAdvertising();
taskYIELD(); // Allow stack to process
}
esp_ble_scan_params_t scan_params = {
.scan_type = (_config.scan_mode == ScanMode::ACTIVE) ?
BLE_SCAN_TYPE_ACTIVE : BLE_SCAN_TYPE_PASSIVE,
.own_addr_type = BLE_ADDR_TYPE_PUBLIC, // Use public address (chip MAC)
.scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL,
.scan_interval = static_cast<uint16_t>((_config.scan_interval_ms * 1000) / 625),
.scan_window = static_cast<uint16_t>((_config.scan_window_ms * 1000) / 625),
.scan_duplicate = BLE_SCAN_DUPLICATE_ENABLE
};
esp_err_t ret = esp_ble_gap_set_scan_params(&scan_params);
if (ret != ESP_OK) {
ERROR("BluedroidPlatform: Set scan params failed: " + std::to_string(ret));
return false;
}
_scan_duration_ms = duration_ms;
_scan_state = ScanState::SETTING_PARAMS;
DEBUG("BluedroidPlatform: Scan params set, waiting for confirmation...");
return true;
}
void BluedroidPlatform::stopScan() {
if (_scan_state == ScanState::IDLE) return;
esp_err_t ret = esp_ble_gap_stop_scanning();
if (ret != ESP_OK) {
WARNING("BluedroidPlatform: Stop scan failed: " + std::to_string(ret));
}
_scan_state = ScanState::STOPPING;
}
bool BluedroidPlatform::isScanning() const {
return _scan_state == ScanState::ACTIVE;
}
//=============================================================================
// Advertising (Peripheral Mode)
//=============================================================================
bool BluedroidPlatform::startAdvertising() {
if (_adv_state != AdvState::IDLE) {
DEBUG("BluedroidPlatform: Advertising already active or starting");
return false;
}
if (_init_state != InitState::READY) {
WARNING("BluedroidPlatform: Cannot advertise - GATTS not ready");
return false;
}
// Skip advertising if at max connections - no point accepting more
if (getConnectionCount() >= _config.max_connections) {
TRACE("BluedroidPlatform: Skipping advertising - at max connections (" +
std::to_string(getConnectionCount()) + "/" +
std::to_string(_config.max_connections) + ")");
return false;
}
DEBUG("BluedroidPlatform: Starting advertising");
buildAdvertisingData();
_adv_state = AdvState::CONFIGURING_DATA;
return true;
}
void BluedroidPlatform::buildAdvertisingData() {
// Build advertising data with service UUID
esp_ble_adv_data_t adv_data = {
.set_scan_rsp = false,
.include_name = true,
.include_txpower = true,
.min_interval = static_cast<uint16_t>((_config.adv_interval_min_ms * 1000) / 625),
.max_interval = static_cast<uint16_t>((_config.adv_interval_max_ms * 1000) / 625),
.appearance = 0x0000,
.manufacturer_len = 0,
.p_manufacturer_data = nullptr,
.service_data_len = 0,
.p_service_data = nullptr,
.service_uuid_len = 16,
.p_service_uuid = nullptr, // Will set below
.flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT)
};
// Parse service UUID from string to bytes
static uint8_t service_uuid_bytes[16];
// UUID::SERVICE = "37145b00-442d-4a94-917f-8f42c5da28e3"
// UUID is stored in little-endian in advertising data
const char* uuid_str = UUID::SERVICE;
int idx = 15; // Start from end (little-endian)
for (int i = 0; i < 36 && idx >= 0; i++) {
if (uuid_str[i] == '-') continue;
unsigned int byte_val;
sscanf(&uuid_str[i], "%02x", &byte_val);
service_uuid_bytes[idx--] = static_cast<uint8_t>(byte_val);
i++; // Skip second hex digit
}
adv_data.p_service_uuid = service_uuid_bytes;
esp_err_t ret = esp_ble_gap_config_adv_data(&adv_data);
if (ret != ESP_OK) {
ERROR("BluedroidPlatform: Config adv data failed: " + std::to_string(ret));
_adv_state = AdvState::IDLE;
}
}
void BluedroidPlatform::buildScanResponseData() {
esp_ble_adv_data_t scan_rsp_data = {
.set_scan_rsp = true,
.include_name = true,
.include_txpower = false,
.min_interval = 0,
.max_interval = 0,
.appearance = 0x0000,
.manufacturer_len = 0,
.p_manufacturer_data = nullptr,
.service_data_len = 0,
.p_service_data = nullptr,
.service_uuid_len = 0,
.p_service_uuid = nullptr,
.flag = 0
};
esp_err_t ret = esp_ble_gap_config_adv_data(&scan_rsp_data);
if (ret != ESP_OK) {
ERROR("BluedroidPlatform: Config scan response failed: " + std::to_string(ret));
_adv_state = AdvState::IDLE;
}
}
void BluedroidPlatform::stopAdvertising() {
if (_adv_state == AdvState::IDLE) return;
esp_err_t ret = esp_ble_gap_stop_advertising();
if (ret != ESP_OK) {
WARNING("BluedroidPlatform: Stop advertising failed: " + std::to_string(ret));
}
_adv_state = AdvState::STOPPING;
}
bool BluedroidPlatform::isAdvertising() const {
return _adv_state == AdvState::ACTIVE;
}
bool BluedroidPlatform::setAdvertisingData(const Bytes& data) {
_custom_adv_data = data;
return true;
}
void BluedroidPlatform::setIdentityData(const Bytes& identity) {
_identity_data = identity;
DEBUG("BluedroidPlatform: Identity data set (" + std::to_string(identity.size()) + " bytes)");
}
//=============================================================================
// Connection Management
//=============================================================================
bool BluedroidPlatform::connect(const BLEAddress& address, uint16_t timeout_ms) {
// Check connection limit first - don't connect if at max
if (getConnectionCount() >= _config.max_connections) {
TRACE("BluedroidPlatform: Skipping connect - at max connections (" +
std::to_string(getConnectionCount()) + "/" +
std::to_string(_config.max_connections) + ")");
return false;
}
// Protect against connecting when memory is critically low
if (ESP.getFreeHeap() < 40000) {
static uint32_t last_low_mem_warn = 0;
if (millis() - last_low_mem_warn > 10000) {
WARNING("BluedroidPlatform: Skipping connection - low memory (" +
std::to_string(ESP.getFreeHeap()) + " bytes free)");
last_low_mem_warn = millis();
}
return false;
}
if (_gattc_if == ESP_GATT_IF_NONE) {
ERROR("BluedroidPlatform: GATTC not registered");
return false;
}
if (_connect_pending) {
WARNING("BluedroidPlatform: Connection already pending");
return false;
}
// Bluedroid can't handle connection while service discovery is in progress
// This causes hash_map_set assertion failures in btu_start_timer
if (_discovery_in_progress != 0xFFFF) {
WARNING("BluedroidPlatform: Cannot connect while discovery in progress for " +
std::to_string(_discovery_in_progress));
return false;
}
esp_bd_addr_t peer_addr;
toEspBdAddr(address, peer_addr);
_connect_pending = true;
_connect_success = false;
_connect_error = 0;
_pending_connect_address = address;
_connect_start_time = millis();
_connect_timeout_ms = timeout_ms;
esp_err_t ret = esp_ble_gattc_open(
_gattc_if,
peer_addr,
static_cast<esp_ble_addr_type_t>(address.type),
true // Direct connection
);
if (ret != ESP_OK) {
ERROR("BluedroidPlatform: GATTC open failed: " + std::to_string(ret));
_connect_pending = false;
return false;
}
DEBUG("BluedroidPlatform: Connection initiated to " + address.toString());
return true;
}
bool BluedroidPlatform::disconnect(uint16_t conn_handle) {
auto* conn = findConnection(conn_handle);
if (!conn) {
WARNING("BluedroidPlatform: Connection not found: " + std::to_string(conn_handle));
return false;
}
esp_err_t ret;
if (conn->local_role == Role::PERIPHERAL) {
ret = esp_ble_gatts_close(_gatts_if, conn->conn_id);
} else {
ret = esp_ble_gattc_close(_gattc_if, conn->conn_id);
}
if (ret != ESP_OK) {
WARNING("BluedroidPlatform: Disconnect failed: " + std::to_string(ret));
return false;
}
return true;
}
void BluedroidPlatform::disconnectAll() {
for (auto& pair : _connections) {
disconnect(pair.first);
}
}
bool BluedroidPlatform::requestMTU(uint16_t conn_handle, uint16_t mtu) {
auto* conn = findConnection(conn_handle);
if (!conn) return false;
if (conn->local_role == Role::CENTRAL) {
esp_err_t ret = esp_ble_gattc_send_mtu_req(_gattc_if, conn->conn_id);
return ret == ESP_OK;
}
// Peripheral mode MTU is negotiated by central
return true;
}
bool BluedroidPlatform::discoverServices(uint16_t conn_handle) {
DEBUG("BluedroidPlatform: discoverServices called for handle " + std::to_string(conn_handle));
auto* conn = findConnection(conn_handle);
if (!conn) {
ERROR("BluedroidPlatform: discoverServices - connection not found for handle " + std::to_string(conn_handle));
return false;
}
// Bluedroid can only handle one GATT operation at a time
// Queue if connection is pending or another discovery is in progress
if (_connect_pending || _discovery_in_progress != 0xFFFF) {
INFO("BluedroidPlatform: GATT busy (connect_pending=" + std::string(_connect_pending ? "yes" : "no") +
" discovery=" + std::to_string(_discovery_in_progress) + "), queueing " + std::to_string(conn_handle));
_pending_discoveries.push(conn_handle);
return true; // Queued successfully
}
DEBUG("BluedroidPlatform: Found connection, conn_id=" + std::to_string(conn->conn_id));
// Parse service UUID for filter
esp_bt_uuid_t service_uuid;
service_uuid.len = ESP_UUID_LEN_128;
// Convert UUID string to bytes (little-endian)
const char* uuid_str = UUID::SERVICE;
int idx = 15;
for (int i = 0; i < 36 && idx >= 0; i++) {
if (uuid_str[i] == '-') continue;
unsigned int byte_val;
sscanf(&uuid_str[i], "%02x", &byte_val);
service_uuid.uuid.uuid128[idx--] = static_cast<uint8_t>(byte_val);
i++;
}
_discovery_in_progress = conn_handle;
conn->discovery_state = BluedroidConnection::DiscoveryState::SEARCHING_SERVICE;
esp_err_t ret = esp_ble_gattc_search_service(_gattc_if, conn->conn_id, &service_uuid);
if (ret != ESP_OK) {
ERROR("BluedroidPlatform: Service search failed: " + std::to_string(ret));
_discovery_in_progress = 0xFFFF;
return false;
}
DEBUG("BluedroidPlatform: Service search started for conn_id=" + std::to_string(conn->conn_id));
return true;
}
//=============================================================================
// GATT Operations
//=============================================================================
bool BluedroidPlatform::write(uint16_t conn_handle, const Bytes& data, bool response) {
auto* conn = findConnection(conn_handle);
if (!conn) {
WARNING("BluedroidPlatform: write - connection not found for handle " + std::to_string(conn_handle));
return false;
}
if (conn->rx_char_handle == 0) {
ERROR("BluedroidPlatform: RX characteristic not discovered for conn " + std::to_string(conn_handle) +
" conn_id=" + std::to_string(conn->conn_id) +
" discovery_state=" + std::to_string(static_cast<int>(conn->discovery_state)));
return false;
}
// Hot path - no logging to avoid heap allocation
esp_err_t ret = esp_ble_gattc_write_char(
_gattc_if,
conn->conn_id,
conn->rx_char_handle,
data.size(),
const_cast<uint8_t*>(data.data()),
response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP,
ESP_GATT_AUTH_REQ_NONE
);
// Yield to let BLE stack process
taskYIELD();
if (ret != ESP_OK) {
WARNING("BluedroidPlatform: Write failed with err " + std::to_string(ret));
}
return ret == ESP_OK;
}
bool BluedroidPlatform::read(uint16_t conn_handle, uint16_t char_handle,
std::function<void(OperationResult, const Bytes&)> callback) {
auto* conn = findConnection(conn_handle);
if (!conn) {
if (callback) callback(OperationResult::DISCONNECTED, Bytes());
return false;
}
// Queue the operation
GATTOperation op;
op.type = OperationType::READ;
op.conn_handle = conn_handle;
op.char_handle = char_handle;
op.callback = callback;
enqueue(op);
return true;
}
bool BluedroidPlatform::enableNotifications(uint16_t conn_handle, bool enable) {
auto* conn = findConnection(conn_handle);
if (!conn) {
WARNING("BluedroidPlatform: enableNotifications - connection not found");
return false;
}
if (conn->tx_char_handle == 0) {
ERROR("BluedroidPlatform: TX char not discovered for conn " + std::to_string(conn_handle));
return false;
}
INFO("BluedroidPlatform: Enabling notifications for conn " + std::to_string(conn_handle) +
" conn_id=" + std::to_string(conn->conn_id) + " tx_char=" + std::to_string(conn->tx_char_handle));
// Step 1: Register for notifications with the Bluedroid stack
// This tells ESP-IDF to route ESP_GATTC_NOTIFY_EVT to our handler
esp_err_t ret = esp_ble_gattc_register_for_notify(
_gattc_if,
conn->peer_addr,
conn->tx_char_handle // The characteristic we want notifications from
);
if (ret != ESP_OK) {
WARNING("BluedroidPlatform: Failed to register for notify: " + std::to_string(ret));
return false;
}
INFO("BluedroidPlatform: Registered for notify OK");
// Step 2: Write to CCCD to enable notifications on the peripheral
if (conn->tx_cccd_handle != 0) {
DEBUG("BluedroidPlatform: Enabling notifications on CCCD handle " +
std::to_string(conn->tx_cccd_handle) + " for conn " + std::to_string(conn_handle));
uint16_t cccd_value = enable ? 0x0001 : 0x0000;
ret = esp_ble_gattc_write_char_descr(
_gattc_if,
conn->conn_id,
conn->tx_cccd_handle,
sizeof(cccd_value),
reinterpret_cast<uint8_t*>(&cccd_value),
ESP_GATT_WRITE_TYPE_RSP,
ESP_GATT_AUTH_REQ_NONE
);
}
DEBUG("BluedroidPlatform: Notifications " + std::string(enable ? "enabled" : "disabled") +
" for conn " + std::to_string(conn_handle));
// Yield to let Bluedroid process
taskYIELD();
return ret == ESP_OK;
}
bool BluedroidPlatform::notify(uint16_t conn_handle, const Bytes& data) {
if (_gatts_if == ESP_GATT_IF_NONE || _tx_char_handle == 0) {
return false;
}
auto* conn = findConnection(conn_handle);
if (!conn) return false;
esp_err_t ret = esp_ble_gatts_send_indicate(
_gatts_if,
conn->conn_id,
_tx_char_handle,
data.size(),
const_cast<uint8_t*>(data.data()),
false // false = notification, true = indication
);
return ret == ESP_OK;
}
bool BluedroidPlatform::notifyAll(const Bytes& data) {
if (_gatts_if == ESP_GATT_IF_NONE || _tx_char_handle == 0) {
return false;
}
bool any_sent = false;
for (auto& pair : _connections) {
if (pair.second.local_role == Role::PERIPHERAL &&
pair.second.notifications_enabled) {
if (notify(pair.first, data)) {
any_sent = true;
}
}
}
return any_sent;
}
bool BluedroidPlatform::executeOperation(const GATTOperation& op) {
auto* conn = findConnection(op.conn_handle);
if (!conn) {
if (op.callback) op.callback(OperationResult::DISCONNECTED, Bytes());
return true; // Operation complete (failed)
}
switch (op.type) {
case OperationType::READ: {
esp_err_t ret = esp_ble_gattc_read_char(
_gattc_if,
conn->conn_id,
op.char_handle,
ESP_GATT_AUTH_REQ_NONE
);
if (ret != ESP_OK) {
WARNING("BluedroidPlatform: Read char failed: " + std::to_string(ret));
return false; // Failed to start
}
return true; // Successfully started, will complete async
}
default:
WARNING("BluedroidPlatform: Unknown operation type");
return false; // Unknown operation type - failed to start
}
}
//=============================================================================
// Connection Query
//=============================================================================
std::vector<ConnectionHandle> BluedroidPlatform::getConnections() const {
std::vector<ConnectionHandle> result;
for (const auto& pair : _connections) {
ConnectionHandle handle;
handle.handle = pair.first;
handle.peer_address = fromEspBdAddr(pair.second.peer_addr, pair.second.addr_type);
handle.local_role = pair.second.local_role;
handle.state = ConnectionState::READY;
handle.mtu = pair.second.mtu;
result.push_back(handle);
}
return result;
}
ConnectionHandle BluedroidPlatform::getConnection(uint16_t handle) const {
auto it = _connections.find(handle);
if (it == _connections.end()) {
return ConnectionHandle();
}
ConnectionHandle result;
result.handle = handle;
result.peer_address = fromEspBdAddr(it->second.peer_addr, it->second.addr_type);
result.local_role = it->second.local_role;
result.state = ConnectionState::READY;
result.mtu = it->second.mtu;
result.rx_char_handle = it->second.rx_char_handle;
result.tx_char_handle = it->second.tx_char_handle;
result.tx_cccd_handle = it->second.tx_cccd_handle;
result.identity_handle = it->second.identity_char_handle;
return result;
}
size_t BluedroidPlatform::getConnectionCount() const {
return _connections.size();
}
bool BluedroidPlatform::isConnectedTo(const BLEAddress& address) const {
esp_bd_addr_t esp_addr;
toEspBdAddr(address, esp_addr);
for (const auto& pair : _connections) {
if (memcmp(pair.second.peer_addr, esp_addr, 6) == 0) {
return true;
}
}
return false;
}
//=============================================================================
// Callback Registration
//=============================================================================
void BluedroidPlatform::setOnScanResult(Callbacks::OnScanResult callback) {
_on_scan_result = callback;
}
void BluedroidPlatform::setOnScanComplete(Callbacks::OnScanComplete callback) {
_on_scan_complete = callback;
}
void BluedroidPlatform::setOnConnected(Callbacks::OnConnected callback) {
_on_connected = callback;
}
void BluedroidPlatform::setOnDisconnected(Callbacks::OnDisconnected callback) {
_on_disconnected = callback;
}
void BluedroidPlatform::setOnMTUChanged(Callbacks::OnMTUChanged callback) {
_on_mtu_changed = callback;
}
void BluedroidPlatform::setOnServicesDiscovered(Callbacks::OnServicesDiscovered callback) {
_on_services_discovered = callback;
}
void BluedroidPlatform::setOnDataReceived(Callbacks::OnDataReceived callback) {
_on_data_received = callback;
}
void BluedroidPlatform::setOnNotifyEnabled(Callbacks::OnNotifyEnabled callback) {
_on_notify_enabled = callback;
}
void BluedroidPlatform::setOnCentralConnected(Callbacks::OnCentralConnected callback) {
_on_central_connected = callback;
}
void BluedroidPlatform::setOnCentralDisconnected(Callbacks::OnCentralDisconnected callback) {
_on_central_disconnected = callback;
}
void BluedroidPlatform::setOnWriteReceived(Callbacks::OnWriteReceived callback) {
_on_write_received = callback;
}
void BluedroidPlatform::setOnReadRequested(Callbacks::OnReadRequested callback) {
_on_read_requested = callback;
}
//=============================================================================
// Platform Info
//=============================================================================
BLEAddress BluedroidPlatform::getLocalAddress() const {
if (!_local_addr_valid) {
// Get local address from GAP
uint8_t addr[6];
uint8_t addr_type = 0;
if (esp_ble_gap_get_local_used_addr(addr, &addr_type) == ESP_OK) {
memcpy(_local_addr, addr, 6);
_local_addr_valid = true;
}
// If still not valid, address will be retrieved on first advertising start
}
return fromEspBdAddr(_local_addr, BLE_ADDR_TYPE_PUBLIC);
}
//=============================================================================
// GAP Event Handler
//=============================================================================
void BluedroidPlatform::handleGapEvent(esp_gap_ble_cb_event_t event,
esp_ble_gap_cb_param_t* param) {
switch (event) {
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
if (param->scan_param_cmpl.status == ESP_BT_STATUS_SUCCESS) {
DEBUG("BluedroidPlatform: Scan params set, starting scan");
_scan_start_time = millis();
uint32_t duration_sec = (_scan_duration_ms > 0) ?
(_scan_duration_ms / 1000) : 0; // 0 = continuous
esp_ble_gap_start_scanning(duration_sec);
_scan_state = ScanState::STARTING;
} else {
ERROR("BluedroidPlatform: Scan param set failed");
_scan_state = ScanState::IDLE;
}
break;
case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
if (param->scan_start_cmpl.status == ESP_BT_STATUS_SUCCESS) {
DEBUG("BluedroidPlatform: Scan started");
_scan_state = ScanState::ACTIVE;
} else {
ERROR("BluedroidPlatform: Scan start failed");
_scan_state = ScanState::IDLE;
}
break;
case ESP_GAP_BLE_SCAN_RESULT_EVT:
handleScanResult(param);
break;
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
DEBUG("BluedroidPlatform: Scan stopped");
_scan_state = ScanState::IDLE;
handleScanComplete();
break;
case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
if (param->adv_data_cmpl.status == ESP_BT_STATUS_SUCCESS) {
DEBUG("BluedroidPlatform: Adv data set");
_adv_state = AdvState::CONFIGURING_SCAN_RSP;
buildScanResponseData();
} else {
ERROR("BluedroidPlatform: Adv data set failed");
_adv_state = AdvState::IDLE;
}
break;
case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
if (param->scan_rsp_data_cmpl.status == ESP_BT_STATUS_SUCCESS) {
DEBUG("BluedroidPlatform: Scan response set, starting advertising");
_adv_state = AdvState::STARTING;
esp_ble_adv_params_t adv_params = {
.adv_int_min = static_cast<uint16_t>((_config.adv_interval_min_ms * 1000) / 625),
.adv_int_max = static_cast<uint16_t>((_config.adv_interval_max_ms * 1000) / 625),
.adv_type = ADV_TYPE_IND,
.own_addr_type = BLE_ADDR_TYPE_PUBLIC, // Use public address (chip MAC)
.peer_addr = {0},
.peer_addr_type = BLE_ADDR_TYPE_PUBLIC,
.channel_map = ADV_CHNL_ALL,
.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY
};
esp_ble_gap_start_advertising(&adv_params);
} else {
ERROR("BluedroidPlatform: Scan response set failed");
_adv_state = AdvState::IDLE;
}
break;
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
handleAdvStart(param->adv_start_cmpl.status);
break;
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
handleAdvStop();
break;
case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT:
DEBUG("BluedroidPlatform: Connection params updated");
break;
default:
break;
}
}
void BluedroidPlatform::handleScanResult(esp_ble_gap_cb_param_t* param) {
if (param->scan_rst.search_evt != ESP_GAP_SEARCH_INQ_RES_EVT) {
if (param->scan_rst.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) {
// Scan duration complete
_scan_state = ScanState::IDLE;
handleScanComplete();
}
return;
}
// Parse advertising data for service UUID
uint8_t* adv_data = param->scan_rst.ble_adv;
uint8_t adv_len = param->scan_rst.adv_data_len;
// Look for 128-bit service UUID
bool has_service = false;
uint8_t service_uuid_bytes[16];
const char* uuid_str = UUID::SERVICE;
int idx = 15;
for (int i = 0; i < 36 && idx >= 0; i++) {
if (uuid_str[i] == '-') continue;
unsigned int byte_val;
sscanf(&uuid_str[i], "%02x", &byte_val);
service_uuid_bytes[idx--] = static_cast<uint8_t>(byte_val);
i++;
}
// Parse AD structures
int pos = 0;
while (pos < adv_len) {
uint8_t len = adv_data[pos++];
if (len == 0 || pos + len > adv_len) break;
uint8_t type = adv_data[pos];
if (type == ESP_BLE_AD_TYPE_128SRV_CMPL ||
type == ESP_BLE_AD_TYPE_128SRV_PART) {
// Check if UUID matches
if (len >= 17 &&
memcmp(&adv_data[pos + 1], service_uuid_bytes, 16) == 0) {
has_service = true;
}
}
pos += len;
}
// Build scan result
ScanResult result;
result.address = fromEspBdAddr(param->scan_rst.bda, param->scan_rst.ble_addr_type);
result.rssi = param->scan_rst.rssi;
result.connectable = (param->scan_rst.ble_evt_type == ESP_BLE_EVT_CONN_ADV);
result.has_reticulum_service = has_service;
// Get device name if present
uint8_t* name = esp_ble_resolve_adv_data(adv_data, ESP_BLE_AD_TYPE_NAME_CMPL, &adv_len);
if (name && adv_len > 0) {
result.name = std::string(reinterpret_cast<char*>(name), adv_len);
}
if (_on_scan_result && result.has_reticulum_service) {
_on_scan_result(result);
}
}
void BluedroidPlatform::handleScanComplete() {
if (_on_scan_complete) {
_on_scan_complete();
}
// In dual mode, restart advertising after scan
if (_config.role == Role::DUAL && _init_state == InitState::READY) {
startAdvertising();
}
}
void BluedroidPlatform::handleAdvStart(esp_bt_status_t status) {
if (status == ESP_BT_STATUS_SUCCESS) {
INFO("BluedroidPlatform: Advertising started");
_adv_state = AdvState::ACTIVE;
} else {
ERROR("BluedroidPlatform: Advertising start failed: " + std::to_string(status));
_adv_state = AdvState::IDLE;
}
}
void BluedroidPlatform::handleAdvStop() {
DEBUG("BluedroidPlatform: Advertising stopped");
_adv_state = AdvState::IDLE;
}
//=============================================================================
// GATTS Event Handler
//=============================================================================
void BluedroidPlatform::handleGattsEvent(esp_gatts_cb_event_t event,
esp_gatt_if_t gatts_if,
esp_ble_gatts_cb_param_t* param) {
switch (event) {
case ESP_GATTS_REG_EVT:
handleGattsRegister(gatts_if, param->reg.status);
break;
case ESP_GATTS_CREATE_EVT:
handleGattsServiceCreated(param->create.service_handle);
break;
case ESP_GATTS_ADD_CHAR_EVT:
handleGattsCharAdded(param->add_char.attr_handle, &param->add_char.char_uuid);
break;
case ESP_GATTS_START_EVT:
handleGattsServiceStarted();
break;
case ESP_GATTS_CONNECT_EVT:
handleGattsConnect(param);
break;
case ESP_GATTS_DISCONNECT_EVT:
handleGattsDisconnect(param);
break;
case ESP_GATTS_WRITE_EVT:
handleGattsWrite(param);
break;
case ESP_GATTS_READ_EVT:
handleGattsRead(param);
break;
case ESP_GATTS_MTU_EVT:
handleGattsMtuChange(param);
break;
case ESP_GATTS_CONF_EVT:
handleGattsConfirm(param);
break;
default:
break;
}
}
void BluedroidPlatform::handleGattsRegister(esp_gatt_if_t gatts_if, esp_gatt_status_t status) {
if (status != ESP_GATT_OK) {
ERROR("BluedroidPlatform: GATTS register failed: " + std::to_string(status));
return;
}
_gatts_if = gatts_if;
DEBUG("BluedroidPlatform: GATTS registered, if=" + std::to_string(gatts_if));
// Create the Reticulum service
setupGattsService();
}
bool BluedroidPlatform::setupGattsService() {
// Parse service UUID
esp_gatt_srvc_id_t service_id;
service_id.is_primary = true;
service_id.id.inst_id = 0;
service_id.id.uuid.len = ESP_UUID_LEN_128;
const char* uuid_str = UUID::SERVICE;
int idx = 15;
for (int i = 0; i < 36 && idx >= 0; i++) {
if (uuid_str[i] == '-') continue;
unsigned int byte_val;
sscanf(&uuid_str[i], "%02x", &byte_val);
service_id.id.uuid.uuid.uuid128[idx--] = static_cast<uint8_t>(byte_val);
i++;
}
// Calculate number of handles needed:
// 1 for service, 2 per characteristic (decl + value), 1 for CCCD on TX
// Service + RX(2) + TX(2+1 CCCD) + Identity(2) = 8 handles
esp_err_t ret = esp_ble_gatts_create_service(_gatts_if, &service_id, 10);
if (ret != ESP_OK) {
ERROR("BluedroidPlatform: Create service failed: " + std::to_string(ret));
return false;
}
_init_state = InitState::GATTS_CREATING_SERVICE;
return true;
}
void BluedroidPlatform::handleGattsServiceCreated(uint16_t service_handle) {
_service_handle = service_handle;
DEBUG("BluedroidPlatform: Service created, handle=" + std::to_string(service_handle));
_init_state = InitState::GATTS_ADDING_CHARS;
_chars_added = 0;
// Helper to parse UUID string to esp_bt_uuid_t
auto parseUuid = [](const char* uuid_str, esp_bt_uuid_t& uuid) {
uuid.len = ESP_UUID_LEN_128;
int idx = 15;
for (int i = 0; i < 36 && idx >= 0; i++) {
if (uuid_str[i] == '-') continue;
unsigned int byte_val;
sscanf(&uuid_str[i], "%02x", &byte_val);
uuid.uuid.uuid128[idx--] = static_cast<uint8_t>(byte_val);
i++;
}
};
// Add RX characteristic (central writes here)
esp_bt_uuid_t rx_uuid;
parseUuid(UUID::RX_CHAR, rx_uuid);
esp_err_t ret = esp_ble_gatts_add_char(
_service_handle,
&rx_uuid,
ESP_GATT_PERM_WRITE,
ESP_GATT_CHAR_PROP_BIT_WRITE | ESP_GATT_CHAR_PROP_BIT_WRITE_NR,
nullptr,
nullptr
);
if (ret != ESP_OK) {
ERROR("BluedroidPlatform: Add RX char failed: " + std::to_string(ret));
}
// Add TX characteristic (we notify from here)
esp_bt_uuid_t tx_uuid;
parseUuid(UUID::TX_CHAR, tx_uuid);
ret = esp_ble_gatts_add_char(
_service_handle,
&tx_uuid,
ESP_GATT_PERM_READ,
ESP_GATT_CHAR_PROP_BIT_NOTIFY,
nullptr,
nullptr
);
if (ret != ESP_OK) {
ERROR("BluedroidPlatform: Add TX char failed: " + std::to_string(ret));
}
// Add Identity characteristic (central reads)
esp_bt_uuid_t id_uuid;
parseUuid(UUID::IDENTITY_CHAR, id_uuid);
ret = esp_ble_gatts_add_char(
_service_handle,
&id_uuid,
ESP_GATT_PERM_READ,
ESP_GATT_CHAR_PROP_BIT_READ,
nullptr,
nullptr
);
if (ret != ESP_OK) {
ERROR("BluedroidPlatform: Add Identity char failed: " + std::to_string(ret));
}
}
void BluedroidPlatform::handleGattsCharAdded(uint16_t attr_handle, esp_bt_uuid_t* char_uuid) {
// Determine which characteristic was added by comparing UUID
// For simplicity, we track by order of addition
_chars_added++;
DEBUG("BluedroidPlatform: Char added, handle=" + std::to_string(attr_handle) +
" (" + std::to_string(_chars_added) + "/" + std::to_string(CHARS_EXPECTED) + ")");
switch (_chars_added) {
case 1:
_rx_char_handle = attr_handle;
break;
case 2:
_tx_char_handle = attr_handle;
// TX needs CCCD for notifications - will be auto-added by stack
_tx_cccd_handle = attr_handle + 1; // CCCD is typically handle+1
break;
case 3:
_identity_char_handle = attr_handle;
break;
}
if (_chars_added >= CHARS_EXPECTED) {
// All characteristics added, start service
_init_state = InitState::GATTS_STARTING_SERVICE;
esp_err_t ret = esp_ble_gatts_start_service(_service_handle);
if (ret != ESP_OK) {
ERROR("BluedroidPlatform: Start service failed: " + std::to_string(ret));
}
}
}
void BluedroidPlatform::handleGattsServiceStarted() {
INFO("BluedroidPlatform: GATTS service started");
// Check if GATTC is also ready (for dual mode)
if (_config.role == Role::PERIPHERAL ||
(_config.role == Role::DUAL && _gattc_if != ESP_GATT_IF_NONE)) {
_init_state = InitState::READY;
INFO("BluedroidPlatform: Ready for connections");
// Start advertising if running
if (_running) {
startAdvertising();
}
}
}
void BluedroidPlatform::handleGattsConnect(esp_ble_gatts_cb_param_t* param) {
uint16_t conn_handle = allocateConnHandle();
BluedroidConnection conn;
conn.conn_id = param->connect.conn_id;
memcpy(conn.peer_addr, param->connect.remote_bda, 6);
conn.addr_type = BLE_ADDR_TYPE_PUBLIC; // Could be determined from GAP
conn.local_role = Role::PERIPHERAL;
conn.mtu = MTU::MINIMUM;
_connections[conn_handle] = conn;
// Log the mapping for debugging
char addr_str[18];
snprintf(addr_str, sizeof(addr_str), "%02X:%02X:%02X:%02X:%02X:%02X",
param->connect.remote_bda[0], param->connect.remote_bda[1], param->connect.remote_bda[2],
param->connect.remote_bda[3], param->connect.remote_bda[4], param->connect.remote_bda[5]);
INFO("BluedroidPlatform: GATTS Connected handle=" + std::to_string(conn_handle) +
" conn_id=" + std::to_string(param->connect.conn_id) +
" addr=" + std::string(addr_str));
// Stop advertising if we've hit max connections
if (getConnectionCount() >= _config.max_connections) {
DEBUG("BluedroidPlatform: At max connections, stopping advertising");
stopAdvertising();
}
if (_on_central_connected) {
ConnectionHandle ch = getConnection(conn_handle);
_on_central_connected(ch);
}
}
void BluedroidPlatform::handleGattsDisconnect(esp_ble_gatts_cb_param_t* param) {
// Find connection by conn_id
uint16_t conn_handle = 0xFFFF;
for (auto& pair : _connections) {
if (pair.second.conn_id == param->disconnect.conn_id) {
conn_handle = pair.first;
break;
}
}
if (conn_handle != 0xFFFF) {
INFO("BluedroidPlatform: Central disconnected, handle=" + std::to_string(conn_handle));
if (_on_central_disconnected) {
ConnectionHandle ch = getConnection(conn_handle);
_on_central_disconnected(ch);
}
_connections.erase(conn_handle);
}
// In dual mode, restart advertising
if (_config.role == Role::DUAL || _config.role == Role::PERIPHERAL) {
if (_running) {
startAdvertising();
}
}
}
void BluedroidPlatform::handleGattsWrite(esp_ble_gatts_cb_param_t* param) {
// Hot path - no logging here to avoid blocking main loop
if (!param->write.is_prep) {
// Regular write (not prepare write)
Bytes data(param->write.value, param->write.len);
// Find connection
uint16_t conn_handle = 0xFFFF;
for (auto& pair : _connections) {
if (pair.second.conn_id == param->write.conn_id) {
conn_handle = pair.first;
break;
}
}
if (param->write.handle == _rx_char_handle) {
// Data write to RX characteristic
if (_on_write_received && conn_handle != 0xFFFF) {
ConnectionHandle ch = getConnection(conn_handle);
_on_write_received(ch, data);
} else {
WARNING("BluedroidPlatform: No callback or invalid handle for RX write");
}
} else if (param->write.handle == _tx_cccd_handle) {
// Notification enable/disable
bool enabled = (param->write.len >= 2 && param->write.value[0] != 0);
if (conn_handle != 0xFFFF) {
auto* conn = findConnection(conn_handle);
if (conn) {
conn->notifications_enabled = enabled;
}
if (_on_notify_enabled) {
ConnectionHandle ch = getConnection(conn_handle);
_on_notify_enabled(ch, enabled);
}
}
}
// Send response if needed
if (param->write.need_rsp) {
esp_ble_gatts_send_response(_gatts_if, param->write.conn_id,
param->write.trans_id, ESP_GATT_OK, nullptr);
}
}
}
void BluedroidPlatform::handleGattsRead(esp_ble_gatts_cb_param_t* param) {
// Hot path - no logging here to avoid blocking main loop
esp_gatt_rsp_t rsp;
memset(&rsp, 0, sizeof(rsp));
if (param->read.handle == _identity_char_handle) {
// Return identity data
rsp.attr_value.handle = param->read.handle;
rsp.attr_value.len = _identity_data.size();
if (rsp.attr_value.len > ESP_GATT_MAX_ATTR_LEN) {
rsp.attr_value.len = ESP_GATT_MAX_ATTR_LEN;
}
memcpy(rsp.attr_value.value, _identity_data.data(), rsp.attr_value.len);
esp_ble_gatts_send_response(_gatts_if, param->read.conn_id,
param->read.trans_id, ESP_GATT_OK, &rsp);
} else {
// Unknown characteristic - send error
esp_ble_gatts_send_response(_gatts_if, param->read.conn_id,
param->read.trans_id, ESP_GATT_READ_NOT_PERMIT, nullptr);
}
}
void BluedroidPlatform::handleGattsMtuChange(esp_ble_gatts_cb_param_t* param) {
uint16_t conn_handle = 0xFFFF;
for (auto& pair : _connections) {
if (pair.second.conn_id == param->mtu.conn_id) {
conn_handle = pair.first;
pair.second.mtu = param->mtu.mtu;
break;
}
}
DEBUG("BluedroidPlatform: MTU changed to " + std::to_string(param->mtu.mtu));
if (_on_mtu_changed && conn_handle != 0xFFFF) {
ConnectionHandle ch = getConnection(conn_handle);
_on_mtu_changed(ch, param->mtu.mtu);
}
}
void BluedroidPlatform::handleGattsConfirm(esp_ble_gatts_cb_param_t* param) {
// Notification confirmation (indication acknowledgment)
// For notifications (not indications), this may not be called
}
//=============================================================================
// GATTC Event Handler
//=============================================================================
void BluedroidPlatform::handleGattcEvent(esp_gattc_cb_event_t event,
esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t* param) {
switch (event) {
case ESP_GATTC_REG_EVT:
handleGattcRegister(gattc_if, param->reg.status);
break;
case ESP_GATTC_OPEN_EVT:
handleGattcConnect(param);
break;
case ESP_GATTC_CLOSE_EVT:
case ESP_GATTC_DISCONNECT_EVT:
handleGattcDisconnect(param);
break;
case ESP_GATTC_SEARCH_RES_EVT:
DEBUG("BluedroidPlatform: SEARCH_RES_EVT received, conn_id=" +
std::to_string(param->search_res.conn_id));
handleGattcSearchResult(param);
break;
case ESP_GATTC_SEARCH_CMPL_EVT:
DEBUG("BluedroidPlatform: SEARCH_CMPL_EVT received, conn_id=" +
std::to_string(param->search_cmpl.conn_id) +
" status=" + std::to_string(param->search_cmpl.status));
handleGattcSearchComplete(param);
break;
case ESP_GATTC_NOTIFY_EVT:
handleGattcNotify(param);
break;
case ESP_GATTC_WRITE_CHAR_EVT:
case ESP_GATTC_WRITE_DESCR_EVT:
handleGattcWrite(param);
break;
case ESP_GATTC_READ_CHAR_EVT:
handleGattcRead(param);
break;
case ESP_GATTC_REG_FOR_NOTIFY_EVT:
if (param->reg_for_notify.status == ESP_GATT_OK) {
DEBUG("BluedroidPlatform: Registered for notifications on handle " +
std::to_string(param->reg_for_notify.handle));
} else {
WARNING("BluedroidPlatform: Failed to register for notify: " +
std::to_string(param->reg_for_notify.status));
}
break;
case ESP_GATTC_CFG_MTU_EVT:
// MTU exchange complete (when we're central)
if (param->cfg_mtu.status == ESP_GATT_OK) {
INFO("BluedroidPlatform: GATTC MTU configured to " + std::to_string(param->cfg_mtu.mtu));
// Find connection and update MTU
for (auto& pair : _connections) {
if (pair.second.conn_id == param->cfg_mtu.conn_id) {
pair.second.mtu = param->cfg_mtu.mtu;
if (_on_mtu_changed) {
ConnectionHandle ch = getConnection(pair.first);
_on_mtu_changed(ch, param->cfg_mtu.mtu);
}
break;
}
}
} else {
WARNING("BluedroidPlatform: MTU config failed: " + std::to_string(param->cfg_mtu.status));
}
break;
default:
break;
}
}
void BluedroidPlatform::handleGattcRegister(esp_gatt_if_t gattc_if, esp_gatt_status_t status) {
if (status != ESP_GATT_OK) {
ERROR("BluedroidPlatform: GATTC register failed: " + std::to_string(status));
return;
}
_gattc_if = gattc_if;
DEBUG("BluedroidPlatform: GATTC registered, if=" + std::to_string(gattc_if));
// If in dual mode and GATTS is ready, mark as fully ready
if (_config.role == Role::CENTRAL ||
(_config.role == Role::DUAL && _init_state == InitState::READY)) {
// Already ready or central-only mode
} else if (_init_state >= InitState::GATTS_STARTING_SERVICE) {
// GATTS service started, now we're fully ready
_init_state = InitState::READY;
INFO("BluedroidPlatform: Ready for connections (GATTC registered)");
}
}
void BluedroidPlatform::handleGattcConnect(esp_ble_gattc_cb_param_t* param) {
_connect_pending = false;
if (param->open.status != ESP_GATT_OK) {
ERROR("BluedroidPlatform: Connection failed: " + std::to_string(param->open.status));
_connect_success = false;
_connect_error = param->open.status;
// Try to start queued discoveries since connection finished
if (!_pending_discoveries.empty() && _discovery_in_progress == 0xFFFF) {
uint16_t next_handle = _pending_discoveries.front();
_pending_discoveries.pop();
DEBUG("BluedroidPlatform: Starting queued discovery for handle " + std::to_string(next_handle));
discoverServices(next_handle);
}
return;
}
_connect_success = true;
uint16_t conn_handle = allocateConnHandle();
BluedroidConnection conn;
conn.conn_id = param->open.conn_id;
memcpy(conn.peer_addr, param->open.remote_bda, 6);
conn.addr_type = BLE_ADDR_TYPE_PUBLIC;
conn.local_role = Role::CENTRAL;
conn.mtu = param->open.mtu;
_connections[conn_handle] = conn;
// Log the mapping for debugging
char addr_str[18];
snprintf(addr_str, sizeof(addr_str), "%02X:%02X:%02X:%02X:%02X:%02X",
param->open.remote_bda[0], param->open.remote_bda[1], param->open.remote_bda[2],
param->open.remote_bda[3], param->open.remote_bda[4], param->open.remote_bda[5]);
INFO("BluedroidPlatform: GATTC Connected handle=" + std::to_string(conn_handle) +
" conn_id=" + std::to_string(param->open.conn_id) +
" addr=" + std::string(addr_str));
if (_on_connected) {
ConnectionHandle ch = getConnection(conn_handle);
_on_connected(ch);
}
// Request MTU update
esp_ble_gattc_send_mtu_req(_gattc_if, param->open.conn_id);
}
void BluedroidPlatform::handleGattcDisconnect(esp_ble_gattc_cb_param_t* param) {
_connect_pending = false;
uint16_t conn_handle = 0xFFFF;
for (auto& pair : _connections) {
if (pair.second.conn_id == param->close.conn_id) {
conn_handle = pair.first;
break;
}
}
if (conn_handle != 0xFFFF) {
INFO("BluedroidPlatform: Peripheral disconnected, handle=" + std::to_string(conn_handle));
// If this connection was doing discovery, clear the flag and start next
if (_discovery_in_progress == conn_handle) {
_discovery_in_progress = 0xFFFF;
if (!_pending_discoveries.empty()) {
uint16_t next_handle = _pending_discoveries.front();
_pending_discoveries.pop();
DEBUG("BluedroidPlatform: Starting queued discovery for handle " + std::to_string(next_handle));
discoverServices(next_handle);
}
}
// Remove this connection from pending discoveries queue
std::queue<uint16_t> temp_queue;
while (!_pending_discoveries.empty()) {
uint16_t h = _pending_discoveries.front();
_pending_discoveries.pop();
if (h != conn_handle) {
temp_queue.push(h);
}
}
_pending_discoveries = std::move(temp_queue);
if (_on_disconnected) {
ConnectionHandle ch = getConnection(conn_handle);
_on_disconnected(ch, param->close.reason);
}
_connections.erase(conn_handle);
}
}
void BluedroidPlatform::handleGattcSearchResult(esp_ble_gattc_cb_param_t* param) {
// Find connection - ONLY search central (GATTC) connections
BluedroidConnection* conn = nullptr;
for (auto& pair : _connections) {
if (pair.second.conn_id == param->search_res.conn_id &&
pair.second.local_role == Role::CENTRAL) {
conn = &pair.second;
break;
}
}
if (!conn) return;
conn->service_start_handle = param->search_res.start_handle;
conn->service_end_handle = param->search_res.end_handle;
DEBUG("BluedroidPlatform: Found service, handles " +
std::to_string(conn->service_start_handle) + "-" +
std::to_string(conn->service_end_handle));
}
void BluedroidPlatform::handleGattcSearchComplete(esp_ble_gattc_cb_param_t* param) {
// Find connection - ONLY search central (GATTC) connections
BluedroidConnection* conn = nullptr;
uint16_t conn_handle = 0xFFFF;
for (auto& pair : _connections) {
if (pair.second.conn_id == param->search_cmpl.conn_id &&
pair.second.local_role == Role::CENTRAL) {
conn = &pair.second;
conn_handle = pair.first;
break;
}
}
if (!conn) {
ERROR("BluedroidPlatform: handleGattcSearchComplete - connection not found for conn_id=" +
std::to_string(param->search_cmpl.conn_id));
// Clear discovery state and try next
_discovery_in_progress = 0xFFFF;
if (!_pending_discoveries.empty()) {
uint16_t next_handle = _pending_discoveries.front();
_pending_discoveries.pop();
discoverServices(next_handle);
}
return;
}
DEBUG("BluedroidPlatform: handleGattcSearchComplete for conn_handle=" + std::to_string(conn_handle) +
" service_start=" + std::to_string(conn->service_start_handle));
if (conn->service_start_handle == 0) {
ERROR("BluedroidPlatform: Service not found for conn " + std::to_string(conn_handle));
// Clear discovery state and try next
_discovery_in_progress = 0xFFFF;
if (_on_services_discovered) {
ConnectionHandle ch = getConnection(conn_handle);
_on_services_discovered(ch, false);
}
if (!_pending_discoveries.empty()) {
uint16_t next_handle = _pending_discoveries.front();
_pending_discoveries.pop();
discoverServices(next_handle);
}
return;
}
// Get all characteristics in the service
conn->discovery_state = BluedroidConnection::DiscoveryState::GETTING_CHARS;
// Get characteristic count first
uint16_t count = 0;
esp_gatt_status_t status = esp_ble_gattc_get_attr_count(
_gattc_if,
param->search_cmpl.conn_id,
ESP_GATT_DB_CHARACTERISTIC,
conn->service_start_handle,
conn->service_end_handle,
0,
&count
);
if (status != ESP_GATT_OK || count == 0) {
ERROR("BluedroidPlatform: No characteristics found, status=" + std::to_string(status) +
" count=" + std::to_string(count));
conn->discovery_state = BluedroidConnection::DiscoveryState::COMPLETE;
// Clear discovery state and try next
_discovery_in_progress = 0xFFFF;
if (_on_services_discovered) {
ConnectionHandle ch = getConnection(conn_handle);
_on_services_discovered(ch, false);
}
if (!_pending_discoveries.empty()) {
uint16_t next_handle = _pending_discoveries.front();
_pending_discoveries.pop();
discoverServices(next_handle);
}
return;
}
{
char buf[80];
snprintf(buf, sizeof(buf), "BluedroidPlatform: Found %u characteristics for conn_handle=%u", count, conn_handle);
DEBUG(buf);
}
// Get all characteristics - use fixed stack buffer to avoid heap fragmentation
// We only need RX, TX, Identity chars so 16 is plenty (most services have <10)
static const uint16_t MAX_CHARS = 16;
esp_gattc_char_elem_t chars[MAX_CHARS];
if (count > MAX_CHARS) {
count = MAX_CHARS; // Limit to what we can handle
}
status = esp_ble_gattc_get_all_char(
_gattc_if,
param->search_cmpl.conn_id,
conn->service_start_handle,
conn->service_end_handle,
chars,
&count,
0
);
if (status == ESP_GATT_OK) {
// Parse UUID helper (same logic as buildAdvertisingData)
auto uuidMatches = [](const esp_bt_uuid_t& uuid, const char* uuid_str) -> bool {
if (uuid.len != ESP_UUID_LEN_128) return false;
uint8_t expected[16];
int idx = 15;
for (int i = 0; i < 36 && idx >= 0; i++) {
if (uuid_str[i] == '-') continue;
unsigned int byte_val;
sscanf(&uuid_str[i], "%02x", &byte_val);
expected[idx--] = static_cast<uint8_t>(byte_val);
i++;
}
return memcmp(uuid.uuid.uuid128, expected, 16) == 0;
};
char buf[64];
for (uint16_t i = 0; i < count; i++) {
if (uuidMatches(chars[i].uuid, UUID::RX_CHAR)) {
conn->rx_char_handle = chars[i].char_handle;
snprintf(buf, sizeof(buf), "BluedroidPlatform: Found RX char, handle=%u", conn->rx_char_handle);
DEBUG(buf);
} else if (uuidMatches(chars[i].uuid, UUID::TX_CHAR)) {
conn->tx_char_handle = chars[i].char_handle;
snprintf(buf, sizeof(buf), "BluedroidPlatform: Found TX char, handle=%u", conn->tx_char_handle);
DEBUG(buf);
// Get descriptor for TX (CCCD)
uint16_t desc_count = 1;
esp_gattc_descr_elem_t descr;
esp_bt_uuid_t cccd_uuid;
cccd_uuid.len = ESP_UUID_LEN_16;
cccd_uuid.uuid.uuid16 = 0x2902; // CCCD UUID
if (esp_ble_gattc_get_descr_by_char_handle(
_gattc_if, param->search_cmpl.conn_id,
chars[i].char_handle, cccd_uuid,
&descr, &desc_count) == ESP_GATT_OK && desc_count > 0) {
conn->tx_cccd_handle = descr.handle;
snprintf(buf, sizeof(buf), "BluedroidPlatform: Found TX CCCD, handle=%u", conn->tx_cccd_handle);
DEBUG(buf);
}
} else if (uuidMatches(chars[i].uuid, UUID::IDENTITY_CHAR)) {
conn->identity_char_handle = chars[i].char_handle;
snprintf(buf, sizeof(buf), "BluedroidPlatform: Found Identity char, handle=%u", conn->identity_char_handle);
DEBUG(buf);
}
}
}
// chars is stack-allocated, no delete needed
conn->discovery_state = BluedroidConnection::DiscoveryState::COMPLETE;
bool success = (conn->rx_char_handle != 0 && conn->tx_char_handle != 0);
{
char buf[128];
snprintf(buf, sizeof(buf), "BluedroidPlatform: Service discovery complete for conn %u RX=%u TX=%u CCCD=%u Identity=%u",
conn_handle, conn->rx_char_handle, conn->tx_char_handle, conn->tx_cccd_handle, conn->identity_char_handle);
INFO(buf);
}
if (_on_services_discovered) {
ConnectionHandle ch = getConnection(conn_handle);
ch.rx_char_handle = conn->rx_char_handle;
ch.tx_char_handle = conn->tx_char_handle;
ch.tx_cccd_handle = conn->tx_cccd_handle;
ch.identity_handle = conn->identity_char_handle;
_on_services_discovered(ch, success);
}
// Clear discovery in progress and start next queued discovery
_discovery_in_progress = 0xFFFF;
if (!_pending_discoveries.empty()) {
taskYIELD(); // Let stack process before next discovery
uint16_t next_handle = _pending_discoveries.front();
_pending_discoveries.pop();
DEBUG("BluedroidPlatform: Starting queued discovery for handle " + std::to_string(next_handle));
discoverServices(next_handle);
}
}
void BluedroidPlatform::handleGattcNotify(esp_ble_gattc_cb_param_t* param) {
// Hot path - no logging to avoid heap allocation on every packet
// Find connection - ONLY search central (GATTC) connections
uint16_t conn_handle = 0xFFFF;
for (auto& pair : _connections) {
if (pair.second.conn_id == param->notify.conn_id &&
pair.second.local_role == Role::CENTRAL) {
conn_handle = pair.first;
break;
}
}
if (conn_handle == 0xFFFF) {
WARNING("BluedroidPlatform: No CENTRAL connection found for notify conn_id=" +
std::to_string(param->notify.conn_id));
return;
}
Bytes data(param->notify.value, param->notify.value_len);
if (_on_data_received) {
ConnectionHandle ch = getConnection(conn_handle);
_on_data_received(ch, data);
}
}
void BluedroidPlatform::handleGattcWrite(esp_ble_gattc_cb_param_t* param) {
// Only log failures - hot path, avoid string allocation on success
if (param->write.status != ESP_GATT_OK) {
WARNING("BluedroidPlatform: Write FAILED conn_id=" + std::to_string(param->write.conn_id) +
" status=" + std::to_string(param->write.status));
}
}
void BluedroidPlatform::handleGattcRead(esp_ble_gattc_cb_param_t* param) {
if (param->read.status != ESP_GATT_OK) {
WARNING("BluedroidPlatform: Read failed: " + std::to_string(param->read.status));
complete(OperationResult::ERROR, Bytes());
return;
}
// Extract read data
Bytes data(param->read.value, param->read.value_len);
DEBUG("BluedroidPlatform: Read complete, " + std::to_string(data.size()) + " bytes");
// Complete the pending operation with the data
complete(OperationResult::SUCCESS, data);
}
//=============================================================================
// Address Conversion
//=============================================================================
BLEAddress BluedroidPlatform::fromEspBdAddr(const esp_bd_addr_t addr,
esp_ble_addr_type_t type) {
BLEAddress result;
memcpy(result.addr, addr, 6);
result.type = type;
return result;
}
void BluedroidPlatform::toEspBdAddr(const BLEAddress& addr, esp_bd_addr_t out_addr) {
memcpy(out_addr, addr.addr, 6);
}
//=============================================================================
// Connection Handle Management
//=============================================================================
uint16_t BluedroidPlatform::allocateConnHandle() {
return _next_conn_handle++;
}
void BluedroidPlatform::freeConnHandle(uint16_t handle) {
// Handle reuse not implemented - with 16-bit handles, unlikely to overflow
}
BluedroidPlatform::BluedroidConnection* BluedroidPlatform::findConnection(uint16_t conn_handle) {
auto it = _connections.find(conn_handle);
if (it != _connections.end()) {
return &it->second;
}
return nullptr;
}
BluedroidPlatform::BluedroidConnection* BluedroidPlatform::findConnectionByAddress(
const esp_bd_addr_t addr) {
for (auto& pair : _connections) {
if (memcmp(pair.second.peer_addr, addr, 6) == 0) {
return &pair.second;
}
}
return nullptr;
}
}} // namespace RNS::BLE
#endif // ESP32 && USE_BLUEDROID