iOS藍(lán)牙開發(fā),踩過的坑

???????近五年做的項(xiàng)目都是和藍(lán)牙有關(guān),網(wǎng)上關(guān)于藍(lán)牙開發(fā)的文章也比比皆是。此文只是記錄我司在實(shí)際工作中遇到的一些特定的應(yīng)用場(chǎng)景和解決小技巧,并對(duì)網(wǎng)絡(luò)上通用的開發(fā)技術(shù)流程的梳理總結(jié)。記錄以備回顧,也分享歡迎交流。

一、特定的使用場(chǎng)景

1、藍(lán)牙外設(shè)設(shè)備升級(jí)等,大數(shù)據(jù)傳輸,耗時(shí)操作

??????? 數(shù)據(jù)發(fā)送時(shí)選擇CBCharacteristicWriteWithResponse,會(huì)影響總交互時(shí)間, 使用CBCharacteristicWriteWithoutResponse又回出現(xiàn)丟包的現(xiàn)象。
[self.peripheral writeValue:data forCharacteristic:self.characteristic type:CBCharacteristicWriteWithResponse];
[self.peripheral writeValue:data forCharacteristic:self.characteristic type:CBCharacteristicWriteWithResponse];
???????如果交互的總時(shí)間我們不能接受,可以選用CBCharacteristicWriteWithoutResponse模式每包20字節(jié)循環(huán)發(fā),但注意每發(fā)12包延遲100毫秒(經(jīng)驗(yàn)值,12包240字節(jié)小于250),即可加快大數(shù)據(jù)傳輸速率。此方法也分享給使用我司外設(shè)硬件的客戶(多為政企,銀行),截止目前iOS13,屢試不爽。

2、需要藍(lán)牙傳輸在后臺(tái)進(jìn)行

???????我們藍(lán)牙開發(fā)為了能使app在后臺(tái)運(yùn)行時(shí)依舊保持與外設(shè)的連接,就需要在工程目錄下的 info.plist 文件中, iOS9及其以前的做法:新建一行 Required background modes , 加入下面兩項(xiàng)。
App shares data using CoreBluetooth
App communicates using CoreBluetooth
???????高版本的XCode直接在項(xiàng)目的Capablities中,在Background Modes下添加Uses Bluetooth LE accessories即可

3、連接斷開后,在后臺(tái)再次掃描周圍設(shè)備再完成重連。

???????后臺(tái)掃描設(shè)備跟前臺(tái)掃描周圍設(shè)備有一點(diǎn)不同:
???????也許是考慮到功耗的原因,在后臺(tái)只能搜索特定的設(shè)備,所以必須要傳Service UUID。不傳的 話一臺(tái)設(shè)備都搜不到。而這時(shí)就需要外設(shè)在廣播包中有Service UUID,需要廣播包中含有。

4、首次使用APP時(shí)候掃描設(shè)備,并顯示出每臺(tái)設(shè)備的mac地址,點(diǎn)擊設(shè)備進(jìn)行綁定。后續(xù)直接鏈接綁定的設(shè)備。

???????此場(chǎng)景Andriod可以做到,iOS不可以,是由于iOS端掃描到的設(shè)備對(duì)象獲取不到mac地址,需要連接上設(shè)備獲取服務(wù)和 特征值后才能得到mac地址。解決方法可以讓硬件工程師在廣播的時(shí)候添加mac地址,如果不然,只能逐個(gè)連接斷開嘗試,獲取匹配的mac值。
最后我們硬件工程師在廣播包中添加了設(shè)備序列號(hào)。根據(jù)序列號(hào)區(qū)分藍(lán)牙設(shè)備。畢竟序列號(hào)區(qū)別于mac地址,用戶看起來更直觀。

另:推薦LightBlue App,基于CoreBluetooth。是BLE開發(fā)的調(diào)試?yán)鳎揂pp上能獲取的數(shù)據(jù),你就能用代碼實(shí)現(xiàn),軟硬件工程師藍(lán)牙開發(fā)必備。

二、CoreBluetooth框架使用

???????目前藍(lán)牙智能硬件主流都是低功耗的藍(lán)牙4.0技術(shù),由于耗電低,也稱作為BLE(Bluetooth Low Energy)),對(duì)應(yīng)iOS的系統(tǒng)庫(kù)是<CoreBluetooth/CoreBluetooth.h>。另外一種是基于Wi-Fi的連接方式,很少見。我們的設(shè)備都是BLE設(shè)備,以下介紹為CoreBluetooth框架。

核心概念:

  • CBCentralManager: 外部設(shè)備管理者
  • CBPeripheral: 連接的外部設(shè)備
  • CBService: 設(shè)備攜帶的服務(wù)
  • CBCharacteristic: 服務(wù)中包含的特征值

使用流程:

創(chuàng)建一個(gè)CBCentralManager實(shí)例
掃描外圍設(shè)備
連接掃描到的設(shè)備
獲得連接設(shè)備的服務(wù)
獲得服務(wù)的特征值
從外圍設(shè)備讀取數(shù)據(jù)
向外圍設(shè)備寫入(發(fā)送)數(shù)據(jù)

三、代碼實(shí)現(xiàn):

1. 初始化

#import <CoreBluetooth/CoreBluetooth.h>
self.centralManager = [[CBCentralManager alloc] initWithDelegate:self
    queue:nil];

2. 搜索掃描外圍設(shè)備

/**
* 初始化成功自動(dòng)調(diào)用
* 必須實(shí)現(xiàn)的代理,用來返回創(chuàng)建的centralManager的狀態(tài)。
* CBCentralManagerStatePoweredOn狀態(tài)才可以掃描到外設(shè):
*/
- (void)centralManagerDidUpdateState:(CBCentralManager *)central{
    switch (central.state) {
        case CBCentralManagerStateUnknown:
            NSLog(@">>>CBCentralManagerStateUnknown");
            break;
        case CBCentralManagerStateResetting:
            NSLog(@">>>CBCentralManagerStateResetting");
            break;
        case CBCentralManagerStateUnsupported:
            NSLog(@">>>CBCentralManagerStateUnsupported");
            break;
        case CBCentralManagerStateUnauthorized:
            NSLog(@">>>CBCentralManagerStateUnauthorized");
            break;
        case CBCentralManagerStatePoweredOff:
            NSLog(@">>>CBCentralManagerStatePoweredOff");
            break;
        case CBCentralManagerStatePoweredOn:
            {
                NSLog(@">>>CBCentralManagerStatePoweredOn");
                /**開始掃描周圍的外設(shè)。
                * 第一個(gè)參數(shù)是用來掃描有指定服務(wù)的外設(shè)。
                * 傳nil表示掃描所有外設(shè)。成功掃描到外設(shè)后調(diào)用didDiscoverPeripheral
                 * */
                [self.centralManager scanForPeripheralsWithServices:nil options:nil];
            }
            break;
        default:
            break;
        }
    }

#pragma mark 發(fā)現(xiàn)外設(shè)

- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *,id> *)advertisementData RSSI:(NSNumber *)RSSI{
    NSLog(@"Find device:%@", [peripheral name]);
    if (![_deviceDic objectForKey:[peripheral name]]) {
         NSLog(@"Find device:%@", [peripheral name]);
        if (peripheral!=nil) {
            if ([peripheral name]!=nil) {
                if ([[peripheral name] hasPrefix:@"過濾"]) {
                    //連接外設(shè)
                    [self.centralManager connectPeripheral:peripheral options:nil];
                 }
             }
         }
     }
}

3.連接外圍設(shè)備

- (void)connectDeviceWithPeripheral:(CBPeripheral *)peripheral
{
    [self.centralManager connectPeripheral:peripheral options:nil];
}

#pragma mark 連接成功回調(diào)

- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral{

    [central stopScan];
    peripheral.delegate = self;
    self.peripheral = peripheral;
    /**
     *外設(shè)的服務(wù)、特征、描述等方法是CBPeripheralDelegate的內(nèi)容,
     *所以要先設(shè)置代理peripheral.delegate = self
     *參數(shù)表示你要的服務(wù)的UUID,nil表示掃描所有服務(wù)。
     *成功發(fā)現(xiàn)服務(wù),回調(diào)didDiscoverServices
     */
     [peripheral discoverServices:@[[CBUUID UUIDWithString:@"服務(wù)UUID"]]];
}

#pragma mark 連接失敗回調(diào)

- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error{
    NSLog(@"%@", error);
}

#pragma mark 取消與外設(shè)的連接回調(diào)

- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error{
     NSLog(@"%@", peripheral);
}

4. 獲得外圍設(shè)備的服務(wù)

#pragma mark 發(fā)現(xiàn)服務(wù)回調(diào)

- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error{

     CBService * __nullable findService = nil;
    // 遍歷服務(wù)
     for (CBService *service in peripheral.services)
     {
         if ([[service UUID] isEqual:[CBUUID UUIDWithString:@"服務(wù)UUID"]])
        {
            findService = service;
        }
     }
     if (findService)
         [peripheral discoverCharacteristics:NULL forService:findService];
}

5、獲得服務(wù)的特征;

#pragma mark 發(fā)現(xiàn)特征回調(diào)
/**發(fā)現(xiàn)特征后,可以根據(jù)特征的properties進(jìn)行:
 *讀readValueForCharacteristic、寫writeValue、訂閱通知setNotifyValue、
 *掃描特征的描述discoverDescriptorsForCharacteristic
 */
- (void)peripheral:(CBPeripheral *)peripheraldidDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error{
    for (CBCharacteristic *characteristic in service.characteristics) {
        if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:@"特征UUID"]]) {
            //讀取成功回調(diào)didUpdateValueForCharacteristic
             self.characteristic = characteristic;
            //接收一次
            //[peripheral readValueForCharacteristic:characteristic];
            //訂閱, 實(shí)時(shí)接收
             [peripheral setNotifyValue:YES forCharacteristic:characteristic];=
            // 發(fā)送下行數(shù)據(jù)
            NSData *data = [@"帶發(fā)送的數(shù)據(jù)"dataUsingEncoding:NSUTF8StringEncoding];
            [self.peripheral writeValue:data forCharacteristic:characteristic type:CBCharacteristicWriteWithResponse];
        }
         //當(dāng)發(fā)現(xiàn)characteristic有descriptor,回調(diào)didDiscoverDescriptorsForCharacteristic
         [peripheral discoverDescriptorsForCharacteristic:characteristic];
     }
}

6.從外圍設(shè)備讀取數(shù)據(jù)

#pragma mark - 獲取數(shù)據(jù)

- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error{
    NSData *data = [characteristic.value dataUsingEncoding:NSUTF8StringEncoding];
}

#pragma mark - 讀取外設(shè)實(shí)時(shí)數(shù)據(jù)

- (void)peripheral:(CBPeripheral *)peripheraldidUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristicerror:(NSError *)error{
     if (characteristic.isNotifying) {
         [peripheral readValueForCharacteristic:characteristic];
     } 
     else {
        //這里出錯(cuò) 一般是連接斷開了
         NSLog(@"%@", characteristic);
        [self.centralManager cancelPeripheralConnection:peripheral];
     }
}

7. 給外圍設(shè)備發(fā)送(寫入)數(shù)據(jù)

/** 根據(jù)獲取到的外圍設(shè)備的寫的服務(wù)特征值 向藍(lán)牙設(shè)備發(fā)送數(shù)據(jù)
 *藍(lán)牙發(fā)送數(shù)據(jù)長(zhǎng)度為20字節(jié),待發(fā)送數(shù)據(jù)大于20字節(jié)需要分包發(fā)送,
 *特別長(zhǎng)的數(shù)據(jù)耗時(shí)較長(zhǎng),解決辦法在文章的開始
 */
- (void)sendData
{
     NSData *data = [@"待發(fā)送數(shù)據(jù)" dataUsingEncoding:NSUTF8StringEncoding];
     [self.peripheral writeValue:data forCharacteristic:self.characteristic type:CBCharacteristicWriteWithResponse];
}

#pragma mark 數(shù)據(jù)寫入成功回調(diào)

- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error{
    NSLog(@"寫入成功");
}

8. 停止掃描、斷開連接

#pragma mark 斷開連接
- (void)disConnectPeripheral{
     /**斷開連接后回調(diào)didDisconnectPeripheral
      *注意斷開后如果要重新掃描這個(gè)外設(shè),
      *需要重新調(diào)用[self.centralManager scanForPeripheralsWithServices:nil options:nil];
      */
     [self.centralManager cancelPeripheralConnection:self.peripheral];
}

- (void)scanDevice
{
    if (_centralManager == nil) {
        self.centralManager = [[CBCentralManager alloc] initWithDelegate:selfqueue:nil];
         [_deviceDic removeAllObjects];
    }
}

#pragma mark 停止掃描外設(shè)

- (void)stopScanPeripheral{
     [self.centralManager stopScan];
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。