Espruino:Примеры/Введение в Bluetooth Light Energy (BLE)

Материал из Онлайн справочника
Перейти к навигации Перейти к поиску

Перевод: Максим Кузьмин (Cubewriter) Контакты:</br>* Skype: cubewriter</br>* E-mail: cubewriter@gmail.com</br>* Максим Кузьмин на freelance.ru
Проверка/Оформление/Редактирование: Мякишев Е.А.


Введение в Bluetooth Light Energy (BLE)[1]

Эта статья – лишь краткое введение в BLE. Если вам нужна более подробная информация, советуем искать её в разделе «Ссылки» на странице о BLE в «Википедии».

BLE работает на той же частоте, что и обычный Bluetooth (2.4 ГГц), но в то же время создан с прицелом на более дешёвую реализацию и гораздо более высокую энергоэффективность.

Есть два разных типа BLE-устройств (Puck.js может быть и тем, и другим):

  • Центральное устройство – обычно это что-то вроде телефона или ПК, подключающегося к другому устройству.
  • Периферийное устройство – это устройство, к которому подключаются (вроде фитнес-браслета).

Очень краткое содержание этой статьи

В Puck.js можно использовать BLE для последовательного (UART) соединения, чтобы через него отправлять и получать символы. Такой тип коммуникации позволяет управлять Puck.js (например, при помощи библиотеки «Puck.js» для работы с Web Bluetooth).

Вы даже можете управлять одним Puck.js при помощи другого Puck.js вот так.

Сама по себе тема BLE более сложна, так что если вы хотите более безопасно управлять другими устройствами или собственными Puck.js, советуем читать дальше!

Рассылка BLE-объявлений

Как правило, Puck.js работает в режиме рассылки BLE-объявлений. Это значит, что он каждую секунду (или вроде того – это можно настроить) рассылает в окружающее пространство несколько байтов информации и не вступает ни в какую двустороннюю коммуникацию.

По умолчанию Puck.js рассылает данные о своём названии и сервисах, которые в нём реализованы (например, Nordic UART – ниже мы расскажем о нём поподробнее).

Некоторые устройства (вроде маяков Eddystone), как правило, своё название не рассылают, но отправляют URL (который можно прочесть на телефоне) и, возможно, уровень заряда батареи.

Примечание: О том, как настроить на Puck.js рассылку BLE-объявлений, смотрите в описании функции NRF.setAdvertising(). Вы также можете воспользоваться функциями NRF.findDevices() и NRF.setScan() для сканирования пакетов BLE-объявлений, рассылаемых другими маяками.

О том, как получать данные объявлений в своём приложении, читайте тут.

Подключение (Puck.js в роли периферийного устройства)

Когда центральное устройство подключается к Puck.js, работающему в режиме периферийного устройства, оно может инициировать процедуры сопряжения и привязки. Сопряжение – это просто обмен информацией о мерах безопасности и принятие решения о том, какую из этих мер использовать. Но всё это происходит только в рамках текущего соединения.

Зато процедура привязки сохраняет эту информацию (к примеру, ключи шифрования), чтобы воспользоваться ею для последующих соединений. В Puck.js эта функция была реализована в версии 1v92.

Примечание: После подключения энергопотребление Puck.js вырастает с 20 до 200 мкА. По этой причине не рекомендуется оставлять подключение Puck.js открытым, а подключаться, только когда нужно отправить или получить команды.

UUID

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

Они могут быть:

  • 16-битными – используйте только те, что были заданы Bluetooth SIG. Вы также можете купить свой собственный.
  • 32-битными – только те, что были одобрены SIG. Используют их редко, так что мы о них рассказывать почти не будем.
  • 128-битными – они бесплатные и не требуют регистрации (пока вы используете по-настоящему случайный UUID – vanity-идентификатор не подойдёт). Вероятность того, что такой UUID совпадёт с каким-то другим, крайне мала, так что они считаются уникальными.

16-битные UUID

В Espruino они задаются либо как числа вроде 0x180D, либо как строки вроде "180D".

128-битные UUID

Этот тип в Espruino всегда задаётся в виде строки – например, "6e400001-b5a3-f393-e0a9-e50e24dcca9e". Тире опциональны, но мы всё же рекомендуем использовать их в целях удобочитаемости.

В Bluetooth 128-битные UUID можно использовать повторно. Например, чтобы создать кастомный UUID, вы должны проделать следующее:

  • Создать случайный 128-битный UUID. Например, при помощи онлайн-инструмента или запустив на Linux-компьютере что-то вроде date | md5sum. В итоге у вас получится что-то вроде 98dcea57f6874f75c1f8290ebf29da57.
  • Добавьте тире для удобочитаемости: 98dcea57-f687-4f75-c1f8-290ebf29da57.
  • Замените символы с 5-го по 8-ой на «0001», чтобы получилось вот так: 98dc0001-f687-4f75-c1f8-290ebf29da57.
  • Для каждого нового URL просто увеличивайте число в этой группе символов с 5-го по 8-ой. Например, так: 98dc0002-f687-4f75-c1f8-290ebf29da57.

В результате по Bluetooth потребуется отправить всего один UUID, а все последующие UUID будут занимать столько же места, что и 16-битные UUID.

Примечание: 16-битные UUID – это всё те же 128-битные UUID в формате 0000xxxx-0000-1000-8000-00805F9B34FB, где xxxx – это 16-битный UUID.

Сервисы и характеристики

Подключившись, центральное устройство получает доступ к сервисам и характеристикам периферийного (Puck.js) устройства. Это называется GATT – это сокращение от англ. «generic attribute profile», что значит «профиль общих атрибутов».

Мы можем дать сервисам и характеристикам удобочитаемые названия, но в микроконтроллере операции с ними будут проводиться при помощи UUID.

Сервисы – это, по сути, просто группы характеристик, и в каждой характеристике хранится какой-то один тип данных. С характеристикой можно выполнять три главные операции:

  • READ (чтение) – центральное устройство отправляет запрос, а периферийное устройство отвечает на него отправкой текущего значения характеристики.
  • WRITE (запись) – центральное устройство отправляет данные и запрос на запись, а периферийное устройство обновляет значение нужной характеристики (в зависимости от типа записи оно также может ответить, что всё прошло нормально).
  • NOTIFY / INDICATE (уведомление / индикатор) – центральное устройство может попросить, чтобы ему присылали уведомления. В результате, если значение характеристики на периферийном устройстве изменится, оно отправит это значение центральному устройству, избавляя его от необходимости проверять, изменилось ли что-нибудь. Это наилучший способ отправки данных. К примеру, вам может понадобиться отправить два идентичных значения – с помощью NOTIFY это делается просто, но с помощью двух идущих друг за другом READ сделать это будет затруднительно. Операции NOTIFY и INDICATE похожи, но не идентичны. О разнице между ними можно почитать, например, вот тут, но мы всё же советуем всегда использовать NOTIFY, а не INDICATE.

Примечание: Характеристика может допускать любую комбинацию трёх этих типов операций. Фактически часто бывает, что характеристика не позволяет проводить с ней операции READ, но выполняет операции NOTIFY, чтобы уведомлять центральное устройство об изменениях.

К примеру, у вас может быть:

  • Сервис Light, у которого есть две характеристики, на которые можно записывать (и, возможно, считывать с них) данные – brightness и hue.
  • Сервис Button, у которого есть характеристика, позволяющая выполнять на ней операции READ и NOTIFY, чтобы считывать состояние кнопки.
  • Сервис Motion, у которого есть характеристика, выполняющая лишь операцию NOTIFY, чтобы отправить данные при возникновении движения.

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

Примечание: Примеры того, как настраивать сервисы и характеристики на Puck.js, читайте в описании функции NRF.setServices().

Сервисы Puck.js

По умолчанию у Puck.js есть сервис Nordic UART (его UUID – 6e400001-b5a3-f393-e0a9-e50e24dcca9e), позволяющий коммуницировать с JS-интерпретатором. Этот сервис поддерживает двустороннюю коммуникацию. У него две характеристики – RX и TX:

  • Характеристика TX (UUID – 6e400002-b5a3-f393-e0a9-e50e24dcca9e) позволяет отправлять данные на Puck.js. Вы можете записать в неё до 20 байт данных, и при каждой записи отправляемые вами символы будут идти напрямую в JS-интерпретатор.
  • Характеристика RX (UUID – 6e400003-b5a3-f393-e0a9-e50e24dcca9e) позволяет получать данные от Puck.js. Их нельзя прочесть при помощи READ, но можно подписаться на NOTIFY и в результате получать все символы, отправляемые этой характеристикой.

Если вы хотите включить светодиод на Puck.js, то можете просто подключиться и выполнить запись с помощью LED.set() (не забудьте про символ новой строки) в характеристику TX, и команда будет выполнена! Это работает и для всех функций, заданных вами ранее.

Примечание: Оставить Espruino и JS-интерпретатор без защиты – не самая лучшая идея. Его можно защитить при помощи пароля, заданного в E.setPassword(), или вообще убрать из BLE-объявлений при помощи NRF.setServices().

Что происходит внутри?

Внутри у BLE-устройства есть таблица характеристик (и других вещей вроде дескрипторов).

Если центральное устройство хочет получить доступ к характеристике, оно сперва должно найти, где у периферийного устройства находится таблица характеристик. Она называется handle, и это обычное целое число вроде «11».

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

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

Подключение (Puck.js в роли центрального устройства)

Чтобы подключиться к другому устройству с Puck.js, вам понадобится его адрес. Он имеет формат aa:bb:cc:dd:ee – это похоже на MAC-адрес, используемый при WiFi-коммуникации. Эти Bluetooth-адреса могут быть «публичными» (это означает, что двух одинаковых публичных адресов в мире попросту нет) и «случайными» (это означает, что они будут уникальными только в каком-то одном месте). У Puck.js случайные адреса.

Примечание: Если Puck.js работает в роли центрального устройства, то почти все ваши действия будут занимать время – иногда 100 миллисекунд, иногда 2 секунды или больше, если вы выполняете подключение. Это значит, что функционал Puck.js работает при помощи функций обратного вызова и промисов. То есть, если вы вызовите функцию, то она скорее всего вернёт результат сразу же и вызовет функцию, которую вы задали дальше.

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

var devices;
NRF.findDevices(function(d) {
  devices = d;
  console.log(devices);
}, 1000);..):

Этот код выполнит 1-секундное (1000 мс) сканирование и напечатает список находящихся рядом устройств, каждое из которых будет представлено в виде объекта BluetoothDevice:

[
  BluetoothDevice {
    "id": "e7:e0:57:ad:36:a2 random",
    "rssi": -45,
    "services": [  ],
    "data": new ArrayBuffer([ ... ]),
    "name": "Puck.js 36a2"
   },
  BluetoothDevice {
    "id": "c0:52:3f:50:42:c9 random",
    "rssi": -65,
    "services": [  ],
    "data": new ArrayBuffer([ ... ]),
    "name": "Puck.js 8f57"
   }
 ]

Далее вы можете вызвать BluetoothDevice.gatt.connect() на каком-нибудь из этих BluetoothDevice, а затем при помощи промисов найти сервис, найти характеристику, выполнить запись в неё и, наконец, отключиться.

devices[0].gatt.connect().then(function(g) {
  gatt = g;
  return gatt.getPrimaryService("6e400001-b5a3-f393-e0a9-e50e24dcca9e");
}).then(function(service) {
  return service.getCharacteristic("6e400002-b5a3-f393-e0a9-e50e24dcca9e");
}).then(function(characteristic) {
  return characteristic.writeValue("LED1.set()\n");
}).then(function() {
  gatt.disconnect();
  console.log("Готово!");
});

Код выше выполняет подключение к Puck.js, отправляет текст, чтобы включить светодиод, и в конце отключается.

Чтобы собрать всё это вместе, нужно поместить в findDevices() функцию connect(). Примерно вот так:

NRF.findDevices(function(devices) {
  if (devices.length<1) throw new Error("Ничего не найдено!");
  devices[0].gatt.connect().then(function(g) {
    gatt = g;
    return gatt.getPrimaryService("6e400001-b5a3-f393-e0a9-e50e24dcca9e");
  }).then(function(service) {
    return service.getCharacteristic("6e400002-b5a3-f393-e0a9-e50e24dcca9e");
  }).then(function(characteristic) {
    return characteristic.writeValue("LED1.set()\n");
  }).then(function() {
    gatt.disconnect();
    console.log("Готово!");
  });
}, 1000);

Но всё это можно сделать и более изящно – см. ниже.

Примечание: Единственный способ отключиться от устройства – это вызвать disconnect() на объекте BluetoothRemoteGATTServer, возвращаемом функцией NRF.connect().

Другие методы подключения

  • Есть простой вспомогательный модуль, очень упрощающий процесс записи данных на другой Puck.js.
  • Для прямого подключения можно воспользоваться функцией NRF.connect(), задав в её аргументе нужный адрес. Его можно найти в свойстве id объекта BluetoothDevice, возвращённого функцией findDevices(). Это удобно, если вам нужно постоянно подключаться к одному и тому же устройству.
  • Вы также можете воспользоваться функцией NRF.requestDevice() вместо NRF.findDevice(). Она создана по примеру функции navigator.bluetooth.requestDevice из Web Bluetooth и позволяет подключаться к любому устройству определённого типа или названия.

См.также

Ссылки на полезные ресурсы

Внешние ссылки