Очередной умный дом (или как потерять 2 месяца из-за одной глупой ошибки)
Предыстория
я, не знающий своей глупости и наивностиНачну я свою эпопею с небольшой предыстории. Надо было мне сделать проект для 9 класса (да, сейчас, чтобы тебя допустили до экзаменов, надо сделать проект и защитить его).
Решил практически сразу, что это будет умный дом (хотя даже воробей умнее его), ведь давно хотел сделать что-то такое, но руки так и не доходили, а тут подвернулся отличный случай + немного выпендриться можно.
Данный пост — мой переделанный диплом, да и вообще первая попытка в написании таких вещей)
План
Для успешной и продуктивной работы нужен план, для себя я составил его примерно таким:
Идея и идеология умного дома.
Железо для построения умного дома.
Код для каждой из частей умного дома.
Принципе, для начала это более чем достаточно. Теперь можно разобрать каждый пункт отдельно.
Идея и идеология умного дома
Посидев несколько дней и обдумав все идеи, в моей голове всплыла утопическая идея такой системы, которая не нуждалась бы в базовом модуле, а каждый такой модуль будет полностью автономен. У системы такого типа, в отличии от той, что использует в своей основе базовый модуль (примером может быть умный дом xiaomi), имеется несколько плюсов и минусов.
Начнем с плюсов. Главный плюс — отказоустойчивость, даже если один из управляющих модулей выйдет из строя, датчики все равно будут отправлять данные пользователю. Второй же плюс — возможность связи модулей на больших расстояниях.
А теперь про минусы: самый значительный минус — автономности, ведь на то, чтобы проснуться, подключиться к интернету, отправить данные и уснуть тратиться больше энергии, чем на то, чтобы передать данные по радиоканалу. Следующий минус состоит в необходимости наличия интернета для работы каждого датчика, иначе он просто не сможет отправить данные.
В итоге я составил список функционала, который умный дом должен реализовывать в ИДЕАЛЕ:
Автономные модули.
Взаимозаменяемость модулей.
Масштабируемость системы.
При работе, выяснилось что времени до дедлайна не так уж и много, так что я решил урезать свой список, оставив в нём только автономность и масштабируемость, хотя взаимозаменяемость добавляется в итоговый проект довольно просто.
И да, забыл сказать, модуль — единичное устройство системы умного дома, будь то wi-fi розетка или датчик температуры.
Для приемо-передачи данных я выбрал MQTT, довольно известный и удобный протокол передачи.
Микроконтроллер
Микроконтроллером, который является основой для каждого модуля умного дома, я выбрал всем хорошо знакомый, даже немного заезженный ESP-12F. Он хорош своими возможностями, в которые входит функция WI-FI (как соединение, так и точка доступа) и отличная производительность, которой с лихвой хватило на все мои задумки, а цена за штуку не превышает 100 рублей в Китае, если брать сразу 10 штук.
распиновка и внутреннее устройство esp12FВообще есть несколько версий esp12, самая первая — esp12Е, она по всем фронтам уступает более новому esp12F. Основные их отличия в форме антенны, у F-версии она сделана более удачно, и в размере Flash памяти: у E-версии в большинстве случаев она составляет 1МБ, в то время как у F-версии она уже 4МБ. Также, вроде как различия в внутренней компоновки компонентов, которая удачнее в F-версии. Еще можно вспомнить самую новую версию — esp12S, которая практически идентична (если я правильно разобрался) своей начинкой, но сделана в более компактном и удобном корпусе для smd пайки.
три версии 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 розеткиДля корпуса были куплены обычная розетка и вилка, которые впоследствии были уничтожены (разобраны на части), а основной корпус был напечатан на 3д принтере.
корпус в реальности и корпус в fusion360внутреннее строение модуляПечатная плата сделана ЛУТом, по рецепту Гайвера, только под утюгом лучше держать около 10 минут, а то краска некорректно перейдет на стеклотекстолит.
плата с стороны компонентовнеудачные платыКонечно, это не первая версия платы, да и вообще, это мой первый опыт в создании печатных плат самому, до неё было 5 неудачных попыток развести печатку, где-то не так отзеркалил, где-то не так развел, где-то плохо краска перенеслась и дорожки получились плохими.
Устройство модулей датчиков
Оба модулей датчиков имеют практически идентичное внутреннее устройство, различия только в самих датчиках, в одном это DHT11, а в другом DS18B20 и их обвязках.
Модули выполняют очень простую функцию: отправлять показания с датчиков по MQTTT.
принципиальная схема модуля для датчика DS18B20(для DHT11 отличие будет только в резисторе, который будет не 4.7К, а 10К)печатная плата для модуля датчика (зеленое на плате — проводки на другой стороне)В этот раз корпус был полностью напечатан на 3д принтере, ведь даже так, себестоимость корпусов дешевле, чем покупать уже готовые коробочки, которые придется дорабатывать!
корпус в реале и корпус в fusion 360корпус без крышкиПитание модуля идет от 3 батареек типа АА.
Прошивка модулей
Наверное самый интересный и самый долгий пункт…
Сначала я хотел использовать RTOS-SDK от ESP, но понял что уйдет много времени чтобы разобраться в нем, поэтому я остановился на Arduino, хоть и от FreeRTOS пришлось отказаться.
Но, в принципе, для моего решения RTOS не нужна.
Для прошивки можно использовать любой программатор, совместимый с esp, но главное не перепутать перемычки (если они есть конечно) логического уровня и не поставить их на 5в, иначе esp12 может попросту сгореть.
Подключал я по этой схеме… и вот как раз таки та загвоздка, из-за которой я и потратил 2 месяца в пустую…забыл минус микроконтроллера к минусу программатора соединить! Оказывается, не дурак эту схему начертил, а дурак её пытался повторить). Пока искал где проблема, переделал много плат, заказал новые микроконтроллеры, подумал что с ними проблема, новый программатор взял, перерыл форумы с этой проблемой…
Ну да ладно, главное что все заработало.
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 += "
\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 += "\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;
}
Само меню выглядит так:
Кнопка «Вернуться в режим настройки», как понятно из названия, отправляет модуль обратно в режим настройки.
Кнопка «Панель управления» открывает пользователю меню управления.
Кнопка «Панель управления» открывает пользователю меню автоматического управления розеткой.
Возвращение к меню управления происходит через 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 += "\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 += "
";
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