CDC Badge OS
Firmware for the CDC Badge v1.0 hardware security key
Loading...
Searching...
No Matches
BluetoothController.cpp
Go to the documentation of this file.
1
7
9#include "cdc_hal/hw_config.h"
10#include "cdc_core/Raii.h"
11#include "cdc_log.h"
12#include "sdkconfig.h"
13#include "esp_attr.h"
14
15static const char* TAG = "BT-Ctrl";
16
17// Full implementation is compiled only when NimBLE support is enabled.
18#if defined(CONFIG_BT_ENABLED) && defined(CONFIG_BT_NIMBLE_ENABLED)
19
20#include "esp_bt.h"
21#include "esp_mac.h"
22#include "nimble/nimble_port.h"
23#include "nimble/nimble_port_freertos.h"
24#include "host/ble_hs.h"
25#include "host/util/util.h"
26#include "services/gap/ble_svc_gap.h"
27#include "services/gatt/ble_svc_gatt.h"
28#include "host/ble_uuid.h"
29#include "host/ble_att.h"
30#include "host/util/util.h"
31#include "host/ble_store.h"
32#include "store/config/ble_store_config.h"
33
35extern "C" void ble_store_config_init(void);
36#include "freertos/FreeRTOS.h"
37#include "freertos/semphr.h"
38#include <cstring>
39#include <algorithm>
40
41namespace cdc::hal {
42
46static void bleHostTask(void* param);
47
51static int gattcSvcDiscCb(uint16_t connHandle, const struct ble_gatt_error* error,
52 const struct ble_gatt_svc* service, void* arg);
53static int gattcChrDiscCb(uint16_t connHandle, const struct ble_gatt_error* error,
54 const struct ble_gatt_chr* chr, void* arg);
55static int gattcReadCb(uint16_t connHandle, const struct ble_gatt_error* error,
56 struct ble_gatt_attr* attr, void* arg);
57static int gattcWriteCb(uint16_t connHandle, const struct ble_gatt_error* error,
58 struct ble_gatt_attr* attr, void* arg);
59
63
64static constexpr uint8_t MAX_REGISTERED_SERVICES = IBluetoothController::MAX_REGISTERED_SERVICES;
66static constexpr uint8_t PLUGIN_SERVICE_SLOT = MAX_REGISTERED_SERVICES - 1;
67static constexpr uint8_t MAX_CHARS_PER_SERVICE = IBluetoothController::MAX_CHARS_PER_SERVICE;
68static constexpr uint8_t MAX_DESCRIPTORS_PER_CHAR = 2;
69static constexpr uint8_t MAX_ADV_UUIDS = 4;
70static constexpr uint8_t MAX_CONN_CALLBACKS = 6;
71static constexpr uint8_t MAX_CONNECTIONS = 2;
72static constexpr uint8_t MAX_SUBSCRIBE_ENTRIES = 16;
73static constexpr uint8_t MAX_BONDS = 5;
74
76static constexpr uint32_t kConnectSettleMs = 50;
78static constexpr uint32_t kDisconnectDrainPollMs = 20;
80static constexpr uint32_t kDisconnectDrainTimeoutMs = 1000;
81
85struct InternalService {
86 bool active = false;
87
88 // NimBLE-native UUID storage
89 ble_uuid_any_t svcUuid;
90 ble_uuid_any_t charUuids[MAX_CHARS_PER_SERVICE];
91
92 // NimBLE characteristic + service definitions (must persist)
93 ble_gatt_chr_def nimbleChars[MAX_CHARS_PER_SERVICE + 1]; // +1 terminator
94 ble_gatt_svc_def nimbleSvcs[2]; // +1 terminator
95
96 // Per-characteristic descriptor backing storage (e.g. HID Report Reference).
97 // Packed layout per descriptor slot: [0] = data length, [1..4] = data bytes.
98 ble_uuid_any_t dscUuids[MAX_CHARS_PER_SERVICE][MAX_DESCRIPTORS_PER_CHAR];
99 ble_gatt_dsc_def dscDefs[MAX_CHARS_PER_SERVICE][MAX_DESCRIPTORS_PER_CHAR + 1];
100 uint8_t dscPacked[MAX_CHARS_PER_SERVICE][MAX_DESCRIPTORS_PER_CHAR][5];
101
102 // Callbacks registered by the module
103 GattWriteCallback writeCallbacks[MAX_CHARS_PER_SERVICE];
104 GattReadCallback readCallbacks[MAX_CHARS_PER_SERVICE];
105 uint8_t numChars = 0;
106};
107
108EXT_RAM_BSS_ATTR static InternalService s_services[MAX_REGISTERED_SERVICES];
109
116static void convertUuid(const BleUuid& src, ble_uuid_any_t& dst) {
117 if (src.type == BleUuid::UUID_16) {
118 dst.u.type = BLE_UUID_TYPE_16;
119 dst.u16.u.type = BLE_UUID_TYPE_16;
120 dst.u16.value = src.u16;
121 } else {
122 dst.u.type = BLE_UUID_TYPE_128;
123 dst.u128.u.type = BLE_UUID_TYPE_128;
124 memcpy(dst.u128.value, src.u128, 16);
125 }
126}
127
134static ble_gatt_chr_flags mapProperties(uint8_t props, uint8_t perms) {
135 ble_gatt_chr_flags flags = 0;
136 if (props & GattProp::READ) flags |= BLE_GATT_CHR_F_READ;
137 if (props & GattProp::WRITE) flags |= BLE_GATT_CHR_F_WRITE;
138 if (props & GattProp::WRITE_NO_RSP) flags |= BLE_GATT_CHR_F_WRITE_NO_RSP;
139 if (props & GattProp::NOTIFY) flags |= BLE_GATT_CHR_F_NOTIFY;
140 if (props & GattProp::INDICATE) flags |= BLE_GATT_CHR_F_INDICATE;
141
142 // Encryption requirements
143 if (perms & GattPerm::READ_ENC) flags |= BLE_GATT_CHR_F_READ_ENC;
144 if (perms & GattPerm::WRITE_ENC) flags |= BLE_GATT_CHR_F_WRITE_ENC;
145
146 return flags;
147}
148
164EXT_RAM_BSS_ATTR static uint8_t s_gattAccessBuf[512];
165
166static int gattServiceAccessCb(uint16_t connHandle, uint16_t attrHandle,
167 struct ble_gatt_access_ctxt* ctxt, void* arg) {
168 auto* svc = static_cast<InternalService*>(arg);
169 if (!svc) return BLE_ATT_ERR_UNLIKELY;
170
171 // Find which characteristic was accessed by matching UUID
172 for (uint8_t i = 0; i < svc->numChars; i++) {
173 if (ble_uuid_cmp(ctxt->chr->uuid, &svc->charUuids[i].u) == 0) {
174 if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR && svc->writeCallbacks[i]) {
175 uint16_t len = OS_MBUF_PKTLEN(ctxt->om);
176 if (len > sizeof(s_gattAccessBuf)) len = sizeof(s_gattAccessBuf);
177 ble_hs_mbuf_to_flat(ctxt->om, s_gattAccessBuf, len, nullptr);
178 return svc->writeCallbacks[i](connHandle, attrHandle, s_gattAccessBuf, len);
179 }
180 if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR && svc->readCallbacks[i]) {
181 uint16_t len = sizeof(s_gattAccessBuf);
182 int rc = svc->readCallbacks[i](connHandle, attrHandle, s_gattAccessBuf, &len);
183 if (rc == 0 && len > 0) {
184 os_mbuf_append(ctxt->om, s_gattAccessBuf, len);
185 }
186 return rc;
187 }
188 return 0; // No callback = allow silently
189 }
190 }
191
192 return BLE_ATT_ERR_UNLIKELY;
193}
194
202static int gattStaticDescriptorAccessCb(uint16_t connHandle, uint16_t attrHandle,
203 struct ble_gatt_access_ctxt* ctxt, void* arg) {
204 (void)connHandle;
205 (void)attrHandle;
206 if (ctxt->op != BLE_GATT_ACCESS_OP_READ_DSC) return BLE_ATT_ERR_UNLIKELY;
207 if (!arg) return BLE_ATT_ERR_UNLIKELY;
208
209 // Layout: [0] = length, [1..4] = bytes
210 const uint8_t* packed = static_cast<const uint8_t*>(arg);
211 uint8_t len = packed[0];
212 return os_mbuf_append(ctxt->om, packed + 1, len) == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
213}
214
218class BluetoothController : public IBluetoothController {
219public:
220 BluetoothController() {
221 instance_ = this;
222 lifecycleMutex_ = xSemaphoreCreateRecursiveMutex();
223 }
224
229 bool init() override;
230 bool start() override;
231 void stop() override;
232 core::ServiceState getState() const override { return state_; }
233 const char* getName() const override { return "bluetooth"; }
235
240 bool enable() override;
241 void disable() override;
242 bool isEnabled() const override { return enabled_; }
243 bool getMacAddress(uint8_t* mac) const override;
244 void setDeviceName(const char* name) override;
245 const char* getDeviceName() const override { return deviceName_; }
246 bool isConnected() const override;
247 void disconnect() override;
248 int8_t getRssi() const override;
249
254 void startAdvertising() override;
255 void stopAdvertising() override;
256 bool isAdvertising() const override { return advertising_; }
257 bool addAdvertisingUuid(const BleUuid& uuid) override;
258 void removeAdvertisingUuid(const BleUuid& uuid) override;
259 bool setAdvertisingManufacturerData(uint16_t companyId,
260 const uint8_t* data, uint16_t len) override;
261 void clearAdvertisingManufacturerData() override;
262 void setAppearance(uint16_t appearance) override;
264
269 bool startScan(uint32_t durationMs, bool keepAdvertising) override;
270 void stopScan() override;
271 bool isScanComplete() const override { return !scanning_; }
272 uint8_t getScanResults(BleScanResult* results, uint8_t maxResults) override;
274
279 bool registerGattService(const GattServiceDef& service,
280 bool pluginReserved = false) override;
281 bool unregisterGattService(const BleUuid& serviceUuid) override;
282 bool sendNotification(uint16_t connHandle, uint16_t attrHandle,
283 const uint8_t* data, uint16_t len) override;
284 uint16_t getMtu() const override;
285 uint16_t getConnectionHandle() const override { return primaryConnHandle(); }
286 void clearAllBonds() override;
288
293 bool connect(const uint8_t* addr, uint8_t addrType) override;
294 void cancelConnect() override;
295 bool discoverServiceByUuid(uint16_t connHandle, const BleUuid& uuid) override;
296 bool writeCharacteristic(uint16_t connHandle, uint16_t attrHandle,
297 const uint8_t* data, uint16_t len,
298 bool withResponse) override;
299 bool readCharacteristic(uint16_t connHandle, uint16_t attrHandle) override;
300 bool enableNotifications(uint16_t connHandle, uint16_t cccdHandle) override;
301 void disconnectHandle(uint16_t connHandle) override;
302 ListenerToken addServiceDiscoveryCallback(ServiceDiscoveryCallback cb) override;
303 ListenerToken addCharacteristicReadCallback(CharacteristicReadCallback cb) override;
304 ListenerToken addNotificationCallback(NotificationCallback cb) override;
305 ListenerToken addWriteCompleteCallback(WriteCompleteCallback cb) override;
306 void removeServiceDiscoveryCallback(ListenerToken token) override;
307 void removeCharacteristicReadCallback(ListenerToken token) override;
308 void removeNotificationCallback(ListenerToken token) override;
309 void removeWriteCompleteCallback(ListenerToken token) override;
310 void setServiceDiscoveryCallback(ServiceDiscoveryCallback cb) override;
311 void setCharacteristicReadCallback(CharacteristicReadCallback cb) override;
312 void setNotificationCallback(NotificationCallback cb) override;
313 void setWriteCompleteCallback(WriteCompleteCallback cb) override;
315
320 ListenerToken addConnectionCallback(ConnectionCallback cb) override;
321 ListenerToken addDisconnectionCallback(DisconnectionCallback cb) override;
322 void removeConnectionCallback(ListenerToken token) override;
323 void removeDisconnectionCallback(ListenerToken token) override;
325
330 ListenerToken addNumericComparisonCallback(NumericComparisonCallback cb) override;
331 void removeNumericComparisonCallback(ListenerToken token) override;
332 void setNumericComparisonCallback(NumericComparisonCallback cb) override;
333 void respondToNumericComparison(uint16_t connHandle, bool accept) override;
334 void setPasskeyCallback(PasskeyCallback cb) override;
335 void setAuthCompleteCallback(AuthCompleteCallback cb) override;
336 ListenerToken addEncryptionChangeCallback(EncChangeCallback cb) override;
337 void removeEncryptionChangeCallback(ListenerToken token) override;
338 bool initiateSecurity(uint16_t connHandle) override;
339 bool getPeerIdAddr(uint16_t connHandle, uint8_t addr[6], uint8_t* addrType) const override;
340 void forgetBond(const uint8_t addr[6], uint8_t addrType) override;
341 uint8_t getBondedDevices(BleBondInfo* out, uint8_t maxCount) const override;
343
348 void onConnect(uint16_t connHandle, bool isPeripheral);
349 void onDisconnect(uint16_t connHandle, int reason);
350 void onSync();
351 void onScanResult(const ble_gap_disc_desc* disc);
352 void onScanComplete();
353 void onAdvComplete();
354 void onPasskeyAction(uint16_t connHandle, const ble_gap_passkey_params* params);
355 void onEncChange(uint16_t connHandle, int status);
356 void onSubscribe(const struct ble_gap_event* event);
357 void onMtuExchange(uint16_t connHandle, uint16_t mtu);
359
363 template <typename CB>
364 struct ListenerSlot {
365 bool active = false;
366 CB callback;
367 };
368
372 struct ConnectionState {
373 bool active = false;
374 uint16_t handle = BLE_HS_CONN_HANDLE_NONE;
375 bool isPeripheral = false; // true = we are peripheral, false = central
376 uint16_t mtu = 23; // ATT MTU including 3-byte header
377 };
378
382 struct SubscribeEntry {
383 bool active = false;
384 uint16_t connHandle;
385 uint16_t attrHandle;
386 bool notify;
387 bool indicate;
388 };
389
390 uint16_t primaryConnHandle() const;
391 int8_t findConnectionSlot(uint16_t handle) const;
392
393private:
394 core::ServiceState state_ = core::ServiceState::UNINITIALIZED;
395
396 // Serializes stack lifecycle transitions (enable/disable and the GATT
397 // register/unregister rebuild) across caller tasks. NimBLE host-task
398 // callbacks never take it, so holding it across the blocking teardown
399 // cannot deadlock against the host task. Recursive: register -> disable ->
400 // enable re-enter on the same task.
401 SemaphoreHandle_t lifecycleMutex_ = nullptr;
402
403 bool enabled_ = false;
404 bool synced_ = false;
405 bool advertising_ = false;
406 bool scanning_ = false;
407 bool scanWasAdvertising_ = false;
408 char deviceName_[32] = "CDC Badge";
409 uint8_t ownAddrType_ = BLE_OWN_ADDR_PUBLIC;
410
412 ConnectionState connections_[MAX_CONNECTIONS] = {};
413
415 SubscribeEntry subscribes_[MAX_SUBSCRIBE_ENTRIES] = {};
416
418 BleScanResult scanResults_[MAX_SCAN_RESULTS] = {};
419 // Per-result flag: name came from a Complete Local Name (0x09). Prevents
420 // downgrading it to a Shortened name (0x08) on later advertising events.
421 bool scanNameComplete_[MAX_SCAN_RESULTS] = {};
422 uint8_t scanResultCount_ = 0;
423
425 BleUuid advUuids_[MAX_ADV_UUIDS] = {};
426 uint8_t advUuidCount_ = 0;
427
429 uint16_t appearance_ = 0;
430
431 ListenerSlot<ConnectionCallback> connCallbacks_[MAX_CONN_CALLBACKS] = {};
432 ListenerSlot<DisconnectionCallback> disconnCallbacks_[MAX_CONN_CALLBACKS] = {};
433 ListenerSlot<NumericComparisonCallback> numCmpCallbacks_[MAX_CONN_CALLBACKS] = {};
434 ListenerSlot<ServiceDiscoveryCallback> svcDiscoveryCallbacks_[MAX_CONN_CALLBACKS] = {};
435 ListenerSlot<CharacteristicReadCallback> charReadCallbacks_[MAX_CONN_CALLBACKS] = {};
436 ListenerSlot<NotificationCallback> notifyCallbacks_[MAX_CONN_CALLBACKS] = {};
437 ListenerSlot<WriteCompleteCallback> writeCompleteCallbacks_[MAX_CONN_CALLBACKS] = {};
438 ListenerSlot<EncChangeCallback> encChangeCallbacks_[MAX_CONN_CALLBACKS] = {};
439
441 PasskeyCallback passkeyCb_;
442 AuthCompleteCallback authCompleteCb_;
443
445 DiscoveredService discoveredSvc_ = {};
446 BleUuid discoverTargetUuid_ = {};
447 uint16_t discoverSvcStart_ = 0;
448 uint16_t discoverSvcEnd_ = 0;
449
450 // Manufacturer data for advertising
451 uint8_t mfgData_[31] = {};
452 uint16_t mfgDataLen_ = 0;
453 uint16_t mfgCompanyId_ = 0;
454 bool mfgDataSet_ = false;
455
456 // Singleton access for callbacks
457public:
458 static BluetoothController* instance_;
459 friend void bleHostTask(void* param);
460 friend int bleGapEventCallback(struct ble_gap_event* event, void* arg);
461 friend int gattcSvcDiscCb(uint16_t, const struct ble_gatt_error*,
462 const struct ble_gatt_svc*, void*);
463 friend int gattcChrDiscCb(uint16_t, const struct ble_gatt_error*,
464 const struct ble_gatt_chr*, void*);
465 friend int gattcReadCb(uint16_t, const struct ble_gatt_error*,
466 struct ble_gatt_attr*, void*);
467 friend int gattcWriteCb(uint16_t, const struct ble_gatt_error*,
468 struct ble_gatt_attr*, void*);
469};
470
474BluetoothController* BluetoothController::instance_ = nullptr;
475
482int bleGapEventCallback(struct ble_gap_event* event, void* arg) {
483 (void)arg;
484 auto* ctrl = BluetoothController::instance_;
485 if (!ctrl) return 0;
486
487 switch (event->type) {
488 case BLE_GAP_EVENT_CONNECT:
489 if (event->connect.status == 0) {
490 struct ble_gap_conn_desc desc;
491 bool isPeripheral = true;
492 if (ble_gap_conn_find(event->connect.conn_handle, &desc) == 0) {
493 isPeripheral = (desc.role == BLE_GAP_ROLE_SLAVE);
494 }
495 ctrl->onConnect(event->connect.conn_handle, isPeripheral);
496 } else {
497 LOG_W(TAG, "Connection failed, status=%d", event->connect.status);
498 }
499 break;
500
501 case BLE_GAP_EVENT_DISCONNECT:
502 ctrl->onDisconnect(event->disconnect.conn.conn_handle,
503 event->disconnect.reason);
504 break;
505
506 case BLE_GAP_EVENT_CONN_UPDATE:
507 LOG_I(TAG, "Connection updated");
508 break;
509
510 case BLE_GAP_EVENT_ADV_COMPLETE:
511 LOG_D(TAG, "Advertising complete");
512 ctrl->onAdvComplete();
513 break;
514
515 case BLE_GAP_EVENT_MTU:
516 LOG_I(TAG, "MTU updated: handle=%d value=%d",
517 event->mtu.conn_handle, event->mtu.value);
518 ctrl->onMtuExchange(event->mtu.conn_handle, event->mtu.value);
519 break;
520
521 case BLE_GAP_EVENT_DISC:
522 ctrl->onScanResult(&event->disc);
523 break;
524
525 case BLE_GAP_EVENT_DISC_COMPLETE:
526 LOG_D(TAG, "Scan complete");
527 ctrl->onScanComplete();
528 break;
529
530 case BLE_GAP_EVENT_PASSKEY_ACTION:
531 ctrl->onPasskeyAction(event->passkey.conn_handle,
532 &event->passkey.params);
533 break;
534
535 case BLE_GAP_EVENT_ENC_CHANGE:
536 ctrl->onEncChange(event->enc_change.conn_handle,
537 event->enc_change.status);
538 break;
539
540 case BLE_GAP_EVENT_REPEAT_PAIRING: {
541 // Peer is re-pairing: delete the old bond so the new one can replace it.
542 struct ble_gap_conn_desc desc;
543 if (ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc) == 0) {
544 ble_store_util_delete_peer(&desc.peer_id_addr);
545 }
546 return BLE_GAP_REPEAT_PAIRING_RETRY;
547 }
548
549 case BLE_GAP_EVENT_SUBSCRIBE:
550 ctrl->onSubscribe(event);
551 break;
552
553 case BLE_GAP_EVENT_NOTIFY_RX:
554 if (event->notify_rx.om) {
555 uint16_t len = OS_MBUF_PKTLEN(event->notify_rx.om);
556 if (len > sizeof(s_gattAccessBuf)) len = sizeof(s_gattAccessBuf);
557 ble_hs_mbuf_to_flat(event->notify_rx.om, s_gattAccessBuf, len, nullptr);
558 for (uint8_t i = 0; i < MAX_CONN_CALLBACKS; i++) {
559 if (ctrl->notifyCallbacks_[i].active &&
560 ctrl->notifyCallbacks_[i].callback) {
561 ctrl->notifyCallbacks_[i].callback(
562 event->notify_rx.conn_handle,
563 event->notify_rx.attr_handle,
564 s_gattAccessBuf, len);
565 }
566 }
567 }
568 break;
569
570 default:
571 break;
572 }
573
574 return 0;
575}
576
581static void bleSyncCallback() {
582 if (BluetoothController::instance_) {
583 BluetoothController::instance_->onSync();
584 }
585}
586
592static void bleResetCallback(int reason) {
593 LOG_E(TAG, "BLE host reset, reason=%d", reason);
594}
595
601static void bleHostTask(void* param) {
602 (void)param;
603 LOG_I(TAG, "NimBLE host task started");
604 nimble_port_run();
605 nimble_port_freertos_deinit();
606}
607
612bool BluetoothController::init() {
613 if (state_ != core::ServiceState::UNINITIALIZED) {
614 return state_ == core::ServiceState::INITIALIZED ||
616 }
617
618 // instance_ is set in the constructor, but reaffirm here in case multiple
619 // instances are ever created (only the most recently initialized wins).
620 instance_ = this;
621
622 // Release classic BT memory (we only use BLE)
623 ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));
624
626 LOG_I(TAG, "Bluetooth controller initialized");
627 return true;
628}
629
634bool BluetoothController::start() {
635 if (state_ == core::ServiceState::INITIALIZED ||
636 state_ == core::ServiceState::STOPPED) {
638 return true;
639 }
640 return state_ == core::ServiceState::STARTED;
641}
642
646void BluetoothController::stop() {
647 cdc::core::RecursiveMutexGuard guard(lifecycleMutex_);
648 if (state_ == core::ServiceState::STARTED) {
649 if (enabled_) {
650 disable();
651 }
653 }
654}
655
660bool BluetoothController::enable() {
661 cdc::core::RecursiveMutexGuard guard(lifecycleMutex_);
662 if (enabled_) {
663 return true;
664 }
665
666 if (state_ != core::ServiceState::STARTED) {
667 LOG_E(TAG, "Cannot enable - service not started");
668 return false;
669 }
670
671 // Initialize NimBLE
672 esp_err_t ret = nimble_port_init();
673 if (ret != ESP_OK) {
674 LOG_E(TAG, "nimble_port_init failed: %d", ret);
675 return false;
676 }
677
678 // Configure NimBLE host
679 ble_hs_cfg.reset_cb = bleResetCallback;
680 ble_hs_cfg.sync_cb = bleSyncCallback;
681 ble_hs_cfg.gatts_register_cb = nullptr;
682 ble_hs_cfg.store_status_cb = nullptr;
683
684 // Security: Display+YesNo for numeric comparison pairing
685 ble_hs_cfg.sm_io_cap = BLE_SM_IO_CAP_DISP_YES_NO;
686 ble_hs_cfg.sm_bonding = 1;
687 ble_hs_cfg.sm_mitm = 1;
688 ble_hs_cfg.sm_sc = 1;
689 ble_hs_cfg.sm_our_key_dist = BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;
690 ble_hs_cfg.sm_their_key_dist = BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;
691
692 // Initialize mandatory GAP and GATT services
693 ble_svc_gap_init();
694 ble_svc_gatt_init();
695
696 // Commit module GATT services registered before BLE was enabled (and restore
697 // those from a previous session, since disable() tears down the GATT DB).
698 for (int i = 0; i < MAX_REGISTERED_SERVICES; i++) {
699 if (!s_services[i].active) continue;
700 int grc = ble_gatts_count_cfg(s_services[i].nimbleSvcs);
701 if (grc == 0) grc = ble_gatts_add_svcs(s_services[i].nimbleSvcs);
702 if (grc != 0) {
703 LOG_E(TAG, "Deferred GATT service slot %d commit failed: %d", i, grc);
704 } else {
705 LOG_I(TAG, "Committed GATT service slot %d", i);
706 }
707 }
708
709 // Wire up the NVS-backed bond store
710 ble_store_config_init();
711
712 // Set device name
713 ble_svc_gap_device_name_set(deviceName_);
714
715 // Start NimBLE host task
716 nimble_port_freertos_init(bleHostTask);
717
718 enabled_ = true;
719 LOG_I(TAG, "Bluetooth enabled");
720 return true;
721}
722
726void BluetoothController::disable() {
727 cdc::core::RecursiveMutexGuard guard(lifecycleMutex_);
728 if (!enabled_) {
729 return;
730 }
731
732 // Down before teardown: GAP callbacks dispatched while the host task drains
733 // gate advertising restarts on these flags.
734 enabled_ = false;
735 synced_ = false;
736
737 // Stop advertising before touching connections so a bonded peer cannot
738 // (re)connect into the teardown window.
739 ble_gap_adv_stop();
740 advertising_ = false;
741
742 // A GAP procedure with a duration timer (scan, pending connection) must be
743 // cancelled before nimble_port_deinit() releases the event queue its callout
744 // is bound to.
745 if (scanning_) {
746 ble_gap_disc_cancel();
747 scanning_ = false;
748 scanWasAdvertising_ = false;
749 }
750 ble_gap_conn_cancel();
751
752 // nimble_port_stop() must not run while a connection is live: in the
753 // STOPPING state the host deinits the ble_hs_timer callout while queued
754 // timer events (e.g. the 30 s SM timer of an encryption re-establishment)
755 // still dereference it - LoadProhibited in npl_freertos_callout_is_active.
756 // Let an in-flight connect surface in the connection table, then terminate
757 // everything and wait until the host task has processed every disconnect.
758 vTaskDelay(pdMS_TO_TICKS(kConnectSettleMs));
759 uint32_t waited = 0;
760 for (;; waited += kDisconnectDrainPollMs) {
761 bool anyActive = false;
762 for (uint8_t i = 0; i < MAX_CONNECTIONS; i++) {
763 if (connections_[i].active) {
764 ble_gap_terminate(connections_[i].handle, BLE_ERR_REM_USER_CONN_TERM);
765 anyActive = true;
766 }
767 }
768 if (!anyActive) break;
769 if (waited >= kDisconnectDrainTimeoutMs) {
770 LOG_W(TAG, "Disconnect drain timed out, forcing BLE shutdown");
771 break;
772 }
773 vTaskDelay(pdMS_TO_TICKS(kDisconnectDrainPollMs));
774 }
775
776 // Shutdown NimBLE and wait for the host task to actually exit before deinit
777 int rc = nimble_port_stop();
778 if (rc == 0) {
779 // nimble_port_stop signals the host task; give it time to drain
780 vTaskDelay(pdMS_TO_TICKS(50));
781 nimble_port_deinit();
782 }
783
784 for (uint8_t i = 0; i < MAX_CONNECTIONS; i++) connections_[i].active = false;
785 for (uint8_t i = 0; i < MAX_SUBSCRIBE_ENTRIES; i++) subscribes_[i].active = false;
786
787 LOG_I(TAG, "Bluetooth disabled");
788}
789
795bool BluetoothController::getMacAddress(uint8_t* mac) const {
796 if (!mac) return false;
797
798 if (enabled_ && synced_) {
799 // Get address from NimBLE
800 int rc = ble_hs_id_copy_addr(ownAddrType_, mac, nullptr);
801 return rc == 0;
802 }
803
804 // Fallback: read from efuse
805 esp_read_mac(mac, ESP_MAC_BT);
806 return true;
807}
808
813void BluetoothController::setDeviceName(const char* name) {
814 if (!name) return;
815
816 strncpy(deviceName_, name, sizeof(deviceName_) - 1);
817 deviceName_[sizeof(deviceName_) - 1] = '\0';
818
819 if (enabled_ && synced_) {
820 ble_svc_gap_device_name_set(deviceName_);
821 }
822}
823
827void BluetoothController::disconnect() {
828 for (uint8_t i = 0; i < MAX_CONNECTIONS; i++) {
829 if (connections_[i].active) {
830 ble_gap_terminate(connections_[i].handle, BLE_ERR_REM_USER_CONN_TERM);
831 }
832 }
833}
834
835bool BluetoothController::isConnected() const {
836 for (uint8_t i = 0; i < MAX_CONNECTIONS; i++) {
837 if (connections_[i].active) return true;
838 }
839 return false;
840}
841
842uint16_t BluetoothController::primaryConnHandle() const {
843 for (uint8_t i = 0; i < MAX_CONNECTIONS; i++) {
844 if (connections_[i].active) return connections_[i].handle;
845 }
846 return BLE_HS_CONN_HANDLE_NONE;
847}
848
849int8_t BluetoothController::findConnectionSlot(uint16_t handle) const {
850 for (int8_t i = 0; i < (int8_t)MAX_CONNECTIONS; i++) {
851 if (connections_[i].active && connections_[i].handle == handle) return i;
852 }
853 return -1;
854}
855
856void BluetoothController::clearAllBonds() {
857 int rc = ble_store_clear();
858 if (rc != 0) {
859 LOG_E(TAG, "ble_store_clear failed: %d", rc);
860 } else {
861 LOG_I(TAG, "All bonds cleared");
862 }
863}
864
865void BluetoothController::onEncChange(uint16_t connHandle, int status) {
866 LOG_I(TAG, "Encryption %s on handle %d (status=%d)",
867 status == 0 ? "established" : "failed", connHandle, status);
868 if (authCompleteCb_) {
869 authCompleteCb_(status == 0);
870 }
871 for (uint8_t i = 0; i < MAX_CONN_CALLBACKS; i++) {
872 if (encChangeCallbacks_[i].active && encChangeCallbacks_[i].callback) {
873 encChangeCallbacks_[i].callback(connHandle, status);
874 }
875 }
876}
877
878void BluetoothController::onMtuExchange(uint16_t connHandle, uint16_t mtu) {
879 int8_t slot = findConnectionSlot(connHandle);
880 if (slot >= 0) {
881 connections_[slot].mtu = mtu;
882 }
883}
884
885void BluetoothController::onSubscribe(const struct ble_gap_event* event) {
886 if (!event) return;
887 LOG_I(TAG, "Subscribe: handle=%d attr=%d notify=%d indicate=%d",
888 event->subscribe.conn_handle, event->subscribe.attr_handle,
889 event->subscribe.cur_notify, event->subscribe.cur_indicate);
890
891 // Find or allocate a subscribe entry
892 int slot = -1;
893 for (int i = 0; i < MAX_SUBSCRIBE_ENTRIES; i++) {
894 if (subscribes_[i].active &&
895 subscribes_[i].connHandle == event->subscribe.conn_handle &&
896 subscribes_[i].attrHandle == event->subscribe.attr_handle) {
897 slot = i;
898 break;
899 }
900 }
901 if (slot < 0) {
902 for (int i = 0; i < MAX_SUBSCRIBE_ENTRIES; i++) {
903 if (!subscribes_[i].active) { slot = i; break; }
904 }
905 }
906 if (slot < 0) {
907 LOG_W(TAG, "Subscribe table full");
908 return;
909 }
910 subscribes_[slot].active = true;
911 subscribes_[slot].connHandle = event->subscribe.conn_handle;
912 subscribes_[slot].attrHandle = event->subscribe.attr_handle;
913 subscribes_[slot].notify = event->subscribe.cur_notify != 0;
914 subscribes_[slot].indicate = event->subscribe.cur_indicate != 0;
915 if (!subscribes_[slot].notify && !subscribes_[slot].indicate) {
916 subscribes_[slot].active = false;
917 }
918}
919
924int8_t BluetoothController::getRssi() const {
925 uint16_t handle = primaryConnHandle();
926 if (handle == BLE_HS_CONN_HANDLE_NONE) {
927 return 0;
928 }
929
930 int8_t rssi = 0;
931 int rc = ble_gap_conn_rssi(handle, &rssi);
932 return (rc == 0) ? rssi : 0;
933}
934
939void BluetoothController::onConnect(uint16_t connHandle, bool isPeripheral) {
940 advertising_ = false;
941 LOG_I(TAG, "Device connected (handle=%d, role=%s)",
942 connHandle, isPeripheral ? "peripheral" : "central");
943
944 // Track in connection table
945 int slot = -1;
946 for (int i = 0; i < MAX_CONNECTIONS; i++) {
947 if (!connections_[i].active) { slot = i; break; }
948 }
949 if (slot >= 0) {
950 connections_[slot].active = true;
951 connections_[slot].handle = connHandle;
952 connections_[slot].isPeripheral = isPeripheral;
953 connections_[slot].mtu = 23;
954 } else {
955 LOG_W(TAG, "Connection table full, dropping handle %d", connHandle);
956 }
957
958 // As central, negotiate a larger ATT MTU up front. Service/characteristic
959 // discovery that follows gives it time to complete before the first write,
960 // so offers (which carry the sender name) and data chunks are not capped at
961 // the 23-byte default.
962 if (!isPeripheral) {
963 ble_gattc_exchange_mtu(connHandle, nullptr, nullptr);
964 }
965
966 // Dispatch to registered listeners
967 for (uint8_t i = 0; i < MAX_CONN_CALLBACKS; i++) {
968 if (connCallbacks_[i].active && connCallbacks_[i].callback) {
969 connCallbacks_[i].callback(connHandle);
970 }
971 }
972}
973
979void BluetoothController::onDisconnect(uint16_t connHandle, int reason) {
980 LOG_I(TAG, "Device disconnected (handle=%d reason=%d)", connHandle, reason);
981
982 bool wasPeripheral = true;
983 int8_t slot = findConnectionSlot(connHandle);
984 if (slot >= 0) {
985 wasPeripheral = connections_[slot].isPeripheral;
986 connections_[slot].active = false;
987 }
988
989 // Drop subscriptions associated with this connection
990 for (uint8_t i = 0; i < MAX_SUBSCRIBE_ENTRIES; i++) {
991 if (subscribes_[i].active && subscribes_[i].connHandle == connHandle) {
992 subscribes_[i].active = false;
993 }
994 }
995
996 // Dispatch to registered listeners
997 for (uint8_t i = 0; i < MAX_CONN_CALLBACKS; i++) {
998 if (disconnCallbacks_[i].active && disconnCallbacks_[i].callback) {
999 disconnCallbacks_[i].callback(connHandle, reason);
1000 }
1001 }
1002
1003 // Role-aware advertising restart: only if we were the peripheral.
1004 advertising_ = false;
1005 if (wasPeripheral) {
1006 startAdvertising();
1007 }
1008}
1009
1013void BluetoothController::onSync() {
1014 synced_ = true;
1015
1016 // Determine best address type
1017 int rc = ble_hs_util_ensure_addr(0);
1018 if (rc != 0) {
1019 LOG_E(TAG, "Failed to ensure address: %d", rc);
1020 return;
1021 }
1022
1023 rc = ble_hs_id_infer_auto(0, &ownAddrType_);
1024 if (rc != 0) {
1025 LOG_E(TAG, "Failed to infer address type: %d", rc);
1026 ownAddrType_ = BLE_OWN_ADDR_PUBLIC;
1027 }
1028
1029 uint8_t addr[6];
1030 ble_hs_id_copy_addr(ownAddrType_, addr, nullptr);
1031 LOG_I(TAG, "BLE synced, addr=%02X:%02X:%02X:%02X:%02X:%02X",
1032 addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]);
1033
1034 // Auto-start advertising after sync
1035 startAdvertising();
1036}
1037
1041
1045void BluetoothController::startAdvertising() {
1046 if (!enabled_ || !synced_) return;
1047
1048 // Stop current advertising if running
1049 if (advertising_) {
1050 ble_gap_adv_stop();
1051 advertising_ = false;
1052 }
1053
1054 struct ble_gap_adv_params advParams = {};
1055 advParams.conn_mode = BLE_GAP_CONN_MODE_UND;
1056 advParams.disc_mode = BLE_GAP_DISC_MODE_GEN;
1057 advParams.itvl_min = BLE_GAP_ADV_FAST_INTERVAL1_MIN;
1058 advParams.itvl_max = BLE_GAP_ADV_FAST_INTERVAL1_MAX;
1059
1060 // Split advertised UUIDs by width. Both 16-bit (e.g. HID 0x1812) and 128-bit
1061 // service UUIDs go into the primary PDU when they fit, so HOGP hosts recognize
1062 // the device and UUID-filtering scanners (which read only the primary payload)
1063 // match it. Overflowing fields spill into the scan response below.
1064 ble_uuid16_t uuid16s[MAX_ADV_UUIDS];
1065 ble_uuid128_t uuid128s[MAX_ADV_UUIDS];
1066 uint8_t num16 = 0, num128 = 0;
1067 for (uint8_t i = 0; i < advUuidCount_; i++) {
1068 if (advUuids_[i].type == BleUuid::UUID_16 && num16 < MAX_ADV_UUIDS) {
1069 uuid16s[num16].u.type = BLE_UUID_TYPE_16;
1070 uuid16s[num16].value = advUuids_[i].u16;
1071 num16++;
1072 } else if (advUuids_[i].type == BleUuid::UUID_128 && num128 < MAX_ADV_UUIDS) {
1073 uuid128s[num128].u.type = BLE_UUID_TYPE_128;
1074 memcpy(uuid128s[num128].value, advUuids_[i].u128, 16);
1075 num128++;
1076 }
1077 }
1078
1079 // Primary advertising data: flags + appearance + 16-bit + 128-bit UUIDs + name.
1080 struct ble_hs_adv_fields fields = {};
1081 fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
1082 fields.tx_pwr_lvl_is_present = 1;
1083 fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO;
1084 if (appearance_ != 0) {
1085 fields.appearance = appearance_;
1086 fields.appearance_is_present = 1;
1087 }
1088 if (num16 > 0) {
1089 fields.uuids16 = uuid16s;
1090 fields.num_uuids16 = num16;
1091 fields.uuids16_is_complete = 1;
1092 }
1093 if (num128 > 0) {
1094 fields.uuids128 = uuid128s;
1095 fields.num_uuids128 = num128;
1096 fields.uuids128_is_complete = 1;
1097 }
1098 fields.name = (uint8_t*)deviceName_;
1099 fields.name_len = strlen(deviceName_);
1100 fields.name_is_complete = 1;
1101
1102 // The 31-byte primary PDU can overflow. Spill into the scan response in order
1103 // of decreasing importance: first the name (an active scanner fetches it from
1104 // the scan response anyway), then the 128-bit service UUIDs.
1105 bool nameInScanRsp = false;
1106 bool uuid128InScanRsp = false;
1107 int rc = ble_gap_adv_set_fields(&fields);
1108 if (rc == BLE_HS_EMSGSIZE) {
1109 fields.name = nullptr;
1110 fields.name_len = 0;
1111 fields.name_is_complete = 0;
1112 nameInScanRsp = true;
1113 rc = ble_gap_adv_set_fields(&fields);
1114 }
1115 if (rc == BLE_HS_EMSGSIZE && num128 > 0) {
1116 fields.uuids128 = nullptr;
1117 fields.num_uuids128 = 0;
1118 fields.uuids128_is_complete = 0;
1119 uuid128InScanRsp = true;
1120 rc = ble_gap_adv_set_fields(&fields);
1121 }
1122 if (rc != 0) {
1123 LOG_E(TAG, "Failed to set adv fields: %d", rc);
1124 return;
1125 }
1126
1127 // Scan response carries whatever did not fit into the primary PDU plus
1128 // manufacturer data.
1129 if (nameInScanRsp || uuid128InScanRsp || mfgDataSet_) {
1130 struct ble_hs_adv_fields rsp = {};
1131
1132 if (nameInScanRsp) {
1133 rsp.name = (uint8_t*)deviceName_;
1134 rsp.name_len = strlen(deviceName_);
1135 rsp.name_is_complete = 1;
1136 }
1137 if (uuid128InScanRsp) {
1138 rsp.uuids128 = uuid128s;
1139 rsp.num_uuids128 = num128;
1140 rsp.uuids128_is_complete = 1;
1141 }
1142
1143 // Manufacturer-specific data (company ID + payload)
1144 uint8_t mfgAdvBuf[33];
1145 if (mfgDataSet_) {
1146 mfgAdvBuf[0] = mfgCompanyId_ & 0xFF;
1147 mfgAdvBuf[1] = (mfgCompanyId_ >> 8) & 0xFF;
1148 memcpy(mfgAdvBuf + 2, mfgData_, mfgDataLen_);
1149 rsp.mfg_data = mfgAdvBuf;
1150 rsp.mfg_data_len = mfgDataLen_ + 2;
1151 }
1152
1153 rc = ble_gap_adv_rsp_set_fields(&rsp);
1154 if (rc != 0) {
1155 LOG_W(TAG, "Failed to set scan response: %d (continuing without)", rc);
1156 }
1157 }
1158
1159 rc = ble_gap_adv_start(ownAddrType_, nullptr, BLE_HS_FOREVER,
1160 &advParams, bleGapEventCallback, nullptr);
1161 if (rc != 0) {
1162 LOG_E(TAG, "Failed to start advertising: %d", rc);
1163 return;
1164 }
1165
1166 advertising_ = true;
1167 LOG_I(TAG, "Advertising started (%d service UUIDs)", advUuidCount_);
1168}
1169
1173void BluetoothController::stopAdvertising() {
1174 if (!advertising_) return;
1175
1176 ble_gap_adv_stop();
1177 advertising_ = false;
1178 LOG_I(TAG, "Advertising stopped");
1179}
1180
1184void BluetoothController::onAdvComplete() {
1185 advertising_ = false;
1186}
1187
1192void BluetoothController::setAppearance(uint16_t appearance) {
1193 if (appearance_ == appearance) return;
1194 appearance_ = appearance;
1195 if (enabled_ && synced_ && advertising_) {
1196 startAdvertising();
1197 }
1198}
1199
1203
1209bool BluetoothController::startScan(uint32_t durationMs, bool keepAdvertising) {
1210 if (!enabled_ || !synced_ || scanning_) return false;
1211
1212 // ESP32-S3 NimBLE supports Peripheral + Observer multi-role, so advertising
1213 // and scanning can run concurrently. keepAdvertising leaves the beacon up so
1214 // two scanning badges still see each other; otherwise stop advertising first.
1215 bool wasAdvertising = advertising_;
1216 if (advertising_ && !keepAdvertising) {
1217 ble_gap_adv_stop();
1218 advertising_ = false;
1219 LOG_D(TAG, "Stopped advertising for scan");
1220 }
1221
1222 // Clear previous results
1223 scanResultCount_ = 0;
1224 memset(scanResults_, 0, sizeof(scanResults_));
1225 memset(scanNameComplete_, 0, sizeof(scanNameComplete_));
1226
1227 struct ble_gap_disc_params discParams = {};
1228 discParams.filter_duplicates = 0; // Allow duplicates to get scan responses with names
1229 discParams.passive = 0; // Active scan to get names
1230 discParams.itvl = 0; // Use defaults
1231 discParams.window = 0;
1232 discParams.filter_policy = 0;
1233 discParams.limited = 0;
1234
1235 // durationMs 0 means scan continuously until stopScan().
1236 int32_t duration = (durationMs == 0) ? BLE_HS_FOREVER
1237 : static_cast<int32_t>(durationMs);
1238 int rc = ble_gap_disc(ownAddrType_, duration, &discParams,
1239 bleGapEventCallback, nullptr);
1240 if (rc != 0) {
1241 LOG_E(TAG, "Failed to start scan: %d", rc);
1242 // Restore advertising if we stopped it
1243 if (wasAdvertising && !keepAdvertising) {
1244 startAdvertising();
1245 }
1246 return false;
1247 }
1248
1249 // Only restore advertising on scan-complete if we actually stopped it.
1250 scanWasAdvertising_ = keepAdvertising ? false : wasAdvertising;
1251 scanning_ = true;
1252 LOG_I(TAG, "Scan started (%lu ms%s)", (unsigned long)durationMs,
1253 keepAdvertising ? ", adv kept" : "");
1254 return true;
1255}
1256
1260void BluetoothController::stopScan() {
1261 if (!scanning_) return;
1262
1263 ble_gap_disc_cancel();
1264 scanning_ = false;
1265 LOG_I(TAG, "Scan stopped");
1266}
1267
1280static bool parseAdvName(const uint8_t* data, uint8_t dataLen,
1281 char* name, size_t nameMaxLen, bool* isComplete = nullptr) {
1282 uint8_t pos = 0;
1283 while (pos < dataLen) {
1284 uint8_t len = data[pos];
1285 if (len == 0 || pos + len > dataLen) break;
1286 uint8_t type = data[pos + 1];
1287 // 0x09 = Complete Local Name, 0x08 = Shortened Local Name
1288 if (type == 0x09 || type == 0x08) {
1289 uint8_t nameLen = len - 1;
1290 size_t copyLen = (nameLen < nameMaxLen - 1) ? nameLen : nameMaxLen - 1;
1291 memcpy(name, &data[pos + 2], copyLen);
1292 name[copyLen] = '\0';
1293 if (isComplete) *isComplete = (type == 0x09);
1294 return true;
1295 }
1296 pos += len + 1;
1297 }
1298 return false;
1299}
1300
1309static void fillScanResult(BleScanResult& result, const ble_gap_disc_desc* disc,
1310 bool* outNameComplete) {
1311 memcpy(result.mac, disc->addr.val, 6);
1312 result.addrType = disc->addr.type;
1313 result.rssi = disc->rssi;
1314 result.name[0] = '\0';
1315
1316 result.advDataLen = (disc->length_data <= sizeof(result.advData))
1317 ? disc->length_data : sizeof(result.advData);
1318 memcpy(result.advData, disc->data, result.advDataLen);
1319
1320 bool isComplete = false;
1321 parseAdvName(disc->data, disc->length_data, result.name, sizeof(result.name), &isComplete);
1322 const bool haveName = (result.name[0] != '\0');
1323 if (outNameComplete) *outNameComplete = haveName && isComplete;
1324 if (!haveName) {
1325 snprintf(result.name, sizeof(result.name), "%02X:%02X:%02X:%02X:%02X:%02X",
1326 result.mac[5], result.mac[4], result.mac[3],
1327 result.mac[2], result.mac[1], result.mac[0]);
1328 }
1329}
1330
1331void BluetoothController::onScanResult(const ble_gap_disc_desc* disc) {
1332 if (!disc) return;
1333
1334 // Update an existing entry (same MAC). Scan responses often carry the name.
1335 for (uint8_t i = 0; i < scanResultCount_; i++) {
1336 if (memcmp(scanResults_[i].mac, disc->addr.val, 6) == 0) {
1337 if (disc->rssi > scanResults_[i].rssi) {
1338 scanResults_[i].rssi = disc->rssi;
1339 }
1340 // Replace the name only with a non-empty one, and never downgrade a
1341 // Complete Local Name (0x09) to a Shortened one (0x08): devices that
1342 // advertise a short name in the primary PDU and the full name in the
1343 // scan response would otherwise flip-flop every interval.
1344 char parsedName[32];
1345 bool isComplete = false;
1346 if (parseAdvName(disc->data, disc->length_data, parsedName, sizeof(parsedName), &isComplete) &&
1347 parsedName[0] != '\0' &&
1348 (isComplete || !scanNameComplete_[i]) &&
1349 strcmp(parsedName, scanResults_[i].name) != 0) {
1350 strncpy(scanResults_[i].name, parsedName, sizeof(scanResults_[i].name) - 1);
1351 scanResults_[i].name[sizeof(scanResults_[i].name) - 1] = '\0';
1352 scanNameComplete_[i] = isComplete;
1353 LOG_D(TAG, "Name updated: %s (evt=0x%02X)", parsedName, disc->event_type);
1354 }
1355 return;
1356 }
1357 }
1358
1359 // New device. Append while there is room; once full, keep the strongest by
1360 // replacing the weakest entry only when the newcomer has a higher RSSI.
1361 uint8_t slot;
1362 if (scanResultCount_ < MAX_SCAN_RESULTS) {
1363 slot = scanResultCount_++;
1364 } else {
1365 uint8_t weakest = 0;
1366 for (uint8_t i = 1; i < scanResultCount_; i++) {
1367 if (scanResults_[i].rssi < scanResults_[weakest].rssi) weakest = i;
1368 }
1369 if (disc->rssi <= scanResults_[weakest].rssi) return;
1370 slot = weakest;
1371 }
1372
1373 bool nameComplete = false;
1374 fillScanResult(scanResults_[slot], disc, &nameComplete);
1375 scanNameComplete_[slot] = nameComplete;
1376 LOG_D(TAG, "Found: %s (RSSI %d) evt=0x%02X dlen=%d",
1377 scanResults_[slot].name, scanResults_[slot].rssi,
1378 disc->event_type, disc->length_data);
1379}
1380
1384void BluetoothController::onScanComplete() {
1385 scanning_ = false;
1386 LOG_I(TAG, "Scan complete, found %d devices", scanResultCount_);
1387
1388 // Restore advertising if it was active before scan
1389 if (scanWasAdvertising_) {
1390 scanWasAdvertising_ = false;
1391 startAdvertising();
1392 }
1393}
1394
1401uint8_t BluetoothController::getScanResults(BleScanResult* results, uint8_t maxResults) {
1402 if (!results || maxResults == 0) return 0;
1403
1404 uint8_t count = (scanResultCount_ < maxResults) ? scanResultCount_ : maxResults;
1405 memcpy(results, scanResults_, count * sizeof(BleScanResult));
1406 return count;
1407}
1408
1412
1418bool BluetoothController::registerGattService(const GattServiceDef& service,
1419 bool pluginReserved) {
1420 cdc::core::RecursiveMutexGuard guard(lifecycleMutex_);
1421 // Reuse the slot already holding this service UUID (idempotent re-register),
1422 // otherwise take a free slot. Registration is allowed while BLE is disabled:
1423 // the slot is stored and committed to NimBLE later from enable().
1424 // System modules draw from slots [0, PLUGIN_SERVICE_SLOT); a plugin draws
1425 // only from the reserved PLUGIN_SERVICE_SLOT, so neither can starve the other.
1426 ble_uuid_any_t wantUuid;
1427 convertUuid(service.uuid, wantUuid);
1428 const int firstSlot = pluginReserved ? PLUGIN_SERVICE_SLOT : 0;
1429 const int lastSlot = pluginReserved ? MAX_REGISTERED_SERVICES : PLUGIN_SERVICE_SLOT;
1430 int slot = -1;
1431 for (int i = firstSlot; i < lastSlot; i++) {
1432 if (s_services[i].active && ble_uuid_cmp(&s_services[i].svcUuid.u, &wantUuid.u) == 0) {
1433 slot = i; break;
1434 }
1435 }
1436 if (slot < 0) {
1437 for (int i = firstSlot; i < lastSlot; i++) {
1438 if (!s_services[i].active) { slot = i; break; }
1439 }
1440 }
1441 if (slot < 0) {
1442 LOG_E(TAG, "No free GATT service slots (max %d)", MAX_REGISTERED_SERVICES);
1443 return false;
1444 }
1445
1446 auto& s = s_services[slot];
1447 memset(&s, 0, sizeof(InternalService));
1448
1449 // Convert service UUID
1450 convertUuid(service.uuid, s.svcUuid);
1451
1452 // Convert characteristics
1453 uint8_t numChars = std::min(service.numCharacteristics, (uint8_t)MAX_CHARS_PER_SERVICE);
1454 s.numChars = numChars;
1455
1456 for (uint8_t i = 0; i < numChars; i++) {
1457 const auto& src = service.characteristics[i];
1458 auto& dst = s.nimbleChars[i];
1459
1460 convertUuid(src.uuid, s.charUuids[i]);
1461
1462 dst.uuid = &s.charUuids[i].u;
1463 dst.access_cb = gattServiceAccessCb;
1464 dst.arg = &s;
1465 dst.descriptors = nullptr;
1466 dst.flags = mapProperties(src.properties, src.permissions);
1467 dst.min_key_size = 0;
1468 dst.val_handle = src.valueHandle;
1469
1470 // Build per-characteristic descriptor list (e.g. HID Report Reference).
1471 uint8_t numDsc = src.numDescriptors;
1472 if (numDsc > MAX_DESCRIPTORS_PER_CHAR) numDsc = MAX_DESCRIPTORS_PER_CHAR;
1473 for (uint8_t d = 0; d < numDsc; d++) {
1474 const GattDescriptor& gd = src.descriptors[d];
1475 uint16_t uuid16 = 0;
1476 switch (gd.kind) {
1477 case GattDescriptorKind::REPORT_REFERENCE: uuid16 = 0x2908; break;
1478 default: uuid16 = 0; break;
1479 }
1480 if (uuid16 == 0) continue;
1481
1482 s.dscUuids[i][d].u.type = BLE_UUID_TYPE_16;
1483 s.dscUuids[i][d].u16.u.type = BLE_UUID_TYPE_16;
1484 s.dscUuids[i][d].u16.value = uuid16;
1485
1486 // Pack as [len][bytes]
1487 uint8_t copyLen = (gd.dataLen <= 4) ? gd.dataLen : 4;
1488 s.dscPacked[i][d][0] = copyLen;
1489 memcpy(&s.dscPacked[i][d][1], gd.data, copyLen);
1490
1491 s.dscDefs[i][d].uuid = &s.dscUuids[i][d].u;
1492 s.dscDefs[i][d].att_flags = BLE_ATT_F_READ;
1493 s.dscDefs[i][d].min_key_size = 0;
1494 s.dscDefs[i][d].access_cb = gattStaticDescriptorAccessCb;
1495 s.dscDefs[i][d].arg = s.dscPacked[i][d];
1496 }
1497 if (numDsc > 0) {
1498 // Terminator descriptor
1499 memset(&s.dscDefs[i][numDsc], 0, sizeof(ble_gatt_dsc_def));
1500 dst.descriptors = s.dscDefs[i];
1501 }
1502
1503 s.writeCallbacks[i] = src.onWrite;
1504 s.readCallbacks[i] = src.onRead;
1505 }
1506
1507 // Terminator
1508 memset(&s.nimbleChars[numChars], 0, sizeof(ble_gatt_chr_def));
1509
1510 // Build NimBLE service definition
1511 s.nimbleSvcs[0].type = BLE_GATT_SVC_TYPE_PRIMARY;
1512 s.nimbleSvcs[0].uuid = &s.svcUuid.u;
1513 s.nimbleSvcs[0].includes = nullptr;
1514 s.nimbleSvcs[0].characteristics = s.nimbleChars;
1515 memset(&s.nimbleSvcs[1], 0, sizeof(ble_gatt_svc_def));
1516
1517 // Commit the slot; enable() (re)adds every active service through the one
1518 // correct init path.
1519 s.active = true;
1520
1521 // BLE not enabled yet: enable() commits this slot later.
1522 if (!enabled_) {
1523 LOG_I(TAG, "GATT service stored, deferred until BLE enable (slot %d)", slot);
1524 return true;
1525 }
1526
1527 // BLE already running: NimBLE cannot add a service to a started GATT server
1528 // in place (ble_svc_gap_init is not re-entrant and asserts on a second
1529 // call), so rebuild the whole stack through the proven disable()/enable()
1530 // path. This drops any active BLE connection.
1531 disable();
1532 if (!enable()) {
1533 s.active = false;
1534 LOG_E(TAG, "GATT service register: BLE restart failed (slot %d)", slot);
1535 return false;
1536 }
1537 LOG_I(TAG, "GATT service registered via BLE restart (slot %d, %d chars)", slot, numChars);
1538 return true;
1539}
1540
1541bool BluetoothController::unregisterGattService(const BleUuid& serviceUuid) {
1542 cdc::core::RecursiveMutexGuard guard(lifecycleMutex_);
1543 ble_uuid_any_t wantUuid;
1544 convertUuid(serviceUuid, wantUuid);
1545 int slot = -1;
1546 for (int i = 0; i < MAX_REGISTERED_SERVICES; i++) {
1547 if (s_services[i].active && ble_uuid_cmp(&s_services[i].svcUuid.u, &wantUuid.u) == 0) {
1548 slot = i; break;
1549 }
1550 }
1551 if (slot < 0) return false;
1552
1553 memset(&s_services[slot], 0, sizeof(InternalService));
1554
1555 // While disabled there is no live GATT DB to rebuild; the slot is simply
1556 // dropped before the next enable() commits the remaining services.
1557 if (!enabled_) {
1558 LOG_I(TAG, "GATT service unregistered (slot %d, deferred)", slot);
1559 return true;
1560 }
1561
1562 // Rebuild the live GATT DB through the proven disable()/enable() path (an
1563 // in-place ble_gatts_reset + ble_svc_gap_init re-init asserts in NimBLE).
1564 // This drops any active BLE connection.
1565 disable();
1566 if (!enable()) {
1567 LOG_E(TAG, "GATT service unregister: BLE restart failed (slot %d)", slot);
1568 return false;
1569 }
1570 LOG_I(TAG, "GATT service unregistered via BLE restart (slot %d)", slot);
1571 return true;
1572}
1573
1582bool BluetoothController::sendNotification(uint16_t connHandle, uint16_t attrHandle,
1583 const uint8_t* data, uint16_t len) {
1584 if (!enabled_ || !data || len == 0) {
1585 return false;
1586 }
1587
1588 auto notifyOne = [&](uint16_t handle) -> bool {
1589 // Check the per-connection CCCD table - skip peers that are not subscribed.
1590 bool subscribed = false;
1591 for (uint8_t i = 0; i < MAX_SUBSCRIBE_ENTRIES; i++) {
1592 if (subscribes_[i].active &&
1593 subscribes_[i].connHandle == handle &&
1594 subscribes_[i].attrHandle == attrHandle &&
1595 subscribes_[i].notify) {
1596 subscribed = true;
1597 break;
1598 }
1599 }
1600 if (!subscribed) return false;
1601
1602 struct os_mbuf* om = ble_hs_mbuf_from_flat(data, len);
1603 if (!om) {
1604 LOG_E(TAG, "Failed to allocate mbuf for notification");
1605 return false;
1606 }
1607 int rc = ble_gatts_notify_custom(handle, attrHandle, om);
1608 if (rc != 0) {
1609 // NimBLE may or may not free the mbuf chain on failure; free
1610 // unconditionally to avoid a leak when rc != 0.
1611 os_mbuf_free_chain(om);
1612 return false;
1613 }
1614 return true;
1615 };
1616
1617 if (connHandle == 0xFFFF || connHandle == BLE_HS_CONN_HANDLE_NONE) {
1618 bool any = false;
1619 for (uint8_t i = 0; i < MAX_CONNECTIONS; i++) {
1620 if (connections_[i].active) {
1621 if (notifyOne(connections_[i].handle)) any = true;
1622 }
1623 }
1624 return any;
1625 }
1626 return notifyOne(connHandle);
1627}
1628
1633uint16_t BluetoothController::getMtu() const {
1634 uint16_t handle = primaryConnHandle();
1635 if (handle == BLE_HS_CONN_HANDLE_NONE) {
1636 return 20; // Default minimum BLE payload (ATT MTU 23 - 3 header bytes)
1637 }
1638 uint16_t mtu = ble_att_mtu(handle);
1639 return (mtu > 3) ? (mtu - 3) : 20;
1640}
1641
1645
1651bool BluetoothController::addAdvertisingUuid(const BleUuid& uuid) {
1652 // Check if already registered
1653 for (uint8_t i = 0; i < advUuidCount_; i++) {
1654 if (advUuids_[i] == uuid) return true;
1655 }
1656
1657 if (advUuidCount_ >= MAX_ADV_UUIDS) {
1658 LOG_E(TAG, "Max advertising UUIDs reached (%d)", MAX_ADV_UUIDS);
1659 return false;
1660 }
1661
1662 advUuids_[advUuidCount_++] = uuid;
1663 LOG_I(TAG, "Advertising UUID registered (%d total)", advUuidCount_);
1664
1665 // Start or restart advertising to include new UUID
1666 if (enabled_ && synced_) {
1667 if (advertising_) {
1668 stopAdvertising();
1669 }
1670 startAdvertising();
1671 }
1672
1673 return true;
1674}
1675
1680void BluetoothController::removeAdvertisingUuid(const BleUuid& uuid) {
1681 for (uint8_t i = 0; i < advUuidCount_; i++) {
1682 if (advUuids_[i] == uuid) {
1683 // Shift remaining UUIDs
1684 for (uint8_t j = i; j < advUuidCount_ - 1; j++) {
1685 advUuids_[j] = advUuids_[j + 1];
1686 }
1687 advUuidCount_--;
1688 LOG_I(TAG, "Advertising UUID removed (%d remaining)", advUuidCount_);
1689
1690 // Restart advertising without removed UUID
1691 if (advertising_) {
1692 stopAdvertising();
1693 startAdvertising();
1694 }
1695 return;
1696 }
1697 }
1698}
1699
1703
1708template <typename CB, size_t N>
1709static IBluetoothController::ListenerToken addListener(
1710 BluetoothController::ListenerSlot<CB> (&slots)[N], CB cb) {
1712 for (size_t i = 0; i < N; i++) {
1713 if (!slots[i].active) {
1714 slots[i].active = true;
1715 slots[i].callback = cb;
1716 return static_cast<IBluetoothController::ListenerToken>(i);
1717 }
1718 }
1720}
1721
1725template <typename CB, size_t N>
1726static void removeListener(
1727 BluetoothController::ListenerSlot<CB> (&slots)[N],
1729 if (token >= N) return;
1730 slots[token].active = false;
1731 slots[token].callback = nullptr;
1732}
1733
1735BluetoothController::addConnectionCallback(ConnectionCallback cb) {
1736 return addListener(connCallbacks_, cb);
1737}
1738
1740BluetoothController::addDisconnectionCallback(DisconnectionCallback cb) {
1741 return addListener(disconnCallbacks_, cb);
1742}
1743
1744void BluetoothController::removeConnectionCallback(ListenerToken token) {
1745 removeListener(connCallbacks_, token);
1746}
1747
1748void BluetoothController::removeDisconnectionCallback(ListenerToken token) {
1749 removeListener(disconnCallbacks_, token);
1750}
1751
1755
1760void BluetoothController::setPasskeyCallback(PasskeyCallback cb) {
1761 passkeyCb_ = cb;
1762}
1763
1768void BluetoothController::setAuthCompleteCallback(AuthCompleteCallback cb) {
1769 authCompleteCb_ = cb;
1770}
1771
1773BluetoothController::addNumericComparisonCallback(NumericComparisonCallback cb) {
1774 return addListener(numCmpCallbacks_, cb);
1775}
1776
1777void BluetoothController::removeNumericComparisonCallback(ListenerToken token) {
1778 removeListener(numCmpCallbacks_, token);
1779}
1780
1781void BluetoothController::setNumericComparisonCallback(NumericComparisonCallback cb) {
1782 for (uint8_t i = 0; i < MAX_CONN_CALLBACKS; i++) numCmpCallbacks_[i].active = false;
1783 if (cb) addListener(numCmpCallbacks_, cb);
1784}
1785
1787BluetoothController::addEncryptionChangeCallback(EncChangeCallback cb) {
1788 return addListener(encChangeCallbacks_, cb);
1789}
1790
1791void BluetoothController::removeEncryptionChangeCallback(ListenerToken token) {
1792 removeListener(encChangeCallbacks_, token);
1793}
1794
1800bool BluetoothController::initiateSecurity(uint16_t connHandle) {
1801 int rc = ble_gap_security_initiate(connHandle);
1802 // BLE_HS_EALREADY: encryption already established or in progress.
1803 if (rc != 0 && rc != BLE_HS_EALREADY) {
1804 LOG_W(TAG, "ble_gap_security_initiate failed: %d", rc);
1805 return false;
1806 }
1807 return true;
1808}
1809
1817bool BluetoothController::getPeerIdAddr(uint16_t connHandle, uint8_t addr[6],
1818 uint8_t* addrType) const {
1819 if (!addr || !addrType) return false;
1820 struct ble_gap_conn_desc desc = {};
1821 if (ble_gap_conn_find(connHandle, &desc) != 0) return false;
1822 std::memcpy(addr, desc.peer_id_addr.val, 6);
1823 *addrType = desc.peer_id_addr.type;
1824 return true;
1825}
1826
1832void BluetoothController::forgetBond(const uint8_t addr[6], uint8_t addrType) {
1833 if (!addr) return;
1834 ble_addr_t peer = {};
1835 peer.type = addrType;
1836 std::memcpy(peer.val, addr, 6);
1837 int rc = ble_gap_unpair(&peer);
1838 if (rc != 0) {
1839 LOG_W(TAG, "ble_gap_unpair failed: %d", rc);
1840 } else {
1841 LOG_I(TAG, "Bond forgotten");
1842 }
1843}
1844
1851uint8_t BluetoothController::getBondedDevices(BleBondInfo* out, uint8_t maxCount) const {
1852 if (!out || maxCount == 0) return 0;
1853
1854 ble_addr_t peers[MAX_BONDS] = {};
1855 int numPeers = 0;
1856 if (ble_store_util_bonded_peers(peers, &numPeers, MAX_BONDS) != 0) {
1857 return 0;
1858 }
1859
1860 uint8_t count = 0;
1861 for (int i = 0; i < numPeers && count < maxCount; i++) {
1862 std::memcpy(out[count].addr, peers[i].val, 6);
1863 out[count].addrType = peers[i].type;
1864 out[count].connected = false;
1865 for (uint8_t c = 0; c < MAX_CONNECTIONS; c++) {
1866 if (!connections_[c].active) continue;
1867 struct ble_gap_conn_desc desc = {};
1868 if (ble_gap_conn_find(connections_[c].handle, &desc) != 0) continue;
1869 if (desc.peer_id_addr.type == peers[i].type &&
1870 std::memcmp(desc.peer_id_addr.val, peers[i].val, 6) == 0) {
1871 out[count].connected = true;
1872 break;
1873 }
1874 }
1875 count++;
1876 }
1877 return count;
1878}
1879
1885void BluetoothController::respondToNumericComparison(uint16_t connHandle, bool accept) {
1886 struct ble_sm_io pkey = {};
1887 pkey.action = BLE_SM_IOACT_NUMCMP;
1888 pkey.numcmp_accept = accept ? 1 : 0;
1889 int rc = ble_sm_inject_io(connHandle, &pkey);
1890 if (rc != 0) {
1891 LOG_E(TAG, "ble_sm_inject_io failed: %d", rc);
1892 }
1893 LOG_I(TAG, "Pairing %s", accept ? "accepted" : "rejected");
1894}
1895
1901void BluetoothController::onPasskeyAction(uint16_t connHandle,
1902 const ble_gap_passkey_params* params) {
1903 if (!params) return;
1904
1905 switch (params->action) {
1906 case BLE_SM_IOACT_NUMCMP: {
1907 LOG_I(TAG, "Numeric comparison: %06lu", (unsigned long)params->numcmp);
1908 LOG_I(TAG, "%s stack free: %lu words", pcTaskGetName(nullptr),
1909 (unsigned long)uxTaskGetStackHighWaterMark(nullptr));
1910 bool dispatched = false;
1911 for (uint8_t i = 0; i < MAX_CONN_CALLBACKS; i++) {
1912 if (numCmpCallbacks_[i].active && numCmpCallbacks_[i].callback) {
1913 numCmpCallbacks_[i].callback(connHandle, params->numcmp);
1914 dispatched = true;
1915 }
1916 }
1917 if (!dispatched) {
1918 // No callback registered: reject by default for safety.
1919 LOG_W(TAG, "No numeric comparison callback registered, rejecting");
1920 respondToNumericComparison(connHandle, false);
1921 }
1922 break;
1923 }
1924
1925 case BLE_SM_IOACT_DISP:
1926 LOG_I(TAG, "Display passkey: %06lu", (unsigned long)params->numcmp);
1927 if (passkeyCb_) {
1928 passkeyCb_(params->numcmp);
1929 }
1930 break;
1931
1932 default:
1933 LOG_W(TAG, "Unhandled passkey action: %d", params->action);
1934 break;
1935 }
1936}
1937
1941
1949bool BluetoothController::setAdvertisingManufacturerData(uint16_t companyId,
1950 const uint8_t* data, uint16_t len) {
1951 if (!data || len > sizeof(mfgData_)) return false;
1952
1953 memcpy(mfgData_, data, len);
1954 mfgDataLen_ = len;
1955 mfgCompanyId_ = companyId;
1956 mfgDataSet_ = true;
1957
1958 if (advertising_) {
1959 stopAdvertising();
1960 startAdvertising();
1961 }
1962 return true;
1963}
1964
1968void BluetoothController::clearAdvertisingManufacturerData() {
1969 mfgDataSet_ = false;
1970 mfgDataLen_ = 0;
1971
1972 if (advertising_) {
1973 stopAdvertising();
1974 startAdvertising();
1975 }
1976}
1977
1981
1990int gattcChrDiscCb(uint16_t connHandle, const struct ble_gatt_error* error,
1991 const struct ble_gatt_chr* chr, void* arg) {
1992 (void)arg;
1993 auto* ctrl = BluetoothController::instance_;
1994 if (!ctrl) return 0;
1995
1996 if (error->status == 0 && chr) {
1997 auto& svc = ctrl->discoveredSvc_;
1999 auto& dc = svc.characteristics[svc.numCharacteristics];
2000 // Use NimBLE's typed helper to safely extract 16-bit UUIDs from
2001 // either BLE_UUID_TYPE_16 or BLE_UUID_TYPE_32 variants. Anything
2002 // larger falls back to 128-bit.
2003 if (chr->uuid.u.type == BLE_UUID_TYPE_16) {
2004 dc.uuid = BleUuid::from16(ble_uuid_u16(&chr->uuid.u));
2005 } else if (chr->uuid.u.type == BLE_UUID_TYPE_32) {
2006 // No native 32-bit support in our generic UUID; convert to 128-bit per BT spec.
2007 uint8_t u128[16] = { 0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00,
2008 0x00, 0x80, 0x00, 0x10, 0x00, 0x00,
2009 0, 0, 0, 0 };
2010 uint32_t v = chr->uuid.u32.value;
2011 u128[12] = v & 0xFF;
2012 u128[13] = (v >> 8) & 0xFF;
2013 u128[14] = (v >> 16) & 0xFF;
2014 u128[15] = (v >> 24) & 0xFF;
2015 dc.uuid = BleUuid::from128(u128);
2016 } else {
2017 dc.uuid = BleUuid::from128(chr->uuid.u128.value);
2018 }
2019 dc.valueHandle = chr->val_handle;
2020 dc.properties = chr->properties;
2021 svc.numCharacteristics++;
2022 }
2023 } else if (error->status == BLE_HS_EDONE) {
2024 LOG_I(TAG, "Char discovery done (%d chars)", ctrl->discoveredSvc_.numCharacteristics);
2025 for (uint8_t i = 0; i < MAX_CONN_CALLBACKS; i++) {
2026 if (ctrl->svcDiscoveryCallbacks_[i].active &&
2027 ctrl->svcDiscoveryCallbacks_[i].callback) {
2028 ctrl->svcDiscoveryCallbacks_[i].callback(
2029 connHandle, &ctrl->discoveredSvc_, true);
2030 }
2031 }
2032 } else {
2033 LOG_E(TAG, "Char discovery error: %d", error->status);
2034 for (uint8_t i = 0; i < MAX_CONN_CALLBACKS; i++) {
2035 if (ctrl->svcDiscoveryCallbacks_[i].active &&
2036 ctrl->svcDiscoveryCallbacks_[i].callback) {
2037 ctrl->svcDiscoveryCallbacks_[i].callback(connHandle, nullptr, true);
2038 }
2039 }
2040 }
2041
2042 return 0;
2043}
2044
2053int gattcSvcDiscCb(uint16_t connHandle, const struct ble_gatt_error* error,
2054 const struct ble_gatt_svc* service, void* arg) {
2055 (void)arg;
2056 auto* ctrl = BluetoothController::instance_;
2057 if (!ctrl) return 0;
2058
2059 auto dispatchFailure = [&]() {
2060 for (uint8_t i = 0; i < MAX_CONN_CALLBACKS; i++) {
2061 if (ctrl->svcDiscoveryCallbacks_[i].active &&
2062 ctrl->svcDiscoveryCallbacks_[i].callback) {
2063 ctrl->svcDiscoveryCallbacks_[i].callback(connHandle, nullptr, true);
2064 }
2065 }
2066 };
2067
2068 if (error->status == 0 && service) {
2069 ctrl->discoverSvcStart_ = service->start_handle;
2070 ctrl->discoverSvcEnd_ = service->end_handle;
2071 LOG_I(TAG, "Service found: handles %d-%d",
2072 service->start_handle, service->end_handle);
2073 } else if (error->status == BLE_HS_EDONE) {
2074 if (ctrl->discoverSvcStart_ != 0) {
2075 int rc = ble_gattc_disc_all_chrs(connHandle,
2076 ctrl->discoverSvcStart_,
2077 ctrl->discoverSvcEnd_,
2078 gattcChrDiscCb, nullptr);
2079 if (rc != 0) {
2080 LOG_E(TAG, "ble_gattc_disc_all_chrs failed: %d", rc);
2081 dispatchFailure();
2082 }
2083 } else {
2084 LOG_W(TAG, "Service not found on remote device");
2085 dispatchFailure();
2086 }
2087 } else {
2088 LOG_E(TAG, "Service discovery error: %d", error->status);
2089 dispatchFailure();
2090 }
2091
2092 return 0;
2093}
2094
2103int gattcReadCb(uint16_t connHandle, const struct ble_gatt_error* error,
2104 struct ble_gatt_attr* attr, void* arg) {
2105 (void)arg;
2106 auto* ctrl = BluetoothController::instance_;
2107 if (!ctrl) return 0;
2108
2109 if (error->status == 0 && attr && attr->om) {
2110 uint16_t len = OS_MBUF_PKTLEN(attr->om);
2111 if (len > sizeof(s_gattAccessBuf)) len = sizeof(s_gattAccessBuf);
2112 ble_hs_mbuf_to_flat(attr->om, s_gattAccessBuf, len, nullptr);
2113 for (uint8_t i = 0; i < MAX_CONN_CALLBACKS; i++) {
2114 if (ctrl->charReadCallbacks_[i].active &&
2115 ctrl->charReadCallbacks_[i].callback) {
2116 ctrl->charReadCallbacks_[i].callback(
2117 connHandle, attr->handle, s_gattAccessBuf, len);
2118 }
2119 }
2120 } else {
2121 LOG_E(TAG, "GATT read failed: %d", error->status);
2122 for (uint8_t i = 0; i < MAX_CONN_CALLBACKS; i++) {
2123 if (ctrl->charReadCallbacks_[i].active &&
2124 ctrl->charReadCallbacks_[i].callback) {
2125 ctrl->charReadCallbacks_[i].callback(connHandle, 0, nullptr, 0);
2126 }
2127 }
2128 }
2129
2130 return 0;
2131}
2132
2141int gattcWriteCb(uint16_t connHandle, const struct ble_gatt_error* error,
2142 struct ble_gatt_attr* attr, void* arg) {
2143 (void)arg;
2144 auto* ctrl = BluetoothController::instance_;
2145 if (!ctrl) return 0;
2146
2147 uint16_t handle = attr ? attr->handle : 0;
2148 for (uint8_t i = 0; i < MAX_CONN_CALLBACKS; i++) {
2149 if (ctrl->writeCompleteCallbacks_[i].active &&
2150 ctrl->writeCompleteCallbacks_[i].callback) {
2151 ctrl->writeCompleteCallbacks_[i].callback(connHandle, handle, error->status);
2152 }
2153 }
2154
2155 return 0;
2156}
2157
2161
2168bool BluetoothController::connect(const uint8_t* addr, uint8_t addrType) {
2169 if (!enabled_ || !synced_ || !addr) return false;
2170
2171 // Must stop advertising to connect as central
2172 if (advertising_) {
2173 ble_gap_adv_stop();
2174 advertising_ = false;
2175 }
2176
2177 ble_addr_t bleAddr;
2178 bleAddr.type = addrType;
2179 memcpy(bleAddr.val, addr, 6);
2180
2181 int rc = ble_gap_connect(ownAddrType_, &bleAddr, 10000, nullptr,
2182 bleGapEventCallback, nullptr);
2183 if (rc != 0) {
2184 LOG_E(TAG, "ble_gap_connect failed: %d", rc);
2185 startAdvertising();
2186 return false;
2187 }
2188
2189 LOG_I(TAG, "Connecting to %02X:%02X:%02X:%02X:%02X:%02X",
2190 addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]);
2191 return true;
2192}
2193
2197void BluetoothController::cancelConnect() {
2198 ble_gap_conn_cancel();
2199}
2200
2207bool BluetoothController::discoverServiceByUuid(uint16_t connHandle, const BleUuid& uuid) {
2208 if (!enabled_) return false;
2209
2210 // Reset discovery state
2211 memset(&discoveredSvc_, 0, sizeof(discoveredSvc_));
2212 discoveredSvc_.uuid = uuid;
2213 discoverTargetUuid_ = uuid;
2214 discoverSvcStart_ = 0;
2215 discoverSvcEnd_ = 0;
2216
2217 ble_uuid_any_t nimbleUuid;
2218 convertUuid(uuid, nimbleUuid);
2219
2220 int rc = ble_gattc_disc_svc_by_uuid(connHandle, &nimbleUuid.u,
2221 gattcSvcDiscCb, nullptr);
2222 if (rc != 0) {
2223 LOG_E(TAG, "ble_gattc_disc_svc_by_uuid failed: %d", rc);
2224 return false;
2225 }
2226
2227 LOG_I(TAG, "Service discovery started (connHandle=%d)", connHandle);
2228 return true;
2229}
2230
2240bool BluetoothController::writeCharacteristic(uint16_t connHandle, uint16_t attrHandle,
2241 const uint8_t* data, uint16_t len,
2242 bool withResponse) {
2243 if (!enabled_ || !data || len == 0) return false;
2244
2245 int rc;
2246 if (withResponse) {
2247 rc = ble_gattc_write_flat(connHandle, attrHandle, data, len,
2248 gattcWriteCb, nullptr);
2249 } else {
2250 // Write-without-response has no confirmation event. Do not invoke
2251 // writeCompleteCallback synchronously - modules treating that as a
2252 // TX confirmation would misreport delivery.
2253 rc = ble_gattc_write_no_rsp_flat(connHandle, attrHandle, data, len);
2254 }
2255
2256 if (rc != 0) {
2257 LOG_E(TAG, "GATT write failed: %d", rc);
2258 return false;
2259 }
2260
2261 return true;
2262}
2263
2270bool BluetoothController::readCharacteristic(uint16_t connHandle, uint16_t attrHandle) {
2271 if (!enabled_) return false;
2272
2273 int rc = ble_gattc_read(connHandle, attrHandle, gattcReadCb, nullptr);
2274 if (rc != 0) {
2275 LOG_E(TAG, "ble_gattc_read failed: %d", rc);
2276 return false;
2277 }
2278
2279 return true;
2280}
2281
2288bool BluetoothController::enableNotifications(uint16_t connHandle, uint16_t cccdHandle) {
2289 if (!enabled_) return false;
2290
2291 uint8_t val[2] = { 0x01, 0x00 };
2292 int rc = ble_gattc_write_flat(connHandle, cccdHandle, val, sizeof(val),
2293 gattcWriteCb, nullptr);
2294 if (rc != 0) {
2295 LOG_E(TAG, "Enable notifications failed: %d", rc);
2296 return false;
2297 }
2298
2299 LOG_I(TAG, "Notifications enabled (cccd=%d)", cccdHandle);
2300 return true;
2301}
2302
2307void BluetoothController::disconnectHandle(uint16_t connHandle) {
2308 ble_gap_terminate(connHandle, BLE_ERR_REM_USER_CONN_TERM);
2309}
2310
2312BluetoothController::addServiceDiscoveryCallback(ServiceDiscoveryCallback cb) {
2313 return addListener(svcDiscoveryCallbacks_, cb);
2314}
2316BluetoothController::addCharacteristicReadCallback(CharacteristicReadCallback cb) {
2317 return addListener(charReadCallbacks_, cb);
2318}
2320BluetoothController::addNotificationCallback(NotificationCallback cb) {
2321 return addListener(notifyCallbacks_, cb);
2322}
2324BluetoothController::addWriteCompleteCallback(WriteCompleteCallback cb) {
2325 return addListener(writeCompleteCallbacks_, cb);
2326}
2327
2328void BluetoothController::removeServiceDiscoveryCallback(ListenerToken t) {
2329 removeListener(svcDiscoveryCallbacks_, t);
2330}
2331void BluetoothController::removeCharacteristicReadCallback(ListenerToken t) {
2332 removeListener(charReadCallbacks_, t);
2333}
2334void BluetoothController::removeNotificationCallback(ListenerToken t) {
2335 removeListener(notifyCallbacks_, t);
2336}
2337void BluetoothController::removeWriteCompleteCallback(ListenerToken t) {
2338 removeListener(writeCompleteCallbacks_, t);
2339}
2340
2341void BluetoothController::setServiceDiscoveryCallback(ServiceDiscoveryCallback cb) {
2342 for (uint8_t i = 0; i < MAX_CONN_CALLBACKS; i++) svcDiscoveryCallbacks_[i].active = false;
2343 if (cb) addListener(svcDiscoveryCallbacks_, cb);
2344}
2345void BluetoothController::setCharacteristicReadCallback(CharacteristicReadCallback cb) {
2346 for (uint8_t i = 0; i < MAX_CONN_CALLBACKS; i++) charReadCallbacks_[i].active = false;
2347 if (cb) addListener(charReadCallbacks_, cb);
2348}
2349void BluetoothController::setNotificationCallback(NotificationCallback cb) {
2350 for (uint8_t i = 0; i < MAX_CONN_CALLBACKS; i++) notifyCallbacks_[i].active = false;
2351 if (cb) addListener(notifyCallbacks_, cb);
2352}
2353void BluetoothController::setWriteCompleteCallback(WriteCompleteCallback cb) {
2354 for (uint8_t i = 0; i < MAX_CONN_CALLBACKS; i++) writeCompleteCallbacks_[i].active = false;
2355 if (cb) addListener(writeCompleteCallbacks_, cb);
2356}
2357
2363 static BluetoothController* g_bluetoothController = new BluetoothController();
2364 return g_bluetoothController;
2365}
2366
2367} // namespace cdc::hal
2368
2369#else // NimBLE not enabled - stub implementation
2370
2371#include "esp_mac.h"
2372#include <cstring>
2373
2374namespace cdc::hal {
2375
2380public:
2385 bool init() override {
2386 LOG_W(TAG, "Bluetooth disabled (NimBLE not configured)");
2388 return true;
2389 }
2390
2394 bool start() override {
2396 return true;
2397 }
2398 void stop() override { state_ = core::ServiceState::STOPPED; }
2399 core::ServiceState getState() const override { return state_; }
2400 const char* getName() const override { return "bluetooth"; }
2401
2402 bool enable() override {
2403 LOG_W(TAG, "Cannot enable - NimBLE not configured in sdkconfig");
2404 return false;
2405 }
2406 void disable() override {}
2407 bool isEnabled() const override { return false; }
2413 bool getMacAddress(uint8_t* mac) const override {
2414 if (mac) esp_read_mac(mac, ESP_MAC_BT);
2415 return mac != nullptr;
2416 }
2417
2421 void setDeviceName(const char* name) override {
2422 if (name) {
2423 strncpy(deviceName_, name, sizeof(deviceName_) - 1);
2424 }
2425 }
2426 const char* getDeviceName() const override { return deviceName_; }
2427 bool isConnected() const override { return false; }
2428 void disconnect() override {}
2429 int8_t getRssi() const override { return 0; }
2430
2431private:
2433 char deviceName_[32] = "CDC Badge";
2434};
2435
2437
2445
2446} // namespace cdc::hal
2447
2448#endif // CONFIG_BT_ENABLED && CONFIG_BT_NIMBLE_ENABLED
static const char * TAG
char name[cdc::hal::ISecureElement::RMEM_NAME_LEN]
uint8_t flags
Shared RAII wrappers for firmware resources.
CDC Log: logging over TinyUSB CDC and UART.
#define LOG_W(tag, fmt,...)
Definition cdc_log.h:146
#define LOG_D(tag, fmt,...)
Definition cdc_log.h:148
#define LOG_I(tag, fmt,...)
Definition cdc_log.h:147
#define LOG_E(tag, fmt,...)
Definition cdc_log.h:145
static constexpr uint8_t MAX_CHARS_PER_SERVICE
static constexpr uint8_t MAX_REGISTERED_SERVICES
bool getMacAddress(uint8_t *mac) const override
Returns BLE MAC address using efuse fallback.
const char * getName() const override
void setDeviceName(const char *name) override
Stores requested device name in local stub buffer.
core::ServiceState getState() const override
const char * getDeviceName() const override
bool start() override
Starts stub controller state.
bool init() override
Initializes stub controller state.
static constexpr ListenerToken INVALID_LISTENER
constexpr uint8_t READ_ENC
constexpr uint8_t WRITE_ENC
constexpr uint8_t INDICATE
constexpr uint8_t NOTIFY
constexpr uint8_t READ
constexpr uint8_t WRITE_NO_RSP
constexpr uint8_t WRITE
static BluetoothControllerStub g_bluetoothController
IBluetoothController * getBluetoothControllerInstance()
Returns singleton Bluetooth stub when NimBLE is unavailable.
IBluetoothController::ListenerToken ListenerToken
void init(hal::IDisplay *display, hal::ISleepController *sleep, LockScreenView *lockScreen)
Initializes shared dependencies used by the settings handlers.
uint8_t u128[16]
GattCharacteristic * characteristics
static BleUuid from16(uint16_t v)
static BleUuid from128(const uint8_t v[16])