ios:和蓝牙过过招

图片来源于网络,侵权立删

概述

公司的项目是医疗类的项目,所以这段一直在和蓝牙打交道。我使用的是苹果原生的框架CoreBluetooth。在对接几个蓝牙设备的过程中,也遇到一些坑,下文我会一一列举。 git上有个库BabyBluetooth 基于原生CoreBluetooth框架进行了封装,使用起来也很方便,大家可以尝试一下。 那么我们开始吧!

正文

在了解下文内容之前,我已默认你已经了解一些基本概念:
  • 什么是中心设备
  • 什么是外围设备
  • 什么是服务(service)
  • 什么是特性(characteristic)
  • 什么是订阅(notify)
  • 什么是UUID
    ...
    基本了解了以上一些概念,下面的内容将比较好理解。

**

需要注明,下面的UUID是我的蓝牙设备中的Service和Characteristic的UUID,要注意根据自己的蓝牙
设备提供的Service和Characteristic的UUID来替换

**

// 蓝牙设备提供的服务的UUID
#define kCGMServiceTwoUUID        @"0000FFF0-0000-1000-8000-00805F9B34FB"

// 蓝牙设备提供的写入特性
#define kCGMCharacteristicOneUUID @"0000FFF1-0000-1000-8000-00805F9B34FB"

// 蓝牙设备提供的notify特性
#define kCGMCharacteristicTwoUUID @"0000FFF2-0000-1000-8000-00805F9B34FB"
那么,先让我们了解下蓝牙交互流程中几个常用的回调。
  • 中心设备CBCentralManager更新设备蓝牙状态的回调
  - (void)centralManagerDidUpdateState:(CBCentralManager *)central
{
    switch (central.state) {
        case CBCentralManagerStatePoweredOn:
        {
            // 扫描外围设备
            [self.centeralManager scanForPeripheralsWithServices:nil options:nil];
        }
            break;
            
        default:
            NSLog(@"设备蓝牙未开启");
            break;
    }
}
  • 中心设备已经发现外围设备回调

**
这里有几个问题值得注意:
**

1. 在ios中蓝牙广播信息中通常会包含以下4种类型的信息。ios的蓝牙通信协议中不接受其他类型的广播信息。因此需要注意的是,如果需要在扫描设备时,通过蓝牙设备的Mac地址来唯一辨别设备,那么需要与蓝牙设备的硬件工程师沟通好:将所需要的Mac地址放到一下几种类型的广播信息中。通常放到kCBAdvDataManufacturerData这个字段中。
kCBAdvDataIsConnectable = 1;
kCBAdvDataLocalName = XXXXXX;
kCBAdvDataManufacturerData = <XXXXXXXX>;
kCBAdvDataTxPowerLevel = 0;

2. 设备的UUID(peripheral.identifier)是由两个设备的mac通过算法得到的,所以不同的手机连接相同的设备,它的UUID都是不同的,无法标识设备。

3. 苹果与蓝牙设备连接通信时,使用的并不是苹果蓝牙模块的Mac地址,使用的是苹果随机生成的十六进制码作为手机蓝牙的Mac与外围蓝牙设备进行交互。如果蓝牙设备与手机在一定时间内多次通信,那么使用的是首次连接时随机生成的十六进制码作为Mac地址,超过这个固定的时间段,手机会清空已随机生成的Mac地址,重新生成。也就是说外围设备是不能通过与苹果手机的交互时所获取的蓝牙Mac地址作为手机的唯一标识的。(这是在与写蓝牙设备的固件工程师联调时根据问题的现象推测的。至于苹果蓝牙通讯协议的底层是否确实完全像我所说的这样,希望了解的读者能提供帮助。在此先谢过。)


 - (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *,id> *)advertisementData RSSI:(NSNumber *)RSSI
{
NSLog(@"advertisementData.kCBAdvDataManufacturerData = %@", advertisementData[@"kCBAdvDataManufacturerData"]);
    _connectPeripheral = peripheral;
//    [self.centeralManager connectPeripheral:peripheral options:nil];
    
   if ([advertisementData[@"kCBAdvDataLocalName"] hasPrefix:@"SN"]){
        NSLog(@"已搜索到设备");
        NSLog(@"peripheral.identifier = %@  peripheral.name = %@", peripheral.identifier, peripheral.name);
        
        [_delegate getAdvertisementData:advertisementData andPeripheral:peripheral];
        
        [_peripheralArray addObject:peripheral];
    }
}
  • 中心设备设备连接成功回调
 - (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
{
    // 设备停止扫描
    [self.centeralManager stopScan];
    
    peripheral.delegate = self;
    
    dispatch_after(2, dispatch_get_main_queue(), ^{
        
        // 查找服务
        [_connectPeripheral discoverServices:@[[CBUUID UUIDWithString:kCGMServiceTwoUUID]]];
    });
}
  • 中心设备设备连接失败回调
 - (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error
{
    [_operationDelegate failToConnect];
}
  • 中心设备设备连接中断回调
 - (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error
{
    NSLog(@"连接断开 %@", [error localizedDescription]);
    [_operationDelegate disconnected];
}
  • 外围设备(CBPeripheral)发现服务(service)回调
  - (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error
{
    if (error) {
        // 输出错误信息
        NSLog(@"discoverServices.error============ %@", [error localizedDescription]);
        
        return;
    }
    
    // 遍历设备提供的服务
    for (CBService *service in peripheral.services) {
        NSLog(@"service.UUID = ------------- = %@", service.UUID.UUIDString);
        
        // 找到需要的服务,并获取该服务响应的特性
        if([service.UUID isEqual:[CBUUID UUIDWithString:kCGMServiceTwoUUID]]) {
            [service.peripheral discoverCharacteristics:nil forService:service];
            NSLog(@"开始查找cgm的characteristic");
        }
    }
}
  • 外围设备发现特性(characteristic)回调
  - (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
{
    if (error) {
        // 输出错误信息
        NSLog(@"discoverCharacteristics.error=========== %@", [error localizedDescription]);
        return;
    }
    
    // 遍历服务中的所有特性
    for (CBCharacteristic *characteristic in service.characteristics) {
        
        if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:kCGMCharacteristicOneUUID]]) {
            // 设置读写的特性
            _readAndWriteCharacteristic = characteristic;
        } else if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:kCGMCharacteristicTwoUUID]]) {
            // 设置需要订阅的特性
            _notifyCharacteristic = characteristic;
            [_connectPeripheral setNotifyValue:YES forCharacteristic:_notifyCharacteristic];
        }
    }
}
  • 外围设备数据更新回调, 可以在此回调方法中读取信息(无论是read的回调,还是notify(订阅)的回调都是此方法)
  - (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
{
    if (error) {
        // 输出错误信息
        NSLog(@"didupadteValueForCharacteristic error ============ %@", [error localizedDescription]);
        return;
    }
    NSLog(@"value ============= %@", characteristic.value);
    
    // 解析数据
    NSData *data = characteristic.value;
    
    // 将NSData转Byte数组
    NSUInteger len = [data length];
    Byte *byteData = (Byte *)malloc(len);
    memcpy(byteData, [data bytes], len);
    NSMutableArray *commandArray = [NSMutableArray arrayWithCapacity:0];
    // Byte数组转字符串
    for (int i = 0; i < len; i++) {
        NSString *str = [NSString stringWithFormat:@"%02x", byteData[i]];
        [commandArray addObject:str];
        NSLog(@"byteData = %@", str);
    }
    // 输出数据
    [_operationDelegate dataWithCharacteristic:commandArray]; 
}
  • 特性已写入外围设备的回调(如果写入类型为CBCharacteristicWriteWithResponse 回调此方法,如果写入类型为CBCharacteristicWriteWithoutResponse不回调此方法)
 - (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
{
    if (error) {
        NSLog(@"write.error=======%@",error.userInfo);
    }
    
    /* When a write occurs, need to set off a re-read of the local CBCharacteristic to update its value */
    
    // 读数据
    if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:kCGMCharacteristicOneUUID]]) {
        [self readCharacter];
    }
}
  • 外围设备订阅特征值状态改变成功的回调

需要注意的是这里是对kCGMCharacteristicOneUUID这个特性进行写入,这里之所以这样操作是因为我的蓝牙设备的蓝牙协议是这样定义的,所以这里不要照抄照搬,要按照你的蓝牙设备的通讯协议来确定,对哪一个特性进行read,对哪个特性进行write,以及对哪个特性进行设置Notify


  - (void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
    if (error) {
        NSLog(@"error = %@", [error localizedDescription]);
    }
    // 对特性kCGMCharacteristicTwoUUID设置notify(订阅),成功以后回调
    if ([characteristic.UUID.UUIDString isEqualToString:kCGMCharacteristicTwoUUID] && characteristic.isNotifying) {
        // 写数据 回调-didWriteValueForCharacteristic
        
        NSLog(@"写数据到cgm设备的characteristic = %@", _readAndWriteCharacteristic.UUID.UUIDString);
        [_operationDelegate writeCharacteristic];
    }
}
另外,除了回调以外,还有几个点需要注意:
  • 搜索外围设备
- (void)searchlinkDevice
{
   // 实现代理
   // 扫描设备
//    _centeralManager = [[CBCentralManager alloc] initWithDelegate:self
//                                                            queue:nil];
   
   if(self.centeralManager.state == CBCentralManagerStatePoweredOff) {
       // 蓝牙关闭的
       
   } else if(self.centeralManager.state == CBCentralManagerStateUnsupported) {
       // 设备不支持蓝牙
   } else if(self.centeralManager.state == CBCentralManagerStatePoweredOn ||
             self.centeralManager.state == CBCentralManagerStateUnknown) {
       
       // 开启的话开始扫描蓝牙设备
       [self.centeralManager scanForPeripheralsWithServices:nil options:nil];
       
       double delayInSeconds = 20.0;
       
       // 扫描20s后未扫描到设备停止扫描
       dispatch_time_t popTime =
       dispatch_time(DISPATCH_TIME_NOW,
                     (int64_t)(delayInSeconds * NSEC_PER_SEC));
       dispatch_after(popTime,
                      dispatch_get_main_queue(),
                      ^(void) {
           [self stopScan];
       });
   }
}
  • 对某个特性(characteristic)写入数据
 - (void)writeCharacter:(NSData *)data
{
    NSLog(@" characteristic.uuid = %@ data ==== %@", _readAndWriteCharacteristic.UUID.UUIDString, data);
    if ([_readAndWriteCharacteristic.UUID isEqual:[CBUUID UUIDWithString:kCGMCharacteristicOneUUID]]) {
        [_connectPeripheral writeValue:data forCharacteristic:_readAndWriteCharacteristic type:CBCharacteristicWriteWithResponse];
    } else {
        [_connectPeripheral writeValue:data
                        forCharacteristic:_readAndWriteCharacteristic
                                     type:CBCharacteristicWriteWithoutResponse];
    }
}
  • 读数据

需要注意的是这里读取蓝牙信息 (但并不是在返回值中接收,要在

  - (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error;

这个回调方法中接收)

 - (void)readCharacter
{
    [_connectPeripheral readValueForCharacteristic:_readAndWriteCharacteristic];
}

另外

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

推荐阅读更多精彩内容

  • 在写这个博客之前,空余时间抽看了近一个月的文档和Demo,系统给的解释很详细,接口也比较实用,唯独有一点,对于设备...
    木易林1阅读 3,217评论 3 4
  • 备注:下面说到的内容都基于蓝牙4.0标准以上,主要以实践为主。 ~ CoreBluetooth.framework...
    未_漆小七阅读 1,504评论 1 8
  • 今天下午,我要练轮滑,我和弟弟一块儿,结果弟弟不会滑,我背着手也能滑,看来弟弟要多加练习了。地上有好多树枝,真的是...
    mark7型阅读 140评论 0 0
  • (文/习酒镇赵半仙) 事后,我想作呕。 跟一个行为检点的男人酒后,在陌生的脏乱陌生女人的床上醒来的罪恶感差不多。 ...
    2632385d067a阅读 564评论 6 20