iOS藍(lán)牙編程

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

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

核心成員

在開(kāi)始前我們回憶下傳統(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è)備,它開(kāi)始告訴外面的世界說(shuō)它這兒有一些數(shù)據(jù)耻瑟,并且能提供一些服務(wù)旨指。另一邊中心開(kāi)始掃描周邊有沒(méi)有合適的設(shè)備,如果發(fā)現(xiàn)后喳整,會(huì)和外設(shè)做連接請(qǐng)求谆构,一旦連接確定后,兩個(gè)設(shè)備就可以傳輸數(shù)據(jù)了框都。

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

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

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

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

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

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

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

開(kāi)始

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

更新:LightBlue For Mac只可以做為Central,不可以做為Peripheral穆役,如需模擬請(qǐng)下載iOS版本

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

建立中心角色 —> 掃描外設(shè)(discover)—> 發(fā)現(xiàn)外設(shè)后連接外設(shè)(connect) —> 掃描外設(shè)中的服務(wù)和特征(discover) —> 與外設(shè)做數(shù)據(jù)交互(explore and interact) —> 斷開(kāi)連接(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)都會(huì)在此。Queue代表藍(lán)牙在哪個(gè)隊(duì)列里面操作淹接,如果傳入nil默認(rèn)為主隊(duì)列十性,值得注意的是后續(xù)的回調(diào)也是在傳入的隊(duì)列中調(diào)用的,所以如果傳入的是非主線程的隊(duì)列蹈集,在delegate中需要操作UI時(shí)需要手動(dòng)切換到主線程

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

掃描外設(shè)

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

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

如果serviceUUIDS為nil則會(huì)掃描周?chē)械脑O(shè)外設(shè)拢肆,否則只會(huì)掃描UUID匹配的外設(shè)。CBCentralManagerScanOptionAllowDuplicatesKey默認(rèn)為false靖诗,表示掃描中發(fā)現(xiàn)過(guò)設(shè)備則跳過(guò)不回調(diào)郭怪,我們這里傳入true,因?yàn)橄旅孀鐾庠O(shè)掉線的處理時(shí)需要用到

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

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

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)過(guò)才保存
          discoveredPeripheralers.append(peripheraler)
       }
   }
}

連接外設(shè)

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

傳入上面保存的外設(shè)對(duì)象败晴,如果連接失敗后會(huì)回調(diào)到 centralManager(central:didFailToConnectPeripheral:error:)浓冒,連接成功后會(huì)回調(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來(lái)發(fā)現(xiàn)服務(wù)慢味,同掃描外設(shè)時(shí)一樣场梆,discoverServices也可以傳入一個(gè)serviceUUIDs參數(shù)來(lái)只獲取需要的服務(wù)

注意墅冷,注意,注意或油,重要的話說(shuō)三遍寞忿。以下的回調(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í)我們就可以訪問(wèn)所有發(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ù)組來(lái)過(guò)濾罐脊,發(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) { //如果特征為訂閱屬性則開(kāi)啟訂閱
            peripheral.setNotifyValue(true, forCharacteristic: characteristic)
        }
    }
}

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

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

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

let data = "hello".dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: true)
//自動(dòng)判斷寫(xiě)特征的類型
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寫(xiě)類型需要與特征的屬性一致裳食,其中WithoutResponse與WithResponse區(qū)別在于前者發(fā)送數(shù)據(jù)后是沒(méi)有回調(diào)的,后者會(huì)回調(diào)到 peripheral(peripheral:didWriteValueForCharacteristic:error:) 來(lái)檢測(cè)是否發(fā)送成功芙沥,如果發(fā)送數(shù)據(jù)傳入的類型與特征不同時(shí)總是會(huì)失敗

由于藍(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讀取着憨,訂閱則只要開(kāi)啟后,外設(shè)有消息都可以收到

直接讀

self.peripheral!.readValueForCharacteristic(characteristic)

訂閱

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

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

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ù)開(kāi)始
                    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)
        ...
    }
}

斷開(kāi)連接

self.centralMgr.cancelPeripheralConnection(self.peripheral!)

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

高級(jí)需求~

外設(shè)掉線檢測(cè)

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

總結(jié)

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

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

參考

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

小小廣告

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市硕勿,隨后出現(xiàn)的幾起案子哨毁,更是在濱河造成了極大的恐慌,老刑警劉巖源武,帶你破解...
    沈念sama閱讀 211,123評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件扼褪,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡软能,警方通過(guò)查閱死者的電腦和手機(jī)迎捺,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén)举畸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)查排,“玉大人,你說(shuō)我怎么就攤上這事抄沮“虾耍” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,723評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵叛买,是天一觀的道長(zhǎng)砂代。 經(jīng)常有香客問(wèn)我,道長(zhǎng)率挣,這世上最難降的妖魔是什么刻伊? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,357評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上捶箱,老公的妹妹穿的比我還像新娘智什。我一直安慰自己,他們只是感情好丁屎,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布荠锭。 她就那樣靜靜地躺著,像睡著了一般晨川。 火紅的嫁衣襯著肌膚如雪证九。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,760評(píng)論 1 289
  • 那天共虑,我揣著相機(jī)與錄音愧怜,去河邊找鬼。 笑死妈拌,一個(gè)胖子當(dāng)著我的面吹牛叫搁,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播供炎,決...
    沈念sama閱讀 38,904評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼渴逻,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了音诫?” 一聲冷哼從身側(cè)響起惨奕,我...
    開(kāi)封第一講書(shū)人閱讀 37,672評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎竭钝,沒(méi)想到半個(gè)月后梨撞,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,118評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡香罐,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評(píng)論 2 325
  • 正文 我和宋清朗相戀三年卧波,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片庇茫。...
    茶點(diǎn)故事閱讀 38,599評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡港粱,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出旦签,到底是詐尸還是另有隱情查坪,我是刑警寧澤,帶...
    沈念sama閱讀 34,264評(píng)論 4 328
  • 正文 年R本政府宣布宁炫,位于F島的核電站偿曙,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏羔巢。R本人自食惡果不足惜望忆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評(píng)論 3 312
  • 文/蒙蒙 一罩阵、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧启摄,春花似錦永脓、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,731評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至威创,卻和暖如春落午,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背肚豺。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,956評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工溃斋, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人吸申。 一個(gè)月前我還...
    沈念sama閱讀 46,286評(píng)論 2 360
  • 正文 我出身青樓梗劫,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親截碴。 傳聞我的和親對(duì)象是個(gè)殘疾皇子梳侨,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評(píng)論 2 348

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