Compare commits

...

15 Commits

Author SHA1 Message Date
chk
0d146a48b0 nix 2026-06-26 16:31:08 +02:00
chk
f6a752cf58 Revert B=180-B (Phase 2): gerade Hand ist b=pi, nicht 0
Die Phase-2-Umstellung (Commits 549d10b, 2197a89) war falsch: sie
machte gerade Hand = b=0. Hardware-Daten zeigen aber gerade = B~180
(z.B. G92 ... B179.20). Folge: G28 fuhr nach b=0 -> ~179 Grad
Einklappen -> Hand schlug in den Arm (Crash).

Zurueck auf den verifizierten Stand 933a017:
- IK: this.b = Math.acos(cosB)
- FK: rotateAroundAxis(vecUnterarm, n, this.b)
- G28: robot.b = Math.PI

Verifiziert mit echten Daten: G92 B179.20 -> b=179.20; G28 -> b=180
(Weg 0.8 Grad, kein Slam); IK-Round-Trip exakt; 452/452 Tests gruen.
Logs nicht enthalten.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 16:30:10 +02:00
chk
549d10b9c0 B um 180° gedreht 2026-06-26 15:53:50 +02:00
chk
2197a8954f G28 und Phase2 Arbeiten 2026-06-26 15:28:34 +02:00
chk
933a017e2e Interrim G28 ohne e 2026-06-26 12:28:40 +02:00
chk
7639266170 G28 Singularität und Planungen 2026-06-26 11:39:20 +02:00
chk
bd1752f567 Phase1 Koordinaten 2026-06-26 10:27:42 +02:00
chk
7205b9d913 y war falsch 2026-06-26 08:27:50 +02:00
chk
29b5f2ae4b G92 sowie arctan2 2026-06-26 06:44:11 +02:00
chk
497d0fbc7b G92 doku & E-Stop-Doku 2026-06-25 19:20:14 +02:00
chk
b96a538b89 G92-Grad + E-Korrektur 2026-06-25 18:58:55 +02:00
chk
8deb7bb8a6 wenig 2026-06-19 06:43:56 +02:00
chk
83cef32a37 Logging 2026-06-14 13:41:02 +02:00
chk
8a669f23d3 Fileservice 2026-06-14 11:43:37 +02:00
chk
a807732b58 Fileservice 2026-06-14 11:18:46 +02:00
48 changed files with 4787 additions and 1117 deletions

View File

@@ -0,0 +1,89 @@
// EmergencyStopButton.ino
// ESP32 WiFi Light Sleep, wacht per GPIO-Interrupt auf Knopfdruck auf
// und sendet sofort einen API-Call. Ziel: <250ms von Knopfdruck bis API.
#include <WiFi.h>
#include <HTTPClient.h>
#include "esp_wifi.h"
#include "esp_sleep.h"
#include "driver/gpio.h"
// ── Konfiguration ────────────────────────────────────────────────────────────
#define BUTTON_PIN 9 // GPIO-Pin des Tasters (gegen GND)
#define WIFI_SSID "DEIN_SSID"
#define WIFI_PASSWORD "DEIN_PASSWORT"
#define API_URL "https://deine-api.example.com/emergency-stop"
#define API_TOKEN "DEIN_API_TOKEN"
// ─────────────────────────────────────────────────────────────────────────────
void connectWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("WiFi verbinden");
while (WiFi.status() != WL_CONNECTED) {
delay(100);
Serial.print(".");
}
Serial.printf("\nVerbunden. IP: %s\n", WiFi.localIP().toString().c_str());
}
void sendEmergencyStop() {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi nicht verbunden API-Call abgebrochen");
return;
}
HTTPClient http;
http.begin(API_URL);
http.addHeader("Content-Type", "application/json");
http.addHeader("Authorization", "Bearer " API_TOKEN);
http.setTimeout(3000);
String body = "{\"event\":\"emergency_stop\",\"device\":\"esp32-estop\"}";
int httpCode = http.POST(body);
if (httpCode > 0) {
Serial.printf("API Response: %d\n", httpCode);
} else {
Serial.printf("HTTP Fehler: %s\n", http.errorToString(httpCode).c_str());
}
http.end();
}
void enterLightSleep() {
// Wakeup bei LOW-Pegel (Taster gegen GND, Pull-Up aktiv)
gpio_wakeup_enable((gpio_num_t)BUTTON_PIN, GPIO_INTR_LOW_LEVEL);
esp_sleep_enable_gpio_wakeup();
esp_light_sleep_start();
// Ab hier läuft der Code weiter, sobald der Taster gedrückt wird
}
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
connectWiFi();
// DTIM=10: ESP32 wacht alle ~1000ms kurz für Beacon auf → ~0.51 mA
esp_wifi_set_ps(WIFI_PS_MAX_MODEM);
Serial.println("Bereit. Warte auf Knopfdruck...");
}
void loop() {
enterLightSleep();
// Wakeup → Taster prüfen (Low-aktiv wegen Pull-Up)
if (digitalRead(BUTTON_PIN) == LOW) {
unsigned long t0 = millis();
sendEmergencyStop();
Serial.printf("Latenz API-Call: %lu ms\n", millis() - t0);
// Warten bis Taster losgelassen (Entprellung)
while (digitalRead(BUTTON_PIN) == LOW) {
delay(10);
}
delay(50);
}
}

View File

@@ -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 12 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)** | **150250 ms** ✅ | ~1 mA | **~80 Tage** |
| Deep Sleep + Reconnect | 6001300 ms ❌ | ~0,02 mA | ~mehrere Jahre |
Deep Sleep scheidet aus: Der WiFi-Reconnect nach dem Aufwachen dauert 6001300 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 12 ms auf, um gepufferte Pakete vom Router abzuholen. Die Verbindungsassoziation bleibt erhalten.
Ein **GPIO-Interrupt** (Leitung auf GND) weckt den ESP32 in **15 ms** — der API-Call kann sofort abgesetzt werden, weil WiFi bereits verbunden ist.
## Latenzbudget
| Schritt | Zeit |
|---|---|
| Wakeup aus Light Sleep | 15 ms |
| WiFi-Verbindung prüfen (bereits aktiv) | 0 ms |
| HTTP-Request aufbauen | 2050 ms |
| TLS-Handshake (HTTPS) | 50150 ms |
| Server-Antwort | 2050 ms |
| **Gesamt** | **~100250 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 ≈ 6780 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 818 Uhr alle 30 min (20 WLAN-Verbindungen/Tag)
- Zusätzlich GPIO-Wake für Ereignisse (innerhalb ~300 ms nach Aufwachen + Reconnect)
- Laufzeit 2000 mAh: **~810 Monate** (Selbstentladung dominant)
Für den Emergency Stop Button ist diese Option **nicht geeignet**, da die WiFi-Reconnect-Zeit die 250-ms-Anforderung überschreitet.

View File

@@ -0,0 +1,28 @@
\relax
\providecommand \babel@aux [2]{\global \let \babel@toc \@gobbletwo }
\@nameuse{bbl@beforestart}
\catcode `"\active
\providecommand\hyper@newdestlabel[2]{}
\providecommand\HyField@AuxAddToFields[1]{}
\providecommand\HyField@AuxAddToCoFields[2]{}
\babel@aux{ngerman}{}
\@writefile{toc}{\contentsline {section}{\numberline {1}Ziel und Anforderungen}{2}{section.1}\protected@file@percent }
\@writefile{toc}{\contentsline {section}{\numberline {2}Architekturentscheidung}{2}{section.2}\protected@file@percent }
\@writefile{toc}{\contentsline {subsection}{\numberline {2.1}Bewertete Optionen}{2}{subsection.2.1}\protected@file@percent }
\@writefile{toc}{\contentsline {subsection}{\numberline {2.2}Entscheidung: WiFi Light Sleep}{2}{subsection.2.2}\protected@file@percent }
\@writefile{toc}{\contentsline {section}{\numberline {3}WiFi Light Sleep -- Funktionsprinzip}{2}{section.3}\protected@file@percent }
\@writefile{toc}{\contentsline {subsection}{\numberline {3.1}DTIM-Einstellung und Stromverbrauch}{3}{subsection.3.1}\protected@file@percent }
\@writefile{toc}{\contentsline {subsection}{\numberline {3.2}Akkulaufzeit (2000 mAh LiPo)}{3}{subsection.3.2}\protected@file@percent }
\@writefile{toc}{\contentsline {section}{\numberline {4}Latenzbudget}{3}{section.4}\protected@file@percent }
\@writefile{toc}{\contentsline {section}{\numberline {5}Hardware}{3}{section.5}\protected@file@percent }
\@writefile{toc}{\contentsline {subsection}{\numberline {5.1}Empfohlene Boards}{3}{subsection.5.1}\protected@file@percent }
\@writefile{toc}{\contentsline {subsection}{\numberline {5.2}Akku-Spezifikation}{3}{subsection.5.2}\protected@file@percent }
\@writefile{toc}{\contentsline {subsection}{\numberline {5.3}Schaltung}{4}{subsection.5.3}\protected@file@percent }
\@writefile{toc}{\contentsline {section}{\numberline {6}Software}{4}{section.6}\protected@file@percent }
\@writefile{toc}{\contentsline {subsection}{\numberline {6.1}Abhängigkeiten (Arduino IDE)}{4}{subsection.6.1}\protected@file@percent }
\@writefile{toc}{\contentsline {subsection}{\numberline {6.2}Konfiguration in \texttt {EmergencyStopButton.ino}}{4}{subsection.6.2}\protected@file@percent }
\@writefile{toc}{\contentsline {subsection}{\numberline {6.3}Ablauf}{4}{subsection.6.3}\protected@file@percent }
\@writefile{toc}{\contentsline {subsection}{\numberline {6.4}Kritische API-Funktion}{5}{subsection.6.4}\protected@file@percent }
\@writefile{toc}{\contentsline {section}{\numberline {7}Deployment-Hinweise}{5}{section.7}\protected@file@percent }
\@writefile{toc}{\contentsline {section}{\numberline {8}Dateien}{5}{section.8}\protected@file@percent }
\gdef \@abspage@last{5}

View File

@@ -0,0 +1,529 @@
This is pdfTeX, Version 3.141592653-2.6-1.40.27 (MiKTeX 25.4) (preloaded format=pdflatex 2025.6.3) 25 JUN 2026 19:13
entering extended mode
restricted \write18 enabled.
%&-line parsing enabled.
**./eStopESP32.tex
(eStopESP32.tex
LaTeX2e <2024-11-01> patch level 2
L3 programming layer <2025-04-29>
(C:\Program Files\MiKTeX\tex/latex/base\article.cls
Document Class: article 2024/06/29 v1.4n Standard LaTeX document class
(C:\Program Files\MiKTeX\tex/latex/base\size11.clo
File: size11.clo 2024/06/29 v1.4n Standard LaTeX file (size option)
)
\c@part=\count272
\c@section=\count273
\c@subsection=\count274
\c@subsubsection=\count275
\c@paragraph=\count276
\c@subparagraph=\count277
\c@figure=\count278
\c@table=\count279
\abovecaptionskip=\skip49
\belowcaptionskip=\skip50
\bibindent=\dimen146
)
(C:\Program Files\MiKTeX\tex/latex/base\inputenc.sty
Package: inputenc 2024/02/08 v1.3d Input encoding file
\inpenc@prehook=\toks17
\inpenc@posthook=\toks18
)
(C:\Program Files\MiKTeX\tex/latex/base\fontenc.sty
Package: fontenc 2021/04/29 v2.0v Standard LaTeX package
)
(C:\Program Files\MiKTeX\tex/generic/babel\babel.sty
Package: babel 2025/05/14 v25.9 The multilingual framework for pdfLaTeX, LuaLaT
eX and XeLaTeX
\babel@savecnt=\count280
\U@D=\dimen147
\l@unhyphenated=\language79
(C:\Program Files\MiKTeX\tex/generic/babel\txtbabel.def)
\bbl@readstream=\read2
\bbl@dirlevel=\count281
*************************************
* Local config file bblopts.cfg used
*
(C:\Program Files\MiKTeX\tex/latex/arabi\bblopts.cfg
File: bblopts.cfg 2005/09/08 v0.1 add Arabic and Farsi to "declared" options of
babel
)
(C:\Program Files\MiKTeX\tex/latex/babel-german\ngerman.ldf
Language: ngerman 2024/12/10 v2.15 German support for babel (post-1996 orthogra
phy)
(C:\Program Files\MiKTeX\tex/generic/babel/locale/de\babel-ngerman.tex
Package babel Info: Importing font and identification data for ngerman
(babel) from babel-de.ini. Reported on input line 11.
)
(C:\Program Files\MiKTeX\tex/latex/babel-german\ngermanb.ldf
Language: ngermanb 2024/12/10 v2.15 German support for babel (post-1996 orthogr
aphy)
Package babel Info: Making " an active character on input line 122.
)))
(C:\Program Files\MiKTeX\tex/latex/geometry\geometry.sty
Package: geometry 2020/01/02 v5.9 Page Geometry
(C:\Program Files\MiKTeX\tex/latex/graphics\keyval.sty
Package: keyval 2022/05/29 v1.15 key=value parser (DPC)
\KV@toks@=\toks19
)
(C:\Program Files\MiKTeX\tex/generic/iftex\ifvtex.sty
Package: ifvtex 2019/10/25 v1.7 ifvtex legacy package. Use iftex instead.
(C:\Program Files\MiKTeX\tex/generic/iftex\iftex.sty
Package: iftex 2024/12/12 v1.0g TeX engine tests
))
\Gm@cnth=\count282
\Gm@cntv=\count283
\c@Gm@tempcnt=\count284
\Gm@bindingoffset=\dimen148
\Gm@wd@mp=\dimen149
\Gm@odd@mp=\dimen150
\Gm@even@mp=\dimen151
\Gm@layoutwidth=\dimen152
\Gm@layoutheight=\dimen153
\Gm@layouthoffset=\dimen154
\Gm@layoutvoffset=\dimen155
\Gm@dimlist=\toks20
(C:\Program Files\MiKTeX\tex/latex/geometry\geometry.cfg))
(C:\Program Files\MiKTeX\tex/latex/amsmath\amsmath.sty
Package: amsmath 2024/11/05 v2.17t AMS math features
\@mathmargin=\skip51
For additional information on amsmath, use the `?' option.
(C:\Program Files\MiKTeX\tex/latex/amsmath\amstext.sty
Package: amstext 2021/08/26 v2.01 AMS text
(C:\Program Files\MiKTeX\tex/latex/amsmath\amsgen.sty
File: amsgen.sty 1999/11/30 v2.0 generic functions
\@emptytoks=\toks21
\ex@=\dimen156
))
(C:\Program Files\MiKTeX\tex/latex/amsmath\amsbsy.sty
Package: amsbsy 1999/11/29 v1.2d Bold Symbols
\pmbraise@=\dimen157
)
(C:\Program Files\MiKTeX\tex/latex/amsmath\amsopn.sty
Package: amsopn 2022/04/08 v2.04 operator names
)
\inf@bad=\count285
LaTeX Info: Redefining \frac on input line 233.
\uproot@=\count286
\leftroot@=\count287
LaTeX Info: Redefining \overline on input line 398.
LaTeX Info: Redefining \colon on input line 409.
\classnum@=\count288
\DOTSCASE@=\count289
LaTeX Info: Redefining \ldots on input line 495.
LaTeX Info: Redefining \dots on input line 498.
LaTeX Info: Redefining \cdots on input line 619.
\Mathstrutbox@=\box53
\strutbox@=\box54
LaTeX Info: Redefining \big on input line 721.
LaTeX Info: Redefining \Big on input line 722.
LaTeX Info: Redefining \bigg on input line 723.
LaTeX Info: Redefining \Bigg on input line 724.
\big@size=\dimen158
LaTeX Font Info: Redeclaring font encoding OML on input line 742.
LaTeX Font Info: Redeclaring font encoding OMS on input line 743.
\macc@depth=\count290
LaTeX Info: Redefining \bmod on input line 904.
LaTeX Info: Redefining \pmod on input line 909.
LaTeX Info: Redefining \smash on input line 939.
LaTeX Info: Redefining \relbar on input line 969.
LaTeX Info: Redefining \Relbar on input line 970.
\c@MaxMatrixCols=\count291
\dotsspace@=\muskip17
\c@parentequation=\count292
\dspbrk@lvl=\count293
\tag@help=\toks22
\row@=\count294
\column@=\count295
\maxfields@=\count296
\andhelp@=\toks23
\eqnshift@=\dimen159
\alignsep@=\dimen160
\tagshift@=\dimen161
\tagwidth@=\dimen162
\totwidth@=\dimen163
\lineht@=\dimen164
\@envbody=\toks24
\multlinegap=\skip52
\multlinetaggap=\skip53
\mathdisplay@stack=\toks25
LaTeX Info: Redefining \[ on input line 2953.
LaTeX Info: Redefining \] on input line 2954.
)
(C:\Program Files\MiKTeX\tex/latex/booktabs\booktabs.sty
Package: booktabs 2020/01/12 v1.61803398 Publication quality tables
\heavyrulewidth=\dimen165
\lightrulewidth=\dimen166
\cmidrulewidth=\dimen167
\belowrulesep=\dimen168
\belowbottomsep=\dimen169
\aboverulesep=\dimen170
\abovetopsep=\dimen171
\cmidrulesep=\dimen172
\cmidrulekern=\dimen173
\defaultaddspace=\dimen174
\@cmidla=\count297
\@cmidlb=\count298
\@aboverulesep=\dimen175
\@belowrulesep=\dimen176
\@thisruleclass=\count299
\@lastruleclass=\count300
\@thisrulewidth=\dimen177
)
(C:\Program Files\MiKTeX\tex/latex/listings\listings.sty
\lst@mode=\count301
\lst@gtempboxa=\box55
\lst@token=\toks26
\lst@length=\count302
\lst@currlwidth=\dimen178
\lst@column=\count303
\lst@pos=\count304
\lst@lostspace=\dimen179
\lst@width=\dimen180
\lst@newlines=\count305
\lst@lineno=\count306
\lst@maxwidth=\dimen181
(C:\Program Files\MiKTeX\tex/latex/listings\lstpatch.sty
File: lstpatch.sty 2024/09/23 1.10c (Carsten Heinz)
)
(C:\Program Files\MiKTeX\tex/latex/listings\lstmisc.sty
File: lstmisc.sty 2024/09/23 1.10c (Carsten Heinz)
\c@lstnumber=\count307
\lst@skipnumbers=\count308
\lst@framebox=\box56
)
(C:\Program Files\MiKTeX\tex/latex/listings\listings.cfg
File: listings.cfg 2024/09/23 1.10c listings configuration
))
Package: listings 2024/09/23 1.10c (Carsten Heinz)
(C:\Program Files\MiKTeX\tex/latex/xcolor\xcolor.sty
Package: xcolor 2024/09/29 v3.02 LaTeX color extensions (UK)
(C:\Program Files\MiKTeX\tex/latex/graphics-cfg\color.cfg
File: color.cfg 2016/01/02 v1.6 sample color configuration
)
Package xcolor Info: Driver file: pdftex.def on input line 274.
(C:\Program Files\MiKTeX\tex/latex/graphics-def\pdftex.def
File: pdftex.def 2024/04/13 v1.2c Graphics/color driver for pdftex
)
(C:\Program Files\MiKTeX\tex/latex/graphics\mathcolor.ltx)
Package xcolor Info: Model `cmy' substituted by `cmy0' on input line 1349.
Package xcolor Info: Model `hsb' substituted by `rgb' on input line 1353.
Package xcolor Info: Model `RGB' extended on input line 1365.
Package xcolor Info: Model `HTML' substituted by `rgb' on input line 1367.
Package xcolor Info: Model `Hsb' substituted by `hsb' on input line 1368.
Package xcolor Info: Model `tHsb' substituted by `hsb' on input line 1369.
Package xcolor Info: Model `HSB' substituted by `hsb' on input line 1370.
Package xcolor Info: Model `Gray' substituted by `gray' on input line 1371.
Package xcolor Info: Model `wave' substituted by `hsb' on input line 1372.
)
(C:\Program Files\MiKTeX\tex/latex/hyperref\hyperref.sty
Package: hyperref 2025-05-20 v7.01m Hypertext links for LaTeX
(C:\Program Files\MiKTeX\tex/latex/kvsetkeys\kvsetkeys.sty
Package: kvsetkeys 2022-10-05 v1.19 Key value parser (HO)
)
(C:\Program Files\MiKTeX\tex/generic/kvdefinekeys\kvdefinekeys.sty
Package: kvdefinekeys 2019-12-19 v1.6 Define keys (HO)
)
(C:\Program Files\MiKTeX\tex/generic/pdfescape\pdfescape.sty
Package: pdfescape 2019/12/09 v1.15 Implements pdfTeX's escape features (HO)
(C:\Program Files\MiKTeX\tex/generic/ltxcmds\ltxcmds.sty
Package: ltxcmds 2023-12-04 v1.26 LaTeX kernel commands for general use (HO)
)
(C:\Program Files\MiKTeX\tex/generic/pdftexcmds\pdftexcmds.sty
Package: pdftexcmds 2020-06-27 v0.33 Utility functions of pdfTeX for LuaTeX (HO
)
(C:\Program Files\MiKTeX\tex/generic/infwarerr\infwarerr.sty
Package: infwarerr 2019/12/03 v1.5 Providing info/warning/error messages (HO)
)
Package pdftexcmds Info: \pdf@primitive is available.
Package pdftexcmds Info: \pdf@ifprimitive is available.
Package pdftexcmds Info: \pdfdraftmode found.
))
(C:\Program Files\MiKTeX\tex/latex/hycolor\hycolor.sty
Package: hycolor 2020-01-27 v1.10 Color options for hyperref/bookmark (HO)
)
(C:\Program Files\MiKTeX\tex/latex/hyperref\nameref.sty
Package: nameref 2023-11-26 v2.56 Cross-referencing by name of section
(C:\Program Files\MiKTeX\tex/latex/refcount\refcount.sty
Package: refcount 2019/12/15 v3.6 Data extraction from label references (HO)
)
(C:\Program Files\MiKTeX\tex/generic/gettitlestring\gettitlestring.sty
Package: gettitlestring 2019/12/15 v1.6 Cleanup title references (HO)
(C:\Program Files\MiKTeX\tex/latex/kvoptions\kvoptions.sty
Package: kvoptions 2022-06-15 v3.15 Key value format for package options (HO)
))
\c@section@level=\count309
)
(C:\Program Files\MiKTeX\tex/latex/etoolbox\etoolbox.sty
Package: etoolbox 2025/02/11 v2.5l e-TeX tools for LaTeX (JAW)
\etb@tempcnta=\count310
)
(C:\Program Files\MiKTeX\tex/generic/stringenc\stringenc.sty
Package: stringenc 2019/11/29 v1.12 Convert strings between diff. encodings (HO
)
)
\@linkdim=\dimen182
\Hy@linkcounter=\count311
\Hy@pagecounter=\count312
(C:\Program Files\MiKTeX\tex/latex/hyperref\pd1enc.def
File: pd1enc.def 2025-05-20 v7.01m Hyperref: PDFDocEncoding definition (HO)
Now handling font encoding PD1 ...
... no UTF-8 mapping file for font encoding PD1
)
(C:\Program Files\MiKTeX\tex/generic/intcalc\intcalc.sty
Package: intcalc 2019/12/15 v1.3 Expandable calculations with integers (HO)
)
\Hy@SavedSpaceFactor=\count313
(C:\Program Files\MiKTeX\tex/latex/hyperref\puenc.def
File: puenc.def 2025-05-20 v7.01m Hyperref: PDF Unicode definition (HO)
Now handling font encoding PU ...
... no UTF-8 mapping file for font encoding PU
)
Package hyperref Info: Option `unicode' set `true' on input line 4040.
Package hyperref Info: Hyper figures OFF on input line 4157.
Package hyperref Info: Link nesting OFF on input line 4162.
Package hyperref Info: Hyper index ON on input line 4165.
Package hyperref Info: Plain pages OFF on input line 4172.
Package hyperref Info: Backreferencing OFF on input line 4177.
Package hyperref Info: Implicit mode ON; LaTeX internals redefined.
Package hyperref Info: Bookmarks ON on input line 4424.
\c@Hy@tempcnt=\count314
(C:\Program Files\MiKTeX\tex/latex/url\url.sty
\Urlmuskip=\muskip18
Package: url 2013/09/16 ver 3.4 Verb mode for urls, etc.
)
LaTeX Info: Redefining \url on input line 4763.
\XeTeXLinkMargin=\dimen183
(C:\Program Files\MiKTeX\tex/generic/bitset\bitset.sty
Package: bitset 2019/12/09 v1.3 Handle bit-vector datatype (HO)
(C:\Program Files\MiKTeX\tex/generic/bigintcalc\bigintcalc.sty
Package: bigintcalc 2019/12/15 v1.5 Expandable calculations on big integers (HO
)
))
\Fld@menulength=\count315
\Field@Width=\dimen184
\Fld@charsize=\dimen185
Package hyperref Info: Hyper figures OFF on input line 6042.
Package hyperref Info: Link nesting OFF on input line 6047.
Package hyperref Info: Hyper index ON on input line 6050.
Package hyperref Info: backreferencing OFF on input line 6057.
Package hyperref Info: Link coloring OFF on input line 6062.
Package hyperref Info: Link coloring with OCG OFF on input line 6067.
Package hyperref Info: PDF/A mode OFF on input line 6072.
(C:\Program Files\MiKTeX\tex/latex/base\atbegshi-ltx.sty
Package: atbegshi-ltx 2021/01/10 v1.0c Emulation of the original atbegshi
package with kernel methods
)
\Hy@abspage=\count316
\c@Item=\count317
\c@Hfootnote=\count318
)
Package hyperref Info: Driver (autodetected): hpdftex.
(C:\Program Files\MiKTeX\tex/latex/hyperref\hpdftex.def
File: hpdftex.def 2025-05-20 v7.01m Hyperref driver for pdfTeX
\Fld@listcount=\count319
\c@bookmark@seq@number=\count320
(C:\Program Files\MiKTeX\tex/latex/rerunfilecheck\rerunfilecheck.sty
Package: rerunfilecheck 2022-07-10 v1.10 Rerun checks for auxiliary files (HO)
(C:\Program Files\MiKTeX\tex/latex/base\atveryend-ltx.sty
Package: atveryend-ltx 2020/08/19 v1.0a Emulation of the original atveryend pac
kage
with kernel methods
)
(C:\Program Files\MiKTeX\tex/generic/uniquecounter\uniquecounter.sty
Package: uniquecounter 2019/12/15 v1.4 Provide unlimited unique counter (HO)
)
Package uniquecounter Info: New unique counter `rerunfilecheck' on input line 2
85.
)
\Hy@SectionHShift=\skip54
)
(C:\Program Files\MiKTeX\tex/latex/enumitem\enumitem.sty
Package: enumitem 2025/02/06 v3.11 Customized lists
\labelindent=\skip55
\enit@outerparindent=\dimen186
\enit@toks=\toks27
\enit@inbox=\box57
\enit@count@id=\count321
\enitdp@description=\count322
)
(C:\Program Files\MiKTeX\tex/latex/l3backend\l3backend-pdftex.def
File: l3backend-pdftex.def 2025-04-14 L3 backend support: PDF output (pdfTeX)
\l__color_backend_stack_int=\count323
)
(eStopESP32.aux)
\openout1 = `eStopESP32.aux'.
LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 30.
LaTeX Font Info: ... okay on input line 30.
LaTeX Font Info: Checking defaults for OMS/cmsy/m/n on input line 30.
LaTeX Font Info: ... okay on input line 30.
LaTeX Font Info: Checking defaults for OT1/cmr/m/n on input line 30.
LaTeX Font Info: ... okay on input line 30.
LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 30.
LaTeX Font Info: ... okay on input line 30.
LaTeX Font Info: Checking defaults for TS1/cmr/m/n on input line 30.
LaTeX Font Info: ... okay on input line 30.
LaTeX Font Info: Checking defaults for OMX/cmex/m/n on input line 30.
LaTeX Font Info: ... okay on input line 30.
LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 30.
LaTeX Font Info: ... okay on input line 30.
LaTeX Font Info: Checking defaults for PD1/pdf/m/n on input line 30.
LaTeX Font Info: ... okay on input line 30.
LaTeX Font Info: Checking defaults for PU/pdf/m/n on input line 30.
LaTeX Font Info: ... okay on input line 30.
Package babel Info: 'ngerman' activates 'ngerman' shorthands.
(babel) Reported on input line 30.
*geometry* driver: auto-detecting
*geometry* detected driver: pdftex
*geometry* verbose mode - [ preamble ] result:
* driver: pdftex
* paper: a4paper
* layout: <same size as paper>
* layoutoffset:(h,v)=(0.0pt,0.0pt)
* modes:
* h-part:(L,W,R)=(89.62709pt, 418.25368pt, 89.6271pt)
* v-part:(T,H,B)=(101.40665pt, 591.5302pt, 152.11pt)
* \paperwidth=597.50787pt
* \paperheight=845.04684pt
* \textwidth=418.25368pt
* \textheight=591.5302pt
* \oddsidemargin=17.3571pt
* \evensidemargin=17.3571pt
* \topmargin=-7.86334pt
* \headheight=12.0pt
* \headsep=25.0pt
* \topskip=11.0pt
* \footskip=30.0pt
* \marginparwidth=50.0pt
* \marginparsep=10.0pt
* \columnsep=10.0pt
* \skip\footins=10.0pt plus 4.0pt minus 2.0pt
* \hoffset=0.0pt
* \voffset=0.0pt
* \mag=1000
* \@twocolumnfalse
* \@twosidefalse
* \@mparswitchfalse
* \@reversemarginfalse
* (1in=72.27pt=25.4mm, 1cm=28.453pt)
\c@lstlisting=\count324
(C:\Program Files\MiKTeX\tex/context/base/mkii\supp-pdf.mkii
[Loading MPS to PDF converter (version 2006.09.02).]
\scratchcounter=\count325
\scratchdimen=\dimen187
\scratchbox=\box58
\nofMPsegments=\count326
\nofMParguments=\count327
\everyMPshowfont=\toks28
\MPscratchCnt=\count328
\MPscratchDim=\dimen188
\MPnumerator=\count329
\makeMPintoPDFobject=\count330
\everyMPtoPDFconversion=\toks29
)
Package hyperref Info: Link coloring OFF on input line 30.
(eStopESP32.out) (eStopESP32.out)
\@outlinefile=\write3
\openout3 = `eStopESP32.out'.
No file eStopESP32.toc.
\tf@toc=\write4
\openout4 = `eStopESP32.toc'.
[1
{C:/Users/kech/AppData/Local/MiKTeX/fonts/map/pdftex/pdftex.map}]
Overfull \hbox (52.44191pt too wide) in paragraph at lines 55--64
[]
[]
[2]
[3]
LaTeX Font Info: Font shape `T1/cmtt/bx/n' in size <12> not available
(Font) Font shape `T1/cmtt/m/n' tried instead on input line 198.
(C:\Program Files\MiKTeX\tex/latex/listings\lstlang1.sty
File: lstlang1.sty 2024/09/23 1.10c listings language file
)
(C:\Program Files\MiKTeX\tex/latex/listings\lstmisc.sty
File: lstmisc.sty 2024/09/23 1.10c (Carsten Heinz)
)
[4]
[5] (eStopESP32.aux)
***********
LaTeX2e <2024-11-01> patch level 2
L3 programming layer <2025-04-29>
***********
Package rerunfilecheck Warning: File `eStopESP32.out' has changed.
(rerunfilecheck) Rerun to get outlines right
(rerunfilecheck) or use package `bookmark'.
Package rerunfilecheck Info: Checksums for `eStopESP32.out':
(rerunfilecheck) Before: D41D8CD98F00B204E9800998ECF8427E;0
(rerunfilecheck) After: 64D67ACE6A872D194C01934CCEAACD2A;2987.
)
Here is how much of TeX's memory you used:
13330 strings out of 469923
201990 string characters out of 5479241
876616 words of memory out of 5000000
39954 multiletter control sequences out of 15000+600000
640366 words of font info for 70 fonts, out of 8000000 for 9000
1141 hyphenation exceptions out of 8191
75i,7n,79p,259b,1262s stack positions out of 10000i,1000n,20000p,200000b,200000s
<C:\Users\kech\AppData\Local\MiKTeX\fonts/pk/ljfour/jknappen/ec/dpi600\ectt1
200.pk> <C:\Users\kech\AppData\Local\MiKTeX\fonts/pk/ljfour/jknappen/ec/dpi600\
ectt1000.pk> <C:\Users\kech\AppData\Local\MiKTeX\fonts/pk/ljfour/jknappen/ec/dp
i600\ectt1095.pk> <C:\Users\kech\AppData\Local\MiKTeX\fonts/pk/ljfour/jknappen/
ec/dpi600\ecbx1200.pk> <C:\Users\kech\AppData\Local\MiKTeX\fonts/pk/ljfour/jkna
ppen/ec/dpi600\ecbx1095.pk> <C:\Users\kech\AppData\Local\MiKTeX\fonts/pk/ljfour
/jknappen/ec/dpi600\tcrm1095.pk> <C:\Users\kech\AppData\Local\MiKTeX\fonts/pk/l
jfour/jknappen/ec/dpi600\ecrm1095.pk> <C:\Users\kech\AppData\Local\MiKTeX\fonts
/pk/ljfour/jknappen/ec/dpi600\ecbx1440.pk> <C:\Users\kech\AppData\Local\MiKTeX\
fonts/pk/ljfour/jknappen/ec/dpi600\ecrm1200.pk> <C:\Users\kech\AppData\Local\Mi
KTeX\fonts/pk/ljfour/jknappen/ec/dpi600\ecbx1728.pk><C:/Program Files/MiKTeX/fo
nts/type1/public/amsfonts/cm/cmmi10.pfb><C:/Program Files/MiKTeX/fonts/type1/pu
blic/amsfonts/cm/cmr10.pfb><C:/Program Files/MiKTeX/fonts/type1/public/amsfonts
/cm/cmsy10.pfb>
Output written on eStopESP32.pdf (5 pages, 181387 bytes).
PDF statistics:
526 PDF objects out of 1000 (max. 8388607)
54 named destinations out of 1000 (max. 500000)
1 words of extra memory for PDF output out of 10000 (max. 10000000)

View File

@@ -0,0 +1,19 @@
\BOOKMARK [1][-]{section.1}{\376\377\000Z\000i\000e\000l\000\040\000u\000n\000d\000\040\000A\000n\000f\000o\000r\000d\000e\000r\000u\000n\000g\000e\000n}{}% 1
\BOOKMARK [1][-]{section.2}{\376\377\000A\000r\000c\000h\000i\000t\000e\000k\000t\000u\000r\000e\000n\000t\000s\000c\000h\000e\000i\000d\000u\000n\000g}{}% 2
\BOOKMARK [2][-]{subsection.2.1}{\376\377\000B\000e\000w\000e\000r\000t\000e\000t\000e\000\040\000O\000p\000t\000i\000o\000n\000e\000n}{section.2}% 3
\BOOKMARK [2][-]{subsection.2.2}{\376\377\000E\000n\000t\000s\000c\000h\000e\000i\000d\000u\000n\000g\000:\000\040\000W\000i\000F\000i\000\040\000L\000i\000g\000h\000t\000\040\000S\000l\000e\000e\000p}{section.2}% 4
\BOOKMARK [1][-]{section.3}{\376\377\000W\000i\000F\000i\000\040\000L\000i\000g\000h\000t\000\040\000S\000l\000e\000e\000p\000\040\040\023\000\040\000F\000u\000n\000k\000t\000i\000o\000n\000s\000p\000r\000i\000n\000z\000i\000p}{}% 5
\BOOKMARK [2][-]{subsection.3.1}{\376\377\000D\000T\000I\000M\000-\000E\000i\000n\000s\000t\000e\000l\000l\000u\000n\000g\000\040\000u\000n\000d\000\040\000S\000t\000r\000o\000m\000v\000e\000r\000b\000r\000a\000u\000c\000h}{section.3}% 6
\BOOKMARK [2][-]{subsection.3.2}{\376\377\000A\000k\000k\000u\000l\000a\000u\000f\000z\000e\000i\000t\000\040\000\050\0002\0000\0000\0000\000\040\000m\000A\000h\000\040\000L\000i\000P\000o\000\051}{section.3}% 7
\BOOKMARK [1][-]{section.4}{\376\377\000L\000a\000t\000e\000n\000z\000b\000u\000d\000g\000e\000t}{}% 8
\BOOKMARK [1][-]{section.5}{\376\377\000H\000a\000r\000d\000w\000a\000r\000e}{}% 9
\BOOKMARK [2][-]{subsection.5.1}{\376\377\000E\000m\000p\000f\000o\000h\000l\000e\000n\000e\000\040\000B\000o\000a\000r\000d\000s}{section.5}% 10
\BOOKMARK [2][-]{subsection.5.2}{\376\377\000A\000k\000k\000u\000-\000S\000p\000e\000z\000i\000f\000i\000k\000a\000t\000i\000o\000n}{section.5}% 11
\BOOKMARK [2][-]{subsection.5.3}{\376\377\000S\000c\000h\000a\000l\000t\000u\000n\000g}{section.5}% 12
\BOOKMARK [1][-]{section.6}{\376\377\000S\000o\000f\000t\000w\000a\000r\000e}{}% 13
\BOOKMARK [2][-]{subsection.6.1}{\376\377\000A\000b\000h\000\344\000n\000g\000i\000g\000k\000e\000i\000t\000e\000n\000\040\000\050\000A\000r\000d\000u\000i\000n\000o\000\040\000I\000D\000E\000\051}{section.6}% 14
\BOOKMARK [2][-]{subsection.6.2}{\376\377\000K\000o\000n\000f\000i\000g\000u\000r\000a\000t\000i\000o\000n\000\040\000i\000n\000\040\000E\000m\000e\000r\000g\000e\000n\000c\000y\000S\000t\000o\000p\000B\000u\000t\000t\000o\000n\000.\000i\000n\000o}{section.6}% 15
\BOOKMARK [2][-]{subsection.6.3}{\376\377\000A\000b\000l\000a\000u\000f}{section.6}% 16
\BOOKMARK [2][-]{subsection.6.4}{\376\377\000K\000r\000i\000t\000i\000s\000c\000h\000e\000\040\000A\000P\000I\000-\000F\000u\000n\000k\000t\000i\000o\000n}{section.6}% 17
\BOOKMARK [1][-]{section.7}{\376\377\000D\000e\000p\000l\000o\000y\000m\000e\000n\000t\000-\000H\000i\000n\000w\000e\000i\000s\000e}{}% 18
\BOOKMARK [1][-]{section.8}{\376\377\000D\000a\000t\000e\000i\000e\000n}{}% 19

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,262 @@
\documentclass[a4paper,11pt]{article}
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage[ngerman]{babel}
\usepackage{geometry}
\usepackage{amsmath}
\usepackage{booktabs}
\usepackage{listings}
\usepackage{xcolor}
\usepackage[unicode=true]{hyperref}
\usepackage{enumitem}
\lstset{
basicstyle=\ttfamily\small,
keywordstyle=\color{blue},
commentstyle=\color{gray},
stringstyle=\color{orange},
backgroundcolor=\color{gray!10},
frame=single,
breaklines=true,
columns=fullflexible
}
\title{\textbf{Emergency Stop Button -- ESP32}\\
\large Technische Dokumentation}
\author{}
\date{Juni 2026}
\begin{document}
\maketitle
\tableofcontents
\newpage
% -----------------------------------------------
\section{Ziel und Anforderungen}
Ein physischer Notaus-Taster soll beim Drücken so schnell wie möglich einen
API-Call absetzen. Der ESP32 befindet sich im Ruhezustand und wird durch den
Tastendruck geweckt.
\begin{itemize}
\item Latenz Knopfdruck $\rightarrow$ API-Call: \textbf{< 250\,ms}
\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}
% -----------------------------------------------
\section{Architekturentscheidung}
\subsection{Bewertete Optionen}
\begin{center}
\begin{tabular}{lllll}
\toprule
Option & Latenz & Ø Strom & Gateway nötig & Komplexität \\
\midrule
Deep Sleep + WiFi (RTC-Memory) & 600--1300\,ms & $\approx$0{,}02\,mA & nein & niedrig \\
\textbf{WiFi Light Sleep (DTIM=10)} & \textbf{150--250\,ms} & \textbf{0{,}5--1\,mA} & \textbf{nein} & \textbf{niedrig} \\
BLE Light Sleep + Gateway & 100--300\,ms & 0{,}5--2\,mA & ja & hoch \\
\bottomrule
\end{tabular}
\end{center}
\subsection{Entscheidung: WiFi Light Sleep}
WiFi Light Sleep erfüllt alle Anforderungen ohne zusätzliche Infrastruktur:
\begin{itemize}
\item Der ESP32 hält die WiFi-Verbindung aufrecht und schläft nur die CPU
\item Ein GPIO-Interrupt weckt den ESP32 sofort beim Tastendruck
\item Der API-Call geht direkt vom ESP32 -- kein Container, kein Gateway
\end{itemize}
\textbf{Warum nicht BLE?} Bluetooth LE wäre minimal sparsamer
($\approx$0{,}5\,mA vs. $\approx$1\,mA), erfordert aber ein dauerhaft laufendes
Gateway-Gerät (Raspberry Pi, PC), das seinerseits Strom verbraucht und eine
Fehlerquelle darstellt.
\textbf{Warum nicht Deep Sleep?} Deep Sleep braucht beim Aufwachen
600--1300\,ms (WiFi-Reconnect), was die Latenzanforderung von 250\,ms
verfehlt.
% -----------------------------------------------
\section{WiFi Light Sleep -- Funktionsprinzip}
Im WiFi Light Sleep (Modem Sleep) schläft die CPU, während der WiFi-Stack
aktiv bleibt. Der Router sendet alle 100\,ms einen Beacon; mit DTIM=10 wacht
der ESP32 automatisch alle $\approx$1000\,ms für 1--2\,ms auf, um gepufferte
Pakete abzuholen. Die Verbindungsassoziation bleibt dabei erhalten.
\subsection{DTIM-Einstellung und Stromverbrauch}
\begin{center}
\begin{tabular}{lll}
\toprule
DTIM & Wakeup-Intervall & Ø Strom \\
\midrule
1 & 100\,ms & 5--10\,mA \\
3 & 300\,ms & 2--4\,mA \\
10 & 1000\,ms & 0{,}5--1\,mA \\
\bottomrule
\end{tabular}
\end{center}
Empfohlen: \texttt{DTIM=10} -- sparsamste Option, Verbindung bleibt stabil.
\subsection{Akkulaufzeit (2000 mAh LiPo)}
Bei $\approx$1\,mA Durchschnittsstrom (DTIM=10, selten gedrückt) und 80\,\% nutzbarer Kapazität:
\[
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}
\begin{center}
\begin{tabular}{ll}
\toprule
Schritt & Zeit \\
\midrule
ESP32 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 \\
\midrule
\textbf{Gesamt} & \textbf{$\approx$100--250\,ms} \\
\bottomrule
\end{tabular}
\end{center}
% -----------------------------------------------
\section{Hardware}
\subsection{Empfohlene Boards}
\begin{center}
\begin{tabular}{llll}
\toprule
Board & Lader & Preis & Bemerkung \\
\midrule
\textbf{FireBeetle ESP32-E} (DFRobot) & integriert & 8--12\,& Low-Power optimiert \\
LOLIN D32 (Wemos) & TP4054 & 5--8\,& günstig, bewährt \\
Adafruit HUZZAH32 Feather & MCP73831 & $\approx$20\,& gute Dokumentation \\
\bottomrule
\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}
ESP32 Taster
GPIO 9 ---- [Taster] ---- GND
(interner Pull-Up aktiv: HIGH = offen, LOW = gedrueckt)
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.
% -----------------------------------------------
\section{Software}
\subsection{Abhängigkeiten (Arduino IDE)}
\begin{itemize}
\item Board-Package: \texttt{esp32} by Espressif (Boards Manager)
\item Bibliotheken: \texttt{WiFi.h}, \texttt{HTTPClient.h} (im esp32-Package enthalten)
\end{itemize}
\subsection{Konfiguration in \texttt{EmergencyStopButton.ino}}
\begin{lstlisting}[language=C]
#define BUTTON_PIN 9
#define WIFI_SSID "DEIN_SSID"
#define WIFI_PASSWORD "DEIN_PASSWORT"
#define API_URL "https://deine-api.example.com/emergency-stop"
#define API_TOKEN "DEIN_API_TOKEN"
\end{lstlisting}
\subsection{Ablauf}
\begin{enumerate}
\item \texttt{setup()}: WiFi verbinden, \texttt{WIFI\_PS\_MAX\_MODEM} aktivieren
\item \texttt{loop()}: \texttt{esp\_light\_sleep\_start()} -- CPU schläft, WiFi aktiv
\item Tastendruck löst GPIO-Interrupt aus -- ESP32 wacht auf
\item \texttt{sendEmergencyStop()} sendet HTTP-POST an API
\item Warten bis Taster losgelassen, zurück zu Schritt 2
\end{enumerate}
\subsection{Kritische API-Funktion}
\begin{lstlisting}[language=C]
void sendEmergencyStop() {
HTTPClient http;
http.begin(API_URL);
http.addHeader("Content-Type", "application/json");
http.addHeader("Authorization", "Bearer " API_TOKEN);
http.setTimeout(3000);
String body = "{\"event\":\"emergency_stop\",\"device\":\"esp32-estop\"}";
int httpCode = http.POST(body);
http.end();
}
\end{lstlisting}
% -----------------------------------------------
\section{Deployment-Hinweise}
\begin{itemize}
\item \textbf{HTTPS}: TLS-Handshake kostet 50--150\,ms. Falls die Latenz
kritisch ist und das Netz vertrauenswürdig, kann HTTP verwendet werden
(dann $\approx$50--100\,ms Gesamt).
\item \textbf{DTIM am Router}: Manche Router ignorieren den DTIM-Wunsch des
Clients. Im Zweifelsfall DTIM=3 am Router einstellen.
\item \textbf{API-Token}: Nicht im Quellcode einchecken -- z.\,B. in eine
separate \texttt{secrets.h} auslagern, die im \texttt{.gitignore} steht.
\item \textbf{Watchdog}: Bei produktivem Einsatz einen Hardware-Watchdog
aktivieren, damit der ESP32 sich bei WiFi-Verlust selbst neu startet.
\end{itemize}
% -----------------------------------------------
\section{Dateien}
\begin{center}
\begin{tabular}{ll}
\toprule
Datei & Beschreibung \\
\midrule
\texttt{EmergencyStopButton.ino} & Arduino-Sketch (Hauptprogramm) \\
\texttt{eStopESP32.tex} & Diese Dokumentation \\
\bottomrule
\end{tabular}
\end{center}
\end{document}

View File

@@ -0,0 +1,20 @@
\babel@toc {ngerman}{}\relax
\contentsline {section}{\numberline {1}Ziel und Anforderungen}{2}{section.1}%
\contentsline {section}{\numberline {2}Architekturentscheidung}{2}{section.2}%
\contentsline {subsection}{\numberline {2.1}Bewertete Optionen}{2}{subsection.2.1}%
\contentsline {subsection}{\numberline {2.2}Entscheidung: WiFi Light Sleep}{2}{subsection.2.2}%
\contentsline {section}{\numberline {3}WiFi Light Sleep -- Funktionsprinzip}{2}{section.3}%
\contentsline {subsection}{\numberline {3.1}DTIM-Einstellung und Stromverbrauch}{3}{subsection.3.1}%
\contentsline {subsection}{\numberline {3.2}Akkulaufzeit (2000 mAh LiPo)}{3}{subsection.3.2}%
\contentsline {section}{\numberline {4}Latenzbudget}{3}{section.4}%
\contentsline {section}{\numberline {5}Hardware}{3}{section.5}%
\contentsline {subsection}{\numberline {5.1}Empfohlene Boards}{3}{subsection.5.1}%
\contentsline {subsection}{\numberline {5.2}Akku-Spezifikation}{3}{subsection.5.2}%
\contentsline {subsection}{\numberline {5.3}Schaltung}{4}{subsection.5.3}%
\contentsline {section}{\numberline {6}Software}{4}{section.6}%
\contentsline {subsection}{\numberline {6.1}Abhängigkeiten (Arduino IDE)}{4}{subsection.6.1}%
\contentsline {subsection}{\numberline {6.2}Konfiguration in \texttt {EmergencyStopButton.ino}}{4}{subsection.6.2}%
\contentsline {subsection}{\numberline {6.3}Ablauf}{4}{subsection.6.3}%
\contentsline {subsection}{\numberline {6.4}Kritische API-Funktion}{5}{subsection.6.4}%
\contentsline {section}{\numberline {7}Deployment-Hinweise}{5}{section.7}%
\contentsline {section}{\numberline {8}Dateien}{5}{section.8}%

View File

@@ -2,13 +2,25 @@
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.
<img src="doc/SoftwareModularisation.svg" width="30%" alt="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:
- Eingabe-Server + WebSocket für G-Code und Steuerbefehle
- Info-Server für Status, Position und einfache Weboberfläche
- `server/InputWS.js` empfängt Nachrichten von WebSocket-Clients, prüft sie auf G-Code oder Datei-Kommandos und gibt Positionsdaten zurück.
- `robot/GCode.js` verarbeitet G-Code, übersetzt ihn in Roboter-Koordinaten und triggert `robot.sendCommand()`.
- `server/InputWS.js` empfängt Nachrichten von WebSocket-Clients, routet G-Code-Befehle lokal und leitet FCodes via `robot/FCodeClient.js` an `appRobotFileservice` weiter.
- `robot/GCode.js` verarbeitet G-Code, übersetzt ihn in Roboter-Koordinaten und triggert `robot.sendCommand()` (kein Datei-Handling mehr).
- `robot/FCodeClient.js` übersetzt FCodes (`FPoint`, `FPlus`, …) in REST-Aufrufe an `appRobotFileservice` (Gateway-Funktion des Drivers).
- `robot/RobotBase.js` ist die abstrakte Basisklasse / der Interface-Vertrag: generische Infrastruktur (Zustand, `sendCommand`, Motor-Geschwindigkeiten) plus die zwei abstrakten Kinematik-Methoden.
- `robot/kinematics/Arm3SegmentLinearX.js` ist die konkrete Kinematik (Inverse + Vorwärts) für den aktuellen Arm. Die Auswahl der Kinematik erfolgt über `robot/KinematicsFactory.js` (Umgebungsvariablen `ROBOT_KINEMATICS` / `ROBOT_KINEMATICS_PARAMS`). Siehe `doc/ToDo_12_InverseKinematikConfig_ROADMAP.md`.
- `robot/RobotConfig.js` liest `data/robot/robot.json` beim Start synchron und gibt einen typisierten Konfigurations-Record zurück (Kinematik-Parameter, Bewegungs-Defaults, Controller-Endpunkte). Env-Variablen überschreiben die JSON-Werte.
@@ -27,13 +39,14 @@ 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
- Datei-Kommandos:
- `FPoint`, `FPlus`, `FMinus`, `FShow`, `FList`, `FLoad <file>`, `FSave <file>`, `FClear`
- `FFirst`, `FLast` — erkannt, aber noch nicht implementiert
- `M20`, `M23`, `M28`, `M29`
- FCodes (Datei-/Programm-Befehle) — werden durch den Driver an `appRobotFileservice` weitergeleitet:
- `FPoint`, `FPlus`, `FMinus`, `FFirst`, `FLast`, `FGoto <n>`
- `FShow [id]`, `FList`, `FLoad <id>`, `FSave <name>`, `FClear`
- `FPlay`, `FStop`
- Vollständige API: `doc/fileserviceAPI.md`
### G-Code-Verarbeitung
@@ -48,7 +61,7 @@ Die Eingaben kommen per WebSocket an den HTTPS-Server und werden in `server/Inpu
- WebSocket-Broadcasts an alle verbundenen Clients
- Nachdem ein G-Code-Befehl verarbeitet wurde, sendet das System `GCode.getM114(robot)` zurück.
- Für Datei-Kommandos gibt `GCode.receiveFC()` ebenfalls die aktuelle Position zurück.
- Für FCodes leitet der Driver das Ergebnis von `appRobotFileservice` weiter (Stepping-Befehle zusätzlich als Pose-Broadcast nach lokaler Ausführung).
- Telnet-Ausgabe an GRBL/FluidNC-Geräte
- `TelnetSenderGRBL.execCommand()` erzeugt `G1`/`G90`-Befehle mit Achsenzuordnung und Feedrate.
- Info-Server API
@@ -103,6 +116,10 @@ Die Eingaben kommen per WebSocket an den HTTPS-Server und werden in `server/Inpu
- Statischer Bearer-Token für `PUT /api/robot`. Fehlt die Variable, generiert
`RobotConfigService` beim ersten Start einen zufälligen Key und speichert ihn in
`data/robot/.apikey` (nicht im Repo). Der Key wird beim Start einmalig geloggt.
- `FILESERVICE_URL`
- Standard: `http://appRobot_Fileservice:2100`
- URL der `appRobotFileservice` — wird von `robot/FCodeClient.js` verwendet.
Im Container-Netz entspricht das dem Docker-Dienstnamen aus dem Portainer-Stack.
- `SHELLY_URL`
- URL für den Shelly Smart Plug Emergency-Stop: `http://<IP>/rpc/Switch.Set?id=0&on=false`
- Überschreibt `controllers.emergencyStop.url` aus `robot.json` (analog zu `GRBL_BASE_IP`).
@@ -208,11 +225,12 @@ Socket geschlossen und der bestehende Reconnect-Mechanismus startet automatisch.
- `data/robot/robot.json` — zentrale Roboter-Konfiguration (Single Source of Truth)
- `robot/GCodeParser.js` — wandelt rohe Nachrichten in strukturierte Befehlsobjekte
- `robot/RobotController.js` — wendet geparste Befehle auf das Modell an (Steuerlogik)
- `robot/GCode.js` — Fassade + Datei-Befehle
- `robot/GCode.js` — Fassade für G-Code-Verarbeitung (Bewegung, Pose, Logging)
- `robot/FCodeClient.js` — Gateway: übersetzt FCodes in REST-Aufrufe an `appRobotFileservice`
- `robot/TelnetSenderGRBL.js`
- `robot/ShellyEmergencyStop.js` — steuert Shelly Smart Plug als Emergency-Stop-Aktor (HTTP GET, kein GCode)
- `robot/fluidnc/FluidNCClient.js` — alternative WebSocket-basierte FluidNC-Anbindung mit Reconnect-Logik (noch nicht integriert)
- `GCodeFiles/`enthalten Beispiel- und Log-G-Code-Dateien
- `GCodeFiles/`G-Code-Programme werden jetzt in `appRobotFileservice` verwaltet
## Laufzeitvoraussetzungen
@@ -235,8 +253,7 @@ Architektur- und Refactoring-Aufgaben sind in `doc/ToDo_*.md` dokumentiert:
| `doc/ToDo_6_RobotController.md` | RobotController-Klasse einführen | ✅ erledigt |
| `doc/ToDo_6a_Speed.md` | Speed-Steuerung: Schalter, `calculateSpeeds()`-Fix, koordinierte Feedrate | ✅ erledigt (WS-Sender offen) |
| `doc/ToDo_6b_FileHandling.md` | File-Handling: fehlende Befehle, Cursor im Speicher, Fehler-Feedback | ✅ ausgelagert → `appRobotFileservice` |
| `doc/draft_filehandeling.md` | File-Handling als externes Projekt `appRobotFileservice` (Driver als Gateway, FCode-Pass-through) | Entwurf |
| `doc/draft_filehandeling_API.md` | API der `appRobotFileservice` (Programme, aktiver Cursor, Teaching/Playback) | Entwurf |
| `doc/fileserviceAPI.md` | REST-API der `appRobotFileservice` (Programme, aktiver Cursor, Teaching/Playback) | ✅ implementiert |
| `doc/ToDo_7_Tests.md` | Testabdeckung und Stabilität | teilweise |
| `doc/ToDo_8_Bugs.md` | Bekannte konkrete Bugs | teilweise |
| `doc/ToDo_9_HardwareFeedback.md` | Hardware-Feedback-Loop (GRBL-Antworten, Command-Queue, Positionsabgleich) | teilweise (Baustein Port→Motor ✅, Pakete 16 offen) |
@@ -266,7 +283,7 @@ ToDo_7 Tests — begleitend zu allen obigen
Kurzübersicht weiterer offener Punkte:
- [ ] Dokumentation der vollständigen G-Code-Syntax erweitern
- [x] `FFirst`/`FLast` (und übriges File-Handling) → ausgelagert in `appRobotFileservice` (siehe `doc/draft_filehandeling.md`)
- [x] `FFirst`/`FLast` und gesamtes File-Handling → ausgelagert in `appRobotFileservice` (siehe `doc/fileserviceAPI.md`)
- [ ] `ROBOT_USE_SPEED_CALC` und `motorSpeeds` im echten Betrieb prüfen
- [ ] `FluidNCClient.js` evaluieren: als Ersatz oder Ergänzung zu `TelnetSenderGRBL`?
- [x] HTTPS-Passphrase aus Env-Variable (`HTTPS_PASSPHRASE`) — erledigt

View File

@@ -30,6 +30,7 @@ services:
- ROBOT_KINEMATICS=arm3segmentlinearx
- ROBOT_GRBL_AUTOREPORT=true
- ROBOT_GRBL_REPORT_INTERVAL=200
- FILESERVICE_URL=http://appRobot_Fileservice:2100
ports:
- "2098:2098"
- "2081:2081"
@@ -54,7 +55,23 @@ services:
- "1003:1003"
networks:
- default
appRobotFileservice:
container_name: appRobot_Fileservice
image: node:24-alpine
working_dir: /usr/src/app
volumes:
- /home/chk/Documents/appRobotFileservice:/usr/src/app
command: sh -c "npm ci || npm install && node --inspect=0.0.0.0:2101 index.js"
restart: unless-stopped
environment:
- NODE_ENV=development
- FILE_SERVICE_PORT=2100
ports:
- "2100:2100" # REST-API
- "2101:2101" # node --inspect (Debug)
networks:
- default
appRobotControl:
image: node:24-alpine

View File

@@ -22,16 +22,14 @@ Anfrager) und **Broadcasts** (an alle verbundenen Clients).
| `Ping` | Heartbeat, wird geloggt | `Ping` | **nur Anfrager** (gezielt) |
| `M114` | Statusabfrage | Positions-JSON (siehe unten) | **nur Anfrager** (gezielt) |
| G-Code (`G1`, `G90`, `G91`, `G28`, `M1`, `M92`, …) | Bewegung/Zustandsänderung | aktuelles Positions-JSON | **alle Clients** (Broadcast) |
| Datei-Befehle (`FShow`, `FList`, `FPoint`, `FPlus`, `FMinus`, `FLoad`, `FSave`, `FClear`, `M20/23/28/29`) | Datei-/Log-Verwaltung | Befehlsergebnis | **alle Clients** (Broadcast) |
| FCodes (`FShow`, `FList`, `FPoint`, `FPlus`, `FMinus`, `FFirst`, `FLast`, `FGoto`, `FLoad`, `FSave`, `FClear`, `FPlay`, `FStop`) | Weiterleitung → `appRobotFileservice` via `robot/FCodeClient.js`; Stepping-Ergebnis (Radian-Zeile) wird lokal ausgeführt | Daten-JSON oder Positions-JSON | **alle Clients** (Broadcast) |
| alles andere | | Fehler-Envelope | **nur Anfrager** (gezielt) |
**Begründung der Trennung:** Eine Bewegung ändert die Roboterposition — das ist ein
Status-Update, das jeder Client (z. B. die Simulation) sehen soll → Broadcast. Eine
reine Abfrage (`Ping`, `M114`) ist eine direkte Antwort an den Anfrager → gezielt.
> **Hinweis:** Feinere Zielsteuerung der Datei-Befehle (z. B. `FShow` als
> Anfrager-only-Antwort) sowie `FFirst`/`FLast` gehören zur Datei-Verwaltung in
> **ToDo 4** und bleiben hier bewusst unverändert.
FCodes (Datei-Befehle) werden durch den Driver als Gateway weitergereicht —
Steuerungen brauchen keine direkte Verbindung zur `appRobotFileservice`.
### Positions-JSON (`M114` / Broadcast nach Bewegung)
@@ -57,7 +55,7 @@ Bei unbekannter Eingabe oder Verarbeitungsfehler erhält **nur der Anfrager**:
|--------|-----------|
| `UNKNOWN_COMMAND` | Eingabe passt auf keinen bekannten Befehl |
| `GCODE_ERROR` | Fehler beim Parsen/Ausführen eines G-Code-Befehls |
| `FILE_ERROR` | Fehler bei einem Datei-Befehl |
| `FILE_ERROR` | Fehler bei einem FCode-Befehl (von `appRobotFileservice` weitergereicht) |
Erfolgs-Antworten (`Ping`, Positions-JSON) bleiben aus Kompatibilitätsgründen im
bisherigen Rohformat; das Envelope gilt nur für Fehler.

208
doc/Info_G92.md Normal file
View File

@@ -0,0 +1,208 @@
# G92 - Homing
die appRobotHoming ermittelt die Position der Gelenke (per Foto oder sonstigen Infos).
Diese werden wie folgt behandelt und umgerechnet.
---
## Befehlsformat
```
G92 X<mm> Y<°> Z<°> A<°> B<°> C<°> E<mm>
```
| Achse | Bedeutung | Einheit |
|-------|-----------------------------------|---------|
| X | Lineare Schiene (xMotor) | mm |
| Y | Schulterwinkel α (alpha) | Grad |
| Z | Ellenbogenwinkel β (beta) | Grad |
| A | Handgelenk 1 (a) | Grad |
| B | Handgelenk 2 (b) | Grad |
| C | Handgelenk 3 (c) | Grad |
| E | Greifer-Öffnung (ein Finger) | mm |
**Beispiel (tatsächlicher Homing-Aufruf):**
```
G92 X158.14 Y4.19 Z57.74 A91.85 B-45.46 C-69.92 E21.20
```
→ Y = 4,19°, Z = 57,74° usw. — alle Winkel direkt in Grad wie in FluidNC/GCode-Konvention.
---
## Geometrische Bedeutung der Winkel (Driver-Konvention)
> **Wichtig für appRobotHoming:** Der Driver interpretiert die G92-Winkel in einer
> **eigenen** Konvention. appRobotHoming muss die physisch gemessenen Gelenkwinkel
> **in diese Konvention umrechnen**, bevor sie als G92 gesendet werden. Die folgenden
> Tabellen sind aus der Kinematik (`Arm3SegmentLinearX`) **verifiziert** (jeweils eine
> Achse isoliert variiert).
### Koordinatenrahmen
- **z = 0** ist die Achse zwischen Base und Arm1 (Schulter) — kein Offset darunter.
- y = nach hinten (Hauptarbeitsrichtung), z = nach oben, x = Linearschiene.
- Alle Armwinkel liegen in der y-z-Ebene (bei fixer x-Schiene).
### Y = α (Oberarm) und Z = β (Unterarm) — ABSOLUT
Beide werden **absolut gegen die Horizontale** gemessen, **nicht** relativ zueinander.
Seit **Phase 1** (Weg 2, siehe doc/Info_Koordinaten.md) zeigt **0° nach y**
(Arbeitsrichtung); verifiziert: `FK(α=0, β=0, gerade Hand) → y = 590`.
| Wert | Oberarm (Y) bzw. Unterarm (Z) |
|-------|----------------------------------------|
| 0° | waagerecht nach **y** (Grundstellung) |
| 90° | senkrecht nach oben |
| 180° | waagerecht nach +y |
⚠️ **Z ist der absolute Unterarmwinkel**, nicht der Ellbogen-Knick gegen den Oberarm.
Misst appRobotHoming den Ellbogen relativ zum Oberarm: `Z = Oberarmwinkel + Ellbogen_relativ`.
(Erst bei der Weiterleitung an FluidNC wird daraus `(β α)` zurückgerechnet, siehe unten.)
### B = Handgelenk-Knick
Verifizierte Referenz (α=0, β=90, A=0, C=0):
| B (G92) | physischer Knick Unterarm↔Hand |
|---------|--------------------------------|
| 0° | 180° (Hand voll zurückgeklappt) |
| 90° | 90° (Hand ⊥ Unterarm) |
| 180° | 0° (Hand **gerade**, in Verlängerung des Unterarms) |
**Gerade Hand = B 180°.** Allgemein: `physischer Knick = 180° B`, also `B = 180° Knick`.
Der Driver (IK) erzeugt B nur im Bereich [0°, 180°].
### A = Unterarm-Dreher (Ellbogen-Roll)
A dreht die **Richtung**, in die das Handgelenk knickt, um die Unterarm-Längsachse —
die Knick-**Größe** bleibt dabei gleich. Verifiziert (α=0, β=90, B=90, C=0), nach Phase 1:
A=0 → Fingerspitze Richtung Schulter (y=160), A=90 → x-Seite (x=90),
A=180 → von der Schulter weg (y=340); Knick konstant 90°.
### C = Hand-Dreher (Roll)
C dreht die Hand um ihre eigene Achse. Verifizierte Referenz (α=0, β=90, A=0, B=90),
nach Phase 1:
| C (G92) | Hand-Roll ψ |
|---------|-------------|
| 0° | +90° |
| 90° | 0° (neutral) |
| 180° | 90° |
→ In dieser Stellung gilt `ψ = 90° C`. **Achtung:** der genaue Zusammenhang hängt von
der Armstellung ab — exakt `ψ = acos(cos β · sin A) c` (Winkel in rad). C selbst ist der
physische Hand-Roll-Gelenkwinkel; nur der Bezug zum Welt-ψ verschiebt sich mit der Pose.
### E = Greifer-Öffnung (mm) + Sehnen-Kopplung
E ist die **Finger-Öffnung in mm** (ein Finger ab Null-Position) — keine Winkel-Umrechnung.
Der Driver leitet daraus den Motorwert ab:
```
eMotor = E b c (b, c in RADIANT!)
= E B°/57.2958 C°/57.2958
```
Grund: die Greifer-Sehne läuft durchs Handgelenk; Knick (B) und Roll (C) ziehen an der Sehne.
appRobotHoming sendet die **reine Öffnung** als E; die Kopplung macht der Driver. Bewegt sich
nur das Handgelenk, kompensiert eMotor, damit die Öffnung konstant bleibt.
### Zusammenfassung: was appRobotHoming senden muss
| Achse | Driver erwartet | Typische Falle |
|-------|------------------------------------------------|-----------------------------------------|
| X | xMotor in mm | — |
| Y | Oberarm absolut (0=waagerecht **y**, 90=hoch) | — |
| Z | Unterarm **absolut** (0=y; nicht relativ zum Oberarm) | relativ statt absolut gesendet |
| A | Unterarm-Dreher; **A=0 → Knick-Achse ∥ x** | Nullpunkt/Vorzeichen |
| B | **180° = gerade Hand**; Knick = 180° B | gemessenen Knick direkt gesendet |
| C | Hand-Roll, **90° = neutral** | Nullpunkt/Vorzeichen |
| E | Öffnung in mm | Motorwert statt Öffnung gesendet |
---
## Interne Verarbeitung (`RobotController.js`)
Winkel-Achsen werden von Grad nach Radiant umgerechnet (D = 180/π):
```
robot.alpha = Y / D (intern: Radiant)
robot.beta = Z / D
robot.a = A / D
robot.b = B / D
robot.c = C / D
```
X bleibt mm, keine Umrechnung.
**Greifer E** wird **nach** B und C gesetzt, damit die kinematische Kopplung stimmt:
```
robot.e = E (Finger-Öffnung, mm)
robot.eMotor = gripperMotorFromOpening(e) (abgeleiteter Motorwert)
```
Bei `Arm3SegmentLinearX`: `eMotor = e b c` (Sehnenkompensation durch Handgelenk).
Bei `Arm3SegmentRotaryBase`: `eMotor = e` (keine Kopplung).
---
## Variante M92 (intern / Test)
```
M92 X<mm> Y<rad> Z<rad> A<rad> B<rad> C<rad> E<mm>
```
Winkel werden **roh als Radiant** übernommen. Für Skripte und Tests, nicht für Homing aus appRobotHoming.
---
## Weiterleitung an FluidNC-Instanzen
Nach dem Setzen der internen Motorslots ruft `robot.sendCommand('G92')` auf jedem registrierten `TelnetSenderGRBL` `execCommand('G92', mOld, mNew)` auf.
Jede Instanz bekommt ihren eigenen `G92`-Befehl mit den Port-Inverse-Achswerten (Rückumrechnung Radiant → Grad, mit Kopplung):
| Instanz | FluidNC-Achsen | Formel |
|---------|----------------------------|-----------------------------------------------------------|
| base | x = xMotor | direkt mm |
| | y = α → Grad | `alpha × D` |
| | z = β−α → Grad | `(beta alpha) × D` |
| elbow | x = a → Grad | `a × D` |
| hand | x = cb → Grad | `(c b) × D` |
| | y = eMotor | direkt (mm oder gekoppelter Motorwert) |
| | z = b → Grad | `b × D` |
`G92` bekommt **kein** `G90`-Prefix und keinen Vorschub — nur die geänderten Achsen werden angehängt. Jede Instanz übernimmt den Werkstück-Koordinaten-Offset (WPos) ohne Bewegung.
**Hinweis:** Nur Achsen mit gesetztem `*MotorChanged`-Flag werden gesendet. Bleibt ein Wert gegenüber dem letzten Driver-Zustand unverändert, schickt die jeweilige Instanz keinen G92 für diese Achse. Nach einem Neustart des Drivers sind alle Flags gesetzt → alle Achsen werden gesendet.
---
## Reporting (`M114` / Web-UI)
| Feld | Quelle | Einheit | Anzeige in public/app.js |
|--------------------|----------------|---------|------------------------------------------|
| `position.x/y/z` | Workspace | mm | direkt |
| `position.a/b/c` | phi/theta/psi | rad | `× 180/π` → Grad |
| `position.e` | `robot.e` | mm | direkt (Greifer-Öffnung) |
| `motorCounts.x` | xMotor | mm | direkt |
| `motorCounts.y/z` | alpha/beta | rad | `× 180/π` → Grad |
| `motorCounts.a/b/c`| a/b/c | rad | `× 180/π` → Grad |
| `motorCounts.e` | `robot.eMotor` | mm | direkt (abgeleiteter Motorwert) |
---
## Behobene Fehler (Kontext)
**Ursprüngliches Problem:** `G92 X158.14 Y4.19 Z57.74 …` lieferte korrekte X-Werte, aber Y≈240 und Z≈3308 im Ergebnis. Ursache: Winkel wurden als Radiant interpretiert, intern aber mit `× D` auf Grad umgerechnet — doppelte Skalierung.
**Drei Korrekturen:**
1. **Grad-Interpretation:** G92 rechnet Eingabe-Winkel jetzt mit `÷ D` in Radiant um (statt roh zu übernehmen).
2. **Greifer-Motorwert:** `robot.e` (Öffnung) wurde gesetzt, aber `sendCommand()` überträgt `robot.eMotor`. Fix: `eMotor = gripperMotorFromOpening(e)` direkt im G92-Zweig, nach dem Setzen von B/C.
3. **Web-UI-Anzeige:** `state-e` zeigte `motorCounts.e × 180/π` (mm × 57,3 = Unsinn). Fix: zeigt jetzt `position.e` (mm direkt).

234
doc/Info_Koordinaten.md Normal file
View File

@@ -0,0 +1,234 @@
# Koordinatensystem, Roboter-Aufstellung & Nullstellung
Diese Datei beschreibt
1. das Koordinatensystem,
2. wie der Roboter darin steht,
3. die angestrebte **ideale Nullstellung** und
4. die Schritte, um den Driver auf diese Konvention zu bringen (**Weg 2: Modell auf Y**).
> **Status:** **Phase 1 (y-Flip) ist umgesetzt und am Roboter verifiziert.** α/β werden
> jetzt von **y** aus gemessen (α=0 → Arm zeigt nach y), passend zu robot.json und
> appRobotHoming. Offen ist **Phase 2** (Handgelenk-/Finger-Nullstellung, B/C).
---
## 1. Koordinatensystem
Aus robot.json (`coordinateSystem`): **rechtshändig**, Längen **mm**, Winkel **Grad**.
| Achse | Richtung | Bedeutung im Aufbau |
|-------|------------|----------------------------------------------|
| x | rechts | entlang der Linearschiene |
| y | „backward" | Arm-Arbeitsrichtung: Arm streckt nach **y** |
| z | oben | Höhe |
**Seitenansicht** (Blick entlang +x, also die y/z-Ebene):
```
z
| ■═══════════════●===========●---o Arm1 ═══ , Arm2 === und Finger --- waagerecht
| Schulter (Ursprung, z=0) ausgestreckt → Fingerspitze o
└───────────────────────────────► y (Arbeitsrichtung)
```
In der y/z-Seitenansicht liegen beide Handgelenk-Achsen **end-on** (als Punkt ●), weil
sie entlang x verlaufen:
- linkes ● = **a-Achse** (Unterarm-Dreher, zwischen Ellbogen und Arm2)
- rechtes ● = **b-Achse** (Hand-Knick-Achse)
**Draufsicht** (Blick von oben auf z; x nach oben, y nach rechts):
```
x
▲ │ │
│ ■════╪═══ Arm1 ═════════╪══ Arm2 ══----o o = Fingerspitze (y ≈ 590)
│ Schulter
└────────────────────────────────────────────► y
│ = a-Achse (Ellbogen↔Arm2) bzw. b-Achse (Hand-Knick).
Bei a=0 laufen BEIDE parallel zur x-Achse (hier als senkrechte Striche).
```
---
## 2. Wie der Roboter darin steht
Gelenk-Kette (robot.json `links`):
**Board → Base (Schiene x) → Arm1 (Oberarm) → Ellbow → Arm2 (Unterarm) → Hand → Palm → Finger.**
- **Linearschiene** entlang x; die Base fährt darauf (`xMotor`, mm).
- **Schultergelenk** (Base→Arm1, `variable y`): Drehung in der y-z-Ebene → **α**.
Bei Gelenkwinkel 0 zeigt Arm1 entlang **y** (`skeleton.to = [0,-250,0]`).
- **Ellbogen** (`variable z`) → **β**; **Unterarm-Dreher** (`variable a`) → **a**;
**Handgelenk-Knick** (`variable b`) → **b**; **Hand-Roll** (`variable c`) → **c**;
**Greifer** (`variable e`) → **e**.
- **z = 0 im Driver-Modell = Schulterachse.** (In der Welt liegt sie ~45 mm über dem
Brett — robot.json Joint1-Origin z=45 —, der Driver rechnet schulter-relativ.)
- Armlängen (aus `links` abgeleitet): **l1 = 250** (Oberarm), **l2 = 250** (Unterarm),
**l3 ≈ 90** (Hand). Σ ≈ **590 mm**.
---
## 3. Ideale Nullstellung (Grundstellung)
**Definition (deine Vorgabe):**
1. **Arm1 entlang y** (Schulter-/y-Gelenk α) → **α = 0**.
2. **Ellbogen so angewinkelt, dass auch Arm2 entlang y** (z-Gelenk β) → **β = 0**
(β ist absolut, also auch direkt entlang y, nicht relativ zum Oberarm).
3. **a-Achse so gedreht, dass die Hand-Knick-Achse genau in x-Richtung läuft****a = 0**.
Diese drei Bedingungen erfüllt der Driver **nach Phase 1 bereits** (verifiziert, s.u.):
| Achse | Nullwert | Bedeutung | Status |
|--------|----------|---------------------------------------------|-------------------------|
| X | (frei) | Schienenposition `xMotor` in mm | — |
| Y (α) | **0°** | Oberarm waagerecht entlang y | ✅ Phase 1 |
| Z (β) | **0°** | Unterarm waagerecht entlang y (gestreckt) | ✅ Phase 1 |
| A (a) | **0°** | Hand-Knick-Achse ∥ x | ✅ (Code erfüllt es) |
| B (b) | 0° (Ziel) | Hand gerade — **derzeit ist gerade = 180°** | ⏳ Phase 2 |
| C (c) | 0° (Ziel) | kein Hand-Roll — **derzeit neutral ≠ 0** | ⏳ Phase 2 |
| E (e) | **0** | Greifer geschlossen / Referenz | — |
→ Resultierende Fingerspitze (α=β=a=0, gerade Hand): **(xMotor, (l1+l2+l3), 0) ≈ (x, 590, 0)**.
(Beobachtet ~550; Differenz steckt in der l3-Ableitung / Resthandstellung.)
---
## 4. Ist-Zustand (nach Phase 1) vs. Ideal
| Aspekt | nach Phase 1 (jetzt) | Ideal (nach Phase 2) |
|------------------------------|-----------------------------|----------------------|
| α=0 zeigt nach | **y** ✅ | y |
| β=0 Unterarm | **y** ✅ | y |
| a=0 Hand-Knick-Achse | **∥ x** ✅ | ∥ x |
| G92 der Grundstellung → y | **590** ✅ | ≈ 590 |
| gerade Hand | **b = 180°** | **b = 0°** |
| neutraler Roll | **c: ψ = 90° C** (posenabh.) | **c = 0°** |
Verifiziert per FK: `FK(α=0, β=0, gerade Hand) → (0, 590, 0)`; bei `a=0` bleibt
`Fingerspitze.x = xMotor` konstant, während b variiert (Knick in der y-z-Ebene).
---
## 5. Schritte (Weg 2)
Reihenfolge nach Workflow: **erst Tests (rot), dann Code, dann grün.**
### Phase 1 — y-Flip ✅ ERLEDIGT (am Roboter verifiziert)
Umgesetzt:
- **Spiegelung an der x-z-Ebene** in `Arm3SegmentLinearX` (`_mirrorWorkspaceY`): die
interne Mathematik (`_ikPlusY`/`_fkPlusY`) rechnet weiter in +y, die öffentlichen
Methoden spiegeln die Workspace-Pose (y, pY, φ, ψ; θ bleibt) → α=0 zeigt nach y.
- **G28** (Home) auf y umgestellt **und Singularität behandelt**: die voll ausgestreckte
Stellung (`|y| = l1+l2+l3`) ist eine Handgelenk-Singularität, in der die IK `a`/`c` nicht
bestimmen kann (Müll wie `a=135°, c=45°` → Finger schräg). G28 setzt dort die Motorwerte
**direkt** (`alpha=beta=a=c=0`, `b=π` = gerade Hand) und füllt die Pose per FK.
- **Tests:** `test/Robot.Kinematics.NegativeY.test.js` (Grundstellung 590, Homing-Pose
in y, Round-Trip in y, a=0 → Knick-Achse ∥ x).
- **Migration:** `Robot.02_UpperArm` und der G28-Test auf y umgestellt.
- **Doku:** `doc/Info_G92.md` Y/Z (und C/A nach der Spiegelung) aktualisiert.
### Phase 2 — Handgelenk-/Finger-Nullstellung (B, C, Greifer) — OFFEN
> **Voraussetzung (User):** Erst die Finger visualisieren/prüfen — dort werden noch Fehler
> vermutet. **a-Achse ist bereits korrekt** (a=0 → Knick-Achse ∥ x). Phase 2 betrifft
> **B (Knick)**, **C (Roll)** und die **Greifer-Kopplung**.
**Ziel-Konvention:** gerade Hand → **B = 0°** (statt 180°); neutraler Roll → **C = 0°**;
Greifer-Kopplung konsistent und gegen die echte Mechanik kalibriert.
#### Vorab-Erkenntnis aus dem Code (wichtig!)
Die b/c/e-Konvention ist an **mehreren** Stellen kodiert, die **gemeinsam** geändert werden
müssen. **Invariante:** solange die Hardware-Nullpunkte nicht neu kalibriert werden, müssen
die an FluidNC gesendeten Port-Werte **gleich bleiben** — eine reine Modell-Umbenennung darf
die Hardware-Bewegung nicht verändern.
Fundstellen:
| Datei / Stelle | aktuelle Kodierung |
|----------------|--------------------|
| `Arm3SegmentLinearX._fkPlusY` | `vHand = rotate(vecUnterarm, n, b)`; `psi = c acos(n.z)`**b=π = gerade** |
| `Arm3SegmentLinearX._ikPlusY` | `b = acos(cosB)` (∈[0,π]); `c = acos(cosC) + psi`**c hat posenabh. Offset** |
| `Arm3SegmentLinearX.gripperMotorFromOpening` | `eMotor = e b c` (b,c in **rad**) — **Greifer-Kopplung #1** |
| `RobotController` G92/M92 | `b = B/D`, `c = C/D`, `eMotor = gripperMotorFromOpening(e)` |
| `RobotController` M1 | `b += B`, `c += C` (relativer Motor-Jog) |
| `RobotController` G28 | `b = π`, `c = 0` (Phase 1) → nach B-Umstellung auf `b = 0` ändern |
| `portInverse.js` | `b = hand.z/D`, `c = hand.x/D + b` (Port→Motor, Hardware-Sync) |
| `TelnetSenderGRBL.execCommand` / `portValue` | Hand-Ports: `z = b·D`, `x = (cb)·D`; **e-Port mit `factorTurnLift=1.2`****Greifer-Kopplung #2** |
#### Aufgaben
1. **Finger visualisieren** (User) → Soll-Bild, gegen das kalibriert wird.
2. **Greifer-Kopplung — aktuell aktiv (identifiziert):** Bei der realen Verkabelung
(`hand.axes = ['c','e','b']`) liegt der Greifer auf dem **y-Port**. Gesendet wird daher
`mNew.e · D = eMotor · D = (e b c) · D` — die Kopplung steckt in
`gripperMotorFromOpening` (→ `eMotor`), der Sender hängt nur noch `·180/π` dran.
- Die **x-Port-Variante** (`e + 1.2·b·D c·D`, mit `factorTurnLift = 1.2`) greift nur
bei anderer Verkabelung → **derzeit toter Pfad**. (`factorOpenTurn = 1.92` ungenutzt.)
- **Folge / Slam:** bei `b = π` (Phase-1-„gerade Hand") wird `eMotor = ebc = −π → 180°`
an den Finger-Motor gesendet → er fährt an den Anschlag und verdreht über die Sehne die
ganze Hand. Phase 2 (`b = 0` = gerade) behebt das automatisch (`eMotor = 0`).
- Aufgabe: Kopplung gegen die echte Sehnenmechanik validieren, toten x-Port-Pfad +
`factorOpenTurn` aufräumen, **Vorzeichen** je nach Motor-Verkabelung prüfen.
3. **B-Konvention (gerade = 0°).** Durchgängig:
- FK/IK in `Arm3SegmentLinearX` (b-Definition / acos-Zweig),
- `gripperMotorFromOpening` nachziehen,
- G92-Eingabe (`b = B/D`) + M1 + G28,
- `portInverse.js` (Umkehrung),
- **Sender-Formeln so kompensieren, dass die FluidNC-Ports unverändert bleiben** —
ODER bewusst die Hardware-Nullpunkte neu kalibrieren (Entscheidung dokumentieren).
4. **C-Nullpunkt (neutral = 0°).** Der `c↔ψ`-Bezug ist **posenabhängig**
(`ψ = acos(cos β · sin a) c`). Ein konstantes `c=0=neutral` ist **nicht global** möglich,
ohne die Hand-Parametrierung zu ändern. Bewerten: c als reinen Gelenkwinkel führen
(Offset herausrechnen) oder die ψ-Definition anpassen.
5. **l3-Ableitung korrigiert** ✅ (`RobotConfig.js`): `l3` kommt jetzt aus **Hand + Finger**
(`|Hand.to[1]| + |FingerA.to[1]|` = 35 + 60 = **95**) statt aus dem Ellbogen-Versatz (90).
Zusätzlich sind `kinematics.l1/l2/l3` in robot.json **explizit überschreibbar** (Vorrang
vor der Ableitung) — zum Kalibrieren auf die gemessene Reichweite.
⚠️ Geometrie liefert Reichweite 595, beobachtet wurden **~550** → l3 (oder l1/l2) sollte
per `kinematics.l3` explizit kalibriert werden (deutet auf l3 ≈ 50, falls l1=l2=250 stimmen).
6. **Tests + Doku** nachziehen: Round-Trip mit neuer Konvention, Greifer-Kopplung,
G92-Referenztabellen in `Info_G92.md`, sowie diese Datei.
#### Ansatz-Entscheidung (vor Umsetzung)
- **Klein/lokal:** nur die **G92-Eingabe** umrechnen (appRobotHoming sendet B=0/C=0 für
gerade/neutral, Driver mappt intern auf die alte Konvention). Wenig Risiko, aber das
interne Modell bleibt „unsauber".
- **Groß/sauber:** interne Konvention durchgängig umstellen (alle Fundstellen oben) mit
Hardware-Port-Invariante. Sauberes All-Zero-Home, aber koordinierter Eingriff.
#### Verifikation
Jede B/C/Greifer-Änderung gegen **Visualisierung UND einen Hardware-Test** (eine Achse
isoliert) prüfen — das Modell allein genügt hier nicht, weil es um die Hardware-Abbildung geht.
### Verifikation (Definition of Done)
- **Phase 1 (erfüllt):** G92 der Grundstellung → Driver `y ≈ 590, z ≈ 0`; appRobotHoming
sendet die gemessenen α/β/a **direkt** (ohne Spiegelung); **G28 fährt sauber gestreckt
nach y** (a=0, kein Singularitäts-Müll); volle Suite grün.
- **Phase 2 (Ziel):** Grundstellung mit **allen** Gelenkwinkeln 0 (inkl. B=C=0); Greifer-
Kopplung vereinheitlicht; Finger visuell korrekt.
---
## Anhang: Stand der Kinematik
- **y-Flip (Phase 1):** Spiegelung in `Arm3SegmentLinearX` (`_mirrorWorkspaceY`, genutzt von
`calculateAngles3D` und `calculatePositionFromMotorAngles`). Am Roboter bestätigt.
- **G28-Singularität (Phase 1):** voll ausgestreckt setzt `RobotController` die Motorwerte
direkt (statt der singulären IK) → Finger sauber entlang y.
- **atan2-Fix** in der IK (`gamma = Math.atan2(pZ, pY)`): macht die interne IK für y
mathematisch korrekt — Voraussetzung des y-Flips.
- **Winkel-Konventionen** (Y/Z/A/B/C/E) sind in [doc/Info_G92.md](Info_G92.md) dokumentiert
und nach Phase 1 aktualisiert.

553
doc/Server_to_Robot.svg Normal file
View File

@@ -0,0 +1,553 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="185.6852mm"
height="185.04396mm"
viewBox="0 0 185.6852 185.04396"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
sodipodi:docname="Server_to_Robot.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="0.7884049"
inkscape:cx="418.56665"
inkscape:cy="298.70438"
inkscape:window-width="1920"
inkscape:window-height="1129"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
showguides="true">
<sodipodi:guide
position="104.23929,266.06288"
orientation="0,-1"
id="guide2"
inkscape:locked="false" />
</sodipodi:namedview>
<defs
id="defs1">
<marker
style="overflow:visible"
id="ArrowTriangleStylized"
refX="0"
refY="0"
orient="auto-start-reverse"
inkscape:stockid="Stylized triangle arrow"
markerWidth="0.80000001"
markerHeight="0.80000001"
viewBox="0 0 1 1"
inkscape:isstock="true"
inkscape:collect="always"
preserveAspectRatio="xMidYMid">
<path
transform="scale(0.5)"
style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
d="m 6,0 c -3,1 -7,3 -9,5 0,0 0,-4 2,-5 -2,-1 -2,-5 -2,-5 2,2 6,4 9,5 z"
id="path4" />
</marker>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter1"
x="-0.30270667"
y="-0.03584998"
width="1.4504006"
height="1.162455">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="1.4976689"
id="feGaussianBlur1" />
</filter>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter2"
x="-0.0071527473"
y="-0.027732722"
width="1.0143055"
height="1.0554654">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="0.24767946"
id="feGaussianBlur2" />
</filter>
<marker
style="overflow:visible"
id="ArrowTriangleStylized-9"
refX="0"
refY="0"
orient="auto-start-reverse"
inkscape:stockid="Stylized triangle arrow"
markerWidth="0.80000001"
markerHeight="0.80000001"
viewBox="0 0 1 1"
inkscape:isstock="true"
inkscape:collect="always"
preserveAspectRatio="xMidYMid">
<path
transform="scale(0.5)"
style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
d="m 6,0 c -3,1 -7,3 -9,5 0,0 0,-4 2,-5 -2,-1 -2,-5 -2,-5 2,2 6,4 9,5 z"
id="path4-0" />
</marker>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter1-7"
x="-0.62539401"
y="-0.033621411"
width="2.5964451"
height="1.1841982">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="1.4976689"
id="feGaussianBlur1-6" />
</filter>
</defs>
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-18.207626,-12.726111)">
<rect
style="fill:#ffffff;stroke:#ffff00;stroke-width:0.79375;stroke-linecap:round;stroke-linejoin:round"
id="rect3"
width="268.13898"
height="222.83389"
x="-2.5991523"
y="-0.36202425"
rx="3.4391522"
ry="3.640882" />
<rect
style="fill:none;stroke:#800000;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:0.363309"
id="rect2-7-3-0-1-1-3-3"
width="24.441923"
height="27.052231"
x="170.64005"
y="158.19603"
ry="7.2152543" />
<rect
style="fill:none;stroke:#800000;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:0.363309"
id="rect2-7-3-0-1-1-3-3-2"
width="24.441923"
height="27.052231"
x="135.24863"
y="155.13577"
ry="7.2152543" />
<rect
style="fill:#ffffff;stroke:#800000;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:0.363309"
id="rect2-7-3-0-1-1-3-3-2-3"
width="24.441923"
height="27.052231"
x="140.22813"
y="156.67696"
ry="7.2152543" />
<rect
style="opacity:1;fill:#ffffff;stroke:#008000;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:0.298561"
id="rect2-1-7-2-0"
width="22.543501"
height="60.748863"
x="174.14117"
y="98.800468"
ry="7.2152543" />
<rect
style="fill:#ffffff;stroke:#008000;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none"
id="rect2-1-7"
width="59.087742"
height="61.460766"
x="62.740757"
y="99.263435"
ry="7.2152543" />
<rect
style="fill:none;stroke:#000080;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none"
id="rect2-7"
width="32.03553"
height="27.052229"
x="68.540405"
y="55.081566"
ry="7.2152543" />
<rect
style="fill:none;stroke:#000080;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:0.25"
id="rect2-7-1"
width="34.888073"
height="75.545448"
x="66.882202"
y="53.743294"
ry="8.3898315" />
<rect
style="fill:none;stroke:#000080;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none"
id="rect2-7-8"
width="14.712612"
height="27.052227"
x="124.56292"
y="54.991467"
ry="7.2152543" />
<rect
style="fill:none;stroke:#000080;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none"
id="rect2-7-8-5"
width="14.712612"
height="27.052227"
x="159.14595"
y="54.767498"
ry="7.2152543" />
<rect
style="opacity:0.684327;fill:#ffffff;stroke:#000080;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none;filter:url(#filter2)"
id="rect2-8"
width="118.05676"
height="30.448875"
x="71.373299"
y="42.542305"
ry="8.5576286" />
<rect
style="fill:#ffffff;stroke:#008000;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:0.586331"
id="rect2-1-7-2"
width="22.543501"
height="60.748863"
x="140.53279"
y="98.616264"
ry="7.2152543" />
<rect
style="opacity:0.187638;fill:#ffffff;stroke:#000080;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none"
id="rect2-3-8"
width="36.781532"
height="27.76413"
x="27.882366"
y="43.876053"
ry="7.2152543" />
<rect
style="fill:none;stroke:#800000;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none"
id="rect2-7-3-0-1-1-3"
width="24.441923"
height="27.052231"
x="82.887558"
y="166.4115"
ry="7.2152543" />
<rect
style="fill:#ffffff;stroke:#800000;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none"
id="rect2-7-3-0-1-1"
width="24.441923"
height="27.052231"
x="77.66507"
y="170.46783"
ry="7.2152543" />
<rect
style="fill:none;stroke:#000080;stroke-width:0.5;stroke-linejoin:round;stroke-dasharray:none"
id="rect1"
width="177.95691"
height="55.182091"
x="18.457626"
y="35.237286"
ry="7.2152543" />
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.7611px;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman, Bold';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000080;stroke:none;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none"
x="23.82712"
y="33.894917"
id="text1"><tspan
sodipodi:role="line"
id="tspan1"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman, ';fill:#000080;stroke:none;stroke-width:0.5"
x="23.82712"
y="33.894917">server.schooltech.ch.</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.7611px;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman, Bold';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#008000;stroke:none;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none"
x="-143.19246"
y="61.593941"
id="text1-9"
transform="rotate(-90)"><tspan
sodipodi:role="line"
id="tspan1-1"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman, ';fill:#008000;stroke:none;stroke-width:0.5"
x="-143.19246"
y="61.593941">MiniPC</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.7611px;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman, Bold';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;opacity:0.572347;fill:#008000;stroke:none;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none"
x="-140.84836"
y="139.27716"
id="text1-9-3"
transform="rotate(-90)"><tspan
sodipodi:role="line"
id="tspan1-1-2"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman, ';fill:#008000;stroke:none;stroke-width:0.5"
x="-140.84836"
y="139.27716">MiniPc2</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.7611px;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman, Bold';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;opacity:0.308682;fill:#008000;stroke:none;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none"
x="-128.90205"
y="173.23196"
id="text1-9-3-8"
transform="rotate(-90)"><tspan
sodipodi:role="line"
id="tspan1-1-2-4"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman, ';text-align:center;text-anchor:middle;fill:#008000;stroke:none;stroke-width:0.5"
x="-128.90205"
y="173.23196">Raspi 3</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman, ';text-align:center;text-anchor:middle;fill:#008000;stroke:none;stroke-width:0.5"
x="-128.90205"
y="182.96658"
id="tspan5">(ohne Video)</tspan></text>
<rect
style="fill:#ffffff;stroke:#000080;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none"
id="rect2"
width="49.595745"
height="27.76413"
x="72.851173"
y="43.900543"
ry="7.2152543" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.7611px;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman, ';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000080;stroke:none;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none"
x="81.393982"
y="60.274261"
id="text2"><tspan
sodipodi:role="line"
id="tspan2"
style="fill:#000080;stroke:none;stroke-width:0.5"
x="81.393982"
y="60.274261">Portal UI</tspan></text>
<rect
style="fill:#ffffff;stroke:#000080;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none"
id="rect2-3"
width="36.781532"
height="27.76413"
x="23.999027"
y="55.154541"
ry="7.2152543" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.7611px;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman, ';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000080;stroke:none;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none"
x="31.118034"
y="71.528259"
id="text2-87"><tspan
sodipodi:role="line"
id="tspan2-4"
style="fill:#000080;stroke:none;stroke-width:0.5"
x="31.118034"
y="71.528259">Admin</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.7611px;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman, ';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;opacity:0.229581;fill:#000080;stroke:none;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none"
x="34.223759"
y="52.143547"
id="text2-87-1"><tspan
sodipodi:role="line"
id="tspan2-4-7"
style="fill:#000080;stroke:none;stroke-width:0.5"
x="34.223759"
y="52.143547">Nginx</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.7611px;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman, ';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000080;stroke:none;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none"
x="72.928909"
y="79.513229"
id="text2-8"><tspan
sodipodi:role="line"
id="tspan2-2"
style="fill:#000080;stroke:none;stroke-width:0.5"
x="72.928909"
y="79.513229">Tunnel</tspan></text>
<rect
style="fill:none;stroke:#008000;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none"
id="rect2-7-3"
width="31.532143"
height="25.70986"
x="68.459602"
y="101.92957"
ry="7.2152543" />
<rect
style="fill:none;stroke:#008000;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none"
id="rect2-7-3-0"
width="32.03553"
height="27.052229"
x="68.916672"
y="130.88501"
ry="7.2152543" />
<rect
style="fill:#ffffff;stroke:#800000;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none"
id="rect2-7-3-0-1"
width="24.441923"
height="27.052231"
x="70.099998"
y="167.20209"
ry="7.2152543" />
<rect
style="fill:#ffffff;stroke:#008000;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none"
id="rect2-1"
width="44.058456"
height="26.757345"
x="74.350037"
y="111.79094"
ry="7.2152543" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.7611px;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman, ';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#008000;stroke:none;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none"
x="82.053864"
y="127.73859"
id="text2-9"><tspan
sodipodi:role="line"
id="tspan2-8"
style="fill:#008000;stroke:none;stroke-width:0.5"
x="82.053864"
y="127.73859">MainApp</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.7611px;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman, ';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#008000;stroke:none;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none"
x="73.756584"
y="109.90114"
id="text2-8-6"><tspan
sodipodi:role="line"
id="tspan2-2-5"
style="fill:#008000;stroke:none;stroke-width:0.5"
x="73.756584"
y="109.90114">Tunnel</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.7611px;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman, ';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#008000;stroke:none;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none"
x="74.230339"
y="148.89832"
id="text2-8-6-5"><tspan
sodipodi:role="line"
id="tspan2-2-5-0"
style="fill:#008000;stroke:none;stroke-width:0.5"
x="74.230339"
y="148.89832">Driver</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.7611px;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman, ';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#008000;stroke:none;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none"
x="72.633484"
y="178.56844"
id="text2-8-6-5-4"><tspan
sodipodi:role="line"
id="tspan2-2-5-0-4"
style="fill:#800000;stroke:none;stroke-width:0.5"
x="72.633484"
y="178.56844">Robot</tspan><tspan
sodipodi:role="line"
style="fill:#800000;stroke:none;stroke-width:0.5"
x="72.633484"
y="188.26981"
id="tspan3">board</tspan></text>
<rect
style="fill:#ffffff;stroke:#000080;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none"
id="rect2-5"
width="22.780821"
height="27.526825"
x="128.87369"
y="43.978241"
ry="7.2152543" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.7611px;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman, ';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000080;stroke:none;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none"
x="137.4165"
y="60.184162"
id="text2-7"><tspan
sodipodi:role="line"
id="tspan2-6"
style="fill:#000080;stroke:none;stroke-width:0.5"
x="137.4165"
y="60.184162">UI</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.7611px;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman, ';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000080;stroke:none;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none"
x="129.6226"
y="79.75872"
id="text2-8-1"><tspan
sodipodi:role="line"
id="tspan2-2-8"
style="fill:#000080;stroke:none;stroke-width:0.5"
x="129.6226"
y="79.75872">T</tspan></text>
<rect
style="fill:#ffffff;stroke:#000080;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none"
id="rect2-5-4"
width="22.780821"
height="27.526825"
x="165.13467"
y="43.754272"
ry="7.2152543" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.7611px;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman, ';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000080;stroke:none;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none"
x="171.99953"
y="59.960194"
id="text2-7-3"><tspan
sodipodi:role="line"
id="tspan2-6-1"
style="fill:#000080;stroke:none;stroke-width:0.5"
x="171.99953"
y="59.960194">UI</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.7611px;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman, ';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000080;stroke:none;stroke-width:0.499999;stroke-linejoin:round;stroke-dasharray:none"
x="163.70226"
y="79.199165"
id="text2-8-1-2"><tspan
sodipodi:role="line"
id="tspan2-2-8-3"
style="fill:#000080;stroke:none;stroke-width:0.5"
x="163.70226"
y="79.199165">T</tspan></text>
<path
style="opacity:0.677704;mix-blend-mode:normal;fill:none;stroke:#338000;stroke-width:5;stroke-linejoin:round;stroke-dasharray:none;marker-end:url(#ArrowTriangleStylized);filter:url(#filter1)"
d="m 139.46805,20.675199 c 0,0 -22.67989,32.409972 -24.62856,38.778949 -4.89798,16.008385 -4.12484,30.729749 -3.84747,40.716632 0.28059,10.10266 1.18759,28.44197 -1.1865,39.86643 -1.8449,8.87791 -8.54281,20.88243 -8.54281,20.88243"
id="path1"
sodipodi:nodetypes="csssc" />
<path
style="opacity:0.270096;mix-blend-mode:normal;fill:none;stroke:#338000;stroke-width:5;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowTriangleStylized-9);filter:url(#filter1-7)"
d="m 182.53306,26.969611 c -16.61161,54.420297 22.65849,44.720607 2.6103,128.616729"
id="path1-8"
sodipodi:nodetypes="cc" />
<text
xml:space="preserve"
style="font-size:11.2889px;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman, ';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;opacity:0.781457;fill:#338000;stroke:#00ffff;stroke-width:10;stroke-linejoin:round;stroke-dasharray:none"
x="139.23076"
y="20.200598"
id="text4"><tspan
sodipodi:role="line"
id="tspan4"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:11.2889px;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman, Bold';fill:#338000;stroke:none;stroke-width:10"
x="139.23076"
y="20.200598">User</tspan></text>
<text
xml:space="preserve"
style="font-size:11.2889px;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman, ';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;opacity:0.781457;fill:#338000;fill-opacity:0.399281;stroke:#00ffff;stroke-width:10;stroke-linejoin:round;stroke-dasharray:none"
x="176.33203"
y="24.640509"
id="text4-8"><tspan
sodipodi:role="line"
id="tspan4-6"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:11.2889px;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman, Bold';fill:#338000;fill-opacity:0.399281;stroke:none;stroke-width:10"
x="176.33203"
y="24.640509">User3</tspan></text>
<text
xml:space="preserve"
style="font-size:3.175px;text-align:center;text-anchor:middle;opacity:0.270096;fill:#ffffff;fill-opacity:0.553957;stroke:#338000;stroke-width:0.799999"
x="173.63927"
y="23.712084"
id="text6"><tspan
sodipodi:role="line"
id="tspan6"
style="stroke-width:0.8"
x="173.63927"
y="23.712084" /></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -0,0 +1,321 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="254.71674mm"
height="103.7089mm"
viewBox="0 0 254.71674 103.7089"
version="1.1"
id="svg5"
inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
sodipodi:docname="SoftwareModularisation.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.73851712"
inkscape:cx="624.22385"
inkscape:cy="295.18612"
inkscape:window-width="1710"
inkscape:window-height="752"
inkscape:window-x="122"
inkscape:window-y="174"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs2">
<rect
x="82.597954"
y="404.86536"
width="207.17191"
height="81.243889"
id="rect342" />
<marker
style="overflow:visible"
id="ArrowTriangleStylized"
refX="0"
refY="0"
orient="auto-start-reverse"
inkscape:stockid="Stylized triangle arrow"
markerWidth="0.80000001"
markerHeight="0.80000001"
viewBox="0 0 1 1"
inkscape:isstock="true"
inkscape:collect="always"
preserveAspectRatio="xMidYMid">
<path
transform="scale(0.5)"
style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
d="m 6,0 c -3,1 -7,3 -9,5 0,0 0,-4 2,-5 -2,-1 -2,-5 -2,-5 2,2 6,4 9,5 z"
id="path4" />
</marker>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter1"
x="-0.046015804"
y="-0.087898715"
width="1.1621232"
height="1.3448635">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="1.4976689"
id="feGaussianBlur1" />
</filter>
</defs>
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(36.090885,-5.8258868)">
<rect
style="fill:#ffffff;stroke:#ffff00;stroke-width:0.79375;stroke-linecap:round;stroke-linejoin:round"
id="rect1"
width="322.79492"
height="150.47044"
x="-50.063141"
y="0.81020516"
rx="3.4391522"
ry="3.640882" />
<rect
style="fill:none;fill-opacity:0.204013;stroke:#ff5555;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round"
id="rect1257"
width="178.77321"
height="59.471653"
x="9.6731005"
y="49.798553"
ry="6.2628565" />
<rect
style="fill:#0000ff;fill-opacity:0.204013;stroke:#0000ff;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round"
id="rect234"
width="77.743065"
height="24.361881"
x="40.125454"
y="60.188179"
ry="6.2628565" />
<text
xml:space="preserve"
transform="scale(0.26458333)"
id="text340"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect342);display:inline;fill:#000000;fill-opacity:1;stroke:#ff5555" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:9.61905px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.240477"
x="52.229092"
y="68.706116"
id="text402"
transform="scale(0.90888935,1.1002439)"><tspan
sodipodi:role="line"
id="tspan400"
style="stroke-width:0.240477"
x="52.229092"
y="68.706116">RoboticsDriver</tspan></text>
<rect
style="fill:#0000ff;fill-opacity:0.204013;stroke:#0000ff;stroke-width:0.547466;stroke-linecap:round;stroke-linejoin:round"
id="rect234-4"
width="31.870832"
height="13.579219"
x="29.924107"
y="92.440994"
ry="6.2544284" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:7.05556px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="31.706272"
y="101.74669"
id="text402-5"><tspan
sodipodi:role="line"
id="tspan400-9"
style="font-size:7.05556px;stroke-width:0.264583"
x="31.706272"
y="101.74669">FluidNC</tspan></text>
<rect
style="fill:#0000ff;fill-opacity:0.204013;stroke:#0000ff;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round"
id="rect234-4-1"
width="29.735825"
height="13.597518"
x="67.371765"
y="92.798347"
ry="6.2628565" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:7.05556px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="68.78791"
y="102.2177"
id="text402-5-4"><tspan
sodipodi:role="line"
id="tspan400-9-5"
style="font-size:7.05556px;stroke-width:0.264583"
x="68.78791"
y="102.2177">FluidNC</tspan></text>
<rect
style="fill:#0000ff;fill-opacity:0.204013;stroke:#0000ff;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round"
id="rect234-4-1-3"
width="29.735825"
height="13.597518"
x="106.22498"
y="92.798347"
ry="6.2628565" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:7.05556px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="112.161"
y="102.11319"
id="text402-5-3-2"><tspan
sodipodi:role="line"
id="tspan400-9-4-6"
style="font-size:7.05556px;stroke-width:0.264583"
x="112.161"
y="102.11319">... </tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:7.05556px;line-height:1.25;font-family:sans-serif;fill:#ff0000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="186.6279"
y="76.870903"
id="text1365"><tspan
sodipodi:role="line"
id="tspan1363"
style="font-size:7.05556px;text-align:end;text-anchor:end;fill:#ff0000;stroke-width:0.264583"
x="186.6279"
y="76.870903">Info, Installation</tspan><tspan
sodipodi:role="line"
style="font-size:7.05556px;text-align:end;text-anchor:end;fill:#ff0000;stroke-width:0.264583"
x="186.6279"
y="85.690353"
id="tspan1367">&amp; Management</tspan></text>
<rect
style="fill:#bcffbc;fill-opacity:1;stroke:#00ff00;stroke-width:0.57906;stroke-linecap:round;stroke-linejoin:round"
id="rect1369"
width="46.882557"
height="19.296309"
x="18.31204"
y="32.985142"
ry="6.2467051" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="20.270605"
y="45.266979"
id="text3141"><tspan
sodipodi:role="line"
id="tspan3139"
style="stroke-width:0.264583"
x="20.270605"
y="45.266979">Eingabe</tspan></text>
<rect
style="fill:#bcffbc;fill-opacity:1;stroke:#00ff00;stroke-width:0.58073;stroke-linecap:round;stroke-linejoin:round"
id="rect1369-9"
width="60.136616"
height="19.294638"
x="53.404377"
y="7.1910405"
ry="6.1369925" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="55.362103"
y="19.472044"
id="text3141-8"><tspan
sodipodi:role="line"
id="tspan3139-7"
style="stroke-width:0.264583"
x="55.362103"
y="19.472044">Programm</tspan></text>
<rect
style="fill:#bcffbc;fill-opacity:1;stroke:#00ff00;stroke-width:0.59402;stroke-linecap:round;stroke-linejoin:round"
id="rect1369-9-7"
width="90.575684"
height="20.620722"
x="127.75317"
y="6.1228967"
ry="6.1341004" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="133.28688"
y="20.546833"
id="text3141-8-4"><tspan
sodipodi:role="line"
id="tspan3139-7-5"
style="stroke-width:0.264583"
x="133.28688"
y="20.546833">Automatisieren</tspan></text>
<rect
style="fill:#bcffbc;fill-opacity:1;stroke:#00ff00;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round"
id="rect1369-9-7-1"
width="55.172501"
height="18.177734"
x="-35.826302"
y="13.41999"
ry="6.1533923" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:9.75471px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.243868"
x="-36.601929"
y="23.712566"
id="text3141-8-4-3"
transform="scale(0.92170774,1.0849426)"><tspan
sodipodi:role="line"
id="tspan3139-7-5-9"
style="stroke-width:0.243868"
x="-36.601929"
y="23.712566">Interaktion</tspan></text>
<rect
style="fill:#bcffbc;fill-opacity:1;stroke:#00ff00;stroke-width:0.572522;stroke-linecap:round;stroke-linejoin:round"
id="rect1369-0"
width="45.814304"
height="19.302847"
x="138.13206"
y="48.384411"
ry="6.2488213" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="140.09389"
y="60.669518"
id="text3141-7"><tspan
sodipodi:role="line"
id="tspan3139-6"
style="stroke-width:0.264583"
x="140.09389"
y="60.669518">Anzeige</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="196.68637"
y="35.060978"
id="text9629"><tspan
sodipodi:role="line"
id="tspan9627"
style="fill:#00ff00;stroke-width:0.264583"
x="196.68637"
y="35.060978">App</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="-2.2734814"
y="38.910786"
id="text9629-1"><tspan
sodipodi:role="line"
id="tspan9627-2"
style="fill:#00ff00;stroke-width:0.264583"
x="-2.2734814"
y="38.910786">App</tspan></text>
<path
style="opacity:0.677704;mix-blend-mode:normal;fill:none;stroke:#338000;stroke-width:5;stroke-linejoin:round;stroke-dasharray:none;marker-end:url(#ArrowTriangleStylized);filter:url(#filter1)"
d="m 30.489165,22.750961 c 39.165646,1.677077 80.639205,15.871211 80.435915,31.846606 -0.27243,21.408071 1.05382,18.470822 -9.37105,37.461762"
id="path1-9"
sodipodi:nodetypes="csc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,21 +0,0 @@
# ToDo 1 — Parsing
## Ziel der Verbesserung
Klare Trennung zwischen G-Code-Parsing und Robotersteuerlogik. Der Parser soll nur lesen und strukturieren, nicht direkt den Roboterzustand verändern.
## Aufgaben
- [x] `GCodeParser` einführen, das G-Code und Nachrichten in strukturierte Befehlsobjekte übersetzt
- [x] Parsing-Regeln definieren für `G90`, `G91`, `G1`, `G28`, `G92`, `M1` und `$J=` sowie Parameter `X`, `Y`, `Z`, `A`, `B`, `C`, `E`, `F`
- [x] Raw-String-Verarbeitung aus `GCode.receiveGCode()` entfernen
- [x] Parser-Resultate als Objekte an den Controller übergeben, nicht als rohe Textbefehle
- [x] Parser-Fehlerfälle klar behandeln: ungültige Syntax, fehlende Werte, unbrauchbare Befehle
## Status
- [x] Implementierung abgeschlossen
- [x] Tests erfolgreich: `npx jest --runInBand`
Erledigt von VStudio Chatbot unter Aufsicht ChK

View File

@@ -4,8 +4,7 @@
> (`GCodeFileManager`) gebaut, sondern in das eigenständige Projekt
> **`appRobotFileservice`** ausgelagert und über FCodes durch den Driver
> weitergereicht. Im Driver bleibt nur ein dünner Proxy. Konzept & Schnittstelle:
> [`draft_filehandeling.md`](draft_filehandeling.md) ·
> [`draft_filehandeling_API.md`](draft_filehandeling_API.md).
> [`fileserviceAPI.md`](fileserviceAPI.md).
> Die folgenden Punkte sind als Vorlage für die Umsetzung *dort* zu lesen.
## Ziel der Verbesserung

View File

@@ -5,8 +5,7 @@
> Die hier beschriebenen Detailprobleme werden **dort** gelöst: Cursor als In-Memory-
> Index (Paket 2), explizite Grad↔Radian-Umrechnung im Fileservice (Paket 3),
> Fehler-Envelope (Paket 4), asynchrones IO (Paket 5). Konzept & Schnittstelle:
> [`draft_filehandeling.md`](draft_filehandeling.md) ·
> [`draft_filehandeling_API.md`](draft_filehandeling_API.md).
> [`fileserviceAPI.md`](fileserviceAPI.md).
> Die folgende Analyse bleibt als Umsetzungs-Vorlage für *jenes* Projekt erhalten.
## Ist-Zustand

View File

@@ -1,98 +0,0 @@
# ToDo 8 — Bekannte Bugs
## Ziel
Konkrete, im Code identifizierte Fehler beheben — unabhängig von den Architektur-Refactorings in den anderen ToDo-Dateien.
---
## Bug 1: `TelnetSenderGRBL` — `close`-Event verliert `this`-Kontext ✅ ERLEDIGT
> Behoben im ToDo-2-Refactoring: Der `close`-Handler nutzt jetzt eine Arrow-Function
> (`robot/TelnetSenderGRBL.js`), `this` zeigt korrekt auf die Sender-Instanz.
**Datei:** `robot/TelnetSenderGRBL.js`, Zeile 5457
**Problem:** Das `close`-Event verwendet eine reguläre `function()` statt einer Arrow Function. Dadurch zeigt `this` innerhalb des Handlers auf das EventEmitter-Objekt, nicht auf die `TelnetSenderGRBL`-Instanz. `this.tSocket = null` hat keinen Effekt — nach einer Verbindungstrennung bleibt `tSocket` auf dem alten, ungültigen Objekt.
```js
// Falsch:
this.tSocket.on("close", function () {
this.tSocket = null; // 'this' ist hier NICHT TelnetSenderGRBL
});
// Richtig:
this.tSocket.on("close", () => {
this.tSocket = null;
});
```
---
## Bug 2: `FFirst` und `FLast` sind nicht implementiert
**Datei:** `robot/GCode.js`
**Problem:** `ContainsFilesCommand()` erkennt `FFirst` und `FLast` und leitet sie an `receiveFC()` weiter. `receiveFC()` behandelt sie aber nicht — die Befehle werden stillschweigend ignoriert und es wird nur `getM114` zurückgegeben.
**Erwartetes Verhalten:**
- `FFirst` — Cursor auf den ersten Eintrag der Log-Datei setzen und die Position anfahren
- `FLast` — Cursor auf den letzten Eintrag setzen und die Position anfahren
---
## Bug 3: G92/M92-Mismatch
**Datei:** `robot/GCode.js`
**Problem:** `containsCommand()` erkennt `G92`, aber `receiveGCode()` prüft auf `g[0] == "M92"`. Ein eingehender Befehl `G92 X10` wird als G-Code erkannt, fällt dann aber durch alle Bedingungen in `receiveGCode()`, und löst unbeabsichtigt `calculateAngles3D()` + `sendCommand()` aus, ohne die Position zu setzen.
**Klärungsbedarf:** Ist G92 oder M92 der korrekte Eingabe-Befehl? Beides konsistent machen.
---
## Bug 4: `logs/`-Verzeichnis wird nicht sichergestellt ✅ ERLEDIGT
> Behoben: `initInputWS()` ruft `ensureLogDir()` (`fs.mkdirSync('./logs', { recursive: true })`)
> beim Start auf. `ensureLogDir` ist exportiert und idempotent.
> Test: `test/InputWS.logDir.test.js`.
**Datei:** `server/InputWS.js`, Zeilen 6667 und 7778
**Problem:** `fs.appendFileSync('./logs/gcode_commands.log', ...)` und `fs.appendFileSync('./logs/pings.log', ...)` crashen beim ersten Aufruf, wenn das `logs/`-Verzeichnis nicht existiert.
**Fix:** Beim Start `fs.mkdirSync('./logs', { recursive: true })` aufrufen, z. B. in `startRobot.js` oder am Anfang von `initInputWS`.
---
## Bug 5: Falscher Finitude-Check in `TelnetSenderGRBL.execCommand`
**Datei:** `robot/TelnetSenderGRBL.js`, Zeile 161
**Problem:**
```js
if(this.aAxisGrbl == "x" && mNew.xMotorChanged && Number.isFinite(mNew.y)){
```
Der Check prüft `mNew.y` statt `mNew.x`. Wenn `mNew.x` `NaN` oder `Infinity` wäre, würde das trotzdem durchgehen.
---
## Bug 6: `containsMCode` matcht zu breit ✅ ERLEDIGT
> Behoben: `containsMCode` nutzt jetzt `s === 'M1' || s.startsWith('M1 ')`.
> Test: `test/GCode.containsMCode.test.js`.
> (Hinweis bleibt: Methode wird im Produktivcode noch nicht aufgerufen.)
**Datei:** `robot/GCode.js`, Zeile 12
**Problem:** `s.indexOf('M1') == 0` trifft auch auf `M10`, `M11`, `M12` usw. zu.
```js
// Aktuell:
static containsMCode(s){ return s.indexOf('M1') == 0 }
// Präziser:
static containsMCode(s){ return s === 'M1' || s.startsWith('M1 ') }
```
Hinweis: Diese Methode wird im aktuellen Code nicht aufgerufen — sie hat keine Wirkung, ist aber irreführend.

View File

@@ -1,292 +0,0 @@
# Draft — File-Handling als externes Projekt `appRobotFileservice` (Driver als Gateway)
> **Status:** Entwurf / Diskussionsgrundlage.
> **Projekte:** Der **Driver** lebt in `appRobotDriver` (dieses Repo). Das gesamte
> G-Code-**Programm**-Handling wird in das eigenständige Projekt
> **`appRobotFileservice`** ausgelagert. Schnittstelle:
> [`draft_filehandeling_API.md`](draft_filehandeling_API.md).
> **Verhältnis zu ToDos:** ersetzt den Driver-internen `GCodeFileManager`-Ansatz aus
> `doc/ToDo_4_GCode.md` und `doc/ToDo_6b_FileHandling.md`.
> **Übergang darf hart sein** — keine Rückwärtskompatibilität nötig.
---
## 1. Motivation
Heute lebt das Datei-Handling in [`robot/GCode.js`](../robot/GCode.js)
(`receiveFC`, `ContainsFilesCommand`, `removeStringFromFile`, `toPiMultiple`, der
`;!`-Cursor) und wird in [`server/InputWS.js`](../server/InputWS.js) gleichberechtigt
neben den Bewegungs-Befehlen geroutet. Das vermischt zwei Verantwortungen:
| | **Bewegung / Hardware** | **Programm-Verwaltung** |
|---|---|---|
| Aufgabe | eine G-Code-Zeile → Achsen bewegen | Programme speichern, anzeigen, durchblättern |
| Zustand | Live-Pose des Roboters | Datei-Inhalte, Cursor, Listen |
| Echtzeit | ja (Telnet/FluidNC) | nein (Storage-/UI-getrieben) |
| Gehört zu | **`appRobotDriver`** | **`appRobotFileservice`** |
---
## 2. Leitprinzip — der Driver ist das einzige Front Door
**Vorgabe:** Alle Steuerungen (Joystick, Tastatur, Bilderkennung,
sensor-gesteuerte Programme …) kennen **nur den Driver**. Sie sprechen die
appRobotFileservice **niemals direkt** an — nur indirekt, *durch den Driver hindurch*.
```
Steuerungen → Driver → appRobotFileservice
(nur EINE Verbindung pro Steuerung: zum Driver)
```
Daraus folgt eine **einseitige Abhängigkeit**:
```
Steuerung ──kennt──► Driver ──kennt──► appRobotFileservice
(Gateway) (passiver Storage-Dienst)
• Der Driver hängt von der appRobotFileservice ab (ruft sie).
• Die appRobotFileservice hängt von NICHTS ab — sie ruft den Driver nie an,
kennt weder dessen URL noch dessen Pose.
• Steuerungen brauchen KEINEN neuen Weg: sie reden weiter nur mit dem Driver.
```
> **Abgrenzung:** Gemeint sind **Steuerungen** (Echtzeit-Eingaben). Die
> **Visualisierungs-/Verwaltungs-UI** der appRobotFileservice ist Teil *jenes*
> Projekts und darf den Fileservice direkt ansprechen — sie ist keine Steuerung.
---
## 3. Befehls-Routing im Driver (der „Pass-through")
Der Driver klassifiziert jede eingehende Nachricht und routet sie:
```
eingehende Nachricht am Driver (WS :2095 oder POST /api/gcode)
├─ Bewegung (G…, M1, M92, G92) → lokal ausführen → Pose broadcast
├─ Status (Ping, M114) → gezielt antworten
├─ FCode (FShow, FList, FPoint …) → an appRobotFileservice weiterreichen
└─ sonst → Fehler-Envelope
```
### FCodes — eine Befehlsfamilie wie die G-/M-Codes
G-Code kennt `G1`, `G2`, `Gx` und `M1`, `M92`, … . Analog bilden die **FCodes** eine
eigene Familie für Datei-/Programm-Befehle — **ohne Sonderzeichen**, einfach `F` +
Wort:
| FCode (Steuerung → Driver) | Bedeutung | Driver leitet weiter an |
|---|---|---|
| `FList` | Programme auflisten | `GET /programs` |
| `FShow [id]` | Inhalt anzeigen | `GET /programs/{id}` |
| `FLoad <id>` | Programm aktiv setzen | `PUT /active` |
| `FSave <name>` | aktiven Puffer speichern | `POST /programs` |
| `FClear` | aktives Programm leeren | `POST /active/clear` |
| `FPoint` | **aktuelle Pose** aufnehmen | `POST /active/points` (Driver hängt Pose an) |
| `FPlus` / `FMinus` | nächste / vorige Zeile | `POST /active/next` / `/prev` |
| `FFirst` / `FLast` | an Anfang / Ende | `POST /active/first` / `/last` |
| `FGoto <n>` | zu Zeile springen | `POST /active/goto` |
| `FPlay` / `FStop` | durchlaufen / anhalten | `POST /active/play` / `/stop` |
**Warum kein Sonderzeichen-Prefix nötig ist:** Eine Bewegungszeile beginnt mit `G`
oder `M`; ein FCode mit `F`+Buchstabe. Das Feedrate-Wort `F1000` ist `F`+Ziffer und
steht **nur innerhalb** einer `G`-Zeile, nie am Anfang. Der Router muss also nur
**am Nachrichtenanfang** prüfen: `F` + Buchstabe → FCode. Damit ist die Familie
kollisionsfrei — gegen die Lesbarkeit spricht nichts.
`FFirst`/`FLast` werden dabei endlich umgesetzt (heute erkannt, aber nicht
implementiert — vgl. ToDo_6b / Bug 2). Konkrete API:
[`draft_filehandeling_API.md`](draft_filehandeling_API.md).
---
## 4. Zwei Datei-Welten — nur eine wandert aus
| Welt | Beispiele | Verbleib |
|---|---|---|
| **Betriebs-Logs** | `logs/gcode_commands.log`, `logs/pings.log` | **bleibt im Driver** |
| **G-Code-Programme** | `GCodeFiles/*.gcode` | **wird ausgelagert** (`appRobotFileservice`) |
Die Logs betreffen den Hardware-/Verbindungsbetrieb und bleiben. Ausgelagert wird
ausschließlich `GCodeFiles/` samt Cursor und FCodes.
---
## 5. Was bleibt im Driver, was wird ausgelagert
| Heute (in [`robot/GCode.js`](../robot/GCode.js)) | Ziel | Anmerkung |
|---|---|---|
| `receiveGCode` / `containsCommand` / `receiveMCode` | **bleibt** | reine Bewegung |
| `getM114` / `GET /api/position` | **bleibt** | Pose-Quelle für `FPoint` |
| `logCommand` / `logPing` | **bleibt** | Betriebs-Logging |
| Routing der FCodes | **bleibt als dünner Proxy** | neuer Gateway-Zweig in `InputWS` |
| `receiveFC` (Programm-Logik) | **appRobotFileservice** | Verwaltung |
| `static fileName`, `;!`-Cursor | **appRobotFileservice** (Cursor: In-Memory-Index, persistiert als `!`-Kommentar) | löst ToDo_6b Paket 2 |
| `removeStringFromFile` | **entfällt** | nur für den `;!`-Hack nötig |
| `toPiMultiple` (Grad→Radian) | **entfällt im Driver** → Umrechnung lebt im Fileservice | siehe §7 |
| Zeilen-String-Bau in `FPoint` | **appRobotFileservice** | Zeilenformat ist Programm-Logik |
Im Driver bleibt also: Bewegung, Pose, Logs — **plus ein dünner Proxy-Zweig**, der
FCodes weiterreicht. Kein `GCodeFiles/`-IO, kein Cursor, **keine** Einheiten-Umrechnung.
---
## 6. Die zwei Kernabläufe
### 6a. Playback (Datei → Roboter)
```
Steuerung → Driver: FPlus
Driver → Fileservice: POST /active/next (Cursor++)
Fileservice → Driver: { line: "G90 G1 x310 y444 … a1.5708 …" } (driver-nativ, Radian)
Driver: receiveGCode(line) → Achsen bewegen
Driver: Pose-Broadcast an alle WS-Clients
```
Die appRobotFileservice liefert eine **fertig ausführbare, driver-native Zeile**; der
Driver führt sie über seinen normalen `receiveGCode`-Pfad aus — *keine*
Sonderbehandlung, *keine* Umrechnung.
### 6b. Teaching / Training (Roboter → Datei) — der robotik-spezifische Fall
Der Arm wird **per Joystick** bewegt; G-Code ist hier **Ausgabe**. Entscheidend:
Beim `FPoint` hat der **Driver die Live-Pose bereits lokal**.
```
Steuerung (Joystick) → Driver: G1 …/$J= (Arm bewegen, lokal)
Steuerung → Driver: FPoint
Driver: hängt die AKTUELLE Pose an (robot.x … robot.e, feedrate)
Driver → Fileservice: POST /active/points { pose:{ x,y,z, a,b,c, e }, feedrate }
Fileservice: Pose → Grad → als G-Code-Zeile persistieren, Cursor ans Ende
Fileservice → Driver: { index, line }
Driver → Steuerung: Bestätigung
```
Der Driver ist die Quelle der Wahrheit für die Pose und reicht sie beim Forwarden
mit. Die appRobotFileservice muss den Driver dafür **nicht** anrufen.
---
## 7. Einheiten: Driver bleibt Radian, der Fileservice rechnet um
Die Datei soll **wie Standard-G-Code aussehen** (Grad, `a-90.00`). Der Driver
arbeitet intern und am G-Code-Eingang in **Radian** (Beleg: `receiveGCode` setzt
`robot.phi = A` ohne Umrechnung). Beides ist vereinbar, ohne dass der Driver etwas
umrechnen muss:
| Achse | `.gcode`-Datei (Storage) | Wire Driver ↔ Fileservice | Driver intern |
|---|---|---|---|
| `x y z` | mm | mm | mm |
| `a b c` (φ/θ/ψ) | **Grad** (`a-90.00`) | **Radian** | Radian |
| `e` (Greifer) | **Grad** | **Radian** | Radian |
| Umrechnung | — | **in der appRobotFileservice** | **keine** |
- **Driver:** rechnet nie um — `toPiMultiple` **entfällt** ersatzlos (harter Übergang).
- **appRobotFileservice:** konvertiert an ihrer **Storage-Grenze**: beim Lesen für
Playback Grad→Radian, beim `FPoint`-Schreiben Radian→Grad. Damit liegt die
Umrechnung an genau **einer** Stelle und ist testbar (löst ToDo_6b Paket 3).
So bleibt die Datei standardnah und lesbar, der Hot-Path im Driver aber sauber.
---
## 8. Storage-Modell der appRobotFileservice: GCode-Datei + JSON-Sidecar
Ziel: am Ende stehen **Dateien, die wie G-Code aussehen** (möglichst nah an einem
Standard). Pro Programm:
```
GCodeFiles/
besteck_spuelmaschine.gcode ← das Programm, sieht aus wie Standard-G-Code (Grad)
besteck_spuelmaschine.json ← Sidecar: Metadaten + Verwaltung
```
- **`.gcode`** (alternativ `.ngc`): standardnahe Bewegungszeilen, Winkel in **Grad**.
Zeitstempel **und** Cursor stehen im **G-Code-Kommentarfeld** (`;…`) — so bleibt die
Zeile standardkonform (Kommentare sind Teil des G-Code-Standards):
- jede Zeile endet mit `;<epoch>` (Aufnahme-Zeitstempel),
- die **Cursor-Zeile** trägt zusätzlich ein `!`: `;<epoch>!`.
```
G90 G1 x0 y300 z0 a90.00 b-90.00 c0.00 e0.00 f1000 ;1759566014
G90 G1 x310 y444 z0.5 a90.00 b-90.00 c0.00 e6.88 f1000 ;1759566112!
G90 G1 x310 y444 z30.5 a90.00 b-90.00 c0.00 e6.88 f1000 ;1759566118
```
Damit ist die `.gcode` **ohne Sidecar vollständig** (Bewegung + Zeitstempel + Cursor).
- **`.json`-Sidecar** (Komfort/Verwaltung): Anzeigename, `createdAt`/`updatedAt`,
`lineCount`, `angleUnit` (`"deg"`), optional benannte Labels (`"pick"`, `"place"`
→ Zeilenindex). Quelle der Wahrheit für Bewegung/Zeitstempel/Cursor bleibt die `.gcode`.
Nach außen (API) werden Programme über **id/Name** angesprochen, **nie über
Dateipfade** — `GCodeFiles/` und das Sidecar-Schema bleiben **intern** in der
appRobotFileservice. Damit entfällt die `../`-Pfad-Problematik (ToDo_6b Paket 4) und
ein späterer Wechsel des Storage bleibt unsichtbar.
---
## 9. Gemeinsamer Zustand: aktives Programm + Cursor (im Fileservice)
Die appRobotFileservice hält genau einen **„aktives Programm + Cursor"**-Zustand als
*Single Source of Truth*. Weil alle Steuerungen durch denselben Driver auf denselben
Fileservice gehen, teilen sie automatisch denselben Cursor — `FPlus` vom Joystick und
gleich darauf `FPlus` von der Bilderkennung sehen denselben Stand.
- `aktivesProgramm` — id/Name (ersetzt `static fileName`).
- `cursor` — während einer Session **Zeilenindex im Speicher** (schnelles Stepping
ohne Neu-Schreiben). Beim Laden aus dem `!`-Kommentar gelesen, beim Speichern/
Entladen als `!` in die Cursor-Zeile zurückgeschrieben — so ist der Cursor
persistiert, **ohne** bei jedem `FPlus` die ganze Datei neu zu schreiben (löst
ToDo_6b Paket 2).
---
## 10. `/api/gcode` & WS — der Steuerungs-Kanal
`POST /api/gcode` am Driver (optional, REST-Alternative zur WS) und die WS `:2095`
sind der **Bewegungs-Eingang für alle Steuerungen**:
- **Zugriff: alle Steuerungen** (Joystick, Tastatur, Bilderkennung, Sensorik).
- **Nicht** die appRobotFileservice — sie pusht nie Bewegung an den Driver; der
Driver führt Playback-Zeilen selbst aus (§6a). Der Fileservice braucht **keinen**
Driver-Zugang.
---
## 11. Durchgereichte Payload-Größen
Der Driver reicht bei `FShow`/`FList` ggf. größere Mengen durch (Datei-Inhalt,
Listen). Das ist akzeptabel: die **appRobotFileservice** hält diese Antworten später
klein (z. B. Paginierung, Kurz-/Übersichtsform), sodass der Durchreich-Weg über den
Driver unkritisch bleibt.
---
## 12. Erforderliche kleine Driver-Ergänzungen
1. **`InputWS`-Router:** neuer Zweig „FCode am Anfang (`F`+Buchstabe) → an Fileservice
forwarden, Antwort zurückreichen". Playback-Zeile lokal ausführen; Verwaltungs-
Antworten gezielt an den Anfrager, Pose-ändernde Aktionen broadcasten (analog ToDo_5).
2. **`FPoint`-Pose:** Der Driver muss die **Live-Pose inkl. Greifer `e`** (und φ/θ/ψ)
mitliefern. Heute setzt `getM114` `e` hart auf `0.0` — sonst geht die
Greiferstellung beim Aufnehmen verloren.
3. **`POST /api/gcode`** (optional): REST-Bewegungs-Eingang für Steuerungen ohne WS.
---
## 13. Offene Fragen
- **FCode-Namen:** bestehende Familie (`FPlus`/`FMinus` …) beibehalten oder einzelne
umbenennen (`FNext`/`FPrev`)? — Empfehlung: bestehende behalten, neue ergänzen.
- **Cursor-Persistenz:** als `!`-Kommentar in der `.gcode` (gewählt) — Häufigkeit des
Zurückschreibens (sofort vs. debounced beim Entladen) noch offen.
- **Sidecar-Umfang:** Metadaten + Labels (Cursor & Zeitstempel liegen in der `.gcode`).
---
## 14. Verweise
- [`draft_filehandeling_API.md`](draft_filehandeling_API.md) — appRobotFileservice-Schnittstelle
- [`ToDo_4_GCode.md`](ToDo_4_GCode.md) · [`ToDo_6b_FileHandling.md`](ToDo_6b_FileHandling.md) — abgelöst/gelöst
- [`ToDo_5_API.md`](ToDo_5_API.md) / [`API.md`](API.md) — Routing & Fehler-Envelope
- [`robot/GCode.js`](../robot/GCode.js) · [`server/InputWS.js`](../server/InputWS.js) · [`server/InfoServer.js`](../server/InfoServer.js)

View File

@@ -1,253 +0,0 @@
# Draft — `appRobotFileservice` API
> **Status:** Entwurf. Schnittstelle des ausgelagerten Programm-Handlings
> (`appRobotFileservice`). Konzept & Rollenteilung:
> [`draft_filehandeling.md`](draft_filehandeling.md).
>
> **Einziger Consumer ist der Driver** (`appRobotDriver`). Steuerungen sprechen die
> appRobotFileservice nie direkt an, sondern schicken **FCodes** an den Driver, der
> sie hierher weiterreicht. Die appRobotFileservice ist **passiv und
> driver-agnostisch**: sie ruft den Driver nie an, kennt weder dessen URL noch dessen
> Pose. (Eine eigene Visualisierungs-UI darf direkt zugreifen — sie ist keine Steuerung.)
---
## 1. Überblick
- **Transport:** HTTP/REST + JSON. Optional ein WebSocket-Event-Kanal (Abschnitt 8).
- **Basis-URL (Vorschlag):** `https://<host>:2100/api`
- **Identität:** Programme über **`id`/Name** — **nie über Dateipfade**. Storage
(`.gcode` + `.json`-Sidecar) ist intern gekapselt.
- **Einheiten am Wire:** **driver-nativ** (φ/θ/ψ und `e` in **Radian**, `x/y/z` in
mm) — exakt die G-Code-Strings, die der Driver ausführt. **Gespeichert** wird in
**Grad** (standardnahe `.gcode`); die appRobotFileservice rechnet an ihrer
Storage-Grenze um (Konzept §7).
- **Auth:** `Bearer <FILE_API_KEY>` für schreibende Operationen (analog `ROBOT_API_KEY`).
---
## 2. Datenmodell
### Program (Metadaten, aus dem `.json`-Sidecar)
```json
{ "id": "besteck_spuelmaschine", "name": "Besteck Spülmaschine",
"lineCount": 12, "angleUnit": "deg",
"createdAt": "2025-10-04T10:25:00Z", "updatedAt": "2025-10-04T10:41:00Z" }
```
### ActiveState (aktives Programm + Cursor — Single Source of Truth)
```json
{ "programId": "besteck_spuelmaschine", "cursor": 4, "lineCount": 12,
"currentLine": "G90 G1 x310 y444 z0.5 a1.5708 b-1.5708 c0 e0.12 f1000",
"playing": false, "version": 7 }
```
> `currentLine` ist **driver-nativ (Radian)** und kommentarfrei — direkt ausführbar.
> Gespeichert wird in **Grad** mit Zeitstempel-Kommentar (`draft_filehandeling.md` §8).
### Pose (vom Driver beim `FPoint` mitgeschickt)
Native Radian-Werte inkl. Greifer `e`:
```json
{ "pose": { "x": 0, "y": 300, "z": 0, "a": 1.5708, "b": -1.5708, "c": 0, "e": 0.12 },
"feedrate": 1000 }
```
---
## 3. FCode ↔ Endpoint-Mapping
Der Driver übersetzt die FCodes der Steuerungen in diese Endpoints:
| FCode | Endpoint | Antwort an Steuerung (über Driver) |
|---|---|---|
| `FList` | `GET /programs` | Liste (gezielt) |
| `FShow [id]` | `GET /programs/{id}` | Inhalt in **Grad** (gezielt) |
| `FLoad <id>` | `PUT /active` | ActiveState (gezielt) |
| `FSave <name>` | `POST /programs` | id (gezielt) |
| `FClear` | `POST /active/clear` | ActiveState (gezielt) |
| `FPoint` | `POST /active/points` | Bestätigung (gezielt) |
| `FPlus` | `POST /active/next` | Bewegung → **Pose-Broadcast** |
| `FMinus` | `POST /active/prev` | Bewegung → **Pose-Broadcast** |
| `FFirst` | `POST /active/first` | Bewegung → **Pose-Broadcast** |
| `FLast` | `POST /active/last` | Bewegung → **Pose-Broadcast** |
| `FGoto <n>` | `POST /active/goto` | Bewegung → **Pose-Broadcast** |
| `FPlay` / `FStop` | `POST /active/play` / `/stop` | Status |
---
## 4. Endpoints — Programm-Verwaltung
### `GET /programs` ← `FList`
```json
{ "programs": [ { "id": "log", "name": "log", "lineCount": 36 }, ] }
```
### `GET /programs/{id}` ← `FShow`
Inhalt + Metadaten für die Anzeige — in **Grad**, wie gespeichert (lesbar):
```json
{ "id": "besteck_spuelmaschine", "displayUnit": "deg",
"lines": [ "G90 G1 x0 y614 z0 a-90.00 b90.00 c0.00 e0 f1000 ;1759566014",
"G90 G1 x0 y300 z0 a90.00 b-90.00 c0.00 e0 f1000 ;1759566052!" ] }
```
> Kommentar `;<epoch>` = Aufnahme-Zeitstempel; ein abschließendes `!` markiert die Cursor-Zeile.
### `POST /programs` ← `FSave`
```jsonc
{ "name": "Demo C", "fromActive": true } // aus aktivem Puffer
// oder expliziter Inhalt (in Grad, wie eine .gcode):
{ "name": "Demo C", "lines": ["G90 G1 x0 y300 … a90.00 …"], "angleUnit": "deg" }
```
`201 { "id": "demo_c", "lineCount": 12 }` (legt `demo_c.gcode` + `demo_c.json` an)
### `PUT /programs/{id}` · `DELETE /programs/{id}`
Inhalt ersetzen / umbenennen · löschen (jeweils `.gcode` **und** `.json`).
---
## 5. Endpoints — Aktives Programm & Cursor
### `GET /active`
Aktuellen `ActiveState` lesen.
### `PUT /active` ← `FLoad`
```json
{ "id": "besteck_spuelmaschine" }
```
`ActiveState`. Validierung: existiert, ≥1 gültige Zeile (sonst `EMPTY_PROGRAM`).
### `POST /active/clear` ← `FClear`
Aktives Programm leeren, Cursor → 0.
### Stepping — `next` · `prev` · `first` · `last` · `goto`
Bewegt den Cursor und gibt die **driver-native, ausführbare Zeile (Radian)** zurück.
Der **Driver führt sie selbst aus** — der Fileservice pusht nichts.
`POST /active/next` · `/prev` · `/first` · `/last` · `/goto` `{ "index": 7 }`
```json
{ "cursor": 5, "line": "G90 G1 x310 y444 z30.5 a1.5708 b-1.5708 c0 e0.12 f1000" }
```
Grenzen: `next` am Ende / `prev` am Anfang → `CURSOR_OUT_OF_RANGE` (optional `wrap`).
---
## 6. Endpoints — Teaching / Aufnahme
### `POST /active/points` ← `FPoint`
Der **Driver schickt die aktuelle Pose mit** (native Radian-Werte). Die
appRobotFileservice rechnet **nach Grad** um, formatiert die Zeile (Feedrate,
Zeitstempel als Kommentar `;<epoch>`) und hängt sie an.
```json
{ "pose": { "x": 0, "y": 300, "z": 0, "a": 1.5708, "b": -1.5708, "c": 0, "e": 0.12 },
"feedrate": 1000 }
```
`201 { "index": 12, "line": "G90 G1 x0 y300 z0 a90.00 b-90.00 c0.00 e6.88 f1000 ;1759566014" }`
### `POST /active/lines`
Rohe Zeile(n) anhängen/einfügen (z. B. Pause `G4`):
```json
{ "line": "G4 P0.5", "atIndex": 8 }
```
### `PUT /active/lines/{index}` · `DELETE /active/lines/{index}`
Einzelne Zeile ersetzen / löschen (Editieren der Aufnahme).
---
## 7. Endpoints — Playback (kontinuierlich)
### `POST /active/play` ← `FPlay`
```jsonc
{ "mode": "run", "fromStart": false } // "run" = bis Ende/Stop; "step" = eine Zeile
```
Die appRobotFileservice liefert die Zeilen getaktet zurück bzw. meldet Fortschritt
über den Event-Kanal (§8); **ausgeführt werden sie vom Driver**. `POST /active/stop`
hält an.
> **„Nächste File"** (Playlist über mehrere Programme) baut darauf auf:
> `POST /playlist/next` lädt das nächste Programm (`PUT /active`) und startet `play`.
---
## 8. Optionaler Event-Kanal (WebSocket)
Für eine Live-UI der appRobotFileservice (Fortschritt) ohne Polling:
```json
{ "event": "cursorMoved", "cursor": 5, "line": "G90 G1 … a1.5708 …" }
{ "event": "activeChanged", "programId": "demo_c", "lineCount": 12 }
{ "event": "playStopped", "cursor": 9, "reason": "end" }
```
(Die *Roboter*-Pose-Updates laufen weiterhin über den Driver-WS-Broadcast — der
Fileservice kennt die Pose nur, soweit der Driver sie beim `FPoint` mitgibt.)
---
## 9. Fehler-Envelope
Konsistent mit dem Driver (`doc/ToDo_5_API.md`): `{ type, code, message, input }`.
Der Driver reicht Fileservice-Fehler unverändert an die Steuerung zurück.
| `code` | Bedeutung |
|---|---|
| `PROGRAM_NOT_FOUND` | `{id}` existiert nicht |
| `INVALID_NAME` | unzulässiger Name (kein Pfad) |
| `EMPTY_PROGRAM` | `FLoad` auf Programm ohne gültige Zeile |
| `CURSOR_OUT_OF_RANGE` | `next`/`prev`/`goto` über die Grenzen |
| `NO_ACTIVE_PROGRAM` | Aktion erfordert geladenes Programm |
| `FILE_ERROR` | Storage-Fehler (`.gcode`/`.json`) |
```json
{ "type": "error", "code": "PROGRAM_NOT_FOUND", "message": "no program 'demo_x'", "input": "demo_x" }
```
---
## 10. Durchgereichte Payloads
`FShow`/`FList` können größere Antworten erzeugen, die der Driver nur durchreicht.
Die appRobotFileservice hält sie **akzeptabel klein** (Paginierung, Übersichtsform),
sodass der Weg über den Driver unkritisch bleibt.
---
## 11. Konfiguration
Die appRobotFileservice braucht **keinen** Driver-Zugang (kein `DRIVER_BASE_URL`).
| Variable | Zweck | Beispiel |
|---|---|---|
| `FILE_SERVICE_PORT` | Port | `2100` |
| `STORAGE_DIR` | Verzeichnis für `.gcode` + `.json` | `./GCodeFiles` |
| `FILE_EXT` | `gcode` oder `ngc` | `gcode` |
| `STORE_ANGLE_UNIT` | Speichereinheit der Winkel | `deg` |
| `FILE_API_KEY` | Bearer-Token (Schreiben) | — |
---
## 12. Beispiel-Flows (durch den Driver)
### Teaching-Session (Joystick → Aufnahme)
```
Steuerung → Driver: FLoad demo_c → Driver: PUT /active {id:"demo_c"}
Steuerung → Driver: G1 …/$J= (Arm bewegen, lokal — Fileservice unbeteiligt)
Steuerung → Driver: FPoint → Driver hängt Live-Pose an,
POST /active/points { pose, feedrate }
… weitere Punkte …
Steuerung → Driver: FSave "Demo C" → Driver: POST /programs {name,fromActive:true}
→ demo_c.gcode + demo_c.json
```
### Playback-Session (Datei → Roboter, schrittweise)
```
Steuerung → Driver: FList → GET /programs (Auswahl)
Steuerung → Driver: FLoad demo_c → PUT /active
Steuerung → Driver: FFirst → POST /active/first → {line (Radian)}
Driver: receiveGCode(line) → Bewegung
Driver: Pose-Broadcast an alle UIs
Steuerung → Driver: FPlus … / FPlay
```
---
## 13. Verweise
- [`draft_filehandeling.md`](draft_filehandeling.md) — Konzept, Gateway-Rolle, Einheiten, Storage
- [`API.md`](API.md) — bestehende Driver-Endpunkte (`/api/position`, WS `:2095`)
- [`ToDo_6b_FileHandling.md`](ToDo_6b_FileHandling.md) — gelöste Detailprobleme

211
doc/fileserviceAPI.md Normal file
View File

@@ -0,0 +1,211 @@
# `appRobotFileservice` — REST API
> **Einziger Consumer ist der Driver** (`appRobotDriver`). Steuerungen sprechen die
> appRobotFileservice nie direkt an, sondern schicken **FCodes** an den Driver, der
> sie hierher weiterreicht (`robot/FCodeClient.js`). Die appRobotFileservice ist
> **passiv und driver-agnostisch**: sie ruft den Driver nie an, kennt weder dessen
> URL noch dessen Pose.
> Eine eigene Visualisierungs-UI darf direkt zugreifen — sie ist keine Steuerung.
---
## 1. Überblick
- **Transport:** HTTP/REST + JSON.
- **Basis-URL:** `http://appRobot_Fileservice:2100/api` (Container-intern) · `http://thinkcentre.local:2100/api` (lokal)
- **Identität:** Programme über **`id`/Name** — **nie über Dateipfade**. Storage
(`.gcode` + `.json`-Sidecar) ist intern gekapselt.
- **Einheiten am Wire:** **driver-nativ** (φ/θ/ψ und `e` in **Radian**, `x/y/z` in
mm) — exakt die G-Code-Strings, die der Driver ausführt. **Gespeichert** wird in
**Grad** (standardnahe `.gcode`); die appRobotFileservice rechnet an ihrer
Storage-Grenze um.
- **Auth:** `Bearer <FILE_API_KEY>` für schreibende Operationen.
---
## 2. Datenmodell
### Program (Metadaten, aus dem `.json`-Sidecar)
```json
{ "id": "besteck_spuelmaschine", "name": "Besteck Spülmaschine",
"lineCount": 12, "angleUnit": "deg",
"createdAt": "2025-10-04T10:25:00Z", "updatedAt": "2025-10-04T10:41:00Z" }
```
### ActiveState (aktives Programm + Cursor — Single Source of Truth)
```json
{ "programId": "besteck_spuelmaschine", "cursor": 4, "lineCount": 12,
"currentLine": "G90 G1 x310 y444 z0.5 a1.5708 b-1.5708 c0 e0.12 f1000",
"playing": false, "version": 7 }
```
> `currentLine` ist **driver-nativ (Radian)** und kommentarfrei — direkt ausführbar.
> Gespeichert wird in **Grad** mit Zeitstempel-Kommentar im Kommentarfeld (`;`).
### Pose (vom Driver beim `FPoint` mitgeschickt)
```json
{ "pose": { "x": 0, "y": 300, "z": 0, "a": 1.5708, "b": -1.5708, "c": 0, "e": 0.12 },
"feedrate": 1000 }
```
---
## 3. FCode ↔ Endpoint-Mapping
Der Driver (`robot/FCodeClient.js`) übersetzt die FCodes der Steuerungen in diese Endpoints:
| FCode | Endpoint | Antwort an Steuerung (über Driver) |
|---|---|---|
| `FList` | `GET /programs` | Liste (Broadcast) |
| `FShow [id]` | `GET /programs/{id}` | Inhalt in **Grad** (Broadcast) |
| `FLoad <id>` | `PUT /active` | ActiveState (Broadcast) |
| `FSave <name>` | `POST /programs` | id (Broadcast) |
| `FClear` | `POST /active/clear` | ActiveState (Broadcast) |
| `FPoint` | `POST /active/points` | Bestätigung (Broadcast) |
| `FPlus` | `POST /active/next` | Bewegung → Driver führt aus → **Pose-Broadcast** |
| `FMinus` | `POST /active/prev` | Bewegung → Driver führt aus → **Pose-Broadcast** |
| `FFirst` | `POST /active/first` | Bewegung → Driver führt aus → **Pose-Broadcast** |
| `FLast` | `POST /active/last` | Bewegung → Driver führt aus → **Pose-Broadcast** |
| `FGoto <n>` | `POST /active/goto` | Bewegung → Driver führt aus → **Pose-Broadcast** |
| `FPlay` / `FStop` | `POST /active/play` / `/stop` | Status (Broadcast) |
---
## 4. Endpoints — Programm-Verwaltung
### `GET /api/programs` ← `FList`
```json
{ "programs": [ { "id": "log", "name": "log", "lineCount": 36 }, ] }
```
### `GET /api/programs/{id}` ← `FShow`
Inhalt + Metadaten — in **Grad**, wie gespeichert (lesbar):
```json
{ "id": "besteck_spuelmaschine", "displayUnit": "deg",
"lines": [ "G90 G1 x0 y614 z0 a-90.00 b90.00 c0.00 e0 f1000 ;1759566014",
"G90 G1 x0 y300 z0 a90.00 b-90.00 c0.00 e0 f1000 ;1759566052!" ] }
```
> `;<epoch>` = Aufnahme-Zeitstempel; abschließendes `!` = Cursor-Zeile.
### `POST /api/programs` ← `FSave`
```jsonc
{ "name": "Demo C", "fromActive": true } // aus aktivem Puffer
// oder expliziter Inhalt (in Grad):
{ "name": "Demo C", "lines": ["G90 G1 x0 y300 … a90.00 …"], "angleUnit": "deg" }
```
`201 { "id": "demo_c", "lineCount": 12 }`
### `PUT /api/programs/{id}` · `DELETE /api/programs/{id}`
Inhalt ersetzen / umbenennen · löschen (jeweils `.gcode` **und** `.json`).
---
## 5. Endpoints — Aktives Programm & Cursor
### `GET /api/active`
Aktuellen `ActiveState` lesen (inkl. `currentLine` in Radian).
### `PUT /api/active` ← `FLoad`
```json
{ "id": "besteck_spuelmaschine" }
```
`ActiveState`. Nicht-existierendes Programm wird **leer angelegt** (für Teaching).
### `POST /api/active/clear` ← `FClear`
Aktives Programm leeren, Cursor → 0.
### Stepping — `next` · `prev` · `first` · `last` · `goto`
Bewegt den Cursor und gibt die **driver-native, ausführbare Zeile (Radian)** zurück.
Der **Driver führt sie selbst aus** — der Fileservice pusht nichts.
`POST /api/active/next` · `/prev` · `/first` · `/last` · `/goto` `{ "index": 7 }`
```json
{ "cursor": 5, "line": "G90 G1 x310 y444 z30.5 a1.5708 b-1.5708 c0 e0.12 f1000" }
```
Grenzen: `next` am Ende / `prev` am Anfang → `CURSOR_OUT_OF_RANGE`.
---
## 6. Endpoints — Teaching / Aufnahme
### `POST /api/active/points` ← `FPoint`
Der **Driver schickt die aktuelle Pose mit** (native Radian-Werte). Die
appRobotFileservice rechnet **nach Grad** um, formatiert die Zeile (Feedrate,
Zeitstempel als Kommentar `;<epoch>`) und hängt sie an.
```json
{ "pose": { "x": 0, "y": 300, "z": 0, "a": 1.5708, "b": -1.5708, "c": 0, "e": 0.12 },
"feedrate": 1000 }
```
`201 { "index": 12, "line": "G90 G1 x0 y300 z0 a90.00 b-90.00 c0.00 e6.88 f1000 ;1759566014" }`
### `POST /api/active/lines`
Rohe Zeile(n) anhängen/einfügen (z. B. Pause `G4`):
```json
{ "line": "G4 P0.5", "atIndex": 8 }
```
### `PUT /api/active/lines/{index}` · `DELETE /api/active/lines/{index}`
Einzelne Zeile ersetzen / löschen.
---
## 7. Endpoints — Playback
### `POST /api/active/play` ← `FPlay`
```jsonc
{ "mode": "run", "fromStart": false }
```
### `POST /api/active/stop` ← `FStop`
---
## 8. Fehler-Envelope
Konsistent mit dem Driver (`doc/ToDo_5_API.md`): `{ type, code, message, input }`.
Der Driver reicht Fileservice-Fehler (`FILE_ERROR`) an die Steuerung zurück.
| `code` | Bedeutung |
|---|---|
| `PROGRAM_NOT_FOUND` | `{id}` existiert nicht |
| `INVALID_NAME` | unzulässiger Name (kein Pfad) |
| `EMPTY_PROGRAM` | `FLoad` auf Programm ohne gültige Zeile |
| `CURSOR_OUT_OF_RANGE` | `next`/`prev`/`goto` über die Grenzen |
| `NO_ACTIVE_PROGRAM` | Aktion erfordert geladenes Programm |
| `FILE_ERROR` | Storage-Fehler (`.gcode`/`.json`) |
```json
{ "type": "error", "code": "PROGRAM_NOT_FOUND", "message": "no program 'demo_x'", "input": "demo_x" }
```
---
## 9. Beispiel-Flows
### Teaching-Session (Joystick → Aufnahme)
```
Steuerung → Driver: FLoad demo_c → Driver: PUT /api/active {id:"demo_c"}
Steuerung → Driver: G1 … (Arm bewegen, lokal — Fileservice unbeteiligt)
Steuerung → Driver: FPoint → Driver hängt Live-Pose an,
POST /api/active/points { pose, feedrate }
… weitere Punkte …
Steuerung → Driver: FSave "Demo C" → Driver: POST /api/programs {name,fromActive:true}
```
### Playback-Session (schrittweise)
```
Steuerung → Driver: FList → GET /api/programs
Steuerung → Driver: FLoad demo_c → PUT /api/active
Steuerung → Driver: FFirst → POST /api/active/first → {line (Radian)}
Driver: receiveGCode(line) → Bewegung
Driver: Pose-Broadcast an alle WS-Clients
Steuerung → Driver: FPlus … / FPlay
```
---
## 10. Verweise
- [`API.md`](API.md) — Driver-Endpunkte (`/api/position`, WS `:2095`)
- [`robot/FCodeClient.js`](../robot/FCodeClient.js) — Gateway-Implementierung im Driver
- [`ToDo_6b_FileHandling.md`](ToDo_6b_FileHandling.md) — gelöste Detailprobleme
- `appRobotFileservice/README.md` — Konzept, Einheiten, Dateiformat, Konfiguration

View File

@@ -10565,3 +10565,890 @@
2026-06-14T07:32:17.813Z ::ffff:127.0.0.1: M114
2026-06-14T07:32:17.848Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T07:32:17.959Z ::ffff:127.0.0.1: G1 X1
2026-06-14T08:39:38.186Z ::ffff:127.0.0.1: M114
2026-06-14T08:39:38.208Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T08:39:38.645Z ::ffff:127.0.0.1: M114
2026-06-14T08:39:38.872Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T08:39:39.103Z ::ffff:127.0.0.1: G1 X1
2026-06-14T08:40:02.998Z ::ffff:127.0.0.1: M114
2026-06-14T08:40:03.013Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T08:40:03.182Z ::ffff:127.0.0.1: M114
2026-06-14T08:40:03.395Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T08:40:10.444Z ::ffff:127.0.0.1: G1 X1
2026-06-14T08:40:22.092Z ::ffff:127.0.0.1: M114
2026-06-14T08:40:22.108Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T08:40:22.273Z ::ffff:127.0.0.1: M114
2026-06-14T08:40:22.501Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T08:40:29.545Z ::ffff:127.0.0.1: G1 X1
2026-06-14T08:40:42.014Z ::ffff:127.0.0.1: M114
2026-06-14T08:40:42.024Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T08:40:42.186Z ::ffff:127.0.0.1: M114
2026-06-14T08:40:42.410Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T08:40:42.625Z ::ffff:127.0.0.1: G1 X1
2026-06-14T08:40:49.232Z ::ffff:127.0.0.1: M114
2026-06-14T08:40:49.245Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T08:40:49.418Z ::ffff:127.0.0.1: M114
2026-06-14T08:40:49.639Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T08:40:49.867Z ::ffff:127.0.0.1: G1 X1
2026-06-14T08:41:14.185Z ::ffff:127.0.0.1: M114
2026-06-14T08:41:14.201Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T08:41:14.420Z ::ffff:127.0.0.1: M114
2026-06-14T08:41:14.634Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T08:41:14.864Z ::ffff:127.0.0.1: G1 X1
2026-06-14T08:41:30.075Z ::ffff:127.0.0.1: M114
2026-06-14T08:41:30.090Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T08:41:30.294Z ::ffff:127.0.0.1: M114
2026-06-14T08:41:30.360Z ::ffff:127.0.0.1: FList
2026-06-14T08:41:30.381Z ::ffff:127.0.0.1: FPlus
2026-06-14T08:41:30.389Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-14T08:41:30.516Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T08:41:30.747Z ::ffff:127.0.0.1: G1 X1
2026-06-14T08:41:43.088Z ::ffff:127.0.0.1: FList
2026-06-14T08:41:43.097Z ::ffff:127.0.0.1: FPlus
2026-06-14T08:41:43.102Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-14T08:41:52.569Z ::ffff:127.0.0.1: FList
2026-06-14T08:41:52.619Z ::ffff:127.0.0.1: FPlus
2026-06-14T08:41:52.640Z ::ffff:127.0.0.1: M114
2026-06-14T08:41:52.660Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-14T08:41:52.709Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T08:41:52.868Z ::ffff:127.0.0.1: M114
2026-06-14T08:41:53.159Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T08:41:53.460Z ::ffff:127.0.0.1: G1 X1
2026-06-14T08:41:56.359Z ::ffff:127.0.0.1: FList
2026-06-14T08:41:56.409Z ::ffff:127.0.0.1: FPlus
2026-06-14T08:41:56.422Z ::ffff:127.0.0.1: M114
2026-06-14T08:41:56.430Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-14T08:41:56.442Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T08:41:56.572Z ::ffff:127.0.0.1: M114
2026-06-14T08:41:56.801Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T08:41:57.043Z ::ffff:127.0.0.1: G1 X1
2026-06-14T08:42:30.513Z ::ffff:127.0.0.1: FList
2026-06-14T08:42:30.565Z ::ffff:127.0.0.1: FPlus
2026-06-14T08:42:30.597Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-14T08:42:30.837Z ::ffff:127.0.0.1: M114
2026-06-14T08:42:30.857Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T08:42:31.049Z ::ffff:127.0.0.1: M114
2026-06-14T08:42:31.273Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T08:42:31.496Z ::ffff:127.0.0.1: G1 X1
2026-06-14T08:42:37.269Z ::ffff:127.0.0.1: FList
2026-06-14T08:42:37.281Z ::ffff:127.0.0.1: M114
2026-06-14T08:42:37.312Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T08:42:37.324Z ::ffff:127.0.0.1: FPlus
2026-06-14T08:42:37.347Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-14T08:42:38.180Z ::ffff:127.0.0.1: M114
2026-06-14T08:42:38.413Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T08:42:38.648Z ::ffff:127.0.0.1: G1 X1
2026-06-14T08:42:40.892Z ::ffff:127.0.0.1: M114
2026-06-14T08:42:40.912Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T08:42:40.945Z ::ffff:127.0.0.1: FList
2026-06-14T08:42:40.982Z ::ffff:127.0.0.1: FPlus
2026-06-14T08:42:40.997Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-14T08:42:41.093Z ::ffff:127.0.0.1: M114
2026-06-14T08:42:41.319Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T08:42:41.548Z ::ffff:127.0.0.1: G1 X1
2026-06-14T09:13:04.091Z ::ffff:127.0.0.1: FList
2026-06-14T09:13:04.245Z ::ffff:127.0.0.1: FPlus
2026-06-14T09:13:04.321Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-14T09:13:04.389Z ::ffff:127.0.0.1: M114
2026-06-14T09:13:04.401Z ::ffff:127.0.0.1: M114
2026-06-14T09:13:04.407Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T09:13:04.619Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T09:13:04.855Z ::ffff:127.0.0.1: G1 X1
2026-06-14T09:13:06.862Z ::ffff:127.0.0.1: FList
2026-06-14T09:13:06.872Z ::ffff:127.0.0.1: M114
2026-06-14T09:13:06.891Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T09:13:06.902Z ::ffff:127.0.0.1: FPlus
2026-06-14T09:13:06.923Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-14T09:13:07.174Z ::ffff:127.0.0.1: M114
2026-06-14T09:13:07.402Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T09:13:07.624Z ::ffff:127.0.0.1: G1 X1
2026-06-14T09:13:28.005Z ::ffff:127.0.0.1: M114
2026-06-14T09:13:28.240Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T09:13:28.517Z ::ffff:127.0.0.1: M114
2026-06-14T09:13:28.515Z ::ffff:127.0.0.1: FList
2026-06-14T09:13:28.542Z ::ffff:127.0.0.1: G1 X1
2026-06-14T09:13:28.559Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T09:13:28.660Z ::ffff:127.0.0.1: FPlus
2026-06-14T09:13:28.696Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-14T09:13:31.390Z ::ffff:127.0.0.1: M114
2026-06-14T09:13:31.420Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T09:13:31.766Z ::ffff:127.0.0.1: FList
2026-06-14T09:13:31.806Z ::ffff:127.0.0.1: FPlus
2026-06-14T09:13:31.819Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-14T09:13:32.017Z ::ffff:127.0.0.1: M114
2026-06-14T09:13:32.235Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T09:13:32.470Z ::ffff:127.0.0.1: G1 X1
2026-06-14T09:13:37.253Z ::ffff:127.0.0.1: M114
2026-06-14T09:13:37.351Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T09:13:37.361Z ::ffff:127.0.0.1: FList
2026-06-14T09:13:37.564Z ::ffff:127.0.0.1: FPlus
2026-06-14T09:13:37.669Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-14T09:13:37.927Z ::ffff:127.0.0.1: M114
2026-06-14T09:13:38.155Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T09:13:38.393Z ::ffff:127.0.0.1: G1 X1
2026-06-14T09:13:40.695Z ::ffff:127.0.0.1: FList
2026-06-14T09:13:40.737Z ::ffff:127.0.0.1: FPlus
2026-06-14T09:13:40.753Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-14T09:13:40.778Z ::ffff:127.0.0.1: M114
2026-06-14T09:13:40.801Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T09:13:40.961Z ::ffff:127.0.0.1: M114
2026-06-14T09:13:41.187Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T09:13:41.421Z ::ffff:127.0.0.1: G1 X1
2026-06-14T09:13:46.992Z ::ffff:127.0.0.1: M114
2026-06-14T09:13:47.119Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T09:13:47.243Z ::ffff:127.0.0.1: FList
2026-06-14T09:13:47.403Z ::ffff:127.0.0.1: FPlus
2026-06-14T09:13:47.446Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-14T09:13:47.465Z ::ffff:127.0.0.1: M114
2026-06-14T09:13:47.721Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T09:13:47.970Z ::ffff:127.0.0.1: G1 X1
2026-06-14T09:13:50.452Z ::ffff:127.0.0.1: FList
2026-06-14T09:13:50.473Z ::ffff:127.0.0.1: M114
2026-06-14T09:13:50.492Z ::ffff:127.0.0.1: FPlus
2026-06-14T09:13:50.495Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T09:13:50.505Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-14T09:13:50.695Z ::ffff:127.0.0.1: M114
2026-06-14T09:13:50.929Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T09:13:51.152Z ::ffff:127.0.0.1: G1 X1
2026-06-14T09:42:55.235Z ::ffff:127.0.0.1: M114
2026-06-14T09:42:55.253Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T09:42:55.275Z ::ffff:127.0.0.1: FList
2026-06-14T09:42:55.310Z ::ffff:127.0.0.1: FPlus
2026-06-14T09:42:55.337Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-14T09:42:55.774Z ::ffff:127.0.0.1: M114
2026-06-14T09:42:56.002Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T09:42:56.225Z ::ffff:127.0.0.1: G1 X1
2026-06-14T09:43:02.800Z ::ffff:127.0.0.1: FList
2026-06-14T09:43:02.842Z ::ffff:127.0.0.1: M114
2026-06-14T09:43:02.857Z ::ffff:127.0.0.1: FPlus
2026-06-14T09:43:02.875Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-14T09:43:02.892Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T09:43:03.165Z ::ffff:127.0.0.1: M114
2026-06-14T09:43:03.433Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T09:43:03.682Z ::ffff:127.0.0.1: G1 X1
2026-06-14T09:43:06.562Z ::ffff:127.0.0.1: FList
2026-06-14T09:43:06.602Z ::ffff:127.0.0.1: FPlus
2026-06-14T09:43:06.615Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-14T09:43:06.746Z ::ffff:127.0.0.1: M114
2026-06-14T09:43:06.974Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T09:43:07.066Z ::ffff:127.0.0.1: M114
2026-06-14T09:43:07.080Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T09:43:07.211Z ::ffff:127.0.0.1: G1 X1
2026-06-14T11:16:51.101Z ::ffff:127.0.0.1: FList
2026-06-14T11:16:51.136Z ::ffff:127.0.0.1: FPlus
2026-06-14T11:16:51.150Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-14T11:16:51.370Z ::ffff:127.0.0.1: M114
2026-06-14T11:16:51.381Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T11:16:51.580Z ::ffff:127.0.0.1: M114
2026-06-14T11:16:51.803Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T11:16:52.031Z ::ffff:127.0.0.1: G1 X1
2026-06-14T11:17:04.735Z ::ffff:127.0.0.1: FList
2026-06-14T11:17:04.743Z ::ffff:127.0.0.1: M114
2026-06-14T11:17:04.750Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T11:17:04.755Z ::ffff:127.0.0.1: FPlus
2026-06-14T11:17:04.766Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-14T11:17:04.961Z ::ffff:127.0.0.1: M114
2026-06-14T11:17:05.183Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T11:17:05.412Z ::ffff:127.0.0.1: G1 X1
2026-06-14T11:36:44.546Z ::ffff:127.0.0.1: FList
2026-06-14T11:36:44.568Z ::ffff:127.0.0.1: FPlus
2026-06-14T11:36:44.584Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-14T11:36:44.658Z ::ffff:127.0.0.1: M114
2026-06-14T11:36:44.670Z ::ffff:127.0.0.1: M114
2026-06-14T11:36:44.672Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T11:36:44.894Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T11:36:45.172Z ::ffff:127.0.0.1: G1 X1
2026-06-14T11:36:46.643Z ::ffff:127.0.0.1: FList
2026-06-14T11:36:46.672Z ::ffff:127.0.0.1: FPlus
2026-06-14T11:36:46.680Z ::ffff:127.0.0.1: M114
2026-06-14T11:36:46.682Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-14T11:36:46.687Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T11:36:46.881Z ::ffff:127.0.0.1: M114
2026-06-14T11:36:47.097Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T11:36:47.331Z ::ffff:127.0.0.1: G1 X1
2026-06-14T11:36:55.467Z ::ffff:127.0.0.1: FList
2026-06-14T11:36:55.476Z ::ffff:127.0.0.1: FPlus
2026-06-14T11:36:55.483Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-14T11:37:15.676Z ::ffff:127.0.0.1: M114
2026-06-14T11:37:15.689Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T11:37:15.844Z ::ffff:127.0.0.1: FList
2026-06-14T11:37:15.864Z ::ffff:127.0.0.1: FPlus
2026-06-14T11:37:15.871Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-14T11:37:15.878Z ::ffff:127.0.0.1: FShow
2026-06-14T11:37:15.896Z ::ffff:127.0.0.1: M114
2026-06-14T11:37:16.117Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T11:37:16.348Z ::ffff:127.0.0.1: G1 X1
2026-06-14T11:37:28.740Z ::ffff:127.0.0.1: FList
2026-06-14T11:37:28.764Z ::ffff:127.0.0.1: FPlus
2026-06-14T11:37:28.776Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-14T11:37:28.778Z ::ffff:127.0.0.1: M114
2026-06-14T11:37:28.786Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T11:37:28.788Z ::ffff:127.0.0.1: FShow
2026-06-14T11:37:29.015Z ::ffff:127.0.0.1: M114
2026-06-14T11:37:29.234Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T11:37:29.461Z ::ffff:127.0.0.1: G1 X1
2026-06-14T11:37:38.711Z ::ffff:127.0.0.1: FList
2026-06-14T11:37:38.725Z ::ffff:127.0.0.1: FPlus
2026-06-14T11:37:38.735Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-14T11:37:38.742Z ::ffff:127.0.0.1: FShow
2026-06-14T11:37:48.038Z ::ffff:127.0.0.1: M114
2026-06-14T11:37:48.045Z ::ffff:127.0.0.1: FList
2026-06-14T11:37:48.097Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T11:37:48.111Z ::ffff:127.0.0.1: FPlus
2026-06-14T11:37:48.151Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-14T11:37:48.196Z ::ffff:127.0.0.1: FShow
2026-06-14T11:37:48.807Z ::ffff:127.0.0.1: M114
2026-06-14T11:37:49.023Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T11:37:49.265Z ::ffff:127.0.0.1: G1 X1
2026-06-14T11:37:51.799Z ::ffff:127.0.0.1: FList
2026-06-14T11:37:51.812Z ::ffff:127.0.0.1: M114
2026-06-14T11:37:51.828Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-14T11:37:51.836Z ::ffff:127.0.0.1: FPlus
2026-06-14T11:37:51.855Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-14T11:37:51.866Z ::ffff:127.0.0.1: FShow
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
2026-06-26T03:31:10.436Z ::ffff:127.0.0.1: FList
2026-06-26T03:31:10.459Z ::ffff:127.0.0.1: FPlus
2026-06-26T03:31:10.466Z ::ffff:127.0.0.1: M114
2026-06-26T03:31:10.484Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T03:31:10.501Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T03:31:10.502Z ::ffff:127.0.0.1: FShow
2026-06-26T03:31:10.665Z ::ffff:127.0.0.1: M114
2026-06-26T03:31:10.885Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T03:31:11.101Z ::ffff:127.0.0.1: G1 X1
2026-06-26T04:00:22.876Z ::ffff:127.0.0.1: FList
2026-06-26T04:00:22.897Z ::ffff:127.0.0.1: FPlus
2026-06-26T04:00:22.931Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T04:00:22.969Z ::ffff:127.0.0.1: FShow
2026-06-26T04:00:23.464Z ::ffff:127.0.0.1: M114
2026-06-26T04:00:23.498Z ::ffff:127.0.0.1: M114
2026-06-26T04:00:23.511Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T04:00:23.743Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T04:00:24.008Z ::ffff:127.0.0.1: G1 X1
2026-06-26T04:36:17.245Z ::ffff:127.0.0.1: M114
2026-06-26T04:36:17.277Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T04:36:17.299Z ::ffff:127.0.0.1: FList
2026-06-26T04:36:17.330Z ::ffff:127.0.0.1: FPlus
2026-06-26T04:36:17.366Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T04:36:17.400Z ::ffff:127.0.0.1: FShow
2026-06-26T04:36:17.472Z ::ffff:127.0.0.1: M114
2026-06-26T04:36:17.716Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T04:36:17.971Z ::ffff:127.0.0.1: G1 X1
2026-06-26T06:18:31.826Z ::ffff:127.0.0.1: M114
2026-06-26T06:18:31.879Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T06:18:31.899Z ::ffff:127.0.0.1: FList
2026-06-26T06:18:31.947Z ::ffff:127.0.0.1: FPlus
2026-06-26T06:18:31.976Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T06:18:31.997Z ::ffff:127.0.0.1: FShow
2026-06-26T06:18:32.115Z ::ffff:127.0.0.1: M114
2026-06-26T06:18:32.356Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T06:18:32.614Z ::ffff:127.0.0.1: G1 X1
2026-06-26T06:20:45.304Z ::ffff:127.0.0.1: M114
2026-06-26T06:20:45.334Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T06:20:45.336Z ::ffff:127.0.0.1: FList
2026-06-26T06:20:45.381Z ::ffff:127.0.0.1: FPlus
2026-06-26T06:20:45.398Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T06:20:45.411Z ::ffff:127.0.0.1: FShow
2026-06-26T06:20:45.539Z ::ffff:127.0.0.1: M114
2026-06-26T06:20:45.769Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T06:20:46.021Z ::ffff:127.0.0.1: G1 X1
2026-06-26T06:21:16.481Z ::ffff:127.0.0.1: FList
2026-06-26T06:21:16.529Z ::ffff:127.0.0.1: FPlus
2026-06-26T06:21:16.537Z ::ffff:127.0.0.1: M114
2026-06-26T06:21:16.548Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T06:21:16.560Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T06:21:16.567Z ::ffff:127.0.0.1: FShow
2026-06-26T06:21:16.765Z ::ffff:127.0.0.1: M114
2026-06-26T06:21:17.013Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T06:21:17.267Z ::ffff:127.0.0.1: G1 X1
2026-06-26T06:21:22.686Z ::ffff:127.0.0.1: FList
2026-06-26T06:21:22.742Z ::ffff:127.0.0.1: FPlus
2026-06-26T06:21:22.759Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T06:21:22.782Z ::ffff:127.0.0.1: FShow
2026-06-26T06:21:22.918Z ::ffff:127.0.0.1: M114
2026-06-26T06:21:23.161Z ::ffff:127.0.0.1: M114
2026-06-26T06:21:23.179Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T06:21:23.201Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T06:21:23.437Z ::ffff:127.0.0.1: G1 X1
2026-06-26T06:21:27.584Z ::ffff:127.0.0.1: M114
2026-06-26T06:21:27.607Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T06:21:27.776Z ::ffff:127.0.0.1: FList
2026-06-26T06:21:27.827Z ::ffff:127.0.0.1: FPlus
2026-06-26T06:21:27.846Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T06:21:27.859Z ::ffff:127.0.0.1: FShow
2026-06-26T06:21:27.938Z ::ffff:127.0.0.1: M114
2026-06-26T06:21:28.209Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T06:21:28.470Z ::ffff:127.0.0.1: G1 X1
2026-06-26T06:21:32.539Z ::ffff:127.0.0.1: FList
2026-06-26T06:21:32.593Z ::ffff:127.0.0.1: FPlus
2026-06-26T06:21:32.617Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T06:21:32.636Z ::ffff:127.0.0.1: FShow
2026-06-26T06:21:32.677Z ::ffff:127.0.0.1: M114
2026-06-26T06:21:32.696Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T06:21:32.907Z ::ffff:127.0.0.1: M114
2026-06-26T06:21:33.145Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T06:21:33.394Z ::ffff:127.0.0.1: G1 X1
2026-06-26T06:21:49.876Z ::ffff:127.0.0.1: FList
2026-06-26T06:21:49.917Z ::ffff:127.0.0.1: FPlus
2026-06-26T06:21:49.927Z ::ffff:127.0.0.1: M114
2026-06-26T06:21:49.936Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T06:21:49.943Z ::ffff:127.0.0.1: M114
2026-06-26T06:21:49.954Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T06:21:49.962Z ::ffff:127.0.0.1: FShow
2026-06-26T06:21:50.224Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T06:21:50.481Z ::ffff:127.0.0.1: G1 X1
2026-06-26T06:21:53.338Z ::ffff:127.0.0.1: FList
2026-06-26T06:21:53.386Z ::ffff:127.0.0.1: FPlus
2026-06-26T06:21:53.406Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T06:21:53.422Z ::ffff:127.0.0.1: FShow
2026-06-26T06:21:53.525Z ::ffff:127.0.0.1: M114
2026-06-26T06:21:53.556Z ::ffff:127.0.0.1: M114
2026-06-26T06:21:53.580Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T06:21:53.772Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T06:21:54.016Z ::ffff:127.0.0.1: G1 X1
2026-06-26T06:23:06.580Z ::ffff:127.0.0.1: FList
2026-06-26T06:23:06.624Z ::ffff:127.0.0.1: FPlus
2026-06-26T06:23:06.647Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T06:23:06.666Z ::ffff:127.0.0.1: FShow
2026-06-26T06:23:06.693Z ::ffff:127.0.0.1: M114
2026-06-26T06:23:06.714Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T06:23:06.966Z ::ffff:127.0.0.1: M114
2026-06-26T06:23:07.218Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T06:23:07.463Z ::ffff:127.0.0.1: G1 X1
2026-06-26T06:25:53.094Z ::ffff:127.0.0.1: FList
2026-06-26T06:25:53.137Z ::ffff:127.0.0.1: M114
2026-06-26T06:25:53.152Z ::ffff:127.0.0.1: FPlus
2026-06-26T06:25:53.163Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T06:25:53.174Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T06:25:53.196Z ::ffff:127.0.0.1: FShow
2026-06-26T06:25:53.335Z ::ffff:127.0.0.1: M114
2026-06-26T06:25:53.565Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T06:25:53.811Z ::ffff:127.0.0.1: G1 X1
2026-06-26T08:24:39.807Z ::ffff:127.0.0.1: M114
2026-06-26T08:24:39.825Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T08:24:39.902Z ::ffff:127.0.0.1: FList
2026-06-26T08:24:39.925Z ::ffff:127.0.0.1: FPlus
2026-06-26T08:24:39.944Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T08:24:39.958Z ::ffff:127.0.0.1: FShow
2026-06-26T08:24:40.035Z ::ffff:127.0.0.1: M114
2026-06-26T08:24:40.264Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T08:24:40.497Z ::ffff:127.0.0.1: G1 X1
2026-06-26T09:26:48.688Z ::ffff:127.0.0.1: FList
2026-06-26T09:26:48.711Z ::ffff:127.0.0.1: M114
2026-06-26T09:26:48.729Z ::ffff:127.0.0.1: FPlus
2026-06-26T09:26:48.732Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T09:26:48.744Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T09:26:48.758Z ::ffff:127.0.0.1: FShow
2026-06-26T09:26:48.922Z ::ffff:127.0.0.1: M114
2026-06-26T09:26:49.142Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T09:26:49.372Z ::ffff:127.0.0.1: G1 X1
2026-06-26T09:27:11.069Z ::ffff:127.0.0.1: FList
2026-06-26T09:27:11.100Z ::ffff:127.0.0.1: M114
2026-06-26T09:27:11.120Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T09:27:11.122Z ::ffff:127.0.0.1: FPlus
2026-06-26T09:27:11.140Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T09:27:11.157Z ::ffff:127.0.0.1: FShow
2026-06-26T09:27:11.295Z ::ffff:127.0.0.1: M114
2026-06-26T09:27:11.517Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T09:27:11.744Z ::ffff:127.0.0.1: G1 X1
2026-06-26T09:27:19.593Z ::ffff:127.0.0.1: FList
2026-06-26T09:27:19.607Z ::ffff:127.0.0.1: M114
2026-06-26T09:27:19.624Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T09:27:19.627Z ::ffff:127.0.0.1: FPlus
2026-06-26T09:27:19.639Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T09:27:19.650Z ::ffff:127.0.0.1: FShow
2026-06-26T09:27:19.814Z ::ffff:127.0.0.1: M114
2026-06-26T09:27:20.029Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T09:27:20.258Z ::ffff:127.0.0.1: G1 X1
2026-06-26T09:27:36.334Z ::ffff:127.0.0.1: FList
2026-06-26T09:27:36.366Z ::ffff:127.0.0.1: M114
2026-06-26T09:27:36.367Z ::ffff:127.0.0.1: FPlus
2026-06-26T09:27:36.382Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T09:27:36.387Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T09:27:36.401Z ::ffff:127.0.0.1: FShow
2026-06-26T09:27:36.558Z ::ffff:127.0.0.1: M114
2026-06-26T09:27:36.772Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T09:27:37.001Z ::ffff:127.0.0.1: G1 X1
2026-06-26T09:28:07.329Z ::ffff:127.0.0.1: M114
2026-06-26T09:28:07.361Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T09:28:07.445Z ::ffff:127.0.0.1: M114
2026-06-26T09:28:07.499Z ::ffff:127.0.0.1: FList
2026-06-26T09:28:07.549Z ::ffff:127.0.0.1: FPlus
2026-06-26T09:28:07.574Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T09:28:07.593Z ::ffff:127.0.0.1: FShow
2026-06-26T09:28:07.692Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T09:28:07.941Z ::ffff:127.0.0.1: G1 X1
2026-06-26T09:28:10.419Z ::ffff:127.0.0.1: M114
2026-06-26T09:28:10.422Z ::ffff:127.0.0.1: FList
2026-06-26T09:28:10.444Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T09:28:10.464Z ::ffff:127.0.0.1: FPlus
2026-06-26T09:28:10.477Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T09:28:10.485Z ::ffff:127.0.0.1: FShow
2026-06-26T09:28:10.667Z ::ffff:127.0.0.1: M114
2026-06-26T09:28:10.883Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T09:28:11.112Z ::ffff:127.0.0.1: G1 X1
2026-06-26T09:35:29.613Z ::ffff:127.0.0.1: M114
2026-06-26T09:35:29.633Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T09:35:29.819Z ::ffff:127.0.0.1: M114
2026-06-26T09:35:29.908Z ::ffff:127.0.0.1: FList
2026-06-26T09:35:29.947Z ::ffff:127.0.0.1: FPlus
2026-06-26T09:35:29.964Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T09:35:29.991Z ::ffff:127.0.0.1: FShow
2026-06-26T09:35:30.059Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T09:35:30.309Z ::ffff:127.0.0.1: G1 X1
2026-06-26T09:35:33.075Z ::ffff:127.0.0.1: FList
2026-06-26T09:35:33.109Z ::ffff:127.0.0.1: FPlus
2026-06-26T09:35:33.125Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T09:35:33.138Z ::ffff:127.0.0.1: FShow
2026-06-26T09:35:33.254Z ::ffff:127.0.0.1: M114
2026-06-26T09:35:33.271Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T09:35:33.429Z ::ffff:127.0.0.1: M114
2026-06-26T09:35:33.652Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T09:35:33.884Z ::ffff:127.0.0.1: G1 X1
2026-06-26T10:18:30.756Z ::ffff:127.0.0.1: FList
2026-06-26T10:18:30.784Z ::ffff:127.0.0.1: FPlus
2026-06-26T10:18:30.808Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T10:18:30.826Z ::ffff:127.0.0.1: FShow
2026-06-26T10:18:31.251Z ::ffff:127.0.0.1: M114
2026-06-26T10:18:31.274Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T10:18:31.425Z ::ffff:127.0.0.1: M114
2026-06-26T10:18:31.639Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T10:18:31.880Z ::ffff:127.0.0.1: G1 X1
2026-06-26T10:26:37.183Z ::ffff:127.0.0.1: FList
2026-06-26T10:26:37.230Z ::ffff:127.0.0.1: FPlus
2026-06-26T10:26:37.250Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T10:26:37.265Z ::ffff:127.0.0.1: FShow
2026-06-26T10:26:37.361Z ::ffff:127.0.0.1: M114
2026-06-26T10:26:37.382Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T10:26:37.461Z ::ffff:127.0.0.1: M114
2026-06-26T10:26:37.683Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T10:26:37.905Z ::ffff:127.0.0.1: G1 X1
2026-06-26T10:26:51.872Z ::ffff:127.0.0.1: FList
2026-06-26T10:26:51.904Z ::ffff:127.0.0.1: M114
2026-06-26T10:26:51.913Z ::ffff:127.0.0.1: FPlus
2026-06-26T10:26:51.923Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T10:26:51.928Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T10:26:51.947Z ::ffff:127.0.0.1: FShow
2026-06-26T10:26:52.112Z ::ffff:127.0.0.1: M114
2026-06-26T10:26:52.341Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T10:26:52.567Z ::ffff:127.0.0.1: G1 X1
2026-06-26T13:23:39.999Z ::ffff:127.0.0.1: FList
2026-06-26T13:23:40.023Z ::ffff:127.0.0.1: FPlus
2026-06-26T13:23:40.048Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T13:23:40.086Z ::ffff:127.0.0.1: FShow
2026-06-26T13:23:40.410Z ::ffff:127.0.0.1: M114
2026-06-26T13:23:40.444Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:23:40.597Z ::ffff:127.0.0.1: M114
2026-06-26T13:23:40.821Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:23:41.051Z ::ffff:127.0.0.1: G1 X1
2026-06-26T13:42:17.961Z ::ffff:127.0.0.1: FList
2026-06-26T13:42:18.005Z ::ffff:127.0.0.1: M114
2026-06-26T13:42:18.006Z ::ffff:127.0.0.1: FPlus
2026-06-26T13:42:18.028Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T13:42:18.041Z ::ffff:127.0.0.1: FShow
2026-06-26T13:42:18.231Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:42:18.289Z ::ffff:127.0.0.1: M114
2026-06-26T13:42:18.311Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:42:18.481Z ::ffff:127.0.0.1: G1 X1
2026-06-26T13:42:25.801Z ::ffff:127.0.0.1: FList
2026-06-26T13:42:25.848Z ::ffff:127.0.0.1: FPlus
2026-06-26T13:42:25.869Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T13:42:25.874Z ::ffff:127.0.0.1: M114
2026-06-26T13:42:25.890Z ::ffff:127.0.0.1: FShow
2026-06-26T13:42:25.894Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:42:26.204Z ::ffff:127.0.0.1: M114
2026-06-26T13:42:26.434Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:42:26.657Z ::ffff:127.0.0.1: G1 X1
2026-06-26T13:42:30.445Z ::ffff:127.0.0.1: FList
2026-06-26T13:42:30.483Z ::ffff:127.0.0.1: FPlus
2026-06-26T13:42:30.501Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T13:42:30.510Z ::ffff:127.0.0.1: M114
2026-06-26T13:42:30.518Z ::ffff:127.0.0.1: FShow
2026-06-26T13:42:30.693Z ::ffff:127.0.0.1: M114
2026-06-26T13:42:30.710Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:42:30.744Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:42:30.982Z ::ffff:127.0.0.1: G1 X1
2026-06-26T13:42:37.605Z ::ffff:127.0.0.1: M114
2026-06-26T13:42:37.612Z ::ffff:127.0.0.1: FList
2026-06-26T13:42:37.650Z ::ffff:127.0.0.1: FPlus
2026-06-26T13:42:37.670Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T13:42:37.684Z ::ffff:127.0.0.1: FShow
2026-06-26T13:42:37.835Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:42:37.877Z ::ffff:127.0.0.1: M114
2026-06-26T13:42:37.901Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:42:38.082Z ::ffff:127.0.0.1: G1 X1
2026-06-26T13:42:41.837Z ::ffff:127.0.0.1: FList
2026-06-26T13:42:41.889Z ::ffff:127.0.0.1: FPlus
2026-06-26T13:42:41.908Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T13:42:41.925Z ::ffff:127.0.0.1: FShow
2026-06-26T13:42:42.165Z ::ffff:127.0.0.1: M114
2026-06-26T13:42:42.381Z ::ffff:127.0.0.1: M114
2026-06-26T13:42:42.409Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:42:42.415Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:42:42.667Z ::ffff:127.0.0.1: G1 X1
2026-06-26T13:42:46.282Z ::ffff:127.0.0.1: FList
2026-06-26T13:42:46.322Z ::ffff:127.0.0.1: FPlus
2026-06-26T13:42:46.340Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T13:42:46.360Z ::ffff:127.0.0.1: FShow
2026-06-26T13:42:46.525Z ::ffff:127.0.0.1: M114
2026-06-26T13:42:46.762Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:42:46.764Z ::ffff:127.0.0.1: M114
2026-06-26T13:42:46.784Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:42:47.017Z ::ffff:127.0.0.1: G1 X1
2026-06-26T13:42:57.899Z ::ffff:127.0.0.1: FList
2026-06-26T13:42:57.940Z ::ffff:127.0.0.1: FPlus
2026-06-26T13:42:57.956Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T13:42:57.972Z ::ffff:127.0.0.1: FShow
2026-06-26T13:42:58.156Z ::ffff:127.0.0.1: M114
2026-06-26T13:42:58.385Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:42:58.387Z ::ffff:127.0.0.1: M114
2026-06-26T13:42:58.406Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:42:58.622Z ::ffff:127.0.0.1: G1 X1
2026-06-26T13:43:07.993Z ::ffff:127.0.0.1: M114
2026-06-26T13:43:08.022Z ::ffff:127.0.0.1: M114
2026-06-26T13:43:08.053Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:43:08.109Z ::ffff:127.0.0.1: FList
2026-06-26T13:43:08.148Z ::ffff:127.0.0.1: FPlus
2026-06-26T13:43:08.170Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T13:43:08.191Z ::ffff:127.0.0.1: FShow
2026-06-26T13:43:08.239Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:43:08.501Z ::ffff:127.0.0.1: G1 X1
2026-06-26T13:43:11.205Z ::ffff:127.0.0.1: FList
2026-06-26T13:43:11.259Z ::ffff:127.0.0.1: FPlus
2026-06-26T13:43:11.284Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T13:43:11.302Z ::ffff:127.0.0.1: FShow
2026-06-26T13:43:11.487Z ::ffff:127.0.0.1: M114
2026-06-26T13:43:11.720Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:43:11.737Z ::ffff:127.0.0.1: M114
2026-06-26T13:43:11.770Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:43:11.965Z ::ffff:127.0.0.1: G1 X1
2026-06-26T13:45:00.280Z ::ffff:127.0.0.1: FList
2026-06-26T13:45:00.322Z ::ffff:127.0.0.1: FPlus
2026-06-26T13:45:00.341Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T13:45:00.363Z ::ffff:127.0.0.1: FShow
2026-06-26T13:45:00.530Z ::ffff:127.0.0.1: M114
2026-06-26T13:45:00.731Z ::ffff:127.0.0.1: M114
2026-06-26T13:45:00.756Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:45:00.770Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:45:01.044Z ::ffff:127.0.0.1: G1 X1
2026-06-26T13:45:09.252Z ::ffff:127.0.0.1: FList
2026-06-26T13:45:09.286Z ::ffff:127.0.0.1: FPlus
2026-06-26T13:45:09.310Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T13:45:09.333Z ::ffff:127.0.0.1: FShow
2026-06-26T13:45:09.534Z ::ffff:127.0.0.1: M114
2026-06-26T13:45:09.782Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:45:10.041Z ::ffff:127.0.0.1: G1 X1
2026-06-26T13:45:10.123Z ::ffff:127.0.0.1: M114
2026-06-26T13:45:10.156Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:45:13.181Z ::ffff:127.0.0.1: FList
2026-06-26T13:45:13.236Z ::ffff:127.0.0.1: FPlus
2026-06-26T13:45:13.258Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T13:45:13.289Z ::ffff:127.0.0.1: FShow
2026-06-26T13:45:13.466Z ::ffff:127.0.0.1: M114
2026-06-26T13:45:13.693Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:45:13.756Z ::ffff:127.0.0.1: M114
2026-06-26T13:45:13.779Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:45:13.928Z ::ffff:127.0.0.1: G1 X1
2026-06-26T13:45:26.981Z ::ffff:127.0.0.1: FList
2026-06-26T13:45:27.042Z ::ffff:127.0.0.1: FPlus
2026-06-26T13:45:27.081Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T13:45:27.100Z ::ffff:127.0.0.1: FShow
2026-06-26T13:45:27.204Z ::ffff:127.0.0.1: M114
2026-06-26T13:45:27.249Z ::ffff:127.0.0.1: M114
2026-06-26T13:45:27.283Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:45:27.448Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:45:27.704Z ::ffff:127.0.0.1: G1 X1
2026-06-26T13:45:36.655Z ::ffff:127.0.0.1: FList
2026-06-26T13:45:36.689Z ::ffff:127.0.0.1: FPlus
2026-06-26T13:45:36.712Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T13:45:36.739Z ::ffff:127.0.0.1: FShow
2026-06-26T13:45:37.079Z ::ffff:127.0.0.1: M114
2026-06-26T13:45:37.323Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:45:37.586Z ::ffff:127.0.0.1: G1 X1
2026-06-26T13:45:37.834Z ::ffff:127.0.0.1: M114
2026-06-26T13:45:37.856Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:45:40.870Z ::ffff:127.0.0.1: FList
2026-06-26T13:45:40.915Z ::ffff:127.0.0.1: FPlus
2026-06-26T13:45:40.936Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T13:45:40.958Z ::ffff:127.0.0.1: FShow
2026-06-26T13:45:41.132Z ::ffff:127.0.0.1: M114
2026-06-26T13:45:41.372Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:45:41.439Z ::ffff:127.0.0.1: M114
2026-06-26T13:45:41.471Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:45:41.629Z ::ffff:127.0.0.1: G1 X1
2026-06-26T13:46:04.163Z ::ffff:127.0.0.1: FList
2026-06-26T13:46:04.211Z ::ffff:127.0.0.1: FPlus
2026-06-26T13:46:04.229Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T13:46:04.250Z ::ffff:127.0.0.1: FShow
2026-06-26T13:46:04.434Z ::ffff:127.0.0.1: M114
2026-06-26T13:46:04.666Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:46:04.912Z ::ffff:127.0.0.1: G1 X1
2026-06-26T13:46:05.010Z ::ffff:127.0.0.1: M114
2026-06-26T13:46:05.038Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:46:18.941Z ::ffff:127.0.0.1: FList
2026-06-26T13:46:18.971Z ::ffff:127.0.0.1: FPlus
2026-06-26T13:46:18.997Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T13:46:19.022Z ::ffff:127.0.0.1: FShow
2026-06-26T13:46:19.145Z ::ffff:127.0.0.1: M114
2026-06-26T13:46:19.162Z ::ffff:127.0.0.1: M114
2026-06-26T13:46:19.187Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:46:19.381Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:46:19.614Z ::ffff:127.0.0.1: G1 X1
2026-06-26T13:46:22.583Z ::ffff:127.0.0.1: M114
2026-06-26T13:46:22.593Z ::ffff:127.0.0.1: FList
2026-06-26T13:46:22.637Z ::ffff:127.0.0.1: FPlus
2026-06-26T13:46:22.662Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T13:46:22.684Z ::ffff:127.0.0.1: FShow
2026-06-26T13:46:22.818Z ::ffff:127.0.0.1: M114
2026-06-26T13:46:22.820Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:46:22.849Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:46:23.074Z ::ffff:127.0.0.1: G1 X1
2026-06-26T13:47:43.276Z ::ffff:127.0.0.1: FList
2026-06-26T13:47:43.313Z ::ffff:127.0.0.1: FPlus
2026-06-26T13:47:43.334Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T13:47:43.359Z ::ffff:127.0.0.1: FShow
2026-06-26T13:47:43.467Z ::ffff:127.0.0.1: M114
2026-06-26T13:47:43.642Z ::ffff:127.0.0.1: M114
2026-06-26T13:47:43.660Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:47:43.696Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:47:43.928Z ::ffff:127.0.0.1: G1 X1
2026-06-26T13:47:47.226Z ::ffff:127.0.0.1: FList
2026-06-26T13:47:47.265Z ::ffff:127.0.0.1: FPlus
2026-06-26T13:47:47.281Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T13:47:47.299Z ::ffff:127.0.0.1: FShow
2026-06-26T13:47:47.480Z ::ffff:127.0.0.1: M114
2026-06-26T13:47:47.695Z ::ffff:127.0.0.1: M114
2026-06-26T13:47:47.704Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:47:47.716Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T13:47:47.949Z ::ffff:127.0.0.1: G1 X1
2026-06-26T14:26:30.318Z ::ffff:127.0.0.1: FList
2026-06-26T14:26:30.386Z ::ffff:127.0.0.1: FPlus
2026-06-26T14:26:30.414Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T14:26:30.444Z ::ffff:127.0.0.1: FShow
2026-06-26T14:26:30.564Z ::ffff:127.0.0.1: M114
2026-06-26T14:26:30.808Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T14:26:31.049Z ::ffff:127.0.0.1: M114
2026-06-26T14:26:31.078Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T14:26:31.095Z ::ffff:127.0.0.1: G1 X1
2026-06-26T14:27:28.871Z ::ffff:127.0.0.1: FList
2026-06-26T14:27:28.923Z ::ffff:127.0.0.1: FPlus
2026-06-26T14:27:28.953Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T14:27:28.964Z ::ffff:127.0.0.1: M114
2026-06-26T14:27:28.978Z ::ffff:127.0.0.1: FShow
2026-06-26T14:27:29.004Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T14:27:29.453Z ::ffff:127.0.0.1: M114
2026-06-26T14:27:29.717Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T14:27:29.979Z ::ffff:127.0.0.1: G1 X1
2026-06-26T14:27:33.289Z ::ffff:127.0.0.1: FList
2026-06-26T14:27:33.371Z ::ffff:127.0.0.1: FPlus
2026-06-26T14:27:33.399Z ::ffff:127.0.0.1: FLoad nichtda
2026-06-26T14:27:33.404Z ::ffff:127.0.0.1: M114
2026-06-26T14:27:33.423Z ::ffff:127.0.0.1: FShow
2026-06-26T14:27:33.646Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-26T14:27:33.903Z ::ffff:127.0.0.1: G1 X1
2026-06-26T14:27:33.905Z ::ffff:127.0.0.1: M114
2026-06-26T14:27:33.931Z ::ffff:127.0.0.1: G1 X1 Y2 Z3

View File

@@ -14700,3 +14700,209 @@
2026-06-14T07:30:37.987Z ::ffff:127.0.0.1 : Ping
2026-06-14T07:32:17.191Z ::ffff:127.0.0.1 : Ping
2026-06-14T07:32:17.745Z ::ffff:127.0.0.1 : Ping
2026-06-14T08:39:38.144Z ::ffff:127.0.0.1 : Ping
2026-06-14T08:39:38.409Z ::ffff:127.0.0.1 : Ping
2026-06-14T08:40:02.952Z ::ffff:127.0.0.1 : Ping
2026-06-14T08:40:02.967Z ::ffff:127.0.0.1 : Ping
2026-06-14T08:40:22.054Z ::ffff:127.0.0.1 : Ping
2026-06-14T08:40:22.062Z ::ffff:127.0.0.1 : Ping
2026-06-14T08:40:41.952Z ::ffff:127.0.0.1 : Ping
2026-06-14T08:40:41.992Z ::ffff:127.0.0.1 : Ping
2026-06-14T08:40:49.183Z ::ffff:127.0.0.1 : Ping
2026-06-14T08:40:49.206Z ::ffff:127.0.0.1 : Ping
2026-06-14T08:41:14.145Z ::ffff:127.0.0.1 : Ping
2026-06-14T08:41:14.180Z ::ffff:127.0.0.1 : Ping
2026-06-14T08:41:30.044Z ::ffff:127.0.0.1 : Ping
2026-06-14T08:41:30.066Z ::ffff:127.0.0.1 : Ping
2026-06-14T08:41:52.557Z ::ffff:127.0.0.1 : Ping
2026-06-14T08:41:52.620Z ::ffff:127.0.0.1 : Ping
2026-06-14T08:41:56.285Z ::ffff:127.0.0.1 : Ping
2026-06-14T08:41:56.375Z ::ffff:127.0.0.1 : Ping
2026-06-14T08:42:30.799Z ::ffff:127.0.0.1 : Ping
2026-06-14T08:42:30.808Z ::ffff:127.0.0.1 : Ping
2026-06-14T08:42:37.230Z ::ffff:127.0.0.1 : Ping
2026-06-14T08:42:37.957Z ::ffff:127.0.0.1 : Ping
2026-06-14T08:42:40.846Z ::ffff:127.0.0.1 : Ping
2026-06-14T08:42:40.854Z ::ffff:127.0.0.1 : Ping
2026-06-14T09:13:04.169Z ::ffff:127.0.0.1 : Ping
2026-06-14T09:13:04.342Z ::ffff:127.0.0.1 : Ping
2026-06-14T09:13:06.836Z ::ffff:127.0.0.1 : Ping
2026-06-14T09:13:06.938Z ::ffff:127.0.0.1 : Ping
2026-06-14T09:13:27.764Z ::ffff:127.0.0.1 : Ping
2026-06-14T09:13:28.432Z ::ffff:127.0.0.1 : Ping
2026-06-14T09:13:31.351Z ::ffff:127.0.0.1 : Ping
2026-06-14T09:13:31.786Z ::ffff:127.0.0.1 : Ping
2026-06-14T09:13:37.220Z ::ffff:127.0.0.1 : Ping
2026-06-14T09:13:37.595Z ::ffff:127.0.0.1 : Ping
2026-06-14T09:13:40.726Z ::ffff:127.0.0.1 : Ping
2026-06-14T09:13:40.742Z ::ffff:127.0.0.1 : Ping
2026-06-14T09:13:46.951Z ::ffff:127.0.0.1 : Ping
2026-06-14T09:13:47.180Z ::ffff:127.0.0.1 : Ping
2026-06-14T09:13:50.433Z ::ffff:127.0.0.1 : Ping
2026-06-14T09:13:50.459Z ::ffff:127.0.0.1 : Ping
2026-06-14T09:42:55.181Z ::ffff:127.0.0.1 : Ping
2026-06-14T09:42:55.544Z ::ffff:127.0.0.1 : Ping
2026-06-14T09:43:02.806Z ::ffff:127.0.0.1 : Ping
2026-06-14T09:43:02.856Z ::ffff:127.0.0.1 : Ping
2026-06-14T09:43:06.488Z ::ffff:127.0.0.1 : Ping
2026-06-14T09:43:07.035Z ::ffff:127.0.0.1 : Ping
2026-06-14T11:16:51.344Z ::ffff:127.0.0.1 : Ping
2026-06-14T11:16:51.346Z ::ffff:127.0.0.1 : Ping
2026-06-14T11:17:04.719Z ::ffff:127.0.0.1 : Ping
2026-06-14T11:17:04.733Z ::ffff:127.0.0.1 : Ping
2026-06-14T11:36:44.450Z ::ffff:127.0.0.1 : Ping
2026-06-14T11:36:44.640Z ::ffff:127.0.0.1 : Ping
2026-06-14T11:36:46.636Z ::ffff:127.0.0.1 : Ping
2026-06-14T11:36:46.659Z ::ffff:127.0.0.1 : Ping
2026-06-14T11:37:15.647Z ::ffff:127.0.0.1 : Ping
2026-06-14T11:37:15.667Z ::ffff:127.0.0.1 : Ping
2026-06-14T11:37:28.754Z ::ffff:127.0.0.1 : Ping
2026-06-14T11:37:28.792Z ::ffff:127.0.0.1 : Ping
2026-06-14T11:37:47.975Z ::ffff:127.0.0.1 : Ping
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
2026-06-26T03:31:10.434Z ::ffff:127.0.0.1 : Ping
2026-06-26T03:31:10.438Z ::ffff:127.0.0.1 : Ping
2026-06-26T04:00:23.262Z ::ffff:127.0.0.1 : Ping
2026-06-26T04:00:23.430Z ::ffff:127.0.0.1 : Ping
2026-06-26T04:36:17.220Z ::ffff:127.0.0.1 : Ping
2026-06-26T04:36:17.238Z ::ffff:127.0.0.1 : Ping
2026-06-26T06:18:31.738Z ::ffff:127.0.0.1 : Ping
2026-06-26T06:18:31.869Z ::ffff:127.0.0.1 : Ping
2026-06-26T06:20:45.255Z ::ffff:127.0.0.1 : Ping
2026-06-26T06:20:45.281Z ::ffff:127.0.0.1 : Ping
2026-06-26T06:21:16.495Z ::ffff:127.0.0.1 : Ping
2026-06-26T06:21:16.497Z ::ffff:127.0.0.1 : Ping
2026-06-26T06:21:22.653Z ::ffff:127.0.0.1 : Ping
2026-06-26T06:21:23.102Z ::ffff:127.0.0.1 : Ping
2026-06-26T06:21:27.537Z ::ffff:127.0.0.1 : Ping
2026-06-26T06:21:27.675Z ::ffff:127.0.0.1 : Ping
2026-06-26T06:21:32.633Z ::ffff:127.0.0.1 : Ping
2026-06-26T06:21:32.640Z ::ffff:127.0.0.1 : Ping
2026-06-26T06:21:49.688Z ::ffff:127.0.0.1 : Ping
2026-06-26T06:21:49.886Z ::ffff:127.0.0.1 : Ping
2026-06-26T06:21:53.274Z ::ffff:127.0.0.1 : Ping
2026-06-26T06:21:53.507Z ::ffff:127.0.0.1 : Ping
2026-06-26T06:23:06.652Z ::ffff:127.0.0.1 : Ping
2026-06-26T06:23:06.656Z ::ffff:127.0.0.1 : Ping
2026-06-26T06:25:53.069Z ::ffff:127.0.0.1 : Ping
2026-06-26T06:25:53.087Z ::ffff:127.0.0.1 : Ping
2026-06-26T08:24:39.787Z ::ffff:127.0.0.1 : Ping
2026-06-26T08:24:39.805Z ::ffff:127.0.0.1 : Ping
2026-06-26T09:26:48.670Z ::ffff:127.0.0.1 : Ping
2026-06-26T09:26:48.675Z ::ffff:127.0.0.1 : Ping
2026-06-26T09:27:11.052Z ::ffff:127.0.0.1 : Ping
2026-06-26T09:27:11.059Z ::ffff:127.0.0.1 : Ping
2026-06-26T09:27:19.569Z ::ffff:127.0.0.1 : Ping
2026-06-26T09:27:19.582Z ::ffff:127.0.0.1 : Ping
2026-06-26T09:27:36.325Z ::ffff:127.0.0.1 : Ping
2026-06-26T09:27:36.338Z ::ffff:127.0.0.1 : Ping
2026-06-26T09:28:07.197Z ::ffff:127.0.0.1 : Ping
2026-06-26T09:28:07.297Z ::ffff:127.0.0.1 : Ping
2026-06-26T09:28:10.358Z ::ffff:127.0.0.1 : Ping
2026-06-26T09:28:10.433Z ::ffff:127.0.0.1 : Ping
2026-06-26T09:35:29.584Z ::ffff:127.0.0.1 : Ping
2026-06-26T09:35:29.593Z ::ffff:127.0.0.1 : Ping
2026-06-26T09:35:33.181Z ::ffff:127.0.0.1 : Ping
2026-06-26T09:35:33.224Z ::ffff:127.0.0.1 : Ping
2026-06-26T10:18:31.191Z ::ffff:127.0.0.1 : Ping
2026-06-26T10:18:31.221Z ::ffff:127.0.0.1 : Ping
2026-06-26T10:26:37.190Z ::ffff:127.0.0.1 : Ping
2026-06-26T10:26:37.324Z ::ffff:127.0.0.1 : Ping
2026-06-26T10:26:51.865Z ::ffff:127.0.0.1 : Ping
2026-06-26T10:26:51.878Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:23:40.342Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:23:40.380Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:42:17.762Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:42:18.249Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:42:25.830Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:42:25.966Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:42:30.266Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:42:30.665Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:42:37.346Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:42:37.827Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:42:41.909Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:42:42.340Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:42:46.277Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:42:46.720Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:42:57.908Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:42:58.354Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:43:07.753Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:43:07.991Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:43:11.231Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:43:11.685Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:45:00.276Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:45:00.689Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:45:09.298Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:45:10.098Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:45:13.192Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:45:13.723Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:45:26.922Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:45:27.209Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:45:36.830Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:45:37.810Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:45:40.875Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:45:41.389Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:46:04.169Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:46:04.957Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:46:18.906Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:46:19.134Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:46:22.320Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:46:22.768Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:47:43.228Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:47:43.620Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:47:47.228Z ::ffff:127.0.0.1 : Ping
2026-06-26T13:47:47.661Z ::ffff:127.0.0.1 : Ping
2026-06-26T14:26:30.291Z ::ffff:127.0.0.1 : Ping
2026-06-26T14:26:31.003Z ::ffff:127.0.0.1 : Ping
2026-06-26T14:27:28.933Z ::ffff:127.0.0.1 : Ping
2026-06-26T14:27:29.207Z ::ffff:127.0.0.1 : Ping
2026-06-26T14:27:33.132Z ::ffff:127.0.0.1 : Ping
2026-06-26T14:27:33.857Z ::ffff:127.0.0.1 : Ping

View File

@@ -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);
})

109
robot/FCodeClient.js Normal file
View File

@@ -0,0 +1,109 @@
// robot/FCodeClient.js
// Translates FCode messages (FPoint, FPlus, FList, …) into REST calls to appRobotFileservice.
// The Driver is the only gateway — controllers never contact the fileservice directly.
function _baseUrl() {
return process.env.FILESERVICE_URL || 'http://appRobot_Fileservice:2100';
}
/** Returns true when the message is an FCode (F + uppercase letter + lowercase, at start). */
function isFCode(message) {
return /^F[A-Z][a-z]+/.test(String(message).trim());
}
/**
* Dispatch an FCode message to the appropriate fileservice endpoint.
* @param {object} robot Current robot state (x,y,z,phi,theta,psi,e,feedrate).
* @param {string} message Raw FCode string, e.g. "FPoint", "FPlus", "FLoad demo".
* @returns {Promise<{type: string, data?: string, line?: string}>}
* type 'step' → line (driver-native GCode, Radian) ready to execute
* type 'list'|'show'|'point'|'ok' → data (JSON string) to broadcast
*/
async function handle(robot, message) {
const msg = String(message).trim();
if (msg.startsWith('FList')) {
const data = await _req('GET', '/api/programs');
return { type: 'list', data: JSON.stringify(data) };
}
if (msg.startsWith('FShow')) {
const id = msg.slice('FShow'.length).trim();
const data = id
? await _req('GET', `/api/programs/${encodeURIComponent(id)}`)
: await _req('GET', '/api/active');
return { type: 'show', data: JSON.stringify(data) };
}
if (msg.startsWith('FLoad')) {
const id = msg.slice('FLoad'.length).trim();
const data = await _req('PUT', '/api/active', { id });
return { type: 'ok', data: JSON.stringify(data) };
}
if (msg.startsWith('FSave')) {
const name = msg.slice('FSave'.length).trim() || 'unnamed';
const data = await _req('POST', '/api/programs', { name, fromActive: true });
return { type: 'ok', data: JSON.stringify(data) };
}
if (msg.startsWith('FClear')) {
const data = await _req('POST', '/api/active/clear');
return { type: 'ok', data: JSON.stringify(data) };
}
if (msg.startsWith('FPoint')) {
// Driver attaches the current pose (Radian) — fileservice converts to degrees for storage.
const pose = {
x: robot.x, y: robot.y, z: robot.z,
a: robot.phi, b: robot.theta, c: robot.psi, e: robot.e,
};
const feedrate = robot.feedrate || 1000;
const data = await _req('POST', '/api/active/points', { pose, feedrate });
return { type: 'point', data: JSON.stringify(data) };
}
if (msg.startsWith('FPlus')) { const d = await _req('POST', '/api/active/next'); return { type: 'step', line: d.line }; }
if (msg.startsWith('FMinus')) { const d = await _req('POST', '/api/active/prev'); return { type: 'step', line: d.line }; }
if (msg.startsWith('FFirst')) { const d = await _req('POST', '/api/active/first'); return { type: 'step', line: d.line }; }
if (msg.startsWith('FLast')) { const d = await _req('POST', '/api/active/last'); return { type: 'step', line: d.line }; }
if (msg.startsWith('FGoto')) {
const index = parseInt(msg.slice('FGoto'.length).trim(), 10);
const d = await _req('POST', '/api/active/goto', { index });
return { type: 'step', line: d.line };
}
if (msg.startsWith('FPlay')) { const d = await _req('POST', '/api/active/play'); return { type: 'ok', data: JSON.stringify(d) }; }
if (msg.startsWith('FStop')) { const d = await _req('POST', '/api/active/stop'); return { type: 'ok', data: JSON.stringify(d) }; }
throw new Error(`Unbekannter FCode: ${msg}`);
}
async function _req(method, path, body) {
const url = `${_baseUrl()}${path}`;
const opts = { method };
if (body !== undefined && body !== null) {
opts.headers = { 'content-type': 'application/json' };
opts.body = JSON.stringify(body);
}
console.log(`[FCode] → ${method} ${url}${body ? ' ' + JSON.stringify(body) : ''}`);
let res;
try {
res = await fetch(url, opts);
} catch (netErr) {
// Fileservice nicht erreichbar (DNS/Connection refused): klare Meldung statt "fetch failed".
console.error(`[FCode] ✖ ${method} ${url}: Fileservice nicht erreichbar (${netErr.message})`);
const e = new Error(`Fileservice nicht erreichbar: ${netErr.message}`);
e.code = 'FILESERVICE_UNREACHABLE';
throw e;
}
if (!res.ok) {
const err = await res.json().catch(() => ({}));
console.error(`[FCode] ✖ ${res.status} ${method} ${path}: ${err.code || ''} ${err.message || res.statusText}`);
const e = new Error(err.message || res.statusText);
e.code = err.code;
e.status = res.status;
throw e;
}
const data = await res.json();
console.log(`[FCode] ← ${res.status} ${method} ${path} ${JSON.stringify(data).slice(0, 160)}`);
return data;
}
module.exports = { isFCode, handle };

View File

@@ -1,14 +1,11 @@
/***
* Receives GCode, processes it and moves the Data to the Roboter-Class
*/
const fs = require('fs');
const RobotController = require('./RobotController');
class GCode{
static fileName = "GCodeFiles/log.gcode";
static containsMCode(s){
return s === 'M1' || s.startsWith('M1 ');
}
@@ -43,45 +40,29 @@ 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":'+ 0.0 +
'}}';
', "e":'+ (robot.eMotor ?? 0) +
'}}';
return text;
}
static toPiMultiple(gCode){
if(gCode == undefined){return gCode;}
var multipleParameters = gCode.split(" ");
var newGString = "";
multipleParameters.forEach((paramet) => {
if(paramet == undefined || !paramet || paramet.length === 0 || paramet == " " || paramet ==""){
}
else if(['A', 'a', 'B', 'b', 'C', 'c','E','e'].includes(paramet.charAt(0)) )
{
var numberParameter = Number(paramet.substring(1, paramet.length))
newGString += " " + paramet[0] + String(numberParameter*Math.PI/180)
}
else{
newGString += " " + paramet;
}
});
return newGString.trim();
};
/**
* Verarbeitet eine rohe G-Code-Nachricht. Parsing + Steuerlogik liegen jetzt im
* GCodeParser bzw. RobotController (ToDo_6); diese Methode bleibt als Fassade
@@ -94,95 +75,6 @@ class GCode{
return RobotController.receive(robot, g);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////77
// Commands for Files
static removeStringFromFile(fileName, stringToRemove) {
try {
const data = fs.readFileSync(fileName, 'utf8');
const modifiedData = data.replace(new RegExp(stringToRemove, 'g'), '');
fs.writeFileSync(fileName, modifiedData, 'utf8');
} catch (err) {
console.error('Error:', err);
}
}
static ContainsFilesCommand(message){
if(message.indexOf('FPoint') !== -1){return true;} // Writes new Point (at end of file)
if(message.indexOf('FPlus') !== -1){return true;} // go to Next Position in Log File
if(message.indexOf('FMinus') !== -1){return true;} // go to Previous Position
if(message.indexOf('FFirst') !== -1){return true;} // set Cursour to First Position of Log File
if(message.indexOf('FLast') !== -1){return true;} // set Cursour to Last Position of Log File
if(message.indexOf('FShow') !== -1){return true;} // Shows/Sends GCode-File
if(message.indexOf('FList') !== -1){return true;} // Lists GCode-Files
if(message.indexOf('FLoad ') !== -1){return true;} // Loads File into Log
if(message.indexOf('FSave ') !== -1){return true;} // Saves Log to GCode-File
if(message.indexOf('FClear') !== -1){return true;} // Clears default Log File
if(message.indexOf('M20') !== -1){return true;} // M20 - List SD Card-Contents // https://marlinfw.org/docs/gcode/M020.html
if(message.indexOf('M23') !== -1){return true;} // M23 - Select SD file
if(message.indexOf('M28') !== -1){return true;} // M28 - Start SD write // M28 [B1] filename
if(message.indexOf('M29') !== -1){return true;} // M29 - Stop SD write
return false;
}
static receiveFC(robot, message){
if(message.indexOf('FShow') !== -1){
try {
const data = fs.readFileSync(this.fileName, 'utf8');
const reply = "XYZ__FShow__XYZ" + data; // prepend header
return reply.replaceAll("G91 ","").replaceAll("G90 ","").replace(/\s?\bt\d+\b\s?/g, '').trim();
} catch (err) {
console.error('Error:', err);
}
}
if(message.indexOf('FPoint') !== -1){
const secondsSinceEpoch = 10*Math.floor(Date.now() / 100);
var strGCode = String(`G90 G1 x${robot.x} y${robot.y} z${robot.z} a${(robot.phi*180/Math.PI).toFixed(2)} b${(robot.theta*180/Math.PI).toFixed(2)} c${(robot.psi*180/Math.PI).toFixed(2)} e${(robot.e*180/Math.PI).toFixed(2)} t${secondsSinceEpoch} f1000` )
this.removeStringFromFile(this.fileName, ';!')
fs.appendFileSync(this.fileName, strGCode+ ';!'+ '\r\n', 'utf8');
}
if(message.indexOf('FMinus') !== -1 || message.indexOf('FPlus') !== -1){
let lines = fs.readFileSync(this.fileName, 'utf8').split('\r\n');
let lineChange = -1;
// Process lines
for (let i = 0; i < lines.length; i++) {
if (lines[i].indexOf(';!') !== -1) {
if(message.indexOf('FMinus') !== -1 && i > 0){
lines[i - 1] += ';!';
lines[i] = lines[i].split(';!')[0];
var gCodePi = this.toPiMultiple(lines[i-1]);
this.receiveGCode(robot, gCodePi);
}
if(message.indexOf('FPlus') !== -1 && i < (lines.length-2) && lineChange == -1){
lines[i + 1] += ';!';
lines[i] = lines[i].split(';!')[0];
lineChange = i+1;
var gCodePi = this.toPiMultiple(lines[i+1]);
this.receiveGCode(robot, gCodePi);
}
}
}
// Write the updated content back
fs.writeFileSync(this.fileName, lines.join('\r\n'), 'utf8');
}
return GCode.getM114(robot);
}
}
module.exports = GCode

View File

@@ -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.
*

View File

@@ -27,10 +27,16 @@ function deriveKinematicParams(links) {
const result = {};
const l1raw = links.Arm1?.skeleton?.to?.[1];
const l2raw = links.Arm2?.skeleton?.to?.[1];
const l3raw = links.Ellbow?.skeleton?.to?.[0];
if (l1raw != null) result.l1 = Math.abs(l1raw);
if (l2raw != null) result.l2 = Math.abs(l2raw);
if (l3raw != null) result.l3 = l3raw;
// l3 = Endeffektor-Länge (Handgelenk → Fingerspitze) = Hand-Segment + Finger.
// FRÜHER fälschlich aus Ellbow.skeleton.to[0] abgeleitet — das ist der seitliche
// Ellbogen-Versatz, nicht die Hand/Finger-Länge (siehe doc/Info_Koordinaten.md, Phase 2).
const handLen = links.Hand?.skeleton?.to?.[1]; // Hand-Segment (z. B. -35)
const fingerLen = links.FingerA?.skeleton?.to?.[1]; // Finger (z. B. -60)
if (handLen != null || fingerLen != null) {
result.l3 = Math.abs(handLen ?? 0) + Math.abs(fingerLen ?? 0);
}
return result;
}
@@ -58,11 +64,13 @@ function load(fsModule, processEnv, consoleObj) {
// Kinematik-Typ und Armlängen (aus links abgeleitet)
const linkParams = deriveKinematicParams(json?.links);
// Explizite kinematics.l1/l2/l3 in robot.json haben Vorrang vor der links-Ableitung
// (zum Kalibrieren der realen Längen, z. B. l3 an die gemessene Reichweite anpassen).
const kinematics = {
type: json?.kinematics?.type ?? DEFAULTS.kinematics.type,
l1: linkParams.l1 ?? DEFAULTS.kinematics.l1,
l2: linkParams.l2 ?? DEFAULTS.kinematics.l2,
l3: linkParams.l3 ?? DEFAULTS.kinematics.l3
l1: json?.kinematics?.l1 ?? linkParams.l1 ?? DEFAULTS.kinematics.l1,
l2: json?.kinematics?.l2 ?? linkParams.l2 ?? DEFAULTS.kinematics.l2,
l3: json?.kinematics?.l3 ?? linkParams.l3 ?? DEFAULTS.kinematics.l3
};
// Bewegungs-Defaults — Env hat Vorrang, dann robot.json, dann Hard-Default

View File

@@ -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 {
@@ -52,14 +52,39 @@ class RobotController {
}
if (cmd === 'G28') {
robot.x = 0;
robot.y = robot.l1 + robot.l2 + robot.l3;
robot.z = 0;
robot.phi = -Math.PI / 2;
robot.theta = Math.PI / 2;
robot.psi = 0;
robot.e = 0;
robot.calculateAngles3D();
// Home = Grundstellung: Arm + Hand gestreckt entlang -y (siehe
// doc/Info_Koordinaten.md). Ziel-Fingerspitze: (0, -(l1+l2+l3), 0).
const reach = robot.l1 + robot.l2 + robot.l3;
const homeY = -reach;
if (Math.abs(Math.abs(homeY) - reach) < 1e-6) {
// Sonderfall voll ausgestreckt: |y| = l1+l2+l3 ist eine Handgelenk-
// Singularität — die IK kann den Unterarm-Dreher a (und damit c) dort
// nicht bestimmen und liefert Müll (z.B. a=135°, c=45°), wodurch der
// Finger schräg/nach unten zeigt. Daher die Motorwerte DIREKT in die
// Grundstellung setzen und die Workspace-Pose per FK füllen.
robot.xMotor = 0;
robot.alpha = 0;
robot.beta = 0;
robot.a = 0;
robot.b = Math.PI; // gerade Hand (Phase-1-Konvention; Phase 2: 0)
robot.c = 0;
// Greifer (e/eMotor) bewusst UNANGETASTET lassen: G28 fährt nur den Arm.
// Würde man e=0 setzen, ergäbe die Kopplung eMotor = e b c bei b=π den
// Wert −π → 180° am Finger-Motor → Anschlag-Slam (siehe
// doc/Info_Koordinaten.md, Phase 2). Phase 2 (b=0 = gerade) löst das sauber.
robot.calculatePositionFromMotorAngles(); // FK -> x=0, y=-(l1+l2+l3), z=0
} else {
// Allgemeiner (nicht-singulärer) Home-Punkt: regulär über die IK.
robot.x = 0;
robot.y = homeY;
robot.z = 0;
robot.phi = Math.PI / 2;
robot.theta = Math.PI / 2;
robot.psi = 0;
robot.e = 0;
robot.calculateAngles3D();
}
robot.sendCommand();
return;
}
@@ -121,14 +146,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');

View File

@@ -33,8 +33,38 @@ class Arm3SegmentLinearX extends RobotBase {
this.l3 = l3;
}
// Berechnet aus XYZ die Motor-Winkel für den GCode
calculateAngles3D(verbose){
// ─────────────────────────────────────────────────────────────────────
// Y-Konvention: Der reale Roboter steht/arbeitet in -y (robot.json:
// Arm1 -> [0,-250,0], coordinateSystem.y = "backward"). Die interne
// Kinematik (_ikPlusY/_fkPlusY) rechnet historisch in +y. Beide
// öffentlichen Methoden spiegeln daher die Workspace-Pose an der x-z-Ebene
// (y, pY, phi, psi; theta bleibt), sodass alpha=0 nach -y zeigt.
// Siehe doc/Info_Koordinaten.md (Weg 2, Phase 1).
// ─────────────────────────────────────────────────────────────────────
/** Reflexion der Workspace-Pose an der x-z-Ebene (Involution: zweimal = Identität). */
_mirrorWorkspaceY() {
this.y = -this.y;
this.pY = -this.pY;
this.phi = -this.phi;
this.psi = -this.psi;
}
calculateAngles3D(verbose) {
// -y-Eingabe in den internen +y-Frame spiegeln, rechnen, dann die
// Workspace-Felder zurückspiegeln (Motorwerte bleiben unberührt).
this._mirrorWorkspaceY();
this._ikPlusY(verbose);
this._mirrorWorkspaceY();
}
calculatePositionFromMotorAngles(verbose = false) {
this._fkPlusY(verbose);
this._mirrorWorkspaceY(); // +y-Ergebnis -> -y Workspace
}
// Berechnet aus XYZ die Motor-Winkel für den GCode (interne +y-Mathematik)
_ikPlusY(verbose){
while(this.phi > Math.PI){this.phi -= 2*Math.PI}
while(this.phi < -Math.PI){this.phi += 2*Math.PI}
while(this.theta > Math.PI){this.theta -= 2*Math.PI}
@@ -56,7 +86,7 @@ class Arm3SegmentLinearX extends RobotBase {
if (r > (this.l1 + this.l2)) { return; }
if (r == 0) { return; }
var gamma = Math.asin(pZ / r);
var gamma = Math.atan2(pZ, pY);
var delta = Math.acos((this.l1 * this.l1 + this.l2 * this.l2 - r * r) / (2 * this.l1 * this.l2));
this.alpha = Math.acos((this.l1 * this.l1 + r * r - this.l2 * this.l2) / (2 * r * this.l1)) + gamma;
this.beta = -Math.PI + (this.alpha + delta);
@@ -95,10 +125,20 @@ 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);
}
calculatePositionFromMotorAngles(verbose = false) {
/**
* 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;
}
_fkPlusY(verbose = false) {
const vecBizeps = {x: this.xMotor, y: this.l1 * Math.cos(this.alpha), z: this.l1 * Math.sin(this.alpha)}
const vecUnterarm = {x: 0, y: Math.cos(this.beta), z: Math.sin(this.beta)}

View File

@@ -1,6 +1,7 @@
// server/InputWS.js
const fs = require('fs');
const WebSocket = require('ws');
const FCodeClient = require('../robot/FCodeClient');
const LOG_DIR = './logs';
@@ -70,18 +71,29 @@ function initInputWS(server, robot, GCode, sharedState) {
return;
}
/* ---------- File commands → broadcast result ----------
* Behaviour kept as-is on purpose: file/log management (and finer-grained
* targeting, e.g. FShow as a requester-only reply) is owned by ToDo 4. */
if (GCode.ContainsFilesCommand(message)) {
/* ---------- FCode (Datei-Befehle) → weiterleiten an appRobotFileservice ----------
* Der Driver ist Gateway: die Steuerungen kennen nur ihn.
* Stepping-Befehle (FPlus/FMinus/…) liefern eine driver-native GCode-Zeile
* (Radian) zurück, die der Driver direkt ausführt und dann broadcastet. */
if (FCodeClient.isFCode(message)) {
console.log("📁 FCode → Fileservice: " + message);
logCommand(sharedState, clientIP, message);
let result;
try {
result = GCode.receiveFC(robot, message);
} catch (err) {
return sendError(ws, 'FILE_ERROR', err.message, message);
}
if (result !== undefined) broadcast(wss, result);
FCodeClient.handle(robot, message)
.then(result => {
if (result.type === 'step' && result.line) {
console.log("📁 FCode Step → execute: " + result.line);
try { GCode.receiveGCode(robot, result.line); } catch (err) {
return sendError(ws, 'GCODE_ERROR', err.message, result.line);
}
broadcast(wss, GCode.getM114(robot));
} else if (result.data) {
broadcast(wss, result.data);
}
})
.catch(err => {
console.error("📁 FCode FEHLER (" + message + "): " + err.message);
sendError(ws, err.code || 'FILE_ERROR', err.message, message);
});
return;
}

151
test/FCodeClient.test.js Normal file
View File

@@ -0,0 +1,151 @@
const FCodeClient = require('../robot/FCodeClient');
const robot = {
x: 0, y: 300, z: 0,
phi: Math.PI / 2, theta: -Math.PI / 2, psi: 0, e: 0,
feedrate: 1000,
};
function mockOk(data) {
global.fetch = jest.fn().mockResolvedValue({
ok: true, status: 200, statusText: 'OK',
json: () => Promise.resolve(data),
});
}
function mockErr(status, code, message) {
global.fetch = jest.fn().mockResolvedValue({
ok: false, status, statusText: 'Error',
json: () => Promise.resolve({ code, message }),
});
}
afterEach(() => { delete global.fetch; });
/* ---------- isFCode ---------- */
describe('isFCode', () => {
test('erkennt alle bekannten FCodes', () => {
for (const f of ['FList','FShow','FLoad demo','FSave Name','FClear','FPoint','FPlus','FMinus','FFirst','FLast','FGoto 3','FPlay','FStop']) {
expect(FCodeClient.isFCode(f)).toBe(true);
}
});
test('erkennt Nicht-FCodes nicht', () => {
expect(FCodeClient.isFCode('G1 X100')).toBe(false);
expect(FCodeClient.isFCode('F1000')).toBe(false);
expect(FCodeClient.isFCode('M114')).toBe(false);
expect(FCodeClient.isFCode('Ping')).toBe(false);
expect(FCodeClient.isFCode('')).toBe(false);
});
});
/* ---------- handle: Listing / Info ---------- */
test('FList → GET /api/programs, type list', async () => {
mockOk([{ id: 'a', name: 'A' }]);
const result = await FCodeClient.handle(robot, 'FList');
expect(result.type).toBe('list');
expect(global.fetch).toHaveBeenCalledWith(
expect.stringContaining('/api/programs'),
expect.objectContaining({ method: 'GET' })
);
expect(JSON.parse(result.data)).toEqual([{ id: 'a', name: 'A' }]);
});
test('FShow ohne id → GET /api/active', async () => {
mockOk({ programId: 'x', cursor: 0 });
const result = await FCodeClient.handle(robot, 'FShow');
expect(global.fetch).toHaveBeenCalledWith(expect.stringContaining('/api/active'), expect.anything());
expect(result.type).toBe('show');
});
test('FShow mit id → GET /api/programs/:id', async () => {
mockOk({ id: 'demo', name: 'Demo' });
await FCodeClient.handle(robot, 'FShow demo');
expect(global.fetch).toHaveBeenCalledWith(expect.stringContaining('/api/programs/demo'), expect.anything());
});
/* ---------- handle: Teaching ---------- */
test('FPoint → POST /api/active/points mit Roboterpose (Radian)', async () => {
mockOk({ index: 0, line: 'G90 G1 x0 y300 z0 a1.5708 b-1.5708 c0 e0 f1000' });
const result = await FCodeClient.handle(robot, 'FPoint');
expect(result.type).toBe('point');
const call = global.fetch.mock.calls[0];
expect(call[1].method).toBe('POST');
const body = JSON.parse(call[1].body);
expect(body.pose.a).toBeCloseTo(Math.PI / 2, 4);
expect(body.pose.b).toBeCloseTo(-Math.PI / 2, 4);
expect(body.feedrate).toBe(1000);
});
/* ---------- handle: Program management ---------- */
test('FLoad → PUT /api/active mit id', async () => {
mockOk({ programId: 'mein_prog', cursor: 0, lineCount: 5 });
const result = await FCodeClient.handle(robot, 'FLoad mein_prog');
expect(result.type).toBe('ok');
const body = JSON.parse(global.fetch.mock.calls[0][1].body);
expect(body).toEqual({ id: 'mein_prog' });
});
test('FSave → POST /api/programs mit name und fromActive:true', async () => {
mockOk({ id: 'test_prog', name: 'Test Prog' });
await FCodeClient.handle(robot, 'FSave Test Prog');
const body = JSON.parse(global.fetch.mock.calls[0][1].body);
expect(body.name).toBe('Test Prog');
expect(body.fromActive).toBe(true);
});
test('FClear → POST /api/active/clear', async () => {
mockOk({ ok: true });
await FCodeClient.handle(robot, 'FClear');
expect(global.fetch).toHaveBeenCalledWith(expect.stringContaining('/api/active/clear'), expect.objectContaining({ method: 'POST' }));
});
/* ---------- handle: Stepping → type step ---------- */
test('FPlus → POST /api/active/next → type step mit Zeile', async () => {
mockOk({ cursor: 1, line: 'G90 G1 x10 y300 z0 a1.5708 b-1.5708 c0 e0 f1000' });
const result = await FCodeClient.handle(robot, 'FPlus');
expect(result.type).toBe('step');
expect(result.line).toContain('G1');
});
test('FMinus → POST /api/active/prev', async () => {
mockOk({ cursor: 0, line: 'G90 G1 x0 y300 z0 a0 b0 c0 e0 f1000' });
const result = await FCodeClient.handle(robot, 'FMinus');
expect(result.type).toBe('step');
});
test('FFirst / FLast', async () => {
mockOk({ cursor: 0, line: 'G90 G1 x0 y0 z0 a0 b0 c0 e0 f1000' });
const r1 = await FCodeClient.handle(robot, 'FFirst');
expect(r1.type).toBe('step');
mockOk({ cursor: 4, line: 'G90 G1 x5 y0 z0 a0 b0 c0 e0 f1000' });
const r2 = await FCodeClient.handle(robot, 'FLast');
expect(r2.type).toBe('step');
});
test('FGoto N → POST /api/active/goto mit index', async () => {
mockOk({ cursor: 3, line: 'G90 G1 x3 y0 z0 a0 b0 c0 e0 f1000' });
await FCodeClient.handle(robot, 'FGoto 3');
const body = JSON.parse(global.fetch.mock.calls[0][1].body);
expect(body.index).toBe(3);
});
/* ---------- Fehlerbehandlung ---------- */
test('Fileservice-Fehler → wirft mit code und status', async () => {
mockErr(404, 'PROGRAM_NOT_FOUND', 'not found');
await expect(FCodeClient.handle(robot, 'FLoad nichtda')).rejects.toMatchObject({
code: 'PROGRAM_NOT_FOUND',
status: 404,
});
});
test('Unbekannter FCode → wirft', async () => {
await expect(FCodeClient.handle(robot, 'FUnbekannt')).rejects.toThrow('Unbekannter FCode');
});

View File

@@ -1,6 +1,26 @@
const GCode = require('../robot/GCode.js');
test('GCode - FPoint', () => {
var x = GCode.ContainsFilesCommand("FPoint") ;
expect(x).toBe(true);
});
// Datei-Operationen liegen jetzt in appRobotFileservice.
// GCode kennt keine ContainsFilesCommand mehr — FCodeClient.isFCode übernimmt die Erkennung.
const FCodeClient = require('../robot/FCodeClient');
test('isFCode erkennt FPoint', () => {
expect(FCodeClient.isFCode('FPoint')).toBe(true);
});
test('isFCode erkennt FList', () => {
expect(FCodeClient.isFCode('FList')).toBe(true);
});
test('isFCode erkennt FPlus / FMinus / FFirst / FLast', () => {
expect(FCodeClient.isFCode('FPlus')).toBe(true);
expect(FCodeClient.isFCode('FMinus')).toBe(true);
expect(FCodeClient.isFCode('FFirst')).toBe(true);
expect(FCodeClient.isFCode('FLast')).toBe(true);
});
test('isFCode: feedrate F1000 ist kein FCode', () => {
expect(FCodeClient.isFCode('F1000')).toBe(false);
});
test('isFCode: G1-Befehl ist kein FCode', () => {
expect(FCodeClient.isFCode('G1 X100')).toBe(false);
});

View File

@@ -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);
});
});

View File

@@ -90,17 +90,25 @@ describe('GCode.receiveGCode', () => {
expect(robot.sendCommand).toHaveBeenCalled()
})
test('G28 setzt Home-Position und löst Bewegung aus', () => {
test('G28 setzt Home-Motorwerte direkt (Singularität), lässt aber den Greifer unangetastet', () => {
const robot = createDummyRobot()
robot.e = 7 // vorher gesetzte Greifer-Öffnung ...
robot.eMotor = 3 // ... darf von G28 NICHT bewegt werden (sonst ebc-Slam bei b=π)
GCode.receiveGCode(robot, 'G28')
expect(robot.x).toBe(0)
expect(robot.z).toBe(0)
expect(robot.y).toBe(robot.l1 + robot.l2 + robot.l3)
expect(robot.phi).toBeCloseTo(-Math.PI / 2)
expect(robot.theta).toBeCloseTo(Math.PI / 2)
expect(robot.calculateAngles3D).toHaveBeenCalledTimes(1)
// Voll ausgestreckt = Handgelenk-Singularität -> Arm-Motorwerte DIREKT, dann FK (nicht IK).
expect(robot.xMotor).toBe(0)
expect(robot.alpha).toBe(0)
expect(robot.beta).toBe(0)
expect(robot.a).toBe(0)
expect(robot.b).toBe(Math.PI) // gerade Hand (Phase-1-Konvention)
expect(robot.c).toBe(0)
// Greifer unverändert -> kein Finger-Slam am Anschlag
expect(robot.e).toBe(7)
expect(robot.eMotor).toBe(3)
expect(robot.calculateAngles3D).not.toHaveBeenCalled()
expect(robot.calculatePositionFromMotorAngles).toHaveBeenCalledTimes(1)
expect(robot.sendCommand).toHaveBeenCalledTimes(1)
})

View File

@@ -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 () => {

View File

@@ -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();

105
test/InputWS.fcode.test.js Normal file
View File

@@ -0,0 +1,105 @@
// Testet das FCode-Routing in InputWS (FCodeClient wird gemockt).
jest.mock('../robot/FCodeClient', () => ({
isFCode: jest.fn((msg) => /^F[A-Z][a-z]+/.test(String(msg).trim())),
handle: jest.fn(),
}));
const http = require('http');
const WebSocket = require('ws');
const FCodeClient = require('../robot/FCodeClient');
const initInputWS = require('../server/InputWS');
const GCode = require('../robot/GCode');
const createDummyRobot = require('./helpers/createDummyRobot');
/* ---------- helpers ---------- */
function listen(server) {
return new Promise((resolve, reject) => {
server.listen(0, () => {
const { port } = server.address();
port ? resolve(port) : reject(new Error('no port'));
});
server.on('error', reject);
});
}
function connect(port) {
return new Promise((resolve, reject) => {
const ws = new WebSocket(`ws://127.0.0.1:${port}`);
ws.on('open', () => resolve(ws));
ws.on('error', reject);
});
}
function nextMessage(ws, timeoutMs = 2000) {
return new Promise((resolve, reject) => {
const t = setTimeout(() => reject(new Error('Timeout')), timeoutMs);
ws.once('message', (d) => { clearTimeout(t); resolve(d.toString()); });
});
}
/* ---------- suite ---------- */
describe('InputWS FCode-Routing', () => {
let server, ws;
beforeEach(() => {
FCodeClient.handle.mockReset();
FCodeClient.isFCode.mockClear();
});
afterEach(async () => {
if (ws) { ws.close(); ws = null; }
if (server) { await new Promise(r => server.close(r)); server = null; }
});
async function setup(robot) {
server = http.createServer();
initInputWS(server, robot || createDummyRobot(), GCode, { connectedClients: [], lastCommands: [], lastPings: [] });
const port = await listen(server);
ws = await connect(port);
return ws;
}
test('FList → broadcastet die Antwortdaten vom Fileservice', async () => {
FCodeClient.handle.mockResolvedValue({ type: 'list', data: JSON.stringify([{ id: 'a' }]) });
await setup();
const replyP = nextMessage(ws);
ws.send('FList');
expect(JSON.parse(await replyP)).toEqual([{ id: 'a' }]);
});
test('FPlus (step) → GCode-Zeile ausführen + M114 broadcasten', async () => {
const line = 'G90 G1 x10 y300 z0 a1.5708 b-1.5708 c0 e0 f1000';
FCodeClient.handle.mockResolvedValue({ type: 'step', line });
const robot = createDummyRobot();
await setup(robot);
const replyP = nextMessage(ws);
ws.send('FPlus');
const msg = JSON.parse(await replyP);
expect(msg.position).toBeDefined();
expect(robot.sendCommand).toHaveBeenCalled();
});
test('Fileservice-Fehler → spezifischer Fehlercode wird an Sender durchgereicht', async () => {
const err = Object.assign(new Error('not found'), { code: 'PROGRAM_NOT_FOUND' });
FCodeClient.handle.mockRejectedValue(err);
await setup();
const replyP = nextMessage(ws);
ws.send('FLoad nichtda');
const msg = JSON.parse(await replyP);
expect(msg.type).toBe('error');
expect(msg.code).toBe('PROGRAM_NOT_FOUND'); // err.code wird durchgereicht (nicht pauschal FILE_ERROR)
expect(msg.input).toBe('FLoad nichtda');
});
test('Fehler ohne code → Fallback FILE_ERROR', async () => {
FCodeClient.handle.mockRejectedValue(new Error('kaputt'));
await setup();
const replyP = nextMessage(ws);
ws.send('FShow');
const msg = JSON.parse(await replyP);
expect(msg.type).toBe('error');
expect(msg.code).toBe('FILE_ERROR');
});
});

View File

@@ -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();

View File

@@ -4,16 +4,16 @@ test('Grade ausgestreckt', () => {
robot = new Robot(300,290,10)
robot.x = 0 ;
robot.y = 600;
robot.y = -600; // -y: voll ausgestreckte Grundstellung
robot.z = 0;
robot.phi = -Math.PI/2;
robot.phi = Math.PI/2; // gespiegelt zu -y (siehe doc/Info_Koordinaten.md)
robot.theta = Math.PI/2;
robot.calculateAngles3D();
expect(robot.pX).toBeLessThanOrEqual(0.00001)
expect(robot.pY).toBe(590)
expect(robot.pY).toBe(-590)
expect(robot.pZ).toBeLessThanOrEqual(0.00001)
expect(robot.alpha).toBeLessThanOrEqual(0.00001)
@@ -24,9 +24,9 @@ test('Grade gewinkelt', () => {
robot = new Robot(300,290,10)
robot.x = 0 ;
robot.y = 300;
robot.y = -300; // -y
robot.z = 0;
robot.phi = -Math.PI/2;
robot.phi = Math.PI/2; // gespiegelt zu -y
robot.theta = Math.PI/2 - Math.PI/3;
@@ -43,7 +43,7 @@ test('schräg gewinkelt 1', () => {
robot = new Robot(300,300,10)
robot.x = 0 ;
robot.y = 310;
robot.y = -310; // -y (phi=0 ist spiegel-invariant)
robot.z = 0;
robot.phi = 0;
robot.theta = Math.PI/2;

View File

@@ -0,0 +1,107 @@
// Phase 1: Der reale Roboter arbeitet in -Y (robot.json: Arm1 -> [0,-250,0]).
// alpha=0 muss nach -y zeigen, nicht nach +y. Siehe doc/Info_Koordinaten.md.
const Robot = require('../robot/kinematics/Arm3SegmentLinearX');
const GCode = require('../robot/GCode');
const D = 180 / Math.PI;
describe('Phase 1 — Arm arbeitet in -Y (alpha=0 zeigt nach -y)', () => {
beforeAll(() => jest.spyOn(console, 'log').mockImplementation(() => {}));
afterAll(() => jest.restoreAllMocks());
const L1 = 250, L2 = 250, L3 = 90;
function fkFromMotors(alphaDeg, betaDeg, aDeg, bDeg, cDeg, xMotor = 0) {
const r = new Robot(L1, L2, L3);
r.xMotor = xMotor;
r.alpha = alphaDeg / D; r.beta = betaDeg / D;
r.a = aDeg / D; r.b = bDeg / D; r.c = cDeg / D;
r.calculatePositionFromMotorAngles();
return r;
}
test('voll ausgestreckt (alpha=beta=0, Hand gerade) -> y ~ -590', () => {
// B=180 = aktuelle "gerade Hand"-Konvention (Phase 2 macht daraus 0)
const r = fkFromMotors(0, 0, 0, 180, 0);
expect(r.y).toBeLessThan(0);
expect(r.y).toBeCloseTo(-(L1 + L2 + L3), 0); // ~ -590
expect(r.z).toBeCloseTo(0, 6);
});
test('gemeldete Homing-Pose landet in -y (war faelschlich +405)', () => {
// G92 X160.53 Y4.53 Z13.93 A124.04 (B=C=0)
const r = fkFromMotors(4.53, 13.93, 124.04, 0, 0, 160.53);
expect(r.y).toBeLessThan(0);
expect(r.y).toBeCloseTo(-405, 0);
expect(r.z).toBeCloseTo(58, 0);
});
test('IK der -y-Grundstellung liefert alpha~0 / beta~0 (nicht ~180)', () => {
const ref = fkFromMotors(0, 0, 0, 180, 0); // -y-ausgestreckte Pose als Referenz
const r = new Robot(L1, L2, L3);
r.x = ref.x; r.y = ref.y; r.z = ref.z;
r.phi = ref.phi; r.theta = ref.theta; r.psi = ref.psi; r.e = 0;
r.calculateAngles3D();
expect(r.alpha).toBeCloseTo(0, 3);
expect(r.beta).toBeCloseTo(0, 3);
});
test('Round-Trip im -y-Arbeitsraum bleibt konsistent', () => {
const A = new Robot(L1, L2, L3);
A.x = 10; A.y = -430; A.z = 30;
A.phi = Math.PI / 7; A.theta = Math.PI / 2; A.psi = Math.PI / 6; A.e = 0;
A.calculateAngles3D();
const B = new Robot(L1, L2, L3);
B.xMotor = A.xMotor; B.alpha = A.alpha; B.beta = A.beta;
B.a = A.a; B.b = A.b; B.c = A.c;
B.calculatePositionFromMotorAngles();
const EPS = 4;
expect(B.x).toBeCloseTo(A.x, EPS);
expect(B.y).toBeCloseTo(A.y, EPS);
expect(B.z).toBeCloseTo(A.z, EPS);
expect(B.phi).toBeCloseTo(A.phi, EPS);
expect(B.theta).toBeCloseTo(A.theta, EPS);
expect(B.psi).toBeCloseTo(A.psi, EPS);
});
test('Nullpose (alpha=beta=a=0, Hand gerade b=180) -> Fingerspitze (xMotor, -590, 0)', () => {
const r = fkFromMotors(0, 0, 0, 180, 0, 7);
expect(r.x).toBeCloseTo(7, 6); // x = xMotor
expect(r.y).toBeCloseTo(-(L1 + L2 + L3), 6);
expect(r.z).toBeCloseTo(0, 6);
});
test('a=0 -> Hand-Knick-Achse laeuft parallel zur x-Achse', () => {
// Bei a=0 knickt die Hand (b) in der y-z-Ebene -> Fingerspitze.x bleibt = xMotor.
const xM = 5;
for (const bDeg of [90, 135, 180, 225]) {
const r = fkFromMotors(0, 0, 0, bDeg, 0, xM);
expect(r.x).toBeCloseTo(xM, 6);
}
// Gegenprobe: a=90 dreht die Knick-Achse aus der y-z-Ebene -> x aendert sich deutlich.
const r90 = fkFromMotors(0, 0, 90, 135, 0, xM);
expect(Math.abs(r90.x - xM)).toBeGreaterThan(1);
});
test('G28 Home: saubere Grundstellung (a=0, c=0, Finger entlang -y) — keine Singularitaets-Garbage', () => {
const robot = new Robot(L1, L2, L3);
GCode.receiveGCode(robot, 'G28');
// Motorwerte sauber gesetzt (nicht der IK-Singularitaets-Muell a=135/c=45)
expect(robot.a).toBeCloseTo(0, 9);
expect(robot.c).toBeCloseTo(0, 9);
expect(robot.alpha).toBeCloseTo(0, 9);
expect(robot.beta).toBeCloseTo(0, 9);
// Workspace: voll ausgestreckt entlang -y
expect(robot.x).toBeCloseTo(0, 6);
expect(robot.y).toBeCloseTo(-(L1 + L2 + L3), 6);
expect(robot.z).toBeCloseTo(0, 6);
// Finger (Handgelenk -> Fingerspitze) zeigt nach -y
const hx = robot.x - robot.pX, hy = robot.y - robot.pY, hz = robot.z - robot.pZ;
const n = Math.hypot(hx, hy, hz);
expect(hy / n).toBeCloseTo(-1, 6);
});
});

View File

@@ -1,236 +1,324 @@
// __tests__/Robot.inverseKinematics.test.js
const Robot = require('../robot/kinematics/Arm3SegmentLinearX');
describe("Robot Kinematics Roundtrip", () => {
test("calculateAngles3D() <-> calculatePositionFromMotorAngles() handgelenk 1", () => {
// === Instanz A: Vorwärts-Kinematik (XYZ -> Motorwinkel) ===
const L1 = 300;
const L2 = 200;
const L3 = 10;
const A = new Robot(L1, L2, L3)
// Beispiel-Eingabe
A.x = 0;
A.y = 310;
A.z = 0;
A.phi = -Math.PI/2;
A.theta = Math.PI/2;
A.psi = 0;
A.e = 0;
A.calculateAngles3D();
// Motorwerte aus Instanz A speichern
const motor = {
xMotor: A.xMotor,
alpha: A.alpha,
beta: A.beta,
a: A.a,
b: A.b,
c: A.c
};
// === Instanz B: Rückwärts-Kinematik (Motorwinkel -> XYZ) ===
const B = new Robot(L1, L2, L3);
B.xMotor = motor.xMotor;
B.alpha = motor.alpha;
B.beta = motor.beta;
B.a = motor.a;
B.b = motor.b;
B.c = motor.c;
// Diese Funktion rekonstruiert nur x, y, z!
B.calculatePositionFromMotorAngles();
// === Vergleich mit Toleranz ===
const EPS = 0.01; // 1/1000 mm Genauigkeit
expect(B.pY).toBeCloseTo(A.pY, EPS);
expect(B.pZ).toBeCloseTo(A.pZ, EPS);
expect(B.x).toBeCloseTo(A.x, EPS);
expect(B.y).toBeCloseTo(A.y, EPS);
expect(B.z).toBeCloseTo(A.z, EPS);
});
test("calculateAngles3D() <-> calculatePositionFromMotorAngles() handgelenk 2", () => {
// === Instanz A: Vorwärts-Kinematik (XYZ -> Motorwinkel) ===
const L1 = 300;
const L2 = 300;
const L3 = 10;
const A = new Robot(L1, L2, L3)
// Beispiel-Eingabe
A.x = 0;
A.y = 410;
A.z = 0;
A.phi = -Math.PI/2;
A.theta = Math.PI/2;
A.psi = 0;
A.e = 0;
A.calculateAngles3D();
// Motorwerte aus Instanz A speichern
const motor = {
xMotor: A.xMotor,
alpha: A.alpha,
beta: A.beta,
a: A.a,
b: A.b,
c: A.c
};
// === Instanz B: Rückwärts-Kinematik (Motorwinkel -> XYZ) ===
const B = new Robot(L1, L2, L3);
B.xMotor = motor.xMotor;
B.alpha = motor.alpha;
B.beta = motor.beta;
B.a = motor.a;
B.b = motor.b;
B.c = motor.c;
// Diese Funktion rekonstruiert nur x, y, z!
B.calculatePositionFromMotorAngles();
// === Vergleich mit Toleranz ===
const EPS = 0.01; // 1/1000 mm Genauigkeit
expect(B.pY).toBeCloseTo(A.pY, EPS);
expect(B.pZ).toBeCloseTo(A.pZ, EPS);
expect(B.x).toBeCloseTo(A.x, EPS);
expect(B.y).toBeCloseTo(A.y, EPS);
expect(B.z).toBeCloseTo(A.z, EPS);
});
test("calculateAngles3D() <-> calculatePositionFromMotorAngles() handgelenk 3", () => {
// === Instanz A: Vorwärts-Kinematik (XYZ -> Motorwinkel) ===
const L1 = 300;
const L2 = 200;
const L3 = 10;
const A = new Robot(L1, L2, L3)
// Beispiel-Eingabe
A.x = 10;
A.y = 500;
A.z = 0;
A.phi = 0; //-Math.PI/2;
A.theta = Math.PI/2;
A.psi = 0;
A.e = 0;
A.calculateAngles3D();
// Motorwerte aus Instanz A speichern
const motor = {
xMotor: A.xMotor,
alpha: A.alpha,
beta: A.beta,
a: A.a,
b: A.b,
c: A.c
};
// === Instanz B: Rückwärts-Kinematik (Motorwinkel -> XYZ) ===
const B = new Robot(L1, L2, L3);
B.xMotor = motor.xMotor;
B.alpha = motor.alpha;
B.beta = motor.beta;
B.a = motor.a;
B.b = motor.b;
B.c = motor.c;
// Diese Funktion rekonstruiert nur x, y, z!
B.calculatePositionFromMotorAngles();
// === Vergleich mit Toleranz ===
const EPS = 0.01; // 1/1000 mm Genauigkeit
expect(B.pY).toBeCloseTo(A.pY, EPS);
expect(B.pZ).toBeCloseTo(A.pZ, EPS);
expect(B.x).toBeCloseTo(A.x, EPS);
expect(B.y).toBeCloseTo(A.y, EPS);
expect(B.z).toBeCloseTo(A.z, EPS);
});
test("calculateAngles3D() <-> calculatePositionFromMotorAngles() handgelenk 4", () => {
// === Instanz A: Vorwärts-Kinematik (XYZ -> Motorwinkel) ===
const L1 = 300;
const L2 = 200;
const L3 = 10;
const A = new Robot(L1, L2, L3)
// Beispiel-Eingabe
A.x = 10;
A.y = 430;
A.z = 30;
A.phi = Math.PI/7; //-Math.PI/2;
A.theta = Math.PI/2;
A.psi = 0;
A.e = 0;
A.calculateAngles3D();
// Motorwerte aus Instanz A speichern
const motor = {
xMotor: A.xMotor,
alpha: A.alpha,
beta: A.beta,
a: A.a,
b: A.b,
c: A.c
};
// === Instanz B: Rückwärts-Kinematik (Motorwinkel -> XYZ) ===
const B = new Robot(L1, L2, L3);
B.xMotor = motor.xMotor;
B.alpha = motor.alpha;
B.beta = motor.beta;
B.a = motor.a;
B.b = motor.b;
B.c = motor.c;
// Diese Funktion rekonstruiert nur x, y, z!
B.calculatePositionFromMotorAngles();
// === Vergleich mit Toleranz ===
const EPS = 0.01; // 1/1000 mm Genauigkeit
expect(B.pY).toBeCloseTo(A.pY, EPS);
expect(B.pZ).toBeCloseTo(A.pZ, EPS);
expect(B.x).toBeCloseTo(A.x, EPS);
expect(B.y).toBeCloseTo(A.y, EPS);
expect(B.z).toBeCloseTo(A.z, EPS);
expect(B.phi).toBeCloseTo(A.phi, EPS);
expect(B.theta).toBeCloseTo(A.theta, EPS);
});
// __tests__/Robot.inverseKinematics.test.js
const Robot = require('../robot/kinematics/Arm3SegmentLinearX');
describe("Robot Kinematics Roundtrip", () => {
test("calculateAngles3D() <-> calculatePositionFromMotorAngles() handgelenk 1", () => {
// === Instanz A: Vorwärts-Kinematik (XYZ -> Motorwinkel) ===
const L1 = 300;
const L2 = 200;
const L3 = 10;
const A = new Robot(L1, L2, L3)
// Beispiel-Eingabe
A.x = 0;
A.y = 310;
A.z = 0;
A.phi = -Math.PI/2;
A.theta = Math.PI/2;
A.psi = 0;
A.e = 0;
A.calculateAngles3D();
// Motorwerte aus Instanz A speichern
const motor = {
xMotor: A.xMotor,
alpha: A.alpha,
beta: A.beta,
a: A.a,
b: A.b,
c: A.c
};
// === Instanz B: Rückwärts-Kinematik (Motorwinkel -> XYZ) ===
const B = new Robot(L1, L2, L3);
B.xMotor = motor.xMotor;
B.alpha = motor.alpha;
B.beta = motor.beta;
B.a = motor.a;
B.b = motor.b;
B.c = motor.c;
// Diese Funktion rekonstruiert nur x, y, z!
B.calculatePositionFromMotorAngles();
// === Vergleich mit Toleranz ===
const EPS = 0.01; // 1/1000 mm Genauigkeit
expect(B.pY).toBeCloseTo(A.pY, EPS);
expect(B.pZ).toBeCloseTo(A.pZ, EPS);
expect(B.x).toBeCloseTo(A.x, EPS);
expect(B.y).toBeCloseTo(A.y, EPS);
expect(B.z).toBeCloseTo(A.z, EPS);
});
test("calculateAngles3D() <-> calculatePositionFromMotorAngles() handgelenk 2", () => {
// === Instanz A: Vorwärts-Kinematik (XYZ -> Motorwinkel) ===
const L1 = 300;
const L2 = 300;
const L3 = 10;
const A = new Robot(L1, L2, L3)
// Beispiel-Eingabe
A.x = 0;
A.y = 410;
A.z = 0;
A.phi = -Math.PI/2;
A.theta = Math.PI/2;
A.psi = 0;
A.e = 0;
A.calculateAngles3D();
// Motorwerte aus Instanz A speichern
const motor = {
xMotor: A.xMotor,
alpha: A.alpha,
beta: A.beta,
a: A.a,
b: A.b,
c: A.c
};
// === Instanz B: Rückwärts-Kinematik (Motorwinkel -> XYZ) ===
const B = new Robot(L1, L2, L3);
B.xMotor = motor.xMotor;
B.alpha = motor.alpha;
B.beta = motor.beta;
B.a = motor.a;
B.b = motor.b;
B.c = motor.c;
// Diese Funktion rekonstruiert nur x, y, z!
B.calculatePositionFromMotorAngles();
// === Vergleich mit Toleranz ===
const EPS = 0.01; // 1/1000 mm Genauigkeit
expect(B.pY).toBeCloseTo(A.pY, EPS);
expect(B.pZ).toBeCloseTo(A.pZ, EPS);
expect(B.x).toBeCloseTo(A.x, EPS);
expect(B.y).toBeCloseTo(A.y, EPS);
expect(B.z).toBeCloseTo(A.z, EPS);
});
test("calculateAngles3D() <-> calculatePositionFromMotorAngles() handgelenk 3", () => {
// === Instanz A: Vorwärts-Kinematik (XYZ -> Motorwinkel) ===
const L1 = 300;
const L2 = 200;
const L3 = 10;
const A = new Robot(L1, L2, L3)
// Beispiel-Eingabe
A.x = 10;
A.y = 500;
A.z = 0;
A.phi = 0; //-Math.PI/2;
A.theta = Math.PI/2;
A.psi = 0;
A.e = 0;
A.calculateAngles3D();
// Motorwerte aus Instanz A speichern
const motor = {
xMotor: A.xMotor,
alpha: A.alpha,
beta: A.beta,
a: A.a,
b: A.b,
c: A.c
};
// === Instanz B: Rückwärts-Kinematik (Motorwinkel -> XYZ) ===
const B = new Robot(L1, L2, L3);
B.xMotor = motor.xMotor;
B.alpha = motor.alpha;
B.beta = motor.beta;
B.a = motor.a;
B.b = motor.b;
B.c = motor.c;
// Diese Funktion rekonstruiert nur x, y, z!
B.calculatePositionFromMotorAngles();
// === Vergleich mit Toleranz ===
const EPS = 0.01; // 1/1000 mm Genauigkeit
expect(B.pY).toBeCloseTo(A.pY, EPS);
expect(B.pZ).toBeCloseTo(A.pZ, EPS);
expect(B.x).toBeCloseTo(A.x, EPS);
expect(B.y).toBeCloseTo(A.y, EPS);
expect(B.z).toBeCloseTo(A.z, EPS);
});
test("calculateAngles3D() <-> calculatePositionFromMotorAngles() handgelenk 4", () => {
// === Instanz A: Vorwärts-Kinematik (XYZ -> Motorwinkel) ===
const L1 = 300;
const L2 = 200;
const L3 = 10;
const A = new Robot(L1, L2, L3)
// Beispiel-Eingabe
A.x = 10;
A.y = 430;
A.z = 30;
A.phi = Math.PI/7; //-Math.PI/2;
A.theta = Math.PI/2;
A.psi = 0;
A.e = 0;
A.calculateAngles3D();
// Motorwerte aus Instanz A speichern
const motor = {
xMotor: A.xMotor,
alpha: A.alpha,
beta: A.beta,
a: A.a,
b: A.b,
c: A.c
};
// === Instanz B: Rückwärts-Kinematik (Motorwinkel -> XYZ) ===
const B = new Robot(L1, L2, L3);
B.xMotor = motor.xMotor;
B.alpha = motor.alpha;
B.beta = motor.beta;
B.a = motor.a;
B.b = motor.b;
B.c = motor.c;
// Diese Funktion rekonstruiert nur x, y, z!
B.calculatePositionFromMotorAngles();
// === Vergleich mit Toleranz ===
const EPS = 0.01; // 1/1000 mm Genauigkeit
expect(B.pY).toBeCloseTo(A.pY, EPS);
expect(B.pZ).toBeCloseTo(A.pZ, EPS);
expect(B.x).toBeCloseTo(A.x, EPS);
expect(B.y).toBeCloseTo(A.y, EPS);
expect(B.z).toBeCloseTo(A.z, EPS);
expect(B.phi).toBeCloseTo(A.phi, EPS);
expect(B.theta).toBeCloseTo(A.theta, EPS);
});
// --- phi/theta/psi Round-Trip (psi != 0) ---
// Prueft ob calculatePositionFromMotorAngles() auch die Orientierung
// (phi, theta, psi) korrekt zurueckrechnet, nicht nur x/y/z.
function roundTrip(L1, L2, L3, x, y, z, phi, theta, psi) {
const A = new Robot(L1, L2, L3);
A.x = x; A.y = y; A.z = z;
A.phi = phi; A.theta = theta; A.psi = psi;
A.calculateAngles3D();
const B = new Robot(L1, L2, L3);
B.xMotor = A.xMotor;
B.alpha = A.alpha;
B.beta = A.beta;
B.a = A.a;
B.b = A.b;
B.c = A.c;
B.calculatePositionFromMotorAngles();
return { A, B };
}
test("phi/theta/psi Round-Trip: psi = +45 Grad", () => {
const { A, B } = roundTrip(300, 200, 10, 10, 430, 30, Math.PI/7, Math.PI/2, Math.PI/4);
const EPS = 4; // 4 Dezimalstellen, ca. 0.1 mrad
expect(B.x).toBeCloseTo(A.x, EPS);
expect(B.y).toBeCloseTo(A.y, EPS);
expect(B.z).toBeCloseTo(A.z, EPS);
expect(B.phi).toBeCloseTo(A.phi, EPS);
expect(B.theta).toBeCloseTo(A.theta, EPS);
expect(B.psi).toBeCloseTo(A.psi, EPS);
});
test("phi/theta/psi Round-Trip: psi = -30 Grad", () => {
const { A, B } = roundTrip(300, 200, 10, 10, 430, 30, Math.PI/7, Math.PI/2, -Math.PI/6);
const EPS = 4;
expect(B.x).toBeCloseTo(A.x, EPS);
expect(B.y).toBeCloseTo(A.y, EPS);
expect(B.z).toBeCloseTo(A.z, EPS);
expect(B.phi).toBeCloseTo(A.phi, EPS);
expect(B.theta).toBeCloseTo(A.theta, EPS);
expect(B.psi).toBeCloseTo(A.psi, EPS);
});
test("phi/theta/psi Round-Trip: psi = +90 Grad, phi = -90 Grad", () => {
const { A, B } = roundTrip(300, 200, 10, 0, 450, 0, -Math.PI/2, Math.PI/2, Math.PI/2);
const EPS = 4;
expect(B.x).toBeCloseTo(A.x, EPS);
expect(B.y).toBeCloseTo(A.y, EPS);
expect(B.z).toBeCloseTo(A.z, EPS);
expect(B.phi).toBeCloseTo(A.phi, EPS);
expect(B.theta).toBeCloseTo(A.theta, EPS);
expect(B.psi).toBeCloseTo(A.psi, EPS);
});
// --- Null-Stellung und reale Positionen in -Y Richtung ---
// Der Roboter arbeitet mit y < 0 als Hauptrichtung.
// Null-Stellung: Arm voll ausgestreckt in -Y, Fingerspitze bei y=-(l1+l2+l3), z=0.
// Hand zeigt ebenfalls in -Y => Handvektor (Fingerspitze->Handgelenk) zeigt in +Y
// => phi = pi/2, theta = pi/2.
test("Null-Stellung: Arm voll ausgestreckt in -Y, y=-(L1+L2+L3), z=0", () => {
const L1 = 300, L2 = 200, L3 = 10;
const { A, B } = roundTrip(L1, L2, L3, 0, -(L1+L2+L3), 0, Math.PI/2, Math.PI/2, 0);
const EPS = 4;
expect(B.x).toBeCloseTo(A.x, EPS);
expect(B.y).toBeCloseTo(A.y, EPS);
expect(B.z).toBeCloseTo(A.z, EPS);
expect(B.phi).toBeCloseTo(A.phi, EPS);
expect(B.theta).toBeCloseTo(A.theta, EPS);
expect(B.psi).toBeCloseTo(A.psi, EPS);
});
test("Zwischenposition in -Y: Fingerspitze bei y=-400, z=100", () => {
const L1 = 300, L2 = 200, L3 = 10;
// Gleiche Handorientierung wie Null-Stellung: phi=pi/2, theta=pi/2
const { A, B } = roundTrip(L1, L2, L3, 0, -400, 100, Math.PI/2, Math.PI/2, 0);
const EPS = 4;
expect(B.x).toBeCloseTo(A.x, EPS);
expect(B.y).toBeCloseTo(A.y, EPS);
expect(B.z).toBeCloseTo(A.z, EPS);
expect(B.phi).toBeCloseTo(A.phi, EPS);
expect(B.theta).toBeCloseTo(A.theta, EPS);
expect(B.psi).toBeCloseTo(A.psi, EPS);
});
});

View File

@@ -24,9 +24,11 @@ const FULL_ROBOT_JSON = {
hand: { ip: 'fluidNcHand.local', port: 5000, protocol: 'telnet', axes: ['c', 'e', 'b'] }
},
links: {
Arm1: { skeleton: { from: [0,0,0], to: [0,-250,0] } },
Arm2: { skeleton: { from: [0,0,0], to: [0,-250,0] } },
Ellbow: { skeleton: { from: [0,0,0], to: [90,0,0] } }
Arm1: { skeleton: { from: [0,0,0], to: [0,-250,0] } },
Arm2: { skeleton: { from: [0,0,0], to: [0,-250,0] } },
Ellbow: { skeleton: { from: [0,0,0], to: [90,0,0] } },
Hand: { skeleton: { from: [0,0,0], to: [0,-35,0] } },
FingerA: { skeleton: { from: [0,0,0], to: [0,-60,0] } }
}
};
@@ -45,8 +47,16 @@ describe('RobotConfig.load — Vollständige robot.json', () => {
expect(cfg.kinematics.l2).toBe(250);
});
test('l3 aus links.Ellbow.skeleton.to[0]', () => {
expect(cfg.kinematics.l3).toBe(90);
test('l3 aus Hand + Finger (Handgelenk → Fingerspitze), NICHT Ellbow-Versatz', () => {
expect(cfg.kinematics.l3).toBe(95); // |Hand.to[1]|=35 + |FingerA.to[1]|=60
});
test('kinematics.l1/l2/l3 explizit in robot.json überschreiben die links-Ableitung', () => {
const json = { ...FULL_ROBOT_JSON, kinematics: { type: 'arm3segmentlinearx', l1: 260, l2: 270, l3: 50 } };
const c = load(makeFs(JSON.stringify(json)), {}, log);
expect(c.kinematics.l1).toBe(260);
expect(c.kinematics.l2).toBe(270);
expect(c.kinematics.l3).toBe(50);
});
test('motion.defaultFeedrate aus robot.json', () => {

View File

@@ -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);

View File

@@ -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(),