美文网首页iOS开发问题集锦程序员
iOS端智能硬件BLE通信技术实现

iOS端智能硬件BLE通信技术实现

作者: 青青河边草2041 | 来源:发表于2019-03-25 23:02 被阅读3次

    [toc]

    当前开发的智能硬件项目中涉及蓝牙通信的目前有三处:

    • 配网时手机端向硬件端请求获取wifi列表
    • 配网时手机将ssid、pwd、userid的信息告知硬件,同时硬件端告知配网结果
    • 特定模式下硬件端向手机端请求信息(涉及项目隐私隐去)

    本项目中BLE通信分三层设计:蓝牙层、传输层、应用层。

    • 蓝牙层:主要封装蓝牙基本的通信方式,包括蓝牙开启/关闭的通知、扫描、读数据等;
    • 传输层:按照智能硬件的BLE通信协议规范实现数据发送时的拆包和接收时的组包,借助蓝牙层实现基本的收发;
    • 应用层:对数据通信结果做封装,暴露给上层用户调用。

    通信过程中的基本数据结构有两类,Packet和Slice,其中Slice组成Packet,下文会做详细描述。

    蓝牙层

    BLE基础

    BLE是Bluetooth Low Energy——蓝牙低功耗技术的简称,基于蓝牙4.0规范实现。值得一提的是,基于4.0之前规范实现的蓝牙技术称为传统蓝牙。在BLE开发中,有两种角色:中央设备(Central, 如手机)和外围设备(Peripheral,如智能硬件)。
    BLE技术是基于GATT(Generic Attribute Profile,一种属性传输协议)进行通信的:

    • 每个GATT由完成不同功能的服务(Service)组成;
    • 每个Service由不同的特征(Characteristic)组成;
    • 每个Characteristic由一个value和一个或多个描述(Descriptor)组成。
      项目中智能硬件就是一个外围设备,它包括三个Service,其中UUID为0xFFF0的Service是我们要扫描的目标,该Service中包括了写、读和控制三个Characteristic。
    typedef NS_ENUM(NSInteger, VBUUIDType)
    {
        VBUUIDNone = 0,
        VBUUIDService = 0xFFF0,
        VBUUIDTxCharacteristic = 0xFFF1, // 手机向硬件发送BLE数据的链路
        VBUUIDRxCharacteristic = 0xFFF2, // 手机从硬件接收BLE数据的链路
        VBUUIDCtsCharacteristic = 0xFFF3, // 标识手机是否可以继续向硬件发送数据的链路,
    };
    

    BLE写操作

    BLE写操作是指手机端向硬件端发送数据,写操作可分为有响应和无响应两种,后者写数据较快。项目中手机给硬件发送数据是通过tx Characteristic链路来写的,并且是无响应式, 根据《PV1低功耗蓝牙通信架构模块设计文档》,写数据前cts和rx链路都必须确认处于开启状态,才能保证写数据后快速收到硬件的数据:

    - (BOOL)canSendDataForPeripheral:(CBPeripheral *)peripheral {
        BOOL isRxCharacterNotify = NO;
        BOOL isCtsCharacterNotify = NO;
        for (CBCharacteristic *character in [peripheral.services.firstObject characteristics]) {
            VBUUIDType uuidType = [VBUUIDUtil typeForUUID:character.UUID];
            switch (uuidType) {
                case VBUUIDRxCharacteristic:
                    isRxCharacterNotify = character.isNotifying;
                    break;
                case VBUUIDCtsCharacteristic:
                    isCtsCharacterNotify = character.isNotifying;
                default:
                    break;
            }
        }
        
        // 只有在cts和rx都开启的情况下,才能发送数据
        BOOL canSendData = isRxCharacterNotify && isCtsCharacterNotify;
        return canSendData;
    }
    
    // Method from VBDataSender class
    /// 发送当前数据
    - (void)sendCurrentPacket
    {
        if (_curPacketIndex >= _packetsToSend.count)
        {
            return;
        }
        NELogVerbose(@"发送第%ld个包", (_curPacketIndex+1));
        
        VBPacket *curPacket = _packetsToSend[_curPacketIndex];
        _state = VBSenderStateWritePackets;
        
        NSArray<NSData *> *slices = [curPacket splitIntoSlices];
        for (NSData *slice in slices)
        {
            [_peripheral writeValue:slice forCharacteristic:_writeCharacteristic type:CBCharacteristicWriteWithoutResponse];
        }
        
        _state = VBSenderStateWaitPacketAck;
        [self startTimer];
    }
    

    BLE读操作

    BLE读操作是指手机读取硬件指定Characteristic的值, 项目中是通过实现CBPeripheralDelegate的方法来读取值的,代码如下:

    // Method from VBBluetoothManager class
    // 3. 读取特征的值
    - (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
    {
        [[NSNotificationCenter defaultCenter] postNotificationName:VBBluetoothBLEDidReceivePeripheralResponse object:characteristic userInfo:nil];
    
        if (!characteristic.value)
        {
            return;
        }
        
        NELogVerbose(@"%s %@", __func__, characteristic.value);
        VBDataBridge *bridge = [self findDataBridgeByPeripheral:peripheral];
        if (bridge) {
            [bridge handlePeripheralResponse:characteristic.value];
        } else {
            NELogError(@"data bridge为空了....");
        }
    }
    

    BLE通知

    BLE通知是指硬件主动给手机发送数据,手机接收硬件rx链路的数据通过setNotify来实现,参考代码如下:

    // Method from VBBluetoothManager class
    - (void)constructDataBridges:(CBPeripheral *)peripheral {
        CBService *primaryService = peripheral.services.firstObject;
        NSArray<CBCharacteristic *> *characteristics = primaryService.characteristics;
        if (!characteristics)
        {
            return;
        }
        CBCharacteristic *txWriteCharacter;
        CBCharacteristic *rxReceiveCharacter;
        for (CBCharacteristic *character in characteristics)
        {
            VBUUIDType uuidType = [VBUUIDUtil typeForUUID:character.UUID];
            if (uuidType == VBUUIDNone) {
                break;
            } else if (uuidType == VBUUIDTxCharacteristic) {
                txWriteCharacter = character;
            } else {
                if (uuidType == VBUUIDRxCharacteristic) {
                    rxReceiveCharacter = character;
                }
                if (!character.isNotifying) {
                    [peripheral setNotifyValue:YES forCharacteristic:character];
                }
            }
        }
        // 省略
        ...
      }
    

    其他注意事项

    手机的读和写操作均通过调用Core Bluetooth框架中的API完成,项目中写数据分成两个步骤:

    1. 手机发送数据给硬件
    2. 硬件回复手机数据接收成功

    只有第2步硬件返回表示写数据成功的Ack才表示数据写成功。

    传输层

    传输层的主要作⽤用是接收来⾃自应⽤层的数据,经过拆包后,发送给智能硬件; 同
    时接收智能硬件回复的结果信息,组包后,结果回调给应⽤用层。

    传输层可以分为发送端和接收端两个部分,发送端负责发送相关命令数据给⾳箱,接收端负责接收硬件回复的结果信息,并通过接⼝回调给发送端。

    基本数据结构

    手机和硬件通信过程中有两个基本的数据结构:数据包(Packet)和切⽚ (Slice)。Packet规定了一个标准的数据结构,⽆论发送还是接收,均需要按照这个结构发送数据,Slice则是将Packet按照20字节等⻓长切分后的结果,设成20字节主要原因Android端同学说BLE写限制一次最多只能写20字节,但是经过我查找相关资料验证,iOS的限制受诸多因素影响,并且可以超过20字节,甚至达到100多字节,具体可参考这篇文章

    Packet

    一个Packet的结构包含包头Header和包体Payload两部分,示意图如下:


    8949C43A-FE55-419A-866D-7B6964E643CA.png

    本项目中和嵌入式协定Packet有如下特点:

    • 每个Packet的长度限制为3000字节,头部长度固定占用12字节,故包体Payload部分最多可有2988字节;
    • 每接收到一个Packet均需校验,同时回复该Packet(即Packet Ack),Packet Ack不超过20字节,因此发送Packet Ack时无需拆包;
    包头

    传输层报文的包头格式定义:

    typedef struct pkgTransHeader{
        Uint8 magic;
        Uint8 ver;
        Uint8 rev;
        Uint8 seq;
        Uint16 cmdid;
        Uint16 checksum;
        Uint32 length;
    } pkgTransHeaderSdef;
    
    56FEE129-44CE-49BF-A2A8-B9C2E6D3F185.png

    对应的OC实现类是VBTransportHeader:

    #import "VBTransportHeader.h"
    #import "VBPacketUtil.h"
    #import <NESafeKit/NSData+NESafeKit.h>
    
    UInt32 const VBTransportHeaderLength = 12;
    UInt8 const VBTransportHeaderOkMagic = 0xEF;
    UInt8 const VBTransportHeaderOkVer = 0x01;
    UInt8 const VBTransportHeaderOkRev = 0x00;
    static const UInt8 VBTransportHeaderDefaultSeq = 0x01;
    
    @interface VBTransportHeader ()
    {
        // 前导标识
        UInt8 _magic;       // 前导标识
        UInt8 _ver;         // 版本号
        UInt8 _rev;         // 保留
        UInt8 _seq;         // 序列号
        VBCmdId _cmdid;     // 命令号
        UInt32 _length;     // 包头+包体的长度
        UInt16 _checksum;   // 包体校验和
        NSData *_data;      // 包头数据
    }
    @end
    
    @implementation VBTransportHeader
    - (instancetype)init
    {
        self = [super init];
        if (self)
        {
            _magic = VBTransportHeaderOkMagic;
            _ver = VBTransportHeaderOkVer;
            _rev = VBTransportHeaderOkRev;
            _seq = VBTransportHeaderDefaultSeq;
            
        }
        return self;
    }
    
    @end
    
    

    其中通信的命令枚举定义如下:

    /**
     app和硬件通信的各个指令
    
     - VBCmdCentralToPeripheral: 中央设备(手机)向硬件发送数据
     - VBCmdPeripheralToCentral: 硬件向中央设备发送数据
     - VBCmdWifiConfigNetRequest: 手机向硬件发送配网请求
     - VBCmdWifiConfigNetResponse: 硬件回复配网结果响应
     - VBCmdNearbyWifiListRequest: 手机向硬件请求附近的wifi列表
     - VBCmdNearbyWifiListResponse: 硬件向手机回复获取的wifi列表结果
     - VBCmdLoopDataRequest: 透传,设备会将手机发送的包原封不动传回来
     - VBCmdLoopDataResponse:  透传的响应
     */
    typedef NS_ENUM(UInt16, VBCmdId)
    {
        VBCmdCentralToPeripheral = 0xE001,
        VBCmdPeripheralToCentral = 0xE002,
        VBCmdWifiConfigNetRequest = 0x1001,
        VBCmdWifiConfigNetResponse = 0x1002,
        VBCmdNearbyWifiListRequest = 0x1003,
        VBCmdNearbyWifiListResponse = 0x1004,
        VBCmdLoopDataRequest = 0x0100,
        VBCmdLoopDataResponse = 0x0101
    };
    
    包体

    传输层报文中根据传输的包体内容可以将报文划分为两类:

    • 链路控制报文,包体内容为链路控制响应码
    • 数据传输报文,包体内容为应用层协议包或者其分包

    链路控制报文与数据传输报文使用cmdid来区分:

    类型 cmdid
    链路控制报文 0xE001 手机向设备, 0xE002设备向手机
    数据报文 其他

    其中链路控制报文的ack/nack响应码定义如下:

    typedef struct ackbody
    {
        Uint8 ackCode; 
    }ackBodySdef;
    
    AF939E68-427E-46B9-9999-B29D68E105DD.png

    对应的OC枚举是VBTransportAck:

    typedef NS_ENUM(UInt8, VBTransportAck)
    {
        VBTransportAckOk = 0x00,
        VBTransportAckOtherError = 0x01,
        VBTransportAckReservedUsage = 0x02,
        VBTransportAckChecksumError = 0x03,
        VBTransportAckHeaderIncorrect = 0x04,
        VBTransportAckOutOfMemory = 0x05,
        VBTransportAckTimeoutReassemble = 0x06,
        VBTransportAckSequenceIncorrect = 0x07,
        VBTransportAckCmdidInconsistent = 0x08,
        VBTransportAckTimeoutReceive = 0x09,
    };
    
    Packet校验

    接收到一个Packet时,需要进行如下流程的校验:

    1. 是否是ack包
    2. 是ack包的情况下,依次检查头部的合法性,校验和的正确性
    3. 当条件1、2都满足时,还需校验ack是否是表示传输层正确接收的结果

    对应实现代码如下:

    /// 是否是ack回复
    ///
    /// - Parameter data: 待检验的数据
    /// - Returns: true是ack,false不是
    + (BOOL)isAck:(NSData *)data
    {
        if (data.length != [VBPacket packetAckSize])
        {
            return NO;
        }
        
        // 获取cmd, 第4和5字节是cmd
        VBCmdId cmd = 0;
        [data ne_getBytes:&cmd range:NSMakeRange(4, 2)];
        
        // 获取长度
        UInt32 length = 0;
        [data ne_getBytes:&length range:NSMakeRange(8, 4)];
        
        NSData *header = [data ne_subdataWithRange:NSMakeRange(0, VBTransportHeaderLength)];
        BOOL isValidHeader = [VBPacketUtil isValidHeader:header] && cmd == VBCmdPeripheralToCentral && length == [VBPacket packetAckSize];
        return isValidHeader;
    }
    
    /// 校验Packet是否有效
    ///
    /// - Parameter data: 外围设备返回的数据
    /// - Returns: true有效, false无效
    + (VBPacketCheckResult *)isValidPacket:(NSData *)data
    {
        VBPacketCheckResult *result = [VBPacketCheckResult new];
        if (!data || data.length < VBTransportHeaderLength)
        {
            return result;
        }
        
        // 检查头部是否合法
        NSData *headerData = [data ne_subdataWithRange:NSMakeRange(0, VBTransportHeaderLength)];
        if (![self isValidHeader:headerData])
        {
            return result;
        }
    
        // 检查长度是否合法
        VBTransportHeader *transportHeader = [[VBTransportHeader alloc] initWithHeaderData:headerData];
        if (!transportHeader || transportHeader.length != data.length)
        {
            return result;
        }
        
        // 检查校验和
        NSUInteger payloadLen = data.length - VBTransportHeaderLength;
        NSData *payload = [data ne_subdataWithRange:NSMakeRange(VBTransportHeaderLength, payloadLen)];
        result.valid = transportHeader.checksum == [self generateChecksumWithHeaderData:[transportHeader dataWithZeroChecksum] payload:payload];
        result.cmd = transportHeader.cmdid;
        result.code = payload;
        return result;
    }
    
    - (BOOL)isAckOk:(NSData *)data
    {
        if (![VBPacketUtil isAck:data])
        {
            return NO;
        }
        
        VBPacketCheckResult *packet = [VBPacketUtil isValidPacket:data];
        if (!packet.valid)
        {
            return NO;
        }
        
        VBTransportAck ack = VBTransportAckOk;
        [packet.code ne_getBytes:&ack length:sizeof(ack)];
        NSError *error = [NSError vb_errorWithBLEError:ack];
        NELogVerbose(@"%@",error.localizedDescription);
        return ack == VBTransportAckOk;
    }
    
    Slice

    项目中,手机与硬件的所有读写操作,我们都认为是Slice传输,Slice特点如下:

    • Slice组成Packet(有些Packet比较短,可能小于20字节,所以有时候一个Slice就是一个Packet)
    • Slice没有头部,在写数据时最长20字节,读数据时取决于硬件一次发的数据量

    传输层

    传输层的设计核心类图如下:


    VBDataBridge.png

    应用层需要发送数据,后者接收到的数据需要处理时,实际是通过VBDataBridge桥接类去进行相应的分发处理:

    • 发送数据时,桥接类调用自己持有的sender类去发送数据
    • 接收数据时,桥接类调用自己持有的receiver类处理数据,receiver处理完,将对应的结果回调给桥接类,桥接类再回调给上层应用层。
    发送类VBDataSender

    VBDataSender类的主要作用是接收通过桥接类转发的应用层字节流数据,然后切成Packet发送(即拆包);在收到硬件返回的结果后,结束整个发送过程,流程图如下:

    VBDataSender.png

    每步操作说明如下:

    1. sender初始时处于idle状态,此时处于空闲状态,没有数据发送;
    2. sender接收桥接类转发的字节流,进入split packet状态,切成N个符合规范的Packet,准备依次发送;
    3. sender发送Packet时,进入write packet状态,开始发送第i个Packet;
    4. 发送第i个Packet后,进入wait packet ack状态,等待该Packet的ack(该Packet的ack,实际是receiver在接收到Packet的ack后,通过桥接类接口告诉sender发送下一个包的过程);
    5. 发送第i个Packet成功后,重新进入write packet状态,发送第i+1个Packet;
    6. sender收到的ACK显示硬件收到的该Packet有误,视为第i个Packet发送失败,这时sender重新进⼊write packet状态,重新发送第i个Packet; 如果重试2次后都失败,则判定为整个发送过程失败,错误信息回调应⽤用层;
    7. sender在N个Packet都发送成功后,进⼊wait response状态等待硬件返回相应命令的结果;
    8. sender在收到硬件反馈的结果后回到idle状态,整个发送过程结束,桥接类将结果回调给应⽤层。

    需注意细节如下:

    • sender在等待每一个Packet的ack时,均需要设计超时时间,默认是3s。如果发送了了一个Packet后,等待了了3s后没有收到ACK,则判定为发送失败,尝试重新发送;
    • sender在等待硬件返回结果时,也需要设计超时时间,默认30s,如果等待30s后,没有收到响应结果,判定为整个发送过程失败,错误信息通过桥接类回调给应用层。
    拆包过程
    @implementation NSData (VBPacket)
    - (NSArray<VBPacket *> *)splitIntoPacketsWithCmdId:(VBCmdId)cmdId
    {
        NSMutableArray<VBPacket *> *packets = [NSMutableArray array];
        NSUInteger maxPayloadSize = VBPacketMaxPacketSize - VBTransportHeaderLength;
        NSUInteger packetNum = self.length == 0 ? 1 : (self.length / maxPayloadSize + (self.length % maxPayloadSize == 0 ? 0 : 1));
        for (int i = 0; i < packetNum; ++i)
        {
            NSUInteger length = 0;
            UInt8 seq = i + 1;
            if (i == packetNum - 1)
            {
                length = self.length - i * maxPayloadSize;
                seq = VBPacketLastSeq;
            }
            else
            {
                length = maxPayloadSize;
            }
            
            // 即使payload为空也可以发送数据,因为包头的data一定不为空
            NSData *payload = [self ne_subdataWithRange:NSMakeRange(i * maxPayloadSize, length)];
            VBPacket *packet = [[VBPacket alloc] initWithSeq:seq cmdId:cmdId payload:payload];
            [packets addObject:packet];
        }
        return [packets copy];
    }
    @end
    
    切片过程
    // Method from VBPacket class
    - (NSArray<NSData *> *)splitIntoSlices
    {
        NSMutableArray<NSData *> *slices = [NSMutableArray array];
        NSData *data = [self data];
        NSUInteger sliceNum = data.length / VBPackeMaxSliceSize + (data.length % VBPackeMaxSliceSize == 0 ? 0 : 1);
        for (int i = 0; i < sliceNum; ++i)
        {
            NSUInteger length = 0;
            if (i == sliceNum - 1)
            {
                length = data.length - i * VBPackeMaxSliceSize;
            }
            else
            {
                length = VBPackeMaxSliceSize;
            }
            NSData *sliceData = [data ne_subdataWithRange:NSMakeRange(i * VBPackeMaxSliceSize, length)];
            if (sliceData)
            {
                [slices addObject:sliceData];
            }
        }
        return [slices copy];
    }
    
    接收类VBDataReceiver

    VBDataReceiver类的主要作用是处理接收到的硬件数据,包括硬件回复的Packet ack和相应命令对应的响应结果,并把结果通过桥接类回调给上层应用层,接收流程图如下:

    VBBluetooth.png

    每步操作说明如下:

    1. 数据发送完毕等待接收数据的状态有两种:wait packet ack和wait packet response;
    2. 当前状态是wait packet ack时,receiver判断是不是表示成功接收的ack,是则通知发送端发送下一个数据包,否则返回错误回调给上层;
    3. 当前状态是wait packet response时,receiver判断当前待接收的Packet序号是否满足条件,满足才接收;
    4. 接收Packet的过程是组包的过程,首先会判断当前接收的数据是否含有Packet Header以及头部检查是否已经check过,未check过则从头部中获取要接收的目标数据的长度;
    5. 已经check过则判断当前接收数据长度是否小于目标数据长度,进而决定是继续接收数据还是组装数据回调给上层。

    需注意以下细节:

    • 手机端在接收到⾳箱返回的Packet ack后⽆无需再做回复;
    • 无论是⼿手机端还是硬件端,对于Packet的回复都在20字节之内,所以可以省去复杂的组包和拆包操作,发送⼀次即可;
    • 手机在接收硬件发送的一个Packet的过程中,会出现下述情况:假如头部指明了了待接收的Packet是150字节长,已经接收了7个Slice,共7 * 20 = 140字节⻓,最后一个Slice硬件发送过来的长度依旧是20字节长的情况,这个时候需要对最后一个Slice切割,前10个字节和前⾯面的140字节长组成一个完整的字节,后10个字节存储起来等待和后面的数据拼接。
    组包过程
    // Method from VBDataReceiver class
    - (void)processResponseData:(NSData *)data
    {
        NSMutableData *curSlice = [NSMutableData data];
        if (_lastSliceTailData.length > 0)
        {
            [curSlice appendData:_lastSliceTailData];
            // remove all data
            [_lastSliceTailData setData:[NSData new]];
        }
        [curSlice appendData:data];
        
        // 当前已接收数据和待接收数据总长度
        NSUInteger curLength = _curPacketLen + curSlice.length;
        VBHeaderCheckResult *checkResult = [VBPacketUtil containRespHeader:curSlice];
        if (!_hasCheckedHeader && checkResult.hasHeader)
        {
            _hasCheckedHeader = YES;
            _targetPacketLen = checkResult.length;
        }
        
        // 已接收和待接收的数据总长度小于目标接收长度
        if (curLength < _targetPacketLen)
        {
            [_receivedSlices appendData:curSlice];
            _curPacketLen = curLength;
        }
        else
        {
            NSUInteger wantingLen =  _targetPacketLen - _curPacketLen;
            NSData *frontData = [data ne_subdataWithRange:NSMakeRange(0, wantingLen)];
            if (frontData.length > 0)
            {
                [_receivedSlices appendData:frontData];
                [self collectPacket];
            }
            
            NSUInteger tailLen = data.length - wantingLen;
            NSData *tailData = [data ne_subdataWithRange:NSMakeRange(wantingLen, tailLen)];
            if (tailData.length > 0)
            {
                [_lastSliceTailData appendData:tailData];
            }
        } 
    }
    
    

    应用层

    应用层对数据收发的桥接类做进一步封装,类图设计如下:

    VBBluetoothManager.png

    以上就是智能硬件BLE通信技术的设计架构和大概实现,所有以和嵌入式端定义的协议文档为实现依据。

    参考

    1. PV1低功耗蓝牙通信架构模块设计文档
    2. Android端智能硬件XX BLE配网技术文档
    3. iOS Bluetooth Low Energy and Custom Hardware — Part 3: Optimizing Data Throughput

    相关文章

      网友评论

        本文标题:iOS端智能硬件BLE通信技术实现

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