Умный дом, начало... esp,websoket
- Войдите на сайт для отправки комментариев
Добрый день! Хочу поделиться наработками по созданию системы управления устройствами, по типу умного дома. Создан простой протокой общения, между управляющим устройством(Контроллером) и подключаемыми клиентами. Количество котроллеров так и клиентов не ограничено. Реализована обратная связь, между котроллерами и клиентами, это основная идея проекта. На данный момент реализовано один тип устройств – relay(Реле). В примере показано упрощенное управление этим устройством. Алгоритм можно, и нужно адаптировать под свои нужны. Для запуска проекта минимально нужно 2 esp, одна в роли сервера обмена, вторая в роли клиента. Также можно использовать сторонний websoket-сервер(для примера Herokuapp.com на Node JS) для обмена. Контроллером выступает веб-страничка, ее можно загрузить на сервер обмена, а можно просто открыть с устройства. Таким образом проект состоит из 3 файлов: Скеч клиента, скеч сервера обмена(если есть необходимость), веб-страничка.
Клиент:
/* * WebSocketClient.ino * * Created on: 24.05.2015 * */ #include <ArduinoJson.h> #include <Arduino.h> #include <ESP8266WiFi.h> #include <WebSocketsClient.h> #include <Hash.h> #include <EEPROM.h> #include <WiFiClient.h> #include <ESP8266WebServer.h> ESP8266WebServer server(80); WebSocketsClient webSocket; #define USE_SERIAL Serial boolean start = true; long previousMillis = 60000; // храним время последнего переключения long interval = 1000*60*5; // интервал между проверкой доступности сервера (300 секунд) long interval_restart = 1000*60*60; // интервал между включение/выключением (60 минут) DynamicJsonBuffer jsonBuffer; String str = ""; String html_header = "<html>\ <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\ <head>\ <title>ESP8266 Settings</title>\ <style>\ body { background-color: #cccccc; font-family: Arial, Helvetica, Sans-Serif; Color: #000088; }\ </style>\ </head>"; void runCommand(String command) { String StatusPin = "[0,0,0,0,0,0,0,0]";//Пример для 8 пинов. String Name = String(WiFi.macAddress()); String Device = "\"Relay\"";//Тип устройства, в перспективе планируеться разширение устройств. boolean OK = false; //Пример управленыя 1 пином, для примера wemos d1 mini, pin - D4(gpio02) if (command == "STATUS"){ if(digitalRead(D4) == LOW ) StatusPin = "[1,1,1,0,0,0,1,0]"; else StatusPin = "[0,1,1,0,0,0,1,0]"; OK =true; } if (command == "ON_1"){ StatusPin = "[1,1,1,0,0,0,1,0]";//Включить digitalWrite(LED_BUILTIN, LOW); OK =true; } if (command == "OFF_1"){ StatusPin = "[0,1,1,0,0,0,1,0]";//Выключить OK =true; digitalWrite(LED_BUILTIN, HIGH); } //Для остальных ON_2,OFF_2,ON_3,OFF_3... и сколько пинов хватет //Если команда выполнена успешно if(OK) webSocket.sendTXT("{\"Name\":\""+Name+"\",\"Status\":"+StatusPin+",\"Device\":"+Device+"}"); else webSocket.sendTXT("{\"Name\":\""+Name+"\",\"Status\":"+"\"Not support command\""+"}"); } //Сдесь обработка собитиый, взята стандартная void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) { switch(type) { case WStype_DISCONNECTED: USE_SERIAL.printf("[WSc] Disconnected!\n"); break; case WStype_CONNECTED: { USE_SERIAL.printf("[WSc] Connected to url: %s\n", payload); runCommand("STATUS"); } break; case WStype_TEXT:{ String text = (char*) payload; USE_SERIAL.printf("[WSc] get text: %s\n", payload); JsonObject& root = jsonBuffer.parseObject(text); //Проверяем коррекность полученого сообщения if (!root.success()){ USE_SERIAL.println("Error message"); } else{//Обрабатует тоько полученые от контроллера, и только если адресованы нам if (root["Name"] == "Controller"){ if (root["Client"] == String(WiFi.macAddress()) || root["Client"] == "ALL") { String command = root["Command"]; runCommand(command); } } } } break; case WStype_BIN: USE_SERIAL.printf("[WSc] get binary length: %s\n", length); hexdump(payload, length); break; case WStype_ERROR: USE_SERIAL.printf("[WSc] Error connection: %s\n", payload); break; } } void setup() { pinMode(D7, INPUT); //Замкнуть на GND, перезапустить, вовдет в режым настройки параметров подключеныя pinMode(D4, OUTPUT); //Пин для управленыя digitalWrite(LED_BUILTIN, HIGH); int buttonState = 0; buttonState = digitalRead(D7); byte len_ssid, len_pass, len_server, len_port ; EEPROM.begin(98); delay(1000); Serial.begin(115200); Serial.println("start"); //Еслы D7 замкныта на GND заходив в режым настройки параметров if (buttonState == HIGH) { len_server = EEPROM.read(94); len_port = EEPROM.read(95); len_ssid = EEPROM.read(96); len_pass = EEPROM.read(97); // Режим STATION WiFi.mode( WIFI_STA); unsigned char* buf_ssid = new unsigned char[32]; unsigned char* buf_pass = new unsigned char[48]; unsigned char* buf_server = new unsigned char[64]; unsigned char* buf_port = new unsigned char[80]; for(byte i = 0; i < len_ssid; i++) buf_ssid[i] = char(EEPROM.read(i)); buf_ssid[len_ssid] = '\x0'; const char *ssid = (const char*)buf_ssid; for(byte i = 0; i < len_pass; i++) buf_pass[i] = char(EEPROM.read(i + 32)); const char *pass = (const char*)buf_pass; buf_pass[len_pass] = '\x0'; for(byte i = 0; i < len_server; i++) buf_server[i] = char(EEPROM.read(i + 48)); const char *server_ = (const char*)buf_server; buf_server[len_server] = '\x0'; for(byte i = 0; i < len_port; i++) buf_port[i] = char(EEPROM.read(i + 80)); const char *port_ = (const char*)buf_port; buf_port[len_port] = '\x0'; Serial.print("SSID: "); Serial.print(ssid); Serial.print(" "); Serial.print("Password: "); Serial.print(pass); Serial.print(" "); Serial.print("Server ws: "); Serial.print(server_); Serial.print(" "); Serial.print("Port ws: "); Serial.println(port_); WiFi.begin(ssid, pass); // Wait for connection int start = 0; while ( WiFi.status() != WL_CONNECTED ) { delay ( 500 ); Serial.print ( "." ); start = start +1; if(start > 100 ) { //Ну удалось подключиться по указаным настройкам, переходим в режым настройки параметров start_server(); break; } } //Если все удачно, подключаемся у серверу if(WiFi.status() == WL_CONNECTED) { Serial.println(); Serial.print("Connected to "); Serial.println(ssid); Serial.print("IP address: "); Serial.println(WiFi.localIP()); //--------------------------- String port_ws; port_ws = String(port_); int port_ws_ = port_ws.toInt(); webSocket.begin(server_, port_ws_, "/"); webSocket.onEvent(webSocketEvent); // webSocket.setAuthorization("user", "Password"); webSocket.setReconnectInterval(5000); } } else {start_server();} } void loop() { webSocket.loop(); server.handleClient(); // здесь будет код, который будет работать постоянно // и который не должен останавливаться на время между переключениями unsigned long currentMillis = millis(); //проверяем не прошел ли нужный интервал, если прошел то if(currentMillis - previousMillis > interval) { // сохраняем время последнего переключения previousMillis = currentMillis; runCommand("STATUS"); } if(currentMillis > interval_restart) { //Перезапускаем ESP ESP.restart(); } } //Форма для ввода параметров void handleRoot() { String str = ""; str += html_header; str += "<body>\ <form method=\"POST\" action=\"ok\">\ <input name=\"ssid\"> WIFI Net</br>\ <input name=\"pswd\"> Password</br></br>\ <input name=\"server_ws\"> Server ws</br></br>\ <input name=\"port_ws\"> Port ws</br></br>\ <input type=SUBMIT value=\"Save settings\">\ </form>\ </body>\ </html>"; server.send ( 200, "text/html", str ); } //Обработка полученых параметров void handleOk(){ String ssid_ap; String pass_ap; String server_ws; String port_ws; unsigned char* buf = new unsigned char[64]; String str = ""; str += html_header; str += "<body>"; EEPROM.begin(98); ssid_ap = server.arg(0); pass_ap = server.arg(1); server_ws = server.arg(2); port_ws = server.arg(3); if(ssid_ap != ""){ EEPROM.write(94,server_ws.length()); EEPROM.write(95,port_ws.length()); EEPROM.write(96,ssid_ap.length()); EEPROM.write(97,pass_ap.length()); ssid_ap.getBytes(buf, ssid_ap.length() + 1); for(byte i = 0; i < ssid_ap.length(); i++) EEPROM.write(i, buf[i]); pass_ap.getBytes(buf, pass_ap.length() + 1); for(byte i = 0; i < pass_ap.length(); i++) EEPROM.write(i + 32, buf[i]); server_ws.getBytes(buf, server_ws.length() + 1); for(byte i = 0; i < server_ws.length(); i++) EEPROM.write(i + 48, buf[i]); port_ws.getBytes(buf, port_ws.length() + 1); for(byte i = 0; i < port_ws.length(); i++) EEPROM.write(i + 80, buf[i]); EEPROM.commit(); EEPROM.end(); str +="Configuration saved in FLASH</br>\ Changes applied after reboot</p></br></br>\ <a href=\"/\">Return</a> to settings page</br>"; } else { str += "No WIFI Net</br>\ <a href=\"/\">Return</a> to settings page</br>"; } str += "</body></html>"; server.send ( 200, "text/html", str ); } //Запуск настройки параметров void start_server() { const char *ssid_ap = "ESPap"; WiFi.mode(WIFI_AP); Serial.print("Configuring access point..."); /* You can remove the password parameter if you want the AP to be open. */ WiFi.softAP(ssid_ap); delay(2000); Serial.println("done"); IPAddress myIP = WiFi.softAPIP(); Serial.print("AP IP address: "); Serial.println(myIP); server.on("/", handleRoot); server.on("/ok", handleOk); server.begin(); Serial.println("HTTP server started"); }Сервер обмена:
#include <ESP8266WiFi.h> #include <WiFiClient.h> #include <ESP8266WebServer.h> #include <EEPROM.h> #include <WebSocketsServer.h> #include <Hash.h> #include <FS.h> String str = ""; boolean conf = false; long previousMillis = 1000; // храним время последнего переключения long interval = 1000; // интервал между миганием byte ledState = LOW; String html_header = "<html>\ <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\ <head>\ <title>ESP8266 Settings</title>\ <style>\ body { background-color: #cccccc; font-family: Arial, Helvetica, Sans-Serif; Color: #000088; }\ </style>\ </head>"; ESP8266WebServer server(80); WebSocketsServer webSocket = WebSocketsServer(81); void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) { switch(type) { case WStype_DISCONNECTED: Serial.printf("[%u] Disconnected!\n", num); break; case WStype_CONNECTED: { IPAddress ip = webSocket.remoteIP(num); Serial.printf("[%u] Connected from %d.%d.%d.%d url: %s\n", num, ip[0], ip[1], ip[2], ip[3], payload); } break; case WStype_TEXT: Serial.printf("[%u] get Text: %s\n", num, payload); webSocket.broadcastTXT(payload); break; case WStype_BIN: Serial.printf("[%u] get binary length: %u\n", num, length); hexdump(payload, length); break; } } void setup(void) { pinMode(LED_BUILTIN, OUTPUT); // Initialize the LED_BUILTIN pin as an output byte len_ssid, len_pass; delay(1000); Serial.begin(115200); Serial.println("start"); EEPROM.begin(98); len_ssid = EEPROM.read(96); len_pass = EEPROM.read(97); if(len_pass > 64) len_pass = 0; if((len_ssid < 33) && (len_ssid != 0)){ // Режим STATION WiFi.mode( WIFI_STA); unsigned char* buf_ssid = new unsigned char[32]; unsigned char* buf_pass = new unsigned char[64]; for(byte i = 0; i < len_ssid; i++) buf_ssid[i] = char(EEPROM.read(i)); buf_ssid[len_ssid] = '\x0'; const char *ssid = (const char*)buf_ssid; for(byte i = 0; i < len_pass; i++) buf_pass[i] = char(EEPROM.read(i + 32)); const char *pass = (const char*)buf_pass; buf_pass[len_pass] = '\x0'; Serial.print("SSID: "); Serial.print(ssid); Serial.print(" "); Serial.print("Password: "); Serial.println(pass); WiFi.begin(ssid, pass); // Wait for connection int start = 0; while ( WiFi.status() != WL_CONNECTED ) { delay ( 500 ); Serial.print ( "." ); start = start +1; if(start > 100 ) { start_server(); break; } } if(WiFi.status() == WL_CONNECTED) { SPIFFS.begin(); Serial.println(); Serial.print("Connected to "); Serial.println(ssid); Serial.print("IP address: "); Serial.println(WiFi.localIP()); //--------------------------- WiFi.softAP("ESP-Server", ""); delay(2000); IPAddress myIP = WiFi.softAPIP(); Serial.print("AP IP address: "); Serial.println(myIP); server.on("/", serveIndexFile); server.on("/status", handleRootStatus); server.begin(); Serial.println("HTTP server started"); } } else // Режим SoftAP { start_server(); } webSocket.begin(); webSocket.onEvent(webSocketEvent); Serial.println("stop"); } void loop() { // здесь будет код, который будет работать постоянно // и который не должен останавливаться на время между переключениями свето unsigned long currentMillis = millis(); //проверяем не прошел ли нужный интервал, если прошел то if(currentMillis - previousMillis > interval) { // сохраняем время последнего переключения previousMillis = currentMillis; // если светодиод не горит, то зажигаем, и наоборот if (ledState == LOW) ledState = HIGH; // Note that this switches the LED *off* else ledState = LOW; // Note that this switches the LED *on* digitalWrite(LED_BUILTIN, ledState); // устанавливаем состояния выхода, чтобы включить или выключить светодиод } server.handleClient(); webSocket.loop(); } void handleRootStatus() { String str = ""; String str_ip = ""; IPAddress ip = WiFi.localIP(); str_ip = String(ip[0])+"." +String(ip[1])+"."+String(ip[2])+"."+String(ip[3]); str += html_header; str += "<body>\ <H1>Server Smart House</H1>\ <p>ip -"+str_ip+ "</body>\ </html>"; server.send ( 200, "text/html", str ); } void handleRoot() { String str = ""; str += html_header; str += "<body>\ <form method=\"POST\" action=\"ok\">\ <input name=\"ssid\"> WIFI Net</br>\ <input name=\"pswd\"> Password</br></br>\ <input type=SUBMIT value=\"Save settings\">\ </form>\ </body>\ </html>"; server.send ( 200, "text/html", str ); } void handleOk(){ String ssid_ap; String pass_ap; unsigned char* buf = new unsigned char[64]; String str = ""; str += html_header; str += "<body>"; EEPROM.begin(98); ssid_ap = server.arg(0); pass_ap = server.arg(1); if(ssid_ap != ""){ EEPROM.write(96,ssid_ap.length()); EEPROM.write(97,pass_ap.length()); ssid_ap.getBytes(buf, ssid_ap.length() + 1); for(byte i = 0; i < ssid_ap.length(); i++) EEPROM.write(i, buf[i]); pass_ap.getBytes(buf, pass_ap.length() + 1); for(byte i = 0; i < pass_ap.length(); i++) EEPROM.write(i + 32, buf[i]); EEPROM.commit(); EEPROM.end(); str +="Configuration saved in FLASH</br>\ Changes applied after reboot</p></br></br>\ <a href=\"/\">Return</a> to settings page</br>"; } else { str += "No WIFI Net</br>\ <a href=\"/\">Return</a> to settings page</br>"; } str += "</body></html>"; server.send ( 200, "text/html", str ); } void start_server() { const char *ssid_ap = "ESPap"; WiFi.mode(WIFI_AP); Serial.print("Configuring access point..."); /* You can remove the password parameter if you want the AP to be open. */ WiFi.softAP(ssid_ap); delay(2000); Serial.println("done"); IPAddress myIP = WiFi.softAPIP(); Serial.print("AP IP address: "); Serial.println(myIP); server.on("/", handleRoot); server.on("/ok", handleOk); server.begin(); Serial.println("HTTP server started"); } void serveIndexFile() { File file = SPIFFS.open("/index.html","r"); server.streamFile(file, "text/html"); file.close(); }Контроллер:
<!DOCTYPE html> <meta charset="utf-8" /> <title>WebSocket Test</title> <head> <style type="text/css"> .checkbox input { position: absolute; z-index: -1; opacity: 0; margin: 10px 0 0 20px; } .checkbox__text { position: relative; padding: 0 0 0 60px; cursor: pointer; } .checkbox__text:before { content: ''; position: absolute; top: -4px; left: 0; width: 50px; height: 26px; border-radius: 13px; background: #CDD1DA; box-shadow: inset 0 2px 3px rgba(0,0,0,.2); transition: .2s; } .checkbox__text:after { content: ''; position: absolute; top: -2px; left: 2px; width: 22px; height: 22px; border-radius: 10px; background: #FFF; box-shadow: 0 2px 5px rgba(0,0,0,.3); transition: .2s; } .checkbox input:checked + .checkbox__text:before { background: #9FD468; } .checkbox input:checked + .checkbox__text:after { left: 26px; } .checkbox input:focus + .checkbox__text:before { box-shadow: inset 0 2px 3px rgba(0,0,0,.2), 0 0 0 3px rgba(255,255,0,.7); } </style> </head> <h2>Contol esp-12e</h2> <input type="text" id="wsurl"> <button id="connect"> Connect </button> <br> <input type="checkbox" id="AutoUpdate"> Autoupdate <input type="checkbox" id="Log" checked = 1> Syslog <div id="Controllers"></div> <!-- <label class="checkbox"> <input type="checkbox" id="LED1"/> <div class="checkbox__text">Led_1</div> </label> <br> <label class="checkbox"> <input type="checkbox" id="LED2"/> <div class="checkbox__text">Led_2</div> </label> --> <!-- <button id="button_ON_LED1">ON LED_1</button> <button id="button_OFF_LED1">OFF LED_1</button> <button id="button_ON_LED2">ON LED_2</button> <button id="button_OFF_LED2">OFF LED_2</button> --> <div id="output"></div> <script language="javascript" type="text/javascript"> //var wsUri = "wss://echo.websocket.org/"; var wsUri = ""; var output; var interval; var clientArray = new Array(); var debug = true;//Через консоль задать true, будет выден лог РѕР±РСена function init() { output = document.getElementById("output"); initWebSocket(); } function initWebSocket() { websocket = new WebSocket(wsUri); websocket.onopen = function(evt) { onOpen(evt) }; websocket.onclose = function(evt) { onClose(evt) }; websocket.onmessage = function(evt) { onMessage(evt) }; websocket.onerror = function(evt) { onError(evt) }; } function onOpen(evt) { writeToScreen("CONNECTED"); doSend(JSON.stringify({"Name":"Controller","Command":"STATUS","Client":"ALL"})); } function onClose(evt) { writeToScreen("DISCONNECTED");//heroku.com setTimeout(initWebSocket,5000); } function onMessage(evt) { var JSONcommand; try { JSONcommand = JSON.parse(evt.data); } catch (e) { writeToScreen('<span style="color: blue;">RESPONSE: ' + evt.data+'</span>'); return; } if( (JSONcommand.Name != "Controller") ) { var res = clientArray.find(function(e){return e == JSONcommand.Name }); if(res == undefined){// clientArray.push(JSONcommand.Name); console.log("add new client"); if (JSONcommand.Device == "Relay") { if (Array.isArray(JSONcommand.Status)) { AddCheckbox(JSONcommand.Name, JSONcommand.Status.length); UpdateStatusRelay(JSONcommand.Name, JSONcommand.Status); } } } else// { if (JSONcommand.Device == "Relay") { UpdateStatusRelay(JSONcommand.Name,JSONcommand.Status); } } } if(debug = 1) writeToScreen('<span style="color: blue;">RESPONSE: ' + evt.data+'</span>'); } function onError(evt) { writeToScreen('<span style="color: red;">ERROR:</span> ' + evt.data); } function doSend(message) { writeToScreen("SENT: " + message); websocket.send(message); } function writeToScreen(message) { if(debug){ var pre = document.createElement("p"); pre.style.wordWrap = "break-word"; pre.innerHTML = message; output.appendChild(pre); } } window.addEventListener("load", function () { var serverhouse = localStorage.getItem("ServerHouse"); if (serverhouse != null) { wsUri = serverhouse; document.getElementById("wsurl").value = wsUri; init(); } }, false); function AddCheckbox(comment,pin) {//Для Relay var root = document.getElementById("Controllers"); var pre = document.createElement("div"); pre.innerHTML = "<H2>"+comment+"</H2>"; root.appendChild(pre); root = pre; var a = 1; while (a <= pin ){ pre = document.createElement("p"); root.appendChild(pre); pre = document.createElement("label"); pre.className = "checkbox"; root.appendChild(pre); root = pre; pre = document.createElement("input"); pre.type = "checkbox"; pre.id = comment+"_pin_"+a; pre.i = a; pre.addEventListener("change",function () { if(this.checked) { doSend(JSON.stringify({"Name":"Controller","Command":"ON_"+this.i,"Client":comment})); } else { doSend(JSON.stringify({"Name":"Controller","Command":"OFF_"+this.i,"Client":comment})); } }); root.appendChild(pre); pre = document.createElement("div"); pre.className = "checkbox__text"; pre.innerHTML = "Led "+a; root.appendChild(pre); root = document.getElementById("Controllers"); pre = document.createElement("br"); root.appendChild(pre); a++; } } function UpdateStatusRelay(comment,pin) { if(Array.isArray(pin)) { for(var i = 0;i <= pin.length-1;i++) { document.getElementById(comment+"_pin_"+(i+1)).checked = pin[i]; } } } document.getElementById("Log").addEventListener("change",function () { if(this.checked) debug = true; else debug = false; }) document.getElementById("AutoUpdate").addEventListener("change",function () { if(this.checked) { interval = setInterval(function () { var root = document.getElementById("Controllers"); while (root.firstChild) { root.removeChild(root.firstChild); } clientArray = []; doSend(JSON.stringify({"Name":"Controller","Command":"STATUS","Client":"ALL"})); },60*1000);//Раз РІ минуту проверяем клиентов } else { clearInterval(interval); } }) document.getElementById("connect").addEventListener("click",function () { wsUri = document.getElementById("wsurl").value; localStorage.setItem("ServerHouse",wsUri); init(); }) </script>Поиграться устройстовм можно тут:
https://test-websoket.herokuapp.com
Строка подключения:
На Herokuapp.com - wss://test-websoket.herokuapp.com
Если 2 есп, ws://192.168.0.4:81/, или ws://"адрес который выдал роутер":81/
Чтобы узнать адрес ,который выдал роутер, можно подключиться к точке "ESP-Server", перейти по адресу http://192.168.4.1/status
Слишком сложно для местных =))
Это еще я упростил, зараз клиенте реализовано: