藍牙 GATT 伺服器

本主題示範如何將藍牙通用屬性 (GATT) 伺服器 API 用於通用 Windows 平台 (UWP) 應用程式。

重要

您必須在 Package.appxmanifest 中宣告 "bluetooth" 功能。

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

重要 API

概觀

Windows 通常會在用戶端角色中運作。 然而,許多情況下都需要 Windows 充當藍牙 LE GATT 伺服器。 幾乎所有 IoT 裝置場景以及大多數跨平台 BLE 通訊都需要 Windows 作為 GATT 伺服器。 此外,向附近的可穿戴裝置發送通知已成為一種熱門的場景,其也需要這項技術。

伺服器作業將圍繞服務提供者和 GattLocalCharacteristic 進行。 這兩個類別將提供宣告、實作並向遠端裝置公開資料階層所需的功能。

定義支援的服務

您的應用程式可以宣告將由 Windows 發佈的一項或多項服務。 每個服務都由 UUID 唯一識別。

屬性和 UUID

每個服務、特性和描述項都由其自己唯一的 128 位元 UUID 定義。

Windows API 都使用術語 GUID,但藍牙標準將其定義為 UUID。 以我們的目的來說,這兩個術語可以互換,因此我們將繼續使用術語 UUID。

如果該屬性是標準屬性並且由藍牙 SIG 定義,則還將有一個相應的 16 位元短識別碼 (例如,電池電量 UUID 為 00002A19-0000-1000-8000-00805F9B34FB,短識別碼為 0x2A19)。 這些標準 UUID 可以在 GattServiceUuidsGattCharacteristicUuids 中看到。

如果您的應用程式正在實作自己的自訂服務,則必須產生自訂 UUID。 這可以在 Visual Studio 中透過工具 -> CreateGuid 輕鬆完成 (使用選項 5 以「xxxxxxxx-xxxx-...xxxx」格式取得)。 該 UUID 現在可用於宣告新的本機服務、特性或描述項。

受限制的服務

以下服務已被系統保留,目前無法發佈:

  1. 裝置資訊服務 (DIS)
  2. 通用屬性設定檔服務 (GATT)
  3. 通用存取設定檔服務 (GAP)
  4. 人性化介面裝置服務 (HOGP)
  5. 掃描參數服務 (SCP)

嘗試建立封鎖的服務將導致呼叫 CreateAsync 傳回 BluetoothError.DisabledByPolicy。

產生的屬性

以下描述項由系統根據建立特性期間提供的 GattLocalCharacteristicParameters 自動產生:

  1. 用戶端特性配置 (如果特性標記為可指示或可通知)。
  2. 特性使用者描述 (如果設定了 UserDescription 屬性)。 如需詳細資訊,請參閱 GattLocalCharacteristicParameters.UserDescription 屬性。
  3. 特性格式 (指定的每種表示格式的一個描述項)。 如需詳細資訊,請參閱 GattLocalCharacteristicParameters.PresentationFormats 屬性。
  4. 特性彙總格式 (如果指定了多種表示格式)。 如需詳細資訊,請參閱 GattLocalCharacteristicParameters.See PresentationFormats property 屬性。
  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:通告用於周邊角色的可連結通告。

當服務同時為 Discoverable 和 Connectable 時,系統會將Service Uuid加入通告套件中。 通告套件只有 31 個位元組,128 位元的 UUID 就佔了其中的 16 個!

請注意,當在前景發佈服務時,應用程式必須在應用程式暫停時呼叫 StopAdvertising。

回應讀取和寫入要求

正如我們在上述宣告所需特性時所看到的,GattLocalCharacteristics 有 3 種類型的事件 - ReadRequested、WriteRequested 和 SubscribedClientsChanged。

參閱

當遠端裝置嘗試從特性讀取值 (且它不是常數值) 時,將呼叫 ReadRequested 事件。 呼叫讀取的特性以及 args (包含有關遠端裝置的資訊) 將傳遞給委派:

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

寫入有兩種類型 - 有回應和無回應。 使用 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 事件時,您可以使用下面描述的程序來確定有關目前訂閱的用戶端裝置的完整資訊: