蓝牙 GATT 客户端Bluetooth GATT Client

本文演示用于通用 Windows 平台 (UWP) 应用的蓝牙通用属性 (GATT) 客户端 API 的用法,以及用于常见 GATT 客户端任务的示例代码:This article demonstrates usage of the Bluetooth Generic Attribute (GATT) Client APIs for Universal Windows Platform (UWP) apps, along with sample code for common GATT client tasks:

  • 查询附近设备Query for nearby devices
  • 连接到设备Connect to device
  • 枚举设备的受支持服务和特征Enumerate the supported services and characteristics of the device
  • 读取和写入特征Read and write to a characteristic
  • 订阅特征值更改时的通知Subscribe for notifications when characteristic value changes

重要

必须在 appxmanifest.xml中声明 "蓝牙" 功能。You must declare the "bluetooth" capability in Package.appxmanifest.

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

重要的 APIImportant APIs

概述Overview

开发人员可使用 Windows.Devices.Bluetooth.GenericAttributeProfile 命名空间中的 API 访问蓝牙 LE 设备。Developers can use the APIs in the Windows.Devices.Bluetooth.GenericAttributeProfile namespace to access Bluetooth LE devices. 蓝牙 LE 设备通过以下内容的集合公开其功能:Bluetooth LE devices expose their functionality through a collection of:

  • 服务Services
  • 特征Characteristics
  • 描述符Descriptors

服务定义 LE 设备的功能合约,并且包含定义该服务的特征的集合。Services define the functional contract of the LE device and contain a collection of characteristics that define the service. 这些特征反过来包括描述特征的描述符。Those characteristics, in turn, contain descriptors that describe the characteristics. 这 3 个术语通常称为设备的属性。These 3 terms are generically known as the attributes of a device.

蓝牙 LE GATT API 公开对象和函数,而不是对原始传输的访问权限。The Bluetooth LE GATT APIs expose objects and functions, rather than access to the raw transport. GATT API 还使开发人员可以在能够执行以下任务的情况下使用蓝牙 LE 设备:The GATT APIs also enable developers to work with Bluetooth LE devices with the ability to perform the following tasks:

  • 执行属性发现Perform attribute discovery
  • 读取和写入属性值Read and Write attribute values
  • 为 Characteristic ValueChanged 事件注册回调Register a callback for Characteristic ValueChanged event

要创建有用的实现,开发人员必须具有应用程序要使用的 GATT 服务和特征的前期知识,并且必须处理特征的特征值,以便由 API 提供的二进制数据在向用户呈现之前转换为有用的数据。To create a useful implementation a developer must have prior knowledge of the GATT services and characteristics the application intends to consume and to process the specific characteristic values such that the binary data provided by the API is transformed into useful data before being presented to the user. 蓝牙 GATT API 仅公开与蓝牙 LE 设备进行通信所需的基本基元。The Bluetooth GATT APIs expose only the basic primitives required to communicate with a Bluetooth LE device. 要解释数据,应用程序配置文件必须经蓝牙 SIG 标准配置文件定义,或经由设备供应商实现的自定义配置文件定义。To interpret the data, an application profile must be defined, either by a Bluetooth SIG standard profile, or a custom profile implemented by a device vendor. 配置文件创建应用程序和设备之间的绑定合约,此合约关于交换的数据所代表的内容以及如何解释它。A profile creates a binding contract between the application and the device, as to what the exchanged data represents and how to interpret it.

为方便起见,蓝牙 SIG 将持续提供公共配置文件的列表For convenience the Bluetooth SIG maintains a list of public profiles available.

查询附近设备Query for nearby devices

可通过两个主要方法查询附近设备:There are two main methods to query for nearby devices:

  • Windows.Devices.Enumeration 中的 DeviceWatcherDeviceWatcher in Windows.Devices.Enumeration
  • Windows.Devices.Bluetooth.Advertisement 中的 AdvertisementWatcherAdvertisementWatcher in Windows.Devices.Bluetooth.Advertisement

第二个方法在播发文档中进行了详细讨论,因此在此处不会进行深入讨论,但基本思路是查找满足特定播发筛选器的附近设备的蓝牙地址。The 2nd method is discussed at length in the Advertisement documentation so it won't be discussed much here but the basic idea is to find the Bluetooth address of nearby devices that satisfy the particular Advertisement Filter. 拥有地址之后,便可以调用 BluetoothLEDevice.FromBluetoothAddressAsync 以获取对设备的引用。Once you have the address, you can call BluetoothLEDevice.FromBluetoothAddressAsync to get a reference to the device.

现在返回到 DeviceWatcher 方法。Now, back to the DeviceWatcher method. 蓝牙 LE 设备就像 Windows 中的任何其他设备一样,可以使用枚举 API 进行查询。A Bluetooth LE device is just like any other device in Windows and can be queried using the Enumeration APIs. 使用 DeviceWatcher 类,并传入指定要查找的设备的查询字符串:Use the DeviceWatcher class and pass a query string specifying the devices to look for:

// 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 事件处理程序中的查询的每个设备,你都会收到 DeviceInformationOnce you've started the DeviceWatcher, you will receive DeviceInformation for each device that satisfies the query in the handler for the Added event for the devices in question. 有关 DeviceWatcher 的更详细介绍,请参阅 Github 上的完整示例。For a more detailed look at DeviceWatcher see the complete sample on Github.

连接到设备Connecting to the device

发现所需设备之后,使用 DeviceInformation.Id 获取相关设备的蓝牙 LE 设备对象:Once a desired device is discovered, use the DeviceInformation.Id to get the Bluetooth LE Device object for the device in question:

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 对象的所有引用的释放(以及如果系统上没有其他应用拥有对设备的引用)都会在一小段超时时间之后触发自动断开连接。On the other hand, disposing of all references to a BluetoothLEDevice object for a device (and if no other app on the system has a reference to the device) will trigger an automatic disconnect after a small timeout period.

bluetoothLeDevice.Dispose();

如果应用需要再次访问设备,则仅仅重新创建设备对象并访问特征(在下一部分中讨论)会触发操作系统在需要时重新连接。If the app needs to access the device again, simply re-creating the device object and accessing a characteristic (discussed in the next section) will trigger the OS to re-connect when necessary. 如果设备处于附近,则可以访问设备,否则它会返回并具有 DeviceUnreachable 错误。If the device is nearby, you'll get access to the device otherwise it will return w/ a DeviceUnreachable error.

备注

通过单独调用此方法创建 BluetoothLEDevice 对象不 (一定) 发起连接。Creating a BluetoothLEDevice object by calling this method alone doesn't (necessarily) initiate a connection. 若要启动连接,请将 GattSession 设置为 true ,或调用 BluetoothLEDevice上的未缓存服务发现方法,或对设备执行读/写操作。To initiate a connection, set GattSession.MaintainConnection to true, or call an uncached service discovery method on BluetoothLEDevice, or perform a read/write operation against the device.

  • 如果将 GattSession 设置为 true,则系统会无限期地等待连接,并在设备可用时连接。If GattSession.MaintainConnection is set to true, then the system waits indefinitely for a connection, and it will connect when the device is available. 应用程序无需等待,因为 MaintainConnection 是一个属性。There's nothing for your application to wait on, since GattSession.MaintainConnection is a property.
  • 对于 GATT 中的服务发现和读/写操作,系统会等待有限但可变的时间。For service discovery and read/write operations in GATT, the system waits a finite but variable time. 从瞬间到几分钟的任何内容。Anything from instantaneous to a matter of minutes. 因素包括堆栈上的流量,以及排队的请求数。Factors inclue the traffic on the stack, and how queued up the request is. 如果没有其他挂起的请求,并且无法访问远程设备,则系统会在超时前等待 7 (7) 秒。如果存在其他挂起的请求,则队列中的每个请求可能需要 7 (7) 秒才能完成,因此,您的用户可能会在队列中等待,等待时间越长。If there are no other pending request, and the remote device is unreachable, then the system will wait for seven (7) seconds before it times out. If there are other pending requests, then each of the requests in the queue can take seven (7) seconds to process, so the further yours is toward the back of the queue, the longer you'll wait.

当前无法取消连接过程。Currently, you can't cancel the connection process.

枚举受支持的服务和特征Enumerating supported services and characteristics

现在你拥有 BluetoothLEDevice 对象,下一步是发现设备公开的数据。Now that you have a BluetoothLEDevice object, the next step is to discover what data the device exposes. 执行此操作的第一步是查询服务:The first step to do this is to query for services:

GattDeviceServicesResult result = await bluetoothLeDevice.GetGattServicesAsync();

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

确定了感兴趣的服务之后,下一步是查询特征。Once the service of interest has been identified, the next step is to query for characteristics.

GattCharacteristicsResult result = await service.GetCharacteristicsAsync();

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

操作系统返回你随后可以对其执行操作的 GattCharacteristic 对象的 ReadOnly 列表。The OS returns a ReadOnly list of GattCharacteristic objects that you can then perform operations on.

对特征执行读取/写入操作Perform Read/Write operations on a characteristic

特征是基于 GATT 的通信的基本单元。The characteristic is the fundamental unit of GATT based communication. 它包含表示设备上一段不同数据的值。It contains a value that represents a distinct piece of data on the device. 例如,电池电量特征具有表示设备电池电量的值。For example, the battery level characteristic has a value that represents the battery level of the device.

读取特征属性以确定支持的操作:Read the characteristic properties to determine what operations are supported:

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.
}

如果支持读取,则可以读取值:If read is supported, you can read the value:

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
}

写入特征遵循类似模式:Writing to a characteristic follows a similar pattern:

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 是或缺的。Tip: DataReader and DataWriter are indispensible when working with the raw buffers you get from many of the Bluetooth APIs.

订阅通知Subscribing for notifications

确保特征支持“指示”或“通知”(检查特征属性以进行确保)。Make sure the characteristic supports either Indicate or Notify (check the characteristic properties to make sure).

旁白:“指示”被视为更可靠,因为每个值更改事件都与来自客户端设备的确认相结合。Aside: Indicate is considered more reliable because each value changed event is coupled with an acknowledgement from the client device. “通知”更为普遍,因为大多数 GATT 事务宁愿节省电量,而不是非常可靠。Notify is more prevalent because most GATT transactions would rather conserve power rather than be extremely reliable. 在任何情况下,它们全部在控制器层进行处理,因此不会涉及到应用。In any case, all of that is handled at the controller layer so the app does not get involved. 但现在你知道,我们将它们简单地统称为“通知”。We'll collectively refer to them as simply "notifications" but now you know.

获取通知之前要考虑两个事项:There are two things to take care of before getting notifications:

  • 写入客户端特征配置描述符 (CCCD)Write to Client Characteristic Configuration Descriptor (CCCD)
  • 处理 Characteristic.ValueChanged 事件Handle the Characteristic.ValueChanged event

写入 CCCD 可向服务器设备告知此客户端要在该特定特征值每次更改时收到通知。Writing to the CCCD tells the Server device that this client wants to know each time that particular characteristic value changes. 要执行此操作:To do this:

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

现在,每当在远程设备上更改值时,便会调用 GattCharacteristic 的 ValueChanged 事件。Now, the GattCharacteristic's ValueChanged event will get called each time the value gets changed on the remote device. 只需实现处理程序即可:All that's left is to implement the handler:

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.
}