[Из песочницы] «Умный» удлинитель с голосовым управлением для гирлянд (esp8266+stm32)

Привет, Хабр. В прошлом году сделал «умный» удлинитель для управления гирляндами на елочке. Но тогда руки так и не дошли написать об этом статью. Исправляюсь.


Сама елочка


image


На елочке 3 гирлянды, а под ней выводок светящихся белых мишек. Когда гирлянд много, встает вопрос — как ими управлять? Каждый раз залезать под елочку и включать/выключать из розетки нужные гирлянды — сомнительное удовольствие.


Конечно, продается большое количество «умных розеток» —, но с голосовым управлением, и так что бы 4 розетки сразу в одном устройстве, без лишних проводов и блоков питания — таких не встречал.


Итак приступим

В качестве корпуса идеально подошел удлинитель «пилот». Замеченный и купленный в ближайшем магазине.


pilot.jpg


Блок питания на 5 вольт и шилд с 4 реле были найдены в запасах мелочевки купленной в на aliexpress.


acdc.jpgrelayshield.jpg


Теперь, самое главное — «мозги». Мозгами в проекте будет плата Wiieva, в которой есть все, что нам надо — экран, микрофон, wifi, форм фактор ардуино, совместимый с реле шилдом. WiFi модуль реализован на суперпопулярном esp8266, управление периферией и работа со звуком — на stm32f105rbt.


wiieva1.jpgwiieva2.jpg


Собираем умный удлинитель

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


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

internals.jpg

Подключаем мозги — соединяем плату Wiieva и шилд с реле

internals1.jpg

Размещаем все компоненты по своим местам

internals2.jpg

Вид сверху на «умный удлинитель» в сборе

topview.jpg

Немного эстетики — печатаем крышечку на 3d принтере

topview2.jpg

Что получилось

IMG_1873.JPG

Как устроена програмная и аппаратная часть


Самая сложная часть проекта — компоненты отвечающие за ввод/вывод звука. Вообще, есть несколько подходов к записи и распознавании звука:


  • распознавание на устройстве
    Плюсы: не требуется интернет подключение
    Минусы: требуется большая вычислительная мощность, очень ограниченный словарный запас, большой процент ошибок.
  • распознавание в облаке, например google или yandex
    Плюсы: хорошее качество, практически не ограниченный словарный запас
    Минусы: требуется интернет подключение, увеличенный latency
    В случае с IoT устройством, имеющим процессор с 64 кб ОЗУ и 160Мгц — сделать уверенное распознавание голосовых команд на борту — невозможно. Можно обучить его распознавать несколько слов и то, предварительно натренировав на свой голос.


Поэтому, для распознавания речи использовал сервис google speech recognition. Казалось бы, не сложная задача, записать звук с микрофона и отправить в google speech recognition. Однако, когда речь идет про устройство на базе esp8266, то задача оказывается не тривиальной.


У esp8266 нет хорошего АЦП, а тот, что есть на борту, технически не позволяет записать ничего отличного от шума. Поэтому, для захвата звука, в качестве достаточном для распознавания речи, как минимум нужен внешний АЦП или еще лучше, внешний процессор, к которому подключен микрофон. Попробовав несколько вариантов — остановился на stm32 + цифровой PDM микрофон.


Следующая задача — управление/передача данных от stm32 к esp8266. UART и i2c были сразу отброшены, как медленные интерфейсы и принято решение использовать SPI. SPI — это синхронный интерфейс с обязательным распределением ролей: мастер и слейв. В связке stm32 и esp8266 основная логика программы выполняется на esp8266, а stm32 — сопроцессор, работающий с периферией. Поэтому, логично назначить esp8266 роль мастера, а stm32 — роль слэйва.


Эта связка дала хороший результат: чистый звук с микрофона без помех и без постороннего шума. Увы, звуковая идиллия продолжалась не долго, ровно, до момента отправки полученного звука через WiFi по http соединению в google.


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


Поэтому, как часто бывает, аппаратную проблему пришлось полечить программно. Логичное решение — копить звук в буфере, а по завершении фразы отправить по http в облако. Казалось, бы делов — сохранить в буфере. Но тут вспоминаем, что у нас всего 40КБ свободного ОЗУ. А даже с частотой оцифровки 8 кгц в 40КБ влезет всего лишь 2 с небольшим секунды записи несжатой речи. Маловато будет.


Решением оказалось предварительно паковать звук кодеком SPEEX — он дает рейт 2KB в секунду, чего более чем достаточно, чтобы записать любую голосовую команду целиком в память, а конец фразы определять алгоритмом VAD (Voice Activity Detector).
scheme.png
Вуаля — такая конструкция заработала, и стала уверенно распознавать любые произносимые фразы.


Про плату Wiieva


Тут, наверное, стоит сделать лирическое отступление. У тех кто, дочитал до этого абзаца, скорее всего возникнет вопрос — неужели столько телодвижений только ради голосового управления елочкой. Простой ответ, конечно, — не только. Пару лет назад, когда esp8266 только появилась у меня, возникла мысль — прикрутить к ней облачное распознавание речи. И, в свободное время, я со знакомым электронщиком неспешно пилил проект, который вылился в плату wiieva и описанную выше конфигурацию. В процессе жизни проект обзавелся кучей фишек, например, mp3 плеер с динамиком, Arduino-совместимый форм фактор, датчики температуры/влажности/давления, тач скрин, USB, ИК диод и слот MicroSD.


Поэтому, основная работа была проделана еще до начала проекта «Елочка», а проект «Елочка» был сделан за пару вечеров, включая операцию по подсадки платы в удлинитель и написания скетча с высокоуровневой логикой.


Скетч с логикой


Программа написана как скетч для Arduino окружения esp8266.


Кроме распознавания голосовых команд, скетч обладает UI — скринсэйвер с красивой елочкой, экран управления с кнопками включения/выключения гирлянд.
В дополнение к локальному управлению, есть http API включения/выключения гирлянд. Это для управления елочкой через общий интерфейс умного дома.


И бонусом, скетч умеет проигрывать mp3 файлы с microSD карты — записал туда несколько новогодних композиций. Так сказать, дополнительная фишечка, для поддержания новогоднего настроения.


Исходники скетча


Инициализация и запуск распознавания


// Подключаем библиотеку аудиозаписи
#include 
WiievaRecorder recorder (2000*5);

// Переменные для распознавалки
unsigned long timeRecorderStart = 0,timeRecorderEnd=0;
bool wasVAD = false;

void startRecognize () {
    // Запуск рекордера
    recorder.start (AIO_AUDIO_IN_SPEEX);
    Serial.printf ("Start recording\n");

    timeRecorderStart = millis();
    timeRecorderEnd=0;
    wasVAD = false;
}


Само распознавание и выполнение команд


void processRecognize () {
    if (!timeRecorderStart) {
        return;
    }

    // Проверка состояния Voice Activity
    bool res = recorder.run ();
    bool vad = recorder.checkVad();

    if (vad && !wasVAD) {
        Serial.printf("VAD: speech started\n");
    }

    wasVAD = wasVAD || vad;

    if (millis () - timeRecorderStart < 3000 || vad)
        timeRecorderEnd = millis ();

    if (res && (!timeRecorderEnd || millis () - timeRecorderEnd < 500))
        // VAD еще не сработал - записываем дальше
        return;

    recorder.stop();
    timeRecorderStart = 0;
    if (!wasVAD) {
        // Не было голосовой активности - выходим
        return;
    }

    // Создаем http клиент и далеам POST в google speech recognition
    HTTPClient http;
    http.begin(url);
    http.addHeader ("Content-Type","audio/x-speex-with-header-byte; rate=8000");
    int httpCode = http.sendRequest ("POST",&recorder,recorder.recordedSize());

    if(httpCode > 0) {
        Serial.printf("[HTTP] POST... code: %d\n", httpCode);
        String payload = http.getString();
        Serial.println(payload);

        String cmd = "toggle";
        // Грепаем по ответу из гугла команду
        // Ответ приходит в JSON, но для простоты мы просто ищем вхождение подстроки с командой
        if (payload.indexOf ("выклю")>=0 || payload.indexOf ("погас")>=0)
            cmd = "off";
        else if (payload.indexOf ("вклю")>=0 || payload.indexOf ("зажг")>=0)
            cmd = "on";

        if (payload.indexOf ("музык")>=0) startPlay();
        else if (payload.indexOf ("все")>=0) controlAllRelay (cmd); else {
            // Ищем имя гирлнянды
            if (payload.indexOf ("шарики")>=0) controlRelay (0,cmd);
            if (payload.indexOf ("свечки")>=0) controlRelay (1,cmd);
            if (payload.indexOf ("мишки")>=0|| payload.indexOf ("виски")>=0) controlRelay (2,cmd);
            if (payload.indexOf ("огоньки")>=0) controlRelay (3,cmd);
        }
    }
    http.end();
}


Под капотом


Оцифровка звука


PDM Микрофон подключен к SPI/I2S2 процессора stm32. В качестве референса использовал этот Application Note от ST


Для того, что бы не загружать процессор данные из I2S получаются с использованием DMA в кольцевой ping-pong буфер.
PDM. Обработка полученных PDM данных происходит по прерываниям от DMA. Работа с прерываниями DMA достаточно стандартная для stm32:
Есть два признака прерывания по заполнению верхний/нижней половин буфера. В обработчике прерывания выбирается половинка буфера, с уже готовыми данными


Затем происходит преобразование буфера из формата PDM в обычный PCM: набор сэмплов (значений уровня сигнала) с требуемой частотой дискретизации.


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


Кодирование в speex


Следующий этап конвейера — упаковка звука кодеком SPEEX. Обработка звука кодеком весьма ресурсоемкий процесс, кушающий много процессорного времени и вызывать его в обработчике прерывания не очень хорошо.


Поэтому упаковка происходит асинхронно, в основном цикле программы — код код часть вторая


Заодно с кодированием в SPEEX анализируется наличие голосовой активности алгоритмом VAD.
А закодированная кодеком speex речь складывается в еще один кольцевой буфер speex_buf, из которого они уже и передаются в esp9266


Передача закодированного буфера из stm32 в esp8266


Интерфейс между esp8266 и stm32 построен по принципу команда → ответ. esp8266 отправляет команду, stm32 отрабатывает команду и возвращает ответ. У части команд вместе с телом команды/или телом ответа передается буфер данных.


Со стороны esp8266 алгоритм работы получился очень простой:
Отправить команду чтения буфера данных и считать данные:


Так выглядит код со стороны esp8266:
код рекордера
код работы с SPI


Со стороны stm32 задача выглядит сложнее:
По прерываниям от SPI парсится код команды, и в зависимости от кода команды выполняются требуемые действия. В нашем случае — пересылка данных из кольцевого буфера SPEEX кодека в SPI


Вместо заключения


Многие интересные моменты, например, такие как проигрывание mp3, подключение графической библиотеки, реализацию драйверов экрана и тач панели, интеграцию с умным домом и многое-многое другое пришлось оставить за скобками этой статьи — получилось бы слишком много текста.


В планах еще допилить активацию распознавания речи по hot-word, например «елочка». Для этого планирую затащить небольшой кусочек pocketsphinx на борт и делать на борту что то типа MFCC+DTW…


несколько полезных ссылок


Исходники скетча


Схемотехника платы


Программа для stm32


Форк Arduino среды для платы Wiieva

© Habrahabr.ru