蓝牙系列八、数据封装和分包

数据封装

iOS 蓝牙框架默认会进行发送数据的分包,默认20字节一个包,但是传输大一点的数据还是会被截断。这种情况下,需要开发者自行控制分包。虽然接收端按序接收数据,但为了合并数据的方便,一般要将数据按照一定的格式进行封装,然后再分包发送。

一种简单的封装格式

在Apple Documents里有一个叫BTLE的Demo。它通过数据末尾添加EOM标识符来表示一个包数据的结束。

img

如上图所示,模拟命令分成15个包传输过来,每个包大小固定位最大20个字符,第1-14包为模拟命令数据,第十五个包只发送了一个EOM字符串,接收端每接收到一个包则把这个数据追加到上一个包数据上,直到收到EOM标识,则把当前收到的所有数据进行解析,解析完成就可以进行下一步业务处理了。

收包关键代码

//更新特征值后(调用readValueForCharacteristic:方法或者外围设备在订阅后更新特征值都会调用此代理方法)
-(void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error{
    if (error) {
        NSLog(@"更新特征值时发生错误,错误信息:%@",error.localizedDescription);
        [self writeToLog:[NSString stringWithFormat:@"更新特征值时发生错误,错误信息:%@",error.localizedDescription]];
        return;
    }
    if (characteristic.value) {
        NSString *value=[[NSString alloc]initWithData:characteristic.value encoding:NSUTF8StringEncoding];
        NSLog(@"读取到特征值:%@",value);
        [self writeToLog:[NSString stringWithFormat:@"读取到特征值:%@",value]];
        if ([value isEqualToString:@"EOM"]) {
            //处理数据
            [self.delegate didReceiveData:self.data complate:YES];

            //处理完毕,清空
            [self.data setLength:0];
        } else {
            [self.data appendData:characteristic.value];
            if(self.delegate) {
                [self.delegate didReceiveData:self.data complate:NO];
            }
        }
    } else {
        [self writeToLog:@"未发现特征值."];
    }
}

发包关键代码:

/** Sends the next amount of data to the connected central
 */
- (void)_sendData
{
    // First up, check if we're meant to be sending an EOM
    static BOOL sendingEOM = NO;

    if (sendingEOM) {
        // send it
        BOOL didSend = [self.peripheralManager updateValue:[@"EOM" dataUsingEncoding:NSUTF8StringEncoding] forCharacteristic:self.characteristicM onSubscribedCentrals:nil];

        // Did it send?
        if (didSend) {
            // It did, so mark it as sent
            sendingEOM = NO;
            NSLog(@"Sent: EOM");
        }

        // It didn't send, so we'll exit and wait for peripheralManagerIsReadyToUpdateSubscribers to call sendData again
        return;
    }

    // We're not sending an EOM, so we're sending data
    // Is there any left to send?

    if (self.sendDataIndex >= self.dataToSend.length) {

        // No data left.  Do nothing
        return;
    }

    // There's data left, so send until the callback fails, or we're done.

    BOOL didSend = YES;

    while (didSend) {
        // Make the next chunk
        // Work out how big it should be
        NSInteger amountToSend = self.dataToSend.length - self.sendDataIndex;

        // Can't be longer than 20 bytes
        if (amountToSend > NOTIFY_MTU) amountToSend = NOTIFY_MTU;

        // Copy out the data we want
        NSData *chunk = [NSData dataWithBytes:self.dataToSend.bytes+self.sendDataIndex length:amountToSend];

        // Send it
        didSend = [self.peripheralManager updateValue:chunk forCharacteristic:self.characteristicM onSubscribedCentrals:nil];

        // If it didn't work, drop out and wait for the callback
        if (!didSend) {
            return;
        }

        NSString *stringFromData = [[NSString alloc] initWithData:chunk encoding:NSUTF8StringEncoding];
        NSLog(@"Sent: %@", stringFromData);

        // It did send, so update our index
        self.sendDataIndex += amountToSend;

        // Was it the last one?
        if (self.sendDataIndex >= self.dataToSend.length) {
            // It was - send an EOM
            // Set this so if the send fails, we'll send it next time
            sendingEOM = YES;

            // Send it
            BOOL eomSent = [self.peripheralManager updateValue:[@"EOM" dataUsingEncoding:NSUTF8StringEncoding] forCharacteristic:self.characteristicM onSubscribedCentrals:nil];

            if (eomSent) {
                // It sent, we're all done
                sendingEOM = NO;
                NSLog(@"Sent: EOM");
            }
            return;
        }
    }
}

TLV

EOM方式封装数据量较大(比如发送数字1,也要单独占一个序号,即20字节)。在我们的App中蓝牙通信控制协议基于 TLV 格式和大端网络字节序。

TLV定义: Type Length Value

  • Type:类型

  • Length: 长度(Value 的长度,不包含 Type 和 Length,如 Value 为空,Length 为 0)

  • Value: 内容

大端网络字节序(Big Endian Order)

  • 首先大小端是面向多字节类型定义的,比如2字节、4字节、8字节整型、长整型、浮点型等,单字节的字符串一般不用考虑。

  • 大端小端存储、传输、以及接收处理需要对应。

  • 大端(Big-Endian)就是高字节(MSB)在前,内存存储体现上,数据的高位更加靠近低地址。

  • 小端(Little-Endian)就是低字节(LSB)在前,内存存储体现上,数据的低位更加靠近低地址。

  • 网络字节序一般是指大端。主机字节序大端、小端都有。发送数据的时候一般都要从 主机字节序 转换为 网络字节序(htons方法)。接收数据的时候一般都要从 网络字节序 转换为 主机字节序(ntohs方法).

举个例子: 假设一个32位 unsigned int型数据0x12 34 56 78,大小端8位存储方式如下:
大端存储方式为0x12 34 56 78

小端存储方式为0x78 56 34 12。
大小端的判断

C语言中的union联合体,这个变量在内存中占用4个字节的空间而不是8个(联合体所占内存空间与成员所占最大内存空间为准,多个变量公用一块内存);
有两个数据成员:int类型变量的i和char类型的数组data;
虽然有两个数据成员,但是这两个成员的存储空间是一样的。

union Data {
    char data[4];
    int i;
} u;

//数组中下标低的,地址也低,按地址从低到高,内存内容依次为:04,03,02,01。总共四字节!
//而把四个字节作为一个整体(不分类型,直接打印十六进制),应该从内存高地址到低地址看,0x01020304,低位04放在低地址上。证明我的32位linux是小端(little-endian)
u.data[0] = 0x04;
u.data[1] = 0x03;
u.data[2] = 0x02;
u.data[3] = 0x01;
printf("i = 0x%x \n", u.i);
短整型大小端互换算法:
#define BigLittleSwap16(A)  ((((uint16)(A) & 0xff00) >> 8) | (((uint16)(A) & 0x00ff) << 8))

蓝牙TLV数据报格式:

img

一个完整的数据报包含Header、 Extent、Message、Checkout字段,每个字段都是TLV格式,并且支持TLV嵌套。

数据分包

一个完整的TLV数据报很大可能超过20个字节,这里MTU = 20字节,发送数据时按20字节发送,并根据接收模块的处理能力做相应延时发送。

# define MTU = 20 是蓝牙单次可处理最大字节长度

//分包发送蓝牙数据
-(void)sendMsgWithSubPackage:(NSData*)msgData 
                  Peripheral:(CBPeripheral*)peripheral
              Characteristic:(CBCharacteristic*)character
{
    for (int i = 0; i < [msgData length]; i += MTU) {
        // 预加 最大包长度,如果依然小于总数据长度,可以取最大包数据大小
        if ((i + MTU) < [msgData length]) {
            NSString *rangeStr = [NSString stringWithFormat:@"%i,%i", i, MTU];
            NSData *subData = [msgData subdataWithRange:NSRangeFromString(rangeStr)];
            [self writePeripheral:peripheral
                       characteristic:character
                                value:subData];
            //根据接收模块的处理能力做相应延时
            usleep(20 * 1000);
        } else {
            NSString *rangeStr = [NSString stringWithFormat:@"%i,%i", i, (int)([msgData length] - i)];
            NSData *subData = [msgData subdataWithRange:NSRangeFromString(rangeStr)];
            [self writePeripheral:peripheral
                       characteristic:character
                                value:subData];
            usleep(20 * 1000);
        }
    }
}

数据合并+解析

接收方维持一个接收缓存,当收到第一个数据片段时,根据 Extent 的 length字段,计算出接收数据的长度 并将数据放入到接收缓存中,继续接收数据片段,直到长度满足要求。长度满足要求后开始数据解析,获取Message字段并抛到上层,最后清空接收缓存。

// 一旦接收到数据,调用saveData写入缓存,parseData会验证数据有效长度并解析tlv数据
BLEInterface.shared.saveData(data)
let package = BLEInterface.shared.parseData()

优化项

  • 创建CBCentralManager时指定一个串行队列,使相关代理方法在子线程同步执行
private let queue = DispatchQueue(label: "BLESendQueue")
let  centralManager = CBCentralManager.init(delegate: self, queue: self.queue)
  • 发送数据操作派发到串行队列中, 为了不阻塞当前线程使用异步方式

    queue.async {
        let index = self.sendIndex
        if let data = BLEInterface.shared.queryWifiConnectStatePackage(index) {
            self.currentBlepl!.writeData(data)
            callback(index)
        }
    }
    
  • 接收和解析数据操作在串行队列中同步执行

private let parserQueue = DispatchQueue(label: "BLEParserQueue")

///接收数据
private func receiveData(_ data: Data) {
    parserQueue.async {
        BLEInterface.shared.saveData(data)
        let package = BLEInterface.shared.parseData()
        if package.result {
            self.queue.async {
                for (_, callback) in self.receiveDataCallbacks {
                    callback(package)
                }
            }
        }
    }
}