关于xamarin:MASA-MAUI-Plugin-安卓蓝牙低功耗二蓝牙通讯

55次阅读

共计 15413 个字符,预计需要花费 39 分钟才能阅读完成。

我的项目背景

MAUI 的呈现,赋予了宽广 Net 开发者开发多平台利用的能力,MAUI 是 Xamarin.Forms 演变而来,然而相比 Xamarin 性能更好,可扩展性更强,构造更简略。然而 MAUI 对于平台相干的实现并不残缺。所以 MASA 团队发展了一个实验性我的项目,意在对微软 MAUI 的补充和扩大

我的项目地址
https://github.com/BlazorComp…

每个性能都有独自的 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 代码
bluetoothGatt = device.connectGatt(this, false, gattCallback);

连贯到 BLE 设施上的 GATT 服务器,须要应用 connectGatt() 办法。此办法采纳三个参数:一个 Context 对象、autoConnect(布尔值,批示是否在可用时主动连贯到 BLE 设施),以及对 BluetoothGattCallback 的援用。该办法 BluetoothGatt 实例,而后可应用该实例执行 GATT 客户端操作。调用方(Android 利用)是 GATT 客户端。BluetoothGattCallback 用于向客户端传递后果(例如连贯状态),以及任何进一步的 GATT 客户端操作。
咱们再看一下 BluetoothGattCallback 的 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

    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 办法。

        Task PlatformWriteValue(byte[] value, bool requireResponse)
        {
            TaskCompletionSource<bool> 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 办法,示意写入并期待返回。

        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 的重写

internal event EventHandler<GattEventArgs> 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<bool> 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 类是咱们自定义的一个类,鉴于篇幅问题这里不全副展现

  public sealed partial class GattService
    {public Task<IReadOnlyList<GattCharacteristic>> GetCharacteristicsAsync()
        {return PlatformGetCharacteristics();
        }
        ...

PlatformGetCharacteristics 的具体实现在该类平台对应的局部类中

    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 代码
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 办法

  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 来实现。

        Task PlatformWriteValue(byte[] value)
        {TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();

            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 代码
@Override
// Characteristic notification
public void onCharacteristicChanged(BluetoothGatt gatt,
        BluetoothGattCharacteristic characteristic) {broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
}

在 GattCharacteristic.cs 中增加

        void OnCharacteristicValueChanged(GattCharacteristicValueChangedEventArgs args)
        {characteristicValueChanged?.Invoke(this, args);
        }
        public event EventHandler<GattCharacteristicValueChangedEventArgs> 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 增加

        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 增加一个发送数据的办法

        public async Task SendDataAsync(string deviceName,Guid servicesUuid,Guid? characteristicsUuid, byte[] dataBytes, EventHandler<GattCharacteristicValueChangedEventArgs> 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 增加测试代码

 public partial class Index
    {
        private string SelectedDevice;
        private List<string> _allDeviceResponse = new List<string>();
        [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

@page "/"
<MButton OnClick="ScanBLEDeviceAsync"> 扫描蓝牙设施 </MButton>

<div class="text-center">
    <MDialog @bind-Value="ShowProgress" Width="500">
        <ChildContent>
            <MCard>
                <MCardTitle>
                    正在扫描蓝牙设施
                </MCardTitle>
                <MCardText>
                    <MProgressCircular Size="40" Indeterminate Color="primary"></MProgressCircular>
                </MCardText>
            </MCard>
        </ChildContent>
    </MDialog>
</div>


@if (BluetoothDeviceList.Any())
{
    <MSelect style="margin-top:10px"
                 Outlined
                 Items="BluetoothDeviceList"
                 ItemText="u=>u"
                 ItemValue="u=>u"
                 TItem="string"
                 TValue="string"
                 TItemValue="string"
                 @bind-Value="SelectedDevice"
                 OnSelectedItemUpdate="item => SelectedDevice = item">
        </MSelect>
}
@if (!string.IsNullOrEmpty(SelectedDevice))
{<MButton OnClick="() => SendDataAsync()"> 发送查问版本指令 </MButton>}

@if (_allDeviceResponse.Any())
{
    <MCard>
        <MTextarea Value="@string.Join(' ',_allDeviceResponse)"></MTextarea>
    </MCard>
}

咱们看一下成果

本文到此结束。

如果你对咱们 MASA 感兴趣,无论是代码奉献、应用、提 Issue,欢送分割咱们

  • WeChat:MasaStackTechOps
  • QQ:7424099

正文完
 0