iOS 物联网iOS DeveloperiOS开发资料收集区

iOS蓝牙通讯开发

2016-06-24  本文已影响3105人  c6e16b2e3a16

刚开发完一款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

上一篇 下一篇

猜你喜欢

热点阅读