Android, работа с BLE - часть 2.
Перевод статьи Making Android BLE work — part 2.
внимание: в цикле статей используется минимальная версия - Android 6
В предыдущей статье мы подробно рассмотрели сканирование устройств. Эта статья - о подключении, отключении и обнаружении сервисов (discovering services).
Подключение к устройству
После удачного сканирования, вы должны подключиться к устройству, вызывая метод connectGatt()
. В результате мы получаем объект – BluetoothGatt
, который будет использоваться для всех GATT операций, такие как чтение и запись характеристик. Однако будьте внимательны, есть две версии метода connectGatt()
. Поздние версии Android имеют еще несколько вариантов, но нам нужна совместимость с Android-6 и мы рассматриваем только эти две:
Внутренняя реализация первой версии – это фактически вызов второй версии с аргументом transport = TRANSPORT_AUTO
. Для подключения BLE устройств такой вариант не подходит. TRANSPORT_AUTO
используется для устройств с поддержкой и BLE и классического Bluetooth протоколов. Это значит, что Android будет сам выбирать протокол подключения. Этот момент практически нигде не описан и может привести к непредсказуемым результатам, много людей сталкивались с такой проблемой. Вот почему вы должны использовать вторую версию connectGatt()
с transport = TRANSPORT_LE
:
Первый аргумент – context
приложения.
Второй аргумент – флаг autoconnect
, говорит подключаться немедленно (false
) или нет (true
). При немедленном подключении (false
) Android будет пытаться соединиться в течение 30 секунд (на большинстве смартфонов), по истечении этого времени придет статус соединения status_code = 133
. Это не официальная ошибка для таймаута соединения. В исходниках Android код фигурирует как GATT_ERROR
. К сожалению, эта ошибка появляется и в других случаях. Имейте ввиду, с autoconnect = false
Android делает соединение только с одним устройством в одно и то же время (это значит если у вас несколько устройств - подключайте их последовательно, а не паралелльно).
Третий аргумент – функция обратного вызова BluetoothGattCallback
(callback) для конкретного устройства. Этот колбек используется для всех связанных с устройством операциях, такие как чтение и запись. Мы рассмотрим это более детально в следующей статье.
Autoconnect = true
Если вы установите autoconnect = true
, Android будет подключаться самостоятельно к устройству всякий раз, когда оно будет обнаружено. Внутри это работает так: Bluetooth стек сканирует сохраненные устройства и когда увидит одно из них – подключается к нему. Это довольно удобно, если вы хотите подключиться к конкретному устройству, когда оно становится доступным. Фактически, это предпочтительный способ для переподключения. Вы просто создаете BluetoothDevice
объект и вызываете connectGattwith
с autoconnect = true
.
Обратите внимание, этот подход работает только, если устройство есть в Bluetooth кеше или устройство было уже сопряжено (bonding). Посмотрите мою предыдущую статью, где подробно объясняется работа с Bluetooth кешем.
При перезагрузке смартфона или выключении/включении Bluetooth (а также Airplane режима) – кеш очистится, это надо проверять перед подключением с autoconnect = true
, что действительно раздражает.
Autoconnect работает только с закешированными и сопряженными (bonded) устройствами!
Для того, чтобы узнать, закешировано устройство или нет, можно использовать небольшой трюк. После создания объекта BluetoothDevice
, вызовите у него getType
, если результат – TYPE_UNKNOWN
, значит устройство не закешировано. В этом случае, необходимо просканировать устройство с этим мак-адресом (используя не агрессивный метод сканирования) и после этого можно использовать автоподключение снова.
Android-6 и ниже имеет известный баг, в котором возникает гонка состояний и автоматическое подключение становится обычным (autoconnect = false
). К счастью, умные ребята из Polidea нашли решение для этого. Настоятельно рекомендуется использовать его, если думаете использовать автоподключение.
Преимущества:
- работает достаточно хорошо на современных версиях Android (прим. переводчика - от Android-8 и выше);
- возможность подключаться к нескольким устройствам одновременно;
Недостатки:
- работает медленнее (Android в этом случае сканирует в режиме
SCAN_MODE_LOW_POWER
, экономя энергию), если сравнивать сканирование в агрессивном режиме + подключение сautoconnect = false
;
Изменения статуса подключения
После вызова connectGatt()
, Bluetooth стек присылает результат в колбек onConnectionStateChange
, он вызывается при любом изменении соединения.
Работа с этим колбеком – достаточно нетривиальная вещь. Большинство простых примеров из сети выглядит так (не обольщайтесь):
Этот код обрабатывает только аргумент newState
и полностью игнорирует status
. В многих случаях это работает и кажется безошибочным. Действительно, после подключения, следующее что нужно сделать – это вызвать discoverServices()
. А в случае отключения - необходимо сделать вызов close()
, чтобы Android освободил все связанные ресурсы в стеке Bluetooth. Эти два момента очень важные для стабильной работы BLE под Android, давайте их обсудим прямо сейчас!
При вызове connectGatt()
, Bluetooth стек регистрирует внутри себя интерфейс для нового клиента (client interface: clientIf
).
Возможно вы заметили такие логи в LogCat:
Здесь видно, что клиент 6
был зарегистрирован после вызова connectGatt()
. Максимальное количество клиентов (подключения) у Android равно 30 (константа GATT_MAX_APPS
в исходниках), при достижении которого – Android не будет подключаться к устройствам вообще и вы будете получать постоянно ошибку подключения. Достаточно странно, но сразу после загрузки Android уже имеет 5 или 6 таких подключенных клиентов, предполагаю, что Android использует их для внутренних нужд. Таким образом, если вы не вызываете метод close()
, то счетчик клиентов увеличивается каждый раз при вызове connectGatt()
. Когда вы вызываете close()
, Bluetooth стек удаляет ваш колбек, счетчик клиентов уменьшается на единицу и освобождает ресурсы клиента.
Важно всегда вызывать 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
:
Это не последний вариант, мы еще улучшим колбек в этой статье. В любом случае, теперь у нас есть обработка ошибок и успешных операций.
Состояние bonding (bondState)
Последний параметр, который необходимо учитывать в колбеке onConnectionStateChange
– это bondState
, состояние сопряжения (bonding) с устройством. Мы получаем этот параметр так:
Состояние bonding может иметь одно из трех значений BOND_NONE
, BOND_BONDING
or BOND_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
:
Обработка ошибок
После того как мы разобрались с успешными операциями, давайте взглянем на ошибки. Есть ряд ситуаций, которые на самом деле “нормальные”, но выдают себя за ошибки.
- Устройство отключилось намеренно. Например, все данные были переданы и больше ему нечего делать. Вы получите статус - 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мс).
При удачной отмене вы увидите примерно такое в логах:
Скорее всего, вы никогда не отмените соединение, для параметра autoconnect = false
. Часто это делается для подключений с autoconnect = true
. Например, когда приложение на переднем плане – вы подключаетесь к вашим устройствам и отключаетесь от них, если приложение переходит в фон.
Прим. переводчика: но это не значит что для autoconnect = false
не надо проводить такую отмену! Скорее всего вы не увидите этого в логах.
Обнаружение сервисов (discovering services)
Как только вы подключились к устройству, необходимо запустить обнаружение его сервисов вызовом discoverServices()
. Bluetooth стек запустит серию низкоуровневых команд для получения сервисов, характеристик и дескрипторов. Это занимает обычно около одной секунды в зависимости от того сколько таких служб, характеристик, дескрипторов имеет ваше устройство. В результате будет вызыван колбек onServicesDiscovered.
Первым делом проверим, есть ли какие ошибки после обнаружения сервисов:
Если есть ошибки (обычно это GATT_INTERNAL_ERROR
со значением 129), делаем отключение устройства, что-то исправить здесь невозможно (нет специальных технических способов для этого). Вы просто отключаете устройство и повторно пробуете подключиться.
Если все прошло удачно, вы получите список сервисов:
Кеширование сервисов.
Bluetooth стек кеширует найденные на устройстве сервисы, характеристики и дескрипторы. Первое подключение вызывает реальное обнаружение сервисов, все последующие – возвращаются кешированные версии. Это соответствует стандарту Bluetooth. Обычно это нормально и сокращает время соединения с устройством. Однако в некоторых случаях, может потребоваться очистить кеш, чтобы снова обнаружить их с устройства при следующем соединении. Типичный сценарий: обновление прошивки, в которой изменяется набор сервисов, характеристик, дескрипторов. Есть скрытый метод очистки кеша и добраться до него нам поможет рефлексия:
Этот метод асинхронный, дайте ему некоторое время для завершения!
Странные штуки в подключении/отключении
Хотя операции подключения и отключения выглядят просто, есть некоторые особенности, которые нужно знать.
- Случайная ошибка 133 при подключении, выше мы разобрались как с ней работать;
- Периодическое зависание подключения, не срабатывает таймаут и не вызывается колбек
onConnectionStateChange
. Это случается не часто, но я видел такие случае при низком уровне батареи или когда устройство находится на границе доступности по расстоянию Bluetooth. Скорее всего общение с устройством происходит, но затем прерывается и зависает. Мой обходной путь – использовать свой таймер подключения и в случае таймаута – закрывать соединение и отключаться; - Некоторые смартфоны имеют проблему с подключением во время сканирования. Например, Huawei P8 Lite один из таких. Останавливаем сканнер перед любым подключением (Прим. переводчика: это правило соблюдаем строго!);
- Все вызовы подключения/отключения асинхронные. То есть неблокирующие, но при этом им нужно время, чтобы выполнится до конца. Избегайте быстрый запуск их друг за другом (Прим. переводчика: я обычно использую задержку 50-100мс между вызовами).
Следующая статья: чтение и запись характеристик.
Теперь мы разобрались с подключением/отключением и обнаружением сервисов, следующая статья – о том, как работать с характеристиками.
Не терпится поработать с BLE? Попробуйте мою библиотеку Blessed for Android. Она использует все подходы из этой серии статей и упрощает работу с BLE в вашем приложении.