a8f0d6ccba
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>
758 lines
27 KiB
C++
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);
|
|
}
|
|
}
|
|
}
|