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 = `${key}` + `${JSON.stringify(value)}`; 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'); }); */