Pentru inceput...
Dupa toata valva creata in mediul "online" de catre transciever-ul WiFi ESP8266, m-am hotarat sa incerc si eu jucaria, nu de alta dar a trecut ceva timp de cand nu am scris cateva linii de cod :).
Asa ca mi-am comandat un modul NodeMcu V1.0. Placa de dezvoltare in cauza nu este altceva decat un transciever ESP8266 caruia i-a fost adaugat un convertorul serial CP2102 si un stabilizator de tensiune.
Dupa ce am "forat" tot internetul pentru a vedea cum se programeaza jucaria, din motive pe care nu le voi enumera aici, am deciz sa folosesc IDE-ul celor de la Arduino. Un tutorial despre cum se poate configura Arduino IDE pentru a programa NodeMcu se poate gasi aici.
In cele ce urmeaza va voi prezenta un server web ce ruleaza pe NodeMcu (numai in reteaua WiFi locala) capabil de a comanda si de a citi starea a 4 leduri. Ce a iesit puteti vedea in filmuletul de mai jos:
ATENTIE: NU PROGRAMATI CONTROLLER-UL PANA NU CITITI PANA LA CAPAT TUTORIAL-UL, DEOARECE PAGINA HTML VA TREBUI INCARCATA IN MEMORIA FLASH A ACESTUIA.
Tutorialul are doua parti: "programarea" frontend-ului, adica a paginii HTML, si programarea ca server a controller-ului NodeMcu.
Inainte de a intra in "amanuntele" problemei haideti sa vedem cum va arata pagina:
Fiecare led poate fi aprins sau stins utilizand "select box-ul" aferent acestuia din coloana "Command". Starea ledului poate fi urmarita in coloana "Status". In imaginea de mai sus, clientul (browser-ul) s-a conectat pentru prima data la server (NodeMcu) la adresa locala "192.168.0.34". Fiind pentru prima data, se presupune ca starea ledurilor nu este cunoscuta.
Dupa incarcarea paginii clientul are doua posibiliati:
In imaginea de mai sus a fost citita starea ledurilor. Pentru ca in cazul de fata este vorba despre primul client al serverului toate ledurile sunt stinse. Haideti acum sa aprindem niste leduri cu un client (FireFox), dupa care sa citim starea acestora cu un alt client (Chrome):
Codul paginii web este scris in HTML si utilizeaza pentru citirea si comandarea ledurilor tehnologia AJAX. Va recomand cu caldura pagina de aici, daca doriti sa aflati mai multe despre aceasta tehnologie. Pentru mine informatiile de acolo au fost de un real ajutor mai ales ca sunt total incepator in web developing :).
Haideti acum sa vedem cum arata codul sursa al paginii web:
In cele ce urmeaza va voi prezenta un server web ce ruleaza pe NodeMcu (numai in reteaua WiFi locala) capabil de a comanda si de a citi starea a 4 leduri. Ce a iesit puteti vedea in filmuletul de mai jos:
Download...
Ca de obicei codul sursa (atat HTML cat si sketch-ul) se pot descarca de aici.ATENTIE: NU PROGRAMATI CONTROLLER-UL PANA NU CITITI PANA LA CAPAT TUTORIAL-UL, DEOARECE PAGINA HTML VA TREBUI INCARCATA IN MEMORIA FLASH A ACESTUIA.
Tutorialul are doua parti: "programarea" frontend-ului, adica a paginii HTML, si programarea ca server a controller-ului NodeMcu.
Circuitul...
I. Pagina web...
Inainte de a intra in "amanuntele" problemei haideti sa vedem cum va arata pagina:
Fiecare led poate fi aprins sau stins utilizand "select box-ul" aferent acestuia din coloana "Command". Starea ledului poate fi urmarita in coloana "Status". In imaginea de mai sus, clientul (browser-ul) s-a conectat pentru prima data la server (NodeMcu) la adresa locala "192.168.0.34". Fiind pentru prima data, se presupune ca starea ledurilor nu este cunoscuta.
Dupa incarcarea paginii clientul are doua posibiliati:
- sa citeasca starea ledurilor apasand butonul "REQUEST";
- sa comande aprinderea/stingerea ledurilor fara cunoasterea starii acestora prin selectarea optiunii ON/OFF din select box-ul aferent fiecarui led si apasarea butonului "SEND".
Daca va veti hotari sa utilizati intr-un fel sau altul server-ul propus de mine va recomand sa cititi starea ledurilor inainte de a le comanda:
In imaginea de mai sus a fost citita starea ledurilor. Pentru ca in cazul de fata este vorba despre primul client al serverului toate ledurile sunt stinse. Haideti acum sa aprindem niste leduri cu un client (FireFox), dupa care sa citim starea acestora cu un alt client (Chrome):
Clientul conectat prin Fire Fox trimite comanda |
Clientul conectat prin Chrome cere citirea ledurilor |
Clientul conectat prin Chrome primeste de la server starea ledurilor |
Haideti acum sa vedem cum arata codul sursa al paginii web:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | <!DOCTYPE html> <!--<meta http-equiv="refresh" content="5;URL='192.168.0.34'">--> <html lang="en"> <head> <meta charset="utf-8" /> <script type="text/javascript"> function test(z) { var i; var t = ""; for (i = 1; i < 5; i++) { var x = document.getElementById("s" + i); t = t + x.options[x.selectedIndex].value; } var xmlHttp = new XMLHttpRequest(); xmlHttp.onreadystatechange = function () { if (xmlHttp.readyState == 4 && xmlHttp.status == 200) { var i; for (i = 1; i < 5; i++) { if (xmlHttp.responseText.charAt(i) == '0') { document.getElementById("led" + i).setAttribute("style", "background-color: red;"); document.getElementById("led" + i).innerHTML="OFF"; document.getElementById("s" + i).selectedIndex=1; } if (xmlHttp.responseText.charAt(i) == '1') { document.getElementById("led" + i).setAttribute("style", "background-color: green;"); document.getElementById("led" + i).innerHTML="ON"; document.getElementById("s" + i).selectedIndex=0; } } } }; if(z==1){ xmlHttp.open("GET", "c"+t, true); } if(z==2){ xmlHttp.open("GET", "r"+t, true); } xmlHttp.send(); } </script> <style> body { background-color: #484545 } .cbtn { border-style: solid; color: #000; font:12px 'Comic Sans MS'; border-color: #000; text-align: center; display: inline-block; margin-top: 10px; margin-bottom:10px; cursor: pointer; width: 100px; height: 40px; } .ledStatus { border-style: solid; color: #000; border-color: #000; display: inline-block; margin-top: 10px; margin-left: 10px; margin-bottom:10px; width: 50px; height: 50px; border-top-left-radius: 25px; border-top-right-radius: 25px; font:15px 'Comic Sans MS'; text-align:center; } .select { font-family: 'Comic Sans MS'; background-color: #666666; color: red; width:100px; } .p1 { border: solid; border-radius: 25px; margin: 10px; } .table { border: 1px solid black; font-family: 'Comic Sans MS'; } </style> </head> <body> <h1 style="font-family: 'Comic Sans MS'">Node Mcu ON/OFF switch</h1> <table width="600" bordercolor="#0000CC" bgcolor="#666666" class="table"> <tr> <th width="100">Led Number</th> <th width="100">Command</th> <th width="200"><div align="center">Status</div></th> </tr> <tr> <td><div align="center">LED1</div></td> <td><div align="center"> <select id="s1" class="select"> <option value="1" >ON</option> <option value="0"selected>OFF</option> </select> </div></td> <td><div align="center"> <button id="led1" class="ledStatus" style="background-color:#999999">?</button> </div></td> </tr> <tr> <td><div align="center">LED2</div></td> <td><div align="center"> <select id="s2" class="select"> <option value="1" >ON</option> <option value="0"selected>OFF</option> </select> </div></td> <td><div align="center"> <button id="led2" class="ledStatus" style="background-color:#999999">?</button> </div></td> </tr> <tr> <td><div align="center">LED3</div></td> <td><div align="center"> <select id="s3" class="select"> <option value="1" >ON</option> <option value="0"selected>OFF</option> </select> </div></td> <td><div align="center"> <button id="led3" class="ledStatus" style="background-color:#999999">?</button> </div></td> </tr> <tr> <td><div align="center">LED4</div></td> <td><div align="center"> <select id="s4" class="select"> <option value="1" >ON</option> <option value="0"selected>OFF</option> </select> </div></td> <td><div align="center"> <button id="led4" class="ledStatus" style="background-color:#999999" te>?</button> </div></td> </tr> </table> <table width="600" bordercolor="#0000CC" bgcolor="#666666" class="table" > <tr> <td width="50%"> <div align="center"> <button id="sen"class="cbtn" style="background-color: green" onClick="test(1)"> SEND </button> </div></td> <td width="50%"> <div align="center"> <button id="req"class="cbtn" style="background-color: red" onClick="test(2)">REQUEST </button> </div></td> </tr> </table> </body> </html> |
Dupa cum se vede nu e mare filosofie (mai ales ca am fost ajutat un pic de excelentul Dreamweaver). Toate elementele vizuale ale paginii sunt stocate intr-un tabel, si se conformeaza regulilor impuse de clasele CSS definite.
Partea care intereseaza este functia "test (z)", din script. Aceasta functie este accesata ori de cate ori este apasat butonul "Send" sau "Request".
In prima parte functia mai sus amintita citeste valorile tuturor select box-urilor si construietse stringul stocat in variabila " var t". Acest string va fi trimis la final catre server.
Acum partea de AJAX (liniile de cod 15-40).
Daca daca nu este prima conectare a clientului la server, cu alte cuvinte pagina se afla deja incarcata in browser, serverul va trimite clientului un sir de caractere de forma "/****" unde fiecare steluta poate fi "1" sau "0" in functie de starea ledului.
Dar ce inseamna "if (xhttp.readyState == 4 && xhttp.status == 200)"?
Accesand urmatorul link veti gasi toate informatiile.
Asadar daca reaspunsul de server a fost primit cu succes (4) si daca si daca serverul a fost gasit (200) se poate trece la procesarea mesajului primit de la server (in bucla for care incepe de la linia 19). Practic se verifica fiecare caracter al sirului, incepand cu prima pozitie (si nu cu zero) si se actioneaza dupa cum urmeaza:
- daca sirul la pozitia "i" contine caracterul '0' atunci: se coloreaza rosu ledul cu id-ul "led"+i, se afiseaza peste ledul cu id-ul "led"+i textul "OFF" iar indexul selectat al select box-ului cu id-ul "s"+i va fi "1" corespunzator textului "OFF".
- daca sirul la pozitia "i" contine caracterul '1' atunci: se coloreaza verde ledul cu id-ul "led"+i, se afiseaza peste ledul cu id-ul "led"+i textul "ON" iar indexul selectat al select box-ului cu id-ul "s"+i va fi "0" corespunzator textului "ON".
Pentru a trimite cererea catre server se folosesc functiile "xmlHttp.open()" si "xmlHttp.send()" .
xmlHttp.open() are ca parametrii:
- "GET" sau "POST", in functie de ce vrem sa transmitem server-ului;
- cererea catre server propriuzisa codata ca un sir de caractere (String);
- cel de-al treilea paramentru reprezinta modul in care se comunica cu serverul (sincron sau asincron). Mai multe informatii aici.
In exemplul de fata se foloseste o cerere de tip "GET" si in functie de butonul apasat se trimite sirul de caractere:
- "c"+t daca a fost trimisa o comanda;
- "r"+t daca se doreste citirea ledurilor.
Cam atat ar fi de zis despre pagina web. Neavand experienta in acest domeniu a fost nevoie sa ma documentez despre acest subiect, ceea ce va reconad si voua.
II. Programarea serverului (NodeMcu)...
Majoritatea exemplelor pe care le-am gasit pe internet, in care NodeMcu este utilizat ca web server, trimiteau pagina clientului lineie cu linie.
Personal nu agreez aceasta metoda din doua motive:
- codul paginii html este scris direct in sketch, fapt care, cel putin din punctul meu de vedere, duce la o citire si gestionare greoaie a codului;
- in majoritatea exemplelor gasite codul este trimis clientului linie cu linie ceea ce duce la timpi mari de incarcare in browser;
Am incercat sa gasesc o metoda prin care sa evit cele doua neajunsuri enumerate anterior. Si am gasit. S P I F F S - Serial Peripheral Interface Flash File System (Pffweeeww) .
Asadar din acronimul de mai sus rezulta ca exista un File System. Daca exista un File System inseamna ca se pot scrie si citi fisiere, iar unul din fisere poate fi chiar fisierul care sa contina codul html.
Cum se utlizeaza SPIFFS pentru NodeMcu?
NodeMcu v1.0 dispune de o memorie flash de 4MB, din care 1MB este ocupat de bootloader. Asta inseamna ca mai raman 3MB disponibili pentru a putea stoca fisiere, spatiu mai mult decat suficient pentru pagini html simple.
Modul in care se incarca fisere in memoria flash a lui NodeMcu este descris aici, iar o descriere mai amanuntita a file system-ului NodeMcu se poate citi aici.
Inainte de a trece la partea de cod un ultim sfat:
Inainte de a incarca fisierele in flash, inarmati-va cu muuuulta rabdare. Chiar daca se incarca un fisier de text de 1KB sau unul de 3MB timpul de incarcare va fi acelasi: mare, ~5 min. Motivul? Se rescrie toata memoria, indiferent de marimea fiserului care se incarca.
Codul sursa pentru NodeMcu...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | ///////////////////////////////////////////////////////////////////////////// //LOCAL NETWORK SERVER WITH NODEMCU // //PETRESCU CRISTIAN // ////////////////////////////////////////////////////////////////////////// //file system reference: //http://arduino.esp8266.com/versions/1.6.5-1160-gef26c5f/doc/reference.html#file-system-object-spiffs //html and java sript reference: //http://www.w3schools.com/ //check my blog :www.hobbyelectro.blogspot.ro #include "FS.h" #include <ESP8266WiFi.h> int ledState[4] = {0, 0, 0, 0}; int leds[4] = {D0, D1, D2, D3}; String buff; const char* ssid = "your ssid"; const char* password = "your password"; WiFiServer server(80); //////////////////////////////////////////////////////////////////////////// //send the html page to client // ////////////////////////////////////////////////////////////////////////// void printPage(WiFiClient k) { File f = SPIFFS.open("/4LEDS.html", "r");//open html page for reading if (!f) { Serial.println("file open failed");// check if file exists return; } buff = f.readString();// read all file at once f.close();// close the file // send data to browser in 2 packets of 2KB each k.print(buff.substring(0, 2000));//send first 2000chars to browser k.print(buff.substring(2000));//send second 2000chars to browser } //////////////////////////////////////////////////////////////////////////// //check if client wants to read the states of leds or want to change them// ////////////////////////////////////////////////////////////////////////// void checkRequest(WiFiClient k, String request) { int i = 0; String response = "/";// add the first char of the response string //check if request it's a command if (request.indexOf("c") != -1) { // check each char of the request for (i = 1; i < 5; i++) { //check if client wants to turn a led off if (request.charAt(i) == '0') { ledState[i - 1] = 0; digitalWrite(leds[i - 1], ledState[i - 1]); // turn led off } // check if client wants to turn a led on if (request.charAt(i) == '1') { ledState[i - 1] = 1; digitalWrite(leds[i - 1], ledState[i - 1]); //turn led on } response.concat(String(ledState[i - 1]));//add state of led to the response string } //Serial.println(response); k.println(response);// send response to client return; } if (request.indexOf("r") != -1) {//check if client wants to read the state of leds for (i = 0; i < 4; i++) { response.concat(String(ledState[i]));//add state of led to the response string } //Serial.println(response); k.println(response);// send response to client return; } } ///////////////////////////////////////////////////////////////////////////////// //send the "ok" response to client // //if it's the first conection or reload then send page, if not check request // ////////////////////////////////////////////////////////////////////////////// void response(WiFiClient k) { k.println("HTTP/1.1 200 OK"); k.println("Content-Type: text/html"); k.println(""); // do not forget this one String request = k.readStringUntil('H');//read request Serial.println(request); //check if request if ((request.indexOf("/c") == -1) && request.indexOf("/r") == -1) { printPage(k); return; } request.remove(0, 5); checkRequest(k, request); } void setup() { SPIFFS.begin();//start spiffs (spi flash file system) Serial.begin(115200);// start serial session Serial.println(); int i; // set led pins as output for (i = 0; i < 4; i++) { pinMode(leds[i], OUTPUT); digitalWrite(leds[i], 0); } Serial.println(); Serial.println(); Serial.print("Connecting to "); Serial.println(ssid); // Connect to WiFi network WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(""); Serial.println("WiFi connected"); // Start the server server.begin(); Serial.println("Server started"); // Print the IP address Serial.print("Use this URL to connect: "); Serial.print("http://"); Serial.print(WiFi.localIP()); Serial.println("/"); } void loop() { // Check if a client has connected WiFiClient client = server.available(); if (!client) { return; } //analyze data received from client response(client); client.flush(); delay(1); //Serial.println("Client disonnected"); //Serial.println(""); } |
Dupa ce clientul s-a conectat la server, cel din urma trimite raspunsul apeland functia response(). Aceasta functie trimite mesajul de intampinare catre client, dupa care analizeaza cererea acestuia.
Astfel daca in cererea trimisa de client nu se regaseste sirul "/c" (coresounzator unei comenzi) sau sirul "/r" (corespunzator unei citiri a starii ledurilor) atunci se apleaza functia "printPage()". Aceasta functie citeste continutul fisierului html, dupa care il trimite in doua pachete de cate 2KB clientului.
Pentru a salva timp si RAM inainte de a incarca fiserul html in flash-ul controller-ului va recomand sa stergeti din acesta toate comentariile, liniile goale, si toate indentarile codului.
Daca in schimb pagina este deja incarcata in browser, iar utlizatorul a trimis o comanda sau o cerere de citire a starii ledurilor, este apelata functia checkRequest().
Aceasta functie are rolul de a construi sirul care va fi trimis clientului ca raspuns (sirul preluat de javascript prin AJAX) si de a controla fizic ledurile (in cazul in care utilizatorul doreste stingerea/aprinderea acestora).
Niciun comentariu:
Trimiteți un comentariu