Auf der Website kann jetzt auch gestoppt werden wenn über die taster manuell gestartet wurde. Manuell steuerung verbssert. Durch movebyacceleration(0) ist die geschwindigkeit gedriftet. Beim drücken wird jetzt konstant beschleunigt bis maximal geschwindigkeit und beim loslassen wird die geschwindigkeit fest auf die aktuelle geschwindigkeit gesetzt. Rückwärts fahren geht jetzt nicht mehr.
233 lines
No EOL
8.9 KiB
C
233 lines
No EOL
8.9 KiB
C
#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() {
|
||
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||
ws = new WebSocket(`${protocol}//${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');
|
||
|
||
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);
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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 || d.running;
|
||
$('timedInfo').textContent = d.timerOn
|
||
? 'Gesperrt, solange die Zeitschaltuhr aktiv ist.'
|
||
: (d.running ? 'Motor läuft bereits.' : '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'; }
|
||
|
||
// ZENTRALE ÄNDERUNG: Prüft d.running ODER d.webRun
|
||
const active = d.running || d.webRun;
|
||
const run = $('btnRun');
|
||
run.disabled = d.overheat || d.timerOn;
|
||
run.textContent = active ? 'Stopp' : 'Start';
|
||
run.className = active ? '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);
|
||
}
|
||
|
||
// ZENTRALE ÄNDERUNG: Stoppt sobald d.running oder d.webRun aktiv ist
|
||
function toggleRun() {
|
||
const active = last.running || last.webRun;
|
||
send('run', active ? 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"; |