ESP32 (20) – Webserver

by luca
23 comments

Uno dei progetti più popolari tra quelli inclusi nel mio tutorial sul chip enc28j60 è sicuramente WebRelay. Tale progetto consente di attivare una uscita di Arduino tramite una semplice pagina web, accessibile anche da smartphone. Oggi vedremo come eseguire WebRelay con il chip esp32; sarà l’occasione per spiegarvi come realizzare un server TCP, in particolare un web server.

Netconn API

Come ormai sapete, il framework esp-idf utilizza la libreria lwip per gestire le comunicazioni di rete. Questa libreria offre diversi livelli di astrazione: il programmatore può decidere di gestire nel proprio programma i pacchetti grezzi (raw) oppure di utilizzare componenti già pronti.

Per realizzare il nostro server TCP, utilizzeremo proprio uno di questi componenti già pronti: le Netconn API.

Il suo utilizzo per realizzare un server è molto semplice ed è schematizzato nei seguenti passi:

webrelay-01

Il metodo netconn_new() crea una nuova connessione, restituendo un puntatore a struct netconn che rappresenta la nuova connessione:

struct netconn *conn;
conn = netconn_new(NETCONN_TCP);

Il parametro passato al metodo indica il tipo di connessione… quelli più comuni sono NETCONN_TCP per una connessione con protocollo TCP e NETCONN_UDP per una con protocollo UDP.

Per utilizzare la connessione in modalità server dobbiamo quindi associarla (bind) ad una specifica porta… ad esempio un server web normalmente è in ascolto sulla porta 80 (443 se in HTTPS):

netconn_bind(conn, NULL, 80);

Il secondo parametro del metodo (NULL sopra) consente di associare la connessione anche ad uno specifico indirizzo IP e può essere utile nel caso il dispositivo abbia più interfacce di rete. Utilizzando NULL (o l’equivalente IP_ADDR_ANY) si chiede alla libreria di effettuare il bind su ogni interfaccia disponibile.

Infine possiamo mettere il programma in ascolto con il metodo listen:

netconn_listen(conn);

Gestiamo una nuova connessione

Utilizzando il metodo netconn_accept() il nostro programma può accettare una nuova connessione in ingresso:

struct netconn *newconn;
netconn_accept(conn, &newconn);

Il metodo restituisce un puntatore ad una nuova struct netconn che rappresenta la connessione stabilita con il client. Questo metodo è bloccante: il programma si ferma finché un client non effettua una richiesta di connessione.

Una volta stabilita la connessione, è possibile utilizzare i metodi netconn_recv() e netconn_write() per ricevere o inviare dati al client:

netconn_recv(struct netconn* aNetConn, struct netbuf** aNetBuf );
netconn_write(struct netconn* aNetConn, const void* aData, size_t aSize, u8_t aApiFlags);

Il metodo netconn_recv(), per ottimizzare l’utilizzo della memoria RAM, gestisce i dati tramite un buffer interno (modalità zero-copy). Per poter accedere ai dati ricevuti è quindi necessario:

  • dichiarare una variabile come puntatore a struct netbuf
  • passare l’indirizzo di tale puntatore come secondo parametro
  • utilizzare il metodo netbuf_data() per ottenere un puntatore ai dati all’interno del netbuffer e la loro lunghezza

webrelay-02

struct netbuf *inbuf;
char *buf;
u16_t buflen;
netconn_recv(conn, &inbuf);
netbuf_data(inbuf, (void**)&buf, &buflen);

Similmente il metodo netconn_write() accetta, come ultimo parametro, un flag per indicare se copiare o meno il contenuto del buffer prima di effettuare l’invio. Per risparmiare memoria è quindi possibile, se si ha la sicurezza che tale buffer non sia alterato da altri thread, indicare come flag NETCONN_NOCOPY:

netconn_write(conn, outbuff, sizeof(outbuff), NETCONN_NOCOPY);

Al termine del dialogo con il client, la connessione può essere chiusa e il buffer liberato:

netconn_close(conn);
netbuf_delete(inbuf);

HTTP server

Quanto abbiamo visto finora, può essere applicato ad un qualsiasi server TCP. Se vogliamo dialogare con un browser Internet dobbiamo “parlare” la stessa lingua, ovvero il protocollo HTTP.

Nel programma di esempio (disponibile su Github) ho quindi implementato una versione minimale di tale protocollo. Quando digitiamo nel browser un indirizzo Internet (es. www.google.com), il browser si collega al server di Google e invia una richiesta nella forma

GET <resource>
[...]

La richiesta può avere diversi campi ma la prima riga contiene sempre il nome della risorsa (pagina, immagine…) che si vuole ottenere. In particolare se si accede alla homepage del sito, la richiesta sarà sempicemente GET /.

Il sito pubblicato da esp32 per controllare il relay è composto da solo due pagine:

  • off.html, visualizzata quando il relay è spento
  • on.html, visualizzata quando il relay è acceso

Ogni pagina contiene una scritta (“Relay is ON|OFF“) e una immagine. L’immagine contiene un link all’altra pagina e cliccandola viene anche cambiato lo stato del relay:

webrelay-03

Il programma identifica la risorsa richiesta verificando il contenuto della richiesta con strstr():

char *first_line = strtok(buf, "\n");
if(strstr(first_line, "GET / ")) [...]
else if(strstr(first_line, "GET /on.html ")) [...]
else if(strstr(first_line, "GET /on.png ")) [...]

Un server HTTP risponde al browser indicando per prima cosa l’esito della richiesta. Se è ok, il codice inviato è 200:

HTTP/1.1 200 OK

quindi indica il media type della risorsa richiesta e infine invia la risorsa. In questo esempio, gli unici media type possibili sono:

[checklist]

  • text/html per le pagine HTML
  • image/png per le immagini

[/checklist]

Contenuto statico

Un server web normalmente memorizza le risorse che compongono il sito pubblicato su un supporto esterno (memory card, disco fisso…). In casi molto semplici è possibile anche includere tutto il contenuto all’interno del programma.

In particolare nell’esempio proposto le pagine HTML e le stringhe di risposta del protocollo HTTP sono incluse come array statici:

const static char http_html_hdr[] = "HTTP/1.1 200 OK\nContent-type: text/html\n\n";
const static char http_png_hdr[] = "HTTP/1.1 200 OK\nContent-type: image/png\n\n";
const static char http_off_hml[] = "";

Per includere anche le immagini, ho sfruttato una funzionalità del framework (embedding binary data). E’ possibile indicare nel file component.mk i files da includere:

webrelay-04

All’interno del programma è possibile accedere al contenuto dei files embedded tramite appositi puntatori:

extern const uint8_t on_png_start[] asm("_binary_on_png_start");
extern const uint8_t on_png_end[]   asm("_binary_on_png_end");
extern const uint8_t off_png_start[] asm("_binary_off_png_start");
extern const uint8_t off_png_end[]   asm("_binary_off_png_end");

la sintassi è sempre _binary_filename_start|end, sostituendo il “.” con “_” nel nome del file. Avendo a disposizione i due puntatori (start ed end) è facile inviare l’intero contenuto del file con il metodo netconn_write() già spiegato:

netconn_write(conn, on_png_start, on_png_end - on_png_start, NETCONN_NOCOPY);

Demo

sottotitoli in italiano disponibili

[youtube id=”rkJ81r9Zpzg” width=”600″ height=”350″]

Related Posts

23 comments

John K Kovach 14 luglio 2017 - 12:55

Luca,
Great article. Thank you for your time, your generosity and the help that it gives. I have been trying to find an example that shows a web server serving a html file from memory. I would like to be able to serve a large SPA file like ReactJS from a SD memory card. Is that possible?

Reply
luca 18 luglio 2017 - 07:33

Hi John, yes it’s possible and I’m working on it! You may also consider (this will be the topic of a future post) to use a partition (FAT or SPIFFS) in the internal flash.

Reply
Marek Hlavsa 19 luglio 2017 - 20:55

Hi Luca,
You publish really great tutorials! Thanks. Regarding the webserver – is there a simple way how to get user input from the web (e.g. to retrieve content from a web form submitted). I am thinking about something like a “simple admin web console” to set-up / administer SSID, user credentials … where text input is necessary.
Thanks in advance
Marek

Reply
luca 20 luglio 2017 - 14:41

Hi Marek, yes it’s quite easy to receive (GET or POST methods) data from the user… you only need to parse the incoming request: GET parameters are added to the URL while POST parameters are added in the request body. One of my future tutorials will be about this!

Reply
Marek Hlavsa 14 agosto 2017 - 16:22

Hi Luca,
Thanks for the answer. One more thing regarding your demo – the web server is working perfectly fine when connected from android (e.g. from mobile phone), but since I connect from Windows (e.g. notebook running Win10) the pictures downloading is extremely slow (and in some cases even stops the web server on ESP32 to listen to requests and respond) – tested on 2 different Windows devices. Any idea what could be the problem (BTW – without the images it works fine even on Windows)?
Thanks in advance
Marek

Reply
lucadentella.it – ESP32 (21) – Mutua autenticazione 25 agosto 2017 - 13:16

[…] Per prima cosa dobbiamo copiare nella cartella di progetto i certificati (CA e server) e la chiave privata (server) e – includerli come text data nel programma (vi ho già mostrato come fare per le immagini del precedente esempio): […]

Reply
Tony 18 settembre 2017 - 13:41

It’s working really really well. I could not believe a http server was running on my ESP.

The keyword “static” makes my compile fail though (Cygwin GCC, ESP SDK 2.1) –I am not entirely sure why.

Reply
Héber 3 ottobre 2017 - 12:50

Hi Luca,
Thanks for all your work, this are really great tutorials. I’m trying to do a merge between two of them, the Access Point (18) and Webserver (20). I’ve tried some times, but it didn’t work. Do you have any example or material that may help me to configure it?
Thanks.

Reply
luca 3 ottobre 2017 - 15:07

Hi Héber, it’s strange… the two elements should work fine: could you share your code and/or any errors?

Reply
Héber 3 ottobre 2017 - 15:08

I’ve already find a way to make it work. thanks a lot.

Reply
Ugur 22 novembre 2017 - 16:04

Hello Luca,

Which relay did you used on this project?
I have 5v jqc-3ff-s-z relay but it did not work.

Thank you.

Reply
Ugur 22 novembre 2017 - 17:03

My mistake was connecting relay from 5v pin-out
Correct way was use relay’s vcc in 3.3v pin-out.

Reply
luca 22 novembre 2017 - 17:22

confirm, the esp32 pins run at 3.3V

Reply
chandan 12 gennaio 2018 - 21:10

Luca,
Can you please provide a sample to run this same webserver in only AP mode?

Reply
luca 13 gennaio 2018 - 15:27

Hi! Yes, it will be the topic of a future tutorial!

Reply
Erik 25 gennaio 2018 - 20:57

Hi Luca,

Great stuff and I applaud your efforts! However neither the standard web server example or SSL examples are working with the current releases of the ESP-IDF. The standard web server does not compile and results in an error:

C:/msys32/home/esp32/luca/14_basic_webserver/main/main.c:232:2: error: unknown type name ‘mdns_server_t’
mdns_server_t* mDNS = NULL;

The SSL Server compiles and I can connect to the SoftAP but trying to bring up the page in a browser results in a connection reset while trying to negotiate TLS. I tried https://192.168.10.1 as well as https://192.168.10.1/off.png just as a test. I tried in Chrome and Firefox and allowed for the invalid certificate in the browser settings.

Reply
luca 26 febbraio 2018 - 14:55

Hi Erik, unfortunately Espressif changed all the methods for mDNS… I’m updating all my examples

Reply
Woot 2 febbraio 2018 - 08:04

Dear Luca,

Your tutorial is really great.
I have question, after web server connected with WIFI AP , it working well.
But if the AP turned off then turned on again, look like esp web server not working anymore (not reconnect to the AP)
I have to reset the esp32, then it comeback working again.

Can i do for automatic reconnect after AP off and then on?

Thank you in advance.

Woot

Reply
luca 2 febbraio 2018 - 10:32

Hi, you have to manage the SYSTEM_EVENT_STA_DISCONNECTED event, see this example.

Reply
Raajesh 16 dicembre 2018 - 04:31

Hi Luca,

Thank you for all the great tutorials. I have a question. How are you able to run 5v relay from 3.3v pin without any additional changes. I am trying to run a 8 channel 5v relay, and though the LED lights up, the relay does not switch on.

Thanks
Raajesh

Reply
luca 17 dicembre 2018 - 12:08

it probably depends on the sensitivity of the relay… but you should use 3.3V relays to be sure that they switch

Reply
Francesco 28 maggio 2020 - 08:50

Ciao Luca,
complimenti per i tutorials!! Grandissimo!

Hai qualche dritta cu come implementare un client udp?

Reply
luca 1 giugno 2020 - 10:50

ciao francesco, è abbastanza semplice, guarda l’esempio fornito dal framework… non ho mai fatto un tutorial perché non ho trovato una applicazione di utilizzo.

Reply

Rispondi a Tony Cancel Reply

cinque × tre =