iOS蓝牙通讯开发

作者: c6e16b2e3a16 | 来源:发表于2016-06-24 12:35 被阅读3105次

    刚开发完一款APP,其中涉及到应用和硬件进行蓝牙通讯需求,记录分享总结.
    场景:APP扫描制定的蓝牙设备,连接设备,交互数据
    准备工作:Xcode7.3,Objective-C,BLE设备

    1.参考资料

    在开发中,主要参考了这位大神的博客:
    http://liuyanwei.jumppo.com/2015/07/17/ios-BLE-1.html
    开源的框架BabyBluetooth:
    https://github.com/coolnameismy/BabyBluetooth
    是用链式语法(类似Masonry)封装了CoreBluetooth框架,链式方法没有提示,不习惯的话容易出处,所以我还是用的CoreBluetooth的API.
    一个中心设备可以连接多个外设,外设同时只能被一个中心设备连接.
    基本知识:
    一个外设(peripheral)可以提供一种或多种服务(service),一种服务有一个或多个特征(characteristic).
    基本工作原理:1.扫描外设;2.连接设备;3.扫描设备的服务;4.扫描服务的特征;5.监听特征,获取蓝牙传回的数据;6.向蓝牙发送指令,接收数据,解析

    2.iPhone作为中心设备(CBCentralManager)连接外设(CBPeripheral)

    CBCentralManager:中心设备,可以连接外设(CBPeripheral),管理设备,发送指令,接收数据等.如我要做的APP作为中心设备来管理外设.
    CBPeripheral:如我的项目中是一款治疗仪,app可以发送指令给治疗仪,请求/同步数据等功能.
    APP可以作为中心设备,也可以作为外接设备,但不能同时既是中心设备,又是外设,这里只考虑APP作为中心设备连接管理外设.

    3.实现步骤

    3.1 导入框架,新建CBCentralManager,设置代理,实现代理方法
    //遵循代理
    <CBCentralManagerDelegate, CBPeripheralDelegate>
    
    //蓝牙管理对象
    @property (strong, nonatomic) CBCentralManager *cbManager;
    //扫描到的设备后需要添加到数组中持有他.
    @property (copy, nonatomic) NSMutableArray<CBPeripheral *> *peripheralArray;
    //连接到的设备
    @property (copy, nonatomic) NSMutableArray<CBPeripheral *> *connectedPeripheralArray;
    //tableView展示外设,可以展示扫描到的设备,手动连接,也可以自动连接设备,展示已连接设备
    @property (strong, nonatomic) UITableView* tableView;
    
    //初始化
    _cbManager = [[CBCentralManager alloc] initWithDelegate:self queue:dispatch_get_main_queue()];
    //扫描外设,先不扫描,等到代理方法中检测到蓝牙可用时调用
    //第一个参数为nil时,扫描所有所有设备
    //第一个参数有值时,扫描指定的一类设备
    //[_cbManager scanForPeripheralsWithServices:nil options:nil];
    
    3.2 CoreBlueTooth框架都以代理方式实现
    3.2.1 CBCentralManagerDelegate
    #pragma mark - CBCentralManagerDelegate
    //监听蓝牙状态,蓝牙状态改变时调用
    //不同状态下可以弹出提示框交互
    //如果单独封装了这个类,可以设置代理或block或通知向控制器传值
    -(void)centralManagerDidUpdateState:(CBCentralManager *)central{
        switch (central.state) {
            case CBCentralManagerStateUnknown:
                NSLog(@">>>蓝牙未知状态");
                break;
            case CBCentralManagerStateResetting:
                NSLog(@">>>蓝牙重启");
                break;
            case CBCentralManagerStateUnsupported:
                NSLog(@">>>不支持蓝牙");
                break;
            case CBCentralManagerStateUnauthorized:
                NSLog(@">>>未授权");
                break;
            case CBCentralManagerStatePoweredOff:
                NSLog(@">>>蓝牙关闭");
                break;
            case CBCentralManagerStatePoweredOn:
                NSLog(@">>>蓝牙打开");
                //蓝牙打开时,再去扫描设备
                [_cbManager scanForPeripheralsWithServices:nil options:nil];
                break;
            default:
                break;
        }
    }
    
    //发现外设时调用
    - (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *,id> *)advertisementData RSSI:(NSNumber *)RSSI {
        
        if (![_peripheralArray containsObject:peripheral]) {
            NSLog(@"发现外设:%@", peripheral);
            //打印结果:
            //发现外设:<CBPeripheral: 0x15f5bac80, identifier = 955A5FFD-790E-BA3C-2A94-29FEA8A14A58, name = TF1603(BLE), state = disconnected>
            //发现设备后,需要持有他
            [_peripheralArray addObject:peripheral];
            // NSLog(@"信号强度:%@", RSSI);
            //如果之前调用扫描外设的方法时,设置了相关参数,只会扫描到指定设备,可以考虑自动连接
            //[_cbManager connectPeripheral:peripheral options:nil];
            //刷新表格显示扫描到的设备
            [tableView reloadData];
            //如果这是单独封装的类,这里需要用代理或block或通知传值给控制器来刷新视图
        }
    }
    
    //外设连接成功时调用
    - (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral {
        NSLog(@"连接成功");
        //将连接的设备添加到_connectedPeripheralArray
        [self.connectedPeripheralArray addObject: peripheral];
        //如果tableView展示的是已经连接的设备
        //[tableView reloadData];
        //如果这是单独封装的类,这里需要用代理或block或通知传值给控制器来刷新视图
        //设置外设代理
        peripheral.delegate = self;
        //搜索服务
        [peripheral discoverServices:nil];
    }
    //外设连接失败时调用
    - (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error {
        NSLog(@"连接失败,%@", [error localizedDescription]);
    }
    //断开连接时调用
    - (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error {
        NSLog(@"断开连接");
        //移除断开的设备
        [_connectedPeripheralArray removeObject:peripheral];
        //这里可以进行一些操作,如之前连接时,监听了某个特征的通知,这里可以取消监听
    }
    
    3.2.2 CBPeripheralDelegate
    //发现服务时调用
    - (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error {
        if (error) {
            NSLog(@"%@发现服务时出错: %@", peripheral.name, [error localizedDescription]);
            return;
        }
        //遍历外设的所有服务
        for (CBService *service in peripheral.services) {
            NSLog(@"外设服务: %@", service);
            //打印结果
            //外设服务: <CBService: 0x15f5d0be0, isPrimary = YES, UUID = 6666>
            //外设服务: <CBService: 0x15f5d1f60, isPrimary = YES, UUID = 7777>
            //我定义了两个宏
            //#define SeriveID6666 @"6666" 
            //#define SeriveID7777 @"7777"
            //每个服务又包含一个或多个特征,搜索服务的特征
            [peripheral discoverCharacteristics:nil forService:service];
        }
    }
    //发现特征时调用,由几个服务,这个方法就会调用几次
    - (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error {
        if (error) {
            NSLog(@"扫描特征出错:%@", [error localizedDescription]);
            return;
        }
        //获取Characteristic的值
        for (CBCharacteristic *characteristic in service.characteristics) {
            NSLog(@"服务server:%@ 的特征:%@, 读写属性:%ld", service.UUID.UUIDString, c, c.properties);
            //第一次调用,打印结果:
            //服务server:6666 的特征:<CBCharacteristic: 0x135e37410, UUID = 8888, properties = 0xDE, value = (null), notifying = NO>, 读写属性:->222
            //如果一个服务包含多种特征,会循环打出其他特征,我的设备正好一个服务只包含一个特征,处理起来方便了许多.
            //第二次调用,打印结果:
            //服务server:7777 的特征:<CBCharacteristic: 0x135e3d260, UUID = 8877, properties = 0x4E, value = (null), notifying = NO>, 读写属性:->78
            //特征也用UUID来区分,注意和service的UUID区别开来
            //和厂家确认,server(uuid=6666)的特征characteristic(uuid=8888)是监控蓝牙设备往APP发数据的,
            //server(uuid=7777)的特征characteristic(uuid=8877)向蓝牙发送指令的.
            //对特征定义宏
            //#define CID8888 @"8888"  //读指令(监控蓝牙设备往APP发数据),6666提供
            //#define CID8877 @"8877"  //APP向蓝牙发指令,7777提供
            //UUID=8888的特征有通知权限,在我的项目中是实时接收蓝牙状态发送过来的数据
            if ([service.UUID.UUIDString isEqualToString:SeriveID6666]) {
                for (CBCharacteristic *c in service.characteristics) {
                    if ([c.UUID.UUIDString isEqualToString:CID8888]) {
                        //设置通知,接收蓝牙实时数据
                        [self notifyCharacteristic:peripheral characteristic:c];
                    }
                }
            }
            if ([service.UUID.UUIDString isEqualToString:SeriveID7777]) {
                [peripheral readValueForCharacteristic:characteristic];
                //获取数据后,进入代理方法:
                //- peripheral: didUpdateValueForCharacteristic: error:
                //根据蓝牙协议发送指令,写在这里是自动发送,也可以写按钮方法,手动操作
                //我将指令封装了一个类,以下三个方法是其中的三个操作,具体是啥不用管,就知道是三个基本操作,返回数据后,会进入代理方法
                //校准时间
                [CBCommunication cbCorrectTime:peripheral characteristic:characteristic];
                //获取mac地址
                [CBCommunication cbGetMacID:peripheral characteristic:characteristic];
                //获取脱机数据
                [CBCommunication cbReadOfflineData:peripheral characteristic:characteristic];
            }
        }
        //描述相关的方法,代理实际项目中没有涉及到,只做了解
        //搜索Characteristic的Descriptors
        for (CBCharacteristic *characteristic in service.characteristics){
            [peripheral discoverDescriptorsForCharacteristic:characteristic];
          //回调方法:
          // - peripheral: didDiscoverDescriptorsForCharacteristic: error:;
          //还有写入读取描述值的方法和代理函数
        }
    }
    
    #pragma mark - 设置通知
    //设置通知
    -(void)notifyCharacteristic:(CBPeripheral *)peripheral
                 characteristic:(CBCharacteristic *)characteristic{
        
        if (characteristic.properties & CBCharacteristicPropertyNotify) {
            [peripheral setNotifyValue:YES forCharacteristic:characteristic];
        //设置通知后,进入代理方法:
        //- peripheral: didUpdateNotificationStateForCharacteristic: characteristic error:
        }
    }
    //取消通知
    -(void)cancelNotifyCharacteristic:(CBPeripheral *)peripheral
                       characteristic:(CBCharacteristic *)characteristic{
        [peripheral setNotifyValue:NO forCharacteristic:characteristic];
    }
    
    //设置通知后调用,监控蓝牙传回的实时数据
    - (void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
        if (error) {
            NSLog(@"错误: %@", error.localizedDescription);
        }
        if (characteristic.isNotifying) {
            [peripheral readValueForCharacteristic:characteristic];
              //获取数据后,进入代理方法:
              //- peripheral: didUpdateValueForCharacteristic: error:
        } else {
            NSLog(@"%@停止通知", characteristic);
        }
    }
    
    3.2.3 向蓝牙发送指令,接收数据,解析

    app向蓝牙发送指令(这是我们设备的一个指令,由于现在iOS不能直接获取蓝牙mac地址了,我们设备的厂家就写了一个指令来获取,这个指令是自定义的,不适用于其他设备,方法通用)


    bluetooth03.png
    //CBCommunication.m(封装协议指令,类方法)
    //获取mac地址
    + (void)cbGetMacID:(CBPeripheral *)peripheral characteristic:(CBCharacteristic *)characteristic {
        NSLog(@"MAC地址");
        Byte b[] = {0xA0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA0};
        NSData *data = [NSData dataWithBytes:&b length:8];
        [CBCommunication writePeripheral:peripheral characteristic:characteristic value:data];
    }
    //通用发送指令方法
    + (void)writePeripheral:(CBPeripheral *)p
             characteristic:(CBCharacteristic *)c
                      value:(NSData *)value {
        //判断属性是否可写
        if (c.properties & CBCharacteristicPropertyWrite) {
            [p writeValue:value forCharacteristic:c type:CBCharacteristicWriteWithResponse];
        } else {
            NSLog(@"该属性不可写");
        }
    }
    

    特征CBCharacteristic中有一个属性是properties,是CBCharacteristicProperties类型的,我这里叫他权限,有read,write,notify,indicate等,这是一个可位移操作的枚举,如第一次打印出来的properties = 0xDE,转为二进制1101 1110

    typedef NS_OPTIONS(NSUInteger, CBCharacteristicProperties) {
        CBCharacteristicPropertyBroadcast                                               = 0x01,
        CBCharacteristicPropertyRead                                                    = 0x02,
        CBCharacteristicPropertyWriteWithoutResponse                                    = 0x04,
        CBCharacteristicPropertyWrite                                                   = 0x08,
        CBCharacteristicPropertyNotify                                                  = 0x10,
        CBCharacteristicPropertyIndicate                                                = 0x20,
        CBCharacteristicPropertyAuthenticatedSignedWrites                               = 0x40,
        CBCharacteristicPropertyExtendedProperties                                      = 0x80,
        CBCharacteristicPropertyNotifyEncryptionRequired NS_ENUM_AVAILABLE(NA, 6_0)     = 0x100,
        CBCharacteristicPropertyIndicateEncryptionRequired NS_ENUM_AVAILABLE(NA, 6_0)   = 0x200
    };
    

    对比发现,这个属性包含的权限可读写

    CBCharacteristicPropertyRead
    CBCharacteristicPropertyWriteWithoutResponse
    CBCharacteristicPropertyWrite 
    CBCharacteristicPropertyNotify
    CBCharacteristicPropertyAuthenticatedSignedWrites
    CBCharacteristicPropertyExtendedProperties
    

    有write时才可写,即向蓝牙发送指令CBCharacteristicPropertyWriteWithoutResponse写入有反馈,CBCharacteristicPropertyWrite写入后无反馈.反馈即提示是否写入成功,代理方法实现

    //用于检测中心向外设写数据是否成功
    - (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
        if (error) {
            NSLog(@"APP发送数据失败:%@",error.localizedDescription);
        } else {
            NSLog(@"APP向设备发送数据成功");
        }
    }
    
    //蓝牙接收到外设发来的数据时调用,数据主要在这里解析
    - (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
        //characteristic.value是NSData类型的,具体开发时,会根据开发协议去解析数据
        NSData *originData = characteristic.value;
        NSLog(@"获取外设发来的数据:%@",originData);
        //根据协议解析数据
        //因为数据是异步返回的,我并不知道现在返回的数据是是哪种数据,返回的数据中应该会有标志位来识别是哪种数据;
        //如下图,我的设备发来的是8byte数据,收到蓝牙的数据后,打印characteristic.value:
        //获取外设发来的数据:<0af37ab219b0>
        //解析数据,判断首尾数据为a0何b0,即为mac地址,不同设备协议不同
    }
    
    bluetooth03.png

    基本的蓝牙功能就可以实现啦!

    4.改进

    4.1 优化搜索方案
    _cbManager = [[CBCentralManager alloc] initWithDelegate:self queue:dispatch_get_main_queue() options:@{CBCentralManagerScanOptionAllowDuplicatesKey:@(NO)];
    

    扫描设备时,不扫描到相同设备,这样可以节约电量,提高app性能.如果需求是需要实时获取设备最新信息的,那就需要设置为YES.

    4.2扫描和连接指定设备
    //如果只想扫描到特定设备,可以用以下方法,前提是你知道服务的uuid.能够提供uuid为SeriveID6666(定义的宏)和SeriveID7777服务的设备.uuid可以由厂家提供,也可以先扫描所有设备,来获取服务.
    //包含一个符合服务的设备即可被搜索到
    CBUUID *uuid01 = [CBUUID UUIDWithString:SeriveID6666];
    CBUUID *uuid02 = [CBUUID UUIDWithString:SeriveID7777];
    NSArray *serives = [NSArray arrayWithObjects:uuid01, uuid02, nil];
    [_cbManager scanForPeripheralsWithServices:serives options:nil];
    
    4.3性能优化

    当扫描到或连接到指定设备后,取消扫描

    5 参考资料

    http://liuyanwei.jumppo.com/2015/08/14/ios-BLE-2.html

    相关文章

      网友评论

      • 轮子糙:最后连接指定设备中的UUID,每台手机连接同一设备获取的也不一致,那怎么确定这是指定的我们想连的外设,换个手机就不灵了
      • Aikesi26:你好,我想问问最大每次可以写多少字节到外设
      • Andy0528:您好,这个订阅和硬件上的notifying有关吗?我订阅的一个特征,为什么无法收到这个实时变化的value呢?只是在绑定特征的时候执行一次。
        c6e16b2e3a16:先确定一下特征是否可读
      • 叶舞清风:我的是开关灯
      • 33a02bf71691:你好,请问可以获取到的手机蓝牙地址吗?
        c6e16b2e3a16:@重复昵称 是的。我网上找的方法像“180A”试了不成功。
        33a02bf71691:@hello_me 我们做的是手机和卡片,你的意思是卡片要定义一个指令 然后手机发送这个指令获取蓝牙地址么
        c6e16b2e3a16:@重复昵称 iOS不能直接获取mac地址,我们的做法是给蓝牙发送一条获取mac地址的指令,蓝牙返回mac地址数据。指令需要蓝牙设备定义。

      本文标题:iOS蓝牙通讯开发

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