См. также:
Secure Scuttlebutt p2p социальная сеть, работающая и в офлайне (есть клиент Manyverse для Android и iOS)
https://yandex.ru/support/station-mini/speaker.html
Использовать Станцию Мини как музыкальную колонку
На Станцию Мини можно транслировать музыку с компьютера, планшета или смартфона через Bluetooth как на обычную беспроводную колонку:
Скажите: Алиса, включи Bluetooth или нажмите кнопку отключения микрофонов и удерживайте ее пять секунд, пока подсветка Станции Мини не замигает.
Включите Bluetooth на компьютере, планшете или смартфоне и запустите поиск устройств Bluetooth.
В списке выберите Станцию Мини и включите музыку.
Пока Станция Мини играет музыку через Bluetooth, Алиса вас не слышит. Чтобы выйти из режима трансляции, разорвите соединение на стороне вашего компьютера, смартфона или планшета.
В далеком-недалеком 2019, в очередной раз повредив провод от своих проводных наушников, чинить/менять его в 100 и 1 раз стало уже лень. И как обычно в таких случаях, прошелся по сайтам магазинов в поисках новых.
Тем бы история и закончилась, но так случилось, что один мой хороший знакомый предложил в тот момент перебраться в мир беспроводного аудио. Да и не просто так перебраться, а в стиле DIY, что показалось все-таки интересным. Ну а время спустя, одним долгим зимним вечером появилась идея оформить итоги этого всего в более-менее подробный пост.
Стоит сказать, что к беспроводным Bluetooth-наушникам я давно присматривался, но все варианты с покупкой готовых отбрасывались из-за "фатального недостатка" присущего большинству доступных моделей чудовищно малое время беспрерывной работы без подзарядки.
Емкость аккумулятора применяемого в беспроводных моделях, в лучшем случае позволяет им до 1-2 суток проработать, прежде чем все это беспроводное волшебство закончится и попросит еды. А поскольку у меня есть привычка иногда просто-таки круглосуточно не снимать наушники, такая продолжительность их работы совсем не устраивает. Можно бы конечно попытаться к какой-то готовой модели получше аккумуляторы поставить, допускаю даже что есть для этого возможности, но это наверно было бы не так интересно.
Но обо всем по порядку. Как уже понятно из названия (для тех кто с данной темой сталкивался), история будет об аудиомодуле Bluetooth модели CSR8645, производства Qualcomm.
Данный чип часто используется в беспроводной передаче звука. Он не относится к "премиум" классу моделей, но и не является бюджетным. Иначе говоря, есть чипы и лучше, есть и хуже (до неприличия хуже). Автором именно CSR8645 расценен как оптимальный для своих опытов.
В целом, за весь мой многолетний опыт с наушниками, уже был набор кое-каких представлений о том, чего я от них ожидаю.
И вот когда окончательно решился на эксперимент с беспроводными (что произошло не сразу, так как некоторое время ушло на изучение вопроса и обдумывание тех или других аспектов и проблем), сформировался более-менее определенный список требований к результату:
качество звука не хуже чем у проводных
отсутствие ощутимой задержки звука/лагов (может быть важно если захочется игры поиграть)
большое время работы без подзарядки
приемлемый вес
наушники не должны являться "вкладышами", или подобными
наушники не должны вызывать дискомфорта или других проблем при работе в них в течении 1-2 суток непрерывно
должна быть совместимость с Windows
Требованиям к качеству звука, отвечает модуль с поддержкой AptX (и большим набором применяемых в нем кодеков, где-то даже была отдельная статья о них). AptX LL был бы вероятно еще лучше, так как задержка звука в нем еще ниже, но почитав о том что далеко не все устройства его поддерживают, решено оставить это на будущее.
Для полноценной работы с Windows, понадобился сторонний Bluetooth-Stack (выбор автора после тестирования нескольких из них BlueSoleil, хоть и у него хватает недостатков). В Linux впринципе, дела с программной поддержкой должны обстоять получше насколько знаю, но лично опыты не проводились.
Еще немного забегая наперед, скажу также что этот список далеко не окончательный. В процессе эксплуатации наушников, много дополнительных пожеланий и хотелок было реализовано (кнопки управления, повышение удобства, улучшения внешнего вида и др.)
CSR8645. В продаже он нашелся в двух вариациях: "чистый" модуль, и уже распаянный на плате с 5-ваттным усилителем.
Стоимость у этих двух вариантов практически одинаковая. А учитывая маленькие размеры модуля, мне не очень-то хотелось медитировать, припаивая контакты к микроскопическим выводам. Как и не хотелось создавать свою плату, или монтировать навесным способом. К тому же все дополнительные элементы (светодиоды, резисторы) на варианте с платой уже присутствуют. В общем тот случай когда наверно не стоит свой велосипед создавать, разве что очень захочется.
Потому быстро и решительно был выбран вариант с уже готовой платой, с расчетом на то чтобы потом ее переделать.
Данный модуль работает только в качестве приемника (существуют и модели-передатчики).
CSR8545Пример CSR8645 с усилителемКопировать все характеристики смысла нет, упомяну разве что физические размеры модуля, так как в первую очередь по ним становится ясно получится его разместить в наушники, или нет.
Так, сам по себе модуль имеет размеры 26.2x13.5 мм. Тогда как размеры вариантов с усилителем от 29x29 мм. Существует несколько вариантов готовых плат с усилителем.
В качестве источника пригоден любой Bluetooth-адаптер, поддерживающий Bluetooth 2.0 и выше.
Автор использовал первый-попавшийся дешевый внешний адаптер к ПК, на основе чипа CSR8510 A10. Выбор внешнего адаптера имеет кое-какие преимущества, к примеру можно его немного переделать (об этом тоже позднее).
Так же проверялась работа с телефоном. Тут нет никаких тонкостей, беспроводные гарнитуры к телефонам давно уже обычное дело и процесс подключения аудиомодулей не отличается от их применения.
Решил переделывать свои старые проводные. О них говорить особо нечего, недорогие и ничем не примечательные особо, мониторные наушники Ergo VD-350. Ценны для автора разве что тем, что были однажды слегка переделаны для улучшения и упрочнения конструкции, и благодаря этому, как только пожелают то могут спокойно упасть на бетон без последствий. Как еще оказалось потом, их конструкция крайне удобна для монтирования данного модуля под боковую черную крышку.
В принципе, подходят какие-угодно с импедансом до 32 Ом, и дешевые и дорогущие (если только не жалко будет к дорогущим лезть с паяльником). Главное, чтобы была возможность в том или другом месте разместить аккумулятор и сам модуль с проводами.
И разумеется, можно использовать не только наушники, но и подпаять модуль к примеру к любой готовой аудиосистеме и использовать ее как беспроводную колонку. Но эти варианты здесь лишь в качестве упоминания, т.к. автору интересны исключительно наушники.
Выбор аккумулятора определяется размерами, весом, требуемым напряжением питания модуля (3.3-4.2V) и пожеланиями к продолжительности автономной работы.
Если использовать уже распаянный модуль (как упоминалось выше), то возможно питание и от больших напряжений (до 12V, зависимо от вариации платы), поскольку на таких платах обычно находится микросхема-стабилизатор, выдающая на выходе те же 3.6 или 3.3V.
Для использования отлично подходят литий-полимерные аккумуляторы (Li-Po) с напряжением 3.7V.
Но автора и они не устроили. Не хотелось устанавливать аккумулятор внутри устройства, а для наружной установки форма Li-Po батарей показалась хуже. Есть смысл ставить аккумулятор внутрь устройства, если планируется делать встроенную подзарядку, то есть без использования внешних зарядных устройств.
В итоге понравилась идея "добавить себе в наушники немного Теслы". И выбор пал на аккумуляторы типа 18650, которые используются в электромобилях, в батареях ноутбуков, фонарях да и много где еще. Являются литий-ионными по технологии, а так же обладают большой емкостью (кроме дешевых моделей сомнительных производителей).
Их недостаток это снова же размер и форма, заставившие хорошо подумать куда же их девать чтобы не мешали. Но в любом случае, 18650 были приняты как окончательный вариант, а их внешний вид был пафосно оценен как "более киберпанковский".
На деле нет принципиальной разницы какой тип аккумуляторов использовать. И те и другие в данном случае (из-за малой нагрузки от модуля по току) достаточно безопасны чтобы носить их при себе, и способны выдать нужные характеристики, так что выбор отчасти субъективный.
Тонкие провода (автор использовал выдранные с какой-то электроники, и парочку более мощных, выдранных со старого компьютерного блока питания).
Микропереключатель типа On/Off (для включения/выключения собственно модуля)
Тактовые кнопки SMD (со старого mp3-плеера удалось достать хорошие экземпляры)
Шлейф для кнопок (отрезан кусочек старого IDE-шлейфа. можно и проводами обойтись)
Отсек для 18650 аккумуляторов (одиночный)
Зарядное устройство для 18650 аккумуляторов (подойдет и простейшая схемка контроля заряда, и что-то навороченное)
И вот когда все элементы были заказаны/выдраны откуда можно, пришло время самого интересного.
Схему подключения чистого модуля приводить не стану, так как не использовалась в этой всей истории по вышеописанным причинам. Поэтому сразу перейдем к варианту с "полуфабрикатом".
Само подключение получилось примерно по принципу "ломать не строить".
Вид платы спереди мало интересен, так как там ломать особо нечего. Примененная мной вариация платы, имеет следующий вид с обратной стороны:
С интересующих нас элементов, здесь присутствуют 2 микросхемы аудиоусилителя (подсвечены красным), обвязка этих микросхем, состоящая из SMD-резисторов (желтая подсветка) и конденсаторов (синяя). А так же микросхема-стабилизатор входного напряжения (белая).
Не стоит пытаться подключать подобные платы с усилителями напрямую к наушникам, 5-ваттные усилители вполне могут их поджарить. Сначала нужно заняться другой деструктивной деятельностью.
Суть переделки состоит в том, чтобы исключить микросхемы усилителей, тем самым подключив выводы LP, LN, RP, RN напрямую к соответствующим выводам CSR8645.
Для этого, нужно выпаять все указанные элементы (кроме белого стабилизатора питания, о нем ниже), и установить вместо них проволочные перемычки, как показано на этой схеме:
При этом желательно ничего лишнего не замкнуть на плате (это возможно в местах где находились выпаянные элементы, если перемычки не изолированные).
Все, после таких нехитрых манипуляций, к плате можно безопасно подключать наушники.
По желанию, можно так же избавиться от микросхемы-стабилизатора питания, помеченной белым. Потребление энергии данной микросхемой очень несущественное, но в целях оптимизации, можно и его исключить. Как показывает опыт, модуль отлично питается напрямую напряжением до 4.2V.
Дальнейшие шаги выглядели вот так (схема в которой отсутствует микрофон и используются 3 кнопки управления из 5 возможных):
Итоговая псевдо-схемаК сожалению фото изнутри приложить нет возможности, пришлось бы все разбирать ради этого.
Выводы LP, LN контакты для левого динамика. RP, RN для правого соответственно.
Активация кнопок происходит замыканием контакта COM на нужный вывод. В данном случае на плате уже установлены резисторы, поэтому никаких дополнительных элементов для работы кнопок не требуется (в отличие от использования чистого модуля).
Для уменьшения количества проводов и упрощения, можно обойтись и вовсе без кнопок, но рекомендую по крайней мере одну подключить (вывод PLY, отвечающий за команду Play/Pause удаленного управления), так как на ней завязаны некоторые сервисные функции, вроде перезапуска сопряжения. Хотя и они не обязательны и все будет работать и так (устройство автоматически сопрягается каждый раз при включении).
В результате, получились довольно монструозные наушники, которые тем не менее, вполне радуют при домашнем использовании и покрывают все перечисленные (и некоторые не перечисленные) требования к ним.
Обозначения:
Аккумулятор
Выключатель наушников
Независимый выключатель светодиода
Дополнительный белый светодиод
Резисторы светодиода на минусе питания
Внешняя антенна
Экранированный антенный кабель
Провода питания модуля
Сигнальный светодиод модуля
Кнопка управления Previous Track
Кнопка Play/Pause
Кнопка Next Track
Собственно, дополнительный яркий белый светодиод был добавлен уже как излишество. Иногда бывает полезен, когда нужно что-то подсветить, но при этом лениво или неудобно брать полноценный фонарь.
Сам модуль размещается под черной пластиковой крышкой, на достаточном расстоянии от динамика. Плата модуля экранирована со всех сторон фольгой поверх изоляции, для подавления сигнала встроенной антенны. Внешняя антенна припаяна к дорожке встроенной антенны, экран антенного кабеля присоединен к минусу питания.
Касательно сигнального светодиода, там небольшая особенность. То что выведено наружу корпуса, это всего лишь оболочка от нерабочего красного светодиода, сквозь которую светят 2 SMD-светодиода с самой платы модуля. Так как трогать их и как-то заменять не хотелось, решил сделать такое вот окно для них.
Отсек аккумулятора крепится к рамке наушников заклепкой. Поскольку внутри этой рамки находится пластина из закаленной стали, в целом все достаточно надежно.
Еще некоторые характеристики:
Вес наушников (с аккумулятором): 220 грамм
Вес аккумулятора: 50-80 грамм
Продолжительность работы 24/24, без переходов в режим ожидания: ~4-5 суток
Продолжительность работы 16/24: до 10 суток
Данные о продолжительности работы актуальны для используемых к настоящему моменту аккумуляторов (2000 и 2200 mAh), есть возможность использовать и более емкие. Используются 2 аккумулятора по очереди, чтобы в случае сигнала о разряде, быстро сменить на уже заряженный.
Bluetooth-передача это очень хороший вариант беспроводного звука, но идеальной по всем параметрам ее назвать все равно вряд ли получится.
Поэтому рассматривались еще некоторые варианты:
На основе WIFI-сети (отброшен из-за неоправданно высокой трудоемкости, автору не известны готовые решения такого типа и предполагает что пришлось бы самому разрабатывать плату и прошивку + возможно сопутствующий софт).
На основе обычного FM передатчика и приемника (отброшен из-за отсутствия какой-либо защиты от перехвата эфира, или высокой трудоемкости в случае реализации чего-то своего шифрованного).
На основе неких чудных китайских модулей, работающих тоже в 2.4ГГц-диапазоне (и по заявлению производителя, не конфликтующих с Wifi и Bluetooth). Этот вариант не отброшен и сейчас, поскольку представляет интерес, в основном из-за большого радиуса работы данных модулей и отсутствия необходимости в программной поддержке (они по сути просто подключаются к аудиовыходу звуковой карты и работают как есть)
В абсолютном большинстве случаев стандартной дальности работы Bluetooth 2.0 (до 10 метров) более чем достаточно. В пределах 1-2 комнат может работать без помех.
Но автору очень уж хотелось больше, и вообще чтобы сигнал пробивал через несколько железобетонных стен и окон, и фантастика в этом роде.
В итоге, проводились эксперименты с добавлением внешних антенн.
Поначалу самодельных, но прирост дальности от них был порядка 10-15% всего.
Позднее нашел парочку антенн от старого радиотелефона, но и с ними ничего лучше тех же +10-15% не получилось. Разве что внешний вид получше, чем у самодельных проводков.
Как-нибудь в другой раз хочется попробовать специальные антенны под 2.4ГГц-диапазон (наподобие тех что применяются в мощных передатчиках Wifi-роутеров), но есть подозрение что и они не особо помогут.
Предполагаю, что дело больше в ограниченной мощности передатчика, а не в антеннах. Если так, то нужно увеличивать мощность передатчика и на стороне источника, и на стороне наушников. Как именно это сделать в случае с данными Bluetooth-модулями, у автора пока нет конкретных идей (и нет достаточного опыта в общем-то в этом). Естественно, увеличение мощности приведет и к увеличению потребления энергии, но это еще вполне приемлемо, если и правда дальность работы принципиально улучшится.
Очень интересно, если у кого-то есть результаты в этом направлении, или схемы/идеи применимые к BT-передатчикам.
Есть возможность с использованием фирменной китайской утилиты и
программатора более тонко настроить плату под себя.
Включить-выключить те или другие функции и поддерживаемые кодеки,
задать "нескучное" имя устройства и сервисные звуки, окирпичить
модуль, и многое другое.
Интереса ради, прежде находил как все это сделать, но так и не появилось особой необходимости (да и не было подходящего программатора, чтобы попробовать без необходимости), потому ссылки не сохранились.
На плате присутствуют выводы для присоединения микрофона.
Проверка показала что качество передаваемого с него звука пригодно для голосового общения, чего достаточно в случае подключения к телефону. Но в виде полноценного микрофона для ПК или ноутбука использовать такой смысла нет.
Если требуется объединить минуса аудиоканалов (например, чтобы сделать вывод через распространенный jack с общим минусом), нужна дополнительная развязка аудиовыхода, либо подключение к внешнему усилителю, имеющему на входе раздельные каналы, а на выходе объединенные.
Без таких дополнений модуль работать не будет (уходит в перезагрузку при прямом замыкании минусов). И более того, в инструкциях часто грозятся что из-за таких замыканий он может сгореть.
Ниже расположен пример схемы дополнительной развязки без усилителя. Выглядит адекватно, хотя сам автор не проверял ее в работе из-за ненадобности в случае наушников.
CSR8645 с усилителем на AliExpress (детали, фото, схемы)
Документация на примере похожей платы
Подробная статья касательно A2DP, кодеков и прочего
На этом вроде бы все. Надеюсь статья окажется интересной, по крайней мере непрофессиональным любителям время от времени поиграть с электроникой (к каким сам автор относится).
Создание кастомного сервиса и тем более клиента Bluetooth Low Energy прогулка по граблям с завязанными глазами. По крайне мере так было для меня 4 года назад, когда я только начинал работать с BLE-устройствами. Сейчас почти каждый мой проект предусматривает использование этого протокола, поэтому в свое время пришлось в нем долго и мучительно разбираться.
Разложить все по полкам помогла книга Мохаммада Афане "Intro to Bluetooth Low Energy" и серия постов на Novel Bits. Лично для меня эта книга стала настоящим открытием. Изначально я делал ее перевод на русский для своих коллег, не имеющим опыт работы с BLE. С согласия автора (огромное ему спасибо) решил опубликовать свою работу здесь. Надеюсь, перевод окажется полезным.
Это первая часть перевода (всего их будет 5), которая рассказывает, что такое BLE, ее возможности и отличия от Bluetooth Classic и описывает архитектуру протокола.
Мохаммад Афане занимается разработкой встроенного программного обеспечения и прошивок с 2006 года. Он работал и консультировал множество крупных компаний, включая такие как Allegion (Schlage locks), Motorola, Technicolor, Audiovox, и Denon & Marantz Group. На протяжении всей своей карьеры он работал над множеством проектов Интернета Вещей, включая: беспроводные электронные дверные замки, спутниковые приемники, беспроводные дверные замки и т.д.
В июле 2015 года он принял решение прекратить работу на полную ставку для того, чтобы основать собственную компанию Novel Bits, LLC, где он делится своими знаниями и опытом на своем web-сайте, локальных тренингах и в электронных книгах, посвященных разработке приложений с поддержкой Bluetooth Low Energy.
Вы можете связаться с Мохаммадом по его электронной почте: mohammad@novelbits.io или через профиль на LinkedIn.
Bluetooth был задуман как технология связи ближнего диапазона, призванная заменить провода в таких устройствах, как компьютерные мыши, клавиатуры или персональные компьютеры. Если у вас есть современный автомобиль или смартфон, то скорее всего вы использовали Bluetooth хотя бы раз в своей жизни. Он повсюду: в громкоговорителях и колонках, беспроводных наушниках, автомобилях, носимых устройствах и даже в шлёпанцах!
Первая официальная версия стандарта была выпущена компанией Ericsson в 1994 году. Разработчики назвали свое изобретение в честь короля Дании Харальда Гормссона по прозвищу Синезубый, объединившего в 10 веке враждовавшие датские племена в единое королевство.
В настоящее время существует два типа устройств с поддержкой Bluetooth:
Bluetooth Classic (BR/EDR), используется в беспроводных громкоговорителях, автомобильных информационно-развлекательных системах и наушниках;
Bluetooth Low Energy (BLE), т.е. Bluetooth с низким энергопотреблением, который появился в версии стандарта Bluetooth 4.0. Он чаще всего применяется в приложениях, чувствительных к энергопотреблению (например в устройствах с батарейным питанием) или в устройствах, передающих небольшие объемы данных с большими перерывами между передачами (например, разнообразные сенсоры параметров окружающей среды или управляющие устройства, такие как беспроводные выключатели).
Эти два типа устройств несовместимы друг с другом, даже если они выпущены под одним брендом или спецификацией. Устройства с поддержкой Bluetooth Classic не могут напрямую связываться с устройствами, использующими BLE. Это причина, по которой некоторые устройства, такие как смартфоны, выполняются с поддержкой обоих типов соединения (так называемые Dual mode Bluetooth devices), что позволяет им обмениваться информацией с обоими типами устройств.
Рис.1: Типы Bluetooth-устройствНесколько важных замечаний о BLE:
Официальная спецификация Bluetooth сочетает оба типа Bluetooth (Classic и BLE), что иногда затрудняет поиск документации, специфичной для BLE;
BLE был введен в версии 4.0 спецификации стандарта Bluetooth, выпущенной в 2010 году;
BLE иногда называют Bluetooth Smart, BTLE или Bluetooth 4.0, что является ошибкой, так как эта версия в действительности включает оба типа Bluetooth;
Bluetooth Classic и BLE работают в одном и том же частотном диапазоне 2.4 ГГц, ISM-диапазон.
Поскольку во многих устройствах Интернета Вещей (IoT) используются небольшие устройства и датчики, BLE стал наиболее часто используемым протоколом связи (в сравнении с Bluetooth Classic) в приложениях Интернета Вещей. В декабре 2016 года группа компаний Bluetooth Special Interest Group (SIG), регулирующая развитие стандарта, выпустила Bluetooth версии 5.0 (для простоты маркетинга была убрана точка из названия, так что официально он называется Bluetooth 5). Большинство улучшений и новых функций, представленных в этой версии, были ориентированы на BLE, а не на Bluetooth Classic.
Вы также могли слышать о другом термине, связанном с Bluetooth Bluetooth Mesh. Bluetooth Mesh был выпущен в июле 2017 года и основан на BLE. Для работы ему требуется полный стек BLE (ПО, которое действует как интерфейс для другого программного или аппаратного обеспечения), но он не является частью основной спецификации Bluetooth. Мы рассмотрим более подробно эту технологию в отдельной главе.
Подводя итог, посмотрим на диаграмму, показывающую прогресс BLE за прошедшие годы с начала его появления:
Рис.2: История BLEНекоторые из наиболее важных технических фактов о BLE включают в себя:
Используемый частотный диапазон 2.400 - 2.4835 ГГц.
Весь частотный диапазон поделен на 40 каналов по 2 МГц каждый.
Максимальная скорость передачи данных по радиоканалу (начиная с Bluetooth версии 5) 2Мбит/с.
Дальность передачи сильно зависит от физического окружения, а также используемого режима передачи. Например, в режиме большой дальности передачи дальность связи будет выше, а скорость передачи ниже, чем в высокоскоростном режиме. Типичная дальность передачи: 10-30 метров.
Потребление электроэнергии также может изменяться в широких пределах. Оно зависит от реализации устройства, различных параметров протокола и используемого чипсета. Типичное потребление BLE-трансивера во время передачи данных как правило не превышает 15 мА.
Обеспечение безопасности не обязательно при обмене данными через BLE и зависит от устройства и реализации приложения разработчиком. Другими словами, существует несколько возможных для реализации уровней обеспечения безопасности.
Для всех операций, связанных с шифрованием, BLE использует алгоритм AES-CCM с длиной ключа 128 бит.
BLE предназначен для передачи данных по каналу с низкой пропускной способностью. Использование BLE для приложений с большим объемом часто передаваемых данных существенно увеличивает потребление электроэнергии и сводит на нет основное преимущество BLE. То есть минимизация использования радиосвязи, насколько это возможно, позволяет достичь минимального уровня потребления энергии.
Версии Bluetooth (в части BLE) являются обратно совместимыми. Тем не менее возможности связи будут ограничены функциями более старой версии. Например, устройство с поддержкой Bluetooth 5 LE может установить связь с устройством с поддержкой Bluetooth 4.1 LE, но возможности, появившиеся в версии 4.2 и более новых, будут недоступны. В то же время они смогут использовать возможности подключения, рассылки и приема широковещательных пакетов, обнаруживать сервисы и характеристики, а также читать и записывать их независимо от поддерживаемой ими версии стандарта, так как эти возможности доступны во всех версиях Bluetooth.
Важно помнить, что существует большая разница между классическим Bluetooth и Bluetooth с низким энергопотреблением с точки зрения технических спецификаций, реализации и типов приложений, для которых они предназначены. Это в дополнение к тому факту, что они несовместимы друг с другом.
Некоторые из упомянутых различий представлены в этой таблице:
Таблица 1. Сравнение Bluetooth Classic и BLE
Bluetooth Classic |
BLE |
Используется для потоковых приложений, таких как трансляция аудио и передача файлов |
Используется в сенсорах, управлении устройствами и приложениях, не требующих передачи больших объемов данных |
Не оптимизирован для низкого энергопотребления, но поддерживает большую скорость передачи (максимум 3 МБит/с, в то время как BLE 5 имеет максимум 2 МБит/с) |
Предназначен для применения в малопотребляющих устройствах с большими интервалами между передачей данных |
Использует 79 радиоканалов |
Использует 40 радиоканалов |
Обнаружение происходит на 32 каналах |
Обнаружение происходит на 3 каналах, что приводит к более быстрому обнаружению и установке соединения по сравнению с Bluetooth Classic |
С момента официального выпуска в 2010 году BLE прошел череду ревизий и изменений. Наиболее важное изменение произошло в декабре 2016 года с внедрением Bluetooth 5, который привнес множество важных улучшений в спецификацию стандарта, большинство из которых касалось BLE. Эти улучшения позволили удвоить скорость передачи, в 4 раза увеличить дальность передачи и в 8 раз увеличить размер широковещательного пакета.
Каждая технология имеет свои ограничения, и BLE не является исключением. Как мы упомянули ранее, BLE наилучшим образом подходит для приложений с небольшим радиусом передачи и редко передаваемыми небольшими объемами данных.
Пропускная способность BLE ограничена физической пропускной способностью радиоканала, т.е. скоростью, с которой данные передаются по радиоканалу. Пропускная способность зависит от используемой версии Bluetooth. Для Bluetooth 4.2 и более ранних, доступна только пропускная способность в 1 Мбит/с. В Bluetooth 5 и более поздних версиях пропускная способность зависит от выбранного режима PHY (Physical Layer, рассматривается в разделе физического уровня). Она может составлять 1 Мбит/с как в более ранних версиях или 2 Мбит/с при использовании высокоскоростной передачи. При использовании функции дальней связи пропускная способность ограничена значениями 500 или 125 Мбит/с. Мы обсудим это более подробно в главе, посвященной Bluetooth 5.
Скорость передачи с точки зрения конечного пользователя всегда будет ниже скорости передачи по радиоканалу в силу следующих факторов:
Промежутки между пакетами данных: спецификация Bluetooth определяет зазор в 150 микросекунд между передаваемыми пакетами как требование для соблюдения спецификации. В этот промежуток времени невозможна передача данных между устройствами.
Служебная информация внутри пакета: каждый пакет содержит помимо полезной нагрузки заголовок и служебные данные, обрабатываемые на уровнях ниже уровня приложения. Они учитываются при передаче данных, но не используются вашим приложением.
Требование на передачу служебной информации периферийным устройством: спецификация требует обязательного ответа ведомого устройства на каждый пакет, переданный ведущим. В случае, когда необходимая для передачи информация отсутствует, передается пустой пакет.
Переотправка пакетов данных: в случае потери пакета или перекрестных помех от находящихся поблизости устройств, потерянные или поврежденные данные отправляются заново.
BLE был разработан для применения на коротких расстояниях, и, следовательно, его диапазон действия ограничен. Вот некоторые факторы, ограничивающие дальность передачи при помощи BLE:
На передачу в ISM-диапазоне 2.4 ГГц сильно влияют окружающие нас препятствия, такие как металлические предметы, бетонные стены, вода и человеческие тела.
Диаграмма направленности и коэффициент усиления антенны.
Корпус устройства, в котором находится антенна, также ухудшает характеристики антенны.
Ориентация устройства в пространстве, от которого зависит ориентация антенны, например в смартфонах.
Для передачи данных с устройства, поддерживающего только BLE-соединение, необходимо другое устройство с поддержкой как BLE, так и IP-соединения. Именно оно будет получать данные и отправлять их в интернет.
Даже с учетом представленных выше ограничений BLE имеет некоторые существенные преимущества перед другими аналогичными технологиями передачи данных для IoT.
Вот некоторые из них:
Меньшее энергопотребление;
По сравнению с другими низкопотребляющими технологиями передачи данных, BLE потребляет гораздо меньше электроэнергии. Это достигается благодаря глубокой оптимизации протокола, выключению передатчика при первой возможности и пересылке малых объемов данных на низкой скорости.
Бесплатный доступ к официальным спецификациям;
Чтобы получить доступ к спецификациям большинства других протоколов вы должны стать членом официальной группы или консорциума по этому стандарту. Стать членом можно за внушительную сумму (от 7500 до 35000 долларов в год). В случае с BLE, спецификации для основных версий (4.0, 4.1, 4.2, 5) доступны для загрузки с сайта Bluetooth абсолютно бесплатно.
Низкая цена модулей и чипсетов по сравнению с другими технологиями;
Наконец, не менее важный фактор наличие в большинстве смартфонов на рынке. Возможно, это наибольшее преимущество BLE перед такими технологиями как ZigBee, Z-Wave и Thread.
Исходя из ограничений и преимуществ, указанных выше, существуют варианты использования, где BLE раскрывается наиболее полно:
Малый объем передаваемых данных;
BLE подходит для случаев, когда устройство передает небольшие объемы данных, например, данные датчиков или команды исполнительных устройств.
Настройка устройств;
В случаях, когда BLE не удовлетворяет основным требованиям системы, он может использоваться для настройки устройства до того, как оно окажется подключенным к основной сети передачи данных.
Например, некоторые устройства с поддержкой WiFi добавляют BLE как вспомогательный протокол вместо использования таких технологий как WiFi Direct. Это технология, которая позволяет двум устройствам с поддержкой WiFi соединяться напрямую, минуя роутер. Вы можете узнать подробнее о ней на Википедии или здесь.
Использование смартфона в качестве интерфейса;
Компактные малопотребляющие устройства обычно не имеют больших экранов и зачастую отображают ограниченное количество данных конечному пользователю,например, путем светодиодной индикации. В настоящее время, благодаря широкому распространению смартфонов, BLE может предложить альтернативный, гораздо более информативный и удобный интерфейс для этих устройств. Еще одним преимуществом смартфона является возможность загрузки данных в облако.
Персональные и носимые устройства;
Для случаев, когда устройство является носимым и находится вне зоны покрытия беспроводных сетей (таких как WiFi или сотовая связь), BLE может оказаться единственным доступным способом подключения.
Устройства без возможности установления соединения.
Вероятно вы слышали или видели ранее такие устройства как маячки. У этих устройств одна простая задача выдавать через определенные промежутки времени в эфир данные так, чтобы другие устройства могли их обнаружить и принять передаваемые данные. Существуют и другие технологии, которые могут быть использованы для этих целей. Тем не менее BLE становится все более популярным, так как большинство людей имеют смартфоны, которые поддерживают BLE из коробки.
Все вышеперечисленные сценарии только выигрывают от использования BLE. С другой стороны, есть условия, при которых, как правило, использование BLE невозможно или не дает ощутимых преимуществ, такие как:
Потоковая передача видео;
Трансляция высококачественного звука (прим.: стала возможна в BLE 5.2);
Передача больших объемов данных в течении длительного времени в тех случаях, когда важно сокращение энергопотребления.
Рисунок ниже иллюстрирует различные уровни, присущие архитектуре BLE. Три главных блока в этой архитектуре приложение, хост и контроллер.
Рис.3: Архитектура BLEВ этой книге мы сфокусируемся на верхних уровнях архитектуры, кратко ознакомившись с нижними уровнями в этой главе. Подробное описание верхних уровней GAP (Generic Access Profile), GATT (Generic Attribute Profile) и Security Manager вынесем в отдельные главы.
Прикладной уровень зависит от варианта использования девайса/приложения и относится к реализации на основе общего профиля доступа (GAP) и общего профиля атрибутов (GATT) он отвечает за то, как ваше приложение обрабатывает данные, полученные от других устройств и отправленные на них, а также управляющую логику.
Эта часть является кодом, который вы написали для своего приложения и, как правило, не является частью BLE-стека для платформы, под которую вы разрабатываете. Эта часть не рассматривается в книге, поскольку она зависит от специфики вашего приложения и способа использования.
Хост включает следующие уровни:
Общий профиль доступа (GAP, Generic Access Profile);
Общий профиль атрибутов (GATT, Generic Attribute Profile);
Протокол атрибутов (ATT, Attribute Protocol);
Менеджер безопасности (SM, Security Manager);
Протокол управления и адаптации логических связей (L2CAP, Logical Link Control and Adaptation Protocol);
Интерфейс хост-контроллера (HCI, Host Controller Interface), зона ответственности хоста.
Контроллер включает следующие уровни:
Физический уровень (PHY, Physical Layer);
Слой связи (Link Layer);
Режим прямого тестирования (DTM, Direct Test Mode);
Интерфейс хост-контроллера (HCI, Host Controller Interface), зона ответственности контроллера.
PHY относится к части оборудования, ответственного за прием, передачу, модуляцию и демодуляцию сигнала. BLE работает в ISM-диапазоне (2.4 ГГЦ), который разделен на 40 каналов по 2 Мгц, как показано на рисунке ниже:
Рис.4: Частотный спектр и радиоканалы в BLEТри выделенных канала носят название Первичных Широковещательных Каналов, в то время, как оставшиеся 37 используются в роли Вторичных Широковещательных и для передачи данных во время соединения. Мы подробно рассмотрим принципы их использования в разделе Адвертайзинг и сканирование, но для начала кратко ознакомимся с ними в этой главе.
Адвертайзинг заключается в рассылке широковещательных пакетов по трем Первичным Каналам Адвертайзинга (или части из них). Это дает возможность обнаружить широковещающее устройство и прочитать его данные сканирующим устройствам. После этого сканирующее устройство может инициировать соединение, если широковещающее разрешает подключение. Также сканирующее устройство может послать запрос на сканирование, и, если широковещающее устройство поддерживает эту функцию, то оно пошлет ответ на сканирование. Запросы на сканирование и ответы на него позволяют передавать дополнительные данные без подключения к устройству.
Вот некоторые другие важные технические детали, касающиеся физического уровня передачи BLE:
Он использует скачкообразную перестройку несущей частоты (FHSS, Frequency Hopping Spread Spectrum), что позволяет двум взаимодействующим устройствам переключаться на случайные предварительно согласованные частоты для обмена данными. Это значительно повышает надежность и позволяет устройствам избегать перегруженных каналов.
Мощность передачи может быть:
Не более: 100 мВт (+20 дБм) для версии 5 и более новых, 10 мВт (+10 дБм) для версии 4.2 и более старых;
Не менее: 0.01 мВт (-20 дБм).
В старых версиях Bluetooth (4.0, 4.1 и 4.2) была доступна только одна скорость передачи 1 Мбит/с. Физический уровень радио (PHY) в этом случае называется 1M PHY и является обязательным во всех версиях, включая Bluetooth 5. В Bluetooth 5 были также введены два новых дополнительных PHY:
2 Мбит/с PHY, используемый для удвоения скорости передачи по сравнению с более ранними версиями Bluetooth.
Зашифрованный PHY, используемый для связи на дальних расстояниях.
Мы рассмотрим эти два новых PHY и концепцию кодирования в главе, посвященной Bluetooth 5.
Канальный уровень отвечает за взаимодействие с физическим уровнем радио и предоставление другим уровням абстракции для взаимодействия с радио (через промежуточный уровень интерфейса хост-контроллера, который мы вскоре обсудим). Он отвечает за управление состоянием радио и соблюдение требований к временным задержкам, необходимых для удовлетворения спецификации BLE. Также он отвечает за управление аппаратно-ускоренными операциями, такими как вычисление контрольных сумм, генерацию случайных чисел и шифрование.
Существует три основных состояния, в которых может находиться устройство с BLE:
Широковещательное состояние (Advertising);
Состояние сканирования (Scanning);
Подключенное состояние.
Когда устройство посылает широковещательные пакеты, оно позволяет сканирующим устройствам обнаружить себя и подключиться. Если широковещающее устройство допускает подключения, сканирующее устройство нашло его и послало запрос на подключение, оба они переходят в подключенное состояние.
Канальный уровень управляет различными состояниями радио, показанными на рисунке:
Рис.5: Состояния канального уровняStandby: состояние по умолчанию, когда радио не передает и не принимает никаких данных.
Advertising: состояние, в котором устройство посылает широковещательные пакеты для обнаружения и чтения другими устройствами.
Scanning: состояние, в котором устройство ищет устройства, посылающие широковещательные пакеты.
Initiating: состояние, в котором начинается процесс установки соединения с устройством, находящимся в состоянии advertising.
Connected: Состояние, в котором одно устройство установило соединение с другим и регулярно обменивается с ним информацией. В подключенном состоянии устройство, которое находилось в состоянии scanning и инициировало соединение, называется ведущим. Устройство, которое рассылало широковещательные пакеты, называется ведомым.
Мы рассмотрим эти состояния более подробно в последующих главах.
Bluetooth адрес:
Bluetooth-устройства идентифицируются посредством 48-битного адреса, похожего на MAC-адрес. Существуют два основных типа адресов: публичный и случайный.
Публичный адрес:
Это фиксированный адрес, запрограммированный на фабрике. Он не может быть изменен и должен быть зарегистрирован в IEEE (также, как и MAC-адреса устройств с поддержкой WiFi или Ethernet).
Случайный адрес:
Так как у производителей есть возможность выбирать, какой тип адреса использовать (публичный или случайный), случайные адреса встречаются более часто, так как они не требуют регистрации в Институте инженеров электротехники и электроники. Случайный адрес программируется на устройстве или генерируется в ходе выполнения программы. Он может относиться к одному из следующих подтипов:
Статический адрес
Используется в качестве замены публичного адреса;
Может быть заново сгенерирован при загрузке кода или оставаться постоянным в течение всего срока службы;
Не может изменяться при включении или выключении.
Частный адрес включает в себя следующие подтипы:
Неразрешимый частный адрес:
Случайный, генерируется на определенный промежуток времени;
Широко не используется.
Разрешимый частный адрес:
Используется для обеспечения безопасности;
Генерируется с использованием ключа (IRK, Identity Resolving Key) и случайного числа;
Периодически меняется (даже во время соединения);
Используется для защиты от отслеживания злоумышленниками;
Доверенные устройства (связанные, описанные в главе, посвященной безопасности) могут расшифровать адрес, используя предварительно сохраненный ключ.
Режим прямого тестирования (DTM, Direct Test Mode) используется исключительно для проведения испытаний радиочасти во время производства или сертификационных испытаний. Он не относится напрямую к теме нашей книги, поэтому мы оставим его без подробного рассмотрения.
Интерфейс хост-контроллера это стандартный протокол, определенный спецификацией Bluetooth, который позволяет уровню хоста коммуницировать с уровнем контроллера. Эти уровни могут быть реализованы на двух раздельных микросхемах или существовать на одной. В этом смысле он также обеспечивает взаимодействие между микросхемами, поэтому разработчик устройства может выбрать два сертифицированных Bluetooth-устройства, контроллер и хост, и быть на 100% уверенным в том, что они совместимы друг с другом в плане связи между уровнями хоста и контроллера.
В случае, когда хост и контроллер находятся на разных микросхемах, связь между ними может быть реализована посредством трех официально поддерживаемых физических интерфейсов: UART, USB или SDIO (Secure Digital Input Output). В случае, когда хост и контроллер находятся на одной и той же микросхеме, интерфейс хост-контроллера будет логическим интерфейсом.
Задача интерфейса хост-контроллера состоит в передаче команд от хоста контроллеру и передаче информации и событий от контроллера к хосту. На рисунке ниже приведен пример обмена командами и событиями между уровнями хоста и контроллера.
Рис.6: Пример пакетов интерфейса хост-контроллераПримеры сообщений включают в себя: пакеты команд, настройку контроллера, запрос действий, управление параметрами соединения, пакеты событий, завершение команд и события состояния.
Протокол L2CAP предоставляет услуги по работе с данными, как ориентированные на соединения, так и без ориентации на них, протоколам более высокого уровня с возможностями мультиплексирования и обеспечения операций по сегментации и обратной сборке. Он заимствован из стандарта Bluetooth Classic и в случае BLE выполняет следующие задачи:
Принимает несколько протоколов с верхних уровней и помещает их в стандартные пакеты BLE, которые передаются на нижние уровни под ним.
Управляет фрагментацией и рекомбинацией пакетов. Он берет большие пакеты с верхних уровней и разбивает их на порции, которые соответствуют максимальному размеру полезной нагрузки BLE, поддерживаемому для передачи. На стороне получателя он принимает несколько пакетов и объединяет их в один пакет, который может быть обработан верхними уровнями.
В случае BLE уровень L2CAP управляет двумя основными протоколами: протоколом атрибутов (ATT, рассмотрен в главе, посвященной GATT) и протоколу управления безопасностью (SMP, рассмотрен в главе, посвященной безопасности).
Протокол атрибутов (АТТ), общий профиль атрибутов (GATT), менеджер безопасности (SM) и общий профиль доступа (GAP) будут подробно рассмотрены в следующих главах.
На этом заканчивается первая глава книги. Большое спасибо дочитавшим. Следующая часть будет посвящена классам устройств и адвертайзингу методу, с помощью которого устройства сообщают о своем присутствии окружающему миру.
Это вторая часть перевода книги Мохаммада Афане Intro to Bluetooth Low Energy. В представленных главах мы поговорим о типах устройств и об адвертайзинге, методе, с помощью которого периферийные устройства сообщают о своем присутствии.Первая часть здесь.
Хочу сразу отметить, что адвертайзинг может использоваться не только для обнаружения устройств, но и для отправки кастомных данных. Например, в портативном мониторе качества воздуха Atmotube, пакеты адвертайзинга и ответа на сканирование используются для передачи сведений о текущих показаниях сенсоров. Это удобно для контроля показаний сенсоров на этапе производства и при сборе данных несколькими устройствами.
Существуют несколько важных определений, с которыми вы будете постоянно сталкиваться при изучении BLE. Два наиболее важных касаются ролей устройства: BLE central и BLE peripheral.
Рассмотрим их более детально.
Периферийное устройство устройство, которое объявляет о своем присутствии путем адвертайзинга, т.е.рассылки широковещательных пакетов, и принимает запросы на соединение от центральных устройств.
Другой связанный термин BLE-передатчик, устройство, которое также рассылает широковещательные пакеты, но имеет одно отличие от периферийного устройства:оно не разрешает другим устройствам устанавливать с ним соединение. С другой стороны, устройство-наблюдатель только обнаруживает устройства, производящие процедуру адвертайзинга, но не имеет возможности инициировать соединение с ними.
Типичный пример устройства, реализующего роль передатчика маячок (beacon). Маячки это устройства, которые передают информацию без возможности установить с ними соединение. Они очень популярны в двух сферах: розничная торговля и определение местоположения внутри помещений.
Например, некоторая сеть магазинов использует мобильное приложение, которое может обнаруживать маячки на территории магазина. Если покупатель, имеющий на своем смартфоне это же приложение, приблизится к маячку, то в приложении отобразится специальная скидка на группу товаров, связанную с маячком.
Отличить маячок от периферийного устройства можно по типу широковещательных пакетов, которые он передает. Существуют различные типы пакетов: некоторые указывают на возможность установить соединение, а другие просто указывают на наличие маячка в этом месте. Когда центральное BLE-устройство обнаруживает широковещательные пакеты другого устройства с BLE (будь оно маячком или периферийным), оно знает, может ли оно начинать процедуру установки соединения или нет в зависимости от типа принятых широковещательных пакетов.
Как только периферийное устройство подключается к центральному, оно принимает на себя роль ведомого. Центральное устройство в таком случае называется ведущим. Это роли, определенные на канальном уровне, тогда как роли периферийного и центрального устройства определены на уровне GAP.
Центральное устройство устройство, которое обнаруживает периферийные устройства и считывает передаваемую ими информацию. Оно также может устанавливать соединение с одним или несколькими устройствами одновременно.
Наблюдатель устройство, схожее по функционалу с центральным, но не имеющее возможности устанавливать соединение с другим устройством.
Рассмотрим возможности и ограничения четырех типов устройств: передатчик, наблюдатель, периферийное и центральное устройство.
Передатчик |
Периферийное устройство |
Наблюдатель |
Центральное устройство |
Не требует наличия приемника |
Требует наличия как приемника, так и передатчика |
Не требует наличия передатчика |
Требует наличия как приемника, так и передатчика |
Не поддерживает двунаправленный обмен данными |
Поддерживает двунаправленный обмен данными |
Не поддерживает двунаправленный обмен данными |
Поддерживает двунаправленный обмен данными |
Упрощенная схема, уменьшенный размер программного стека BLE |
Требует полного программного стека BLE |
Упрощенная схема, уменьшенный размер программного стека BLE |
Требует полного программного стека BLE |
Табл. 1: Сравнение типов устройств
Протокол BLE асимметричен. Большая часть тяжелой работы, связанной с управлением соединениями, управлением временем и обработкой информации, лежит на центральном устройстве. Это помогает снизить энергопотребление и требования к вычислительной мощности периферийного устройства, что позволяет интегрировать BLE в компактные устройства с ограниченными ресурсами, например, устройства с батарейным или аккумуляторным питанием.
Центральное устройство BLE также может иметь батарейное питание, но обычно имеет перезаряжаемый аккумулятор большой емкости. Как правило, роль центрального устройства на себя берет смартфон, компьютер или планшет.
Центральное устройство может быть подключено к нескольким периферийным одновременно. Характерный пример смартфон, подключенный к умным часам, термостату умного дома и фитнес-трекеру одновременно.
В некоторых случаях поддержка BLE-устройством центральной и периферийной роли одновременно приносит заметную пользу. Например, устройство может контролировать несколько датчиков (периферийных устройств) и в то же время иметь возможность передавать данные с этих датчиков на смартфон, обеспечивая доступ к ним из интерфейса мобильного приложения.
Рис. 1: Смартфон в качестве многоролевого устройстваОдним из наиболее значимых преимуществ BLE перед другими похожими малопотребляющими технологиями, такими как ZigBee, Z-Wave, Thread и др.,) является его наличие в большинстве смартфонов, представленных на рынке. Практически все смартфоны уже имели на борту Bluetooth Classic с самых ранних дней, и большинство производителей чипсетов Bluetooth теперь внедряют в свои чипы поддержку и BLE, и Bluetooth Classic. В результате в настоящее время подавляющее большинство смартфонов поддерживает BLE.
Для смартфона возможность взаимодействовать с устройствами BLE дает пару существенных преимуществ:
Смартфоны предоставляют пользователям привычный интерфейс. Использование мобильного приложения для взаимодействия с BLE-устройством зачастую оказывается удобнее непосредственного взаимодействия с этим устройством.
Смартфоны, как правило, постоянно подключены к Интернету. Это означает, что данные, полученные с BLE-устройства, могут быть переданы в облако и сохранены для последующего анализа и обработки.
В настоящий момент существуют две основные мобильные операционные системы: Android и iOS. Android представил встроенную поддержку BLE API в версии Android 4.3 (выпущена в июле 2012 года), в то время как iOS сделал то же самое немного раньше в октябре 2011 года.
Важно отметить, что многое зависит от возможностей аппаратного обеспечения операционной системы. В случае с iOS, поддержку BLE имеют все устройства, начиная с iPhone 4s. Ситуация с Android гораздо сложнее. Эта операционная система работает на устройствах разных производителей с разной аппаратной конфигурацией, поэтому нет простого способа определить, какие устройства первыми начали поддерживать BLE. Эта проблема фрагментации Android представляет большую проблему при разработке приложений, использующих BLE, которые должны работать одинаково на всех существующих Android-устройствах.
Общий профиль доступа предоставляет фреймворк, который определяет способы взаимодействия BLE-устройств друг с другом. Он включает в себя следующие аспекты:
Режимы и роли устройств;
Обнаружение устройств: рассылка пакетов адвертайзинга, сканирование, параметры рассылки и сканирования, содержимое пакетов;
Установка соединения: инициация, подтверждение, параметры соединения;
Обеспечение безопасности
Реализация этого фреймворка является обязательной согласно официальной спецификации, и это то, что позволяет устройствам BLE обмениваться данными и взаимодействовать друг с другом.
Мы кратко осветили состояния сканирования и рассылки пакетов адвертайзинга BLE-устройств и упомянули, что периферийное устройство всегда запускается в состоянии рассылки пакетов адвертайзинга, даже если оно предназначено для работы в подключенном состоянии большую часть времени. Чтобы два устройства могли обнаружить друг друга, одно из них должно рассылать пакеты адвертайзинга, а другое сканировать первичные широковещательные каналы (радиоканалы 37, 38, 39) в поисках пакетов адвертайзинга, отправленных периферийным устройством.
Если периферийное устройство поддерживает возможность подключения и центральное устройство обнаружило его, они могут установить соединение. В этой главе мы сфокусируемся на начальных состояниях периферийного и центрального устройства: адвертайзинг и сканирование.
В состоянии адвертайзинга устройство рассылает пакеты, содержащие полезную информацию для других устройств, чтобы они приняли и обработали её. Пакеты посылаются через определенные интервалы времени, которые называются интервалы адвертайзинга.
В BLE существуют 40 радиоканалов, разнесенных на 2 МГц (от центра до центра), как показано на рисунке ниже. Три канала называются каналами первичного адвертайзинга, в то время как оставшиеся 37 каналов используются для вторичного адвертайзинга, а также для передачи пакетов данных во время соединения.
Рис. 8: Радиоканалы в BLEЗамечание: Так как устройство посылает пакеты адвертайзинга на одном из этих каналов и, как правило, постоянно переключается между ними, они (каналы) разнесены далеко по спектру друг от друга для того, чтобы избежать перекрестных помех между устройствами, вещающими на разных каналах. Также, расположение этих каналов на спектре выбрано таким, чтобы избежать помех от наиболее часто используемых Wi-Fi каналов.
Процесс адвертайзинга всегда начинается с посылки широковещательного пакета по трем первичным каналам адвертайзинга или части из них. Это позволяет центральным устройствам найти периферийные и прочитать и пакеты адвертайзинга. Затем центральное устройство может запустить процесс подключения, если периферийное устройство поддерживает такую возможность.
Также центральное устройство может послать запрос на сканирование и, если периферийное поддерживает такую возможность, оно пошлет пакет, содержащий ответ на сканирование. Запросы на сканирование и ответы позволяют периферийному устройству отправить дополнительные данные, которые не поместились в основной пакет адвертайзинга без установки соединения.
Примечание: длина первичного пакета адвертайзинга ограничена 31 байтами. Длина вторичного пакета адвертайзинга может составлять до 254 байт.
Как мы упоминали ранее, некоторые устройства (маячки) всегда остаются в состоянии адвертайзинга и не принимают запросы на подключение, в то время как другие (периферийные устройства) позволяют переход в подключенное состояние, если центральное устройство инициирует соединение.
Главное преимущество нахождения в состоянии адвертайзинга состоит в том, что множество центральных устройств могут получать данные с одного периферийного без необходимости в подключении. В то же время есть существенные недостатки, такие как отсутствие защиты данных и невозможность для периферийного устройства получать данные от центрального устройства (передача данных является однонаправленной).
Рис. 9: Устройства, имеющие и не имеющие возможность подключенияЦентральные устройства, находясь в поиске пакетов адвертайзинга от периферийных устройств, перестраиваются между тремя первичными каналами адвертайзинга, прослушивая каждый из них в определенный момент времени. Для того, чтобы центральное устройство обнаружило периферийное, оно должно быть настроено на тот же канал, на котором в данный момент вещает периферийное. Для того, чтобы увеличить вероятность этого события и ускорить его наступление, можно изменять некоторые параметры адвертайзинга и сканирования.
Устройство, которое прослушивает каналы адвертайзинга в поисках пакетов адвертайзинга а затем посылает запросы сканирования, находится в режиме активного сканирования, а устройство, которое только принимает пакеты адвертайзинга, и не посылает запросов на сканирование, соответственно находится в режиме пассивного сканирования.
Рис. 10: Пассивное и активное сканированиеСобытие адвертайзинга состоит из нескольких пакетов, отправленных по всем или нескольким из трех каналов первичного адвертайзинга (37, 38 и 39). Существует семь типов событий адвертайзинга (их можно рассматривать как различные типы пакетов):
Подключаемое и сканируемое ненаправленное событие.
Этот тип позволяет другим устройствам принимать пакеты, посылать запросы сканирования отправителю и устанавливать с ним соединение.
Подключаемое ненаправленное событие.
Позволяет другим устройствам принимать пакеты и устанавливать соединение с их отправителем.
Подключаемое направленное событие.
Позволяет определенному устройству принимать пакеты и устанавливать соединение с отправителем
Неподключаемое и несканируемое ненаправленное событие.
Позволяет всем устройствам принимать пакеты. В то же время отклоняет все запросы сканирования и попытки установить соединение.
Неподключаемое и несканируемое направленное событие.
Позволяет определенному устройству принимать пакеты без возможности установить соединение или послать запрос сканирования.
Сканируемое ненаправленное событие.
Дает возможность другим устройствам посылать запросы сканирования отправителю для получения дополнительного пакета данных.
Сканируемое направленное событие.
Позволяет определенному устройству посылать запросы сканирования отправителю пакета адвертайзинга для получения дополнительного пакета данных.
Под параметрами адвертайзинга понимают:
Интервал адвертайзинга.
Наиболее важный параметр из относящихся к адвертайзингу это интервал адвертайзинга. Значение этого параметра может дискретно изменяться в пределах от 20 миллисекунд до 10.24 секунд, с шагом в 625 микросекунд. Интервал адвертайзинга оказывает большое влияние на продолжительность работы от батареи, поэтому выбору его значения следует уделить самое пристальное внимание. Рекомендуется выбирать наибольший интервал адвертайзинга, позволяющий соблюсти баланс между скоростью обнаружения и энергопотреблением.
Данные адвертайзинга и ответа на сканирование.
Давайте посмотрим на формат пакета адвертайзинга и составляющие его поля. Стоит отметить, что пакет ответа на сканирование использует такой же формат.
Рис. 11: Формат пакета адвертайзинга (из спецификации стандарта Bluetooth 5)Данные адвертайзинга используют формат, аналогичный формату TLV (Type-Length-Value, Тип-Длина-Значение), используемому для передачи данных. Отличие состоит в том, что в пакетах адвертайзинга длина данных следует перед их типом. Данные адвертайзинга входят в состав протокольных данных (PDU, Protocol Data Unit) BLE-пакета и включает в себя:
Длину: длину данных, которые следуют за самим значением длины, включая тип данных и непосредственно данные.
Тип данных адвертайзинга: тип данных адвертайзинга, содержащихся в этой структуре TLV.
Данные адвертайзинга: непосредственно данные.
Типы данных адвертайзинга определены в дополнении спецификации Bluetooth (не в основном документе).
Ниже приведены одни из наиболее часто встречающихся типов данных:
Local Name: имя устройства, считываемое при его обнаружении другими устройствами, производящими процедуру сканирования.
Tx Power Level: Мощность передачи, измеряемая в дБм.
Flags: множество однобитных логических флагов (переменные, которые могут принимать одно из двух значений, истина [1] или ложь [0], включающее в себя:
Limited Discoverable Mode (ограниченный режим обнаружения);
General Discoverable Mode (общий режим обнаружения);
BR/EDR Not Supported (возможность поддержки классического протокола Bluetooth);
Возможность одновременной поддержки классического и Low Energy Bluetooth на одном устройстве со стороны контроллера;
Возможность одновременной поддержки классического и Low Energy Bluetooth на одном устройстве со стороны хоста.
Примечание: понятия BR (Basic Rate, базовая пропускная способность) и EDR (Enhanced Data Rate, расширенная пропускная способность) относятся к Bluetooth Classic.
Service Solicitation: список из одного или нескольких UUID, показывающий, какие сервисы поддерживаются и представлены GATT-сервером устройства. Это помогает центральному устройству узнать о поддерживаемых периферийным устройством сервисах до установления соединения.
Appearance: вид, определяет тип устройства в соответствии со спецификацией стандарта. Включает в себя такие виды как телефон, измеритель сердечного ритма, брелок для ключей и множество других.
Если вы не можете найти вид, к которому можно было бы отнести ваше устройство, вы всегда можете оставить ему значение по умолчанию Неопределенный.
Существует три основных параметра сканирования:
Scan Type (тип сканирования): пассивное или активное.
Scan Window (окно сканирования): определяет, длительность сканирования.
Scan Interval (интервал сканирования): определяет частоту повторения сканирования.
Центральное устройство прослушивает один из первичных каналов адвертайзинга в течении всего окна сканирования с периодом, равным интервалу сканирования, причем каждое последующее сканирование проходит на новом канале.
Рис. 12: Параметры сканирования__________________________________
В следующей статье мы рассмотрим вопросы, связанные с соединениями, а также разберемся с сервисами, характеристиками и способами работы с ними.
Новый хаб от Xiaomi с поддержкой технологий Zigbee 3, Bluetooth Mesh, HomeKit и его подключение к достаточно популярной системе умного дома Home Assistant, интересует?
Устройства умного дома можно встретить на разных беспроводных протоколах.
Важно понимать, что поверх каждого протокола производители устройств накладывают что-то своё. А это значит, что нельзя выбрать какой-то один протокол и все устройства всех фирм будут автоматически поддерживаться.
Чаще всего новички выбирают устройства на технологии Wi-Fi. Ведь Wi-Fi роутер сегодня есть у всех. Умным устройством можно пользоваться сразу после покупки. Но тут есть нюанс: в количестве устройств слабость Wi-Fi. Роутеры от провайдеров в большинстве своём тот ещё хлам, способный справиться с 1-2 десятками устройств. И пять новых умных лампочек могут быть проблемой для всей сети.
Здесь выходом будет хороший двухдиапазонный роутер. Весь умный дом можно повесить на диапазон 2.4 ГГц, а мультимедиа-устройства (смартфоны, ноутбуки, телевизоры, колонки) на 5 ГГц.
Устройства на Bluetooth новички выбирают так же охотно, ведь сегодня смартфоны есть почти у всех. Проблема в том, что дальность Bluetooth весьма ограничена. Уже из соседней комнаты вы не сможете посмотреть температуру на датчике или включить чайник или лампочку.
В таких случаях производители рекомендуют покупать BLE Gateway. Это устройство, которое будет посредником между Bluetooth и серверами производителя. Здесь уже далеко не все пользователи соглашаются на дополнительные траты и лишнюю железку в доме.
Некоторые производители встраивают возможности BLE Gateway в камеры и лампы, закрывая проблему лишних затрат и лишнего устройства в доме.
От этой технологии новички отказываются чаще всего из-за необходимости приобретать дополнительное устройство-посредник, ведь ничего работающего с Zigbee в их доме скорее всего нет. Некоторые производители встраивают поддержку этой технологии в умные колонки и, на мой взгляд, это очень интересный ход.
Дополнительную проблему составляет, что каждый такой Gateway поддерживает только дочерние устройства своего производителя. Купив устройства Philips Hue, IKEA, Sonoff, Xiaomi и Tuya, вы, скорее всего, должны будете докупить пять Gateway соответственно.
Эта технология заслуживает внимания по следующим причинам:
Фирма Xiaomi сделала многое для продвижения технологии Zigbee в альтернативных системах умного дома. Их старенький Xiaomi Gateway 2 (DGNWG02LM, lumi.gateway.v3) имел на борту "режим разработчика", который открывал локальный протокол доступа к управлению Zigbee устройствами этого шлюза. Интеграции этого протокола есть в множестве open source систем.
В евро-версии этого шлюза Xiaomi Gateway EU (DGNWG05LM, lumi.gateway.mieu01), а также в обновлённой версии Xiaomi Gateway 3 (ZNDMWG03LM, lumi.gateway.mgl03) этого протокола нет.
Обновлённая версия шлюза получила новый чип на Zigbee 3 (EFR32MG1B), а также поддержку технологии Bluetooth Mesh и HomeKit. В HomeKit поддерживаются не все устройства, будьте внимательны.
В отличие от всех остальных шлюзов, обновлённая версия имеет уникальную особенность: на ней программно можно открыть Telnet-доступ. Доступ открывается только при наличии Mi Home токена, так что всё вполне секьюрно.
В этом шлюзе стоит чип серии EFR32 от фирмы Silicon Labs. Те в свою очередь поставляют вместе с чипом набор SDK. В составе SDK есть MQTT-транспорт, обеспечивающий доступ к Zigbee проколу из любого ПО, установленного как на шлюзе, так и за его пределами.
По умолчанию MQTT-брокер не доступен извне, но у нас ведь теперь есть Telnet!
В брокере есть два корневых топика: это "сырые" данные Zigbee и обработанные данные от Xiaomi. Я решил взять за основу обработанные данные. Там атрибуты устройств хоть и описаны псевдокодами, но в них всё же проще разобраться человеку, ничего не понимающему в Zigbee.
В итоге получился такой вот компонент для Home Assistant XiaomiGateway3.
Он автоматически включает Telnet и публичный MQTT, используя токен Mi Home.
Сейчас токен нужно получать нехитрым образом (инструкция в readme). Но в будущем я планирую добавить получение токена с серверов Xiaomi, используя аккаунт Mi Home. Ведь недавно в сети появилась рабочая реализация авторизации в их облаке.
Сейчас компонент получает список устройств и последние значения их атрибутов с Хаба. Но в дальнейшем я планирую добавить получение списка устройств из облака. Там есть пользовательские названия всех устройств.
С этим пришлось повозиться. Работа с Bluetooth-устройствами не отражается в MQTT. Зато все данные отражаются в консоли. Поэтому компонент подключается к хабу через Telnet отдельным потоком, перезапускает утилиту работы с Bluetooth и читает её вывод в реальном времени. Это самый стабильный способ, что я нашёл. В syslog данные от этой утилиты попадают с перебоями. Моих знаний Linux не хватает, чтоб понять, почему так происходит.
Первым делом компонент научился поддерживать стандартный набор Bluetooth-устройств Xiaomi: датчики температуры, анализаторы почвы и освещенности, фумигатор.
А пару дней назад в нём появилась поддержка умных Bluetooth-замков. На сегодняшний день это единственный из известных мне способов подключить BLE-умный замок Xiaomi в альтернативную систему умного дома.
Чайника у меня нет, но присылайте логи добавлю и его. Правда чайники Xiaomi не поддерживают функцию удалённого включения, это большой минус.
BLE-устройства и их атрибуты отображаются по мере поступления данных. Когда появится поддержка облака полный список BLE устройств с их моделями, именами и последними посланными данными можно будет получить оттуда. Такая информация на хабе не хранится.
Поддержка Bluetooth Mesh ламп пока в разработке. Работа с ними сильно отличается от BLE-устройств.
Грандиозные.
Нужно отладить работу хаба со всем списком официально поддерживаемых Zigbee-устройств. Добавить возможность настройки "тонких" параметров:
Нужно добавить поддержку облака для получения токена хаба и полного списка Zigbee и Bluetooth-устройств.
Нужно добавить поддержку Bluetooth Mesh ламп.
И самое главное добавить поддержку устройств других производителей. Да, это возможно. Мне удалось подключить все сторонние устройства, что у меня были, и управлять ими. Такие устройства не отображаются в Mi Home и HomeKit. Но управлять ими можно с помощью "сырых" Zigbee-команд.
Для понимания полного масштаба проблемы такой поддержки загляните в исходники замечательного проекта zigbee2mqtt: devices, fromZigbee, toZigbee.
Почти каждое устройство требует свой собственный обработчик. В случае с Xiaomi Gateway 3 и официально поддерживаемыми устройствами роль такого обработчика выполняет софт хаба.
Другие мои разработки можно найти на GitHub. Среди русскоговорящей аудитории наиболее популярный проект YandexStation. Глобально очень хорошо себя зарекомендовал SonoffLAN. Но, думаю, XiaomiGateway3 его легко обгонит. За развитием этого и других моих проектов можно следить на моём канале Telegram.
Первая версия компонента XiaomiGateway3 для Home Assistant вышла 4 месяца назад, и с тех пор много всего изменилось. В прошлой статье я писал про создание компонента. А в этой статье расскажу, почему это решение так заинтересовало сотни пользователей.
Эта модель шлюза действительно получилась очень удачной. Иначе как можно объяснить, что компания Xiaomi уже два раза успела обновить прошивку шлюза на своих заводах. Это единственная модель шлюза, у которой прошивка теперь обновляется настолько оперативно на производстве.
В сентябре вышла первая версия компонента. В октябре, после долгих задержек, покупателям начали приходить шлюзы уже с новой прошивкой. В ней Telnet был закрыт паролем. В ноябре прошивка на заводах снова обновилась, и Telnet в ней совсем пропал. Определить проблемный шлюз можно по дате производства на коробке 2020.10 и выше.
В миг шлюз превратился из простого Plug and Play устройства в устройство, которое нужно обязательно вскрыть и прошить для интеграции в альтернативные системы умного дома.
Но шлюз хорош не только своим современным Zigbee-чипом и наличием чипа Bluetooth. Выбранный компанией-производителем SoC от Realtek позволяет в любой момент записать на шлюз любую прошивку, подключив всего три провода UART.
Если вы не любите паять, можно воспользоваться специальной прищепкой с контактами. Есть даже удачные примеры прошивки с бельевой прищепкой. Главное помнить, что обрыв контактов во время прошивки или UART на 5 В вместо 3.3 В вполне могут спалить микросхемы шлюза.
В развитии этого хаба участвует довольно много крутых людей. @serrj-sv собрал скрипт под Windows, который может прошить шлюз в полуавтоматическом режиме. А @zvldz собрал альтернативную версию прошивки, которая на 99% соответствует оригинальной. В ней поправлено недоразумение с закрытым Telnet и ещё пара мелочей. По особенностям прошивки и другим вопросам можно писать в этот чат Telegram.
Все полезные ссылки можно найти в вики проекта.
Многие гики не любят облака и стараются с ними не связываться. Идеология облаков нарушает и главный девиз Home Assistant: конфиденциальность прежде всего (privacy first).
С другой стороны, если при данном подходе сохраняется полноценное локальное управление (local control, вторая часть девиза Home Assistant), то ничего страшного в облаках нет.
Экосистемы производителей добавляют устройствам больше свободы и возможностей управления, включая внешний доступ из коробки и прямое подключение к популярным голосовым ассистентам вроде Яндекс.Алисы и Google Assistant.
Также с этим подходом новые пользователи могут плавно осваивать Open Source системы умного дома: если что-то не получается, можно легко продолжить пользоваться устройством в экосистеме производителя.
Да, где-то на китайском сервере будет хранится информация, включен ли у вас в туалете свет. Так ли много на вас компромата у лампочки? Особенно если сравнивать с данными в вашем браузере или смартфоне. А полный доступ к прошивке шлюза позволяет пытливому уму легко отследить, какая именно информация отправляется на родину.
Компонент поддерживает:
все популярные устройства: выключатели, кнопки, розетки, реле, лампочки, датчики движения, температуры, открытия, протечки, вибрации, газа, дыма, шторы и замки;
редкие устройства вроде термостата Aqara Thermostat S2 (KTWKQ03ES) такой термостат пока не поддерживается даже в zigbee2mqtt;
самые свежие устройства вроде новых: реле Aqara Relay T1 и высокоточный датчик присутствия Aqara Hight Precision Motion Sensor (RTCGQ13LM).
Альтернатива: разнообразные DIY и коммерческие Zigbee-стики и DIY-хабы. Вот довольно большое русскоязычное сообщество в Telegram, где могут ответить на ваши вопросы по поводу Zigbee.
Компонент поддерживает:
все популярные датчики: разнообразные датчики температуры с экраном, датчик ухода за растениями, фумигатор, ночник и умный кубик Рубика;
редкие устройства вроде сейфа Xiaomi Safe Box (BGX-5/X1-3001) да, есть и такое устройство;
самые свежие датчики вроде новых датчиков двери, протечки и движения на технологии BLE;
различные дверные замки экосистемы Xiaomi.
Компонент не поддерживает не BLE устройства вроде чайника и самоката Xiaomi.
Альтернатива: шлюз на основе ESP32 или встроенный Bluetooth на сервере умного дома. Обычно этими способами поддерживаются только популярные датчики температуры альтернативной поддержки дверных замков я не встречал.
Компонент поддерживает новые Mesh-лампы экосистемы Xiaomi под брендами MiJia и Yeelight. И один китайский пользователь уже второй месяц пытается добавить поддержку Mesh-выключателей. А я никак не найду время рассмотреть его pull request.
Новые лампы очень выгодно отличаются ценой и функциями от аналогов на технологиях Wi-Fi и Zigbee, поэтому к ним стоит присмотреться. Об одной из таких ламп я рассказываю в своей статье про адаптивное освещение.
Альтернатива: на ум приходит только новый хаб Yeelight и подключение его к Home Assistant через протокол HomeKit. Open Source проекты с поддержкой Mesh-ламп я не встречал.
Пользователи не любят хабы крупных компаний из-за того, что они поддерживают только устройства своего производителя. Так вот Xiaomi Gateway 3 лишен этого недостатка.
Я добавил в него режим, в котором Home Assistant напрямую подключается к Zigbee-чипу хаба через интеграцию Zigbee Home Automation.
Конечно, у подхода есть минусы:
Zigbee-чип перестаёт работать с Mi Home и начинает работать только с Home Assistant;
по количеству поддерживаемых устройств ZHA сильно уступает проекту zigbee2mqtt.
Но есть и плюсы:
в Китае не узнают, включен ли у вас в туалете свет;
в любой момент можно вернуть хаб в обычный режим работы c Mi Home без последствий для родной прошивки хаба;
BLE-датчики и Mesh-лампы продолжают работать в этом режиме;
команда Home Assistant активно развивает проект ZHA в рамках своей основной работы.
Альтернатива: шлюз Sonoff ZBBridge, прошитый Tasmota.
В некоторых случаях для поддержки сторонних Zigbee-устройств режим ZHA включать не обязательно.
Изучая проблему, почему лампы IKEA E27 из российских магазинов не подключаются к хабам Xiaomi, я пришел к выводу, что в хабах зашита поддержка лишь семи моделей ламп, хотя проект zigbee2mqtt поддерживает более 30 моделей ИКЕА.
Дело осталось за малым вместо настоящей модели лампы подсунуть хабу ту, которую он поддерживает. Реализовать код, организующий подмену модели устройства в момент добавления устройства в хаб, оказалось несложно. Как оказалось, этот способ отлично подошёл для диммеров и реле других фирм.
И самое интересное в данном способе то, что устройства работают и управляются в Mi Home без участия Home Assistant. И могут участвовать в автоматизациях.
Конечно, не стоит рассчитывать, что таким образом заработают любые устройства. Например, у меня не получилось заставить работать лампочки и датчик Philips Hue, а также кнопку Sonoff. А умные розетки, добавленные таким образом, не показывают энергопотребление.
Компонент поддерживает опциональную интеграцию с облаком. При этом компонентом можно пользоваться и без интеграции с облаком, просто добавив шлюз по IP-адресу и токену Mi Home.
Но если вы авторизуетесь в облаке Xiaomi, все данные о шлюзе загрузятся автоматически. Кроме адреса и токена шлюза из облака загрузятся все имена ваших Zigbee, BLE и Mesh-устройств. Вам не придётся снова заполнять их в Home Assistant, выясняя, что за устройство скрывается за именем 0x00158D0007396A5D.
Помимо получения данных о шлюзе и его устройствах - облачная интеграция позволяет получить Mi Home токены от любых Wi-Fi устройств в вашем аккаунте. Если ваш аккаунт использует одновременно разные сервера (например, европейский и китайский), это тоже поддерживается.
Функция получения токенов работает даже если у вас нет шлюза Xiaomi Gateway 3.
Один из самых популярных Zigbee-датчиков в экосистеме Xiaomi это датчик движения. Ранее я пользовался вторым шлюзом Xiaomi и писал автоматизации для этого датчика в Node-RED.
Довольно давно я придумал автоматизацию прогрессивного таймера для датчика движения с опцией быстрого возвращения. Первое движение человека перед датчиком запускало небольшой таймер, а следующие движения - запускали уже увеличенный таймер. Таким образом при постоянном нахождении в комнате свет мог гореть дольше, а при кратковременном пробегании мимо датчика - свет выключался быстро. Хорошо работает в помещении вроде кухни.
Также при обнаружении движения сразу после выключения света - свет включался с увеличенным таймером. Полезно, когда свет выключается не вовремя и злой человек машет рукой датчику.
Логика работы показана на картинке
Радиосвязь всегда менее надёжнее провода. Сигнал от датчика может по разным причинам не дойти в центр. Zigbee и Bluetooth работают на той же частоте, что Wi-Fi и микроволновки.
Для обнаружения возможных проблем в компоненте есть опциональная статистика по каждому Zigbee и BLE устройству.
В ней можно узнать время последнего сообщения от датчика, качество сигнала, количество пропущенных сообщений и через какой роутер устройство работает.
Хотя многие привыкли судить о стабильности в сети именно по качеству сигнала, на мой взгляд именно количество пропущенных сообщений является наиболее показательной величиной.
Именно этот показатель помог мне выпустить две заплатки и уменьшить количество пропусков срабатывания у популярного датчика движения Aqara Motion Sensor. Одна заплатка попала в компонент Home Assistant, а вторая в проект zigbee2mqtt.
За четыре месяца компонент оброс огромным количеством функционала и успел получить более 400 звёзд на GitHub. Но мысли по дальнейшему развитию и не думают кончаться.
Ещё остаётся добавить корректную работу с Bluetooth-устройствами при использовании нескольких хабов на одном сервере Home Assistant. Дело в том, что BLE датчики и Mesh-лампы не привязаны к какому-либо одному хабу. Все хабы могут получать данные с окружающих сенсоров и управлять окружающими лампами. Кстати огромный плюс в сравнении с технологией Zigbee.
Так же в планах добавить настройку параметров дочерних устройств - режим interlock в реле Aqara (переключатель пропал в последних версиях приложения Mi Home), чувствительность и задержки между срабатываниями нового датчика присутствия Aqara и многие другие.
Отдельным направление развития остаётся поддержка конвертеров проекта zigbee2mqtt. Если его удастся довести до релиза, пользователь сможет выбрать, как именно подключать имеющиеся у него устройства:
оригинальное ПО Xiaomi - готово
интеграция Zigbee Home Automation - готово
конвертеры zigbee2mqtt - есть рабочий прототип
И что немаловажно - переключаться между режимами можно в любой момент без последствий для оригинальной прошивки хаба.
На моём GitHub можно найти ссылки на другие компоненты и статьи. А за их развитием можно следить на моём канале в Telegram.
У кого из нас нет добротной техники из прошлого, которая работает так, как и не снилось множеству ширпотребных вещей из настоящего. Старые усилители, проигрыватели пластинок и радиолы. Да, было время, когда не умели делать хрупко и недолговечно в погоне за дешевизной. Эти вещи еще проработают долго. И звучание какого-нибудь старого усилителя будет радовать и нас и наших детей.
Правда, в них отсутствует много всяких цифровых штучек. Разные flac и тому подобные форматы. Управление проигрыванием, музыкой и фильмами через телефон или компьютер. Возможность запустить музыку с Youtube, Last.fm или выбрать интернет радио. Удаленное проигрывание медиа через DLNA. Или просто возможность подключить ваш компьютер через Bluetooth и выводить весь звук через большие колонки. Или что там еще напридумывают нам в будущем для облегчения нашей аудиофильской жизни.
Но что нам мешает использовать все эти технологии в старой технике? Да и почему обязательно в старой У вас есть RCA, miniJack 3.5 или S/PDIF разъем на вашей магнитоле? Тогда мы идем к вам и И делаем из старого усилителя многофункциональный медиа сервер с помощью Raspberry Pi. Подключаем нашу Raspberry Pi через RCA и обновляем нашу технику до "острия технологической атаки". Не обязательно использовать RCA. Найдите аналоговый или цифровой вход на своей технике и выберите нужную комплектацию вашей Raspberry Pi. Я буду рассматривать вопрос сугубо с практической точки зрения. Как настроить все быстро на Raspberry Pi? Ведь статей профессионалов об особенностях работы той или иной технологии достаточно, чтобы не останавливаться на этом. Я возьму в качестве примера Raspberry Pi и плату для цифровой обработки звука на основе чипа PCM 5102 A.
Существует множество различных вариантов музыкальных DAC систем на любой вкус и цвет. Выбор только за вами. Вот некоторые из них для Raspberry Pi. DAC на русском ЦАП цифро-аналоговый преобразователь. Я возьму один из самых недорогих аналогов данного устройства. Судя по различным отзывам на веб просторах, очень даже неплохо себя зарекомендовавшую PCM 5102 A . На ней 6 контактов, и мы с легкостью подключим ее к нашей Raspberry Pi через GPIO разъем. Причем, подойдет любая серия нашей Малинки. Ведь для проигрывания музыки производственных мощностей хватит и самой младшей из них Raspberry Pi Zero. Хотя, без точных входных данных для любой поставленной задачи перед нашей Raspberry Pi, все относительно. И подобное утверждение является оценочным мнением автора. Далее для подключения к GPIO нашей платы используем следующую схему:
BCK -> Pin 12 (GPIO18) DATA(SCLK) -> Pin 40 (GPIO21)LRCK -> Pin 35 (GPIO19) GND -> Pin 39 (GND)GND -> Pin 34 (GND) VCC -> Pin 2 (5v)
И нам потребуется подключить драйвер устройства в config.txt :
sudo nano /boot/config.txt
меняем на:
dtoverlay=hifiberry-dac #dtparam=audio=on
Перезагрузимся у убедимся, что в журналах ядра все ок:
dmesg |grep hifi
И посмотрим нашу карточку через листинг доступных устройств alsa:
aplay -l
**** List of PLAYBACK Hardware Devices **** card 0: sndrpihifiberry [sndrpihifiberry_dac], device 0: HifiBerry DAC HiFi pcm5102a-hifi-0 [HifiBerry DAC HiFi pcm5102a-hifi-0] Subdevices: 1/1 Subdevice #0: subdevice #0
Если что-то пошло не так, включаем режим отладки:
sudo nano /boot/config.txt dtdebug=1
И смотрим, что происходит:
sudo dmesg sudo vcdbg log msg
И, по идее, запустив плеер, допустим, mplayer с каким-нибудь mp3, можно услышать музыку через канал на вашей аппаратуре, к которой вы подключились.
Технология Bluetooth не устаревает и активно развивается. Bluetooth передатчики постоянно обновляют версию. Слушать музыку через нее в HiEnd качестве можно. Она рождена для использования радио канала с большой загруженностью радио эфира. Это немаловажно при использовании ее в городе для передачи медиа контента. Правда, при использовании встроенного передатчика на Малинке не все гладко. И помехи при прослушивании музыки через нее могут быть слышны. Как вариант использовать внешнюю USB Bluetooth антенну. Она позволит избежать потерь, что, в общем-то, отдельная тема, требующая изучения. И тем не менее, достаточно просто и быстро через Bluetooth пробрасывать звук. Скажем, с компьютера на Raspberry Pi и далее через усилитель на колонки. Или с телефона подключаться в Raspberry и слушать музыку. Поэтому, запускаем Bluetooth на Малине. Для запуска нам понадобятся три компонента
Bluetooth сервер
Pulse-audio
Bluetooth клиент
Это не обязательный набор, но, как показала практика, все три компонента позволяют быстро и прозрачно запускать Bluetooth на Raspberry Pi. В результате, мы должны получить три работающие службы:
bluetooth.service
bt-agent.service
pulseaudio
Первая используется как серверная часть, Bt-agent как блютуз
клиент для обработки входящих запросов по авторизации. Это удобно
для автоматизации процессов подключения без заморочек ручного
подключения к нашему каналу. И pulse аудио как сервер,
обрабатывающий звук, и как прозрачная прослойка для автоматического
перенаправления его между устройствами. Установим необходимые
пакеты: sudo apt-get install pulseaudio
pulseaudio-module-bluetooth bluez-tools
Bключим в группы pulse-access bluetooth пользователя pi:
sudo usermod -a -G bluetooth pi
sudo usermod -a -G pulse-access pi
sudo adduser pi pulse-access
Запускаем при старте аудио сервер pulseaudio:
systemctl --user enable pulseaudio
Для работы нашей Малины в режиме блютуз с постоянным обнаружением и с профилем A2DP приемника поправим конфигурацию:
sudo nano /etc/bluetooth/main.conf
и в нем:
Class = 0x41C DiscoverableTimeout = 0 A2DP
Стандарт позволяет передавать звук стереосигналом и с определенными кодеками сжатия звука и согласованными параметрами по битрейду и частотой дискретизации. Класс устройства наглядно описан тут.
И Class = 0x41C в соответствии с этим ресурсом соответствует параметрам:
Major Device Class -> Audio/Video
Minor device class -> Portable Audio
Это те параметры, которые будут видны сканирующему Bluetooth устройству, чтобы разобраться, какие сервисы доступны через это устройство. В общем, можно не заморачиваться этими параметрами и выставить, например, Class = 0x20043C Что просто будет соответствовать большему количеству сервисов. Если вас вдруг не устраивает звук, тогда переходим к настройкам звука pulseaudio:
sudo nano /etc/pulse/daemon.conf
и смотрим "основные" настройки типа:
; resample-method = speex-float-1 ; default-sample-format = s16le; default-sample-rate = 44100 ; alternate-sample-rate = 48000
В качестве мануала по pulseaudio есть неплохой ресурс.
Кто-то рекомендует resample-metod=ffmpeg или resample-metod=speex-float-9, решать вам, если услышите разницу. Выставляем нужные параметры и убираем ";" чтобы раскомментировать строчку.
После изменений перезапускаем pulseaudio:
pulseaudio -k && pulseaudio --start
И не забываем посмотреть, сколько процессорного времени отъест pulseaudio при ваших HI-FI настройках:
htop
По загрузке процессора 3-ей Raspberry Pi с использованием ЦАП (DAC) аудио платы на основе микросхемы PCM5102 файл flac 24bit читается с загрузкой около :
10-15% для resample-metod=speex-float-1
30% для resample-metod=ffmpeg
60% для resample-metod=speex-float-9
Ну и напоследок, посмотрим теперь в сторону передачи звука через WiFi с помощью технологии DLNA.
Если у вас на вашей Raspberry Pi находится медиа сервер, то для того чтобы ваша коллекция музыки была видна на других устройствах, поддерживающих DLNA, вам необходимо установить DLNA server. Здесь все просто: sudo apt-get install minidlna Рекомендую ознакомиться с возможностями Minidlna здесь.
В файле конфигурации правим папку с нашей медиатекой:
sudo nano /etc/minidlna.conf
И там указываем правильно папку:
media_dir=/home/pi/Music
Права на папку должны быть следующими:
sudo chmod -R 755 папка
Проверяем командой:
sudo -u minidlna ls -l папка
Cтартуем сервис:
sudo sevice minidlna start
И теперь в нашем проводнике в закладке сеть должно быть что-то вроде:
И, кликая на эту ссылку, открываем в браузере статус нашего сервиса:
А проиграть файлы можно скажем через "проигрыватель windows media":
Или запустить их на андроид с помощью "HI-FI Cast" приложения:
Если задача обратная и мы хотим воспроизводить музыку и фильмы, просматривать фото, которые находятся где-то удаленно на сервере через нашу Raspberry Pi, то нам потребуется установить на нее dlna render. С устройства из приложения, позволяющего находить наш raspberry media render, мы будем перенаправлять медиа контент на Raspberry Pi.
Я нашел два рендера прекрасно работающих на Raspberry Pi. У каждой из них есть свои плюсы.
Проект развивается и полон последователей. Из основных возможностей стоит отметить что, этот рендер переключает устройства вывода, настраивает громкость и имеет уникальный идентификатор. Это позволит настроить в сети несколько подобных устройств. Есть более подробная статья про использование всех возможностей DLNA/UPnP как распределенной структуры устройств, взаимодействующих по этим протоколам и конкретно этого рендера. Инсталляция не должна вызвать сложностей. Проинсталлируем необходимые пакеты и gstreamer как движок для нашего рендера:
sudo apt-get install autoconf automake libtool gitsudo apt-get install libupnp-dev libgstreamer1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libavsudo apt-get install gstreamer1.0-alsa sudo apt-get install gstreamer1.0-pulseaudio
Возьмем копию проекта и соберем его:
git clone https://github.com/hzeller/gmrender-resurrect.git cd gmrender-resurrect ./autogen.sh ./configure make sudo make install
и запускаем:
gmediarenderer
Найти и управлять нашим сетевым проигрывателем через Android устройство удаленно позволит программа типа DLNA Controller. Воспользуйтесь одной из подобных программ для этого: HiFi Cast, Airpincast, Bubbleupnpcast
https://wiki.gnome.org/Projects/Rygel
Проект представляет из себя не только dlna рендер. Это полноценный медиа сервер UpnPMedia Server, который позволит и расшаривать и перенаправлять музыку видео и фото на любое UPnP/DLNA поддерживающее устройство. Из заявленных возможностей есть конвертация на лету записи в тот формат, который будет поддерживаться устройством воспроизведения. Взаимодействие со сторонними media плеерами такими как Totem, Rhythmbox, VLC. Удаленные запросы UPnP конвертируются в MPRIS запросы и позволяют взаимодействовать с этими media проигрывателями. Пример такого взаимодействия Rygel и VLC рассказан на странице David Wiencer.
Суть в том, что мы запускаем VLC плеер. Устанавливаем в конфигурационном файле mpris поддержку. И при работающем Rygel приложении vlc воспроизводит медиа контент, который мы посылаем ему через DLNA Rygel.
Проинсталлируем Rygel и VLC:
sudo apt-get install rygel vlc rygel-playbin
Если у вас еще не стоят gstreamer библиотеки:
sudo apt-get install libupnp-dev libgstreamer1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav
Убедимся, что в конфигурации включена поддержка MPRIS:
nano ~/.config/rygel.conf
[general]upnp-enabled=trueenable-transcoding=false[Tracker]enabled=false[MediaExport]enabled=false[Playbin]enabled=false[GstLaunch]enabled=false[MPRIS]enabled=true[External]enabled=false
И далее, создаем скрипт David Wiesner, который запускает сначала VLC затем Rygel:
sudo nano rygel-vlc.sh
И в нем:
#!/bin/bashvlcCall="vlc intf dummy fullscreen no-osd"function cleanup(){for pid in $(pgrep -f "$vlcCall"); dokill -9 $piddonekillall rygel}function waitCpuDecrease(){pid=$1lastCpu="0.0"while true; docpu=$(ps S -p $pid -o pcpu=)sleep 0.2[ $(bc <<< "$cpu < $lastCpu") == 1 ] && breaklastCpu=$cpudone}killall rygel and vlccleanuplaunch vlc in background$vlcCall &wait until vlc has done most stuffwaitCpuDecrease $!start rygelrygel
Запустим его в фоновом режиме:
sudo ./rygel-vlc.sh &
И проверим наш DLNA render также через Andriod с помощью: HiFi Cast, Airpincast, Bubbleupnpcast.
И если вы не готовы экспериментировать с Linux , то в качестве собранных, готовых к работе "из коробки" Raspberry Pi с вышеописанным функционалом мы можем вам предложить наборы ViaMyBox "Музыка флер"!
Дистрибутив и код вы можете найти здесь.
https://github.com/viatc/viamybox
Надеюсь со временем, мы увидим и рассмотрим новые интересные проекты для Raspberry Pi на эту тему!
Портал о Hi-Fi, Hi-End технике(и не только) - hifiNews.ru подготовил перевод теста новых наушников британской компании Bowers & Wilkins (B&W).
Bowers & Wilkins PI7Bowers & Wilkins PI7представляют собой полностью беспроводные наушники, которые являются более дорогой из двух новых моделей данного типа, представленных в каталоге компании.
Надо заметить, что при разработке своих наушников B&W применят подход, отличный от компании Focal. Стоимость ее моделей никогда не превышает психологический барьер в $1000, в то время как модели Focal часто стоят дороже. Кроме того, если Focal придерживается последовательной концепции в дизайне своих наушников, подход Bowers & Wilkins более гибкий. И поскольку в настоящее время беспроводные модели находятся на пике популярности, эта британская компания решила выпустить и такую модель.
Однако стоит заметить, что Bowers & Wilkins никогда не принимает
поспешных решений. Ее наушники создаются в результате серьезных
исследований, и имеют интересные технические решения, а также
впечатляющий дизайн. Продолжат ли PI7 эту хорошую традицию
компании? Пришло время нам это узнать.
PI7 - это первая полностью беспроводная модель от Bowers and Wilkins, но компания уже несколько лет выпускает наушники, поэтому неудивительно, что в PI7 нашли свое отражение несколько уже имеющихся у нее решений. Разумеется, здесь есть и несколько изменений в конструкции, связанных с тем, что данные наушники полностью беспроводные. Среди уже знакомого нам в PI7 9,2-миллиметровые динамические излучатели, аналогичные тем, которые мы видели в предыдущих разработках компании.
В PI7 к ним добавлен балансный излучатель, который работает на высоких частотах. Подобное мы тоже видели, в модели PI3, и это позволяет теоретически сочетать мощные басы от динамического излучателя с аккуратными верхами балансного. Ключевой особенностью PI7 является то, что каждый излучатель в них подключен к собственному усилителю. Заявленная частотная характеристика наушников составляет 10 Гц - 20 кГц.
Bowers & Wilkins утверждает, что до этого момента она воздерживалась от выпуска такой модели потому, что ее инженеров не удовлетворяло качество звука при использовании Bluetooth (интересный аргумент для компании с довольно широким ассортиментом беспроводных наушников, ну да ладно), а PI7 имеет полный из доступных на сегодня набор улучшений данной технологии. Наушники поддерживают все разновидности кодеков aptX, известных на данный момент: обычный, HD, с низкой задержкой LLC и адаптивный. Они также имеют поддержку AAC и, разумеется, стандартного кодека SBC. Дополнительно, имеется технология BLE, информирующая вас об уровне заряда аккумулятора, и функция управления через приложение. Разумеется, использование всего этого богатства будет возможным только в случае соответствующей поддержки и со стороны передающего устройства.
Для кодека aptX это означает, что, по крайней мере, теоретически, передачу аудио с параметрами 24 бит/48 кГц можно будет осуществлять. Наушники связываются между собой с использованием сигнала с такими же параметрами. Лично я считаю, что все это не слишком целесообразно. Если вы хотите слушать записи High resolution в мобильных условиях, то, скорее всего, предпочтете традиционное проводное соединение. Но, по крайней мере, вы можете быть уверенным, что сигнал на наушники приходит без дополнительного сжатия.
Из других положительных моментов следует отметить, что PI7 оснащены фирменной системой шумоподавления Bowers & Wilkins. PI7 имеют шесть микрофонов для работы адаптивного шумоподавления (настраиваемого в приложении), с передачей голоса, что было сделано и другими компаниями, но ни одна из них не приблизилась к Bowers. Есть несколько настроек для этой системы в зависимости от окружающей обстановки (лично мне такое не нужно, но, возможно, кому-то пригодится), а также возможность выключить всю дополнительную обработку. Можно с уверенностью сказать, что это наиболее универсальная на сегодня система шумоподавления на рынке, и поместить ее в пару вкладышей настоящий подвиг.
Учитывая, сколько в каждом наушнике находится излучателей, усилителей, микрофонов и тому подобного, места для размещения аккумуляторов там невелико. Bowers & Wilkins указывает продолжительность автономной работы PI7 в четыре часа, и я думаю, что это реалистичный показатель, хотя он может зависеть от выбранного кодека Bluetooth, а также от громкости прослушивания. Как и во многих подобных моделях, на помощь здесь приходит чехол-зарядник. Он обеспечивает четыре цикла заряда вкладышей, и может дать два часа прослушивания музыки после 15-минутной зарядки. Возможно, это не то устройство, которое я взял бы с собой в рейс в Новую Зеландию, но его хватит на неделю обычных поездок.
Это все хорошо, но есть в данных наушниках и некоторые особенности, которые я пока не встречал у конкурентов. Одна из них то, на что еще способен чехол PI7. В его основании находится разъем USB-C, который не только заряжает внутреннюю батарею, но и позволяет передавать аудио. Вы можете подключить его через специальный кабель к аналоговому выходу устройства (разъем 3,5 мм), сигнал с которого будет оцифрован, и передан на вкладыши по Bluetooth с кодеком aptX HD. Это значительно расширяет возможности использования PI7 и выгодно отличает их от конкурирующих моделей, у которых нет подобной функции.
Кроме того, Bower & Wilkins удалось сделать беспроводные
наушники очень привлекательными и удобными в использовании.
Полированные металлические секции вкладышей выглядят превосходно, а
общая форма и размер PI7 достаточно компактны, чтобы они слишком
торчали из вашего уха. При этом наушники B&W сидят в ушах не
только комфортно, но и надежно, по крайней мере, для обычных
пользователей, не увлекающихся экстремальными видами
спорта.
Большая часть тестирования PI7 производилась с помощью Oppo Find
X2 Neo, который имеет поддержку aptX Adaptive и HD, для
качественного воспроизведения музыки. Часть тестирования также
проводилось с помощьюAstell & Kern Kann. Затем
iPad Pro был использован для тестирования качества звука с AAC, а
ноутбук Lenovo T15P обеспечил возможность тестирования через USB и
3,5-мм подключения. Музыкальный материал почти полностью был с
сервисов Qobuz и Deezer, также использовалось некоторое количество
видеороликов.
PI7 продемонстрировали звучание, которое я уже отмечал для модели Signature, но в миниатюре. Сопряжение с Oppo сработало сразу, а подсказки и сообщения, выводимые приложением, значительно упрощают процесс подключения. После установки соединения PI7 работали стабильно и безупречно переподключались 19 раз из 20.
Надо заметить, что Oppo и PI7, работающие вместе, очень чувствительны к определенным типам помех. Например, мониторы дверных датчиков в супермаркетах заставят два устройства либо разорвать соединение, либо пропустить трек. Впрочем, это происходило не во всех магазинах, так что серьезной претензии здесь нет. Но принять во внимание этот факт стоит.
За исключением этого, PI7 сразу произвели на меня сильное впечатление. Во-первых, в эту модель были перенесены все особенности, которыми Bowers & Wilkins наделила свои полноразмерные наушники. Чувствительность сенсоров управления PI7 на обоих вкладышах и работа системы шумоподавления были выше всяких похвал. Качество звука при разговоре по телефону приемлемое, если шум ветра не слишком высок, но это относится ко всем моделям данного класса. На практике PI7 ничем не уступает ни одному из конкурентов, которые я тестировал.
И наконец, хорошее звучание. Действительно, хорошее. Со всеми беспроводными наушниками есть один волшебный момент когда вы забываете, что используете именно беспроводную связь, даже при внимательном прослушивании музыки, и PI7 в этом отношении превосходит всех.
Typhoons от Royal Blood, возможно, не самая Hi-Fi-запись, однако ее насыщенный, взрывной звук способен прижать вас к креслу. PI7 удается поддерживать порядок среди этого музыкального хаоса, не теряя при этом его напора. Даже работая на высоких уровнях громкости, PI7 невозмутимы. Замечательно то, что контролируемость звука не уменьшает удовольствия от прослушивания музыки. Мощная начальная композиция Trouble's Coming по-прежнему остается в полной мере бунтарской, хотя и чуть-чуть управляемой.
Если вы хотите насладиться чем-то более аудиофильским, то PI7 способны и на это. Композиция Wilted Пэрис Джексон имела богатую и широкую звуковую сцену, в которой и вокал, и аккомпанирующие инструменты достоверны и правильно расположены в пространстве. Даже при интенсивной работе шумоподавления какого-либо искусственного влияния на звук практически не заметно. PI7 звучали так, как будто я использовал проводное соединение, и даже с учетом того, что в моем распоряжении было несколько классных наушников такого типа, я не спешил расстаться с этими вкладышами.
Переключение на AAC через iPad действительно частично снижает качество звука, потому что у этого кодека более низкий битрейт. Если на Oppo музыка с сервисов Qobuz и (с потерями) Deezer звучит по-разному, то на iPad эта разница теряется. Это все еще хороший звук, но, очевидно, Apple оставила в нем какой-то запас, что бы полностью реализовать в собственном AirPod Pro, и, тем самым, дать им определенные преимущества. Однако в случае мобильного использования звук все равно вполне хорош.
А как насчет системы подключения через чехол? Она работает и
работает хорошо. USB-соединение практически не имеет преимуществ по
сравнению с Oppo, поэтому использование PI7 с ноутбуком становится
бесполезным занятием. Подключение через аналоговый выход шумнее,
чем другие варианты, и несколько снижает качество звука, но это все
еще уникальная функция, изучением которой, я не сомневаюсь, сейчас
вплотную занимаются конкуренты. Насколько она нужна именно вам
отдельный вопрос, но подобное, на мой взгляд, сейчас есть только у
PI7.
В основе любых дебатов о ценности PI7 для многих пользователей - оправдывают ли они стоимость на $150 фунтов выше, чем у AirPod Pro.
стоимость в России на 05.2021 ~ 29 990
На этот вопрос есть два ответа: один совершенно очевидный, а другой - более тонкий. Если вы не являетесь пользователем iPhone и у вас есть достойная реализация aptX, то качество звука PI7 за счет более совершенного кодека будет выше. Если вы пользователь iOS, решение больше зависит от личных предпочтений и от того, есть ли у вас дополнительные $150, но я думаю, что PI7 все же будут лучше. Качество реализации шумоподавления, качество сборки и уровень комфорта, предлагаемые здесь, в значительной степени являются лучшими в данном классе. Bowers & Wilkins не спешила выпускать полностью беспроводные наушники, но результат впечатляет и на сегодня PI7 являются бесспорной самой выгодной покупкой.
Понравилось
Превосходное качество звука, особенно с высококачественными кодеками Bluetooth
Отличное шумоподавление
Удобные и качественно сделанныеНе понравилось
Небольшое время автономной работы вкладышей
Звучание с использованием кодека AAC не впечатляет
Возможны конфликты с внешним оборудованием
Высокая стоимость
Оценки:
Качество сборки: 9
Легкость использования: 9
Качество звука: 9
Дизайн: 9
Чувствительность: 9
Вердикт: 9
Основные технические характеристики Bowers & Wilkins PI7 |
|
Тип: |
вкладыши, полностью беспроводные, с активным подавлением шумов |
Излучатели: |
9,2 мм динамический + арматурный |
Частотный диапазон: |
10 Гц - 20 кГц |
Чехол-зарядник в комплекте: |
да |
Заменяемые внутриушные вставки: |
да |
Соединение: |
Bluetooth SBC, AAC, aptX, aptX HD, LLC, Adaptive |
При подготовке обзора использовались материалы с www.avforums.com (перевод с английского - hifiNews.RU)
В последний год я разрабатывал Bluetooth Low Energy (BLE) приложения под iOS и это оказалось довольно простым. Далее было портирование их на Android насколько это могло быть сложным?
Могу точно сказать это было сложней, чем представлял, мне пришлось приложить немало усилий для стабильной работы под Android. Я изучил много статей в свободном доступе, некоторые оказались ошибочными, многие были очень полезными и помогли в деле. В этой серии статей я хочу описать свои выводы, чтобы вы не тратили уйму времени на поиски как я.
Google документация по BLE очень общая, в некоторых случаях нет важной информации или она устарела, примеры приложений не показывают, как правильно использовать BLE. Я обнаружил лишь несколько источников, как правильно сделать BLE.Презентация Stuart Kentдает замечательный материал для старта. Для некоторых продвинутых тем есть хорошая статьяNordic.
Android BLE API это низкоуровневые операции, в реальных приложениях нужно использовать несколько слоев абстракции (как например сделано из коробки в iOS-CoreBluetooth). Обычно нужно самостоятельно сделать: очередь команд, bonding, обслуживание соединений, обработка ошибок и багов, мультипоточный доступ . Самые известные библиотеки:SweetBlue,RxAndroidBleиNordic. На мой взгляд самая легкая для изучения - Nordic,см. детали тут.
Производители делают изменения в Android BLE стекеили полностью заменяют на свою реализацию. И надо учитывать разницу поведения для разных устройств в приложении. То что прекрасно работает на одном телефоне, может не работать на других! В целом не все так плохо, например реализация Samsung сделана лучше собственной реализации от Google!
В Android есть несколько известных (и неизвестных) баговкоторые должны быть обработаны, особенно в версиях 4,5 и 6. Более поздние версии работают намного лучше, но тоже имеют определенные проблемы, такие как случайные сбои соединения с ошибкой 133. Подробнее об этом ниже.
Не претендую на то, что я решил все проблемы, но мне удалось выйти на приемлемый уровень. Начнем со сканирования.
Перед подключением к устройству вам нужно его просканировать.
Это делается при помощи классаBluetoothLeScanner
:
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();BluetoothLeScanner scanner = adapter.getBluetoothLeScanner();if (scanner != null) { scanner.startScan(filters, scanSettings, scanCallback); Log.d(TAG, "scan started");} else { Log.e(TAG, "could not get scanner object");}
Сканер пытается найти устройства в соответствии снастройками
filters
иscanSettings
, при обнаружении
устройства вызываетсяscanCallback
:
private final ScanCallback scanCallback = new ScanCallback() { @Override public void onScanResult(int callbackType, ScanResult result) { BluetoothDevice device = result.getDevice(); // ...do whatever you want with this found device } @Override public void onBatchScanResults(List<ScanResult> results) { // Ignore for now } @Override public void onScanFailed(int errorCode) { // Ignore for now }};
В результате сканирования мы получаем
экземплярScanResult
, в котором есть
объектBluetoothDevice
, его используют для подключения
к устройству. Но прежде чем начать подключаться, поговорим о
сканировании подробнее,ScanResult
содержит несколько
полезных сведений об устройстве:
Advertisement data- массив байтов с информацией
об устройстве, для большинства устройств это имя и UUID сервисов,
можно задать вfilters
имя устройства и UUID сервисов
для поиска конкретных устройств.
RSSI уровень- уровень сигнала (насколько близко устройство).
дополнительные данные, см. документацию
поScanResult
здесь.
Помним про жизненный
циклActivity
,onScanResult
может вызываться
многократно для одних и тех же устройств, при
пересозданииActivity
сканирование может запускаться
повторно, вызываю лавину вызововonScanResult
.
Вообще можно передать null вместо фильтров и получить все ближайшие устройства, иногда это полезно, но чаще требуются устройства с определенным именем или набором UUID сервисов.
Используется если вам необходимо найти устройства определенной категории, например мониторы артериального давления со стандартным сервисным UUID: 1810. При сканировании устройство может содержать вAdvertisement dataUUID сервис, который характеризует это устройство. На самом деле эти данные ненадежные, фактически сервисы могут не поддерживаться, или подделыватьсяAdvertisement dataданные, в общем тут есть творческий момент.
Прим. переводчика: одно из моих устройств со специфичной прошивкой, вообще не содержало список UUID сервисов вAdvertisement data, хотя все остальные прошивки этого устройства работали ожидаемо.
Пример сканирования службы с артериальным давлением:
UUID BLP_SERVICE_UUID = UUID.fromString("00001810-0000-1000-8000-00805f9b34fb");UUID[] serviceUUIDs = new UUID[]{BLP_SERVICE_UUID};List<ScanFilter> filters = null;if(serviceUUIDs != null) { filters = new ArrayList<>(); for (UUID serviceUUID : serviceUUIDs) { ScanFilter filter = new ScanFilter.Builder() .setServiceUuid(new ParcelUuid(serviceUUID)) .build(); filters.add(filter); }}scanner.startScan(filters, scanSettings, scanCallback);
Обратите внимание на короткий UUID (например1810
),
он называется16-bit UUID
и является частью
длинного128-bit UUID
(в данном
случае00001810-000000-1000-8000-000-00805f9b34fb
).
Короткий UUID это BASE_PART длинного UUID, см. спецификациюздесь.
Поиск устройств использует точное совпадение имени устройства, обычно это применяется в двух случаях:
поиск конкретного устройства
поиск конкретной модели устройства, например, мой нагрудный
напульсник Polar H7 определяется как Polar H7 391BBB014, первая
часть - Polar H7 общая для всех таких устройств этой модели, а
последняя часть 391BBB014 - уникальный серийный номер. Это очень
распространенная практика. Если вы хотите найти все устройства
Polar H7, то фильтр по имени вам не поможет, придется искать
подстроку у всех отсканированных устройств
вScanResult
. Пример с поискомточнопо
имени:
String[] names = new String[]{"Polar H7 391BB014"};List<ScanFilter> filters = null;if(names != null) { filters = new ArrayList<>(); for (String name : names) { ScanFilter filter = new ScanFilter.Builder() .setDeviceName(name) .build(); filters.add(filter); }}scanner.startScan(filters, scanSettings, scanCallback);
Обычно применяется дляпереподключенияк уже известным устройствам. Обычно мы не знаем MAC-адрес девайса, если не сканировали его раньше, иногда адрес печатается на коробке или на корпусе самого устройства, особенно это касается медицинских приборов. Существует другой способ повторного подключения, но в некоторых случаях придется еще раз сканировать устройство, например при очистке кеша Bluetooth.
String[] peripheralAddresses = new String[]{"01:0A:5C:7D:D0:1A"};// Build filters listList<ScanFilter> filters = null;if (peripheralAddresses != null) { filters = new ArrayList<>(); for (String address : peripheralAddresses) { ScanFilter filter = new ScanFilter.Builder() .setDeviceAddress(address) .build(); filters.add(filter); }}scanner.startScan(filters, scanSettings, scanByServiceUUIDCallback);
Вероятно вы уже поняли, что можно комбинировать в фильтре UUID, имя и MAC-адрес устройства. Выглядит неплохо, но на практике я не применял такое. Хотя может быть вам это пригодится.
ScanSettings
объясняют Android как сканировать
устройства. Там есть ряд настроек, которые можно задать, ниже
полный пример:
ScanSettings scanSettings = new ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE) .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT) .setReportDelay(0L) .build();
Безусловно, это самый важный параметр. Определяет метод и время сканирования в Bluetooth стеке. Такая операция требует много энергии и необходим контроль над этим процессом, чтобы не разрядить батарею телефона быстро. Есть 4 режима работы, в соответствии с руководствомNordics и официальной документацией:
SCAN_MODE_LOW_POWER
. В этом режиме Android
сканирует 0.5с, потом делает паузу на 4.5с. Поиск может занять
относительно длительное время, зависит от того насколько часто
устройство посылает пакет advertisement данных.
SCAN_MODE_BALANCED
. Время сканирования: 2с, время
паузы: 3с, компромиссный режим работы.
SCAN_MODE_LOW_LATENCY
. В этом случае, Android
сканирует непрерывно, что очевидно требует больше энергозатрат, при
этом получаются лучшие результаты сканирования. Режим подходит если
вы хотите найти свое устройство как можно быстрее. Не стоит
использовать для длительного сканирования.
SCAN_MODE_OPPORTUNISTIC
. Результаты будут получены,
если сканирование выполняется другими приложениями! Строго говоря,
это вообще не гарантирует, что обнаружится ваше устройство. Стек
Android использует этот режим в случае долгого сканирования, для
понижения качества результатов (см. ниже Непрерывное
сканирование).
Эта настройка контролирует как будет вызываться callback
соScanResult
в соответствии с заданными фильтрами, есть
3 варианта:
CALLBACK_TYPE_ALL_MATCHES
. Callback будет вызывать
каждый раз, при получении advertisement пакета от устройств. На
практике - каждые 200-500мс будет срабатывать сallback, в
зависимости от частоты отправки advertisement пакетов
устройствами.
CALLBACK_TYPE_FIRST_MATCH
. Callback сработает один
раз для устройства, даже если оно далее будет снова посылать
advertisement пакеты.
CALLBACK_TYPE_MATCH_LOST
. Callback будет вызван,
если получен первый advertisement пакет от устройства и дальнейшие
advertisement пакеты не обнаружены. Немного странное поведение.
В практике обычно используются
настройкаCALLBACK_TYPE_ALL_MATCHES
илиCALLBACK_TYPE_FIRST_MATCH
.
Правильный тип зависит от конкретного случая. Если не знаете -
используйтеCALLBACK_TYPE_ALL_MATCHES
, это дает больше
контроля при получении callback, если вы останавливаете
сканирование после получения нужных результатов - фактически
этоCALLBACK_TYPE_FIRST_MATCH
.
Настройка того, как Android определяет совпадения.
MATCH_MODE_AGGRESSIVE
. Агрессивность
обуславливается поиском минимального количества advertisement
пакетов и устройств даже со слабым сигналом.
MATCH_MODE_STICKY
. В противоположность, этот режим
требует большего количества advertisement пакетов и хорошего уровня
сигнала от устройств.
Я не тестировал эти настройки подробно, но я в основном
используюMATCH_MODE_AGGRESSIVE
, это помогает быстрее
найти устройства.
Параметр определяет сколько advertisement данных необходимо для совпадения.
MATCH_NUM_ONE_ADVERTISEMENT
. Одного пакета
достаточно.
MATCH_NUM_FEW_ADVERTISEMENT
. Несколько пакетов
нужно для соответствия.
MATCH_NUM_MAX_ADVERTISEMENT
. Максимальное
количество advertisement данных, которые устройство может
обработать за один временной кадр.
Нет большой необходимости в таком низкоуровневом контроле. Все что вам надо - быстро найти свое устройство, обычно используются первые 2 варианта.
Задержка для вызова сallback в миллисекундах. Если она больше
нуля, Android будет собирать результаты в течение этого времени и
вышлет их сразу все в обработчикеonBatchScanResults
.
Важно понимать чтоonScanResult
не будет вызываться.
Обычно применяется, когда есть несколько устройств одного типа и мы
хотим дать пользователю выбрать одно из них. Единственная проблема
здесь - предоставить информацию пользователю для выбора, это должен
быть не только MAC-адрес (например имя устройства).
Важно: естьизвестный багдля Samsung S6 / Samsung S6 Edge, когда все результаты сканирования имеют один и тот же RSSI (уровень сигнала) при задержке больше нуля.
В результате процесса сканирования вы получаете список BLE устройств и при этом данные устройств кешируются в Bluetooth стеке. Там хранится основная информация: имя, MAC-адрес, тип адреса (публичный, случайный), тип устройства (Classic, Dual, BLE) и т.д. Android нужны эти данные, чтобы подключится к устройству быстрее. Он кеширует все устройства, которые видит при сканировании. Для каждого из них записывается небольшой файл с данными. Когда вы пытаетесь подключиться к устройству, стек Android ищет соответствующий файл, чтобы прочитать данные для подключения. Важный момент - одного MAC-адреса недостаточно для успешного подключения к устройству!
Bluetooth кеш, как и любой другой, не существует вечно, есть 3 ситуации, когда он очищается:
Выключение и включение системного переключателя Bluetooth
Перезагрузка телефона
Очистка данных приложения (в ручном режиме в настройках телефона)
Это достаточно неудобный момент для разработчиков, потому что телефон часто перезагружается, пользователь может включать-выключать самолетный режим. Есть еще различия между производителями телефонов, например на некоторых телефонах Samsung, кеш не очищался при выключении Bluetooth.
Это значит, что нельзя полагаться на данные об устройстве из BT кеша. Есть небольшой трюк, он поможет узнать закешировано ли устройство или нет:
// Get device object for a mac addressBluetoothDevice device = bluetoothAdapter.getRemoteDevice(peripheralAddress)// Check if the peripheral is cached or notint deviceType = device.getType();if(deviceType == BluetoothDevice.DEVICE_TYPE_UNKNOWN) { // The peripheral is not cached} else { // The peripheral is cached}
Это важный момент, если нужно подключиться к устройству позже, не сканируя его. Подробнее об этом позже.
Вообще хорошая практика избегать непрерывного сканирования потому что, это очень энергоемкая операция, а пользователи любят, когда батарея их смартфона работает долго. Если вам действительно нужно постоянное сканирование, например при поиске BLE-маячков, выберите настройки сканирования с низким потреблением и ограничивайте время сканирования, например когда приложение находится только на переднем плане (foreground), либо сканируйте с перерывами.
Плохая новость в том, что Google в последнее время ограничивает (неофициально) непрерывное сканирование:
c Android 8.1сканирование без фильтров
блокируется при выключенном экране. Если у вас нет
никакихScanFilters
, Android приостановит сканирование,
когда экран выключен и продолжит, когда экран снова будет
включен.Комментарии от Google.Это
очевидно очередной способ энергосбережения от Google.
c Android 7 вы можете сканировать только в течение 30 минут,
после чего Android меняет параметры
наSCAN_MODE_OPPORTUNISTIC
.Очевидное решение,
перезапускать сканирование с периодом менее, чем30 мин. Посмотритеcommitв исходном коде.
с Android 7 запуск и останов сканирования более 5 раз за 30 секундвременно отключает сканирование.
Google значительно усложнил сканирование на переднем плане. Для фонового режима вы столкнетесь с еще большими трудностями! Новые версии Android имеют лимиты на работу служб в фоновом режиме, обычно после 10 минут работы, фоновый сервис прекращает свою работу принудительно. Посмотрите возможные решения этой проблемы:
Обсуждение наStackOverflow
СтатьяDavid Young
Есть еще несколько важных моментов, прежде чем мы закончим статью. Для начала сканирования нужны системные разрешения (permissions):
<uses-permission android:name="android.permission.BLUETOOTH" /><uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /><uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
Убедитесь, что все разрешения одобрены, или запросите их у
пользователя. РазрешениеACCESS_COARSE_LOCATION
Google
считает опасным и для него требуется обязательное согласие
пользователя.
private boolean hasPermissions() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (getApplicationContext().checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { requestPermissions(new String[] { Manifest.permission.ACCESS_COARSE_LOCATION }, ACCESS_COARSE_LOCATION_REQUEST); return false; } } return true;}
Прим. переводчика, в моем проекте для корректной работы с
BLE потребовалось еще 2
разрешения:ACCESS_FINE_LOCATION
(для
API<23) иACCESS_BACKGROUND_LOCATION
обсуждение на
Stackoverflow.
В итоге полный список разрешений включая версию Android10:
<uses-permission android:name="android.permission.BLUETOOTH" /><uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /><uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /><uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /><uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
После получения всех нужный разрешений, нужно проверить включен
Bluetooth, если нет - используйтеIntent
для запуска
запроса на включение:
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();if (!bluetoothAdapter.isEnabled()) { Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);}
Мы научились запускать сканирование BLE устройств с учетом жизненного цикла Activity (Fragment / Service), использовать фильтры и различные настройки сканирования, также узнали все нужные разрешения (permissions) для удачного запуска сканирования и особенности работы Android-Bluetooth кеша. В следующей статье мы погрузимся глубже в процесс подключения и отключения к устройствам.
Спасибо!
Часть #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
, 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
содержит новое состояние
подключения и может иметь 4 значения:
STATE_CONNECTED
STATE_DISCONNECTED
STATE_CONNECTING
STATE_DISCONNECTING
Значения говорят сами за себя. Хотя
состоянияSTATE_CONNECTING
,STATE_DISCONNECTING
есть
в документации, на практике я их не встречал. Так что, в принципе,
можно не обрабатывать их, но для уверенности, я предлагаю их явно
учитывать (прим. переводчика - и это лучше, чем не обрабатывать
их), вызываяclose()
только в том случае если
устройство действительно отключено.
В примере выше, переменная статуса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();}
Это не последний вариант, мы еще улучшим колбек в этой статье. В любом случае, теперь у нас есть обработка ошибок и успешных операций.
Последний параметр, который необходимо учитывать в
колбеке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 часто встречается при попытках подключиться к устройству, особенно во время разработки. Этот статус может иметь множество причин, некоторые из них можно контролировать:
Убедитесь, что вы всегда вызываетеclose()
при
отключении. Если этого не сделать, в следующий раз при подключении
вы точно получитеstatus=133
;
Всегда используйтеTRANSPORT_LE
в
вызовеconnectGatt()
;
Перезагрузите смартфон. Возможно Bluetooth стек выбрал лимит по клиентским подключениям или есть внутренняя проблема. (Прим. переводчика: я сначала выключал/включал Bluetooth, потом Airplane режим и если не помогало - перезагружал);
Проверьте что устройство посылает advertising пакеты.
ВызовconnectGatt()
сautoconnect =
false
имеет таймаут 30 секунд, после чего присылает
ошибкуstatus=133
;
Замените/зарядите батарею на устройстве. Обычно устройства работают нестабильно при низком заряде;
Если вы попробовали все способы выше и все еще получаете статус
133, необходимо простоповторить подключение! Это
одна из Android ошибок, которую мне так и не удалось понять или
решить. Иногда вы получаете 133 при подключении к устройству, но
если вызыватьclose()
и переподключиться, то все
работает без проблем! Есть подозрение, что проблема в кеше Android
и вызовclose()
сбрасывает его состояние для конкретного
устройства. Если кто-нибудь поймет, как решить эту проблему дайте
мне знать!
Для отключения устройства вам необходимо сделать шаги:
вызвать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
не надо проводить такую
отмену!
Как только вы подключились к устройству, необходимо запустить
обнаружение его сервисов вызовом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 в вашем приложении.
Часть #2 (connecting/disconnecting)
Часть #3 (read/write), вы здесь
Впредыдущей статьемы подробно поговорили о подключении/отключении BLE устройств. Эта статья очтенииизаписихарактеристик, а такжевключении-выключении уведомлений.
Многие разработчики, которые начинают работать с BLE на Android, сталкиваются с проблемами чтения/записи BLE характеристик. НаStackoverflowполно людей, предлагающих просто использовать задержки Большинство таких советов неверные.
Есть две основные причины проблем:
Операции чтения/записи асинхронные. Это значит,
что вызов метода вернется немедленно, но результат вызова вы
получите немного позже в соответствующих колбеках.
НапримерonCharacteristicRead()
илиonCharacteristicWrite()
.
Одновременно может быть запущена только одна
операция. Нужно дождаться выполнения текущей операции, и
затем, запускать следующую. В исходном
кодеBluetoothGatt
есть блокирующая переменная, которая
при запуске операции устанавливается и при вызове колбека
сбрасывается. Google забыла про это упомянуть в документации
(Прим. переводчика: речь идет
оmDeviceBusy
иmDeviceBusyLock
здесь).
Первая причина, на самом деле, не является проблемой, такова природа BLE. Асинхронное программирование это распространенная штука, используется, например, при сетевых вызовах. Однако вторая причина раздражает и требует специального подхода.
Ниже кусок кодаBluetoothGatt.java
с блокировкой
переменнойmDeviceBusy
, перед чтением
характеристики:
public boolean readCharacteristic(BluetoothGattCharacteristic characteristic) { if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) == 0) { return false; } if (VDBG) Log.d(TAG, "readCharacteristic() - uuid: " + characteristic.getUuid()); if (mService == null || mClientIf == 0) return false; BluetoothGattService service = characteristic.getService(); if (service == null) return false; BluetoothDevice device = service.getDevice(); if (device == null) return false; synchronized (mDeviceBusy) { if (mDeviceBusy) return false; mDeviceBusy = true; } try { mService.readCharacteristic(mClientIf, device.getAddress(), characteristic.getInstanceId(), AUTHENTICATION_NONE); } catch (RemoteException e) { Log.e(TAG, "", e); mDeviceBusy = false; return false; } return true;}
Когда приходит результат чтения/записи,
переменнаяmDeviceBusy
сбрасывается в false снова:
public void onCharacteristicRead(String address, int status, int handle, byte[] value) { if (VDBG) { Log.d(TAG, "onCharacteristicRead() - Device=" + address + " handle=" + handle + " Status=" + status); } if (!address.equals(mDevice.getAddress())) { return; } synchronized (mDeviceBusy) { mDeviceBusy = false; }....
Выполнять чтение/запись по одной операции за раз неудобно, но
любое сложное приложение должно это учитывать. Решение этой
проблемы - использованиеочереди команд. Все BLE
библиотеки, которые я ранее упоминал, так или иначе реализуют
очередь. Это одна из лучших практик! Идея простая каждая команда
сначала добавляется в очередь. Затем команда забирается из очереди
на исполнение, после результата, команда помечается как завершенная
и, удаляется из очереди. Запускать команды можно в любое время, но
они выполняются точно в том порядке, в котором поступают в очередь.
Это очень упрощает разработку под BLE. В iOS аналогично работает
фреймворкCoreBluetooth
(Прим. переводчика: который
намного удобнее, чем реализация Bluetooth стека в
Android).
Очередь создается для каждого объектаBluetoothGatt
.
К счастью, Android сможет обрабатывать очереди от нескольких
объектовBluetoothGatt
, вам не нужно об этом
беспокоиться (Прим. переводчика: у меня это не сработало, я
использовал глобальную очередь команд для всех устройств).
Есть много способов создать очередь, мы будем использовать простую
очередьQueue
сRunnable
для каждой команды и
переменнойcommandQueueBusy
для отслеживания работы
команды:
private Queue<Runnable> commandQueue;private boolean commandQueueBusy;
Мы добавляем новый экземплярRunnable
в очередь при
выполнении команды. Ниже пример чтения характеристики
(readCharacteristic):
public boolean readCharacteristic(final BluetoothGattCharacteristic characteristic) { if(bluetoothGatt == null) { Log.e(TAG, "ERROR: Gatt is 'null', ignoring read request"); return false; } // Check if characteristic is valid if(characteristic == null) { Log.e(TAG, "ERROR: Characteristic is 'null', ignoring read request"); return false; } // Check if this characteristic actually has READ property if((characteristic.getProperties() & PROPERTY_READ) == 0 ) { Log.e(TAG, "ERROR: Characteristic cannot be read"); return false; } // Enqueue the read command now that all checks have been passed boolean result = commandQueue.add(new Runnable() { @Override public void run() { if(!bluetoothGatt.readCharacteristic(characteristic)) { Log.e(TAG, String.format("ERROR: readCharacteristic failed for characteristic: %s", characteristic.getUuid())); completedCommand(); } else { Log.d(TAG, String.format("reading characteristic <%s>", characteristic.getUuid())); nrTries++; } } }); if(result) { nextCommand(); } else { Log.e(TAG, "ERROR: Could not enqueue read characteristic command"); } return result;}
В этом методе, сначала проверяем все ли готово для выполнения
(наличие и тип характеристики) и логгируем ошибки, если они есть.
ВнутриRunnable
, фактически вызывается
методreadCharacteristic()
, который выдает команду на
устройство. Мы также отслеживаем сколько было попыток, чтобы
сделать повтор в случае ошибки (Прим. переводчика: это лучшая
тактика, чтобы добиться стабильной работы с устройством). Если
чтение характеристики возвращаетfalse
, мы логгируем
ошибку, завершаем команду, чтобы можно было запустить следующую.
Наконец вызываетсяnextCommand()
, чтобы запустить
следующую команду из очереди:
private void nextCommand() { // If there is still a command being executed then bail out if(commandQueueBusy) { return; } // Check if we still have a valid gatt object if (bluetoothGatt == null) { Log.e(TAG, String.format("ERROR: GATT is 'null' for peripheral '%s', clearing command queue", getAddress())); commandQueue.clear(); commandQueueBusy = false; return; } // Execute the next command in the queue if (commandQueue.size() > 0) { final Runnable bluetoothCommand = commandQueue.peek(); commandQueueBusy = true; nrTries = 0; bleHandler.post(new Runnable() { @Override public void run() { try { bluetoothCommand.run(); } catch (Exception ex) { Log.e(TAG, String.format("ERROR: Command exception for device '%s'", getName()), ex); } } }); }}
Обратите внимание, мы используем методpeek()
для
получения объектаRunnable
из очереди, чтобы можно было
повторить запуск позже. Этот метод не удаляет объект из
очереди.
Результат чтения будет отправлен в ваш колбек:
@Overridepublic void onCharacteristicRead(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, int status) { // Perform some checks on the status field if (status != GATT_SUCCESS) { Log.e(TAG, String.format(Locale.ENGLISH,"ERROR: Read failed for characteristic: %s, status %d", characteristic.getUuid(), status)); completedCommand(); return; } // Characteristic has been read so processes it ... // We done, complete the command completedCommand();}
Мы завершаем командуcompletedCommand()
после
обработки нового значения. Это помогает избежать одновременный
вызов другой команды и состояния гонки.
Теперь мы готовы завершить команду,
убираемRunnable
из очереди через
вызовpoll()
и запускаем следующую из очереди:
private void completedCommand() { commandQueueBusy = false; isRetrying = false; commandQueue.poll(); nextCommand();}
В некоторых случаях (ошибка, неожиданное значение), вам нужно
будет повторить команду. Сделать это просто, так как
объектRunnable
остается в очереди до
вызоваcompletedCommand()
. Чтобы не уйти в бесконечное
повторение проверяем лимит на повторы:
private void retryCommand() { commandQueueBusy = false; Runnable currentCommand = commandQueue.peek(); if(currentCommand != null) { if (nrTries >= MAX_TRIES) { // Max retries reached, give up on this one and proceed Log.v(TAG, "Max number of tries reached"); commandQueue.poll(); } else { isRetrying = true; } } nextCommand();}
Чтение характеристики достаточно простая операция, а запись требует дополнительных пояснений. Для выполнения записи нужно предоставитьхарактеристику,массив байтовитип записи. Существует несколько типов записи, важные для нас это:
WRITE_TYPE_DEFAULT
(вы получите ответ от устройства,
например, код завершения);
WRITE_TYPE_NO_RESPONSE
(никакого ответа от
устройства не будет).
Использовать тот или иной тип зависит от вашего устройства и характеристики (иногда она поддерживает оба типа записи, иногда только один конкретный тип).
В Android каждая характеристика имеет дефолтный тип записи, который определяется при ее создании. Ниже фрагмент кода из исходников Android, где определяется тип:
...if ((mProperties & PROPERTY_WRITE_NO_RESPONSE) != 0) { mWriteType = WRITE_TYPE_NO_RESPONSE;} else { mWriteType = WRITE_TYPE_DEFAULT;}...
Как вы видите, это работает нормально, если характеристика
поддерживает только один их двух типов записи. Если характеристика
поддерживает оба типа, то значение по умолчанию
будетWRITE_TYPE_NO_RESPONSE
. Имейте это ввиду!
Перед записью можно проверить характеристику, поддерживает ли она нужный тип записи:
// Check if this characteristic actually supports this writeTypeint writeProperty;switch (writeType) { case WRITE_TYPE_DEFAULT: writeProperty = PROPERTY_WRITE; break; case WRITE_TYPE_NO_RESPONSE : writeProperty = PROPERTY_WRITE_NO_RESPONSE; break; case WRITE_TYPE_SIGNED : writeProperty = PROPERTY_SIGNED_WRITE; break; default: writeProperty = 0; break;}if((characteristic.getProperties() & writeProperty) == 0 ) { Log.e(TAG, String.format(Locale.ENGLISH,"ERROR: Characteristic <%s> does not support writeType '%s'", characteristic.getUuid(), writeTypeToString(writeType))); return false;}
Я рекомендую всегда явно указывать тип записи и не полагаться на дефолтные настройки выбранные Android!
Итак, запись массива байтовbytesToWrite
в
характеристику выглядит так:
characteristic.setValue(bytesToWrite);characteristic.setWriteType(writeType);if (!bluetoothGatt.writeCharacteristic(characteristic)) { Log.e(TAG, String.format("ERROR: writeCharacteristic failed for characteristic: %s", characteristic.getUuid())); completedCommand();} else { Log.d(TAG, String.format("writing <%s> to characteristic <%s>", bytes2String(bytesToWrite), characteristic.getUuid())); nrTries++;}
Кроме самостоятельного чтения и записи характеристик, вы можете включить или отключить уведомления от устройств. При включении уведомления, устройство сообщит вам о появлении новых данных и отправит их автоматически.
Для включения уведомлений нужно сделать две вещи в Android:
вызватьsetCharacteristicNotification
. Bluetooth
стек будет ожидать уведомления для этой характеристики.
записать1или2какunsigned
int16
в дескриптор конфигурации характеристик (Client
Characteristic Configuration, сокращенно - ССС). Дескриптор CCC
имеет короткий UUID2902.
Почему1или2? Потому что под капотом Bluetooth стека есть Уведомление и Индикация. Полученное Уведомление не подтверждаются стеком Bluetooth, а Индикация наоборот подтверждается стеком. При использовании Индикации, устройство будет точно знать, что данные получены и может их, например, удалить из локального хранилища. С точки зрения Android приложения нет разницы: в обоих случаях вы просто получите массив байтов и Bluetooth стек уведомит устройство об этом, если вы используете Индикацию. Итак,1включает уведомления,2 индикацию. Чтобы выключить их, записываем0. Вы должны самостоятельно определить, что записать в дескриптор CCC.
В iOS методsetNotify()
делает всю работу за вас.
Ниже пример, как сделать тоже самое на Android, там сначала идут
проверки входных параметров, определяется что записать в дескриптор
и, наконец команда отправляется в очередь:
private final String CCC_DESCRIPTOR_UUID = "00002902-0000-1000-8000-00805f9b34fb";public boolean setNotify(BluetoothGattCharacteristic characteristic, final boolean enable) { // Check if characteristic is valid if(characteristic == null) { Log.e(TAG, "ERROR: Characteristic is 'null', ignoring setNotify request"); return false; } // Get the CCC Descriptor for the characteristic final BluetoothGattDescriptor descriptor = characteristic.getDescriptor(UUID.fromString(CCC_DESCRIPTOR_UUID)); if(descriptor == null) { Log.e(TAG, String.format("ERROR: Could not get CCC descriptor for characteristic %s", characteristic.getUuid())); return false; } // Check if characteristic has NOTIFY or INDICATE properties and set the correct byte value to be written byte[] value; int properties = characteristic.getProperties(); if ((properties & PROPERTY_NOTIFY) > 0) { value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE; } else if ((properties & PROPERTY_INDICATE) > 0) { value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE; } else { Log.e(TAG, String.format("ERROR: Characteristic %s does not have notify or indicate property", characteristic.getUuid())); return false; } final byte[] finalValue = enable ? value : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE; // Queue Runnable to turn on/off the notification now that all checks have been passed boolean result = commandQueue.add(new Runnable() { @Override public void run() { // First set notification for Gatt object if(!bluetoothGatt.setCharacteristicNotification(descriptor.getCharacteristic(), enable)) { Log.e(TAG, String.format("ERROR: setCharacteristicNotification failed for descriptor: %s", descriptor.getUuid())); } // Then write to descriptor descriptor.setValue(finalValue); boolean result; result = bluetoothGatt.writeDescriptor(descriptor); if(!result) { Log.e(TAG, String.format("ERROR: writeDescriptor failed for descriptor: %s", descriptor.getUuid())); completedCommand(); } else { nrTries++; } } }); if(result) { nextCommand(); } else { Log.e(TAG, "ERROR: Could not enqueue write command"); } return result;}
Результат записи в CCC дескриптор обрабатывается в
колбекеonDescriptorWrite
. Здесь вы должны отличить
запись в CCC от записей в другие дескрипторы. Во время обработки
колбека, мы также должны хранить, какие в данный момент
характеристики уведомляются.
@Overridepublic void onDescriptorWrite(BluetoothGatt gatt, final BluetoothGattDescriptor descriptor, final int status) { // Do some checks first final BluetoothGattCharacteristic parentCharacteristic = descriptor.getCharacteristic(); if(status!= GATT_SUCCESS) { Log.e(TAG, String.format("ERROR: Write descriptor failed value <%s>, device: %s, characteristic: %s", bytes2String(currentWriteBytes), getAddress(), parentCharacteristic.getUuid())); } // Check if this was the Client Configuration Descriptor if(descriptor.getUuid().equals(UUID.fromString(CCC_DESCRIPTOR_UUID))) { if(status==GATT_SUCCESS) { // Check if we were turning notify on or off byte[] value = descriptor.getValue(); if (value != null) { if (value[0] != 0) { // Notify set to on, add it to the set of notifying characteristics notifyingCharacteristics.add(parentCharacteristic.getUuid()); } } else { // Notify was turned off, so remove it from the set of notifying characteristics notifyingCharacteristics.remove(parentCharacteristic.getUuid()); } } } // This was a setNotify operation .... } else { // This was a normal descriptor write.... ... }); } completedCommand();}
Чтобы узнать из какой характеристики пришло уведомление
используйте методisNotifying()
:
public boolean isNotifying(BluetoothGattCharacteristic characteristic) { return notifyingCharacteristics.contains(characteristic.getUuid());}
К сожалению, нельзя включить столько уведомлений, сколько хочешь. Начиная с Android-5 лимит равен 15. В более старых версиях он был равен 7 или даже 4. Большинство смартфонов поддерживают 15 уведомлений. Не забывайте отключать их, если они вам больше не нужны, чтобы не исчерпать лимит.
Итак, мы научились читать/писать характеристики, включать/выключать уведомления, а значит готовы использовать это в реальном проекте. Я думаю, что устройства BLE можно разделить на две категории:
Простые устройства. Например, термометр, который использует официальный Bluetooth Health Thermometer сервис. Такие устройства легко использовать, вы просто включаете уведомления и данные начинают поступать. Здесь мы используем только операции чтения характеристики, запись не нужна;
Сложные устройства. Это может быть любое устройство, но обычно все они используют свой внутренний протокол обмена данными. Часто эти протоколы не спроектированы под BLE, а просто транслируют внутренний последовательный протокол в BLE, где одна характеристика используется для отправки данных, а другая для приема. Сложность в том, что вам требуется знать большое количество команд для работы с устройством: авторизация, обновление пользовательских параметров, параметров самого устройства, получение сохраненных данных и т.д.
Простые устройства обычно не создают проблем с потоками, для сложных следует работать внимательно. Чтение, запись и уведомления в этом случае будут чередоваться и могут мешать друг другу, особенно если у вас устройство с высокой частотой передачи данных (30Hz или около).
Типичная проблема с потоками выглядит так:
приходит уведомление
вы отправляете событие в свою собственную очередь для обработки
запускается обработку полученных данных
в это время приходит новое уведомление и перезаписывает
предыдущее значение вBluetoothGattCharacteristic
если ваша обработка данных медленная, вы потенциально теряете значение из первого уведомления.
Причины такого поведения:
как только сообщение доставлено, Android будет отправлять следующее(если оно есть). Посколько обработка данных отправляется в другой поток, текущий освобождается и Android продолжит доставку уведомлений;
Androidпереиспользует BluetoothGattCharacteristic
объекты внутри. Они создаются в время обнаружения сервисов
(services discovering) и после
этогопереспользуютсямногократно. Таким образом,
когда приходит уведомления Android сохраняет значение в
объектBluetoothGattCharacteristic
. Если характеристика
в этот момент обрабатывается в другом потоке мы получим гонку
состояний (race condition) и результат будет непредсказуемым.
Очевидно, чтонужно всегда работать с копией массива байтов. Получили данные, сразу же делаем копию и работаем с ней.
Ниже пример, который использует такую тактику:
@Overridepublic void onCharacteristicChanged(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) { // Copy the byte array so we have a threadsafe copy final byte[] value = new byte[characteristic.getValue().length]; System.arraycopy( characteristic.getValue(), 0, value, 0, characteristic.getValue().length); // Characteristic has new value so pass it on for processing bleHandler.post(new Runnable() { @Override public void run() { myProcessor.onCharacteristicUpdate(BluetoothPeripheral.this, value, characteristic); } });}
Есть несколько дополнительных рекомендаций по работе с BLE на Android. Поскольку стек BLE в основном асинхронный, у нас есть мульти-поточная обработка задач.
Android использует потоки:
При сканировании (результаты приходят
вmain
поток);
Вызове колбековBluetoothGattCallback
(выполняются в
потокахBinder
);
Обработка результатов сканирования на main потоке не будет
проблемой. Но с потоками Binder все немного сложнее. При вызове
колбека на потоке Binder, Android не будет отправлять новые данные
пока не закончится обработка текущих, то есть поток Binder
блокируется пока ваш код не завершится. Следует избегать тяжелых
операций в колбеках, никакихsleep()
или что-то
подобное. Кроме того, никаких новых вызовов в
объектеBluetoothGatt
, пока вы находитесь в потоке
Binder, хотя большинство методов асинхронные.
Я рекомендую следующее:
Всегда выполняйте вызовыBluetoothGattCallback
в
отдельном потоке, возможно даже из потока пользовательского
интерфейса (Прим. переводчика: работать на main потоке - плохая
идея, если у вас есть активный обмен с устройством, обязательно
будут залипания UI, не делайте так);
Освобождайте потокиBinder
как можно быстрее
иникогдане блокируйте их;
Самый простой способ выполнить рекомендации выше создать
выделенныйHandler
и использовать его для обработки
данных и выдачи новых команд. Обратите внимание, я уже
использовалHandler
на примере кода для
колбекаonCharacteristicUpdate
.
Объявление объекта:
Handler bleHandler = new Handler();
Если хотите
запуститьHandler
наmain
потоке:
Handler bleHandler = new Handler(Looper.getMainLooper());
Прокрутите назад и взгляните на наш
методnextCommand()
,
каждыйRunnable
выполняется в нашем
собственномHandler
, следовательно, мы гарантируем, что
все команды выполняются вне потокаBinder
.
В этой статье мы разобрались с чтением и записью характеристик, включением и выключением уведомлений/нотификаций. В следующей статье, мы детально изучим процесс спряжения с устройством (bonding).
Не терпится поработать с BLE? Попробуйтемою библиотеку Blessed for Android. Она использует все подходы из этой серии статей и упрощает работу с BLE в вашем приложении.
Содержание
Часть #2 (connecting/disconnecting)
Часть #4 (bonding), вы здесь
Впредыдущей статьемы разобрались с операциями чтения/записи, включения/выключения нотификаций и организации очереди команд. В этой статье мы поговорим осопряжении устройств(Прим. переводчика далее я буду использовать термин bonding).
Некоторые устройства для правильной работы требуют bonding. Технически это обозначает, что генерируются ключи шифрования, обмениваются и хранятся, для безопасного обмена данными. При запуске процедуры bonding, Android может запросить у пользователя согласие, пин-код или кодовую фразу. При следующих подключениях, Android уже знает, что устройство сопряжено и обмен ключами шифрования происходит скрытно без участия пользователя. Использование bonding делает подключение к устройству более безопасным, так как соединение зашифровано.
Тема bonding плохо описана в документации Google, полностью
непонятно, как приложение должно работать с bonding. Первое на что
вы обратите внимание это методcreateBond()
. Что
интересно, в iOS такого метода нет вообще и
фреймворкCoreBluetooth
делает все за вас! Тогда зачем
вызыватьcreateBond()
? Кажется немного странным, вам
заранее надо знать, какие устройства требуют bonding, а какие нет.
Протокол Bluetooth был спроектирован так, что обычно устройства
явно говорят им требуется или нет bonding. Я копнул немного глубже
и поэкспериментировал. Чтобы разобраться с этим, ушло некоторое
время, но в конце концов, все оказалось просто.
Принципы работы с bonding:
Пусть Android сам работает с bonding.Android
сделает bonding за вас, когда устройство скажет, что нужен bonding,
или во время операции чтения/записи зашифрованной характеристики. В
большинстве случаев не надо
вызыватьcreateBond()
самостоятельно (Прим.
переводчика: мне пришлось это делать самостоятельно, из-за
особенностей прошивки устройства. Кроме того, Samsung работает
по-другому, чем другие вендоры);
Нельзя запускать другие операции, в процессе работы bonding.Если вы будете запускать обнаружение сервисов или читать/писать характеристики, это приведет к ошибками и сбросу соединения. Просто дождитесь пока Android выполнит bonding;
Продолжайте очередь операций после завершения bonding.Как только операция bonding завершилась, продолжайте выполнение операций из очереди;
Если вы знаете, что делаете, и это необходимовы
можете вызватьcreateBond()
для запуска bonding с
устройством самостоятельно. Но это должно быть исключением.
Есть три причины, по которым запускается процесс bonding:
При соединении с устройством, оно сигнализирует, что требуется bonding, до любых других операций;
Характеристика может быть зашифрована для чтения или
записи.При попытке прочитать или записать такую
характеристику, запустится bonding. Если он пройдет удачно
чтение/запись также выполнится, в случае ошибки bonding
чтение/запись выполнится с
ошибкойINSUFFICIENT_AUTHENTICATION
. Такая же ошибка
есть в iOS.
Вызапускаете процесс bonding
самостоятельночерез вызовcreateBond()
. Если
этого требует ваше устройство, оно вероятно не будет совместимо с
iOS, так как там нет аналогичного метода. Но формально в протоколе
Bluetooth такое возможно.
Давайте обсудим каждый случай.
Если устройство требует bonding сразу после подключения, то при
вызове колбекаonConnectionStateChange
состояние bonding
будетBOND_BONDING
. Это означает что идет процесс
bonding ивы не должны ничего делать в этот момент,
например вызыватьdiscoverServices()
, до тех пор пока
процесс bonding не закончится! Иначе возможны неожиданные
дисконнекты или ошибки обнаружения сервисов. Поэтому следует
специально обрабатывать эту ситуацию
вonConnectionStateChanged
:
// Take action depending on the bond stateif(bondstate == BOND_NONE || bondstate == BOND_BONDED) { // Connected to device, now proceed to discover it's services ... } else if (bondstate == BOND_BONDING) { // Bonding process has already started let it complete Log.i(TAG, "waiting for bonding to complete");}
Чтобы следить, как идет процесс bonding, необходимо
зарегистрировать колбекBroadcastReceiver
для
интентаACTION_BOND_STATE_CHANGED
до
вызоваconnectGatt
. Этот колбек будет вызываться
несколько раз в процессе bonding.
context.registerReceiver(bondStateReceiver, new IntentFilter(ACTION_BOND_STATE_CHANGED));private final BroadcastReceiver bondStateReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); // Ignore updates for other devices if (bluetoothGatt == null || !device.getAddress().equals(bluetoothGatt.getDevice().getAddress())) return; // Check if action is valid if(action == null) return; // Take action depending on new bond state if (action.equals(ACTION_BOND_STATE_CHANGED)) { final int bondState = intent.getIntExtra(EXTRA_BOND_STATE, ERROR); final int previousBondState = intent.getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, -1); switch (bondState) { case BOND_BONDING: // Bonding started ... break; case BOND_BONDED: // Bonding succeeded ... break; case BOND_NONE: // Oh oh ... break; } } }};
После завершения bonding, мы запускаем обнаружение сервисов (service discovery), если они еще не обнаружены, это можно проверить:
case BOND_BONDED: // Bonding succeeded Log.d(TAG, "bonded"); // Check if there are services if(bluetoothGatt.getServices().isEmpty()) { // No services discovered yet bleHandler.post(new Runnable() { @Override public void run() { Log.d(TAG, String.format("discovering services of '%s'", getName())); boolean result = bluetoothGatt.discoverServices(); if (!result) { Log.e(TAG, "discoverServices failed to start"); } } }); }
Вот и все, что касается особенностей bonding при подключении.
Если bonding стартует при чтении/записи зашифрованной
характеристики, то самая первая операция чтения/записи окончится с
ошибкойGATT_INSUFFICIENT_AUTHENTICATION
. На версиях
Android-6, 7 вы получите эту ошибку
вonCharacteristicRead
/onCharacteristicWrite
,
при этом процесс bonding уже будет запущен внутри Android. С версии
Android-8 ошибки не будет и Android самостоятельно повторит
операцию после завершения bonding. Получается на Android-6, 7 надо
повторить операцию чтения/записи самостоятельно. Итак, вам надо
поймать ошибку и сделать повтор операции после bonding.
При получении такой ошибки, не продолжайте запуск операций:
public void onCharacteristicRead(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, int status) { // Perform some checks on the status field if (status != GATT_SUCCESS) { if (status == GATT_INSUFFICIENT_AUTHENTICATION ) { // Characteristic encrypted and needs bonding, // So retry operation after bonding completes // This only happens on Android 5/6/7 Log.w(TAG, "read needs bonding, bonding in progress"); return; } else { Log.e(TAG, String.format(Locale.ENGLISH,"ERROR: Read failed for characteristic: %s, status %d", characteristic.getUuid(), status)); completedCommand(); return; } }...
После bonding проверяем, есть ли операция в процессе выполнения и повторяем ее:
case BOND_BONDED: // Bonding succeeded Log.d(TAG, "bonded"); // Check if there are services ... // If bonding was triggered by a read/write, we must retry it if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { if (commandQueueBusy && !manuallyBonding) { bleHandler.postDelayed(new Runnable() { @Override public void run() { Log.d(TAG, "retrying command after bonding"); retryCommand(); } }, 50); } }
Как я говорил выше, лучше не
вызыватьcreateBond
самостоятельно, хотя сделать это,
конечно можно. Спросите себя, это действительно необходимо? На iOS
нет эквивалента методаcreateBond()
, если этот метод
единственный способ сделать bonding для вашего устройства, то
скорее всего оно несовместимо с iOS. Это прямо указывается в
документации iOS. Я перепробовал несколько десятков BLE устройств,
и только в единственном случае я
вызывалcreateBond()
самостоятельно из-за исключительных
обстоятельств.
При вызовеcreateBond
самостоятельно, также нельзя
ничего делать, пока bonding не завершится и требуется
регистрировать колбекBroadcastReceiver
для отслеживания
процесса. Если устройство уже сопряжено (bonding завершился),
тоcreateBond()
вызовет ошибку, надо проверить состояние
bonding перед вызовом.
Еще одна причина
запускатьcreateBond()
самостоятельно упростить
повторное подключение. ОбъектBluetoothDevice
можно
получить при помощи MAC-адреса, если устройство закешировано или
сопряжено (bonding). Таким образом вам не придется снова
сканировать устройство Может пригодиться! (Прим. переводчика: я
как раз работал с таким вариантом подключения, его требовалось
сделать полностью детерминированным, разбитым на подфазы, для
точного понимания что происходит).
Как пользователь Android, я могу увидеть список сопряженных устройств в Bluetooth настройках. Там можно удалить устройство, bonding также будет удален.
Требуется некоторое время на удаление устройства.
Достаточно странно, что нет официального способа удалить bonding
устройства программно. Это можно сделать, используя скрытый
методremoveBond()
, доступный через механизм рефлексии
в Java:
try { Method method = device.getClass().getMethod("removeBond", (Class[]) null); result = (boolean) method.invoke(device, (Object[]) null); if (result) { Log.i(TAG, "Successfully removed bond"); } return result;} catch (Exception e) { Log.e(TAG, "ERROR: could not remove bond"); e.printStackTrace(); return false;}
Большинство BLE устройств поддерживают bonding только с одним смартфоном. Типичный сценарий, когда мы теряем bonding такой:
Смартфон А делает bonding с устройством Х
Смартфон B делает bonding с устройством Х
Смартфон А переподключается к устройству Х, и теперь bonding потерян.
При реконнекте смартфон А получит состояние
bondingBOND_NONE
в
колбекеBroadcastReceiver
. Сравнивайте предыдущее
состояние bonding, чтобы понять была потеря или нет:
case BOND_NONE: if(previousBondState == BOND_BONDING) { // Bonding failed ... } else { // Bond lost ... } disconnect(); break;
Если случилась потеря bonding, отключаемся от устройства, иначе будут происходить странные вещи и соединение с устройством не будет нормально работать. Когда вы делаете реконнект, Android снова запускает процедуру bonding. Тоже самое происходит и при обрыве связи.
Существует мелкий баг, о котором следует знать. При потере bonding, кажется нужнаодна секундадля того, чтобы Bluetooth стек обновил свое внутреннее состояние. Если сделать реконнект сразу после потери bonding, Android может сказать, что устройство все еще сопряжено, но на самом деле это будет не так. Сделайте задержку в одну секунду перед переподключением.
Прим. переводчика: не нашел толковой замены слова pairing, спаривание - звучит неблагозвучно здесь.
Когда Android запускает процесс bonding, может появится всплывающее окно. Я говорю может, потому что некоторые вендоры используют свою логику показа этого попапа (Прим. переводчика: на моем Samsung-S9, после обновления до Android-10, это попап стал появляться всегда, при коннекте любого нового устройства, до этого обновления, такого не было). На смартфонах Google (или других вендоров, где код Android в этой части не изменялся), всплывающий попап появляется только при определенный условиях.
Pairing попап появляется на переднем фоне если:
Устройство недавно было в режиме обнаружения;
Устройство было обнаружено недавно;
Устройство недавно было выбрано в сборщике устройств;
Экран настроек Bluetooth виден.
Значение недавно означаетв течение последних 60 секунд. Условия выглядят непонятными, поэтому лучше посмотреть наисходный код. Если все эти условия не выполняются, то вместо попапа появится уведомление, которое большинство пользователей не замечает. Но если они заметят и нажмут на него, всплывающее окно сбивает с толку своей опцией доступа к контактам. Ужасный UI по-моему! Некоторые производители (справедливо) решили исправить такое поведение! На устройствах Samsung всплывающее окно-подтверждение (для подключений в режиме JustWorks) вообще не отображается, а всплывающие окна всегда появляются на переднем плане. При этом всплывающее окно открывается только при вводе PIN-кода или кодовой фразы. Никаких доступов к контактам и всегда передний план. Так намного лучше!
Так что, если вдруг вы захотите, чтобы всплывающее окно всегда отображалось на переднем плане, запускайте обнаружение на одну секунду перед подключением к устройству. Выглядит как хак, но это работает. Код ниже:
public void startPairingPopupHack() { String manufacturer = Build.MANUFACTURER; if(!manufacturer.equals("samsung")) { bluetoothAdapter.startDiscovery(); callBackHandler.postDelayed(new Runnable() { @Override public void run() { Log.d(TAG, "popup hack completed"); bluetoothAdapter.cancelDiscovery(); } }, 1000); }}
Важный момент здесь вы не должны запускать никакие BLE операции пока попап на экране. Подождите ответа от пользователя.
Если учтете все эти моменты, bonding будет работать как часики!
На этом мы завершаем цикл статей о BLE в Android (Прим. переводчика: я готовлю отдельную статью-заключение, где опишу свои подходы к работе с BLE устройствами на Android, небольшие ньюансы и решения для стабильной продолжительной работы с устройствами). Надеюсь эта информация будет полезной вам и сделает работу с BLE комфортнее. Чем больше знаешь про BLE, тем лучше работает ваше приложение. Успехов!
Не терпится поработать с BLE? Попробуйтемою библиотеку Blessed for Android. Она использует все подходы из этой серии статей и упрощает работу с BLE в вашем приложении.
Около года назад я приобрел данный девайс для контроля частоты сердечных сокращений (далее - ЧСС) во время тренировок. Датчик отлично подсоединяется к телефону, умным часам по Bluetooth, но обычно, фитнесс-приложения, анализирующие подобного рода данные требуют либо подписки, либо нагружены излишне сложными аналитиками, которые мне, как рядовому пользователю не очень интересны. Поэтому у меня родилась идея написать свое приложение для контроля ЧСС во время тренировок для IOS на Swift.
Bluetooth Low Energy - очень популярный и распространённый протокол обмена данными, который мы используем повсеместно и который становится все популярнее с каждым днем. У меня даже чайник на кухне управляется дистанционно через BLE. Low energy, кстати, гораздо сниженное энергопотребление в отличие от "голого" Bluetooth, настолько сниженное, что устройство готово общаться по данному протоколу на одной батарейке несколько месяцев, а то и лет.
Конечно, цитировать и переписывать спецификацию протокола BLE 5.2 нет никакого смысла, поэтому ограничимся основными понятиями.
В зависимости от использования и назначения, устройство Bluetooth может быть:
Центральным (главным) - получает данные от периферийного устройства (наш телефон)
Периферийным - устройство, которое отправляет данные на центральное устройство (датчик ЧСС)
Рекламные или оповещательные данные отправляются с периферийного устройства в виде пакетов, которые содержат в себе основную информацию об устройстве: его название, а также его функциональные возможности. Задача центрального устройства, получить их, прочитать, а после выделить из списка периферии необходимый для подключения девайс.
Объем рекламных пакетов не очень большой и вместить всю информацию об устройстве не способен. Чтобы получить доступ ко всем возможностям и характеристикам устройства, необходимо выполнить подключение, после чего считать его данные, которые, в свою очередь, могут быть предоставлены в виде:
Сервиса (услуг) - набор данных, описывающих функции устройства. В нашем случае мы увидим службу получения ЧСС.
Характеристик - дополнительных описаний сервисов устройства. Например характеристика изменения сердечного ритма в секунду, а также положения датчика на теле.
Переходя к абстракциям, сервисом является некий шкаф, в котором много ящиков - характеристик. Причем каждый сервис уникален и представлен идентификатором UUID, который может быть 16-битным или 128-битным, в зависимости от типов сервисов.
Создадим проект в Xcode с одноимённым названием, после чего добавим несколько необходимых Label в Main.storyboard и перетянем outlets этих labels во View Controller, закрепим их с помощью constraints, а также скроем их для первоначального изображения в методе viewDidLoad, как я сделал это на изображении:
Я создал outlets для текстовых значений "121" и "грудь", другие же текстовые значения просто закрепил на view, так как изменений в них делать мы не планируем.
Отладку и демонстрацию работы необходимо совершать на реальном устройстве, так как симулятор не поддерживает возможность работы по протоколу Bluetooth.
В файле Info.plist проекта необходимо добавить свойство: Bluetooth Always Usage Description и прикрепить к нему описание, чтобы уведомить пользователя об использовании данных по Bluetooth при первом запуске приложения. Если данное свойство не добавить в список, то приложение "упадет" с одноименной ошибкой. Не забывайте про это!
Тут все просто, для подключения библиотеки воспользуемся следующей строчкой:
import CoreBluetooth
Вспомним, что по протоколу у нас существуют так называемые центральные и периферийные устройства, логично предположить, что основной функционал работы данного протокола будет исполнен методами делегатов централи и периферии.
Для начала создадим переменную центрального (главного) устройства в проекте рядом с объявлением других переменных:
var centralManager: CBCentralManager!
Теперь, чтобы получить доступ к методам необходимо назначить ViewController делегатом, но предварительно подпишем его под протокол CBCentralManagerDelegate. Сделать это предлагаю в extension ViewController, так будет рациональнее.
extension ViewController: CBCentralManagerDelegate {}
Xcode на такое пользовательское действие отреагирует ошибкой: "Type 'ViewController' does not conform to protocol 'CBCentralManagerDelegate'", оповещая, что данный протокол требует обязательную реализацию метода: "func centralManagerDidUpdateState(_ central: CBCentralManager)". Нажмем "fix", добавив этот метод в проект. Данный метод нужен для автоматической проверки состояния центрального менеджера, которого мы создали ранее.
Чтобы отобразить все состояния центрального менеджера, в теле метода "func centralManagerDidUpdateState(_ central: CBCentralManager)" напишем:
func centralManagerDidUpdateState(_ central: CBCentralManager) { switch central.state { }
Xcode автоматически предложит вставить все возможные состояния данного условия, соглашаемся с ним. А в каждом из состояний напишем функцию print("это состояние"):
extension ViewController: CBCentralManagerDelegate { func centralManagerDidUpdateState(_ central: CBCentralManager) { switch central.state { case .unknown: print ("central.state is unknown") case .resetting: print ("central.state is resetting") case .unsupported: print ("central.state is unsupported") case .unauthorized: print ("central.state is unauthorized") case .poweredOff: print ("central.state is poweredOff") case .poweredOn: print ("central.state is poweredOn") @unknown default: break } }}
Теперь нам осталось проинициализировать переменную "centralManager" и задать ей делегирование. Сделаем это в методе "viewDidLoad", а в качестве параметра очереди напишем "nil", определяя всю работу про Bluetooth в главной очереди.
override func viewDidLoad() { super.viewDidLoad() centralManager = CBCentralManager(delegate: self, queue: nil) heartRateLabel.isHidden = true bodyLocationLabel.isHidden = true }
Собираем проект, запускаем на устройстве с включенным Bluetooth, видим системный запрос за его использование, соглашаемся и получаем в консоль заветное сообщение "central.state is poweredOn", которое сигнализирует нам о том, что центральный менеджер готов к работе. Если выключить Bluetooth на телефоне, то в консоли появится логичное "central.state is poweredOff".
Центральный менеджер ждет дальнейших указаний, и сейчас он их получит. Для этого в методе "centralManagerDidUpdateState" в случае ".poweredOn" после метода "print" пишем:
centralManager.scanForPeripherals(withServices: nil)
Менеджер начнет сканировать все доступные вокруг устройства, а чтобы мы смогли увидеть их в консоли приложения, необходимо реализовать метод делегата в extension ViewController ниже метода "centralManagerDidUpdateState" следующим образом:
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { print(peripheral) }
Запускаем приложение... И теперь в консоли мы можем увидеть множество всех доступных для подключения устройств. Замечательно! Одним из них и является необходимый для подключения пульсометр. Но чтобы упростить поиск пульсометра, можно воспользоваться некоторой хитростью, которую я сейчас покажу.
Я ранее упомянул наличие данного идентификатора в протоколе Bluetooth как уникальную характеристику для различных устройств, поэтому могу сказать вам, что пульсометры обладают таким уникальным UUID для своей непосредственной службы измерения ЧСС. Список всех UUID можно также найти в спецификации, из которой я нашел нужный: "0x180D". Добавим новую константу в проект над объявленными ранее outlets:
let heartRateUUID = CBUUID(string: "0x180D")
Также обновим метод "centralManager.scanForPeripherals(withServices: nil)" добавив в него вышенаписанный идентификатор пульсометра:
case .poweredOn: print ("central.state is poweredOn") centralManager.scanForPeripherals(withServices: [heartRateUUID] )
Теперь центральный менеджер находится в поиске устройств с данным UUID, и после некоторого времени в консоли появиться заветное устройство:
<CBPeripheral: 0x280214000, identifier = D5A5CD3E-33AC-7245-4294-4FFB9B986DFC, name = COOSPO H6 0062870, state = disconnected>
Теперь необходимо создать переменную в проекте, с которой мы сможем связать данное устройство, для этого рядом с "var centralManager: CBCentralManager!" напишем:
var heartRatePeripheral: CBPeripheral!
А в методе "didDiscover peripheral" свяжем найденное устройство с вышеобъявленной переменной и прекратим поиск новых устройств с помощью метода:
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { print(peripheral) heartRatePeripheral = peripheral centralManager.stopScan() }
Для этого напишем под строкой "centralManager.stopScan()":
centralManager.connect(heartRatePeripheral, options: nil)
Нам уже удалось подключиться к пульсометру, но чтобы это действительно увидеть, необходимо реализовать еще один метод делегата "didConnect peripheral" ниже метода "didDiscover peripheral", который автоматически вызывается при подключении нового устройства:
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { print("Соединение установлено") }
Собираем проект, запускаем на устройстве и видим в консоле заветное "Соединение установлено". Хороший результат, теперь двигаемся дальше.
После того, как соединение установлено, необходимо сделать запрос об услугах (сервисах), которые данный пульсометр готов предоставить. Для этого после установки соединения вызовем метод "heartRatePeripheral.discoverServices()" в методе "didConnect", который примет следующий вид:
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { print("Соединение установлено") heartRatePeripheral.discoverServices(nil) }
Запрос на получение сервисов сделан, а чтобы их увидеть и начать с ними работать, необходимо расширить класс протоколом "CBPeripheralDelegate" в самом низу нашего проекта и вызвать метод "peripheral(_:didDiscoverServices:)" следующим образом:
extension ViewController: CBPeripheralDelegate { func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { guard let services = peripheral.services else { return } for service in services { print(service) } }}
Метод получает сервисы, сообщает об этом центральному менеджеру и выводит их в консоль. В данный момент консоль будет пуста, так как необходимо делегировать данный протокол периферийному устройству "heartRatePeripheral". Сделаем это после инициализации периферийного устройства в следующем методе:
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { print(peripheral) heartRatePeripheral = peripheral heartRatePeripheral.delegate = self centralManager.stopScan() centralManager.connect(heartRatePeripheral, options: nil) }
Отлично, делегат обьявлен, метод получения сервисов написан, запустим программу на телефоне и получим в консоль список служб пульсометра:
<CBService: 0x2824b4340, isPrimary = YES, UUID = Heart Rate>
<CBService: 0x2824b4240, isPrimary = YES, UUID = Battery>
<CBService: 0x2824b4280, isPrimary = YES, UUID = Device Information>
<CBService: 0x2824b4200, isPrimary = YES, UUID = 8FC3FD00-F21D-11E3-976C-0002A5D5C51B>
Не все сервисы нам интересны и оставить необходимо лишь первый. Для этого можно провести так называемую фильтрацию с помощью идентификатора UUID в методе "heartRatePeripheral.discoverServices()"
heartRatePeripheral.discoverServices([heartRateUUID])
Вот теперь список служб отобразится в виде "<CBService: 0x2824b4340, isPrimary = YES, UUID = Heart Rate>", из которой мы сможем извлечь нужные нам характеристики - ящики ( шкафа мы уже получили).
Шкаф-сервис нам известен, осталось посмотреть, что он предлагает и получить это. Сделаем запрос на получение характеристик, для этого в теле метода "didDiscoverServices - peripheral" реализуем метод - поиск:
extension ViewController: CBPeripheralDelegate { func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { guard let services = peripheral.services else { return } for service in services { peripheral.discoverCharacteristics(nil, for: service) } }}
Теперь доступный сервис будет посылать свои характеристики, а увидеть мы их сможем в самостоятельном методе делегата "CBPeripheralDelegate" под названием "didDiscoverCharacteristicsFor". Реализуем его и выведем в консоль все доступные характеристики:
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { guard let characteristics = service.characteristics else { return } for characteristic in characteristics { print(characteristic) } }
Запускаем программу, видим, что характеристики получены, а консоль заполнилась следующими строками:
<CBCharacteristic: 0x28024c120, UUID = 2A37, properties = 0x10, value = {length = 2, bytes = 0x0469}, notifying = NO>
<CBCharacteristic: 0x28024c180, UUID = 2A38, properties = 0x2, value = {length = 1, bytes = 0x01}, notifying = NO>
Видно, что у данной службы две характеристики, имеющие два уникальных идентификатора. Из спецификации на Bluetooth узнаем, что UUID = 2A37 отвечает за измерение ЧСС, а UUID = 2A38 за положение датчика на теле. Положение датчика на теле не самая интересная характеристика в данной теме, но будет полезно считать и ее.
Для удобства добавим в проект два уникальных идентификатора данных характеристик следующим образом:
let heartRateUUID = CBUUID(string: "0x180D") let heartRateCharacteristicCBUUID = CBUUID(string: "2A37") let bodyLocationCharacteristicCBUUID = CBUUID(string: "2A38")
Характеристики отличаются друг от друга типами свойств. Например, характеристика ЧСС имеет свойство ".notify" т.е. она уведомляет об изменении значения ЧСС, а характеристика положения на теле имеет свойство ".read", т.е. может быть считана напрямую. Данное пояснение необходимо, чтобы правильно получить значения из них.
Характеристика выведена консоль, теперь нужно лишь реализовать метода считывая значений из нее. Для этого напишем запрос на чтение значений "peripheral.readValue(for: characteristic)"
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { guard let characteristics = service.characteristics else { return } for characteristic in characteristics { peripheral.readValue(for: characteristic) } }
Запрос написан, как вы догадываетесь, нужно реализовать еще один метод "peripheral(_:didUpdateValueFor:error:)" делегата "CBPeripheralDelegate", который будет в асинхронном режиме получать ответ с данного запроса, причем в данном методе напишем конструкцию "switch - case", чтобы была возможность разделить характеристики по уникальному идентификатору:
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { switch characteristic.uuid { case bodySensorLocationCharacteristicCBUUID: print(characteristic.value ?? "no value") default: print("Unhandled Characteristic UUID: \(characteristic.uuid)") }}
В консоли после выполнения данной программы появится строка "1 bytes". Это нужный результат, потому что мы пытались вывести объект типа "data".
Чтобы "распасить" данный байт, необходимо снова прибегнуть к спецификации, из которой мы сможем понять, как данный байт заполнен. Ускоряя результат, реализуем ниже функцию получения строки положения на теле датчика ЧСС из этого байта:
private func bodyLocation(from characteristic: CBCharacteristic) -> String { guard let characteristicData = characteristic.value, let byte = characteristicData.first else { return "Error" } switch byte { case 0: return "Другое" case 1: return "Грудь" case 2: return "Запястье" case 3: return "Палец" case 4: return "Ладонь" case 5: return "Мочка уха" case 6: return "Нога" default: return "Резерв" } }
И теперь вызовем данную функцию в методе "didUpdateValueFor characteristic", одновременно выводя результат на экран телефона (не забудем показать скрытый label для положения датчика):
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { switch characteristic.uuid { case bodyLocationCharacteristicCBUUID: let bodySensorLocation = bodyLocation(from: characteristic) bodyLocationLabel.text = bodySensorLocation bodyLocationLabel.isHidden = false default: print("Unhandled Characteristic UUID: \(characteristic.uuid)") } }
Ура! Характеристика успешно получена, прочитана и выведена на экран!
Не совсем ясно, где еще можно носить данный пульсометр, поэтому существует данная характеристика :)
Осталось совсем немного, и теперь нужно получить значения из характеристики ЧСС. Как мы помним, у нее тип значения ".notify", поэтому нам нужно как бы "подписаться на нее", чтобы она присылала обновленные значения ЧСС. Для этого нужно выполнить метод "peripheral.setNotifyValue(true, for: characteristic)" в функции "didDiscoverCharacteristicsFor service:
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { guard let characteristics = service.characteristics else { return } for characteristic in characteristics { peripheral.readValue(for: characteristic) peripheral.setNotifyValue(true, for: characteristic) } }
Если запустить приложение, то в консоли появятся стоки:
Unhandled Characteristic UUID: 2A37
Unhandled Characteristic UUID: 2A37
Unhandled Characteristic UUID: 2A37
Именно в этой характеристики и лежат данные о ЧСС. Теперь необходимо провернуть такую же развертку этих данных, обращаясь к спецификации. В некоторых моделях данные могут быть представлены либо 1 либо 2 байтами. Чтобы не получить конфуз, реализуем метод для "парсинга" этих данных в нужном порядке в протоколе "CBPeripheralDelegate".
private func heartRate(from characteristic: CBCharacteristic) -> Int { guard let characteristicData = characteristic.value else { return -1 } let byteArray = [UInt8](characteristicData) let firstBitValue = byteArray[0] & 0x01 if firstBitValue == 0 { return Int(byteArray[1]) } else { return (Int(byteArray[1]) << 8) + Int(byteArray[2]) } }
И, наконец, добавим еще один case в методе "peripheral(_:didUpdateValueFor:error:)", в котором получим ЧСС, а также обновим и покажем label пользовательского интерфейса:
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { switch characteristic.uuid { case bodyLocationCharacteristicCBUUID: let bodySensorLocation = bodyLocation(from: characteristic) bodyLocationLabel.text = bodySensorLocation bodyLocationLabel.isHidden = false case heartRateCharacteristicCBUUID: let bpm = heartRate(from: characteristic) heartRateLabel.text = String(bpm) heartRateLabel.isHidden = false default: print("Unhandled Characteristic UUID: \(characteristic.uuid)") } }
Поздравляю!
Теперь данные с пульсометра выводятся на экран телефона. Я даже слегка нервничаю :)
В целом гайд по использованию Bluetooth для подключения датчика ЧСС вышел немного большим и местами сложным, надеюсь, что основной смысл мне удалось донести. Конечно, есть еще несколько нереализованных методов, которые можно было бы добавить (например, метод переподключения при обрыве соединения), но я посчитал этого набора достаточным, чтобы в меру оценить лаконичность и удобность библиотеки на swift CoreBluetooth.
Всем успехов и спасибо!
Стандарт Bluetooth 5.0 вышел в 2016 году, 2019-м появилась
версия
5.2. За последнее время Apple провела две конференции
WWDC 2017,
WWDC 2019 посвященных CoreBluetooth. Активно развивается
технология построения mesh сетей. Все стало еще лучше,
быстрее и эффективнее. Интерес к этому направлению только растет.
Выстроены целые системы управления на этой технологии.
Мы же задались целью автоматизировать рутинные операции и повысить
безопасность доступа пользователей на свое рабочее место. В статье
разберем, что было решено предложить пользователям, поговорим
немного о технологии BLE (хотя, как тут кратко?) на примере
небольшого проекта, который запускается на двух смартфонах и
позволяет передавать данные в обе стороны, ну а в конце познакомлю
с нашим приложением GM MOBILE ASSISTANT.
Я работаю swift-разработчиком в компании Гетмобит. Мы создаем ИТ-инфраструктуру а-персональных рабочих мест, которая позволит изменить традиционное восприятие рабочей среды, станет более гибкой и независимой от локации, где много внимания уделяется вопросам обеспечения безопасности. Наша экосистема называется GM SMART SYSTEM, ее ключевой элемент устройство GM-Box, сочетающее тонкий клиент и VOIP телефонию. Более подробно, с нашей инфраструктурой, можно ознакомиться в статьях моего коллеги.
Не секрет, что во многих компаниях политика корпоративной
безопасности запрещает использование коротких, читабельных паролей.
Обязательна определенная длина, спецсимволы, использование
заглавного регистра. Все эти звездочки, решетки, подчеркивания
приходится вводить при каждом входе. Когда такие пароли начинают
записывать и хранить на бумажке рядом с рабочим местом, такая
безопасность явно рискует выйти боком В некоторых компаниях
практикуется использование USB-токенов, а к экзотике можно отнести
NFC/RFID смарт-карты. Такие сложные решения требует затрат,
внедрения и техподдержки. При этом, токен или карту вполне могут
украсть, получив доступ к чувствительным данным.
Мы решили избавиться от этих проблем, сделав доступной авторизацию
через мобильный телефон. Решение основано на том, что пользователь
однажды ввел свои учетные данные в приложении на смартфоне и может
забыть про ручной ввод. Мобильный телефон есть у каждого
пользователя, приложение доступно в AppStore и Google Play и
является частью GM SMART SYSTEM, т.е. работает из коробки, не
требует затрат на поддержку. Данные хранятся в защищенной области,
доступ к приложению закрыт пин-кодом или биометрией, передаваемые
данные шифруются. Мы понимаем, что любую систему можно взломать, но
всегда встает вопрос целесообразности.
Определив смартфон, как средство для авторизации в нашей инфраструктуре, встал вопрос выбора кроссплатформенного решения iOS/Android. Не во всех компаниях разрешено использование WI-FI, USB API не доступно для iOS без дополнительной головной боли, Bluetooth порезан до BLE опять же в iOS. Была даже идея использовать звук но это отдельная история. По итогу, из всего того, что можно, наиболее практичным показался BLE. Пошли изучать матчасть.
После изучения довольно большого количества информации, пришли к
выводу, что важно хорошо понимать базовые принципы взаимодействия
BLE устройств. Поэтому постарался максимально точно отобразить их
на схеме ниже, она применима для любой платформы.
Технология BLE реализует клиент-серверную модель, является
самостоятельным сегментом Bluetooth, рассчитана на низкое
энергопотребление и относительно небольшую пропускную способность
передачи данных. Вполне достаточно для обмена небольшими
пакетами.
На схеме пунктиром выделены несколько Peripheral (GATT server),
которые ожидают входящие подключения и что бы заявить о себе в
радиоэфире, после инициализации, Peripheral, запускает процесс
вещания Advertising пакетов с заданной частотой, чем чаще, тем выше
вероятность обнаружения. Central (GATT client) запускает процесс
поиска (как правило, запускается на несколько секунд) и пытается
найти все устройства в радиусе 10-1500 (Bluetooth 5) метров.
Дистанция, конечно, сильно зависит от преград и помех. После
обнаружения получаем возможность подключиться, прочитать профиль
устройства, подписаться на характеристики (тип NOTIFY), передать
данные (WRITE). Любой атрибут имеет уникальный uuid
идентификатор.
GATT (Generic
Attribute Profile) профиль является общей спецификацией для
отправки и получения коротких фрагментов данных, строится на основе
протокола атрибутов АТТ.
Intro to Bluetooth Generic Attribute Profile.
ATT (Attribute Protocol) протокол атрибутов,
оптимизированный для работы на BLE-устройствах. Использует
настолько мало байт, насколько это возможно. Каждый атрибут
идентифицируется уникальным универсальным идентификатором UUID.
Service совокупность характеристик.
Characteristic по сути, это канал для передачи данных,
ограниченного заданной функциональностью:
Чтобы лучше понять приведенную выше схему, написал небольшое приложение. В проекте используется Core Bluetooth фреймворк. Запускать нужно на 2-х телефонах. На первом выбираем GATT сервер, на втором GATT клиент. После подключения можно пересылать текстовые сообщения. Ниже, разберем по порядку ключевые моменты создания профилей.
После создания нового проекта в Xcode добавляем ключ в
info.plist
<key>NSBluetoothAlwaysUsageDescription</key><string> Описание для чего будет использоваться bluetooth в приложении</string>
Важным моментом, особенно для профиля peripheral, является работа в фоновом режиме, если его не включить, то приложение не будет отправлять Advertising пакеты при сворачивании или выключенном экране. Для активации нужно добавить Background Modes и выбрать два пункта, так как мы хотим работать с обоими профилями.
Для управления профилем нам потребуются два класса: CBPeripheralManager, CBPeripheralManagerDelegate. Атрибуты задаются с помощью: CBMutableService, CBMutableCharacteristic, CBMutableDescriptor. Причем созданные атрибуты вкладываются один в другой по цепочке дескриптор, характеристика, сервис.
В начале создадим UUID для каждого атрибута планируемого профиля.
static let primaryServiceUUID =CBUUID(string: "bf52e2d6-ff52-43cb-99a0-872fa3dde94f")static let readCharacteristicUUID =CBUUID(string: "f438775d-e605-42b8-abe7-6e31ed52ea87")static let transferCharacteristicUUID =CBUUID(string: "a82ae020-e171-42f8-a31c-5f612926f041")
Затем нужно создать две характеристики. Первая рассчитана только на передачу некоторой предварительно подготовленной информации. Central сможет считать данные после подключения.
var readCharacteristic =CBMutableCharacteristic(type: UUIDs.readCharacteristicUUID,properties:[.read],value:"some identificator".data(using: .utf8),permissions:[.readable])
Вторая будет выполнять роль канала передачи данных. Свойство .notify ставиться, чтобы можно было отправлять уведомления, но для получения Central необходимо подписаться на них. Второе свойство .writeWithoutResponse означает, что запись в характеристику осуществляется без подтверждения.
var transferCharacteristic =CBMutableCharacteristic(type: UUIDs.transferCharacteristicUUID,properties:[.notify, .writeWithoutResponse],value:nil,permissions:[.readable, .writeable])
Теперь создаем сервис. Ставим primary=true, чтобы он был добавлен в перечень сервисов в advertising пакете.
var primaryService = CBMutableService(type:UUIDs.primaryServiceUUID, primary:true)
Добавляем характеристики.
primaryService.characteristics = [readCharacteristic, transferCharacteristic]
Созданные атрибуты передаем в CBPeripheralManager.
manager = CBPeripheralManager(delegate: self, queue: nil)for service in gattProfile.services {manager.add(service)}
При создании CBPeripheralManager необходимо указать делегат CBPeripheralManagerDelegate. Если после инициализации peripheral manager функция
(void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral
возвращает состояние peripheral.state = .poweredOn,
значит manager готов к работе.
GATT профиль заполнен. Осталось собрать Advertising пакет и можно
запускать передачу в радиоэфир. Структура пакета это массив
[String:Any] значений. Основной параметр, который нам нужно задать
идентификатор primary сервиса.
struct Advertisment {var value = [CBAdvertisementDataServiceUUIDsKey:[UUIDs.primaryServiceUUID]]}
Можно было бы еще добавить параметр CBAdvertisementDataLocalNameKey, но в данном случае это не имеет значения. Наши данные для advertising пакета объединяются с advertising данными смартфона. Этот параметр перезаписывается и как правило localName мы видим как iPhone 7.
manager.startAdvertising(Advertisment().value)
Для реализации клиентского профиля нам потребуются классы:
CBCentralManager, CBCentralManagerDelegate, CBPeripheralDelegate.
Последний нужен для работы с найденным GATT сервером, т.к. получаем
экземпляр CBPeripheral.
Для того, чтобы найти сервер нам нужно создать экземпляр менеджера
CBCentralManager и запустить поиск.
var manager = CBCentralManager(delegate: self, queue: nil)manager.scanForPeripherals(withServices: [UUIDs.primaryServiceUUID],options: managerOptions)
В качестве параметра withServices: передаем массив uuids по этим идентификаторам будет осуществляться автоматическая фильтрация, будут возвращены только те peripherals, идентификаторы которых заданы в массиве. Передавая nil можно обнаружить все активные BLE устройства поблизости. В нашем случае был создан только один сервис, его uuid и передаем в качестве параметра. Параметр options: не обязателен, я передаю туда:
private let managerOptions = [CBCentralManagerScanOptionAllowDuplicatesKey:false,CBCentralManagerOptionShowPowerAlertKey:false]
Первый параметр означает, что за время поиска мне вернется только первый обнаруженный Advertising пакет от конкретного устройства, остальные будут игнорироваться. Второй параметр означает, что выключаю системный диалог с предупреждением о состоянии bluetooth. Состояние будет приходить, а вот диалог не отобразится.
Если устройство с указанным uuid сервисом было обнаружено, то метод didDiscoverPeripheral:
- (void)centralManager:(CBCentralManager *)centraldidDiscoverPeripheral:(CBPeripheral *)peripheraladvertisementData:(NSDictionary<NSString *, id> *)advertisementDataRSSI:(NSNumber *)RSSI;
вернет объект (CBPeripheral *)peripheral. Обязательно нужно сохранить на него ссылку.
var device:CBPeripheral = peripheral
Итак, после обнаружения устройства, мы можем подключиться к нему.
manager.connect(device, options: nil)
Подключением и отключением управляет CBCentralManager. Отключиться можно командой:
manager.cancelPeripheralConnection(cancelingDevice)
После успешного подключения делаем запрос на сервисы:
device?.discoverServices(nil)
В качестве параметра можем передать массив uuids сервисов, которые хотели бы прочитать. В данном случае стоит nil значит peripheral вернет все имеющиеся.
Чтение характеристик происходит схожим образом, только отправить запрос нужно индивидуального для каждого сервиса. Метод didDiscoverServices возвращает массив найденных сервисов и дальше, выбрав нужный делаем запрос на характеристики. Я не стал запрашивать для всех, т.к. у iPhone параллельно запущено еще несколько служебных, для понимания процесса это только запутает.
guard let services = device?.services else { return }let filterServices = services.filter { (service) -> Bool inservice.uuid == UUIDs.primaryServiceUUID}for service in filterServices {device?.discoverCharacteristics(nil, for: service)}
Найденные характеристики возвращает метод didDiscoverCharacteristicsFor service:, т.е. последовательно для каждого сервиса. Необходимо подписаться на характеристику c идентификатором transferCharacteristicUUID.
if (characteristic.uuid == UUIDs.transferCharacteristicUUID) {device?.setNotifyValue(true, for: characteristic)}
Теперь можно передавать данные между созданными профилями GATT клиент/сервер.
Клиент отправляет данные таким образом:
device?.writeValue(text.data(using: .utf8)!,for: transferCharacteristic,type: .withoutResponse)
Принимает методом делегата CBPeripheralDelegate:
- (void)peripheral:(CBPeripheral *)peripheraldidUpdateValueForCharacteristic:(CBCharacteristic *)characteristicerror:(nullable NSError *)error;
Подробнее можно посмотреть в коде репозитория, ссылка на проект в начале раздела.
let mtu = device?.maximumWriteValueLength (for: .withoutResponse)
let mtu = connectedCentral?.maximumUpdateValueLength
- (void)peripheralManager:(CBPeripheralManager *)peripheralcentral:(CBCentral *)centraldidSubscribeToCharacteristic:(CBCharacteristic *)characteristic;
func updateValue(_ value: Data,for characteristic: CBMutableCharacteristic,onSubscribedCentrals centrals: [CBCentral]?) -> Bool
- (void)peripheralManagerIsReadyToUpdateSubscribers:(CBPeripheralManager *)peripheral;
Изучив основные принципы коммуникаций между BLE устройствами мы,
методом проб и ошибок, постарались разработать удобный интерфейс, в
котором максимально сократили действия со стороны пользователя для
входа в свое рабочее окружение. Теперь один раз требуется ввести
учетные данные в приложение, а для последующих подключений только
идентификатор GM-Box. Всю остальную работу по подключению, проверке
и безопасному хранению учетных данных, приложение берет на себя. В
результате, подходя к рабочему месту, пользователю достаточно
нажать одну кнопку и можно приступить к работе.
Теперь предлагаю пройтись по основным возможностям нашего
приложения.
Пару слов для понимания, что такое идентификатор GM-Box. Что бы
как-то отличать одно устройство от другого мы придумали уникальный
четырехзначный номер, который отображается на мониторе GM-Box. Для
того, чтобы авторизоваться, пользователю необходимо ввести этот
номер в приложении. После однократного ввода он сохраняется и
отображается на кнопке.
Сразу после запуска приложения начинается фоновый поиск устройств,
составляется реестр найденных. Операция длится около четырех секунд
и проходит незаметно для пользователя.
Экран, который отображается ниже, пользователь увидит только в том
случае, если за время поиска были найдены не все устройства или
введен несуществующий идентификатор, приложение о нем ничего не
знает, поэтому попробует найти.
manager.scanForPeripherals(withServices: [UUIDs.primaryServiceUUID],options: managerOptions)
Если приложение только установлено, то после ввода
идентификатора GM-Box, необходимо ввести свои учетные данные. С
этого же экрана происходит переподключение, если данные были
введены некорректно и проверка прошла с ошибкой.
В любой момент пользователь может завершить сессию. Стандартный
способ вызвать диалог и подтвердить завершение.
Функционал не требует защищать доступ к приложению в обязательном порядке. По желанию, пользователь может сам установить ПИН-код, либо выбрать вход используя биометрию. После 10-ти попыток неверно введенного значения ПИН-кода, учетные данные стираются.
Подробнее с функционалом приложения можно ознакомиться на
AppStore или
Google Play.
Дальнейшее развитие приложения будет связано с добавлением новых
возможностей. В том числе, планируется ввести автоматический вход в
рабочую сессию при поднесении смартфона на достаточно близкое
расстояние к GM-Box. Также, по результатам проведенных пилотов,
некоторые заказчики хотели бы получить функционал не использующий
алгоритм хранения учётных данных на телефоне, несмотря на все
предпринятые меры по обеспечению безопасности хранения и передачи
данных на смартфонах. Кроме этого, анализ обратной связи от
пользователей показал, что была бы удобна функция быстрого
блокирования рабочей сессии, если сотруднику
необходимо отойти или переключиться на разговор с коллегой эту
опцию мы тоже рассматриваем.