本文目录
- 1、蓝牙介绍
- 2、iBeacon
- 3、iOS 蓝牙
- 4、中心模式的使用
- 5、外设模式的使用
- 6、后台运行蓝牙服务
- 7、第三方框架
1、蓝牙介绍
2、iBeacon
具体讲解见 Beacon
iBeacon 是苹果公司 2013 年 9 月发布的移动设备用 OS(iOS7)上配备的新功能。其工作方式是,配备有低功耗蓝牙(BLE)通信功能的设备使用 BLE 技术向周围发送自己特有的 ID,接收到该 ID 的应用软件会根据该 ID 采取一些行动。比如,在店铺里设置 iBeacon 通信模块的话,便可让 iPhone 和 iPad 上运行一资讯告知服务器,或者由服务器向顾客发送折扣券及进店积分。此外,还可以在家电发生故障或停止工作时使用 iBeacon 向应用软件发送资讯。
苹果 WWDC 14 之后,对 iBeacon 加大了技术支持和对其用于室内地图的应用有个更明确的规划。苹果公司公布了 iBeacon for Developers 和 Maps for Developers 等专题页面。
3、iOS 蓝牙
3.1 常见简称
MFi:make for ipad ,iphone, itouch 专们为苹果设备制作的设备,开发使用 ExternalAccessory 框架。认证流程挺复杂的,而且对公司的资质要求较高,详见 iOS - MFi 认证。
BLE:buletouch low energy,蓝牙 4.0 设备因为低耗电,所以也叫做 BLE,开发使用 CoreBluetooth 框架。
-
GATT Profile(Generic Attribute Profile):GATT 配置文件是一个通用规范,用于在 BLE 链路上发送和接收被称为 “属性”(Attribute)的数据块。目前所有的 BLE 应用都基于 GATT。
- 定义两个 BLE 设备通过叫做 Service 和 Characteristic 的东西进行通信。中心设备和外设需要双向通信的话,唯一的方式就是建立 GATT 连接。
- GATT 连接是独占的。基于 GATT 连接的方式的,只能是一个外设连接一个中心设备。
- 配置文件是设备如何在特定的应用程序中工作的规格说明,一个设备可以实现多个配置文件。
-
GAP(Generic Access Profile):用来控制设备连接和广播,GAP 使你的设备被其他设备可见,并决定了你的设备是否可以或者怎样与合同设备进行交互。
- GATT 连接,必需先经过 GAP 协议。
- GAP 给设备定义了若干角色,主要两个:外围设备(Peripheral)和中心设备(Central)。
- 在 GAP 中外围设备通过两种方式向外广播数据:Advertising Data Payload(广播数据)和 Scan Response Data Payload(扫描回复)。
Profile:并不是实际存在于 BLE 外设上的,它只是一个被 Bluetooth SIG(一个以制定蓝牙规范,以推动蓝牙技术为宗旨的跨国组织)或者外设设计者预先定义的 Service 的集合。
Service:服务,是把数据分成一个个的独立逻辑项,它包含一个或者多个 Characteristic。每个 Service 有一个 UUID 唯一标识。UUID 有 16 bit 的,或者 128 bit 的。16 bit 的 UUID 是官方通过认证的,需要花钱购买,128 bit 是自定义的,可以自己设置。每个外设会有很多服务,每个服务中包含很多字段,这些字段的权限一般分为读 read,写 write,通知 notiy 几种,就是我们连接设备后具体需要操作的内容。
Characteristic:特征,GATT 事务中的最低界别,Characteristic 是最小的逻辑数据单元,当然它可能包含一个组关联的数据,例如加速度计的 X/Y/Z 三轴值。与 Service 类似,每个 Characteristic 用 16 bit 或者 128 bit 的 UUID 唯一标识。每个设备会提供服务和特征,类似于服务端的 API,但是机构不同。
Description:每个 Characteristic 可以对应一个或多个 Description 用户描述 Characteristic 的信息或属性。
Peripheral、Central:外设和中心,发起连接的是 Central,被连接的设备为 Peripheral。
3.2 工作模式
-
蓝牙通信中,首先需要提到的就是 central 和 peripheral 两个概念。这是设备在通信过程中扮演的两种角色。直译过来就是 [中心] 和 [周边(可以理解为外设)]。iOS 设备既可以作为 central,也可以作为 peripheral,这主要取决于通信需求。
-
例如在和心率监测仪通信的过程中,监测仪作为 peripheral,iOS 设备作为 central。区分的方式即是这两个角色的重要特点:提供数据的是谁,谁就是 peripheral;需要数据的是谁,谁就是 central。就像是 client 和 server 之间的关系一样。
-
-
那怎么发现 peripheral 呢
在 BLE 中,最常见的就是广播。实际上,peripheral 在不停的发送广播,希望被 central 找到。广播的信息中包含它的名字等信息。如果是一个温度调节器,那么广播的信息应该还会包含当前温度什么的。那么 central 的作用则是去 scan,找到需要连接的 peripheral,连接后便可进行通信了。
当 central 成功连上 peripheral 后,它便可以获取 peripheral 提供的所有 service 和 characteristic。通过对 characteristic 的数据进行读写,便可以实现 central 和 peripheral 的通信。
-
CoreBluetooth 框架的核心其实是两个东西,central 和 peripheral, 对应他们分别有一组相关的 API 和类。
-
这两组 API 分别对应不同的业务场景,如下图,左侧叫做中心模式,就是以你的手机(App)作为中心,连接其他的外设的场景。而右侧称为外设模式,使用手机作为外设连接其他中心设备操作的场景。
-
-
iOS 设备(App)作为 central 时:
当 central 和 peripheral 通信时,绝大部分操作都在 central 这边。此时,central 被描述为 CBCentralManager,这个类提供了扫描、寻找、连接 peripheral(被描述为 CBPeripheral)的方法。
-
下图标示了 central 和 peripheral 在 Core Bluetooth 中的表示方式:
当你操作 peripheral 的时候,实际上是在和它的 service 和 characteristic 打交道,这两个分别由 CBService 和 CBCharacteristic 表示。
-
iOS 设备(App)作为 Peripheral 时:
在 OS X 10.9 和 iOS 6 以后,设备除了能作为 central 外,还可以作为 peripheral。也就是说,可以发起数据,而不像以前只能管理数据了。
那么在此时,它被描述为 CBPeripheralManager,既然是作为 peripheral,那么这个类提供的主要方法则是对 service 的管理,同时还兼备着向 central 广播数据的功能。peripheral 同样会对 central 的读写要求做出相应。
-
下图则是设备作为 central 和 Peripheral 的示意图:
在充当 peripheral 时,CBPeripheralManager 处理的是可变的 service 和 characteristic,分别由 CBMutableService 和 CBMutableCharacteristic 表示。
-
中心模式(CBCentralManager)流程:
- 1、建立中心角色
- 2、扫描外设(discover)
- 3、连接外设(connect)
- 4、扫描外设中的服务和特征(discover)
- 4.1 获取外设的 services
- 4.2 获取外设的 Characteristics,获取 Characteristics 的值,获取 Characteristics 的 Descriptor 和 Descriptor 的值
- 5、与外设做数据交互(explore and interact)
- 6、订阅 Characteristic 的通知
- 7、断开连接(disconnect)
-
外设模式(CBPeripheralManager)流程:
- 1、启动一个 Peripheral 管理对象
- 2、设置本地 Peripheral 服务、特性、描述、权限等等
- 3、设置 Peripheral 发送广播
- 4、设置处理订阅、取消订阅、读 characteristic、写 characteristic 的委托方法
3.3 服务、特征和特征的属性
一个 peripheral 包含一个或多个 service,或提供关于信号强度的信息。service 是数据和相关行为的集合。例如,一个心率监测仪的数据就可能是心率数据。
-
service 本身又是由 characteristic 或者其他 service 组成的。characteristic 又提供了更为详细的 service 信息。还是以心率监测仪为例,service 可能会包含两个 characteristic,一个描述当前心率带的位置,一个描述当前心率的数据。
每个 characteristic 属性分为这么几种:读,写,通知这么几种方式。
-
外设、服务、特征间的关系
- 一个 CBPeripheral(蓝牙设备) 有一个或者多个 CBService(服务),而每一个 CBService 有一个或者多个 CBCharacteristic(特征),通过可写的 CBCharacteristic 发送数据,而每一个 CBCharacteristic 有一个或者多个 Description 用于描述 characteristic 的信息或属性。
3.4 设备状态
-
蓝牙设备状态:
- 1、待机状态(standby):设备没有传输和发送数据,并且没有连接到任何设备。
- 2、广播状态(Advertiser):周期性广播状态。
- 3、扫描状态(Scanner):主动寻找正在广播的设备。
- 4、发起链接状态(Initiator):主动向扫描设备发起连接。
- 5、主设备(Master):作为主设备连接到其他设备。
- 6、从设备(Slave):作为从设备连接到其他设备。
-
五种工作状态:
- 准备(standby)
- 广播(advertising)
- 监听扫描(Scanning)
- 发起连接(Initiating)
- 已连接(Connected)
3.5 蓝牙和版本的使用限制
蓝牙 2.0:越狱设备
蓝牙 4.0:iOS 6 以上
MFi 认证设备:无限制
3.6 设置系统使用蓝牙权限
-
设置系统使用蓝牙权限
4、中心模式的使用
中心模式的应用场景:主设备(手机去扫描连接外设,发现外设服务和属性,操作服务和属性的应用。一般来说,外设(蓝牙设备,比如智能手环之类的东西)会由硬件工程师开发好,并定义好设备提供的服务,每个服务对于的特征,每个特征的属性(只读,只写,通知等等)。
蓝牙程序需要使用真机调试。
4.1 App 连接外设的实现
-
1、建立中心角色
- 导入 CoreBluetooth 头文件,建立中心设备管理类,设置主设备委托。
-
2、扫描外设(discover)
扫描外设的方法需要放在 centralManager 成功打开的代理方法
- (void)centralManagerDidUpdateState:(CBCentralManager *)central
中,因为只有设备成功打开,才能开始扫描,否则会报错。扫描到外设后会进入代理方法
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *, id> *)advertisementData RSSI:(NSNumber *)RSSI;
中。
-
3、连接外设(connect)
对要连接的设备需要进行强引用,否则会报错。
一个主设备最多能连 7 个外设,每个外设最多只能给一个主设备连接,连接成功,失败,断开会进入各自的代理方法中。
-
4、扫描外设中的服务和特征(discover)
- 设备连接成功后,就可以扫描设备的服务了,同样是通过委托形式,扫描到结果后会进入委托方法。但是这个委托已经不再是主设备的委托(CBCentralManagerDelegate),而是外设的委托(CBPeripheralDelegate),这个委托包含了主设备与外设交互的许多回调方法,包括获取 services,获取 characteristics,获取 characteristics 的值,获取 characteristics 的 Descriptor,和 Descriptor的值,写数据,读 RSSI,用通知的方式订阅数据等等。
5 把数据写到 Characteristic 中
6、订阅 Characteristic 的通知
7、断开连接(disconnect)
运行效果
4.2 作为 Central 时的数据读写
4.2.1 初始化 CBCentralManager
第一步先进行初始化,可以使用
initWithDelegate:queue:options:
方法:上面的代码中,将 self 设置为代理,用于接收各种 central 事件。将 queue 设置为 nil,则表示直接在主线程中运行。
初始化 central manager 之后,设置的代理会调用
centralManagerDidUpdateState:
方法,所以需要去遵循<CBCentralManagerDelegate>
协议。这个 did update state 的方法,能获得当前设备是否能作为 central。关于这个协议的实现和其他方法,接下来会讲到,也可以先看看官方 API。
4.2.2 搜索当前可用的 peripheral
可以使用 CBCentralManager 的
scanForPeripheralsWithServices:options:
方法来扫描周围正在发出广播的 Peripheral 设备。第一个参数为 nil,表示所有周围全部可用的设备。在实际应用中,你可以传入一个 CBUUID 的数组(注意,这个 UUID 是 service 的 UUID 数组),表示只搜索当前数组包含的设备(每个 peripheral 的 service 都有唯一标识 UUID)。所以,如果你传入了这样一个数组,那么 central manager 则只会去搜素包含这些 service UUID 的 Peripheral。
CBUUID 是和 peripheral 相关的,和 central 本身关系不大,如果你是做的硬件对接,那么可以向硬件同事询问。
在调用
scanForPeripheralsWithServices:options:
方法之后,找到可用设备,系统会回调(每找到一个都会回调)centralManager:didDiscoverPeripheral:advertisementData:RSSI:
。该方法会返回找到的 peripheral,所以你可以使用数组将找到的 peripheral 存起来。当你找到你需要的那个 peripheral 时,可以调用 stop 方法来停止搜索。
4.2.3 连接 peripheral
找到你需要的 peripheral 之后,下一步就是调用
connectPeripheral:options:
方法来连接。当连接成功后,会回调方法
centralManager:didConnectPeripheral:
。在这个方法中,你可以去记录当前的连接状态等数据。不过在进行其他操作之前,你应该给已连接的这个 peripheral 设置代理(需要去遵循
<CBPeripheralDelegate>
协议),这样才能收到 peripheral 的回调(可以就写在上面这个方法中)。注意:在连接设备之前需要对要连接的设备进行强引用,否则会报错
4.2.4搜索 peripheral 的 service
当与 peripheral 成功建立连接以后,就可以通信了。第一步是先找到当前 peripheral 提供的 service,因为 service 广播的数据有大小限制(貌似是 31 bytes),所以你实际找到的 service 的数量可能要比它广播时候说的数量要多。调用 CBPeripheral 的
discoverServices:
方法可以找到当前 peripheral 的所有 service。在实际项目中,这个参数应该不是 nil 的,因为 nil 表示查找所有可用的 Service,但实际上,你可能只需要其中的某几个。搜索全部的操作既耗时又耗电,所以应该提供一个要搜索的 service 的 UUID 数组。
当找到特定的 Service 以后,会回调
<CBPeripheralDelegate>
的peripheral:didDiscoverServices:
方法。Core Bluetooth 提供了 CBService 类来表示 service,找到以后,它们以数组的形式存入了当前 peripheral 的 services 属性中,你可以在当前回调中遍历这个属性。如果是搜索的全部 service 的话,你可以选择在遍历的过程中,去对比 UUID 是不是你要找的那个。
4.2.5 搜索 service 的 characteristic
找到需要的 service 之后,下一步是找它所提供的 characteristic。如果搜索全部 characteristic,那调用 CBPeripheral 的
discoverCharacteristics:forService:
方法即可。如果是搜索当前 service 的 characteristic,那还应该传入相应的 CBService 对象。同样是出于节能的考虑,第一个参数在实际项目中应该是 characteristic 的 UUID 数组。也同样能在最佳实践中介绍。
找到所有 characteristic 之后,回调
peripheral:didDiscoverCharacteristicsForService:error:
方法,此时 Core Bluetooth 提供了 CBCharacteristic 类来表示 characteristic。可以通过以下代码来遍历找到的 characteristic。同样也可以通过添加 UUID 的判断来找到需要的 characteristic。
4.2.6 读取 characteristic 数据
characteristic 包含了 service 要传输的数据。例如温度设备中表达温度的 characteristic,就可能包含着当前温度值。这时我们就可以通过读取 characteristic,来得到里面的数据。
当找到 characteristic 之后,可以通过调用 CBPeripheral 的
readValueForCharacteristic:
方法来进行读取。当你调用上面这方法后,会回调
peripheral:didUpdateValueForCharacteristic:error:
方法,其中包含了要读取的数据。如果读取正确,可以用以下方式来获得值:注意,不是所有 characteristic 的值都是可读的,你可以通过 CBCharacteristicPropertyRead options 来进行判断。如果你尝试读取不可读的数据,那上面的代理方法会返回相应的 error。
4.2.7 订阅 Characteristic 数据
其实使用
readValueForCharacteristic:
方法并不是实时的。考虑到很多实时的数据,比如心率这种,那就需要订阅 characteristic 了。可以通过调用 CBPeripheral 的
setNotifyValue:forCharacteristic:
方法来实现订阅,注意第一个参数是 YES。如果是订阅,成功与否的回调是
peripheral:didUpdateNotificationStateForCharacteristic:error:
,读取中的错误会以 error 形式传回。当然也不是所有 characteristic 都允许订阅,依然可以通过 CBCharacteristicPropertyNoify options 来进行判断。
当订阅成功以后,那数据便会实时的传回了,数据的回调依然和之前读取 characteristic 的回调相同(注意,不是订阅的那个回调)
peripheral:didUpdateValueForCharacteristic:error:
。
4.2.8 向 characteristic 写数据
写数据其实是一个很常见的需求,如果 characteristic 可写,你可以通过 CBPeripheral 类的
writeValue:forCharacteristic:type:
方法来向设备写入 NSData 数据。关于写入数据的 type,如上面这行代码,type 就是 CBCharacteristicWriteWithResponse,表示当写入成功时,要进行回调。更多的类型可以参考 CBCharacteristicWriteType 枚举。
如果写入成功后要回调,那么回调方法是
peripheral:didWriteValueForCharacteristic:error:
。如果写入失败,那么会包含到 error 参数返回。注意:characteristic 也可能并不支持写操作,可以通过 CBCharacteristic 的 properties 属性来判断。
4.3 数据读写 - 知识补充
4.3.1 CBUUID
CBUUID 对象是用于 BLE 通信中 128 位的唯一标示符。peripheral 的 service,characteristic,characteristic descriptor 都包含这个属性。这个类包含了一系列生成 UUID 的方法。
UUID 有 16 位的,也有 128 位的。其中 SIG 组织提供了一部分 16 位的 UUID,这部分 UUID 主要用于公共设备,例如有个用蓝牙连接的心率监测仪,如果是用的公共的 UUID,那么无论谁做一个 app,都可以进行连接,因为它的 UUID 是 SIG 官方提供的,是公开的。如果公司是要做一个只能自己的 app 才能连接的设备,那么就需要硬件方面自定义 UUID。(关于这方面,包括通信的 GATT 协议、广播流程等详细介绍,可以看 iOS - GATT Profile 简介 这篇文章。讲得比较详细,能在很大程度上帮助我们理解 BLE 通信)。
CBUUID 类提供了可以将 16 位 UUID 转为 128 位 UUID 的方法。下面的代码是 SIG 提供的 16 位的心率 service UUID 转为 128 位 UUID 的方法:
如果需要获取 NSString 形式的 UUID,可以访问 CBUUID 的 UUIDString 只读属性。
4.3.2 设备唯一标识符
在有些时候,需要获取 peripheral 的唯一标示符(比如要做自动连接或绑定用户等操作),但是在搜索到 peripheral 之后,只能拿到 identifier,而且这个 identifier 根据连接的 central 不同而不同。也就是说,不同的手机连上之后,identifier 是不同的。虽然比较坑爹,但是这并不影响你做蓝牙自动连接。
唯一标示符(并且不会变的)是设备的 MAC 地址,对于 Android 来说,轻轻松松就能拿到,但对于 iOS,目前这一属性还是私有的。
-
如果一定有这样的需求(即一定要使用 MAC 地址),可以和硬件工程师沟通,使用下面的某一种方式解决:
- 将 MAC 地址写在某一个蓝牙特征中,当我们连接蓝牙设备之后,通过某一个特征获取 MAC 地址。
- 将 MAC 地址放在蓝牙设备的广播数据当中,然后在广播的时候,将 MAC 地址以广播的形式发出来,在不建立连接的情况下,就能拿到 MAC 地址。
- 我们可以通过蓝牙设备的出厂设备或者后期手动修改蓝牙设备的 name,作为唯一标识。
4.3.3 检查设备是否能作为 central
初始化 CBCentralManager 的时候,传入的 self 代理会触发回调
centralManagerDidUpdateState:
。在该方法中可通过central.state
来获得当前设备是否能作为 central。state 为 CBManagerState 枚举类型,具体定义如下:只有当
state == CBManagerStatePoweredOn
时,才代表正常。
4.3.4 检查 characteristic 访问权限
如果不检查也没事,因为无权访问会在回调中返回 error,但这毕竟是马后炮。如果有需要在读写之前检测,可以通过 characteristic 的 properties 属性来判断。该属性为 CBCharacteristicProperties 的 NS_OPIONS。
多个权限可以通过
|
和&
来判断是否支持,比如判断是否支持读或写。
4.3.5 写入后是否回调
在写入 characteristic 时,可以选择是否在写入后进行回调。调用方法和枚举常量如下。
回调方法为
所以即使没有判断写入权限,也可以通过回调的 error 来判断,但这样比起写入前判断更耗资源。
4.4 数据读写 - 最佳实践
- 在设备上一般都有很多地方要用到无线电通信,Wi-Fi、传统的蓝牙、以及使用 BLE 通信的 app 等等。这些服务都是很耗资源的,尤其是在 iOS 设备上。所以这里会讲解到如何正确的使用 BLE 以达到节能的效果。
4.4.1 只扫描你需要的 peripheral
在调用 CBCentralManager 的
scanForPeripheralsWithServices:options:
方法时,central 会打开无线电去监听正在广播的 peripheral,并且这一过程不会自动超时。所以需要我们手动设置 timer 去停掉。如果只需要连接一个 peripheral,那应该在
centralManager:didConnectPeripheral:
的回调中,用 stopScan 方法停止搜索。
4.4.2 只在必要的时候设置 CBCentralManagerScanOptionAllowDuplicatesKey
peripheral 每秒都在发送大量的数据包,
scanForPeripheralsWithServices:options:
方法会将同一 peripheral 发出的多个数据包合并为一个事件,然后每找到一个 peripheral 都会调用centralManager:didDiscoverPeripheral:advertisementData:RSSI:
方法。另外,当已发现的 peripheral 发送的数据包有变化时,这个代理方法同样会调用。以上合并事件的操作是
scanForPeripheralsWithServices:options:
的默认行为,即未设置 option 参数。如果不想要默认行为,可将 option 设置为 CBCentralManagerScanOptionAllowDuplicatesKey。设置以后,每收到广播,就会调用上面的回调(无论广播数据是否一样)。关闭默认行为一般用于以下场景:根据 peripheral 的距离来初始化连接(根据可用信号强度 RSSI 来判断)。设置这个 option 会对电池寿命和 app 的性能产生不利影响,所以一定要在必要的时候,再对其进行设置。
4.4.3 正确的搜索 service 与 characteristic
在搜索过程中,并不是所有的 service 和 characteristic 都是我们需要的,如果全部搜索,依然会造成不必要的资源浪费。假设你只需要用到 peripheral 提供的众多 service 中的两个,那么在搜索 service 的时候可以设置要搜索的 service 的 UUID。
用这种方式搜索到 service 以后,也可以用类似的办法来限制 characteristic 的搜索范围(
discoverCharacteristics:forService:
)。
4.4.4 接收 characteristic 数据
-
接收 characteristic 数据的方式有两种:
- 在需要接收数据的时候,调用
readValueForCharacteristic:
,这种是需要主动去接收的。 - 用
setNotifyValue:forCharacteristic:
方法订阅,当有数据发送时,可以直接在回调中接收。
- 在需要接收数据的时候,调用
如果 characteristic 的数据经常变化,那么采用订阅的方式更好。
4.4.5 适时断开连接
在不用和 peripheral 通信的时候,应当将连接断开,这也对节能有好处。
-
在以下两种情况下,连接应该被断开:
- 当 characteristic 不再发送数据时。(可以通过 isNotifying 属性来判断)
- 你已经接收到了你所需要的所有数据时。
以上两种情况,都需要先结束订阅,然后断开连接。
注意:
cancelPeripheralConnection:
是非阻塞性的,如果在 peripheral 挂起的状态去尝试断开连接,那么这个断开操作可能执行,也可能不会。因为可能还有其他的 central 连着它,所以取消连接并不代表底层连接也断开。从 app 的层面来讲,在决定断开 peripheral 的时候,会调用 CBCentralManagerDelegate 的centralManager:didDisconnectPeripheral:error:
方法。
4.4.6 再次连接 peripheral
-
CoreBluetooth 提供了三种再次连接 peripheral 的方式:
- 调用
retrievePeripheralsWithIdentifiers:
方法,重连已知的 peripheral 列表中的 peripheral(以前发现的,或者以前连接过的)。 - 调用
retrieveConnectedPeripheralsWithServices:
方法,重新连接当前【系统】已经连接的 peripheral。 - 调用
scanForPeripheralsWithServices:options:
方法,连接搜索到的 peripheral。
- 调用
-
是否需要重新连接以前连接过的 peripheral 要取决于你的需求,下图展示了当你尝试重连时可以选择的流程:
三列代表着三种重连的方式。当然这也是你可以选择进行实现的,这三种方式也并不是都需要去实现,依然取决于你的需求。
-
1、尝试连接已知的 peripheral
在第一次成功连上 peripheral 之后,iOS 设备会自动给 peripheral 生成一个 identifier(NSUUID 类型),这个标识符可通过 peripheral.identifier 来访问。这个属性由 CBPeriperal 的父类 CBPeer 提供,API 注释写着: The unique, persistent identifier associated with the peer.
因为 iOS 拿不到 peripheral 的 MAC 地址,所以无法唯一标识每个硬件设备,根据这个注释来看,应该 Apple 更希望你使用这个 identifer 而不是 MAC 地址。值得注意的是,不同的 iOS 连接同一个 peripheral 获得的 identifier 是不一样的。所以如果一定要获得唯一的 MAC 地址,可以和硬件工程师协商,让 peripheral 返给你。
当第一次连接上 peripheral 并且系统自动生成 identifier 之后,我们需要将它存下来(可以使用 NSUserDefaults)。在再次连接的时候,使用
retrievePeripheralsWithIdentifiers:
方法将之前记录的 peripheral 读取出来,然后我们去调用connectPeripheral:options:
方法来进行重新连接。调用这个方法之后,会返回一个 CBPeripheral 的数组,包含了以前连过的 peripheral。如果这个数组为空,则说明没找到,那么你需要去尝试另外两种重连方式。如果这个数组有多个值,那么你应该提供一个界面让用户去选择。
如果用户选择了一个,那么可以调用
connectPeripheral:options:
方法来进行连接,连接成功之后依然会走centralManager:didConnectPeripheral:
回调。-
注意,连接失败通常有一下几个原因:
- peripheral 与 central 的距离超出了连接范围。
- 有一些 BLE 设备的地址是周期性变化的。所以,即使 peripheral 就在旁边,如果它的地址已经变化,而你记录的地址已经变化了,那么也是连接不上的。如果是因为这种原因连接不上,那你需要调用
scanForPeripheralsWithServices:options:
方法来进行重新搜索。
更多关于随机地址的资料可以看 《苹果产品的蓝牙附件设计指南》。
-
2、连接系统已经连接过的 peripheral
另外一种重连的方式是通过检测当前系统是否已经连上了需要的 peripheral(可能被其他 app 连接了)。调用
retrieveConnectedPeripheralsWithServices:
会返回一个 CBPeripheral 的数组。因为当前可能不止一个 peripheral 连上的,所以你可以通过传入一个 service 的 CBUUID 的数组来过滤掉一些不需要的 peripheral。同样,这个数组有可能为空,也有可能不为空,处理方式和上一节的方式相同。找到要连接的 peripheral 之后,处理方式也和上一节相同。
4.4.7 自动连接
可以在程序启动或者需要使用蓝牙的时候,判断是否需要自动连接。如果需要,则可以尝试连接已知的 peripheral。这个重连上一个小节刚好提到过:在上一次连接成功后,记录 peripheral 的 identifier,然后重连的时候,读取即可。
在自动连接这一块,还有一个小坑。在使用
retrievePeripheralsWithIdentifiers:
方法将之前记录的 peripheral 读取出来,然后我们去调用connectPeripheral:options:
方法来进行重新连接。我之前怎么试都有问题,最后在 CBCentralManager 的文档上找到了这样一句话:Pending connection attempts are also canceled automatically when peripheral is deallocated.这句话的意思是说,在 peripheral 的引用释放之后,连接会自动取消。因为我在读取出来之后,接收的 CBPeripheral 是临时变量,没有强引用,所以出了作用域就自动释放了,从而连接也自动释放了。所以在自动连接的时候,读取出来别忘了去保存引用。
4.4.8 连接超时
- 因为 CoreBluetooth 并未帮我们处理连接超时相关的操作,所以超时的判断还需要自己维护一个 timer。可以在 start scan 的时候启动(注意如果是自动连接,那么重连的时候也需要启动),然后在搜索到以后 stop timer。当然,如果超时,则看你具体的处理方式了,可以选择 stop scan,然后让用户手动刷新。
4.4.9 蓝牙名称更新
在 peripheral 修改名字过后,iOS 存在搜索到蓝牙名字还未更新的问题。先来说一下出现这个问题的原因,以下是摘自 Apple Developer Forums 上的回答:
There are 2 names to consider. The advertising name and the GAP (Generic Access Profile) name.
For a peripheral which iOS has never connected before, the ‘name’ property reported is the advertising name. Once it is connected, the GAP name is cached, and is reported as the peripheral’s name. GAP name is considered a “better” name due to the size restrictions on the advertising name.
There is no rule that says both names must match. That depends on your use case and implementation. Some people will consider the GAP name as the fixed name, but the advertising name more of an “alias”, as it can easily be changed.
If you want both names in sync, you should change the GAP name as well along with the advertised name. Implemented properly, your CB manager delegate will receive a call to – peripheralDidUpdateName:
If you want to manually clear the cache, you need to reset the iOS device.
大致意思是:peripheral 其实存在两个名字,一个 advertising name,一个 GAP name。在没有连接过时,收到的 CBPeripheral 的 name 属性是 advertising name(暂且把这个名字称为正确的名字,因为在升级或换名字之后,这个名字才是最新的)。一旦 iOS 设备和 peripheral 连接过,GAP name 就会被缓存,与此同时,CBPeripheral 的 name 属性变成 GAP name,所以在搜索到设备时,打印 CBPeripheral 的 name,怎么都没有变。上文给出的解释是,因为数据大小限制,GAP name 更优于 advertising name。这两个名字不要求要相同,并且,如果要清除 GAP name 的缓存,那么需要重置 iOS 设备。
下面来说一下解决方案,主要分为两种,一种是更新 GAP name,一种是直接拿 advertising name。
更新 GAP name 的方式我目前没找到方法,有些人说是 Apple 的 bug,这个还不清楚,希望有解决方案的朋友联系我。
那就来说下怎么拿到 advertising name 吧。
centralManager:didDiscoverPeripheral:advertisementData:RSSI:
方法中可以通过 advertisementData 来拿到 advertising name,如下:
objc NSLog(@"%@", advertisementData[CBAdvertisementDataLocalNameKey]);
然后可以选择把这个 name 返回外部容器来进行显示,用户也可以通过这个来进行选择。
-
关于这个部分查找的资料有:
4.5 数据读写 - OTA 固件升级与文件传输
OTA(Over-the-Air):空中传输,一般用于固件升级,网上的资料大多是怎么给手机系统升级,少部分资料是 peripheral 怎么接收并进行升级,唯独没有 central 端怎么传输的。其实文件传输很简单,只是蓝牙传输的数据大小使得这一步骤稍显复杂。
首先,文件传输,其实也是传输的数据,即 NSData,和普通的 peripheral 写入没什么区别。固件升级的文件一般是
.bin
文件,也有.zip
的。不过这些文件,都是数据,所以首先将文件转为 NSData。但是 data 一般很长,毕竟是文件。直接通过
writeValue:forCharacteristic:type:
写入的话,不会有任何回调。哪怕是错误的回调,都没有。这是因为蓝牙单次传输的数据大小是有限制的。具体的大小我不太明确,看到 StackOverflow 上有人给出的 20 bytes,我就直接用了,并没有去具体查证(不过试了试 30 bytes,回调数据长度错误)。既然长度是 20,那在每次发送成功的回调中,再进行发送就好,直到发送完成。下面来讨论下是怎么做的吧。
-
1、区别普通写入与文件写入
分割数据并发送,每次都要记录上一次已经写入长度(偏移量 self.otaSubDataOffset),然后截取 20 个长度。需要注意的是最后一次的长度,注意不要越界了。
数据的发送和普通写入没什么区别。
-
2、当前已发送长度与发送结束的回调
因为 OTA 的写入可能需要做进度条之类的,所以最好和普通的写入回调区分开。
在每次写入成功中,判断是否已经发送完成(已发送的长度和总长度相比)。如果还未发送完成,则返回已发送的长度给控制器(可以通过代理实现)。如果已发送完成,则返回发送完成(可以通过代理实现)。