前言
本文記錄了博主第一次接觸藍牙,到使用 App 同周邊藍牙設備通信的過程。只討論 App 作為中心設備的情況,不包含 App 作為周邊設備的情形。
iOS 中使用 Core Bluetooth 框架實現藍牙通信。Core Bluetooth 是基于藍牙 4.0 的低功耗模式實現的。
藍牙的連接類似于 Client/Server 構架模型。中心設備作為客戶端,周邊設備作為服務端,掃描并建立連接進行數據交換。
在開始編碼前,先熟悉 iOS 在藍牙通信中涉及到的幾個類,搬磚費不費力。
準備
藍牙相關的類圖:
- CBCentralManager 類表示中心設備,掃描發現周邊藍牙設備,周邊藍牙設備用 CBPeripheral 類表示。
- 一個藍牙設備可能存在多種用途,每一種用途對應一個服務,使用 CBService 表示,比如心率傳感器有心率監測服務。
- 一個服務可以細分為多種特征,使用 CBCharacteristic 表示,比如心率監測服務中,含有心率的測量值、地理位置的定位等 Characteristic。
- 一個特征可以有多種描述,用 CBDescriptor 表示。
以上涉及到的 CBService,CBCharacteristic,CBDescriptor 類都繼承自 CBAttribute,它們有一個共同的屬性 CBUUID,用來作為唯一的標識。
Peripheral 作為 Server 端, Central 作為 Client, Peripheral 廣播自己的 Services 和 Characteristics, Central 可以選擇訂閱某一個具體的 Service, 也可以一次性訂閱全部的 Server(不建議這么做)。獲取到某個 Service 之后,同樣需要繼續發現這個服務下的 Characteristics。Peripheral 和 Central 之間通過 Characteristic 建立一個雙向的數據通道。
注意一定要用真機測試。
編碼
在 iOS 10 之后需要在 Info.plist 文件里面設置 NSBluetoothPeripheralUsageDescription 字段,添加訪問藍牙權限的描述,否則強行訪問藍牙功能會造成 Crash。
開始,初始化一個中心設備:
CBCentralManager *centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil options:nil];
_centralManager = centralManager;
這里有一個注意點,CBCentralManager 的創建是異步的,如果初始化完成之后沒有被當前創建它的類所持有,就會在下一次 RunLoop 迭代的時候釋放。當然 CBCentralManager 實例如果不是在 ViewController 中創建的,那么持有 CBCentralManager 的這個類在初始化之后也必須被 ViewController 持有,否則控制臺會有如下的錯誤輸出:
[CoreBluetooth] XPC connection invalid
如果成功初始化,就會回調 CBCentralManagerDelegate:
// 在 cetral 的狀態變為 CBManagerStatePoweredOn 的時候開始掃描
- (void)centralManagerDidUpdateState:(CBCentralManager *)central {
if (central.state == CBManagerStatePoweredOn) {
[_centralManager scanForPeripheralsWithServices:nil options:nil];
}
}
中心設備處于 PowerOn 狀態的時候開始掃描周邊設備,可以使用指定的 UUID 發現特定的 Service,也可以傳入 nil,表示發現所有周邊的藍牙設備,不過還是建議只發現自己需要服務的設備。發現之后會回調如下方法:
- (void)centralManager:(CBCentralManager *)central
didDiscoverPeripheral:(CBPeripheral *)peripheral
advertisementData:(NSDictionary<NSString *, id> *)advertisementData RSSI:(NSNumber *)RSSI {
if (!peripheral.name) return; // Ingore name is nil peripheral.
if (![_peripheralsList containsObject:peripheral]) {
[_peripheralsList addObject:peripheral];
_peripherals = _peripheralsList.copy;
}
}
成功發現設備后選擇一個 peripheral 建立連接,在建立連接之后停止發現:
[_centralManager connectPeripheral:peripheral options:nil];
連接成功后會繼續回調 CBCentralManagerDelegate 中的方法:
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral {
peripheral.delegate = self;
// Client to do discover services method...
CBUUID *seriveUUID = [CBUUID UUIDWithString:@"d2009d00-6000-1000-8000-XXXX"];
// `nil` 代表發現所有服務。
[peripheral discoverServices:@[seriveUUID]];
}
連接成功該周邊設備之后,再發現需要使用該設備的具體服務。
接下來就是響應 CBPeripheralDelegate 代理方法了。
成功發現周邊設備的某個服務之后響應方法:
// 發現服務
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(nullable NSError *)error {
NSArray *services = peripheral.services;
if (services) {
CBService *service = services[0];
CBUUID *writeUUID = [CBUUID UUIDWithString: TRANSFER_SERVICE_UUID];
CBUUID *notifyUUID = [CBUUID UUIDWithString: TRANSFER_SERVICE_UUID];
[peripheral discoverCharacteristics:@[writeUUID, notifyUUID] forService:service]; // 發現服務
}
}
發現服務(CBService)之后,還需要發現該服務下的特征(Characteristic)。這里通常會有兩中特征:寫特征和通知特征。
發現特征之后一定要打開通知特性,否者寫入數據之后,不會收到回復數據。
// 發現特征
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(nullable NSError *)error {
if (!error) {
NSArray *characteristicArray = service.characteristics;
if(characteristicArray.count > 1) {
CBCharacteristic *writeCharacteristic = characteristicArray[0];
CBCharacteristic *notifyCharacteristic = characteristicArray[1];
// 通知使能, `YES` enable notification only, `NO` disable notifications and indications
[peripheral setNotifyValue:YES forCharacteristic:notifyCharacteristic];
}
} else {
NSLog(@"Discover Charactertics Error : %@", error);
}
}
使用 writeCharactersitc 寫入數據:
[peripheral writeValue:writeData.copy forCharacteristic:writeCharactersitc type:CBCharacteristicWriteWithResponse];
寫入數據之后,在需要回復的前提下會回調如下兩個代理方法:
// 寫入成功
- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error {
if (!error) {
NSLog(@"Write Success");
} else {
NSLog(@"WriteVale Error = %@", error);
}
}
// 寫入成功后的應答
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
if (error) {
NSLog(@"update value error: %@", error);
} else {
NSData *responseData = characteristic.value;
}
}
至此,一次完整的藍牙通信就完成了。
藍牙數據包的載荷比較小,在應答的過程中,經常需要進行拆包、組合。包的第一個字節代表包的序列號。