К 2020 году на планете будет 50 миллиардов устройств из мира «интернета вещей» (IoT). Это предсказание Cisco, сделанное в 2013 году, начинает сбываться. Кроме умных холодильников с подключением к облаку и роботов-пылесосов, сейчас появляется довольно много полезных систем — прототипы с неносимой электроникой, которая помогает по косвенным признакам определить состояние человека, прототип умной парковки и системы управления автомобильным трафиком (Intel)... Возможно, IoT перевернет экономику. Когда-нибудь, возможно уже совсем скоро, нанороботы в человеческой крови, которые вводят точные дозы лекарства и следят за состоянием человека, будут скачивать обновление через беспроводное подключение к облаку. Какой дивный новый мир открывается перед хакерами :).

Концепция IoT
Концепция IoT
 

Погружение в мир машин

Сегодня я расскажу, как сделать свою первую сеть для IoT, мостик между миром окружающим и миром программ, и поделюсь методикой построения интернета вещей. Несмотря на то что некоторые небезосновательно считают это просто рекламным трюком (обычный М2М в новой обертке с подешевевшими компонентами и интернетом), системы с довольно мощными вычислительными ресурсами и малым энергопотреблением все больше завоевывают сердца пользователей, маркетологов и инженеров. Интернет вещей (англ. Internet of Things, IoT) — это концепция вычислительной сети физических объектов («вещей»), оснащенных встроенными технологиями для взаимодействия друг с другом или с внешней средой.

Реализация IoT
Реализация IoT

Реализации интернета вещей очень разнятся по протоколам, по типу сетей и по цене. Обычно общее в них — это набор сенсоров и (или) актуаторов (исполнительных устройств) с первичной сборкой и обработкой данных и пересылкой в хаб или gateway, где происходит дальнейшая обработка данных, отправка команд на сенсоры или пересылка в облако, на серверы (возможно, для работы с алгоритмами из мира Big Data, если данных очень много) и к пользователю.

Рецепт же моей реализации довольно прост: берем набор датчиков с технологией Bluetooth Smart для сбора сведений об окружающей среде, смартфон, облако по вкусу. Смешиваем, взбалтываем — и получаем маленький IoT.

 

План действий

Для нашего простого IoT подойдет схема считывания показаний измеренных физических величин (например, температура и влажность), простая их обработка и отправка клиенту. Завершающий этап — отображение данных на стороне клиента. В самом кратком виде наши действия можно описать тремя пунктами:

  1. Для начала нужно выбрать сам датчик или набор датчиков, с которых мы будем получать измеренные данные, и способ обработки полученных данных.
  2. Затем нужно определиться с тем, как мы будем общаться с датчиками, определить объемы данных и понять, как мы будем строить общение.
  3. Наконец, нам нужно найти подходящий клиент для нашей сети и описать работу с ним.
 

Выбор орудия

Роль датчика и предварительного обработчика будет играть SensorTag 2 (или SensorTag 2015) от Texas Instruments. Как вариант — Ардуино с BLE-шилдом или BLEduino.

Техасовский инструмент мне понравился ценой (примерно 3 тысячи рублей) и возможностями. Представь, что на этой платке размером 5 x 6,7 x 1,4 см есть целых десять разных сенсоров на любой вкус: датчик освещенности, цифровой микрофон, магнитометр, датчик влажности, барометр, акселерометр, гироскоп, геркон, температурный датчик и температурный датчик по ИК-излучению, и все это с возможностью передавать данные по BLE (Bluetooth low energy, малопотребляющий Bluetooth, Bluetooth Smart), или по 6LoWPAN, или по ZigBee, а скоро ожидается выход Wi-Fi SensorTag. Железный человек с Бэтменом обзавидовались бы. А еще SensorTag может быть брелоком, и у него красненький силиконовый чехольчик (приятно потрогать).


На этой платке размером 5x6,7x1,4 см есть целых десять разных сенсоров на любой вкус: датчик освещенности, цифровой микрофон, магнитометр, датчик влажности, барометр, акселерометр, гироскоп, геркон, температурный датчик и температурный датчик по ИК-излучению.

Внутри главной микросхемы СС2650, сердца SensorTag 2, крутится операционная система реального времени (TI-RTOS), которая вместе с фирменным BLE-стеком обеспечивает надежное управление, по сути, тремя разными микроконтроллерами:

  1. Ядро первого микроконтроллера — Cortex-M3 (на нем обычно выполняется написанное нами пользовательское приложение).
  2. Ядро второго — Cortex-M0 (отвечает за физический уровень, радиосвязь).
  3. Отдельный контроллер для датчиков (помогает быстро получать от них данные).

Основная идея заключается в том, чтобы уменьшить разряд батареи, включая и отключая разные ядра в нужное время. Например, если требуется радиосвязь, включаем «радиоядро», отключаем контроллер сенсоров, и наоборот. В результате этих ухищрений достигается снижение энергопотребления до 75% по сравнению с другими BLE-микросхемами. По документации от TI, если устройство отправляет данные раз в секунду, то оно проработает один год от батарейки-монетки CR2032. Если же устройство не будет слать данные, а будет лишь опрашивать датчики раз в секунду, то может продержаться десять лет!


Устройство работает один год от батарейки-монетки CR2032. А если устройство не будет слать данные, а лишь опрашивать датчики раз в секунду, то может продержаться десять лет!
 

Выбор способа общения

Почему BLE? Да потому, что он идеален для IoT:

  • Как ленивец из мультика, он очень крепко спит (мало потребляет энергии), просыпается, быстро передает данные и снова засыпает.
  • Андроидофоны с поддержкой BLE-стека сейчас есть у каждого, а именно его мы будем использовать в качестве хаба и перевалочного пункта (gateway) на пути к облаку.
 

Ключевые термины и понятия BLE

  • Профиль общих атрибутов (GATT), GATT-профиль — это общие спецификации для отправки и получения коротких фрагментов данных («атрибутов») во время BLE-связи. Все существующие BLE-устройства применяют профили на его основе.
    GATT разработан согласно Bluetooth SIG (ассоциация разработчиков стандартов и протоколов для Bluetooth) и определяет множество профилей для устройств с BLE.

  • Профиль — это определение того, как устройство работает в конкретном приложении. Обрати внимание, что устройство может реализовывать больше одного профиля. Например, устройство может содержать профили «монитор сердечного ритма» и «детектор уровня заряда батареи».
  • Протокол атрибутов (АТТ). GATT — это верхний слой в BLE-стеке над атрибутом протокола (АТТ). Также именуется GATT/АТТ. АТТ оптимизирован для работы на BLE-устройствах. С этой целью он посылает насколько возможно меньше байтов. У каждого атрибута есть уникальный универсальный идентификатор (UUID). Он представляет собой стандартизированный 128-битный строковый ID, который используется для однозначной идентификации информации. Атрибуты, передаваемые через АТТ, могут быть двух типов: характеристики и службы.
  • Характеристики — содержат одно значение и дескрипторы, описывающие значения характеристик. Характеристики можно рассматривать как тип.
  • Дескрипторы — определения атрибутов, которые описывают характерные значения. Например, дескриптор может указать удобочитаемое описание, диапазон для значения характеристики или единицы измерения, относящиеся к характеристике и ее значению.
  • Сервис (служба) — это набор характеристик. Например, можно создать сервис под названием «монитор сердечного ритма», который включает в себя такие характеристики, как измерение частоты сердечных сокращений. Найти список существующих GATT профилей и служб можно на bluetooth.org.

Теперь рассмотрим архитектуру GATT (General Attribute Profile):

  • GATT определяет структуры, с помощью которых идет обмен данными и в которых данные сохраняются;
  • сервисы оперируют данными, которые предоставляются с использованием характеристик;
  • клиент использует эти данные.
Архитектура GATT (General Attribute Profile)
Архитектура GATT (General Attribute Profile)
Структура стека TI Bluetooth low energy
Структура стека TI Bluetooth low energy
 

Обзор подходящих инструментов

Для начала работы с SensorTag 2 нужно установить среду разработки от TI — Code Composer Studio (CCS) или IDE IAR. Установщик CCS можно найти по следующей ссылке. Там тебе предложат зарегистрироваться, и затем можно будет скачать установщик (для Windows или Linux). IAR можно найти тут. Нужно выбрать IAR Embedded Workbench for ARM, и после регистрации тебе будет доступна версия без ограничений по размеру компилируемого кода, но с лицензией на месяц или с ограничениями (32 Кбайт компилируемого кода), но без лимита по времени использования. Существует и набор облачных инструментов TI, в числе которых среда CCS Cloud.

Рекомендую попробовать все перечисленные IDE, тем более что их не так много. Также для работы с нашим gateway/хаб-устройством (смартфон с OS Android) нужно установить Java Development Kit (JDK) 7 и Android Studio. Кроме того, рекомендую взять программатор-отладчик cc-devpack-debug. Эта платка не только может прошивать и отлаживать SensorTag 2, но и умеет работать с другими контроллерами от TI (например, СС3200 и СС1310).

Предположим, что у нас нет отладчика, и будем прошивать SensorTag 2 через Android- или iOS-устройство по воздуху. Как это сделать, расскажу дальше.

Еще полезно скачать и установить Bluetooth Low Energy Software Stack, или просто BLE-STACK. Нам пригодится стек версии 2.1.1. После установки пакета в папке ti на жестком диске ты найдешь разные полезные примеры, документацию и прошивки для ST2 под разные IDE. Также для удобной работы из-под Windows с визуализацией и парсингом служб, профилей и пакетов BLE можно установить настольное приложение TI BLE Device Monitor и SmartRF Studio или [BTool] (из состава пакета BLE-STACK).

RTFM: читаем документацию перед началом работы

Из полезной документации перед началом работы я советую почитать введение в разработку BLE-приложений CC2640 BLE Software Developers Guide.

На страничке, посвященной SensorTag 2, есть также достаточно много полезной информации про устройство. Например, там рассказано, как написать приложение к SensorTag 2 для смартфона c использованием JavaScript-фреймворка.

Горсть SensorTag
Горсть SensorTag
Отладчик cc-devpack-debug
Отладчик cc-devpack-debug

Для того чтобы понять, что мы делаем, советую зайти на страничку SimpleLink Academy. На ней в форме лабораторных работ и викторин дается введение в TI-RTOS (операционная система реального времени (RTOS) от TI, которая вместе с BLE-STACK является частью прошивки SensorTag), знакомство с ее понятиями и сущностями (например, есть рассказ о таких общих понятиях для всех RTOS, как семафор или задача). Следующие за этим лабораторные работы посвящены как раз Bluetooth Smart:

  • Фундаментальные концепции устройства BLE-стека.
  • Создание своего кастомного профиля в BLE.
  • Создание простой сети.
  • Работа с другой IDE (Sensor Controller Studio) и пример для SensorTag — снятие данных по шине I2C от датчика освещенности.

Да, чуть не забыл: можно и не менять прошивку SensorTag 2, если интересно только программирование на стороне Android. Скачать исходники для Android всегда можно c Git-репозитория по ссылке. Это же приложение есть и на Google Play. Так что можешь просто экспериментировать на Android-стороне.

 

Работа с железом

Для запуска SensorTag (если мы это делаем в первый раз) нужно вытащить пленку, чтобы запитать плату от батарейки. В дальнейшем включать/выключать устройство можно, нажимая на кнопку сбоку с гравировкой включения (не узнать ее может только совсем дикий человек, который не видел даже телевизора :)).

Итак, мы установили CCS. Запустим ее. Выберем рабочую область. Работа с CCS происходит так же, как и в других Eclipse-подобных средах.

  1. Откроем TI Resource Explorer через пункт меню View -> Resource Explorer (Examples).
  2. Откроем папку SimpleLink Academy -> Bluetooth Smart.
  3. Выберем проект Project Zero -> СС2650DK SensorTag -> ProjectZeroApp.
  4. Нажмем кнопку Import the example project into CCS в правой части Resource Explorer. Дальнейшие шаги описаны в этом окне, и при выполнении шагов рядом с их значками появляется зеленая галочка. После этого в Project Explorer слева и в рабочей области должны появиться две папки — два проекта (ProjectZeroApp_CC2650STK и ProjectZeroStack_CC2650STK). Если ProjectZeroStack_CC2650STK не добавился, нужно добавить его вручную. В этих примерах BLE-стек и проект с самим нашим приложением связаны и импортируются, компилируются вместе. В других средах и примерах, не из SimpleLink Academy (например, из пакета BLE SDK), нужно добавлять стек и приложение самому.
  5. Если у тебя есть cc-devpack-debug, можно прошить SensorTag им. Необходимо сначала подключить devpack к ПК, а затем — SensorTag к devpack. Сам процесс несложен и описан в SimpleLink Academy. Сc-devpack-debug пригодится и для возвращения исходной прошивки-примера, если что-то пошло не так.
  6. Компилируем проект, нажав сочетание клавиш Ctrl + B или на иконку молотка на панели.
  7. Отладка проекта осуществляется нажатием клавиши F11 или нажатием на иконку зеленого жучка.
  8. Завершение отладки — Ctrl + F2 или нажатие на иконку красного квадрата.
  9. Нажатие F8 — запуск/остановка, продолжение отладки и выполнение программы.
  10. После начала выполнения программы и в случае, если у нас есть подключенный отладчик, в терминале последовательного порта появится служебная информация об инициализации трех BLE-служб:
    1. состояние светодиода (светит или нет);
    2. состояние кнопки (нажата или нет);
    3. состояние данных (есть у нас данные или нет).

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

В отсылаемый широковещательный пакет могут быть включены и полезные данные, например измеряемая датчиком температура (режим Advertisement). Если в терминале ничего не отображается, можно попробовать запустить SmartRF Flash Programmer v2 и оживить с его помощью устройство.

Ура, промежуточный результат: мы сделали Bluetooth-маячок (почти iBeacon :)). Пока он только посылает в мир состояние светодиода и кнопки. Теперь можно запустить приложение наподобие BLEScanner на телефоне и прочитать атрибуты (данные) устройства.

Импорт проектов в CCS
Импорт проектов в CCS
Запуск и отслеживание служб в терминале
Запуск и отслеживание служб в терминале
 

Соединение и навигация по службам

Для того чтобы взаимодействовать с BLE-устройствами, нам нужно будет использовать что-то, что выступало бы в качестве центрального устройства по отношению к нему. В нашем случае это будет Android-смартфон (с версией не ниже 4.3, я испытал Nexus 5X и Xperia C3), но может быть и ПК.

Техас Instruments предлагает несколько ПК-инструментов для этого. Инструменты, в свою очередь, обмениваются данными через последовательный порт с помощью специальной микропрограммы, загруженной на CC2650 устройства. Последовательность команд соответствует HCI-стандарту по определению Bluetooth SIG и позволяет проводить контроль хост-слоев (таких как GATT, GAP и Security Manager) в дополнение к канальному уровню команд, необходимых для HCI-спецификации. Инструменты типа BTool и Ti BLE Device Monitor используют этот интерфейс для подключения и взаимодействия с другими BLE-устройствами. Можно использовать SensorTag с проектом HostTest; этот проект позволяет пользователю вызывать GATT- и АТТ-функции BLE-стека через последовательный интерфейс (виртуальный COM-порт, например).


Интерфейс хост-контроллера (HCI) — важная вещь в мире Bluetooth, которая связывает железо и нашу программу.

Если ПК-инструмент не находит BLE-устройства автоматически, это можно сделать вручную, нажав на кнопку «Сканировать» в программе Device Monitor из пакета BLE-стека.

Внешний вид приложения BLE Device Monitor
Внешний вид приложения BLE Device Monitor
Состояние светодиода через службу BLE
Состояние светодиода через службу BLE
 

Создание прошивки

Прошивка через приложение на Android- или iOS-устройстве выглядит следующим образом.

  1. После того как мы нашли наше BLE-устройство и соединились с ним, в конце списка с показаниями разных сенсоров найдем пункт TI OAD Service (служба прошивки по воздуху), нажмем ее, в появившемся активити мы увидим две кнопки: Factory и Custom (заводское или «свое» обновление). Выбрав кнопку Custom, можно найти файл .bin, предварительно скопированный в файловую систему Android, с нашей прошивкой, получившейся в CCS или IAR, и нажать кнопку Start Programming.
  2. Получим bin-файл с прошивкой в CCS, кликнув правой кнопкой мыши на Project -> Properties -> Build -> Steps -> Post build steps.
  3. Добавим в поле Post build steps следующие строки:

    "${CCS_INSTALL_ROOT}/utils/tiobj2bin/tiobj2bin" "${BuildArtifactFileName}" "${BuildArtifactFileBaseName}.bin"
    "${CG_TOOL_ROOT}/bin/armofd" "${CG_TOOL_ROOT}/bin/armhex" "${CCS_INSTALL_ROOT}/utils/tiobj2bin/mkhex4bin"

Пересоберем проект после применения изменений, и bin-файлы должны появиться в папке output проекта.

А вот так выглядит получение bin-файла в IAR:

  1. Кликаем правой кнопкой мыши на проекте -> Options -> Output Converter.
  2. Выбираем Ensure Generate additional output.
  3. Устанавливаем Output format в binary.

Рекомендую собирать проект и получать bin-файл в IAR, так проще, да и в CCS не всегда удавалось получить bin-файл. Проблема и решение указаны на форуме E2E.

 

Прочитаем имя устройства

Используя свой GATT-эмулятор клиентского устройства, находим сервис под названием универсального доступа (идентификаторы UUID 0x1800), развернем его, чтобы найти название устройства (идентификатор UUID 0x2A00), а потом прочитаем это значение.

 

Изменим объявление данных

Две переменные — advertData и scanRspData — содержат данные устройства и будут передавать во время режима Advertisement принимающей стороне, чтобы она увидела устройство и подключилась к нему.

 

Изменение LOCAL_NAME

В ProjectZero.с находим массив advertData (поиск через Ctrl + F) и изменим LOCAL_NAME_COMPLETE из advertData, чтобы поменять имя устройства.
Щелкнем правой кнопкой мыши на проект ProjectZeroApp и выберем Build, проект соберется. Запустим измененный код и увидим, когда включим поиск Bluetooth-устройств, что название изменилось.

iOS имеет свойство кешировать ранее известные имена устройств и не может уточнить твое новое имя, поэтому находим вызов функции GGS_SetParameter(GGS_DEVICE_NAME_ATT, ...) в функцию ProjectZero.с::ProjectZero_init() и изменим содержимое массива, на который указывает ссылка.

Службы и характеристики BLE с состоянием светодиодов
Службы и характеристики BLE с состоянием светодиодов
Внешний вид приложения SensorTagApp
Внешний вид приложения SensorTagApp
 

На стороне Android

Итак, в качестве клиента мы выбрали Android-девайс. Приступим!


Рекомендую прочитать о программировании BLE-устройств на Android документацию на DeveloperAndroid и использовать приложение SensorTagApp, о котором я рассказал выше (скажу по секрету: в SensorTagApp уже все написано).

C Android 4.3 (уровень API 18) вводится встроенная в платформу поддержка Bluetooth с низким энергопотреблением для центрального устройства и предоставляется API, который приложения могут использовать для обнаружения устройств, запроса услуг и для того, чтобы читать/писать характеристики.

 

Роли

Здесь перечислены роли, которые применяются в устройстве Android при взаимодействии с BLE-устройствами: центральная и периферийная. Устройство в центральной роли сканирует, ищет Advertisement-рассылки других устройств.

Представь, что у тебя есть Android-телефон и фитнес-трекер, который является BLE-устройством. Телефон поддерживает центральную роль; трекер поддерживает периферийную роль (для установления соединения BLE нам нужна одна из двух вещей, устанавливать соединение не могут устройства, которые поддерживают только периферийную роль, и две вещи, которые поддерживают только центральную роль).

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

Как приложение SensorTag, так и пример из developer.android.com (Android-приложение) являются клиентами GATT. Приложение получает данные от сервера GATT — BLE-монитора сердцебиений, который поддерживает профиль сердечного ритма. Но можно в качестве альтернативы создать свое Android-приложение, чтобы играть роль сервера GATT (см. класс BluetoothGattServer для получения дополнительной информации).

 

Права доступа BLE

Для того чтобы использовать функции Bluetooth в нашем приложении, мы должны объявить разрешение BLUETOOTH permission. Это разрешение на выполнение какой-либо связи Bluetooth, например запрашивание соединения, прием соединений и передачу данных.

Чтобы приложение инициировало обнаружение устройств или управляло настройками Bluetooth, нужно объявить разрешение BLUETOOTH_ADMIN. Примечание: если используем разрешение BLUETOOTH_ADMIN, то также нужно иметь разрешение BLUETOOTH.

Объявляем разрешение Bluetooth в файле-манифесте приложения. Например:

<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>

Если мы хотим, чтобы приложение было доступно только для BLE-совместимых устройств, пишем в манифесте следующее:

<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>

Однако, если планируется сделать приложение доступным для устройств, которые не поддерживают BLE, мы все равно должны включить этот элемент в манифест, но установить при необходимости ="false". Затем во время выполнения можно определить BLE-доступность с помощью диспетчера пакетов PackageManager.hasSystemFeature() (код из примера):

// Use this check to determine whether BLE is supported on the device. Then
// you can selectively disable BLE-related features.
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
  Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show();
  finish();
}
 

Настройка BLE

Перед тем как приложение сможет обмениваться данными через BLE, мы должны убедиться, что BLE поддерживается на устройстве, и если да, то убедиться, что он включен. Обрати внимание, что эта проверка необходима, только если установлена в false.

Если BLE не поддерживается, то мы должны корректно отключить все функции BLE. Если BLE поддерживается, но отключен, мы можем попросить пользователя включить Bluetooth, не выходя из приложения. Эта настройка выполняется в два этапа, с помощью адаптера Bluetooth.

1. Получить экземпляр BluetoothAdapter.

BluetoothAdapter требуется для любых Bluetooth activity. BluetoothAdapter представляет собой собственный адаптер Bluetooth-устройства (радио Bluetooth). Один адаптер Bluetooth для всей системы, и наше приложение может взаимодействовать с ним с использованием этого объекта. Фрагмент кода ниже показывает, как получить адаптер. Обрати внимание, что этот подход использует getSystemService (), чтобы вернуть экземпляр Bluetooth Manager, который затем используется для получения адаптера. Android 4.3 (API Level 18) вводит диспетчер Bluetooth:

// Initializes Bluetooth adapter.
final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = bluetoothManager.getAdapter();

2. Включить Bluetooth.

Далее необходимо убедиться, что Bluetooth включен, проверив свойство isEnabled(). Если этот метод возвращает false, то функции Bluetooth выключены. Следующий фрагмент кода проверяет, включена ли функция Bluetooth. Если это не так, сниппет выводит сообщение об ошибке, предлагающее пользователю зайти в настройки, чтобы включить Bluetooth.

private BluetoothAdapter mBluetoothAdapter;
...
// Ensures Bluetooth is available on the device and it is enabled. If not,
// displays a dialog requesting user permission to enable Bluetooth.
if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
  Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
  startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}

3. Поиск BLE-устройств.

Для поиска BLE-устройств используем метод startLeScan(). Этот метод принимает BluetoothAdapter.LeScanCallback в качестве параметра. Мы должны реализовать обратный вызов, который возвращает результаты сканирования. Поскольку сканирование интенсивно тратит заряд батареи нашего Android, мы должны соблюдать следующие правила:

  • как только мы найдем нужное устройство, остановим сканирование;
  • никогда не сканировать в цикле и установить лимит времени на сканирование. Устройство, которое ранее было доступно, возможно, вышло за пределы диапазона, и продолжение сканирования разряжает батарею.

Следующий фрагмент кода показывает, как запускать и останавливать сканирование:

// Activity for scanning and displaying available BLE devices.
public class DeviceScanActivity extends ListActivity {
  private BluetoothAdapter mBluetoothAdapter;
  private boolean mScanning;
  private Handler mHandler;

  // Stops scanning after 10 seconds.
  private static final long SCAN_PERIOD = 10000;
  ...
  private void scanLeDevice(final boolean enable) {
    if (enable) {
      // Stops scanning after a pre-defined scan period.
      mHandler.postDelayed(new Runnable() {
        @Override
        public void run() {
          mScanning = false;
          mBluetoothAdapter.stopLeScan(mLeScanCallback);
        }
      }, SCAN_PERIOD);
      mScanning = true;
      mBluetoothAdapter.startLeScan(mLeScanCallback);
    } else {
      mScanning = false;
      mBluetoothAdapter.stopLeScan(mLeScanCallback);
    }
    ...
  }
  ...
}

Если мы хотим сканировать только определенные типы периферийных устройств с определенными атрибутами, вместо этого можно вызвать startLeScan(UUID[], BluetoothAdapter.LeScanCallback) — предоставляет массив идентификаторов объектов UUID, определяющих GATT-службы, которые поддерживает наше приложение.

Вот пример реализации BluetoothAdapter.LeScanCallback в виде интерфейса, используемого для доставки результата BLE-сканирования:

private LeDeviceListAdapter mLeDeviceListAdapter;
...
// Device scan callback.
private BluetoothAdapter.LeScanCallback mLeScanCallback = new BluetoothAdapter.LeScanCallback() {
  @Override
  public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
    runOnUiThread(new Runnable() {
      @Override
      public void run() {
        mLeDeviceListAdapter.addDevice(device);
        mLeDeviceListAdapter.notifyDataSetChanged();
      }
    });
  }
};

Примечание: можно сканировать только устройства Bluetooth LE или устройства Bluetooth Classic, как описано в разделе Bluetooth. Нельзя сканировать Bluetooth LE и Bluetooth Classic устройства одновременно.

 

Подключение к серверу GATT

Первым шагом во взаимодействии с BLE-устройством будет подключение к серверу GATT на устройстве. Чтобы подключиться к серверу GATT на BLE-устройстве, мы используем метод connectGatt(). Этот метод принимает три параметра: Context объекта, autoConnect (логическое значение, указывающее, следует ли автоматически подключаться к BLE-устройству, как только оно станет доступным), а также ссылку на BluetoothGattCallback:

mBluetoothGatt = device.connectGatt(this, false, mGattCallback);

Это подключение к GATT-серверу на удаленном устройстве и возвращает экземпляр BluetoothGatt, который затем можно использовать для проведения GATT-клиентских операций. Абонент (Android-приложение) является GATT-клиентом. BluetoothGattCallback используется для передачи результатов клиенту.

В этом примере BLE-приложение обеспечивает активити DeviceControlActivity для подключения, отображения данных и показа GATT служб и характеристик, поддерживаемых устройством. На основе UI эта деятельность связывает активити и сервис под названием BluetoothLeService, который взаимодействует с BLE.

// A service that interacts with the BLE device via the Android BLE API.
public class BluetoothLeService extends Service {
private final static String TAG = BluetoothLeService.class.getSimpleName();

private BluetoothManager mBluetoothManager;
private BluetoothAdapter mBluetoothAdapter;
private String mBluetoothDeviceAddress;
private BluetoothGatt mBluetoothGatt;
private int mConnectionState = STATE_DISCONNECTED;

private static final int STATE_DISCONNECTED = 0;
private static final int STATE_CONNECTING = 1;
private static final int STATE_CONNECTED = 2;

public final static String ACTION_GATT_CONNECTED =
  "com.example.bluetooth.le.ACTION_GATT_CONNECTED";
public final static String ACTION_GATT_DISCONNECTED =
  "com.example.bluetooth.le.ACTION_GATT_DISCONNECTED";
public final static String ACTION_GATT_SERVICES_DISCOVERED =
  "com.example.bluetooth.le.ACTION_GATT_SERVICES_DISCOVERED";
public final static String ACTION_DATA_AVAILABLE =
  "com.example.bluetooth.le.ACTION_DATA_AVAILABLE";
public final static String EXTRA_DATA =
  "com.example.bluetooth.le.EXTRA_DATA";

public final static UUID UUID_HEART_RATE_MEASUREMENT =
  UUID.fromString(SampleGattAttributes.HEART_RATE_MEASUREMENT);

// Various callback methods defined by the BLE API.
private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
  @Override
  public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
    String intentAction;
    if (newState == BluetoothProfile.STATE_CONNECTED) {
      intentAction = ACTION_GATT_CONNECTED;
      mConnectionState = STATE_CONNECTED;
      broadcastUpdate(intentAction);
      Log.i(TAG, "Connected to GATT server.");
      Log.i(TAG, "Attempting to start service discovery:" + mBluetoothGatt.discoverServices());
    } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
      intentAction = ACTION_GATT_DISCONNECTED;
      mConnectionState = STATE_DISCONNECTED;
      Log.i(TAG, "Disconnected from GATT server.");
      broadcastUpdate(intentAction);
    }
  }

  @Override
  // New services discovered.
  public void onServicesDiscovered(BluetoothGatt gatt, int status) {
    if (status == BluetoothGatt.GATT_SUCCESS) {
      broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);
    } else {
      Log.w(TAG, "onServicesDiscovered received: " + status);
    }
  }

  @Override
  // Result of a characteristic read operation.
  public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
    if (status == BluetoothGatt.GATT_SUCCESS) {
      broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
    }
  }
  ...
};

Когда конкретный обратный вызов срабатывает, он вызывает соответствующий broadcastUpdate() вспомогательный метод. Обрати внимание, что извлечение данных в этом примере выполняется в соответствии с Bluetooth Heart Rate Measurement profile specifications (профиль и UUID монитора сердцебиений), — список стандартизированных UUID профилей и служб можно найти на bluetooth.org. Можно использовать и другие профили, например для измерения температуры или влажности. Также можно сделать свой необычный профиль, скажем наличие или отсутствие котика на кухне у холодильника.

private void broadcastUpdate(final String action) {
  final Intent intent = new Intent(action);
  sendBroadcast(intent);
}

private void broadcastUpdate(final String action, final BluetoothGattCharacteristic characteristic) {
  final Intent intent = new Intent(action);

  // This is special handling for the Heart Rate Measurement profile. Data
  // parsing is carried out as per profile specifications.
  if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) {
    int flag = characteristic.getProperties();
    int format = -1;
    if ((flag & 0x01) != 0) {
      format = BluetoothGattCharacteristic.FORMAT_UINT16;
      Log.d(TAG, "Heart rate format UINT16.");
    } else {
      format = BluetoothGattCharacteristic.FORMAT_UINT8;
      Log.d(TAG, "Heart rate format UINT8.");
    }
    final int heartRate = characteristic.getIntValue(format, 1);
    Log.d(TAG, String.format("Received heart rate: %d", heartRate));
    intent.putExtra(EXTRA_DATA, String.valueOf(heartRate));
  } else {
    // For all other profiles, writes the data formatted in HEX.
    final byte[] data = characteristic.getValue();
    if (data != null && data.length > 0) {
      final StringBuilder stringBuilder = new StringBuilder(data.length);
      for(byte byteChar : data)
        stringBuilder.append(String.format("%02X ", byteChar));
      intent.putExtra(EXTRA_DATA, new String(data) + "\n" + stringBuilder.toString());
    }
  }
  sendBroadcast(intent);
}

После возврата обратно в DeviceControlActivity эти события обрабатываются в классе широковещательного приемника BroadcastReceiver:

// Handles various events fired by the Service.
// ACTION_GATT_CONNECTED: connected to a GATT server.
// ACTION_GATT_DISCONNECTED: disconnected from a GATT server.
// ACTION_GATT_SERVICES_DISCOVERED: discovered GATT services.
// ACTION_DATA_AVAILABLE: received data from the device. This can be a
// result of read or notification operations.
private final BroadcastReceiver mGattUpdateReceiver = new BroadcastReceiver() {
  @Override
  public void onReceive(Context context, Intent intent) {
    final String action = intent.getAction();
    if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) {
      mConnected = true;
      updateConnectionState(R.string.connected);
      invalidateOptionsMenu();
    } else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) {
      mConnected = false;
      updateConnectionState(R.string.disconnected);
      invalidateOptionsMenu();
      clearUI();
    } else if (BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED.equals(action)) {
      // Show all the supported services and characteristics on the user interface.
      displayGattServices(mBluetoothLeService.getSupportedGattServices());
    } else if (BluetoothLeService.ACTION_DATA_AVAILABLE.equals(action)) {
      displayData(intent.getStringExtra(BluetoothLeService.EXTRA_DATA));
    }
  }
};
 

Чтение атрибутов

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

public class DeviceControlActivity extends Activity {
  ...
  // Demonstrates how to iterate through the supported GATT
  // Services/Characteristics.
  // In this sample, we populate the data structure that is bound to the
  // ExpandableListView on the UI.
  private void displayGattServices(List<BluetoothGattService> gattServices) {
    if (gattServices == null) return;
    String uuid = null;
    String unknownServiceString = getResources().
      getString(R.string.unknown_service);
    String unknownCharaString = getResources().
      getString(R.string.unknown_characteristic);
    ArrayList<HashMap<String, String>> gattServiceData =
      new ArrayList<HashMap<String, String>>();
    ArrayList<ArrayList<HashMap<String, String>>> gattCharacteristicData
      = new ArrayList<ArrayList<HashMap<String, String>>>();
    mGattCharacteristics =
      new ArrayList<ArrayList<BluetoothGattCharacteristic>>();

    // Loops through available GATT Services.
    for (BluetoothGattService gattService : gattServices) {
      HashMap<String, String> currentServiceData =
        new HashMap<String, String>();
      uuid = gattService.getUuid().toString();
      currentServiceData.put(LIST_NAME, SampleGattAttributes.lookup(uuid, unknownServiceString));
      currentServiceData.put(LIST_UUID, uuid);
      gattServiceData.add(currentServiceData);

      ArrayList<HashMap<String, String>> gattCharacteristicGroupData =
        new ArrayList<HashMap<String, String>>();
      List<BluetoothGattCharacteristic> gattCharacteristics =
        gattService.getCharacteristics();
      ArrayList<BluetoothGattCharacteristic> charas =
        new ArrayList<BluetoothGattCharacteristic>();
      // Loops through available Characteristics.
      for (BluetoothGattCharacteristic gattCharacteristic : gattCharacteristics) {
        charas.add(gattCharacteristic);
        HashMap<String, String> currentCharaData =
          new HashMap<String, String>();
        uuid = gattCharacteristic.getUuid().toString();
        currentCharaData.put(LIST_NAME, SampleGattAttributes.lookup(uuid, unknownCharaString));
        currentCharaData.put(LIST_UUID, uuid);
        gattCharacteristicGroupData.add(currentCharaData);
      }
      mGattCharacteristics.add(charas);
      gattCharacteristicData.add(gattCharacteristicGroupData);
    }
    ...
  }
  ...
}
 

GATT и получение уведомлений (нотификация)

Чтобы воспользоваться функцией уведомления о поступлении конкретной характеристики, измененной на устройстве, нужно установить уведомление для признака, используя метод setCharacteristicNotification():

private BluetoothGatt mBluetoothGatt;
BluetoothGattCharacteristic characteristic;
boolean enabled;
...
mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
...
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
  UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);

Уведомления включаются для характеристики в onCharacteristicChanged(), когда обратный вызов срабатывает, если значения характеристик изменились на удаленном устройстве.

@Override
// Characteristic notification
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
  broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
  ...
 

Закрытие клиентского GATT-соединения

Как только твое приложение закончило использовать BLE-устройство, оно должно вызвать метод close(), чтобы освободить ресурсы надлежащим образом.

public void close() {
  if (mBluetoothGatt == null) {
    return;
  }
  mBluetoothGatt.close();
  mBluetoothGatt = null;
}
 

Итоги

Мы познакомились с железом и софтом для создания простой сети на Bluetooth low energy, почитали и записали атрибуты (значения показаний сенсоров). Попробовали создать свою прошивку и сделали консоль для IoT с помощью Android-девайса. В дальнейшем можно подключить нашу сеть к облаку и получать/посылать данные через удобный веб-интерфейс. Жди продолжения в следующем номере!

WARNING


Осторожно прошивай SensorTag. Если батарейка начинает садиться, то замени ее.

Неосторожное продолжительное нажатие кнопки включения или пользовательской кнопки может привести к переходу SensorTag на другой стандарт вещания. Зажми вместе кнопку включения и пользовательскую кнопку и держи шесть секунд. После этого устройство вернется в BLE-режим.

1 комментарий

  1. Аватар

    http://rusdelphi.com

    29.04.2016 в 02:57

    Спасибо за освещение данной темы. Всё руки не доходят сделать что-то подобное для дома.

Оставить мнение