BLE адаптер на ESP32 под ардуйно

Глядя на обилие дешевых ESP32 модулей захотелось мне сделать из них что нибудь полезное. Для работы мне нужен был BLE адаптер с последовательным интерфейсом пригодный для разных применений вроде организации беспроводного канала связи между железками или сбора телеметрии с нескольких устройств. Ну, а для большей радости от процесса была выбрана платформа ардуйно. Эта статья — о том что получилось.

Недостатки существующих решений

На али есть масса готовых решений вроде JDY-08 или HM-10, но они закрытые, не позволяют соединяться одновременно с несколькими устройствами и не предоставляют средств управления потоком данных. Еще об одном недостатке речь пойдет ниже.

Важность управления потоком данных

Любой канал связи имеет ограниченную пропускную способность. В случае BLE соединения она довольно ограничена и сильно зависит от условий приема. Что же произойдет при попытке передать больше данных, чем канал связи способен пропустить через себя? То же, что при попытке наливать в ванну больше, чем из нее вытекает — вода выльется на пол, а данные потеряются. Если они потеряются в канале связи, адаптер может хотя бы отследить, какие конкретно фрагменты потерялись. А если потеря происходит в последовательном канале связи, то как то контролировать масштаб этой потери просто невозможно. Чтобы этого не допускать, полезно использовать аппаратное управление потоком — сигналы RTS/CTS. Особенно важен RTS на стороне адаптера, поскольку он предотвращает переполнение его приемного буфера. Этот сигнал должен соединяться со входом CTS на другой стороне последовательного соединения. В конфигурации адаптера сигнал RTS активен по умолчанию при использовании аппаратного порта. Вы можете не соединять его физически, если в нем нет необходимости.

Как это работает — передача данных посредством BLE

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

Роли и потоки данных между BLE устройствами

Роли и потоки данных между BLE устройствами

Периферийное устройство содержит набор сервисов. Сервис представляет собой коллекцию характеристик. Характеристика фактически является буфером данных размером до 512 байт. Характеристики могут иметь дескрипторы, которые описывают их свойства и тоже являются буфером данных, только меньшего размера (бритва Оккама скучает без дела). Центральное устройство лишено всей этой внутренней структуры, оно лишь может устанавливать соединение с периферийным устройством, записывать данные в его характеристики и подписываться на обновления. Адаптер имеет единственную характеристику, через нее и происходит обмен данными. При этом адаптер может работать как в роли центрального устройства, так и в роли периферийного, так и в двух ролях одновременно. В качестве центрального устройства он может устанавливать соединение с периферийными устройствами в количестве до 4 одновременно (это ограничение реализации BLE стэка).

Смысл соединения

Тут полезно задаться вопросом — в чем смысл соединения между центральным и периферийным устройством? Например, в классическом BT есть последовательный канал SPP, он гарантирует целостность потока данных и либо доставляет их в потоке, либо рвет соединение. Аналогичную семантику имеет TCP/IP соединение. Оказывается, что BLE соединение ничего не гарантирует вообще, оно нужно просто чтобы хранить контекст связи двух устройств. Но рваться по собственной инициативе оно тоже может. Обновления характеристик могут как угодно повреждаться, теряться и переупорядочиваться в процессе передачи.

Прозрачная передача против пакетной

И здесь мы подходим ко второму фундаментальному недостатку существующих решений — они ориентированы на прозрачную передачу потока данных. Это конечно удобно для пользователя, но правильно ли? Ведь BLE не может гарантировать целостность этого потока. Адаптер делит его на фрагменты, с которыми в канале связи может произойти все что угодно. В результате поток данных может быть произвольно модифицирован. Единственный известный человечеству способ обеспечить целостность данных при передаче по такому ненадежному каналу — это делить его на фрагменты и добавлять к ним средства проверки целостности (контрольные суммы). А если нужен поток с гарантией целостности, то добавлять средства контроля доставки в нужном порядке, подтверждение доставки с приемной стороны и повтор передачи на передающей стороне. Мы не пойдем настолько далеко в нашем адаптере. Но деление данных на фрагменты в нем предусмотрено. Он получает их в виде фрагментов на передающей стороне и доставляет фрагмент с сохранением границ на приемную сторону. Так что пользователь лишен необходимости делить поток на фрагменты самостоятельно. И последнее, но не менее важное обстоятельство, — с прозрачной передачей потоковых данных невозможно реализовать передачу данных в несколько соединений одновременно.

Протокол управления

Для управления адаптером и передачи данных он реализует простой протокол асинхронный протокол, схематически показанный на следующем рисунке.

Протокол управления адаптером

Протокол управления адаптером

В качестве управляющей подсистемы может выступать сколь угодно простое устройство, имеющее последовательный порт. От него адаптер получает команды и данные для передачи. Ему он в свою очередь передает данные, полученные от подключенных устройств, нотификацию о своем состоянии и отладочные сообщения. Команд всего две — сброс и подключение списка устройств. Устройства идентифицируются по их адресу. Идентификация по имени не предусмотрена, поскольку она требует дополнительной процедуры поиска устройства, к тому же имена не являются уникальными.

Реализация протокола на языке python содержится в файле python/ble_multi_adapter.py

Передача бинарных данных

Кодирование бинарных данных

Кодирование бинарных данных

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

Расширенный пакетный режим

Адаптер предполагает что полученный им пакет данных можно записать в характеристику, что ограничивает его размер. Адаптер использует фрагменты данных до 244 байт, которые теоретически должны передаваться без дальнейшей фрагментации. Чтобы передавать пакеты данных большего размера адаптер реализует расширенный пакетный режим, в котором пакеты делятся на фрагменты, каждый из которых снабжен однобайтным заголовком и трехбайтовой контрольной суммой, как показано на рисунке ниже.

Расширенный пакетный режим

Расширенный пакетный режим

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

Обработка ошибок

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

Проблемы

В ходе работы над проектом было обнаружено немало проблем и даже багов в библиотеках и ардуйно классах на их основе.

Как уже отмечалось, использование управления потоком последовательного порта это хорошо и правильно. Но есть один нюанс. При использовании двухстороннего управления RTS/CTS возможна взаимоблокировка передатчика и приемника, когда они оба пытаются записать что то в последовательный канал, при том, что у обоих буфер приема переполнен. Взаимоблокировка возникает от того, что и передатчик и приемник могут либо передавать, либо принимать данные, но не могут это делать одновременно. Есть казалось бы простое и очевидное решение — не добавлять данные в буфер передачи, если в нем нет для них места. К несчастью библиотека ESP32 не позволяет достоверно узнать размер свободного места в буфере передатчика. Так что на данный момент рекомендация сводится к тому, чтобы использовать RTS, чтобы исключить переполнение приемного буфера адаптера, но не использовать CTS.

Следует иметь ввиду, что при работе адаптера через USB CDC нет никакого управления потоком вообще. Новые данные просто перетирают старые.

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

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

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

Компиляция, настройки

Для компиляции не потребуется ничего, кроме ардуйно. В настройках (Additional board manager URLs) добавьте ссылку на пакет поддержки ESP32:

https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json

Загрузите пакет esp32 by Espressif Systems в менеджере плат. Выберите ESP32C3 Dev Module или ESP32S3 Dev Module в зависимости от того, какой процессор у вас на плате. С другими возможно тоже работает, я тестировал на этих двух. Разрешите в настройках платы USB CDC On Boot. Установите правильный COM порт, соответствующий подключенной плате, и плату можно прошивать. Имейте ввиду, что платы вообще без прошивки при подключении входят в цикл перезагрузки. Чтобы прошить такую плату, ее нужно перевести в режим загрузки. Для этого нажмите и удерживайте кнопку BOOT, потом нажмите и отпустите кнопку RST, затем отпустите BOOT. После прошивки нужно нажать RST, чтобы выйти из режима загрузки.

Проект имеет множество настроек времени компиляции, которые вынесены в заголовочный файл mx_config.h. Он включает другой заголовочный файл (по умолчанию default_config.h), который вы можете скопировать, переименовать и настроить по своему вкусу. В настройках вы можете выбрать имя устройства, режим его работы, адрес устройства для автоматического соединения, если вам нужен канал связи, который устанавливается автоматически, и много другое.

Производительность

Максимальная скорость передачи данных, полученная в эхо-тесте для одного соединения, составляет около 4kБ/сек в одну сторону (+ столько же в другую). Для четырех соединений в каждом из них скорость падает до 1.5kБ/сек, что видимо является ограничением последовательного порта. При желании его скорость можно увеличить в настройках (UART_BAUD_RATE).

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

Энергопотребление

Модуль ESP32C3 Super Mini в покое потребляет около 65 мА. Под нагрузкой при приеме 50 коротких сообщений в секунду с 3 соединений потребление возрастает до 120 мА. Модуль на ESP32S3 ожидаемо потребляет больше за счет двух процессорных ядер — около 90 мА в покое. Под нагрузкой, однако, потребление возрастает не так сильно — до 115 мА. В целом нормально, но не для батарейного питания.

Дальность связи

Сильно зависит от антенны. Наихудший вариант — чип антенна, вроде тех, что стоят на модулях ESP32C3 Super Mini. Она превращает электрическую энергию преимущественно в тепло. Не стоит ожидать от нее дальности больше 10 метров. Печатная антенна уже значительно лучше. Наилучшие результаты дают внешние антенны, даже самые простые. Если на плате стоит чип антенна, и нет разъема для внешней, — просто удалите чип антенну и припаяйте внешнюю, как показано на фото

Внешняя антенна припаянная вместо чип антенны к ESP32C3 Super Mini

Внешняя антенна припаянная вместо чип антенны к ESP32C3 Super Mini

Дополнительно увеличить дальность можно за счет программного увеличения мощности передатчика. Эта опция включена по умолчанию в файле конфигурации (TX_PW_BOOST). В таком варианте дальность работы на открытой местности составляет 100 м. В помещении возможна устойчивая работа между соседними этажами через железобетонное перекрытие.

Совместимость

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

Исходный код

Лежит тут https://github.com/olegv142/esp32-ble

Директория проекта для ардуйно ble_uart_mx

© Habrahabr.ru