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:
Shadowtrance
2026-05-23 06:49:37 +10:00
committed by GitHub
parent 895e6bc50d
commit 5c78d55b04
72 changed files with 7155 additions and 352 deletions
+2
View File
@@ -8,3 +8,5 @@ idf_component_register(
PRIV_INCLUDE_DIRS "private/"
REQUIRES TactilityKernel driver vfs fatfs
)
idf_component_optional_requires(PRIVATE bt)
@@ -0,0 +1,8 @@
description: ESP32 BLE driver (NimBLE)
compatible: "espressif,esp32-ble"
properties:
_unused:
type: int
default: 0
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/bindings/bindings.h>
#include <tactility/drivers/esp32_ble.h>
#ifdef __cplusplus
extern "C" {
#endif
DEFINE_DEVICETREE(esp32_ble, struct Esp32BleConfig)
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
/** No device-tree configuration required for the NimBLE BLE driver. */
struct Esp32BleConfig {
int _unused; /**< Placeholder — driver reads all config from NimBLE Kconfig. */
};
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,34 @@
#pragma once
#ifdef ESP_PLATFORM
#include <sdkconfig.h>
#endif
#if defined(CONFIG_BT_NIMBLE_ENABLED)
#include <tactility/drivers/bluetooth_hid_device.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
enum BleHidProfile { None, KbConsumer, Mouse, KbMouse, Gamepad };
struct Device;
bool ble_hid_get_active(struct Device* device);
void ble_hid_set_active(struct Device* device, bool v);
uint16_t ble_hid_get_conn_handle(struct Device* device);
void ble_hid_set_conn_handle(struct Device* device, uint16_t h);
// device must be the hid_device child Device*.
void ble_hid_init_gatt();
bool ble_hid_switch_profile(struct Device* hid_child, BleHidProfile profile);
#ifdef __cplusplus
}
#endif
#endif // CONFIG_BT_NIMBLE_ENABLED
@@ -0,0 +1,170 @@
#pragma once
#ifdef ESP_PLATFORM
#include <sdkconfig.h>
#endif
#if defined(CONFIG_BT_NIMBLE_ENABLED)
#include <tactility/drivers/bluetooth.h>
#include <tactility/error.h>
// Must be included before any NimBLE header: log_common.h (pulled in by ble_hs.h)
// defines LOG_LEVEL_* as macros with the same names as tactility/log.h's LogLevel enum.
// Including tactility/log.h first ensures the enum is compiled before the macros shadow it.
#include <tactility/log.h>
#include <host/ble_gap.h>
#include <host/ble_gatt.h>
#include <host/ble_hs.h>
#include <host/ble_uuid.h>
#include <esp_timer.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <atomic>
// ---- Per-module headers (structs, accessors, sub-API externs) ----
#include <bluetooth/esp32_ble_spp.h>
#include <bluetooth/esp32_ble_midi.h>
#include <bluetooth/esp32_ble_hid.h>
#define BLE_MAX_CALLBACKS 8
struct BleCallbackEntry {
BtEventCallback fn;
void* ctx;
};
struct BleCtx {
// Mutexes
SemaphoreHandle_t radio_mutex; // guards radio state transitions
SemaphoreHandle_t cb_mutex; // guards callbacks array
// Radio / scan state (atomic — read from multiple tasks)
std::atomic<BtRadioState> radio_state;
std::atomic<bool> scan_active;
// Set by Tactility HID host to prevent simultaneous central connection during name resolution
std::atomic<bool> hid_host_active;
// Event callbacks (guarded by cb_mutex)
BleCallbackEntry callbacks[BLE_MAX_CALLBACKS];
size_t callback_count;
// Connection handles + active flags (atomic — accessed from multiple tasks)
std::atomic<uint16_t> spp_conn_handle;
std::atomic<bool> spp_active;
std::atomic<uint16_t> midi_conn_handle;
std::atomic<bool> midi_active;
std::atomic<bool> midi_use_indicate; // true when client subscribed for INDICATE (e.g. Windows)
std::atomic<bool> hid_active;
std::atomic<bool> link_encrypted;
std::atomic<int> pending_reset_count;
// Timers
esp_timer_handle_t midi_keepalive_timer; // 2-second periodic Active Sensing
esp_timer_handle_t adv_restart_timer; // one-shot after connect failure (500 ms)
// One-shot timer used to dispatch dispatchDisable off the NimBLE host task.
// nimble_port_stop() must not be called from the NimBLE host task itself.
esp_timer_handle_t disable_timer;
// BLE device name (set before or after radio enable; applied in dispatch_enable)
char device_name[BLE_DEVICE_NAME_MAX + 1];
// Scan data (guarded by scan_mutex)
SemaphoreHandle_t scan_mutex;
BtPeerRecord scan_results[64];
ble_addr_t scan_addrs[64];
size_t scan_count;
// Device reference (passed to BtEventCallback)
struct Device* device;
// Child devices (created by esp32_ble_start_device, destroyed by stop_device)
struct Device* serial_child;
struct Device* midi_child;
struct Device* hid_device_child;
};
// Always returns the root BLE device's BleCtx regardless of which device is passed.
BleCtx* ble_get_ctx(struct Device* device);
// ---- General field accessors (defined in esp32_ble.cpp) ----
BtRadioState ble_get_radio_state(struct Device* device);
bool ble_hid_get_host_active(struct Device* device);
bool ble_get_scan_active(struct Device* device);
void ble_set_scan_active(struct Device* device, bool v);
// ---- Scan data management (defined in esp32_ble_scan.cpp) ----
void ble_scan_clear_results(struct Device* device);
// ---- Event publishing ----
void ble_publish_event(struct Device* device, struct BtEvent event);
// ---- Advertising helpers (defined in esp32_ble.cpp) ----
void ble_start_advertising(struct Device* device, const ble_uuid128_t* svc_uuid); // svc_uuid=nullptr → name-only
void ble_start_advertising_hid(struct Device* device, uint16_t appearance);
void ble_schedule_adv_restart(struct Device* device, uint64_t delay_us);
// ---- GAP scan callback (defined in esp32_ble_scan.cpp) ----
int ble_gap_disc_event_handler(struct ble_gap_event* event, void* arg);
void ble_resolve_next_unnamed_peer(struct Device* device, size_t start_idx);
// ---- Child driver definitions (one per profile sub-module) ----
extern struct Driver esp32_ble_serial_driver;
extern struct Driver esp32_ble_midi_driver;
extern struct Driver esp32_ble_hid_device_driver;
// ---- SPP GATT (defined in esp32_ble_spp.cpp) ----
// device must be the serial child Device*.
void ble_spp_init_gatt_handles(struct Device* serial_child);
error_t ble_spp_start_internal(struct Device* serial_child);
// ---- MIDI GATT (defined in esp32_ble_midi.cpp) ----
// device must be the midi child Device*.
void ble_midi_init_gatt_handles(struct Device* midi_child);
error_t ble_midi_start_internal(struct Device* midi_child);
// ---- Cross-module GATT char / service arrays ----
// Non-const: the .arg field is set to the child Device* at init time so that
// NimBLE access callbacks can retrieve the context without a global pointer.
extern struct ble_gatt_chr_def nus_chars_with_handle[]; // esp32_ble_spp.cpp
extern struct ble_gatt_chr_def midi_chars[]; // esp32_ble_midi.cpp
// ---- Cross-module service UUIDs ----
extern const ble_uuid128_t NUS_SVC_UUID; // esp32_ble_spp.cpp
extern const ble_uuid128_t MIDI_SVC_UUID; // esp32_ble_midi.cpp
// ---- Cross-module GATT handle variables ----
extern uint16_t nus_tx_handle; // esp32_ble_spp.cpp
extern uint16_t midi_io_handle; // esp32_ble_midi.cpp
extern uint16_t hid_kb_input_handle; // esp32_ble_hid.cpp
extern uint16_t hid_consumer_input_handle; // esp32_ble_hid.cpp
extern uint16_t hid_mouse_input_handle; // esp32_ble_hid.cpp
extern uint16_t hid_gamepad_input_handle; // esp32_ble_hid.cpp
// ---- HID active report map / appearance ----
extern const uint8_t* active_hid_rpt_map; // esp32_ble_hid.cpp
extern size_t active_hid_rpt_map_len; // esp32_ble_hid.cpp
extern uint16_t hid_appearance; // esp32_ble_hid.cpp
extern BleHidProfile current_hid_profile; // esp32_ble_hid.cpp
// ---- HCI gate (ble_hci_gate.c) — P4/esp-hosted only ----
// Controls whether hci_rx_handler forwards packets to NimBLE.
// Set true after nimble_port_init(), false before nimble_port_stop().
#if defined(CONFIG_ESP_HOSTED_ENABLED)
#ifdef __cplusplus
extern "C" {
#endif
void ble_hci_gate_set_active(bool active);
bool ble_hci_gate_wait_idle(int max_ms);
#ifdef __cplusplus
}
#endif
#endif // CONFIG_ESP_HOSTED_ENABLED
#endif // CONFIG_BT_NIMBLE_ENABLED
@@ -0,0 +1,38 @@
#pragma once
#ifdef ESP_PLATFORM
#include <sdkconfig.h>
#endif
#if defined(CONFIG_BT_NIMBLE_ENABLED)
#include <tactility/drivers/bluetooth_midi.h>
#include <tactility/error.h>
#include <esp_timer.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
struct Device;
bool ble_midi_get_active(struct Device* device);
void ble_midi_set_active(struct Device* device, bool v);
uint16_t ble_midi_get_conn_handle(struct Device* device);
void ble_midi_set_conn_handle(struct Device* device, uint16_t h);
bool ble_midi_get_use_indicate(struct Device* device);
void ble_midi_set_use_indicate(struct Device* device, bool v);
// MIDI keepalive timer helpers — timer handle lives in BleCtx.
// ble_midi_ensure_keepalive creates the timer if needed and starts it periodically.
// ble_midi_stop_keepalive stops (but does not delete) the timer.
error_t ble_midi_ensure_keepalive(struct Device* device, esp_timer_cb_t cb, uint64_t period_us);
void ble_midi_stop_keepalive(struct Device* device);
#ifdef __cplusplus
}
#endif
#endif // CONFIG_BT_NIMBLE_ENABLED
@@ -0,0 +1,27 @@
#pragma once
#ifdef ESP_PLATFORM
#include <sdkconfig.h>
#endif
#if defined(CONFIG_BT_NIMBLE_ENABLED)
#include <tactility/drivers/bluetooth_serial.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
struct Device;
bool ble_spp_get_active(struct Device* device);
void ble_spp_set_active(struct Device* device, bool v);
uint16_t ble_spp_get_conn_handle(struct Device* device);
void ble_spp_set_conn_handle(struct Device* device, uint16_t h);
#ifdef __cplusplus
}
#endif
#endif // CONFIG_BT_NIMBLE_ENABLED
@@ -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);