MASA MAUI Plugin 安卓藍牙低功耗(二)藍牙通訊

語言: CN / TW / HK

MASA MAUI Plugin 安卓藍牙低功耗(二)藍牙通訊

項目背景

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技術來開發相應功能。

介紹

上一篇文章我們實現了藍牙BLE的掃描功能,這裏我們繼續實現通訊功能。 本文JAVA相關代碼均來自安卓開發者官網

開發步驟

連接到 GATT 服務器

通用屬性配置文件Generic Attribute Profile簡稱GATT。 GATT定義了屬性類型並規定了如何使用,包括了一個數據傳輸和存儲的框架和一些基本操作。中間包含了一些概念如特性characteristics,服務services等。同時還定義了發現服務,特性和服務間的連接的處理過程,也包括讀寫特性值。 我們使用移遠的FC410舉例 在這裏插入圖片描述

通過nRF connect工具可以查看設備的配置,該設備有一個前綴為FFFF的主服務,該服務下有一個前綴為FF01的特徵,該特徵具有通知Notify 和寫入Write兩種屬性(如果有Notify,那麼就會有描述符)。換句話説我們可以通過這個特徵給設備發送數據,而且可以通過訂閲該特徵值變化事件,來獲取設備通過藍牙的返回信息。 與 BLE 設備交互的第一步便是連接到 GATT 服務器。更具體地説,是連接到設備上的 GATT 服務器。 我們先看一下JAVA的實現方式

java JAVA代碼 bluetoothGatt = device.connectGatt(this, false, gattCallback); 連接到 BLE 設備上的 GATT 服務器,需要使用 connectGatt() 方法。此方法採用三個參數:一個 Context 對象、autoConnect(布爾值,指示是否在可用時自動連接到 BLE 設備),以及對 BluetoothGattCallback 的引用。該方法 BluetoothGatt 實例,然後可使用該實例執行 GATT 客户端操作。調用方(Android 應用)是 GATT 客户端。BluetoothGattCallback 用於向客户端傳遞結果(例如連接狀態),以及任何進一步的 GATT 客户端操作。 我們再看一下BluetoothGattCallback 的JAVA實現

```java JAVA 代碼 // Various callback methods defined by the BLE API. private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() { @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { String intentAction; if (newState == BluetoothProfile.STATE_CONNECTED) { intentAction = ACTION_GATT_CONNECTED; connectionState = STATE_CONNECTED; broadcastUpdate(intentAction); Log.i(TAG, "Connected to GATT server."); Log.i(TAG, "Attempting to start service discovery:" + bluetoothGatt.discoverServices());

        } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
            intentAction = ACTION_GATT_DISCONNECTED;
            connectionState = STATE_DISCONNECTED;
            Log.i(TAG, "Disconnected from GATT server.");
            broadcastUpdate(intentAction);
        }
    }

    @Override
    // New services discovered
    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
        if (status == BluetoothGatt.GATT_SUCCESS) {
            broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);
        } else {
            Log.w(TAG, "onServicesDiscovered received: " + status);
        }
    }

    @Override
    // Result of a characteristic read operation
    public void onCharacteristicRead(BluetoothGatt gatt,
            BluetoothGattCharacteristic characteristic,
            int status) {
        if (status == BluetoothGatt.GATT_SUCCESS) {
            broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
        }
    }
 ...

``` 因為日後還需要實現其他平台的功能,我們的想法是所有公共部分都放到項目根目錄,平台相關的實現,放到對應Platforms目錄下對應平台的文件夾內,然後通過分部類的方式組織類結構。平台相關的方法起名以Platform為前綴。 我們先在Masa.Blazor.Maui.Plugin.Bluetooth項目Platforms->Android目錄新建一個名稱為RemoteGattServer.android.cs的分部類,然後添加初始化方法和BluetoothGattCallback

```csharp partial class RemoteGattServer { private Android.Bluetooth.BluetoothGatt _gatt; private Android.Bluetooth.BluetoothGattCallback _gattCallback;

    private void PlatformInit()
    {
        _gattCallback = new GattCallback(this);
        _gatt = ((Android.Bluetooth.BluetoothDevice)Device).ConnectGatt(Android.App.Application.Context, false, _gattCallback);
    }

    public static implicit operator Android.Bluetooth.BluetoothGatt(RemoteGattServer gatt)
    {
        return gatt._gatt;
    }
    internal event EventHandler<CharacteristicEventArgs> CharacteristicRead;
    internal event EventHandler<GattEventArgs> ServicesDiscovered;
    private bool _servicesDiscovered = false;

...

    internal class GattCallback : Android.Bluetooth.BluetoothGattCallback
    {
        private readonly RemoteGattServer _remoteGattServer;

        internal GattCallback(RemoteGattServer remoteGattServer)
        {
            _remoteGattServer = remoteGattServer;
        }

... public override void OnCharacteristicWrite(Android.Bluetooth.BluetoothGatt gatt, Android.Bluetooth.BluetoothGattCharacteristic characteristic, Android.Bluetooth.GattStatus status) { System.Diagnostics.Debug.WriteLine($"CharacteristicWrite {characteristic.Uuid} Status:{status}"); _remoteGattServer.CharacteristicWrite?.Invoke(_remoteGattServer, new CharacteristicEventArgs { Characteristic = characteristic, Status = status }); } } } ... internal class ConnectionStateEventArgs : GattEventArgs { public Android.Bluetooth.ProfileState State { get; internal set; } }

internal class CharacteristicEventArgs : GattEventArgs
{
    public Android.Bluetooth.BluetoothGattCharacteristic Characteristic
    {
        get; internal set;
    }
}

``` 在PlatformInit方法中連接到 GATT 服務器。自定義的GattCallback 集成自 Android.Bluetooth.BluetoothGattCallback,篇幅問題,這裏只展示CharacteristicWrite一個方法的重寫,要實現完整功能還至少需要額外重寫ServicesDiscovered、ConnectionStateChanged、CharacteristicChanged、CharacteristicRead、DescriptorRead、DescriptorWrite四個方法,詳細請參考源代碼。在我們向設備特徵值發送數據時,會觸發OnCharacteristicWrite方法,方法內部觸發我們自定義的CharacteristicWrite。

寫入藍牙指令

官方文檔示例中沒有給出特徵值寫入的示例,這裏我們自己實現。 我們新建GattCharacteristic類,在項目根目錄新建GattCharacteristic.cs,在Android目錄新建GattCharacteristic.android.cs 在GattCharacteristic.android.cs中添加PlatformWriteValue方法。

```csharp Task PlatformWriteValue(byte[] value, bool requireResponse) { TaskCompletionSource tcs = null;

        if (requireResponse)
        {
            tcs = new TaskCompletionSource<bool>();

            void handler(object s, CharacteristicEventArgs e)
            {
                if (e.Characteristic == _characteristic)
                {
                    Service.Device.Gatt.CharacteristicWrite -= handler;

                    if (!tcs.Task.IsCompleted)
                    {
                        tcs.SetResult(e.Status == Android.Bluetooth.GattStatus.Success);
                    }
                }
            };

            Service.Device.Gatt.CharacteristicWrite += handler;
        }

        bool written = _characteristic.SetValue(value);
        _characteristic.WriteType = requireResponse ? Android.Bluetooth.GattWriteType.Default : Android.Bluetooth.GattWriteType.NoResponse;
        written = ((Android.Bluetooth.BluetoothGatt)Service.Device.Gatt).WriteCharacteristic(_characteristic);

        if (written && requireResponse)
            return tcs.Task;

        return Task.CompletedTask;
    }

``` 通過_characteristic.SetValue將需要發送的字節數組存儲到該特徵值的本地存儲中,然後通過WriteCharacteristic發送到遠程Gatt服務器。 這裏用到了TaskCompletionSource,主要還是起到異步轉同步作用。安卓藍牙的寫特徵屬性分為WRITE_TYPE_DEFAULT(寫入)和WRITE_TYPE_NO_RESPONSE(寫入無返回),參數requireResponse就表示是否需要設備返回,如果需要返回,就將TaskCompletionSource存儲的結果以Task形式返回調用者。 我們在GattCharacteristic中添加WriteValueWithResponseAsync方法,表示寫入並等待返回。

```csharp public Task WriteValueWithResponseAsync(byte[] value) { ThrowOnInvalidValue(value); return PlatformWriteValue(value, true); }

    private void ThrowOnInvalidValue(byte[] value)
    {
        if (value is null)
            throw new ArgumentNullException("value");

        if (value.Length > 512)
            throw new ArgumentOutOfRangeException("value", "Attribute value cannot be longer than 512 bytes");
    }

``` 因為藍牙限制單次寫入的長度最大為512,所以我們這裏做一下長度檢查。 這樣的組織結構,當我們再添加其他平台的實現代碼時,就可以直接通過調用PlatformWriteValue來調用具體平台的實現代碼了。 想對藍牙進行寫入操作,當然需要先找到藍牙設備的服務id和特徵值id才行。所以我們繼續在GattCallback中添加一個OnConnectionStateChange的重寫

```csharp internal event EventHandler ServicesDiscovered; ... internal class GattCallback : Android.Bluetooth.BluetoothGattCallback { ... public override void OnConnectionStateChange(Android.Bluetooth.BluetoothGatt gatt, Android.Bluetooth.GattStatus status, Android.Bluetooth.ProfileState newState) { System.Diagnostics.Debug.WriteLine($"ConnectionStateChanged Status:{status} NewState:{newState}"); _remoteGattServer.ConnectionStateChanged?.Invoke(_remoteGattServer, new ConnectionStateEventArgs { Status = status, State = newState }); if (newState == Android.Bluetooth.ProfileState.Connected) { if (!_remoteGattServer._servicesDiscovered) gatt.DiscoverServices(); } else { _remoteGattServer.Device.OnGattServerDisconnected(); } } } private async Task WaitForServiceDiscovery() { if (_servicesDiscovered) return true;

        TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();

        void handler(object s, GattEventArgs e)
        {
            ServicesDiscovered -= handler;

            if (!tcs.Task.IsCompleted)
            {
                tcs.SetResult(true);
            }
        };

        ServicesDiscovered += handler;
        return await tcs.Task;
    }

    Task PlatformConnect()
    {
        TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();

        void handler(object s, ConnectionStateEventArgs e)
        {
            ConnectionStateChanged -= handler;

            switch (e.Status)
            {
                case Android.Bluetooth.GattStatus.Success:
                    tcs.SetResult(e.State == Android.Bluetooth.ProfileState.Connected);
                    break;

                default:
                    tcs.SetResult(false);
                    break;
            }
        }

        ConnectionStateChanged += handler;
        bool success = _gatt.Connect();
        if (success)
        {
            if (IsConnected)
                return Task.FromResult(true);

            return tcs.Task;
        }
        else
        {
            ConnectionStateChanged -= handler;
            return Task.FromException(new OperationCanceledException());
        }
    }

    async Task<List<GattService>> PlatformGetPrimaryServices(BluetoothUuid? service)
    {
        var services = new List<GattService>();

        await WaitForServiceDiscovery();

        foreach (var serv in _gatt.Services)
        {
            // if a service was specified only add if service uuid is a match
            if (serv.Type == Android.Bluetooth.GattServiceType.Primary && (!service.HasValue || service.Value == serv.Uuid))
            {
                services.Add(new GattService(Device, serv));
            }
        }

        return services;
    }
    ...
}
...
internal class GattEventArgs : EventArgs
{
    public Android.Bluetooth.GattStatus Status
    {
        get; internal set;
    }
}

``` 當設備連接或斷開與某個設備的連接時,會觸發我們重寫的OnConnectionStateChange方法,然後我們在方法內部,判斷如果是連接的狀態(ProfileState.Connected),就去通過gatt服務的DiscoverServices來查找設備的服務及特徵值信息等。 PlatformGetPrimaryServices方法用來找到BLE設備的所有主服務(通過GattServiceType.Primary來判斷是否為主服務),返回一個GattService列表,GattService類是我們自定義的一個類,鑑於篇幅問題這裏不全部展示

csharp public sealed partial class GattService { public Task<IReadOnlyList<GattCharacteristic>> GetCharacteristicsAsync() { return PlatformGetCharacteristics(); } ... PlatformGetCharacteristics的具體實現在該類平台對應的部分類中

csharp partial class GattService { private Task<IReadOnlyList<GattCharacteristic>> PlatformGetCharacteristics() { List<GattCharacteristic> characteristics = new List<GattCharacteristic>(); foreach (var characteristic in NativeService.Characteristics) { characteristics.Add(new GattCharacteristic(this, characteristic)); } return Task.FromResult((IReadOnlyList<GattCharacteristic>)characteristics.AsReadOnly()); } ...

打開藍牙監聽

以上一系列操作我們已經可以拿到具體的這個設備的服務和具體的特徵值了,對於BLE設備,大部分都是通過Notify屬性進行廣播的。我們需要開啟一個廣播監聽 我看參考一下JAVA代碼

java JAVA 代碼 private BluetoothGatt bluetoothGatt; BluetoothGattCharacteristic characteristic; boolean enabled; ... bluetoothGatt.setCharacteristicNotification(characteristic, enabled); ... BluetoothGattDescriptor descriptor = characteristic.getDescriptor( UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG)); descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); bluetoothGatt.writeDescriptor(descriptor); 開啟廣播監聽的方式是向對應描述符寫入一個指令(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)即可開啟廣播。 我們在GattCharacteristic.android.cs添加PlatformStartNotifications方法

```csharp private async Task PlatformStartNotifications() { byte[] data;

        if (_characteristic.Properties.HasFlag(Android.Bluetooth.GattProperty.Notify))
            data = Android.Bluetooth.BluetoothGattDescriptor.EnableNotificationValue.ToArray();
        else if (_characteristic.Properties.HasFlag(Android.Bluetooth.GattProperty.Indicate))
            data = Android.Bluetooth.BluetoothGattDescriptor.EnableIndicationValue.ToArray();
        else
            return;

        ((Android.Bluetooth.BluetoothGatt)Service.Device.Gatt).SetCharacteristicNotification(_characteristic, true);
        var descriptor = await GetDescriptorAsync(GattDescriptorUuids.ClientCharacteristicConfiguration);
        await descriptor.WriteValueAsync(data);
    }

``` 這裏判斷是否支持Notify,然後調用EnableNotificationValue構造一個打開監聽的指令data,然後通過GetDescriptorAsync拿到這個特徵值對應的描述符,這裏很簡單隻要調用安卓對應特徵值的GetDescriptor即可,這裏就不展示代碼了。一個BLE設備如果有通知的屬性,那麼他一定會有描述符,打開或者關閉通知都需要通過描述符寫入指令來控制,所有對特徵值的操作然後通過WriteValueAsync->PlatformWriteValue來實現。

```csharp Task PlatformWriteValue(byte[] value) { TaskCompletionSource tcs = new TaskCompletionSource();

        void handler(object s, DescriptorEventArgs e)
        {
            if (e.Descriptor == _descriptor)
            {
                Characteristic.Service.Device.Gatt.DescriptorWrite -= handler;

                if (!tcs.Task.IsCompleted)
                {
                    tcs.SetResult(e.Status == Android.Bluetooth.GattStatus.Success);
                }
            }
        };

        Characteristic.Service.Device.Gatt.DescriptorWrite += handler;
        bool written = _descriptor.SetValue(value);
        written = ((Android.Bluetooth.BluetoothGatt)Characteristic.Service.Device.Gatt).WriteDescriptor(_descriptor);
        if (written)
            return tcs.Task;

        return Task.FromException(new OperationCanceledException());
    }

```

接收 GATT 通知

到此我們已經實現了連接設備、獲取主服務和特徵值、寫入數據、打開通知監聽,最後還剩一個就是監聽特徵值的變化,為某個特徵啟用通知後,如果遠程設備上的特徵發生更改(我們收到消息),則會觸發 onCharacteristicChanged() 回調:

java JAVA代碼 @Override // Characteristic notification public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic); }

在GattCharacteristic.cs中添加

```csharp void OnCharacteristicValueChanged(GattCharacteristicValueChangedEventArgs args) { characteristicValueChanged?.Invoke(this, args); } public event EventHandler CharacteristicValueChanged { add { characteristicValueChanged += value; AddCharacteristicValueChanged();

        }
        remove
        {
            characteristicValueChanged -= value;
            RemoveCharacteristicValueChanged();
        }
    }
    ...
   public sealed class GattCharacteristicValueChangedEventArgs : EventArgs
    {
        internal GattCharacteristicValueChangedEventArgs(byte[] newValue)
        {
            Value = newValue;
        }
    public byte[] Value { get; private set; }
}

``` 在平台對應的GattCharacteristic.android.cs添加

csharp void AddCharacteristicValueChanged() { Service.Device.Gatt.CharacteristicChanged += Gatt_CharacteristicChanged; } void RemoveCharacteristicValueChanged() { Service.Device.Gatt.CharacteristicChanged -= Gatt_CharacteristicChanged; } private void Gatt_CharacteristicChanged(object sender, CharacteristicEventArgs e) { if (e.Characteristic == _characteristic) OnCharacteristicValueChanged(new GattCharacteristicValueChangedEventArgs(e.Characteristic.GetValue())); } 這裏的實現思路和之前是一樣的。

測試

我們在MasaMauiBluetoothService添加一個發送數據的方法

```csharp public async Task SendDataAsync(string deviceName,Guid servicesUuid,Guid? characteristicsUuid, byte[] dataBytes, EventHandler gattCharacteristicValueChangedEventArgs) { BluetoothDevice blueDevice = _discoveredDevices.FirstOrDefault(o => o.Name == deviceName);

        var primaryServices = await blueDevice.Gatt.GetPrimaryServicesAsync();
        var primaryService = primaryServices.First(o => o.Uuid.Value == servicesUuid);

        var characteristics = await primaryService.GetCharacteristicsAsync();
        var characteristic = characteristics.FirstOrDefault(o => (o.Properties & GattCharacteristicProperties.Write) != 0);
        if (characteristicsUuid != null)
        {
            characteristic = characteristics.FirstOrDefault(o => o.Uuid.Value == characteristicsUuid);
        }

        await characteristic.StartNotificationsAsync();
        characteristic.CharacteristicValueChanged += gattCharacteristicValueChangedEventArgs;
        await characteristic.WriteValueWithResponseAsync(dataBytes);
    }

``` 在Masa.Blazor.Maui.Plugin.BlueToothSample項目的Index.razor.cs添加測試代碼

```csharp public partial class Index { private string SelectedDevice; private List _allDeviceResponse = new List(); [Inject] private MasaMauiBluetoothService BluetoothService { get; set; } ... private async Task SendDataAsync(string cmd= "AT+QVERSION") { var byteData = System.Text.Encoding.Default.GetBytes(cmd); await SendDataAsync(SelectedDevice, byteData); }

    private async Task SendDataAsync(string deviceSerialNo, byte[] byteData)
    {
        if (byteData.Any())
        {
            _allDeviceResponse = new List<string>();

if ANDROID

            await BluetoothService.SendDataAsync(deviceSerialNo,Guid.Parse("0000ffff-0000-1000-8000-00805f9b34fb"),null, byteData, onCharacteristicChanged);

endif

        }
    }

    void onCharacteristicChanged(object sender, GattCharacteristicValueChangedEventArgs e)
    {
        var deviceResponse = System.Text.Encoding.Default.GetString(e.Value);
        _allDeviceResponse.Add(deviceResponse);
        InvokeAsync(() => { StateHasChanged(); });
    }
}

向設備發送查詢版本號的指令“AT+QVERSION”,設備返回通過onCharacteristicChanged方法獲取,設備返回的是二進制數組,所以需要轉成字符串顯示出來。 簡單在寫個界面修改Index.razor Masa Blazor組件: [Masa Blazor](https://www.masastack.com/blazor)html @page "/" 掃描藍牙設備

正在掃描藍牙設備

@if (BluetoothDeviceList.Any()) { } @if (!string.IsNullOrEmpty(SelectedDevice)) { }

@if (_allDeviceResponse.Any()) { } ``` 我們看一下效果

在這裏插入圖片描述

本文到此結束

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

  • WeChat:MasaStackTechOps
  • QQ:7424099