From b19489d836ff934a6b778ab4b7f3dfa17b413976 Mon Sep 17 00:00:00 2001 From: chk <79915315+ChKendel@users.noreply.github.com> Date: Fri, 12 Jun 2026 23:43:21 +0200 Subject: [PATCH] Dokumentation --- README.md | 22 ++- doc/15_EmergencyStop_done.md | 315 +++++++++++++++++++++++++++++++++++ logs/gcode_commands.log | 5 + logs/pings.log | 2 + 4 files changed, 342 insertions(+), 2 deletions(-) create mode 100644 doc/15_EmergencyStop_done.md diff --git a/README.md b/README.md index 8c6f640..dd99996 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,12 @@ Die Eingaben kommen per WebSocket an den HTTPS-Server und werden in `server/Inpu - Statischer Bearer-Token für `PUT /api/robot`. Fehlt die Variable, generiert `RobotConfigService` beim ersten Start einen zufälligen Key und speichert ihn in `data/robot/.apikey` (nicht im Repo). Der Key wird beim Start einmalig geloggt. +- `SHELLY_URL` + - URL für den Shelly Smart Plug Emergency-Stop: `http:///rpc/Switch.Set?id=0&on=false` + - Überschreibt `controllers.emergencyStop.url` aus `robot.json` (analog zu `GRBL_BASE_IP`). + - **Wichtig in Docker:** `.local`-mDNS-Hostnamen werden im Container nicht aufgelöst — + stattdessen die echte IP verwenden (z.B. `http://192.168.0.99/rpc/Switch.Set?id=0&on=false`). + - Details: `doc/15_EmergencyStop_done.md` ### HTTPS-Konfiguration @@ -125,7 +131,13 @@ Relevante Abschnitte für den Driver: "controllers": { "base": { "ip": "fluidNcBase.local", "port": 2300, "protocol": "telnet", "axes": ["x","y","z"], "heartbeatInterval": 10000 }, "elbow": { "ip": "fluidNcEllbow.local", "port": 5000, "protocol": "telnet", "axes": ["a",null,null], "heartbeatInterval": 10000 }, - "hand": { "ip": "fluidNcHand.local", "port": 5000, "protocol": "telnet", "axes": ["c","e","b"], "heartbeatInterval": 10000 } + "hand": { "ip": "fluidNcHand.local", "port": 5000, "protocol": "telnet", "axes": ["c","e","b"], "heartbeatInterval": 10000 }, + "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" + } } } ``` @@ -172,11 +184,15 @@ Socket geschlossen und der bestehende Reconnect-Mechanismus startet automatisch. - `/style.css` - `/allApps.css` - API-Endpunkte: - - `/api/status` + - `/api/status` — Sender-Status inkl. `isGCodeReceiver`-Flag - `/api/position` - `/api/robot` — `GET`: aktuelle `robot.json`; `PUT`: überschreibt sie (Auth erforderlich) - `/api/robot/history` — Liste aller Snapshots - `/api/robot/history/:ts` — einen bestimmten Snapshot abrufen + - `/api/power-status` — Shelly-Schaltzustand (`armed: true/false`, Spannung, Leistung) + - `/api/emergency-stop` — `POST`: Feed Hold `!` an alle FluidNC + Shelly Strom AUS + - `/api/power-on` — `POST`: Shelly Strom EIN + - `/api/alarm-unlock` — `POST`: `$X` an alle FluidNC (nach Strom-Neustart) ## Wichtige Dateien @@ -194,6 +210,7 @@ Socket geschlossen und der bestehende Reconnect-Mechanismus startet automatisch. - `robot/RobotController.js` — wendet geparste Befehle auf das Modell an (Steuerlogik) - `robot/GCode.js` — Fassade + Datei-Befehle - `robot/TelnetSenderGRBL.js` +- `robot/ShellyEmergencyStop.js` — steuert Shelly Smart Plug als Emergency-Stop-Aktor (HTTP GET, kein GCode) - `robot/fluidnc/FluidNCClient.js` — alternative WebSocket-basierte FluidNC-Anbindung mit Reconnect-Logik (noch nicht integriert) - `GCodeFiles/` — enthalten Beispiel- und Log-G-Code-Dateien @@ -224,6 +241,7 @@ Architektur- und Refactoring-Aufgaben sind in `doc/ToDo_*.md` dokumentiert: | `doc/ToDo_10_VerbindungsVerlust.md` | Verbindungsverlust erkennen, Watchdog, UI-Statusanzeige | offen | | `doc/ToDo_12_InverseKinematikConfig_ROADMAP.md` | Austauschbare Kinematik: RobotBase, KinematicsFactory, Grab-It | ✅ erledigt | | `doc/ToDo_14_robot_json_service.md` | robot.json als REST-Service, RobotConfigService, RobotConfig | teilweise (Schritte 1–4 in appRobotDriver ✅, Schritte 5–7 offen) | +| `doc/15_EmergencyStop_done.md` | Emergency Stop: Shelly + FluidNC Feed Hold, API, UI, Restart-Ablauf | ✅ erledigt | | `doc/ToDo_49_Cleanup.md` | Pre-Release-Cleanup: tote Code, Zertifikate, ToDos, README | offen | ### Empfohlene Bearbeitungsreihenfolge diff --git a/doc/15_EmergencyStop_done.md b/doc/15_EmergencyStop_done.md new file mode 100644 index 0000000..207d441 --- /dev/null +++ b/doc/15_EmergencyStop_done.md @@ -0,0 +1,315 @@ +# 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 | diff --git a/logs/gcode_commands.log b/logs/gcode_commands.log index 6ec7abb..a659fdd 100644 --- a/logs/gcode_commands.log +++ b/logs/gcode_commands.log @@ -10429,3 +10429,8 @@ 2026-06-12T21:15:19.320Z ::ffff:127.0.0.1: M114 2026-06-12T21:15:19.533Z ::ffff:127.0.0.1: G1 X1 Y2 Z3 2026-06-12T21:15:19.762Z ::ffff:127.0.0.1: G1 X1 +2026-06-12T21:21:46.340Z ::ffff:127.0.0.1: M114 +2026-06-12T21:21:46.356Z ::ffff:127.0.0.1: G1 X1 Y2 Z3 +2026-06-12T21:21:46.525Z ::ffff:127.0.0.1: M114 +2026-06-12T21:21:46.740Z ::ffff:127.0.0.1: G1 X1 Y2 Z3 +2026-06-12T21:21:46.964Z ::ffff:127.0.0.1: G1 X1 diff --git a/logs/pings.log b/logs/pings.log index 656598c..1f59253 100644 --- a/logs/pings.log +++ b/logs/pings.log @@ -14646,3 +14646,5 @@ 2026-06-12T21:13:04.257Z ::ffff:127.0.0.1 : Ping 2026-06-12T21:15:19.049Z ::ffff:127.0.0.1 : Ping 2026-06-12T21:15:19.098Z ::ffff:127.0.0.1 : Ping +2026-06-12T21:21:46.291Z ::ffff:127.0.0.1 : Ping +2026-06-12T21:21:46.316Z ::ffff:127.0.0.1 : Ping