22 KiB
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', () => {})inrobot/TelnetSenderGRBL.js(Z. ~123). Alles in diesem ToDo hängt daran.
FluidNC-Protokoll: gesicherte Fakten (recherchiert)
Quellen: Serial Protocol, Automatic Reporting, Cross-Channel #750.
-
?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 bestehendetelnet-stream-Socket kann?senden und den<…>-Report zurücklesen. -
Statuswahl über
$10(Bitmaske). Bit0 =MPosstattWPos, Bit1 =Bf(Puffer). →$10=3liefertMPos+Bf(auf der eingesetzten FluidNC-Version verifizieren).MPosist die richtige Quelle für die Rückrechnung: der Treiber sendet dieportValue()-Werte als absolute G-Code-Koordinaten,MPosist offset-fest.WCO(Work Coordinate Offset) erscheint periodisch;WPos = MPos − WCO. Für uns irrelevant, solange wirMPoslesen.- Beispiel:
<Idle|MPos:151.000,149.000,-1.000|Bf:15,128|FS:0,0|WCO:12,28,78>
-
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 übermoveTimezu schätzen (das ignoriert Beschleunigung). -
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. -
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 dieappRobot_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=lockstepergibt nur mitROBOT_SPEED_MODE=correctSinn (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.
connection.on('data', …)durch echtes Lesen ersetztok→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
- 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
- über
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
okeingegangen 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 (EnvROBOT_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.
- Beim Verbindungsaufbau je Controller konfigurieren — opt-in (
_configureAutoReport()):$10=3→ Report enthältMPosundBf(Protokoll-Fakt 2)$Report/Interval=N(DefaultN=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
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
MPosmit Softwareposition vergleichen / bei Abweichung warnen. Bewusst aufgeschoben: der Vergleich braucht diemotorStateFromPorts()-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. - Status (
Idle/Run/Alarm/Hold) +MPos+Bffür denInfoServerbereitgestelltgetStatus()umgrblState,machinePosition,plannerBlocksFree,rxBytesFree,lastError,lastReportAterweitert;/api/statusreicht 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).
// 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 gehtMPos → Motorwerte → Vorwärtskinematik → Pose, beide Schritte eindeutig. Der gesamte Sync-Pfad ist damit eindeutig.
Umsetzung (Paket 4) — ✅ ERLEDIGT:
motorStateFromPorts()in den Produktiv-Code gehoben:robot/portInverse.js.test/Robot.PortInverse.test.jsimportiert jetzt diese Funktion (statt inline) — die 15 Verifikations-Tests sind damit der Dauer-Guard für den Produktivcode.- Im Sync verdrahtet:
RobotController.syncFromHardware()ruftmotorStateFromPorts(). Die Round-Trip-Invariante ist übertest/Robot.PortInverse.test.js(A/B/C) abgedeckt.
Hinweis: Gelesen wird auf dem aktiven Sender
TelnetSenderGRBL(imdata-Handler, siehe Paket 1) — nicht aufFluidNCClient.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).
- G-Code-Befehl
M114 R— durchGCodeParser+RobotControllergeroutetGCodeParsererhält jetzt Flag-Token ohne Wert (R→params.R = true)GCode.containsCommand('M114 …')erkennt es; das bestehende exakteM114(Software- Position) inInputWSbleibt unberührt (wird vorher per=== "M114"abgefangen)
- Async-Dispatch:
applyCommandgibt fürM114 Rein Promise zurück;receivereicht es nur dann hoch (sonst synchron wie bisher).InputWSwartet 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. - Ablauf des Sync (
syncFromHardware):- an
base/elbow/handjerequestStatusReport()→ sendet?, wartet auf<…>(Timeout 1 s) motorStateFromPorts(...)→ sieben Motorwerte (linear/eindeutig, ToDo_9a)- auf den Roboter schreiben (
robot.xMotor/alpha/beta/a/b/c/eMotor) robot.calculatePositionFromMotorAngles()→ Posex/y/z+phi/theta/psimotorPosition/motorPositionOldaufnull→ nächster Move rechnet von der echten Position
- an
- dem Client die übernommene Pose zurückmelden — als Broadcast von
getM114(konsistent mit Bewegungen; erreicht auch die Simulation, nicht nur den Anfrager) - kein automatisches Nachfahren — Sync ruft kein
sendCommand(); es geht garantiert nichts an die Controller (per Test abgesichert:execCommandwird nie aufgerufen) - Guards: fehlende Controller-Rolle, ungültige/zu kurze
MPosund?-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) undrobot.motorPosition(Ziel), überportValue()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 == IdleUNDMPos ≈ 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)
- Fertig, wenn alle drei Controller
- Lockstep-Gate (B5, nur bei
ROBOT_MOTION_SYNC=lockstep): den nächsten Schritt erst freigeben, wenn der langsamste ControllerIdle/Ziel erreicht hat. Infreerunentfällt das Gate — die Koordination kommt allein aus der Feedrate (ToDo_6acorrect). - 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)
- über
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:
- Fortschritt wird mehrdeutig.
motorPositionOld/motorPositionhalten 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. - 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.
# 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: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.solange (freie Planner-Blöcke > schwelle) → nächsten Queue-Eintrag sendenmoveTimeals 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ächstenBf-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) donewird aus demBf/state-Report abgeleitet (Idleam 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)
moveTimenur zur Überbrückung zwischen Reports nutzen- Sicherheitsboden: zusätzlich freie RX-Bytes aus
Bfbeachten (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
freeruntaktet jeder Controller eigenständig nach seinemBf. Inlockstepkommt zusätzlich das Gate aus Paket 5 dazu — der nächste Schritt wird erst gesendet, wenn der langsamste ControllerIdle/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) — inlockstepzudem ein Auslöser, härter zu warten. moveTimenur Hint: DaBfdie echte Wahrheit liefert, ist die Schätzungenauigkeit (dist/feedrateignoriert 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 genutztenrobot/TelnetSenderGRBL.js(Telnet-Kanal) — derdata-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 beiToDo_2(Sender-Interface) als Referenz für das Event-Modell mit evaluiert werden.
Reihenfolge / Abhängigkeiten
- Paket 1 (lesen) ist die Basis für alles Weitere.
- Paket 3 (Auto-Report
$10=3+$Report/Interval,MPos/Bfparsen) liefert die Daten für Paket 4, 5, 6. - Baustein (Umkehr-Kinematik inkl. B3-Zweigwahl) → dann Paket 4 (Sync, async).
- Paket 5 (Fortschritt) und Paket 6 (Queue) bauen auf Paket 3 auf; Freerun zuerst, Lockstep (B5) erst danach.