@@ -13,6 +13,77 @@ WebSocket → GCode → calculateAngles3D → sendCommand → tSocket.write()
→ wird nie gelesen (data => {})
→ wird nie gelesen (data => {})
```
```
> Der blinde Kanal ist genau eine Stelle: `socket.on('data', () => {})` in
> `robot/TelnetSenderGRBL.js` (Z. ~123). Alles in diesem ToDo hängt daran.
---
## FluidNC-Protokoll: gesicherte Fakten (recherchiert)
Quellen: [Serial Protocol ](http://wiki.fluidnc.com/en/support/serial_protocol ),
[Automatic Reporting ](http://wiki.fluidnc.com/en/support/interface/automatic_reporting ),
[Cross-Channel #750 ](https://github.com/bdring/FluidNC/issues/750 ).
1. * * `?` funktioniert über Telnet.** FluidNC verarbeitet Realtime-Kommandos (`?` ,
Feed-Hold, Cycle-Start, Reset) über eine **Channel-Abstraktion ** auf * allen * Kanälen
(USB, WiFi, Telnet, WebUI). Der bestehende `telnet-stream` -Socket kann `?` senden und
den `<…>` -Report zurücklesen.
2. **Statuswahl über `$10` (Bitmaske). ** Bit0 = `MPos` statt `WPos` , Bit1 = `Bf` (Puffer).
→ * * `$10=3` liefert `MPos` + `Bf` ** (auf der eingesetzten FluidNC-Version verifizieren).
- **`MPos` ist die richtige Quelle** für die Rückrechnung: der Treiber sendet die
`portValue()` -Werte als * absolute * G-Code-Koordinaten, `MPos` ist offset-fest.
- `WCO` (Work Coordinate Offset) erscheint periodisch; `WPos = MPos − WCO` . Für uns
irrelevant, solange wir `MPos` lesen.
- Beispiel: `<Idle|MPos:151.000,149.000,-1.000|Bf:15,128|FS:0,0|WCO:12,28,78>`
3. * * `Bf:` = das kanonische Flow-Control-Signal.** Erste Zahl = freie Planner-Blöcke,
zweite = freie RX-Bytes. Damit liest man die **echte ** Pufferfüllung, statt sie über
`moveTime` zu * schätzen * (das ignoriert Beschleunigung).
4. **Auto-Reporting statt Polling. ** `$Report/Interval=N` (ms) lässt FluidNC den Status
während der Bewegung **selbst ** pushen — **pro Kanal ** einstellbar, im Stillstand nur
bei Änderung. Ersetzt das `?` -Polling und hält die Netzlast vorhersehbar gedeckelt.
5. **Cross-Channel-Bleed-Through (Caveat). ** `ok` /Reports können auf * anderen * Kanälen
auftauchen als dem auslösenden (Issue #750 ). Da pro FluidNC mehrere Kanäle aktiv sind
(Treiber-Telnet **und ** die `appRobot_Access*` -WebUI-Container), muss der Parser nach
**Nachrichtentyp ** demultiplexen (`ok` / `error:` / `<…>` ) und fremde Zeilen tolerieren —
**kein ** striktes 1:1 Request→Response annehmen.
---
## Designentscheidungen (festgeschrieben)
**B3 — Umkehr-Kinematik ist disambiguierbar. ** Global nicht eindeutig, aber im Arbeitsraum
dieses Roboters per **dokumentierter physikalischer Zusatzbedingung ** auflösbar
(z. B. „Ellbogen höher als Hand" bzw. „Ellbogen hinter der x-Achse"). `motorStateFromPorts()`
bekommt eine feste Zweig-Wahl-Regel; die exakte Vorzeichen-Konvention wird beim Herleiten
der Umkehrung gepinnt.
**B5 — Lockstep als abschaltbare Absicherung. ** Durch die koordinierte Feedrate (ToDo_6a
`correct` ) treffen ohnehin alle Achsen * gleichzeitig * am nächsten Ziel ein — Lockstep ist
die Absicherung, nicht der Hauptmechanismus. Umsetzung als Env-Schalter; **Freerun zuerst **
(reines Zeit-/`Bf` -Pacing), echtes Lockstep erst nach Paket 3/5 (braucht den Feedback-Kanal).
**B6 — Sync ist ein G-Code-Befehl. ** Läuft durch `GCodeParser` + `RobotController`
(nicht als Sonderfall in `InputWS` wie heute `M114` ). **Folge: ** Sync ist der erste
* asynchrone * Befehl — er muss auf die `?` -Antworten aller drei Controller * warten * , bevor
er den Roboterzustand setzt. Die Dispatch-Kette (`RobotController.applyCommand` ) muss dafür
erstmals einen Promise zurückgeben/awaiten können.
### Schalter-Übersicht (Namen sind Vorschläge)
| Env | Werte | Default | Wirkung |
|---|---|---|---|
| `ROBOT_SPEED_MODE` | `legacy` / `correct` | `legacy` | koordinierte Feedrate (ToDo_6a) |
| `ROBOT_USE_QUEUE` | `false` / `true` | `false` | zeitgesteuerte Sende-Queue (Paket 6) |
| `ROBOT_MOTION_SYNC` | `freerun` / `lockstep` | `freerun` | Schritt-für-Schritt-Synchronisation der 3 Controller |
> **Konsistenz-Regel:** `ROBOT_MOTION_SYNC=lockstep` ergibt nur mit `ROBOT_SPEED_MODE=correct`
> Sinn (sonst kommen die Controller zu unterschiedlichen Zeiten an und Lockstep müsste hart
> warten). Beim Start einmal prüfen und ggf. warnen.
---
---
## Paket 1: GRBL-Antworten lesen
## Paket 1: GRBL-Antworten lesen
@@ -26,21 +97,30 @@ WebSocket → GCode → calculateAngles3D → sendCommand → tSocket.write()
## Paket 2: Command-Queue mit ok-Handshake
## Paket 2: Command-Queue mit ok-Handshake
> **Verhältnis zu Paket 6:** Dies ist die *einfache, synchrone* Variante (ein Befehl pro `ok`).
> Die ausgebaute, `Bf`-basierte und abschaltbare Queue steht in **Paket 6** und löst dasselbe
> Problem flüssiger. Paket 2 kann als Zwischenschritt dienen oder direkt in Paket 6 aufgehen.
- [ ] Sendepuffer einführen: Befehle erst abschicken, wenn das vorherige `ok` eingegangen ist
- [ ] Sendepuffer einführen: Befehle erst abschicken, wenn das vorherige `ok` eingegangen ist
- GRBL hat intern ~128 Byte Puffer — bei schnellen Befehlsfolgen (Datei abspielen) droht sonst Puffer-Überlauf und stille Befehlsverwerfung
- GRBL hat intern ~128 Byte Puffer — bei schnellen Befehlsfolgen (Datei abspielen) droht sonst Puffer-Überlauf und stille Befehlsverwerfung
- Alternative: GRBL Line-Counting-Protokoll (sendet mehrere Befehle, zählt Zeichen im Puffer)
- Alternative: GRBL Line-Counting-Protokoll (sendet mehrere Befehle, zählt Zeichen im Puffer)
→ in Paket 6 über die zweite `Bf` -Zahl (freie RX-Bytes) abgedeckt
- [ ] Timeout für ausbleibende `ok` -Antworten definieren
- [ ] Timeout für ausbleibende `ok` -Antworten definieren
- nach X ms ohne Antwort: Fehler loggen, ggf. Verbindung zurücksetzen
- nach X ms ohne Antwort: Fehler loggen, ggf. Verbindung zurücksetzen
## Paket 3: Hardwareposition abfragen (`?`-Status )
## Paket 3: Hardwareposition lesen (Auto-Report statt Polling )
- [ ] Periodisch GRBL-Statusabfrage senden: `?`
- [ ] Beim Verbindungsaufbau je Controller konfigurieren:
- GRBL antwortet mit `<Idle|MPos:0.000,0.000,0.000|WPos:0.000,0.000,0.000>`
- `$10=3` setzen → Report enthält `MPos` **und ** `Bf` (Protokoll-Fakt 2)
- Alternative: nach jedem abgeschlossenen Move abfrag en
- `$Report/Interval=N` setzen (z. B. `N=100…200` ) → FluidNC **pusht ** den Status währ end
- [ ] Gemeldete Hardware-Position mit Softwarepositio n ( `robot.x/y/z` ) vergleichen
der Bewegung selbst (Protokoll-Fakt 4). Kei n `?` -Polling-Loop nötig; `?` bleibt nur als
- bei Abweichung: warnen ode r s ynchronisieren
Einzelabfrage on demand (z. B. fü r S ync, Paket 4).
- [ ] `data` -Handler (Paket 1) parst die gepushten `<…>` -Reports: `state` , `MPos` , `Bf`
- robust gegen Cross-Channel-Fremdzeilen (Protokoll-Fakt 5) — nach Typ demultiplexen
- [ ] Gemeldete Hardware-Position (`MPos` ) mit Softwareposition vergleichen
- bei Abweichung: warnen oder synchronisieren (→ Paket 4)
- schützt gegen Drift durch Endschalter-Auslösung, Motor-Stall, Verbindungsunterbrechung
- schützt gegen Drift durch Endschalter-Auslösung, Motor-Stall, Verbindungsunterbrechung
- [ ] Status (`Idle` , `Run` , `Alarm` , `Hold` ) für den `InfoServer` bereitstellen
- [ ] Status (`Idle` , `Run` , `Alarm` , `Hold` ) + `Bf` für den `InfoServer` bereitstellen
- `/api/status` um GRBL-Zustand erweitern
- `/api/status` um GRBL-Zustand erweitern
---
---
@@ -60,6 +140,8 @@ Kopplung **explizit auflösen** — sie ergibt sich nicht automatisch.
- Eingang: pro Sender die gelesenen Port-Werte (`{x, y, z}` Base, `{a}` Elbow, `{c, e, b}` Hand)
- 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}`
- Ausgang: `{xMotor, alpha, beta, a, b, c, eMotor}`
- Grad→Rad zurückrechnen, `factorTurnLift` /`handOpenInMM` herausrechnen, gekoppelte Ports auflösen
- Grad→Rad zurückrechnen, `factorTurnLift` /`handOpenInMM` herausrechnen, gekoppelte Ports auflösen
- **Zweig-Wahl (B3):** wo die Lösung mehrdeutig ist, die dokumentierte physikalische
Zusatzbedingung anwenden (z. B. „Ellbogen höher als Hand"). Konvention im Code festhalten.
- [ ] **Round-Trip-Invariante ** als Test: `portValue(motorStateFromPorts(p)) ≈ p`
- [ ] **Round-Trip-Invariante ** als Test: `portValue(motorStateFromPorts(p)) ≈ p`
- dasselbe Muster wie `test/Robot.Kinematics.RoundTrip.test.js`
- dasselbe Muster wie `test/Robot.Kinematics.RoundTrip.test.js`
- schützt die Umkehrfunktion gegen Drift gegenüber `portValue()`
- schützt die Umkehrfunktion gegen Drift gegenüber `portValue()`
@@ -76,12 +158,16 @@ Kopplung **explizit auflösen** — sie ergibt sich nicht automatisch.
Nötig nach Homing, manuellem Jog, Endschalter-Auslösung oder Reconnect — die Software weiß
Nötig nach Homing, manuellem Jog, Endschalter-Auslösung oder Reconnect — die Software weiß
sonst nicht, wo der Roboter physisch wirklich steht.
sonst nicht, wo der Roboter physisch wirklich steht.
- [ ] Neuer Eingabe-Befehl , z. B. `M114 R` (Read-Hardware) oder WS-Message `syncFromHardware`
- [ ] **G-Code-Befehl ** (B6) , z. B. `M114 R` (Read-Hardware) — durch `GCodeParser` +
`RobotController` geroutet, **nicht ** als Sonderfall in `InputWS` wie heute `M114`
- klar abgegrenzt vom bestehenden `M114` , das nur die **Software ** -Position zurückgibt
- klar abgegrenzt vom bestehenden `M114` , das nur die **Software ** -Position zurückgibt
(`GCode.getM114(robot)` in `server/InputWS.js` )
(`GCode.getM114(robot)` in `server/InputWS.js` )
- [ ] **Async-Dispatch (B6-Folge): ** `RobotController.applyCommand` muss für diesen Befehl
einen Promise zurückgeben und auf die `?` -Antworten warten — der erste asynchrone Befehl
im bisher synchronen Dispatch-Pfad.
- [ ] Ablauf des Sync:
- [ ] Ablauf des Sync:
1. an alle drei Sender `?` senden, je `MPos` aus der Antwort parsen (Paket 3)
1. an alle drei Sender einmalig `?` senden, je `MPos` aus der Antwort parsen (Paket 3)
2. `motorStateFromPorts(...)` → sieben Motorwerte rekonstruieren (Baustein oben)
2. `motorStateFromPorts(...)` → sieben Motorwerte rekonstruieren (Baustein oben, inkl. B3-Zweigwahl )
3. diese auf den Roboter schreiben: `robot.xMotor/alpha/beta/a/b/c/eMotor = …`
3. diese auf den Roboter schreiben: `robot.xMotor/alpha/beta/a/b/c/eMotor = …`
4. **Vorwärtskinematik ** anstoßen: `robot.calculatePositionFromMotorAngles()`
4. **Vorwärtskinematik ** anstoßen: `robot.calculatePositionFromMotorAngles()`
→ füllt `robot.x/y/z` und `phi/theta/psi` aus den Hardwarewerten
→ füllt `robot.x/y/z` und `phi/theta/psi` aus den Hardwarewerten
@@ -104,7 +190,8 @@ also wie weit sich jeder Controller schon zur Ziel-Position bewegt hat (0…100
- `mStart` = Port-Werte vor dem Move, `mTarget` = gesendete Port-Werte
- `mStart` = Port-Werte vor dem Move, `mTarget` = gesendete Port-Werte
- liegt bereits vor: `robot.motorPositionOld` (Start) und `robot.motorPosition` (Ziel),
- liegt bereits vor: `robot.motorPositionOld` (Start) und `robot.motorPosition` (Ziel),
über `portValue()` in Port-Werte umgerechnet
über `portValue()` in Port-Werte umgerechnet
- [ ] Während der Bewegung periodisch `?` pollen (Paket 3) und je Controller berechnen:
- [ ] Aus den **auto-gepushten ** Reports (Paket 3, `$Report/Interval` ) je Controller berechnen
— **kein ** eigenes Polling:
```
```
fortschritt_i = |MPos_jetzt − Start_i| / |Ziel_i − Start_i| (auf 0…1 geklemmt)
fortschritt_i = |MPos_jetzt − Start_i| / |Ziel_i − Start_i| (auf 0…1 geklemmt)
` ``
` ``
@@ -114,11 +201,12 @@ also wie weit sich jeder Controller schon zur Ziel-Position bewegt hat (0…100
(der langsamste bestimmt, wann der Schritt fertig ist)
(der langsamste bestimmt, wann der Schritt fertig ist)
- im **Korrekt-Modus** (ToDo_6a) sollten alle Controller etwa gleich schnell fertig sein —
- 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)
eine große Spreizung der Einzel-Fortschritte ist dort ein Warnsignal (Feedrate-Fehler)
- [ ] **Lockstep-Gate (B5, nur bei ` ROBOT_MOTION_SYNC=lockstep`):** den nächsten Schritt erst
freigeben, wenn der langsamste Controller ` Idle`/Ziel erreicht hat. In ` freerun` entfällt
das Gate — die Koordination kommt allein aus der Feedrate (ToDo_6a ` correct`).
- [ ] Fortschritt + Status nach außen geben
- [ ] Fortschritt + Status nach außen geben
- über ` InfoServer` (` /api/status`) und/oder als WS-Push an die Clients
- über ` InfoServer` (` /api/status`) und/oder als WS-Push an die Clients
- ermöglicht eine Fortschrittsanzeige beim Abspielen von Dateien (ToDo_6b)
- 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?
### Offener Kernpunkt: Was, wenn währenddessen der nächste Befehl kommt?
@@ -146,12 +234,11 @@ ist. So bleibt die Bewegung flüssig (GRBL-Planner läuft nie leer), nichts geht
(kein Puffer-Überlauf), und das Netz wird nicht mit Dauer-Polling oder Befehls-Salven
(kein Puffer-Überlauf), und das Netz wird nicht mit Dauer-Polling oder Befehls-Salven
belastet.
belastet.
### Schalter (Pflicht — Absicherung wie bei ToDo_6a)
### Schalter
| Env | Default | Wirkung |
` ROBOT_USE_QUEUE` (siehe zentrale Schalter-Übersicht oben). ` false` = **exakt heutiges
|---|---|---|
Fire-and-forget**, byte-identisch zu vorher; ` true` = Queue-Logik aktiv. Wie ` ROBOT_SPEED_MODE`
| ` 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. |
greift der Umbau **nur** bei ` true` — das bestehende Sender-Test-Sicherheitsnetz bleibt gültig.
| ` ROBOT_USE_QUEUE` | ` true` | Neue zeitgesteuerte Queue-Logik aktiv. |
` ``yaml
` ``yaml
# docker-compose.yaml → appRobotDriver
# docker-compose.yaml → appRobotDriver
@@ -159,45 +246,37 @@ environment:
- ROBOT_USE_QUEUE=false # oder: true
- ROBOT_USE_QUEUE=false # oder: true
` ``
` ``
> Wie ` ROBOT_SPEED_MODE` greift der Umbau **nur** bei ` true`. Solange ` false`, ist der
### Takt: ` Bf` ist die Wahrheit, ` moveTime` nur der Vorhersage-Hint
> Sende-Pfad unverändert — das bestehende Sicherheitsnetz (Sender-Tests) bleibt gültig.
### Idee: zwei Uhren — eine ge schätzte (gratis), eine gemessene (kostet Netz)
Die Recherche ändert den ursprünglichen „r eine Zeit schätzung"-Entwurf: FluidNC liefert die
**echte** Pufferfüllung (` Bf`) ohnehin frei Haus über Auto-Reporting (Protokoll-Fakt 3+4).
Damit muss nichts mehr aufwändig geschätzt werden.
Der Trick, das Netz **nicht** zu überlasten: primär nach einer **geschätzten** Uhr takten,
- **` Bf`-basierter Haupttakt (echte Wahrheit, kein Extra-Traffic):** Aus den auto-gepushten
die ` ?`-Messung nur sparsam zur Korrektur einsetzen.
Reports (Paket 3) kennt der Treiber je Controller die freien Planner-Blöcke. Regel:
- **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
solange (freie Planner-Blöcke > schwelle) → nächsten Queue-Eintrag senden
nächster Send, wenn: jetzt >= controllerFreiAb − vorlauf
` ``
` ``
` vorlauf` = kleine Sicherheitsmarge, damit immer ~1 Move im GRBL-Planner wartet und die
Hält den Planner gefüllt (flüssige Bewegung) und kann **nie** überlaufen — das ist das
Bewegu ng nicht stockt. Das ist ein reiner Timer → **null Zusatz-Traffic** .
Standard-GRBL-Streami ng, nur mit gepushtem statt gepolltem Status .
- **` moveTime` als prädiktiver Zusatz (aus ToDo_6a):** zwischen zwei Reports überbrückt die
- **Gemessene Uhr (Korrektur, sparsam):** Die Schätzung driftet (Beschleunigung/Abbremsen
geschätzte Ausführzeit die Lücke (z. B. um zu entscheiden, ob *jetzt schon* der nächste
stecken nicht in ` dist/feedrate`, kurze Moves dauern real länger). Deshalb ab und zu —
Eintrag sinnvoll ist). Korrigiert wird die Schätzung laufend durch den nächsten ` Bf`-Push;
**nicht** bei jedem Move — per ` ?` (Paket 3) den echten Stand holen und ` controllerFreiAb`
sie ist nie die alleinige Quelle.
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
### Eintrag in der Queue
- [ ] Queue-Eintrag hält: geparster Befehl / Motorziel, ` moveTime` (geschätz t), die je
- [ ] Queue-Eintrag hält: geparster Befehl / Motorziel, ` moveTime` (Hin t), die je
Sender resultierenden G-Code-Strings, Status (` pending → sent → done`)
Sender resultierenden G-Code-Strings, Status (` pending → sent → done`)
- [ ] ` done` wird gesetzt durch geschätzte Uhr **oder** (falls gepollt) durch ` ?`=` Idle` am Ziel
- [ ] ` done` wird aus dem ` Bf`/` state`-Report abgeleitet (` Idle` am Ziel), nicht geraten
### Pacing-Schleife
### Pacing-Schleife
- [ ] je Controller ` controllerFreiAb` führen, Sendezeitpunkt aus ` moveTime − vorlauf` ableiten
- [ ] je Controller die freien Planner-Blöcke aus dem letzten Report führen; senden, solange
- [ ] Tiefe im GRBL-Planner begrenzen (z. B. ≤ 1– 2 vorausgesendete Moves) — flüssig, aber
über Schwelle — In-Flight-Tiefe damit implizit begrenzt ( flüssig, aber steuerbar)
noch steuerbar
- [ ] ` moveTime` nur zur Überbrückung zwischen Reports nutzen
- [ ] Drift-Korrektur per ratenbegrenztem ` ? `; bei großer Abweichung Schätzung neu setzen
- [ ] **Sicherheitsboden:** zusätzlich freie RX-Bytes aus ` Bf ` beachten (zweite Zahl) — nie
- [ ] **Optionaler Sicherheitsboden:** zusätzlich Character-Counting (Summe ungequittierter
mehr senden, als in den RX-Puffer passt (Character-Counting fällt damit faktisch ab)
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"
### Verhalten bei „neuer Befehl kommt mitten in der Bewegung"
@@ -224,14 +303,32 @@ Zwei Betriebsarten — je nach Quelle der Befehle:
- [ ] **Alarm/Error** mitten in der Queue (Paket 1) → Queue leeren, Senden stoppen, Fehler melden
- [ ] **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
- [ ] **Sync-Command (Paket 4)** bei nicht-leerer Queue → erst Queue leeren; die gepufferten
Ziele sind nach einem Re-Homing veraltet
Ziele sind nach einem Re-Homing veraltet
- [ ] **Drei Controller driften auseinander:** Im Korrekt-Modus (ToDo_6a) laufen sie auf
- [ ] **Lockstep vs. Freerun (B5):** In ` freerun` taktet jeder Controller eigenständig nach
dieselbe ` moveTime` → ihre ` controllerFreiAb` sollten zusammenbleiben; große Spreizung is t
seinem ` Bf`. In ` lockstep` kommt zusätzlich das Gate aus Paket 5 dazu — der nächste Schrit t
ein Warnsignal (Feedrate-/Schätzfehler)
wird erst gesendet, wenn der *langsamste* Controller ` Idle`/Ziel meldet.
- [ ] **Schätzgüte:** ` moveTime = dist/feedrate` ignoriert Beschleunigung; kurze Moves dauern
- [ ] **Drei Controller driften auseinander:** Im Korrekt-Modus (ToDo_6a) sollten die ` Bf`-Stände
real länger. Vorlauf-Marge muss das abfangen; später ggf. Trapez-Profil-Schätzung verfeinern
zusammenbleiben; große Spreizung ist ein Warnsignal (Feedrate-Fehler) — in ` lockstep` zudem
ein Auslöser, härter zu warten.
- [ ] **` moveTime` nur Hint:** Da ` Bf` die echte Wahrheit liefert, ist die Schätzungenauigkeit
(` dist/feedrate` ignoriert Beschleunigung) unkritisch — sie überbrückt nur die Lücke zwischen
zwei Reports. Keine Trapez-Profil-Verfeinerung nötig.
---
---
## Hinweis zur Implementierung
## 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.
- **Aktiver Pfad:** Gelesen und konfiguriert (` $10`, ` $Report/Interval`) wird auf dem
produktiv genutzten ` robot/TelnetSenderGRBL.js` (Telnet-Kanal) — der ` data`-Handler dort
ist die zentrale Stelle (Paket 1).
- ` robot/fluidnc/FluidNCClient.js` (bidirektionale WebSocket-Anbindung, Port 81, Reconnect +
` EventEmitter`) ist eine *alternative* Anbindung. Nicht der aktive Pfad, kann aber bei
` ToDo_2` (Sender-Interface) als Referenz für das Event-Modell mit evaluiert werden.
## Reihenfolge / Abhängigkeiten
1. **Paket 1** (lesen) ist die Basis für alles Weitere.
2. **Paket 3** (Auto-Report ` $10=3` + ` $Report/Interval`, ` MPos`/` Bf` parsen) liefert die Daten
für Paket 4, 5, 6.
3. **Baustein ** (Umkehr-Kinematik inkl. B3-Zweigwahl) → dann **Paket 4 ** (Sync, async).
4. **Paket 5 ** (Fortschritt) und **Paket 6 ** (Queue) bauen auf Paket 3 auf;
**Freerun zuerst ** , **Lockstep ** (B5) erst danach.