BLE(低功耗蓝牙)和 IPWorks BLE 入门
本文涵盖了低功耗蓝牙 (BLE) 的一般概念,并提供了使用 / nsoftware IPWorks BLE 的实用说明
我们建议下载并安装 IPWorks BLE 以跟随本文。提供 .NET 和 Delphi 的免费试用版。
BLE(低功耗蓝牙)和 IPWorks BLE 简介
IPWorks BLE 包含一个组件,即 BLEClient
,它提供了对低功耗蓝牙 (BLE) 操作的直接访问。BLEClient
组件使得处理远程 BLE 设备上 GATT 服务器公开的服务、特性和描述符变得容易。
本文将首先概述低功耗蓝牙和 GATT 数据模型,然后深入探讨如何使用 BLEClient
组件。通过阅读本文,您将学会如何:
- 扫描并连接到远程 BLE 设备上的 GATT 服务器
- 发现服务、特性和描述符(统称为“GATT 对象”)
- 在
BLEClient
中导航已发现的 GATT 对象层次结构 - 从 GATT 服务器读取、写入和发布数据
- 使用特性订阅来利用实时数据更新
本文使用 TI SensorTag 作为远程设备(型号 CC2650STK 固件版本 1.43)。本文中的所有代码片段均用 C# 编写,并且在下面的代码块中提供了变量。文章中的所有屏幕截图均描绘了 IPWorks BLE .NET Edition 工具包中包含的“bleclient”演示应用程序。
//Global Variables
const string TI_SENSORTAG_ID = "546C0E795800"; // The Server Id of our TI SensorTag device.
private Bleclient bleclient1 = new Bleclient();
低功耗蓝牙概述
什么是低功耗蓝牙?
在讨论低功耗蓝牙 (BLE) 是什么之前,重要的是要指出它不是什么。许多人已经熟悉俗称的“经典蓝牙”;它自 1990 年代后期问世以来,经历了多次迭代。由于 BLE 共享“蓝牙”名称,一个常见的误解是它是经典蓝牙的“功能”。实际上,BLE 与经典蓝牙几乎没有共同之处,这可能会让人感到惊讶;它是一种独立的、拥有自己协议和技术的功能。
BLE 大约在 2010 年推出,从一开始就被设计为尽可能地节省能源,并使用尽可能低成本的组件。与经典蓝牙可以长时间传输大量数据不同,BLE 适用于只需要周期性传输小块数据的使用场景。
因此,BLE 不针对经典蓝牙已经表现出色的领域;它旨在实现新的用例。最好的例子就是“物联网”(IoT);物联网设备的普及率持续上升,BLE 是连接它们的完美技术。
BLE 数据模型:GATT
属性 (ATT) 协议
BLE 规范中最基本的客户端/服务器关系由一种称为属性 (ATT) 协议定义,其数据模型建立在属性概念之上。在 ATT 协议的数据模型中,有一个服务器包含属性,一个客户端可以向服务器发送请求来与这些属性进行交互。属性本身存储在一个平面表中,它们由以下四个部分组成:
- 一个 16 位属性句柄(称为 Id),用于唯一标识服务器上的属性
- 一个值,即属性实际持有的数据
- 一个 UUID,一个通用唯一 ID,指定值中包含的数据类型
- 一组权限,适用于所有客户端
在实践中,ATT 协议(及其数据模型)的级别有点太低,不实用。相反,该组件允许您使用基于通用属性 (GATT) 配置文件的配置文件。然而,由于 GATT 配置文件构建在 ATT 协议之上,因此理解属性的基本知识以及 GATT 数据模型继承了 ATT 协议的客户端/服务器架构是很有帮助的。
通用属性 (GATT) 配置文件
在 BLE 术语中,“配置文件”通常指一项规范,除其他外,它赋予具有与该配置文件关联的 UUID 的任何服务、特性或描述符以意义。例如,与心率配置文件关联的 UUID 赋予心率服务、其特性以及这些特性的描述符以意义。
但让我们退一步——服务、特性和描述符究竟是什么?它们之间如何关联?属性又如何融入其中?GATT 配置文件回答了所有这些问题,甚至更多。
我们提到过,BLE 的核心数据单元是属性,每个属性都有一个 UUID,并且服务器上的所有属性都存储在一个平面表中。通过赋予某些 UUID 意义,GATT 配置文件能够将服务器属性表中的一系列属性解释为“服务”、“特性”或“描述符”(通常称为“GATT 对象”)。换句话说,GATT 配置文件指定了服务、特性和描述符的通用定义。
所有其他 BLE 配置文件都从 GATT 配置文件扩展而来,因此它们都使用 GATT 配置文件定义的服务、特性和描述符的概念。通过强制执行一个通用的框架来解释属性、发现 GATT 对象、数据操作等;GATT 配置文件确保任何 GATT 客户端都可以与任何 GATT 服务器通信。正因为如此,所有BLE 设备都必须支持 GATT 配置文件。
我们将在文章后面讨论发现、数据操作等。现在,我们将通过描述服务、特性和描述符来结束我们的 BLE 概述。
服务
服务是 GATT 层次结构中的顶级“容器”对象,并且相当简单。每个服务都有一个 Id(回想一下 GATT 对象在 ATT 层只是属性范围)和一个 UUID,并且至少包含一个特性。服务还可以引用其他服务,这些服务被称为“包含服务”。配置文件通常定义少量服务(其中许多只定义一个)。
请注意,GATT 服务器包含同一服务的多个实例是可能且完全合法的(事实上,服务中的特性或特性上的描述符也可能发生类似的重复)。正因为如此,当需要唯一标识时,BLEClient
使用 Id 而不是 UUID。
Characteristics
特性总是由服务拥有,是数据在 GATT 层次结构中实际存在的地方。它们有一个 Id、一个 UUID、属性(称为“标志”)和一个值。它们还可以附加零个或多个描述符,每个描述符都提供关于特性的一些额外元数据。
特性是 GATT 对象中最复杂和最多样化的类型;配置文件倾向于为每个服务定义多个特性。
描述符
描述符是 GATT 对象中最简单且最不多样化的,只有 Id、UUID 和值。它们附加到特性上,以便用特定的元数据片段增强它们。
许多描述符实际上由核心 BLE 规范(而非特定配置文件)定义,并被许多配置文件使用;很少有配置文件定义自己的描述符。例如,“特性用户描述”描述符可以应用于特性以公开描述该特性的用户友好字符串。
扫描
与任何远程 BLE 设备交互的第一步是让 BLEClient
开始扫描广告。广告是 BLE 服务器发送的数据包,用于向客户端提供各种信息。BLE 规范定义了许多不同的广告字段;一些更常见的是服务器的 Id 和名称、支持服务的 UUID、服务器是否接受连接以及制造商数据。
BLEClient
提供了以下用于扫描和广告的 API
StartScanning
和StopScanning
方法控制扫描状态。Scanning
属性返回当前的扫描状态。StartScan
和StopScan
事件在扫描状态发生变化时触发。Advertisement
事件在扫描期间每次收到广告时触发。- 可以启用
ActiveScanning
属性以请求来自具有更详细信息的设备的扫描响应。 ServiceData
和ManufacturerData*
配置设置可用于从广告中获取额外信息(如果适用)。
首先,调用 StartScanning
方法,它接受一个字符串参数;如果您传递一个逗号分隔的服务 UUID 列表,BLEClient
将指示系统过滤掉任何未广播支持所有指定 UUID 的设备。扫描开始时将触发 StartScan
事件。
要扫描所有服务,请将空字符串传递给 StartScanning
。要仅扫描特定服务,请将逗号分隔的服务 UUID 列表传递给 StartScanning
。
当 BLEClient
正在扫描时,每次系统接收到广告事件时,Advertisement
事件都会触发。至少,ServerId
事件参数将被填充;其余事件参数根据广告数据包中实际存在的数据进行填充。
调用 StopScanning
方法来结束扫描。请注意,在尝试连接到服务器时,如有必要,扫描会自动停止。如果应用程序进入后台或在其他系统特定情况下,扫描也可能停止。当扫描停止时,StopScan
事件将触发。
基本扫描和广告处理示例
// StartScan event handler.
bleclient1.OnStartScan += (s, e) => Console.WriteLine("Scanning has started");
// StopScan event handler.
bleclient1.OnStopScan += (s, e) => Console.WriteLine("Scanning has stopped");
// Advertisement event handler.
bleclient1.OnAdvertisement += (s, e) => {
// Your application should make every effort to handle the Advertisement event quickly.
// BLEClient fires it as often as necessary, often multiple times per second.
Console.WriteLine("Advertisement Received:" +
"\r\n\tServerId: " + e.ServerId +
"\r\n\tName: " + e.Name +
"\r\n\tRSSI: " + e.RSSI +
"\r\n\tTxPower: " + e.TxPower +
"\r\n\tServiceUuids: " + e.ServiceUuids +
"\r\n\tServicesWithData: " + e.ServicesWithData +
"\r\n\tSolicitedServiceUuids: " + e.SolicitedServiceUuids +
"\r\n\tManufacturerCompanyId: " + e.ManufacturerCompanyId +
// We use BitConverter.ToString() to print data as hex bytes; this also prevents
// an issue where the string could be cut off early if the data has a 0 byte in it.
"\r\n\tManufacturerCompanyData: " + BitConverter.ToString(e.ManufacturerDataB) +
"\r\n\tIsConnectable: " + e.IsConnectable +
"\r\n\tIsScanResponse: " + e.IsScanResponse);
};
// Scan for all devices.
bleclient1.StartScanning("");
// Wait a while...
bleclient1.StopScanning();
过滤扫描示例
// Scan for devices which are advertising at least these UUIDs. You can use a mixture
// of 16-, 32-, and 128-bit UUID strings, they'll be converted to 128-bit internally.
bleclient1.StartScanning("180A,0000180F,00001801-0000-1000-8000-00805F9B34FB");
// ...
bleclient1.StopScanning();
ActiveScanning
属性可以在调用 StartScanning
之前设置。主动扫描与被动扫描的区别在于,对于每个接收到的广告数据包,系统将请求额外发送一个“扫描响应”数据包。远程设备通常会将不同的数据放置在扫描响应数据包中。Advertisement
事件的 IsScanResponse
参数指示事件触发的数据包是普通广告还是扫描响应。
主动扫描示例
// Enable active scanning.
bleclient1.ActiveScanning = true;
bleclient1.StartScanning("");
// ...
bleclient1.StopScanning();
请注意,扫描响应数据包的 IsConnectable
事件参数始终为 false
。另请注意,并非所有平台都支持指定使用主动扫描还是被动扫描。对于此类平台,UseActiveScanning
配置设置将不可用,并且 IsScanResponse
事件参数将始终为 false
。
以下是一些如何使用 IPWorks BLE .NET Edition 中的 BLEClient 演示扫描设备的示例。
BLEClient 演示:基本和过滤扫描
BLEClient 演示:主动扫描
连接与断开
BLEClient
提供了以下 API,用于管理连接和获取有关当前连接服务器的信息
Connect
和Disconnect
方法。Connected
和Disconnected
事件。ServerId
和ServerName
属性。ServerUpdate
事件,在服务器名称更改时触发。
连接就像调用 Connect
方法并传入您希望连接的服务器的 Id 一样简单。如有必要,BLEClient
将自动停止扫描并断开与当前连接服务器的连接,然后尝试连接到所需服务器。
请记住,分配给 BLE 设备的服务器 Id 不一定是稳定的;在您未连接期间它可能已经改变。陈旧的服务器 Id 和超出范围的设备是连接尝试失败的两个最常见原因。
要断开与 BLE 服务器的连接,请调用 Disconnect
。这将清除 Services
、Characteristics
和 Descriptors
集合属性中已发现的 GATT 对象,并让系统知道它可以清除与设备连接相关的资源。
连接和断开连接示例
// Connect to our TI SensorTag device.
bleclient1.Connect(TI_SENSORTAG_ID);
// Use BLEClient...
// Disconnect from the device.
bleclient1.Disconnect();
请注意,在某些平台上,系统可能不会实际打开与设备的连接,直到您尝试对其进行某种操作,例如发现。类似地,当您调用 Disconnect
时,系统可能不会立即关闭与设备的连接,特别是如果其他应用程序仍在使用它。
如果无法与您知道应该能够连接的设备建立连接,请确保该设备在范围内、已开机并配置为接受连接。有些设备会宣传自己可连接,但无论如何都会拒绝大多数连接(例如,BLE 鼠标可能会宣传自己可连接,但如果它已经连接到另一个客户端,则不允许连接)。
下面是一个示例,展示如何使用 IPWorks BLE .NET Edition 中的 BLEClient 演示连接到设备。
BLEClient 演示:连接
发现
正如我们在 GATT 配置文件讨论中提到的,GATT 客户端在处理 GATT 服务器公开的服务、特性和描述符之前,必须先发现它们。由于能效是 BLE 的核心关注点,因此客户端通常应仅在需要时才尝试发现所需的 GATT 对象。
BLEClient
提供了以下用于发现 GATT 对象的 API
DiscoverServices
、DiscoverCharacteristics
和DiscoverDescriptors
方法,用于对发现过程进行精细控制。Discover
方法,用于启动多级发现过程。Discovered
事件,每次发现服务、特性或描述符时触发。IncludeRediscovered
配置设置,指定是否应为已发现的 GATT 对象再次触发Discovered
事件(默认启用)。AutoDiscoverCharacteristics
、AutoDiscoverDescriptors
和AutoDiscoverIncludedServices
配置设置,可以启用这些设置以使BLEClient
在发现过程中自动发现其他 GATT 对象(默认全部禁用)。
注意:有关已发现 GATT 对象存储位置的信息,请参阅导航已发现数据。
需要注意的是,服务必须在特性之前被发现。特性必须在描述符之前被发现。在发现包含服务之前尝试发现特性将导致错误。
服务发现
服务是第一个必须发现的 GATT 对象。DiscoverServices
方法用于发现根服务和包含服务,并接受一个逗号分隔的服务 UUID 列表,用于过滤发现过程。
每发现一个新的服务,无论是根服务还是包含服务,BLEClient
都会向 Services
集合属性添加一个项并触发 Discovered
事件。服务可以随时发现。无需在连接后立即发现所有服务;建议仅在需要时发现服务以节省能源。
服务发现示例
// Discovered event handler.
bleclient1.OnDiscovered += (s, e) => {
Console.WriteLine("Service discovered:" +
"\r\n\tService Id: " + e.ServiceId +
// The discovered service's 128-bit UUID string, in the format
// "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
"\r\n\tUUID: " + e.Uuid +
// For standard services whose UUIDs are defined by the Bluetooth SIG,
// the Description event parameter will contain the name of the service.
"\r\n\tDescription: " + e.Description);
};
// Discover all root services.
bleclient1.DiscoverServices("", "");
// Discover all services included by a discovered service whose Id is "000100000000".
// (The TI SensorTag doesn't have any included services, this is just an example.)
bleclient1.DiscoverServices("", "000100000000");
过滤服务发现示例
// Discover specific root services. These three UUIDs will cause the Device Information,
// Luxometer, and Humidity services to be discovered on our CC2650STK TI SensorTag.
// (Since the latter two are non-standard, you have to use their full UUIDs.)
bleclient1.DiscoverServices("180A,F000AA70-0451-4000-B000-000000000000,F000AA20-0451-4000-B000-000000000000", "");
// Discover specific services included by a discovered service whose Id is "000100000000".
// (The TI SensorTag doesn't have any included services, this is just an example.)
bleclient1.DiscoverServices("FFFFFFFF-9000-4000-B000-000000000000", "000100000000")
以下是一些如何使用 IPWorks BLE .NET Edition 中的 BLEClient 演示发现服务的示例。
BLEClient 演示:服务发现
BLEClient 演示:过滤服务发现
特性发现
发现服务后,您可以使用 DiscoverCharacteristics
方法发现其特性。它接受您希望发现其特性的服务的 Id,以及一个逗号分隔的特性 UUID 列表,用于过滤发现过程。
每发现一个新的特性,BLEClient
都会向 Characteristics
集合属性添加一个项(有关其工作原理的更多信息,请参阅导航已发现数据部分)并触发 Discovered
事件。特性可以随时发现;您不限于在发现服务后立即发现它们,而应优先仅在需要时发现它们以节省能源。
特性发现示例
// Discovered event handler.
bleclient1.OnDiscovered += (s, e) => {
Console.WriteLine("Characteristic discovered:" +
"\r\n\tOwning Service Id: " + e.ServiceId +
"\r\n\tCharacteristic Id: " + e.CharacteristicId +
// The discovered characteristic's 128-bit UUID string, in the format
// "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
"\r\n\tUUID: " + e.Uuid +
// For standard characteristics whose UUIDs are defined by the Bluetooth SIG,
// the Description event parameter will contain the name of the characteristic.
"\r\n\tDescription: " + e.Description);
};
// Discover all characteristics for the service whose Id is "000900000000".
// (On our CC2650STK TI SensorTag, this is the Device Information Service.)
bleclient1.DiscoverCharacteristics("000900000000", "");
过滤特性发现示例
// Discover specific characteristics for the service whose Id is "000900000000".
// (On our CC2650STK TI SensorTag, this is the Device Information Service.)
// This will cause the System Id, Model Number String, and Manufacturer Name String
// characteristics to be discovered, respectively.
bleclient1.DiscoverCharacteristics("000900000000", "2A23,2A24,2A29");
以下是一些如何使用 IPWorks BLE .NET Edition 中的 BLEClient 演示发现特性的示例。
BLEClient 演示:特性发现
BLEClient 演示:过滤特性发现
描述符发现
最后,您可以使用 DiscoverDescriptors
方法发现已发现特性的描述符,该方法接受所属服务和特性的 Id。
每发现一个新的描述符,BLEClient
都会向 Descriptors
集合属性添加一个项(有关其工作原理的更多信息,请参阅导航已发现数据部分)并触发 Discovered
事件。描述符可以随时发现;您不限于在发现特性后立即发现它们,而应优先仅在需要时发现它们以节省能源。
请注意,在某些条件下,BLEClient
可能会自动尝试发现特定特性的描述符。有关更多信息,请参阅惰性初始化特性字段部分。
描述符发现示例
// Discovered event handler.
bleclient1.OnDiscovered += (s, e) => {
Console.WriteLine("Descriptor discovered:" +
"\r\n\tOwning Service Id: " + e.ServiceId +
"\r\n\tOwning Characteristic Id: " + e.CharacteristicId +
"\r\n\tDescriptor Id: " + e.DescriptorId +
// The discovered descriptor's 128-bit UUID string, in the format
// "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
"\r\n\tUUID: " + e.Uuid +
// For standard descriptors whose UUIDs are defined by the Bluetooth SIG,
// the Description event parameter will contain the name of the descriptor.
"\r\n\tDescription: " + e.Description);
};
// Discover all descriptors for characteristic Id "001C001D0000", owned by
// service Id "001C00000000". (On our CC2650STK TI SensorTag, this is the
// Battery Level characteristic, which is owned by the Battery Service.)
bleclient1.DiscoverDescriptors("001C00000000", "001C001D0000");
下面是一个示例,展示如何使用 IPWorks BLE .NET Edition 中的 BLEClient 演示发现描述符。
BLEClient 演示:描述符发现
多级发现
虽然仅在需要时发现 GATT 对象是最节能的,但在某些情况下,能够一次发现多个级别的 GATT 对象会很有帮助。对于这些情况,BLEClient
提供了几个选项。
第一个选项是使用 Discover
方法,该方法接受以下四个参数:
ServiceUuids
- 逗号分隔的服务 UUID 列表,用于限制服务发现步骤。CharacteristicUuids
- 逗号分隔的特性 UUID 列表,用于限制所有已发现服务的特性发现步骤。DiscoverDescriptors
- 一个布尔值,用于确定是否对所有已发现的特性执行描述符发现步骤。IncludedByServiceId
- 已发现服务的 Id,如果存在,则表示此多级发现应尝试发现由该服务(及其他)包含的服务,而不是根服务。
多级发现示例
string serviceUUIDs = "180A,F000AA70-0451-4000-B000-000000000000,F000AA20-0451-4000-B000-000000000000";
string characteristicUUIDs = ""; //All Characteristics
bool discoverDescriptors = true;
string includedByServiceId = ""; //No included services
// This will discover all characteristics and descriptors for specific services.
bleclient1.Discover(serviceUUIDs, characteristicUUIDs, discoverDescriptors, includedByServiceId);
// This will discover everything on the server, but won't discover
// the "includes"/"included by" service relationships.
bleclient1.Discover("", "", true, "");
第二个选项是使用一个或多个 AutoDiscoverCharacteristics
、AutoDiscoverDescriptors
和 AutoDiscoverIncludedServices
配置设置,以便调用 DiscoverService
、DiscoverCharacteristics
和 DiscoverDescriptors
方法将触发额外的发现步骤。
AutoDiscoverIncludedServices 示例
// This will cause BLEClient to attempt to discover all root and included services.
bleclient1.Config("AutoDiscoverIncludedServices=True");
bleclient1.DiscoverServices("", "");
// Note that these configuration settings also technically affect the Discover() method too.
// So with the "AutoDiscoverIncludedServices" setting enabled, this call will now discover
// everything on the server, _with_ the "includes"/"included by" service relationships.
bleclient1.Discover("", "", true, "");
下面是一个示例,展示如何使用 IPWorks BLE .NET Edition 中的 BLEClient 演示进行多级发现。
导航已发现数据
发现完您感兴趣的 GATT 对象后,就该开始使用它们了。BLEClient
提供了以下 API,用于在您发现的 GATT 对象之间导航并获取其信息:
对于服务
Services
集合属性,其中包含一个表示所有已发现服务(根服务或包含服务)的项列表。每个项都具有以下字段:Id
- 此服务的 Id。Uuid
- 此服务的 UUID。Description
- 此服务的用户友好名称,如果它是蓝牙 SIG 定义的标准服务。IncludedSvcIds
- 此服务包含的服务 Id 的逗号分隔列表。ParentSvcIds
- 包含此服务的服务 Id 的逗号分隔列表。
Service
属性,可以设置为已发现服务的 Id 来选择它。
对于特性
Characteristics
集合属性,其中包含一个项目列表,表示为当前由Service
属性选择的服务发现的所有特性。每个项目都具有以下字段Id
- 此特性的 Id。Uuid
- 此特性的 UUID。Description
- 此特性的用户友好名称,如果它是蓝牙 SIG 定义的标准特性。Flags
和CanSubscribe
- 此特性的标志位字段,以及一个便捷字段,如果此特性具有Notify
或Indicate
标志中的任何一个,则为true
。Subscribed
- 您是否当前订阅了此特性。CachedValue
- 系统为此特性缓存的最新值。UserDescription
- 如果此特性存在用户描述符,则此字段将包含其值。ValueFormatCount
、ValueFormatIndex
、ValueFormat
、ValueExponent
和ValueUnit
- 如果此特性存在任何特性表示格式和/或特性聚合格式描述符,则可以使用这些字段获取其值。
Characteristic
属性,可以设置为已发现特性的 Id 来选择它。
对于描述符
Descriptors
集合属性,其中包含一个项目列表,表示为当前由Characteristic
属性选择的特性发现的所有描述符。每个项目都具有以下字段Id
- 此描述符的 Id。Uuid
- 此描述符的 UUID。Description
- 此描述符的用户友好名称,如果它是蓝牙 SIG 定义的标准描述符。CachedValue
- 系统为此描述符缓存的最新值。
正如您所看到的,GATT 对象被组织成树状层次结构。要导航该层次结构,您首先需要遍历 Services
集合属性。这允许您检查已发现服务的信息。
如果您希望处理为某个服务发现的特性,请将 Service
属性设置为该服务的 Id。这将导致 Characteristics
集合属性被填充,您可以遍历它来检查特性信息。
最后,您可以将 Characteristic
属性设置为已发现特性的 Id。这将用表示该特性已发现描述符的项填充 Descriptors
集合属性。
导航已发现数据示例
// Loop through all discovered GATT objects and print out their UUIDs and descriptions.
foreach (Service s in bleclient1.Services) {
Console.WriteLine("Service: " + s.Description + " (" + s.Uuid + ")");
// Select this service and loop through its characteristics.
bleclient1.Service = s.Id;
foreach (Characteristic c in bleclient1.Characteristics) {
Console.WriteLine("\tCharacteristic: " + c.Description + " (" + c.Uuid + ")");
// Select this characteristic and loop through its descriptors.
bleclient1.Characteristic = c.Id;
foreach (Descriptor d in bleclient1.Descriptors) {
Console.WriteLine("\t\tDescriptor: " + d.Description + " (" + d.Uuid + ")");
}
}
}
BLEClient 演示:导航已发现数据
惰性初始化特性字段
如上所述,Characteristics
集合属性中某些项的字段是根据特定描述符(如果该描述符存在于特性中)持有的值进行初始化的。由于在初始化这些字段之前必须尝试发现与其关联的描述符,因此它们是惰性初始化的。
BLEClient
会在与设备连接期间跟踪每个特性的描述符发现尝试。当您首次访问惰性初始化字段时,BLEClient
将自动尝试发现其关联的描述符(必要时触发 Discovered
事件),除非已经进行了此类尝试。无论哪种情况,一旦尝试完成,该字段即可初始化。
这种行为的一个很好的例子可以在描述符发现部分使用的 BLEClient 演示的屏幕截图中看到;当点击“电池电量”特性时,“特性表示格式”和“客户端特性配置”描述符会自动发现。
下表显示了 Characteristics
集合属性中的哪些字段是惰性初始化的,以及与它们关联的描述符
字段 | 关联的描述符 |
Flags (以及任何依赖它的字段) | 特性扩展属性 (0x2900) |
已订阅 | 客户端特性配置 (0x2902) |
UserDescription | 特性用户描述 (0x2901) |
ValueFormatCount 、ValueFormatIndex 、ValueFormat 、ValueExponent 和 ValueUnit | 特性表示格式 (0x2904), 特性聚合格式 (0x2905) |
读取数据
虽然仅通过导航 GATT 对象树就可以了解很多信息,但处理服务器上实际数据的能力同样重要。BLEClient
提供了两种读取特性和描述符值的方法,旨在用于不同的情况。
读取值的第一个方法,前面提到过,是使用 Characteristics
和 Descriptors
集合属性中的项所公开的 CachedValue
字段。当您查询 CachedValue
字段时,BLEClient
会返回系统内置值缓存当前存储的任何内容。
第二种读取值的方法自然是直接从服务器设备读取。这通过使用 ReadValue
方法完成,该方法接受三个参数。要从特性读取,请传递其 Id 以及其所属服务的 Id,并为空描述符 Id 传递空字符串。要从描述符读取,请传递这两个 Id 以及描述符 Id。如果读取请求成功,该值将由 ReadValue
方法返回,并且 Value
事件也将触发。
如果特性的值经常变化,请考虑订阅该特性(如果可能)而不是轮询,以减少功耗。有关更多信息,请参阅特性订阅部分。
读取缓存值示例
// Print the cached value for the Luxometer Data characteristic (which you can
// assume we've already found and assigned to a variable called "luxChara").
byte[] rawLuxVal = luxChara.CachedValueB;
ushort luxVal = BitConverter.ToUInt16(rawLuxVal, 0);
Console.WriteLine("Luxometer Value: " + luxVal);
// Print the cached value for the Client Characteristic Configuration descriptor on
// the Luxometer Data characteristic (again, assume it's stored in "luxCCCD").
byte[] rawLuxCCCD = luxCCCD.CachedValueB;
Console.WriteLine("Luxometer CCCD bytes: " + BitConverter.ToString(rawLuxCCCD));
读取实时值示例
// Value event handler.
bleclient1.OnValue += (s, e) => {
if (string.IsNullOrEmpty(e.DescriptorId)) {
Console.WriteLine("Read value {" + BitConverter.ToString(e.ValueB) +
"} for characteristic with UUID " + e.Uuid);
} else {
Console.WriteLine("Read value {" + BitConverter.ToString(e.ValueB) +
"} for descriptor with UUID " + e.Uuid);
}
};
// Print the live value for the Battery Level characteristic. These Ids
// are correct for our CC2650STK TI SensorTag, but yours might differ.
byte[] rawBatteryVal = bleclient1.ReadValue("001C00000000", "001C001D0000", "");
Console.WriteLine("Battery Level Value: " + (int)rawBatteryVal[0]);
// Print the live value for the Characteristic Presentation Format descriptor on
// the Battery Level characteristic. Again, your Ids might differ.
byte[] rawBatteryPF = bleclient1.ReadValue("001C00000000", "001C001D0000", "001C001D0021");
Console.WriteLine("Battery Level Presentation Format bytes: " + BitConverter.ToString(rawBatteryPF));
BLEClient 演示:读取数据
写入和发布数据
WriteValue
方法可用于向支持此功能的特性和描述符写入值。要写入特性,请传递其 Id、其所属服务的 Id 以及您希望写入的值。要写入描述符,请一并传递其 Id。如果写入成功,则会触发 WriteResponse
事件。
写入值示例
// WriteResponse event handler.
bleclient1.OnWriteResponse += (s, e) => {
if (string.IsNullOrEmpty(e.DescriptorId)) {
Console.WriteLine("Successfully wrote to characteristic with UUID " + e.Uuid);
} else {
Console.WriteLine("Successfully wrote to descriptor with UUID " + e.Uuid);
}
};
// Write to the Luxometer Config characteristic. These Ids are correct
// for our CC2650STK TI SensorTag, but yours might differ.
bleclient1.WriteValue("004200000000", "004200460000", "", new byte[] { 0x1 });
// Write to the Client Characteristic Configuration descriptor on the
// Luxometer Data characteristic. Again, your Ids might differ.
bleclient1.WriteValue("004200000000", "004200430000", "004200430045", new byte[] { 0x1 });
对于具有“无响应写入”标志的特性,您还可以使用 PostValue
方法写入值(描述符不支持此功能)。PostValue
与 WriteValue
的区别在于,即使写入请求失败,服务器也不会发送任何形式的响应。如果您的用例可以适应这种行为,请考虑使用 PostValue
,因为缺乏响应使其比 WriteValue
更节能。
发布值示例
// The CC2650STK TI SensorTag doesn't have any characteristics which support
// write without response, so this is just an example.
bleclient1.PostValue("00AA00000000", "00AA00BB0000", new byte[] { 0x1, 0x2, 0x3 });
BLEClient 演示:写入数据
特性订阅
BLE GATT 数据模型最重要的功能之一是 GATT 服务器能够实时向感兴趣的 GATT 客户端发送特性值更新。这种推式模型避免了轮询的需要,从而提高了能源效率。
对于支持订阅的特性,GATT 客户端可以订阅通知或指示以获取值更新。两者之间的区别很简单:通知不被 GATT 客户端确认,而指示则被确认。请注意,特性不必同时支持两种订阅类型;例如,许多特性只支持通知。
BLEClient
使特性订阅变得简单易用。首先要做的是通过查询相关特性的 CanSubscribe
字段来检查特性是否支持订阅。如果返回 true
,您可以使用以下三种方法之一订阅和取消订阅特性:
- 调用
Subscribe
和Unsubscribe
方法。 - 设置特性的
Subscribed
字段。 - 使用
WriteValue
方法直接写入特性的客户端特性配置描述符 (CCCD)。
无论您选择哪种方法,随着特性的订阅状态发生变化,都会触发 Subscribed
和 Unsubscribed
事件。当您订阅了某个特性后,每当 BLEClient
收到该特性的值更新时,都会触发 Value
事件。
在使用特性订阅时,需要注意以下几点
- 订阅通常不会在连接之间保留。
BLEClient
不限制您可以拥有的并发特性订阅数量,但您的系统可能会。- 对于同时支持通知和指示的特性,
BLEClient
默认会优先选择通知。您可以启用PreferIndications
配置设置来更改新订阅的此行为。- 请注意,当直接写入 CCCD 时,
BLEClient
没有偏好。
- 请注意,当直接写入 CCCD 时,
订阅示例
// Subscribed event handler.
bleclient1.OnSubscribed += (s, e) => {
Console.WriteLine("Subscribed to characteristic:" +
"\r\n\tID: " + e.CharacteristicId +
"\r\n\tUUID: " + e.Uuid +
"\r\n\tDescription: " + e.Description);
};
// Unsubscribed event handler.
bleclient1.OnUnsubscribed += (s, e) => {
Console.WriteLine("Unsubscribed from characteristic:" +
"\r\n\tID: " + e.CharacteristicId +
"\r\n\tUUID: " + e.Uuid +
"\r\n\tDescription: " + e.Description);
};
// Value event handler.
bleclient1.OnValue += (s, e) => {
Console.WriteLine("Value update received for characteristic: " +
"\r\n\tID: " + e.CharacteristicId +
"\r\n\tUUID: " + e.Uuid +
"\r\n\tDescription: " + e.Description +
"\r\n\tValue: " + BitConverter.ToString(e.ValueB));
};
// Assume that we've already found the Luxometer Data characteristic,
// its owning service, and the Client Characteristic Configuration
// descriptor on it; and we've stored them in variables called "luxSvc",
// "luxData", and "luxCCCD".
// Subscribe and unsubscribe using methods.
bleclient1.Subscribe(luxSvc.Id, luxData.Id);
// ...
bleclient1.Unsubscribe(luxSvc.Id, luxData.Id);
// Subscribe and unsubscribe using the "Subscribed" field.
luxData.Subscribed = true;
// ...
luxData.Subscribed = false;
// Subscribe and unsubscribe by writing directly to the CCCD.
bleclient1.WriteValue(luxSvc.Id, luxData.Id, luxCCCD.Id, new byte[] { 1, 0 });
// ...
bleclient1.WriteValue(luxSvc.Id, luxData.Id, luxCCCD.Id, new byte[] { 0, 0 });