iOS 藍(lán)牙開發(fā)技術(shù)分享

一壤躲、技術(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è)備的特征

三统阿、申請權(quán)限

1彩倚、需要在info.plist文件中添加相對應(yīng)的鍵值對 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ù)serviceUUID來辨別是否是我們連接的設(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踩娘、獲取已配對過的藍(lán)牙外設(shè)
- (NSArray<CBPeripheral *> *)retrievePeripheralsWithIdentifiers:(NSArray<NSUUID *> *)identifiers NS_AVAILABLE(10_9, 7_0);

- (NSArray<CBPeripheral *> *)retrieveConnectedPeripheralsWithServices:(NSArray<CBUUID *> *)serviceUUIDs NS_AVAILABLE(10_9, 7_0);
  • 4刮刑、掃描外設(shè)(如果參數(shù)傳nil喉祭,表示掃描所有外設(shè))和停止掃描
- (void)scanForPeripheralsWithServices:(nullable NSArray<CBUUID *> *)serviceUUIDs options:(nullable NSDictionary<NSString *, id> *)options;

- (void)stopScan;
  • 5、連接指定外設(shè)
- (void)connectPeripheral:(CBPeripheral *)peripheral options:(nullable NSDictionary<NSString *, id> *)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<NSString *, id> *)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<CBService *> *services;
  • 5、發(fā)現(xiàn)服務(wù)
- (void)discoverServices:(nullable NSArray<CBUUID *> *)serviceUUIDs;
  • 6柳琢、發(fā)現(xiàn)子服務(wù)
- (void)discoverIncludedServices:(nullable NSArray<CBUUID *> *)includedServiceUUIDs forService:(CBService *)service;
  • 7绍妨、發(fā)現(xiàn)特征
- (void)discoverCharacteristics:(nullable NSArray<CBUUID *> *)characteristicUUIDs forService:(CBService *)service;
  • 8、藍(lán)牙發(fā)送數(shù)據(jù)有字節(jié)長度大小限制柬脸,該函數(shù)是獲取允許最大字節(jié)長度限制
- (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<CBService *> *)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活逆、檢測中心設(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炫七、篩選出peripheralnil的外設(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)牙支持的最大長度利耍,直接發(fā)送即可蚌本。2盔粹,如果發(fā)送的數(shù)據(jù)長度大于藍(lán)牙支持最大長度, 需要進(jìn)行分包發(fā)送程癌,每段長度設(shè)置成當(dāng)前藍(lán)牙支持的指定長度舷嗡,若有剩余,則直接發(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è)備信息的原因是centralstateCBCentralManagerStateUnknown,這個(gè)狀態(tài)表示手機(jī)設(shè)備的藍(lán)牙狀態(tài)為未開啟援雇。解決方法:需要在此委托方法中監(jiān)聽藍(lán)牙狀態(tài)的狀態(tài)改變?yōu)?code>ON時(shí),去開啟掃描操作(具體看外設(shè)藍(lán)牙狀態(tài)代理方法)椎扬。

  • 問題2:外設(shè)藍(lán)牙名稱被修改后可能搜索不到的問題
    : 在測試的過程中正常獲取藍(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:是否能長時(shí)間處于后臺(tái)
    :可以贿肩,后臺(tái)長時(shí)間執(zhí)行需要開啟Background Modes,并勾選如圖選項(xiàng)失仁。

    Background Modes

可以在App啟動(dòng)的方法中可以檢測后臺(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++); // 檢測后臺(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)牙連接成功需要多長時(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ī)測試的結(jié)果)茬射,畢竟對于 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è)備都對應(yīng)一個(gè)唯一的peripheral.identifier或ServiceUUID,所以可以利用他們獲取到之前連接的外設(shè)數(shù)組氮唯,根據(jù)這個(gè)標(biāo)識(shí)鉴吹,匹配到對應(yīng)的設(shè)備和實(shí)現(xiàn)重連機(jī)制。

  • 3惩琉、若手動(dòng)斷開外設(shè)連接豆励,需要將之從外設(shè)數(shù)組中移除掉。

  • 問題9:如何保證發(fā)送數(shù)據(jù)的完整性
    :在做水下無人機(jī)的藍(lán)牙發(fā)送指令給攝像頭時(shí)瞒渠,存在一個(gè)問題就是發(fā)送的指令過長肆糕,大約200字節(jié)左右,但是海思提供的攝像頭藍(lán)牙內(nèi)部能接收的緩沖區(qū)長度只有16~18字節(jié)左右的長度在孝,所以做 了一個(gè)分包發(fā)送的操作,保證了數(shù)據(jù)的完整性淮摔。_limitLength表示自定義每次發(fā)包限制的長度大小私沮。系統(tǒng)提供了函數(shù)可根據(jù)寫入類型CBCharacteristicWriteWithResponse、CBCharacteristicWriteWithoutResponse獲取最大的寫入長度:- (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)過測試尚洽,當(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)配對過的藍(lán)牙外設(shè)
    : 系統(tǒng)一共提供了兩個(gè)函數(shù)來獲取已經(jīng)配對過的藍(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)牙測試
    : 首先測試藍(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)對方的(因?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)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子昵骤,更是在濱河造成了極大的恐慌树碱,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件变秦,死亡現(xiàn)場離奇詭異成榜,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)蹦玫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門赎婚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人樱溉,你說我怎么就攤上這事挣输。” “怎么了福贞?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵撩嚼,是天一觀的道長。 經(jīng)常有香客問我挖帘,道長完丽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任拇舀,我火速辦了婚禮逻族,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘骄崩。我一直安慰自己瓷耙,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布刁赖。 她就那樣靜靜地躺著,像睡著了一般长搀。 火紅的嫁衣襯著肌膚如雪宇弛。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天源请,我揣著相機(jī)與錄音枪芒,去河邊找鬼。 笑死谁尸,一個(gè)胖子當(dāng)著我的面吹牛舅踪,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播良蛮,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼抽碌,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了决瞳?” 一聲冷哼從身側(cè)響起货徙,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤左权,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后痴颊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體赏迟,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年蠢棱,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了锌杀。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡泻仙,死狀恐怖糕再,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情饰豺,我是刑警寧澤亿鲜,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站冤吨,受9級特大地震影響蒿柳,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜漩蟆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一垒探、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧怠李,春花似錦圾叼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至髓介,卻和暖如春惕鼓,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背唐础。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工箱歧, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人一膨。 一個(gè)月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓呀邢,卻偏偏與公主長得像,于是被迫代替她去往敵國和親豹绪。 傳聞我的和親對象是個(gè)殘疾皇子价淌,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容