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>
175 lines
6.9 KiB
Markdown
175 lines
6.9 KiB
Markdown
# Debug RS485 — Historique des corrections
|
||
|
||
Ce document trace les problèmes rencontrés avec la communication RS485 Modbus et les modifications apportées pour la faire fonctionner.
|
||
|
||
---
|
||
|
||
## Contexte matériel
|
||
|
||
| Paramètre | Valeur |
|
||
|-----------|--------|
|
||
| Baudrate réel Epever | **115 200** (et non 9 600 par défaut) |
|
||
| UART ESP32 | Serial2 |
|
||
| RX GPIO | 35 (input-only) |
|
||
| TX GPIO | 32 |
|
||
| Adresse esclave | 1 |
|
||
| Mode | RTU, 8N1, half-duplex |
|
||
|
||
> Le baudrate est le premier point de blocage : l'Epever Tracer 4210N peut être configuré à des vitesses non standards. La valeur 115 200 a été découverte grâce à la sonde de boot (`probeRegistreBatterie`).
|
||
|
||
---
|
||
|
||
## Abandon de la librairie ModbusRTU — passage en mode "brut"
|
||
|
||
### Ancienne approche (callbacks, morte mais encore dans le fichier)
|
||
|
||
Le code utilisait la librairie `ModbusRTU` avec une chaîne de callbacks asynchrones :
|
||
|
||
```
|
||
gererModbus()
|
||
└─ mb.readIreg(0x3100, cbPV)
|
||
└─ cbPV → mb.readIreg(0x310C, cbLoad)
|
||
└─ cbLoad → mb.readIreg(0x311A, cbSOC)
|
||
└─ cbSOC → mb.readIreg(0x3200, cbStatus)
|
||
└─ cbStatus → mb.readIreg(0x3300, cbEnergie)
|
||
└─ cbEnergie → mb.readIsts(0x200C, cbJourNuit)
|
||
└─ cbJourNuit → finaliserLecture()
|
||
```
|
||
|
||
**Problèmes :**
|
||
- La lib gérait mal le timing RS485 half-duplex à 115 200 bauds, générant des trames corrompues ou des timeouts
|
||
- Aucun vidage du buffer RX entre requêtes → les octets résiduels polluaient les trames suivantes
|
||
- Pas de diagnostic au boot pour vérifier la communication avant d'entrer dans la boucle
|
||
|
||
Les callbacks `cbPV`, `cbLoad`, `cbSOC`, `cbStatus`, `cbEnergie`, `cbJourNuit` sont **toujours présents dans le fichier mais ne sont plus appelés**. Seuls `mb.begin()` et `mb.master()` restent actifs (initialisation du Serial2 interne à la lib).
|
||
|
||
### Nouvelle approche (Serial2 direct, synchrone)
|
||
|
||
`gererModbus()` appelle `effectuerLectureBruteEpever()` qui effectue toutes les lectures de façon séquentielle via Serial2 direct + CRC Modbus calculé manuellement :
|
||
|
||
```
|
||
effectuerLectureBruteEpever()
|
||
1. lireRegistresBruts(0x3100, 8) → PV + batterie [FC04, fatal]
|
||
2. lireRegistresBruts(0x310C, 5) → load + température [FC04, fatal]
|
||
3. lireRegistresBruts(0x311A, 1) → SOC % [FC04, fatal]
|
||
4. lireRegistresBruts(0x3200, 2) → statut batterie [FC04, fatal]
|
||
5. lireHorlogeEpever() → RTC Epever [FC03, non-fatal]
|
||
6. lireRegistresBruts(0x3302, 18) → énergie kWh [FC04, non-fatal]
|
||
7. lireEntreesDiscretesBrut(0x200C)→ jour/nuit [FC02, non-fatal]
|
||
```
|
||
|
||
---
|
||
|
||
## Corrections des registres
|
||
|
||
### Énergie kWh (base décalée)
|
||
|
||
| | Ancienne version | Nouvelle version |
|
||
|-|-----------------|-----------------|
|
||
| Base de lecture | 0x3300 | **0x3302** |
|
||
| Énergie générée jour | offset 0 (0x3300/01) | offset 2 (0x3304/05) |
|
||
| Énergie générée total | offset 6 (0x3306/07) | offset 8 (0x330A/0B) |
|
||
| Énergie consommée jour | offset 8 (0x3308/09) | offset 10 (0x330C/0D) |
|
||
| Énergie consommée total | offset 14 (0x330E/0F) | offset 16 (0x3312/13) |
|
||
|
||
Source de référence : `MODBUS-Protocol-v25.pdf` (tableau des registres input 0x3302 à 0x3313).
|
||
|
||
### Puissance sortie de charge (32 bits)
|
||
|
||
L'ancien callback lisait `bufLoad[2] * 0.01f` (16 bits uniquement, registre 0x310E seul).
|
||
La puissance est en réalité sur **32 bits** (0x310E = word L, 0x310F = word H) :
|
||
|
||
```cpp
|
||
// Avant
|
||
state.loadPower = bufLoad[2] * 0.01f;
|
||
|
||
// Après
|
||
state.loadPower = u32x100(load, 2); // (reg[3] << 16 | reg[2]) * 0.01f
|
||
```
|
||
|
||
### Détection jour/nuit (logique hybride)
|
||
|
||
Le registre FC02 `0x200C` retourne `1 = Nuit, 0 = Jour`. Mais si l'Epever renvoie nuit alors que le panneau produit clairement (PV > 2 V), l'état est incohérent côté UI.
|
||
|
||
```cpp
|
||
// Avant (simple inversion)
|
||
state.sun = !bufJourNuit[0];
|
||
|
||
// Après (hybride : registre + tension PV)
|
||
state.sun = !nuit || state.pv > 2.0f;
|
||
```
|
||
|
||
---
|
||
|
||
## Mécanismes de robustesse ajoutés
|
||
|
||
### `viderRx()` — purge systématique du buffer avant chaque trame
|
||
|
||
Entre deux requêtes Modbus, des octets parasites peuvent s'accumuler dans le FIFO Serial2 (echos, bruit, réponse tardive). Sans purge, ces octets polluent la réponse suivante et génèrent des CRC invalides.
|
||
|
||
```cpp
|
||
static void viderRx(const char *raison) {
|
||
// lit et logue tous les octets résiduels présents avant l'envoi
|
||
}
|
||
```
|
||
|
||
Appelé en tête de chaque fonction de lecture brute.
|
||
|
||
### `probeRegistreBatterie()` — sonde de boot RS485
|
||
|
||
Au démarrage, avant d'entrer dans la boucle normale, le code envoie une trame Modbus manuelle FC04 sur le registre 0x3104 (tension batterie) et vérifie :
|
||
- réception d'octets
|
||
- adresse esclave correcte
|
||
- CRC valide
|
||
- valeur de tension cohérente
|
||
|
||
Si le baudrate principal échoue, la sonde retente à 9 600 et 115 200.
|
||
|
||
### CRC Modbus calculé et vérifié manuellement
|
||
|
||
```cpp
|
||
static uint16_t crc16Modbus(const uint8_t *buf, size_t len);
|
||
```
|
||
|
||
Vérifié sur chaque trame reçue avant d'extraire les valeurs. En cas d'échec CRC, la trame est rejetée, l'erreur est comptée, et le cycle suivant repart proprement.
|
||
|
||
### Lectures non-fatales (énergie, RTC, jour/nuit)
|
||
|
||
Certaines lectures peuvent légitimement échouer (registres optionnels, timing serré) sans invalider les données essentielles. Le cycle continue et conserve les dernières valeurs valides :
|
||
|
||
- Énergie (0x3302) : non-fatal, conserve le dernier kWh connu
|
||
- Jour/nuit (FC02 0x200C) : non-fatal, fallback sur `state.pv > 2.0f`
|
||
- Horloge Epever (0x9013) : non-fatal, `epeverClockOk = false`
|
||
|
||
### Synchronisation RTC ESP32 depuis l'Epever
|
||
|
||
```cpp
|
||
static void calerHorlogeEspDepuisEpever();
|
||
```
|
||
|
||
Après lecture des holding registers 0x9013–0x9015 (secondes/minutes/heures/jour/mois/année), l'heure système ESP32 est calée via `settimeofday()`. Rafraîchissement toutes les 6h (`INTERVALLE_SYNC_RTC`).
|
||
|
||
---
|
||
|
||
## Fonctions FC couvertes
|
||
|
||
| Fonction Modbus | Usage |
|
||
|----------------|-------|
|
||
| FC02 (Read Discrete Inputs) | Registre 0x200C — état jour/nuit |
|
||
| FC03 (Read Holding Registers) | Registres 0x9013–0x9015 — horloge RTC |
|
||
| FC04 (Read Input Registers) | PV, batterie, load, SOC, statut, énergie |
|
||
| FC16 (Write Multiple Holding Registers) | Réglage horloge RTC Epever |
|
||
|
||
---
|
||
|
||
## Résumé des points de blocage résolus
|
||
|
||
| Problème | Cause | Correction |
|
||
|---------|-------|-----------|
|
||
| Aucune réponse RS485 | Baudrate 9600 au lieu de 115200 | `MODBUS_BAUDRATE 115200` + sonde boot |
|
||
| Trames corrompues | Buffer RX pollué entre requêtes | `viderRx()` systématique |
|
||
| Timeouts aléatoires | Lib ModbusRTU inadaptée au timing | Passage en Serial2 direct |
|
||
| Énergie kWh à zéro | Mauvaise base de registre (0x3300 vs 0x3302) | Correction offsets selon PDF |
|
||
| Puissance load erronée | Lecture 16 bits au lieu de 32 bits | `u32x100()` sur 0x310E/0F |
|
||
| État soleil incohérent | FC02 seul, pas de fallback PV | Logique hybride `!nuit \|\| pv > 2V` |
|