OSX蓝牙LE外围设备传输速率较慢



背景信息:

我已经为OSX实现了一个蓝牙LE外设,它暴露了两个特性(使用核心蓝牙)。一个是可读的,一个是可写的(都有指示)。我在iOS上实现了蓝牙LE Central,它将从可读特性读取并写入可写特性。我已经设置好了,每次读取特征值时,都会更新该值(与本例类似)。通过这种设置,我获得的传输速率慢得可怜(以大约340字节/秒的测量持续速度达到峰值)。这个速度是实际数据,而不是包括数据包细节、ACK等的度量

问题:

这种持续的速度太慢了。我考虑了两种解决方案:

  1. 核心蓝牙中有一些参数我错过了,它将帮助我提高速度
  2. 我需要使用IOBluetooth类而不是CoreBluetooth来实现自定义蓝牙LE服务

我相信,我已经用尽了选项1。我看不到任何其他可以调整的参数。我被限制为每条消息发送20个字节。任何其他事情,我都会在iOS设备上收到关于未知错误、不太可能的错误或值为"不长"的神秘错误。由于演示项目还指示了一个20字节的MTU,我接受这可能是不可能的。

所以我只剩下选项2了。我正试图以某种方式修改OSX上蓝牙LE的连接参数,希望能提高传输速度(通过将最小和最大连接间隔分别设置为20ms和40ms,以及每个连接间隔发送多个BT数据包)。看起来在IOBluetooth上提供我自己的SDP服务是在OSX上实现这一点的唯一途径。这方面的问题是,关于如何做到这一点的文档可以忽略不计,甚至根本不存在。

这篇文章告诉我如何实现我自己的服务(尽管使用了弃用的API),但它并没有解释注册SDP服务所需的参数。所以我想知道:

  1. 在哪里可以找到此字典所需的参数
  2. 如何定义这些参数以提供蓝牙LE服务
  3. 除了在OSX上通过另一个框架(Python库?可以访问蓝牙堆栈的虚拟机中的Linux?我想完全避免这种情况。)

我决定我最好的做法是尝试在虚拟机中使用Linux,因为有更多的文档可用,访问源代码有望保证我能找到解决方案。对于同样面临这个问题的人,以下是如何在OS X上发出连接参数更新请求(有点)。

步骤1

安装Linux虚拟机。我在Linux Mint 15(64位肉桂)中使用了Virtual Box。

步骤2

允许在虚拟机中使用OS X蓝牙设备。尝试将蓝牙USB控制器转发到虚拟机将显示错误消息。要允许这样做,您需要停止所有正在使用控制器的操作。在我的机器上,这包括从命令行发出以下命令:

sudo launchctl unload /System/Library/LaunchDaemons/com.apple.blued.plist

这将终止OS X蓝牙守护程序。试图从活动监视器中杀死blued只会导致它自动重新启动。

sudo kextunload -b com.apple.iokit.BroadcomBluetoothHostControllerUSBTransport

在我的MacBook上,我有一个Broadcom控制器,这是OS X用于它的内核模块。不要担心发出这些命令。要撤消更改,您可以关闭电源并重新启动机器(请注意,在某些情况下,当玩BT控制器时,它进入了糟糕的状态,我实际上不得不在重新启动之前关闭机器大约10秒,以清除易失性内存)。

如果在运行这两个命令后仍然无法安装BT控制器,则可以运行kextstat | grep Bluetooth并查看其他与蓝牙相关的内核模块,然后尝试卸载它们。我有一个名为IOBluetoothFamily和IOBlueto齿SerialManager的,它们不需要卸载。

步骤3

启动虚拟机并获取Linux BT堆栈。我在这里查看了bluez Git回购。我特别使用git checkout tags/5.14获取了5.14发布标签,只是为了确保它至少是一个标记版本,并且不太可能被破坏。5.14是截至撰写此答案时的最新标签。

步骤4

构建bluez。这是使用引导程序完成的,然后进行配置,然后进行安装。我在configure上使用了--prefix=/opt/bluez标志,以防止覆盖安装蓝牙堆栈。此外,出于下一步中所述的原因,我使用了--enable-maintainer-mode配置标志。您可能还需要使用--disable-systemd来进行配置。Bluez有一堆工具和实用程序,可以用于各种用途。为了使用构建的蓝牙守护程序,您需要使用sudo service bluetooth stop停止系统守护程序。然后,您可以使用sudo /opt/bluez/libexec/bluetooth/bluetoothd -n -d启动已构建的程序(这将在具有调试输出的非守护进程模式下启动)。

步骤5

通过bluez运行您的LE服务。您可以查看bluez/plugins/gatt-example.c了解如何执行此操作。我直接修改了这一点,删除了不必要的代码,并使用电池服务代码作为我自己的服务和特性的模板。您需要重新编译bluez才能将此代码添加到蓝牙守护进程中。需要注意的一件事(这让我在处理这件事时遇到了一两天的麻烦)是,iOS缓存了GATT服务列表,并且不会在每个连接上读取/刷新。如果你添加服务或特性或更改UUID,你需要在iOS设备上禁用蓝牙,然后重新启用它。这在Apples文档中没有记录,也没有编程方法。

步骤6

不幸的是,这就是事情变得棘手的地方。Bluez没有内置支持使用其任何实用程序发出连接参数更新请求。我不得不自己写。我目前正在查看他们是否希望我的代码包含在bluez堆栈中。我目前无法发布代码,因为我需要先看看bluez开发人员是否对代码感兴趣,然后得到我工作场所的批准才能发布代码。然而,我目前可以解释我为获得支持所做的工作。

步骤7

使用蓝牙标准为自己做好准备。任何4.0或更高版本都将提供您需要的详细信息。阅读以下章节。

  • 关于主机到控制器的HCI流程,请参见第2卷,E部分,4.1
  • HCI ACL数据包格式见第2卷E部分5.4.2
  • 信令分组格式见第3卷A部分第4节
  • 连接参数更新请求格式见第3卷A部分4.20

您基本上需要编写代码来格式化数据包,然后将它们写入hci设备。HCI ACL数据包标头将包含4个字节。这后面是4个字节的信令命令的长度和信道id。然后是您的信号有效载荷,在我的情况下是12个字节(用于连接参数更新请求)。

然后,您可以将它们写入设备,类似于bluez/lib/hci.c中的hci_send_cmd。我将每个数据包头作为自己的结构进行处理,并将它们分别作为iovec写入设备。我把我的新函数放在hci.c文件中,并用bluez/lib/hci_lib.h中的函数原型公开了它。然后我修改了bluez/tools/hcitool.c,允许我从命令行调用此方法。在我的情况下,我这样做是为了使命令与lecup命令几乎相同,因为它需要相同的参数(lecup不能使用,因为它是在主端调用的,而不是在从端调用)。

重新编译了所有这些,然后,瞧,我可以在hcitool上使用我的新命令将参数发送到蓝牙控制器。在发送我的命令后,它会按照预期与iOS设备重新协商。

评论

这个过程不适合胆小的人。希望将此方法或其他设置连接参数的方法添加到bluez中,以简化此过程。理想情况下,苹果也会在某个时候允许通过核心蓝牙或IOBluetooth实现这一功能(这是可能的,但没有文档/很难做到,我放弃了苹果库)。我已经深入兔子洞,了解了更多关于蓝牙规范的信息,然后我想我必须简单地更改MacBook和iPhone之间的连接参数。希望这将在某个时候对某人有所帮助(即使是我在回顾我是如何做到这一点的)。

我知道我在这篇文章中省略了很多细节,以保持简短(即bluez工具上的用法)。如果有什么不清楚的地方,请发表评论。

如果您使用CoreBluetooth实现外围设备,您可以通过将-[CBPeripheralManager setDesiredConnectionLatency:forCentral:]调用为Low、Medium或High(其中低延迟意味着更高的带宽)来请求一些自定义的连接参数。文档中没有具体说明这意味着什么,所以我们必须自己测试它。

在OSX外围设备上,当您将所需的延迟设置为低时,间隔仍然为22.5ms,这与7.5ms的最小值相去甚远。

  • 在OSX Yosemite 10.10.4上,这就是CBPeripheralManagerConnectionLatency值的含义:
    • :最小时间间隔:18(22.5ms),最大时间间隔:18[22.5ms],从延迟:4个事件,超时:200(2s)
    • 中等:最小间隔:32(40ms),最大间隔:32
    • :最小间隔:160(200ms),最大间隔:160

这是我用来在OSX上运行CBPeripheralManager的代码。我使用BLE Explorer将安卓设备作为中心,并将蓝牙流量转储到Btsnoop文件中。

// clang main.m -framework Foundation -framework IOBluetooth
#import <Foundation/Foundation.h>
#import <IOBluetooth/IOBluetooth.h>
@interface MyPeripheralManagerDelegate: NSObject<CBPeripheralManagerDelegate>
@property (nonatomic, assign) CBPeripheralManager* peripheralManager;
@property (nonatomic) CBPeripheralManagerConnectionLatency nextLatency;
@end
@implementation MyPeripheralManagerDelegate
+ (NSString*)stringFromCBPeripheralManagerState:(CBPeripheralManagerState)state {
switch (state) {
case CBPeripheralManagerStatePoweredOff: return @"PoweredOff";
case CBPeripheralManagerStatePoweredOn: return @"PoweredOn";
case CBPeripheralManagerStateResetting: return @"Resetting";
case CBPeripheralManagerStateUnauthorized: return @"Unauthorized";
case CBPeripheralManagerStateUnknown: return @"Unknown";
case CBPeripheralManagerStateUnsupported: return @"Unsupported";
}
}
+ (CBUUID*)LatencyCharacteristicUuid {
return [CBUUID UUIDWithString:@"B81672D5-396B-4803-82C2-029D34319015"];
}
- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral {
NSLog(@"CBPeripheralManager entered state %@", [MyPeripheralManagerDelegate stringFromCBPeripheralManagerState:peripheral.state]);
if (peripheral.state == CBPeripheralManagerStatePoweredOn) {
NSDictionary* dict = @{CBAdvertisementDataLocalNameKey: @"ConnLatencyTest"};
// Generated with uuidgen
CBUUID *serviceUuid = [CBUUID UUIDWithString:@"7AE48DEE-2597-4B4D-904E-A3E8C7735738"];
CBMutableService* service = [[CBMutableService alloc] initWithType:serviceUuid primary:TRUE];
// value:nil makes it a dynamic-valued characteristic
CBMutableCharacteristic* latencyCharacteristic = [[CBMutableCharacteristic alloc] initWithType:MyPeripheralManagerDelegate.LatencyCharacteristicUuid properties:CBCharacteristicPropertyRead value:nil permissions:CBAttributePermissionsReadable];
service.characteristics = @[latencyCharacteristic];
[self.peripheralManager addService:service];
[self.peripheralManager startAdvertising:dict];
NSLog(@"startAdvertising. isAdvertising: %d", self.peripheralManager.isAdvertising);
}
}
- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral
error:(NSError *)error {
if (error) {
NSLog(@"Error advertising: %@", [error localizedDescription]);
}
NSLog(@"peripheralManagerDidStartAdvertising %d", self.peripheralManager.isAdvertising);
}
+ (CBPeripheralManagerConnectionLatency) nextLatencyAfter:(CBPeripheralManagerConnectionLatency)latency {
switch (latency) {
case CBPeripheralManagerConnectionLatencyLow: return CBPeripheralManagerConnectionLatencyMedium;
case CBPeripheralManagerConnectionLatencyMedium: return CBPeripheralManagerConnectionLatencyHigh;
case CBPeripheralManagerConnectionLatencyHigh: return CBPeripheralManagerConnectionLatencyLow;
}
}
+ (NSString*)describeLatency:(CBPeripheralManagerConnectionLatency)latency {
switch (latency) {
case CBPeripheralManagerConnectionLatencyLow: return @"Low";
case CBPeripheralManagerConnectionLatencyMedium: return @"Medium";
case CBPeripheralManagerConnectionLatencyHigh: return @"High";
}
}
- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveReadRequest:(CBATTRequest *)request {
if ([request.characteristic.UUID isEqualTo:MyPeripheralManagerDelegate.LatencyCharacteristicUuid]) {
[self.peripheralManager setDesiredConnectionLatency:self.nextLatency forCentral:request.central];
NSString* description = [MyPeripheralManagerDelegate describeLatency: self.nextLatency];
request.value = [description dataUsingEncoding:NSUTF8StringEncoding];
[self.peripheralManager respondToRequest:request withResult:CBATTErrorSuccess];
NSLog(@"didReceiveReadRequest:latencyCharacteristic. Responding with %@", description);
self.nextLatency = [MyPeripheralManagerDelegate nextLatencyAfter:self.nextLatency];
} else {
NSLog(@"didReceiveReadRequest: (unknown) %@", request);
}
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyPeripheralManagerDelegate *peripheralManagerDelegate = [[MyPeripheralManagerDelegate alloc] init];
CBPeripheralManager* peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:peripheralManagerDelegate queue:nil];
peripheralManagerDelegate.peripheralManager = peripheralManager;
[[NSRunLoop currentRunLoop] run];
}
return 0;
}

最新更新