Интернет вещей плотно вошел в нашу жизнь и используется повсеместно. Для меня же это возможность не только пользоваться, но еще и создавать разные умные устройства.
Меня зовут Евгений Глейзерман, я — Head of KasperskyOS IoT Protection Development в «Лаборатории Касперского». Отвечаю за различные IoT-продукты на собственной микроядерной операционной системе KasperskyOS: шлюзы, контроллеры, блоки телематики и т. д. А еще я иногда ковыряю устройства поменьше, на которые KasperskyOS пока установить нельзя. В данной статье хочу рассказать о своем хобби-проекте и поделиться возможностями esp-32 на примере DIY-девайса для автозвука: как я собрал пульт, регулирующий громкость по Bluetooth, взяв за основу популярный микроконтроллер.
Все началось с того, что у меня автомобиль с полностью кастомным автозвуком: источник, процессор, усилители, динамики, акустические провода. Немного поясню про кастомность.
В обычном автомобиле штатная магнитола (головное устройство, ГУ) совмещает в себе функции источника звука, его обработки и усиления. На выходе имеем аналоговый сигнал, сразу подающийся на динамики.
В моем варианте (стандартном для тюнинга автозвука) сигнал идет так:
Если интересно посмотреть, то выглядит все вот так.
Кстати, на фото не видно самого процессора Madbit, он находится под усилителем в левом верхнем углу и выглядит вот так.
Для управления громкостью используется процессор Madbit, у которого нет штатного пульта, но есть возможность подключения резистивных кнопок (то есть где нажатие кнопки меняет сопротивление между контактами). Мне же резистивные пульты не понравились по причине отсутствия обратной связи (отображения уровня громкости). Кроме того, все доступные варианты были с «кнопками», а мне непременно хотелось «крутилку». Тут и возникла необходимость кастомного решения.
В моем авто есть кнопки на руле и крутилки магнитолы, но они для задачи не подходят по разным причинам. Так, кнопки и крутилки управляют громкостью внутри усилителя штатного головного устройства, а у меня в машине для воспроизведения качественного звука он не используется.
Протокол кнопок/крутилок на руле можно исследовать или даже получить нажатия из CAN-шины — но так делать я не хотел, потому что штатный функционал регулировки громкости мне еще пригодится, да и вмешиваться в электронику машины любительским девайсом — слишком инвазивно.
До разработки пульта я попробовал чисто программный вариант. При подключении к ГУ девайсов из серии CarPlay AI Box (по сути Android box, выводящий свою картинку по протоколу CarPlay на экран магнитолы) есть возможность получить события от дополнительной крутилки на штатной магнитоле в виде событий DPAD_LEFT и DPAD_RIGHT и забиндить эти события на любые скрипты, в том числе регулировку громкости.
Такой подход работает, но смущает довольно высокая latency. Также я обнаружил, что когда активна камера заднего вида, то события от кнопок уже не приходят, а именно в этот момент как раз часто хочется сделать потише.
Итак, решено, надо делать отдельный девайс.
Крутить громкость нам надо на звуковом процессоре Madbit (далее — процессор).
Штатных способов управления три:
Первые два варианта я практически сразу отмел, так как хочется не только менять уровень громкости, а еще и видеть его текущий уровень. Еще хотелось менять громкость крутилкой-энкодером, а не кнопками + и -, а это усложнило бы схему.
Третий вариант выглядел наиболее перспективно. Но Android-приложение нужно на чем-то запустить, при этом добавив аппаратную крутилку управления и исключив задержки. В этот момент меня посетила мысль исключить из схемы Android и повторить протокол Android-приложения на микроконтроллере с Bluetooth. Забегая вперед, скажу: штатное приложение использует Bluetooth-профиль SPP (Serial Port Profile) и кастомный протокол поверх него для обмена информацией с процессором.
В качестве сердца идеально подходит ESP32: мощи достаточно, на борту есть Bluetooth, стоит копейки. Есть великое множество плат на ESP32. Я решил начать с ESP32-S3: она помощнее и умеет USB Host, а у меня была мысль попробовать USB Macropad c крутилкой в качестве органа управления.
Но уже во время попытки написать код прототипа меня ждал облом: линкер ругался на отсутствие функций, реализующих Bluetooth-стек. В итоге выяснилось, что ESP32-S3 умеет Bluetooth Low Energy, но не умеет Bluetooth Classic, по которому управляется процессор.
Что ж, на этот раз, изучив таблицу, я решил взять обычную ESP32. На код замена микроконтроллера никак не повлияла. С идеями о USB Macropad я распрощался. Вместо него взял обычный энкодер EC-11, а в качестве экрана — 128*32 SSD1306.
Также по ходу дела потребовались всякие проводки, разъемы, макетные платы и 3d-принтер для корпуса. Не все железо было на руках, дремели-3D-принтеры пришлось покупать и все это осваивать. Но поскольку мы сейчас говорим только про софт, то не вижу смысла это подробно описывать: одна картинка здесь лучше тысячи слов.
Сильно не осуждайте, у меня бэкграунд программиста :)
И, конечно, музыка для рабочего настроения, за подбор которой у меня отвечает специальный скрипт.
Для начала я пошел простым путем — написал производителю процессора с просьбой предоставить описание протокола, по которому работает Android-приложение.
Производитель любезно предоставил мне пару файлов (выкладываю с его согласия):
По сути все управление сводится к передаче вот такой структурки:
Тем не менее этого не хватило для полного понимания протокола.
Например, непонятно, от чего считать crc, непонятно, как парсить ответы, как устроено поле data. Да и процесс подключения к устройству по Bluetooth хотелось рассмотреть подробнее.
Для полного понимания протокола я исследовал, как работает Android-приложение. Этого уже было достаточно, чтобы полностью понять протокол, оставалось только реализовать его под ESP32.
В качестве фреймворка я взял ESP-IDF от Espressif, производителя ESP32, а не популярный Arduino. Это позволило шире использовать возможности платы.
TLDR все выложено тут.
Далее пробегусь по исходникам и буду подсвечивать только моменты, которые показались мне интересными.
Для вывода на экран используется библиотека LVGL.
Был приятно удивлен ее возможностями, все почти как при разработке под десктопы: есть готовые виджеты, менюшки, layout, обработка устройств ввода и прочее.
Интересно, что в LVGL есть готовые иконки для некоторых символов, и для отображения значка Bluetooth достаточно сделать так:
Еще в LVGL зашиты шрифты разного размера, в результате появилась такая проблема: высота экрана 32, и шрифт был 32, и по логике текст должен был быть на весь экран, но он был меньше. Пришлось кастомизировать шрифты: для отображения уровня громкости я выбрал шрифт Lato, он мне понравился визуально. По размеру идеально влез размер 44: странно, но разбираться лениво :) Подготовил шрифт по инструкции и включил его использование:
Этот модуль обрабатывает «крутилку».
В ESP32 есть аппаратный счетчик, позволяющий реагировать на изменение сигнала с GPIO (general-purpose input/output).
Это позволяет сделать очень чувствительную обработку сигнала с минимальными задержками, особенно учитывая, что под капотом FreeRTOS с вытесняющей многозадачностью.
Однако мне все равно потребовался привычный поллинг: у громкости есть верхний и нижний пределы, а хардварному счетчику это никак не объяснить. В итоге поллится не GPIO, а значение, которое насчитал хардварный счетчик:
Этот модуль отвечает за работу с Bluetooth и протокол взаимодействия с процессором.
Логика работы с Bluetooth по большей части заимствована из примера в репозитории производителя. Основные действия по взаимодействию с процессором происходят в readTask и runCommand.
В рамках работы над данным модулем удалось познакомиться с различными API, доступными в FreeRTOS. Например, можно создавать задачи (xTaskCreate), исполнение которых контролируется планировщиком (Task scheduling).
Кстати, плата у нас двухъядерная, так что без синхронизации и обмена данными между задачами нам не обойтись. В нашем случае удобно использовать Message Buffers (Message buffer example), по сути это готовый вариант producer-consumer-очереди во FreeRTOS.
В какой-то момент прототип пульта заработал и появились более банальные вопросы: а к какому устройству подключаться? Какой ПИН-код использовать? В ранних версиях я просто захардкодил Bluetooth-адрес своего проца и 1234, но хотелось найти более гибкий способ. Кроме того, ко мне пришли желающие повторить мой пультик совершенно без IT-опыта. Не предлагать же им пересобрать прошивку со своими параметрами :) Кстати, я пока не нашёл способа удобной дистрибуции собранных прошивок так, чтобы не нужно было делать компиляцию перед прошивкой. Похоже, что в esp-idf это не предусмотрено.
Так я подошел к процедуре первоначальной настройки устройства.
Нужно придумать для пользователя способ задать настройки устройства и где-то их сохранить. На борту устройства есть Wi-Fi, в том числе в режиме точки доступа, который я и использовал. А в ESP-IDF есть веб-сервер. Достаточно захардкодить в обработчике HTTP GET примитивную веб-страницу, а в HTTP POST сохранить настройки. Таким образом можно сделать настройку пульта, используя браузер, с чем может справиться человек вне IT.
Храниться настройки будут в NVS: это такой специальный раздел на файловой системе для хранения пар ключ-значение.
Кстати, на устройстве есть файловая система и таблица разделов, и ее даже можно модифицировать. А в моем случае это вовсе было необходимо: дело в том, что я не сильно заморачивался по поводу размера прошивки, и в итоге раздел, выделенный по умолчанию под код, начал переполняться.
Больше всего места потребляли Bluetooth- и Wi-Fi-стеки, библиотека LVGL, ну и, конечно, С++ Runtime (например, iostream), без которого точно можно было обойтись.
Я же просто отредактировал таблицу разделов, выделив под код 3 Мб вместо штатно предусмотренного 1 Мб. Сделал это, удалив разделы OTA: обновление по воздуху мне не нужно.
Вот и все основные этапы моей работы над пультом — разработка завершилась успехом :) Рабочий образец уже стоит в авто и со своими задачами справляется — громкость музыки можно легко и регулировать, и видеть текущий уровень громкости. И копию для желающих я тоже сделал, так что дизайн «ушел в народ». А сам я не только сделал управление качественным звуком удобнее, но и заодно повеселился с ESP32 и FreeRTOS. Статья получилась больше обзорная, если же вас заинтересовали детали, то милости прошу в комментарии.
Как я сказал в начале, IoT — это и подобное «баловство», и коммерческая разработка. И если вы «горите» не только умными лампочками и самодельными пультиками, но и серьезными промышленными вещами, присоединяйтесь к моему IoT-подразделению в Kaspersky, где мы строим не просто работающий, но и безопасный интернет вещей на профессиональном уровне :)
Меня зовут Евгений Глейзерман, я — Head of KasperskyOS IoT Protection Development в «Лаборатории Касперского». Отвечаю за различные IoT-продукты на собственной микроядерной операционной системе KasperskyOS: шлюзы, контроллеры, блоки телематики и т. д. А еще я иногда ковыряю устройства поменьше, на которые KasperskyOS пока установить нельзя. В данной статье хочу рассказать о своем хобби-проекте и поделиться возможностями esp-32 на примере DIY-девайса для автозвука: как я собрал пульт, регулирующий громкость по Bluetooth, взяв за основу популярный микроконтроллер.
Кастомный пульт, кнопки, крутилки
Все началось с того, что у меня автомобиль с полностью кастомным автозвуком: источник, процессор, усилители, динамики, акустические провода. Немного поясню про кастомность.
В обычном автомобиле штатная магнитола (головное устройство, ГУ) совмещает в себе функции источника звука, его обработки и усиления. На выходе имеем аналоговый сигнал, сразу подающийся на динамики.
В моем варианте (стандартном для тюнинга автозвука) сигнал идет так:
Если интересно посмотреть, то выглядит все вот так.
Кстати, на фото не видно самого процессора Madbit, он находится под усилителем в левом верхнем углу и выглядит вот так.
Для управления громкостью используется процессор Madbit, у которого нет штатного пульта, но есть возможность подключения резистивных кнопок (то есть где нажатие кнопки меняет сопротивление между контактами). Мне же резистивные пульты не понравились по причине отсутствия обратной связи (отображения уровня громкости). Кроме того, все доступные варианты были с «кнопками», а мне непременно хотелось «крутилку». Тут и возникла необходимость кастомного решения.
В моем авто есть кнопки на руле и крутилки магнитолы, но они для задачи не подходят по разным причинам. Так, кнопки и крутилки управляют громкостью внутри усилителя штатного головного устройства, а у меня в машине для воспроизведения качественного звука он не используется.
Протокол кнопок/крутилок на руле можно исследовать или даже получить нажатия из CAN-шины — но так делать я не хотел, потому что штатный функционал регулировки громкости мне еще пригодится, да и вмешиваться в электронику машины любительским девайсом — слишком инвазивно.
До разработки пульта я попробовал чисто программный вариант. При подключении к ГУ девайсов из серии CarPlay AI Box (по сути Android box, выводящий свою картинку по протоколу CarPlay на экран магнитолы) есть возможность получить события от дополнительной крутилки на штатной магнитоле в виде событий DPAD_LEFT и DPAD_RIGHT и забиндить эти события на любые скрипты, в том числе регулировку громкости.
Такой подход работает, но смущает довольно высокая latency. Также я обнаружил, что когда активна камера заднего вида, то события от кнопок уже не приходят, а именно в этот момент как раз часто хочется сделать потише.
Итак, решено, надо делать отдельный девайс.
Рассматриваю варианты
Крутить громкость нам надо на звуковом процессоре Madbit (далее — процессор).
Штатных способов управления три:
- ИК-пульт.
- Резистивные кнопки или их эмулятор.
- Android-приложение по Bluetooth.
Первые два варианта я практически сразу отмел, так как хочется не только менять уровень громкости, а еще и видеть его текущий уровень. Еще хотелось менять громкость крутилкой-энкодером, а не кнопками + и -, а это усложнило бы схему.
Третий вариант выглядел наиболее перспективно. Но Android-приложение нужно на чем-то запустить, при этом добавив аппаратную крутилку управления и исключив задержки. В этот момент меня посетила мысль исключить из схемы Android и повторить протокол Android-приложения на микроконтроллере с Bluetooth. Забегая вперед, скажу: штатное приложение использует Bluetooth-профиль SPP (Serial Port Profile) и кастомный протокол поверх него для обмена информацией с процессором.
Готовлю железо
В качестве сердца идеально подходит ESP32: мощи достаточно, на борту есть Bluetooth, стоит копейки. Есть великое множество плат на ESP32. Я решил начать с ESP32-S3: она помощнее и умеет USB Host, а у меня была мысль попробовать USB Macropad c крутилкой в качестве органа управления.
Но уже во время попытки написать код прототипа меня ждал облом: линкер ругался на отсутствие функций, реализующих Bluetooth-стек. В итоге выяснилось, что ESP32-S3 умеет Bluetooth Low Energy, но не умеет Bluetooth Classic, по которому управляется процессор.
Что ж, на этот раз, изучив таблицу, я решил взять обычную ESP32. На код замена микроконтроллера никак не повлияла. С идеями о USB Macropad я распрощался. Вместо него взял обычный энкодер EC-11, а в качестве экрана — 128*32 SSD1306.
Также по ходу дела потребовались всякие проводки, разъемы, макетные платы и 3d-принтер для корпуса. Не все железо было на руках, дремели-3D-принтеры пришлось покупать и все это осваивать. Но поскольку мы сейчас говорим только про софт, то не вижу смысла это подробно описывать: одна картинка здесь лучше тысячи слов.
Сильно не осуждайте, у меня бэкграунд программиста :)
И, конечно, музыка для рабочего настроения, за подбор которой у меня отвечает специальный скрипт.
Изучаю протокол
Для начала я пошел простым путем — написал производителю процессора с просьбой предоставить описание протокола, по которому работает Android-приложение.
Производитель любезно предоставил мне пару файлов (выкладываю с его согласия):
По сути все управление сводится к передаче вот такой структурки:
typedef struct
{
uint8_t prefix[5]; // префикс [CMD]
uint8_t cmd;
uint8_t sizediv4;
uint8_t crc;
uint8_t data; //начало данных
}TProtocol;
Тем не менее этого не хватило для полного понимания протокола.
Например, непонятно, от чего считать crc, непонятно, как парсить ответы, как устроено поле data. Да и процесс подключения к устройству по Bluetooth хотелось рассмотреть подробнее.
Для полного понимания протокола я исследовал, как работает Android-приложение. Этого уже было достаточно, чтобы полностью понять протокол, оставалось только реализовать его под ESP32.
Пишу софт
В качестве фреймворка я взял ESP-IDF от Espressif, производителя ESP32, а не популярный Arduino. Это позволило шире использовать возможности платы.
TLDR все выложено тут.
Далее пробегусь по исходникам и буду подсвечивать только моменты, которые показались мне интересными.
display.{h,cpp}
Для вывода на экран используется библиотека LVGL.
Был приятно удивлен ее возможностями, все почти как при разработке под десктопы: есть готовые виджеты, менюшки, layout, обработка устройств ввода и прочее.
Интересно, что в LVGL есть готовые иконки для некоторых символов, и для отображения значка Bluetooth достаточно сделать так:
bluetoothLabel = lv_label_create(scr);
lv_label_set_text(bluetoothLabel, LV_SYMBOL_BLUETOOTH);
Еще в LVGL зашиты шрифты разного размера, в результате появилась такая проблема: высота экрана 32, и шрифт был 32, и по логике текст должен был быть на весь экран, но он был меньше. Пришлось кастомизировать шрифты: для отображения уровня громкости я выбрал шрифт Lato, он мне понравился визуально. По размеру идеально влез размер 44: странно, но разбираться лениво :) Подготовил шрифт по инструкции и включил его использование:
static lv_style_t style;
lv_style_init(&style);
lv_style_set_text_font(&style, &lato_44);
lv_obj_add_style(volumeLabel, &style, 0);
encoder.{h,cpp}
Этот модуль обрабатывает «крутилку».
В ESP32 есть аппаратный счетчик, позволяющий реагировать на изменение сигнала с GPIO (general-purpose input/output).
Это позволяет сделать очень чувствительную обработку сигнала с минимальными задержками, особенно учитывая, что под капотом FreeRTOS с вытесняющей многозадачностью.
Однако мне все равно потребовался привычный поллинг: у громкости есть верхний и нижний пределы, а хардварному счетчику это никак не объяснить. В итоге поллится не GPIO, а значение, которое насчитал хардварный счетчик:
while (true)
{
auto newVal = hwCounter.getValue();
if (newVal != val) {
volume += (newVal - val);
volume = std::min<int>(volume, Madbit::Volume::MAX);
volume = std::max<int>(volume, Madbit::Volume::MIN);
madwiim->setVolume(volume);
val = newVal;
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
madbit.{h,cpp}
Этот модуль отвечает за работу с Bluetooth и протокол взаимодействия с процессором.
Логика работы с Bluetooth по большей части заимствована из примера в репозитории производителя. Основные действия по взаимодействию с процессором происходят в readTask и runCommand.
В рамках работы над данным модулем удалось познакомиться с различными API, доступными в FreeRTOS. Например, можно создавать задачи (xTaskCreate), исполнение которых контролируется планировщиком (Task scheduling).
Кстати, плата у нас двухъядерная, так что без синхронизации и обмена данными между задачами нам не обойтись. В нашем случае удобно использовать Message Buffers (Message buffer example), по сути это готовый вариант producer-consumer-очереди во FreeRTOS.
В какой-то момент прототип пульта заработал и появились более банальные вопросы: а к какому устройству подключаться? Какой ПИН-код использовать? В ранних версиях я просто захардкодил Bluetooth-адрес своего проца и 1234, но хотелось найти более гибкий способ. Кроме того, ко мне пришли желающие повторить мой пультик совершенно без IT-опыта. Не предлагать же им пересобрать прошивку со своими параметрами :) Кстати, я пока не нашёл способа удобной дистрибуции собранных прошивок так, чтобы не нужно было делать компиляцию перед прошивкой. Похоже, что в esp-idf это не предусмотрено.
Так я подошел к процедуре первоначальной настройки устройства.
init{h,cpp}
Нужно придумать для пользователя способ задать настройки устройства и где-то их сохранить. На борту устройства есть Wi-Fi, в том числе в режиме точки доступа, который я и использовал. А в ESP-IDF есть веб-сервер. Достаточно захардкодить в обработчике HTTP GET примитивную веб-страницу, а в HTTP POST сохранить настройки. Таким образом можно сделать настройку пульта, используя браузер, с чем может справиться человек вне IT.
Храниться настройки будут в NVS: это такой специальный раздел на файловой системе для хранения пар ключ-значение.
Кстати, на устройстве есть файловая система и таблица разделов, и ее даже можно модифицировать. А в моем случае это вовсе было необходимо: дело в том, что я не сильно заморачивался по поводу размера прошивки, и в итоге раздел, выделенный по умолчанию под код, начал переполняться.
Больше всего места потребляли Bluetooth- и Wi-Fi-стеки, библиотека LVGL, ну и, конечно, С++ Runtime (например, iostream), без которого точно можно было обойтись.
Я же просто отредактировал таблицу разделов, выделив под код 3 Мб вместо штатно предусмотренного 1 Мб. Сделал это, удалив разделы OTA: обновление по воздуху мне не нужно.
Заключение
Вот и все основные этапы моей работы над пультом — разработка завершилась успехом :) Рабочий образец уже стоит в авто и со своими задачами справляется — громкость музыки можно легко и регулировать, и видеть текущий уровень громкости. И копию для желающих я тоже сделал, так что дизайн «ушел в народ». А сам я не только сделал управление качественным звуком удобнее, но и заодно повеселился с ESP32 и FreeRTOS. Статья получилась больше обзорная, если же вас заинтересовали детали, то милости прошу в комментарии.
Как я сказал в начале, IoT — это и подобное «баловство», и коммерческая разработка. И если вы «горите» не только умными лампочками и самодельными пультиками, но и серьезными промышленными вещами, присоединяйтесь к моему IoT-подразделению в Kaspersky, где мы строим не просто работающий, но и безопасный интернет вещей на профессиональном уровне :)