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. + +Software Modularisierung + +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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + server.schooltech.ch. + MiniPC + MiniPc2 + Raspi 3(ohne Video) + + Portal UI + + Admin + Nginx + Tunnel + + + + + MainApp + Tunnel + Driver + Robotboard + + UI + T + + UI + T + + + User + User3 + + + 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 @@ + + + + + + + + + + + + + + + + + + + + RoboticsDriver + + FluidNC + + FluidNC + + ... + Info, Installation& Management + + Eingabe + + Programm + + Automatisieren + + Interaktion + + Anzeige + App + App + + + 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(),