Содержание
Часть #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 =
false
Android делает соединение только с одним устройством в
одно и то же время (это значит если у вас несколько устройств -
подключайте их последовательно, а не паралелльно). Третий аргумент
функция обратного
вызова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_BONDING
orBOND_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 в вашем приложении.