美文网首页iOS开发-蓝牙
探究蓝牙 CoreBluetooth 实现数据传输

探究蓝牙 CoreBluetooth 实现数据传输

作者: 树根曰 | 来源:发表于2017-05-02 16:35 被阅读0次

    最近项目中需要与硬件进行蓝牙连接, 实现数据交互.
    一般来说, 外设会由硬件工程师开发好,并定义好设备提供的服务, 每个服务对于的特征, 每个特征的属性(只读, 只写, 通知等等). 本文例子的业务场景,就是用一手机app去读写蓝牙设备.
    在这里主要说一下 iOS 设备作为中心模式 连接外设的实现思路.

    一、蓝牙中心模式流程

    1. 建立中心角色
    2. 扫描外设(discover)
    3. 连接外设(connect)
    4. 扫描外设中的服务和特征(discover)
        - 4.1 获取外设的services
        - 4.2 获取外设的Characteristics,获取Characteristics的值,获取Characteristics的Descriptor和Descriptor的值
        - 4.3 读取数据
    5. 与外设做数据交互(explore and interact)
        - 5 .1 写数据
    6. 订阅Characteristic的通知
    7. 断开连接(disconnect)
    

    二、实现步骤
    1 . 导入 CoreBluetooth 头文件 #import <CoreBluetooth/CoreBluetooth.h> , 建立主设备管理类, 设置主设备委托

    #import <CoreBluetooth/CoreBluetooth.h>
    
    @interface CentralVewController ()<CBCentralManagerDelegate,CBPeripheralDelegate>
    {
        //系统蓝牙设备管理对象, 可以把他理解为主设备, 通过他, 可以去扫描和链接外设
        CBCentralManager *_centralManager;
        //用于保存被发现设备
        NSMutableArray *_allPeripherals;
    }
    @end
    @implementation CentralVewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.view.backgroundColor = [UIColor whiteColor];
        self.navigationItem.title = @"蓝牙开门";
        
        //初始化并设置委托和线程队列,最好一个线程的参数可以为nil,默认会就main线程
        _centralManager = [[CBCentralManager alloc]initWithDelegate:self queue:dispatch_get_main_queue()];
        //扫描的所有设备
        _allPeripherals = [NSMutableArray array];
    }
    

    2 . 扫描外设, 扫描外设的方法我们放在centralManager成功打开的委托中, 因为只有设备成功打开, 才能开始扫描, 否则会报错.

    //这个方法主要是来检查IOS设备的蓝牙硬件的状态的,比如说你的设备不支持蓝牙4.0,或者说你的设备的蓝牙没有开启,没有被授权什么的,一般是在你确定了你的IOS设备的蓝牙处于打开的情况下,你才应该执行扫描的动作,
    - (void)centralManagerDidUpdateState:(CBCentralManager *)central {
        
        switch (central.state) {
            case CBManagerStatePoweredOff:
                
    //系统蓝牙关闭了,请先打开蓝牙
                NSLog(@"state = CBManagerStatePoweredOff");
                break;
            case CBManagerStatePoweredOn:
                
                NSLog(@"state = CBManagerStatePoweredOn");
    //开始扫描周围外设
                [_centralManager scanForPeripheralsWithServices:nil options:nil];
                break;
                
            default:
                break;
        }
    }
    

    3 . 连接外设 (connect peripheral)

    //扫描到设备会进入该方法(根据扫描到的设备数会多次调用)
    -(void)centralManager:(CBCentralManager *)central
    didDiscoverPeripheral:(CBPeripheral *)peripheral
        advertisementData:(NSDictionary *)advertisementData
                     RSSI:(NSNumber *)RSSI{
        
        //这个方法是一旦扫描到外设就会调用的方法,注意此时并没有连接上外设,这个方法里面,你可以解析出当前扫描到的外设的广播包信息,当前RSSI等,现在很多的做法是,会根据广播包带出来的设备名,初步判断是不是自己公司的设备,才去连接这个设备,就是在这里面进行判断的
        
        //另外,当已发现的 peripheral  发送的数据包有变化时,这个代理方法同样会调用
        //在搜索过程中,并不是所有的 service 和 characteristic 都是我们需要的,如果全部搜索,依然会造成不必要的资源浪费。
        
        NSLog(@"扫描到设备 = %@ ",peripheral);
        NSLog(@"扫描到设备名称 = %@ ",peripheral.name);
        NSLog(@"扫描到设备的标识 = %@ ",peripheral.identifier.UUIDString);
        NSData *data = [advertisementData objectForKey:@"kCBAdvDataManufacturerData"];
        NSString *aStr= [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        aStr = [aStr stringByReplacingOccurrencesOfString:@" " withString:@""];
        NSLog(@"aStr:%@",aStr);
        NSLog(@"advertisementData:%@",advertisementData);
        NSLog(@"信号强度RSSI = %@",RSSI);
        // 一个周边可能会被多次发现
        [self matchDeviceWithPeripherals:peripheral];
    }
    #pragma mark 匹配设备
    //在这里匹配自己需要连接的设备
    - (void)matchDeviceWithPeripherals:(CBPeripheral *)peripheral {
        
        
        if (![_allPeripherals containsObject:peripheral]) {
            //将设备添加到数组中后, 在寻找匹配可连接的设备, 进行连接
            [_allPeripherals addObject:peripheral];
            
            //连接设备
            [_centralManager connectPeripheral:peripheral options:nil];
            
            //当你找到你需要的那个 peripheral 时,可以调用stop方法来停止搜索。
            [_centralManager stopScan];
            NSLog(@"Scanning stopped");
            //刷新表
            [self.tableView reloadData];
        }
    }
    

    4 . 扫描外设中的服务和特征
    4 . 1 获取外设的services

    #pragma mark 4.1获取外设的services
    //连接到Peripherals-成功
    - (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral{
        
        NSLog(@"---成功连接到设备 : %@",peripheral.name);
        
        //设置的peripheral委托CBPeripheralDelegate
        //@interface ViewController : UIViewController<CBCentralManagerDelegate,CBPeripheralDelegate>
        [peripheral setDelegate:self];
        
        /*
        扫描外设Services,成功后会进入方法:-(void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error{
        在实际项目中,这个参数应该不是nil的,因为nil表示查找所有可用的Service,但实际上,你可能只需要其中的某几个。搜索全部的操作既耗时又耗电,所以应该提供一个要搜索的 service 的 UUID 数组。
         */
        [peripheral discoverServices:@[[CBUUID UUIDWithString:SERVICE_UUID]]];
    }
    //扫描到Services
    -(void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error{
        //在调用 CBCentralManager 的 scanForPeripheralsWithServices:options: 方法时,central 会打开无线电去监听正在广播的 peripheral,并且这一过程不会自动超时。(所以需要我们手动设置 timer 去停掉)
        NSLog(@"---扫描到服务 :%@",peripheral.services);
        
        if (error) {
            NSLog(@"---扫描到Services : %@ 出现错误 : %@", peripheral.name, [error localizedDescription]);
            return;
        }
        
        //如果是搜索的全部 service 的话,你可以选择在遍历的过程中,去对比 UUID 是不是你要找的那个。
        for (CBService *service in peripheral.services) {
            NSLog(@"---扫描到Services的 UUID = %@",service.UUID);
            
            /*
             扫描每个service的Characteristics,扫描到后会进入方法: -(void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
             
             同样是出于节能的考虑,第一个参数在实际项目中应该是 characteristic 的 UUID 数组。
             */
            [peripheral discoverCharacteristics:@[[CBUUID UUIDWithString:CHARACTERISTIC_UUID]] forService:service];
        }
    }
    

    4 . 2 获取外设的Characteristics,获取Characteristics的值,获取Characteristics的Descriptor和Descriptor的值

    #pragma mark  获取外设的Characteristics,获取Characteristics的值,获取Characteristics的Descriptor和Descriptor的值
    //扫描到Characteristics
    -(void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error{
        
        if (error) {
            NSLog(@"---发现 characteristics : %@  出现错误 : %@", service.UUID, [error localizedDescription]);
            return;
        }
        
        /*
         发现了(指定)的特征值了,如果你想要有所动作,你可以直接在这里做,比如有些属性为 notify 的 Characteristics ,你想要监听他们的值,可以这样写
         
         当找到 characteristic 之后,可以通过调用CBPeripheral的readValueForCharacteristic:方法来进行读取。
         其实使用readValueForCharacteristic:方法并不是实时的。考虑到很多实时的数据,比如心率这种,那就需要订阅 characteristic 了。
         */
        for (CBCharacteristic *characteristic in service.characteristics) {
            
            NSLog(@"服务 service UUID :%@ 的 特征 Characteristic UUID : %@",service.UUID,characteristic.UUID);
            
            if ([[characteristic.UUID UUIDString] isEqualToString:CHARACTERISTIC_UUID]) {
                
                //成功与否的回调是peripheral:didUpdateNotificationStateForCharacteristic:error:,读取中的错误会以 error 形式传回:
                //当然也不是所有 characteristic 都允许订阅,依然可以通过CBCharacteristicPropertyNoify options 来进行判断。
                [peripheral setNotifyValue:YES forCharacteristic:characteristic]; //不想监听的时候,设置为:NO 就行了
                
                //如果写入成功后要回调,那么回调方法是peripheral:didWriteValueForCharacteristic:error:。如果写入失败,那么会包含到 error 参数返回。
                [self writeCharacteristic:peripheral characteristic:characteristic];// 1.写数据
                
            }else if ([[characteristic.UUID UUIDString] isEqualToString:@""]){
                
                //获取Characteristic的值,读到数据会进入方法:-(void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
                [peripheral readValueForCharacteristic:characteristic];// 2.读数据
            }else{
                
                //搜索Characteristic的Descriptors,读到数据会进入方法:-(void)peripheral:(CBPeripheral *)peripheral didDiscoverDescriptorsForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
                [peripheral discoverDescriptorsForCharacteristic:characteristic];// 3.获取特征描述
            }
            
    //注: 这里根据自己需求 或读数据, 或写数据
        }
    }
    

    4 .3 读取数据

    #pragma mark - 读取回调特征值
    //获取的charateristic的值
    -(void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error{
        //打印出characteristic的UUID和值
        //!注意,value的类型是NSData,具体开发时,会根据外设协议制定的方式去解析数据
        
     //这个可是重点了,你收的一切数据,基本都从这里得到,你只要判断一下 [characteristic.UUID UUIDString] 符合你们定义的哪个,然后进行处理就行,值为:characteristic.value 一切数据都是这个,至于怎么解析,得看你们自己的了
    
        //[characteristic.UUID UUIDString]  注意: UUIDString 这个方法是IOS 7.1之后才支持的,要是之前的版本,得要自己写一个转换方法
        NSLog(@"--- receiveData = %@,fromCharacteristic.UUID = %@",characteristic.value,characteristic.UUID);
        
        NSData *data = characteristic.value;//特征的值
        NSString *cValueStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        NSLog(@"--- 读取回调特征值 receiveData = %@",cValueStr);
        /*
         注意,不是所有 characteristic 的值都是可读的,你可以通过CBCharacteristicPropertyRead options 来进行判断
         如果你尝试读取不可读的数据,那上面的代理方法会返回相应的 error。
         */
    }
    //搜索到Characteristic的Descriptors
    -(void)peripheral:(CBPeripheral *)peripheral didDiscoverDescriptorsForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error{
        
        //打印出Characteristic和他的Descriptors
        NSLog(@"--- 搜索到Characteristic uuid:%@",characteristic.UUID);
        for (CBDescriptor *d in characteristic.descriptors) {
            
            NSLog(@"--- 特征描述符 Descriptor uuid:%@",d.UUID);
        }
        
    }
    //获取到Descriptors的值
    -(void)peripheral:(CBPeripheral *)peripheral didUpdateValueForDescriptor:(CBDescriptor *)descriptor error:(NSError *)error{
        //打印出DescriptorsUUID 和value
        //这个descriptor都是对于characteristic的描述,一般都是字符串,所以这里我们转换成字符串去解析
        NSLog(@"--- 特征描述符 characteristic descriptor.UUID:%@  value:%@",[NSString stringWithFormat:@"%@",descriptor.UUID],descriptor.value);
    }
    

    5 . 与外设做数据交互
    5 .1 写数据

    //在 didDiscoverCharacteristicsForService 方法中, 通过判断 UUID 来对相应的特征写数据
    //写数据
    -(void)writeCharacteristic:(CBPeripheral *)peripheral
                characteristic:(CBCharacteristic *)characteristic {
        
        //打印出 characteristic 的权限,可以看到有很多种,这是一个NS_OPTIONS,就是可以同时用于好几个值,常见的有read,write,notify,indicate,知知道这几个基本就够用了,前连个是读写权限,后两个都是通知,两种不同的通知方式。
        
        NSLog(@"--- characteristic.properties = %lu", (unsigned long)characteristic.properties);
        
        
        //只有 characteristic.properties 有write的权限才可以写
        if(characteristic.properties & CBCharacteristicPropertyWriteWithoutResponse){
            
            //发送开门命令
            NSString *dataStr = @"自己需要发送的数据";
            NSData *data = [NSData dataWithData:[dataStr dataUsingEncoding:NSASCIIStringEncoding]];
            /*
             最好一个type参数可以为CBCharacteristicWriteWithResponse或CBCharacteristicWriteWithoutResponse,区别是是否会有反馈
             */
            [peripheral writeValue:data forCharacteristic:characteristic type:CBCharacteristicWriteWithoutResponse];
            
            NSLog(@"---可以数据");
        }else{
            NSLog(@"---无法写入数据");
        }
    }
    - (void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
        //这个方法被调用是因为你主动调用方法: setNotifyValue:forCharacteristic 给你的反馈
        NSLog(@"---你更新了对特征值:%@ 的通知",[characteristic.UUID UUIDString]);
    }
    

    6 . 订阅Characteristic的通知

    #pragma mark - 6 订阅Characteristic的通知
    //设置通知
    -(void)notifyCharacteristic:(CBPeripheral *)peripheral
                 characteristic:(CBCharacteristic *)characteristic{
        //设置通知,数据通知会进入:didUpdateValueForCharacteristic方法
        [peripheral setNotifyValue:YES forCharacteristic:characteristic];
        
    }
    
    //取消通知
    -(void)cancelNotifyCharacteristic:(CBPeripheral *)peripheral
                       characteristic:(CBCharacteristic *)characteristic{
        
        [peripheral setNotifyValue:NO forCharacteristic:characteristic];
    }
    

    7 . 断开连接(disconnect)

    #pragma mark - 7 断开连接(disconnect)
    //一般在交互结束之后, 应马上断掉连接
    //停止扫描并断开连接
    -(void)disconnectPeripheral:(CBCentralManager *)centralManager
                     peripheral:(CBPeripheral *)peripheral{
        //停止扫描
        [centralManager stopScan];
        //断开连接
        [centralManager cancelPeripheralConnection:peripheral];
    }
    

    此外, 还有一些其他代理方法, 可根据自身需要来设置

    //连接到Peripherals-失败
    -(void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error{
        
        //看苹果的官方解释 {@link connectPeripheral:options:} ,也就是说链接外设失败了
        NSLog(@"---连接到名称为(%@)的设备-失败,原因:%@",[peripheral name],[error localizedDescription]);
    }
    
    //Peripherals断开连接
    - (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error{
        
        //自己看看官方的说明,这个函数被调用是有前提条件的,首先你的要先调用过了 connectPeripheral:options:这个方法,其次是如果这个函数被回调的原因不是因为你主动调用了 cancelPeripheralConnection 这个方法,那么说明,整个蓝牙连接已经结束了,不会再有回连的可能,得要重来了
        
        //如果你想要尝试回连外设,可以在这里调用一下链接函数
        NSLog(@"---外设连接断开连接 %@: 原因: %@", [peripheral name], [error localizedDescription]);
    }
    //根据 信号强度 估算距离
    - (void)peripheral:(CBPeripheral *)peripheral didReadRSSI:(NSNumber *)RSSI error:(NSError *)error {
        
        //这个就是你主动调用了 [peripheral readRSSI];方法回调的RSSI,你可以根据这个RSSI估算一下距离什么的
        NSLog(@"---peripheral Current RSSI:%@",RSSI);
        
    }
    

    这些是 iOS 连接外设的大体过程 , 在这里不忍吐槽一下,CoreBluetooth所有方法都是通过委托完成,代码冗余且顺序凌乱, 一整条链下来要近10几个委托方法,并且不断的在委托方法中调用方法再进入其他的委托,导致代码很零散。

    最后, 写了一个 DEMO, 有兴趣的可以下载看看.😊

    参考:
    http://www.saitjr.com/ios/core-bluetooth-read-write-as-central-role.html

    相关文章

      网友评论

        本文标题:探究蓝牙 CoreBluetooth 实现数据传输

        本文链接:https://www.haomeiwen.com/subject/xqqwzttx.html