Windows平台BLE蓝牙程序开发实战:从扫描到数据通信

张开发
2026/4/14 9:15:33 15 分钟阅读

分享文章

Windows平台BLE蓝牙程序开发实战:从扫描到数据通信
1. Windows平台BLE开发环境搭建开发Windows平台的BLE蓝牙应用首先需要确保你的开发环境准备就绪。我推荐使用Visual Studio 2022作为开发工具它提供了完整的Windows应用开发支持。安装时记得勾选使用C的桌面开发和通用Windows平台开发这两个工作负载它们包含了我们需要的蓝牙开发组件。在项目中我们需要引用Windows SDK Contracts这个关键库。这个库封装了Windows系统底层的蓝牙API让我们能够方便地调用各种蓝牙功能。具体操作是在解决方案资源管理器中右键点击引用选择添加引用然后找到Windows SDK Contracts并勾选。这个步骤看似简单但很多新手容易忽略导致后续代码编译报错。除了主库之外我们还需要引入几个关键的命名空间using Windows.Devices.Bluetooth; using Windows.Devices.Bluetooth.Advertisement; using Windows.Devices.Bluetooth.GenericAttributeProfile; using Windows.Devices.Enumeration;在实际项目中我发现很多开发者会遇到权限问题。Windows应用要使用蓝牙功能必须在Package.appxmanifest文件中声明蓝牙权限。打开这个文件切换到功能选项卡勾选蓝牙选项。如果不做这一步应用在运行时就会抛出权限异常这点我当初也踩过坑。2. BLE设备扫描与发现2.1 配置蓝牙扫描器蓝牙设备扫描是整个开发流程的第一步也是基础中的基础。Windows提供了BluetoothLEAdvertisementWatcher类来实现这个功能。在我的项目中通常会这样初始化扫描器deviceWatcher new BluetoothLEAdvertisementWatcher(); deviceWatcher.ScanningMode BluetoothLEScanningMode.Active; deviceWatcher.SignalStrengthFilter.InRangeThresholdInDBm -80; deviceWatcher.SignalStrengthFilter.OutOfRangeThresholdInDBm -90; deviceWatcher.SignalStrengthFilter.OutOfRangeTimeout TimeSpan.FromMilliseconds(5000); deviceWatcher.SignalStrengthFilter.SamplingInterval TimeSpan.FromMilliseconds(2000);这里有几个关键参数需要注意ScanningMode设置为Active会主动扫描设备获取更多信息但耗电更高InRangeThresholdInDBm信号强度阈值只有信号强于-80dBm的设备才会被报告OutOfRangeTimeout设备超出范围后多久标记为不可用2.2 处理扫描结果扫描到设备后我们需要处理Received事件来获取设备信息。这里有个技巧不是所有发现的设备都需要立即连接我们可以根据设备名称或服务UUID进行过滤private void DeviceWatcher_Received(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args) { BluetoothLEDevice.FromBluetoothAddressAsync(args.BluetoothAddress).Completed async (asyncInfo, asyncStatus) { if (asyncStatus AsyncStatus.Completed) { BluetoothLEDevice currentDevice asyncInfo.GetResults(); if (currentDevice.Name?.ToUpper().StartsWith(FS) ?? false) { // 处理目标设备 } currentDevice.Dispose(); } }; }在实际项目中我发现及时释放不用的BluetoothLEDevice对象很重要否则会导致资源泄漏。另外设备名称可能为null所以要用null条件运算符?.和null合并运算符??来做安全判断。3. BLE设备连接与服务发现3.1 建立设备连接连接BLE设备是整个流程中最容易出问题的环节之一。我总结了一个稳定的连接流程public async Task ConnectAsync(string mac) { // 检查是否已连接 if (IsConnect mac CurrentDeviceMAC) return; // 断开现有连接 if (IsConnect) CloseConnect(); // 构建设备ID string Id $BluetoothLE#BluetoothLE{macAddress}-{mac}; CurrentDevice await BluetoothLEDevice.FromIdAsync(Id); // 设置连接状态变更回调 CurrentDevice.ConnectionStatusChanged CurrentDevice_ConnectionStatusChanged; }这里有几个经验分享连接前先检查是否已连接相同设备避免重复连接使用MAC地址构建设备ID比从扫描列表获取更可靠一定要注册连接状态变更回调处理意外断开情况3.2 发现服务与特征值成功连接设备后下一步是发现GATT服务和特征值。这个过程需要特别注意异步操作的处理CurrentDevice.GetGattServicesAsync().Completed async (asyncInfo, asyncStatus) { if (asyncStatus AsyncStatus.Completed) { var services asyncInfo.GetResults().Services; foreach (var service in services) { if (service.Uuid.ToString().StartsWith(6e400001)) { CurrentService service; await FindCharacteristic(); } } } };在实际开发中我发现服务发现有时会失败特别是当设备连接不稳定时。我的解决方案是加入重试机制最多尝试3次每次间隔500ms。另外用完的GattDeviceService对象要及时Dispose否则会导致资源泄漏。4. BLE数据通信实现4.1 数据接收处理设置特征值通知是接收BLE数据的关键步骤。这里有个细节需要注意必须在写入客户端配置描述符后才能收到通知CurrentNotifyCharacteristic.ValueChanged CurrentNotifyCharacteristic_ValueChanged; await CurrentNotifyCharacteristic.WriteClientCharacteristicConfigurationDescriptorAsync( GattClientCharacteristicConfigurationDescriptorValue.Notify);处理接收到的数据时要注意数据可能是分片的。我通常会实现一个简单的数据缓冲区根据协议头尾标识来组合完整的数据包private void CurrentNotifyCharacteristic_ValueChanged(GattCharacteristic sender, GattValueChangedEventArgs args) { byte[] data; CryptographicBuffer.CopyToByteArray(args.CharacteristicValue, out data); // 处理数据分片和组合 }4.2 数据发送实现发送数据到BLE设备看似简单但有很多细节需要注意。我的经验是数据分包发送每包不超过20字节BLE协议限制加入简单的流控机制避免发送过快导致丢包记录发送状态便于错误处理和重试public async Taskbool Write(byte[] data) { const int chunkSize 20; for (int i 0; i data.Length; i chunkSize) { int length Math.Min(chunkSize, data.Length - i); byte[] chunk new byte[length]; Array.Copy(data, i, chunk, 0, length); var status await CurrentWriteCharacteristic.WriteValueAsync( CryptographicBuffer.CreateFromByteArray(chunk), GattWriteOption.WriteWithoutResponse); if (status ! GattCommunicationStatus.Success) return false; await Task.Delay(10); // 简单流控 } return true; }在实际项目中我发现WriteWithResponse虽然可靠但速度慢而WriteWithoutResponse速度快但可能丢包。根据应用场景选择合适的写入方式很重要。对于关键数据我通常会实现一个简单的ACK确认机制。

更多文章