Хабрахабр

Bluetooth LE не так уж и страшен, или Как улучшить пользовательский опыт без особых усилий

Недавно мы в команде придумали и реализовали функцию передачи денег по воздуху с помощью технологии Bluetooth LE. Я хочу рассказать вам, как мы это сделали и что Apple предоставляет нам из инструментов. Многие разработчики думают что Bluetooth — это сложно, ведь это достаточно низкоуровневый протокол, и по нему не так много специалистов. Но всё не так страшно, и на самом деле использовать эту функцию очень просто! А те функции, которые можно реализовать с помощью Bluetooth LE, безусловно, интересны и впоследствии позволят выделить ваше приложение среди конкурентов.


Давайте сначала разберёмся, что это вообще за технология и в чём её отличие от классического Bluetooth.

Что такое Bluetooth LE?

Почему разработчики Bluetooth назвали эту технологию именно Low Energy? Ведь с каждой новой версией Bluetooth энергопотребление и без того многократно снижалось. Ответ кроется в этой батарейке.

Её диаметр всего 2 см, а ёмкость около 220 мА*ч. Когда инженеры разрабатывали Bluetooth LE, они стремились к тому, чтобы устройство с такой батарейкой работало несколько лет. И у них это получилось! Bluetooth LE-устройства c таким элементом питания могут работать от года. Кто из вас еще по-старинке выключает Bluetooth на телефоне для экономии энергии, как это делали в 2000-м? Зря вы это делаете — экономия будет меньше 10 секунд работы телефона в день. А функциональность вы отключаете очень большую, такую как Handoff, AirDrop и другие.

Они усовершенствовали классический протокол? Чего же инженеры добились, разработав Bluetooth LE? Просто оптимизировали все процессы? Сделали его более энергоэфективным? Они полностью переделали архитектуру стека Bluetooth и добились того, что теперь, чтобы быть видимым для всех других устройств, необходимо меньше времени находиться в эфире и занимать канал. Нет. А с новой архитектурой теперь можно стандартизировать любое новое устройство, благодаря чему разработчики со всего мира могут коммуницировать с устройством, а значит, и с легкостью писать новые приложения для управления им. В свою очередь это позволило хорошо сэкономить на энергопотреблении. Кроме того, в архитектуру заложен принцип self-discovery: при подключении к устройству не нужно вводить никакие пин-коды, и если ваше приложение умеет общаться с этим устройством, подключение занимает считанные миллисекунды.

  • Меньше времени в эфире.
  • Меньше расход энергии.
  • Новая архитектура.
  • Уменьшено время подключения.

За счёт чего удалось инженерам сделать такой колоссальный скачок в энергоэффективности?

А вот задержка подключения стала меньше: 15-30 мс вместо 100 мс у классического Bluetooth. Частота осталась та же: 2,4 ГГц, не сертифицируемая и свободная для использования во многих странах. Интервал передачи не сильно, но изменился — вместо 0,625 мс стало 3 мс. Расстояние работы осталось таким же — 100 м.

Конечно же, что-то должно было пострадать. Но не могло же из-за этого энергопотребление уменьшиться в десятки раз. Вы, наверное, скажете, что это смешная скорость для 2018 года. И это скорость: вместо 24 Мбит/с стало 0,27 Мбит/с.

Где используется Bluetooth LE?

И уже успела завоевать много сфер. Технология эта немолодая, впервые она появилась в iPhone 4s. Сейчас уже есть даже чипы размером с кофейное зерно. Bluetooth LE используется во всех устройствах умного дома и в носимой электронике.

А как эта технология применяется в программном обеспечении?

И сейчас вы можете встретить эту технологию в таких сервисах, как AirDrop, Devices quick start, Share passwords, Handoff. Поскольку Apple была первой, кто встроил в своё устройство Bluetooth и начал её использовать, то к настоящему времени они достаточно хорошо продвинулись и встроили технологию в свою экосистему. Вдобавок, Apple выложила в открытый доступ документацию, как сделать так, чтобы на ваши собственные устройства приходили уведомления из всех приложений. И даже уведомления в часах сделаны через Bluetooth LE. Какие бывают роли устройств в рамках Bluetooth LE?

По такому принципу работают iBeacons и навигация в помещениях. Broаdcaster. Отправляет сообщения всем, кто находится рядом, к этому устройству нельзя подключиться.

Соединения не создаёт. Observer. Слушает, что происходит вокруг, и получает данные только от общедоступных сообщений.

Почему их не назвали просто Server-Client? А вот с Central и Peripheral интереснее. А вот и нет. Логично же, судя по названию.

Это периферийное устройство, которое потребляет меньше энергии и к которому подключается более мощный Central. Потому что Peripheral, на самом деле, выступает как сервер. К нему может подключиться только одно устройство, и у Peripheral есть какие-то данные. Peripheral может извещать, что он находится рядом и какие у него есть службы. А Central может сканировать эфир в поиске устройств, отправлять запросы на подключение, подключаться к любому количеству устройств, может читать, записывать и подписывать на данные у Peripheral.

Что же нам, как разработчикам, доступно в экосистеме Apple?

Что нам доступно?

iOS/Mac OS:

  • Peripheral и Central.
  • Фоновый режим.
  • Восстановление состояния.
  • Интервал подключения 15 мс.

watchOS/tvOS:

  • watchOS 4+/tvOS 9+.
  • Только Сentral.
  • Максимум два подключения.
  • Apple watch series 2+/ AppleTv 4+.
  • Отключение при переходе в фоновый режим.
  • Интервал подключения 30 мс.

Самое важно различие — интервал подключения. На что он влияет? Чтобы ответить на этот вопрос, сначала нужно разобраться, как работает протокол Bluetooth LE и почему такая небольшая разница в абсолютных значениях очень важна.

Как работает протокол

Как происходит процесс поиска и подключения?

Интервал может быть достаточно большим и способен варьироваться в зависимости от текущего статуса устройства, режима энергосбережения и других настроек. Peripheral сообщает о своем присутствии с частотой advertisement-интервала, его пакет очень маленький и содержит всего несколько идентификаторов сервисов, которые предоставляет устройство, а также имя устройства. Advertisement-интервал никак не коррелирует c интервалом подключения и определяется самим устройством в зависимости от энергопотребления и своих настроек. Apple советует разработчикам внешних устройств привязывать длину интервала к акселерометру: увеличивать интервал, если устройством не пользуются, а когда оно активно — уменьшать, чтобы быстро находить устройство. Нам он в экосистеме Apple недоступен и неизвестен, им полностью управляет система.

Но это при подключении, а что же происходит при чтении/записи? После того, как мы нашли устройство, отправляем запрос на подключение, и вот тут на сцену выходит интервал подключения — время, через которое второе устройство может ответить на запрос.

Но нужно понимать, что если оба устройства не поддерживают одинаковый интервал, то будет выбран максимальный из них. Интервал подключения также фигурирует и при чтении данных — его уменьшение в 2 раза увеличивает скорость передачи данных.

Давайте рассмотрим, из чего состоит пакет с информацией, который передает Peripheral.

В протоколе версии 4. MTU (maximum transmission unit) такого пакета определяется в процессе подключения и варьируется от устройства к устройству и в зависимости от операционной системы. В версии 4. 0 MTU был около 30, и размер полезных данных не превышал 20 байтов. Но, к сожалению, эту версию протокола поддерживают только устройства младше IPhone 5s. 2 всё поменялось, теперь можно передавать около 520 байтов. С записью, в целом, похожая ситуация. Размер накладных расходов, независимо от размера MTU, составляет 7 байтов: сюда входят ATT и L2CAP заголовков.

Режим без ответа значительно ускоряет передачу данных, поскольку нет интервала ожидания перед следующей записью. Есть только два режима: с ответом и без. Доступ к этому режиму записи может ограничить сама система, потому что он считается менее энергоэкономичным. Но этот режим доступен не всегда, не на всех устройствах и не на всех системах. В iOS eсть метод, в котором можно проверить перед записью, доступен ли такой режим.

Теперь давайте рассмотрим, из чего состоит протокол.

Слой приложения — эта ваша логика, описанная поверх CoreBluetooth. Протокол состоит из 5 уровней. ATT (Attributes Layer) используется для управления вашими характеристиками и передачей ваших данных. GATT (Generic Attributes Layer) служит для обмена сервисами и характеристиками, которые есть на устройствах. Controller — это уже сам BT-чип. L2CAP — низкоуровневый протокол обмена данными.

Вы, наверное, спросите, что такое GATT и как мы можем с ним работать?

Характеристика — это объект, в котором хранятся ваши данные, словно переменная. GATT состоит из характеристики и сервисов. У сервиса есть название — UUID, вы сами его выбираете. А сервис — это группа, в которой находятся ваши характеристики, словно пространство имён. Сервис может содержать в себе дочерний сервис.

Значение (Value) характеристики — это NSData, сюда вы можете записывать и хранить данные. У характеристики тоже есть свой UUID — фактически, имя. В протоколе Bluetooth есть много дескрипторов, но в Apple-системах пока доступно только два: человеческое описание и формат данных. Дескрипторы — это описание вашей характеристики, вы можете описать, какие данные вы ожидаете в этой характеристике, или что они означают. Также есть уровни доступа (Permissions) для вашей характеристики:

Попробуем сами

У нас появилась идея сделать возможность передачи денег по воздуху, ничего не требуя от получателя. Представьте, вот ломаете голову над очень интересной задачей, пишете идеальный код, и тут коллега предлагает сходить за кофе. А вы так увлечены задачей, что не можете отлучиться, и просите его купить вам чашечку вкусного капучино. Он приносит вам кофе, и нужно вернуть ему деньги. Можно перевести по номеру телефона, работает отлично. Но вот неловкая ситуация — вы не знаете его номера. Ну вот так, три года работаете, а номерами не обменялись 🙂

Как в AirDrop. Поэтому мы решили сделать возможность передачи денег тем, кто находится рядом, без ввода каких-либо пользовательских данных. Давайте посмотрим, что нам для этого нужно. Просто выбрать пользователя и отправить нужную ему сумму.

Отображение PUSH

Нам нужно, чтобы отправитель:

  1. Мог найти все устройства, которые находятся рядом и поддерживают наш сервис.
  2. Мог прочитать реквизиты.
  3. И мог отослать сообщение получателю о том, что успешно отправил ему деньги.

Получатель, в свою очередь, должен уметь сообщать окружающим отправителям, что у него есть сервис с нужными данными, и уметь получать сообщения от отправителя. Как происходит процесс перевода денег по реквизитам у нас в банке, думаю, описывать не стоит. А теперь попробуем это реализовать.

Как я говорил, это UUID. Для начала нужно придумать названия нашего сервиса и характеристик. Просто генерируем их и сохраняем на Peripheral и Central, чтобы на обоих устройствах были одинаковые.

Вы сами можете купить себе такой номер и никто его использовать не будет. Вы вольны использовать любые UUID, кроме тех, которые оканчиваются вот так: XXXXXXXX-0000-1000-8000-00805F9B34FB, — они зарезервированы под разные компании. Это будет стоить $2500.

Нужно просто указать делегатов. Далее нам нужно будет создать менеджеры: один для передачи денежных средств, другой для получения. Мы создаем оба, потому что и отправителем, и получателем может быть одно лицо в разное время. Передавать у нас будет Central, получать Peripheral.

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

Пропишем UUID и укажем, что он primary — то есть сервис является главным для этого устройства. Для начала создадим сервис. Хороший пример: пульсомер, для которого главным сервисом будет текущее состояние пульса, а состояние батареи — это второстепенная информация.

Регистрируем их в нашем сервисе, потом добавляем в менеджер, запускаем обнаружение и указываем UUID сервиса, чтобы все устройства, которые находятся рядом, могли узнать о нашем сервисе до подключения к нему. Далее создаем две характеристики: одну для чтения реквизитов получателя, вторую для записи, чтобы получатель мог узнать об отправке денег. Эти данные помещаются в пакет, который отправляет Central в ходе вещания.

Запустим поиск и подключение. Получатель готов, приступим к отправителю.

При нахождении мы их получаем в методе делегата и сразу подключаемся. При включении менеджера мы запускаем поиск устройств с нашим сервисом. Важно: нужно сохранять strong-ссылку на все Peripheral, с которыми вы работаете, иначе они «утекут».

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

Мы успешно подключились к получателю, теперь нужно прочитать его реквизиты.

И после их получения будет вызван метод делегата, в котором будут перечислены все сервисы, доступные на данном устройстве. Мы после подключения уже запросили все сервисы с устройства. Результат можно будет найти по UUID в методе делегата, в котором хранятся данные для перевода. Находим нужный и запрашиваем его характеристики. Все сервисы, характеристики и их значения кешируются системой, так что запрашивать их впоследствии каждый раз необязательно. Пробуем их прочитать, и получим искомое опять в методе делегата.

Для этого нужно реализовать процесс отправки сообщения. Всё, мы отправили деньги за кофе, самое время показать получателю красивое уведомление, чтобы он ждал рубли на своем счёте.

Но до этого вам её нужно получить с устройства, как мы делали до этого. У отправителя достаем нужную нам характеристику, в данном случае мы её взяли из сохраненного значения. А дальше просто записываете данные в нужную характеристику.

Тут вы можете прочитать данные, которые вам отправляют, ответить на какую-либо ошибкой, например, нет доступа, или данной характеристики не существует. После этого на другом устройстве получаем в методе делегата запрос на запись. А нам нужно, чтобы работало и в фоне! Всё будет работать, но только если оба устройства включены и приложения активны.

Для этого нужно в info.plist указать ключ, в каком режиме мы хотим использовать, в Peripheral или Central. Apple позволяет использовать Bluetooth в фоне.

Теперь нам доступен и фоновый режим. Далее в менеджере нужно указать ключ восстановления и создать метод делегата. Если приложение заснёт или будет выгружено из памяти, то при нахождении нужного Peripheral или при подключении Central оно проснётся, а менеджер восстановится с вашим ключом.

Но тут к нам прибегают дизайнеры и говорят: «Хотим вставить фотографии пользователей, чтобы им было легче находить друг друга». Всё отлично, уже готовы релизиться. У нас в характеристику можно записать всего какие-то 500 байтов, а на каких-то устройствах вообще 20 🙁 Что же делать?

Спустимся глубже

Чтобы решить эту проблему, нам пришлось спуститься глубже.

Но в iOS 11 у нас есть доступ к протоколу L2CAP. Сейчас мы общались устройствами на уровне GATT/ATT. Пакеты отправляются с MTU 2 Кб, не нужно ни во что перекодировать, применяется обычный NSStream. Однако в этом случае придётся самостоятельно позаботиться о передаче данных. Скорость передачи данных до 394 Килобит/с., по заверению Apple.

И понадобилось открыть канал. Допустим, вы передаёте какие-либо данные вашего сервиса от Peripheral к Central в виде обычных характеристик. Номер динамический, система сама выбирает, какой PSM открыть в данный момент. Вы открываете его на Peripheral, в ответ получаете PSM — это номер канала, к которому можно подключиться, и нужно с помощью тех же характеристик передать его Central. Давайте рассмотрим, как это сделать. После передачи можно уже на Сentral подключиться к Peripheral и обмениваться данными в удобном для вас формате.

Можно делать и без шифрования, тогда это немного ускорит передачу. Для начала на Peripheral открываем порт с шифрованием.

Далее мы в методе делегата получаем PSM и отправляем на другое устройство.

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

С Central еще проще, мы просто подключаемся к каналу с нужным номером…

В них вы можете передавать абсолютно любые данные любого размера, и строить поверх L2CAP свой протокол. … и после этого получаем нужные нам стримы. Так мы и реализовали у себя передачу фотографий получателя.

Но есть подводные камни, куда же без них.

Подводные камни

Давайте рассмотрим подводные камни при работе в фоновом режиме. Поскольку вам доступны роли Peripheral и Central, вы можете подумать. что в фоне можете определять, какие устройства рядом находятся в фоновом режиме, а какие в активном. В теории так и должно было быть, но Apple ввела ограничение: телефоны, которые находятся в фоновом режиме, будь то Central или Peripheral, не доступны для других телефонов, которые тоже находятся в фоновом режиме. Также телефоны, которые находятся в фоновом режиме, не видны с неiOS-устройств. Давайте рассмотрим почему так происходит.

которые предоставляет это устройство. Когда ваше устройство активно, оно посылает обычный broadcast-пакет, в котором может быть имя устройства и список сервисов. И overflow данные — всё что не поместилось.

Если приложение активно, то при сканировании с iOS-устройства оно читает эти данные, а при переходе в фон — игнорирует. Когда же устройство переходит в фоновый режим, оно не передает название, а список поддерживаемых сервисов переносит в overflow-данные. Остальные операционные системы Apple всегда игнорируют overflow-данные, поэтому если вы будете искать устройства, поддерживающие ваш сервис, то получите пустой массив. Поэтому при переходе в фон вы не сможете видеть приложения, которые также находятся в фоне. А если подключиться к каждому устройству, которое находится рядом, и запросить поддерживаемые сервисы, то в списке, возможно, будет ваш сервис, и вы сможете с ним работать.

И вдруг в какой-то момент мы стали получать в консоли эту ошибку: Дальше мы уже готовились передавать в тестирование, правили мелкие недочёты, занимались оптимизацией.

CoreBluetooth[WARNING] Unknown error: 124

Самое плохое было в том, что никакой метод делегата не вызывался, мы даже не могли никак обыграть эту ошибку для пользователя. Просто сообщение в лог — и тишина, всё замирало. Никаких особых изменений не вносилось, поэтому мы начали откатываться по коммитам. И обнаружили, что однажды оптимизировали код и переделали способ записи данных. Проблема крылась в том, что не все клиенты были обновлены, поэтому возникала такая ошибка.

.write != .writeWithoutResponse

Мы, счастливые, что всё исправили, побежали скорее передавать в тестирование, а они нам почти сразу возвращают: «Ваши модные фоточки не работают. Они все недогруженные приходят». Мы начали пробовать, и правда, иногда, на разных устройствах, в разное время приходят битые фотографии. Начали искать причину.

Сразу подумали, что дело в разных версиях. И тут снова увидели прежнюю ошибку. Мы взгрустнули… Но после полного удаления старой версии со всех тестовых устройств ошибка всё равно воспроизводилась.

CoreBluetooth[WARNING] Unknown error: 722
CoreBluetooth[WARNING] Unknown error: 249
CoreBluetooth[WARNING] Unknown error: 312

Начали искать инструмент для отладки. Первое, что нам попалось, это Apple Bluetooth Explorer. Мощная программа, много всего умеет, но вот для отладки протокола Bluetooth LE одна маленькая вкладка с поиском устройств и получением характеристик. А нам-то нужно было анализировать L2CAP.

Оказалась вполне приличная программа, правда, с дизайном из iOS 7. Потом нашли LightBlue Explorer. И работает стабильнее. Может делать то же самое, что и Bluetooth Explorer, а еще умеет подписываться на характеристики. Всё хорошо, но опять без L2CAP.

И тут нам вспомнился всем известный сниффер WireShark.

Хотя это не страшно, что мы, не найдем винду, что ли. Оказалось, он знаком с Bluetooth LE: может читать L2CAP, но только под Windows. То есть нужно было найти где-то устройство в официальном магазине. Самый большой минус — программа работает только с определенным устройством. Мы даже начали просматривать зарубежные онлайн-магазины. А вы сами понимаете, в большой компании вряд ли одобрят покупку непонятного устройства на барахолке.

Она позволяет смотреть траффик, которой идет на OS X-устройстве. Но тут обнаружили в Additional Xcode Tools программу PacketLogger. Он у нас уже был отдельной библиотеки. А почему бы не переписать наш MoneyDrop под OS X? Мы просто заменили UIImage на NSImage, всё завелось само через 10 минут.

Сразу стало понятно, что в момент передачи данных по L2CAP записывалась одна из характеристик. Наконец-то мы могли читать пакеты, которыми обмениваются устройства. После исправления проблем с передачей фотографии не было. А из-за того, что канал был полностью занят передачей фотографии, iOS игнорировала запись, а отправитель после игнора обрывал канал.

На этом всё, спасибо за прочтение 🙂

Полезные ссылки

WWDC/CoreBluetooth:
Bluetooth
YouTube

  • Arrow Electronics → Bluetooth Low Energy Series
Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть