> For the complete documentation index, see [llms.txt](https://itdocs.kvantorium21.ru/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://itdocs.kvantorium21.ru/it-engineering/i.-internet-veshei/case-1b/mini-keis-2-umnaya-lampa.md).

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

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

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

{% hint style="info" %}
Информация про модуль 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>
{% endhint %}

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

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

1. Нужно собрать устройство. К `GPIO 17` подключить модуль RGB матрицы Troyka контактом `D1`.\
   [Распиновка платы на родительской странице "О ESP32"](/it-engineering/i.-internet-veshei/case-1b/o-esp32.md#raspinovka). Питание для него от пина `3.3v`.

   <figure><img src="https://1234485992-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LuCyLoyEs8r9SqV2MS6%2Fuploads%2F74qCgq2riRbEilE7Fhkl%2Fimage.png?alt=media&amp;token=4d7f4850-9d82-49fa-919c-1072dd05aff6" alt="" width="188"><figcaption><p>Светодиодная RGB матрица 4×4 (Troyka-модуль)</p></figcaption></figure>
2. Создаём новый файл в Arduino IDE с кодом:

   <pre class="language-cpp" data-line-numbers><code class="lang-cpp">#include &#x3C;RMaker.h>
   #include &#x3C;WiFi.h>
   #include &#x3C;WiFiProv.h>
   #include &#x3C;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, &#x26;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(NETWORK_PROV_SCHEME_BLE, NETWORK_PROV_SCHEME_HANDLER_FREE_BTDM, NETWORK_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);
   }
   </code></pre>

   <div data-gb-custom-block data-tag="hint" data-style="info" class="hint hint-info"><p>Изучите код, прочитайте комментарии к коду!</p></div>
3. Добавьте библиотеку [**`Adafruit_NeoPixel`**](https://github.com/adafruit/Adafruit_NeoPixel). Вы можете это сделать через пункт `"Управление библиотками"` или скачать из интернета и добавить как zip библиотеку.
4. В инструментах вы должны выбрать:

   * Плата: ESP32 Arduino > ESP32 Dev Module;
   * Partition Scheme: RainMaker.

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

   <div data-gb-custom-block data-tag="hint" data-style="info" class="hint hint-info"><p>На этом этапе могут возникнуть две известные проблемы...</p><ul><li>Плата пишет что-то на английском, но не выдаёт ссылку, тогда нужно сбросить настройки WI-FI, зажав <code>BOOT</code> на 3 секунды и отпустив.<br>* Если плата уже была до этого настроена, тогда она будет выводить после прошивки текст на английском, QR-код уже не выведется. Это нормально, но пользоваться дальше можно!</li><li>Плата пишет всякие кракозябры, хотя скорость последовательного порта выставлена верно. Вам нужно сбросить настройки прошивки. Для этого зажмите кнопку <code>BOOT</code> на плате на 10 секунд. По истечению времени отпустите, плата перезапустит код и в монитор порта текст станет читабельный и выведется ссылка.</li></ul></div>
6. Откройте приложение `RainMaker`. В приложении RainMaker нажать на кнопку с иконкой `ПЛЮС`, запуститься сканер QR-кода.
7. Отсканировать сканером QR-код с страницы с браузера.
8. В приложении `RainMaker` выбрать точку доступа для устройства, дать пароль и подождать подключения и всяких других настроек.
9. На главной странице `RainMaker` появится или обновится ваш девайс.

{% hint style="warning" %}
**Проблема с настрокой точки доступа:**

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

{% hint style="success" %}
**Дополнительное задание:**

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

   <pre class="language-cpp" data-overflow="wrap"><code class="lang-cpp">my_device.updateAndReportParam(PARAM_NAME, value); // Изменяем состояние тумблера в RainMaker
   </code></pre>
2. Установить для девайса тип "Lightbulb" с картинкой, на которой изображена лампочка.\
   \* Подсказка, [UI типы вы найдёте тут](/it-engineering/i.-internet-veshei/case-1b/iot-cloud-esp-rainmaker.md#standartnye-tipy).
3. Необходимо для слайдера **`Hue`** поменять UI тип **`Hue Slider`** на **`Hue Circle`**.
4. Для параметра насыщенности и яркости поменять тип параметра на более подходящий - **`Saturarion Slider`** и **`Brightness Slider`**.
5. Сделать ещё вариант заливки матрицы с UI типом **Dropdown** (выпадающий список).\
   Пример использования **`Dropdown`** есть в стандартном примере **`RMakerCustomAirCooler`**.\
   Первый режим (допустим с названием `Fill`) дожен будет у вас закрашивать все светодиоды моментально как  это было изначально, а второй (с именем `ColorWipe`) с помощью функции **`ColorWipe`** будет закрашивать матрицу постепенно.\
   Посмотрите какие функции доступны [в библиотеке для управление матрицой](https://learn.adafruit.com/adafruit-neopixel-uberguide/arduino-library-use).

   <pre class="language-cpp" data-overflow="wrap" data-line-numbers><code class="lang-cpp">// Переделанная функция заполнения каждого сегмента из примера Амперки
   void ColorWipe(uint32_t color, uint8_t wait)
   {
     for (uint16_t i = 0; i &#x3C; ledMatrix.numPixels(); i++) {
       // Заполняем текущий сегмент выбранным цветом
       //ledMatrix.setPixelColor(i, color); // Этот метод также работает
       ledMatrix.fill(color, i, 1);
       ledMatrix.show();
       delay(wait); // Ждём
     }
   }
   </code></pre>

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

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

      <pre class="language-cpp" data-overflow="wrap" data-line-numbers><code class="lang-cpp">static const char* fill_modes[] = { "Fill", "ColorWipe" }; // Массив со значениями режимов
      </code></pre>
   2. Добавьте define со значением режима по умолчанию, например равную 1:

      <pre class="language-cpp" data-overflow="wrap" data-line-numbers><code class="lang-cpp">#define DEFAULT_FILL_MODE 1 // Значение режима заливки по умолчанию
      </code></pre>

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

      <pre class="language-cpp" data-overflow="wrap" data-line-numbers><code class="lang-cpp">int fill_mode = DEFAULT_FILL_MODE; // Переменая хрененения режим заливки
      </code></pre>

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

      <pre class="language-cpp" data-overflow="wrap" data-line-numbers><code class="lang-cpp">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);
      </code></pre>

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

      <pre class="language-cpp" data-overflow="wrap" data-line-numbers><code class="lang-cpp"><strong>  ...
      </strong><strong>} else if (strcmp(param_name, "Mode") == 0) {
      </strong>  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);
      }
      </code></pre>

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