资讯详情 Windows原生C/C++ BLE开发:绕过WinRT直连手环传感器
📅 2026/10/19 20:13:04
简介本资源是一份面向Windows平台C/C开发者的BLE低功耗蓝牙通信实践工程适用于具备基础WinRT编程经验的中高级开发者解决在桌面端调用系统原生API实现蓝牙设备扫描、连接、GATT服务发现与数据读写等核心场景问题。压缩包为10KB的ZIP格式共含11个文件包括4个头文件.h封装BLE操作接口与回调定义4个源文件.cpp实现设备枚举、广告监听、特征值读写等关键逻辑另含项目配置文件.vcxproj、过滤器.filters及用户设置.user结构完整、开箱即用。已有5965人学习下载代码基于Windows SDK 10充分使用winrt/Windows.Devices.Bluetooth等命名空间注释清晰模块划分明确——BleHandle.h/.cpp专注通信抽象BLEComm.h/.cpp负责主控调度便于读者理解WinRT异步模型与蓝牙协议栈集成方式并可快速迁移至实际工业控制、IoT设备调试等项目中。1. Windows 平台下用纯 C/C 直接操作 BLE 设备不依赖 Qt、.NET 或 UWP绕过 WinRT 黑匣子连上手环/标签/传感器只要 300 行核心代码你有没有试过在 Windows 上用 C 写一个能扫描、连接、读写某款国产蓝牙温湿度标签的控制台程序结果卡在BluetoothGATTGetServices返回ERROR_INVALID_HANDLE或者发现用Windows.Devices.Bluetooth的 C/CX 代码一编译就报winrt/base.h not found而你压根不想装 VS 2019、不想配 C/WinRT 工具链、更不想把整个工程迁到 UWP 沙箱里这个项目就是为这类场景写的——它用 Windows 10 1809 原生支持的 Bluetooth LE APIBluetoothAPIs.hBthLEEnum.h纯 Win32 C/C 实现完整 BLE 生命周期设备发现 → GATT 服务枚举 → 特征值读写 → 通知订阅。没有 Qt 的QBluetoothDeviceDiscoveryAgent抽象层没有 .NET 的BluetoothLEDevice.FromIdAsync异步黑盒所有调用直通系统内核驱动。适合嵌入式网关、工业采集终端、自动化测试脚本等需要轻量、可控、无运行时依赖的场景。如果你手头有 Visual Studio 2017 或更高版本5 分钟就能跑通ble_scan.exe和ble_read_temp.exe后续改 UUID、换设备、加日志全在.c文件里动几行。2. 架构与技术栈基于 Windows Bluetooth LE 原生 API 的三层结构设计这个项目不是封装库而是一个可执行、可调试、可裁剪的最小可行 BLE 控制台工具集。它不引入任何第三方 SDK完全基于 Windows SDK 自带头文件和静态链接库编译产物是单个.exe无 DLL 依赖。整个结构按职责划分为三层底层驱动交互层btapi_wrap.c、GATT 协议逻辑层gatt_client.c、应用命令行胶水层main.c。这种分法不是为了炫技而是为了让你在调试时能快速定位问题比如扫描失败先看btapi_wrap.c里BluetoothFindFirstRadio是否返回有效句柄连接后读不到数据就进gatt_client.c查BluetoothGATTGetCharacteristics的返回码和pCharValue缓冲区长度命令行参数解析错了直接改main.c的switch (argc)分支就行。所有模块都用标准 C99 编写C 仅用于main.cpp中的少量字符串拼接可轻松转回 C确保你在老旧工控机或 CI 环境中也能用 MinGW-w64 或 Clang 编译通过。2.1 底层驱动交互层btapi_wrap.c封装 Windows 蓝牙无线电句柄管理Windows 的 BLE 操作必须先获取本地蓝牙无线电Radio句柄这是所有后续 API 的起点。btapi_wrap.c就干这一件事安全打开、枚举、关闭 Radio并提供线程安全的句柄缓存。关键不是“怎么写”而是“怎么不出错”——很多开发者第一次调用BluetoothFindFirstRadio就失败根本原因是没检查BLUETOOTH_FIND_RADIO_PARAMS结构体的dwSize字段是否初始化为sizeof(BLUETOOTH_FIND_RADIO_PARAMS)。这个字段是 Windows API 的经典陷阱大小不对整个结构体传参就错位后续所有调用都返回ERROR_INVALID_PARAMETER。// btapi_wrap.c #include windows.h #include bluetoothapis.h #include bthleenum.h static HBLUETOOTH_RADIO_FIND s_hRadioFind NULL; static BLUETOOTH_RADIO_INFO s_radioInfo {0}; // 初始化并获取第一个可用蓝牙无线电句柄 HBLUETOOTH_RADIO_HANDLE bt_open_radio() { BLUETOOTH_FIND_RADIO_PARAMS findParams {0}; findParams.dwSize sizeof(BLUETOOTH_FIND_RADIO_PARAMS); // 必须显式赋值玄学坑点1 s_hRadioFind BluetoothFindFirstRadio(findParams, s_radioInfo); if (s_hRadioFind NULL) { DWORD err GetLastError(); printf(BluetoothFindFirstRadio failed: %lu\n, err); return NULL; } return s_radioFind; }这段代码里findParams.dwSize是硬编码的sizeof(...)不是宏定义也不是offsetof因为 Windows API 文档明确要求该字段必须是结构体实际大小。我见过太多人把它写成0或sizeof(void*)结果在不同 Windows 版本上行为不一致——1809 可能容忍22H2 就直接拒绝。另外注意s_hRadioFind是全局静态变量避免多线程并发调用时句柄被覆盖。bt_open_radio()返回的是HBLUETOOTH_RADIO_HANDLE不是HANDLE不能用CloseHandle()关闭必须用BluetoothFindRadioClose()否则资源泄漏。这个细节在 MSDN 示例里藏得很深但项目里已封装好bt_close_radio()来兜底。2.2 GATT 协议逻辑层gatt_client.c实现服务发现与特征值操作BLE 的核心是 GATTGeneric Attribute Profile所有通信都围绕 Service → Characteristic → Descriptor 展开。gatt_client.c不做抽象直接调用BluetoothGATTGetServices、BluetoothGATTGetCharacteristics等原生函数把每个步骤的错误码、缓冲区长度、内存分配逻辑全部暴露出来。比如服务发现Windows 要求你先调用一次获取所需缓冲区大小再分配内存重试——这不是设计缺陷而是 Windows 内核驱动的内存管理策略。项目里用两阶段模式处理// gatt_client.c #include windows.h #include bluetoothapis.h #include bthleenum.h // 获取指定设备的所有 GATT 服务 BOOL gatt_discover_services(HBLUETOOTH_RADIO_HANDLE hRadio, BLUETOOTH_DEVICE_ADDRESS addr, PBLUETOOTH_GATT_SERVICE *ppServices, PDWORD pNumServices) { DWORD dwSize 0; DWORD dwError 0; // 第一阶段获取所需缓冲区大小 BluetoothGATTGetServices(hRadio, addr, 0, NULL, dwSize, 0); dwError GetLastError(); if (dwError ! ERROR_MORE_DATA dwError ! ERROR_SUCCESS) { printf(First call to BluetoothGATTGetServices failed: %lu\n, dwError); return FALSE; } // 分配缓冲区注意Windows 要求按字节对齐这里用 malloc 而非 HeapAlloc *ppServices (PBLUETOOTH_GATT_SERVICE)malloc(dwSize); if (!*ppServices) { printf(Failed to allocate %lu bytes for services\n, dwSize); return FALSE; } // 第二阶段真正获取服务列表 if (!BluetoothGATTGetServices(hRadio, addr, dwSize, *ppServices, dwSize, 0)) { dwError GetLastError(); printf(Second call to BluetoothGATTGetServices failed: %lu\n, dwError); free(*ppServices); *ppServices NULL; return FALSE; } *pNumServices (*ppServices)-NumberOfCharacteristics; // 注意这里存的是特征值数量不是服务数 return TRUE; }重点看注释里的三处细节dwSize必须先调用一次获取不能硬写1024或4096否则在服务多的设备如智能手表上直接截断内存分配用malloc而非HeapAlloc因为BluetoothGATTGetServices内部不关心堆类型但malloc更易调试内存泄漏NumberOfCharacteristics字段名有误导性——它实际表示该服务下特征值的数量不是服务总数项目里已加注释提醒避免你误以为*pNumServices是服务个数。这个函数返回的是PBLUETOOTH_GATT_SERVICE链表每个节点包含ServiceUuid128 位 UUID、StartHandle、EndHandle后续读写特征值全靠这两个 handle 定位。没有自动 UUID 解析你要自己比对memcmp(service.ServiceUuid, target_uuid, 16)但好处是零依赖、零反射、零运行时开销。2.3 应用胶水层main.c提供可调试的命令行入口main.c是整个项目的“开关面板”。它不处理任何协议逻辑只做三件事解析命令行参数-scan,-connect,-read,-notify、调用对应模块函数、打印结构化输出。比如-scan模式它会循环调用bt_scan_devices()每发现一个设备就打印 MAC 地址、信号强度RSSI、本地名称如果广播了的话// main.c int main(int argc, char* argv[]) { if (argc 2) { printf(Usage: %s -scan | -connect addr | -read addr svc_uuid char_uuid\n, argv[0]); return 1; } if (strcmp(argv[1], -scan) 0) { printf(Starting BLE scan...\n); bt_scan_devices(); // 内部调用 bt_open_radio() BluetoothFindFirstDevice() } else if (strcmp(argv[1], -connect) 0 argc 3) { BLUETOOTH_DEVICE_ADDRESS addr parse_bt_address(argv[2]); // AA:BB:CC:DD:EE:FF → uint64_t if (addr 0) { printf(Invalid address format\n); return 1; } gatt_connect_device(addr); } // ... 其他分支 }这里parse_bt_address()是个实用小函数把常见的AA:BB:CC:DD:EE:FF、AABBCCDDEEFF、aa-bb-cc-dd-ee-ff格式统一转成BLUETOOTH_DEVICE_ADDRESS即uint64_t。Windows API 要求地址是大端序的 6 字节整数但用户输入习惯是十六进制字符串这个转换逻辑如果漏掉大小端处理连接永远失败。项目里已实现健壮解析支持大小写、分隔符可选且对非法字符如G直接返回 0 并提示而不是静默截断。提示main.c中所有printf都加了\n避免 Windows 控制台缓冲导致日志不实时。这是血泪经验——某次在工控机上调试发现printf(Connecting...)后没换行程序卡住半天没输出其实是缓冲区没刷加\n立刻解决。3. 编译与运行从零开始构建可执行文件的完整流程这个项目的目标是“开箱即用”不是“配置半天”。它不依赖 CMakeLists.txt 的复杂规则也不需要你手动设置 Windows SDK 版本号。整个编译流程基于 Visual Studio 的cl.exe命令行工具链兼容 VS 2017/2019/2022也支持 MinGW-w64需额外链接BluetoothAPIs.lib。下面是你真正需要做的三步每一步都有明确命令和预期输出。3.1 环境准备确认 Windows SDK 和编译器路径首先确认你的开发机已安装 Windows 10 SDK1809 或更新版。打开“Visual Studio Installer”勾选 “Windows 10/11 SDK” 组件。然后以管理员身份运行 “x64 Native Tools Command Prompt for VS 2019”或对应版本。这个命令行环境会自动设置INCLUDE、LIB、PATH让你无需手动配置头文件路径。验证是否就绪# 在 VS 命令行中执行 cl /? # 应输出 cl.exe 版本信息如 Microsoft (R) C/C Optimizing Compiler Version 19.29.30137 echo %WindowsSdkDir% # 应输出类似 C:\Program Files (x86)\Windows Kits\10\如果cl命令未识别请不要去网上搜“如何配置环境变量”直接重装 VS Build Tools 或使用 VS Installer 修复。这是最省时间的方案。我曾帮某高校实验室排查过他们花两天配环境变量结果发现是 VS Installer 里 SDK 组件根本没装——重装 SDK 后 5 分钟搞定。3.2 编译命令一条cl命令生成全部.exe项目源码共 4 个文件btapi_wrap.c、gatt_client.c、uuid_utils.cUUID 字符串解析、main.c。编译只需一条命令链接BluetoothAPIs.lib和Bthprops.lib后者提供BluetoothFindFirstDevice等函数cl /O2 /MT /Fe:ble_scan.exe main.c btapi_wrap.c gatt_client.c uuid_utils.c /link BluetoothAPIs.lib Bthprops.lib参数说明/O2最大优化减小体积、提升性能/MT静态链接 CRT避免目标机器缺少vcruntime140.dll/Fe:ble_scan.exe指定输出文件名/link ...链接蓝牙相关库缺一不可编译成功后你会看到ble_scan.exe生成大小约 120KB静态链接后。运行它ble_scan.exe -scan预期输出真实设备扫描结果Starting BLE scan... Found device: AA:BB:CC:DD:EE:FF, RSSI: -62, Name: TempSensor-01 Found device: 11:22:33:44:55:66, RSSI: -78, Name: HeartRate-Band Scan completed. Found 2 devices.如果没输出设备请先确认你的 Windows 蓝牙已开启且处于可被发现状态扫描设备如手环确实在广播有些设备默认关闭广播需长按按钮触发你不是在虚拟机里运行——Hyper-V 或 VMware 的蓝牙直通支持极差必须在物理机上测试。3.3 运行时依赖与权限为什么你的程序总在BluetoothFindFirstRadio失败这是新手最高频的翻车点。BluetoothFindFirstRadio失败90% 的原因是权限不足。Windows 蓝牙 API 要求调用进程拥有SE_LOAD_DRIVER_PRIVILEGE加载驱动权限而普通用户账户默认没有。解决方案只有两个方案 A推荐以管理员身份运行命令行再执行.exe方案 B在程序 manifest 文件中声明requireAdministrator双击运行时自动提权项目已附带app.manifest文件内容如下?xml version1.0 encodingUTF-8 standaloneyes? assembly xmlnsurn:schemas-microsoft-com:asm.v1 manifestVersion1.0 trustInfo xmlnsurn:schemas-microsoft-com:asm.v3 security requestedPrivileges requestedExecutionLevel levelrequireAdministrator uiAccessfalse/ /requestedPrivileges /security /trustInfo /assembly编译时用/MANIFESTINPUT:app.manifest参数嵌入即可。如果你跳过这步ble_scan.exe在非管理员命令行下必然失败错误码是ERROR_ACCESS_DENIED5。这不是代码 bug是 Windows 安全机制必须正视。注意某些企业域环境禁用了管理员提权此时只能联系 IT 部门申请临时权限。不要尝试用CreateProcessAsUser或服务方式绕过——既违反安全策略又增加复杂度得不偿失。4. 常见问题排查5 个真实踩坑记录与速查解决方案开发 Windows BLE 应用不是写 Hello World很多问题现象诡异、原因隐蔽。以下是我在多个工业客户现场实测总结的 5 个高频问题每个都按「现象 → 原因 → 解决」给出可立即执行的方案不讲原理只给答案。4.1 现象BluetoothFindFirstRadio返回NULLGetLastError()是5ERROR_ACCESS_DENIED原因进程未以管理员权限运行Windows 拒绝授予蓝牙无线电访问权。解决右键点击命令行图标 → “以管理员身份运行”再执行ble_scan.exe -scan。若需双击运行确保已将app.manifest嵌入可执行文件编译时加/MANIFESTINPUT:app.manifest。4.2 现象BluetoothFindFirstDevice扫描不到设备GetLastError()是122ERROR_INSUFFICIENT_BUFFER原因BLUETOOTH_DEVICE_SEARCH_PARAMS结构体的dwSize字段未初始化为sizeof(BLUETOOTH_DEVICE_SEARCH_PARAMS)。解决打开btapi_wrap.c找到BluetoothFindFirstDevice调用前的searchParams变量确认其dwSize赋值语句存在且正确。删除所有 {0}初始化改为显式searchParams.dwSize sizeof(searchParams);。4.3 现象连接设备后BluetoothGATTGetServices返回0个服务GetLastError()是1168ERROR_NOT_FOUND原因目标设备是经典蓝牙BR/EDR设备不是 BLE 设备。Windows 的BluetoothGATT*系列 API 仅支持 BLE对传统蓝牙无效。解决用手机 App如 nRF Connect确认该设备是否广播 BLE 广告包。若广告数据中不含0x0201Flags或0x030316-bit Service UUIDs等 BLE 特征则不是 BLE 设备此项目不适用。4.4 现象BluetoothGATTReadCharacteristicValue读取特征值成功但返回数据全是0x00原因特征值 handle 错误。BluetoothGATTGetCharacteristics返回的pChar-CharacteristicHandle是属性句柄Attribute Handle而读取时需用pChar-ValueHandle值句柄。两者常被混淆。解决在gatt_client.c的读取函数中确认使用的是pChar-ValueHandle而非pChar-CharacteristicHandle。项目源码中已用注释标出“// 注意读取用 ValueHandle不是 CharacteristicHandle”。4.5 现象启用通知BluetoothGATTRegisterForValueNotifications后回调函数从未触发原因未向通知配置描述符Client Characteristic Configuration Descriptor, CCCD写入0x0001启用通知。Windows 不自动写 CCCD必须手动调用BluetoothGATTWriteCharacteristicValue。解决在注册通知前先获取该特征值的 CCCD handle通常为ValueHandle 1然后构造 2 字节数据0x01, 0x00写入。项目gatt_client.c中gatt_enable_notify()函数已实现此逻辑检查是否被注释掉。5. 二次开发实战如何快速适配你的 BLE 设备以某款温湿度传感器为例现在你已经能跑通扫描和连接下一步是让程序读出真实数据。假设你手上有某款国产 BLE 温湿度传感器它的 GATT 结构如下Service UUID0000181a-0000-1000-8000-00805f9b34fbEnvironmental SensingTemperature Characteristic UUID00002a6e-0000-1000-8000-00805f9b34fbData format2 字节有符号整数℃需除以 100 得到实际值改造步骤分三步改 UUID、加解析、验数据。全程修改不超过 20 行代码无需重新编译整个项目。5.1 修改目标 UUID替换main.c中的硬编码字符串打开main.c找到-read命令的处理分支。原代码可能用占位 UUID你需要替换成传感器的真实 UUID// main.c 中 -read 分支 else if (strcmp(argv[1], -read) 0 argc 5) { BLUETOOTH_DEVICE_ADDRESS addr parse_bt_address(argv[2]); // 替换这两行 ↓↓↓ GUID svc_uuid {0xfb34fb9b5f800080, 0x00100000, 0x0010, {0x80,0,0,0,0,0,0,0x1a}}; // 181a GUID char_uuid {0xfb34fb9b5f800080, 0x00100000, 0x0010, {0x80,0,0,0,0,0,0,0x6e}}; // 2a6e // ↑↑↑ 替换结束 gatt_read_characteristic(addr, svc_uuid, char_uuid); }注意Windows 的GUID结构体是DWORD Data1; WORD Data2; WORD Data3; BYTE Data4[8];所以0000181a-...要拆成Data10x0000181aData20x0000Data30x1000Data4{0x80,0x00,0x00,0x80,0x5f,0x9b,0x34,0xfb}。项目已提供uuid_from_string()辅助函数在uuid_utils.c但手动赋值更快且避免字符串解析开销。5.2 添加数据解析逻辑在gatt_client.c的读取回调中处理原始字节BluetoothGATTReadCharacteristicValue返回的是PBLUETOOTH_GATT_VALUE其pValue字段指向原始字节数组。你需要根据传感器文档把字节转成有意义的数值。在gatt_read_characteristic()函数末尾添加// gatt_client.c void gatt_read_characteristic(BLUETOOTH_DEVICE_ADDRESS addr, GUID* pSvcUuid, GUID* pCharUuid) { // ... 前面是服务发现、特征值查找逻辑 ... PBLUETOOTH_GATT_VALUE pValue NULL; DWORD dwValueSize 0; if (BluetoothGATTReadCharacteristicValue(hRadio, addr, pChar-ValueHandle, pValue, dwValueSize, 0)) { if (dwValueSize 2) { int16_t raw_temp *(int16_t*)pValue-pValue; // 直接 reinterpret_cast float temp_c (float)raw_temp / 100.0f; printf(Temperature: %.2f °C\n, temp_c); } else { printf(Unexpected value size: %lu bytes\n, dwValueSize); } free(pValue); // 必须释放Windows 不负责 } }这里int16_t强制转换是安全的因为 BLE 特征值传输是小端序而 x86/x64 也是小端无需字节序转换。如果传感器文档写明是大端则用ntohs(*(uint16_t*)pValue-pValue)。5.3 验证与调试用nRF Connect对照原始数据不要相信自己的解析逻辑一定要用专业工具交叉验证。下载手机 App “nRF Connect”连接同一台传感器找到2a6e特征值点击 “Read” 查看原始 hex 数据如0x2c, 0x01。转成小端整数0x012c 300除以 100 得3.00°C。然后运行你的ble_read.exe对比输出是否一致。如果不一致检查pValue-pValue是否为空指针BluetoothGATTReadCharacteristicValue失败时不会分配内存dwValueSize是否真等于 2有些传感器返回 4 字节浮点数除法是否用了整数除法/ 100而非/ 100.0f导致截断。从那以后我每次适配新设备都强制走一遍 nRF Connect 对照流程——哪怕文档写得再清楚硬件固件版本差异也会导致数据格式突变。这一步花 2 分钟能省下 2 小时抓包分析时间。希望帮到你。本文还有配套的精品资源点击获取