蓝牙属于近场通讯中的一种,
iOS
中使用Core Bluetooth
框架实现蓝牙通信,Core Bluetooth
支持蓝牙低功耗的4.0
模式,就是通常说称之的BLE
,在生活中BLE
无处不在,例如智能家居,健身器材和智能玩具等,利用苹果提供的Core Bluetooth
框架可以实现和BLE
设备进行通信。
蓝牙中各角色的理解
在蓝牙开发中我们把提供服务的一方称之为周边设备,接收服务的一方称之为中央设备,典型的例子就是苹果手表和iPhone
配对时的关系,苹果手表向iPhone
提供用户的运动数据,所以此种情况苹果手表是周边设备,iPhone
是中央设备,在Core Bluetooth
框架中分别对应如下:
-
centralManager
:中央设备的处理类 -
peripheralManager
:周边设备的处理类
明确了周边设备和中央设备后,接下来是如何发现对方并建立连接,在我们平时使用的手机搜索蓝牙的过程中,都是先从搜索列表中选择某个蓝牙设备,在进行配对连接。peripheral
通过广播的形式向外界提供service
,service
会绑定一个独一无二的UUID
,有BTSIG UUID
和Custom UUID
二种,UUID
用来确定中央设备连接周边设备时确定身份用的。
每个service
会有多个characteristic
,characteristic
也有自己的UUID
,characteristic
可以理解为周边设备提供的具体服务,其UUID
用来区分提供的每一个具体服务,因为一个service
是可以提供多种具体服务的,中央设备通过UUID
来读写这些服务。
在双方建立了连接后就要商议如何发送和接受数据了,数据传输协议部分我们不用细究,Core Bluetooth
都为我们处理好了,至于MTU最大最大传输单元现在是是271bytes
,数据超过了就会分段发送。
实战演示
CBPeripheralManager
新建一个PeripheralViewController
类并继承UIViewController
,定义成员变量peripheralManager
并初始化,同时设置代理,由于篇幅有限这里只贴出关键代码:
peripheralManager = CBPeripheralManager(delegate: self, queue: nil)
Peripheral Manager delegate
代理必须实现的方法如下:
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
}
}
当蓝牙服务可用时,需要创建service
并关联相应的characteristic
,代码中的UUID
都是定义的字符串常量。
peripheralManager.startAdvertising([CBAdvertisementDataServiceUUIDsKey: [TextOrMapServiceUUID]])
通过startAdvertising
方法来向外界发送广播。
由于
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
时调用,这里我们准备发送数据,发送数据的过程中和中央设备需要约定一个标识表明数据是否发送完毕,这里采用了EOM
标志作为结束位,采用二进制流的形式进行发送。
func peripheralManagerIsReady(toUpdateSubscribers peripheral: CBPeripheralManager) {
sendData()
}
此回调在CBPeripheralManager
准备发送下一段数据时发送,这里一般用来实现保证分段数据按顺序发送给中央设备。
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
发送数据给外围设备时调用,这里我们模拟中央设备发送打开地图的指令给iPhone
。
CBCentralManager
新建一个CentralViewController
类并继承UIViewController
,定义成员变量centralManager
并初始化,同时设置代理,由于篇幅有限这里只贴出关键代码:
centralManager = CBCentralManager(delegate: self, queue: nil)
Central Manager delegate
代理必须要实现的方法如下:
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()
}
以上是关于连接的几个回调函数,连接成功后就停止扫描,然后调用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
}
}
}
此回调用来发现services
,实际开发中这里可能用列表展示发现的服务,让用户进行相应的选择。
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
这个方法,能拿到外围设备发送过来的数据。
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
通知,比如下线或者离开的情况,这里进行简单的打印。
总结
对蓝牙开发中的外围设备,中央设备,UUID
,service
和characteristic
等基本概念进行了简单介绍,并利用Core Bluetooth
框架进行了简单的demo
演示,主要是需要理解几个特定代理方法即可,同时由于iOS
的限制,iPhone
在作为外设时在广播的时候是不能发送额外数据的,这点需要注意。