Привет, Хабр. В прошлом году сделал "умный" удлинитель для управления гирляндами на елочке. Но тогда руки так и не дошли написать об этом статью. Исправляюсь.
Сама елочка
На елочке 3 гирлянды, а под ней выводок светящихся белых мишек. Когда гирлянд много, встает вопрос — как ими управлять? Каждый раз залезать под елочку и включать/выключать из розетки нужные гирлянды — сомнительное удовольствие.
Конечно, продается большое количество "умных розеток" — но с голосовым управлением, и так что бы 4 розетки сразу в одном устройстве, без лишних проводов и блоков питания — таких не встречал.
Итак приступим
В качестве корпуса идеально подошел удлинитель "пилот". Замеченный и купленный в ближайшем магазине.
Блок питания на 5 вольт и шилд с 4 реле были найдены в запасах мелочевки купленной в на aliexpress.
Теперь, самое главное — "мозги". Мозгами в проекте будет плата Wiieva, в которой есть все, что нам надо — экран, микрофон, wifi, форм фактор ардуино, совместимый с реле шилдом. WiFi модуль реализован на суперпопулярном esp8266, управление периферией и работа со звуком — на stm32f105rbt.
Собираем умный удлинитель
- Вырезаем в корпусе отверстие под экран. Под вырез попала одна розетка и старый выключатель — невелика потеря.
С обратной стороны корпуса, внизу двухсторонний скотч — чтобы плата плотнее сидела
Разделяем шину, к которой подключены розетки, и выводим провода от каждой розетки отдельно. Монтируем силовую часть с блоком питания.
Подключаем мозги — соединяем плату 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...
несколько полезных ссылок
Комментарии (9)
mr_filliny
04.01.2018 07:24«и так что бы 4 розетки сразу в одном устройстве» — тройник- есть такая штука в продаже))) и простая розетка радио управляемая типа комплектов Expert и прочих — работает и на реплики из телевизора не отзывается)))
olegator99 Автор
04.01.2018 07:37В обычный тройник-пилот 4-е таких розетки скорее всего не влезут — размеры не позволят, да и управлять ими можно только с родного пульта, без всякого голосового управления, и без интеграции с умным домом…
А те, что влезут, например, Fibaro будут стоить ~22т.р. за комплект.
AlexanderS
04.01.2018 11:43Я подобную штуку как-то делать пытался на Arduino + EasyVR 2.0. Никаких облаков, работало только от моего голоса. Но там есть вылез знатный косяк: пока обучаешь EasyVR в одном помещении — всё нормально, но как только переносишь девайс в другое помещение процент успешных срабатываний сразу обваливается.
olegator99 Автор
04.01.2018 12:09+1Увы, без серьезных вычислительных мощностей/приличного объема памяти/тренировки hmm и нейронки на большом объеме записей сделать speaker independent распознавание, которое будет работать в любом акустическом окружении практически не реально.
Думаю, что порог входа для качественного распознавания набора команд on-device нужна железка порядка Raspberry PI
Sasha95
У Вас какая версия esp-шки? Если использовать esp32, то там 12 битное ацп, думаю, можно было бы и им обойтись. И может быть, вообще обойтись без stm-ки.
olegator99 Автор
В плате используется esp8266 c 10-ти битным АЦП, но проблема в том, что если включен wifi модуль, то с АЦП считывается только шум.
esp32 не успел пробовать. Недавно заказал несколько модулей, как приедут — попробую. К встроенному в esp32 АЦП все же отношусь скептически. С большой вероятностью будут такие же грабли — сильная зашумленость от wifi.
А вариант с внешним цифровым PDM микрофоном планирую опробовать и без stm32 — есть вероятность, что потянет.
dernuss
Esp32 это все таки 2 ядра на 240 МГц. Должно на всё хватить.
olegator99 Автор
По производительности конечно потянет. Больше опасаюсь за режим работы I2S. Подключение PDM микрфона к I2S это "фирменный" хак stm32, который возможно на esp32 не прокатит.
А программно собирать по битику синхронный PDM поток мегабит/сек — надругательство над контроллером.