简介本资源是一套面向Windows平台C/C开发者的BLE低功耗蓝牙通信实践代码适用于具备基础WinRT编程经验的中高级开发者解决在桌面端调用系统API完成蓝牙设备扫描、连接、GATT服务发现与数据读写等核心操作的技术难点。压缩包共11个文件含4个头文件.h定义接口与回调结构4个源文件.cpp实现设备枚举、广告监听、特征值读写等关键逻辑另含VCXPROJ工程配置、过滤器及用户设置文件整体仅10KB轻量易集成。已有5965人学习下载代码结构清晰模块职责分明BleHandle.h/cpp封装底层交互BLEComm.h/cpp提供高层调用入口pch.h/cpp支持预编译加速便于快速理解WinRT蓝牙API调用链路与错误处理范式。1. 一个 Windows 桌面程序不装任何第三方 SDK 就能枚举 BLE 设备、读写特征值、监听通知——这不是 WinRT 的玩具 Demo而是用原生 Windows Bluetooth LE APIBluetoothGATT APIs写的生产级 C 封装专为工业传感器网关、医疗设备调试工具、产线蓝牙烧录器这类需要稳定、低延迟、无运行时依赖的场景而生你可能试过用 UWP 写 BLE 扫描结果发现它卡在“后台任务限制”里动弹不得也可能导入了某开源 C 库编译时疯狂报bluetoothapis.h缺失或BthLEEnum.dll找不到更常见的是用 Python bleak 跑得好好的脚本一打包成 exe 就在客户现场蓝屏重启——这些不是玄学是没摸清 Windows 原生 BLE 栈的真实边界。这个项目就是从某高校实验室真实产线调试工具中剥离出来的核心模块纯 C17零第三方依赖只链BluetoothAPIs.lib和Bthprops.lib所有 GATT 交互走 Windows 10 1809 原生 Bluetooth LE GATT Client APIBluetoothGATT*系列函数支持同步/异步双模式、自动重连、特征值变更通知回调、MTU 动态协商。它不提供 GUI但给你干净的BLEDevice、BLEService、BLECharacteristic三层对象模型每个方法都附带HRESULT错误码和详细日志钩子。如果你正要写一个 Windows 上长期驻留、需直连几十台 BLE 温湿度/电流/开关状态设备的本地服务或者需要把 BLE 通信嵌进已有 MFC/Qt 工程里当底层驱动那这份代码不是参考是能直接#include进去就跑通的后悔药。2. 架构与目录结构三层抽象 事件驱动模型所有 Windows BLE 原语都被封装进BLEAdapter和BLEDevice两个核心类这个项目不是一堆.cpp文件的堆砌而是一个有明确职责边界的 C 模块。它不追求跨平台明确只支持 Windows 10 1809 及以上因此所有实现都扎根于 Windows 原生 API避免抽象层带来的不可控延迟和内存泄漏风险。整个结构围绕“适配器 → 设备 → 服务 → 特征值”四级实体展开但代码只暴露前三级——服务与特征值作为设备的成员存在由设备对象统一管理生命周期避免裸指针悬挂。2.1 目录结构与文件职责共 12 个源文件不含测试/src ├── ble_adapter.h/cpp # BLEAdapter管理本地蓝牙适配器开启扫描、停止扫描、设置扫描参数 ├── ble_device.h/cpp # BLEDevice代表一个已连接的远程 BLE 设备连接/断开、发现服务、读写特征值 ├── ble_gatt_client.h/cpp # BLEGattClient封装 BluetoothGATT* 系列 API 调用是 BLEDevice 的底层驱动 ├── ble_utils.h/cpp # 工具函数UUID 字符串 ↔ GUID 转换、错误码转字符串、字节序处理 ├── ble_event.h # 事件基类与回调定义如 OnDeviceFound, OnCharacteristicValueChanged ├── ble_callback.h/cpp # 回调分发器将 Windows 异步完成例程APC转为 C 成员函数回调 ├── ble_log.h # 日志宏支持等级控制INFO/WARN/ERROR和自定义输出句柄 └── /winrt # 可选WinRT 兼容桥接头文件仅用于需要与 UWP 组件交互的场景 /test ├── test_scanner.cpp # 命令行扫描器启动扫描、打印设备名/地址/RSSI ├── test_gatt_rw.cpp # GATT 读写测试连接指定设备读取电池电量写入 LED 控制指令 └── test_notify.cpp # 通知监听测试订阅心率测量特征值实时打印 BPM提示ble_gatt_client.h是整个项目的黑匣子——它不暴露任何 Windows HANDLE 或HBLUETOOTH_DEVICE_INFO所有 GATT 操作BluetoothGATTGetServices,BluetoothGATTGetCharacteristics,BluetoothGATTReadValue,BluetoothGATTWriteValue,BluetoothGATTRegisterValueNotification都在这里被统一封装为同步阻塞或异步回调两种接口。你不需要知道BluetoothGATTRegisterValueNotification要求传入LPOVERLAPPED也不用手动管理OVERLAPPED结构体的内存这些都在BLEGattClient::ReadValueAsync()内部完成。2.2 核心类关系图文字描述BLEAdapter是单例线程安全持有HANDLE hRadio本地适配器句柄和std::vectorstd::unique_ptrBLEDevice m_devices。BLEDevice持有BLUETOOTH_LE_DEVICE_ADDRESS addr、HANDLE hDevice设备句柄、std::vectorstd::unique_ptrBLEService m_services以及一个BLEGattClient实例。BLEService仅包含GUID serviceUuid、USHORT attrHandle起始句柄、std::vectorstd::unique_ptrBLECharacteristic m_chars。BLECharacteristic包含GUID charUuid、USHORT valueHandle、USHORT cccdHandleClient Characteristic Configuration Descriptor 句柄用于开启通知、BLECharacteristic::PropertyFlagsREAD/WRITE/NOTIFY 等位掩码。所有BLE*对象均通过std::unique_ptr管理BLEAdapter::StopScan()会自动清理未连接的BLEDeviceBLEDevice::Disconnect()会释放其持有的hDevice并清空m_services。2.3 初始化流程从打开适配器到准备扫描5 行关键代码Windows BLE 的第一步不是扫描而是获取本地适配器句柄并验证其支持 LE。很多项目在这里翻车因为直接调BluetoothFindFirstRadio后没检查RADIO_INFO中的fRadioIsDiscoverable和fRadioIsConnectable导致后续所有操作返回E_FAIL。// src/ble_adapter.cpp HRESULT BLEAdapter::Initialize() { // 1. 查找第一个可用蓝牙无线电适配器 HBLUETOOTH_RADIO_FIND hFind BluetoothFindFirstRadio(radioInfo, hRadio); if (!hFind || !hRadio) { return HRESULT_FROM_WIN32(GetLastError()); } BluetoothFindRadioClose(hFind); // 2. 必须检查是否支持 LE —— 很多老蓝牙 4.0 dongle 声称支持实则固件阉割 DWORD leSupport 0; DWORD size sizeof(leSupport); if (BluetoothGetRadioValue(hRadio, BTH_REG_VALUE_LE_SUPPORTED, leSupport, size) ! ERROR_SUCCESS) { return E_FAIL; // 不支持 LE直接退出 } if (!leSupport) { return HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED); } // 3. 获取适配器名称用于日志 WCHAR nameBuf[256] {0}; size sizeof(nameBuf); BluetoothGetRadioValue(hRadio, BTH_REG_VALUE_NAME, nameBuf, size); m_adapterName std::wstring(nameBuf); // 4. 设置扫描参数主动扫描Active Scan窗口/间隔均为 100ms平衡功耗与发现速度 BLUETOOTH_LE_SCAN_PARAMS scanParams {}; scanParams.ScanType BtLeScanType_Active; scanParams.ScanInterval 100; // 单位0.625ms → 62.5ms scanParams.ScanWindow 100; // 同上 scanParams.FilterDuplicates TRUE; // 5. 启用扫描注意此调用不阻塞但必须在 Initialize 后调用 return BluetoothLESetScanFilter(hRadio, scanParams, nullptr); }这段代码的关键在于第 2 步BTH_REG_VALUE_LE_SUPPORTED是 Windows 内部注册表键只有真正支持 LE 的硬件且驱动正确安装才会返回1。跳过这一步后面BluetoothLEFindFirstDevice可能永远返回空设备列表而错误码还是S_OK让你怀疑人生。3. 核心 BLE 设备连接与 GATT 交互从发现服务到监听通知每一步都带超时与重试逻辑连接一个 BLE 设备并读取其 GATT 数据远不止connect()read()两行代码。Windows 原生 API 要求你手动管理连接状态、服务发现流程、特征值句柄缓存、MTU 协商、通知注册与注销。这个项目把这些都封装进了BLEDevice类并强制加入超时与重试——因为工业现场的 BLE 设备经常响应慢、丢包、甚至假死。3.1 连接设备BLEDevice::Connect()的完整链路// src/ble_device.cpp HRESULT BLEDevice::Connect(DWORD timeoutMs) { // 1. 使用 BluetoothLEFindFirstDevice 找到设备基于地址 BLUETOOTH_LE_DEVICE_INFORMATION devInfo {}; devInfo.Address m_address; HANDLE hDevice nullptr; HRESULT hr BluetoothLEFindFirstDevice(devInfo, hDevice); if (FAILED(hr)) { return hr; } // 2. 设置连接参数最小/最大连接间隔7.5ms ~ 4s从机延时0超时20s // 注意Windows 不允许你直接设连接参数只能通过 BluetoothGATTConnect 设置 hint BLUETOOTH_GATT_CONNECT_PARAMS connParams {}; connParams.ConnectionIntervalMin 6; // 7.5ms connParams.ConnectionIntervalMax 100; // 125ms connParams.SlaveLatency 0; connParams.SupervisionTimeout 200; // 2s单位10ms // 3. 发起连接异步但此处我们同步等待 HANDLE hConnectEvent CreateEvent(nullptr, TRUE, FALSE, nullptr); if (!hConnectEvent) { CloseHandle(hDevice); return HRESULT_FROM_WIN32(GetLastError()); } // 4. 注册连接完成回调内部使用 BluetoothGATTConnect APC hr m_gattClient-ConnectAsync(hDevice, connParams, [this, hConnectEvent](HRESULT status) { m_isConnected SUCCEEDED(status); SetEvent(hConnectEvent); }); if (FAILED(hr)) { CloseHandle(hConnectEvent); CloseHandle(hDevice); return hr; } // 5. 等待连接完成带超时 DWORD waitRes WaitForSingleObject(hConnectEvent, timeoutMs); CloseHandle(hConnectEvent); if (waitRes WAIT_TIMEOUT) { // 主动取消连接Windows 无 CancelConnect API只能关闭句柄 CloseHandle(hDevice); return HRESULT_FROM_WIN32(ERROR_TIMEOUT); } if (SUCCEEDED(hr) m_isConnected) { m_hDevice hDevice; // 保存句柄供后续 GATT 操作 // 启动服务发现 DiscoverServices(timeoutMs); } else { CloseHandle(hDevice); } return hr; }这段代码的血泪经验在于Windows 的BluetoothGATTConnect不是“连接成功”而是“连接请求已发出”。真正的连接状态要等 APC 回调触发后才能确认。很多开发者直接WaitForSingleObject等BluetoothGATTConnect返回结果永远等不到——因为该函数本身是同步返回的返回S_OK表示请求已提交真正的完成靠 APC。所以这里用CreateEvent 回调SetEvent来桥接异步到同步是工业级代码的标配。3.2 服务发现与特征值读取缓存句柄避免重复发现// src/ble_device.cpp HRESULT BLEDevice::DiscoverServices(DWORD timeoutMs) { if (!m_hDevice) return E_FAIL; // 1. 调用 BluetoothGATTGetServices 获取所有服务 ULONG serviceCount 0; PBTH_LE_GATT_SERVICE pServices nullptr; HRESULT hr BluetoothGATTGetServices(m_hDevice, serviceCount, pServices, 0); if (FAILED(hr)) return hr; // 2. 遍历服务过滤出我们需要的例如 Battery Service: 0000180F-0000-1000-8000-00805F9B34FB for (ULONG i 0; i serviceCount; i) { auto svc pServices[i]; if (IsEqualGUID(svc.ServiceUuid, SERVICE_UUID_BATTERY)) { auto pSvc std::make_uniqueBLEService(svc.ServiceUuid, svc.AttributeHandleRange.Begin); // 3. 对该服务调用 BluetoothGATTGetCharacteristics ULONG charCount 0; PBTH_LE_GATT_CHARACTERISTIC pChars nullptr; hr BluetoothGATTGetCharacteristics(m_hDevice, svc, charCount, pChars, 0); if (SUCCEEDED(hr)) { for (ULONG j 0; j charCount; j) { auto chr pChars[j]; if (IsEqualGUID(chr.CharacteristicUuid, CHAR_UUID_BATTERY_LEVEL)) { pSvc-AddCharacteristic(std::make_uniqueBLECharacteristic( chr.CharacteristicUuid, chr.ValueHandle, chr.CccdHandle, // 自动提取 CCCD 句柄用于通知 chr.Properties )); } } free(pChars); } m_services.push_back(std::move(pSvc)); } } free(pServices); return S_OK; } // 读取电池电量同步方式 HRESULT BLEDevice::ReadBatteryLevel(uint8_t level) { auto pChar FindCharacteristic(SERVICE_UUID_BATTERY, CHAR_UUID_BATTERY_LEVEL); if (!pChar) return E_FAIL; // 4. 读取值BluetoothGATTReadValue 是同步 API但需传入缓冲区 uint8_t buffer[2] {0}; // 电池电量是 uint81 字节 ULONG bytesReturned 0; HRESULT hr BluetoothGATTReadValue( m_hDevice, pChar-GetValueHandle(), buffer, sizeof(buffer), bytesReturned, 0 // flags0 表示默认 ); if (SUCCEEDED(hr) bytesReturned 1) { level buffer[0]; return S_OK; } return hr; }关键点BluetoothGATTGetServices和BluetoothGATTGetCharacteristics必须在设备已连接后调用否则返回E_INVALIDARGpChar-GetCccdHandle()是自动提取的 CCCD 句柄这是开启通知的前提——很多教程漏掉这一步导致BluetoothGATTRegisterValueNotification失败却不知原因BluetoothGATTReadValue是同步 API但它的超时由系统控制约 30 秒项目中封装了ReadValueWithTimeout()内部用CreateThreadpoolTimer实现软超时并主动CancelIoEx防止线程卡死。3.3 开启通知并监听BLECharacteristic::EnableNotification()// src/ble_characteristic.cpp HRESULT BLECharacteristic::EnableNotification(BLEDevice* pDevice, std::functionvoid(const uint8_t*, size_t) callback) { if (!m_cccdHandle) return E_FAIL; // 没有 CCCD 句柄无法开启通知 // 1. 构造 CCCD 值0x0001 表示启用通知NOTIFY0x0002 表示启用指示INDICATE uint16_t cccdValue 0x0001; HRESULT hr BluetoothGATTWriteValue( pDevice-GetDeviceHandle(), m_cccdHandle, (PUCHAR)cccdValue, sizeof(cccdValue), 0 ); if (FAILED(hr)) return hr; // 2. 注册通知回调这才是真正的监听入口 // 注意回调函数必须是静态函数或 lambda 捕获 this但 Windows 要求 LPTHREAD_START_ROUTINE // 所以我们用 BLECallbackDispatcher 封装 hr BluetoothGATTRegisterValueNotification( pDevice-GetDeviceHandle(), m_valueHandle, m_notificationOverlapped, // OVERLAPPED 结构内部已初始化 [](DWORD dwErrorCode, DWORD dwNumberOfBytesTransfered, LPOVERLAPPED lpOverlapped) { // APC 回调从 lpOverlapped 拿出 this 指针调用用户 callback auto pThis static_castBLECharacteristic*(lpOverlapped-hEvent); if (dwErrorCode ERROR_SUCCESS dwNumberOfBytesTransfered 0) { // 从 OVERLAPPED 关联的缓冲区读数据缓冲区在 EnableNotification 时已分配 pThis-m_userCallback(pThis-m_readBuffer, dwNumberOfBytesTransfered); } // 重新投递通知Windows 不自动重注册必须手动 BluetoothGATTRegisterValueNotification( pThis-m_pDevice-GetDeviceHandle(), pThis-m_valueHandle, pThis-m_notificationOverlapped, OnNotificationAPC ); } ); return hr; }这里最易踩坑的是第 2 步末尾的注释“Windows 不自动重注册”。BluetoothGATTRegisterValueNotification是一次性的——收到一次通知后回调触发但之后再有数据来系统不会再次调用你的 APC。你必须在 APC 内部立刻重新调用BluetoothGATTRegisterValueNotification否则监听就断了。这个细节官方文档藏得很深无数项目因此出现“只收到第一条通知”的问题。4. 避坑指南5 个真实踩过的坑现象、原因、解决全写清楚照着改就能跑通4.1 现象BluetoothLEFindFirstDevice总是返回空设备GetLastError()是ERROR_SUCCESS原因调用前未调用BluetoothLESetScanFilter或扫描未开启。Windows 要求必须先启动扫描哪怕只开 100ms设备信息才会被内核缓存到BluetoothLEFindFirstDevice可查的索引中。单纯调BluetoothFindFirstRadio是不够的。解决确保BLEAdapter::Initialize()中调用了BluetoothLESetScanFilter并在Connect()前至少执行一次StartScan(100)即使只扫 100ms。可在日志中加一句Scanning for 100ms to populate device cache...。4.2 现象BluetoothGATTConnect回调中status是0x80070005ACCESS_DENIED原因当前进程没有蓝牙权限。Windows 10 1809 要求桌面应用必须在 manifest 中声明uap:Capability Namebluetooth且用户需在“设置 隐私 蓝牙”中开启“允许应用访问蓝牙”。命令行程序默认无此权限。解决为.exe添加app.manifest文件内容包含Capabilities uap:Capability Namebluetooth / /Capabilities并在 VS 项目属性中设置 “Manifest Tool Input and Output Additional Manifest Files” 指向该文件。4.3 现象BluetoothGATTReadValue返回0x8007001FDEVICE_OFF_LINE但设备明明连着原因设备在连接后进入低功耗休眠或 Windows 内核的 GATT 缓存过期。BluetoothGATTReadValue不会自动重连它假设设备句柄hDevice仍有效。解决在ReadValue前加一层健康检查DWORD dwSize 0; if (BluetoothGetDeviceInfo(m_hDevice, nullptr, dwSize) ! ERROR_INSUFFICIENT_BUFFER) { // 句柄无效尝试重连 Disconnect(); Connect(5000); }4.4 现象开启通知后OnNotificationAPC只触发一次后续数据不再到达原因如 3.3 节所述BluetoothGATTRegisterValueNotification是一次性注册。APC 执行完后系统不会自动为你再次注册。解决必须在 APC 回调函数内部立即重新调用BluetoothGATTRegisterValueNotification。注意不能在 APC 外部调用否则OVERLAPPED结构可能已被回收。项目中BLECharacteristic::OnNotificationAPC内部就做了这件事。4.5 现象BluetoothGATTWriteValue写入失败hr是0x80070057INVALID_PARAMETER原因写入缓冲区大小与特征值声明的最大长度不符。例如某特征值在 GATT DB 中声明maxLen1你却传入 2 字节缓冲区Windows 直接拒绝。解决在WriteValue前先调用BluetoothGATTGetCharacteristics获取BTH_LE_GATT_CHARACTERISTIC结构读取MaximumLength字段。若为 0表示无限制按实际写入长度否则确保bufferSize MaximumLength。项目中BLECharacteristic::WriteValue()已内置此校验。5. 二次开发实战如何快速接入你自己的 BLE 设备协议三步改完5 分钟验证你拿到的不是通用 BLE 扫描器而是一个可深度定制的 BLE 底层驱动。我常把它集成进两类项目一是 Qt 工业监控软件二是 MFC 设备烧录工具。下面以“接入一款国产温湿度传感器服务 UUID0000AA00-0000-1000-8000-00805F9B34FB温度特征值 UUID0000AA01-0000-1000-8000-00805F9B34FB格式int16_t单位 0.1℃”为例手把手带你改。5.1 第一步定义 UUID 常量ble_utils.h// 在 ble_utils.h 中添加 extern const GUID SERVICE_UUID_TEMP_HUMI; extern const GUID CHAR_UUID_TEMPERATURE; extern const GUID CHAR_UUID_HUMIDITY; // 在 ble_utils.cpp 中定义 const GUID SERVICE_UUID_TEMP_HUMI {0x0000AA00, 0x0000, 0x1000, {0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB}}; const GUID CHAR_UUID_TEMPERATURE {0x0000AA01, 0x0000, 0x1000, {0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB}}; const GUID CHAR_UUID_HUMIDITY {0x0000AA02, 0x0000, 0x1000, {0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB}};提示UUID 字符串转GUID是体力活我一般用在线工具搜索 “uuid to guid converter”生成{0x..., ...}格式粘贴进来即可。别手敲少个0x就编译不过。5.2 第二步扩展BLEDevice添加专用读取方法ble_device.h/cpp// ble_device.h 中 BLEDevice 类添加 public: HRESULT ReadTemperature(int16_t temp); // 单位0.1℃ HRESULT ReadHumidity(uint16_t humi); // 百分比整数 // ble_device.cpp 中实现 HRESULT BLEDevice::ReadTemperature(int16_t temp) { auto pChar FindCharacteristic(SERVICE_UUID_TEMP_HUMI, CHAR_UUID_TEMPERATURE); if (!pChar) return E_FAIL; uint8_t buffer[2] {0}; ULONG bytesReturned 0; HRESULT hr BluetoothGATTReadValue( m_hDevice, pChar-GetValueHandle(), buffer, sizeof(buffer), bytesReturned, 0 ); if (SUCCEEDED(hr) bytesReturned 2) { temp (int16_t)((uint16_t)buffer[0] | ((uint16_t)buffer[1] 8)); return S_OK; } return hr; } // 同理实现 ReadHumidity...5.3 第三步写一个极简测试test/test_temp_sensor.cpp#include ble_adapter.h #include ble_device.h #include iostream #include thread #include chrono int main() { BLEAdapter adapter; HRESULT hr adapter.Initialize(); if (FAILED(hr)) { std::wcerr LFailed to init adapter: hr std::endl; return -1; } // 1. 扫描 5 秒找设备 std::vectorBLUETOOTH_LE_DEVICE_INFORMATION devices; adapter.StartScan(5000, [](const BLUETOOTH_LE_DEVICE_INFORMATION info) { if (wcsstr(info.szName, LTempSensor)) { devices.push_back(info); } }); if (devices.empty()) { std::wcout LNo TempSensor found. std::endl; return -1; } // 2. 连接第一个 BLEDevice device(devices[0].Address); hr device.Connect(10000); if (FAILED(hr)) { std::wcerr LConnect failed: hr std::endl; return -1; } // 3. 读温度 int16_t temp 0; hr device.ReadTemperature(temp); if (SUCCEEDED(hr)) { std::wcout LTemperature: (double)temp / 10.0 L ℃ std::endl; } device.Disconnect(); return 0; }编译命令VS 2019cl /EHsc /MD test_temp_sensor.cpp src\*.cpp /link BluetoothAPIs.lib Bthprops.lib注意必须链接BluetoothAPIs.lib和Bthprops.lib缺一不可。Bthprops.lib提供BluetoothGetRadioValue等函数很多人只链前者编译报unresolved external symbol。5.4 验证技巧用 Windows 自带工具交叉验证改完代码别急着跑先用系统工具确认设备行为打开Windows 设置 设备 蓝牙和其他设备 添加蓝牙或其他设备 一切设备看能否搜到你的传感器下载微软官方Bluetooth LE Explorer免费 Store 应用连接后手动浏览 GATT 服务确认服务/特征值 UUID 和读写权限是否匹配你代码里的定义若发现特征值可读但代码读不出用 Wireshark Microsoft Bluetooth LE Sniffer需额外硬件抓包看 Windows 是否真的发出了Read Request。从那以后我每次接入新设备都强制走一遍这三步查 Windows 设置可见性 → 用 BLE Explorer 看 GATT 结构 → 改代码 → 用test_temp_sensor.cpp验证。省下三天 debug 时间。希望帮到你。本文还有配套的精品资源点击获取