蓝牙 GATT 客户端

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

重要

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

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

重要的 API

概述

开发人员可以使用 Windows.Devices.Bluetooth.GenericAttributeProfile 命名空间中的 API 来访问蓝牙 LE 设备。 蓝牙 LE 设备通过集合公开其功能:

  • 服务
  • 特征
  • 描述符

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

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

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

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

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

示例

有关完整示例,请参阅蓝牙低功耗示例

查询附近的设备

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

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

现在,返回到 DeviceWatcher 方法。 蓝牙 LE 设备与 Windows 中的其他任何设备一样,可以使用枚举 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 后,你将收到每个设备的 DeviceInformation,这些设备满足有关设备的“已添加”事件的处理程序中的查询。 有关 DeviceWatcher 的详细信息,请参阅 Github 上的完整示例

连接到设备

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

async void 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 重新连接。 如果设备位于附近,你将访问设备,否则它将返回 w/ a DeviceUnreachable 错误。

注意

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

  • 如果将 GattSession.MaintainConnection 设置为 true,则系统会无限期地等待连接,并在设备可用时连接。 应用程序无需等待,因为 GattSession.MaintainConnection 是一个属性。
  • 对于 GATT 中的服务发现和读/写操作,系统会等待有限但可变的时间。 从瞬间到几分钟的任何内容。 因素包括堆栈上的流量,以及请求排队的方式。 如果没有其他挂起的请求,并且无法访问远程设备,则系统会在超时前等待七 (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
}

订阅通知

确保特征支持指示或通知(检查特征属性以确保)。

指示被视为更可靠,因为每个值更改事件都与来自客户端设备的确认相耦合。 通知更为普遍,因为大多数 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.
}