Files
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

175 lines
6.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 0x90130x9015 (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 0x90130x9015 — 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` |