В первой части рассказывалось, как подключить Bluetooth геймпад к Arduino. Тогда были использованы немного устаревшие, хоть и до сих пор доступные, компоненты. Теперь пришло время разобраться, как сделать то же самое на платформе ESP32.
Эта статья разделена на две части, и, хотя задача везде одинаковая, способы ее реализации отличаются в зависимости от выбранной платформы.
Часть 1 (Arduino) – первая часть рекомендуется к прочтению, в ней рассказано, что такое USB Host Shield и как его использовать, эта библиотека будет нужна и на ESP32
Часть 2 (ESP32) – вы здесь
Далее будет много текста и ни одной картинки - почти вся вторая часть посвящена собственно процессу портирования библиотеки на ESP32. Поэтому, если вам нужен просто готовый код для встраивания в проект – перематывайте ниже, там будут ссылки на готовую библиотеку и пример использования.
Какой Bluetooth есть в ESP32
Если коротко, то в Bluetooth, помимо разных версий, есть еще и два отличающихся друг от друга стандарта:
Bluetooth Classic (BR/EDR)
Bluetooth Low Energy (BLE)
Подробнее о различиях этих стандартов Bluetooth можно почитать, например, в статье Bluetooth Low Energy: подробный гайд для начинающих. Некоторые устройства Bluetooth могут поддерживать только один из стандартов, а другие – оба одновременно (Dual mode Bluetooth).
Варианты ESP32 также бывают разные:
С поддержкой BR/EDR + Bluetooth LE v4.2 (те самые Dual mode)
С поддержкой только Bluetooth LE v5.0
Первые наиболее распространены, и если у вас уже есть ESP32, то с большой вероятностью в нем будет поддержка Dual mode и поэтому он подойдет для подключения геймпада, абсолютное большинство которых поддерживает исключительно Bluetooth Classic.
Bluetooth API в ESP32
Здесь пора сделать примечание о том, что разработку под ESP32 можно вести с использованием двух различных фреймворков (espruino в этот список я намеренно не включаю):
ESP-IDF (Espressif IoT Development Framework) – нативный для ESP32 фреймворк, который можно использовать из командой строки (CLI) либо через удобные плагины для IDE (VSCode и Eclipse)
ESP32 Arduino Core – Arduino-совместимая оболочка над ESP-IDF, позволяющая писать код в привычном для Arduino стиле и с использованием привычных функций из стандартных библиотек. ESP32 Arduino Core можно использовать как и с обычной Arduino IDE, так и с VSCode или PlatformIO
Второй вариант, в принципе, позволяет реализовать большинство наиболее частых сценариев для Arduino проектов, однако, следует учитывать, что по функциональности он все равно проигрывает первому варианту – далеко не все, что есть в ESP32, в принципе присутствует в виде стандартного Arduino API, поэтому иногда все равно придется использовать API из ESP-IDF, что приведет к коду в смешанном стиле. Мне, например, не очень симпатизирует такое соседство xTaskCreate и loop в одном проекте.
Поддержка же Bluetooth в ESP32 Arduino Core в основном сводится к классам для работы с BLE сервисами, а для Bluetooth Classic есть только реализация BluetoothSerial.
Поэтому, для полноценной работы с Bluetooth необходимо начинать разработку с использованием ESP-IDF. После привычной Arduino IDE, конечно, требуется некоторое время, чтобы настроить окружение, минимально изучить документацию и привыкнуть к особенностям многозадачной FreeRTOS, но все это совершенно не представляет сложности – ESP-IDF все равно остается достаточно высокоуровневым фреймворком, при использовании которого вам не придется разбираться в тонкостях регистров микропроцессора, а ваш код не будет наполовину состоять из ассемблерных вставок. Документация по ESP-IDF присутствует в достаточном объеме и качественном виде прямо от вендора.
Итак, какие возможности для работы с Bluetooth предоставляет ESP-IDF ?
Bluedroid стек, поддерживает как BLE, так и Bluetooth Classic
NimBLE стек, поддерживает только BLE
VHCI (Virtual Host Controller Interface) – низкоуровневый доступ к контроллеру Bluetooth при помощи HCI команд, это что-то типа стандартизированного API на уровне Bluetooth чипа
Хотя первые два стека и считаются высокоуровневыми Bluetooth API, для их применения вам придется детально разобраться во многих тонкостях Bluetooth, включая разнообразные GATT/GAP/SDP/L2CAP и так далее. Примеры (examples) в ESP-IDF, конечно, есть для всех перечисленных API, но примеры эти настолько абстрактные, что легче от них не становится – готового кода, чтобы взять и быстро подключить Bluetooth устройство к ESP32, нет.
Гугл подсказывает еще несколько возможных вариантов для решения задачи:
BlueKitchen BTstack – еще одна реализация BT стека, opensource, есть порты для множества процессорных архитектур (в том числе для ESP32), возможно бесплатное использование в некоммерческих целях. Эта версия Bluetooth API выглядит попроще, чем Bluedroid, есть даже готовый пример с подключением клавиатуры, из которого, наверное, можно сделать и более сложный вариант для геймпада. Такой вариант все равно не выглядит как быстрый
Есть проект на GitHub (https://github.com/aed3/PS4-esp32) – 300+ звезд, что неудивительно, поскольку это единственный работающий и готовый к использованию пример подключения DualShock 4 к ESP32. В этом примере присутствует хардкод BT-адреса устройства для подключения и нет полноценной поддержки сопряжения.
Второй проект (https://github.com/StryderUK/BluetoothHID) более продвинутый, но репозиторий явно заброшенный, требует замены файлов внутри SDK, и привязан к конкретной, уже старой, версии SDK, вести разработку на которой не хотелось бы. Как поведет себя этот пример с актуальной версией SDK, я не проверял. Разбираться с потенциальными вопросами обратной совместимости версий ESP-IDF – тоже вариант небыстрый.
Все описанные выше варианты реализации по быстроте и удобству вчистую проигрывают той старой библиотеке USB Host Shield 2.0, которая была использована в первой части статьи для классической платы Arduino. Поэтому я задумался, а есть ли способ просто перенести все те наработки по Bluetooth, которые там есть, на ESP32 ?
Портирование Bluetooth стека из USB Host Shield 2.0 на ESP32
Для начала я заглянул в исходники библиотеки. Основная часть исходного кода библиотеки ожидаемо реализует поддержку разнообразных USB устройств – проводные клавиатуры, мышки, флэшки, и так далее. Работа с Bluetooth сводится к подключению специального типа USB устройства – Bluetooth Dongle (BTD). Обработка пакетов данных для BTD завернута в своего рода state-машину, по сути, представляющую упрощенную реализацию Bluetooth стека, тем не менее достаточную для подключения простых BT устройств, включая геймпады. Главное, что я обнаружил - весь код обмена пакетами данных вдоль и поперек содержит аббревиатуру HCI. Для тех, кто давно работает с Bluetooth на низком уровне это, вероятно, очевидный вывод, но для меня это было находкой, означавшей, что есть возможность взять код от USB Host Shield 2.0 вместе со всей имеющейся там поддержкой BT-устройств, и перенести его на ESP32, просто заменив обмен HCI командами через USB контроллер на API-вызовы VHCI ESP32.
При более детальном изучении исходников обнаружилось пара особенностей:
Bluetooth state-машина сама вычитывает пакеты из входного буфера USB, то есть делает Polling, и сразу же выполняет обработку этих пакетов
Отправка исходящих пакетов в сторону BT устройства в библиотеке происходит непосредственно из state-машины, прямо внутри методов, которые заняты обработкой входящих пакетов.
Как оказалось, такая схема работы без изменений на ESP32 не переносится:
Механизма Polling в ESP32 нет, VHCI интерфейс доставляет входящие пакеты данных в callback-функцию.
Методы VHCI для отправки пакетов не работают, если их вызывать изнутри callback-функций получения данных
Чтобы полностью не переписывать Bluetooth state-машину, при портировании библиотеки на ESP32 я поступил следующим образом:
Callback-функция чтения входящих пакетов от BT не занимается их обработкой – вместо этого все пакеты сохраняются в отдельный ringbuffer (на самом деле в два разных буфера – один для HCI пакетов, второй для ACL пакетов).
Отдельный поток вычитывает пакеты уже из ringbuffer-ов и доставляет их в методы state-машины. В этом случае, когда обработчики начинают вызывать функции отправки данных, получается, что это происходит уже за пределами callback-ов чтения.
В итоге новая схема оказалась вполне работоспособной и быстродействующей. По крайней мере, ESP32 успевает обработать все 1000 пакетов в секунду, которые отправляет DualShock 4 с данными о состоянии кнопок и джойстиков геймпада.
Далее оставались косметические правки вроде замены отладочного логирования на принятую в ESP32 библиотеку esp_log, добавление недостающих #include и #define, после чего новый компонент (это название «библиотеки», принятое в ESP-IDF) был готов.
Компонент BTD_VHCI для ESP-IDF
Скачать готовый компонент можно отсюда - btd_vhci
Если у вас еще не установлен ESP-IDF, то это нужно сделать.
Скрытый текст
Ссылки для быстрого старта с ESP-IDF и VSCode:
Installation - https://github.com/espressif/vscode-esp-idf-extension/blob/master/docs/tutorial/install.md
Basic Use of the Extension - https://github.com/espressif/vscode-esp-idf-extension/blob/master/docs/tutorial/basic_use.md
FreeRTOS Overview - https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/freertos.html#using-freertos
Создание нового пустого проекта делается через F1: ESP-IDF: New Project, далее Choose Template, выбрать ESP-IDF (get-started и шаблон sample_project)
В проекте необходимо сделать необходимые настройки sdkconfig (делаются через F1: ESP-IDF: SDK Configuration Editor)
BluetoothD: Enabled
Host: Disabled
Controller: Enabled
Bluetooth controller mode: BR/EDR Only
BR/EDR Sync: HCI
HCI mode: VHCI
Скрытый текст
Необходимые опции в sdkconfig:
#
# Bluetooth
#
CONFIG_BT_ENABLED=y
CONFIG_BT_CONTROLLER_ONLY=y
CONFIG_BT_CONTROLLER_ENABLED=y
#
# Controller Options
#
CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=y
CONFIG_BTDM_CTRL_BR_EDR_MAX_ACL_CONN=2
CONFIG_BTDM_CTRL_BR_EDR_MAX_SYNC_CONN=0
CONFIG_BTDM_CTRL_BR_EDR_SCO_DATA_PATH_HCI=y
CONFIG_BTDM_CTRL_BR_EDR_SCO_DATA_PATH_EFF=0
CONFIG_BTDM_CTRL_PCM_ROLE_EFF=0
CONFIG_BTDM_CTRL_PCM_POLAR_EFF=0
CONFIG_BTDM_CTRL_LEGACY_AUTH_VENDOR_EVT=y
CONFIG_BTDM_CTRL_LEGACY_AUTH_VENDOR_EVT_EFF=y
CONFIG_BTDM_CTRL_BLE_MAX_CONN_EFF=0
CONFIG_BTDM_CTRL_BR_EDR_MAX_ACL_CONN_EFF=2
CONFIG_BTDM_CTRL_BR_EDR_MAX_SYNC_CONN_EFF=0
CONFIG_BTDM_CTRL_PINNED_TO_CORE_0=y
CONFIG_BTDM_CTRL_PINNED_TO_CORE=0
CONFIG_BTDM_CTRL_HCI_MODE_VHCI=y
То же самое в Menuconfig:
Далее, к проекту нужно добавить зависимость btd_vhci:
Способ 1: через IDF Component Manager, выполнить команду
idf.py add-dependency -pink0d/btd_vhci
(команда запускается в терминале, открыть который можно через F1: ESP-IDF: Open ESP-IDF Terminal)Способ 2: скопировать содержимое репозитория Github в директорию components\btd_vhci внутри проекта и добавить вручную
REQUIRES btd_vhci nvs_flash
в CMakeLists.txt для компонента main
Для использования C++ классов из библиотеки, следует переименовать main.c в main.cpp и обновить имя файла в CMakeLists.txt компонента main, а к точке входа в приложение добавить модификатор extern "C"
Что еще должно быть в проекте и внутри app_main:
Глобальный экземпляр класса для получения данных от BT-устройства. В примере ниже это
PS4BT PS4;
nvs_flash_init()
– инициализации flash памяти для внутренних нужд Bluetooth контроллера ESP32btd_vhci_init()
инициализация библиотекиxTaskCreatePinnedToCore(...)
запуск основной задачиbtd_vhci_autoconnect(...)
запуск автоматического соединения с BT устройством. При запуске эта задача пытается найти ранее сохраненный адрес BT устройства, и если такой адрес был ранее сохранен во flash-памяти ESP32, то Bluetooth контроллер начнет ожидать соединения с ним. Если по истечении 30 секунд подключение не будет установлено, или же сохраненного адреса в принципе не было, то Bluetooth контроллер перейдет в режим сопряжения. При успешном сопряжении адрес подключенного устройства будет записан во flash-память
Код основной задачи:
Должен обязательно содержать
btd_vhci_mutex_lock();
иbtd_vhci_mutex_unlock();
при доступе к классам библиотеки, поскольку они не являются потокобезопаснымиВ примере ниже выполняется простой опрос состояния контроллера PlayStation 4 и вывод данных в консоль
Пример в виде готового проекта есть на GitHub - btd_vhci_examples_ESP-IDF
Скрытый текст
Полный код примера:
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "PS4BT.h"
#include "btd_vhci.h"
PS4BT PS4;
bool printAngle, printTouch;
uint8_t oldL2Value, oldR2Value;
static const char *LOG_TAG = "main";
// print controller status
void ps4_print() {
if (PS4.connected()) {
if (PS4.getAnalogHat(LeftHatX) > 137 || PS4.getAnalogHat(LeftHatX) < 117 || PS4.getAnalogHat(LeftHatY) > 137 || PS4.getAnalogHat(LeftHatY) < 117 || PS4.getAnalogHat(RightHatX) > 137 || PS4.getAnalogHat(RightHatX) < 117 || PS4.getAnalogHat(RightHatY) > 137 || PS4.getAnalogHat(RightHatY) < 117) {
ESP_LOGI(LOG_TAG, "L_x = %d, L_y = %d, R_x = %d, R_y = %d",
PS4.getAnalogHat(LeftHatX),PS4.getAnalogHat(LeftHatY),
PS4.getAnalogHat(RightHatX),PS4.getAnalogHat(RightHatY));
}
if (PS4.getAnalogButton(L2) || PS4.getAnalogButton(R2)) { // These are the only analog buttons on the PS4 controller
ESP_LOGI(LOG_TAG, "L2 = %d, R2 = %d",PS4.getAnalogButton(L2),PS4.getAnalogButton(R2));
}
if (PS4.getAnalogButton(L2) != oldL2Value || PS4.getAnalogButton(R2) != oldR2Value) // Only write value if it's different
PS4.setRumbleOn(PS4.getAnalogButton(L2), PS4.getAnalogButton(R2));
oldL2Value = PS4.getAnalogButton(L2);
oldR2Value = PS4.getAnalogButton(R2);
if (PS4.getButtonClick(PS))
ESP_LOGI(LOG_TAG, "PS");
if (PS4.getButtonClick(TRIANGLE)) {
ESP_LOGI(LOG_TAG, "Triangle");
PS4.setRumbleOn(RumbleLow);
}
if (PS4.getButtonClick(CIRCLE)) {
ESP_LOGI(LOG_TAG, "Circle");
PS4.setRumbleOn(RumbleHigh);
}
if (PS4.getButtonClick(CROSS)) {
ESP_LOGI(LOG_TAG, "Cross");
PS4.setLedFlash(10, 10); // Set it to blink rapidly
}
if (PS4.getButtonClick(SQUARE)) {
ESP_LOGI(LOG_TAG, "Square");
PS4.setLedFlash(0, 0); // Turn off blinking
}
if (PS4.getButtonClick(UP)) {
ESP_LOGI(LOG_TAG, "UP");
PS4.setLed(Red);
} if (PS4.getButtonClick(RIGHT)) {
ESP_LOGI(LOG_TAG, "RIGHT");
PS4.setLed(Blue);
} if (PS4.getButtonClick(DOWN)) {
ESP_LOGI(LOG_TAG, "DOWN");
PS4.setLed(Yellow);
} if (PS4.getButtonClick(LEFT)) {
ESP_LOGI(LOG_TAG, "LEFT");
PS4.setLed(Green);
}
if (PS4.getButtonClick(L1))
ESP_LOGI(LOG_TAG, "L1");
if (PS4.getButtonClick(L3))
ESP_LOGI(LOG_TAG, "L3");
if (PS4.getButtonClick(R1))
ESP_LOGI(LOG_TAG, "R1");
if (PS4.getButtonClick(R3))
ESP_LOGI(LOG_TAG, "R3");
if (PS4.getButtonClick(SHARE))
ESP_LOGI(LOG_TAG, "SHARE");
if (PS4.getButtonClick(OPTIONS)) {
ESP_LOGI(LOG_TAG, "OPTIONS");
printAngle = !printAngle;
}
if (PS4.getButtonClick(TOUCHPAD)) {
ESP_LOGI(LOG_TAG, "TOUCHPAD");
printTouch = !printTouch;
}
if (printAngle) { // Print angle calculated using the accelerometer only
ESP_LOGI(LOG_TAG,"Pitch: %lf Roll: %lf", PS4.getAngle(Pitch), PS4.getAngle(Roll));
}
if (printTouch) { // Print the x, y coordinates of the touchpad
if (PS4.isTouching(0) || PS4.isTouching(1)) // Print newline and carriage return if any of the fingers are touching the touchpad
ESP_LOGI(LOG_TAG, "");
for (uint8_t i = 0; i < 2; i++) { // The touchpad track two fingers
if (PS4.isTouching(i)) { // Print the position of the finger if it is touching the touchpad
ESP_LOGI(LOG_TAG, "X = %d, Y = %d",PS4.getX(i),PS4.getY(i));
}
}
}
}
}
void ps4_loop_task(void *task_params) {
while (1) {
btd_vhci_mutex_lock(); // lock mutex so controller's data is not updated meanwhile
ps4_print(); // print PS4 status
btd_vhci_mutex_unlock(); // unlock mutex
vTaskDelay(1);
}
}
extern "C" void app_main(void)
{
esp_err_t ret;
// initialize flash
ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK( ret );
// initilize the library
ret = btd_vhci_init();
if (ret != ESP_OK) {
ESP_LOGE(LOG_TAG, "BTD init error!");
}
ESP_ERROR_CHECK( ret );
// run example code
xTaskCreatePinnedToCore(ps4_loop_task,"ps4_loop_task",10*1024,NULL,2,NULL,1);
// run auto connect task
btd_vhci_autoconnect(&PS4);
while (1) {
vTaskDelay(pdMS_TO_TICKS(100));
}
// main task should not return
}
На этом функционал библиотеки в общем-то и заканчивается, оставляя простор, собственно, для DIY-проектов.
Пока что я успел перенести в библиотеку только классы для клавиатуры-мышки (BTHID) и классы для наиболее распространенных контроллеров (PS4, PS5, Xbox), и даже не все из этого получилось протестировать с настоящими устройствами. Конечно, хотелось бы перенести и Serial Port Profile (SPP), но он уже заметно сложнее, а также потребует в целом переосмыслить многопоточность и потоковую безопасность внутри библиотеки.
Использование BTD_VHCI из ESP32 Arduino Core
Использование этого же кода с фреймворком ESP32 Arduino Core, теоретически, тоже возможно. Должен предупредить, что это будет ни разу не легче и не быстрее, чем сразу перейти на ESP-IDF.
Причина в том, что ESP32 Arduino Core состоит из готового набора библиотек, заранее собранных с дефолтным sdkconfig, в котором включен Bludroid стек, а доступа непосредственно к VHCI функциям, похоже, нет. Espressif предоставляет возможность кастомизации конфига через инструмент под названием Library Builder, то есть возможность установить специфичные опции для ESP32 все-таки существует. Чтобы сделать это, кроме установки ESP32 Arduino Core в Arduino IDE, нужно выполнить целую последовательность шагов:
Установить ESP-IDF и Library Builder
Изменить нужные опции в sdkconfig, который используется при сборке библиотек
Собрать свой кастомный билд ESP32 ArduinoCore (как написано в документации, это занимает несколько часов)
Подменить sdk, скачанный через Board Manager в Arduino IDE, на свой кастомный билд
Этот путь я пока что не проходил, поэтому не привожу подробного описания в текущей статье, которая и так уже получилась слишком объемной. Возможно, если к этой теме возникнет интерес, то все-таки проверю совместимость библиотеки с пересобранным ESP32 Arduino Core и сделаю дополнение уже в виде третьей части статьи.
marshallab
Тема интересная, как и сложная