Emergency Stop
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
Reference in New Issue
Block a user