Comunicazione Seriale

Un primo approccio

 

 

Qui abbiamo un primo esempio che fa uso delle Web Serial API.

Questo esempio consiste in una comunicazione tra una pagina web che viene eseguita su di un broweser Chrome o Edge, entrambe abilitati a gestire queste API altri broweser potrebbero non essere abilitati, e Arduino collegato al PC mediante un cavo alla porta usb (ce funge da porta seriale). In pratica la pagina web ha due bottoni uno "Connettiti ad Arduino" ed "Invia dati". Cliccando sul primo si effettua la connessione ad Arduino, il broweser chiederà l'autorizzazione all'accesso tramite porta seriale COM3, poi cliccando sull'altro la pagina web invierà un messaggio ad Arduino che sarà in ascolto. Come riceverà il messaggio Arduino invierà alla pagina web un messaggio di risposta. Tutto qui, ma ci sono tutti gli estremi per comprendere il codice necessario.

 

 

comSer.html: esegui

  1 <!DOCTYPE html>
  2 <html lang="it">
  3 <head>
  4   <meta charset="UTF-8">
  5   <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6   <title>Comunicazione con Arduino tramite Web Serial API</title>
  7   <link rel="icon" href="https://www.cersil.it/ar/img/favicon.ico">
  8   <link href="../../../css/pag.css" rel="stylesheet" type="text/css" />
  9 </head>
 10 <body>
 11   <p class="tit2">&nbsp;</p>
 12 <p class="tit3">Comunicazione con Arduino</p>
 13   <p class="tit3">&nbsp;<span class="sotto">Tramite Web Serial API</span></p>
 14 <p class="tit2">&nbsp;</p>
 15 <p class="tit2">&nbsp;</p>
 16 <table border="0" align="center" cellpadding="0" cellspacing="0">
 17     <tbody>
 18       <tr>
 19                 <!-- Pulsanti per connettersi e inviare dati ad Arduino -->
 20         <td><button id="connectButton">Connetti ad Arduino</button> 
 21             <button id="sendDataButton" disabled>Invia dati  </button></td>
 22       </tr>
 23       <tr>
 24           <td><textarea class="corpo" id="receivedData" rows="8" cols="30" readonly>
 25                                                                     </textarea></td>
 26       </tr>
 27   </tbody>
 28 </table>
 29 <p class="tit2">&nbsp;</p>
 30   <p>&nbsp;  </p>
 31 <script>
 32     let port;
 33     let writer;
 34     let reader;
 35 
 36     // Funzione per gestire la connessione ad Arduino via Serial
 37     async function connectToArduino() {
 38       try {
 39         // Richiedi all'utente di selezionare una porta seriale
 40         port = await navigator.serial.requestPort();
 41         // Apri la porta seriale con un baud rate di 9600
 42         await port.open({ baudRate: 9600 });
 43 
 44         // Abilita il pulsante di invio dati
 45         document.getElementById('sendDataButton').disabled = false;
 46 
 47         // Inizializza il writer per inviare dati
 48         writer = port.writable.getWriter();
 49 
 50         // Inizializza il reader per leggere dati
 51         reader = port.readable.getReader();
 52 
 53         // Chiama la funzione per leggere dati in ingresso
 54         readLoop();
 55 
 56         console.log('Connessione ad Arduino riuscita!');
 57       } catch (error) {
 58         console.error('Errore durante la connessione ad Arduino:', error);
 59       }
 60     }
 61 
 62     // Funzione per leggere dati in ingresso
 63     async function readLoop() {
 64       const textDecoder = new TextDecoder();
 65       while (true) {
 66         const { value, done } = await reader.read();
 67         if (done) {
 68           console.log('[readLoop] DONE', done);
 69           reader.releaseLock();
 70           break;
 71         }
 72         // Decodifica e visualizza i dati ricevuti
 73         const receivedText = textDecoder.decode(value);
 74         console.log('Dati ricevuti da Arduino:', receivedText);
 75         document.getElementById('receivedData').value += receivedText;
 76       }
 77     }
 78 
 79     // Funzione per inviare dati ad Arduino
 80     async function sendDataToArduino() {
 81       try {
 82         // Verifica se la porta è aperta
 83         if (!port) {
 84           console.error('Arduino non è connesso.');
 85           return;
 86         }
 87 
 88         // Dati da inviare ad Arduino
 89         const data = 'Hello Arduino\n';
 90         // Converti i dati in un array di byte
 91         const dataArray = new TextEncoder().encode(data);
 92 
 93         // Invia i dati
 94         await writer.write(dataArray);
 95 
 96         console.log('Dati inviati ad Arduino:', data);
 97       } catch (error) {
 98         console.error('Errore durante l\'invio dei dati ad Arduino:', error);
 99       }
100     }
101 
102     // Aggiungi event listener ai pulsanti
103     document.getElementById('connectButton').addEventListener('click', 
104                                                              connectToArduino);
105     document.getElementById('sendDataButton').addEventListener('click', 
106                                                             sendDataToArduino);
107     </script>
108 </body>
109 </html>

 

Descrizione.

Nella paggina vengono creati due bottoni ed una textarea:

connectButton eseguirà la connessione ad Arduino

sendDataButton inizialmente disattivato quando la connessione sarà avvenuta diventera attivo e premendolo invierà i dati ad Arduino

receivedData una textarea che riceverà i messaggi scambiati

 

Entriamo nello <script>

 

32     let port;
33     let writer;
34     let reader;

Vengono dichiarate tre variabili globali

 

La prima funzione: connectToArduino()

 

 36     // Funzione per gestire la connessione ad Arduino via Serial
 37     async function connectToArduino() {
 38       try {
 39         // Richiedi all'utente di selezionare una porta seriale
 40         port = await navigator.serial.requestPort();
 41         // Apri la porta seriale con un baud rate di 9600
 42         await port.open({ baudRate: 9600 });
 43 
 44         // Abilita il pulsante di invio dati
 45         document.getElementById('sendDataButton').disabled = false;
 46 
 47         // Inizializza il writer per inviare dati
 48         writer = port.writable.getWriter();
 49 
 50         // Inizializza il reader per leggere dati
 51         reader = port.readable.getReader();
 52 
 53         // Chiama la funzione per leggere dati in ingresso
 54         readLoop();
 55 
 56         console.log('Connessione ad Arduino riuscita!');
 57       } catch (error) {
 58         console.error('Errore durante la connessione ad Arduino:', error);
 59       }
 60     }

 

async e await

In JavaScript, la parola chiave async viene utilizzata per dichiarare una funzione asincrona. Le funzioni asincrone sono utili per eseguire operazioni che richiedono del tempo, come richieste di rete, accesso a file, temporizzazioni e altre attività che non possono essere eseguite immediatamente.

Quando una funzione è dichiarata async, può utilizzare la parola chiave await per aspettare il completamento di una Promessa.

Funzione async

  • Una funzione definita con async restituisce sempre una Promessa.
  • All'interno di una funzione async, è possibile utilizzare await per sospendere l'esecuzione della funzione fino a quando una Promessa non viene risolta o rigettata.

Parola chiave await

  • La parola chiave await può essere usata solo all'interno di una funzione async.
  • await fa sì che l'esecuzione della funzione async si sospenda fino a quando la Promessa che segue await non viene risolta.
  • Se la Promessa viene risolta, await restituisce il valore risolto.
  • Se la Promessa viene rigettata, await genera l'errore.

port = await navigator.serial.requestPort(); Richiede all'utente di selezionare una porta seriale. Questo farà aprire un popup che chiederà di scegliere la porta da aprire, nel nostro caso la COM3.

a seguire apre la porta, abilita il pulsante per l'invio dei dati, inizializza writer e reader per poter scrivere e leggere

chiama la funzione readLoop(); per leggere i dati in arrivo.

Questo è nel blocco try;{} se tutto va bene nella console verrà visualizzato il messaggio di ok altrimenti catch{} invierà il messaggio di errore.

 

Funzione readLoop();

che si occupa di leggere il messaggio in arrivo da Arduino.

 

 62     // Funzione per leggere dati in ingresso
 63     async function readLoop() {
 64       const textDecoder = new TextDecoder();
 65       while (true) {
 66         const { value, done } = await reader.read();
 67         if (done) {
 68           console.log('[readLoop] DONE', done);
 69           reader.releaseLock();
 70           break;
 71         }
 72         // Decodifica e visualizza i dati ricevuti
 73         const receivedText = textDecoder.decode(value);
 74         console.log('Dati ricevuti da Arduino:', receivedText);
 75         document.getElementById('receivedData').value += receivedText;
 76       }
 77     }

 

const textDecoder = new TextDecoder();

I dati inviati e ricevuti attraverso una porta com sono sotto forma di serie di byte. Qui stiamo istaziando un'oggetto della classe TextDecoder();che con il suo metodo trasformerà i byte in stringa (decodifica)

const receivedText = textDecoder.decode(value);

 

1  // Crea una nuova istanza di TextDecoder con la codifica predefinita UTF-8
2  const textDecoder = new TextDecoder();  
3  // Un array di byte che rappresenta la stringa "Hello, World!" in UTF-8
4  const uint8Array = new Uint8Array([72, 101, 108, 108, 111, 44, 32, 87, 111,114, 108, 100, 33]);
5  // Decodifica l'array di byte in una stringa
6  constdecodedString =  textDecoder.decode(uint8Array); 
7  console.log(decodedString); // Output: Hello, World!

 

Costruttore TextDecoder

Il costruttore di TextDecoder può essere usato con o senza parametri. Se non si specifica alcun parametro, la codifica predefinita sarà UTF-8.

 

// Con codifica specificata

const textDecoder = new TextDecoder('utf-8');

 

Metodo decode

Il metodo principale di TextDecoder è decode, che prende un BufferSource (come un Uint8Array) e restituisce una stringa decodificata.

const decodedString = textDecoder.decode(uint8Array);

 

I byte del flusso vengono così sempre decodificati in una stringa. Ma se si inviassero dati numerici separati da un \n e questi dati ci servissero poi per fare dei calcoli allora dovremmo trasformare le stringhe in numeri

  1. Ricezione e Decodifica dei Dati: Ricevi i byte e decodificali in una stringa.
  2. Parsing della Stringa: Convertila in un numero utilizzando le funzioni di parsing appropriate.

Ecco un esempio pratico:

 

1    async function readLoop() {
2     const textDecoder = new TextDecoder();
3     while (true) {
4       const { value, done } = await reader.read();
5       if (done) {
6         console.log('[readLoop] DONE', done);
7          reader.releaseLock();
8         break;
9        }
10    
11       // Decodifica il chunk di dati ricevuto in una stringa
12       const receivedText = textDecoder.decode(value);
13   
14        // Supponiamo che i dati siano numeri separati da nuove linee
15       const numbers = receivedText.split('\n').filter(Boolean).map(Number);
16    
17        // Esegui operazioni matematiche sui numeri
18      numbers.forEach(num => {
19          console.log('Numero ricevuto:', num);
20        console.log('Numero al quadrato:', num * num);
21        });
22   
23       // Aggiorna la textarea con i dati ricevuti (per esempio)
24        document.getElementById('receivedData').value += receivedText;
25     }
26   }

 

Spiegazione del codice

  1. Ricezione dei dati: const { value, done } = await reader.read();
  2. Decodifica dei dati: const receivedText = textDecoder.decode(value);
  3. Parsing della stringa:
    • receivedText.split('\n') divide la stringa in un array di sottostringhe separate da \n.
    • .filter(Boolean) rimuove eventuali stringhe vuote dall'array.
    • .map(Number) converte ogni stringa in un numero. Se una stringa non può essere convertita, il risultato sarà NaN (Not a Number).
  4. Operazioni matematiche: Itera su ogni numero e esegue operazioni matematiche, come calcolare il quadrato del numero.

Note aggiuntive

  • Parsing: Se i numeri sono in un formato specifico (es. float con separatore decimale), puoi usare funzioni più specifiche come parseFloat per numeri in virgola mobile o parseInt per numeri interi.
  • Gestione degli errori: È buona pratica controllare se il parsing è avvenuto correttamente, ad esempio controllando se num è NaN.

Esempio di gestione degli errori:

 

1 numbers.forEach(num => {
2    if (!isNaN(num)) {
3      console.log('Numero ricevuto:', num);
4      console.log('Numero al quadrato:', num * num);
5    } else {
6      console.error('Errore di parsing:', num);
7    }
8 });

 

Quando ricevi una sequenza di numeri che può contenere sia interi che numeri in virgola mobile (double), puoi utilizzare parseFloat per effettuare la conversione. La funzione parseFloat è in grado di gestire sia interi che numeri con la virgola.

 

------------------------------------------------------

 

const { value, done } = await reader.read();

In JavaScript, il codice sopra utilizza la destructuring assignment per estrarre i valori di proprietà da un oggetto restituito dalla chiamata reader.read();

Destructuring Assignment

La destructuring assignment è una sintassi di JavaScript che permette di estrarre valori da array o proprietà da oggetti in variabili distinte.

 

reader.read();

Nel contesto delle API di stream, come la Web Serial API o altre API di lettura di stream, reader.read() è un metodo che legge un "chunk" (pezzo) di dati dallo stream. Questo metodo restituisce una Promessa che si risolve con un oggetto di tipo { value, done }. e rimane in attesa await finchè non si conclude.

 

value e done

  • value: Contiene i dati letti dallo stream. Può essere un Uint8Array, una stringa o qualsiasi altro tipo di dato che lo stream stia fornendo.
  • done: È un valore booleano che indica se lo stream ha terminato la lettura. done sarà true quando non ci sono più dati da leggere nello stream.

Immagginiamo di aver creato una classe: MyClasse che ha due proprietà: propuno e propdue più un metodo: metodtre(); normalmente istanziamo un oggetto della classe MyClasse: const obj = new MyClass(); per leggere le proprietà di questo oggetto dovremo fare: const p1=obj.propuno; e p2=obj.propdue; per eseguire il metodo: pbj.metodtre(); invece potremmo estrarre tutte e tre in una sola volta così. {propuno, propdue, metodtre}=obj; comodo no.

 

Ecco un esempio:

 

1   class MyClass{
2     constructor(propuno, propdue) {
3       this.propuno = propuno;
4       this.propdue = propdue;
5     }
6  
7    metod() {
8      console.log("Eseguo il metodo.");
9    }
10  }
11  
12  const obj = new MyClass("valore1", "valore2");
13  
14  // Estrarre le proprietà dell'oggetto e il metodo
15  const { propuno, propdue, metod: metodoOggetto } = obj;
16  
17  console.log(propuno); // Output: valore1
18  console.log(propdue); // Output: valore2
19  
20  // Eseguire il metodo, mantenendo l'oggetto come contesto
21  metodoOggetto(); // Output: Eseguo il metodo.

 

 

Esempio di lettura di uno stream:

 

1  async function readStream() {
2    // Supponiamo che reader sia stato già inizializzato
3    while (true) {
4      const { value, done } = await reader.read();
5      if (done) {
6        console.log('Stream terminato');
7        break;
8      }
9      console.log('Chunk di dati ricevuto:', value);
10    }
11  }

 

Il ciclo while con true si ripete sempre. Ad ogni ciclo reader.read(); invia un'oggetto che contiene il chunck di dati in value e true o false in done. Se il messaggio o il flusso di dati fosse molto lungo costituito ad es. da tre chunck, con il primo ciclo reader.read(); invia il primo chunk e manda in done false perchè il flusso ancora non è terminato, così pure il secondo ciclo, ma al terzo invia l'ultimo chunk ma essendo terminato il flusso invierà in done true, questo farà uscire da while con break;

 

Ed infine la Funzione per inviare dati ad Arduino: async function sendDataToArduino() {...

Questocodice è intuitivo. Fa esattamente l'opposto di async function readLoop() {...

 

Funzione sendDataToArduino();

che si occupa di scrivere sulla porta seriale:

 

 79     // Funzione per inviare dati ad Arduino
 80     async function sendDataToArduino() {
 81       try {
 82         // Verifica se la porta è aperta
 83         if (!port) {
 84           console.error('Arduino non è connesso.');
 85           return;
 86         }
 87 
 88         // Dati da inviare ad Arduino
 89         const data = 'Hello Arduino\n';
 90         // Converti i dati in un array di byte
 91         const dataArray = new TextEncoder().encode(data);
 92 
 93         // Invia i dati
 94         await writer.write(dataArray);
 95 
 96         console.log('Dati inviati ad Arduino:', data);
 97       } catch (error) {
 98         console.error('Errore durante l\'invio dei dati ad Arduino:', error);
 99       }
100     }
101 
102     // Aggiungi event listener ai pulsanti
103     document.getElementById('connectButton').addEventListener('click', 
104                                                              connectToArduino);
105     document.getElementById('sendDataButton').addEventListener('click', 
106                                                             sendDataToArduino);

 

Si commenta da solo.

 

Cosa fa arduino?

 

Codice Arduino:

void setup() {
  // Inizializza la comunicazione seriale a 9600 baud
  Serial.begin(9600);
}

void loop() {
  // Controlla se ci sono dati disponibili sulla porta seriale
  if (Serial.available()) {
    // Leggi i dati dalla porta seriale fino al carattere di nuova riga
    String data = Serial.readStringUntil('\n');
    // Rimuovi eventuali spazi vuoti all'inizio e alla fine della stringa
    data.trim();
    // Stampa i dati ricevuti sulla porta seriale
    Serial.println("Dati ricevuti: " + data);

    // Invia una risposta ad Arduino
    String response = "Risposta: OK";
    Serial.println(response);
  }
}

 

Si commenta da se.