✨ Sistema RFID con ESP32, RC522, OLED, Relè, LED e Buzzer

Una guida completa per realizzare un accesso elettronico affidabile, elegante e totalmente integrato con ESPHome.

🔧 Componenti necessari

  • ESP32 DevKit
  • Lettore RFID RC522 (SPI)
  • Display OLED SSD1306 128×64 (I2C)
  • Modulo relè 5V
  • LED verde + LED rosso (con resistenze 220–330 Ω)
  • Buzzer attivo a 3 pin
  • Alimentazione 5V
  • Cavi dupont

🧩 Collegamenti elettrici

Tabella completa dei collegamenti ESP32 → Componenti

RC522 (SPI)

Funzione RC522Pin RC522Pin ESP32
AlimentazioneVCC3.3V
MassaGNDGND
ResetRSTGPIO22
SS / SDASDAGPIO5
SCKSCKGPIO18
MOSIMOSIGPIO23
MISOMISOGPIO19


Display OLED SSD1306 (I2C)

Funzione OLEDPin OLEDPin ESP32
AlimentazioneVCC3.3V
MassaGNDGND
SDASDAGPIO21
SCLSCLGPIO17


LED di stato

LEDAnodo → ESP32Catodo →
VerdeGPIO14 (via resistenza)GND
RossoGPIO27 (via resistenza)GND


Relè cancello

FunzionePin modulo relèPin ESP32
SegnaleINGPIO32
AlimentazioneVCC5V
MassaGNDGND


Buzzer attivo 3 pin

FunzionePin buzzerPin ESP32
AlimentazioneVCC5V
MassaGNDGND
SegnaleI/OGPIO26

💡 Funzionamento del sistema

  • Quando un tag RFID viene avvicinato, l’ESP32 legge l’UID tramite il modulo RC522.
  • Se il tag è autorizzato:
    • Il relè invia un impulso per aprire il cancello
    • Il LED verde si accende per 7 secondi
    • Il display mostra “ACCESSO CONSENTITO”
    • Il buzzer emette due beep
  • Se il tag non è autorizzato:
    • Il LED rosso si accende per 7 secondi
    • Il display mostra “ACCESSO NEGATO”
    • Il buzzer emette tre beep rapidi

🧠 Codice ESPHome completo e funzionante

esphome:
  name: esphome-web-568b34
  friendly_name: Esp32 RFID
  min_version: 2025.11.0
  name_add_mac_suffix: false
  on_boot:
    priority: -100
    then:
      - switch.turn_on: rele

esp32:
  variant: esp32
  framework:
    type: esp-idf
    advanced:
      minimum_chip_revision: "3.1"
      sram1_as_iram: true    

logger:
  level: INFO

api:

ota:
  platform: esphome

wifi:
  ssid: Xxxx
  password: xxxx

# ---------------- SPI + RC522 ----------------
spi:
  clk_pin: 18
  mosi_pin: 23
  miso_pin: 19

rc522_spi:
  cs_pin: 5
  reset_pin: 22
  update_interval: 500ms #valore iniziale 1 sc
  on_tag:
    then:
      - lambda: |-
          std::string uid = x;

          uint32_t now = millis();
          if (now - id(ultimo_lettura) < 2000) {
            return;
          }
          id(ultimo_lettura) = now;

          ESP_LOGI("rfid", "Tag rilevato: %s", uid.c_str());

          id(ultimo_tag).publish_state(uid);

          for (auto &t : id(tag_autorizzati)) {
            if (t == uid) {
              ESP_LOGI("rfid", "Accesso consentito");

              id(accesso_consentito).publish_state(true);
              id(accesso_negato).publish_state(false);

              id(led_ok).execute();
              id(display_ok).execute();

              return;
            }
          }

          ESP_LOGW("rfid", "Accesso negato");

          id(accesso_consentito).publish_state(false);
          id(accesso_negato).publish_state(true);

          id(led_ko).execute();
          id(display_ko).execute();

# ---------------- Lista badge autorizzati ----------------
globals:
  - id: tag_autorizzati
    type: std::vector<std::string>
    initial_value: '{"67-89-90-C9","04-52-DD-01-7A-3B-03","67-FA-0A-D9"}'

  - id: ultimo_lettura
    type: uint32_t
    initial_value: '0'

# ---------------- OUTPUT (LED) ----------------
output:
  - platform: gpio
    pin: 14
    id: led_verde_pin

  - platform: gpio
    pin: 27
    id: led_rosso_pin
  
# ---------------- Script vari ----------------
script:
  - id: led_ok
    then:
      - switch.turn_on: buzzer
      - delay: 120ms
      - switch.turn_off: buzzer
      - delay: 120ms
      - switch.turn_on: buzzer
      - delay: 120ms
      - switch.turn_off: buzzer

      - switch.turn_off: rele
      - delay: 2s
      - switch.turn_on: rele

      - light.turn_on: led_verde
      - delay: 7s
      - light.turn_off: led_verde
      - lambda: |-
          id(accesso_consentito).publish_state(false);

  - id: led_ko
    then:
      - switch.turn_on: buzzer
      - delay: 80ms
      - switch.turn_off: buzzer
      - delay: 80ms
      - switch.turn_on: buzzer
      - delay: 80ms
      - switch.turn_off: buzzer
      - delay: 80ms
      - switch.turn_on: buzzer
      - delay: 80ms
      - switch.turn_off: buzzer

      - light.turn_on: led_rosso
      - delay: 7s
      - light.turn_off: led_rosso
      - lambda: |-
          id(accesso_negato).publish_state(false);

  - id: display_ok
    then:
      - display.page.show: page_ok
      - delay: 8s
      - display.page.show: page_idle

  - id: display_ko
    then:
      - display.page.show: page_ko
      - delay: 8s
      - display.page.show: page_idle
  
# ---------------- LED verde e rosso ----------------
light:
  - platform: binary
    id: led_verde
    name: "LED Verde RFID"
    output: led_verde_pin

  - platform: binary
    id: led_rosso
    name: "LED Rosso RFID"
    output: led_rosso_pin

# ---------------- RELÈ ----------------
switch:
  - platform: gpio
    id: rele
    name: "Relè Cancello"
    pin:
      number: 32
      inverted: true
    restore_mode: ALWAYS_ON

  - platform: gpio
    id: buzzer
    name: "Buzzer RFID"
    pin:
      number: 26
    restore_mode: ALWAYS_OFF

# ---------------- SENSORI RFID ----------------
text_sensor:
  - platform: template
    id: ultimo_tag
    name: "Ultimo TAG RFID"
  - platform: wifi_info
    ip_address:
      name: "ESP32 IP Address"
    ssid:
      name: "ESP32 WiFi SSID"
  - platform: version
    name: "Firmware ESPHome"           

button:
  - platform: restart
    id: riavvia_esp
    name: "Riavvia Rfid ESP32"  

  - platform: template
    id: apri_cancello
    name: "Apri Cancello"
    on_press:
      - switch.turn_off: rele
      - delay: 2s
      - switch.turn_on: rele 

      - switch.turn_on: buzzer
      - delay: 120ms
      - switch.turn_off: buzzer
      - delay: 120ms
      - switch.turn_on: buzzer
      - delay: 120ms
      - switch.turn_off: buzzer
      - delay: 120ms
      - switch.turn_on: buzzer
      - delay: 120ms
      - switch.turn_off: buzzer

      - light.turn_on: led_verde
      - delay: 7s
      - light.turn_off: led_verde 
          
binary_sensor:
  - platform: template
    id: accesso_consentito
    name: "Accesso RFID Consentito"

  - platform: template
    id: accesso_negato
    name: "Accesso RFID Negato"

# ---------------- OLED SSD1306 ----------------
i2c:
  sda: 21
  scl: 17
  scan: false
  frequency: 100kHz

font:
  - file: "gfonts://Roboto"
    id: font1
    size: 20

display:
  - platform: ssd1306_i2c
    model: "SSD1306 128x64"
    address: 0x3C
    id: oled
    update_interval: 500ms
    pages:
      - id: page_idle
        lambda: |-
          it.printf(0, 0, id(font1), "RFID");
          it.printf(0, 28, id(font1), "READY");

      - id: page_ok
        lambda: |-
          it.printf(0, 0, id(font1), "ACCESSO");
          it.printf(0, 28, id(font1), "CONSENTITO");

      - id: page_ko
        lambda: |-
          it.printf(0, 0, id(font1), "ACCESSO");
          it.printf(0, 28, id(font1), "NEGATO");

sensor:
  - platform: uptime
    name: Uptime Sensor
    filters:
      - lambda: return x / 3600;
    unit_of_measurement: "h"
    accuracy_decimals: 2

Come ho stimato il SOC di una batteria LiFePO₄ con ESP32, Victron e ESPHome

In questo articolo ti porto dentro, passo passo, quello che abbiamo fatto insieme: stimare lo stato di carica (SOC) di una batteria LiFePO₄ da 51,2 V usando solo:

  • un ESP32
  • un Victron SmartSolar (via BLE)
  • ESPHome
  • e… un po’ di logica nel codice YAML 😄

Niente BMS con BLE, niente dati “magici” dalla batteria: 👉 il SOC lo ricaviamo noi, a partire dalla tensione.

1. Cos’è il SOC e perché non lo leggiamo dalla batteria

Lo stato di carica (SOC) è, in pratica, “quanta energia c’è ancora dentro la batteria”, espresso in percentuale:

  • 100% → batteria piena
  • 0% → batteria scarica

Nel nostro caso:

  • la batteria è una LiFePO₄ DCHOUSE 51,2 V
  • non abbiamo un collegamento diretto al suo BMS
  • quindi non possiamo leggere il SOC via BLE o CAN

Però abbiamo due cose fondamentali:

  1. La tensione della batteria (letta dal Victron via BLE)
  2. Lo stato del regolatore MPPT (Bulk, Absorption, Float, ecc.)

Da qui nasce l’idea:

“Se conosco la tensione e so in che stato è il caricatore, posso stimare il SOC.”

2. L’idea di base: SOC da tensione

Le batterie LiFePO₄ hanno una curva tensione ↔ SOC abbastanza nota. In pratica, a batteria a riposo:

  • sopra una certa tensione → sei vicino al 100%
  • sotto una certa tensione → sei vicino allo 0%
  • in mezzo → c’è una curva abbastanza piatta ma comunque mappabile

Quello che abbiamo fatto è:

  • definire una serie di punti di riferimento:textv_points = {54.4, 53.6, 53.1, 52.8, 52.3, 52.0, 51.5, 51.2, 48.0, 40.0} soc_points = {100, 90, 80, 70, 60, 45, 30, 20, 10, 0}
  • prendere la tensione letta dal Victron
  • trovare in quale “tratto” della curva ci troviamo
  • interpolare tra i due punti più vicini per ottenere un SOC “continuo”

3. Il cuore del sistema: il sensore template in ESPHome

Tutto il calcolo del SOC avviene in un sensore template di ESPHome.

Codice YAML del sensore SOC

yaml

esphome:
  name: esphome-web-660f74
  friendly_name: Victron 35
  min_version: 2025.11.0
  name_add_mac_suffix: false

esp32:
  variant: esp32
  framework:
    type: esp-idf

logger:
  level: INFO

api:

ota:
- platform: esphome

wifi:
  networks:
    - ssid: xx
      password:xx

esp32_ble_tracker:

external_components:
  - source: github://Fabian-Schmidt/esphome-victron_ble

victron_ble:
  - id: MySmartSolar
    mac_address: "de630e81b151"
    bindkey: "9d9701c7ec7acd40e063725bc6ce5591"

sensor:
  - platform: victron_ble
    victron_ble_id: MySmartSolar
    name: "Battery Voltage"
    id: battery_voltage
    type: BATTERY_VOLTAGE

  - platform: victron_ble
    victron_ble_id: MySmartSolar
    name: "Battery Current"
    id: battery_current
    type: BATTERY_CURRENT

  - platform: victron_ble
    victron_ble_id: MySmartSolar
    name: "Yield Today"
    type: YIELD_TODAY

  - platform: victron_ble
    victron_ble_id: MySmartSolar
    name: "PV Power"
    id: pv_power
    type: PV_POWER

  - platform: victron_ble
    victron_ble_id: MySmartSolar
    name: "Load Current"
    type: LOAD_CURRENT

  - platform: wifi_signal
    name: "ESP32 WiFi Strength"
    update_interval: 60s

  - platform: integration
    name: "Solar Energy Produced"
    sensor: pv_power
    unit_of_measurement: "Wh"
    time_unit: h
    accuracy_decimals: 2
    state_class: total_increasing
    device_class: energy

  - platform: uptime
    name: Uptime Sensor
    filters:
      - lambda: return x / 3600.0;
    unit_of_measurement: "h"
    accuracy_decimals: 2

  - platform: internal_temperature
    name: "Temperatura interna"
    update_interval: 60s

  # SOC intelligente
  - platform: template
    name: "SOC stimato da tensione"
    id: soc_voltage
    unit_of_measurement: "%"
    accuracy_decimals: 0
    lambda: |-
      float v = id(battery_voltage).state;
      std::string state = id(mppt_state).state;

      // Normalizza in minuscolo
      std::transform(state.begin(), state.end(), state.begin(), ::tolower);

      // 100% SOLO in float
      if (state.find("float") != std::string::npos) {
        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.4) return 99;

      if (v <= 40.0) return 0;

      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;





binary_sensor:
  - platform: victron_ble
    victron_ble_id: MySmartSolar
    name: "MPPT is in Fault state"
    type: DEVICE_STATE_FAULT

  - platform: victron_ble
    victron_ble_id: MySmartSolar
    name: "MPPT has Error"
    type: CHARGER_ERROR

  - platform: victron_ble
    victron_ble_id: MySmartSolar
    name: "MPPT in FLOAT"
    id: mppt_in_float
    type: DEVICE_STATE_FLOAT

text_sensor:
  - platform: victron_ble
    victron_ble_id: MySmartSolar
    name: "MPPT state"
    id: mppt_state
    type: DEVICE_STATE

  - platform: victron_ble
    victron_ble_id: MySmartSolar
    name: "MPPT Error reason"
    type: CHARGER_ERROR

  - platform: wifi_info
    ip_address:
      name: "ESP32 IP Address"
    ssid:
      name: "ESP32 WiFi SSID"

  - platform: version
    name: "Firmware ESPHome"

output:
  - platform: gpio
    pin: 2
    id: led_ident

switch:
  - platform: output
    name: "LED Identificazione ESP32"
    output: led_ident

button:
  - platform: restart
    id: riavvia_esp
    name: "Riavvia V35 ESP32"

i2c:
  sda: 21
  scl: 22
  scan: true

font:
  - file: "gfonts://Roboto"
    id: my_font
    size: 18

globals:
  - id: lcd_page
    type: int
    restore_value: no
    initial_value: '0'

  # FLAG: SOC pronto
  - id: soc_ready
    type: bool
    initial_value: 'false'

interval:
  - interval: 1s
    then:
      - lambda: |-
          if (!id(soc_ready) &&
              id(soc_voltage).state > 0 &&
              id(battery_voltage).state > 0) {
            id(soc_ready) = true;
          }

  - interval: 6s
    then:
      - lambda: |-
          id(lcd_page)++;
          if (id(lcd_page) > 4) id(lcd_page) = 0;

display:
  - platform: ssd1306_i2c
    model: "SSD1306 128x64"
    address: 0x3C
    lambda: |-
      int page = id(lcd_page);

      if (page == 0) {
        it.printf(0, 0, id(my_font), "PV Power:");
        it.printf(0, 20, id(my_font), "%.1f W", id(pv_power).state);
      }

      if (page == 1) {
        it.printf(0, 0, id(my_font), "Batt Volt:");
        it.printf(0, 20, id(my_font), "%.2f V", id(battery_voltage).state);
      }

      if (page == 2) {
        it.printf(0, 0, id(my_font), "Batt Curr:");
        it.printf(0, 20, id(my_font), "%.2f A", id(battery_current).state);
      }

      if (page == 3) {
        it.printf(0, 0, id(my_font), "MPPT State:");
        it.printf(0, 20, id(my_font), "%s", id(mppt_state).state.c_str());
      }

      if (page == 4) {

        if (!id(soc_ready)) {
          it.printf(0, 0, id(my_font), "SOC in attesa...");
          return;
        }

        float soc = id(soc_voltage).state;
        float vbat = id(battery_voltage).state;

        it.printf(0, 0, id(my_font), "SOC Batteria:");
        it.printf(0, 20, id(my_font), "%.0f %%", soc);
        it.printf(0, 40, id(my_font), "%.2f V", vbat);
      }

Cosa fa, in parole semplici

  • Legge la tensione: v = id(battery_voltage).state;
  • Legge lo stato del Victron: state = id(mppt_state).state;
  • Se il Victron è in Float e la tensione è alta → forza 100%
  • Altrimenti:
    • confronta v con i valori in v_points
    • trova il segmento giusto
    • calcola il SOC interpolando tra i due punti più vicini

4. Mostrare il SOC sul display OLED

Una volta calcolato il SOC, lo usiamo nel blocco display: per mostrarlo sul piccolo OLED SSD1306.

Estratto del display (pagina SOC)

yaml

display:
  - platform: ssd1306_i2c
    model: "SSD1306 128x64"
    address: 0x3C
    lambda: |-
      int page = id(lcd_page);

      // ... altre pagine ...

      if (page == 4) {

        if (!id(soc_ready)) {
          it.printf(0, 2, id(my_font), "SOC in attesa...");
          return;
        }

        float soc = id(soc_voltage).state;
        float vbat = id(battery_voltage).state;

        it.printf(0, 2,  id(my_font), "SOC Batteria:");
        it.printf(0, 22, id(my_font), "%.0f %%", soc);
        it.printf(0, 42, id(my_font), "%.2f V", vbat);
      }

Qui succedono tre cose:

  1. Controllo soc_ready: non mostriamo il SOC finché i valori non sono sensati (tensione e SOC > 0).
  2. Lettura del SOC calcolato:cppfloat soc = id(soc_voltage).state;
  3. Stampa su display:
    • prima riga: testo “SOC Batteria:”
    • seconda riga: valore in %
    • terza riga: tensione in V

5. Il ruolo del flag soc_ready

Per evitare di mostrare numeri “a caso” all’avvio, abbiamo introdotto un flag globale:

yaml

globals:
  - id: soc_ready
    type: bool
    initial_value: 'false'

E un interval che lo attiva solo quando i dati sono validi:

yaml

interval:
  - interval: 1s
    then:
      - lambda: |-
          if (!id(soc_ready) &&
              id(soc_voltage).state > 0 &&
              id(battery_voltage).state > 0) {
            id(soc_ready) = true;
          }

Finché soc_ready è false, sul display appare:

cpp

it.printf(0, 2, id(my_font), "SOC in attesa...");

Così l’utente capisce che il sistema sta ancora “stabilizzando” i dati.

6. Limiti e onestà del metodo

È importante essere chiari: questo SOC è una stima, non un valore assoluto da BMS professionale.

Punti di forza

  • Non richiede BMS con BLE
  • Usa solo ciò che già hai: Victron + ESP32
  • È comprensibile, modificabile, documentabile
  • Per uso domestico/fotovoltaico è più che sufficiente

Limiti strutturali

  • Le LiFePO₄ hanno una curva molto piatta: piccole variazioni di tensione possono spostare molto il SOC stimato
  • Sotto carico o in carica, la tensione è “sporcata” da cadute interne
  • Temperatura, invecchiamento, cavi, ecc. possono spostare la curva reale
  • Non c’è conteggio degli Ah (coulomb counting), quindi non è un SOC “da laboratorio”

7. Perché è comunque una soluzione intelligente

Non stiamo fingendo che la batteria “parli”: 👉 siamo noi a ricavare il SOC, usando:

  • la tensione reale del banco
  • lo stato del Victron (Float)
  • una curva tensione ↔ SOC ragionata
  • un po’ di logica in ESPHome

È una soluzione:

  • trasparente (il codice è leggibile)
  • didattica (capisci cosa succede)
  • pratica (funziona davvero sul campo)

Ed è perfetta per chi vuole:

  • monitorare il proprio impianto fotovoltaico
  • capire come si comporta la batteria
  • avere un’indicazione di SOC senza comprare hardware aggiuntivo

📡 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