数据封装
iOS 蓝牙框架默认会进行发送数据的分包,默认20字节一个包,但是传输大一点的数据还是会被截断。这种情况下,需要开发者自行控制分包。虽然接收端按序接收数据,但为了合并数据的方便,一般要将数据按照一定的格式进行封装,然后再分包发送。
一种简单的封装格式
在Apple Documents里有一个叫BTLE的Demo。它通过数据末尾添加EOM标识符来表示一个包数据的结束。
如上图所示,模拟命令分成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数据报格式:
一个完整的数据报包含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)
}
}
}
}
}