Files
appRobotDriver/public/app.js
2026-06-25 18:58:55 +02:00

350 lines
16 KiB
JavaScript
Raw Permalink 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.
document.addEventListener('DOMContentLoaded', function() {
function fmt(v) {
if (v === undefined || v === null || isNaN(v)) return '';
return Number(v).toFixed(0);
}
function updatePosition() {
fetch('/api/position')
.then(res => res.json())
.then(data => {
const p = data.position || {};
const m = data.motorCounts || {};
document.getElementById('state-x').textContent = fmt(p.x);
document.getElementById('state-y').textContent = fmt(p.y);
document.getElementById('state-z').textContent = fmt(p.z);
document.getElementById('state-phi').textContent = fmt(p.a*180/Math.PI);
document.getElementById('state-theta').textContent = fmt(p.b*180/Math.PI);
document.getElementById('state-psi').textContent = fmt(p.c*180/Math.PI);
// Greifer-Öffnung in mm (Workspace) — keine Grad-Umrechnung.
document.getElementById('state-e').textContent = fmt(p.e);
/* Motor-Zustand */
document.getElementById('motor-x').textContent = fmt(m.x);
document.getElementById('motor-y').textContent = fmt(m.y*180/Math.PI);
document.getElementById('motor-z').textContent = fmt(m.z*180/Math.PI);
document.getElementById('motor-a').textContent = fmt(m.a*180/Math.PI);
document.getElementById('motor-b').textContent = fmt(m.b*180/Math.PI);
document.getElementById('motor-c').textContent = fmt(m.c*180/Math.PI);
// Greifer-Motorwert (eMotor, abgeleitet aus e/b/c) — roh, wie motor-x.
document.getElementById('motor-e').textContent = fmt(m.e);
})
.catch(err => console.error('Error fetching position:', err));
}
function updateStatus() {
fetch('/api/status')
.then(response => response.json())
.then(data => {
// WebClients
const clientsUl = document.getElementById('clients');
clientsUl.innerHTML = '';
data.clients.forEach(client => {
const li = document.createElement('li');
li.textContent = client;
clientsUl.appendChild(li);
});
// Sender
const sendersUl = document.getElementById('senderList');
sendersUl.innerHTML = '';
data.senders.forEach(sender => {
const li = document.createElement('li');
const state = sender.state || 'disconnected';
if (sender.isGCodeReceiver === false) {
// Shelly / EmergencyStop: nur Name + Zustand, keine URL
li.textContent = `${sender.name}: ${state}`;
li.classList.add('shelly');
} else {
// Telnet / FluidNC: URL anzeigen wenn vorhanden
li.textContent = sender.url
? `${sender.name} (${sender.url}): ${state}`
: `${sender.name}: ${state}`;
}
li.classList.add(state.toLowerCase());
sendersUl.appendChild(li);
});
// Letzte Commands
const commandsUl = document.getElementById('commandList');
commandsUl.innerHTML = '';
data.lastCommands.forEach(cmd => {
const li = document.createElement('li');
li.textContent = cmd;
commandsUl.appendChild(li);
});
// Letzte Pings
const pingsUl = document.getElementById('pingList');
pingsUl.innerHTML = '';
data.lastPings.forEach(ping => {
const li = document.createElement('li');
li.textContent = ping;
pingsUl.appendChild(li);
});
})
.catch(error => console.error('Error fetching status:', error));
}
// ── Robot.json + History ─────────────────────────────────────────────────
let robotJsonActive = 'current';
let robotJsonLastSerialized = null;
function renderJsonTree(data, container) {
container.innerHTML = '';
const tree = document.createElement('div');
tree.className = 'json-tree';
for (const [key, value] of Object.entries(data)) {
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
const details = document.createElement('details');
const summary = document.createElement('summary');
summary.textContent = key;
details.appendChild(summary);
const pre = document.createElement('pre');
pre.textContent = JSON.stringify(value, null, 2);
details.appendChild(pre);
tree.appendChild(details);
} else {
const row = document.createElement('div');
row.className = 'json-scalar';
row.innerHTML =
`<span class="json-key">${key}</span>` +
`<span class="json-val">${JSON.stringify(value)}</span>`;
tree.appendChild(row);
}
}
container.appendChild(tree);
}
function updateRobotJson() {
const url = robotJsonActive === 'current'
? '/api/robot'
: `/api/robot/history/${robotJsonActive}`;
fetch(url)
.then(res => res.ok ? res.json() : Promise.reject(res.status))
.then(data => {
const serialized = JSON.stringify(data);
document.getElementById('robotJsonLabel').textContent =
robotJsonActive === 'current' ? '(aktuell)' : `(${robotJsonActive})`;
if (serialized !== robotJsonLastSerialized) {
robotJsonLastSerialized = serialized;
renderJsonTree(data, document.getElementById('robotJsonTree'));
}
})
.catch(err => {
document.getElementById('robotJsonTree').textContent = `Fehler: ${err}`;
});
}
function setHistoryActive(ts) {
robotJsonActive = ts;
robotJsonLastSerialized = null; // Neuaufbau erzwingen beim Snapshot-Wechsel
updateRobotJson();
document.querySelectorAll('#robotHistoryList li').forEach(l => {
l.classList.toggle('rh-active', l.dataset.ts === ts);
});
}
function updateRobotHistory() {
fetch('/api/robot/history')
.then(res => res.json())
.then(({ history }) => {
const ul = document.getElementById('robotHistoryList');
ul.innerHTML = '';
const liCurrent = document.createElement('li');
liCurrent.textContent = 'robot.json (aktuell)';
liCurrent.dataset.ts = 'current';
if (robotJsonActive === 'current') liCurrent.classList.add('rh-active');
liCurrent.addEventListener('click', () => setHistoryActive('current'));
ul.appendChild(liCurrent);
history.forEach(entry => {
const ts = entry.filename.slice(6, -5); // robot_YYYYMMDD_HHmmss.json → YYYYMMDD_HHmmss
const li = document.createElement('li');
li.textContent = entry.filename;
li.dataset.ts = ts;
if (robotJsonActive === ts) li.classList.add('rh-active');
li.addEventListener('click', () => setHistoryActive(ts));
ul.appendChild(li);
});
})
.catch(err => console.error('Error fetching robot history:', err));
}
// ── Emergency Stop Panel ─────────────────────────────────────────────
// SVG-Button: Farbe + Text je nach armed-Zustand.
// armed=true → rot "EMERGENCY STOP" → POST /api/emergency-stop
// armed=false → grün "START ROBOT" → POST /api/power-on
let _lastArmed = null;
function updateEmergencyStopButton(armed) {
if (armed === _lastArmed) return;
_lastArmed = armed;
const stops = document.querySelectorAll('#estopGrad stop');
const textPath = document.querySelector('#emergency-stop textPath');
const textEl = document.querySelector('#emergency-stop text');
const btnInner = document.querySelector('#emergency-stop circle:last-of-type');
const btnOuter = document.querySelector('#emergency-stop circle:first-of-type');
const label = document.getElementById('armed-status');
if (armed) {
// ── E-STOP: Gelber Ring + roter Pilz — laut, hervorspringend ────────────
if (btnOuter) { btnOuter.setAttribute('fill', '#FFD700'); btnOuter.setAttribute('stroke', '#C8960A'); }
if (btnInner) { btnInner.setAttribute('r', '21'); btnInner.setAttribute('stroke', '#660000'); }
if (stops[0]) stops[0].setAttribute('stop-color', '#ff5555');
if (stops[1]) stops[1].setAttribute('stop-color', '#cc0000');
if (stops[2]) stops[2].setAttribute('stop-color', '#880000');
if (textEl) textEl.setAttribute('fill', '#1a1000');
if (textPath) textPath.textContent = 'EMERGENCY STOP';
if (label) { label.textContent = 'Strom AN'; label.className = 'estop-armed-label armed'; }
} else {
// ── POWER ON: Dunkler Navy-Ring + blauer Knopf — ruhig, klar ────────────
// Kein gelber Ring → kein E-Stop-Charakter.
// Kompakter Knopf (r 19 statt 21) → wirkt wie klassischer Power-Switch.
if (btnOuter) { btnOuter.setAttribute('fill', '#1a3050'); btnOuter.setAttribute('stroke', '#0d1e33'); }
if (btnInner) { btnInner.setAttribute('r', '19'); btnInner.setAttribute('stroke', '#091929'); }
if (stops[0]) stops[0].setAttribute('stop-color', '#5c9fcf');
if (stops[1]) stops[1].setAttribute('stop-color', '#255f96');
if (stops[2]) stops[2].setAttribute('stop-color', '#0c3660');
if (textEl) textEl.setAttribute('fill', '#9dc8e8');
if (textPath) textPath.textContent = 'START ROBOT';
if (label) { label.textContent = '○ Kein Strom'; label.className = 'estop-armed-label disarmed'; }
}
}
// ── Click-Handler (einmalig registriert, liest _lastArmed dynamisch) ─────────
//
// Zeigt Lade-/Erfolgs-/Fehlerstatus unter dem Button.
// Schreibt console.warn ⚠️ für den Browser-Dev-Tools-Log.
async function handleEstopClick() {
const armed = _lastArmed;
const url = armed ? '/api/emergency-stop' : '/api/power-on';
const action = armed ? 'EmergencyStop' : 'PowerOn';
const statusEl = document.getElementById('estop-action-status');
if (statusEl) { statusEl.textContent = '⏳ …'; statusEl.className = 'estop-status'; }
console.warn(`⚠️ [${action}] wird ausgeführt …`);
try {
const res = await fetch(url, { method: 'POST' });
const data = await res.json();
const ok = data.ok || (data.results || []).every(r => r.ok || r.skipped);
if (ok) {
if (statusEl) { statusEl.textContent = `${action} OK`; statusEl.className = 'estop-status ok'; }
console.warn(`⚠️ [${action}] OK`);
} else {
const failed = (data.results || [])
.filter(r => !r.ok && !r.skipped)
.map(r => `${r.name}(${r.error || '?'})`)
.join(', ');
if (statusEl) { statusEl.textContent = `⚠️ Teilfehler: ${failed}`; statusEl.className = 'estop-status err'; }
console.warn(`⚠️ [${action}] Teilfehler — ${failed}`);
}
} catch (err) {
if (statusEl) { statusEl.textContent = `❌ Netzwerkfehler`; statusEl.className = 'estop-status err'; }
console.error(`❌ [${action}] Netzwerkfehler: ${err.message}`);
}
}
// onclick einmalig setzen — bleibt dauerhaft, Handler liest _lastArmed dynamisch.
const estopDiv = document.getElementById('emergency-stop');
if (estopDiv) estopDiv.onclick = handleEstopClick;
async function pollPowerStatus() {
try {
const res = await fetch('/api/power-status');
if (!res.ok) return;
const data = await res.json();
// Immer aktualisieren (auch bei ok:false → disarmed als sicherer Fallback)
updateEmergencyStopButton(data.ok ? data.armed : false);
} catch { /* Netzwerkfehler → Button bleibt im letzten Zustand */ }
}
// Sofort sicheren Startzustand (grün "START ROBOT") setzen, damit der onclick
// sofort aktiv ist — noch bevor der erste Poll antwortet.
updateEmergencyStopButton(false);
pollPowerStatus();
setInterval(pollPowerStatus, 2000);
const btnAlarmUnlock = document.getElementById('btn-alarm-unlock');
const alarmUnlockStatus = document.getElementById('alarm-unlock-status');
if (btnAlarmUnlock) {
btnAlarmUnlock.addEventListener('click', async () => {
btnAlarmUnlock.disabled = true;
alarmUnlockStatus.textContent = 'Wird ausgeführt…';
alarmUnlockStatus.className = 'estop-status';
try {
const res = await fetch('/api/alarm-unlock', { method: 'POST' });
const data = await res.json();
if (data.ok) {
alarmUnlockStatus.textContent = '✅ Alarm entsperrt';
alarmUnlockStatus.className = 'estop-status ok';
} else {
const failed = (data.results || [])
.filter(r => !r.ok && !r.skipped)
.map(r => r.name)
.join(', ');
alarmUnlockStatus.textContent = `⚠️ Fehlgeschlagen: ${failed || 'unbekannt'}`;
alarmUnlockStatus.className = 'estop-status err';
}
} catch (err) {
alarmUnlockStatus.textContent = `❌ Fehler: ${err.message}`;
alarmUnlockStatus.className = 'estop-status err';
} finally {
btnAlarmUnlock.disabled = false;
}
});
}
updateStatus();
updatePosition();
updateRobotJson();
updateRobotHistory();
setInterval(() => {
updateStatus();
updatePosition();
if (robotJsonActive === 'current') updateRobotJson();
}, 1000);
setInterval(updateRobotHistory, 30000);
});
document.querySelectorAll('.section').forEach(sec => {
const id = sec.dataset.id;
const saved = localStorage.getItem('section_' + id);
if (saved === 'collapsed') sec.classList.add('collapsed');
sec.querySelector('h2').addEventListener('click', () => {
sec.classList.toggle('collapsed');
localStorage.setItem(
'section_' + id,
sec.classList.contains('collapsed') ? 'collapsed' : 'open'
);
});
});
/* Initial-Zustand
['clients','pings'].forEach(id => {
if (!localStorage.getItem('section_' + id))
localStorage.setItem('section_' + id, 'collapsed');
});
*/