MASA MAUI Plugin(三) IOS蓝牙低功耗蓝牙扫描
MASA MAUI Plugin IOS蓝牙低功耗(三)蓝牙扫描
项目背景
MAUI的出现,赋予了广大Net开发者开发多平台应用的能力,MAUI 是Xamarin.Forms演变而来,但是相比Xamarin性能更好,可扩展性更强,结构更简单。但是MAUI对于平台相关的实现并不完整。所以MASA团队开展了一个实验性项目,意在对微软MAUI的补充和扩展,项目地址https://github.com/BlazorComponent/MASA.Blazor/tree/feature/Maui/src/Masa.Blazor.Maui.Plugin,每个功能都有单独的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"的错误,请检查你的数据线,重新插拔或者更换原装线。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具