iOS 蓝牙开发·基础篇

基础概念

蓝牙通讯基于 client-server 构架,由两个角色进行通讯,分别是 CentralPeripheral,中文环境能够理解为主设备从设备swift

Peripheral 角色主要做为数据的提供方,好比 Apple Watch;Central 主要做为数据需求方,一般为 iPhone/iPad 等产品。缓存

Peripheral 提供一系列的服务(service),每种服务含有一个或多个特征属性(characteristics)。bash

两种角色要相互通讯,双方就必需要有惟一的标识。

iOS 提供 CoreBuletooth 框架,做为蓝牙 4.0 通讯的基础。对应每个 Peripheral,CoreBuletooth 框架会为其分配一个 UUID,据测试,通常状况下,这个 UUID 在一个 iOS 设备上是不会改变的,但对于不一样的 iOS 设备,拿到的 UUID 却又不一样。Central 的 ID 由底层框架维护,上层并不知晓。app

在通讯时,一方向外界广播数据,并携带接收方的 ID,周边全部蓝牙设备都会收到消息,若是发现本身与该 ID 匹配,则处理消息,否者忽略。框架

在创建链接前,须要知道周边设备的 UUID,须要一个发现的过程。异步

发现

未链接的蓝牙设备,会不断的向周边广播数据。数据中携带它具备的服务(Service),名称,信号强度,厂商信息等。在 iOS 中以 CBPeripheral 类表示。ide

中心设备经过发现的方式来找到周边设备进而创建链接通讯。性能

CBCentralManager 类表明 iOS 设备,开始使用前,先进行初始化操做。测试

let centralManager = CBCentralManager()
centralManager.delegate = self
复制代码

CBCentralManager初始或会驱动硬件,这个过程是异步的,若是驱动成功/失败,会经过代理方法告知:ui

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 中的数据:

  1. 直接读取
open func readValue(for characteristic: CBCharacteristic)
复制代码

若是读取到数据,回调代理方法:

optional public func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?)
复制代码

数据存储于 charateristic.value中。

并非全部的特征都是可读的,需提早检查检查 CBCharacteristicproperties 属性。确保它包含 CBCharacteristicPropertyRead,若是不包含,上述代理方法会返回错误。

  1. 订阅通知

有些状况下,特征值会动态的变化,若是都须要手动去获取效率会比较低。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

相关文章
相关标签/搜索