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

Сегодня и мы попробуем сделать что-то такое, а также добавим управление голосом с помощью виртуальных ассистентов Салют.

Что понадобится?

  • Контролер ESP32 – на самом деле, будет достаточно любого контроллера с возможностью подключения к интернету: это может быть и ESP8266, и обычная Arduino с подключенным Ethernet-шилдом. Код для платформ может отличаться, обратите на это внимание. Примечание: весь код приведённый в статье тестировался на ESP32.

  • RGB – светодиодная матрица на основе адресных светодиодов WS2812B. Мы будем использовать матрицу 8x8, но не стоит ограничивать свою фантазию. Важно: для питания светодиодов напряжения контроллера не будет хватать, так что позаботьтесь о блоке питания на 5 вольт.

  • Программа Blynk – это платформа для разработки IoT-устройств. Понадобится установить приложение на ваш мобильный телефон.

  • Arduino IDE – среда разработки ПО для микроконтроллеров на C-подобном языке программирования.

  • SmartApp Code – среда для разработки смартапов (навыков для виртуальных ассистентов Салют).

Часть первая. Hardware

Подключим нашу светодиодную матрицу к контроллеру и блоку питания. Важно, что земля должна быть общая и у контроллера, и у матрицы, а вот питание на контроллер можно подавать отдельно. Например, через USB.


На данной схеме приведён пример подключения. Чёрные провода – это “земля”, красные – “питание”, а синий – для управления матрицей. Провода, выходящие за пределы макетной доски, подключаются к блоку питания.
На данной схеме приведён пример подключения. Чёрные провода – это “земля”, красные – “питание”, а синий – для управления матрицей. Провода, выходящие за пределы макетной доски, подключаются к блоку питания.

На матрице расположены три пина: DIN, VIN и GND. Первый необходим для управления матрицей и подключается к одному из пинов контроллера (я подключил к 26-му), второй и третий это питание – плюс и минус.

Часть вторая. Настройка Blynk

После установки приложения Blynk на телефон и регистрации необходимо создать новый проект. Назовём его Гирлянда, выберем в качестве девайса ESP32 Dev Board, тип подключения – Wi-Fi.

Проект в Blynk – это поле, на которое можно размещать различные виджеты, позволяющие общаться (отправлять и получать данные) с нашим девайсом. Для начала добавим на наше поле виджет, отвечающий за управление цветом – zeRGBa .

После добавления виджета на поле настроим его. Для того, чтобы перейти в меню настройки виджета, достаточно нажать на него.

  1. Мы хотим, чтобы значение цвета передавалось в одной переменной, для этого поставим тумблер в положении MEGRE.

  2. Укажем виртуальный пин, с которым будет связан этот виджет – V0.

  3. В формате RGB значение каждого из цветовых каналов варьируется от 0 до 255, так что изменим максимальные значения с дефолтного 1023 на 255 на каждом из каналов.

Но с помощью этого виджета мы сможем управлять только цветом, а хотелось бы ещё включать и выключать режимы мигания и смены изображения. Для включения и выключения режима мигания создадим кнопку и привяжем её к виртуальному пину – V1. Кнопка – бинарная сущность, она может отправлять либо 0, либо 1.

Добавим ещё немного функций нашему девайсу. Будем хранить в памяти несколько изображений и переключаться между ними. Для этого добавим ещё один виджет – слайдер. В статье ограничимся только двумя изображениями, но не ограничивайте свою фантазию. (Кстати, матрицы могут быть не только 8x8.)

В текущей реализации все картинки хранятся на устройстве, в коде, так что нам нужно будет отправлять только её номер. Поскольку количество картинок может меняться, будем использовать слайдер, указав в качестве значений необходимый нам диапазон картинок. У нас их будет всего две – сердечко и смайлик – и номера будут соответственно 1 и 2. Слайдер также необходимо соединить с виртуальным пином, V2.

При создании проекта вам на почту пришёл AUTH TOKEN, он нам будет очень нужен и при программировании микроконтроллера, и при создании навыка для виртуальных ассистентов Салют. Его также всегда можно посмотреть в настройках проекта, для этого достаточно нажать на символ гайки.

Часть третья. Программирование микроконтроллера

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

Один из подходов, используемый для "рисования" – это “закрашивание” светодиодов с конкретным индексом. Таким образом, для рисунка сердечка нам понадобится закрасить эти светодиоды:

Это достаточно удобно: нам нужно хранить только номера, а цвет может быть любым. Но если мы захотим сделать изображение с использованием разных цветов, например такое, как на превью статьи:

то нам придётся хранить несколько массивов – по одному для каждого цвета.

Теперь перейдём в Arduino IDE и запрограммируем наш контроллер. Описывать процесс настройки Arduino IDE для работы с ESP32 – слишком долго и скучно, кроме того, эту информацию легко найти в сети, например здесь.

Разберём непосредственно код:

#include <WiFi.h>
#include <WiFiClient.h>
#include <BlynkSimpleEsp32.h>
#include <Adafruit_NeoPixel.h>


#define PIN 26
#define NUMPIXELS 64


// Флаг показывающий режим работы - 0 - свечение, 1 - мигание
int flag = 0;
// Флаг показывающий состояние светодиодов - 0 - горят, 1 - выключены
int neopixel_status = 0;
// таймер
unsigned long timer = 0;
// флаг показывающий, какую картинку включать
int image = 1;

// Инициализация светодиодной ленты
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);

// AUTH TOKEN из приложения Blynk
char auth[] = "";

// Данные для подклчения к Wi-Fi сети
char ssid[] = "";
char pass[] = "";

// Глобальные переменные, отвечающие за цвет
int R = 100, G = 100, B = 100;

// Функция, окрашивающаяя всю матрицу в один цвет
void color(int R, int G, int B) {
    for (int i = 0; i < NUMPIXELS; i++) pixels.setPixelColor(i, pixels.Color(R, G, B));
    pixels.show();
}

// Функция по отрисовке разноцветного смайлика
void draw_smile() {
    color(0, 0, 0);
    int circle[] = {2, 3, 4, 5, 9, 14, 16, 23, 24, 31, 32, 39, 40, 47, 49, 54, 58, 59, 60, 61};
    int eyes[] = {18, 21};
    int mouth[] = {42, 45, 51, 52};

    for (int i = 0; i < 20; i++) {
        pixels.setPixelColor(circle[i], pixels.Color(0, 50, 150));
    }
    for (int i = 0; i < 2; i++) {
        pixels.setPixelColor(eyes[i], pixels.Color(0, 150, 50));
    }
    for (int i = 0; i < 4; i++) {
        pixels.setPixelColor(mouth[i], pixels.Color(150, 0, 0));
    }
    pixels.show();
}

// Функция по отрисовке сердечка заданного цвета
void draw_heart(int R, int G, int B) {
    color(0, 0, 0);
    int heart[] = {9, 10, 13, 14, 16, 17, 18, 19, 20, 21, 22, 23, 
                   24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36,
                   37, 38, 39, 41, 42, 43, 44, 45, 46, 50, 51, 52, 53, 59, 60};
    for (int i = 0; i < 40; i++) {
        pixels.setPixelColor(heart[i], pixels.Color(R, G, B));
    }
    pixels.show();
}


// Обработчик виртуального пина V2. Изменение положения слайдера - меняет изображения
BLYNK_WRITE(V2)
{
    image = param.asInt();
    select_image(image);
}

// Обработчик виртуального пина V1. В переменную флаг записывается положение кнопки
BLYNK_WRITE(V1)
{
    flag = param.asInt();
}

/* Обработчик виртуального пина V0. В соответствующие переменные, отвечающие за составление цвета, записываются полученные значение, а затем отрисовывается картинка.
 */
BLYNK_WRITE(V0)
{
    R = param[0].asInt();
    G = param[1].asInt();
    B = param[2].asInt();
    select_image(image);
}

// Функция отрисовывающая картинку, в зависимости от номера
void select_image(int type) {
    switch (type) {
        case 1:
            draw_heart(R, G, B);
            break;
        case 2:
            draw_smile();
            break;
    };
}

void setup() {
    Blynk.begin(auth, ssid, pass); // Подключение к Blynk-серверу
    pixels.begin(); // Инициализация светодиодов
    select_image(image); // отображение значения по умолчанию (белое сердечко)
}


void loop() {
    Blynk.run();
    if (flag != 0) {
        if (timer == 0) {
            if (neopixel_status == 0) {
                neopixel_status = 1;
                color(0, 0, 0);
                timer = millis();
            } else {
                neopixel_status = 0;
                select_image(image);
                timer = millis();
            }
        } else if (millis() - timer > 1000) {
            if (neopixel_status == 0) {
                neopixel_status = 1;
                color(0, 0, 0);
                timer = millis();
            } else {
                neopixel_status = 0;
                select_image(image);
                timer = millis();
            }
        }
    }

}

Сначала подключим необходимые нам библиотеки – для работы с Wi-Fi, Blynk и светодиодной лентой (Adafruit_NeoPixel). Затем зададим константы: количество светодиодов, пин, к которому подключена лента, и период срабатывания таймера (в миллисекундах).

После этого проинициализируем светодиодную ленту и заполним необходимые данные для подключения к Blynk: SSID и пароль от Wi-Fi, а также AUTH TOKEN.

У нас будет использоваться несколько функций:

  • void color(int R, int G, int B) – окрашивает все светодиоды в матрице в заданный цвет. По своей сути это своеобразная "заливка";

  • void draw_smile() – рисует разноцветный смайлик заранее заданными цветами. У нас есть три массива, каждый из которых отвечает за соответствующую часть смайлика, то есть за один цвет; void draw_heart(int R, int G, int B) – рисует сердце, заданного цвета;

  • void select_image(int type) – в зависимости от входного параметра выбирает, какое изображение отрисовывать.

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

Поскольку распараллеливание задач на микроконтроллерах представляет собой довольно сложную задачу, мы сымитируем работу параллельного потока с помощью нескольких флагов. Это происходит в функции loop.

Если вы всё сделали правильно, то уже сейчас вы можете управлять своим устройством с помощью мобильного приложения Blynk. Но самое интересное ещё впереди!

Часть четвёртая. Голосовое управление

Для того, чтобы не открывать каждый раз приложение на телефоне, а использовать все преимущества голосового управления, создадим Chat App.

Chat App — это простой тип смартапа, который можно создать даже без навыков программирования. Он представляет собой диалоговое приложение между пользователем и виртуальным ассистентом, в котором пользователь отправляет запрос, а ассистент возвращает ответ. Для создания Chat App-ов будем использовать среду SmartApp Code, подробную документацию к которой можно найти тут.

Для начала поймём, что мы хотим от нашего приложения. В нашем Chat App будут доступны следующие команды:

  • выбор картинки;

  • выбор цвета для картинки;

  • включение и отключение режима мигания;

  • полное выключение дисплея.

Чтобы обрабатывать сообщения пользователя в SmartApp Code, создадим интенты и сущности.

Интент — это намерение, которую пользователь формулирует в конкретной реплике. Например, фразы: “включи”, “зажги свет” и подобные для умных устройств или "какая сегодня погода?" при общении с ассистентом.

Сущность — это конкретное значение, которое может содержаться в запросе пользователя.

Например, в запросе пользователя “включи красный свет” “включи” – это намерение, а “красный” – сущность, отвечающая за цвет.

У нас будет два типа сущностей:

  • Цвет – color, который в качестве значения будет отправлять массив, содержащий значения цвета в RGB-формате.

  • Изображение – image, который в качестве значения будет отправлять порядковый номер изображения, что соответствует значению отправляемому с помощью слайдера.

Откроем SmartApp Code, создадим новый проект и в нём добавим необходимые нам сущности. Добавим сущность color и заполним справочник. В качестве значений для каждого из элементов необходимо указать список в формате ["R", "G", "B"]. Например, для красного цвета – ["255", "0","0"]. Это аналог значений, отправляемых с нашего zeRGBa в приложении.

Теперь нужно создать интенты. В качестве примера рассмотрим интент выбора цвета: /EnableColor.

Для корректной работы интента будем использовать так называемый "слотфиллинг". То есть сущности, которые будут извлекаться из фразы пользователя, будут помещаться в специальные поля – слоты.

Но слотфилинг нужен далеко не всегда. Очень часто интент можно задать просто набором фраз. Например, интент отвечающий за выключение – /Off.

Все интенты и сущности будут доступны в репозитории, ссылка на который будет в самом конце статьи. Подробную инструкцию по разворачиванию проекта можно найти тут.

После того, как мы создадим все интенты и сущности, нам необходимо создать сценарий, в котором и будет производиться взаимодействие между нашим приложением и устройством. Одной из причин выбора Blynk стало API, которое они предоставляют. Так что всё, что нам нужно, это отправлять запрос. Напишем небольшую функцию на js:

function set_pin_value(pin, value) {
    var context = $jsapi.context();
    var options = {
        dataType: "json",
        headers: {
            "Content-Type": "application/json",
        },
        body: value
    };
    var response = $http.put("http://blynk-cloud.com/" + context.injector.TOKEN + "/update/" + pin, options);
}

Давайте разберёмся, что в ней происходит. Кажется, что всё стандартно, за исключением какого-то context, внутри которого лежит какой-то injector, из которого мы и достаём нужный нам TOKEN (это всё тот же токен, который мы получили в blynk). Где же лежит этот injector? А задаётся он, внутри автоматически создаваемого файла chatbot.yaml. Необходимо лишь добавить в конец этого файла строки:

injector:
  TOKEN: YOUR_BLINK_TOKEN

Так, с функцией разобрались, но теперь нужно её где-то разместить, а затем и вызвать. В SmartApp Code, во вкладке "Сценарии", можно создать папку js, а в ней – файл blynk.js, в который и нужно поместить нашу функцию.

Но мало её создать, нужно где-то её вызывать. Для этого открываем файл сценария main.sc и начинаем создавать состояния:

require: zenflow.sc
  module = sys.zfl-common

require: slotfilling/slotFilling.sc
    module = sys.zb-common
  
require: js/blynk.js


theme: /
    state: EnableColor
        intent!: /EnableColor
        script:
            set_pin_value("V0",$parseTree._color);
        a: Готово

    state: Blink
        intent!: /Blink
        script:
             set_pin_value("V1",["1"]);
        a: Готово

    state: StopBlink
        intent!: /StopBlink
        script:
            set_pin_value("V1",["0"]);
        a: Готово

    state: Off
        intent!: /Off
        script:
            set_pin_value("V1",["0"]);
            set_pin_value("V0",["0","0","0"]);
        a: Готово

    state: ChangeImage
        intent!: /ChangeImage
        script:
            set_pin_value("V2",[$parseTree._image]);
        a: Готово

    state: Fallback
        event!: noMatch
        a: Вы сказали: {{$parseTree.text}}
        

Рассмотрим для примера какой-нибудь стейт, например – смену цвета:

    state: EnableColor
        intent!: /EnableColor
        script:
            set_pin_value("V0",$parseTree._color);
            if ($parseTree._image == 1){
                set_pin_value("V2",[$parseTree._image])
            }
        a: Готово

В том случае, если срабатывает интент смены цвета, мы отправляем на виртуальный пин V2 значение, которое попало в отвечающий за цвета слот (аналог модуля zeRGBa в приложении Blynk).

На что важно обратить внимание: когда мы создавали интенты, мы указывали переменные, в которые попадут извлечённые нами сущности. Чтобы получить эти значения в коде, мы должны обратиться к объекту $parseTree и запросить переменную с использованием нижнего подчёркивания перед названием. Например, $parseTree._color.

Давайте соберём наш навык и протестируем его с помощью тестового виджета внутри SmartApp Code. Для этого необходимо нажать на кнопку Собрать, а затем запустить тестовый виджет, нажав на зелёную стрелку. Напишем что-нибудь в поле для ввода. Например: включи синее сердце.

Осталось опубликовать ваше приложение, если вы хотите, чтобы им могли воспользоваться ваши друзья. После прохождения модерации и публикации оно появится в каталоге приложений на устройствах Sber. Если девайсов под рукой нет, можно запустить приложение на смартфоне – в Сбер Салют. Для этого необходимо просто создать черновик в SmartMarket Studio и указать в качестве вебхука – вебхук из SmartApp Code. Его можно найти в разделе Публикации.

Заключение

Это был пример несложного проекта, с помощью которого можно сделать первые шаги в разработке собственных умных устройств с использованием инструментов SmartApp Code. Этим, конечно, возможности платформы SmartMarket не ограничиваются, подробнее о возможностях разработки можно узнать здесь. А исходный код для навыка и для микроконтроллера можно найти в моём репозитории.

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


  1. dcoder_mm
    19.08.2021 13:19
    +2

    А я один прочитал заголовок как "Светодиодная матрица: управляем домашним утюгом с помощью голоса" и пришел в пост с очень завышенными ожиданиями?


    1. x-tea
      19.08.2021 14:05
      +1

      Не один... =)


      1. Nikita_64
        19.08.2021 14:18
        +1

        И я…


        1. habrabkin
          19.08.2021 14:23
          +1

          Рядом Самсунг утюгом управлял и я также прочитал))


          1. shaggyone
            19.08.2021 18:36
            +1

            И меня посчитайте, пожалуйста


            1. dcoder_mm
              19.08.2021 19:07

              Нас собралось уже 5 человек. Самое время организовать стартап и сделать управляемый голосом утюг со светодиодной матрицей.

              Ключевые фичи:

              • Если сказать утюгу "пшшшш" он ответит паром "пшшшш". Можно подключить машинное обучение, чтобы пользователь мог вести долгие и интересные диалоги с утюгом. Пшшш? Пшшш!

              • Саркастичные напоминания о том, что вы забыли погладить одежду (каждый месяц новая фраза, подписка от 9.99$ в месяц).

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

              • Защита от детей (нужно набрать 16-значный пароль колесом регулировки температуры, чтобы разблокировать утюг)

              • Дополнительные режимы "барбекю", "попкорн" и "сувид" с каталогом рецептов.

              • Бесплатное приложение, напоминающее пользователю проверить, выключил ли он утюг, когда тот отходит больше чем на 500м от дома (+ in app purchase для отключения уведомления)

              Буква S в слове Утюг это Security: поддерживается современный протокол шифрования WEP для беспроводной сети.


              1. usa_habro_user
                22.08.2021 11:19

                Посчитал всех, учитывая "карму" ;) Действительно, заголовок статьи "провокационный"!


                1. gev
                  02.11.2021 17:07

                  +1


    1. wrewolf
      14.09.2021 12:53

      Так это же нативочка от сбера подъехала


  1. ainu
    16.09.2021 13:58

    ESP32 позволяет распознавать intent офлайн, мощи хватает у него, только I2S микрофон нужен. На крайний случай приглашение (а-ля Салют! Эй сири! Окей гугл!).

    Другой вопрос — у вас есть API для голосового распознавания? Чтото не видно ссылок на документацию, цены и так далее. Я имею в виду сервис а-ля Wit.AI, чтобы без мобильного приложения, а сразу туда пулять wav файл, и в ответ JSON. Без Салюта как девайса в принципе и без мобильного приложения.


    1. ainu
      16.09.2021 14:02

      Алсо, лучше сразу советовать PlatformIO вместо Arduino IDE, а не вот это вот, ну и на ESP32 можно без всяких blynk поднять web-сервер, слушать порт, принимать GET и POST запросы, там всё равно несколько строчек.