NodeMCU – odbieranie danych przez protokół HTTP
W ostatnim artykule z serii dowiedzieliśmy się w jaki sposób odebrać dane konfiguracyjne za pomocą protokołu UDP. Dziś zaprezentuję alternatywną wersję konfiguracji – tym razem przez specjalną stronę hostowaną na ESP na której użytkownik może wprowadzić odpowiednie dane.
Na początku chciałbym zaznaczyć, że konfigurator, który stworzymy w tym poradniku będzie minimalnym minimum. Po lekturze artykułu każdy zainteresowany będzie mógł go sobie upiększyć dodając jakieś regułki CSS lub zwiększyć jego możliwości konfiguracyjne.
Na początek zobaczmy w oficjalnym repozytorium twórców bibliotek Arduino dla ESP8266 jak postawić prosty serwer: https://github.com/esp8266/Arduino/blob/master/doc/esp8266wifi/server-examples.rst
Wgrajmy ten przykład, otwórzmy monitor portu szeregowego i wejdźmy na adres IP naszego NodeMCU.
Jak widać każde żądanie rozpoczyna się od nazwy metody HTTP (w tym przypadku GET), lokalizacji, jaką klient odpytuje (tutaj '/’) oraz wersji protokołu HTTP (HTTP/1.1). Ta konkretna linijka na pewno przyda nam się później, gdyż będziemy musieli w różny sposób obsłużyć metodę GET oraz POST.
Żeby móc odebrać dane POST musimy nieco przerobić gotowy przykład z
while (client.connected()) { // read line by line what the client (web browser) is requesting if (client.available()) { String line = client.readStringUntil('\r'); Serial.print(line); // wait for end of client's request, that is marked with an empty line if (line.length() == 1 && line[0] == '\n') { client.println(prepareHtmlPage()); break; } } }
na
String line; while(client.connected()) { for(int x = 0; client.available(); x++) { line = client.readStringUntil('\r'); line.trim(); Serial.println(line); } } client.println(prepareHtmlPage()); break;
Po wgraniu tak przerobionego szkicu możemy puścić testowe zapytanie np. Postmanem.
Jak widać w ostatniej linijce mojego posta znajdują się parametry, które przesłałem. Trzeba je będzie później jakoś rozkodować.
Kilka ważnych metod
Przed pisaniem klasy przyjrzyjmy się kilku metodom, które będą nam potrzebne:
- server.available() – w przypadku oczekującego zapytania zwraca instancję klienta
- client.connected() – zwraca prawdę, dopóki będzie istniało połączenie pomiędzy klientem, a serwerem
- client.available() – zwraca prawdę, jeżeli w buforze klienta dostępne są jakieś dane (np. nagłówki HTTP)
- client.readStringUntil() – odczytuje dane z bufora aż do napotkania konkretnego znaku
Nagłówek klasy
Tak jak i w poprzednich artykułach zaczniemy od ogólnego obrazu klasy, którą będziemy tworzyć:
WebConfig.h
#ifndef WEBCONFIG_H #define WEBCONFIG_H #include <ESP8266WiFi.h> class WebConfig { private: WiFiServer server = WiFiServer(80); String prepareHtmlPage(String htmlContent); String prepareNetworksForm(); String fetchNetworks(); bool decodeAndSaveData(String data); void urlDecode(char* data); public: WebConfig(); void listen(); }; #endif /* WEBCONFIG_H */
Z publicznie dostępnych metod mamy tutaj konstruktor oraz metodę listen(), która posłuży nam do nasłuchiwania żądań HTTP w odpowiednim momencie. Pozostałe metody są prywatne i służą do działań wykonywanych „pod maską”. Pierwsze 3 z nich (prepareHtmlPage, prepareNetworksForm, fetchNetworks) służą do generowania odpowiedniego kodu HTML. decodeAndSaveData jest odpowiedzialne za odczytanie danych przesłanych POSTem i zapisanie ich w pamięci. urlDecode będzie metodą pomocniczą do rozkodowania znaków specjalnych.
Implementacja
Zajmijmy się teraz implementacją w pliku WebConfig.cpp. Na początek zróbmy najłatwiejsze, czyli konstruktor.
WebConfig::WebConfig() { server.begin(); }
Wystarczy, że wrzucimy do niego linijkę server.begin() z przykładu, który omawialiśmy.
Zaimplementujmy teraz funkcje odpowiedzialen za generowanie HTML:
String WebConfig::prepareHtmlPage(String htmlContent) { String htmlPage = String("HTTP/1.1 200 OK\r\n") + "Content-Type: text/html; charset=utf-8\r\n" + "Connection: close\r\n" + "\r\n" + "<!DOCTYPE HTML>" + "<html>" + htmlContent + "</html>" + "\r\n"; return htmlPage; } String WebConfig::prepareNetworksForm() { String content = String("<p>Wybierz z poniższej listy nazwę sieci WiFi, do której urządzenie ma się podłączyć.</p>") + "<form action='.' method='POST'>" + " <label for='ssid'><p>Nazwa sieci</p></label>" + " <select id='ssid' name='ssid'>" + fetchNetworks() + " </select>" + " <label for='password'><p>Hasło (opcjonalnie)</p></label>" + " <p><input id='password' type='password' name='password'/></p>" + " <input type='submit' value='Zapisz ustawienia'/>" + "</form>"; return content; }
Jak widać odwołujemy się tutaj pomiędzy linijkami HTML do metody o nazwie fetchNetworks(). Wygląda ona podobnie do tej, którą implementowaliśmy przy okazji UDP:
String WebConfig::fetchNetworks() { int networksCount = WiFi.scanNetworks(); String result = ""; for(int i = 0; i < networksCount; i++) { int32_t signalStrength = WiFi.RSSI(i); int32_t signalQuality; if(signalStrength <= -100) { signalQuality = 0; } else if(signalStrength >= -50) { signalQuality = 100; } else { signalQuality = 2 * (signalStrength + 100); } String encryptionType; switch(WiFi.encryptionType(i)) { case ENC_TYPE_WEP: encryptionType = "WEP"; break; case ENC_TYPE_TKIP: encryptionType = "WPA/PSK"; break; case ENC_TYPE_CCMP: encryptionType = "WPA2/PSK"; break; case ENC_TYPE_AUTO: encryptionType = "WPA/WPA2/PSK"; break; case ENC_TYPE_NONE: encryptionType = "Sieć otwarta"; break; default: encryptionType = "?"; } result += "<option value='" + WiFi.SSID(i) + "'>" + WiFi.SSID(i) + " (" + encryptionType + ", " + signalQuality + "%)</option>"; } return result; }
Skoro generowanie HTML mamy już za sobą to przejdźmy do metody oczekującej na nadejście zapytania:
void WebConfig::listen() { WiFiClient client = server.available(); if(client) { bool isRequestProcessed = false; String line; int responseType = -1; while(client.connected()) { for(int x = 0; client.available(); x++) { isRequestProcessed = true; line = client.readStringUntil('\r'); line.trim(); if(x == 0) { if(line.startsWith("GET")) { responseType = 0; } else if(line.startsWith("POST")) { responseType = 1; } } } if(isRequestProcessed) { switch(responseType) { case 0: client.println(prepareHtmlPage(prepareNetworksForm())); break; case 1: if(decodeAndSaveData(line)) { client.println(prepareHtmlPage("Dane konfiguracyjne zostaly zapisane")); } else { client.println(prepareHtmlPage("Wystąpił błąd podczas zapisywania danych")); } break; default: client.println(prepareHtmlPage("Nie rozpoznano polecenia")); } break; } } delay(1); client.stop(); } }
Jest to mocno przerobiony kod z przykładu z githuba. Trzeba było wprowadzić kilka zmian, aby można było odebrać dane POST oraz rozróżnić typ zapytania (GET / POST). Następnie w zależności od przesłanych danych w zapytaniu ESP musi zwrócić konkretną odpowiedź. W przypadku zapytania GET (stosujemy tutaj bardzo duże uproszczenie – każdy GET będzie zwracał ten sam wynik, podobnie jak każdy POST) trzeba zwrócić formularz z dostępnymi sieciami (funkcja prepareNetworksForm()), a w przypadku POSTa zapisujemy dane, które otrzymaliśmy.
Aby zapisać dane zastosujemy funkcję decodeAndSaveData():
bool WebConfig::decodeAndSaveData(String data) { String ssid = "", pass = ""; data.replace("+", " "); String field = ""; for(int x = 0; x < data.length(); x++) { if(data[x] == '&' || x == data.length() - 1) { if(data[x] != '&') { field += data[x]; } if(field.startsWith("ssid=")) { ssid = field.substring(5); } else if(field.startsWith("password=")) { pass = field.substring(9); } field = ""; } else { field += data[x]; } } if(ssid == "") { return false; } char ssidArr[32]; char passwordArr[32]; ssid.toCharArray(ssidArr, 32); pass.toCharArray(passwordArr, 32); urlDecode(ssidArr); urlDecode(passwordArr); ConfigManager configManager = ConfigManager::getInstance(); configManager.load(); configManager.setSSID(ssidArr); configManager.setPassword(passwordArr); return configManager.save(); }
Tutaj na samym początku zamieniamy wszystkie + na spacje (tak są one kodowane), a następnie rozbieramy ten długi ciąg na mniejsze podciągi i analizujemy ich treść. Jeżeli znajdziemy tam wartość dla klucza ssid lub password to zapisujemy je do odpowiednich zmiennych.
Po przeanalizowaniu całego ciągu sprawdzamy, czy znaleźliśmy interesujące nas wartości i jeżeli tak to trzeba je jeszcze rozkodować i zapisać. Jeżeli chodzi o dekodowanie tych stringów to posłużymy się bardzo dobrą implementacją stąd.
void WebConfig::urlDecode(char* data) { // Create two pointers that point to the start of the data char *leader = data; char *follower = leader; // While we're not at the end of the string (current character not NULL) while (*leader) { // Check to see if the current character is a % if (*leader == '%') { // Grab the next two characters and move leader forwards leader++; char high = *leader; leader++; char low = *leader; // Convert ASCII 0-9A-F to a value 0-15 if (high > 0x39) high -= 7; high &= 0x0f; // Same again for the low byte: if (low > 0x39) low -= 7; low &= 0x0f; // Combine the two into a single byte and store in follower: *follower = (high << 4) | low; } else { // All other characters copy verbatim *follower = *leader; } // Move both pointers to the next character: leader++; follower++; } // Terminate the new string with a NULL character to trim it off *follower = 0; }
Skoro mamy już całą klasę za sobą to wystarczy, że do głównego pliku programu dopiszemy klika linijek:
[...] #include "WebConfig.h" [...] WebConfig webConfig; void setup() { [...] while(true) { udpMessengerServie.listen(); webConfig.listen(); } [...] }
Test
Po wgraniu szkicu do procesora i wejściu na adres 192.168.1.1 w sieci „Nettigo Config” powinniśmy ujrzeć podobną stronę do tej:
A po zapisaniu ustawień zobaczymy taki oto komunikat:
Teraz wystarczy, że uruchomimy ESP ponownie i powinno ono już połączyć się do naszej docelowej sieci :)
Dostępne są wszystkie pliki i biblioteki z tego wpisu gdzieś do pobrania?? składanie tego z różnych wpisów np udpMessengerServie.listen(); o którym nie ma tu słowa to trochę dziwne.
Witam, gdzie można pobrać całe paczki z przykładów ??
Cześć,
udało mi się znaleźć taką paczkę gdzieś na dysku: https://www.dropbox.com/s/l78o9anyxwobmss/esp8266_complete.zip?dl=0
niestety nie mam teraz jak zweryfikować czy pokrywa się ona z tym co jest w artykule, ale na pierwszy rzut oka wygląda ok :)