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

我的项目背景

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

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理