Максим Данилин, 15.03.2023

Bluetooth-связь между Flipper Zero и Arduino

Привет!

В нашей прошлой статье мы рассмотрели основы создания простых приложений для Flipper Zero.

В этот раз мы воспользуемся Bluetooth-возможностями Флиппера и напишем Bluetooth-приложение. Мы научимся изменять прошивку Flipper Zero под себя, отлаживать приложения через Flipper CLI, разберёмся в структуре протокола BLE, научимся работать с BLE-сервисами и характеристиками, используя Bluetooth-модуль HM-10.

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

Это Важно! Проект Flipper Zero активно развивается, и на момент выхода данной статьи официальной документации по созданию собственных приложений нет, а сам API полностью не описан. Вполне возможно, что со временем API изменится, и код из этой статьи станет неактуальным. Статья актуальна для прошивки Флиппера 0.78.1.

Описанный в статье код приложений — своего рода «костыль» для организации связи между Arduino и Флиппером. Но, тем не менее, код рабочий, и такие приложения вполне имеют право на жизнь, пока документация и прошивка Флиппера находятся в разработке.

Как обычно, весь исходный код приложений, а также Arduino-код мы выложили в репозитории flipperzero-examples на GitHub.

Модуль BLE

Bluetooth-функциональность на Flipper Zero встроена в главный микроконтроллер STM32WB55RG. На борту чипа — модуль Bluetooth Low Energy (или сокращённо BLE) спецификации 5.3.

Технология Low Energy отличается от более ранних версий Bluetooth, поэтому вашей Arduino понадобится полноценный BLE-модуль для связи с Флиппером.

В этом проекте для общения с Флиппером мы будем использовать Troyka-модуль BLE.

ble_irl

Этот Troyka-модуль состоит из очень популярного в народе модуля HM-10 от Jinan Huamao и дополнительной обвязки с более удобными Troyka-контактами для подключения. Сердце модуля HM-10 — чип CC2541 от компании Texas Instruments с Bluetooth Low Enery спецификации 4.0. Даже без управляющей платы модуль HM-10 — мощное устройство с собственными пинами GPIO, способное к автономной работе.

Модуль HM-10 управляется по интерфейсу UART с помощью АТ-команд. Для работы с BLE-модулем, его настройки и прошивки крайне удобно использовать преобразователь USB-UART. Мы будем использовать USB-UART-преобразователь в формате Troyka-модуля на базе чипа CP2102, но вам подойдёт любой USB-UART, который есть под рукой.

Возможно, пригодится! Если вы планируете использовать такой же преобразователь на чипе CP2102 в системе Windows 7–10, вам может понадобиться драйвер CP2102 для Windows.

Подключаем BLE-модуль к преобразователю USB-UART. Пин TX модуля к пину RX преобразователя, пин RX модуля к пину TX преобразователя; земля к земле, питание к питанию.

ble_and_usb

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

Следующий шаг — установка самой свежей прошивки для вашего BLE-модуля HM-10.

Это Важно! Для связи с Flipper Zero вам понадобится самая свежая прошивка версии V709 или выше! Если вы уже обзавелись модулем HM-10 и никогда его не обновляли, его прошивка наверняка окажется устаревшей.

Прошивка модуля HM-10 обновляется программой HMSoft от производителя. Скачать свежую прошивку можно с сайта Jinan Huamao (возможно, понадобится VPN), или же можете скачать архив с нашего сайта.

Это важно! Программа для прошивки модуля доступна только под операционные системы Windows.

В среде Windows подключаемся к BLE-модулю HM-10 через терминал Bray++. Выбираем нужный нам COM-порт USB-UART-преобразователя. Устанавливаем параметры соединения:

  • Скорость соединения 9600 бит/с.
  • Бит чётности — none.
  • Количество бит данных — 8.
  • Количество стоповых бит — 1.

Проверить работоспособность модуля можно командой AT. Если модуль работает исправно, он ответит OK. Для удобства чтения ответов AT-команды лучше отправлять с управляющими символами возврата каретки \r (от англ. CR — Carriage Return) и новой строки \n (от англ. NL — New Line). Если достучаться до модуля не удаётся, попробуйте изменить скорость передачи на 115200 бит/с.

Проверить версию прошивки модуля можно командой AT+VERR?. Прошивка нашего модуля устарела — HMSoft V606.

bray_1

Переведите модуль в режим прошивки. Это можно сделать, отправив модулю команду AT+SBLUP. При переходе в режим прошивки светодиод-индикатор на плате модуля начнёт непрерывно тускло гореть.

Прервите последовательное соединение с модулем в терминале и запустите программу HMSoft для прошивки модуля. В окне программы выберите путь до бинарного (.bin) файла прошивки.

win_1

Выберите нужный COM-порт преобразователя USB-UART и нажмите на кнопку Load Image. Запустится процесс загрузки прошивки в модуль HM-10.

win_2

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

bray_2

Мы установили прошивку версии V710.

Организация BLE-соединения

В принципе, ось Windows нам больше не нужна. Перейдём на Linux, учитывая, что приложение для Флиппера мы будем писать именно там. Как и в предыдущей статье, посвящённой программированию под Flipper Zero, мы используем настольный компьютер под управлением Ubuntu 22.04.1 LTS (Jammy Jellyfish).

Прежде всего, склонируйте себе исходный код свежей прошивки Flipper Zero, если не делали этого раньше. А затем соберите прошивку, используя fbt (Flipper Build Tool).

cd ~
sudo apt update && sudo apt install openocd clang-format-13 dfu-util protobuf-compiler
git clone --recursive https://github.com/flipperdevices/flipperzero-firmware.git
cd flipperzero-firmware
brew bundle --verbose
./fbt

Для ликбеза немного окунёмся в теорию BLE-связи. Взглянем на схему протокола BLE и расшифруем, что есть что.

layer_model

  • PHY (Physical Layer) — физический слой. Отвечает за радиоканал устройства.
  • LL (Link Layer) — содержит всю последовательность байтов в передаваемом сообщении.
  • HCI (Host Controller Interface) — протокол обмена между слоями или микросхемами BLE, если Controller и Host реализованы на разных чипах.
  • L2CAP (Logical Link Control and Adaptation Protocol) — слой, отвечающий за формирование пакетов, деление на кадры, контроль ошибок и сборку пакетов.
  • SMP (Security Manager Protocol) — протокол, отвечающий за шифрование пакетов.
  • GAP (Generic Access Profile) — профиль, который отвечает за первоначальный обмен данными между устройствами.
  • GATT (Generic ATTribute Profile) — профиль, который определяет способ обмена данными между двумя BLE-устройствами, используя концепцию сервисов (Services) и характеристик (Characteristics).

Из всех этих компонентов нас будут интересовать только GAP и GATT.

Профиль GAP делает ваше BLE-устройство видимым для внешнего мира и определяет, как два устройства могут (или не могут) взаимодействовать друг с другом. Прежде всего данный профиль устанавливает тип соединения.

Устройства BLE могут общаться друг с другом двумя способами: «Broadcaster + Observer» или «Central + Peripheral».

Соединение «Broadcaster + Observer» более редкое. При таком типе соединения Broadcaster — это какой-нибудь датчик, например температуры или влажности. Этот датчик посылает периодические сообщения, которые прослушивает Observer. При этом Broadcaster не знает, прослушивает ли его кто-нибудь в данный момент или нет.

Соединение «Central + Peripheral» более распространённое. Периферия (Peripheral) — это небольшие устройства с низким энергопотреблением и ограниченными ресурсами, которые могут подключаться к гораздо более мощному центральному устройству. Периферийным устройством может быть, например, монитор сердечного ритма или бесконтактная метка с поддержкой BLE. Центральное устройство (Central) — это устройство с гораздо большей вычислительной мощностью и памятью. Обычно центральным устройством является мобильный телефон или планшет. При соединении «Central + Peripheral», устройство Central обнаруживает устройство Peripheral и инициирует соединение. После успешного соединения Central берёт на себя роль управления соединением и его временными интервалами.

В профиле GAP могут быть реализованы два способа передачи данных: Advertising Data или Scan Response. При обоих способах передачи полезная нагрузка в одном пакете идентична — до 31 байта данных.

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

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

В этом проекте центральным устройством станет BLE-модуль HM-10, а сам Флиппер будет периферийным устройством. Данные же будут передаваться обоими методами.

Почему периферийное устройство — именно Флиппер, ведь он намного мощнее? Всё дело в его прошивке. На текущий момент Bluetooth API для создания собственных BLE-приложений для Flipper Zero довольно ограничен. Пока что Флиппер может выступать только в роли периферийного устройства.

В прошивке Флиппера есть два GAP-профиля (см. исходный файл прошивки furi_hal_bt.c):

  • FuriHalBtProfileHidKeyboard — профиль, при котором Flipper Zero выступает как HID-устройство ввода (мышь, клавиатура, кликер, презентёр и т. д.). Данный профиль используется в приложении Remote, которое находится в меню «Applications → Bluetooth». Соответствует BtProfileHidKeyboard в Bluetooth API.
  • FuriHalBtProfileSerial — профиль для организации соединения Flipper Zero с мобильными приложениями. Соответствует BtProfileSerial в Bluetooth API.

Это важно! Казалось бы, Serial-профиль — это то, что нам нужно для связи с Ардуино, но это не так. Данный профиль лишь называется так же, как в Arduino. В действительности он отличается от привычного нам последовательного интерфейса.

В Flipper Zero реализована технология удалённого вызова процедур или RPC (Remote Procedure Call). С помощью этой технологии мы можем управлять Флиппером или получать с него данные, используя приложения, построенные на других платформах. Например, мы можем подключиться к Флипперу с компьютера через приложение qFlipper по USB-кабелю. Или же можем использовать мобильные приложения под разные мобильные ОС и Bluetooth-связь. RPC существенно расширяет функциональность приложений. С помощью RPC мы можем, например, обновить прошивку Флиппера по Bluetooth, транслировать экран Флиппера на компьютере или удалённо работать с файловой системой Флиппера.

Программное обеспечение RPC нуждается в транспортном уровне для передачи данных. При подключении Флиппера к компьютеру транспортом выступает последовательный интерфейс Serial через USB. А если Флиппер подключается к мобильному приложению по Bluetooth, то транспортом является сервис профиля FuriHalBtProfileSerial. Данный профиль имеет в своем составе сервис последовательного интерфейса SerialSvc (см. исходный файл serial_service.c), но используется он для обмена RPC-командами.

В обоих профилях для соединения Флиппера с другими BLE-устройствами используются Pairing и Bonding.

Pairing (Сопряжение) — процесс, при котором устройства обмениваются информацией, необходимой для установки зашифрованного соединения. Это включает в себя проверку подлинности двух сопрягаемых устройств и само шифрование соединения. Проверка связывающихся устройств осуществляется аутентификацией по шестизначному паролю (pass) или ключу (key). Наверняка вы хоть раз видели всплывающее окошко с шестью рандомными цифрами на вашем телефоне при попытке Bluetooth-соединения. Так же делает и Флиппер.

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

Это очень важно! На данном этапе мы встречаем проблему. Дело в том, что пароль аутентификации в прошивке Флиппера генерируется рандомно. При каждом новом сопряжении — новое число. На BLE-модуле HM-10 мы тоже можем установить шестизначный пароль специальной AT-командой. Однако функциональность модуля HM-10 и набор управляющих AT-команд ограничены. Мы не можем получить AT-командами из чипа CC2541 динамический пароль, полученный от Флиппера, или сказать модулю HM-10: «Подожди, я сперва посмотрю, какой там пароль!» Модуль HM-10 запросит соединение у Флиппера, и если их пароли не совпадут сразу, соединение прервётся.

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

Заходим в директорию с прошивкой Флиппера flipperzero-firmware и находим в ней исходный С-файл gap.c в папке firmware/targets/f7/ble_glue.

Находим в этом файле обработчик события EVT_BLUE_GAP_PASS_KEY_REQUEST и меняем рандомную генерацию пароля на какой-нибудь постоянный пароль. Пусть Bluetooth-паролем нашего Флиппера будет 123456.

case EVT_BLUE_GAP_PASS_KEY_REQUEST: {
    // Generate random PIN code
    // uint32_t pin = rand() % 999999; //-V1064
    uint32_t pin = 123456;

Готово! Но теперь прошивка нашего Флиппера изменилась, и нам нужно пересобрать её. Переходим в корневую директорию прошивки flipperzero-firmware и запускаем Flipper Build Tool:

cd ~/flipperzero-firmware
./fbt

Теперь зальём нашу модифицированную прошивку в Флиппер. Прошивку можно залить по USB или через программатор ST-Link (ведь внутри Флиппера чип STM32).

Если вы хотите попробовать ST-Link, подключите программатор к пинам SWC, SIO, GND на корпусе Флиппера, а сам программатор к USB-порту компьютера. Загрузка прошивки в этом случае выполняется командой ./fbt flash.

Если хотите загрузить прошивку по USB, просто подключите Флиппер к компьютеру через разъём Type-C и воспользуйтесь командой:

./fbt flash_usb

Дождитесь окончания прошивки Флиппера.

Теперь попробуем подключиться к Флипперу, используя BLE-модуль HM-10.

Сперва настроим Flipper Zero: переходим в раздел «Settings → Bluetooth» и включаем Bluetooth. Перед этим можно очистить список всех привязанных (Bonded) устройств, выбрав опцию Forget All Paired Devices.

После включения Bluetooth профиль FuriHalBtProfileSerial будет выставлен по умолчанию. То есть Флиппер будет ожидать подключение именно от мобильного приложения.

flipper_1.png

Для отладки BLE-соединения и наблюдения за ходом процесса мы будем использовать Flipper Command Line Interface (Flipper CLI). Это крайне полезная утилита для общения с Флиппером без графического интерфейса через командую строку. Она обязательно пригодится, если вы будете писать любые собственные приложения для Flipper Zero.

На Флиппере переходим в раздел «Settings → System», ищем параметр Log Level и устанавливаем его значение в Debug:

flipper_2.png

Подключаем Flipper Zero к компьютеру USB-кабелем. Открываем новое окно терминала, переходим в корневую директорию прошивки Флиппера и запускаем Flipper CLI:

cd ~/flipperzero-firmware
./fbt cli

В появившемся клиенте вводим команду log. Теперь вы можете отслеживать в терминале всё, что происходит на Флиппере. Мы будем наблюдать за Bluetooth-соединением, так что оставляем терминал открытым.

fbt_cli_1

Теперь настроим BLE-модуль. Подключаем модуль к компьютеру через преобразователь USB-UART. Для отправки AT-команд будем использовать очень удобный графический терминал Cutecom. Запустим его в новом окне терминала:

cutecom

Внимательно изучите документацию на BLE-модуль HM-10. Список всех AT-команд модуля можно найти в архиве документации от производителя на официальном сайте или скачать с нашего сайта, если официальный недоступен.

Отправляем в модуль следующие AT-команды. Не забываем управляющие символы CR и NL.

  • AT — проверяем, что модуль работает. Получаем ответ ОК.
  • AT+RENEW — откатываем модуль к заводским настройкам. Получаем ответ OK+RENEW. Обратите внимание, что на прошивке модуля версии 7** скорость последовательного соединения будет по умолчанию выставлена на 115200 бит/с.
  • AT+MODE2 — устанавливаем режим работы модуля в 2. Получаем ответ OK+Set:2. Это режим «Remote control mode», где мы можем настраивать модуль AT-командами как до установки соединения, так и после. Плюс после установки соединения можно получать сырые данные по UART.
  • AT+IMME1 — устанавливаем тип работы модуля в 1. Получаем ответ OK+Set:1. Если устройство является центральным (Central), то при типе работы 1 модуль не начнёт работу, пока мы напрямую не отправим ему AT-команду к соединению. Если же тип работы выставлен в 0, то после подачи питания модуль начнёт автоматическое обнаружение и подключение к доступным или последним BLE-устройствам.
  • AT+ROLE1 — устанавливаем роль устройства в 1. Получаем ответ OK+Set:1. Здесь 1 соответствует центральному устройству (Central), а 0 — периферийному (Peripheral).
  • AT+TYPE3 — устанавливаем тип сопряжения (Pairing) и привязки (Bonding) в 3. Получаем ответ OK+Set:3. Тип 3 соответствует сопряжению устройств с аутентификацией по шестизначному паролю с последующей привязкой. Настройки привязанных устройств будут храниться в энергонезависимой памяти модуля HM-10. Если настройки вашего соединения изменились и вам нужно удалить привязанные устройства, очистьте энергонезависимую память AT-командой AT+ERASE.
  • AT+PASS123456 — задаём шестизначный пароль для аутентификации. В прошивке Флиппера мы установили статичный пароль 123456. Ставим его и здесь.
  • AT+NOTI1 — устанавливаем оповещения о состоянии соединения в 1. Если оповещения включены, мы будем получать от модуля информацию, если вдруг подключение пропадёт или изменится.
  • AT+RESET — перезагружаем BLE-модуль HM-10.

cutecom_1

Такие временные параметры и интервалы, как AT+TCON?, AT+COMI?, AT+COMA?, AT+COLA?, AT+COSU? оставляем по умолчанию. Все дефолтные временные значения модуля HM-10 хоть и вызывают Warning, но вполне устраивают наш Флиппер.

Запустим на модуле процесс сканирования BLE-устройств. Делается это AT-командой AT+DISC?. Ответом на команду будет список всех найденных периферийных BLE-устройств, их имена, их MAC-адреса и уровень радиосигнала до них (RSSI). Ответ OK+DISCE сообщает о завершении сканирования.

cutecom_2

Модуль HM-10 обнаружил наш Флиппер под именем Flipper Aebri0 с MAC-адресом 80:E1:26:12:D4:FA.

Наконец, подключимся к Флипперу. Для периферийных устройств со статическим MAC-адресом подключение осущевляется AT-командой AT+CON<P1>, где <P1> — это MAC-адрес устройства. Существуют и другие команды для подключения, но мы будем пользоваться именно этой. При желании можете самостоятельно изучить datasheet на модуль HM-10.

Подключаемся к Флипперу по MAC-адресу. Сообщение OK+CONNA от модуля говорит нам о попытке подключения, а сообщение OK+CONN — что подключение установлено. Сообщения OK+CONNF и OK+CONNE говорят об ошибке или неудачной попытке соединения. В этом случае ещё раз проверьте все настройки модуля и Флиппера.

cutecom_3

Если подключение было удачно, то в Flipper CLI мы увидим целую гору полезных логов:

fbt_cli_2

Некоторые записи в логах имеют разные цвета, а также идентификаторы типа [D]. Данные идентификаторы показывают уровень детализации лога. Логирование во Flipper CLI имеет следующие уровни детализации:

  • I (Info) — информационное сообщение.
  • W (Warning) — сообщение-предупреждение.
  • E (Error) — сообщение об ошибке.
  • D (Debug) — сообщение отладки. Используется в процессе тестирования приложений и прочего ПО.
  • T (Trace) — самое детальное сообщение. Используется для мониторинга самых низкоуровневых процессов Флиппера, например обращений к участку памяти по адресу.

В нашем логе видно, что после подключения Flipper Zero создал для нас новую RPC-сессию и готов к дешифровке команд.

На самом же экране Флиппера значок Bluetooth-соединения в левом верхнем углу изменится на новый:

flipper_3.png

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

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

Профиль GATT — это надстройка над протоколом атрибутов Attribute Protocol (ATT). Данный протокол описывает, каким образом данные организованы на BLE-сервере и какие существуют методы для их чтения и записи. Этот протокол даёт нам такие понятия, как сервисы (Services), характеристики (Characteristics), таблица атрибутов, UUID.

Есть хорошая аналогия, которая объясняет структуру протокола. Представьте себе, что BLE-устройство — это большой книжный шкаф с полками. На каждой полке представлены книги конкретной тематики, например фантастика или детективы. Кроме этого у шкафа есть картотека, по которой мы можем быстро найти, где стоит нужная книга, не копаясь в шкафу. Так вот, сам шкаф с книгами — это наш GATT-профиль. Полки — это доступные нам сервисы (Services). Книги на каждой полке — характеристики (Characteristics) этого сервиса. Картотека книг — таблица атрибутов.

Описание сервисов, характеристик и прочих связанных данных профиля хранится в таблице атрибутов. Чтобы было удобно обращаться к конкретному сервису или характеристике, всё, что есть в таблице атрибутов, проиндексировано. Для индексации используется универсальный уникальный идентификатор или UUID (от англ. Universally Unique ID). UUID представляет собой код длиной в 16 или 128 бит. 16-битные UUID используются для официально принятых BLE-служб, а 128-битные для всех прочих пользовательских служб.

Сам атрибут таблицы — некое дискретное значение, которое состоит из нескольких полей:

  • Указатель атрибута (Attribute Handle) — это простой порядковый номер записи в таблице. Занимает 2 байта.
  • Тип атрибута (Attribute Type) — это UUID, который описывает его тип. Занимает 2 байта или 16 байт в зависимости от типа.
  • Значение атрибута (Attribute Value) — это данные, индексируемые указателем атрибута. Данные занимают произвольное количество байт.
  • Разрешения атрибутов (Attribute Permissions) — часть атрибута, которая описывает разрешения на чтение и запись данных. Поле занимает разное число байт в зависимости от имплементации.

Используя модуль HM-10, посмотрим, какие сервисы (Services) нам предоставляет Bluetooth-профиль Флиппера. Для этого воспользуемся AT-командой AT+FINDSERVICES?.

cutecom_4

Ответом на команду будет список доступных сервисов. Каждая строка состоит из трёх полей, разделённых :.

  • Первое поле, 2 байта, начальный указатель сервиса (Start Handle).
  • Второе поле, 2 байта, конечный указатель сервиса (End Handle).
  • Третье поле, 16 или 128 бит, UUID сервиса.

Нам доступно пять сервисов. Четыре из них имеют 16-битный идентификатор и один 128-битный. 16-битные индентификаторы официально регламентированы.

При желании вы можете взглянуть на список и назначение идентификаторов в PDF, составленный Bluetooth SIG.

Проверив идентификаторы из таблицы, мы можем узнать назначение сервисов:

  • 0x1800 — Generic Access service.
  • 0x1801 — Generic Attribute service.
  • 0x180A — Device Information service.
  • 0x180F — Battery service.

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

В прошивке Flipper Zero данные сервисы имплементированы в исходных файлах dev_info_service.c и battery_service.c соответственно.

Последний сервис 8FE5B3D5-2E7F-4A98-2A48-7ACC60FE0000 в таблице — это как раз сервис последовательного интерфейса SerialSrv, который используется как транспорт для RPC. И действительно, мы можем убедиться в этом, взглянув на исходный файл serial_service.c в прошивке Флиппера:

static const uint8_t service_uuid[] =
    {0x00, 0x00, 0xfe, 0x60, 0xcc, 0x7a, 0x48, 0x2a, 0x98, 0x4a, 0x7f, 0x2e, 0xd5, 0xb3, 0xe5, 0x8f};

Посмотрим, какие характеристики (Characteristics) доступны нам в том или ином сервисе.

Это делается AT-командой AT+CHAR<P1><P2>?, где <P1> — это начальный указатель сервиса (Start Handle), а <P2> — конечный указатель сервиса (End Handle). Эти указатели мы получили предыдущей командой.

Для примера взглянем на характеристики сервиса аккумулятора (0x180F). Отправим в модуль HM-10 команду AT+CHAR0017001D?.

cutecom_5

Ответом на команду будет список характеристик сервиса. Каждая строка состоит из трёх полей, разделённых :.

  • Первое поле, 2 байта, указатель характеристики (Characteristic Handle).
  • Второе поле, 14 бит, свойства характеристики (Characteristic properties) или её разрешения.
  • Третье поле, 16 или 128 бит, UUID характеристики.

Характеристика может иметь следующие свойства:

  • RD — Read. Если установлено, позволяет клиентам читать эту характеристику.
  • WR — Write. Если установлено, разрешает клиентам использовать запись характеристики. Получает ответ-подтверждение от сервера.
  • WN — Write-Without-Response. Запись характеристики без подтверждения.
  • NO — Notify. Если установлено, разрешает уведомления о значении характеристики без специального подтверждения от сервера.
  • IN — Indicate. Если установлено, разрешает индикацию значения характеристики с ответом-подтверждением.

Существуют и другие свойства, но на Bluetooth-модуле HM-10 нам доступны только эти.

UUID-характеристики также можно посмотреть в списке назначения идентификаторов в PDF.

Как видно, в сервисе 0x180F нам доступны две характеристики.

  • Характеристика 0x2A1A — Battery Power State со свойствами чтения и уведомления.
  • Характеристика 0x2A19 — Battery Level со свойствами чтения и уведомления.

Получим значение характеристики.

Это делается AT-командой AT+READDATA<P1>?, где <P1> — это указатель характеристики (Characteristic Handle).

Для примера взглянем на значение характеристики уровня заряда аккумулятора (0x2A19). Отправим в модуль HM-10 команду AT+READDATA0019?.

cutecom_6

В ответе мы получили один ASCII-байт d или 0x64, что в десятеричной системе соответствует числу 100. То есть заряд аккумулятора нашего Флиппера максимальный — 100%.

При желании можете сами изучить другие сервисы и характеристики, а мы двинемся дальше.

Изучим сервис последовательного интерфейса 8FE5B3D5-2E7F-4A98-2A48-7ACC60FE0000. Взглянем на доступные характеристики:

cutecom_7

Характеристика 19ED82AE-ED21-4C9D-4145-228E62FE0000 используется для записи данных, а характеристика 19ED82AE-ED21-4C9D-4145-228E61FE0000 для чтения.

Заглянув в прошивке Флиппера в исходный файл сервиса serial_service.c, мы можем увидеть как раз именно эти идентификаторы.

static const uint8_t char_tx_uuid[] =
    {0x00, 0x00, 0xfe, 0x61, 0x8e, 0x22, 0x45, 0x41, 0x9d, 0x4c, 0x21, 0xed, 0xae, 0x82, 0xed, 0x19};
static const uint8_t char_rx_uuid[] =
    {0x00, 0x00, 0xfe, 0x62, 0x8e, 0x22, 0x45, 0x41, 0x9d, 0x4c, 0x21, 0xed, 0xae, 0x82, 0xed, 0x19};

Попробуем записать какие-нибудь данные в характеристику, а затем прочесть их.

Для записи данных в характеристику используется AT-команда AT+SEND_DATA<P1><P2><DATA>, где <P1> — это указатель характеристики (Characteristic Handle), <P2> — свойство характеристики, а <DATA> — это, собственно, данные. Для чтения характеристики используется уже знакомая AT-команда AT+READDATA<P1>?.

Попробуем установить значение в характеристику с указателем 0x0021, а затем прочесть её. Пусть значением будет строка из четырёх символов ABCD. Отправим данные без уведомления от сервера WN. Данные отправляем в формате ASCII, и вместе с контрольными символами CR и NL значение характеристики займет 6 байт.

Команда для установки характеристики будет выглядить так: AT+SEND_DATAWN0021ABCD, а команда для чтения: AT+READDATA0021?.

cutecom_8

Таким образом мы изменили значение характеристики. В логах терминала Flipper CLI мы увидим, что BLE-сервис SerialSvc Флиппера получил наши 6 байт.

fbt_cli_3

Однако больше никакой информации об отправленной строке нет. Дело в том, что эти данные были получены и отправлены на дешифровку в RPC-поток и как бы застряли там, потому что строка ABCD не является никакой командой.

Решить эту проблему можно модификацией прошивки и написанием собственного FAP-приложения для Flipper Zero.

Пишем FAP-приложение

Перед тем как писать приложение, нам нужно разобраться с RPC.

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

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

Решением может быть создание копии профиля FuriHalBtProfileSerial, но под другим именем. Таким образом мы сохраним как текущий GAP-профиль, так и GATT-профиль (сервисы и характеристики), но обойдём использование профиля в сессии RPC.

Для введения нового профиля нам придётся изменить прошивку Флиппера под себя — благо много правок вносить не нужно.

Назовем наш новый профиль символично: BtProfileArduino.

Сперва откроем заголовочный файл bt.h (applications/services/bt/bt_service/). В нём меняем перечисление профилей BtProfile, вводим новый профиль BtProfileArduino под индексом 2:

typedef enum {
    BtProfileSerial,
    BtProfileHidKeyboard,
    BtProfileArduino,
} BtProfile;

Таким же образом вводим новый профиль в FURI HAL Флиппера.

Открываем заголовочный файл furi_hal_bt.h (firmware/targets/furi_hal_include/). В нём меняем перечисление профилей FuriHalBtProfile и вводим новый профиль с аналогичным именем FuriHalBtProfileArduino под индексом 2:

typedef enum {
    FuriHalBtProfileSerial,
    FuriHalBtProfileHidKeyboard,
    FuriHalBtProfileArduino,

    // Keep last for Profiles number calculation
    FuriHalBtProfileNumber,
} FuriHalBtProfile;

Затем открываем файл bt.с (applications/services/bt/bt_service/). Здесь нас интересует функция смены BLE-профилей bt_change_profile. В этой функции добавляем ветку условия для переключения на наш новый профиль:

FuriHalBtProfile furi_profile;
if(message->data.profile == BtProfileHidKeyboard) {
    furi_profile = FuriHalBtProfileHidKeyboard;
} else if (message->data.profile == BtProfileSerial) {
    furi_profile = FuriHalBtProfileSerial;
} else {
    furi_profile = FuriHalBtProfileArduino;
}

Наконец, опишем конфигурацию нового профиля. Открываем файл furi_hal_bt.c (firmware/targets/f7/furi_hal/). Находим описание профилей FuriHalBtProfileConfig profile_config (36 строка). Добавляем конфигурацию нашего профиля FuriHalBtProfileArduino, полностью скопировав её с FuriHalBtProfileSerial:

FuriHalBtProfileConfig profile_config[FuriHalBtProfileNumber] = {
    [FuriHalBtProfileSerial] =
        {
            .start = furi_hal_bt_serial_start,
            .stop = furi_hal_bt_serial_stop,
            .config =
                {
                    .adv_service_uuid = 0x3080,
                    .appearance_char = 0x8600,
                    .bonding_mode = true,
                    .pairing_method = GapPairingPinCodeShow,
                    .mac_address = FURI_HAL_BT_DEFAULT_MAC_ADDR,
                    .conn_param =
                        {
                            .conn_int_min = 0x18, // 30 ms
                            .conn_int_max = 0x24, // 45 ms
                            .slave_latency = 0,
                            .supervisor_timeout = 0,
                        },
                },
        },
    [FuriHalBtProfileHidKeyboard] =
        {
            .start = furi_hal_bt_hid_start,
            .stop = furi_hal_bt_hid_stop,
            .config =
                {
                    .adv_service_uuid = HUMAN_INTERFACE_DEVICE_SERVICE_UUID,
                    .appearance_char = GAP_APPEARANCE_KEYBOARD,
                    .bonding_mode = true,
                    .pairing_method = GapPairingPinCodeVerifyYesNo,
                    .mac_address = FURI_HAL_BT_DEFAULT_MAC_ADDR,
                    .conn_param =
                        {
                            .conn_int_min = 0x18, // 30 ms
                            .conn_int_max = 0x24, // 45 ms
                            .slave_latency = 0,
                            .supervisor_timeout = 0,
                        },
                },
        },
    [FuriHalBtProfileArduino] =
        {
            .start = furi_hal_bt_serial_start,
            .stop = furi_hal_bt_serial_stop,
            .config =
                {
                    .adv_service_uuid = 0x3080,
                    .appearance_char = 0x8600,
                    .bonding_mode = true,
                    .pairing_method = GapPairingPinCodeShow,
                    .mac_address = FURI_HAL_BT_DEFAULT_MAC_ADDR,
                    .conn_param =
                        {
                            .conn_int_min = 0x18, // 30 ms
                            .conn_int_max = 0x24, // 45 ms
                            .slave_latency = 0,
                            .supervisor_timeout = 0,
                        },
                },
        },
};

Готово! Пересобираем изменённую прошивку. Переходим в корневую директорию прошивки flipperzero-firmware и запускаем Flipper Build Tool:

cd ~/flipperzero-firmware
./fbt

Загружаем обновлённую прошивку во Флиппер по USB:

./fbt flash_usb

Теперь создадим простое FAP-приложение для чтения и отправки данных.

Процесс создания FAP-приложений с нуля мы описали в предыдущей статье. Здесь мы рассмотрим особенности именно Bluetooth-приложения.

Мы назвали наше приложение bt_serial_example.

Исходный код можете найти в репозитории flipperzero-examples.

В обязательном файле-манифесте приложения application.fam в поле requires указываем bt. Тем самым говорим, что наше приложение планирует работать с Bluetooth-сервисом Флиппера. Приложение разместим в категории Misc.

App(
    appid="bt_serial_example",
    name="BT Serial example application",
    apptype=FlipperAppType.EXTERNAL,
    entry_point="bt_serial_example_app",
    cdefines=["APP_BT_SERIAL_EXAMPLE"],
    requires=[
        "bt",
    ],
    stack_size=1 * 1024,
    order=10,
    fap_icon="emoji_smile_icon_10x10px.png",
    fap_category="Misc",
)

Что будет делать наше приложение?

Пусть приложение выводит нам данные, полученные с BLE-сервиса последовательного интерфейса SerialSvc. То есть те данные, что находятся в RX-характеристике с UUID 19ED82AE-ED21-4C9D-4145-228E61FE0000. Это данные, которые мы будем отправлять с модуля HM-10. В то же время пусть приложение время от времени само публикует какие-нибудь данные в TX-характеристику с UUID 19ED82AE-ED21-4C9D-4145-228E62FE0000. Эти данные мы будем считывать на модуле HM-10.

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

Само же приложение постараемся сделать максимально простым, без графического интерфейса. Из кнопок Флиппера опишем работу только для кнопки «Назад», чтобы закрывать приложение. Для вывода информации будем использовать логирование во Flipper CLI.

Заголовочный файл нашего приложения bt_serial_example_app.h:

#pragma once

#include <furi.h>
#include <bt/bt_service/bt.h>
#include <gui/gui.h>

struct BtSerialExampleApp {
    Gui* gui;
    ViewPort* view_port;
    FuriMessageQueue* event_queue;
    Bt* bt;
    FuriTimer* timer;

    uint8_t byte_to_send;
};

typedef struct BtSerialExampleApp BtSerialExampleApp;

Посмотрим на то, что мы будем использовать в главной структуре приложения BtSerialExampleApp. Используем Gui и ViewPort для построения минимального интерфейса, который нам нужен для обработки кнопки. Очередь сообщений FuriMessageQueue для хранения сообщений событий кнопки. Bt для связи с потоком, отвечающим за Bluetooth в RTOS на Flipper Zero. byte_to_send — это, собственно, байт, который будем устанавливать в TX-характеристку. FuriTimer — таймер для изменения отправляемого байта через равные промежутки времени.

Теперь посмотрим на исходный код самого приложения:

#include <furi.h>
#include <furi_hal_bt.h>
#include <bt/bt_service/bt.h>
#include <input/input.h>

#include "bt_serial_example_app.h"

#define TAG "BtSerialExampleApp"

static uint16_t bt_serial_event_callback(SerialServiceEvent event, void* ctx) {
    UNUSED(ctx);

    if(event.event == SerialServiceEventTypeDataReceived) {
        FuriString* data_str;
        data_str = furi_string_alloc();
        for(size_t i = 0; i < event.data.size; i++) {
            furi_string_cat_printf(data_str, "0x%02X ", event.data.buffer[i]);
        }

        FURI_LOG_I(TAG, "Got data: %s", furi_string_get_cstr(data_str));
        furi_string_free(data_str);
    }

    return 0;
}

static void bt_serial_example_app_connection_status_changed_callback(BtStatus status, void* ctx) {
    furi_assert(ctx);
    BtSerialExampleApp* app = ctx;

    if(status == BtStatusConnected) {
        FURI_LOG_I(TAG, "Starting Timer!");
        furi_timer_start(app->timer, 1000);
    }
}

static void bt_serial_example_app_timer_callback(void* ctx) {
    furi_assert(ctx);
    BtSerialExampleApp* app = ctx;

    FURI_LOG_I(TAG, "Sending byte: %d!", app->byte_to_send);
    furi_hal_bt_serial_tx(&(app->byte_to_send), 1);
    app->byte_to_send++;
}

static void bt_serial_example_app_input_callback(InputEvent* input_event, void* ctx) {
    furi_assert(ctx);
    FuriMessageQueue* event_queue = ctx;
    furi_message_queue_put(event_queue, input_event, FuriWaitForever);
}

BtSerialExampleApp* bt_serial_example_app_alloc() {
    BtSerialExampleApp* app = malloc(sizeof(BtSerialExampleApp));

    app->gui = furi_record_open(RECORD_GUI);
    app->view_port = view_port_alloc();

    view_port_input_callback_set(
        app->view_port, bt_serial_example_app_input_callback, app->event_queue);
    gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen);

    app->event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));

    app->byte_to_send = 0;

    app->bt = furi_record_open(RECORD_BT);
    app->timer =
        furi_timer_alloc(bt_serial_example_app_timer_callback, FuriTimerTypePeriodic, app);

    return app;
}

void bt_serial_example_app_free(BtSerialExampleApp* app) {
    furi_assert(app);

    furi_record_close(RECORD_BT);
    app->bt = NULL;

    view_port_enabled_set(app->view_port, false);
    gui_remove_view_port(app->gui, app->view_port);
    view_port_free(app->view_port);
    app->view_port = NULL;

    furi_message_queue_free(app->event_queue);
    app->event_queue = NULL;

    furi_record_close(RECORD_GUI);
    app->gui = NULL;

    furi_timer_stop(app->timer);
    furi_timer_free(app->timer);
    app->timer = NULL;

    free(app);
}

int32_t bt_serial_example_app(void* p) {
    UNUSED(p);

    BtSerialExampleApp* app = bt_serial_example_app_alloc();

    bt_disconnect(app->bt);
    bt_set_profile(app->bt, BtProfileArduino);
    furi_hal_bt_serial_set_event_callback(128, bt_serial_event_callback, NULL);
    bt_set_status_changed_callback(
        app->bt, bt_serial_example_app_connection_status_changed_callback, app);
    furi_hal_bt_start_advertising();

    InputEvent event;

    while(1) {
        if(furi_message_queue_get(app->event_queue, &event, 100) == FuriStatusOk) {
            if(event.type == InputTypePress) {
                if(event.key == InputKeyBack) break;
            }
        }
    }

    furi_hal_bt_serial_set_event_callback(0, NULL, NULL);
    bt_disconnect(app->bt);

    bt_serial_example_app_free(app);

    return 0;
}

Разберёмся, как работает приложение.

Прежде всего, как и во всех FAP-приложениях, у нас есть функция для выделения памяти bt_serial_example_app_alloc() под все наши компоненты и освобождения памяти bt_serial_example_app_free(), когда приложение завершает работу.

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

app->gui = furi_record_open(RECORD_GUI);
app->view_port = view_port_alloc();

app->event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
view_port_input_callback_set(
app->view_port, bt_serial_example_app_input_callback, app->event_queue);
gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen);

При получении событий с кнопок Флиппера помещаем их в нашу очередь:

static void bt_serial_example_app_input_callback(InputEvent* input_event, void* ctx) {
    furi_assert(ctx);
    FuriMessageQueue* event_queue = ctx;
    furi_message_queue_put(event_queue, input_event, FuriWaitForever);
}

А в точке входа приложения bt_serial_example_app() создаём бесконечный цикл для обработки событий кнопок. Если нажата клавиша «Назад», выходим из бесконечного цикла и тем самым движемся к завершению работы приложения.

while(1) {
    if(furi_message_queue_get(app->event_queue, &event, 100) == FuriStatusOk) {
        if(event.type == InputTypePress) {
            if(event.key == InputKeyBack) break;
        }
    }
}

Всё это — минимальный, уже знакомый нам по предыдущей статье скелет приложения.

Теперь подробнее про Bluetooth. Рассмотрим, что происходит при запуске приложения после выделения памяти под всё.

Сначала мы отключаем наш Bluetooth. Пусть это будет своего рода перезагрузка, если вдруг Bluetooth-соединение с другим устройством активно в данный момент.

bt_disconnect(app->bt);

Устанавливаем Bluetooth-профиль GAP/GATT для нашего приложения. Здесь используем созданный нами профиль BtProfileArduino.

bt_set_profile(app->bt, BtProfileArduino);

Далее мы устанавливаем собственную логику обработки событий BLE-сервиса SerialSrv. Функцией furi_hal_bt_serial_set_event_callback() мы указываем на callback-функцию, которая будет выполняться при получении события от сервиса. В качестве передаваемого контекста не предаём ничего. Устанавливаем размер буфера для входных данных в 128 байт. Этого нам более чем достаточно.

Эта та самая функция, которая в совокупности с созданным ранее профилем BtProfileArduino позволяет нам читать данные с последовательного интерфейса, минуя RPC.

furi_hal_bt_serial_set_event_callback(128, bt_serial_event_callback, NULL);

В самой же callback-функции, если были получены новые данные (SerialServiceEventTypeDataReceived), формируем из них читабельную строку и выводим эту строку в терминал Flipper CLI макроcом FURI_LOG_I. Обратите внимание на используемый тип FuriString для работы со строками в Флиппере. Для данного типа в прошивке имеется огромный функционал с оптимальным использованием ресурсов.

static uint16_t bt_serial_event_callback(SerialServiceEvent event, void* ctx) {
    UNUSED(ctx);

    if(event.event == SerialServiceEventTypeDataReceived) {
        FuriString* data_str;
        data_str = furi_string_alloc();
        for(size_t i = 0; i < event.data.size; i++) {
            furi_string_cat_printf(data_str, "0x%02X ", event.data.buffer[i]);
        }

        FURI_LOG_I(TAG, "Got data: %s", furi_string_get_cstr(data_str));
        furi_string_free(data_str);
    }

    return 0;
}

Далее после указания callback-функции SerialSrv сервиса мы задаём callback-функцию изменения статуса Bluetooth-подключения bt_set_status_changed_callback(). Крайне полезная функция, которая поможет вам отследить момент установки и разрыва соединения между устройствами. Например, мы используем эту функцию для определения времени, когда мы можем начать отправлять наш исходящий байт в характеристику. В качестве контекста передаём указатель на структуру нашего приложения.

bt_set_status_changed_callback(app->bt, bt_serial_example_app_connection_status_changed_callback, app);

В самой callback-функции запускаем таймер timer с частотой в 1000 мс, если Bluetooth-соединение между устройствами успешно установлено (BtStatusConnected).

static void bt_serial_example_app_connection_status_changed_callback(BtStatus status, void* ctx) {
    furi_assert(ctx);
    BtSerialExampleApp* app = ctx;

    if(status == BtStatusConnected) {
        FURI_LOG_I(TAG, "Starting Timer!");
        furi_timer_start(app->timer, 1000);
    }
}

Этот таймер используется для отправки байта в TX-характеристику. В функции выделения памяти мы задаём указатель на callback-функцию таймера. Данная функция будет срабатывать при каждом тике таймера.

app->timer = furi_timer_alloc(bt_serial_example_app_timer_callback, FuriTimerTypePeriodic, app);

В нашем случае в callback-функции таймера мы отправляем байт byte_to_send в TX-характеристику сервиса SerialSrv. Помещаем данные в исходящий буфер функцией furi_hal_bt_serial_tx() с указанием количества отправляемых байтов. После отправки байта увеличиваем его значение на единицу, чтобы данные в характеристике менялись.

static void bt_serial_example_app_timer_callback(void* ctx) {
    furi_assert(ctx);
    BtSerialExampleApp* app = ctx;

    FURI_LOG_I(TAG, "Sending byte: %d!", app->byte_to_send);
    furi_hal_bt_serial_tx(&(app->byte_to_send), 1);
    app->byte_to_send++;
}

Когда все callback-функции в приложении установлены, мы запускаем рекламную рассылку пакетов, делая наш Флиппер доступным для обнаружения другими устройствами:

furi_hal_bt_start_advertising();

Когда приложение завершает работу, мы отключаем Bluetooth, очистив перед этим callback-функцию сервиса SerialSrv.

furi_hal_bt_serial_set_event_callback(0, NULL, NULL);
bt_disconnect(app->bt);

Компилируем приложение и загружаем его на Флиппер:

./fbt fap_deploy

Давайте протестируем приложение.

На компьютере запускаем Flipper CLI для мониторинга приложения.

На самом Флиппере запускаем созданное приложение из раздела «Applications → Misc».

flipper_4.png

Подключаем модуль HM-10 к Флипперу. В терминале Cutecom отправляем модулю AT-команду AT+CON<P1> на подключение по MAC-адресу.

cutecom_3

Сразу после подключения мы можем наблюдать логи нашего приложения в Flipper CLI. После установки соединения таймер приложения начнёт работу, и байт будет публиковаться в TX-характеристику с UUID 19ED82AE-ED21-4C9D-4145-228E62FE0000 с частотой в 1 секунду.

fbt_cli_4

Проверим полученный байт на стороне HM-10. Характеристика 19ED82AE-ED21-4C9D-4145-228E62FE0000 имеет указатель 0x0023. Мы можем прочесть значение характеристики командой AT+READDATA0023?. Попробуйте прочесть характеристику несколько раз и убедиться, что данные изменяются.

А теперь отправим данные с модуля HM-10 Флипперу. Используем RX-характеристику с UUID 19ED82AE-ED21-4C9D-4145-228E61FE0000. Характеристика имеет указатель 0x0021. Установим в качестве характеристики строку из четырёх ASCII-символов ABCD. Отправляем в модуль команду AT+SEND_DATAWR0021ABCD.

cutecom_9

В Flipper CLI среди логов нашего приложения мы можем видеть, что данные были получены. Полученные 6 байтов 0x41, 0x42, 0x43, 0x44, 0x0D, 0x0A как раз соответствуют строке ABCD с символами CR и NL.

fbt_cli_5

Всё работает!

Какими ещё функциями модуля HM-10 мы можем воспользоваться?

Мы можем установить стандартный путь отправки данных. Чтобы не вводить команду AT+SEND_DATA в терминал каждый раз, мы можем писать данные в терминал напрямую. Например, в нашем случае мы отправляем данные в характеристику с указателем 0x0021. Для установки стандартного пути используется AT-команда AT+SET_WAY<P1><P2>, где <P1> — это свойство характеристики а <P2> — указатель характеристики. Таким образом, отправив один раз команду AT+SET_WAYWR0021, мы сможем писать любые данные в терминал уже без команд.

Ещё мы можем настроить уведомления (Notifications). Например, сейчас наш таймер меняет значение байта в характеристике с указателем 0x0023 раз в секунду. Но что если мы хотим моментально получить данные, если характеристика изменилась, а не вводить команду чтения данных AT+READDATA? Для этого существуют уведомления. Для установки уведомления используется AT-команда AT+NOTIFY_ON<P1>, где <P1> — это указатель характеристики. Включим уведомления по характеристике 0x0023 командой AT+NOTIFY_ON0023. Если уведомление включено, мы можем включить индикацию. Индикация позволит свежим данным из изменившейся характеристики сразу поступать в терминал самостоятельно. Индикация включается AT-командой AT+INDICA_ON<P1>, где <P1> — это указатель характеристики. Попробуйте включить индикацию характеристики 0x0023 командой AT+INDICA_ON0023 и смотрите, как данные поступают в терминал с частотой, заданной нашим таймером.

cutecom_10

Выключить индикацию можно командой AT+INDICA_OFF<P1>. Все эти команды, транслирующие данные напрямую в Serial-интерфейс, призваны упросить для вас соединение модуля HM-10 с микроконтроллером.

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

Для того чтобы MAC-адреса успешно подключённых устройств сохранялись в памяти модуля, используется настройка AT+SAVE0. Также потребуется установить соответствующий тип работы модуля. Сделать это можно уже знакомой командой AT+IMME0. Если значение этой команды установлено в 0, то при подаче питания модуль не будет ждать управляющие команды и начнёт работу незамедлительно.

В нашем случае, если мы введём следующие команды-настройки в модуль, а затем после сканирования успешно подключимся к Флипперу по MAC-адресу 80:E1:26:12:D4:FA, то следующее подключение уже произойдёт автоматически.

cutecom_11

Дистанционное управление машинкой

Мы разобрались с BLE-приложениями на Flipper Zero. Теперь используем полученные знания для создания уже полноценного функционального приложения с графическим интерфейсом, в нашем случае для дистанционного управления машинкой.

Мы назвали приложение bluetooth_teleop и разместили его в разделе Misc:

flipper_5.png

Исходный код приложения мы разместили в репозитории flipperzero-examples на GitHub.

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

В приложении мы используем созданный нами Bluetooth-профиль BtProfileArduino. Приложение не читает значения характеристик, а только отправляет их. Для обмена данными используем ТХ-характеристику 19ED82AE-ED21-4C9D-4145-228E61FE0000 сервиса SerialSrv.

Для модуля HM-10 мы произвели окончательную настройку под наше FAP-приложение. Модуль читает значение характеристики 19ED82AE-ED21-4C9D-4145-228E61FE0000. Также мы включили нотификацию и индикацию данной характеристики и организовали автоподключение.

Полный набор отправленных в модуль AT-команд выглядит так: AT, AT+MODE2, AT+IMME1, AT+ROLE1, AT+TYPE3, AT+PASS123456, AT+NOTI000, AT+SAVE0, AT+IMME0, AT+DISC?, AT+CON80E12612D4FA, AT+NOTIFY_ON0023, AT+INDICA_ON0023.

При подаче питания модуль HM-10 начинает поиск Флиппера. При подключении к Флипперу модуль передаёт изменившиеся данные в характеристике 0x0023 сразу в терминал.

Приложение состоит из двух графических интерфейсов.

Первый интерфейс называется WaitConnection. Он активируется при запуске приложения и сообщает нам, что приложение ожидает Bluetooth-подключение. Для заставки мы выбрали забавную картинку с дельфинчиком из прошивки Флиппера. Также мы использовали нотификации Флиппера. При ожидании подключения светодиод быстро мигает жёлтым цветом.

flipper_6.png

Если подключение установлено, приложение переключает нас на второй графический интерфейс Control. При этом светодиод Флиппера станет непрерывно гореть синим цветом.

Данный интерфейс уже отвечает за обработку нажатия кнопок Флиппера и отсылку данных в характеристику сервиса.

Флиппер имеет курсор из четырёх кнопок «Вверх», «Вниз», «Влево» и «Вправо», которые, очевидно, можно использовать для задания направления движения Арудино-машинки.

Было бы неплохо динамически изменять скорость движения по четырём направлениям, но кнопки Флиппера не аналоговые. Поэтому для задания скорости мы решили использовать кнопку «ОК». Всего мы сделали 5 скоростей, которые переключаются нажатием на кнопку «ОК».

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

Одновременно можно нажимать любое количество кнопок.

flipper_multiple

Выход из приложения осуществляется нажатием на кнопку «Назад».

Если Bluetooth-соединение с модулем HM-10 прервётся, приложение переключит нас на первый графический интерфейс WaitConnection и снова будет ожидать подключение.

Какие данные передаёт приложение?

Все данные для нашей ТХ-характеристики уложились всего в 1 байт. В эту однобайтовую команду мы закодировали текущее состояние кнопок Flipper Zero и значение скорости. Это легко сделать, изменяя соответствующие биты байта. Четыре младших бита отданы под текущее значение скорости. Четыре старших бита — состояние четырёх кнопок Флиппера.

В общем виде команда выглядит так — 0bABCDEEEE, где:

  • A — бит состояния кнопки «Влево».
  • B — бит состояния кнопки «Вправо».
  • C — бит состояния кнопки «Вниз».
  • D — бит состояния кнопки «Вперёд».
  • EEEE — 4-битная переменная текущей скорости машинки.

Команды записываются в ТХ-характеристику по таймеру каждые несколько миллисекунд.

Теперь опишем саму Ардуино-машинку.

В качестве шасси машинки мы выбрали шасси робота Драгстер. Управляющая плата робота — Iskra Neo, наш аналог Arduino Leonardo. Плата удобна для нас тем, что у неё два аппаратных Serial-интерфейса.

На Драгстер мы установили Troyka Slot Shield. BLE Troyka-модуль подключён к контроллеру через Slot Shield по пинам последовательного интерфейса 0 и 1.

Получилась вот такая машинка:

car_irl

Arduino-код машинки максимально прост:

constexpr uint8_t EN_1_PIN = 5;
constexpr uint8_t EN_2_PIN = 6;
constexpr uint8_t DIR_1_PIN = 4;
constexpr uint8_t DIR_2_PIN = 7;

uint8_t cmd = 0x00;

void setup() {
    Serial1.begin(115200);
    pinMode(EN_1_PIN, OUTPUT);
    pinMode(EN_2_PIN, OUTPUT);
    pinMode(DIR_1_PIN, OUTPUT);
    pinMode(DIR_2_PIN, OUTPUT);
}

void parse_cmd(uint8_t cmd) {
    float linear = 0;
    float angular = 0;

    uint8_t speed = 51 * (cmd & 0x0F);

    if((cmd >> 7) & 1) angular -= 1;
    if((cmd >> 6) & 1) angular += 1;
    if((cmd >> 4) & 1) linear += 1;
    if((cmd >> 5) & 1) linear -= 1;

    float l = (linear - angular) / 2 * speed;
    float r = (linear + angular) / 2 * speed;

    digitalWrite(DIR_1_PIN, l > 0 ? 1 : 0);
    digitalWrite(DIR_2_PIN, r > 0 ? 0 : 1);
    analogWrite(EN_1_PIN, abs(l));
    analogWrite(EN_2_PIN, abs(r));
}

void loop() {
    if (Serial1.available()) {
        cmd = Serial1.read();
        parse_cmd(cmd);
    }
}

Опрашиваем последовательный интерфейс, к которому подключён Troyka-модуль BLE, на наличие новых команд (cmd) из характеристики сервиса. Если поступает новая команда, анализируем её биты.

Затем вычисляем векторы направления линейной скорости машинки linear и угловой скорости angular. Используя векторы направления и текущее значение скорости speed, формируем ШИМ-сигнал (в диапазоне [0, 255]) и направление вращения для правого мотора r и левого мотора l. Вычисленные ШИМ-сигналы отправляем на пины драйвера моторов.

Демонстрация работы дистанционного управления:

Заключение

На этот раз мы разобрались в принципах и особенностях работы Bluetooth-соединения между Flipper Zero и модулем HM-10. Для начала мы сделали тестовую программу, которая шлёт данные по Bluetooth LE, используя сервис последовательного соединения SerialSrv без RPC под свои нужды. А затем приспособили эту программу для дистанционного управления машинкой на Arduino клавишами Флиппера.

Воспользовавшись этими наработками, вы сможете написать своё приложение для Flipper Zero, которое превратит его в BLE-пульт дистанционного управления или позволит обмениваться данными по Bluetooth с вашими DIY-гаджетами!

Полезные ссылки