Compare commits
2 Commits
319fae944a
...
8a669f23d3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a669f23d3 | ||
|
|
a807732b58 |
30
README.md
30
README.md
@@ -7,8 +7,9 @@ Dieses Projekt empfängt G-Code und Robotersteuerbefehle, berechnet Inverse Kine
|
|||||||
- `startRobot.js` startet zwei HTTPS-Server:
|
- `startRobot.js` startet zwei HTTPS-Server:
|
||||||
- Eingabe-Server + WebSocket für G-Code und Steuerbefehle
|
- Eingabe-Server + WebSocket für G-Code und Steuerbefehle
|
||||||
- Info-Server für Status, Position und einfache Weboberfläche
|
- Info-Server für Status, Position und einfache Weboberfläche
|
||||||
- `server/InputWS.js` empfängt Nachrichten von WebSocket-Clients, prüft sie auf G-Code oder Datei-Kommandos und gibt Positionsdaten zurück.
|
- `server/InputWS.js` empfängt Nachrichten von WebSocket-Clients, routet G-Code-Befehle lokal und leitet FCodes via `robot/FCodeClient.js` an `appRobotFileservice` weiter.
|
||||||
- `robot/GCode.js` verarbeitet G-Code, übersetzt ihn in Roboter-Koordinaten und triggert `robot.sendCommand()`.
|
- `robot/GCode.js` verarbeitet G-Code, übersetzt ihn in Roboter-Koordinaten und triggert `robot.sendCommand()` (kein Datei-Handling mehr).
|
||||||
|
- `robot/FCodeClient.js` übersetzt FCodes (`FPoint`, `FPlus`, …) in REST-Aufrufe an `appRobotFileservice` (Gateway-Funktion des Drivers).
|
||||||
- `robot/RobotBase.js` ist die abstrakte Basisklasse / der Interface-Vertrag: generische Infrastruktur (Zustand, `sendCommand`, Motor-Geschwindigkeiten) plus die zwei abstrakten Kinematik-Methoden.
|
- `robot/RobotBase.js` ist die abstrakte Basisklasse / der Interface-Vertrag: generische Infrastruktur (Zustand, `sendCommand`, Motor-Geschwindigkeiten) plus die zwei abstrakten Kinematik-Methoden.
|
||||||
- `robot/kinematics/Arm3SegmentLinearX.js` ist die konkrete Kinematik (Inverse + Vorwärts) für den aktuellen Arm. Die Auswahl der Kinematik erfolgt über `robot/KinematicsFactory.js` (Umgebungsvariablen `ROBOT_KINEMATICS` / `ROBOT_KINEMATICS_PARAMS`). Siehe `doc/ToDo_12_InverseKinematikConfig_ROADMAP.md`.
|
- `robot/kinematics/Arm3SegmentLinearX.js` ist die konkrete Kinematik (Inverse + Vorwärts) für den aktuellen Arm. Die Auswahl der Kinematik erfolgt über `robot/KinematicsFactory.js` (Umgebungsvariablen `ROBOT_KINEMATICS` / `ROBOT_KINEMATICS_PARAMS`). Siehe `doc/ToDo_12_InverseKinematikConfig_ROADMAP.md`.
|
||||||
- `robot/RobotConfig.js` liest `data/robot/robot.json` beim Start synchron und gibt einen typisierten Konfigurations-Record zurück (Kinematik-Parameter, Bewegungs-Defaults, Controller-Endpunkte). Env-Variablen überschreiben die JSON-Werte.
|
- `robot/RobotConfig.js` liest `data/robot/robot.json` beim Start synchron und gibt einen typisierten Konfigurations-Record zurück (Kinematik-Parameter, Bewegungs-Defaults, Controller-Endpunkte). Env-Variablen überschreiben die JSON-Werte.
|
||||||
@@ -30,10 +31,11 @@ Die Eingaben kommen per WebSocket an den HTTPS-Server und werden in `server/Inpu
|
|||||||
- `G92` (wird intern als `M92` verarbeitet — setzt Motorposition ohne Bewegung)
|
- `G92` (wird intern als `M92` verarbeitet — setzt Motorposition ohne Bewegung)
|
||||||
- Messungen in `X`, `Y`, `Z`, `A`, `B`, `C`, `E`, `F`
|
- Messungen in `X`, `Y`, `Z`, `A`, `B`, `C`, `E`, `F`
|
||||||
- `M1` für direkte Motor-Koordinaten
|
- `M1` für direkte Motor-Koordinaten
|
||||||
- Datei-Kommandos:
|
- FCodes (Datei-/Programm-Befehle) — werden durch den Driver an `appRobotFileservice` weitergeleitet:
|
||||||
- `FPoint`, `FPlus`, `FMinus`, `FShow`, `FList`, `FLoad <file>`, `FSave <file>`, `FClear`
|
- `FPoint`, `FPlus`, `FMinus`, `FFirst`, `FLast`, `FGoto <n>`
|
||||||
- `FFirst`, `FLast` — erkannt, aber noch nicht implementiert
|
- `FShow [id]`, `FList`, `FLoad <id>`, `FSave <name>`, `FClear`
|
||||||
- `M20`, `M23`, `M28`, `M29`
|
- `FPlay`, `FStop`
|
||||||
|
- Vollständige API: `doc/fileserviceAPI.md`
|
||||||
|
|
||||||
### G-Code-Verarbeitung
|
### G-Code-Verarbeitung
|
||||||
|
|
||||||
@@ -48,7 +50,7 @@ Die Eingaben kommen per WebSocket an den HTTPS-Server und werden in `server/Inpu
|
|||||||
|
|
||||||
- WebSocket-Broadcasts an alle verbundenen Clients
|
- WebSocket-Broadcasts an alle verbundenen Clients
|
||||||
- Nachdem ein G-Code-Befehl verarbeitet wurde, sendet das System `GCode.getM114(robot)` zurück.
|
- Nachdem ein G-Code-Befehl verarbeitet wurde, sendet das System `GCode.getM114(robot)` zurück.
|
||||||
- Für Datei-Kommandos gibt `GCode.receiveFC()` ebenfalls die aktuelle Position zurück.
|
- Für FCodes leitet der Driver das Ergebnis von `appRobotFileservice` weiter (Stepping-Befehle zusätzlich als Pose-Broadcast nach lokaler Ausführung).
|
||||||
- Telnet-Ausgabe an GRBL/FluidNC-Geräte
|
- Telnet-Ausgabe an GRBL/FluidNC-Geräte
|
||||||
- `TelnetSenderGRBL.execCommand()` erzeugt `G1`/`G90`-Befehle mit Achsenzuordnung und Feedrate.
|
- `TelnetSenderGRBL.execCommand()` erzeugt `G1`/`G90`-Befehle mit Achsenzuordnung und Feedrate.
|
||||||
- Info-Server API
|
- Info-Server API
|
||||||
@@ -103,6 +105,10 @@ Die Eingaben kommen per WebSocket an den HTTPS-Server und werden in `server/Inpu
|
|||||||
- Statischer Bearer-Token für `PUT /api/robot`. Fehlt die Variable, generiert
|
- Statischer Bearer-Token für `PUT /api/robot`. Fehlt die Variable, generiert
|
||||||
`RobotConfigService` beim ersten Start einen zufälligen Key und speichert ihn in
|
`RobotConfigService` beim ersten Start einen zufälligen Key und speichert ihn in
|
||||||
`data/robot/.apikey` (nicht im Repo). Der Key wird beim Start einmalig geloggt.
|
`data/robot/.apikey` (nicht im Repo). Der Key wird beim Start einmalig geloggt.
|
||||||
|
- `FILESERVICE_URL`
|
||||||
|
- Standard: `http://appRobot_Fileservice:2100`
|
||||||
|
- URL der `appRobotFileservice` — wird von `robot/FCodeClient.js` verwendet.
|
||||||
|
Im Container-Netz entspricht das dem Docker-Dienstnamen aus dem Portainer-Stack.
|
||||||
- `SHELLY_URL`
|
- `SHELLY_URL`
|
||||||
- URL für den Shelly Smart Plug Emergency-Stop: `http://<IP>/rpc/Switch.Set?id=0&on=false`
|
- URL für den Shelly Smart Plug Emergency-Stop: `http://<IP>/rpc/Switch.Set?id=0&on=false`
|
||||||
- Überschreibt `controllers.emergencyStop.url` aus `robot.json` (analog zu `GRBL_BASE_IP`).
|
- Überschreibt `controllers.emergencyStop.url` aus `robot.json` (analog zu `GRBL_BASE_IP`).
|
||||||
@@ -208,11 +214,12 @@ Socket geschlossen und der bestehende Reconnect-Mechanismus startet automatisch.
|
|||||||
- `data/robot/robot.json` — zentrale Roboter-Konfiguration (Single Source of Truth)
|
- `data/robot/robot.json` — zentrale Roboter-Konfiguration (Single Source of Truth)
|
||||||
- `robot/GCodeParser.js` — wandelt rohe Nachrichten in strukturierte Befehlsobjekte
|
- `robot/GCodeParser.js` — wandelt rohe Nachrichten in strukturierte Befehlsobjekte
|
||||||
- `robot/RobotController.js` — wendet geparste Befehle auf das Modell an (Steuerlogik)
|
- `robot/RobotController.js` — wendet geparste Befehle auf das Modell an (Steuerlogik)
|
||||||
- `robot/GCode.js` — Fassade + Datei-Befehle
|
- `robot/GCode.js` — Fassade für G-Code-Verarbeitung (Bewegung, Pose, Logging)
|
||||||
|
- `robot/FCodeClient.js` — Gateway: übersetzt FCodes in REST-Aufrufe an `appRobotFileservice`
|
||||||
- `robot/TelnetSenderGRBL.js`
|
- `robot/TelnetSenderGRBL.js`
|
||||||
- `robot/ShellyEmergencyStop.js` — steuert Shelly Smart Plug als Emergency-Stop-Aktor (HTTP GET, kein GCode)
|
- `robot/ShellyEmergencyStop.js` — steuert Shelly Smart Plug als Emergency-Stop-Aktor (HTTP GET, kein GCode)
|
||||||
- `robot/fluidnc/FluidNCClient.js` — alternative WebSocket-basierte FluidNC-Anbindung mit Reconnect-Logik (noch nicht integriert)
|
- `robot/fluidnc/FluidNCClient.js` — alternative WebSocket-basierte FluidNC-Anbindung mit Reconnect-Logik (noch nicht integriert)
|
||||||
- `GCodeFiles/` — enthalten Beispiel- und Log-G-Code-Dateien
|
- `GCodeFiles/` — G-Code-Programme werden jetzt in `appRobotFileservice` verwaltet
|
||||||
|
|
||||||
## Laufzeitvoraussetzungen
|
## Laufzeitvoraussetzungen
|
||||||
|
|
||||||
@@ -235,8 +242,7 @@ Architektur- und Refactoring-Aufgaben sind in `doc/ToDo_*.md` dokumentiert:
|
|||||||
| `doc/ToDo_6_RobotController.md` | RobotController-Klasse einführen | ✅ 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_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 | ✅ ausgelagert → `appRobotFileservice` |
|
| `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/fileserviceAPI.md` | REST-API der `appRobotFileservice` (Programme, aktiver Cursor, Teaching/Playback) | ✅ implementiert |
|
||||||
| `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_7_Tests.md` | Testabdeckung und Stabilität | teilweise |
|
||||||
| `doc/ToDo_8_Bugs.md` | Bekannte konkrete Bugs | 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) |
|
| `doc/ToDo_9_HardwareFeedback.md` | Hardware-Feedback-Loop (GRBL-Antworten, Command-Queue, Positionsabgleich) | teilweise (Baustein Port→Motor ✅, Pakete 1–6 offen) |
|
||||||
@@ -266,7 +272,7 @@ ToDo_7 Tests — begleitend zu allen obigen
|
|||||||
Kurzübersicht weiterer offener Punkte:
|
Kurzübersicht weiterer offener Punkte:
|
||||||
|
|
||||||
- [ ] Dokumentation der vollständigen G-Code-Syntax erweitern
|
- [ ] Dokumentation der vollständigen G-Code-Syntax erweitern
|
||||||
- [x] `FFirst`/`FLast` (und übriges File-Handling) → ausgelagert in `appRobotFileservice` (siehe `doc/draft_filehandeling.md`)
|
- [x] `FFirst`/`FLast` und gesamtes File-Handling → ausgelagert in `appRobotFileservice` (siehe `doc/fileserviceAPI.md`)
|
||||||
- [ ] `ROBOT_USE_SPEED_CALC` und `motorSpeeds` im echten Betrieb prüfen
|
- [ ] `ROBOT_USE_SPEED_CALC` und `motorSpeeds` im echten Betrieb prüfen
|
||||||
- [ ] `FluidNCClient.js` evaluieren: als Ersatz oder Ergänzung zu `TelnetSenderGRBL`?
|
- [ ] `FluidNCClient.js` evaluieren: als Ersatz oder Ergänzung zu `TelnetSenderGRBL`?
|
||||||
- [x] HTTPS-Passphrase aus Env-Variable (`HTTPS_PASSPHRASE`) — erledigt
|
- [x] HTTPS-Passphrase aus Env-Variable (`HTTPS_PASSPHRASE`) — erledigt
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ services:
|
|||||||
- ROBOT_KINEMATICS=arm3segmentlinearx
|
- ROBOT_KINEMATICS=arm3segmentlinearx
|
||||||
- ROBOT_GRBL_AUTOREPORT=true
|
- ROBOT_GRBL_AUTOREPORT=true
|
||||||
- ROBOT_GRBL_REPORT_INTERVAL=200
|
- ROBOT_GRBL_REPORT_INTERVAL=200
|
||||||
|
- FILESERVICE_URL=http://appRobot_Fileservice:2100
|
||||||
ports:
|
ports:
|
||||||
- "2098:2098"
|
- "2098:2098"
|
||||||
- "2081:2081"
|
- "2081:2081"
|
||||||
@@ -54,7 +55,23 @@ services:
|
|||||||
- "1003:1003"
|
- "1003:1003"
|
||||||
networks:
|
networks:
|
||||||
- default
|
- default
|
||||||
|
|
||||||
|
appRobotFileservice:
|
||||||
|
container_name: appRobot_Fileservice
|
||||||
|
image: node:24-alpine
|
||||||
|
working_dir: /usr/src/app
|
||||||
|
volumes:
|
||||||
|
- /home/chk/Documents/appRobotFileservice:/usr/src/app
|
||||||
|
command: sh -c "npm ci || npm install && node --inspect=0.0.0.0:2101 index.js"
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=development
|
||||||
|
- FILE_SERVICE_PORT=2100
|
||||||
|
ports:
|
||||||
|
- "2100:2100" # REST-API
|
||||||
|
- "2101:2101" # node --inspect (Debug)
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
|
||||||
appRobotControl:
|
appRobotControl:
|
||||||
image: node:24-alpine
|
image: node:24-alpine
|
||||||
|
|||||||
10
doc/API.md
10
doc/API.md
@@ -22,16 +22,14 @@ Anfrager) und **Broadcasts** (an alle verbundenen Clients).
|
|||||||
| `Ping` | Heartbeat, wird geloggt | `Ping` | **nur Anfrager** (gezielt) |
|
| `Ping` | Heartbeat, wird geloggt | `Ping` | **nur Anfrager** (gezielt) |
|
||||||
| `M114` | Statusabfrage | Positions-JSON (siehe unten) | **nur Anfrager** (gezielt) |
|
| `M114` | Statusabfrage | Positions-JSON (siehe unten) | **nur Anfrager** (gezielt) |
|
||||||
| G-Code (`G1`, `G90`, `G91`, `G28`, `M1`, `M92`, …) | Bewegung/Zustandsänderung | aktuelles Positions-JSON | **alle Clients** (Broadcast) |
|
| G-Code (`G1`, `G90`, `G91`, `G28`, `M1`, `M92`, …) | Bewegung/Zustandsänderung | aktuelles Positions-JSON | **alle Clients** (Broadcast) |
|
||||||
| Datei-Befehle (`FShow`, `FList`, `FPoint`, `FPlus`, `FMinus`, `FLoad`, `FSave`, `FClear`, `M20/23/28/29`) | Datei-/Log-Verwaltung | Befehlsergebnis | **alle Clients** (Broadcast) |
|
| FCodes (`FShow`, `FList`, `FPoint`, `FPlus`, `FMinus`, `FFirst`, `FLast`, `FGoto`, `FLoad`, `FSave`, `FClear`, `FPlay`, `FStop`) | Weiterleitung → `appRobotFileservice` via `robot/FCodeClient.js`; Stepping-Ergebnis (Radian-Zeile) wird lokal ausgeführt | Daten-JSON oder Positions-JSON | **alle Clients** (Broadcast) |
|
||||||
| alles andere | – | Fehler-Envelope | **nur Anfrager** (gezielt) |
|
| alles andere | – | Fehler-Envelope | **nur Anfrager** (gezielt) |
|
||||||
|
|
||||||
**Begründung der Trennung:** Eine Bewegung ändert die Roboterposition — das ist ein
|
**Begründung der Trennung:** Eine Bewegung ändert die Roboterposition — das ist ein
|
||||||
Status-Update, das jeder Client (z. B. die Simulation) sehen soll → Broadcast. Eine
|
Status-Update, das jeder Client (z. B. die Simulation) sehen soll → Broadcast. Eine
|
||||||
reine Abfrage (`Ping`, `M114`) ist eine direkte Antwort an den Anfrager → gezielt.
|
reine Abfrage (`Ping`, `M114`) ist eine direkte Antwort an den Anfrager → gezielt.
|
||||||
|
FCodes (Datei-Befehle) werden durch den Driver als Gateway weitergereicht —
|
||||||
> **Hinweis:** Feinere Zielsteuerung der Datei-Befehle (z. B. `FShow` als
|
Steuerungen brauchen keine direkte Verbindung zur `appRobotFileservice`.
|
||||||
> Anfrager-only-Antwort) sowie `FFirst`/`FLast` gehören zur Datei-Verwaltung in
|
|
||||||
> **ToDo 4** und bleiben hier bewusst unverändert.
|
|
||||||
|
|
||||||
### Positions-JSON (`M114` / Broadcast nach Bewegung)
|
### Positions-JSON (`M114` / Broadcast nach Bewegung)
|
||||||
|
|
||||||
@@ -57,7 +55,7 @@ Bei unbekannter Eingabe oder Verarbeitungsfehler erhält **nur der Anfrager**:
|
|||||||
|--------|-----------|
|
|--------|-----------|
|
||||||
| `UNKNOWN_COMMAND` | Eingabe passt auf keinen bekannten Befehl |
|
| `UNKNOWN_COMMAND` | Eingabe passt auf keinen bekannten Befehl |
|
||||||
| `GCODE_ERROR` | Fehler beim Parsen/Ausführen eines G-Code-Befehls |
|
| `GCODE_ERROR` | Fehler beim Parsen/Ausführen eines G-Code-Befehls |
|
||||||
| `FILE_ERROR` | Fehler bei einem Datei-Befehl |
|
| `FILE_ERROR` | Fehler bei einem FCode-Befehl (von `appRobotFileservice` weitergereicht) |
|
||||||
|
|
||||||
Erfolgs-Antworten (`Ping`, Positions-JSON) bleiben aus Kompatibilitätsgründen im
|
Erfolgs-Antworten (`Ping`, Positions-JSON) bleiben aus Kompatibilitätsgründen im
|
||||||
bisherigen Rohformat; das Envelope gilt nur für Fehler.
|
bisherigen Rohformat; das Envelope gilt nur für Fehler.
|
||||||
|
|||||||
@@ -4,8 +4,7 @@
|
|||||||
> (`GCodeFileManager`) gebaut, sondern in das eigenständige Projekt
|
> (`GCodeFileManager`) gebaut, sondern in das eigenständige Projekt
|
||||||
> **`appRobotFileservice`** ausgelagert und über FCodes durch den Driver
|
> **`appRobotFileservice`** ausgelagert und über FCodes durch den Driver
|
||||||
> weitergereicht. Im Driver bleibt nur ein dünner Proxy. Konzept & Schnittstelle:
|
> weitergereicht. Im Driver bleibt nur ein dünner Proxy. Konzept & Schnittstelle:
|
||||||
> [`draft_filehandeling.md`](draft_filehandeling.md) ·
|
> [`fileserviceAPI.md`](fileserviceAPI.md).
|
||||||
> [`draft_filehandeling_API.md`](draft_filehandeling_API.md).
|
|
||||||
> Die folgenden Punkte sind als Vorlage für die Umsetzung *dort* zu lesen.
|
> Die folgenden Punkte sind als Vorlage für die Umsetzung *dort* zu lesen.
|
||||||
|
|
||||||
## Ziel der Verbesserung
|
## Ziel der Verbesserung
|
||||||
|
|||||||
@@ -5,8 +5,7 @@
|
|||||||
> Die hier beschriebenen Detailprobleme werden **dort** gelöst: Cursor als In-Memory-
|
> Die hier beschriebenen Detailprobleme werden **dort** gelöst: Cursor als In-Memory-
|
||||||
> Index (Paket 2), explizite Grad↔Radian-Umrechnung im Fileservice (Paket 3),
|
> Index (Paket 2), explizite Grad↔Radian-Umrechnung im Fileservice (Paket 3),
|
||||||
> Fehler-Envelope (Paket 4), asynchrones IO (Paket 5). Konzept & Schnittstelle:
|
> Fehler-Envelope (Paket 4), asynchrones IO (Paket 5). Konzept & Schnittstelle:
|
||||||
> [`draft_filehandeling.md`](draft_filehandeling.md) ·
|
> [`fileserviceAPI.md`](fileserviceAPI.md).
|
||||||
> [`draft_filehandeling_API.md`](draft_filehandeling_API.md).
|
|
||||||
> Die folgende Analyse bleibt als Umsetzungs-Vorlage für *jenes* Projekt erhalten.
|
> Die folgende Analyse bleibt als Umsetzungs-Vorlage für *jenes* Projekt erhalten.
|
||||||
|
|
||||||
## Ist-Zustand
|
## Ist-Zustand
|
||||||
|
|||||||
@@ -1,292 +0,0 @@
|
|||||||
# 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)
|
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
# 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
|
|
||||||
211
doc/fileserviceAPI.md
Normal file
211
doc/fileserviceAPI.md
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
# `appRobotFileservice` — REST API
|
||||||
|
|
||||||
|
> **Einziger Consumer ist der Driver** (`appRobotDriver`). Steuerungen sprechen die
|
||||||
|
> appRobotFileservice nie direkt an, sondern schicken **FCodes** an den Driver, der
|
||||||
|
> sie hierher weiterreicht (`robot/FCodeClient.js`). 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.
|
||||||
|
- **Basis-URL:** `http://appRobot_Fileservice:2100/api` (Container-intern) · `http://thinkcentre.local:2100/api` (lokal)
|
||||||
|
- **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.
|
||||||
|
- **Auth:** `Bearer <FILE_API_KEY>` für schreibende Operationen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 im Kommentarfeld (`;`).
|
||||||
|
|
||||||
|
### Pose (vom Driver beim `FPoint` mitgeschickt)
|
||||||
|
```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 (`robot/FCodeClient.js`) übersetzt die FCodes der Steuerungen in diese Endpoints:
|
||||||
|
|
||||||
|
| FCode | Endpoint | Antwort an Steuerung (über Driver) |
|
||||||
|
|---|---|---|
|
||||||
|
| `FList` | `GET /programs` | Liste (Broadcast) |
|
||||||
|
| `FShow [id]` | `GET /programs/{id}` | Inhalt in **Grad** (Broadcast) |
|
||||||
|
| `FLoad <id>` | `PUT /active` | ActiveState (Broadcast) |
|
||||||
|
| `FSave <name>` | `POST /programs` | id (Broadcast) |
|
||||||
|
| `FClear` | `POST /active/clear` | ActiveState (Broadcast) |
|
||||||
|
| `FPoint` | `POST /active/points` | Bestätigung (Broadcast) |
|
||||||
|
| `FPlus` | `POST /active/next` | Bewegung → Driver führt aus → **Pose-Broadcast** |
|
||||||
|
| `FMinus` | `POST /active/prev` | Bewegung → Driver führt aus → **Pose-Broadcast** |
|
||||||
|
| `FFirst` | `POST /active/first` | Bewegung → Driver führt aus → **Pose-Broadcast** |
|
||||||
|
| `FLast` | `POST /active/last` | Bewegung → Driver führt aus → **Pose-Broadcast** |
|
||||||
|
| `FGoto <n>` | `POST /active/goto` | Bewegung → Driver führt aus → **Pose-Broadcast** |
|
||||||
|
| `FPlay` / `FStop` | `POST /active/play` / `/stop` | Status (Broadcast) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Endpoints — Programm-Verwaltung
|
||||||
|
|
||||||
|
### `GET /api/programs` ← `FList`
|
||||||
|
```json
|
||||||
|
{ "programs": [ { "id": "log", "name": "log", "lineCount": 36 }, … ] }
|
||||||
|
```
|
||||||
|
|
||||||
|
### `GET /api/programs/{id}` ← `FShow`
|
||||||
|
Inhalt + Metadaten — 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!" ] }
|
||||||
|
```
|
||||||
|
> `;<epoch>` = Aufnahme-Zeitstempel; abschließendes `!` = Cursor-Zeile.
|
||||||
|
|
||||||
|
### `POST /api/programs` ← `FSave`
|
||||||
|
```jsonc
|
||||||
|
{ "name": "Demo C", "fromActive": true } // aus aktivem Puffer
|
||||||
|
// oder expliziter Inhalt (in Grad):
|
||||||
|
{ "name": "Demo C", "lines": ["G90 G1 x0 y300 … a90.00 …"], "angleUnit": "deg" }
|
||||||
|
```
|
||||||
|
→ `201 { "id": "demo_c", "lineCount": 12 }`
|
||||||
|
|
||||||
|
### `PUT /api/programs/{id}` · `DELETE /api/programs/{id}`
|
||||||
|
Inhalt ersetzen / umbenennen · löschen (jeweils `.gcode` **und** `.json`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Endpoints — Aktives Programm & Cursor
|
||||||
|
|
||||||
|
### `GET /api/active`
|
||||||
|
Aktuellen `ActiveState` lesen (inkl. `currentLine` in Radian).
|
||||||
|
|
||||||
|
### `PUT /api/active` ← `FLoad`
|
||||||
|
```json
|
||||||
|
{ "id": "besteck_spuelmaschine" }
|
||||||
|
```
|
||||||
|
→ `ActiveState`. Nicht-existierendes Programm wird **leer angelegt** (für Teaching).
|
||||||
|
|
||||||
|
### `POST /api/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 /api/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`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Endpoints — Teaching / Aufnahme
|
||||||
|
|
||||||
|
### `POST /api/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 /api/active/lines`
|
||||||
|
Rohe Zeile(n) anhängen/einfügen (z. B. Pause `G4`):
|
||||||
|
```json
|
||||||
|
{ "line": "G4 P0.5", "atIndex": 8 }
|
||||||
|
```
|
||||||
|
|
||||||
|
### `PUT /api/active/lines/{index}` · `DELETE /api/active/lines/{index}`
|
||||||
|
Einzelne Zeile ersetzen / löschen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Endpoints — Playback
|
||||||
|
|
||||||
|
### `POST /api/active/play` ← `FPlay`
|
||||||
|
```jsonc
|
||||||
|
{ "mode": "run", "fromStart": false }
|
||||||
|
```
|
||||||
|
|
||||||
|
### `POST /api/active/stop` ← `FStop`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Fehler-Envelope
|
||||||
|
|
||||||
|
Konsistent mit dem Driver (`doc/ToDo_5_API.md`): `{ type, code, message, input }`.
|
||||||
|
Der Driver reicht Fileservice-Fehler (`FILE_ERROR`) 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" }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Beispiel-Flows
|
||||||
|
|
||||||
|
### Teaching-Session (Joystick → Aufnahme)
|
||||||
|
```
|
||||||
|
Steuerung → Driver: FLoad demo_c → Driver: PUT /api/active {id:"demo_c"}
|
||||||
|
Steuerung → Driver: G1 … (Arm bewegen, lokal — Fileservice unbeteiligt)
|
||||||
|
Steuerung → Driver: FPoint → Driver hängt Live-Pose an,
|
||||||
|
POST /api/active/points { pose, feedrate }
|
||||||
|
… weitere Punkte …
|
||||||
|
Steuerung → Driver: FSave "Demo C" → Driver: POST /api/programs {name,fromActive:true}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Playback-Session (schrittweise)
|
||||||
|
```
|
||||||
|
Steuerung → Driver: FList → GET /api/programs
|
||||||
|
Steuerung → Driver: FLoad demo_c → PUT /api/active
|
||||||
|
Steuerung → Driver: FFirst → POST /api/active/first → {line (Radian)}
|
||||||
|
Driver: receiveGCode(line) → Bewegung
|
||||||
|
Driver: Pose-Broadcast an alle WS-Clients
|
||||||
|
Steuerung → Driver: FPlus … / FPlay
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Verweise
|
||||||
|
|
||||||
|
- [`API.md`](API.md) — Driver-Endpunkte (`/api/position`, WS `:2095`)
|
||||||
|
- [`robot/FCodeClient.js`](../robot/FCodeClient.js) — Gateway-Implementierung im Driver
|
||||||
|
- [`ToDo_6b_FileHandling.md`](ToDo_6b_FileHandling.md) — gelöste Detailprobleme
|
||||||
|
- `appRobotFileservice/README.md` — Konzept, Einheiten, Dateiformat, Konfiguration
|
||||||
@@ -10565,3 +10565,172 @@
|
|||||||
2026-06-14T07:32:17.813Z ::ffff:127.0.0.1: M114
|
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.848Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
2026-06-14T07:32:17.959Z ::ffff:127.0.0.1: G1 X1
|
2026-06-14T07:32:17.959Z ::ffff:127.0.0.1: G1 X1
|
||||||
|
2026-06-14T08:39:38.186Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T08:39:38.208Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T08:39:38.645Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T08:39:38.872Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T08:39:39.103Z ::ffff:127.0.0.1: G1 X1
|
||||||
|
2026-06-14T08:40:02.998Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T08:40:03.013Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T08:40:03.182Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T08:40:03.395Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T08:40:10.444Z ::ffff:127.0.0.1: G1 X1
|
||||||
|
2026-06-14T08:40:22.092Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T08:40:22.108Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T08:40:22.273Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T08:40:22.501Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T08:40:29.545Z ::ffff:127.0.0.1: G1 X1
|
||||||
|
2026-06-14T08:40:42.014Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T08:40:42.024Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T08:40:42.186Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T08:40:42.410Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T08:40:42.625Z ::ffff:127.0.0.1: G1 X1
|
||||||
|
2026-06-14T08:40:49.232Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T08:40:49.245Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T08:40:49.418Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T08:40:49.639Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T08:40:49.867Z ::ffff:127.0.0.1: G1 X1
|
||||||
|
2026-06-14T08:41:14.185Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T08:41:14.201Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T08:41:14.420Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T08:41:14.634Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T08:41:14.864Z ::ffff:127.0.0.1: G1 X1
|
||||||
|
2026-06-14T08:41:30.075Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T08:41:30.090Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T08:41:30.294Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T08:41:30.360Z ::ffff:127.0.0.1: FList
|
||||||
|
2026-06-14T08:41:30.381Z ::ffff:127.0.0.1: FPlus
|
||||||
|
2026-06-14T08:41:30.389Z ::ffff:127.0.0.1: FLoad nichtda
|
||||||
|
2026-06-14T08:41:30.516Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T08:41:30.747Z ::ffff:127.0.0.1: G1 X1
|
||||||
|
2026-06-14T08:41:43.088Z ::ffff:127.0.0.1: FList
|
||||||
|
2026-06-14T08:41:43.097Z ::ffff:127.0.0.1: FPlus
|
||||||
|
2026-06-14T08:41:43.102Z ::ffff:127.0.0.1: FLoad nichtda
|
||||||
|
2026-06-14T08:41:52.569Z ::ffff:127.0.0.1: FList
|
||||||
|
2026-06-14T08:41:52.619Z ::ffff:127.0.0.1: FPlus
|
||||||
|
2026-06-14T08:41:52.640Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T08:41:52.660Z ::ffff:127.0.0.1: FLoad nichtda
|
||||||
|
2026-06-14T08:41:52.709Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T08:41:52.868Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T08:41:53.159Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T08:41:53.460Z ::ffff:127.0.0.1: G1 X1
|
||||||
|
2026-06-14T08:41:56.359Z ::ffff:127.0.0.1: FList
|
||||||
|
2026-06-14T08:41:56.409Z ::ffff:127.0.0.1: FPlus
|
||||||
|
2026-06-14T08:41:56.422Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T08:41:56.430Z ::ffff:127.0.0.1: FLoad nichtda
|
||||||
|
2026-06-14T08:41:56.442Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T08:41:56.572Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T08:41:56.801Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T08:41:57.043Z ::ffff:127.0.0.1: G1 X1
|
||||||
|
2026-06-14T08:42:30.513Z ::ffff:127.0.0.1: FList
|
||||||
|
2026-06-14T08:42:30.565Z ::ffff:127.0.0.1: FPlus
|
||||||
|
2026-06-14T08:42:30.597Z ::ffff:127.0.0.1: FLoad nichtda
|
||||||
|
2026-06-14T08:42:30.837Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T08:42:30.857Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T08:42:31.049Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T08:42:31.273Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T08:42:31.496Z ::ffff:127.0.0.1: G1 X1
|
||||||
|
2026-06-14T08:42:37.269Z ::ffff:127.0.0.1: FList
|
||||||
|
2026-06-14T08:42:37.281Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T08:42:37.312Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T08:42:37.324Z ::ffff:127.0.0.1: FPlus
|
||||||
|
2026-06-14T08:42:37.347Z ::ffff:127.0.0.1: FLoad nichtda
|
||||||
|
2026-06-14T08:42:38.180Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T08:42:38.413Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T08:42:38.648Z ::ffff:127.0.0.1: G1 X1
|
||||||
|
2026-06-14T08:42:40.892Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T08:42:40.912Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T08:42:40.945Z ::ffff:127.0.0.1: FList
|
||||||
|
2026-06-14T08:42:40.982Z ::ffff:127.0.0.1: FPlus
|
||||||
|
2026-06-14T08:42:40.997Z ::ffff:127.0.0.1: FLoad nichtda
|
||||||
|
2026-06-14T08:42:41.093Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T08:42:41.319Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T08:42:41.548Z ::ffff:127.0.0.1: G1 X1
|
||||||
|
2026-06-14T09:13:04.091Z ::ffff:127.0.0.1: FList
|
||||||
|
2026-06-14T09:13:04.245Z ::ffff:127.0.0.1: FPlus
|
||||||
|
2026-06-14T09:13:04.321Z ::ffff:127.0.0.1: FLoad nichtda
|
||||||
|
2026-06-14T09:13:04.389Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T09:13:04.401Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T09:13:04.407Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T09:13:04.619Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T09:13:04.855Z ::ffff:127.0.0.1: G1 X1
|
||||||
|
2026-06-14T09:13:06.862Z ::ffff:127.0.0.1: FList
|
||||||
|
2026-06-14T09:13:06.872Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T09:13:06.891Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T09:13:06.902Z ::ffff:127.0.0.1: FPlus
|
||||||
|
2026-06-14T09:13:06.923Z ::ffff:127.0.0.1: FLoad nichtda
|
||||||
|
2026-06-14T09:13:07.174Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T09:13:07.402Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T09:13:07.624Z ::ffff:127.0.0.1: G1 X1
|
||||||
|
2026-06-14T09:13:28.005Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T09:13:28.240Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T09:13:28.517Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T09:13:28.515Z ::ffff:127.0.0.1: FList
|
||||||
|
2026-06-14T09:13:28.542Z ::ffff:127.0.0.1: G1 X1
|
||||||
|
2026-06-14T09:13:28.559Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T09:13:28.660Z ::ffff:127.0.0.1: FPlus
|
||||||
|
2026-06-14T09:13:28.696Z ::ffff:127.0.0.1: FLoad nichtda
|
||||||
|
2026-06-14T09:13:31.390Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T09:13:31.420Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T09:13:31.766Z ::ffff:127.0.0.1: FList
|
||||||
|
2026-06-14T09:13:31.806Z ::ffff:127.0.0.1: FPlus
|
||||||
|
2026-06-14T09:13:31.819Z ::ffff:127.0.0.1: FLoad nichtda
|
||||||
|
2026-06-14T09:13:32.017Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T09:13:32.235Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T09:13:32.470Z ::ffff:127.0.0.1: G1 X1
|
||||||
|
2026-06-14T09:13:37.253Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T09:13:37.351Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T09:13:37.361Z ::ffff:127.0.0.1: FList
|
||||||
|
2026-06-14T09:13:37.564Z ::ffff:127.0.0.1: FPlus
|
||||||
|
2026-06-14T09:13:37.669Z ::ffff:127.0.0.1: FLoad nichtda
|
||||||
|
2026-06-14T09:13:37.927Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T09:13:38.155Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T09:13:38.393Z ::ffff:127.0.0.1: G1 X1
|
||||||
|
2026-06-14T09:13:40.695Z ::ffff:127.0.0.1: FList
|
||||||
|
2026-06-14T09:13:40.737Z ::ffff:127.0.0.1: FPlus
|
||||||
|
2026-06-14T09:13:40.753Z ::ffff:127.0.0.1: FLoad nichtda
|
||||||
|
2026-06-14T09:13:40.778Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T09:13:40.801Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T09:13:40.961Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T09:13:41.187Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T09:13:41.421Z ::ffff:127.0.0.1: G1 X1
|
||||||
|
2026-06-14T09:13:46.992Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T09:13:47.119Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T09:13:47.243Z ::ffff:127.0.0.1: FList
|
||||||
|
2026-06-14T09:13:47.403Z ::ffff:127.0.0.1: FPlus
|
||||||
|
2026-06-14T09:13:47.446Z ::ffff:127.0.0.1: FLoad nichtda
|
||||||
|
2026-06-14T09:13:47.465Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T09:13:47.721Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T09:13:47.970Z ::ffff:127.0.0.1: G1 X1
|
||||||
|
2026-06-14T09:13:50.452Z ::ffff:127.0.0.1: FList
|
||||||
|
2026-06-14T09:13:50.473Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T09:13:50.492Z ::ffff:127.0.0.1: FPlus
|
||||||
|
2026-06-14T09:13:50.495Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T09:13:50.505Z ::ffff:127.0.0.1: FLoad nichtda
|
||||||
|
2026-06-14T09:13:50.695Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T09:13:50.929Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T09:13:51.152Z ::ffff:127.0.0.1: G1 X1
|
||||||
|
2026-06-14T09:42:55.235Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T09:42:55.253Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T09:42:55.275Z ::ffff:127.0.0.1: FList
|
||||||
|
2026-06-14T09:42:55.310Z ::ffff:127.0.0.1: FPlus
|
||||||
|
2026-06-14T09:42:55.337Z ::ffff:127.0.0.1: FLoad nichtda
|
||||||
|
2026-06-14T09:42:55.774Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T09:42:56.002Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T09:42:56.225Z ::ffff:127.0.0.1: G1 X1
|
||||||
|
2026-06-14T09:43:02.800Z ::ffff:127.0.0.1: FList
|
||||||
|
2026-06-14T09:43:02.842Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T09:43:02.857Z ::ffff:127.0.0.1: FPlus
|
||||||
|
2026-06-14T09:43:02.875Z ::ffff:127.0.0.1: FLoad nichtda
|
||||||
|
2026-06-14T09:43:02.892Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T09:43:03.165Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T09:43:03.433Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T09:43:03.682Z ::ffff:127.0.0.1: G1 X1
|
||||||
|
2026-06-14T09:43:06.562Z ::ffff:127.0.0.1: FList
|
||||||
|
2026-06-14T09:43:06.602Z ::ffff:127.0.0.1: FPlus
|
||||||
|
2026-06-14T09:43:06.615Z ::ffff:127.0.0.1: FLoad nichtda
|
||||||
|
2026-06-14T09:43:06.746Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T09:43:06.974Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T09:43:07.066Z ::ffff:127.0.0.1: M114
|
||||||
|
2026-06-14T09:43:07.080Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||||
|
2026-06-14T09:43:07.211Z ::ffff:127.0.0.1: G1 X1
|
||||||
|
|||||||
@@ -14700,3 +14700,49 @@
|
|||||||
2026-06-14T07:30:37.987Z ::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.191Z ::ffff:127.0.0.1 : Ping
|
||||||
2026-06-14T07:32:17.745Z ::ffff:127.0.0.1 : Ping
|
2026-06-14T07:32:17.745Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T08:39:38.144Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T08:39:38.409Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T08:40:02.952Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T08:40:02.967Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T08:40:22.054Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T08:40:22.062Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T08:40:41.952Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T08:40:41.992Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T08:40:49.183Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T08:40:49.206Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T08:41:14.145Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T08:41:14.180Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T08:41:30.044Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T08:41:30.066Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T08:41:52.557Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T08:41:52.620Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T08:41:56.285Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T08:41:56.375Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T08:42:30.799Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T08:42:30.808Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T08:42:37.230Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T08:42:37.957Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T08:42:40.846Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T08:42:40.854Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T09:13:04.169Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T09:13:04.342Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T09:13:06.836Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T09:13:06.938Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T09:13:27.764Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T09:13:28.432Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T09:13:31.351Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T09:13:31.786Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T09:13:37.220Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T09:13:37.595Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T09:13:40.726Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T09:13:40.742Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T09:13:46.951Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T09:13:47.180Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T09:13:50.433Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T09:13:50.459Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T09:42:55.181Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T09:42:55.544Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T09:43:02.806Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T09:43:02.856Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T09:43:06.488Z ::ffff:127.0.0.1 : Ping
|
||||||
|
2026-06-14T09:43:07.035Z ::ffff:127.0.0.1 : Ping
|
||||||
|
|||||||
93
robot/FCodeClient.js
Normal file
93
robot/FCodeClient.js
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
// robot/FCodeClient.js
|
||||||
|
// Translates FCode messages (FPoint, FPlus, FList, …) into REST calls to appRobotFileservice.
|
||||||
|
// The Driver is the only gateway — controllers never contact the fileservice directly.
|
||||||
|
|
||||||
|
function _baseUrl() {
|
||||||
|
return process.env.FILESERVICE_URL || 'http://appRobot_Fileservice:2100';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true when the message is an FCode (F + uppercase letter + lowercase, at start). */
|
||||||
|
function isFCode(message) {
|
||||||
|
return /^F[A-Z][a-z]+/.test(String(message).trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch an FCode message to the appropriate fileservice endpoint.
|
||||||
|
* @param {object} robot Current robot state (x,y,z,phi,theta,psi,e,feedrate).
|
||||||
|
* @param {string} message Raw FCode string, e.g. "FPoint", "FPlus", "FLoad demo".
|
||||||
|
* @returns {Promise<{type: string, data?: string, line?: string}>}
|
||||||
|
* type 'step' → line (driver-native GCode, Radian) ready to execute
|
||||||
|
* type 'list'|'show'|'point'|'ok' → data (JSON string) to broadcast
|
||||||
|
*/
|
||||||
|
async function handle(robot, message) {
|
||||||
|
const msg = String(message).trim();
|
||||||
|
|
||||||
|
if (msg.startsWith('FList')) {
|
||||||
|
const data = await _req('GET', '/api/programs');
|
||||||
|
return { type: 'list', data: JSON.stringify(data) };
|
||||||
|
}
|
||||||
|
if (msg.startsWith('FShow')) {
|
||||||
|
const id = msg.slice('FShow'.length).trim();
|
||||||
|
const data = id
|
||||||
|
? await _req('GET', `/api/programs/${encodeURIComponent(id)}`)
|
||||||
|
: await _req('GET', '/api/active');
|
||||||
|
return { type: 'show', data: JSON.stringify(data) };
|
||||||
|
}
|
||||||
|
if (msg.startsWith('FLoad')) {
|
||||||
|
const id = msg.slice('FLoad'.length).trim();
|
||||||
|
const data = await _req('PUT', '/api/active', { id });
|
||||||
|
return { type: 'ok', data: JSON.stringify(data) };
|
||||||
|
}
|
||||||
|
if (msg.startsWith('FSave')) {
|
||||||
|
const name = msg.slice('FSave'.length).trim() || 'unnamed';
|
||||||
|
const data = await _req('POST', '/api/programs', { name, fromActive: true });
|
||||||
|
return { type: 'ok', data: JSON.stringify(data) };
|
||||||
|
}
|
||||||
|
if (msg.startsWith('FClear')) {
|
||||||
|
const data = await _req('POST', '/api/active/clear');
|
||||||
|
return { type: 'ok', data: JSON.stringify(data) };
|
||||||
|
}
|
||||||
|
if (msg.startsWith('FPoint')) {
|
||||||
|
// Driver attaches the current pose (Radian) — fileservice converts to degrees for storage.
|
||||||
|
const pose = {
|
||||||
|
x: robot.x, y: robot.y, z: robot.z,
|
||||||
|
a: robot.phi, b: robot.theta, c: robot.psi, e: robot.e,
|
||||||
|
};
|
||||||
|
const feedrate = robot.feedrate || 1000;
|
||||||
|
const data = await _req('POST', '/api/active/points', { pose, feedrate });
|
||||||
|
return { type: 'point', data: JSON.stringify(data) };
|
||||||
|
}
|
||||||
|
if (msg.startsWith('FPlus')) { const d = await _req('POST', '/api/active/next'); return { type: 'step', line: d.line }; }
|
||||||
|
if (msg.startsWith('FMinus')) { const d = await _req('POST', '/api/active/prev'); return { type: 'step', line: d.line }; }
|
||||||
|
if (msg.startsWith('FFirst')) { const d = await _req('POST', '/api/active/first'); return { type: 'step', line: d.line }; }
|
||||||
|
if (msg.startsWith('FLast')) { const d = await _req('POST', '/api/active/last'); return { type: 'step', line: d.line }; }
|
||||||
|
if (msg.startsWith('FGoto')) {
|
||||||
|
const index = parseInt(msg.slice('FGoto'.length).trim(), 10);
|
||||||
|
const d = await _req('POST', '/api/active/goto', { index });
|
||||||
|
return { type: 'step', line: d.line };
|
||||||
|
}
|
||||||
|
if (msg.startsWith('FPlay')) { const d = await _req('POST', '/api/active/play'); return { type: 'ok', data: JSON.stringify(d) }; }
|
||||||
|
if (msg.startsWith('FStop')) { const d = await _req('POST', '/api/active/stop'); return { type: 'ok', data: JSON.stringify(d) }; }
|
||||||
|
|
||||||
|
throw new Error(`Unbekannter FCode: ${msg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _req(method, path, body) {
|
||||||
|
const url = `${_baseUrl()}${path}`;
|
||||||
|
const opts = { method };
|
||||||
|
if (body !== undefined && body !== null) {
|
||||||
|
opts.headers = { 'content-type': 'application/json' };
|
||||||
|
opts.body = JSON.stringify(body);
|
||||||
|
}
|
||||||
|
const res = await fetch(url, opts);
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json().catch(() => ({}));
|
||||||
|
const e = new Error(err.message || res.statusText);
|
||||||
|
e.code = err.code;
|
||||||
|
e.status = res.status;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { isFCode, handle };
|
||||||
115
robot/GCode.js
115
robot/GCode.js
@@ -1,14 +1,11 @@
|
|||||||
/***
|
/***
|
||||||
* Receives GCode, processes it and moves the Data to the Roboter-Class
|
* Receives GCode, processes it and moves the Data to the Roboter-Class
|
||||||
*/
|
*/
|
||||||
const fs = require('fs');
|
|
||||||
const RobotController = require('./RobotController');
|
const RobotController = require('./RobotController');
|
||||||
|
|
||||||
|
|
||||||
class GCode{
|
class GCode{
|
||||||
|
|
||||||
static fileName = "GCodeFiles/log.gcode";
|
|
||||||
|
|
||||||
static containsMCode(s){
|
static containsMCode(s){
|
||||||
return s === 'M1' || s.startsWith('M1 ');
|
return s === 'M1' || s.startsWith('M1 ');
|
||||||
}
|
}
|
||||||
@@ -55,33 +52,12 @@ class GCode{
|
|||||||
', "a":'+ robot.a +
|
', "a":'+ robot.a +
|
||||||
', "b":'+ robot.b +
|
', "b":'+ robot.b +
|
||||||
', "c":'+ robot.c +
|
', "c":'+ robot.c +
|
||||||
', "e":'+ 0.0 +
|
', "e":'+ (robot.e ?? 0) +
|
||||||
'}}';
|
'}}';
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static toPiMultiple(gCode){
|
|
||||||
if(gCode == undefined){return gCode;}
|
|
||||||
|
|
||||||
var multipleParameters = gCode.split(" ");
|
|
||||||
var newGString = "";
|
|
||||||
multipleParameters.forEach((paramet) => {
|
|
||||||
if(paramet == undefined || !paramet || paramet.length === 0 || paramet == " " || paramet ==""){
|
|
||||||
|
|
||||||
}
|
|
||||||
else if(['A', 'a', 'B', 'b', 'C', 'c','E','e'].includes(paramet.charAt(0)) )
|
|
||||||
{
|
|
||||||
var numberParameter = Number(paramet.substring(1, paramet.length))
|
|
||||||
newGString += " " + paramet[0] + String(numberParameter*Math.PI/180)
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
newGString += " " + paramet;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return newGString.trim();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verarbeitet eine rohe G-Code-Nachricht. Parsing + Steuerlogik liegen jetzt im
|
* Verarbeitet eine rohe G-Code-Nachricht. Parsing + Steuerlogik liegen jetzt im
|
||||||
* GCodeParser bzw. RobotController (ToDo_6); diese Methode bleibt als Fassade
|
* GCodeParser bzw. RobotController (ToDo_6); diese Methode bleibt als Fassade
|
||||||
@@ -94,95 +70,6 @@ class GCode{
|
|||||||
return RobotController.receive(robot, g);
|
return RobotController.receive(robot, g);
|
||||||
}
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////77
|
|
||||||
// Commands for Files
|
|
||||||
|
|
||||||
static removeStringFromFile(fileName, stringToRemove) {
|
|
||||||
try {
|
|
||||||
const data = fs.readFileSync(fileName, 'utf8');
|
|
||||||
const modifiedData = data.replace(new RegExp(stringToRemove, 'g'), '');
|
|
||||||
fs.writeFileSync(fileName, modifiedData, 'utf8');
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static ContainsFilesCommand(message){
|
|
||||||
if(message.indexOf('FPoint') !== -1){return true;} // Writes new Point (at end of file)
|
|
||||||
if(message.indexOf('FPlus') !== -1){return true;} // go to Next Position in Log File
|
|
||||||
if(message.indexOf('FMinus') !== -1){return true;} // go to Previous Position
|
|
||||||
if(message.indexOf('FFirst') !== -1){return true;} // set Cursour to First Position of Log File
|
|
||||||
if(message.indexOf('FLast') !== -1){return true;} // set Cursour to Last Position of Log File
|
|
||||||
|
|
||||||
|
|
||||||
if(message.indexOf('FShow') !== -1){return true;} // Shows/Sends GCode-File
|
|
||||||
if(message.indexOf('FList') !== -1){return true;} // Lists GCode-Files
|
|
||||||
if(message.indexOf('FLoad ') !== -1){return true;} // Loads File into Log
|
|
||||||
if(message.indexOf('FSave ') !== -1){return true;} // Saves Log to GCode-File
|
|
||||||
if(message.indexOf('FClear') !== -1){return true;} // Clears default Log File
|
|
||||||
|
|
||||||
if(message.indexOf('M20') !== -1){return true;} // M20 - List SD Card-Contents // https://marlinfw.org/docs/gcode/M020.html
|
|
||||||
if(message.indexOf('M23') !== -1){return true;} // M23 - Select SD file
|
|
||||||
if(message.indexOf('M28') !== -1){return true;} // M28 - Start SD write // M28 [B1] filename
|
|
||||||
if(message.indexOf('M29') !== -1){return true;} // M29 - Stop SD write
|
|
||||||
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static receiveFC(robot, message){
|
|
||||||
|
|
||||||
if(message.indexOf('FShow') !== -1){
|
|
||||||
try {
|
|
||||||
const data = fs.readFileSync(this.fileName, 'utf8');
|
|
||||||
const reply = "XYZ__FShow__XYZ" + data; // prepend header
|
|
||||||
return reply.replaceAll("G91 ","").replaceAll("G90 ","").replace(/\s?\bt\d+\b\s?/g, '').trim();
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(message.indexOf('FPoint') !== -1){
|
|
||||||
|
|
||||||
const secondsSinceEpoch = 10*Math.floor(Date.now() / 100);
|
|
||||||
|
|
||||||
var strGCode = String(`G90 G1 x${robot.x} y${robot.y} z${robot.z} a${(robot.phi*180/Math.PI).toFixed(2)} b${(robot.theta*180/Math.PI).toFixed(2)} c${(robot.psi*180/Math.PI).toFixed(2)} e${(robot.e*180/Math.PI).toFixed(2)} t${secondsSinceEpoch} f1000` )
|
|
||||||
this.removeStringFromFile(this.fileName, ';!')
|
|
||||||
fs.appendFileSync(this.fileName, strGCode+ ';!'+ '\r\n', 'utf8');
|
|
||||||
}
|
|
||||||
|
|
||||||
if(message.indexOf('FMinus') !== -1 || message.indexOf('FPlus') !== -1){
|
|
||||||
let lines = fs.readFileSync(this.fileName, 'utf8').split('\r\n');
|
|
||||||
|
|
||||||
let lineChange = -1;
|
|
||||||
|
|
||||||
// Process lines
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
if (lines[i].indexOf(';!') !== -1) {
|
|
||||||
if(message.indexOf('FMinus') !== -1 && i > 0){
|
|
||||||
lines[i - 1] += ';!';
|
|
||||||
lines[i] = lines[i].split(';!')[0];
|
|
||||||
var gCodePi = this.toPiMultiple(lines[i-1]);
|
|
||||||
this.receiveGCode(robot, gCodePi);
|
|
||||||
}
|
|
||||||
if(message.indexOf('FPlus') !== -1 && i < (lines.length-2) && lineChange == -1){
|
|
||||||
lines[i + 1] += ';!';
|
|
||||||
lines[i] = lines[i].split(';!')[0];
|
|
||||||
lineChange = i+1;
|
|
||||||
var gCodePi = this.toPiMultiple(lines[i+1]);
|
|
||||||
this.receiveGCode(robot, gCodePi);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the updated content back
|
|
||||||
fs.writeFileSync(this.fileName, lines.join('\r\n'), 'utf8');
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return GCode.getM114(robot);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = GCode
|
module.exports = GCode
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// server/InputWS.js
|
// server/InputWS.js
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const WebSocket = require('ws');
|
const WebSocket = require('ws');
|
||||||
|
const FCodeClient = require('../robot/FCodeClient');
|
||||||
|
|
||||||
const LOG_DIR = './logs';
|
const LOG_DIR = './logs';
|
||||||
|
|
||||||
@@ -70,18 +71,24 @@ function initInputWS(server, robot, GCode, sharedState) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- File commands → broadcast result ----------
|
/* ---------- FCode (Datei-Befehle) → weiterleiten an appRobotFileservice ----------
|
||||||
* Behaviour kept as-is on purpose: file/log management (and finer-grained
|
* Der Driver ist Gateway: die Steuerungen kennen nur ihn.
|
||||||
* targeting, e.g. FShow as a requester-only reply) is owned by ToDo 4. */
|
* Stepping-Befehle (FPlus/FMinus/…) liefern eine driver-native GCode-Zeile
|
||||||
if (GCode.ContainsFilesCommand(message)) {
|
* (Radian) zurück, die der Driver direkt ausführt und dann broadcastet. */
|
||||||
|
if (FCodeClient.isFCode(message)) {
|
||||||
logCommand(sharedState, clientIP, message);
|
logCommand(sharedState, clientIP, message);
|
||||||
let result;
|
FCodeClient.handle(robot, message)
|
||||||
try {
|
.then(result => {
|
||||||
result = GCode.receiveFC(robot, message);
|
if (result.type === 'step' && result.line) {
|
||||||
} catch (err) {
|
try { GCode.receiveGCode(robot, result.line); } catch (err) {
|
||||||
return sendError(ws, 'FILE_ERROR', err.message, message);
|
return sendError(ws, 'GCODE_ERROR', err.message, result.line);
|
||||||
}
|
}
|
||||||
if (result !== undefined) broadcast(wss, result);
|
broadcast(wss, GCode.getM114(robot));
|
||||||
|
} else if (result.data) {
|
||||||
|
broadcast(wss, result.data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => sendError(ws, 'FILE_ERROR', err.message, message));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
151
test/FCodeClient.test.js
Normal file
151
test/FCodeClient.test.js
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
const FCodeClient = require('../robot/FCodeClient');
|
||||||
|
|
||||||
|
const robot = {
|
||||||
|
x: 0, y: 300, z: 0,
|
||||||
|
phi: Math.PI / 2, theta: -Math.PI / 2, psi: 0, e: 0,
|
||||||
|
feedrate: 1000,
|
||||||
|
};
|
||||||
|
|
||||||
|
function mockOk(data) {
|
||||||
|
global.fetch = jest.fn().mockResolvedValue({
|
||||||
|
ok: true, status: 200, statusText: 'OK',
|
||||||
|
json: () => Promise.resolve(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockErr(status, code, message) {
|
||||||
|
global.fetch = jest.fn().mockResolvedValue({
|
||||||
|
ok: false, status, statusText: 'Error',
|
||||||
|
json: () => Promise.resolve({ code, message }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => { delete global.fetch; });
|
||||||
|
|
||||||
|
/* ---------- isFCode ---------- */
|
||||||
|
|
||||||
|
describe('isFCode', () => {
|
||||||
|
test('erkennt alle bekannten FCodes', () => {
|
||||||
|
for (const f of ['FList','FShow','FLoad demo','FSave Name','FClear','FPoint','FPlus','FMinus','FFirst','FLast','FGoto 3','FPlay','FStop']) {
|
||||||
|
expect(FCodeClient.isFCode(f)).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('erkennt Nicht-FCodes nicht', () => {
|
||||||
|
expect(FCodeClient.isFCode('G1 X100')).toBe(false);
|
||||||
|
expect(FCodeClient.isFCode('F1000')).toBe(false);
|
||||||
|
expect(FCodeClient.isFCode('M114')).toBe(false);
|
||||||
|
expect(FCodeClient.isFCode('Ping')).toBe(false);
|
||||||
|
expect(FCodeClient.isFCode('')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ---------- handle: Listing / Info ---------- */
|
||||||
|
|
||||||
|
test('FList → GET /api/programs, type list', async () => {
|
||||||
|
mockOk([{ id: 'a', name: 'A' }]);
|
||||||
|
const result = await FCodeClient.handle(robot, 'FList');
|
||||||
|
expect(result.type).toBe('list');
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('/api/programs'),
|
||||||
|
expect.objectContaining({ method: 'GET' })
|
||||||
|
);
|
||||||
|
expect(JSON.parse(result.data)).toEqual([{ id: 'a', name: 'A' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('FShow ohne id → GET /api/active', async () => {
|
||||||
|
mockOk({ programId: 'x', cursor: 0 });
|
||||||
|
const result = await FCodeClient.handle(robot, 'FShow');
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(expect.stringContaining('/api/active'), expect.anything());
|
||||||
|
expect(result.type).toBe('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('FShow mit id → GET /api/programs/:id', async () => {
|
||||||
|
mockOk({ id: 'demo', name: 'Demo' });
|
||||||
|
await FCodeClient.handle(robot, 'FShow demo');
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(expect.stringContaining('/api/programs/demo'), expect.anything());
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ---------- handle: Teaching ---------- */
|
||||||
|
|
||||||
|
test('FPoint → POST /api/active/points mit Roboterpose (Radian)', async () => {
|
||||||
|
mockOk({ index: 0, line: 'G90 G1 x0 y300 z0 a1.5708 b-1.5708 c0 e0 f1000' });
|
||||||
|
const result = await FCodeClient.handle(robot, 'FPoint');
|
||||||
|
expect(result.type).toBe('point');
|
||||||
|
const call = global.fetch.mock.calls[0];
|
||||||
|
expect(call[1].method).toBe('POST');
|
||||||
|
const body = JSON.parse(call[1].body);
|
||||||
|
expect(body.pose.a).toBeCloseTo(Math.PI / 2, 4);
|
||||||
|
expect(body.pose.b).toBeCloseTo(-Math.PI / 2, 4);
|
||||||
|
expect(body.feedrate).toBe(1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ---------- handle: Program management ---------- */
|
||||||
|
|
||||||
|
test('FLoad → PUT /api/active mit id', async () => {
|
||||||
|
mockOk({ programId: 'mein_prog', cursor: 0, lineCount: 5 });
|
||||||
|
const result = await FCodeClient.handle(robot, 'FLoad mein_prog');
|
||||||
|
expect(result.type).toBe('ok');
|
||||||
|
const body = JSON.parse(global.fetch.mock.calls[0][1].body);
|
||||||
|
expect(body).toEqual({ id: 'mein_prog' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('FSave → POST /api/programs mit name und fromActive:true', async () => {
|
||||||
|
mockOk({ id: 'test_prog', name: 'Test Prog' });
|
||||||
|
await FCodeClient.handle(robot, 'FSave Test Prog');
|
||||||
|
const body = JSON.parse(global.fetch.mock.calls[0][1].body);
|
||||||
|
expect(body.name).toBe('Test Prog');
|
||||||
|
expect(body.fromActive).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('FClear → POST /api/active/clear', async () => {
|
||||||
|
mockOk({ ok: true });
|
||||||
|
await FCodeClient.handle(robot, 'FClear');
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(expect.stringContaining('/api/active/clear'), expect.objectContaining({ method: 'POST' }));
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ---------- handle: Stepping → type step ---------- */
|
||||||
|
|
||||||
|
test('FPlus → POST /api/active/next → type step mit Zeile', async () => {
|
||||||
|
mockOk({ cursor: 1, line: 'G90 G1 x10 y300 z0 a1.5708 b-1.5708 c0 e0 f1000' });
|
||||||
|
const result = await FCodeClient.handle(robot, 'FPlus');
|
||||||
|
expect(result.type).toBe('step');
|
||||||
|
expect(result.line).toContain('G1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('FMinus → POST /api/active/prev', async () => {
|
||||||
|
mockOk({ cursor: 0, line: 'G90 G1 x0 y300 z0 a0 b0 c0 e0 f1000' });
|
||||||
|
const result = await FCodeClient.handle(robot, 'FMinus');
|
||||||
|
expect(result.type).toBe('step');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('FFirst / FLast', async () => {
|
||||||
|
mockOk({ cursor: 0, line: 'G90 G1 x0 y0 z0 a0 b0 c0 e0 f1000' });
|
||||||
|
const r1 = await FCodeClient.handle(robot, 'FFirst');
|
||||||
|
expect(r1.type).toBe('step');
|
||||||
|
|
||||||
|
mockOk({ cursor: 4, line: 'G90 G1 x5 y0 z0 a0 b0 c0 e0 f1000' });
|
||||||
|
const r2 = await FCodeClient.handle(robot, 'FLast');
|
||||||
|
expect(r2.type).toBe('step');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('FGoto N → POST /api/active/goto mit index', async () => {
|
||||||
|
mockOk({ cursor: 3, line: 'G90 G1 x3 y0 z0 a0 b0 c0 e0 f1000' });
|
||||||
|
await FCodeClient.handle(robot, 'FGoto 3');
|
||||||
|
const body = JSON.parse(global.fetch.mock.calls[0][1].body);
|
||||||
|
expect(body.index).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ---------- Fehlerbehandlung ---------- */
|
||||||
|
|
||||||
|
test('Fileservice-Fehler → wirft mit code und status', async () => {
|
||||||
|
mockErr(404, 'PROGRAM_NOT_FOUND', 'not found');
|
||||||
|
await expect(FCodeClient.handle(robot, 'FLoad nichtda')).rejects.toMatchObject({
|
||||||
|
code: 'PROGRAM_NOT_FOUND',
|
||||||
|
status: 404,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Unbekannter FCode → wirft', async () => {
|
||||||
|
await expect(FCodeClient.handle(robot, 'FUnbekannt')).rejects.toThrow('Unbekannter FCode');
|
||||||
|
});
|
||||||
@@ -1,6 +1,26 @@
|
|||||||
const GCode = require('../robot/GCode.js');
|
// Datei-Operationen liegen jetzt in appRobotFileservice.
|
||||||
|
// GCode kennt keine ContainsFilesCommand mehr — FCodeClient.isFCode übernimmt die Erkennung.
|
||||||
test('GCode - FPoint', () => {
|
const FCodeClient = require('../robot/FCodeClient');
|
||||||
var x = GCode.ContainsFilesCommand("FPoint") ;
|
|
||||||
expect(x).toBe(true);
|
test('isFCode erkennt FPoint', () => {
|
||||||
});
|
expect(FCodeClient.isFCode('FPoint')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isFCode erkennt FList', () => {
|
||||||
|
expect(FCodeClient.isFCode('FList')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isFCode erkennt FPlus / FMinus / FFirst / FLast', () => {
|
||||||
|
expect(FCodeClient.isFCode('FPlus')).toBe(true);
|
||||||
|
expect(FCodeClient.isFCode('FMinus')).toBe(true);
|
||||||
|
expect(FCodeClient.isFCode('FFirst')).toBe(true);
|
||||||
|
expect(FCodeClient.isFCode('FLast')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isFCode: feedrate F1000 ist kein FCode', () => {
|
||||||
|
expect(FCodeClient.isFCode('F1000')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isFCode: G1-Befehl ist kein FCode', () => {
|
||||||
|
expect(FCodeClient.isFCode('G1 X100')).toBe(false);
|
||||||
|
});
|
||||||
|
|||||||
95
test/InputWS.fcode.test.js
Normal file
95
test/InputWS.fcode.test.js
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
// Testet das FCode-Routing in InputWS (FCodeClient wird gemockt).
|
||||||
|
jest.mock('../robot/FCodeClient', () => ({
|
||||||
|
isFCode: jest.fn((msg) => /^F[A-Z][a-z]+/.test(String(msg).trim())),
|
||||||
|
handle: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const http = require('http');
|
||||||
|
const WebSocket = require('ws');
|
||||||
|
const FCodeClient = require('../robot/FCodeClient');
|
||||||
|
const initInputWS = require('../server/InputWS');
|
||||||
|
const GCode = require('../robot/GCode');
|
||||||
|
const createDummyRobot = require('./helpers/createDummyRobot');
|
||||||
|
|
||||||
|
/* ---------- helpers ---------- */
|
||||||
|
|
||||||
|
function listen(server) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
server.listen(0, () => {
|
||||||
|
const { port } = server.address();
|
||||||
|
port ? resolve(port) : reject(new Error('no port'));
|
||||||
|
});
|
||||||
|
server.on('error', reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function connect(port) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const ws = new WebSocket(`ws://127.0.0.1:${port}`);
|
||||||
|
ws.on('open', () => resolve(ws));
|
||||||
|
ws.on('error', reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextMessage(ws, timeoutMs = 2000) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const t = setTimeout(() => reject(new Error('Timeout')), timeoutMs);
|
||||||
|
ws.once('message', (d) => { clearTimeout(t); resolve(d.toString()); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- suite ---------- */
|
||||||
|
|
||||||
|
describe('InputWS FCode-Routing', () => {
|
||||||
|
let server, ws;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
FCodeClient.handle.mockReset();
|
||||||
|
FCodeClient.isFCode.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (ws) { ws.close(); ws = null; }
|
||||||
|
if (server) { await new Promise(r => server.close(r)); server = null; }
|
||||||
|
});
|
||||||
|
|
||||||
|
async function setup(robot) {
|
||||||
|
server = http.createServer();
|
||||||
|
initInputWS(server, robot || createDummyRobot(), GCode, { connectedClients: [], lastCommands: [], lastPings: [] });
|
||||||
|
const port = await listen(server);
|
||||||
|
ws = await connect(port);
|
||||||
|
return ws;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('FList → broadcastet die Antwortdaten vom Fileservice', async () => {
|
||||||
|
FCodeClient.handle.mockResolvedValue({ type: 'list', data: JSON.stringify([{ id: 'a' }]) });
|
||||||
|
await setup();
|
||||||
|
const replyP = nextMessage(ws);
|
||||||
|
ws.send('FList');
|
||||||
|
expect(JSON.parse(await replyP)).toEqual([{ id: 'a' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('FPlus (step) → GCode-Zeile ausführen + M114 broadcasten', async () => {
|
||||||
|
const line = 'G90 G1 x10 y300 z0 a1.5708 b-1.5708 c0 e0 f1000';
|
||||||
|
FCodeClient.handle.mockResolvedValue({ type: 'step', line });
|
||||||
|
const robot = createDummyRobot();
|
||||||
|
await setup(robot);
|
||||||
|
const replyP = nextMessage(ws);
|
||||||
|
ws.send('FPlus');
|
||||||
|
const msg = JSON.parse(await replyP);
|
||||||
|
expect(msg.position).toBeDefined();
|
||||||
|
expect(robot.sendCommand).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Fileservice-Fehler → machine-readable FILE_ERROR an Sender', async () => {
|
||||||
|
const err = Object.assign(new Error('not found'), { code: 'PROGRAM_NOT_FOUND' });
|
||||||
|
FCodeClient.handle.mockRejectedValue(err);
|
||||||
|
await setup();
|
||||||
|
const replyP = nextMessage(ws);
|
||||||
|
ws.send('FLoad nichtda');
|
||||||
|
const msg = JSON.parse(await replyP);
|
||||||
|
expect(msg.type).toBe('error');
|
||||||
|
expect(msg.code).toBe('FILE_ERROR');
|
||||||
|
expect(msg.input).toBe('FLoad nichtda');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user