diff --git a/README.md b/README.md index 67046de..ad2630f 100644 --- a/README.md +++ b/README.md @@ -144,9 +144,14 @@ Architektur- und Refactoring-Aufgaben sind in `doc/ToDo_*.md` dokumentiert: | `doc/ToDo_4_GCode.md` | G-Code- und Datei-Handling trennen | | `doc/ToDo_5_API.md` | WebSocket-Antwortlogik strukturieren | | `doc/ToDo_6_RobotController.md` | RobotController-Klasse einführen | +| `doc/ToDo_6a_Speed.md` | Speed-Steuerung: `calculateSpeeds()` reparieren, per-Achse Feedrate | +| `doc/ToDo_6b_FileHandling.md` | File-Handling: fehlende Befehle, Cursor im Speicher, Fehler-Feedback | | `doc/ToDo_7_Tests.md` | Testabdeckung und Stabilität | | `doc/ToDo_8_Bugs.md` | Bekannte konkrete Bugs | | `doc/ToDo_9_HardwareFeedback.md` | Hardware-Feedback-Loop (GRBL-Antworten, Command-Queue, Positionsabgleich) | +| `doc/ToDo_10_VerbindungsVerlust.md` | Verbindungsverlust erkennen, Watchdog, UI-Statusanzeige | +| `doc/ToDo_12_InverseKinematikConfig_ROADMAP.md` | Austauschbare Kinematik: RobotBase + Robot7M, Env-Konfiguration | +| `doc/ToDo_49_Cleanup.md` | Pre-Release-Cleanup: tote Code, Zertifikate, ToDos, README | ### Empfohlene Bearbeitungsreihenfolge @@ -156,8 +161,11 @@ ToDo_3 Config — Fundament für alles Weitere ToDo_1 Parser ┐ ToDo_6 RobotController ┘ zusammen, da eng verzahnt ToDo_4 Datei-Handling — danach, klar abgrenzbar +ToDo_6a Speed-Steuerung — calculateSpeeds bugfix, dann Sender-Integration +ToDo_6b File-Handling Detail — fehlende F-Befehle, Cursor im Speicher ToDo_2 Sender-Interface — mit Entscheidung: Telnet vs. FluidNC-WebSocket ToDo_9 Hardware-Feedback — baut auf ToDo_2 auf +ToDo_10 Verbindungsverlust — baut auf ToDo_2 auf, parallel zu ToDo_9 möglich ToDo_5 API — parallel zu ToDo_2/4 möglich ToDo_7 Tests — begleitend zu allen obigen ``` diff --git a/doc/ToDo_10_VerbindungsVerlust.md b/doc/ToDo_10_VerbindungsVerlust.md new file mode 100644 index 0000000..24c5776 --- /dev/null +++ b/doc/ToDo_10_VerbindungsVerlust.md @@ -0,0 +1,77 @@ +# ToDo 10 — Verbindungsverlust erkennen und anzeigen + +## Problem + +Wenn der Telnet-Server (oder später die WS-Verbindung) wegbricht — kein Strom, Netzwerkausfall, Firewall-Drop — wird das weder im Container-Log noch auf der Info-Webseite angezeigt. Die Status-Seite zeigt dauerhaft „alles grün", obwohl kein Sender mehr erreichbar ist. + +Die Ursache: TCP-Sockets können „still sterben". Der Socket ist auf Node.js-Seite noch offen, das `close`-Event feuert nicht (z. B. bei Firewall-Drop oder Netzwerk-Timeout), und der aktuelle Health-Check in `InfoServer.js` prüft nur `tSocket != null` — das bleibt `true`. + +## Abgrenzung zu anderen ToDos + +| ToDo | Thema | Überschneidung | +|---|---|---| +| **ToDo_2** | Reconnect-Strategie, Sender-Interface, `state`-Property | Fundament — ToDo_10 baut darauf auf und ergänzt den Watchdog | +| **ToDo_5** | Health-Checks in `/api/status` | Dort angelegt; ToDo_10 füllt die fehlende Tiefe | +| **ToDo_9** | Timeout bei ausbleibenden GRBL-`ok`-Antworten | Ergänzend: ToDo_9 = Protokoll-Ebene, ToDo_10 = Verbindungs-Ebene | + +**Was hier neu ist:** aktiver Watchdog-Heartbeat (auch bei scheinbar offenem Socket), vollständige State-Machine mit sichtbarem Reconnect-Zyklus, und UI-Visualisierung in `public/app.js` — das deckt kein anderes ToDo ab. + +**Voraussetzung:** ToDo_2 muss abgeschlossen sein (`state`-Property und Sender-Interface existieren). + +--- + +## Hinweis: Das Problem betrifft Telnet und WebSocket gleichermaßen + +Das „stille Sterben" ist kein Telnet-spezifisches Problem — es ist ein TCP-Problem. WebSocket läuft ebenfalls über TCP und hat dieselbe Schwachstelle, wenn kein aktiver Keepalive verwendet wird. + +Der Unterschied liegt im verfügbaren Gegenmittel: + +| Transport | Klasse | Mechanismus | +|---|---|---| +| **Telnet** | `TelnetSenderGRBL.js` | Applikations-Watchdog: Timer + Schreib-Probe (kein Protokoll-Support) | +| **WebSocket** | `WSSenderGrbl.js` | WebSocket Ping/Pong (RFC 6455, Protokollebene) — sauberer und standardisiert; aktuell **nicht** aktiviert | + +`WSSenderGrbl.js` verwaltet das `ws`-WebSocket-Objekt direkt und hat bereits eine State-Machine (`state`, `reconnectAttempt`, `reconnectTimer`). Die Reconnect-Logik bei `close`- und `error`-Events ist implementiert. Was noch fehlt, ist der aktive Ping/Pong-Heartbeat für den Fall des stillen TCP-Todes. + +`FluidNCClient.js` ist ein älterer, paralleler Ansatz und wird von `WSSenderGrbl.js` **nicht** verwendet. + +--- + +## Paket 1: Watchdog im Sender + +`WSSenderGrbl.js` hat bereits State-Machine und Reconnect-Logik. Offen bleibt: + +- [ ] Aktiven Heartbeat einführen — nicht nur auf das `close`-Event warten + - **WebSocket (`WSSenderGrbl.js`):** WebSocket Ping/Pong aktivieren — `ws.ping()` alle 30 s, bei ausbleibendem Pong Verbindung als tot markieren und Reconnect auslösen + - **Telnet (`TelnetSenderGRBL.js`):** Timer + applikationsseitige Schreib-Probe, da kein Protokoll-Support + - erkennt „stille" Socket-Tode, bei denen kein `close`-Event feuert +- [ ] State-Machine im Sender konsolidieren + - Zustände: `connected` | `connecting` | `reconnecting` | `disconnected` + - `reconnectAttempt` (Zähler) und `reconnectDelay` (aktuelle Wartezeit) als Properties + - bei Verbindungsverlust: `state` → `disconnected`, Reconnect-Zyklus starten (1 min / 2 min Intervall) +- [ ] Fehlerzustand und Reconnect-Fortschritt loggbar machen + - im Container-Log erkennbar: welcher Sender, welcher Zustand, nächster Versuch wann + +## Paket 2: Status-API vertiefen + +- [ ] `InfoServer.js` `/api/status` liefert echten Sender-Zustand aus der State-Machine + - nicht mehr nur `tSocket != null`, sondern `sender.getStatus()` mit `state`, `reconnectAttempt`, `lastSeen` + - bei `disconnected` oder `reconnecting`: `health`-Summary auf `degraded` setzen +- [ ] `lastSeen`-Timestamp pro Sender führen + - wann wurde zuletzt erfolgreich gesendet oder eine Antwort empfangen? + - hilft Diagnose: Sender tot seit X Sekunden + +## Paket 3: UI-Visualisierung + +- [ ] `public/app.js` wertet den Sender-Status aus `/api/status` aus + - Farb-Indikator pro Sender: grün / gelb (reconnecting) / rot (disconnected) + - bei `reconnecting`: Anzeige von Versuchs-Nummer und nächstem Retry-Zeitpunkt +- [ ] Statuswechsel sofort sichtbar machen + - `/api/status` wird periodisch (z. B. alle 10 s) abgefragt oder per WebSocket-Push aktualisiert + +## Betroffene Dateien + +- `robot/TelnetSenderGRBL.js` (oder künftiger Sender) — Watchdog-Logik, State-Machine +- `server/InfoServer.js` — tieferer Status-Abruf via `getStatus()` +- `public/app.js` — Visualisierung des Verbindungszustands +- `startRobot.js` — keine Änderung erwartet, Sender-Interface bleibt gleich diff --git a/doc/ToDo_12_InverseKinematikConfig_ROADMAP.md b/doc/ToDo_12_InverseKinematikConfig_ROADMAP.md new file mode 100644 index 0000000..c72f393 --- /dev/null +++ b/doc/ToDo_12_InverseKinematikConfig_ROADMAP.md @@ -0,0 +1,154 @@ +# Roadmap 12 — Austauschbare Kinematik + +## Ziel + +Der `appRobotDriver` soll als generischer G-Code-Treiber für beliebige Roboterarme +funktionieren. Die Kinematik ist der einzige arm-spezifische Teil — alles andere +(G-Code, WebSocket, Sender, Konfiguration) ist roboter-unabhängig. + +## Konventionen (festgelegt) + +- **Workspace-Koordinaten:** `x, y, z, phi, theta, psi` + `e` (Greifer) — gilt für alle + 6-DOF-Arme mit Greifer. Das ist der implizite Vertrag dieses Frameworks. +- **Motoranzahl:** 7 Slots (`x, y, z, a, b, c, e` in `RobotMotorPosition`) bleiben fest. + Das Framework ist für 6-DOF + Greifer ausgelegt. +- **Ordner:** `robot/kinematics/` (nicht `inverseKinematics/` — enthält beide Richtungen) +- **Klassenname:** beschreibend für die physikalische Struktur, z. B. `Arm3SegmentLinearX` + +--- + +## Dateistruktur (Zielzustand) + +``` +robot/ +├── RobotBase.js ← Interface + generische Infrastruktur +├── Robot.js ← dauerhafter Alias: module.exports = RobotBase +├── KinematicsFactory.js ← lädt Kinematik-Klasse anhand Env-Variable +└── kinematics/ + ├── Arm3SegmentLinearX.js ← aktuelle Implementierung + └── .js ← zukünftige Implementierungen +``` + +## Wo ist das Interface? + +**`RobotBase` ist das Interface** — als abstrakte Basisklasse (JavaScript-Idiom). +Es definiert zwei Dinge: +1. Die **Infrastruktur**, die jede Implementierung erbt (State, `sendCommand`, ...) +2. Den **Vertrag**: zwei Methoden, die jede Implementierung überschreiben *muss* + +`RobotBase` ist die einzige Klasse, die alle anderen Module (`GCode.js`, `InputWS.js`, +Sender, ...) kennen müssen. Sie importieren nie eine konkrete Kinematik. + +## Wie greift der restliche Code auf den Roboter zu? + +**Heute und während der Transition:** +``` +startRobot.js → require('./robot/Robot') → Robot.js (Alias) → Arm3SegmentLinearX +GCode.js → bekommt robot als Parameter → sieht nur RobotBase-Methoden +InputWS.js → bekommt robot als Parameter → sieht nur RobotBase-Methoden +``` + +`GCode.js`, `InputWS.js` und alle Sender erhalten `robot` bereits als Parameter — +sie importieren `Robot.js` gar nicht. **Für sie ändert sich nichts.** + +**Langfristig (nach Abschluss Phase 2):** +``` +startRobot.js → KinematicsFactory → instantiiert Arm3SegmentLinearX (oder anderen) +robot/Robot.js → dauerhafter Alias für RobotBase (nicht mehr für eine Implementierung) +``` + +`robot/Robot.js` bleibt erhalten, zeigt aber auf `RobotBase`: +```js +// robot/Robot.js — dauerhaft +module.exports = require('./RobotBase'); +``` + +Damit kann externer Code weiterhin `require('./robot/Robot')` schreiben und bekommt +die Basisklasse — z. B. für `instanceof`-Checks oder zum Ableiten in Tests. + +--- + +## Phase 0 — `RobotBase` und Interface-Vertrag + +`Robot.js` enthält heute zwei Dinge: generische Infrastruktur und arm-spezifische Kinematik. +Der Schnitt: + +**`RobotBase` (generisch, nie überschreiben):** +- Zustandsvariablen: `x, y, z, phi, theta, psi, e, feedrate, moveRelative` +- Motor-Zustand: `xMotor, alpha, beta, a, b, c, eMotor` + Changed-Flags +- `sendCommand()`, `createMotorPosition()`, `calculateSpeeds()` +- `cmdReceivers`, `savedPoints` + +**Interface-Vertrag (abstrakt, muss überschrieben werden):** +```js +calculateAngles3D() // Workspace → Motorwinkel (schreibt auf this.*) +calculatePositionFromMotorAngles() // Motorwinkel → Workspace (schreibt auf this.*) +``` + +- [ ] `robot/RobotBase.js` anlegen — generische Infrastruktur aus `Robot.js` +- [ ] Beide Kinematik-Methoden in `RobotBase` als Stub mit `throw new Error('not implemented')` +- [ ] JSDoc: Interface-Vertrag dokumentieren +- [ ] `rotateAroundAxis()` wandert in `RobotBase` als geschützte Hilfsmethode + +--- + +## Phase 1 — `Arm3SegmentLinearX` als erste Implementierung + +``` +robot/ +├── RobotBase.js +└── kinematics/ + └── Arm3SegmentLinearX.js ← bisheriger Robot.js-Kinematik-Teil +``` + +- [ ] `robot/kinematics/Arm3SegmentLinearX.js` anlegen + - `class Arm3SegmentLinearX extends RobotBase` + - Konstruktor: `constructor(l1, l2, l3)` → `super()` + Längen + - `calculateAngles3D()` — unverändert übernommen + - `calculatePositionFromMotorAngles()` — unverändert übernommen +- [ ] `robot/Robot.js` wird zum Kompatibilitäts-Alias für die Übergangsperiode: + ```js + module.exports = require('./kinematics/Arm3SegmentLinearX'); + ``` +- [ ] Alle bestehenden Tests müssen grün bleiben — kein Verhalten ändert sich + (`Robot.Kinematics.RoundTrip.test.js` ist das primäre Sicherheitsnetz) + +--- + +## Phase 2 — Konfiguration über Umgebungsvariable + +```yaml +# docker-compose.yml +environment: + ROBOT_KINEMATICS: arm3segmentlinearx + ROBOT_KINEMATICS_PARAMS: '{"l1": 250, "l2": 264, "l3": 100}' +``` + +- [ ] `ROBOT_KINEMATICS` — Bezeichner der Kinematik-Klasse (Default: `arm3segmentlinearx`) +- [ ] `ROBOT_KINEMATICS_PARAMS` — JSON mit Konstruktor-Parametern +- [ ] `KinematicsFactory.js` oder direkt in `startRobot.js`: + ```js + const kin = loadKinematics(process.env.ROBOT_KINEMATICS, params); + const robot = new kin(params.l1, params.l2, params.l3); + ``` +- [ ] Unbekannte Kinematik → klare Fehlermeldung beim Start, kein silent fail +- [ ] Neue Variablen ins zentrale Config-Modul aufnehmen (koordinieren mit `ToDo_3_Config`) + +--- + +## Phase 3 — Zweite Kinematik-Implementierung + +Erst wenn ein konkreter zweiter Roboter definiert ist. + +- [ ] Physikalische Spezifikation dokumentieren (DOF, Achsen, Gelenkreihenfolge) +- [ ] `robot/kinematics/.js` anlegen — nur die zwei Kinematik-Methoden +- [ ] RoundTrip-Tests für die neue Implementierung schreiben +- [ ] Prüfen ob die 7 Motor-Slots ausreichen; falls nicht → `RobotMotorPosition` anpassen + +--- + +## Abhängigkeiten + +- Phase 1 ist unabhängig von allen anderen ToDos — kann sofort angegangen werden +- Phase 2 koordiniert mit `ToDo_3_Config` +- Phase 3 hat keine zeitliche Vorgabe — wird bei Bedarf aufgenommen diff --git a/doc/ToDo_49_Cleanup.md b/doc/ToDo_49_Cleanup.md new file mode 100644 index 0000000..33d0bfd --- /dev/null +++ b/doc/ToDo_49_Cleanup.md @@ -0,0 +1,126 @@ +# ToDo 49 — Cleanup vor Auslieferung + +Dieser Cleanup wird als letzter Schritt vor einer Auslieferung durchgeführt, nachdem +die funktionalen ToDos abgeschlossen sind. Einige Punkte sind bereits jetzt erledigbar, +andere hängen von vorgelagerten ToDos ab. + +--- + +## 1. ToDo-Dateien aufräumen + +**Vollständig erledigte ToDos löschen** (alle Punkte `[x]`, keine offenen): + +- [ ] `doc/ToDo_1_Parsing.md` — vollständig erledigt +- [ ] `doc/ToDo_2_Anbindung.md` — vollständig erledigt +- [ ] `doc/ToDo_5_API.md` — vollständig erledigt +- [ ] `doc/ToDo_8_Bugs.md` — vollständig erledigt (nutzt `✅`-Marker statt `[x]`) + +**Teilweise erledigte ToDos bereinigen:** + +- [ ] `doc/ToDo_7_Tests.md` — erledigte Punkte entfernen, nur offene behalten + +**README.md-Tabelle anpassen:** + +- [ ] Zeilen der gelöschten ToDo-Dateien aus der Tabelle entfernen +- [ ] Prioritätsreihenfolge im README auf den aktuellen Stand bringen + +--- + +## 2. README.md Review + +- [ ] Alle Abschnitte auf Aktualität prüfen — insbesondere nach den Refactorings aus + ToDo_1, ToDo_2, ToDo_5 +- [ ] `robot/Robot.js` beschreiben was es nach ToDo_12 ist (Alias → `RobotBase`) +- [ ] Env-Variablen-Liste: `ROBOT_KINEMATICS` und `ROBOT_KINEMATICS_PARAMS` ergänzen + (nach ToDo_12 Phase 2) +- [ ] Tippfehler `GRBL_ELLBOW_IP` prüfen: ist das Legacy-Absicht oder korrigierbar? +- [ ] Laufzeitvoraussetzungen: `logs/`-Verzeichnis-Hinweis prüfen (nach ToDo_3 evtl. obsolet) +- [ ] Einmal vollständig lesen und gegen den tatsächlichen Code abgleichen + +--- + +## 3. Toter Code entfernen + +Folgende Felder in `robot/Robot.js` werden nirgendwo im Projekt gelesen oder gesetzt +(Grep über alle `.js` ohne `node_modules` ergibt nur die Deklaration selbst): + +- [ ] `this.speedX`, `this.speedY`, `this.speedZ` — nie referenziert +- [ ] `this.doAnimate` — nie referenziert +- [ ] `this.showFunctions` — nie referenziert +- [ ] `this.lastCommandSend` / `this.oldCommandTime` — gesetzt, nie gelesen +- [ ] `this.savedPoints`, `this.atPointNr` — gesetzt, nie gelesen + +Folgende Methoden in `robot/GCode.js` werden nie aufgerufen: + +- [ ] `containsMCode()` — hat einen Test, wird im Produktivcode aber nie aufgerufen; + entweder integrieren oder entfernen (inklusive Test) +- [ ] `receiveMCode()` — nie aufgerufen, kann entfernt werden + +--- + +## 4. `robot/Robot.js` Alias aufräumen *(abhängig von ToDo_12 Phase 2)* + +- [ ] `robot/Robot.js` zeigt nach Phase 1 auf `Arm3SegmentLinearX` (Übergangsalias) +- [ ] Nach Phase 2: auf `RobotBase` umzeigen: + ```js + module.exports = require('./RobotBase'); + ``` +- [ ] Kommentar in `robot/Robot.js` aktualisieren: Zweck des Alias erklären + +--- + +## 5. Sicherheit: Zertifikate und private Schlüssel aus Git entfernen + +Die folgenden Dateien liegen aktuell im Repository und sollten dort **nicht** sein: + +- [ ] `https/localhost.key` — privater Schlüssel +- [ ] `https/localhost2.key` — privater Schlüssel +- [ ] `https/key.pem` — privater Schlüssel +- [ ] `https/localhost.pem` — Zertifikat +- [ ] `https/cert_abcd.pfx` — enthält privaten Schlüssel +- [ ] Diese Dateien aus Git-History entfernen (`git filter-repo` oder `BFG`) +- [ ] `.gitignore` um `https/*.key`, `https/*.pem`, `https/*.pfx`, `https/*.cer` ergänzen +- [ ] Eine `https/README.md` oder `https/EXAMPLE.md` anlegen mit Anleitung zur + Zertifikatserzeugung (z. B. `openssl` self-signed für lokale Entwicklung) + +--- + +## 6. GCodeFiles/ aus Git entfernen + +`GCodeFiles/` enthält Nutzungsprotokolle aus 2025 — das ist Laufzeitdata, kein Quellcode. + +- [ ] `.gitignore` um `GCodeFiles/*.gcode` ergänzen (oder `GCodeFiles/log*.gcode`) +- [ ] Eine Beispieldatei `GCodeFiles/example.gcode` ins Repository nehmen +- [ ] Bestehende Log-Dateien aus Git-History entfernen (optional, je nach Sensitivität) + +--- + +## 7. Naming-Inkonsistenz: `WSSenderGrbl.js` + +- [ ] `WSSenderGrbl.js` exportiert eine Klasse namens `TelnetSenderGRBL` — das ist + der alte Name aus der Telnet-Ära +- [ ] Klasse in `WSSenderGrbl.js` umbenennen auf `WSSenderGrbl` +- [ ] Alle Importe prüfen (aktuell: `startRobot.js`, Tests) + +--- + +## 8. Hilfsdateien prüfen + +- [ ] `install.bat` / `runTest.bat` — aktuell und korrekt? Oder löschen? +- [ ] `sendTest_Client.py` — gepflegt? Ins README erwähnen oder löschen +- [ ] `https/info.txt` — Inhalt prüfen; falls nur interne Notizen: löschen +- [ ] `robot/fluidnc/FluidNCClient.js` — wird von `WSSenderGrbl.js` nicht mehr verwendet; + entweder löschen oder als "experimentell/archiviert" kennzeichnen + +--- + +## 9. Console.log-Hygiene + +Der Code hat viele `console.log`-Aufrufe — einige sind wichtig, andere Debugging-Reste. + +- [ ] `robot/Robot.js` `sendCommand()`: ausführliches Log bei jedem Befehl — für Produktion + eher auf `debug`-Level oder hinter eine `verbose`-Flag +- [ ] `robot/GCode.js`: Motor-Position-Log in `receiveMCode` (ohnehin toter Code) +- [ ] Sinnvolle Logs behalten (Verbindungsaufbau, Fehler, Start-Info) +- [ ] Kandidat für späteren Ausbau: leichtgewichtiger Logger statt nacktem `console.log` + (z. B. Log-Level via Env-Variable) diff --git a/doc/ToDo_6a_Speed.md b/doc/ToDo_6a_Speed.md new file mode 100644 index 0000000..4c67ada --- /dev/null +++ b/doc/ToDo_6a_Speed.md @@ -0,0 +1,114 @@ +# ToDo 6a — Speed-Steuerung + +## Ist-Zustand und Defizite + +Die Feedrate-Logik ist auf zwei Ebenen defekt: + +### Ebene 1: `calculateSpeeds()` berechnet NaN + +`Robot.calculateSpeeds(oldPos, newPos)` liest `oldPos.xMotor`, `oldPos.alpha`, `oldPos.beta` — +diese Properties existieren in `RobotMotorPosition` **nicht**. Dort heißen sie `x`, `y`, `z`. +Ergebnis: alle `motorSpeeds`-Werte sind `NaN`. Die Methode ist seit ihrer Einführung kaputt +und fällt nur nicht auf, weil `ROBOT_USE_SPEED_CALC` standardmäßig `false` ist. + +```js +// Zeile ~214 Robot.js — falsch: +this.motorSpeeds.x = (this.xMotor - oldPos.xMotor) / time; // oldPos.xMotor → undefined +this.motorSpeeds.y = (this.alpha - oldPos.alpha) / time; // oldPos.alpha → undefined +this.motorSpeeds.z = (this.beta - oldPos.beta) / time; // oldPos.beta → undefined + +// Richtig (RobotMotorPosition-Felder): +this.motorSpeeds.x = (this.xMotor - oldPos.x) / time; +this.motorSpeeds.y = (this.alpha - oldPos.y) / time; +this.motorSpeeds.z = (this.beta - oldPos.z) / time; +``` + +### Ebene 2: `motorSpeeds` werden von keinem Sender gelesen + +Selbst wenn `calculateSpeeds()` korrekte Werte lieferte, ignorieren beide Sender die +berechneten Geschwindigkeiten vollständig: + +- **`TelnetSenderGRBL`**: sendet `mNew.feedrate` (Kartesisch, mm/min) an alle Achsen gleich +- **`WSSenderGrbl`**: sendet immer `this.maxSpeedF`, ignoriert `mNew.feedrate` komplett + +Das ist grundsätzlich falsch: Der Roboter hat drei unabhängige GRBL-Controller. Damit die +Fingerspitze mit `F1000 mm/min` fährt, muss jede Achse mit einer **anderen** Winkelgeschwindigkeit +drehen — je nachdem, wie weit sie sich für diesen Schritt bewegt. Alle Achsen mit derselben +Feedrate zu befehlen führt zu nicht-linearen Werkzeugbahnen. + +### Ebene 3: `FPoint` kodiert Feedrate hart + +```js +// GCode.js FPoint — immer f1000, egal was robot.feedrate ist: +var strGCode = `G90 G1 x${robot.x} ... f1000` +``` + +--- + +## Konzept: Korrekte Feedrate-Verteilung + +Gesamtziel: Die Kartesische Feedrate `F` (mm/min) des G-Code-Befehls bestimmt, wie lange +der Bewegungsschritt dauert. Diese Zeit wird auf alle Achsen aufgeteilt. + +``` +Zeit = kartesische_Distanz / F_mm_per_min + +Achsen-Feedrate[i] = Achsen-Delta[i] / Zeit + (in Grad/min, da GRBL-Achsen typisch in Grad konfiguriert) +``` + +Der Sender kennt seine Achse (`xAxisGrbl`, `yAxisGrbl`, `zAxisGrbl`) und kann die passende +Geschwindigkeit aus `motorPosition.speeds` lesen, wenn diese korrekt befüllt sind. + +--- + +## Pakete + +### Paket 1: `calculateSpeeds()` reparieren + +- [ ] Property-Namen korrigieren: `oldPos.x/y/z/a/b/c/e` statt `oldPos.xMotor/alpha/beta/...` +- [ ] Einheit klären: `motorSpeeds` in rad/min oder direkt in Grad/min? + - Sender wandeln Motorwinkel bereits von rad → Grad (`* 180/π`) für Positionen + - Einheitlichste Lösung: `motorSpeeds` ebenfalls in Grad/min speichern, oder + die Umrechnung konsistent im Sender vornehmen +- [ ] Grenzfall: Wenn kartesische Distanz null ist (reine Gelenkbewegung), Distanz + über Handgelenk-Punkt verwenden — das ist bereits so angelegt, aber mit den + falschen Property-Namen +- [ ] Unit-Test: `calculateSpeeds` mit bekannten Werten, prüfen dass keine NaN entstehen + +### Paket 2: `motorSpeeds` in den Sender durchreichen + +- [ ] `motorPosition.speeds` korrekt befüllen (passiert bereits via `this.motorPosition.speeds = {...this.motorSpeeds}` nach `calculateSpeeds`) +- [ ] Sender-Interface (`execCommand`): wenn `ROBOT_USE_SPEED_CALC` aktiv und + `motorPosition.speeds[achse]` verfügbar und > 0 → diesen Wert als `F` verwenden +- [ ] Jeder Sender (`TelnetSenderGRBL`, `WSSenderGrbl`) kennt seine Achse und holt + die passende Geschwindigkeit aus `speeds`: + ```js + // Beispiel: Sender ist für Achse "a" zuständig + const f = (useSpeedCalc && mNew.speeds.a > 0) ? mNew.speeds.a : mNew.feedrate; + data += ` f${f.toFixed(2)}`; + ``` +- [ ] Fallback: wenn `ROBOT_USE_SPEED_CALC=false` oder Speeds nicht berechnet → + `mNew.feedrate` wie bisher (Rückwärtskompatibilität) + +### Paket 3: `WSSenderGrbl` Feedrate-Handling vereinheitlichen + +- [ ] `WSSenderGrbl.execCommand()` auf dasselbe Feedrate-Schema wie `TelnetSenderGRBL` umstellen + - aktuell: immer `this.maxSpeedF` → ignoriert das `F` aus dem G-Code-Befehl komplett + - richtig: `mNew.feedrate` verwenden (bzw. per-Achse aus Paket 2) +- [ ] `maxSpeedF` als Obergrenze behalten (Clamp), nicht als feste Ausgabe + +### Paket 4: Kleinere Korrekturen + +- [ ] `FPoint` in `GCode.receiveFC()`: `robot.feedrate` statt hardcodierten `1000` speichern +- [ ] `ROBOT_USE_SPEED_CALC`-Flag dokumentieren: was ändert sich mit/ohne Flag? + Klarer Kommentar in `Robot.js` und im README + +## Betroffene Dateien + +- `robot/Robot.js` — `calculateSpeeds()` bugfix, Einheitenwahl +- `robot/RobotMotorPosition.js` — ggf. `speeds`-Feld klarer benennen +- `robot/TelnetSenderGRBL.js` — per-Achse Feedrate aus `speeds` +- `robot/WSSenderGrbl.js` — Feedrate auf `mNew.feedrate` umstellen + Clamp +- `robot/GCode.js` — `FPoint`: `robot.feedrate` statt `1000` +- `test/GCode.speed.test.js` — Tests für NaN-freie Berechnung ergänzen diff --git a/doc/ToDo_6b_FileHandling.md b/doc/ToDo_6b_FileHandling.md new file mode 100644 index 0000000..237bf38 --- /dev/null +++ b/doc/ToDo_6b_FileHandling.md @@ -0,0 +1,113 @@ +# ToDo 6b — File-Handling + +## Ist-Zustand + +`GCode.receiveFC()` implementiert nur einen Bruchteil der erkannten Befehle: + +| Befehl | Erkannt | Implementiert | Anmerkung | +|---|---|---|---| +| `FPoint` | ✅ | ✅ | Speichert aktuelle Position | +| `FPlus` | ✅ | ✅ | Cursor vorwärts | +| `FMinus` | ✅ | ✅ | Cursor rückwärts | +| `FShow` | ✅ | ✅ | Gibt Dateiinhalt zurück | +| `FFirst` | ✅ | ❌ | Cursor auf erste Zeile | +| `FLast` | ✅ | ❌ | Cursor auf letzte Zeile | +| `FList` | ✅ | ❌ | Listet verfügbare G-Code-Dateien | +| `FLoad ` | ✅ | ❌ | Lädt eine andere Datei als aktive | +| `FSave ` | ✅ | ❌ | Speichert aktive Datei unter neuem Namen | +| `FClear` | ✅ | ❌ | Leert die aktive Log-Datei | +| `M20` | ✅ | ❌ | List SD/Files | +| `M23` | ✅ | ❌ | Select file | +| `M28` | ✅ | ❌ | Start write | +| `M29` | ✅ | ❌ | Stop write | + +Zusätzliche strukturelle Probleme: + +- **Cursor lebt in der Datei** (`';!'`-Marker): jedes `FPlus`/`FMinus` liest und schreibt die ganze Datei neu +- **Synchrones File-IO** (`readFileSync`/`writeFileSync`) blockiert den Node.js-Event-Loop +- **Kein Fehler-Feedback** an den WebSocket-Client — Fehler gehen nur auf `console.error` +- **Aktive Datei ist statisch**: `static fileName = "GCodeFiles/log.gcode"` — `FLoad` kann das nicht ändern, weil es nicht implementiert ist +- **`toPiMultiple()`** ist eine String-Manipulation aus der Vor-Parser-Ära, inkonsistent mit `GCodeParser` +- **`FPoint` hardcodiert `f1000`** — sollte `robot.feedrate` verwenden (auch in ToDo_6a) +- **Kein Directory-Listing für `FList`** — welche Dateien sind gültig? + +--- + +## Konzept + +Die gesamte Datei-Logik wird in eine eigene Klasse `GCodeFileManager` ausgelagert +(gemäß ToDo_4). Diese ToDo beschreibt die **inhaltliche Umsetzung** — unabhängig davon, +ob sie noch in `GCode.js` oder schon in `GCodeFileManager` landet. + +Der **Cursor** wird aus der Datei in den Speicher verlagert: `currentLineIndex` als +Instanzvariable, die nur beim Laden einer Datei initialisiert wird. Das spart permanentes +Neu-Schreiben der Datei. + +--- + +## Pakete + +### Paket 1: Fehlende Befehle implementieren + +- [ ] **`FFirst`** — `currentLineIndex` auf 0 setzen, erste Zeile anfahren +- [ ] **`FLast`** — `currentLineIndex` auf letzte Zeile setzen, anfahren +- [ ] **`FClear`** — aktive Log-Datei leeren, `currentLineIndex` auf 0 +- [ ] **`FList`** — Inhalte von `GCodeFiles/` auflesen, Liste zurückgeben + ``` + XYZ__FList__XYZ + log.gcode + log_2025_10_04.gcode + ... + ``` +- [ ] **`FLoad `** — benannte Datei aus `GCodeFiles/` laden, `currentLineIndex` + auf den `;!`-Marker setzen (Rückwärtskompatibilität), oder auf 0 falls kein Marker +- [ ] **`FSave `** — aktiven Puffer unter `GCodeFiles/` speichern +- [ ] `M20`/`M23`/`M28`/`M29` — Marlin-kompatible Aliases für `FList`/`FLoad`/`FSave`/Stop, + oder explizit als „nicht unterstützt" mit Fehlermeldung antworten + +### Paket 2: Cursor in den Speicher verlagern + +Aktuell wird der Cursor als `';!'` direkt in die Datei geschrieben. Das zwingt bei +jedem `FPlus`/`FMinus` die gesamte Datei neu zu schreiben. + +- [ ] `currentLineIndex` als Instanzvariable einführen (startet bei 0 oder am `';!'`-Marker beim Laden) +- [ ] `FPlus`/`FMinus` nur noch `currentLineIndex` ändern, Datei bleibt unverändert +- [ ] `';!'`-Marker beim **Speichern** (`FSave`) optional mitschreiben (für externe Kompatibilität) +- [ ] Beim Laden einer bestehenden Datei: Zeile mit `';!'` suchen → `currentLineIndex` setzen, + dann `';!'`-Marker aus der Datei entfernen (einmalige Migration) + +### Paket 3: `toPiMultiple` durch Parser-Integration ersetzen + +Gespeicherte Dateien enthalten Winkel in Grad (`a45.00`), der Parser erwartet +Zahlenwerte ohne Umrechnungspflicht. Die Konversion muss explizit und testbar sein. + +- [ ] Beim Lesen einer Zeile aus der Datei: erkennen, ob Winkel in Grad oder Rad vorliegen + - Marker-Ansatz: `FPoint` könnte einen Einheiten-Kommentar schreiben (`;unit=deg`) + - Oder: Konvention festlegen und dokumentieren (Dateien immer in Grad, Anzeige in Grad) +- [ ] `toPiMultiple()` durch eine klar benannte Funktion `degreesToRadians(gCodeString)` ersetzen, + die intern `GCodeParser.parse()` nutzt statt raw-String-Manipulation +- [ ] `FPoint` sollte die Einheit konsistent speichern — Grad ist sinnvoller für Lesbarkeit + +### Paket 4: Fehler-Feedback und Validierung + +- [ ] Bei Dateifehlern (nicht gefunden, Leserechte) → strukturierte Fehlermeldung an den + WebSocket-Client zurückgeben (gemäß Fehler-Envelope aus ToDo_5) +- [ ] `FLoad` validiert, dass die Zieldatei existiert und mindestens eine gültige G-Code-Zeile enthält +- [ ] Dateinamen in `FLoad`/`FSave` sanitizen: keine `../`-Pfade, nur `GCodeFiles/`-Verzeichnis + +### Paket 5: Asynchrones File-IO (optional, später) + +Alle aktuellen `readFileSync`/`writeFileSync`-Aufrufe blockieren den Event-Loop. +Bei kleinen Dateien (< 1 MB) ist das tolerierbar. Bei größeren Programmen oder +häufigem `FPoint`-Schreiben wird es spürbar. + +- [ ] `FPoint`-Schreiben auf `fs.appendFile` (async) umstellen — Cursor ist im Speicher, + kein Lesen nötig +- [ ] `FShow`/`FList` auf `fs.readFile` (async) umstellen +- [ ] `FLoad`/`FSave` async + +## Betroffene Dateien + +- `robot/GCode.js` (oder künftig `robot/GCodeFileManager.js` nach ToDo_4) +- `server/InputWS.js` — Fehler-Routing für File-Befehle +- `test/GCode.FileOperation.test.js` — stark erweitern diff --git a/runTest.bat b/runTest.bat deleted file mode 100755 index 18de984..0000000 --- a/runTest.bat +++ /dev/null @@ -1 +0,0 @@ -npm test \ No newline at end of file diff --git a/sendTest_Client.py b/test/sendTest_Client.py old mode 100755 new mode 100644 similarity index 96% rename from sendTest_Client.py rename to test/sendTest_Client.py index 8fc27a6..4659224 --- a/sendTest_Client.py +++ b/test/sendTest_Client.py @@ -1,88 +1,88 @@ -import websocket, json -import time -import can - - - - - - -def on_message(ws, message): - j = message - if(type(message) == str): - j = json.loads(message) - - if(type(j) == list): - for x in j: - on_message(ws, x) - else: - print(j) - if 'id' not in j: - print("Message ohne ID empfangen. Wird ignoriert.") - return - if 'cmd' not in j: - print("Message ohne CMD empfangen. Wird ignoriert.") - return - - id = j['id'] # 0x01 - cmd = j['cmd'] # 0xf6 # Speed Mode Command - - if "data" in j: - dat = j["data"] # [0x02,0x80,0x02] - data = [cmd] + dat - else: - data = [cmd] - - dlc = len(data)+1 - cr = id - for i in data: - cr += i - - CRC = cr & 0xFF - - msg = can.Message(arbitration_id=id, dlc=dlc, data=data+[CRC], is_extended_id=False) - - try: - bus.send(msg) - #print(f"CAN Message sent on {bus.channel_info}") - - except can.CanError: - print("CAN Message NOT sent: " + str(can.CanError)) - print("CAN Message NOT sent: " + str(can.CanError.error_code)) - - start_time = curtime = time.time() - while 1: - tdiff = start_time + 0.001 - curtime - if tdiff <= 0.: - break - msg = bus.recv(tdiff) - curtime = time.time() - if (msg is None): - continue - - print("CAN Reply is received:") - print(msg) - - -def on_error(ws, error): - print("WebSocket Error:" + error) - -def on_close(a, b, c): - print("### WebSocket closed ###") - -def on_open(ws): - print("WebSocket Connection open") - - -if __name__ == "__main__": - - websocket.enableTrace(True) - ws = websocket.WebSocketApp("ws://01-KW-S0139:9875", - on_message = on_message, - on_error = on_error, - on_close = on_close) - ws.on_open = on_open - - with can.interface.Bus(channel='can0', bustype='socketcan') as bus: - +import websocket, json +import time +import can + + + + + + +def on_message(ws, message): + j = message + if(type(message) == str): + j = json.loads(message) + + if(type(j) == list): + for x in j: + on_message(ws, x) + else: + print(j) + if 'id' not in j: + print("Message ohne ID empfangen. Wird ignoriert.") + return + if 'cmd' not in j: + print("Message ohne CMD empfangen. Wird ignoriert.") + return + + id = j['id'] # 0x01 + cmd = j['cmd'] # 0xf6 # Speed Mode Command + + if "data" in j: + dat = j["data"] # [0x02,0x80,0x02] + data = [cmd] + dat + else: + data = [cmd] + + dlc = len(data)+1 + cr = id + for i in data: + cr += i + + CRC = cr & 0xFF + + msg = can.Message(arbitration_id=id, dlc=dlc, data=data+[CRC], is_extended_id=False) + + try: + bus.send(msg) + #print(f"CAN Message sent on {bus.channel_info}") + + except can.CanError: + print("CAN Message NOT sent: " + str(can.CanError)) + print("CAN Message NOT sent: " + str(can.CanError.error_code)) + + start_time = curtime = time.time() + while 1: + tdiff = start_time + 0.001 - curtime + if tdiff <= 0.: + break + msg = bus.recv(tdiff) + curtime = time.time() + if (msg is None): + continue + + print("CAN Reply is received:") + print(msg) + + +def on_error(ws, error): + print("WebSocket Error:" + error) + +def on_close(a, b, c): + print("### WebSocket closed ###") + +def on_open(ws): + print("WebSocket Connection open") + + +if __name__ == "__main__": + + websocket.enableTrace(True) + ws = websocket.WebSocketApp("ws://01-KW-S0139:9875", + on_message = on_message, + on_error = on_error, + on_close = on_close) + ws.on_open = on_open + + with can.interface.Bus(channel='can0', bustype='socketcan') as bus: + ws.run_forever() \ No newline at end of file