Началось все как всегда с малого. Я просто купил самую простую ленту за 300р. на 12В и подключил ее блок питания через умную розетку. Розетка работает как и положено, в дом с Алисой интегрирована, все работает все отлично, но меня не устраивал факт того что есть Блок питания и целый отдельный блок в виде умной розетки. Помимо этого нет возможности управлять яркостью.
Материалы
Итак сегодня мы будем это менять! Начнем с бюджета.
- Светодиодная лента я взял 5м плотностью 60 диодов на метр, однако можно взять и другой длины и плотности
- Линейный преобразователь постоянного тока есть много разных конфигураций, этот меня привлек своим размером.
- Блок питания тут не забывайте учесть сколько ватт потребляет ваша лента (можно вычитать в описании)
- Транзистор тут сразу комплект из 5-и шт.
- Набор резисторов Большой набор, на большое количество номиналов. Нам надо лишь два.
- Микроконтроллер NodeMCU на базе ESP8266 можно взять такой, или для большей компактности такой та-же ESP8266 но на более компактной плате.
Итак суммарно наш бюджет составил 1168р. на момент написания этого материала. Однако всегда можно разобрать какой-нибудь старый прибор, если у вас таковой имеется и вам не придется заказывать, как минимум, партию резисторов из-за двух штук. Можно еще порассуждать про всякие термоусадки для упаковки всей конструкции, но этот момент я оставлю на ваше усмотрение, тут как говорится кто во что горазд.
Сборка
Прейдем к части сборки. Из приборов нам потребуется паяльник и мультиметр. Для начала ознакомимся со схемой:
Начать надо с начала, как бы странно это не прозвучало. Берем провод с вилкой и подключаем его ко входу нашего блока питания. Выход +12В с этого блока припаиваем к соответствующим контактам нашего линейного преобразователя. Далее нам надо отрегулировать наш преобразователь. Берем маленькую, плоскую отвертку, подключаем щупы мультиметра на соответствующие контакты выхода преобразователя, включаем режим измерения постоянного тока, и крутим подстроечный резистор пока не накрутим около 3.3В. Если все получилось, двигаемся дальше. На микроконтроллере есть несколько пинов (ног) на +3.3В и GND (он-же земля, он-же минус) подключайтесь к тем, которые вам удобнее (все операции с подключением и пайкой делаем только после того как отключите блок питания из розетки. Для управления транзистором я выбрал ногу D5, но вам совершенно ничего не мешает выбрать любую другую ногу с поддержкой ШИМ (ВАЖНО! Именно с помощью ШИМ мы и будем управлять яркостью света).
Выше приведено схематичное изображение NodeMCU, тут вы можете наблюдать какие из пинов поддерживают ШИМ (знак ~ возле пина).
Когда спаяем все в кучу, очень удобно вывести контакты для ленты в какие-либо быстрозажимные клеммы, вариантов масса, под винт, с зажимом, тут опять все на вашей фантазии.
Программирование
Следующий важный этап — программирование. Для начала убедитесь что ваша Arduino IDE умеет работать с микроконтроллерами ESP
Нам понадобятся две библиотеки ESP8266WiFi.h
и PubSubClient.h
Первая как не трудно догадаться позволяет нашей ESP8266 работать с Wi-Fi и должна уже присутствовать у вас после установки поддержки плат ESP, а вторая, это клиент MQTT брокера. (Что такое MQTT брокер и почему нужен именно он, я расскажу в отдельной статье)
Ищем указанные библиотеки в менеджере библиотек нашей IDE:
Без лишних слов вот весь код управления нашим устройством:
#include <EEPROM.h> #include <ESP8266WiFi.h> #include "PubSubClient.h" #define LED D5 #define BUTTON_PIN D2 // Переменные WI-FI const char* ssid = "ИМЯ ВАШЕЙ СЕТИ"; const char* password = "ПАРОЛЬ ВАШЕЙ СЕТИ"; WiFiClient espClient; // Переменные MQTT const char* mqtt_server = "АДРЕС СЕРВЕРА MQTT"; const int mqtt_port = ПОРТ СЕРВЕРА MQTT; const char* powerTopic = "LD03032024/power"; const char* levelTopic = "LD03032024/level"; const char* powerStatusTopic = "LD03032024/powerStatus"; const char* levelStatusTopic = "LD03032024/levelStatus"; PubSubClient client(espClient); // Переменные для работы с кнопкой const int SHORT_PRESS_TIME = 500; const int LONG_PRESS_TIME = 1000; bool pressedOnce = false; int lastBtnState = LOW; unsigned long pressedTime = 0; unsigned long oldPressedTime = 0; unsigned long releasedTime = 0; unsigned long oldReleasedTime = 0; long pressDuration; // Переменные общего назначения bool ledOn = false; // Включен ли диод uint8_t ledPower = 255; // Переменная хранит данные о яркости в диапазоне от 0 до 255 int inputLedPower; // Переменная хранит данные о яркости в процентах (относительное число, поступившее от брокера) uint32_t tmr; // хранит состояние таймера millis() для подсчета прошедшего времени bool needUpdateStatus = false; // требуется обновление статуса MQTT (статусы будут опубликованы в loop()) void setup(){ Serial.begin(115200); pinMode(LED, OUTPUT); pinMode(D1,INPUT_PULLUP); pinMode(BUTTON_PIN, INPUT_PULLUP); analogWriteFreq(21000); digitalWrite(LED, 1); ledOn = true; initWiFi(); delay(1000); Serial.print("RSSI: "); Serial.println(WiFi.RSSI()); clientConnect(); delay(1000); client.publish(powerStatusTopic, "1"); client.publish(levelStatusTopic, "100"); } void clientConnect(){ if (client.connected()) { return; } while (!client.connected()) { client.setServer(mqtt_server, mqtt_port); client.setCallback(MQTTcallback); Serial.println("Connecting to MQTT..."); if (client.connect("LD23032024", "ПОЛЬЗОВАТЕЛЬ MQTT", "ПАРОЛЬ MQTT")) { Serial.println("connected"); client.subscribe(levelTopic); client.subscribe(powerTopic); } else { Serial.print("failed with state "); Serial.println(client.state()); delay(2000); } } } void initWiFi() { WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); Serial.print("Connecting to WiFi"); while (WiFi.status() != WL_CONNECTED) { Serial.print('.'); delay(1000); } Serial.println(WiFi.localIP()); // Автоматическое переподключение при потере соединения WiFi.setAutoReconnect(true); WiFi.persistent(true); } void buttonHandler(){ int currentState = digitalRead(BUTTON_PIN); if(lastBtnState == HIGH && currentState == LOW){ // Кнопка нажата pressedTime = millis(); oldPressedTime = millis(); } if (lastBtnState == LOW && currentState == LOW){ // Кнопка удерживается if (millis() - oldPressedTime > 1500){ ledPower -= 5; delay(50); analogWrite(LED, ledPower); needUpdateStatus = true; Serial.print("ledPower: "); Serial.println(ledPower); } } else if(lastBtnState == LOW && currentState == HIGH) { // Кнопка отпущена releasedTime = millis(); if (pressedOnce){ // Это двойное нажатие digitalWrite(LED, HIGH); ledPower = 255; pressedOnce = false; Serial.println("Is double click"); }else{ pressedOnce = true; } pressDuration = releasedTime - pressedTime; oldReleasedTime = millis(); oldPressedTime = millis(); needUpdateStatus = true; } if (pressedOnce && millis() - oldReleasedTime > 500){ if(pressDuration < SHORT_PRESS_TIME){ ledOn = !ledOn; ledPower = 255; digitalWrite(LED, ledOn); Serial.println("Short"); }else if(pressDuration >= LONG_PRESS_TIME && pressDuration <1400){ //Serial.println("Long"); } pressedOnce = false; needUpdateStatus = true; Serial.print("ledPower: "); Serial.println(ledPower); Serial.print("ledOn: "); Serial.println(ledOn); } lastBtnState = currentState; } void MQTTcallback(char* topic, byte* payload, unsigned int length) { char payload_string[length + 1]; Serial.print("Message arrived in topic: "); Serial.println(topic); memcpy(payload_string, payload, length); payload_string[length] = '\0'; inputLedPower = atoi(payload_string); if (strcmp(topic, powerTopic) == 0){ ledPower = 255 * inputLedPower; inputLedPower = ledPower; ledOn = inputLedPower; }else if (strcmp(topic, levelTopic) == 0){ ledPower = (255 / 100) * inputLedPower; ledOn = true; } analogWrite(LED, ledPower); needUpdateStatus = true; Serial.print("Message:"); Serial.println(inputLedPower); Serial.print("length:"); Serial.println(length); Serial.println(); Serial.println("-----------------------"); Serial.print("ledPower: "); Serial.println(ledPower); Serial.print("ledOn: "); Serial.println(ledOn); Serial.println("-----------------------"); } void loop() { if (tmr + 60000 < millis() ){ clientConnect(); Serial.println("Client reconnect"); tmr = millis(); } if (needUpdateStatus){ if (ledOn) { client.publish(powerStatusTopic, "1"); }else{ client.publish(powerStatusTopic, "0"); } char ldPower[3]; utoa(ledPower * 100 / 255, ldPower, 10); client.publish(levelStatusTopic, ldPower); Serial.println("Client status updated"); Serial.print("ledPower: "); Serial.println(ldPower); Serial.print("ledOn: "); Serial.println(ledOn); needUpdateStatus = false; } client.loop(); buttonHandler(); }
Вот собственно и весь код, теперь все что вам надо, это вставить этот код в ваш проект, подключить плату ESP8266 и загрузить код на плату. Дожидаемся окончания загрузки. Все, наше устройство готово, При включении питания, на D5
устанавливается 1
или HIGH
(можно просто сказать что это логическая единица, или +3,3В) В любом из случаев это откроет наш транзистор, и светодиодная лента включится.
Думаю надо немного рассказать по принцип работы с MQTT. Одним из важных понятий тут является Топик это грубо выражаясь Канал или Контекст, или еще какое-либо похожее слово (более всего по смыслу подходит Топик). Наше устройство постоянно опрашивает MQTT брокер на предмет Нет ли чего нового для меня в этом конкретном топике
и если что-то новое есть, устройство послушно выполняет. По сути название топика это строка, но в большинстве случаев есть некоторые рекомендации по формированию такой строки к примеру можно использовать такой шаблон Дом/Комната/Устройство/ВидКоманд
в коде я указал четыре топика:
const char* powerTopic = "LD03032024/power"; const char* levelTopic = "LD03032024/level"; const char* powerStatusTopic = "LD03032024/powerStatus"; const char* levelStatusTopic = "LD03032024/levelStatus";
Названия топиков не особо соответствует шаблону, но это исключительно для теста. Вам же я рекомендую назвать топики более осознано. Топик
это топик включения и выключения, топик power
level
для установки уровня яркости, топики powerStatus
и levelStatus
работает в обратную сторону, т.е. наше устройство не принимает а наоборот публикует сообщения в данных топиках, сообщая нашему брокеру о своем состоянии.
Итак, как теперь стало очевидно, нам потребуется некий MQTT брокер. Конечно иметь свой сервер будет весьма удобно, и я опишу как сделать себе такой дома, но для его работы с Алисой нам потребуется реализовать целую кучу отдельных технологий, что в принципе потянет на отдельную ветку статей. Сегодня мы ограничимся каким-либо публичным сервисом MQTT, допустим wqtt.ru
После регистрации заходим в контрольную панель, переходим в настройки, и добавляем новое устройство:
Укажем имя и расположение устройства, а на следующем этапе перейдем к настройка органов управления:
Тип органа управления сначала укажем выключатель.
Топик управления это тот что у нас в коде отвечает за включение (по коду подает HIGH
на пин D5
)
void MQTTcallback(char* topic, byte* payload, unsigned int length) { ... if (strcmp(topic, powerTopic) == 0){ ledPower = 256 * inputLedPower; ...
вот область кода отвечающая за обработку команд этого топика.
Добавим еще один орган управление с типом Регулятор:
Тут в принципе все аналогично предыдущему этапу, за исключением того что данный орган управления передает в топик не 1
и 0
а диапазон значений от 1
до 100
, тут кстати наглядно можно рассказать о еще одном участке кода:
}else if (strcmp(topic, levelTopic) == 0){ ledPower = (255 / 100) * inputLedPower; ... }
С точки зрения человека, нам понятнее управление яркостью в диапазоне от 1 до 100… допустим это процент яркости. Но у плат ESP разрешение ШИМ несколько побольше…
(по описанию все источники утверждают о диапазоне от 0 до 1023, однако на моей плате при таком значении ШИМ работал неправильно, полностью игнорируя почти весь диапазон, а верно он стал работать только при диапазоне от 0 до 255. Очевидно китайцы что-то неверно нахимичили со значениями по умолчанию, думаю analogWriteRange(range)
заботливо прописанная в методе setup()
без труда решит этот вопрос, но что-то мне подсказывает что даже 256 значений будет вполне достаточно)
Так вот, получается нам нужно наши удобные проценты, превратить в диапазон от 0 до 255, в приведенном коде мы путем расчета соотношения именно это и делаем, главное не забыть что опубликовать данные в топике состояния надо в привычных пользователь (нам) значениях.
Есть еще один нюанс, дело в том что мой транзистор при включении через топик LD03032024/level
издавал весьма неприятный писк. И дело тут вот в чем. По умолчанию на ШИМ частота прямоугольной волны составляет 1 кГц. Это значение входит в слышимый диапазон человека, собственно потому мы его и слышим. Дабы избежать этого неприятного явления в методе setup()
укажите частоту работы ШИМ вне пределов слышимого диапазона:
void setup(){ ... analogWriteFreq(21000); digitalWrite(LED, 1); ...
Я указал 21 кГц. Ну и финальный этап — обновление устройств Яндекс:
Ну вот и все, все остальные ваши забавы с устройством и Алисой я оставлю на вашей совести, но как очевидно уже понятно, голосовые команды и сценарии назначенные в умном доме от Яндекс будут работать как и на заводских устройствах.