蓝牙 GATT 服务器
本主题演示如何使用适用于 通用 Windows 平台 (UWP) 应用的蓝牙泛型属性 (GATT) 服务器 API。
重要
必须在 Package.appxmanifest 中声明“蓝牙”功能。
<Capabilities> <DeviceCapability Name="bluetooth" /> </Capabilities>
重要的 API
概述
Windows 通常以客户端角色运行。 然而,许多方案也要求 Windows 充当蓝牙 LE GATT 服务器。 几乎所有 IoT 设备的方案,以及大多数跨平台 BLE 通信都需要 Windows 成为 GATT 服务器。 此外,向附近的可穿戴设备发送通知已成为一种需要这项技术的热门方案。
服务器操作将围绕服务提供商和 GattLocalCharacteristic。 这两个类会提供声明、实现数据层次结构并向远程设备公开所需的功能。
定义受支持的服务
你的应用可以声明一个或多个将由 Windows 发布的服务。 每个服务都由 UUID 唯一标识。
属性和 UUID
每个服务、特征和描述符都由它自己的唯一 128 位 UUID 定义。
Windows API 都使用术语 GUID,但蓝牙标准将这些 API 定义为 UUID。 出于我们的目的,这两个术语是可互换的,因此我们将继续使用术语 UUID。
如果属性是标准的并按照蓝牙 SIG 的定义进行定义,则它还会具有对应的 16 位短 ID(例如,电池电量 UUID 是 00002A19-0000-1000-8000-00805F9B34FB,而短 ID 是 0x2A19)。 可以在 GattServiceUuids 和 GattCharacteristicUuids 中看到这些标准 UUID。
如果你的应用正在实现它自己的自定义服务,则必须生成自定义 UUID。 这可以在 Visual Studio 中通过“工具”->“创建 Guid”轻松实现(使用选项 5 可采用“xxxxxxxx-xxxx-...xxxx”格式获取它)。 此 uuid 现在可用于声明新的本地服务、特征或描述符。
受限服务
以下服务由系统保留,目前无法发布:
- 设备信息服务 (DIS)
- 通用属性配置文件服务 (GATT)
- 通用访问配置文件服务 (GAP)
- 扫描参数服务 (SCP)
尝试创建阻止的服务将导致从对 CreateAsync 的调用返回 BluetoothError.DisabledByPolicy。
生成的属性
系统根据创建特征期间提供的 GattLocalCharacteristicParameters 自动生成以下描述符:
- 客户端特征配置(如果特征被标记为可指示或可标记)。
- 特征用户说明(如果设置了 UserDescription 属性)。 有关详细信息,请参阅 GattLocalCharacteristicParameters.UserDescription 属性。
- 特征格式(指定的每个演示文稿格式的一个描述符)。 有关详细信息,请参阅 GattLocalCharacteristicParameters.PresentationFormats 属性。
- 特征聚合格式(如果指定了多个演示文稿格式)。 有关详细信息,请参阅 GattLocalCharacteristicParameters.See PresentationFormats 属性。
- 特征扩展属性(如果特征用扩展属性位标记)。
扩展属性描述符的值通过 ReliableWrites 和 WritableAuxiliaries 特征属性确定。
尝试创建保留描述符将导致异常。
请注意,目前不支持广播。 指定 Broadcast GattCharacteristicProperty 将导致异常。
构建服务和特征的层次结构
GattServiceProvider 用于创建和播发根主服务定义。 每个服务都需要它自己的 ServiceProvider 对象,该对象采用 GUID:
GattServiceProviderResult result = await GattServiceProvider.CreateAsync(uuid);
if (result.Error == BluetoothError.Success)
{
serviceProvider = result.ServiceProvider;
//
}
主要服务是 GATT 树的顶层。 主要服务包含特征和其他服务(称为“包含”或辅助服务)。
现在,使用所需的特征和描述符填充服务:
GattLocalCharacteristicResult characteristicResult = await serviceProvider.Service.CreateCharacteristicAsync(uuid1, ReadParameters);
if (characteristicResult.Error != BluetoothError.Success)
{
// An error occurred.
return;
}
_readCharacteristic = characteristicResult.Characteristic;
_readCharacteristic.ReadRequested += ReadCharacteristic_ReadRequested;
characteristicResult = await serviceProvider.Service.CreateCharacteristicAsync(uuid2, WriteParameters);
if (characteristicResult.Error != BluetoothError.Success)
{
// An error occurred.
return;
}
_writeCharacteristic = characteristicResult.Characteristic;
_writeCharacteristic.WriteRequested += WriteCharacteristic_WriteRequested;
characteristicResult = await serviceProvider.Service.CreateCharacteristicAsync(uuid3, NotifyParameters);
if (characteristicResult.Error != BluetoothError.Success)
{
// An error occurred.
return;
}
_notifyCharacteristic = characteristicResult.Characteristic;
_notifyCharacteristic.SubscribedClientsChanged += SubscribedClientsChanged;
如上所示,这也是声明每个特征支持的操作的事件处理程序的好位置。 若要正确响应请求,应用必须定义并设置属性支持的每个请求类型的事件处理程序。 注册处理程序失败将导致请求立即由系统完成,且 不太可能的Error 。
常量特征
有时,某些特征值在应用的生存期内不会更改。 在这种情况下,建议声明一个常量特征,以防止不必要的应用激活:
byte[] value = new byte[] {0x21};
var constantParameters = new GattLocalCharacteristicParameters
{
CharacteristicProperties = (GattCharacteristicProperties.Read),
StaticValue = value.AsBuffer(),
ReadProtectionLevel = GattProtectionLevel.Plain,
};
var characteristicResult = await serviceProvider.Service.CreateCharacteristicAsync(uuid4, constantParameters);
if (characteristicResult.Error != BluetoothError.Success)
{
// An error occurred.
return;
}
发布服务
完全定义服务后,下一步是发布对服务的支持。 这会通知 OS 在远程设备执行服务发现时应返回该服务。 必须设置两个属性 - IsDiscoverable 和 IsConnectable:
GattServiceProviderAdvertisingParameters advParameters = new GattServiceProviderAdvertisingParameters
{
IsDiscoverable = true,
IsConnectable = true
};
serviceProvider.StartAdvertising(advParameters);
- IsDiscoverable:将友好名称播发到播发中的远程设备,使设备可发现。
- IsConnectable:播发可连接的播发以便在外围角色中使用。
当服务既可发现又可连接时,系统将向播发数据包添加服务 Uuid。 播发数据包中只有 31 个字节,128 位 UUID 占用其中 16 个字节!
请注意,当服务在前台发布时,应用程序必须在应用程序挂起时调用 StopAdvertising。
响应读取和写入请求
正如我们在声明所需特征时看到的,GattLocalCharacteristics 有 3 种类型的事件 - ReadRequested、WriteRequested 和 SubscribedClientsChanged。
读取
当远程设备尝试从特征(而不是常量值)读取值时,将调用 ReadRequested 事件。 调用读取的特征以及参数(包含有关远程设备的信息)将传递给委托:
characteristic.ReadRequested += Characteristic_ReadRequested;
// ...
async void ReadCharacteristic_ReadRequested(GattLocalCharacteristic sender, GattReadRequestedEventArgs args)
{
var deferral = args.GetDeferral();
// Our familiar friend - DataWriter.
var writer = new DataWriter();
// populate writer w/ some data.
// ...
var request = await args.GetRequestAsync();
request.RespondWithValue(writer.DetachBuffer());
deferral.Complete();
}
写
当远程设备尝试将值写入特征时,将调用 WriteRequested 事件,其中包含有关远程设备的详细信息,该特征是写入到哪些特征以及值本身:
characteristic.ReadRequested += Characteristic_ReadRequested;
// ...
async void WriteCharacteristic_WriteRequested(GattLocalCharacteristic sender, GattWriteRequestedEventArgs args)
{
var deferral = args.GetDeferral();
var request = await args.GetRequestAsync();
var reader = DataReader.FromBuffer(request.Value);
// Parse data as necessary.
if (request.Option == GattWriteOption.WriteWithResponse)
{
request.Respond();
}
deferral.Complete();
}
有 2 种类型的写入 - 具有无响应。 使用 GattWriteOption(GattWriteRequest 对象上的属性)确定远程设备正在执行的写入类型。
向订阅的客户端发送通知
GATT 服务器操作的最常见情况是通知执行将数据推送到远程设备的关键功能。 有时,需要通知所有订阅的客户端,但有时可能需要选择要将新值发送到的设备:
async void NotifyValue()
{
var writer = new DataWriter();
// Populate writer with data
// ...
await notifyCharacteristic.NotifyValueAsync(writer.DetachBuffer());
}
当新设备订阅通知时,将调用 SubscribedClientsChanged 事件:
characteristic.SubscribedClientsChanged += SubscribedClientsChanged;
// ...
void _notifyCharacteristic_SubscribedClientsChanged(GattLocalCharacteristic sender, object args)
{
List<GattSubscribedClient> clients = sender.SubscribedClients;
// Diff the new list of clients from a previously saved one
// to get which device has subscribed for notifications.
// You can also just validate that the list of clients is expected for this app.
}
注意
应用程序可以使用 MaxNotificationSize 属性获取特定客户端的最大通知大小。 大于最大大小的任何数据都将被系统截断。
处理 GattLocalCharacteristic.SubscribedClientsChanged 事件时,可以使用下面所述的过程来确定有关当前订阅的客户端设备的完整信息:
- SubscribedClientsChanged 事件的 args 是 GattLocalCharacteristic 对象。
- 访问该对象的 GattLocalCharacteristic.SubscribedClients 属性,该属性是 GattSubscribedClient 对象的集合。
- 循环访问该集合。 对于每个元素,请执行以下操作:
- 访问 GattSubscribedClient.Session 属性,该属性是 GattSession 对象。
- 访问 GattSession.DeviceId 属性,该属性是 BluetoothDeviceId 对象。
- 访问 BluetoothDeviceId.Id 属性,即设备 ID 字符串。
- 将设备 ID 字符串传递给 BluetoothLEDevice.FromIdAsync 以检索 BluetoothLEDevice 对象。 可以从该对象获取有关设备的完整信息。