iOS藍芽開發 Bluetooth藍芽CoreBluetooth 藍芽中心裝置的實現 藍芽外設的實現 有Demo

wuhao丶發表於2019-02-28

轉載請註明本文地址:www.jianshu.com/p/38a4c6451…

目的

最近公司在做一個iOS藍芽專案,在開發的過程中簡單整理了一些與之相關的基礎知識,在這裡分享一下。整理包括以下內容:

1、iOS藍芽開發的關鍵詞
2、藍芽的簡單介紹
3、CoreBluetooth框架
4、實現iOS藍芽外設(Demo
5、實現iOS藍芽中心裝置(Demo

Demo的執行gif圖如下,中心裝置可以從外設讀取資料,也可以向外設寫入資料。外設也可以向中心裝置傳送資料。
PS:需要使用真機測試。

藍芽外設與中心裝置之間的資料傳輸
藍芽外設與中心裝置之間的資料傳輸

iOS的藍芽開發是圍繞著CoreBluetooth框架來實現的。
下面先從iOS藍芽開發的基本概念說起。

一、iOS藍芽開發的關鍵詞

中心裝置:就是用來掃描周圍藍芽硬體的裝置,比如通過你手機的藍芽來掃描並連線智慧手環,這時候你的手機就是中心裝置。

外設:被掃描的裝置。比如當你用手機的藍芽掃描連線智慧手環的時候,智慧手環就是外設。

中心裝置和外設
中心裝置和外設

廣播:就是外設不停的散播藍芽訊號,讓中心裝置可以掃描到。

外設廣播
外設廣播

服務(services):外設廣播和執行的時候會有服務,可以理解成一個功能模組,中心裝置可以讀取服務。外設可以有多個服務。

特徵(characteristic):在服務中的一個單位,一個服務可以有多個特徵,特徵會有一個value,一般讀寫的資料就是這個value。

服務和特徵.png
服務和特徵.png

UUID:區分不同的服務和特徵,可以理解為服務和特徵的身份證。我們可以用UUID來挑選需要的服務和特徵。

二、藍芽的簡單介紹

偷個懶:藍芽百科
藍芽( Bluetooth® ):是一種短距離無線通訊技術 ,可實現固定裝置、移動裝置和樓宇個人域網之間的短距離資料交換(使用2.4—2.485GHz的ISM波段的UHF無線電波)。藍芽4.2釋出於2014年12月2日,本文釋出的時候,藍芽的最新版本為4.2。

三、CoreBluetooth框架

藍芽開發層次圖
藍芽開發層次圖

如上圖所示,iOS中的藍芽開發框架CoreBluetooth處在藍芽低功耗協議棧的上面,我們開發的時候只是使用CoreBluetooth這個框架,通過CoreBluetooth可以輕鬆實現外設或中心裝置的開發。

CoreBluetooth可以分為兩大模組,中心裝置central,外設peripheral,它們倆各有自己的一套API供我們使用。

中心裝置和外設使用.png
中心裝置和外設使用.png

上圖左邊的就是中心裝置的開發類,我們平時是使用CBCentralManager來進行相關操作。

  • CBCentralManager: 藍芽中心裝置管理類,用來統一排程中心裝置的開發
  • CBPeripheral :藍芽外設,例如藍芽手環、心率監測儀。
  • CBService :藍芽外設的服務,可以有0個或者多個服務。
  • CBCharacteristic :服務中的特徵,每一個藍芽服務中可以有0個或多個特徵,特徵中包含資料資訊。
  • CBUUID:可以理解為服務或特徵的身份證,可以用來選擇需要的服務和特徵。

右邊是外設開發相關類,一般是圍繞著CBPeripheralManager來進行編碼。

  • CBPeripheralManager: 藍芽外設開發時使用,用來開發藍芽外設的中心管理類。
  • CBCentral:藍芽中心裝置,例如用來連線藍芽手環的手機。
  • CBMutableService:外設開發的時候可以新增多個服務,所有這裡用CBMutableService來建立新增服務。
  • CBMutableCharacteristic:每個服務中可以有多個特徵,外設開發給服務新增特徵的時候使用這個類。
  • CBATTRequest:讀或者寫請求。它的例項物件有一個value屬性,用來裝載外設進行藍芽讀取或寫入請求時的資料。一般在外設寫入或讀取的回撥方法中有這一個引數。
    外設新增服務和特徵
    外設新增服務和特徵

四、實現iOS藍芽外設(Demo

外設管理器
外設管理器

1、首先匯入CoreBluetooth框架,並遵守協議

#import <CoreBluetooth/CoreBluetooth.h>
// 遵守CBPeripheralManagerDelegate協議
@interface ViewController () <CBPeripheralManagerDelegate>複製程式碼

2、建立外設管理物件,用一個屬性來強引用這個物件。並且在建立的時候設定代理,宣告放到哪個執行緒。

@property (nonatomic, strong) CBPeripheralManager *peripheralManager;

// 建立外設管理器,會回撥peripheralManagerDidUpdateState方法
self.peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:dispatch_get_main_queue()];複製程式碼

3、當建立CBPeripheralManager的時候,會回撥判斷藍芽狀態的方法。當藍芽狀態沒問題的時候建立外設的Service(服務)和Characteristics(特徵)。

/*
 裝置的藍芽狀態
 CBManagerStateUnknown = 0,  未知
 CBManagerStateResetting,    重置中
 CBManagerStateUnsupported,  不支援
 CBManagerStateUnauthorized, 未驗證
 CBManagerStatePoweredOff,   未啟動
 CBManagerStatePoweredOn,    可用
 */
- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral {
    if (peripheral.state == CBManagerStatePoweredOn) {
        // 建立Service(服務)和Characteristics(特徵)
        [self setupServiceAndCharacteristics];
        // 根據服務的UUID開始廣播
        [self.peripheralManager startAdvertising:@{CBAdvertisementDataServiceUUIDsKey:@[[CBUUID UUIDWithString:SERVICE_UUID]]}];
    }
}複製程式碼

可以先用巨集來做兩個標識字串,用來建立服務和特徵的UUID。
最終把建立好的特徵放進服務,把服務放入中心管理器。

#define SERVICE_UUID @"CDD1"
#define CHARACTERISTIC_UUID @"CDD2"

/** 建立服務和特徵 */
- (void)setupServiceAndCharacteristics {
    // 建立服務
    CBUUID *serviceID = [CBUUID UUIDWithString:SERVICE_UUID];
    CBMutableService *service = [[CBMutableService alloc] initWithType:serviceID primary:YES];
    // 建立服務中的特徵
    CBUUID *characteristicID = [CBUUID UUIDWithString:CHARACTERISTIC_UUID];
    CBMutableCharacteristic *characteristic = [
                                               [CBMutableCharacteristic alloc]
                                               initWithType:characteristicID
                                               properties:
                                               CBCharacteristicPropertyRead |
                                               CBCharacteristicPropertyWrite |
                                               CBCharacteristicPropertyNotify
                                               value:nil
                                               permissions:CBAttributePermissionsReadable |
                                               CBAttributePermissionsWriteable
                                               ];
    // 特徵新增進服務
    service.characteristics = @[characteristic];
    // 服務加入管理
    [self.peripheralManager addService:service];

    // 為了手動給中心裝置傳送資料
    self.characteristic = characteristic;
}複製程式碼

注意CBCharacteristicPropertyNotify這個引數,只有設定了這個引數,在中心裝置中才能訂閱這個特徵。
一般開發中可以設定兩個特徵,一個用來傳送資料,一個用來接收中心裝置寫過來的資料,我們這裡為了方便就只設定了一個特徵。
最後用一個屬性拿到這個特徵,是為了後面單獨傳送資料的時候使用,資料的寫入和讀取最終還是要通過特徵來完成。

4、當中心裝置讀取這個外設的資料的時候會回撥這個方法。

/** 中心裝置讀取資料的時候回撥 */
- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveReadRequest:(CBATTRequest *)request {
    // 請求中的資料,這裡把文字框中的資料發給中心裝置
    request.value = [self.textField.text dataUsingEncoding:NSUTF8StringEncoding];
    // 成功響應請求
    [peripheral respondToRequest:request withResult:CBATTErrorSuccess];
}複製程式碼

5、當中心裝置寫入資料的時候,外設會呼叫下面這個方法。

/** 中心裝置寫入資料的時候回撥 */
- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveWriteRequests:(NSArray<CBATTRequest *> *)requests {
    // 寫入資料的請求
    CBATTRequest *request = requests.lastObject;
    // 把寫入的資料顯示在文字框中
    self.textField.text = [[NSString alloc] initWithData:request.value encoding:NSUTF8StringEncoding];
}複製程式碼

6、還有一個主動給中心裝置傳送資料的方法。

/** 通過固定的特徵傳送資料到中心裝置 */
- (IBAction)didClickPost:(id)sender {
    BOOL sendSuccess = [self.peripheralManager updateValue:[self.textField.text dataUsingEncoding:NSUTF8StringEncoding] forCharacteristic:self.characteristic onSubscribedCentrals:nil];
    if (sendSuccess) {
        NSLog(@"資料傳送成功");
    }else {
        NSLog(@"資料傳送失敗");
    }
}複製程式碼

7、中心裝置訂閱成功的時候回撥。

/** 訂閱成功回撥 */
-(void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central didSubscribeToCharacteristic:(CBCharacteristic *)characteristic {
    NSLog(@"%s",__FUNCTION__);
}複製程式碼

8、中心裝置取消訂閱的時候回撥。

/** 取消訂閱回撥 */
-(void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central didUnsubscribeFromCharacteristic:(CBCharacteristic *)characteristic {
    NSLog(@"%s",__FUNCTION__);
}複製程式碼

以上就是iOS藍芽外設的基本實現流程,當然還有更多的地方可以進一步處理,這就需要投入更多的時間來學習實驗了。

下面進入iOS藍芽開發的主要部分,中心裝置的實現,這也是手機App通常擔任的角色。

五、實現iOS藍芽中心裝置(Demo

中心裝置管理器
中心裝置管理器

1、同外設開發一樣,首先要匯入CoreBluetooth框架。

#import <CoreBluetooth/CoreBluetooth.h>複製程式碼

2、遵守的協議與外設開發不同,中心裝置的開發需要遵循如下兩個協議。

@interface ViewController () <CBCentralManagerDelegate,CBPeripheralDelegate>複製程式碼

3、建立中心管理器並用屬性強引用,建立的時候也會設定代理和選擇執行緒。

@property (nonatomic, strong) CBCentralManager *centralManager;

// 建立中心裝置管理器,會回撥centralManagerDidUpdateState
self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:dispatch_get_main_queue()];複製程式碼

4、當建立中心管理物件的時候,會回撥如下方法用來判斷中心裝置的藍芽狀態。當藍芽狀態沒問題的時候,可以根據外設服務的UUID來掃描需要的外設。所以自然而然的就想到了要定義與外設UUID相同的巨集。

/** 判斷手機藍芽狀態 */
#define SERVICE_UUID        @"CDD1"
#define CHARACTERISTIC_UUID @"CDD2"


- (void)centralManagerDidUpdateState:(CBCentralManager *)central {
    // 藍芽可用,開始掃描外設
    if (central.state == CBManagerStatePoweredOn) {
        NSLog(@"藍芽可用");
        // 根據SERVICE_UUID來掃描外設,如果不設定SERVICE_UUID,則掃描所有藍芽裝置
        [central scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:SERVICE_UUID]] options:nil];
    }
    if(central.state==CBCentralManagerStateUnsupported) {
        NSLog(@"該裝置不支援藍芽");
    }
    if (central.state==CBCentralManagerStatePoweredOff) {
        NSLog(@"藍芽已關閉");
    }
}複製程式碼

5、當掃描到外設之後,就會回撥下面這個方法,可以在這個方法中繼續設定篩選條件,例如根據外設名字的字首來選擇,如果符合條件就進行連線。

/** 發現符合要求的外設,回撥 */
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *, id> *)advertisementData RSSI:(NSNumber *)RSSI {
    // 對外設物件進行強引用
    self.peripheral = peripheral;

//    if ([peripheral.name hasPrefix:@"WH"]) {
//        // 可以根據外設名字來過濾外設
//        [central connectPeripheral:peripheral options:nil];
//    }

    // 連線外設
    [central connectPeripheral:peripheral options:nil];
}複製程式碼

7、當連線成功的時候,就會來到下面這個方法。為了省電,當連線上外設之後,就讓中心裝置停止掃描,並且別忘記設定連線上的外設的代理。在這個方法里根據UUID進行服務的查詢。

/** 連線成功 */
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral{
    // 可以停止掃描
    [self.centralManager stopScan];
    // 設定代理
    peripheral.delegate = self;
    // 根據UUID來尋找服務
    [peripheral discoverServices:@[[CBUUID UUIDWithString:SERVICE_UUID]]];
    NSLog(@"連線成功");
}複製程式碼

8、連線失敗和斷開連線也有各自的回撥方法。在斷開連線的時候,我們可以設定自動重連,根據專案需求來自定義裡面的程式碼。

/** 連線失敗的回撥 */
-(void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error {
    NSLog(@"連線失敗");
}

/** 斷開連線 */
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error {
    NSLog(@"斷開連線");
    // 斷開連線可以設定重新連線
    [central connectPeripheral:peripheral options:nil];
}複製程式碼

9、下面開始處理代理方法。
最開始就是發現服務的方法。這個方法裡可以遍歷服務,找到需要的服務。由於上面做的外設只有一個服務,所以我這裡直接取服務中的最後一個lastObject就行了。
找到服務之後,連貫的動作繼續根據特徵的UUID尋找服務中的特徵。

/** 發現服務 */
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error {

    // 遍歷出外設中所有的服務
    for (CBService *service in peripheral.services) {
        NSLog(@"所有的服務:%@",service);
    }

    // 這裡僅有一個服務,所以直接獲取
    CBService *service = peripheral.services.lastObject;
    // 根據UUID尋找服務中的特徵
    [peripheral discoverCharacteristics:@[[CBUUID UUIDWithString:CHARACTERISTIC_UUID]] forService:service];
}複製程式碼

10、下面這個方法裡做的事情不少。
當發現特徵之後,與服務一樣可以遍歷特徵,根據外設開發人員給的文件找出不同特徵,做出相應的操作。
我的外設只設定了一個特徵,所以也是直接通過lastObject拿到特徵。
再重複一遍,一般開發中可以設定兩個特徵,一個用來傳送資料,一個用來接收中心裝置寫過來的資料。
這裡用一個屬性引用特徵,是為了後面通過這個特徵向外設寫入資料或傳送指令。
readValueForCharacteristic方法是直接讀一次這個特徵上的資料。

/** 發現特徵回撥 */
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error {

    // 遍歷出所需要的特徵
    for (CBCharacteristic *characteristic in service.characteristics) {
        NSLog(@"所有特徵:%@", characteristic);
        // 從外設開發人員那裡拿到不同特徵的UUID,不同特徵做不同事情,比如有讀取資料的特徵,也有寫入資料的特徵
    }

    // 這裡只獲取一個特徵,寫入資料的時候需要用到這個特徵
    self.characteristic = service.characteristics.lastObject;

    // 直接讀取這個特徵資料,會呼叫didUpdateValueForCharacteristic
    [peripheral readValueForCharacteristic:self.characteristic];

    // 訂閱通知
    [peripheral setNotifyValue:YES forCharacteristic:self.characteristic];
}複製程式碼

setNotifyValue:(BOOL)enabled forCharacteristic:(CBCharacteristic *)characteristic方法是對這個特徵進行訂閱,訂閱成功之後,就可以監控外設中這個特徵值得變化了。

11、當訂閱的狀態發生改變的時候,下面的方法就派上用場了。

/** 訂閱狀態的改變 */
-(void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
    if (error) {
        NSLog(@"訂閱失敗");
        NSLog(@"%@",error);
    }
    if (characteristic.isNotifying) {
        NSLog(@"訂閱成功");
    } else {
        NSLog(@"取消訂閱");
    }
}複製程式碼

12、外設可以傳送資料給中心裝置,中心裝置也可以從外設讀取資料,當發生這些事情的時候,就會回撥這個方法。通過特種中的value屬性拿到原始資料,然後根據需求解析資料。

/** 接收到資料回撥 */
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
    // 拿到外設傳送過來的資料
    NSData *data = characteristic.value;
    self.textField.text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}複製程式碼

13、中心裝置可以向外設寫入資料,也可以向外設傳送請求或指令,當需要進行這些操作的時候該怎麼辦呢。

  • 首先把要寫入的資料轉化為NSData格式,然後根據上面拿到的寫入資料的特徵,運用方法writeValue:(NSData )data forCharacteristic:(CBCharacteristic )characteristic type:(CBCharacteristicWriteType)type來進行資料的寫入。

  • 當寫入資料的時候,系統也會回撥這個方法peripheral:(CBPeripheral )peripheral didWriteValueForCharacteristic:(nonnull CBCharacteristic )characteristic error:(nullable NSError *)error 。

/** 寫入資料 */
- (IBAction)didClickPost:(id)sender {
    // 用NSData型別來寫入
    NSData *data = [self.textField.text dataUsingEncoding:NSUTF8StringEncoding];
    // 根據上面的特徵self.characteristic來寫入資料
    [self.peripheral writeValue:data forCharacteristic:self.characteristic type:CBCharacteristicWriteWithResponse];
}


/** 寫入資料回撥 */
- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(nonnull CBCharacteristic *)characteristic error:(nullable NSError *)error {
    NSLog(@"寫入成功");
}複製程式碼

14、中心裝置如何主動從外設讀取資料呢。

  • 用正在連線的外設物件來呼叫readValueForCharacteristic方法,並且把將要讀取資料的特徵作為引數,這樣就可以主動拿一次資料了。
    去到第12步的回撥方法中,在特徵的value屬性中拿到這次的資料。
/** 讀取資料 */
- (IBAction)didClickGet:(id)sender {
    [self.peripheral readValueForCharacteristic:self.characteristic];
}複製程式碼

後記

中心裝置的開發是需要配合外設來進行的,一般會有硬體工程師或嵌入式工程師給出通訊協議,根據協議來對專案的各種需求進行操作。

本文所述的示例程式碼在這裡:Demo
推薦簡單又好用的分類集合:WHKit

github地址:github.com/remember17

相關文章