MASA MAUI Plugin 安卓蓝牙低功耗(二)蓝牙通讯

项目背景

MAUI的出现,赋予了广大Net开发者开发多渠道应用的才能,MAUI 是Xamarin.Forms演化而来,可是比较Xamarin性能更好,可扩展性更强,结构更简单。可是MAUI对于渠道相关的完成并不完好。所以MASA团队开展了一个实验性项目,意在对微软MAUI的弥补和扩展

项目地址github.com/BlazorCompo…

每个功用都有独自的demo演示项目,考虑到app安装文件体积(尽管MAUI现已集成裁剪功用,可是该功用对于代码本身有影响),届时每一个功用都会以独自的nuget包的方式提供,便利测验,现在项目才刚刚开始,可是信任很快就会有能够交给的内容啦。

前语

本系列文章面向移动开发小白,从零开始进行渠道相关功用开发,演示怎么参考渠道的官方文档运用MAUI技能来开发相应功用。

介绍

上一篇文章咱们完成了蓝牙BLE的扫描功用,这里咱们持续完成通讯功用。 本文JAVA相关代码均来自安卓开发者官网

开发步骤

衔接到 GATT 服务器

通用特点配置文件Generic Attribute Profile简称GATT。 GATT界说了特点类型并规定了怎么运用,包含了一个数据传输和存储的结构和一些根本操作。中间包含了一些概念如特性characteristics,服务services等。同时还界说了发现服务,特性和服务间的衔接的处理过程,也包含读写特性值。 咱们运用移远的FC410举例

MASA MAUI Plugin 安卓蓝牙低功耗(二)蓝牙通讯

经过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 MAUI Plugin 安卓蓝牙低功耗(二)蓝牙通讯

本文到此结束

如果你对咱们的开源项目感兴趣,无论是代码贡献、运用、提 Issue,欢迎联系咱们

  • WeChat:MasaStackTechOps
  • QQ:7424099