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
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
// ble_hci_gate.c — Strong override of the weak hci_rx_handler from vhci_drv.c.
|
||||
// Gates HCI packet delivery so packets are silently dropped when NimBLE is not
|
||||
// initialised, preventing null-npl_funcs crashes during radio teardown (P4 only).
|
||||
//
|
||||
// On ESP32-P4, sdio_process_rx_task runs independently of the NimBLE host and can
|
||||
// deliver an HCI packet after nimble_port_deinit() has zeroed npl_funcs.
|
||||
// dispatch_disable() calls ble_hci_gate_set_active(false) + ble_hci_gate_wait_idle()
|
||||
// before touching NimBLE, ensuring no in-flight or future hci_rx_handler call can
|
||||
// dereference NimBLE internals.
|
||||
|
||||
#include <sdkconfig.h>
|
||||
#if defined(CONFIG_BT_NIMBLE_ENABLED) && defined(CONFIG_ESP_HOSTED_ENABLED)
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdatomic.h>
|
||||
#include <string.h>
|
||||
#include <esp_log.h>
|
||||
#include <esp_err.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
// NimBLE transport + HCI
|
||||
#include <nimble/transport.h>
|
||||
#include <nimble/hci_common.h>
|
||||
#include <os/os_mbuf.h>
|
||||
|
||||
// H4 packet type indicators (same values used in vhci_drv.c)
|
||||
#define HCI_H4_EVT 0x04
|
||||
#define HCI_H4_ACL 0x02
|
||||
|
||||
// Local defines from vhci_drv.c (not exported by any header)
|
||||
#define BLE_HCI_EVENT_HDR_LEN (2)
|
||||
|
||||
static const char* TAG = "ble_hci_gate";
|
||||
|
||||
// ---- Gate state (zero-init = gate closed until dispatch_enable opens it) ----
|
||||
static atomic_bool s_hci_active = ATOMIC_VAR_INIT(false);
|
||||
static atomic_int s_hci_refcount = ATOMIC_VAR_INIT(0);
|
||||
|
||||
void ble_hci_gate_set_active(bool active) {
|
||||
atomic_store_explicit(&s_hci_active, active, memory_order_seq_cst);
|
||||
}
|
||||
|
||||
// Spin-wait until all in-flight hci_rx_handler calls complete (max_ms timeout).
|
||||
// Returns true if drained within the timeout.
|
||||
bool ble_hci_gate_wait_idle(int max_ms) {
|
||||
for (int elapsed = 0; elapsed < max_ms; elapsed++) {
|
||||
if (atomic_load_explicit(&s_hci_refcount, memory_order_acquire) == 0)
|
||||
return true;
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
}
|
||||
return (atomic_load_explicit(&s_hci_refcount, memory_order_acquire) == 0);
|
||||
}
|
||||
|
||||
// Strong override — replaces the H_WEAK_REF hci_rx_handler in vhci_drv.c.
|
||||
// All four esp-hosted transport drivers (SDIO, SPI, SPI-HD, UART) call this symbol.
|
||||
int hci_rx_handler(uint8_t *buf, size_t buf_len) {
|
||||
// Fast path: gate closed — NimBLE not ready, drop silently
|
||||
if (!atomic_load_explicit(&s_hci_active, memory_order_acquire))
|
||||
return ESP_OK;
|
||||
|
||||
// Hold a reference so dispatch_disable() waits for us to finish
|
||||
atomic_fetch_add_explicit(&s_hci_refcount, 1, memory_order_acquire);
|
||||
|
||||
// Double-check: gate may have closed between the first check and the increment
|
||||
if (!atomic_load_explicit(&s_hci_active, memory_order_acquire)) {
|
||||
atomic_fetch_sub_explicit(&s_hci_refcount, 1, memory_order_release);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// ---- Original vhci_drv.c (NimBLE branch) logic ----
|
||||
uint8_t *data = buf;
|
||||
uint32_t len_total_read = buf_len;
|
||||
int rc;
|
||||
|
||||
if (data[0] == HCI_H4_EVT) {
|
||||
uint8_t *evbuf;
|
||||
int totlen = BLE_HCI_EVENT_HDR_LEN + data[2];
|
||||
|
||||
if (totlen > UINT8_MAX + BLE_HCI_EVENT_HDR_LEN) {
|
||||
ESP_LOGE(TAG, "Rx: len[%d] > max INT, drop", totlen);
|
||||
atomic_fetch_sub_explicit(&s_hci_refcount, 1, memory_order_release);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
if (totlen > MYNEWT_VAL(BLE_TRANSPORT_EVT_SIZE)) {
|
||||
ESP_LOGE(TAG, "Rx: len[%d] > max BLE, drop", totlen);
|
||||
atomic_fetch_sub_explicit(&s_hci_refcount, 1, memory_order_release);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
if (data[1] == BLE_HCI_EVCODE_HW_ERROR) {
|
||||
ESP_LOGE(TAG, "Rx: HW_ERROR");
|
||||
atomic_fetch_sub_explicit(&s_hci_refcount, 1, memory_order_release);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
if ((data[1] == BLE_HCI_EVCODE_LE_META) &&
|
||||
(data[3] == BLE_HCI_LE_SUBEV_ADV_RPT ||
|
||||
data[3] == BLE_HCI_LE_SUBEV_EXT_ADV_RPT)) {
|
||||
evbuf = ble_transport_alloc_evt(1);
|
||||
if (!evbuf) {
|
||||
ESP_LOGW(TAG, "Rx: Drop ADV Report: OOM (not fatal)");
|
||||
atomic_fetch_sub_explicit(&s_hci_refcount, 1, memory_order_release);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
} else {
|
||||
evbuf = ble_transport_alloc_evt(0);
|
||||
if (!evbuf) {
|
||||
ESP_LOGE(TAG, "Rx: transport_alloc_evt(0) failed");
|
||||
atomic_fetch_sub_explicit(&s_hci_refcount, 1, memory_order_release);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
}
|
||||
|
||||
memset(evbuf, 0, sizeof *evbuf);
|
||||
memcpy(evbuf, &data[1], totlen);
|
||||
rc = ble_transport_to_hs_evt(evbuf);
|
||||
if (rc) {
|
||||
ESP_LOGE(TAG, "Rx: transport_to_hs_evt failed");
|
||||
atomic_fetch_sub_explicit(&s_hci_refcount, 1, memory_order_release);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
} else if (data[0] == HCI_H4_ACL) {
|
||||
struct os_mbuf *m = ble_transport_alloc_acl_from_ll();
|
||||
if (!m) {
|
||||
ESP_LOGE(TAG, "Rx: alloc_acl_from_ll failed");
|
||||
atomic_fetch_sub_explicit(&s_hci_refcount, 1, memory_order_release);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
if ((rc = os_mbuf_append(m, &data[1], len_total_read - 1)) != 0) {
|
||||
ESP_LOGE(TAG, "Rx: os_mbuf_append failed; rc=%d", rc);
|
||||
os_mbuf_free_chain(m);
|
||||
atomic_fetch_sub_explicit(&s_hci_refcount, 1, memory_order_release);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
ble_transport_to_hs_acl(m);
|
||||
}
|
||||
|
||||
atomic_fetch_sub_explicit(&s_hci_refcount, 1, memory_order_release);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
#endif // CONFIG_BT_NIMBLE_ENABLED && CONFIG_ESP_HOSTED_ENABLED
|
||||
@@ -0,0 +1,45 @@
|
||||
@startuml Bluetooth Thread Model
|
||||
|
||||
box "nimble_host task\n(NimBLE internal, 4 KB stack)" #LightBlue
|
||||
participant "gap_event_handler\n(esp32_ble.cpp)" as GAP
|
||||
participant "ble_gatt\ncallbacks" as GATT
|
||||
participant "bt_event_bridge\n(Bluetooth.cpp)" as Bridge
|
||||
participant "hidHostGapCb\n& discovery\n(BluetoothHidHost.cpp)" as HID
|
||||
end box
|
||||
|
||||
box "main_dispatcher task\n(Tactility main task)" #LightGreen
|
||||
participant "settings I/O\n& peer updates" as Dispatch
|
||||
end box
|
||||
|
||||
box "App tasks" #LightYellow
|
||||
participant "BtManage\nBtPeerSettings" as Apps
|
||||
end box
|
||||
|
||||
box "esp_timer task" #LightGray
|
||||
participant "advRestart\nmidiKeepalive\nhidEncRetry" as Timers
|
||||
end box
|
||||
|
||||
box "LVGL task\n(GuiService)" #MistyRose
|
||||
participant "hidHostKeyboard\nReadCb / mouseReadCb" as LVGL
|
||||
end box
|
||||
|
||||
GAP -> Bridge : ble_publish_event() →\nBtEventCallback (bt_event_bridge)
|
||||
Bridge -> Dispatch : getMainDispatcher().dispatch()\n(settings::load/save, autoConnect)
|
||||
Dispatch -> Apps : publishEventCpp → PubSub callbacks
|
||||
GATT -> GAP : GATT access within\nnimble_host task
|
||||
HID -> Dispatch : getMainDispatcher().dispatch()\n(indev cleanup, ProfileStateChanged)
|
||||
Timers -> GAP : advRestartCallback\n(calls ble_start_advertising)
|
||||
LVGL -> HID : lv_indev read callbacks\n(LVGL tick)
|
||||
|
||||
note over GAP, GATT
|
||||
NO file I/O on NimBLE host task —
|
||||
stringstream blows the 4 KB stack
|
||||
end note
|
||||
|
||||
note over GAP, Bridge
|
||||
Driver fires BtEvent via callback array.
|
||||
Bridge (Tactility layer) translates to
|
||||
C++ PubSub<BtEvent> and dispatches I/O.
|
||||
end note
|
||||
|
||||
@enduml
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,589 @@
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <sdkconfig.h>
|
||||
#endif
|
||||
|
||||
#if defined(CONFIG_BT_NIMBLE_ENABLED)
|
||||
|
||||
#include <bluetooth/esp32_ble_internal.h>
|
||||
|
||||
#include <host/ble_gap.h>
|
||||
#include <host/ble_gatt.h>
|
||||
#include <host/ble_hs_mbuf.h>
|
||||
#include <services/gap/ble_svc_gap.h>
|
||||
#include <services/gatt/ble_svc_gatt.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
constexpr auto* TAG = "esp32_ble_hid";
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
|
||||
|
||||
// ---- HID device context (stored as driver data of the HID device child) ----
|
||||
|
||||
#include <atomic>
|
||||
|
||||
struct BleHidDeviceCtx {
|
||||
std::atomic<uint16_t> hid_conn_handle;
|
||||
};
|
||||
|
||||
// ---- Module globals ----
|
||||
|
||||
BleHidProfile current_hid_profile = BleHidProfile::None;
|
||||
uint16_t hid_appearance = 0x03C1; // Keyboard (default)
|
||||
const uint8_t* active_hid_rpt_map = nullptr;
|
||||
size_t active_hid_rpt_map_len = 0;
|
||||
|
||||
// GATT attribute handles — written by NimBLE via val_handle pointers below.
|
||||
uint16_t hid_kb_input_handle;
|
||||
uint16_t hid_consumer_input_handle;
|
||||
uint16_t hid_mouse_input_handle;
|
||||
uint16_t hid_gamepad_input_handle;
|
||||
|
||||
// ---- Static UUID objects for HID 16-bit UUIDs ----
|
||||
|
||||
static const ble_uuid16_t UUID16_RPT_REF = BLE_UUID16_INIT(0x2908);
|
||||
static const ble_uuid16_t UUID16_HID_INFO = BLE_UUID16_INIT(0x2A4A);
|
||||
static const ble_uuid16_t UUID16_RPT_MAP = BLE_UUID16_INIT(0x2A4B);
|
||||
static const ble_uuid16_t UUID16_HID_CTRL = BLE_UUID16_INIT(0x2A4C);
|
||||
static const ble_uuid16_t UUID16_HID_REPORT = BLE_UUID16_INIT(0x2A4D);
|
||||
static const ble_uuid16_t UUID16_PROTO_MODE = BLE_UUID16_INIT(0x2A4E);
|
||||
static const ble_uuid16_t UUID16_HID_SVC = BLE_UUID16_INIT(0x1812);
|
||||
|
||||
static uint8_t hid_protocol_mode = 0x01; // 0x00=Boot, 0x01=Report
|
||||
|
||||
// ============================================================================
|
||||
// Per-profile HID Report Maps
|
||||
// ============================================================================
|
||||
|
||||
// Keyboard + Consumer (IDs 1 and 2)
|
||||
static const uint8_t hid_rpt_map_kb_consumer[] = {
|
||||
0x05, 0x01, 0x09, 0x06, 0xA1, 0x01,
|
||||
0x85, 0x01,
|
||||
0x05, 0x07, 0x19, 0xE0, 0x29, 0xE7, 0x15, 0x00, 0x25, 0x01,
|
||||
0x75, 0x01, 0x95, 0x08, 0x81, 0x02,
|
||||
0x75, 0x08, 0x95, 0x01, 0x81, 0x01,
|
||||
0x05, 0x08, 0x19, 0x01, 0x29, 0x05, 0x75, 0x01, 0x95, 0x05, 0x91, 0x02,
|
||||
0x75, 0x03, 0x95, 0x01, 0x91, 0x01,
|
||||
0x15, 0x00, 0x25, 0x73, 0x05, 0x07, 0x19, 0x00, 0x29, 0x73,
|
||||
0x75, 0x08, 0x95, 0x06, 0x81, 0x00,
|
||||
0xC0,
|
||||
0x05, 0x0C, 0x09, 0x01, 0xA1, 0x01,
|
||||
0x85, 0x02,
|
||||
0x15, 0x00, 0x26, 0xFF, 0x03, 0x19, 0x00, 0x2A, 0xFF, 0x03,
|
||||
0x75, 0x10, 0x95, 0x01, 0x81, 0x00,
|
||||
0xC0,
|
||||
};
|
||||
|
||||
// Mouse only (ID 1, 4 bytes)
|
||||
static const uint8_t hid_rpt_map_mouse[] = {
|
||||
0x05, 0x01, 0x09, 0x02, 0xA1, 0x01,
|
||||
0x85, 0x01,
|
||||
0x09, 0x01, 0xA1, 0x00,
|
||||
0x05, 0x09, 0x19, 0x01, 0x29, 0x05,
|
||||
0x15, 0x00, 0x25, 0x01, 0x95, 0x05, 0x75, 0x01, 0x81, 0x02,
|
||||
0x95, 0x01, 0x75, 0x03, 0x81, 0x01,
|
||||
0x05, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x38,
|
||||
0x15, 0x81, 0x25, 0x7F, 0x75, 0x08, 0x95, 0x03, 0x81, 0x06,
|
||||
0xC0, 0xC0,
|
||||
};
|
||||
|
||||
// Keyboard + Consumer + Mouse (IDs 1, 2, 3)
|
||||
static const uint8_t hid_rpt_map_kb_mouse[] = {
|
||||
0x05, 0x01, 0x09, 0x06, 0xA1, 0x01,
|
||||
0x85, 0x01,
|
||||
0x05, 0x07, 0x19, 0xE0, 0x29, 0xE7, 0x15, 0x00, 0x25, 0x01,
|
||||
0x75, 0x01, 0x95, 0x08, 0x81, 0x02,
|
||||
0x75, 0x08, 0x95, 0x01, 0x81, 0x01,
|
||||
0x05, 0x08, 0x19, 0x01, 0x29, 0x05, 0x75, 0x01, 0x95, 0x05, 0x91, 0x02,
|
||||
0x75, 0x03, 0x95, 0x01, 0x91, 0x01,
|
||||
0x15, 0x00, 0x25, 0x73, 0x05, 0x07, 0x19, 0x00, 0x29, 0x73,
|
||||
0x75, 0x08, 0x95, 0x06, 0x81, 0x00,
|
||||
0xC0,
|
||||
0x05, 0x0C, 0x09, 0x01, 0xA1, 0x01,
|
||||
0x85, 0x02,
|
||||
0x15, 0x00, 0x26, 0xFF, 0x03, 0x19, 0x00, 0x2A, 0xFF, 0x03,
|
||||
0x75, 0x10, 0x95, 0x01, 0x81, 0x00,
|
||||
0xC0,
|
||||
0x05, 0x01, 0x09, 0x02, 0xA1, 0x01,
|
||||
0x85, 0x03,
|
||||
0x09, 0x01, 0xA1, 0x00,
|
||||
0x05, 0x09, 0x19, 0x01, 0x29, 0x05,
|
||||
0x15, 0x00, 0x25, 0x01, 0x95, 0x05, 0x75, 0x01, 0x81, 0x02,
|
||||
0x95, 0x01, 0x75, 0x03, 0x81, 0x01,
|
||||
0x05, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x38,
|
||||
0x15, 0x81, 0x25, 0x7F, 0x75, 0x08, 0x95, 0x03, 0x81, 0x06,
|
||||
0xC0, 0xC0,
|
||||
};
|
||||
|
||||
// Gamepad only (ID 1, 8 bytes)
|
||||
static const uint8_t hid_rpt_map_gamepad[] = {
|
||||
0x05, 0x01, 0x09, 0x05, 0xA1, 0x01,
|
||||
0x85, 0x01,
|
||||
0x05, 0x09, 0x19, 0x01, 0x29, 0x10,
|
||||
0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x10, 0x81, 0x02,
|
||||
0x05, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x32, 0x09, 0x35, 0x09, 0x33, 0x09, 0x34,
|
||||
0x15, 0x81, 0x25, 0x7F, 0x75, 0x08, 0x95, 0x06, 0x81, 0x02,
|
||||
0xC0,
|
||||
};
|
||||
|
||||
// ---- Per-profile Report Reference descriptor data ----
|
||||
// Format: {Report ID, Report Type} — Type: 1=Input, 2=Output
|
||||
|
||||
static const uint8_t rpt_ref_kbc_kb_in[2] = {1, 1};
|
||||
static const uint8_t rpt_ref_kbc_cs_in[2] = {2, 1};
|
||||
static const uint8_t rpt_ref_kbc_kb_out[2] = {1, 2};
|
||||
static const uint8_t rpt_ref_ms_in[2] = {1, 1};
|
||||
static const uint8_t rpt_ref_kbm_kb_in[2] = {1, 1};
|
||||
static const uint8_t rpt_ref_kbm_cs_in[2] = {2, 1};
|
||||
static const uint8_t rpt_ref_kbm_ms_in[2] = {3, 1};
|
||||
static const uint8_t rpt_ref_kbm_kb_out[2] = {1, 2};
|
||||
static const uint8_t rpt_ref_gp_in[2] = {1, 1};
|
||||
|
||||
// ---- HID GATT callbacks ----
|
||||
|
||||
static int hid_dsc_access(uint16_t /*conn_handle*/, uint16_t /*attr_handle*/,
|
||||
struct ble_gatt_access_ctxt* ctxt, void* arg) {
|
||||
if (ctxt->op == BLE_GATT_ACCESS_OP_READ_DSC) {
|
||||
const uint8_t* data = (const uint8_t*)arg;
|
||||
int rc = os_mbuf_append(ctxt->om, data, 2);
|
||||
return (rc == 0) ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
|
||||
}
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
|
||||
static int hid_chr_access(uint16_t /*conn_handle*/, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt* ctxt, void* /*arg*/) {
|
||||
uint16_t uuid16 = ble_uuid_u16(ctxt->chr->uuid);
|
||||
|
||||
switch (ctxt->op) {
|
||||
case BLE_GATT_ACCESS_OP_READ_CHR: {
|
||||
if (uuid16 == 0x2A4A) {
|
||||
static const uint8_t hid_info[4] = { 0x11, 0x01, 0x00, 0x02 };
|
||||
int rc = os_mbuf_append(ctxt->om, hid_info, sizeof(hid_info));
|
||||
return (rc == 0) ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
|
||||
}
|
||||
if (uuid16 == 0x2A4B) {
|
||||
if (active_hid_rpt_map == nullptr || active_hid_rpt_map_len == 0)
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
int rc = os_mbuf_append(ctxt->om, active_hid_rpt_map, active_hid_rpt_map_len);
|
||||
return (rc == 0) ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
|
||||
}
|
||||
if (uuid16 == 0x2A4E) {
|
||||
int rc = os_mbuf_append(ctxt->om, &hid_protocol_mode, 1);
|
||||
return (rc == 0) ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
|
||||
}
|
||||
if (uuid16 == 0x2A4D) {
|
||||
static const uint8_t zeros[8] = {};
|
||||
size_t report_len = 8;
|
||||
if (attr_handle == hid_consumer_input_handle) report_len = 2;
|
||||
else if (attr_handle == hid_mouse_input_handle) report_len = 4;
|
||||
int rc = os_mbuf_append(ctxt->om, zeros, report_len);
|
||||
return (rc == 0) ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
|
||||
}
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
|
||||
case BLE_GATT_ACCESS_OP_WRITE_CHR: {
|
||||
if (uuid16 == 0x2A4C) return 0; // HID Control Point — no action
|
||||
if (uuid16 == 0x2A4E) {
|
||||
if (OS_MBUF_PKTLEN(ctxt->om) >= 1) {
|
||||
os_mbuf_copydata(ctxt->om, 0, 1, &hid_protocol_mode);
|
||||
LOG_I(TAG, "Protocol Mode -> %u", hid_protocol_mode);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (uuid16 == 0x2A4D) {
|
||||
if (OS_MBUF_PKTLEN(ctxt->om) >= 1) {
|
||||
uint8_t leds = 0;
|
||||
os_mbuf_copydata(ctxt->om, 0, 1, &leds);
|
||||
LOG_I(TAG, "KB LED state: 0x%02x", leds);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
|
||||
default:
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Per-profile HID descriptor arrays ----
|
||||
|
||||
static struct ble_gatt_dsc_def hid_kbc_kb_dscs[] = { { .uuid = &UUID16_RPT_REF.u, .att_flags = BLE_ATT_F_READ, .access_cb = hid_dsc_access, .arg = (void*)rpt_ref_kbc_kb_in }, { 0 } };
|
||||
static struct ble_gatt_dsc_def hid_kbc_cs_dscs[] = { { .uuid = &UUID16_RPT_REF.u, .att_flags = BLE_ATT_F_READ, .access_cb = hid_dsc_access, .arg = (void*)rpt_ref_kbc_cs_in }, { 0 } };
|
||||
static struct ble_gatt_dsc_def hid_kbc_out_dscs[] = { { .uuid = &UUID16_RPT_REF.u, .att_flags = BLE_ATT_F_READ, .access_cb = hid_dsc_access, .arg = (void*)rpt_ref_kbc_kb_out }, { 0 } };
|
||||
static struct ble_gatt_dsc_def hid_ms_dscs[] = { { .uuid = &UUID16_RPT_REF.u, .att_flags = BLE_ATT_F_READ, .access_cb = hid_dsc_access, .arg = (void*)rpt_ref_ms_in }, { 0 } };
|
||||
static struct ble_gatt_dsc_def hid_kbm_kb_dscs[] = { { .uuid = &UUID16_RPT_REF.u, .att_flags = BLE_ATT_F_READ, .access_cb = hid_dsc_access, .arg = (void*)rpt_ref_kbm_kb_in }, { 0 } };
|
||||
static struct ble_gatt_dsc_def hid_kbm_cs_dscs[] = { { .uuid = &UUID16_RPT_REF.u, .att_flags = BLE_ATT_F_READ, .access_cb = hid_dsc_access, .arg = (void*)rpt_ref_kbm_cs_in }, { 0 } };
|
||||
static struct ble_gatt_dsc_def hid_kbm_ms_dscs[] = { { .uuid = &UUID16_RPT_REF.u, .att_flags = BLE_ATT_F_READ, .access_cb = hid_dsc_access, .arg = (void*)rpt_ref_kbm_ms_in }, { 0 } };
|
||||
static struct ble_gatt_dsc_def hid_kbm_out_dscs[] = { { .uuid = &UUID16_RPT_REF.u, .att_flags = BLE_ATT_F_READ, .access_cb = hid_dsc_access, .arg = (void*)rpt_ref_kbm_kb_out }, { 0 } };
|
||||
static struct ble_gatt_dsc_def hid_gp_dscs[] = { { .uuid = &UUID16_RPT_REF.u, .att_flags = BLE_ATT_F_READ, .access_cb = hid_dsc_access, .arg = (void*)rpt_ref_gp_in }, { 0 } };
|
||||
|
||||
// ---- Per-profile HID characteristic arrays ----
|
||||
|
||||
static struct ble_gatt_chr_def hid_chars_kb_consumer[] = {
|
||||
{ .uuid = &UUID16_HID_INFO.u, .access_cb = hid_chr_access, .flags = BLE_GATT_CHR_F_READ },
|
||||
{ .uuid = &UUID16_RPT_MAP.u, .access_cb = hid_chr_access, .flags = BLE_GATT_CHR_F_READ },
|
||||
{ .uuid = &UUID16_HID_CTRL.u, .access_cb = hid_chr_access, .flags = BLE_GATT_CHR_F_WRITE_NO_RSP },
|
||||
{ .uuid = &UUID16_PROTO_MODE.u, .access_cb = hid_chr_access, .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE_NO_RSP },
|
||||
{ .uuid = &UUID16_HID_REPORT.u, .access_cb = hid_chr_access, .descriptors = hid_kbc_kb_dscs,
|
||||
.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY, .val_handle = &hid_kb_input_handle },
|
||||
{ .uuid = &UUID16_HID_REPORT.u, .access_cb = hid_chr_access, .descriptors = hid_kbc_cs_dscs,
|
||||
.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY, .val_handle = &hid_consumer_input_handle },
|
||||
{ .uuid = &UUID16_HID_REPORT.u, .access_cb = hid_chr_access, .descriptors = hid_kbc_out_dscs,
|
||||
.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_NO_RSP },
|
||||
{ 0 }
|
||||
};
|
||||
|
||||
static struct ble_gatt_chr_def hid_chars_mouse[] = {
|
||||
{ .uuid = &UUID16_HID_INFO.u, .access_cb = hid_chr_access, .flags = BLE_GATT_CHR_F_READ },
|
||||
{ .uuid = &UUID16_RPT_MAP.u, .access_cb = hid_chr_access, .flags = BLE_GATT_CHR_F_READ },
|
||||
{ .uuid = &UUID16_HID_CTRL.u, .access_cb = hid_chr_access, .flags = BLE_GATT_CHR_F_WRITE_NO_RSP },
|
||||
{ .uuid = &UUID16_PROTO_MODE.u, .access_cb = hid_chr_access, .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE_NO_RSP },
|
||||
{ .uuid = &UUID16_HID_REPORT.u, .access_cb = hid_chr_access, .descriptors = hid_ms_dscs,
|
||||
.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY, .val_handle = &hid_mouse_input_handle },
|
||||
{ 0 }
|
||||
};
|
||||
|
||||
static struct ble_gatt_chr_def hid_chars_kb_mouse[] = {
|
||||
{ .uuid = &UUID16_HID_INFO.u, .access_cb = hid_chr_access, .flags = BLE_GATT_CHR_F_READ },
|
||||
{ .uuid = &UUID16_RPT_MAP.u, .access_cb = hid_chr_access, .flags = BLE_GATT_CHR_F_READ },
|
||||
{ .uuid = &UUID16_HID_CTRL.u, .access_cb = hid_chr_access, .flags = BLE_GATT_CHR_F_WRITE_NO_RSP },
|
||||
{ .uuid = &UUID16_PROTO_MODE.u, .access_cb = hid_chr_access, .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE_NO_RSP },
|
||||
{ .uuid = &UUID16_HID_REPORT.u, .access_cb = hid_chr_access, .descriptors = hid_kbm_kb_dscs,
|
||||
.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY, .val_handle = &hid_kb_input_handle },
|
||||
{ .uuid = &UUID16_HID_REPORT.u, .access_cb = hid_chr_access, .descriptors = hid_kbm_cs_dscs,
|
||||
.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY, .val_handle = &hid_consumer_input_handle },
|
||||
{ .uuid = &UUID16_HID_REPORT.u, .access_cb = hid_chr_access, .descriptors = hid_kbm_ms_dscs,
|
||||
.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY, .val_handle = &hid_mouse_input_handle },
|
||||
{ .uuid = &UUID16_HID_REPORT.u, .access_cb = hid_chr_access, .descriptors = hid_kbm_out_dscs,
|
||||
.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_NO_RSP },
|
||||
{ 0 }
|
||||
};
|
||||
|
||||
static struct ble_gatt_chr_def hid_chars_gamepad[] = {
|
||||
{ .uuid = &UUID16_HID_INFO.u, .access_cb = hid_chr_access, .flags = BLE_GATT_CHR_F_READ },
|
||||
{ .uuid = &UUID16_RPT_MAP.u, .access_cb = hid_chr_access, .flags = BLE_GATT_CHR_F_READ },
|
||||
{ .uuid = &UUID16_HID_CTRL.u, .access_cb = hid_chr_access, .flags = BLE_GATT_CHR_F_WRITE_NO_RSP },
|
||||
{ .uuid = &UUID16_PROTO_MODE.u, .access_cb = hid_chr_access, .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE_NO_RSP },
|
||||
{ .uuid = &UUID16_HID_REPORT.u, .access_cb = hid_chr_access, .descriptors = hid_gp_dscs,
|
||||
.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY, .val_handle = &hid_gamepad_input_handle },
|
||||
{ 0 }
|
||||
};
|
||||
|
||||
// ---- Per-profile GATT service arrays ----
|
||||
|
||||
static const struct ble_gatt_svc_def gatt_svcs_none[] = {
|
||||
{ .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &NUS_SVC_UUID.u, .characteristics = nus_chars_with_handle },
|
||||
{ .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &MIDI_SVC_UUID.u, .characteristics = midi_chars },
|
||||
{ 0 }
|
||||
};
|
||||
static const struct ble_gatt_svc_def gatt_svcs_kb_consumer[] = {
|
||||
{ .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &NUS_SVC_UUID.u, .characteristics = nus_chars_with_handle },
|
||||
{ .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &MIDI_SVC_UUID.u, .characteristics = midi_chars },
|
||||
{ .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &UUID16_HID_SVC.u, .characteristics = hid_chars_kb_consumer },
|
||||
{ 0 }
|
||||
};
|
||||
static const struct ble_gatt_svc_def gatt_svcs_mouse[] = {
|
||||
{ .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &NUS_SVC_UUID.u, .characteristics = nus_chars_with_handle },
|
||||
{ .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &MIDI_SVC_UUID.u, .characteristics = midi_chars },
|
||||
{ .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &UUID16_HID_SVC.u, .characteristics = hid_chars_mouse },
|
||||
{ 0 }
|
||||
};
|
||||
static const struct ble_gatt_svc_def gatt_svcs_kb_mouse[] = {
|
||||
{ .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &NUS_SVC_UUID.u, .characteristics = nus_chars_with_handle },
|
||||
{ .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &MIDI_SVC_UUID.u, .characteristics = midi_chars },
|
||||
{ .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &UUID16_HID_SVC.u, .characteristics = hid_chars_kb_mouse },
|
||||
{ 0 }
|
||||
};
|
||||
static const struct ble_gatt_svc_def gatt_svcs_gamepad[] = {
|
||||
{ .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &NUS_SVC_UUID.u, .characteristics = nus_chars_with_handle },
|
||||
{ .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &MIDI_SVC_UUID.u, .characteristics = midi_chars },
|
||||
{ .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &UUID16_HID_SVC.u, .characteristics = hid_chars_gamepad },
|
||||
{ 0 }
|
||||
};
|
||||
|
||||
// ---- HID field accessor implementations ----
|
||||
|
||||
static BleCtx* hid_root_ctx(struct Device* device) {
|
||||
return ble_get_ctx(device);
|
||||
}
|
||||
|
||||
bool ble_hid_get_active(struct Device* device) {
|
||||
return hid_root_ctx(device)->hid_active.load();
|
||||
}
|
||||
void ble_hid_set_active(struct Device* device, bool v) {
|
||||
hid_root_ctx(device)->hid_active.store(v);
|
||||
}
|
||||
uint16_t ble_hid_get_conn_handle(struct Device* device) {
|
||||
if (device == nullptr) return (uint16_t)BLE_HS_CONN_HANDLE_NONE;
|
||||
BleHidDeviceCtx* hid_ctx = (BleHidDeviceCtx*)device_get_driver_data(device);
|
||||
return hid_ctx ? hid_ctx->hid_conn_handle.load() : (uint16_t)BLE_HS_CONN_HANDLE_NONE;
|
||||
}
|
||||
void ble_hid_set_conn_handle(struct Device* device, uint16_t h) {
|
||||
if (device == nullptr) return;
|
||||
BleHidDeviceCtx* hid_ctx = (BleHidDeviceCtx*)device_get_driver_data(device);
|
||||
if (hid_ctx) hid_ctx->hid_conn_handle.store(h);
|
||||
}
|
||||
|
||||
// ---- GATT profile switch ----
|
||||
// device must be the HID device child Device*.
|
||||
|
||||
bool ble_hid_switch_profile(struct Device* device, BleHidProfile profile) {
|
||||
if (profile == current_hid_profile) return true;
|
||||
LOG_I(TAG, "switchGattProfile: %d -> %d", (int)current_hid_profile, (int)profile);
|
||||
|
||||
ble_gap_adv_stop();
|
||||
|
||||
BleHidDeviceCtx* hid_ctx = (BleHidDeviceCtx*)device_get_driver_data(device);
|
||||
if (hid_ctx && hid_ctx->hid_conn_handle.load() != BLE_HS_CONN_HANDLE_NONE) {
|
||||
ble_gap_terminate(hid_ctx->hid_conn_handle.load(), BLE_ERR_REM_USER_CONN_TERM);
|
||||
hid_ctx->hid_conn_handle.store(BLE_HS_CONN_HANDLE_NONE);
|
||||
}
|
||||
|
||||
ble_gatts_reset();
|
||||
ble_svc_gap_init();
|
||||
ble_svc_gatt_init();
|
||||
|
||||
const struct ble_gatt_svc_def* svcs = gatt_svcs_none;
|
||||
const uint8_t* new_rpt_map = nullptr;
|
||||
size_t new_rpt_map_len = 0;
|
||||
switch (profile) {
|
||||
case BleHidProfile::KbConsumer: svcs = gatt_svcs_kb_consumer; new_rpt_map = hid_rpt_map_kb_consumer; new_rpt_map_len = sizeof(hid_rpt_map_kb_consumer); break;
|
||||
case BleHidProfile::Mouse: svcs = gatt_svcs_mouse; new_rpt_map = hid_rpt_map_mouse; new_rpt_map_len = sizeof(hid_rpt_map_mouse); break;
|
||||
case BleHidProfile::KbMouse: svcs = gatt_svcs_kb_mouse; new_rpt_map = hid_rpt_map_kb_mouse; new_rpt_map_len = sizeof(hid_rpt_map_kb_mouse); break;
|
||||
case BleHidProfile::Gamepad: svcs = gatt_svcs_gamepad; new_rpt_map = hid_rpt_map_gamepad; new_rpt_map_len = sizeof(hid_rpt_map_gamepad); break;
|
||||
default: svcs = gatt_svcs_none; break;
|
||||
}
|
||||
|
||||
int rc = ble_gatts_count_cfg(svcs);
|
||||
if (rc == 0) {
|
||||
rc = ble_gatts_add_svcs(svcs);
|
||||
if (rc != 0) {
|
||||
LOG_E(TAG, "switchGattProfile: gatts_add_svcs failed rc=%d", rc);
|
||||
return false; // don't update profile — GATT state is inconsistent
|
||||
}
|
||||
} else {
|
||||
LOG_E(TAG, "switchGattProfile: gatts_count_cfg failed rc=%d", rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ble_gatts_add_svcs() only adds definitions to a pending list.
|
||||
// ble_gatts_start() converts them into live ATT entries.
|
||||
// Without this call, all GATT reads return ATT errors and Windows
|
||||
// cannot install the HID driver → Phase 2 reconnect never occurs.
|
||||
rc = ble_gatts_start();
|
||||
if (rc != 0) {
|
||||
LOG_E(TAG, "switchGattProfile: gatts_start failed rc=%d", rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
active_hid_rpt_map = new_rpt_map;
|
||||
active_hid_rpt_map_len = new_rpt_map_len;
|
||||
|
||||
ble_svc_gap_device_name_set(CONFIG_TT_DEVICE_NAME);
|
||||
ble_att_set_preferred_mtu(BLE_ATT_MTU_MAX);
|
||||
// Do NOT call ble_svc_gatt_changed(0, 0xFFFF) here.
|
||||
// switchGattProfile() is always called while disconnected (ble_gatts_mutable()
|
||||
// requires no active connection). ble_gatts_chr_updated() would persist a
|
||||
// "value_changed=1" flag in NVS for every bonded-but-disconnected peer.
|
||||
// When a bonded peer (e.g. Windows HID host) reconnects, NimBLE immediately
|
||||
// sends a Service Changed indication; Windows disconnects to re-discover GATT
|
||||
// without ACKing the indication; NimBLE re-sends on the next reconnect → loop.
|
||||
// GATT handles are deterministic (same registration order each time), so
|
||||
// bonded peers can reuse their cached handles and no indication is needed.
|
||||
|
||||
current_hid_profile = profile;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ble_hid_init_gatt() {
|
||||
current_hid_profile = BleHidProfile::None;
|
||||
active_hid_rpt_map = nullptr;
|
||||
active_hid_rpt_map_len = 0;
|
||||
int rc = ble_gatts_count_cfg(gatt_svcs_none);
|
||||
if (rc != 0) {
|
||||
LOG_E(TAG, "gatts_count_cfg failed (rc=%d)", rc);
|
||||
} else {
|
||||
rc = ble_gatts_add_svcs(gatt_svcs_none);
|
||||
if (rc != 0) {
|
||||
LOG_E(TAG, "gatts_add_svcs failed (rc=%d)", rc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- HID Device sub-API implementations ----
|
||||
// All functions receive the HID device child Device* and operate on BleHidDeviceCtx
|
||||
// stored as its driver data.
|
||||
|
||||
static error_t hid_device_start(struct Device* device, enum BtHidDeviceMode mode) {
|
||||
// Clean up any leftover context from a previous session (e.g. BLE was disabled
|
||||
// while connected: dispatch_disable() skips the normal hid_device_stop cleanup).
|
||||
BleHidDeviceCtx* old_ctx = (BleHidDeviceCtx*)device_get_driver_data(device);
|
||||
delete old_ctx; // no-op if nullptr
|
||||
// Create driver data for this HID session.
|
||||
BleHidDeviceCtx* hid_ctx = new BleHidDeviceCtx();
|
||||
hid_ctx->hid_conn_handle.store(BLE_HS_CONN_HANDLE_NONE);
|
||||
device_set_driver_data(device, hid_ctx);
|
||||
|
||||
BleHidProfile profile;
|
||||
uint16_t appearance;
|
||||
switch (mode) {
|
||||
case BT_HID_DEVICE_MODE_MOUSE:
|
||||
profile = BleHidProfile::Mouse;
|
||||
appearance = 0x03C2;
|
||||
break;
|
||||
case BT_HID_DEVICE_MODE_KEYBOARD_MOUSE:
|
||||
profile = BleHidProfile::KbMouse;
|
||||
appearance = 0x03C0;
|
||||
break;
|
||||
case BT_HID_DEVICE_MODE_GAMEPAD:
|
||||
profile = BleHidProfile::Gamepad;
|
||||
appearance = 0x03C4;
|
||||
break;
|
||||
default: // BT_HID_DEVICE_MODE_KEYBOARD
|
||||
profile = BleHidProfile::KbConsumer;
|
||||
appearance = 0x03C1;
|
||||
break;
|
||||
}
|
||||
|
||||
hid_appearance = appearance;
|
||||
if (!ble_hid_switch_profile(device, profile)) {
|
||||
delete (BleHidDeviceCtx*)device_get_driver_data(device);
|
||||
device_set_driver_data(device, nullptr);
|
||||
return ERROR_INVALID_STATE;
|
||||
}
|
||||
ble_hid_set_active(device, true);
|
||||
ble_start_advertising_hid(device, hid_appearance);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t hid_device_stop(struct Device* device) {
|
||||
ble_hid_set_active(device, false);
|
||||
ble_gap_adv_stop();
|
||||
BleHidDeviceCtx* hid_ctx = (BleHidDeviceCtx*)device_get_driver_data(device);
|
||||
uint16_t conn = hid_ctx ? hid_ctx->hid_conn_handle.load() : (uint16_t)BLE_HS_CONN_HANDLE_NONE;
|
||||
if (conn != BLE_HS_CONN_HANDLE_NONE) {
|
||||
// Connected: terminate and let the DISCONNECT handler switch profile to None.
|
||||
// ble_gatts_mutable() returns false while a connection is live, so calling
|
||||
// switch_profile here would assert inside ble_svc_gap_init().
|
||||
ble_gap_terminate(conn, BLE_ERR_REM_USER_CONN_TERM);
|
||||
// Do NOT clear hid_conn_handle or delete hid_ctx:
|
||||
// the DISCONNECT handler in esp32_ble.cpp uses hid_conn_handle for was_hid detection.
|
||||
// esp32_ble_hid_device_stop_device() (device lifecycle) will free hid_ctx later.
|
||||
} else {
|
||||
// Not connected: GATT is mutable, switch profile immediately.
|
||||
if (current_hid_profile != BleHidProfile::None) {
|
||||
if (!ble_hid_switch_profile(device, BleHidProfile::None)) {
|
||||
LOG_E(TAG, "hid_device_stop: switch to None failed — GATT state inconsistent");
|
||||
}
|
||||
}
|
||||
delete hid_ctx;
|
||||
device_set_driver_data(device, nullptr);
|
||||
}
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t hid_device_send_key(struct Device* device, uint8_t keycode, bool pressed) {
|
||||
BleHidDeviceCtx* hid_ctx = (BleHidDeviceCtx*)device_get_driver_data(device);
|
||||
if (hid_ctx == nullptr || hid_ctx->hid_conn_handle.load() == BLE_HS_CONN_HANDLE_NONE) {
|
||||
return ERROR_INVALID_STATE;
|
||||
}
|
||||
uint8_t report[8] = {};
|
||||
if (pressed) report[2] = keycode;
|
||||
struct os_mbuf* om = ble_hs_mbuf_from_flat(report, sizeof(report));
|
||||
if (om == nullptr) return ERROR_INVALID_STATE;
|
||||
int rc = ble_gatts_notify_custom(hid_ctx->hid_conn_handle.load(), hid_kb_input_handle, om);
|
||||
if (rc != 0) os_mbuf_free_chain(om);
|
||||
return (rc == 0) ? ERROR_NONE : ERROR_INVALID_STATE;
|
||||
}
|
||||
|
||||
static error_t hid_notify(uint16_t conn_handle, uint16_t attr_handle,
|
||||
const uint8_t* data, size_t len) {
|
||||
if (conn_handle == BLE_HS_CONN_HANDLE_NONE) return ERROR_INVALID_STATE;
|
||||
if (attr_handle == 0) return ERROR_INVALID_STATE; // handle not registered for current profile
|
||||
struct os_mbuf* om = ble_hs_mbuf_from_flat(data, len);
|
||||
if (om == nullptr) return ERROR_INVALID_STATE;
|
||||
int rc = ble_gatts_notify_custom(conn_handle, attr_handle, om);
|
||||
if (rc != 0) os_mbuf_free_chain(om);
|
||||
return (rc == 0) ? ERROR_NONE : ERROR_INVALID_STATE;
|
||||
}
|
||||
|
||||
static error_t hid_device_send_keyboard(struct Device* device, const uint8_t* report, size_t len) {
|
||||
BleHidDeviceCtx* hid_ctx = (BleHidDeviceCtx*)device_get_driver_data(device);
|
||||
if (hid_ctx == nullptr) return ERROR_INVALID_STATE;
|
||||
uint8_t buf[8] = {};
|
||||
memcpy(buf, report, len < sizeof(buf) ? len : sizeof(buf));
|
||||
return hid_notify(hid_ctx->hid_conn_handle.load(), hid_kb_input_handle, buf, sizeof(buf));
|
||||
}
|
||||
|
||||
static error_t hid_device_send_consumer(struct Device* device, const uint8_t* report, size_t len) {
|
||||
BleHidDeviceCtx* hid_ctx = (BleHidDeviceCtx*)device_get_driver_data(device);
|
||||
if (hid_ctx == nullptr) return ERROR_INVALID_STATE;
|
||||
uint8_t buf[2] = {};
|
||||
memcpy(buf, report, len < sizeof(buf) ? len : sizeof(buf));
|
||||
return hid_notify(hid_ctx->hid_conn_handle.load(), hid_consumer_input_handle, buf, sizeof(buf));
|
||||
}
|
||||
|
||||
static error_t hid_device_send_mouse(struct Device* device, const uint8_t* report, size_t len) {
|
||||
BleHidDeviceCtx* hid_ctx = (BleHidDeviceCtx*)device_get_driver_data(device);
|
||||
if (hid_ctx == nullptr) return ERROR_INVALID_STATE;
|
||||
uint8_t buf[4] = {};
|
||||
memcpy(buf, report, len < sizeof(buf) ? len : sizeof(buf));
|
||||
return hid_notify(hid_ctx->hid_conn_handle.load(), hid_mouse_input_handle, buf, sizeof(buf));
|
||||
}
|
||||
|
||||
static error_t hid_device_send_gamepad(struct Device* device, const uint8_t* report, size_t len) {
|
||||
BleHidDeviceCtx* hid_ctx = (BleHidDeviceCtx*)device_get_driver_data(device);
|
||||
if (hid_ctx == nullptr) return ERROR_INVALID_STATE;
|
||||
uint8_t buf[8] = {};
|
||||
memcpy(buf, report, len < sizeof(buf) ? len : sizeof(buf));
|
||||
return hid_notify(hid_ctx->hid_conn_handle.load(), hid_gamepad_input_handle, buf, sizeof(buf));
|
||||
}
|
||||
|
||||
static bool hid_device_is_connected(struct Device* device) {
|
||||
BleHidDeviceCtx* hid_ctx = (BleHidDeviceCtx*)device_get_driver_data(device);
|
||||
return hid_ctx != nullptr && hid_ctx->hid_conn_handle.load() != BLE_HS_CONN_HANDLE_NONE;
|
||||
}
|
||||
|
||||
extern const BtHidDeviceApi nimble_hid_device_api = {
|
||||
.start = hid_device_start,
|
||||
.stop = hid_device_stop,
|
||||
.send_key = hid_device_send_key,
|
||||
.send_keyboard = hid_device_send_keyboard,
|
||||
.send_consumer = hid_device_send_consumer,
|
||||
.send_mouse = hid_device_send_mouse,
|
||||
.send_gamepad = hid_device_send_gamepad,
|
||||
.is_connected = hid_device_is_connected,
|
||||
};
|
||||
|
||||
// ---- HID device child driver lifecycle ----
|
||||
|
||||
static error_t esp32_ble_hid_device_stop_device(struct Device* device) {
|
||||
// Safety cleanup: free any BleHidDeviceCtx that was not deleted by hid_device_stop()
|
||||
// (e.g. if the BLE device is stopped while HID is still connected).
|
||||
BleHidDeviceCtx* hid_ctx = (BleHidDeviceCtx*)device_get_driver_data(device);
|
||||
delete hid_ctx; // safe even if nullptr
|
||||
device_set_driver_data(device, nullptr);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
Driver esp32_ble_hid_device_driver = {
|
||||
.name = "esp32_ble_hid_device",
|
||||
.compatible = nullptr,
|
||||
.start_device = nullptr,
|
||||
.stop_device = esp32_ble_hid_device_stop_device,
|
||||
.api = &nimble_hid_device_api,
|
||||
.device_type = &BLUETOOTH_HID_DEVICE_TYPE,
|
||||
.owner = nullptr,
|
||||
.internal = nullptr,
|
||||
};
|
||||
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
#endif // CONFIG_BT_NIMBLE_ENABLED
|
||||
@@ -0,0 +1,263 @@
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <sdkconfig.h>
|
||||
#endif
|
||||
|
||||
#if defined(CONFIG_BT_NIMBLE_ENABLED)
|
||||
|
||||
#include <bluetooth/esp32_ble_internal.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <vector>
|
||||
#include <host/ble_gap.h>
|
||||
#include <host/ble_gatt.h>
|
||||
#include <host/ble_hs_mbuf.h>
|
||||
|
||||
constexpr auto* TAG = "esp32_ble_midi";
|
||||
#include <esp_timer.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
// ---- MIDI device context (stored as driver data of the MIDI child device) ----
|
||||
|
||||
struct BleMidiCtx {
|
||||
SemaphoreHandle_t rx_mutex;
|
||||
std::deque<std::vector<uint8_t>> rx_queue;
|
||||
};
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
|
||||
|
||||
// ---- BLE MIDI UUIDs ----
|
||||
|
||||
// 03B80E5A-EDE8-4B33-A751-6CE34EC4C700
|
||||
const ble_uuid128_t MIDI_SVC_UUID = BLE_UUID128_INIT(
|
||||
0x00, 0xC7, 0xC4, 0x4E, 0xE3, 0x6C, 0x51, 0xA7,
|
||||
0x33, 0x4B, 0xE8, 0xED, 0x5A, 0x0E, 0xB8, 0x03
|
||||
);
|
||||
|
||||
// 7772E5DB-3868-4112-A1A9-F2669D106BF3
|
||||
static const ble_uuid128_t MIDI_IO_UUID = BLE_UUID128_INIT(
|
||||
0xF3, 0x6B, 0x10, 0x9D, 0x66, 0xF2, 0xA9, 0xA1,
|
||||
0x12, 0x41, 0x68, 0x38, 0xDB, 0xE5, 0x72, 0x77
|
||||
);
|
||||
|
||||
uint16_t midi_io_handle;
|
||||
|
||||
// ---- MIDI field accessor implementations ----
|
||||
|
||||
static BleCtx* midi_root_ctx(struct Device* device) {
|
||||
return ble_get_ctx(device);
|
||||
}
|
||||
|
||||
bool ble_midi_get_active(struct Device* device) {
|
||||
return midi_root_ctx(device)->midi_active.load();
|
||||
}
|
||||
void ble_midi_set_active(struct Device* device, bool v) {
|
||||
midi_root_ctx(device)->midi_active.store(v);
|
||||
}
|
||||
uint16_t ble_midi_get_conn_handle(struct Device* device) {
|
||||
return midi_root_ctx(device)->midi_conn_handle.load();
|
||||
}
|
||||
void ble_midi_set_conn_handle(struct Device* device, uint16_t h) {
|
||||
midi_root_ctx(device)->midi_conn_handle.store(h);
|
||||
}
|
||||
bool ble_midi_get_use_indicate(struct Device* device) {
|
||||
return midi_root_ctx(device)->midi_use_indicate.load();
|
||||
}
|
||||
void ble_midi_set_use_indicate(struct Device* device, bool v) {
|
||||
midi_root_ctx(device)->midi_use_indicate.store(v);
|
||||
}
|
||||
|
||||
error_t ble_midi_ensure_keepalive(struct Device* device, esp_timer_cb_t cb, uint64_t period_us) {
|
||||
BleCtx* ctx = midi_root_ctx(device);
|
||||
if (ctx->midi_keepalive_timer == nullptr) {
|
||||
esp_timer_create_args_t args = {};
|
||||
args.callback = cb;
|
||||
args.arg = device;
|
||||
args.dispatch_method = ESP_TIMER_TASK;
|
||||
args.name = "ble_midi_as";
|
||||
int rc = esp_timer_create(&args, &ctx->midi_keepalive_timer);
|
||||
if (rc != ESP_OK) {
|
||||
LOG_E(TAG, "midi keepalive timer create failed (rc=%d)", rc);
|
||||
return ERROR_INVALID_STATE;
|
||||
}
|
||||
}
|
||||
// Stop first in case timer is already running
|
||||
esp_timer_stop(ctx->midi_keepalive_timer);
|
||||
int rc = esp_timer_start_periodic(ctx->midi_keepalive_timer, period_us);
|
||||
if (rc != ESP_OK) {
|
||||
LOG_E(TAG, "midi keepalive timer start failed (rc=%d)", rc);
|
||||
return ERROR_INVALID_STATE;
|
||||
}
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
void ble_midi_stop_keepalive(struct Device* device) {
|
||||
BleCtx* ctx = midi_root_ctx(device);
|
||||
if (ctx->midi_keepalive_timer != nullptr) {
|
||||
esp_timer_stop(ctx->midi_keepalive_timer);
|
||||
}
|
||||
}
|
||||
|
||||
// midi_chr_access is called with the midi child Device* (set via ble_midi_init_gatt_handles).
|
||||
static int midi_chr_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt* ctxt, void* arg) {
|
||||
if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) {
|
||||
uint16_t len = OS_MBUF_PKTLEN(ctxt->om);
|
||||
LOG_I(TAG, "MIDI RX %u bytes", (unsigned)len);
|
||||
struct Device* device = (struct Device*)arg; // midi child device
|
||||
BleMidiCtx* mctx = (BleMidiCtx*)device_get_driver_data(device);
|
||||
if (mctx != nullptr && len > 0) {
|
||||
std::vector<uint8_t> packet(len);
|
||||
os_mbuf_copydata(ctxt->om, 0, len, packet.data());
|
||||
{
|
||||
xSemaphoreTake(mctx->rx_mutex, portMAX_DELAY);
|
||||
mctx->rx_queue.push_back(std::move(packet));
|
||||
while (mctx->rx_queue.size() > 16) mctx->rx_queue.pop_front();
|
||||
xSemaphoreGive(mctx->rx_mutex);
|
||||
}
|
||||
struct BtEvent e = {};
|
||||
e.type = BT_EVENT_MIDI_DATA_RECEIVED;
|
||||
ble_publish_event(device, e);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct ble_gatt_chr_def midi_chars[] = {
|
||||
{
|
||||
.uuid = &MIDI_IO_UUID.u,
|
||||
.access_cb = midi_chr_access,
|
||||
.arg = nullptr, // set to midi child Device* in ble_midi_init_gatt_handles()
|
||||
.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE_NO_RSP | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_INDICATE,
|
||||
.val_handle = &midi_io_handle,
|
||||
},
|
||||
{ 0 }
|
||||
};
|
||||
|
||||
void ble_midi_init_gatt_handles(struct Device* midi_child) {
|
||||
// Store the midi child Device* as the GATT callback arg so that midi_chr_access
|
||||
// can retrieve BleMidiCtx via device_get_driver_data without a global pointer.
|
||||
// midi_io_handle is written by NimBLE via the val_handle pointer above.
|
||||
midi_chars[0].arg = midi_child;
|
||||
}
|
||||
|
||||
// ---- MIDI Active Sensing keepalive ----
|
||||
// Callback arg is the midi child Device*.
|
||||
|
||||
static void midi_keepalive_cb(void* arg) {
|
||||
struct Device* device = (struct Device*)arg; // midi child device
|
||||
uint16_t conn = ble_midi_get_conn_handle(device);
|
||||
if (conn == BLE_HS_CONN_HANDLE_NONE) return;
|
||||
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) return;
|
||||
int rc = ble_midi_get_use_indicate(device)
|
||||
? ble_gatts_indicate_custom(conn, midi_io_handle, om)
|
||||
: ble_gatts_notify_custom(conn, midi_io_handle, om);
|
||||
if (rc != 0) os_mbuf_free_chain(om);
|
||||
}
|
||||
|
||||
// ---- MIDI sub-API implementations ----
|
||||
// All functions receive the midi child Device*.
|
||||
|
||||
static error_t midi_start(struct Device* device) {
|
||||
ble_midi_set_active(device, true);
|
||||
// Create 2-second periodic Active Sensing timer to prevent Windows BLE MIDI
|
||||
// driver from declaring the connection idle and disconnecting (~8-10 s timeout).
|
||||
error_t rc = ble_midi_ensure_keepalive(device, midi_keepalive_cb, 2'000'000);
|
||||
if (rc != ERROR_NONE) return rc;
|
||||
ble_start_advertising(device, &MIDI_SVC_UUID);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t ble_midi_start_internal(struct Device* midi_child) {
|
||||
return midi_start(midi_child);
|
||||
}
|
||||
|
||||
static error_t midi_stop(struct Device* device) {
|
||||
ble_midi_set_active(device, false);
|
||||
ble_midi_stop_keepalive(device);
|
||||
uint16_t conn = ble_midi_get_conn_handle(device);
|
||||
if (conn != BLE_HS_CONN_HANDLE_NONE) {
|
||||
ble_gap_terminate(conn, BLE_ERR_REM_USER_CONN_TERM);
|
||||
ble_midi_set_conn_handle(device, BLE_HS_CONN_HANDLE_NONE);
|
||||
}
|
||||
if (!ble_spp_get_active(device) && !ble_hid_get_active(device)) {
|
||||
ble_gap_adv_stop();
|
||||
}
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t midi_send(struct Device* device, const uint8_t* msg, size_t len) {
|
||||
uint16_t conn = ble_midi_get_conn_handle(device);
|
||||
if (conn == BLE_HS_CONN_HANDLE_NONE) return ERROR_INVALID_STATE;
|
||||
// BLE MIDI 2-byte header: [0x80|(ts_high&0x3F)][0x80|(ts_low&0x7F)]
|
||||
uint8_t header[2] = { 0x80, 0x80 };
|
||||
struct os_mbuf* om = ble_hs_mbuf_from_flat(header, 2);
|
||||
if (om == nullptr) return ERROR_INVALID_STATE;
|
||||
if (os_mbuf_append(om, msg, len) != 0) {
|
||||
os_mbuf_free_chain(om);
|
||||
LOG_E(TAG, "midi_send: mbuf append failed");
|
||||
return ERROR_INVALID_STATE;
|
||||
}
|
||||
LOG_I(TAG, "midi_send %u bytes (indicate=%d)", (unsigned)len, (int)ble_midi_get_use_indicate(device));
|
||||
int rc = ble_midi_get_use_indicate(device)
|
||||
? ble_gatts_indicate_custom(conn, midi_io_handle, om)
|
||||
: ble_gatts_notify_custom(conn, midi_io_handle, om);
|
||||
if (rc != 0) {
|
||||
os_mbuf_free_chain(om);
|
||||
LOG_E(TAG, "midi_send failed rc=%d", rc);
|
||||
}
|
||||
return (rc == 0) ? ERROR_NONE : ERROR_INVALID_STATE;
|
||||
}
|
||||
|
||||
static bool midi_is_connected(struct Device* device) {
|
||||
return ble_midi_get_conn_handle(device) != BLE_HS_CONN_HANDLE_NONE;
|
||||
}
|
||||
|
||||
extern const BtMidiApi nimble_midi_api = {
|
||||
.start = midi_start,
|
||||
.stop = midi_stop,
|
||||
.send = midi_send,
|
||||
.is_connected = midi_is_connected,
|
||||
};
|
||||
|
||||
// ---- MIDI child driver lifecycle ----
|
||||
|
||||
static error_t esp32_ble_midi_start_device(struct Device* device) {
|
||||
BleMidiCtx* mctx = new BleMidiCtx();
|
||||
mctx->rx_mutex = xSemaphoreCreateMutex();
|
||||
device_set_driver_data(device, mctx);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t esp32_ble_midi_stop_device(struct Device* device) {
|
||||
BleMidiCtx* mctx = (BleMidiCtx*)device_get_driver_data(device);
|
||||
if (mctx != nullptr) {
|
||||
vSemaphoreDelete(mctx->rx_mutex);
|
||||
delete mctx;
|
||||
device_set_driver_data(device, nullptr);
|
||||
}
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
Driver esp32_ble_midi_driver = {
|
||||
.name = "esp32_ble_midi",
|
||||
.compatible = nullptr,
|
||||
.start_device = esp32_ble_midi_start_device,
|
||||
.stop_device = esp32_ble_midi_stop_device,
|
||||
.api = &nimble_midi_api,
|
||||
.device_type = &BLUETOOTH_MIDI_TYPE,
|
||||
.owner = nullptr,
|
||||
.internal = nullptr,
|
||||
};
|
||||
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
#endif // CONFIG_BT_NIMBLE_ENABLED
|
||||
@@ -0,0 +1,236 @@
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <sdkconfig.h>
|
||||
#endif
|
||||
|
||||
#if defined(CONFIG_BT_NIMBLE_ENABLED)
|
||||
|
||||
#include <bluetooth/esp32_ble_internal.h>
|
||||
|
||||
#include <host/ble_gap.h>
|
||||
#include <host/ble_hs_mbuf.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
constexpr auto* TAG = "esp32_ble_scan";
|
||||
#include <tactility/log.h>
|
||||
|
||||
// ---- Module-static scan context ----
|
||||
// Set at the start of ble_resolve_next_unnamed_peer; valid for the duration of
|
||||
// the sequential resolution chain (single-device, single-scan at a time).
|
||||
// Using BleCtx* (not Device*) so we avoid keeping a static Device reference.
|
||||
static BleCtx* s_scan_ctx = nullptr;
|
||||
|
||||
// ---- Scan data helpers ----
|
||||
|
||||
void ble_scan_clear_results(struct Device* device) {
|
||||
BleCtx* ctx = ble_get_ctx(device);
|
||||
xSemaphoreTake(ctx->scan_mutex, portMAX_DELAY);
|
||||
ctx->scan_count = 0;
|
||||
memset(ctx->scan_results, 0, sizeof(ctx->scan_results));
|
||||
xSemaphoreGive(ctx->scan_mutex);
|
||||
}
|
||||
|
||||
// ---- GAP scan callback ----
|
||||
|
||||
int ble_gap_disc_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_DISC: {
|
||||
const auto& disc = event->disc;
|
||||
|
||||
BtPeerRecord record = {};
|
||||
memcpy(record.addr, disc.addr.val, BT_ADDR_LEN);
|
||||
record.addr_type = disc.addr.type;
|
||||
record.rssi = disc.rssi;
|
||||
record.paired = false;
|
||||
record.connected = false;
|
||||
|
||||
struct ble_hs_adv_fields fields;
|
||||
if (ble_hs_adv_parse_fields(&fields, disc.data, disc.length_data) == 0) {
|
||||
if (fields.name != nullptr && fields.name_len > 0) {
|
||||
size_t copy_len = std::min<size_t>(fields.name_len, BT_NAME_MAX);
|
||||
memcpy(record.name, fields.name, copy_len);
|
||||
record.name[copy_len] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
xSemaphoreTake(ctx->scan_mutex, portMAX_DELAY);
|
||||
bool found = false;
|
||||
for (size_t i = 0; i < ctx->scan_count; ++i) {
|
||||
if (memcmp(ctx->scan_results[i].addr, record.addr, BT_ADDR_LEN) == 0) {
|
||||
// Deduplicate: merge name from SCAN_RSP without clobbering ADV_IND name
|
||||
if (record.name[0] != '\0') {
|
||||
memcpy(ctx->scan_results[i].name, record.name, BT_NAME_MAX + 1);
|
||||
}
|
||||
ctx->scan_results[i].rssi = record.rssi;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found && ctx->scan_count < 64) {
|
||||
ctx->scan_results[ctx->scan_count] = record;
|
||||
ctx->scan_addrs[ctx->scan_count] = disc.addr; // full addr (type+val)
|
||||
ctx->scan_count++;
|
||||
}
|
||||
xSemaphoreGive(ctx->scan_mutex);
|
||||
}
|
||||
|
||||
struct BtEvent e = {};
|
||||
e.type = BT_EVENT_PEER_FOUND;
|
||||
e.peer = record;
|
||||
ble_publish_event(device, e);
|
||||
break;
|
||||
}
|
||||
|
||||
case BLE_GAP_EVENT_DISC_COMPLETE:
|
||||
LOG_I(TAG, "Scan complete (reason=%d)", event->disc_complete.reason);
|
||||
// Keep scan_active=true; resolveNextUnnamedPeer clears it and fires ScanFinished
|
||||
// once name resolution finishes, so the UI spinner stays active throughout.
|
||||
ble_resolve_next_unnamed_peer(device, 0);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---- GATT Device Name resolution ----
|
||||
//
|
||||
// After a scan completes, briefly connect to each device that didn't include its
|
||||
// name in advertising data and read Generic Access Device Name (UUID 0x2A00).
|
||||
//
|
||||
// Resolution is sequential: connect → read → disconnect → next device.
|
||||
// Skip resolution if a profile server or HID host connection is active —
|
||||
// a simultaneous central connection would fail with BLE_HS_EALREADY.
|
||||
|
||||
static int name_read_callback(uint16_t conn_handle, const struct ble_gatt_error* error,
|
||||
struct ble_gatt_attr* attr, void* arg) {
|
||||
BleCtx* ctx = s_scan_ctx;
|
||||
|
||||
if (error->status == 0 && attr != nullptr) {
|
||||
size_t idx = (size_t)(uintptr_t)arg;
|
||||
uint16_t len = OS_MBUF_PKTLEN(attr->om);
|
||||
if (len > 0 && len <= (uint16_t)BT_NAME_MAX) {
|
||||
char name_buf[BT_NAME_MAX + 1] = {};
|
||||
os_mbuf_copydata(attr->om, 0, len, name_buf);
|
||||
{
|
||||
xSemaphoreTake(ctx->scan_mutex, portMAX_DELAY);
|
||||
if (idx < ctx->scan_count && ctx->scan_results[idx].name[0] == '\0') {
|
||||
memcpy(ctx->scan_results[idx].name, name_buf, len);
|
||||
ctx->scan_results[idx].name[len] = '\0';
|
||||
LOG_I(TAG, "Name resolved (idx=%u): %s", (unsigned)idx, name_buf);
|
||||
}
|
||||
BtPeerRecord record = (idx < ctx->scan_count) ? ctx->scan_results[idx] : BtPeerRecord{};
|
||||
xSemaphoreGive(ctx->scan_mutex);
|
||||
|
||||
struct BtEvent e = {};
|
||||
e.type = BT_EVENT_PEER_FOUND;
|
||||
e.peer = record;
|
||||
ble_publish_event(ctx->device, e);
|
||||
}
|
||||
}
|
||||
return 0; // wait for BLE_HS_EDONE
|
||||
}
|
||||
|
||||
// BLE_HS_EDONE, ATT error, or timeout — done with this device
|
||||
ble_gap_terminate(conn_handle, BLE_ERR_REM_USER_CONN_TERM);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int name_res_gap_callback(struct ble_gap_event* event, void* arg) {
|
||||
size_t idx = (size_t)(uintptr_t)arg;
|
||||
BleCtx* ctx = s_scan_ctx;
|
||||
|
||||
switch (event->type) {
|
||||
case BLE_GAP_EVENT_CONNECT:
|
||||
if (event->connect.status == 0) {
|
||||
LOG_I(TAG, "Name resolution: connected (idx=%u handle=%u)", (unsigned)idx, event->connect.conn_handle);
|
||||
static const ble_uuid16_t device_name_uuid = BLE_UUID16_INIT(0x2A00);
|
||||
int rc = ble_gattc_read_by_uuid(event->connect.conn_handle,
|
||||
1, 0xFFFF,
|
||||
&device_name_uuid.u,
|
||||
name_read_callback, arg);
|
||||
if (rc != 0) {
|
||||
LOG_W(TAG, "Name resolution: read_by_uuid failed rc=%d", rc);
|
||||
ble_gap_terminate(event->connect.conn_handle, BLE_ERR_REM_USER_CONN_TERM);
|
||||
}
|
||||
} else {
|
||||
LOG_I(TAG, "Name resolution: connect failed (idx=%u status=%d)", (unsigned)idx, event->connect.status);
|
||||
ble_resolve_next_unnamed_peer(ctx->device, idx + 1);
|
||||
}
|
||||
break;
|
||||
|
||||
case BLE_GAP_EVENT_DISCONNECT:
|
||||
LOG_I(TAG, "Name resolution: disconnected (idx=%u)", (unsigned)idx);
|
||||
ble_resolve_next_unnamed_peer(ctx->device, idx + 1);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void ble_resolve_next_unnamed_peer(struct Device* device, size_t start_idx) {
|
||||
BleCtx* ctx = ble_get_ctx(device);
|
||||
s_scan_ctx = ctx;
|
||||
|
||||
// Skip if a profile server or HID host connection attempt is active —
|
||||
// initiating a central connection simultaneously would fail (BLE_HS_EALREADY).
|
||||
if (ble_midi_get_active(device) || ble_spp_get_active(device) ||
|
||||
ble_hid_get_active(device) || ble_hid_get_host_active(device)) {
|
||||
LOG_I(TAG, "Name resolution: skipping (server or HID host active)");
|
||||
ble_set_scan_active(device, false);
|
||||
struct BtEvent e = {};
|
||||
e.type = BT_EVENT_SCAN_FINISHED;
|
||||
ble_publish_event(device, e);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t i = start_idx;
|
||||
while (true) {
|
||||
ble_addr_t addr = {};
|
||||
bool found = false;
|
||||
{
|
||||
xSemaphoreTake(ctx->scan_mutex, portMAX_DELAY);
|
||||
while (i < ctx->scan_count) {
|
||||
if (ctx->scan_results[i].name[0] == '\0') {
|
||||
addr = ctx->scan_addrs[i];
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
xSemaphoreGive(ctx->scan_mutex);
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
LOG_I(TAG, "Name resolution: complete (%u devices)", (unsigned)i);
|
||||
ble_set_scan_active(device, false);
|
||||
struct BtEvent e = {};
|
||||
e.type = BT_EVENT_SCAN_FINISHED;
|
||||
ble_publish_event(device, e);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t own_addr_type;
|
||||
ble_hs_id_infer_auto(0, &own_addr_type);
|
||||
|
||||
void* idx_arg = (void*)(uintptr_t)i;
|
||||
int rc = ble_gap_connect(own_addr_type, &addr, 1500, nullptr,
|
||||
name_res_gap_callback, idx_arg);
|
||||
if (rc == 0) {
|
||||
return; // name_res_gap_callback continues the chain
|
||||
}
|
||||
|
||||
LOG_I(TAG, "Name resolution: ble_gap_connect failed idx=%u rc=%d, skipping", (unsigned)i, rc);
|
||||
++i;
|
||||
}
|
||||
}
|
||||
|
||||
#endif // CONFIG_BT_NIMBLE_ENABLED
|
||||
@@ -0,0 +1,239 @@
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <sdkconfig.h>
|
||||
#endif
|
||||
|
||||
#if defined(CONFIG_BT_NIMBLE_ENABLED)
|
||||
|
||||
#include <bluetooth/esp32_ble_internal.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <vector>
|
||||
#include <host/ble_gap.h>
|
||||
#include <host/ble_gatt.h>
|
||||
#include <host/ble_hs_mbuf.h>
|
||||
|
||||
constexpr auto* TAG = "esp32_ble_spp";
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
// ---- SPP device context (stored as driver data of the serial child device) ----
|
||||
|
||||
struct BleSppCtx {
|
||||
SemaphoreHandle_t rx_mutex;
|
||||
std::deque<std::vector<uint8_t>> rx_queue;
|
||||
};
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
|
||||
|
||||
// ---- NUS (Nordic UART Service) UUIDs ----
|
||||
|
||||
// 6E400001-B5A3-F393-E0A9-E50E24DCCA9E
|
||||
const ble_uuid128_t NUS_SVC_UUID = BLE_UUID128_INIT(
|
||||
0x9E, 0xCA, 0xDC, 0x24, 0x0E, 0xE5, 0xA9, 0xE0,
|
||||
0x93, 0xF3, 0xA3, 0xB5, 0x01, 0x00, 0x40, 0x6E
|
||||
);
|
||||
|
||||
// 6E400002 RX (write from client → device)
|
||||
static const ble_uuid128_t NUS_RX_UUID = BLE_UUID128_INIT(
|
||||
0x9E, 0xCA, 0xDC, 0x24, 0x0E, 0xE5, 0xA9, 0xE0,
|
||||
0x93, 0xF3, 0xA3, 0xB5, 0x02, 0x00, 0x40, 0x6E
|
||||
);
|
||||
|
||||
// 6E400003 TX (notify device → client)
|
||||
static const ble_uuid128_t NUS_TX_UUID = BLE_UUID128_INIT(
|
||||
0x9E, 0xCA, 0xDC, 0x24, 0x0E, 0xE5, 0xA9, 0xE0,
|
||||
0x93, 0xF3, 0xA3, 0xB5, 0x03, 0x00, 0x40, 0x6E
|
||||
);
|
||||
|
||||
uint16_t nus_tx_handle;
|
||||
|
||||
// nus_chr_access is called with the serial child Device* (set via ble_spp_init_gatt_handles).
|
||||
static int nus_chr_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt* ctxt, void* arg) {
|
||||
if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) {
|
||||
uint16_t len = OS_MBUF_PKTLEN(ctxt->om);
|
||||
LOG_I(TAG, "NUS RX %u bytes", (unsigned)len);
|
||||
struct Device* device = (struct Device*)arg; // serial child device
|
||||
BleSppCtx* sctx = (BleSppCtx*)device_get_driver_data(device);
|
||||
if (sctx != nullptr && len > 0) {
|
||||
std::vector<uint8_t> packet(len);
|
||||
os_mbuf_copydata(ctxt->om, 0, len, packet.data());
|
||||
{
|
||||
xSemaphoreTake(sctx->rx_mutex, portMAX_DELAY);
|
||||
sctx->rx_queue.push_back(std::move(packet));
|
||||
while (sctx->rx_queue.size() > 16) sctx->rx_queue.pop_front();
|
||||
xSemaphoreGive(sctx->rx_mutex);
|
||||
}
|
||||
struct BtEvent e = {};
|
||||
e.type = BT_EVENT_SPP_DATA_RECEIVED;
|
||||
ble_publish_event(device, e);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct ble_gatt_chr_def nus_chars_with_handle[] = {
|
||||
{
|
||||
.uuid = &NUS_RX_UUID.u,
|
||||
.access_cb = nus_chr_access,
|
||||
.arg = nullptr, // set to serial child Device* in ble_spp_init_gatt_handles()
|
||||
.flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_NO_RSP,
|
||||
},
|
||||
{
|
||||
.uuid = &NUS_TX_UUID.u,
|
||||
.access_cb = nus_chr_access,
|
||||
.arg = nullptr, // set to serial child Device* in ble_spp_init_gatt_handles()
|
||||
.flags = BLE_GATT_CHR_F_NOTIFY,
|
||||
.val_handle = &nus_tx_handle,
|
||||
},
|
||||
{ 0 }
|
||||
};
|
||||
|
||||
void ble_spp_init_gatt_handles(struct Device* serial_child) {
|
||||
// Store the serial child Device* as the GATT callback arg so that nus_chr_access
|
||||
// can retrieve BleSppCtx via device_get_driver_data without a global pointer.
|
||||
// nus_tx_handle is written by NimBLE via the val_handle pointer above.
|
||||
nus_chars_with_handle[0].arg = serial_child;
|
||||
nus_chars_with_handle[1].arg = serial_child;
|
||||
}
|
||||
|
||||
// ---- SPP field accessor implementations ----
|
||||
|
||||
static BleCtx* spp_root_ctx(struct Device* device) {
|
||||
return ble_get_ctx(device);
|
||||
}
|
||||
|
||||
bool ble_spp_get_active(struct Device* device) {
|
||||
return spp_root_ctx(device)->spp_active.load();
|
||||
}
|
||||
void ble_spp_set_active(struct Device* device, bool v) {
|
||||
spp_root_ctx(device)->spp_active.store(v);
|
||||
}
|
||||
uint16_t ble_spp_get_conn_handle(struct Device* device) {
|
||||
return spp_root_ctx(device)->spp_conn_handle.load();
|
||||
}
|
||||
void ble_spp_set_conn_handle(struct Device* device, uint16_t h) {
|
||||
spp_root_ctx(device)->spp_conn_handle.store(h);
|
||||
}
|
||||
|
||||
// ---- SPP sub-API implementations ----
|
||||
// All functions receive the serial child Device*.
|
||||
|
||||
static error_t spp_start(struct Device* device) {
|
||||
ble_spp_set_active(device, true);
|
||||
ble_start_advertising(device, &NUS_SVC_UUID);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t ble_spp_start_internal(struct Device* serial_child) {
|
||||
return spp_start(serial_child);
|
||||
}
|
||||
|
||||
static error_t spp_stop(struct Device* device) {
|
||||
ble_spp_set_active(device, false);
|
||||
uint16_t conn = ble_spp_get_conn_handle(device);
|
||||
if (conn != BLE_HS_CONN_HANDLE_NONE) {
|
||||
ble_gap_terminate(conn, BLE_ERR_REM_USER_CONN_TERM);
|
||||
ble_spp_set_conn_handle(device, BLE_HS_CONN_HANDLE_NONE);
|
||||
}
|
||||
// Do NOT restart advertising after user-initiated stop — restarting name-only
|
||||
// advertising causes bonded Windows hosts to auto-reconnect in a tight loop.
|
||||
if (!ble_midi_get_active(device) && !ble_hid_get_active(device)) {
|
||||
ble_gap_adv_stop();
|
||||
}
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t spp_write(struct Device* device, const uint8_t* data, size_t len, size_t* written) {
|
||||
uint16_t conn = ble_spp_get_conn_handle(device);
|
||||
if (conn == BLE_HS_CONN_HANDLE_NONE) {
|
||||
if (written) *written = 0;
|
||||
return ERROR_INVALID_STATE;
|
||||
}
|
||||
struct os_mbuf* om = ble_hs_mbuf_from_flat(data, len);
|
||||
if (om == nullptr) {
|
||||
if (written) *written = 0;
|
||||
return ERROR_INVALID_STATE;
|
||||
}
|
||||
int rc = ble_gatts_notify_custom(conn, nus_tx_handle, om);
|
||||
if (rc != 0) {
|
||||
os_mbuf_free_chain(om);
|
||||
if (written) *written = 0;
|
||||
return ERROR_INVALID_STATE;
|
||||
}
|
||||
if (written) *written = len;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t spp_read(struct Device* device, uint8_t* data, size_t max_len, size_t* read_out) {
|
||||
BleSppCtx* sctx = (BleSppCtx*)device_get_driver_data(device);
|
||||
if (sctx == nullptr || data == nullptr || max_len == 0) {
|
||||
if (read_out) *read_out = 0;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
xSemaphoreTake(sctx->rx_mutex, portMAX_DELAY);
|
||||
if (sctx->rx_queue.empty()) {
|
||||
xSemaphoreGive(sctx->rx_mutex);
|
||||
if (read_out) *read_out = 0;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
auto& front = sctx->rx_queue.front();
|
||||
size_t copy_len = std::min(front.size(), max_len);
|
||||
memcpy(data, front.data(), copy_len);
|
||||
sctx->rx_queue.pop_front();
|
||||
xSemaphoreGive(sctx->rx_mutex);
|
||||
if (read_out) *read_out = copy_len;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static bool spp_is_connected(struct Device* device) {
|
||||
return ble_spp_get_conn_handle(device) != BLE_HS_CONN_HANDLE_NONE;
|
||||
}
|
||||
|
||||
extern const BtSerialApi nimble_serial_api = {
|
||||
.start = spp_start,
|
||||
.stop = spp_stop,
|
||||
.write = spp_write,
|
||||
.read = spp_read,
|
||||
.is_connected = spp_is_connected,
|
||||
};
|
||||
|
||||
// ---- Serial child driver lifecycle ----
|
||||
|
||||
static error_t esp32_ble_serial_start_device(struct Device* device) {
|
||||
BleSppCtx* sctx = new BleSppCtx();
|
||||
sctx->rx_mutex = xSemaphoreCreateMutex();
|
||||
device_set_driver_data(device, sctx);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t esp32_ble_serial_stop_device(struct Device* device) {
|
||||
BleSppCtx* sctx = (BleSppCtx*)device_get_driver_data(device);
|
||||
if (sctx != nullptr) {
|
||||
vSemaphoreDelete(sctx->rx_mutex);
|
||||
delete sctx;
|
||||
device_set_driver_data(device, nullptr);
|
||||
}
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
Driver esp32_ble_serial_driver = {
|
||||
.name = "esp32_ble_serial",
|
||||
.compatible = nullptr,
|
||||
.start_device = esp32_ble_serial_start_device,
|
||||
.stop_device = esp32_ble_serial_stop_device,
|
||||
.api = &nimble_serial_api,
|
||||
.device_type = &BLUETOOTH_SERIAL_TYPE,
|
||||
.owner = nullptr,
|
||||
.internal = nullptr,
|
||||
};
|
||||
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
#endif // CONFIG_BT_NIMBLE_ENABLED
|
||||
@@ -1,4 +1,9 @@
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <sdkconfig.h>
|
||||
#endif
|
||||
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
@@ -14,6 +19,12 @@ extern Driver esp32_sdmmc_driver;
|
||||
#endif
|
||||
extern Driver esp32_spi_driver;
|
||||
extern Driver esp32_uart_driver;
|
||||
#if defined(CONFIG_BT_NIMBLE_ENABLED)
|
||||
extern Driver esp32_bluetooth_driver;
|
||||
extern Driver esp32_ble_serial_driver;
|
||||
extern Driver esp32_ble_midi_driver;
|
||||
extern Driver esp32_ble_hid_device_driver;
|
||||
#endif
|
||||
|
||||
static error_t start() {
|
||||
/* We crash when construct fails, because if a single driver fails to construct,
|
||||
@@ -26,12 +37,24 @@ static error_t start() {
|
||||
#endif
|
||||
check(driver_construct_add(&esp32_spi_driver) == ERROR_NONE);
|
||||
check(driver_construct_add(&esp32_uart_driver) == ERROR_NONE);
|
||||
#if defined(CONFIG_BT_NIMBLE_ENABLED)
|
||||
check(driver_construct_add(&esp32_bluetooth_driver) == ERROR_NONE);
|
||||
check(driver_construct_add(&esp32_ble_serial_driver) == ERROR_NONE);
|
||||
check(driver_construct_add(&esp32_ble_midi_driver) == ERROR_NONE);
|
||||
check(driver_construct_add(&esp32_ble_hid_device_driver) == ERROR_NONE);
|
||||
#endif
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop() {
|
||||
/* We crash when destruct fails, because if a single driver fails to destruct,
|
||||
* there is no guarantee that the previously destroyed drivers can be recovered */
|
||||
#if defined(CONFIG_BT_NIMBLE_ENABLED)
|
||||
check(driver_remove_destruct(&esp32_ble_hid_device_driver) == ERROR_NONE);
|
||||
check(driver_remove_destruct(&esp32_ble_midi_driver) == ERROR_NONE);
|
||||
check(driver_remove_destruct(&esp32_ble_serial_driver) == ERROR_NONE);
|
||||
check(driver_remove_destruct(&esp32_bluetooth_driver) == ERROR_NONE);
|
||||
#endif
|
||||
check(driver_remove_destruct(&esp32_gpio_driver) == ERROR_NONE);
|
||||
check(driver_remove_destruct(&esp32_i2c_driver) == ERROR_NONE);
|
||||
check(driver_remove_destruct(&esp32_i2s_driver) == ERROR_NONE);
|
||||
|
||||
Reference in New Issue
Block a user