11 KiB
15 — Emergency Stop (NotAus) — Implementierung
Datum: 2026-06-12
Status: ✅ implementiert und getestet
Überblick
Der Emergency Stop schaltet den Roboter auf zwei Wegen sofort ab:
- Feed Hold — alle FluidNC-Controller erhalten das Realtime-Byte
!(sofortiger Bewegungs-Stopp, keine Verzögerung) - Stromabschaltung — ein Shelly Smart Plug schneidet die Versorgungsspannung über einen HTTP-GET-Aufruf ab
Beide Aktionen laufen parallel (Promise.allSettled). Fällt der Shelly-Aufruf aus, stoppt der Feed-Hold trotzdem die Bewegung — und umgekehrt.
Nach dem Neustart der Anlage wird mit Alarm-Unlock ($X) jeder FluidNC-Controller aus dem ALARM-Zustand befreit, bevor neue Bewegungsbefehle akzeptiert werden.
Architektur
robot.json (controllers.emergencyStop)
│
▼
RobotConfig.load() ← liest protocol, url, urlOn, urlStatus
│
▼
startRobot.js ← verzweigt auf ShellyEmergencyStop (isGCodeReceiver: false)
│ oder TelnetSenderGRBL (isGCodeReceiver: true)
▼
InfoServer (senders[]) ← hält alle Sender-Instanzen
│
├── POST /api/emergency-stop → instance.emergencyStop() auf ALLEN Sendern
├── POST /api/power-on → instance.powerOn() nur auf Shelly-Sendern
├── POST /api/alarm-unlock → instance.alarmUnlock() auf ALLEN Sendern
└── GET /api/power-status → instance.getArmed() des ersten Shelly-Senders
Beteiligte Dateien
| Datei | Rolle |
|---|---|
robot/SenderInterface.js |
Abstrakte Basisklasse; Default-no-ops für emergencyStop() und alarmUnlock() |
robot/TelnetSenderGRBL.js |
Überschreibt emergencyStop() → sendet !; alarmUnlock() → sendet $X\r\n |
robot/ShellyEmergencyStop.js |
Neue Klasse: steuert Shelly Smart Plug per HTTP GET |
robot/RobotConfig.js |
Liest emergencyStop-Eintrag aus robot.json (protocol, url, urlOn, urlStatus) |
startRobot.js |
Erzeugt ShellyEmergencyStop-Instanz, setzt isGCodeReceiver: false |
server/InfoServer.js |
Stellt die vier API-Endpunkte bereit, gibt isGCodeReceiver im Status aus |
public/app.js |
SVG-Button, Polling /api/power-status, Click-Handler mit console.warn |
public/index.html |
Emergency-Stop-Panel (SVG-Button, Alarm-Unlock-Button, Status-Divs) |
public/style.css |
Stile für Panel, SVG-Button, Alarm-Unlock-Button, Status-Texte |
data/robot/robot.json |
Konfiguration: controllers.emergencyStop mit allen drei URLs |
robot.json — Konfiguration
"controllers": {
"base": { "ip": "...", "port": 2300, "protocol": "telnet", ... },
"elbow": { ... },
"hand": { ... },
"emergencyStop": {
"protocol": "shelly",
"url": "http://<SHELLY-IP>/rpc/Switch.Set?id=0&on=false",
"urlOn": "http://<SHELLY-IP>/rpc/Switch.Set?id=0&on=true",
"urlStatus": "http://<SHELLY-IP>/rpc/Switch.GetStatus?id=0"
}
}
Wichtig — Docker/mDNS: .local-Hostnamen (mDNS) werden innerhalb von Docker-Containern nicht aufgelöst. Die FluidNC-Controller werden über GRBL_BASE_IP etc. als echte IPs konfiguriert; für den Shelly muss entweder die IP direkt in robot.json eingetragen werden, oder die Env-Variable SHELLY_URL wird gesetzt.
Env-Variable SHELLY_URL
SHELLY_URL=http://192.168.0.99/rpc/Switch.Set?id=0&on=false
Überschreibt url aus robot.json (analog zu GRBL_BASE_IP). urlOn und urlStatus werden dann automatisch aus SHELLY_URL abgeleitet. Werden urlOn/urlStatus explizit in robot.json gesetzt, haben diese Vorrang.
ShellyEmergencyStop.js
Konstruktor
new ShellyEmergencyStop(url, options = {})
// url = Switch.Set?id=0&on=false (Off-URL)
// options.urlOn = Switch.Set?id=0&on=true (optional, sonst aus url abgeleitet)
// options.urlStatus = Switch.GetStatus?id=0 (optional, sonst aus url abgeleitet)
URL-Ableitung (Fallback, wenn nicht explizit konfiguriert):
_onUrl = url.replace('on=false', 'on=true')_statusUrl = url.replace(/\/rpc\/.*$/, '/rpc/Switch.GetStatus?id=0')
Methoden
| Methode | Aktion | Shelly-RPC |
|---|---|---|
emergencyStop() |
Strom abschalten | Switch.Set?id=0&on=false |
powerOn() |
Strom einschalten | Switch.Set?id=0&on=true |
alarmUnlock() |
no-op (kein FluidNC-Alarm) | — |
getArmed() |
Schaltzustand abfragen | Switch.GetStatus?id=0 |
getStatus() |
Sender-Status für InfoServer | — |
getArmed() — Rückgabe
{ "ok": true, "armed": true, "voltage": 234.9, "power": 15.5 }
armed: true = output: true im Shelly-Response = Strom eingeschaltet = Roboter bestromt.
State-Machine
| State | Bedeutung |
|---|---|
ready |
Shelly bereit, noch kein Aufruf |
stopped |
emergencyStop() erfolgreich ausgeführt |
error |
HTTP-Fehler oder Netzwerkfehler |
disconnected |
disconnect() aufgerufen |
TelnetSenderGRBL — Emergency Stop
emergencyStop()
Sendet das Realtime-Byte ! (FluidNC Feed Hold):
- Kein Zeilenende (
\r\n) — sofortige Wirkung, unabhängig vom aktuellen Befehlspuffer - Loggt:
⚠️ [EmergencyStop] Feed Hold '!' → <hostname> - Gibt
{ ok: false, error: 'not connected' }wenn kein Socket
alarmUnlock()
Sendet $X\r\n (FluidNC Alarm Unlock):
- Entsperrt den ALARM-Zustand nach Power-Loss (z.B.
ALARM:1) - Nur aufrufen nachdem Roboterstellung manuell geprüft wurde
SenderInterface — Default-no-ops
SenderInterface.js definiert Basisimplementierungen, damit alle Sender das Interface erfüllen ohne jede Methode selbst zu implementieren:
async emergencyStop() { return { ok: true, skipped: true }; }
async alarmUnlock() { return { ok: true, skipped: true }; }
skipped: true wird von InfoServer als Erfolg gewertet (zählt nicht als Fehler).
InfoServer — API-Endpunkte
GET /api/power-status
Fragt den Shelly (ersten Sender mit getArmed()-Methode) nach dem Schaltzustand:
{ "ok": true, "armed": true, "voltage": 234.9, "power": 15.5 }
Kein Shelly konfiguriert:
{ "ok": false, "armed": false, "error": "no shelly configured" }
POST /api/emergency-stop
Ruft emergencyStop() auf allen Sendern parallel auf:
- FluidNC: sendet
! - Shelly: schneidet Strom ab
- Loggt:
⚠️ [EmergencyStop] 2026-06-12T... — [Base:ok, Elbow:ok, Hand:ok, EmergencyStop:ok]
{ "ok": true, "at": "2026-06-12T16:34:08.863Z", "results": [...] }
POST /api/power-on
Ruft powerOn() nur auf Sendern auf, die diese Methode haben (Shelly):
- Loggt:
⚠️ [PowerOn] 2026-06-12T...
POST /api/alarm-unlock
Ruft alarmUnlock() auf allen Sendern auf:
- FluidNC: sendet
$X\r\n - Shelly: skipped
- Loggt:
⚠️ [AlarmUnlock] 2026-06-12T...
GET /api/status — isGCodeReceiver
Das Status-Objekt jedes Senders enthält neu das Feld isGCodeReceiver:
{
"name": "EmergencyStop",
"isGCodeReceiver": false,
"state": "stopped",
...
}
false = Shelly (kein G-Code-Empfänger, keine URL in der UI anzeigen).
startRobot.js — Sender-Erzeugung
for (const [key, ctrl] of Object.entries(cfg.controllers)) {
if (ctrl.protocol === 'shelly') {
const instance = new ShellyClass(ctrl.url, {
urlOn: ctrl.urlOn,
urlStatus: ctrl.urlStatus,
});
senders.push({ name, instance, isGCodeReceiver: false });
} else {
// Telnet (FluidNC)
const instance = new TelnetSenderClass(ctrl.ip, ctrl.port, ...axes7, { heartbeatInterval });
senders.push({ name, instance, isGCodeReceiver: true });
}
}
// Nur Telnet-Sender empfangen G-Code
senders.filter(s => s.isGCodeReceiver).forEach(s => robot.cmdReceivers.push(s.instance));
Frontend — app.js
SVG-Button (Zwei-Zustands-Design)
Der SVG-Button wechselt komplett das Erscheinungsbild je nach armed-Zustand:
| Zustand | Outer Ring | Inner Button | Gradient | Text |
|---|---|---|---|---|
armed: true |
Gold #FFD700 |
Rot, r=21 | #ff5555 → #cc0000 → #880000 |
EMERGENCY STOP |
armed: false |
Navy #1a3050 |
Blau, r=19 | #5c9fcf → #255f96 → #0c3660 |
START ROBOT |
Polling
// Alle 2 Sekunden + sofort beim Start
pollPowerStatus();
setInterval(pollPowerStatus, 2000);
GET /api/power-status → updateEmergencyStopButton(data.armed).
Startzustand: updateEmergencyStopButton(false) — damit der onclick sofort aktiv ist, bevor der erste Poll antwortet.
Click-Handler (handleEstopClick)
- Liest
_lastArmeddynamisch zum Klick-Zeitpunkt - Zeigt Ladezustand (
⏳ …), Erfolg (✅ EmergencyStop OK) oder Fehler (⚠️ Teilfehler: …) im#estop-action-status-Div - Schreibt
console.warn('⚠️ [EmergencyStop] …')für Browser-DevTools-Log
FluidNC-Controller-Panel
Shelly-Sender werden ohne URL angezeigt (isGCodeReceiver === false), mit CSS-Kreisen statt Emoji:
| State | Farbe |
|---|---|
ready |
Hellblau #89bcde |
stopped |
Fast-Weiß #dce9f5 |
error |
Gedämpft-Rot #9a3a3a |
disconnected |
Grau #4a5a6a |
Restart-Ablauf nach NotAus
1. Roboter mechanisch prüfen — steht er frei?
2. [START ROBOT] klicken → POST /api/power-on
└── Shelly schaltet Strom ein
3. FluidNC bootet (ca. 5–10 s)
4. [Restart — Alarm-Unlock] klicken → POST /api/alarm-unlock
└── alle FluidNC-Controller erhalten $X\r\n
5. Roboter ist wieder betriebsbereit
Logging
| Ort | Level | Beispiel |
|---|---|---|
| Node / Container | console.warn |
⚠️ [EmergencyStop] 2026-06-12T16:34:08Z — [Base:ok, Elbow:ok, Hand:ok, EmergencyStop:ok] |
| Node / Container | console.warn |
⚠️ [EmergencyStop] Feed Hold '!' → fluidNcBase.local |
| Node / Container | console.warn |
⚠️ [Shelly] power OFF → OK |
| Browser DevTools | console.warn |
⚠️ [EmergencyStop] wird ausgeführt … |
| Browser DevTools | console.warn |
⚠️ [EmergencyStop] OK |
Tests
| Datei | Abgedeckte Szenarien |
|---|---|
test/ShellyEmergencyStop.test.js |
Konstruktor, URL-Ableitung, explizite urlOn/urlStatus, emergencyStop/powerOn/getArmed (ok, HTTP-Fehler, Netzwerkfehler, null-URL) |
test/Sender.Telnet.emergencyStop.test.js |
TelnetSenderGRBL.emergencyStop/alarmUnlock (verbunden, getrennt, write-Fehler); SenderInterface-Defaults |
test/RobotConfig.test.js |
DEFAULTS.emergencyStop, url/urlOn/urlStatus aus robot.json, SHELLY_URL-Env-Override, keine ip/port/axes |
test/StartRobot.test.js |
4 Sender inkl. Shelly; isGCodeReceiver:false; Shelly nicht in cmdReceivers |
test/InfoServer.test.js |
POST /api/emergency-stop, /api/alarm-unlock, /api/power-on; GET /api/power-status (mit/ohne Shelly); isGCodeReceiver im Status |