iOS CoreBluetooth 的使用講解

最近研究了iOS下鏈接藍牙打印機,實現打印購物小票的功能,對iOS中BLE 4.0的使用有了必定的瞭解,這裏記錄一下對BLE 4.0的理解。html

因爲不少文章同時講CBCentralManager和CBPeripheralManager,因此很容易傻傻分不清楚。不多把iPhone做爲藍牙外設在廣播發送數據的情形,今天我就從iOS app開發的角度講一些BLE 4.0的使用。數組

概念

CBPeripheral 藍牙外設,好比藍牙手環、藍牙心跳監視器、藍牙打印機。 CBCentralManager藍牙外設管理中心,與手機的藍牙硬件模板關聯,能夠獲取到手機中藍牙模塊的一些狀態等,可是管理的就是藍牙外設。bash

CBService 藍牙外設的服務,每個藍牙外設都有0個或者多個服務。而每個藍牙服務又可能包含0個或者多個藍牙服務,也可能包含0個或者多個藍牙特性。服務器

CBCharacteristic 每個藍牙特性中都包含有一些數據或者信息。 網絡

BLE之間的關係圖.png

分析

咱們通常的交互,是app做爲客戶端,而用戶的實際數據多存儲在服務器上,因此app客戶端主動經過網絡接口從服務器端獲取數據,而後在app中展現這些數據。app

而藍牙有一些不一樣,app是外設管理中心(CBCentralManager),可是它也是客戶端。而實際的數據是從藍牙外設(CBPeripheral),也就是藍牙手環等這類設備中獲取,因此CBPeripheral就至關因而服務器,與他們有些不一樣的是,藍牙數據傳輸是服務器(CBPeripheral)一直在廣播發送數據,app客戶端鏈接監聽某個藍牙後,就會收到其發送過來的數據展現。框架

藍牙外設,無論有沒有別的設備鏈接它,藍牙外設都會廣播發送數據。ide

情景一 只涉及從藍牙外設中讀數據測試

藍牙手環ui

藍牙手環一直往外廣播發送心跳和走路的步數,當咱們的app經過藍牙鏈接到藍牙手環後,就能夠在外設的代理方法中,獲取廣播發出的數據了,而後在app的UI中更新數據便可。

情景二 往藍牙外設中寫數據

藍牙打印機

藍牙打印機是app中經過藍牙鏈接到藍牙打印機以後,利用外設的代理方法,往藍牙打印機中寫入數據後,藍牙打印機就會自動打印出小票。

情景三 兩臺iOS 設備經過app互傳文件

一臺設備不能既是外設,又是管理中心。

它能夠既廣播發送數據,又獲取其餘設備的數據,可是它只能扮演一種角色,若是iOS 設備A 經過藍牙主動鏈接了 設備B,那麼設備A是CBCentral,設備B是CBPeripheral;可是若是是設備B鏈接了設備A,那麼設備B就是CBCentral,設備A是CBPeripheral

#代碼實戰

第一步,建立CBCentralManager。 第二步,掃描可鏈接的藍牙外設(必須在藍牙模塊打開的前提下)。 第三步,鏈接目標藍牙外設。 第四步,查詢目標藍牙外設下的服務。 第五步,遍歷服務中的特性,獲取特性中的數據或者保存某些可寫的特性,或者設置某些特性值改變時,通知主動獲取。 第六步,在通知更新特性中值的方法中讀取特性中的數據(再設置特性的通知爲YES的狀況下)。 第七步,讀取特性中的值。 第八步,若是有可寫特性,而且須要向藍牙外設寫入數據時,寫入數據發送給藍牙外設。

首先是是在咱們app中,建立一個CBCentralManager:

// 1.建立管理中心,這裏也能夠設置子線程
    CBCentralManager *manager = [[CBCentralManager alloc] initWithDelegate:self queue:dispatch_get_main_queue()];
複製代碼

建立完以後,就會調用一次CBCentralManagerDelegate的代理方法:

- (void)centralManagerDidUpdateState:(CBCentralManager *)central
{
    NSLog(@"%@",central);
    switch (central.state) {
        case CBCentralManagerStatePoweredOn:
            NSLog(@"打開,可用");
            [_manager scanForPeripheralsWithServices:nil options:@{CBCentralManagerScanOptionAllowDuplicatesKey:@(NO)}];
            break;
        case CBCentralManagerStatePoweredOff:
            NSLog(@"可用,未打開");
            break;
        case CBCentralManagerStateUnsupported:
            NSLog(@"SDK不支持");
            break;
        case CBCentralManagerStateUnauthorized:
            NSLog(@"程序未受權");
            break;
        case CBCentralManagerStateResetting:
            NSLog(@"CBCentralManagerStateResetting");
            break;
        case CBCentralManagerStateUnknown:
            NSLog(@"CBCentralManagerStateUnknown");
            break;
    }
}
複製代碼

該代理方法,在藍牙模板的狀態發生改變的時候,就會回調。應該在藍牙打開的狀態下,再去搜索掃描可用的藍牙外設列表。 掃描藍牙外設是經過以下方法:

- (void)scanForPeripheralsWithServices:(nullable NSArray<CBUUID *> *)serviceUUIDs options:(nullable NSDictionary<NSString *, id> *)options;
複製代碼

第一個參數是服務的CBUUID數組,咱們能夠搜索具備某一類服務的藍牙設備,比較重要。 掃描到藍牙外設後,會調用CBCentralManagerDelegate的這個代理方法:

- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *, id> *)advertisementData RSSI:(NSNumber *)RSSI;
複製代碼

該方法一次只返回一個藍牙外設的信息。第二個參數是掃描到的藍牙外設,第三個參數是藍牙外設中 的額外數據,RSSI是信號強度的參數。

由於可能某個藍牙是無用的或者重複掃描到某一個藍牙,因此咱們須要剔除一些無用的藍牙,替換掉舊的藍牙外設(可能該外設的參數有變化,不是攜帶的數據,是外設自己的參數變化)。

- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *, id> *)advertisementData RSSI:(NSNumber *)RSSI
{
    if (peripheral.name.length <= 0) {
        return ;
    }
    
    NSLog(@"Discovered name:%@,identifier:%@,advertisementData:%@,RSSI:%@", peripheral.name, peripheral.identifier,advertisementData,RSSI);
    if (self.deviceArray.count == 0) {
        NSDictionary *dict = @{@"peripheral":peripheral, @"RSSI":RSSI};
        [self.deviceArray addObject:dict];
    } else {
        BOOL isExist = NO;
        for (int i = 0; i < self.deviceArray.count; i++) {
            NSDictionary *dict = [self.deviceArray objectAtIndex:i];
            CBPeripheral *per = dict[@"peripheral"];
            if ([per.identifier.UUIDString isEqualToString:peripheral.identifier.UUIDString]) {
                isExist = YES;
                NSDictionary *dict = @{@"peripheral":peripheral, @"RSSI":RSSI};
                [_deviceArray replaceObjectAtIndex:i withObject:dict];
            }
        }
        
        if (!isExist) {
            NSDictionary *dict = @{@"peripheral":peripheral, @"RSSI":RSSI};
            [self.deviceArray addObject:dict];
        }
    }
    
    [self.tableView reloadData];
}
複製代碼

這樣就獲取到了藍牙設備列表,咱們能夠在表格中展現藍牙設備列表

藍牙外設列表.png
到這裏只獲取到了可鏈接的藍牙外設,當咱們鏈接到某個藍牙外設後,就能夠去獲取它的數據了。

在cell點擊事件中鏈接某個藍牙外設:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSDictionary *dict = [self.deviceArray objectAtIndex:indexPath.row];
    CBPeripheral *peripheral = dict[@"peripheral"];
    // 鏈接某個藍牙外設
    [self.manager connectPeripheral:peripheral options:@{CBConnectPeripheralOptionNotifyOnDisconnectionKey:@(YES)}];
    // 設置外設的代理是爲了後面查詢外設的服務和外設的特性,以及特性中的數據。
    [peripheral setDelegate:self];
    // 既然已經鏈接到某個藍牙了,那就不須要在繼續掃描外設了
    [self.manager stopScan];
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
}
複製代碼

鏈接某個外設成功後,查找其具備的服務

- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
{
    NSLog(@"didConnectPeripheral");
    // 鏈接成功後,查找服務
    [peripheral discoverServices:nil];
}

- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error
{
    NSLog(@"didFailToConnectPeripheral");
}
複製代碼

查找服務的代理方法就是CBPeripheralDelegate中的了:

#pragma mark - CBPeripheralDelegate
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(nullable NSError *)error
{
    NSString *UUID = [peripheral.identifier UUIDString];
    NSLog(@"didDiscoverServices:%@",UUID);
    if (error) {
        NSLog(@"出錯");
        return;
    }
    
    CBUUID *cbUUID = [CBUUID UUIDWithString:UUID];
    NSLog(@"cbUUID:%@",cbUUID);
    
    for (CBService *service in peripheral.services) {
        NSLog(@"service:%@",service.UUID);
        //若是咱們知道要查詢的特性的CBUUID,能夠在參數一中傳入CBUUID數組。
        [peripheral discoverCharacteristics:nil forService:service];
    }
}
複製代碼

再而後是遍歷服務中的特性:

- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(nullable NSError *)error
{
    if (error) {
        NSLog(@"出錯");
        return;
    }
    
    for (CBCharacteristic *character in service.characteristics) {
        // 這是一個枚舉類型的屬性
        CBCharacteristicProperties properties = character.properties;
        if (properties & CBCharacteristicPropertyBroadcast) {
            //若是是廣播特性
        }
        
        if (properties & CBCharacteristicPropertyRead) {
            //若是具有讀特性,便可以讀取特性的value
            [peripheral readValueForCharacteristic:character];
        }
        
        if (properties & CBCharacteristicPropertyWriteWithoutResponse) {
            //若是具有寫入值不須要響應的特性
            //這裏保存這個能夠寫的特性,便於後面往這個特性中寫數據
            _chatacter = character;
        }
        
        if (properties & CBCharacteristicPropertyWrite) {
            //若是具有寫入值的特性,這個應該會有一些響應
        }
        
        if (properties & CBCharacteristicPropertyNotify) {
            //若是具有通知的特性,無響應
            [peripheral setNotifyValue:YES forCharacteristic:character];
        }
    }
}
複製代碼

而後通知的代理方法以下:

- (void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(nonnull CBCharacteristic *)characteristic error:(nullable NSError *)error
{
    if (error) {
        NSLog(@"錯誤didUpdateNotification:%@",error);
        return;
    }
    
    CBCharacteristicProperties properties = characteristic.properties;
    if (properties & CBCharacteristicPropertyRead) {
        //若是具有讀特性,便可以讀取特性的value
        [peripheral readValueForCharacteristic:characteristic];
    }
}
複製代碼

讀取特性中的value的方法以下:

// 讀取新值的結果
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
{
    if (error) {
        NSLog(@"錯誤:%@",error);
        return;
    }
    
    NSData *data = characteristic.value;
    if (data.length <= 0) {
        return;
    }
    NSString *info = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    
    NSLog(@"info:%@",info);
}
複製代碼

到這裏,獲取藍牙外設廣播發送出來的值就已經完畢了。

想要向藍牙外設寫入數據,則調用以下方法:

 [peripheral writeValue:infoData forCharacteristic:_chatacter type:CBCharacteristicWriteWithoutResponse];
複製代碼

只是這裏的_chatacter參數應該是遍歷服務器的特性時,遍歷出來的那個可寫的特性。若是藍牙外設沒有可寫特性,則不能向其寫入數據。

另外取消與某藍牙外設的鏈接方法是:

[self.manager cancelPeripheralConnection:peripheral];
複製代碼

CBCentralManagerDelegate中也有斷開藍牙鏈接的代理方法:

- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error;
複製代碼

iOS 10 補充

經 @一腳踢飛提醒:developer.apple.com/reference/c…

Important: To protect user privacy, an iOS app linked on or after iOS 10.0, and which accesses the Bluetooth interface, must statically declare the intent to do so. Include the NSBluetoothPeripheralUsageDescription key in your app’s Info.plist  file and provide a purpose string for this key. If your app attempts to access the Bluetooth interface without a corresponding purpose string, your app exits。 tip: This key is supported in iOS 6.0 and later.

可是我測試在iOS 10.0.1中測試,不加NSBluetoothPeripheralUsageDescription,工程仍然能夠正常使用。 而後加上NSBluetoothPeripheralUsageDescription後,

應用啓動時也並無像定位、推送等那樣的提示😞 😞 😞。在設置中,藍牙功能目前還並未看到容許使用的應用列表,估計蘋果只是在將來規劃的吧。

補充

鑑於常常有人問爲啥工程裏能搜到藍牙打印機,可是卻搜不到其餘手機的藍牙?

那是由於藍牙技術發展至今,也從 1.x 發展到 4.0了,藍牙通訊使用的材料、技術等都發生了變化。這就是爲何有的打印機支持 2.0、3.0、4.0,若是你使用的是CoreBluetooth庫,而打印機不支持 藍牙 4.0,那你固然搜索不到藍牙打印機啦!

手機設置裏的藍牙搜索功能,使用的是什麼技術實現的,有木有兼容 2.0、3.0、4.0那就不得而知了。

而 iOS 中的 藍牙庫 也不止 CoreBluetooth 一個,還有其餘的呢!

GameKit.framework:iOS7以前的藍牙通信框架,從iOS7開始過時,可是目前多數應用仍是基於此框架。

MultipeerConnectivity.framework:iOS7開始引入的新的藍牙通信開發框架,用於取代GameKit。

CoreBluetooth.framework:功能強大的藍牙開發框架,要求設備必須支持藍牙4.0。

更多關於藍牙相關的知識:

藍牙--百度百科

能夠只看iOS中三個藍牙庫的介紹

到這裏藍牙的基本使用就結束了! Have fun!

相關文章
相關標籤/搜索