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ī)則是下圖這樣的:
底層的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è)所能提供的服務(wù)信息笆环,這些信息是可以用來標(biāo)記數(shù)據(jù)交互攒至、或者作為通道交互數(shù)據(jù),其實它的數(shù)據(jù)模型和網(wǎng)絡(luò)是非常類似的躁劣,可以理解為建立了多個通道的即時通訊迫吐。
藍(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è)的時候址晕,要用到的類是:CBPeripheralManager
、CBCharacteristic
作為外設(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)容了尊惰,我們在寫封裝庫的時候可以參考著來寫,那樣整體庫的邏輯就非常的清晰了泥兰。下面是腦圖: