蓝鸟 0.8.9

Bluejay 0.8.9

测试已测试
语言语言 SwiftSwift
许可 MIT
发布最后发布2021年11月
SPM支持 SPM

Jeremy ChiangNigel Brooke 维护。



Bluejay 0.8.9

  • 作者:
  • Jeremy Chiang

Bluejay

CocoaPods Compatible Carthage compatible Platform license

蓝鸟是一个用于构建可靠的蓝牙 LE 应用程序的简单 Swift 框架。

蓝鸟的主要目标是

  • 简化与单个蓝牙 LE 外围设备的通信
  • 轻松可靠地处理蓝牙操作
  • 利用 Swift 功能和约定

索引

特色功能

  • 基于回调的 API
  • 异步操作队列,提供更同步和可预测的行为
  • 后台任务模式,批量操作避免“回调金字塔”问题
  • 简单的数据序列化和反序列化协议
  • 轻松且安全地观察连接状态
  • 强大的后台恢复支持
  • 扩展的错误处理和日志支持

需求

  • 推荐使用iOS 11或更高版本
  • 推荐使用Xcode 11.3.1或更高版本
  • 推荐使用Swift 5或更高版本

安装

CocoaPods

pod 'Bluejay', '~> 0.8'

或者尝试最新的master版本

pod 'Bluejay', :git => 'https://github.com/steamclock/bluejay.git', :branch => 'master'

Carthage

github "steamclock/bluejay" ~> 0.8
github "DaveWoodCom/XCGLogger" ~> 6.1.0

有关其他说明,请参阅官方Carthage文档

注意:需要蓝鸟框架(Bluejay.framework)、Objective-C异常桥接框架(ObjcExceptionBridging.framework)和XCGLogger框架(XCGLogger.framework)。

导入

import Bluejay

示例

iOS模拟器不支持蓝牙,你可能没有可调试的蓝牙低功耗外设,因此我们为你准备了一对示例应用程序进行测试。

  1. BluejayHeartSensorDemo:可以连接到蓝牙低功耗心率传感器的应用程序。
  2. DittojayHeartSensorDemo:虚拟的蓝牙低功耗心率传感器。

尝试使用蓝雀

  1. 获取两台iOS设备——一台运行 蓝雀演示,另一台运行 迪图杰演示
  2. 授予 蓝雀演示 通知权限。
  3. 授予 迪图杰演示 后台模式权限。
  4. 使用 蓝雀演示 连接。

尝试连接后的后台恢复

  1. 蓝雀演示 中,点击 "结束心率监听"。
  • 这是为了防止我们在终止应用程序后立即触发状态恢复,因为当我们能够根据自己的闲暇和时机手动触发蓝牙事件时,验证状态恢复更加清晰和容易。
  1. 点击 "终止应用程序"。
  • 这将导致应用程序崩溃,但同时也模拟由于内存压力导致的程序终止,并且 允许 CoreBluetooth 缓存当前会话并等待蓝牙事件开始状态恢复。
  1. 迪图杰演示 中,点击 "鸣叫" 以 复活 蓝雀演示
  • 这将向终止 蓝雀演示 的设备发送蓝牙事件,并且其 CoreBluetooth 堆栈将在后台唤醒应用程序并执行一些快速任务,例如在本例中为了验证和调试目的安排几个本地通知。

用法

初始化

创建蓝雀实例

let bluejay = Bluejay()

虽然创建一个蓝雀实例并在所有地方使用它很方便,但您也可以在应用程序的特定部分创建实例,并在使用后将其销毁。然而,需要注意的是,蓝雀的每个实例都有自己的 CBCentralManager,这使得多实例方法稍微复杂一些。

一旦创建了实例,您就可以启动Bluejay了,然后它会初始化CoreBluetooth会话。请注意,创建Bluejay实例和运行Bluejay实例是两个不同的操作。

如果要在您的AppDelegate的application(_:didFinishLaunchingWithOptions:)中启动Bluejay以支持后台恢复,则必须始终这样做;否则,您可以在应用程序中的任何适当位置自由启动Bluejay。例如,不需要后台恢复的应用程序通常从初始视图控制器中初始化和启动他们的Bluejay实例。

bluejay.start()

如果您的应用程序需要Bluetooth在后台工作,那么您必须支持应用程序的后台恢复。虽然Bluejay已经为您简化了大部分后台恢复的工作,但仍需要做额外的工作,我们还建议查看相关苹果文档。后台恢复很棘手,很难做正确。

Bluejay还支持CoreBluetooth迁移,以便与其他蓝牙库或您自己的蓝牙代码一起工作。

蓝牙事件

通过ConnectionObserver协议,一个类可以监视并响应主要蓝牙和连接相关的事件。

public protocol ConnectionObserver: class {
    func bluetoothAvailable(_ available: Bool)
    func connected(to peripheral: PeripheralIdentifier)
    func disconnected(from peripheral: PeripheralIdentifier)
}

您可以使用以下方法注册一个连接观察者:

bluejay.register(connectionObserver: batteryLabel)

由于Bluejay只持有注册观察者的弱引用,因此取消注册连接观察者不是必需的,因为Bluejay会在下一个事件触发时清除nil观察者。但如果您需要在此之前这么做,可以使用下面的方法。

bluejay.unregister(connectionObserver: rssiLabel)

服务和特征

在蓝牙术语中,服务是一组属性,特征是属于该组的属性。例如,能够检测心率的BLE外围设备通常有一个名为“心率”的服务,其UUID为“180D”。在该服务中包含特性,如“身体传感器位置”,其UUID为“2A38”,以及“心率测量”,其UUID为“2A37”。

许多这些服务和特征都是由蓝牙SIG组织规定的标准,大多数硬件都采用其规范。例如,大多数BLE外围设备都实现了具有UUID“180A”的“设备信息”服务,其中可以找到固件版本、序列号和其他硬件详细信息。当然,还有很多BLE用法未被蓝牙核心规范涵盖,并且定制硬件通常有自己的独特服务和特性。

以下是您如何在Bluejay中使用服务和特性

let heartRateService = ServiceIdentifier(uuid: "180D")
let bodySensorLocation = CharacteristicIdentifier(uuid: "2A38", service: heartRateService)
let heartRate = CharacteristicIdentifier(uuid: "2A37", service: heartRateService)

Bluejay使用ServiceIdentifierCharacteristicIdentifier结构体来避免在期待特征时意外指定服务等问题。

扫描

Bluejay 提供强大的扫描 API,可以简单地使用或自定义以满足多种使用场景。

CoreBluetooth 使用服务来扫描设备。换句话说,CoreBluetooth(因此也是 Bluejay)期望你事先知道一个或多个你想要扫描的外设包含的公共服务。

基本扫描

这个简单的调用仅会在发现新设备时通知你,并在扫描完成后通知你。

bluejay.scan(
    serviceIdentifiers: [heartRateService],
    discovery: { [weak self] (discovery, discoveries) -> ScanAction in
        guard let weakSelf = self else {
            return .stop
        }

        weakSelf.discoveries = discoveries
        weakSelf.tableView.reloadData()

        return .continue
    },
    stopped: { (discoveries, error) in
        if let error = error {
            debugPrint("Scan stopped with error: \(error.localizedDescription)")
        }
        else {
            debugPrint("Scan stopped without error.")
        }
})

扫描结果 (ScanDiscovery, [ScanDiscovery]) 包含当前发现,后跟至今为止所有发现的数组。

停止结果包含停止之前可用的最后一批发现,以及(如果有)一个错误。如果没有错误,则表示扫描是故意或预期中被停止的。

扫描行动

ScanAction 在发现回调结束时返回,以告知 Bluejay 是否继续扫描或停止。

public enum ScanAction {
    case `continue`
    case blacklist
    case stop
    case connect(ScanDiscovery, (ConnectionResult) -> Void)
}

返回 blacklist 将忽略当前扫描会话中相同外设的任何未来发现。这仅在 allowDuplicates 设置为 true 时有用。有关更多信息,请参阅 Apple 的文档 CBCentralManagerScanOptionAllowDuplicatesKey

返回 connect 将使 Bluejay 停止扫描并发起你的连接请求。如果你找到所寻找的外设并希望立即连接,这将非常有用。

提示:您可以在扫描调用之外设置 ConnectionResult 块,以减少回调嵌套。

监控

使用扫描 API 的另一种有用方式是连续扫描,即监控,例如观察附近外设的 RSSI 变化以估计它们的位置。

bluejay.scan(
    duration: 15,
    allowDuplicates: true,
    serviceIdentifiers: nil,
    discovery: { [weak self] (discovery, discoveries) -> ScanAction in
        guard let weakSelf = self else {
            return .stop
        }

        weakSelf.discoveries = discoveries
        weakSelf.tableView.reloadData()

        return .continue
    },
    expired: { [weak self] (lostDiscovery, discoveries) -> ScanAction in
        guard let weakSelf = self else {
            return .stop
        }

        debugPrint("Lost discovery: \(lostDiscovery)")

        weakSelf.discoveries = discoveries
        weakSelf.tableView.reloadData()

        return .continue
}) { (discoveries, error) in
        if let error = error {
            debugPrint("Scan stopped with error: \(error.localizedDescription)")
        }
        else {
            debugPrint("Scan stopped without error.")
        }
}

allowDuplicates 设置为 true 将停止将同一外设的多个发现合并成一个发现回调。相反,每次接收到外设的广告包时,您都会收到一个发现调用。这将 消耗更多电量,并且在后台无法工作

警告:如果在扫描过程中应用处于后台运行,允许重复扫描将会因为错误而停止。

expired 回调仅在 allowDuplicates 为 true 时调用。当 Bluejay 估计先前发现的周边设备可能超出范围或不再广播时,将会调用该回调。本质上,当将 allowDuplicates 设置为 true 时,每发现一个周边设备,就会为该设备启动一个计时器开始倒计时。如果该设备处于范围内,即使它广播间隔较慢,该设备也有可能再次被扫描到,导致计时器刷新。如果没有,计时器到期且未刷新,Bluejay 便会进行推测并建议该设备不再可访问。请注意,这只是一个估计。

警告:serviceIdentifiers 设置为 nil 将导致拾取附近所有可用的蓝牙周边设备,但苹果公司不建议这样做。长时间扫描可能会引起电池和CPU问题,并且它也不支持后台运行。这不是私有API调用,而是当您在测试和原型设计时需要一个快速解决方案时可供选择的一个选项。

提示:指定至少一个特定的服务标识符是在iOS中扫描蓝牙设备最常见的方式。如果需要扫描所有蓝牙设备,建议使用 duration 参数,在5 ~ 10秒后停止扫描,以避免无限扫描并超出硬件负载。

连接

请注意,Bluejay 被设计为与单个 BLE 周边设备一起工作。目前不支持一次连接多个设备,如果 Bluejay 已经连接或仍在连接,则连接请求将失败。尽管这可能对一些复杂的应用程序来说是一个限制,但它更常被视为一种保护措施,以确保您的应用程序不会发起不必要的或错误连接。

bluejay.connect(selectedSensor, timeout: .seconds(15)) { result in
    switch result {
    case .success:
        debugPrint("Connection attempt to: \(selectedSensor.description) is successful")
    case .failure(let error):
        debugPrint("Failed to connect with error: \(error.localizedDescription)")
    }
}

超时

您还可以指定连接请求的超时时间,默认为无超时。

public enum Timeout {
    case seconds(TimeInterval)
    case none
}

提示:我们建议您始终为连接请求设置至少15秒的超时时间。

断开连接

要断开连接

bluejay.disconnect()

Bluejay 还支持对断开连接有更精细的控制。

计划断开连接

计划断开连接将与所有其他Bluejay API请求一样进行排队,因此断开连接尝试将等待其轮到,直到所有排队任务完成。

要执行计划断开连接,只需调用

bluejay.disconnect()

立即断开连接

立即断开连接将立即失败并清空队列中所有任务(即使它们仍在运行),然后立即断开。

有两种方法可以执行立即断开连接:

bluejay.disconnect(immediate: true)
bluejay.cancelEverything()

预期与意外断开连接

Bluejay的日志将详细描述断开连接是否预期,这在调试与断开连接相关的问题以及解释为什么Bluejay尝试或没有尝试自动重连很重要。

disconnect或带有断开连接的cancelEverything的任何显式调用都会导致预期断开连接。

所有其他断开连接事件都将被视为意外。例如:

  • 如果连接尝试由于硬件错误失败(不是超时):
  • 如果连接的设备移出范围:
  • 如果连接的设备耗尽电量或被关掉:
  • 如果连接的设备的蓝牙模块崩溃且不再可协商:

取消所有任务

除了disconnect以外还存在cancelEverything API,是因为有时我们想取消队列中的所有任务,但保留连接。

bluejay.cancelEverything(shouldDisconnect: false)

自动重连

默认情况下,shouldAutoReconnect设置为true,Bluejay将在意外断开连接后始终尝试自动重连。

在以下情况下,Bluejay只会将shouldAutoReconnect设置为false

  1. 如果您手动调用disconnect并且断开连接成功。
  2. 如果您手动调用cancelEverything并且其断开连接成功。

当Bluejay成功连接到外围设备时,它也会默认将shouldAutoReconnect重置为true,因为我们通常希望尽快重新连接到同一设备,如果在正常使用过程中意外失去连接。

但是,在某些情况下,自动重连不是期望的行为。在这些情况下,请使用DisconnectHandler来评估和覆盖自动重连。

断开连接处理程序

断开连接处理程序是一个单一的委托,适用于执行主要恢复、重试或重置操作,例如在断开连接时重新启动扫描。

此处理程序的目的在于帮助您避免在常规连接、断开连接、读取、写入和监听调用的错误回调中编写和重复主要复苏和错误处理逻辑。请使用断开连接处理程序在断开连接的非常结束时执行一次性和重大操作。

除了帮助您在断开连接时避免在各个回调中出现冗余和冲突的逻辑外,断开连接处理程序还可以允许您评估和控制Bluejay的自动重连行为。

例如,此协议实现将在任何断开连接时始终关闭自动重连,无论是预期的还是意外的情况。

func didDisconnect(
  from peripheral: PeripheralIdentifier,
  with error: Error?,
  willReconnect autoReconnect: Bool) -> AutoReconnectMode {
    return .change(shouldAutoReconnect: false)
}

我们还预计,对于大多数应用程序,不同的视图控制器可能希望以不同的方式处理断开连接,因此,请在用户导航到应用程序的不同部分时简单地注册并替换现有的断开连接处理程序。

bluejay.registerDisconnectHandler(handler: self)

与连接观察者类似,除非您需要,否则您不需要显式注销。

连接状态

您的Bluejay实例具有这些属性,可以帮助您做出有关连接的决定

  • isBluetoothAvailable
  • isBluetoothStateUpdateImminent
  • isConnecting
  • isConnected
  • isDisconnecting
  • shouldAutoReconnect
  • isScanning
  • hasStarted
  • defaultWarningOptions
  • isBackgroundRestorationEnabled

反序列化和序列化

在Bluejay中,读取、写入和监听特性非常简单。大多数相关工作集中在构建您数据的反序列化和序列化。让我们快速看看Bluejay是如何通过ReceivableSendable协议在您的应用中标准化这一过程的。

Receivable

代表您希望从外围设备读取和接收的数据的模型应该都符合Receivable协议。

以下是对心率测量特性的示例的部分:

import Bluejay
import Foundation

struct HeartRateMeasurement: Receivable {

    private var flags: UInt8 = 0
    private var measurement8bits: UInt8 = 0
    private var measurement16bits: UInt16 = 0
    private var energyExpended: UInt16 = 0
    private var rrInterval: UInt16 = 0

    private var isMeasurementIn8bits = true

    var measurement: Int {
        return isMeasurementIn8bits ? Int(measurement8bits) : Int(measurement16bits)
    }

    init(bluetoothData: Data) throws {
        flags = try bluetoothData.extract(start: 0, length: 1)

        isMeasurementIn8bits = (flags & 0b00000001) == 0b00000000

        if isMeasurementIn8bits {
            measurement8bits = try bluetoothData.extract(start: 1, length: 1)
        } else {
            measurement16bits = try bluetoothData.extract(start: 1, length: 2)
        }
    }

}

注意,您可以使用Bluejay为Data添加的extract函数轻松解析所需字节。我们计划在将来为此提供更多保护和错误处理。

最后,虽然这不是必要的,而且这取决于上下文,但我们建议只公开您模型所需的和计算出的属性。

Sendable

代表您希望发送到外围设备的数据的模型应该都符合Sendable协议。简而言之,这有助于Bluejay确定如何将您的模型转换为Data

import Foundation
import Bluejay

struct Coffee: Sendable {

    let data: UInt8

    init(coffee: CoffeeEnum) {
        data = UInt8(coffee.rawValue)
    }

    func toBluetoothData() -> Data {
        return Bluejay.combine(sendables: [data])
    }

}

辅助函数combine使得将数据分组和排序变得更加容易。

发送和接收原语

在某些情况下,您可能希望发送或接收的数据足够简单,以至于创建实现了SendableReceivable的自定义结构体来保存它是不必要的复杂。对于这些情况,Bluejay还向后使几个内置Swift类型符合SendableReceivable协议。Int8Int16Int32Int64UInt8UInt16UInt32UInt64Data都符合这两个协议,因此它们可以直接发送或接收。

有意不使IntUInt符合。蓝牙值总是以特定的位宽发送和/或接收。《Int》预期位宽是不明确的,这通常表明程序员错误,因为在特性中未考虑蓝牙设备期望的位宽。

交互

一旦您使用ReceivableSendable协议建模了数据模型,Bluejay中的读取、写入和监听API应该会无缝地为您处理反序列化和序列化。您所需做的所有事情只是指定泛型包装器类型:ReadResult<T>WriteResult<T>

读取

以下是一个示例,展示如何读取传感器身体位置特性,将其值转换为相应的字符串并在UI中显示。

let heartRateService = ServiceIdentifier(uuid: "180D")
let sensorLocation = CharacteristicIdentifier(uuid: "2A38", service: heartRateService)

bluejay.read(from: sensorLocation) { [weak self] (result: ReadResult<UInt8>) in
    guard let weakSelf = self else {
	     return
    }

    switch result {
    case .success(let location):
        debugPrint("Read from sensor location is successful: \(location)")

        var locationString = "Unknown"

        switch location {
        case 0:
            locationString = "Other"
        case 1:
            locationString = "Chest"
        case 2:
            locationString = "Wrist"
        case 3:
            locationString = "Finger"
        case 4:
            locationString = "Hand"
        case 5:
            locationString = "Ear Lobe"
        case 6:
            locationString = "Foot"
        default:
            locationString = "Unknown"
        }

        weakSelf.sensorLocationCell.detailTextLabel?.text = locationString
    case .failure(let error):
        debugPrint("Failed to read sensor location with error: \(error.localizedDescription)")
    }
}

写入

写入特性与读取非常类似

let heartRateService = ServiceIdentifier(uuid: "180D")
let sensorLocation = CharacteristicIdentifier(uuid: "2A38", service: heartRateService)

bluejay.write(to: sensorLocation, value: UInt8(2)) { result in
    switch result {
    case .success:
        debugPrint("Write to sensor location is successful.")
    case .failure(let error):
        debugPrint("Failed to write sensor location with error: \(error.localizedDescription)")
    }
}

监听

监听在特性上打开广播并允许您接收其通知。

与读取和写入不同,完成块的调用只进行一次,监听回调是持久的。在调用接收块之前可能需要几分钟(或永远都不会),并且该块可能被调用多次。

一些蓝牙设备在断开连接时会关闭通知,而另一些不会。话虽如此,当您不需要再监听时,通常最佳做法是始终显式地使用endListen函数关闭该特性的广播。

并非所有特性都支持监听,这是必须由蓝牙设备本身启用的特性。

let heartRateService = ServiceIdentifier(uuid: "180D")
let heartRateCharacteristic = CharacteristicIdentifier(uuid: "2A37", service: heartRateService)

bluejay.listen(to: heartRateCharacteristic, multipleListenOption: .replaceable)
{ [weak self] (result: ReadResult<HeartRateMeasurement>) in
        guard let weakSelf = self else {
            return
        }

        switch result {
        case .success(let heartRate):
            weakSelf.heartRate = heartRate
            weakSelf.tableView.reloadData()
        case .failure(let error):
            debugPrint("Failed to listen with error: \(error.localizedDescription)")
        }
}

多重监听选项

每个特征值只能安装一个监听回调。如果需要在同一特征值上有多个观察者,您仍然可以使用一个Bluejay监听器并在此监听器内创建自己的应用程序特定通知来做到这一点。

在您的监听调用中传入适当的MultipleListenOption,既可以防止在同一特征值上进行多次监听尝试,也可以有意覆盖现有的监听。

/// Ways to handle calling listen on the same characteristic multiple times.
public enum MultipleListenOption: Int {
    /// New listen on the same characteristic will not overwrite an existing listen.
    case trap
    /// New listens on the same characteristic will replace the existing listen.
    case replaceable
}

后台任务

Bluejay还支持在后台线程中执行一系列较长的读取、写入和监听操作。后台任务中的每个操作都是阻塞的,将在完成之前不会返回。

当您需要完成特定的大任务,例如同步或升级到新固件时,这非常有用。这也适用于与基于通知的蓝牙模块一起工作时,您需要暂停和等待蓝牙执行,主要是监听操作,但又不想阻塞主线程。

当所有操作完成且无错误时,或在后台任务中的任何一个操作失败时,Bluejay将在主线程上调用您的完成块。

以下是一个虚构的例子,尝试通过相同的密码同时获取用户和管理员对蓝牙设备的访问权限

var isUserAuthenticated = false
var isAdminAuthenticated = false

bluejay.run(backgroundTask: { (peripheral) -> (Bool, Bool) in
    // 1. No need to perform any Bluetooth tasks if there's no password to try.
    guard let password = enteredPassword else {
      return (false, false)
    }

    // 2. Flush auth characteristics in case they are still broadcasting unwanted data.
    try peripheral.flushListen(to: userAuth, nonZeroTimeout: .seconds(3), completion: {
        debugPrint("Flushed buffered data on the user auth characteristic.")
    })

    try peripheral.flushListen(to: adminAuth, nonZeroTimeout: .seconds(3), completion: {
        debugPrint("Flushed buffered data on the admin auth characteristic.")
    })

    // 3. Sanity checks, making sure the characteristics are not broadcasting anymore.
    try peripheral.endListen(to: userAuth)
    try peripheral.endListen(to: adminAuth)

    // 4. Attempt authentication.
    if let passwordData = password.data(using: .utf8) {
        debugPrint("Begin authentication...")

        try peripheral.writeAndListen(
            writeTo: userAuth,
            value: passwordData,
            listenTo: userAuth,
            timeoutInSeconds: .seconds(15),
            completion: { (response: UInt8) -> ListenAction in
                if let responseCode = AuthResponse(rawValue: response) {
                    isUserAuthenticated = responseCode == .success
                }

                return .done
        })

        try peripheral.writeAndListen(
            writeTo: adminAuth,
            value: passwordData,
            listenTo: adminAuth,
            timeoutInSeconds: .seconds(15),
            completion: { (response: UInt8) -> ListenAction in
                if let responseCode = AuthResponse(rawValue: response) {
                    isAdminAuthenticated = responseCode == .success
                }

                return .done
        })
    }

    // 5. Return results of authentication.
    return (isUserAuthenticated, isAdminAuthenticated)
}, completionOnMainThread: { (result) in
    switch result {
    case .success(let authResults):
        debugPrint("Is user authenticated: \(authResults.0)")
        debugPrint("Is admin authenticated: \(authResults.1)")
    case .failure(let error):
        debugPrint("Background task failed with error: \(error.localizedDescription)")
    }
})

重要

虽然Bluejay不能崩溃,因为它具有内置的错误处理功能,会通知您以下违规情况,但这些规则仍然值得提出

  1. 不要backgroundTask块内部调用任何常规的read/write/listen函数。请使用您提供的SynchronizedPeripheral及其read/write/listen API。
  2. 当后台任务仍在运行时,backgroundTask块之外常规的read/write/listen调用也将不会工作。

请注意,由于backgroundTask块在后台线程上运行,您需要小心访问该块中任何的全局或捕获的数据,以考虑线程安全,就像处理任何GCD或OperationQueue任务一样。为帮助解决这个问题,请使用run(userData:backgroundTask:completionOnMainThread:)传递您希望在后台任务中具有线程安全访问权限的对象。

后台恢复

CoreBluetooth 允许在应用处于后台或被内存排除时继续处理活跃的蓝牙操作。在 Bluejay 中,我们称此功能和行为为“后台恢复”。例如,完成的挂起的连接请求或触发的订阅特征可以导致系统唤醒或重新启动应用。这可以在不需要用户启动应用的情况下,同步设备数据。

为了支持后台蓝牙,需要进行以下两步操作:

  1. 授予应用在后台使用蓝牙的权限:
  2. 实现和处理状态恢复:

后台权限:

这是简单的一步。只需在 Xcode 项目中启用 Background Modes 功能,并确保启用 Uses Bluetooth LE accessories 即可。

状态恢复:

Bluejay 已经为您处理了大部分复杂的状态恢复实现。但是,您仍然需要做一些事情来帮助 Bluejay 帮助您。

  1. 创建具有恢复标识符的后台恢复配置:
  2. 始终在 AppDelegate 的 application(_:didFinishLaunchingWithOptions:) 中启动您的 Bluejay 实例:
  3. 始终传递 launchOptions 给 Bluejay:
  4. 设置一个 BackgroundRestorer 和一个 ListenRestorer 来处理恢复结果:
import Bluejay
import UIKit

let bluejay = Bluejay()

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        let backgroundRestoreConfig = BackgroundRestoreConfig(
            restoreIdentifier: "com.steamclock.bluejayHeartSensorDemo",
            backgroundRestorer: self,
            listenRestorer: self,
            launchOptions: launchOptions)

        let backgroundRestoreMode = BackgroundRestoreMode.enable(backgroundRestoreConfig)

        let options = StartOptions(
          enableBluetoothAlert: true,
          backgroundRestore: backgroundRestoreMode)

        bluejay.start(mode: .new(options))

        return true
    }

}

extension AppDelegate: BackgroundRestorer {
    func didRestoreConnection(
      to peripheral: PeripheralIdentifier) -> BackgroundRestoreCompletion {
        // Opportunity to perform syncing related logic here.
        return .continue
    }

    func didFailToRestoreConnection(
      to peripheral: PeripheralIdentifier, error: Error) -> BackgroundRestoreCompletion {
        // Opportunity to perform cleanup or error handling logic here.
        return .continue
    }
}

extension AppDelegate: ListenRestorer {
    func didReceiveUnhandledListen(
      from peripheral: PeripheralIdentifier,
      on characteristic: CharacteristicIdentifier,
      with value: Data?) -> ListenRestoreAction {
        // Re-install or defer installing a callback to a notifying characteristic.
        return .promiseRestoration
    }
}

尽管 Bluejay 简化了后台恢复到只有几条初始化规则和两个协议,但仍然可能难以正确实现。如果您有任何问题,请与我们联系。

监听恢复:

如果您的应用被移除内存,将丢失所有监听回调。然而,蓝牙设备仍然可以在其正在监听的特征上广播。监听恢复在您的应用在后台恢复时,为您提供了恢复和响应此通知的机会。

如果您需要重新安装监听,只需在 didReceiveUnhandledListen(from:on:with:) 返回 .promiseRestoration 之前,像通常那样在设置新的监听时调用 listen。否则,返回 .stopListen 请求 Bluejay 关闭该特征的通知。

/**
 * Available actions to take on an unhandled listen event from background restoration.
 */
public enum ListenRestoreAction {
    /// Bluejay will continue to receive but do nothing with the incoming listen events until a new listener is installed.
    case promiseRestoration
    /// Bluejay will attempt to turn off notifications on the peripheral.
    case stopListen
}
extension AppDelegate: ListenRestorer {
    func didReceiveUnhandledListen(
      from peripheral: PeripheralIdentifier,
      on characteristic: CharacteristicIdentifier,
      with value: Data?) -> ListenRestoreAction {
        // Re-install or defer installing a callback to a notifying characteristic.
        return .promiseRestoration
    }
}

高级用法

以下部分将演示Bluejay的几个高级用法。

写入和组装

我们使用的一个蓝牙模块在数据小于软件或硬件的最大包大小时,并不总是发送整个数据包。为了处理可能被分成未知数量数据包的接收数据,我们添加了与《SynchronizedPeripheral》上的《writeAndListen》类似的《writeAndAssemble》函数。因此,至少目前,这仅在使用《背景任务》时支持。

使用《writeAndAssemble》时,我们仍然期望你知道您正在接收的数据的总大小,但Bluejay会持续监听和接收数据包,直到达到期望的大小后,才会尝试将数据反序列化为所需的对象。

如果出现悬挂或异常长时间等待的情况,您还可以指定超时。

以下是一个示例,说明如何向蓝牙模块写入一个请求值,使其可以通过特性中的通知返回我们想要的值。当然,我们无法确定并且无法控制模块会发送多少个数据包。

try peripheral.writeAndAssemble(
    writeTo: Characteristics.rigadoTX,
    value: ReadRequest(handle: Registers.system.firmwareVersion),
    listenTo: Characteristics.rigadoRX,
    expectedLength: FirmwareVersion.length,
    completion: { (firmwareVersion: FirmwareVersion) -> ListenAction in
        settings.firmware = firmwareVersion.string
        return .done
})

刷新监听

某些蓝牙模块在失去与您的应用程序的连接时,会暂停发送数据,然后在连接重新建立时从上次中断的地方重新发送相同的数据集。这通常不是问题,但对于将多个用途和值超载一个特性的蓝牙模块来说,就会带来问题。

例如,当应用程序重新打开时,您可能需要重新验证用户身份。但如果身份验证需要在仍发送来自之前请求的不完整数据集的同特性上监听,那么您将得到意外的值,并且在尝试反序列化与身份验证相关的对象时很可能会崩溃。

为了处理这个问题,在开始关键操作之前刷新可通知的特性通常是一个好主意。这仅在《背景任务》内的《SynchronizedPeripheral》上可用。

try peripheral.flushListen(to: auth, nonZeroTimeout: .seconds(3), completion: {
    debugPrint("Flushed buffered data on the auth characteristic.")
})

nonZeroTimeout 指定了预测清空操作最可能完成所需的不接收数据的时间长度。在上面的示例中,并不是说清空操作在3秒后会硬性停止,而是只有在Bluejay在等待3秒后仍没有数据可清空时才会停止。只要有数据传入,它将继续清空动作。

CoreBluetooth 迁移

如果您想要在一个现有的CoreBluetooth堆栈上启动Bluejay,可以在调用start函数时指定.use来代替.new

bluejay.start(mode: .use(manager: anotherManager, peripheral: alreadyConnectedPeripheral))

您还可以使用这个函数将Bluejay的CoreBluetooth堆栈传输到另一个蓝牙库或您自己的程序中。

public func stopAndExtractBluetoothState() ->
    (manager: CBCentralManager, peripheral: CBPeripheral?)

最后,您可以使用hasStarted属性来检查Bluejay是否已启动或停止。

监控外围设备服务

一些外围设备在运行时可以添加或移除服务,Bluejay提供了一个基本的方式来响应这种情况。请参考项目中的BluejayHeartSensorDemoDittojayHeartSensorDemo获取更多示例。

bluejay.register(serviceObserver: self)
func didModifyServices(
  from peripheral: PeripheralIdentifier,
  invalidatedServices: [ServiceIdentifier]) {
    if invalidatedServices.contains(where: { invalidatedServiceIdentifier -> Bool in
        invalidatedServiceIdentifier == chirpCharacteristic.service
    }) {
        endListen(to: chirpCharacteristic)
    } else if invalidatedServices.isEmpty {
        listen(to: chirpCharacteristic)
    }
}

来自苹果的注意事项

如果您之前发现过已更改的任何服务,这些服务现在在invalidatedServices参数中提供,并无法再使用。您可以使用discoverServices:方法来发现外围数据库中添加的任何新服务,或者查找您曾经使用(并希望继续使用)的任何已失效的服务是否已被添加到外围数据库中的其他位置。

API 文档

我们使用内联文档和Jazzy有更详细的Bluejay API 文档