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>
333 lines
11 KiB
HTML
333 lines
11 KiB
HTML
<!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>
|