Files
appRobotDriver/doc/ToDo_9_HardwareFeedback.md
2026-06-14 10:32:31 +02:00

385 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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: `<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.** *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.