Emergency Stop

This commit is contained in:
chk
2026-06-12 18:16:15 +02:00
parent 6fc6605080
commit 59d4cf7df4
17 changed files with 1098 additions and 540 deletions

View File

@@ -173,6 +173,91 @@ document.addEventListener('DOMContentLoaded', function() {
.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();

View File

@@ -76,6 +76,43 @@
<h2>Robot.json History</h2>
<ul id="robotHistoryList"></ul>
</div>
<div id="emergencyStopPanel" class="section half" data-id="emergencystop">
<h2>Emergency Stop</h2>
<div class="estop-actions">
<!-- E-Stop / Start-Button: Farbe + Text wechseln je nach Strom-Zustand -->
<div class="estop-center">
<div id="emergency-stop" title="Emergency Stop">
<svg width="78" height="78" viewBox="0 0 78 78" xmlns="http://www.w3.org/2000/svg">
<defs>
<radialGradient id="estopGrad" cx="38%" cy="32%" r="62%">
<stop offset="0%" stop-color="#ff5555"/>
<stop offset="65%" stop-color="#cc0000"/>
<stop offset="100%" stop-color="#880000"/>
</radialGradient>
<path id="estop-textarc" d="M 9,39 A 30,30 0 0,0 69,39"/>
</defs>
<!-- Äusserer Ring -->
<circle cx="39" cy="39" r="36" fill="#FFD700" stroke="#C8960A" stroke-width="1.5"/>
<!-- Knopf -->
<circle cx="39" cy="39" r="21" fill="url(#estopGrad)" stroke="#660000" stroke-width="2"/>
<!-- Gebogener Text -->
<text font-size="7.5" fill="#1a1000" font-weight="bold"
font-family="system-ui,sans-serif" letter-spacing="0.3" word-spacing="5">
<textPath href="#estop-textarc" startOffset="50%" text-anchor="middle">EMERGENCY STOP</textPath>
</text>
</svg>
</div>
<div id="armed-status" class="estop-armed-label"></div>
</div>
<button id="btn-alarm-unlock" class="btn btn-unlock">
Restart — Alarm-Unlock
</button>
<div id="alarm-unlock-status" class="estop-status"></div>
</div>
</div>
</div>
<script src="app.js"></script>

View File

@@ -256,4 +256,90 @@ h1 {
#robotHistoryList li.rh-active::before {
content: "▶ ";
font-size: 10px;
}
}
/* ── Emergency Stop Panel ────────────────────────────────────────────────── */
.estop-actions {
margin-top: 12px;
display: flex;
flex-direction: column;
gap: 12px;
}
/* Zentriert den SVG-Button horizontal im Panel */
.estop-center {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
/* SVG E-Stop / Start-Button */
#emergency-stop {
display: block; /* override framework display:none wenn nötig */
position: relative;
width: 78px;
height: 78px;
cursor: pointer;
flex-shrink: 0;
transition: transform 0.1s ease;
}
#emergency-stop:hover svg {
filter: brightness(1.12);
}
#emergency-stop:active {
transform: scale(0.93);
}
/* Zustandsanzeige unter dem Button */
.estop-armed-label {
font-size: 12px;
font-weight: bold;
min-height: 16px;
color: var(--muted);
}
.estop-armed-label.armed { color: #e74c3c; }
.estop-armed-label.disarmed { color: #2ecc71; }
.btn {
display: inline-block;
padding: 9px 18px;
border: none;
border-radius: 5px;
font-size: 14px;
font-family: Arial, sans-serif;
font-weight: bold;
cursor: pointer;
transition: background 0.15s, opacity 0.15s;
align-self: flex-start;
}
.btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
/* Alarm-Unlock: Bernstein / Orange — Recovery-Aktion */
.btn-unlock {
background: #c87800;
color: #fff;
}
.btn-unlock:hover:not(:disabled) {
background: #a56200;
}
/* Status-Zeile unterhalb des Buttons */
.estop-status {
font-size: 13px;
min-height: 18px;
color: var(--text);
opacity: 0.7;
}
.estop-status.ok { color: #2ecc71; opacity: 1; }
.estop-status.err { color: #e74c3c; opacity: 1; }