基礎概念
藍芽通訊基於 client-server 構架,由兩個角色進行通訊,分別是 Central
和 Peripheral
,中文環境可以理解為主裝置
和從裝置
。
Peripheral
角色主要作為資料的提供方,比如 Apple Watch;Central
主要作為資料需求方,通常為 iPhone/iPad 等產品。
Peripheral
提供一系列的服務(service),每種服務含有一個或多個特徵屬性(characteristics)。
iOS 提供 CoreBuletooth 框架,作為藍芽 4.0 通訊的基礎。對應每一個 Peripheral
,CoreBuletooth 框架會為其分配一個 UUID,據測試,一般情況下,這個 UUID 在一個 iOS 裝置上是不會改變的,但對於不同的 iOS 裝置,拿到的 UUID 卻又不同。Central
的 ID 由底層框架維護,上層並不知曉。
在通訊時,一方向外界廣播資料,並攜帶接收方的 ID,周邊所有藍芽裝置都會收到訊息,如果發現自己與該 ID 匹配,則處理訊息,否者忽略。
在建立連線前,需要知道周邊裝置的 UUID,需要一個發現的過程。
發現
未連線的藍芽裝置,會不斷的向周邊廣播資料。資料中攜帶它具有的服務(Service),名稱,訊號強度,廠商資訊等。在 iOS 中以 CBPeripheral
類表示。
中心裝置通過發現的方式來找到周邊裝置進而建立連線通訊。
CBCentralManager 類代表 iOS 裝置,開始使用前,先進行初始化操作。
let centralManager = CBCentralManager()
centralManager.delegate = self
複製程式碼
CBCentralManager
初始或會驅動硬體,這個過程是非同步的,如果驅動成功/失敗,會通過代理方法告知:
func centralManagerDidUpdateState(_ central: CBCentralManager)
複製程式碼
提示:如果代理方法沒有呼叫,控制檯輸出
[CoreBluetooth] XPC connection invalid
,很可能是centralManager
例項被釋放了。詳細參考XPC Error CoreBluetooth | Apple Developer Forums
Central
進入 powerOn
狀態後,才可執行掃描:
centralManager.scanForPeripherals(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey: false])
複製程式碼
如果清楚自己所需的藍芽裝置服務,withServices 引數傳入具體的服務 UUID 列表,如果傳 nil
預設發現所有裝置。options
用於指定發現的規則,比較常用的是 CBCentralManagerScanOptionAllowDuplicatesKey
,預設情況下,中心裝置每收到一個包,就是呼叫一次 Delegate 方法,設定為 false
,會每隔一段時間接收一次周邊裝置的廣播訊號,如果只是為了發現連線裝置,這樣設定可以節省電量以及提高 App 的效能。
centralManager
發現的裝置的回撥:
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
// peripheral:發現的裝置
// advertisementData: 廣播資料字典
// RSSI: 訊號強度
}
複製程式碼
廣播資料包含的內容在Advertisement Data Retrieval Keys | Apple Developer Documentation。需要注意的是,這裡含有的名稱欄位和peripheral.name
的值並不一定相同。後者是前者的系統級快取,並且沒有給出快取失效時間,如果藍芽裝置中途修改過名稱,讀到的很可能是失效的快取,真實的名稱在 CBAdvertisementDataLocalNameKey
中。雖然修改名稱的可能性很小,但是難免也會有人遇到這個坑。
需要注意的是,發現的裝置必須主動去持有,否者在超出作用域就釋放了。
連線
發現目標裝置後,連線裝置。
central.connect(peripheral, options: nil)
複製程式碼
如果裝置連線成功,回撥代理方法:
optional public func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral)
複製程式碼
如果連線失敗,回撥:
optional public func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?)
複製程式碼
該方法也會在裝置超出連線返回的時候呼叫。
在 iOS 中,存在系統級的藍芽連線方式,叫做配對。配對的裝置可在設定->藍芽->我的裝置
檢視。配對的裝置,即使 kill 應用程式,底層仍會持有連線的狀態,CBCentralManager
提供獲取已配對裝置的方法。
open func retrieveConnectedPeripherals(withServices serviceUUIDs: [CBUUID]) -> [CBPeripheral]
複製程式碼
不過,需要提供獲取已連線裝置的 serviceUUIDs
,無論是哪個應用程式配對的藍芽裝置只要具有對應的服務都會被獲取。
最後,獲取到的裝置只是底層連線,上層應用仍然需要呼叫 connectPeripheral:options:
方法,等待centralManager:didConnectPeripheral:
回撥,使其進入連線狀態。
建立過連線的裝置,系統都會快取它的資訊,只要它的硬體唯一標識沒有修改,系統為其分配的 identifier
會始終不變。所以,建立過連線的裝置,可通過儲存它的 identifier
來再次獲取,並建立連線。
open func retrievePeripherals(withIdentifiers identifiers: [UUID]) -> [CBPeripheral]
複製程式碼
發現服務和特徵
建立連線後,需要發現該藍芽裝置所具有的服務,每個服務有對應的 UUID。
peripheral.discoverServices(nil)
複製程式碼
一般藍芽裝置具有多種服務, 如果希望發現所有的服務,傳 nil
。在開發中,我們清楚所需要的服務型別,最好傳入具體的服務 UUID。成功發現服務會回撥 CBPeripheralDelegate
的如下方法:
optional public func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?)
複製程式碼
服務型別儲存在 services
屬性中。
發現目標服務後,下一步是該發現服務中的具體特徵(Characteristics),因為資料的讀寫物件是它。
peripheral.discoverCharacteristics(nil, for: service)
複製程式碼
同樣特徵也具有 UUID,如果傳 nil
將獲取到該服務下的所有特徵,當然如果知道目標特徵的 UUID,傳對應的 ID 能夠提高效率。特徵發現成功後,會有如下回撥:
optional public func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?)
複製程式碼
目標特徵儲存在 service.characteristics
中,接著可以對它們進行讀寫操作。
讀特徵值
單一的資料儲存在 Service 的 Characteristic 中,比如說溫度。
有兩種方式讀取 Characteristic 中的資料:
- 直接讀取
open func readValue(for characteristic: CBCharacteristic)
複製程式碼
如果讀取到資料,回撥代理方法:
optional public func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?)
複製程式碼
資料儲存於 charateristic.value
中。
並不是所有的特徵都是可讀的,需提前檢查檢查 CBCharacteristic
的properties
屬性。確保它包含 CBCharacteristicPropertyRead
,如果不包含,上述代理方法會返回錯誤。
- 訂閱通知
有些情況下,特徵值會動態的變化,如果都需要手動去獲取效率會比較低。Characteristic 還有一個被訂閱的功能,訂閱之後一旦特徵值發生變化就會通過回撥方法通知 App。
peripheral.setNotifyValue(true, for: characteristic)
複製程式碼
訂閱成功與否的狀態同樣通過代理方法返回:
optional public func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?)
複製程式碼
當然並不是所有的 Characteristic 都有被訂閱的功能。同樣需要檢查properties
屬性,包含 Notify
或者 Indicate
才能被訂閱。
寫特徵值
有時候還需要向 Peripheral 傳送資料,實際上是對它的某個特徵值進行寫操作,寫操作有兩種形式,有回覆和沒有回覆。
peripheral.peripheral.writeValue(data, for: characteristic, type: .withResponse)
複製程式碼
.withResponse
表示有回覆的型別,回覆以代理的形式通知:
optional public func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?)
複製程式碼
如果寫入操作失敗,錯誤資訊儲存在 error
中。
.withoutResponse
不會有回覆,只會盡最大努力的寫,但無法保證能寫成功,如果寫失敗,不會有任何通知和錯誤資訊。
傳入的 data 資料,在內部會被拷貝,後續修改或者釋放影響寫操作。
同樣,並不是所有的特徵都具有寫的功能,在寫之前需檢查該特徵的 properties
屬性是否包含 write
或者 writeWithoutResponse
。