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.
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"> </p>
12 <p class="tit3">Comunicazione con Arduino</p>
13 <p class="tit3"> <span class="sotto">Tramite Web Serial API</span></p>
14 <p class="tit2"> </p>
15 <p class="tit2"> </p>
16 <table border="0" align="center" cellpadding="0" cellspacing="0">
17 <tbody>
18 <tr>
19
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"> </p>
30 <p> </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
- Ricezione e Decodifica dei Dati: Ricevi i byte e decodificali in una stringa.
- 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
- Ricezione dei dati: const { value, done } = await reader.read();
- Decodifica dei dati: const receivedText = textDecoder.decode(value);
- 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).
- 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.
|