ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C#桌面端BLE开发实战:Windows.Devices.Bluetooth连接与数据收发

C#桌面端BLE开发实战:Windows.Devices.Bluetooth连接与数据收发 1. 为什么桌面端BLE开发值得单独拎出来讲很多人第一次接触蓝牙开发是在移动端Android有BluetoothGatt、iOS有CoreBluetooth文档齐全、社区活跃遇到问题一搜一大把。但换到Windows桌面端情况就完全不一样了。你可能已经习惯了C#写上位机、写WinForm/WPF工具突然有一天产品经理跑过来说“咱们这个设备要加个蓝牙连接功能”你打开Visual Studio一搜发现方案五花八门有老牌的32feet.NET、有InTheHand.Net.Bluetooth还有Windows 10之后微软官方推的Windows.Devices.Bluetooth。选哪个怎么选选完了怎么用这一连串问题足以让一个写了五六年C#的老手也犯迷糊。我写这篇文章的出发点很简单把我自己在实际项目中用Windows.Devices.Bluetooth做BLE设备连接和数据收发的完整经验整理出来。这套API是Windows Runtime的一部分从Windows 10开始正式提供好处是系统原生支持、不需要额外装驱动或第三方库、跟UWP和WPF/WinForm都能配合使用。坏处是它的文档偏“官方”很多实际开发中会遇到的坑——比如异步操作的线程调度、设备配对状态的处理、特征值读写的超时重试——文档里要么一笔带过要么压根不提。这篇文章适合谁看如果你已经会用C#写基本的桌面应用对async/await不陌生但对BLE协议栈和Windows.Devices.Bluetooth这套API还比较陌生那这篇内容就是给你准备的。我会从BLE的基本概念讲起然后一步步拆解设备发现、连接、服务枚举、特征值读写、通知订阅的完整流程最后附上可以直接跑的代码和我在实际项目中踩过的坑。代码基于.NET 6 WPF环境验证WinForm项目同样适用核心逻辑完全一致。注意Windows.Devices.Bluetooth要求Windows 10版本1607及以上开发时项目需要引用Windows SDK的WinRT API。在.NET Framework项目中需要额外引用Windows.winmd而.NET 5/6/7/8项目则通过TargetFramework指定net6.0-windows10.0.19041.0这样的格式来获得支持。2. BLE核心概念快速扫盲2.1 BLE协议栈的层次结构BLE的全称是Bluetooth Low Energy从蓝牙4.0开始引入跟经典蓝牙Classic Bluetooth是两套不同的协议栈。经典蓝牙主要用于音频传输、文件传输这类高带宽场景而BLE主打低功耗、小数据量、间歇性通信非常适合传感器数据采集、智能家居控制、穿戴设备这类应用。从协议栈的角度看BLE分为三层控制器层、主机层和应用层。控制器层包括物理层PHY和链路层LL负责射频收发和链路管理主机层包括L2CAP、ATT、GATT、SMP等协议负责逻辑链路控制和数据封装应用层就是我们开发者直接打交道的GATT客户端/服务端。Windows.Devices.Bluetooth这套API主要工作在主机层和应用层它把底层的HCI交互全部封装掉了我们只需要关注GATT层面的操作。理解这个层次结构很重要因为它决定了你能做什么、不能做什么。比如你想修改BLE的广播间隔或者发射功率Windows.Devices.Bluetooth是做不到的那些参数在控制器层需要更底层的接口。但如果你只是要连接设备、读写特征值、订阅通知这套API完全够用。2.2 GATT的角色划分与数据模型GATTGeneric Attribute Profile是BLE数据交互的核心。它定义了一个层次化的数据模型最顶层是Profile往下是Service再往下是Characteristic最底层是Descriptor。每个Service、Characteristic和Descriptor都有一个UUID来标识UUID可以是16位的标准UUID或128位的自定义UUID。在GATT模型中有两个角色GATT Server服务端和GATT Client客户端。服务端持有数据客户端发起读写请求。在Windows.Devices.Bluetooth的场景下我们的C#程序通常扮演GATT Client的角色去连接外设比如一个BLE温度计读取它的温度数据。但反过来Windows也可以作为GATT Server让其他设备来连接我们这个后面会简单提一下。每个Characteristic有两个关键属性Properties和Value。Properties定义了该特征值支持哪些操作比如Read、Write、WriteWithoutResponse、Notify、Indicate。Value就是实际的数据类型是字节数组。Descriptor则是对Characteristic的补充描述比如Client Characteristic Configuration DescriptorCCCD用来控制通知的开关。2.3 Windows.Devices.Bluetooth的API全景Windows.Devices.Bluetooth命名空间下有几个核心类我列一个表方便你快速定位类名用途BluetoothLEDevice代表一个BLE设备是连接的入口BluetoothLEAdvertisementWatcher扫描BLE广播用于发现设备GattDeviceService代表设备上的一个GATT服务GattCharacteristic代表服务下的一个特征值GattDescriptor代表特征值的描述符GattSession管理GATT会话控制连接保持BluetoothLEAdvertisement广播数据包包含设备名称、厂商数据等这些类之间的关系是先用BluetoothLEAdvertisementWatcher扫描到设备拿到BluetoothAddress然后通过BluetoothLEDevice.FromBluetoothAddressAsync获取BluetoothLEDevice对象再通过GetGattServicesAsync获取服务列表接着通过GetCharacteristicsAsync获取特征值列表最后对特征值进行读写或订阅通知。整个流程是异步的而且每一步都可能失败——设备可能已经断开、服务可能不存在、特征值可能不支持你要的操作。所以异常处理和超时控制是必须的后面会详细讲。3. 开发环境搭建与项目配置3.1 项目类型与目标框架选择Windows.Devices.Bluetooth是WinRT API这意味着它对项目类型和目标框架有要求。如果你用的是WPF或WinForm目标框架需要设置为net6.0-windows10.0.19041.0或更高版本。这个TargetFramework的写法看起来有点怪但它是.NET 5之后引入的“Windows TFM”机制用来指定最低Windows版本。具体操作是在.csproj文件里这样写Project SdkMicrosoft.NET.Sdk PropertyGroup OutputTypeWinExe/OutputType TargetFrameworknet6.0-windows10.0.19041.0/TargetFramework UseWPFtrue/UseWPF SupportedOSPlatformVersion10.0.17763.0/SupportedOSPlatformVersion /PropertyGroup /Project这里的10.0.19041.0是Windows 10 2004的版本号17763是Windows 10 1809。SupportedOSPlatformVersion可以比TargetFramework低表示你允许应用在更低版本上运行但编译时用的API集是19041的。如果你不确定用户环境建议把SupportedOSPlatformVersion设到17763这个版本覆盖了绝大多数还在用的Windows 10系统。提示如果你用的是.NET Framework 4.6.1以上版本也可以使用这套API但需要手动添加对Windows.winmd的引用路径通常在C:\Program Files (x86)\Windows Kits\10\UnionMetadata\下面。这种方式比较麻烦建议尽量升级到.NET 6。3.2 必要的NuGet包和引用在.NET 6的Windows TFM下Windows.Devices.Bluetooth是自动可用的不需要额外装NuGet包。但如果你需要用到一些辅助功能比如异步操作的扩展方法可以装一个System.Threading.Tasks.Extensions。另外如果你要在WPF中绑定设备列表可能还需要Microsoft.Xaml.Behaviors.Wpf来处理事件绑定。有一个容易忽略的点在WPF项目中WinRT API的异步操作返回的是IAsyncOperation 它跟标准的Task 不完全一样。虽然.NET 6提供了扩展方法可以把IAsyncOperation转成Task但在某些场景下比如需要取消操作时直接使用IAsyncOperation的AsTask方法会更灵活。我个人的习惯是统一用.AsTask()转换这样代码风格一致异常处理也统一。3.3 权限声明与清单配置在UWP项目中你需要在Package.appxmanifest里声明蓝牙权限。但在WPF/WinForm项目中没有清单文件的概念权限是隐式授予的——只要用户账户有蓝牙访问权限API就能用。不过有一个例外如果你的应用需要后台访问蓝牙设备或者需要访问受保护的设备可能需要在注册表中做额外配置。大多数场景下不需要。还有一个实际开发中经常遇到的问题Windows的蓝牙开关。如果用户关闭了系统蓝牙API调用会直接抛异常。所以在程序启动时最好先检查蓝牙适配器的状态。可以用BluetoothAdapter.GetDefaultAsync()来获取默认适配器如果返回null说明系统没有蓝牙适配器或者蓝牙被关闭了。var adapter await BluetoothAdapter.GetDefaultAsync(); if (adapter null) { // 提示用户检查蓝牙适配器 return; } if (!adapter.IsLowEnergySupported) { // 提示用户该适配器不支持BLE return; }这段代码看起来简单但实际项目中我见过太多人忘了加结果用户一运行就崩溃报一个莫名其妙的COM异常。4. BLE设备发现与连接全流程4.1 使用BluetoothLEAdvertisementWatcher扫描设备设备发现是BLE通信的第一步。Windows.Devices.Bluetooth提供了BluetoothLEAdvertisementWatcher类来扫描周围的BLE广播。这个类的工作方式是你设置一个扫描间隔和扫描窗口系统会在后台持续扫描每收到一个广播包就触发Received事件。var watcher new BluetoothLEAdvertisementWatcher { ScanningMode BluetoothLEScanningMode.Active }; watcher.Received OnAdvertisementReceived; watcher.Stopped OnWatcherStopped; watcher.Start();ScanningMode有两个选项Active和Passive。Active模式会主动发送扫描请求获取更多的广播数据比如设备名称但功耗更高Passive模式只被动接收广播包不发送任何请求。对于大多数应用场景Active模式更合适因为你能拿到更完整的设备信息。Received事件的参数是BluetoothLEAdvertisementReceivedEventArgs里面包含BluetoothAddress、Advertisement、RawSignalStrengthInDBm等信息。BluetoothAddress是一个ulong类型的值它是设备的唯一标识后续连接设备时需要用到。private void OnAdvertisementReceived( BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args) { var address args.BluetoothAddress; var name args.Advertisement.LocalName; var rssi args.RawSignalStrengthInDBm; // 在UI线程上更新设备列表 Dispatcher.Invoke(() { // 去重并添加到列表 }); }这里有一个关键点Received事件是在后台线程触发的如果你要更新UI必须通过Dispatcher.Invoke切回UI线程。我见过不少人在这个事件里直接操作ObservableCollection结果程序随机崩溃排查半天才发现是线程问题。注意扫描到的设备可能会重复出现同一个设备在几秒钟内可能触发多次Received事件。所以在更新设备列表时需要按BluetoothAddress去重。另外有些设备不广播名称LocalName会是空字符串这时候你可以尝试从Advertisement.DataSections里解析厂商自定义数据来识别设备。4.2 从BluetoothAddress到BluetoothLEDevice扫描到设备后下一步是获取BluetoothLEDevice对象。有两种方式一种是用BluetoothLEDevice.FromBluetoothAddressAsync(ulong address)另一种是用BluetoothLEDevice.FromIdAsync(string deviceId)。前者更直接后者需要你先通过其他方式拿到DeviceInformation的Id。var device await BluetoothLEDevice.FromBluetoothAddressAsync(address); if (device null) { // 设备不可用可能已经离开范围 return; }这里有一个非常重要的细节FromBluetoothAddressAsync返回null并不一定意味着设备不在范围内也可能是系统缓存的问题。Windows会缓存已配对设备的信息有时候即使设备已经断开FromBluetoothAddressAsync仍然会返回一个对象但后续操作会失败。所以拿到BluetoothLEDevice对象后最好检查一下ConnectionStatus属性。if (device.ConnectionStatus BluetoothConnectionStatus.Disconnected) { // 尝试连接 }但ConnectionStatus也不是实时的它反映的是上一次系统更新的状态。真正要建立连接还是得通过GATT操作来触发。当你第一次调用GetGattServicesAsync时系统会尝试建立物理连接如果成功ConnectionStatus会变成Connected。4.3 连接状态管理与自动重连BLE连接不是一劳永逸的。设备可能因为距离过远、电量不足、干扰等原因断开连接。Windows.Devices.Bluetooth提供了ConnectionStatusChanged事件来通知连接状态变化device.ConnectionStatusChanged OnConnectionStatusChanged; private void OnConnectionStatusChanged(BluetoothLEDevice sender, object args) { if (sender.ConnectionStatus BluetoothConnectionStatus.Disconnected) { // 触发重连逻辑 } }但这里有个坑ConnectionStatusChanged事件的触发时机并不总是可靠的。有时候设备已经断开了但事件过了好几秒才触发有时候事件触发了但ConnectionStatus的值还是Connected。所以我的做法是不依赖这个事件做实时判断而是在每次读写操作时检查返回值如果失败就触发重连。重连的逻辑也很讲究。不能一断开就立刻重连因为如果设备真的不在范围内频繁重连会消耗大量资源。我的做法是采用指数退避策略第一次断开后等1秒重连失败后等2秒再失败等4秒最多等30秒。同时设置一个最大重连次数超过后就放弃并通知用户。private async Task ReconnectWithBackoff(ulong address) { int delay 1000; for (int i 0; i 5; i) { await Task.Delay(delay); var device await BluetoothLEDevice.FromBluetoothAddressAsync(address); if (device ! null) { var services await device.GetGattServicesAsync(); if (services.Status GattCommunicationStatus.Success) { // 重连成功 return; } } delay * 2; } // 重连失败通知用户 }4.4 配对与未配对设备的处理BLE设备分为已配对和未配对两种。已配对设备在系统中保存了配对信息连接时不需要重新配对未配对设备可以直接连接并读写那些不需要加密的特征值但如果特征值要求加密就必须先配对。在Windows.Devices.Bluetooth中配对是通过device.DeviceInformation.Pairing.PairAsync()来完成的。但配对操作会弹出一个系统对话框需要用户确认。这在自动化场景下很不方便所以如果你的设备不需要加密尽量在固件设计时就避免要求配对。var pairingResult await device.DeviceInformation.Pairing.PairAsync( DevicePairingProtectionLevel.Encryption); if (pairingResult.Status DevicePairingResultStatus.Paired) { // 配对成功 }提示PairAsync有多个重载可以指定保护级别。对于BLE设备通常用Encryption就够了不需要用EncryptionAndAuthentication后者会要求MITM保护交互更复杂。5. GATT服务与特征值操作详解5.1 枚举GATT服务与特征值拿到BluetoothLEDevice对象后下一步是枚举它提供的GATT服务。用GetGattServicesAsync方法var servicesResult await device.GetGattServicesAsync( BluetoothCacheMode.Uncached); if (servicesResult.Status ! GattCommunicationStatus.Success) { // 枚举失败 return; } foreach (var service in servicesResult.Services) { var uuid service.Uuid; // 根据UUID判断是不是你要的服务 }这里有一个关键参数BluetoothCacheMode。它有两个值Cached和Uncached。Cached表示从系统缓存中读取服务列表速度快但可能不是最新的Uncached表示强制从设备重新读取速度慢但准确。我的建议是第一次连接时用Uncached确保拿到完整的服务列表后续操作可以用Cached提高速度。拿到服务后继续枚举特征值var characteristicsResult await service.GetCharacteristicsAsync( BluetoothCacheMode.Uncached); foreach (var characteristic in characteristicsResult.Characteristics) { var uuid characteristic.CharacteristicUuid; var props characteristic.CharacteristicProperties; // 根据UUID和属性判断用途 }CharacteristicProperties是一个枚举包含Read、Write、WriteWithoutResponse、Notify、Indicate等值。你可以用HasFlag方法来判断某个特征值是否支持特定操作。5.2 读取特征值数据读取特征值用ReadValueAsync方法var readResult await characteristic.ReadValueAsync( BluetoothCacheMode.Uncached); if (readResult.Status GattCommunicationStatus.Success) { var reader DataReader.FromBuffer(readResult.Value); byte[] data new byte[readResult.Value.Length]; reader.ReadBytes(data); // 处理数据 }这里用到了DataReader它是WinRT中处理二进制数据的工具类。readResult.Value是一个IBuffer不能直接转成byte数组必须通过DataReader来读。这是WinRT API的一个特点跟.NET的Stream不太一样。读取操作有几个注意事项第一如果特征值不支持Read属性ReadValueAsync会返回AccessDenied第二读取的数据长度不能超过特征值的MTU限制默认MTU是23字节实际可用数据是20字节左右第三读取操作可能超时默认超时时间是几秒钟如果设备响应慢可能会失败。5.3 写入特征值数据写入特征值用WriteValueAsync方法var writer new DataWriter(); writer.WriteBytes(new byte[] { 0x01, 0x02, 0x03 }); var writeResult await characteristic.WriteValueAsync( writer.DetachBuffer(), GattWriteOption.WriteWithResponse);GattWriteOption有两个值WriteWithResponse和WriteWithoutResponse。前者会等待设备确认可靠性高但速度慢后者不等待确认速度快但可能丢包。选择哪个取决于你的应用场景如果是控制指令用WriteWithResponse如果是高频传感器数据用WriteWithoutResponse。写入操作也有MTU限制。如果数据超过MTU需要分段写入。Windows.Devices.Bluetooth没有自动分段的功能需要你自己实现。我的做法是先通过GattSession的MaxPduSize属性获取当前MTU然后按MTU大小分段写入。var session await GattSession.FromDeviceIdAsync(device.DeviceId); var mtu session.MaxPduSize; // 按mtu分段5.4 订阅通知与指示BLE的通知机制是设备在数据变化时主动推送给客户端。要接收通知需要先向CCCD写入0x0001通知或0x0002指示然后监听ValueChanged事件。var cccd await characteristic.GetDescriptorsAsync(); var notifyDescriptor cccd.Descriptors.FirstOrDefault( d d.Uuid GattDescriptorUuids.ClientCharacteristicConfiguration); var writer new DataWriter(); writer.WriteBytes(new byte[] { 0x01, 0x00 }); await notifyDescriptor.WriteValueAsync(writer.DetachBuffer()); characteristic.ValueChanged OnCharacteristicValueChanged;ValueChanged事件的参数是GattValueChangedEventArgs里面包含CharacteristicValueIBuffer和Timestamp。同样需要用DataReader来读取数据。注意ValueChanged事件是在后台线程触发的更新UI时需要Dispatcher.Invoke。另外通知的频率可能很高如果处理逻辑耗时较长建议用队列缓冲避免阻塞事件处理线程。6. 实战中的坑与排查技巧6.1 常见异常与错误码速查在实际开发中我遇到过各种各样的异常。下面整理一个速查表异常/错误码原因解决方法Unreachable设备不在范围内或已断开检查设备状态触发重连AccessDenied特征值不支持该操作或需要配对检查CharacteristicProperties必要时配对ProtocolError数据格式错误或MTU超限检查数据长度分段写入Timeout设备响应超时增加重试逻辑检查信号强度ResourceInUse设备已被其他程序占用关闭其他占用程序或重启蓝牙服务6.2 线程调度与UI更新前面反复提到线程问题这里再强调一次。Windows.Devices.Bluetooth的所有异步方法在await之后默认会回到调用时的上下文。如果你在UI线程调用await之后还在UI线程如果你在后台线程调用await之后还在后台线程。但事件回调Received、ValueChanged、ConnectionStatusChanged是在后台线程触发的不受这个规则影响。所以我的建议是所有事件回调里第一件事就是Dispatcher.Invoke切回UI线程然后再做后续处理。如果处理逻辑比较耗时可以先把数据拷贝出来再在后台线程处理处理完再切回UI线程更新界面。6.3 设备缓存与连接失败的处理Windows会缓存BLE设备的服务信息有时候设备固件更新了服务列表变了但系统缓存还是旧的。这时候用Cached模式枚举服务会拿到错误的结果。解决方法是用Uncached模式强制刷新或者调用device.DeviceInformation.Pairing.UnpairAsync()清除配对信息后重新配对。另一个常见问题是连接失败。有时候FromBluetoothAddressAsync返回了对象但GetGattServicesAsync一直失败。这通常是因为设备已经被其他程序连接了或者设备的广播已经停止但系统缓存还在。我的做法是先检查ConnectionStatus如果是Disconnected尝试用Uncached模式枚举服务如果还是失败就等几秒后重试。6.4 性能优化与功耗控制BLE的核心优势是低功耗但如果使用不当反而会很耗电。几个优化点第一扫描时设置合适的ScanningMode不需要Active模式时就用Passive第二扫描到目标设备后立即停止扫描不要一直开着第三通知订阅只在需要时开启不需要时及时关闭第四合理设置连接间隔Windows.Devices.Bluetooth没有直接暴露连接间隔参数但可以通过GattSession的MaintainConnection属性来控制是否保持连接。var session await GattSession.FromDeviceIdAsync(device.DeviceId); session.MaintainConnection true; // 保持连接 // 不需要时设为false7. 完整代码示例与项目结构7.1 核心类设计我把整个BLE操作封装成了一个BleDeviceManager类对外暴露Connect、Disconnect、ReadCharacteristic、WriteCharacteristic、SubscribeNotification等方法。内部维护设备对象、服务列表、特征值字典和重连状态。public class BleDeviceManager { private BluetoothLEDevice _device; private GattDeviceService _targetService; private GattCharacteristic _targetCharacteristic; private BluetoothLEAdvertisementWatcher _watcher; public event EventHandlerbyte[] DataReceived; public event EventHandlerbool ConnectionStatusChanged; public async Taskbool ConnectAsync(ulong address) { _device await BluetoothLEDevice.FromBluetoothAddressAsync(address); if (_device null) return false; _device.ConnectionStatusChanged OnConnectionStatusChanged; var services await _device.GetGattServicesAsync( BluetoothCacheMode.Uncached); if (services.Status ! GattCommunicationStatus.Success) return false; // 查找目标服务 _targetService services.Services.FirstOrDefault( s s.Uuid TargetServiceUuid); if (_targetService null) return false; var chars await _targetService.GetCharacteristicsAsync( BluetoothCacheMode.Uncached); _targetCharacteristic chars.Characteristics.FirstOrDefault( c c.CharacteristicUuid TargetCharacteristicUuid); return _targetCharacteristic ! null; } public async Taskbyte[] ReadAsync() { var result await _targetCharacteristic.ReadValueAsync( BluetoothCacheMode.Uncached); if (result.Status ! GattCommunicationStatus.Success) return null; var reader DataReader.FromBuffer(result.Value); byte[] data new byte[result.Value.Length]; reader.ReadBytes(data); return data; } public async Taskbool WriteAsync(byte[] data) { var writer new DataWriter(); writer.WriteBytes(data); var result await _targetCharacteristic.WriteValueAsync( writer.DetachBuffer(), GattWriteOption.WriteWithResponse); return result GattCommunicationStatus.Success; } public async Taskbool SubscribeAsync() { var descriptors await _targetCharacteristic .GetDescriptorsAsync(); var cccd descriptors.Descriptors.FirstOrDefault( d d.Uuid GattDescriptorUuids .ClientCharacteristicConfiguration); if (cccd null) return false; var writer new DataWriter(); writer.WriteBytes(new byte[] { 0x01, 0x00 }); var result await cccd.WriteValueAsync(writer.DetachBuffer()); if (result ! GattCommunicationStatus.Success) return false; _targetCharacteristic.ValueChanged OnValueChanged; return true; } private void OnValueChanged(GattCharacteristic sender, GattValueChangedEventArgs args) { var reader DataReader.FromBuffer(args.CharacteristicValue); byte[] data new byte[args.CharacteristicValue.Length]; reader.ReadBytes(data); DataReceived?.Invoke(this, data); } }7.2 WPF界面绑定与交互在WPF中我用一个ListView显示扫描到的设备一个TextBox显示接收到的数据几个Button控制连接、断开、读写操作。设备列表用ObservableCollection 绑定BleDeviceInfo包含Name、Address、Rssi等属性。public class BleDeviceInfo { public string Name { get; set; } public ulong Address { get; set; } public short Rssi { get; set; } public string DisplayName string.IsNullOrEmpty(Name) ? $未知设备 ({Address:X}) : Name; }扫描按钮点击时启动WatcherReceived事件里按Address去重后添加到集合。连接按钮点击时调用ConnectAsync成功后启用读写按钮。7.3 数据解析与协议处理实际项目中BLE设备传过来的数据通常有特定的协议格式。比如一个温度计可能返回两个字节第一个字节是整数部分第二个字节是小数部分。或者一个自定义协议包含帧头、长度、命令字、数据和校验和。我的做法是单独写一个协议解析类把字节数组解析成有意义的对象。这样上层业务逻辑不需要关心字节层面的细节。public class TemperatureParser { public static double Parse(byte[] data) { if (data.Length 2) throw new ArgumentException(数据长度不足); int integerPart data[0]; int decimalPart data[1]; return integerPart decimalPart / 100.0; } }8. 进阶话题与扩展方向8.1 作为GATT Server提供服务前面讲的都是Windows作为GATT Client去连接外设。反过来Windows也可以作为GATT Server让其他设备来连接。这需要用到GattServiceProvider类可以动态创建服务并发布广播。var serviceProviderResult await GattServiceProvider .CreateAsync(TargetServiceUuid); var serviceProvider serviceProviderResult.ServiceProvider; var characteristicResult await serviceProvider.Service .CreateCharacteristicAsync( TargetCharacteristicUuid, new GattLocalCharacteristicParameters { CharacteristicProperties GattCharacteristicProperties.Read | GattCharacteristicProperties.Notify });这个场景适合做设备模拟器或者网关应用。比如你想测试自己的BLE客户端代码但手头没有真实设备就可以用Windows做一个虚拟设备。8.2 多设备并发连接的管理在实际项目中经常需要同时连接多个BLE设备。Windows.Devices.Bluetooth支持多设备并发但每个设备都需要独立的BluetoothLEDevice对象和GattSession。管理多个设备时建议用一个Dictionaryulong, BleDeviceManager来维护每个设备一个Manager实例。需要注意的是Windows对同时连接的BLE设备数量有限制具体数量取决于蓝牙适配器的能力通常在7个左右。超过限制后新的连接会失败。所以如果你的应用需要连接大量设备要考虑分批连接或者用其他方案。8.3 与经典蓝牙的共存有些设备同时支持BLE和经典蓝牙。在Windows中这两种设备的枚举和连接方式不同。经典蓝牙用BluetoothDevice类BLE用BluetoothLEDevice类。如果你的应用需要同时处理两种设备建议分开处理不要混在一起。另外有些USB蓝牙适配器同时支持BLE和经典蓝牙但有些只支持其中一种。在程序启动时可以通过BluetoothAdapter的IsLowEnergySupported和IsClassicSupported属性来判断。8.4 跨平台方案的对比如果你的项目需要跨平台Windows.Devices.Bluetooth就不适用了。跨平台方案有Plugin.BLEXamarin/MAUI、32feet.NET等。Plugin.BLE的API设计跟Windows.Devices.Bluetooth比较接近迁移成本不高。32feet.NET则更偏向经典蓝牙BLE支持相对较弱。选择方案时要考虑目标平台、API成熟度、社区活跃度和长期维护性。如果只做Windows平台Windows.Devices.Bluetooth是首选如果需要跨平台Plugin.BLE更合适。我在实际项目中的体会是Windows.Devices.Bluetooth虽然有一些坑但整体来说是一套设计合理、功能完整的API。只要理解了它的异步模型和线程规则大部分问题都能顺利解决。最关键的一点是不要假设任何操作都会成功每一步都要检查返回值做好异常处理和重试逻辑。BLE通信本质上是不稳定的信号干扰、设备休眠、电量不足都可能导致操作失败健壮的错误处理比优雅的代码结构更重要。
返回列表