LittleBluetooth 是一个库,它可以帮助您开发需要与低功耗蓝牙设备配合使用的应用程序。 它使用 Swift
和 Combine
框架编写,因此仅与 iOS 13、macOS 10.15、watchOS 6.0 及更高版本兼容。 它将使 CoreBlueTooth 的使用变得非常容易:连接到外围设备并读取特征码只需几行代码即可完成
StartLittleBlueTooth
.startDiscovery(for: self.littleBT, withServices: [CBUUID(string: HRMCostants.HRMService)])
.prefix(1)
.connect(for: self.littleBT)
.read(for: self.littleBT, from: hrmSensorChar)
.sink(receiveCompletion: { (result) in
print("Result: \(result)")
switch result {
case .finished:
break
case .failure(let error):
print("Error while changing sensor position: \(error)")
break
}
}) { (value: HeartRateSensorPositionResponse) in // Specify the concrete type
print("Value: \(value)")
}
.store(in: &disposeBag)
LittleBluetooth 的一个实例只能控制一个外围设备,您可以根据需要使用多个实例来控制多个外围设备,但请先阅读 Apple 论坛上的这个答案,以了解拥有多个 CBCentralManager
实例的影响。
该库仍在开发中,因此您需要自行承担使用风险。
[!注意]
虽然 1.0.0 版本可以在 swift 6 上成功编译,即使启用了完全并发检查,但这并不意味着它是线程安全的。 CoreBluetooth 尚未实现线程安全,并且很难使其完全兼容。 这就是为什么暴露的类被标记为 @unchecked Sendable
。 对于之前的 swift 版本,可以解析版本 0.8.0
。
Data
对象读取到您想要的具体类型,或者将您的具体类型写入 Data
对象。Error
规范化,如果您需要更多信息,您可以随时访问内部 CBError
将以下内容添加到您的 Cartfile
github "DrAma999/LittleBlueTooth" ~> 1.0.0
由于该框架支持大多数 Apple 设备,因此您可能希望通过在 carthage update
命令后添加选项 --platform
来为特定平台构建。 例如
carthage update --platform iOS`
此步骤是超级可选的: 该库与 Nordic 库 Core Bluetooth Mock 有一个子依赖关系,它帮助我创建单元测试,如果您想启动单元测试,您必须将其添加到您的 Cartfile 中并使用 LittleBlueToothForTest
产品而不是 LittleBlueTooth
,请注意,此目标仅用于使用模拟运行测试。
将以下依赖项添加到您的 Package.swift 文件
.package(url: "https://github.com/DrAma999/LittleBlueTooth.git", from: "0.7.1")
或者,只需从 XCode 菜单 Swift packages 添加 URL。
预编译的 XCFramework 可在 github 的发布部分下载。
创建一个 LittleBluetoothConfiguration
对象并将其传递给 LittleBlueTooth
的 init 方法。 所有 LittleBluetoothConfiguration
属性都是可选的。
var littleBTConf = LittleBluetoothConfiguration()
littleBT = LittleBlueTooth(with: littleBTConf)
您可以扫描带或不带超时,超时后您会收到 .scanTimeout
错误。 您可以为每种操作设置超时时间,例如扫描
anycanc = littleBT.startDiscovery(withServices: [littleChar.service])
.timeout(DispatchQueue.SchedulerTimeType.Stride(timeout.dispatchInterval), scheduler: DispatchQueue.main, options: nil, error: .scanTimeout)
请注意,每个找到的外围设备都会发布到订阅者链,直到您停止扫描请求或连接到设备(当您连接时,扫描会自动暂停)。
扫描并停止:
// Remember that the AnyCancellable resulting from the `sink` must have a strong reference
// Also pay attention to eventual retain cycles
anycanc = littleBT.startDiscovery(withServices: [littleChar.service])
.filter { (discovery) -> Bool in
print("discovery \(discovery)")
if let name = discovery.advertisement.localName, name == "PunchLX" {
return true
}
return false
}
.flatMap{ (discovery) -> AnyPublisher<PeripheralDiscovery, LittleBluetoothError> in
print("Discovery: \(discovery)")
return self.littleBT.stopDiscovery()map {discovery}.eraseToAnyPublisher()
}
.sink(receiveCompletion: { result in
print("Result: \(result)")
switch result {
case .finished:
break
case .failure(let error):
// Handle errors
print("Error: \(error)")
}
}, receiveValue: { (periph) in
print("Discovered Peripheral \(periph)")
})
扫描并连接:
一旦您启动连接命令,扫描过程将自动停止。
// Remember that the AnyCancellable resulting from the `sink` must have a strong reference
// Also pay attention to eventual retain cycles
anycanc = littleBT.startDiscovery(withServices: [littleChar.service])
.filter { (discovery) -> Bool in
print("discovery \(discovery)")
if let name = discovery.advertisement.localName, name == "PunchLX" {
return true
}
return false
}
.flatMap { (discovery)-> AnyPublisher<Peripheral, LittleBluetoothError> in
self.littleBT.connect(to: discovery)
}
.sink(receiveCompletion: { result in
print("Result: \(result)")
switch result {
case .finished:
break
case .failure(let error):
// Handle errors
}
}, receiveValue: { (periph) in
print("Connected Peripheral \(periph)")
})
扫描并缓冲外围设备:
// Remember that the AnyCancellable resulting from the `sink` must have a strong reference
// Also pay attention to eventual retain cycles
anycanc = littleBT.startDiscovery(withServices: [littleChar.service])
.collect(10)
.map{ (discoveries) -> AnyPublisher<[PeripheralDiscovery], LittleBluetoothError> in
print("Discoveries: \(discoveries)")
return self.littleBT.stopDiscovery().map {discoveries}.eraseToAnyPublisher()
}
.sink(receiveCompletion: { result in
print("Result: \(result)")
switch result {
case .finished:
break
case .failure(let error):
// Handle errors
print("Error: \(error)")
}
}, receiveValue: { (peripherals) in
print("Discovered Peripherals \(peripherals)")
})
从发现连接:
PeripheralDiscovery
是您通常从扫描中获得的表示,它具有外围设备的 UUID
和广播信息。
// Taken a discovery from scan
anycanc = self.littleBT.connect(to: discovery)
.sink(receiveCompletion: { result in
print("Result: \(result)")
switch result {
case .finished:
break
case .failure(let error):
// Handle errors
}
}, receiveValue: { (periph) in
print("Connected Peripheral \(periph)")
})
直接从外围设备标识符连接:
PeripheralIdentifier
是 CBPeripheral
标识符的包装器,这使您可以仅通过知道外围设备的 UUID
来连接到外围设备。
anycanc = self.littleBT.connect(to: peripheralIs)
.sink(receiveCompletion: { result in
print("Result: \(result)")
switch result {
case .finished:
break
case .failure(let error):
// Handle errors
}
}, receiveValue: { (periph) in
print("Connected Peripheral \(periph)")
})
从特征码读取:
要从特征码读取,首先您必须创建 LittleBluetoothCharacteristic
的一个实例并定义您要读取的数据。
let littleChar = LittleBlueToothCharacteristic(characteristic: "19B10011-E8F2-537E-4F6C-D104768A1214", for: "19B10010-E8F2-537E-4F6C-D104768A1214")
您要读取的类或结构必须符合 Readable
协议,基本上这意味着它可以从 Data
对象实例化。
例如,这里我声明了一个 Acceleration
结构,其中包含来自传感器的加速度数据。
struct Acceleration: Readable {
let measureAx: Float
let measureAy: Float
let measureAz: Float
let measureGx: Float
let measureGy: Float
let measureGz: Float
let timestamp: TimeInterval
init(from bluetoothData: Data) throws {
let timeInt: UInt32 = try bluetoothData.extract(start: 0, length: 4)
timestamp = TimeInterval(exactly: timeInt.littleEndian)! / 1000.0
var measureInt: Int16 = try bluetoothData.extract(start: 4, length: 2)
measureAx = Float(measureInt.littleEndian) / 100.0
measureInt = try bluetoothData.extract(start: 6, length: 2)
measureAy = Float(measureInt.littleEndian) / 100.0
measureInt = try bluetoothData.extract(start: 8, length: 2)
measureAz = Float(measureInt.littleEndian) / 100.0
var measureGyroInt: Int32 = try bluetoothData.extract(start: 10, length: 4)
measureGx = Float(measureGyroInt.littleEndian) / 100.0
measureGyroInt = try bluetoothData.extract(start: 14, length: 4)
measureGy = Float(measureGyroInt.littleEndian) / 100.0
measureGyroInt = try bluetoothData.extract(start: 18, length: 4)
measureGz = Float(measureGyroInt.littleEndian) / 100.0
}
}
之后,只需调用 read 方法即可。
anycanc = littleBT.startDiscovery(withServices: [littleChar.service])
.filter { (discovery) -> Bool in
if let name = discovery.advertisement.localName, name == "PunchLX" {
return true
}
return false
}
.flatMap { (discovery)-> AnyPublisher<Peripheral, LittleBluetoothError> in
self.littleBT.connect(to: discovery)
}
.flatMap{_ -> AnyPublisher<LedState, LittleBluetoothError> in
self.littleBT.read(from: self.littleChar)
}
.sink(receiveCompletion: { result in
print("Result: \(result)")
switch result {
case .finished:
break
case .failure(let error):
print("Error: \(error)")
// Handle error
}
}, receiveValue: { (acc) in
print("Read \(acc)")
})
写入特征码:
要写入特征码,首先您必须创建 LittleBluetoothCharacteristic
的一个实例并定义您要读取的数据。
let littleChar = LittleBlueToothCharacteristic(characteristic: "19B10011-E8F2-537E-4F6C-D104768A1214", for: "19B10010-E8F2-537E-4F6C-D104768A1214")
您要写入的类或结构必须符合 Writable
协议,基本上这意味着它可以转换为 Data
对象。
例如,这里我声明了一个简单的对象,它可以打开和关闭 LED。
struct LedState: Writable {
let isOn: Bool
var data: Data {
return isOn ? Data([0x01]) : Data([0x00])
}
}
之后,只需调用 write 发布者即可。
littleBT.write(to: charateristic, value: ledState)
WriteAndListen:
有时您需要将命令写入“控制点”并读取来自 BT 设备的后续回复。 这意味着将自己作为侦听器附加到特征码,写入命令并等待回复。 通过使用“写入和监听”,此过程变得非常简单。
anycanc = littleBT.startDiscovery(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey : false])
.flatMap { discovery in
self.littleBT.connect(to: discovery)
}
.flatMap { _ in
self.littleBT.writeAndListen(from: littleCharateristic, value: ledState)
}
.sink(receiveCompletion: { completion in
print("Result: \(result)")
switch result {
case .finished:
break
case .failure(let error):
print("Error: \(error)")
// Handle error
}
}) { (answer: LedState) in
print("Answer \(answer)")
}
您可以通过不同的方式监听特征码。
监听:
创建您的 LittleCharacteristic
实例后,发送 startListen(from:)
并附加订阅者。 当然,您要读取的对象必须符合 Readable
对象。
anycanc = littleBT.startDiscovery(withServices: [littleChar.service])
.filter { (discovery) -> Bool in
print("discovery \(discovery)")
if let name = discovery.advertisement.localName, name == "PunchLX" {
return true
}
return false
}
.flatMap { (discovery)-> AnyPublisher<Peripheral, LittleBluetoothError> in
self.littleBT.connect(to: discovery)
}
.flatMap{_ -> AnyPublisher<LedState, LittleBluetoothError>in
self.littleBT.startListen(from: self.littleChar)
}
.sink(receiveCompletion: { result in
print("Result: \(result)")
}, receiveValue: { (acc) in
print("Read \(acc)")
})
注意:如果您停止监听特征码,那么您拥有更多订阅者也没关系。 监听过程将停止。 您有责任提供业务逻辑来避免此行为。
可连接监听:
创建您的 LittleCharacteristic
实例后,发送 connectableListenPublisher(for: valueType:)
。 当然,您要读取的对象必须符合 Readable
对象。 当您想要创建更多订阅者并在以后附加它们时,这很有用。 当您准备好时,只需调用 connect()
方法,通知将开始流式传输。
let connectable = littleBT.connectableListenPublisher(for: charateristic, valueType: ButtonState.self)
// First subscriber
connectable
.sink(receiveCompletion: { completion in
print("Completion \(completion)")
}) { (answer) in
print("Sub1 \(answer)")
}
.store(in: &disposeBag)
// Second subscriber
connectable
.sink(receiveCompletion: { completion in
print("Completion \(completion)")
}) { (answer) in
print("Sub2: \(answer)")
}
.store(in: &disposeBag)
littleBT.startDiscovery(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey : false])
.map { disc -> PeripheralDiscovery in
print("Discovery discovery \(disc)")
return disc
}
.flatMap { discovery in
self.littleBT.connect(to: discovery)
}
.map { _ -> Void in
self.cancellable = connectable.connect()
return ()
}
.sink(receiveCompletion: { completion in
print("Completion \(completion)")
}) { (answer) in
print("Answer \(answer)")
}
.store(in: &disposeBag)
多重监听:
如果您需要在单个订阅者上接收更多通知,则此发布者是为您量身定制的。 只需激活一个或多个通知并订阅 listenPublisher
发布者。 一旦外围设备自动连接,它就开始流式传输所有通知。 现在,您有责任过滤和转换来自 CBCharacteristic
的 Data
对象到您的类型。
// First publisher
littleBT.listenPublisher
.filter { charact -> Bool in
charact.id == charateristicOne.id
}
.tryMap { (characteristic) -> ButtonState in
try characteristic.value()
}
.mapError { (error) -> LittleBluetoothError in
if let er = error as? LittleBluetoothError {
return er
}
return .emptyData
}
.sink(receiveCompletion: { completion in
print("Completion \(completion)")
}) { (answer) in
print("Sub1: \(answer)")
}
.store(in: &self.disposeBag)
// Second publisher
littleBT.listenPublisher
.filter { charact -> Bool in
charact.id == charateristicOne.id
}
.tryMap { (characteristic) -> LedState in
try characteristic.value()
}.mapError { (error) -> LittleBluetoothError in
if let er = error as? LittleBluetoothError {
return er
}
return .emptyData
}
.sink(receiveCompletion: { completion in
print("Completion \(completion)")
}) { (answer) in
print("Sub2: \(answer)")
}
.store(in: &self.disposeBag)
littleBT.startDiscovery(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey : false])
.map { disc -> PeripheralDiscovery in
print("Discovery discovery \(disc)")
return disc
}
.flatMap { discovery in
self.littleBT.connect(to: discovery)
}
.flatMap { periph in
self.littleBT.enableListen(from: charateristicOne)
}
.flatMap { periph in
self.littleBT.enableListen(from: charateristicTwo)
}
.sink(receiveCompletion: { completion in
print("Completion \(completion)")
}) { (answer) in
}
.store(in: &disposeBag)
断开连接可以是显式的或意外的。 当您调用该方法时,断开连接是显式的
self.littleBT.disconnect()
意外断开连接可能是由于不同的原因造成的:设备重置、设备超出范围等
无论它是意外的还是显式的,LittleBlueTooth
都会在注册断开连接后清理所有内容。
连接事件观察器:
connectionEventPublisher
会告知您连接到设备时发生的情况。 连接事件由不同的状态定义
.connected(CBPeripheral)
:在 connect
命令后连接了一个外围设备.autoConnected(CBPeripheral)
:自动连接了一个外围设备,当您使用 autoconnectionHandler
时会触发此事件.ready(CBPeripheral)
:此状态表示您现在可以向外围设备发送命令。 为什么是 ready 而不仅仅是 connected? 因为您可能已经设置了一些 connectionTasks
,而 ready 意味着,如果它们存在,它们已经被执行。.connectionFailed(CBPeripheral, error: LittleBluetoothError?)
:当连接期间出现问题时.disconnected(CBPeripheral, error: LittleBluetoothError?)
:当外围设备已断开连接时,可能是显式断开连接或意外断开连接外围设备状态观察器:
它可以用于对外围设备状态进行更精细的控制,它们来自 CBPeripheralStates
有时,连接后您需要执行一些重复性任务,例如通过发送密钥或 NONCE 进行身份验证。 这些操作存储在 connectionTasks
属性中,并在正常连接或自动连接后执行。 所有其他操作将在完成此操作后执行。
自动连接由 autoconnectionHandler
处理程序管理。 您可以检查错误并确定是否需要自动连接。 如果您返回 true
,则连接过程将启动,一旦找到外围设备,就会建立连接。 如果您返回 false
,iOS 将不会尝试建立连接。 如果应用程序具有正确的权限,则连接过程也将在后台保持活动状态,要取消只需调用 disconnect
。 建立连接后,.autoConnected(PeripheralIdentifier)
事件将流式传输到 connectionEventPublisher
如果您想取消它,您必须发送显式断开连接。
在以下情况下,自动连接将被中断
应用程序权限 | 条件 |
---|---|
App 没有在 bkg 中运行的 BT 权限 | 显式断开连接,App 被用户/系统杀死,暂停时 |
App 具有在 bkg 中运行的 BT 权限 | 显式断开连接,App 被用户/系统杀死 |
App 具有在 bkg 中运行的 BT 权限,并启用了状态恢复 | 显式断开连接,App 被用户杀死 |
首先阅读 Apple 文档这里,这里 以及我在 Medium 上的文章。
要使状态恢复/保存功能生效,首先必须使用一个字典实例化 LittleBluetTooth
,该字典的键 CBCentralManagerOptionRestoreIdentifierKey
对应一个特定的字符串标识符,并通过使用 LittleBluetoothConfiguration
添加一个在状态恢复期间调用的处理程序。 您必须也选择启用蓝牙 LE 配件的后台模式。 此外,需要注意的是,状态恢复总是生效,而不仅仅是在后台。 例如,如果您使用滑动方式关闭应用程序,下次您重新启动它时,中央管理器将返回之前的状态,您必须考虑到这一点。 如果您只想在后台运行某些操作,只需询问 UIApplication 的状态并应用您的业务逻辑。
如果您的应用程序因后台蓝牙事件而被唤醒,它将调用 applicationDidFinishLauching
以及一个字典。 使用键 UIApplicationLaunchOptionsBluetoothCentralsKey
,您会收到一个 CBCentralManager 实例标识符数组,这些实例在应用程序关闭之前正在工作。 您有机会通过从启动选项字典中提取标识符并将其传递给 LittleBlueToothConfiguration
(或者您可以简单地使用常量实例化)来恢复 LittleBlueTooth
中央管理器。 如果触发了状态恢复事件,处理程序将收到一个 Restored
对象。 恢复的对象可以是 Peripheral
及其实例,也可以是扫描及其发现发布器,后者将发布所有发现的外围设备。 即使外围设备已断开连接,也会返回外围设备。 要再次收到关于外围设备状态的通知,请仅在外围设备处于就绪状态时订阅 connectionEventPublisher
,才可以发送其他命令。
如果您不希望 LittleBluetooth 管理状态恢复,您可以订阅 restoreStatePublisher
发布器,您将收到一个 CentralRestorer
对象,其中包含所有必要的信息,以便您自己管理状态恢复。 注意
autoconnectionHandler
,LittleBluetooth 将尝试重新建立连接。有时,提取已连接的外围设备和中央管理器并将其传递给另一个框架可能很有用。 例如,如果您需要使用 Nordic 库进行 OTA 固件更新,则这是必需的。 提取正是为此目的而进行的。
let extractedState = littleBT.extract()
在提取之前,您需要停止监听您正在监听的所有特征。 提取的状态是一个元组 (central: CBCentralManager, peripheral: CBPeripheral?)
,其中包含使用的 CBCentralManger
和一个 CBPeripheral
(如果已连接)。 您还可以通过传递您已提取的相同对象来重启 LittleBlueTooth 实例。
self.littleBT.restart(with: extractedState.central, peripheral: extractedState.peripheral)
大多数功能也封装在自定义运算符中。
常量 StartLittleBlueTooth
是一种语法糖,可帮助您准备具有正确错误类型的管道
StartLittleBlueTooth
.startDiscovery(for: self.littleBT, withServices: [CBUUID(string: HRMCostants.HRMService)])
.prefix(1)
// ...
.startDiscovery
运算符可以在不同的时间返回多个发现结果,您需要收集、过滤、添加前缀,然后在下一步连接之前获得正确的结果。
在获得 PeripheralDiscovery
或 PeripheralIdentifier
之后,您可以连接到该设备。
StartLittleBlueTooth
.startDiscovery(for: self.littleBT, withServices: [CBUUID(string: HRMCostants.HRMService)])
.prefix(1)
.connect(for: self.littleBT)
// .sink( ...
读取很简单,如下所示
StartLittleBlueTooth
.read(for: self.littleBT, from: hrmSensorChar)
.sink(receiveCompletion: { (result) in
print("Result: \(result)")
switch result {
case .finished:
break
case .failure(let error):
print("Error while changing sensor position: \(error)")
break
}
}) { (value: HeartRateSensorPositionResponse) in // Specify the concrete type
print("Value: \(value)")
}
.store(in: &disposeBag)
请注意,为了使编译器理解下一步函数的泛型类型,您可能需要指定具体的类型。
StartLittleBlueTooth
.write(for: self.littleBT, to: hrmControlPointChar, value: UInt8(0x01))
.sink(receiveCompletion: { (result) in
print("Result: \(result)")
switch result {
case .finished:
break
case .failure(let error):
print("Error while writing control point: \(error)")
break
}
}) {}
.store(in: &disposeBag)
直接监听(启用并从特征获取结果)
StartLittleBlueTooth
.startListen(for: self.littleBT, from: hrmRateChar)
.sink(receiveCompletion: { (result) in
print("Result: \(result)")
switch result {
case .finished:
break
case .failure(let error):
print("Error while trying to listen: \(error)")
}
}) { (value: HeartRateMeasurementResponse) in
self.hrmRateLabel.text = String(value.value)
}
.store(in: &disposeBag)
启用对特征的监听,并在 littleBT.listenPublisher
之前或之后附加
.enableListen(for: self.littleBT, from: charateristicOne)
停止
.disableListen(for: self.littleBT, from: hrmRateChar)
要断开与设备的连接,只需调用
.disconnect(for: self.littleBT)
要使用发布器或自定义运算符启动操作,您必须附加订阅者。 结果 AnyCancellable
必须存储在属性中或 disposebag 中,您必须保证管道的存在直到结束。
Jazzy 文档可在此处获取:here
示例应用程序可以从此处下载。 它还需要下载一个 macOS 或 iOS 应用程序来模拟心率监测器。
CBManager
和 CBPeripheral
提取请使用 Github,说明您做了什么,如何做的,您期望什么以及您得到了什么。
由于我是在业余时间从事这个项目,因此非常感谢您的帮助。 欢迎提出拉取请求。
如果没有参考 Polidea 的库 RXBluetooth Kit(如果您需要在较低的目标上部署,请检查它)和 Bluejay,另一个很棒的 iOS 库,这项工作永远不可能完成。
图标由 Freepik 制作,来自 www.flaticon.com
MIT 许可证
版权所有 (c) 2020 Andrea Finollo
特此授予任何获得本软件及相关文档文件(“软件”)副本的人员免费许可,以不受限制地处理本软件,包括但不限于使用、复制、修改、合并、出版、分发、再许可和/或出售软件副本的权利,并允许向其提供软件的人员遵守以下条件
以上版权声明和本许可声明应包含在本软件的所有副本或主要部分中。
本软件按“原样”提供,不提供任何形式的明示或暗示担保,包括但不限于适销性、特定用途适用性和非侵权性担保。 在任何情况下,作者或版权所有者均不对任何索赔、损害或其他责任负责,无论是在合同、侵权或其他方面,由软件或软件的使用或其他处理引起的或与之相关的。