智能家居早已不再是一个单纯的概念,但是一直没机会做个东西练练手。这段时间有幸学习了个智能手表的项目,在这里把自己的一点心得,以及所遇到的问题给大家分享下,其中大部分是我自己的理解,肯定有很多地方写的不是很正确,欢迎专业大神来指正。
- 概念
情景:一块手表被手机的蓝牙连接,手机APP可以操作手表。
-
中央设备:CBCentralManager,手机
-
外设:CBPeripheral,手表
-
广播:手表发出的信号,中央设备搜索外设时,通过广播来识别。广播中有包含外设信息,如设备名、设备距离。另外硬件工程师可以将其他信息写在广播信息里,如厂商信息、MAC地址等。
-
服务:CBService,个人理解为手表支持的蓝牙服务,服务有指定ID,包含特征信息。
-
特征:CBCharacteristic,个人理解为蓝牙通道,是手机与手表之间通信的通道,硬件工程师给手表设置了通道的ID,手机在连接上设备后读取通道,识别后存储变量,用来写入指令以及监听手表反馈。
-
指令:一个16进制的Byte数组,由于蓝牙限制,有长度限制。(我做的项目最大长度是20字节,不知道其他的是不是也一样。)
-
DFU升级:用来升级硬件系统,手机端给手表发送手表系统安装包,处于升级模式的手表与普通模式不同,手机搜索的时候方法也不一样,我称之为DFU模式。(因为我只做了这一个项目,所以这样定义,不知道正确与否。)
- 实际操作(以手表项目为例)
搜索外设(普通模式)
self.centralMgr = [[CBCentralManager alloc] initWithDelegate:self queue:dispatch_get_main_queue() options:nil];
CBCentralManager:中央管理器,初始化时设置代理。options参数说明:
NSDictionary *options = @{
CBCentralManagerOptionShowPowerAlertKey:@YES,//搜索时蓝牙未打开,会给出提示
CBCentralManagerScanOptionAllowDuplicatesKey:@YES,//是否会重复扫描已经发现的设备
CBCentralManagerOptionRestoreIdentifierKey:@"aString",//唯一标识字符串,用以恢复蓝牙连接。(未使用到)
CBCentralManagerScanOptionSolicitedServiceUUIDsKey:@[@"CBUUID1",@"CBUUID2"],//已知UUID,搜索时指定UUID。值为 CBUUID 类型数组,此处为了方便说明,我用字符串代替。
CBConnectPeripheralOptionNotifyOnConnectionKey:@NO,//搜索到设备后是否提示框,一般为NO
CBConnectPeripheralOptionNotifyOnDisconnectionKey:@NO,//外设断开时是否有提示框
};
监听代理方法,判断当前设备的蓝牙情况:
-(void)centralManagerDidUpdateState:(CBCentralManager *)central {
switch (central.state) {
case CBCentralManagerStatePoweredOff://蓝牙关闭
break;
case CBCentralManagerStatePoweredOn://蓝牙打开,开始搜索外设
[self.centralMgr scanForPeripheralsWithServices:nil options:nil];
break;
case CBCentralManagerStateResetting://蓝牙重置
break;
case CBCentralManagerStateUnauthorized://APP没有蓝牙权限
break;
case CBCentralManagerStateUnknown://未知错误
break;
case CBCentralManagerStateUnsupported://当前设备不支持
break;
default:
break;
}
}
scanForPeripheralsWithServices,该方法是开始搜索外设,监听搜索结果。
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI {
id factoryID = [advertisementData objectForKey:@"kCBAdvDataManufacturerData"];//获取厂商ID,由硬件工程师写入
if ([factoryID isKindOfClass:[NSData class]]) {
NSString *strID = [NSString convertDataToHexStr:factoryID];
if ([strID hasPrefix:@"fafa"]) {
NSLog(@"搜索设备 peripheral=%@ factoryID = %@",peripheral,factoryID);
NSLog(@"连接设备的 信号强度 = %@",RSSI);
self.currPeripheral = peripheral;
[self.centralMgr connectPeripheral:peripheral options:nil];
[self.centralMgr stopScan];
}
}
}
实际开发中,不会用peripheral.name来识别设备,因为设备名称会重复,我的项目中使用advertisementData找出厂商ID,该内容由硬件工程师写入。
到这里搜索外设就告一段落。但这是广播模式的搜索,还有一种情况是此设备已被系统蓝牙配对,那么此设备是搜索不到的。作为一个外设,原则上是只能被一个设备连接,一旦被连接了,就不会被其他设备搜索到。APP开发中,虽然APP没连接手表,但系统蓝牙已连接,所以此时,手表的状态还是连接中。那么需要另一种方式找到要连接的设备:
NSArray *arr = [self.centralMgr retrieveConnectedPeripheralsWithServices:@[[CBUUID UUIDWithString:@"111"],[CBUUID UUIDWithString:@"222"]]];
但此时注意一点,就是获取到的数组只是CBPeripheral元素,只有连接上设备后才能读取信号强度,peripheral.RSSI属性已在8.0后废弃。我在项目中会先用该方法,遍历得到的数组,如果有需要连接的设备,则直接连接,而不再进行广播搜索。参数必填,不能为空,内容为已知的服务UUID。
连接外设
监听代理方法:
//连接成功
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
{
//[self.currPeripheral discoverServices:nil]];
[self.currPeripheral discoverServices:@[[CBUUID UUIDWithString:@"111"],[CBUUID UUIDWithString:@"222"]]];
[self.currPeripheral setDelegate:self];
self.m_deviceName = peripheral.name;
[[NSNotificationCenter defaultCenter]postNotificationName:kNotificationName_BT_Connected object:nil];
}
//连接失败
- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error {
//此时连接发生错误
NSLog(@"connected periphheral failed");
self.m_tpye = FTConnectBTType_Fail;
}
连接成功后,设置代理,去读取服务,discoverServices,该方法可以设置参数,已知的特征CBUUID数组。如果设置了,则只会在代理中返回包含该UUID的服务。
//获取服务后的回调
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error
{
if (error)
{
NSLog(@"didDiscoverServices : %@", [error localizedDescription]);
return;
}
for (CBService *s in peripheral.services)
{
NSLog(@"Service found with UUID : %@", s.UUID);
if ([s.UUID.UUIDString isEqualToString:@"111"] || [s.UUID.UUIDString isEqualToString:@"222"] ) {
[s.peripheral discoverCharacteristics:nil forService:s];
}
}
}
获取到服务后,识别正确的服务,通过service.UUID.UUIDString来判断。然后用方法discoverCharacteristics去读取服务中的特征,也就是我上面提到的通道。
//获取特征后的回调
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error{
if (error)
{
NSLog(@"didDiscoverCharacteristicsForService error : %@", [error localizedDescription]);
return;
}
for (CBCharacteristic *c in service.characteristics) {
if ([c.UUID.UUIDString isEqualToString:@"111"]) {
self.m_characteristicA2 = c;
} else if ([c.UUID.UUIDString isEqualToString:@"222"]) {
self.m_characteristicA3 = c;
[peripheral setNotifyValue:YES forCharacteristic:c];
}
}
}
CBCharacteristic类,需要注意该通道是监听通道还是写入通道,我的项目中,111是写入通道;222是监听通道,需要加上[peripheral setNotifyValue:YES forCharacteristic:c];这句来监听通道。当APP给手表下发指令时,通过111写入,然后监听222通道的返回值。
写入指令
- (void)writeVlueWithDataValue:(NSData*)data forCharacteristic:(CBCharacteristic*)characteristic_for noticCharacteristic:(CBCharacteristic*)characteristic_notic {
//[self.currPeripheral writeValue:data forCharacteristic:characteristic_for type:CBCharacteristicWriteWithResponse];
[self.currPeripheral writeValue:data forCharacteristic:characteristic_for type:CBCharacteristicWriteWithoutResponse];
}
注意 type 参数,枚举值,CBCharacteristicWriteWithResponse 和 CBCharacteristicWriteWithoutResponse,看单词就应该能懂,写入后有没有反馈的。但这需要看写入的通道属性,是否支持反馈。如果不反馈,则应该用CBCharacteristicWriteWithoutResponse,否则会写入不成功。
监听写入后蓝牙反馈不信息,实现代理方法:
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
{
if (error)
{
NSLog(@"didUpdateValueForCharacteristic error : %@", error.localizedDescription);
return;
}
NSString *value = [NSString convertDataToHexStr:characteristic.value];
NSLog(@"BT getResault value = %@",value);
//do something you need
}
至此,蓝牙基本功能已经实现。
- DFU升级
DFU升级,还有个概念叫OTA升级,但OTA升级概念广泛,本项目中是给手表进行升级,在此称作DFU升级。
iOS中的DFU升级,有几个库文件需要下载:iOSDFULibrary.framework、Zip.framework、Settings.bundle。
不过以上是Swift版,OC版请参考:https://www.jianshu.com/p/eb5b1e26adf7 我也是参考这篇文章做的,再次感谢作者。
首先在 Appdelegate 的 didFinishLaunchingWithOptions 方法中设置
NSDictionary* defaults = [NSDictionary dictionaryWithObjects:@[@"2.3", [NSNumber numberWithInt:12], @NO] forKeys:@[@"key_diameter", @"dfu_number_of_packets", @"dfu_force_dfu"]];
[[NSUserDefaults standardUserDefaults] registerDefaults:defaults];
我也不知道为什么要这样写,查了一些资料也没有结果,如果有哪位大神知道,还请告知。
其次,搜索的方法也需要更改
dispatch_queue_t centralQueue = dispatch_queue_create("no.nordicsemi.ios.FanTuProject", DISPATCH_QUEUE_SERIAL);
self.centralMgr = [[CBCentralManager alloc] initWithDelegate:self queue:centralQueue options:nil];
我想静静,别问我静静是谁。
连接方法一样,下面主要看DFU相关的操作。
导入代理:<LoggerDelegate,DFUServiceDelegate,DFUProgressDelegate>
声明一个属性:@property (strong, nonatomic) DFUServiceController *controller;
手机里存放一个手表安装包UK10805095_pkt.zip。
然后开始DFU:
- (void)createDFU {
if (isUpdating) {
return;
}
CBCentralManager *manager = self.manager.centralMgr;
CBPeripheral *device = self.manager.currPeripheral;
NSString * zipPath = [[NSBundle mainBundle] pathForResource:@"UK10805095_pkt"
ofType:@"zip"];
// To start the DFU operation the DFUServiceInitiator must be used
DFUServiceInitiator *initiator = [[DFUServiceInitiator alloc] initWithCentralManager:manager target:device];
/** 旧方法 */
//[initiator withFirmwareFile:selectedFirmware];
/** 新方法 */
initiator = [initiator withFirmware:self.selectedFirmware];
initiator.forceDfu = [[[NSUserDefaults standardUserDefaults] valueForKey:@"dfu_force_dfu"] boolValue];
initiator.packetReceiptNotificationParameter = [[[NSUserDefaults standardUserDefaults] valueForKey:@"dfu_number_of_packets"] intValue];
initiator.logger = self;
initiator.delegate = self;
initiator.progressDelegate = self;
initiator.enableUnsafeExperimentalButtonlessServiceInSecureDfu = YES;
_controller = [initiator start];
}
下面监听代理:
#pragma mark - Device selection delegate methods
-(void)logWith:(enum LogLevel)level message:(NSString *)message
{
NSLog(@"%ld: %@", (long) level, message);
}
//升级error信息
- (void)didErrorOccur:(enum DFUError)error withMessage:(NSString * _Nonnull)message {
NSLog(@"Error %ld: %@", (long) error, message);
self.m_headerView.m_lblTitle.text = @"连接过程中出现错误";
self.m_headerView.m_lblSubTitle.text = @"请将手表靠近手机并确保手表有电";
self.m_btnConect.hidden = NO;
[self.m_imageLoading stopLayerAnimation];
self.m_lblProgress.hidden = YES;
}
//DFU状态变化监听
-(void)dfuStateDidChangeTo:(enum DFUState)state
{
self.isUpgradeing = NO;
switch (state) {
case DFUStateConnecting:
NSLog(@"DFUStateConnecting");
break;
case DFUStateStarting:
NSLog(@"Starting DFU...");
self.isUpgradeing = YES;
break;
case DFUStateEnablingDfuMode:
NSLog(@"Enabling DFU Bootloader...");
break;
case DFUStateUploading:
NSLog(@"Uploading..");
break;
case DFUStateValidating:
NSLog(@"Validating...");
break;
case DFUStateCompleted://升级成功
[self upgradeSuccess];
break;
case DFUStateDisconnecting:
break;
case DFUStateAborted:
break;
default:
break;
}
}
//升级中,有进度信息
- (void)dfuProgressDidChangeFor:(NSInteger)part outOf:(NSInteger)totalParts to:(NSInteger)progress currentSpeedBytesPerSecond:(double)currentSpeedBytesPerSecond avgSpeedBytesPerSecond:(double)avgSpeedBytesPerSecond{
// NSLog(@"part:%ld, totalParts:%ld, progress:%ld, currentSpeedBytesPerSecond:%f, avgSpeedBytesPerSecond:%f", part, totalParts, progress, currentSpeedBytesPerSecond, avgSpeedBytesPerSecond);
//打印更新进度
// self.m_progress.progress = progress / 100.0;
}
//升级失败
- (void)dfuError:(enum DFUError)error didOccurWithMessage:(NSString * _Nonnull)message{
NSLog(@"Error %ld: %@", (long) error, message);
}
至此,升级过程就完成了,还需要一些工程文件的设置,请参考刚才那个链接。不再重复阐述。
有个坑需要注意下,项目刚开始那会,升级成功后无法连接到设备,看了下现象:手表被系统蓝牙连接着,但是 retrieveConnectedPeripheralsWithServices 获取不到外设。经过反复查证,是因为硬件工程师在升级成功后重新发出广播,导致之前的配对信息失效,如果不幸你也遇到这样的坑,那让硬件工程师去解决。
简单介绍完毕,蓝牙深究起来很复杂。iOS系统有ANCS,所以一些功能,如通过蓝牙挂断电话、有新来电时硬件设备给出相应提示,这些功能苹果自身的ANCS会帮你解决,不需要你代码设置。当然硬件工程师在写代码的时候也是遵循ANCS协议。