Интернет вещей плотно вошел в нашу жизнь и используется повсеместно. Для меня же это возможность не только пользоваться, но еще и создавать разные умные устройства.



Меня зовут Евгений Глейзерман, я — 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 (далее — процессор).
Штатных способов управления три:
  1. ИК-пульт.
  2. Резистивные кнопки или их эмулятор.
  3. 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, где мы строим не просто работающий, но и безопасный интернет вещей на профессиональном уровне :)

Комментарии (8)


  1. Moog_Prodigy
    08.08.2024 18:28

    @evgley , а вот вы как считаете, можете ли вы ( не лично, компания, вы ведь в блоге компании пишете) дать гарантию на то, что с софтом от вашей компании не получится ситуация как у KrowdStrike? И если да, то ответственность какая? И каков процент надежности всего этого барахла, грузящего компы и это даже не майнинг?


    1. evgley Автор
      08.08.2024 18:28
      +3

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

      1. Исходный код не содержал ошибки. Понятно что весь софт с багами, однако есть практики значительно минимизирующие шанс подобных ошибок.Например, статические и динамические анализаторы, secure coding guideline, secure code review и т.п

      2. Было проведено тщательное тестирование, обнаружившее данную ошибку ДО выкладывания на сервера обновлений

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

      Все 3 описанные митигации реализованы в продуктах Лаборатории Касперского.

      Что ещё хочется отметить: антивирус и операционная система - продукты, полноценно протестировать совместимость которых - задача нетривиальная ввиду бесконечного количества возможных окружений/сценариев/API. Было бы интересно иметь защиту от подобных ситуаций в архитектуре решения, делающей изначально невозможной подобные ситуации.
      И тут мы плавно подходим к операционным системам с микроядром. В таких ОС антивирус был бы обычным приложением, работающим в юзер-моде. При падении чего-то в антивирусе, есть хорошие шансы что микроядерная ОС сможет это пережить.

      Кстати, в Kaspersky OS микроядро :)


      1. strvv
        08.08.2024 18:28

        С другой стороны:

        1. Даже анализаторы кода не спасают от кросс-багов, когда ошибка заложена в библиотеке, используемой ПО, но собираемой другими. Как с ssh в этом году. При этом, всё окружение с собой не унесешь, уже долбанные калькуляторы по размеру больше windows xp без драйверов.

        1. Тестирование. Больной вопрос. Если брать узкоспециализированное ПО, то можно ограничить варианты и с достаточной достоверностью сказать что ПО с допустимым уровнем ошибок.

        2. Согласен, при обнаружении отзывать сбойную версию и готовить хотя-бы заплатку, пока разберутся в проблеме и её решат.

        3. Ну и микроядра не панацея. Если отвалится важный сервис - совсем не важно что вопроса к пуговицам костюма нет (ядро осталось работать).


  1. voldemar_d
    08.08.2024 18:28

    auto newVal = hwCounter.getValue();

    if (newVal != val) {

    Почему бы сравнение величины с предыдущим значением не делать уже после того, как она ограничена min/max?


    1. evgley Автор
      08.08.2024 18:28

      Тут важно что hwCounter получает значение из аппаратного счётчика, он работает как работает, и там нет никаких минимальных\максимальных значений (кроме ограничений HW)

      С учётом этого нюанса пока не понял как написать лучше, и главное - чего мы этим исправлением достигнем.


      1. voldemar_d
        08.08.2024 18:28
        +1

        Я имел ввиду, что нежелательно вызывать setVolume, если громкость уже и так максимальная. Можно добавить еще одну проверку:

        auto newVal = hwCounter.getValue();
        auto lastVol = volume;
        if (newVal != val) {
          volume += (newVal - val);
          volume = std::min<int>(volume, Madbit::Volume::MAX);
          volume = std::max<int>(volume, Madbit::Volume::MIN);
          if (lastVol != volume) {
            madwiim->setVolume(volume);
            lastVol = volume;
          }
          val = newVal;
        }

        Не знаю, как оно внутри устроено, но если внутри madwiim->setVolume и так есть подобная проверка, то можно ничего не добавлять, конечно.


        1. evgley Автор
          08.08.2024 18:28

          Понял что имеется ввиду. В последней версии исходника уже исправлено. Я там правда другую штуку реализовывал: выяснилось что у разных пользователей разные предпочтения по чувствительности энкодера, сделал эту часть настраиваемой (параметр encoderSpeedDiv)


  1. kos9078
    08.08.2024 18:28

    Я у себя сделал иначе. У меня в машине нет CAN-multimedia и кнопки на руле максимально "тупые". Поэтому я взял пластик от более "жирной" модели, с большим числом кнопок и сделал 2 платы.
    В руле стоит 4 платы с кнопками. Суммарное количество кнопок - 16. К ним, штатнно, идёт 4 провода. И все что умеет - управлять магнитолой и переключать "экраны" на приборке.
    Изначально мне хотелось добавить круиз-контроль (функционал есть, но производитель сэкономил на проводке).
    Позже, мне захотелось листать треки не только на родном ГУ и управлять усилителем.

    Для этого одну из плат разместил в самом руле вместо родной платы с кнопками, подвёл к ней все остальные платы, пустил "наружу" по родным двум из 4х проводов can, а по двум другим - питание. На второй плате терминировал родную проводку (управление гу и приборкой) + добавил 3 провода от круиза и 2 от can-drive (для чтения некоторых параметров). Также на этой плате расположился радиомодуль для управления сторонними системами. Всё спрятано под штатным пластиком и нигде не торчит (это была главная задача проекта).

    По рассыпухе: десяток tlp172a, 2xstm32, esp32 для wifi+bluetooth (также используется для обновления прошивок на stm32), 3хcan-трансивера, разъёмы m/f с алика чтобы воткнуть в штатную проводку и ничего не резать...
    Себестоимость партии из 5 комплектов в 20м году была 6к с платами, доставкой и автоматизированной сборкой у китайцев... Но один установочный комплект дешевле 5-6к не получалось, т.к. оригинальный жгут проводов руля все равно должен был бы пойти по нож. А он, на том же, алике 2-3к стоит.
    Короче, мою, даже упрощённую систему (в виде full-kit), никто не готов был купить за 4-5к, зато покупали у другого обиталя форума плату с 4 кнопками за 8к)