一、技術(shù)背景
本文主要是從藍(lán)牙的掃描、連接、收發(fā)數(shù)據(jù)、打印等方向快速熟悉藍(lán)牙開發(fā),記錄了在開發(fā)過程中遇到的的問題及解決方法。在分享之前,我們需要清楚幾個(gè)BLE相關(guān)的概念。
二、基本概念
藍(lán)牙,指的是BLE(Bluetooth Low Energy/低功耗藍(lán)牙),一般應(yīng)用蘋果的官方框架基于<CoreBluetooth/CoreBluetooth.h>框架進(jìn)行開發(fā)。
中心設(shè)備:用于掃描周邊藍(lán)牙外設(shè)的設(shè)備,比如我們上面所說的中心者模式,此時(shí)我們的手機(jī)就是中心設(shè)備。
外設(shè):被掃描的藍(lán)牙設(shè)備,比如我們上面所說的用我們的手機(jī)連接小米手環(huán),這時(shí)候小米手環(huán)就是外設(shè)。
廣播:外部設(shè)備不停的散播的藍(lán)牙信號(hào),讓中心設(shè)備可以掃描到,也是我們開發(fā)中接收數(shù)據(jù)的入口。
服務(wù)(Service):外部設(shè)備在與中心設(shè)備連接后會(huì)有服務(wù),可以理解成一個(gè)功能模塊,中心設(shè)備可以讀取服務(wù),篩選我們想要的服務(wù),并從中獲取出我們想要特征。(外設(shè)可以有多個(gè)服務(wù))
特征(Characteristic):服務(wù)中的一個(gè)單位,一個(gè)服務(wù)可以多個(gè)特征,而特征會(huì)有一個(gè)value,一般我們向藍(lán)牙設(shè)備寫入數(shù)據(jù)、從藍(lán)牙設(shè)備讀取數(shù)據(jù)就是這個(gè)value
UUID:區(qū)分不同服務(wù)和特征的唯一標(biāo)識(shí),使用該字端我們可以獲取我們想要的服務(wù)或者特征
核心類:CBCentralManager 中心設(shè)備管理類、CBCentral 中心設(shè)備、CBPeripheralManager 外設(shè)設(shè)備管理類、CBPeripheral 外設(shè)設(shè)備、CBUUID 外圍設(shè)備服務(wù)特征的唯一標(biāo)志、CBService 外圍設(shè)備的服務(wù)、CBCharacteristic 外圍設(shè)備的特征。
三、申請(qǐng)權(quán)限
1、需要在info.plist文件中添加相對(duì)應(yīng)的鍵值對(duì)Privacy - Bluetooth Always Usage Description,否則會(huì)閃退。
四、核心重點(diǎn):藍(lán)牙數(shù)據(jù)接收的一般流程
1、藍(lán)牙開啟后,不斷地在進(jìn)行廣播信號(hào)
2、掃描藍(lán)牙
3、發(fā)現(xiàn)(discover)外設(shè)設(shè)備(可根據(jù)service的UUID來辨別是否是我們連接的設(shè)備)
4、成功連接外設(shè)設(shè)備
5、調(diào)用代理方法發(fā)現(xiàn)「服務(wù)」
6、調(diào)用代理方法發(fā)現(xiàn)「服務(wù)」里的「特征」
7、發(fā)現(xiàn)硬件用于傳輸數(shù)據(jù)的「特征」(App發(fā)送數(shù)據(jù)給硬件時(shí),會(huì)用到這個(gè)「特征)
8、發(fā)現(xiàn)硬件用于數(shù)據(jù)輸出的「特征」,進(jìn)行「監(jiān)聽」(硬件就是從這個(gè)「特征」中發(fā)送數(shù)據(jù)給手機(jī)端)
9、利用數(shù)據(jù)輸入「特征」發(fā)送數(shù)據(jù),或者等待數(shù)據(jù)輸出「特征」發(fā)出來的數(shù)據(jù)
五、中心設(shè)備-相關(guān)函數(shù)
1、創(chuàng)建一個(gè)中心設(shè)備
- (instancetype)init;
- (instancetype)initWithDelegate:(nullable id<CBCentralManagerDelegate>)delegate
? ? ? ? ? ? ? ? ? ? ? ? ? queue:(nullable dispatch_queue_t)queue;
- (instancetype)initWithDelegate:(nullable id<CBCentralManagerDelegate>)delegate
? ? ? ? ? ? ? ? ? ? ? ? ? queue:(nullable dispatch_queue_t)queue
? ? ? ? ? ? ? ? ? ? ? ? options:(nullable NSDictionary<NSString *, id> *)options NS_AVAILABLE(10_9, 7_0) NS_DESIGNATED_INITIALIZER;
2、中心設(shè)備是否正在掃描
@property(nonatomic, assign, readonly) BOOL isScanning NS_AVAILABLE(10_13, 9_0);
3、獲取已配對(duì)過的藍(lán)牙外設(shè)
- (NSArray *)retrievePeripheralsWithIdentifiers:(NSArray *)identifiers NS_AVAILABLE(10_9, 7_0);- (NSArray *)retrieveConnectedPeripheralsWithServices:(NSArray *)serviceUUIDs NS_AVAILABLE(10_9, 7_0);
4、掃描外設(shè)(如果參數(shù)傳nil,表示掃描所有外設(shè))和停止掃描
- (void)scanForPeripheralsWithServices:(nullable NSArray *)serviceUUIDs options:(nullable NSDictionary *)options;- (void)stopScan;
5、連接指定外設(shè)
- (void)connectPeripheral:(CBPeripheral *)peripheral options:(nullable NSDictionary *)options;
6、取消指定外設(shè)
- (void)cancelPeripheralConnection:(CBPeripheral *)peripheral;
7、監(jiān)聽中心設(shè)備的狀態(tài)
- (void)centralManagerDidUpdateState:(CBCentralManager *)central;
主要是獲取當(dāng)前中心外設(shè)狀態(tài):
typedef NS_ENUM(NSInteger, CBManagerState) {
? ? CBManagerStateUnknown = 0, // 未知外設(shè)類型
? ? CBManagerStateResetting,? // 正在重置藍(lán)牙外設(shè)
? ? CBManagerStateUnsupported,
? ? CBManagerStateUnauthorized,
? ? CBManagerStatePoweredOff,
? ? CBManagerStatePoweredOn,
} NS_ENUM_AVAILABLE(10_13, 10_0);
8、掃描到外設(shè)就會(huì)調(diào)用一次的代理方法
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI;
9、成功連接指定外設(shè)的代理回調(diào)
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral;
10、連接失敗后的代理回調(diào)
- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error;
11、連接外設(shè)失敗后的代理回調(diào)
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error;
六、外設(shè)設(shè)備-相關(guān)函數(shù)
1、外設(shè)名稱
@property(retain, readonly, nullable) NSString *name;
2、外設(shè)信號(hào)強(qiáng)度
@property(retain, readonly, nullable) NSNumber *RSSI NS_DEPRECATED(10_7, 10_13, 5_0, 8_0);
3、外設(shè)設(shè)備連接狀態(tài)
@property(readonly) CBPeripheralState state;
typedef NS_ENUM(NSInteger, CBPeripheralState) {
? ? CBPeripheralStateDisconnected = 0, // 斷開連接狀態(tài)
? ? CBPeripheralStateConnecting, // 正在連接狀態(tài)
? ? CBPeripheralStateConnected, // 已連接狀態(tài)
? ? CBPeripheralStateDisconnecting NS_AVAILABLE(10_13, 9_0), // 正在斷開狀態(tài)
} NS_AVAILABLE(10_9, 7_0);
4、獲取外設(shè)服務(wù)
@property(retain, readonly, nullable) NSArray *services;
5、發(fā)現(xiàn)服務(wù)
- (void)discoverServices:(nullable NSArray *)serviceUUIDs;
6、發(fā)現(xiàn)子服務(wù)
- (void)discoverIncludedServices:(nullable NSArray *)includedServiceUUIDs forService:(CBService *)service;
7、發(fā)現(xiàn)特征
- (void)discoverCharacteristics:(nullable NSArray *)characteristicUUIDs forService:(CBService *)service;
8、藍(lán)牙發(fā)送數(shù)據(jù)有字節(jié)長(zhǎng)度大小限制,該函數(shù)是獲取允許最大字節(jié)長(zhǎng)度限制
- (NSUInteger)maximumWriteValueLengthForType:(CBCharacteristicWriteType)type NS_AVAILABLE(10_12, 9_0);
9、發(fā)送數(shù)據(jù)
- (void)writeValue:(NSData *)data forCharacteristic:(CBCharacteristic *)characteristic type:(CBCharacteristicWriteType)type;
10、發(fā)現(xiàn)特征的描述
- (void)discoverDescriptorsForCharacteristic:(CBCharacteristic *)characteristic;
11、 發(fā)送數(shù)據(jù)通過描述
- (void)writeValue:(NSData *)data forDescriptor:(CBDescriptor *)descriptor;
12、外設(shè)名稱改變的監(jiān)聽
- (void)peripheralDidUpdateName:(CBPeripheral *)peripheral NS_AVAILABLE(10_9, 6_0);
13、 服務(wù)修改的監(jiān)聽
- (void)peripheral:(CBPeripheral *)peripheral didModifyServices:(NSArray *)invalidatedServices NS_AVAILABLE(10_9, 7_0);
14、信號(hào)強(qiáng)度改變的監(jiān)聽
- (void)peripheralDidUpdateRSSI:(CBPeripheral *)peripheral error:(nullable NSError *)error NS_DEPRECATED(10_7, 10_13, 5_0, 8_0);- (void)peripheral:(CBPeripheral *)peripheral didReadRSSI:(NSNumber *)RSSI error:(nullable NSError *)error NS_AVAILABLE(10_13, 8_0);
15、 發(fā)現(xiàn)服務(wù)和子服務(wù)
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(nullable NSError *)error;- (void)peripheral:(CBPeripheral *)peripheral didDiscoverIncludedServicesForService:(CBService *)service error:(nullable NSError *)error;
16、通過服務(wù)獲取特征
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(nullable NSError *)error;
17、特征發(fā)生改變后的監(jiān)聽
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error;
18、數(shù)據(jù)發(fā)送結(jié)果的回調(diào)
- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error;
19、發(fā)現(xiàn)描述通過特征
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverDescriptorsForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error;
19、描述值發(fā)生改變的監(jiān)聽
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForDescriptor:(CBDescriptor *)descriptor error:(nullable NSError *)error;
120、發(fā)送描述結(jié)果的回調(diào)
- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForDescriptor:(CBDescriptor *)descriptor error:(nullable NSError *)error;
七、掃描外設(shè)設(shè)備和停止掃描
1、檢測(cè)中心設(shè)備的藍(lán)牙狀態(tài)
// 掃描可用藍(lán)牙外設(shè)
- (void)fs_scanPeripheralsSuccess:(FSScanPerpheralsSuccess)success
? ? ? ? ? ? ? ? ? ? ? ? ? failure:(FSScanPeripheralFailure)failure {
? ? _scanPerpheralSuccess = success;
? ? _scanPerpheralFailure = failure;
? ? NSString *msg = nil;
// 在掃描設(shè)備前,需要判斷當(dāng)前中心設(shè)備的藍(lán)牙狀態(tài),只有開啟后才能進(jìn)行掃描工作
? ? switch (_centralManager.state) {
? ? ? ? case CBManagerStatePoweredOn:{
? ? ? ? ? ? msg = @"藍(lán)牙已開啟,允許連接藍(lán)牙外設(shè)";
? ? ? ? ? ? // 掃描的核心方法
? ? ? ? ? ? [_centralManager scanForPeripheralsWithServices:nil options:nil];
? ? ? ? ? ? FSLog(@"掃描階段 -- %@",msg);
? ? ? ? ? ? return;
? ? ? ? }
? ? ? ? ? ? break;
? ? ? ? case CBManagerStatePoweredOff:{
? ? ? ? ? ? msg = @"藍(lán)牙是關(guān)閉狀態(tài),需要打開才能連接藍(lán)牙外設(shè)";
? ? ? ? }
? ? ? ? ? ? break;
? ? ? ? case CBManagerStateUnauthorized: {
? ? ? ? ? ? msg = @"藍(lán)牙權(quán)限未授權(quán)";
? ? ? ? }
? ? ? ? ? ? break;
? ? ? ? case CBManagerStateUnsupported:{
? ? ? ? ? ? msg = @"平臺(tái)不支持藍(lán)牙";
? ? ? ? }
? ? ? ? ? ? break;
? ? ? ? case CBManagerStateUnknown: {
? ? ? ? ? ? msg = @"未知狀態(tài)";
? ? ? ? }
? ? ? ? ? ? break;
? ? ? ? default:
? ? ? ? ? ? break;
? ? }
? ? [self initBluetoothConfig];
? ? FSLog(@"%@",msg);
}
// 停止掃描
- (void)fs_stopScan {
? ? [_centralManager stopScan];
}
2、 代理方法獲取中心設(shè)備藍(lán)牙狀態(tài)的回調(diào)
#pragma mark - CBCentralManagerDelegate - 中央設(shè)備的代理方法
// 獲取當(dāng)前中央設(shè)備的藍(lán)牙狀態(tài),如果藍(lán)牙不可用,這回調(diào)回去,若藍(lán)牙可用,則搜索設(shè)備
- (void)centralManagerDidUpdateState:(CBCentralManager *)central {
? ? if(central.state != CBManagerStatePoweredOn) {
? ? ? ? if(_scanPerpheralFailure) {
? ? ? ? ? ? _scanPerpheralFailure(central.state);
? ? ? ? }
? ? }else {
? ? ? ? [central scanForPeripheralsWithServices:nil options:nil];
? ? }
? ? FSLog(@"中央設(shè)備的藍(lán)牙狀態(tài): %ld", central.state);
}
3、 獲取掃描到的外設(shè)設(shè)備: 需要做幾個(gè)核心操作:
3.1、篩選出peripheral為nil的外設(shè)信息
3.2、根據(jù)唯一的標(biāo)識(shí)UUID,避免相同外設(shè)重復(fù)添加到集合中
3.3、自動(dòng)重連:記錄上一次連接的外設(shè)UUID,然后通過UUID獲取peripheral進(jìn)行重連
// 掃描藍(lán)牙設(shè)備
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *,id> *)advertisementData RSSI:(NSNumber *)RSSI {
? ? // 在掃描的過程中會(huì)有很多不可用的藍(lán)牙設(shè)備信息,name為nil,需要排除掉
? ? if(peripheral.name.length <= 0 || peripheral == nil) {
? ? ? ? return;
? ? }
? ? // 在掃描過程中,存在同一臺(tái)設(shè)備被多次掃描到,所以在添加到可用設(shè)備集合中需要進(jìn)行篩選,相同的設(shè)備不需要重復(fù)添加
? ? if(_peripherals.count == 0) {
? ? ? ? [_peripherals addObject:peripheral];
? ? ? ? [_rssis addObject:RSSI];
? ? } else {
? ? ? ? __block BOOL isExist = NO; // block中獲取外部變量,若要改值,需要__block處理
? ? ? ? // UUIDString是每臺(tái)設(shè)備的唯一標(biāo)識(shí),所以通過UUIDString查詢集合中是否已存在藍(lán)牙外設(shè)
? ? ? ? [_peripherals enumerateObjectsUsingBlock:^(CBPeripheral *? _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
? ? ? ? ? ? CBPeripheral *per = [_peripherals objectAtIndex:idx];
? ? ? ? ? ? if ([per.identifier.UUIDString isEqualToString:peripheral.identifier.UUIDString]) {
? ? ? ? ? ? ? ? isExist = YES;
? ? ? ? ? ? ? ? [_peripherals replaceObjectAtIndex:idx withObject:peripheral];
? ? ? ? ? ? ? ? [_rssis replaceObjectAtIndex:idx withObject:RSSI];
? ? ? ? ? ? }
? ? ? ? }];
? ? ? ? // 集合中不存在,則添加,存在如上則代替
? ? ? ? if (!isExist) {
? ? ? ? ? ? [_peripherals addObject:peripheral];
? ? ? ? ? ? [_rssis addObject:RSSI];
? ? ? ? }
? ? }
? ? // 來這里說明成功掃描到藍(lán)牙設(shè)備,回調(diào)出去
? ? if(_scanPerpheralSuccess){
? ? ? ? _scanPerpheralSuccess(_peripherals, _rssis);
? ? }
? ? // 自動(dòng)連接上一次連接的外設(shè)
? ? if (_isAutoConnect) {
? ? ? ? NSString *uuid = [self fs_previousConnectionPeripheralUUID];
? ? ? ? if ([peripheral.identifier.UUIDString isEqualToString:uuid]) {
? ? ? ? ? ? peripheral.delegate = self;
? ? ? ? ? ? [_centralManager connectPeripheral:peripheral options:nil];
? ? ? ? }
? ? }
? ? FSLog(@"掃描到的外設(shè)名稱: %@", peripheral.name);
}
八、連接外設(shè)
1、在連接外設(shè)前需要判斷是否正在連接有其他外設(shè),如果有需要先取消連接后再重新連接外設(shè),需要注意的是,取消連接時(shí)需要清除保存的此時(shí)連接的外設(shè)UUID,以及保存在集合可打印的數(shù)據(jù)。
// 連接指定藍(lán)牙設(shè)備
- (void)fs_connectPeripheral:(CBPeripheral *)peripheral
? ? ? ? ? ? ? ? ? completion:(FSConnectPeripheralCompletion)completion {
? ? _connectCompletion = completion;
? ? if(_connectedPerpheral) { // 如果正在連接的有藍(lán)牙外設(shè),需要先取消連接后再連接新的藍(lán)牙設(shè)備
? ? ? ? [self fs_canclePeripheralConnected:peripheral];
? ? }
? ? [self connectPeripheral:peripheral];
? ? // 連接超時(shí)的相關(guān)處理
? // TODO: ......
}
// 連接藍(lán)牙設(shè)備
- (void)connectPeripheral:(CBPeripheral *)peripheral{
? ? [_centralManager connectPeripheral:peripheral options:nil];
? ? peripheral.delegate = self;
}
// 自動(dòng)連接
- (void)fs_autoConnectPreviousPeripheral:(FSConnectPeripheralCompletion)completion {
? ? _connectCompletion = completion;
? ? _isAutoConnect = YES;
? ? if (_centralManager.state == CBManagerStatePoweredOn) {
? ? ? ? // 掃描外設(shè)
? ? ? ? [_centralManager scanForPeripheralsWithServices:nil options:nil];
? ? }
}
// 取消藍(lán)牙連接
- (void)fs_canclePeripheralConnected:(CBPeripheral *)peripheral {
? ? if (!peripheral) return;
? ? // 取消后需要清除保存的藍(lán)牙外設(shè)的uuid
? ? [self fs_removePreviousConnectionPeripheralUUID];
? ? [_centralManager cancelPeripheralConnection:peripheral];
? ? _connectedPerpheral = nil;
? ? // 既然取消了連接,那么就不能發(fā)送數(shù)據(jù), 所以需要將發(fā)送數(shù)據(jù)的數(shù)組清除掉
? ? [_writeChatacterDatas removeAllObjects];
}
2、外設(shè)設(shè)備管理類連接的代理方法
// 藍(lán)牙外設(shè)連接成功后的代理回調(diào)
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral {
? ? // 藍(lán)牙設(shè)備
? ? _connectedPerpheral = peripheral;
? ? // 連接成功后停止掃描
? ? [_centralManager stopScan];
? ? // 保存當(dāng)前藍(lán)牙外設(shè),便于下次自動(dòng)連接
? ? [self fs_savePreviousConnectionPeripheralUUID:peripheral.identifier.UUIDString];
? ? // 成功連接后的結(jié)果回調(diào)出去
? ? if(_connectCompletion) {
? ? ? ? _connectCompletion(peripheral, nil);
? ? }
? ? // 處于連接狀態(tài)
? ? _state = kFSBLEStageConnection;
? ? // 外設(shè)代理
? ? peripheral.delegate = self;
? ? // 發(fā)現(xiàn)服務(wù)
? ? [peripheral discoverServices:nil];
? ? FSLog(@"成功連接藍(lán)牙外設(shè): %@", peripheral.identifier.UUIDString);
}
// 連接失敗后的回調(diào)
- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error{
? ? if (_connectCompletion) {
? ? ? ? _connectCompletion(peripheral,error);
? ? }
? ? _state = kFSBLEStageConnection;
? ? FSLog(@"連接藍(lán)牙外設(shè)失敗Error: %@", error);
}
// 斷開藍(lán)牙連接
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error {
? ? _connectedPerpheral = nil;
? ? [_writeChatacterDatas removeAllObjects];
? ? if (_disConnectCompletion) {
? ? ? ? _disConnectCompletion(peripheral,error);
? ? }
? ? _state = kFSBLEStageConnection;
? ? FSLog(@"斷開藍(lán)牙外設(shè)連接:%@ -- %@", peripheral, error);
}
九、發(fā)現(xiàn)服務(wù)和特征
1、連接成功后會(huì)調(diào)用外設(shè)代理方法,通過下列幾個(gè)函數(shù)發(fā)現(xiàn)服務(wù)和特征
#pragma mark - CBPeripheralDelegate - 外設(shè)的代理方法
// 發(fā)現(xiàn)服務(wù)的回調(diào)
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error {
? ? if(error) {
? ? ? ? FSLog(@"發(fā)現(xiàn)服務(wù)錯(cuò)誤: %@", error);
? ? ? ? return;
? ? }
? ? FSLog(@"發(fā)現(xiàn)服務(wù)數(shù)組:%@",peripheral.services);
? ? for (CBService *service in peripheral.services) {
? ? ? ? [peripheral discoverCharacteristics:nil forService:service];
? ? }
? ? _state = kFSBLEStageSeekServices;
}
// 發(fā)現(xiàn)特性
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(nullable NSError *)error{
? ? if (error) {
? ? ? ? FSLog(@"發(fā)現(xiàn)特性出錯(cuò) 錯(cuò)誤原因: %@",error.domain);
? ? }else{
? ? ? ? for (CBCharacteristic *character in service.characteristics) {
? ? ? ? ? ? CBCharacteristicProperties properties = character.properties;
? ? ? ? ? ? if (properties & CBCharacteristicPropertyWrite) {
? ? ? ? ? ? ? ? NSDictionary *dict = @{@"character":character,@"type":@(CBCharacteristicWriteWithResponse)};
? ? ? ? ? ? ? ? [_writeChatacterDatas addObject:dict];
? ? ? ? ? ? }
? ? ? ? }
? ? }
? ? if (_writeChatacterDatas.count > 0) {
? ? ? ? _state = kFSBLEStageSeekCharacteristics;
? ? }
}
十、寫數(shù)據(jù)操作
1、發(fā)送數(shù)據(jù)有兩種情況:1,當(dāng)發(fā)送的數(shù)據(jù)小于藍(lán)牙支持的最大長(zhǎng)度,直接發(fā)送即可。2,如果發(fā)送的數(shù)據(jù)長(zhǎng)度大于藍(lán)牙支持最大長(zhǎng)度, 需要進(jìn)行分包發(fā)送,每段長(zhǎng)度設(shè)置成當(dāng)前藍(lán)牙支持的指定長(zhǎng)度,若有剩余,則直接發(fā)送即可。
// 發(fā)送數(shù)據(jù)
- (void)fs_writeData:(NSData *)data completion:(FSWriteCompletion)completion {
? ? if (!_connectedPerpheral) {
? ? ? ? if (completion) {
? ? ? ? ? ? completion(NO,_connectedPerpheral,@"藍(lán)牙設(shè)備未連接");
? ? ? ? }
? ? ? ? return;
? ? }
? ? if (self.writeChatacterDatas.count == 0) {
? ? ? ? if (completion) {
? ? ? ? ? ? completion(NO,_connectedPerpheral,@"該藍(lán)牙設(shè)備不支持發(fā)送數(shù)據(jù)");
? ? ? ? }
? ? ? ? return;
? ? }
? ? NSDictionary *dict = [_writeChatacterDatas lastObject];
? ? _writeCount = 0;
? ? _responseCount = 0;
? ? if (_limitLength <= 0) {
? ? ? ? _results = completion;
? ? ? ? [_connectedPerpheral writeValue:data forCharacteristic:dict[@"character"] type:[dict[@"type"] integerValue]];
? ? ? ? _writeCount ++;
? ? ? ? return;
? ? }
? ? if (data.length <= _limitLength) {
? ? ? ? _results = completion;
? ? ? ? [_connectedPerpheral writeValue:data forCharacteristic:dict[@"character"] type:[dict[@"type"] integerValue]];
? ? ? ? _writeCount ++;
? ? } else {
? ? ? ? // 分段發(fā)送
? ? ? ? NSInteger index = 0;
? ? ? ? for (index = 0; index < data.length - _limitLength; index += _limitLength) {
? ? ? ? ? ? NSData *subData = [data subdataWithRange:NSMakeRange(index, _limitLength)];
? ? ? ? ? ? [_connectedPerpheral writeValue:subData forCharacteristic:dict[@"character"] type:[dict[@"type"] integerValue]];
? ? ? ? ? ? _writeCount++;
? ? ? ? }
? ? ? ? _results = completion;
? ? ? ? NSData *leftData = [data subdataWithRange:NSMakeRange(index, data.length - index)];
? ? ? ? if (leftData) {
? ? ? ? ? ? [_connectedPerpheral writeValue:leftData forCharacteristic:dict[@"character"] type:[dict[@"type"] integerValue]];
? ? ? ? ? ? _writeCount++;
? ? ? ? }
? ? }
}
2、數(shù)據(jù)發(fā)送之后結(jié)果的回調(diào)也是外設(shè)管理類的代理函數(shù)
- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
? ? if (!_results) {
? ? ? ? return;
? ? }
? ? _responseCount ++;
? ? if (_writeCount != _responseCount) {
? ? ? ? return;
? ? }
? ? if (error) {
? ? ? ? FSLog(@"發(fā)送數(shù)據(jù)失敗: %@",error);
? ? ? ? _results(NO,_connectedPerpheral,@"數(shù)據(jù)發(fā)送失敗");
? ? } else {
? ? ? ? _results(YES,_connectedPerpheral,@"數(shù)據(jù)已成功發(fā)送至藍(lán)牙設(shè)備");
? ? }
}
十一、開發(fā)過程中遇到的問題
問題1:直接調(diào)用- (void)fs_scanPeripheralsSuccess:(FSScanPerpheralsSuccess)success failure:(FSScanPeripheralFailure)failure函數(shù)時(shí),搜索不到設(shè)備的問題,返回的nil。
答:當(dāng)首次調(diào)用函數(shù)搜索設(shè)備外設(shè)時(shí),無法獲取外設(shè)設(shè)備信息的原因是central的state為CBCentralManagerStateUnknown,這個(gè)狀態(tài)表示手機(jī)設(shè)備的藍(lán)牙狀態(tài)為未開啟。解決方法:需要在此委托方法中監(jiān)聽藍(lán)牙狀態(tài)的狀態(tài)改變?yōu)镺N時(shí),去開啟掃描操作(具體看外設(shè)藍(lán)牙狀態(tài)代理方法)。
問題2:外設(shè)藍(lán)牙名稱被修改后可能搜索不到的問題
答: 在測(cè)試的過程中正常獲取藍(lán)牙名稱是通過peripheral.name獲取,但是可能存在這種情況是當(dāng)修改連接過的藍(lán)牙名稱后,可能存在搜索不到的情況。解決方法:在藍(lán)牙的廣播數(shù)據(jù)中 根據(jù)@"kCBAdvDataLocalName"這個(gè)key便可獲得準(zhǔn)確的藍(lán)牙名稱。
問題3:調(diào)用斷開藍(lán)牙的接口,手機(jī)藍(lán)牙并沒有馬上與外設(shè)斷開連接,而是等待5秒左右的時(shí)間后才真正斷開。
答:解決方法:可以與硬件開發(fā)的同事溝通,從設(shè)備收到數(shù)據(jù)后主動(dòng)斷開連接即可。
問題4:是否能長(zhǎng)時(shí)間處于后臺(tái)
答:可以,后臺(tái)長(zhǎng)時(shí)間執(zhí)行需要開啟Background Modes,并勾選如圖選項(xiàng)。

Background Modes
可以在App啟動(dòng)的方法中可以檢測(cè)后臺(tái)是藍(lán)牙的處理情況如圖:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {? ? ? ? UIDevice *device = [UIDevice currentDevice];? ? BOOL backgroundSupported = NO;? ? if([device respondsToSelector:@selector(isMultitaskingSupported)]) {? ? ? ? backgroundSupported = YES;? ? }? ? ? ? if (backgroundSupported) {? ? ? ? __block int index = 0;? ? ? ? NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {? ? ? ? ? ? // 執(zhí)行藍(lán)牙相關(guān)操作? ? ? ? ? ? NSLog(@"[SDK - Background] - %d", index++); // 檢測(cè)后臺(tái)是藍(lán)牙的執(zhí)行情況? ? ? ? }];? ? ? ? [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];? ? ? ? [timer fire]; // 用了fire方法之后會(huì)立即執(zhí)行定時(shí)器的方法? ? }? ? return YES;}
問題5:藍(lán)牙允許連接的最大距離支持是多少
答: iOS 藍(lán)牙允許連接的最大距離的限制是10m。
問題6:藍(lán)牙連接成功需要多長(zhǎng)時(shí)間
答:正常連接周圍的藍(lán)牙外設(shè)一般時(shí)在5秒內(nèi),如圖下:

連接成功的時(shí)間差

連接成功的時(shí)間差
問題7:藍(lán)牙成功發(fā)送數(shù)據(jù)需要多少時(shí)間
答:下圖的發(fā)送數(shù)據(jù)時(shí)一張圖

發(fā)送的是圖片
問題8:多臺(tái)設(shè)備是否能同時(shí)連接
答:官方文檔,以及藍(lán)牙底層協(xié)議,說明理論上可以支持到同時(shí)連接 7 個(gè),但這 7 個(gè)能同時(shí)正常工作么?貌似不能(三個(gè)藍(lán)牙耳機(jī)測(cè)試的結(jié)果),畢竟對(duì)于 iOS 而言,藍(lán)牙也是一種資源,同時(shí)連接和同時(shí)使用消耗,占用的資源肯定不同,而且不同手機(jī),性能也不同。
實(shí)現(xiàn)思路:
一個(gè)中心設(shè)備CBCentralManager,連接多個(gè)外設(shè)設(shè)備CBPeripheral,創(chuàng)建一個(gè)中心設(shè)備,需要連接何種設(shè)備,就單獨(dú)去連接即可(換句話說就是多次實(shí)現(xiàn)單連接)。
for(Model *model in _peripheralMarr) {CBPeripheral? *perip = mo.peripheral;[self.centralManager connectPeripheral:perip options:nil];perip.delegate =self;[self.perip discoverServices:nil];}
1、將成功添加的外設(shè)CBPeripheral添加到外設(shè)數(shù)組中(連接成功后處理)
2、 每個(gè)外設(shè)設(shè)備都對(duì)應(yīng)一個(gè)唯一的peripheral.identifier或ServiceUUID,所以可以利用他們獲取到之前連接的外設(shè)數(shù)組,根據(jù)這個(gè)標(biāo)識(shí),匹配到對(duì)應(yīng)的設(shè)備和實(shí)現(xiàn)重連機(jī)制。
3、若手動(dòng)斷開外設(shè)連接,需要將之從外設(shè)數(shù)組中移除掉。
問題9:如何保證發(fā)送數(shù)據(jù)的完整性
答:在做水下無人機(jī)的藍(lán)牙發(fā)送指令給攝像頭時(shí),存在一個(gè)問題就是發(fā)送的指令過長(zhǎng),大約200字節(jié)左右,但是海思提供的攝像頭藍(lán)牙內(nèi)部能接收的緩沖區(qū)長(zhǎng)度只有16~18字節(jié)左右的長(zhǎng)度,所以做 了一個(gè)分包發(fā)送的操作,保證了數(shù)據(jù)的完整性。_limitLength表示自定義每次發(fā)包限制的長(zhǎng)度大小。系統(tǒng)提供了函數(shù)可根據(jù)寫入類型CBCharacteristicWriteWithResponse、CBCharacteristicWriteWithoutResponse獲取最大的寫入長(zhǎng)度:- (NSUInteger)maximumWriteValueLengthForType:(CBCharacteristicWriteType)type
if (data.length <= _limitLength) {
? ? ? ? _results = completion;
? ? ? ? [_connectedPerpheral writeValue:data forCharacteristic:dict[@"character"] type:[dict[@"type"] integerValue]];
? ? ? ? _writeCount ++;
? //根據(jù)接收模塊的處理能力做相應(yīng)延時(shí),因?yàn)樗{(lán)牙設(shè)備處理指令需要時(shí)間,所以我這邊給了400~500毫秒
? ? ? ? usleep(400 * 1000);
? ? } else {
? ? ? ? // 分段發(fā)送
? ? ? ? NSInteger index = 0;
? ? ? ? for (index = 0; index < data.length - _limitLength; index += _limitLength) {
? ? ? ? ? ? NSData *subData = [data subdataWithRange:NSMakeRange(index, _limitLength)];
? ? ? ? ? ? [_connectedPerpheral writeValue:subData forCharacteristic:dict[@"character"] type:[dict[@"type"] integerValue]];
? ? ? ? ? ? _writeCount++;
? ? ? ? ? ? usleep(400 * 1000);
? ? ? ? }
? ? ? ? _results = completion;
? ? ? ? NSData *leftData = [data subdataWithRange:NSMakeRange(index, data.length - index)];
? ? ? ? if (leftData) {
? ? ? ? ? ? [_connectedPerpheral writeValue:leftData forCharacteristic:dict[@"character"] type:[dict[@"type"] integerValue]];
? ? ? ? ? ? _writeCount++;
? ? ? ? ? ? usleep(400 * 1000);
? ? ? ? }
? ? }
問題10:如何實(shí)現(xiàn)重連機(jī)制
答: 文中提供的重連機(jī)制是,自動(dòng)重連函數(shù)被調(diào)用之后,會(huì)設(shè)置一個(gè)全局標(biāo)識(shí)為_isAutoConnect=YES,然后判斷手機(jī)設(shè)備的藍(lán)牙是否開啟,若開啟,則重連掃描外設(shè)設(shè)備,當(dāng)掃到上一次連接的藍(lán)牙設(shè)備后就會(huì)調(diào)用連接的代理函數(shù),并停止掃描。
原理:如果手動(dòng)殺掉APP,那么再次打開APP的時(shí)候APP是不會(huì)自動(dòng)連接設(shè)備的,但是由于系統(tǒng)藍(lán)牙此時(shí)還是與手表連接中的,所以需要重新掃描設(shè)備(因?yàn)樵趻呙璧拇砗瘮?shù)中添加了自動(dòng)連接的邏輯),經(jīng)過測(cè)試,當(dāng)掃描到上次連接上的藍(lán)牙外設(shè)后就會(huì)停止。
方式一:直接掃描重連
// 公共的 自動(dòng)重連函數(shù)
- (void)fs_autoConnectPreviousPeripheral:(FSConnectPeripheralCompletion)completion {
? ? _connectCompletion = completion;
? ? _isAutoConnect = YES;
? ? if (_centralManager.state == CBManagerStatePoweredOn) {
? ? ? ? [_centralManager scanForPeripheralsWithServices:nil options:nil];
? ? }
}
在函數(shù)- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *,id> *)advertisementData RSSI:(NSNumber *)RSSI中實(shí)現(xiàn)。
? ? // 自動(dòng)連接上一次連接的外設(shè)
? ? if (_isAutoConnect) {
? ? ? ? NSString *uuid = [self fs_previousConnectionPeripheralUUID];
? ? ? ? if ([peripheral.identifier.UUIDString isEqualToString:uuid]) {
? ? ? ? ? ? peripheral.delegate = self;
? ? ? ? ? ? [_centralManager connectPeripheral:peripheral options:nil];
? ? ? ? }
? ? }
方式二:通過系統(tǒng)提供的函數(shù)retrieveConnectedPeripheralsWithServices
NSArray *temp = [_centralManager retrieveConnectedPeripheralsWithServices:@[[CBUUID UUIDWithString:ServiceUUID]]];
if(temp.count>0) {
? ? CBPeripheral *per = temp[0];
? ? per.delegate = self;
? ? [_centralManager connectPeripheral:peripheral options:nil];
}
方式三:通過系統(tǒng)提供的函數(shù)retrievePeripheralsWithIdentifiers
NSArray<CBPeripheral *> *knownPeripherals = [_centralManager retrievePeripheralsWithIdentifiers:@[peripheral.identifier]];
? ? if (knownPeripherals.count == 0) {
? ? ? ? return;
? ? }
? ? self.peripheral = knownPeripherals[0];
? ? self.peripheral.delegate = self;
? ? [_centralManager connectPeripheral:self.peripheral
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? options:@{CBConnectPeripheralOptionNotifyOnDisconnectionKey: @YES}];
問題11:如何獲取已經(jīng)配對(duì)過的藍(lán)牙外設(shè)
答: 系統(tǒng)一共提供了兩個(gè)函數(shù)來獲取已經(jīng)配對(duì)過的藍(lán)牙外設(shè),NSArray *[_centralManager retrieveConnectedPeripheralsWithServices:<#(nonnull NSArray<CBUUID *> *)#>];( CBUUID指的是ServiceUUID)、[_centralManager retrievePeripheralsWithIdentifiers:<#(nonnull NSArray<NSUUID *> *)#>];參數(shù)是個(gè)已連接的ServiceUUID或Identifiers的數(shù)組,是個(gè)必填項(xiàng),若傳@[]空數(shù)組,則返回值是nil。
問題12:開發(fā)藍(lán)牙 APP,有什么工具可以協(xié)助藍(lán)牙測(cè)試
答: 首先測(cè)試藍(lán)牙必須時(shí)真機(jī),其次安裝了藍(lán)牙調(diào)試助手或LightBlue等第三方App來調(diào)試藍(lán)牙的開發(fā)
IMG_9657.PNG
問題13:App作為中心設(shè)備端,連接到藍(lán)牙設(shè)備之后,如何獲取外設(shè)設(shè)備的Mac地址。
答:iOS端是無法直接獲取設(shè)備的Mac地址,但是可以間接獲取,但都需要和硬件工程師進(jìn)行溝通。
1,將藍(lán)牙外設(shè)廣播里,提供Mac地址,這樣中心設(shè)備端在掃描階段,可以直接讀取廣播里的值,從而獲取到外設(shè)設(shè)備的Mac地址。
2,可以在外設(shè)設(shè)備的某個(gè)服務(wù)的特征中,提供Mac地址,但是前提是要確定是讀取哪個(gè)特征,UUID是多少。
問題14:為什么兩個(gè) iPhone 手機(jī)的都打開藍(lán)牙之后,卻相互搜不到彼此手機(jī)上的同個(gè)藍(lán)牙Demo。
答:在藍(lán)牙通信中,分為中心端和設(shè)備端。而通常手機(jī)藍(lán)牙Demo都處在中心端狀態(tài),也就是只能接收廣播,而自己沒有向周圍發(fā)送廣播。所以兩臺(tái)手機(jī)之間一般是無法發(fā)現(xiàn)對(duì)方的(因?yàn)榇蠹叶际侵行亩耍?/p>
十二、階段性總結(jié)
上述代碼基本完成了App掃描外設(shè)設(shè)備、連接外設(shè)設(shè)備到發(fā)送數(shù)據(jù)的基本流程,需要深化的點(diǎn)在用戶體驗(yàn)相關(guān),比如:連接超時(shí)后的處理等。后續(xù)分享會(huì)加入發(fā)送數(shù)據(jù)后的打印操作,待續(xù)。