MASA MAUI Plugin IOS藍芽低功耗(三)藍芽掃描

MASA技術團隊發表於2022-11-24

專案背景

MAUI的出現,賦予了廣大Net開發者開發多平臺應用的能力,MAUI 是Xamarin.Forms演變而來,但是相比Xamarin效能更好,可擴充套件性更強,結構更簡單。但是MAUI對於平臺相關的實現並不完整。所以MASA團隊開展了一個實驗性專案,意在對微軟MAUI的補充和擴充套件,
專案地址:https://github.com/BlazorComp...

每個功能都有單獨的demo演示專案,考慮到app安裝檔案體積(雖然MAUI已經整合裁剪功能,但是該功能對於程式碼本身有影響),屆時每一個功能都會以單獨的nuget包的形式提供,方便測試,現在專案才剛剛開始,但是相信很快就會有可以交付的內容啦。

前言

本系列文章面向移動開發小白,從零開始進行平臺相關功能開發,演示如何參考平臺的官方文件使用MAUI技術來開發相應功能。

介紹

之前兩篇文章我們實現了安卓藍芽BLE的相關功能,本文我們將IOS的BLE功能實現一下。
,考慮到Swift語法對於c#開發人員更友好,本文示例程式碼參考Swift,相關程式碼來自蘋果開發者官網
https://developer.apple.com/d...

開發步驟

修改專案

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/d...

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,篇幅問題我們這裡先只重寫DiscoveredPeripheralUpdatedState,我們這次的演示不需要實現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.ContainsDevices.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 儲存結果。
我們還需實現PlatformIsEnabledIsEnabledPlatformCheckAndRequestBluetoothPermission方法,用來在掃描之前檢查藍芽是否可用並且已經經過使用者授權

        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方法中指定了需要檢查的藍芽許可權,BasePlatformPermissionEnsureDeclared方法用來檢查是否在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之前的版本,需要將NSBluetoothAlwaysUsageDescriptionNSBluetoothPeripheralUsageDescription同時新增。

藍芽掃描的效果和安卓機是完全一樣的,這裡就不展示了。前文詳情

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

相關文章