Shadowtrance 5c78d55b04
Bluetooth (#518)
* Bluetooth LE addition

* fixes

* use the psram!

helps a little on S3 (t-deck)

* custom device name

* Update symbols.c

* Feedback + fixes

Fixes external app start/stop server (child devices)
Fixes BtManage causing a full system hang upon disabling bt when a device is connected to the host.

* updoot

* more updoot

* move back!

* Revert "move back!"

This reverts commit d3694365c634acc5db62ac59771c496cb971a727.

* fix some of the things

* Addressing feedback? hmm

* Fixes

Bug 1 — Reconnect loop / Reconnect not working fixed
Bug 2 — Name-only advertising overwrites HID advertising
Bug 3 — BleHidDeviceCtx leak on re-enable
Enhancement — HID device auto-start on radio re-enable

* stuff...

* update for consistency with others

* fix crashes

and some bonus symbols

* a few symbols, i2c speed, cdn message

100kHz i2c speed seems to be more compatible with m5stack modules...and probably in general.
cdn message no longer applies

* Hide BT Settings when bt not enabled

* Addressing things and device fixes

* Missed one!

* stuff
2026-05-22 22:49:37 +02:00

1122 lines
47 KiB
C++

#ifdef ESP_PLATFORM
#include <sdkconfig.h>
#endif
#if defined(CONFIG_BT_NIMBLE_ENABLED)
#include <bluetooth/esp32_ble_internal.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <host/ble_att.h>
#include <host/ble_gap.h>
#include <host/ble_hs.h>
#include <host/ble_sm.h>
#include <host/ble_store.h>
#include <host/ble_uuid.h>
#include <host/util/util.h>
#include <nimble/nimble_port.h>
#include <nimble/nimble_port_freertos.h>
#include <services/gap/ble_svc_gap.h>
#include <services/gatt/ble_svc_gatt.h>
#include <store/config/ble_store_config.h>
#include <atomic>
#include <cstring>
constexpr auto* TAG = "esp32_ble";
#include <tactility/log.h>
#include <esp_timer.h>
// ble_store_config_init() is not declared in the public header in some IDF versions.
extern "C" void ble_store_config_init(void);
// Sub-module API structs (defined in their respective cpp files).
extern const BtHidDeviceApi nimble_hid_device_api;
extern const BtSerialApi nimble_serial_api;
extern const BtMidiApi nimble_midi_api;
// File-static BleCtx pointer used only by NimBLE host callbacks whose signature
// is fixed by the NimBLE API (on_sync, on_reset) and cannot carry a Device*.
// All other callbacks receive Device* via their void* arg parameter.
static BleCtx* s_ctx = nullptr;
// ---- Context accessor ----
// Returns the BleCtx for any BLE device (root or child).
// If device is the root BLE device (BLUETOOTH_TYPE) its driver_data IS the BleCtx.
// If device is a child BLE device its parent is the root BLE device.
// We must NOT use device_get_parent blindly: the DTS root node is the parent of ble0,
// so device_get_parent(ble0) returns a non-null node whose driver_data is not BleCtx.
BleCtx* ble_get_ctx(struct Device* device) {
if (device_get_type(device) == &BLUETOOTH_TYPE) {
return (BleCtx*)device_get_driver_data(device);
}
struct Device* parent = device_get_parent(device);
return parent ? (BleCtx*)device_get_driver_data(parent) : nullptr;
}
// ---- Forward declarations ----
static void host_task(void* param);
static void on_sync();
static void on_reset(int reason);
static void dispatch_enable(BleCtx* ctx);
static void dispatch_disable(BleCtx* ctx);
static int gap_event_handler(struct ble_gap_event* event, void* arg);
// ---- General field accessor implementations ----
BtRadioState ble_get_radio_state(struct Device* device) {
return ble_get_ctx(device)->radio_state.load();
}
bool ble_hid_get_host_active(struct Device* device) {
return ble_get_ctx(device)->hid_host_active.load();
}
bool ble_get_scan_active(struct Device* device) {
return ble_get_ctx(device)->scan_active.load();
}
void ble_set_scan_active(struct Device* device, bool v) {
ble_get_ctx(device)->scan_active.store(v);
}
// ---- Event publishing ----
void ble_publish_event(struct Device* device, struct BtEvent event) {
BleCtx* ctx = ble_get_ctx(device);
if (!ctx) return;
// Copy under mutex so callbacks can safely call add/remove_event_callback
BleCallbackEntry local[BLE_MAX_CALLBACKS];
size_t count;
xSemaphoreTake(ctx->cb_mutex, portMAX_DELAY);
count = ctx->callback_count;
memcpy(local, ctx->callbacks, count * sizeof(BleCallbackEntry));
xSemaphoreGive(ctx->cb_mutex);
for (size_t i = 0; i < count; i++) {
local[i].fn(ctx->device, local[i].ctx, event);
}
}
// ---- Advertising restart helper ----
//TODO: Fix Restart name-only advertising after a rename.
static void adv_restart_callback(void* arg) {
struct Device* device = (struct Device*)arg;
BleCtx* ctx = ble_get_ctx(device);
if (ctx->radio_state.load() != BT_RADIO_STATE_ON) return;
uint16_t hid_conn = ble_hid_get_conn_handle(ctx->hid_device_child);
if (ctx->midi_active.load() && ctx->midi_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) {
ble_start_advertising(device, &MIDI_SVC_UUID);
} else if (ctx->spp_active.load() && ctx->spp_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) {
ble_start_advertising(device, &NUS_SVC_UUID);
} else if (ctx->hid_active.load() && hid_conn == BLE_HS_CONN_HANDLE_NONE) {
ble_start_advertising_hid(device, hid_appearance);
}
}
void ble_schedule_adv_restart(struct Device* device, uint64_t delay_us) {
BleCtx* ctx = ble_get_ctx(device);
if (delay_us == 0) {
adv_restart_callback(device);
return;
}
if (ctx->adv_restart_timer == nullptr) {
esp_timer_create_args_t args = {};
args.callback = adv_restart_callback;
args.arg = device;
args.dispatch_method = ESP_TIMER_TASK;
args.name = "ble_adv_restart";
int rc = esp_timer_create(&args, &ctx->adv_restart_timer);
if (rc != ESP_OK) {
LOG_E(TAG, "adv_restart timer create failed (rc=%d)", rc);
return;
}
}
esp_timer_stop(ctx->adv_restart_timer);
int rc = esp_timer_start_once(ctx->adv_restart_timer, delay_us);
if (rc != ESP_OK) {
LOG_E(TAG, "adv_restart timer start failed (rc=%d)", rc);
}
}
// ---- GAP connection event handler ----
static int gap_event_handler(struct ble_gap_event* event, void* arg) {
struct Device* device = (struct Device*)arg;
BleCtx* ctx = ble_get_ctx(device);
switch (event->type) {
case BLE_GAP_EVENT_CONNECT:
if (event->connect.status == 0) {
LOG_I(TAG, "Connected (handle=%u hid_active=%d hid_conn=%u)",
event->connect.conn_handle,
(int)ctx->hid_active.load(),
(unsigned)ble_hid_get_conn_handle(ctx->hid_device_child));
// Do NOT call ble_gap_security_initiate() here.
// Windows BLE MIDI initiates encryption itself; calling here creates a race
// with REPEAT_PAIRING+RETRY → two concurrent SM procedures → disconnect.
// On esp_hosted, ENC_CHANGE can arrive BEFORE CONNECT, so re-initiating
// on an already-encrypted link triggers unwanted re-pairing.
} else {
LOG_W(TAG, "Connection failed (status=%d)", event->connect.status);
// Delay restart so NimBLE can clean up SMP/connection state before peer retries.
ble_schedule_adv_restart(device, 500'000);
}
break;
case BLE_GAP_EVENT_DISCONNECT: {
LOG_I(TAG, "Disconnected (reason=%d)", event->disconnect.reason);
uint16_t hdl = event->disconnect.conn.conn_handle;
bool was_spp = (ctx->spp_conn_handle.load() == hdl);
bool was_midi = (ctx->midi_conn_handle.load() == hdl);
bool was_hid = (ble_hid_get_conn_handle(ctx->hid_device_child) == hdl);
if (was_spp) ctx->spp_conn_handle.store(BLE_HS_CONN_HANDLE_NONE);
if (was_midi) { ctx->midi_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); ctx->midi_use_indicate.store(false); }
if (was_hid) ble_hid_set_conn_handle(ctx->hid_device_child, BLE_HS_CONN_HANDLE_NONE);
ctx->link_encrypted.store(false);
// If HID was stopped while connected, switch profile to None now that the
// connection is gone. ble_gatts_mutable() is true here (no active connection,
// advertising stopped by hid_device_stop), so switch_profile is safe.
// Skip during shutdown — ble_gatts_reset() is unsafe while nimble_port_stop() runs.
if (was_hid && !ctx->hid_active.load() && current_hid_profile != BleHidProfile::None &&
ctx->radio_state.load() != BT_RADIO_STATE_OFF_PENDING) {
if (!ble_hid_switch_profile(ctx->hid_device_child, BleHidProfile::None)) {
LOG_E(TAG, "disconnect: switch to None failed — GATT state inconsistent");
}
}
// Restart advertising whenever a service is active without a live connection.
// Covers both normal disconnect and Windows discovery-only connections.
// Skip if the radio is going OFF_PENDING — nimble_port_stop() is in progress
// and calling ble_gap_adv_start() from within the NimBLE host task while the
// controller is shutting down would block the host task and hang nimble_port_stop().
if (ctx->radio_state.load() != BT_RADIO_STATE_OFF_PENDING) {
uint16_t hid_conn_now = ble_hid_get_conn_handle(ctx->hid_device_child);
if (ctx->midi_active.load() && ctx->midi_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) {
ble_start_advertising(device, &MIDI_SVC_UUID);
} else if (ctx->spp_active.load() && ctx->spp_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) {
ble_start_advertising(device, &NUS_SVC_UUID);
} else if (ctx->hid_active.load() && hid_conn_now == BLE_HS_CONN_HANDLE_NONE) {
ble_start_advertising_hid(device, hid_appearance);
}
}
break;
}
case BLE_GAP_EVENT_SUBSCRIBE:
LOG_I(TAG, "Subscribe attr=%u cur_notify=%u cur_indicate=%u (nus_tx=%u midi_io=%u)",
event->subscribe.attr_handle,
(unsigned)event->subscribe.cur_notify,
(unsigned)event->subscribe.cur_indicate,
nus_tx_handle, midi_io_handle);
if (event->subscribe.attr_handle != nus_tx_handle &&
event->subscribe.attr_handle != midi_io_handle &&
event->subscribe.cur_indicate) {
// Service Changed subscription — ignore (Windows discovers on its own)
LOG_I(TAG, "Service Changed subscription (attr=%u) — ignoring",
event->subscribe.attr_handle);
} else if (event->subscribe.attr_handle == nus_tx_handle) {
if (!ctx->spp_active.load()) {
LOG_I(TAG, "SPP CCCD subscribed but spp_active=false — ignoring");
break;
}
ctx->spp_conn_handle.store(event->subscribe.conn_handle);
LOG_I(TAG, "SPP client subscribed (nus_tx=%u)", nus_tx_handle);
// Fire PROFILE_STATE_CHANGED so Tactility bridge can persist the profile
// off the NimBLE host task (file I/O → stack overflow on nimble_host).
{
struct ble_gap_conn_desc sub_desc = {};
if (ble_gap_conn_find(event->subscribe.conn_handle, &sub_desc) == 0) {
struct BtEvent e = {};
e.type = BT_EVENT_PROFILE_STATE_CHANGED;
memcpy(e.profile_state.addr, sub_desc.peer_id_addr.val, 6);
e.profile_state.profile = BT_PROFILE_SPP;
e.profile_state.state = BT_PROFILE_STATE_CONNECTED;
ble_publish_event(device, e);
}
}
} else if (event->subscribe.attr_handle == midi_io_handle) {
if ((event->subscribe.cur_notify || event->subscribe.cur_indicate) && !ctx->midi_active.load()) {
LOG_I(TAG, "MIDI CCCD subscribed but midi_active=false — ignoring");
break;
}
if (event->subscribe.cur_notify || event->subscribe.cur_indicate) {
ctx->midi_conn_handle.store(event->subscribe.conn_handle);
ctx->midi_use_indicate.store(event->subscribe.cur_indicate != 0);
LOG_I(TAG, "MIDI client subscribed (midi_io=%u indicate=%d)",
midi_io_handle, (int)ctx->midi_use_indicate.load());
// Fire PROFILE_STATE_CHANGED so Tactility bridge persists the profile.
{
struct ble_gap_conn_desc sub_desc = {};
if (ble_gap_conn_find(event->subscribe.conn_handle, &sub_desc) == 0) {
struct BtEvent e = {};
e.type = BT_EVENT_PROFILE_STATE_CHANGED;
memcpy(e.profile_state.addr, sub_desc.peer_id_addr.val, 6);
e.profile_state.profile = BT_PROFILE_MIDI;
e.profile_state.state = BT_PROFILE_STATE_CONNECTED;
ble_publish_event(device, e);
}
}
// Send MIDI Active Sensing immediately after subscription.
static const uint8_t active_sensing_pkt[3] = { 0x80, 0x80, 0xFE };
struct os_mbuf* as_om = ble_hs_mbuf_from_flat(active_sensing_pkt, 3);
if (as_om != nullptr) {
int as_rc = ctx->midi_use_indicate.load()
? ble_gatts_indicate_custom(ctx->midi_conn_handle.load(), midi_io_handle, as_om)
: ble_gatts_notify_custom(ctx->midi_conn_handle.load(), midi_io_handle, as_om);
if (as_rc != 0) os_mbuf_free_chain(as_om);
LOG_I(TAG, "Active Sensing (subscribe) rc=%d", as_rc);
}
} else {
ctx->midi_conn_handle.store(BLE_HS_CONN_HANDLE_NONE);
ctx->midi_use_indicate.store(false);
LOG_I(TAG, "MIDI client unsubscribed");
}
} else if (event->subscribe.cur_notify &&
(event->subscribe.attr_handle == hid_kb_input_handle ||
event->subscribe.attr_handle == hid_consumer_input_handle ||
event->subscribe.attr_handle == hid_mouse_input_handle ||
event->subscribe.attr_handle == hid_gamepad_input_handle)) {
const char* rpt_name =
(event->subscribe.attr_handle == hid_kb_input_handle) ? "keyboard" :
(event->subscribe.attr_handle == hid_consumer_input_handle) ? "consumer" :
(event->subscribe.attr_handle == hid_mouse_input_handle) ? "mouse" :
(event->subscribe.attr_handle == hid_gamepad_input_handle) ? "gamepad" : "unknown";
if (!ctx->hid_active.load()) {
LOG_I(TAG, "HID CCCD subscribed (%s) but hid_active=false — ignoring", rpt_name);
break;
}
LOG_I(TAG, "HID CCCD subscribed: %s (attr=%u conn=%u)",
rpt_name, event->subscribe.attr_handle, event->subscribe.conn_handle);
if (ble_hid_get_conn_handle(ctx->hid_device_child) == BLE_HS_CONN_HANDLE_NONE) {
ble_hid_set_conn_handle(ctx->hid_device_child, event->subscribe.conn_handle);
}
}
break;
case BLE_GAP_EVENT_MTU:
LOG_I(TAG, "MTU updated (conn=%u mtu=%u)",
event->mtu.conn_handle, event->mtu.value);
break;
case BLE_GAP_EVENT_CONN_UPDATE: {
struct ble_gap_conn_desc desc = {};
if (ble_gap_conn_find(event->conn_update.conn_handle, &desc) == 0) {
LOG_I(TAG, "Conn params updated (status=%d itvl=%u latency=%u timeout=%u)",
event->conn_update.status, desc.conn_itvl,
desc.conn_latency, desc.supervision_timeout);
}
break;
}
case BLE_GAP_EVENT_CONN_UPDATE_REQ:
*event->conn_update_req.self_params = *event->conn_update_req.peer_params;
return 0;
case BLE_GAP_EVENT_ENC_CHANGE:
LOG_I(TAG, "Encryption changed (conn=%u status=%d)",
event->enc_change.conn_handle, event->enc_change.status);
if (event->enc_change.status == 0) {
ctx->link_encrypted.store(true);
// For HID device: set hid_conn_handle at encryption time so Phase 2 bonded
// reconnects are tracked even when BLE_GAP_EVENT_SUBSCRIBE doesn't fire.
// Windows only writes HID CCCDs in Phase 2; NimBLE may restore them from NVS
// silently (no SUBSCRIBE event). Without this, hid_conn_handle stays NONE
// and hid_device_is_connected() returns false for the entire Phase 2 session.
if (ctx->hid_active.load() &&
ble_hid_get_conn_handle(ctx->hid_device_child) == BLE_HS_CONN_HANDLE_NONE) {
ble_hid_set_conn_handle(ctx->hid_device_child, event->enc_change.conn_handle);
LOG_I(TAG, "HID conn handle set on ENC_CHANGE (conn=%u)",
event->enc_change.conn_handle);
}
// Fire PAIR_RESULT so Tactility bridge can persist the paired device
// off the NimBLE host task (file I/O → stack overflow on nimble_host).
struct ble_gap_conn_desc desc = {};
if (ble_gap_conn_find(event->enc_change.conn_handle, &desc) == 0) {
struct BtEvent e = {};
e.type = BT_EVENT_PAIR_RESULT;
memcpy(e.pair_result.addr, desc.peer_id_addr.val, 6);
e.pair_result.result = BT_PAIR_RESULT_SUCCESS;
e.pair_result.profile = ctx->midi_active.load() ? BT_PROFILE_MIDI
: ctx->spp_active.load() ? BT_PROFILE_SPP
: ctx->hid_active.load() ? BT_PROFILE_HID_DEVICE
: BT_PROFILE_HID_HOST;
ble_publish_event(device, e);
}
}
// Re-send Active Sensing now that the link is encrypted.
if (event->enc_change.status == 0 &&
ctx->midi_conn_handle.load() == event->enc_change.conn_handle) {
static const uint8_t as_pkt[3] = { 0x80, 0x80, 0xFE };
struct os_mbuf* om = ble_hs_mbuf_from_flat(as_pkt, 3);
if (om != nullptr) {
int rc = ctx->midi_use_indicate.load()
? ble_gatts_indicate_custom(ctx->midi_conn_handle.load(), midi_io_handle, om)
: ble_gatts_notify_custom(ctx->midi_conn_handle.load(), midi_io_handle, om);
if (rc != 0) os_mbuf_free_chain(om);
LOG_I(TAG, "Active Sensing (post-enc) rc=%d", rc);
}
}
break;
case BLE_GAP_EVENT_PASSKEY_ACTION: {
LOG_I(TAG, "Passkey action (conn=%u action=%u)",
event->passkey.conn_handle, event->passkey.params.action);
struct ble_sm_io pkey = {};
pkey.action = event->passkey.params.action;
if (pkey.action == BLE_SM_IOACT_NONE) {
ble_sm_inject_io(event->passkey.conn_handle, &pkey);
}
break;
}
case BLE_GAP_EVENT_REPEAT_PAIRING: {
LOG_I(TAG, "Repeat pairing (conn=%u encrypted=%d)",
event->repeat_pairing.conn_handle, (int)ctx->link_encrypted.load());
struct ble_gap_conn_desc desc;
if (ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc) == 0) {
ble_store_util_delete_peer(&desc.peer_id_addr);
if (!ctx->link_encrypted.load()) {
// Fire BOND_LOST so Tactility bridge removes the stored device record.
struct BtEvent e = {};
e.type = BT_EVENT_PAIR_RESULT;
memcpy(e.pair_result.addr, desc.peer_id_addr.val, 6);
e.pair_result.result = BT_PAIR_RESULT_BOND_LOST;
ble_publish_event(device, e);
}
}
// If already encrypted, IGNORE so the current session continues.
// RETRY would start a 30-second SMP timer that always expires (peer won't
// respond to a new Pair Request on an encrypted link) → forced disconnect.
if (ctx->link_encrypted.load()) {
LOG_I(TAG, "Repeat pairing: link encrypted — ignoring");
return BLE_GAP_REPEAT_PAIRING_IGNORE;
}
return BLE_GAP_REPEAT_PAIRING_RETRY;
}
default:
break;
}
return 0;
}
// ---- NimBLE host sync / reset callbacks ----
static void on_sync() {
LOG_I(TAG, "NimBLE synced (nus_tx=%u midi_io=%u hid_kb=%u hid_cs=%u hid_ms=%u hid_gp=%u)",
nus_tx_handle, midi_io_handle,
hid_kb_input_handle, hid_consumer_input_handle,
hid_mouse_input_handle, hid_gamepad_input_handle);
BleCtx* ctx = s_ctx;
if (ctx == nullptr) return;
ctx->pending_reset_count.store(0);
uint8_t own_addr_type;
int rc = ble_hs_util_ensure_addr(0);
if (rc != 0) LOG_E(TAG, "ensure_addr failed (rc=%d)", rc);
rc = ble_hs_id_infer_auto(0, &own_addr_type);
if (rc != 0) LOG_E(TAG, "infer addr type failed (rc=%d)", rc);
// Sync GATT handle values — pass the child device so the GATT callbacks
// can retrieve child driver data (BleSppCtx / BleMidiCtx) without globals.
ble_spp_init_gatt_handles(ctx->serial_child);
ble_midi_init_gatt_handles(ctx->midi_child);
ctx->radio_state.store(BT_RADIO_STATE_ON);
struct BtEvent e = {};
e.type = BT_EVENT_RADIO_STATE_CHANGED;
e.radio_state = BT_RADIO_STATE_ON;
ble_publish_event(ctx->device, e);
// The Tactility bridge handles auto-start (SPP/MIDI/HID) in response to
// BT_EVENT_RADIO_STATE_CHANGED(ON), dispatched to the main task above.
// On a multi-core ESP32 the main task can preempt the NimBLE host task
// between ble_publish_event() and here, call hid/spp/midi_device_start(),
// set hid/spp/midi_active, and already start profile-specific advertising.
// Only start name-only advertising if no profile has been activated yet;
// otherwise we would overwrite the correct profile advertising with name-only,
// causing Windows to connect without seeing the HID/SPP/MIDI service UUID.
if (!ctx->hid_active.load() && !ctx->spp_active.load() && !ctx->midi_active.load()) {
ble_start_advertising(ctx->device, nullptr);
}
}
static void dispatch_disable_timer_cb(void* arg) {
BleCtx* ctx = (BleCtx*)arg;
dispatch_disable(ctx);
}
static void on_reset(int reason) {
LOG_W(TAG, "NimBLE host reset (reason=%d)", reason);
BleCtx* ctx = s_ctx;
if (ctx == nullptr) return;
if (ctx->radio_state.load() == BT_RADIO_STATE_ON_PENDING) {
int count = ctx->pending_reset_count.fetch_add(1) + 1;
if (count == 3) {
LOG_E(TAG, "BT controller unresponsive after 3 resets — giving up");
// Can't call nimble_port_stop() from the NimBLE host task.
// Fire a one-shot esp_timer (delay=0 → fires on esp_timer task immediately).
if (ctx->disable_timer != nullptr) {
esp_timer_start_once(ctx->disable_timer, 0);
}
}
}
}
static void host_task(void* param) {
LOG_I(TAG, "BLE host task started");
nimble_port_run();
// nimble_port_deinit() is called by dispatch_disable() after nimble_port_stop() returns.
nimble_port_freertos_deinit();
}
// ---- Advertising helpers ----
void ble_start_advertising(struct Device* device, const ble_uuid128_t* svc_uuid) {
ble_gap_adv_stop();
// Always sync the GAP name from ctx right before building the advertising packet,
// so bluetooth_set_device_name() is honoured regardless of call timing.
BleCtx* _name_ctx = ble_get_ctx(device);
if (_name_ctx) ble_svc_gap_device_name_set(_name_ctx->device_name);
int rc;
if (svc_uuid != nullptr) {
const char* name = ble_svc_gap_device_name();
uint8_t name_len = (uint8_t)strlen(name);
uint8_t short_len = (name_len > 8) ? 8 : name_len;
ble_uuid128_t uuid_copy = *svc_uuid;
struct ble_hs_adv_fields fields;
memset(&fields, 0, sizeof(fields));
fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
fields.uuids128 = &uuid_copy;
fields.num_uuids128 = 1;
fields.uuids128_is_complete = 1;
fields.name = (const uint8_t*)name;
fields.name_len = short_len;
fields.name_is_complete = 0; // AD type 0x08 = Shortened Local Name
rc = ble_gap_adv_set_fields(&fields);
if (rc != 0) {
LOG_W(TAG, "startAdvertising: set_fields with name failed rc=%d, retrying", rc);
fields.name = nullptr;
fields.name_len = 0;
rc = ble_gap_adv_set_fields(&fields);
if (rc != 0) {
LOG_E(TAG, "startAdvertising: set_fields failed rc=%d", rc);
return;
}
}
struct ble_hs_adv_fields rsp;
memset(&rsp, 0, sizeof(rsp));
rsp.name = (const uint8_t*)name;
rsp.name_len = name_len;
rsp.name_is_complete = 1;
rc = ble_gap_adv_rsp_set_fields(&rsp);
if (rc != 0) {
LOG_W(TAG, "startAdvertising: rsp_set_fields rc=%d (non-fatal)", rc);
}
LOG_I(TAG, "startAdvertising: UUID mode (uuid[0..3]=%02x%02x%02x%02x)",
svc_uuid->value[0], svc_uuid->value[1],
svc_uuid->value[2], svc_uuid->value[3]);
} else {
struct ble_hs_adv_fields fields;
memset(&fields, 0, sizeof(fields));
fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
const char* name = ble_svc_gap_device_name();
fields.name = (const uint8_t*)name;
fields.name_len = (uint8_t)strlen(name);
fields.name_is_complete = 1;
rc = ble_gap_adv_set_fields(&fields);
if (rc != 0) {
LOG_E(TAG, "startAdvertising: set_fields failed rc=%d", rc);
return;
}
LOG_I(TAG, "startAdvertising: name-only mode");
}
struct ble_gap_adv_params adv_params;
memset(&adv_params, 0, sizeof(adv_params));
adv_params.conn_mode = BLE_GAP_CONN_MODE_UND;
adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN;
adv_params.itvl_min = 160; // 100 ms
adv_params.itvl_max = 240; // 150 ms
rc = ble_gap_adv_start(BLE_OWN_ADDR_PUBLIC, nullptr, BLE_HS_FOREVER,
&adv_params, gap_event_handler, device);
if (rc != 0 && rc != BLE_HS_EALREADY) {
LOG_E(TAG, "startAdvertising: adv_start failed rc=%d", rc);
} else {
LOG_I(TAG, "startAdvertising: OK");
}
}
void ble_start_advertising_hid(struct Device* device, uint16_t appearance) {
ble_gap_adv_stop();
// Always sync the GAP name from ctx right before building the advertising packet.
BleCtx* _name_ctx = ble_get_ctx(device);
if (_name_ctx) ble_svc_gap_device_name_set(_name_ctx->device_name);
const char* name = ble_svc_gap_device_name();
uint8_t name_len = (uint8_t)strlen(name);
uint8_t short_len = (name_len > 8) ? 8 : name_len;
static const ble_uuid16_t hid_uuid16 = BLE_UUID16_INIT(0x1812);
struct ble_hs_adv_fields fields;
memset(&fields, 0, sizeof(fields));
fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
fields.appearance = appearance;
fields.appearance_is_present = 1;
fields.uuids16 = &hid_uuid16;
fields.num_uuids16 = 1;
fields.uuids16_is_complete = 1;
fields.name = (const uint8_t*)name;
fields.name_len = short_len;
fields.name_is_complete = 0;
int rc = ble_gap_adv_set_fields(&fields);
if (rc != 0) {
LOG_W(TAG, "startAdvertisingHid: set_fields with name failed rc=%d, retrying", rc);
fields.name = nullptr;
fields.name_len = 0;
rc = ble_gap_adv_set_fields(&fields);
if (rc != 0) {
LOG_E(TAG, "startAdvertisingHid: set_fields failed rc=%d", rc);
return;
}
}
struct ble_hs_adv_fields rsp;
memset(&rsp, 0, sizeof(rsp));
rsp.name = (const uint8_t*)name;
rsp.name_len = name_len;
rsp.name_is_complete = 1;
rc = ble_gap_adv_rsp_set_fields(&rsp);
if (rc != 0) {
LOG_W(TAG, "startAdvertisingHid: rsp_set_fields rc=%d (non-fatal)", rc);
}
struct ble_gap_adv_params adv_params;
memset(&adv_params, 0, sizeof(adv_params));
adv_params.conn_mode = BLE_GAP_CONN_MODE_UND;
adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN;
adv_params.itvl_min = 160;
adv_params.itvl_max = 240;
rc = ble_gap_adv_start(BLE_OWN_ADDR_PUBLIC, nullptr, BLE_HS_FOREVER,
&adv_params, gap_event_handler, device);
if (rc != 0 && rc != BLE_HS_EALREADY) {
LOG_E(TAG, "startAdvertisingHid: adv_start failed rc=%d", rc);
} else {
LOG_I(TAG, "startAdvertisingHid: OK");
}
}
// ---- Dispatch helpers ----
static void dispatch_enable(BleCtx* ctx) {
LOG_I(TAG, "dispatch_enable()");
if (ctx->radio_state.load() != BT_RADIO_STATE_OFF) {
LOG_W(TAG, "Cannot enable from current state");
return;
}
ctx->radio_state.store(BT_RADIO_STATE_ON_PENDING);
{
struct BtEvent e = {};
e.type = BT_EVENT_RADIO_STATE_CHANGED;
e.radio_state = BT_RADIO_STATE_ON_PENDING;
ble_publish_event(ctx->device, e);
}
int rc = nimble_port_init();
if (rc != 0) {
LOG_E(TAG, "nimble_port_init failed (rc=%d)", rc);
ctx->radio_state.store(BT_RADIO_STATE_OFF);
struct BtEvent e = {};
e.type = BT_EVENT_RADIO_STATE_CHANGED;
e.radio_state = BT_RADIO_STATE_OFF;
ble_publish_event(ctx->device, e);
return;
}
ble_hs_cfg.sync_cb = on_sync;
ble_hs_cfg.reset_cb = on_reset;
ble_hs_cfg.store_status_cb = ble_store_util_status_rr;
ble_hs_cfg.sm_io_cap = BLE_SM_IO_CAP_NO_IO;
ble_hs_cfg.sm_bonding = 1;
ble_hs_cfg.sm_mitm = 1;
ble_hs_cfg.sm_sc = 1;
ble_hs_cfg.sm_our_key_dist = BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;
ble_hs_cfg.sm_their_key_dist = BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;
ble_store_config_init();
ble_hs_cfg.store_read_cb = ble_store_config_read;
ble_hs_cfg.store_write_cb = ble_store_config_write;
ble_hs_cfg.store_delete_cb = ble_store_config_delete;
// Clear any stale "value_changed" flags left in NVS CCCD records.
// Old firmware called ble_svc_gatt_changed(0, 0xFFFF) from switchGattProfile(),
// which sets value_changed=1 in NVS for all bonded-but-disconnected peers.
// On reconnect NimBLE sends a Service Changed indication; Windows disconnects
// without ACKing (it re-discovers instead), so NimBLE never clears the flag,
// causing an infinite reconnect loop. Purge all value_changed flags on every
// enable so the first reconnect after re-enable always succeeds.
{
// Zero-initialize: peer_addr={0} matches any peer (== BLE_ADDR_ANY),
// chr_val_handle=0 matches any handle — both are wildcards per the
// ble_store_config_find_cccd() implementation.
struct ble_store_key_cccd cccd_key = {};
struct ble_store_value_cccd cccd_val = {};
int n_cleared = 0;
while (ble_store_read_cccd(&cccd_key, &cccd_val) == 0) {
if (cccd_val.value_changed) {
cccd_val.value_changed = 0;
ble_store_write_cccd(&cccd_val);
n_cleared++;
}
cccd_key.idx++;
}
if (n_cleared > 0) {
LOG_I(TAG, "Cleared %d stale CCCD value_changed flag(s) from NVS", n_cleared);
}
}
ble_svc_gap_init();
ble_svc_gatt_init();
// Register base GATT services (NUS + MIDI; HID added by switch_profile when started)
ble_hid_init_gatt();
ble_svc_gap_device_name_set(ctx->device_name);
ble_att_set_preferred_mtu(BLE_ATT_MTU_MAX);
#if defined(CONFIG_ESP_HOSTED_ENABLED)
ble_hci_gate_set_active(true); // Open gate: NimBLE transport pool is ready
#endif
// Start NimBLE host task (on_sync will fire when ready)
nimble_port_freertos_init(host_task);
}
static void dispatch_disable(BleCtx* ctx) {
LOG_I(TAG, "dispatch_disable()");
if (ctx->radio_state.load() == BT_RADIO_STATE_OFF) {
LOG_W(TAG, "Already off");
return;
}
ctx->radio_state.store(BT_RADIO_STATE_OFF_PENDING);
{
struct BtEvent e = {};
e.type = BT_EVENT_RADIO_STATE_CHANGED;
e.radio_state = BT_RADIO_STATE_OFF_PENDING;
ble_publish_event(ctx->device, e);
}
// Blocking: waits for nimble_port_run() to exit.
// Do NOT call ble_gap_adv_stop()/disc_cancel() before — if controller is
// unresponsive they generate more HCI timeouts before the stop takes effect.
// The HCI gate must stay OPEN here: nimble_port_stop() → ble_hs_stop() sends
// HCI commands and needs to receive the command-complete events back from the
// controller. Closing the gate before this point starves NimBLE of those events,
// causing HCI timeouts and a cascade of resets that crash in ble_hs_timer_sched.
nimble_port_stop();
#if defined(CONFIG_ESP_HOSTED_ENABLED)
// Close the gate NOW — after nimble_port_stop() returns the NimBLE host task has
// exited and nimble_port_deinit() is about to zero npl_funcs. Any HCI packet
// arriving after this point must not reach ble_transport_alloc_evt().
ble_hci_gate_set_active(false);
if (!ble_hci_gate_wait_idle(20)) {
LOG_W(TAG, "HCI gate drain timed out");
}
#endif
nimble_port_deinit();
ctx->spp_conn_handle.store(BLE_HS_CONN_HANDLE_NONE);
ctx->spp_active.store(false);
ctx->midi_conn_handle.store(BLE_HS_CONN_HANDLE_NONE);
ctx->midi_active.store(false);
ctx->hid_active.store(false);
ctx->link_encrypted.store(false);
ctx->pending_reset_count.store(0);
current_hid_profile = BleHidProfile::None;
active_hid_rpt_map = nullptr;
active_hid_rpt_map_len = 0;
// Clear hid_conn_handle in the child device driver data if still alive.
ble_hid_set_conn_handle(ctx->hid_device_child, BLE_HS_CONN_HANDLE_NONE);
if (ctx->midi_keepalive_timer != nullptr) {
esp_timer_stop(ctx->midi_keepalive_timer);
esp_timer_delete(ctx->midi_keepalive_timer);
ctx->midi_keepalive_timer = nullptr;
}
if (ctx->adv_restart_timer != nullptr) {
esp_timer_stop(ctx->adv_restart_timer);
esp_timer_delete(ctx->adv_restart_timer);
ctx->adv_restart_timer = nullptr;
}
ctx->radio_state.store(BT_RADIO_STATE_OFF);
{
struct BtEvent e = {};
e.type = BT_EVENT_RADIO_STATE_CHANGED;
e.radio_state = BT_RADIO_STATE_OFF;
ble_publish_event(ctx->device, e);
}
}
// ---- BluetoothApi implementations ----
static error_t api_get_radio_state(struct Device* device, enum BtRadioState* state) {
BleCtx* ctx = (BleCtx*)device_get_driver_data(device);
if (!ctx || !state) return ERROR_INVALID_ARGUMENT;
*state = ctx->radio_state.load();
return ERROR_NONE;
}
static error_t api_set_radio_enabled(struct Device* device, bool enabled) {
BleCtx* ctx = (BleCtx*)device_get_driver_data(device);
if (!ctx) return ERROR_INVALID_STATE;
xSemaphoreTake(ctx->radio_mutex, portMAX_DELAY);
if (enabled) {
dispatch_enable(ctx);
} else {
dispatch_disable(ctx);
}
xSemaphoreGive(ctx->radio_mutex);
return ERROR_NONE;
}
static error_t api_scan_start(struct Device* device) {
BleCtx* ctx = (BleCtx*)device_get_driver_data(device);
if (!ctx) return ERROR_INVALID_STATE;
if (ctx->radio_state.load() != BT_RADIO_STATE_ON) {
LOG_W(TAG, "scan_start: radio not on");
return ERROR_INVALID_STATE;
}
if (ctx->scan_active.load()) {
LOG_W(TAG, "scan_start: already scanning");
return ERROR_INVALID_STATE;
}
ble_scan_clear_results(device);
struct ble_gap_disc_params disc_params = {};
disc_params.passive = 0;
disc_params.filter_duplicates = 1;
uint8_t own_addr_type;
ble_hs_id_infer_auto(0, &own_addr_type);
int rc = ble_gap_disc(own_addr_type, 5000, &disc_params, ble_gap_disc_event_handler, device);
if (rc != 0 && rc != BLE_HS_EALREADY) {
LOG_E(TAG, "ble_gap_disc failed (rc=%d)", rc);
return ERROR_UNDEFINED;
}
ctx->scan_active.store(true);
{
struct BtEvent e = {};
e.type = BT_EVENT_SCAN_STARTED;
ble_publish_event(device, e);
}
return ERROR_NONE;
}
static error_t api_scan_stop(struct Device* device) {
BleCtx* ctx = (BleCtx*)device_get_driver_data(device);
if (!ctx) return ERROR_INVALID_STATE;
ble_gap_disc_cancel();
ctx->scan_active.store(false);
struct BtEvent e = {};
e.type = BT_EVENT_SCAN_FINISHED;
ble_publish_event(device, e);
return ERROR_NONE;
}
static bool api_is_scanning(struct Device* device) {
BleCtx* ctx = (BleCtx*)device_get_driver_data(device);
return ctx != nullptr && ctx->scan_active.load();
}
static error_t api_pair(struct Device* /*device*/, const BtAddr /*addr*/) {
// Pairing handled automatically by NimBLE SM during connection.
// ENC_CHANGE fires when encryption is established (PAIR_RESULT event).
return ERROR_NONE;
}
static error_t api_unpair(struct Device* device, const BtAddr addr) {
// Remove from NimBLE bond store. Settings file removal is handled by
// the Tactility bluetooth::unpair() wrapper which calls settings::remove().
ble_addr_t ble_addr = {};
ble_addr.type = BLE_ADDR_PUBLIC;
memcpy(ble_addr.val, addr, BT_ADDR_LEN);
ble_store_util_delete_peer(&ble_addr);
return ERROR_NONE;
}
static error_t api_get_paired_peers(struct Device* /*device*/, struct BtPeerRecord* /*out*/, size_t* /*count*/) {
// Paired peer enumeration reads filesystem — handled in Tactility layer.
return ERROR_NOT_SUPPORTED;
}
static error_t api_connect(struct Device* device, const BtAddr addr, enum BtProfileId profile) {
BleCtx* ctx = (BleCtx*)device_get_driver_data(device);
if (!ctx) return ERROR_INVALID_STATE;
if (profile == BT_PROFILE_HID_DEVICE) {
return nimble_hid_device_api.start(ctx->hid_device_child, BT_HID_DEVICE_MODE_KEYBOARD);
} else if (profile == BT_PROFILE_SPP) {
return ble_spp_start_internal(ctx->serial_child);
} else if (profile == BT_PROFILE_MIDI) {
return ble_midi_start_internal(ctx->midi_child);
}
// BT_PROFILE_HID_HOST is handled entirely in the Tactility layer.
return ERROR_NOT_SUPPORTED;
}
static error_t api_disconnect(struct Device* device, const BtAddr addr, enum BtProfileId profile) {
BleCtx* ctx = (BleCtx*)device_get_driver_data(device);
if (!ctx) return ERROR_INVALID_STATE;
if (profile == BT_PROFILE_HID_DEVICE) {
return nimble_hid_device_api.stop(ctx->hid_device_child);
} else if (profile == BT_PROFILE_SPP) {
return nimble_serial_api.stop(ctx->serial_child);
} else if (profile == BT_PROFILE_MIDI) {
return nimble_midi_api.stop(ctx->midi_child);
}
return ERROR_NOT_SUPPORTED;
}
static error_t api_add_event_callback(struct Device* device, void* cb_ctx, BtEventCallback fn) {
BleCtx* ctx = (BleCtx*)device_get_driver_data(device);
if (!ctx || !fn) return ERROR_INVALID_ARGUMENT;
xSemaphoreTake(ctx->cb_mutex, portMAX_DELAY);
if (ctx->callback_count >= BLE_MAX_CALLBACKS) {
xSemaphoreGive(ctx->cb_mutex);
return ERROR_OUT_OF_MEMORY;
}
ctx->callbacks[ctx->callback_count].fn = fn;
ctx->callbacks[ctx->callback_count].ctx = cb_ctx;
ctx->callback_count++;
xSemaphoreGive(ctx->cb_mutex);
return ERROR_NONE;
}
static error_t api_remove_event_callback(struct Device* device, BtEventCallback fn) {
BleCtx* ctx = (BleCtx*)device_get_driver_data(device);
if (!ctx || !fn) return ERROR_INVALID_ARGUMENT;
xSemaphoreTake(ctx->cb_mutex, portMAX_DELAY);
for (size_t i = 0; i < ctx->callback_count; i++) {
if (ctx->callbacks[i].fn == fn) {
// Shift remaining entries down
for (size_t j = i; j + 1 < ctx->callback_count; j++) {
ctx->callbacks[j] = ctx->callbacks[j + 1];
}
ctx->callback_count--;
xSemaphoreGive(ctx->cb_mutex);
return ERROR_NONE;
}
}
xSemaphoreGive(ctx->cb_mutex);
return ERROR_NOT_FOUND;
}
static error_t api_set_device_name(struct Device* device, const char* name) {
BleCtx* ctx = (BleCtx*)device_get_driver_data(device);
if (!ctx || !name) return ERROR_INVALID_ARGUMENT;
if (strlen(name) > BLE_DEVICE_NAME_MAX) return ERROR_INVALID_ARGUMENT;
xSemaphoreTake(ctx->radio_mutex, portMAX_DELAY);
strncpy(ctx->device_name, name, BLE_DEVICE_NAME_MAX);
ctx->device_name[BLE_DEVICE_NAME_MAX] = '\0';
if (ctx->radio_state.load() == BT_RADIO_STATE_ON) {
ble_svc_gap_device_name_set(ctx->device_name);
// Restart advertising so the new name is broadcast immediately.
// ble_schedule_adv_restart checks active profiles; no-op if nothing is advertising.
ble_schedule_adv_restart(device, 0);
}
xSemaphoreGive(ctx->radio_mutex);
return ERROR_NONE;
}
static error_t api_get_device_name(struct Device* device, char* buf, size_t buf_len) {
BleCtx* ctx = (BleCtx*)device_get_driver_data(device);
if (!ctx || !buf || buf_len == 0) return ERROR_INVALID_ARGUMENT;
xSemaphoreTake(ctx->radio_mutex, portMAX_DELAY);
strncpy(buf, ctx->device_name, buf_len - 1);
buf[buf_len - 1] = '\0';
xSemaphoreGive(ctx->radio_mutex);
return ERROR_NONE;
}
static void api_set_hid_host_active(struct Device* device, bool active) {
BleCtx* ctx = (BleCtx*)device_get_driver_data(device);
if (ctx) ctx->hid_host_active.store(active);
}
static void api_fire_event(struct Device* device, struct BtEvent event) {
BleCtx* ctx = (BleCtx*)device_get_driver_data(device);
if (ctx) ble_publish_event(device, event);
}
// ---- BluetoothApi struct ----
const BluetoothApi nimble_bluetooth_api = {
.get_radio_state = api_get_radio_state,
.set_radio_enabled = api_set_radio_enabled,
.scan_start = api_scan_start,
.scan_stop = api_scan_stop,
.is_scanning = api_is_scanning,
.pair = api_pair,
.unpair = api_unpair,
.get_paired_peers = api_get_paired_peers,
.connect = api_connect,
.disconnect = api_disconnect,
.add_event_callback = api_add_event_callback,
.remove_event_callback = api_remove_event_callback,
.set_device_name = api_set_device_name,
.get_device_name = api_get_device_name,
.set_hid_host_active = api_set_hid_host_active,
.fire_event = api_fire_event,
};
// ---- Driver definitions ----
// Child driver structs (esp32_ble_serial_driver, esp32_ble_midi_driver,
// esp32_ble_hid_device_driver) are defined in their respective sub-module files
// and declared extern in esp32_ble_internal.h.
// ---- Driver lifecycle ----
static void create_child_device(struct Device* parent, const char* name,
Driver* drv, struct Device*& out) {
out = new Device { .name = name, .config = nullptr, .parent = nullptr, .internal = nullptr };
device_construct(out);
device_set_parent(out, parent);
device_set_driver(out, drv);
device_add(out);
device_start(out);
}
static void destroy_child_device(struct Device*& child) {
if (child == nullptr) return;
device_stop(child);
device_remove(child);
device_destruct(child);
delete child;
child = nullptr;
}
static error_t esp32_ble_start_device(struct Device* device) {
BleCtx* ctx = new BleCtx();
ctx->radio_mutex = xSemaphoreCreateMutex();
ctx->cb_mutex = xSemaphoreCreateMutex();
ctx->radio_state.store(BT_RADIO_STATE_OFF);
ctx->scan_active.store(false);
ctx->hid_host_active.store(false);
ctx->callback_count = 0;
ctx->spp_conn_handle.store(BLE_HS_CONN_HANDLE_NONE);
ctx->spp_active.store(false);
ctx->midi_conn_handle.store(BLE_HS_CONN_HANDLE_NONE);
ctx->midi_active.store(false);
ctx->midi_use_indicate.store(false);
ctx->hid_active.store(false);
ctx->link_encrypted.store(false);
ctx->pending_reset_count.store(0);
ctx->midi_keepalive_timer = nullptr;
ctx->adv_restart_timer = nullptr;
ctx->disable_timer = nullptr;
// Device name: prefer the Kconfig-supplied TT_DEVICE_NAME, fall back to "Tactility"
const char* cfg_name = CONFIG_TT_DEVICE_NAME;
if (cfg_name == nullptr || cfg_name[0] == '\0') cfg_name = "Tactility";
strncpy(ctx->device_name, cfg_name, BLE_DEVICE_NAME_MAX);
ctx->device_name[BLE_DEVICE_NAME_MAX] = '\0';
ctx->device = device;
ctx->serial_child = nullptr;
ctx->midi_child = nullptr;
ctx->hid_device_child = nullptr;
ctx->scan_mutex = xSemaphoreCreateMutex();
ctx->scan_count = 0;
memset(ctx->scan_results, 0, sizeof(ctx->scan_results));
// Create the disable timer used to dispatch dispatchDisable off the NimBLE host task.
esp_timer_create_args_t disable_args = {};
disable_args.callback = dispatch_disable_timer_cb;
disable_args.arg = ctx;
disable_args.dispatch_method = ESP_TIMER_TASK;
disable_args.name = "ble_disable";
int rc = esp_timer_create(&disable_args, &ctx->disable_timer);
if (rc != ESP_OK) {
LOG_E(TAG, "start_device: disable timer create failed (rc=%d)", rc);
}
device_set_driver_data(device, ctx);
s_ctx = ctx;
// Create child devices for the serial, MIDI and HID device profiles.
// device_start() on each child will invoke start_device (for serial/midi)
// which initialises their driver data (BleSppCtx / BleMidiCtx).
create_child_device(device, "ble_serial", &esp32_ble_serial_driver, ctx->serial_child);
create_child_device(device, "ble_midi", &esp32_ble_midi_driver, ctx->midi_child);
create_child_device(device, "ble_hid_device", &esp32_ble_hid_device_driver, ctx->hid_device_child);
return ERROR_NONE;
}
static error_t esp32_ble_stop_device(struct Device* device) {
BleCtx* ctx = (BleCtx*)device_get_driver_data(device);
if (!ctx) return ERROR_NONE;
// Destroy child devices before stopping the radio and freeing the context.
// device_stop() on each child will invoke stop_device (for serial/midi/hid)
// which frees their driver data.
destroy_child_device(ctx->hid_device_child);
destroy_child_device(ctx->midi_child);
destroy_child_device(ctx->serial_child);
if (ctx->radio_state.load() != BT_RADIO_STATE_OFF) {
dispatch_disable(ctx);
}
if (ctx->scan_mutex != nullptr) {
vSemaphoreDelete(ctx->scan_mutex);
ctx->scan_mutex = nullptr;
}
if (ctx->disable_timer != nullptr) {
esp_timer_stop(ctx->disable_timer);
esp_timer_delete(ctx->disable_timer);
ctx->disable_timer = nullptr;
}
s_ctx = nullptr;
device_set_driver_data(device, nullptr);
delete ctx;
return ERROR_NONE;
}
// ---- Driver registration ----
Driver esp32_bluetooth_driver = {
.name = "esp32_bluetooth",
.compatible = (const char*[]) { "espressif,esp32-ble", nullptr },
.start_device = esp32_ble_start_device,
.stop_device = esp32_ble_stop_device,
.api = &nimble_bluetooth_api,
.device_type = &BLUETOOTH_TYPE,
.owner = nullptr,
.internal = nullptr,
};
#endif // CONFIG_BT_NIMBLE_ENABLED