iOS CoreBluetooth 学习记录

前言

最近公司的项目中恰好用到了 CoreBluetooth 相关的知识,在看了网上不少文章及官方文档的介绍后,如今对大概的框架有了基本的了解。html

在此,写下这篇文章记录本身这段时间的成果。文章的最后会有一个小Demo,简单的实现了一些基本的功能,供各位看官把玩。Demo的代码能够去个人Github中下载。git

这篇文章主要以实践的方式,实现了蓝牙交互中的两个角色。关于基本概念的介绍,官方文档中介绍的很是详细,你们能够仔细研究一下。github

Central 和 Peripheral

核心概念

CoreBluetooth 中最关键的两个角色就是 Central(中心) 和 Peripheral(周边)。数组

Central 在链接中做为主动发起者会去寻找待链接的 Peripheral。缓存

Peripheral 通常是提供服务的一方, Central 获取 Peripheral 提供的服务而后来完成特定的任务。bash

Peripheral 经过向空中广播数据的方式来使咱们能感知到它的存在。Central 经过扫描搜索来发现周围正在广播数据的 Peripheral, 找到指定的 Peripheral 后,发送链接请求进行链接,链接成功后则与 Peripheral 进行一些数据交互, Peripheral 则会经过合适的方式对 Central 进行响应。app

实现 Central 的功能

这是在开发中最多见的一个需求,须要自身做为 Central 的对象,去发现一些外设的服务,并根据其提供的数据进行相应的操做。例如,链接一个小米手环,根据其提供的步数、心率等信息调整UI的显示。框架

实现步骤:学习

1. 建立Central Manager

centralManager = CBCentralManager(delegate: self, queue: nil, options: [CBCentralManagerOptionShowPowerAlertKey : true])
复制代码
  • delegate: centralManager的代理
  • queue: 接收回调事件的线程
  • options:
    • CBCentralManagerOptionShowPowerAlertKey: 当蓝牙状态为 powered off时,系统会弹出提示框。优化

    • CBCentralManagerOptionRestoreIdentifierKey:生成一个惟一的标识符,用于后续应用恢复这个manager。

当建立了 CentralManager 以后, CBCentralManagerDelegate 会经过下面的回调告知你,当前设备是否支持你去使用蓝牙的功能。

func centralManagerDidUpdateState(_ central: CBCentralManager)
复制代码

经过 central.state 能够获取当前的状态

case unknown        //未知状态
case resetting      // 链接断开,即将重置
case unsupported    // 该设备不支持蓝牙
case unauthorized   // 蓝牙未受权
case poweredOff     // 蓝牙关闭
case poweredOn      // 蓝牙正常开启
复制代码

只有当蓝牙的状态为正常开启的状态时,才能进行后续的步骤。

2. 搜索正在广播数据的 Peripheral 设备

1. 开始搜索
self.centralManager?.scanForPeripherals(withServices: [CBUUID(string: UUID_SERVICE)], options: [CBCentralManagerOptionShowPowerAlertKey : true])
复制代码
  • serviceUUIDs: 写入这个参数,说明搜索拥有这个特定服务的 Peripheral,若传入nil,则搜索附近全部的 Peripheral。

UUID_SERVICE 一般是由Central及Peripheral本身定义的,两端使用同一个UUID。

2. 处理搜索结果

每当 CentralManager 搜索到一个 Peripheral 设备时,就会经过代理方法进行回调。若是你后面须要链接这个 Peripheral,须要定义一个 CBPeripheral 类型的对象来指向(强引用)这个对象,这样系统暂时就不会释放这个对象了。

func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber)
复制代码
  • advertisementData: Peripheral 开始广播时,放入的数据。
  • RSSI: 表明发现的 Peripheral 的信号强度。

当发现自身须要的 Peripheral 时,为了减小蓝牙对电量的消耗,能够中止CentralManager的扫描。 self.centralManager?.stopScan()

3. 链接自身须要的 Peripheral 设备

1. 开始链接
self.centralManager?.connect(peripheral, options: nil)
复制代码
2. 处理链接结果
  • 当 CentralManager 与 Peripheral 成功链接时
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral)
复制代码

当成功链接 Peripheral 以后,咱们须要去查找 Peripheral 为咱们提供的服务。为了能收到 Peripheral 查找的结果,咱们须要去遵照 Peripheral 对象的代理方法 CBPeripheralDelegate。

self.configPeripheral?.delegate = self
复制代码
  • 当 CentralManager 与 Peripheral 链接失败时
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?)
复制代码

4. 搜索链接设备的 Services

1. 查找 Service
self.configPeripheral?.discoverServices([CBUUID(string: UUID_SERVICE)])
复制代码

这里咱们能够传入一个关于 Service 的一个数组,查找本身须要的 Service。固然也能够传入 nil,这样就会查找到 Peripheral 提供的所有 Service。

可是,通常来讲,为了节省电量以及一些没必要要的时间浪费,会传入本身须要的 Service 的数组。

2. 处理搜索结果
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?)
复制代码

CBPeripheralDelegate 的 搜索 Service 回调。在这里出现错误以后,能够进行重试或者抛出中止链接流程。

若成功的话,则能够继续搜索对应 Service 的 Characteristic。

5. 搜索 Services 的 Characteristic

1.开始搜索
peripheral.discoverCharacteristics([CBUUID(string: UUID_READABLE), CBUUID(string: UUID_WRITEABLE)], for: service)
复制代码
  • characteristicUUIDs: 参数接收一个 Characteristic UUID 的数组对象。若传入,则搜索对应的 Characteristic,不然则会搜索 Service 拥有的全部 Characteristic。
2. 处理搜索结果
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?)
复制代码

CBPeripheralDelegate 的 搜索 Characteristic 的回调。若 Characteristic 有多个,则此方法会回调屡次。

3. 使用 Characteristic
  • 读取 Characteristic 的值
peripheral.readValue(for: characteristic)
复制代码

当你尝试去读取一个 Characteristic 的值时, Peripheral 会经过下面的代理回调来返回结果,你能够经过 Characteristic 的 value 属性来获得这个值。

并非全部的 Characteristic 的值都是可读的,决定一个 Characteristic 的值是否可读是经过检查 Characteristic 的 Properties 属性是否包含 CBCharacteristicPropertyRead 常量来判断的。当你尝试去读取一个值不可读的 Characteristic 时,下面的代理方法会返回一个Error供你处理。

func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?)
复制代码
  • 订阅 Characteristic 的值

当一个咱们须要读取 Characteristic 的值,频繁变化时,read 操做就会显得很繁琐。这个时候咱们就能够经过订阅的方式获取值得更新。若是订阅了某个 Characteristic 以后,每当值有变化时,也会经过 peripheral(_ peripheral:, didUpdateValueFor characteristic:, error:) 方法回调使咱们收到每次更新的值。

peripheral.setNotifyValue(true, for: characteristic)
复制代码

当咱们订阅了一个 Characteristic 后,CBPeripheralDelegate 会经过下面的回调,使咱们知道订阅的 Characteristic 的状态。

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

并非全部的 Characteristic 都提供订阅功能,决定一个 Characteristic 是否能订阅是经过检查 Characteristic 的 properties 属性是否包含 CBCharacteristicPropertyNotify 或者 CBCharacteristicPropertyIndicate 常量来判断的。

  • 写入数据到 Characteristic

决定 Characteristic 的值是否可写,须要经过查看 Characteristic 的 properties 属性是否包含 CBCharacteristicPropertyWriteWithoutResponse 或者 CBCharacteristicPropertyWrite 常量来判断的。

peripheral.writeValue(data, for: characteristic, type: .withResponse)
复制代码

type: 写入类型。

  • .withResponse:写入后,会经过下面的回调告知写入的结果。
  • .withoutResponse:无回调消息。
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?)
复制代码

实现 Peripheral 的功能

将自身声明为一个 Peripheral,可为 Central 对象提供一些服务。例如:在A手机利用链接B手机时,B手机此时就是 Peripheral 对象。

实现步骤:

1. 建立 Peripheral Manager

myPeripheral = CBPeripheralManager(delegate: self, queue: nil, options: [CBPeripheralManagerOptionShowPowerAlertKey : true])
复制代码

当建立了 Peripheral 以后, CBPeripheralManagerDelegate 会经过下面的回调告知你,当前设备是否支持你去使用蓝牙的功能。

状态同 CentralManager 同样的。

func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager)
复制代码

只有当蓝牙的状态为正常开启的状态时,才能进行后续的步骤。

2. 设置自身的 Services 和 Characteristics

let characteristic_read = CBUUID(string: UUID_READABLE)
let characteristic_write = CBUUID(string: UUID_WRITEABLE)
        
let serviceUUID = CBUUID(string: UUID_SERVICE)
        
// 为服务指定一个特征    读取特征   可被订阅
myCharacteristic_beRead = CBMutableCharacteristic(type: characteristic_read,
                                                    properties: [.read, .notify],
                                                    value: nil,
                                                    permissions: .readable)
        
myCharacteristic_beWrite = CBMutableCharacteristic(type: characteristic_write,
                                                    properties: .write,
                                                    value: nil,
                                                    permissions: .writeable)
        
        
// 建立一个服务
myService = CBMutableService(type: serviceUUID, primary: true)
复制代码

上面的代码中,咱们建立了两个 Characteristic,一个可读且可被订阅,另外一个可写。这个是经过实例化时,传入的 properties 及 permissions 的值来指定的。

这里须要注意的是 value 传入的值是 nil。由于,若是你指定了 Characteristic 的值,那么该值将被缓存而且该 Characteristic 的 properties 和 permissions 将被设置为可读的。所以,若是你须要 Characteristic 的值是可写的,或者你但愿在 Service 发布后,Characteristic 的值在 lifetime(生命周期)中依然能够更改,你必须将该 Characteristic 的值指定为 nil。经过这种方式能够确保 Characteristic 的值,在 PeripheralManager 收到来自链接的 Central 的读或者写请求的时候,可以被动态处理。

3. 添加服务特征

// 将特征加入到服务中
myService!.characteristics = ([myCharacteristic_beRead, myCharacteristic_beWrite] as! [CBCharacteristic])
        
// 将服务加入到外设中
myPeripheral?.add(myService!)
复制代码

这里咱们将本身的服务构建完成,并将服务添加至 Peripheral 中。这样,当 Peripheral 向外界发送广播时,就能够搜索这个服务获取相应的支持。

4. 广播本身的服务

// 广播本身的service
myPeripheral?.startAdvertising([CBAdvertisementDataServiceUUIDsKey: [myService!.uuid], CBAdvertisementDataLocalNameKey: "我建立了一个房间"])
复制代码

在广播时,能够同时携带一些数据。这里能够传入一个字典,可是这个字典只支持传入 CBAdvertisementDataLocalNameKey 及 CBAdvertisementDataServiceUUIDsKey。

关于广播方法官方文档的说明以下:

When in the foreground, an application can utilize up to 28 bytes of space in the initial advertisement data for any combination of the supported advertising data types. If this space is used up, there are an additional 10 bytes of space in the scan response that can be used only for the local name. Note that these sizes do not include the 2 bytes of header information that are required for each new data type. Any service UUIDs that do not fit in the allotted space will be added to a special "overflow" area, and can only be discovered by an iOS device that is explicitly scanning for them.

While an application is in the background, the local name will not be used and all service UUIDs will be placed in the "overflow" area. However, applications that have not specified the "bluetooth-peripheral" background mode will not be able to advertise anything while in the background.

大概意思是:

当处于前台时,应用程序能够在初始广告数据中利用28个字节的空间用来初始化广播数据字典,该字典包含两个支持的 key。若是此空间已用完,扫描响应时最后还会添加10个字节的空间,只能用于Local Name。

请注意,这些大小不包括每种新数据类型所需的2个字节的头部信息。任何不适合分配空间的服务UUID都将添加到特殊的“溢出”区域,而且只能由明确扫描它们的iOS设备发现。

当应用程序在后台时,将不使用本地名称,而且全部服务UUID将放置在“溢出”区域中。可是,未指定“蓝牙后台运行”背景模式的应用程序将没法在后台播听任何内容。

5. 响应 Central 的读写请求

/// 收到来自中心设备读取数据的请求
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest)
    
/// 收到来自中心设备写入数据的请求
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest])
复制代码

当 Peripheral 收到来自 Central 的读写请求时,CBPeripheralManagerDelegate 会经过上面两个方法来进行回调。读写请求以 CBATTRequest 对象来传递。

当咱们收到请求时,能够根据 CBATTRequest 请求的一些属性来判断 Central 指定要读写的 Characteristic 是否和设备服务库中的 Characteristic 是否相匹配。

if request.characteristic.uuid.isEqual(CBUUID(string: UUID_READABLE)) { 
    // do something 
    myPeripheral?.respond(to: request, withResult: CBATTError.Code.success)
} else {
    // not match
    myPeripheral?.respond(to: request, withResult: CBATTError.Code.readNotPermitted)
}
复制代码

最后使用 respond(to:, withResult:) 回应 Central 请求。

这里的 result 是一个 CBATTError.Code 类型,这里定义了不少对 Request 响应的枚举,能够根据对 Request 的响应,返回相应的 CBATTError.Code 值。

蓝牙学习小实践

利用蓝牙链接两个设备,进行一场激情的五子棋小游戏吧。

示例工程能够去个人Giuhub下载。

目前工程内完成了基本的两端交互逻辑。对于掉线重连等优化问题目前示例工程内还没有体现。

建立房间: 实现 Local Peripheral 端功能。将本身建立了房间的消息进行广播,使其它玩家能够扫描到房间进入游戏。并利用一个可被订阅的 Characteristic 将消息传输给 Central,一个可写的 Characteristic 接收来自 Central 的消息。

寻找房间: 实现 Local Central 端功能。能够扫描其它玩家建立的房间并加入游戏。经过可写的 Characteristic 将消息传输给 Peripheral,订阅一个 Characteristic 来获取值更新的通知。

相关文章
相关标签/搜索