概述
基础概念
蓝牙通信基于 client-server 构架,由两个角色进行通信,分别是 Central
和 Peripheral
,中文环境可以理解为主设备
和从设备
。
Peripheral
角色主要作为数据的提供方,比如 Apple Watch;Central
主要作为数据需求方,通常为 iPhone/iPad 等产品。
Peripheral
提供一系列的服务(service),每种服务含有一个或多个特征属性(characteristics)。
iOS 提供 CoreBuletooth 框架,作为蓝牙 4.0 通信的基础。对应每一个 Peripheral
,CoreBuletooth 框架会为其分配一个 UUID,据测试,一般情况下,这个 UUID 在一个 iOS 设备上是不会改变的,但对于不同的 iOS 设备,拿到的 UUID 却又不同。Central
的 ID 由底层框架维护,上层并不知晓。
在通信时,一方向外界广播数据,并携带接收方的 ID,周边所有蓝牙设备都会收到消息,如果发现自己与该 ID 匹配,则处理消息,否者忽略。
在建立连接前,需要知道周边设备的 UUID,需要一个发现的过程。
发现
未连接的蓝牙设备,会不断的向周边广播数据。数据中携带它具有的服务(Service),名称,信号强度,厂商信息等。在 iOS 中以 CBPeripheral
类表示。
中心设备通过发现的方式来找到周边设备进而建立连接通信。
CBCentralManager 类代表 iOS 设备,开始使用前,先进行初始化操作。
let centralManager = CBCentralManager()
centralManager.delegate = self
复制代码
CBCentralManager
初始或会驱动硬件,这个过程是异步的,如果驱动成功/失败,会通过代理方法告知:
func centralManagerDidUpdateState(_ central: CBCentralManager)
复制代码
提示:如果代理方法没有调用,控制台输出
[CoreBluetooth] XPC connection invalid
,很可能是centralManager
实例被释放了。详细参考XPC Error CoreBluetooth | Apple Developer Forums
Central
进入 powerOn
状态后,才可执行扫描:
centralManager.scanForPeripherals(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey: false])
复制代码
如果清楚自己所需的蓝牙设备服务,withServices 参数传入具体的服务 UUID 列表,如果传 nil
默认发现所有设备。options
用于指定发现的规则,比较常用的是 CBCentralManagerScanOptionAllowDuplicatesKey
,默认情况下,中心设备每收到一个包,就是调用一次 Delegate 方法,设置为 false
,会每隔一段时间接收一次周边设备的广播信号,如果只是为了发现连接设备,这样设置可以节省电量以及提高 App 的性能。
centralManager
发现的设备的回调:
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
// peripheral:发现的设备
// advertisementData: 广播数据字典
// RSSI: 信号强度
}
复制代码
广播数据包含的内容在Advertisement Data Retrieval Keys | Apple Developer Documentation。需要注意的是,这里含有的名称字段和peripheral.name
的值并不一定相同。后者是前者的系统级缓存,并且没有给出缓存失效时间,如果蓝牙设备中途修改过名称,读到的很可能是失效的缓存,真实的名称在 CBAdvertisementDataLocalNameKey
中。虽然修改名称的可能性很小,但是难免也会有人遇到这个坑。
需要注意的是,发现的设备必须主动去持有,否者在超出作用域就释放了。
连接
发现目标设备后,连接设备。
central.connect(peripheral, options: nil)
复制代码
如果设备连接成功,回调代理方法:
optional public func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral)
复制代码
如果连接失败,回调:
optional public func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?)
复制代码
该方法也会在设备超出连接返回的时候调用。
在 iOS 中,存在系统级的蓝牙连接方式,叫做配对。配对的设备可在设置->蓝牙->我的设备
查看。配对的设备,即使 kill 应用程序,底层仍会持有连接的状态,CBCentralManager
提供获取已配对设备的方法。
open func retrieveConnectedPeripherals(withServices serviceUUIDs: [CBUUID]) -> [CBPeripheral]
复制代码
不过,需要提供获取已连接设备的 serviceUUIDs
,无论是哪个应用程序配对的蓝牙设备只要具有对应的服务都会被获取。
最后,获取到的设备只是底层连接,上层应用仍然需要调用 connectPeripheral:options:
方法,等待centralManager:didConnectPeripheral:
回调,使其进入连接状态。
建立过连接的设备,系统都会缓存它的信息,只要它的硬件唯一标识没有修改,系统为其分配的 identifier
会始终不变。所以,建立过连接的设备,可通过保存它的 identifier
来再次获取,并建立连接。
open func retrievePeripherals(withIdentifiers identifiers: [UUID]) -> [CBPeripheral]
复制代码
发现服务和特征
建立连接后,需要发现该蓝牙设备所具有的服务,每个服务有对应的 UUID。
peripheral.discoverServices(nil)
复制代码
一般蓝牙设备具有多种服务, 如果希望发现所有的服务,传 nil
。在开发中,我们清楚所需要的服务类型,最好传入具体的服务 UUID。成功发现服务会回调 CBPeripheralDelegate
的如下方法:
optional public func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?)
复制代码
服务类型保存在 services
属性中。
发现目标服务后,下一步是该发现服务中的具体特征(Characteristics),因为数据的读写对象是它。
peripheral.discoverCharacteristics(nil, for: service)
复制代码
同样特征也具有 UUID,如果传 nil
将获取到该服务下的所有特征,当然如果知道目标特征的 UUID,传对应的 ID 能够提高效率。特征发现成功后,会有如下回调:
optional public func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?)
复制代码
目标特征保存在 service.characteristics
中,接着可以对它们进行读写操作。
读特征值
单一的数据存储在 Service 的 Characteristic 中,比如说温度。
有两种方式读取 Characteristic 中的数据:
- 直接读取
open func readValue(for characteristic: CBCharacteristic)
复制代码
如果读取到数据,回调代理方法:
optional public func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?)
复制代码
数据存储于 charateristic.value
中。
并不是所有的特征都是可读的,需提前检查检查 CBCharacteristic
的properties
属性。确保它包含 CBCharacteristicPropertyRead
,如果不包含,上述代理方法会返回错误。
- 订阅通知
有些情况下,特征值会动态的变化,如果都需要手动去获取效率会比较低。Characteristic 还有一个被订阅的功能,订阅之后一旦特征值发生变化就会通过回调方法通知 App。
peripheral.setNotifyValue(true, for: characteristic)
复制代码
订阅成功与否的状态同样通过代理方法返回:
optional public func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?)
复制代码
当然并不是所有的 Characteristic 都有被订阅的功能。同样需要检查properties
属性,包含 Notify
或者 Indicate
才能被订阅。
写特征值
有时候还需要向 Peripheral 发送数据,实际上是对它的某个特征值进行写操作,写操作有两种形式,有回复和没有回复。
peripheral.peripheral.writeValue(data, for: characteristic, type: .withResponse)
复制代码
.withResponse
表示有回复的类型,回复以代理的形式通知:
optional public func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?)
复制代码
如果写入操作失败,错误信息保存在 error
中。
.withoutResponse
不会有回复,只会尽最大努力的写,但无法保证能写成功,如果写失败,不会有任何通知和错误信息。
传入的 data 数据,在内部会被拷贝,后续修改或者释放影响写操作。
同样,并不是所有的特征都具有写的功能,在写之前需检查该特征的 properties
属性是否包含 write
或者 writeWithoutResponse
。
转载于:https://juejin.im/post/5a5dd9be6fb9a01ca2678060
最后
以上就是精明钢笔为你收集整理的iOS 蓝牙开发·基础篇的全部内容,希望文章能够帮你解决iOS 蓝牙开发·基础篇所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复