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