Arduino ed Internet

Da GolemWiki.
Versione del 1 giu 2018 alle 22:17 di Giomba (discussione | contributi) (Risistemata, riscritta la parte sulle reti in maniera semplificata, eliminati imprecisioni e errori, tuttavia rimane un "corso molto molto accelerato sulle reti")
(diff) ← Versione meno recente | Versione attuale (diff) | Versione più recente → (diff)


Ci sono applicazioni per cui è comodo avere Arduino collegato in rete, ad esempio per un sistema di domotica, cioè di controllo intelligente della casa: per controllare le luci da remoto, per accendere il riscaldamento o controllare la temperatura dall'ufficio o dal cellulare.

Per questi piccoli progetti è adatta la Ethernet Shield, una scheda già pronta per essere montata su un Arduino e che mette a disposizione:

  • una porta Ethernet controllata tramite un chip dedicato, per la connettività in rete;
  • uno slot per microSD, utile per salvare le informazioni catturate (monitor);

Esistono anche delle wireless shield, molto simili, che però usano le reti senza fili, tra cui WiFi.

Ma attenzione: questa scheda dipende molto dalla potenza del microprocessore di Arduino, poiché è lui che deve occuparsi, come vedremo, di diversi protocolli di rete.

Dimenticatevi quindi di poter tirare su in questo modo un sito web con tante pagine, database, eccetera: per questo serve un computer ad uso generale, per quanto piccolo, ad esempio un Raspberry Pi, una BeagleBone o un Olinuxino, anziché Arduino.

Concetti di base sulle reti

In questa sezione sono introdotti alcuni concetti fondamentali sulle reti, che è bene aver chiaro prima di cimentarsi nella realizzazione di dispositivi di rete.

Alcuni concetti sono stati volutamente trascurati o omessi per semplicità di trattazione (e si vedrà che già così la strada è lunga).

Si consiglia comunque di leggere la parte di teoria anche a coloro che già la sanno, a mo' di ripasso, dopodiché passare alla pratica.

Golem-template-note-warning.png I concetti espressi nella seguente trattazione non sono sufficienti per la creazione di dispositivi sicuri, perciò è bene utilizzarli solo come punto di partenza per la realizzazione di prototipi, e successivamente approfondire. Potrebbe essere molto fastidioso - o addirittura pericoloso - che qualcuno possa, dalla rete Internet, accendere la nostra caldaia mentre non siamo a casa o spiare i nostri familiari.


Teoria

Livelli di astrazione

Livelli di astrazione nelle reti

Tutti i dispositivi collegati in rete devono rispettare certe regole per potersi identificare, e per poter parlare fra loro senza creare conflitti, così come ogni persona ha un proprio nome, e segue delle regole durante una conversazione (es. non parla mentre altri stanno già parlando).

Per avere connettività, è stato pensato uno schema che permette la connettività a 7 diversi livelli.

Ne descriveremo 5, fondamentali per capire il funzionamento, ma ci preoccuperemo solo dei "dettagli" dei 3 a più alto livello.

Livello fisico

Non ci interesseremo del livello fisico, basta sapere che - com'è ovvio immaginare - una rete cablata su Ethernet è ben diversa da una rete senza fili WiFi già a partire dal mezzo fisico in cui si propagano le informazioni.

Livello di collegamento

Permette ai dispositivi in una (piccola) rete di parlare tra di loro.

A questo livello appartiene il MAC Address, un indirizzo univoco per ogni dispositivo esistente. Esso viene assegnato in fabbrica ad ogni dispositivo e non cambia mai.

Livello di rete

Permette ai dispositivi di parlare non solo con i dispositivi della stessa rete, ma anche con quelli di reti diverse.

A questo livello appartiene l'indirizzo IP, un indirizzo univoco nella rete.

IP vs MAC

Perché c'è bisogno di due indirizzi, un MAC e un IP, per presentarsi in rete? Non sarebbe sufficiente il MAC che è già univoco di suo?

I due indirizzi hanno uno scopo e una struttura completamente diversa:

  • l'indirizzo MAC (Media Access Control) serve per permettere la comunicazione in presenza di più dispositivi, serve cioè per identificarli. Ogni produttore assegna gli indirizzi MAC ai propri dispositivi facendo uso di un registro in comune con gli altri produttori, in modo da evitare che due dispositivi diversi possano avere lo stesso indirizzo. Come i codici fiscali, anche gli indirizzi MAC quindi sono molto variegati tra loro, e non danno alcuna informazione su come raggiungere il dispositivo, ma si limitano ad identificarlo.
  • l'indirizzo IP (Internet protocol) serve invece per permettere la comunicazione tra dispositivi di reti diverse, per trovare la loro collocazione, e per creare reti logiche di dispositivi, raggruppandone un sottoinsieme. Come gli indirizzi geografici, anche gli indirizzi IP di dispositivi "vicini" tra loro sono simili (cioè di dispositivi che si trovano nella stessa rete), e forniscono una chiara identificazione di dove si trovano.

Un dispositivio con indirizzo MAC AA:BB:CC:DD:EE:FF lo conserverà per sempre, ma potrà cambiare il suo indirizzo IP da 192.0.2.100 a 192.168.0.100 a seconda della rete in cui si trova.

Si rifletta sull'analogia (MAC = codice fiscale) e (IP = indirizzo geografico).

Livello di trasporto

Il livello di rete permette di trasportare i dati da un dispositivo ad un altro, ma, quando questi dati sono giunti al dispositivo di destinazione, a chi vanno consegnati? Se un dispositivo esegue più applicazioni di rete, a quale applicazione devo dare questi dati? Si pensi al computer, che permette di navigare sul web e contemporaneamente vedere un film in streaming: quando i dati giungono al computer, come fa a sapere se sono un pezzo di film o una pagina web?

Questo problema viene risolto dal livello di trasporto.

Il protocollo TCP che utilizziamo si occupa anche del controllo di trasmissione, cioè di rendere affidabile la comunicazione dati in rete tra mittente e destinatario: farà di tutto per impedire che i dati vengano persi o giungano corrotti a destinazione, eventualmente trasmettendoli più volte. TCP si occupa anche di controllare il flusso della trasmissione e evitare congestione all'interno della rete.

Una caratteristica importante di TCP è il concetto di porta, un numero che permette di distinguere i diversi processi a cui sono destinati i dati trasmessi. Un Arduino, pur eseguendo un solo compito, dovrà comunque offrire il suo servizio rimanendo in ascolto dietro una certa porta.

Livello applicazione

Una volta giunti al dispositivo giusto e al servizio giusto, i dati vengono finalmente letti dal nostro programma, secondo un altro protocollo detto di livello applicativo. Dovremo preoccuparci di scrivere le azioni che deve eseguire il protocollo nel nostro sketch di Arduino.

Il protocollo che useremo è HTTP (Hyper Text Transfer Protocol), che sta alla base del web.

Golem-template-note-info.png Ogni livello realizza un'astrazione, ossia può essere utilizzato senza doversi preoccupare di come funzionino i livello sottostanti. Per esempio, se abbiamo in casa uno smartphone collegato tramite WiFi e un computer fisso tramite Ethernet, se ci preoccupiamo di farli dialogare a livello di rete, ci basta conoscere i loro indirizzi IP senza doversi preoccupare dei livelli sottostanti (collegamento e fisico) che sono realizzati con due tecnologie differenti.


Pratica

MAC Address

La libreria per lo shield necessita dell'indirizzo MAC del dispositivo, che è solitamente scritto su un adesivo sotto l'Ethernet Shield. Ha una forma del tipo d8:ab:cd:ef:03:69

IP Address

La libreria necessita anche dell'indirizzo IP del dispositivo. Questo viene solitamente scritto nella forma 1.2.3.4, dove ogni numero rappresenta un byte (e quindi può assumere valori che vanno da 0 a 255). Ricordiamo che l'IP cambia a seconda della rete in cui questo è inserito: pertanto, può darsi che debba essere modificato nello sketch quando ci si sposta tra casa, ufficio, officina.

Come scegliere l'indirizzo IP?

Chi è che dà gli indirizzi in una rete?

Teoricamente ogni dispositivo può autoassegnarsi un indirizzo, ma questo non viene fatto su oggetti "quotidiani" (cellulari, computer, etc..) perché verrebbero a crearsi altrimenti conflitti quando due o più si assegnano lo stesso indirizzo. C'è quindi un computer dedicato che esegue il protocollo DHCP Dynamic Host Configuration Protocol che ha l'autorità di dare ad ognuno il suo indirizzo evitando i conflitti; nelle reti domestiche questo compito viene svolto dal modem del gestore telefonico.

Il server DHCP dispone di un insieme di indirizzi da cui pescarne a caso uno libero da assegnare a chi ne faccia richiesta. Nelle reti domestiche, questo insieme è in genere nell'intervallo 192.168.0.2-192.168.0.254 oppure 192.168.1.2-192.168.1.254.

Per i dispositivi che si collegano a servizi offerti da altri su Internet non ci interessa che abbiano un IP particolare, e quindi lo possono anche chiedere al modem col meccanismo del DHCP: ogni volta che si collegano, può essergli assegnato un indirizzo diverso: si dice che hanno un IP dinamico.

I dispositivi a cui invece ci dobbiamo collegare, che sono essi stessi fornitori di servizi, dovrebbero avere sempre lo stesso IP, noto e indipendente dalle volontà del server DHCP, così da poter essere facilmente raggiungibili: si dice che hanno un IP statico.

Anche il nostro Arduino dovrà avere un IP statico, che gli assegneremo nel programma.

Prima però dovremo assicurarci di due cose:

  • quale rete di IP è in uso nella propria rete domestica;
  • se l'IP scelto è disponibile, ovvero se il server DHCP non l'ha già assegnato a qualcuno;

Da un computer connesso, vediamo la rete:

Su Linux:

$ ip addr
1: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
   link/ether aa:bb:cc:dd:ee:ff brd ff:ff:ff:ff:ff:ff
   inet 192.168.0.6/24 brd 192.168.0.255 scope global noprefixroute eth0
      valid_lft forever preferred_lft forever
   inet6 fe80::aabb:ccff:fedd:eeff/64 scope link 
      valid_lft forever preferred_lft forever

Su Windows:

> ipconfig
Connection-specific DNS Suffix  . : golem.linux.it.
IP Address. . . . . . . . . . . . : 192.168.0.6
Subnet Mask . . . . . . . . . . . : 255.255.255.0
Default Gateway . . . . . . . . . : 192.168.0.1

In questi casi la rete è 192.168.0.0.

Sempre dal computer connesso, proviamo a contattare un indirizzo a caso per vedere se è già stato assegnato. Per scegliere un indirizzo, basterà scegliere casualmente un numero da inserire al posto dell'ultimo byte. (Questo è vero solo se la maschera di rete è 255.255.255.0 o /24, come nella totalità delle reti domestiche, come nell'esempio).

Supponiamo di aver scelto 192.168.0.80.

ping 192.168.0.80

Se il comando risponde Destination Host Unreachable, allora l'indirizzo è libero e possiamo usarlo; altrimenti è già occupato e occorre sceglierne un altro.

Golem-template-note-warning.png Il fatto che l'indirizzo sia libero in questo momento non dà nessuna garanzia sul fatto che lo sarà anche in futuro: il server DHCP, completamente ignaro dei vostri esperimenti, potrebbe decidere di assegnare quell'indirizzo a qualche nuovo dispositivo che si collega, per esempio allo smartphone di un vostro amico che è venuto a farvi visita. E quando lo inviterete a collegarsi al vostro sensore perfettamente funzionante con Arduino, non funzionerà niente e farete una brutta figura. Perciò, è bene modificare le impostazioni del server DHCP sul vostro modem e ridurre l'intervallo degli indirizzi liberi, per esempio 192.168.0.2-192.168.0.127, e assegnare al vostro Arduino un indirizzo al di fuori, per esempio 192.168.0.128.


Servizi e Servitori

Quando ci colleghiamo ad un server, questo può offrire diversi servizi: trasferimento files (FTP), posta elettronica (POP3, IMAP, SMTP), e tanti altri. Uno stesso computer può offrire più servizi contemporaneamente e, per poter discriminare a chi mandare cosa, il protocollo TCP prevede una struttura a porte. Ad ogni servizio è associato un numero di porta.

Per esempio, quando con un browser si richiede una pagina web, si bussa alla sua porta TCP numero 80, e tramite quella porta fluisce la comunicazione.

Metteremo il nostro Arduino in grado di poter rispondere alle richieste fatte sulla porta 80, così basterà collegarsi ad esso con un comune browser per poterci interagire.

HTTP

Dopo che il browser ha inviato la richiesta di connessione al server, incomincia uno scambio di informazioni fra i due che gli permette di accordarsi sul tipo di dati che si stanno scambiando.

In particolare, il browser invia la richiesta della pagina web, seguita da una linea bianca per indicare il termine:

GET /sensore.html HTTP/1.1
Host: 192.168.0.80

e il server, cioè il nostro Arduino, gli deve rispondere con un codice di approvazione o di errore (a seconda se la pagina è disponibile o no), specificando il formato e la lunghezza della pagina (testo semplice di 3 bytes nel nostro caso) e accodare finalmente il contenuto:

HTTP/1.1 200 OK
Content-Length: 3
Content-Type: text/plain; charset=utf-8

497

Interrogando il nostro Arduino con un browser all'indirizzo http://192.168.0.80/ vedremo quindi 497.

Questo può essere visto nel dettaglio direttamente nel seguente codice Arduino, in particolare lo scambio di dati: attivando la trasmissione USB si vede in diretta tutta la comunicazione HTTP fra lui e il browser.

Si osservi che in realtà questo codice non è strettamente conforme al protocollo HTTP, in quanto esegue nessun controllo sulla pagina che viene richiesta (risponde sempre con la stessa pagina), né tantomeno specifica la lunghezza dei dati (chiude brutalmente la connessione dopo che ha trasmesso tutti i dati).

Il codice Arduino

Il codice altro non è che l'esempio EthernetServer di Arduino, leggermente modificato e commentato in italiano. In linea di massima questo programma resta in attesa di una richiesta HTTP, dopodiché legge il valore analogico del pin Analog0 e lo trasmette al client connesso. Per avere letture sensate bisognerà collegare un sensore analogico, che in questo esempio è stato simulato da un semplice potenziometro.

#include <SPI.h>
#include <Ethernet.h>

// Pin del sensore da monitorare via Ethernet
const byte PIN_SENSORE = A0;

// Qui va il MAC address della tua shield
byte mac[] = {
  0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED
};

// E qui l'indirizzo IP che dovrà essere associato
IPAddress ip(192, 168, 1, 177);

// Inizializza il "server" EthernetServer con la porta 80 (HTTP)
EthernetServer server(80);

void setup() {
  // Apre la comunicazione seriale: è utile per monitorare via USB
  // cosa sta succedendo
  Serial.begin(9600);
  
  
  // Avvia la connessione ethernet ed il server
  Ethernet.begin(mac, ip);
  server.begin();
  
  // Messaggi di controllo inviati via USB
  Serial.print("Il server Arduino risponde all'indirizzo ");
  Serial.println(Ethernet.localIP());
}


void loop() {
  // "ascolta" sulla porta 80 in attesa di un client
  EthernetClient client = server.available();
  
  // se un client si è connesso...
  if (client) {
    Serial.println("Un nuovo client!");
    
    // una richiesta HTTP termina con una linea bianca (un doppio a-capo)
    // serve quindi una variabile per ricordare se si è già ricevuto un a-capo
    boolean lineaCorrenteBianca = true;
    
    // Finché il client è connesso avviene lo scambio dei dati
    while (client.connected()) {
      if (client.available()) {
        // Leggo un byte trasmesso
        char c = client.read();
        
        Serial.write(c);
        
        // se avevo già ricevuto un a-capo (lineaCorrenteBianca == true)
        // e ricevo un nuovo a-capo ('\n') è finita la richiesta del client
        // e posso inviare la pagina web
        if (c == '\n' && lineaCorrenteBianca) {
          // invio la risposta HTTP standard
          client.println("HTTP/1.1 200 OK");
          client.println("Content-Type: text/html");
          // Chiudo la connessione al termine della tramsissione
          client.println("Connection: close");
          // Aggiorna la pagina ogni 5 secondi
          client.println("Refresh: 5");
          client.println();
          
          /* ****** Qui comincia la pagina HTML vera e propria ****** */
          client.println("<!DOCTYPE HTML>");
          client.println("<html>");
          
          int lettura_sensore = analogRead(PIN_SENSORE);
          client.print("Il valore della lettura del pin ");
          client.print(PIN_SENSORE);
          client.print(" è ");
          client.print(lettura_sensore);
          
          client.println("</html>");
          /* ******  Termine della pagina HTML ****** */

          // break esce dal ciclo while, perché lo scambio dati è terminato
          break;
        }
        // Se ricevo un a-capo devo ricordare che ho iniziato una nuova riga
        if (c == '\n') {
          lineaCorrenteBianca = true;
        }
        // Se invece ricevo tutti gli altri caratteri, fatta eccezione per
        // '\r', significa che la richiesta del client non è ancora terminata
        else if (c != '\r') {
          lineaCorrenteBianca = false;
        }
      }
    }
    // piccola pausa per la trasmissione, dopodiché si chiude la connessione
    delay(1);
    client.stop();
    Serial.println("client disconnesso!");
  }
}