Мини-кейс 2: Умная лампа

Умная лампа - RGB-матрица с слайдерами выбора оттенка, насыщенности и яркости.

Перед выполнением прочтите

Информация про модуль RGB LED матрицы от Амперки: http://wiki.amperka.ru/%D0%BF%D1%80%D0%BE%D0%B4%D1%83%D0%BA%D1%82%D1%8B:troyka-rgb-matrix

Информация о библиотеке Adafruit_NeoPixel: https://learn.adafruit.com/adafruit-neopixel-uberguide/arduino-library-use

Создание мини-проекта

Сделаем мини-проект с RGB модулем, который управляется через интерфейс RainMaker. Представим, что наш RGB модуль будет умной лампочкой, которая может менять цвет.

  1. Нужно собрать устройство. К GPIO 17 подключить модуль RGB матрицы Troyka контактом D1. Распиновка платы на родительской странице "О ESP32". Питание для него от пина 3.3v.

  2. Создаём новый файл в Arduino IDE с кодом:

    #include <RMaker.h>
    #include <WiFi.h>
    #include <WiFiProv.h>
    #include <Adafruit_NeoPixel.h> // Библиотека для работы с RGB-матрицей
    
    #define SERVICE_NAME "PROV_2" // Имя устройства при подключении по bluetooth, нужно для настройки, когда плата ещё не настроена на WI-FI точку
    #define POP "abcd2" // ? Пароль для устройства при подключении по bluetooth
    
    #define BOOT_BTN_PIN 0 // GPIO пин кнопки BOOT
    
    #define RGB_MATRIX_PIN 17 // GPIO пина, к которому подключена RGB-матрица
    #define MATRIX_LED_DEFAULT_STATE false // Состояние матрицы при старте
    
    #define MATRIX_LED_COUNT 16 // Количество светодиодов в матрице
    
    #define DEFAULT_LED_HUE 0 // Значение слайдера hue для LED по умолчанию
    #define DEFAULT_LED_SATURATION 255 // Значение слайдера насышенности для LED по умолчанию
    #define DEFAULT_LED_BRIGHTNESS 64 // Значение слайдера яркости для LED по умолчанию
    
    Adafruit_NeoPixel ledMatrix = Adafruit_NeoPixel(MATRIX_LED_COUNT, RGB_MATRIX_PIN, NEO_GRB + NEO_KHZ800); // Создаём объект класса Adafruit_NeoPixel
    
    int matrix_led_gpio = RGB_MATRIX_PIN; // Переменная вашего светодиода
    bool matrix_led_state = MATRIX_LED_DEFAULT_STATE; // Переменная для хранения состояния вашего rgb-модуля
    int led_hue = DEFAULT_LED_HUE; // Значение оттенка цвета вашей лампы
    int led_saturation = DEFAULT_LED_SATURATION; // Значение насыщенности вашей ламы
    int led_brightness = DEFAULT_LED_BRIGHTNESS; // Значение яркости вашей ламы
    
    // Фреймворк предоставляет некоторые стандартные типы устройств, такие как выключатель, лампочка, вентилятор, датчик температуры.
    // Но вы также можете определить пользовательские устройства, используя объект базового класса 'Device', как показано здесь
    Device my_device("SmartLED", ESP_RMAKER_DEVICE_OTHER, &matrix_led_gpio);
    
    void sysProvEvent(arduino_event_t *sys_event)
    {
        switch (sys_event->event_id) {      
            case ARDUINO_EVENT_PROV_START:
              // Для ESP32
              Serial.printf("\nProvisioning Started with name \"%s\" and PoP \"%s\" on BLE\n", SERVICE_NAME, POP);
              printQR(SERVICE_NAME, POP, "ble");   
              // Для ESP32S2
              //Serial.printf("\nProvisioning Started with name \"%s\" and PoP \"%s\" on SoftAP\n", service_name, pop);
              //printQR(service_name, pop, "softap");   
              break;
        }
    }
    
    // Функция обработки изменений
    void write_callback(Device *device, Param *param, const param_val_t val, void *priv_data, write_ctx_t *ctx)
    {
      const char *device_name = device->getDeviceName();
      const char *param_name = param->getParamName();
    
      if(strcmp(param_name, ESP_RMAKER_DEF_POWER_NAME) == 0) { // Если параметр с именем Power
        matrix_led_state = val.val.b; // Записываем значение состояния вашего светодиода в переменную, обратите внимание, что b - это тип bool
        Serial.printf("Received param %s = %s for %s\n", param_name, (matrix_led_state? "true" : "false"), device_name);
        if (!matrix_led_state) {
          ledMatrix.clear(); // Очистить матрицу
          ledMatrix.show(); // Отправить изменения на матрицу
        } else {
          uint32_t rgbcolor = ledMatrix.ColorHSV(led_hue, led_saturation, led_brightness);
          ledMatrix.fill(rgbcolor); // Закрасить матрицу
          ledMatrix.show(); // Отправить изменения на матрицу
        }
        param->updateAndReport(val);
      } else if (strcmp(param_name, ESP_RMAKER_DEF_HUE_NAME) == 0) { // Если параметр с именем Hue
        led_hue = map(val.val.i, 0, 360, 0, 65535); // Записываем значение в переменную, i - это тип int и перевести в нужный дапазон по инфе с библиотеки
        if (matrix_led_state) { // Если тумблер матрицы вкл, тогда установим новый hue
          uint32_t rgbcolor = ledMatrix.ColorHSV(led_hue, led_saturation, led_brightness);
          ledMatrix.fill(rgbcolor); // Закрасить матрицу
          ledMatrix.show(); // Отправить изменения на матрицу
        }
        Serial.printf("Received param %s = %d for %s\n", param_name, led_hue, device_name);
        param->updateAndReport(val);
      } else if (strcmp(param_name, ESP_RMAKER_DEF_SATURATION_NAME) == 0) { // Если параметр с именем Saturation
        led_saturation = val.val.i; // Записываем значение яркости вашей лампы в переменную, i - это тип int
        if (matrix_led_state) { // Если тумблер матрицы вкл, тогда установим новую яркость
          uint32_t rgbcolor = ledMatrix.ColorHSV(led_hue, led_saturation, led_brightness);
          ledMatrix.fill(rgbcolor); // Закрасить матрицу
          ledMatrix.show(); // Отправить изменения на матрицу
        }
        Serial.printf("Received param %s = %d for %s\n", param_name, led_saturation, device_name);
        param->updateAndReport(val);
      } else if (strcmp(param_name, ESP_RMAKER_DEF_BRIGHTNESS_NAME) == 0) { // Если параметр с именем Brightness
        led_brightness = val.val.i; // Записываем значение яркости вашей лампы в переменную, i - это тип int
        if (led_brightness == 0) { // На слайдере установлено 0
            matrix_led_state = false; // Установить переменную состояния светодиода на выключен
            ledMatrix.clear(); // Очистить матрицу
            ledMatrix.show(); // Отправить изменения на матрицу
            my_device.updateAndReportParam(ESP_RMAKER_DEF_POWER_NAME, false); // Изменяем состояние тумблера на выкл в RainMaker
            Serial.printf("Toggle State to false.\n");
        } else if (matrix_led_state) { // Если тумблер лампы вкл, тогда установим новую яркость
          uint32_t rgbcolor = ledMatrix.ColorHSV(led_hue, led_saturation, led_brightness);
          ledMatrix.fill(rgbcolor); // Изменить заливку
          ledMatrix.show(); // Отправить изменения на матрицу
        }
        Serial.printf("Received param %s = %d for %s\n", param_name, led_brightness, device_name);
        param->updateAndReport(val);
      }
    }
    
    void setup()
    {
      Serial.begin(115200); // Инициализируем скорость Serial
      pinMode(BOOT_BTN_PIN, INPUT); // Настраиваем пин кнопки BOOT на INPUT
    
      ledMatrix.begin(); // Инициализация RGB-матрицы
      if (!MATRIX_LED_DEFAULT_STATE) { // Если матрица изначально должна быть выключена
        ledMatrix.clear(); // Очистить матрицу
        ledMatrix.show(); // Отправить изменения на матрицу
      }
      
      Node my_node; // Объявляем Node
      my_node = RMaker.initNode("ESP RainMaker Node"); // Инициализируем Node
    
      // Создаём устройство для управления светодиодом
      my_device.addNameParam(); // Устнаваливаем имя устройства
      my_device.addPowerParam(MATRIX_LED_DEFAULT_STATE); // Устанавливаем стандатное значение
      my_device.assignPrimaryParam(my_device.getParamByName(ESP_RMAKER_DEF_POWER_NAME)); // Устанавливаем имя параметра Power
    
      // Создаём и добавляем слайдер hue
      Param hue_param(ESP_RMAKER_DEF_HUE_NAME, ESP_RMAKER_PARAM_HUE, value(DEFAULT_LED_HUE), PROP_FLAG_READ | PROP_FLAG_WRITE);  // Указываем название создаваемого параметра, тип параметра, стандартное значение и задаём права
      hue_param.addBounds(value(0), value(360), value(1)); // Знач-е диапазона от 0 до 360, в данном случае праметр только для отображения, т.к. значение будет приходить всё-равно от 0 до 360. Если не указать будет от 0 и до 0
      hue_param.addUIType(ESP_RMAKER_UI_HUE_SLIDER); // Устанавливаем тип отображения как HUE-слайдер
      my_device.addParam(hue_param); // Добавляем параметр устройству
    
      // Создаём и добавляем слайдер saturation
      Param saturation_param(ESP_RMAKER_DEF_SATURATION_NAME, ESP_RMAKER_PARAM_SPEED, value(DEFAULT_LED_SATURATION), PROP_FLAG_READ | PROP_FLAG_WRITE);  // Указываем название создаваемого параметра, тип параметра, стандартное значение и задаём права
      saturation_param.addBounds(value(0), value(255), value(1)); // Значение от 0 до 255
      saturation_param.addUIType(ESP_RMAKER_UI_SLIDER); // Устанавливаем тип отображения как слайдер
      my_device.addParam(saturation_param); // Добавляем параметр устройству 
    
      // Создаём и добавляем слайдер brightness
      Param brightness_param(ESP_RMAKER_DEF_BRIGHTNESS_NAME, ESP_RMAKER_PARAM_SPEED, value(DEFAULT_LED_BRIGHTNESS), PROP_FLAG_READ | PROP_FLAG_WRITE);  // Указываем название создаваемого параметра, тип параметра, стандартное значение и задаём права
      brightness_param.addBounds(value(0), value(255), value(1)); // Значение от 0 до 255
      brightness_param.addUIType(ESP_RMAKER_UI_SLIDER); // Устанавливаем тип отображения как слайдер
      my_device.addParam(brightness_param); // Добавляем параметр устройству
      
      my_device.addCb(write_callback); // Устанавливаем функцию на обработчик
      my_node.addDevice(my_device); // Добавить девайс на ноду
    
      // Опционально
      RMaker.enableOTA(OTA_USING_PARAMS); // Включить беспроводное обновление
      RMaker.setTimeZone("Europe/Moscow"); // Установить часовой пояс https://rainmaker.espressif.com/docs/time-service.html
      RMaker.enableTZService(); // Кроме того, включите службу часового пояса и позвольте приложениям телефона установить соответствующий часовой пояс
    
      RMaker.enableSchedule(); // Включить расписание для нашего устройства
      
      RMaker.start(); // Запуск сервиса на устройстве
    
      WiFi.onEvent(sysProvEvent);
      WiFiProv.beginProvision(WIFI_PROV_SCHEME_BLE, WIFI_PROV_SCHEME_HANDLER_FREE_BTDM, WIFI_PROV_SECURITY_1, POP, SERVICE_NAME);
    }
    
    void loop()
    {
      // Обрабатываем нажатие на кнопка BOOT
      if(digitalRead(BOOT_BTN_PIN) == LOW) {
          delay(100); // Для исключения дребезка на тактовой кнопке
          int startTime = millis(); // Время на номент нажатия кнопки
          while(digitalRead(BOOT_BTN_PIN) == LOW) delay(50); // Цикл, который создаёт паузу для ожидания отжатия кнопки
          int endTime = millis(); // Время на момент отжатия кнопки
          int pressTime = endTime - startTime; // Время, на которое кнопка была нажата
          // Условия по времени нажатия
          if (pressTime > 10000) { // Если кнопка была зажата на более 10 секунд, тогда сбросить всё
            Serial.printf("Reset to factory.\n");
            RMakerFactoryReset(2);
          } else if (pressTime > 3000) { // Иначе если кнопка была зажата больше чем на 3 секунды, но меньше 10, тогда сбросить настройки WI-FI
            Serial.printf("Reset Wi-Fi.\n");
            RMakerWiFiReset(2);
          } else { // В остальных случаях
            // ToDo
         }
      }
      delay(10);
    }

    Изучите код, прочитайте комментарии к коду!

  3. Добавьте библиотеку Adafruit_NeoPixel. Вы можете это сделать через пункт "Управление библиотками" или скачать из интернета и добавить как zip библиотеку.

  4. В инструментах вы должны выбрать:

    • Плата: ESP32 Arduino > ESP32 Dev Module;

    • Partition Scheme: RainMaker.

    И выберите порт платы. Загрузите прошивку. Никаких пинов для перевода контроллера в режим прошивки зажимать не нужно.

  5. Далее дайдите в монитор порта, установить скорость 115200. Выведется информация о том, как подключить плату к сервису. Там должна быть строка с QR-кодом и ссылка. Скопируйте ссылку и вставьте в адресную строку браузера.

    На этом этапе могут возникнуть две известные проблемы...

    • Плата пишет что-то на английском, но не выдаёт ссылку, тогда нужно сбросить настройки WI-FI, зажав BOOT на 3 секунды и отпустив. * Если плата уже была до этого настроена, тогда она будет выводить после прошивки текст на английском, QR-код уже не выведется. Это нормально, но пользоваться дальше можно!

    • Плата пишет всякие кракозябры, хотя скорость последовательного порта выставлена верно. Вам нужно сбросить настройки прошивки. Для этого зажмите кнопку BOOT на плате на 10 секунд. По истечению времени отпустите, плата перезапустит код и в монитор порта текст станет читабельный и выведется ссылка.

  6. Откройте приложение RainMaker. В приложении RainMaker нажать на кнопку с иконкой ПЛЮС, запуститься сканер QR-кода.

  7. Отсканировать сканером QR-код с страницы с браузера.

  8. В приложении RainMaker выбрать точку доступа для устройства, дать пароль и подождать подключения и всяких других настроек.

  9. На главной странице RainMaker появится или обновится ваш девайс.

Проблема с настрокой точки доступа:

Если необходимо сменить WI-FI точку для устройства, но в приложении не отображает список доступных точек, тогда необходимо на экране нажать на "Join Other Network" и ввести название точки и пароль к ней самостоятельно.

Дополнительное задание:

  1. Необходимо сделать, чтобы кнопка BOOT на плате отключала/включала (меняла состояние) нашу "Умную лампочку". Пример можно подсмотреть в прошлом мини-кейсе. * Важное замечание о том, что когда меняете состояние на устройстве, то ещё нужно в сервисе RainMaker также поменять состояние:

    my_device.updateAndReportParam(PARAM_NAME, value); // Изменяем состояние тумблера в RainMaker
  2. Установить для девайса тип "Lightbulb" с картинкой, на которой изображена лампочка. * Подсказка, UI типы вы найдёте тут.

  3. Необходимо для слайдера Hue поменять UI тип Hue Slider на Hue Circle.

  4. Для параметра насыщенности и яркости поменять тип параметра на более подходящий - Saturarion Slider и Brightness Slider.

  5. Сделать ещё вариант заливки матрицы с UI типом Dropdown (выпадающий список). Пример использования Dropdown есть в стандартном примере RMakerCustomAirCooler. Первый режим (допустим с названием Fill) дожен будет у вас закрашивать все светодиоды моментально как это было изначально, а второй (с именем ColorWipe) с помощью функции ColorWipe будет закрашивать матрицу постепенно. Посмотрите какие функции доступны в библиотеке для управление матрицой.

    // Переделанная функция заполнения каждого сегмента из примера Амперки
    void ColorWipe(uint32_t color, uint8_t wait)
    {
      for (uint16_t i = 0; i < ledMatrix.numPixels(); i++) {
        // Заполняем текущий сегмент выбранным цветом
        //ledMatrix.setPixelColor(i, color); // Этот метод также работает
        ledMatrix.fill(color, i, 1);
        ledMatrix.show();
        delay(wait); // Ждём
      }
    }

    *Подсказка реализации:

    1. Добавьте глобальную переменную modes с определёнными названиями режимов:

      static const char* fill_modes[] = { "Fill", "ColorWipe" }; // Массив со значениями режимов
    2. Добавьте define со значением режима по умолчанию, например равную 1:

      #define DEFAULT_FILL_MODE 1 // Значение режима заливки по умолчанию

      А также добавьте, глобальную переменную, в которую будете записывать значение режима при выборе:

      int fill_mode = DEFAULT_FILL_MODE; // Переменая хрененения режим заливки

      ...

    3. Добавьте в функцию setup новый параметр:

      Param mode_param("Mode", ESP_RMAKER_PARAM_MODE, value(fill_modes[DEFAULT_FILL_MODE - 1]), PROP_FLAG_READ | PROP_FLAG_WRITE);
      mode_param.addValidStrList(fill_modes, 2); // Подставляем массив возможных режимов и размер массива
      mode_param.addUIType(ESP_RMAKER_UI_DROPDOWN); // Устанавливаем тип UI
      my_device.addParam(mode_param);

      ...

    4. В функции - обработчике изменений write_callbackнеобходимо добавить обработку этого нового параметра, что если полученное значение == "Fill", тогда установить певый режим, иначе, если значение равно "ColorWipe, тогда установить второй режим:

        ...
      } else if (strcmp(param_name, "Mode") == 0) {
        const char* mode = val.val.s;
        if (strcmp(mode, "Fill") == 0) {
          fill_mode = 1;
        } else if (strcmp(mode, "ColorWipe") == 0) {
          fill_mode = 2;
        }
        Serial.printf("Received param %s = %d for %s\n", param_name, mode, device_name);
        param->updateAndReport(val);
      }

      ...

    5. В местах кода, где необходимо управлять матрицой (включать/выключать или изменять параметры) необходимо написать логическое условие какой режим будет выполнятся... первый (изначальный) или второй (новый - функция ColorWipe). В силу того, что эти строки будут повторяться, то советую оптимизировать код, создав одну функцию, которую будете вызывать в необходимых местах кода.

Last updated