探究蓝牙|探究蓝牙 CoreBluetooth 实现数据传输

最近项目中需要与硬件进行蓝牙连接, 实现数据交互.
一般来说, 外设会由硬件工程师开发好,并定义好设备提供的服务, 每个服务对于的特征, 每个特征的属性(只读, 只写, 通知等等). 本文例子的业务场景,就是用一手机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)

【探究蓝牙|探究蓝牙 CoreBluetooth 实现数据传输】二、实现步骤
1 . 导入 CoreBluetooth 头文件 #import , 建立主设备管理类, 设置主设备委托
#import @interface CentralVewController () { //系统蓝牙设备管理对象, 可以把他理解为主设备, 通过他, 可以去扫描和链接外设 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 = https://www.it610.com/article/[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 [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 = https://www.it610.com/article/%@,fromCharacteristic.UUID = %@",characteristic.value,characteristic.UUID); NSData *data = characteristic.value; //特征的值 NSString *cValueStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; NSLog(@"--- 读取回调特征值 receiveData = https://www.it610.com/article/%@",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

    推荐阅读