luni, 4 ianuarie 2016

NODEMCU WEB SERVER -


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:


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
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:

  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).  



Cam asta ar fi. Astept parerile voastre.








Niciun comentariu:

Trimiteți un comentariu