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);
59static int gattcDscDiscCb(uint16_t connHandle, const struct ble_gatt_error* error,
60 uint16_t chr_val_handle, const struct ble_gatt_dsc* dsc, void* arg);
61
65
66static constexpr uint8_t MAX_REGISTERED_SERVICES = IBluetoothController::MAX_REGISTERED_SERVICES;
68static constexpr uint8_t PLUGIN_SERVICE_SLOT = MAX_REGISTERED_SERVICES - 1;
69static constexpr uint8_t MAX_CHARS_PER_SERVICE = IBluetoothController::MAX_CHARS_PER_SERVICE;
70static constexpr uint8_t MAX_DESCRIPTORS_PER_CHAR = 2;
71static constexpr uint8_t MAX_ADV_UUIDS = 4;
72static constexpr uint8_t MAX_CONN_CALLBACKS = 6;
73static constexpr uint8_t MAX_CONNECTIONS = 2;
74static constexpr uint8_t MAX_SUBSCRIBE_ENTRIES = 16;
75static constexpr uint8_t MAX_BONDS = 5;
76
78static constexpr uint32_t kConnectSettleMs = 50;
80static constexpr uint32_t kDisconnectDrainPollMs = 20;
82static constexpr uint32_t kDisconnectDrainTimeoutMs = 1000;
83
87struct InternalService {
88 bool active = false;
89
90 // NimBLE-native UUID storage
91 ble_uuid_any_t svcUuid;
92 ble_uuid_any_t charUuids[MAX_CHARS_PER_SERVICE];
93
94 // NimBLE characteristic + service definitions (must persist)
95 ble_gatt_chr_def nimbleChars[MAX_CHARS_PER_SERVICE + 1]; // +1 terminator
96 ble_gatt_svc_def nimbleSvcs[2]; // +1 terminator
97
98 // Per-characteristic descriptor backing storage (e.g. HID Report Reference).
99 // Packed layout per descriptor slot: [0] = data length, [1..4] = data bytes.
100 ble_uuid_any_t dscUuids[MAX_CHARS_PER_SERVICE][MAX_DESCRIPTORS_PER_CHAR];
101 ble_gatt_dsc_def dscDefs[MAX_CHARS_PER_SERVICE][MAX_DESCRIPTORS_PER_CHAR + 1];
102 uint8_t dscPacked[MAX_CHARS_PER_SERVICE][MAX_DESCRIPTORS_PER_CHAR][5];
103
104 // Callbacks registered by the module
105 GattWriteCallback writeCallbacks[MAX_CHARS_PER_SERVICE];
106 GattReadCallback readCallbacks[MAX_CHARS_PER_SERVICE];
107 uint8_t numChars = 0;
108};
109
110EXT_RAM_BSS_ATTR static InternalService s_services[MAX_REGISTERED_SERVICES];
111
118static void convertUuid(const BleUuid& src, ble_uuid_any_t& dst) {
119 if (src.type == BleUuid::UUID_16) {
120 dst.u.type = BLE_UUID_TYPE_16;
121 dst.u16.u.type = BLE_UUID_TYPE_16;
122 dst.u16.value = src.u16;
123 } else {
124 dst.u.type = BLE_UUID_TYPE_128;
125 dst.u128.u.type = BLE_UUID_TYPE_128;
126 memcpy(dst.u128.value, src.u128, 16);
127 }
128}
129
136static ble_gatt_chr_flags mapProperties(uint8_t props, uint8_t perms) {
137 ble_gatt_chr_flags flags = 0;
138 if (props & GattProp::READ) flags |= BLE_GATT_CHR_F_READ;
139 if (props & GattProp::WRITE) flags |= BLE_GATT_CHR_F_WRITE;
140 if (props & GattProp::WRITE_NO_RSP) flags |= BLE_GATT_CHR_F_WRITE_NO_RSP;
141 if (props & GattProp::NOTIFY) flags |= BLE_GATT_CHR_F_NOTIFY;
142 if (props & GattProp::INDICATE) flags |= BLE_GATT_CHR_F_INDICATE;
143
144 // Encryption requirements
145 if (perms & GattPerm::READ_ENC) flags |= BLE_GATT_CHR_F_READ_ENC;
146 if (perms & GattPerm::WRITE_ENC) flags |= BLE_GATT_CHR_F_WRITE_ENC;
147
148 return flags;
149}
150
166EXT_RAM_BSS_ATTR static uint8_t s_gattAccessBuf[512];
167
168static int gattServiceAccessCb(uint16_t connHandle, uint16_t attrHandle,
169 struct ble_gatt_access_ctxt* ctxt, void* arg) {
170 auto* svc = static_cast<InternalService*>(arg);
171 if (!svc) return BLE_ATT_ERR_UNLIKELY;
172
173 // Find which characteristic was accessed by matching UUID
174 for (uint8_t i = 0; i < svc->numChars; i++) {
175 if (ble_uuid_cmp(ctxt->chr->uuid, &svc->charUuids[i].u) == 0) {
176 if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR && svc->writeCallbacks[i]) {
177 uint16_t len = OS_MBUF_PKTLEN(ctxt->om);
178 if (len > sizeof(s_gattAccessBuf)) len = sizeof(s_gattAccessBuf);
179 ble_hs_mbuf_to_flat(ctxt->om, s_gattAccessBuf, len, nullptr);
180 return svc->writeCallbacks[i](connHandle, attrHandle, s_gattAccessBuf, len);
181 }
182 if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR && svc->readCallbacks[i]) {
183 uint16_t len = sizeof(s_gattAccessBuf);
184 int rc = svc->readCallbacks[i](connHandle, attrHandle, s_gattAccessBuf, &len);
185 if (rc == 0 && len > 0) {
186 os_mbuf_append(ctxt->om, s_gattAccessBuf, len);
187 }
188 return rc;
189 }
190 return 0; // No callback = allow silently
191 }
192 }
193
194 return BLE_ATT_ERR_UNLIKELY;
195}
196
204static int gattStaticDescriptorAccessCb(uint16_t connHandle, uint16_t attrHandle,
205 struct ble_gatt_access_ctxt* ctxt, void* arg) {
206 (void)connHandle;
207 (void)attrHandle;
208 if (ctxt->op != BLE_GATT_ACCESS_OP_READ_DSC) return BLE_ATT_ERR_UNLIKELY;
209 if (!arg) return BLE_ATT_ERR_UNLIKELY;
210
211 // Layout: [0] = length, [1..4] = bytes
212 const uint8_t* packed = static_cast<const uint8_t*>(arg);
213 uint8_t len = packed[0];
214 return os_mbuf_append(ctxt->om, packed + 1, len) == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
215}
216
220class BluetoothController : public IBluetoothController {
221public:
222 BluetoothController() {
223 instance_ = this;
224 lifecycleMutex_ = xSemaphoreCreateRecursiveMutex();
225 }
226
231 bool init() override;
232 bool start() override;
233 void stop() override;
234 core::ServiceState getState() const override { return state_; }
235 const char* getName() const override { return "bluetooth"; }
237
242 bool enable() override;
243 void disable() override;
244 void notifySystemReady() override;
245 bool isEnabled() const override { return enabled_; }
246 bool getMacAddress(uint8_t* mac) const override;
247 void setDeviceName(const char* name) override;
248 const char* getDeviceName() const override { return deviceName_; }
249 bool isConnected() const override;
250 void disconnect() override;
251 int8_t getRssi() const override;
252
257 void startAdvertising() override;
258 void stopAdvertising() override;
259 bool isAdvertising() const override { return ble_gap_adv_active() != 0; }
260 bool addAdvertisingUuid(const BleUuid& uuid) override;
261 void removeAdvertisingUuid(const BleUuid& uuid) override;
262 bool setAdvertisingManufacturerData(uint16_t companyId,
263 const uint8_t* data, uint16_t len) override;
264 void clearAdvertisingManufacturerData() override;
265 void setAppearance(uint16_t appearance) override;
267
272 bool startScan(uint32_t durationMs, bool keepAdvertising) override;
273 void stopScan() override;
274 bool isScanComplete() const override { return !scanning_; }
275 uint8_t getScanResults(BleScanResult* results, uint8_t maxResults) override;
277
282 bool registerGattService(const GattServiceDef& service,
283 bool pluginReserved = false) override;
284 bool unregisterGattService(const BleUuid& serviceUuid) override;
285 bool sendNotification(uint16_t connHandle, uint16_t attrHandle,
286 const uint8_t* data, uint16_t len) override;
287 uint16_t getMtu() const override;
288 uint16_t getConnectionHandle() const override { return primaryConnHandle(); }
289 void clearAllBonds() override;
291
296 bool connect(const uint8_t* addr, uint8_t addrType) override;
297 void cancelConnect() override;
298 bool discoverServiceByUuid(uint16_t connHandle, const BleUuid& uuid) override;
299 bool writeCharacteristic(uint16_t connHandle, uint16_t attrHandle,
300 const uint8_t* data, uint16_t len,
301 bool withResponse) override;
302 bool readCharacteristic(uint16_t connHandle, uint16_t attrHandle) override;
303 bool enableNotifications(uint16_t connHandle, uint16_t cccdHandle) override;
304 bool subscribeToCharacteristic(uint16_t connHandle, uint16_t valueHandle) override;
305 void disconnectHandle(uint16_t connHandle) override;
306 ListenerToken addServiceDiscoveryCallback(ServiceDiscoveryCallback cb) override;
307 ListenerToken addCharacteristicReadCallback(CharacteristicReadCallback cb) override;
308 ListenerToken addNotificationCallback(NotificationCallback cb) override;
309 ListenerToken addWriteCompleteCallback(WriteCompleteCallback cb) override;
310 void removeServiceDiscoveryCallback(ListenerToken token) override;
311 void removeCharacteristicReadCallback(ListenerToken token) override;
312 void removeNotificationCallback(ListenerToken token) override;
313 void removeWriteCompleteCallback(ListenerToken token) override;
314 void setServiceDiscoveryCallback(ServiceDiscoveryCallback cb) override;
315 void setCharacteristicReadCallback(CharacteristicReadCallback cb) override;
316 void setNotificationCallback(NotificationCallback cb) override;
317 void setWriteCompleteCallback(WriteCompleteCallback cb) override;
319
324 ListenerToken addConnectionCallback(ConnectionCallback cb) override;
325 ListenerToken addDisconnectionCallback(DisconnectionCallback cb) override;
326 void removeConnectionCallback(ListenerToken token) override;
327 void removeDisconnectionCallback(ListenerToken token) override;
329
334 ListenerToken addNumericComparisonCallback(NumericComparisonCallback cb) override;
335 void removeNumericComparisonCallback(ListenerToken token) override;
336 void setNumericComparisonCallback(NumericComparisonCallback cb) override;
337 void respondToNumericComparison(uint16_t connHandle, bool accept) override;
338 void setPasskeyCallback(PasskeyCallback cb) override;
339 void setAuthCompleteCallback(AuthCompleteCallback cb) override;
340 ListenerToken addEncryptionChangeCallback(EncChangeCallback cb) override;
341 void removeEncryptionChangeCallback(ListenerToken token) override;
342 bool initiateSecurity(uint16_t connHandle) override;
343 bool getPeerIdAddr(uint16_t connHandle, uint8_t addr[6], uint8_t* addrType) const override;
344 void forgetBond(const uint8_t addr[6], uint8_t addrType) override;
345 uint8_t getBondedDevices(BleBondInfo* out, uint8_t maxCount) const override;
347
352 void onConnect(uint16_t connHandle, bool isPeripheral);
353 void onDisconnect(uint16_t connHandle, int reason);
354 void onSync();
355 void onScanResult(const ble_gap_disc_desc* disc);
356 void onScanComplete();
357 void onAdvComplete();
358 void onPasskeyAction(uint16_t connHandle, const ble_gap_passkey_params* params);
359 void onEncChange(uint16_t connHandle, int status);
360 void onSubscribe(const struct ble_gap_event* event);
361 void onMtuExchange(uint16_t connHandle, uint16_t mtu);
363
367 template <typename CB>
368 struct ListenerSlot {
369 bool active = false;
370 CB callback;
371 };
372
376 struct ConnectionState {
377 bool active = false;
378 uint16_t handle = BLE_HS_CONN_HANDLE_NONE;
379 bool isPeripheral = false; // true = we are peripheral, false = central
380 uint16_t mtu = 23; // ATT MTU including 3-byte header
381 };
382
386 struct SubscribeEntry {
387 bool active = false;
388 uint16_t connHandle;
389 uint16_t attrHandle;
390 bool notify;
391 bool indicate;
392 };
393
394 uint16_t primaryConnHandle() const;
395 int8_t findConnectionSlot(uint16_t handle) const;
396
397private:
399 bool enableNow();
400
401 core::ServiceState state_ = core::ServiceState::UNINITIALIZED;
402
403 // Serializes stack lifecycle transitions (enable/disable and the GATT
404 // register/unregister rebuild) across caller tasks. NimBLE host-task
405 // callbacks never take it, so holding it across the blocking teardown
406 // cannot deadlock against the host task. Recursive: register -> disable ->
407 // enable re-enter on the same task.
408 SemaphoreHandle_t lifecycleMutex_ = nullptr;
409
410 bool enabled_ = false;
411 bool synced_ = false;
412 bool systemReady_ = false;
413 bool pendingEnable_ = false;
414 bool advertising_ = false;
415 bool scanning_ = false;
416 bool scanWasAdvertising_ = false;
417 char deviceName_[32] = "CDC Badge";
418 uint8_t ownAddrType_ = BLE_OWN_ADDR_PUBLIC;
419
421 ConnectionState connections_[MAX_CONNECTIONS] = {};
422
424 SubscribeEntry subscribes_[MAX_SUBSCRIBE_ENTRIES] = {};
425
427 BleScanResult scanResults_[MAX_SCAN_RESULTS] = {};
428 // Per-result flag: name came from a Complete Local Name (0x09). Prevents
429 // downgrading it to a Shortened name (0x08) on later advertising events.
430 bool scanNameComplete_[MAX_SCAN_RESULTS] = {};
431 uint8_t scanResultCount_ = 0;
432
434 BleUuid advUuids_[MAX_ADV_UUIDS] = {};
435 uint8_t advUuidCount_ = 0;
436
438 uint16_t appearance_ = 0;
439
440 ListenerSlot<ConnectionCallback> connCallbacks_[MAX_CONN_CALLBACKS] = {};
441 ListenerSlot<DisconnectionCallback> disconnCallbacks_[MAX_CONN_CALLBACKS] = {};
442 ListenerSlot<NumericComparisonCallback> numCmpCallbacks_[MAX_CONN_CALLBACKS] = {};
443 ListenerSlot<ServiceDiscoveryCallback> svcDiscoveryCallbacks_[MAX_CONN_CALLBACKS] = {};
444 ListenerSlot<CharacteristicReadCallback> charReadCallbacks_[MAX_CONN_CALLBACKS] = {};
445 ListenerSlot<NotificationCallback> notifyCallbacks_[MAX_CONN_CALLBACKS] = {};
446 ListenerSlot<WriteCompleteCallback> writeCompleteCallbacks_[MAX_CONN_CALLBACKS] = {};
447 ListenerSlot<EncChangeCallback> encChangeCallbacks_[MAX_CONN_CALLBACKS] = {};
448
450 PasskeyCallback passkeyCb_;
451 AuthCompleteCallback authCompleteCb_;
452
454 DiscoveredService discoveredSvc_ = {};
455 BleUuid discoverTargetUuid_ = {};
456 uint16_t discoverSvcStart_ = 0;
457 uint16_t discoverSvcEnd_ = 0;
458
460 uint16_t pendingSubValueHandle_ = 0;
461 uint16_t pendingSubCccdHandle_ = 0;
462
463 // Manufacturer data for advertising
464 uint8_t mfgData_[31] = {};
465 uint16_t mfgDataLen_ = 0;
466 uint16_t mfgCompanyId_ = 0;
467 bool mfgDataSet_ = false;
468
469 // Singleton access for callbacks
470public:
471 static BluetoothController* instance_;
472 friend void bleHostTask(void* param);
473 friend int bleGapEventCallback(struct ble_gap_event* event, void* arg);
474 friend int gattcSvcDiscCb(uint16_t, const struct ble_gatt_error*,
475 const struct ble_gatt_svc*, void*);
476 friend int gattcChrDiscCb(uint16_t, const struct ble_gatt_error*,
477 const struct ble_gatt_chr*, void*);
478 friend int gattcReadCb(uint16_t, const struct ble_gatt_error*,
479 struct ble_gatt_attr*, void*);
480 friend int gattcWriteCb(uint16_t, const struct ble_gatt_error*,
481 struct ble_gatt_attr*, void*);
482 friend int gattcDscDiscCb(uint16_t, const struct ble_gatt_error*,
483 uint16_t, const struct ble_gatt_dsc*, void*);
484};
485
489BluetoothController* BluetoothController::instance_ = nullptr;
490
497int bleGapEventCallback(struct ble_gap_event* event, void* arg) {
498 (void)arg;
499 auto* ctrl = BluetoothController::instance_;
500 if (!ctrl) return 0;
501
502 switch (event->type) {
503 case BLE_GAP_EVENT_CONNECT:
504 if (event->connect.status == 0) {
505 struct ble_gap_conn_desc desc;
506 bool isPeripheral = true;
507 if (ble_gap_conn_find(event->connect.conn_handle, &desc) == 0) {
508 isPeripheral = (desc.role == BLE_GAP_ROLE_SLAVE);
509 }
510 ctrl->onConnect(event->connect.conn_handle, isPeripheral);
511 } else {
512 LOG_W(TAG, "Connection failed, status=%d", event->connect.status);
513 }
514 break;
515
516 case BLE_GAP_EVENT_DISCONNECT:
517 ctrl->onDisconnect(event->disconnect.conn.conn_handle,
518 event->disconnect.reason);
519 break;
520
521 case BLE_GAP_EVENT_CONN_UPDATE:
522 LOG_I(TAG, "Connection updated");
523 break;
524
525 case BLE_GAP_EVENT_ADV_COMPLETE:
526 LOG_D(TAG, "Advertising complete");
527 ctrl->onAdvComplete();
528 break;
529
530 case BLE_GAP_EVENT_MTU:
531 LOG_I(TAG, "MTU updated: handle=%d value=%d",
532 event->mtu.conn_handle, event->mtu.value);
533 ctrl->onMtuExchange(event->mtu.conn_handle, event->mtu.value);
534 break;
535
536 case BLE_GAP_EVENT_DISC:
537 ctrl->onScanResult(&event->disc);
538 break;
539
540 case BLE_GAP_EVENT_DISC_COMPLETE:
541 LOG_D(TAG, "Scan complete");
542 ctrl->onScanComplete();
543 break;
544
545 case BLE_GAP_EVENT_PASSKEY_ACTION:
546 ctrl->onPasskeyAction(event->passkey.conn_handle,
547 &event->passkey.params);
548 break;
549
550 case BLE_GAP_EVENT_ENC_CHANGE:
551 ctrl->onEncChange(event->enc_change.conn_handle,
552 event->enc_change.status);
553 break;
554
555 case BLE_GAP_EVENT_REPEAT_PAIRING: {
556 // Peer is re-pairing: delete the old bond so the new one can replace it.
557 struct ble_gap_conn_desc desc;
558 if (ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc) == 0) {
559 ble_store_util_delete_peer(&desc.peer_id_addr);
560 }
561 return BLE_GAP_REPEAT_PAIRING_RETRY;
562 }
563
564 case BLE_GAP_EVENT_SUBSCRIBE:
565 ctrl->onSubscribe(event);
566 break;
567
568 case BLE_GAP_EVENT_NOTIFY_RX:
569 if (event->notify_rx.om) {
570 uint16_t len = OS_MBUF_PKTLEN(event->notify_rx.om);
571 if (len > sizeof(s_gattAccessBuf)) len = sizeof(s_gattAccessBuf);
572 ble_hs_mbuf_to_flat(event->notify_rx.om, s_gattAccessBuf, len, nullptr);
573 for (uint8_t i = 0; i < MAX_CONN_CALLBACKS; i++) {
574 if (ctrl->notifyCallbacks_[i].active &&
575 ctrl->notifyCallbacks_[i].callback) {
576 ctrl->notifyCallbacks_[i].callback(
577 event->notify_rx.conn_handle,
578 event->notify_rx.attr_handle,
579 s_gattAccessBuf, len);
580 }
581 }
582 }
583 break;
584
585 default:
586 break;
587 }
588
589 return 0;
590}
591
596static void bleSyncCallback() {
597 if (BluetoothController::instance_) {
598 BluetoothController::instance_->onSync();
599 }
600}
601
607static void bleResetCallback(int reason) {
608 LOG_E(TAG, "BLE host reset, reason=%d", reason);
609}
610
616static void bleHostTask(void* param) {
617 (void)param;
618 LOG_I(TAG, "NimBLE host task started");
619 nimble_port_run();
620 nimble_port_freertos_deinit();
621}
622
627bool BluetoothController::init() {
628 if (state_ != core::ServiceState::UNINITIALIZED) {
629 return state_ == core::ServiceState::INITIALIZED ||
631 }
632
633 // instance_ is set in the constructor, but reaffirm here in case multiple
634 // instances are ever created (only the most recently initialized wins).
635 instance_ = this;
636
637 // Release classic BT memory (we only use BLE)
638 ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));
639
641 LOG_I(TAG, "Bluetooth controller initialized");
642 return true;
643}
644
649bool BluetoothController::start() {
650 if (state_ == core::ServiceState::INITIALIZED ||
651 state_ == core::ServiceState::STOPPED) {
653 return true;
654 }
655 return state_ == core::ServiceState::STARTED;
656}
657
661void BluetoothController::stop() {
662 cdc::core::RecursiveMutexGuard guard(lifecycleMutex_);
663 if (state_ == core::ServiceState::STARTED) {
664 if (enabled_) {
665 disable();
666 }
668 }
669}
670
680bool BluetoothController::enable() {
681 cdc::core::RecursiveMutexGuard guard(lifecycleMutex_);
682 if (enabled_) {
683 return true;
684 }
685 if (!systemReady_) {
686 pendingEnable_ = true;
687 LOG_I(TAG, "BLE enable deferred until system ready");
688 return true;
689 }
690 return enableNow();
691}
692
696void BluetoothController::notifySystemReady() {
697 cdc::core::RecursiveMutexGuard guard(lifecycleMutex_);
698 systemReady_ = true;
699 if (pendingEnable_ && !enabled_) {
700 pendingEnable_ = false;
701 enableNow();
702 }
703}
704
709bool BluetoothController::enableNow() {
710 cdc::core::RecursiveMutexGuard guard(lifecycleMutex_);
711 if (enabled_) {
712 return true;
713 }
714
715 if (state_ != core::ServiceState::STARTED) {
716 LOG_E(TAG, "Cannot enable - service not started");
717 return false;
718 }
719
720 // Initialize NimBLE
721 esp_err_t ret = nimble_port_init();
722 if (ret != ESP_OK) {
723 LOG_E(TAG, "nimble_port_init failed: %d", ret);
724 return false;
725 }
726
727 // Configure NimBLE host
728 ble_hs_cfg.reset_cb = bleResetCallback;
729 ble_hs_cfg.sync_cb = bleSyncCallback;
730 ble_hs_cfg.gatts_register_cb = nullptr;
731 ble_hs_cfg.store_status_cb = nullptr;
732
733 // Security: Display+YesNo for numeric comparison pairing
734 ble_hs_cfg.sm_io_cap = BLE_SM_IO_CAP_DISP_YES_NO;
735 ble_hs_cfg.sm_bonding = 1;
736 ble_hs_cfg.sm_mitm = 1;
737 ble_hs_cfg.sm_sc = 1;
738 ble_hs_cfg.sm_our_key_dist = BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;
739 ble_hs_cfg.sm_their_key_dist = BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;
740
741 // Initialize mandatory GAP and GATT services
742 ble_svc_gap_init();
743 ble_svc_gatt_init();
744
745 // Commit module GATT services registered before BLE was enabled (and restore
746 // those from a previous session, since disable() tears down the GATT DB).
747 for (int i = 0; i < MAX_REGISTERED_SERVICES; i++) {
748 if (!s_services[i].active) continue;
749 int grc = ble_gatts_count_cfg(s_services[i].nimbleSvcs);
750 if (grc == 0) grc = ble_gatts_add_svcs(s_services[i].nimbleSvcs);
751 if (grc != 0) {
752 LOG_E(TAG, "Deferred GATT service slot %d commit failed: %d", i, grc);
753 } else {
754 LOG_I(TAG, "Committed GATT service slot %d", i);
755 }
756 }
757
758 // Wire up the NVS-backed bond store
759 ble_store_config_init();
760
761 // Set device name
762 ble_svc_gap_device_name_set(deviceName_);
763
764 // Start NimBLE host task
765 nimble_port_freertos_init(bleHostTask);
766
767 enabled_ = true;
768 LOG_I(TAG, "Bluetooth enabled");
769 return true;
770}
771
775void BluetoothController::disable() {
776 cdc::core::RecursiveMutexGuard guard(lifecycleMutex_);
777 if (!enabled_) {
778 return;
779 }
780
781 // Down before teardown: GAP callbacks dispatched while the host task drains
782 // gate advertising restarts on these flags.
783 enabled_ = false;
784 synced_ = false;
785
786 // Stop advertising before touching connections so a bonded peer cannot
787 // (re)connect into the teardown window.
788 ble_gap_adv_stop();
789 advertising_ = false;
790
791 // A GAP procedure with a duration timer (scan, pending connection) must be
792 // cancelled before nimble_port_deinit() releases the event queue its callout
793 // is bound to.
794 if (scanning_) {
795 ble_gap_disc_cancel();
796 scanning_ = false;
797 scanWasAdvertising_ = false;
798 }
799 ble_gap_conn_cancel();
800
801 // nimble_port_stop() must not run while a connection is live: in the
802 // STOPPING state the host deinits the ble_hs_timer callout while queued
803 // timer events (e.g. the 30 s SM timer of an encryption re-establishment)
804 // still dereference it - LoadProhibited in npl_freertos_callout_is_active.
805 // Let an in-flight connect surface in the connection table, then terminate
806 // everything and wait until the host task has processed every disconnect.
807 vTaskDelay(pdMS_TO_TICKS(kConnectSettleMs));
808 uint32_t waited = 0;
809 for (;; waited += kDisconnectDrainPollMs) {
810 bool anyActive = false;
811 for (uint8_t i = 0; i < MAX_CONNECTIONS; i++) {
812 if (connections_[i].active) {
813 ble_gap_terminate(connections_[i].handle, BLE_ERR_REM_USER_CONN_TERM);
814 anyActive = true;
815 }
816 }
817 if (!anyActive) break;
818 if (waited >= kDisconnectDrainTimeoutMs) {
819 LOG_W(TAG, "Disconnect drain timed out, forcing BLE shutdown");
820 break;
821 }
822 vTaskDelay(pdMS_TO_TICKS(kDisconnectDrainPollMs));
823 }
824
825 // Shutdown NimBLE and wait for the host task to actually exit before deinit
826 int rc = nimble_port_stop();
827 if (rc == 0) {
828 // nimble_port_stop signals the host task; give it time to drain
829 vTaskDelay(pdMS_TO_TICKS(50));
830 nimble_port_deinit();
831 }
832
833 for (uint8_t i = 0; i < MAX_CONNECTIONS; i++) connections_[i].active = false;
834 for (uint8_t i = 0; i < MAX_SUBSCRIBE_ENTRIES; i++) subscribes_[i].active = false;
835
836 LOG_I(TAG, "Bluetooth disabled");
837}
838
844bool BluetoothController::getMacAddress(uint8_t* mac) const {
845 if (!mac) return false;
846
847 if (enabled_ && synced_) {
848 // Get address from NimBLE
849 int rc = ble_hs_id_copy_addr(ownAddrType_, mac, nullptr);
850 return rc == 0;
851 }
852
853 // Fallback: read from efuse
854 esp_read_mac(mac, ESP_MAC_BT);
855 return true;
856}
857
862void BluetoothController::setDeviceName(const char* name) {
863 if (!name) return;
864
865 strncpy(deviceName_, name, sizeof(deviceName_) - 1);
866 deviceName_[sizeof(deviceName_) - 1] = '\0';
867
868 if (enabled_ && synced_) {
869 ble_svc_gap_device_name_set(deviceName_);
870 }
871}
872
876void BluetoothController::disconnect() {
877 for (uint8_t i = 0; i < MAX_CONNECTIONS; i++) {
878 if (connections_[i].active) {
879 ble_gap_terminate(connections_[i].handle, BLE_ERR_REM_USER_CONN_TERM);
880 }
881 }
882}
883
884bool BluetoothController::isConnected() const {
885 for (uint8_t i = 0; i < MAX_CONNECTIONS; i++) {
886 if (connections_[i].active) return true;
887 }
888 return false;
889}
890
891uint16_t BluetoothController::primaryConnHandle() const {
892 for (uint8_t i = 0; i < MAX_CONNECTIONS; i++) {
893 if (connections_[i].active) return connections_[i].handle;
894 }
895 return BLE_HS_CONN_HANDLE_NONE;
896}
897
898int8_t BluetoothController::findConnectionSlot(uint16_t handle) const {
899 for (int8_t i = 0; i < (int8_t)MAX_CONNECTIONS; i++) {
900 if (connections_[i].active && connections_[i].handle == handle) return i;
901 }
902 return -1;
903}
904
905void BluetoothController::clearAllBonds() {
906 int rc = ble_store_clear();
907 if (rc != 0) {
908 LOG_E(TAG, "ble_store_clear failed: %d", rc);
909 } else {
910 LOG_I(TAG, "All bonds cleared");
911 }
912}
913
914void BluetoothController::onEncChange(uint16_t connHandle, int status) {
915 LOG_I(TAG, "Encryption %s on handle %d (status=%d)",
916 status == 0 ? "established" : "failed", connHandle, status);
917 if (authCompleteCb_) {
918 authCompleteCb_(status == 0);
919 }
920 for (uint8_t i = 0; i < MAX_CONN_CALLBACKS; i++) {
921 if (encChangeCallbacks_[i].active && encChangeCallbacks_[i].callback) {
922 encChangeCallbacks_[i].callback(connHandle, status);
923 }
924 }
925}
926
927void BluetoothController::onMtuExchange(uint16_t connHandle, uint16_t mtu) {
928 int8_t slot = findConnectionSlot(connHandle);
929 if (slot >= 0) {
930 connections_[slot].mtu = mtu;
931 }
932}
933
934void BluetoothController::onSubscribe(const struct ble_gap_event* event) {
935 if (!event) return;
936 LOG_I(TAG, "Subscribe: handle=%d attr=%d notify=%d indicate=%d",
937 event->subscribe.conn_handle, event->subscribe.attr_handle,
938 event->subscribe.cur_notify, event->subscribe.cur_indicate);
939
940 // Find or allocate a subscribe entry
941 int slot = -1;
942 for (int i = 0; i < MAX_SUBSCRIBE_ENTRIES; i++) {
943 if (subscribes_[i].active &&
944 subscribes_[i].connHandle == event->subscribe.conn_handle &&
945 subscribes_[i].attrHandle == event->subscribe.attr_handle) {
946 slot = i;
947 break;
948 }
949 }
950 if (slot < 0) {
951 for (int i = 0; i < MAX_SUBSCRIBE_ENTRIES; i++) {
952 if (!subscribes_[i].active) { slot = i; break; }
953 }
954 }
955 if (slot < 0) {
956 LOG_W(TAG, "Subscribe table full");
957 return;
958 }
959 subscribes_[slot].active = true;
960 subscribes_[slot].connHandle = event->subscribe.conn_handle;
961 subscribes_[slot].attrHandle = event->subscribe.attr_handle;
962 subscribes_[slot].notify = event->subscribe.cur_notify != 0;
963 subscribes_[slot].indicate = event->subscribe.cur_indicate != 0;
964 if (!subscribes_[slot].notify && !subscribes_[slot].indicate) {
965 subscribes_[slot].active = false;
966 }
967}
968
973int8_t BluetoothController::getRssi() const {
974 uint16_t handle = primaryConnHandle();
975 if (handle == BLE_HS_CONN_HANDLE_NONE) {
976 return 0;
977 }
978
979 int8_t rssi = 0;
980 int rc = ble_gap_conn_rssi(handle, &rssi);
981 return (rc == 0) ? rssi : 0;
982}
983
988void BluetoothController::onConnect(uint16_t connHandle, bool isPeripheral) {
989 advertising_ = false;
990 LOG_I(TAG, "Device connected (handle=%d, role=%s)",
991 connHandle, isPeripheral ? "peripheral" : "central");
992
993 // Track in connection table
994 int slot = -1;
995 for (int i = 0; i < MAX_CONNECTIONS; i++) {
996 if (!connections_[i].active) { slot = i; break; }
997 }
998 if (slot >= 0) {
999 connections_[slot].active = true;
1000 connections_[slot].handle = connHandle;
1001 connections_[slot].isPeripheral = isPeripheral;
1002 connections_[slot].mtu = 23;
1003 } else {
1004 LOG_W(TAG, "Connection table full, dropping handle %d", connHandle);
1005 }
1006
1007 // As central, negotiate a larger ATT MTU up front. Service/characteristic
1008 // discovery that follows gives it time to complete before the first write,
1009 // so offers (which carry the sender name) and data chunks are not capped at
1010 // the 23-byte default.
1011 if (!isPeripheral) {
1012 ble_gattc_exchange_mtu(connHandle, nullptr, nullptr);
1013 }
1014
1015 // Dispatch to registered listeners
1016 for (uint8_t i = 0; i < MAX_CONN_CALLBACKS; i++) {
1017 if (connCallbacks_[i].active && connCallbacks_[i].callback) {
1018 connCallbacks_[i].callback(connHandle);
1019 }
1020 }
1021}
1022
1028void BluetoothController::onDisconnect(uint16_t connHandle, int reason) {
1029 LOG_I(TAG, "Device disconnected (handle=%d reason=%d)", connHandle, reason);
1030
1031 bool wasPeripheral = true;
1032 int8_t slot = findConnectionSlot(connHandle);
1033 if (slot >= 0) {
1034 wasPeripheral = connections_[slot].isPeripheral;
1035 connections_[slot].active = false;
1036 }
1037
1038 // Drop subscriptions associated with this connection
1039 for (uint8_t i = 0; i < MAX_SUBSCRIBE_ENTRIES; i++) {
1040 if (subscribes_[i].active && subscribes_[i].connHandle == connHandle) {
1041 subscribes_[i].active = false;
1042 }
1043 }
1044
1045 // Dispatch to registered listeners
1046 for (uint8_t i = 0; i < MAX_CONN_CALLBACKS; i++) {
1047 if (disconnCallbacks_[i].active && disconnCallbacks_[i].callback) {
1048 disconnCallbacks_[i].callback(connHandle, reason);
1049 }
1050 }
1051
1052 // Role-aware advertising restart: only if we were the peripheral.
1053 advertising_ = false;
1054 if (wasPeripheral) {
1055 startAdvertising();
1056 }
1057}
1058
1062void BluetoothController::onSync() {
1063 synced_ = true;
1064
1065 // Determine best address type
1066 int rc = ble_hs_util_ensure_addr(0);
1067 if (rc != 0) {
1068 LOG_E(TAG, "Failed to ensure address: %d", rc);
1069 return;
1070 }
1071
1072 rc = ble_hs_id_infer_auto(0, &ownAddrType_);
1073 if (rc != 0) {
1074 LOG_E(TAG, "Failed to infer address type: %d", rc);
1075 ownAddrType_ = BLE_OWN_ADDR_PUBLIC;
1076 }
1077
1078 uint8_t addr[6];
1079 ble_hs_id_copy_addr(ownAddrType_, addr, nullptr);
1080 LOG_I(TAG, "BLE synced, addr=%02X:%02X:%02X:%02X:%02X:%02X",
1081 addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]);
1082
1083 // Auto-start advertising after sync
1084 startAdvertising();
1085}
1086
1090
1094void BluetoothController::startAdvertising() {
1095 if (!enabled_ || !synced_) return;
1096
1097 // Stop any active advertising before reconfiguring. Query the real radio
1098 // state, not just the cached flag: a flag desync must never leave NimBLE
1099 // advertising while we restart it (which then returns BLE_HS_EALREADY and,
1100 // with isAdvertising() reading the flag, spins reconcile() into a re-advertise
1101 // loop).
1102 if (ble_gap_adv_active()) {
1103 ble_gap_adv_stop();
1104 }
1105 advertising_ = false;
1106
1107 struct ble_gap_adv_params advParams = {};
1108 advParams.conn_mode = BLE_GAP_CONN_MODE_UND;
1109 advParams.disc_mode = BLE_GAP_DISC_MODE_GEN;
1110 advParams.itvl_min = BLE_GAP_ADV_FAST_INTERVAL1_MIN;
1111 advParams.itvl_max = BLE_GAP_ADV_FAST_INTERVAL1_MAX;
1112
1113 // Split advertised UUIDs by width. Both 16-bit (e.g. HID 0x1812) and 128-bit
1114 // service UUIDs go into the primary PDU when they fit, so HOGP hosts recognize
1115 // the device and UUID-filtering scanners (which read only the primary payload)
1116 // match it. Overflowing fields spill into the scan response below.
1117 ble_uuid16_t uuid16s[MAX_ADV_UUIDS];
1118 ble_uuid128_t uuid128s[MAX_ADV_UUIDS];
1119 uint8_t num16 = 0, num128 = 0;
1120 for (uint8_t i = 0; i < advUuidCount_; i++) {
1121 if (advUuids_[i].type == BleUuid::UUID_16 && num16 < MAX_ADV_UUIDS) {
1122 uuid16s[num16].u.type = BLE_UUID_TYPE_16;
1123 uuid16s[num16].value = advUuids_[i].u16;
1124 num16++;
1125 } else if (advUuids_[i].type == BleUuid::UUID_128 && num128 < MAX_ADV_UUIDS) {
1126 uuid128s[num128].u.type = BLE_UUID_TYPE_128;
1127 memcpy(uuid128s[num128].value, advUuids_[i].u128, 16);
1128 num128++;
1129 }
1130 }
1131
1132 // Primary advertising data: flags + appearance + 16-bit + 128-bit UUIDs + name.
1133 struct ble_hs_adv_fields fields = {};
1134 fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
1135 fields.tx_pwr_lvl_is_present = 1;
1136 fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO;
1137 if (appearance_ != 0) {
1138 fields.appearance = appearance_;
1139 fields.appearance_is_present = 1;
1140 }
1141 if (num16 > 0) {
1142 fields.uuids16 = uuid16s;
1143 fields.num_uuids16 = num16;
1144 fields.uuids16_is_complete = 1;
1145 }
1146 if (num128 > 0) {
1147 fields.uuids128 = uuid128s;
1148 fields.num_uuids128 = num128;
1149 fields.uuids128_is_complete = 1;
1150 }
1151 fields.name = (uint8_t*)deviceName_;
1152 fields.name_len = strlen(deviceName_);
1153 fields.name_is_complete = 1;
1154
1155 // The 31-byte primary PDU can overflow. Spill into the scan response in order
1156 // of decreasing importance: first the name (an active scanner fetches it from
1157 // the scan response anyway), then the 128-bit service UUIDs.
1158 bool nameInScanRsp = false;
1159 bool uuid128InScanRsp = false;
1160 int rc = ble_gap_adv_set_fields(&fields);
1161 if (rc == BLE_HS_EMSGSIZE) {
1162 fields.name = nullptr;
1163 fields.name_len = 0;
1164 fields.name_is_complete = 0;
1165 nameInScanRsp = true;
1166 rc = ble_gap_adv_set_fields(&fields);
1167 }
1168 if (rc == BLE_HS_EMSGSIZE && num128 > 0) {
1169 fields.uuids128 = nullptr;
1170 fields.num_uuids128 = 0;
1171 fields.uuids128_is_complete = 0;
1172 uuid128InScanRsp = true;
1173 rc = ble_gap_adv_set_fields(&fields);
1174 }
1175 if (rc != 0) {
1176 LOG_E(TAG, "Failed to set adv fields: %d", rc);
1177 return;
1178 }
1179
1180 // Scan response carries whatever did not fit into the primary PDU plus
1181 // manufacturer data.
1182 if (nameInScanRsp || uuid128InScanRsp || mfgDataSet_) {
1183 struct ble_hs_adv_fields rsp = {};
1184
1185 if (nameInScanRsp) {
1186 rsp.name = (uint8_t*)deviceName_;
1187 rsp.name_len = strlen(deviceName_);
1188 rsp.name_is_complete = 1;
1189 }
1190 if (uuid128InScanRsp) {
1191 rsp.uuids128 = uuid128s;
1192 rsp.num_uuids128 = num128;
1193 rsp.uuids128_is_complete = 1;
1194 }
1195
1196 // Manufacturer-specific data (company ID + payload)
1197 uint8_t mfgAdvBuf[33];
1198 if (mfgDataSet_) {
1199 mfgAdvBuf[0] = mfgCompanyId_ & 0xFF;
1200 mfgAdvBuf[1] = (mfgCompanyId_ >> 8) & 0xFF;
1201 memcpy(mfgAdvBuf + 2, mfgData_, mfgDataLen_);
1202 rsp.mfg_data = mfgAdvBuf;
1203 rsp.mfg_data_len = mfgDataLen_ + 2;
1204 }
1205
1206 rc = ble_gap_adv_rsp_set_fields(&rsp);
1207 if (rc != 0) {
1208 LOG_W(TAG, "Failed to set scan response: %d (continuing without)", rc);
1209 }
1210 }
1211
1212 rc = ble_gap_adv_start(ownAddrType_, nullptr, BLE_HS_FOREVER,
1213 &advParams, bleGapEventCallback, nullptr);
1214 if (rc != 0 && rc != BLE_HS_EALREADY) {
1215 LOG_E(TAG, "Failed to start advertising: %d", rc);
1216 return;
1217 }
1218
1219 advertising_ = true;
1220 LOG_I(TAG, "Advertising started (%d service UUIDs)", advUuidCount_);
1221}
1222
1226void BluetoothController::stopAdvertising() {
1227 if (!advertising_) return;
1228
1229 ble_gap_adv_stop();
1230 advertising_ = false;
1231 LOG_I(TAG, "Advertising stopped");
1232}
1233
1237void BluetoothController::onAdvComplete() {
1238 advertising_ = false;
1239}
1240
1245void BluetoothController::setAppearance(uint16_t appearance) {
1246 if (appearance_ == appearance) return;
1247 appearance_ = appearance;
1248 if (enabled_ && synced_ && advertising_) {
1249 startAdvertising();
1250 }
1251}
1252
1256
1262bool BluetoothController::startScan(uint32_t durationMs, bool keepAdvertising) {
1263 if (!enabled_ || !synced_ || scanning_) return false;
1264
1265 // ESP32-S3 NimBLE supports Peripheral + Observer multi-role, so advertising
1266 // and scanning can run concurrently. keepAdvertising leaves the beacon up so
1267 // two scanning badges still see each other; otherwise stop advertising first.
1268 bool wasAdvertising = advertising_;
1269 if (advertising_ && !keepAdvertising) {
1270 ble_gap_adv_stop();
1271 advertising_ = false;
1272 LOG_D(TAG, "Stopped advertising for scan");
1273 }
1274
1275 // Clear previous results
1276 scanResultCount_ = 0;
1277 memset(scanResults_, 0, sizeof(scanResults_));
1278 memset(scanNameComplete_, 0, sizeof(scanNameComplete_));
1279
1280 struct ble_gap_disc_params discParams = {};
1281 discParams.filter_duplicates = 0; // Allow duplicates to get scan responses with names
1282 discParams.passive = 0; // Active scan to get names
1283 discParams.itvl = 0; // Use defaults
1284 discParams.window = 0;
1285 discParams.filter_policy = 0;
1286 discParams.limited = 0;
1287
1288 // durationMs 0 means scan continuously until stopScan().
1289 int32_t duration = (durationMs == 0) ? BLE_HS_FOREVER
1290 : static_cast<int32_t>(durationMs);
1291 int rc = ble_gap_disc(ownAddrType_, duration, &discParams,
1292 bleGapEventCallback, nullptr);
1293 if (rc != 0) {
1294 LOG_E(TAG, "Failed to start scan: %d", rc);
1295 // Restore advertising if we stopped it
1296 if (wasAdvertising && !keepAdvertising) {
1297 startAdvertising();
1298 }
1299 return false;
1300 }
1301
1302 // Only restore advertising on scan-complete if we actually stopped it.
1303 scanWasAdvertising_ = keepAdvertising ? false : wasAdvertising;
1304 scanning_ = true;
1305 LOG_I(TAG, "Scan started (%lu ms%s)", (unsigned long)durationMs,
1306 keepAdvertising ? ", adv kept" : "");
1307 return true;
1308}
1309
1313void BluetoothController::stopScan() {
1314 if (!scanning_) return;
1315
1316 ble_gap_disc_cancel();
1317 scanning_ = false;
1318 LOG_I(TAG, "Scan stopped");
1319}
1320
1333static bool parseAdvName(const uint8_t* data, uint8_t dataLen,
1334 char* name, size_t nameMaxLen, bool* isComplete = nullptr) {
1335 uint8_t pos = 0;
1336 while (pos < dataLen) {
1337 uint8_t len = data[pos];
1338 if (len == 0 || pos + len > dataLen) break;
1339 uint8_t type = data[pos + 1];
1340 // 0x09 = Complete Local Name, 0x08 = Shortened Local Name
1341 if (type == 0x09 || type == 0x08) {
1342 uint8_t nameLen = len - 1;
1343 size_t copyLen = (nameLen < nameMaxLen - 1) ? nameLen : nameMaxLen - 1;
1344 memcpy(name, &data[pos + 2], copyLen);
1345 name[copyLen] = '\0';
1346 if (isComplete) *isComplete = (type == 0x09);
1347 return true;
1348 }
1349 pos += len + 1;
1350 }
1351 return false;
1352}
1353
1362static void fillScanResult(BleScanResult& result, const ble_gap_disc_desc* disc,
1363 bool* outNameComplete) {
1364 memcpy(result.mac, disc->addr.val, 6);
1365 result.addrType = disc->addr.type;
1366 result.rssi = disc->rssi;
1367 result.name[0] = '\0';
1368
1369 result.advDataLen = (disc->length_data <= sizeof(result.advData))
1370 ? disc->length_data : sizeof(result.advData);
1371 memcpy(result.advData, disc->data, result.advDataLen);
1372
1373 bool isComplete = false;
1374 parseAdvName(disc->data, disc->length_data, result.name, sizeof(result.name), &isComplete);
1375 const bool haveName = (result.name[0] != '\0');
1376 if (outNameComplete) *outNameComplete = haveName && isComplete;
1377 if (!haveName) {
1378 snprintf(result.name, sizeof(result.name), "%02X:%02X:%02X:%02X:%02X:%02X",
1379 result.mac[5], result.mac[4], result.mac[3],
1380 result.mac[2], result.mac[1], result.mac[0]);
1381 }
1382}
1383
1384void BluetoothController::onScanResult(const ble_gap_disc_desc* disc) {
1385 if (!disc) return;
1386
1387 // Update an existing entry (same MAC). Scan responses often carry the name.
1388 for (uint8_t i = 0; i < scanResultCount_; i++) {
1389 if (memcmp(scanResults_[i].mac, disc->addr.val, 6) == 0) {
1390 if (disc->rssi > scanResults_[i].rssi) {
1391 scanResults_[i].rssi = disc->rssi;
1392 }
1393 // Replace the name only with a non-empty one, and never downgrade a
1394 // Complete Local Name (0x09) to a Shortened one (0x08): devices that
1395 // advertise a short name in the primary PDU and the full name in the
1396 // scan response would otherwise flip-flop every interval.
1397 char parsedName[32];
1398 bool isComplete = false;
1399 if (parseAdvName(disc->data, disc->length_data, parsedName, sizeof(parsedName), &isComplete) &&
1400 parsedName[0] != '\0' &&
1401 (isComplete || !scanNameComplete_[i]) &&
1402 strcmp(parsedName, scanResults_[i].name) != 0) {
1403 strncpy(scanResults_[i].name, parsedName, sizeof(scanResults_[i].name) - 1);
1404 scanResults_[i].name[sizeof(scanResults_[i].name) - 1] = '\0';
1405 scanNameComplete_[i] = isComplete;
1406 LOG_D(TAG, "Name updated: %s (evt=0x%02X)", parsedName, disc->event_type);
1407 }
1408 return;
1409 }
1410 }
1411
1412 // New device. Append while there is room; once full, keep the strongest by
1413 // replacing the weakest entry only when the newcomer has a higher RSSI.
1414 uint8_t slot;
1415 if (scanResultCount_ < MAX_SCAN_RESULTS) {
1416 slot = scanResultCount_++;
1417 } else {
1418 uint8_t weakest = 0;
1419 for (uint8_t i = 1; i < scanResultCount_; i++) {
1420 if (scanResults_[i].rssi < scanResults_[weakest].rssi) weakest = i;
1421 }
1422 if (disc->rssi <= scanResults_[weakest].rssi) return;
1423 slot = weakest;
1424 }
1425
1426 bool nameComplete = false;
1427 fillScanResult(scanResults_[slot], disc, &nameComplete);
1428 scanNameComplete_[slot] = nameComplete;
1429 LOG_D(TAG, "Found: %s (RSSI %d) evt=0x%02X dlen=%d",
1430 scanResults_[slot].name, scanResults_[slot].rssi,
1431 disc->event_type, disc->length_data);
1432}
1433
1437void BluetoothController::onScanComplete() {
1438 scanning_ = false;
1439 LOG_I(TAG, "Scan complete, found %d devices", scanResultCount_);
1440
1441 // Restore advertising if it was active before scan
1442 if (scanWasAdvertising_) {
1443 scanWasAdvertising_ = false;
1444 startAdvertising();
1445 }
1446}
1447
1454uint8_t BluetoothController::getScanResults(BleScanResult* results, uint8_t maxResults) {
1455 if (!results || maxResults == 0) return 0;
1456
1457 uint8_t count = (scanResultCount_ < maxResults) ? scanResultCount_ : maxResults;
1458 memcpy(results, scanResults_, count * sizeof(BleScanResult));
1459 return count;
1460}
1461
1465
1471bool BluetoothController::registerGattService(const GattServiceDef& service,
1472 bool pluginReserved) {
1473 cdc::core::RecursiveMutexGuard guard(lifecycleMutex_);
1474 // Reuse the slot already holding this service UUID (idempotent re-register),
1475 // otherwise take a free slot. Registration is allowed while BLE is disabled:
1476 // the slot is stored and committed to NimBLE later from enable().
1477 // System modules draw from slots [0, PLUGIN_SERVICE_SLOT); a plugin draws
1478 // only from the reserved PLUGIN_SERVICE_SLOT, so neither can starve the other.
1479 ble_uuid_any_t wantUuid;
1480 convertUuid(service.uuid, wantUuid);
1481 const int firstSlot = pluginReserved ? PLUGIN_SERVICE_SLOT : 0;
1482 const int lastSlot = pluginReserved ? MAX_REGISTERED_SERVICES : PLUGIN_SERVICE_SLOT;
1483 int slot = -1;
1484 for (int i = firstSlot; i < lastSlot; i++) {
1485 if (s_services[i].active && ble_uuid_cmp(&s_services[i].svcUuid.u, &wantUuid.u) == 0) {
1486 slot = i; break;
1487 }
1488 }
1489 if (slot < 0) {
1490 for (int i = firstSlot; i < lastSlot; i++) {
1491 if (!s_services[i].active) { slot = i; break; }
1492 }
1493 }
1494 if (slot < 0) {
1495 LOG_E(TAG, "No free GATT service slots (max %d)", MAX_REGISTERED_SERVICES);
1496 return false;
1497 }
1498
1499 auto& s = s_services[slot];
1500 memset(&s, 0, sizeof(InternalService));
1501
1502 // Convert service UUID
1503 convertUuid(service.uuid, s.svcUuid);
1504
1505 // Convert characteristics
1506 uint8_t numChars = std::min(service.numCharacteristics, (uint8_t)MAX_CHARS_PER_SERVICE);
1507 s.numChars = numChars;
1508
1509 for (uint8_t i = 0; i < numChars; i++) {
1510 const auto& src = service.characteristics[i];
1511 auto& dst = s.nimbleChars[i];
1512
1513 convertUuid(src.uuid, s.charUuids[i]);
1514
1515 dst.uuid = &s.charUuids[i].u;
1516 dst.access_cb = gattServiceAccessCb;
1517 dst.arg = &s;
1518 dst.descriptors = nullptr;
1519 dst.flags = mapProperties(src.properties, src.permissions);
1520 dst.min_key_size = 0;
1521 dst.val_handle = src.valueHandle;
1522
1523 // Build per-characteristic descriptor list (e.g. HID Report Reference).
1524 uint8_t numDsc = src.numDescriptors;
1525 if (numDsc > MAX_DESCRIPTORS_PER_CHAR) numDsc = MAX_DESCRIPTORS_PER_CHAR;
1526 for (uint8_t d = 0; d < numDsc; d++) {
1527 const GattDescriptor& gd = src.descriptors[d];
1528 uint16_t uuid16 = 0;
1529 switch (gd.kind) {
1530 case GattDescriptorKind::REPORT_REFERENCE: uuid16 = 0x2908; break;
1531 default: uuid16 = 0; break;
1532 }
1533 if (uuid16 == 0) continue;
1534
1535 s.dscUuids[i][d].u.type = BLE_UUID_TYPE_16;
1536 s.dscUuids[i][d].u16.u.type = BLE_UUID_TYPE_16;
1537 s.dscUuids[i][d].u16.value = uuid16;
1538
1539 // Pack as [len][bytes]
1540 uint8_t copyLen = (gd.dataLen <= 4) ? gd.dataLen : 4;
1541 s.dscPacked[i][d][0] = copyLen;
1542 memcpy(&s.dscPacked[i][d][1], gd.data, copyLen);
1543
1544 s.dscDefs[i][d].uuid = &s.dscUuids[i][d].u;
1545 s.dscDefs[i][d].att_flags = BLE_ATT_F_READ;
1546 s.dscDefs[i][d].min_key_size = 0;
1547 s.dscDefs[i][d].access_cb = gattStaticDescriptorAccessCb;
1548 s.dscDefs[i][d].arg = s.dscPacked[i][d];
1549 }
1550 if (numDsc > 0) {
1551 // Terminator descriptor
1552 memset(&s.dscDefs[i][numDsc], 0, sizeof(ble_gatt_dsc_def));
1553 dst.descriptors = s.dscDefs[i];
1554 }
1555
1556 s.writeCallbacks[i] = src.onWrite;
1557 s.readCallbacks[i] = src.onRead;
1558 }
1559
1560 // Terminator
1561 memset(&s.nimbleChars[numChars], 0, sizeof(ble_gatt_chr_def));
1562
1563 // Build NimBLE service definition
1564 s.nimbleSvcs[0].type = BLE_GATT_SVC_TYPE_PRIMARY;
1565 s.nimbleSvcs[0].uuid = &s.svcUuid.u;
1566 s.nimbleSvcs[0].includes = nullptr;
1567 s.nimbleSvcs[0].characteristics = s.nimbleChars;
1568 memset(&s.nimbleSvcs[1], 0, sizeof(ble_gatt_svc_def));
1569
1570 // Commit the slot; enable() (re)adds every active service through the one
1571 // correct init path.
1572 s.active = true;
1573
1574 // BLE not enabled yet: enable() commits this slot later.
1575 if (!enabled_) {
1576 LOG_I(TAG, "GATT service stored, deferred until BLE enable (slot %d)", slot);
1577 return true;
1578 }
1579
1580 // BLE already running: NimBLE cannot add a service to a started GATT server
1581 // in place (ble_svc_gap_init is not re-entrant and asserts on a second
1582 // call), so rebuild the whole stack through the proven disable()/enable()
1583 // path. This drops any active BLE connection.
1584 disable();
1585 if (!enable()) {
1586 s.active = false;
1587 LOG_E(TAG, "GATT service register: BLE restart failed (slot %d)", slot);
1588 return false;
1589 }
1590 LOG_I(TAG, "GATT service registered via BLE restart (slot %d, %d chars)", slot, numChars);
1591 return true;
1592}
1593
1594bool BluetoothController::unregisterGattService(const BleUuid& serviceUuid) {
1595 cdc::core::RecursiveMutexGuard guard(lifecycleMutex_);
1596 ble_uuid_any_t wantUuid;
1597 convertUuid(serviceUuid, wantUuid);
1598 int slot = -1;
1599 for (int i = 0; i < MAX_REGISTERED_SERVICES; i++) {
1600 if (s_services[i].active && ble_uuid_cmp(&s_services[i].svcUuid.u, &wantUuid.u) == 0) {
1601 slot = i; break;
1602 }
1603 }
1604 if (slot < 0) return false;
1605
1606 memset(&s_services[slot], 0, sizeof(InternalService));
1607
1608 // While disabled there is no live GATT DB to rebuild; the slot is simply
1609 // dropped before the next enable() commits the remaining services.
1610 if (!enabled_) {
1611 LOG_I(TAG, "GATT service unregistered (slot %d, deferred)", slot);
1612 return true;
1613 }
1614
1615 // Rebuild the live GATT DB through the proven disable()/enable() path (an
1616 // in-place ble_gatts_reset + ble_svc_gap_init re-init asserts in NimBLE).
1617 // This drops any active BLE connection.
1618 disable();
1619 if (!enable()) {
1620 LOG_E(TAG, "GATT service unregister: BLE restart failed (slot %d)", slot);
1621 return false;
1622 }
1623 LOG_I(TAG, "GATT service unregistered via BLE restart (slot %d)", slot);
1624 return true;
1625}
1626
1635bool BluetoothController::sendNotification(uint16_t connHandle, uint16_t attrHandle,
1636 const uint8_t* data, uint16_t len) {
1637 if (!enabled_ || !data || len == 0) {
1638 return false;
1639 }
1640
1641 auto notifyOne = [&](uint16_t handle) -> bool {
1642 // Check the per-connection CCCD table - skip peers that are not subscribed.
1643 bool subscribed = false;
1644 for (uint8_t i = 0; i < MAX_SUBSCRIBE_ENTRIES; i++) {
1645 if (subscribes_[i].active &&
1646 subscribes_[i].connHandle == handle &&
1647 subscribes_[i].attrHandle == attrHandle &&
1648 subscribes_[i].notify) {
1649 subscribed = true;
1650 break;
1651 }
1652 }
1653 if (!subscribed) return false;
1654
1655 struct os_mbuf* om = ble_hs_mbuf_from_flat(data, len);
1656 if (!om) {
1657 LOG_E(TAG, "Failed to allocate mbuf for notification");
1658 return false;
1659 }
1660 int rc = ble_gatts_notify_custom(handle, attrHandle, om);
1661 if (rc != 0) {
1662 // NimBLE may or may not free the mbuf chain on failure; free
1663 // unconditionally to avoid a leak when rc != 0.
1664 os_mbuf_free_chain(om);
1665 return false;
1666 }
1667 return true;
1668 };
1669
1670 if (connHandle == 0xFFFF || connHandle == BLE_HS_CONN_HANDLE_NONE) {
1671 bool any = false;
1672 for (uint8_t i = 0; i < MAX_CONNECTIONS; i++) {
1673 if (connections_[i].active) {
1674 if (notifyOne(connections_[i].handle)) any = true;
1675 }
1676 }
1677 return any;
1678 }
1679 return notifyOne(connHandle);
1680}
1681
1686uint16_t BluetoothController::getMtu() const {
1687 uint16_t handle = primaryConnHandle();
1688 if (handle == BLE_HS_CONN_HANDLE_NONE) {
1689 return 20; // Default minimum BLE payload (ATT MTU 23 - 3 header bytes)
1690 }
1691 uint16_t mtu = ble_att_mtu(handle);
1692 return (mtu > 3) ? (mtu - 3) : 20;
1693}
1694
1698
1704bool BluetoothController::addAdvertisingUuid(const BleUuid& uuid) {
1705 // Check if already registered
1706 for (uint8_t i = 0; i < advUuidCount_; i++) {
1707 if (advUuids_[i] == uuid) return true;
1708 }
1709
1710 if (advUuidCount_ >= MAX_ADV_UUIDS) {
1711 LOG_E(TAG, "Max advertising UUIDs reached (%d)", MAX_ADV_UUIDS);
1712 return false;
1713 }
1714
1715 advUuids_[advUuidCount_++] = uuid;
1716 LOG_I(TAG, "Advertising UUID registered (%d total)", advUuidCount_);
1717
1718 // Start or restart advertising to include new UUID
1719 if (enabled_ && synced_) {
1720 if (advertising_) {
1721 stopAdvertising();
1722 }
1723 startAdvertising();
1724 }
1725
1726 return true;
1727}
1728
1733void BluetoothController::removeAdvertisingUuid(const BleUuid& uuid) {
1734 for (uint8_t i = 0; i < advUuidCount_; i++) {
1735 if (advUuids_[i] == uuid) {
1736 // Shift remaining UUIDs
1737 for (uint8_t j = i; j < advUuidCount_ - 1; j++) {
1738 advUuids_[j] = advUuids_[j + 1];
1739 }
1740 advUuidCount_--;
1741 LOG_I(TAG, "Advertising UUID removed (%d remaining)", advUuidCount_);
1742
1743 // Restart advertising without removed UUID
1744 if (advertising_) {
1745 stopAdvertising();
1746 startAdvertising();
1747 }
1748 return;
1749 }
1750 }
1751}
1752
1756
1761template <typename CB, size_t N>
1762static IBluetoothController::ListenerToken addListener(
1763 BluetoothController::ListenerSlot<CB> (&slots)[N], CB cb) {
1765 for (size_t i = 0; i < N; i++) {
1766 if (!slots[i].active) {
1767 slots[i].active = true;
1768 slots[i].callback = cb;
1769 return static_cast<IBluetoothController::ListenerToken>(i);
1770 }
1771 }
1773}
1774
1778template <typename CB, size_t N>
1779static void removeListener(
1780 BluetoothController::ListenerSlot<CB> (&slots)[N],
1782 if (token >= N) return;
1783 slots[token].active = false;
1784 slots[token].callback = nullptr;
1785}
1786
1788BluetoothController::addConnectionCallback(ConnectionCallback cb) {
1789 return addListener(connCallbacks_, cb);
1790}
1791
1793BluetoothController::addDisconnectionCallback(DisconnectionCallback cb) {
1794 return addListener(disconnCallbacks_, cb);
1795}
1796
1797void BluetoothController::removeConnectionCallback(ListenerToken token) {
1798 removeListener(connCallbacks_, token);
1799}
1800
1801void BluetoothController::removeDisconnectionCallback(ListenerToken token) {
1802 removeListener(disconnCallbacks_, token);
1803}
1804
1808
1813void BluetoothController::setPasskeyCallback(PasskeyCallback cb) {
1814 passkeyCb_ = cb;
1815}
1816
1821void BluetoothController::setAuthCompleteCallback(AuthCompleteCallback cb) {
1822 authCompleteCb_ = cb;
1823}
1824
1826BluetoothController::addNumericComparisonCallback(NumericComparisonCallback cb) {
1827 return addListener(numCmpCallbacks_, cb);
1828}
1829
1830void BluetoothController::removeNumericComparisonCallback(ListenerToken token) {
1831 removeListener(numCmpCallbacks_, token);
1832}
1833
1834void BluetoothController::setNumericComparisonCallback(NumericComparisonCallback cb) {
1835 for (uint8_t i = 0; i < MAX_CONN_CALLBACKS; i++) numCmpCallbacks_[i].active = false;
1836 if (cb) addListener(numCmpCallbacks_, cb);
1837}
1838
1840BluetoothController::addEncryptionChangeCallback(EncChangeCallback cb) {
1841 return addListener(encChangeCallbacks_, cb);
1842}
1843
1844void BluetoothController::removeEncryptionChangeCallback(ListenerToken token) {
1845 removeListener(encChangeCallbacks_, token);
1846}
1847
1853bool BluetoothController::initiateSecurity(uint16_t connHandle) {
1854 int rc = ble_gap_security_initiate(connHandle);
1855 // BLE_HS_EALREADY: encryption already established or in progress.
1856 if (rc != 0 && rc != BLE_HS_EALREADY) {
1857 LOG_W(TAG, "ble_gap_security_initiate failed: %d", rc);
1858 return false;
1859 }
1860 return true;
1861}
1862
1870bool BluetoothController::getPeerIdAddr(uint16_t connHandle, uint8_t addr[6],
1871 uint8_t* addrType) const {
1872 if (!addr || !addrType) return false;
1873 struct ble_gap_conn_desc desc = {};
1874 if (ble_gap_conn_find(connHandle, &desc) != 0) return false;
1875 std::memcpy(addr, desc.peer_id_addr.val, 6);
1876 *addrType = desc.peer_id_addr.type;
1877 return true;
1878}
1879
1885void BluetoothController::forgetBond(const uint8_t addr[6], uint8_t addrType) {
1886 if (!addr) return;
1887 ble_addr_t peer = {};
1888 peer.type = addrType;
1889 std::memcpy(peer.val, addr, 6);
1890 // ble_gap_unpair() returns BLE_HS_EBUSY without deleting anything when the
1891 // peer distributed an IRK and advertising/discovery is active (the state in
1892 // the Bluetooth menu). Delete the bond records directly, as clearAllBonds()
1893 // does for all peers; the resolving-list entry is rebuilt empty on boot.
1894 int rc = ble_store_util_delete_peer(&peer);
1895 if (rc != 0) {
1896 LOG_W(TAG, "ble_store_util_delete_peer failed: %d", rc);
1897 } else {
1898 LOG_I(TAG, "Bond forgotten");
1899 }
1900}
1901
1908uint8_t BluetoothController::getBondedDevices(BleBondInfo* out, uint8_t maxCount) const {
1909 if (!out || maxCount == 0) return 0;
1910
1911 ble_addr_t peers[MAX_BONDS] = {};
1912 int numPeers = 0;
1913 if (ble_store_util_bonded_peers(peers, &numPeers, MAX_BONDS) != 0) {
1914 return 0;
1915 }
1916
1917 uint8_t count = 0;
1918 for (int i = 0; i < numPeers && count < maxCount; i++) {
1919 std::memcpy(out[count].addr, peers[i].val, 6);
1920 out[count].addrType = peers[i].type;
1921 out[count].connected = false;
1922 for (uint8_t c = 0; c < MAX_CONNECTIONS; c++) {
1923 if (!connections_[c].active) continue;
1924 struct ble_gap_conn_desc desc = {};
1925 if (ble_gap_conn_find(connections_[c].handle, &desc) != 0) continue;
1926 if (desc.peer_id_addr.type == peers[i].type &&
1927 std::memcmp(desc.peer_id_addr.val, peers[i].val, 6) == 0) {
1928 out[count].connected = true;
1929 break;
1930 }
1931 }
1932 count++;
1933 }
1934 return count;
1935}
1936
1942void BluetoothController::respondToNumericComparison(uint16_t connHandle, bool accept) {
1943 struct ble_sm_io pkey = {};
1944 pkey.action = BLE_SM_IOACT_NUMCMP;
1945 pkey.numcmp_accept = accept ? 1 : 0;
1946 int rc = ble_sm_inject_io(connHandle, &pkey);
1947 if (rc != 0) {
1948 LOG_E(TAG, "ble_sm_inject_io failed: %d", rc);
1949 }
1950 LOG_I(TAG, "Pairing %s", accept ? "accepted" : "rejected");
1951}
1952
1958void BluetoothController::onPasskeyAction(uint16_t connHandle,
1959 const ble_gap_passkey_params* params) {
1960 if (!params) return;
1961
1962 switch (params->action) {
1963 case BLE_SM_IOACT_NUMCMP: {
1964 LOG_I(TAG, "Numeric comparison: %06lu", (unsigned long)params->numcmp);
1965 LOG_I(TAG, "%s stack free: %lu words", pcTaskGetName(nullptr),
1966 (unsigned long)uxTaskGetStackHighWaterMark(nullptr));
1967 bool dispatched = false;
1968 for (uint8_t i = 0; i < MAX_CONN_CALLBACKS; i++) {
1969 if (numCmpCallbacks_[i].active && numCmpCallbacks_[i].callback) {
1970 numCmpCallbacks_[i].callback(connHandle, params->numcmp);
1971 dispatched = true;
1972 }
1973 }
1974 if (!dispatched) {
1975 // No callback registered: reject by default for safety.
1976 LOG_W(TAG, "No numeric comparison callback registered, rejecting");
1977 respondToNumericComparison(connHandle, false);
1978 }
1979 break;
1980 }
1981
1982 case BLE_SM_IOACT_DISP:
1983 LOG_I(TAG, "Display passkey: %06lu", (unsigned long)params->numcmp);
1984 if (passkeyCb_) {
1985 passkeyCb_(params->numcmp);
1986 }
1987 break;
1988
1989 default:
1990 LOG_W(TAG, "Unhandled passkey action: %d", params->action);
1991 break;
1992 }
1993}
1994
1998
2006bool BluetoothController::setAdvertisingManufacturerData(uint16_t companyId,
2007 const uint8_t* data, uint16_t len) {
2008 if (!data || len > sizeof(mfgData_)) return false;
2009
2010 memcpy(mfgData_, data, len);
2011 mfgDataLen_ = len;
2012 mfgCompanyId_ = companyId;
2013 mfgDataSet_ = true;
2014
2015 if (advertising_) {
2016 stopAdvertising();
2017 startAdvertising();
2018 }
2019 return true;
2020}
2021
2025void BluetoothController::clearAdvertisingManufacturerData() {
2026 mfgDataSet_ = false;
2027 mfgDataLen_ = 0;
2028
2029 if (advertising_) {
2030 stopAdvertising();
2031 startAdvertising();
2032 }
2033}
2034
2038
2047int gattcChrDiscCb(uint16_t connHandle, const struct ble_gatt_error* error,
2048 const struct ble_gatt_chr* chr, void* arg) {
2049 (void)arg;
2050 auto* ctrl = BluetoothController::instance_;
2051 if (!ctrl) return 0;
2052
2053 if (error->status == 0 && chr) {
2054 auto& svc = ctrl->discoveredSvc_;
2056 auto& dc = svc.characteristics[svc.numCharacteristics];
2057 // Use NimBLE's typed helper to safely extract 16-bit UUIDs from
2058 // either BLE_UUID_TYPE_16 or BLE_UUID_TYPE_32 variants. Anything
2059 // larger falls back to 128-bit.
2060 if (chr->uuid.u.type == BLE_UUID_TYPE_16) {
2061 dc.uuid = BleUuid::from16(ble_uuid_u16(&chr->uuid.u));
2062 } else if (chr->uuid.u.type == BLE_UUID_TYPE_32) {
2063 // No native 32-bit support in our generic UUID; convert to 128-bit per BT spec.
2064 uint8_t u128[16] = { 0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00,
2065 0x00, 0x80, 0x00, 0x10, 0x00, 0x00,
2066 0, 0, 0, 0 };
2067 uint32_t v = chr->uuid.u32.value;
2068 u128[12] = v & 0xFF;
2069 u128[13] = (v >> 8) & 0xFF;
2070 u128[14] = (v >> 16) & 0xFF;
2071 u128[15] = (v >> 24) & 0xFF;
2072 dc.uuid = BleUuid::from128(u128);
2073 } else {
2074 dc.uuid = BleUuid::from128(chr->uuid.u128.value);
2075 }
2076 dc.valueHandle = chr->val_handle;
2077 dc.properties = chr->properties;
2078 svc.numCharacteristics++;
2079 }
2080 } else if (error->status == BLE_HS_EDONE) {
2081 LOG_I(TAG, "Char discovery done (%d chars)", ctrl->discoveredSvc_.numCharacteristics);
2082 for (uint8_t i = 0; i < MAX_CONN_CALLBACKS; i++) {
2083 if (ctrl->svcDiscoveryCallbacks_[i].active &&
2084 ctrl->svcDiscoveryCallbacks_[i].callback) {
2085 ctrl->svcDiscoveryCallbacks_[i].callback(
2086 connHandle, &ctrl->discoveredSvc_, true);
2087 }
2088 }
2089 } else {
2090 LOG_E(TAG, "Char discovery error: %d", error->status);
2091 for (uint8_t i = 0; i < MAX_CONN_CALLBACKS; i++) {
2092 if (ctrl->svcDiscoveryCallbacks_[i].active &&
2093 ctrl->svcDiscoveryCallbacks_[i].callback) {
2094 ctrl->svcDiscoveryCallbacks_[i].callback(connHandle, nullptr, true);
2095 }
2096 }
2097 }
2098
2099 return 0;
2100}
2101
2110int gattcSvcDiscCb(uint16_t connHandle, const struct ble_gatt_error* error,
2111 const struct ble_gatt_svc* service, void* arg) {
2112 (void)arg;
2113 auto* ctrl = BluetoothController::instance_;
2114 if (!ctrl) return 0;
2115
2116 auto dispatchFailure = [&]() {
2117 for (uint8_t i = 0; i < MAX_CONN_CALLBACKS; i++) {
2118 if (ctrl->svcDiscoveryCallbacks_[i].active &&
2119 ctrl->svcDiscoveryCallbacks_[i].callback) {
2120 ctrl->svcDiscoveryCallbacks_[i].callback(connHandle, nullptr, true);
2121 }
2122 }
2123 };
2124
2125 if (error->status == 0 && service) {
2126 ctrl->discoverSvcStart_ = service->start_handle;
2127 ctrl->discoverSvcEnd_ = service->end_handle;
2128 LOG_I(TAG, "Service found: handles %d-%d",
2129 service->start_handle, service->end_handle);
2130 } else if (error->status == BLE_HS_EDONE) {
2131 if (ctrl->discoverSvcStart_ != 0) {
2132 int rc = ble_gattc_disc_all_chrs(connHandle,
2133 ctrl->discoverSvcStart_,
2134 ctrl->discoverSvcEnd_,
2135 gattcChrDiscCb, nullptr);
2136 if (rc != 0) {
2137 LOG_E(TAG, "ble_gattc_disc_all_chrs failed: %d", rc);
2138 dispatchFailure();
2139 }
2140 } else {
2141 LOG_W(TAG, "Service not found on remote device");
2142 dispatchFailure();
2143 }
2144 } else {
2145 LOG_E(TAG, "Service discovery error: %d", error->status);
2146 dispatchFailure();
2147 }
2148
2149 return 0;
2150}
2151
2160int gattcReadCb(uint16_t connHandle, const struct ble_gatt_error* error,
2161 struct ble_gatt_attr* attr, void* arg) {
2162 (void)arg;
2163 auto* ctrl = BluetoothController::instance_;
2164 if (!ctrl) return 0;
2165
2166 if (error->status == 0 && attr && attr->om) {
2167 uint16_t len = OS_MBUF_PKTLEN(attr->om);
2168 if (len > sizeof(s_gattAccessBuf)) len = sizeof(s_gattAccessBuf);
2169 ble_hs_mbuf_to_flat(attr->om, s_gattAccessBuf, len, nullptr);
2170 for (uint8_t i = 0; i < MAX_CONN_CALLBACKS; i++) {
2171 if (ctrl->charReadCallbacks_[i].active &&
2172 ctrl->charReadCallbacks_[i].callback) {
2173 ctrl->charReadCallbacks_[i].callback(
2174 connHandle, attr->handle, s_gattAccessBuf, len);
2175 }
2176 }
2177 } else {
2178 LOG_E(TAG, "GATT read failed: %d", error->status);
2179 for (uint8_t i = 0; i < MAX_CONN_CALLBACKS; i++) {
2180 if (ctrl->charReadCallbacks_[i].active &&
2181 ctrl->charReadCallbacks_[i].callback) {
2182 ctrl->charReadCallbacks_[i].callback(connHandle, 0, nullptr, 0);
2183 }
2184 }
2185 }
2186
2187 return 0;
2188}
2189
2198int gattcWriteCb(uint16_t connHandle, const struct ble_gatt_error* error,
2199 struct ble_gatt_attr* attr, void* arg) {
2200 (void)arg;
2201 auto* ctrl = BluetoothController::instance_;
2202 if (!ctrl) return 0;
2203
2204 uint16_t handle = attr ? attr->handle : 0;
2205 for (uint8_t i = 0; i < MAX_CONN_CALLBACKS; i++) {
2206 if (ctrl->writeCompleteCallbacks_[i].active &&
2207 ctrl->writeCompleteCallbacks_[i].callback) {
2208 ctrl->writeCompleteCallbacks_[i].callback(connHandle, handle, error->status);
2209 }
2210 }
2211
2212 return 0;
2213}
2214
2218
2225bool BluetoothController::connect(const uint8_t* addr, uint8_t addrType) {
2226 if (!enabled_ || !synced_ || !addr) return false;
2227
2228 // Must stop advertising to connect as central
2229 if (advertising_) {
2230 ble_gap_adv_stop();
2231 advertising_ = false;
2232 }
2233
2234 ble_addr_t bleAddr;
2235 bleAddr.type = addrType;
2236 memcpy(bleAddr.val, addr, 6);
2237
2238 int rc = ble_gap_connect(ownAddrType_, &bleAddr, 10000, nullptr,
2239 bleGapEventCallback, nullptr);
2240 if (rc != 0) {
2241 LOG_E(TAG, "ble_gap_connect failed: %d", rc);
2242 startAdvertising();
2243 return false;
2244 }
2245
2246 LOG_I(TAG, "Connecting to %02X:%02X:%02X:%02X:%02X:%02X",
2247 addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]);
2248 return true;
2249}
2250
2254void BluetoothController::cancelConnect() {
2255 ble_gap_conn_cancel();
2256}
2257
2264bool BluetoothController::discoverServiceByUuid(uint16_t connHandle, const BleUuid& uuid) {
2265 if (!enabled_) return false;
2266
2267 // Reset discovery state
2268 memset(&discoveredSvc_, 0, sizeof(discoveredSvc_));
2269 discoveredSvc_.uuid = uuid;
2270 discoverTargetUuid_ = uuid;
2271 discoverSvcStart_ = 0;
2272 discoverSvcEnd_ = 0;
2273
2274 ble_uuid_any_t nimbleUuid;
2275 convertUuid(uuid, nimbleUuid);
2276
2277 int rc = ble_gattc_disc_svc_by_uuid(connHandle, &nimbleUuid.u,
2278 gattcSvcDiscCb, nullptr);
2279 if (rc != 0) {
2280 LOG_E(TAG, "ble_gattc_disc_svc_by_uuid failed: %d", rc);
2281 return false;
2282 }
2283
2284 LOG_I(TAG, "Service discovery started (connHandle=%d)", connHandle);
2285 return true;
2286}
2287
2297bool BluetoothController::writeCharacteristic(uint16_t connHandle, uint16_t attrHandle,
2298 const uint8_t* data, uint16_t len,
2299 bool withResponse) {
2300 if (!enabled_ || !data || len == 0) return false;
2301
2302 int rc;
2303 if (withResponse) {
2304 rc = ble_gattc_write_flat(connHandle, attrHandle, data, len,
2305 gattcWriteCb, nullptr);
2306 } else {
2307 // Write-without-response has no confirmation event. Do not invoke
2308 // writeCompleteCallback synchronously - modules treating that as a
2309 // TX confirmation would misreport delivery.
2310 rc = ble_gattc_write_no_rsp_flat(connHandle, attrHandle, data, len);
2311 }
2312
2313 if (rc != 0) {
2314 LOG_E(TAG, "GATT write failed: %d", rc);
2315 return false;
2316 }
2317
2318 return true;
2319}
2320
2327bool BluetoothController::readCharacteristic(uint16_t connHandle, uint16_t attrHandle) {
2328 if (!enabled_) return false;
2329
2330 int rc = ble_gattc_read(connHandle, attrHandle, gattcReadCb, nullptr);
2331 if (rc != 0) {
2332 LOG_E(TAG, "ble_gattc_read failed: %d", rc);
2333 return false;
2334 }
2335
2336 return true;
2337}
2338
2345bool BluetoothController::enableNotifications(uint16_t connHandle, uint16_t cccdHandle) {
2346 if (!enabled_) return false;
2347
2348 uint8_t val[2] = { 0x01, 0x00 };
2349 int rc = ble_gattc_write_flat(connHandle, cccdHandle, val, sizeof(val),
2350 gattcWriteCb, nullptr);
2351 if (rc != 0) {
2352 LOG_E(TAG, "Enable notifications failed: %d", rc);
2353 return false;
2354 }
2355
2356 LOG_I(TAG, "Notifications enabled (cccd=%d)", cccdHandle);
2357 return true;
2358}
2359
2373int gattcDscDiscCb(uint16_t connHandle, const struct ble_gatt_error* error,
2374 uint16_t chr_val_handle, const struct ble_gatt_dsc* dsc, void* arg) {
2375 (void)arg;
2376 (void)chr_val_handle;
2377 auto* ctrl = BluetoothController::instance_;
2378 if (!ctrl) return 0;
2379
2380 if (error->status == 0 && dsc) {
2381 if (dsc->uuid.u.type == BLE_UUID_TYPE_16 && dsc->uuid.u16.value == 0x2902 &&
2382 ctrl->pendingSubCccdHandle_ == 0) {
2383 ctrl->pendingSubCccdHandle_ = dsc->handle;
2384 }
2385 return 0;
2386 }
2387
2388 // Discovery finished (BLE_HS_EDONE) or failed: write whatever we found.
2389 uint16_t cccd = ctrl->pendingSubCccdHandle_;
2390 if (cccd == 0) {
2391 cccd = static_cast<uint16_t>(ctrl->pendingSubValueHandle_ + 1);
2392 LOG_W(TAG, "No CCCD found for handle %d, falling back to %d",
2393 ctrl->pendingSubValueHandle_, cccd);
2394 }
2395 ctrl->pendingSubValueHandle_ = 0;
2396 ctrl->pendingSubCccdHandle_ = 0;
2397 ctrl->enableNotifications(connHandle, cccd);
2398 return 0;
2399}
2400
2407bool BluetoothController::subscribeToCharacteristic(uint16_t connHandle, uint16_t valueHandle) {
2408 if (!enabled_ || valueHandle == 0) return false;
2409
2410 // The descriptor range ends where the last-discovered service ends; without
2411 // a prior service discovery there is no bound, so use the +1 heuristic.
2412 if (discoverSvcEnd_ == 0 || valueHandle >= discoverSvcEnd_) {
2413 return enableNotifications(connHandle, static_cast<uint16_t>(valueHandle + 1));
2414 }
2415
2416 pendingSubValueHandle_ = valueHandle;
2417 pendingSubCccdHandle_ = 0;
2418 int rc = ble_gattc_disc_all_dscs(connHandle, valueHandle, discoverSvcEnd_,
2419 gattcDscDiscCb, nullptr);
2420 if (rc != 0) {
2421 LOG_E(TAG, "ble_gattc_disc_all_dscs failed: %d", rc);
2422 pendingSubValueHandle_ = 0;
2423 return enableNotifications(connHandle, static_cast<uint16_t>(valueHandle + 1));
2424 }
2425 return true;
2426}
2427
2432void BluetoothController::disconnectHandle(uint16_t connHandle) {
2433 ble_gap_terminate(connHandle, BLE_ERR_REM_USER_CONN_TERM);
2434}
2435
2437BluetoothController::addServiceDiscoveryCallback(ServiceDiscoveryCallback cb) {
2438 return addListener(svcDiscoveryCallbacks_, cb);
2439}
2441BluetoothController::addCharacteristicReadCallback(CharacteristicReadCallback cb) {
2442 return addListener(charReadCallbacks_, cb);
2443}
2445BluetoothController::addNotificationCallback(NotificationCallback cb) {
2446 return addListener(notifyCallbacks_, cb);
2447}
2449BluetoothController::addWriteCompleteCallback(WriteCompleteCallback cb) {
2450 return addListener(writeCompleteCallbacks_, cb);
2451}
2452
2453void BluetoothController::removeServiceDiscoveryCallback(ListenerToken t) {
2454 removeListener(svcDiscoveryCallbacks_, t);
2455}
2456void BluetoothController::removeCharacteristicReadCallback(ListenerToken t) {
2457 removeListener(charReadCallbacks_, t);
2458}
2459void BluetoothController::removeNotificationCallback(ListenerToken t) {
2460 removeListener(notifyCallbacks_, t);
2461}
2462void BluetoothController::removeWriteCompleteCallback(ListenerToken t) {
2463 removeListener(writeCompleteCallbacks_, t);
2464}
2465
2466void BluetoothController::setServiceDiscoveryCallback(ServiceDiscoveryCallback cb) {
2467 for (uint8_t i = 0; i < MAX_CONN_CALLBACKS; i++) svcDiscoveryCallbacks_[i].active = false;
2468 if (cb) addListener(svcDiscoveryCallbacks_, cb);
2469}
2470void BluetoothController::setCharacteristicReadCallback(CharacteristicReadCallback cb) {
2471 for (uint8_t i = 0; i < MAX_CONN_CALLBACKS; i++) charReadCallbacks_[i].active = false;
2472 if (cb) addListener(charReadCallbacks_, cb);
2473}
2474void BluetoothController::setNotificationCallback(NotificationCallback cb) {
2475 for (uint8_t i = 0; i < MAX_CONN_CALLBACKS; i++) notifyCallbacks_[i].active = false;
2476 if (cb) addListener(notifyCallbacks_, cb);
2477}
2478void BluetoothController::setWriteCompleteCallback(WriteCompleteCallback cb) {
2479 for (uint8_t i = 0; i < MAX_CONN_CALLBACKS; i++) writeCompleteCallbacks_[i].active = false;
2480 if (cb) addListener(writeCompleteCallbacks_, cb);
2481}
2482
2488 static BluetoothController* g_bluetoothController = new BluetoothController();
2489 return g_bluetoothController;
2490}
2491
2492} // namespace cdc::hal
2493
2494#else // NimBLE not enabled - stub implementation
2495
2496#include "esp_mac.h"
2497#include <cstring>
2498
2499namespace cdc::hal {
2500
2505public:
2510 bool init() override {
2511 LOG_W(TAG, "Bluetooth disabled (NimBLE not configured)");
2513 return true;
2514 }
2515
2519 bool start() override {
2521 return true;
2522 }
2523 void stop() override { state_ = core::ServiceState::STOPPED; }
2524 core::ServiceState getState() const override { return state_; }
2525 const char* getName() const override { return "bluetooth"; }
2526
2527 bool enable() override {
2528 LOG_W(TAG, "Cannot enable - NimBLE not configured in sdkconfig");
2529 return false;
2530 }
2531 void disable() override {}
2532 bool isEnabled() const override { return false; }
2538 bool getMacAddress(uint8_t* mac) const override {
2539 if (mac) esp_read_mac(mac, ESP_MAC_BT);
2540 return mac != nullptr;
2541 }
2542
2546 void setDeviceName(const char* name) override {
2547 if (name) {
2548 strncpy(deviceName_, name, sizeof(deviceName_) - 1);
2549 }
2550 }
2551 const char* getDeviceName() const override { return deviceName_; }
2552 bool isConnected() const override { return false; }
2553 void disconnect() override {}
2554 int8_t getRssi() const override { return 0; }
2555
2556private:
2558 char deviceName_[32] = "CDC Badge";
2559};
2560
2562
2570
2571} // namespace cdc::hal
2572
2573#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])