#include #include #include #include #include #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); } } }