📡 Monitor VE.Direct con ESP32 e OLED: il progetto definitivo per il tuo impianto solare ⚡

Realizzare un monitor solare professionale, elegante e completamente personalizzato non è mai stato così semplice. In questo articolo ti mostro come ho costruito un sistema completo basato su ESP32, display OLED e protocollo VE.Direct, capace di leggere in tempo reale tutti i dati del regolatore Victron e mostrarli sia su schermo che su una dashboard web moderna e responsive.

Un progetto che unisce elettronica, programmazione e design… e che porta il tuo impianto solare a un livello superiore.

💙 Firmato: TechConnectHub

🔥 Perché questo progetto è speciale

Questo monitor non è un semplice lettore di dati: è un vero e proprio cruscotto intelligente per il tuo impianto fotovoltaico.

Ecco cosa fa:

  • Legge in tempo reale i dati VE.Direct del regolatore Victron
  • Mostra i valori principali su un display OLED 128×64
  • Offre una dashboard web moderna, scura, elegante e aggiornata ogni secondo
  • Funziona con hostname dedicato: `http://victron-monitor.localMostra:
  • Potenza pannelli (W)
  • Tensione pannelli (V)
  • Tensione batteria (V)
  • Corrente (A)
  • Produzione totale (Wh)
  • Produzione giornaliera (Wh)
  • Stato MPPT (Bulk, Absorption, Float…)
  • Numero di serie del regolatore
  • Versione firmware
  • Include una pagina OLED dedicata con il logo TechConnectHub
  • Sincronizzazione perfetta senza perdere pacchetti VE.Direct

Un progetto pensato per essere affidabile, bello da vedere e semplice da installare.

🧩 Hardware necessario

  • ESP32 DevKit (qualsiasi versione con WiFi)
  • Display OLED 128×64 I2C
  • Cavo VE.Direct → UART (TX → GPIO 32)
  • Alimentazione 5V
  • Qualche cavetto Dupont

🛠️ Collegamenti elettrici

  • Victron TX → ESP32 GPIO 32
  • Victron GND → ESP32 GND
  • OLED SDA → ESP32 GPIO 21
  • OLED SCL → ESP32 GPIO 22
  • OLED VCC → 3.3V
  • OLED GND → GND

💻 Il firmware completo

#include <Arduino.h>
#include <U8g2lib.h>
#include <WiFi.h>
#include <WebServer.h>

// -------------------- DISPLAY --------------------
U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

// -------------------- VARIABILI GLOBALI --------------------
float battery_voltage = 0;
float battery_current = 0;
float pv_power = 0;
float pv_voltage = 0;
float yield_total = 0;
float yield_today_real = 0;
String mppt_state = "Off";
float soc_voltage = 0;  // Aggiunto SOC

String serial_number = "";
String firmware_ver = "";

unsigned long lastUpdate = 0;
unsigned long lastPageSwitch = 0;
int lcd_page = 0;
bool soc_ready = false;  // Flag SOC pronto

// -------------------- VE.DIRECT BUFFER --------------------
String veLine = "";
bool packetReady = false;

float new_voltage = 0;
float new_current = 0;
float new_pv_power = 0;
float new_pv_voltage = 0;
float new_yield_total = 0;
float new_yield_today_real = 0;
String new_state = "Off";
float new_soc_voltage = 0;  // Aggiunto SOC

String new_serial = "";
String new_fw = "";

// -------------------- WIFI CONFIG --------------------
const char* ssid     = "Bxxxx";
const char* password = "xx";

// -------------------- WIFI WATCHDOG --------------------
unsigned long lastWiFiCheck = 0;
unsigned long wifiReconnectAttempts = 0;
const unsigned long WIFI_CHECK_INTERVAL = 10000; // 10 secondi
const unsigned long MAX_RECONNECT_ATTEMPTS = 5;
bool wifiConnected = false;

// -------------------- WEB SERVER --------------------
WebServer server(80);

// -------------------- FUNZIONE CALCOLO SOC --------------------
float calculateSOC(float v, String state) {

  // Normalizza lo stato in minuscolo
  state.toLowerCase();

  // 100% SOLO in float e sopra 54.40 V
  if (state.indexOf("float") != -1) {
    if (v > 54.40) return 100;
  }

  const int N = 10;
  float v_points[N]   = {54.4, 53.6, 53.1, 52.8, 52.3, 52.0, 51.5, 51.2, 48.0, 40.0};
  float soc_points[N] = {100,  90,   80,   70,   60,   40,   30,   20,   10,   0};

  // EVITA 100% fuori dal float
  if (v > 54.40) return 99;

  // Sotto il minimo → 0%
  if (v <= 40.0) return 0;

  // Interpolazione lineare
  for (int i = 0; i < N - 1; i++) {
    if (v <= v_points[i] && v > v_points[i + 1]) {
      float t = (v - v_points[i + 1]) / (v_points[i] - v_points[i + 1]);
      return soc_points[i + 1] + t * (soc_points[i] - soc_points[i + 1]);
    }
  }

  return 0;
}


// -------------------- HTML PAGE --------------------
const char* htmlPage = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>VE.Direct Monitor</title>
<style>
  body { font-family: Arial; background:#0d1117; color:#e6edf3; padding:20px; }
  h1 { text-align:center; color:#58a6ff; margin-bottom:25px; }
  .section-title { font-size:22px; margin-top:25px; margin-bottom:10px; color:#f0883e; text-align:center; }
  .card { background:#161b22; padding:18px; border-radius:12px; margin-bottom:15px; border:1px solid #30363d; }
  .label { font-size:18px; color:#8b949e; }
  .value { font-size:26px; font-weight:bold; margin-top:5px; }
  .watt { color:#f0883e; }
  .volt { color:#3fb950; }
  .amp  { color:#d29922; }
  .wh   { color:#a371f7; }
  .state { color:#58a6ff; }
  .soc   { color:#f0883e; }
  .footer { text-align:center; margin-top:30px; font-size:18px; color:#58a6ff; font-weight:bold; }
  .wifi-status { position:fixed; top:10px; right:10px; padding:5px 10px; border-radius:15px; font-size:12px; }
  .wifi-connected { background:#3fb950; color:white; }
  .wifi-disconnected { background:#f85149; color:white; }
</style>
</head>
<body>

<div id="wifiStatus" class="wifi-status wifi-disconnected">WiFi: Disconnesso</div>

<h1>VE.Direct Monitor</h1>

<div class="section-title">Dati in tempo reale</div>

<div class="card"><div class="label">Pannelli</div><div class="value watt" id="pv_power">-- W</div></div>
<div class="card"><div class="label">Tensione Pannelli</div><div class="value volt" id="pv_voltage">-- V</div></div>
<div class="card"><div class="label">Batteria</div><div class="value volt" id="battery_voltage">-- V</div></div>
<div class="card"><div class="label">Corrente</div><div class="value amp" id="battery_current">-- A</div></div>
<div class="card"><div class="label">SOC Batteria</div><div class="value soc" id="soc_voltage">-- %</div></div>
<div class="card"><div class="label">Produzione Totale</div><div class="value wh" id="yield_total">-- Wh</div></div>
<div class="card"><div class="label">Produzione Giornaliera</div><div class="value wh" id="yield_today_real">-- Wh</div></div>
<div class="card"><div class="label">Stato MPPT</div><div class="value state" id="mppt_state">--</div></div>

<div class="section-title">Informazioni Regolatore</div>

<div class="card"><div class="label">Seriale</div><div class="value state" id="serial_number">--</div></div>
<div class="card"><div class="label">Firmware</div><div class="value state" id="firmware_ver">--</div></div>

<div class="footer">By TechConnectHub</div>

<script>
function updateWifiStatus(connected) {
  const element = document.getElementById('wifiStatus');
  element.className = connected ? 'wifi-status wifi-connected' : 'wifi-status wifi-disconnected';
  element.innerText = 'WiFi: ' + (connected ? 'Connesso' : 'Disconnesso');
}

function updateData() {
  fetch('/api')
    .then(response => {
      updateWifiStatus(true);
      return response.json();
    })
    .then(data => {
      document.getElementById('pv_power').innerText        = data.pv_power + " W";
      document.getElementById('pv_voltage').innerText      = data.pv_voltage.toFixed(2) + " V";
      document.getElementById('battery_voltage').innerText = data.battery_voltage.toFixed(2) + " V";
      document.getElementById('battery_current').innerText = data.battery_current.toFixed(2) + " A";
      document.getElementById('soc_voltage').innerText     = data.soc_voltage.toFixed(0) + " %";
      document.getElementById('yield_total').innerText     = data.yield_total + " Wh";
      document.getElementById('yield_today_real').innerText= data.yield_today_real + " Wh";
      document.getElementById('mppt_state').innerText      = data.mppt_state;
      document.getElementById('serial_number').innerText   = data.serial_number;
      document.getElementById('firmware_ver').innerText    = data.firmware_ver;
    })
    .catch(error => {
      updateWifiStatus(false);
      console.error('Errore di connessione:', error);
    });
}

setInterval(updateData, 1000);
updateData();
</script>

</body>
</html>
)rawliteral";

// -------------------- VE.DIRECT PARSER --------------------
String csToState(int cs) {
  switch (cs) {
    case 0: return "Off";
    case 2: return "Fault";
    case 3: return "Bulk";
    case 4: return "Absorption";
    case 5: return "Float";
    case 7: return "Equalize";
    default: return "Unknown";
  }
}

void parseVEDirectLine(const String &line) {
  int sep = line.indexOf('\t');
  if (sep < 0) return;

  String key = line.substring(0, sep);
  String val = line.substring(sep + 1);
  long iv = val.toInt();

  if (key == "V") new_voltage = iv / 1000.0f;
  else if (key == "I") new_current = iv / 1000.0f;
  else if (key == "PPV") new_pv_power = iv;
  else if (key == "VPV") new_pv_voltage = iv / 1000.0f;
  else if (key == "H19") new_yield_total = iv * 10.0f;
  else if (key == "H20") new_yield_today_real = iv * 10.0f;
  else if (key == "CS") new_state = csToState(iv);
  else if (key == "SER#") new_serial = val;
  else if (key == "FW") {
    float fw = iv / 100.0f;
    new_fw = String(fw, 2);
  }
  else if (key == "Checksum") packetReady = true;
}

void readVEDirect() {
  while (Serial2.available()) {
    char c = Serial2.read();
    if (c == '\n') { parseVEDirectLine(veLine); veLine = ""; }
    else if (c != '\r') veLine += c;
  }

  if (packetReady) {
    battery_voltage = new_voltage;
    battery_current = new_current;
    pv_power        = new_pv_power;
    pv_voltage      = new_pv_voltage;
    yield_total     = new_yield_total;
    yield_today_real= new_yield_today_real;
    mppt_state      = new_state;
    
    // Calcola il SOC basato sulla tensione
    new_soc_voltage = calculateSOC(new_voltage, new_state);
    soc_voltage = new_soc_voltage;
    
    serial_number   = new_serial;
    firmware_ver    = new_fw;
    lastUpdate = millis();
    packetReady = false;
    
    // Imposta SOC come pronto se abbiamo dati validi
    if (!soc_ready && soc_voltage > 0 && battery_voltage > 0) {
      soc_ready = true;
    }
  }
}

// -------------------- WIFI MANAGEMENT --------------------
void WiFiEvent(WiFiEvent_t event) {
  switch (event) {
    case ARDUINO_EVENT_WIFI_STA_CONNECTED:
      Serial.println("Connesso all'AP");
      wifiConnected = true;
      wifiReconnectAttempts = 0;
      break;
      
    case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
      Serial.println("Disconnesso dall'AP");
      wifiConnected = false;
      break;
      
    case ARDUINO_EVENT_WIFI_STA_GOT_IP:
      Serial.print("IP ottenuto: ");
      Serial.println(WiFi.localIP());
      break;
      
    default:
      break;
  }
}

void wifiWatchdog() {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("WiFi disconnesso, tentativo di riconnessione...");
    
    if (wifiReconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
      WiFi.disconnect();
      delay(100);
      WiFi.reconnect();
      wifiReconnectAttempts++;
      Serial.print("Tentativo ");
      Serial.println(wifiReconnectAttempts);
    } else {
      Serial.println("Troppi tentativi falliti, reset completo WiFi...");
      WiFi.disconnect(true);
      delay(1000);
      WiFi.begin(ssid, password);
      wifiReconnectAttempts = 0;
    }
  } else {
    wifiReconnectAttempts = 0;
  }
}

// -------------------- SETUP --------------------
void setup() {
  Serial.begin(115200);
  Serial2.begin(19200, SERIAL_8N1, 32, 33);

  u8g2.begin();
  u8g2.enableUTF8Print();

  // Configurazione WiFi
  WiFi.onEvent(WiFiEvent);
  WiFi.mode(WIFI_STA);
  WiFi.setHostname("victron-monitor");
  WiFi.setAutoReconnect(true);
  WiFi.persistent(true);

  Serial.println("Connessione al WiFi...");
  WiFi.begin(ssid, password);

  // Timeout connessione iniziale (30 secondi)
  unsigned long startTime = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - startTime < 30000) {
    delay(500);
    Serial.print(".");
  }

  if (WiFi.status() == WL_CONNECTED) {
    Serial.println("\nConnesso!");
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());
  } else {
    Serial.println("\nFallito connessione WiFi iniziale");
  }

  // Configurazione server web
  server.on("/", []() {
    server.send(200, "text/html", htmlPage);
  });

  server.on("/api", []() {
    String json = "{";
    json += "\"pv_power\":" + String(pv_power) + ",";
    json += "\"pv_voltage\":" + String(pv_voltage) + ",";
    json += "\"battery_voltage\":" + String(battery_voltage) + ",";
    json += "\"battery_current\":" + String(battery_current) + ",";
    json += "\"soc_voltage\":" + String(soc_voltage) + ",";
    json += "\"yield_total\":" + String(yield_total) + ",";
    json += "\"yield_today_real\":" + String(yield_today_real) + ",";
    json += "\"serial_number\":\"" + serial_number + "\",";
    json += "\"firmware_ver\":\"" + firmware_ver + "\",";
    json += "\"mppt_state\":\"" + mppt_state + "\"";
    json += "}";
    server.send(200, "application/json", json);
  });

  server.begin();
  Serial.println("Server HTTP avviato");
}

// -------------------- LOOP --------------------
void loop() {
  server.handleClient();
  readVEDirect();

  // Gestione WiFi watchdog ogni 10 secondi
  if (millis() - lastWiFiCheck > WIFI_CHECK_INTERVAL) {
    wifiWatchdog();
    lastWiFiCheck = millis();
  }

  // Cambio pagina display ogni 4 secondi
  if (millis() - lastPageSwitch > 4000) {
    lcd_page = (lcd_page + 1) % 11;  // Modificato per 11 pagine (0-10)
    lastPageSwitch = millis();
  }

  // Aggiornamento display
  u8g2.firstPage();
  do {
    if (millis() - lastUpdate > 15000) {
      u8g2.setFont(u8g2_font_ncenB12_tr);
      u8g2.drawStr(0, 30, "No VE.Direct...");
    } else {
      switch (lcd_page) {
        case 0:
          u8g2.setFont(u8g2_font_ncenB14_tr);
          u8g2.drawStr(0, 16, "Pannelli");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          u8g2.print(pv_power); u8g2.print(" W");
          break;

        case 1:
          u8g2.setFont(u8g2_font_ncenB14_tr);
          u8g2.drawStr(0, 16, "V Pannelli");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          u8g2.print(pv_voltage, 2); u8g2.print(" V");
          break;

        case 2:
          u8g2.setFont(u8g2_font_ncenB14_tr);
          u8g2.drawStr(0, 16, "Batteria");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          u8g2.print(battery_voltage, 2); u8g2.print(" V");
          break;

        case 3:
          u8g2.setFont(u8g2_font_ncenB14_tr);
          u8g2.drawStr(0, 16, "Corrente");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          u8g2.print(battery_current, 2); u8g2.print(" A");
          break;

        case 4:
          u8g2.setFont(u8g2_font_ncenB14_tr);
          u8g2.drawStr(0, 16, "Totale");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          u8g2.print(yield_total); u8g2.print(" Wh");
          break;

        case 5:
          u8g2.setFont(u8g2_font_ncenB14_tr);
          u8g2.drawStr(0, 16, "Giorno");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          u8g2.print(yield_today_real); u8g2.print(" Wh");
          break;

        case 6:
          u8g2.setFont(u8g2_font_ncenB12_tr);
          u8g2.drawStr(0, 16, "SER#");
          u8g2.setFont(u8g2_font_7x14B_tr);
          u8g2.setCursor(0, 55);
          u8g2.print(serial_number);
          break;

        case 7:
          u8g2.setFont(u8g2_font_ncenB12_tr);
          u8g2.drawStr(0, 16, "FW");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          u8g2.print(firmware_ver);
          break;

        case 8:
          u8g2.setFont(u8g2_font_7x14B_tr);
          u8g2.drawStr(0, 40, "TechConnectHub");
          break;

        case 9:
          u8g2.setFont(u8g2_font_ncenB14_tr);
          u8g2.drawStr(0, 16, "Stato MPPT");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          u8g2.print(mppt_state);
          break;

        case 10:  // PAGINA 10: SOC BATTERIA
          u8g2.setFont(u8g2_font_ncenB14_tr);
          u8g2.drawStr(0, 16, "SOC Batteria");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          
          if (!soc_ready) {
            u8g2.print("Attesa...");
          } else {
            u8g2.print(soc_voltage, 0); u8g2.print(" %");
          }
          break;
      }
    }
  } while (u8g2.nextPage());
}
  • font migliorati
  • pagina web divisa in sezioni
  • scritta TechConnectHub su OLED e web
  • parsing VE.Direct completo
  • sincronizzazione perfetto

🌐 Dashboard web moderna e responsive

La dashboard web è stata progettata per essere:

  • leggibile anche da smartphone
  • elegante grazie al tema scuro
  • aggiornata ogni secondo
  • divisa in due sezioni:
  • Dati in tempo reale
  • Informazioni del regolatore

Il footer mostra con orgoglio:

By TechConnectHub

🖥️ Display OLED: semplice, chiaro, professionale

Il display OLED mostra i dati in rotazione ogni 4 secondi:

  • Potenza pannelli
  • Tensione pannelli
  • Tensione batteria
  • Corrente
  • Produzione totale
  • Produzione giornaliera
  • Numero di serie
  • Firmware
  • Logo TechConnectHub

I font sono stati calibrati per essere:

  1. grandi dove serve (es. 1.68)
  2. compatti dove necessario (SER#)
  3. sempre leggibili

Versione v2 con integrazione SOC

#include <Arduino.h>
#include <U8g2lib.h>
#include <WiFi.h>
#include <WebServer.h>

// -------------------- DISPLAY --------------------
U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

// -------------------- VARIABILI GLOBALI --------------------
float battery_voltage = 0;
float battery_current = 0;
float pv_power = 0;
float pv_voltage = 0;
float yield_total = 0;
float yield_today_real = 0;
String mppt_state = "Off";
float soc_voltage = 0;  // Aggiunto SOC

String serial_number = "";
String firmware_ver = "";

unsigned long lastUpdate = 0;
unsigned long lastPageSwitch = 0;
int lcd_page = 0;
bool soc_ready = false;  // Flag SOC pronto

// -------------------- VE.DIRECT BUFFER --------------------
String veLine = "";
bool packetReady = false;

float new_voltage = 0;
float new_current = 0;
float new_pv_power = 0;
float new_pv_voltage = 0;
float new_yield_total = 0;
float new_yield_today_real = 0;
String new_state = "Off";
float new_soc_voltage = 0;  // Aggiunto SOC

String new_serial = "";
String new_fw = "";

// -------------------- WIFI CONFIG --------------------
const char* ssid     = "xx";
const char* password = "sxxx";

// -------------------- WIFI WATCHDOG --------------------
unsigned long lastWiFiCheck = 0;
unsigned long wifiReconnectAttempts = 0;
const unsigned long WIFI_CHECK_INTERVAL = 10000; // 10 secondi
const unsigned long MAX_RECONNECT_ATTEMPTS = 5;
bool wifiConnected = false;

// -------------------- WEB SERVER --------------------
WebServer server(80);

// -------------------- FUNZIONE CALCOLO SOC --------------------
float calculateSOC(float v, const String& state) {
  // 100% SOLO in FLOAT
  if (state == "Float" || state == "FLOAT" || state == "float") {
    if (v > 53.3) return 100;
  }

  const int N = 10;
  float v_points[N]   = {54.4, 53.6, 53.1, 52.8, 52.3, 52.0, 51.5, 51.2, 48.0, 40.0};
  float soc_points[N] = {100,  90,   80,   70,   60,   45,   30,   20,   10,   0};

  // EVITA 100% fuori dal FLOAT
  if (v >= v_points[0]) {
    return 99;   // massimo SOC fuori dal Float
  }

  if (v <= v_points[N-1]) return soc_points[N-1];

  for (int i = 0; i < N - 1; i++) {
    if (v <= v_points[i] && v > v_points[i + 1]) {
      float t = (v - v_points[i + 1]) / (v_points[i] - v_points[i + 1]);
      return soc_points[i + 1] + t * (soc_points[i] - soc_points[i + 1]);
    }
  }

  return 0;
}

// -------------------- HTML PAGE --------------------
const char* htmlPage = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>VE.Direct Monitor</title>
<style>
  body { font-family: Arial; background:#0d1117; color:#e6edf3; padding:20px; }
  h1 { text-align:center; color:#58a6ff; margin-bottom:25px; }
  .section-title { font-size:22px; margin-top:25px; margin-bottom:10px; color:#f0883e; text-align:center; }
  .card { background:#161b22; padding:18px; border-radius:12px; margin-bottom:15px; border:1px solid #30363d; }
  .label { font-size:18px; color:#8b949e; }
  .value { font-size:26px; font-weight:bold; margin-top:5px; }
  .watt { color:#f0883e; }
  .volt { color:#3fb950; }
  .amp  { color:#d29922; }
  .wh   { color:#a371f7; }
  .state { color:#58a6ff; }
  .soc   { color:#f0883e; }
  .footer { text-align:center; margin-top:30px; font-size:18px; color:#58a6ff; font-weight:bold; }
  .wifi-status { position:fixed; top:10px; right:10px; padding:5px 10px; border-radius:15px; font-size:12px; }
  .wifi-connected { background:#3fb950; color:white; }
  .wifi-disconnected { background:#f85149; color:white; }
</style>
</head>
<body>

<div id="wifiStatus" class="wifi-status wifi-disconnected">WiFi: Disconnesso</div>

<h1>VE.Direct Monitor</h1>

<div class="section-title">Dati in tempo reale</div>

<div class="card"><div class="label">Pannelli</div><div class="value watt" id="pv_power">-- W</div></div>
<div class="card"><div class="label">Tensione Pannelli</div><div class="value volt" id="pv_voltage">-- V</div></div>
<div class="card"><div class="label">Batteria</div><div class="value volt" id="battery_voltage">-- V</div></div>
<div class="card"><div class="label">Corrente</div><div class="value amp" id="battery_current">-- A</div></div>
<div class="card"><div class="label">SOC Batteria</div><div class="value soc" id="soc_voltage">-- %</div></div>
<div class="card"><div class="label">Produzione Totale</div><div class="value wh" id="yield_total">-- Wh</div></div>
<div class="card"><div class="label">Produzione Giornaliera</div><div class="value wh" id="yield_today_real">-- Wh</div></div>
<div class="card"><div class="label">Stato MPPT</div><div class="value state" id="mppt_state">--</div></div>

<div class="section-title">Informazioni Regolatore</div>

<div class="card"><div class="label">Seriale</div><div class="value state" id="serial_number">--</div></div>
<div class="card"><div class="label">Firmware</div><div class="value state" id="firmware_ver">--</div></div>

<div class="footer">By TechConnectHub</div>

<script>
function updateWifiStatus(connected) {
  const element = document.getElementById('wifiStatus');
  element.className = connected ? 'wifi-status wifi-connected' : 'wifi-status wifi-disconnected';
  element.innerText = 'WiFi: ' + (connected ? 'Connesso' : 'Disconnesso');
}

function updateData() {
  fetch('/api')
    .then(response => {
      updateWifiStatus(true);
      return response.json();
    })
    .then(data => {
      document.getElementById('pv_power').innerText        = data.pv_power + " W";
      document.getElementById('pv_voltage').innerText      = data.pv_voltage.toFixed(2) + " V";
      document.getElementById('battery_voltage').innerText = data.battery_voltage.toFixed(2) + " V";
      document.getElementById('battery_current').innerText = data.battery_current.toFixed(2) + " A";
      document.getElementById('soc_voltage').innerText     = data.soc_voltage.toFixed(0) + " %";
      document.getElementById('yield_total').innerText     = data.yield_total + " Wh";
      document.getElementById('yield_today_real').innerText= data.yield_today_real + " Wh";
      document.getElementById('mppt_state').innerText      = data.mppt_state;
      document.getElementById('serial_number').innerText   = data.serial_number;
      document.getElementById('firmware_ver').innerText    = data.firmware_ver;
    })
    .catch(error => {
      updateWifiStatus(false);
      console.error('Errore di connessione:', error);
    });
}

setInterval(updateData, 1000);
updateData();
</script>

</body>
</html>
)rawliteral";

// -------------------- VE.DIRECT PARSER --------------------
String csToState(int cs) {
  switch (cs) {
    case 0: return "Off";
    case 2: return "Fault";
    case 3: return "Bulk";
    case 4: return "Absorption";
    case 5: return "Float";
    case 7: return "Equalize";
    default: return "Unknown";
  }
}

void parseVEDirectLine(const String &line) {
  int sep = line.indexOf('\t');
  if (sep < 0) return;

  String key = line.substring(0, sep);
  String val = line.substring(sep + 1);
  long iv = val.toInt();

  if (key == "V") new_voltage = iv / 1000.0f;
  else if (key == "I") new_current = iv / 1000.0f;
  else if (key == "PPV") new_pv_power = iv;
  else if (key == "VPV") new_pv_voltage = iv / 1000.0f;
  else if (key == "H19") new_yield_total = iv * 10.0f;
  else if (key == "H20") new_yield_today_real = iv * 10.0f;
  else if (key == "CS") new_state = csToState(iv);
  else if (key == "SER#") new_serial = val;
  else if (key == "FW") {
    float fw = iv / 100.0f;
    new_fw = String(fw, 2);
  }
  else if (key == "Checksum") packetReady = true;
}

void readVEDirect() {
  while (Serial2.available()) {
    char c = Serial2.read();
    if (c == '\n') { parseVEDirectLine(veLine); veLine = ""; }
    else if (c != '\r') veLine += c;
  }

  if (packetReady) {
    battery_voltage = new_voltage;
    battery_current = new_current;
    pv_power        = new_pv_power;
    pv_voltage      = new_pv_voltage;
    yield_total     = new_yield_total;
    yield_today_real= new_yield_today_real;
    mppt_state      = new_state;
    
    // Calcola il SOC basato sulla tensione
    new_soc_voltage = calculateSOC(new_voltage, new_state);
    soc_voltage = new_soc_voltage;
    
    serial_number   = new_serial;
    firmware_ver    = new_fw;
    lastUpdate = millis();
    packetReady = false;
    
    // Imposta SOC come pronto se abbiamo dati validi
    if (!soc_ready && soc_voltage > 0 && battery_voltage > 0) {
      soc_ready = true;
    }
  }
}

// -------------------- WIFI MANAGEMENT --------------------
void WiFiEvent(WiFiEvent_t event) {
  switch (event) {
    case ARDUINO_EVENT_WIFI_STA_CONNECTED:
      Serial.println("Connesso all'AP");
      wifiConnected = true;
      wifiReconnectAttempts = 0;
      break;
      
    case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
      Serial.println("Disconnesso dall'AP");
      wifiConnected = false;
      break;
      
    case ARDUINO_EVENT_WIFI_STA_GOT_IP:
      Serial.print("IP ottenuto: ");
      Serial.println(WiFi.localIP());
      break;
      
    default:
      break;
  }
}

void wifiWatchdog() {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("WiFi disconnesso, tentativo di riconnessione...");
    
    if (wifiReconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
      WiFi.disconnect();
      delay(100);
      WiFi.reconnect();
      wifiReconnectAttempts++;
      Serial.print("Tentativo ");
      Serial.println(wifiReconnectAttempts);
    } else {
      Serial.println("Troppi tentativi falliti, reset completo WiFi...");
      WiFi.disconnect(true);
      delay(1000);
      WiFi.begin(ssid, password);
      wifiReconnectAttempts = 0;
    }
  } else {
    wifiReconnectAttempts = 0;
  }
}

// -------------------- SETUP --------------------
void setup() {
  Serial.begin(115200);
  Serial2.begin(19200, SERIAL_8N1, 32, 33);

  u8g2.begin();
  u8g2.enableUTF8Print();

  // Configurazione WiFi
  WiFi.onEvent(WiFiEvent);
  WiFi.mode(WIFI_STA);
  WiFi.setHostname("victron-monitor");
  WiFi.setAutoReconnect(true);
  WiFi.persistent(true);

  Serial.println("Connessione al WiFi...");
  WiFi.begin(ssid, password);

  // Timeout connessione iniziale (30 secondi)
  unsigned long startTime = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - startTime < 30000) {
    delay(500);
    Serial.print(".");
  }

  if (WiFi.status() == WL_CONNECTED) {
    Serial.println("\nConnesso!");
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());
  } else {
    Serial.println("\nFallito connessione WiFi iniziale");
  }

  // Configurazione server web
  server.on("/", []() {
    server.send(200, "text/html", htmlPage);
  });

  server.on("/api", []() {
    String json = "{";
    json += "\"pv_power\":" + String(pv_power) + ",";
    json += "\"pv_voltage\":" + String(pv_voltage) + ",";
    json += "\"battery_voltage\":" + String(battery_voltage) + ",";
    json += "\"battery_current\":" + String(battery_current) + ",";
    json += "\"soc_voltage\":" + String(soc_voltage) + ",";
    json += "\"yield_total\":" + String(yield_total) + ",";
    json += "\"yield_today_real\":" + String(yield_today_real) + ",";
    json += "\"serial_number\":\"" + serial_number + "\",";
    json += "\"firmware_ver\":\"" + firmware_ver + "\",";
    json += "\"mppt_state\":\"" + mppt_state + "\"";
    json += "}";
    server.send(200, "application/json", json);
  });

  server.begin();
  Serial.println("Server HTTP avviato");
}

// -------------------- LOOP --------------------
void loop() {
  server.handleClient();
  readVEDirect();

  // Gestione WiFi watchdog ogni 10 secondi
  if (millis() - lastWiFiCheck > WIFI_CHECK_INTERVAL) {
    wifiWatchdog();
    lastWiFiCheck = millis();
  }

  // Cambio pagina display ogni 4 secondi
  if (millis() - lastPageSwitch > 4000) {
    lcd_page = (lcd_page + 1) % 11;  // Modificato per 11 pagine (0-10)
    lastPageSwitch = millis();
  }

  // Aggiornamento display
  u8g2.firstPage();
  do {
    if (millis() - lastUpdate > 15000) {
      u8g2.setFont(u8g2_font_ncenB12_tr);
      u8g2.drawStr(0, 30, "No VE.Direct...");
    } else {
      switch (lcd_page) {
        case 0:
          u8g2.setFont(u8g2_font_ncenB14_tr);
          u8g2.drawStr(0, 16, "Pannelli");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          u8g2.print(pv_power); u8g2.print(" W");
          break;

        case 1:
          u8g2.setFont(u8g2_font_ncenB14_tr);
          u8g2.drawStr(0, 16, "V Pannelli");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          u8g2.print(pv_voltage, 2); u8g2.print(" V");
          break;

        case 2:
          u8g2.setFont(u8g2_font_ncenB14_tr);
          u8g2.drawStr(0, 16, "Batteria");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          u8g2.print(battery_voltage, 2); u8g2.print(" V");
          break;

        case 3:
          u8g2.setFont(u8g2_font_ncenB14_tr);
          u8g2.drawStr(0, 16, "Corrente");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          u8g2.print(battery_current, 2); u8g2.print(" A");
          break;

        case 4:
          u8g2.setFont(u8g2_font_ncenB14_tr);
          u8g2.drawStr(0, 16, "Totale");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          u8g2.print(yield_total); u8g2.print(" Wh");
          break;

        case 5:
          u8g2.setFont(u8g2_font_ncenB14_tr);
          u8g2.drawStr(0, 16, "Giorno");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          u8g2.print(yield_today_real); u8g2.print(" Wh");
          break;

        case 6:
          u8g2.setFont(u8g2_font_ncenB12_tr);
          u8g2.drawStr(0, 16, "SER#");
          u8g2.setFont(u8g2_font_7x14B_tr);
          u8g2.setCursor(0, 55);
          u8g2.print(serial_number);
          break;

        case 7:
          u8g2.setFont(u8g2_font_ncenB12_tr);
          u8g2.drawStr(0, 16, "FW");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          u8g2.print(firmware_ver);
          break;

        case 8:
          u8g2.setFont(u8g2_font_7x14B_tr);
          u8g2.drawStr(0, 40, "TechConnectHub");
          break;

        case 9:
          u8g2.setFont(u8g2_font_ncenB14_tr);
          u8g2.drawStr(0, 16, "Stato MPPT");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          u8g2.print(mppt_state);
          break;

        case 10:  // PAGINA 10: SOC BATTERIA
          u8g2.setFont(u8g2_font_ncenB14_tr);
          u8g2.drawStr(0, 16, "SOC Batteria");
          u8g2.setFont(u8g2_font_fub17_tr);
          u8g2.setCursor(0, 60);
          
          if (!soc_ready) {
            u8g2.print("Attesa...");
          } else {
            u8g2.print(soc_voltage, 0); u8g2.print(" %");
          }
          break;
      }
    }
  } while (u8g2.nextPage());
}

🚀 Conclusione

Questo progetto trasforma un semplice ESP32 in un monitor solare professionale, elegante e completamente personalizzato.

È perfetto per chi vuole:

  • monitorare il proprio impianto solare in tempo reale
  • avere una dashboard moderna e accessibile da qualsiasi dispositivo
  • integrare estetica e funzionalità
  • costruire un prodotto degno di essere venduto

Un progetto che unisce tecnica, design e passione.

Firmato con orgoglio:

💙 TechConnectHub

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *