美文网首页iOS CoreBluetooth
iOS 蓝牙4.0开发 --- 本机作为外围设备角色开发

iOS 蓝牙4.0开发 --- 本机作为外围设备角色开发

作者: roast_duck | 来源:发表于2017-11-29 15:27 被阅读18次

    文中???的地方表示还有待填坑,但是可以忽略。

    本机作为服务提供者。

    CBPeripheralManager对象表示外围设备(当前提供服务的app)。用于发布和广播服务,响应中心设备发来的读写请求。
    CBCentral对象表示中心设备。我们需要处理它发过来的request

    CBPeripheralManagerDelegate协议

    1. 创建外围设备管理对象

    /*
     @param options 一个字典值
     *  用于在实例化时,蓝牙断开是否提示用户,默认为NO
     *  CBPeripheralManagerOptionShowPowerAlertKey : NSNumber Bool
     *  用于唯一标识外围设备对象的字符串 : NSString * (16位或32位或者是蓝牙技术联盟规定的16位标识)
     *  CBPeripheralManagerOptionRestoreIdentifierKey
     */
    self.peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil options:nil];
    

    创建好后调用代理方法检查蓝牙状态

    /**
     *  成功实例化一个外围设备对象时调用,确定设备是否支持蓝牙以及是否可用
     */
    - (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral {
        // 判断状态
    }
    

    状态参数具体查看相关概念章节中的CBManagerState部分

    2. 创建特征

    /**
     创建一个特征
     */
    - (CBMutableCharacteristic *)createCharacteristicWithUUID:(CBUUID *)uuid {
        CBMutableCharacteristic *characteristic =
          [[CBMutableCharacteristic alloc] initWithType:uuid
            properties:
             CBCharacteristicPropertyRead | CBCharacteristicPropertyWrite
            value:[@"songyang" dataUsingEncoding:NSUTF8StringEncoding]
            permissions:CBAttributePermissionsReadable];
            
        return characteristic;
    }
    

    UUID:特征的唯一标识,UUID相关请参看相关概念章节
    properties:特征的属性,具体查看相关概念章节中的CBCharacteristicProperties部分
    value:特征的值,如果在此处设置了值,那么值会被缓存并且properties和ermissions被设为只读。如果需要值可写或者值能动态变化,就需要实例化时置value为nil。
    permissions:特征的值的权限。具体查看相关概念章节中的CBAttributePermissions部分

    Tip:两个特征的建议配置

    • 订阅方式能使中心设备以较节能的方式获取值,这样外围设备也能做到在需要的时候才响应请求。所以苹果鼓励使用CBCharacteristicPropertyNotify
    • 为了保证安全,建议使用CBCharacteristicPropertyNotifyEncryptionRequired或者CBCharacteristicPropertyIndicateEncryptionRequired,只允许受信任设备交互,尤其是特征的值能被写入的情况。这样才会在链接时看到是否允许配对的界面框???。
    • 另外要记得保持一致。总不能properties设了只读,然后pemissions又设成可写吧。

    3. 创建服务

    /**
     创建一个服务
    
     @param uuid 唯一标识
      param primary YES为主服务,用于描述一个设备的主要功能。主服务可以被其他服务引用。
                    NO为次要服务,用于描述一个被它引用的其他服务的相关功能
                    如计步器主要服务是记录步数,次要服务可以是记录时间,距离等。
     */
    - (CBMutableService *)createServiceWithUUID:(CBUUID *)uuid {
        CBMutableService *service = [[CBMutableService alloc] initWithType:uuid primary:YES];
        return service;
    }
    

    将服务和特征进行关联

    // 一个服务可以有多个特征
    service.characteristics = @[characteristic];
    

    4. 发布服务

    /**
     发布服务
     * 一旦发布服务,服务和它的特征会被缓存,服务不能再被修改
     */
    [self.peripheralManager addService:service];
    

    添加服务后调用代理

    - (void)peripheralManager:(CBPeripheralManager *)peripheral didAddService:(CBService *)service error:(NSError *)error {
        // 处理后续或错误
    }
    

    5. 向外广播服务

    /**
     广播部分服务和特征。
     */
    - (void)adviseServices:(NSArray *)services {
        for (CBMutableService *service in services) {
            // 广播数据
            [self.peripheralManager startAdvertising:@{CBAdvertisementDataServiceUUIDsKey :@[service.UUID]}];
        }
    }
    

    startAdvertising:的参数advertisementData是一个字典值,其中包含要广播出去的数据和它对应的key。需要注意的iOS中这些数据不仅对大小有限制,数据内容也有。比如广告包中虽然可以包含外围设备的很多信息,但是iOS中只能传递以下两个:

    // CBPeripheralManager 仅支持两个key:
    * `CBAdvertisementDataLocalNameKey` : value = 服务名称
    * `CBAdvertisementDataServiceUUIDsKey` : value = 服务UUID数组
    

    对于大小限制,一个广告包为31个字节,除去必要的2个字节作为头信息(数据段的长度和类型),剩下的为数据段。如果app正在运行,以上两个key代表的信息大小限制为28个字节;如果大小不够,在响应包中还能额外使用10个字节,但是仅能用于传递服务名字。
      不符合已分配空间的UUID会加入一个特殊的“溢出”包,该类型数据包只能被iOS设备扫描???(此处还需填坑)。
    如果app在后台运行,则只能通过“溢出”包广播服务的UUID。

    蓝牙数据包格式:Bluetooth 4.0 specification, Volume 3, Part C, Section 11
    参考链接:https://www.cnblogs.com/smart-mutouren/p/5882038.html


    开始广播服务后,会调用代理

    - (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral error:(NSError *)error {
        // 处理后续或错误
    }
    

    接下来就是等待中心设备扫描和连接。当连接建立后,就可以收到中心设备的请求并开始处理(通过代理方法)。

    一旦链接建立,外围设备就不需要再广播广播包。因为设备间可以直接进行数据交互(交互方式???)。此时为了节省电量,应该及时停止广播:

    [self.peripheralManager stopAdvertising];
    

    Tip

    • 什么时候需要广播广播包?需要和中心设备连接的时候,如果有需要,甚至可以在创建好CBPeripheralManager对象后就开始广播。不过扮演外围角色的app并没有探测其他设备的方式,而用户是能直接知道的。所以最好提供一个界面让用户自己选择是否startAdvitising:
    • 发布服务和广播服务都必须在powered on状态下进行。
    • 如果此时app进入后台,而我们又没有进行后台处理,广播会停止。

    6. 响应中心设备发来的读写请求

    响应读取请求(两种方式)
    • 中心设备调用readValueForCharacteristic发送读取请求,外围设备收到后,会调用代理
    /**
     收到读取数据的请求
     @param request 读数据的请求,已被包装过(CBATTRequest对象)
     */
    - (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveReadRequest:(CBATTRequest *)request {
        // 根据请求中的信息进行响应
        // 确认请求的特征UUID是否存在
        if ([request.characteristic.UUID isEqual:self.characteristic.UUID])
            {   }
        // 检查读取的偏移量(从哪开始读)是否超出本地数据长度
        if (request.offset > self.characteristic.value.length) {
            // 超出则返回越界错误
            [self.eripheralManager respondToRequest:request withResult:CBATTErrorInvalidOffset];
            return;
        }
        // 从请求的偏移量开始读取之后的数据
        request.value = [self.characteristic.value subdataWithRange:NSMakeRange(request.offset, self.characteristic.value.length - request.offset)];
        // 更新请求的值后,向中心设备回传请求结果(必须调用,成功则返回数据,失败返回错误)
        [self.peripheralManager respondToRequest:request withResult:CBATTErrorSuccess];
    }
    
    • 中心设备通过订阅方式读取数据
    /**
     收到中心设备(setNotifyValue:YES forCharacteristic:)调用该代理
     */
    - (void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central didSubscribeToCharacteristic:(CBCharacteristic *)characteristic {
        // 修改特征值
        // centrals 为nil,表示向所有订阅了的中心设备发通知。当然也可以通过数组指定通知
        // 当发送通知的队列不足时,该方法返回NO;此时进入等待,当有可用队列时会触发代理peripheralManagerIsReadyToUpdateSubscribers:
        BOOL didSendValue = [myPeripheralManager updateValue:updatedValue forCharacteristic:characteristic onSubscribedCentrals:nil];
        if (!didSendValue) {
            // 将未发送的特征保存,之后重新发送
            [self.shouldSendArray addObject:characteristic];
        }
    }
    /**
     有可用队列时,调用该代理继续发送通知
     */
    - (void)peripheralManagerIsReadyToUpdateSubscribers:(CBPeripheralManager *)peripheral {
    
        BOOL didSendValue = [self.peripheralManager updateValue:updatedValue forCharacteristic:characteristic onSubscribedCentrals:nil];
    }
    /**
     收到中心设备(setNotifyValue:NO forCharacteristic:)取消订阅特征的请求
     */
    - (void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central didUnsubscribeFromCharacteristic:(CBCharacteristic *)characteristic {
    
    }
    
    响应写入请求

    收到该请求,调用代理

    /**
     收到中心设备写入数据的请求
     @param requests 写数据的请求,已被包装过。注意这里是数组,可能包含多个写请求
     */
    - (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveWriteRequests:(NSArray<CBATTRequest *> *)requests {
        // 确认权限后,能写则写。写的时候也需要考虑偏移量
        myCharacteristic.value = request.value;
        // 如果所有请求都被完成,回传写入成功。随便取一个request
        // 如果数组中有任意一个请求完成失败,那么后续请求都不用再响应。直接将失败的原因回传
        // 该方法必须调用
        [self.peripheralManager respondToRequest:requests.firstObject withResult:CBATTErrorSuccess];
    }
    

    7. 调试问题

    笔者在设备调试时,曾一直卡在以下问题上。
    现填坑如下:

    1. XPC Connection invalid
        CBPeripheralManager对象需要被全局持有(使用属性)。如果你将Manager封装在B类中,然后在C类中使用;那么C类也需要全局持有B类。原因是CBPeripheralManager对象(包括CBCentralManager对象)是异步创建的,创建成局部对象会很快被释放。
      参考链接:@蒋小飞http://www.jianshu.com/p/ec659ffcacfe的“编码”部分。
    2. 设备不支持(state:unsupported)
        这个一方面可能是你的设备真的不支持BLE(通过你的设备信息去了解),另一方面可能是项目配置问题。
        笔者使用mac app项目作为外部设备,然后用真机作为中心设备。运行mac app时一直出现以上两个问题。解决如下:(运行环境macOS 10.13,xcode 9.2 beta)
      Targets->Capabilities中勾选

    相关文章

      网友评论

        本文标题:iOS 蓝牙4.0开发 --- 本机作为外围设备角色开发

        本文链接:https://www.haomeiwen.com/subject/onaubxtx.html