🚀 Introduzione
Vuoi controllare temperatura e umidità e vedere come sono cambiate durante la giornata? 🌡️📊
In questo progetto realizzerai un monitor ambientale con storico delle ultime 24 ore, utilizzando Arduino, un sensore DHT22, un display OLED e il modulo RTC DS3231.
Arduino registrerà automaticamente una lettura ogni ora e conserverà i dati nella memoria EEPROM.
Tramite un pulsante potrai visualizzare:
Vuoi controllare temperatura e umidità e vedere come sono cambiate durante la giornata? 🌡️📊
In questo progetto realizzerai un monitor ambientale con storico delle ultime 24 ore, utilizzando Arduino, un sensore DHT22, un display OLED e il modulo RTC DS3231.
Arduino registrerà automaticamente una lettura ogni ora e conserverà i dati nella memoria EEPROM.
Tramite un pulsante potrai visualizzare:
- valori ambientali attuali;
- temperatura minima e massima;
- umidità minima e massima;
- singole registrazioni orarie.
🎯 Cosa realizzerai
Con Arduino creerai un sistema che:
Con Arduino creerai un sistema che:
- 🌡️ misura la temperatura ambientale;
- 💧 misura l’umidità relativa;
- ⏰ legge data e ora dal modulo DS3231;
- 🗂️ conserva fino a 24 registrazioni;
- 💾 salva lo storico nella EEPROM;
- 📉 calcola i valori minimi;
- 📈 calcola i valori massimi;
- 📟 mostra i dati su display OLED;
- 🔘 permette di cambiare schermata;
- ⚠️ segnala gli errori del sensore.
🧰 Materiale necessario
- 1x Arduino UNO;
- 1x sensore DHT22;
- 1x resistenza da 10 kΩ, se utilizzi il sensore senza modulo;
- 1x modulo RTC DS3231;
- 1x batteria CR2032;
- 1x display OLED 0,96 pollici I2C SSD1306;
- 1x pulsante;
- cavetti jumper;
- breadboard;
- cavo USB.
🛒 Non hai ancora i componenti?
Molti dei progetti presenti su progettiarduino.com utilizzano componenti elettronici di base come LED, resistenze, sensori e breadboard.
Se stai iniziando con Arduino, il modo più semplice è utilizzare un kit completo per principianti, che include quasi tutti i componenti necessari per realizzare molti dei progetti presenti sul sito.
👉 Kit Arduino consigliato:
ℹ️ Link di affiliazione Amazon: in qualità di Affiliato Amazon ricevo un guadagno dagli acquisti idonei, senza alcun costo aggiuntivo per te.
https://amzn.to/4b3dxjW
📘 Vuoi imparare Arduino passo dopo passo?
Se vuoi approfondire e realizzare progetti più completi, puoi dare un'occhiata anche ai miei libri dedicati ad Arduino.
All'interno troverai spiegazioni semplici, schemi chiari e molti progetti pratici pensati per chi parte da zero.
👉 Scopri i miei libri su Arduino:
ℹ️ Link di affiliazione Amazon: in qualità di Affiliato Amazon ricevo un guadagno dagli acquisti idonei, senza alcun costo aggiuntivo per te.
https://amzn.to/4rfzjXE
Molti dei progetti presenti su progettiarduino.com utilizzano componenti elettronici di base come LED, resistenze, sensori e breadboard.
Se stai iniziando con Arduino, il modo più semplice è utilizzare un kit completo per principianti, che include quasi tutti i componenti necessari per realizzare molti dei progetti presenti sul sito.
👉 Kit Arduino consigliato:
ℹ️ Link di affiliazione Amazon: in qualità di Affiliato Amazon ricevo un guadagno dagli acquisti idonei, senza alcun costo aggiuntivo per te.
https://amzn.to/4b3dxjW
📘 Vuoi imparare Arduino passo dopo passo?
Se vuoi approfondire e realizzare progetti più completi, puoi dare un'occhiata anche ai miei libri dedicati ad Arduino.
All'interno troverai spiegazioni semplici, schemi chiari e molti progetti pratici pensati per chi parte da zero.
👉 Scopri i miei libri su Arduino:
ℹ️ Link di affiliazione Amazon: in qualità di Affiliato Amazon ricevo un guadagno dagli acquisti idonei, senza alcun costo aggiuntivo per te.
https://amzn.to/4rfzjXE
📚 Librerie necessarie
Per realizzare il progetto devi installare:
Il programma utilizza inoltre:
Per realizzare il progetto devi installare:
- Adafruit SSD1306;
- Adafruit GFX Library;
- DHT sensor library;
- Adafruit Unified Sensor;
- RTClib.
Il programma utilizza inoltre:
- Wire;
- EEPROM.
🔌 Schema di collegamento
Display OLED SSD1306
Modulo RTC DS3231
Sensore DHT22
Se utilizzi un modulo già assemblato:
Pulsante
Display OLED SSD1306
- VCC → 5V;
- GND → GND;
- SDA → A4;
- SCL → A5.
Modulo RTC DS3231
- VCC → 5V;
- GND → GND;
- SDA → A4;
- SCL → A5.
Sensore DHT22
Se utilizzi un modulo già assemblato:
- VCC → 5V;
- GND → GND;
- DATA → pin digitale 2.
Pulsante
- un terminale → pin digitale 3;
- terminale opposto → GND.
💻 Codice Arduino
Il programma registra una nuova lettura quando cambia l’ora indicata dal modulo RTC.
Ogni registrazione contiene:
Il programma registra una nuova lettura quando cambia l’ora indicata dal modulo RTC.
Ogni registrazione contiene:
- temperatura;
- umidità;
- ora della misurazione.
/*
============================================================
Progetto 130 - Monitor ambiente con storico
Registra temperatura e umidita delle ultime 24 ore
nella memoria EEPROM
============================================================
*/
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <DHT.h>
#include <RTClib.h>
#include <EEPROM.h>
// Dimensioni e indirizzo del display OLED
#define LARGHEZZA_DISPLAY 128
#define ALTEZZA_DISPLAY 64
#define RESET_OLED -1
#define INDIRIZZO_OLED 0x3C
// Collegamenti del sensore e del pulsante
#define PIN_DHT 2
#define TIPO_DHT DHT22
#define PIN_PULSANTE 3
// Numero massimo di registrazioni conservate
const byte NUMERO_REGISTRAZIONI = 24;
// Intervallo di lettura del sensore
const unsigned long INTERVALLO_LETTURA = 2000;
// Tempo antirimbalzo del pulsante
const unsigned long TEMPO_DEBOUNCE = 180;
// Firma utilizzata per verificare
// se la EEPROM contiene dati validi
const uint16_t FIRMA_EEPROM = 13024;
// Indirizzi iniziali della memoria EEPROM
const int EEPROM_FIRMA = 0;
const int EEPROM_NUMERO_DATI = 2;
const int EEPROM_PROSSIMA_POSIZIONE = 3;
const int EEPROM_ULTIMA_ORA = 4;
const int EEPROM_STORICO = 5;
// Oggetti utilizzati per il display,
// il sensore DHT22 e il modulo RTC
Adafruit_SSD1306 display(
LARGHEZZA_DISPLAY,
ALTEZZA_DISPLAY,
&Wire,
RESET_OLED
);
DHT dht(PIN_DHT, TIPO_DHT);
RTC_DS3231 rtc;
// Ogni registrazione contiene temperatura,
// umidità e ora della misurazione
struct Registrazione {
int16_t temperaturaDecimi;
byte umidita;
byte ora;
};
// Array che contiene lo storico in memoria RAM
Registrazione storico[NUMERO_REGISTRAZIONI];
// Valori ambientali correnti
float temperatura = 0.0;
float umidita = 0.0;
// Indica se l'ultima lettura è valida
bool sensoreValido = false;
// Numero di registrazioni realmente disponibili
byte numeroDati = 0;
// Posizione in cui verrà scritto il prossimo campione
byte prossimaPosizione = 0;
// Ultima ora già registrata
byte ultimaOraRegistrata = 255;
// Schermate disponibili
enum Schermata {
SCHERMATA_ATTUALE,
SCHERMATA_RIEPILOGO,
SCHERMATA_STORICO
};
Schermata schermataAttuale = SCHERMATA_ATTUALE;
// Indice della registrazione visualizzata
byte indiceStoricoVisualizzato = 0;
// Variabili temporali utilizzate da millis()
unsigned long ultimaLettura = 0;
unsigned long ultimaPressione = 0;
void setup() {
// Il pulsante è collegato tra il pin e GND
pinMode(PIN_PULSANTE, INPUT_PULLUP);
// Avvia sensore e comunicazione I2C
dht.begin();
Wire.begin();
// Inizializza il display OLED
if (!display.begin(
SSD1306_SWITCHCAPVCC,
INDIRIZZO_OLED
)) {
while (true) {
}
}
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextWrap(false);
mostraAvvio();
// Controlla che il modulo RTC sia collegato
if (!rtc.begin()) {
mostraErroreRtc();
while (true) {
}
}
// Se il DS3231 ha perso l'alimentazione,
// imposta data e ora di compilazione
if (rtc.lostPower()) {
rtc.adjust(
DateTime(
F(__DATE__),
F(__TIME__)
)
);
}
// Recupera lo storico dalla memoria EEPROM
caricaStorico();
// Esegue subito una prima lettura
leggiSensore();
DateTime adesso = rtc.now();
// Registra immediatamente un primo dato
// se non è già presente una lettura per l'ora corrente
controllaRegistrazioneOraria(adesso);
mostraSchermata(adesso);
ultimaLettura = millis();
}
void loop() {
DateTime adesso = rtc.now();
// Gestisce il cambio delle schermate
gestisciPulsante();
// Legge il sensore DHT22 ogni due secondi
if (millis() - ultimaLettura >= INTERVALLO_LETTURA) {
ultimaLettura = millis();
leggiSensore();
controllaRegistrazioneOraria(adesso);
mostraSchermata(adesso);
}
}
void leggiSensore() {
// Acquisisce temperatura e umidità
float nuovaTemperatura = dht.readTemperature();
float nuovaUmidita = dht.readHumidity();
// Ignora letture non valide
if (isnan(nuovaTemperatura) ||
isnan(nuovaUmidita)) {
sensoreValido = false;
return;
}
sensoreValido = true;
temperatura = nuovaTemperatura;
umidita = nuovaUmidita;
}
void controllaRegistrazioneOraria(
const DateTime& adesso
) {
// Non salva dati se il sensore
// non ha restituito valori validi
if (!sensoreValido) {
return;
}
byte oraCorrente = adesso.hour();
// Registra un nuovo campione soltanto
// quando cambia l'ora
if (oraCorrente == ultimaOraRegistrata) {
return;
}
salvaNuovaRegistrazione(
temperatura,
umidita,
oraCorrente
);
ultimaOraRegistrata = oraCorrente;
// Salva anche l'ultima ora nella EEPROM
EEPROM.update(
EEPROM_ULTIMA_ORA,
ultimaOraRegistrata
);
}
void salvaNuovaRegistrazione(
float nuovaTemperatura,
float nuovaUmidita,
byte ora
) {
// Converte la temperatura in decimi di grado
storico[prossimaPosizione].temperaturaDecimi =
(int16_t)(nuovaTemperatura * 10.0);
// Limita e salva l'umidità come numero intero
storico[prossimaPosizione].umidita =
constrain((int)nuovaUmidita, 0, 100);
storico[prossimaPosizione].ora = ora;
// Salva nella EEPROM soltanto
// la registrazione appena modificata
int indirizzo =
EEPROM_STORICO +
prossimaPosizione * sizeof(Registrazione);
EEPROM.put(
indirizzo,
storico[prossimaPosizione]
);
// Aumenta il numero dei dati disponibili
// fino al limite massimo di 24
if (numeroDati < NUMERO_REGISTRAZIONI) {
numeroDati++;
}
// Passa alla posizione successiva
prossimaPosizione++;
// Dopo l'ultima posizione riparte dalla prima
if (prossimaPosizione >= NUMERO_REGISTRAZIONI) {
prossimaPosizione = 0;
}
// Aggiorna i dati di controllo nella EEPROM
EEPROM.update(
EEPROM_NUMERO_DATI,
numeroDati
);
EEPROM.update(
EEPROM_PROSSIMA_POSIZIONE,
prossimaPosizione
);
}
void caricaStorico() {
uint16_t firmaSalvata;
// Legge la firma dalla EEPROM
EEPROM.get(
EEPROM_FIRMA,
firmaSalvata
);
// Se la firma non è valida,
// inizializza uno storico vuoto
if (firmaSalvata != FIRMA_EEPROM) {
inizializzaEeprom();
return;
}
// Recupera le informazioni principali
numeroDati = EEPROM.read(
EEPROM_NUMERO_DATI
);
prossimaPosizione = EEPROM.read(
EEPROM_PROSSIMA_POSIZIONE
);
ultimaOraRegistrata = EEPROM.read(
EEPROM_ULTIMA_ORA
);
// Controlla che i valori siano nei limiti
if (numeroDati > NUMERO_REGISTRAZIONI ||
prossimaPosizione >= NUMERO_REGISTRAZIONI) {
inizializzaEeprom();
return;
}
// Carica tutte le registrazioni dalla EEPROM
for (byte i = 0; i < NUMERO_REGISTRAZIONI; i++) {
int indirizzo =
EEPROM_STORICO +
i * sizeof(Registrazione);
EEPROM.get(
indirizzo,
storico[i]
);
}
}
void inizializzaEeprom() {
// Azzera lo stato dello storico
numeroDati = 0;
prossimaPosizione = 0;
ultimaOraRegistrata = 255;
// Salva la firma e i valori iniziali
EEPROM.put(
EEPROM_FIRMA,
FIRMA_EEPROM
);
EEPROM.update(
EEPROM_NUMERO_DATI,
numeroDati
);
EEPROM.update(
EEPROM_PROSSIMA_POSIZIONE,
prossimaPosizione
);
EEPROM.update(
EEPROM_ULTIMA_ORA,
ultimaOraRegistrata
);
// Inizializza anche l'array in RAM
for (byte i = 0; i < NUMERO_REGISTRAZIONI; i++) {
storico[i].temperaturaDecimi = 0;
storico[i].umidita = 0;
storico[i].ora = 0;
}
}
void gestisciPulsante() {
// Con INPUT_PULLUP il pulsante premuto
// viene letto come LOW
if (digitalRead(PIN_PULSANTE) == LOW &&
millis() - ultimaPressione >= TEMPO_DEBOUNCE) {
ultimaPressione = millis();
// Dalla schermata principale passa al riepilogo
if (schermataAttuale == SCHERMATA_ATTUALE) {
schermataAttuale = SCHERMATA_RIEPILOGO;
}
// Dal riepilogo passa allo storico
else if (schermataAttuale ==
SCHERMATA_RIEPILOGO) {
schermataAttuale = SCHERMATA_STORICO;
indiceStoricoVisualizzato = 0;
}
// Nello storico mostra il campione successivo
else {
indiceStoricoVisualizzato++;
// Dopo l'ultima registrazione
// torna alla schermata principale
if (indiceStoricoVisualizzato >= numeroDati) {
indiceStoricoVisualizzato = 0;
schermataAttuale = SCHERMATA_ATTUALE;
}
}
mostraSchermata(rtc.now());
}
}
void mostraSchermata(const DateTime& adesso) {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
if (schermataAttuale == SCHERMATA_ATTUALE) {
mostraValoriAttuali(adesso);
} else if (
schermataAttuale == SCHERMATA_RIEPILOGO
) {
mostraRiepilogo();
} else {
mostraStorico();
}
// Mostra un avviso se l'ultima lettura
// del DHT22 non è valida
if (!sensoreValido) {
display.fillRect(
84,
54,
43,
10,
SSD1306_BLACK
);
display.setTextSize(1);
display.setCursor(87, 55);
display.print("DHT ERR");
}
display.display();
}
void mostraValoriAttuali(
const DateTime& adesso
) {
display.setTextSize(1);
// Titolo e ora corrente
display.setCursor(0, 0);
display.print("MONITOR AMBIENTE");
display.setCursor(92, 0);
stampaDueCifre(adesso.hour());
display.print(":");
stampaDueCifre(adesso.minute());
display.drawLine(
0,
10,
127,
10,
SSD1306_WHITE
);
// Temperatura attuale
display.setTextSize(2);
display.setCursor(0, 18);
display.print(temperatura, 1);
display.print((char)247);
display.print("C");
// Umidità attuale
display.setCursor(75, 18);
display.print(umidita, 0);
display.print("%");
display.setTextSize(1);
// Numero di registrazioni disponibili
display.setCursor(0, 45);
display.print("Storico: ");
display.print(numeroDati);
display.print("/");
display.print(NUMERO_REGISTRAZIONI);
display.setCursor(0, 56);
display.print("Premi: riepilogo");
}
void mostraRiepilogo() {
display.setTextSize(1);
display.setCursor(0, 0);
display.print("MINIMI E MASSIMI");
display.drawLine(
0,
10,
127,
10,
SSD1306_WHITE
);
// Se non esistono registrazioni,
// mostra un messaggio dedicato
if (numeroDati == 0) {
display.setCursor(14, 28);
display.print("Storico vuoto");
display.setCursor(9, 44);
display.print("Attendi una lettura");
return;
}
float tempMin;
float tempMax;
byte umidMin;
byte umidMax;
calcolaMinimiMassimi(
tempMin,
tempMax,
umidMin,
umidMax
);
display.setCursor(0, 16);
display.print("Temp min: ");
display.print(tempMin, 1);
display.print((char)247);
display.print("C");
display.setCursor(0, 28);
display.print("Temp max: ");
display.print(tempMax, 1);
display.print((char)247);
display.print("C");
display.setCursor(0, 42);
display.print("Umid min: ");
display.print(umidMin);
display.print("%");
display.setCursor(0, 54);
display.print("Umid max: ");
display.print(umidMax);
display.print("%");
}
void mostraStorico() {
display.setTextSize(1);
display.setCursor(0, 0);
display.print("STORICO ORARIO");
display.drawLine(
0,
10,
127,
10,
SSD1306_WHITE
);
if (numeroDati == 0) {
display.setCursor(14, 28);
display.print("Storico vuoto");
display.setCursor(8, 44);
display.print("Premi per tornare");
return;
}
// Recupera la registrazione in ordine cronologico,
// dalla più vecchia alla più recente
Registrazione dato =
leggiRegistrazioneCronologica(
indiceStoricoVisualizzato
);
display.setCursor(0, 16);
display.print("Campione ");
display.print(indiceStoricoVisualizzato + 1);
display.print("/");
display.print(numeroDati);
display.setCursor(0, 29);
display.print("Ora: ");
stampaDueCifre(dato.ora);
display.print(":00");
display.setCursor(0, 42);
display.print("Temp: ");
display.print(
dato.temperaturaDecimi / 10.0,
1
);
display.print((char)247);
display.print("C");
display.setCursor(0, 55);
display.print("Umid: ");
display.print(dato.umidita);
display.print("%");
}
Registrazione leggiRegistrazioneCronologica(
byte indice
) {
byte posizioneReale;
// Se il buffer non è ancora pieno,
// i dati iniziano dalla posizione zero
if (numeroDati < NUMERO_REGISTRAZIONI) {
posizioneReale = indice;
} else {
// Quando il buffer è pieno,
// la prossima posizione indica il dato più vecchio
posizioneReale =
(prossimaPosizione + indice) %
NUMERO_REGISTRAZIONI;
}
return storico[posizioneReale];
}
void calcolaMinimiMassimi(
float& tempMin,
float& tempMax,
byte& umidMin,
byte& umidMax
) {
// Utilizza la prima registrazione
// per inizializzare i valori
Registrazione primoDato =
leggiRegistrazioneCronologica(0);
tempMin =
primoDato.temperaturaDecimi / 10.0;
tempMax = tempMin;
umidMin = primoDato.umidita;
umidMax = primoDato.umidita;
// Analizza tutte le registrazioni successive
for (byte i = 1; i < numeroDati; i++) {
Registrazione dato =
leggiRegistrazioneCronologica(i);
float temperaturaDato =
dato.temperaturaDecimi / 10.0;
if (temperaturaDato < tempMin) {
tempMin = temperaturaDato;
}
if (temperaturaDato > tempMax) {
tempMax = temperaturaDato;
}
if (dato.umidita < umidMin) {
umidMin = dato.umidita;
}
if (dato.umidita > umidMax) {
umidMax = dato.umidita;
}
}
}
void stampaDueCifre(int valore) {
// Aggiunge lo zero iniziale ai valori
// inferiori a dieci
if (valore < 10) {
display.print("0");
}
display.print(valore);
}
void mostraAvvio() {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(17, 14);
display.print("Monitor ambiente");
display.setCursor(18, 30);
display.print("Storico 24 ore");
display.setCursor(28, 47);
display.print("Progetto 130");
display.display();
delay(1800);
}
void mostraErroreRtc() {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(27, 15);
display.print("ERRORE RTC");
display.setCursor(9, 31);
display.print("DS3231 non rilevato");
display.setCursor(15, 47);
display.print("Controlla i cavi");
display.display();
}
============================================================
Progetto 130 - Monitor ambiente con storico
Registra temperatura e umidita delle ultime 24 ore
nella memoria EEPROM
============================================================
*/
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <DHT.h>
#include <RTClib.h>
#include <EEPROM.h>
// Dimensioni e indirizzo del display OLED
#define LARGHEZZA_DISPLAY 128
#define ALTEZZA_DISPLAY 64
#define RESET_OLED -1
#define INDIRIZZO_OLED 0x3C
// Collegamenti del sensore e del pulsante
#define PIN_DHT 2
#define TIPO_DHT DHT22
#define PIN_PULSANTE 3
// Numero massimo di registrazioni conservate
const byte NUMERO_REGISTRAZIONI = 24;
// Intervallo di lettura del sensore
const unsigned long INTERVALLO_LETTURA = 2000;
// Tempo antirimbalzo del pulsante
const unsigned long TEMPO_DEBOUNCE = 180;
// Firma utilizzata per verificare
// se la EEPROM contiene dati validi
const uint16_t FIRMA_EEPROM = 13024;
// Indirizzi iniziali della memoria EEPROM
const int EEPROM_FIRMA = 0;
const int EEPROM_NUMERO_DATI = 2;
const int EEPROM_PROSSIMA_POSIZIONE = 3;
const int EEPROM_ULTIMA_ORA = 4;
const int EEPROM_STORICO = 5;
// Oggetti utilizzati per il display,
// il sensore DHT22 e il modulo RTC
Adafruit_SSD1306 display(
LARGHEZZA_DISPLAY,
ALTEZZA_DISPLAY,
&Wire,
RESET_OLED
);
DHT dht(PIN_DHT, TIPO_DHT);
RTC_DS3231 rtc;
// Ogni registrazione contiene temperatura,
// umidità e ora della misurazione
struct Registrazione {
int16_t temperaturaDecimi;
byte umidita;
byte ora;
};
// Array che contiene lo storico in memoria RAM
Registrazione storico[NUMERO_REGISTRAZIONI];
// Valori ambientali correnti
float temperatura = 0.0;
float umidita = 0.0;
// Indica se l'ultima lettura è valida
bool sensoreValido = false;
// Numero di registrazioni realmente disponibili
byte numeroDati = 0;
// Posizione in cui verrà scritto il prossimo campione
byte prossimaPosizione = 0;
// Ultima ora già registrata
byte ultimaOraRegistrata = 255;
// Schermate disponibili
enum Schermata {
SCHERMATA_ATTUALE,
SCHERMATA_RIEPILOGO,
SCHERMATA_STORICO
};
Schermata schermataAttuale = SCHERMATA_ATTUALE;
// Indice della registrazione visualizzata
byte indiceStoricoVisualizzato = 0;
// Variabili temporali utilizzate da millis()
unsigned long ultimaLettura = 0;
unsigned long ultimaPressione = 0;
void setup() {
// Il pulsante è collegato tra il pin e GND
pinMode(PIN_PULSANTE, INPUT_PULLUP);
// Avvia sensore e comunicazione I2C
dht.begin();
Wire.begin();
// Inizializza il display OLED
if (!display.begin(
SSD1306_SWITCHCAPVCC,
INDIRIZZO_OLED
)) {
while (true) {
}
}
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextWrap(false);
mostraAvvio();
// Controlla che il modulo RTC sia collegato
if (!rtc.begin()) {
mostraErroreRtc();
while (true) {
}
}
// Se il DS3231 ha perso l'alimentazione,
// imposta data e ora di compilazione
if (rtc.lostPower()) {
rtc.adjust(
DateTime(
F(__DATE__),
F(__TIME__)
)
);
}
// Recupera lo storico dalla memoria EEPROM
caricaStorico();
// Esegue subito una prima lettura
leggiSensore();
DateTime adesso = rtc.now();
// Registra immediatamente un primo dato
// se non è già presente una lettura per l'ora corrente
controllaRegistrazioneOraria(adesso);
mostraSchermata(adesso);
ultimaLettura = millis();
}
void loop() {
DateTime adesso = rtc.now();
// Gestisce il cambio delle schermate
gestisciPulsante();
// Legge il sensore DHT22 ogni due secondi
if (millis() - ultimaLettura >= INTERVALLO_LETTURA) {
ultimaLettura = millis();
leggiSensore();
controllaRegistrazioneOraria(adesso);
mostraSchermata(adesso);
}
}
void leggiSensore() {
// Acquisisce temperatura e umidità
float nuovaTemperatura = dht.readTemperature();
float nuovaUmidita = dht.readHumidity();
// Ignora letture non valide
if (isnan(nuovaTemperatura) ||
isnan(nuovaUmidita)) {
sensoreValido = false;
return;
}
sensoreValido = true;
temperatura = nuovaTemperatura;
umidita = nuovaUmidita;
}
void controllaRegistrazioneOraria(
const DateTime& adesso
) {
// Non salva dati se il sensore
// non ha restituito valori validi
if (!sensoreValido) {
return;
}
byte oraCorrente = adesso.hour();
// Registra un nuovo campione soltanto
// quando cambia l'ora
if (oraCorrente == ultimaOraRegistrata) {
return;
}
salvaNuovaRegistrazione(
temperatura,
umidita,
oraCorrente
);
ultimaOraRegistrata = oraCorrente;
// Salva anche l'ultima ora nella EEPROM
EEPROM.update(
EEPROM_ULTIMA_ORA,
ultimaOraRegistrata
);
}
void salvaNuovaRegistrazione(
float nuovaTemperatura,
float nuovaUmidita,
byte ora
) {
// Converte la temperatura in decimi di grado
storico[prossimaPosizione].temperaturaDecimi =
(int16_t)(nuovaTemperatura * 10.0);
// Limita e salva l'umidità come numero intero
storico[prossimaPosizione].umidita =
constrain((int)nuovaUmidita, 0, 100);
storico[prossimaPosizione].ora = ora;
// Salva nella EEPROM soltanto
// la registrazione appena modificata
int indirizzo =
EEPROM_STORICO +
prossimaPosizione * sizeof(Registrazione);
EEPROM.put(
indirizzo,
storico[prossimaPosizione]
);
// Aumenta il numero dei dati disponibili
// fino al limite massimo di 24
if (numeroDati < NUMERO_REGISTRAZIONI) {
numeroDati++;
}
// Passa alla posizione successiva
prossimaPosizione++;
// Dopo l'ultima posizione riparte dalla prima
if (prossimaPosizione >= NUMERO_REGISTRAZIONI) {
prossimaPosizione = 0;
}
// Aggiorna i dati di controllo nella EEPROM
EEPROM.update(
EEPROM_NUMERO_DATI,
numeroDati
);
EEPROM.update(
EEPROM_PROSSIMA_POSIZIONE,
prossimaPosizione
);
}
void caricaStorico() {
uint16_t firmaSalvata;
// Legge la firma dalla EEPROM
EEPROM.get(
EEPROM_FIRMA,
firmaSalvata
);
// Se la firma non è valida,
// inizializza uno storico vuoto
if (firmaSalvata != FIRMA_EEPROM) {
inizializzaEeprom();
return;
}
// Recupera le informazioni principali
numeroDati = EEPROM.read(
EEPROM_NUMERO_DATI
);
prossimaPosizione = EEPROM.read(
EEPROM_PROSSIMA_POSIZIONE
);
ultimaOraRegistrata = EEPROM.read(
EEPROM_ULTIMA_ORA
);
// Controlla che i valori siano nei limiti
if (numeroDati > NUMERO_REGISTRAZIONI ||
prossimaPosizione >= NUMERO_REGISTRAZIONI) {
inizializzaEeprom();
return;
}
// Carica tutte le registrazioni dalla EEPROM
for (byte i = 0; i < NUMERO_REGISTRAZIONI; i++) {
int indirizzo =
EEPROM_STORICO +
i * sizeof(Registrazione);
EEPROM.get(
indirizzo,
storico[i]
);
}
}
void inizializzaEeprom() {
// Azzera lo stato dello storico
numeroDati = 0;
prossimaPosizione = 0;
ultimaOraRegistrata = 255;
// Salva la firma e i valori iniziali
EEPROM.put(
EEPROM_FIRMA,
FIRMA_EEPROM
);
EEPROM.update(
EEPROM_NUMERO_DATI,
numeroDati
);
EEPROM.update(
EEPROM_PROSSIMA_POSIZIONE,
prossimaPosizione
);
EEPROM.update(
EEPROM_ULTIMA_ORA,
ultimaOraRegistrata
);
// Inizializza anche l'array in RAM
for (byte i = 0; i < NUMERO_REGISTRAZIONI; i++) {
storico[i].temperaturaDecimi = 0;
storico[i].umidita = 0;
storico[i].ora = 0;
}
}
void gestisciPulsante() {
// Con INPUT_PULLUP il pulsante premuto
// viene letto come LOW
if (digitalRead(PIN_PULSANTE) == LOW &&
millis() - ultimaPressione >= TEMPO_DEBOUNCE) {
ultimaPressione = millis();
// Dalla schermata principale passa al riepilogo
if (schermataAttuale == SCHERMATA_ATTUALE) {
schermataAttuale = SCHERMATA_RIEPILOGO;
}
// Dal riepilogo passa allo storico
else if (schermataAttuale ==
SCHERMATA_RIEPILOGO) {
schermataAttuale = SCHERMATA_STORICO;
indiceStoricoVisualizzato = 0;
}
// Nello storico mostra il campione successivo
else {
indiceStoricoVisualizzato++;
// Dopo l'ultima registrazione
// torna alla schermata principale
if (indiceStoricoVisualizzato >= numeroDati) {
indiceStoricoVisualizzato = 0;
schermataAttuale = SCHERMATA_ATTUALE;
}
}
mostraSchermata(rtc.now());
}
}
void mostraSchermata(const DateTime& adesso) {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
if (schermataAttuale == SCHERMATA_ATTUALE) {
mostraValoriAttuali(adesso);
} else if (
schermataAttuale == SCHERMATA_RIEPILOGO
) {
mostraRiepilogo();
} else {
mostraStorico();
}
// Mostra un avviso se l'ultima lettura
// del DHT22 non è valida
if (!sensoreValido) {
display.fillRect(
84,
54,
43,
10,
SSD1306_BLACK
);
display.setTextSize(1);
display.setCursor(87, 55);
display.print("DHT ERR");
}
display.display();
}
void mostraValoriAttuali(
const DateTime& adesso
) {
display.setTextSize(1);
// Titolo e ora corrente
display.setCursor(0, 0);
display.print("MONITOR AMBIENTE");
display.setCursor(92, 0);
stampaDueCifre(adesso.hour());
display.print(":");
stampaDueCifre(adesso.minute());
display.drawLine(
0,
10,
127,
10,
SSD1306_WHITE
);
// Temperatura attuale
display.setTextSize(2);
display.setCursor(0, 18);
display.print(temperatura, 1);
display.print((char)247);
display.print("C");
// Umidità attuale
display.setCursor(75, 18);
display.print(umidita, 0);
display.print("%");
display.setTextSize(1);
// Numero di registrazioni disponibili
display.setCursor(0, 45);
display.print("Storico: ");
display.print(numeroDati);
display.print("/");
display.print(NUMERO_REGISTRAZIONI);
display.setCursor(0, 56);
display.print("Premi: riepilogo");
}
void mostraRiepilogo() {
display.setTextSize(1);
display.setCursor(0, 0);
display.print("MINIMI E MASSIMI");
display.drawLine(
0,
10,
127,
10,
SSD1306_WHITE
);
// Se non esistono registrazioni,
// mostra un messaggio dedicato
if (numeroDati == 0) {
display.setCursor(14, 28);
display.print("Storico vuoto");
display.setCursor(9, 44);
display.print("Attendi una lettura");
return;
}
float tempMin;
float tempMax;
byte umidMin;
byte umidMax;
calcolaMinimiMassimi(
tempMin,
tempMax,
umidMin,
umidMax
);
display.setCursor(0, 16);
display.print("Temp min: ");
display.print(tempMin, 1);
display.print((char)247);
display.print("C");
display.setCursor(0, 28);
display.print("Temp max: ");
display.print(tempMax, 1);
display.print((char)247);
display.print("C");
display.setCursor(0, 42);
display.print("Umid min: ");
display.print(umidMin);
display.print("%");
display.setCursor(0, 54);
display.print("Umid max: ");
display.print(umidMax);
display.print("%");
}
void mostraStorico() {
display.setTextSize(1);
display.setCursor(0, 0);
display.print("STORICO ORARIO");
display.drawLine(
0,
10,
127,
10,
SSD1306_WHITE
);
if (numeroDati == 0) {
display.setCursor(14, 28);
display.print("Storico vuoto");
display.setCursor(8, 44);
display.print("Premi per tornare");
return;
}
// Recupera la registrazione in ordine cronologico,
// dalla più vecchia alla più recente
Registrazione dato =
leggiRegistrazioneCronologica(
indiceStoricoVisualizzato
);
display.setCursor(0, 16);
display.print("Campione ");
display.print(indiceStoricoVisualizzato + 1);
display.print("/");
display.print(numeroDati);
display.setCursor(0, 29);
display.print("Ora: ");
stampaDueCifre(dato.ora);
display.print(":00");
display.setCursor(0, 42);
display.print("Temp: ");
display.print(
dato.temperaturaDecimi / 10.0,
1
);
display.print((char)247);
display.print("C");
display.setCursor(0, 55);
display.print("Umid: ");
display.print(dato.umidita);
display.print("%");
}
Registrazione leggiRegistrazioneCronologica(
byte indice
) {
byte posizioneReale;
// Se il buffer non è ancora pieno,
// i dati iniziano dalla posizione zero
if (numeroDati < NUMERO_REGISTRAZIONI) {
posizioneReale = indice;
} else {
// Quando il buffer è pieno,
// la prossima posizione indica il dato più vecchio
posizioneReale =
(prossimaPosizione + indice) %
NUMERO_REGISTRAZIONI;
}
return storico[posizioneReale];
}
void calcolaMinimiMassimi(
float& tempMin,
float& tempMax,
byte& umidMin,
byte& umidMax
) {
// Utilizza la prima registrazione
// per inizializzare i valori
Registrazione primoDato =
leggiRegistrazioneCronologica(0);
tempMin =
primoDato.temperaturaDecimi / 10.0;
tempMax = tempMin;
umidMin = primoDato.umidita;
umidMax = primoDato.umidita;
// Analizza tutte le registrazioni successive
for (byte i = 1; i < numeroDati; i++) {
Registrazione dato =
leggiRegistrazioneCronologica(i);
float temperaturaDato =
dato.temperaturaDecimi / 10.0;
if (temperaturaDato < tempMin) {
tempMin = temperaturaDato;
}
if (temperaturaDato > tempMax) {
tempMax = temperaturaDato;
}
if (dato.umidita < umidMin) {
umidMin = dato.umidita;
}
if (dato.umidita > umidMax) {
umidMax = dato.umidita;
}
}
}
void stampaDueCifre(int valore) {
// Aggiunge lo zero iniziale ai valori
// inferiori a dieci
if (valore < 10) {
display.print("0");
}
display.print(valore);
}
void mostraAvvio() {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(17, 14);
display.print("Monitor ambiente");
display.setCursor(18, 30);
display.print("Storico 24 ore");
display.setCursor(28, 47);
display.print("Progetto 130");
display.display();
delay(1800);
}
void mostraErroreRtc() {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(27, 15);
display.print("ERRORE RTC");
display.setCursor(9, 31);
display.print("DS3231 non rilevato");
display.setCursor(15, 47);
display.print("Controlla i cavi");
display.display();
}
⚙️ Come funziona
Arduino legge temperatura e umidità dal sensore DHT22 ogni due secondi.
Le letture correnti vengono mostrate nella schermata principale.
Quando l’ora cambia, Arduino registra una nuova misurazione nello storico.
Per esempio:
👉 Se Arduino viene riavviato, lo storico viene recuperato dalla memoria EEPROM.
Arduino legge temperatura e umidità dal sensore DHT22 ogni due secondi.
Le letture correnti vengono mostrate nella schermata principale.
Quando l’ora cambia, Arduino registra una nuova misurazione nello storico.
Per esempio:
- alle 14:00 viene salvata una lettura;
- alle 15:00 viene salvata quella successiva;
- dopo 24 registrazioni lo storico contiene circa un giorno completo.
👉 Se Arduino viene riavviato, lo storico viene recuperato dalla memoria EEPROM.
🗂️ Struttura dello storico
Ogni registrazione viene conservata in una struttura:
struct Registrazione {
int16_t temperaturaDecimi;
byte umidita; byte ora;
};
La temperatura viene moltiplicata per 10.
Per esempio:
Ogni registrazione viene conservata in una struttura:
struct Registrazione {
int16_t temperaturaDecimi;
byte umidita; byte ora;
};
La temperatura viene moltiplicata per 10.
Per esempio:
- 23,4 °C viene memorizzata come 234;
- 18,7 °C viene memorizzata come 187.
🔄 Buffer circolare
Lo storico utilizza un buffer circolare di 24 elementi.
Quando lo spazio disponibile termina, il programma ricomincia dalla prima posizione e sostituisce la registrazione più vecchia.
In questo modo:
Lo storico utilizza un buffer circolare di 24 elementi.
Quando lo spazio disponibile termina, il programma ricomincia dalla prima posizione e sostituisce la registrazione più vecchia.
In questo modo:
- la memoria utilizzata rimane sempre la stessa;
- non è necessario cancellare manualmente i dati;
- vengono conservate sempre le registrazioni più recenti.
💾 Salvataggio nella EEPROM
La EEPROM mantiene i dati anche quando Arduino viene spento.
Il programma salva:
Una firma numerica permette di capire se la EEPROM contiene dati validi.
Se la firma non viene trovata, Arduino crea automaticamente uno storico vuoto.
La EEPROM mantiene i dati anche quando Arduino viene spento.
Il programma salva:
- numero delle registrazioni disponibili;
- posizione della prossima scrittura;
- ultima ora registrata;
- tutte le misurazioni dello storico.
Una firma numerica permette di capire se la EEPROM contiene dati validi.
Se la firma non viene trovata, Arduino crea automaticamente uno storico vuoto.
📟 Schermate disponibili
Il monitor utilizza tre modalità di visualizzazione.
Schermata principale
Mostra:
Schermata riepilogo
Mostra i valori minimi e massimi presenti nello storico:
Schermata storico
Mostra una registrazione alla volta:
Il monitor utilizza tre modalità di visualizzazione.
Schermata principale
Mostra:
- ora corrente;
- temperatura;
- umidità;
- numero di registrazioni memorizzate.
Schermata riepilogo
Mostra i valori minimi e massimi presenti nello storico:
- temperatura minima;
- temperatura massima;
- umidità minima;
- umidità massima.
Schermata storico
Mostra una registrazione alla volta:
- posizione nello storico;
- ora della lettura;
- temperatura;
- umidità.
⚠️ Gestione degli errori
Se il sensore DHT22 restituisce un valore non valido:
Se il modulo RTC non viene rilevato, il programma mostra un errore e interrompe il funzionamento.
Se il sensore DHT22 restituisce un valore non valido:
- gli ultimi dati corretti rimangono visibili;
- la lettura non viene salvata nello storico;
- sul display appare l’indicazione DHT ERR;
- Arduino prova nuovamente dopo due secondi.
Se il modulo RTC non viene rilevato, il programma mostra un errore e interrompe il funzionamento.
🧠 Perché è un progetto importante
Con questo progetto impari a:
Con questo progetto impari a:
- realizzare uno storico di misurazioni;
- utilizzare strutture personalizzate;
- creare un buffer circolare;
- salvare dati nella EEPROM;
- ridurre lo spazio occupato dai valori;
- utilizzare il modulo RTC DS3231;
- analizzare valori minimi e massimi;
- creare più schermate OLED;
- gestire errori senza perdere i dati;
- organizzare un progetto Arduino più complesso.
🔄 Varianti possibili
Vuoi migliorare il progetto?
Vuoi migliorare il progetto?
- 📊 aggiungi un grafico dello storico;
- 💾 utilizza una scheda microSD;
- 📅 salva anche giorno e mese;
- 🔘 aggiungi un secondo pulsante per tornare indietro;
- 🗑️ crea una funzione per cancellare lo storico;
- 🔔 aggiungi allarmi per temperatura e umidità;
- 🌬️ controlla automaticamente una ventola;
- 💨 attiva un umidificatore;
- 📶 invia i dati tramite Wi-Fi;
- 📟 utilizza un display OLED più grande;
- 🕒 registra un campione ogni 30 minuti;
- 🔋 aggiungi un indicatore della batteria.