dashboard
This commit is contained in:
parent
5712bf54e3
commit
1c9a743ec3
791
mobkom_dashboard.html
Normal file
791
mobkom_dashboard.html
Normal file
@ -0,0 +1,791 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>MOBKOM MQTT Dashboard</title>
|
||||
<script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0b0f14;
|
||||
--card: #171c22;
|
||||
--card2: #20262e;
|
||||
--border: #2e3742;
|
||||
--text: #e7eef6;
|
||||
--muted: #9aa6b2;
|
||||
--dim: #697581;
|
||||
--cyan: #00c8d7;
|
||||
--green: #2fd17c;
|
||||
--red: #ff5c5c;
|
||||
--orange: #ffb020;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
background: radial-gradient(circle at top left, rgba(0,200,215,.08), transparent 34%), var(--bg);
|
||||
color: var(--text);
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
.top {
|
||||
max-width: 1500px;
|
||||
margin: 0 auto 16px auto;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
h1 { margin: 0; font-size: 26px; letter-spacing: -0.03em; }
|
||||
.subtitle { color: var(--muted); font-size: 13px; margin-top: 4px; }
|
||||
|
||||
.connect-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
input, button { font: inherit; }
|
||||
|
||||
input[type="text"] {
|
||||
width: 260px;
|
||||
color: var(--text);
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input[type="text"]:focus {
|
||||
border-color: var(--cyan);
|
||||
}
|
||||
|
||||
button {
|
||||
border: 1px solid rgba(255,255,255,.08);
|
||||
border-radius: 10px;
|
||||
padding: 9px 12px;
|
||||
background: var(--card2);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) { filter: brightness(1.12); }
|
||||
button:disabled { opacity: .45; cursor: not-allowed; }
|
||||
|
||||
.primary {
|
||||
background: linear-gradient(135deg, #00d4e6, #00a8b7);
|
||||
color: #061014;
|
||||
font-weight: 700;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.danger {
|
||||
color: #ffb7b7;
|
||||
border-color: rgba(255,92,92,.3);
|
||||
background: rgba(255,92,92,.12);
|
||||
}
|
||||
|
||||
.status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 9px 12px;
|
||||
border-radius: 999px;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
background: var(--orange);
|
||||
box-shadow: 0 0 12px rgba(255,176,32,.75);
|
||||
}
|
||||
|
||||
.dot.ok { background: var(--green); box-shadow: 0 0 12px rgba(47,209,124,.75); }
|
||||
.dot.err { background: var(--red); box-shadow: 0 0 12px rgba(255,92,92,.75); }
|
||||
|
||||
.dashboard {
|
||||
max-width: 1500px;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(240px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.card {
|
||||
min-height: 420px;
|
||||
background: linear-gradient(180deg, rgba(255,255,255,.035), rgba(255,255,255,.012)), var(--card);
|
||||
border: 1px solid rgba(255,255,255,.075);
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 18px 42px rgba(0,0,0,.32);
|
||||
}
|
||||
|
||||
.card-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.card-title h2 {
|
||||
margin: 0;
|
||||
color: var(--cyan);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.card-title span {
|
||||
color: var(--dim);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.sub-btn {
|
||||
font-size: 13px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.subscribed {
|
||||
color: #b6f7d1;
|
||||
background: rgba(47,209,124,.12);
|
||||
border-color: rgba(47,209,124,.28);
|
||||
}
|
||||
|
||||
.rows { display: grid; gap: 9px; }
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 82px 1fr auto;
|
||||
gap: 10px;
|
||||
align-items: baseline;
|
||||
padding: 10px;
|
||||
border-radius: 12px;
|
||||
background: rgba(0,0,0,.16);
|
||||
border: 1px solid rgba(255,255,255,.05);
|
||||
}
|
||||
|
||||
.name { color: var(--muted); font-size: 13px; }
|
||||
.value {
|
||||
color: var(--text);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 16px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.unit { color: var(--dim); font-size: 12px; }
|
||||
.path {
|
||||
grid-column: 1 / -1;
|
||||
color: var(--dim);
|
||||
font-size: 11px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.led-tools {
|
||||
display: grid;
|
||||
grid-template-columns: 74px 1fr 1fr;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
input[type="color"] {
|
||||
width: 74px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
background: var(--card2);
|
||||
border: 1px solid var(--border);
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.patterns {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.patterns button {
|
||||
font-size: 12px;
|
||||
padding: 8px 8px;
|
||||
border-radius: 9px;
|
||||
}
|
||||
|
||||
.matrix {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
gap: 5px;
|
||||
padding: 10px;
|
||||
max-width: 360px;
|
||||
background: rgba(0,0,0,.22);
|
||||
border: 1px solid rgba(255,255,255,.06);
|
||||
border-radius: 14px;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.led {
|
||||
aspect-ratio: 1 / 1;
|
||||
border-radius: 5px;
|
||||
background: #2b3036;
|
||||
border: 1px solid rgba(255,255,255,.06);
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.led:hover {
|
||||
outline: 1px solid var(--cyan);
|
||||
}
|
||||
|
||||
.led-status {
|
||||
margin-top: 12px;
|
||||
padding: 10px;
|
||||
min-height: 40px;
|
||||
color: var(--muted);
|
||||
background: rgba(0,0,0,.16);
|
||||
border: 1px solid rgba(255,255,255,.05);
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.log {
|
||||
max-width: 1500px;
|
||||
margin: 16px auto 0 auto;
|
||||
padding: 12px 16px;
|
||||
min-height: 44px;
|
||||
max-height: 130px;
|
||||
overflow-y: auto;
|
||||
color: var(--muted);
|
||||
background: rgba(23,28,34,.78);
|
||||
border: 1px solid rgba(255,255,255,.06);
|
||||
border-radius: 16px;
|
||||
font-size: 12px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.dashboard { grid-template-columns: repeat(2, minmax(260px, 1fr)); }
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
body { padding: 12px; }
|
||||
.dashboard { grid-template-columns: 1fr; }
|
||||
.card { min-height: auto; }
|
||||
input[type="text"] { width: 100%; }
|
||||
.connect-box { width: 100%; justify-content: flex-start; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="top">
|
||||
<div>
|
||||
<h1>MOBKOM MQTT Dashboard</h1>
|
||||
<div class="subtitle">Topics gruppiert nach GPS, SENS, IMU und LED-Matrix</div>
|
||||
</div>
|
||||
|
||||
<div class="connect-box">
|
||||
<input id="brokerInput" type="text" value="ws://192.168.0.95:9001" />
|
||||
<button id="connectBtn" class="primary">Verbinden</button>
|
||||
<button id="disconnectBtn" disabled>Trennen</button>
|
||||
<span class="status"><span id="statusDot" class="dot"></span><span id="statusText">Nicht verbunden</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main class="dashboard">
|
||||
<section class="card">
|
||||
<div class="card-head">
|
||||
<div class="card-title"><h2>GPS</h2><span>MOBKOM/GPS/#</span></div>
|
||||
<button class="sub-btn primary" data-sub="gps">Abonnieren</button>
|
||||
</div>
|
||||
<div class="rows" id="gpsRows"></div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="card-head">
|
||||
<div class="card-title"><h2>SENS</h2><span>MOBKOM/SENS/#</span></div>
|
||||
<button class="sub-btn primary" data-sub="sens">Abonnieren</button>
|
||||
</div>
|
||||
<div class="rows" id="sensRows"></div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="card-head">
|
||||
<div class="card-title"><h2>IMU</h2><span>MOBKOM/IMU/#</span></div>
|
||||
<button class="sub-btn primary" data-sub="imu">Abonnieren</button>
|
||||
</div>
|
||||
<div class="rows" id="imuRows"></div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="card-head">
|
||||
<div class="card-title"><h2>LED Matrix</h2><span>MOBKOM/LED/state_matrix</span></div>
|
||||
<button class="sub-btn primary" data-sub="led">Abonnieren</button>
|
||||
</div>
|
||||
|
||||
<div class="led-tools">
|
||||
<input id="colorInput" type="color" value="#00e5ee" />
|
||||
<button id="readAllBtn">Alle lesen</button>
|
||||
<button id="clearBtn" class="danger">Clear</button>
|
||||
</div>
|
||||
|
||||
<div class="patterns">
|
||||
<button data-pattern="rainbow">Regenbogen</button>
|
||||
<button data-pattern="wave">Welle</button>
|
||||
<button data-pattern="heart">Herz</button>
|
||||
<button data-pattern="smile">Smiley</button>
|
||||
<button data-pattern="swiss">Schweiz</button>
|
||||
<button data-pattern="checker">Schachbrett</button>
|
||||
<button data-pattern="frame">Rahmen</button>
|
||||
<button data-pattern="x">X</button>
|
||||
<button data-pattern="plus">Plus</button>
|
||||
<button data-pattern="diamond">Diamant</button>
|
||||
<button data-pattern="spiral">Spirale</button>
|
||||
<button data-pattern="random">Random</button>
|
||||
</div>
|
||||
|
||||
<div id="matrix" class="matrix"></div>
|
||||
<div id="ledStatus" class="led-status">Status: -</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div id="log" class="log">Bereit.</div>
|
||||
|
||||
<script>
|
||||
const TOPIC_LED_SET = "MOBKOM/LED/set_pixel";
|
||||
const TOPIC_LED_GET = "MOBKOM/LED/get_pixel";
|
||||
const TOPIC_LED_GET_MATRIX = "MOBKOM/LED/get_matrix";
|
||||
const TOPIC_LED_CLEAR = "MOBKOM/LED/clear";
|
||||
const TOPIC_LED_MATRIX = "MOBKOM/LED/set_matrix";
|
||||
const TOPIC_LED_STATE = "MOBKOM/LED/state";
|
||||
const TOPIC_LED_STATE_MATRIX = "MOBKOM/LED/state_matrix";
|
||||
|
||||
const groups = {
|
||||
gps: {
|
||||
filter: "MOBKOM/GPS/#",
|
||||
container: "gpsRows",
|
||||
rows: [
|
||||
["N", "MOBKOM/GPS/N", ""],
|
||||
["E", "MOBKOM/GPS/E", ""],
|
||||
["Höhe", "MOBKOM/GPS/hoehe", "m"],
|
||||
["Speed", "MOBKOM/GPS/geschwindigkeit", "km/h"]
|
||||
]
|
||||
},
|
||||
sens: {
|
||||
filter: "MOBKOM/SENS/#",
|
||||
container: "sensRows",
|
||||
rows: [
|
||||
["Temp", "MOBKOM/SENS/temp", "°C"],
|
||||
["Hum", "MOBKOM/SENS/humidity", "%"],
|
||||
["Press", "MOBKOM/SENS/pressure", "hPa"]
|
||||
]
|
||||
},
|
||||
imu: {
|
||||
filter: "MOBKOM/IMU/#",
|
||||
container: "imuRows",
|
||||
rows: [
|
||||
["Pitch", "MOBKOM/IMU/pitch", "°"],
|
||||
["Roll", "MOBKOM/IMU/roll", "°"],
|
||||
["Yaw", "MOBKOM/IMU/yaw", "°"],
|
||||
["X", "MOBKOM/IMU/x", ""],
|
||||
["Y", "MOBKOM/IMU/y", ""],
|
||||
["Z", "MOBKOM/IMU/z", ""]
|
||||
]
|
||||
},
|
||||
led: {
|
||||
filter: "MOBKOM/LED/state_matrix"
|
||||
}
|
||||
};
|
||||
|
||||
const brokerInput = document.getElementById("brokerInput");
|
||||
const connectBtn = document.getElementById("connectBtn");
|
||||
const disconnectBtn = document.getElementById("disconnectBtn");
|
||||
const statusDot = document.getElementById("statusDot");
|
||||
const statusText = document.getElementById("statusText");
|
||||
const colorInput = document.getElementById("colorInput");
|
||||
const matrix = document.getElementById("matrix");
|
||||
const ledStatus = document.getElementById("ledStatus");
|
||||
const readAllBtn = document.getElementById("readAllBtn");
|
||||
const clearBtn = document.getElementById("clearBtn");
|
||||
const logBox = document.getElementById("log");
|
||||
|
||||
let client = null;
|
||||
let isPainting = false;
|
||||
let paintMode = "draw";
|
||||
let patternBusy = false;
|
||||
|
||||
const subscribed = { gps: false, sens: false, imu: false, led: false };
|
||||
const valueEls = {};
|
||||
const ledEls = [];
|
||||
|
||||
function log(msg) {
|
||||
const time = new Date().toLocaleTimeString();
|
||||
logBox.textContent = `[${time}] ${msg}\n` + logBox.textContent;
|
||||
}
|
||||
|
||||
function setStatus(text, state) {
|
||||
statusText.textContent = text;
|
||||
statusDot.classList.remove("ok", "err");
|
||||
if (state === "ok") statusDot.classList.add("ok");
|
||||
if (state === "err") statusDot.classList.add("err");
|
||||
}
|
||||
|
||||
function formatValue(raw) {
|
||||
const n = Number(raw);
|
||||
if (!Number.isFinite(n)) return raw;
|
||||
if (Math.abs(n) >= 10) return n.toFixed(2);
|
||||
return n.toFixed(4);
|
||||
}
|
||||
|
||||
function buildRows() {
|
||||
for (const key of ["gps", "sens", "imu"]) {
|
||||
const group = groups[key];
|
||||
const container = document.getElementById(group.container);
|
||||
container.innerHTML = "";
|
||||
|
||||
for (const [name, topic, unit] of group.rows) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "row";
|
||||
|
||||
const nameEl = document.createElement("div");
|
||||
nameEl.className = "name";
|
||||
nameEl.textContent = name;
|
||||
|
||||
const valueEl = document.createElement("div");
|
||||
valueEl.className = "value";
|
||||
valueEl.textContent = "-";
|
||||
|
||||
const unitEl = document.createElement("div");
|
||||
unitEl.className = "unit";
|
||||
unitEl.textContent = unit;
|
||||
|
||||
const pathEl = document.createElement("div");
|
||||
pathEl.className = "path";
|
||||
pathEl.textContent = topic;
|
||||
|
||||
row.appendChild(nameEl);
|
||||
row.appendChild(valueEl);
|
||||
row.appendChild(unitEl);
|
||||
row.appendChild(pathEl);
|
||||
container.appendChild(row);
|
||||
|
||||
valueEls[topic] = valueEl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hexToRgb(hex) {
|
||||
const h = hex.replace("#", "");
|
||||
return [parseInt(h.slice(0,2), 16), parseInt(h.slice(2,4), 16), parseInt(h.slice(4,6), 16)];
|
||||
}
|
||||
|
||||
function rgbToCss(rgb) {
|
||||
return `rgb(${Number(rgb[0]) || 0}, ${Number(rgb[1]) || 0}, ${Number(rgb[2]) || 0})`;
|
||||
}
|
||||
|
||||
function setLedUi(x, y, rgb) {
|
||||
const idx = y * 8 + x;
|
||||
if (ledEls[idx]) ledEls[idx].style.background = rgbToCss(rgb);
|
||||
}
|
||||
|
||||
function publish(topic, payload) {
|
||||
if (!client || !client.connected) {
|
||||
log("Nicht verbunden. Publish abgebrochen.");
|
||||
return false;
|
||||
}
|
||||
client.publish(topic, payload, { qos: 0, retain: false });
|
||||
return true;
|
||||
}
|
||||
|
||||
function sendPixel(x, y, rgb) {
|
||||
setLedUi(x, y, rgb);
|
||||
publish(TOPIC_LED_SET, JSON.stringify({ pos: [x, y], rgb: rgb }));
|
||||
}
|
||||
|
||||
function readPixel(x, y) {
|
||||
publish(TOPIC_LED_GET, JSON.stringify({ pos: [x, y] }));
|
||||
}
|
||||
|
||||
function readAllPixels() {
|
||||
publish(TOPIC_LED_GET_MATRIX, "ok");
|
||||
ledStatus.textContent = "Status: get_matrix gesendet";
|
||||
}
|
||||
|
||||
function clearMatrix() {
|
||||
publish(TOPIC_LED_CLEAR, "clear");
|
||||
for (let y = 0; y < 8; y++) for (let x = 0; x < 8; x++) setLedUi(x, y, [0,0,0]);
|
||||
ledStatus.textContent = "Status: clear gesendet";
|
||||
}
|
||||
|
||||
function applyMatrix(matrixData) {
|
||||
if (!Array.isArray(matrixData) || matrixData.length !== 8) return false;
|
||||
|
||||
for (let y = 0; y < 8; y++) {
|
||||
if (!Array.isArray(matrixData[y]) || matrixData[y].length !== 8) return false;
|
||||
|
||||
for (let x = 0; x < 8; x++) {
|
||||
const rgb = matrixData[y][x];
|
||||
if (!Array.isArray(rgb) || rgb.length < 3) return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (let y = 0; y < 8; y++) {
|
||||
for (let x = 0; x < 8; x++) {
|
||||
setLedUi(x, y, matrixData[y][x]);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleLedState(payload) {
|
||||
ledStatus.textContent = "Status: " + payload;
|
||||
try {
|
||||
const data = JSON.parse(payload);
|
||||
|
||||
// Format 1: {"matrix": [[[r,g,b], ...], ...]}
|
||||
if (data && Array.isArray(data.matrix)) {
|
||||
if (applyMatrix(data.matrix)) return;
|
||||
}
|
||||
|
||||
// Format 2: [[[r,g,b], ...], ...]
|
||||
if (applyMatrix(data)) return;
|
||||
|
||||
// Format 3: [{"pos":[x,y], "rgb":[r,g,b]}]
|
||||
const list = Array.isArray(data) ? data : [data];
|
||||
for (const item of list) {
|
||||
if (!item || !Array.isArray(item.pos) || !Array.isArray(item.rgb)) continue;
|
||||
const x = Number(item.pos[0]);
|
||||
const y = Number(item.pos[1]);
|
||||
if (x < 0 || x > 7 || y < 0 || y > 7) continue;
|
||||
setLedUi(x, y, item.rgb);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function createMatrix() {
|
||||
matrix.innerHTML = "";
|
||||
ledEls.length = 0;
|
||||
for (let y = 0; y < 8; y++) {
|
||||
for (let x = 0; x < 8; x++) {
|
||||
const led = document.createElement("button");
|
||||
led.type = "button";
|
||||
led.className = "led";
|
||||
led.title = `(${x}, ${y})`;
|
||||
|
||||
led.addEventListener("pointerdown", (ev) => {
|
||||
ev.preventDefault();
|
||||
isPainting = true;
|
||||
paintMode = ev.button === 2 ? "erase" : "draw";
|
||||
const rgb = paintMode === "erase" ? [0,0,0] : hexToRgb(colorInput.value);
|
||||
sendPixel(x, y, rgb);
|
||||
});
|
||||
|
||||
led.addEventListener("pointerenter", () => {
|
||||
if (!isPainting) return;
|
||||
const rgb = paintMode === "erase" ? [0,0,0] : hexToRgb(colorInput.value);
|
||||
sendPixel(x, y, rgb);
|
||||
});
|
||||
|
||||
led.addEventListener("contextmenu", (ev) => ev.preventDefault());
|
||||
matrix.appendChild(led);
|
||||
ledEls.push(led);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hsvToRgb(h, s, v) {
|
||||
let r = 0, g = 0, b = 0;
|
||||
const i = Math.floor(h * 6);
|
||||
const f = h * 6 - i;
|
||||
const p = v * (1 - s);
|
||||
const q = v * (1 - f * s);
|
||||
const t = v * (1 - (1 - f) * s);
|
||||
switch (i % 6) {
|
||||
case 0: r = v; g = t; b = p; break;
|
||||
case 1: r = q; g = v; b = p; break;
|
||||
case 2: r = p; g = v; b = t; break;
|
||||
case 3: r = p; g = q; b = v; break;
|
||||
case 4: r = t; g = p; b = v; break;
|
||||
case 5: r = v; g = p; b = q; break;
|
||||
}
|
||||
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
|
||||
}
|
||||
|
||||
function sendPattern(name, fn) {
|
||||
if (patternBusy) return;
|
||||
patternBusy = true;
|
||||
|
||||
const matrixPayload = [];
|
||||
|
||||
for (let y = 0; y < 8; y++) {
|
||||
const row = [];
|
||||
for (let x = 0; x < 8; x++) {
|
||||
const rgb = fn(x, y);
|
||||
row.push(rgb);
|
||||
setLedUi(x, y, rgb);
|
||||
}
|
||||
matrixPayload.push(row);
|
||||
}
|
||||
|
||||
const ok = publish(TOPIC_LED_MATRIX, JSON.stringify({ matrix: matrixPayload }));
|
||||
ledStatus.textContent = ok
|
||||
? "Status: Muster als ganze Matrix gesendet: " + name
|
||||
: "Status: Muster konnte nicht gesendet werden";
|
||||
|
||||
patternBusy = false;
|
||||
}
|
||||
|
||||
function runPattern(name) {
|
||||
const c = hexToRgb(colorInput.value);
|
||||
const off = [0,0,0];
|
||||
const points = (arr) => new Set(arr.map(p => p.join(",")));
|
||||
|
||||
if (name === "rainbow") sendPattern("Regenbogen", (x,y) => hsvToRgb((x + y) / 14, 1, 1));
|
||||
if (name === "wave") sendPattern("Welle", (x,y) => Math.abs(y - Math.round(3.5 + Math.sin(x * 0.9) * 2.2)) <= 1 ? c : off);
|
||||
if (name === "checker") sendPattern("Schachbrett", (x,y) => (x + y) % 2 === 0 ? c : off);
|
||||
if (name === "frame") sendPattern("Rahmen", (x,y) => (x === 0 || x === 7 || y === 0 || y === 7) ? c : off);
|
||||
if (name === "x") sendPattern("X", (x,y) => (x === y || x + y === 7) ? c : off);
|
||||
if (name === "plus") sendPattern("Plus", (x,y) => (x === 3 || x === 4 || y === 3 || y === 4) ? c : off);
|
||||
if (name === "diamond") sendPattern("Diamant", (x,y) => (Math.abs(x - 3.5) + Math.abs(y - 3.5) <= 3) ? c : off);
|
||||
if (name === "random") sendPattern("Random", () => hsvToRgb(Math.random(), 1, 0.7 + Math.random() * 0.3));
|
||||
if (name === "swiss") sendPattern("Schweiz", (x,y) => (((x === 3 || x === 4) && y >= 1 && y <= 6) || ((y === 3 || y === 4) && x >= 1 && x <= 6)) ? [255,255,255] : [255,0,0]);
|
||||
|
||||
if (name === "heart") {
|
||||
const set = points([[1,1],[2,1],[5,1],[6,1],[0,2],[3,2],[4,2],[7,2],[0,3],[7,3],[1,4],[6,4],[2,5],[5,5],[3,6],[4,6]]);
|
||||
sendPattern("Herz", (x,y) => set.has(`${x},${y}`) ? c : off);
|
||||
}
|
||||
|
||||
if (name === "smile") {
|
||||
const set = points([[2,2],[5,2],[2,3],[5,3],[1,5],[2,6],[3,6],[4,6],[5,6],[6,5]]);
|
||||
sendPattern("Smiley", (x,y) => set.has(`${x},${y}`) ? c : off);
|
||||
}
|
||||
|
||||
if (name === "spiral") {
|
||||
const set = points([[0,0],[1,0],[2,0],[3,0],[4,0],[5,0],[6,0],[7,0],[7,1],[7,2],[7,3],[7,4],[7,5],[7,6],[7,7],[6,7],[5,7],[4,7],[3,7],[2,7],[1,7],[0,7],[0,6],[0,5],[0,4],[0,3],[0,2],[0,1],[1,1],[2,1],[3,1],[4,1],[5,1],[6,1],[6,2],[6,3],[6,4],[6,5],[6,6],[5,6],[4,6],[3,6],[2,6],[1,6],[1,5],[1,4],[1,3],[1,2],[2,2],[3,2],[4,2],[5,2],[5,3],[5,4],[5,5],[4,5],[3,5],[2,5],[2,4],[2,3],[3,3],[4,3],[4,4],[3,4]]);
|
||||
sendPattern("Spirale", (x,y) => set.has(`${x},${y}`) ? c : off);
|
||||
}
|
||||
}
|
||||
|
||||
function updateSubButton(key) {
|
||||
const btn = document.querySelector(`[data-sub="${key}"]`);
|
||||
if (!btn) return;
|
||||
if (subscribed[key]) {
|
||||
btn.textContent = "Abonniert";
|
||||
btn.classList.remove("primary");
|
||||
btn.classList.add("subscribed");
|
||||
} else {
|
||||
btn.textContent = "Abonnieren";
|
||||
btn.classList.add("primary");
|
||||
btn.classList.remove("subscribed");
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSubscribe(key) {
|
||||
if (!client || !client.connected) {
|
||||
log("Erst verbinden.");
|
||||
return;
|
||||
}
|
||||
const topic = groups[key].filter;
|
||||
if (!subscribed[key]) {
|
||||
client.subscribe(topic, { qos: 0 });
|
||||
subscribed[key] = true;
|
||||
log("Subscribed: " + topic);
|
||||
} else {
|
||||
client.unsubscribe(topic);
|
||||
subscribed[key] = false;
|
||||
log("Unsubscribed: " + topic);
|
||||
}
|
||||
updateSubButton(key);
|
||||
}
|
||||
|
||||
function connectMqtt() {
|
||||
const url = brokerInput.value.trim();
|
||||
if (!url) return;
|
||||
|
||||
if (client) {
|
||||
try { client.end(true); } catch (_) {}
|
||||
}
|
||||
|
||||
setStatus("Verbinde...", "idle");
|
||||
client = mqtt.connect(url, {
|
||||
clientId: "mobkom_web_" + Math.random().toString(16).slice(2),
|
||||
clean: true,
|
||||
keepalive: 60,
|
||||
connectTimeout: 8000,
|
||||
reconnectPeriod: 2500
|
||||
});
|
||||
|
||||
client.on("connect", () => {
|
||||
setStatus("Verbunden", "ok");
|
||||
connectBtn.disabled = true;
|
||||
disconnectBtn.disabled = false;
|
||||
log("MQTT verbunden: " + url);
|
||||
});
|
||||
|
||||
client.on("message", (topic, message) => {
|
||||
const payload = message.toString();
|
||||
if (topic === TOPIC_LED_STATE || topic === TOPIC_LED_STATE_MATRIX) {
|
||||
handleLedState(payload);
|
||||
} else if (valueEls[topic]) {
|
||||
valueEls[topic].textContent = formatValue(payload);
|
||||
}
|
||||
});
|
||||
|
||||
client.on("reconnect", () => setStatus("Verbinde neu...", "idle"));
|
||||
client.on("close", () => {
|
||||
setStatus("Getrennt", "idle");
|
||||
connectBtn.disabled = false;
|
||||
disconnectBtn.disabled = true;
|
||||
});
|
||||
client.on("error", (err) => {
|
||||
setStatus("Fehler", "err");
|
||||
log("MQTT Fehler: " + (err.message || err));
|
||||
});
|
||||
}
|
||||
|
||||
function disconnectMqtt() {
|
||||
if (client) client.end();
|
||||
for (const key of Object.keys(subscribed)) {
|
||||
subscribed[key] = false;
|
||||
updateSubButton(key);
|
||||
}
|
||||
}
|
||||
|
||||
buildRows();
|
||||
createMatrix();
|
||||
|
||||
connectBtn.addEventListener("click", connectMqtt);
|
||||
disconnectBtn.addEventListener("click", disconnectMqtt);
|
||||
clearBtn.addEventListener("click", clearMatrix);
|
||||
readAllBtn.addEventListener("click", readAllPixels);
|
||||
|
||||
for (const btn of document.querySelectorAll("[data-sub]")) {
|
||||
btn.addEventListener("click", () => toggleSubscribe(btn.dataset.sub));
|
||||
}
|
||||
|
||||
for (const btn of document.querySelectorAll("[data-pattern]")) {
|
||||
btn.addEventListener("click", () => runPattern(btn.dataset.pattern));
|
||||
}
|
||||
|
||||
window.addEventListener("pointerup", () => { isPainting = false; });
|
||||
window.addEventListener("pointercancel", () => { isPainting = false; });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user