Tutto il codice Arduino delle centraline ScienziAria
In questa pagina troverai gli sketch menzionati nella guida per costruire una centralina di monitoraggio della qualità dell'aria e relativi approfondimenti.
Per comodità abbiamo incluso anche dei link per scaricare il codice sorgente in formato .ino
e le schede tecniche dell'ESP32 e dei sensori utilizzati in PDF.
Indice
- Schede tecniche
- Versione base KS0578
- Versione base PMS5003
- Variante risparmio energetico KS0578
- Variante risparmio energetico PMS5003
- Variante monitoraggio continuo KS0578
- Variante monitoraggio continuo PMS5003
- Esempio lettura modalità attiva PMS5003
Schede tecniche
Le schede tecniche qui raccolte sono tutte in inglese. Per la documentazione più recente dell'ESP32, fare riferimento al sito ufficiale Espressif.
Versione base KS0578
Torna all'indice 👆️
File sorgente: scienziaria-centralina-ks0578.ino
Codice da copiare:
/**
*
* Codice sorgente per configurare una centralina di monitoraggio della qualità
* dell'aria ispirata a quelle del progetto ScienziAria di Comunità Circolare.
*
* GUIDA PASSO PASSO per costruire la tua CENTRALINA FAI DA TE
* 👉️ https://comunitacircolare.it/articoli/scienziaria-guida-centralina
*
* Questa versione di base è pensata espressamente per chi non ha particolari
* competenze tecniche ed è incentrata quindi sulla semplicità di realizzazione.
*
* Se non hai mai programmato, NIENTE PANICO! Ti basta inserire i tre parametri
* richiesti qui sotto all'interno delle virgolette, senza toccare nient'altro.
*
* Altrimenti, modifica pure il codice come ti pare (a tuo rischio e pericolo).
*
* \author Michele Nuzzolese
* \copyright Pubblico dominio con licenza Unlicense: https://unlicense.org
* \sa Progetto ScienziAria: https://comunitacircolare.it/progetti/scienziaria
*
*/
/* ## PARAMETRI RICHIESTI ## */
// 1. Nome della rete Wi-Fi, da ricopiare esattamente come appare nella lista di
// reti disponibili sul tuo smartphone o computer (includendo eventuali spazi o
// punteggiatura e rispettando la differenza tra lettere maiuscole e minuscole).
const char *NOME_RETE_WIFI = "scrivi qui il nome della tua rete Wi-Fi";
// 2. Password del Wi-Fi: occhio a maiuscole/minuscole/errori di battitura/etc.
const char *PASSWORD_WIFI = "qui dentro ci va la password";
// 3. `Write API Key` (la trovi nella tab `API Keys` del tuo canale ThingSpeak).
const char *WRITE_API_KEY = "0RM414VR41C4P1T0";
/* ## FINE CONFIGURAZIONI OBBLIGATORIE ##
*
* Congratulazioni! Non ti resta che caricare questo codice nella tua centralina
* con Arduino IDE (per istruzioni passo passo c'è sempre la guida in alto 👆️).
*
* ## INIZIO PROGRAMMA (E PARAMETRI OPZIONALI) ##
*
* Il codice sorgente ti fa paura? Scappa ora, prima che sia troppo tardi! 😜️
*
* Nel seguito troverai tutti i parametri opzionali legati alla logica interna
* della centralina. C'è qualche commento per facilitare la comprensione, ma non
* potevo dilungarmi troppo. Nel dubbio, puoi sempre usare i valori predefiniti.
*
* Se invece vuoi spiegazioni più dettagliate, ecco l'articolo che fa per te:
* https://comunitacircolare.it/articoli/scienziaria-approfondimenti-centraline
*
*/
#include <Wire.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <NetworkClientSecure.h>
#include <esp32-hal-cpu.h>
#include <esp_sleep.h>
// I valori indicati di seguito si basano sulle istruzioni della guida e devono
// rispecchiare l'effettivo collegamento tra il sensore e la scheda ESP32:
const uint8_t PIN_SDA = 27; // Terzo pin del sensore (da sinistra a destra).
const uint8_t PIN_SCL = 26; // Quarto pin del sensore (da sinistra a destra).
// Frequenza con cui la centralina invia nuove rilevazioni (in minuti).
const uint8_t INTERVALLO_AGGIORNAMENTI_M = 4;
// Tempo di attesa per letture stabili dall'avvio della ventola (in secondi).
const uint8_t STABILIZZAZIONE_SENSORE_S = 30;
// Minimo intervallo di tempo tra due letture contigue (in millisecondi).
const uint32_t POLLING_SENSORE_MS = 1200;
// Numero massimo di rilevazioni del sensore su cui calcolare la media.
const uint8_t MAX_LETTURE_SENSORE = 6;
// Limite di tempo per tentare di connettersi al Wi-Fi (in millisecondi).
const uint32_t TIMEOUT_WIFI_MS = 10000;
// Certificato del server di ThingSpeak per la connessione sicura via HTTPS.
const char *CA_CERT =
"-----BEGIN CERTIFICATE-----\n"
"MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh\n"
"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n"
"d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\n"
"MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT\n"
"MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\n"
"b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG\n"
"9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI\n"
"2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx\n"
"1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ\n"
"q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz\n"
"tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ\n"
"vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP\n"
"BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV\n"
"5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY\n"
"1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4\n"
"NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG\n"
"Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91\n"
"8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe\n"
"pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl\n"
"MrY=\n"
"-----END CERTIFICATE-----";
// Limite di tempo per effettuare l'handshake SSL (in secondi).
const uint32_t TIMEOUT_HANDSHAKE_SSL_S = 5;
// Limite di tempo per stabilire la connessione con il server (in millisecondi).
const uint32_t TIMEOUT_CONNESSIONE_CLIENT_MS = 5000;
// Limite di tempo per ricevere la risposta del server (in millisecondi).
const uint32_t TIMEOUT_RISPOSTA_SERVER_MS = 5000;
// Numero massimo di tentativi di invio della richiesta GET.
const uint32_t MAX_TENTATIVI_RICHIESTA = 3;
// Fattore di backoff per nuovi tentativi di invio (in millisecondi).
const uint32_t FATTORE_BACKOFF_MS = 600;
// Limite massimo di attesa prima di un nuovo tentativo (in millisecondi).
const uint32_t MAX_BACKOFF_MS = 3000;
// Per semplicità, usiamo delle variabili globali per le polveri sottili:
uint16_t pm1 = 0; // Concentrazione di PM1 rilevata dal sensore.
uint16_t pm25 = 0; // Concentrazione di PM2,5 rilevata dal sensore.
uint16_t pm10 = 0; // Concentrazione di PM10 rilevata dal sensore.
/* ## FINE PARAMETRI OPZIONALI ##
*
* Da qui in poi ci sono le funzioni: "pezzi di codice" con uno scopo specifico,
* che possono essere richiamate all'occorrenza dal programma principale.
*
* A proposito, uno sketch inizia sempre con la funzione `setup()` (eseguita una
* sola volta) per poi passare alla funzione `loop()` (ripetuta ad libitum).
*
* Nel nostro caso il `loop()` non serve, perché l'ESP32 riparte dal `setup()`
* dopo che la centralina va in risparmio energetico (v. modalità `deep sleep`).
*
* ## INIZIO FUNZIONI HELPER ##
*
* Come suggerisce il nome, sono funzioni scritte per aiutare con determinati
* compiti (nella fattispecie, leggere il sensore e ibernare la centralina).
*
*/
/**
* Legge il sensore (presupponendo che la comunicazione sia già stata aperta) e
* memorizza le misurazioni di polveri sottili nelle apposite variabili globali.
*
* @return `true` se la lettura ha esito positivo, `false` altrimenti.
*/
bool readPM() {
// Assicuriamoci che il buffer I2C sia vuoto prima di ricevere nuovi dati
while (Wire.available()) {
Wire.read();
}
// Leggiamo 32 byte dal bus I2C in base al protocollo di trasporto del sensore
if (Wire.requestFrom(0x2a, 32) != 32) {
return false; // risposta non conforme al protocollo di trasporto
}
uint8_t datiSensore[32] = { 0 };
uint16_t checksumCalcolato = 0;
// Prepariamo i dati da elaborare e i checksum per i controlli di integrità
for (uint8_t i = 0; i < 32; i++) {
datiSensore[i] = Wire.read();
checksumCalcolato += datiSensore[i];
}
checksumCalcolato -= datiSensore[30] + datiSensore[31];
// Validiamo i dati ricevuti e controlliamo l'esito della trasmissione seriale
if (checksumCalcolato == 0 ) {
return false; // sensore non ancora disponibile (tutti i dati uguali a 0)
}
uint16_t checksumRicevuto = (datiSensore[30] << 8) | datiSensore[31];
if (checksumRicevuto != checksumCalcolato) {
return false; // errore di trasmissione (checksum non corrispondenti)
}
// Elaboriamo le rilevazioni di PM e memorizziamole nelle rispettive variabili
pm1 = (datiSensore[10] << 8) | datiSensore[11];
pm25 = (datiSensore[12] << 8) | datiSensore[13];
pm10 = (datiSensore[14] << 8) | datiSensore[15];
return true; // lettura effettuata con successo
}
/**
* Mette la centralina in ibernazione per risparmiare energia quando non rileva.
*
* Nota: in questa versione di base, solo l'ESP32 va in modalità sleep. Quindi:
*
* 1. La ventola del sensore PM gira sempre (e consuma corrente).
* 2. Al risveglio dell'ESP32 sono già disponibili rilevazioni stabili.
* 3. L'attività continuativa potrebbe influire sul tempo di vita del sensore.
*
* (Anche il sensore ha una modalità sleep, ma qui non viene usata per mantenere
* più semplice la fase di assemblaggio. Per una variante più efficiente, vedi:
* https://comunitacircolare.it/articoli/scienziaria-approfondimenti-centraline)
*/
void hibernate() {
// Disabilitiamo il Wi-Fi (v. documentazione "Sleep Modes" dell'ESP32)
WiFi.mode(WIFI_OFF);
// Disattiviamo esplicitamente i domini che non ci servono (attivandoli prima
// a causa di un bug, v. https://esp32.com/viewtopic.php?p=132473#p132473)
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_OFF);
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_ON);
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_OFF);
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_ON);
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_OFF);
esp_sleep_pd_config(ESP_PD_DOMAIN_XTAL, ESP_PD_OPTION_ON);
esp_sleep_pd_config(ESP_PD_DOMAIN_XTAL, ESP_PD_OPTION_OFF);
// Calcoliamo il tempo mancante fino al prossimo ciclo in millisecondi
uint32_t sleepMillis = 60000UL * INTERVALLO_AGGIORNAMENTI_M - millis();
esp_deep_sleep(1000ULL * sleepMillis); // e lo convertiamo in microsecondi
// Nota: in realtà l'ESP32 non tiene il tempo con gran precisione (a lungo
// andare tende a perdere qualche secondo), ma ai nostri fini va bene così.
}
/* ## FINE FUNZIONI HELPER (E INIZIO PROGRAMMA PRINCIPALE) ## */
/**
* Qui dentro c'è un intero ciclo di misurazioni e invio dei dati rilevati.
*
* Questa funzione infatti viene eseguita non solo appena la centralina riceve
* corrente, ma anche ogni volta che si risveglia dalla modalità ibernazione.
*/
void setup() {
/*** IMPOSTAZIONI PRELIMINARI ***/
// Se siamo al primo ciclo della centralina (e cioè al primo avvio dell'ESP32)
if (esp_sleep_get_wakeup_cause() != ESP_SLEEP_WAKEUP_TIMER) {
// Aspettiamo che il sensore si stabilizzi prima di richiedere dati
esp_deep_sleep(1000000ULL * STABILIZZAZIONE_SENSORE_S);
}
// Nota: la ventola del sensore di particelle ha bisogno di tempo per creare
// un flusso d'aria costante (indispensabile per misurazioni affidabili), ma
// finché è alimentata poi continua a girare anche se l'ESP32 è in deep sleep
// (v. anche il commento sulla funzione `hibernate()` poco sopra). Per questo
// basta stabilizzare il sensore una sola volta, ovvero quando centralina è
// appena stata collegata alla corrente (e la ventola parte da ferma).
// Assicuriamoci che la frequenza della CPU sia 80 MHz per risparmiare energia
if (getCpuFrequencyMhz() != 80) {
setCpuFrequencyMhz(80);
}
// Nota: se la imposti con Arduino IDE prima di caricare lo sketch (dal menu
// `Tools` o `Strumenti` -> `CPU Frequency`) potrai fare a meno sia delle tre
// righe di codice precedenti che della libreria `esp32-hal-cpu.h` in alto. E
// già che ci sei, puoi anche controllare che `Core Debug Level` sia impostato
// su `None` (tanto gli eventuali log non si potrebbero comunque leggere, dato
// che la centralina non sarà collegata al PC). Queste voci di menu però sono
// disponibili solo se hai già selezionato una scheda ESP32 compatibile.
/*** LETTURA DEL SENSORE ***/
// Inizializziamo la comunicazione I2C per ricevere dati dal sensore
Wire.begin(PIN_SDA, PIN_SCL);
// Totale di letture effettuate in questo ciclo di esecuzione della centralina
uint8_t lettureRiuscite = 0, lettureFallite = 0;
// Media delle misurazioni effettuate in questo ciclo
float pm10media = 0, pm25media = 0, pm1media = 0;
// Leggiamo il sensore ad intervalli definiti in `POLLING_SENSORE_MS` per il
// numero di volte definito in `MAX_LETTURE_SENSORE` e ne calcoliamo la media
while (1) { // usiamo `break` per non aspettare a vuoto dopo l'ultima lettura
// Contiamo sia i tentativi di lettura falliti che quelli riusciti
if (!readPM()) {
lettureFallite++;
} else {
lettureRiuscite++;
// Calcoliamo la media cumulativa delle misurazioni effettuate
pm10media = (pm10 + (lettureRiuscite - 1) * pm10media) / lettureRiuscite;
pm25media = (pm25 + (lettureRiuscite - 1) * pm25media) / lettureRiuscite;
pm1media = (pm1 + (lettureRiuscite - 1) * pm1media) / lettureRiuscite;
}
// Se abbiamo raggiunto il limite di letture, usciamo dal `while`
if (lettureRiuscite + lettureFallite >= MAX_LETTURE_SENSORE)
break;
// Altrimenti attendiamo fino alla prossima rilevazione
delay(POLLING_SENSORE_MS);
}
// Nota: chiudere esplicitamente la comunicazione con il sensore è superfluo,
// perché tutta la memoria dell'ESP32 verrà comunque deallocata a fine ciclo.
// Percentuale di successo del sensore durante questo ciclo della centralina
float esitoLettureSensore = 100.0 * lettureRiuscite / (lettureRiuscite + lettureFallite);
/*** CONNESSIONE AL WI-FI ***/
uint32_t inizioWifiMillis = millis();
WiFi.begin(NOME_RETE_WIFI, PASSWORD_WIFI);
// Controlliamo il Wi-Fi finché siamo connessi o sforiamo il limite di tempo
while (WiFi.status() != WL_CONNECTED) {
if ((millis() - inizioWifiMillis) > TIMEOUT_WIFI_MS) {
// Senza Wi-Fi non possiamo inviare i dati, quindi mettiamo la centralina
// in modalità risparmio energetico per poi riprovarci al prossimo ciclo.
hibernate();
}
// Nota: il timeout è necessario per via di un bug che potrebbe verificarsi
// in condizioni di bassa potenza del segnale Wi-Fi, in cui l'ESP32 smette
// di tentare la riconnessione e resta indefinitamente bloccata in questo
// `while` (anche qualora la rete dovesse tornare a essere disponibile).
//
// A prescindere dall'implementazione dell'ESP32, nel nostro caso prevedere
// un timeout è comunque positivo in termini di efficienza energetica (come
// misura preventiva verso eventuali indisponibilità prolungate del Wi-Fi).
}
// Potenza del segnale Wi-Fi (Received Signal Strength Indicator)
int8_t rssi = WiFi.RSSI();
// Tempo di connessione al Wi-Fi (in secondi)
float tempoWifi = (millis() - inizioWifiMillis) / 1000.0;
/*** INVIO DATI A THINGSPEAK ***/
// Creiamo la richiesta nel formato definito dall'API di ThingSpeak
String uri = "/update?api_key=" + String(WRITE_API_KEY);
// Inviamo i dati sulle polveri sottili solo se è riuscita almeno una lettura
if (lettureRiuscite > 0) {
uri += "&field1=" + String(pm10media, 2)
+ "&field2=" + String(pm25media, 2)
+ "&field3=" + String(pm1media, 2);
}
// Invece questi dati di controllo li inviamo sempre e comunque
uri += "&field4=" + String(esitoLettureSensore, 2)
+ "&field5=" + String(rssi)
+ "&field6=" + String(tempoWifi, 3);
// Prepariamo i client per la richiesta GET via HTTPS
NetworkClientSecure client; // Client per la gestione del protocollo HTTPS
HTTPClient httpClient; // Client per la gestione della richiesta GET
// Impostiamo il certificato e il timeout per l'handshake SSL/TLS
client.setCACert(CA_CERT);
client.setHandshakeTimeout(TIMEOUT_HANDSHAKE_SSL_S);
// Impostiamo i timeout per la connessione del client e la risposta del server
// (v. https://github.com/espressif/arduino-esp32/issues/1433#issuecomment-475875579)
httpClient.setConnectTimeout(TIMEOUT_CONNESSIONE_CLIENT_MS);
httpClient.setTimeout(TIMEOUT_RISPOSTA_SERVER_MS);
// Nota: di default la connessione TCP viene lasciata aperta per il riuso, ma
// purtroppo non sembra sopravvivere al riavvio dell'ESP32 nemmeno se i client
// sono preservati nella memoria RTC. Il guadagno sarebbe comunque marginale.
httpClient.begin(client, "api.thingspeak.com", 443, uri, true);
uint8_t contatoreRichieste = 0;
uint32_t attesaBackoff = 0;
// Tentiamo l'invio ad intervalli calcolati usando un algoritmo di backoff
// esponenziale in base ai parametri `FATTORE_BACKOFF_MS` e `MAX_BACKOFF_MS`
// per un massimo numero di volte definito in `MAX_TENTATIVI_RICHIESTA`
while (httpClient.GET() != HTTP_CODE_OK) {
// Se abbiamo raggiunto il limite di tentativi, usciamo dal `while`
if (++contatoreRichieste >= MAX_TENTATIVI_RICHIESTA)
break;
// Altrimenti attendiamo fino alla prossima richiesta
attesaBackoff = FATTORE_BACKOFF_MS << (contatoreRichieste - 1);
attesaBackoff = min(attesaBackoff, MAX_BACKOFF_MS);
delay(attesaBackoff);
// Nota: oggettivamente, non era affatto necessario un meccanismo di backoff
// esponenziale: sarebbe bastato anche solo riprovare a intervalli fissi...
// ma ormai è stato già implementato, quindi tanto vale lasciarlo lì! :P
}
// Nota: questo ciclo è finito, evitiamo di deallocare i client per lo stesso
// motivo per cui non abbiamo deinizializzato la comunicazione con il sensore.
// Andiamo in modalità risparmio energetico fino al prossimo ciclo
hibernate();
}
/**
* Questa funzione è obbligatoria. Ma anche no ¯\_(ツ)_/¯
*/
void loop() {}
Versione base PMS5003
Torna all'indice 👆️
File sorgente: scienziaria-centralina-pms5003.ino
Codice da copiare:
/**
*
* Codice sorgente per configurare una centralina di monitoraggio della qualità
* dell'aria ispirata a quelle del progetto ScienziAria di Comunità Circolare.
*
* GUIDA PASSO PASSO per costruire la tua CENTRALINA FAI DA TE
* 👉️ https://comunitacircolare.it/articoli/scienziaria-guida-centralina
*
* Questa versione di base è pensata espressamente per chi non ha particolari
* competenze tecniche ed è incentrata quindi sulla semplicità di realizzazione.
*
* Se non hai mai programmato, NIENTE PANICO! Ti basta inserire i tre parametri
* richiesti qui sotto all'interno delle virgolette, senza toccare nient'altro.
*
* Altrimenti, modifica pure il codice come ti pare (a tuo rischio e pericolo).
*
* \author Michele Nuzzolese
* \copyright Pubblico dominio con licenza Unlicense: https://unlicense.org
* \sa Progetto ScienziAria: https://comunitacircolare.it/progetti/scienziaria
*
*/
/* ## PARAMETRI RICHIESTI ## */
// 1. Nome della rete Wi-Fi, da ricopiare esattamente come appare nella lista di
// reti disponibili sul tuo smartphone o computer (includendo eventuali spazi o
// punteggiatura e rispettando la differenza tra lettere maiuscole e minuscole).
const char *NOME_RETE_WIFI = "scrivi qui il nome della tua rete Wi-Fi";
// 2. Password del Wi-Fi: occhio a maiuscole/minuscole/errori di battitura/etc.
const char *PASSWORD_WIFI = "qui dentro ci va la password";
// 3. `Write API Key` (la trovi nella tab `API Keys` del tuo canale ThingSpeak).
const char *WRITE_API_KEY = "0RM414VR41C4P1T0";
/* ## FINE CONFIGURAZIONI OBBLIGATORIE ##
*
* Congratulazioni! Non ti resta che caricare questo codice nella tua centralina
* con Arduino IDE (per istruzioni passo passo c'è sempre la guida in alto 👆️).
*
* ## INIZIO PROGRAMMA (E PARAMETRI OPZIONALI) ##
*
* Il codice sorgente ti fa paura? Scappa ora, prima che sia troppo tardi! 😜️
*
* Nel seguito troverai tutti i parametri opzionali legati alla logica interna
* della centralina. C'è qualche commento per facilitare la comprensione, ma non
* potevo dilungarmi troppo. Nel dubbio, puoi sempre usare i valori predefiniti.
*
* Se invece vuoi spiegazioni più dettagliate, ecco l'articolo che fa per te:
* https://comunitacircolare.it/articoli/scienziaria-approfondimenti-centraline
*
*/
#include <WiFi.h>
#include <HTTPClient.h>
#include <NetworkClientSecure.h>
#include <esp32-hal-cpu.h>
#include <esp_sleep.h>
// I valori indicati di seguito si basano sulle istruzioni della guida e devono
// rispecchiare l'effettivo collegamento tra il sensore e la scheda ESP32:
const uint8_t PIN_RX2 = 26; // Da collegare al pin TXD del sensore.
const uint8_t PIN_TX2 = 27; // Da collegare al pin RXD del sensore.
// Frequenza con cui la centralina invia nuove rilevazioni (in minuti).
const uint8_t INTERVALLO_AGGIORNAMENTI_M = 4;
// Tempo di attesa per letture stabili dall'avvio della ventola (in secondi).
const uint8_t STABILIZZAZIONE_SENSORE_S = 30;
// Minimo intervallo di tempo tra due letture contigue (in millisecondi).
const uint32_t POLLING_SENSORE_MS = 1200;
// Numero massimo di rilevazioni del sensore su cui calcolare la media.
const uint8_t MAX_LETTURE_SENSORE = 6;
// Limite di tempo per tentare di connettersi al Wi-Fi (in millisecondi).
const uint32_t TIMEOUT_WIFI_MS = 10000;
// Certificato del server di ThingSpeak per la connessione sicura via HTTPS.
const char *CA_CERT =
"-----BEGIN CERTIFICATE-----\n"
"MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh\n"
"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n"
"d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\n"
"MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT\n"
"MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\n"
"b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG\n"
"9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI\n"
"2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx\n"
"1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ\n"
"q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz\n"
"tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ\n"
"vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP\n"
"BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV\n"
"5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY\n"
"1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4\n"
"NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG\n"
"Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91\n"
"8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe\n"
"pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl\n"
"MrY=\n"
"-----END CERTIFICATE-----";
// Limite di tempo per effettuare l'handshake SSL (in secondi).
const uint32_t TIMEOUT_HANDSHAKE_SSL_S = 5;
// Limite di tempo per stabilire la connessione con il server (in millisecondi).
const uint32_t TIMEOUT_CONNESSIONE_CLIENT_MS = 5000;
// Limite di tempo per ricevere la risposta del server (in millisecondi).
const uint32_t TIMEOUT_RISPOSTA_SERVER_MS = 5000;
// Numero massimo di tentativi di invio della richiesta GET.
const uint32_t MAX_TENTATIVI_RICHIESTA = 3;
// Fattore di backoff per nuovi tentativi di invio (in millisecondi).
const uint32_t FATTORE_BACKOFF_MS = 600;
// Limite massimo di attesa prima di un nuovo tentativo (in millisecondi).
const uint32_t MAX_BACKOFF_MS = 3000;
// Per semplicità, usiamo delle variabili globali per le polveri sottili:
uint16_t pm1 = 0; // Concentrazione di PM1 rilevata dal sensore.
uint16_t pm25 = 0; // Concentrazione di PM2,5 rilevata dal sensore.
uint16_t pm10 = 0; // Concentrazione di PM10 rilevata dal sensore.
/* ## FINE PARAMETRI OPZIONALI ##
*
* Da qui in poi ci sono le funzioni: "pezzi di codice" con uno scopo specifico,
* che possono essere richiamate all'occorrenza dal programma principale.
*
* A proposito, uno sketch inizia sempre con la funzione `setup()` (eseguita una
* sola volta) per poi passare alla funzione `loop()` (ripetuta ad libitum).
*
* Nel nostro caso il `loop()` non serve, perché l'ESP32 riparte dal `setup()`
* dopo che la centralina va in risparmio energetico (v. modalità `deep sleep`).
*
* ## INIZIO FUNZIONI HELPER ##
*
* Come suggerisce il nome, sono funzioni scritte per aiutare con determinati
* compiti (nella fattispecie, leggere il sensore e ibernare la centralina).
*
*/
/**
* Inizializza la comunicazione con il sensore e lo predispone alla lettura.
*
* Nota: abbiamo optato per un'implementazione meno efficiente ma più semplice e
* robusta, per prevenire potenziali rilevazioni fallite in casi limite (sulla
* base del funzionamento del sensore). Per approfondimenti, vedi l'articolo:
* https://comunitacircolare.it/articoli/scienziaria-approfondimenti-centraline
*
*/
void initPM() {
// Inizializziamo la comunicazione seriale
Serial2.begin(9600, SERIAL_8N1, PIN_RX2, PIN_TX2);
// Comando per mettere il sensore in `passive mode` (invio dati su richiesta)
uint8_t comandoPassiveMode[7] = { 0x42, 0x4D, 0xE1, 0x00, 0x00, 0x01, 0x70 };
// Inviamo il comando (a cui il sensore risponde con un messaggio di conferma)
Serial2.write(comandoPassiveMode, 7);
// Assicuriamoci che il sensore sia pronto per la lettura prima di proseguire
delay(942); // tempo di attesa = test empirici + ampio margine di sicurezza
}
/**
* Legge il sensore (presupponendo che la comunicazione sia già stata aperta) e
* memorizza le misurazioni di polveri sottili nelle apposite variabili globali.
*
* @return `true` se la lettura ha esito positivo, `false` altrimenti.
*/
bool readPM() {
// Scartiamo eventuali dati spuri che potrebbero interferire con la lettura
while (Serial2.available()) {
Serial2.read();
}
// Comando per richiedere una lettura quando il sensore è in `passive mode`
uint8_t comandoRead[7] = { 0x42, 0x4D, 0xE2, 0x00, 0x00, 0x01, 0x71 };
// Inviamo il comando (a cui il sensore risponde con i dati della lettura)
Serial2.write(comandoRead, 7);
// Aspettiamo che la trasmessione seriale del comando sia avvenuta per intero
Serial2.flush();
// Aspettiamo la risposta (dovrebbero bastare 34 ms, ma noi siamo prudenti :P)
delay(42);
// Assicuriamoci di aver ricevuto tutti e soli i 32 byte che ci aspettiamo in
// base al protocollo di trasporto (rif. scheda tecnica del sensore PMS5003)
if (Serial2.available() != 32) {
return false; // risposta assente o non conforme al protocollo di trasporto
}
uint8_t datiSensore[32] = { 0 };
uint16_t checksumCalcolato = 0;
// Prepariamo i dati da elaborare e i checksum per i controlli di integrità
for (uint8_t i = 0; i < 32; i++) {
datiSensore[i] = Serial2.read();
checksumCalcolato += datiSensore[i];
}
checksumCalcolato -= datiSensore[30] + datiSensore[31];
// Validiamo i dati ricevuti e controlliamo l'esito della trasmissione seriale
if (checksumCalcolato == 0 || checksumCalcolato == 0xAB) {
return false; // sensore non disponibile (tutti i dati uguali a 0)
}
uint16_t checksumRicevuto = (datiSensore[30] << 8) | datiSensore[31];
if (checksumRicevuto != checksumCalcolato) {
return false; // errore di trasmissione (checksum non corrispondenti)
}
// Elaboriamo le rilevazioni di PM e memorizziamole nelle rispettive variabili
pm1 = (datiSensore[10] << 8) | datiSensore[11];
pm25 = (datiSensore[12] << 8) | datiSensore[13];
pm10 = (datiSensore[14] << 8) | datiSensore[15];
return true; // lettura effettuata con successo
}
/**
* Mette la centralina in ibernazione per risparmiare energia quando non rileva.
*
* Nota: in questa versione di base, solo l'ESP32 va in modalità sleep. Quindi:
*
* 1. La ventola del sensore PM gira sempre (e consuma corrente).
* 2. Al risveglio dell'ESP32 sono già disponibili rilevazioni stabili.
* 3. L'attività continuativa potrebbe influire sul tempo di vita del sensore.
*
* (Anche il sensore ha una modalità sleep, ma qui non viene usata per mantenere
* più semplice la fase di assemblaggio. Per una variante più efficiente, vedi:
* https://comunitacircolare.it/articoli/scienziaria-approfondimenti-centraline)
*/
void hibernate() {
// Disabilitiamo il Wi-Fi (v. documentazione "Sleep Modes" dell'ESP32)
WiFi.mode(WIFI_OFF);
// Disattiviamo esplicitamente i domini che non ci servono (attivandoli prima
// a causa di un bug, v. https://esp32.com/viewtopic.php?p=132473#p132473)
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_OFF);
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_ON);
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_OFF);
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_ON);
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_OFF);
esp_sleep_pd_config(ESP_PD_DOMAIN_XTAL, ESP_PD_OPTION_ON);
esp_sleep_pd_config(ESP_PD_DOMAIN_XTAL, ESP_PD_OPTION_OFF);
// Calcoliamo il tempo mancante fino al prossimo ciclo in millisecondi
uint32_t sleepMillis = 60000UL * INTERVALLO_AGGIORNAMENTI_M - millis();
esp_deep_sleep(1000ULL * sleepMillis); // e lo convertiamo in microsecondi
// Nota: in realtà l'ESP32 non tiene il tempo con gran precisione (a lungo
// andare tende a perdere qualche secondo), ma ai nostri fini va bene così.
}
/* ## FINE FUNZIONI HELPER (E INIZIO PROGRAMMA PRINCIPALE) ## */
/**
* Qui dentro c'è un intero ciclo di misurazioni e invio dei dati rilevati.
*
* Questa funzione infatti viene eseguita non solo appena la centralina riceve
* corrente, ma anche ogni volta che si risveglia dalla modalità ibernazione.
*/
void setup() {
/*** IMPOSTAZIONI PRELIMINARI ***/
// Se siamo al primo ciclo della centralina (e cioè al primo avvio dell'ESP32)
if (esp_sleep_get_wakeup_cause() != ESP_SLEEP_WAKEUP_TIMER) {
// Aspettiamo che il sensore si stabilizzi prima di richiedere dati
esp_deep_sleep(1000000ULL * STABILIZZAZIONE_SENSORE_S);
}
// Nota: la ventola del sensore di particelle ha bisogno di tempo per creare
// un flusso d'aria costante (indispensabile per misurazioni affidabili), ma
// finché è alimentata poi continua a girare anche se l'ESP32 è in deep sleep
// (v. anche il commento sulla funzione `hibernate()` poco sopra). Per questo
// basta stabilizzare il sensore una sola volta, ovvero quando centralina è
// appena stata collegata alla corrente (e la ventola parte da ferma).
// Assicuriamoci che la frequenza della CPU sia 80 MHz per risparmiare energia
if (getCpuFrequencyMhz() != 80) {
setCpuFrequencyMhz(80);
}
// Nota: se la imposti con Arduino IDE prima di caricare lo sketch (dal menu
// `Tools` o `Strumenti` -> `CPU Frequency`) potrai fare a meno sia delle tre
// righe di codice precedenti che della libreria `esp32-hal-cpu.h` in alto. E
// già che ci sei, puoi anche controllare che `Core Debug Level` sia impostato
// su `None` (tanto gli eventuali log non si potrebbero comunque leggere, dato
// che la centralina non sarà collegata al PC). Queste voci di menu però sono
// disponibili solo se hai già selezionato una scheda ESP32 compatibile.
/*** LETTURA DEL SENSORE ***/
// Inizializziamo la comunicazione con il sensore
initPM();
// Totale di letture effettuate in questo ciclo di esecuzione della centralina
uint8_t lettureRiuscite = 0, lettureFallite = 0;
// Media delle misurazioni effettuate in questo ciclo
float pm10media = 0, pm25media = 0, pm1media = 0;
// Leggiamo il sensore ad intervalli definiti in `POLLING_SENSORE_MS` per il
// numero di volte definito in `MAX_LETTURE_SENSORE` e ne calcoliamo la media
while (1) { // usiamo `break` per non aspettare a vuoto dopo l'ultima lettura
// Contiamo sia i tentativi di lettura falliti che quelli riusciti
if (!readPM()) {
lettureFallite++;
} else {
lettureRiuscite++;
// Calcoliamo la media cumulativa delle misurazioni effettuate
pm10media = (pm10 + (lettureRiuscite - 1) * pm10media) / lettureRiuscite;
pm25media = (pm25 + (lettureRiuscite - 1) * pm25media) / lettureRiuscite;
pm1media = (pm1 + (lettureRiuscite - 1) * pm1media) / lettureRiuscite;
}
// Se abbiamo raggiunto il limite di letture, usciamo dal `while`
if (lettureRiuscite + lettureFallite >= MAX_LETTURE_SENSORE)
break;
// Altrimenti attendiamo fino alla prossima rilevazione
delay(POLLING_SENSORE_MS);
}
// Nota: chiudere esplicitamente la comunicazione con il sensore è superfluo,
// perché tutta la memoria dell'ESP32 verrà comunque deallocata a fine ciclo.
// Percentuale di successo del sensore durante questo ciclo della centralina
float esitoLettureSensore = 100.0 * lettureRiuscite / (lettureRiuscite + lettureFallite);
/*** CONNESSIONE AL WI-FI ***/
uint32_t inizioWifiMillis = millis();
WiFi.begin(NOME_RETE_WIFI, PASSWORD_WIFI);
// Controlliamo il Wi-Fi finché siamo connessi o sforiamo il limite di tempo
while (WiFi.status() != WL_CONNECTED) {
if ((millis() - inizioWifiMillis) > TIMEOUT_WIFI_MS) {
// Senza Wi-Fi non possiamo inviare i dati, quindi mettiamo la centralina
// in modalità risparmio energetico per poi riprovarci al prossimo ciclo.
hibernate();
}
// Nota: il timeout è necessario per via di un bug che potrebbe verificarsi
// in condizioni di bassa potenza del segnale Wi-Fi, in cui l'ESP32 smette
// di tentare la riconnessione e resta indefinitamente bloccata in questo
// `while` (anche qualora la rete dovesse tornare a essere disponibile).
//
// A prescindere dall'implementazione dell'ESP32, nel nostro caso prevedere
// un timeout è comunque positivo in termini di efficienza energetica (come
// misura preventiva verso eventuali indisponibilità prolungate del Wi-Fi).
}
// Potenza del segnale Wi-Fi (Received Signal Strength Indicator)
int8_t rssi = WiFi.RSSI();
// Tempo di connessione al Wi-Fi (in secondi)
float tempoWifi = (millis() - inizioWifiMillis) / 1000.0;
/*** INVIO DATI A THINGSPEAK ***/
// Creiamo la richiesta nel formato definito dall'API di ThingSpeak
String uri = "/update?api_key=" + String(WRITE_API_KEY);
// Inviamo i dati sulle polveri sottili solo se è riuscita almeno una lettura
if (lettureRiuscite > 0) {
uri += "&field1=" + String(pm10media, 2)
+ "&field2=" + String(pm25media, 2)
+ "&field3=" + String(pm1media, 2);
}
// Invece questi dati di controllo li inviamo sempre e comunque
uri += "&field4=" + String(esitoLettureSensore, 2)
+ "&field5=" + String(rssi)
+ "&field6=" + String(tempoWifi, 3);
// Prepariamo i client per la richiesta GET via HTTPS
NetworkClientSecure client; // Client per la gestione del protocollo HTTPS
HTTPClient httpClient; // Client per la gestione della richiesta GET
// Impostiamo il certificato e il timeout per l'handshake SSL/TLS
client.setCACert(CA_CERT);
client.setHandshakeTimeout(TIMEOUT_HANDSHAKE_SSL_S);
// Impostiamo i timeout per la connessione del client e la risposta del server
// (v. https://github.com/espressif/arduino-esp32/issues/1433#issuecomment-475875579)
httpClient.setConnectTimeout(TIMEOUT_CONNESSIONE_CLIENT_MS);
httpClient.setTimeout(TIMEOUT_RISPOSTA_SERVER_MS);
// Nota: di default la connessione TCP viene lasciata aperta per il riuso, ma
// purtroppo non sembra sopravvivere al riavvio dell'ESP32 nemmeno se i client
// sono preservati nella memoria RTC. Il guadagno sarebbe comunque marginale.
httpClient.begin(client, "api.thingspeak.com", 443, uri, true);
uint8_t contatoreRichieste = 0;
uint32_t attesaBackoff = 0;
// Tentiamo l'invio ad intervalli calcolati usando un algoritmo di backoff
// esponenziale in base ai parametri `FATTORE_BACKOFF_MS` e `MAX_BACKOFF_MS`
// per un massimo numero di volte definito in `MAX_TENTATIVI_RICHIESTA`
while (httpClient.GET() != HTTP_CODE_OK) {
// Se abbiamo raggiunto il limite di tentativi, usciamo dal `while`
if (++contatoreRichieste >= MAX_TENTATIVI_RICHIESTA)
break;
// Altrimenti attendiamo fino alla prossima richiesta
attesaBackoff = FATTORE_BACKOFF_MS << (contatoreRichieste - 1);
attesaBackoff = min(attesaBackoff, MAX_BACKOFF_MS);
delay(attesaBackoff);
// Nota: oggettivamente, non era affatto necessario un meccanismo di backoff
// esponenziale: sarebbe bastato anche solo riprovare a intervalli fissi...
// ma ormai è stato già implementato, quindi tanto vale lasciarlo lì! :P
}
// Nota: questo ciclo è finito, evitiamo di deallocare i client per lo stesso
// motivo per cui non abbiamo deinizializzato la comunicazione con il sensore.
// Andiamo in modalità risparmio energetico fino al prossimo ciclo
hibernate();
}
/**
* Questa funzione è obbligatoria. Ma anche no ¯\_(ツ)_/¯
*/
void loop() {}
Variante risparmio energetico KS0578
Torna all'indice 👆️
File sorgente: scienziaria-centralina-ks0578-sleep.ino
Codice da copiare:
/**
*
* Codice sorgente per configurare una centralina di monitoraggio della qualità
* dell'aria ispirata a quelle del progetto ScienziAria di Comunità Circolare.
*
* GUIDA PASSO PASSO per costruire la tua CENTRALINA FAI DA TE
* 👉️ https://comunitacircolare.it/articoli/scienziaria-guida-centralina
*
* La versione della guida è pensata espressamente per chi non ha particolari
* competenze tecniche ed è incentrata quindi sulla semplicità di realizzazione.
*
* Questa variante sfrutta la modalità risparmio energetico del sensore sia per
* consumare meno corrente che (soprattutto) per prolungarne la longevità media.
* A parte questo è identica alla versione base, con cui condivide la stragrande
* maggioranza del codice e dei commenti (anche per facilitarne il raffronto).
*
* Se non hai mai programmato, NIENTE PANICO! Ti basta inserire i tre parametri
* richiesti qui sotto all'interno delle virgolette, senza toccare nient'altro.
*
* Altrimenti, modifica pure il codice come ti pare (a tuo rischio e pericolo).
*
* \author Michele Nuzzolese
* \copyright Pubblico dominio con licenza Unlicense: https://unlicense.org
* \sa Progetto ScienziAria: https://comunitacircolare.it/progetti/scienziaria
*
*/
/* ## PARAMETRI RICHIESTI ## */
// 1. Nome della rete Wi-Fi, da ricopiare esattamente come appare nella lista di
// reti disponibili sul tuo smartphone o computer (includendo eventuali spazi o
// punteggiatura e rispettando la differenza tra lettere maiuscole e minuscole).
const char *NOME_RETE_WIFI = "scrivi qui il nome della tua rete Wi-Fi";
// 2. Password del Wi-Fi: occhio a maiuscole/minuscole/errori di battitura/etc.
const char *PASSWORD_WIFI = "qui dentro ci va la password";
// 3. `Write API Key` (la trovi nella tab `API Keys` del tuo canale ThingSpeak).
const char *WRITE_API_KEY = "0RM414VR41C4P1T0";
/* ## FINE CONFIGURAZIONI OBBLIGATORIE ##
*
* Congratulazioni! Non ti resta che caricare questo codice nella tua centralina
* con Arduino IDE (per istruzioni passo passo c'è sempre la guida in alto 👆️).
*
* ## INIZIO PROGRAMMA (E PARAMETRI OPZIONALI) ##
*
* Il codice sorgente ti fa paura? Scappa ora, prima che sia troppo tardi! 😜️
*
* Nel seguito troverai tutti i parametri opzionali legati alla logica interna
* della centralina. C'è qualche commento per facilitare la comprensione, ma non
* potevo dilungarmi troppo. Nel dubbio, puoi sempre usare i valori predefiniti.
*
* Se invece vuoi spiegazioni più dettagliate, ecco l'articolo che fa per te:
* https://comunitacircolare.it/articoli/scienziaria-approfondimenti-centraline
*
*/
#include <Wire.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <NetworkClientSecure.h>
#include <esp32-hal-cpu.h>
#include <esp_sleep.h>
// I valori indicati di seguito si basano sulle istruzioni della guida e devono
// rispecchiare l'effettivo collegamento tra il sensore e la scheda ESP32:
const uint8_t PIN_SDA = 27; // Terzo pin del sensore (da sinistra a destra).
const uint8_t PIN_SCL = 26; // Quarto pin del sensore (da sinistra a destra).
const uint8_t PIN_SET = 25; // Da collegare al pin SET del sensore.
// Frequenza con cui la centralina invia nuove rilevazioni (in minuti).
const uint8_t INTERVALLO_AGGIORNAMENTI_M = 4;
// Tempo di attesa per letture stabili dall'avvio della ventola (in secondi).
const uint32_t STABILIZZAZIONE_SENSORE_S = 30;
// Minimo intervallo di tempo tra due letture contigue (in millisecondi).
const uint32_t POLLING_SENSORE_MS = 1200;
// Numero massimo di rilevazioni del sensore su cui calcolare la media.
const uint8_t MAX_LETTURE_SENSORE = 6;
// Limite di tempo per tentare di connettersi al Wi-Fi (in millisecondi).
const uint32_t TIMEOUT_WIFI_MS = 10000;
// Certificato del server di ThingSpeak per la connessione sicura via HTTPS.
const char *CA_CERT =
"-----BEGIN CERTIFICATE-----\n"
"MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh\n"
"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n"
"d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\n"
"MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT\n"
"MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\n"
"b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG\n"
"9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI\n"
"2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx\n"
"1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ\n"
"q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz\n"
"tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ\n"
"vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP\n"
"BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV\n"
"5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY\n"
"1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4\n"
"NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG\n"
"Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91\n"
"8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe\n"
"pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl\n"
"MrY=\n"
"-----END CERTIFICATE-----";
// Limite di tempo per effettuare l'handshake SSL (in secondi).
const uint32_t TIMEOUT_HANDSHAKE_SSL_S = 5;
// Limite di tempo per stabilire la connessione con il server (in millisecondi).
const uint32_t TIMEOUT_CONNESSIONE_CLIENT_MS = 5000;
// Limite di tempo per ricevere la risposta del server (in millisecondi).
const uint32_t TIMEOUT_RISPOSTA_SERVER_MS = 5000;
// Numero massimo di tentativi di invio della richiesta GET.
const uint32_t MAX_TENTATIVI_RICHIESTA = 3;
// Fattore di backoff per nuovi tentativi di invio (in millisecondi).
const uint32_t FATTORE_BACKOFF_MS = 600;
// Limite massimo di attesa prima di un nuovo tentativo (in millisecondi).
const uint32_t MAX_BACKOFF_MS = 3000;
// Per semplicità, usiamo delle variabili globali per le polveri sottili:
uint16_t pm1 = 0; // Concentrazione di PM1 rilevata dal sensore.
uint16_t pm25 = 0; // Concentrazione di PM2,5 rilevata dal sensore.
uint16_t pm10 = 0; // Concentrazione di PM10 rilevata dal sensore.
/* ## FINE PARAMETRI OPZIONALI ##
*
* Da qui in poi ci sono le funzioni: "pezzi di codice" con uno scopo specifico,
* che possono essere richiamate all'occorrenza dal programma principale.
*
* A proposito, uno sketch inizia sempre con la funzione `setup()` (eseguita una
* sola volta) per poi passare alla funzione `loop()` (ripetuta ad libitum).
*
* Nel nostro caso il `loop()` non serve, perché l'ESP32 riparte dal `setup()`
* dopo che la centralina va in risparmio energetico (v. modalità `deep sleep`).
*
* ## INIZIO FUNZIONI HELPER ##
*
* Come suggerisce il nome, sono funzioni scritte per aiutare con determinati
* compiti (nella fattispecie, leggere il sensore e ibernare la centralina).
*
*/
/**
* Legge il sensore (presupponendo che la comunicazione sia già stata aperta) e
* memorizza le misurazioni di polveri sottili nelle apposite variabili globali.
*
* @return `true` se la lettura ha esito positivo, `false` altrimenti.
*/
bool readPM() {
// Assicuriamoci che il buffer I2C sia vuoto prima di ricevere nuovi dati
while (Wire.available()) {
Wire.read();
}
// Leggiamo 32 byte dal bus I2C in base al protocollo di trasporto del sensore
if (Wire.requestFrom(0x2a, 32) != 32) {
return false; // risposta non conforme al protocollo di trasporto
}
uint8_t datiSensore[32] = { 0 };
uint16_t checksumCalcolato = 0;
// Prepariamo i dati da elaborare e i checksum per i controlli di integrità
for (uint8_t i = 0; i < 32; i++) {
datiSensore[i] = Wire.read();
checksumCalcolato += datiSensore[i];
}
checksumCalcolato -= datiSensore[30] + datiSensore[31];
// Validiamo i dati ricevuti e controlliamo l'esito della trasmissione seriale
if (checksumCalcolato == 0 ) {
return false; // sensore non ancora disponibile (tutti i dati uguali a 0)
}
uint16_t checksumRicevuto = (datiSensore[30] << 8) | datiSensore[31];
if (checksumRicevuto != checksumCalcolato) {
return false; // errore di trasmissione (checksum non corrispondenti)
}
// Elaboriamo le rilevazioni di PM e memorizziamole nelle rispettive variabili
pm1 = (datiSensore[10] << 8) | datiSensore[11];
pm25 = (datiSensore[12] << 8) | datiSensore[13];
pm10 = (datiSensore[14] << 8) | datiSensore[15];
return true; // lettura effettuata con successo
}
/**
* Mette la centralina in ibernazione per risparmiare energia quando non rileva.
*
* Nota: in questa variante, sia l'ESP32 che il sensore vanno in modalità sleep.
* Bisognerà quindi stabilizzare nuovamente il sensore al risveglio dell'ESP32,
* perché la ventola smette di girare quando l'attività del sensore è sospesa.
*/
void hibernate() {
// Disabilitiamo il Wi-Fi (v. documentazione "Sleep Modes" dell'ESP32)
WiFi.mode(WIFI_OFF);
// Mettiamo il sensore in modalità risparmio energetico
digitalWrite(PIN_SET, LOW);
// Blocchiamo il GPIO per assicurarci che lo stato del pin non cambi durante
// la modalità deep sleep (v. documentazione "GPIO & RTC GPIO" dell'ESP32)
gpio_hold_en((gpio_num_t)PIN_SET);
gpio_deep_sleep_hold_en();
// Disattiviamo esplicitamente i domini che non ci servono (attivandoli prima
// a causa di un bug, v. https://esp32.com/viewtopic.php?p=132473#p132473)
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_OFF);
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_ON);
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_OFF);
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_ON);
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_OFF);
esp_sleep_pd_config(ESP_PD_DOMAIN_XTAL, ESP_PD_OPTION_ON);
esp_sleep_pd_config(ESP_PD_DOMAIN_XTAL, ESP_PD_OPTION_OFF);
// Calcoliamo il tempo mancante fino al prossimo ciclo in millisecondi
uint32_t sleepMillis = 60000UL * INTERVALLO_AGGIORNAMENTI_M - millis();
esp_deep_sleep(1000ULL * sleepMillis); // e lo convertiamo in microsecondi
// Nota: in realtà l'ESP32 non tiene il tempo con gran precisione (a lungo
// andare tende a perdere qualche secondo), ma ai nostri fini va bene così.
}
/* ## FINE FUNZIONI HELPER (E INIZIO PROGRAMMA PRINCIPALE) ## */
/**
* Qui dentro c'è un intero ciclo di misurazioni e invio dei dati rilevati.
*
* Questa funzione infatti viene eseguita non solo appena la centralina riceve
* corrente, ma anche ogni volta che si risveglia dalla modalità ibernazione.
*/
void setup() {
/*** IMPOSTAZIONI PRELIMINARI ***/
// Assicuriamoci che la frequenza della CPU sia 80 MHz per risparmiare energia
if (getCpuFrequencyMhz() != 80) {
setCpuFrequencyMhz(80);
}
// Nota: se la imposti con Arduino IDE prima di caricare lo sketch (dal menu
// `Tools` o `Strumenti` -> `CPU Frequency`) potrai fare a meno sia delle tre
// righe di codice precedenti che della libreria `esp32-hal-cpu.h` in alto. E
// già che ci sei, puoi anche controllare che `Core Debug Level` sia impostato
// su `None` (tanto gli eventuali log non si potrebbero comunque leggere, dato
// che la centralina non sarà collegata al PC). Queste voci di menu però sono
// disponibili solo se hai già selezionato una scheda ESP32 compatibile.
// Se la centralina era in ibernazione (e quindi questo non è il primo avvio)
if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_TIMER) {
// Disabilitiamo il blocco del GPIO per riattivare il sensore
gpio_hold_dis((gpio_num_t)PIN_SET);
}
// Inizializziamo il pin per gestire la modalità sleep del sensore:
pinMode(PIN_SET, OUTPUT); // il livello di output predefinito è LOW
digitalWrite(PIN_SET, HIGH); // ma il sensore deve essere HIGH per monitorare
// Entriamo in light sleep mentre aspettiamo che il sensore si stabilizzi
esp_sleep_enable_timer_wakeup(1000000ULL * STABILIZZAZIONE_SENSORE_S);
esp_light_sleep_start();
// Nota: la ventola del sensore di particelle ha bisogno di tempo per creare
// un flusso d'aria costante (indispensabile per misurazioni affidabili). In
// questa variante stabilizziamo il sensore a ogni ciclo di esecuzione della
// centralina, poiché la ventola resta ferma durante la modalità ibernazione
// (v. anche il commento sulla funzione `hibernate()` poco sopra).
/*** LETTURA DEL SENSORE ***/
// Inizializziamo la comunicazione I2C per ricevere dati dal sensore
Wire.begin(PIN_SDA, PIN_SCL);
// Totale di letture effettuate in questo ciclo di esecuzione della centralina
uint8_t lettureRiuscite = 0, lettureFallite = 0;
// Media delle misurazioni effettuate in questo ciclo
float pm10avg = 0, pm25avg = 0, pm1avg = 0;
// Leggiamo il sensore ad intervalli definiti in `POLLING_SENSORE_MS` per il
// numero di volte definito in `MAX_LETTURE_SENSORE` e ne calcoliamo la media
while (1) { // usiamo `break` per non aspettare a vuoto dopo l'ultima lettura
// Contiamo sia i tentativi di lettura falliti che quelli riusciti
if (!readPM()) {
lettureFallite++;
} else {
lettureRiuscite++;
// Calcoliamo la media cumulativa delle misurazioni effettuate
pm10avg = (pm10 + (lettureRiuscite - 1) * pm10avg) / lettureRiuscite;
pm25avg = (pm25 + (lettureRiuscite - 1) * pm25avg) / lettureRiuscite;
pm1avg = (pm1 + (lettureRiuscite - 1) * pm1avg) / lettureRiuscite;
}
// Se abbiamo raggiunto il limite di letture, usciamo dal `while`
if (lettureRiuscite + lettureFallite >= MAX_LETTURE_SENSORE)
break;
// Altrimenti attendiamo fino alla prossima rilevazione
delay(POLLING_SENSORE_MS);
}
// Nota: chiudere esplicitamente la comunicazione con il sensore è superfluo,
// perché tutta la memoria dell'ESP32 verrà comunque deallocata a fine ciclo.
// Percentuale di successo del sensore durante questo ciclo della centralina
float esitoLettureSensore = 100.0 * lettureRiuscite / (lettureRiuscite + lettureFallite);
/*** CONNESSIONE AL WI-FI ***/
uint32_t inizioWifiMillis = millis();
WiFi.begin(NOME_RETE_WIFI, PASSWORD_WIFI);
// Controlliamo il Wi-Fi finché siamo connessi o sforiamo il limite di tempo
while (WiFi.status() != WL_CONNECTED) {
if ((millis() - inizioWifiMillis) > TIMEOUT_WIFI_MS) {
// Senza Wi-Fi non possiamo inviare i dati, quindi mettiamo la centralina
// in modalità risparmio energetico per poi riprovarci al prossimo ciclo.
hibernate();
}
// Nota: il timeout è necessario per via di un bug che potrebbe verificarsi
// in condizioni di bassa potenza del segnale Wi-Fi, in cui l'ESP32 smette
// di tentare la riconnessione e resta indefinitamente bloccata in questo
// `while` (anche qualora la rete dovesse tornare a essere disponibile).
//
// A prescindere dall'implementazione dell'ESP32, nel nostro caso prevedere
// un timeout è comunque positivo in termini di efficienza energetica (come
// misura preventiva verso eventuali indisponibilità prolungate del Wi-Fi).
}
// Potenza del segnale Wi-Fi (Received Signal Strength Indicator)
int8_t rssi = WiFi.RSSI();
// Tempo di connessione al Wi-Fi (in secondi)
float tempoWifi = (millis() - inizioWifiMillis) / 1000.0;
/*** INVIO DATI A THINGSPEAK ***/
// Creiamo la richiesta nel formato definito dall'API di ThingSpeak
String uri = "/update?api_key=" + String(WRITE_API_KEY);
// Inviamo i dati sulle polveri sottili solo se è riuscita almeno una lettura
if (lettureRiuscite > 0) {
uri += "&field1=" + String(pm10avg, 2)
+ "&field2=" + String(pm25avg, 2)
+ "&field3=" + String(pm1avg, 2);
}
// Invece questi dati di controllo li inviamo sempre e comunque
uri += "&field4=" + String(esitoLettureSensore, 2)
+ "&field5=" + String(rssi)
+ "&field6=" + String(tempoWifi, 3);
// Prepariamo i client per la richiesta GET via HTTPS
NetworkClientSecure client; // Client per la gestione del protocollo HTTPS
HTTPClient httpClient; // Client per la gestione della richiesta GET
// Impostiamo il certificato e il timeout per l'handshake SSL/TLS
client.setCACert(CA_CERT);
client.setHandshakeTimeout(TIMEOUT_HANDSHAKE_SSL_S);
// Impostiamo i timeout per la connessione del client e la risposta del server
// (v. https://github.com/espressif/arduino-esp32/issues/1433#issuecomment-475875579)
httpClient.setConnectTimeout(TIMEOUT_CONNESSIONE_CLIENT_MS);
httpClient.setTimeout(TIMEOUT_RISPOSTA_SERVER_MS);
// Nota: di default la connessione TCP viene lasciata aperta per il riuso, ma
// purtroppo non sembra sopravvivere al riavvio dell'ESP32 nemmeno se i client
// sono preservati nella memoria RTC. Il guadagno sarebbe comunque marginale.
httpClient.begin(client, "api.thingspeak.com", 443, uri, true);
uint8_t contatoreRichieste = 0;
uint32_t attesaBackoff = 0;
// Tentiamo l'invio ad intervalli calcolati usando un algoritmo di backoff
// esponenziale in base ai parametri `FATTORE_BACKOFF_MS` e `MAX_BACKOFF_MS`
// per un massimo numero di volte definito in `MAX_TENTATIVI_RICHIESTA`
while (httpClient.GET() != HTTP_CODE_OK) {
// Se abbiamo raggiunto il limite di tentativi, usciamo dal `while`
if (++contatoreRichieste >= MAX_TENTATIVI_RICHIESTA)
break;
// Altrimenti attendiamo fino alla prossima richiesta
attesaBackoff = FATTORE_BACKOFF_MS << (contatoreRichieste - 1);
attesaBackoff = min(attesaBackoff, MAX_BACKOFF_MS);
delay(attesaBackoff);
// Nota: oggettivamente, non era affatto necessario un meccanismo di backoff
// esponenziale: sarebbe bastato anche solo riprovare a intervalli fissi...
// ma ormai è stato già implementato, quindi tanto vale lasciarlo lì! :P
}
// Nota: questo ciclo è finito, evitiamo di deallocare i client per lo stesso
// motivo per cui non abbiamo deinizializzato la comunicazione con il sensore.
// Andiamo in modalità risparmio energetico fino al prossimo ciclo
hibernate();
}
/**
* Questa funzione è obbligatoria. Ma anche no ¯\_(ツ)_/¯
*/
void loop() {}
Variante risparmio energetico PMS5003
Torna all'indice 👆️
File sorgente: scienziaria-centralina-pms5003-sleep.ino
Codice da copiare:
/**
*
* Codice sorgente per configurare una centralina di monitoraggio della qualità
* dell'aria ispirata a quelle del progetto ScienziAria di Comunità Circolare.
*
* GUIDA PASSO PASSO per costruire la tua CENTRALINA FAI DA TE
* 👉️ https://comunitacircolare.it/articoli/scienziaria-guida-centralina
*
* La versione della guida è pensata espressamente per chi non ha particolari
* competenze tecniche ed è incentrata quindi sulla semplicità di realizzazione.
*
* Questa variante sfrutta la modalità risparmio energetico del sensore sia per
* consumare meno corrente che (soprattutto) per prolungarne la longevità media.
* A parte questo è identica alla versione base, con cui condivide la stragrande
* maggioranza del codice e dei commenti (anche per facilitarne il raffronto).
*
* Se non hai mai programmato, NIENTE PANICO! Ti basta inserire i tre parametri
* richiesti qui sotto all'interno delle virgolette, senza toccare nient'altro.
*
* Altrimenti, modifica pure il codice come ti pare (a tuo rischio e pericolo).
*
* \author Michele Nuzzolese
* \copyright Pubblico dominio con licenza Unlicense: https://unlicense.org
* \sa Progetto ScienziAria: https://comunitacircolare.it/progetti/scienziaria
*
*/
/* ## PARAMETRI RICHIESTI ## */
// 1. Nome della rete Wi-Fi, da ricopiare esattamente come appare nella lista di
// reti disponibili sul tuo smartphone o computer (includendo eventuali spazi o
// punteggiatura e rispettando la differenza tra lettere maiuscole e minuscole).
const char *NOME_RETE_WIFI = "scrivi qui il nome della tua rete Wi-Fi";
// 2. Password del Wi-Fi: occhio a maiuscole/minuscole/errori di battitura/etc.
const char *PASSWORD_WIFI = "qui dentro ci va la password";
// 3. `Write API Key` (la trovi nella tab `API Keys` del tuo canale ThingSpeak).
const char *WRITE_API_KEY = "0RM414VR41C4P1T0";
/* ## FINE CONFIGURAZIONI OBBLIGATORIE ##
*
* Congratulazioni! Non ti resta che caricare questo codice nella tua centralina
* con Arduino IDE (per istruzioni passo passo c'è sempre la guida in alto 👆️).
*
* ## INIZIO PROGRAMMA (E PARAMETRI OPZIONALI) ##
*
* Il codice sorgente ti fa paura? Scappa ora, prima che sia troppo tardi! 😜️
*
* Nel seguito troverai tutti i parametri opzionali legati alla logica interna
* della centralina. C'è qualche commento per facilitare la comprensione, ma non
* potevo dilungarmi troppo. Nel dubbio, puoi sempre usare i valori predefiniti.
*
* Se invece vuoi spiegazioni più dettagliate, ecco l'articolo che fa per te:
* https://comunitacircolare.it/articoli/scienziaria-approfondimenti-centraline
*
*/
#include <WiFi.h>
#include <HTTPClient.h>
#include <NetworkClientSecure.h>
#include <esp32-hal-cpu.h>
#include <esp_sleep.h>
// I valori indicati di seguito si basano sulle istruzioni della guida e devono
// rispecchiare l'effettivo collegamento tra il sensore e la scheda ESP32:
const uint8_t PIN_RX2 = 26; // Da collegare al pin TXD del sensore.
const uint8_t PIN_TX2 = 27; // Da collegare al pin RXD del sensore.
const uint8_t PIN_SET = 25; // Da collegare al pin SET del sensore.
// Frequenza con cui la centralina invia nuove rilevazioni (in minuti).
const uint8_t INTERVALLO_AGGIORNAMENTI_M = 4;
// Tempo di attesa per letture stabili dall'avvio della ventola (in secondi).
const uint32_t STABILIZZAZIONE_SENSORE_S = 30;
// Minimo intervallo di tempo tra due letture contigue (in millisecondi).
const uint32_t POLLING_SENSORE_MS = 1200;
// Numero massimo di rilevazioni del sensore su cui calcolare la media.
const uint8_t MAX_LETTURE_SENSORE = 6;
// Limite di tempo per tentare di connettersi al Wi-Fi (in millisecondi).
const uint32_t TIMEOUT_WIFI_MS = 10000;
// Certificato del server di ThingSpeak per la connessione sicura via HTTPS.
const char *CA_CERT =
"-----BEGIN CERTIFICATE-----\n"
"MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh\n"
"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n"
"d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\n"
"MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT\n"
"MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\n"
"b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG\n"
"9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI\n"
"2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx\n"
"1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ\n"
"q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz\n"
"tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ\n"
"vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP\n"
"BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV\n"
"5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY\n"
"1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4\n"
"NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG\n"
"Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91\n"
"8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe\n"
"pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl\n"
"MrY=\n"
"-----END CERTIFICATE-----";
// Limite di tempo per effettuare l'handshake SSL (in secondi).
const uint32_t TIMEOUT_HANDSHAKE_SSL_S = 5;
// Limite di tempo per stabilire la connessione con il server (in millisecondi).
const uint32_t TIMEOUT_CONNESSIONE_CLIENT_MS = 5000;
// Limite di tempo per ricevere la risposta del server (in millisecondi).
const uint32_t TIMEOUT_RISPOSTA_SERVER_MS = 5000;
// Numero massimo di tentativi di invio della richiesta GET.
const uint32_t MAX_TENTATIVI_RICHIESTA = 3;
// Fattore di backoff per nuovi tentativi di invio (in millisecondi).
const uint32_t FATTORE_BACKOFF_MS = 600;
// Limite massimo di attesa prima di un nuovo tentativo (in millisecondi).
const uint32_t MAX_BACKOFF_MS = 3000;
// Per semplicità, usiamo delle variabili globali per le polveri sottili:
uint16_t pm1 = 0; // Concentrazione di PM1 rilevata dal sensore.
uint16_t pm25 = 0; // Concentrazione di PM2,5 rilevata dal sensore.
uint16_t pm10 = 0; // Concentrazione di PM10 rilevata dal sensore.
/* ## FINE PARAMETRI OPZIONALI ##
*
* Da qui in poi ci sono le funzioni: "pezzi di codice" con uno scopo specifico,
* che possono essere richiamate all'occorrenza dal programma principale.
*
* A proposito, uno sketch inizia sempre con la funzione `setup()` (eseguita una
* sola volta) per poi passare alla funzione `loop()` (ripetuta ad libitum).
*
* Nel nostro caso il `loop()` non serve, perché l'ESP32 riparte dal `setup()`
* dopo che la centralina va in risparmio energetico (v. modalità `deep sleep`).
*
* ## INIZIO FUNZIONI HELPER ##
*
* Come suggerisce il nome, sono funzioni scritte per aiutare con determinati
* compiti (nella fattispecie, leggere il sensore e ibernare la centralina).
*
*/
/**
* Inizializza la comunicazione con il sensore e lo predispone alla lettura.
*
* Nota: abbiamo optato per un'implementazione meno efficiente ma più semplice e
* robusta, per prevenire potenziali rilevazioni fallite in casi limite (sulla
* base del funzionamento del sensore). Per approfondimenti, vedi l'articolo:
* https://comunitacircolare.it/articoli/scienziaria-approfondimenti-centraline
*
*/
void initPM() {
// Inizializziamo la comunicazione seriale
Serial2.begin(9600, SERIAL_8N1, PIN_RX2, PIN_TX2);
// Comando per mettere il sensore in `passive mode` (invio dati su richiesta)
uint8_t comandoPassiveMode[7] = { 0x42, 0x4D, 0xE1, 0x00, 0x00, 0x01, 0x70 };
// Inviamo il comando (a cui il sensore risponde con un messaggio di conferma)
Serial2.write(comandoPassiveMode, 7);
// Assicuriamoci che il sensore sia pronto per la lettura prima di proseguire
delay(942); // tempo di attesa = test empirici + ampio margine di sicurezza
}
/**
* Legge il sensore (presupponendo che la comunicazione sia già stata aperta) e
* memorizza le misurazioni di polveri sottili nelle apposite variabili globali.
*
* @return `true` se la lettura ha esito positivo, `false` altrimenti.
*/
bool readPM() {
// Scartiamo eventuali dati spuri che potrebbero interferire con la lettura
while (Serial2.available()) {
Serial2.read();
}
// Comando per richiedere una lettura quando il sensore è in `passive mode`
uint8_t comandoRead[7] = { 0x42, 0x4D, 0xE2, 0x00, 0x00, 0x01, 0x71 };
// Inviamo il comando (a cui il sensore risponde con i dati della lettura)
Serial2.write(comandoRead, 7);
// Aspettiamo che la trasmessione seriale del comando sia avvenuta per intero
Serial2.flush();
// Aspettiamo la risposta (dovrebbero bastare 34 ms, ma noi siamo prudenti :P)
delay(42);
// Assicuriamoci di aver ricevuto tutti e soli i 32 byte che ci aspettiamo in
// base al protocollo di trasporto (rif. scheda tecnica del sensore PMS5003)
if (Serial2.available() != 32) {
return false; // risposta assente o non conforme al protocollo di trasporto
}
uint8_t datiSensore[32] = { 0 };
uint16_t checksumCalcolato = 0;
// Prepariamo i dati da elaborare e i checksum per i controlli di integrità
for (uint8_t i = 0; i < 32; i++) {
datiSensore[i] = Serial2.read();
checksumCalcolato += datiSensore[i];
}
checksumCalcolato -= datiSensore[30] + datiSensore[31];
// Validiamo i dati ricevuti e controlliamo l'esito della trasmissione seriale
if (checksumCalcolato == 0 || checksumCalcolato == 0xAB) {
return false; // sensore non disponibile (tutti i dati uguali a 0)
}
uint16_t checksumRicevuto = (datiSensore[30] << 8) | datiSensore[31];
if (checksumRicevuto != checksumCalcolato) {
return false; // errore di trasmissione (checksum non corrispondenti)
}
// Elaboriamo le rilevazioni di PM e memorizziamole nelle rispettive variabili
pm1 = (datiSensore[10] << 8) | datiSensore[11];
pm25 = (datiSensore[12] << 8) | datiSensore[13];
pm10 = (datiSensore[14] << 8) | datiSensore[15];
return true; // lettura effettuata con successo
}
/**
* Mette la centralina in ibernazione per risparmiare energia quando non rileva.
*
* Nota: in questa variante, sia l'ESP32 che il sensore vanno in modalità sleep.
* Bisognerà quindi stabilizzare nuovamente il sensore al risveglio dell'ESP32,
* perché la ventola smette di girare quando l'attività del sensore è sospesa.
*/
void hibernate() {
// Disabilitiamo il Wi-Fi (v. documentazione "Sleep Modes" dell'ESP32)
WiFi.mode(WIFI_OFF);
// Mettiamo il sensore in modalità risparmio energetico
digitalWrite(PIN_SET, LOW);
// Blocchiamo il GPIO per assicurarci che lo stato del pin non cambi durante
// la modalità deep sleep (v. documentazione "GPIO & RTC GPIO" dell'ESP32)
gpio_hold_en((gpio_num_t)PIN_SET);
gpio_deep_sleep_hold_en();
// Disattiviamo esplicitamente i domini che non ci servono (attivandoli prima
// a causa di un bug, v. https://esp32.com/viewtopic.php?p=132473#p132473)
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_OFF);
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_ON);
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_OFF);
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_ON);
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_OFF);
esp_sleep_pd_config(ESP_PD_DOMAIN_XTAL, ESP_PD_OPTION_ON);
esp_sleep_pd_config(ESP_PD_DOMAIN_XTAL, ESP_PD_OPTION_OFF);
// Calcoliamo il tempo mancante fino al prossimo ciclo in millisecondi
uint32_t sleepMillis = 60000UL * INTERVALLO_AGGIORNAMENTI_M - millis();
esp_deep_sleep(1000ULL * sleepMillis); // e lo convertiamo in microsecondi
// Nota: in realtà l'ESP32 non tiene il tempo con gran precisione (a lungo
// andare tende a perdere qualche secondo), ma ai nostri fini va bene così.
}
/* ## FINE FUNZIONI HELPER (E INIZIO PROGRAMMA PRINCIPALE) ## */
/**
* Qui dentro c'è un intero ciclo di misurazioni e invio dei dati rilevati.
*
* Questa funzione infatti viene eseguita non solo appena la centralina riceve
* corrente, ma anche ogni volta che si risveglia dalla modalità ibernazione.
*/
void setup() {
/*** IMPOSTAZIONI PRELIMINARI ***/
// Assicuriamoci che la frequenza della CPU sia 80 MHz per risparmiare energia
if (getCpuFrequencyMhz() != 80) {
setCpuFrequencyMhz(80);
}
// Nota: se la imposti con Arduino IDE prima di caricare lo sketch (dal menu
// `Tools` o `Strumenti` -> `CPU Frequency`) potrai fare a meno sia delle tre
// righe di codice precedenti che della libreria `esp32-hal-cpu.h` in alto. E
// già che ci sei, puoi anche controllare che `Core Debug Level` sia impostato
// su `None` (tanto gli eventuali log non si potrebbero comunque leggere, dato
// che la centralina non sarà collegata al PC). Queste voci di menu però sono
// disponibili solo se hai già selezionato una scheda ESP32 compatibile.
// Se la centralina era in ibernazione (e quindi questo non è il primo avvio)
if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_TIMER) {
// Disabilitiamo il blocco del GPIO per riattivare il sensore
gpio_hold_dis((gpio_num_t)PIN_SET);
}
// Inizializziamo il pin per gestire la modalità sleep del sensore:
pinMode(PIN_SET, OUTPUT); // il livello di output predefinito è LOW
digitalWrite(PIN_SET, HIGH); // ma il sensore deve essere HIGH per monitorare
// Entriamo in light sleep mentre aspettiamo che il sensore si stabilizzi
esp_sleep_enable_timer_wakeup(1000000ULL * STABILIZZAZIONE_SENSORE_S);
esp_light_sleep_start();
// Nota: la ventola del sensore di particelle ha bisogno di tempo per creare
// un flusso d'aria costante (indispensabile per misurazioni affidabili). In
// questa variante stabilizziamo il sensore a ogni ciclo di esecuzione della
// centralina, poiché la ventola resta ferma durante la modalità ibernazione
// (v. anche il commento sulla funzione `hibernate()` poco sopra).
/*** LETTURA DEL SENSORE ***/
// Inizializziamo la comunicazione con il sensore
initPM();
// Totale di letture effettuate in questo ciclo di esecuzione della centralina
uint8_t lettureRiuscite = 0, lettureFallite = 0;
// Media delle misurazioni effettuate in questo ciclo
float pm10avg = 0, pm25avg = 0, pm1avg = 0;
// Leggiamo il sensore ad intervalli definiti in `POLLING_SENSORE_MS` per il
// numero di volte definito in `MAX_LETTURE_SENSORE` e ne calcoliamo la media
while (1) { // usiamo `break` per non aspettare a vuoto dopo l'ultima lettura
// Contiamo sia i tentativi di lettura falliti che quelli riusciti
if (!readPM()) {
lettureFallite++;
} else {
lettureRiuscite++;
// Calcoliamo la media cumulativa delle misurazioni effettuate
pm10avg = (pm10 + (lettureRiuscite - 1) * pm10avg) / lettureRiuscite;
pm25avg = (pm25 + (lettureRiuscite - 1) * pm25avg) / lettureRiuscite;
pm1avg = (pm1 + (lettureRiuscite - 1) * pm1avg) / lettureRiuscite;
}
// Se abbiamo raggiunto il limite di letture, usciamo dal `while`
if (lettureRiuscite + lettureFallite >= MAX_LETTURE_SENSORE)
break;
// Altrimenti attendiamo fino alla prossima rilevazione
delay(POLLING_SENSORE_MS);
}
// Nota: chiudere esplicitamente la comunicazione con il sensore è superfluo,
// perché tutta la memoria dell'ESP32 verrà comunque deallocata a fine ciclo.
// Percentuale di successo del sensore durante questo ciclo della centralina
float esitoLettureSensore = 100.0 * lettureRiuscite / (lettureRiuscite + lettureFallite);
/*** CONNESSIONE AL WI-FI ***/
uint32_t inizioWifiMillis = millis();
WiFi.begin(NOME_RETE_WIFI, PASSWORD_WIFI);
// Controlliamo il Wi-Fi finché siamo connessi o sforiamo il limite di tempo
while (WiFi.status() != WL_CONNECTED) {
if ((millis() - inizioWifiMillis) > TIMEOUT_WIFI_MS) {
// Senza Wi-Fi non possiamo inviare i dati, quindi mettiamo la centralina
// in modalità risparmio energetico per poi riprovarci al prossimo ciclo.
hibernate();
}
// Nota: il timeout è necessario per via di un bug che potrebbe verificarsi
// in condizioni di bassa potenza del segnale Wi-Fi, in cui l'ESP32 smette
// di tentare la riconnessione e resta indefinitamente bloccata in questo
// `while` (anche qualora la rete dovesse tornare a essere disponibile).
//
// A prescindere dall'implementazione dell'ESP32, nel nostro caso prevedere
// un timeout è comunque positivo in termini di efficienza energetica (come
// misura preventiva verso eventuali indisponibilità prolungate del Wi-Fi).
}
// Potenza del segnale Wi-Fi (Received Signal Strength Indicator)
int8_t rssi = WiFi.RSSI();
// Tempo di connessione al Wi-Fi (in secondi)
float tempoWifi = (millis() - inizioWifiMillis) / 1000.0;
/*** INVIO DATI A THINGSPEAK ***/
// Creiamo la richiesta nel formato definito dall'API di ThingSpeak
String uri = "/update?api_key=" + String(WRITE_API_KEY);
// Inviamo i dati sulle polveri sottili solo se è riuscita almeno una lettura
if (lettureRiuscite > 0) {
uri += "&field1=" + String(pm10avg, 2)
+ "&field2=" + String(pm25avg, 2)
+ "&field3=" + String(pm1avg, 2);
}
// Invece questi dati di controllo li inviamo sempre e comunque
uri += "&field4=" + String(esitoLettureSensore, 2)
+ "&field5=" + String(rssi)
+ "&field6=" + String(tempoWifi, 3);
// Prepariamo i client per la richiesta GET via HTTPS
NetworkClientSecure client; // Client per la gestione del protocollo HTTPS
HTTPClient httpClient; // Client per la gestione della richiesta GET
// Impostiamo il certificato e il timeout per l'handshake SSL/TLS
client.setCACert(CA_CERT);
client.setHandshakeTimeout(TIMEOUT_HANDSHAKE_SSL_S);
// Impostiamo i timeout per la connessione del client e la risposta del server
// (v. https://github.com/espressif/arduino-esp32/issues/1433#issuecomment-475875579)
httpClient.setConnectTimeout(TIMEOUT_CONNESSIONE_CLIENT_MS);
httpClient.setTimeout(TIMEOUT_RISPOSTA_SERVER_MS);
// Nota: di default la connessione TCP viene lasciata aperta per il riuso, ma
// purtroppo non sembra sopravvivere al riavvio dell'ESP32 nemmeno se i client
// sono preservati nella memoria RTC. Il guadagno sarebbe comunque marginale.
httpClient.begin(client, "api.thingspeak.com", 443, uri, true);
uint8_t contatoreRichieste = 0;
uint32_t attesaBackoff = 0;
// Tentiamo l'invio ad intervalli calcolati usando un algoritmo di backoff
// esponenziale in base ai parametri `FATTORE_BACKOFF_MS` e `MAX_BACKOFF_MS`
// per un massimo numero di volte definito in `MAX_TENTATIVI_RICHIESTA`
while (httpClient.GET() != HTTP_CODE_OK) {
// Se abbiamo raggiunto il limite di tentativi, usciamo dal `while`
if (++contatoreRichieste >= MAX_TENTATIVI_RICHIESTA)
break;
// Altrimenti attendiamo fino alla prossima richiesta
attesaBackoff = FATTORE_BACKOFF_MS << (contatoreRichieste - 1);
attesaBackoff = min(attesaBackoff, MAX_BACKOFF_MS);
delay(attesaBackoff);
// Nota: oggettivamente, non era affatto necessario un meccanismo di backoff
// esponenziale: sarebbe bastato anche solo riprovare a intervalli fissi...
// ma ormai è stato già implementato, quindi tanto vale lasciarlo lì! :P
}
// Nota: questo ciclo è finito, evitiamo di deallocare i client per lo stesso
// motivo per cui non abbiamo deinizializzato la comunicazione con il sensore.
// Andiamo in modalità risparmio energetico fino al prossimo ciclo
hibernate();
}
/**
* Questa funzione è obbligatoria. Ma anche no ¯\_(ツ)_/¯
*/
void loop() {}
Variante monitoraggio continuo KS0578
Torna all'indice 👆️
File sorgente: scienziaria-monitoraggio-continuo-ks0578.ino
Codice da copiare:
/**
*
* Centralina mobile (ESP32 + KS0578 + LED RGB + DHT22)
*
* GUIDA PASSO PASSO per costruire la tua CENTRALINA FAI DA TE
* 👉️ https://comunitacircolare.it/articoli/scienziaria-guida-centralina
*
* Questa variante permette un monitoraggio continuo ed è ispirata al codice
* della centralina mobile del progetto ScienziAria di Comunità Circolare.
*
* Oltre alle polveri sottili, rileva anche temperatura e umidità relativa. Può
* essere alimentata con un power bank per smartphone e connettersi via hotspot
* Wi-Fi per aggiornare periodicamente i grafici sul canale ThingSpeak.
*
* Facendo riferimento alle schede tecniche dei componenti, abbiamo stimato un
* consumo di corrente medio non superiore ai 160 mA (che può comunque variare
* a seconda delle effettive modalità di realizzazione e uso della centralina).
*
* In base alle impostazioni predefinite, la concentrazione di polveri sottili
* viene misurata ogni secondo e segnalata da una spia luminosa. La centralina
* carica dati online ogni 16 secondi circa, a meno di problemi di connettività.
* Se connessa al Wi-Fi, la luce è fissa; altrimenti, l'indicatore lampeggia.
*
* COLORE LED STATO DELLA CENTRALINA
* Azzurro ... Stabilizzazione sensore PM
* Verde ..... Concentrazione PM10 bassa
* Blu ....... Concentrazione PM10 media
* Rosso ..... Concentrazione PM10 alta
* Bianco .... Misurazione non riuscita
* Magenta ... Aggiornamento ThingSpeak
*
* CAMPI DEL CANALE THINGSPEAK
* 1. PM10: media mobile esponenziale di rilevazioni dall'ultimo aggiornamento.
* 2. PM2,5: come sopra.
* 3. PM1: come sopra.
* 4. Potenza segnale Wi-Fi: valore RSSI (in dBm) al momento del caricamento.
* 5. Stato sensore KS0578: % di letture riuscite dall'ultimo aggiornamento.
* 6. Stato sensore DHT22: 1 se la lettura è riuscita, 0 altrimenti.
* 7. Temperatura: in °Celsius, rilevata al momento del caricamento.
* 8. Umidità relativa: in %, rilevata al momento del caricamento.
*
* Per ulteriori dettagli implementativi, il codice è proprio qui sotto! :)
*
* \author Michele Nuzzolese
* \copyright Pubblico dominio con licenza Unlicense: https://unlicense.org
* \sa Progetto ScienziAria: https://comunitacircolare.it/progetti/scienziaria
*
*/
#include <DHT.h>
#include <Wire.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <NetworkClientSecure.h>
#include <esp32-hal-cpu.h>
// I parametri obbligatori sono gli stessi della versione base della centralina,
// mentre i parametri opzionali sono inclusi in tutte le altre costanti globali.
// Come rete Wi-Fi può andar bene anche un hotspot del cellulare.
const char *NOME_RETE_WIFI = "hotspot";
// Password del Wi-Fi (o dell'hotspot).
const char *PASSWORD_WIFI = "scienziaria";
// Chiave API del canale ThingSpeak.
const char *WRITE_API_KEY = "4M43F4C10CH3VU01";
// I valori indicati di seguito si basano sulle istruzioni della guida e devono
// rispecchiare l'effettivo collegamento tra i componenti e la scheda ESP32:
const uint8_t PIN_SDA = 27; // Terzo pin del KS0578 (da sinistra a destra).
const uint8_t PIN_SCL = 26; // Quarto pin del KS0578 (da sinistra a destra).
const uint8_t PIN_DHT = 25; // Secondo pin del DHT22 (pin DATA).
const uint8_t PIN_LED_R = 12; // Pin rosso del LED RGB.
const uint8_t PIN_LED_G = 13; // Pin verde del LED RGB.
const uint8_t PIN_LED_B = 14; // Pin blu del LED RGB.
// Frequenza con cui inviare nuove rilevazioni (in millisecondi), considerando
// le limitazioni di ThingSpeak (v. https://thingspeak.com/pages/license_faq).
const uint32_t INTERVALLO_AGGIORNAMENTI_MS = 15423;
// Tempo di attesa per letture stabili dall'avvio della centralina (in secondi).
const uint8_t STABILIZZAZIONE_SENSORE_PM_S = 30;
// Frequenza con cui leggere il sensore di polveri sottili (in millisecondi).
const uint32_t INTERVALLO_MONITORAGGIO_PM_MS = 1000;
// Frequenza con cui far lampeggiare il LED (in millisecondi).
const uint32_t INTERVALLO_INTERMITTENZA_LED_MS = 500;
// Soglia oltre la quale segnalare un livello PM alto.
const uint8_t SOGLIA_PM10_ALTA = 50;
// Soglia sotto la quale segnalare un livello PM basso.
const uint8_t SOGLIA_PM10_BASSA = 25;
// Tipo di dati personalizzato per gestire i colori dell'indicatore LED.
typedef enum {
OFF,
RED,
GREEN,
BLUE,
YELLOW,
MAGENTA,
CYAN,
WHITE
} led_color_t;
// Colore del LED in base allo stato della centralina.
const led_color_t COLORE_STABILIZZAZIONE_SENSORE = CYAN,
COLORE_PM10_BASSO = GREEN,
COLORE_PM10_MEDIO = BLUE,
COLORE_PM10_ALTO = RED,
COLORE_LETTURA_FALLITA = WHITE,
COLORE_AGGIORNAMENTO_THINGSPEAK = MAGENTA;
// Certificato del server di ThingSpeak per la connessione sicura via HTTPS.
const char *CA_CERT =
"-----BEGIN CERTIFICATE-----\n"
"MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh\n"
"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n"
"d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\n"
"MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT\n"
"MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\n"
"b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG\n"
"9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI\n"
"2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx\n"
"1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ\n"
"q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz\n"
"tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ\n"
"vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP\n"
"BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV\n"
"5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY\n"
"1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4\n"
"NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG\n"
"Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91\n"
"8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe\n"
"pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl\n"
"MrY=\n"
"-----END CERTIFICATE-----";
// Limite di tempo per effettuare l'handshake SSL (in secondi).
const uint8_t TIMEOUT_HANDSHAKE_SSL_S = 4;
// Limite di tempo per stabilire la connessione con il server (in millisecondi).
const uint32_t TIMEOUT_CONNESSIONE_CLIENT_MS = 3000;
// Limite di tempo per ricevere la risposta del server (in millisecondi).
const uint32_t TIMEOUT_RISPOSTA_SERVER_MS = 3000;
// Tempo di attesa prima di ritentare dopo un aggiornamento fallito (in ms).
const uint32_t ATTESA_NUOVO_TENTATIVO_MS = 2342;
// In questa variante servono più variabili globali rispetto alla versione base,
// per gestire lo stato della centralina e l'esecuzione dei vari task (compiti).
uint16_t pm1 = 0; // Concentrazione di PM1 rilevata dal sensore KS0578.
uint16_t pm25 = 0; // Concentrazione di PM2,5 rilevata dal sensore KS0578.
uint16_t pm10 = 0; // Concentrazione di PM10 rilevata dal sensore KS0578.
float temperatura = NAN; // Temperatura rilevata dal sensore DHT22.
float umidita = NAN; // Umidità relativa rilevata dal sensore DHT22.
DHT dht(PIN_DHT, DHT22); // Sensore DHT22 (vedi libreria `DHT.h` di Adafruit).
NetworkClientSecure client; // Client per la connessione sicura HTTPS.
HTTPClient httpClient; // Client per l'invio della richiesta GET.
// Media mobile esponenziale della concentrazione PM dall'ultimo aggiornamento.
float pm10media = 0, pm25media = 0, pm1media = 0;
// Totale di letture effettuate del sensore PM dall'ultimo aggiornamento.
uint8_t lettureRiuscite = 0, lettureFallite = 0;
bool ledAcceso = false; // Stato attuale del LED.
led_color_t coloreLed = OFF; // Colore attuale del LED.
// Timer per il task di monitoraggio delle polveri sottili.
uint32_t millisMonitoraggio = 0;
// Timer per il task di lampeggiamento del LED.
uint32_t millisLed = 0;
// Timer per il task di aggiornamento del canale ThingSpeak.
uint32_t millisAggiornamento = 1000L * STABILIZZAZIONE_SENSORE_PM_S;
// Intervallo del prossimo task di aggiornamento del canale ThingSpeak.
uint32_t intervalloAggiornamento = INTERVALLO_AGGIORNAMENTI_MS;
// Nota: come alternativa a tutte queste costanti e variabili globali potremmo
// incapsulare la centralina in un'apposita classe (separata dalla logica della
// gestione dei task, che non riguardano l'implementazione specifica ma solo il
// comportamento definito da un'interfaccia pubblica). Sebbene questo approccio
// basato sul principio di occultamento delle informazioni (information hiding)
// sia più teoricamente corretto e ingegneristicamente robusto, introduce anche
// complessità progettuali sproporzionate per questa specifica applicazione (ma
// lo consigliamo come esercizio sulle tecniche della programmazione a oggetti).
/**
* Legge il KS0578 (presupponendo che la comunicazione sia già stata aperta) e
* memorizza le misurazioni di polveri sottili nelle apposite variabili globali.
*
* Nota: stessa funzione della versione base della centralina (con il KS0578).
*
* @return `true` se la lettura ha esito positivo, `false` altrimenti.
*/
bool readPM() {
// Assicuriamoci che il buffer I2C sia vuoto prima di ricevere nuovi dati
while (Wire.available()) {
Wire.read();
}
// Leggiamo 32 byte dal bus I2C in base al protocollo di trasporto del sensore
if (Wire.requestFrom(0x2a, 32) != 32) {
return false; // risposta non conforme al protocollo di trasporto
}
uint8_t datiSensore[32] = { 0 };
uint16_t checksumCalcolato = 0;
// Prepariamo i dati da elaborare e i checksum per i controlli di integrità
for (uint8_t i = 0; i < 32; i++) {
datiSensore[i] = Wire.read();
checksumCalcolato += datiSensore[i];
}
checksumCalcolato -= datiSensore[30] + datiSensore[31];
// Validiamo i dati ricevuti e controlliamo l'esito della trasmissione seriale
if (checksumCalcolato == 0) {
return false; // sensore non ancora disponibile (tutti i dati uguali a 0)
}
uint16_t checksumRicevuto = (datiSensore[30] << 8) | datiSensore[31];
if (checksumRicevuto != checksumCalcolato) {
return false; // errore di trasmissione (checksum non corrispondenti)
}
// Elaboriamo le rilevazioni di PM e memorizziamole nelle rispettive variabili
pm1 = (datiSensore[10] << 8) | datiSensore[11];
pm25 = (datiSensore[12] << 8) | datiSensore[13];
pm10 = (datiSensore[14] << 8) | datiSensore[15];
return true; // lettura effettuata con successo
}
/**
* Legge il DHT22 (presupponendo che la comunicazione sia già stata aperta) e
* memorizza le misurazioni di temperatura e umidità nelle apposite variabili.
*
* @return `true` se la lettura ha esito positivo, `false` altrimenti.
*/
bool readDHT() {
temperatura = dht.readTemperature();
umidita = dht.readHumidity();
if (isnan(umidita) || isnan(temperatura)) {
return false; // lettura fallita
}
return true; // lettura effettuata con successo
}
/**
* Imposta la spia della centralina in base al colore passato come parametro.
*
* @param color Il colore da impostare (oppure `OFF` per spegnere il LED).
*/
void setLed(led_color_t color) {
if (ledAcceso && color == coloreLed) {
return;
}
if (color == OFF) {
ledAcceso = false;
digitalWrite(PIN_LED_R, LOW);
digitalWrite(PIN_LED_G, LOW);
digitalWrite(PIN_LED_B, LOW);
return;
}
coloreLed = color;
ledAcceso = true;
switch (color) {
case GREEN:
digitalWrite(PIN_LED_R, LOW);
digitalWrite(PIN_LED_G, HIGH);
digitalWrite(PIN_LED_B, LOW);
break;
case BLUE:
digitalWrite(PIN_LED_R, LOW);
digitalWrite(PIN_LED_G, LOW);
digitalWrite(PIN_LED_B, HIGH);
break;
case RED:
digitalWrite(PIN_LED_R, HIGH);
digitalWrite(PIN_LED_G, LOW);
digitalWrite(PIN_LED_B, LOW);
break;
case YELLOW:
digitalWrite(PIN_LED_R, HIGH);
digitalWrite(PIN_LED_G, HIGH);
digitalWrite(PIN_LED_B, LOW);
break;
case MAGENTA:
digitalWrite(PIN_LED_R, HIGH);
digitalWrite(PIN_LED_G, LOW);
digitalWrite(PIN_LED_B, HIGH);
break;
case CYAN:
digitalWrite(PIN_LED_R, LOW);
digitalWrite(PIN_LED_G, HIGH);
digitalWrite(PIN_LED_B, HIGH);
break;
case WHITE:
digitalWrite(PIN_LED_R, HIGH);
digitalWrite(PIN_LED_G, HIGH);
digitalWrite(PIN_LED_B, HIGH);
break;
default:
// abbiamo già coperto esaustivamente tutte le casistiche possibili
break;
}
}
/**
* Prepara la centralina alla fase di monitoraggio continuo.
*/
void setup() {
// Impostiamo la frequenza della CPU a 80 MHz per consumare meno corrente
setCpuFrequencyMhz(80);
// Inizializziamo i pin del LED RGB
pinMode(PIN_LED_R, OUTPUT);
pinMode(PIN_LED_G, OUTPUT);
pinMode(PIN_LED_B, OUTPUT);
// Accendiamo il LED per indicare che il sensore è in fase di stabilizzazione
setLed(COLORE_STABILIZZAZIONE_SENSORE);
// Inizializziamo il Wi-Fi
WiFi.begin(NOME_RETE_WIFI, PASSWORD_WIFI);
// Nota: l'ESP32 implementa l'API WiFi di Arduino impostando la riconnessione
// automatica di default, ma dai nostri test è risultato più efficace gestirla
// con un evento (strategia consigliata anche nella documentazione dell'ESP32,
// v. https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html#wi-fi-reconnect).
// Disabilitiamo la riconnessione automatica
WiFi.setAutoReconnect(false);
// Registriamo la funzione da richiamare in caso di disconnessione dal Wi-Fi
WiFi.onEvent(
[](WiFiEvent_t e, WiFiEventInfo_t i) { // gestore evento (funzione anonima)
WiFi.reconnect();
},
WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED); // evento disconnessione
// Inizializziamo la communicazione con i sensori
Wire.begin(PIN_SDA, PIN_SCL);
dht.begin();
// Impostiamo il certificato del client e il timeout per l'handshake SSL/TLS
client.setCACert(CA_CERT);
client.setHandshakeTimeout(TIMEOUT_HANDSHAKE_SSL_S);
// Impostiamo i timeout per la connessione al client e la risposta del server
// (v. https://github.com/espressif/arduino-esp32/issues/1433#issuecomment-475875579)
httpClient.setConnectTimeout(TIMEOUT_CONNESSIONE_CLIENT_MS);
httpClient.setTimeout(TIMEOUT_RISPOSTA_SERVER_MS);
// Aspettiamo che il sensore di polveri sottili si sia stabilizzato
while (1000L * STABILIZZAZIONE_SENSORE_PM_S > millis()) {
// Nel frattempo, facciamo lampeggiare il LED se il Wi-Fi è disconnesso
if (WiFi.status() != WL_CONNECTED) {
delay(INTERVALLO_INTERMITTENZA_LED_MS);
setLed(OFF);
delay(INTERVALLO_INTERMITTENZA_LED_MS);
setLed(COLORE_STABILIZZAZIONE_SENSORE);
// Nota: bloccare l'esecuzione per tempi ragionevolmente brevi come quello
// di un LED che lampeggia è del tutto innocuo in questo contesto (e rende
// anche più immediata l'implementazione, eliminando il caso limite in cui
// il Wi-Fi dovesse riconnettersi appena prima che il LED venga riacceso).
}
}
}
/**
* Monitora il PM e il LED, controlla il Wi-Fi e aggiorna il canale ThingSpeak.
*/
void loop() {
// Controlliamo se è il momento di eseguire il task di monitoraggio
if (millis() - millisMonitoraggio >= INTERVALLO_MONITORAGGIO_PM_MS) {
millisMonitoraggio = millis();
if (!readPM()) {
lettureFallite++;
setLed(COLORE_LETTURA_FALLITA);
} else {
lettureRiuscite++;
// Qui c'è un operatore ternario innestato in un altro operatore ternario
// (ma è meno complesso di quanto possa sembrare, vedi i commenti a lato)
setLed(pm10 < SOGLIA_PM10_BASSA // se questa condizione è vera
? COLORE_PM10_BASSO // allora restituisce questo valore
: (pm10 < SOGLIA_PM10_ALTA // altrimenti: se questa è vera
? COLORE_PM10_MEDIO // allora restituisce questo valore
: COLORE_PM10_ALTO)); // altrimenti: restituisce questo qui
if (lettureRiuscite == 1) { // passo iniziale per il calcolo della media
pm10media = pm10;
pm25media = pm25;
pm1media = pm1;
} else {
const float ALFA = 0.69; // parametro per la media mobile esponenziale
pm10media += ALFA * (pm10 - pm10media);
pm25media += ALFA * (pm25 - pm25media);
pm1media += ALFA * (pm1 - pm1media);
}
} // fine controllo lettura
} // fine task monitoraggio
// Segnaliamo che il WiFi è disconnesso facendo lampeggiare il LED
if (WiFi.status() != WL_CONNECTED) {
if (millis() - millisLed >= INTERVALLO_INTERMITTENZA_LED_MS) {
millisLed = millis();
setLed(ledAcceso ? OFF : coloreLed);
}
// Nota: bloccare l'esecuzione interferirebbe con il task di monitoraggio,
// quindi abbiamo usato un timer per il LED e gestito l'evento per eseguire
// la funzione di riconnessione al Wi-Fi in parallelo, in un thread a parte.
} else {
// Se il WiFi si riconnette mentre il LED è ancora spento, riaccendiamolo
if (!ledAcceso) {
setLed(coloreLed);
}
// Nota: questo controllo è ridondante perché svolto anche all'interno della
// funzione, ma lo sappiamo solo perché l'abbiamo scritta noi. Se avessimo a
// che fare con un generico metodo setter, potremmo non essere a conoscenza
// di simili dettagli implementativi (e un controllo aggiuntivo contribuisce
// comunque alla chiarezza del codice, a un costo computazionale irrisorio).
// Controlliamo se è il momento di eseguire il task di aggiornamento
if (millis() - millisAggiornamento >= intervalloAggiornamento) {
// Elaboriamo i dati di controllo da inviare al canale ThingSpeak
int8_t rssi = WiFi.RSSI();
float esitoLetturePM = 100.0 * lettureRiuscite / (lettureRiuscite + lettureFallite);
bool esitoLetturaDHT = readDHT();
// Creiamo la richiesta nel formato definito dall'API di ThingSpeak
String uri = "/update?api_key=" + String(WRITE_API_KEY);
// Inviamo i dati sulle polveri sottili solo se disponibili
if (lettureRiuscite > 0) {
uri += "&field1=" + String(pm10media, 2)
+ "&field2=" + String(pm25media, 2)
+ "&field3=" + String(pm1media, 2);
}
// Inviamo sempre e comunque i dati di controllo
uri += "&field4=" + String(rssi)
+ "&field5=" + String(esitoLetturePM, 2)
+ "&field6=" + String((int)esitoLetturaDHT);
// Inviamo i dati su temperatura e umidità solo se disponibili
if (esitoLetturaDHT) {
uri += "&field7=" + String(temperatura, 2)
+ "&field8=" + String(umidita, 2);
}
// Predisponiamo l'invio della richiesta GET e aspettiamo la risposta
httpClient.begin(client, "api.thingspeak.com", 443, uri, true);
led_color_t colorePrecedente = coloreLed;
setLed(COLORE_AGGIORNAMENTO_THINGSPEAK);
int16_t codiceRisposta = httpClient.GET(); // funzione bloccante
// Nota: in buone condizioni di connettività ci vuole solitamente meno di
// mezzo secondo, mentre nei casi peggiori la richiesta andrà in timeout.
// Il primo invio richiede fino a 2 secondi per stabilire una connessione
// criptata. Il protocollo HTTP è più veloce ma insicuro (ed è meglio non
// trasmettere in chiaro informazioni sensibili come la chiave API).
// Reimpostiamo il timer del task di aggiornamento e il colore del LED
millisAggiornamento = millis();
setLed(colorePrecedente);
// Aggiorniamo l'intervallo del task (in base all'esito della richiesta)
if (codiceRisposta != HTTP_CODE_OK) {
intervalloAggiornamento = ATTESA_NUOVO_TENTATIVO_MS;
} else {
intervalloAggiornamento = INTERVALLO_AGGIORNAMENTI_MS;
// Resettiamo i contatori delle letture fino al prossimo aggiornamento
lettureRiuscite = lettureFallite = 0;
// (la media mobile delle rilevazioni invece si aggiorna in automatico)
}
// Terminiamo la richiesta appena effettuata (a prescindere dal suo esito)
httpClient.end();
} // fine task aggiornamento
} // fine controllo wifi
} // fine loop
Variante monitoraggio continuo PMS5003
Torna all'indice 👆️
File sorgente: scienziaria-monitoraggio-continuo-pms5003.ino
Codice da copiare:
/**
*
* Centralina mobile (ESP32 + PMS5003 + LED RGB + DHT22)
*
* GUIDA PASSO PASSO per costruire la tua CENTRALINA FAI DA TE
* 👉️ https://comunitacircolare.it/articoli/scienziaria-guida-centralina
*
* Questa variante permette un monitoraggio continuo ed è ispirata al codice
* della centralina mobile del progetto ScienziAria di Comunità Circolare.
*
* Oltre alle polveri sottili, rileva anche temperatura e umidità relativa. Può
* essere alimentata con un power bank per smartphone e connettersi via hotspot
* Wi-Fi per aggiornare periodicamente i grafici sul canale ThingSpeak.
*
* Facendo riferimento alle schede tecniche dei componenti, abbiamo stimato un
* consumo di corrente medio non superiore ai 160 mA (che può comunque variare
* a seconda delle effettive modalità di realizzazione e uso della centralina).
*
* In base alle impostazioni predefinite, la concentrazione di polveri sottili
* viene misurata ogni secondo e segnalata da una spia luminosa. La centralina
* carica dati online ogni 16 secondi circa, a meno di problemi di connettività.
* Se connessa al Wi-Fi, la luce è fissa; altrimenti, l'indicatore lampeggia.
*
* COLORE LED STATO DELLA CENTRALINA
* Azzurro ... Stabilizzazione sensore PM
* Verde ..... Concentrazione PM10 bassa
* Blu ....... Concentrazione PM10 media
* Rosso ..... Concentrazione PM10 alta
* Bianco .... Misurazione non riuscita
* Magenta ... Aggiornamento ThingSpeak
*
* CAMPI DEL CANALE THINGSPEAK
* 1. PM10: media mobile esponenziale di rilevazioni dall'ultimo aggiornamento.
* 2. PM2,5: come sopra.
* 3. PM1: come sopra.
* 4. Potenza segnale Wi-Fi: valore RSSI (in dBm) al momento del caricamento.
* 5. Stato sensore PMS5003: % di letture riuscite dall'ultimo aggiornamento.
* 6. Stato sensore DHT22: 1 se la lettura è riuscita, 0 altrimenti.
* 7. Temperatura: in °Celsius, rilevata al momento del caricamento.
* 8. Umidità relativa: in %, rilevata al momento del caricamento.
*
* Per ulteriori dettagli implementativi, il codice è proprio qui sotto! :)
*
* \author Michele Nuzzolese
* \copyright Pubblico dominio con licenza Unlicense: https://unlicense.org
* \sa Progetto ScienziAria: https://comunitacircolare.it/progetti/scienziaria
*
*/
#include <DHT.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <NetworkClientSecure.h>
#include <esp32-hal-cpu.h>
// I parametri obbligatori sono gli stessi della versione base della centralina,
// mentre i parametri opzionali sono inclusi in tutte le altre costanti globali.
// Come rete Wi-Fi può andar bene anche un hotspot del cellulare.
const char *NOME_RETE_WIFI = "hotspot";
// Password del Wi-Fi (o dell'hotspot).
const char *PASSWORD_WIFI = "scienziaria";
// Chiave API del canale ThingSpeak.
const char *WRITE_API_KEY = "4M43F4C10CH3VU01";
// I valori indicati di seguito si basano sulle istruzioni della guida e devono
// rispecchiare l'effettivo collegamento tra i componenti e la scheda ESP32:
const uint8_t PIN_RX2 = 26; // Da collegare al pin TXD del sensore.
const uint8_t PIN_TX2 = 27; // Da collegare al pin RXD del sensore.
const uint8_t PIN_DHT = 25; // Secondo pin del DHT22 (pin DATA).
const uint8_t PIN_LED_R = 12; // Pin rosso del LED RGB.
const uint8_t PIN_LED_G = 13; // Pin verde del LED RGB.
const uint8_t PIN_LED_B = 14; // Pin blu del LED RGB.
// Frequenza con cui inviare nuove rilevazioni (in millisecondi), considerando
// le limitazioni di ThingSpeak (v. https://thingspeak.com/pages/license_faq).
const uint32_t INTERVALLO_AGGIORNAMENTI_MS = 15423;
// Tempo di attesa per letture stabili dall'avvio della centralina (in secondi).
const uint8_t STABILIZZAZIONE_SENSORE_PM_S = 30;
// Frequenza con cui leggere il sensore di polveri sottili (in millisecondi).
const uint32_t INTERVALLO_MONITORAGGIO_PM_MS = 1000;
// Frequenza con cui far lampeggiare il LED (in millisecondi).
const uint32_t INTERVALLO_INTERMITTENZA_LED_MS = 500;
// Soglia oltre la quale segnalare un livello PM alto.
const uint8_t SOGLIA_PM10_ALTA = 50;
// Soglia sotto la quale segnalare un livello PM basso.
const uint8_t SOGLIA_PM10_BASSA = 25;
// Tipo di dati personalizzato per gestire i colori dell'indicatore LED.
typedef enum {
OFF,
RED,
GREEN,
BLUE,
YELLOW,
MAGENTA,
CYAN,
WHITE
} led_color_t;
// Colore del LED in base allo stato della centralina.
const led_color_t COLORE_STABILIZZAZIONE_SENSORE = CYAN,
COLORE_PM10_BASSO = GREEN,
COLORE_PM10_MEDIO = BLUE,
COLORE_PM10_ALTO = RED,
COLORE_LETTURA_FALLITA = WHITE,
COLORE_AGGIORNAMENTO_THINGSPEAK = MAGENTA;
// Certificato del server di ThingSpeak per la connessione sicura via HTTPS.
const char *CA_CERT =
"-----BEGIN CERTIFICATE-----\n"
"MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh\n"
"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n"
"d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\n"
"MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT\n"
"MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\n"
"b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG\n"
"9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI\n"
"2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx\n"
"1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ\n"
"q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz\n"
"tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ\n"
"vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP\n"
"BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV\n"
"5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY\n"
"1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4\n"
"NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG\n"
"Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91\n"
"8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe\n"
"pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl\n"
"MrY=\n"
"-----END CERTIFICATE-----";
// Limite di tempo per effettuare l'handshake SSL (in secondi).
const uint8_t TIMEOUT_HANDSHAKE_SSL_S = 4;
// Limite di tempo per stabilire la connessione con il server (in millisecondi).
const uint32_t TIMEOUT_CONNESSIONE_CLIENT_MS = 3000;
// Limite di tempo per ricevere la risposta del server (in millisecondi).
const uint32_t TIMEOUT_RISPOSTA_SERVER_MS = 3000;
// Tempo di attesa prima di ritentare dopo un aggiornamento fallito (in ms).
const uint32_t ATTESA_NUOVO_TENTATIVO_MS = 2342;
// In questa variante servono più variabili globali rispetto alla versione base,
// per gestire lo stato della centralina e l'esecuzione dei vari task (compiti).
uint16_t pm1 = 0; // Concentrazione di PM1 rilevata dal sensore PMS5003.
uint16_t pm25 = 0; // Concentrazione di PM2,5 rilevata dal sensore PMS5003.
uint16_t pm10 = 0; // Concentrazione di PM10 rilevata dal sensore PMS5003.
float temperatura = NAN; // Temperatura rilevata dal sensore DHT22.
float umidita = NAN; // Umidità relativa rilevata dal sensore DHT22.
DHT dht(PIN_DHT, DHT22); // Sensore DHT22 (vedi libreria `DHT.h` di Adafruit).
NetworkClientSecure client; // Client per la connessione sicura HTTPS.
HTTPClient httpClient; // Client per l'invio della richiesta GET.
// Media mobile esponenziale della concentrazione PM dall'ultimo aggiornamento.
float pm10media = 0, pm25media = 0, pm1media = 0;
// Totale di letture effettuate del sensore PM dall'ultimo aggiornamento.
uint8_t lettureRiuscite = 0, lettureFallite = 0;
bool ledAcceso = false; // Stato attuale del LED.
led_color_t coloreLed = OFF; // Colore attuale del LED.
// Timer per il task di monitoraggio delle polveri sottili.
uint32_t millisMonitoraggio = 0;
// Timer per il task di lampeggiamento del LED.
uint32_t millisLed = 0;
// Timer per il task di aggiornamento del canale ThingSpeak.
uint32_t millisAggiornamento = 1000L * STABILIZZAZIONE_SENSORE_PM_S;
// Intervallo del prossimo task di aggiornamento del canale ThingSpeak.
uint32_t intervalloAggiornamento = INTERVALLO_AGGIORNAMENTI_MS;
// Nota: come alternativa a tutte queste costanti e variabili globali potremmo
// incapsulare la centralina in un'apposita classe (separata dalla logica della
// gestione dei task, che non riguardano l'implementazione specifica ma solo il
// comportamento definito da un'interfaccia pubblica). Sebbene questo approccio
// basato sul principio di occultamento delle informazioni (information hiding)
// sia più teoricamente corretto e ingegneristicamente robusto, introduce anche
// complessità progettuali sproporzionate per questa specifica applicazione (ma
// lo consigliamo come esercizio sulle tecniche della programmazione a oggetti).
/**
* Invia al sensore il comando per entrare in modalità passiva.
*
* @return `true` se la risposta del sensore è positiva, `false` altrimenti.
*/
bool beginPM() {
// Comando per mettere il sensore in `passive mode` (invio dati su richiesta)
uint8_t comandoPassiveMode[7] = { 0x42, 0x4D, 0xE1, 0x00, 0x00, 0x01, 0x70 };
// Inviamo il comando (a cui il sensore risponde con un messaggio di conferma)
Serial2.write(comandoPassiveMode, 7);
// Aspettiamo che la trasmessione seriale del comando sia avvenuta per intero
Serial2.flush();
// Aspettiamo la risposta (dovrebbero bastare 9 ms, ma noi siamo prudenti :P)
delay(23);
// Il messaggio di conferma del sensore che ci aspettiamo è lungo 8 byte
if (Serial2.available() != 8) {
return false; // risposta assente o non conforme al protocollo di trasporto
}
// Confermiamo la risposta del sensore
if (Serial2.read() == 0x42
&& Serial2.read() == 0x4D
&& Serial2.read() == 0x00
&& Serial2.read() == 0x04
&& Serial2.read() == 0xE1
&& Serial2.read() == 0x00
&& Serial2.read() == 0x01
&& Serial2.read() == 0x74) {
return true; // inizializzazione effettuata con successo
}
return false; // inizializzazione non riuscita
// Nota: quando il sensore entra in modalità passiva per la prima volta dalla
// sua accensione (o riattivazione dal risparmio energetico) può aver bisogno
// di fino a 900 ms circa per aggiornare la sua struttura dati interna con le
// nuove misurazioni. In questa variante però non serve bloccare l'esecuzione
// per evitare che la prossima rilevazione fallisca, poiché sappiamo di dover
// ancora stabilizzare il sensore (e quell'attesa sarà più che sufficiente).
}
/**
* Legge il sensore (presupponendo che la comunicazione sia già stata aperta) e
* memorizza le misurazioni di polveri sottili nelle apposite variabili globali.
*
* @return `true` se la lettura ha esito positivo, `false` altrimenti.
*/
bool readPM() {
// Scartiamo eventuali dati spuri che potrebbero interferire con la lettura
while (Serial2.available()) {
Serial2.read();
}
// Comando per richiedere una lettura quando il sensore è in `passive mode`
uint8_t comandoRead[7] = { 0x42, 0x4D, 0xE2, 0x00, 0x00, 0x01, 0x71 };
// Inviamo il comando (a cui il sensore risponde con i dati della lettura)
Serial2.write(comandoRead, 7);
// Aspettiamo che la trasmessione seriale del comando sia avvenuta per intero
Serial2.flush();
// Aspettiamo la risposta (dovrebbero bastare 34 ms, ma noi siamo prudenti :P)
delay(42);
// Assicuriamoci di aver ricevuto tutti e soli i 32 byte che ci aspettiamo in
// base al protocollo di trasporto (rif. scheda tecnica del sensore PMS5003)
if (Serial2.available() != 32) {
return false; // risposta assente o non conforme al protocollo di trasporto
}
uint8_t datiSensore[32] = { 0 };
uint16_t checksumCalcolato = 0;
// Prepariamo i dati da elaborare e i checksum per i controlli di integrità
for (uint8_t i = 0; i < 32; i++) {
datiSensore[i] = Serial2.read();
checksumCalcolato += datiSensore[i];
}
checksumCalcolato -= datiSensore[30] + datiSensore[31];
// Validiamo i dati ricevuti e controlliamo l'esito della trasmissione seriale
if (checksumCalcolato == 0 || checksumCalcolato == 0xAB) {
return false; // sensore non disponibile (tutti i dati uguali a 0)
}
uint16_t checksumRicevuto = (datiSensore[30] << 8) | datiSensore[31];
if (checksumRicevuto != checksumCalcolato) {
return false; // errore di trasmissione (checksum non corrispondenti)
}
// Elaboriamo le rilevazioni di PM e memorizziamole nelle rispettive variabili
pm1 = (datiSensore[10] << 8) | datiSensore[11];
pm25 = (datiSensore[12] << 8) | datiSensore[13];
pm10 = (datiSensore[14] << 8) | datiSensore[15];
return true; // lettura effettuata con successo
}
/**
* Legge il DHT22 (presupponendo che la comunicazione sia già stata aperta) e
* memorizza le misurazioni di temperatura e umidità nelle apposite variabili.
*
* @return `true` se la lettura ha esito positivo, `false` altrimenti.
*/
bool readDHT() {
temperatura = dht.readTemperature();
umidita = dht.readHumidity();
if (isnan(umidita) || isnan(temperatura)) {
return false; // lettura fallita
}
return true; // lettura effettuata con successo
}
/**
* Imposta la spia della centralina in base al colore passato come parametro.
*
* @param color Il colore da impostare (oppure `OFF` per spegnere il LED).
*/
void setLed(led_color_t color) {
if (ledAcceso && color == coloreLed) {
return;
}
if (color == OFF) {
ledAcceso = false;
digitalWrite(PIN_LED_R, LOW);
digitalWrite(PIN_LED_G, LOW);
digitalWrite(PIN_LED_B, LOW);
return;
}
coloreLed = color;
ledAcceso = true;
switch (color) {
case GREEN:
digitalWrite(PIN_LED_R, LOW);
digitalWrite(PIN_LED_G, HIGH);
digitalWrite(PIN_LED_B, LOW);
break;
case BLUE:
digitalWrite(PIN_LED_R, LOW);
digitalWrite(PIN_LED_G, LOW);
digitalWrite(PIN_LED_B, HIGH);
break;
case RED:
digitalWrite(PIN_LED_R, HIGH);
digitalWrite(PIN_LED_G, LOW);
digitalWrite(PIN_LED_B, LOW);
break;
case YELLOW:
digitalWrite(PIN_LED_R, HIGH);
digitalWrite(PIN_LED_G, HIGH);
digitalWrite(PIN_LED_B, LOW);
break;
case MAGENTA:
digitalWrite(PIN_LED_R, HIGH);
digitalWrite(PIN_LED_G, LOW);
digitalWrite(PIN_LED_B, HIGH);
break;
case CYAN:
digitalWrite(PIN_LED_R, LOW);
digitalWrite(PIN_LED_G, HIGH);
digitalWrite(PIN_LED_B, HIGH);
break;
case WHITE:
digitalWrite(PIN_LED_R, HIGH);
digitalWrite(PIN_LED_G, HIGH);
digitalWrite(PIN_LED_B, HIGH);
break;
default:
// abbiamo già coperto esaustivamente tutte le casistiche possibili
break;
}
}
/**
* Prepara la centralina alla fase di monitoraggio continuo.
*/
void setup() {
// Impostiamo la frequenza della CPU a 80 MHz per consumare meno corrente
setCpuFrequencyMhz(80);
// Inizializziamo i pin del LED RGB
pinMode(PIN_LED_R, OUTPUT);
pinMode(PIN_LED_G, OUTPUT);
pinMode(PIN_LED_B, OUTPUT);
// Accendiamo il LED per indicare che il sensore è in fase di stabilizzazione
setLed(COLORE_STABILIZZAZIONE_SENSORE);
// Inizializziamo il Wi-Fi
WiFi.begin(NOME_RETE_WIFI, PASSWORD_WIFI);
// Nota: l'ESP32 implementa l'API WiFi di Arduino impostando la riconnessione
// automatica di default, ma dai nostri test è risultato più efficace gestirla
// con un evento (strategia consigliata anche nella documentazione dell'ESP32,
// v. https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html#wi-fi-reconnect).
// Disabilitiamo la riconnessione automatica
WiFi.setAutoReconnect(false);
// Registriamo la funzione da richiamare in caso di disconnessione dal Wi-Fi
WiFi.onEvent(
[](WiFiEvent_t e, WiFiEventInfo_t i) { // gestore evento (funzione anonima)
WiFi.reconnect();
},
WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED); // evento disconnessione
// Inizializziamo la communicazione con i sensori
Serial2.begin(9600, SERIAL_8N1, PIN_RX2, PIN_TX2);
dht.begin();
// Impostiamo il sensore in modalità passiva
beginPM();
// Nota: la funzione `beginPM()` restituisce un valore in base all'esito, per
// cui potremmo gestire un eventuale errore (ad esempio ritentando per un tot
// di volte e/o segnalandolo attraverso una specifica configurazione del LED).
// Questa eventualità però è decisamente rara per un sensore funzionante e in
// ogni caso verrebbe riflessa dallo stato della centralina (LED perennemente
// bianco) e dal relativo campo di controllo sul canale ThingSpeak (in quanto
// `esitoLetturePM` sarebbe uguale a zero). Per questo motivo, abbiamo quindi
// deciso semplicemente di ignorarla del tutto e andare avanti in beatitudine.
// Impostiamo il certificato del client e il timeout per l'handshake SSL/TLS
client.setCACert(CA_CERT);
client.setHandshakeTimeout(TIMEOUT_HANDSHAKE_SSL_S);
// Impostiamo i timeout per la connessione al client e la risposta del server
// (v. https://github.com/espressif/arduino-esp32/issues/1433#issuecomment-475875579)
httpClient.setConnectTimeout(TIMEOUT_CONNESSIONE_CLIENT_MS);
httpClient.setTimeout(TIMEOUT_RISPOSTA_SERVER_MS);
// Aspettiamo che il sensore di polveri sottili si sia stabilizzato
while (1000L * STABILIZZAZIONE_SENSORE_PM_S > millis()) {
// Nel frattempo, facciamo lampeggiare il LED se il Wi-Fi è disconnesso
if (WiFi.status() != WL_CONNECTED) {
delay(INTERVALLO_INTERMITTENZA_LED_MS);
setLed(OFF);
delay(INTERVALLO_INTERMITTENZA_LED_MS);
setLed(COLORE_STABILIZZAZIONE_SENSORE);
// Nota: bloccare l'esecuzione per tempi ragionevolmente brevi come quello
// di un LED che lampeggia è del tutto innocuo in questo contesto (e rende
// anche più immediata l'implementazione, eliminando il caso limite in cui
// il Wi-Fi dovesse riconnettersi appena prima che il LED venga riacceso).
}
}
}
/**
* Monitora il PM e il LED, controlla il Wi-Fi e aggiorna il canale ThingSpeak.
*/
void loop() {
// Controlliamo se è il momento di eseguire il task di monitoraggio
if (millis() - millisMonitoraggio >= INTERVALLO_MONITORAGGIO_PM_MS) {
millisMonitoraggio = millis();
if (!readPM()) {
lettureFallite++;
setLed(COLORE_LETTURA_FALLITA);
} else {
lettureRiuscite++;
// Qui c'è un operatore ternario innestato in un altro operatore ternario
// (ma è meno complesso di quanto possa sembrare, vedi i commenti a lato)
setLed(pm10 < SOGLIA_PM10_BASSA // se questa condizione è vera
? COLORE_PM10_BASSO // allora restituisce questo valore
: (pm10 < SOGLIA_PM10_ALTA // altrimenti: se questa è vera
? COLORE_PM10_MEDIO // allora restituisce questo valore
: COLORE_PM10_ALTO)); // altrimenti: restituisce questo qui
if (lettureRiuscite == 1) { // passo iniziale per il calcolo della media
pm10media = pm10;
pm25media = pm25;
pm1media = pm1;
} else {
const float ALFA = 0.69; // parametro per la media mobile esponenziale
pm10media += ALFA * (pm10 - pm10media);
pm25media += ALFA * (pm25 - pm25media);
pm1media += ALFA * (pm1 - pm1media);
}
} // fine controllo lettura
} // fine task monitoraggio
// Segnaliamo che il WiFi è disconnesso facendo lampeggiare il LED
if (WiFi.status() != WL_CONNECTED) {
if (millis() - millisLed >= INTERVALLO_INTERMITTENZA_LED_MS) {
millisLed = millis();
setLed(ledAcceso ? OFF : coloreLed);
}
// Nota: bloccare l'esecuzione interferirebbe con il task di monitoraggio,
// quindi abbiamo usato un timer per il LED e gestito l'evento per eseguire
// la funzione di riconnessione al Wi-Fi in parallelo, in un thread a parte.
} else {
// Se il WiFi si riconnette mentre il LED è ancora spento, riaccendiamolo
if (!ledAcceso) {
setLed(coloreLed);
}
// Nota: questo controllo è ridondante perché svolto anche all'interno della
// funzione, ma lo sappiamo solo perché l'abbiamo scritta noi. Se avessimo a
// che fare con un generico metodo setter, potremmo non essere a conoscenza
// di simili dettagli implementativi (e un controllo aggiuntivo contribuisce
// comunque alla chiarezza del codice, a un costo computazionale irrisorio).
// Controlliamo se è il momento di eseguire il task di aggiornamento
if (millis() - millisAggiornamento >= intervalloAggiornamento) {
// Elaboriamo i dati di controllo da inviare al canale ThingSpeak
int8_t rssi = WiFi.RSSI();
float esitoLetturePM = 100.0 * lettureRiuscite / (lettureRiuscite + lettureFallite);
bool esitoLetturaDHT = readDHT();
// Creiamo la richiesta nel formato definito dall'API di ThingSpeak
String uri = "/update?api_key=" + String(WRITE_API_KEY);
// Inviamo i dati sulle polveri sottili solo se disponibili
if (lettureRiuscite > 0) {
uri += "&field1=" + String(pm10media, 2)
+ "&field2=" + String(pm25media, 2)
+ "&field3=" + String(pm1media, 2);
}
// Inviamo sempre e comunque i dati di controllo
uri += "&field4=" + String(rssi)
+ "&field5=" + String(esitoLetturePM, 2)
+ "&field6=" + String((int)esitoLetturaDHT);
// Inviamo i dati su temperatura e umidità solo se disponibili
if (esitoLetturaDHT) {
uri += "&field7=" + String(temperatura, 2)
+ "&field8=" + String(umidita, 2);
}
// Predisponiamo l'invio della richiesta GET e aspettiamo la risposta
httpClient.begin(client, "api.thingspeak.com", 443, uri, true);
led_color_t colorePrecedente = coloreLed;
setLed(COLORE_AGGIORNAMENTO_THINGSPEAK);
int16_t codiceRisposta = httpClient.GET(); // funzione bloccante
// Nota: in buone condizioni di connettività ci vuole solitamente meno di
// mezzo secondo, mentre nei casi peggiori la richiesta andrà in timeout.
// Il primo invio richiede fino a 2 secondi per stabilire una connessione
// criptata. Il protocollo HTTP è più veloce ma insicuro (ed è meglio non
// trasmettere in chiaro informazioni sensibili come la chiave API).
// Reimpostiamo il timer del task di aggiornamento e il colore del LED
millisAggiornamento = millis();
setLed(colorePrecedente);
// Aggiorniamo l'intervallo del task (in base all'esito della richiesta)
if (codiceRisposta != HTTP_CODE_OK) {
intervalloAggiornamento = ATTESA_NUOVO_TENTATIVO_MS;
} else {
intervalloAggiornamento = INTERVALLO_AGGIORNAMENTI_MS;
// Resettiamo i contatori delle letture fino al prossimo aggiornamento
lettureRiuscite = lettureFallite = 0;
// (la media mobile delle rilevazioni invece si aggiorna in automatico)
}
// Terminiamo la richiesta appena effettuata (a prescindere dal suo esito)
httpClient.end();
} // fine task aggiornamento
} // fine controllo wifi
} // fine loop
Esempio lettura modalità attiva PMS5003
Torna all'indice 👆️
File sorgente: scienziaria-lettura-pms5003.ino
Codice da copiare:
/**
*
* Lettura sensore di polveri sottili PMS5003 in modalità attiva
*
* Codice di esempio nell'ambito del progetto ScienziAria di Comunità Circolare
* per il monitoraggio della qualità dell'aria (e in particolare PM10 e PM2,5).
*
* GUIDA PASSO PASSO per costruire la tua CENTRALINA FAI DA TE
* 👉️ https://comunitacircolare.it/articoli/scienziaria-guida-centralina
*
* \author Michele Nuzzolese
* \copyright Pubblico dominio con licenza Unlicense: https://unlicense.org
* \sa Progetto ScienziAria: https://comunitacircolare.it/progetti/scienziaria
*
*/
const uint8_t PIN_RX2 = 26; // Da collegare al pin TXD del PMS5003.
const uint8_t PIN_TX2 = 27; // Da collegare al pin RXD del PMS5003.
uint16_t pm1 = 0, pm25 = 0, pm10 = 0; // Concentrazione polveri sottili.
void setup() {
// Inizializziamo il Monitor Seriale
Serial.begin(115200);
// Inizializziamo la comunicazione con il sensore
Serial2.begin(9600, SERIAL_8N1, PIN_RX2, PIN_TX2);
// Stampiamo l'intestazione della tabella sul monitor seriale
Serial.println();
Serial.println(".---------------------.");
Serial.println("| PM1 | PM2,5 | PM10 |");
Serial.println("|------|-------|------|");
}
void loop() {
// Timer per il task di controllo periodico dei dati.
static uint32_t millisControllo = 0;
// Controlliamo le variazioni ogni 3 secondi
if (millis() - millisControllo >= 3000) {
// Reimpostiamo il timer per il prossimo controllo
millisControllo = millis();
// Stampiamo la concentrazione di polveri sottili sul monitor seriale
Serial.printf("| %3d | %3d | %3d |\r\n", pm1, pm25, pm10);
}
// Queste variabili ci serviranno per il task di lettura continua del sensore:
static uint8_t indice = 0; // In informatica si inizia a contare da zero :P
static uint8_t datiSensore[6];
static uint16_t checksumCalcolato, checksumRicevuto;
// Controlliamo se abbiamo ricevuto un byte dal sensore (nota: qui non c'è un
// timer, per cui questo controllo viene eseguito ad ogni iterazione del loop)
if (Serial2.available()) {
// Leggiamo il byte dal buffer seriale
int byte = Serial2.read();
// Decidiamo che fare in base a quanti e quali altri byte abbiamo già letto
switch (indice) {
// Controlliamo individualmente i primi due byte, poiché denotano l'inizio
// dell'intera sequenza di 32 byte che contiene i dati inviati dal PMS5003
// (cfr. protocollo di trasporto menzionato nella scheda tecnica).
// La sequenza inizia con 0x42 ("0x" denota la base esadecimale)
case 0:
// Se riceviamo il byte che ci aspettiamo
if (byte == 0x42) {
// Iniziamo a calcolare il checksum
checksumCalcolato = byte;
// E ci prepariamo a ricevere il byte successivo della sequenza
indice++;
}
// Altrimenti non cambiamo nulla (e ricontrolliamo al prossimo byte)
break;
// Dopo aver riconosciuto il primo byte, verifichiamo anche il secondo
case 1:
// Se riceviamo il byte che ci aspettiamo
if (byte == 0x4D) {
// Continuiamo a calcolare il checksum
checksumCalcolato += byte;
// E ci prepariamo a ricevere il byte successivo della sequenza
indice++;
} else {
// Altrimenti azzeriamo l'indice e ricominciamo la lettura
indice = 0;
}
break;
// Anche il terzo e il quarto byte sono fissi, ma non ci interessano e
// possiamo quindi accorparli al trattamento degli altri byte generici.
default:
// Continuiamo a calcolare il checksum
checksumCalcolato += byte;
// Se siamo in un punto della sequenza che ha un byte relativo al PM...
if (indice >= 10 && indice <= 15) {
datiSensore[indice - 10] = byte; // ...lo memorizziamo in un array
}
// E ci prepariamo come sempre a ricevere il byte successivo
indice++;
break;
// La sequenza termina con l'invio del checksum calcolato dal sensore
case 30:
// Elaboriamo il byte più significativo del checksum ricevuto
checksumRicevuto = byte << 8;
// E ci prepariamo a ricevere l'ultimo byte della sequenza
indice++;
break;
// Infine controlliamo l'ultimo byte e l'intera sequenza
case 31:
// Elaboriamo il byte meno significativo del checksum ricevuto
checksumRicevuto |= byte;
// Se i checksum corrispondono, la trasmissione è andata a buon fine
if (checksumCalcolato == checksumRicevuto) {
// Elaboriamo i byte relativi al PM letti precedentemente e
// memorizziamoli nelle apposite variabili globali
pm1 = (datiSensore[0] << 8) | datiSensore[1];
pm25 = (datiSensore[2] << 8) | datiSensore[3];
pm10 = (datiSensore[4] << 8) | datiSensore[5];
} else {
// Se invece si è verificato un errore durante la trasmissione,
// possiamo gestirlo qui oppure anche semplicemente ignorarlo.
// In quest'ultimo caso, le variabili preserveranno il loro valore
// attuale (quello di inizializzazione o dell'ultima lettura valida).
}
// Fine! Azzeriamo l'indice e prepariamoci per la prossima sequenza
indice = 0;
break;
} // fine controllo indice
} // fine task lettura continua
}
Link utili
Torna all'indice 👆️