diff --git a/EmergencyStopButton/EmergencyStopButton.md b/EmergencyStopButton/EmergencyStopButton.md
new file mode 100644
index 0000000..f860aad
--- /dev/null
+++ b/EmergencyStopButton/EmergencyStopButton.md
@@ -0,0 +1,86 @@
+# Emergency Stop Button — Erkenntnisse & Entscheidungen
+
+## Hardware-Wahl
+
+### ESP32-C3 Super Mini — abgelehnt
+Das kompakte Board hat **keinen Laderegler** (kein TP4056/MCP73831, kein JST-Akku-Anschluss).
+Zudem zieht eine dauerhaft leuchtende Power-LED 1–2 mA — selbst ohne WLAN-Betrieb würde ein kleiner Akku in wenigen Tagen leer sein.
+
+### DFRobot FireBeetle 2 — gewählt
+- Integrierter Laderegler + JST-PH-2.0-Anschluss direkt am Board
+- Low-Power optimiert (ab Werk ~15 µA im Deep Sleep)
+- Kein Zusatz-Hardware nötig für Akkubetrieb
+
+## Akku-Spezifikation für den FireBeetle 2
+
+Suchbegriffe:
+- `LiPo Akku 3.7V JST PH2.0 2000mAh Schutzschaltung`
+- `Li-Po 1S 3.7V protected JST-PH 2.0mm 2000mAh`
+
+Pflichtmerkmale:
+| Merkmal | Wert |
+|---|---|
+| Typ | LiPo / Li-Polymer, **1S** (1 Zelle) |
+| Spannung | **3,7 V** nominal |
+| Stecker | **JST PH, 2,0 mm Raster, 2-polig** |
+| Schutzschaltung | **Ja** (BMS/PCM) |
+| Kapazität | **2000 mAh** |
+
+> **Achtung:** Polarität vor dem Einstecken mit Multimeter prüfen — JST-PH-Stecker sind nicht normiert. Sicherste Option: Akku direkt bei DFRobot kaufen.
+
+## Architektur-Entscheidung: WiFi Light Sleep (Priorität: 250 ms Latenz)
+
+Die **250 ms Latenz** vom Knopfdruck bis zum API-Call ist das primäre Ziel.
+
+| Option | Latenz | Ø Strom | Laufzeit (2000 mAh) |
+|---|---|---|---|
+| **WiFi Light Sleep (DTIM=10)** | **150–250 ms** ✅ | ~1 mA | **~80 Tage** |
+| Deep Sleep + Reconnect | 600–1300 ms ❌ | ~0,02 mA | ~mehrere Jahre |
+
+Deep Sleep scheidet aus: Der WiFi-Reconnect nach dem Aufwachen dauert 600–1300 ms — die 250-ms-Anforderung wird klar verfehlt.
+
+## WiFi Light Sleep — Funktionsprinzip
+
+Die CPU schläft, der WiFi-Stack bleibt aktiv. Mit DTIM=10 wacht der ESP32 alle ~1000 ms für 1–2 ms auf, um gepufferte Pakete vom Router abzuholen. Die Verbindungsassoziation bleibt erhalten.
+
+Ein **GPIO-Interrupt** (Leitung auf GND) weckt den ESP32 in **1–5 ms** — der API-Call kann sofort abgesetzt werden, weil WiFi bereits verbunden ist.
+
+## Latenzbudget
+
+| Schritt | Zeit |
+|---|---|
+| Wakeup aus Light Sleep | 1–5 ms |
+| WiFi-Verbindung prüfen (bereits aktiv) | 0 ms |
+| HTTP-Request aufbauen | 20–50 ms |
+| TLS-Handshake (HTTPS) | 50–150 ms |
+| Server-Antwort | 20–50 ms |
+| **Gesamt** | **~100–250 ms** ✅ |
+
+## Akkulaufzeit (WiFi Light Sleep, 2000 mAh)
+
+```
+Durchschnittsstrom (DTIM=10, Taster selten gedrückt): ~1 mA
+Nutzbare Kapazität (80 %): 1600 mAh
+Selbstentladung LiPo: ~2 mAh/Tag
+
+Laufzeit ≈ 1600 mAh / 1 mA ≈ 1600 h ≈ 67–80 Tage
+```
+
+> Zum Vergleich: Mit 1000 mAh (alter Stand) waren es ~40 Tage.
+
+## GPIO Wake-Up — technische Details
+
+- **Pegel-Trigger** (kein Flanken-Trigger): die Leitung muss >ein paar ms auf GND bleiben.
+- **Pull-Up intern** aktivieren: im Ruhezustand HIGH, Ereignis zieht auf GND.
+- **Wake-fähige Pins** sind nur RTC/LP-GPIOs — im FireBeetle-2-Datenblatt prüfen.
+- API: `esp_sleep_enable_gpio_wakeup()` / `esp_light_sleep_start()`
+
+## Vergleich: Alternative Deep-Sleep-Architektur (nicht für E-Stop geeignet)
+
+Falls in einem anderen Projekt Latenz < 1 s ausreicht und Akkulaufzeit Monate betragen soll:
+
+- Deep Sleep, WLAN nur 8–18 Uhr alle 30 min (20 WLAN-Verbindungen/Tag)
+- Zusätzlich GPIO-Wake für Ereignisse (innerhalb ~300 ms nach Aufwachen + Reconnect)
+- Laufzeit 2000 mAh: **~8–10 Monate** (Selbstentladung dominant)
+
+Für den Emergency Stop Button ist diese Option **nicht geeignet**, da die WiFi-Reconnect-Zeit die 250-ms-Anforderung überschreitet.
diff --git a/EmergencyStopButton/eStopESP32.tex b/EmergencyStopButton/eStopESP32.tex
index 809c9aa..0818930 100644
--- a/EmergencyStopButton/eStopESP32.tex
+++ b/EmergencyStopButton/eStopESP32.tex
@@ -41,7 +41,7 @@ Tastendruck geweckt.
\begin{itemize}
\item Latenz Knopfdruck $\rightarrow$ API-Call: \textbf{< 250\,ms}
- \item Stromversorgung: LiPo 1000\,mAh (kabellos, batteriebetrieben)
+ \item Stromversorgung: LiPo 2000\,mAh (kabellos, batteriebetrieben)
\item Möglichst lange Akkulaufzeit (Taster wird selten gedrückt, $\leq$1\,×/h)
\item Einfache, wartungsarme Architektur
\end{itemize}
@@ -105,13 +105,17 @@ DTIM & Wakeup-Intervall & Ø Strom \\
Empfohlen: \texttt{DTIM=10} -- sparsamste Option, Verbindung bleibt stabil.
-\subsection{Akkulaufzeit (1000 mAh LiPo)}
+\subsection{Akkulaufzeit (2000 mAh LiPo)}
-Bei $\approx$1\,mA Durchschnittsstrom (DTIM=10, selten gedrückt):
+Bei $\approx$1\,mA Durchschnittsstrom (DTIM=10, selten gedrückt) und 80\,\% nutzbarer Kapazität:
\[
-t = \frac{1000~\mathrm{mAh}}{1~\mathrm{mA}} \approx 40~\mathrm{Tage}
+t = \frac{1600~\mathrm{mAh}}{1~\mathrm{mA}} \approx 67\text{--}80~\mathrm{Tage}
\]
+Die \textbf{Latenz von 250\,ms} hat Vorrang vor maximaler Akkulaufzeit -- Deep Sleep scheidet
+daher aus (WiFi-Reconnect 600--1300\,ms). Mit 2000\,mAh und WiFi Light Sleep sind
+ca.\ 2,5 Monate Betrieb ohne Laden realistisch.
+
% -----------------------------------------------
\section{Latenzbudget}
@@ -148,6 +152,27 @@ Adafruit HUZZAH32 Feather & MCP73831 & $\approx$20\,€ & gute Dokumentation \\
\end{tabular}
\end{center}
+\subsection{Akku-Spezifikation}
+
+Für den FireBeetle\,2 wird ein \textbf{1S LiPo mit JST-PH-2,0-Stecker und BMS} benötigt:
+
+\begin{center}
+\begin{tabular}{ll}
+\toprule
+Merkmal & Wert \\
+\midrule
+Typ & LiPo / Li-Polymer, 1 Zelle (1S) \\
+Nennspannung & 3{,}7\,V (voll: 4{,}2\,V) \\
+Stecker & JST PH, 2{,}0\,mm Raster, 2-polig \\
+Schutzschaltung & Ja (BMS/PCM) -- Pflicht bei Deep-Sleep-Betrieb \\
+Kapazität & 2000\,mAh \\
+\bottomrule
+\end{tabular}
+\end{center}
+
+\textbf{Achtung Polarität:} JST-PH-Stecker sind nicht normiert -- Polung vor dem
+Einstecken mit Multimeter prüfen. Sicherste Quelle: Akku direkt bei DFRobot kaufen.
+
\subsection{Schaltung}
\begin{lstlisting}
@@ -155,7 +180,7 @@ ESP32 Taster
GPIO 9 ---- [Taster] ---- GND
(interner Pull-Up aktiv: HIGH = offen, LOW = gedrueckt)
-LiPo 1000mAh ---- BAT+ / BAT- des Boards
+LiPo 2000mAh (JST PH 2.0, mit BMS) ---- BAT+ / BAT- des Boards
\end{lstlisting}
Kein weiteres Bauteil nötig -- der interne Pull-Up des ESP32 reicht.
diff --git a/README.md b/README.md
index 94bf0b5..845cb2e 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,17 @@
Dieses Projekt empfängt G-Code und Robotersteuerbefehle, berechnet Inverse Kinematik für einen mehrgliedrigen Roboterarm und leitet die resultierenden Achsenbefehle an mehrere GRBL/FluidNC-Telnet-Sender weiter.
+## Einbindung ins Projekt
+
+Der Driver steht zwischen Eingabe-Programmen (appInput oder appAutomasiation oder jede beliebige andere Eingabeform)
+welche die Eingabe steuert. Die Eingabe wird eben an den Driver weitergegeben. Hier im Driver werden die Welt-Koordinaten
+in Motor-Koordinaten umgerechnet, es werden die Motoren angesteuert.
+
+
+
+Die Motor--Steuerung erfolgt (momentan) per GCode an FluidNC Driver--Boards. Diese arbeiten jeweils mit
+Motor-Koordinaten in den X, Y und Z-Achsen. Es werden mehrere FluidNC Boards unterstützt.
+
## Architektur
- `startRobot.js` startet zwei HTTPS-Server:
@@ -28,7 +39,7 @@ Die Eingaben kommen per WebSocket an den HTTPS-Server und werden in `server/Inpu
- Antwort: Positionsdaten des Roboters im JSON-Format.
- G-Code-Befehle:
- `G90`, `G91`, `G1`, `G28`
- - `G92` (wird intern als `M92` verarbeitet — setzt Motorposition ohne Bewegung)
+ - `G92` setzt die Motorposition ohne Bewegung. Winkel (`Y`,`Z`,`A`,`B`,`C`) in **Grad** (G-Code-Konvention, wie FluidNC); `X` in mm; `E` = Greifer-Öffnung in mm (ab Null-Position eines Fingers), wird intern über die Greifer-Kopplung in `eMotor` umgerechnet. (`M92` macht dasselbe, erwartet die Winkel aber roh in **Radiant**.)
- Messungen in `X`, `Y`, `Z`, `A`, `B`, `C`, `E`, `F`
- `M1` für direkte Motor-Koordinaten
- FCodes (Datei-/Programm-Befehle) — werden durch den Driver an `appRobotFileservice` weitergeleitet:
diff --git a/doc/Server_to_Robot.svg b/doc/Server_to_Robot.svg
new file mode 100644
index 0000000..c30153c
--- /dev/null
+++ b/doc/Server_to_Robot.svg
@@ -0,0 +1,553 @@
+
+
+
+
diff --git a/doc/SoftwareModularisation.svg b/doc/SoftwareModularisation.svg
new file mode 100644
index 0000000..d9cb681
--- /dev/null
+++ b/doc/SoftwareModularisation.svg
@@ -0,0 +1,321 @@
+
+
+
+
diff --git a/logs/gcode_commands.log b/logs/gcode_commands.log
index 8b9cdd2..5ef0fbf 100644
--- a/logs/gcode_commands.log
+++ b/logs/gcode_commands.log
@@ -10809,3 +10809,205 @@
2026-06-14T11:37:52.055Z ::ffff:127.0.0.1: M114
2026-06-14T11:37:52.277Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T11:37:52.509Z ::ffff:127.0.0.1: G1 X1
+2026-06-25T16:03:59.536Z ::ffff:127.0.0.1: FList
+2026-06-25T16:03:59.571Z ::ffff:127.0.0.1: FPlus
+2026-06-25T16:03:59.593Z ::ffff:127.0.0.1: FLoad nichtda
+2026-06-25T16:03:59.614Z ::ffff:127.0.0.1: FShow
+2026-06-25T16:03:59.621Z ::ffff:127.0.0.1: M114
+2026-06-25T16:03:59.637Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:03:59.836Z ::ffff:127.0.0.1: M114
+2026-06-25T16:04:00.049Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:04:00.282Z ::ffff:127.0.0.1: G1 X1
+2026-06-25T16:04:11.315Z ::ffff:127.0.0.1: FList
+2026-06-25T16:04:11.353Z ::ffff:127.0.0.1: FPlus
+2026-06-25T16:04:11.356Z ::ffff:127.0.0.1: M114
+2026-06-25T16:04:11.369Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:04:11.371Z ::ffff:127.0.0.1: FLoad nichtda
+2026-06-25T16:04:11.382Z ::ffff:127.0.0.1: FShow
+2026-06-25T16:04:11.508Z ::ffff:127.0.0.1: M114
+2026-06-25T16:04:11.722Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:04:11.953Z ::ffff:127.0.0.1: G1 X1
+2026-06-25T16:04:20.552Z ::ffff:127.0.0.1: M114
+2026-06-25T16:04:20.569Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:04:20.569Z ::ffff:127.0.0.1: FList
+2026-06-25T16:04:20.605Z ::ffff:127.0.0.1: FPlus
+2026-06-25T16:04:20.617Z ::ffff:127.0.0.1: FLoad nichtda
+2026-06-25T16:04:20.634Z ::ffff:127.0.0.1: FShow
+2026-06-25T16:04:20.771Z ::ffff:127.0.0.1: M114
+2026-06-25T16:04:20.998Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:04:21.222Z ::ffff:127.0.0.1: G1 X1
+2026-06-25T16:08:34.871Z ::ffff:127.0.0.1: M114
+2026-06-25T16:08:34.888Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:08:34.888Z ::ffff:127.0.0.1: FList
+2026-06-25T16:08:34.915Z ::ffff:127.0.0.1: FPlus
+2026-06-25T16:08:34.926Z ::ffff:127.0.0.1: FLoad nichtda
+2026-06-25T16:08:34.936Z ::ffff:127.0.0.1: FShow
+2026-06-25T16:08:35.051Z ::ffff:127.0.0.1: M114
+2026-06-25T16:08:35.264Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:08:35.494Z ::ffff:127.0.0.1: G1 X1
+2026-06-25T16:08:38.150Z ::ffff:127.0.0.1: FList
+2026-06-25T16:08:38.171Z ::ffff:127.0.0.1: M114
+2026-06-25T16:08:38.185Z ::ffff:127.0.0.1: FPlus
+2026-06-25T16:08:38.186Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:08:38.200Z ::ffff:127.0.0.1: FLoad nichtda
+2026-06-25T16:08:38.209Z ::ffff:127.0.0.1: FShow
+2026-06-25T16:08:38.344Z ::ffff:127.0.0.1: M114
+2026-06-25T16:08:38.558Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:08:38.789Z ::ffff:127.0.0.1: G1 X1
+2026-06-25T16:14:41.227Z ::ffff:127.0.0.1: FList
+2026-06-25T16:14:41.286Z ::ffff:127.0.0.1: FPlus
+2026-06-25T16:14:41.305Z ::ffff:127.0.0.1: FLoad nichtda
+2026-06-25T16:14:41.318Z ::ffff:127.0.0.1: FShow
+2026-06-25T16:14:41.333Z ::ffff:127.0.0.1: M114
+2026-06-25T16:14:41.350Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:14:41.531Z ::ffff:127.0.0.1: M114
+2026-06-25T16:14:41.755Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:14:41.992Z ::ffff:127.0.0.1: G1 X1
+2026-06-25T16:25:20.901Z ::ffff:127.0.0.1: M114
+2026-06-25T16:25:21.135Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:25:21.372Z ::ffff:127.0.0.1: G1 X1
+2026-06-25T16:25:21.445Z ::ffff:127.0.0.1: FList
+2026-06-25T16:25:21.476Z ::ffff:127.0.0.1: FPlus
+2026-06-25T16:25:21.491Z ::ffff:127.0.0.1: FLoad nichtda
+2026-06-25T16:25:21.490Z ::ffff:127.0.0.1: M114
+2026-06-25T16:25:21.502Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:25:21.505Z ::ffff:127.0.0.1: FShow
+2026-06-25T16:25:28.958Z ::ffff:127.0.0.1: M114
+2026-06-25T16:25:29.178Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:25:29.424Z ::ffff:127.0.0.1: G1 X1
+2026-06-25T16:25:29.442Z ::ffff:127.0.0.1: FList
+2026-06-25T16:25:29.470Z ::ffff:127.0.0.1: FPlus
+2026-06-25T16:25:29.482Z ::ffff:127.0.0.1: M114
+2026-06-25T16:25:29.485Z ::ffff:127.0.0.1: FLoad nichtda
+2026-06-25T16:25:29.492Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:25:29.498Z ::ffff:127.0.0.1: FShow
+2026-06-25T16:25:42.243Z ::ffff:127.0.0.1: M114
+2026-06-25T16:25:42.326Z ::ffff:127.0.0.1: FList
+2026-06-25T16:25:42.351Z ::ffff:127.0.0.1: FPlus
+2026-06-25T16:25:42.357Z ::ffff:127.0.0.1: M114
+2026-06-25T16:25:42.364Z ::ffff:127.0.0.1: FLoad nichtda
+2026-06-25T16:25:42.371Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:25:42.375Z ::ffff:127.0.0.1: FShow
+2026-06-25T16:25:42.465Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:25:42.689Z ::ffff:127.0.0.1: G1 X1
+2026-06-25T16:25:49.667Z ::ffff:127.0.0.1: FList
+2026-06-25T16:25:49.696Z ::ffff:127.0.0.1: FPlus
+2026-06-25T16:25:49.708Z ::ffff:127.0.0.1: M114
+2026-06-25T16:25:49.710Z ::ffff:127.0.0.1: FLoad nichtda
+2026-06-25T16:25:49.721Z ::ffff:127.0.0.1: FShow
+2026-06-25T16:25:49.723Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:25:49.886Z ::ffff:127.0.0.1: M114
+2026-06-25T16:25:50.103Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:25:50.331Z ::ffff:127.0.0.1: G1 X1
+2026-06-25T16:26:01.509Z ::ffff:127.0.0.1: M114
+2026-06-25T16:26:01.592Z ::ffff:127.0.0.1: FList
+2026-06-25T16:26:01.617Z ::ffff:127.0.0.1: M114
+2026-06-25T16:26:01.624Z ::ffff:127.0.0.1: FPlus
+2026-06-25T16:26:01.632Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:26:01.638Z ::ffff:127.0.0.1: FLoad nichtda
+2026-06-25T16:26:01.652Z ::ffff:127.0.0.1: FShow
+2026-06-25T16:26:01.744Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:26:01.980Z ::ffff:127.0.0.1: G1 X1
+2026-06-25T16:26:26.068Z ::ffff:127.0.0.1: M114
+2026-06-25T16:26:26.152Z ::ffff:127.0.0.1: FList
+2026-06-25T16:26:26.179Z ::ffff:127.0.0.1: M114
+2026-06-25T16:26:26.186Z ::ffff:127.0.0.1: FPlus
+2026-06-25T16:26:26.197Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:26:26.200Z ::ffff:127.0.0.1: FLoad nichtda
+2026-06-25T16:26:26.213Z ::ffff:127.0.0.1: FShow
+2026-06-25T16:26:26.297Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:26:26.533Z ::ffff:127.0.0.1: G1 X1
+2026-06-25T16:30:08.621Z ::ffff:127.0.0.1: M114
+2026-06-25T16:30:08.639Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:30:08.744Z ::ffff:127.0.0.1: M114
+2026-06-25T16:30:09.003Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:30:09.009Z ::ffff:127.0.0.1: FList
+2026-06-25T16:30:09.039Z ::ffff:127.0.0.1: FPlus
+2026-06-25T16:30:09.056Z ::ffff:127.0.0.1: FLoad nichtda
+2026-06-25T16:30:09.068Z ::ffff:127.0.0.1: FShow
+2026-06-25T16:30:09.256Z ::ffff:127.0.0.1: G1 X1
+2026-06-25T16:30:11.200Z ::ffff:127.0.0.1: M114
+2026-06-25T16:30:11.365Z ::ffff:127.0.0.1: FList
+2026-06-25T16:30:11.401Z ::ffff:127.0.0.1: FPlus
+2026-06-25T16:30:11.418Z ::ffff:127.0.0.1: FLoad nichtda
+2026-06-25T16:30:11.429Z ::ffff:127.0.0.1: M114
+2026-06-25T16:30:11.431Z ::ffff:127.0.0.1: FShow
+2026-06-25T16:30:11.433Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:30:11.447Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:30:11.664Z ::ffff:127.0.0.1: G1 X1
+2026-06-25T16:36:03.192Z ::ffff:127.0.0.1: FList
+2026-06-25T16:36:03.222Z ::ffff:127.0.0.1: FPlus
+2026-06-25T16:36:03.223Z ::ffff:127.0.0.1: M114
+2026-06-25T16:36:03.235Z ::ffff:127.0.0.1: FLoad nichtda
+2026-06-25T16:36:03.248Z ::ffff:127.0.0.1: FShow
+2026-06-25T16:36:03.479Z ::ffff:127.0.0.1: M114
+2026-06-25T16:36:08.260Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:36:08.503Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:36:13.731Z ::ffff:127.0.0.1: G1 X1
+2026-06-25T16:36:25.750Z ::ffff:127.0.0.1: M114
+2026-06-25T16:36:25.972Z ::ffff:127.0.0.1: M114
+2026-06-25T16:36:26.067Z ::ffff:127.0.0.1: FList
+2026-06-25T16:36:26.098Z ::ffff:127.0.0.1: FPlus
+2026-06-25T16:36:26.111Z ::ffff:127.0.0.1: FLoad nichtda
+2026-06-25T16:36:26.124Z ::ffff:127.0.0.1: FShow
+2026-06-25T16:36:30.778Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:36:30.998Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:36:36.241Z ::ffff:127.0.0.1: G1 X1
+2026-06-25T16:36:39.714Z ::ffff:127.0.0.1: FList
+2026-06-25T16:36:39.749Z ::ffff:127.0.0.1: FPlus
+2026-06-25T16:36:39.763Z ::ffff:127.0.0.1: FLoad nichtda
+2026-06-25T16:36:39.778Z ::ffff:127.0.0.1: FShow
+2026-06-25T16:36:39.813Z ::ffff:127.0.0.1: M114
+2026-06-25T16:36:39.830Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:36:40.002Z ::ffff:127.0.0.1: M114
+2026-06-25T16:36:40.224Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:36:45.463Z ::ffff:127.0.0.1: G1 X1
+2026-06-25T16:36:56.688Z ::ffff:127.0.0.1: FList
+2026-06-25T16:36:56.716Z ::ffff:127.0.0.1: FPlus
+2026-06-25T16:36:56.734Z ::ffff:127.0.0.1: FLoad nichtda
+2026-06-25T16:36:56.747Z ::ffff:127.0.0.1: FShow
+2026-06-25T16:36:56.884Z ::ffff:127.0.0.1: M114
+2026-06-25T16:36:56.892Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:36:56.972Z ::ffff:127.0.0.1: M114
+2026-06-25T16:36:57.212Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:36:57.462Z ::ffff:127.0.0.1: G1 X1
+2026-06-25T16:36:59.277Z ::ffff:127.0.0.1: M114
+2026-06-25T16:36:59.305Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:36:59.536Z ::ffff:127.0.0.1: FList
+2026-06-25T16:36:59.567Z ::ffff:127.0.0.1: FPlus
+2026-06-25T16:36:59.582Z ::ffff:127.0.0.1: FLoad nichtda
+2026-06-25T16:36:59.596Z ::ffff:127.0.0.1: FShow
+2026-06-25T16:36:59.793Z ::ffff:127.0.0.1: M114
+2026-06-25T16:37:00.013Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:37:00.237Z ::ffff:127.0.0.1: G1 X1
+2026-06-25T16:37:15.021Z ::ffff:127.0.0.1: M114
+2026-06-25T16:37:15.030Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:39:35.648Z ::ffff:127.0.0.1: M114
+2026-06-25T16:39:35.659Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:41:46.209Z ::ffff:127.0.0.1: M114
+2026-06-25T16:41:46.340Z ::ffff:127.0.0.1: FList
+2026-06-25T16:41:46.367Z ::ffff:127.0.0.1: FPlus
+2026-06-25T16:41:46.382Z ::ffff:127.0.0.1: FLoad nichtda
+2026-06-25T16:41:46.398Z ::ffff:127.0.0.1: FShow
+2026-06-25T16:41:46.399Z ::ffff:127.0.0.1: M114
+2026-06-25T16:41:46.416Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:41:46.437Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:41:46.671Z ::ffff:127.0.0.1: G1 X1
+2026-06-25T16:42:28.868Z ::ffff:127.0.0.1: M114
+2026-06-25T16:42:29.070Z ::ffff:127.0.0.1: FList
+2026-06-25T16:42:29.086Z ::ffff:127.0.0.1: FPlus
+2026-06-25T16:42:29.093Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:42:29.099Z ::ffff:127.0.0.1: FLoad nichtda
+2026-06-25T16:42:29.108Z ::ffff:127.0.0.1: FShow
+2026-06-25T16:42:29.211Z ::ffff:127.0.0.1: M114
+2026-06-25T16:42:29.223Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:42:29.326Z ::ffff:127.0.0.1: G1 X1
+2026-06-25T16:42:31.618Z ::ffff:127.0.0.1: M114
+2026-06-25T16:42:31.633Z ::ffff:127.0.0.1: FList
+2026-06-25T16:42:31.636Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:42:31.669Z ::ffff:127.0.0.1: FPlus
+2026-06-25T16:42:31.684Z ::ffff:127.0.0.1: FLoad nichtda
+2026-06-25T16:42:31.695Z ::ffff:127.0.0.1: FShow
+2026-06-25T16:42:31.758Z ::ffff:127.0.0.1: M114
+2026-06-25T16:42:31.991Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
+2026-06-25T16:42:32.218Z ::ffff:127.0.0.1: G1 X1
diff --git a/logs/pings.log b/logs/pings.log
index 59076ae..2c526b8 100644
--- a/logs/pings.log
+++ b/logs/pings.log
@@ -14762,3 +14762,49 @@
2026-06-14T11:37:48.506Z ::ffff:127.0.0.1 : Ping
2026-06-14T11:37:51.780Z ::ffff:127.0.0.1 : Ping
2026-06-14T11:37:51.825Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:03:59.576Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:03:59.604Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:04:11.272Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:04:11.329Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:04:20.515Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:04:20.538Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:08:34.823Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:08:34.834Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:08:38.112Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:08:38.138Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:14:41.281Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:14:41.295Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:25:20.653Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:25:21.462Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:25:28.700Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:25:29.460Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:25:41.995Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:25:42.332Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:25:49.649Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:25:49.680Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:26:01.271Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:26:01.590Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:26:25.831Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:26:26.148Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:30:08.515Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:30:08.577Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:30:10.959Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:30:11.400Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:36:03.194Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:36:03.235Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:36:25.716Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:36:25.728Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:36:39.770Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:36:39.778Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:36:56.756Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:36:56.869Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:36:59.231Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:36:59.560Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:37:15.012Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:39:35.638Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:41:45.963Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:41:46.370Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:42:28.634Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:42:29.198Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:42:31.500Z ::ffff:127.0.0.1 : Ping
+2026-06-25T16:42:31.578Z ::ffff:127.0.0.1 : Ping
diff --git a/public/app.js b/public/app.js
index 406241f..d6bbe3e 100644
--- a/public/app.js
+++ b/public/app.js
@@ -20,7 +20,8 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById('state-theta').textContent = fmt(p.b*180/Math.PI);
document.getElementById('state-psi').textContent = fmt(p.c*180/Math.PI);
- document.getElementById('state-e').textContent = fmt(m.e*180/Math.PI);
+ // Greifer-Öffnung in mm (Workspace) — keine Grad-Umrechnung.
+ document.getElementById('state-e').textContent = fmt(p.e);
@@ -33,6 +34,7 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById('motor-b').textContent = fmt(m.b*180/Math.PI);
document.getElementById('motor-c').textContent = fmt(m.c*180/Math.PI);
+ // Greifer-Motorwert (eMotor, abgeleitet aus e/b/c) — roh, wie motor-x.
document.getElementById('motor-e').textContent = fmt(m.e);
})
diff --git a/robot/GCode.js b/robot/GCode.js
index d4741ca..a784d6c 100755
--- a/robot/GCode.js
+++ b/robot/GCode.js
@@ -40,20 +40,25 @@ class GCode{
static getM114(robot){
+ // position = Workspace (x/y/z in mm, a/b/c als Euler-Winkel phi/theta/psi in rad,
+ // e = Greifer-Öffnung in mm).
+ // motorCounts = die 7 Motor-Slots, inkl. e = eMotor (abgeleiteter Greifer-Motorwert,
+ // NICHT die mm-Öffnung — die steht in position.e).
let text = '{"position":{ "x":'+robot.x+
', "y":'+robot.y+
', "z":'+robot.z+
- ', "a":' +robot.phi +
- ', "b":' +robot.theta +
- ', "c":' +robot.psi + '},' +
+ ', "a":' +robot.phi +
+ ', "b":' +robot.theta +
+ ', "c":' +robot.psi +
+ ', "e":' +(robot.e ?? 0) + '},' +
'"motorCounts":{ "x":'+ robot.xMotor +
- ', "y":'+ robot.alpha +
+ ', "y":'+ robot.alpha +
', "z":'+ robot.beta +
- ', "a":'+ robot.a +
- ', "b":'+ robot.b +
+ ', "a":'+ robot.a +
+ ', "b":'+ robot.b +
', "c":'+ robot.c +
- ', "e":'+ (robot.e ?? 0) +
- '}}';
+ ', "e":'+ (robot.eMotor ?? 0) +
+ '}}';
return text;
}
diff --git a/robot/RobotBase.js b/robot/RobotBase.js
index 1d6af0c..913e8e4 100644
--- a/robot/RobotBase.js
+++ b/robot/RobotBase.js
@@ -264,6 +264,21 @@ class RobotBase{
};
}
+ /**
+ * Greifer-Öffnung (Workspace, `e` in mm) → Greifer-Motorwert (`eMotor`).
+ *
+ * Default: keine Kopplung (`eMotor = e`). Kinematiken, bei denen die Greifer-Sehne
+ * mechanisch durchs Handgelenk läuft, überschreiben dies, um die Handgelenk-Winkel
+ * herauszurechnen (siehe {@link Arm3SegmentLinearX}). Wird sowohl von
+ * `calculateAngles3D()` als auch beim Setzen per G92/M92 genutzt — eine Quelle.
+ *
+ * @param {number} e Finger-Öffnung in mm (ab Null-Position)
+ * @returns {number} zugehöriger Greifer-Motorwert
+ */
+ gripperMotorFromOpening(e) {
+ return e;
+ }
+
/**
* Rückwärts-Kinematik: Motorwinkel → Workspace-Koordinaten.
*
diff --git a/robot/RobotController.js b/robot/RobotController.js
index 638173e..ff26e95 100644
--- a/robot/RobotController.js
+++ b/robot/RobotController.js
@@ -8,7 +8,7 @@
* der Controller kennt nur strukturierte Befehle, keine rohen Textstrings.
*/
const GCodeParser = require('./GCodeParser');
-const { motorStateFromPorts } = require('./portInverse');
+const { motorStateFromPorts, D } = require('./portInverse');
class RobotController {
@@ -121,14 +121,31 @@ class RobotController {
}
if (cmd === 'M92' || cmd === 'G92') {
+ // Beide setzen die Motorposition ohne Bewegung, unterscheiden sich aber in den
+ // Winkel-EINHEITEN:
+ // G92 → GRAD (G-Code-Konvention für Rotationsachsen, wie FluidNC und die
+ // "Position Motoren"-Anzeige in public/app.js). Intern sind die
+ // Winkel-Slots in Radiant → Grad/D umrechnen (D = 180/π).
+ // M92 → RADIANT, roh in die internen Slots (interne/Test-Variante).
+ // X ist die lineare mm-Schiene, E die Greifer-Öffnung in mm (ab Null-Position
+ // eines Fingers) — beide ohne Winkel-Umrechnung.
+ const angScale = (cmd === 'G92') ? 1 / D : 1;
robot.createMotorPosition();
- if (Number.isFinite(params.X)) { robot.xMotor = params.X; robot.xMotorChanged = true; }
- if (Number.isFinite(params.Y)) { robot.alpha = params.Y; robot.yMotorChanged = true; }
- if (Number.isFinite(params.Z)) { robot.beta = params.Z; robot.zMotorChanged = true; }
- if (Number.isFinite(params.A)) { robot.a = params.A; robot.aMotorChanged = true; }
- if (Number.isFinite(params.B)) { robot.b = params.B; robot.bMotorChanged = true; }
- if (Number.isFinite(params.C)) { robot.c = params.C; robot.cMotorChanged = true; }
- if (Number.isFinite(params.E)) { robot.e = params.E; robot.eMotorChanged = true; }
+ if (Number.isFinite(params.X)) { robot.xMotor = params.X; robot.xMotorChanged = true; }
+ if (Number.isFinite(params.Y)) { robot.alpha = params.Y * angScale; robot.yMotorChanged = true; }
+ if (Number.isFinite(params.Z)) { robot.beta = params.Z * angScale; robot.zMotorChanged = true; }
+ if (Number.isFinite(params.A)) { robot.a = params.A * angScale; robot.aMotorChanged = true; }
+ if (Number.isFinite(params.B)) { robot.b = params.B * angScale; robot.bMotorChanged = true; }
+ if (Number.isFinite(params.C)) { robot.c = params.C * angScale; robot.cMotorChanged = true; }
+ // E nach B/C setzen: der Greifer-Motorwert hängt über die Kinematik-Kopplung
+ // von b und c ab. robot.e = Finger-Öffnung (mm), eMotor = abgeleiteter Motorwert.
+ // Ohne diese eMotor-Ableitung bliebe der Greiferwert stale (alte E-Inkonsistenz):
+ // sendCommand() verschickt eMotor, nicht e.
+ if (Number.isFinite(params.E)) {
+ robot.e = params.E;
+ robot.eMotor = robot.gripperMotorFromOpening(robot.e);
+ robot.eMotorChanged = true;
+ }
robot.calculatePositionFromMotorAngles();
robot.sendCommand('G92');
diff --git a/robot/kinematics/Arm3SegmentLinearX.js b/robot/kinematics/Arm3SegmentLinearX.js
index 3b83794..25b7ad6 100644
--- a/robot/kinematics/Arm3SegmentLinearX.js
+++ b/robot/kinematics/Arm3SegmentLinearX.js
@@ -95,7 +95,17 @@ class Arm3SegmentLinearX extends RobotBase {
while(this.a > Math.PI){this.a -= 2*Math.PI}
while(this.a < -Math.PI){this.a += 2*Math.PI}
- this.eMotor = this.e - this.b - this.c;
+ this.eMotor = this.gripperMotorFromOpening(this.e);
+ }
+
+ /**
+ * Greifer-Kopplung dieses Arms: die Finger-Sehne läuft durchs Handgelenk, daher
+ * ziehen Knick (`b`) und Dreh (`c`) am Greifer mit. `eMotor` kompensiert das, damit
+ * die Finger-Öffnung `e` (mm, ab Null-Position eines Fingers) unabhängig von der
+ * Handstellung bleibt. Einzige Quelle für diese Kopplung (auch via G92/M92 genutzt).
+ */
+ gripperMotorFromOpening(e) {
+ return e - this.b - this.c;
}
calculatePositionFromMotorAngles(verbose = false) {
diff --git a/test/GCode.receiveGCode.G92.test.js b/test/GCode.receiveGCode.G92.test.js
index 1592655..39ce3e8 100755
--- a/test/GCode.receiveGCode.G92.test.js
+++ b/test/GCode.receiveGCode.G92.test.js
@@ -113,5 +113,22 @@ describe("Robot G92", () => {
// ("Wenn nur G92 x3 gegeben wird, dann wird trotzdem auch y und z gesendet. schlecht." );
});
+
+ test("G92 E: Greifer-Öffnung (mm) → eMotor über Kopplung e - b - c", () => {
+ const robot = new Robot(300, 300, 20);
+ const D = 180 / Math.PI;
+
+ // B/C in Grad rein (→ intern Radiant), E in mm. eMotor muss aus e, b, c abgeleitet
+ // werden — die Greifer-Sehne läuft durchs Handgelenk (Arm3SegmentLinearX-Kopplung).
+ GCode.receiveGCode(robot, "G92 B30 C-45 E10");
+
+ expect(robot.b).toBeCloseTo(30 / D, 6); // 30° → rad
+ expect(robot.c).toBeCloseTo(-45 / D, 6); // -45° → rad
+ expect(robot.e).toBe(10); // mm, unverändert
+ expect(robot.eMotor).toBeCloseTo(10 - robot.b - robot.c, 6); // = e - b - c
+
+ // Konsistenz: identische Kopplung wie der reguläre Bewegungspfad (calculateAngles3D).
+ expect(robot.eMotor).toBeCloseTo(robot.gripperMotorFromOpening(robot.e), 12);
+ });
});
diff --git a/test/InfoServer.test.js b/test/InfoServer.test.js
index 1914e6a..80fa0a9 100644
--- a/test/InfoServer.test.js
+++ b/test/InfoServer.test.js
@@ -204,7 +204,8 @@ describe('InfoServer', () => {
const httpsOptions = { key, cert, passphrase: 'abcd' };
const sharedState = { connectedClients: [], lastCommands: [], lastPings: [] };
- const robot = { x: 10, y: 20, z: 30, phi: 0.1, theta: 0.2, psi: 0.3, xMotor: 0, alpha: 0, beta: 0, a: 0, b: 0, c: 0 };
+ // e = Greifer-Öffnung (mm) → position.e; eMotor = Greifer-Motorwert → motorCounts.e.
+ const robot = { x: 10, y: 20, z: 30, phi: 0.1, theta: 0.2, psi: 0.3, xMotor: 0, alpha: 0, beta: 0, a: 0, b: 0, c: 0, e: 2.5, eMotor: 7 };
const senders = [];
server = createInfoServer(httpsOptions, sharedState, robot, GCode, senders);
@@ -214,7 +215,8 @@ describe('InfoServer', () => {
expect(statusCode).toBe(200);
const json = JSON.parse(body);
- expect(json.position).toEqual({ x: 10, y: 20, z: 30, a: 0.1, b: 0.2, c: 0.3 });
+ expect(json.position).toEqual({ x: 10, y: 20, z: 30, a: 0.1, b: 0.2, c: 0.3, e: 2.5 });
+ expect(json.motorCounts.e).toBe(7); // Motorwert (eMotor), nicht die mm-Öffnung
});
test('returns 404 for unknown endpoints', async () => {
diff --git a/test/InputWS.api.test.js b/test/InputWS.api.test.js
index fdca0b8..25dd9f2 100644
--- a/test/InputWS.api.test.js
+++ b/test/InputWS.api.test.js
@@ -101,7 +101,7 @@ describe('InputWS API response routing', () => {
a.send('M114');
const parsed = JSON.parse(await aReply);
- expect(parsed.position).toEqual({ x: 5, y: 6, z: 7, a: 0, b: 0, c: 0 });
+ expect(parsed.position).toEqual({ x: 5, y: 6, z: 7, a: 0, b: 0, c: 0, e: 0 });
expect(await bSilent).toBe(true);
a.close();
@@ -123,8 +123,8 @@ describe('InputWS API response routing', () => {
const aParsed = JSON.parse(await aReply);
const bParsed = JSON.parse(await bReply);
- expect(aParsed.position).toEqual({ x: 1, y: 2, z: 3, a: 0, b: 0, c: 0 });
- expect(bParsed.position).toEqual({ x: 1, y: 2, z: 3, a: 0, b: 0, c: 0 });
+ expect(aParsed.position).toEqual({ x: 1, y: 2, z: 3, a: 0, b: 0, c: 0, e: 0 });
+ expect(bParsed.position).toEqual({ x: 1, y: 2, z: 3, a: 0, b: 0, c: 0, e: 0 });
expect(robot.sendCommand).toHaveBeenCalled();
a.close();
diff --git a/test/InputWS.test.js b/test/InputWS.test.js
index 81786f3..96b90b9 100644
--- a/test/InputWS.test.js
+++ b/test/InputWS.test.js
@@ -88,7 +88,7 @@ describe('InputWS', () => {
const message = await messagePromise;
const parsed = JSON.parse(message);
- expect(parsed.position).toEqual({ x: 12, y: 34, z: 56, a: 1, b: 2, c: 3 });
+ expect(parsed.position).toEqual({ x: 12, y: 34, z: 56, a: 1, b: 2, c: 3, e: 0 });
expect(parsed.motorCounts).toBeDefined();
client.close();
@@ -108,7 +108,7 @@ describe('InputWS', () => {
const message = await messagePromise;
const parsed = JSON.parse(message);
- expect(parsed.position).toEqual({ x: 1, y: 2, z: 3, a: 0, b: 0, c: 0 });
+ expect(parsed.position).toEqual({ x: 1, y: 2, z: 3, a: 0, b: 0, c: 0, e: 0 });
expect(robot.sendCommand).toHaveBeenCalled();
client.close();
diff --git a/test/RobotController.test.js b/test/RobotController.test.js
index 961db77..507fa90 100644
--- a/test/RobotController.test.js
+++ b/test/RobotController.test.js
@@ -65,20 +65,38 @@ describe('RobotController (ToDo_6)', () => {
expect(robot.sendCommand).toHaveBeenCalledWith('G92');
});
- test('applyCommand: G92 verhält sich identisch zu M92 (Bug 3)', () => {
+ test('applyCommand: G92 interpretiert Winkel als Grad (→ rad), X bleibt mm', () => {
const robot = createDummyRobot();
robot.createMotorPosition = jest.fn();
+ // G92 nutzt die G-Code-Konvention (Grad). Intern landen die Winkel in Radiant,
+ // X (lineare Schiene) bleibt unverändert in mm. Vgl. M92 oben (Roh-Radiant).
RobotController.applyCommand(robot, { command: 'G92', params: { X: 5, Y: 0.5, A: 0.3 } });
+ const DEG2RAD = Math.PI / 180;
expect(robot.createMotorPosition).toHaveBeenCalledTimes(1);
expect(robot.xMotor).toBe(5);
- expect(robot.alpha).toBe(0.5);
- expect(robot.a).toBe(0.3);
+ expect(robot.alpha).toBeCloseTo(0.5 * DEG2RAD, 10);
+ expect(robot.a).toBeCloseTo(0.3 * DEG2RAD, 10);
expect(robot.calculatePositionFromMotorAngles).toHaveBeenCalled();
expect(robot.sendCommand).toHaveBeenCalledWith('G92');
});
+ test('applyCommand: G92 E setzt Greifer-Öffnung (mm) und leitet eMotor ab', () => {
+ const robot = createDummyRobot();
+ robot.createMotorPosition = jest.fn();
+ robot.b = 0.2; // Handgelenk-Knick
+ robot.c = -0.5; // Hand-Dreher
+
+ // E ist mm (keine Grad/rad-Umrechnung). eMotor wird über die Greifer-Kopplung
+ // aus e, b, c abgeleitet — sonst bliebe der an FluidNC gesendete Wert stale.
+ RobotController.applyCommand(robot, { command: 'G92', params: { E: 10, B: 0.2 * (180 / Math.PI), C: -0.5 * (180 / Math.PI) } });
+
+ expect(robot.e).toBe(10);
+ expect(robot.eMotor).toBeCloseTo(10 - robot.b - robot.c, 10); // = 10 - 0.2 - (-0.5) = 10.3
+ expect(robot.sendCommand).toHaveBeenCalledWith('G92');
+ });
+
test('applyCommand: ungültiger Befehl wird ignoriert', () => {
const robot = createDummyRobot();
RobotController.applyCommand(robot, null);
diff --git a/test/helpers/createDummyRobot.js b/test/helpers/createDummyRobot.js
index c18df29..bd497c2 100644
--- a/test/helpers/createDummyRobot.js
+++ b/test/helpers/createDummyRobot.js
@@ -20,12 +20,17 @@ function createDummyRobot() {
a: 0,
b: 0,
c: 0,
+ eMotor: 0,
// Geometrie
l1: 10,
l2: 10,
l3: 10,
+ // Greifer-Kopplung (RobotBase-Default: keine Kopplung). Konkrete Kinematiken
+ // überschreiben dies; siehe Arm3SegmentLinearX.gripperMotorFromOpening.
+ gripperMotorFromOpening(e) { return e - this.b - this.c; },
+
// Methoden → jest.fn erlaubt Call-Tracking
calculateAngles3D: jest.fn(),
calculatePositionFromMotorAngles: jest.fn(),