RZBluetooth
RZBluetooth 的目标是让 Core Bluetooth 更加容易使用和测试。它提供了一个基于块的 API,具有状态管理、自动发现和支持公共协议等功能。RZMockBluetooth 包含一组 Core Bluetooth 模拟对象,通过连接 CBCentralManager 和 CBPeripheralManager API,可以在应用程序中启用设备模拟。
快速入门
为了更好地了解 RZBluetooth,以下代码块将打印出附近第一个心率监测仪的心率,每当有新的读数时。请注意,此代码可以在您的单元测试目标中运行。
self.centralManager = [[RZBCentralManager alloc] init];
[self.centralManager scanForPeripheralsWithServices:@[CBUUID rzb_UUIDForHeartRateService] options:@{} onDiscoveredPeripheral:^(RZBScanInfo *scanInfo, NSError *error) {
[self.centralManager stopScan];
self.peripheral = scanInfo.peripheral;
[self.peripheral rzb_addHeartRateObserver:^(RZBHeartRateMeasurement *measurement, NSError *error) {
NSLog(@"%@", measurement);
} completion:^(NSError *error) {
if (error) {
NSLog(@"Error=%@", error);
}
}];
}];
另外,在 Swift 中
centralManager = RZBCentralManager()
centralManager.scanForPeripheralsWithServices([CBUUID.rzb_UUIDForHeartRateService()], options: nil) { scanInfo, error in
guard let peripheral = scanInfo?.peripheral else {
print("ERROR: \(error!)")
return
}
self.centralManager.stopScan()
peripheral.addHeartRateObserver({ measurement, error in
guard let heartRate = measurement?.heartRate else { return }
print("HEART RATE: \(heartRate)")
}, completion: { error in
guard let error = error else { return }
print("ERROR: \(error)")
})
}
此块代码将等待蓝牙开启并扫描支持心率服务的新的外设。当找到外设时,应用程序将连接到外设,发现心率服务并观察特征。当特征被通知时,将 NSData*
对象序列化为更开发者友好的对象。
安装
RZBluetooth 通过 CocoaPods 提供。要安装它,将以下行添加到您的 Podfile
pod 'RZBluetooth'
基于块的API
RZBluetooth公开了一个基于块的API,用于读取、写入并在值更改时接收通知。
RZBPeripheral *peripheral = [self.centralManager peripheralForUUID:uuid];
[peripheral readCharacteristicUUID:[CBUUID rzb_UUIDForBatteryLevelCharacteristic]
serviceUUID:[CBUUID rzb_UUIDForBatteryService]
completion:^(CBCharacteristic *characteristic, NSError *error) {
NSData *valueIActuallyWant = characteristic.value;
}];
[self enableNotifyForCharacteristicUUID:[CBUUID rzb_UUIDForBatteryLevelCharacteristic]
serviceUUID:[CBUUID rzb_UUIDForBatteryService]
onUpdate:^(CBCharacteristic *characteristic, NSError *error) {
// New value is notified by the peripheral
} completion:^(CBCharacteristic *characteristic, NSError *error) {
// Notification has been configured
}];
内部,RZBluetooth使用命令模式来简化代理管理。
- 如果未连接,外设将自动连接。
- 服务和特征将自动发现。
- 多次读取和写入调用不会导致超出所需数量的连接或发现事件。发现事件将被分批并触发在下一次运行循环迭代时。
这隐藏了大量的代理回调痛苦。在Core Bluetooth实现中,代理回调通常导致不同服务之间的紧密耦合。这使得编写可重用代码变得非常具有挑战性。命令模式放松了这种耦合,允许独立开发和支持不同的蓝牙服务。这使Service级别API的开发成为可能,即使外围设备不支持该服务,也可以在相同的代码库中进行开发和支持。
服务级别API
连接到蓝牙设备的开发人员不希望读取和写入NSData
块。开发人员希望与表现力API进行交互,这些API具有包含服务和朋友特有域知识的真实模型对象。RZBluetooth提供了许多标准蓝牙服务的API。这些为开发人员提供了扩展RZBluetooth以支持他们自己专有服务的方法。
- (void)exampleOperations
{
RZBPeripheral *peripheral = [self.centralManager peripheralForUUID:uuid];
[peripheral rzb_addBatteryLevelObserver:^(NSUInteger batteryLevel, NSError *error) {
// Update UI for the battery level.
} completion:^(NSError *error) {
// Completion indicating that the battery monitor has been set up.
}];
[peripheral rzb_readSensorLocation:^(RZBBodyLocation location) {
}];
[peripheral rzb_addHeartRateObserver:^(RZBHeartRateMeasurement *measurement, NSError *error) {
} completion:^(NSError *error) {
}];
}
错误处理
所有Core Bluetooth错误都传递给客户端,但是RZBluetooth添加了一些错误以帮助澄清一些状态角落案例。
CBCentralManagerState
如果执行操作时中心处于“终端”状态,将生成一个带有错误代码RZBluetooth[Unsupported|Unauthorized|PoweredOff]
的错误。如果状态为未知或重置,RZBluetooth将等待状态变为开机状态后再发送命令,或者将命令失败并返回适当的错误。
无法发现的服务和特征
如果在外围设备上执行操作,但该服务或特征不存在,将生成一个错误对象以清楚地说明错误情况。`RZBluetoothDiscoverServiceError` 和 `RZBluetoothDiscoverCharacteristicError` 中的 userInfo 字典将以 `RZBluetoothUndiscoveredUUIDsKey` 作为键,填充未发现的 UUID。
用户发起的超时
如果使用 `RZBUserInteraction` 启用操作,并且操作时间超过超时时间,则命令将失败,并且完成块将以错误对象触发。错误代码将是 `RZBluetoothTimeoutError`。
蓝牙使用模式
大多数蓝牙应用程序使用的行为模式有以下几种:
- 扫描应用程序可以与之交互的外围设备。
- 与已知外围设备进行可用性交互。
- 与已知外围设备进行用户交互。
扫描
扫描新外围设备通常是用户发起的操作,它能收集所有附近的设备,并允许用户确认他们想要与之交互的设备。务必指定所需服务的 UUID。
考虑您应用程序的 UX(用户体验)
- 提示用户执行所需设备操作以使设备可见。大多数心率监测器除非佩戴,否则不可被发现。
- 您需要提供一个设备列表供用户选择吗?您能否告诉用户找到了太多设备并建议关闭其他设备?
- 如果有多个设备,用户如何确保选择正确的设备?
- 使用哪种类型的安全机制?在选择完成前,通过读取或写入受保护属性来初始化 SSN 配对过程。
一旦选择了一个设备,外设UUID可以在应用程序启动之间持久化。此外,请注意,外设UUID是iOS设备的唯一标识符,不应在计算机之间共享。
可用性交互
可用性交互是一组应该在设备每次可用时执行的操作。设备同步通常基于这一点构建。RZBPeripheral提供连接代理来帮助管理此类交互。
peripheral.connectionDelegate = self
peripheral.maintainConnection = YES;
}
//
- (void)peripheral:(RZBPeripheral *)peripheral connectionEvent:(RZBPeripheralStateEvent)event error:(NSError *)error;
{
if (event == RZBPeripheralStateEventConnectSuccess) {
// perform any connection set up here that should occur on every connection
}
else {
// The device is disconnected. maintainConnection will attempt a connection event
// immediately after this. This default maintainConnection behavior may not be
// desired for your application. A backoff timer or other behavior could be
// implemented here.
}
}
通常应忽略所有传输层错误,而大多数其他错误应被视为致命错误。
用户交互
Core Bluetooth和RZBluetooth操作默认不超时。然而,用户启动的操作需要超时,以便UI向用户报告问题。这种行为可以通过RZBUserInteraction
对象轻松启用。
[RZBUserInteraction setTimeout:5.0];
[RZBUserInteraction perform:^{
[self.peripheral rzb_fetchBatteryLevel:^(NSUInteger level, NSError *error) {
// The error object could have status code RZBluetoothTimeoutError
}];
}];
后台支持
如果应用程序在后台模式中指定了bluetooth-central
,RZBluetooth将指定一个CBCentralManagerOptionRestoreIdentifierKey
。此键会在建立连接尝试成功时使应用程序恢复。RZBCentralManager
有一个属性restorationHandler
,当外设恢复时触发。
日志支持
RZBluetooth通过RZBSetLogHandler
方法提供打印描述所有CoreBluetooth交互的日志消息的功能。通常可以直接发送到NSLog,但如果您的应用程序具有特殊的记录器,集成应该很简单。
RZBPeripheral 子类
对于更复杂的外围设备,通常需要更复杂的关联状态和处理程序回调。如果您认为子类可以帮助您的实现,您可以使用 RZBCentralManager
上的特殊初始化器,当发现新外围设备时创建您的子类。
测试
Core Bluetooth 的测试可能具有挑战性。RZBluetooth 包含一个库,RZMockBluetooth
,允许您使用模拟 Core Bluetooth 对象来测试您的蓝牙和应用程序代码。使用模拟库,您可以程序化地伪造蓝牙设备事件,应用程序中看到的核心蓝牙对象将持续管理它们的模拟状态。尽管实际的对象是等效的 RZBMock
对象,但所有应用程序代码都将使用核心蓝牙提供的相同 API。
例如
[self.mockCentralManager fakeStateChange:CBManagerStatePoweredOn];
// Triggers: - (void)centralManagerDidUpdateState:(CBCentralManager *)centralManager
// Configures: centralManager.state == CBManagerStatePoweredOn
[self.mockCentralManager fakeDisconnectPeripheralWithUUID:identifier
error:nil];
// Triggers: - (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error
// Configures: peripheral.state = CBPeripheralStateDisconnected
以下模拟对象可使用:
核心蓝牙 | 模拟对象 |
---|---|
CBCentralManager | RZBMockCentralManager |
CBPeripheral | RZBMockPeripheral |
CBPeripheralManager | RZBMockPeripheralManager |
模拟对象反映了 CoreBluetooth 栈并支持与它们的核心蓝牙等效对象相同的公共 API。所有模拟对象遵循两种模式。当在模拟对象上调用核心蓝牙 API 时,它将立即将该方法调用转发到 mockDelegate
。模拟对象还支持大量以 fake
为前缀的方法。这些所有模拟方法都与对象代理的代理方法相关联。当方法被调用时,它将在对象的分配队列中引发代理触发器。
作为开发者,您可以直接使用 RZBMockCentralManager
或 RZBMockPeripheralManager
,或使用 RZBMockEnable(YES)
。这将 swizzle 的 CBCentralManager
和 CBPeripheralManager
的 alloc 并返回等效的 RZBMock 对象。RZBMockCentralManager 将创建 RZBMockPeripheral
对象而不是 CBPeripheral
对象。
伪造外围设备
蓝牙的另一种出色的测试策略是使用 CBPeripheralManager API 实现模拟外围设备。这允许开发者在硬件仍在开发阶段时针对相同的蓝牙服务进行测试。
宝 سرمایهگذاری میکند یک کلاس بنیادی به نام RZBSimulatedDevice
که به سادهسازی API CBPeripheralManager
کمک میکند. این آی嘉年华 یک منتقلدهنده از CBPeripheralManager
است و چند دستیار برای کار با CBPeripheralManager
فراهم میکند. همچنین پشتیبانی از برخی خدمات معمول مانند میزان باتری و اطلاعات دستگاه را فراهم میکند. با این، یک پوسته کوچک از یک برنامه iOS یا macOS میتوان نوشته شود تا این دستگاه پیرامونی تقلبی شود.
این ممکن است به نظر برسد که تأمل زیادی با کمبود بهرهوری است. اما ارزش توسعه با استفاده از RZBluetooth خیلی بالا است به دلیل پشتیبانیاش از شبیهسازی در خوشههای رم.
شبیهسازی
RZBMockBluetooth چندین آیتم شبیهسازی فراهم میکند که از آیتمهای تقلبی استفاده میکند تا CBCentralManager
و API CBPeripheralManager
را متصل کند. این امکان را برای توسعهدهنده فراهم میکند که دستگاه پیرامونی تقلبی توسعهیافته بالا را درون برنامه به عنوان یک پشته بلوتوث کامل استفاده کند. این با Hilfe نیز درون سفارشسازنده可行的 است، که به طور معمول برای توسعه Core Bluetooth بیفایده بوده است. با این، توسعهدهنده میتواند دستگاه پیرامونی تقلبی را درون واحد تستها یا از طریق یک منوی تغییرات درون برنامه استفاده کند.
نقشههای تجمیعی
برای درک اینکه شبیهسازی در مقایسه با استفاده واقعی بلوتوث چگونه است، اینجا یک نقشهی تجمیعی از خواندن با استفاده از بلوتوث آورده شده است
اینجا یک نقشهی تجمیعی از درخواست خواندن مشابه که از طریق شبیهسازی انجام شده است آورده شده است
توسعه یک دستگاه تقلبی
این بخش مراحل ایجاد یک دستگاه پیرامونی با سرویس باتری و استفاده از آن درون یک واحد تست را توضیح میدهد.
مدلسازی سرویس بلوتوث
قدم اول مدلسازی سرویس و ویژگیهای بلوتوث با Core Bluetooth است.
CBMutableService *batteryService =
[[CBMutableService alloc] initWithType:[CBUUID rzb_UUIDForBatteryService] primary:NO];
CBMutableCharacteristic *batteryCharacteristic =
[[CBMutableCharacteristic alloc] initWithType:[CBUUID rzb_UUIDForBatteryLevelCharacteristic]
properties:CBCharacteristicPropertyRead | CBCharacteristicPropertyIndicate
value:nil
permissions:CBAttributePermissionsReadable];
batteryService.characteristics = @[batteryCharacteristic];
[self addService:batteryService];
这将添加一个带读取和指示支持的电池服务和特征。通过指定nil作为值,CoreBluetooth会得知这应该是一个动态值,将通过回调来提供。查看更多信息,请参阅设置您的服务和特性,位于CoreBluetooth文档。
处理蓝牙事件
下一步是处理CoreBluetooth触发的回调。这可以是读取请求、写入请求,或者当特性被观察或取消观察时触发的订阅更改。在这个例子中,提供一个假电池级别相对简单。
__block typeof(self) welf = (id)self;
[self addReadCallbackForCharacteristicUUID:[CBUUID rzb_UUIDForBatteryLevelCharacteristic] handler:^CBATTError (CBATTRequest *request) {
NSNumber *batteryNumber = welf.values[RZBBatteryLevelKey];
uint8_t batteryLevel = [batteryNumber unsignedIntegerValue];
request.value = [NSData dataWithBytes:&batteryLevel length:1];
return CBATTErrorSuccess;
}];
这为电池特性注册了一个读取处理程序,它会捕获表示值的内存中的状态,并使用新的数据将蓝牙请求作为响应。这响应该读取请求,但没有配置电池级别的方法。
注意,RZBSimulatedDevice
提供了一个名为values
的字典来存储任意数据。这是为了让特性可以作为类别添加到RZBSimulatedDevice
中。
暴露给开发者API
接下来,模拟设备需要提供一些面向开发者的API来配置通过蓝牙暴露的内存中的状态。此实现还提供了指示支持,以通知任何观察到的外设电池级别已更改。
- (void)setBatteryLevel:(uint8_t)level
{
self.values[RZBBatteryLevelKey] = @(level);
CBMutableCharacteristic *batteryCharacteristic = [self characteristicForUUID:[CBUUID rzb_UUIDForBatteryLevelCharacteristic]];
NSData *value = [NSData dataWithBytes:&level length:1];
[self.peripheralManager updateValue:value
forCharacteristic:batteryCharacteristic
onSubscribedCentrals:nil];
}
- (uint8_t)batteryLevel
{
return [self.values[RZBBatteryLevelKey] unsignedIntegerValue];
}
然后,可以使用此API修改模拟设备状态
// Update the battery level and send a notification to any central observing the battery level
device.batteryLevel = 88;
模拟连接
RZBMockBluetooth
能够在你的应用CBCentralManager
和CBPeripheralManager
之间建立模拟连接。 RZBSimulatedConnection
允许测试开发者通过程序性API控制连接行为。
示例
// Do this once at app load
RZBEnableMock(YES);
// Obtain the existing application CBCentralManager, which is really an RZBMockCentralManager since mocking is enabled.
CBCentralManager *centralManager = [[CBCentralManager alloc] init];
// Create a fake peripheral
self.fakePeripheral = [[RZBSimulatedDevice alloc] init];
// Setup the simulated central and the simulated connection
self.central = [[RZBSimulatedCentral alloc] initWithMockCentralManager:centralManager.mock];
[central addSimulatedDeviceWithIdentifier:[NSUUID UUID]
peripheralManager:self.fakePeripheral.peripheralManager];
self.connection = [central connectionForIdentifier:identifier]
// Disconnect or prevent connection.
self.connection.connectable = NO;
// ...runloop spins...
// Become connectable again
self.connection.connectable = YES;
模拟回调
对于大多数集成测试场景,只需要连接属性。连接对象为每个可用的代理方法(如扫描、读取、写入、通知、连接等)都提供了一个 RZBSimulatedCallback
。通过这些模拟回调,可以轻松地向蓝牙堆栈核心的任何部分注入错误。
例如,在1秒后引发连接错误
self.connection.connectCallback.injectError = [NSError rzb_connectionError];
self.connection.connectCallback.delay = 1.0;
单元测试
最后一部是构建一个单元测试套件来验证蓝牙实现的行为。RZBluetooth提供了一个基类,RZBSimulatedTestCase
,它配置所有上述对象并提供对连接对象的访问。一个好例子是RZBProfileBatteryTests,它提供了一些简单的读取和观察测试。
应用集成
在内模拟中,一个良好的策略是创建一个模拟控制器,该控制器保持对 RZBSimulatedCentral
和 RZBSimulatedConnection
的引用。这可以展示一个 UIViewController
子类,甚至是一系列 UIAlertController
来配置模拟。
一些建议
- 使用两指三指点击启用模拟。
- 将模拟状态保存在
NSUserDefaults
中,并在应用启动时进行配置。 - 使用
UIAlertController
简化对任意属性的配置。 - 创建多个外设以调试扫描。