kugelmuehle/web_ui.h

226 lines
No EOL
8.7 KiB
C
Raw 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.

#pragma once
#include <Arduino.h>
// Komplette Weboberfläche im Flash des Programms – kein LittleFS-Upload nötig.
static const char INDEX_HTML[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Kugelmühle</title>
<style>
:root { --bg:#1b1d21; --card:#272a30; --txt:#eceff4; --dim:#8b93a1; --ok:#3fb950; --warn:#f85149; --acc:#2f81f7; }
* { box-sizing:border-box; }
body { margin:0; font-family:system-ui,Arial,sans-serif; background:var(--bg); color:var(--txt); padding:16px; }
.wrap { max-width:460px; margin:0 auto; display:grid; gap:14px; }
.card { background:var(--card); border-radius:12px; padding:16px; }
h1 { font-size:1.2rem; margin:0; display:flex; justify-content:space-between; align-items:center; }
h2 { font-size:.8rem; margin:0 0 10px; color:var(--dim); text-transform:uppercase; letter-spacing:.06em; }
.dot { width:10px; height:10px; border-radius:50%; background:var(--warn); }
.dot.on { background:var(--ok); }
.grid { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
.val { font-size:1.8rem; font-weight:700; }
.lbl { font-size:.8rem; color:var(--dim); }
.hot { color:var(--warn); }
.state { font-weight:700; }
.state.run { color:var(--ok); }
.state.err { color:var(--warn); }
button { width:100%; padding:14px; border:0; border-radius:8px; font-size:1rem; font-weight:600; color:#fff; background:var(--acc); cursor:pointer; }
button.go { background:var(--ok); }
button.stop { background:var(--warn); }
button.off { background:#444b57; }
button:disabled { opacity:.4; cursor:not-allowed; }
.row { display:flex; gap:10px; align-items:center; margin-top:10px; }
.row > * { flex:1; }
input[type=number], input[type=time] { width:100%; padding:10px; border-radius:8px; border:1px solid #444b57; background:#1b1d21; color:var(--txt); font-size:1rem; }
input[type=range] { width:100%; }
.small { font-size:.8rem; color:var(--dim); margin-top:6px; }
select { width:100%; padding:10px; border-radius:8px; border:1px solid #444b57; background:#1b1d21; color:var(--txt); font-size:1rem; }
.val.dim { opacity:.45; }
</style>
</head>
<body>
<div class="wrap">
<div class="card">
<h1>Kugelmühle <span class="dot" id="conn" title="Verbindung"></span></h1>
</div>
<div class="card">
<h2>Status</h2>
<div class="grid">
<div><div class="val" id="temp">--</div><div class="lbl">Temperatur °C (Limit <span id="maxT">--</span>)</div></div>
<div><div class="val" id="rpm">--</div><div class="lbl">U/min</div></div>
<div><div class="state" id="state">--</div><div class="lbl">Motor</div></div>
<div><div class="state" id="clock">--:--</div><div class="lbl">Uhrzeit</div></div>
<div style="grid-column:1/-1">
<div class="val" id="runTime">00:00:00</div>
<div class="lbl">Laufzeit seit letztem Start<span id="remainInfo"></span></div>
</div>
</div>
</div>
<div class="card">
<h2>Steuerung</h2>
<button id="btnRun" onclick="toggleRun()">Start</button>
<div class="small" id="runHint"></div>
<div class="row">
<button id="btnTimer" onclick="toggleTimer()">Zeitschaltuhr</button>
</div>
<div class="small">Bei aktiver Zeitschaltuhr läuft der Motor nur im eingestellten Zeitfenster.</div>
</div>
<div class="card">
<h2>Für eine bestimmte Zeit starten</h2>
<div class="row">
<input type="number" id="durVal" min="0" step="any" value="2">
<select id="durUnit">
<option value="60">Minuten</option>
<option value="3600" selected>Stunden</option>
<option value="86400">Tage</option>
</select>
</div>
<div class="row"><button class="go" id="btnTimed" onclick="startTimed()">Jetzt starten</button></div>
<div class="small" id="timedInfo"></div>
</div>
<div class="card">
<h2>Drehzahl (Automatik / Start)</h2>
<div><b id="speedVal">--</b> U/min</div>
<input type="range" id="speed" min="50" max="1987" step="50"
oninput="$('speedVal').textContent=this.value"
onchange="send('speed', +this.value)">
</div>
<div class="card">
<h2>Zeitfenster</h2>
<div class="row">
<div><div class="lbl">Start</div><input type="time" id="tStart"></div>
<div><div class="lbl">Ende</div><input type="time" id="tEnd"></div>
</div>
<div class="row"><button onclick="sendWindow()">Übernehmen</button></div>
</div>
<div class="card">
<h2>Einstellungen</h2>
<div class="row">
<div><div class="lbl">Max. Temperatur °C</div><input type="number" id="maxTemp" min="-20" max="150" step="5"></div>
<div><div class="lbl">Beschleunigung</div><input type="number" id="accel" min="1000" max="30000" step="500"></div>
</div>
<div class="row"><button onclick="sendSettings()">Übernehmen</button></div>
</div>
</div>
<script>
const $ = id => document.getElementById(id);
let ws, last = {};
function connect() {
ws = new WebSocket(`ws://${location.host}/ws`);
ws.onopen = () => $('conn').classList.add('on');
ws.onclose = () => { $('conn').classList.remove('on'); setTimeout(connect, 2000); };
ws.onmessage = e => render(JSON.parse(e.data));
}
function send(cmd, v, sec) {
if (ws && ws.readyState === 1) ws.send(JSON.stringify({ cmd, v, sec }));
}
const pad = n => String(n).padStart(2, '0');
// Sekunden -> "HH:MM:SS" bzw. "2 T HH:MM:SS"
function fmt(s) {
s = Math.max(0, Math.floor(s));
const d = Math.floor(s / 86400); s %= 86400;
const h = Math.floor(s / 3600); s %= 3600;
const m = Math.floor(s / 60);
return (d > 0 ? d + ' T ' : '') + pad(h) + ':' + pad(m) + ':' + pad(s % 60);
}
// Voraussichtliches Ende (Uhr des Browsers)
function endText(remainSec) {
const end = new Date(Date.now() + remainSec * 1000);
const t = pad(end.getHours()) + ':' + pad(end.getMinutes()) + ' Uhr';
if (end.toDateString() === new Date().toDateString()) return t;
return end.toLocaleDateString('de-DE', { weekday: 'short', day: '2-digit', month: '2-digit' }) + ' ' + t;
}
function setIfIdle(id, value) {
const el = $(id);
if (document.activeElement !== el) el.value = value; // nicht überschreiben, während getippt/gezogen wird
}
function render(d) {
last = d;
$('temp').textContent = d.temp;
$('temp').classList.toggle('hot', d.overheat);
$('maxT').textContent = d.maxTemp;
$('rpm').textContent = d.rpm;
$('clock').textContent = d.time;
$('runTime').textContent = fmt(d.runSec);
$('runTime').classList.toggle('dim', !d.running);
$('remainInfo').textContent = d.timed
? ' · noch ' + fmt(d.remain) + ' (bis ' + endText(d.remain) + ')'
: '';
$('btnTimed').disabled = d.overheat || d.timerOn;
$('timedInfo').textContent = d.timerOn
? 'Gesperrt, solange die Zeitschaltuhr aktiv ist.'
: 'Läuft ab jetzt für die gewählte Dauer und stoppt dann von selbst (max. 30 Tage). Nur per Web möglich.';
const st = $('state');
st.className = 'state';
if (d.overheat) { st.textContent = 'ÜBERHITZT'; st.classList.add('err'); }
else if (d.running) { st.textContent = 'Läuft'; st.classList.add('run'); }
else { st.textContent = 'Gestoppt'; }
const run = $('btnRun');
run.disabled = d.overheat || d.timerOn;
run.textContent = d.webRun ? 'Stopp' : 'Start';
run.className = d.webRun ? 'stop' : 'go';
$('runHint').textContent = d.timerOn ? 'Start/Stopp ist gesperrt, solange die Zeitschaltuhr aktiv ist.' : '';
const tm = $('btnTimer');
tm.textContent = 'Zeitschaltuhr: ' + (d.timerOn ? 'AN' : 'AUS');
tm.className = d.timerOn ? 'go' : 'off';
$('speed').max = d.maxRpm;
if (document.activeElement !== $('speed')) { $('speed').value = d.autoSpeed; $('speedVal').textContent = d.autoSpeed; }
setIfIdle('tStart', pad(d.sh) + ':' + pad(d.sm));
setIfIdle('tEnd', pad(d.eh) + ':' + pad(d.em));
setIfIdle('maxTemp', d.maxTemp);
setIfIdle('accel', d.accel);
}
function toggleRun() { send('run', last.webRun ? 0 : 1); }
function startTimed() {
const sec = Math.round(+$('durVal').value * +$('durUnit').value);
if (!(sec >= 60) || sec > 2592000) {
alert('Bitte eine Dauer zwischen 1 Minute und 30 Tagen eingeben.');
return;
}
if (sec > 12 * 3600 && !confirm('Die Mühle läuft dann ' + fmt(sec) + ' lang ohne Aufsicht weiter. Wirklich starten?')) return;
send('run', 1, sec);
}
function toggleTimer() { send('timer', last.timerOn ? 0 : 1); }
function sendWindow() {
const [sh, sm] = $('tStart').value.split(':').map(Number);
const [eh, em] = $('tEnd').value.split(':').map(Number);
if ([sh, sm, eh, em].some(isNaN)) return;
send('window', [sh, sm, eh, em]);
}
function sendSettings() {
send('maxTemp', +$('maxTemp').value);
send('accel', +$('accel').value);
}
connect();
</script>
</body>
</html>
)rawliteral";