iOS 藍(lán)牙開發(fā)流程總結(jié)

一、技術(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ù)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容