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";
String serial_number = "";
String firmware_ver = "";
unsigned long lastUpdate = 0;
unsigned long lastPageSwitch = 0;
int lcd_page = 0;
// -------------------- 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";
String new_serial = "";
String new_fw = "";
// -------------------- WIFI CONFIG --------------------
const char* ssid = "BB1";
const char* password = "strangeapartament";
// -------------------- WIFI WATCHDOG --------------------
unsigned long lastWiFiCheck = 0;
void wifiWatchdog() {
bool wifiDead =
WiFi.status() != WL_CONNECTED || // caso 1: disconnessione reale
WiFi.localIP().toString() == "0.0.0.0" || // caso 2: perdita IP
!WiFi.isConnected(); // caso 3: ghost connected
if (wifiDead) {
WiFi.disconnect(true);
delay(200);
WiFi.begin(ssid, password);
}
}
// -------------------- WEB SERVER --------------------
WebServer server(80);
// -------------------- 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; }
.footer { text-align:center; margin-top:30px; font-size:18px; color:#58a6ff; font-weight:bold; }
</style>
</head>
<body>
<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">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 update() {
fetch('/api')
.then(r => r.json())
.then(d => {
document.getElementById('pv_power').innerText = d.pv_power + " W";
document.getElementById('pv_voltage').innerText = d.pv_voltage.toFixed(2) + " V";
document.getElementById('battery_voltage').innerText = d.battery_voltage.toFixed(2) + " V";
document.getElementById('battery_current').innerText = d.battery_current.toFixed(2) + " A";
document.getElementById('yield_total').innerText = d.yield_total + " Wh";
document.getElementById('yield_today_real').innerText= d.yield_today_real + " Wh";
document.getElementById('mppt_state').innerText = d.mppt_state;
document.getElementById('serial_number').innerText = d.serial_number;
document.getElementById('firmware_ver').innerText = d.firmware_ver;
});
}
setInterval(update, 1000);
update();
</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;
serial_number = new_serial;
firmware_ver = new_fw;
lastUpdate = millis();
packetReady = false;
}
}
// -------------------- SETUP --------------------
void setup() {
Serial.begin(115200);
Serial2.begin(19200, SERIAL_8N1, 32, 33);
u8g2.begin();
u8g2.enableUTF8Print();
WiFi.mode(WIFI_STA);
WiFi.setHostname("victron-monitor");
WiFi.setAutoReconnect(true);
WiFi.persistent(false);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) { delay(500); }
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 += "\"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();
}
// -------------------- LOOP --------------------
void loop() {
server.handleClient();
readVEDirect();
// WATCHDOG WIFI DEFINITIVO
if (millis() - lastWiFiCheck > 5000) {
wifiWatchdog();
lastWiFiCheck = millis();
}
if (millis() - lastPageSwitch > 4000) {
lcd_page = (lcd_page + 1) % 10;
lastPageSwitch = millis();
}
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;
}
}
} 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
🚀 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