Управляем светодиодной лентой с помощью Алисы

Началось все как всегда с малого. Я просто купил самую простую ленту за 300р. на 12В и подключил ее блок питания через умную розетку. Розетка работает как и положено, в дом с Алисой интегрирована, все работает все отлично, но меня не устраивал факт того что есть Блок питания и целый отдельный блок в виде умной розетки. Помимо этого нет возможности управлять яркостью.

Материалы

Итак сегодня мы будем это менять! Начнем с бюджета.

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

Сборка

Прейдем к части сборки. Из приборов нам потребуется паяльник и мультиметр. Для начала ознакомимся со схемой:

Начать надо с начала, как бы странно это не прозвучало. Берем провод с вилкой и подключаем его ко входу нашего блока питания. Выход +12В с этого блока припаиваем к соответствующим контактам нашего линейного преобразователя. Далее нам надо отрегулировать наш преобразователь. Берем маленькую, плоскую отвертку, подключаем щупы мультиметра на соответствующие контакты выхода преобразователя, включаем режим измерения постоянного тока, и крутим подстроечный резистор пока не накрутим около 3.3В. Если все получилось, двигаемся дальше. На микроконтроллере есть несколько пинов (ног) на +3.3В и GND (он-же земля, он-же минус) подключайтесь к тем, которые вам удобнее (все операции с подключением и пайкой делаем только после того как отключите блок питания из розетки. Для управления транзистором я выбрал ногу D5, но вам совершенно ничего не мешает выбрать любую другую ногу с поддержкой ШИМ (ВАЖНО! Именно с помощью ШИМ мы и будем управлять яркостью света).

Выше приведено схематичное изображение NodeMCU, тут вы можете наблюдать какие из пинов поддерживают ШИМ (знак ~ возле пина).

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

Программирование

Следующий важный этап — программирование. Для начала убедитесь что ваша Arduino IDE умеет работать с микроконтроллерами ESP

Нам понадобятся две библиотеки ESP8266WiFi.h и PubSubClient.h Первая как не трудно догадаться позволяет нашей ESP8266 работать с Wi-Fi и должна уже присутствовать у вас после установки поддержки плат ESP, а вторая, это клиент MQTT брокера. (Что такое MQTT брокер и почему нужен именно он, я расскажу в отдельной статье)

Ищем указанные библиотеки в менеджере библиотек нашей IDE:

ESP MQTT Client

Без лишних слов вот весь код управления нашим устройством:

#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 кГц. Ну и финальный этап — обновление устройств Яндекс:

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

5 1 голос
Рейтинг статьи
Подписаться
Уведомить о
0 комментариев
Межтекстовые Отзывы
Посмотреть все комментарии