Dopo aver imparato come collegare il chip esp32 alla nostra rete wifi, oggi vediamo come inviare e ricevere dati.
lwIP
Il framework esp-idf utilizza la libreria lwIP per implementare lo stack di protocolli TCP/IP. Questa libreria, inizialmente sviluppata da Adam Dunkels e ora mantenuta da una community di sviluppatori con licenza opensource, è molto utilizzata in ambito embedded per le sue ridotte dimensioni e per le numerose features:
[checklist]
- supporta i principali procotolli dello stack: IP, ICMP, UDP, TCP, IGMP, ARP, PPPoS, PPPoE
- include diversi clients: DHCP, DNS, SNMP…
- offre diversi livelli di astrazione: raw API, sequential API e BSD-style API
[/checklist]
In rete sono disponibili diverse risorse (tutorial, manuali…) relativi all’utilizzo di tale libreria; un buon documento introduttivo, sebbene non riferito al chip ESP32, è quello di Atmel: AT04055 – Using the lwIP Network Stack.
La libreria lwIP si trova all’interno della cartella components/lwip del framework. Il collegamento tra le funzioni core del framework e la libreria lwIP è contenuto nel codice tcpip_adapter:
HTTP client
In questo tutorial vedremo un primo esempio, molto semplice, di comunicazione TCP: l’implementazione di un client HTTP. Il protocollo HTTP (Hypertext Transfer Protocol) è il “linguaggio” parlato dai siti web; il client HTTP che andremo a sviluppare si comporterà quindi come un browser:
- apre una connessione verso un server che ospita un sito web
- invia la richiesta per una risorsa (pagina HTML, immagine…)
- riceve la risposta e la stampa a video (via seriale)
Vediamo le fasi nel dettaglio; il sorgente completo dell’esempio è disponibile nel mio repository su Github.
Connessione TCP
Nella prima fase, il client effettua una connessione, utilizzando il protocollo TCP, al server. Un server può accettare diverse connessioni in ingresso: per distinguere i vari servizi che il server offre, il protocollo TCP utilizza il concetto di porta, un identificativo numerico che va da 0 a 65535. Alcuni servizi utilizzano porte standard: i siti web sono normalmente “esposti” sulla porta 80 (o 443 se utilizzano la versione cifrata del protocollo, HTTPS).
Applicativamente la connessione verso un server viene rappresentata da un socket: possiamo immaginare il socket come un “tubo” attraverso il quale transitano i dati che scambiamo con il server al quale siamo collegati.
Dopo aver configurato l’interfaccia di rete come abbiamo imparato nei precedenti tutorial (in particolare è il comando tcpip_adapter_init() a inizializzare la libreria lwIP) dobbiamo quindi creare un nuovo socket:
const struct addrinfo hints = { .ai_family = AF_INET, .ai_socktype = SOCK_STREAM, }; int s = socket(res->ai_family, res->ai_socktype, 0); |
Nel nostro caso il socket s utilizzerà il protocollo TCP (SOCK_STREAM) della “famiglia” di protocolli IP (AF_INET). I protocolli che lwIP supporta sono definiti nel file sockets.h:
Il terzo parametro (0 nel nostro caso), viene utilizzato per specificare l’ID del protocollo solo in caso di SOCK_RAW.
Se la chiamata va a buon fine, la variabile s rappresenta l’identificativo numerico del nuovo socket creato. In caso di errore invece viene restituito -1.
Una volta ottenuto un socket, è possibile effettuare la connessione al server remoto con la funzione:
connect(int socket, const struct sockaddr *address, socklen_t size) |
che ha come parametri l’identificativo del socket da utilizzare, la struttura (sockaddr) che contiene le informazioni sull’indirizzo del server remoto e la dimensione di tale struttura.
Normalmente si conosce il nome del server a cui ci si vuole collegare, non direttamente il suo indirizzo IP. La conversione tra nome e indirizzo IP avviene tramite il servizio DNS (Domain Name System). Possiamo utilizzare la funzione getaddrinfo per interrogare i server DNS:
struct addrinfo *res; int result = getaddrinfo(CONFIG_WEBSITE, "80", &hints, &res); |
Se la risoluzione DNS ha esito positivo, la struttura res contiene tutte le informazioni necessarie per effettuare la connessione:
result = connect(s, res->ai_addr, res->ai_addrlen); |
Richiesta HTTP
Una volta aperta la connessione, possiamo inviare al server la richiesta per la risorsa che vogliamo ottenere. Una semplice richiesta HTTP ha la seguente forma:
GET risorsa HTTP/1.1 Host: sitoweb
La richiesta utilizza il metodo GET, specifica il percorso della risorsa, la versione del protocollo e il nome del sito web a cui la risorsa appartiene (necessario se il server ospita più siti web). Una cosa molto importante da sapere è che ogni richiesta termina con una riga vuota.
Ad esempio utilizziamo una pagina sul mio sito che in maniera randomica restituisce un aforisma (ho già usato questa pagina in un tutorial relativo al chip enc28j60):
Ho sottolineato il nome del sito e cerchiato il percorso della risorsa. La richiesta da inviare al server per ottenere tale risorsa sarà quindi:
GET /demo/aphorisms.php HTTP/1.1 Host: www.lucadentella.it
Nell’esempio che trovate su Github ho parametrizzato (via menuconfig) sia il sito web che la risorsa, quindi la costante che contiene la richiesta da inviare ha la forma:
// HTTP request static const char *REQUEST = "GET "CONFIG_RESOURCE" HTTP/1.1\n" "Host: "CONFIG_WEBSITE"\n" "User-Agent: ESP32\n" "\n"; |
Ho aggiunto un ulteriore parametro, non obbligatorio: lo User-Agent. Tramite questo parametro è possibile indicare al server di destinazione quale applicativo sta inviando la richiesta (nome del browser, sua versione…). Questa informazione è utilizzata sia a fini statistici, sia per adattare il sito web alle caratteristiche/capacità del programma che lo visualizzerà.
L’invio della richiesta al server avviene con la funzione write:
result = write(s, REQUEST, strlen(REQUEST)); |
Risposta
E’ possibile ricevere dati da un socket tramite la funzione read():
char recv_buf[100]; [...] do { bzero(recv_buf, sizeof(recv_buf)); r = read(s, recv_buf, sizeof(recv_buf) - 1); for(int i = 0; i < r; i++) { putchar(recv_buf[i]); } } while(r > 0); |
La funzione read riceve – se disponibili – dei dati dal socket, li memorizza nel buffer indicato come parametro e restituisce il numero di bytes letti.
Demo
Ecco uno screenshot dell’esempio in funzione:
Ciao Luca,
innanzitutto complimenti per l’ottimo blog, chiaro ed esaustivo.
Ti chiedo un aiuto, è da poco che “gioco” con la ESP32. Devo realizzare una connessione peer-to-peer tra due ESP32 usando wifi o bluetooth, il requisito che è si scambino dei semplici comandi (es. “comando1”, “comando2” ecc.) con un tempo di attivazione molto ridotto, poche decine di millisecondi, da quando quella denominata master riceve un input da GPIO.
Qual è la soluzione migliore? Hai un esempio di aiuto?
Grazie
Ciao Omar e grazie per i complimenti, fanno sempre piacere 😉 Per la tua applicazione vanno bene entrambi i protocolli… penso che l’uso del wifi sia più semplice (niente pairing…). Sto preparando proprio un tutorial sull’uso delle netconn API di lwip per connessioni socket e un altro sulla modalità Access Point di ESP32 che ti saranno sicuramente utili 😉
Ottima notizia! Spero di leggerlo presto allora.
Lo pubblicherai sul blog e su Github?
Grazie
sicuramente 😉 nelle prossime settimane
Ciao Luca,
Adoro i tuoi tutorial!!
Solo ho qualche piccolo problemino con questo programma…
Ho provato con google e con il tuo sito demo degli aforismi, ma continua a darmi “Unable to connect to the target website”
Inoltre ho dovuto disattivare il brownout sensor perché continuava a resettarsi, ma immagino questo sia un problema della scheda xD
Thanks Luca for your tutors,
got request errors till I added some \r
static const char *REQUEST = “GET “WEB_RESOURCE” HTTP/1.1\r\n”
“Host: “WEB_SITE”\r\n”
“User-Agent: ESP32\r\n”
“\r\n”;
Thanks Marius, you’re right: HTTP specs require \r\n but some servers also accept \n.
Thanks for your tutorials, I hope you will continue writing them for a long time.
The first flashing of this tutorial gave me a panic error. Looking in tutorial 02 I noticed that there were a NVS initialization. That solved the panic error.
Also I got Bad Request until I changed \n for \r\n.
ciao Luca,
Grazie per i tuoi tutorial!! sono un beginer con l hardware. vorrei stabilire una connessione wifi tra il mio pc e esp32 per trasferire dati ( wifi scanner ) raccolti dal esp32 collegando il mio esp32 direttamente sulla presa ( non sul pc). ho provato di usare il tutorial 8 ma mi sono perso.
grazie in anticipo per una soluzione.
Ciao Briand… prova a vedere se il tutorial su mqtt fa al caso tuo!
I had to add “ESP_ERROR_CHECK(nvs_flash_init());”
to make it work. Thanks for the amazing tutorial !
Hello, When will you cover Websockets