Umbau 12: Robot-Kinematics als extends RobotBase
This commit is contained in:
@@ -85,10 +85,10 @@ calculateAngles3D() // Workspace → Motorwinkel (schreibt auf t
|
||||
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
|
||||
- [x] `robot/RobotBase.js` anlegen — generische Infrastruktur aus `Robot.js`
|
||||
- [x] Beide Kinematik-Methoden in `RobotBase` als Stub mit `throw new Error('not implemented')`
|
||||
- [x] JSDoc: Interface-Vertrag dokumentieren
|
||||
- [x] `rotateAroundAxis()` wandert in `RobotBase` als geschützte Hilfsmethode
|
||||
|
||||
---
|
||||
|
||||
@@ -101,16 +101,16 @@ robot/
|
||||
└── Arm3SegmentLinearX.js ← bisheriger Robot.js-Kinematik-Teil
|
||||
```
|
||||
|
||||
- [ ] `robot/kinematics/Arm3SegmentLinearX.js` anlegen
|
||||
- [x] `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:
|
||||
- [x] `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
|
||||
- [x] Alle bestehenden Tests müssen grün bleiben — kein Verhalten ändert sich
|
||||
(`Robot.Kinematics.RoundTrip.test.js` ist das primäre Sicherheitsnetz)
|
||||
|
||||
---
|
||||
@@ -124,15 +124,17 @@ environment:
|
||||
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`:
|
||||
- [x] `ROBOT_KINEMATICS` — Bezeichner der Kinematik-Klasse (Default: `arm3segmentlinearx`)
|
||||
- [x] `ROBOT_KINEMATICS_PARAMS` — JSON mit Konstruktor-Parametern
|
||||
- [x] `KinematicsFactory.js` (`createRobotFromEnv`), eingebunden in `startRobot.js`:
|
||||
```js
|
||||
const kin = loadKinematics(process.env.ROBOT_KINEMATICS, params);
|
||||
const robot = new kin(params.l1, params.l2, params.l3);
|
||||
const robot = createRobotFromEnv(processEnv, { l1: 250, l2: 264, l3: 100 });
|
||||
```
|
||||
- [ ] Unbekannte Kinematik → klare Fehlermeldung beim Start, kein silent fail
|
||||
- [x] Unbekannte Kinematik → klare Fehlermeldung beim Start, kein silent fail
|
||||
- [ ] Neue Variablen ins zentrale Config-Modul aufnehmen (koordinieren mit `ToDo_3_Config`)
|
||||
→ **offen:** `ToDo_3_Config` ist noch nicht umgesetzt. Die Factory liest `process.env`
|
||||
vorerst direkt (gleicher Stil wie `ROBOT_DEFAULT_FEEDRATE`); das zentrale Config-Modul
|
||||
kann die beiden Variablen später übernehmen. In `docker-compose.yaml` bereits dokumentiert.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -43,6 +43,195 @@ WebSocket → GCode → calculateAngles3D → sendCommand → tSocket.write()
|
||||
- [ ] Status (`Idle`, `Run`, `Alarm`, `Hold`) für den `InfoServer` bereitstellen
|
||||
- `/api/status` um GRBL-Zustand erweitern
|
||||
|
||||
---
|
||||
|
||||
## Baustein für Paket 4 + 5: Rückabbildung Port → Motorwerte
|
||||
|
||||
Beide folgenden Pakete brauchen denselben Baustein: aus den von GRBL gemeldeten
|
||||
`MPos`-Werten der drei Controller die **sieben Motorwerte des Roboters** rekonstruieren
|
||||
(`xMotor, alpha, beta, a, b, c, eMotor`).
|
||||
|
||||
Das ist die **Umkehrung von `portValue()`** (`robot/TelnetSenderGRBL.js`). `portValue()`
|
||||
bildet *eine Roboter-Achse → einen GRBL-Port-Wert* ab, dabei koppeln einige Ports mehrere
|
||||
Achsen (z. B. der z-Port der Hand mischt `c, b, z, y`). Die Rückrichtung muss diese
|
||||
Kopplung **explizit auflösen** — sie ergibt sich nicht automatisch.
|
||||
|
||||
- [ ] `motorStateFromPorts(portReadings)` definieren — algebraische Umkehrung von `portValue()`
|
||||
- Eingang: pro Sender die gelesenen Port-Werte (`{x, y, z}` Base, `{a}` Elbow, `{c, e, b}` Hand)
|
||||
- Ausgang: `{xMotor, alpha, beta, a, b, c, eMotor}`
|
||||
- Grad→Rad zurückrechnen, `factorTurnLift`/`handOpenInMM` herausrechnen, gekoppelte Ports auflösen
|
||||
- [ ] **Round-Trip-Invariante** als Test: `portValue(motorStateFromPorts(p)) ≈ p`
|
||||
- dasselbe Muster wie `test/Robot.Kinematics.RoundTrip.test.js`
|
||||
- schützt die Umkehrfunktion gegen Drift gegenüber `portValue()`
|
||||
|
||||
> Hinweis: Gelesen wird auf dem **aktiven** Sender `TelnetSenderGRBL` (im `data`-Handler,
|
||||
> siehe Paket 1) — nicht auf `FluidNCClient.js`.
|
||||
|
||||
---
|
||||
|
||||
## Paket 4: Hardware-Position auslesen und übernehmen (Sync-Command)
|
||||
|
||||
**Ziel:** Ein Befehl liest die echten Motor-Koordinaten aller drei Controller aus,
|
||||
übernimmt sie als neuen Soll-Zustand und passt die berechnete Roboter-Pose entsprechend an.
|
||||
Nötig nach Homing, manuellem Jog, Endschalter-Auslösung oder Reconnect — die Software weiß
|
||||
sonst nicht, wo der Roboter physisch wirklich steht.
|
||||
|
||||
- [ ] Neuer Eingabe-Befehl, z. B. `M114 R` (Read-Hardware) oder WS-Message `syncFromHardware`
|
||||
- klar abgegrenzt vom bestehenden `M114`, das nur die **Software**-Position zurückgibt
|
||||
(`GCode.getM114(robot)` in `server/InputWS.js`)
|
||||
- [ ] Ablauf des Sync:
|
||||
1. an alle drei Sender `?` senden, je `MPos` aus der Antwort parsen (Paket 3)
|
||||
2. `motorStateFromPorts(...)` → sieben Motorwerte rekonstruieren (Baustein oben)
|
||||
3. diese auf den Roboter schreiben: `robot.xMotor/alpha/beta/a/b/c/eMotor = …`
|
||||
4. **Vorwärtskinematik** anstoßen: `robot.calculatePositionFromMotorAngles()`
|
||||
→ füllt `robot.x/y/z` und `phi/theta/psi` aus den Hardwarewerten
|
||||
5. `motorPosition`/`motorPositionOld` zurücksetzen, damit der nächste Move sauber von
|
||||
der echten Position aus rechnet (sonst falscher Speed-Delta im Korrekt-Modus)
|
||||
- [ ] dem anfragenden Client die übernommene Pose zurückmelden (`reply(ws, …)`)
|
||||
- [ ] **kein** automatisches Nachfahren — Sync ändert nur den Soll-Zustand, sendet keinen Move
|
||||
|
||||
> Warum nicht einfach „letzten gesendeten Wert" merken? Weil die Hardware nach Homing/Jog/
|
||||
> Stall von dem abweicht, was zuletzt gesendet wurde — genau diese Differenz soll Sync auflösen.
|
||||
|
||||
---
|
||||
|
||||
## Paket 5: Bewegungs-Fortschritt ermitteln (Move-Progress)
|
||||
|
||||
**Ziel:** Herausfinden, wie weit FluidNC einen laufenden Move bereits abgearbeitet hat —
|
||||
also wie weit sich jeder Controller schon zur Ziel-Position bewegt hat (0…100 %).
|
||||
|
||||
- [ ] Beim Absenden eines Moves Start und Ziel je Controller festhalten
|
||||
- `mStart` = Port-Werte vor dem Move, `mTarget` = gesendete Port-Werte
|
||||
- liegt bereits vor: `robot.motorPositionOld` (Start) und `robot.motorPosition` (Ziel),
|
||||
über `portValue()` in Port-Werte umgerechnet
|
||||
- [ ] Während der Bewegung periodisch `?` pollen (Paket 3) und je Controller berechnen:
|
||||
```
|
||||
fortschritt_i = |MPos_jetzt − Start_i| / |Ziel_i − Start_i| (auf 0…1 geklemmt)
|
||||
```
|
||||
- [ ] Aggregat-Fortschritt + Fertig-Erkennung:
|
||||
- **Fertig**, wenn alle drei Controller `state == Idle` UND `MPos ≈ Ziel`
|
||||
- der Gesamt-Fortschritt eines Schritts = **Minimum** über die Controller
|
||||
(der langsamste bestimmt, wann der Schritt fertig ist)
|
||||
- im **Korrekt-Modus** (ToDo_6a) sollten alle Controller etwa gleich schnell fertig sein —
|
||||
eine große Spreizung der Einzel-Fortschritte ist dort ein Warnsignal (Feedrate-Fehler)
|
||||
- [ ] Fortschritt + Status nach außen geben
|
||||
- über `InfoServer` (`/api/status`) und/oder als WS-Push an die Clients
|
||||
- ermöglicht eine Fortschrittsanzeige beim Abspielen von Dateien (ToDo_6b)
|
||||
- [ ] Zusammenspiel mit der Command-Queue (Paket 2): erst den nächsten Move senden, wenn
|
||||
der vorige `Idle` erreicht hat → verhindert, dass Fortschritt mehrerer Moves verschwimmt
|
||||
|
||||
### Offener Kernpunkt: Was, wenn währenddessen der nächste Befehl kommt?
|
||||
|
||||
Heute ist der Treiber **fire-and-forget**: ein neuer Befehl wird sofort an alle drei GRBLs
|
||||
geschickt und landet in deren Planner-Puffer. Das hat zwei Folgen, die Paket 5 für sich
|
||||
genommen nicht löst:
|
||||
|
||||
1. **Fortschritt wird mehrdeutig.** `motorPositionOld`/`motorPosition` halten nur die
|
||||
*letzten zwei* Stellungen. Sind mehrere Moves gleichzeitig im GRBL-Puffer, weiß der
|
||||
Treiber nicht mehr, *welcher* gerade fährt — der `?`-Fortschritt bezieht sich dann auf
|
||||
das falsche Start/Ziel-Paar.
|
||||
2. **Stille Befehlsverwerfung.** Kommen Befehle dauerhaft schneller als sie ausgeführt
|
||||
werden, läuft GRBLs 128-Byte-RX-Puffer über → Befehle gehen verloren (vgl. Paket 2).
|
||||
|
||||
Die saubere Lösung ist eine **zeitgesteuerte Sende-Queue** — bewusst als größerer,
|
||||
**abschaltbarer** Umbau in Paket 6.
|
||||
|
||||
---
|
||||
|
||||
## Paket 6: Zeitgesteuerte Sende-Queue (`ROBOT_USE_QUEUE`)
|
||||
|
||||
**Ziel:** Befehle nicht mehr blind sofort raushauen, sondern in einer Queue puffern und
|
||||
**zeitlich getaktet** absenden — jeden Befehl gerade rechtzeitig, bevor der vorige fertig
|
||||
ist. So bleibt die Bewegung flüssig (GRBL-Planner läuft nie leer), nichts geht verloren
|
||||
(kein Puffer-Überlauf), und das Netz wird nicht mit Dauer-Polling oder Befehls-Salven
|
||||
belastet.
|
||||
|
||||
### Schalter (Pflicht — Absicherung wie bei ToDo_6a)
|
||||
|
||||
| Env | Default | Wirkung |
|
||||
|---|---|---|
|
||||
| `ROBOT_USE_QUEUE` | `false` | **Aus = exakt heutiges Fire-and-forget.** Jeder Befehl geht sofort an alle Sender, kein Pacing, keine Queue. Byte-identisch zu vorher. |
|
||||
| `ROBOT_USE_QUEUE` | `true` | Neue zeitgesteuerte Queue-Logik aktiv. |
|
||||
|
||||
```yaml
|
||||
# docker-compose.yaml → appRobotDriver
|
||||
environment:
|
||||
- ROBOT_USE_QUEUE=false # oder: true
|
||||
```
|
||||
|
||||
> Wie `ROBOT_SPEED_MODE` greift der Umbau **nur** bei `true`. Solange `false`, ist der
|
||||
> Sende-Pfad unverändert — das bestehende Sicherheitsnetz (Sender-Tests) bleibt gültig.
|
||||
|
||||
### Idee: zwei Uhren — eine geschätzte (gratis), eine gemessene (kostet Netz)
|
||||
|
||||
Der Trick, das Netz **nicht** zu überlasten: primär nach einer **geschätzten** Uhr takten,
|
||||
die `?`-Messung nur sparsam zur Korrektur einsetzen.
|
||||
|
||||
- **Geschätzte Uhr (Haupttakt, kein Netzverkehr):** Jeder Queue-Eintrag trägt seine
|
||||
**voraussichtliche Ausführzeit** — die liegt mit `moveTime` aus ToDo_6a bereits vor.
|
||||
Der Treiber führt je Controller einen lokalen Zeitstempel `controllerFreiAb`:
|
||||
```
|
||||
beim Senden: controllerFreiAb += moveTime_dieses_Befehls
|
||||
nächster Send, wenn: jetzt >= controllerFreiAb − vorlauf
|
||||
```
|
||||
`vorlauf` = kleine Sicherheitsmarge, damit immer ~1 Move im GRBL-Planner wartet und die
|
||||
Bewegung nicht stockt. Das ist ein reiner Timer → **null Zusatz-Traffic**.
|
||||
|
||||
- **Gemessene Uhr (Korrektur, sparsam):** Die Schätzung driftet (Beschleunigung/Abbremsen
|
||||
stecken nicht in `dist/feedrate`, kurze Moves dauern real länger). Deshalb ab und zu —
|
||||
**nicht** bei jedem Move — per `?` (Paket 3) den echten Stand holen und `controllerFreiAb`
|
||||
nachjustieren. Auslöser: Queue läuft fast leer, ein langer Move (einmal mittendrin prüfen),
|
||||
oder ein fester Maximaltakt. **`?` strikt raten-begrenzen** (z. B. ≤ 5 Hz, GRBL-üblich) →
|
||||
Netzlast bleibt gedeckelt.
|
||||
|
||||
### Eintrag in der Queue
|
||||
|
||||
- [ ] Queue-Eintrag hält: geparster Befehl / Motorziel, `moveTime` (geschätzt), die je
|
||||
Sender resultierenden G-Code-Strings, Status (`pending → sent → done`)
|
||||
- [ ] `done` wird gesetzt durch geschätzte Uhr **oder** (falls gepollt) durch `?`=`Idle` am Ziel
|
||||
|
||||
### Pacing-Schleife
|
||||
|
||||
- [ ] je Controller `controllerFreiAb` führen, Sendezeitpunkt aus `moveTime − vorlauf` ableiten
|
||||
- [ ] Tiefe im GRBL-Planner begrenzen (z. B. ≤ 1–2 vorausgesendete Moves) — flüssig, aber
|
||||
noch steuerbar
|
||||
- [ ] Drift-Korrektur per ratenbegrenztem `?`; bei großer Abweichung Schätzung neu setzen
|
||||
- [ ] **Optionaler Sicherheitsboden:** zusätzlich Character-Counting (Summe ungequittierter
|
||||
Zeilenlängen < 128 B) als Netz gegen Puffer-Überlauf, falls die Schätzung mal stark danebenliegt
|
||||
|
||||
### Verhalten bei „neuer Befehl kommt mitten in der Bewegung"
|
||||
|
||||
Zwei Betriebsarten — je nach Quelle der Befehle:
|
||||
|
||||
- [ ] **Anhängen (Datei abspielen / ToDo_6b):** Reihenfolge erhalten, nach Schätzung takten,
|
||||
In-Flight-Tiefe begrenzen. Kein Befehl wird verworfen.
|
||||
- [ ] **Neuester gewinnt (interaktives Streaming, z. B. 3D-Input / Bodytracker):** Trifft ein
|
||||
neues Ziel ein, während noch ungesendete (`pending`) Einträge in der Queue liegen, den
|
||||
veralteten Schwanz **ersetzen** statt anzuhängen (Coalescing) → niedrige Latenz, kein
|
||||
Auflaufen veralteter Zwischenziele. (Kann als Unter-Schalter / spätere Ausbaustufe kommen.)
|
||||
- [ ] **Backpressure:** maximale Queue-Länge festlegen. Bei Dauer-Überlauf entweder
|
||||
Ältestes-verwerfen (Streaming) oder Druck an den WS-Client zurückgeben (Datei) — nie
|
||||
unbegrenzt wachsen lassen.
|
||||
|
||||
### Fortschritt mit Pipelining (behebt die Mehrdeutigkeit aus Paket 5)
|
||||
|
||||
- [ ] Den „aktuell fahrenden" Eintrag in der Queue markieren (Kopf vorrücken, wenn geschätzte
|
||||
Uhr abgelaufen **oder** `?`=`Idle`). Erst dieser Kopf liefert das richtige Start/Ziel-Paar
|
||||
für die `?`-Fortschrittsrechnung aus Paket 5.
|
||||
|
||||
### Kanten / Sonderfälle
|
||||
|
||||
- [ ] **Alarm/Error** mitten in der Queue (Paket 1) → Queue leeren, Senden stoppen, Fehler melden
|
||||
- [ ] **Sync-Command (Paket 4)** bei nicht-leerer Queue → erst Queue leeren; die gepufferten
|
||||
Ziele sind nach einem Re-Homing veraltet
|
||||
- [ ] **Drei Controller driften auseinander:** Im Korrekt-Modus (ToDo_6a) laufen sie auf
|
||||
dieselbe `moveTime` → ihre `controllerFreiAb` sollten zusammenbleiben; große Spreizung ist
|
||||
ein Warnsignal (Feedrate-/Schätzfehler)
|
||||
- [ ] **Schätzgüte:** `moveTime = dist/feedrate` ignoriert Beschleunigung; kurze Moves dauern
|
||||
real länger. Vorlauf-Marge muss das abfangen; später ggf. Trapez-Profil-Schätzung verfeinern
|
||||
|
||||
---
|
||||
|
||||
## Hinweis zur Implementierung
|
||||
|
||||
`robot/fluidnc/FluidNCClient.js` ist eine bidirektionale WebSocket-Anbindung an FluidNC (Port 81) mit Reconnect-Logik und `EventEmitter`-Interface — diese Klasse ist eine gute Grundlage für alle drei Pakete und sollte bei der Umsetzung von `ToDo_2` (Sender-Interface) mit evaluiert werden.
|
||||
|
||||
Reference in New Issue
Block a user