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>
This commit is contained in:
@@ -0,0 +1,332 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>KC868-A2 — Émulateur QEMU</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0d1117;
|
||||
--surface: #161b22;
|
||||
--border: #30363d;
|
||||
--accent: #e94560;
|
||||
--vert: #00b894;
|
||||
--rouge: #d63031;
|
||||
--jaune: #fdcb6e;
|
||||
--bleu: #74b9ff;
|
||||
--texte: #e6edf3;
|
||||
--muted: #8b949e;
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--texte);
|
||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* === En-tête === */
|
||||
header {
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 0.5rem 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
font-size: 0.85rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
header h1 { font-size: 0.95rem; font-weight: 600; }
|
||||
.badge {
|
||||
padding: 0.15rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.badge-ok { background: var(--vert); color: #000; }
|
||||
.badge-err { background: var(--rouge); color: #fff; }
|
||||
.badge-warn{ background: var(--jaune); color: #000; }
|
||||
.spacer { flex: 1; }
|
||||
|
||||
/* === Layout 3 volets === */
|
||||
.layout {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Volet gauche — état GPIO */
|
||||
.panel-gpio {
|
||||
width: 220px;
|
||||
flex-shrink: 0;
|
||||
background: var(--surface);
|
||||
border-right: 1px solid var(--border);
|
||||
overflow-y: auto;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
.section {
|
||||
font-size: 0.65rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--muted);
|
||||
margin: 0.75rem 0 0.4rem;
|
||||
}
|
||||
.section:first-child { margin-top: 0; }
|
||||
.gpio-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.35rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.gpio-label { color: var(--muted); }
|
||||
.gpio-val { font-weight: 600; font-family: monospace; }
|
||||
.on { color: var(--vert); }
|
||||
.off { color: var(--muted); }
|
||||
.err { color: var(--rouge); }
|
||||
.num { color: var(--bleu); }
|
||||
|
||||
/* Volet droit — webserver + serial */
|
||||
.panel-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
iframe {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: #1a1a2e;
|
||||
}
|
||||
|
||||
/* Terminal série */
|
||||
.serial-bar {
|
||||
height: 36px;
|
||||
background: var(--surface);
|
||||
border-top: 1px solid var(--border);
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 0.75rem;
|
||||
font-size: 0.7rem;
|
||||
color: var(--muted);
|
||||
flex-shrink: 0;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.serial-bar:hover { color: var(--texte); }
|
||||
.terminal {
|
||||
height: 180px;
|
||||
background: #0a0c10;
|
||||
overflow-y: auto;
|
||||
padding: 0.4rem 0.75rem;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 0.72rem;
|
||||
flex-shrink: 0;
|
||||
transition: height 0.2s;
|
||||
}
|
||||
.terminal.collapsed { height: 0; padding: 0; }
|
||||
.log-line { color: #b2bec3; white-space: pre-wrap; line-height: 1.45; }
|
||||
.log-line.warn { color: var(--jaune); }
|
||||
.log-line.err { color: var(--rouge); }
|
||||
.log-line.ok { color: var(--vert); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<h1>⚡ KC868-A2 — Émulateur QEMU ESP32</h1>
|
||||
<span id="badge-esp" class="badge badge-err">ESP32 démarrage…</span>
|
||||
<span id="badge-mb" class="badge badge-err">Modbus --</span>
|
||||
<span class="spacer"></span>
|
||||
<span style="color:var(--muted);font-size:0.7rem">
|
||||
WebServer : <a href="http://localhost:10080" target="_blank"
|
||||
style="color:var(--bleu)">localhost:10080</a>
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div class="layout">
|
||||
|
||||
<!-- Volet gauche : état système -->
|
||||
<div class="panel-gpio">
|
||||
|
||||
<div class="section">RS485 / Modbus</div>
|
||||
<div class="gpio-row">
|
||||
<span class="gpio-label">État</span>
|
||||
<span class="gpio-val" id="g-rs485">--</span>
|
||||
</div>
|
||||
|
||||
<div class="section">Relais</div>
|
||||
<div class="gpio-row">
|
||||
<span class="gpio-label">GPIO15 – Relay 1</span>
|
||||
<span class="gpio-val" id="g-r1">--</span>
|
||||
</div>
|
||||
<div class="gpio-row">
|
||||
<span class="gpio-label">GPIO2 – Relay 2</span>
|
||||
<span class="gpio-val" id="g-r2">--</span>
|
||||
</div>
|
||||
|
||||
<div class="section">Entrées numériques</div>
|
||||
<div class="gpio-row">
|
||||
<span class="gpio-label">GPIO36 – DI1</span>
|
||||
<span class="gpio-val" id="g-di1">--</span>
|
||||
</div>
|
||||
<div class="gpio-row">
|
||||
<span class="gpio-label">GPIO39 – DI2</span>
|
||||
<span class="gpio-val" id="g-di2">--</span>
|
||||
</div>
|
||||
|
||||
<div class="section">Mode</div>
|
||||
<div class="gpio-row">
|
||||
<span class="gpio-label">Contrôle</span>
|
||||
<span class="gpio-val" id="g-mode">--</span>
|
||||
</div>
|
||||
|
||||
<div class="section">Solaire</div>
|
||||
<div class="gpio-row">
|
||||
<span class="gpio-label">Ensoleillement</span>
|
||||
<span class="gpio-val" id="g-sun">--</span>
|
||||
</div>
|
||||
<div class="gpio-row">
|
||||
<span class="gpio-label">Batterie</span>
|
||||
<span class="gpio-val num" id="g-bat">--</span>
|
||||
</div>
|
||||
<div class="gpio-row">
|
||||
<span class="gpio-label">Tension PV</span>
|
||||
<span class="gpio-val num" id="g-pv">--</span>
|
||||
</div>
|
||||
<div class="gpio-row">
|
||||
<span class="gpio-label">Courant PV</span>
|
||||
<span class="gpio-val num" id="g-pvc">--</span>
|
||||
</div>
|
||||
<div class="gpio-row">
|
||||
<span class="gpio-label">SOC</span>
|
||||
<span class="gpio-val num" id="g-soc">--</span>
|
||||
</div>
|
||||
<div class="gpio-row">
|
||||
<span class="gpio-label">Temp. bat.</span>
|
||||
<span class="gpio-val num" id="g-temp">--</span>
|
||||
</div>
|
||||
<div class="gpio-row">
|
||||
<span class="gpio-label">Statut charge</span>
|
||||
<span class="gpio-val" id="g-stat">--</span>
|
||||
</div>
|
||||
|
||||
<div class="section">Load</div>
|
||||
<div class="gpio-row">
|
||||
<span class="gpio-label">Puissance</span>
|
||||
<span class="gpio-val num" id="g-load">--</span>
|
||||
</div>
|
||||
|
||||
<div class="section">Énergie</div>
|
||||
<div class="gpio-row">
|
||||
<span class="gpio-label">Prod. jour</span>
|
||||
<span class="gpio-val num" id="g-egenj">--</span>
|
||||
</div>
|
||||
<div class="gpio-row">
|
||||
<span class="gpio-label">Conso. jour</span>
|
||||
<span class="gpio-val num" id="g-econj">--</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Volet droit : iframe webserver + terminal série -->
|
||||
<div class="panel-right">
|
||||
<iframe id="esp-frame" src="http://localhost:10080" title="WebServer ESP32"></iframe>
|
||||
|
||||
<div class="serial-bar" onclick="toggleTerminal()">
|
||||
<span>▼ Terminal série (UART0)</span>
|
||||
<span id="serial-count" style="margin-left:auto;font-family:monospace">0 lignes</span>
|
||||
</div>
|
||||
<div class="terminal" id="terminal"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
// --- Polling état ESP32 ---
|
||||
async function pollState() {
|
||||
try {
|
||||
const d = await (await fetch('/api/state', { signal: AbortSignal.timeout(2000) })).json();
|
||||
|
||||
badge('badge-esp', d.rs485_ok ? 'ESP32 OK' : 'ESP32 WiFi', d.rs485_ok ? 'ok' : 'warn');
|
||||
badge('badge-mb', d.rs485_ok ? 'Modbus OK' : 'Modbus ERR', d.rs485_ok ? 'ok' : 'err');
|
||||
|
||||
gpio('g-rs485', d.rs485_ok ? '● OK' : '○ ERR', d.rs485_ok ? 'on' : 'err');
|
||||
gpio('g-r1', d.relay1 ? '● ON' : '○ OFF', d.relay1 ? 'on' : 'off');
|
||||
gpio('g-r2', d.relay2 ? '● ON' : '○ OFF', d.relay2 ? 'on' : 'off');
|
||||
gpio('g-di1', d.di1 ? '● APP' : '○ REL', d.di1 ? 'on' : 'off');
|
||||
gpio('g-di2', d.di2 ? '● APP' : '○ REL', d.di2 ? 'on' : 'off');
|
||||
gpio('g-mode', d.autoMode ? 'Auto' : 'Manuel', 'num');
|
||||
gpio('g-sun', d.sun ? '☀ Jour' : '🌙 Nuit', d.sun ? 'on' : 'off');
|
||||
|
||||
num('g-bat', d.battery.toFixed(2) + ' V');
|
||||
num('g-pv', d.pv.toFixed(2) + ' V');
|
||||
num('g-pvc', d.pvCurrent.toFixed(2) + ' A');
|
||||
num('g-soc', d.batSOC + ' %');
|
||||
num('g-temp', d.batTemperature.toFixed(1) + ' °C');
|
||||
num('g-load', d.loadPower.toFixed(1) + ' W');
|
||||
num('g-egenj',d.energieGenJour.toFixed(2) + ' kWh');
|
||||
num('g-econj',d.energieConJour.toFixed(2) + ' kWh');
|
||||
|
||||
const statuts = ['Arrêt', 'Float', 'Boost', 'Égalisation'];
|
||||
gpio('g-stat', statuts[d.batStatut] || '--', 'num');
|
||||
|
||||
} catch {
|
||||
badge('badge-esp', 'ESP32 hors ligne', 'err');
|
||||
gpio('g-rs485', '○ ERR', 'err');
|
||||
}
|
||||
}
|
||||
|
||||
function badge(id, txt, type) {
|
||||
const el = document.getElementById(id);
|
||||
el.textContent = txt;
|
||||
el.className = 'badge badge-' + type;
|
||||
}
|
||||
function gpio(id, txt, cls) {
|
||||
const el = document.getElementById(id);
|
||||
el.textContent = txt;
|
||||
el.className = 'gpio-val ' + cls;
|
||||
}
|
||||
function num(id, txt) {
|
||||
const el = document.getElementById(id);
|
||||
el.textContent = txt;
|
||||
el.className = 'gpio-val num';
|
||||
}
|
||||
|
||||
// --- Terminal série via SSE ---
|
||||
let lineCount = 0;
|
||||
const terminal = document.getElementById('terminal');
|
||||
const evtSrc = new EventSource('/serial');
|
||||
|
||||
evtSrc.onmessage = e => {
|
||||
const line = document.createElement('div');
|
||||
line.className = 'log-line'
|
||||
+ (e.data.match(/err|error|erreur|fail/i) ? ' err' : '')
|
||||
+ (e.data.match(/warn|timeout|hors ligne/i) ? ' warn' : '')
|
||||
+ (e.data.match(/ok|prêt|ready|démarr/i) ? ' ok' : '');
|
||||
line.textContent = e.data;
|
||||
terminal.appendChild(line);
|
||||
lineCount++;
|
||||
document.getElementById('serial-count').textContent = lineCount + ' lignes';
|
||||
// Garder max 1000 lignes
|
||||
while (terminal.children.length > 1000) terminal.removeChild(terminal.firstChild);
|
||||
terminal.scrollTop = terminal.scrollHeight;
|
||||
};
|
||||
|
||||
// --- Toggle terminal ---
|
||||
function toggleTerminal() {
|
||||
terminal.classList.toggle('collapsed');
|
||||
}
|
||||
|
||||
// --- Démarrage ---
|
||||
pollState();
|
||||
setInterval(pollState, 3000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user