一个基于块的框架,用于使用 CoreBluetooth API 构建 Bluetooth 4.0 低功耗(即智能或 LE)iOS 7 或 OS X 10.9 应用程序。包括 Deanna 和 DeannaMac,分别用于 iOS 和 OS X 中的 TI SensorTag 通信的应用。
蓝牙低功耗(BLE)事务在其本质上是两步的(请求-响应):CoreBluetooth 抽象化了此协议,使得请求行为与响应行为分离。两个阶段通过委托模式来协调:启动请求阶段的对象有一个代表对象,其有一个代表方法来处理相应的响应阶段。虽然功能强大,但委托模式可能难以使用,因为两步事务的行为被分成代码中的两个不同位置。
更方便的编程模式是使用与请求定义的回调块。当收到响应时,回调块可以被执行以处理它。“YmsCoreBluetooth 的设计意图是使用 Objective-C 块来定义 BLE 请求的响应行为”因此,这些请求包括
CoreBluetooth 的数据对象层次结构可以这样描述
但是,现有的 CoreBluetooth API 并没有将 BLE 请求映射到数据对象层级。例如,连接到一个 CBPeripheral 实例是通过 CBCentralManager 实例实现的,而不是通过 CBPeripheral。写操作、读操作和设置 CBCharacteristic 的通知状态都是从 CBPeripheral 实例发起的,而不是从 CBCharacteristic。YmsCoreBluetooth 提供了一个 API,该 API 可以更自然地将操作映射到数据对象层级。
YMSCoreBluetooth 定义了容器类,这些类对应于 CoreBluetooth 对象层级
但是,它们与 CoreBluetooth 的区别在于操作是基于对象类型的
应用程序 Deanna 和 DeannaMac 是演示应用程序,用于说明 YmsCoreBluetooth 的使用。这两个应用程序都使用 YmsCoreBluetooth 来表征 TI SensorTag,并提供对设备上六个传感器的可控制和可观察性。它还具有扫描其他 BLE 设备的能力。
应用程序 Deanna 最近进行了更改(版本 0.943),以支持 Wayne Dahlberg 提供的图形设计贡献。
在下面的代码示例中,self
是 YMSCBCentralManager 的子类的实例。
__weak DEACentralManager *this = self;
[self scanForPeripheralsWithServices:nil
options:options
withBlock:^(CBPeripheral *peripheral, NSDictionary *advertisementData, NSNumber *RSSI, NSError *error) {
if (error) {
NSLog(@"Something bad happened with scanForPeripheralWithServices:options:withBlock:");
return;
}
NSLog(@"DISCOVERED: %@, %@, %@ db", peripheral, peripheral.name, RSSI);
[this handleFoundPeripheral:peripheral];
}];
在下面的代码示例中,self
是 YMSCBCentralManager 的子类的实例,而 identifiers
是一个包含 NSUUID
实例的数组,这些实例映射到已由 iOS 设备发现的设备。
[self retrievePeripheralsWithIdentifiers:identifiers];
注意 - 由于弃用了 [YMSCBCentralManager retrievePeripherals:withBlock:],此 API 在 iOS7 中已更改。
// I AM OLD CODE.
__weak DEACentralManager *this = self;
[self retrievePeripherals:peripheralUUIDs
withBlock:^(CBPeripheral *peripheral) {
[this handleFoundPeripheral:peripheral];
}];
在下面的代码示例中,self
是 YMSCBPeripheral 的子类的实例。请注意,在回调中,发现服务和特性是以嵌套方式处理的。
- (void)connect {
// Watchdog aware method
[self resetWatchdog];
[self connectWithOptions:nil withBlock:^(YMSCBPeripheral *yp, NSError *error) {
if (error) {
return;
}
// Example where only a subset of services is to be discovered.
//[yp discoverServices:[yp servicesSubset:@[@"temperature", @"simplekeys", @"devinfo"]] withBlock:^(NSArray *yservices, NSError *error) {
[yp discoverServices:[yp services] withBlock:^(NSArray *yservices, NSError *error) {
if (error) {
return;
}
for (YMSCBService *service in yservices) {
if ([service.name isEqualToString:@"simplekeys"]) {
__weak DEASimpleKeysService *thisService = (DEASimpleKeysService *)service;
[service discoverCharacteristics:[service characteristics] withBlock:^(NSDictionary *chDict, NSError *error) {
[thisService turnOn];
}];
} else if ([service.name isEqualToString:@"devinfo"]) {
__weak DEADeviceInfoService *thisService = (DEADeviceInfoService *)service;
[service discoverCharacteristics:[service characteristics] withBlock:^(NSDictionary *chDict, NSError *error) {
[thisService readDeviceInfo];
}];
} else {
__weak DEABaseService *thisService = (DEABaseService *)service;
[service discoverCharacteristics:[service characteristics] withBlock:^(NSDictionary *chDict, NSError *error) {
for (NSString *key in chDict) {
YMSCBCharacteristic *ct = chDict[key];
//NSLog(@"%@ %@ %@", ct, ct.cbCharacteristic, ct.uuid);
[ct discoverDescriptorsWithBlock:^(NSArray *ydescriptors, NSError *error) {
if (error) {
return;
}
for (YMSCBDescriptor *yd in ydescriptors) {
NSLog(@"Descriptor: %@ %@ %@", thisService.name, yd.UUID, yd.cbDescriptor);
}
}];
}
}];
}
}
}];
}];
}
在下面的代码示例中,self
是 YMSCBService 的子类的实例。所有发现的特性都被存储在 [YMSCBService characteristicDict] 中。
- (void)readDeviceInfo {
YMSCBCharacteristic *system_idCt = self.characteristicDict[@"system_id"];
__weak DEADeviceInfoService *this = self;
[system_idCt readValueWithBlock:^(NSData *data, NSError *error) {
NSMutableString *tmpString = [NSMutableString stringWithFormat:@""];
unsigned char bytes[data.length];
[data getBytes:bytes];
for (int ii = (int)data.length; ii >= 0;ii--) {
[tmpString appendFormat:@"%02hhx",bytes[ii]];
if (ii) {
[tmpString appendFormat:@":"];
}
}
NSLog(@"system id: %@", tmpString);
_YMS_PERFORM_ON_MAIN_THREAD(^{
this.system_id = tmpString;
});
}];
YMSCBCharacteristic *model_numberCt = self.characteristicDict[@"model_number"];
[model_numberCt readValueWithBlock:^(NSData *data, NSError *error) {
if (error) {
NSLog(@"ERROR: %@", error);
return;
}
NSString *payload = [[NSString alloc] initWithData:data encoding:NSStringEncodingConversionAllowLossy];
NSLog(@"model number: %@", payload);
_YMS_PERFORM_ON_MAIN_THREAD(^{
this.model_number = payload;
});
}];
}
在下面的代码示例中,self
是 YMSCBService 的子类的实例。在此示例中,先向 'config' 特性写入,然后读取 'calibration' 特性。
- (void)requestCalibration {
if (self.isCalibrating == NO) {
__weak DEABarometerService *this = self;
YMSCBCharacteristic *configCt = self.characteristicDict[@"config"];
[configCt writeByte:0x2 withBlock:^(NSError *error) {
if (error) {
NSLog(@"ERROR: write request to barometer config to start calibration failed.");
return;
}
YMSCBCharacteristic *calibrationCt = this.characteristicDict[@"calibration"];
[calibrationCt readValueWithBlock:^(NSData *data, NSError *error) {
if (error) {
NSLog(@"ERROR: read request to barometer calibration failed.");
return;
}
this.isCalibrating = NO;
char val[data.length];
[data getBytes:&val length:data.length];
int i = 0;
while (i < data.length) {
uint16_t lo = val[i];
uint16_t hi = val[i+1];
uint16_t cx = ((lo & 0xff)| ((hi << 8) & 0xff00));
int index = i/2 + 1;
if (index == 1) self.c1 = cx;
else if (index == 2) this.c2 = cx;
else if (index == 3) this.c3 = cx;
else if (index == 4) this.c4 = cx;
else if (index == 5) this.c5 = cx;
else if (index == 6) this.c6 = cx;
else if (index == 7) this.c7 = cx;
else if (index == 8) this.c8 = cx;
i = i + 2;
}
this.isCalibrating = YES;
this.isCalibrated = YES;
}];
}];
}
}
YmsCoreBluetooth在没有使用块(blocks)处理BLE响应的地方之一是特色通知更新。这是因为此类更新是非同步和不可预测的。因此,必须为YMSCBService的任何子类实现处理器方法[YMSCBService notifyCharacteristicHandler:error:]
来处理此类更新。
以下代码示例中,self
是YMSCBService子类的一个实例。
- (void)turnOn {
__weak DEABaseService *this = self;
YMSCBCharacteristic *configCt = self.characteristicDict[@"config"];
[configCt writeByte:0x1 withBlock:^(NSError *error) {
if (error) {
NSLog(@"ERROR: %@", error);
return;
}
NSLog(@"TURNED ON: %@", this.name);
}];
YMSCBCharacteristic *dataCt = self.characteristicDict[@"data"];
[dataCt setNotifyValue:YES withBlock:^(NSError *error) {
NSLog(@"Data notification for %@ on", this.name);
}];
_YMS_PERFORM_ON_MAIN_THREAD(^{
this.isOn = YES;
});
}
- (void)notifyCharacteristicHandler:(YMSCBCharacteristic *)yc error:(NSError *)error {
if (error) {
return;
}
if ([yc.name isEqualToString:@"data"]) {
NSData *data = yc.cbCharacteristic.value;
char val[data.length];
[data getBytes:&val length:data.length];
int16_t v0 = val[0];
int16_t v1 = val[1];
int16_t v2 = val[2];
int16_t v3 = val[3];
int16_t amb = ((v2 & 0xff)| ((v3 << 8) & 0xff00));
int16_t objT = ((v0 & 0xff)| ((v1 << 8) & 0xff00));
double tempAmb = calcTmpLocal(amb);
__weak DEATemperatureService *this = self;
_YMS_PERFORM_ON_MAIN_THREAD(^{
this.ambientTemp = @(tempAmb);
this.objectTemp = @(calcTmpTarget(objT, tempAmb));
});
}
}
重要:为了通过KVO让主线程中的UI组件知道属性已更改,您必须在主线程上更新该属性。使用GCD调用dispatch_async()
的实现宏_YMS_PERFORM_ON_MAIN_THREAD
可以完成此操作。
#define _YMS_PERFORM_ON_MAIN_THREAD(block) dispatch_async(dispatch_get_main_queue(), block);
YmsCoreBluetooth使用的回调模式使用单个回调来处理成功和失败的响应。这通过包含一个error
参数来实现。如果error
对象不是nil
,则可以实施处理失败的逻辑。否则,将实施处理成功的逻辑。
^(NSError *error) {
if (error) {
// Code to handle failure
return;
}
// Code to handle success
}
YmsCoreBluetooth框架是位于YmsCoreBluetooth
目录下以YMSCB
为前缀的文件集。
Deanna iOS应用程序的文件以DEA
为前缀,位于Deanna
目录中。
DeannaMac OS X应用程序的文件以DEM
为前缀,位于DeannaMac
目录中。
为了更好地了解YmsCoreBluetooth的工作原理,建议首先阅读以下BLE服务实现的源代码:
然后是TI SensorTag的BLE外围实现
然后是管理所有已知外围设备的应用程序服务
类层次结构有助于说明上述类与YmsCoreBluetooth框架之间的关系。
通过阅读教程中如何为TI SensorTag完成实例的方法,了解如何编写自己的蓝牙低功耗服务。
虽然YmsCoreBluetooth相当实用,但仍有改进的空间。请将任何问题或问题提交给YmsCoreBluetooth的GitHub项目。
我正在整理使用YmsCoreBluetooth的项目列表。如果你不害羞,请让我知道,感谢您使用YmsCoreBluetooth!即使你害羞,也请让我知道,我保证保密。
以下设备上测试了代码
移除了对现已过时的 iOS 9 CoreBluetooth 代理方法的支持
管理性更改
添加了支持 BLE 地址的方法
测试了 Mac 环境
查看 以前更改列表
Copyright 2013-2014 Yummy Melon Software LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://apache.ac.cn/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: Charles Y. Choi <[email protected]>