OSX Bluetooth LE Скорость передачи периферийных устройств низкая

Справочная информация:

Я реализовал Bluetooth LE Peripheral для OSX, который демонстрирует две характеристики (используя CoreBluetooth). Один доступен для чтения, а другой - для записи (оба с указаниями). Я реализовал Bluetooth LE Central на iOS, который будет считывать из читаемой характеристики и записывать в записываемую характеристику. Я настроил его так, чтобы каждый раз, когда значение признака считывалось, значение обновлялось (способом, подобным этому примеру). Скорости передачи, которые я получаю с этой настройкой, являются патетически медленными (максимальная при измеренной постоянной скорости примерно 340 байт / с). Эта скорость является фактическими данными, а не мерой, включающей детали пакета, ACK и так далее.

Проблема:

Эта устойчивая скорость слишком медленная. Я рассмотрел два решения:

  1. В CoreBluetooth есть какой-то параметр, который я пропустил и который поможет мне увеличить скорость.
  2. Мне нужно будет реализовать собственный сервис Bluetooth LE, используя классы IOBluetooth вместо CoreBluetooth.

Я полагаю, я исчерпал вариант 1. Я не вижу никаких других параметров, которые я могу настроить. Я ограничен отправкой 20 байтов за сообщение. Что-нибудь еще, и я получаю загадочные ошибки на устройстве iOS относительно Неизвестных ошибок, Маловероятных ошибок, или значение, являющееся "Не длинным". Поскольку демонстрационный проект также указывает 20-байтовый MTU, я согласен, что это, вероятно, невозможно.

Итак, у меня остался вариант 2. Я пытаюсь каким-то образом изменить параметры подключения для Bluetooth LE на OSX, чтобы, надеюсь, позволить мне увеличить скорость передачи (установив минимальные и максимальные интервалы conn равными 20 мс и 40 мс соответственно - как а также отправка нескольких BT-пакетов за интервал соединения). Похоже, что предоставление собственной службы SDP на IOBluetooth - единственный способ добиться этого на OSX. Проблема в том, что документация о том, как это сделать, ничтожна или вообще не существует.

Это говорит мне о том, как реализовать мой собственный сервис (хотя и с использованием устаревшего API), однако не объясняет обязательные параметры для регистрации сервиса SDP. Так что мне осталось интересно:

  1. Где я могу найти необходимые параметры для этого словаря?
  2. Как определить эти параметры, чтобы предложить услугу Bluetooth LE?
  3. Есть ли альтернатива предоставлению периферийного устройства Bluetooth LE в OSX через другую платформу (библиотека Python? Linux на виртуальной машине с доступом к стеку Bluetooth? Я бы вообще хотел этого избежать).

2 ответа

Решение

Я решил, что лучше всего попытаться использовать Linux на виртуальной машине, так как имеется больше документации и доступ к исходному коду, я надеюсь, поможет мне найти решение. Для тех, кто также сталкивается с этой проблемой, вот как вы можете отправить запрос на обновление параметров подключения в OS X (вроде).

Шаг 1

Установите виртуальную машину Linux. Я использовал Virtual Box с Linux Mint 15 (64-битная Cinnamon).

Шаг 2

Разрешить использование устройства OS X Bluetooth в вашей виртуальной машине. Попытка переслать USB-контроллер Bluetooth на виртуальную машину выдаст сообщение об ошибке. Чтобы разрешить это, вам нужно остановить все, что использует контроллер. На моей машине это включало выдачу следующих команд из командной строки:

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

Это убьет демон OS X Bluetooth. Попытка уничтожить синего с монитора активности просто приведет к его автоматическому повторному запуску.

sudo kextunload -b com.apple.iokit.BroadcomBluetoothHostControllerUSBTransport

На моем MacBook у меня есть контроллер Broadcom, и это модуль ядра, который OS X использует для него. Не беспокойтесь о выдаче этих команд. Чтобы отменить изменения, вы можете выключить и перезагрузить компьютер (обратите внимание, что в некоторых случаях, когда вы играете с контроллером BT и он перешел в плохое состояние, мне пришлось фактически оставить устройство выключенным на ~10 секунд, прежде чем перезагрузить компьютер, чтобы очистить его. энергозависимая память).

Если после выполнения этих двух команд вы все еще не можете смонтировать контроллер BT, вы можете запустить kextstat | grep Bluetooth и посмотрите другие модули ядра, связанные с Bluetooth, а затем попробуйте также выгрузить их. У меня есть IOBluetoothFamily и IOBluetoothSerialManager, которые не нужно выгружать.

Шаг 3

Запустите свою ВМ и получите свой стек Linux BT. Я проверил репозиторий Bluez Git отсюда. Я специально взял тег выпуска 5.14, используя git checkout tags/5.14 просто чтобы быть уверенным, что это была хотя бы помеченная версия и с меньшей вероятностью она будет взломана. 5.14 является новейшим тегом на момент написания этого ответа.

Шаг 4

Сборка bluez. Это было сделано с помощью начальной загрузки, затем настройте, затем сделайте и сделайте установку. Я использовал --prefix=/opt/bluez установите флажок конфигурации, чтобы предотвратить перезапись стека установки Bluetooth. Кроме того, я использовал --enable-maintainer-mode настроить флаг по причине, указанной на следующем шаге. Вам также может понадобиться --disable-systemd чтобы настроить его. У Bluez есть куча инструментов и утилит, которые вы можете использовать для разных целей. Чтобы использовать встроенный демон Bluetooth, вам нужно остановить системный демон, используя sudo service bluetooth stop, Вы можете запустить встроенный с помощью sudo /opt/bluez/libexec/bluetooth/bluetoothd -n -d (запускается в не-демоническом режиме с выводом отладки).

Шаг 5

Запустите сервис LE через bluez. Вы можете просмотреть bluez/plugins/gatt-example.c как это сделать. Я непосредственно изменил это, удалив ненужный код и используя сервисный код аккумулятора в качестве шаблона для моей собственной службы и характеристик. Вам нужно перекомпилировать bluez, чтобы этот код был добавлен в демон bluetooth. Одна вещь, на которую нужно обратить внимание (это вызвало у меня один-два дня трудностей с установкой этой работы), заключалось в том, что iOS кэширует список служб GATT, и это не считывается / обновляется при каждом соединении. Если вы добавляете услугу или характеристику или изменяете UUID, вам нужно отключить Bluetooth на вашем устройстве iOS, а затем снова включить его. Это не документировано в документации Apple, и нет никакого программного способа сделать это.

Шаг 6

К сожалению, здесь все становится сложнее. Bluez не имеет встроенной поддержки для выдачи запроса на обновление параметров подключения с помощью какой-либо из его утилит. Я должен был написать это сам. В настоящее время я вижу, хотят ли они, чтобы мой код был включен в стек bluez. Я не могу опубликовать код в настоящее время, так как мне нужно сначала узнать, интересуются ли разработчики bluez кодом, а затем получить разрешение от моего рабочего места, чтобы дать код. Однако в настоящее время я могу объяснить, что я сделал, чтобы включить поддержку.

Шаг 7

Заправьтесь в стандарте Bluetooth. Любая версия 4.0 или выше будет содержать детали, которые вам нужны. Прочитайте следующие разделы.

  • Смотри т. 2, часть E, 4.1 для потока HCI между хостом и контроллером.
  • Смотри т. 2, часть E, 5.4.2 для формата пакета данных HCL ACL.
  • Смотри т. 3, часть A, 4 для формата пакета сигнализации.
  • Смотри т. 3, часть A, 4.20 для формата запроса на обновление параметров соединения.

В основном вам нужно написать код для форматирования пакетов, а затем записать их на устройство hci. Заголовок пакета данных HCI ACL будет содержать 4 байта. Далее следуют 4 байта для длины команды сигнализации и идентификатора канала. Затем следует полезная нагрузка вашего сигнала, которая в моем случае составляла 12 байтов (для запроса на обновление параметров соединения).

Затем вы можете записать их на устройство, подобное hci_send_cmd в bluez/lib/hci.c, Я сделал каждый заголовок пакета как его собственную структуру и записал их как iovecс устройством. Я поместил мою новую функцию в файл hci.c и выставил ее с прототипом функции в bluez/lib/hci_lib.h, Я тогда модифицировал bluez/tools/hcitool.c чтобы позволить мне вызвать этот метод из командной строки. В моем случае я сделал так, чтобы команда была почти идентична команде lecup, так как она требует тех же параметров (lecup не может быть использован, так как он вызывается на ведущей стороне, а не на ведомой).

Перекомпилировал все это, и тогда, вуаля, я могу использовать мою новую команду на hcitool для отправки параметров на контроллер Bluetooth. После отправки моей команды, он, как и ожидалось, повторно согласовывает с устройством iOS.

Комментарии

Этот процесс не для слабонервных. Надеемся, что этот или другой метод настройки параметров подключения добавлен в bluez для упрощения этого процесса. В идеале Apple в какой-то момент даст возможность делать это через CoreBluetooth или IOBluetooth (это могло быть возможно, но недокументировано / трудно сделать, я отказался от библиотек Apple). Я путешествовал по кроличьей норе и узнал гораздо больше о спецификациях Bluetooth, тогда я подумал, что мне придется просто изменить параметры соединения между MacBook и iPhone. Надеюсь, в какой-то момент это кому-нибудь пригодится (даже если я проверю, как я это сделал).

Я знаю, что упустил много деталей в этом, чтобы сделать это несколько кратким (то есть использование в инструментах bluez). Пожалуйста, прокомментируйте, если что-то не понятно.

Если вы реализуете свое Периферийное устройство с использованием CoreBluetooth, вы можете запросить несколько настроенных параметров соединения, вызвав -[CBPeripheralManager setDesiredConnectionLatency:forCentral:] Низкий, Средний или Высокий (где Низкая задержка означает более высокую пропускную способность). В документации не указано, что это значит, поэтому мы должны проверить это сами.

На периферии OSX, когда вы устанавливаете желаемую задержку на Низкий, интервал по-прежнему составляет 22,5 мс, что далеко от минимума 7,5 мс.

  • На OSX Yosemite 10.10.4 это то, что значения CBPeripheralManagerConnectionLatency означают:
    • Низкий: минимальный интервал: 18 (22,5 мс), максимальный интервал: 18 (22,5 мс), задержка ведомого: 4 события, время ожидания: 200 (2 с).
    • Средний: минимальный интервал: 32 (40 мс), максимальный интервал: 32 (40 мс), задержка ведомого: 6 событий, время ожидания: 200 (2 с)
    • Высокий: минимальный интервал: 160 (200 мс), максимальный интервал: 160 (200 мс), задержка ведомого: 2 события, время ожидания: 300 (3 с)

Вот код, который я использовал для запуска CBPeripheralManager в OSX. Я использовал устройство Android в качестве центрального, используя BLE Explorer, и записывал трафик Bluetooth в файл 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;
}
Другие вопросы по тегам