Очередной умный дом (или как потерять 2 месяца из-за одной глупой ошибки)

Предыстория

я, не знающий своей глупости и наивностия, не знающий своей глупости и наивности

Начну я свою эпопею с небольшой предыстории. Надо было мне сделать проект для 9 класса (да, сейчас, чтобы тебя допустили до экзаменов, надо сделать проект и защитить его).

Решил практически сразу, что это будет умный дом (хотя даже воробей умнее его), ведь давно хотел сделать что-то такое, но руки так и не доходили, а тут подвернулся отличный случай + немного выпендриться можно.

Данный пост — мой переделанный диплом, да и вообще первая попытка в написании таких вещей)

План

Для успешной и продуктивной работы нужен план, для себя я составил его примерно таким:

  1. Идея и идеология умного дома.

  2. Железо для построения умного дома.

  3. Код для каждой из частей умного дома.

Принципе, для начала это более чем достаточно. Теперь можно разобрать каждый пункт отдельно.

Идея и идеология умного дома

Посидев несколько дней и обдумав все идеи, в моей голове всплыла утопическая идея такой системы, которая не нуждалась бы в базовом модуле, а каждый такой модуль будет полностью автономен. У системы такого типа, в отличии от той, что использует в своей основе базовый модуль (примером может быть умный дом xiaomi), имеется несколько плюсов и минусов.
Начнем с плюсов. Главный плюс — отказоустойчивость, даже если один из управляющих модулей выйдет из строя, датчики все равно будут отправлять данные пользователю. Второй же плюс — возможность связи модулей на больших расстояниях.

А теперь про минусы: самый значительный минус — автономности, ведь на то, чтобы проснуться, подключиться к интернету, отправить данные и уснуть тратиться больше энергии, чем на то, чтобы передать данные по радиоканалу. Следующий минус состоит в необходимости наличия интернета для работы каждого датчика, иначе он просто не сможет отправить данные.
В итоге я составил список функционала, который умный дом должен реализовывать в ИДЕАЛЕ:

  1. Автономные модули.

  2. Взаимозаменяемость модулей.

  3. Масштабируемость системы.

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

И да, забыл сказать, модуль — единичное устройство системы умного дома, будь то wi-fi розетка или датчик температуры.

Для приемо-передачи данных я выбрал MQTT, довольно известный и удобный протокол передачи.

Микроконтроллер

Микроконтроллером, который является основой для каждого модуля умного дома, я выбрал всем хорошо знакомый, даже немного заезженный ESP-12F. Он хорош своими возможностями, в которые входит функция WI-FI (как соединение, так и точка доступа) и отличная производительность, которой с лихвой хватило на все мои задумки, а цена за штуку не превышает 100 рублей в Китае, если брать сразу 10 штук.

распиновка и внутреннее устройство esp12Fраспиновка и внутреннее устройство esp12F

Вообще есть несколько версий esp12, самая первая — esp12Е, она по всем фронтам уступает более новому esp12F. Основные их отличия в форме антенны, у F-версии она сделана более удачно, и в размере Flash памяти: у E-версии в большинстве случаев она составляет 1МБ, в то время как у F-версии она уже 4МБ. Также, вроде как различия в внутренней компоновки компонентов, которая удачнее в F-версии. Еще можно вспомнить самую новую версию — esp12S, которая практически идентична (если я правильно разобрался) своей начинкой, но сделана в более компактном и удобном корпусе для smd пайки.

три версии esp12 рядомтри версии esp12 рядом

Устройство WI-FI розетки

Теперь можно рассмотреть каждый модуль, всего их будет 3.

Что из себя вообще представляет умная (wifi) розетка — устройство, включаемый в обычную розетку и которое может управлять (как автоматически, так и в ручном режиме) нагрузкой, включенную в это устройство.

Есть два основных способа реализовать это:

1 способ — использовать реле, вместе с транзисторным ключом (реле 5В, а микроконтроллер питается от 3.3В).
2 способ — использовать симистор.

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

симистор и релесимистор и реле

Я решил все же использовать реле, ведь оно было уже куплено и схема была отработана, а с симистор пришлось бы разбираться.

принципиальная схемапринципиальная схема

Как видно на схеме, для правильной работы реле, ему нужен транзисторный ключ, ведь esp12F питается от 3.3В, а реле на 5В, да и такую нагрузку лучше не включать прямо в пин микроконтроллера, может сгореть.

2a9940d51d6c7519b4d6ac34c2b66b03.png

Как AC/DC преобразователь может использоваться любой блок питания без корпуса, я использовал готовый для таких целей блок питания с алика, с 220В переменного на 5В постоянного и 1А на выходе, что даже избыточно, ведь вся схема в работе потребляет не более 0,3–0,2А. Для транзисторного ключа идеально подошел кт315, который уже хорошо известен многим радиолюбителям.

50f81547d8f12fe52c0c1beef4e1a8e8.jpeg

Как было сказано раньше, esp12F питается от 3.3В, а блок питания выдает все 5В, для того, чтобы МК не сгорел, для его питания, сразу после блока питания стоит понижающий линейный преобразователь AMS1117.

печатная плата для wi-fi розеткипечатная плата для wi-fi розетки

Для корпуса были куплены обычная розетка и вилка, которые впоследствии были уничтожены (разобраны на части), а основной корпус был напечатан на 3д принтере.

корпус в реальности и корпус в fusion360корпус в реальности и корпус в fusion360внутреннее строение модулявнутреннее строение модуля

Печатная плата сделана ЛУТом, по рецепту Гайвера, только под утюгом лучше держать около 10 минут, а то краска некорректно перейдет на стеклотекстолит.

плата с стороны компонентовплата с стороны компонентовнеудачные платынеудачные платы

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

Устройство модулей датчиков

Оба модулей датчиков имеют практически идентичное внутреннее устройство, различия только в самих датчиках, в одном это DHT11, а в другом DS18B20 и их обвязках.

Модули выполняют очень простую функцию: отправлять показания с датчиков по MQTTT.

принципиальная схема модуля для датчика DS18B20(для DHT11 отличие будет только в резисторе, который будет не 4.7К,а 10К)принципиальная схема модуля для датчика DS18B20(для DHT11 отличие будет только в резисторе, который будет не 4.7К, а 10К)печатная плата для модуля датчика(зеленое на плате - проводки на другой стороне)печатная плата для модуля датчика (зеленое на плате — проводки на другой стороне)

В этот раз корпус был полностью напечатан на 3д принтере, ведь даже так, себестоимость корпусов дешевле, чем покупать уже готовые коробочки, которые придется дорабатывать!

корпус в реале и корпус в fusion 360корпус в реале и корпус в fusion 360корпус без крышкикорпус без крышки

Питание модуля идет от 3 батареек типа АА.

Прошивка модулей

Наверное самый интересный и самый долгий пункт…

Сначала я хотел использовать RTOS-SDK от ESP, но понял что уйдет много времени чтобы разобраться в нем, поэтому я остановился на Arduino, хоть и от FreeRTOS пришлось отказаться.

Но, в принципе, для моего решения RTOS не нужна.

Для прошивки можно использовать любой программатор, совместимый с esp, но главное не перепутать перемычки (если они есть конечно) логического уровня и не поставить их на 5в, иначе esp12 может попросту сгореть.

ac30bfaad42a73f9104cba0c80b3f552.jpeg

Подключал я по этой схеме… и вот как раз таки та загвоздка, из-за которой я и потратил 2 месяца в пустую…забыл минус микроконтроллера к минусу программатора соединить! Оказывается, не дурак эту схему начертил, а дурак её пытался повторить). Пока искал где проблема, переделал много плат, заказал новые микроконтроллеры, подумал что с ними проблема, новый программатор взял, перерыл форумы с этой проблемой…

Ну да ладно, главное что все заработало.

MQTT

Как я и говорил раньше, решил использовать MQTT для своих целей. Сам протокол состоит из нескольких сущностей:

  • Брокер — сервер, который управляет передачей данных, создает топики.

  • Топик — канал передачи данных, на который может подписываться устройство «подписчик» и в который может публиковать данные устройство «издатель».

  • Подписчик — устройство, подписывающееся на топик и получаемое из этого топика все данные, публикующиеся в него.

  • Издатель — устройство, публикующее в топик данные .

Примерная схема передачи данных по протоколу MQTT. Разные цвета - разные топики.Примерная схема передачи данных по протоколу MQTT. Разные цвета — разные топики.

Одно и тоже устройство может быть как подписчиком, так и издателем одновременно, причем в разных топиках.

Изначально была идея использовать брокер от yandex iot, но он оказался менее удобным для моего проекта, хотя можно сразу прикрепить сервер обработки и хранения данных, но это стоило бы довольно дорого, поэтому выбрал wqtt, который стоит всего 300р в год и можно добавить поддержку Яндекс Алисы).

Настройка модулей

Вообще, у каждого модуля есть два основных режима работы: режим настройки модуля и режим работы. В режим настройки входят настройка wifi в которому модуль подключается и настройка ip модуля, для обращения к нему через браузер.

Давайте разберем мою реализацию режима настройки.

Для начала, нам нужно подключить нужные библиотеки:

#include 
#include 
#include 
#include 
#include 

Следом идут определение константан для точки доступа самого модуля, wifi к которому она подключается и к MQTT:

#define RELEPIN 4 // пин МК с реле

const char *ssid_ap = "Rozetka_Setup";  //имя точки доступа модуля
const char *password_ap = "12345678";  //пароль точки доступа модуля
const char *ID = "rele_1";// типовое, статичное ID модуля

String ssid = "";  //имя wifi
String password = "";  //пароль wifi
String device_id; // пользовательское ID модуля
bool setup_mode; // true - первичная настройка модуля, false - основная работа модуля
bool rele;// true - реле включено, false - реле выключено

const char *mqtt_server = "M5.WQTT.RU"; // Имя сервера MQTT
const int mqtt_port = 2602; // Порт для подключения к серверу MQTT
const char *mqtt_user = "user"; // Логин от серверa
const char *mqtt_pass = "pass"; // Пароль от сервера

IPAddress local_ip(192, 168, 1, 1);//IP для точки доступа модуля
IPAddress gateway(192, 168, 1, 1);//гейт для точки доступа модуля
IPAddress subnet(255, 255, 255, 0);//маска для точки доступа модуля

IPAddress local_ip_2(192, 168, 0, 250);//IP для WIFI
IPAddress gateway_2(192, 168, 0, 1);//гейт для WIFI
IPAddress subnet_2(255, 255, 255, 0);//маска для WIFI

ESP8266WebServer server(80);// server для настройки

WiFiClient wclient;
PubSubClient client(wclient, mqtt_server, mqtt_port);

Setup блок программы:

pinMode(RELEPIN, OUTPUT);
Serial.begin(115200);
EEPROM.begin(256);//подключаем EEPROM
setup_mode = EEPROM.read(45);// читаем из EERPOM текущий режим
rele = EEPROM.read(60);// читаем из EERPOM текущее состояние реле
EEPROM.end();//отключаем EEPROM
if (rele) digitalWrite(RELEPIN, HIGH); else digitalWrite(RELEPIN, LOW);

delay(1000);
WiFi.softAP(ssid_ap, password_ap);//настраиваем точку доступа(название и пароль)
WiFi.softAPConfig(local_ip, gateway, subnet);//настраиваем точку доступа(ip для подключения)
delay(100);
server.on("/", handle_OnConnect);//handle для первой начальной страницы
server.on("/end_setup", handle_EndSetup);//handle для окончания настройки
server.on("/action_page", handleForm);//handle для выбора продолжить или закончить настройку
server.onNotFound(handle_NotFound);
server.begin();//запуск сервера
Serial.println("HTTP server started");

Почему я использую подключение по через браузер по типу «http://192.168.1.1/», я попросту не нашел альтернативы. Конечно, есть технология Mdns, которая позволяет обращаться через браузер просто по имени «http://esp8266/», но её не поддерживают android устройства ! Я не понимаю почему, но именно так, гугл не может добавить поддержку Mdns, хотя у эпл она давно уже есть…

Как работают hadle? Изначально мы переходим по адресу http://192.168.1.1/, а последняя »/» и является своеобразным «указателем» на определенный handle, а сам по себе любой handle является программой, который мы настраиваем. Например адрес «http://192.168.1.1/end_setup» указывает на handle, который завершает настройку модуля.

Рассмотрим сами handle:

данный Handle передает пользователю веб-страничку для настройки модуля-розетки, для модулей -датчиков данная страничка будет немного иная -

void handle_OnConnect() {// handle, который отправляет начальную страницу
  server.send(200, "text/html", SendHTML());
}

String SendHTML() {//отправка HTML страницы для настройки датчика- розетки
  String ptr = " \n";
  ptr += "\n";
  ptr += "WIFI Control\n";
  ptr += "\n";
  ptr += "\n";
  ptr += "\n";
  ptr += "

Настройка WIFI

\n"; ptr += "

Укажите название и пароль от нужной wifi сети

\n"; ptr += "
"; ptr += "Название:
"; ptr += ""; ptr += "
"; ptr += "Пароль:
"; ptr += ""; ptr += "
"; ptr += "Название модуля:
"; ptr += "
"; ptr += "Local_ip:
"; ptr += ""; ptr += "
"; ptr += "Gateway:
"; ptr += ""; ptr += "
"; ptr += "

"; ptr += ""; ptr += "
"; ptr += " \n"; ptr += "\n"; return ptr; } String SendHTML() {//Отправка HTML страницы для настройки модуля - датчика String ptr = " \n"; ptr += "\n"; ptr += "WIFI Control\n"; ptr += "\n"; ptr += "\n"; ptr += "\n"; ptr += "

Настройка WIFI

\n"; ptr += "

Укажите название и пароль от нужной wifi сети

\n"; ptr += "
"; ptr += "Название:
"; ptr += ""; ptr += "
"; ptr += "Пароль:
"; ptr += ""; ptr += "
"; ptr += "Тайминг отправки(в секундах):
"; ptr += ""; ptr += "
"; ptr += "Название модуля:
"; ptr += "
"; ptr += "

"; ptr += ""; ptr += "
"; ptr += "\n"; ptr += "\n"; return ptr; }
Меню для настройки: слева - для модуля-датчика, справа - для модуля-розеткиМеню для настройки: слева — для модуля-датчика, справа — для модуля-розетки

По нажатии кнопки «закончить» в действие вступает следующий handle -

void handleForm() {//версия для модуля - розетки
  ssid = server.arg("WIFI_NAME");
  password = server.arg("WIFI_password");
  device_id = server.arg("DEVICE_ID");
  local_ip_2.fromString(server.arg("LOCAL_IP"));
  gateway_2.fromString(server.arg("GATEWAY"));
  write_string_EEPROM(200, server.arg("LOCAL_IP"));
  write_string_EEPROM(220, server.arg("GATEWAY"));
  Serial.print("WIFI: ");
  Serial.println(ssid);

  Serial.print("password: ");
  Serial.println(password);

  server.send(200, "text/html", SendEndHTML()); //Send web page
}

void handleForm() {//версия для модуля - датчика
  ssid = server.arg("WIFI_NAME");
  password = server.arg("WIFI_password");
  device_id =  server.arg("DEVICE_ID");
  tmi = server.arg("TIMING").toInt();
  Serial.print("WIFI: ");
  Serial.println(ssid);

  Serial.print("password: ");
  Serial.println(password);

  server.send(200, "text/html", SendEndHTML()); //Send web page
}

String SendEndHTML() {
  String ptr = "\n";
  ptr += "\n";
  ptr += "WIFI Control\n";
  ptr += "\n";
  ptr += "\n";
  ptr += "\n";
  ptr += "

Вернуться к настройки

\n"; ptr += "

Продолжить

\n"; ptr += "\n"; ptr += "\n"; return ptr; }
форма выбораформа выбора

При нажатии на кнопку «Вернуться к настройки» пользователь вернется на первую страницу, а при нажатии на кнопку «продолжить» сработает следующий handle, который перезапишет в EEPROM ssid и пароль от WIFI, переведет модуль в режим нормальной работы -

void handle_EndSetup() {
  write_string_EEPROM(0, ssid);
  write_string_EEPROM(20, password);
  EEPROM.begin(256);
  EEPROM.write(45, false);
  setup_mode = false;
  EEPROM.commit();
  EEPROM.end();
  WiFi.softAPdisconnect(true);
  ESP.reset();
}

Немного про саму запись в EEPROM. Для записи и чтения строк в него используются две функции -

void write_string_EEPROM (int Addr, String Str) {//Запись строки в EEPROM
//Addr - начальный байт, str - строка. Строка не может быть больше 15 символов
  byte lng = Str.length();
  EEPROM.begin (256);
  EEPROM.write(Addr , lng);
  unsigned char* buf = new unsigned char[15];
  Str.getBytes(buf, lng + 1);
  Addr++;
  for (byte i = 0; i < lng; i++) {
    EEPROM.write(Addr + i, buf[i]);
    delay(10);
  }
  EEPROM.commit();
  EEPROM.end();
}

char read_string_EEPROM (int Addr) {// Чтение строки из EEPROM
//Addr - начальный байт
  EEPROM.begin(256);
  byte lng = EEPROM.read(Addr);
  char buf = new char[15];
  Addr++;
  for (byte i = 0; i < lng && i < 15; i++) buf[i] = char(EEPROM.read(i + Addr));
  buf[lng] = '\x0';
  EEPROM.end();
  return buf;
}

Вообще, хорошо бы вместо EEPROM использовать FS.h (файловую систему), но тогда до меня это не дошло.

Режим нормально работы (модуль — розетка)

Теперь, когда наши модули настроены, можно рассмотреть их основные функции.

Рассмотрим SETUP, который запускается при старте модуля в данном режиме:

pinMode(RELEPIN, OUTPUT);
Serial.begin(115200);
EEPROM.begin(256);//подключаем EEPROM
setup_mode = EEPROM.read(45);// читаем из EERPOM текущий режим
rele = EEPROM.read(60);// читаем из EERPOM текущее состояние реле
EEPROM.end();//отключаем EEPROM
if (rele) digitalWrite(RELEPIN, HIGH); else digitalWrite(RELEPIN, LOW);

local_ip_2.fromString(String(read_string_EEPROM(200)));//читаем IP из eeprom 
    gateway_2.fromString(String(read_string_EEPROM(220)));//читаем гейт из eeprom 
    WiFi.begin(read_string_EEPROM(0), read_string_EEPROM(20));
    int sm = 0;
    while (WiFi.status() != WL_CONNECTED) {
      delay(500); sm++;
      Serial.print(".");
      if (sm > 120) {
        handle_ReturnSetup();
      }
    }
    WiFi.config(local_ip_2, gateway_2, subnet_2);
    Serial.println("WiFi connected");
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());
    read_auto();
    server.on("/", handle_OnConnect_2);
    server.on("/select_auto", handle_SelectAuto);
    server.on("/rele_auto", handle_ReleAuto);
    server.on("/delete_page", handle_Delete);
    server.on("/return_setup", handle_ReturnSetup);
    server.on("/control_menu", handle_ControlMenu);
    server.on("/rele_off", handle_ReleOff);
    server.on("/rele_on", handle_ReleOn);
    server.on("/auto_off", handle_AutoOff);
    server.on("/auto_on", handle_AutoOn);
    server.onNotFound(handle_NotFound);
    server.begin();
    Serial.println("HTTP server started");

    if (client.connect(MQTT::Connect(ID).set_auth(mqtt_user, mqtt_pass))) {
      Serial.println("Connected to MQTT server");
      client.subscribe("/datk"); // подписываемся на топик с данными датчиков
      //client.subscribe("/cmd"); // подписываемся на топик с командами
      client.set_callback(callback);
    } else {
      Serial.println("Could not connect to MQTT server");
    }

Обработчик MQTT:


void perek(bool per) { //per = true = прямой режим, per = true = обратный режим
  EEPROM.begin(256);
  if (auto_mode) {
    if (per) {
      rele = 0;
      digitalWrite(RELEPIN, LOW);
    }
    else {
      rele = 1;
      digitalWrite(RELEPIN, HIGH);
    }
  }
  else {
    if (per) {
      rele = 1;
      digitalWrite(RELEPIN, HIGH);
    }
    else {
      rele = 0;
      digitalWrite(RELEPIN, LOW);
    }
  }
  EEPROM.write(60, rele);
  EEPROM.commit();
  EEPROM.end();
}

void callback(const MQTT::Publish& pub)
{
  Serial.print(pub.topic()); // выводим в сериал порт название топика
  Serial.print(" => ");
  Serial.print(pub.payload_string()); // выводим в сериал порт значение полученных данных
  String payload = pub.payload_string();
  if (String(pub.topic()) == "/datk") // проверяем из нужного ли нам топика пришли данные
  {
    String tmpstr = "", namest, znachs;
    int datat;
    int mod = 0;
    bool tr = false;
    for (int i = 0 ; i < payload.length(); i++) {
      if (payload[i] != '#') tmpstr += payload[i]; else {
        switch (mod) {
          case 0:
            namest = tmpstr;
            break;
          case 1:
            datat = tmpstr.toInt();
            break;
          case 2:
            znachs = tmpstr;
            break;
        }
        tmpstr = "";
        mod++;
      }
    }
    for (int i = 0; i < lng && !tr; i++) {
      if (names[i] == namest) {
        data[i] = datat;
        tr = true;
      }
    }
    if (!tr) {
      lng++;
      names[lng - 1] = namest;
      data[lng - 1] = datat;
      znach[lng - 1] = znachs;
    }
    if (auto_stat && namest == auto_name) {
      switch (auto_oper) {
        case 0:
          if (auto_data > datat) perek(true);
          if (auto_data < datat) perek(false);
          break;
        case 1:
          if (auto_data < datat) perek(true);
          if (auto_data > datat) perek(false);
          break;
        case 2:
          if (auto_data = datat) perek(true);
          if (auto_data != datat) perek(false);
          break;
      }
    }
  }

Функция «Perek» используется для правильного переключения реле.

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

Первый же handle:

void handle_OnConnect_2() {
  server.send(200, "text/html", SendMenuHTML());
}

String SendMenuHTML() {
  String ptr = " \n";
  ptr += "\n";
  ptr += "WIFI Control\n";
  ptr += "\n";
  ptr += "\n";
  ptr += "\n";
  ptr += "

WIFI меню

\n"; ptr += "

Выберете пункт меню:

\n"; ptr += "

"; ptr += "

"; ptr += "

"; ptr += "\n"; ptr += "\n"; return ptr; }

Само меню выглядит так:

cc6472837997d353ebe42c5683cb474b.png

Кнопка «Вернуться в режим настройки», как понятно из названия, отправляет модуль обратно в режим настройки.
Кнопка «Панель управления» открывает пользователю меню управления.
Кнопка «Панель управления» открывает пользователю меню автоматического управления розеткой.

Возвращение к меню управления происходит через handle:

void handle_ReturnSetup() {
  EEPROM.begin(256);
  EEPROM.write(45, true);
  setup_mode = true;
  EEPROM.commit();
  EEPROM.end();
  ESP.reset();
}

Для перехода к меню управления используется handle (Больше handle богу handle!):

void handle_ControlMenu() {
  server.send(200, "text/html", SendControlMenuHTML());
}

String SendControlMenuHTML() {
  String ptr = " \n";
  ptr += "\n";
  ptr += "WIFI Control\n";
  ptr += "\n";
  ptr += "\n";
  ptr += "\n";
  ptr += "

Панель управления

\n"; ptr += "

"; if (rele) { ptr += "

"; } if (!rele) { ptr += "

"; } ptr += "

Датчики:

\n"; //ptr += "

Датчики:

\n"; for (int i = 0 ; i < lng; i++) { ptr += "

" + names[i] + " : " + data[i] + " " + znach[i] + "

"; } ptr += "
"; ptr += "Название: "; ptr += " "; //ptr += "

"; ptr += ""; ptr += "
"; ptr += "

"; ptr += "\n"; ptr += "\n"; return ptr; }
Панель управленияПанель управления

Рассмотрим элементы управления сверху-вниз. В самом верху находится кнопка возвращения в первоначальное меню, следом идет управления реле/розеткой, меняющее свое значение в зависимости от состояние реле, и в зависимости от него используются один из этих handle:

void handle_ReleOff() {//выключание реле
  rele = 0;
  digitalWrite(RELEPIN, LOW);
  EEPROM.begin(256);
  EEPROM.write(60, 0);
  EEPROM.commit();
  EEPROM.end();
  server.send(200, "text/html", SendControlMenuHTML());
}

void handle_ReleOn() {//включение реле
  rele = 1;
  digitalWrite(RELEPIN, HIGH  );
  EEPROM.begin(256);
  EEPROM.write(60, 1);
  EEPROM.commit();
  EEPROM.end();
  server.send(200, "text/html", SendControlMenuHTML());
}

Далее список датчиков и их показаний, которые можно удалить в следующей форме. Список датчиков обновляется по мере поступления данных, обновление страницы идет с помощью кнопки «обновить».

Удаление датчиков идет с помощью handle:

void handle_Delete() {
  String delname = server.arg("DELETE_NAME");
  bool tr = false;
  for (int i = 0 ; i < lng; i++) {
    if (tr) {
      names[i - 1] = names[i];
      data[i - 1] = data[i];
      znach[i - 1] = znach[i];
    }
    else if (names[i] == delname) {
      tr = true;
    }
  }
  if (tr) lng--;
  server.send(200, "text/html", SendControlMenuHTML());
}

Программа ищет имя данного датчика и удаляет его, перемещая список на 1 пункт ниже, после найденного элемента.

Поднимемся на уровень выше и перейдем к настройки автоматического управления розеткой.
Сам handle, отвечающий за отправку данной веб-страницы пользователю выглядит так:

void handle_ReleAuto() {
  server.send(200, "text/html", SendControlReleAutoHTML());
}

String SendControlReleAutoHTML() {
  String ptr = " \n";
  ptr += "\n";
  ptr += "WIFI Control\n";
  ptr += "\n";
  ptr += "\n";
  ptr += "\n";
  ptr += "

Настройка автоматического управления розетки

"; if (auto_stat) { ptr += "

Автоматическое управления включено

"; } else { ptr += "

Автоматическое управления выключено

"; } ptr += "

Текущее условие:" + auto_name + " "; switch (auto_oper) { case 0 : ptr += "> "; break; case 1 : ptr += "< "; break; case 2 : ptr += "= "; break; } if (auto_mode) ptr += String(auto_data) + " выключать

"; else ptr += String(auto_data) + " включать "; ptr += "

Выберете датчик и параметр для него:

\n"; ptr += "
"; ptr += " "; ptr += " "; ptr += " "; ptr += ""; ptr += " "; //ptr += "

"; ptr += ""; ptr += "
"; ptr += "

"; if (auto_stat) { ptr += "

"; } else { ptr += "

"; } ptr += "

"; ptr += "\n"; ptr += "\n"; return ptr; }

В начало программы добавляются константы:

String auto_name = "example";
int auto_data = 10;
int auto_oper = 0;
bool auto_stat = false;// false - не работает, true - работает
bool auto_mode = false; //false - включать, true - выключать

String names[10];// массив имен модулей
String znach[10];// массив едениц измерения модулей
int data[10];// массив данных модулей
int lng = 0;//используемая длина
Меню автоматического управленияМеню автоматического управления

Рассмотрим также сверху-вниз, пропуская название сверху. Сначала нам дается информация о том, включено или нет авто управление. Ниже показывается текущее условие. Чтобы перенастроить модуль надо выбрать в форме ниже модуль, условие, значение и действия, а по окончанию нажать кнопку «настроить». Кнопка «обновить» служит для обновления списка датчиков.

Настройка происходит также через handle:

void handle_SelectAuto() {
  auto_name = names[server.arg("DAT_NAME").toInt()];
  auto_oper = server.arg("OPER").toInt();
  auto_data = server.arg("DATA_P").toInt();
  auto_mode = server.arg("ON_OFF").toInt();
  write_string_EEPROM(70, auto_name);
  write_string_EEPROM(110, server.arg("OPER"));
  write_string_EEPROM(90, server.arg("DATA_P"));
  EEPROM.begin(256);
  EEPROM.write(130, auto_mode);
  EEPROM.commit();
  EEPROM.end();
  server.send(200, "text/html", SendControlReleAutoHTML());
}

Включение/выключение авто управление происходит по типу выключения/выключения реле:

void handle_AutoOff() {//отключение авто управления
  auto_stat = 0;
  server.send(200, "text/html", SendControlReleAutoHTML());
}

void handle_AutoOn() {//включение авто управления
  auto_stat = 1;
  server.send(200, "text/html", SendControlReleAutoHTML());
}

Ну и последняя кнопка возвращает обратно в меню.

Режим нормально работы (модуль — датчик)

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

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

Вся программа содержится в блоке SETUP, рассмотрим сначала датчик на DHT11:

#include "DHT.h"//библиотека DHT

#define DHTTYPE DHT11
uint8_t DHTPin = 5; 
int tempC = 0;
int humC = 0;
int tmi = 10;// тайминг отправки данных
DHT dht(DHTPin, DHTTYPE); 

void setup() {
  pinMode(DHTPin, INPUT);
  Serial.begin(115200);
  EEPROM.begin(256);
  setup_mode = EEPROM.read(85);
  EEPROM.end();
  device_id = read_string_EEPROM(40);
  if (!setup_mode) {//режим нормально работы
    dht.begin();
    tmi = String(read_string_EEPROM(200)).toInt(); //считываем тайминг отправки данных
    local_ip.fromString(String(read_string_EEPROM(200)));
    gateway.fromString(String(read_string_EEPROM(220)));
    WiFi.begin(read_string_EEPROM(0), read_string_EEPROM(20));
    int sm = 0;
    while (WiFi.status() != WL_CONNECTED) {
      delay(500); sm++;
      Serial.print(".");
      if (sm > 120) {
        ReturnSetup();
      }
    }
    
    Serial.println("WiFi connected");
    tempC = (int)dht.readTemperature(); //считывание температуры в цельсиях
  humC = (int)dht.readHumidity(); //считывание влажности воздуха
    if (client.connect(MQTT::Connect(ID).set_auth(mqtt_user, mqtt_pass))) {
      Serial.println("Connected to MQTT server");
    } else {
      Serial.println("Could not connect to MQTT server");
    }
    client.publish("/datk", device_id+"_t" + "#" + String(tempC) + "#градусов#");
    client.publish("/datk", device_id+"_hm" + "#" + String(humC) + "#влажности#");
    delay(100);
    ESP.deepSleep(tmi * 1000000);//модуль засыпает на определенное время
  }
  else {//режим настройки
    delay(1000);
    WiFi.softAP(ssid_ap, password_ap);
    WiFi.softAPConfig(local_ip, gateway, subnet);
    delay(100);
    server.on("/", handle_OnConnect);
    server.on("/end_setup", handle_EndSetup);
    server.on("/action_page", handleForm);
    server.onNotFound(handle_NotFound);
    server.begin();
    Serial.println("HTTP server started");
  }
}

Теперь SETUP для модуля с ds18b20:

#include //библиотека ds18b20

#define ONE_WIRE_BUS 5
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature DS18B20(&oneWire);
char temperatureCString[7];
int tempC = 0;
int tmi = 10;//тайминг отправки данных

void getTemperature()//считывание температуры с датчика
{
  do {
    DS18B20.requestTemperatures();
    tempC = DS18B20.getTempCByIndex(0);
    dtostrf(tempC, 2, 2, temperatureCString);
    delay(100);
  } while (tempC == 85.0 || tempC == (-127.0));
}

void setup() {
	Serial.begin(115200);
  EEPROM.begin(256);
  setup_mode = EEPROM.read(85);
  EEPROM.end();
  device_id = read_string_EEPROM(40);
  if (!setup_mode) {//режим нормально работы
    DS18B20.begin();
    tmi = String(read_string_EEPROM(200)).toInt(); //считываем тайминг отправки данных
    local_ip.fromString(String(read_string_EEPROM(200)));
    gateway.fromString(String(read_string_EEPROM(220)));
    WiFi.begin(read_string_EEPROM(0), read_string_EEPROM(20));
    int sm = 0;
    while (WiFi.status() != WL_CONNECTED) {
      delay(500); sm++;
      Serial.print(".");
      if (sm > 120) {
        ReturnSetup();
      }
    }
    getTemperature();//считываем температуру
    Serial.println("WiFi connected");
    if (client.connect(MQTT::Connect(ID).set_auth(mqtt_user, mqtt_pass))) {
      Serial.println("Connected to MQTT server");
    } else {
      Serial.println("Could not connect to MQTT server");
    }
    client.publish("/datk", device_id + "#" + String(tempC) + "#градусов#");м;
    delay(100);
    ESP.deepSleep(tmi * 1000000);//модуль засыпает на определенное время
  }
  else {//режим настройки
    delay(1000);
    WiFi.softAP(ssid_ap, password_ap);
    WiFi.softAPConfig(local_ip, gateway, subnet);
    delay(100);
    server.on("/", handle_OnConnect);
    server.on("/end_setup", handle_EndSetup);
    server.on("/action_page", handleForm);
    server.onNotFound(handle_NotFound);
    server.begin();
    Serial.println("HTTP server started");
  }
}

Тут также, как и с датчиком — розеткой, если модуль не может подключиться к WIFI более 1 минуты, то модуль переходит в режим настройки. В момент «сна» ESP практически полностью выключена, работает только RTC таймер, а просыпается от того, что пин, подключенный к RTC таймеру и к пину «Reset», подаёт положительный сигнал, перезагружая МК .

Видео с работой системы

Заключение

Принципе, с натяжкой, это можно назвать «системой умного дома». Конечно, она работает, но есть много нюансов, недоработок… Как начальный проект я доволен, буду дорабатывать, например, добавлю поддержку нескольких модулей — розеток, так же более сложны условия и нормальное использование возможностей MQTT. Также не помешает глобальный рефакторинг кода…

Листинги и файлы

Листинг программы для модуля — розетки
#include 
#include 
#include 
#include 
#include 

#define RELEPIN 4

const char *ssid_ap = "Rozetka_Setup";  //имя точки доступа модуля
const char *password_ap = "12345678";  //пароль точки доступа модуля
const char *ID = "rele_1";

String ssid = "";  //имя wifi
String password = "";  //пароль wifi
String device_id; // ID модуля
bool setup_mode; // true - первичная настройка модуля, false - основная работа модуля
bool rele;

String auto_name = "example";
int auto_data = 10;
int auto_oper = 0;
bool auto_stat = false;// false - не работает, true - работает
bool auto_mode = false; //false - включать, true - выключать

const char *mqtt_server = "M5.WQTT.RU"; // Имя сервера MQTT
const int mqtt_port = 2602; // Порт для подключения к серверу MQTT
const char *mqtt_user = "u_RSELYN"; // Логин от серверa
const char *mqtt_pass = "bhbtIJue"; // Пароль от сервера

String names[10];// массив имен модулей
String znach[10];// массив единиц измерения модулей
int data[10];// массив данных модулей
int lng = 0;//используемая длина


IPAddress local_ip(192, 168, 1, 1);
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 255, 0);

IPAddress local_ip_2(192, 168, 0, 250);
IPAddress gateway_2(192, 168, 0, 1);
IPAddress subnet_2(255, 255, 255, 0);

ESP8266WebServer server(80);// server для настройки

WiFiClient wclient;
PubSubClient client(wclient, mqtt_server, mqtt_port);

void write_string_EEPROM (int Addr, String Str) {
  byte lng = Str.length();
  EEPROM.begin (256);
  EEPROM.write(Addr , lng);
  unsigned char* buf = new unsigned char[15];
  Str.getBytes(buf, lng + 1);
  Addr++;
  for (byte i = 0; i < lng; i++) {
    EEPROM.write(Addr + i, buf[i]);
    delay(10);
  }
  EEPROM.commit();
  EEPROM.end();
}

char *read_string_EEPROM (int Addr) {
  EEPROM.begin(256);
  byte lng = EEPROM.read(Addr);
  char* buf = new char[15];
  Addr++;
  for (byte i = 0; i < lng && i < 15; i++) buf[i] = char(EEPROM.read(i + Addr));
  buf[lng] = '\x0';
  EEPROM.end();
  return buf;
}



void setup() {
  pinMode(RELEPIN, OUTPUT);
  Serial.begin(115200);
  EEPROM.begin(256);
  setup_mode = EEPROM.read(45);// читаем из EERPOM текущий режим
  rele = EEPROM.read(60);// читаем из EERPOM текущее состояние реле
  EEPROM.end();
  if (rele) digitalWrite(RELEPIN, HIGH); else digitalWrite(RELEPIN, LOW);
  if (!setup_mode) { //если модуль в режиме нормальной работы
    
    local_ip_2.fromString(String(read_string_EEPROM(200)));//читаем IP из eeprom 
    gateway_2.fromString(String(read_string_EEPROM(220)));//читаем гейт из eeprom 

    //Serial.println(read_string_EEPROM(0));
    //Serial.println(read_string_EEPROM(20));
    WiFi.begin(read_string_EEPROM(0), read_string_EEPROM(20));
    int sm = 0;
    while (WiFi.status() != WL_CONNECTED) {
      delay(500); sm++;
      Serial.print(".");
      if (sm > 120) {
        handle_ReturnSetup();
      }
    }
    WiFi.config(local_ip_2, gateway_2, subnet_2);
    Serial.println("WiFi connected");
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());
    read_auto();
    server.on("/", handle_OnConnect_2);
    server.on("/select_auto", handle_SelectAuto);
    server.on("/rele_auto", handle_ReleAuto);
    server.on("/delete_page", handle_Delete);
    server.on("/return_setup", handle_ReturnSetup);
    server.on("/control_menu", handle_ControlMenu);
    server.on("/rele_off", handle_ReleOff);
    server.on("/rele_on", handle_ReleOn);
    server.on("/auto_off", handle_AutoOff);
    server.on("/auto_on", handle_AutoOn);
    server.onNotFound(handle_NotFound);
    server.begin();
    Serial.println("HTTP server started");

    if (client.connect(MQTT::Connect(ID).set_auth(mqtt_user, mqtt_pass))) {
      Serial.println("Connected to MQTT server");
      client.subscribe("/datk"); // подписывааемся на топик с данными датчиков
      //client.subscribe("/cmd"); // подписывааемся на топик с командами
      client.set_callback(callback);
    } else {
      Serial.println("Could not connect to MQTT server");
    }
  }
  else {//если модуль в режиме настройки
    delay(1000);
    WiFi.softAP(ssid_ap, password_ap);
    WiFi.softAPConfig(local_ip, gateway, subnet);
    delay(100);
    server.on("/", handle_OnConnect);
    server.on("/end_setup", handle_EndSetup);
    server.on("/action_page", handleForm);
    server.onNotFound(handle_NotFound);
    server.begin();
    Serial.println("HTTP server started");
  }
}

void read_auto() {
  EEPROM.begin(256);
  auto_mode = EEPROM.read(130);
  EEPROM.end();
  auto_data = String(read_string_EEPROM(90)).toInt();
  Serial.println(read_string_EEPROM(90));
  auto_oper = String(read_string_EEPROM(110)).toInt();
  auto_name = read_string_EEPROM(70);
}

void perek(bool per) { //per = true = прямой режим, per = true = обратный режим
  EEPROM.begin(256);
  if (auto_mode) {
    if (per) {
      rele = 0;
      digital
    
            

© Habrahabr.ru