This commit is contained in:
96
tools/button-simulator/public/app.js
Normal file
96
tools/button-simulator/public/app.js
Normal file
@@ -0,0 +1,96 @@
|
||||
const $ = (sel, root = document) => root.querySelector(sel);
|
||||
const $$ = (sel, root = document) => [...root.querySelectorAll(sel)];
|
||||
|
||||
async function api(path, body) {
|
||||
const opts = { method: body ? "POST" : "GET" };
|
||||
if (body) {
|
||||
opts.headers = { "Content-Type": "application/json" };
|
||||
opts.body = JSON.stringify(body);
|
||||
}
|
||||
const res = await fetch(path, opts);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
function formatLog(entries) {
|
||||
return entries
|
||||
.map((e) => {
|
||||
const ts = new Date(e.t).toLocaleTimeString();
|
||||
const arrow = e.direction === "out" ? "→" : e.direction === "in" ? "←" : e.direction === "err" ? "✖" : "·";
|
||||
return `${ts} ${arrow} ${e.text}`;
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
let lastKnownButtons = null;
|
||||
|
||||
async function refreshStatus() {
|
||||
try {
|
||||
const s = await api("/api/status");
|
||||
$("#status").textContent = s.connected ? "online" : "offline";
|
||||
$("#status").className = "status " + (s.connected ? "online" : "offline");
|
||||
|
||||
if (!lastKnownButtons) {
|
||||
$("#brokerUrl").value = s.brokerUrl;
|
||||
$("#hbEnabled").checked = s.heartbeatEnabled;
|
||||
$("#hbInterval").value = s.heartbeatIntervalMs;
|
||||
for (const key of ["start1", "stop1", "start2", "stop2"]) {
|
||||
const card = $(`.button-card[data-button="${key}"]`);
|
||||
card.querySelector(".mac").value = s.buttons[key].mac;
|
||||
card.querySelector(".mv").value = s.buttons[key].voltage;
|
||||
card.querySelector(".mv-value").textContent = s.buttons[key].voltage;
|
||||
}
|
||||
lastKnownButtons = s.buttons;
|
||||
}
|
||||
|
||||
$("#log").textContent = formatLog(s.log);
|
||||
$("#log").scrollTop = $("#log").scrollHeight;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
$("#btnConnect").addEventListener("click", async () => {
|
||||
await api("/api/connect", { brokerUrl: $("#brokerUrl").value.trim() });
|
||||
refreshStatus();
|
||||
});
|
||||
|
||||
$("#btnDisconnect").addEventListener("click", async () => {
|
||||
await api("/api/disconnect", {});
|
||||
refreshStatus();
|
||||
});
|
||||
|
||||
$("#btnHbApply").addEventListener("click", async () => {
|
||||
await api("/api/heartbeat", {
|
||||
enabled: $("#hbEnabled").checked,
|
||||
intervalMs: parseInt($("#hbInterval").value, 10),
|
||||
});
|
||||
refreshStatus();
|
||||
});
|
||||
|
||||
$$(".button-card").forEach((card) => {
|
||||
const button = card.dataset.button;
|
||||
const macInput = card.querySelector(".mac");
|
||||
const mvInput = card.querySelector(".mv");
|
||||
const mvValue = card.querySelector(".mv-value");
|
||||
const pressBtn = card.querySelector(".press");
|
||||
|
||||
macInput.addEventListener("change", async () => {
|
||||
await api("/api/config", { button, mac: macInput.value.trim() });
|
||||
});
|
||||
|
||||
mvInput.addEventListener("input", () => {
|
||||
mvValue.textContent = mvInput.value;
|
||||
});
|
||||
|
||||
mvInput.addEventListener("change", async () => {
|
||||
await api("/api/battery", { button, voltage: parseInt(mvInput.value, 10) });
|
||||
});
|
||||
|
||||
pressBtn.addEventListener("click", async () => {
|
||||
await api("/api/press", { button });
|
||||
refreshStatus();
|
||||
});
|
||||
});
|
||||
|
||||
refreshStatus();
|
||||
setInterval(refreshStatus, 1500);
|
||||
95
tools/button-simulator/public/index.html
Normal file
95
tools/button-simulator/public/index.html
Normal file
@@ -0,0 +1,95 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>AquaMaster Button-Simulator</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>AquaMaster Button-Simulator</h1>
|
||||
<div class="connection">
|
||||
<input id="brokerUrl" type="text" placeholder="mqtt://192.168.10.1:1883" />
|
||||
<button id="btnConnect" class="primary">Verbinden</button>
|
||||
<button id="btnDisconnect">Trennen</button>
|
||||
<span id="status" class="status offline">offline</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="grid">
|
||||
<div class="lane lane1">
|
||||
<h2>Bahn 1</h2>
|
||||
<div class="button-card" data-button="start1">
|
||||
<div class="button-header">
|
||||
<span class="role">Start 1</span>
|
||||
<input class="mac" type="text" spellcheck="false" />
|
||||
</div>
|
||||
<button class="press press-start">PRESS</button>
|
||||
<div class="battery">
|
||||
<label>Akku (mV): <span class="mv-value">3700</span></label>
|
||||
<input class="mv" type="range" min="3000" max="4200" step="10" value="3700" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-card" data-button="stop1">
|
||||
<div class="button-header">
|
||||
<span class="role">Stop 1</span>
|
||||
<input class="mac" type="text" spellcheck="false" />
|
||||
</div>
|
||||
<button class="press press-stop">PRESS</button>
|
||||
<div class="battery">
|
||||
<label>Akku (mV): <span class="mv-value">3700</span></label>
|
||||
<input class="mv" type="range" min="3000" max="4200" step="10" value="3700" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lane lane2">
|
||||
<h2>Bahn 2</h2>
|
||||
<div class="button-card" data-button="start2">
|
||||
<div class="button-header">
|
||||
<span class="role">Start 2</span>
|
||||
<input class="mac" type="text" spellcheck="false" />
|
||||
</div>
|
||||
<button class="press press-start">PRESS</button>
|
||||
<div class="battery">
|
||||
<label>Akku (mV): <span class="mv-value">3700</span></label>
|
||||
<input class="mv" type="range" min="3000" max="4200" step="10" value="3700" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-card" data-button="stop2">
|
||||
<div class="button-header">
|
||||
<span class="role">Stop 2</span>
|
||||
<input class="mac" type="text" spellcheck="false" />
|
||||
</div>
|
||||
<button class="press press-stop">PRESS</button>
|
||||
<div class="battery">
|
||||
<label>Akku (mV): <span class="mv-value">3700</span></label>
|
||||
<input class="mv" type="range" min="3000" max="4200" step="10" value="3700" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="controls">
|
||||
<label>
|
||||
<input id="hbEnabled" type="checkbox" checked />
|
||||
Heartbeat + Battery automatisch senden
|
||||
</label>
|
||||
<label>
|
||||
Intervall (ms):
|
||||
<input id="hbInterval" type="number" min="500" max="60000" step="500" value="3000" />
|
||||
</label>
|
||||
<button id="btnHbApply">Übernehmen</button>
|
||||
</section>
|
||||
|
||||
<section class="log-wrap">
|
||||
<h3>Log</h3>
|
||||
<pre id="log"></pre>
|
||||
</section>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
189
tools/button-simulator/public/style.css
Normal file
189
tools/button-simulator/public/style.css
Normal file
@@ -0,0 +1,189 @@
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.connection {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.connection input {
|
||||
flex: 1;
|
||||
min-width: 220px;
|
||||
padding: 8px 10px;
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 6px;
|
||||
color: #e2e8f0;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 8px 14px;
|
||||
border: 1px solid #334155;
|
||||
background: #1e293b;
|
||||
color: #e2e8f0;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
button:hover { background: #334155; }
|
||||
button.primary { background: #2563eb; border-color: #2563eb; }
|
||||
button.primary:hover { background: #1d4ed8; }
|
||||
|
||||
.status {
|
||||
padding: 4px 10px;
|
||||
border-radius: 99px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.status.online { background: #14532d; color: #86efac; }
|
||||
.status.offline { background: #7f1d1d; color: #fca5a5; }
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.lane {
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.lane h2 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.button-card {
|
||||
background: #0f172a;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.button-card:last-child { margin-bottom: 0; }
|
||||
|
||||
.button-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.role {
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.mac {
|
||||
flex: 1;
|
||||
padding: 6px 8px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 4px;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.press {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 2px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.press-start { background: #166534; border-color: #166534; }
|
||||
.press-start:hover { background: #15803d; }
|
||||
.press-stop { background: #991b1b; border-color: #991b1b; }
|
||||
.press-stop:hover { background: #b91c1c; }
|
||||
.press:active { transform: scale(0.98); }
|
||||
|
||||
.battery label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.battery input[type="range"] { width: 100%; }
|
||||
|
||||
.controls {
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.controls label { font-size: 13px; display: flex; align-items: center; gap: 6px; }
|
||||
.controls input[type="number"] {
|
||||
width: 80px;
|
||||
padding: 6px 8px;
|
||||
background: #0f172a;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 4px;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.log-wrap {
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.log-wrap h3 { margin: 0 0 8px; font-size: 13px; color: #94a3b8; }
|
||||
|
||||
#log {
|
||||
margin: 0;
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
font-family: monospace;
|
||||
font-size: 11px;
|
||||
white-space: pre-wrap;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
Reference in New Issue
Block a user