Files
appRobotDriver/doc/15_EmergencyStop_done.md
2026-06-12 23:43:21 +02:00

11 KiB
Raw Blame History

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

"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/statusisGCodeReceiver

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-statusupdateEmergencyStopButton(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. 510 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