OSX蓝牙LE外设传输速率很慢

时间:2014-01-29 15:42:40

标签: ios macos bluetooth iobluetooth

背景资料:

我为OSX实现了蓝牙LE外设,它暴露了两个特性(使用CoreBluetooth)。一个是可读的,一个是可写的(两者都有指示)。我在iOS上实现了一个Bluetooth LE Central,它将从可读特性中读取并写入可写特征。我已将其设置为每次读取特征值时,都会更新值(以similar to this example的方式)。我通过此设置获得的传输速率非常缓慢(以大约340 字节 /秒的测量持续速度达到顶峰)。此速度是实际数据,而不是包括数据包详细信息,ACK等的度量。

问题:

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

  1. CoreBluetooth中有一些我错过的参数可以帮助我提高速度。
  2. 我需要使用IOBluetooth类而不是CoreBluetooth来实现自定义蓝牙LE服务。
  3. 我相信,我已经用尽了选项1.我没有看到任何其他可以调整的参数。我只限于每封邮件发送20个字节。还有其他任何东西,我在iOS设备上收到有关未知错误,不太可能错误或值“不长”的神秘错误。由于演示项目还指示了一个20字节的MTU,我会接受这可能是不可能的。

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

    This告诉我如何实现自己的服务(虽然使用了弃用API),但是,它没有解释注册SDP服务所需的参数。所以我想知道:

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

2 个答案:

答案 0 :(得分:5)

我认为我最好的做法是尝试在VM中使用Linux,因为有更多可用文档,访问源代码有望保证我能找到解决方案。对于也遇到此问题的任何人,请按照以下方式在OS X上发布连接参数更新请求(等等)。

第1步

安装Linux VM。我使用Virtual Box和Linux Mint 15(64位Cinnamon)。

第2步

允许在VM中使用OS X Bluetooth设备。尝试将蓝牙USB控制器转发到VM将显示错误消息。为此,您需要停止使用控制器的所有内容。在我的机器上,包括从命令行发出以下命令:

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

这会杀死OS X蓝牙守护进程。尝试从活动监视器中杀死蓝色只会导致它自动重新启动。

sudo kextunload -b com.apple.iokit.BroadcomBluetoothHostControllerUSBTransport

在我的MacBook上,我有一个Broadcom控制器,这是OS X使用的内核模块。不要担心发出这些命令。要撤消更改,您可以关闭并重新启动计算机(请注意,在某些情况下,当使用BT控制器并且它处于不良状态时,我必须让机器断电约10秒才能重新启动以清除易变的记忆)。

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

第3步

启动您的VM并获取您的Linux BT堆栈。我查看了bluez Git repo from here。我特意使用git checkout tags/5.14抓取5.14版本标签,以确保它至少是一个标记版本,不太可能被破坏。在写这个答案时,5.14是最新的标签。

第4步

构建bluez。这是使用bootstrap完成的,然后配置,然后make和make install。我在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步

Bluetooth Standard上为自己做好准备。任何4.0或更高版本都将具有您需要的详细信息。阅读以下部分。

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

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

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

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

评论

这个过程不适合胆小的人。希望将此设置或其他一些设置连接参数的方法添加到bluez以简化此过程。理想情况下,Apple也允许在某些时候通过CoreBluetooth或IOBluetooth这样做(这可能是有可能的,但是没有记录/很难这样做,我放弃了Apple库)。我已经从兔子洞中走了出来,学到了更多关于蓝牙规格的知识,然后我想我只需要改变MacBook和iPhone之间的连接参数。希望这会在某些时候对某人有所帮助(即使我检查了我是如何做到的)。

我知道我已经遗漏了很多细节,以便保持一些简短(即使用bluez工具)。如果有什么不清楚,请评论。

答案 1 :(得分:2)

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

在OSX外设上,当您将所需的延迟设置为低时,间隔仍为22.5ms,远远低于7.5ms的最小值。

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

这是我用来在OSX上运行CBPeripheralManager的代码。我使用Android设备作为核心,使用BLE Explorerdumped the Bluetooth traffic to a Btsnoop file

// 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;
}