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');
});
*/