# 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: 1. **Feed Hold** — alle FluidNC-Controller erhalten das Realtime-Byte `!` (sofortiger Bewegungs-Stopp, keine Verzögerung) 2. **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 ```json "controllers": { "base": { "ip": "...", "port": 2300, "protocol": "telnet", ... }, "elbow": { ... }, "hand": { ... }, "emergencyStop": { "protocol": "shelly", "url": "http:///rpc/Switch.Set?id=0&on=false", "urlOn": "http:///rpc/Switch.Set?id=0&on=true", "urlStatus": "http:///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 ```js 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 ```json { "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 '!' → ` - 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: ```js 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: ```json { "ok": true, "armed": true, "voltage": 234.9, "power": 15.5 } ``` Kein Shelly konfiguriert: ```json { "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]` ```json { "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`: ```json { "name": "EmergencyStop", "isGCodeReceiver": false, "state": "stopped", ... } ``` `false` = Shelly (kein G-Code-Empfänger, keine URL in der UI anzeigen). --- ## startRobot.js — Sender-Erzeugung ```js 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 ```js // 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 `_lastArmed` dynamisch 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 |