ESP32 (8) – connessione tcp

luca 30/01/2017 6

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:

  • 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

 

lwip

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:

lwip-01

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)

lwip-02

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:

lwip-03

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

lwip-04Ho 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:

esp-tcp01

6 Comments »

  1. Omar Morando 09/05/2017 at 08:17 - Reply

    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

    • luca 09/05/2017 at 16:21 - Reply

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

  2. Omar Morando 11/05/2017 at 08:37 - Reply

    Ottima notizia! Spero di leggerlo presto allora.
    Lo pubblicherai sul blog e su Github?
    Grazie

    • luca 11/05/2017 at 09:30 - Reply

      sicuramente ;) nelle prossime settimane

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