# ToDo 9 — Hardware-Feedback-Loop ## Ziel Der Roboter-Treiber soll nicht nur Befehle senden, sondern auch Antworten der Hardware lesen. Nur so können Fehler erkannt, Positionen verifiziert und Befehlsfolgen zuverlässig synchronisiert werden. Aktuell ist der Datenfluss vollständig blind: ``` WebSocket → GCode → calculateAngles3D → sendCommand → tSocket.write() ↓ GRBL antwortet mit "ok" / "error" → 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: `` 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.** *Aktualisiert nach der Analyse (ToDo_9a):* Die **Port→Motor**-Rückrechnung, die der Sync braucht, ist linear und **eindeutig** — keine Zweig-Wahl nötig. Die Ellbogen-oben/unten- Mehrdeutigkeit betrifft nur die **kartesische** Inverskinematik `calculateAngles3D()` (Pose → Gelenkwinkel), die der Sync nicht verwendet. Falls dort je eine Disambiguierung gebraucht wird, gilt die physikalische Zusatzbedingung („Ellbogen höher als Hand" bzw. „hinter der x-Achse"). **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 | | `ROBOT_GRBL_AUTOREPORT` | `false` / `true` | `false` | **umgesetzt (Paket 3).** Schreibt beim Connect `$10=3` + `$Report/Interval` in die persistenten FluidNC-Settings. Default AUS → ohne Flag wird NICHTS an die Hardware geschrieben; geparst werden nur die Antworten des ohnehin laufenden `?`-Heartbeats. | | `ROBOT_GRBL_REPORT_INTERVAL` | ms (Zahl) | `200` | **umgesetzt (Paket 3).** Intervall für `$Report/Interval`, nur wirksam bei `ROBOT_GRBL_AUTOREPORT=true`. | > **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 — ✅ ERLEDIGT > Umgesetzt in `robot/TelnetSenderGRBL.js`. Der vorher blinde Kanal wird gelesen: > `tSocket.on('data', …)` → `_handleIncomingData()` (zeilen-gepuffert, fragmentierungs- > sicher) → `_handleResponseLine()` (demultiplext nach Typ). Tests: > `test/Sender.Telnet.responseParsing.test.js`. - [x] `connection.on('data', …)` durch echtes Lesen ersetzt - `ok` → `lastOk`-Zeitstempel; `error:`/`ALARM:` → `lastError` + Log - `<…>`-Reports → `_parseStatusReport()` (siehe Paket 3) - **wirft nie** (try/catch um den data-Handler) — ein Parsefehler darf den Prozess nicht abreißen - [x] Fehlerantworten nach außen meldbar gemacht - über `getStatus()` (`lastError`) → `InfoServer` `/api/status`; kein EventEmitter-Umbau nötig - der raw-`socket.on('data')`-Handler für `_lastDataAt` (Heartbeat-Liveness) bleibt unverändert > **Bewusst nicht hier:** ein `ok`-Handshake / Sende-Queue (das ist Paket 2 / Paket 6). > Paket 1 *liest und meldet* nur — der Sende-Pfad bleibt unangetastet (fire-and-forget). ## 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 - 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) → in Paket 6 über die zweite `Bf`-Zahl (freie RX-Bytes) abgedeckt - [ ] Timeout für ausbleibende `ok`-Antworten definieren - nach X ms ohne Antwort: Fehler loggen, ggf. Verbindung zurücksetzen ## Paket 3: Hardwareposition lesen (Auto-Report statt Polling) — ✅ weitgehend erledigt > Report-Parsing + InfoServer-Anbindung umgesetzt. Die Settings-Writes (`$10`, > `$Report/Interval`) sind **opt-in** (Env `ROBOT_GRBL_AUTOREPORT`, Default AUS), weil sie > persistente FluidNC-NVS-Settings schreiben und auf der eingesetzten FluidNC-Version > verifiziert werden müssen. Tests: `test/Sender.Telnet.responseParsing.test.js`, > `test/InfoServer.test.js`. - [x] Beim Verbindungsaufbau je Controller konfigurieren — **opt-in** (`_configureAutoReport()`): - `$10=3` → Report enthält `MPos` **und** `Bf` (Protokoll-Fakt 2) - `$Report/Interval=N` (Default `N=200`) → FluidNC **pusht** den Status selbst (Protokoll-Fakt 4) - **nur** wenn `ROBOT_GRBL_AUTOREPORT=true`; ohne Flag bleibt der `?`-Heartbeat (alle 10 s) die einzige Statusquelle — ausreichend für Anzeige, kein Schreibzugriff auf die Hardware - [x] `data`-Handler (Paket 1) parst die `<…>`-Reports: `state`, `MPos`/`WPos`, `Bf` - robust gegen Cross-Channel-Fremdzeilen (Protokoll-Fakt 5) — nach Typ demultiplext, fremde Zeilen werden ignoriert; zerstörte Felder lassen den alten Wert stehen - [ ] **Offen (→ Paket 4):** Gemeldete `MPos` mit Softwareposition vergleichen / bei Abweichung warnen. Bewusst aufgeschoben: der Vergleich braucht die `motorStateFromPorts()`-Rückrechnung (ToDo_9a) und eine Roboter-Referenz im Sender — beides gehört zum Sync-Command (Paket 4). Der Sender liest und meldet die Hardwareposition bereits; der *Abgleich* fehlt noch. - [x] Status (`Idle`/`Run`/`Alarm`/`Hold`) + `MPos` + `Bf` für den `InfoServer` bereitgestellt - `getStatus()` um `grblState`, `machinePosition`, `plannerBlocksFree`, `rxBytesFree`, `lastError`, `lastReportAt` erweitert; `/api/status` reicht sie durch --- ## Baustein für Paket 4 + 5: Rückabbildung Port → Motorwerte — ✅ DURCHGERECHNET > **Erledigt als Analyse.** Vollständige Herleitung: **`doc/ToDo_9a_PortRueckrechnung.md`**. > Verifikation: **`test/Robot.PortInverse.test.js`** (15 Tests grün). 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`). **Ergebnis:** Für die produktive Verkabelung (`startRobot.js`) ist die Abbildung Motorwerte → gesendete GRBL-Achswerte **linear und eindeutig umkehrbar** — auf Port-Ebene gibt es **keine** Mehrdeutigkeit. `factorTurnLift`/`handOpenInMM` kommen in der produktiven Verkabelung gar nicht vor (nur in nicht-genutzten `portValue`-Zweigen). ```js // D = 180/π ; r = { base:{x,y,z}, elbow:{x}, hand:{x,y,z} } xMotor = r.base.x alpha = r.base.y / D beta = (r.base.z + r.base.y) / D a = r.elbow.x / D b = r.hand.z / D c = (r.hand.x + r.hand.z) / D eMotor = r.hand.y / D ``` > **B3 ist hier kein Thema.** Die Ellbogen-oben/unten-Mehrdeutigkeit steckt allein in der > kartesischen Inverskinematik `calculateAngles3D()` (Pose → Gelenkwinkel). Der Sync nutzt > diese Richtung nicht — er geht `MPos → Motorwerte → Vorwärtskinematik → Pose`, beide > Schritte eindeutig. Der gesamte Sync-Pfad ist damit eindeutig. Umsetzung (Paket 4) — ✅ ERLEDIGT: - [x] `motorStateFromPorts()` in den Produktiv-Code gehoben: **`robot/portInverse.js`**. `test/Robot.PortInverse.test.js` importiert jetzt diese Funktion (statt inline) — die 15 Verifikations-Tests sind damit der Dauer-Guard für den Produktivcode. - [x] Im Sync verdrahtet: `RobotController.syncFromHardware()` ruft `motorStateFromPorts()`. Die Round-Trip-Invariante ist über `test/Robot.PortInverse.test.js` (A/B/C) abgedeckt. > 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) — ✅ ERLEDIGT **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. > Umgesetzt. Befehl: **`M114 R`**. Pfad: `GCodeParser` → `RobotController.applyCommand` > (M114-R-Branch) → `RobotController.syncFromHardware()` (async). Tests: > `test/RobotController.sync.test.js`, `test/Sender.Telnet.responseParsing.test.js` > (requestStatusReport). **Datenquelle: aktiv `?` + await** (gewählt; funktioniert auch > ohne Auto-Report). - [x] **G-Code-Befehl** `M114 R` — durch `GCodeParser` + `RobotController` geroutet - `GCodeParser` erhält jetzt Flag-Token ohne Wert (`R` → `params.R = true`) - `GCode.containsCommand('M114 …')` erkennt es; das bestehende exakte `M114` (Software- Position) in `InputWS` bleibt unberührt (wird vorher per `=== "M114"` abgefangen) - [x] **Async-Dispatch:** `applyCommand` gibt für `M114 R` ein Promise zurück; `receive` reicht es nur dann hoch (sonst synchron wie bisher). `InputWS` wartet auf das Promise und broadcastet erst danach die neue Pose; Fehler gehen als Fehler-Envelope an den Anfrager. **Alle anderen Befehle bleiben byte-identisch synchron.** - [x] Ablauf des Sync (`syncFromHardware`): 1. an `base`/`elbow`/`hand` je `requestStatusReport()` → sendet `?`, wartet auf `<…>` (Timeout 1 s) 2. `motorStateFromPorts(...)` → sieben Motorwerte (linear/eindeutig, ToDo_9a) 3. auf den Roboter schreiben (`robot.xMotor/alpha/beta/a/b/c/eMotor`) 4. `robot.calculatePositionFromMotorAngles()` → Pose `x/y/z` + `phi/theta/psi` 5. `motorPosition`/`motorPositionOld` auf `null` → nächster Move rechnet von der echten Position - [x] dem Client die übernommene Pose zurückmelden — als Broadcast von `getM114` (konsistent mit Bewegungen; erreicht auch die Simulation, nicht nur den Anfrager) - [x] **kein** automatisches Nachfahren — Sync ruft **kein** `sendCommand()`; es geht garantiert nichts an die Controller (per Test abgesichert: `execCommand` wird nie aufgerufen) - [x] **Guards:** fehlende Controller-Rolle, ungültige/zu kurze `MPos` und `?`-Timeout führen zu einem sauberen Fehler an den Client — **nie** zu Müll im Roboterzustand **Sender→Rolle-Zuordnung:** `startRobot.js` setzt `instance.controllerRole = key` (`base`/`elbow`/`hand`); `syncFromHardware` mappt darüber. Ändert sich die Verkabelung, muss `portInverse.js` mitgezogen werden — der Round-Trip-Test schützt davor. > 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 - [ ] 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) ``` - [ ] 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) - [ ] **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 - über `InfoServer` (`/api/status`) und/oder als WS-Push an die Clients - ermöglicht eine Fortschrittsanzeige beim Abspielen von Dateien (ToDo_6b) ### 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 `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` greift der Umbau **nur** bei `true` — das bestehende Sender-Test-Sicherheitsnetz bleibt gültig. ```yaml # docker-compose.yaml → appRobotDriver environment: - ROBOT_USE_QUEUE=false # oder: true ``` ### Takt: `Bf` ist die Wahrheit, `moveTime` nur der Vorhersage-Hint Die Recherche ändert den ursprünglichen „reine Zeitschä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. - **`Bf`-basierter Haupttakt (echte Wahrheit, kein Extra-Traffic):** Aus den auto-gepushten Reports (Paket 3) kennt der Treiber je Controller die freien Planner-Blöcke. Regel: ``` solange (freie Planner-Blöcke > schwelle) → nächsten Queue-Eintrag senden ``` Hält den Planner gefüllt (flüssige Bewegung) und kann **nie** überlaufen — das ist das Standard-GRBL-Streaming, nur mit gepushtem statt gepolltem Status. - **`moveTime` als prädiktiver Zusatz (aus ToDo_6a):** zwischen zwei Reports überbrückt die geschätzte Ausführzeit die Lücke (z. B. um zu entscheiden, ob *jetzt schon* der nächste Eintrag sinnvoll ist). Korrigiert wird die Schätzung laufend durch den nächsten `Bf`-Push; sie ist nie die alleinige Quelle. ### Eintrag in der Queue - [ ] Queue-Eintrag hält: geparster Befehl / Motorziel, `moveTime` (Hint), die je Sender resultierenden G-Code-Strings, Status (`pending → sent → done`) - [ ] `done` wird aus dem `Bf`/`state`-Report abgeleitet (`Idle` am Ziel), nicht geraten ### Pacing-Schleife - [ ] je Controller die freien Planner-Blöcke aus dem letzten Report führen; senden, solange über Schwelle — In-Flight-Tiefe damit implizit begrenzt (flüssig, aber steuerbar) - [ ] `moveTime` nur zur Überbrückung zwischen Reports nutzen - [ ] **Sicherheitsboden:** zusätzlich freie RX-Bytes aus `Bf` beachten (zweite Zahl) — nie mehr senden, als in den RX-Puffer passt (Character-Counting fällt damit faktisch ab) ### 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 - [ ] **Lockstep vs. Freerun (B5):** In `freerun` taktet jeder Controller eigenständig nach seinem `Bf`. In `lockstep` kommt zusätzlich das Gate aus Paket 5 dazu — der nächste Schritt wird erst gesendet, wenn der *langsamste* Controller `Idle`/Ziel meldet. - [ ] **Drei Controller driften auseinander:** Im Korrekt-Modus (ToDo_6a) sollten die `Bf`-Stände 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 - **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.