// =====================================
// Ondata di calore - ESP32 + 2x DS18B20
// Invio temperatura acquario ad Adafruit IO
// Relè attivo HIGH
// OLED SH1107 128x128 I2C
//
// Collegamenti:
// - DS18B20 sonda 1: DATA -> GPIO25
// - DS18B20 sonda 2: DATA -> GPIO26
// - Ogni DS18B20: resistenza pull-up 4.7 kOhm tra DATA e 3.3V
// - Relè riscaldatore: IN -> GPIO18
// - OLED SH1107 I2C: SDA -> GPIO21, SCL -> GPIO22
// - Alimentazione sensori: 3.3V e GND
// =====================================

#include "esp_task_wdt.h"
#include "esp_system.h"

#include <math.h>
#include <OneWire.h>
#include <DallasTemperature.h>

#include <Wire.h>
#include <Adafruit_SH110X.h>
#include <U8g2_for_Adafruit_GFX.h>

// -----------------------------
// WIFI / ADAFRUIT IO
// -----------------------------
#include <WiFi.h>
#include "esp_wifi.h"
#include "AdafruitIO_WiFi.h"

// Credenziali Adafruit IO
// NON lasciare chiavi reali in sketch condivisi.
#define IO_USERNAME "*******"
#define IO_KEY "*******"

// Rete locale WiFi
#define WIFI_SSID "IoThings"
#define WIFI_PASS "*******"

// Se la rete IoThings riconosce il dispositivo tramite MAC,
// puoi lasciare USE_CUSTOM_MAC a true.
// Se non ti serve, metti false.
#define USE_CUSTOM_MAC true

// MAC locale/unicast già usato nel tuo altro sketch.
// Deve essere applicato prima di io.connect().
uint8_t newMAC[6] = { 0xB6, 0xC5, 0xB6, 0x7C, 0x89, 0x88 };

AdafruitIO_WiFi io(IO_USERNAME, IO_KEY, WIFI_SSID, WIFI_PASS);

// Feed Adafruit IO dove invio la temperatura finale dell'acquario.
// Deve esistere su Adafruit IO oppure verrà creato/gestito dalla libreria.
AdafruitIO_Feed *temp_acq_lab = io.feed("temp_acq_lab");

// Invio ogni 15 minuti
const unsigned long AIO_SEND_INTERVAL_MS = 15UL * 60UL * 1000UL;

// Se Adafruit IO è offline, non stampo/tento a ogni loop ma al massimo ogni minuto
const unsigned long AIO_RETRY_OFFLINE_MS = 60UL * 1000UL;

unsigned long lastAioSend = 0;
unsigned long lastAioAttempt = 0;
bool firstAioSendDone = false;

// -----------------------------
// WATCHDOG
// -----------------------------
#define WDT_TIMEOUT 40  // secondi

// -----------------------------
// PIN
// -----------------------------
// I2C principale per OLED SH1107
#define I2C_SDA 21
#define I2C_SCL 22

// DS18B20 su due bus OneWire separati
// GPIO25 e GPIO26 sono scelti per evitare GPIO2 LED onboard,
// GPIO16 RGB, e GPIO4/GPIO5.
#define ONE_WIRE_1 25
#define ONE_WIRE_2 26

// Relè attivo HIGH:
// LOW  = relè spento
// HIGH = relè acceso
const int RELAY_PIN = 18;

// -----------------------------
// DS18B20 su due bus separati
// -----------------------------
OneWire oneWire1(ONE_WIRE_1);
OneWire oneWire2(ONE_WIRE_2);

DallasTemperature sensor1(&oneWire1);
DallasTemperature sensor2(&oneWire2);

// -----------------------------
// OLED SH1107
// -----------------------------
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 128
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C

Adafruit_SH1107 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
U8G2_FOR_ADAFRUIT_GFX u8g2;

// -----------------------------
// Parametri lettura temperatura
// -----------------------------
const int N_READS = 6;

// Se le due sonde differiscono più di 0.5 °C,
// uso la temperatura più alta e segnalo ALERT.
const float MAX_DIFF = 0.5f;

// Isteresi controllo relè
const float HYST = 0.2f;

// Tempo minimo tra due commutazioni del relè.
// Evita accensioni/spegnimenti troppo ravvicinati.
const unsigned long MIN_SWITCH = 30000UL;

// -----------------------------
// Programma setpoint ondata di calore
// -----------------------------
byte ora = 0;

// Temperatura iniziale: viene poi aggiornata con la prima lettura valida
float temp_i = 20.0f;

// Incremento orario
float temp_d = 0.2f;

// Temperatura finale massima
float temp_f = 32.0f;

// Temperatura di riferimento corrente
float temp_r = temp_i + temp_d;

// Temperatura attuale finale/mediata
float temp_a = NAN;

// -----------------------------
// Timer programma
// -----------------------------
unsigned long temporif = 0;
unsigned long pausa = 60UL * 60UL * 1000UL;  // 1 ora
int minpass = 0;

// -----------------------------
// Stato relè
// -----------------------------
bool StatoRele = false;
unsigned long lastSwitch = 0;

// -----------------------------
// Display
// -----------------------------
unsigned long screenTimer = 0;
bool screenMode = false;

// -----------------------------
// Utilità: nome reset
// -----------------------------
const char *nomeReset(esp_reset_reason_t reason) {
  switch (reason) {
    case ESP_RST_UNKNOWN: return "UNKNOWN";
    case ESP_RST_POWERON: return "POWERON";
    case ESP_RST_EXT: return "EXT RESET";
    case ESP_RST_SW: return "SOFTWARE";
    case ESP_RST_PANIC: return "PANIC";
    case ESP_RST_INT_WDT: return "INT WDT";
    case ESP_RST_TASK_WDT: return "TASK WDT";
    case ESP_RST_WDT: return "WDT";
    case ESP_RST_DEEPSLEEP: return "DEEPSLEEP";
    case ESP_RST_BROWNOUT: return "BROWNOUT";
    case ESP_RST_SDIO: return "SDIO";
    default: return "UNDEFINED";
  }
}

// -----------------------------
// Utilità: controllo lettura DS18B20
// -----------------------------
static inline bool validDS18(float t) {
  if (isnan(t)) return false;
  if (t == DEVICE_DISCONNECTED_C) return false;  // -127
  if (t == 85.0f) return false;                  // valore spurio tipico
  if (t < -55.0f || t > 125.0f) return false;
  return true;
}

// Risoluzione 9 bit: conversione circa 94 ms.
// Uso 110 ms per margine.
static inline uint16_t convDelayMs9bit() {
  return 110;
}

// -----------------------------
// Applica MAC WiFi personalizzato
// -----------------------------
void applicaMACWiFi() {
  if (!USE_CUSTOM_MAC) return;

  WiFi.mode(WIFI_STA);
  WiFi.persistent(false);
  WiFi.setAutoReconnect(true);

  delay(100);

  // Nessuna connessione deve essere già attiva.
  WiFi.disconnect(false, false);
  delay(100);

  esp_err_t err = esp_wifi_set_mac(WIFI_IF_STA, newMAC);

  if (err == ESP_OK) {
    Serial.print("MAC WiFi impostato: ");
    Serial.println(WiFi.macAddress());
  } else {
    Serial.print("ERRORE impostazione MAC. Codice: ");
    Serial.println(err);
    Serial.print("MAC attuale: ");
    Serial.println(WiFi.macAddress());
  }
}

// -----------------------------
// Connessione ad Adafruit IO
// -----------------------------
void connectAdafruitIO() {
  applicaMACWiFi();

  Serial.print("Connessione WiFi/Adafruit IO a SSID: ");
  Serial.println(WIFI_SSID);

  Serial.print("MAC prima della connessione: ");
  Serial.println(WiFi.macAddress());

  io.connect();

  unsigned long t0 = millis();

  while (io.status() < AIO_CONNECTED && millis() - t0 < 30000UL) {
    io.run();
    Serial.print(".");
    delay(500);
  }

  if (io.status() >= AIO_CONNECTED) {
    Serial.println("\nAdafruit IO connesso");
    Serial.print("IP: ");
    Serial.println(WiFi.localIP());
    Serial.print("MAC in uso: ");
    Serial.println(WiFi.macAddress());
  } else {
    Serial.println("\nTimeout connessione Adafruit IO");
    Serial.print("WiFi.status = ");
    Serial.println(WiFi.status());
    Serial.print("IP = ");
    Serial.println(WiFi.localIP());
    Serial.print("MAC in uso = ");
    Serial.println(WiFi.macAddress());
  }
}

// -----------------------------
// Media di N letture valide.
// Ritorna NAN se nessuna lettura è valida.
// -----------------------------
float readMeanTemp(DallasTemperature &sens) {
  float sum = 0.0f;
  int ok = 0;

  for (int i = 0; i < N_READS; i++) {
    // NON bloccante perché in setup() imposto setWaitForConversion(false)
    sens.requestTemperatures();

    delay(convDelayMs9bit());

    float t = sens.getTempCByIndex(0);

    if (validDS18(t)) {
      sum += t;
      ok++;
    }

    // Piccola pausa fra campioni
    delay(40);
  }

  if (ok == 0) return NAN;
  return sum / ok;
}

// -----------------------------
// Decide la temperatura finale.
// Se entrambe le sonde sono valide e vicine: media.
// Se sono valide ma troppo diverse: usa la più alta e attiva alarm.
// Se una sola è valida: usa quella.
// -----------------------------
void chooseTfinal(float T1, float T2, float &Tfinal, bool &alarm) {
  Tfinal = NAN;
  alarm = false;

  if (!isnan(T1) && !isnan(T2)) {
    float diff = fabsf(T1 - T2);

    if (diff <= MAX_DIFF) {
      Tfinal = (T1 + T2) / 2.0f;
    } else {
      Tfinal = (T1 > T2) ? T1 : T2;
      alarm = true;
    }

  } else if (!isnan(T1)) {
    Tfinal = T1;

  } else if (!isnan(T2)) {
    Tfinal = T2;
  }
}

// -----------------------------
// Stampa temperatura o ERR sul display
// -----------------------------
void printTempOrErr(float t, uint8_t decimals = 1) {
  if (!isnan(t)) {
    u8g2.print(t, decimals);
  } else {
    u8g2.print("ERR");
  }
}

// -----------------------------
// Invio temperatura acquario ad Adafruit IO
// -----------------------------
void inviaTemperaturaAdafruitIO(float Tfinal) {
  unsigned long nowMs = millis();

  // Invio:
  // - subito alla prima lettura valida
  // - poi ogni 15 minuti
  bool due = (!firstAioSendDone) || (nowMs - lastAioSend >= AIO_SEND_INTERVAL_MS);

  if (!due) return;

  // Se offline, ritento al massimo ogni minuto
  if (lastAioAttempt > 0 && nowMs - lastAioAttempt < AIO_RETRY_OFFLINE_MS) {
    return;
  }

  lastAioAttempt = nowMs;

  if (isnan(Tfinal)) {
    Serial.println("Adafruit IO: temperatura non valida, non invio");
    return;
  }

  if (io.status() >= AIO_CONNECTED) {
    Serial.print("Invio Adafruit IO temp_acq_lab = ");
    Serial.println(Tfinal, 2);

    temp_acq_lab->save(Tfinal);

    lastAioSend = nowMs;
    firstAioSendDone = true;

  } else {
    Serial.println("Adafruit IO non connesso: invio saltato");
  }
}

// -----------------------------
// Messaggio boot su display
// -----------------------------
void showBootInfoOnDisplay(const char *line1, const char *line2) {
  display.clearDisplay();

  u8g2.setFont(u8g2_font_6x12_tf);

  u8g2.setCursor(0, 18);
  u8g2.print(line1);

  u8g2.setCursor(0, 36);
  u8g2.print(line2);

  display.display();
}

// -----------------------------
// SETUP
// -----------------------------
void setup() {
  Serial.begin(115200);
  delay(200);

  // -----------------------------
  // Relè
  // -----------------------------
  // Relè attivo HIGH:
  // LOW = spento all'avvio, condizione sicura.
  pinMode(RELAY_PIN, OUTPUT);
  digitalWrite(RELAY_PIN, LOW);
  StatoRele = false;

  // -----------------------------
  // I2C + display
  // -----------------------------
  // SDA = GPIO21
  // SCL = GPIO22
  Wire.begin(I2C_SDA, I2C_SCL);
  Wire.setClock(100000);
  Wire.setTimeOut(50);

  if (!display.begin(SCREEN_ADDRESS, true)) {
    Serial.println("Display SH1107 NON trovato");
  } else {
    Serial.println("Display SH1107 OK");
  }

  display.clearDisplay();
  display.display();

  u8g2.begin(display);
  u8g2.setForegroundColor(SH110X_WHITE);
  u8g2.setBackgroundColor(SH110X_BLACK);

  // -----------------------------
  // Reset reason
  // -----------------------------
  esp_reset_reason_t rr = esp_reset_reason();

  Serial.print("BOOT reset_reason = ");
  Serial.print((int)rr);
  Serial.print(" - ");
  Serial.println(nomeReset(rr));

  showBootInfoOnDisplay("BOOT OK", nomeReset(rr));
  delay(1200);

  // -----------------------------
  // DS18B20
  // -----------------------------
  sensor1.begin();
  sensor2.begin();

  // 9 bit: più veloce, sufficiente per controllo termico dinamico.
  sensor1.setResolution(9);
  sensor2.setResolution(9);

  // Fondamentale: evita blocchi lunghi dentro requestTemperatures().
  sensor1.setWaitForConversion(false);
  sensor2.setWaitForConversion(false);

  // -----------------------------
  // WiFi / Adafruit IO
  // -----------------------------
  connectAdafruitIO();

  // -----------------------------
  // Lettura iniziale setpoint
  // -----------------------------
  float T1 = readMeanTemp(sensor1);
  float T2 = readMeanTemp(sensor2);

  // Doppia lettura per evitare letture parassite iniziali.
  delay(1000);

  T1 = readMeanTemp(sensor1);
  T2 = readMeanTemp(sensor2);

  float Tfinal;
  bool alarm;

  chooseTfinal(T1, T2, Tfinal, alarm);

  if (!isnan(Tfinal)) {
    temp_i = Tfinal;
    temp_r = fminf(Tfinal + temp_d, temp_f);
  }

  Serial.print("Temperatura iniziale = ");
  Serial.println(temp_i, 2);

  Serial.print("Setpoint iniziale = ");
  Serial.println(temp_r, 2);

  // -----------------------------
  // Watchdog
  // -----------------------------
  esp_task_wdt_config_t wdt_config = {
    .timeout_ms = WDT_TIMEOUT * 1000,
    .idle_core_mask = (1 << 0) | (1 << 1),
    .trigger_panic = true
  };

  esp_err_t wdt_err = esp_task_wdt_init(&wdt_config);

  if (wdt_err == ESP_OK) {
    Serial.println("WDT init OK");
  } else if (wdt_err == ESP_ERR_INVALID_STATE) {
    Serial.println("WDT gia' inizializzato dal core: continuo");
  } else {
    Serial.print("Errore init WDT: ");
    Serial.println(wdt_err);
  }

  wdt_err = esp_task_wdt_add(NULL);

  if (wdt_err == ESP_OK) {
    Serial.println("WDT add loop OK");
  } else if (wdt_err == ESP_ERR_INVALID_STATE) {
    Serial.println("Task loop gia' registrato nel WDT: continuo");
  } else {
    Serial.print("Errore add loop al WDT: ");
    Serial.println(wdt_err);
  }

  temporif = millis();
  screenTimer = millis();

  showBootInfoOnDisplay("SETUP COMPLETO", "Avvio controllo");
  delay(1000);
}

// -----------------------------
// LOOP
// -----------------------------
void loop() {
  esp_task_wdt_reset();

  // Mantiene viva la connessione Adafruit IO.
  // La libreria gestisce anche eventuali riconnessioni.
  io.run();

  // -----------------------------
  // 1) Lettura sonde
  // -----------------------------
  float T1 = readMeanTemp(sensor1);

  esp_task_wdt_reset();
  io.run();

  float T2 = readMeanTemp(sensor2);

  esp_task_wdt_reset();
  io.run();

  // -----------------------------
  // 2) Temperatura finale
  // -----------------------------
  float Tfinal;
  bool alarm;

  chooseTfinal(T1, T2, Tfinal, alarm);
  temp_a = Tfinal;

  // -----------------------------
  // 3) Invio temperatura acquario ad Adafruit IO
  // -----------------------------
  inviaTemperaturaAdafruitIO(Tfinal);

  // -----------------------------
  // 4) Timer ore/minuti programma termico
  // -----------------------------
  minpass = (millis() - temporif) / 60000UL;

  if (ora < 100 && (millis() - temporif > pausa)) {
    temporif = millis();
    ora++;

    temp_r += temp_d;
    if (temp_r > temp_f) temp_r = temp_f;
  }

  // -----------------------------
  // 5) Alterna schermata ogni 5 secondi
  // -----------------------------
  if (millis() - screenTimer > 5000UL) {
    screenTimer = millis();
    screenMode = !screenMode;
  }

  // -----------------------------
  // 6) Display
  // -----------------------------
  display.clearDisplay();

  if (!screenMode) {
    // Schermata principale: temperatura attuale
    u8g2.setFont(u8g2_font_logisoso24_tf);

    if (!isnan(Tfinal)) {
      u8g2.setCursor(0, 32);
      u8g2.print(Tfinal, 1);
      u8g2.print(" C");
    } else {
      u8g2.setCursor(10, 32);
      u8g2.print("ERR");
    }

    u8g2.setFont(u8g2_font_6x10_tf);

    u8g2.setCursor(0, 55);
    u8g2.print("T1: ");
    printTempOrErr(T1, 1);

    u8g2.setCursor(0, 67);
    u8g2.print("T2: ");
    printTempOrErr(T2, 1);

    u8g2.setCursor(0, 79);
    u8g2.print("T.rif: ");
    u8g2.print(temp_r, 1);

    if (alarm) {
      u8g2.setCursor(85, 67);
      u8g2.print("ALERT");
    }

    u8g2.setCursor(0, 96);
    u8g2.print("AIO: ");
    u8g2.print(io.status() >= AIO_CONNECTED ? "OK" : "OFF");

    u8g2.setCursor(0, 110);
    u8g2.print("RELE: ");
    u8g2.print(StatoRele ? "ON" : "OFF");

  } else {
    // Schermata programma
    u8g2.setFont(u8g2_font_6x12_tf);

    u8g2.setCursor(0, 18);
    u8g2.print("t.iniz: ");
    u8g2.print(temp_i, 1);
    u8g2.print(" C");

    u8g2.setCursor(0, 33);
    u8g2.print("t.incr/h: ");
    u8g2.print(temp_d, 1);
    u8g2.print(" C");

    u8g2.setCursor(0, 48);
    u8g2.print("t.fin: ");
    u8g2.print(temp_f, 1);
    u8g2.print(" C");

    u8g2.setCursor(0, 63);
    u8g2.print("t.rif: ");
    u8g2.print(temp_r, 1);

    u8g2.setCursor(0, 78);
    u8g2.print("ore: ");
    u8g2.print(ora);
    u8g2.print("  min: ");
    u8g2.print(minpass);

    u8g2.setCursor(0, 93);
    u8g2.print("adesso: ");
    printTempOrErr(temp_a, 1);

    if (alarm) {
      u8g2.setCursor(0, 114);
      u8g2.print("!!! ALERT SONDE !!!");
    } else {
      u8g2.setCursor(0, 114);
      u8g2.print("WiFi/AIO: ");
      u8g2.print(io.status() >= AIO_CONNECTED ? "OK" : "OFF");
    }
  }

  display.display();

  // -----------------------------
  // 7) Controllo relè riscaldatore
  // -----------------------------
  // Sicurezza:
  // se non ho temperatura valida, spengo il relè.
  if (!isnan(Tfinal)) {
    if (!StatoRele && (Tfinal < (temp_r - HYST)) && (millis() - lastSwitch > MIN_SWITCH)) {

      StatoRele = true;
      digitalWrite(RELAY_PIN, HIGH);
      lastSwitch = millis();

      Serial.println("RELE ON");

    } else if (StatoRele && (Tfinal > (temp_r + HYST)) && (millis() - lastSwitch > MIN_SWITCH)) {

      StatoRele = false;
      digitalWrite(RELAY_PIN, LOW);
      lastSwitch = millis();

      Serial.println("RELE OFF");
    }

  } else {
    StatoRele = false;
    digitalWrite(RELAY_PIN, LOW);
  }

  esp_task_wdt_reset();

  // Aggiornamento non troppo lento.
  delay(500);
}

Ultime modifiche: venerdì, 15 maggio 2026, 16:19