蓝牙 GATT 客户端

本文演示如何使用适用于Windows应用的蓝牙泛型属性(GATT)客户端 API。

Important

必须在 Package.appxmanifest 中声明“蓝牙”功能。

<Capabilities> <DeviceCapability Name="bluetooth" /> </Capabilities>

概述

可以使用 Windows.Devices.Bluetooth.GenericAttributeProfile 命名空间中的 API 来访问蓝牙 LE 设备。 Bluetooth LE 设备通过以下一组内容提供其功能:

  • 服务
  • 特征
  • 描述 符

服务定义 LE 设备的功能协定,并包含定义服务的特征集合。 这些特征反过来又包含描述特征的描述符。 这 3 个术语通常称为设备的属性。

蓝牙 LE GATT API 公开对象和函数,而不是访问原始传输。 GATT API 还允许你使用蓝牙 LE 设备来执行以下任务:

  • 执行属性发现
  • 读取和写入属性值
  • 注册特征 ValueChanged 事件的回调

若要创建有用的实现,必须事先了解 GATT 服务和应用程序打算使用的特征,并处理特定特征值,以便 API 提供的二进制数据在呈现给用户之前转换为有用的数据。 蓝牙 GATT API 仅公开与蓝牙 LE 设备通信所需的基本基元。 若要解释数据,必须通过蓝牙 SIG 标准配置文件或设备供应商实现的自定义配置文件来定义应用程序配置文件。 配置文件在应用程序和设备之间构成一项具有约束力的约定,规定交换的数据表示什么以及应如何解释这些数据。

为方便起见,蓝牙 SIG 维护可用的 公共配置文件列表

查询附近的设备

有两种主要方法可用于查询附近的设备:

第二种方法在 广告 文档中进行了长时间讨论,因此此处不会讨论太多,但基本思路是查找满足特定 广告筛选器的附近设备的蓝牙地址。 获得地址后,可以调用 BluetoothLEDevice.FromBluetoothAddressAsync 来获取对设备的引用。

现在,返回到 DeviceWatcher 方法。 蓝牙 LE 设备就像Windows中的其他任何设备一样,可以使用 Enumeration API查询。 使用 DeviceWatcher 类并传递一个查询字符串,该字符串指定要查找的设备:

// Query for extra properties you want returned
string[] requestedProperties = { "System.Devices.Aep.DeviceAddress", "System.Devices.Aep.IsConnected" };

DeviceWatcher deviceWatcher =
            DeviceInformation.CreateWatcher(
                    BluetoothLEDevice.GetDeviceSelectorFromPairingState(false),
                    requestedProperties,
                    DeviceInformationKind.AssociationEndpoint);

// Register event handlers before starting the watcher.
// Added, Updated and Removed are required to get all nearby devices
deviceWatcher.Added += DeviceWatcher_Added;
deviceWatcher.Updated += DeviceWatcher_Updated;
deviceWatcher.Removed += DeviceWatcher_Removed;

// EnumerationCompleted and Stopped are optional to implement.
deviceWatcher.EnumerationCompleted += DeviceWatcher_EnumerationCompleted;
deviceWatcher.Stopped += DeviceWatcher_Stopped;

// Start the watcher.
deviceWatcher.Start();

启动 DeviceWatcher 后,对于每个满足查询条件的设备,你都会在相关设备的 Added 事件处理程序中收到其 DeviceInformation。 若要更详细地了解 DeviceWatcher,请参阅 Github 上的完整示例

连接到设备

发现所需设备后,请使用 DeviceInformation.Id 获取相关设备的蓝牙 LE 设备对象:

private async Task ConnectDevice(DeviceInformation deviceInfo)
{
    // Note: BluetoothLEDevice.FromIdAsync must be called from a UI thread because it may prompt for consent.
    BluetoothLEDevice bluetoothLeDevice = await BluetoothLEDevice.FromIdAsync(deviceInfo.Id);
    // ...
}

另一方面,释放对某个设备的 BluetoothLEDevice 对象的所有引用(如果系统上没有其他应用持有对该设备的引用),将在短暂超时后触发自动断开连接。

bluetoothLeDevice.Dispose();

如果应用需要再次访问设备,只需重新创建设备对象并访问特征(下一部分所述)将在必要时触发 OS 重新连接。 如果设备在附近,你将能够访问该设备;否则会返回 DeviceUnreachable 错误。

注释

单独调用此方法创建 BluetoothLEDevice 对象并不(一定)启动连接。 若要启动连接,请将 GattSession.MaintainConnection 设置为true或调用蓝牙LEDevice 上的未缓存服务发现方法,或对设备执行读/写操作。

  • 如果 GattSession.MaintainConnection 设置为 true,则系统无限期等待连接,并在设备可用时连接。 应用程序没有任何需要等待的内容,因为 GattSession.MaintainConnection 是一个属性。
  • 对于 GATT 中的服务发现和读/写操作,系统等待有限但可变的时间。 从瞬间到几分钟不等。 因素包括堆栈上的流量,以及请求排队的方式。 如果没有其他挂起的请求,并且远程设备无法访问,则系统将在超时前等待 7(7) 秒。如果有其他挂起的请求,则队列中的每个请求可能需要 7(7) 秒来处理,因此,越往队列的后面,等待的时间越长。

目前,无法取消连接过程。

枚举支持的服务和特征

现在你有了 BluetoothLEDevice 对象,下一步是发现设备公开的数据。 执行此操作的第一步是查询服务:

GattDeviceServicesResult result = await bluetoothLeDevice.GetGattServicesAsync();

if (result.Status == GattCommunicationStatus.Success)
{
    var services = result.Services;
    // ...
}

确定相关服务后,下一步是查询特征。

GattCharacteristicsResult result = await service.GetCharacteristicsAsync();

if (result.Status == GattCommunicationStatus.Success)
{
    var characteristics = result.Characteristics;
    // ...
}

OS 返回可以对其执行操作的 GattCharacteristic 对象的 ReadOnly 列表。

对特征执行读/写操作

特征是基于 GATT 的通信的基本单位。 它包含一个值,该值表示设备上的不同数据片段。 例如,电池电量特征具有一个值,该值表示设备的电池电量。

读取特征属性以确定支持的操作:

GattCharacteristicProperties properties = characteristic.CharacteristicProperties

if(properties.HasFlag(GattCharacteristicProperties.Read))
{
    // This characteristic supports reading from it.
}
if(properties.HasFlag(GattCharacteristicProperties.Write))
{
    // This characteristic supports writing to it.
}
if(properties.HasFlag(GattCharacteristicProperties.Notify))
{
    // This characteristic supports subscribing to notifications.
}

如果支持读取,则可以读取值:

GattReadResult result = await selectedCharacteristic.ReadValueAsync();
if (result.Status == GattCommunicationStatus.Success)
{
    var reader = DataReader.FromBuffer(result.Value);
    byte[] input = new byte[reader.UnconsumedBufferLength];
    reader.ReadBytes(input);
    // Utilize the data as needed
}

向特征写入数据遵循类似的方式:

var writer = new DataWriter();
// WriteByte used for simplicity. Other common functions - WriteInt16 and WriteSingle
writer.WriteByte(0x01);

GattCommunicationStatus result = await selectedCharacteristic.WriteValueAsync(writer.DetachBuffer());
if (result == GattCommunicationStatus.Success)
{
    // Successfully wrote to device
}

小窍门

使用从许多蓝牙 API 获取的原始缓冲区时,DataReaderDataWriter 是不可或缺的。

订阅通知

确保该特征支持 IndicateNotify 中的任意一种(请检查该特征的属性以确认)。

Indicate 被视为更可靠,因为每个值更改事件都与来自客户端设备的确认相耦合。 Notify 更为普遍,因为大多数 GATT 事务更倾向于节省功耗,而不是追求极高的可靠性。 在任何情况下,所有操作都在控制器层进行处理,因此应用不涉及。 我们将将它们统称为“通知”。

在收到通知之前,需要注意两件事:

  • 写入客户端特征配置描述符(CCCD)
  • 处理 Characteristic.ValueChanged 事件

写入 CCCD 会告知服务器设备,该客户端希望在该特征值每次发生变化时都获知。 为此,请按以下步骤操作:

GattCommunicationStatus status = await selectedCharacteristic.WriteClientCharacteristicConfigurationDescriptorAsync(
                        GattClientCharacteristicConfigurationDescriptorValue.Notify);
if(status == GattCommunicationStatus.Success)
{
    // Server has been informed of clients interest.
}

现在,每次远程设备上更改值时,GattCharacteristic 的 ValueChanged 事件都会被调用。 现在只剩下实现处理程序了:

characteristic.ValueChanged += Characteristic_ValueChanged;

...

void Characteristic_ValueChanged(GattCharacteristic sender,
                                    GattValueChangedEventArgs args)
{
    // An Indicate or Notify reported that the value has changed.
    var reader = DataReader.FromBuffer(args.CharacteristicValue)
    // Parse the data however required.
}

示例

有关完整示例,请参阅 Bluetooth Low Energy 示例