Files
kc868-a2_solar/src/modbus_epever.cpp
T
gilles a8f0d6ccba Initial commit — KC868-A2 contrôleur solaire ESP32
Fonctionnalités :
- Lecture RS485 Modbus Epever Tracer 4210N (115200 bps, FC03/FC04/FC16)
- Moteur de règles JSON (LittleFS) — commande automatique des relais
- Interface web mobile-first (dashboard, règles, config, historique, EPEVER, debug)
- WiFi AP+STA simultanés avec reconnexion automatique et portail captif
- mDNS configurable (pv.local par défaut)
- Configuration registres EPEVER depuis l'UI (18 registres holding)
- Historique basse/haute résolution avec graphes canvas
- VPN WireGuard optionnel (désactivé par défaut, config via UI)
- OTA firmware + filesystem via ElegantOTA
- Deep sleep / économie d'énergie

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 19:25:01 +02:00

758 lines
27 KiB
C++

#include <ModbusRTU.h>
#include <Arduino.h>
#include <Preferences.h>
#include <time.h>
#include <sys/time.h>
#include "config.h"
#include "state.h"
#include "debug_log.h"
static ModbusRTU mb;
static uint32_t intervalleJour = INTERVALLE_MODBUS; // ms — mode soleil
static uint32_t intervalleNuit = 30000UL; // ms — mode veille
static uint32_t intervalCourant() {
if (state.last_update == 0) return intervalleJour; // première lecture toujours rapide
return state.sun ? intervalleJour : intervalleNuit;
}
void setIntervallesModbus(uint32_t jour_ms, uint32_t nuit_ms) {
intervalleJour = jour_ms;
intervalleNuit = nuit_ms;
Preferences p; p.begin("modbus", false);
p.putUInt("jour", jour_ms);
p.putUInt("nuit", nuit_ms);
p.end();
Serial.printf("[Modbus] Intervalles — jour:%ums nuit:%ums\n", jour_ms, nuit_ms);
}
void getIntervallesModbus(uint32_t &jour_ms, uint32_t &nuit_ms) {
jour_ms = intervalleJour;
nuit_ms = intervalleNuit;
}
// Buffers de réception — un par groupe de registres
static uint16_t bufPV[8]; // 0x3100..0x3107 : PV, batterie, courant/puissance charge
static uint16_t bufLoad[5]; // 0x310C..0x3110 : load + température batterie
static uint16_t bufSOC[1]; // 0x311A : SOC %
static uint16_t bufStatus[2]; // 0x3200 : Battery status | 0x3201 : Charging status
static uint16_t bufEnergie[16]; // 0x3304..0x3313 : kWh consommés/générés
static bool bufJourNuit[1]; // 0x200C : jour/nuit (FC02 discrete input)
static unsigned long tDerniereLecture = 0;
static unsigned long tDebutRequete = 0;
static bool lectureEnCours = false;
static uint32_t nbLecturesOK = 0;
static uint32_t nbErreurs = 0;
static uint8_t derniereErreur = 0;
static const char *derniereEtape = "aucune";
static unsigned long tDerniereSyncRtc = 0;
static bool rtcSyncedOnce = false;
static const unsigned long INTERVALLE_SYNC_RTC = 21600000UL; // 6h
static bool dernierSun = false;
static bool dernierSunValide = false;
static void finaliserLecture();
static uint16_t crc16Modbus(const uint8_t *buf, size_t len) {
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < len; i++) {
crc ^= buf[i];
for (uint8_t bit = 0; bit < 8; bit++) {
crc = (crc & 1) ? (crc >> 1) ^ 0xA001 : crc >> 1;
}
}
return crc;
}
static void dumpHex(const char *prefix, const uint8_t *buf, size_t len) {
String ligne;
ligne.reserve(40 + len * 3);
ligne += prefix;
ligne += " (";
ligne += (unsigned)len;
ligne += " octets):";
for (size_t i = 0; i < len; i++) {
char hex[4];
snprintf(hex, sizeof(hex), " %02X", buf[i]);
ligne += hex;
}
Serial.println(ligne);
debugLogLine(ligne.c_str());
}
static void viderRx(const char *raison) {
uint8_t buf[MODBUS_DEBUG_RX_MAX];
size_t n = 0;
while (Serial2.available() && n < sizeof(buf)) {
buf[n++] = (uint8_t)Serial2.read();
}
while (Serial2.available()) Serial2.read();
if (n > 0) {
debugLogf("[Modbus][debug] RX vidé avant %s", raison);
dumpHex("[Modbus][debug] octets parasites", buf, n);
}
}
static bool probeRegistreBatterie(uint32_t baudrate) {
#if MODBUS_DEBUG_BOOT
debugLogf("[Modbus][probe] Test direct 0x3104 à %u bauds, esclave %d",
baudrate, MODBUS_ADRESSE);
Serial2.end();
delay(20);
Serial2.begin(baudrate, SERIAL_8N1, PIN_RS485_RX, PIN_RS485_TX);
delay(50);
viderRx("probe");
uint8_t req[8] = {
MODBUS_ADRESSE,
0x04,
0x31, 0x04,
0x00, 0x01,
0x00, 0x00
};
uint16_t crc = crc16Modbus(req, 6);
req[6] = crc & 0xFF;
req[7] = crc >> 8;
dumpHex("[Modbus][probe] TX", req, sizeof(req));
Serial2.write(req, sizeof(req));
Serial2.flush();
uint8_t resp[MODBUS_DEBUG_RX_MAX];
size_t n = 0;
unsigned long t0 = millis();
unsigned long dernierOctet = t0;
while ((millis() - t0) < 700 && n < sizeof(resp)) {
while (Serial2.available() && n < sizeof(resp)) {
resp[n++] = (uint8_t)Serial2.read();
dernierOctet = millis();
}
if (n >= 7 && (millis() - dernierOctet) > 20) break;
delay(1);
}
if (n == 0) {
debugLogf("[Modbus][probe] Aucun octet reçu à %u bauds", baudrate);
debugLogf("[Modbus][probe] Causes probables: A/B inversés, GND absent, mauvais baudrate, mauvais ID, Epever non alimenté.");
return false;
}
dumpHex("[Modbus][probe] RX", resp, n);
if (n < 5) {
debugLogf("[Modbus][probe] Réponse trop courte: bruit RS485 ou baudrate incorrect probable.");
return false;
}
if (resp[0] != MODBUS_ADRESSE) {
debugLogf("[Modbus][probe] Adresse inattendue: reçu %u, attendu %u",
resp[0], MODBUS_ADRESSE);
return false;
}
if (resp[1] & 0x80) {
debugLogf("[Modbus][probe] Exception Modbus fonction 0x%02X code 0x%02X",
resp[1], n > 2 ? resp[2] : 0);
return false;
}
if (resp[1] != 0x04 || resp[2] != 0x02 || n < 7) {
debugLogf("[Modbus][probe] Format inattendu pour lecture input register 0x3104.");
return false;
}
uint16_t crcCalc = crc16Modbus(resp, 5);
uint16_t crcRx = (uint16_t)resp[5] | ((uint16_t)resp[6] << 8);
if (crcCalc != crcRx) {
debugLogf("[Modbus][probe] CRC invalide: calcul 0x%04X, reçu 0x%04X", crcCalc, crcRx);
return false;
}
uint16_t brut = ((uint16_t)resp[3] << 8) | resp[4];
debugLogf("[Modbus][probe] OK à %u bauds: batterie %.2f V (brut 0x%04X)",
baudrate, brut * 0.01f, brut);
return true;
#else
(void)baudrate;
return false;
#endif
}
static bool lireRegistresBrutsFc(uint8_t fonction, uint16_t registre, uint16_t quantite,
uint16_t *dest, uint16_t timeoutMs, bool logSucces = false) {
if (quantite == 0 || quantite > 24) return false;
viderRx("lecture brute");
uint8_t req[8] = {
MODBUS_ADRESSE,
fonction,
(uint8_t)(registre >> 8), (uint8_t)(registre & 0xFF),
(uint8_t)(quantite >> 8), (uint8_t)(quantite & 0xFF),
0x00, 0x00
};
uint16_t crc = crc16Modbus(req, 6);
req[6] = crc & 0xFF;
req[7] = crc >> 8;
Serial2.write(req, sizeof(req));
Serial2.flush();
const size_t attendu = 5 + (size_t)quantite * 2;
uint8_t resp[MODBUS_DEBUG_RX_MAX];
size_t n = 0;
unsigned long t0 = millis();
unsigned long dernierOctet = t0;
while ((millis() - t0) < timeoutMs && n < sizeof(resp)) {
while (Serial2.available() && n < sizeof(resp)) {
resp[n++] = (uint8_t)Serial2.read();
dernierOctet = millis();
}
if (n >= attendu && (millis() - dernierOctet) > 5) break;
delay(1);
}
if (n == 0) {
nbErreurs++;
derniereErreur = 0xE0;
derniereEtape = "Brut";
debugLogf("[Modbus][brut] Timeout FC%02u registre 0x%04X, aucun octet reçu", fonction, registre);
dumpHex("[Modbus][brut] TX", req, sizeof(req));
return false;
}
if (n < attendu) {
nbErreurs++;
derniereErreur = 0xE2;
derniereEtape = "Brut court";
debugLogf("[Modbus][brut] Réponse courte FC%02u registre 0x%04X: reçu %u, attendu %u",
fonction, registre, (unsigned)n, (unsigned)attendu);
dumpHex("[Modbus][brut] TX", req, sizeof(req));
dumpHex("[Modbus][brut] RX", resp, n);
return false;
}
uint16_t crcCalc = crc16Modbus(resp, attendu - 2);
uint16_t crcRx = (uint16_t)resp[attendu - 2] | ((uint16_t)resp[attendu - 1] << 8);
if (crcCalc != crcRx) {
nbErreurs++;
derniereErreur = 0xE1;
derniereEtape = "Brut CRC";
debugLogf("[Modbus][brut] CRC invalide FC%02u registre 0x%04X: calcul 0x%04X, reçu 0x%04X",
fonction, registre, crcCalc, crcRx);
dumpHex("[Modbus][brut] TX", req, sizeof(req));
dumpHex("[Modbus][brut] RX", resp, n);
return false;
}
if (resp[0] != MODBUS_ADRESSE || resp[1] != fonction || resp[2] != quantite * 2) {
nbErreurs++;
derniereErreur = resp[1];
derniereEtape = "Brut format";
debugLogf("[Modbus][brut] Format inattendu FC%02u registre 0x%04X", fonction, registre);
dumpHex("[Modbus][brut] TX", req, sizeof(req));
dumpHex("[Modbus][brut] RX", resp, n);
return false;
}
for (uint16_t i = 0; i < quantite; i++) {
dest[i] = ((uint16_t)resp[3 + i * 2] << 8) | resp[4 + i * 2];
}
if (logSucces) {
debugLogf("[Modbus][brut] OK FC%02u registre 0x%04X x%u", fonction, registre, quantite);
}
return true;
}
static bool lireRegistresBruts(uint16_t registre, uint16_t quantite, uint16_t *dest,
uint16_t timeoutMs, bool logSucces = false) {
return lireRegistresBrutsFc(0x04, registre, quantite, dest, timeoutMs, logSucces);
}
static bool lireHoldingBruts(uint16_t registre, uint16_t quantite, uint16_t *dest,
uint16_t timeoutMs, bool logSucces = false) {
return lireRegistresBrutsFc(0x03, registre, quantite, dest, timeoutMs, logSucces);
}
static bool ecrireHoldingMultiplesBrut(uint16_t registre, uint16_t quantite,
const uint16_t *valeurs, uint16_t timeoutMs) {
if (quantite == 0 || quantite > 12) return false;
viderRx("écriture holding brute");
uint8_t req[MODBUS_DEBUG_RX_MAX];
size_t len = 7 + (size_t)quantite * 2 + 2;
if (len > sizeof(req)) return false;
req[0] = MODBUS_ADRESSE;
req[1] = 0x10;
req[2] = registre >> 8;
req[3] = registre & 0xFF;
req[4] = quantite >> 8;
req[5] = quantite & 0xFF;
req[6] = quantite * 2;
for (uint16_t i = 0; i < quantite; i++) {
req[7 + i * 2] = valeurs[i] >> 8;
req[8 + i * 2] = valeurs[i] & 0xFF;
}
uint16_t crc = crc16Modbus(req, len - 2);
req[len - 2] = crc & 0xFF;
req[len - 1] = crc >> 8;
dumpHex("[Modbus][write] TX", req, len);
Serial2.write(req, len);
Serial2.flush();
uint8_t resp[8];
size_t n = 0;
unsigned long t0 = millis();
while ((millis() - t0) < timeoutMs && n < sizeof(resp)) {
while (Serial2.available() && n < sizeof(resp)) resp[n++] = (uint8_t)Serial2.read();
if (n >= 8) break;
delay(1);
}
if (n < 8) {
debugLogf("[Modbus][write] Réponse courte FC16 registre 0x%04X: %u octets", registre, (unsigned)n);
if (n) dumpHex("[Modbus][write] RX", resp, n);
return false;
}
uint16_t crcCalc = crc16Modbus(resp, 6);
uint16_t crcRx = (uint16_t)resp[6] | ((uint16_t)resp[7] << 8);
bool ok = resp[0] == MODBUS_ADRESSE && resp[1] == 0x10 &&
resp[2] == (registre >> 8) && resp[3] == (registre & 0xFF) &&
resp[4] == (quantite >> 8) && resp[5] == (quantite & 0xFF) &&
crcCalc == crcRx;
dumpHex("[Modbus][write] RX", resp, n);
debugLogf("[Modbus][write] FC16 0x%04X x%u -> %s", registre, quantite, ok ? "OK" : "ERREUR");
return ok;
}
static bool lireEntreesDiscretesBrut(uint16_t adresse, bool *dest, uint16_t timeoutMs) {
viderRx("lecture discrete brute");
uint8_t req[8] = {
MODBUS_ADRESSE,
0x02,
(uint8_t)(adresse >> 8), (uint8_t)(adresse & 0xFF),
0x00, 0x01,
0x00, 0x00
};
uint16_t crc = crc16Modbus(req, 6);
req[6] = crc & 0xFF;
req[7] = crc >> 8;
Serial2.write(req, sizeof(req));
Serial2.flush();
uint8_t resp[8];
size_t n = 0;
unsigned long t0 = millis();
while ((millis() - t0) < timeoutMs && n < 6) {
while (Serial2.available() && n < sizeof(resp)) resp[n++] = (uint8_t)Serial2.read();
if (n >= 6) break;
delay(1);
}
if (n < 6) {
debugLogf("[Modbus][brut] Jour/nuit 0x%04X ignoré: pas de réponse FC02", adresse);
return false;
}
uint16_t crcCalc = crc16Modbus(resp, 4);
uint16_t crcRx = (uint16_t)resp[4] | ((uint16_t)resp[5] << 8);
if (resp[0] != MODBUS_ADRESSE || resp[1] != 0x02 || resp[2] != 1 || crcCalc != crcRx) {
debugLogf("[Modbus][brut] Jour/nuit 0x%04X ignoré: format/CRC invalide", adresse);
dumpHex("[Modbus][brut] RX FC02", resp, n);
return false;
}
*dest = (resp[3] & 0x01) != 0;
debugLogf("[Modbus][brut] FC02 0x%04X = %u", adresse, *dest ? 1 : 0);
return true;
}
static float u32x100(const uint16_t *reg, uint8_t indexL) {
// Le PDF EPEVER indique les 32 bits en deux registres: L puis H.
return (((uint32_t)reg[indexL + 1] << 16) | reg[indexL]) * 0.01f;
}
static void calerHorlogeEspDepuisEpever() {
struct tm tmRtc = {};
tmRtc.tm_year = state.epeverYear - 1900;
tmRtc.tm_mon = state.epeverMonth - 1;
tmRtc.tm_mday = state.epeverDay;
tmRtc.tm_hour = state.epeverHour;
tmRtc.tm_min = state.epeverMinute;
tmRtc.tm_sec = state.epeverSecond;
time_t epoch = mktime(&tmRtc);
if (epoch <= 0) {
state.espClockOk = false;
debugLogf("[RTC] Impossible de convertir l'heure Epever en epoch");
return;
}
timeval tv = { epoch, 0 };
settimeofday(&tv, nullptr);
state.espClockOk = true;
debugLogf("[RTC] Horloge ESP32 calée depuis Epever");
}
static void lireHorlogeEpever(bool force) {
unsigned long maintenant = millis();
if (!force && rtcSyncedOnce && (maintenant - tDerniereSyncRtc) < INTERVALLE_SYNC_RTC) return;
uint16_t rtc[3];
if (!lireHoldingBruts(0x9013, 3, rtc, 700, true)) {
state.epeverClockOk = false;
debugLogf("[Modbus][rtc] Horloge Epever indisponible");
return;
}
state.epeverSecond = rtc[0] & 0xFF;
state.epeverMinute = (rtc[0] >> 8) & 0xFF;
state.epeverHour = rtc[1] & 0xFF;
state.epeverDay = (rtc[1] >> 8) & 0xFF;
state.epeverMonth = rtc[2] & 0xFF;
state.epeverYear = 2000 + ((rtc[2] >> 8) & 0xFF);
bool valide = state.epeverSecond < 60 && state.epeverMinute < 60 &&
state.epeverHour < 24 && state.epeverDay >= 1 &&
state.epeverDay <= 31 && state.epeverMonth >= 1 &&
state.epeverMonth <= 12;
state.epeverClockOk = valide;
debugLogf("[Modbus][rtc] %04u-%02u-%02u %02u:%02u:%02u (%s)",
state.epeverYear, state.epeverMonth, state.epeverDay,
state.epeverHour, state.epeverMinute, state.epeverSecond,
valide ? "OK" : "invalide");
if (valide) {
tDerniereSyncRtc = maintenant;
rtcSyncedOnce = true;
calerHorlogeEspDepuisEpever();
}
}
static void enregistrerChangementSoleil(bool nouveauSun) {
if (!dernierSunValide) {
dernierSun = nouveauSun;
dernierSunValide = true;
return;
}
if (nouveauSun == dernierSun) return;
dernierSun = nouveauSun;
uint8_t idx = state.sunHistoryHead;
state.sunHistoryState[idx] = nouveauSun;
if (state.espClockOk) {
time_t now = time(nullptr);
struct tm tmNow;
localtime_r(&now, &tmNow);
snprintf(state.sunHistoryTime[idx], sizeof(state.sunHistoryTime[idx]),
"%04d-%02d-%02d %02d:%02d:%02d",
tmNow.tm_year + 1900, tmNow.tm_mon + 1, tmNow.tm_mday,
tmNow.tm_hour, tmNow.tm_min, tmNow.tm_sec);
} else if (state.epeverClockOk) {
snprintf(state.sunHistoryTime[idx], sizeof(state.sunHistoryTime[idx]),
"%04u-%02u-%02u %02u:%02u:%02u",
state.epeverYear, state.epeverMonth, state.epeverDay,
state.epeverHour, state.epeverMinute, state.epeverSecond);
} else {
snprintf(state.sunHistoryTime[idx], sizeof(state.sunHistoryTime[idx]),
"uptime %lus", millis() / 1000);
}
state.sunHistoryHead = (state.sunHistoryHead + 1) % 5;
if (state.sunHistoryCount < 5) state.sunHistoryCount++;
state.sunHistoryValid = true;
debugLogf("[SUN] Changement état -> %s à %s",
nouveauSun ? "JOUR" : "NUIT", state.sunHistoryTime[idx]);
}
static bool effectuerLectureBruteEpever() {
uint16_t pv[8];
uint16_t load[5];
uint16_t soc[1];
uint16_t status[2];
uint16_t energie[18];
bool nuit = false;
debugLogf("[Modbus][brut] Début cycle lecture Epever");
if (!lireRegistresBruts(0x3100, 8, pv, 700, true)) return false;
if (!lireRegistresBruts(0x310C, 5, load, 700, true)) return false;
if (!lireRegistresBruts(0x311A, 1, soc, 700, true)) return false;
if (!lireRegistresBruts(0x3200, 2, status, 700, true)) return false;
lireHorlogeEpever(false);
if (lireRegistresBruts(0x3302, 18, energie, 900, false)) {
// Base 0x3302 selon MODBUS-Protocol-v25.pdf:
// 0x3304/05 conso jour, 0x330A/0B conso totale,
// 0x330C/0D production jour, 0x3312/13 production totale.
state.energieConJour = u32x100(energie, 2);
state.energieConTotal = u32x100(energie, 8);
state.energieGenJour = u32x100(energie, 10);
state.energieGenTotal = u32x100(energie, 16);
debugLogf("[Modbus][brut] Energie: genJ=%.2fkWh consoJ=%.2fkWh genTot=%.2fkWh consoTot=%.2fkWh",
state.energieGenJour, state.energieConJour,
state.energieGenTotal, state.energieConTotal);
} else {
debugLogf("[Modbus][brut] Energie ignorée, les valeurs précédentes sont conservées");
}
state.pv = pv[0] * 0.01f;
state.pvCurrent = pv[1] * 0.01f;
state.battery = pv[4] * 0.01f;
state.loadVoltage = load[0] * 0.01f;
state.loadCurrent = load[1] * 0.01f;
state.loadPower = u32x100(load, 2); // 0x310E/0x310F L/H
state.batTemperature = (int16_t)load[4] * 0.01f;
state.batSOC = (uint8_t)constrain((int)soc[0], 0, 100);
uint8_t batVoltStatus = status[0] & 0x0F;
state.batSousVoltage = (batVoltStatus == 2);
state.batSurVoltage = (batVoltStatus == 1);
state.batStatut = (status[1] >> 2) & 0x03;
if (lireEntreesDiscretesBrut(0x200C, &nuit, 500)) {
// Le registre officiel dit 1=Nuit, 0=Jour. Si le PV est clairement
// présent, on force jour pour éviter un état incohérent côté UI.
state.sun = !nuit || state.pv > 2.0f;
} else {
state.sun = state.pv > 2.0f;
}
enregistrerChangementSoleil(state.sun);
debugLogf("[Modbus][brut] Bruts: 3110(temp)=0x%04X 311A(SOC)=%u 3200=0x%04X 3201=0x%04X sun=%u",
load[4], soc[0], status[0], status[1], state.sun ? 1 : 0);
finaliserLecture();
return true;
}
bool reglerHorlogeEpever(uint16_t annee, uint8_t mois, uint8_t jour,
uint8_t heure, uint8_t minute, uint8_t seconde) {
if (lectureEnCours) {
debugLogf("[Modbus][rtc] Réglage refusé: cycle lecture en cours");
return false;
}
if (annee < 2000 || annee > 2099 || mois < 1 || mois > 12 || jour < 1 || jour > 31 ||
heure > 23 || minute > 59 || seconde > 59) {
debugLogf("[Modbus][rtc] Réglage refusé: date/heure invalide");
return false;
}
uint16_t regs[3];
regs[0] = ((uint16_t)minute << 8) | seconde; // 0x9013
regs[1] = ((uint16_t)jour << 8) | heure; // 0x9014
regs[2] = ((uint16_t)(annee - 2000) << 8) | mois; // 0x9015
lectureEnCours = true;
bool ok = ecrireHoldingMultiplesBrut(0x9013, 3, regs, 1000);
lectureEnCours = false;
if (ok) {
rtcSyncedOnce = false;
lireHorlogeEpever(true);
}
return ok;
}
static void debugBootModbus() {
#if MODBUS_DEBUG_BOOT
debugLogf("--- Diagnostic RS485 boot ---");
debugLogf(" UART ESP32 : Serial2");
debugLogf(" RX GPIO : %d", PIN_RS485_RX);
debugLogf(" TX GPIO : %d", PIN_RS485_TX);
debugLogf(" Adresse Epever : %d", MODBUS_ADRESSE);
debugLogf(" Baud principal : %u", (uint32_t)MODBUS_BAUDRATE);
debugLogf(" Trame test : FC04 registre 0x3104 tension batterie");
debugLogf(" Rappel câblage : Epever A/D+ vers KC868 A, Epever B/D- vers KC868 B, GND recommandé");
bool okPrincipal = probeRegistreBatterie(MODBUS_BAUDRATE);
if (!okPrincipal && MODBUS_BAUDRATE != 9600) probeRegistreBatterie(9600);
if (!okPrincipal && MODBUS_BAUDRATE != 115200) probeRegistreBatterie(115200);
debugLogf("--- Fin diagnostic RS485 boot ---");
#endif
}
// Fin de chaîne — appelé après la dernière lecture
static void finaliserLecture() {
state.rs485_ok = true;
state.last_update = millis();
lectureEnCours = false;
nbLecturesOK++;
debugLogf("Modbus OK #%u — Bat:%.2fV %d%% PV:%.2fV %.2fA Load:%.1fW %s",
nbLecturesOK,
state.battery, state.batSOC, state.pv, state.pvCurrent,
state.loadPower, state.sun ? "JOUR" : "NUIT");
}
static const char* codeModbus(uint8_t code) {
switch (code) {
case 0x01: return "Fonction non supportée";
case 0x02: return "Adresse registre invalide";
case 0x03: return "Valeur invalide";
case 0x04: return "Erreur matérielle esclave";
case 0xE0: return "Timeout (pas de réponse)";
case 0xE1: return "CRC invalide";
case 0xE2: return "Exception générale";
default: return "Inconnu";
}
}
static void erreurLecture(const char *etape, uint8_t code) {
state.rs485_ok = false;
lectureEnCours = false;
nbErreurs++;
derniereErreur = code;
derniereEtape = etape;
debugLogf("Modbus ERREUR #%u [%s] code=0x%02X (%s), ok=%u, uptime=%lums",
nbErreurs, etape, code, codeModbus(code), nbLecturesOK, millis());
debugLogf("[Modbus][aide] Si timeout: vérifier A/B, GND, baudrate, ID=%d, RJ45 Epever non branché sur Ethernet.",
MODBUS_ADRESSE);
}
// Chaîne de lectures : PV → Load → SOC → Status → Energie → JourNuit → fin
static bool cbJourNuit(Modbus::ResultCode ev, uint16_t, void*) {
if (ev == Modbus::EX_SUCCESS) {
// 0x200C FC02 : bit D0 = 1 → Nuit, 0 → Jour
state.sun = !bufJourNuit[0];
} else {
Serial.printf("Modbus [JourNuit] : 0x%02X — ignoré\n", ev);
}
finaliserLecture(); // non-fatal : on finalise dans tous les cas
return true;
}
static bool cbEnergie(Modbus::ResultCode ev, uint16_t, void*) {
if (ev == Modbus::EX_SUCCESS) {
// Lecture depuis 0x3300, registres 32 bits little-endian (L word first)
state.energieGenJour = ((uint32_t)bufEnergie[1] << 16 | bufEnergie[0]) * 0.01f; // 0x3300-01
state.energieGenTotal = ((uint32_t)bufEnergie[7] << 16 | bufEnergie[6]) * 0.01f; // 0x3306-07
state.energieConJour = ((uint32_t)bufEnergie[9] << 16 | bufEnergie[8]) * 0.01f; // 0x3308-09
state.energieConTotal = ((uint32_t)bufEnergie[15] << 16 | bufEnergie[14]) * 0.01f; // 0x330E-0F
tDebutRequete = millis();
mb.readIsts(MODBUS_ADRESSE, 0x200C, bufJourNuit, 1, cbJourNuit); // FC02 discrete input
} else {
Serial.printf("Modbus [Energie] : 0x%02X — ignoré\n", ev);
finaliserLecture(); // non-fatal
}
return true;
}
static bool cbStatus(Modbus::ResultCode ev, uint16_t, void*) {
if (ev != Modbus::EX_SUCCESS) { erreurLecture("Status", ev); return true; }
// 0x3200 D3-D0 : 00=Normal, 01=Over voltage, 02=Under voltage, 03=Over discharge
uint8_t batVoltStatus = bufStatus[0] & 0x0F;
state.batSousVoltage = (batVoltStatus == 2);
state.batSurVoltage = (batVoltStatus == 1);
// 0x3201 D3-D2 : 00=No charge, 01=Float, 02=Boost, 03=Equalization
state.batStatut = (bufStatus[1] >> 2) & 0x03;
tDebutRequete = millis();
mb.readIreg(MODBUS_ADRESSE, 0x3300, bufEnergie, 16, cbEnergie);
return true;
}
static bool cbSOC(Modbus::ResultCode ev, uint16_t, void*) {
if (ev != Modbus::EX_SUCCESS) { erreurLecture("SOC", ev); return true; }
state.batSOC = (uint8_t)bufSOC[0];
tDebutRequete = millis();
mb.readIreg(MODBUS_ADRESSE, 0x3200, bufStatus, 2, cbStatus); // 0x3200 + 0x3201
return true;
}
static bool cbLoad(Modbus::ResultCode ev, uint16_t, void*) {
if (ev != Modbus::EX_SUCCESS) { erreurLecture("Load", ev); return true; }
state.loadVoltage = bufLoad[0] * 0.01f; // 0x310C
state.loadCurrent = bufLoad[1] * 0.01f; // 0x310D
state.loadPower = bufLoad[2] * 0.01f; // 0x310E
// bufLoad[3] = 0x310F réservé
state.batTemperature = (int16_t)bufLoad[4] * 0.01f; // 0x3110 signé
tDebutRequete = millis();
mb.readIreg(MODBUS_ADRESSE, 0x311A, bufSOC, 1, cbSOC);
return true;
}
static bool cbPV(Modbus::ResultCode ev, uint16_t, void*) {
if (ev != Modbus::EX_SUCCESS) { erreurLecture("PV", ev); return true; }
state.pv = bufPV[0] * 0.01f; // 0x3100 Tension PV
state.pvCurrent = bufPV[1] * 0.01f; // 0x3101 Courant PV
state.battery = bufPV[4] * 0.01f; // 0x3104 Tension batterie
tDebutRequete = millis();
mb.readIreg(MODBUS_ADRESSE, 0x310C, bufLoad, 5, cbLoad);
return true;
}
void initModbus() {
Preferences p; p.begin("modbus", true);
intervalleJour = p.getUInt("jour", INTERVALLE_MODBUS);
intervalleNuit = p.getUInt("nuit", 30000UL);
p.end();
debugLogf("--- Modbus init ---");
debugLogf(" Adresse esclave : %d", MODBUS_ADRESSE);
debugLogf(" Baud rate : %u", (uint32_t)MODBUS_BAUDRATE);
debugLogf(" TX GPIO : %d", PIN_RS485_TX);
debugLogf(" RX GPIO : %d", PIN_RS485_RX);
debugLogf(" Timeout requête : %d ms", TIMEOUT_MODBUS);
debugLogf(" Intervalle jour : %u ms", intervalleJour);
debugLogf(" Intervalle nuit : %u ms", intervalleNuit);
debugBootModbus();
Serial2.end();
delay(20);
Serial2.begin(MODBUS_BAUDRATE, SERIAL_8N1, PIN_RS485_RX, PIN_RS485_TX);
viderRx("démarrage ModbusRTU");
mb.begin(&Serial2);
mb.master();
debugLogf(" → Serial2 + Modbus master démarrés");
debugLogf("-------------------");
}
// ---------------------------------------------------------------------------
// API partagée pour epever_config.cpp
// ---------------------------------------------------------------------------
bool isModbusBusy() { return lectureEnCours; }
bool modbusAcquire() { if (lectureEnCours) return false; lectureEnCours = true; return true; }
void modbusRelease() { lectureEnCours = false; }
bool modbusLireHolding(uint16_t reg, uint16_t qty, uint16_t *dest, uint16_t timeoutMs) {
return lireHoldingBruts(reg, qty, dest, timeoutMs);
}
bool modbusEcrireHolding(uint16_t reg, uint16_t qty, const uint16_t *vals, uint16_t timeoutMs) {
return ecrireHoldingMultiplesBrut(reg, qty, vals, timeoutMs);
}
void gererModbus() {
unsigned long maintenant = millis();
if (!lectureEnCours && (maintenant - tDerniereLecture) >= intervalCourant()) {
tDerniereLecture = maintenant;
tDebutRequete = maintenant;
lectureEnCours = true;
debugLogf("[Modbus] Début lecture brute — uptime=%lus, baud=%u, ID=%d, erreurs=%u, dernière=%s/0x%02X",
maintenant / 1000, (uint32_t)MODBUS_BAUDRATE, MODBUS_ADRESSE,
nbErreurs, derniereEtape, derniereErreur);
if (!effectuerLectureBruteEpever()) {
state.rs485_ok = false;
lectureEnCours = false;
debugLogf("[Modbus][brut] Cycle échoué — dernière=%s/0x%02X, erreurs=%u",
derniereEtape, derniereErreur, nbErreurs);
}
}
}