蓝牙 GATT 服务器

本主题演示如何使用适用于通用 Windows 平台 (UWP)应用的蓝牙泛型属性(GATT)服务器 API。

Important

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

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

重要的 API

概述

Windows通常在客户端角色中运行。 然而,许多方案也要求Windows充当蓝牙 LE GATT 服务器。 几乎所有 IoT 设备的方案,以及大多数跨平台 BLE 通信都需要Windows GATT 服务器。 此外,向附近的可穿戴设备发送通知已成为一种需要这项技术的热门方案。

服务器操作将以 Service Provider 和 GattLocalCharacteristic 为中心。 这两个类将提供向远程设备声明、实现和公开数据层次结构所需的功能。

定义受支持的服务

你的应用可以声明一个或多个服务,这些服务将由Windows发布。 每个服务都由 UUID 唯一标识。

属性和 UUID

每个服务、特征和描述符都由它自己的唯一 128 位 UUID 定义。

Windows API 都使用术语 GUID,但蓝牙标准将这些 API 定义为 UUID。 出于我们的目的,这两个术语是可互换的,因此我们将继续使用术语 UUID。

如果该属性是标准属性并由蓝牙 SIG 定义的,则它还将具有相应的 16 位短 ID(例如,电池电量 UUID 为 0000 2A19-0000-1000-8000-8000-00805F9B34FB,短 ID 为 0x2A19)。 可以在 GattServiceUuidsGattCharacteristicUuids 中看到这些标准 UUID。

如果你的应用正在实现它自己的自定义服务,则必须生成自定义 UUID。 这在 Visual Studio 中通过“工具” -> “CreateGuid”即可轻松完成(使用选项 5 以获取“xxxxxxxx-xxxx-...xxxx”格式)。 此 uuid 现在可用于声明新的本地服务、特征或描述符。

受限服务

以下服务由系统保留,目前无法发布:

  1. 设备信息服务 (DIS)
  2. 通用属性配置文件服务 (GATT)
  3. 通用访问规范服务(GAP)
  4. 扫描参数服务 (SCP)

尝试创建被策略阻止的服务时,调用 CreateAsync 将返回 BluetoothError.DisabledByPolicy。

生成的属性

系统根据创建特征期间提供的 GattLocalCharacteristicParameters 自动生成以下描述符:

  1. 客户端特征配置(如果该特征被标记为可指示或可通知)。
  2. 特征用户说明(如果设置了 UserDescription 属性)。 有关详细信息,请参阅 GattLocalCharacteristicParameters.UserDescription 属性。
  3. 特征格式(指定的每个演示文稿格式的一个描述符)。 有关详细信息,请参阅 GattLocalCharacteristicParameters.PresentationFormats 属性。
  4. 特征聚合格式(如果指定了多个演示文稿格式)。 有关详细信息,请参阅 GattLocalCharacteristicParameters 的 PresentationFormats 属性。
  5. 特征扩展属性(如果特征用扩展属性位标记)。

扩展属性描述符的值通过 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;

如上所示,这也是声明每个特征支持的操作的事件处理程序的好位置。 若要正确响应请求,应用必须定义并设置属性支持的每个请求类型的事件处理程序。 未注册处理程序会导致系统立即以 UnlikelyError 完成该请求。

常量特征

有时,某些特征值在应用的生存期内不会更改。 在这种情况下,建议声明一个常量特征,以防止不必要的应用激活:

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

发布服务

完全定义服务后,下一步是发布对服务的支持。 这会通知操作系统,在远程设备执行服务发现时,应返回该服务。 必须设置两个属性 - IsDiscoverable 和 IsConnectable:

GattServiceProviderAdvertisingParameters advParameters = new GattServiceProviderAdvertisingParameters
{
    IsDiscoverable = true,
    IsConnectable = true
};
serviceProvider.StartAdvertising(advParameters);
  • IsDiscoverable:在广播中向远程设备公布友好名称,使设备可被发现。
  • IsConnectable:播发用于外围设备角色的可连接广播。

当服务同时可被发现且可连接时,系统会将 Service 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.  
}

Note

应用程序可以使用 MaxNotificationSize 属性获取特定客户端的最大通知大小。 大于最大大小的任何数据都将被系统截断。

处理 GattLocalCharacteristic.SubscribedClientsChanged 事件时,可以使用下面所述的过程来确定有关当前订阅的客户端设备的完整信息: