Archivi categoria: i miei allievi

Configurare Arduino UNO R4 WiFi per Arduino Cloud

Nelle prossime settimane svolgeremo una serie di attività di laboratorio dedicate alla realizzazione di semplici applicazioni IoT con Arduino Cloud, utilizzando come scheda di riferimento Arduino UNO R4 WiFi. Per lavorare in modo ordinato ed efficace, questa guida introduttiva nasce con un obiettivo preciso: accompagnare gli studenti passo dopo passo nella configurazione della scheda e nel collegamento al Cloud, così da essere operativo fin da subito.

Queste attività sono importanti perché ci permetteranno di passare dal classico circuito “che funziona sul banco” a un sistema realmente utile e osservabile: potremo monitorare da computer o da dispositivi mobili lo stato dei sensori (temperatura, luce, presenza, umidità, ecc.), visualizzare i dati su dashboard, registrare misure nel tempo e, quando necessario, intervenire da remoto attivando uscite e comandi (ad esempio LED, relè o attuatori). In altre parole, costruiremo piccoli prototipi che riproducono lo stesso schema operativo di molte soluzioni industriali e domotiche: sensore > rete > dashboard > controllo, con attenzione a affidabilità, leggibilità dei dati e sicurezza di base.

Questa guida è tratta dalla guida ufficiale su cui ho apportato alcune mie modifiche e le ho rese “più didattiche”.

Di seguito dettaglio la procedura essenziale per portare Arduino UNO R4 WiFi “online” collegandola all’Arduino Cloud.

Arduino UNO R4 WiFi, come già dettagliato in precedenti tutorial, integra un modulo radio dedicato (ESP32-S3) che mette a disposizione la connettività WiFi: grazie a questo componente la scheda può collegarsi a Internet e dialogare con i servizi Cloud senza hardware aggiuntivo. Da qui nasce il passaggio chiave dei nostri laboratori: non ci limiteremo a far “funzionare un circuito”, ma impareremo a pubblicare dati, visualizzarli su una dashboard e, quando serve, inviare comandi da remoto.

Al termine della configurazione potrete:

  • caricare sketch anche via rete (OTA, over-the-air) quando previsto dal flusso Cloud,
  • costruire dashboard consultabili da PC o smartphone,
  • monitorare variabili e sensori in tempo reale e, in prospettiva, realizzare prototipi utili per domotica, automazione e telemetria.

Cosa serve

  • Arduino UNO R4 WiFi
  • Arduino Cloud (account gratuito e accesso alla piattaforma)

Configurazione e procedura

Prima di iniziare, se non avete mai utilizzato Arduino Cloud, vi consiglio di dare un’occhiata alla guida “Getting Started With the Arduino Cloud” che vi aiuterà a capire i concetti base e vi farà risparmiare tempo durante la configurazione, però durante le lezioni che seguiranno vi fornirò tutte le indicazioni.

Continua a leggere

Flashcard per lo studio – utilizzare Anki

Visto che siamo in vacanza riprendo i miei appunti sull’organizzazione dello studio, iniziata con le schede Quick Reference, tutto con l’idea di realizzare in un futuro un manuale con esempi da fornire agli studenti. Ho ripreso lo studio delle lingue e quindi ho pensato di realizzare qualcosa in merito all’uso delle flashcard strumento che ritengo utile.

Parto da una domanda che mi è stata fatta da uno studente di seconda superiore poco prima delle vacanze natalizie:

“Prof, ma lei come studiava quando aveva la nostra età?”

È una domanda che torna spesso, e ogni volta mi ricorda una cosa importante: molti studenti immaginano che esista un “metodo segreto”, una tecnica risolutiva che rende lo studio facile e veloce. La mia risposta, invece, è sempre piuttosto disarmante: non studiavo in modo così diverso da voi. Riassunti, schemi, appunti ordinati… e soprattutto flashcard.

Le flashcard, però, non erano una soluzione “magica”. Erano uno strumento semplice, che funzionava solo a una condizione: usarle con costanza, a piccole dosi, nel tempo. Ho sempre pensato che nello studio valga una regola che sembra banale ma è decisiva: meglio poco, ma ripetuto nel tempo. È questo che trasforma un ripasso occasionale in un apprendimento stabile. E naturalmente, con gli anni, ognuno costruisce il proprio equilibrio: c’è chi rende più efficaci gli schemi, chi preferisce i riassunti, chi trova nelle flashcard la leva migliore per fissare concetti, definizioni, formule o passaggi procedurali.

Oggi, quando mi capita di usare le flashcard per imparare (ad esempio per le lingue o per memorizzare lessico tecnico), mi affido spesso ad Anki. Non perché sia “l’ennesima app per fare flashcard”, ma perché introduce un’idea molto concreta: invece di ripassare tutto allo stesso modo e negli stessi momenti, ti aiuta a ripassare solo ciò che serve, quando serve. Ti porta a fare una cosa che tende a essere faticosa ma estremamente efficace: provare a ricordare attivamente prima di guardare la risposta. E quel tentativo di richiamo, ripetuto con regolarità, è spesso la differenza tra “l’ho letto” e “lo so”.

Per gli studenti questo significa ridurre l’effetto delle maratone dell’ultimo minuto e costruire memoria a lungo termine con un metodo più sostenibile. Per i docenti significa avere uno strumento che rende più gestibile il ripasso distribuito e la personalizzazione, senza limitarsi al classico “ripasso pre-verifica”.
C’è poi un aspetto che, a scuola, conta più di quanto sembri: Anki è un progetto libero e open source (in particolare nella versione desktop), sostenuto da una comunità ampia e da un ecosistema di estensioni. In pratica, non sei legato a una piattaforma chiusa: puoi costruire nel tempo un archivio di materiali che resta tuo, riutilizzabile e migliorabile anno dopo anno.

Nei paragrafi che seguono vediamo cosa rende Anki diverso da altri strumenti di studio, quali vantaggi offre in modo concreto e come iniziare con un tutorial essenziale e due esempi pratici, subito adattabili alle vostre discipline.

Gli appunti che seguono sono una sintesi del manuale che trovate sul sito di riferimento e che ho riformulato e sintetizzato, troverete poi alcuni esempi di flashcard sull’uso di Arduino.

Anki nello studio

C’è un momento che studenti e docenti conoscono bene: hai studiato “davvero”, magari anche con impegno costante, eppure dopo pochi giorni ti accorgi che alcuni passaggi si sono sfilacciati. Non è (solo) una questione di volontà, è più spesso una questione di come ripassi e quando lo fai.

Anki è un programma di flashcard “intelligenti” che pianifica per voi il ripasso: vi mostra una domanda, vi chiede di rispondere, poi usa il vostro feedback per decidere quando rivedrete quella stessa informazione. In altre parole: voi pensate ai contenuti, Anki si occupa del calendario del ripasso.

Anki nasce esattamente per rispondere a questo problema quotidiano: non vi chiede di studiare di più, vi aiuta a sprecare meno. Invece di ripassare tutto allo stesso modo e nello stesso momento, vi porta a rivedere ciò che state per dimenticare, quando serve davvero. La logica di fondo è quella di allenare il cervello con due leve molto concrete: richiamo attivo (provare a ricordare prima di controllare) e ripetizione distanziata (rivedere a intervalli crescenti).

Perché Anki è interessante rispetto ad altri applicativi

  1. Trasforma lo studio passivo in apprendimento che resta
    Rileggere, sottolineare, “ripassare scorrendo” sono abitudini comprensibili: danno l’idea di essere produttivi. Ma spesso si basano sul riconoscimento, non sul recupero. Quando invece vi trovate davanti a una domanda e dovete rispondere senza aiuti, state facendo ciò che il manuale chiama test di richiamo attivo: è faticoso, sì, ma è proprio quella fatica “buona” che rende la memoria più stabile.
  2. Ripassate meno, ma meglio (ripetizione distanziata)
    Il cervello tende a scartare in fretta ciò che non viene usato. Ecco perché il ripasso “a ondate” (tutto la sera prima) funziona male sul lungo periodo. Anki applica invece la ripetizione distanziata: ciò che ricordate bene torna dopo più tempo; ciò che vi è difficile ricompare prima. Il risultato pratico è semplice: non ripassate “a caso”, ma in modo mirato, con intervalli che crescono mentre la memoria si consolida.
  3. È personalizzabile: funziona per materie diverse e livelli diversi
    Anki non è “solo per le lingue” o “solo per medicina”. Se una cosa si può chiedere in forma di domanda (e verificare con una risposta), allora si può allenare con Anki: definizioni, formule, passaggi di procedure, date, classificazioni, concetti chiave e potete farlo anche con materiali ricchi: immagini, audio, video, notazione scientifica (LaTeX/Math).
  4. Open source e free: vantaggi concreti
    Qui Anki ha un valore particolare, soprattutto a scuola:

    • Desktop open source: il codice di Anki è pubblicato con licenza GNU AGPL v3 o successiva (con alcune parti sotto licenze differenti, come indicato nel repository). Questo significa continuità, verificabilità e un progetto che non dipende da una “scatola chiusa”.
    • Ecosistema di add-on: Anki si estende con componenti aggiuntivi, spesso nati da bisogni reali di docenti e studenti (lingue, layout, gestione, workflow).
    • Controllo e portabilità dei materiali: puoi esportare e trasferire mazzi “impacchettati” con contenuti e media, utile per backup e condivisione ragionata.
    • Multi-piattaforma e sync: esiste un servizio di sincronizzazione gratuito (AnkiWeb) per tenere allineati i contenuti tra dispositivi.
    • Android open source: AnkiDroid è su GitHub e dichiara licenze libere (GPL-3.0, con componenti specifiche sotto altre licenze).

Nota utile per evitare equivoci: su iOS l’app ufficiale AnkiMobile è a pagamento; la pagina App Store spiega che le vendite finanziano lo sviluppo.

  1. Un “sistema” più che un’app
    Molti strumenti per flashcard si fermano al “fronte/retro”. Anki, invece, è un piccolo sistema di studio: statistiche, ricerca, tag, opzioni di pianificazione, modelli delle carte, sincronizzazione, backup. Per chi vuole ottimizzare ulteriormente, oggi esiste anche un pianificatore alternativo (FSRS) integrato nelle opzioni.

Mini-tutorial usare Anki

Continua a leggere

Dirimere i conflitti con il pensiero computazionale – educazione civica – sketch Arduino – lezione 03

Ora traduciamo lo pseudocodice in uno sketch Arduino. Usiamo input da Serial Monitor per simulare le scelte e il LED integrato (e un buzzer opzionale) per avere feedback fisico. L’obiettivo non è “automatizzare i conflitti”, ma allenare il pensiero procedurale e la gestione consapevole dei passi. Si faccia riferimento alla lezione precedente (seguire il link) in cui è stato mostrato il diagramma di flusso nella modalità Mermaid e classica.

Nella dimostrazione viene usato un Arduino UNO R4 WiFi.

Di seguito una possibile soluzione, il funzionamento lo trovate nei commenti.

/* Prof. Maffucci Michele
  data: 18.11.25
  Risoluzione di un conflitto
  Input da Serial Monitor

  - LED integrato (pin 13) usato come feedback visivo
  - Buzzer opzionale su pin 5 (beep brevi)
  Istruzioni:
  1) Aprire la Serial Monitor (115200 baud).
  2) Rispondere alle domande con i tasti indicati (y/n, a/f) o premi invio quando richiesto.
*/

const int pinLed = LED_BUILTIN;  // pin 13
const int pinBuzzer = 5;         // pin a cui è connesso il Buzzer
int numeroBeep = 0;

bool chiaveMsg = 0;  // per controllo stampa msg

// emissione della nota
void beep(int durataMs) {
  tone(pinBuzzer, 880);  // nota La4
  delay(durataMs);
  noTone(pinBuzzer);
}

// sequenza di beep ripetuta all'avvio del programma
void sequenzaAvvio() {
  for (int i = 0; i < 10; i++) {
    beep(100);
    delay(100);
  }
}

// Attende che ci sia almeno un carattere sulla seriale e lo legge (minuscolo)
char leggiChar() {
  while (!Serial.available()) {
    // lampeggio lento del LED per indicare "in attesa"
    digitalWrite(pinLed, HIGH);
    delay(200);
    digitalWrite(pinLed, LOW);
    delay(200);
  }

  // Legge un solo byte dalla seriale e lo mette in c (di tipo char).
  // Questo è il primo carattere che l’utente ha inviato.
  char c = Serial.read();

  // Svuota il buffer rimanente leggendo tutto quello che è ancora arrivato
  // (ad esempio il \n dell’invio, o più caratteri se l’utente ha incollato del testo).
  while (Serial.available())
    Serial.read();
  if (c >= 'A' && c <= 'Z')  // Conversione in minuscolo se il carattere è una lettera maiuscola.
    c = c - 'A' + 'a';
  return c;
}

// -- Utility: attende qualsiasi tasto (usata come "invio per continuare")
// “un puntatore a char costante”, cioè una stringa in stile C non modificabile
// (tipicamente una stringa letterale come "Ciao").

void attendiConferma() {
  const char* messaggio = "Premi invio per continuare";
  Serial.println(messaggio);
  while (!Serial.available()) {
    digitalWrite(pinLed, HIGH);
    delay(150);
    digitalWrite(pinLed, LOW);
    delay(150);
  }
  // svuota
  while (Serial.available())
    Serial.read();
  beep(100);
}

// -- Utility: domanda sì/no
bool domandaSiNo(const char* domanda) {
  Serial.println(domanda);
  Serial.println("Digita 'y' per SI, 'n' per NO");

  //ripetiamo finché l’utente non inserisce una risposta valida.
  // Il ciclo si interrompe con un return (es. quando l’utente digita ‘y’ o ‘n’).
  for (;;) {
    char c = leggiChar();
    if (c == 'y') {
      Serial.println("Hai risposto: SI");
      beep(100);
      return true;
    }
    if (c == 'n') {
      Serial.println("Hai risposto: NO");
      beep(100);
      return false;
    }
    Serial.println("Risposta non valida. Usa 'y' o 'n'.");
  }
}

// Pausa "simulata breve" al posto di 2 minuti reali
void pausaBreve(const char* motivo, int secondi = 5) {
  Serial.print("Pausa: ");
  Serial.print(motivo);
  Serial.print(" (");
  Serial.print(secondi);
  Serial.println("s)");
  for (int i = 0; i < secondi; i++) {
    digitalWrite(pinLed, HIGH);
    delay(300);
    digitalWrite(pinLed, LOW);
    delay(700);
    Serial.print(".");
  }
  Serial.println();
  beep(100);
}

void setup() {
  pinMode(pinLed, OUTPUT);
  Serial.begin(115200);
  delay(1000);
  Serial.println("=== Risoluzione Conflitto - VERSIONE BASE ===");
  Serial.println("Benvenuto! Segui le istruzioni sul Serial Monitor.");
  pinMode(pinBuzzer, OUTPUT);
  sequenzaAvvio();
}

void loop() {
  // 1) Sicurezza del confronto
  bool sicuro = false;
  do {
    sicuro = domandaSiNo("E' sicuro parlare adesso?");
    if (!sicuro) {
      Serial.println("OK, facciamo un time-out breve al posto di 2 minuti.");
      pausaBreve("time-out", 5);
    }
  } while (!sicuro);

  // 2) Definizione del problema
  Serial.println("Definisci il problema in UNA frase (in classe va bene scriverla su carta).");
  attendiConferma();

  // 3) Turni + Parafrasi
  bool parafrasiOk = false;
  do {
    Serial.println("Turno A (60s simulati).");
    attendiConferma();
    Serial.println("Turno B (60s simulati).");
    attendiConferma();
    parafrasiOk = domandaSiNo("Parafrasi reciproca corretta?");
  } while (!parafrasiOk);

  // 4) Opzioni di soluzione (almeno 2)
  int numeroOpzioni = 0;
  Serial.println("Genera almeno 2 opzioni di soluzione.");
  for (;;) {
    Serial.println("a = aggiungi opzione, f = fine (consentita solo se >= 2 opzioni)");
    char c = leggiChar();
    if (c == 'a') {
      numeroOpzioni++;
      Serial.print("Opzione aggiunta. Totale: ");
      Serial.println(numeroOpzioni);
      beep(100);
    } else if (c == 'f') {
      if (numeroOpzioni >= 2) {
        Serial.println("OK, passiamo alla decisione.");
        beep(100);
        break;
      } else {
        Serial.println("Servono almeno 2 opzioni prima di terminare.");
      }
    } else {
      Serial.println("Scelta non valida. Usa 'a' o 'f'.");
    }
  }

  // 5) Decisione finale
  bool accordo = domandaSiNo("C'e' accordo su una opzione?");
  if (accordo) {
    Serial.println("Piano d'azione: definire chi/fa/cosa/entro quando (scrivere su carta).");
    Serial.println("Promemoria: verifica dopo 24 ore.");
    Serial.println("FINE. Riavvio tra 5 secondi...");
    pausaBreve("chiusura", 5);
  } else {
    Serial.println("Coinvolgi un mediatore o rinvia il confronto.");
    Serial.println("Ritorno al controllo 'E' sicuro parlare?'.");
    pausaBreve("ritorno", 3);
  }
}

Arduino esercizi – Base marziana ELYSIUM – correzione fase 1 – solo pulsante

In riferimento alla lezione: Arduino esercizi – Base marziana ELYSIUM, riporto di seguito tre possibili soluzioni, nei commenti di ogni sketch la spiegazione sul funzionamento.

Fase 1 – solo pulsante

// Prof. Maffucci Michele
// data 12.11.25
// Base marziana ELYSIUM - fase 1 - solo pulsante

const int PIN_LED = 9;
const int PIN_PULSANTE = 2;  // pull-down esterno: HIGH quando premuto

void setup() {
  pinMode(PIN_LED, OUTPUT);
  pinMode(PIN_PULSANTE, INPUT);  // nessun pull-up interno
  digitalWrite(PIN_LED, LOW);
}

void loop() {
  int statoPulsante = digitalRead(PIN_PULSANTE);

  if (statoPulsante == HIGH) {    // pulsante premuto
    digitalWrite(PIN_LED, HIGH);  // accendo la luce di cortesia
    delay(1500);                  // 1,5 s
    digitalWrite(PIN_LED, LOW);  // spengo
    delay(50);                   // piccolo anti-rimbalzo
  }
}

Con questa soluzione alla pressione del pulsante il LED si accende e se rilasciate si spegne, però se mantenete premuto il pulsante notare un veloce spegnimento ed una riaccensione del LED ovvero ciò che stiamo facendo e riaccendere immediatamente il LED.

Per evitare ciò bisogna implementare la versione che evita il retrigger.

Evitare il retrigger significa impedire che, tenendo il pulsante premuto, il ciclo riparta subito dopo lo spegnimento del LED e lo riaccenda di nuovo. Per evitarlo, dopo la sequenza (LED acceso per 1,5 s > LED spento) si controlla se il pulsante è ancora premuto: finché lo è, si rimane dentro un ciclo while bloccante che attende il rilascio. Solo quando il pulsante torna allo stato di riposo si esce dal while, si applica un breve anti-rimbalzo (es. 50 ms) e il programma riprende il loop normale.

ATTENZIONE

il while è bloccante ciò vuol dire che blocca l’intera esecuzione del programma. Va benissimo per questo esercizio semplice; in progetti più complessi è preferibile preferisce rilevare il fronte di salita (variabile di stato) o un approccio non bloccante con millis() che ancora non conoscete (mi sto rivolgendo ai ragazzi di 3′) e che vedremo più avanti.

Fase 1 – solo pulsante – evita il retrigger con while bloccante

// Prof. Maffucci Michele
// data 12.11.25
// Base marziana ELYSIUM - fase 1B - solo pulsante
// evitare il retrigger

const int PIN_LED = 9;
const int PIN_PULSANTE = 2;  // pull-down esterno: HIGH quando premuto

void setup() {
  pinMode(PIN_LED, OUTPUT);
  pinMode(PIN_PULSANTE, INPUT);  // nessun pull-up interno
  digitalWrite(PIN_LED, LOW);
}

void loop() {
  int statoPulsante = digitalRead(PIN_PULSANTE);

  if (statoPulsante == HIGH) {    // pulsante premuto
    digitalWrite(PIN_LED, HIGH);  // accendo la luce di cortesia
    delay(1500);                  // 1,5 s
    digitalWrite(PIN_LED, LOW);   // spengo

    // attendo il rilascio per evitare retrigger continui
    while (digitalRead(PIN_PULSANTE) == HIGH) {
      delay(10);
    }
    delay(50);                    // piccolo anti-rimbalzo
  }
}

Attenzione alla parte di codice:

...
while (digitalRead(PIN_PULSANTE) == HIGH) {
      delay(10);
    }
...

potreste essere tentati di scrivere:

...
while (statoPulsante == HIGH) {
      delay(10);
    }
...

se agite in questo modo, dopo la prima accensione e il successivo spegnimento non riuscirete più ad accendere il LED con successive pressioni del pulsante.

Considerate statoPulsante come uno “scatto fotografico” fatto ad inizio loop(), pertanto se scrivete:

while (statoPulsante == HIGH) { ... }

la condizione resta sempre vera (perché non non viene mai aggiornata).
Risultato: si resta bloccati per sempre in quel while dopo la prima attivazione > il LED si accende una volta e poi il programma non torna più al loop().

Fase 1 – solo pulsante – evita il retrigger senza while bloccante

Fornisco un’altra soluzione in cui non si fa uso del while bloccante.

Invece di “restare fermi” dentro un while finché il pulsante non viene rilasciato, leggiamo il pulsante ad ogni giro del loop e facciamo scattare l’azione solo quando cambia da NON premuto a premuto. Questo momento si chiama fronte di salita.

// Prof. Maffucci Michele
// data 12.11.25
// Base marziana ELYSIUM - fase 1C - solo pulsante
// evitare il retrigger senza while bloccante
// con variabile di stato

const int PIN_LED      = 9;
const int PIN_PULSANTE = 2;   // pull-down esterno: HIGH = premuto

bool premutoPrima = false;

void setup() {
  pinMode(PIN_LED, OUTPUT);
  pinMode(PIN_PULSANTE, INPUT);   // nessun pull-up interno
}

void loop() {
  // Snapshot della lettura
  int statoPulsante = digitalRead(PIN_PULSANTE);

  // Converto HIGH/LOW in booleano
  bool premuto;
  if (statoPulsante == HIGH) {
    premuto = true;    // pulsante premuto
  } else {
    premuto = false;   // pulsante rilasciato
  }

  // Fronte di salita: ora premuto, prima no
  if (premuto && !premutoPrima) {
    digitalWrite(PIN_LED, HIGH);
    delay(1500);
    digitalWrite(PIN_LED, LOW);
    delay(50); // anti-rimbalzo semplice
  }

  // Aggiorna lo stato per il prossimo giro
  premutoPrima = premuto;
}

Come funziona il programma

  • premuto = stato attuale del pulsante (TRUE se è premuto ora, FALSE altrimenti).
  • premutoPrima = stato del pulsante al giro precedente del loop.
  • Condizione di innesco: premuto == true e premutoPrima == false > significa “hai appena iniziato a premere”.

Quando la condizione è vera:

  1. esegui l’azione (es. LED acceso 1,5 s, poi spento),
  2. non restate bloccato: il loop continua a girare,
  3. finché tenete premuto, la condizione non si ripete (perché ora premutoPrima è anch’esso TRUE),
  4. quando rilasciate, premuto torna FALSE; alla prossima pressione avrete di nuovo il fronte di salita.

Questa soluzione offre i seguenti vantaggi:

  • Niente blocchi: il programma continua a leggere ingressi/aggiornare uscite mentre il LED è stato gestito (utile in progetti più grandi).
  • No retrigger con pressione prolungata: scatta una sola volta all’inizio della pressione.
  • Struttura “a eventi”: reagisce ai cambiamenti, non al livello costante del pulsante.

Di seguito la tabella “fronte di salita” (versione con pull-down: HIGH = premuto)

Fase Descrizione Lettura pin premuto (ora) premutoPrima (prima) Condizione premuto && !premutoPrima Azione
T0 Riposo prima della pressione LOW FALSE FALSE FALSE Nessuna
T1 Inizio pressione (fronte salita) HIGH TRUE FALSE TRUE Esegui sequenza LED
T2 Tenuto premuto HIGH TRUE TRUE FALSE Nessuna
T3 Rilascio LOW FALSE TRUE FALSE Nessuna (solo aggiornamento stato)
T4 Nuova pressione (nuovo fronte) HIGH TRUE FALSE TRUE Esegui sequenza LED

Buon Coding a tutti 🙂

Arduino – lettura di caratteri da Serial Monitor

La Serial Monitor è la finestra di chat tra noi e la scheda Arduino. Da un lato c’è il computer (con l’IDE o l’editor web), dall’altro c’è il microcontrollore. Quando apriamo la Serial Monitor, stiamo aprendo un canale in cui possiamo leggere messaggi che Arduino ci invia (testi, numeri, stati dei sensori) e scrivergli comandi (caratteri, parole, numeri) per fargli fare cose. È il modo più semplice e immediato per “vedere dentro” un programma che sta funzionando su una scheda senza schermo.

Saper usare la Serial Monitor è utile perché in elettronica e automazione non basta “caricare lo sketch e sperare che funzioni tutto”, bisogna capire cosa succede: quanto misura un sensore, se un calcolo è corretto, se un evento si verifica davvero. Con Serial.print() e Serial.println() facciamo in modo che Arduino ci spieghi passo passo cosa sta facendo. Questo è fondamentale per il debug: quando qualcosa non funziona, far “parlare” il codice è spesso la via più rapida per scoprire l’errore.

Concetti chiave

  • Velocità (baud rate): indica il numero di caratteri al secondo che vengono trasmessi. Questa velocità deve coincidere tra sketch e Serial Monitor, altrimenti ricevete  caratteri non corretti. Usare Serial.begin(9600) o altre velocità tipiche (9600/115200).
  • Buffer di ricezione: i byte arrivano e restano nel buffer finché non li leggete. Serial.available() ti dice quanti.
  • Terminatori di riga nel Serial Monitor: potete inviare No line ending (Nessun fine riga), Newline (\n) (A capo (NL)), Carriage return (\r) (Ritorno carrello (CR)), Both NL & CR (Entrambi NL & CR). Scegliendo A campo (NL) potete leggere “una riga per volta”.

Durante le precedenti lezioni di ripasso abbiamo visto in più occasioni come usare la Serial Monitor, quindi questa che state leggendo è una lezione di ripasso, di seguito i link alle lezioni precedenti:

Riporto di seguito una serie di programmi standard che possono essere riutilizzati in più occasioni:

Esempio 01: echo con conteggio: leggo caratteri subito (non bloccante)

// Prof. Maffucci Michele
// data: 11.11.25
// Fare "echo" dei caratteri ricevuti e contare quanti ne vengono letti.
// Impostazioni Serial Monitor: qualsiasi line ending (anche nessuno va bene).

unsigned long totale_byte = 0;

void setup() {
  Serial.begin(115200); // per Arduino R4, per R3 impostare 9600
  while (!Serial) {}    // necessario su alcune schede USB native
  Serial.println("Echo pronto: digita qualcosa...");
}

void loop() {
  while (Serial.available() > 0) {     // ciclo finché c'è input
    int c = Serial.read();             // leggo un byte
    totale_byte++;
    Serial.write(c);                   // rimando lo stesso byte (echo)
  }

  // Ogni 2 secondi stampo lo stato sulla Serial Monitor
  static unsigned long t0 = 0;
  if (millis() - t0 > 2000) {
    Serial.print("\nByte letti finora: ");
    Serial.println(totale_byte);
    t0 = millis();
  }
}

Dovreste notare che se avete impostato “Entrambi NL &CR” e digitate ad esempio A e premete invio il conteggio dei byte viene fatto in questo modo:

  • A > 1 byte
  • \r > Carriage Return > 1 byte
  • \n > Newline > 1 byte

totale = 3 byte.

Se impostate “A capo (NL)” otterrete un totale di 2 byte (A + \n).
Con “Nessuna fine riga” ottenete 1 byte (solo A).

Impostazione Monitor Cosa viene inviato dopo il carattere Byte totali per tasto
No line ending (nulla) 1
Newline \n 2
Carriage return \r 2
Both NL & CR \r\n 3

Esempio 02: lettura “riga per riga” con \n (line ending = Newline – “A capo(NL))

// Prof. Maffucci Michele
// data: 11.11.25
// Leggere comandi testuali una riga alla volta (terminatore '\n').
// Impostazioni Serial Monitor: "Newline" come line ending.

// DIM = dimensione massima del buffer di input, inclusa la chiusura '\0'.
// Sceglietela abbastanza grande da contenere la riga più lunga che prevedete.
const byte DIM = 64;

// riga[] è un array di char (buffer) dove accumuliamo i caratteri ricevuti
// fino all'invio '\n'. Essendo "C string", va CHIUSA con '\0' quando completa.
char riga[DIM];

// i è l'indice di scrittura corrente dentro riga[] (prossima posizione libera).
// Parte da 0 e cresce a ogni carattere valido letto; non deve mai superare DIM-1.
byte i = 0;

void setup() {
  Serial.begin(115200); // per Arduino Uno R4, per R3 impostare 9600
  while (!Serial) {}
  Serial.println("Inserisci un comando (LED_ON / LED_OFF / HELP) e premi Invio");
}

void loop() {
  while (Serial.available() > 0) {
    char c = (char)Serial.read();

    // Se il Monitor è su "Both NL & CR", scarto il ritorno carrello '\r'
    if (c == '\r') continue;

    if (c == '\n') {            // RIGA COMPLETA
      riga[i] = '\0';           // Chiudo la stringa C (terminatore)
      eseguiComando(riga);      // Passo l'intera riga alla funzione di parsing
      i = 0;                    // Reset per la prossima riga
    } else if (i < DIM - 1) { // Protezione da overflow: lascio 1 posto per '\0' riga[i++] = c; // Accumulo il carattere e avanzo l'indice } // Se il buffer è pieno, i caratteri extra vengono ignorati } } void eseguiComando(char* cmd) { // strcmp(a,b) confronta due stringhe C (char*) e restituisce: // 0 --> uguali
  //  <0 --> a < b // >0  --> a > b
  // ricordare: è case-sensitive ("LED_ON" != "led_on").

  if (strcmp(cmd, "LED_ON") == 0) {   // esattamente "LED_ON"?
    Serial.println("Accendo (simulato) LED");
  } else if (strcmp(cmd, "LED_OFF") == 0) {
    Serial.println("Spengo (simulato) LED");
  } else if (strcmp(cmd, "HELP") == 0) {
    Serial.println("Comandi validi: LED_ON, LED_OFF, HELP");
  } else {
    Serial.print("Comando sconosciuto: ");
    Serial.println(cmd);
  }
}

ricordo che strcmp(...)

  • confronta contenuti delle C-string fino al primo ‘\0’
  • ritorna 0 quando le stringhe sono identiche (è il caso che usiamo negli if)
  • è case-sensitive: “LED_ON” è diverso da “led_on”
  • per confronti su prefisso: strncmp(cmd, “PWM:”, 4) == 0
  • ricorda di passargli stringhe chiuse con ‘\0’ (come facciamo con riga[i] = ‘\0’;)

Ho parlato su queste pagine in più occasioni degli array (seguite il link) un ripasso veloce:

  • un array è una sequenza contigua di celle tutte dello stesso tipo, indicizzate da 0 a N-1;
  • un char buf[N] è spesso usato come stringa C: la parte “valida” termina con '\0' (byte zero);
  • mai uscire dai limiti: per una stringa lunga al massimo N-1, lascia sempre l’ultimo posto per '\0';
  • accesso in scrittura: mantenere un indice (es. i) e verifica i <N-1 prima di scrivere e poi chiudere con buf[i]='\0';
  • per evitare “sporcizia” residua, non serve azzerare tutto il buffer ogni volta: basta chiudere correttamente la stringa;
  • per copiare in sicurezza: strncpy(dst, src, N-1); dst[N-1] = '\0';
  • se preferite evitare la gestione manuale di '\0', valutate String di Arduino; però per prestazioni/memoria prevedibili (e in contesti embedded reali) gli array di char restano la scelta più affidabile.

Esempio 03: lettura “fino a terminatore” con blocco controllato

// Prof. Maffucci Michele
// data: 11.11.25
// Lettura "fino a terminatore" con blocco controllato
// Leggere una sequenza di caratteri dall’utente fino a un terminatore (es. '#')
// usando funzioni bloccanti ma con un timeout breve, così la loop resta reattiva.

const byte DIM = 32;    // dimensione massima del buffer, compreso lo '\0' finale
const char TERM = '#';  // carattere terminatore scelto per “chiudere” l’input

char buf[DIM];  // buffer dove accumulo i caratteri ricevuti

void setup() {
  Serial.begin(115200);
  while (!Serial) {}       // su schede con USB nativa: attendo l’apertura della porta
  Serial.setTimeout(100);  // timeout per tutte le funzioni bloccanti Serial (100 ms)
                           // -> se l’utente NON digita nulla, la readBytesUntil attenderà
                           //    al massimo 100 ms e poi restituirà 0 byte letti
  Serial.println("Digita testo e termina con '#' (terminatore).");
  Serial.println("Suggerimento: imposta il line ending su 'No line ending'.");
}

void loop() {
  // readBytesUntil BLOCCA fino a quando:
  //  - incontra il terminatore (TERM), oppure
  //  - raggiunge (DIM-1) caratteri (lasciamo 1 posto per '\0'), oppure
  //  - scade il timeout impostato con setTimeout(...)
  //
  // Ritorna quanti byte REALMENTE copiati in buf (senza includere il terminatore).
  size_t n = Serial.readBytesUntil(TERM, buf, DIM - 1);

  if (n > 0) {      // ho letto qualcosa (terminatore trovato o buffer quasi pieno)
    buf[n] = '\0';  // chiudo la C-string

    // Se nel Serial Monitor usate "Both NL & CR" ed inviate per errore \r o \n
    // dentro al testo, potreste voler ripulire la stringa:
    // stripCRLF(buf);

    Serial.print("Ricevuto (");
    Serial.print(n);
    Serial.print(" char): ");
    Serial.println(buf);

    // Esempio: gestisco un comando speciale
    if (strcmp(buf, "RESET") == 0) {  // strcmp ritorna 0 se le stringhe sono identiche
      Serial.println("-> Comando RESET eseguito (simulato).");
    } else if (strncmp(buf, "PWM:", 4) == 0) {  // prefisso "PWM:" seguito da valore

      // "buf" contiene l'intera riga ricevuta, es. "PWM:128"
      // Confrontato il prefisso "PWM:", vogliamo estrarre solo la parte numerica ("128").
      // "buf + 4" significa: punta al 5° carattere della stringa (salta 'P','W','M',':').
      // atoi(...) converte la sottostringa numerica in int (qui: 128).

      int v = atoi(buf + 4);
      if (v >= 0 && v <= 255) { Serial.print("-> Imposto PWM a ");
        Serial.println(v);
      } else {
        Serial.println("-> Valore PWM fuori range (0..255).");
      }
    } else {
      Serial.println("-> Testo generico ricevuto.");
    }

    // readBytesUntil CONSUMA il terminatore dal flusso,
    // quindi non dovete "buttarlo" voi. Se invece usate readBytes (senza Until),
    // il terminatore resterebbe nel buffer e dovreste occuparvene manualmente.
  }

  // Se n == 0, non ho letto nulla: o non è arrivato niente entro 100 ms,
  // o l'utente ha inviato meno caratteri del previsto e non ha terminato con '#'.
  // In ogni caso il loop rimane libero di fare altro.
}

setTimeout() controlla l’attesa massima delle funzioni bloccanti come readBytesUntil(), readString(), parseInt() ecc.

Esempio 04: Lettura di numeri con parseInt() e validazioni

// Prof. Maffucci Michele
// data: 11.11.25
// Acquisire un numero intero inserito dall'utente, con controllo di validità.
// Impostazioni: line ending opzionale; consigliato "A capo(NL)".

void setup() {
  Serial.begin(115200); // per Arduino Uno R4, per R3 impostare 9600
  while (!Serial) {}
  Serial.setTimeout(3000);  // 3 s per digitare il numero
  Serial.println("Inserisci un numero intero tra 0 e 255 e premi Invio:");
}

void loop() {
  if (Serial.available() > 0) {
    long val = Serial.parseInt();  // blocca finché trova cifre o va in timeout
    if (val >= 0 && val <= 255) { Serial.print("OK, hai scritto: "); Serial.println(val); } else { Serial.println("Valore non valido o fuori range (0..255). Riprova."); } // Svuoto eventuali residui sulla linea while (Serial.available() > 0) {
      Serial.read();
    }
    Serial.println("Inserisci un nuovo numero:");
  }
}

parseInt() e parseFloat() saltano caratteri non numerici, leggono il numero e rispettano setTimeout().

Esempio 05: parser a stati non bloccante

// Prof. Maffucci Michele
// data: 11.11.25
// Parser “a stato” senza blocchi
// Leggere comandi testuali dalla Serial senza usare funzioni bloccanti.
// Formati accettati (esempi):
//   - "S1\n"   -> LED ON
//   - "S0\n"   -> LED OFF
//   - "PWM:123\n" -> imposta duty a 123
//
// Strategia:
//  - Usiamo una MACCHINA A STATI: IN_ATTESA -> IN_TOKEN -> (':' visto?) IN_VALORE -> (newline) ESEGUI
//  - Leggiamo byte con available()/read() (mai bloccare).
//  - Accumuliamo in due buffer separati: token (comando) e valore (parametro).
//  - Al newline '\n' consideriamo il comando “completo” e lo eseguiamo.
//  - Ignoriamo '\r' (utile se il Monitor invia "Both NL & CR").
//
// Vantaggi: loop reattiva (puoi fare sensori/attuatori mentre leggi), nessun timeout da gestire.

enum StatoInput {      // Enum = elenco di costanti intere leggibili che rappresentano gli stati possibili
  IN_ATTESA,           // Non sto leggendo nulla (skip spazi, attendo primo char utile del token)
  IN_TOKEN,            // Sto leggendo il nome del comando (es. "S1" o "PWM")
  IN_VALORE            // Ho visto ':' e sto leggendo il valore (es. "123")
};
// Nota: di default, IN_ATTESA=0, IN_TOKEN=1, IN_VALORE=2 (interi consecutivi).

StatoInput stato = IN_ATTESA;   // stato corrente della macchina a stati

// Dimensioni dei buffer: lascia sempre 1 char per il terminatore '\0'
const byte DIM_TOKEN = 8;
const byte DIM_VAL   = 8;

char token[DIM_TOKEN];          // buffer per il comando (senza parametri)
char valore[DIM_VAL];           // buffer per il parametro (se presente)
byte iT = 0, iV = 0;            // indici di scrittura nei buffer

// (Opzionale) Normalizza in minuscolo: utile se vuoi comandi case-insensitive
char toLowerChar(char c) {
  if (c >= 'A' && c <= 'Z') return c + ('a' - 'A');
  return c;
}

void setup() {
  Serial.begin(115200);
  while (!Serial) {}
  Serial.println("Comandi: S0, S1, PWM:<0..255>  (termina con Invio)");
  Serial.println("Esempi: S1\\n   S0\\n   PWM:128\\n");
}

void loop() {
  // ---- QUI PUOI FARE ALTRO (letture sensori, logica, attuatori) ----
  // NIENTE nel parser sotto blocca l'esecuzione.

  while (Serial.available() > 0) {
    char c = (char)Serial.read();     // prendo 1 byte dal buffer RX
    if (c == '\r') continue;          // ignoro CR (se Monitor manda Both NL & CR)

    if (c == '\n') {                  // newline = riga/ comando completo
      token[iT]  = '\0';              // chiudi stringhe C
      valore[iV] = '\0';

      esegui(token, valore);          // interpreta ed esegui

      // reset parser per prossima riga
      stato = IN_ATTESA;
      iT = iV = 0;
      continue;
    }

    // Se non è newline, gestisco in base allo stato
    switch (stato) {

      case IN_ATTESA:
        // Salto eventuali spazi iniziali, poi inizio a costruire il token
        if (c == ' ' || c == '\t') {
          // rimango in attesa
        } else {
          stato = IN_TOKEN;           // ho trovato il primo char "utile"
          if (iT < DIM_TOKEN - 1) {
            token[iT++] = c;          // salvo nel token
          }
          // se pieno, i char extra verranno scartati (vedi controllo nei case seguenti)
        }
        break;

      case IN_TOKEN:
        if (c == ':') {
          // Due casi:
          // - Comandi con parametro (es. "PWM:123"): passo allo stato IN_VALORE
          // - Comandi senza parametro ma con ':' accidentale: comunque vado in IN_VALORE (sarà vuoto)
          stato = IN_VALORE;
        } else if (c == ' ' || c == '\t') {
          // Ignoro spazi in mezzo al token (scelta di design: potresti anche “chiudere” il token qui)
        } else {
          // Accumulo altri caratteri del token se c'è spazio
          if (iT < DIM_TOKEN - 1) token[iT++] = c;
          // Se il token supera la capienza, i char extra vengono silenziosamente scartati
        }
        break;

      case IN_VALORE:
        // Tutto ciò che arriva fino al newline è il valore (spazi inclusi)
        if (iV < DIM_VAL - 1) valore[iV++] = c;
        // Se supera la capienza, scarto il resto del valore
        break;
    }
  }
}

// Esegue il comando interpretando token e valore
void esegui(const char* tk, const char* val) {
  // --- Esempio: comandi "S1" e "S0" (LED simulato) ---
  // strcmp ritorna 0 quando le stringhe sono identiche (case-sensitive).
  if (strcmp(tk, "S1") == 0) {
    Serial.println("LED = ON (simulato)");
    return;
  }
  if (strcmp(tk, "S0") == 0) {
    Serial.println("LED = OFF (simulato)");
    return;
  }

  // --- Esempio: comando con parametro "PWM:<0..255>" ---
  // Confronto solo il prefisso "PWM" (3 caratteri). Se uguale, mi aspetto il valore in 'val'.
  if (strncmp(tk, "PWM", 3) == 0) {
    // Converte il valore da stringa a intero.
    // Nota: atoi non segnala errori; in casi reali preferisci controllare che 'val' contenga solo cifre.
    int duty = atoi(val);
    if (duty >= 0 && duty <= 255) {
      Serial.print("Imposto PWM a ");
      Serial.println(duty);
    } else {
      Serial.println("PWM fuori range (0..255).");
    }
    return;
  }

  // --- Default: comando sconosciuto ---
  Serial.print("Comando sconosciuto: '");
  Serial.print(tk);
  Serial.print("'");
  if (val[0] != '\0') {
    Serial.print(" con valore '");
    Serial.print(val);
    Serial.print("'");
  }
  Serial.println();
}

Funzionamento:

  1. Accumulo byte finché ce n’è (available()>0).
  2. Transizioni di stato:
    • IN_ATTESA > ignora spazi > primo carattere utile > IN_TOKEN
    • IN_TOKEN > caratteri “nome comando”; se arriva ':' > IN_VALORE
    • IN_VALORE > accumulo fino al newline
  3. Newline \n = “fine comando”: chiudo le stringhe con '\0' e chiamo esegui(...)
  4. protezione buffer: sempre DIM-1 per lasciare spazio al '\0'. Char extra > scartati
  5. niente blocchi: nessun readString/parse*; la loop() resta fluida.

Non ricordo se avevo già parlato di enum ma vi scrivo qualcosina.

Un enum elenca costanti intere con nomi leggibili, esempio:

enum StatoInput { IN_ATTESA, IN_TOKEN, IN_VALORE };

equivale (di default) a: IN_ATTESA=0, IN_TOKEN=1, IN_VALORE=2.

Usarlo in una macchina a stati è utile perché:

  • leggibilità: stato = IN_TOKEN; è auto-esplicativo
  • sicurezza: eviti “numeri di significato ambiguo” (0,1,2) sparsi nel codice
  • aiuta con switch (stato) { … }

Tipo sottostante: in C/C++ classico l’enum usa un intero (di solito int); su AVR è tipicamente int.
In C++11 esiste anche enum class (scoped enum), con scope ristretto e nessuna conversione implicita a int:

enum class StatoInput { InAttesa, InToken, InValore };
StatoInput s = StatoInput::InAttesa;
// switch(s) { case StatoInput::InToken: ... }

Qualche NOTA importante a questa lezione, dedicato ai:

  1. “lamentosi”
  2. studenti e non, bravi o svogliati
  3. a chi si dimentica che ciò che scrivo è gratuito ed in primis è rivolto ai miei studenti che hanno la priorità
  4. alle persone che fanno parte della categoria dei “flamer” o dei “troll” quindi per me identificabili come “ciucci” e “perdi tempo a tradimento” (come diceva un mio insegnante)

dico:

  1. è stato abbastanza faticoso scrivere questa lezione, pertanto non troverete le consuete dimostrazioni animate alla fine di ogni sketch
  2. copiate ed incollate e verificate se non ci sono errori, non dovrebbero essercene, ma la stanchezza può averne aggiunto qualcuno… consiglio: fate dell’errore un motivo di miglioramento
  3. se notate qualche formattazione del codice di esempio (codice tutto su un riga) ciò è imputabile a qualche carattere precedente nello sketch che aggiunge un errore di formattazione, poiché ancora non ho trovato soluzione, fate “copia ed incolla” nell’IDE Arduino ed una successiva “Formattazione automatica” dal menù strumenti questo aggiusta tutto
  4. non assegnerò come di consueto gli esercizi finali per due motivi:
    • credo che sia già un esercizio capire il funzionamento degli sketch (per me è stato un bel ripasso) 🙂
    • vedi punto 2 (stanchezza)

Buon studio a tutti 🙂