本文示範如何使用藍牙通用屬性(GATT)用戶端 API 來支援 Windows 應用程式。
Important
你必須在 Package.appxmanifest 中宣告「藍牙」功能。
<Capabilities> <DeviceCapability Name="bluetooth" /> </Capabilities>
Overview
你可以使用 Windows.Devices.Bluetooth.GenericAttributeProfile 命名空間中的 API 來存取藍牙 LE 裝置。 藍牙 LE 裝置透過以下一系列方式暴露其功能:
- Services
- 特徵
- 描述 符
服務定義了 LE 裝置的功能合約,並包含一組定義服務的特性。 這些特徵本身又包含用來描述這些特徵的描述項。 這三個詞彙通常被稱為裝置的屬性。
Bluetooth LE GATT API 提供的是物件和函式,而不是對底層傳輸機制的直接存取。 GATT API 也讓您能與藍牙 LE 裝置合作,並執行以下任務:
- 執行屬性探索
- 讀取與寫入屬性值
- 註冊一個回調以回應特徵值變更事件
要建立有用的實作,你必須先行了解應用程式打算使用的 GATT 服務與特性,並處理特定的特性值,使 API 提供的二進位資料在呈現給使用者前能被轉換成有用資料。 藍牙 GATT API 僅揭露與藍牙 LE 裝置通訊所需的基本原語。 要解讀資料,必須定義應用程式設定檔,無論是藍牙 SIG 標準配置檔,或是由裝置廠商實作的自訂設定檔。 設定檔會在應用程式與裝置之間建立約束合約,規範交換資料的意義及解讀方式。
為方便起見,藍牙SIG維護一份 公開帳號清單 。
查詢附近裝置
查詢附近裝置有兩種主要方法:
- DeviceWatcher 位於 Windows.Devices.Enumeration
- BluetoothLEAdvertisementWatcher 位於 Windows.Devices.Bluetooth.Advertisement
第二種方法在 廣告 文件中有詳細說明,這裡不會多談,但基本概念是找到符合 特定廣告過濾器的附近藍牙位址。 一旦你拿到地址,就可以打電話給 BluetoothLEDevice.FromBluetoothAddressAsync 來取得該裝置的參考資料。
現在,回到 DeviceWatcher 的方法。 藍牙 LE 裝置與Windows中任何其他裝置無異,可使用 Enumeration API 查詢。 使用 DeviceWatcher 類別並傳遞一個查詢字串,指定要尋找的裝置:
// 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 事件處理常式中收到 DeviceInformation。 想更詳細了解 DeviceWatcher,請參閱完整範例於 Github。
連線至裝置
一旦發現想要的裝置,使用 DeviceInformation.Id 取得該裝置的藍牙LE裝置物件:
private async Task 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 物件的參考(且系統中沒有其他應用程式有該裝置的參考),會在短暫逾時後自動斷線。
bluetoothLeDevice.Dispose();
如果應用程式需要再次存取該裝置,只要重新建立裝置物件並存取某個特徵(下節將討論),作業系統就會在必要時重新連線。 如果裝置就在附近,即可存取該裝置;否則會傳回 DeviceUnreachable 錯誤。
Note
僅靠呼叫這個方法建立 BluetoothLEDevice 物件,並不一定會啟動連線。 要啟動連線,請將 GattSession.MaintainConnection 設為 true,或在 BluetoothLEDvice 呼叫未快取的服務發現方法,或對裝置執行讀寫操作。
- 如果 GattSession.MaintainConnection 設為 true,系統會無限期等待連線,當裝置可用時才會連線。 你的應用程式沒有任何需要等待的項目,因為 GattSession.MaintainConnection 是一個屬性。
- 在 GATT 中,服務發現與讀寫操作時,系統等待有限但可變的時間。 從瞬間到幾分鐘不等。 因素包括堆疊上的流量,以及請求的排隊程度。 若沒有其他待處理請求,且遠端裝置無法存取,系統會等待七(7)秒後逾時。如果還有其他待處理的請求,那麼排隊中每個請求處理時間可能要七(7)秒,所以你的請求越靠近排隊後方,等待的時間就越長。
目前,你無法取消連線流程。
列舉支援的服務與特性
現在你已經有一個 BluetoothLEDevice 物件,下一步就是找出該裝置會暴露哪些資料。 第一步是查詢服務:
GattDeviceServicesResult result = await bluetoothLeDevice.GetGattServicesAsync();
if (result.Status == GattCommunicationStatus.Success)
{
var services = result.Services;
// ...
}
一旦確定了目標服務,下一步就是查詢其特性。
GattCharacteristicsResult result = await service.GetCharacteristicsAsync();
if (result.Status == GattCommunicationStatus.Success)
{
var characteristics = result.Characteristics;
// ...
}
作業系統會回傳一個 GattCharacteristic 物件的 ReadOnly 清單,你可以對這些物件執行操作。
對特徵執行讀寫操作
特性是以 GATT 為基礎的通訊之基本單位。 它包含一個代表裝置上獨立資料的值。 例如,電池電量特性有一個代表裝置電池電量的數值。
閱讀特徵屬性以判斷支援哪些操作:
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.
}
如果支援讀取,你可以讀到以下數值:
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
}
寫入特徵的模式類似:
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
}
Tip
DataReader 和 DataWriter 在處理許多藍牙 API 原始緩衝區時不可或缺。
訂閱通知
請確認該特徵支援 Indicate 或 Notify 其中之一(請檢查該特徵的屬性以確認)。
Indicate 被認為更可靠,因為每個值變更事件都會與用戶端裝置的確認連結。
Notify 較普遍的原因是大多數 GATT 交易寧願節省電力,也不願極度可靠。 無論如何,這些都是在控制器層處理,應用程式不會介入。 我們統稱它們為「通知」。
在收到通知前,有兩件事需要先處理:
- 寫入用戶端特徵配置描述符(CCCD)
- 處理 Characteristic.ValueChanged 事件
寫入 CCCD 會告知伺服器裝置,此用戶端希望在該特徵值每次變更時都能得知。 若要這樣做:
GattCommunicationStatus status = await selectedCharacteristic.WriteClientCharacteristicConfigurationDescriptorAsync(
GattClientCharacteristicConfigurationDescriptorValue.Notify);
if(status == GattCommunicationStatus.Success)
{
// Server has been informed of clients interest.
}
現在,每次遠端裝置的值被更改時,都會呼叫 GattCharacteristic 的 ValueChanged 事件。 接下來只剩下實作處理常式:
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.
}
Examples
完整範例請參見 Bluetooth 低功耗範例。