Guest post created with ♥ by Eugenio 🙂|
Introduzione
Questa guida introduttiva parlerà di come funziona LineaMeteoStazione, un po’ più tecnicamente.
Innanzitutto, come mi è venuta in mente l’idea di creare questo tipo di Stazione Meteo? L’idea è nata principalmente dalla mia passione per l’osservazione dei fenomeni meteorologici e dell’ambiente in generale. Il fatto è che non mi piaceva avere solo una stazione meteorologica del mercato, dove di solito non sai che tipo di sensori usano o soprattutto per quanto riguarda lo schermo solare che spesso è fatto in un modo che tu debba solo sostituirlo perché le prestazioni non sono sufficienti. È qui che ho iniziato a trovare un modo per costruire la mia stazione meteorologica che può utilizzare molti sensori e pannelli solari diversi ed è tutto gestibile dagli utenti senza riprogrammare l’unità!
Microcontrollore ESP32 e Attiny85
La prima scelta da fare è stata quella relativa al microcontrollore per raccogliere i dati. Ho deciso di utilizzare l’ESP32, poiché ero interessato alla funzionalità WiFi e alla velocità della CPU, nel caso ne avessi avuto bisogno. Ho deciso di utilizzare la scheda di sviluppo EZSBC perché ha a bordo un monitoraggio accurato del SOC della batteria utilizzando l’IC LC709203F e comunicando tramite I2C è possibile ottenere il voltaggio e la percentuale della cella.
La scheda utilizza solo 12uA durante il deep sleep, il che rende la scheda un’ottima soluzione per sviluppare un progetto e ridurre il consumo energetico. Poiché l’anemometro e il pluviometro necessitano di un monitoraggio costante per essere precisi e l’ESP32 deve entrare in modalità Deep Sleep per risparmiare energia, avevo bisogno di trovare un modo per monitorare costantemente il pluviometro e l’anemometro ma allo stesso tempo ridurre il consumo energetico.
Mentre c’è anche un’opzione per programmare tutto usando il processore ULP di ESP32, alla fine scelgo di usare Attiny85 come soluzione al mio problema. L’ESP32 gestirà l’invio dei dati tramite WiFi e l’Attiny85 raccoglierà i dati dal pluviometro e dall’anemometro e quindi rispedirà l’ESP32 quando richiede i dati.
Ecco lo schema e il PCB di tutti i componenti, inclusa la scheda luminosa, che è quella a cui sono collegati il sensore Lux e UV QUI SU GITHUB I FILE PER EAGLE
Stazione Meteo Autonoma con Caricatore Solare e Alimentazione a Batteria
La principale fonte di energia della stazione meteorologica è una batteria al litio 18650 e il sole con l’uso di un pannello solare le cui dimensioni dipendono dalla località in cui vivi come spiegato nell’articolo generale relativo alla stazione meteorologica. Ho usato una scheda di ricarica solare per ricaricare in sicurezza la batteria.
È anche possibile utilizzare una scheda come questa per ricaricare la batteria tramite la porta USB della scheda di sviluppo.
Raccolta dati: ESP32, Attiny85 e sensori
Per raccogliere i dati ora esamineremo il codice utilizzato e come funziona il codice. Prima di tutto, devi avere l’ IDE Arduino installato almeno la versione 1.8.13 e dovrai installare dal gestore della scheda di Arduino l’ESP32 seguendo questi passaggi:
Per installare la scheda ESP32 nel tuo IDE Arduino, segui queste istruzioni seguenti:
- Nel tuo IDE Arduino, vai su File > Preferenze
- Immettere https://dl.espressif.com/dl/package_esp32_index.json nel campo “Ulteriori URL di gestione schede”. Quindi, fare clic sul pulsante “OK”:
Nota: se si dispone già dell’URL delle schede ESP8266, è possibile separare gli URL con una virgola come segue: https://dl.espressif.com/dl/package_esp32_index.json, http:/ /arduino.esp8266.com/stable/package_esp8266com_index.json - Apri Gestione schede. Vai su Strumenti > Bacheca > Gestore bacheche…
- Cerca ESP32 e premi il pulsante di installazione per “ESP32 di Espressif Systems” e attendi il completamento dell’installazione.
Puoi trovare maggiori informazioni su questo articolo “Esp32: piedinatura, specifiche e configurazione dell’Arduino IDE”.
Una volta installato, puoi programmare ESP32 con questo codice .
Il codice funziona includendo prima le librerie che useremo:
///////////////////////////////LIBRARY DECLARATION////////////////////////////////////////
#include <Arduino.h>
#include <HTTPUpdate.h>
#include <WiFi.h>
#include <FirebaseESP32.h> //https://github.com/mobizt/Firebase-ESP32
#include "ClosedCube_SHT31D.h" //https://github.com/closedcube/ClosedCube_SHT31D_Arduino
#include "Adafruit_VEML6075.h" //https://github.com/adafruit/Adafruit_VEML6075
#include "DFRobot_SHT20.h" //https://github.com/DFRobot/DFRobot_SHT20
#include "LC709203F.h" //https://github.com/EzSBC/ESP32_Bat_Pro/blob/main/LC709203F.zip
#include "Max44009.h" //https://github.com/RobTillaart/Max44009
#include <WiFiManager.h> // https://github.com/tzapu/WiFiManager
#include <Wire.h>
WiFiManager wifiManager;
#include <WebServer.h>
#include <ESPmDNS.h>
#include <HTTPUpdateServer.h>
#include <WiFiClient.h>
Tutte queste librerie possono essere trovate nel gestore delle librerie dell’IDE di Arduino cercandole o seguendo il link sul codice, ma alcune di esse sono già preinstallate se hai installato il modulo ESP.
Creiamo tutte le dichiarazioni e le variabili che ci servono per far funzionare il codice e dobbiamo anche inserire i dettagli di Firebase che sono quelli che hai ricavato dall’articolo che parla della Stazione Meteo. Puoi anche inserire i dettagli della tua unità Ota se desideri utilizzare la funzione remota dell’unità Ota che è ampiamente spiegata QUI . Avrai bisogno dell’API. Quando è necessario un aggiornamento, sarà necessario modificare il numero di versione.
//OTA REMOTE//
// To inject firmware info into binary file, You have to use following macro according to let
// OTAdrive to detect binary info automatically
#define ProductKey "" // Replace with your own APIkey
#define Version "1.0.1.1"
#define MakeFirmwareInfo(k, v) "&_FirmwareInfo&k=" k "&v=" v "&FirmwareInfo_&"
void update();
//OTA LOCAL//
WebServer httpServer(80);
HTTPUpdateServer httpUpdater;
const char* host = "esp32-webupdate";
String DEVICE1OTA;
unsigned long TIMEROTA;
//--------------------------------------------------------------------------------------//
///BATTERY OBJECT//
LC709203F gg; // create a gas gauge object.
//DECLARATION SENSORS//
ClosedCube_SHT31D sht3xd;
DFRobot_SHT20 sht20;
//LIGHT SENSOR DEFINE//
Adafruit_VEML6075 uv = Adafruit_VEML6075();
Max44009 myLux(0x4A);
//I2C COMUNICATION ADDRESS//
const int I2CSlaveAddress = 8; // I2C Address.
////////////////////*********FIREBASE DETAILS************///////////////////////////////////
#define FIREBASE_HOST "" // the project name address from firebase id
#define FIREBASE_AUTH "" // the secret key generated from firebase
FirebaseData Weather;
//WIND DIRECTION//
#define WindDirectionDavis 32 // anemometer
#define WindDirectionMisol 33 // anemometer
////////////RAIN////////////////
byte PluvioFlag = 0; // detect interrupt of rain
byte rainrateI2C = 0;
byte rainrateMaxI2C = 0;
byte rainrateMaxI2CIntensity = 0;
////////////WIND/////////////////
int WindDirectionDavisValue;
int WindDirectionMisolValue;
byte Rotations;
byte GustRotations;
/////TIMING VARIALABLES//////
unsigned long timeout; // waiting for WiFi connection
bool res; // WiFi Status
unsigned int sampletime;
byte ReadByte = 0;
byte WiFiReset = 0;
//////////TEMPERATURE///////////
//SHT2X//
float tempSHT2x; // temperature in centigrade
float temp_f; // temperature in fahrenheit
int humiditySHT2x; // humidity variable
//SHT3X//
float temperatureSHT3x; // temperature in centigrade
int humiditySHT3x; // humidity variable
//////////////UV/////////////////
float UVindex; // variable UVindex
byte UVDETECT = 0;
Il codice funziona in modo molto semplice. Il codice prima verifica se è disponibile una connessione WiFi e se non è memorizzato sul dispositivo entra in modalità AP grazie alla libreria del gestore WiFi. Dopo essere entrato in modalità AP rimane per 5 minuti in attesa di un setup altrimenti si riavvierà. Si inizia quindi inizializzando i sensori. Questo avviene quando inizializziamo i sensori utilizzando il codice di base della libreria. Impostiamo anche l’IC che monitora lo stato della batteria.
void InitSensors()
{
sht20.initSHT20(); // Init SHT20 Sensor
delay(100);
sht20.checkSHT20();
sht3xd.begin(0x44);
sht3xd.readSerialNumber();
if (!gg.begin()) {
while (1) delay(10);
}
gg.setCellCapacity(LC709203F_APA_3000MAH);
//gg.setAlarmVoltage(3.4);
gg.setCellProfile( LC709203_NOM3p7_Charge4p2 ) ;
if (UVDETECT == 0)
{
if (! uv.begin()) {
UVDETECT = 1;
}
}
if (UVDETECT == 0)
{
uv.setIntegrationTime(VEML6075_100MS);
uv.setHighDynamic(false);
uv.setForcedMode(false);
uv.setCoefficients(2.22, 1.17, // UVA_A and UVA_B coefficients
2.95, 1.58, // UVB_C and UVB_D coefficients
0.004770, 0.006135); // UVA and UVB responses
}
myLux.setAutomaticMode();
}
Quindi nella funzione initData() come mostrato nel codice completo controlliamo se dobbiamo resettare il WiFi o se ci sono aggiornamenti OTA e leggiamo i byte provenienti dall’Attiny85. Verifichiamo anche il tempo di campionamento da impostare quando i dati vengono inviati al server. Il controllo da Firebase è molto semplice e può essere eseguito in un codice di esempio come di seguito
if (Firebase.getInt(Weather, "/Connection/DEVICE1/ResetWiFi"))
{
WiFiReset = Weather.to<int>();
}
- Weather è l’oggetto di Firebase che abbiamo creato all’inizio del codice.
- ‘getInt’ è il tipo di variabile che otterremo.
Ed è così che leggiamo il byte proveniente dall’Attiny85 con la funzione dichiarata nel codice come ‘byte readTiny’
PluvioFlag = readTiny(I2CSlaveAddress);
Nel loop, leggiamo i sensori e aggiorniamo i dati al Firebase usando come esempio questa riga di codice:
Firebase.setInt(Weather, "/Time/Communication/PluvioFlag", PluvioFlag);
Programmare Attiny85 per la raccolta dei dati dell’anemometro e del pluviometro
Per programmare Attiny85 possiamo utilizzare un Arduino e un Arduino IDE come spiegato in questo articolo o in “Scheda di programmazione ATtiny”. Il codice per Attiny è qui sotto o lo trovate anche QUI. L’Attiny è programmato per monitorare l’interrupt proveniente dal pluviometro e dall’anemometro e per inviare i dati all’ESP32 una volta richiesto. PIOGGIA e VENTO sono i 2 cicli ISR. Invia le Rotazioni e le rotazioni massime rilevate nell’ultimo periodo prima di trasmettere all’ESP32. Calcola anche il tasso di pioggia in base alla temporizzazione passata tra i 2 impulsi e invia i dati di pioggia, tasso di pioggia e tasso di pioggia max all’ESP32. (Vedi funzione PluvioDataEngine())
Per ridurre i consumi Attiny rimane in modalità Idle con la funzione sleepNow() ed è programmato alla frequenza di clock di 1MHz.
#include <TinyWireS.h> // Requires fork by Rambo with onRequest support
//#include <avr/wdt.h> // watchdog
#include <avr/sleep.h>
#include "PinChangeInterrupt.h"
// Requires headers for AVR defines and ISR function
//#include <avr/io.h>
//#include <avr/interrupt.h>
// Choose a valid PinChangeInterrupt pin of your Arduino board
#define INT_PINRAIN PB4
#define INT_PINANEMOMETER PB1
const int I2CSlaveAddress = 8; // I2C Address.
byte ReadByte = 0;
////////////RAIN////////////////
volatile byte PluvioFlag = 0; // detect interrupt of rain
volatile byte RainFlag = 0; // detect interrupt of rain
volatile unsigned long ContactBounceTimeRain = 0; // Timer to avoid double counting in interrupt
float mmGoccia = 0.3; // tipping bucket count in mm
unsigned int gocce = 0; // tipping bucket movements
volatile unsigned long time1; // rain rate timing calculation
unsigned long PluvioStep = 0; // rain rate timing calculation
unsigned long PluvioOldStep = 0; // rain rate timing calculation
float rainrate = 0; // real-time rainrate
float rainrateMax = 0;
byte rainrateI2C = 0;
byte rainrateMaxI2C = 0;
byte rainrateMaxI2CIntensity = 0;
////////////WIND/////////////////
volatile byte Rotations; // cup rotation counter used in interrupt routine
volatile unsigned long ContactBounceTime = 0;
unsigned long calculation = 0; //millis() for sample time
unsigned int average = 3000; // sample time for wind speed
byte Gust; // gust variable
void setup() {
sleepNow();
//wdt_enable(WDTO_8S); // Watchdog
//pinMode(LED_PIN, OUTPUT);
//cli(); // Disable interrupts during setup
//PCMSK |= (1 << INTERRUPT_PINRAIN); // Enable interrupt handler (ISR) for our chosen interrupt pin (PCINT1/PB1/pin 6)
//GIMSK |= (1 << PCIE); // Enable PCINT interrupt in the general interrupt mask
pinMode(INT_PINRAIN, INPUT); // Set our interrupt pin as input with a pullup to keep it stable
pinMode(INT_PINANEMOMETER, INPUT); // Set our interrupt pin as input with a pullup to keep it stable
//sei(); //last line of setup - enable interrupts after setup
attachPCINT(digitalPinToPCINT(INT_PINRAIN), RAIN, FALLING);
attachPCINT(digitalPinToPCINT(INT_PINANEMOMETER), WIND, FALLING);
TinyWireS.begin(I2CSlaveAddress); // Begin I2C Communication
TinyWireS.onRequest(transmit); // When requested, call function transmit()
}
void loop() {
sleepNow();
if (millis() - calculation >= average)
{
calculation = millis();
Rotations = 0;
}
if (Rotations > Gust)
{
Gust = Rotations;
}
if (RainFlag == 1) {
RainFlag = 0;
PluvioOldStep = PluvioStep;
PluvioStep = time1;
gocce++; // incrementa numero basculate
}
PluvioDataEngine();
//wdt_reset(); // feed the watchdog
//delay(10);
}
//-------------------------------------------------------------------
void transmit() {
switch (ReadByte) {
case 0:
TinyWireS.send(PluvioFlag);
PluvioFlag = 0;
ReadByte = 1;
break;
case 1:
TinyWireS.send(Rotations);
ReadByte = 2;
break;
case 2:
TinyWireS.send(Gust);
Gust = 0;
ReadByte = 3;
break;
case 3:
TinyWireS.send(rainrateI2C);
ReadByte = 4;
break;
case 4:
TinyWireS.send(rainrateMaxI2C);
rainrateMaxI2C = 0;
ReadByte = 5;
break;
case 5:
TinyWireS.send(rainrateMaxI2CIntensity);
rainrateMaxI2CIntensity = 0;
ReadByte = 0;
break;
}
}
void RAIN()
{
if ((millis() - ContactBounceTimeRain) > 250 ) { // debounce the switch contact.
PluvioFlag++;
RainFlag = 1;
ContactBounceTimeRain = millis();
time1 = millis();
}
}
void WIND()
{
if ((millis() - ContactBounceTime) > 15 ) { // debounce the switch contact.
Rotations++;
ContactBounceTime = millis();
}
}
void PluvioDataEngine() {
if (((PluvioStep - PluvioOldStep) != 0) && (gocce >= 2)) {
if ((millis() - PluvioStep) > (PluvioStep - PluvioOldStep)) {
rainrate = 3600 / (((millis() - PluvioStep) / 1000)) * mmGoccia;
rainrateMax = rainrate / 10;
if (rainrate < 1) {
gocce = 0;
rainrate = 0;
}
} else {
rainrate = 3600 / (((PluvioStep - PluvioOldStep) / 1000)) * mmGoccia;
rainrateMax = rainrate / 10;
}
} else {
rainrate = 0.0;
}
if (rainrate > 255)
{
rainrateI2C = 255;
}
else
{
rainrateI2C = (byte)rainrate;
}
if (rainrateI2C > rainrateMaxI2C)
{
rainrateMaxI2C = rainrateI2C;
}
if (rainrateMax > rainrateMaxI2CIntensity)
{
rainrateMaxI2CIntensity = (byte)rainrateMax;
}
}
void sleepNow() // here we put the arduino to sleep
{
/* Now is the time to set the sleep mode. In the Atmega8 datasheet
http://www.atmel.com/dyn/resources/prod_documents/doc2486.pdf on page 35
there is a list of sleep modes which explains which clocks and
wake up sources are available in which sleep modus.
In the avr/sleep.h file, the call names of these sleep modus are to be found:
The 5 different modes are:
SLEEP_MODE_IDLE -the least power savings
SLEEP_MODE_ADC
SLEEP_MODE_PWR_SAVE
SLEEP_MODE_STANDBY
SLEEP_MODE_PWR_DOWN -the most power savings
For now, we want as much power savings as possible, so we
choose the according
sleep modus: SLEEP_MODE_PWR_DOWN
Timer 2 overflow interrupt is only able to wake up the ATmega in PWR_SAVE
*/
// disable ADC
ADCSRA = 0;
set_sleep_mode(SLEEP_MODE_IDLE); // sleep mode is set here
sleep_enable(); // enables the sleep bit in the mcucr register
// so sleep is possible. just a safety pin
sleep_mode(); // here the device is actually put to sleep!!
// THE PROGRAM CONTINUES FROM HERE AFTER WAKING UP
sleep_disable(); // first thing after waking from sleep:
// disable sleep...
}
Links
- LineaMeteoStazione: Guida Tecnica Dispositivo Master, Invio e Raccolta Dati
- LineaMeteoStazione: Guida Tecnica Display
- LineaMeteoStazione: Guida Tecnica Ricevitore, rete e dispositivo di gestione
- LineaMeteoStazione: La Stazione Meteo Personalizzabile con ESP32, ESP8266, Attiny85 e aggiornamenti OTA
Per la stazione metereologica pre-assemblata o l’origine dei materiale per favore scrivimi Eugenio
eugenioiaquinta@outlook.it