藍(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