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


Сама елочка


image


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


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


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


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



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



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



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


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

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



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



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



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



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



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







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


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


  • распознавание на устройстве
    Плюсы: не требуется интернет подключение
    Минусы: требуется большая вычислительная мощность, очень ограниченный словарный запас, большой процент ошибок.
  • распознавание в облаке, например 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).

Вуаля — такая конструкция заработала, и стала уверенно распознавать любые произносимые фразы.


Про плату Wiieva


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


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


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


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


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


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


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


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


// Подключаем библиотеку аудиозаписи
#include <WiievaRecorder.h>
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

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


  1. Sasha95
    04.01.2018 01:39

    У Вас какая версия esp-шки? Если использовать esp32, то там 12 битное ацп, думаю, можно было бы и им обойтись. И может быть, вообще обойтись без stm-ки.


    1. olegator99 Автор
      04.01.2018 07:21

      В плате используется esp8266 c 10-ти битным АЦП, но проблема в том, что если включен wifi модуль, то с АЦП считывается только шум.
      esp32 не успел пробовать. Недавно заказал несколько модулей, как приедут — попробую. К встроенному в esp32 АЦП все же отношусь скептически. С большой вероятностью будут такие же грабли — сильная зашумленость от wifi.
      А вариант с внешним цифровым PDM микрофоном планирую опробовать и без stm32 — есть вероятность, что потянет.


      1. dernuss
        04.01.2018 20:33

        Esp32 это все таки 2 ядра на 240 МГц. Должно на всё хватить.


        1. olegator99 Автор
          04.01.2018 23:25

          По производительности конечно потянет. Больше опасаюсь за режим работы I2S. Подключение PDM микрфона к I2S это "фирменный" хак stm32, который возможно на esp32 не прокатит.
          А программно собирать по битику синхронный PDM поток мегабит/сек — надругательство над контроллером.


  1. izzholtik
    04.01.2018 02:09

    /)


  1. mr_filliny
    04.01.2018 07:24

    «и так что бы 4 розетки сразу в одном устройстве» — тройник- есть такая штука в продаже))) и простая розетка радио управляемая типа комплектов Expert и прочих — работает и на реплики из телевизора не отзывается)))


    1. olegator99 Автор
      04.01.2018 07:37

      В обычный тройник-пилот 4-е таких розетки скорее всего не влезут — размеры не позволят, да и управлять ими можно только с родного пульта, без всякого голосового управления, и без интеграции с умным домом…
      А те, что влезут, например, Fibaro будут стоить ~22т.р. за комплект.


  1. AlexanderS
    04.01.2018 11:43

    Я подобную штуку как-то делать пытался на Arduino + EasyVR 2.0. Никаких облаков, работало только от моего голоса. Но там есть вылез знатный косяк: пока обучаешь EasyVR в одном помещении — всё нормально, но как только переносишь девайс в другое помещение процент успешных срабатываний сразу обваливается.


  1. olegator99 Автор
    04.01.2018 12:09
    +1

    Увы, без серьезных вычислительных мощностей/приличного объема памяти/тренировки hmm и нейронки на большом объеме записей сделать speaker independent распознавание, которое будет работать в любом акустическом окружении практически не реально.


    Думаю, что порог входа для качественного распознавания набора команд on-device нужна железка порядка Raspberry PI