Files
appRobotDriver/public/app.js
2026-06-12 18:47:28 +02:00

330 lines
14 KiB
JavaScript
Raw 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);
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 =
`<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 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
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'; }
}
}
// ── 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');
});
*/