MAUI Blazor学习5-BLE低功耗蓝牙
MAUI Blazor学习5-BLE低功耗蓝牙
MAUI Blazor系列目录
- MAUI Blazor学习1-移动客户端Shell布局 - SunnyTrudeau - 博客园 (cnblogs.com)
- MAUI Blazor学习2-创建移动客户端Razor页面 - SunnyTrudeau - 博客园 (cnblogs.com)
- MAUI Blazor学习3-绘制ECharts图表 - SunnyTrudeau - 博客园 (cnblogs.com)
- MAUI Blazor学习4-绘制BootstrapBlazor.Chart图表 - SunnyTrudeau - 博客园 (cnblogs.com)
目前MAUI提供了很多跨平台访问设备的能力,类似前一代的Xamarin.Essentials,但是MAUI不提供跨平台的蓝牙库,这真是巨大的遗憾。
https://learn.microsoft.com/zh-cn/dotnet/maui/platform-integration/?view=net-maui-7.0
.NET 多平台应用 UI (.NET MAUI) 支持的每个平台都提供可从 C# 访问的唯一操作系统和平台 API。 .NET MAUI 提供跨平台 API 来访问此平台的大部分功能,其中包括访问传感器、访问应用正在运行的设备的相关信息、检查网络连接、安全地存储数据以及启动基于浏览器的身份验证流。
物联网项目中经常要用到蓝牙连接,网上有一些第三方免费开源蓝牙库,我试用过这几个:
- Plugin.BLE,在Xamarin时期就一直用,接口封装很丰富,可以对扫描设备做详细的设置,适合物联网项目使用。
- InTheHand.BluetoothLE,老牌蓝牙库,我在Windows平台用过它的经典蓝牙库,能用,但是它家的BLE低功耗蓝牙库接口封装太简陋了,很多扫描参数都没有开放出来,在我的项目中没法用。
- Shiny.BluetoothLE,看规划很宏大,框架里边糅合了很多杂七杂八的东西,跨平台蓝牙库还处于alpha状态,我写了一点代码跑不起来。
对于MAUI Blazor项目而言,还可以考虑使用基于浏览器的低功耗蓝牙接口,网上有JavaScript的BLE库,我对js不熟,没有试过。
申请蓝牙权限
在MaBlaApp项目NuGet安装Plugin.BLE,注意包含预览版。
<PackageReference Include="Plugin.BLE" Version="3.0.0-beta.2" />
注意安卓12配套的SDK 31对蓝牙权限申请做了破坏性变更。参见:
https://www.cnblogs.com/fly263/archive/2022/09/21/16715525.html
《Android12蓝牙权限适配说明 - fly263 - 博客园.html》
https://developer.android.google.cn/about/versions/12/features/bluetooth-permissions
《Android 12 中的新蓝牙权限 _ Android Developers.html》
在申请蓝牙权限的时候,需要描述适用安卓SDK版本。
D:\Software\gitee\mauiblazorapp\MaBlaApp\Platforms\Android\AndroidManifest.xml
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" /> <!-- csproj文件指定SupportedOSPlatformVersion android 28.0 可以继续使用安卓9的权限 --> <!--<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.BLUETOOTH"/> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>--> <!-- csproj文件指定SupportedOSPlatformVersion android 31.0 使用安卓12的权限 --> <!-- Android 12以下才需要定位权限,Android 9以下官方建议申请ACCESS_COARSE_LOCATION --> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="30"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30"/> <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30"/> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30"/> <!-- Android 12在不申请定位权限时,必须加上android:usesPermissionFlags="neverForLocation",否则搜不到设备 --> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation"/> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
把MaBlaApp.csproj项目配置的安卓SDK版本也从24升级到31。
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">31.0</SupportedOSPlatformVersion>
自定义蓝牙权限类,根据安卓版本动态申请。
D:\Software\gitee\mauiblazorapp\MaBlaApp\Platforms\Android\BluetoothPermissions.cs
/// <summary> /// 自定义蓝牙权限,MAUI暂不提供蓝牙权限 /// </summary> public class BluetoothPermissions : Permissions.BasePlatformPermission { public override (string androidPermission, bool isRuntime)[] RequiredPermissions => GetRequiredPermissions(); //根据安卓平台版本,返回对应的申请权限 private (string androidPermission, bool isRuntime)[] GetRequiredPermissions() { var permissions = new List<string>(); if (DeviceInfo.Version.Major >= 12) { // Android 版本大于等于 12 时,申请新的蓝牙权限 permissions.Add(global::Android.Manifest.Permission.BluetoothScan); permissions.Add(global::Android.Manifest.Permission.BluetoothConnect); } else { //csproj文件指定SupportedOSPlatformVersion android 28.0可以继续使用安卓9的权限 permissions.Add(global::Android.Manifest.Permission.Bluetooth); permissions.Add(global::Android.Manifest.Permission.BluetoothAdmin); permissions.Add(global::Android.Manifest.Permission.AccessCoarseLocation); permissions.Add(global::Android.Manifest.Permission.AccessFineLocation); } var result = new List<(string androidPermission, bool isRuntime)>(); foreach (var permission in permissions) { result.Add((permission, true)); } return result.ToArray(); } }
扫描蓝牙外设
新建低功耗蓝牙测试者BleTester类,实现扫描外设功能。这个DEMO的逻辑假设找到目标外设就结束扫描。
D:\Software\gitee\mauiblazorapp\MaBlaApp\Data\BleTester.cs
#region 扫描外设 /// <summary> /// 开始扫描 /// </summary> /// <returns></returns> public async Task<bool> StartScanAsync() { //检查获取蓝牙权限 bool isPermissionPass = await CheckAndRequestBluetoothPermission(); if (!isPermissionPass) return false; _scanForAedCts = new CancellationTokenSource(); try { CurrentAdapter.DeviceDiscovered += Adapter_DeviceDiscovered; CurrentAdapter.ScanTimeoutElapsed += Adapter_ScanTimeoutElapsed; //蓝牙扫描时间 CurrentAdapter.ScanTimeout = 10 * 1000; //默认LowPower CurrentAdapter.ScanMode = ScanMode.LowPower; Debug.WriteLine($"开始扫描外设, IsAvailable={CurrentBle.IsAvailable}, IsOn={CurrentBle.IsOn}, State={CurrentBle.State}, ScanMode={CurrentAdapter.ScanMode}, ScanTimeout={CurrentAdapter.ScanTimeout}"); await CurrentAdapter.StartScanningForDevicesAsync(cancellationToken: _scanForAedCts.Token); Debug.WriteLine($"结束扫描外设"); } catch (OperationCanceledException) { Debug.WriteLine($"扫描外设任务取消"); } catch (Exception ex) { Debug.WriteLine($"扫描外设出错, {ex.Message}"); } finally { CurrentAdapter.DeviceDiscovered -= Adapter_DeviceDiscovered; CurrentAdapter.ScanTimeoutElapsed -= Adapter_ScanTimeoutElapsed; } return true; } /// <summary> /// 检查获取蓝牙权限 /// </summary> /// <returns></returns> public async Task<bool> CheckAndRequestBluetoothPermission() { #if ANDROID var status = await Permissions.CheckStatusAsync<BluetoothPermissions>(); if (status == PermissionStatus.Granted) return true; status = await Permissions.RequestAsync<BluetoothPermissions>(); if (status == PermissionStatus.Granted) return true; #endif return true; } public string TagDeviceName { get; set; } = ""; public IDevice Device = null; public string TagDeviceInfo { get; private set; } = ""; private void Adapter_DeviceDiscovered(object sender, DeviceEventArgs e) { //[0:] 扫描到蓝牙设备honor Band 4-7E8, Id=00000000-0000-0000-0000-f4bf805ad7e8, Name=honor Band 4-7E8, Rssi=-50, State=Disconnected, AdvertisementRecords.Count=5 Debug.WriteLine($"扫描到蓝牙设备{e.Device}, Id={e.Device.Id}, Name={e.Device.Name}, Rssi={e.Device.Rssi}, State={e.Device.State}, AdvertisementRecords.Count={e.Device.AdvertisementRecords.Count}"); string localName = e.Device.Name; if (string.Compare(localName, TagDeviceName, true) == 0) { TagDeviceInfo = $"{e.Device}, Id={e.Device.Id}, Name={e.Device.Name}, Rssi={e.Device.Rssi}, State={e.Device.State}, AdvertisementRecords.Count={e.Device.AdvertisementRecords.Count}"; Device = e.Device; //如果找到目标外设,退出扫描 if (!_scanForAedCts.IsCancellationRequested) _scanForAedCts.Cancel(false); } } private void Adapter_ScanTimeoutElapsed(object sender, EventArgs e) { Debug.WriteLine("蓝牙扫描超时结束"); } #endregion
新建页面测试BLE功能,扫描的目标外设是一个智能手表。
D:\Software\gitee\mauiblazorapp\MaBlaApp\Pages\TestBle.razor
//扫描外设 private async void ScanDevice() { MyBleTester.TagDeviceName = "honor Band 4-7E8"; IsScanning = true; IsFound = false; //开始扫描 IsFound = await MyBleTester.StartScanAsync(); IsScanning = false; //异步更新UI await InvokeAsync(() => StateHasChanged()); }
连接外设
在BleTester类,实现连接外设功能。
D:\Software\gitee\mauiblazorapp\MaBlaApp\Data\BleTester.cs
#region 连接外设 //连接蓝牙外设 public async Task<bool> ConnectDeviceAsync() { Debug.WriteLine($"开始连接{TagDeviceName}"); //连接外设 //设置forceBleTransport=true, 否则错误GattCallback error: 133 //这个是函数只发起连接,不代表连接成功 await CurrentAdapter.ConnectToDeviceAsync(Device, new ConnectParameters(false, true)); //订阅连接丢失 CurrentAdapter.DeviceDisconnected += CurrentAdapter_DeviceDisconnected; //订阅连接断开 CurrentAdapter.DeviceConnectionLost += CurrentAdapter_DeviceConnectionLost; Debug.WriteLine($"连接成功{TagDeviceName}"); return true; } //订阅连接丢失 private void CurrentAdapter_DeviceConnectionLost(object? sender, DeviceErrorEventArgs e) { Debug.WriteLine($"蓝牙连接丢失, {e.Device?.State}"); CurrentAdapter.DeviceConnectionLost -= CurrentAdapter_DeviceConnectionLost; CurrentAdapter.DeviceDisconnected -= CurrentAdapter_DeviceDisconnected; } //订阅连接断开 private void CurrentAdapter_DeviceDisconnected(object? sender, DeviceEventArgs e) { Debug.WriteLine($"蓝牙连接状态变化, {e.Device?.State}"); CurrentAdapter.DeviceConnectionLost -= CurrentAdapter_DeviceConnectionLost; CurrentAdapter.DeviceDisconnected -= CurrentAdapter_DeviceDisconnected; } #endregion
读写特征值数据
在BleTester类,实现读写特征值数据功能。这个DEMO演示读取常规信息服务的设备名特征值。BLE传输数据是比较麻烦的,需要先获取服务对象,再获取特征值对象,再读写数据,需要熟悉BLE的协议特点。在开发物联网APP时,需要开发外设的嵌入式软件工程师提供详细的服务和特征值GUID资料。
D:\Software\gitee\mauiblazorapp\MaBlaApp\Data\BleTester.cs
public string ReadDeviceNameResult { get; private set; } = "未读取"; //读取设备名 public async Task<bool> ReadDeviceName() { ReadDeviceNameResult = "未读取"; Debug.WriteLine($"开始获取服务"); //获取服务集合 var services = await Device.GetServicesAsync(); var infoes = services.Select(x => $"{x.Id}: Name={x.Name}, IsPrimary={x.IsPrimary}"); string msg = $"服务Uuid: " + string.Join(", ", infoes); Debug.WriteLine(msg); //获取常规信息服务 Guid genericServiceGuid = Guid.Parse("00001800-0000-1000-8000-00805f9b34fb"); var genericService = await Device.GetServiceAsync(genericServiceGuid); if (genericService == null) { Debug.WriteLine($"获取常规信息服务{genericServiceGuid}失败"); return false; } Debug.WriteLine($"开始获取特征值"); //获取特征值集合 var characteristics = await genericService.GetCharacteristicsAsync(); infoes = characteristics.Select(x => $"{x.Id}: {x.Properties}"); msg = $"特征值: " + string.Join(", ", infoes); Debug.WriteLine(msg); //获取设备名特征值 Guid deviceNameCharacteristicGuid = Guid.Parse("00002a00-0000-1000-8000-00805f9b34fb"); var deviceNameCharacteristic = characteristics.FirstOrDefault(x => x.Id == deviceNameCharacteristicGuid); if (deviceNameCharacteristic == null) { Debug.WriteLine($"获取设备名特征值{deviceNameCharacteristicGuid}失败"); return false; } //读取设备名特征值 var ary = await ReadDataAsync(deviceNameCharacteristic); if (ary is not null) { ReadDeviceNameResult = Encoding.ASCII.GetString(ary); } #region notify类型特征值接收消息通知 //notifyCharacteristic.ValueUpdated += NotifyCharacteristic_ValueUpdated; #endregion return true; } //读特征值 private async Task<byte[]> ReadDataAsync(ICharacteristic characteristic) { //根据Plugin.BLE要求,在主线程读写数据 var result = await MainThread.InvokeOnMainThreadAsync(async () => { try { //读取数据 byte[] ary = await characteristic.ReadAsync(); Debug.WriteLine($"读取成功,长度={ary.Length}"); return ary; } catch (Exception ex) { Debug.WriteLine($"读取错误, 目标设备蓝牙连接状态={Device?.State}, {ex.Message}"); return null; } }); return result; }
测试运行
在安卓手机(Android 12)上运行,可以扫描到目标智能手表,可以连接,并读取设备名。
部分控制台日志:
[0:] 开始扫描外设, IsAvailable=True, IsOn=True, State=On, ScanMode=LowPower, ScanTimeout=10000
[BluetoothLeScanner] onScannerRegistered() - status=0 scannerId=9 mScannerId=0
[0:] 扫描到蓝牙设备BT SPEAKER app, Id=00000000-0000-0000-0000-a23c7efdd11e, Name=BT SPEAKER app, Rssi=-95, State=Disconnected, AdvertisementRecords.Count=5
[0:] 扫描到蓝牙设备, Id=00000000-0000-0000-0000-382d5860a0e8, Name=, Rssi=-89, State=Disconnected, AdvertisementRecords.Count=1
[0:] 扫描到蓝牙设备, Id=00000000-0000-0000-0000-563ce2c76684, Name=, Rssi=-82, State=Disconnected, AdvertisementRecords.Count=1
[0:] 扫描到蓝牙设备TY, Id=00000000-0000-0000-0000-a09208760d93, Name=TY, Rssi=-75, State=Disconnected, AdvertisementRecords.Count=5
[0:] 扫描到蓝牙设备midea, Id=00000000-0000-0000-0000-9cc12d75974e, Name=midea, Rssi=-93, State=Disconnected, AdvertisementRecords.Count=5
[0:] 扫描到蓝牙设备honor Band 4-7E8, Id=00000000-0000-0000-0000-f4bf805ad7e8, Name=honor Band 4-7E8, Rssi=-50, State=Disconnected, AdvertisementRecords.Count=5
[0:] 结束扫描外设
[0:] 开始连接honor Band 4-7E8
[BluetoothGatt] connect() - device: F4:BF:80:**:**:**, auto: false
[BluetoothGatt] registerApp()
[BluetoothGatt] registerApp() - UUID=ed7bb811-38c5-43a6-853e-944e15e8bcbc
[BluetoothGatt] onClientRegistered() - status=0 clientIf=9
[BluetoothGatt] onClientConnectionState() - status=0 clientIf=9 device=F4:BF:80:**:**:**
[0:] 连接成功honor Band 4-7E8
[0:] 连接成功honor Band 4-7E8
[BluetoothGatt] onConnectionUpdated() - Device=F4:BF:80:**:**:** interval=6 latency=0 timeout=500 status=0
[BluetoothGatt] onConnectionUpdated() - Device=F4:BF:80:**:**:** interval=36 latency=0 timeout=500 status=0
[0:] 开始获取服务
[BluetoothGatt] discoverServices() - device: F4:BF:80:**:**:**
[BluetoothGatt] onSearchComplete() = Device=F4:BF:80:**:**:** Status=0
[0:] 服务Uuid: 00001800-0000-1000-8000-00805f9b34fb: Name=Generic Access, IsPrimary=True, 00001801-0000-1000-8000-00805f9b34fb: Name=Generic Attribute, IsPrimary=True, 0000180a-0000-1000-8000-00805f9b34fb: Name=Device Information, IsPrimary=True, 0000fe86-0000-1000-8000-00805f9b34fb: Name=Unknown Service, IsPrimary=True, 00001812-0000-1000-8000-00805f9b34fb: Name=Human Interface Device, IsPrimary=True, 00003802-0000-1000-8000-00805f9b34fb: Name=Unknown Service, IsPrimary=True
[0:] 开始获取特征值
[0:] 特征值: 00002a00-0000-1000-8000-00805f9b34fb: Read, 00002a01-0000-1000-8000-00805f9b34fb: Read, 00002aa6-0000-1000-8000-00805f9b34fb: Read, 00002ac9-0000-1000-8000-00805f9b34fb: Read
[BluetoothGatt] readCharacteristic() - uuid: 00002a00-0000-1000-8000-00805f9b34fb
[BluetoothGatt] onCharacteristicRead() - Device=F4:BF:80:**:**:** handle=3 Status=0
[0:] 读取成功,长度=16
问题
MAUI连接蓝牙外设的性能有时候不如Android原生APP,我看了一下Plugin.BLE的源代码,最终是调用Xamarin封装的Android接口函数,这个也跟外设的蓝牙芯片型号有关,有的好有的不好。
DEMO代码地址:https://gitee.com/woodsun/mauiblazorapp