Arduino je šikovná embeeded platforma, ktorú je možné využiť napríklad na stavbu izbového termostatu s Ethernet konektivitou a webserverom implementovaným na Arduine. Implementácia využíva Arduino Uno / Nano s čipom AVR ATmega328P v kombinácii s Ethernet shieldom Wiznet W5100 / W5500 s ktorým komunikuje cez SPI zbernicu, resp. ICSP header. Programovo funguje termostat v režime webservera, kde dokáže prijímať požiadavky (requesty) od klientov v sieti po HTTP protokole a odosielať na ne odpoveď (response) - HTML / JSON kód a vykonávať pri ich spustení funkcie backendu (vykonanie scriptu, ovládanie GPIO, zápis do pamäte, funkčnosť...). Termostat je prístupný z LAN siete v ktorej sa nachádza, pričom je vybavený webovým rozhraním ktoré slúži na konfiguráciu všetkých prvkov termostatu, t.j. cieľovej (referenčnej) teploty a hysterézy. Webserver umožňuje beh niekoľkých na sebe nezávislých HTML stránok, ktoré môžu mať informatívny (textový výstup), alebo aj funkcionálny charakter (implementovaný backend s funkciami termostatu). Webserver beží na štandardnom HTTP porte - 80.
Termostat dokáže automatizovane prostredníctvom GPIO výstupu ovládať signalizačné relé pre zapnutie / vypnutie kotla. Dokáže tak nahradiť existujúci izbový termostat a sprístupniť ho v sieti klientom. Termostat dokáže obsluhovať akékoľvek zariadenie s prehliadačom - počítač / smartfón / tablet / Smart TV a podobne. Ako rozhodovací algoritmus sa využíva cieľová teplota s hysterézou, ktorá sa porovnáva s nameranou teplotou z digitálneho senzora teploty Dallas DS18B20. Cieľová teplota a hysteréza je načítaná z EEPROM pamäte, kde je uchovaná aj v prípade výpadku napájania permanentne a pri zápise nových dát sa prepíše. Rozlíšenie senzora DS18B20 pri meraní je 12-bitov, čomu rozpovedá rozlíšenie na teploty 0.0625 °C, čo je minimálny rozlišovací krok medzi rozlišnými meraniami. Dáta po OneWire zbernici môžu prísť do mikrokontroléru po vyžiadaní za 500 až 1000 ms v závislosti od počtu senzorov na OneWire zbernici, dĺžke zbernice a podobne... Rozhodovacia logika termostatu sa vykonáva každých 10 sekúnd nezávisle na webaplikácii, nevyžaduje sa keep-alive spojenie pre vykonanie logiky, systém tak funguje autonómne a nevyžaduje pozornosť používateľa.
Po hardvérovej stránke projekt využíva:HTML stránky bežiace na Arduine:
Webové rozhranie je navrhnuté pre prispôsobenie sa väčším i menším obrazovkám. Je reponzívne, podporuje širokouhlé obrazovky s vysokým rozlíšením, ale aj mobilné zariadenia. Rozhranie využíva importované CSS štýly Bootstrap frameworku z externého CDN servera, ktorý načíta client-side zariadenie pri otvorení stránky bežiacej na Arduine. Nakoľko je Arduino Uno limitované pamäťou, dokáže spustiť iba stránky s veľkosťou pár kB. Importovaním CSS štýlov z externého servera umožní žnížiť výkonové a pamäťové zaťaženie Arduina. Programová implementácia (pri Arduine Uno) využíva cca 70% flash pamäte (32kB - 4kB Bootloader) a 44% RAM pamäte (2kB). Statické časti webovej stránky (hlavička a pätička HTML dokumentu, linkovanie Bootstrap CSS, meta tagy, HTTP response hlavička, Content Type, formulár a ďalšie) sú uložené priamo vo flash pamäti Arduina, čo dokáže výrazne redukovať veľkosť používanej RAM pamäte pre obsah generovaný používateľovi. Webserver je tak stabilnejší a zvláda aj multi-pripojenie viacerých zariadení v sieti súčasne. Spotreba celého termostatu je na úrovni do 200 mA pri 5V napájaní - pod 1W.
Aby ostali nastavené hodnoty zachované aj po výpadku napájania, sú uložené do EEPROM pamäte Arduina (celkom k dispozícii 512 B). Referenčná teplota je zapísaná na offset 10, hysteréza na offset 100. Každá z hodnôt zaberá maximálne 5B v EEPROM pamäti + ukončovací znak - terminátor. Limit prepisov EEPROM je na úrovni 100-tisíc prepisov. Dáta sa prepisujú iba pri odoslaní HTML formulára. Prevádzka termostatu je tak maximálne šetrná k EEPROM pamäti s cieľom maximalizovať jej životnosť. V prípade, že zariadenie pri prvom spustení nemá nič uložené na spomenutých EEPROM offsetoch, vykoná sa automatický zápis s predvolenými hodnotami - referencia: 20.25 °C, hysteréza 0.25 °C, tzv. fail-safe riešenie, aby bol termostat ihneď funkčný a pripravený k prevádzke.
Prostredníctvom meta tagu Refresh vykonáva webserver obnovu celej stránky každých 30 sekúnd a prostredníctvom Javascriptu sa vypisuje do HTML stránky aj orientačný čas do refreshu. Do tohto času je potrebné stihnúť zapísať zmenu pre termostat, inak sa input okná pre číselné vstupy do formulára resetujú pri obnovení stránky. Na základe spätnej väzby od používateľov Android zariadení bol čas pre Refresh predĺžený z 10 na 30 sekúnd. Nakoľko built-in knižnica Ethernet neobsahuje využitie asynchrónneho webservera (ktorý je možné využiť napríklad u mikrokontrolérov Espressif ESP8266 / ESP32), je nutné prepisovať celú stránku. Dynamický údaj, ktorý sa predovšetkým mení je aktuálna hodnota výstupu - Zapnutý / Vypnutý, ktorý informuje prevádzkovateľa o skutočnom stave výstupu spoločne aj s farebným označením. Nakoľko sa logika systému vykonáva nezávisle na webserveri, do refreshu môže už byť výstup v inom stave, ako aktuálne vypísanom vo webaplikácii. Zmena výstupu je ihneď vypísaná napríklad na UART monitor (115200 baud/s). Na webovej stránke termostatu nájde používateľ aj informácie o uptime zariadenia (ako dlho beží), t.j. čas v dňoch, hodinách, minútach a sekundách.
Termostat vykuruje od nameranej teploty 22.49 °C a nižšej. V prípade dosiahnutia teploty 23.01 °C a vyššej sa výstup vypne, signalizačné relé sa rozpojí a plynový kotol prestáva kúriť. Nastáva dobeh vykurovania a chladnutie miestnosti v ktorej sa vykonávajú merania. Termostat sa opäť aktivuje až pri dosiahnutí teploty 22.49 °C, alebo nižšej.
Názov knižnice | Funkcia knižnice | Stiahnuť |
---|---|---|
Dallas |
Knižnica pre AVR mikrokontroléry (ATmega) Arduino Uno / Nano / Mega. Umožňuje komunikáciu so senzorom Dallas DS18B20 na OneWire zbernici. Možnosť komunikácie po normálnom, alebo parazitnom zapojení. |
Stiahnuť |
/*|----------------------------------------------------------|*/ /*|HTTP webserver - FORM - HTML - PROCESSING - EEPROM |*/ /*|AUTHOR: Martin Chlebovec |*/ /*|EMAIL: martinius96@gmail.com |*/ /*|----------------------------------------------------------|*/ #include <avr\wdt.h> #include <SPI.h> #include <Ethernet.h> #include <EEPROM.h> #include <OneWire.h> #include <DallasTemperature.h> #define ONE_WIRE_BUS 5 OneWire oneWire(ONE_WIRE_BUS); DallasTemperature sensorsA(&oneWire); const int rele = 6; byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; //physical mac address byte ip[] = { 192, 168, 4, 1 }; EthernetServer server(80); //server port const char terminator1[2] = " "; const char terminator3[2] = "?"; const char terminator4[2] = "&"; const char terminator5[2] = "="; unsigned long cas = 0; long day = 86400000; // 86400000 milliseconds in a day long hour = 3600000; // 3600000 milliseconds in an hour long minute = 60000; // 60000 milliseconds in a minute long second = 1000; // 1000 milliseconds in a second float teplota; String stav; boolean isFloat(String tString) { String tBuf; boolean decPt = false; if (tString.charAt(0) == '+' || tString.charAt(0) == '-') tBuf = &tString[1]; else tBuf = tString; for (int x = 0; x < tBuf.length(); x++) { if (tBuf.charAt(x) == '.') { if (decPt) return false; else decPt = true; } else if (tBuf.charAt(x) < '0' || tBuf.charAt(x) > '9') return false; } return true; } void writeString(char add, String data) { int _size = data.length(); int i; for (i = 0; i < _size; i++) { EEPROM.write(add + i, data[i]); } EEPROM.write(add + _size, '\0'); //Add termination null character for String Data } String read_String(char add) { int i; char data[100]; //Max 100 Bytes int len = 0; unsigned char k; k = EEPROM.read(add); while (k != '\0' && len < 500) //Read until null character { k = EEPROM.read(add + len); data[len] = k; len++; } data[len] = '\0'; return String(data); } void setup() { String a = read_String(10); String b = read_String(100); if (a == "" || a == NULL) { writeString(100, "0.25"); } if (b == "" || b == NULL) { writeString(10, "20.25"); } sensorsA.begin(); pinMode(rele, OUTPUT); digitalWrite(rele, HIGH); Serial.begin(115200); sensorsA.requestTemperatures(); delay(2000); //Ethernet.begin(mac); //DHCP Ethernet.begin(mac, ip); //STATIC IP server.begin(); Serial.println(F("Webaplikaciu vytvoril: Martin Chlebovec")); Serial.println(F("Build 1.0.4 z 18. Sep 2021")); Serial.println(F("Ethernet shield na IP:")); Serial.println(Ethernet.localIP()); wdt_enable(WDTO_8S); } void loop() { wdt_reset(); if ((millis() - cas) >= 10000 || cas == 0) { //Ethernet.maintain(); //odkomentovat iba ak sa pouziva Ethernet.begin(mac); cas = millis(); Serial.println(F("Ethernet shield na IP:")); Serial.println(Ethernet.localIP()); teplota = sensorsA.getTempCByIndex(0); String referencia = read_String(10); String hystereza = read_String(100); float referencia_teplota = referencia.toFloat(); float hystereza_teplota = hystereza.toFloat(); float minus_hystereza_teplota = (-1 * hystereza_teplota); float rozdiel = referencia_teplota - teplota; if (rozdiel > hystereza_teplota) { Serial.println(F("Kotol zapnuty")); stav = "ZAP"; digitalWrite(rele, LOW); } else if (rozdiel < minus_hystereza_teplota) { Serial.println(F("Kotol vypnuty")); stav = "VYP"; digitalWrite(rele, HIGH); } else { Serial.println(F("Rozdiel cielovej a aktuálnej teploty nie je nad, ani pod hysterezou. Stav vystupu sa nemeni.")); Serial.print(F("Aktualny stav vystupu pre kotol: ")); Serial.println(stav); } sensorsA.requestTemperatures(); } EthernetClient client = server.available(); if (client) { while (client.connected()) { if (client.available()) { String line = client.readStringUntil('\n'); char str[line.length() + 1]; line.toCharArray(str, line.length()); char *method; char *request; method = strtok(str, terminator1); request = strtok(NULL, terminator1); if (String(request) == "/") { int days = millis() / day ; //number of days unsigned int hours = (millis() % day) / hour; //the remainder from days division (in milliseconds) divided by hours, this gives the full hours unsigned int minutes = ((millis() % day) % hour) / minute ; //and so on... unsigned int seconds = (((millis() % day) % hour) % minute) / second; //HLAVNA ROOT HTTP STRANKA client.println(F("HTTP/1.1 200 OK")); client.println(F("Content-Type: text/html")); client.println(); client.println(F("<!DOCTYPE html>")); client.println(F("<html>")); client.println(F("<head>")); client.println(F("<script type='text/javascript'>")); client.println(F("window.smartlook||(function(d) {")); client.println(F("var o=smartlook=function(){ o.api.push(arguments)},h=d.getElementsByTagName('head')[0];")); client.println(F("var c=d.createElement('script');o.api=new Array();c.async=true;c.type='text/javascript';")); client.println(F("c.charset='utf-8';c.src='https://rec.smartlook.com/recorder.js';h.appendChild(c);")); client.println(F("})(document);")); client.println(F("smartlook('init', '6beae97f98b9844b761672af23f38fc60b962338');")); client.println(F("</script>")); client.println(F("<meta charset='utf-8'>")); client.println(F("<meta name='author' content='Martin Chlebovec'>")); client.println(F("<meta http-equiv='Refresh' content='30'; />")); client.println(F("<meta name='viewport' content='width=device-width, initial-scale=1'>")); client.println(F("<link rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css'>")); client.println(F("<script type='text/javascript'>")); client.println(F("var timeleft = 30;")); client.println(F("var downloadTimer = setInterval(function(){")); client.println(F("if(timeleft <= 0){")); client.println(F("clearInterval(downloadTimer);")); client.println(F("document.getElementById(\"countdown\").innerHTML = \"Refresh...\";")); client.println(F("} else {")); client.println(F("document.getElementById(\"countdown\").innerHTML = timeleft + \" sekúnd do refreshu\";")); client.println(F("}")); client.println(F("timeleft -= 1;")); client.println(F("}, 1000);")); client.println(F("</script>")); client.println(F("<title>HTTP webserver - Arduino + Ethernet</title>")); client.println(F("</head>")); client.println(F("<body>")); client.println(F("<center><h3>Zadajte dáta pre webserver (budú uložené do EEPROM):</h3>")); client.println(F("<form action='/action.html' method='get'>")); client.println("<b>Referenčná teplota:</b><br><input type='text' id='fname' name='fname' value=" + read_String(10) + "><br>"); client.println("<b>Hysteréza:</b><br><input type='text' id='fname2' name='fname2' value=" + read_String(100) + "><br>"); client.println(F("<input type='submit' class='btn btn-success' value='Zapísať'>")); client.println(F("</form><hr>")); if (stav == "ZAP") { client.println(F("<b><font color='green'>Výstup: Zapnutý</font></b>")); } if (stav == "VYP") { client.println(F("<b><font color='red'>Výstup: Vypnutý</font></b>")); } client.println(F("<div id=\"countdown\"></div>")); client.print(F("<b>Aktuálna teplota senzora DS18B20:</b> ")); client.print(teplota); client.println(F(" °C")); client.print(F("<hr>")); client.print(F("<b>Uptime: </b>")); client.print(days); client.print(F("d")); client.print(F(" ")); client.print(hours); client.print(F("h")); client.print(F(" ")); client.print(minutes); client.print(F("m")); client.print(F(" ")); client.print(seconds); client.print(F("s")); client.println(F("<h3>Autor: Martin Chlebovec - martinius96@gmail.com - https://martinius96.github.io/termostat-ethernet/</h3>")); client.println(F("<h4>Verzia free - 1.0.4 build: 18. Sep. 2021</h4>")); client.println(F("</center>")); client.println(F("</body>")); client.println(F("</html>")); delay(1); client.stop(); client.flush(); } else if (String(request) == "/get_data.json") { //PODSTRANKA PRE VYCITANIE DAT (INYM MIKROKONTROLEROM) client.println(F("HTTP/1.1 200 OK")); client.println(F("Content-Type: application/json")); client.println(); client.println(F("{")); client.print(F("\"Hysteresis\":")); client.print(read_String(100)); client.println(F(",")); client.print(F("\"Target_Temperature\":")); client.print(read_String(10)); client.println(F(",")); client.print(F("\"Actual_Temperature\":")); client.println(String(teplota)); client.println(F("}")); delay(1); client.stop(); client.flush(); } else if (String(request) == "/favicon.ico") { //fix chybajuceho faviconu client.stop(); } else { String myString = String(request); if (myString.startsWith("/action.html")) { char* parameter; char* value; char* hodnota1; char* hodnota2; parameter = strtok(request, terminator3); Serial.println(parameter); value = strtok(NULL, terminator3); hodnota1 = strtok(value, terminator4); hodnota2 = strtok(NULL, terminator4); char* H_1; char* H_2; strtok(hodnota1, terminator5); H_1 = strtok(NULL, terminator5); strtok(hodnota2, terminator5); H_2 = strtok(NULL, terminator5); String first_param = String(H_1); String second_param = String(H_2); if (isFloat(first_param)) { writeString(10, String(H_1)); } else { Serial.println(F("Pouzivatelsky vstup pre kluc fname (Cielova teplota) nie je cislo!")); } if (isFloat(second_param)) { writeString(100, String(H_2)); } else { Serial.println(F("Pouzivatelsky vstup pre kluc fname2 (Hystereza) nie je cislo!")); } client.println(F("HTTP/1.1 200 OK")); client.println(F("Content-Type: text/html")); client.println(); client.println(F("<!DOCTYPE html>")); client.println(F("<html>")); client.println(F("<head>")); client.println(F("<meta charset='utf-8'>")); client.println(F("<meta http-equiv='Refresh' content='5; url=/' />")); client.println(F("<title>HTTP webserver - Arduino + Ethernet</title>")); client.println(F("</head>")); client.println(F("<body>")); client.println(F("<center><h3>Server prijal data z formulára:</h3>")); if (!isFloat(second_param) || !isFloat(second_param)) { client.println(F("<h3><font color='red'>Zadane udaje nie su cisla!!! Opakujte pokus po presmerovaní.</font></h3>")); } else { client.println("<li><b>Referenčná teplota: </b>" + String(H_1) + "</li>"); client.println("<li><b>Hysteréza: </b>" + String(H_2) + "</li>"); } client.println(F("<b>Presmerovanie... Prosim cakajte</b></center>")); client.println(F("</body>")); client.println(F("</html>")); delay(1); client.stop(); client.flush(); } else { client.println(F("HTTP/1.1 404 Not Found")); client.println(); delay(1); client.stop(); client.flush(); } } } } } }
Implementácia obsahuje programy pre statickú / dynamickú IPv4 adresu priradenú k Ethernet shieldu. Termostat je určený iba pre interiérové teploty! (nad 0°C), čomu je prispôsobená aj logika systému! Termostatom je možné nahradiť už existujúci izbový termostat, možno dočasne nahradiť ohrievač v akváriu / teráriu pre udržiavanie stálej teploty.