iOS开发问题集锦程序员

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

2019-03-25  本文已影响3人  青青河边草2041

[toc]

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

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

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

蓝牙层

BLE基础

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

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有如下特点:

包头

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

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特点如下:

传输层

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


VBDataBridge.png

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

发送类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状态,整个发送过程结束,桥接类将结果回调给应⽤层。

需注意细节如下:

拆包过程
@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过则判断当前接收数据长度是否小于目标数据长度,进而决定是继续接收数据还是组装数据回调给上层。

需注意以下细节:

组包过程
// 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
上一篇 下一篇

猜你喜欢

热点阅读