iOS笔记之-蓝牙浅析

智能家居早已不再是一个单纯的概念,但是一直没机会做个东西练练手。这段时间有幸学习了个智能手表的项目,在这里把自己的一点心得,以及所遇到的问题给大家分享下,其中大部分是我自己的理解,肯定有很多地方写的不是很正确,欢迎专业大神来指正。

- 概念

情景:一块手表被手机的蓝牙连接,手机APP可以操作手表。

  1. 中央设备:CBCentralManager,手机

  2. 外设:CBPeripheral,手表

  3. 广播:手表发出的信号,中央设备搜索外设时,通过广播来识别。广播中有包含外设信息,如设备名、设备距离。另外硬件工程师可以将其他信息写在广播信息里,如厂商信息、MAC地址等。

  4. 服务:CBService,个人理解为手表支持的蓝牙服务,服务有指定ID,包含特征信息。

  5. 特征:CBCharacteristic,个人理解为蓝牙通道,是手机与手表之间通信的通道,硬件工程师给手表设置了通道的ID,手机在连接上设备后读取通道,识别后存储变量,用来写入指令以及监听手表反馈。

  6. 指令:一个16进制的Byte数组,由于蓝牙限制,有长度限制。(我做的项目最大长度是20字节,不知道其他的是不是也一样。)

  7. 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协议。

转载于:https://my.oschina.net/wolfcub1110/blog/1928676

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值