11.7. GATT 操作

一個特徵(characteristic)只是以具名值的形式存在於 GATT 資料庫中。真正讓它有用的,是用戶端可以對它執行的那一小組定義明確的操作。每個特徵都會以一個 屬性位元遮罩(property bitmask) 宣告它所支援的操作——一個無物可揭露的伺服器可以發布唯讀的值,一個控制暫存器可能是唯寫的,而一個串流發送更新的感測器則會設定 notify 位元。用戶端會在探索(discovery)期間發現這個位元遮罩並加以遵守。

這五種操作分別是 readwritewrite without responsenotifyindicate。它們可分為兩組——拉取(用戶端發出請求)與推送(伺服器主動傳送)。

11.7.1. 拉取:read 與 write

這兩者最為單純,看起來就跟函式呼叫一模一樣。

  • Read。 用戶端請求目前的值,伺服器將其回傳。一次往返,用戶端取得伺服器為該特徵所設定的任何位元組,而伺服器對於是誰讀取了該值則一無所知。

  • Write。 用戶端送出新的位元組,伺服器將其儲存(並可選擇性地對新值執行應用邏輯)。它有兩種形式:

    • 帶回應的寫入(Write with response)——伺服器予以確認,並在狀態為非零時引發對應的應用錯誤。可靠,一次往返。

    • 不帶回應的寫入(Write without response)——伺服器靜默地儲存這些位元組;用戶端完全不會收到任何確認。較快(不需等待確認的往返),且對串流很有用,代價是只能透過旁路讀回(side-channel readback)才能得知錯誤。

aioble 中,用戶端側的 API 將這項選擇隱藏在單一的 aioble.ClientCharacteristic.write() 方法之後,並搭配一個 response 關鍵字(True / False / None,會依對端所廣播的內容自動選擇)。

11.7.2. 推送:notify 與 indicate

對於感測器資料而言,拉取模型是錯誤的選擇。一條必須由手機每秒輪詢的心率帶,會在上百次不必要的無線電事件上耗費電力;而一條只在有新讀數時才推送一個值的心率帶,正是 BLE 一開始存在的意義所在。

GATT 以 伺服器主動發起(server-initiated) 的操作解決了這個問題。用戶端 訂閱(subscribe) 一個特徵;自此之後,每當伺服器更新該值時,新值就會被推送過連結傳給用戶端。它有兩種形式:

  • Notify。 發送後即不再過問(fire-and-forget)。伺服器將一則通知排入佇列,連結層在下一個連線事件期間將其傳出,用戶端便收到它。在 GATT 層級並沒有確認;連結層一般的重傳機制會處理無線電端的遺失,但應用程式不會看到該值已被處理的任何確認。

  • Indicate。 伺服器送出一則通知,並且 在送出下一則之前先等待用戶端在 GATT 層級的確認。一次只能有一則 indication。當伺服器需要確知用戶端確實看到了該值時就會使用它——例如一個關鍵警報特徵,或一個設定確認。

兩張並排的圖,分別呈現一個伺服器與一個 用戶端。左圖中,用戶端送出「read」, 伺服器以該值回覆。連續三次讀取, 每次都是一對箭頭。右圖中, 用戶端只送出一次「subscribe」,接著 伺服器在它自行選定的時點推送三個「notify」 封包,期間沒有任何用戶端請求。

拉取(read)相對於推送(notify)。使用通知時,用戶端只需訂閱一次,伺服器便會在值有所變動時隨時推送新值。

訂閱是透過寫入附加於特徵之上的一個描述子來完成的——也就是 用戶端特徵設定描述子(Client Characteristic Configuration Descriptor)(CCCD,0x2902)。寫入 0x0001 會啟用通知,0x0002 會啟用指示,0x0000 則兩者皆停用。aioble.ClientCharacteristic.subscribe() 方法會為你執行這項寫入,並提供 notify=Trueindicate=True 兩個關鍵字旗標。

訂閱之後,用戶端便以 notified()indicated() 等待傳入的推送——兩者都是非同步協程,會暫停直到下一個推送抵達為止。

11.7.3. MTU 支配酬載大小

每一種操作都受到連線在連結建立時所協商出的 MTU 所限制。預設 MTU 為 23 個位元組,扣掉 GATT 標頭後留給特徵值位元組的有 20 個位元組。任何比這更大的資料,都必須要嘛塞進一個更大的 MTU(透過 aioble.DeviceConnection.exchange_mtu() 向上協商,在相機上最高可達 512 個位元組),要嘛拆分成多個特徵或多則通知。

由用戶端發起、對大於 MTU 之值的讀取與寫入,會在幕後由 GATT 的 長型(long) 程序處理(Read Long/Prepare-Write + Execute-Write);aioble 會透明地執行這些程序,因此以超大值呼叫 read() / write() 只是多花一些往返次數而已。而由伺服器發起的通知與指示則 不會 被分片——一次推送受限於 MTU,超出的部分由應用程式自行拆分成多則通知,或完全改用 GATT 以外的方式處理。

對於真正大量的傳輸——一張擷取的影格、一批量測資料、一塊韌體 blob——正確的做法通常是完全跳脫 GATT,改用 L2CAP 通道 來處理(參見 L2CAP 通道)。

11.7.4. 兩側一覽

這五種操作在連線的每一側分別以不同的方式呈現: