iOS藍(lán)牙編程

本文出自: http://mokai.me/bluetooth-guide.html

藍(lán)牙技術(shù),很早以前就被有了,如今已更新4.0版本。很多熱門技術(shù)都是基于它工作的,如Android平臺的NFC,iOS的iBeancon,Apple Watch的WatchConnectivity框架等,現(xiàn)在的智能家居基本也是基于藍(lán)牙4.0與APP進(jìn)行通信,可見藍(lán)牙在實(shí)踐工作中的重要性。在iOS中,藍(lán)牙是基于4.0標(biāo)準(zhǔn)的,設(shè)備間低功耗通信。

核心成員

在開始前我們回憶下傳統(tǒng)的Socket編程,里面有Server服務(wù)端與Client端的區(qū)別。那么在藍(lán)牙編程也是如此,其中Peripheral外設(shè)相當(dāng)于Socket編程中的Server服務(wù)端,Central中心相當(dāng)于Client客戶端(ps吐槽下,Central中心,作為服務(wù)端,不更適合嗎!)

[圖片上傳失敗...(image-2840cf-1514799480530)]

你可以理解外設(shè)是一個(gè)廣播數(shù)據(jù)的設(shè)備,它開始告訴外面的世界說它這兒有一些數(shù)據(jù),并且能提供一些服務(wù)。另一邊中心開始掃描周邊有沒有合適的設(shè)備,如果發(fā)現(xiàn)后,會和外設(shè)做連接請求,一旦連接確定后,兩個(gè)設(shè)備就可以傳輸數(shù)據(jù)了。

在iOS6之后,iOS 設(shè)備可以是外設(shè),也可以是中心,就像Socket編程中一樣,你可以是服務(wù)端也可以是客戶端。

服務(wù)(service)和特征(characteristic)

每個(gè)藍(lán)牙4.0的設(shè)備都是通過服務(wù)和特征來展示自己的,一個(gè)設(shè)備必然包含一個(gè)或多個(gè)服務(wù),每個(gè)服務(wù)下面又包含若干個(gè)特征。特征是與外界交互的最小單位。比如說,智能音響設(shè)備,用服務(wù)A標(biāo)識播放模塊,特征A1來表示播放上一首,特征A2來表示播放下一首;服務(wù)B標(biāo)識設(shè)置模塊,特征B1設(shè)置彩燈顏色。這樣做的目的主要為了模塊化。

外設(shè),服務(wù),特征都有一個(gè)UUID來標(biāo)識

上面說了設(shè)備可以是外設(shè),也可以是中心,也就是會有二種模式

  • 本地中心 -> 遠(yuǎn)程外設(shè)
  • 本地外設(shè) -> 遠(yuǎn)程中心

不過在智能家居開發(fā)中,大部分硬件藍(lán)牙都是擔(dān)任外設(shè)的角色,也就是說我們應(yīng)用只要扮演中心即可了。

開始

本篇只講述第一種模式的本地中心,遠(yuǎn)程外設(shè)端可借助 藍(lán)牙調(diào)試神器LightBlue For Mac。需要了解第二種模式可以移步創(chuàng)建外設(shè)

更新:LightBlue For Mac只可以做為Central,不可以做為Peripheral,如需模擬請下載iOS版本

藍(lán)牙交互的流程大致為

建立中心角色 —> 掃描外設(shè)(discover)—> 發(fā)現(xiàn)外設(shè)后連接外設(shè)(connect) —> 掃描外設(shè)中的服務(wù)和特征(discover) —> 與外設(shè)做數(shù)據(jù)交互(explore and interact) —> 斷開連接(disconnect)。

下面我們一一講到

建立中心角色

在本地中心角色中,使用CBCentralManager類管理,我們創(chuàng)建一個(gè)CBCentralManager類

let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
let centralMgr = CBCentralManager(delegate: self, queue: queue)

上面的delegate為CBCentralManagerDelegate,后續(xù)藍(lán)牙相關(guān)的回調(diào)都會在此。Queue代表藍(lán)牙在哪個(gè)隊(duì)列里面操作,如果傳入nil默認(rèn)為主隊(duì)列,值得注意的是后續(xù)的回調(diào)也是在傳入的隊(duì)列中調(diào)用的,所以如果傳入的是非主線程的隊(duì)列,在delegate中需要操作UI時(shí)需要手動(dòng)切換到主線程

CBCentralManager對象創(chuàng)建后會回調(diào)到centralManagerDidUpdateState方法來檢測藍(lán)牙可用狀態(tài),這時(shí)我們可以提醒用戶設(shè)備是否支持藍(lán)牙,是否打開了藍(lán)牙

掃描外設(shè)

let serviceUUIDS: Array<CBUUID> = [CBUUID(string: "FFDD")]
self.centralMgr.scanForPeripheralsWithServices(serviceUUIDS, options: [CBCentralManagerScanOptionAllowDuplicatesKey : true])

//停止掃描
self.centralMgr.stopScan()

如果serviceUUIDS為nil則會掃描周圍所有的設(shè)外設(shè),否則只會掃描UUID匹配的外設(shè)。CBCentralManagerScanOptionAllowDuplicatesKey默認(rèn)為false,表示掃描中發(fā)現(xiàn)過設(shè)備則跳過不回調(diào),我們這里傳入true,因?yàn)橄旅孀鐾庠O(shè)掉線的處理時(shí)需要用到

傳入的serviceUUIDS數(shù)組元素為CBUUID類型,千萬不要傳入String,后面的操作也是如此,不然會碰到很多奇葩問題

發(fā)現(xiàn)外設(shè)后會回調(diào)到centralManager(central:didDiscoverPeripheral:advertisementData:RSSI:) ,perpheral則代表著外設(shè),我們需要保存起來,后續(xù)的對外設(shè)的操作都是基于perpheral對象的

func centralManager(central: CBCentralManager!, didDiscoverPeripheral peripheral: CBPeripheral!, advertisementData: [NSObject : AnyObject]!, RSSI: NSNumber!) {
   for i in 0..<discoveredPeripheralers.count {
       var peripheraler = discoveredPeripheralers[I]
       if(!peripheral.identifier.isEqual(peripheraler.peripheral.identifier)){ //未發(fā)現(xiàn)過才保存
          discoveredPeripheralers.append(peripheraler)
       }
   }
}

連接外設(shè)

self.centralMgr.connectPeripheral(peripheral, options: nil)

傳入上面保存的外設(shè)對象,如果連接失敗后會回調(diào)到 centralManager(central:didFailToConnectPeripheral:error:),連接成功后會回調(diào)到 centralManager(central:didConnectPeripheral:),這個(gè)時(shí)候我們只是連接上外設(shè)而已,還需要發(fā)現(xiàn)外設(shè)中的服務(wù)與特征

發(fā)現(xiàn)服務(wù)與特征

外設(shè)連接成功后我們把peripheral保存好,并設(shè)置好peripheral的delegate(CBPeripheralDelegate),然后調(diào)用discoverServices來發(fā)現(xiàn)服務(wù),同掃描外設(shè)時(shí)一樣,discoverServices也可以傳入一個(gè)serviceUUIDs參數(shù)來只獲取需要的服務(wù)

注意,注意,注意,重要的話說三遍。以下的回調(diào)都是CBPeripheralDelegate的了,不再是CBCentralManagerDelegate的回調(diào)

func centralManager(central: CBCentralManager!, didConnectPeripheral peripheral: CBPeripheral!) {
    self.peripheral = peripheral
    self.peripheral.delegate = self
    let serviceUUIDS: Array<CBUUID> = [CBUUID(string: "FF12")]
    self.peripheral.discoverServices(serviceUUIDS)
}

發(fā)現(xiàn)服務(wù)后回調(diào)到peripheral(peripheral:didDiscoverServices:),這時(shí)我們就可以訪問所有發(fā)現(xiàn)的服務(wù)一一去發(fā)現(xiàn)服務(wù)下的特征

func peripheral(peripheral: CBPeripheral!, didDiscoverServices error: NSError!) {
    if(error != nil) {
        log(error)
        return
    }
    for item in peripheral.services {
        let service = item as! CBService
        let characteristicUUIDs: Array<CBUUID> = [CBUUID(string: "FF02"), CBUUID(string: "FF04")]
        peripheral.discoverCharacteristics(characteristicUUIDs, forService: service)  //發(fā)現(xiàn)特征
    }
}

同樣特征也可以傳入characteristicUUIDs數(shù)組來過濾,發(fā)現(xiàn)特征后回調(diào)

func peripheral(peripheral: CBPeripheral!, didDiscoverCharacteristicsForService service: CBService!, error: NSError!) {
    if(error != nil){
        log(error)
        return
    }
    for item in service.characteristics {
        let characteristic = item as! CBCharacteristic
        if(characteristic.properties == .Notify) { //如果特征為訂閱屬性則開啟訂閱
            peripheral.setNotifyValue(true, forCharacteristic: characteristic)
        }
    }
}

每進(jìn)入一次回調(diào)代表發(fā)現(xiàn)一個(gè)服務(wù)中的特征而不是外設(shè)所有的特征,外設(shè)、服務(wù)、特征從左至右都是上下級一對多的關(guān)系。
每個(gè)特征都有個(gè)屬性,代表著它是可寫、可讀等,一個(gè)特征可同時(shí)擁有讀寫權(quán)限,如上面的訂閱其實(shí)是一種訂閱者模式的讀取數(shù)據(jù)

發(fā)送數(shù)據(jù)

拿到可寫的特征后,通過writeValue發(fā)送數(shù)據(jù)包

let data = "hello".dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: true)
//自動(dòng)判斷寫特征的類型
var type: CBCharacteristicWriteType = .WithoutResponse
if(writeCharacteristic.properties == CBCharacteristicProperties.Write) {
    type = .WithResponse
}
self.peripheral!.writeValue(data, forCharacteristic: writeCharacteristic, type: type)

把要發(fā)送的文本轉(zhuǎn)換為二進(jìn)制,發(fā)送到相應(yīng)的特征即可。值得注意的是第三個(gè)參數(shù)type寫類型需要與特征的屬性一致,其中WithoutResponse與WithResponse區(qū)別在于前者發(fā)送數(shù)據(jù)后是沒有回調(diào)的,后者會回調(diào)到 peripheral(peripheral:didWriteValueForCharacteristic:error:) 來檢測是否發(fā)送成功,如果發(fā)送數(shù)據(jù)傳入的類型與特征不同時(shí)總是會失敗

由于藍(lán)牙的緩沖大小只有20bytes,那么如果我們發(fā)送的數(shù)據(jù)包大小不能大于20bytes,所以得分多次發(fā)送

func writeValue(data: NSData, withCharacteristic characteristic: CBCharacteristic) -> Bool {
    if(self.peripheral == nil) {
        return false
    }
    var didSend = false
    var sendDataIndex = 0
    let  NOTIFY_MTU = 20
    while (data.length - sendDataIndex != 0) {
        //剩下的數(shù)據(jù)大小
        var amountToSend = data.length - sendDataIndex
        // 不能大于20bytes
        if (amountToSend > NOTIFY_MTU) {
            amountToSend = NOTIFY_MTU
        }
        let chunk = NSData(bytes: data.bytes + sendDataIndex, length: amountToSend)
        var type: CBCharacteristicWriteType = .WithoutResponse
        if(characteristic.properties == CBCharacteristicProperties.Write) {
            type = .WithResponse
        }
        self.peripheral!.writeValue(chunk, forCharacteristic: characteristic, type: type)
        sendDataIndex += amountToSend
    }
    return true
}

讀取數(shù)據(jù)

分為二種,直接讀、訂閱,顧名思義,直接讀就是手動(dòng)調(diào)用API讀取,訂閱則只要開啟后,外設(shè)有消息都可以收到

直接讀

self.peripheral!.readValueForCharacteristic(characteristic)

訂閱

self.peripheral!.setNotifyValue(true, forCharacteristic: characteristic)

兩種回調(diào)都會回調(diào)到 peripheral(peripheral:didUpdateValueForCharacteristic:error:),上面也提到因?yàn)樗{(lán)牙的緩沖大小,需要發(fā)送多次,那么在讀取時(shí)也需要接收多次,才能保證數(shù)據(jù)的一體性,所以通常都會在數(shù)據(jù)包的開始用 EOM 來標(biāo)識一段數(shù)據(jù)的開始,數(shù)據(jù)結(jié)束后再次用 EOM 來標(biāo)識,所以我們接收數(shù)據(jù)時(shí)會這樣

let updatingEOMFlag = "EOM"
func peripheral(peripheral: CBPeripheral!, didUpdateValueForCharacteristic characteristic: CBCharacteristic!, error: NSError!) {
    if(error != nil) {
        log(error)
        return
    }
    if(characteristic.value != nil) {
        var data = characteristic.value!
        var string = NSString(data: data, encoding: NSUTF8StringEncoding)
        log(string)
        
        //接收多段數(shù)據(jù)
        if(self.updatingEOMFlag != nil) {
            if(self.updatingEOMFlag == string) {
                var EOMEndFlag = false
                for i in 0..<self.updatingDatas.count { //數(shù)據(jù)結(jié)束
                    var updatingData = self.updatingDatas[I]
                    if(updatingData.characteristic.UUID.isEqual(characteristic.UUID)) {
                        data = updatingData.data
                        string = NSString(data: data, encoding: NSUTF8StringEncoding)
                        self.updatingDatas.removeAtIndex(i) //刪除緩存數(shù)據(jù)
                        EOMEndFlag = true
                        break
                    }
                }
                if(!EOMEndFlag) {//數(shù)據(jù)開始
                    let updatingData = UpdatingDataer(characteristic: characteristic, data: NSMutableData())
                    self.updatingDatas!.append(updatingData)
                    return
                }
            } else {
                if var updatingData = (self.updatingDatas?.filter{ $0.characteristic.UUID.isEqual(characteristic.UUID) }) where updatingData.count == 1 && updatingData[0].data != nil { //數(shù)據(jù)中間
                    updatingData[0].data.appendData(data)
                    return
                }
            }
        }
        //在此最終得到完整數(shù)據(jù)
        let stringData = StringData(string: string as? String, data: data)

        //觸發(fā)delegate與通知回調(diào)
        ...
    }
}

斷開連接

self.centralMgr.cancelPeripheralConnection(self.peripheral!)

至此,整個(gè)流程就完了

高級需求~

外設(shè)掉線檢測

所謂掉線就是外設(shè)發(fā)現(xiàn)了后,過了一段時(shí)間失去信號了。喵了下系統(tǒng)框架,沒有找到相關(guān)外設(shè)掉線的檢測,唯一有點(diǎn)像的就是發(fā)現(xiàn)外設(shè)里面的RSSI,代表設(shè)備信號強(qiáng)度,值越小信息越好。

總結(jié)

  • 在藍(lán)牙交互的二種角色中,通常APP端扮演中央Central的角色,設(shè)備扮演外設(shè)Peripheral的角色
  • 創(chuàng)建CBCentralManager對象時(shí)傳入的Queue決定了后續(xù)CBCentralManagerDelegate、CBPeripheralDelegate等回調(diào)的所在線程
  • 一個(gè)外設(shè)設(shè)備可包含一個(gè)或多個(gè)服務(wù),一個(gè)服務(wù)可包含一個(gè)或多個(gè)特征,讀寫操作最終是針對特征。
  • 藍(lán)牙的緩沖大小只有20bytes,在發(fā)送數(shù)據(jù)時(shí)最多只能發(fā)送20bytes,所以得分多次發(fā)送,數(shù)據(jù)的一體性可以用 EOM 標(biāo)識符表標(biāo)識

更新: 提供了一個(gè)讀寫的Central端Demo,Peripheral端請用上述iOS版LightBlue模擬

參考

Core Bluetooth Programming Guide
譯-iOS藍(lán)牙編程指南

小小廣告

本人目前是一名自由職業(yè)者,接受移動(dòng)兩端的項(xiàng)目開發(fā),如果你有需求或者有資源請速與我聯(lián)系吧,QQ865425695

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容