ToDos anpassen

This commit is contained in:
chk
2026-06-09 12:05:18 +02:00
parent c777f871cd
commit 2da589dfa3
8 changed files with 679 additions and 88 deletions

View File

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

View File

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

View File

@@ -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
└── <NextRobot>.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/<Name>.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

126
doc/ToDo_49_Cleanup.md Normal file
View File

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

114
doc/ToDo_6a_Speed.md Normal file
View File

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

113
doc/ToDo_6b_FileHandling.md Normal file
View File

@@ -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 <file>` | ✅ | ❌ | Lädt eine andere Datei als aktive |
| `FSave <file>` | ✅ | ❌ | 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 <dateiname>`** — benannte Datei aus `GCodeFiles/` laden, `currentLineIndex`
auf den `;!`-Marker setzen (Rückwärtskompatibilität), oder auf 0 falls kein Marker
- [ ] **`FSave <dateiname>`** — aktiven Puffer unter `GCodeFiles/<dateiname>` 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

View File

@@ -1 +0,0 @@
npm test

174
sendTest_Client.py → test/sendTest_Client.py Executable file → Normal file
View File

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