项目背景 MAUI的出现,赋予了广大Net开发者开发多平台应用的能力,MAUI 是Xamarin.Forms演变而来,但是相比Xamarin性能更好,可扩展性更强,结构更简单。但是MAUI对于平台相关的实现并不完整。所以MASA团队开展了一个实验性项目,意在对微软MAUI的补充和扩展
项目地址:https://github.com/BlazorComponent/MASA.Blazor/tree/main/src/Masa.Blazor.Maui.Plugin
每个功能都有单独的demo演示项目,考虑到app安装文件体积(虽然MAUI已经集成裁剪功能,但是该功能对于代码本身有影响),届时每一个功能都会以单独的nuget包的形式提供,方便测试,现在项目才刚刚开始,但是相信很快就会有可以交付的内容啦。
前言 本系列文章面向移动开发小白,从零开始进行平台相关功能开发,演示如何参考平台的官方文档使用MAUI技术来开发相应功能。
介绍 微软的MAUI并没有提供蓝牙低功耗设备的相关功能,而物联网开发中蓝牙低功耗是十分常见的,所以我们今天自己集成一个。 由于蓝牙功能设计的内容比较多,篇幅有限,本文只集成一个最基本的蓝牙扫描功能,意在抛砖引玉。后续会陆续更新其他蓝牙通讯功能的文章。本文蓝牙低功耗简称为BLE 如果你对BLE的相关概念不了解,可以参考 开发者官网链接: 蓝牙低功耗-安卓 https://developer.android.google.cn/guide/topics/connectivity/bluetooth-le/
本文JAVA相关代码均来自安卓开发者官网
开发步骤 新建项目 在vs中新建一个基于MAUI Blazor 的项目MauiBlueToothDemo ,然后添加一个MAUI类库 项目Masa.Maui.Plugin.Bluetooth
添加权限 项目创建好了之后,我们首先介绍一下BLE需要的安卓权限,相信大家对各种APP首次打开的权限确认弹窗应该不会陌生。
在应用中使用蓝牙功能,必须声明 BLUETOOTH 蓝牙权限,需要此权限才能执行任何蓝牙通信,例如请求连接、接受连接和传输数据等。 由于 LE 信标通常与位置相关联,还须声明 ACCESS_FINE_LOCATION 权限。没有此权限,扫描将无法返回任何结果。 如果适配 Android 9(API 级别 28)或更低版本,可以声明 ACCESS_COARSE_LOCATION 权限而非 ACCESS_FINE_LOCATION 权限 如果想让应用启动设备发现或操纵蓝牙设置,还须声明 BLUETOOTH_ADMIN 权限。注意:如果使用 LUETOOTH_ADMIN 权限,则您必须拥有 BLUETOOTH 权限。 在MauiBlueToothDemo 项目中的AndroidManifest.xml 添加权限,我们这里面向Android 9以上版本。
1 2 3 4 5 6 <uses-permission android:name ="android.permission.BLUETOOTH" /> <uses-permission android:name ="android.permission.BLUETOOTH_ADMIN" /> <uses-permission android:name ="android.permission.ACCESS_FINE_LOCATION" />
Android 6.0之后,只在AndroidManifest.xml 声明权限已经不够了,出于安全考虑,必须动态申请权限,也就是需要在使用特定功能之前提示用户进行权限确认。 我们在Masa.Maui.Plugin.Bluetooth 项目的Platforms _Android 下新建MasaMauiBluetoothService 类,并添加一个内部类BluetoothPermissions ,MAUI的默认权限没有包含蓝牙低功耗,所以我们需要扩展一个自定义的蓝牙权限类,只要继承自 Permissions.BasePermission 即可
1 2 3 4 5 6 7 8 9 10 private class BluetoothPermissions : Permissions.BasePlatformPermission { public override (string androidPermission, bool isRuntime )[] RequiredPermissions => new List<(string androidPermission, bool isRuntime)> { (global ::Android.Manifest.Permission.AccessFineLocation, true ), (global ::Android.Manifest.Permission.Bluetooth, true ), (global ::Android.Manifest.Permission.BluetoothAdmin, true ), }.ToArray(); }
我们在MasaMauiBluetoothService类内部添加一个方法,来实现动态获取权限
1 2 3 4 5 6 7 8 9 10 11 12 public async Task<bool > CheckAndRequestBluetoothPermission (){ var status = await Permissions.CheckStatusAsync<BluetoothPermissions>(); if (status == PermissionStatus.Granted) return true ; status = await Permissions.RequestAsync<BluetoothPermissions>(); if (status == PermissionStatus.Granted) return true ; return false ; }
检查权限 的当前状态,使用 Permissions.CheckStatusAsync 方法。 向用户请求权限 ,使用 Permissions.RequestAsync 方法。 如果用户以前授予了权限,并且尚未撤消该权限,则此方法将返回 Granted 而不向用户显示对话框。
设置BLE BLE的开发第一步骤就是设置BLE 为什么要设置BLE,因为我们在使用BLE进行通讯之前,需要验证设备是否支持BLE或者检查BLE是否开启。我们先看一下java的实现方式
1 2 3 4 5 6 7 JAVA 代码 private BluetoothAdapter bluetoothAdapter;... final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); bluetoothAdapter = bluetoothManager.getAdapter();
在编写平台相关代码时,安卓的系统管理服务都是同getSystemService 方法获取的,该方法的参数为系统服务的名称,对应在MAUI中的方法为Android.App.Application.Context.GetSystemService ,流程是完全一样的,语法稍有不同,我们如法炮制,在MasaMauiBluetoothService 中添加一个构造函数,和两个字段
1 2 3 4 5 6 7 private readonly BluetoothManager _bluetoothManager;private readonly BluetoothAdapter _bluetoothAdapter;public MasaMauiBluetoothService (){ _bluetoothManager = (BluetoothManager)Android.App.Application.Context.GetSystemService(Android.App.Application.BluetoothService); _bluetoothAdapter = _bluetoothManager?.Adapter; }
GetSystemService 返回BluetoothManager 实例,然后通过BluetoothManager 获取 BluetoothAdapter , BluetoothAdapter 代表设备自身的蓝牙适配器,之后的蓝牙操作都需要通过BluetoothAdapter 完成 继续在MasaMauiBluetoothService 添加一个检查蓝牙适配器是否存在并开启的方法
1 2 3 4 public bool IsEnabled (){ return _bluetoothAdapter is {IsEnabled: true }; }
BLE扫描 与BLE设备通讯,首先需要扫描出附近的BLE设备,我们先看看Java怎么实现的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 JAVA 代码 public class DeviceScanActivity extends ListActivity { private BluetoothAdapter bluetoothAdapter; private boolean mScanning; private Handler handler; private static final long SCAN_PERIOD = 10000 ; ... private void scanLeDevice (final boolean enable) { if (enable) { handler.postDelayed(new Runnable () { @Override public void run () { mScanning = false ; bluetoothAdapter.stopLeScan(leScanCallback); } }, SCAN_PERIOD); mScanning = true ; bluetoothAdapter.startLeScan(leScanCallback); } else { mScanning = false ; bluetoothAdapter.stopLeScan(leScanCallback); } ... } ... }
扫描设备需要使用bluetoothAdapter.startLeScan 方法,并指定一个BluetoothAdapter.LeScanCallback 回调方法作为参数 我们再看一下LeScanCallback 的Java实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 JAVA 代码 private LeDeviceListAdapter leDeviceListAdapter;... private BluetoothAdapter.LeScanCallback leScanCallback = new BluetoothAdapter .LeScanCallback() { @Override public void onLeScan (final BluetoothDevice device, int rssi, byte [] scanRecord) { runOnUiThread(new Runnable () { @Override public void run () { leDeviceListAdapter.addDevice(device); leDeviceListAdapter.notifyDataSetChanged(); } }); } };
因为扫描很耗费资源,所以示例代码通过runOnUiThread 设置扫描进程在设备的前台运行,扫描到设备后触发leScanCallback 回调,然后通过私有的LeDeviceListAdapter 字段保存扫描到的设备列表。 我们如法炮制这部分功能,在MasaMauiBluetoothService 中添加一个继承自ScanCallback 内部类DevicesCallback ,ScanCallback 类 对应安卓的leScanCallback
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 private class DevicesCallback : ScanCallback { private readonly EventWaitHandle _eventWaitHandle = new (false , EventResetMode.AutoReset); public List<BluetoothDevice> Devices { get ; } = new (); public void WaitOne () { Task.Run(async () => { await Task.Delay(5000 ); _eventWaitHandle.Set(); }); _eventWaitHandle.WaitOne(); } public override void OnScanResult (ScanCallbackType callbackType, ScanResult result ) { System.Diagnostics.Debug.WriteLine("OnScanResult" ); if (!Devices.Contains(result.Device)) { Devices.Add(result.Device); } base .OnScanResult(callbackType, result); } }
篇幅问题我们这里只重写OnScanResult 一个方法。当有设备被扫描到就会触发这个方法,然后就可以通过ScanResult 的Device 属性来获取设备信息。 我们在MAUI中打印调试信息可以使用System.Diagnostics.Debug.WriteLine 真机调试的信息会被打印到vs的输出控制台。 我们添加一个属性Devices 用于汇总收集扫描到的设备信息。这里使用了EventWaitHandle 用于在异步操作时控制线程间的同步,线程在 EventWaitHandle 上将一直受阻,直到未受阻的线程调用 Set 方法,没用过的可以自行查看微软文档。 继续在MasaMauiBluetoothService 添加字段,并在构造函数初始化。
1 2 3 4 5 6 7 8 9 10 11 private readonly ScanSettings _settings;private readonly DevicesCallback _callback;public MasaMauiBluetoothService (){ _bluetoothManager = (BluetoothManager)Android.App.Application.Context.GetSystemService(Android.App.Application.BluetoothService); _bluetoothAdapter = _bluetoothManager?.Adapter; _settings = new ScanSettings.Builder() .SetScanMode(Android.Bluetooth.LE.ScanMode.Balanced) ?.Build(); _callback = new DevicesCallback(); }
这里也很好理解,ScanSettings 通过ScanSettings.Builder() 构造,用来配置蓝牙的扫描模式,我们这里使用平衡模式,具体式有如下三种:
ScanSettings.SCAN_MODE_LOW_POWER 低功耗模式(默认扫描模式,如果扫描应用程序不在前台,则强制使用此模式。) ScanSettings.SCAN_MODE_BALANCED 平衡模式 ScanSettings.SCAN_MODE_LOW_LATENCY 高功耗模式(建议仅在应用程序在前台运行时才使用此模式。)
最后添加ScanLeDeviceAsync 方法
1 2 3 4 5 6 7 8 9 10 11 public async Task<IReadOnlyCollection<BluetoothDevice>> ScanLeDeviceAsync () { _bluetoothAdapter.BluetoothLeScanner.StartScan (null, _settings, _callback); await Task.Run (() => { _callback.WaitOne (); }); _bluetoothAdapter.BluetoothLeScanner.StopScan (_callback); return _callback.Devices.AsReadOnly (); }
StartScan 方法的第一个参数是过滤条件,可以根据名称等进行过滤,我们暂不设置过滤。
测试 编译Masa.Maui.Plugin.Bluetooth 项目,然后在MauiBlueToothDemo 项目中引用Masa.Maui.Plugin.Bluetooth.dll 。 修改MauiBlueToothDemo 的Index 页面,页面使用了对MAUI支持良好的Masa Blazor组件: Masa Blazor
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @page "/" <MButton OnClick ="ScanBLEDeviceAsync" > 扫描蓝牙设备</MButton > <div class ="text-center" > <MDialog @bind-Value ="ShowProgress" Width ="500" > <ChildContent > <MCard > <MCardTitle > 正在扫描蓝牙设备 </MCardTitle > <MCardText > <MProgressCircular Size ="40" Indeterminate Color ="primary" > </MProgressCircular > </MCardText > </MCard > </ChildContent > </MDialog > </div > <MCard Class ="mx-auto" MaxWidth ="400" Tile > @foreach (var item in BluetoothDeviceList) { <MListItem > <MListItemContent > <MListItemTitle > @item</MListItemTitle > </MListItemContent > </MListItem > } </MCard >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 using Masa.Maui.Plugin.Bluetooth;using Microsoft.AspNetCore.Components;namespace MauiBlueToothDemo.Pages { public partial class Index { private bool ShowProgress { get ; set ; } private List<string > BluetoothDeviceList { get ; set ; } = new (); [Inject ] private MasaMauiBluetoothService BluetoothService { get ; set ; } private async Task ScanBLEDeviceAsync () { if (BluetoothService.IsEnabled()) { if (await BluetoothService.CheckAndRequestBluetoothPermission()) { ShowProgress = true ; var deviceList = await BluetoothService.ScanLeDeviceAsync(); BluetoothDeviceList = deviceList.Where(o => !string .IsNullOrEmpty(o.Name)).Select(o => o.Name).Distinct().ToList(); ShowProgress = false ; } } } } }
不要忘记在MauiProgram.cs注入写好的MasaMauiBluetoothService
1 2 3 #if ANDROID builder.Services.AddSingleton<MasaMauiBluetoothService>(); #endif
我们真机运行一下看看效果
同时在vs的输出中可以看到打印的日志
本文到此结束,下一篇我们实现具体的BLE的通讯。
如果你对我们MASA感兴趣,无论是代码贡献、使用、提 Issue,欢迎联系我们
WeChat:MasaStackTechOps
QQ:7424099