Dokumentation

This commit is contained in:
chk
2026-06-12 23:43:21 +02:00
parent 6de258bc01
commit b19489d836
4 changed files with 342 additions and 2 deletions

View File

@@ -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://<IP>/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://<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"
}
}
}
```
@@ -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 14 in appRobotDriver ✅, Schritte 57 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

View File

@@ -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://<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
```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 '!' → <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:
```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. 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 |

View File

@@ -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

View File

@@ -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