Kleine Arbeiten
This commit is contained in:
12
README.md
12
README.md
@@ -230,11 +230,13 @@ Architektur- und Refactoring-Aufgaben sind in `doc/ToDo_*.md` dokumentiert:
|
||||
| `doc/ToDo_1_Parsing.md` | G-Code-Parser-Schicht einführen | ✅ erledigt |
|
||||
| `doc/ToDo_2_Anbindung.md` | Sender-Interface und Orchestrierung | ✅ erledigt |
|
||||
| `doc/ToDo_3_Config.md` | Zentralisierte Konfiguration | offen |
|
||||
| `doc/ToDo_4_GCode.md` | G-Code- und Datei-Handling trennen | offen |
|
||||
| `doc/ToDo_4_GCode.md` | G-Code- und Datei-Handling trennen | ✅ ausgelagert → `appRobotFileservice` |
|
||||
| `doc/ToDo_5_API.md` | WebSocket-Antwortlogik strukturieren | ✅ erledigt |
|
||||
| `doc/ToDo_6_RobotController.md` | RobotController-Klasse einführen | ✅ erledigt |
|
||||
| `doc/ToDo_6a_Speed.md` | Speed-Steuerung: Schalter, `calculateSpeeds()`-Fix, koordinierte Feedrate | ✅ erledigt (WS-Sender offen) |
|
||||
| `doc/ToDo_6b_FileHandling.md` | File-Handling: fehlende Befehle, Cursor im Speicher, Fehler-Feedback | offen |
|
||||
| `doc/ToDo_6b_FileHandling.md` | File-Handling: fehlende Befehle, Cursor im Speicher, Fehler-Feedback | ✅ ausgelagert → `appRobotFileservice` |
|
||||
| `doc/draft_filehandeling.md` | File-Handling als externes Projekt `appRobotFileservice` (Driver als Gateway, FCode-Pass-through) | Entwurf |
|
||||
| `doc/draft_filehandeling_API.md` | API der `appRobotFileservice` (Programme, aktiver Cursor, Teaching/Playback) | Entwurf |
|
||||
| `doc/ToDo_7_Tests.md` | Testabdeckung und Stabilität | teilweise |
|
||||
| `doc/ToDo_8_Bugs.md` | Bekannte konkrete Bugs | teilweise |
|
||||
| `doc/ToDo_9_HardwareFeedback.md` | Hardware-Feedback-Loop (GRBL-Antworten, Command-Queue, Positionsabgleich) | teilweise (Baustein Port→Motor ✅, Pakete 1–6 offen) |
|
||||
@@ -251,9 +253,9 @@ ToDo_8 Bugs beheben — kurz, blockiert nichts anderes
|
||||
ToDo_3 Config — Fundament für alles Weitere
|
||||
ToDo_1 Parser ┐
|
||||
ToDo_6 RobotController ┘ zusammen, da eng verzahnt
|
||||
ToDo_4 Datei-Handling — danach, klar abgrenzbar
|
||||
ToDo_4 Datei-Handling — ausgelagert → appRobotFileservice (siehe drafts)
|
||||
ToDo_6a Speed-Steuerung — calculateSpeeds bugfix, dann Sender-Integration
|
||||
ToDo_6b File-Handling Detail — fehlende F-Befehle, Cursor im Speicher
|
||||
ToDo_6b File-Handling Detail — ausgelagert → appRobotFileservice (siehe drafts)
|
||||
ToDo_2 Sender-Interface — mit Entscheidung: Telnet vs. FluidNC-WebSocket
|
||||
ToDo_9 Hardware-Feedback — baut auf ToDo_2 auf
|
||||
ToDo_10 Verbindungsverlust — baut auf ToDo_2 auf, parallel zu ToDo_9 möglich
|
||||
@@ -264,7 +266,7 @@ ToDo_7 Tests — begleitend zu allen obigen
|
||||
Kurzübersicht weiterer offener Punkte:
|
||||
|
||||
- [ ] Dokumentation der vollständigen G-Code-Syntax erweitern
|
||||
- [ ] `FFirst`/`FLast`-Befehle in `GCode.receiveFC()` implementieren
|
||||
- [x] `FFirst`/`FLast` (und übriges File-Handling) → ausgelagert in `appRobotFileservice` (siehe `doc/draft_filehandeling.md`)
|
||||
- [ ] `ROBOT_USE_SPEED_CALC` und `motorSpeeds` im echten Betrieb prüfen
|
||||
- [ ] `FluidNCClient.js` evaluieren: als Ersatz oder Ergänzung zu `TelnetSenderGRBL`?
|
||||
- [x] HTTPS-Passphrase aus Env-Variable (`HTTPS_PASSPHRASE`) — erledigt
|
||||
|
||||
@@ -3,8 +3,6 @@ services:
|
||||
appRobotGuacamole:
|
||||
image: abesnier/guacamole:latest
|
||||
container_name: appRobot_guacamole
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- /home/chk/Documents/appServerInstallation/guacamole/config:/config/guacamole
|
||||
- /home/chk/Documents/appServerInstallation/guacamole/postgres:/config/postgres
|
||||
@@ -29,6 +27,9 @@ services:
|
||||
- GRBL_ELLBOW_IP=192.168.0.202
|
||||
- GRBL_HAND_IP=192.168.0.250
|
||||
- ROBOT_SPEED_MODE=correct
|
||||
- ROBOT_KINEMATICS=arm3segmentlinearx
|
||||
- ROBOT_GRBL_AUTOREPORT=true
|
||||
- ROBOT_GRBL_REPORT_INTERVAL=200
|
||||
ports:
|
||||
- "2098:2098"
|
||||
- "2081:2081"
|
||||
@@ -36,6 +37,7 @@ services:
|
||||
- default
|
||||
|
||||
|
||||
|
||||
appRobotSimulation:
|
||||
container_name: appRobot_Simulation
|
||||
image: node:24-alpine
|
||||
@@ -45,6 +47,8 @@ services:
|
||||
environment:
|
||||
- TARGET_SERVER=wss://appRobot_Driver:2095
|
||||
command: npm start
|
||||
depends_on:
|
||||
- appRobotDriver
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "1003:1003"
|
||||
@@ -70,50 +74,29 @@ services:
|
||||
- appRobotDriver
|
||||
restart: unless-stopped
|
||||
|
||||
|
||||
appRobotHoming:
|
||||
image: node:24-alpine
|
||||
image: node:20-bullseye
|
||||
container_name: appRobot_Homing
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- /home/chk/Documents/appRobotHoming:/app
|
||||
- /home/chk/Documents/AppRobotVideo/public/snapshots:/app/public/snapshots
|
||||
environment:
|
||||
- WSS_VIDEO_DRIVER=wss://localhost:8448
|
||||
- WSS_VIDEO_DRIVER=wss://appRobot_Webcam:8448
|
||||
- WSS_URL=wss://appRobot_Driver:2095
|
||||
- HTTPS_PORT=2093
|
||||
- WEBCAM_URL=http://appRobotWebcam:8444
|
||||
- WEBCAM_URL=http://appRobot_Webcam:8444
|
||||
- BODYTRACKER_URL=http://appRobotBodyTracker:8446
|
||||
ports:
|
||||
- "2093:2093"
|
||||
depends_on:
|
||||
- appRobotDriver
|
||||
command: >
|
||||
/bin/sh -lc "npm ci || npm install && node server/server.js"
|
||||
/bin/bash -lc "apt-get update -qq && apt-get install -y --no-install-recommends python3-pip && pip3 install --quiet --no-cache-dir opencv-python-headless numpy && npm ci || npm install && node server/server.js"
|
||||
networks:
|
||||
- default
|
||||
restart: unless-stopped
|
||||
|
||||
# appRobotDirectBase:
|
||||
# image: node:20-bullseye
|
||||
# container_name: appRobot_DirectBase
|
||||
# network_mode: host
|
||||
# working_dir: /app
|
||||
# volumes:
|
||||
# - /home/chk/Documents/appRobotDirectControlBase:/app
|
||||
# environment:
|
||||
# - FluidNcHost=192.168.0.183
|
||||
# - FluidNcPort=80
|
||||
# - PORT=2098
|
||||
# ports:
|
||||
# - "2098:2098"
|
||||
# command: >
|
||||
# /bin/bash -lc "
|
||||
# npm ci || npm install &&
|
||||
# node server/server.js
|
||||
# "
|
||||
# restart: unless-stopped
|
||||
|
||||
|
||||
|
||||
appRobot_Tunnel:
|
||||
image: alpine:latest
|
||||
@@ -137,13 +120,14 @@ services:
|
||||
-R 0.0.0.0:9710:appRobot_Control:10010 \
|
||||
-R 0.0.0.0:9798:appRobot_Driver:2098 \
|
||||
-R 0.0.0.0:9712:appRobot_Simulation:1003\
|
||||
-R 0.0.0.0:9743:AppRobotWebcam:8444 \
|
||||
-R 0.0.0.0:9743:appRobot_Webcam:8444 \
|
||||
-R 0.0.0.0:9780:appRobot_guacamole:8080 \
|
||||
-R 0.0.0.0:9793:appRobot_Homing:2093 \
|
||||
-R 0.0.0.0:9725:appRobot_AccessBase:443 \
|
||||
-R 0.0.0.0:9726:appRobot_AccessEllbow:443 \
|
||||
-R 0.0.0.0:9727:appRobot_AccessHand:443 \
|
||||
-R 0.0.0.0:9744:appRobot_CodeServer:8443 \
|
||||
-R 0.0.0.0:7060:overleaf:80 \
|
||||
tunnel@server.schooltech.ch -p 2255
|
||||
"
|
||||
|
||||
@@ -151,44 +135,9 @@ services:
|
||||
image: cloudflare/cloudflared:latest
|
||||
container_name: appServer_cloudflare
|
||||
command: tunnel --no-autoupdate run --token eyJhIjoiOWUyYzk0OTI1ZWVlNmE4NjRiZjllZGRiM2ZmMDRmMTUiLCJ0IjoiZDc2YzI2MjAtZGE0ZC00OTJmLWI5YjgtODNjMjgwNjQ5MTFlIiwicyI6IllUbGpPREJtTURndFpHSTVZUzAwWkRnekxXRTRNek10TXpaaE56WTBabUpsT1RBMSJ9
|
||||
# networks:
|
||||
# - default
|
||||
# - appRobotNet
|
||||
restart: unless-stopped
|
||||
|
||||
|
||||
|
||||
|
||||
appRobot_nextcloud_db:
|
||||
image: mariadb:latest
|
||||
container_name: appRobot_nextcloud_db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: KantiWattwilABC
|
||||
MYSQL_DATABASE: nextcloud
|
||||
MYSQL_USER: nextcloud
|
||||
MYSQL_PASSWORD: KantiABC122nextcloudX3!
|
||||
volumes:
|
||||
- /home/chk/Documents/appRobotNextCloud/db:/var/lib/mysql
|
||||
|
||||
appRobot_nextcloud:
|
||||
image: nextcloud:latest
|
||||
container_name: appRobot_nextcloud
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_PASSWORD: KantiABC122nextcloudX3!
|
||||
MYSQL_DATABASE: nextcloud
|
||||
MYSQL_USER: nextcloud
|
||||
MYSQL_HOST: appRobot_nextcloud_db
|
||||
volumes:
|
||||
- /home/chk/Documents/appRobotNextCloud/nextcloud:/var/www/html
|
||||
- /home/chk/Documents:/mnt/server:rw
|
||||
depends_on:
|
||||
- appRobot_nextcloud_db
|
||||
ports:
|
||||
- "9183:80"
|
||||
|
||||
|
||||
appRobot_AccessBase:
|
||||
image: node:20-bullseye
|
||||
# Alternativ: node:20-alpine (kleiner, aber evtl. openssl/ca/certs nachziehen)
|
||||
@@ -228,7 +177,6 @@ services:
|
||||
|
||||
appRobot_AccessHand:
|
||||
image: node:20-bullseye
|
||||
# Alternativ: node:20-alpine (kleiner, aber evtl. openssl/ca/certs nachziehen)
|
||||
container_name: appRobot_AccessHand
|
||||
working_dir: /app
|
||||
volumes:
|
||||
@@ -245,24 +193,7 @@ services:
|
||||
restart: unless-stopped
|
||||
|
||||
|
||||
appRobot_CodeServer:
|
||||
image: lscr.io/linuxserver/code-server
|
||||
container_name: appRobot_CodeServer
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
PUID: "1000"
|
||||
PGID: "1000"
|
||||
TZ: "Europe/Rome"
|
||||
AUTH: "none"
|
||||
PASSWORD: "Albula60"
|
||||
ports:
|
||||
- "9743:8443"
|
||||
volumes:
|
||||
- /home/chk/Documents/appRobotDriver:/workspace/appRobotDriver
|
||||
- /home/chk/Documents/appRobotHoming:/workspace/appRobotHoming
|
||||
- /home/chk/Documents/appRobotControl:/workspace/appRobotControl
|
||||
- /home/chk/Documents/AppRobotVideo/public/snapshots:/workspace/appRobotHoming/public/snapshots
|
||||
working_dir: /workspace
|
||||
|
||||
|
||||
yolo:
|
||||
image: ultralytics/ultralytics:latest
|
||||
@@ -276,7 +207,6 @@ services:
|
||||
|
||||
|
||||
|
||||
|
||||
webcam:
|
||||
build:
|
||||
context: /tmp
|
||||
@@ -285,28 +215,29 @@ services:
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /usr/src/app
|
||||
EXPOSE 8444
|
||||
EXPOSE 8424
|
||||
image: approbotwebcam:latest
|
||||
container_name: AppRobotWebcam
|
||||
container_name: appRobot_Webcam
|
||||
restart: unless-stopped
|
||||
# 8444 am Host veröffentlicht → direkter LAN-Zugriff (http://<host>:8444) bleibt.
|
||||
# Wenn ALLES über den Proxy läuft, diesen ports-Block entfernen → proxy-only.
|
||||
ports:
|
||||
- "8444:8444"
|
||||
command: sh -c "npm install --omit=dev && node server.js"
|
||||
command: >
|
||||
sh -c "(apt-get update && apt-get install -y --no-install-recommends i965-va-driver libva-drm2 vainfo) || echo 'WARN: VA-Treiber-Install fehlgeschlagen – H.264 evtl. nicht verfuegbar';
|
||||
npm install --omit=dev && node server.js"
|
||||
volumes:
|
||||
- ${APP_PATH:-.}:/usr/src/app
|
||||
- /home/chk/Documents/appRobotWebcam:/usr/src/app
|
||||
|
||||
devices:
|
||||
# by-id (Host) → /dev/videoN (Container) – stabil über Reboots und USB-Re-Plugs.
|
||||
# Rechte Seite = Pfad den cameras.json + FFmpeg im Container sehen.
|
||||
- /dev/v4l/by-id/usb-046d_0825_3BB3FE20-video-index0:/dev/video0 # cam0 – C270 (046d:0825)
|
||||
- /dev/v4l/by-id/usb-046d_081b_342D4F40-video-index0:/dev/video2 # cam1 – C270 (046d:081b)
|
||||
- /dev/v4l/by-id/usb-046d_HD_Pro_Webcam_C920_9C5591DF-video-index0:/dev/video4 # cam2 – C920
|
||||
- /dev/dri:/dev/dri
|
||||
group_add:
|
||||
- video
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=8444
|
||||
- LIBVA_DRIVER_NAME=i965
|
||||
|
||||
networks:
|
||||
default:
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# ToDo 4 — G-Code und Datei-Handling
|
||||
|
||||
> **✅ Erledigt / abgelöst:** Das Datei-Handling wird **nicht** mehr Driver-intern
|
||||
> (`GCodeFileManager`) gebaut, sondern in das eigenständige Projekt
|
||||
> **`appRobotFileservice`** ausgelagert und über FCodes durch den Driver
|
||||
> weitergereicht. Im Driver bleibt nur ein dünner Proxy. Konzept & Schnittstelle:
|
||||
> [`draft_filehandeling.md`](draft_filehandeling.md) ·
|
||||
> [`draft_filehandeling_API.md`](draft_filehandeling_API.md).
|
||||
> Die folgenden Punkte sind als Vorlage für die Umsetzung *dort* zu lesen.
|
||||
|
||||
## Ziel der Verbesserung
|
||||
|
||||
G-Code-Logik sauber von Datei-Management trennen. Die Bewegungssteuerung soll nicht durch Dateibefehle oder File-IO verwässert werden.
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# ToDo 6b — File-Handling
|
||||
|
||||
> **✅ Erledigt / abgelöst:** Das File-Handling wird in das eigenständige Projekt
|
||||
> **`appRobotFileservice`** ausgelagert (Driver als Gateway, FCodes als Pass-through).
|
||||
> Die hier beschriebenen Detailprobleme werden **dort** gelöst: Cursor als In-Memory-
|
||||
> Index (Paket 2), explizite Grad↔Radian-Umrechnung im Fileservice (Paket 3),
|
||||
> Fehler-Envelope (Paket 4), asynchrones IO (Paket 5). Konzept & Schnittstelle:
|
||||
> [`draft_filehandeling.md`](draft_filehandeling.md) ·
|
||||
> [`draft_filehandeling_API.md`](draft_filehandeling_API.md).
|
||||
> Die folgende Analyse bleibt als Umsetzungs-Vorlage für *jenes* Projekt erhalten.
|
||||
|
||||
## Ist-Zustand
|
||||
|
||||
`GCode.receiveFC()` implementiert nur einen Bruchteil der erkannten Befehle:
|
||||
|
||||
@@ -79,6 +79,8 @@ erstmals einen Promise zurückgeben/awaiten können.
|
||||
| `ROBOT_SPEED_MODE` | `legacy` / `correct` | `legacy` | koordinierte Feedrate (ToDo_6a) |
|
||||
| `ROBOT_USE_QUEUE` | `false` / `true` | `false` | zeitgesteuerte Sende-Queue (Paket 6) |
|
||||
| `ROBOT_MOTION_SYNC` | `freerun` / `lockstep` | `freerun` | Schritt-für-Schritt-Synchronisation der 3 Controller |
|
||||
| `ROBOT_GRBL_AUTOREPORT` | `false` / `true` | `false` | **umgesetzt (Paket 3).** Schreibt beim Connect `$10=3` + `$Report/Interval` in die persistenten FluidNC-Settings. Default AUS → ohne Flag wird NICHTS an die Hardware geschrieben; geparst werden nur die Antworten des ohnehin laufenden `?`-Heartbeats. |
|
||||
| `ROBOT_GRBL_REPORT_INTERVAL` | ms (Zahl) | `200` | **umgesetzt (Paket 3).** Intervall für `$Report/Interval`, nur wirksam bei `ROBOT_GRBL_AUTOREPORT=true`. |
|
||||
|
||||
> **Konsistenz-Regel:** `ROBOT_MOTION_SYNC=lockstep` ergibt nur mit `ROBOT_SPEED_MODE=correct`
|
||||
> Sinn (sonst kommen die Controller zu unterschiedlichen Zeiten an und Lockstep müsste hart
|
||||
@@ -86,14 +88,23 @@ erstmals einen Promise zurückgeben/awaiten können.
|
||||
|
||||
---
|
||||
|
||||
## Paket 1: GRBL-Antworten lesen
|
||||
## Paket 1: GRBL-Antworten lesen — ✅ ERLEDIGT
|
||||
|
||||
- [ ] `connection.on('data', data => {})` in `TelnetSenderGRBL` ersetzen durch echtes Lesen
|
||||
- GRBL antwortet auf jeden G-Code-Befehl mit `ok` oder `error: <Meldung>`
|
||||
- Antworten parsen und ins Log schreiben
|
||||
- [ ] Fehlerantworten nach außen meldbar machen
|
||||
- an `InfoServer` oder über einen EventEmitter
|
||||
- damit der WebSocket-Client Feedback bekommt, ob ein Befehl angenommen wurde
|
||||
> Umgesetzt in `robot/TelnetSenderGRBL.js`. Der vorher blinde Kanal wird gelesen:
|
||||
> `tSocket.on('data', …)` → `_handleIncomingData()` (zeilen-gepuffert, fragmentierungs-
|
||||
> sicher) → `_handleResponseLine()` (demultiplext nach Typ). Tests:
|
||||
> `test/Sender.Telnet.responseParsing.test.js`.
|
||||
|
||||
- [x] `connection.on('data', …)` durch echtes Lesen ersetzt
|
||||
- `ok` → `lastOk`-Zeitstempel; `error:`/`ALARM:` → `lastError` + Log
|
||||
- `<…>`-Reports → `_parseStatusReport()` (siehe Paket 3)
|
||||
- **wirft nie** (try/catch um den data-Handler) — ein Parsefehler darf den Prozess nicht abreißen
|
||||
- [x] Fehlerantworten nach außen meldbar gemacht
|
||||
- über `getStatus()` (`lastError`) → `InfoServer` `/api/status`; kein EventEmitter-Umbau nötig
|
||||
- der raw-`socket.on('data')`-Handler für `_lastDataAt` (Heartbeat-Liveness) bleibt unverändert
|
||||
|
||||
> **Bewusst nicht hier:** ein `ok`-Handshake / Sende-Queue (das ist Paket 2 / Paket 6).
|
||||
> Paket 1 *liest und meldet* nur — der Sende-Pfad bleibt unangetastet (fire-and-forget).
|
||||
|
||||
## Paket 2: Command-Queue mit ok-Handshake
|
||||
|
||||
@@ -108,20 +119,29 @@ erstmals einen Promise zurückgeben/awaiten können.
|
||||
- [ ] Timeout für ausbleibende `ok`-Antworten definieren
|
||||
- nach X ms ohne Antwort: Fehler loggen, ggf. Verbindung zurücksetzen
|
||||
|
||||
## Paket 3: Hardwareposition lesen (Auto-Report statt Polling)
|
||||
## Paket 3: Hardwareposition lesen (Auto-Report statt Polling) — ✅ weitgehend erledigt
|
||||
|
||||
- [ ] Beim Verbindungsaufbau je Controller konfigurieren:
|
||||
- `$10=3` setzen → Report enthält `MPos` **und** `Bf` (Protokoll-Fakt 2)
|
||||
- `$Report/Interval=N` setzen (z. B. `N=100…200`) → FluidNC **pusht** den Status während
|
||||
der Bewegung selbst (Protokoll-Fakt 4). Kein `?`-Polling-Loop nötig; `?` bleibt nur als
|
||||
Einzelabfrage on demand (z. B. für Sync, Paket 4).
|
||||
- [ ] `data`-Handler (Paket 1) parst die gepushten `<…>`-Reports: `state`, `MPos`, `Bf`
|
||||
- robust gegen Cross-Channel-Fremdzeilen (Protokoll-Fakt 5) — nach Typ demultiplexen
|
||||
- [ ] Gemeldete Hardware-Position (`MPos`) mit Softwareposition vergleichen
|
||||
- bei Abweichung: warnen oder synchronisieren (→ Paket 4)
|
||||
- schützt gegen Drift durch Endschalter-Auslösung, Motor-Stall, Verbindungsunterbrechung
|
||||
- [ ] Status (`Idle`, `Run`, `Alarm`, `Hold`) + `Bf` für den `InfoServer` bereitstellen
|
||||
- `/api/status` um GRBL-Zustand erweitern
|
||||
> Report-Parsing + InfoServer-Anbindung umgesetzt. Die Settings-Writes (`$10`,
|
||||
> `$Report/Interval`) sind **opt-in** (Env `ROBOT_GRBL_AUTOREPORT`, Default AUS), weil sie
|
||||
> persistente FluidNC-NVS-Settings schreiben und auf der eingesetzten FluidNC-Version
|
||||
> verifiziert werden müssen. Tests: `test/Sender.Telnet.responseParsing.test.js`,
|
||||
> `test/InfoServer.test.js`.
|
||||
|
||||
- [x] Beim Verbindungsaufbau je Controller konfigurieren — **opt-in** (`_configureAutoReport()`):
|
||||
- `$10=3` → Report enthält `MPos` **und** `Bf` (Protokoll-Fakt 2)
|
||||
- `$Report/Interval=N` (Default `N=200`) → FluidNC **pusht** den Status selbst (Protokoll-Fakt 4)
|
||||
- **nur** wenn `ROBOT_GRBL_AUTOREPORT=true`; ohne Flag bleibt der `?`-Heartbeat (alle 10 s)
|
||||
die einzige Statusquelle — ausreichend für Anzeige, kein Schreibzugriff auf die Hardware
|
||||
- [x] `data`-Handler (Paket 1) parst die `<…>`-Reports: `state`, `MPos`/`WPos`, `Bf`
|
||||
- robust gegen Cross-Channel-Fremdzeilen (Protokoll-Fakt 5) — nach Typ demultiplext, fremde
|
||||
Zeilen werden ignoriert; zerstörte Felder lassen den alten Wert stehen
|
||||
- [ ] **Offen (→ Paket 4):** Gemeldete `MPos` mit Softwareposition vergleichen / bei Abweichung
|
||||
warnen. Bewusst aufgeschoben: der Vergleich braucht die `motorStateFromPorts()`-Rückrechnung
|
||||
(ToDo_9a) und eine Roboter-Referenz im Sender — beides gehört zum Sync-Command (Paket 4).
|
||||
Der Sender liest und meldet die Hardwareposition bereits; der *Abgleich* fehlt noch.
|
||||
- [x] Status (`Idle`/`Run`/`Alarm`/`Hold`) + `MPos` + `Bf` für den `InfoServer` bereitgestellt
|
||||
- `getStatus()` um `grblState`, `machinePosition`, `plannerBlocksFree`, `rxBytesFree`,
|
||||
`lastError`, `lastReportAt` erweitert; `/api/status` reicht sie durch
|
||||
|
||||
---
|
||||
|
||||
@@ -155,42 +175,56 @@ eMotor = r.hand.y / D
|
||||
> diese Richtung nicht — er geht `MPos → Motorwerte → Vorwärtskinematik → Pose`, beide
|
||||
> Schritte eindeutig. Der gesamte Sync-Pfad ist damit eindeutig.
|
||||
|
||||
Offen für die spätere **Umsetzung** (Paket 4, nicht mehr Analyse):
|
||||
Umsetzung (Paket 4) — ✅ ERLEDIGT:
|
||||
|
||||
- [ ] `motorStateFromPorts()` aus der Analyse in den Produktiv-Code heben (Ort: Sender oder
|
||||
Kinematik-Helfer) und im Sync verdrahten
|
||||
- [ ] **Round-Trip-Invariante** als Dauer-Test mitführen: `portValue(motorStateFromPorts(p)) ≈ p`
|
||||
— schützt gegen Drift, falls sich die Verkabelung in `startRobot.js` ändert
|
||||
- [x] `motorStateFromPorts()` in den Produktiv-Code gehoben: **`robot/portInverse.js`**.
|
||||
`test/Robot.PortInverse.test.js` importiert jetzt diese Funktion (statt inline) — die
|
||||
15 Verifikations-Tests sind damit der Dauer-Guard für den Produktivcode.
|
||||
- [x] Im Sync verdrahtet: `RobotController.syncFromHardware()` ruft `motorStateFromPorts()`.
|
||||
Die Round-Trip-Invariante ist über `test/Robot.PortInverse.test.js` (A/B/C) abgedeckt.
|
||||
|
||||
> Hinweis: Gelesen wird auf dem **aktiven** Sender `TelnetSenderGRBL` (im `data`-Handler,
|
||||
> siehe Paket 1) — nicht auf `FluidNCClient.js`.
|
||||
|
||||
---
|
||||
|
||||
## Paket 4: Hardware-Position auslesen und übernehmen (Sync-Command)
|
||||
## Paket 4: Hardware-Position auslesen und übernehmen (Sync-Command) — ✅ ERLEDIGT
|
||||
|
||||
**Ziel:** Ein Befehl liest die echten Motor-Koordinaten aller drei Controller aus,
|
||||
übernimmt sie als neuen Soll-Zustand und passt die berechnete Roboter-Pose entsprechend an.
|
||||
Nötig nach Homing, manuellem Jog, Endschalter-Auslösung oder Reconnect — die Software weiß
|
||||
sonst nicht, wo der Roboter physisch wirklich steht.
|
||||
|
||||
- [ ] **G-Code-Befehl** (B6), z. B. `M114 R` (Read-Hardware) — durch `GCodeParser` +
|
||||
`RobotController` geroutet, **nicht** als Sonderfall in `InputWS` wie heute `M114`
|
||||
- klar abgegrenzt vom bestehenden `M114`, das nur die **Software**-Position zurückgibt
|
||||
(`GCode.getM114(robot)` in `server/InputWS.js`)
|
||||
- [ ] **Async-Dispatch (B6-Folge):** `RobotController.applyCommand` muss für diesen Befehl
|
||||
einen Promise zurückgeben und auf die `?`-Antworten warten — der erste asynchrone Befehl
|
||||
im bisher synchronen Dispatch-Pfad.
|
||||
- [ ] Ablauf des Sync:
|
||||
1. an alle drei Sender einmalig `?` senden, je `MPos` aus der Antwort parsen (Paket 3)
|
||||
2. `motorStateFromPorts(...)` → sieben Motorwerte rekonstruieren (Baustein oben — linear/eindeutig, ToDo_9a)
|
||||
3. diese auf den Roboter schreiben: `robot.xMotor/alpha/beta/a/b/c/eMotor = …`
|
||||
4. **Vorwärtskinematik** anstoßen: `robot.calculatePositionFromMotorAngles()`
|
||||
→ füllt `robot.x/y/z` und `phi/theta/psi` aus den Hardwarewerten
|
||||
5. `motorPosition`/`motorPositionOld` zurücksetzen, damit der nächste Move sauber von
|
||||
der echten Position aus rechnet (sonst falscher Speed-Delta im Korrekt-Modus)
|
||||
- [ ] dem anfragenden Client die übernommene Pose zurückmelden (`reply(ws, …)`)
|
||||
- [ ] **kein** automatisches Nachfahren — Sync ändert nur den Soll-Zustand, sendet keinen Move
|
||||
> Umgesetzt. Befehl: **`M114 R`**. Pfad: `GCodeParser` → `RobotController.applyCommand`
|
||||
> (M114-R-Branch) → `RobotController.syncFromHardware()` (async). Tests:
|
||||
> `test/RobotController.sync.test.js`, `test/Sender.Telnet.responseParsing.test.js`
|
||||
> (requestStatusReport). **Datenquelle: aktiv `?` + await** (gewählt; funktioniert auch
|
||||
> ohne Auto-Report).
|
||||
|
||||
- [x] **G-Code-Befehl** `M114 R` — durch `GCodeParser` + `RobotController` geroutet
|
||||
- `GCodeParser` erhält jetzt Flag-Token ohne Wert (`R` → `params.R = true`)
|
||||
- `GCode.containsCommand('M114 …')` erkennt es; das bestehende exakte `M114` (Software-
|
||||
Position) in `InputWS` bleibt unberührt (wird vorher per `=== "M114"` abgefangen)
|
||||
- [x] **Async-Dispatch:** `applyCommand` gibt für `M114 R` ein Promise zurück; `receive`
|
||||
reicht es nur dann hoch (sonst synchron wie bisher). `InputWS` wartet auf das Promise und
|
||||
broadcastet erst danach die neue Pose; Fehler gehen als Fehler-Envelope an den Anfrager.
|
||||
**Alle anderen Befehle bleiben byte-identisch synchron.**
|
||||
- [x] Ablauf des Sync (`syncFromHardware`):
|
||||
1. an `base`/`elbow`/`hand` je `requestStatusReport()` → sendet `?`, wartet auf `<…>` (Timeout 1 s)
|
||||
2. `motorStateFromPorts(...)` → sieben Motorwerte (linear/eindeutig, ToDo_9a)
|
||||
3. auf den Roboter schreiben (`robot.xMotor/alpha/beta/a/b/c/eMotor`)
|
||||
4. `robot.calculatePositionFromMotorAngles()` → Pose `x/y/z` + `phi/theta/psi`
|
||||
5. `motorPosition`/`motorPositionOld` auf `null` → nächster Move rechnet von der echten Position
|
||||
- [x] dem Client die übernommene Pose zurückmelden — als Broadcast von `getM114` (konsistent
|
||||
mit Bewegungen; erreicht auch die Simulation, nicht nur den Anfrager)
|
||||
- [x] **kein** automatisches Nachfahren — Sync ruft **kein** `sendCommand()`; es geht garantiert
|
||||
nichts an die Controller (per Test abgesichert: `execCommand` wird nie aufgerufen)
|
||||
- [x] **Guards:** fehlende Controller-Rolle, ungültige/zu kurze `MPos` und `?`-Timeout führen
|
||||
zu einem sauberen Fehler an den Client — **nie** zu Müll im Roboterzustand
|
||||
|
||||
**Sender→Rolle-Zuordnung:** `startRobot.js` setzt `instance.controllerRole = key`
|
||||
(`base`/`elbow`/`hand`); `syncFromHardware` mappt darüber. Ändert sich die Verkabelung, muss
|
||||
`portInverse.js` mitgezogen werden — der Round-Trip-Test schützt davor.
|
||||
|
||||
> Warum nicht einfach „letzten gesendeten Wert" merken? Weil die Hardware nach Homing/Jog/
|
||||
> Stall von dem abweicht, was zuletzt gesendet wurde — genau diese Differenz soll Sync auflösen.
|
||||
|
||||
292
doc/draft_filehandeling.md
Normal file
292
doc/draft_filehandeling.md
Normal file
@@ -0,0 +1,292 @@
|
||||
# Draft — File-Handling als externes Projekt `appRobotFileservice` (Driver als Gateway)
|
||||
|
||||
> **Status:** Entwurf / Diskussionsgrundlage.
|
||||
> **Projekte:** Der **Driver** lebt in `appRobotDriver` (dieses Repo). Das gesamte
|
||||
> G-Code-**Programm**-Handling wird in das eigenständige Projekt
|
||||
> **`appRobotFileservice`** ausgelagert. Schnittstelle:
|
||||
> [`draft_filehandeling_API.md`](draft_filehandeling_API.md).
|
||||
> **Verhältnis zu ToDos:** ersetzt den Driver-internen `GCodeFileManager`-Ansatz aus
|
||||
> `doc/ToDo_4_GCode.md` und `doc/ToDo_6b_FileHandling.md`.
|
||||
> **Übergang darf hart sein** — keine Rückwärtskompatibilität nötig.
|
||||
|
||||
---
|
||||
|
||||
## 1. Motivation
|
||||
|
||||
Heute lebt das Datei-Handling in [`robot/GCode.js`](../robot/GCode.js)
|
||||
(`receiveFC`, `ContainsFilesCommand`, `removeStringFromFile`, `toPiMultiple`, der
|
||||
`;!`-Cursor) und wird in [`server/InputWS.js`](../server/InputWS.js) gleichberechtigt
|
||||
neben den Bewegungs-Befehlen geroutet. Das vermischt zwei Verantwortungen:
|
||||
|
||||
| | **Bewegung / Hardware** | **Programm-Verwaltung** |
|
||||
|---|---|---|
|
||||
| Aufgabe | eine G-Code-Zeile → Achsen bewegen | Programme speichern, anzeigen, durchblättern |
|
||||
| Zustand | Live-Pose des Roboters | Datei-Inhalte, Cursor, Listen |
|
||||
| Echtzeit | ja (Telnet/FluidNC) | nein (Storage-/UI-getrieben) |
|
||||
| Gehört zu | **`appRobotDriver`** | **`appRobotFileservice`** |
|
||||
|
||||
---
|
||||
|
||||
## 2. Leitprinzip — der Driver ist das einzige Front Door
|
||||
|
||||
**Vorgabe:** Alle Steuerungen (Joystick, Tastatur, Bilderkennung,
|
||||
sensor-gesteuerte Programme …) kennen **nur den Driver**. Sie sprechen die
|
||||
appRobotFileservice **niemals direkt** an — nur indirekt, *durch den Driver hindurch*.
|
||||
|
||||
```
|
||||
Steuerungen → Driver → appRobotFileservice
|
||||
(nur EINE Verbindung pro Steuerung: zum Driver)
|
||||
```
|
||||
|
||||
Daraus folgt eine **einseitige Abhängigkeit**:
|
||||
|
||||
```
|
||||
Steuerung ──kennt──► Driver ──kennt──► appRobotFileservice
|
||||
(Gateway) (passiver Storage-Dienst)
|
||||
|
||||
• Der Driver hängt von der appRobotFileservice ab (ruft sie).
|
||||
• Die appRobotFileservice hängt von NICHTS ab — sie ruft den Driver nie an,
|
||||
kennt weder dessen URL noch dessen Pose.
|
||||
• Steuerungen brauchen KEINEN neuen Weg: sie reden weiter nur mit dem Driver.
|
||||
```
|
||||
|
||||
> **Abgrenzung:** Gemeint sind **Steuerungen** (Echtzeit-Eingaben). Die
|
||||
> **Visualisierungs-/Verwaltungs-UI** der appRobotFileservice ist Teil *jenes*
|
||||
> Projekts und darf den Fileservice direkt ansprechen — sie ist keine Steuerung.
|
||||
|
||||
---
|
||||
|
||||
## 3. Befehls-Routing im Driver (der „Pass-through")
|
||||
|
||||
Der Driver klassifiziert jede eingehende Nachricht und routet sie:
|
||||
|
||||
```
|
||||
eingehende Nachricht am Driver (WS :2095 oder POST /api/gcode)
|
||||
│
|
||||
├─ Bewegung (G…, M1, M92, G92) → lokal ausführen → Pose broadcast
|
||||
├─ Status (Ping, M114) → gezielt antworten
|
||||
├─ FCode (FShow, FList, FPoint …) → an appRobotFileservice weiterreichen
|
||||
└─ sonst → Fehler-Envelope
|
||||
```
|
||||
|
||||
### FCodes — eine Befehlsfamilie wie die G-/M-Codes
|
||||
|
||||
G-Code kennt `G1`, `G2`, `Gx` und `M1`, `M92`, … . Analog bilden die **FCodes** eine
|
||||
eigene Familie für Datei-/Programm-Befehle — **ohne Sonderzeichen**, einfach `F` +
|
||||
Wort:
|
||||
|
||||
| FCode (Steuerung → Driver) | Bedeutung | Driver leitet weiter an |
|
||||
|---|---|---|
|
||||
| `FList` | Programme auflisten | `GET /programs` |
|
||||
| `FShow [id]` | Inhalt anzeigen | `GET /programs/{id}` |
|
||||
| `FLoad <id>` | Programm aktiv setzen | `PUT /active` |
|
||||
| `FSave <name>` | aktiven Puffer speichern | `POST /programs` |
|
||||
| `FClear` | aktives Programm leeren | `POST /active/clear` |
|
||||
| `FPoint` | **aktuelle Pose** aufnehmen | `POST /active/points` (Driver hängt Pose an) |
|
||||
| `FPlus` / `FMinus` | nächste / vorige Zeile | `POST /active/next` / `/prev` |
|
||||
| `FFirst` / `FLast` | an Anfang / Ende | `POST /active/first` / `/last` |
|
||||
| `FGoto <n>` | zu Zeile springen | `POST /active/goto` |
|
||||
| `FPlay` / `FStop` | durchlaufen / anhalten | `POST /active/play` / `/stop` |
|
||||
|
||||
**Warum kein Sonderzeichen-Prefix nötig ist:** Eine Bewegungszeile beginnt mit `G`
|
||||
oder `M`; ein FCode mit `F`+Buchstabe. Das Feedrate-Wort `F1000` ist `F`+Ziffer und
|
||||
steht **nur innerhalb** einer `G`-Zeile, nie am Anfang. Der Router muss also nur
|
||||
**am Nachrichtenanfang** prüfen: `F` + Buchstabe → FCode. Damit ist die Familie
|
||||
kollisionsfrei — gegen die Lesbarkeit spricht nichts.
|
||||
|
||||
`FFirst`/`FLast` werden dabei endlich umgesetzt (heute erkannt, aber nicht
|
||||
implementiert — vgl. ToDo_6b / Bug 2). Konkrete API:
|
||||
[`draft_filehandeling_API.md`](draft_filehandeling_API.md).
|
||||
|
||||
---
|
||||
|
||||
## 4. Zwei Datei-Welten — nur eine wandert aus
|
||||
|
||||
| Welt | Beispiele | Verbleib |
|
||||
|---|---|---|
|
||||
| **Betriebs-Logs** | `logs/gcode_commands.log`, `logs/pings.log` | **bleibt im Driver** |
|
||||
| **G-Code-Programme** | `GCodeFiles/*.gcode` | **wird ausgelagert** (`appRobotFileservice`) |
|
||||
|
||||
Die Logs betreffen den Hardware-/Verbindungsbetrieb und bleiben. Ausgelagert wird
|
||||
ausschließlich `GCodeFiles/` samt Cursor und FCodes.
|
||||
|
||||
---
|
||||
|
||||
## 5. Was bleibt im Driver, was wird ausgelagert
|
||||
|
||||
| Heute (in [`robot/GCode.js`](../robot/GCode.js)) | Ziel | Anmerkung |
|
||||
|---|---|---|
|
||||
| `receiveGCode` / `containsCommand` / `receiveMCode` | **bleibt** | reine Bewegung |
|
||||
| `getM114` / `GET /api/position` | **bleibt** | Pose-Quelle für `FPoint` |
|
||||
| `logCommand` / `logPing` | **bleibt** | Betriebs-Logging |
|
||||
| Routing der FCodes | **bleibt als dünner Proxy** | neuer Gateway-Zweig in `InputWS` |
|
||||
| `receiveFC` (Programm-Logik) | **appRobotFileservice** | Verwaltung |
|
||||
| `static fileName`, `;!`-Cursor | **appRobotFileservice** (Cursor: In-Memory-Index, persistiert als `!`-Kommentar) | löst ToDo_6b Paket 2 |
|
||||
| `removeStringFromFile` | **entfällt** | nur für den `;!`-Hack nötig |
|
||||
| `toPiMultiple` (Grad→Radian) | **entfällt im Driver** → Umrechnung lebt im Fileservice | siehe §7 |
|
||||
| Zeilen-String-Bau in `FPoint` | **appRobotFileservice** | Zeilenformat ist Programm-Logik |
|
||||
|
||||
Im Driver bleibt also: Bewegung, Pose, Logs — **plus ein dünner Proxy-Zweig**, der
|
||||
FCodes weiterreicht. Kein `GCodeFiles/`-IO, kein Cursor, **keine** Einheiten-Umrechnung.
|
||||
|
||||
---
|
||||
|
||||
## 6. Die zwei Kernabläufe
|
||||
|
||||
### 6a. Playback (Datei → Roboter)
|
||||
|
||||
```
|
||||
Steuerung → Driver: FPlus
|
||||
Driver → Fileservice: POST /active/next (Cursor++)
|
||||
Fileservice → Driver: { line: "G90 G1 x310 y444 … a1.5708 …" } (driver-nativ, Radian)
|
||||
Driver: receiveGCode(line) → Achsen bewegen
|
||||
Driver: Pose-Broadcast an alle WS-Clients
|
||||
```
|
||||
|
||||
Die appRobotFileservice liefert eine **fertig ausführbare, driver-native Zeile**; der
|
||||
Driver führt sie über seinen normalen `receiveGCode`-Pfad aus — *keine*
|
||||
Sonderbehandlung, *keine* Umrechnung.
|
||||
|
||||
### 6b. Teaching / Training (Roboter → Datei) — der robotik-spezifische Fall
|
||||
|
||||
Der Arm wird **per Joystick** bewegt; G-Code ist hier **Ausgabe**. Entscheidend:
|
||||
Beim `FPoint` hat der **Driver die Live-Pose bereits lokal**.
|
||||
|
||||
```
|
||||
Steuerung (Joystick) → Driver: G1 …/$J= (Arm bewegen, lokal)
|
||||
Steuerung → Driver: FPoint
|
||||
Driver: hängt die AKTUELLE Pose an (robot.x … robot.e, feedrate)
|
||||
Driver → Fileservice: POST /active/points { pose:{ x,y,z, a,b,c, e }, feedrate }
|
||||
Fileservice: Pose → Grad → als G-Code-Zeile persistieren, Cursor ans Ende
|
||||
Fileservice → Driver: { index, line }
|
||||
Driver → Steuerung: Bestätigung
|
||||
```
|
||||
|
||||
Der Driver ist die Quelle der Wahrheit für die Pose und reicht sie beim Forwarden
|
||||
mit. Die appRobotFileservice muss den Driver dafür **nicht** anrufen.
|
||||
|
||||
---
|
||||
|
||||
## 7. Einheiten: Driver bleibt Radian, der Fileservice rechnet um
|
||||
|
||||
Die Datei soll **wie Standard-G-Code aussehen** (Grad, `a-90.00`). Der Driver
|
||||
arbeitet intern und am G-Code-Eingang in **Radian** (Beleg: `receiveGCode` setzt
|
||||
`robot.phi = A` ohne Umrechnung). Beides ist vereinbar, ohne dass der Driver etwas
|
||||
umrechnen muss:
|
||||
|
||||
| Achse | `.gcode`-Datei (Storage) | Wire Driver ↔ Fileservice | Driver intern |
|
||||
|---|---|---|---|
|
||||
| `x y z` | mm | mm | mm |
|
||||
| `a b c` (φ/θ/ψ) | **Grad** (`a-90.00`) | **Radian** | Radian |
|
||||
| `e` (Greifer) | **Grad** | **Radian** | Radian |
|
||||
| Umrechnung | — | **in der appRobotFileservice** | **keine** |
|
||||
|
||||
- **Driver:** rechnet nie um — `toPiMultiple` **entfällt** ersatzlos (harter Übergang).
|
||||
- **appRobotFileservice:** konvertiert an ihrer **Storage-Grenze**: beim Lesen für
|
||||
Playback Grad→Radian, beim `FPoint`-Schreiben Radian→Grad. Damit liegt die
|
||||
Umrechnung an genau **einer** Stelle und ist testbar (löst ToDo_6b Paket 3).
|
||||
|
||||
So bleibt die Datei standardnah und lesbar, der Hot-Path im Driver aber sauber.
|
||||
|
||||
---
|
||||
|
||||
## 8. Storage-Modell der appRobotFileservice: GCode-Datei + JSON-Sidecar
|
||||
|
||||
Ziel: am Ende stehen **Dateien, die wie G-Code aussehen** (möglichst nah an einem
|
||||
Standard). Pro Programm:
|
||||
|
||||
```
|
||||
GCodeFiles/
|
||||
besteck_spuelmaschine.gcode ← das Programm, sieht aus wie Standard-G-Code (Grad)
|
||||
besteck_spuelmaschine.json ← Sidecar: Metadaten + Verwaltung
|
||||
```
|
||||
|
||||
- **`.gcode`** (alternativ `.ngc`): standardnahe Bewegungszeilen, Winkel in **Grad**.
|
||||
Zeitstempel **und** Cursor stehen im **G-Code-Kommentarfeld** (`;…`) — so bleibt die
|
||||
Zeile standardkonform (Kommentare sind Teil des G-Code-Standards):
|
||||
- jede Zeile endet mit `;<epoch>` (Aufnahme-Zeitstempel),
|
||||
- die **Cursor-Zeile** trägt zusätzlich ein `!`: `;<epoch>!`.
|
||||
|
||||
```
|
||||
G90 G1 x0 y300 z0 a90.00 b-90.00 c0.00 e0.00 f1000 ;1759566014
|
||||
G90 G1 x310 y444 z0.5 a90.00 b-90.00 c0.00 e6.88 f1000 ;1759566112!
|
||||
G90 G1 x310 y444 z30.5 a90.00 b-90.00 c0.00 e6.88 f1000 ;1759566118
|
||||
```
|
||||
|
||||
Damit ist die `.gcode` **ohne Sidecar vollständig** (Bewegung + Zeitstempel + Cursor).
|
||||
- **`.json`-Sidecar** (Komfort/Verwaltung): Anzeigename, `createdAt`/`updatedAt`,
|
||||
`lineCount`, `angleUnit` (`"deg"`), optional benannte Labels (`"pick"`, `"place"`
|
||||
→ Zeilenindex). Quelle der Wahrheit für Bewegung/Zeitstempel/Cursor bleibt die `.gcode`.
|
||||
|
||||
Nach außen (API) werden Programme über **id/Name** angesprochen, **nie über
|
||||
Dateipfade** — `GCodeFiles/` und das Sidecar-Schema bleiben **intern** in der
|
||||
appRobotFileservice. Damit entfällt die `../`-Pfad-Problematik (ToDo_6b Paket 4) und
|
||||
ein späterer Wechsel des Storage bleibt unsichtbar.
|
||||
|
||||
---
|
||||
|
||||
## 9. Gemeinsamer Zustand: aktives Programm + Cursor (im Fileservice)
|
||||
|
||||
Die appRobotFileservice hält genau einen **„aktives Programm + Cursor"**-Zustand als
|
||||
*Single Source of Truth*. Weil alle Steuerungen durch denselben Driver auf denselben
|
||||
Fileservice gehen, teilen sie automatisch denselben Cursor — `FPlus` vom Joystick und
|
||||
gleich darauf `FPlus` von der Bilderkennung sehen denselben Stand.
|
||||
|
||||
- `aktivesProgramm` — id/Name (ersetzt `static fileName`).
|
||||
- `cursor` — während einer Session **Zeilenindex im Speicher** (schnelles Stepping
|
||||
ohne Neu-Schreiben). Beim Laden aus dem `!`-Kommentar gelesen, beim Speichern/
|
||||
Entladen als `!` in die Cursor-Zeile zurückgeschrieben — so ist der Cursor
|
||||
persistiert, **ohne** bei jedem `FPlus` die ganze Datei neu zu schreiben (löst
|
||||
ToDo_6b Paket 2).
|
||||
|
||||
---
|
||||
|
||||
## 10. `/api/gcode` & WS — der Steuerungs-Kanal
|
||||
|
||||
`POST /api/gcode` am Driver (optional, REST-Alternative zur WS) und die WS `:2095`
|
||||
sind der **Bewegungs-Eingang für alle Steuerungen**:
|
||||
|
||||
- **Zugriff: alle Steuerungen** (Joystick, Tastatur, Bilderkennung, Sensorik).
|
||||
- **Nicht** die appRobotFileservice — sie pusht nie Bewegung an den Driver; der
|
||||
Driver führt Playback-Zeilen selbst aus (§6a). Der Fileservice braucht **keinen**
|
||||
Driver-Zugang.
|
||||
|
||||
---
|
||||
|
||||
## 11. Durchgereichte Payload-Größen
|
||||
|
||||
Der Driver reicht bei `FShow`/`FList` ggf. größere Mengen durch (Datei-Inhalt,
|
||||
Listen). Das ist akzeptabel: die **appRobotFileservice** hält diese Antworten später
|
||||
klein (z. B. Paginierung, Kurz-/Übersichtsform), sodass der Durchreich-Weg über den
|
||||
Driver unkritisch bleibt.
|
||||
|
||||
---
|
||||
|
||||
## 12. Erforderliche kleine Driver-Ergänzungen
|
||||
|
||||
1. **`InputWS`-Router:** neuer Zweig „FCode am Anfang (`F`+Buchstabe) → an Fileservice
|
||||
forwarden, Antwort zurückreichen". Playback-Zeile lokal ausführen; Verwaltungs-
|
||||
Antworten gezielt an den Anfrager, Pose-ändernde Aktionen broadcasten (analog ToDo_5).
|
||||
2. **`FPoint`-Pose:** Der Driver muss die **Live-Pose inkl. Greifer `e`** (und φ/θ/ψ)
|
||||
mitliefern. Heute setzt `getM114` `e` hart auf `0.0` — sonst geht die
|
||||
Greiferstellung beim Aufnehmen verloren.
|
||||
3. **`POST /api/gcode`** (optional): REST-Bewegungs-Eingang für Steuerungen ohne WS.
|
||||
|
||||
---
|
||||
|
||||
## 13. Offene Fragen
|
||||
|
||||
- **FCode-Namen:** bestehende Familie (`FPlus`/`FMinus` …) beibehalten oder einzelne
|
||||
umbenennen (`FNext`/`FPrev`)? — Empfehlung: bestehende behalten, neue ergänzen.
|
||||
- **Cursor-Persistenz:** als `!`-Kommentar in der `.gcode` (gewählt) — Häufigkeit des
|
||||
Zurückschreibens (sofort vs. debounced beim Entladen) noch offen.
|
||||
- **Sidecar-Umfang:** Metadaten + Labels (Cursor & Zeitstempel liegen in der `.gcode`).
|
||||
|
||||
---
|
||||
|
||||
## 14. Verweise
|
||||
|
||||
- [`draft_filehandeling_API.md`](draft_filehandeling_API.md) — appRobotFileservice-Schnittstelle
|
||||
- [`ToDo_4_GCode.md`](ToDo_4_GCode.md) · [`ToDo_6b_FileHandling.md`](ToDo_6b_FileHandling.md) — abgelöst/gelöst
|
||||
- [`ToDo_5_API.md`](ToDo_5_API.md) / [`API.md`](API.md) — Routing & Fehler-Envelope
|
||||
- [`robot/GCode.js`](../robot/GCode.js) · [`server/InputWS.js`](../server/InputWS.js) · [`server/InfoServer.js`](../server/InfoServer.js)
|
||||
253
doc/draft_filehandeling_API.md
Normal file
253
doc/draft_filehandeling_API.md
Normal file
@@ -0,0 +1,253 @@
|
||||
# Draft — `appRobotFileservice` API
|
||||
|
||||
> **Status:** Entwurf. Schnittstelle des ausgelagerten Programm-Handlings
|
||||
> (`appRobotFileservice`). Konzept & Rollenteilung:
|
||||
> [`draft_filehandeling.md`](draft_filehandeling.md).
|
||||
>
|
||||
> **Einziger Consumer ist der Driver** (`appRobotDriver`). Steuerungen sprechen die
|
||||
> appRobotFileservice nie direkt an, sondern schicken **FCodes** an den Driver, der
|
||||
> sie hierher weiterreicht. Die appRobotFileservice ist **passiv und
|
||||
> driver-agnostisch**: sie ruft den Driver nie an, kennt weder dessen URL noch dessen
|
||||
> Pose. (Eine eigene Visualisierungs-UI darf direkt zugreifen — sie ist keine Steuerung.)
|
||||
|
||||
---
|
||||
|
||||
## 1. Überblick
|
||||
|
||||
- **Transport:** HTTP/REST + JSON. Optional ein WebSocket-Event-Kanal (Abschnitt 8).
|
||||
- **Basis-URL (Vorschlag):** `https://<host>:2100/api`
|
||||
- **Identität:** Programme über **`id`/Name** — **nie über Dateipfade**. Storage
|
||||
(`.gcode` + `.json`-Sidecar) ist intern gekapselt.
|
||||
- **Einheiten am Wire:** **driver-nativ** (φ/θ/ψ und `e` in **Radian**, `x/y/z` in
|
||||
mm) — exakt die G-Code-Strings, die der Driver ausführt. **Gespeichert** wird in
|
||||
**Grad** (standardnahe `.gcode`); die appRobotFileservice rechnet an ihrer
|
||||
Storage-Grenze um (Konzept §7).
|
||||
- **Auth:** `Bearer <FILE_API_KEY>` für schreibende Operationen (analog `ROBOT_API_KEY`).
|
||||
|
||||
---
|
||||
|
||||
## 2. Datenmodell
|
||||
|
||||
### Program (Metadaten, aus dem `.json`-Sidecar)
|
||||
```json
|
||||
{ "id": "besteck_spuelmaschine", "name": "Besteck Spülmaschine",
|
||||
"lineCount": 12, "angleUnit": "deg",
|
||||
"createdAt": "2025-10-04T10:25:00Z", "updatedAt": "2025-10-04T10:41:00Z" }
|
||||
```
|
||||
|
||||
### ActiveState (aktives Programm + Cursor — Single Source of Truth)
|
||||
```json
|
||||
{ "programId": "besteck_spuelmaschine", "cursor": 4, "lineCount": 12,
|
||||
"currentLine": "G90 G1 x310 y444 z0.5 a1.5708 b-1.5708 c0 e0.12 f1000",
|
||||
"playing": false, "version": 7 }
|
||||
```
|
||||
> `currentLine` ist **driver-nativ (Radian)** und kommentarfrei — direkt ausführbar.
|
||||
> Gespeichert wird in **Grad** mit Zeitstempel-Kommentar (`draft_filehandeling.md` §8).
|
||||
|
||||
### Pose (vom Driver beim `FPoint` mitgeschickt)
|
||||
Native Radian-Werte inkl. Greifer `e`:
|
||||
```json
|
||||
{ "pose": { "x": 0, "y": 300, "z": 0, "a": 1.5708, "b": -1.5708, "c": 0, "e": 0.12 },
|
||||
"feedrate": 1000 }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. FCode ↔ Endpoint-Mapping
|
||||
|
||||
Der Driver übersetzt die FCodes der Steuerungen in diese Endpoints:
|
||||
|
||||
| FCode | Endpoint | Antwort an Steuerung (über Driver) |
|
||||
|---|---|---|
|
||||
| `FList` | `GET /programs` | Liste (gezielt) |
|
||||
| `FShow [id]` | `GET /programs/{id}` | Inhalt in **Grad** (gezielt) |
|
||||
| `FLoad <id>` | `PUT /active` | ActiveState (gezielt) |
|
||||
| `FSave <name>` | `POST /programs` | id (gezielt) |
|
||||
| `FClear` | `POST /active/clear` | ActiveState (gezielt) |
|
||||
| `FPoint` | `POST /active/points` | Bestätigung (gezielt) |
|
||||
| `FPlus` | `POST /active/next` | Bewegung → **Pose-Broadcast** |
|
||||
| `FMinus` | `POST /active/prev` | Bewegung → **Pose-Broadcast** |
|
||||
| `FFirst` | `POST /active/first` | Bewegung → **Pose-Broadcast** |
|
||||
| `FLast` | `POST /active/last` | Bewegung → **Pose-Broadcast** |
|
||||
| `FGoto <n>` | `POST /active/goto` | Bewegung → **Pose-Broadcast** |
|
||||
| `FPlay` / `FStop` | `POST /active/play` / `/stop` | Status |
|
||||
|
||||
---
|
||||
|
||||
## 4. Endpoints — Programm-Verwaltung
|
||||
|
||||
### `GET /programs` ← `FList`
|
||||
```json
|
||||
{ "programs": [ { "id": "log", "name": "log", "lineCount": 36 }, … ] }
|
||||
```
|
||||
|
||||
### `GET /programs/{id}` ← `FShow`
|
||||
Inhalt + Metadaten für die Anzeige — in **Grad**, wie gespeichert (lesbar):
|
||||
```json
|
||||
{ "id": "besteck_spuelmaschine", "displayUnit": "deg",
|
||||
"lines": [ "G90 G1 x0 y614 z0 a-90.00 b90.00 c0.00 e0 f1000 ;1759566014",
|
||||
"G90 G1 x0 y300 z0 a90.00 b-90.00 c0.00 e0 f1000 ;1759566052!" ] }
|
||||
```
|
||||
> Kommentar `;<epoch>` = Aufnahme-Zeitstempel; ein abschließendes `!` markiert die Cursor-Zeile.
|
||||
|
||||
### `POST /programs` ← `FSave`
|
||||
```jsonc
|
||||
{ "name": "Demo C", "fromActive": true } // aus aktivem Puffer
|
||||
// oder expliziter Inhalt (in Grad, wie eine .gcode):
|
||||
{ "name": "Demo C", "lines": ["G90 G1 x0 y300 … a90.00 …"], "angleUnit": "deg" }
|
||||
```
|
||||
→ `201 { "id": "demo_c", "lineCount": 12 }` (legt `demo_c.gcode` + `demo_c.json` an)
|
||||
|
||||
### `PUT /programs/{id}` · `DELETE /programs/{id}`
|
||||
Inhalt ersetzen / umbenennen · löschen (jeweils `.gcode` **und** `.json`).
|
||||
|
||||
---
|
||||
|
||||
## 5. Endpoints — Aktives Programm & Cursor
|
||||
|
||||
### `GET /active`
|
||||
Aktuellen `ActiveState` lesen.
|
||||
|
||||
### `PUT /active` ← `FLoad`
|
||||
```json
|
||||
{ "id": "besteck_spuelmaschine" }
|
||||
```
|
||||
→ `ActiveState`. Validierung: existiert, ≥1 gültige Zeile (sonst `EMPTY_PROGRAM`).
|
||||
|
||||
### `POST /active/clear` ← `FClear`
|
||||
Aktives Programm leeren, Cursor → 0.
|
||||
|
||||
### Stepping — `next` · `prev` · `first` · `last` · `goto`
|
||||
Bewegt den Cursor und gibt die **driver-native, ausführbare Zeile (Radian)** zurück.
|
||||
Der **Driver führt sie selbst aus** — der Fileservice pusht nichts.
|
||||
|
||||
`POST /active/next` · `/prev` · `/first` · `/last` · `/goto` `{ "index": 7 }`
|
||||
```json
|
||||
{ "cursor": 5, "line": "G90 G1 x310 y444 z30.5 a1.5708 b-1.5708 c0 e0.12 f1000" }
|
||||
```
|
||||
Grenzen: `next` am Ende / `prev` am Anfang → `CURSOR_OUT_OF_RANGE` (optional `wrap`).
|
||||
|
||||
---
|
||||
|
||||
## 6. Endpoints — Teaching / Aufnahme
|
||||
|
||||
### `POST /active/points` ← `FPoint`
|
||||
Der **Driver schickt die aktuelle Pose mit** (native Radian-Werte). Die
|
||||
appRobotFileservice rechnet **nach Grad** um, formatiert die Zeile (Feedrate,
|
||||
Zeitstempel als Kommentar `;<epoch>`) und hängt sie an.
|
||||
```json
|
||||
{ "pose": { "x": 0, "y": 300, "z": 0, "a": 1.5708, "b": -1.5708, "c": 0, "e": 0.12 },
|
||||
"feedrate": 1000 }
|
||||
```
|
||||
→ `201 { "index": 12, "line": "G90 G1 x0 y300 z0 a90.00 b-90.00 c0.00 e6.88 f1000 ;1759566014" }`
|
||||
|
||||
### `POST /active/lines`
|
||||
Rohe Zeile(n) anhängen/einfügen (z. B. Pause `G4`):
|
||||
```json
|
||||
{ "line": "G4 P0.5", "atIndex": 8 }
|
||||
```
|
||||
|
||||
### `PUT /active/lines/{index}` · `DELETE /active/lines/{index}`
|
||||
Einzelne Zeile ersetzen / löschen (Editieren der Aufnahme).
|
||||
|
||||
---
|
||||
|
||||
## 7. Endpoints — Playback (kontinuierlich)
|
||||
|
||||
### `POST /active/play` ← `FPlay`
|
||||
```jsonc
|
||||
{ "mode": "run", "fromStart": false } // "run" = bis Ende/Stop; "step" = eine Zeile
|
||||
```
|
||||
Die appRobotFileservice liefert die Zeilen getaktet zurück bzw. meldet Fortschritt
|
||||
über den Event-Kanal (§8); **ausgeführt werden sie vom Driver**. `POST /active/stop`
|
||||
hält an.
|
||||
|
||||
> **„Nächste File"** (Playlist über mehrere Programme) baut darauf auf:
|
||||
> `POST /playlist/next` lädt das nächste Programm (`PUT /active`) und startet `play`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Optionaler Event-Kanal (WebSocket)
|
||||
|
||||
Für eine Live-UI der appRobotFileservice (Fortschritt) ohne Polling:
|
||||
```json
|
||||
{ "event": "cursorMoved", "cursor": 5, "line": "G90 G1 … a1.5708 …" }
|
||||
{ "event": "activeChanged", "programId": "demo_c", "lineCount": 12 }
|
||||
{ "event": "playStopped", "cursor": 9, "reason": "end" }
|
||||
```
|
||||
(Die *Roboter*-Pose-Updates laufen weiterhin über den Driver-WS-Broadcast — der
|
||||
Fileservice kennt die Pose nur, soweit der Driver sie beim `FPoint` mitgibt.)
|
||||
|
||||
---
|
||||
|
||||
## 9. Fehler-Envelope
|
||||
|
||||
Konsistent mit dem Driver (`doc/ToDo_5_API.md`): `{ type, code, message, input }`.
|
||||
Der Driver reicht Fileservice-Fehler unverändert an die Steuerung zurück.
|
||||
|
||||
| `code` | Bedeutung |
|
||||
|---|---|
|
||||
| `PROGRAM_NOT_FOUND` | `{id}` existiert nicht |
|
||||
| `INVALID_NAME` | unzulässiger Name (kein Pfad) |
|
||||
| `EMPTY_PROGRAM` | `FLoad` auf Programm ohne gültige Zeile |
|
||||
| `CURSOR_OUT_OF_RANGE` | `next`/`prev`/`goto` über die Grenzen |
|
||||
| `NO_ACTIVE_PROGRAM` | Aktion erfordert geladenes Programm |
|
||||
| `FILE_ERROR` | Storage-Fehler (`.gcode`/`.json`) |
|
||||
|
||||
```json
|
||||
{ "type": "error", "code": "PROGRAM_NOT_FOUND", "message": "no program 'demo_x'", "input": "demo_x" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Durchgereichte Payloads
|
||||
|
||||
`FShow`/`FList` können größere Antworten erzeugen, die der Driver nur durchreicht.
|
||||
Die appRobotFileservice hält sie **akzeptabel klein** (Paginierung, Übersichtsform),
|
||||
sodass der Weg über den Driver unkritisch bleibt.
|
||||
|
||||
---
|
||||
|
||||
## 11. Konfiguration
|
||||
|
||||
Die appRobotFileservice braucht **keinen** Driver-Zugang (kein `DRIVER_BASE_URL`).
|
||||
|
||||
| Variable | Zweck | Beispiel |
|
||||
|---|---|---|
|
||||
| `FILE_SERVICE_PORT` | Port | `2100` |
|
||||
| `STORAGE_DIR` | Verzeichnis für `.gcode` + `.json` | `./GCodeFiles` |
|
||||
| `FILE_EXT` | `gcode` oder `ngc` | `gcode` |
|
||||
| `STORE_ANGLE_UNIT` | Speichereinheit der Winkel | `deg` |
|
||||
| `FILE_API_KEY` | Bearer-Token (Schreiben) | — |
|
||||
|
||||
---
|
||||
|
||||
## 12. Beispiel-Flows (durch den Driver)
|
||||
|
||||
### Teaching-Session (Joystick → Aufnahme)
|
||||
```
|
||||
Steuerung → Driver: FLoad demo_c → Driver: PUT /active {id:"demo_c"}
|
||||
Steuerung → Driver: G1 …/$J= (Arm bewegen, lokal — Fileservice unbeteiligt)
|
||||
Steuerung → Driver: FPoint → Driver hängt Live-Pose an,
|
||||
POST /active/points { pose, feedrate }
|
||||
… weitere Punkte …
|
||||
Steuerung → Driver: FSave "Demo C" → Driver: POST /programs {name,fromActive:true}
|
||||
→ demo_c.gcode + demo_c.json
|
||||
```
|
||||
|
||||
### Playback-Session (Datei → Roboter, schrittweise)
|
||||
```
|
||||
Steuerung → Driver: FList → GET /programs (Auswahl)
|
||||
Steuerung → Driver: FLoad demo_c → PUT /active
|
||||
Steuerung → Driver: FFirst → POST /active/first → {line (Radian)}
|
||||
Driver: receiveGCode(line) → Bewegung
|
||||
Driver: Pose-Broadcast an alle UIs
|
||||
Steuerung → Driver: FPlus … / FPlay
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. Verweise
|
||||
- [`draft_filehandeling.md`](draft_filehandeling.md) — Konzept, Gateway-Rolle, Einheiten, Storage
|
||||
- [`API.md`](API.md) — bestehende Driver-Endpunkte (`/api/position`, WS `:2095`)
|
||||
- [`ToDo_6b_FileHandling.md`](ToDo_6b_FileHandling.md) — gelöste Detailprobleme
|
||||
@@ -1,94 +0,0 @@
|
||||
services:
|
||||
|
||||
appRobotGuacamole:
|
||||
image: abesnier/guacamole:latest
|
||||
container_name: appRobot_guacamole
|
||||
ports:
|
||||
- "9080:8080"
|
||||
volumes:
|
||||
- C:/Users/kech/SynologyDrive/2026-AppServer-AppRobot/guacamole/guacamole/config:/config/guacamole
|
||||
- C:/Users/kech/SynologyDrive/2026-AppServer-AppRobot/guacamole/postgres:/config/postgres
|
||||
restart: unless-stopped
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
networks:
|
||||
- default
|
||||
|
||||
appRobotDriver:
|
||||
container_name: appRobot_Driver
|
||||
image: node:24-alpine
|
||||
working_dir: /usr/src/app
|
||||
volumes:
|
||||
- C:/Users/kech/SynologyDrive/2026-AppServer-AppRobot/appRobotDriver:/usr/src/app
|
||||
command: npm start
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- GRBL_BASE_IP=192.168.0.183
|
||||
- GRBL_ELLBOW_IP=192.168.0.202
|
||||
- GRBL_HAND_IP=192.168.0.250
|
||||
ports:
|
||||
- "2096:2095"
|
||||
- "2098:2098"
|
||||
expose:
|
||||
- "2095"
|
||||
networks:
|
||||
- default
|
||||
|
||||
|
||||
appRobotSimulation:
|
||||
container_name: appRobot_Simulation
|
||||
image: node:24-alpine
|
||||
working_dir: /usr/src/app
|
||||
volumes:
|
||||
- C:/Users/kech/SynologyDrive/2026-AppServer-AppRobot/appRobotSimulation:/usr/src/app
|
||||
environment:
|
||||
- TARGET_SERVER=wss://appRobot_Driver:2095
|
||||
command: npm start
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "1003:1003"
|
||||
networks:
|
||||
- default
|
||||
|
||||
|
||||
appRobotControl:
|
||||
image: node:24-alpine
|
||||
container_name: appRobot_Control
|
||||
working_dir: /app
|
||||
command: sh -c "npm install && node 3DInput.js"
|
||||
ports:
|
||||
- "10010:10010"
|
||||
volumes:
|
||||
- C:/Users/kech/SynologyDrive/2026-AppServer-AppRobot/appRobotControl:/app
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- TARGET_SERVER=wss://appRobot_Driver:2095
|
||||
depends_on:
|
||||
- appRobotDriver
|
||||
restart: unless-stopped
|
||||
|
||||
appRobotHoming:
|
||||
image: node:24-alpine
|
||||
container_name: appRobot_Homing
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- C:/Users/kech/SynologyDrive/2026-AppServer-AppRobot/appRobotHoming:/app
|
||||
- C:/Users/kech/SynologyDrive/2026-AppServer-AppRobot/AppRobotVideo/public/snapshots:/app/public/snapshots
|
||||
environment:
|
||||
- WSS_VIDEO_DRIVER=wss://localhost:8448
|
||||
- WSS_URL=wss://appRobot_Driver:2095
|
||||
- HTTPS_PORT=2093
|
||||
ports:
|
||||
- "2093:2093"
|
||||
depends_on:
|
||||
- appRobotDriver
|
||||
command: >
|
||||
/bin/sh -lc "npm ci || npm install && node server/server.js"
|
||||
networks:
|
||||
- default
|
||||
restart: unless-stopped
|
||||
|
||||
|
||||
networks:
|
||||
default:
|
||||
driver: bridge
|
||||
@@ -1,19 +1,5 @@
|
||||
services:
|
||||
|
||||
appRobotGuacamole:
|
||||
image: abesnier/guacamole:latest
|
||||
container_name: appRobot_guacamole
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- /home/chk/Documents/appServerInstallation/guacamole/config:/config/guacamole
|
||||
- /home/chk/Documents/appServerInstallation/guacamole/postgres:/config/postgres
|
||||
restart: unless-stopped
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
networks:
|
||||
- default
|
||||
|
||||
appRobotDriver:
|
||||
container_name: appRobot_Driver
|
||||
image: node:24-alpine
|
||||
@@ -23,225 +9,21 @@ services:
|
||||
command: npm start
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- NODE_OPTIONS=--inspect=0.0.0.0:2081
|
||||
- GRBL_BASE_IP=192.168.0.183
|
||||
- GRBL_ELLBOW_IP=192.168.0.202
|
||||
- GRBL_HAND_IP=192.168.0.250
|
||||
# Austauschbare Kinematik (siehe doc/ToDo_12_InverseKinematikConfig_ROADMAP.md).
|
||||
# Defaults entsprechen exakt dem bisherigen Verhalten.
|
||||
- ROBOT_SPEED_MODE=correct
|
||||
- ROBOT_KINEMATICS=arm3segmentlinearx
|
||||
- ROBOT_KINEMATICS_PARAMS={"l1": 250, "l2": 264, "l3": 100}
|
||||
- ROBOT_GRBL_AUTOREPORT=true
|
||||
- ROBOT_GRBL_REPORT_INTERVAL=200
|
||||
ports:
|
||||
- "2096:2095"
|
||||
- "2098:2098"
|
||||
expose:
|
||||
- "2095"
|
||||
- "2081:2081"
|
||||
networks:
|
||||
- default
|
||||
|
||||
|
||||
appRobotSimulation:
|
||||
container_name: appRobot_Simulation
|
||||
image: node:24-alpine
|
||||
working_dir: /usr/src/app
|
||||
volumes:
|
||||
- /home/chk/Documents/appRobotSimulation:/usr/src/app
|
||||
environment:
|
||||
- TARGET_SERVER=wss://appRobot_Driver:2095
|
||||
command: npm start
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "1003:1003"
|
||||
networks:
|
||||
- default
|
||||
|
||||
|
||||
appRobotControl:
|
||||
image: node:24-alpine
|
||||
container_name: appRobot_Control
|
||||
working_dir: /app
|
||||
command: sh -c "npm install && node 3DInput.js"
|
||||
ports:
|
||||
- "10010:10010"
|
||||
volumes:
|
||||
- /home/chk/Documents/appRobotControl:/app
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- TARGET_SERVER=wss://appRobot_Driver:2095
|
||||
depends_on:
|
||||
- appRobotDriver
|
||||
restart: unless-stopped
|
||||
|
||||
appRobotHoming:
|
||||
image: node:24-alpine
|
||||
container_name: appRobot_Homing
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- /home/chk/Documents/appRobotHoming:/app
|
||||
- /home/chk/Documents/AppRobotVideo/public/snapshots:/app/public/snapshots
|
||||
environment:
|
||||
- WSS_VIDEO_DRIVER=wss://localhost:8448
|
||||
- WSS_URL=wss://appRobot_Driver:2095
|
||||
- HTTPS_PORT=2093
|
||||
ports:
|
||||
- "2093:2093"
|
||||
depends_on:
|
||||
- appRobotDriver
|
||||
command: >
|
||||
/bin/sh -lc "npm ci || npm install && node server/server.js"
|
||||
networks:
|
||||
- default
|
||||
restart: unless-stopped
|
||||
|
||||
# appRobotDirectBase:
|
||||
# image: node:20-bullseye
|
||||
# container_name: appRobot_DirectBase
|
||||
# network_mode: host
|
||||
# working_dir: /app
|
||||
# volumes:
|
||||
# - /home/chk/Documents/appRobotDirectControlBase:/app
|
||||
# environment:
|
||||
# - FluidNcHost=192.168.0.183
|
||||
# - FluidNcPort=80
|
||||
# - PORT=2098
|
||||
# ports:
|
||||
# - "2098:2098"
|
||||
# command: >
|
||||
# /bin/bash -lc "
|
||||
# npm ci || npm install &&
|
||||
# node server/server.js
|
||||
# "
|
||||
# restart: unless-stopped
|
||||
|
||||
|
||||
|
||||
appRobot_Tunnel:
|
||||
image: alpine:latest
|
||||
container_name: appRobot_Tunnel
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- TZ=Europe/Zurich
|
||||
volumes:
|
||||
- /home/chk/Documents/AppServerPortalUI/.ssh:/root/.ssh:ro
|
||||
command: >
|
||||
/bin/sh -c "
|
||||
apk add --no-cache openssh-client autossh &&
|
||||
autossh -M 0 -N -o StrictHostKeyChecking=no \
|
||||
-i /root/.ssh/id_ed25519 \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o ServerAliveInterval=60 \
|
||||
-o ServerAliveCountMax=10 \
|
||||
-o ExitOnForwardFailure=yes \
|
||||
-N \
|
||||
-R 0.0.0.0:9703:portainer:9000 \
|
||||
-R 0.0.0.0:9710:appRobot_Control:10010 \
|
||||
-R 0.0.0.0:9798:appRobot_Driver:2098 \
|
||||
-R 0.0.0.0:9712:appRobot_Simulation:1003\
|
||||
-R 0.0.0.0:9743:AppRobotVideo:8443 \
|
||||
-R 0.0.0.0:9780:appRobot_guacamole:8080 \
|
||||
-R 0.0.0.0:9793:appRobot_Homing:2093 \
|
||||
-R 0.0.0.0:9725:appRobot_AccessBase:443 \
|
||||
-R 0.0.0.0:9726:appRobot_AccessEllbow:443 \
|
||||
-R 0.0.0.0:9727:appRobot_AccessHand:443 \
|
||||
tunnel@server.schooltech.ch -p 2255
|
||||
"
|
||||
|
||||
cloudflared:
|
||||
image: cloudflare/cloudflared:latest
|
||||
container_name: appServer_cloudflare
|
||||
command: tunnel --no-autoupdate run --token eyJhIjoiOWUyYzk0OTI1ZWVlNmE4NjRiZjllZGRiM2ZmMDRmMTUiLCJ0IjoiZDc2YzI2MjAtZGE0ZC00OTJmLWI5YjgtODNjMjgwNjQ5MTFlIiwicyI6IllUbGpPREJtTURndFpHSTVZUzAwWkRnekxXRTRNek10TXpaaE56WTBabUpsT1RBMSJ9
|
||||
# networks:
|
||||
# - default
|
||||
# - appRobotNet
|
||||
restart: unless-stopped
|
||||
|
||||
|
||||
|
||||
|
||||
appRobot_nextcloud_db:
|
||||
image: mariadb:latest
|
||||
container_name: appRobot_nextcloud_db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: KantiWattwilABC
|
||||
MYSQL_DATABASE: nextcloud
|
||||
MYSQL_USER: nextcloud
|
||||
MYSQL_PASSWORD: KantiABC122nextcloudX3!
|
||||
volumes:
|
||||
- /home/chk/Documents/appRobotNextCloud/db:/var/lib/mysql
|
||||
|
||||
appRobot_nextcloud:
|
||||
image: nextcloud:latest
|
||||
container_name: appRobot_nextcloud
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_PASSWORD: KantiABC122nextcloudX3!
|
||||
MYSQL_DATABASE: nextcloud
|
||||
MYSQL_USER: nextcloud
|
||||
MYSQL_HOST: appRobot_nextcloud_db
|
||||
volumes:
|
||||
- /home/chk/Documents/appRobotNextCloud/nextcloud:/var/www/html
|
||||
- /home/chk/Documents:/mnt/server:rw
|
||||
depends_on:
|
||||
- appRobot_nextcloud_db
|
||||
ports:
|
||||
- "9183:80"
|
||||
|
||||
|
||||
appRobot_AccessBase:
|
||||
image: node:20-bullseye
|
||||
# Alternativ: node:20-alpine (kleiner, aber evtl. openssl/ca/certs nachziehen)
|
||||
container_name: appRobot_AccessBase
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- /home/chk/Documents/appRobotControlScara:/app
|
||||
environment:
|
||||
- FluidNcHost=192.168.0.183 #fluidncbase.local
|
||||
- FluidNcPort=80
|
||||
- PORT=443
|
||||
command: >
|
||||
/bin/bash -lc "
|
||||
npm ci || npm install &&
|
||||
node server/server.js
|
||||
"
|
||||
restart: unless-stopped
|
||||
|
||||
|
||||
appRobot_AccessEllbow:
|
||||
image: node:20-bullseye
|
||||
# Alternativ: node:20-alpine (kleiner, aber evtl. openssl/ca/certs nachziehen)
|
||||
container_name: appRobot_AccessEllbow
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- /home/chk/Documents/appRobotControlScara:/app
|
||||
environment:
|
||||
- FluidNcHost=192.168.0.202 #fluidncellbow.local
|
||||
- FluidNcPort=80
|
||||
- PORT=443
|
||||
command: >
|
||||
/bin/bash -lc "
|
||||
npm ci || npm install &&
|
||||
node server/server.js
|
||||
"
|
||||
restart: unless-stopped
|
||||
|
||||
appRobot_AccessHand:
|
||||
image: node:20-bullseye
|
||||
# Alternativ: node:20-alpine (kleiner, aber evtl. openssl/ca/certs nachziehen)
|
||||
container_name: appRobot_AccessHand
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- /home/chk/Documents/appRobotControlScara:/app
|
||||
environment:
|
||||
- FluidNcHost=192.168.0.250 #fluidnchand.local
|
||||
- FluidNcPort=80
|
||||
- PORT=443
|
||||
command: >
|
||||
/bin/bash -lc "
|
||||
npm ci || npm install &&
|
||||
node server/server.js
|
||||
"
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
default:
|
||||
driver: bridge
|
||||
|
||||
@@ -10479,3 +10479,89 @@
|
||||
2026-06-14T04:33:51.805Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T04:33:51.860Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T04:33:52.102Z ::ffff:127.0.0.1: G1 X1
|
||||
2026-06-14T05:17:34.207Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T05:17:34.440Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T05:17:34.682Z ::ffff:127.0.0.1: G1 X1
|
||||
2026-06-14T05:19:19.662Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T05:19:19.899Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T05:19:20.136Z ::ffff:127.0.0.1: G1 X1
|
||||
2026-06-14T05:19:23.482Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T05:19:23.492Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T05:20:40.271Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T05:20:40.492Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T05:20:40.722Z ::ffff:127.0.0.1: G1 X1
|
||||
2026-06-14T05:20:42.484Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T05:20:42.498Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T05:22:42.150Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T05:22:42.372Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T05:22:42.591Z ::ffff:127.0.0.1: G1 X1
|
||||
2026-06-14T05:22:44.666Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T05:22:44.682Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T05:59:06.033Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T05:59:06.060Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T05:59:06.230Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T05:59:06.459Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T05:59:06.682Z ::ffff:127.0.0.1: G1 X1
|
||||
2026-06-14T05:59:14.364Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T05:59:14.389Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T05:59:14.579Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T05:59:14.810Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T05:59:15.047Z ::ffff:127.0.0.1: G1 X1
|
||||
2026-06-14T05:59:21.641Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T05:59:21.662Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T05:59:21.915Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T05:59:22.141Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T05:59:22.378Z ::ffff:127.0.0.1: G1 X1
|
||||
2026-06-14T05:59:33.235Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T05:59:33.253Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T05:59:33.774Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T05:59:33.992Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T05:59:34.219Z ::ffff:127.0.0.1: G1 X1
|
||||
2026-06-14T05:59:43.288Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T05:59:43.321Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T05:59:43.617Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T05:59:43.843Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T05:59:44.068Z ::ffff:127.0.0.1: G1 X1
|
||||
2026-06-14T06:00:01.712Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T06:00:01.736Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T06:00:02.053Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T06:00:02.286Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T06:00:02.515Z ::ffff:127.0.0.1: G1 X1
|
||||
2026-06-14T06:00:13.944Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T06:00:14.028Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T06:00:14.037Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T06:00:14.160Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T06:00:14.388Z ::ffff:127.0.0.1: G1 X1
|
||||
2026-06-14T06:00:26.793Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T06:00:26.826Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T06:00:27.032Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T06:00:27.255Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T06:00:27.510Z ::ffff:127.0.0.1: G1 X1
|
||||
2026-06-14T06:00:33.304Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T06:00:33.520Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T06:00:33.747Z ::ffff:127.0.0.1: G1 X1
|
||||
2026-06-14T06:01:08.852Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T06:01:09.006Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T06:01:09.043Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T06:01:09.114Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T06:01:09.368Z ::ffff:127.0.0.1: G1 X1
|
||||
2026-06-14T06:01:35.823Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T06:01:35.982Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T06:01:36.013Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T06:01:36.056Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T06:01:36.309Z ::ffff:127.0.0.1: G1 X1
|
||||
2026-06-14T07:30:33.026Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T07:30:33.246Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T07:30:33.477Z ::ffff:127.0.0.1: G1 X1
|
||||
2026-06-14T07:30:35.424Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T07:30:35.432Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T07:30:37.899Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T07:30:38.024Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T07:30:38.042Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T07:30:38.131Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T07:30:38.364Z ::ffff:127.0.0.1: G1 X1
|
||||
2026-06-14T07:32:17.457Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T07:32:17.699Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T07:32:17.813Z ::ffff:127.0.0.1: M114
|
||||
2026-06-14T07:32:17.848Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-14T07:32:17.959Z ::ffff:127.0.0.1: G1 X1
|
||||
|
||||
@@ -14666,3 +14666,37 @@
|
||||
2026-06-14T04:33:34.893Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T04:33:51.379Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T04:33:51.767Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T05:17:33.987Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T05:19:19.429Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T05:19:23.471Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T05:20:40.050Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T05:20:42.471Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T05:22:41.934Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T05:22:44.654Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T05:59:05.974Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T05:59:06.000Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T05:59:14.310Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T05:59:14.333Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T05:59:21.602Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T05:59:21.676Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T05:59:33.204Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T05:59:33.547Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T05:59:43.223Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T05:59:43.380Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T06:00:01.676Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T06:00:01.788Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T06:00:13.713Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T06:00:14.008Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T06:00:26.750Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T06:00:26.782Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T06:00:33.082Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T06:01:08.583Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T06:01:08.951Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T06:01:35.569Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T06:01:35.944Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T07:30:32.804Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T07:30:35.400Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T07:30:37.637Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T07:30:37.987Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T07:32:17.191Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-14T07:32:17.745Z ::ffff:127.0.0.1 : Ping
|
||||
|
||||
@@ -210,7 +210,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
if (stops[2]) stops[2].setAttribute('stop-color', '#880000');
|
||||
if (textEl) textEl.setAttribute('fill', '#1a1000');
|
||||
if (textPath) textPath.textContent = 'EMERGENCY STOP';
|
||||
if (label) { label.textContent = '● Bestromt'; label.className = 'estop-armed-label armed'; }
|
||||
if (label) { label.textContent = 'Strom AN'; label.className = 'estop-armed-label armed'; }
|
||||
} else {
|
||||
// ── POWER ON: Dunkler Navy-Ring + blauer Knopf — ruhig, klar ────────────
|
||||
// Kein gelber Ring → kein E-Stop-Charakter.
|
||||
|
||||
@@ -31,6 +31,7 @@ class GCode{
|
||||
static containsCommand(s){
|
||||
|
||||
if(s.indexOf('M1 ') !== -1){return true;} // M1-Commands = G1-Command only for Motor-Coordinates
|
||||
if(s.indexOf('M114') === 0){return true;} // M114 R - Hardware-Sync (MPos lesen, ToDo_9 Paket 4)
|
||||
if(s.indexOf('G') !== 0){return false;}
|
||||
if(s.indexOf('G90') == 0){return true;}
|
||||
if(s.indexOf('G91') == 0){return true;}
|
||||
@@ -88,7 +89,9 @@ class GCode{
|
||||
* funktionieren.
|
||||
*/
|
||||
static receiveGCode(robot, g){
|
||||
RobotController.receive(robot, g);
|
||||
// Rückgabe durchreichen: synchron `undefined` wie bisher, oder ein Promise,
|
||||
// falls ein asynchroner Befehl (Hardware-Sync, ToDo_9 Paket 4) enthalten war.
|
||||
return RobotController.receive(robot, g);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////77
|
||||
|
||||
@@ -43,7 +43,14 @@ class GCodeParser {
|
||||
const params = {};
|
||||
for (let i = 1; i < tokens.length; i++) {
|
||||
const token = tokens[i].trim();
|
||||
if (token.length < 2) {
|
||||
if (token.length === 0) {
|
||||
continue;
|
||||
}
|
||||
if (token.length === 1) {
|
||||
// Einzelner Buchstabe = Flag ohne Zahlenwert (z. B. 'R' in 'M114 R').
|
||||
if (/^[A-Za-z]$/.test(token)) {
|
||||
params[token.toUpperCase()] = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,18 +8,26 @@
|
||||
* der Controller kennt nur strukturierte Befehle, keine rohen Textstrings.
|
||||
*/
|
||||
const GCodeParser = require('./GCodeParser');
|
||||
const { motorStateFromPorts } = require('./portInverse');
|
||||
|
||||
class RobotController {
|
||||
|
||||
/**
|
||||
* Parst eine rohe Nachricht und wendet alle enthaltenen Befehle der Reihe nach an.
|
||||
*
|
||||
* Rückgabe: `undefined` (synchron, wie bisher) für alle gewöhnlichen Befehle.
|
||||
* Nur wenn ein asynchroner Befehl enthalten war (Hardware-Sync, ToDo_9 Paket 4)
|
||||
* wird ein Promise zurückgegeben, das auf dessen Abschluss wartet.
|
||||
* @param {object} robot Robotermodell
|
||||
* @param {string|Buffer} message rohe G-Code-Nachricht
|
||||
*/
|
||||
static receive(robot, message) {
|
||||
const commands = GCodeParser.parse(message);
|
||||
if (!commands.length) return;
|
||||
commands.forEach(parsed => this.applyCommand(robot, parsed));
|
||||
const results = commands.map(parsed => this.applyCommand(robot, parsed));
|
||||
const pending = results.filter(r => r && typeof r.then === 'function');
|
||||
if (pending.length === 0) return; // synchroner Pfad unverändert
|
||||
return Promise.all(pending).then(arr => arr[arr.length - 1]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,6 +114,12 @@ class RobotController {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd === 'M114' && params.R === true) {
|
||||
// Hardware-Sync (ToDo_9 Paket 4): liest die echten MPos aller Controller
|
||||
// und übernimmt sie als Soll-Zustand. Asynchron → gibt ein Promise zurück.
|
||||
return this.syncFromHardware(robot);
|
||||
}
|
||||
|
||||
if (cmd === 'M92' || cmd === 'G92') {
|
||||
robot.createMotorPosition();
|
||||
if (Number.isFinite(params.X)) { robot.xMotor = params.X; robot.xMotorChanged = true; }
|
||||
@@ -121,6 +135,80 @@ class RobotController {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hardware-Sync (ToDo_9 Paket 4): liest die echten Achs-Positionen (`MPos`) der
|
||||
* drei Controller, rekonstruiert daraus die sieben Motorwerte und übernimmt sie
|
||||
* als neuen Soll-Zustand. Danach Vorwärtskinematik → Pose.
|
||||
*
|
||||
* Bewegt den Roboter NICHT — es wird kein `sendCommand()`/Move ausgelöst, nur der
|
||||
* interne Zustand an die Realität angeglichen (nach Homing/Jog/Stall/Reconnect).
|
||||
*
|
||||
* @param {object} robot
|
||||
* @param {{timeoutMs?: number}} [options]
|
||||
* @returns {Promise<{x,y,z,phi,theta,psi}>} die übernommene Pose
|
||||
*/
|
||||
static async syncFromHardware(robot, options = {}) {
|
||||
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 1000;
|
||||
const receivers = (robot && robot.cmdReceivers) || [];
|
||||
|
||||
// Sender nach Rolle zuordnen (controllerRole wird in startRobot.js gesetzt).
|
||||
const byRole = {};
|
||||
for (const s of receivers) {
|
||||
if (s && s.controllerRole) byRole[s.controllerRole] = s;
|
||||
}
|
||||
for (const role of ['base', 'elbow', 'hand']) {
|
||||
if (!byRole[role]) {
|
||||
throw new Error(`Sync: Controller '${role}' fehlt (kein Sender mit controllerRole='${role}')`);
|
||||
}
|
||||
}
|
||||
|
||||
// Frische Reports von allen drei Controllern anfordern (aktiv '?', mit await).
|
||||
const [baseSnap, elbowSnap, handSnap] = await Promise.all([
|
||||
byRole.base.requestStatusReport(timeoutMs),
|
||||
byRole.elbow.requestStatusReport(timeoutMs),
|
||||
byRole.hand.requestStatusReport(timeoutMs),
|
||||
]);
|
||||
|
||||
// MPos-Arrays validieren (FluidNC meldet ggf. mehr Achsen; nur die nötigen lesen).
|
||||
const need = (snap, role, n) => {
|
||||
const mp = snap && snap.machinePosition;
|
||||
if (!Array.isArray(mp) || mp.length < n || !mp.slice(0, n).every(Number.isFinite)) {
|
||||
throw new Error(`Sync: ${role} lieferte keine gültige MPos (${JSON.stringify(mp)})`);
|
||||
}
|
||||
return mp;
|
||||
};
|
||||
const b = need(baseSnap, 'base', 3);
|
||||
const e = need(elbowSnap, 'elbow', 1);
|
||||
const h = need(handSnap, 'hand', 3);
|
||||
|
||||
// Port → Motorwerte (linear/eindeutig, ToDo_9a) → auf den Roboter schreiben.
|
||||
const m = motorStateFromPorts({
|
||||
base: { x: b[0], y: b[1], z: b[2] },
|
||||
elbow: { x: e[0] },
|
||||
hand: { x: h[0], y: h[1], z: h[2] },
|
||||
});
|
||||
robot.xMotor = m.xMotor;
|
||||
robot.alpha = m.alpha;
|
||||
robot.beta = m.beta;
|
||||
robot.a = m.a;
|
||||
robot.b = m.b;
|
||||
robot.c = m.c;
|
||||
robot.eMotor = m.eMotor;
|
||||
|
||||
// Vorwärtskinematik: füllt x/y/z + phi/theta/psi aus den Hardwarewerten.
|
||||
robot.calculatePositionFromMotorAngles();
|
||||
|
||||
// motorPosition zurücksetzen, damit der nächste Move den Speed-Delta von der
|
||||
// echten Position aus rechnet (sonst falscher Feedrate im Korrekt-Modus).
|
||||
robot.motorPosition = null;
|
||||
robot.motorPositionOld = null;
|
||||
|
||||
return {
|
||||
x: robot.x, y: robot.y, z: robot.z,
|
||||
phi: robot.phi, theta: robot.theta, psi: robot.psi,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RobotController;
|
||||
|
||||
@@ -54,6 +54,33 @@ module.exports = class TelnetSenderGRBL extends SenderInterface {
|
||||
this.autoConnect = options.autoConnect !== false;
|
||||
this.isTestMode = false;
|
||||
|
||||
// ── Hardware-Feedback (ToDo_9 Paket 1/3) ──────────────────────────────
|
||||
// Eingehende GRBL/FluidNC-Antworten werden geparst (vorher verworfen).
|
||||
this._rxBuffer = ''; // Zeilen-Puffer für fragmentierte data-Events
|
||||
this.grblState = null; // 'Idle' | 'Run' | 'Alarm' | 'Hold' | ...
|
||||
this.machinePosition = null; // [x, y, z, …] aus MPos (oder WPos)
|
||||
this.machinePositionType = null; // 'MPos' | 'WPos'
|
||||
this.plannerBlocksFree = null; // erste Bf-Zahl (freie Planner-Blöcke)
|
||||
this.rxBytesFree = null; // zweite Bf-Zahl (freie RX-Bytes)
|
||||
this.lastResponse = null; // letzte empfangene Zeile (roh)
|
||||
this.lastError = null; // letzte error:/ALARM:-Zeile
|
||||
this.lastOk = 0; // Zeitstempel des letzten 'ok'
|
||||
this.lastReportAt = 0; // Zeitstempel des letzten <…>-Reports
|
||||
this._statusWaiters = []; // offene requestStatusReport()-Promises (Sync, Paket 4)
|
||||
|
||||
// Auto-Reporting (Paket 3) — opt-in, schreibt persistente FluidNC-Settings.
|
||||
// Default AUS: ohne Flag wird KEIN $10/$Report-Kommando an die Hardware
|
||||
// gesendet; der Treiber liest nur die Antworten des ohnehin laufenden
|
||||
// ?-Heartbeats.
|
||||
this.autoReport = options.autoReport !== undefined
|
||||
? !!options.autoReport
|
||||
: (process.env.ROBOT_GRBL_AUTOREPORT === 'true');
|
||||
this.reportInterval = Number.isFinite(options.reportInterval)
|
||||
? options.reportInterval
|
||||
: (Number.isFinite(Number(process.env.ROBOT_GRBL_REPORT_INTERVAL))
|
||||
? Number(process.env.ROBOT_GRBL_REPORT_INTERVAL)
|
||||
: 200);
|
||||
|
||||
if (urlGRBL === "test.test") {
|
||||
this.tSocket = { written: "", write(txt){ this.written = txt; } };
|
||||
this.isTestMode = true;
|
||||
@@ -128,6 +155,7 @@ module.exports = class TelnetSenderGRBL extends SenderInterface {
|
||||
this.tSocket.on('close', () => {
|
||||
console.log("Telnet Closed " + this.urlGRBLstr);
|
||||
this._stopHeartbeat();
|
||||
this._rejectStatusWaiters(new Error(`${this.urlGRBLstr}: Verbindung geschlossen`));
|
||||
this.tSocket = null;
|
||||
this._rawSocket = null;
|
||||
if (this.shouldReconnect) {
|
||||
@@ -141,6 +169,13 @@ module.exports = class TelnetSenderGRBL extends SenderInterface {
|
||||
// Zeitstempel jeder eingehenden Nachricht festhalten (für Heartbeat-Timeout).
|
||||
socket.on('data', () => { this._lastDataAt = Date.now(); });
|
||||
|
||||
// Eingehende GRBL/FluidNC-Antworten lesen (ToDo_9 Paket 1).
|
||||
// Vorher wurde dieser Kanal verworfen — jetzt werden ok/error/<…>-Reports
|
||||
// geparst. Frischer Zeilen-Puffer pro Verbindung (Reste der toten
|
||||
// Verbindung dürfen die neue nicht verfälschen).
|
||||
this._rxBuffer = '';
|
||||
this.tSocket.on('data', (chunk) => this._handleIncomingData(chunk));
|
||||
|
||||
this.state = 'connected';
|
||||
this.error = null;
|
||||
this.reconnectAttempt = 0;
|
||||
@@ -153,6 +188,9 @@ module.exports = class TelnetSenderGRBL extends SenderInterface {
|
||||
|
||||
// Heartbeat starten: erkennt NotAus / tote Verbindungen.
|
||||
this._startHeartbeat();
|
||||
|
||||
// Auto-Reporting konfigurieren (Paket 3) — nur bei aktivem Opt-in.
|
||||
this._configureAutoReport();
|
||||
});
|
||||
|
||||
socket.on('error', (error) => {
|
||||
@@ -238,6 +276,182 @@ module.exports = class TelnetSenderGRBL extends SenderInterface {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Konfiguriert FluidNC-Auto-Reporting (ToDo_9 Paket 3) — nur bei aktivem Opt-in.
|
||||
*
|
||||
* Schreibt persistente Controller-Settings:
|
||||
* $10=3 → Statusreport enthält MPos und Bf
|
||||
* $Report/Interval=N → FluidNC pusht den Status während der Bewegung selbst
|
||||
*
|
||||
* Default AUS (ROBOT_GRBL_AUTOREPORT != 'true'): ohne Opt-in wird NICHTS gesendet,
|
||||
* der Treiber liest nur die Antworten des ohnehin laufenden ?-Heartbeats.
|
||||
* @private
|
||||
*/
|
||||
_configureAutoReport() {
|
||||
if (!this.autoReport || !this.tSocket) return;
|
||||
try {
|
||||
this.tSocket.write('$10=3\r\n');
|
||||
this.tSocket.write(`$Report/Interval=${this.reportInterval}\r\n`);
|
||||
console.log(
|
||||
`[TelnetSenderGRBL] ${this.urlGRBLstr}: Auto-Reporting aktiviert ` +
|
||||
`($10=3, $Report/Interval=${this.reportInterval})`
|
||||
);
|
||||
} catch (err) {
|
||||
// Fehler kommt ohnehin über das 'error'-Event; hier nur defensiv loggen.
|
||||
console.log(
|
||||
`[TelnetSenderGRBL] ${this.urlGRBLstr}: Auto-Report-Setup fehlgeschlagen: ${err.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet einen eingehenden data-Chunk vom TelnetSocket (ToDo_9 Paket 1).
|
||||
* Puffert über Zeilengrenzen (TCP-Chunks zerteilen Nachrichten beliebig) und
|
||||
* gibt jede vollständige Zeile an _handleResponseLine().
|
||||
*
|
||||
* Wirft nie — ein Parsefehler darf den data-Handler (und damit den Prozess)
|
||||
* nicht abreißen lassen.
|
||||
* @private
|
||||
*/
|
||||
_handleIncomingData(chunk) {
|
||||
try {
|
||||
this._rxBuffer += chunk.toString('utf8');
|
||||
|
||||
// Schutz gegen unbegrenztes Wachstum, falls Zeilenenden ausbleiben.
|
||||
if (this._rxBuffer.length > 8192) {
|
||||
this._rxBuffer = this._rxBuffer.slice(-8192);
|
||||
}
|
||||
|
||||
let idx;
|
||||
while ((idx = this._rxBuffer.search(/\r?\n/)) !== -1) {
|
||||
const line = this._rxBuffer.slice(0, idx);
|
||||
const nlLen = this._rxBuffer[idx] === '\r' ? 2 : 1;
|
||||
this._rxBuffer = this._rxBuffer.slice(idx + nlLen);
|
||||
this._handleResponseLine(line);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`[TelnetSenderGRBL] ${this.urlGRBLstr}: data-Parse-Fehler: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Klassifiziert eine einzelne Antwortzeile und aktualisiert den geparsten Zustand.
|
||||
* Demultiplext nach Nachrichtentyp (ToDo_9, Protokoll-Fakt 5): toleriert fremde
|
||||
* Zeilen (Cross-Channel-Bleed-Through), nimmt kein striktes 1:1 Request→Response an.
|
||||
* @private
|
||||
*/
|
||||
_handleResponseLine(line) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) return;
|
||||
this.lastResponse = trimmed;
|
||||
|
||||
if (trimmed[0] === '<') {
|
||||
this._parseStatusReport(trimmed);
|
||||
return;
|
||||
}
|
||||
if (trimmed === 'ok') {
|
||||
this.lastOk = Date.now();
|
||||
return;
|
||||
}
|
||||
if (/^error:/i.test(trimmed) || /^ALARM/i.test(trimmed)) {
|
||||
this.lastError = trimmed;
|
||||
console.log(`[TelnetSenderGRBL] ${this.urlGRBLstr}: GRBL meldet ${trimmed}`);
|
||||
return;
|
||||
}
|
||||
// Alles andere (Start-Banner, [MSG:…], Echo, fremde Kanäle): bewusst ignorieren.
|
||||
}
|
||||
|
||||
/**
|
||||
* Parst einen FluidNC-Statusreport: <State|MPos:x,y,z|Bf:blocks,bytes|…>.
|
||||
* Speichert State, Maschinenposition (MPos bevorzugt, sonst WPos) und Bf.
|
||||
* Defensiv: unvollständige/zerstörte Felder werden übersprungen, nie geworfen.
|
||||
* @private
|
||||
*/
|
||||
_parseStatusReport(line) {
|
||||
const inner = line.replace(/^</, '').replace(/>$/, '');
|
||||
const fields = inner.split('|');
|
||||
if (!fields.length) return;
|
||||
|
||||
if (fields[0]) this.grblState = fields[0];
|
||||
|
||||
for (let i = 1; i < fields.length; i++) {
|
||||
const sep = fields[i].indexOf(':');
|
||||
if (sep === -1) continue;
|
||||
const key = fields[i].slice(0, sep);
|
||||
const value = fields[i].slice(sep + 1);
|
||||
if (!value) continue;
|
||||
|
||||
if (key === 'MPos' || key === 'WPos') {
|
||||
const nums = value.split(',').map(Number);
|
||||
if (nums.length && nums.every(Number.isFinite)) {
|
||||
this.machinePosition = nums;
|
||||
this.machinePositionType = key;
|
||||
}
|
||||
} else if (key === 'Bf') {
|
||||
const nums = value.split(',').map(Number);
|
||||
if (Number.isFinite(nums[0])) this.plannerBlocksFree = nums[0];
|
||||
if (Number.isFinite(nums[1])) this.rxBytesFree = nums[1];
|
||||
}
|
||||
}
|
||||
this.lastReportAt = Date.now();
|
||||
this._resolveStatusWaiters();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fordert einen frischen Statusreport an (ToDo_9 Paket 4): sendet das
|
||||
* FluidNC-Realtime-Byte '?' und wartet auf den nächsten geparsten `<…>`-Report.
|
||||
*
|
||||
* Löst mit einem Snapshot ({grblState, machinePosition, …}) auf, sobald ein
|
||||
* Report eintrifft, oder wirft nach `timeoutMs` ohne Antwort. Verändert den
|
||||
* Roboterzustand NICHT — reines Lesen.
|
||||
*
|
||||
* @param {number} timeoutMs
|
||||
* @returns {Promise<{grblState, machinePosition, machinePositionType, plannerBlocksFree, rxBytesFree}>}
|
||||
*/
|
||||
requestStatusReport(timeoutMs = 1000) {
|
||||
if (!this.tSocket || typeof this.tSocket.write !== 'function') {
|
||||
return Promise.reject(new Error(`${this.urlGRBLstr}: not connected`));
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const waiter = { resolve, reject, timer: null };
|
||||
waiter.timer = this.setTimeoutFn(() => {
|
||||
this._statusWaiters = this._statusWaiters.filter(w => w !== waiter);
|
||||
reject(new Error(`${this.urlGRBLstr}: Statusreport-Timeout nach ${timeoutMs}ms`));
|
||||
}, timeoutMs);
|
||||
this._statusWaiters.push(waiter);
|
||||
try {
|
||||
this.tSocket.write('?');
|
||||
} catch (err) {
|
||||
this.clearTimeoutFn(waiter.timer);
|
||||
this._statusWaiters = this._statusWaiters.filter(w => w !== waiter);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Löst alle offenen requestStatusReport()-Promises mit dem aktuellen Snapshot auf. @private */
|
||||
_resolveStatusWaiters() {
|
||||
if (!this._statusWaiters || this._statusWaiters.length === 0) return;
|
||||
const snapshot = {
|
||||
grblState: this.grblState,
|
||||
machinePosition: this.machinePosition,
|
||||
machinePositionType: this.machinePositionType,
|
||||
plannerBlocksFree: this.plannerBlocksFree,
|
||||
rxBytesFree: this.rxBytesFree,
|
||||
};
|
||||
const waiters = this._statusWaiters;
|
||||
this._statusWaiters = [];
|
||||
waiters.forEach(w => { this.clearTimeoutFn(w.timer); w.resolve(snapshot); });
|
||||
}
|
||||
|
||||
/** Bricht offene requestStatusReport()-Promises ab (z. B. bei Verbindungsverlust). @private */
|
||||
_rejectStatusWaiters(reason) {
|
||||
if (!this._statusWaiters || this._statusWaiters.length === 0) return;
|
||||
const waiters = this._statusWaiters;
|
||||
this._statusWaiters = [];
|
||||
waiters.forEach(w => { this.clearTimeoutFn(w.timer); w.reject(reason); });
|
||||
}
|
||||
|
||||
send(command) {
|
||||
if (!this.tSocket || typeof this.tSocket.write !== 'function') {
|
||||
return false;
|
||||
@@ -259,12 +473,22 @@ module.exports = class TelnetSenderGRBL extends SenderInterface {
|
||||
error: this.error,
|
||||
isTestMode: !!this.isTestMode,
|
||||
reconnectAttempt: this.reconnectAttempt,
|
||||
reconnectTimer: !!this.reconnectTimer
|
||||
reconnectTimer: !!this.reconnectTimer,
|
||||
// Hardware-Feedback (ToDo_9 Paket 1/3)
|
||||
grblState: this.grblState,
|
||||
machinePosition: this.machinePosition,
|
||||
machinePositionType: this.machinePositionType,
|
||||
plannerBlocksFree: this.plannerBlocksFree,
|
||||
rxBytesFree: this.rxBytesFree,
|
||||
lastError: this.lastError,
|
||||
lastReportAt: this.lastReportAt,
|
||||
autoReport: !!this.autoReport
|
||||
};
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this._stopHeartbeat();
|
||||
this._rejectStatusWaiters(new Error(`${this.urlGRBLstr}: disconnect`));
|
||||
|
||||
if (this.isTestMode) {
|
||||
this.tSocket = null;
|
||||
|
||||
38
robot/portInverse.js
Normal file
38
robot/portInverse.js
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Port→Motor-Rückrechnung (ToDo_9 Paket 4, Baustein).
|
||||
*
|
||||
* Rekonstruiert aus den von den drei GRBL/FluidNC-Controllern gemeldeten
|
||||
* Maschinen-Achswerten (`MPos`) die sieben Motorwerte des Roboters.
|
||||
*
|
||||
* Herleitung + Verifikation: doc/ToDo_9a_PortRueckrechnung.md
|
||||
* Tests: test/Robot.PortInverse.test.js (15 Tests)
|
||||
*
|
||||
* Gilt für die PRODUKTIV-Verkabelung (startRobot.js):
|
||||
* base: GRBL x←xMotor, y←alpha·D, z←(beta−alpha)·D
|
||||
* elbow: GRBL x←a·D
|
||||
* hand: GRBL x←(c−b)·D, y←eMotor·D, z←b·D
|
||||
*
|
||||
* Die Abbildung ist linear und EINDEUTIG umkehrbar — keine Zweig-Wahl nötig.
|
||||
* Ändert sich die Verkabelung in startRobot.js, muss diese Umkehrung mitgezogen
|
||||
* werden; der Round-Trip-Test `portValue(motorStateFromPorts(p)) ≈ p` schützt davor.
|
||||
*/
|
||||
|
||||
const D = 180 / Math.PI;
|
||||
|
||||
/**
|
||||
* @param {{base:{x:number,y:number,z:number}, elbow:{x:number}, hand:{x:number,y:number,z:number}}} r
|
||||
* GRBL-Readings (Grad bzw. mm) der drei Controller.
|
||||
* @returns {{xMotor:number, alpha:number, beta:number, a:number, b:number, c:number, eMotor:number}}
|
||||
*/
|
||||
function motorStateFromPorts(r) {
|
||||
const xMotor = r.base.x; // x-Port = xMotor (mm, direkt)
|
||||
const alpha = r.base.y / D; // y-Port = alpha·D
|
||||
const beta = (r.base.z + r.base.y) / D; // z-Port = (beta−alpha)·D ⇒ beta = z/D + alpha
|
||||
const a = r.elbow.x / D; // Elbow x-Port = a·D
|
||||
const b = r.hand.z / D; // Hand z-Port = b·D
|
||||
const c = (r.hand.x + r.hand.z) / D; // Hand x-Port = (c−b)·D ⇒ c = x/D + b
|
||||
const eMotor = r.hand.y / D; // Hand y-Port = eMotor·D
|
||||
return { xMotor, alpha, beta, a, b, c, eMotor };
|
||||
}
|
||||
|
||||
module.exports = { motorStateFromPorts, D };
|
||||
@@ -52,7 +52,14 @@ function createInfoServer(httpsOptions, sharedState, robot, GCode, senders, opti
|
||||
reconnectAttempt: status.reconnectAttempt || 0,
|
||||
reconnectTimer: !!status.reconnectTimer,
|
||||
health,
|
||||
reason
|
||||
reason,
|
||||
// Hardware-Feedback (ToDo_9 Paket 1/3): GRBL-Zustand aus dem Sender.
|
||||
grblState: status.grblState ?? null,
|
||||
machinePosition: status.machinePosition ?? null,
|
||||
plannerBlocksFree: status.plannerBlocksFree ?? null,
|
||||
rxBytesFree: status.rxBytesFree ?? null,
|
||||
lastError: status.lastError ?? null,
|
||||
lastReportAt: status.lastReportAt ?? null
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -52,11 +52,20 @@ function initInputWS(server, robot, GCode, sharedState) {
|
||||
if (GCode.containsCommand(message)) {
|
||||
console.log("🔵 GCode.receiveGCode: Incoming command: " + message);
|
||||
logCommand(sharedState, clientIP, message);
|
||||
let result;
|
||||
try {
|
||||
GCode.receiveGCode(robot, message);
|
||||
result = GCode.receiveGCode(robot, message);
|
||||
} catch (err) {
|
||||
return sendError(ws, 'GCODE_ERROR', err.message, message);
|
||||
}
|
||||
// Asynchroner Befehl (z. B. Hardware-Sync M114 R, ToDo_9 Paket 4): erst nach
|
||||
// Abschluss antworten; Fehler maschinenlesbar an den Anfrager zurückgeben.
|
||||
if (result && typeof result.then === 'function') {
|
||||
result
|
||||
.then(() => broadcast(wss, GCode.getM114(robot)))
|
||||
.catch(err => sendError(ws, 'GCODE_ERROR', err.message, message));
|
||||
return;
|
||||
}
|
||||
broadcast(wss, GCode.getM114(robot));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -112,6 +112,8 @@ function createApp(options = {}) {
|
||||
heartbeatInterval: ctrl.heartbeatInterval,
|
||||
deadTimeout: 2 * ctrl.heartbeatInterval,
|
||||
});
|
||||
// Rolle (base/elbow/hand) für den Hardware-Sync (ToDo_9 Paket 4) festhalten.
|
||||
instance.controllerRole = key;
|
||||
senders.push({ name, instance, isGCodeReceiver: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +117,13 @@ describe('InfoServer', () => {
|
||||
reconnectAttempt: 0,
|
||||
reconnectTimer: false,
|
||||
health: 'ok',
|
||||
reason: undefined
|
||||
reason: undefined,
|
||||
grblState: null,
|
||||
machinePosition: null,
|
||||
plannerBlocksFree: null,
|
||||
rxBytesFree: null,
|
||||
lastError: null,
|
||||
lastReportAt: null
|
||||
},
|
||||
{
|
||||
name: 'Hand',
|
||||
@@ -129,7 +135,13 @@ describe('InfoServer', () => {
|
||||
reconnectAttempt: 0,
|
||||
reconnectTimer: false,
|
||||
health: 'disconnected',
|
||||
reason: 'no active socket connection'
|
||||
reason: 'no active socket connection',
|
||||
grblState: null,
|
||||
machinePosition: null,
|
||||
plannerBlocksFree: null,
|
||||
rxBytesFree: null,
|
||||
lastError: null,
|
||||
lastReportAt: null
|
||||
}
|
||||
]);
|
||||
});
|
||||
@@ -175,7 +187,13 @@ describe('InfoServer', () => {
|
||||
reconnectAttempt: 2,
|
||||
reconnectTimer: true,
|
||||
health: 'warning',
|
||||
reason: undefined
|
||||
reason: undefined,
|
||||
grblState: null,
|
||||
machinePosition: null,
|
||||
plannerBlocksFree: null,
|
||||
rxBytesFree: null,
|
||||
lastError: null,
|
||||
lastReportAt: null
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
const TelnetSender = require('../robot/TelnetSenderGRBL');
|
||||
const MotorPosition = require('../robot/RobotMotorPosition');
|
||||
const Robot = require('../robot/kinematics/Arm3SegmentLinearX');
|
||||
// Produktiv-Funktion unter Test (vorher inline in dieser Datei, jetzt extrahiert).
|
||||
const { motorStateFromPorts } = require('../robot/portInverse');
|
||||
|
||||
const D = 180 / Math.PI;
|
||||
|
||||
@@ -27,18 +29,9 @@ function buildSenders() {
|
||||
};
|
||||
}
|
||||
|
||||
/* ---------- Die Umkehrung: GRBL-Readings → 7 Motorwerte ---------- */
|
||||
// r = { base:{x,y,z}, elbow:{x}, hand:{x,y,z} } (GRBL-Achswerte, Grad bzw. mm)
|
||||
function motorStateFromPorts(r) {
|
||||
const xMotor = r.base.x; // x-Port = xMotor (mm, direkt)
|
||||
const alpha = r.base.y / D; // y-Port = alpha·D
|
||||
const beta = (r.base.z + r.base.y) / D; // z-Port = (beta-alpha)·D ⇒ beta = z/D + alpha
|
||||
const a = r.elbow.x / D; // Elbow x-Port = a·D
|
||||
const b = r.hand.z / D; // Hand z-Port = b·D
|
||||
const c = (r.hand.x + r.hand.z) / D; // Hand x-Port = (c-b)·D ⇒ c = x/D + b
|
||||
const eMotor = r.hand.y / D; // Hand y-Port = eMotor·D
|
||||
return { xMotor, alpha, beta, a, b, c, eMotor };
|
||||
}
|
||||
/* ---------- Die Umkehrung kommt jetzt aus robot/portInverse.js ----------
|
||||
* (vorher hier inline; extrahiert für ToDo_9 Paket 4, damit Produktivcode und
|
||||
* dieser Verifikations-Test dieselbe Funktion nutzen.) */
|
||||
|
||||
/* ---------- Hilfen ---------- */
|
||||
// {x,y,z,a,b,c,e}-pos aus Motorwerten (Feld-Mapping der RobotMotorPosition)
|
||||
|
||||
141
test/RobotController.sync.test.js
Normal file
141
test/RobotController.sync.test.js
Normal file
@@ -0,0 +1,141 @@
|
||||
'use strict';
|
||||
// Tests für ToDo_9 Paket 4: Hardware-Sync-Command (M114 R).
|
||||
// - syncFromHardware liest MPos aller 3 Controller → Motorwerte → Pose
|
||||
// - bewegt den Roboter NICHT (kein sendCommand)
|
||||
// - robust gegen fehlende Rolle / ungültige/fehlende MPos
|
||||
|
||||
const RobotController = require('../robot/RobotController');
|
||||
const { motorStateFromPorts, D } = require('../robot/portInverse');
|
||||
const Robot = require('../robot/kinematics/Arm3SegmentLinearX');
|
||||
|
||||
// ── Fake-Sender mit Rolle und vorgegebenem MPos-Snapshot ─────────────────────
|
||||
function fakeSender(role, machinePosition, { fail } = {}) {
|
||||
return {
|
||||
controllerRole: role,
|
||||
requestStatusReport: jest.fn(() =>
|
||||
fail
|
||||
? Promise.reject(new Error(`${role}: timeout`))
|
||||
: Promise.resolve({ grblState: 'Idle', machinePosition, machinePositionType: 'MPos' })
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/** Roboter mit den drei Sendern, deren MPos die gegebenen Motorwerte kodieren. */
|
||||
function robotWithMotors(m) {
|
||||
const robot = new Robot(250, 264, 100);
|
||||
// Produktiv-Verkabelung: base[x,y,z]=[xMotor, alpha·D, (beta−alpha)·D],
|
||||
// elbow[x]=a·D, hand[x,y,z]=[(c−b)·D, eMotor·D, b·D]
|
||||
const base = [m.xMotor, m.alpha * D, (m.beta - m.alpha) * D];
|
||||
const elbow = [m.a * D];
|
||||
const hand = [(m.c - m.b) * D, m.eMotor * D, m.b * D];
|
||||
robot.cmdReceivers = [
|
||||
fakeSender('base', base),
|
||||
fakeSender('elbow', elbow),
|
||||
fakeSender('hand', hand),
|
||||
];
|
||||
return robot;
|
||||
}
|
||||
|
||||
describe('RobotController.syncFromHardware (ToDo_9 Paket 4)', () => {
|
||||
|
||||
beforeAll(() => { jest.spyOn(console, 'log').mockImplementation(() => {}); });
|
||||
afterAll(() => { jest.restoreAllMocks(); });
|
||||
|
||||
test('rekonstruiert Motorwerte aus MPos und übernimmt sie', async () => {
|
||||
const motors = { xMotor: 100, alpha: 0.30, beta: 0.50, a: 0.20, b: 0.40, c: 0.90, eMotor: 1.0 };
|
||||
const robot = robotWithMotors(motors);
|
||||
|
||||
await RobotController.syncFromHardware(robot);
|
||||
|
||||
expect(robot.xMotor).toBeCloseTo(motors.xMotor, 6);
|
||||
expect(robot.alpha).toBeCloseTo(motors.alpha, 6);
|
||||
expect(robot.beta).toBeCloseTo(motors.beta, 6);
|
||||
expect(robot.a).toBeCloseTo(motors.a, 6);
|
||||
expect(robot.b).toBeCloseTo(motors.b, 6);
|
||||
expect(robot.c).toBeCloseTo(motors.c, 6);
|
||||
expect(robot.eMotor).toBeCloseTo(motors.eMotor, 6);
|
||||
});
|
||||
|
||||
test('rechnet die Pose vor und gibt sie zurück (gleiche wie Referenz-Roboter)', async () => {
|
||||
const motors = { xMotor: 42, alpha: 0.10, beta: 0.10, a: 0.05, b: 1.20, c: 1.25, eMotor: 0.0 };
|
||||
const robot = robotWithMotors(motors);
|
||||
|
||||
const pose = await RobotController.syncFromHardware(robot);
|
||||
|
||||
const ref = new Robot(250, 264, 100);
|
||||
Object.assign(ref, motors);
|
||||
ref.calculatePositionFromMotorAngles();
|
||||
|
||||
for (const k of ['x', 'y', 'z', 'phi', 'theta', 'psi']) {
|
||||
expect(pose[k]).toBeCloseTo(ref[k], 6);
|
||||
}
|
||||
});
|
||||
|
||||
test('bewegt den Roboter NICHT (kein execCommand auf den Sendern)', async () => {
|
||||
const robot = robotWithMotors({ xMotor: 10, alpha: 0.1, beta: 0.2, a: 0.1, b: 0.2, c: 0.3, eMotor: 0 });
|
||||
// execCommand-Spy auf alle Sender; darf nie aufgerufen werden
|
||||
robot.cmdReceivers.forEach(s => { s.execCommand = jest.fn(); });
|
||||
|
||||
await RobotController.syncFromHardware(robot);
|
||||
|
||||
robot.cmdReceivers.forEach(s => expect(s.execCommand).not.toHaveBeenCalled());
|
||||
});
|
||||
|
||||
test('setzt motorPosition zurück (nächster Move rechnet von echter Position)', async () => {
|
||||
const robot = robotWithMotors({ xMotor: 5, alpha: 0, beta: 0, a: 0, b: 0, c: 0, eMotor: 0 });
|
||||
robot.motorPosition = { dummy: true };
|
||||
robot.motorPositionOld = { dummy: true };
|
||||
|
||||
await RobotController.syncFromHardware(robot);
|
||||
|
||||
expect(robot.motorPosition).toBeNull();
|
||||
expect(robot.motorPositionOld).toBeNull();
|
||||
});
|
||||
|
||||
test('fragt alle drei Controller aktiv ab (requestStatusReport)', async () => {
|
||||
const robot = robotWithMotors({ xMotor: 0, alpha: 0, beta: 0, a: 0, b: 0, c: 0, eMotor: 0 });
|
||||
await RobotController.syncFromHardware(robot);
|
||||
robot.cmdReceivers.forEach(s => expect(s.requestStatusReport).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
test('wirft, wenn eine Controller-Rolle fehlt', async () => {
|
||||
const robot = new Robot(250, 264, 100);
|
||||
robot.cmdReceivers = [fakeSender('base', [0, 0, 0]), fakeSender('hand', [0, 0, 0])]; // elbow fehlt
|
||||
await expect(RobotController.syncFromHardware(robot)).rejects.toThrow(/elbow/);
|
||||
});
|
||||
|
||||
test('wirft bei ungültiger MPos (zu wenige Achsen)', async () => {
|
||||
const robot = new Robot(250, 264, 100);
|
||||
robot.cmdReceivers = [
|
||||
fakeSender('base', [1, 2]), // nur 2 statt 3 Werte
|
||||
fakeSender('elbow', [0]),
|
||||
fakeSender('hand', [0, 0, 0]),
|
||||
];
|
||||
await expect(RobotController.syncFromHardware(robot)).rejects.toThrow(/base/);
|
||||
});
|
||||
|
||||
test('wirft, wenn ein Controller nicht antwortet (Timeout propagiert)', async () => {
|
||||
const robot = new Robot(250, 264, 100);
|
||||
robot.cmdReceivers = [
|
||||
fakeSender('base', [0, 0, 0]),
|
||||
fakeSender('elbow', [0], { fail: true }),
|
||||
fakeSender('hand', [0, 0, 0]),
|
||||
];
|
||||
await expect(RobotController.syncFromHardware(robot)).rejects.toThrow(/timeout/);
|
||||
});
|
||||
|
||||
test('Dispatch: applyCommand(M114 R) liefert ein Promise', () => {
|
||||
const robot = robotWithMotors({ xMotor: 0, alpha: 0, beta: 0, a: 0, b: 0, c: 0, eMotor: 0 });
|
||||
const result = RobotController.applyCommand(robot, { command: 'M114', params: { R: true } });
|
||||
expect(result && typeof result.then).toBe('function');
|
||||
return result; // sauber auflösen lassen
|
||||
});
|
||||
|
||||
test('receive(): bare M114 ohne R löst keinen Sync aus (synchron, undefined)', () => {
|
||||
const robot = robotWithMotors({ xMotor: 0, alpha: 0, beta: 0, a: 0, b: 0, c: 0, eMotor: 0 });
|
||||
robot.cmdReceivers.forEach(s => s.requestStatusReport.mockClear());
|
||||
const result = RobotController.receive(robot, 'M114');
|
||||
expect(result).toBeUndefined();
|
||||
robot.cmdReceivers.forEach(s => expect(s.requestStatusReport).not.toHaveBeenCalled());
|
||||
});
|
||||
});
|
||||
260
test/Sender.Telnet.responseParsing.test.js
Normal file
260
test/Sender.Telnet.responseParsing.test.js
Normal file
@@ -0,0 +1,260 @@
|
||||
'use strict';
|
||||
// Tests für ToDo_9 Paket 1/3:
|
||||
// Paket 1 — eingehende GRBL/FluidNC-Antworten lesen und klassifizieren
|
||||
// Paket 3 — opt-in Auto-Report-Konfiguration ($10=3, $Report/Interval)
|
||||
|
||||
const { EventEmitter } = require('events');
|
||||
const TelnetSenderGRBL = require('../robot/TelnetSenderGRBL');
|
||||
|
||||
// ── Hilfsfunktionen ──────────────────────────────────────────────────────────
|
||||
|
||||
function makeRawSocket() {
|
||||
const em = new EventEmitter();
|
||||
return Object.assign(em, {
|
||||
write: jest.fn(),
|
||||
end: jest.fn(),
|
||||
destroy: jest.fn(),
|
||||
setKeepAlive: jest.fn(),
|
||||
});
|
||||
}
|
||||
|
||||
function makeTelnetSocket() {
|
||||
const em = new EventEmitter();
|
||||
return Object.assign(em, {
|
||||
write: jest.fn(),
|
||||
end: jest.fn(),
|
||||
destroy: jest.fn(),
|
||||
});
|
||||
}
|
||||
|
||||
/** Sender mit gemockten Abhängigkeiten; simuliert erfolgreichen Verbindungsaufbau. */
|
||||
function setup(extraOptions = {}) {
|
||||
const rawSocket = makeRawSocket();
|
||||
const telnetSock = makeTelnetSocket();
|
||||
|
||||
const sender = new TelnetSenderGRBL(
|
||||
'robot.local', 5000,
|
||||
'x', 'y', 'z', null, null, null, null,
|
||||
{
|
||||
netModule: { createConnection: jest.fn(() => rawSocket) },
|
||||
TelnetSocketClass: jest.fn(() => telnetSock),
|
||||
setIntervalFn: jest.fn(() => 1),
|
||||
clearIntervalFn: jest.fn(),
|
||||
setTimeoutFn: jest.fn(() => 99),
|
||||
clearTimeoutFn: jest.fn(),
|
||||
autoConnect: false,
|
||||
...extraOptions,
|
||||
}
|
||||
);
|
||||
|
||||
sender.connect();
|
||||
rawSocket.emit('connect');
|
||||
|
||||
/** Simuliert vom Controller eingehende Bytes. */
|
||||
const recv = (str) => telnetSock.emit('data', Buffer.from(str, 'utf8'));
|
||||
|
||||
return { sender, rawSocket, telnetSock, recv };
|
||||
}
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('TelnetSenderGRBL — Antworten lesen (ToDo_9 Paket 1)', () => {
|
||||
|
||||
beforeAll(() => { jest.spyOn(console, 'log').mockImplementation(() => {}); });
|
||||
afterAll(() => { jest.restoreAllMocks(); });
|
||||
|
||||
test('parst Statusreport: State, MPos und Bf', () => {
|
||||
const { sender, recv } = setup();
|
||||
recv('<Idle|MPos:151.000,149.000,-1.000|Bf:15,128>\r\n');
|
||||
|
||||
expect(sender.grblState).toBe('Idle');
|
||||
expect(sender.machinePosition).toEqual([151.0, 149.0, -1.0]);
|
||||
expect(sender.machinePositionType).toBe('MPos');
|
||||
expect(sender.plannerBlocksFree).toBe(15);
|
||||
expect(sender.rxBytesFree).toBe(128);
|
||||
});
|
||||
|
||||
test('parst Run-Zustand', () => {
|
||||
const { sender, recv } = setup();
|
||||
recv('<Run|MPos:1,2,3|FS:500,0>\r\n');
|
||||
expect(sender.grblState).toBe('Run');
|
||||
expect(sender.machinePosition).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
test('nutzt WPos, wenn kein MPos vorhanden', () => {
|
||||
const { sender, recv } = setup();
|
||||
recv('<Idle|WPos:10.5,20.5,30.5>\r\n');
|
||||
expect(sender.machinePosition).toEqual([10.5, 20.5, 30.5]);
|
||||
expect(sender.machinePositionType).toBe('WPos');
|
||||
});
|
||||
|
||||
test('puffert über fragmentierte data-Events', () => {
|
||||
const { sender, recv } = setup();
|
||||
recv('<Idle|MPos:1.0');
|
||||
// Noch keine vollständige Zeile → Zustand unverändert
|
||||
expect(sender.machinePosition).toBeNull();
|
||||
|
||||
recv('00,2.000,3.000|Bf:10,120>\r\n');
|
||||
expect(sender.machinePosition).toEqual([1.0, 2.0, 3.0]);
|
||||
expect(sender.plannerBlocksFree).toBe(10);
|
||||
});
|
||||
|
||||
test('verarbeitet mehrere Zeilen in einem Chunk', () => {
|
||||
const { sender, recv } = setup();
|
||||
recv('ok\r\nok\r\n<Idle|MPos:0,0,0>\r\n');
|
||||
expect(sender.grblState).toBe('Idle');
|
||||
expect(sender.lastResponse).toBe('<Idle|MPos:0,0,0>');
|
||||
expect(sender.lastOk).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('erkennt "ok" und setzt lastOk', () => {
|
||||
const { sender, recv } = setup();
|
||||
expect(sender.lastOk).toBe(0);
|
||||
recv('ok\r\n');
|
||||
expect(sender.lastOk).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('erkennt error:-Zeile und legt sie in lastError ab', () => {
|
||||
const { sender, recv } = setup();
|
||||
recv('error:9\r\n');
|
||||
expect(sender.lastError).toBe('error:9');
|
||||
});
|
||||
|
||||
test('erkennt ALARM-Zeile', () => {
|
||||
const { sender, recv } = setup();
|
||||
recv('ALARM:1\r\n');
|
||||
expect(sender.lastError).toBe('ALARM:1');
|
||||
});
|
||||
|
||||
test('ignoriert fremde/unbekannte Zeilen ohne zu werfen (Cross-Channel)', () => {
|
||||
const { sender, recv } = setup();
|
||||
expect(() => recv('[MSG:some banner]\r\n')).not.toThrow();
|
||||
expect(sender.grblState).toBeNull();
|
||||
expect(sender.lastError).toBeNull();
|
||||
// lastResponse wird trotzdem gesetzt
|
||||
expect(sender.lastResponse).toBe('[MSG:some banner]');
|
||||
});
|
||||
|
||||
test('zerstörter Report wirft nicht und lässt alten Zustand bestehen', () => {
|
||||
const { sender, recv } = setup();
|
||||
recv('<Idle|MPos:1,2,3>\r\n');
|
||||
expect(() => recv('<Run|MPos:nope,bad,vals>\r\n')).not.toThrow();
|
||||
// State wird übernommen, kaputte Position aber nicht
|
||||
expect(sender.grblState).toBe('Run');
|
||||
expect(sender.machinePosition).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
test('Zeilen-Puffer wird bei Reconnect zurückgesetzt', () => {
|
||||
const { sender, rawSocket, telnetSock, recv } = setup();
|
||||
recv('<Idle|MPos:1.0'); // unvollständig im Puffer
|
||||
expect(sender._rxBuffer.length).toBeGreaterThan(0);
|
||||
|
||||
// Reconnect: neuer Socket, connect erneut
|
||||
telnetSock.emit('close');
|
||||
rawSocket.emit('connect');
|
||||
expect(sender._rxBuffer).toBe('');
|
||||
});
|
||||
|
||||
test('getStatus() liefert die Hardware-Feedback-Felder', () => {
|
||||
const { sender, recv } = setup();
|
||||
recv('<Hold|MPos:5,6,7|Bf:12,100>\r\n');
|
||||
const st = sender.getStatus();
|
||||
expect(st.grblState).toBe('Hold');
|
||||
expect(st.machinePosition).toEqual([5, 6, 7]);
|
||||
expect(st.plannerBlocksFree).toBe(12);
|
||||
expect(st.rxBytesFree).toBe(100);
|
||||
expect(st.autoReport).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TelnetSenderGRBL — Auto-Report Opt-in (ToDo_9 Paket 3)', () => {
|
||||
|
||||
beforeAll(() => { jest.spyOn(console, 'log').mockImplementation(() => {}); });
|
||||
afterAll(() => { jest.restoreAllMocks(); });
|
||||
|
||||
test('Default AUS: sendet beim Connect keine Settings-Kommandos', () => {
|
||||
const { telnetSock } = setup(); // autoReport nicht gesetzt
|
||||
expect(telnetSock.write).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('Opt-in EIN: sendet $10=3 und $Report/Interval beim Connect', () => {
|
||||
const { telnetSock } = setup({ autoReport: true, reportInterval: 150 });
|
||||
expect(telnetSock.write).toHaveBeenCalledWith('$10=3\r\n');
|
||||
expect(telnetSock.write).toHaveBeenCalledWith('$Report/Interval=150\r\n');
|
||||
});
|
||||
|
||||
test('Opt-in EIN: nutzt Default-Intervall 200, wenn nicht angegeben', () => {
|
||||
const { telnetSock } = setup({ autoReport: true });
|
||||
expect(telnetSock.write).toHaveBeenCalledWith('$Report/Interval=200\r\n');
|
||||
});
|
||||
});
|
||||
|
||||
describe('TelnetSenderGRBL — requestStatusReport (ToDo_9 Paket 4)', () => {
|
||||
|
||||
beforeAll(() => { jest.spyOn(console, 'log').mockImplementation(() => {}); });
|
||||
afterAll(() => { jest.restoreAllMocks(); });
|
||||
|
||||
// Setup mit erfassbarem Timeout-Callback (für den Timeout-Pfad).
|
||||
function setupTimed() {
|
||||
const rawSocket = makeRawSocket();
|
||||
const telnetSock = makeTelnetSocket();
|
||||
let timeoutCb = null;
|
||||
const sender = new TelnetSenderGRBL(
|
||||
'robot.local', 5000, 'x', 'y', 'z', null, null, null, null,
|
||||
{
|
||||
netModule: { createConnection: jest.fn(() => rawSocket) },
|
||||
TelnetSocketClass: jest.fn(() => telnetSock),
|
||||
setIntervalFn: jest.fn(() => 1),
|
||||
clearIntervalFn: jest.fn(),
|
||||
setTimeoutFn: jest.fn((cb) => { timeoutCb = cb; return 42; }),
|
||||
clearTimeoutFn: jest.fn(),
|
||||
autoConnect: false,
|
||||
}
|
||||
);
|
||||
sender.connect();
|
||||
rawSocket.emit('connect');
|
||||
const recv = (str) => telnetSock.emit('data', Buffer.from(str, 'utf8'));
|
||||
return { sender, rawSocket, telnetSock, recv, fireTimeout: () => timeoutCb && timeoutCb() };
|
||||
}
|
||||
|
||||
test('sendet ? und löst beim nächsten Report auf', async () => {
|
||||
const { sender, telnetSock, recv } = setupTimed();
|
||||
const p = sender.requestStatusReport(1000);
|
||||
expect(telnetSock.write).toHaveBeenCalledWith('?');
|
||||
|
||||
recv('<Idle|MPos:1,2,3|Bf:15,128>\r\n');
|
||||
const snap = await p;
|
||||
expect(snap.grblState).toBe('Idle');
|
||||
expect(snap.machinePosition).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
test('wirft bei Timeout ohne Report', async () => {
|
||||
const { sender, fireTimeout } = setupTimed();
|
||||
const p = sender.requestStatusReport(1000);
|
||||
fireTimeout();
|
||||
await expect(p).rejects.toThrow(/Timeout/);
|
||||
});
|
||||
|
||||
test('wirft, wenn nicht verbunden', async () => {
|
||||
const { sender } = setupTimed();
|
||||
sender.tSocket = null;
|
||||
await expect(sender.requestStatusReport(1000)).rejects.toThrow(/not connected/);
|
||||
});
|
||||
|
||||
test('bricht offene Anfrage bei Verbindungsverlust ab', async () => {
|
||||
const { sender, telnetSock } = setupTimed();
|
||||
const p = sender.requestStatusReport(1000);
|
||||
telnetSock.emit('close');
|
||||
await expect(p).rejects.toThrow(/geschlossen/);
|
||||
});
|
||||
|
||||
test('mehrere gleichzeitige Anfragen werden alle vom selben Report aufgelöst', async () => {
|
||||
const { sender, recv } = setupTimed();
|
||||
const p1 = sender.requestStatusReport(1000);
|
||||
const p2 = sender.requestStatusReport(1000);
|
||||
recv('<Run|MPos:4,5,6>\r\n');
|
||||
const [s1, s2] = await Promise.all([p1, p2]);
|
||||
expect(s1.machinePosition).toEqual([4, 5, 6]);
|
||||
expect(s2.machinePosition).toEqual([4, 5, 6]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user