iOS 藍(lán)牙開發(fā) CoreBluetooth 應(yīng)用

iOS CoreBluetooth 應(yīng)用學(xué)習(xí)

目前iOS的藍(lán)牙應(yīng)用主要應(yīng)用在穿戴锭碳、音箱、耳機(jī)短距離傳輸?shù)阮I(lǐng)域勿璃,應(yīng)用場景非常廣闊擒抛。而目前對于開發(fā)者來說推汽,應(yīng)用較多的只有BLE4.0,因為蘋果的2.0藍(lán)牙是需要MFI(make for iphone)驗證的歧沪,而廠商的利潤本來就非常低了歹撒,還得搞個MFI認(rèn)證的話就不賺錢了。所以本篇文章只是學(xué)習(xí)藍(lán)牙4.0的開發(fā)應(yīng)用诊胞。

本篇主要想從兩方面來分析和學(xué)習(xí)CoreBluetooth框架暖夭,讓iPhone 分別作為藍(lán)牙外設(shè) 和作為藍(lán)牙中央。首先我們需要先導(dǎo)入CoreBluetooth.framework撵孤。然后我們先來看看iOS藍(lán)牙使用的流程迈着,調(diào)用規(guī)則是下圖這樣的:

CoreBluetooth Image

底層的GATT和ATT、L2CAP我們是不直接調(diào)用的邪码,感興趣的同學(xué)可以進(jìn)一步學(xué)習(xí)一下GATT和L2CAP裕菠,這兩個在嵌入式用得比較多。我們主要是使用CoreBluetooth提供的方法來進(jìn)行與設(shè)備的交互霞扬。

手機(jī)作為藍(lán)牙中央

通常這種情況會多一些糕韧,因為手機(jī)具備了強(qiáng)大的運算能力和出色的表達(dá)能力枫振,應(yīng)用領(lǐng)域可以參考運動手環(huán)喻圃、心率測試儀、血壓計等等粪滤。也就是下面這種情況:

工作圖

但是這種情況下斧拍,如果以網(wǎng)絡(luò)的模型來看,此刻的iOS設(shè)備是作為客戶端的杖小,而藍(lán)牙設(shè)備就作為服務(wù)器端肆汹。這是因為藍(lán)牙外設(shè)時需要建立一個藍(lán)牙通信,設(shè)定好服務(wù)和特征值予权、描述數(shù)據(jù)等信息昂勉,然后廣播到空氣中,iPhone通過服務(wù)搜索扫腺,發(fā)現(xiàn)設(shè)備岗照,才進(jìn)行連接的。

設(shè)備廣播

我們先來看看外設(shè)所能提供的服務(wù)信息笆环,這些信息是可以用來標(biāo)記數(shù)據(jù)交互攒至、或者作為通道交互數(shù)據(jù),其實它的數(shù)據(jù)模型和網(wǎng)絡(luò)是非常類似的躁劣,可以理解為建立了多個通道的即時通訊迫吐。

外設(shè)架構(gòu)

藍(lán)牙外設(shè)段可以理解為能提供以下字段作為傳輸通道

1、服務(wù) Service

1.1 特征值 Characteristic

    1.1.1 描述符 

1.2 特征值  Characteristic

    1.2.1 描述符

2账忘、服務(wù) Service

2.1 特征值 Characteristic

    2.1.1 描述符

2.2 特征值 Characteristic

    2.2.1 描述符

當(dāng)iPhone作為中心的時候志膀,主要用到的類庫有兩個:CBCentralManager(外圍設(shè)備管理器)熙宇、CBPeripheral(遠(yuǎn)端外圍設(shè)備)

CBCentralManager 介紹

iPhone是通過這個類來進(jìn)行藍(lán)牙設(shè)備的發(fā)現(xiàn)、管理梧却、連接奇颠、異常處理。
初始化:

centralMgr = CBCentralManager.init(delegate: self, queue: DispatchQueue.main, options: [CBConnectPeripheralOptionNotifyOnConnectionKey:true,CBConnectPeripheralOptionNotifyOnDisconnectionKey:true,CBConnectPeripheralOptionNotifyOnNotificationKey:true])
/* 參數(shù)說明
  CBCentralManagerOptionShowPowerAlertKey
  填一個Bool值放航,用來指定如果藍(lán)牙設(shè)備斷電的時候烈拒,系統(tǒng)是否會發(fā)出警告
  CBCentralManagerOptionRestoreIdentifierKey
  填一個String作為唯一標(biāo)記
*/

掃描外設(shè):

centralMgr.scanForPeripherals(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey:true])
/** 參數(shù)說明
  CBCentralManagerScanOptionAllowDuplicatesKey
  一個布爾值,指定是否應(yīng)在沒有重復(fù)過濾的情況下運行掃描广鳍。
  
  CBCentralManagerScanOptionSolicitedServiceUUIDsKey
  指定服務(wù)UUID的數(shù)組掃描特定設(shè)備
*/

停止掃描:

centralMgr.stopScan() 

連接設(shè)備:

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let item = itemArray[indexPath.row]
        cbPeripheral = item
        centralMgr.connect(item, options: [CBConnectPeripheralOptionNotifyOnConnectionKey:true,CBConnectPeripheralOptionNotifyOnDisconnectionKey:true])
        tableView.deselectRow(at: indexPath, animated: true)
    }
/** 特別說明一下連接的可選參數(shù)
  CBConnectPeripheralOptionNotifyOnConnectionKey
  填一個Bool值荆几,指定后臺連接外圍設(shè)備時,是否告知系統(tǒng)赊时,并彈窗提示
  CBConnectPeripheralOptionNotifyOnDisconnectionKey
  填一個Bool值吨铸,指定后臺斷開外圍設(shè)備時,是否告知系統(tǒng)祖秒,并彈窗提示
  CBConnectPeripheralOptionNotifyOnNotificationKey
  填一個Bool值诞吱,指定系統(tǒng)是否對外圍發(fā)過來的每一個通知都彈窗提示
  CBConnectPeripheralOptionEnabTransportBridgeingKey
  如果已經(jīng)通過低功耗藍(lán)牙連接,則可以橋接經(jīng)典藍(lán)牙的配置文件(GATT)
  CBConnectPeripheralOptionRequiresANCS
  填一個Bool值竭缝,設(shè)定連接設(shè)備時是否需要連接(ANCS)服務(wù)房维,接收推送服務(wù)
  CBConnectPeripheralOptionStarDelayKey
  填一個Bool,設(shè)置系統(tǒng)連接前是否要延遲
*/

CBCentralManager是通過接收代理的方式來獲得設(shè)備的連接狀態(tài)以及信息變動抬纸,所以可以參考以下的代理消息設(shè)置:

/// 連接上了
    /// - Parameters:
    ///   - central: centerMgr
    ///   - peripheral: 藍(lán)牙外設(shè)
    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
      //連接上后咙俩,選擇跳到另一個界面去進(jìn)行更多的通訊交互
      //    performSegue(withIdentifier: "peripheralSegue", sender: peripheral)
    }
    
    /// 斷開連接
    /// - Parameters:
    ///   - central: centerMgr
    ///   - peripheral: 藍(lán)牙外設(shè)
    ///   - error: 錯誤原因
    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        
    }
?
    ///ANCS授權(quán)狀態(tài)回調(diào)
    func centralManager(_ central: CBCentralManager, didUpdateANCSAuthorizationFor peripheral: CBPeripheral) {
        
    }
   
    
    /// 發(fā)現(xiàn)藍(lán)牙設(shè)備回調(diào)
    /// - Parameters:
    ///   - central: centerMgr
    ///   - peripheral: 藍(lán)牙外設(shè)
    ///   - advertisementData: 描述
    ///   - RSSI: 信號強(qiáng)度
    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        if !itemArray.contains(peripheral) && (peripheral.name != nil){
            itemArray.append(peripheral)
            listTable.reloadData()
        }
    }
    
    //斷開回連
    func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
        
        centralMgr.connect(cbPeripheral, options: [CBConnectPeripheralOptionNotifyOnConnectionKey:true,CBConnectPeripheralOptionNotifyOnDisconnectionKey:true])
        
    }

CBPeripheral 藍(lán)牙設(shè)備的交互

其實這個CBPeripheral就是俄羅斯套娃的結(jié)構(gòu),一層環(huán)節(jié)一層湿故,先去獲取了服務(wù)阿趁,然后根據(jù)服務(wù)獲取服務(wù)的特征值、描述符坛猪;

發(fā)現(xiàn)服務(wù):

/*
1脖阵、discoverServices 發(fā)現(xiàn)外圍設(shè)備的指定服務(wù)
2、discoverIncludedServices([CBUUID]?,for:CBService) 發(fā)現(xiàn)先前發(fā)現(xiàn)的服務(wù)中所包含的服務(wù)
3墅茉、services:[CBService]? 外圍設(shè)備已發(fā)現(xiàn)的服務(wù)列表
*/
mainPeripheral.discoverServices(nil)

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

 //服務(wù)發(fā)現(xiàn)回調(diào)
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        for service in peripheral.services! {
            //根據(jù)設(shè)備回調(diào)的服務(wù)命黔,再去請求特征值
            peripheral.discoverCharacteristics(nil, for: service)
            print(service.uuid.uuidString,"services")
        }
    }

設(shè)備特征值回調(diào):

 //設(shè)備特征回調(diào)
    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        for character in service.characteristics! {
            print(character.uuid.uuidString,"characteristics")
            peripheral.setNotifyValue(true, for: character)//訂閱所有特征值的通知
            peripheral.readValue(for: character)//讀取所有特征值
            peripheral.discoverDescriptors(for: character)//讀取所有特征值的描述符
        }
        peripheralTable.reloadData()
    }

描述符回調(diào):

 //描述符回調(diào)
    func peripheral(_ peripheral: CBPeripheral, didDiscoverDescriptorsFor characteristic: CBCharacteristic, error: Error?) {
        for descriptor in characteristic.descriptors! {
            print(descriptor.uuid.uuidString,"descriptors")
        }
        peripheralTable.reloadData()
    }

建立通訊之后,需要對訂閱的值或者通道進(jìn)行監(jiān)聽躁锁,監(jiān)聽如下:

 //更新特征值通知
    func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
        print("\(characteristic.uuid.uuidString): \(characteristic.uuid.data)")
    }
    //設(shè)備值更新通知
    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
        if (characteristic.value != nil) {
            let k = String.init(data: characteristic.value!, encoding: String.Encoding.utf8)
                  print("\(characteristic.uuid.uuidString): \(k)")//收到來自通知的數(shù)據(jù)
        }
         
    }

還有其他一些狀態(tài)變更纷铣,可參考下面:

//設(shè)備寫入成功回調(diào)
    func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
        print("characteristic is writed!")
    }
    
    //服務(wù)名更改
    func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService]) {
        
    }
    //外設(shè)名字變更
    func peripheralDidUpdateName(_ peripheral: CBPeripheral) {
        
    }

讀取特征值和描述符:

readValue(for:CBCharacteristic) 讀取指定特征值

readValue(for:CBDescriptor) 讀取指定特征描述符的值

寫入特征值和描述符:

writeValue(Data,for:CBCharacteristic战转,type:CBCharacteristicWriteType)寫入特征值

writeValue(Data搜立,for:CBDescriptor)寫入特征描述符的值

maximumWriteValueLength(for type: CBCharacteristicWriteType) -> Int可以通過單個寫入類型發(fā)送到特征的最大數(shù)據(jù)量(以字節(jié)為單位)。CBCharacteristicWriteType 表示可能寫入特征值的類型的值,withResponse 寫入值成功時有返回槐秧;withoutResopnse 寫入值成功時不設(shè)返回值啄踊。

手機(jī)作為藍(lán)牙外設(shè)

在實際的應(yīng)用場景中忧设,這種情況會比較少需要用上,可能會用于手機(jī)APP之間的小數(shù)據(jù)交互等等颠通。當(dāng)手機(jī)作為外設(shè)的時候址晕,要用到的類是:CBPeripheralManagerCBCharacteristic

手機(jī)作為外設(shè)

作為外設(shè)意味著顿锰,需要為Central提供Service谨垃、Characteristic、Descriptor硼控,同樣的刘陶,我們也需要造一個俄羅斯套娃出來,一層一層套起來牢撼。

初始化:

phermgr = CBPeripheralManager.init(delegate: self, queue: nil)

當(dāng)收到了藍(lán)牙狀態(tài)監(jiān)測到成功打開的時候匙隔,需要為它添加服務(wù),開始套娃

let serviceUUID1 = "EE00"
let notifyCharacteristicUUID = "EE01"
let readCharacteristicUUID =  "EE02"
let writeCharacteristicUUID = "EE03"
let LocalNameKey = "Gt_0"
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
        switch peripheral.state {
        case CBManagerState.poweredOn:  //藍(lán)牙已打開正常
            NSLog("啟動成功熏版,開始搜索")
             //不限制
            let cbu = CBUUID.init(string:CBUUIDCharacteristicUserDescriptionString)
            
            //服務(wù)
            let service1 = CBMutableService.init(type: CBUUID.init(string: serviceUUID1), primary: true)
            
            let noti = CBMutableCharacteristic.init(type: CBUUID.init(string: notifyCharacteristicUUID), properties: .notify, value: nil, permissions: .readable)
            //特征值纷责,只讀
            let chart_0 = CBMutableCharacteristic.init(type: CBUUID.init(string: readCharacteristicUUID), properties: .read, value: nil, permissions: .readable)
            //特征值,只寫
            let chart_1 = CBMutableCharacteristic.init(type: CBUUID.init(string: writeCharacteristicUUID), properties: .write, value: nil, permissions: .writeable)
            //描述符
            let des_0 = CBMutableDescriptor.init(type: cbu, value: "name")
            let des_1 = CBMutableDescriptor.init(type: cbu, value: "name")
            
            chart_0.descriptors = [des_0]
            chart_1.descriptors = [des_1]
            service1.characteristics = [chart_0,chart_1,noti]
            //增加1個服務(wù)
            phermgr.add(service1)
            tips.isHidden = false
            
        case CBManagerState.unauthorized: //無BLE權(quán)限
            NSLog("無BLE權(quán)限")
        case CBManagerState.poweredOff: //藍(lán)牙未打開
            NSLog("藍(lán)牙未開啟")
        default:
            NSLog("狀態(tài)無變化")
        }
          
    }

當(dāng)服務(wù)加入以后撼短,就可以開始廣播數(shù)據(jù)了:

 func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) {
        print("peripheralManager didAdd")
        //加入服務(wù)以后再打開廣播
        peripheral.startAdvertising([CBAdvertisementDataServiceUUIDsKey:[CBUUID.init(string: serviceUUID1)],CBAdvertisementDataLocalNameKey:LocalNameKey])
    }

只有打開了服務(wù)再膳,Central才能搜索到設(shè)備

當(dāng)它作為Peripheral的時候,我們可以理解為服務(wù)器開啟了阔加,所以我們要為它添加應(yīng)答的內(nèi)容饵史,如下:

func peripheralManagerIsReady(toUpdateSubscribers peripheral: CBPeripheralManager) {
        print("peripheralManagerIsReady")
    }
    func peripheralManagerDidStartAdvertising(_ peripheral: CBPeripheralManager, error: Error?) {
        print("peripheralManagerDidStartAdvertising")
         
    }
    
    func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) {
        print("subscribe")
        //訂閱成功之后我們給它發(fā)送一個數(shù)據(jù)
//        self.sendData(characteristic: characteristic)
        didSendChara = characteristic
        if (timeAction != nil) {
            timeAction.invalidate()
            timeAction = nil
        }
        timeAction = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(sendData), userInfo: nil, repeats: true)
//        timeAction.fire()
    }
    //收到讀的請求
    func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest) {
        
        print("didReceiveRead")
        //最好做一下是否有讀權(quán)限
        if request.characteristic.properties == CBCharacteristicProperties.read {
            
            request.value = Data.init(bytes: [0x02,0x03], count: 2)
            peripheral.respond(to: request, withResult: .success)
//            self.sendData(characteristic: request.characteristic)
        }else{
            peripheral.respond(to: request, withResult: .writeNotPermitted)
        }
        
    }
    func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
        print("didReceiveWrite")
        let request = requests[0]
        for req in requests {
            print(req.characteristic.uuid.uuidString)
        }
       //先檢查是否有寫權(quán)限
        if request.characteristic.properties == .write {
            peripheral.respond(to: request, withResult: .success)
            
        }else{
            peripheral.respond(to: request, withResult: .writeNotPermitted)
        }
    }
    
    //取消訂閱
    func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) {
        
    }
?
//數(shù)據(jù)發(fā)送
    @objc func sendData(){
         let date = Date.init()
         let dateformate = DateFormatter.init()
         dateformate.dateFormat = "yyyy-MM-dd HH:mm:ss"
         let str = dateformate.string(from: date)
         let data = str.data(using:String.Encoding.utf8)!
         phermgr.updateValue(data, for: didSendChara as! CBMutableCharacteristic, onSubscribedCentrals: nil)
        print("sendData")
     }

再深入一步

為了再深入的看到整個CoreBluetooth的架構(gòu)满钟,我去整理了CoreBluetooth的結(jié)構(gòu)腦圖胜榔,方便后續(xù)繼續(xù)開展學(xué)習(xí)和方便記憶。其實總的來看湃番,所有的API都是在套接夭织,一層套一層,最終到達(dá)上層的時候吠撮,我們只能看到少部分的內(nèi)容了尊惰,我們在寫封裝庫的時候可以參考著來寫,那樣整體庫的邏輯就非常的清晰了泥兰。下面是腦圖:


CoreBlueTooth.png

Demo地址:https://github.com/110201041018/CoreBluetoothDemo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末弄屡,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子鞋诗,更是在濱河造成了極大的恐慌膀捷,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件削彬,死亡現(xiàn)場離奇詭異全庸,居然都是意外死亡秀仲,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進(jìn)店門壶笼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來神僵,“玉大人,你說我怎么就攤上這事覆劈”@瘢” “怎么了?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵责语,是天一觀的道長氓英。 經(jīng)常有香客問我,道長鹦筹,這世上最難降的妖魔是什么铝阐? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮铐拐,結(jié)果婚禮上徘键,老公的妹妹穿的比我還像新娘。我一直安慰自己遍蟋,他們只是感情好吹害,可當(dāng)我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著虚青,像睡著了一般它呀。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上棒厘,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天纵穿,我揣著相機(jī)與錄音,去河邊找鬼奢人。 笑死谓媒,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的何乎。 我是一名探鬼主播句惯,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼支救!你這毒婦竟也來了抢野?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤各墨,失蹤者是張志新(化名)和其女友劉穎指孤,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體欲主,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡邓厕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年逝嚎,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片详恼。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡补君,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出昧互,到底是詐尸還是另有隱情挽铁,我是刑警寧澤,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布敞掘,位于F島的核電站叽掘,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏玖雁。R本人自食惡果不足惜更扁,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望赫冬。 院中可真熱鬧浓镜,春花似錦、人聲如沸劲厌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽补鼻。三九已至哄啄,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間风范,已是汗流浹背咨跌。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留乌企,地道東北人虑润。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓成玫,卻偏偏與公主長得像加酵,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子哭当,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,435評論 2 359

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