MASA MAUI Plugin 安卓藍牙低功耗(一)藍牙掃描

語言: CN / TW / HK

項目背景

MAUI的出現,賦予了廣大Net開發者開發多平台應用的能力,MAUI 是Xamarin.Forms演變而來,但是相比Xamarin性能更好,可擴展性更強,結構更簡單。但是MAUI對於平台相關的實現並不完整。所以MASA團隊開展了一個實驗性項目,意在對微軟MAUI的補充和擴展,項目地址:http://github.com/BlazorComponent/MASA.Blazor/tree/main/src/Masa.Blazor.Maui.Plugin

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

前言

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

介紹

微軟的MAUI並沒有提供藍牙低功耗設備的相關功能,而物聯網開發中藍牙低功耗是十分常見的,所以我們今天自己集成一個。 由於藍牙功能設計的內容比較多,篇幅有限,本文只集成一個最基本的藍牙掃描功能,意在拋磚引玉。後續會陸續更新其他藍牙通訊功能的文章。本文藍牙低功耗簡稱為BLE 如果你對BLE的相關概念不瞭解,可以參考 開發者官網鏈接: 藍牙低功耗-安卓 http://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以上版本。

xml <!--藍牙權限--> <uses-permission android:name="android.permission.BLUETOOTH" /> <!--讓應用啟動設備發現或操縱藍牙設置--> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <!-- 如果設配Android9及更低版本,可以申請 ACCESS_COARSE_LOCATION --> <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即可

csharp 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類內部添加一個方法,來實現動態獲取權限

```csharp public async Task CheckAndRequestBluetoothPermission() { var status = await Permissions.CheckStatusAsync();

        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的實現方式

java JAVA 代碼 private BluetoothAdapter bluetoothAdapter; ... // Initializes Bluetooth adapter. final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); bluetoothAdapter = bluetoothManager.getAdapter(); 在編寫平台相關代碼時,安卓的系統管理服務都是同getSystemService方法獲取的,該方法的參數為系統服務的名稱,對應在MAUI中的方法為Android.App.Application.Context.GetSystemService,流程是完全一樣的,語法稍有不同,我們如法炮製,在MasaMauiBluetoothService中添加一個構造函數,和兩個字段

csharp 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 獲取BluetoothAdapterBluetoothAdapter代表設備自身的藍牙適配器,之後的藍牙操作都需要通過BluetoothAdapter完成 繼續在MasaMauiBluetoothService添加一個檢查藍牙適配器是否存在並開啟的方法

csharp public bool IsEnabled() { return _bluetoothAdapter is {IsEnabled: true}; }

BLE掃描

與BLE設備通訊,首先需要掃描出附近的BLE設備,我們先看看Java怎麼實現的 ```java JAVA 代碼 /* * Activity for scanning and displaying available BLE devices. / public class DeviceScanActivity extends ListActivity {

private BluetoothAdapter bluetoothAdapter;
private boolean mScanning;
private Handler handler;

// Stops scanning after 10 seconds.
private static final long SCAN_PERIOD = 10000;
...
private void scanLeDevice(final boolean enable) {
    if (enable) {
        // Stops scanning after a pre-defined scan period.
        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實現

java JAVA 代碼 private LeDeviceListAdapter leDeviceListAdapter; ... // Device scan callback. 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內部類DevicesCallbackScanCallback類 對應安卓的leScanCallback

```csharp 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一個方法。當有設備被掃描到就會觸發這個方法,然後就可以通過ScanResultDevice屬性來獲取設備信息。 我們在MAUI中打印調試信息可以使用System.Diagnostics.Debug.WriteLine真機調試的信息會被打印到vs的輸出控制枱。 我們添加一個屬性Devices用於彙總收集掃描到的設備信息。這裏使用了EventWaitHandle 用於在異步操作時控制線程間的同步,線程在 EventWaitHandle 上將一直受阻,直到未受阻的線程調用 Set 方法,沒用過的可以自行查看微軟文檔。 繼續在MasaMauiBluetoothService添加字段,並在構造函數初始化。

csharp 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方法

cpp 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。 修改MauiBlueToothDemoIndex頁面,頁面使用了對MAUI支持良好的Masa Blazor組件: Masa Blazor

```html @page "/" 掃描藍牙設備

正在掃描藍牙設備

@foreach (var item in BluetoothDeviceList) { @item } ```

```csharp using Masa.Maui.Plugin.Bluetooth; using Microsoft.AspNetCore.Components;

namespace MauiBlueToothDemo.Pages { public partial class Index { private bool ShowProgress { get; set; } private List 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

```csharp

if ANDROID

    builder.Services.AddSingleton<MasaMauiBluetoothService>();

endif

``` 我們真機運行一下看看效果

在這裏插入圖片描述

同時在vs的輸出中可以看到打印的日誌

在這裏插入圖片描述

本文到此結束,下一篇我們實現具體的BLE的通訊。


如果你對我們MASA感興趣,無論是代碼貢獻、使用、提 Issue,歡迎聯繫我們

  • WeChat:MasaStackTechOps
  • QQ:7424099