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:
grandi dove serve (es. 1.68)
compatti dove necessario (SER#)
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
17 Maggio 2026