藍牙屬于近場通訊中的一種,
iOS中使用Core Bluetooth框架實現(xiàn)藍牙通信,Core Bluetooth支持藍牙低功耗的4.0模式,就是通常說稱之的BLE,在生活中BLE無處不在,例如智能家居,健身器材和智能玩具等,利用蘋果提供的Core Bluetooth框架可以實現(xiàn)和BLE設備進行通信。
藍牙中各角色的理解
在藍牙開發(fā)中我們把提供服務的一方稱之為周邊設備,接收服務的一方稱之為中央設備,典型的例子就是蘋果手表和iPhone配對時的關系,蘋果手表向iPhone提供用戶的運動數(shù)據(jù),所以此種情況蘋果手表是周邊設備,iPhone是中央設備,在Core Bluetooth 框架中分別對應如下:
-
centralManager:中央設備的處理類 -
peripheralManager:周邊設備的處理類
明確了周邊設備和中央設備后,接下來是如何發(fā)現(xiàn)對方并建立連接,在我們平時使用的手機搜索藍牙的過程中,都是先從搜索列表中選擇某個藍牙設備,在進行配對連接。peripheral通過廣播的形式向外界提供service,service會綁定一個獨一無二的UUID,有BTSIG UUID和Custom UUID二種,UUID用來確定中央設備連接周邊設備時確定身份用的。
每個service會有多個characteristic,characteristic也有自己的UUID,characteristic可以理解為周邊設備提供的具體服務,其UUID用來區(qū)分提供的每一個具體服務,因為一個service是可以提供多種具體服務的,中央設備通過UUID來讀寫這些服務。
在雙方建立了連接后就要商議如何發(fā)送和接受數(shù)據(jù)了,數(shù)據(jù)傳輸協(xié)議部分我們不用細究,Core Bluetooth都為我們處理好了,至于MTU最大最大傳輸單元現(xiàn)在是是271bytes,數(shù)據(jù)超過了就會分段發(fā)送。
實戰(zhàn)演示
CBPeripheralManager
新建一個PeripheralViewController類并繼承UIViewController,定義成員變量peripheralManager并初始化,同時設置代理,由于篇幅有限這里只貼出關鍵代碼:
peripheralManager = CBPeripheralManager(delegate: self, queue: nil)
Peripheral Manager delegate
代理必須實現(xiàn)的方法如下:
extension PeripheralViewController: CBPeripheralManagerDelegate {
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
switch peripheral.state {
case .poweredOn:
textCharacteristic = CBMutableCharacteristic(type: textCharacteristicUUID, properties: .notify, value: nil, permissions: .readable)
mapCharacteristic = CBMutableCharacteristic(type: mapCharacteristicUUID, properties: .writeWithoutResponse, value: nil, permissions: .writeable)
let service = CBMutableService(type: TextOrMapServiceUUID, primary: true)
service.characteristics = [textCharacteristic, mapCharacteristic]
peripheralManager.add(service)
default: return
}
}
當藍牙服務可用時,需要創(chuàng)建service并關聯(lián)相應的characteristic,代碼中的UUID都是定義的字符串常量。
peripheralManager.startAdvertising([CBAdvertisementDataServiceUUIDsKey: [TextOrMapServiceUUID]])
通過startAdvertising方法來向外界發(fā)送廣播。
由于
iOS的限制,當iOS設備作為周邊設備向外廣播時是無法利用CBAdvertisementDataManufacturerDataKey攜帶manufacturer data的。
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral,
didSubscribeTo characteristic: CBCharacteristic) {
guard characteristic == textCharacteristic else { return }
prepareDataAndSend()
}
func prepareDataAndSend() {
guard let data = textView.text.data(using: .utf8) else { return }
self.dataToSend = data
sendDataIndex = 0
sendData()
}
func sendData() {
if sendingEOM {
let didSend = peripheralManager.updateValue("EOM".data(using: .utf8)!, for: textCharacteristic, onSubscribedCentrals: nil)
if didSend {
sendingEOM = false
print("Sent: EOM")
}
return
}
let numberOfBytes = (dataToSend as NSData).length
guard sendDataIndex < numberOfBytes else { return }
var didSend = true
while didSend {
var amountToSend = numberOfBytes - sendDataIndex
if amountToSend > notifyMTU {
amountToSend = notifyMTU
}
let chunk = dataToSend.withUnsafeBytes{(body: UnsafePointer<UInt8>) in
return Data(
bytes: body + sendDataIndex,
count: amountToSend
)
}
didSend = peripheralManager.updateValue(chunk, for: textCharacteristic, onSubscribedCentrals: [])
if !didSend { return }
guard let stringFromData = String(data: chunk, encoding: .utf8) else { return }
print("Sent: \(stringFromData)")
sendDataIndex += amountToSend
if sendDataIndex >= dataToSend.count {
sendingEOM = true
let eomSent = peripheralManager.updateValue("EOM".data(using: .utf8)!, for: textCharacteristic, onSubscribedCentrals: nil)
if eomSent {
sendingEOM = false
print("Sent: EOM")
}
return
}
}
}
此回調會在中央設備訂閱了當初廣播的characteristic時調用,這里我們準備發(fā)送數(shù)據(jù),發(fā)送數(shù)據(jù)的過程中和中央設備需要約定一個標識表明數(shù)據(jù)是否發(fā)送完畢,這里采用了EOM標志作為結束位,采用二進制流的形式進行發(fā)送。
func peripheralManagerIsReady(toUpdateSubscribers peripheral: CBPeripheralManager) {
sendData()
}
此回調在CBPeripheralManager準備發(fā)送下一段數(shù)據(jù)時發(fā)送,這里一般用來實現(xiàn)保證分段數(shù)據(jù)按順序發(fā)送給中央設備。
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
guard let request = requests.first, request.characteristic == mapCharacteristic else {
peripheral.respond(to: requests.first!, withResult: .attributeNotFound)
return
}
map() { locationManager?.stopUpdatingLocation() }
peripheral.respond(to: request, withResult: .success)
}
fileprivate func map(completionHandler: () -> Void) {
locationManager = CLLocationManager()
locationManager?.delegate = self
locationManager?.desiredAccuracy = kCLLocationAccuracyBest
locationManager?.requestWhenInUseAuthorization()
if CLLocationManager.authorizationStatus() == .authorizedWhenInUse || CLLocationManager.authorizationStatus() == .authorizedAlways {
locationManager?.startUpdatingLocation()
}
}
此回調在中央設備針對響應的characteristic發(fā)送數(shù)據(jù)給外圍設備時調用,這里我們模擬中央設備發(fā)送打開地圖的指令給iPhone。
CBCentralManager
新建一個CentralViewController類并繼承UIViewController,定義成員變量centralManager并初始化,同時設置代理,由于篇幅有限這里只貼出關鍵代碼:
centralManager = CBCentralManager(delegate: self, queue: nil)
Central Manager delegate
代理必須要實現(xiàn)的方法如下:
func centralManagerDidUpdateState(_ central: CBCentralManager) {
switch central.state {
case .poweredOn: scan()
case .poweredOff, .resetting: cleanup()
default: return
}
}
func scan() {
centralManager.scanForPeripherals(withServices: [TextOrMapServiceUUID], options: [CBCentralManagerScanOptionAllowDuplicatesKey: NSNumber(value: true as Bool)])
}
func cleanup() {
guard discoveredPeripheral?.state != .disconnected,
let services = discoveredPeripheral?.services else {
centralManager.cancelPeripheralConnection(discoveredPeripheral!)
return
}
for service in services {
if let characteristics = service.characteristics {
for characteristic in characteristics {
if characteristic.uuid.isEqual(textCharacteristicUUID) {
if characteristic.isNotifying {
discoveredPeripheral?.setNotifyValue(false, for: characteristic)
return
}
}
}
}
}
centralManager.cancelPeripheralConnection(discoveredPeripheral!)
}
藍牙可用時開始掃描,通過UUID掃描外圍設備廣播的服務。
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
guard RSSI_range.contains(RSSI.intValue) && discoveredPeripheral != peripheral else { return }
discoveredPeripheral = peripheral
centralManager.connect(peripheral, options: [:])
}
需要檢查RSSI強度,只有藍牙信號強度在一定范圍內才開始嘗試進行連接。
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
if let error = error { print(error.localizedDescription) }
cleanup()
}
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
centralManager.stopScan()
data.removeAll()
peripheral.delegate = self
peripheral.discoverServices([TextOrMapServiceUUID])
}
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
if (peripheral == discoveredPeripheral) {
cleanup()
}
scan()
}
以上是關于連接的幾個回調函數(shù),連接成功后就停止掃描,然后調用peripheral.discoverServices方法,這會來到Peripheral Delegate中的相應代理方法。
Peripheral Delegate
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
if let error = error {
print(error.localizedDescription)
cleanup()
return
}
guard let services = peripheral.services else { return }
for service in services {
peripheral.discoverCharacteristics([textCharacteristicUUID, mapCharacteristicUUID], for: service)
}
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
if let error = error {
print(error.localizedDescription)
cleanup()
return
}
guard let characteristics = service.characteristics else { return }
for characteristic in characteristics {
if characteristic.uuid == textCharacteristicUUID {
textCharacteristic = characteristic
peripheral.setNotifyValue(true, for: characteristic)
} else if characteristic.uuid == mapCharacteristicUUID {
mapCharacteristic = characteristic
}
}
}
此回調用來發(fā)現(xiàn)services,實際開發(fā)中這里可能用列表展示發(fā)現(xiàn)的服務,讓用戶進行相應的選擇。
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
if let error = error {
print(error.localizedDescription)
return
}
if characteristic == textCharacteristic {
guard let newData = characteristic.value else { return }
let stringFromData = String(data: newData, encoding: .utf8)
if stringFromData == "EOM" {
textView.text = String(data: data, encoding: .utf8)
data.removeAll()
} else {
data.append(newData)
}
}
}
此回調對應peripheralManager.updateValue這個方法,能拿到外圍設備發(fā)送過來的數(shù)據(jù)。
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
if let error = error { print(error.localizedDescription) }
guard characteristic.uuid == textCharacteristicUUID else { return }
if characteristic.isNotifying {
print("Notification began on \(characteristic)")
} else {
print("Notification stopped on \(characteristic). Disconnecting...")
}
}
此回調處理外圍設備的characteristic通知,比如下線或者離開的情況,這里進行簡單的打印。
總結
對藍牙開發(fā)中的外圍設備,中央設備,UUID,service和characteristic等基本概念進行了簡單介紹,并利用Core Bluetooth 框架進行了簡單的demo演示,主要是需要理解幾個特定代理方法即可,同時由于iOS的限制,iPhone在作為外設時在廣播的時候是不能發(fā)送額外數(shù)據(jù)的,這點需要注意。