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); document.getElementById('state-e').textContent = fmt(m.e*180/Math.PI); /* 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); 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'; const label = sender.url ? `${sender.name} (${sender.url}): ${state}` : `${sender.name}: ${state}`; li.textContent = label; 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 + Click-Handler je nach armed-Zustand wechseln. // 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 btnInner = document.querySelector('#emergency-stop circle:last-of-type'); const label = document.getElementById('armed-status'); if (armed) { // Rot: Roboter bestromt → Klick = Emergency Stop 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 (btnInner) btnInner.setAttribute('stroke', '#660000'); if (textPath) textPath.textContent = 'EMERGENCY STOP'; if (label) { label.textContent = '● Bestromt'; label.className = 'estop-armed-label armed'; } } else { // Grün: Strom AUS → Klick = Strom einschalten (Start Robot) if (stops[0]) stops[0].setAttribute('stop-color', '#88ff99'); if (stops[1]) stops[1].setAttribute('stop-color', '#00aa44'); if (stops[2]) stops[2].setAttribute('stop-color', '#005522'); if (btnInner) btnInner.setAttribute('stroke', '#003311'); if (textPath) textPath.textContent = 'START ROBOT'; if (label) { label.textContent = '○ Kein Strom'; label.className = 'estop-armed-label disarmed'; } } const div = document.getElementById('emergency-stop'); if (div) { div.onclick = armed ? () => fetch('/api/emergency-stop', { method: 'POST' }) : () => fetch('/api/power-on', { method: 'POST' }); } } async function pollPowerStatus() { try { const res = await fetch('/api/power-status'); if (!res.ok) return; const data = await res.json(); if (data.ok) updateEmergencyStopButton(data.armed); } catch { /* Netzwerkfehler → Button bleibt im letzten Zustand */ } } 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'); }); */