写点什么

MASA MAUI Plugin IOS 蓝牙低功耗(三)蓝牙扫描

  • 2022 年 10 月 11 日
    浙江
  • 本文字数:5341 字

    阅读完需:约 18 分钟

项目背景

MAUI 的出现,赋予了广大 Net 开发者开发多平台应用的能力,MAUI 是 Xamarin.Forms 演变而来,但是相比 Xamarin 性能更好,可扩展性更强,结构更简单。但是 MAUI 对于平台相关的实现并不完整。所以 MASA 团队开展了一个实验性项目,意在对微软 MAUI 的补充和扩展,项目地址


每个功能都有单独的 demo 演示项目,考虑到 app 安装文件体积(虽然 MAUI 已经集成裁剪功能,但是该功能对于代码本身有影响),届时每一个功能都会以单独的 nuget 包的形式提供,方便测试,现在项目才刚刚开始,但是相信很快就会有可以交付的内容啦。

前言

本系列文章面向移动开发小白,从零开始进行平台相关功能开发,演示如何参考平台的官方文档使用 MAUI 技术来开发相应功能。

介绍

之前两篇文章我们实现了安卓蓝牙 BLE 的相关功能,本文我们将 IOS 的 BLE 功能实现一下。,考虑到 Swift 语法对于 c#开发人员更友好,本文示例代码参考 Swift,相关代码来自苹果开发者官网https://developer.apple.com/documentation

开发步骤

修改项目

Masa.Blazor.Maui.Plugin.Bluetooth 项目中的 Platforms->iOS 文件夹下,添加一个部分类 MasaMauiBluetoothService,在安卓中有 BluetoothManager,在 ios 中对应的是 CBCentralManager,但是不同有安卓还有个适配器 Adapter 的概念,在 ios 中关于设备扫描、连接和管理外围设备的对象,都是通过 CBCentralManager 直接管理的,我们看一下他的初始化方法


init(    delegate: CBCentralManagerDelegate?,    queue: DispatchQueue?,    options: [String : Any]? = nil)
复制代码


delegate:接收中心事件的委托。相当于我们在安装中实现的 DevicesCallback


queue:用于调度中心角色事件的调度队列。如果该值为 nil,则中央管理器将使用主队列分派中心角色事件。这个我们可以简单的理解为和安卓的 UI 线程或者后台线程对应,更详尽的说明请参考https://developer.apple.com/documentation/dispatch/dispatchqueue


options:配置信息,我们这里只用到了 ShowPowerAlert,代表蓝牙设备如果不可用,给用户提示信息。就好比你用了不符合标准的数据线,iphone 会给你提示是一个意思。


 public static partial class MasaMauiBluetoothService    {        private static BluetoothDelegate _delegate = new();        public static CBCentralManager _manager = new CBCentralManager(_delegate, DispatchQueue.DefaultGlobalQueue, new CBCentralInitOptions        {            ShowPowerAlert = true,        });        private sealed class BluetoothDelegate : CBCentralManagerDelegate        {            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 DiscoveredPeripheral(CBCentralManager central, CBPeripheral peripheral, NSDictionary advertisementData, NSNumber RSSI) { System.Diagnostics.Debug.WriteLine("OnScanResult"); if (!Devices.Contains(peripheral)) { Devices.Add(peripheral); } } [Preserve] public override void UpdatedState(CBCentralManager central) { } } }
复制代码


我们将 MasaMauiBluetoothService 修改为静态类,我们自定义的 BluetoothDelegate 继承自 CBCentralManagerDelegate,篇幅问题我们这里先只重写 DiscoveredPeripheral UpdatedState,我们这次的演示不需要实现 UpdatedState,但是这里的重写必须先放上去,否则调试过程会出现下面的报错


ObjCRuntime.ObjCException: 'Objective-C exception thrown. Name: NSInvalidArgumentException Reason: -[Masa_Blazor_Maui_Plugin_Bluetooth_MasaMauiBluetoothService_BluetoothDelegate centralManagerDidUpdateState:]: unrecognized selector sent to instance 0x284bfe200


另外有一点需要特别注意,这个 UpdatedState 方法我没有实现的代码,那么我就需要添加一个[Preserve],这样是为了防止链接器 在生成 nuget 包的时候把这个方法帮我优化掉。



实现发现附近设备功能,_eventWaitHandle 和安卓一样,我这里只是实现了一个异步转同步方便直接通过 Devices 拿到结果,如果小伙伴不喜欢后期我会添加不阻塞的方式。这里之所以可以 Devices.Contains Devices.Add 是因为我们在 BluetoothDevice 类中实现了隐式转换如下是 iOS 目录下 BluetoothDevice.ios.cs 的部分代码


    partial class BluetoothDevice    {        ...        private BluetoothDevice(CBPeripheral peripheral)        {            _peripheral = peripheral;        }
public static implicit operator BluetoothDevice(CBPeripheral peripheral) { return peripheral == null ? null : new BluetoothDevice(peripheral); }
public static implicit operator CBPeripheral(BluetoothDevice device) { return device._peripheral; } ...
复制代码


ios 扫描外围设备是通过 scanForPeripherals 我们继续在 MasaMauiBluetoothService 添加一个扫描附件设备的方法,我们看一下 Swift 的文档


func scanForPeripherals(    withServices serviceUUIDs: [CBUUID]?,    options: [String : Any]? = nil)
复制代码


serviceUUIDs:代表需要过滤的服务 UUID,类似安卓的 scanFilter 对象。option:提供扫描的选项,我们这里用到了 AllowDuplicatesKey,该值指定扫描是否应在不重复筛选的情况下运行我们参照实现以下我们的 PlatformScanForDevices 方法


        private static async Task<IReadOnlyCollection<BluetoothDevice>> PlatformScanForDevices()        {            if (!_manager.IsScanning)            {                _manager.ScanForPeripherals(new CBUUID[] { }, new PeripheralScanningOptions                {                    AllowDuplicatesKey = true                });
await Task.Run(() => { _delegate.WaitOne(); });
_manager.StopScan(); _discoveredDevices = _delegate.Devices.AsReadOnly(); }

return _discoveredDevices; }
复制代码


通过 _cbCentralManager.IsScanning 来判断是否处于扫描状态,如果没有,那就就通过 ScanForPeripherals 扫描外围设备,扫描 5 秒之后(BluetoothDelegate 内部控制)通过 StopScan 停止扫描,并通过 _discoveredDevices 保存结果。我们还需实现 PlatformIsEnabledIsEnabled PlatformCheckAndRequestBluetoothPermission 方法,用来在扫描之前检查蓝牙是否可用并且已经经过用户授权


        public static bool PlatformIsEnabledIsEnabled()        {            return _manager.State == CBManagerState.PoweredOn;        }        public static async Task<PermissionStatus> PlatformCheckAndRequestBluetoothPermission()        {            PermissionStatus status = await Permissions.CheckStatusAsync<BluetoothPermissions>();
if (status == PermissionStatus.Granted) return status;
if (status == PermissionStatus.Denied && DeviceInfo.Platform == DevicePlatform.iOS) { // Prompt the user to turn on in settings // On iOS once a permission has been denied it may not be requested again from the application return status; }
status = await Permissions.RequestAsync<BluetoothPermissions>(); return status; } private class BluetoothPermissions : Permissions.BasePlatformPermission { protected override Func<IEnumerable<string>> RequiredInfoPlistKeys => () => new string[] { "NSBluetoothAlwaysUsageDescription", "NSBluetoothPeripheralUsageDescription" };
public override Task<PermissionStatus> CheckStatusAsync() { EnsureDeclared(); return Task.FromResult(GetBleStatus()); } private PermissionStatus GetBleStatus() //Todo:Needs to be replenished { var status = _cbCentralManager.State; return status switch { CBManagerState.PoweredOn=> PermissionStatus.Granted, CBManagerState.Unauthorized => PermissionStatus.Denied, CBManagerState.Resetting => PermissionStatus.Restricted, _ => PermissionStatus.Unknown, }; } }
复制代码


PlatformIsEnabledIsEnabled 方法中通过 _cbCentralManager.State == CBManagerState.PoweredOn 来判断蓝牙是否可用。该状态一共有如下枚举,从字面意思很好理解 Unknown, //手机没有识别到蓝牙 Resetting, //手机蓝牙已断开连接 Unsupported, //手机蓝牙功能没有权限 Unauthorized, //手机蓝牙功能没有权限 PoweredOff,//手机蓝牙功能关闭 PoweredOn //蓝牙开启且可用


权限检查这里和安卓有一些区别,在重写的 RequiredInfoPlistKeys 方法中指定了需要检查的蓝牙权限,BasePlatformPermission EnsureDeclared 方法用来检查是否在 Info.plist 文件添加了需要的权限,GetBleStatus 方法通过 _cbCentralManager 的状态,来检查授权情况。


我们在 Masa.Blazor.Maui.Plugin.Bluetooth 的根目录添加部分类 MasaMauiBluetoothService.cs,向使用者提供 ScanForDevicesAsync 等方法,方法内部通过 PlatformScanForDevices 来调用具体平台的实现。


    public static partial class MasaMauiBluetoothService    {        private static IReadOnlyCollection<BluetoothDevice> _discoveredDevices;        public static Task<IReadOnlyCollection<BluetoothDevice>> ScanForDevicesAsync()        {            return PlatformScanForDevices();        }                public static bool IsEnabled()        {            return PlatformIsEnabledIsEnabled();        }
public static async Task<PermissionStatus> CheckAndRequestBluetoothPermission() { return await PlatformCheckAndRequestBluetoothPermission(); } }
复制代码

使用

右键 Masa.Blazor.Maui.Plugin.Bluetooth 项目,点击打包,生成一个 nuget 包,在 Masa.Blazor.Maui.Plugin.BlueToothSample 项目中离线安装即可,代码的使用与安卓完全一样,只是权限配置方式不同在 Masa.Blazor.Maui.Plugin.BlueToothSample 项目的 Platforms->iOS->Info.plist 中添加蓝牙相关权限


  <key>NSBluetoothAlwaysUsageDescription</key>  <string>App required to access Bluetooth</string>  <key>NSBluetoothPeripheralUsageDescription</key>  <string>App required to access Bluetooth</string>
复制代码


NSBluetoothAlwaysUsageDescription 对应 iOS 13 以上版本,对于 iOS 13 之前的版本,需要将 NSBluetoothAlwaysUsageDescription NSBluetoothPeripheralUsageDescription 同时添加。


蓝牙扫描的效果和安卓机是完全一样的,这里就不展示了。前文详情

iOS 调试及错误排查

目前在 windows 的 vs 环境调试 MAUI 的 ios 程序,是不需要 mac 电脑支持的,数据线连上后会显示一个本地设备,但是你仍然需要一个开发者账号,vs 会调用 apple 开发者 api 自动帮你配置好需要的证书。



1、如果没有显示检查 Xamarin->iOS 设置,热重启是否开启



2、调试过程如果提示类似 Could not find executable for C:\Users\xxx\AppData\Local\Temp\hbjayi2h.ydn 找不到文件的情况,右键选择清理项目即可,如果无法解决手动删除 bin 和 obj 目录重试


3、调试过程如果 app 无故退出,排查一下考虑 APP 的启动和调试断点时间,iOS 要求所有方法必须在 17 秒之内返回,否则 iOS 系统将停止该应用


4、调试过程出现 Deploy Error: An Lockdown error occurred. The error code was "MuxError"的错误,请检查你的数据线,重新插拔或者更换原装线。


本文到此结束。


如果你对我们 MASA 感兴趣,无论是代码贡献、使用、提 Issue,欢迎联系我们


  • WeChat:MasaStackTechOps

  • QQ:7424099

发布于: 刚刚阅读数: 3
用户头像

还未添加个人签名 2021.10.26 加入

MASA技术团队官方账号,我们专注于.NET现代应用开发解决方案,Wechat:MasaStackTechOps ,Website:www.masastack.com

评论

发布
暂无评论
MASA MAUI Plugin IOS蓝牙低功耗(三)蓝牙扫描_MASA_MASA技术团队_InfoQ写作社区