MAUI Blazor学习5-BLE低功耗蓝牙

MAUI Blazor学习5-BLE低功耗蓝牙

 MAUI Blazor系列目录

  1. MAUI Blazor学习1-移动客户端Shell布局 - SunnyTrudeau - 博客园 (cnblogs.com)
  2. MAUI Blazor学习2-创建移动客户端Razor页面 - SunnyTrudeau - 博客园 (cnblogs.com)
  3. MAUI Blazor学习3-绘制ECharts图表 - SunnyTrudeau - 博客园 (cnblogs.com)
  4. 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项目而言,还可以考虑使用基于浏览器的低功耗蓝牙接口,网上有JavaScriptBLE库,我对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

 

posted on 2023-01-27 09:22  SunnyTrudeau  阅读(3707)  评论(8编辑  收藏  举报