Stazione meteo con ESP32 che si blocca dopo un po' di tempo

Salve a tutti, ho costruito una stazione meteorologica con esp32, sensori BMP 280 per la pressione (e come temp secondaria) e dht22 per la temperatura e l'umidità. Il microcontrollore invia dati ogni ora ad un BOT telegram e inoltre posso richiamare i dati quando mi pare e piace con la dicitura /readings. Chiedo scusa se lo sketch sottostante possa non essere il massimo della scrittura, ma nel mio caso funziona egregiamente, e fa ciò che mi interessa.
Ho solo un problema, ossia ogni tot di tempo si blocca il microcontrollore e non invia più i dati e non risponde ai /readings e devo riavviarlo manualmente. Ho capito subito che il problema possono essere le variabili String che infatti vanno ad intasare la memoria RAM. Nello sketch è inserita anche una funzione per resettare la esp32, e di conseguenza i dati (temp e Ur min e max), che si attiva ogni 24 ore; però purtroppo il microcontrollore si blocca prima che passino appunto queste 24h. Ho letto che il problema delle String potrebbe essere ovviato grazie alla libreria PROGMEM che mette i contenuti delle String nella flash rom, ma nel mio caso non saprei proprio come utilizzarla, dato che ci sono tante variabili String. Qualcuno mi potrebbe aiutare a capire come ovviare a questo problema?

PS. tengo a precisare che nello sketch sono inserite due librerie per il bot di telegram, la Universal telegram bot che era quella che ho utilizzato inizialmente, e poi successivamente ho integrato la CTbot.h che è quella che fortunatamente mi permette di inviare in automatico i dati ogni ora. Questa automazione con la Universal telegram bot funzionava ma relativamente: i dati venivano inviati in automatico solo nel momento in cui facevo almeno una lettura con /readings mentre la libreria CTBot invia in automatico i dati senza che ci sia stato almeno un messaggio /readings al momento dell'avvio.

Ecco lo sketch:

#include <WiFi.h>

#include "CTBot.h" //////////////////Libreria per farmi mandare i dati in automatico
CTBot myBot;

#include <WiFiClientSecure.h>
#include <UniversalTelegramBot.h> // Universal Telegram Bot per ricevere tramite /readings i dati
#include <ArduinoJson.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <DHT.h>

// DHT Sensor Library
#include <Adafruit_BMP280.h>
#define DHTTYPE  DHT22           // Type of DHT Sensor DHT22 (AM2302)
#define DHTPIN 25                // Digital pin connected to the DHT sensor
DHT dht(DHTPIN, DHTTYPE);        // creating dht Instance


//////////////////////Variabili per invio messaggi su telegram/////////////////////
String chat_id;
String readings;
int i; //////////Riguarda il FOR contenuto nel void handleNewMessages
String message;

/////////////////temp e UR min e max sensore dht22////////////////////////////////
float temp = 0; //raccoglie misura temperatura
float um = 0; //raccoglie misura umidità
float temp_min = 0; //si ricorda della temperatura min. registrata
float um_min = 0; //si ricorda dell’umidità min. registrata
float temp_max = 0; //si ricorda della temperatura max. registrata
float um_max = 0; //si ricorda dell’umidità min. registrata
float v1, v2; //variabili iniziali per um_min e temp_min

/////////////////temp min e max sensore bmp280////////////////////////////////
float temp2 = 0; //raccoglie misura temperatura
float temp2_min = 0; //si ricorda della temperatura min. registrata
float temp2_max = 0; //si ricorda della temperatura max. registrata
float v4; //variabili iniziali per um_min e temp_min


/////////////TIME//////////////////////////////

#include <NTPClient.h>
#include <WiFiUdp.h>

WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org");
//Week Days
String weekDays[7] = {"Domenica", "Lunedì", "Martedì", "Mercoledì", "Giovedì", "Venerdì", "Sabato"};

//Month names
String months[12] = {"Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno", "Luglio", "Agosto", "Settembre", "Ottobre", "Novembre", "Dicembre"};

////Variabili per il TIME///////////////
int monthDay;
int currentMonth;
int currentYear;
String currentMonthName;
String weekDay;
int currentHour;
int currentMinute;
String formattedTime;

// BMP280 connect to ESP32 I2C (GPIO 21 = SDA, GPIO 22 = SCL)
Adafruit_BMP280 bmp;             // Create bmp instance

// Replace with your network credentials
const char* ssid = "XXXXXXXXXXXXXX";
const char* password = "XXXXXXXXXXXXX";

// Use @myidbot to find out the chat ID of an individual or a group
// Also note that you need to click "start" on a bot before it can
// message you
int64_t CHAT_IDD3 = XXXXXXXXXXXX; ////////////USATO PER MESSAGGIO AUTOMATICO CON LIBRERIA CTBBOT.H

#define CHAT_ID "XXXXXXXXXX"
#define CHAT_ID2 "XXXXXXXXXX" // Your Chat ID
#define CHAT_ID3 "XXXXXXXXXXX"

// Initialize Telegram BOT
#define BOTtoken "XXXXXXXXXXXXXXXXXXXXXXXXXXX"  // your Bot Token (Got from Botfather)

WiFiClientSecure client;
UniversalTelegramBot bot(BOTtoken, client);

//Checks for new messages every 1 second.
int botRequestDelay = 1000;
unsigned long lastTimeBotRan;


// Get BMP280 & DHT22 sensor readings and return them as a String variable
String getReadings() {
  float temperature, temperature2,humidity, pressureR, pressure2, Altitude,pressure,pressureA, pressure2plus, heatIndex, Dew_Point, Wet_Bulb, Dew_PointC, realealtitude, pressurelvm;
  String Mese, Giorno, Anno; String giornosettimana; long minuti; int ora; String orario;
  
  temperature = dht.readTemperature();
  temperature2 = bmp.readTemperature();
  humidity = dht.readHumidity();
  pressure = bmp.readPressure();
  pressureA = (bmp.readPressure() / 100);
  pressure2 = (bmp.readPressure() / 100)+ 4.41;////////toglie  9 hpa ogni 100m (9 x ettometri di altitudine)
  pressureR =  (bmp.readPressure() / pow((1 - (0.0065 * 49 / (dht.readTemperature() + 0.0065 * 49 + 273.15))), 5.25588)) /100;/////hypsometric equation
  Altitude = bmp.readAltitude();
  heatIndex = dht.computeHeatIndex(temperature, humidity, false);
  Dew_Point = (log(humidity / 100) + ((17.27 * temperature) / (237.3 + temperature))) / 17.27;
  Dew_PointC = (237.3 * Dew_Point) / (1 - Dew_Point);
  Wet_Bulb =  temperature * (0.45 + 0.006 * humidity * (pressureA / 1060));
  realealtitude = (44330.0 * (1.0 - pow((pressureA / pressureR), 0.19029495)));
  Mese = currentMonthName;
  Giorno = monthDay;
  Anno = currentYear;
  giornosettimana = weekDay;
  ora = currentHour;
  minuti = currentMinute;
  orario = formattedTime;
 
  message = "Ciao! \n";
  message += "Oggi è: " + String(giornosettimana) + ", " + String(Giorno) + " " + String(Mese) + " " + String(Anno) + " e sono le " + String(orario) + "\n\n";
  message += "I dati meteo attuali sono: \n\n";
  message += "Temperatura: " + String(temperature) + " ºC \n\n";
  message += "Temp min: " + String (temp_min) + " ºC \n\n";
  message += "Temp max: " + String (temp_max) + " ºC \n\n";
  message += "Temperatura sensore 2: " + String(temperature2) + " ºC \n\n";
  message += "Temp min 2: " + String (temp2_min) + " ºC \n\n";
  message += "Temp max 2: " + String (temp2_max) + " ºC \n\n";
  message += "Umidità: " + String (humidity) + " % \n\n";
  message += "UR min: " + String (um_min) + " % \n\n";
  message += "UR max: " + String (um_max) + " % \n\n";
  message += "Pressione a lvm: " + String (pressureR) + " hpa (equazione ipsometrica) \n\n";
  message += "Pressione a lvm 2ª equazione: " + String (pressure2) + " hPa (equazione che riduce 1 hpa ogni 8m di alt.) \n\n";
  message += "Pressione all'altitudine: " + String (pressureA) + " hPa \n\n";
  message += "Altitudine reale: " + String (realealtitude) + " m slm \n\n";
  message += "Altitudine in base hPa: " + String (Altitude) + " m slm \n\n";
  message += "Indice di calore: " + String (heatIndex) + " ºC \n\n";
  message += "Dew Point: " + String (Dew_PointC) + " ºC \n\n";
  message += "Wet Bulb: " + String (Wet_Bulb) + " ºC \n\n";
 
  return message;
}


//Handle what happens when you receive new messages
void handleNewMessages(int numNewMessages) {
  Serial.println("handleNewMessages");
  Serial.println(String(numNewMessages));

////////////la variabile Int i è nel globale////////////////7
  for (i = 0; i < numNewMessages; i++) {
    // Chat id of the requester
    chat_id = String(bot.messages[i].chat_id);
    if (chat_id != CHAT_ID && chat_id != CHAT_ID2 && chat_id != CHAT_ID3) {
      bot.sendMessage(chat_id, "Unauthorized user", "");
      continue;
    }

    // Print the received message
    String text = bot.messages[i].text;
    Serial.println(text);

    String from_name = bot.messages[i].from_name;

    if (text == "/start") {
      String welcome = "Benvenuto nella tua stazione meteo, " + from_name + ".\n\n";
      welcome += "Utilizza il seguente comando per verificare i dati in tempo reale.\n\n";
      welcome += "/readings \n\n";
      welcome += "Ultima foto dalla tua webcam.\n\n";
      welcome += "/photo \n\n";

      bot.sendMessage(chat_id, welcome, "");
    }

    if (text == "/photo") {
      
      bot.sendPhoto(chat_id, "http://saldinapoli.altervista.org/webcam.php"); 
    }

    if (text == "/readings") {
      readings = getReadings();
      bot.sendMessage(chat_id, readings, "");
    }
  }
}


void setup() {
  Serial.begin(115200);
  
  // Init BMP280 sensor
  if (!bmp.begin(0x76)) {
    Serial.println("Could not find a valid BMP280 sensor, check wiring!");
    while (1);
  }

  // Init DHT22 sensor
  dht.begin();
  float t = dht.readTemperature();
  
  // Connect to Wi-Fi

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  client.setCACert(TELEGRAM_CERTIFICATE_ROOT);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi..");
  }
  // Print ESP32 Local IP Address
  Serial.println(WiFi.localIP());
  delay(5000);
  
  myBot.wifiConnect(ssid, password); //////////////per la liberia CTbot.h////////////////////////
  myBot.setTelegramToken (BOTtoken);


  timeClient.begin();
  // Set offset time in seconds to adjust for your timezone, for example:
  // GMT +1 = 3600
  // GMT +8 = 28800
  // GMT -1 = -3600
  // GMT 0 = 0
  timeClient.setTimeOffset(7200);
}
void(* resetFunc) (void) = 0; //declare reset function at address 0 - MUST BE ABOVE LOOP ---> PER RESETTARE 

void loop() {

  if(mytimer3(0)){ //////MESSAGGIO AUTOMATICO CHAT_IDD3
  readings = getReadings();
   myBot.sendMessage(CHAT_IDD3, readings, ""); /////////INVIO MESSAGGIO AUTOMATICO CTbot.h
  
  }


  if ( millis()  >= 86400000) resetFunc(); //chiama reset ogni 24 hours (1 Day).
  
  if (millis() > lastTimeBotRan + botRequestDelay)  {
    int numNewMessages = bot.getUpdates(bot.last_message_received + 1);

    while (numNewMessages) {
      Serial.println("got response");
      handleNewMessages(numNewMessages);
      numNewMessages = bot.getUpdates(bot.last_message_received + 1);
    }
    lastTimeBotRan = millis();
  }

/////////FUNZIONI PER TEMP E UMIDITA' MIN E MAX///////////////
  temp = dht.readTemperature();
  um = dht.readHumidity();
  temp2 = bmp.readTemperature();
 
if (v1 == 0) { um_min = um;}
if (v2 == 0) { temp_min = temp;}
if (temp > temp_max) { temp_max = temp;}
if (um > um_max) { um_max = um; }
if (um_min >= um) { um_min = um; v1++; }
if (temp_min >= temp) { temp_min = temp; v2++; }

if (v4 == 0) { temp2_min = temp2;}
if (temp2 > temp2_max) { temp2_max = temp2;}
if (temp2_min >= temp2) { temp2_min = temp2; v4++; }


  Serial.print("Current humidity = ");
  Serial.print(dht.readHumidity());
  Serial.print("%  ");
  Serial.print("temperature = ");
  Serial.print(dht.readTemperature());
  Serial.println("C  ");
  delay(1000);

  //////////TIME//////////////////
  timeClient.update();

  unsigned long epochTime = timeClient.getEpochTime();
  Serial.print("Epoch Time: ");
  Serial.println(epochTime);

  formattedTime = timeClient.getFormattedTime();
  Serial.print("Formatted Time: ");
  Serial.println(formattedTime);

  currentHour = timeClient.getHours();
  Serial.print("Hour: ");
  if (timeClient.getHours() < 10) Serial.print("0");
  Serial.println(currentHour);

  currentMinute = timeClient.getMinutes();
  Serial.print("Minutes: ");
  if (timeClient.getMinutes() < 10) Serial.print("0");
  Serial.println(currentMinute);

  int currentSecond = timeClient.getSeconds();
  Serial.print("Seconds: ");
  Serial.println(currentSecond);

  weekDay = weekDays[timeClient.getDay()];
  Serial.print("Week Day: ");
  Serial.println(weekDay);

  //Get a time structure
  struct tm *ptm = gmtime ((time_t *)&epochTime);

  monthDay = ptm->tm_mday;
  Serial.print("Month day: ");
  Serial.println(monthDay);

  currentMonth = ptm->tm_mon + 1;
  Serial.print("Month: ");
  Serial.println(currentMonth);

  currentMonthName = months[currentMonth - 1];
  Serial.print("Month name: ");
  Serial.println(currentMonthName);

  currentYear = ptm->tm_year + 1900;
  Serial.print("Year: ");
  Serial.println(currentYear);

  //Print complete date:
  String currentDate = String(currentYear) + "-" + String(currentMonth) + "-" + String(monthDay);
  Serial.print("Current date: ");
  Serial.println(currentDate);

  Serial.println("");

  delay(2000);
}

  int mytimer3 (int timer13){ ///////////TIMER MILLIS PER INVIO DATI AUTOMATICAMENTE OGNI ORA////////
  static long t23, dt23;
  int ret3 = 0; 
  dt23 = millis() - t23;
  if(dt23 >= 3600000){
    t23 = millis();
    ret3 =1;
  }
  return ret3;
}

ciao, se dici che non arriva in tempo alla funzione di reset puoi provare ad abbassare il valore da 86400000 a 43200000 così lo resetti ogni 12 ore.
Inoltre se lasci collegato il pc con la seriale attiva dovrebbe dirti se va in crash e come mai.

Ciao, grazie per il tuo intervento.
Purtroppo il reset ogni 12 ore non vorrei farlo anche perchè come vedi nello sketch ci sono variabili che mi danno di temperatura e umidità le minime e le massime, e resettando a 12 ore queste si azzerano, e quindi avrei problemi a stabilire quali siano stati i valori min e max della giornata. Per il fatto di tenerlo collegato al serial per capire cosa succede, ho già provato, ma semplicemente accade che non dà più dati (avevo messo un serial print per la temperatua per capire quando si sarebbe fermato). Cioè il sistema si blocca completamente. Mi sono informato online e purtroppo ciò accade quando la RAM si riempie per colpa delle String. Ci sono modi per posizionare queste nella memoria flash, e richiamarle nella RAM solo quando serve, e si fa o con la funzione PROGMEM e o con la funzione F ; però per quanto riguarda il PROGMEM questo è utile nei casi in cui la stringa contiene "una frase" costante. E per quanto riguarda la funzione "F" ho visto che questa viene utilizzata nei serial print del tipo: Serial.print(F("HELLO WORLD!));

Il mio sketch contiene delle String che purtroppo non sono tutte costanti ma contengono a loro volta delle variabili ne loro interno, ad esempio

message += "Oggi è: " + String(giornosettimana) + ", " + String(Giorno) + " " + String(Mese) + " " + String(Anno) + " e sono le " + String(orario) + "\n\n";

Quindi non saprei cosa fare in casi come questo....

si capisco il problema....
Con le stringhe hai la frammentazione dell'heap che può generarti il blocco.
In un mio progetto con un d1 mini e Blink avevo notato che le stringhe troppo lunghe mi davano parecchi problemi.
Hai provato a dividere tutto in messaggi più corti?

Puoi anche dichiarare le stringhe come array di caratteri di dimensione fissa.
Ad esempio per i mesi: prendi il mese con più lettere e quella sarà la dimensione dell'array.

Sì, magari provo così, grazie mille. Vorrei capire solo una cosa, la RAM che viene occupata dalle stringhe, viene occupata di volta in volta che questa stringa viene scritta ad esempio nel serial monitor ? Cioè mettiamo caso che io non stia utilizzando il bot telegram, ma stia utilizzando i SerialPrint per leggere i valori, e imposto che il print di questi valori avvenga ogni 5 secondi: è la somma di questi print ogni 5 secondi ad occupare la RAM?

@tonynapoli2309 : c'è un intero thread che spiega la problematica della classe String e offre delle soluzioni ... prova a leggerlo !

Guglielmo

Grazie mille, Guglielmo lo leggo subito!

Per il serial print puoi usare la funzione F che usa la memoria flash invece della RAM.
Qui trovi un articolo di come funziona l'heap fragmentation

Ottimo consiglio! Lo stavo leggendo poco fa :sweat_smile:

Dovresti vedere il log e capire esattamente perché l'ESP32 va in eccezione.
Più che l'utilizzo delle String in se, che su ESP32 non è un gran problema per via della notevole quantità di RAM disponibile (520Kb), è l'utilizzo che ne fai ad essere piuttosto "strano" e potenzialmente "pericoloso".

Utilizzi una variabile globale ( String message;) nella funzione dove poi ritorni la variabile globale stessa come risultato e che assegni ad un ulteriore variabile globale (String readings;) ... è un non sense!
La variabile l'hai già modificata, a che serve tutto 'sto giro inutile? Invia direttamente la variabile message!

Questo modo di concatenare le stringhe è sconsigliabile perché frammenta molto facilmente l'heap.

message += "Temp min: " + String (temp_min) + " ºC \n\n";

// Sarebbe meglio se diventa (più lungo, ma più "safe")
message += "Temp min: ";
message  += temp_min;
message  += " ºC \n\n";

Inoltre usi delle librerie inutili o meglio non strettamente necessarie come quella per il tempo NTP che è già egregiamente gestita dal core ESP e non servono cose esterne.
Anche i delay piuttosto lunghi non sono una buona idea... Se la libreria per Telegram (che è bloccante) ci mette un po' con connessione/ricezione/invio e poi sommi il tempo del delay() è facile arrivare ad attivare il watchdog.

Grazie mille. Ho appena modificato la sezione dei vari "message" così come mi hai suggerito. Per quanto riguarda l'invio della variabile message, anche se sopprimo la variabile readings o getReadings sia nel if text = /readings che nel loop dove c'è il timer che invia in automatico i messaggi, ed eseguo l'invio direttamente della sezione message, non mi escono i dati quando scrivo /readings o ad ogni ciclo del timer. Però ho soppresso la String readings, e ho messo direttamente

if (text == "/readings") {
      bot.sendMessage(chat_id, getReadings(), "");

Per quanto concerne i delay, gli unici che utilizzo sono quelli per la Wifi e un ritardo, però utilizzando la funzione millis per fare il check ogni secondo di nuovi messaggi (int botRequestDelay = 1000;). E' quest'ultima che intendi?

Quello che intendevo io è modificare la funzione getReadings() in void (quindi cancella il return alla fine) e poi semplicemente fai cosi:

  getReadings(); // Get readings modifica la String globale message in funzione dei valori letti dai sensori
  myBot.sendMessage(CHAT_IDD3, message, ""); 

Per quanto riguarda i delay() ce ne sono un paio anche nel loop.

Ci sono anche altre ottimizzazioni che si possono fare, ad esempio nella funzione getReading() a che servono queste copie di variabili?

  Mese = currentMonthName;
  Giorno = monthDay;
  Anno = currentYear;
  giornosettimana = weekDay;
  ora = currentHour;
  minuti = currentMinute;
  orario = formattedTime;

Usa direttamente le variabili currentMonthName, monthDay etc etc!!

In realtà anche queste variabili per come la vedo io sono ridonanti perché le puoi recuperare rapidamente con la libreria NTPClient solo quando servono realmente, ma questo è ancora passabile tutto sommato.
E' la necessità di fare la copia della copia della copia delle variabili che non mi spiego.