ESP8266 FAN Controller - opis programu
Programy Rig And Log
GET HTTPS4
-
Czytnik kart RFID w domofonie
Eeprom Reader Windows
Eeprom Reader Android
-
PDF Printer
PDF Cutter
RDP Klient
DIP Switch Konfigurator
ELFRO GeoFencing
-
ESP8266 FAN Controller
Arduino porady i przykłady
IO22D08 Library
-
Edytorek Zdjęć
Kill Process
LAN SCANNER
-
Backup serwera i plików
ESP8266 FAN Controller ESP8266 FAN Controller - opis programu
Opis programu.
Po uruchomieniu Arduno uruchamia się pusty przykładowy szkic:
void setup() { // put your setup code here, to run once: } void loop() { // put your main code here, to run repeatedly: }
Składa się on z dwóch funkcji: setup() i loop()
Pierwsza z nich uruchamiana jest raz - wpisujemy tutaj dane konfiguracyjne inicjalizację zmiennych itd rzeczy, które wykonają się tylko 1 raz podczas uruchomienia systemu.
Funkcja loop() wykonywana jest cyklicznie z maksymalną możliwą częstotliwością. Jeżeli program opuści pętlę loop() automatycznie powróci do niej ponownie i tak bez przerwy.
Dobrym zwyczajem programistycznym jest aby w funkcji loop było jak najmniej elementów blokujących np. delay(500000); Szczególnie wymagane to jest przy bibliotekach serwera, które użyjemy.
Przykład mruganie diodą led. co 0,5 sekundy z blokowaniem kodu:
void loop() { digitalWrite(D0, 1); delay(500);// czekamy ... czekamy digitalWrite(D0, 0); delay(500); }
Można zamienić na wersję bez blokowania:
unsigned long timeTick = 0; bool lastLedState = false; void loop() { if (millis() - timeTick >= 500) { lastLedState = !lastLedState; digitalWrite(D0, lastLedState); timeTick = millis(); } }
Kod jest bardziej skomplikowany ale w międzyczasie można wykonywać inne rzeczy.
Wracamy jednak do tematu opracowania.
Aby uruchomić nasz serwer potrzebujemy dołączyć dodatkowe biblioteki do naszego programu. Są to zestawy gotowych funkcji i procedur do obsługi odpowiednich urządzeń, a my nie musimy się martwić skomplikowanym kodem.
Bez wnikania na samym początku szkicu wpisujemy:
#include <Arduino.h> #include <ESP8266WiFi.h> #include <ESPAsyncTCP.h> #include <ESPAsyncWebServer.h> #include <DNSServer.h> #include <ArduinoJson.h> #include <EEPROM.h>
Część bibliotek jest standardowo dostępnych w środowisku Arduino część zaś dodaliśmy sami w poprzednim artykule. Patrz: ESP8266 FAN Controller
Pierwszą czynnością jest uruchomienie WiFi:
char ssid[33] = "MY_WIFI_NETWORK"; char PASS[33] = "pass"; void setup() { Serial.begin(115200); WiFi.begin(ssid, PASS); while (WiFi.status() != WL_CONNECTED) { delay(1000); Serial.println("Connecting to WiFi.."); } Serial.print("Connected to: "); Serial.println(ssid); Serial.print("My IP is: "); Serial.println(WiFi.localIP()); }
oczywiście jest to najprostszy przykład nazwę sieci i hasło wpisujemy w zmiennych ssid i PASS. Rozwiązanie jest najprostsze jednak ma kilka wad
- nie obsługuje statycznego adresu IP
- brak rozwiązania w przypadku braku możliwości połączenia z siecią.
Aby dodać obsługę statycznych adresów IP musimy je najpierw zdefiniować :
const char* FAN_AP_NAME ="FAN_CONTROLLER"; char ssid[33] = "MY_WIFI_NETWORK"; char PASS[33] = "pass"; bool DHCP=true; IPAddress IP(192, 168, 1, 190); //domyślne adresy IP IPAddress GATE(192, 168, 1, 1); IPAddress MASK(255, 255, 255, 0); IPAddress DNS1(194, 204, 152, 34); IPAddress DNS2(194, 204, 159, 1); bool isInAP_Mode = false; // Czy WiFi jest w trybie Access Point'a unsigned long apStartTime; // Kiedy Access Point wystartował ???
Następnie w kodzie dodamy możliwość konfiguracji adresów IP - statyczną jeżeli DHCP=false lub dynamiczną gdy DHCP=true
void setup() { Serial.begin(115200); WiFi.disconnect(); WiFi.hostname("NAZWA_HOSTA"); if (!DHCP) // CZY STATYCZNY ADRES IP CZY DYNAMICZNY ??? WiFi.config(IP, GATE, MASK, DNS1, DNS2); WiFi.begin(ssid, PASS); //INICJALIZACJA WIFI
Teraz gdy WiFi jest zainicjowane będziemy sprawdzać co sekundę czy jest połączenie z WiFi. Po 100 sprawdzeniach jeżeli nadal nie ma połączenia uruchomimy WiFi w trybie Access Pointa - umożliwi to konfigurację np. za pomocą telefonu opisaną w poprzednim artykule. Patrz: ESP8266 FAN Controller
// próba połączenia z WiFi spróbuj max 100 razy co sekundę) int i = 0; while (WiFi.status() != WL_CONNECTED && i < 100) { delay(1000); Serial.println("Connecting to WiFi.."); i++; } // Czy Połączono ??? if (WiFi.status() == WL_CONNECTED) //TAK { Serial.print("Connected to: "); Serial.println(ssid); Serial.print("My IP is: "); Serial.println(WiFi.localIP()); } else //NIE { Serial.println("Not connected to WiFi - Acces Point Mode"); WiFi.disconnect(); WiFi.softAP("NAZWA_SIECI_SSID"); Serial.print("My IP is: 192.168.4.1"); //Acces Point ma zawsze ten adres IP niezaleznie od konfiguracji isInAP_Mode = true; apStartTime = millis(); }
Powyższy kod cały czas jest w pętli setup - czyli wykona się tylko raz. Jeżeli WiFi nie połączy się z siecią urządzenie wystartuje w trybie Access Pointa. Dobrze byłoby jednak gdyby ten tryb nie trwał wiecznie w przypadkach gdy wejście w w ten tryb nastąpi nieoczekiwanie np po zanikach napięcia.
Kod resetu wpiszemy już w funkcji loop(). Dodatkowo ustawimy aby w przypadku utraty połączenia z WiFi sterownik próbował połączyć się ponownie a po wielu nieudanych próbach też się zrestartował.
... unsigned long tenSec = 0; // zmienna do pętli 10 sekund int recon = 0; // liczba powtórzeń gdy brak połaczenia z WiFi ... void REBOOT() { delay(1000); ESP.reset(); delay(1000); } ... void loop() { if (millis() - tenSec > 10000) // sprawdzaj co 10 sekund { if (WiFi.status() != WL_CONNECTED) // jeżeli brak połączenia lub połączenie zostało utracone { recon++; if ( recon > 100) REBOOT(); // uruchom ponownie po 50 próbach (1000 sekund) if (recon % 2 == 0) WiFi.reconnect(); // próbuj połączyć co 20 sekund } else recon = 0; if (isInAP_Mode && millis() - apStartTime > 600000) REBOOT(); // jeżeli Access Point Mode restart po 10 minutach tenSec = millis(); } }
Tak pokrótce wygląda uruchomienie WiFi. Teraz należy uruchomić serwer. Na początku dodajemy zmienną
AsyncWebServer server(80);
Serwer już działa. Jednak nie wie jak odpowiadać na nasze zapytania. dlatego dalej w konfiguracji (pętla setup) dodajemy obsługę zdarzeń. Dodaje się ją wywołując funkcję server.on. ma ona kilka implementacji.
Zaczniemy od głównej strony index.html. żeby było uniwersalnie powinna się ładować po zapytaniu /, /index.htm, /index.html lub /index
server.on("/", HTTP_GET, &indexHTML); server.on("/index.html", HTTP_GET, &indexHTML); server.on("/index.htm", HTTP_GET, &indexHTML); server.on("/index", HTTP_GET, &indexHTML);
musimy dodać funkcję obsługi w kodzie. musi się ona znajdowac powyżej funkcji setup
void indexHTML(AsyncWebServerRequest * request) { request->send(200, "text/plain", "OK"); }
Na zapytanie serwera zwróci on tekst OK. Jednak strona z OK nie jest zbyt atrakcyjna. Można przesłać dłuższy tekst z kodem HTML jednak na dłuższą metę dla bardziej skomplikowanych stron może to być problem.
Płytka ESP 8266 w uproszczeniu może mieć pamięć podzieloną na dwie części pierwsza na pamięć programu druga na pliki. Do drugiej możemy mieć dostęp z programu jak do karty SD. Jest to bardzo wygodne rozwiązanie. Zapiszemy tam wszystkie pliki naszego serwera, a w programie będziemy je wysyłać bezpośrednio nie zabierając pamięci programu na kod stron, obrazki, skrypty i inne elementy.
Aby to było możliwe wpiszemy najpierw inicjalizację SPIFFS:
if (!SPIFFS.begin()) { Serial.println("Failed to mount FS"); return; }
Teraz serwer może wysłać dowolny plik poprzez SPIFFS
server.on("/fan_off.gif", HTTP_GET, [](AsyncWebServerRequest * request) { request->send(SPIFFS, "/fan_off.gif", "image/gif"); });
na koniec definicji zdarzeń serwera (server.on) definiujemy stronę onNotFound(nie znaleziono) i uruchamiamy serwer:
server.onNotFound(indexHTML); server.begin();
Najprostszym rozwiązaniem na stronę nie znaleziono - jest przekierowanie na stronę główną co zrobiłem.
Serwer już powinien działać. Jednak nie przesyła on na razie żadnych danych poza statycznymi stronami.
Potrzebujemy przesłać stan zmiennych np. w naszym przypadku stan wentylatorów, czy ustawienia konfiguracyjne.
Dobrze aby strona załadowała się już z tymi danymi np ustawienia wentylatorów a później w razie potrzeby aktualizowała je cyklicznie.
Jeżeli w kodzie html wpiszemy zmienne w postaci %VAR1% %VAR2% itd gdzie VAR1 to nazwa zmiennej np FAN_VALUE, to serwer może je przetworzyć i zastąpić wymaganą wartością.
Do przetworzenia i dodania zmiennych służy nam funkcja processor.
Najpierw dodajemy funkcję procesora przed funkcją setup()
String processor(const String& var) { if (var == "FAN_VALUE") return String(fanValue); return var; }
teraz w funkcji server.on dodajemy obsługę procesora
void indexHTML(AsyncWebServerRequest * request) { request->send(SPIFFS, "/index.html", String(), false, processor); }
Serwer wyśle już przetworzoną stronę html.
Jednak potrzebujemy aby strona pobierała i wysyłała dane o stanie wentylatorów na bieżąco. Wymaga to napisania funkcji po stronie serwera jak i przeglądarki.
Przeglądarka internetowa z zaladowaną stroną musi wysłać zapytanie na serwer. Zastosowałem tutaj metodę HTTP_GET. Jest ona najprostsza i łatwo też za jej pomocą zintegrować nasze urządzenie z innym oprogramowaniem.
Utworzyłem dwie strony get_fans.html i set_fans.html
Strona get_fans.html zwraca stan wentylatorów:
void get_fans_html() { server.on("/get_fans.html", HTTP_GET, [](AsyncWebServerRequest * request) { request->send(SPIFFS, "/get_fans.html", String(), false, processor2); }); }
Gdzie processor2 - odpowiedzialny jest za obsługę zmiennych i ich zaktualizowanie.
Po stronie przeglądarki wysyłane jest cyklicznie proste zapytaniew javascript:
function Receive() { req = new XMLHttpRequest(); var d = new Date(); var n = d.getTime(); req.open("GET", "get_fans.html?TS=" + n + "", true); req.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); req.onload = onLoad; req.onerror = onError; req.send(null); } function onError(e) { ... } function onLoad(data) { .. try { var lines = data.target.response; ... } catch (e) { } ... }
Strona html wysyła zapytanie na serwer o pobranie strony get_fans.html?TS=.... gdzie po ts dodawany jest znacznik czasu. Omija to ewentualne cache przeglądarki. Żądanie pobrania strony za każdym razem jest inne i strona jest ładowana z serwera(sterownika). Po odebraniu dane są dalej aktualizowane w skrypcie. Nie będę ty szczegółowo tego analizował - każda realizacja ma inne zmienne więc i inną obsługę. Po szczegóły zapraszam do źródeł programu.
Obsługa strony set_Fans jest bardziej skomplikowana. Za jej pomocą ustawiamy wartości elementów. Tu także wykorzystywana jest metoda HTTP_GET. Kod javaScript:
function sendIO(command) { req2 = new XMLHttpRequest(); var d = new Date(); var n = d.getTime(); req2.open("GET", "set_fans.html?TS=" + n + "&" + command, true); req2.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); req2.send(null); }
HTTP_GET umożliwia wysłanie danych bezpośrednio w linku po znaku zapytania np:
set_fans.html?TS=10239218919&e1=1&e2=1&e3=1&e4=1&v1=111&v2=222&v3=345&v4=1023
Gdzie TS- podobnie jak poprzednio jest znacznikiem czasu a kolejno e1 do e4 określają włączenie lub wyłączenie odpowiedniego wentylatora (1 włącz 0 wyłącz) a v1 do v4 - prędkość obrotową 0 to 0% a 1023 to 100%
Takie rozwiązanie pozwala w łatwy sposób sterować urządzeniem z dowolnej innej aplikacji, własnej strony internetowej czy umożliwia prostą integrację z resztą automatyki domowej.
Obsługa set_fans.html jest trudniejsza po stronie sterownika. Musimy pobrać argumenty z zapytania(query) wykorzystujemy tutaj funkcję getParam. Może to wyglądać tak:
bool fanEnabled[4] = {1, 1, 1, 1}; int fanValues[4] = {511, 511, 511, 511}; ... void set_fans_html() { server.on("/set_fans.html", HTTP_GET, [](AsyncWebServerRequest * request) { bool isDirty = false; String inputMessage = ""; for (byte i = 0; i < 4; i++) { String e1 = "e"; String e = e1 + String(i); if (request->hasParam(e)) { inputMessage = request->getParam(e)->value(); byte old = fanEnabled[i]; if (inputMessage == "1") fanEnabled[i] = 1; else fanEnabled[i] = 0; if (old != fanEnabled[i]) isDirty = true; } } for (byte i = 0; i < 4; i++) { String v1 = "v"; String v = v1 + String(i); if (request->hasParam(v)) { inputMessage = request->getParam(v)->value(); int val = inputMessage.toInt(); if(val<0) val=0; if (val>1023) val=1023; byte old = fanValues[i]; fanValues[i] = val; if (old != fanValues[i]) isDirty = true; } } if (isDirty) { writeFanValues(); // zapisz do EEPROM procedIO(); // ustaw wartości PWM } request->send(200, "text/html", "OK"); }); }
Po krótce serwer działa dane wentylatorów się zapisują i przesyłają. Pozostaje jeszcze główna część - konfiguracja jej zapis odczyt i przesyłanie na stronę.
Wykorzystamy tutaj metodę HTTP_POST służy ona do przesyłania dużo większej ilości danych, które nie są widoczne bezpośrednio w linku do strony. Przesyłane są w tle.
Aby przesłać dużo zmiennych po stronie klienta sformatujemy je w plik JSON. Jest to standard przesyłania danych. Przykładowy plik JSON wygląda tak:
{ SSID: "MOJA_SIEC_WIFI", PASS1: "pass", PASS2: "pass", isDHCP: "false", IP: "192.168.0.190", GATE: "192168.0.1", MASK: "255.255.255.0", DNS1: "194.204.152.34", DNS2: "194.204.159.1", securityLevel: "0", userName: "admin", userPass1: "admin", userPass2: "admin" }
Dane formatujemy po stronie przeglądarki internetowej. Z utworznego w kodzie html formularza pobieramy dane. Należy zadbać o sprawdzenie poprawności danych zakresu czy nie występują nieodpowiednie znaki np " . następnie dane wysyłamy metodą post:
req2 = new XMLHttpRequest(); req2.open("POST", "syscfg.html", true); req2.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); req2.onload = function () { ... } req2.onerror = function () { ... } req2.send(dane);
Po stronie sterownika odczytujemy otrzymane dane i zapisujemy na naszą "kartę SD" czyli SPIFFS:
void config_html() { // wyślij stronę na żądanie GET server.on("/syscfg.html", HTTP_GET, [](AsyncWebServerRequest * request) { if (!auth1(request)) return; request->send(SPIFFS, "/syscfg.html", String(), false, processor2); }); //Odbierz dane POST server.on("/syscfg.html", HTTP_POST, [](AsyncWebServerRequest * request) { int params = request->params(); // for (int i = 0; i < params; i++) { if (params > 0) { AsyncWebParameter* p = request->getParam(0); if (p->isPost()) { // zapis File configFile = SPIFFS.open("/syscfg.json", "w"); if (configFile){ request->send(200, "text/plain", "OK"); configFile.print(p->value()); configFile.close(); return; } } } request->send(405, "text/plain", "ERR"); }); }
Dane są konfiguracyjne już się zapisują w naszym sterowniku, Ale podczas uruchomienia sterownika powinny zostać odczytane, skonwertowane i przypisane do zmiennych.
Aby je odczytać podczas uruchomienia sterownika wywołujemy funkcję :
bool readSystemConfig() { bool jsonOK = false; if (SPIFFS.exists("/syscfg.json")) { File configFile = SPIFFS.open("/syscfg.json", "r"); if (configFile) { size_t size = configFile.size(); std::unique_ptr<char[]> buf(new char[size]); configFile.readBytes(buf.get(), size); DynamicJsonBuffer jsonBuffer; JsonObject& json = jsonBuffer.parseObject(buf.get()); configFile.close(); if (json.success()) { jsonOK = true; if (json.containsKey("SSID")) strcpy(ssid, json["SSID"]); else jsonOK = false; if (json.containsKey("PASS1")) strcpy(PASS, json["PASS1"]); else jsonOK = false; .... } } } return jsonOK; }
Oczywiście kod całego programu zawiera dodatkowe elementy. Tutaj opisałem jedynie jego newralgiczne fragmenty. Całość do pobrania jest stąd:
Projekt szkicu Arduno: FAN_Controller.zip ~60kB
Projekt płytki i schemat: schematyPDF.zip ~1,4MB