Очередной умный дом (или как потерять 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В, да и такую нагрузку лучше не включать прямо в пин микроконтроллера, может сгореть.

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

Как было сказано раньше, 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 может попросту сгореть.

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

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

MQTT

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

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

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

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

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

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

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

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

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

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

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

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

#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <EEPROM.h>
#include <PubSubClient.h>

Следом идут определение константан для точки доступа самого модуля, 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 = "<!DOCTYPE html><html lang='ru-RU'><head><meta charset='UTF-8'/> <html>\n";
  ptr += "<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=no\">\n";
  ptr += "<title>WIFI Control</title>\n";
  ptr += "<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}\n";
  ptr += "body{margin-top: 50px;} h1 {color: #444444;margin: 50px auto 30px;} h3 {color: #444444;margin-bottom: 50px;}\n";
  ptr += ".button {display: block;width: 80px;background-color: #1abc9c;border: none;color: white;padding: 13px 30px;text-decoration: none;font-size: 25px;margin: 0px auto 35px;cursor: pointer;border-radius: 4px;}\n";
  ptr += ".button-on {background-color: #1abc9c;}\n";
  ptr += ".button-on:active {background-color: #16a085;}\n";
  ptr += ".button-off {background-color: #34495e;}\n";
  ptr += ".button-off:active {background-color: #2c3e50;}\n";
  ptr += "p {font-size: 14px;color: #888;margin-bottom: 10px;}\n";
  ptr += "</style>\n";
  ptr += "</head>\n";
  ptr += "<body>\n";
  ptr += "<h1>Настройка WIFI</h1>\n";
  ptr += "<h3>Укажите название и пароль от нужной wifi сети</h3>\n";
  ptr += "<form action=\"/action_page\">";
  ptr += "Название:<br>";
  ptr += "<input type=\"text\" name=\"WIFI_NAME\" value=\"тест_название\">";
  ptr += "<br>";
  ptr += "Пароль:<br>";
  ptr += "<input type=\"text\" name=\"WIFI_password\" value=\"123456789\">";
  ptr += "<br>";
  ptr += "Название модуля:<br>";
  ptr += "<input type=\"text\" maxlength=\"14\" name=\"DEVICE_ID\" value=\"розетка\"><br>";
  ptr += "Local_ip:<br>";
  ptr += "<input type=\"text\" name=\"LOCAL_IP\" value=\"192.168.0.250\">";
  ptr += "<br>";
  ptr += "Gateway:<br>";
  ptr += "<input type=\"text\" name=\"GATEWAY\" value=\"192.168.0.1\">";
  ptr += "<br>";
  ptr += "<br><br>";
  ptr += "<input type=\"submit\" value=\"Закончить\">";
  ptr += "</form>";
  ptr += "</body>\n";
  ptr += "</html>\n";
  return ptr;
}

String SendHTML() {//Отправка HTML страницы для настройки модуля - датчика
  String ptr = "<!DOCTYPE html><html lang='ru-RU'><head><meta charset='UTF-8'/> <html>\n";
  ptr += "<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=no\">\n";
  ptr += "<title>WIFI Control</title>\n";
  ptr += "<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}\n";
  ptr += "body{margin-top: 50px;} h1 {color: #444444;margin: 50px auto 30px;} h3 {color: #444444;margin-bottom: 50px;}\n";
  ptr += ".button {display: block;width: 80px;background-color: #1abc9c;border: none;color: white;padding: 13px 30px;text-decoration: none;font-size: 25px;margin: 0px auto 35px;cursor: pointer;border-radius: 4px;}\n";
  ptr += ".button-on {background-color: #1abc9c;}\n";
  ptr += ".button-on:active {background-color: #16a085;}\n";
  ptr += ".button-off {background-color: #34495e;}\n";
  ptr += ".button-off:active {background-color: #2c3e50;}\n";
  ptr += "p {font-size: 14px;color: #888;margin-bottom: 10px;}\n";
  ptr += "</style>\n";
  ptr += "</head>\n";
  ptr += "<body>\n";
  ptr += "<h1>Настройка WIFI</h1>\n";
  ptr += "<h3>Укажите название и пароль от нужной wifi сети</h3>\n";
  ptr += "<form action=\"/action_page\">";
  ptr += "Название:<br>";
  ptr += "<input type=\"text\" name=\"WIFI_NAME\" value=\"тест_название\">";
  ptr += "<br>";
  ptr += "Пароль:<br>";
  ptr += "<input type=\"text\" name=\"WIFI_password\" value=\"123456789\">";
  ptr += "<br>";
  ptr += "Тайминг отправки(в секундах):<br>";
  ptr += "<input type=\"text\" name=\"TIMING\" value=\"1\">";
  ptr += "<br>";
  ptr += "Название модуля:<br>";
  ptr += "<input type=\"text\" maxlength=\"10\" name=\"DEVICE_ID\" value=\"datc_temp\"><br>";
  ptr += "<br><br>";
  ptr += "<input type=\"submit\" value=\"Закончить\">";
  ptr += "</form>";
  ptr += "</body>\n";
  ptr += "</html>\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 = "<!DOCTYPE html><html lang='ru-RU'><head><meta charset='UTF-8'/><html>\n";
  ptr += "<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=no\">\n";
  ptr += "<title>WIFI Control</title>\n";
  ptr += "<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}\n";
  ptr += "body{margin-top: 50px;} h1 {color: #444444;margin: 50px auto 30px;}\n";
  ptr += "p {font-size: 14px;color: #888;margin-bottom: 10px;}\n";
  ptr += "</style>\n";
  ptr += "</head>\n";
  ptr += "<body>\n";
  ptr += "<a href='/'><h1>Вернуться к настройки</h1></a>\n";
  ptr += "<a href='/end_setup'><h1>Продолжить</h1></a>\n";
  ptr += "</body>\n";
  ptr += "</html>\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 = "<!DOCTYPE html> <html lang='ru-RU'><head><meta charset='UTF-8'/><html>\n";
  ptr += "<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=no\">\n";
  ptr += "<title>WIFI Control</title>\n";
  ptr += "<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}\n";
  ptr += "body{margin-top: 50px;} h1 {color: #444444;margin: 50px auto 30px;} h3 {color: #444444;margin-bottom: 50px;}\n";
  ptr += ".button {display: block;width: 80px;background-color: #1abc9c;border: none;color: white;padding: 13px 30px;text-decoration: none;font-size: 25px;margin: 0px auto 35px;cursor: pointer;border-radius: 4px;}\n";
  ptr += ".button-on {background-color: #1abc9c;}\n";
  ptr += ".button-on:active {background-color: #16a085;}\n";
  ptr += ".button-off {background-color: #34495e;}\n";
  ptr += ".button-off:active {background-color: #2c3e50;}\n";
  ptr += "p {font-size: 14px;color: #888;margin-bottom: 10px;}\n";
  ptr += "</style>\n";
  ptr += "</head>\n";
  ptr += "<body>\n";
  ptr += "<h1>WIFI меню</h1>\n";
  ptr += "<h3>Выберете пункт меню:</h3>\n";
  ptr += "<br><input type=\"button\" value=\"Вернуться в режим настройки\" onClick=\"document.location = '/return_setup'\" /><br>";
  ptr += "<br><input type=\"button\" value=\"Панель управления\" onClick=\"document.location = '/control_menu'\" /><br>";
  ptr += "<br><input type=\"button\" value=\"Автоматическое управление розеткой\" onClick=\"document.location = '/rele_auto'\" /><br>";
  ptr += "</body>\n";
  ptr += "</html>\n";
  return ptr;
}

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

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

Возвращение к меню управления происходит через 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 = "<!DOCTYPE html><html lang='ru-RU'><head><meta charset='UTF-8'/> <html>\n";
  ptr += "<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=no\">\n";
  ptr += "<title>WIFI Control</title>\n";
  ptr += "<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}\n";
  ptr += "body{margin-top: 50px;} h1 {color: #444444;margin: 50px auto 30px; } h3 {color: #444444;margin-bottom: 50px;} h4 {color: #444444;margin-bottom: 20px;}\n";
  ptr += "p {font-size: 14px;color: #888;margin-bottom: 10px;}\n";
  ptr += "</style>\n";
  ptr += "</head>\n";
  ptr += "<body>\n";
  ptr += "<h1>Панель управления</h1>\n";
  ptr += "<br><input type=\"button\" value=\"Вернуться в меню\" onClick=\"document.location = '/'\" /><br>";
  if (rele) {
    ptr += "<br><input type=\"button\" value=\"Включить розетку\" onClick=\"document.location = '/rele_off'\" /><br>";
  }
  if (!rele) {
    ptr += "<br><input type=\"button\" value=\"Выключить розетку\" onClick=\"document.location = '/rele_on'\" /><br>";
  }
  ptr += "<h3>Датчики:</h3>\n";
  //ptr += "<h4>Датчики:</h4>\n";
  for (int i = 0 ; i < lng; i++) {
    ptr += "<h4>" + names[i] + " : " + data[i] + " " + znach[i] + "</h4>";
  }
  ptr += "<form action=\"/delete_page\">";
  ptr += "Название:  ";
  ptr += "<input type=\"text\" name=\"DELETE_NAME\" value=\"модуль\">   ";
  //ptr += "<br><br>";
  ptr += "<input type=\"submit\" value=\"удалить\">";
  ptr += "</form>";
  ptr += "<br><input type=\"button\" value=\"обновить\" onClick=\"document.location = '/control_menu'\" /><br>";
  ptr += "</body>\n";
  ptr += "</html>\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 = "<!DOCTYPE html><html lang='ru-RU'><head><meta charset='UTF-8'/> <html>\n";
  ptr += "<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=no\">\n";
  ptr += "<title>WIFI Control</title>\n";
  ptr += "<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}\n";
  ptr += "body{margin-top: 50px;} h1 {color: #444444;margin: 50px auto 30px; } h3 {color: #444444;margin-bottom: 50px;} h4 {color: #444444;margin-bottom: 20px;}\n";
  ptr += "p {font-size: 14px;color: #888;margin-bottom: 10px;}\n";
  ptr += "</style>\n";
  ptr += "</head>\n";
  ptr += "<body>\n";
  ptr += "<h1>Настройка автоматического управления розетки </h1>";
  if (auto_stat) {
    ptr += "<h3>Автоматическое управления включено</h3>";
  }
  else {
    ptr += "<h3>Автоматическое управления выключено</h3>";
  }
  ptr += "<h3>Текущее условие:" + auto_name + " ";
  switch (auto_oper) {
    case 0 : ptr += "> ";
      break;
    case 1 : ptr += "< ";
      break;
    case 2 : ptr += "= ";
      break;
  }
  if (auto_mode)
    ptr += String(auto_data) + " выключать </h3>";
  else
    ptr += String(auto_data) + " включать </h3>";
  ptr += "<h3>Выберете датчик и параметр для него:</h3>\n";
  ptr += "<form action=\"/select_auto\">";
  ptr += "<select size=\"1\" name=\"DAT_NAME\">";
  for (int i = 0 ; i < lng; i++) {
    ptr += "<option value=\"" + String(i) + "\">" + names[i] + "</option>";
  }
  ptr += "</select>  ";
  ptr += "<select size=\"1\" name=\"OPER\">";
  ptr += "<option value=0>></option>";
  ptr += "<option value=1><</option>";
  ptr += "<option value=2>=</option>";
  ptr += "</select>  ";
  ptr += "</select>  ";
  ptr += "<input type=\"text\" name=\"DATA_P\" value=\"10\">";
  ptr += "<select size=\"1\" name=\"ON_OFF\">";
  ptr += "<option value=0>Включать</option>";
  ptr += "<option value=1>Выключать</option>";
  ptr += "</select>  ";
  //ptr += "<br><br>";
  ptr += "<input type=\"submit\" value=\"настроить\">";
  ptr += "</form>";
  ptr += "<br><input type=\"button\" value=\"обновить\" onClick=\"document.location = '/rele_auto'\" /><br>";
  if (auto_stat) {
    ptr += "<br><input type=\"button\" value=\"выключить автопереключение\" onClick=\"document.location = '/auto_off'\" /><br>";
  }
  else {
    ptr += "<br><input type=\"button\" value=\"включить автопереключение\" onClick=\"document.location = '/auto_on'\" /><br>";
  }
  ptr += "<br><input type=\"button\" value=\"Вернуться в меню\" onClick=\"document.location = '/'\" /><br>";
  ptr += "</body>\n";
  ptr += "</html>\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 <DallasTemperature.h>//библиотека 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 <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <EEPROM.h>
#include <PubSubClient.h>

#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;
      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 loop() {
  server.handleClient();
  if (client.connected()) {
    client.loop();
  }
}

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;
      }
    }
  }

  /*if (String(pub.topic()) == "/сmd"){
    if(pub.payload_string()=="ON")
    {
      rele = 1;
    digitalWrite(RELEPIN, HIGH  );
    EEPROM.begin(256);
    EEPROM.write(60, 1);
    EEPROM.commit();
    EEPROM.end();
    }
    else
    {
      rele = 0;
    digitalWrite(RELEPIN, LOW  );
    EEPROM.begin(256);
    EEPROM.write(60, 0);
    EEPROM.commit();
    EEPROM.end();
    }
    }*/
}


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

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());
}

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


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();
  Serial.println(auto_name);
  Serial.println(auto_oper);
  Serial.println(auto_data);
  Serial.println(auto_mode);
  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());
}

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());
}

void handle_OnConnect() {
  server.send(200, "text/html", SendHTML());
}

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

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 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();
}

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

void handle_NotFound() {
  server.send(404, "text/plain", "Not found");
}

String SendEndHTML() {
  String ptr = "<!DOCTYPE html><html lang='ru-RU'><head><meta charset='UTF-8'/><html>\n";
  ptr += "<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=no\">\n";
  ptr += "<title>WIFI Control</title>\n";
  ptr += "<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}\n";
  ptr += "body{margin-top: 50px;} h1 {color: #444444;margin: 50px auto 30px;}\n";
  ptr += "p {font-size: 14px;color: #888;margin-bottom: 10px;}\n";
  ptr += "</style>\n";
  ptr += "</head>\n";
  ptr += "<body>\n";
  ptr += "<a href='/'><h1>Вернуться к настройки</h1></a>\n";
  ptr += "<a href='/end_setup'><h1>Продолжить</h1></a>\n";
  ptr += "</body>\n";
  ptr += "</html>\n";
  return ptr;
}

String SendHTML() {
  String ptr = "<!DOCTYPE html><html lang='ru-RU'><head><meta charset='UTF-8'/> <html>\n";
  ptr += "<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=no\">\n";
  ptr += "<title>WIFI Control</title>\n";
  ptr += "<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}\n";
  ptr += "body{margin-top: 50px;} h1 {color: #444444;margin: 50px auto 30px;} h3 {color: #444444;margin-bottom: 50px;}\n";
  ptr += ".button {display: block;width: 80px;background-color: #1abc9c;border: none;color: white;padding: 13px 30px;text-decoration: none;font-size: 25px;margin: 0px auto 35px;cursor: pointer;border-radius: 4px;}\n";
  ptr += ".button-on {background-color: #1abc9c;}\n";
  ptr += ".button-on:active {background-color: #16a085;}\n";
  ptr += ".button-off {background-color: #34495e;}\n";
  ptr += ".button-off:active {background-color: #2c3e50;}\n";
  ptr += "p {font-size: 14px;color: #888;margin-bottom: 10px;}\n";
  ptr += "</style>\n";
  ptr += "</head>\n";
  ptr += "<body>\n";
  ptr += "<h1>Настройка WIFI</h1>\n";
  ptr += "<h3>Укажите название и пароль от нужной wifi сети</h3>\n";
  ptr += "<form action=\"/action_page\">";
  ptr += "Название:<br>";
  ptr += "<input type=\"text\" name=\"WIFI_NAME\" value=\"тест_название\">";
  ptr += "<br>";
  ptr += "Пароль:<br>";
  ptr += "<input type=\"text\" name=\"WIFI_password\" value=\"123456789\">";
  ptr += "<br>";
  ptr += "Название модуля:<br>";
  ptr += "<input type=\"text\" maxlength=\"14\" name=\"DEVICE_ID\" value=\"розетка\"><br>";
  ptr += "Local_ip:<br>";
  ptr += "<input type=\"text\" name=\"LOCAL_IP\" value=\"192.168.0.250\">";
  ptr += "<br>";
  ptr += "Gateway:<br>";
  ptr += "<input type=\"text\" name=\"GATEWAY\" value=\"192.168.0.1\">";
  ptr += "<br>";
  ptr += "<br><br>";
  ptr += "<input type=\"submit\" value=\"Закончить\">";
  ptr += "</form>";
  ptr += "</body>\n";
  ptr += "</html>\n";
  return ptr;
}

String SendMenuHTML() {
  String ptr = "<!DOCTYPE html> <html lang='ru-RU'><head><meta charset='UTF-8'/><html>\n";
  ptr += "<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=no\">\n";
  ptr += "<title>WIFI Control</title>\n";
  ptr += "<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}\n";
  ptr += "body{margin-top: 50px;} h1 {color: #444444;margin: 50px auto 30px;} h3 {color: #444444;margin-bottom: 50px;}\n";
  ptr += ".button {display: block;width: 80px;background-color: #1abc9c;border: none;color: white;padding: 13px 30px;text-decoration: none;font-size: 25px;margin: 0px auto 35px;cursor: pointer;border-radius: 4px;}\n";
  ptr += ".button-on {background-color: #1abc9c;}\n";
  ptr += ".button-on:active {background-color: #16a085;}\n";
  ptr += ".button-off {background-color: #34495e;}\n";
  ptr += ".button-off:active {background-color: #2c3e50;}\n";
  ptr += "p {font-size: 14px;color: #888;margin-bottom: 10px;}\n";
  ptr += "</style>\n";
  ptr += "</head>\n";
  ptr += "<body>\n";
  ptr += "<h1>WIFI меню</h1>\n";
  ptr += "<h3>Выберете пункт меню:</h3>\n";
  ptr += "<br><input type=\"button\" value=\"Вернуться в режим настройки\" onClick=\"document.location = '/return_setup'\" /><br>";
  ptr += "<br><input type=\"button\" value=\"Панель управления\" onClick=\"document.location = '/control_menu'\" /><br>";
  ptr += "<br><input type=\"button\" value=\"Автоматическое управление розеткой\" onClick=\"document.location = '/rele_auto'\" /><br>";
  ptr += "</body>\n";
  ptr += "</html>\n";
  return ptr;
}

String SendControlMenuHTML() {
  String ptr = "<!DOCTYPE html><html lang='ru-RU'><head><meta charset='UTF-8'/> <html>\n";
  ptr += "<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=no\">\n";
  ptr += "<title>WIFI Control</title>\n";
  ptr += "<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}\n";
  ptr += "body{margin-top: 50px;} h1 {color: #444444;margin: 50px auto 30px; } h3 {color: #444444;margin-bottom: 50px;} h4 {color: #444444;margin-bottom: 20px;}\n";
  ptr += "p {font-size: 14px;color: #888;margin-bottom: 10px;}\n";
  ptr += "</style>\n";
  ptr += "</head>\n";
  ptr += "<body>\n";
  ptr += "<h1>Панель управления</h1>\n";
  ptr += "<br><input type=\"button\" value=\"Вернуться в меню\" onClick=\"document.location = '/'\" /><br>";
  if (rele) {
    ptr += "<br><input type=\"button\" value=\"Включить розетку\" onClick=\"document.location = '/rele_off'\" /><br>";
  }
  if (!rele) {
    ptr += "<br><input type=\"button\" value=\"Выключить розетку\" onClick=\"document.location = '/rele_on'\" /><br>";
  }
  ptr += "<h3>Датчики:</h3>\n";
  //ptr += "<h4>Датчики:</h4>\n";
  for (int i = 0 ; i < lng; i++) {
    ptr += "<h4>" + names[i] + " : " + data[i] + " " + znach[i] + "</h4>";
  }
  ptr += "<form action=\"/delete_page\">";
  ptr += "Название:  ";
  ptr += "<input type=\"text\" name=\"DELETE_NAME\" value=\"модуль\">   ";
  //ptr += "<br><br>";
  ptr += "<input type=\"submit\" value=\"удалить\">";
  ptr += "</form>";
  ptr += "<br><input type=\"button\" value=\"обновить\" onClick=\"document.location = '/control_menu'\" /><br>";
  ptr += "</body>\n";
  ptr += "</html>\n";
  return ptr;
}

String SendControlReleAutoHTML() {
  String ptr = "<!DOCTYPE html><html lang='ru-RU'><head><meta charset='UTF-8'/> <html>\n";
  ptr += "<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=no\">\n";
  ptr += "<title>WIFI Control</title>\n";
  ptr += "<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}\n";
  ptr += "body{margin-top: 50px;} h1 {color: #444444;margin: 50px auto 30px; } h3 {color: #444444;margin-bottom: 50px;} h4 {color: #444444;margin-bottom: 20px;}\n";
  ptr += "p {font-size: 14px;color: #888;margin-bottom: 10px;}\n";
  ptr += "</style>\n";
  ptr += "</head>\n";
  ptr += "<body>\n";
  ptr += "<h1>Настройка автоматического управления розетки </h1>";
  if (auto_stat) {
    ptr += "<h3>Автоматическое управления включено</h3>";
  }
  else {
    ptr += "<h3>Автоматическое управления выключено</h3>";
  }
  ptr += "<h3>Текущее условие:" + auto_name + " ";
  switch (auto_oper) {
    case 0 : ptr += "> ";
      break;
    case 1 : ptr += "< ";
      break;
    case 2 : ptr += "= ";
      break;
  }
  if (auto_mode)
    ptr += String(auto_data) + " выключать </h3>";
  else
    ptr += String(auto_data) + " включать </h3>";
  ptr += "<h3>Выберете датчик и параметр для него:</h3>\n";
  ptr += "<form action=\"/select_auto\">";
  ptr += "<select size=\"1\" name=\"DAT_NAME\">";
  for (int i = 0 ; i < lng; i++) {
    ptr += "<option value=\"" + String(i) + "\">" + names[i] + "</option>";
  }
  ptr += "</select>  ";
  ptr += "<select size=\"1\" name=\"OPER\">";
  ptr += "<option value=0>></option>";
  ptr += "<option value=1><</option>";
  ptr += "<option value=2>=</option>";
  ptr += "</select>  ";
  ptr += "</select>  ";
  ptr += "<input type=\"text\" name=\"DATA_P\" value=\"10\">";
  ptr += "<select size=\"1\" name=\"ON_OFF\">";
  ptr += "<option value=0>Включать</option>";
  ptr += "<option value=1>Выключать</option>";
  ptr += "</select>  ";
  //ptr += "<br><br>";
  ptr += "<input type=\"submit\" value=\"настроить\">";
  ptr += "</form>";
  ptr += "<br><input type=\"button\" value=\"обновить\" onClick=\"document.location = '/rele_auto'\" /><br>";
  if (auto_stat) {
    ptr += "<br><input type=\"button\" value=\"выключить автопереключение\" onClick=\"document.location = '/auto_off'\" /><br>";
  }
  else {
    ptr += "<br><input type=\"button\" value=\"включить автопереключение\" onClick=\"document.location = '/auto_on'\" /><br>";
  }
  ptr += "<br><input type=\"button\" value=\"Вернуться в меню\" onClick=\"document.location = '/'\" /><br>";
  ptr += "</body>\n";
  ptr += "</html>\n";
  return ptr;
}
Листинг программы для модуля - датчика DHT11
#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <EEPROM.h>
#include <PubSubClient.h>
#include "DHT.h"

#define DHTTYPE DHT11
uint8_t DHTPin = 5; 
int tempC = 0;
int humC = 0;
int tmi = 10;
DHT dht(DHTPin, DHTTYPE); 

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

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

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"; // Пароль от сервера

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

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

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

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(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)));
    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) {
        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");
  }
}

void loop() {
  if (setup_mode)
    server.handleClient();
}

void handle_OnConnect() {
  server.send(200, "text/html", SendHTML());
}

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


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
}

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

void handle_NotFound() {
  server.send(404, "text/plain", "Not found");
}

String SendEndHTML() {
  String ptr = "<!DOCTYPE html><html lang='ru-RU'><head><meta charset='UTF-8'/><html>\n";
  ptr += "<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=no\">\n";
  ptr += "<title>WIFI Control</title>\n";
  ptr += "<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}\n";
  ptr += "body{margin-top: 50px;} h1 {color: #444444;margin: 50px auto 30px;}\n";
  ptr += "p {font-size: 14px;color: #888;margin-bottom: 10px;}\n";
  ptr += "</style>\n";
  ptr += "</head>\n";
  ptr += "<body>\n";
  ptr += "<a href='/'><h1>Вернуться к настройки</h1></a>\n";
  ptr += "<a href='/end_setup'><h1>Продолжить</h1></a>\n";
  ptr += "</body>\n";
  ptr += "</html>\n";
  return ptr;
}

String SendHTML() {
  String ptr = "<!DOCTYPE html><html lang='ru-RU'><head><meta charset='UTF-8'/> <html>\n";
  ptr += "<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=no\">\n";
  ptr += "<title>WIFI Control</title>\n";
  ptr += "<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}\n";
  ptr += "body{margin-top: 50px;} h1 {color: #444444;margin: 50px auto 30px;} h3 {color: #444444;margin-bottom: 50px;}\n";
  ptr += ".button {display: block;width: 80px;background-color: #1abc9c;border: none;color: white;padding: 13px 30px;text-decoration: none;font-size: 25px;margin: 0px auto 35px;cursor: pointer;border-radius: 4px;}\n";
  ptr += ".button-on {background-color: #1abc9c;}\n";
  ptr += ".button-on:active {background-color: #16a085;}\n";
  ptr += ".button-off {background-color: #34495e;}\n";
  ptr += ".button-off:active {background-color: #2c3e50;}\n";
  ptr += "p {font-size: 14px;color: #888;margin-bottom: 10px;}\n";
  ptr += "</style>\n";
  ptr += "</head>\n";
  ptr += "<body>\n";
  ptr += "<h1>Настройка WIFI</h1>\n";
  ptr += "<h3>Укажите название и пароль от нужной wifi сети</h3>\n";
  ptr += "<form action=\"/action_page\">";
  ptr += "Название:<br>";
  ptr += "<input type=\"text\" name=\"WIFI_NAME\" value=\"тест_название\">";
  ptr += "<br>";
  ptr += "Пароль:<br>";
  ptr += "<input type=\"text\" name=\"WIFI_password\" value=\"123456789\">";
  ptr += "<br>";
  ptr += "Тайминг отправки(в секундах):<br>";
  ptr += "<input type=\"text\" name=\"TIMING\" value=\"1\">";
  ptr += "<br>";
  ptr += "Название модуля:<br>";
  ptr += "<input type=\"text\" maxlength=\"10\" name=\"DEVICE_ID\" value=\"datc_temp\"><br>";
  ptr += "<br><br>";
  ptr += "<input type=\"submit\" value=\"Закончить\">";
  ptr += "</form>";
  ptr += "</body>\n";
  ptr += "</html>\n";
  return ptr;
}
Листинг программы для модуля - датчика ds18b20
#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <EEPROM.h>
#include <PubSubClient.h>
#include <OneWire.h>
#include <DallasTemperature.h>

#define ONE_WIRE_BUS 5
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature DS18B20(&oneWire);
char temperatureCString[7];
int tempC = 0;
int tmi = 10;

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

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

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"; // Пароль от сервера

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

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

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

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 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) {
    tmi = String(read_string_EEPROM(200)).toInt();
    local_ip.fromString(String(read_string_EEPROM(200)));
    gateway.fromString(String(read_string_EEPROM(220)));
    DS18B20.begin();
    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) {
        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");
  }
}

void loop() {
  if (setup_mode)
    server.handleClient();
}

void handle_OnConnect() {
  server.send(200, "text/html", SendHTML());
}

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


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
}

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

void handle_NotFound() {
  server.send(404, "text/plain", "Not found");
}

String SendEndHTML() {
  String ptr = "<!DOCTYPE html><html lang='ru-RU'><head><meta charset='UTF-8'/><html>\n";
  ptr += "<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=no\">\n";
  ptr += "<title>WIFI Control</title>\n";
  ptr += "<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}\n";
  ptr += "body{margin-top: 50px;} h1 {color: #444444;margin: 50px auto 30px;}\n";
  ptr += "p {font-size: 14px;color: #888;margin-bottom: 10px;}\n";
  ptr += "</style>\n";
  ptr += "</head>\n";
  ptr += "<body>\n";
  ptr += "<a href='/'><h1>Вернуться к настройки</h1></a>\n";
  ptr += "<a href='/end_setup'><h1>Продолжить</h1></a>\n";
  ptr += "</body>\n";
  ptr += "</html>\n";
  return ptr;
}

String SendHTML() {
  String ptr = "<!DOCTYPE html><html lang='ru-RU'><head><meta charset='UTF-8'/> <html>\n";
  ptr += "<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=no\">\n";
  ptr += "<title>WIFI Control</title>\n";
  ptr += "<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}\n";
  ptr += "body{margin-top: 50px;} h1 {color: #444444;margin: 50px auto 30px;} h3 {color: #444444;margin-bottom: 50px;}\n";
  ptr += ".button {display: block;width: 80px;background-color: #1abc9c;border: none;color: white;padding: 13px 30px;text-decoration: none;font-size: 25px;margin: 0px auto 35px;cursor: pointer;border-radius: 4px;}\n";
  ptr += ".button-on {background-color: #1abc9c;}\n";
  ptr += ".button-on:active {background-color: #16a085;}\n";
  ptr += ".button-off {background-color: #34495e;}\n";
  ptr += ".button-off:active {background-color: #2c3e50;}\n";
  ptr += "p {font-size: 14px;color: #888;margin-bottom: 10px;}\n";
  ptr += "</style>\n";
  ptr += "</head>\n";
  ptr += "<body>\n";
  ptr += "<h1>Настройка WIFI</h1>\n";
  ptr += "<h3>Укажите название и пароль от нужной wifi сети</h3>\n";
  ptr += "<form action=\"/action_page\">";
  ptr += "Название:<br>";
  ptr += "<input type=\"text\" name=\"WIFI_NAME\" value=\"тест_название\">";
  ptr += "<br>";
  ptr += "Пароль:<br>";
  ptr += "<input type=\"text\" name=\"WIFI_password\" value=\"123456789\">";
  ptr += "<br>";
  ptr += "Тайминг отправки(в секундах):<br>";
  ptr += "<input type=\"text\" name=\"TIMING\" value=\"1\">";
  ptr += "<br>";
  ptr += "Название модуля:<br>";
  ptr += "<input type=\"text\" maxlength=\"14\" name=\"DEVICE_ID\" value=\"datc_temp\"><br>";
  ptr += "<br><br>";
  ptr += "<input type=\"submit\" value=\"Закончить\">";
  ptr += "</form>";
  ptr += "</body>\n";
  ptr += "</html>\n";
  return ptr;
}

Комментарии 34

    +7
    Так это диплом или проект для 9 класса?
      0
      Одинаково, у нас это называли дипломом и проектом
        0
        Интересно, в какой школе такие требования к проектам?
          0
          Вообще, сейчас во всех школах(наверное) сейчас нужно делать проекты. Если вы про «сложность» проекта, то, на самом деле там не такие жесткие требования, можно даже не защищаться, а просто отправить работу на «конкурс», тебе сделают диплом и как бы все. Просто давно хотел сделать что-то типа умного дома, а вот и момент удачный)
            0
            Это в какой стране такие школы?)
              0
              Россия)
                0
                Вы очевидно сами должны понимать что это если не бред, то заблуждение. Нет ни чего подобного ни в ВУЗах ни в Общеобразовательных заведениях(школах). Подобного рода проекты либо разработки возможны, но только в специализированных «колледжах»(техникумах), которые априори не являются школами.
                Здоровья всем, физического, психического и душевного.
                  0
                  Я говорю как есть, я учусь в 9 классе самой обычной школы.
                  Да, нам не давали задания делать такие «сложные» проекты, кто-то просто в последний день взял и скопировал с инета и сдал и прошел. Просто у меня давно была мысль сделать что-то подобное(хотя, по моему мнению мое решение максимально сырое и делается довольно просто). В данных проектах учеников не ограничивают обычно, хоть делай собственный компьютер, хоть анализ рынка, хоть проект о важности ежедневного хождение в туалет (с последним я не уверен).
      +7
      даже если это физмат школа, для 9-го класса это отлично. Прикрутите какой-нибудь томограф, ещё и для ВУЗа диплом получится.
        +1
        Томограф… хм..)
        –6

        Господи, какая жесть! Идея хорошая, но реализация…
        Не надо это использовать, под, никогда, пожалейте соседей. Блок питания с АлиЭкспресса, китайские модули из электроного мусора, печатный корпус, отсутствие даже простейшего плавкого предохранителя, китайские реле на неизвестный ток.

          +4
          кх, знаю, у меня знаний практически нет, особенно по технике безопасности.
          А чем вам блок питания не понравился? (https://aliexpress.ru/item/32958430448.html?spm=a2g0s.9042311.0.0.264d33edErZjho&sku_id=66421514448)
          Реле брал в Чип дипе, знаю на какой ток.
          А про корпус, что с ним не так(pla пластик)?
          Да и модули вроде нормальные
          Был бы рад если бы объяснили, хотя-бы примерно, где у меня ошибки, чтобы не допускал их больше
            +2
            как минимум на нагрузки более 100Вт лучше использовать или твердотельные реле или контакторы. На реле конечно написано 16А но это ток аннигиляции скорее, а никак не то на что можно рассчитывать при эксплуатации.
            Как проект неплохо, даже очень. Как результат — по сути это велосипед, конечно.
            Я правильно понял что вся децентрализация сводится к использованию облачного брокера? В таком случае у вас появляются две проблемы вместо одной (неисправность локального брокера) — облачный брокер сам по себе может перестать быть, и может перестать быть интернет, тогда система превратится в тыкву?
            Посмотрите на конструктив уже имеющихся решений от того же сонофа, как облачные так и в DIY режиме. Там можно подсмотреть как схемотехнику так и логику работы.
            Причем не знаю как большинство но лично я сразу отметаю любые системы, которые 100% завязаны на облако(а), так как мне не хотелось бы идти в туалет в темноте потому что у меня интернет отключили, к примеру.
              0
              Про реле, у меня была идея использовать вместо него симистор, но не успел реализовать. Контакторы довольно габаритные, все же хотелось сделать как можно меньше саму розетку.
              Под децентрализацию(если так вообще правильно называть) я подразумевал то, что во многих системах, если я опять же правильно понял, есть устройство — хаб, через которое система и работает. Я хотел сделать так, чтобы у меня этого хаба не было, а все датчики могли работать сами по себе(через интернет), а модуль — розетка работал чисто как wifi-реле и интерфейс система-пользователь. Про интернет, у меня есть несколько идей, которые я опять же не успел реализовать, например, esp же может работать одновременно как точка доступа, так и подключаться к уже существующему WIFI и, тогда у нас получается гибрид моей системы и системы с хабом…
              Спасибо, посмотрю решения от сонофа. Я просто пока смотрел только от xiomy и яндекса. У яндекса розетка рассчитана тоже примерно на 10А.
              Вы про 100вт может перепутали? Просто как-то мало тогда реле может
                0
                1) Сама по себе розетка имеет право на жизнь как устройство, не спорю. Когда их становится 2,3,5 и более они превращаются в дикий колхоз. Поэтому опять же я предпочитаю контакторы, установленные в центральном щите, которые коммутируют нужные линии. Если требуется управлять конкретным устройством (типа кофемашины, к примеру), всегда можно поставить внутрь esp8266 и там, скорее всего, не понадобятся мощные реле, так как мы будем дублировать интерфейсные кнопки. Обратно — розетка как отдельный девайс не нужна. Честно говоря, я не могу придумать сценария где лично я хотел бы именно вот такую прокладку между розеткой и устройством на постоянной основе, возможно пару штук на полочке для реализации каких-то временных нужд типа елочной гирлянды на новый год.
                2)По поводу хаба — вы от него не отказались, вы его вынесли наружу, что не избавило от хаба как от сущности, но добавило +1 точку отказа — интернет. Тот же HomeAssistant в любой его ипостаси (малинка, виртуалка, разное) продолжил бы работать и без интернета, локально пользователь не заметит разницы.
                + умоляю, не забывайте про юзабилити. Считайте что эксплуатировать вашу систему будет склонный к насилию психопат, у которого есть ваш домашний адрес (старая цитата). Я бы явно начал нервничать если бы мне пришлось для включения света или для заварить кофе при отвале инета — коннектиться к сети, открывать браузер и идти на страничку нажимать кнопку. Вы уже умеете в MQTT, привяжите устройства к локальному брокеру или возьмите готовую систему если это не идет вразрез с принципами. Как второй пример — представьте что вам нужно чтобы это работало у людей старшего поколения. Попробуйте обучить бабушку 70+ лет пользованию и поймете что юзабилити в имеющемся виде немного не идеально.
                3) Про 100Вт я не перепутал а перестраховался. Я слишком хорошо знаю как горят компактные реле. Нет, оно не сдохнет если пропустить 8-10 ампер при номинале в 10. И даже завтра не сдохнет. Но сгорит или залипнет оно обязательно через 30 включений, через 50 или 100, даже контакторы имеют нормированную наработку. Тут уже выбор каждого, рисковать или сделать нормально. Проще говоря — нет, я не верю в ноунейм реле и номинал делю минимум на 10. То есть не 100Вт конечно а даже ватт 300-500 я готов там оставить «надолго» но не более. Если там будет оригинальный omron (что сильно вряд ли) — запас по току можно пересмотреть.
                  0
                  Понял вас, спасибо за советы.
                  Хочу спросить ещё немного в сторону реле, как относитесь к симисторам, хорошая ли замена(вроде как твердотельные реле построены на них), просто обычно в разных родах покупных WiFi розеток стоят именно реле на 10-16А?
                    0

                    Удивительно, что никто не обратил внимание на третий вариант (а он есть). Реле не может нормально работать с индуктивными нагрузками и вообще процесс коммутации для реле самый сложный. Зато во включенном состоянии — это идеальный контакт. С другой стороны, у включенного симистора всегда присутствует приличное падение при протекании тока. Его нельзя сильно нагружать без радиатора. Но у него есть замечательное свойство — можно сделать, чтобы он включался практически без тока (в нуле), а выключение (особенно индуктивных нагрузок) — вообще сказка. Он всегда выключается только тогда, когда ток через него прекратит течь. Поэтому третий вариант — запараллелить реле и симистор. Единственное — алгоритм включение\выключения здесь сложнее. Включение: "Включаем симистор — небольшая пауза — включаем реле". Выключение: "Выключаем реле — небольшая пауза (время размыкания контактов) — выключаем симистор". В этом случае мы получаем отсутствие коммутационных проблем для реле (симистор спасает контакты реле) и отсутствие нагрева во включенном состоянии (реле спасает симистор, шунтируя его). Кстати, симистор в данной схеме не требует радиатора (надеюсь объяснять не требуется почему) и реле можно взять попроще (не нужен запас на коммутацию). Именно так я делал, когда надо было коммутировать большие токи (16А) в малом объеме. Это было устройство, аналогичное Вашему, только я делал его лет 25 назад. А вообще для девятого класса это замечательно! Молодец!

                    0

                    Кратко:
                    Симистор лучше — отсутствием дребезга контактов.
                    Симистор хуже — наличием значительного падения напряжения (1,5-2 В при токе в 10 А дадут немалый нагрев) и возможностью ложных срабатываний (dV/dt).

              +1

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

              Печатный корпус не нравится по той же причине - pla неплохо горит.

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

                0
                Если я правильно понял, вы имеете ввиду модули линейного регулятора, а не esp?
                Просто проблема чип и дипа в том, что он чрезмерно дорогой(хотя перепроверил, сейчас в чип и дипе есть блоки питания примерно такой-же стоимости)…
                На счет pla понял, надо думать чем его заменять в таких устройствах(хотя это просто демонстративная модель)
                Спасибо за советы, попытаюсь тогда исправить эти недостатки
            0
            Спешу напомнить: автор поста заканчивает 9-й класс средней школы. Да, я смотрю на код и у меня чуть ли не кровь из глаз в некоторых местах, но и я в его годы писал не лучше (до сих пор где-то валяется код в одну строчку и без комментариев). Порадуйтесь, лучше, что парень взял, собрал и запрогал это всё. Некоторые его сверстники не могут дробь 2х/2 сократить (и я не шучу!)
              0

              А что с кодом нет так? Вроде код, как код. Бывает и хуже.

            0
            Я случайно отклонил комментарий, подумал что это кнопка ответить.
            У меня esp в корпусе. Датчик ds18b20 вынесен вообще на проводе из корпуса.
            С датчиком dht11 ситуация слегка похуже, он вклеен в корпус, но его рабочая часть вылазит, тут, наверное, могут быть некоторые погрешности. disk.yandex.ru/i/H-Xrj2RK9B6i8g фотография
              0

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

              0
              (удалено)
                0
                5VDC/5W
                если правильно понимаю, ток обмотки реле 1А, тут КТ315 не подойдет, у него макс ток коллектора 100мА
                  0
                  Я проверял через лбп потребление реле(если я правильно все понял) и получается в открытом состоянии у него ток 0,04А
                    0

                    Типичная мощность потребления таких реле — 100..400 мВт (20..80 мА при 5 В). "Старичок" 315 — вполне уместен.

                    0

                    P.S. Линейный стабилизатор (AMS1117) — не лучшый вариант для батарейных приборов.


                    • Какой получился ресурс батареек?
                    • У серии 1117 есть требование по мин. нагрузке. Не увеличивается ли напряжение на ESP сверх 3,3 В пока он в "спячке"?
                      0
                      на счет ресурса батареек…
                      Вышло не лучшим образом)
                      у меня уже сели, хотя пользовался довольно мало.
                      На счет AMS1117 почитаю и поищу замену тогда

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое