Arduino je vynikajúca embedded platforma, ideálna pre vytvorenie izbového termostatu s Ethernet konektivitou a webserverom implementovaným priamo na Arduine. Implementácia využíva populárne modely ako Arduino Uno / Nano s čipom AVR ATmega328P a Ethernet shieldom Wiznet W5100 / W5500, pričom komunikácia prebieha cez SPI zberniciu alebo ICSP header. Termostat funguje ako webserver, schopný prijímať a spracovávať požiadavky od klientov v sieti prostredníctvom HTTP protokolu. Následne odpovedá HTML / JSON kódom a vykonáva príslušné funkcie backendu, vrátane spustenia skriptov, ovládania GPIO, zápisu do pamäte a ďalších funkcií. Termostat je prístupný z lokálnej siete (LAN), pričom disponuje webovým rozhraním, ktoré umožňuje konfiguráciu všetkých parametrov termostatu, ako sú cieľová teplota a hysteréza. Webserver podporuje viacero nezávislých HTML stránok, ktoré môžu mať informatívny obsah alebo implementovaný funkčný backend s rôznymi funkciami termostatu. Systém beží na štandardnom HTTP porte - 80, čím je zabezpečená kompatibilita so všetkými zariadeniami a jednoduchý prístup prostredníctvom webového prehliadača. Vytvorte si vlastný inteligentný termostat pomocou tejto výkonnej Arduino platformy s Ethernet konektivitou.
Termostat je schopný automaticky ovládať signalizačné relé pre zapnutie a vypnutie kotla prostredníctvom GPIO výstupu. Týmto spôsobom môže nahradit existujúci izbový termostat a umožniť prístup klientom cez sieť. Podporuje širokú škálu zariadení s webovým prehliadačom, vrátane počítačov, smartfónov, tabletov, Smart TV a podobne. Rozhodovací algoritmus termostatu využíva cieľovú teplotu s hysterézou, ktorá sa porovnáva s aktuálnou teplotou z digitálneho senzora teploty Dallas DS18B20. Cieľová teplota a hysteréza sú ukladané v EEPROM pamäti, aby boli trvale dostupné aj pri výpadku napájania. Senzor DS18B20 dosahuje rozlíšenie 12 bitov, čo znamená minimálny rozlišovací krok 0,0625 °C medzi jednotlivými meraniami. Dáta získané cez OneWire zbernicu môžu byť vyžiadané do mikrokontroléru v intervaloch od 500 do 1000 ms, v závislosti od počtu pripojených senzorov a dĺžky zbernice. Rozhodovacia logika termostatu sa vykonáva každých 10 sekúnd nezávisle na webovej aplikácii, čo znamená, že nie je potrebné udržiavať keep-alive spojenie pre vykonanie logiky. Systém tak funguje autonómne a nevyžaduje aktívnu pozornosť používateľa. Vytvorte si inteligentný a efektívny systém vykurovania s týmto pokročilým termostatom.
Z hardvérovej perspektívy využíva projekt nasledujúce komponenty:HTML stránky bežiace na Arduine:
Webové rozhranie je navrhnuté s ohľadom na prispôsobenie sa obrazovkám rôznych veľkostí, či už ide o väčšie alebo menšie displeje. Je plne reaktívne a podporuje širokouhlé obrazovky s vysokým rozlíšením, ako aj mobilné zariadenia. Rozhranie využíva importované CSS štýly z Bootstrap frameworku, ktoré sú hostované na externom CDN serveri. Tieto štýly sa načítavajú na strane klienta pri otvorení stránky bežiacej na Arduine. Vzhľadom na obmedzenú pamäť Arduino Uno, ktoré má k dispozícii len niekoľko kilobytov, umožňuje importovanie CSS štýlov z externého servera znížiť zaťaženie pamäte a výkonu. Programová implementácia (pre Arduino Uno) využíva približne 70% flash pamäte (32 kB - 4 kB Bootloader) a 44% RAM pamäte (2 kB). Statické časti webovej stránky, ako sú hlavička a päta HTML dokumentu, odkazy na Bootstrap CSS, meta tagy, HTTP response hlavička, Content Type, formuláre a ďalšie, sú uložené priamo vo flash pamäti Arduina. Toto riešenie výrazne znižuje veľkosť používanej RAM pamäte pre obsah generovaný používateľovi. Webserver je tak stabilnejší a schopný spracovať viacero súčasných pripojení zariadení v sieti. Celková spotreba celého termostatu je udržaná na úrovni do 200 mA pri 5V napájaní, čo zodpovedá menej než 1W. Táto efektívnosť prispieva k nízkej energetickej náročnosti termostatu.
Uloženie nastavených hodnôt do EEPROM pamäte Arduino (k dispozícii 512 B) zabezpečuje zachovanie týchto hodnôt aj po výpadku napájania. Referenčná teplota je zapisovaná na offset 10, hysteréza na offset 100. Každá z hodnôt zaberá maximálne 5 B v EEPROM pamäti, vrátane ukončovacieho znaku - terminátora. Limit prepisovania EEPROM je nastavený na úroveň 100-tisíc prepisov. Dáta sa prepisujú iba pri odoslaní HTML formulára, čo minimalizuje záťaž na EEPROM pamäť. V prípade, že zariadenie nemá pri prvom spustení nič uložené na uvedených EEPROM offsetoch, automaticky sa vykoná zápis s predvolenými hodnotami - referencia: 20.25 °C, hysteréza 0.25 °C. Toto tzv. fail-safe riešenie zabezpečuje, aby bol termostat okamžite funkčný a pripravený na prevádzku, aj bez ručného nastavenia hodnôt. Celkový prístup k EEPROM pamäti je optimalizovaný tak, aby prevádzka termostatu bola maximálne šetrná k EEPROM pamäti, s cieľom maximalizovať jej životnosť a zabezpečiť dlhodobú spoľahlivosť.
Webserver vykonáva obnovu celej stránky každých 30 sekúnd prostredníctvom meta tagu Refresh. Prostredníctvom Javascriptu sa v HTML stránke zobrazuje orientačný čas do ďalšieho obnovenia, pričom používatelia majú 30 sekúnd na vykonanie zmien pre termostat predtým, než sa input okná 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. Vzhľadom k tomu, že 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 potrebné preposlať celú stránku. Dynamický údaj, ktorý sa najčastejšie mení, je aktuálna hodnota výstupu - Zapnutý / Vypnutý, čo informuje prevádzkovateľa o aktuálnom stave výstupu spolu s farebným označením. Logika systému beží nezávisle na webserveri, takže do času refreshu môže byť stav výstupu iný, ako je aktuálne vypísaný 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, teda o tom, ako dlho termostat beží. Uptime je zobrazený v dňoch, hodinách, minútach a sekundách. Tieto informácie umožňujú prevádzkovateľovi monitorovať trvanie činnosti termostatu.
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 - Arduino + Wiznet W5100 Ethernet shield |*/
/*|AUTHOR: Martin Chlebovec |*/
/*|EMAIL: martinius96@gmail.com |*/
/*|Na kávu: paypal.me/chlebovec |*/
/*|----------------------------------------------------------|*/
#include <avr\wdt.h>
#include <SPI.h>
#include <Ethernet.h> //Wiznet W5100
//#include <Ethernet2.h> //Wiznet W5500
#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 = "VYP";
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("<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.