Русский
Русский
English
Статистика
Реклама

Перевод Android Bluetooth Low Energy (BLE) готовим правильно, часть 2 (connectingdisconnecting)

Часть 2Часть 2

Содержание

Часть #1 (scanning).

Часть #2 (connecting/disconnecting), вы здесь.

Впредыдущей статьемы подробно рассмотрели сканирование устройств. Эта статья - оподключении,отключениииобнаружении сервисов(discovering services).

Подключение к устройству

После удачного сканирования, вы должны подключиться к устройству, вызывая методconnectGatt(). В результате мы получаем объект BluetoothGatt, который будет использоваться для всехGATT операций, такие как чтение и запись характеристик. Однако будьте внимательны, есть две версии методаconnectGatt(). Поздние версии Android имеют еще несколько вариантов, но нам нужна совместимость с Android-6 поэтому мы рассматриваем только эти две:

BluetoothGatt connectGatt(Context context, boolean autoConnect,        BluetoothGattCallback callback)BluetoothGatt connectGatt(Context context, boolean autoConnect,        BluetoothGattCallback callback, int transport)

Внутренняя реализация первой версии это фактически вызов второй версии с аргументомtransport = TRANSPORT_AUTO. Для подключения BLE устройств такой вариант не подходит.TRANSPORT_AUTOиспользуется для устройств с поддержкой и BLE и классического Bluetooth протоколов. Это значит, что Android будет сам выбирать протокол подключения. Этот момент практически нигде не описан и может привести к непредсказуемым результатам, много людей сталкивались с такой проблемой. Вот почему вы должны использовать вторую версиюconnectGatt()сtransport = TRANSPORT_LE:

BluetoothGatt gatt = device.connectGatt(context, false,     bluetoothGattCallback, TRANSPORT_LE);

Первый аргумент contextприложения. Второй аргумент флагautoconnect, говорит подключаться немедленно (false) или нет (true). При немедленном подключении (false) Android будет пытаться соединиться в течение 30 секунд (на большинстве смартфонов), по истечении этого времени придет статус соединенияstatus_code = 133. Это не официальная ошибка для таймаута соединения. В исходниках Android код фигурирует какGATT_ERROR. К сожалению, эта ошибка появляется и в других случаях. Имейте ввиду, сautoconnect = falseAndroid делает соединение только с одним устройством в одно и то же время (это значит если у вас несколько устройств - подключайте их последовательно, а не паралелльно). Третий аргумент функция обратного вызоваBluetoothGattCallback(callback) для конкретного устройства. Этот колбек используется для всех связанных с устройством операциях, такие как чтение и запись. Мы рассмотрим это более детально в следующей статье.

Autoconnect = true

Если вы установитеautoconnect = true, Android будет подключаться самостоятельно к устройству всякий раз, когда оно будет обнаружено. Внутри это работает так: Bluetooth стек сканирует сохраненные устройства и когда увидит одно из них подключается к нему. Это довольно удобно, если вы хотите подключиться к конкретному устройству, когда оно становится доступным. Фактически, это предпочтительный способ для переподключения. Вы просто создаетеBluetoothDeviceобъект и вызываетеconnectGattсautoconnect = true.

BluetoothDevice device = bluetoothAdapter.getRemoteDevice("12:34:56:AA:BB:CC");BluetoothGatt gatt = device.connectGatt(context, true, bluetoothGattCallback, TRANSPORT_LE);

Обратите внимание, этот подход работает только, если устройство есть в Bluetooth кеше или устройство было уже сопряжено (bonding). Посмотрите моюпредыдущую статью, где подробно объясняется работа с Bluetooth кешем. При перезагрузке смартфона или выключении/включении Bluetooth (а также Airplane режима) кеш очистится, это надо проверять перед подключением сautoconnect = true, что действительно раздражает.

Autoconnect работает только с закешированными и сопряженными (bonded) устройствами!

Для того, чтобы узнать, закешировано устройство или нет, можно использовать небольшой трюк. После создания объектаBluetoothDevice, вызовите у негоgetType, если результат TYPE_UNKNOWN, значит устройство не закешировано. В этом случае, необходимо просканировать устройство с этим мак-адресом (используя не агрессивный метод сканирования) и после этого можно использовать автоподключение снова.

Android-6 и ниже имеет известный баг, в котором возникает гонка состояний и автоматическое подключение становится обычным (autoconnect = false). К счастью, умные ребята из Polidea нашлирешение для этого. Настоятельно рекомендуется использовать его, если думаете использовать автоподключение.

Преимущества:

  • работает достаточно хорошо на современных версиях Android (прим. переводчика - от Android-8 и выше).

  • возможность подключаться к нескольким устройствам одновременно;

Недостатки:

  • работает медленнее, если сравнивать сканирование в агрессивном режиме + подключение сautoconnect = false. Потому что Android в этом случае сканирует в режимеSCAN_MODE_LOW_POWER, экономя энергию.

Изменения статуса подключения

После вызоваconnectGatt(), Bluetooth стек присылает результат в колбекonConnectionStateChange, он вызывается при любом изменении соединения.

Работа с этим колбеком достаточно нетривиальная вещь. Большинство простых примеров из сети выглядит так (не обольщайтесь):

public void onConnectionStateChange(final BluetoothGatt gatt,                                     final int status,                                     final int newState) {    if (newState == BluetoothProfile.STATE_CONNECTED) {        gatt.discoverServices();    } else {        gatt.close();    }}

Этот код обрабатывает только аргументnewStateи полностью игнорируетstatus. В многих случаях это работает и кажется безошибочным. Действительно, после подключения, следующее что нужно сделать это вызватьdiscoverServices(). А в случае отключения - необходимо сделать вызовclose(), чтобы Android освободил все связанные ресурсы в стеке Bluetooth. Эти два момента очень важные для стабильной работы BLE под Android, давайте их обсудим прямо сейчас!

При вызовеconnectGatt(), Bluetooth стек регистрирует внутри себя интерфейс для нового клиента (client interface: clientIf).

Возможно вы заметили такие логи в LogCat:

D/BluetoothGatt: connect() - device: B0:49:5F:01:20:XX, auto: falseD/BluetoothGatt: registerApp()D/BluetoothGatt: registerApp()  UUID=0e47c0cf-ef134afb-9f548cf3e9e808d5D/BluetoothGatt: onClientRegistered()  status=0 clientIf=6

Здесь видно, что клиент6был зарегистрирован после вызоваconnectGatt(). Максимальное количество клиентов (подключения) у Android равно 30 (константаGATT_MAX_APPSв исходниках), при достижении которого Android не будет подключаться к устройствам вообще и вы будете получать постоянно ошибку подключения. Достаточно странно, но сразу после загрузки Android уже имеет 5 или 6 таких подключенных клиентов, предполагаю, что Android использует их для внутренних нужд. Таким образом, если вы не вызываете методclose(), то счетчик клиентов увеличивается каждый раз при вызовеconnectGatt(). Когда вы вызываетеclose(), Bluetooth стек удаляет ваш колбек, счетчик клиентов уменьшается на единицу и освобождает ресурсы клиента.

D/BluetoothGatt: close()D/BluetoothGatt: unregisterApp()  mClientIf=6

Важно всегда вызыватьclose()после отключения! А сейчас обсудим основные случаи дисконнекта устройств.

Состояние подключения (newState)

ПеременнаяnewStateсодержит новое состояние подключения и может иметь 4 значения:

  • STATE_CONNECTED

  • STATE_DISCONNECTED

  • STATE_CONNECTING

  • STATE_DISCONNECTING

Значения говорят сами за себя. Хотя состоянияSTATE_CONNECTING,STATE_DISCONNECTINGесть в документации, на практике я их не встречал. Так что, в принципе, можно не обрабатывать их, но для уверенности, я предлагаю их явно учитывать (прим. переводчика - и это лучше, чем не обрабатывать их), вызываяclose()только в том случае если устройство действительно отключено.

Статус подключения (status)

В примере выше, переменная статусаstatusполностью игнорировалась, но в действительности обрабатывать ее важно. Эта переменная, по сути, является кодом ошибки. Вы можете получитьGATT_SUCCESSв результате как подключения, так и контролируемого отключения. Таким образом, мы можем по-разному обрабатывать контролируемое или внезапное отключение устройства. Если вы получили значение отличное отGATT_SUCCESS, значит что-то пошло не так и вstatusбудет указана причина. К сожалению, объектBluetoothGattдает очень мало кодов ошибок, все они описаныздесь. Чаще всего вы будете встречаться с кодом 133 (GATT_ERROR). Который не имеет точного описания, и просто говорит произошла какая-то ошибка. Не очень информативно, подробнее обGATT_ERRORпозже.

Теперь мы знаем, что обозначают переменныеnewStateиstatus, давайте улучшим наш колбекonConnectionStateChange:

public void onConnectionStateChange(final BluetoothGatt gatt,                                     final int status,                                     final int newState) {if(status == GATT_SUCCESS) {    if (newState == BluetoothProfile.STATE_CONNECTED) {        // Мы подключились, можно запускать обнаружение сервисов        gatt.discoverServices();    } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {        // Мы успешно отключились (контролируемое отключение)        gatt.close();    } else {        // мы или подключаемся или отключаемся, просто игнорируем эти статусы    }} else {   // Произошла ошибка... разбираемся, что случилось!   ...   gatt.close();} 

Это не последний вариант, мы еще улучшим колбек в этой статье. В любом случае, теперь у нас есть обработка ошибок и успешных операций.

Состояние bonding (bondState)

Последний параметр, который необходимо учитывать в колбекеonConnectionStateChange этоbondState, состояние сопряжения (bonding) с устройством. Мы получаем этот параметр так:

int bondstate = device.getBondState();

Состояние bonding может иметь одно из трех значенийBOND_NONE,BOND_BONDINGorBOND_BONDED. Каждое из них влияет на то, как обрабатывать подключение.

  • BOND_NONE, нет проблем, можно вызыватьdiscoverServices();

  • BOND_BONDING, устройство в процессе сопряжения, нельзя вызыватьdiscoverServices(), так как Bluetooth стек в работе и запускdiscoverServices()может прервать сопряжение и вызвать ошибку соединения.discoverServices()вызываем только после того, как пройдет сопряжение (bonding);

  • BOND_BONDED, для Android-8 и выше, можно запускатьdiscoverServices()без задержки. Для версий 7 и ниже может потребоваться задержка перед вызовом. Если ваше устройство имеетService Changed Characteristic, то Bluetooth стек в этот момент еще обрабатывает их и запускdiscoverServices()без задержки может вызвать ошибку соединения. Добавьте 1000-1500мс задержки, конкретное значение зависит от количества характеристик на устройстве. Используйте задержку всегда, если вы не знаете сколькоService Changed Characteristicимеет устройство.

Теперь мы можем учитывать состояниеbondStateвместе сstatusиnewState:

if (status == GATT_SUCCESS) {    if (newState == BluetoothProfile.STATE_CONNECTED) {        int bondstate = device.getBondState();        // Обрабатываем bondState        if(bondstate == BOND_NONE || bondstate == BOND_BONDED) {            // Подключились к устройству, вызываем discoverServices с задержкой            int delayWhenBonded = 0;            if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {                delayWhenBonded = 1000;            }            final int delay = bondstate == BOND_BONDED ? delayWhenBonded : 0;            discoverServicesRunnable = new Runnable() {                @Override                public void run() {                    Log.d(TAG, String.format(Locale.ENGLISH, "discovering services of '%s' with delay of %d ms", getName(), delay));                    boolean result = gatt.discoverServices();                    if (!result) {                        Log.e(TAG, "discoverServices failed to start");                    }                    discoverServicesRunnable = null;                }            };            bleHandler.postDelayed(discoverServicesRunnable, delay);        } else if (bondstate == BOND_BONDING) {            // Bonding в процессе, ждем когда закончится            Log.i(TAG, "waiting for bonding to complete");        }....

Обработка ошибок

После того как мы разобрались с успешными операциями, давайте взглянем на ошибки. Есть ряд ситуаций, которые на самом деле нормальные, но выдают себя за ошибки.

  • Устройство отключилось намеренно. Например, все данные были переданы и больше ему нечего делать. Вы получите статус - 19 (GATT_CONN_TERMINATE_PEER_USER);

  • Истекло время ожидания соединения и устройство отключилось само. В этом случае придет статус - 8 (GATT_CONN_TIMEOUT);

  • Низкоуровневая ошибка соединения, которая привела к отключению. Обычно это статус - 133 (GATT_ERROR) или более конкретный код, если повезет;

  • Bluetooth стек не смог подключится ни разу. Здесь также получим статус - 133 (GATT_ERROR);

  • Соединение было потеряно в процессеbondingилиdiscoverServices. Необходимо выяснить причину и возможно повторить попытку подключения.

Первые два случая абсолютно нормальные явления и все что нужно сделать - это вызыватьclose()и подчистить ссылки на объектBluetoothGatt, если необходимо. В остальных случаях, либо ваш код, либо устройство, что-то делает не так. Вы возможно захотите уведомить UI или другие части приложения о проблеме, повторить подключение или еще каким-то образом отреагировать на ситуацию. Взгляните как я сделал это вмоей библиотеке.

Статус 133 при подключении (connecting)

Статус - 133 часто встречается при попытках подключиться к устройству, особенно во время разработки. Этот статус может иметь множество причин, некоторые из них можно контролировать:

  • Убедитесь, что вы всегда вызываетеclose()при отключении. Если этого не сделать, в следующий раз при подключении вы точно получитеstatus=133;

  • Всегда используйтеTRANSPORT_LEв вызовеconnectGatt();

  • Перезагрузите смартфон. Возможно Bluetooth стек выбрал лимит по клиентским подключениям или есть внутренняя проблема. (Прим. переводчика: я сначала выключал/включал Bluetooth, потом Airplane режим и если не помогало - перезагружал);

  • Проверьте что устройство посылает advertising пакеты. ВызовconnectGatt()сautoconnect = falseимеет таймаут 30 секунд, после чего присылает ошибкуstatus=133;

  • Замените/зарядите батарею на устройстве. Обычно устройства работают нестабильно при низком заряде;

Если вы попробовали все способы выше и все еще получаете статус 133, необходимо простоповторить подключение! Это одна из Android ошибок, которую мне так и не удалось понять или решить. Иногда вы получаете 133 при подключении к устройству, но если вызыватьclose()и переподключиться, то все работает без проблем! Есть подозрение, что проблема в кеше Android и вызовclose()сбрасывает его состояние для конкретного устройства. Если кто-нибудь поймет, как решить эту проблему дайте мне знать!

Отключение по запросу (disconnect)

Для отключения устройства вам необходимо сделать шаги:

  • вызватьdisconnect();

  • подождать обновления статуса вonConnectionStateChange;

  • вызватьclose();

  • освободить связанные с объектом gatt ресурсы;

Командаdisconnect()фактически разрывает соединение с устройством и обновляет внутреннее состояние Bluetooth стека. Затем вызывается колбекonConnectionStateChangeс новым состоянием disconnected.

Вызовclose()удаляет вашBluetoothGattCallbackи освобождает клиента в Bluetooth стеке.

Наконец, удалениеBluetoothGattосвободит все связанные с подключением ресурсы.

Отключение неправильно

В примерах из сети можно увидеть, разные примеры отключения, например:

  • вызватьdisconnect()

  • сразувызватьclose()

Это будет работать более-менее. Да устройство отключится, но вы никогда не получите вызов колбека с состоянием disconnected. Дело в том, чтоdisconnect()операция асинхронная (не блокирует поток и имеет свое время выполнения), аclose()немедленно удаляет коллбек! Получается, когда Android будет готов вызвать колбек, его уже не будет.

Иногда в примерах не вызываютdisconnect(), а толькоclose(). Это приведет к отключению устройства, но это неправильный способ, посколькуdisconnect()отключает активное соединение и отменяет ожидающее автоматическое подключение (вызов сautoconnect = true). Поэтому, если вы вызываете толькоclose(), любое ожидающее автоподключение может привести к новому подключению.

Отмена попытки подключения

Если вы хотите отменить подключение послеconnectGatt(), вам нужно вызватьdisconnect(). Так как в этому моменту вы еще не подключены, колбекonConnectionStateChangeне сработает! Просто подождите некоторое время послеdisconnect()и после этого вызывайтеclose()(прим. переводчика: обычно это 50-100мс).

При удачной отмене вы увидите примерно такое в логах:

D/BluetoothGatt: cancelOpen()  device: CF:A9:BA:D9:62:9E

Скорее всего, вы никогда не отмените соединение, для параметраautoconnect = false. Часто это делается для подключений сautoconnect = true. Например, когда приложение на переднем плане вы подключаетесь к вашим устройствам и отключаетесь от них, если приложение переходит в фон.

Прим. переводчика: но это не значит что дляautoconnect = falseне надо проводить такую отмену!

Обнаружение сервисов (discovering services)

Как только вы подключились к устройству, необходимо запустить обнаружение его сервисов вызовомdiscoverServices(). Bluetooth стек запустит серию низкоуровневых команд для получениясервисов,характеристикидескрипторов. Это занимает обычно около одной секунды в зависимости от того сколько таких служб, характеристик, дескрипторов имеет ваше устройство. В результате будет вызыван колбекonServicesDiscovered.

Первым делом проверим, есть ли какие ошибки после обнаружения сервисов:

// Проверяем есть ли ошибки? Если да - отключаемсяif (status == GATT_INTERNAL_ERROR) {    Log.e(TAG, "Service discovery failed");    disconnect();    return;}

Если есть ошибки (обычно этоGATT_INTERNAL_ERRORсо значением 129), делаем отключение устройства, что-то исправить здесь невозможно (нет специальных технических способов для этого). Вы просто отключаете устройство и повторно пробуете подключиться.

Если все прошло удачно, вы получите список сервисов:

final List<BluetoothGattService> services = gatt.getServices();Log.i(TAG, String.format(Locale.ENGLISH,"discovered %d services for '%s'", services.size(), getName()));// Работа со списком сервисов (если требуется)...

Кеширование сервисов.

Bluetooth стек кеширует найденные на устройстве сервисы, характеристики и дескрипторы. Первое подключение вызывает реальное обнаружение сервисов, все последующие возвращаются кешированные версии. Это соответствует стандарту Bluetooth. Обычно это нормально и сокращает время соединения с устройством. Однако в некоторых случаях, может потребоваться очистить кеш, чтобы снова обнаружить их с устройства при следующем соединении. Типичный сценарий: обновление прошивки, в которой изменяется набор сервисов, характеристик, дескрипторов. Есть скрытый метод очистки кеша и добраться до него нам поможет механизм рефлексии Java:

private boolean clearServicesCache() {    boolean result = false;    try {        Method refreshMethod = bluetoothGatt.getClass().getMethod("refresh");        if(refreshMethod != null) {            result = (boolean) refreshMethod.invoke(bluetoothGatt);        }    } catch (Exception e) {        Log.e(TAG, "ERROR: Could not invoke refresh method");    }    return result;}

Этот метод асинхронный, дайте ему некоторое время для завершения!

Странные штуки в подключении/отключении

Хотя операции подключения и отключения выглядят просто, есть некоторые особенности, которые нужно знать.

  • Случайная ошибка 133 при подключении, выше мы разобрались как с ней работать;

  • Периодическое зависание подключения, не срабатывает таймаут и не вызывается колбекonConnectionStateChange. Это случается не часто, но я видел такие случае при низком уровне батареи или когда устройство находится на границе доступности по расстоянию Bluetooth. Скорее всего общение с устройством происходит, но затем прерывается и зависает. Мой обходной путь использовать свой таймер подключения и в случае таймаута закрывать соединение и отключаться;

  • Некоторые смартфоны имеют проблему с подключением во время сканирования. Например, Huawei P8 Lite один из таких. Останавливаем сканнер перед любым подключением (Прим. переводчика: это правило соблюдаем строго!);

  • Все вызовы подключения/отключения асинхронные. То есть неблокирующие, но при этом им нужно время, чтобы выполнится до конца. Избегайте быстрый запуск их друг за другом (Прим. переводчика: я обычно использую задержку 50-100мс между вызовами).

Следующая статья: чтение и запись характеристик.

Теперь мы разобрались с подключением/отключением и обнаружением сервисов, следующая статья о том, как работать с характеристиками.

Не терпится поработать с BLE? Попробуйтемою библиотеку Blessed for Android. Она использует все подходы из этой серии статей и упрощает работу с BLE в вашем приложении.

Источник: habr.com
К списку статей
Опубликовано: 15.01.2021 20:21:00
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Java

Разработка мобильных приложений

Разработка под android

Android

Bluetooth

Bluetooth le

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru