简介:面向Windows平台C/C++开发者的低功耗蓝牙(BLE)操作示例工程,基于WinRT API开发,解决原生C++代码中调用蓝牙协议栈进行设备通信的问题。示例代码覆盖设备发现、广播监听、GATT服务连接、特征值读取与写入等常用环节,适合正在学习Windows蓝牙编程或希望快速实现BLE功能的工程师。
压缩包体积约10KB,共11个文件。文件组成以4个头文件和4个C++源文件为主,头文件负责声明接口与数据结构,源文件负责具体业务逻辑;另附Visual Studio工程配置与筛选文件,方便直接打开编译或迁移复用。工程将蓝牙操作集中封装为独立模块,并包含DLL入口和预编译头,整体结构清晰,能够直观展示WinRT组件在传统C++工程中如何集成与调用。已有5951人学习该资源,体量小巧、目标明确,既可当作入门材料,也能在项目开发中直接参考其中的连接管理与数据处理流程。 做 Windows 平台上的 BLE 开发,尤其还想用 C/C++ 而不是 C#,第一反应通常是去翻微软官方的 Bluetooth 文档。翻完基本就懵了:官方示例几乎全是 C#/JavaScript/WinRT,C++ 的例子少得可怜;Win32 传统蓝牙 API 又主要覆盖 BR/EDR,对 BLE 的 GATT 这套支持非常有限。网上能搜到的 C++ 源码要么是远古时代的,要么干脆编译不过。
我最早也在这个坑里卡了很久,后来把 Windows.Devices.Bluetooth 这套 WinRT API 用 C++/WinRT 封装跑通之后,整个开发链路才算打开。这篇文章会把从扫描、连接、GATT 服务发现、特征值读写、通知订阅,到配对邦定和 MTU 协商的完整实操经验梳理一遍,适合想在 Windows 上用 C/C++ 跟 BLE 设备通信的开发者参考,也适合刚入手 BLE 开发、想弄明白整套流程该怎么走的新手。如果你手里有一块 ESP32、Nordic 开发板,或者蓝牙串口模块,跟着做一遍就能快速验证。
1. 技术路线之争:为什么绕不开UWP这套API
1.1 传统Win32蓝牙API为什么不行
Windows 上传统的蓝牙 API 有两个主要入口:一个是 Winsock 风格的 Bluetooth socket API,一个是 SetupAPI/DeviceIoControl 那套底层接口。这两个东西设计时主要面向蓝牙 2.0/3.0 时代的 BR/EDR 经典蓝牙,对应的是 RFCOMM、SDP 服务发现这一套模型。
BLE 和 BR/EDR 在底层接入层(LL)以上走的协议栈几乎完全不同。BLE 的核心是 GATT 这个属性协议,而 GATT 数据是打包在 ATT(Attribute Protocol)PDU 里的;传统 Winsock 蓝牙接口根本没有暴露 ATT/GATT 层的能力,你拿它做 BLE 连接、发现服务、收发 characteristic,基本等于用电话线上网跑 4K 视频——不是不行,是压根没这个通道。
如果你非要用 Win32 API 做 BLE,能查到的资料大多是指向“微软在 Windows 8 之后把 BLE 的支持放在 WinRT 里”,然后就没有然后了。这是一条死胡同,没必要硬闯。
1.2 C++/WinRT 是当前最舒服的姿势
WinRT(Windows Runtime)是微软在 Win8 之后引入的组件化 API 体系,Windows.Devices.Bluetooth 全家桶都建在它上面。早期 C++ 开发者通过 C++/CX(微软对 C++ 做的扩展语法,带 ^ 和 ref class 那种)来调用,但这个语法比较别扭,而且和标准 C++ 的互操作体验一般。
C++/WinRT 是微软后来力推的标准 C++ 封装,头文件库形式,纯 C++17 就能用,不需要 C++/CX 那套非标准语法。关键点在于:C++/WinRT 的 API 可以在普通 Win32 桌面程序里直接用,不一定非要写成 UWP 应用。这一点很多人有误解,以为做 WinRT 开发就必须上 UWP 模板、必须签 MSIX 包。实际上你创建一个普通的控制台程序或 Win32 窗口程序,初始化 WinRT 运行时之后,就可以像调用普通 C++ 库一样调用蓝牙 API。
1.3 开发环境准备
我目前的开发环境是 Visual Studio 2022 + Windows 11 SDK,用 CMake 构建。C++/WinRT 的库不是自己单独下载的,Windows SDK 装好之后,头文件和实现都打包在里面了。配置步骤大概是:
- Visual Studio 安装时勾选“使用 C++ 的桌面开发”工作负载,MSVC 工具链和 Windows SDK 一起装。
- 项目属性里确认 C++ 语言标准是 C++17 或更高。
- include 目录里确认有
<winrt/Windows.Devices.Bluetooth.h>这个头文件可访问就算 OK。 - 代码开头调用
winrt::init_apartment();初始化 WinRT 运行时。
如果你用 VSCode 写 C/C++,也可以,但注意 VSCode 本身不带 MSVC 编译器,需要配合 Visual Studio 的 Developer Command Prompt 环境来跑 CMake 和 cl.exe,或者安装 Ninja + clang 组合。建议刚开始直接上 Visual Studio,省去环境变量配置的心智负担。
1.4 权限与打包的坑
还有一个常见困惑:使用了蓝牙 API 是否需要像 UWP 那样在 Package.appxmanifest 里声明bluetoothcapability?如果是普通桌面程序(非 MSIX 打包),调用这些 API 时系统不会严格强制 manifest 权限声明。但如果你发布的是 MSIX 打包应用,就一定要在 manifest 里加<DeviceCapability Name="bluetooth"/>,否则运行时访问会被拒。
Windows 10/11 系统层面的蓝牙开关也要确认是打开的,否则设备枚举会返回空列表。这个听起来很基础,但我遇到过好几次明明代码写对了却扫不到设备,最后发现是系统飞行模式开着把蓝牙关了。
2. 扫描与过滤:找到你要的那台BLE设备
2.1 BluetoothLEAdvertisementWatcher 的基本用法
BLE 设备广播的核心 API 是BluetoothLEAdvertisementWatcher。它会侦听空中的广播报文,收到后会触发Received回调,回调里携带BluetoothLEAdvertisementReceivedEventArgs,里面包含设备地址、RSSI、广播数据和可选的扫描响应数据。
一个最基础的控制台扫描程序大概是这个样子:
#include <winrt/Windows.Devices.Bluetooth.Advertisement.h> #include <winrt/Windows.Foundation.Collections.h> #include <iostream> using namespace winrt; using namespace Windows::Devices::Bluetooth::Advertisement; void StartScan() { BluetoothLEAdvertisementWatcher watcher; watcher.Received([](BluetoothLEAdvertisementWatcher const&, BluetoothLEAdvertisementReceivedEventArgs const& args) { auto address = args.BluetoothAddress(); auto rssi = args.RawSignalStrengthInDBm(); auto localName = args.Advertisement().LocalName(); std::string name = winrt::to_string(localName); std::cout << "addr=" << std::hex << address << " rssi=" << std::dec << rssi << " name=" << name << std::endl; }); watcher.Start(); }注意BluetoothAddress()返回的是 64 位整数,通常用%012llX这种格式打印成 6 字节 MAC 地址的样子。广播里没带设备名称时,LocalName()是空字符串,这非常常见,别因为没名字就以为设备没广播。
2.2 广播类型与 Active/Passive 扫描的区别
热度词里有“ble广播类型”,这块值得展开。BLE 广播 PDU 主要分几类:ADV_IND(可连接的广播)、ADV_SCAN_IND(可扫描但不可连接)、ADV_NONCONN_IND(不可连接也不可扫描)、ADV_DIRECT_IND(定向连接广播,通常用于快速重连)。
BluetoothLEAdvertisementWatcher的ScanningMode可以选择 Active 或 Passive。Active 模式会主动向设备发送 SCAN_REQ,设备收到后如果支持会回复 SCAN_RSP,里面通常包含完整的设备名和附加服务数据;Passive 模式只被动接收广播,不发请求,更省电但拿到的信息少。
实际操作中,如果你在找某个外设,强烈建议用 Active 模式,因为很多设备的设备名只在广播包里放简写,完整名称要等扫描响应才发出来。开启方式就是给ScanningMode赋值:
watcher.ScanningMode(BluetoothLEScanningMode::Active);2.3 按服务UUID或厂商数据过滤
生产环境里扫描设备往往不是扫到啥都收,而是要过滤出目标设备。常见手段有两种:
一种是按服务 UUID 过滤。BLE 广播数据里允许塞 Service UUID 列表,Advertisement().ServiceUuids()可以拿到。你可以遍历这个集合,判断是否包含你要连接的那个服务的 UUID(比如自定义服务是 128 位 UUID,或 SIG 定义的 16 位 UUID 如 0xFFE0)。
另一种是按厂商自定义数据过滤。Nordic、TI、Dialog 这类厂商芯片常见的做法是把设备 MAC 或名字塞在 Manufacturer Specific Data 里。C++/WinRT 里拿到的ManufacturerData()是一个IVector<BluetoothLEManufacturerData>,每项有两个关键字段:CompanyId(厂商 ID,比如 0x0059 是 Nordic)和Data(IBuffer,需要转成字节数组做解析)。
我自己的习惯是优先用服务 UUID 过滤,因为比解析厂商数据简单,而且不容易被不同厂商的广播格式差异坑到。
2.4 扫描时容易踩的坑
第一个坑:Watcher 重复 Start 会抛异常或触发Stopped事件。同一时刻一个 watcher 实例只能处于 Running 状态,如果想重新扫,要先Stop(),等待Stopped事件触发后再Start()。不要连续调用两次 Start。
第二个坑:Received 回调跑在非 UI 线程。回调线程是线程池,不是主线程。控制台程序无所谓,但如果你做的是窗口程序,回调里更新 UI 控件必须要DispatcherQueue切回 UI 线程。
第三个坑:扫描到设备的BluetoothAddress()是公共地址还是随机地址,这个信息很重要。很多 BLE 设备为了防止被追踪,默认使用随机地址,每次广播地址可能不同。如果你用地址做设备记忆,会发现设备重连时地址对不上,这是因为随机地址的重解析需要配合 IRK(身份解析密钥),Windows 系统层面邦定后会自动处理地址解析,但你应用层如果在扫描阶段就存了原始地址,就可能和对不上。
3. 连接与GATT:把设备的“功能菜单”翻出来
3.1 GATT 结构先聊清楚
GATT(Generic Attribute Profile)是一个层级结构:设备(Device)→ 服务(Service)→ 特征(Characteristic)→ 描述符(Descriptor)。用一句话概括:服务是功能分类,特征是这个分类里具体的可读可写属性,描述符是特征的辅助配置。
生活化类比:把一个 BLE 设备看成一家餐厅。设备是餐厅本身,服务是“饮料区”“主食区”“甜品区”,特征是“可乐”“汉堡”“蛋糕”,描述符则是“这个蛋糕是否允许被改成现做”的配置项。你要喝可乐,得先找到饮料区,再找到可乐这个条目,然后才能下单。
GATT 里所有服务、特征都有 UUID 标识。16 位 UUID 是蓝牙 SIG 定义的标准属性,比如 Battery Service 是 0x180F,Device Information 是 0x180A;自定义的 128 位 UUID 通常由厂商自己定义,格式类似{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}。
Windows 的 API 里,GattDeviceService::Uuid()和GattCharacteristic::Uuid()返回的是一个winrt::guid,打印格式需要用winrt::to_hstring转一下。16 位 UUID 在 API 里会变成 0000xxxx-0000-1000-8000-00805f9b34fb 这种完整 GUID 形式,这是蓝牙 SIG 的基 UUID,别觉得是自己代码错了。
3.2 从地址到设备对象
扫描之后,拿到设备的BluetoothAddress(以及地址类型),接下来要获得BluetoothLEDevice对象:
#include <winrt/Windows.Devices.Bluetooth.h> using namespace Windows::Devices::Bluetooth; uint64_t targetAddress = 0xXXXXXXXXXXXX; BluetoothLEDevice device = co_await BluetoothLEDevice::FromBluetoothAddressAsync(targetAddress);如果你不熟悉co_await,需要开启 C++20 的协程支持。C++/WinRT 的异步操作默认要以协程方式等待,这是初学者最容易卡住的地方。C++17 模式下可以用get()同步等待:
auto deviceOp = BluetoothLEDevice::FromBluetoothAddressAsync(targetAddress); auto device = deviceOp.get();FromBluetoothAddressAsync本质上是一个连接操作。这个 API 返回后,不保证物理链路已经建立,但设备对象已经可用,后续的 GATT 操作会触发实际的连接流程。
3.3 枚举服务和特征的代码骨架
连接后最重要的事是枚举 GATT 服务,找出你要操作的特征。下面这段代码把服务的 UUID 和它下面的所有特征 UUID 都打出来:
#include <winrt/Windows.Devices.Bluetooth.GenericAttributeProfile.h> using namespace Windows::Devices::Bluetooth::GenericAttributeProfile; void DumpServices(BluetoothLEDevice const& device) { auto servicesResult = device.GetGattServicesAsync().get(); for (auto const& service : servicesResult.Services()) { std::wcout << L"Service: " << std::wstring(service.Uuid()) << std::endl; auto charsResult = service.GetCharacteristicsAsync().get(); for (auto const& characteristic : charsResult.Characteristics()) { std::wcout << L" Char: " << std::wstring(characteristic.Uuid()) << std::endl; } } }这里有个隐藏的属性访问器问题:device.GetGattServicesAsync()返回的GattDeviceServicesResult里Services()是一个向量,但当设备尚未完全准备好时可能会是空的。实际使用中可以先判断servicesResult.Status()是否为GattCommunicationStatus::Success,再处理服务列表。
3.4 访问权限和被缓存的坑
调用GetGattServicesAsync()之前,在 UWP 应用中有一个常见的RequestAccessAsync()步骤,用于请求访问蓝牙设备的权限。桌面程序里这个调用不是强制必须的,但如果你发现设备对象拿不到任何服务,建议还是在文档里确认当前 Windows 版本的权限策略。
另外一个很毒的坑:Windows 会缓存 GATT 服务发现结果。设备端改了服务结构或特征 UUID,应用重新连接后拿到的还是旧缓存。解决办法是让用户去系统设置里删除该设备的配对记录,再重新配对;或者设备端通过改变广播名、匹配码等方式强制系统重新发现。3 开发阶段我经常改服务端代码,这个缓存问题浪费了我不少时间。
4. 特征值读写与通知订阅:真正收发数据的地方
4.1 读特征值:ReadValueAsync
拿到GattCharacteristic后,读操作非常简单:
auto readResult = characteristic.ReadValueAsync().get(); if (readResult.Status() == GattCommunicationStatus::Success) { auto buffer = readResult.Value(); // 把 IBuffer 转成字节数组 auto reader = Windows::Storage::Streams::DataReader::FromBuffer(buffer); uint8_t byte = reader.ReadByte(); }ReadValueAsync适用于读那些静态或低频变化的数据,比如电池电量、设备序列号。如果你的设备需要频繁推送数据,别用轮询读,走通知订阅。
4.2 写特征值:WriteWithResponse 与 WriteWithoutResponse
写特征是 BLE 控制指令的下发通道。注意区分两种写类型:
- WriteWithResponse:设备收到数据后会回复 ACK,应用可以确认这次写入是否成功,适合关键控制指令。
- WriteWithoutResponse:只发数据不等待确认,吞吐率更高,适合大数据流。
C++/WinRT 的写操作:
Windows::Storage::Streams::DataWriter writer; writer.WriteBytes(std::vector<uint8_t>{0x01, 0x02, 0x03}); auto status = characteristic.WriteValueAsync(writer.DetachBuffer(), GattWriteOption::WriteWithResponse).get(); if (status == GattCommunicationStatus::Success) { // 写入成功 }需要说清楚的是,写类型并不完全由你决定,每个特征在服务端定义时就指定了属性(读、写、写无应答等)。你可以看characteristic.CharacteristicProperties()里是否包含GattCharacteristicProperties::WriteWithoutResponse,再决定用哪种方式写。如果特征本身只支持 WriteWithResponse,你强行用 WriteWithoutResponse 会失败。
4.3 通知订阅:从“轮询”到“推送”
BLE 最有价值的地方就是 Notify/Indicate 机制:设备端数据变化时主动上报,应用不用反复读。订阅步骤如下:
第一步,给特征的ValueChanged事件挂接回调:
characteristic.ValueChanged([](GattCharacteristic const& c, GattValueChangedEventArgs const& args) { auto reader = Windows::Storage::Streams::DataReader::FromBuffer(args.CharacteristicValue()); // 读数据,注意这里也是线程池回调 });第二步,开启 CCCD(Client Characteristic Configuration Descriptor)。这一步经常被忽略,因为很多新手以为挂上ValueChanged事件就会自动收到数据。实际上,服务端是否上报通知,取决于客户端有没有写 CCCD 这个描述符:
auto cccdStatus = characteristic.WriteClientCharacteristicConfigurationDescriptorAsync( GattClientCharacteristicConfigurationDescriptorValue::Notify ).get();只有这个调用返回 Success 后,设备端才会开始往你这推数据。关闭通知时写GattClientCharacteristicConfigurationDescriptorValue::None。
4.4 分包和 MTU 的问题
默认情况下,BLE 一个 ATT 数据包能承载的用户数据只有 20 字节(因为 MTU 默认 23 字节,减去 ATT 头等开销)。很多新手写完一个WriteValueAsync传了 100 字节数据发现只收到 20 字节,就开始怀疑代码有问题。这就是 MTU 没协商导致的。
Windows 上如果你没有做任何 MTU 协商动作,实际写入大量数据时系统可能直接报错,也可能只发送了部分。建议开发阶段先查一下特征支持的写长度,GattCharacteristic没有直接的 “max write length” 属性,但你可以写一个小的探测包来确认。真正的 MTU 协商放到后面专门讲。
5. 配对邦定与MTU协商:最容易翻车的两座山
5.1 Pairing 和 Bonding 到底在什么场景需要
先澄清两个概念:Pairing(配对)是临时建立安全连接的过程;Bonding(邦定)是在配对基础上交换并保存长期密钥,之后重连不需要重新配对。热度词里“ble调试助手绑定(bond)”应该就是这个场景。
很多 BLE 外设不需要配对,比如普通的温度传感器、ibeacon,广播直接读就行。但有些设备设计时要求加密连接,比如智能锁、数字钥匙、心率带,不配对就不让你访问 GATT 服务。Windows 上的表现是:你在GetGattServicesAsync或ReadValueAsync时返回GattCommunicationStatus::AccessDenied。
5.2 Windows 上怎么用 C++ 触发配对
UWP/WinRT 里配对相关的 API 是DeviceInformation.Pairing.CustomPairing。你需要先从设备对象的DeviceInformation拿到配对信息,然后调用PairAsync:
#include <winrt/Windows.Devices.Enumeration.h> using namespace Windows::Devices::Enumeration; auto deviceInfo = device.DeviceInformation(); auto pairingKinds = DevicePairingKinds::ConfirmOnly; auto result = deviceInfo.Pairing().Custom().PairAsync(pairingKinds).get(); if (result.Status() == DevicePairingResultStatus::Paired) { // 配对成功 }DevicePairingKinds有几种常用值:ConfirmOnly(用户点确认即可)、ProvidePin(需要输入 PIN 码显示在设备端)、ConfirmPinMatch(两端显示同一个 PIN,用户确认匹配)。实际开发中,如果你的设备是那种没有屏幕的传感器,通常是 Just Works 方式配对,用ConfirmOnly就行;如果设备有小屏幕显示 6 位数字,那就用ProvidePin,在事件回调里把 PIN 传进去。
ProvidePin藏在事件里,示例代码略复杂,核心是挂接PairingRequested事件,在里面根据DevicePairingRequestedEventArgs类型返回 PIN。这部分如果你第一次写,很容易找不到ProvidePin的赋值位置——它不在PairAsync参数里,而在事件回调里调用args.Accept(pin)。
5.3 配对影响全局:这个坑请务必重视
Windows 的配对操作是系统级的状态,不是应用级。你用自己程序配对的设备,会被记录在 Windows 的蓝牙设备列表里,之后你系统里的其他程序也能看到它。反过来,如果你的程序在BluetoothLEDevice::FromBluetoothAddressAsync时系统已经存了这个设备的配对信息,它会自动尝试重新邦定。
有几次我在调试过程中改了设备端的安全参数,导致 Windows 这边旧密钥失效,应用层报错。这时候最有效的办法不是改代码,而是去系统的蓝牙设置里删掉这个设备,重新配对。所以我调试反复修改安全性相关的代码时,会先手动清理配对记录。
5.4 MTU 协商的正确理解
MTU(Maximum Transmission Unit)指的是 ATT 层最大传输单元。BLE 4.0 默认 MTU 是 23 字节,其中 3 字节是 ATT 头,所以单个数据包最多带 20 字节用户数据。要提高吞吐,需要客户端和服务端协商一个更大的 MTU。
Windows 上系统在连接建立后会自动和远端协商 MTU,你可以在GattSession对象里读取协商结果:
#include <winrt/Windows.Devices.Bluetooth.GenericAttributeProfile.h> auto session = GattSession::FromDeviceIdAsync(device.BluetoothDeviceId()).get(); uint16_t maxPduSize = session.MaxPduSize();这个MaxPduSize()就是实际能承载的 ATT PDU 大小,减去 3 个字节 ATT 头,才是你真正能写的数据长度。比如MaxPduSize() == 247,那单包用户数据最多 244 字节。
重点来了:UWP/WinRT 这套公开 API 没有提供主动请求特定 MTU 值的接口。MTU 协商由系统自动处理,你只能读结果。如果设备端和系统协商后 MTU 没达到你预期,能做的就是改服务端配置(比如 ESP32 里把ATT_MTU默认值调大),或者走到 HCI 层自己发LE Configure MTU命令——那是另一个深水区,一般应用不值得做。
测试时用 ESP32 这类开发板很直观:ESP-IDF 里默认 MTU 是 517 或 247,你可以用esp_ble_gatt_set_mtu设置。Windows 连接后,GattSession::MaxPduSize()读到的值应该和 ESP32 协商值一致。不一致的话检查一下设备端是否真的在广播阶段就允许更高 MTU。
6. 抓包调试与跨平台移植的实用建议
6.1 没有逻辑分析仪,用这套组合也能调
做 BLE 开发,光看自己代码日志不够,最好能看到空中报文。最便宜的办法是装一个 BLE 抓包工具配合 Windows 的 HCI 日志:
- Wireshark 安装时勾选 “Microsoft Bluetooth HCI Extensible Sniffer”,然后用管理员权限运行
msbtsniffer,就能抓到经过 Windows 协议栈的 BLE HCI 包。 - 如果 Wireshark 抓不到,常见原因是微软的抓包驱动和你的蓝牙适配器不兼容。Intel 的无线网卡普遍支持,Realtek 的有些勉强。
手机端用 Nordic 的 nRF Connect 或者厂商的“BLE 调试助手”,是验证设备行为最快的途径。开发时我常用思路是:先用手机 App 确认设备端广播、服务和特征读写一切正常,再用 Windows 端的程序去连,这样把问题边界划清楚——是设备端问题还是 Windows API 调用问题,一测便知。
6.2 用 ESP32 做冒烟测试设备
如果你手头没有现成的 BLE 外设,建议直接拿一块 ESP32 开发板,用 ESP-IDF 或 Arduino 写一个简单的 GATT Server,广播一个自定义服务和特征。原因是 ESP32 资料多、成本低、改起来方便,可以随时调整广播名、服务 UUID、MTU 大小来模拟各种异常情况。
我做过的冒烟测试设备长这样:广播名TEST_BLE,一个自定义服务{0000ffe0-0000-1000-8000-00805f9b34fb},里面一个可读写特征{0000ffe1-0000-1000-8000-00805f9b34fb},每 1 秒通过 Notify 推一个递增计数。Windows 端程序连接后能扫描到、能读写、能收通知,整套流程就算验证通过。
6.3 跨平台移植的提前思考
如果你的产品后面可能跑 Linux 或移动端,建议别把 C++/WinRT 的 API 调用散落在业务代码里,而是封装一个薄薄的一层抽象接口,比如IBleScanner、IBleGattClient。这样 Windows 上实现用 WinRT,Linux 上实现用 BlueZ 的 D-Bus API(或者直接上 SimpleBLE 这类跨平台库),业务层不动。
SimpleBLE 这个库我个人比较推荐看源码学思路,它内部对 Windows 底层就是封装 WinRT,对 Linux 封装 BlueZ,对 macOS 封装 CoreBluetooth。它的设计简洁,但功能覆盖有限,复杂应用还是得自己封装。如果项目一开始就确定要跨 Windows/Linux,直接用 SimpleBLE 起步能省不少时间。
6.4 调试阶段值得投入的一次性准备工作
再分享一个经验:先把日志系统做好,再做协议联调。BLE 开发的特点是异步事件多——扫描回调、连接状态变化、通知回调,全在不同的线程上跑,日志如果没带时间戳和线程 ID,出问题根本没法定位。
我自己的日志格式至少包含三要素:时间戳(毫秒级)、线程 ID、日志级别。连接状态变化时打一条:“connect start / connected / disconnected / access denied”,扫描到设备时打地址和 RSSI,收到通知时打长度和前几个字节。这样排查问题基本靠日志就能还原现场,而不是靠猜。
写完这篇,回头再看
C/C++ 做 Windows BLE 开发这条路没有想象中那么冷门,但确实没有一条官方给出的“傻瓜式”路径。只要把技术栈选对,坚持用 Windows.Devices.Bluetooth + C++/WinRT,剩下的就是从扫描、连接、GATT 到配对一步步打通。我最初被卡得最惨的地方是不知道 Win32 程序也能直接调 WinRT API,以及 MTU 协商不受应用层控制这两个认知盲区,希望这篇文章能把这两座山替你提前铲平。
本文还有配套的精品资源,点击获取