ESP32 (20) – Webserver

luca 08/07/2017 10

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 <risorsa>
[...]

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:

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

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

10 Comments »

  1. John K Kovach 14/07/2017 at 12:55 - Reply

    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?

    • luca 18/07/2017 at 07:33 - Reply

      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.

  2. Marek Hlavsa 19/07/2017 at 20:55 - Reply

    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

    • luca 20/07/2017 at 14:41 - Reply

      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!

  3. Marek Hlavsa 14/08/2017 at 16:22 - Reply

    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

  4. Tony 18/09/2017 at 13:41 - Reply

    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.

  5. Héber 03/10/2017 at 12:50 - Reply

    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.

    • luca 03/10/2017 at 15:07 - Reply

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

    • Héber 03/10/2017 at 15:08 - Reply

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

Leave A Response »

Questo sito usa i cookie per poterti offrire una migliore esperienza di navigazione maggiori informazioni

Questo sito utilizza i cookie per fonire la migliore esperienza di navigazione possibile. Continuando a utilizzare questo sito senza modificare le impostazioni dei cookie o clicchi su "Accetta" permetti al loro utilizzo.

Chiudi