简介本资源是一套面向Windows平台C/C开发者的BLE低功耗蓝牙通信实践代码适用于具备基础WinRT编程经验的中高级开发者解决在桌面端调用系统API完成BLE设备扫描、连接、GATT服务发现与数据读写等核心操作的技术落地问题。压缩包共11个文件含4个头文件.h定义接口与回调结构4个源文件.cpp实现设备枚举、广告监听、特征值读写等关键逻辑另含VCXPROJ工程配置、过滤器及用户设置文件整体仅10KB轻量易集成。已有5965人学习下载代码结构清晰模块职责分明BleHandle.h/cpp封装BLE会话管理BLEComm.h/cpp提供高层通信接口pch.h/cpp支持预编译加速便于快速理解WinRT蓝牙API调用链路与错误处理范式。1. C/C 在 Windows 上直接操作 BLE 设备不是调用 WinRT 就能连上得绕过 UWP 沙箱、处理 GATT 句柄生命周期、手动管理 ATT 协议层状态你手头有一块 Nordic nRF52840 开发板跑着标准的 BLE Peripheral 固件广播名是SensorNode-01提供一个0x2A19Battery Level特征值和一个自定义0xABC1通知通道。你想用本地 C 程序在 Windows 10/11 上稳定读取电量、持续接收传感器数据——不是写个 UWP 应用不是靠 Python 的 bleak 包做胶水层而是用原生 Win32 API Bluetooth LE 原始接口从BluetoothGATT系列函数开始逐字节控制连接、发现、读写、订阅全过程。这份资源就是一套可编译、可调试、可嵌入工业采集模块的 C BLE 控制模板它不依赖 .NET、不封装成 COM 组件、不走 Windows Runtime 抽象层而是直连BluetoothAPIs.h和Bluetoothleapis.h把BluetoothGATTGetServices返回的BTH_LE_GATT_SERVICE结构体、BluetoothGATTGetCharacteristics拿到的BTH_LE_GATT_CHARACTERISTIC数组、以及每个 characteristic 对应的BluetoothGATTGetDescriptors描述符列表全部暴露为可操作对象。适合某高校嵌入式实验室做 Windows 侧 BLE 数据网关开发也适合某公司工控设备配套的本地配置工具链集成。如果你正被 WinRT 的线程模型卡住、被IBluetoothLEDevice初始化失败报错折磨、或发现BluetoothGATTWriteValueAsync总是返回E_PENDING却无从排查——这份资源就是你该停下来的实操锚点。2. 为什么必须绕开 WinRTWindows BLE 原生 API 分层与BluetoothGATT函数族选型依据2.1 Windows BLE 通信栈的真实分层从 L2CAP 到 GATT 的四层映射关系Windows 对 BLE 的支持并非单一层抽象。底层是蓝牙协议栈驱动bthport.sysbthmini.sys向上暴露两类编程接口一是面向传统 BR/EDR 的旧式BluetoothAPIs.h含BluetoothFindFirstDevice等二是专为 BLE 设计的Bluetoothleapis.h含BluetoothGATT*系列。关键在于WinRT 的Windows.Devices.Bluetooth.Advertisement和Windows.Devices.Bluetooth.GenericAttributeProfile实际是第三层封装——它把BluetoothGATTOpenDevice打开的句柄、BluetoothGATTGetServices发起的 SDP 查询、甚至BluetoothGATTRegisterEvent的回调注册全部包裹进异步任务链和 COM 对象生命周期中。而原生 C/C 项目若强制使用 WinRT会立刻触发两个硬约束第一必须链接windowsapp.lib并启用/ZW编译开关这直接禁用多数工业场景依赖的/MT静态 CRT第二所有 BLE 对象如GattCharacteristic必须在 ASTAApplication Single-Threaded Apartment线程中创建而传统 Win32 GUI 程序主线程默认是 STA控制台程序更是 MTA强行切换线程模型极易引发RPC_E_WRONG_THREAD。因此本资源彻底放弃 WinRT选择Bluetoothleapis.h中的纯 C 函数族——它们本质是内核驱动暴露的用户态 IOCTL 封装调用时无需 COM 初始化、不依赖 Windows Runtime 组件、可自由在任意线程调用且返回的HANDLE可直接用CloseHandle释放内存模型完全可控。2.2BluetoothGATTOpenDevice的设备路径构造如何从蓝牙地址生成合法LPCWSTRBluetoothGATTOpenDevice不接受 MAC 地址字符串如AA:BB:CC:DD:EE:FF也不接受 WinRT 风格的BluetoothAddress结构体它要求一个形如L\\\\?\\BLUETOOTH#v1.0#AAAABBBBCCCC#{bb7bb41e-6a5d-4e0c-b70a-1f327492872a}的设备路径。这个路径由三部分拼接而成前缀L\\\\?\\BLUETOOTH#v1.0# 蓝牙地址十六进制大写无分隔符字符串如AA BB CC DD EE FF→AABBCCDDEEFF 后缀L#{bb7bb41e-6a5d-4e0c-b70a-1f327492872a}。后缀是固定 GUID代表 BLE 设备类不可省略或替换。实际代码中需先将字符串地址转为ULONGLONG再格式化为 12 位十六进制#include windows.h #include bluetoothleapis.h #include stdio.h // 将 AA:BB:CC:DD:EE:FF 转为 AABBCCDDEEFF bool ParseBtAddress(const wchar_t* addrStr, ULONGLONG* outAddr) { WCHAR temp[13] {0}; int idx 0; for (int i 0; addrStr[i] idx 12; i) { if (addrStr[i] L0 addrStr[i] L9) { temp[idx] addrStr[i]; } else if (addrStr[i] LA addrStr[i] LF) { temp[idx] addrStr[i]; } else if (addrStr[i] La addrStr[i] Lf) { temp[idx] addrStr[i] - La LA; } } if (idx ! 12) return false; *outAddr _wtoi64(temp); return true; } // 构造完整设备路径 bool BuildDevicePath(const wchar_t* addrStr, WCHAR* outPath, size_t pathLen) { ULONGLONG btAddr; if (!ParseBtAddress(addrStr, btAddr)) return false; // 格式化为 12 位大写十六进制 WCHAR hexAddr[13] {0}; swprintf_s(hexAddr, L%012I64X, btAddr); // 拼接完整路径 const WCHAR* prefix L\\\\?\\BLUETOOTH#v1.0#; const WCHAR* suffix L#{bb7bb41e-6a5d-4e0c-b70a-1f327492872a}; if (swprintf_s(outPath, pathLen, L%s%s%s, prefix, hexAddr, suffix) 0) { return false; } return true; }提示swprintf_s是安全版本避免缓冲区溢出%012I64X确保输出 12 位大写十六进制不足位补零。若传入地址为00:11:22:33:44:55最终路径为L\\\\?\\BLUETOOTH#v1.0#001122334455#{bb7bb41e-6a5d-4e0c-b70a-1f327492872a}。此路径是BluetoothGATTOpenDevice唯一认可的输入错一位字符如小写a或多一个-都会返回INVALID_HANDLE_VALUE。2.3BluetoothGATTGetServices的服务发现机制为何必须显式调用且不能跳过BLE 设备的服务Service列表并非连接后自动缓存BluetoothGATTOpenDevice仅建立逻辑连接并返回句柄真正的服务发现需主动调用BluetoothGATTGetServices。该函数内部会向设备发送 ATT Read By Group Type RequestOpCode0x10扫描0x0001到0xFFFF句柄范围查找0x2800Primary Service和0x2801Secondary Service声明。返回的PBTH_LE_GATT_SERVICE数组中每个元素包含AttributeHandleRange起止句柄、Uuid16 位或 128 位 UUID、IsPrimary标志。注意BluetoothGATTGetServices是阻塞调用超时由系统决定通常 3~5 秒若设备未响应函数返回ERROR_TIMEOUT若服务数超过缓冲区需先调用一次获取所需大小再分配足够内存重试。本资源模板中我们封装为两阶段调用// 第一阶段获取所需缓冲区大小 DWORD servicesSize 0; DWORD result BluetoothGATTGetServices( hDevice, // 由 BluetoothGATTOpenDevice 返回 0, // 无特殊标志 servicesSize, // 输出所需字节数 nullptr, // 缓冲区为空仅查询大小 0 // 超时毫秒0 表示默认 ); if (result ! ERROR_INSUFFICIENT_BUFFER) { // 处理其他错误如 ERROR_DEVICE_NOT_CONNECTED return false; } // 第二阶段分配内存并获取服务列表 std::vectorBYTE servicesBuffer(servicesSize); PBTH_LE_GATT_SERVICE pServices reinterpret_castPBTH_LE_GATT_SERVICE(servicesBuffer.data()); result BluetoothGATTGetServices( hDevice, 0, servicesSize, pServices, 0 ); if (result ! ERROR_SUCCESS) { // 记录错误码常见为 ERROR_TIMEOUT 或 ERROR_INVALID_HANDLE return false; }参数说明hDevice是有效设备句柄第二个参数为标志位目前保留为 0servicesSize是输入输出参数首次调用时传入 0函数返回所需字节数pServices是指向缓冲区的指针最后一个 0 表示使用系统默认超时。此两阶段模式是 Windows BLE API 的通用范式同样适用于BluetoothGATTGetCharacteristics和BluetoothGATTGetDescriptors。3. 特征值读写与通知订阅BluetoothGATTReadValue、BluetoothGATTWriteValue与BluetoothGATTRegisterEvent的协同流程3.1BluetoothGATTReadValue的同步读取如何解析BTH_LE_GATT_VALUE结构体中的原始字节BluetoothGATTReadValue用于同步读取指定 characteristic 的当前值。它不返回字符串或整数而是填充一个BTH_LE_GATT_VALUE结构体其中Data字段是PUCHAR类型的原始字节指针DataSize是字节数。例如读取0x2A19Battery Level时Data指向一个BYTE值为0x42即 66%而非66字符串。关键点在于Data内存由系统分配调用者必须在使用后立即调用BluetoothGATTFreeAttributeValue释放否则内存泄漏。以下为完整读取流程// 假设已通过 BluetoothGATTGetCharacteristics 获取到目标 characteristic // pChar 为 PBTH_LE_GATT_CHARACTERISTIC 指针 BTH_LE_GATT_VALUE value {0}; DWORD result BluetoothGATTReadValue( hDevice, pChar-AttributeHandleRange.Begin, // 读取起始句柄通常是 characteristic 值句柄 0, // 标志位0 表示普通读取 value, // 输出结构体 0 // 超时0 表示默认 ); if (result ERROR_SUCCESS value.Data ! nullptr value.DataSize 0) { // 解析电池电量按 BLE 规范0x2A19 是 UINT8直接取第一个字节 BYTE batteryLevel *value.Data; printf(Battery Level: %u%%\n, batteryLevel); // 必须释放 Data 内存 BluetoothGATTFreeAttributeValue(value); } else { // 处理错误如 ERROR_ACCESS_DENIED未订阅 CCCD、ERROR_TIMEOUT }注意pChar-AttributeHandleRange.Begin是 characteristic 值的句柄不是 declaration 句柄declaration 句柄通常比 value 小 1。若不确定可用BluetoothGATTGetCharacteristics返回的pChar-AttributeHandleRange.Begin直接作为读取句柄——这是最稳妥做法。BluetoothGATTFreeAttributeValue是强制调用步骤遗漏会导致每读一次泄漏DataSize字节。3.2BluetoothGATTWriteValue的写入类型区分WRITE_WITHOUT_RESPONSE与SIGNED_WRITE的适用场景BluetoothGATTWriteValue支持三种写入模式由第二个参数dwFlags控制0默认Write Request带响应设备必须返回 Write ResponseOpCode0x13否则函数阻塞超时BLUETOOTH_GATT_WRITE_WITHOUT_RESPONSEWrite Command无响应设备不回包吞吐量高但无法确认写入成功BLUETOOTH_GATT_SIGNED_WRITE签名写入需设备支持签名认证极少用于普通传感器。工业场景中向配置 characteristic如采样周期设置写入必须用默认模式确保设备已接收向 LED 控制等实时性要求高的 characteristic 写入可用BLUETOOTH_GATT_WRITE_WITHOUT_RESPONSE避免延迟。写入数据需构造BTH_LE_GATT_VALUE结构体// 向 characteristic 写入 2 字节0x0001表示开启 BYTE writeData[2] {0x01, 0x00}; // 小端序 BTH_LE_GATT_VALUE writeValue {0}; writeValue.Data writeData; writeValue.DataSize sizeof(writeData); DWORD result BluetoothGATTWriteValue( hDevice, pChar-AttributeHandleRange.Begin, // 目标 characteristic 值句柄 0, // 默认带响应写入 writeValue, 0 ); if (result ! ERROR_SUCCESS) { // 错误码可能为 ERROR_ACCESS_DENIED未配对、ERROR_INVALID_PARAMETER句柄错 }关键细节writeValue.Data必须指向有效内存DataSize必须准确pChar-AttributeHandleRange.Begin是值句柄非 descriptor 句柄写入失败常见原因为 characteristic 不支持写Properties字段无BLUETOOTH_GATT_CHARACTERISTIC_PROPERTY_WRITE标志需提前检查。3.3BluetoothGATTRegisterEvent实现通知接收事件回调函数的线程安全与句柄管理要接收 characteristic 的通知Notification或指示Indication不能轮询必须注册事件。BluetoothGATTRegisterEvent接收一个BTH_LE_GATT_EVENT_TYPE类型如BTH_LE_GATT_EVENT_VALUE_CHANGED和一个回调函数指针。回调函数运行在系统线程池中非调用线程因此所有共享数据如接收缓冲区、状态标志必须加锁或使用原子操作。本资源采用CRITICAL_SECTION保护接收队列CRITICAL_SECTION g_csRecv; std::queuestd::vectorBYTE g_notificationQueue; VOID CALLBACK NotificationCallback( HANDLE hDevice, PVOID pContext, BTH_LE_GATT_EVENT_TYPE EventType, PVOID pEventData, DWORD EventDataSize ) { if (EventType BTH_LE_GATT_EVENT_VALUE_CHANGED) { PBTH_LE_GATT_EVENT_VALUE_CHANGED pEvent static_castPBTH_LE_GATT_EVENT_VALUE_CHANGED(pEventData); EnterCriticalSection(g_csRecv); std::vectorBYTE data(pEvent-Data, pEvent-Data pEvent-DataSize); g_notificationQueue.push(data); LeaveCriticalSection(g_csRecv); } } // 注册通知事件 HANDLE hNotifyEvent nullptr; DWORD result BluetoothGATTRegisterEvent( hDevice, BTH_LE_GATT_EVENT_VALUE_CHANGED, nullptr, // pEventContext此处为 null NotificationCallback, hNotifyEvent // 输出事件句柄用于后续取消 ); if (result ! ERROR_SUCCESS) { // 处理注册失败 }重要hNotifyEvent是事件注册句柄必须保存并在断开连接时调用BluetoothGATTUnregisterEvent(hNotifyEvent)否则系统资源泄漏。回调中pEvent-Data指向的数据是临时内存必须立即拷贝如std::vectorBYTE不可保存指针。EnterCriticalSection是线程安全底线漏掉会导致g_notificationQueue崩溃。4. 配置描述符CCCD写入BluetoothGATTWriteValue操作0x2902descriptor 的完整流程与权限校验4.1 CCCDClient Characteristic Configuration Descriptor的作用与句柄定位BLE 规范中若 characteristic 声明支持通知Properties含BLUETOOTH_GATT_CHARACTERISTIC_PROPERTY_NOTIFY或指示INDICATE则其后必有一个0x2902Client Characteristic Configuration Descriptordescriptor。该 descriptor 的值决定客户端是否启用通知0x0000禁用、0x0001启用通知、0x0002启用指示。不写入 CCCD设备绝不会发送通知包这是初学者最高频的“连上了却收不到数据”原因。定位 CCCD 句柄需两步先用BluetoothGATTGetDescriptors获取 characteristic 的所有 descriptors再遍历匹配Uuid.Value.ShortUuid 0x2902// 获取 characteristic 的 descriptors DWORD descSize 0; BluetoothGATTGetDescriptors( hDevice, pChar-AttributeHandleRange.Begin, // 从 characteristic 值句柄开始查 0, descSize, nullptr, 0 ); if (descSize 0) { std::vectorBYTE descBuffer(descSize); PBTH_LE_GATT_DESCRIPTOR pDescs reinterpret_castPBTH_LE_GATT_DESCRIPTOR(descBuffer.data()); BluetoothGATTGetDescriptors( hDevice, pChar-AttributeHandleRange.Begin, 0, descSize, pDescs, 0 ); // 遍历查找 CCCD for (DWORD i 0; i descSize / sizeof(BTH_LE_GATT_DESCRIPTOR); i) { if (pDescs[i].Uuid.Value.ShortUuid 0x2902) { // 找到 CCCDpDescs[i].AttributeHandleRange.Begin 即为其句柄 cccdHandle pDescs[i].AttributeHandleRange.Begin; break; } } }注意BluetoothGATTGetDescriptors的第一个参数是characteristic 的值句柄pChar-AttributeHandleRange.Begin不是 declaration 句柄pDescs[i].AttributeHandleRange.Begin是 descriptor 自身的句柄即写入目标。4.2 向 CCCD 写入0x0001启用通知的完整代码与权限检查写入 CCCD 与写入普通 characteristic 值语法一致但需确保 characteristic 支持 notify 属性// 检查 characteristic 是否支持通知 if (!(pChar-Properties BLUETOOTH_GATT_CHARACTERISTIC_PROPERTY_NOTIFY)) { printf(Characteristic does not support NOTIFY\n); return false; } // 构造 CCCD 写入数据0x0001小端序 BYTE cccdData[2] {0x01, 0x00}; BTH_LE_GATT_VALUE cccdValue {0}; cccdValue.Data cccdData; cccdValue.DataSize sizeof(cccdData); DWORD result BluetoothGATTWriteValue( hDevice, cccdHandle, // CCCD descriptor 句柄 0, // 带响应写入确保设备接收 cccdValue, 0 ); if (result ! ERROR_SUCCESS) { // 常见错误ERROR_ACCESS_DENIED —— 设备未配对或 CCCD 不可写 // ERROR_INVALID_PARAMETER —— cccdHandle 无效 }权限校验BluetoothGATTWriteValue对 CCCD 的写入受设备端 ACL 控制。若设备固件未将 CCCD descriptor 的Properties设为WRITE则写入必然失败。此时需检查设备端 GATT DB 定义确保0x2902descriptor 的属性包含BLE_GATT_CHAR_PROP_WRITE。4.3 避坑CCCD 写入失败的三大高频原因与验证方法现象 1BluetoothGATTWriteValue返回ERROR_ACCESS_DENIED但设备已配对原因Windows 蓝牙栈要求对 CCCD 的写入必须在设备配对后进行且配对需为“加密配对”MITM Protection而非简单 Just Works。若设备端只支持 Just WorksWindows 可能拒绝写入 CCCD。解决在 Windows 设置 → 蓝牙 → 设备列表中右键目标设备 → “移除设备”然后重新配对配对过程中若弹出 PIN 码输入框输入0000或1234强制触发加密流程若无 PIN 窗口则设备不支持加密配对需修改设备固件增加 MITM 支持。现象 2写入成功返回ERROR_SUCCESS但NotificationCallback从未触发原因BluetoothGATTRegisterEvent注册的是BTH_LE_GATT_EVENT_VALUE_CHANGED事件但设备实际发送的是BTH_LE_GATT_EVENT_INDICATE指示因 characteristic 属性为INDICATE而非NOTIFY。解决检查pChar-Properties若含BLUETOOTH_GATT_CHARACTERISTIC_PROPERTY_INDICATE则需注册BTH_LE_GATT_EVENT_INDICATE事件并在回调中处理BTH_LE_GATT_EVENT_INDICATE类型同时 CCCD 应写入0x0002而非0x0001。现象 3BluetoothGATTGetDescriptors返回 0 个 descriptor无法定位 CCCD原因BluetoothGATTGetDescriptors的起始句柄传错了。常见错误是传入pChar-AttributeHandleRange.Begin - 1declaration 句柄或pChar-AttributeHandleRange.End超出范围。解决严格使用pChar-AttributeHandleRange.Begin值句柄作为起始句柄若仍无结果用 nRF Connect 等工具连接同一设备查看其 descriptor 列表确认设备端确实存在0x2902descriptor若不存在则设备固件未正确声明 CCCD属设备端缺陷。5. 连接稳定性与资源清理BluetoothGATTCloseDevice的调用时机与句柄泄漏防护5.1BluetoothGATTCloseDevice的唯一正确调用点必须在所有 GATT 操作之后、事件注销之前BluetoothGATTCloseDevice是关闭设备连接的终极函数但它不是简单的“断开连接”。其行为包括终止所有未完成的 GATT 请求、清空内部缓存、释放关联的内核对象。若在BluetoothGATTRegisterEvent返回的hNotifyEvent未注销前调用BluetoothGATTCloseDevice系统可能崩溃或回调函数被重复调用若在BluetoothGATTGetServices等函数返回的内存如pServices未释放前调用会导致内存访问违规。正确顺序为调用BluetoothGATTUnregisterEvent(hNotifyEvent)注销所有事件调用BluetoothGATTFreeAttributeValue释放所有BTH_LE_GATT_VALUE的Datafree()或delete[]所有BluetoothGATTGet*函数分配的缓冲区内存最后调用BluetoothGATTCloseDevice(hDevice)。// 清理函数示例 void CleanupBLEConnection(HANDLE hDevice, HANDLE hNotifyEvent) { // 1. 注销事件 if (hNotifyEvent ! nullptr) { BluetoothGATTUnregisterEvent(hNotifyEvent); hNotifyEvent nullptr; } // 2. 释放所有 BTH_LE_GATT_VALUE.Data // 此处需维护一个 value 指针列表在读写后加入清理时遍历释放 // 3. 释放 services/characteristics/descriptors 缓冲区 // 同上需维护分配的内存块列表 // 4. 关闭设备 if (hDevice ! INVALID_HANDLE_VALUE) { BluetoothGATTCloseDevice(hDevice); hDevice INVALID_HANDLE_VALUE; } // 5. 清理临界区 DeleteCriticalSection(g_csRecv); }关键hNotifyEvent和hDevice必须分别管理BluetoothGATTUnregisterEvent不会自动关闭hDeviceBluetoothGATTCloseDevice也不会自动注销事件。二者是独立资源必须显式释放。5.2 句柄泄漏的静默危害如何用 Windows Performance Analyzer 定位未关闭的 GATT 句柄Windows 蓝牙栈的 GATT 句柄是内核对象泄漏后不会立即报错但累积到一定数量约 1000 个会导致新连接失败错误码为ERROR_TOO_MANY_OPEN_FILES实际是句柄耗尽。常规任务管理器无法查看蓝牙句柄需用 Windows Performance AnalyzerWPA抓取内核事件下载 Windows SDK安装 WPA 工具以管理员身份运行 WPA新建录制会话勾选Kernel Logger和Bluetoothprovider运行你的 BLE 程序执行多次连接-断开循环停止录制加载.etl文件在Profiles→Analysis→Handles中筛选Type为BluetoothGATTDevice的对象查看Count列若数值持续增长且不归零即存在句柄泄漏。本资源模板中所有HANDLE变量均初始化为INVALID_HANDLE_VALUE所有BluetoothGATT*调用后立即检查返回值失败时强制进入CleanupBLEConnection从源头堵死泄漏路径。5.3 连接中断的自动重连策略基于BluetoothGATTGetDeviceInformation的状态轮询Windows 不提供连接状态变更事件BluetoothGATTOpenDevice成功后设备可能随时断开如电量耗尽、超出范围但hDevice句柄仍为有效值后续BluetoothGATTReadValue会返回ERROR_DEVICE_NOT_CONNECTED。因此必须设计心跳检测。本资源采用轻量级方案每 5 秒调用一次BluetoothGATTGetDeviceInformation它仅读取设备基础信息如蓝牙地址、名称开销极小且断开时立即返回ERROR_DEVICE_NOT_CONNECTEDbool IsDeviceConnected(HANDLE hDevice) { BTH_LE_DEVICE_INFORMATION info {0}; DWORD result BluetoothGATTGetDeviceInformation( hDevice, info, 0 ); return (result ERROR_SUCCESS); } // 主循环中 while (running) { if (!IsDeviceConnected(hDevice)) { printf(Device disconnected, attempting reconnect...\n); // 执行重连逻辑BluetoothGATTOpenDevice → 服务发现 → CCCD 写入 ReconnectToDevice(); } Sleep(5000); // 5秒心跳 }优势BluetoothGATTGetDeviceInformation是唯一能快速探测连接状态的原生 API比尝试读取任意 characteristic 更可靠某些 characteristic 可能缓存旧值5 秒间隔平衡了实时性与系统负载。6. 实战技巧用BluetoothGATTEnumerateHandles解析未知 GATT 结构与逆向工程设备协议6.1BluetoothGATTEnumerateHandles的作用当BluetoothGATTGetServices返回空时的最后手段BluetoothGATTGetServices依赖设备端正确实现 GATT Service Discovery 流程。但某些低成本 BLE 模块尤其国产替代芯片固件存在缺陷Read By Group Type RequestOpCode0x10无响应导致BluetoothGATTGetServices返回ERROR_TIMEOUT但设备实际广播正常、可被发现。此时BluetoothGATTEnumerateHandles是救命稻草——它不依赖服务发现而是向设备发送 ATT Exchange MTU RequestOpCode0x02和 Find Information RequestOpCode0x04暴力扫描0x0001到0xFFFF句柄范围返回所有存在的句柄及其属性UUID、权限。虽然效率低约 10~20 秒但能获取完整 GATT 表快照。// 扫描全部句柄0x0001 ~ 0xFFFF DWORD handleCount 0; DWORD result BluetoothGATTEnumerateHandles( hDevice, 0x0001, // StartHandle 0xFFFF, // EndHandle handleCount,// 输出句柄数量 nullptr, // 缓冲区为空仅查询大小 0 ); if (result ! ERROR_INSUFFICIENT_BUFFER) { return false; } // 分配缓冲区每个句柄占 8 字节2字节句柄 2字节属性 4字节UUID std::vectorBYTE handlesBuffer(handleCount * 8); PUSHORT pHandles reinterpret_castPUSHORT(handlesBuffer.data()); PUSHORT pPermissions pHandles handleCount; PUUID pUuids reinterpret_castPUUID(pPermissions handleCount); result BluetoothGATTEnumerateHandles( hDevice, 0x0001, 0xFFFF, handleCount, pHandles, 0 );返回的pHandles[i]是句柄值pPermissions[i]是属性位如0x02表示可读pUuids[i]是 128 位 UUID。若设备使用 16 位 UUID如0x2A19其pUuids[i].Data4[0]和pUuids[i].Data4[1]存储短 UUID。6.2 从句柄列表逆向构建 GATT 结构识别 service、characteristic、descriptor 的三步法BluetoothGATTEnumerateHandles返回的是扁平句柄列表需人工识别层级。BLE GATT 结构遵循固定模式Service Declaration句柄值为NpPermissions[N] 0x01可读pUuids[N] 0x2800Primary Service或0x2801Secondary ServiceCharacteristic Declaration句柄值为N1pUuids[N1] 0x2803其后紧跟N2为 characteristic 值句柄Descriptor位于 characteristic 值句柄之后常见0x2902CCCD、0x2901User Description。以下为简化识别逻辑for (DWORD i 0; i handleCount; i) { USHORT handle pHandles[i]; USHORT perm pPermissions[i]; // 检查是否为 Primary Service if ((perm 0x01) IsUuid16(pUuids[i], 0x2800)) { printf(Service at handle 0x%04X\n, handle); // 向后扫描找下一个 0x2800 或结束 for (DWORD j i1; j handleCount; j) { if (IsUuid16(pUuids[j], 0x2800) || IsUuid16(pUuids[j], 0x2801)) { printf( Service range: 0x%04X - 0x%04X\n, handle, pHandles[j-1]); break; } } } // 检查是否为 Characteristic Declaration if ((perm 0x01) IsUuid16(pUuids[i], 0x2803)) { printf( Char decl at 0x%04X, value at 0x%04X\n, handle, handle1); // 检查 handle1 是否为 descriptor如 0x2902 for (DWORD j i2; j handleCount pHandles[j] handle2; j) { if (IsUuid16(pUuids[j], 0x2902)) { printf( CCCD at 0x%04X\n, pHandles[j]); } } } }IsUuid16函数需提取pUuids[i]的Data4[0]和 Data4[本文还有配套的精品资源点击获取