iOS 蓝牙开发和注意点

前言

  • 蓝牙传输所用的框架是<CoreBluetooth/CoreBluetooth.h>
  • 蓝牙连接需要中心管理者和外部设备,我们所做的开发基本是围绕中心管理来的;
  • 蓝牙设备发过来的每个数据包,为了保证数据在传输的时候没有丢失,一般需要包头,包尾,校验和
  • 有很多蓝牙协议很复杂,需要把数据转化成二进制进行转化解析,对于高字节,低字节,小端模式,大端模式,符号位,位运算这些基本概念需要了解清楚

1.关于Mac地址的获取

自iOS7之后,苹果不支持获取Mac地址,只能用UUID来标识设备,要注意的是同一个设备在不同手机上显示的UUID不相同,但有的设备可以通过 “180A”这个服务来发现特征,再来读取 “2A23”这个特征值,可以获得Mac地址。如果你的蓝牙设备不支持这样获取,你可以跟硬件工程师沟通,来获得Mac地址,添加一个获取地址命令或者增加一个含地址的特征值都可以很容易的获取。上面获取地址的前提都是需要先建立连接,如果一定要在扫描的时候获得Mac地址,让硬件工程师把数据写入广播包里,看是否可行;

2.蓝牙连接流程

如果你不是新手,又不想浪费时间,请直接看第三点 注意点,核心部分

  • 建立中心设备管理者
  • 扫描外设
  • 连接外设
  • 扫描外设中的服务
  • 扫描外设中的特征
  • 订阅或读取特征值
  • 获取外设中的数据

建立中心设备管理者

 // 创建之后会马上检查蓝牙的状态,nil默认为主线程
self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil]

蓝牙线程没必要去开异步线程,在主线程消耗不了什么性能

扫描外设

// 蓝牙状态发生改变,这个方法一定要实现
- (void)centralManagerDidUpdateState:(CBCentralManager *)central
{
    // 蓝牙状态可用
    if (central.state == CBCentralManagerStatePoweredOn) {
        
        // 如果蓝牙支持后台模式,一定要指定服务,否则在后台断开连接不上,如果不支持,可设为nil, option里的CBCentralManagerScanOptionAllowDuplicatesKey默认为NO, 如果设置为YES,允许搜索到重名,会很耗电
        [self.centralManager scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:kServiceUUID]] options:nil];
    }
}

连接外设

 /**
 * 发现设备
 * @param peripheral 设备
 * @param advertisementData 广播内容
 * @param RSSI 信号强度
 */
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *,id> *)advertisementData RSSI:(NSNumber *)RSSI
{
    // 判断是否是你需要连接的设备
    if ([peripheral.name isEqualToString:kPeripheralName]) {
        peripheral.delegate = self;
        // 一定要记得把外设保存起来
        self.selectedPeripheral = peripheral;
        // 开始连接设备
        [self.centralManager connectPeripheral:self.selectedPeripheral options:nil];
    }
}

扫描外设中的服务

/**
 * 已经连接上设备
 */
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
{
    // 停止扫描
    [self.centralManager stopScan];
    // 发现服务
    [self.selectedPeripheral discoverServices:nil];
}

扫描外设中的特征

/**
 * 已经发现服务
 */
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error
{
    for (CBService *service in peripheral.services) {
        if ([service.UUID isEqual:[CBUUID UUIDWithString:kServiceUUID]]) {
            // 根据你要的那个服务去发现特性
            [self.selectedPeripheral discoverCharacteristics:nil forService:service];
        }
        
        // 这里我是根据 180A 用来获取Mac地址,没什么实际作用,可删掉
        if ([service.UUID isEqual:[CBUUID UUIDWithString:@"180A"]]) {
            [self.selectedPeripheral discoverCharacteristics:nil forService:service];
        }
    }
}

订阅或读取特征值

/**
 * 已经发现特性
 */
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
{
    for (CBCharacteristic *characteristic in service.characteristics) {
        if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:@"2A23"]]) {
            // 这里是读取Mac地址, 可不要, 数据固定, 用readValueForCharacteristic, 不用setNotifyValue:setNotifyValue
            [self.selectedPeripheral readValueForCharacteristic:characteristic];
        }
        
        
        if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:kCharacteristicUUID]]) {
            // 订阅特性,当数据频繁改变时,一般用它, 不用readValueForCharacteristic
            [peripheral setNotifyValue:YES forCharacteristic:characteristic];
            
            // 获取电池电量
            unsigned char send[4] = {0x5d, 0x08, 0x01, 0x3b};
            NSData *sendData = [NSData dataWithBytes:send length:4];
            
            // 这里的type类型有两种 CBCharacteristicWriteWithResponse CBCharacteristicWriteWithoutResponse,它的属性枚举可以组合
            [self.selectedPeripheral writeValue:sendData forCharacteristic:characteristic type:CBCharacteristicWriteWithoutResponse];
            
            /*
             characteristic 属性
             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
             };
             */
            
            NSLog(@"%@",characteristic);
            // 打印结果为 <CBCharacteristic: 0x1702a2a00, UUID = FFF6, properties = 0x16, value = (null), notifying = NO>
            
            //  我的结果 为 0x16  (0x08 & 0x16)结果不成立, (0x04 & 0x16)结果成立,那写入类型就是 CBCharacteristicPropertyWriteWithoutResponse
        }
    }
}

获取外设中的数据

/**
 * 数据更新的回调
 */
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
{
    // 这里收到的数据都是16进制,有两种转换,一种就直接转字符串,另一种是转byte数组,看用哪种方便
    
    // 直接转字符串
    NSString *orStr = characteristic.value.description;
    NSString *str = [orStr substringWithRange:NSMakeRange(1, orStr.length - 2)];
    NSString *dataStr = [str stringByReplacingOccurrencesOfString:@" " withString:@""];
    NSLog(@"dataStr = %@",dataStr);
    
    // 转Byte数组
    Byte *byte = (Byte *)characteristic.value.bytes;
    
    //_______________________________________________________________________________________________________________
    // 解析你的协议,附几个解协议或许能用到的函数
}

设备连接断开

- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error
{
    // 让它自动重连
    [self.centralManager scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:kServiceUUID]] options:nil];
}

这是系统代理方法,如果要主动断开需要调用 - (void)cancelPeripheralConnection:(CBPeripheral *)peripheral; 这个方法

写入数据成功的回调

- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
{
        // 读取数据
    [self.selectedPeripheral readValueForCharacteristic:characteristic];
}

如果类型是CBCharacteristicWriteWithoutResponse,不会走这个方法;

3.注意点,核心部分,请仔细看

  1. 做蓝牙前一定要去商城下个LightBlue,一个设备有很多服务,服务中又有很多特性,特性中又分读的,写的等,有了LightBlue,你可以很快的找到你需要的特性;

    LightBlue截图

    从上图中我们可以清晰的看到每个服务中又多少个特性,特性的属性Read、Write、Write Without Response、Notify等也标明的很清楚,

  2. 一般的蓝牙都要支持重连和后台运行,如果扫描设备的时候,用这个方法- (void)scanForPeripheralsWithServices:options:没有指定特定的服务,而是用nil代替,设备在后台断开的时候是不会重连的;

  3. 蓝牙是可以同时连接多个外部设备

  4. 关于readValueForCharacteristicsetNotifyValue:forCharacteristic: 的区别, readValueForCharacteristic适合用来读取数据不怎么更新的特征值, 如果获取的数据是经常更新的,那就 一定要用setNotifyValue:forCharacteristic:来订阅这个特征;

  5. 当我们写入命令时writeValue:forCharacteristic:type:,这个type类型到时是用CBCharacteristicWriteWithResponse还是用CBCharacteristicWriteWithoutResponse会有疑惑,先看一下特性属性的枚举,它们是可以组合的

        /*
          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
          };
    

再来看看我打印的两个特征值,第一个是获取Mac地址的特性,另一个是获取数据的特性
<CBCharacteristic: 0x1700b8ae0, UUID = System ID, properties = 0x2, value = (null), notifying = NO>
<CBCharacteristic: 0x1702a2a00, UUID = FFF6, properties = 0x16, value = (null), notifying = NO>
第一个0x2对应只可读, 第二个 (0x16 & 0x08)不成立,(0x16 & 0x04)成立,所以用CBCharacteristicWriteWithoutResponse,而且这个特征值还可读,可以通知

  1. 代理方法- (void)centralManagerDidUpdateState:(CBCentralManager *)central;一定要调用,否则会报错,这个方法只要设置中心设备的代理之后,就一定会走,我们最开始的扫描外设应放在这个方法里;

  2. 对于是否要单独创建一个工具类来获取蓝牙数据,如果只是一个界面需要用到蓝牙数据,我觉得完全没必要,如果是多个界面的话,最好还是创建一个工具类。

  3. 如果蓝牙支持要支持后台模式,只需要去把蓝牙后台模式打开

    后台运行蓝牙

    记住只要勾选Uses Bluetooth LE accessories就行了,别勾选Acts As a Bluetooth LE accessory,除非你把你的手机当做外部设备使用;

  4. 如果蓝牙设备是是手环或者手表,一般支持ANCS协议,使得可以收到苹果手机的来电、短信及各种应用的通知信息,但这是要求配对的,配对后的设备,只要你手机开着蓝牙,它都会自动连接,你会发现怎么都搜索不到设备,你每次连接设备前需要用retrieveConnectedPeripheralsWithServices方法来查看设备是否已经连接上;
    推荐的连接流程

    ReconnectingToAPeripheral_2x.png

简单又详细的Demo地址!

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

推荐阅读更多精彩内容