探究蓝牙 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)

二、实现步骤
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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容