Noveo

Наш блог Основы работы с Bluetooth в iOS

Основы работы с Bluetooth в iOS

Привет! Меня зовут Кирилл, я старший разработчик в аутсорс-компании Noveo. Наши клиенты очень разные, задачи у них тоже разные, и мы с удовольствием решаем их все. Одно из моих любимых направлений — это связь мобильных приложений с объектами вне их инфраструктуры, с различными интересными гаджетами, датчиками и объектами из мира интернета вещей. Сегодня поговорим как раз про это направление, а точнее, про Bluetooth.

Noveo working with Bluetooth

Наверное, никому из вас не нужно объяснять, что такое блютус и зачем он применяется. Сейчас эта технология беспроводной передачи данных имеет индекс версии 5.1 и очень сильно скакнула в развитии. Свой первый айфон я купил в 2009 году, и даже тогда мне приходилось выслушивать тонны хейта по поводу несовершенства этого девайса. Одним из саааамых частых и бесящих меня поинтов было постоянные упреки в отсутствии передачи файлов по блютусу. И вообще в неполноценности Bluetooth на iOS и невозможности им пользоваться, кроме как подключить беспроводные наушники. Курьезный факт, но уже через 3 года iOS имела самый мощный среди мобильных платформ фреймворк, который конкуренты еще долго не смогут догнать (ИМХО, и не догнали). И правда, большинство вещей, о которых я сегодня расскажу, были представлены в 2012 году (а часть и вовсе в 2011) и практически не менялись с тех пор.

Noveo working with Bluetooth timeline

Итак, блютус. Я не буду давать каких-то формальных объяснений или уходить глубоко в обзорную историю. Пройдусь максимально поверхностно. Для нас, как для разработчиков, это интерфейс взаимодействия с внешними устройствами. История его развития начинается в далёком 1998 году, но более-менее массовое распространение он получил в середине 2000-х с широким развитием рынка смартфонов и коммуникаторов. На заре iOS, вплоть до 4-й версии, разработчики не имели возможности взаимодействовать с устройствами посредством блютус вообще, но всё изменилось в 2011 году, с выходом iOS 5. Ключом к такому стремительному росту стало принятие в 2010 году стандарта Bluetooth 4.0, который стал отвечать требованием рынка по параметрам скорости и энергопотребления. Последнее играло ключевую роль. Именно тогда появился стандарт, а точнее спецификация, BLE — Bluetooth Low Energy, который позволил сильно расширить сферу применения этой беспроводной технологии.

 

В чем же особенности BLE? Ну, как не сложно догадаться из названия, главная фишка в экстремально (на 2010 год уж точно) низком энергопотреблении. Спецификация позволяет разрабатывать устройства, которые будут минимально расходовать свою батарею и работать до одного года на одном заряде небольшого элемента питания. Спецификацию разработала компания Bluetooth Special Interest Group (SIG) и эти ребята настолько заморочились, насколько, как мне кажется, это было вообще возможно. Чтобы не давать волю полёта фантазии разработчикам и не плодить кучу конкурирующих стандартов, SIG разработала спецификацию под все возможные типы устройств, начиная с носимых датчиков пульса, заканчивая стационарными дверными замками. Именно взаимодействие через Bluetooth LE нам доступно через встроенный фреймворк CoreBluetooth. Apple тоже проделали гигантскую работу, скрыв от нас все детали работы с низкоуровневыми штучками, и предоставили очень простой API, с которым разберется и любой начинающий iOS-разработчик!

Bluetooth Low Energy в деталях

Основой BLE является профиль GATT — General Attribute Profile, который предоставляет нам абстракции сервиса и характеристик. Характеристика — это, грубо говоря, ячейка данных, а сервис — это некоторое логическое их объединение. Может звучать непонятно, но сейчас я приведу пример, и всё сразу встанет на свои места. Самый классический и на самом деле распространенный пример — термометр. Он может иметь сервис измерения температуры и иметь две характеристики: единицу измерения (℃, ℉) и собственно саму температуру. А еще он может определять влажность и для неё будет отдельный сервис с одной лишь характеристикой — влажности в процентах.

Noveo working with Bluetooth thermometer

Всё взаимодействие в BLE строится на классической клиент-серверной модели: тут тоже есть клиент и сервер, есть запросы. С этого момента я буду плавно переходить к самому фреймворку CoreBluetooth и объяснять новые термины уже ближе к предметной области, т.к. большинство терминов совпадают с терминами BLE, только имеют префикс CB — CoreBluetooth.

CoreBluetooth

Итак, CoreBluetooth. Как уже было сказано, появился в iOS 5, был серьезно доработан в iOS 6 и существует до сих пор, не претерпев каких-либо серьезных изменений с тех пор. Как я уже сказал, всё взаимодействие строится на классической клиент-серверной модели, и в CB у нас есть сервер Peripheral и клиент Сentral. Такой нейминг может слегка путать некоторое время, т.к. мы привыкли, что именно сервер является неким центром, а CB переворачивает все с ног на голову. Но с другой стороны, всё логично: Peripheral device — периферийное устройство, внешнее по отношению к нашему, центральному. Peripheral device’ом может быть фитнес-трекер, умная лампочка, замок или целая система умного дома, а может быть и другое мобильное устройство или компьютер.

Noveo working with Bluetooth Peripheral and Central

Поиск

Хотя процесс и является клиент-серверным, в отличии от HTTP, точный адрес или алиас устройства нам не известен, поэтому, чтобы начать с ним взаимодействие, необходимо его найти в эфире. Процесс обнаружения устройства называется сканированием или дискаверингом. Он выполняется Central-устройством, той стороной, которая хочет к чему-то подключиться. Процесс заключается в сканировании Bluetooth-эфира на предмет так называемых Advertisment-пакетов, которые должно отправлять периферийное устройство. Advertisement-пакет — это крохотный пакет, регулярно отправляемый периферийным устройством, когда оно хочет, чтобы его нашли. Он обычно содержит базовую информацию, необходимую для того, чтобы понять класс устройства: термометр, замок, самокат или другой айфон. В зависимости от режима работы устройства и настроек его энергопотребления пакеты могут рассылаться с различными интервалом, от раза в несколько микросекунд до раза в десятки секунд, поэтому длительность сканирования напрямую зависит от режима работы периферийного устройства.

Noveo working with Bluetooth Search

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

 

Итак, мы хотим найти термометр, который для нас является периферийным устройством. Значит, мы в этом случае будем клиентом, то есть Central Device. Объект, который представляет клиента в фреймворке CoreBluetooth, имеет тип CBCentralManager. Как несложно догадаться, всё взаимодействие c внешним устройством предполагается асинхронным. И еще проще догадаться, что Apple предпочла паттерн «делегат» для обеспечения асинхронной работы. Нужный нам делегат имеет тип CBCentralManagerDelegate и объект, реализующий этот протокол, и будет получать уведомления о событиях, связанных с поиском и подключением.

 

Как только мы будем готовы сканировать эфир, нужно вызвать метод scanForPeripherals у менеджера CBCentralManager. Как только наше устройство увидит новый рекламный пакет, будет вызван метод делегата didDiscoverPeripheral с объектом CBPeripheral в параметрах метода. Когда мы найдем нужное устройство или устройства, нам нужно остановить сканирование методом stopScan и вызвать connect(to:), указав экземпляр класса CBPeripheral.

Noveo working with Bluetooth Peripheral

Поиск сервисов

Как только соединение будет установлено, мы можем начать опрашивать устройство. С этого момента мы уже работаем с объектом CBPeripheral и его делегатом CBPeripheralDelegate, которые представляют внешнее устройство, к которому мы подключились. Чтобы считать какое-то значение (характеристику), сначала нужно найти сервис, в составе которого эта характеристика существует. Запустим поиск сервисов. Мы можем как указать сервис или сервисы, которые нас точно интересуют, так и не указывать их и найти все, что реализует устройство.

UUID

Тут нужно отвлечься и объяснить, как мы можем отличать сервисы друг от друга. Идентификатором для них и их характеристик служит UUID, уникальный идентификатор, который бывает двух видов: 16 бит и 128 бит. 128-битные идентификаторы мы можем генерировать и использовать на своё усмотрение при написании собственных клиент-серверных приложений. А вот 16-битные зарезервированы SIG. Помните, я говорил, что эти ребята сильно упоролись и описали почти всё, что взаимодействует или может взаимодействовать по BLE? Так вот они пронумеровали все эти сервисы и характеристики и дали им уникальный 16-битный UUID. Если вы когда-либо захотите разработать железное устройство, с которым сможет работать не только ваше приложение, то вам следует изучить их спеки и следовать этим рекомендациям. Ну а если вы разрабатываете приложение для какого-то класса устройств, вы можете использовать эту спеку, чтобы найти идентификатор нужного вам сервиса. Мы ищем термометр, можем смело указывать UUID 0x1809 и игнорировать остальные (если они есть).

Поиск характеристик

Окей, мы нашли сервисы, о чем получим уведомление в делегате. Теперь схожим образом можем найти характеристики в них, для каждой вызовем discoverCharacteristics(for:). Точно так же мы можем указать UUIDы интересующих нас характеристик и получить все, но это будет медленнее. Когда процесс исследования характеристик завершится, делегат получит уведомление вызовом метода didDiscoverCharacteristicsForService, а сами характеристики будут доступны в соответствующем свойстве.

Noveo working with Bluetooth Searching for characteristics

Характеристики

На этом этапе мы наконец добрались до самого «вкусного» — до данных, а точнее, до характеристик. В большинстве несложных кейсов их можно разделить по типам операций, которые они поддерживают: read, write, notify. На самом деле опций (корректнее — свойств) несколько больше, но в рамках сегодняшней статьи мы рассмотрим только эти.

Noveo working with Bluetooth Characteristics

Характеристика должна поддерживать хотя бы одну из них, но может поддерживать и все вместе. Интересным тут является тип notify. Как несложно догадаться, вместо прямого считывания у нас есть возможность получить уведомление, когда устройство самостоятельно решит отправить нам данные, например, когда они изменились. Возвращаясь к примеру с термометром, характеристика с единицей измерения будет иметь флаг read и write, а сама температура — read и notify. Единицу измерения достаточно считать при подключении, а вот температуру считывать по таймеру — не лучшее решение. Мы же пытаемся быть Low Energy — постоянно опрашивать устройство может быть накладно как для нас, так и для периферийного устройства.

 

Для работы с характеристиками используется три основных метода:

 

— readValue(for characteristic: CBCharacteristic)

— writeValue(_ data: Data, for characteristic: CBCharacteristic, type: CBCharacteristicWriteType)

— setNotifyValue(_ enabled: Bool, for characteristic: CBCharacteristic)

 

С чтением данных всё просто: дернули метод readValue — отправится запрос на устройство. Ответ получим в делегате CBPeripheralDelegate, когда данные придут. То же самое произойдет, если данные обновятся и мы будем подписаны на них с помощью setNotifyValue().

 

Запись же бывает двух видов — withResponse и withoutResponse. Не вдаваясь в подробности, это запись с и без подтверждения. Конечно, withResponse медленнее, т.к. на каждый запрос записи будет генерироваться ответ. Это особенно стоит учитывать для передачи относительно больших объемов данных — запись withoutResponse будет предпочтительнее в этом случае.

Ограничения

Кстати, а что по лимитам? Не забываем, что в названии маячит LE, что значит, что он был спроектирован с максимальной экономией на всём, в том числе и размерах пакетов. BLE — это совсем не про стриминг и большие объемы данных. Скорость по стандарту 4.0, согласно Википедии, всего 0,27 Мбит/сек.

 

По умолчанию размер пакета с полезной информацией равен 23 байта, из которых 3 зарезервированных системой. Остаётся 20 байт полезной нагрузки. При работе с BLE вам придется столкнуться с этим ограничением и придумывать, как делить ваши данные на пакеты вручную, т.к. очень часто 20 байт оказыватся недостаточно. В ряде случаев, когда обмен информацией будет происходить с современным смартфоном, данное ограничение может быть увеличено вплоть до 512 байт. CoreBluetooth берет на себя ответственность за переговоры о максимальном MTU, равно как и за автоматическое деление на пакеты, если это поддерживается устройством. Если вам нужна другая скорость или объемы, то, вероятно, вам нужно использовать L2CAP-канал и работать более низкоуровнево, минуя GATT, благо такая возможность есть с iOS 11 (только для устройств Apple).

Серверная часть

На данном этапе вы уже познакомились со всеми ключевыми классами и понятиями CoreBluetooth и, скорее всего, уже сможете написать своё первое приложение, которое будет подключаться к BLE-устройству. А что, если этим устройством будет другой айфон? Давайте вкратце рассмотрим обратную сторону — сторону сервера, или, в нашем случае, Peripheral.

 

По аналогии с CBCentralManager, для создания сервера у нас есть CBPeripheralManager и его CBPeripheralManagerDelegate. Для создания нашего сервиса нам нужно будет в первую очередь собрать сервис и его характеристики. Пусть у нас будет только один сервис с единственной характеристикой, доступной только для чтения. Ответная часть CBService на стороне сервера — это CBMutualService. Да, API CoreBluetooth не «освифтили», поэтому у нас есть префикс Mutual для мутабельных версий классов. При создании нам нужно указать UUID, который можно сгенерировать в консоле командой uuidgen и указать, основной ли это сервис у нашего устройства.

 

После создания сервиса его нужно наполнить характеристиками. Их представляет класс CBMutableCharacteristic. При его создании указывается

 

— UUID,

— свойства,

— начальное значение (для статичных характеристик)

— и права.

 

Собираем наше крохотное дерево с одним листочком, добавляем в менеджер — и можем заявить о себе путем рассылки рекламных пакетов. Для этого у CBPeripheralManager есть метод startAdvertising. На вход он принимает словарь с параметрами рассылки. Я не хочу углубляться сейчас в детали структуры рекламного пакета, но кое-что хотел бы подсветить.

 

В начале рассказа о CBCentral я намеренно упустил момент, что при сканировании эфира можно указать UUID сервисов, в которых мы заинтересованы. Согласитесь, было бы глупо и долго сканировать эфир, и потом подключаться ко всем найденым устройствам, просто чтобы узнать, есть ли у них искомый сервис или нет. Вместо этого сервер может добавить в рекламный пакет UUID наших главных сервисов, а клиенты при сканировании смогут фильтровать устройства, которые им не интересны без подключения и считывания списка сервисов. Сделать это мы можем, как раз указав соответствующий ключ в упомянутый ранее словарь на начале Advertising`а.

 

Как только начнется рассылка пакетов, мы получим соответствующее уведомление в делегат. И как только к нам подключиться клиент… Ничего не произойдет. У делегата менеджера нет соответствующего метода. Вместо этого у нас есть методы для реакции на события чтения, записи и подписки на характеристики. Используя эти методы, мы можем решить, готовы ли мы сейчас предоставить или записать значение, провалидировать его и так далее.

Состояния

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

  • unknown,
  • resetting — BLE-стек перезагружается,
  • unsupported — BLE не поддерживается,
  • unauthorized — пользователь отклонил передачу прав,
  • poweredOff — Bluetooth выключен,
  • poweredOn — Bluetooth включен, можно работать.

 

Начинать поиск устройств, рассылать Advertisement-пакеты и даже добавлять сервисы в менеджер можно только в poweredOn состоянии, поэтому обделить вниманием этот метод делегата не получиться, на это нам намекает и сам протокол делегата менеджера — метод для отслеживания состояния единственный помечен как обязательный.

Безопасность

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

 

В случае, когда требуется дополнительная защита, можно установить соответствующие права на характеристику. При попытке чтения или записи такой характиристики сервер отклонит процесс, сообщив, что общение должно быть зашифровано, и ответив на запрос ошибкой Insufficient Authentication. Чтобы продолжить, клиенту нужно создать защищенное соединение, которое мы чаще привыкли называть пейрингом, реже — бондингом.

 

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

 

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

Noveo working with Bluetooth Security

Пермишены

Говоря о безопасности, стоит упомянуть о том, что с недавних пор Apple требует указать в Info.plist текст с объяснением для конечного пользователя: для чего вашему приложению нужен доступ к Bluetooth. При первой попытке сканирования или начала отправки рекламных пакетов система спросит пользователя, разрешает ли он использовать Bluetooth, точно так же, как это обычно происходит при запросе прав на фотогалерею или пуш-уведомления. У соседей по платформе, в Android, система также требует прав на локацию низкой точности, т.к. потенциально сканирование можно использовать для определения положения пользователя. Возможно, такие правила не за горами и в iOS.

Переподключение

При работе с Bluetooth, как и с любой другой технологией связи, случаются разрывы соединения. Разрыв может быть вызван как по инициативе одной из сторон, так и при потере сигнала. Фреймворк CoreBluetooth устроен таким образом, что пытается постоянно поддерживать соединение до тех пор, пока не будет вызван метод cancelConnection(from:). Таким образом, если связь прервалась по причине неустойчивого соединения, или же при выключении удаленного устройства, CoreBluetooth будет пытаться восстановить соединение в фоновом режиме, пока запущено ваше приложение. Он самостоятельно будет сканировать эфир, и как только появится возможность соединиться, сделает это. Кажется довольно простой вещью, но поверьте — вам не захочется писать этот алгоритм с нуля. Коллеги по платформе, опять же, не имеют такого механизма из коробки, а команда iOS экономит несколько дней обсуждений и имплементации.

 

В примере выше мы рассмотрели кейс, где дисконнект был неожиданным для нас явлением. Но стоит рассмотреть переподключение как естественный процесс жизненного цикла. Допустим, поработав с устройством пару минут, мы узнали от него всё, что хотели на данный момент, и не хотим больше тратить ресурсы на поддержание соединения. В этом случае мы можем сохранить уникальный UUID периферийного устройства и вызвать метод cancelConnection(from:). Позже, когда нам снова потребуются какие-то данные от устройства, мы можем использовать сохраненный UUID для подключения. Подсвечу тот факт, что метод connect принимает на вход параметр «экземпляр класса CBPeripheral», который не имеет публичного конструктора. Для получения экземпляра у нас есть метод retrievePeripherals(withIdentifiers:) у CBCentralManager, которому можно скормить массив UUID.

 

Важно, что метод работет уже не с CBUUID, а с UUID из Foundation библиотеки, т.к. это уже платформенная реализация и не имеет референса в спецификации CoreBluetooth. После маппинга UUID в массив CBPerepheral можно к ним подключаться.

 

Тут может возникнуть вопрос: как быть, если искомое устройство не в зоне видимости Bluetooth или же ушло в сон на какое-то время. В этом случае… ничего не произойдет. Мы не получим ошибки таймаута или какой-то другой. CoreBluetooth будет пытаться подключиться к устройству бесконечно, пока вы не вызовете cancelConnection(from:). Такая логика может показаться странной: действительно, зачем мне бесконечно подключаться к термометру, если он не в зоне видимости? Но не забываем, что сценариев использования очень много, и мы работаем с его LE-версией. Это значит, что какие-то устройства by design могут выходить в эфир на довольно редкие промежутки времени для экономии батареи. Тогда такое поведение приобретает смысл. В конечном итоге накинуть логику с таймаутом намного легче, чем написать цепочку переподключения, будь этот таймаут системным.

Работа в бэкграунде

Как и все ресурсоёмкие операции в iOS, работа с устройствами в бэкграунде по умолчанию недоступна. При сворачивании приложения, будь оно сервером или клиентом, оно потеряет соединение и, как и все остальные, перестанет обслуживать все очереди. Но по возвращению в форграунд CoreBluetooth восстановит соединение.

 

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

 

Для каждой роли, сервера и клиента, есть отдельный чекбокс. Можно отметить оба. При активации бэкграунд-мода приложение сможет взаимодействовать с устройствами, читать и писать характеристики, сканировать сеть и т.д., но, конечно же, процесс этот не вечный, и, как это происходит с почти с любым приложением, рано или поздно iOS может выгрузить из памяти приложение. С этим ничего не поделать, и придется с этим жить. Хорошая новость в том, что по возвращению в приложение в CB есть возможность восстановить стейт со всеми подключенными устройствами, найденными характеристиками и т.д. Всё вышесказанное распространяется на случаи, когда приложение было выгружено по просьбе системы, а не закрыто пользователем из таск-менеджера.

 

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

 

Важно отметить, что при реализации соединения между двумя iPhone, iPad или Mac без соответствующего бэкграунд-мода у серверной части уход в бэкграунд будет сопровождаться отключением зарегистрированных сервисов, а не завершением Bluetooth-соединения. То есть удаленная сторона (клиент) не получит события отключения, но получит уведомление didModifyServices о том, что часть сервисов у Peripheral-устройства пропало. Помните, в этом случае вы подключены к одному физическому устройству — другому телефону, планшету или компьютеру, а не к программе, как в случае HTTP-взаимодействия.

iBeacon

iBeacon — интересная технология, придуманная Apple для определения локации пользователя внутри помещений, где GPS или ГЛОНАСС недоступен.

 

Технология до жути проста. Совсем недавно я рассказывал об Advertisement-пакете, который отправляется устройством, когда оно хочет, чтобы его нашли. Обычно в нем содержится информация об имени устройства и главных сервисах. Apple предположили, что вместо этого можно передать идентификатор (а точнее три) без подключения к самому устройству. Само подключение в принципе невозможно: в рекламном пакете есть информация о том, что это устройство не поддерживает соединение. Помимо упомянутых идентификаторов, устройство отправляет калибровочные данные: уровень сигнала в 1 метре от себя, таким образом iOS может делать вывод об удаленности от устройства. В конечном итоге полезная нагрузка пакета будет состоять из

  • UUID 16 байт,
  • Major 2 байт,
  • Minor 2 байт,
  • Tx 2 байт.

 

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

 

Как это ни странно, для работы с iBeacon придется использовать CoreLocation, а не CoreBluetooth. Это и логично с точки зрения выдачи пользователем прав на определение местоположения. Тут можно захотеть схитрить и не просить права на локацию, а просто сканировать эфир с помощью CB. Но схитрить не получится: информация об идентификаторах не будет доступна. Даже если бы этот хак работал, я думаю, всё равно легальную полезную работу разработчики бы осуществляли при помощи CoreLocation.

 

Вкратце как это работает: вы определяете так называемый BeaconRegion, который можно создать с различным уровнем точности: указать один UUID, UUID + Major или же всю тройку идентификаторов. Далее попросить LocationManager отслеживать, когда устройство войдет в это регион. Далее CL в тесной коллаборации с CB начнет свою работу, сканируя эфир и пытаясь найти нужный маяк. Конечно же, система постарается это делать максимально энергоэффективно и не будет убивать батарею, чтобы поскорее найти маячок. В зависимости от текущего состояния приложения, состояния аккумулятора и статуса блокировки экрана система будет дискретно сканировать эфир и, если вам повезёт, уведомит вас о вхождении в регион или выходе из него (дернет метод). Таким образом можно, к примеру, настроить систему нотификаций на такое событие. Приятным бонусом является то, что, имея соответствующие права на локацию и бэкграунд-режим, CL разбудит приложение даже из глубокого сна, даже если пользователь явно закрыл ваше приложение свайпом из таск-менеджера.

 

После того, как вы получили уведомление о вхождении в регион, можно начать отслеживать маячки более точно, получая информацию об их удалении от устройства. Такой процесс называется ranging и может подсказать вам, насколько далеко вы находитесь от маяка, используя абстрактные Immediate, Near и Far. Более точное расстояние вам не удастся стабильно измерять, ведь расстояние измеряется на основе уровня сигнала до устройства и калибровочного значения из рекламного пакета, а на уровень сигнала могут влиять десятки факторов, от скопления людей поблизости до уровня влажности в конкретный день.

 

Наверное, тема iBeacon заслуживает отдельного доклада, потому что тут есть десятки подводных камней, которые расставила Apple, защищая АКБ пользователя.

L2CAP-соединение

В 2017 году Apple представила разработчикам возможность не только коммуницировать с Apple-устройствами через GATT-протокол, но и открывать своё низкоуровневое соединение через нижележащий протокол L2CAP. Это позволяет написать свою реализацию взаимодействия по радиоканалу, минуя ограничения характеристик GATT. Это, конечно, сложнее в реализации, чем читать и записывать характеристики, но всё равно достаточно просто.

Noveo Working with Bluetooth L2CAP

Со стороны сервера мы всё так же должны просканировать эфир, найти и подключиться к устройству. Если устройство поддерживает такой тип соединения, оно будет содержать специальный сервис с характеристикой, содержащей ID канала. Для начала обмена информацией нужно считать этот ID и попросить CBPeripheral открыть канал методом openL2CAPChannel().

 

Если вы реализуете серверную часть, то вам нужно заявить о том, что вы поддерживаете L2CAP. Для этого в соответствующем менеджере есть метод publishL2CAPChannelWithEncryption(), который подготовит канал, сервис и характеристику и автоматически опубликует нужные данные.

 

После успешного соединения делегаты соответствующих сторон процесса получат уведомление didOpenL2CAPChannel с указанием канала. Сам канал имеет два свойства — inputStream и outputStream, которые позволят реализовать и чтение, и запись данных по каналу, используя стандартную для iOS / Mac абстракцию потоков.

Грабли

Мы уже очень близко к завершению моего повествования. В конце я бы хотел кратко осветить те грабли, на которые пришлось наступить при работе с CL.

Кеширование

Разрабатывая и отлаживая приложение с BLE, всегда помните о кешировании, над которым вы не властны. CoreBluetooth попытается закешировать и сервисы, и характеристики для вашего устройства. Помимо этого, кешируется мета-информация, например имя устройства.

 

На одном из проектов потребовалось написать симулятор железного устройства, чтобы QA могли тестировать взаимодействие на всякие краевые кейсы, которые с реальным девайсом не воспроизвести или же воспроизведение было крайне долгим. При написании серверной части мы можем изменить имя нашего устройства в рекламном пакете и надеяться, что мы увидим измененное имя. Если бы мы были единственными пользователями Bluetooth на устройстве, всё бы было замечательно: указали имя в рекламном пакете, и именно его увидит клиент. Но Bluetooth на устройстве использует и ОС, часто отправляя рекламные пакеты с именем устройства из системных настроек. Например, для быстрого обнаружения устройства сервисом AirDrop. Если устройство, где вы планируете тестировать вашу клиентскую часть, уже поймало хоть один рекламный пакет, то имя закешируется, и при попытке чтения имени CBPeripheral мы увидим именно это закешированное имя, а не то, которое указали в настройках рекламного пакета.

 

 

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

Версионирование

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

Хардварные проблемы

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

 

Так, в самом начале моего пути на проекте с BLE я столкнулся с проблемой. Написал и отладил клиентскую сторону по документации, проверил с написанным симулятором, все отлично работает. Когда через месяц пришел реальный девайс — его просто нельзя было найти. Я вижу, что он есть в эфире, а уведомления в делегат не вижу. Несколько дней переписок, и по счастливой случайности я методом перебора понимаю, что если отключить фильтрацию по UUID сервисов, всё работает. Пришлось фильтровать по имени, а имя иногда менялось, и тут вспоминаем проблему с кешированием. Короче, весело.

 

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

 

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

 

Ковид подкинул новые проблемы, которые необходимо решать. Дело в том, что BLE устройства, особенно малых габаритов, особенно если у нах на борту есть шифрование, может быть тяжело притащить в Россию до того, как оно прошло всякие сертификации. Но тестировать нужно, а как я уже сказал, никакой симулятор не заменит реальное устройство. Если раньше можно было просто отправиться в очередную командировку, то сейчас это уже крайне сложно. На одном из проектов оказался спасением новый мак на м1, который позволял запускать приложение и работать по VNC с реальным устройством. Но вот на другом проекте такой трюк не сработал (не удалось выяснить, почему), и пришлось поднимать миниферму на джейлбрейкнутых девайсах.

 

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

НазадПредыдущий пост ВпередСледующий пост

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: