350 lines
16 KiB
JavaScript
350 lines
16 KiB
JavaScript
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');
|
||
});
|
||
*/ |