Compare commits
15 Commits
8a669f23d3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05f0dd619a | ||
|
|
81664ac0c9 | ||
|
|
0d146a48b0 | ||
|
|
f6a752cf58 | ||
|
|
549d10b9c0 | ||
|
|
2197a8954f | ||
|
|
933a017e2e | ||
|
|
7639266170 | ||
|
|
bd1752f567 | ||
|
|
7205b9d913 | ||
|
|
29b5f2ae4b | ||
|
|
497d0fbc7b | ||
|
|
b96a538b89 | ||
|
|
8deb7bb8a6 | ||
|
|
83cef32a37 |
89
EmergencyStopButton/EmergencyStopButton.ino
Normal file
89
EmergencyStopButton/EmergencyStopButton.ino
Normal 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.5–1 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);
|
||||
}
|
||||
}
|
||||
86
EmergencyStopButton/EmergencyStopButton.md
Normal file
86
EmergencyStopButton/EmergencyStopButton.md
Normal 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 1–2 mA — selbst ohne WLAN-Betrieb würde ein kleiner Akku in wenigen Tagen leer sein.
|
||||
|
||||
### DFRobot FireBeetle 2 — gewählt
|
||||
- Integrierter Laderegler + JST-PH-2.0-Anschluss direkt am Board
|
||||
- Low-Power optimiert (ab Werk ~15 µA im Deep Sleep)
|
||||
- Kein Zusatz-Hardware nötig für Akkubetrieb
|
||||
|
||||
## Akku-Spezifikation für den FireBeetle 2
|
||||
|
||||
Suchbegriffe:
|
||||
- `LiPo Akku 3.7V JST PH2.0 2000mAh Schutzschaltung`
|
||||
- `Li-Po 1S 3.7V protected JST-PH 2.0mm 2000mAh`
|
||||
|
||||
Pflichtmerkmale:
|
||||
| Merkmal | Wert |
|
||||
|---|---|
|
||||
| Typ | LiPo / Li-Polymer, **1S** (1 Zelle) |
|
||||
| Spannung | **3,7 V** nominal |
|
||||
| Stecker | **JST PH, 2,0 mm Raster, 2-polig** |
|
||||
| Schutzschaltung | **Ja** (BMS/PCM) |
|
||||
| Kapazität | **2000 mAh** |
|
||||
|
||||
> **Achtung:** Polarität vor dem Einstecken mit Multimeter prüfen — JST-PH-Stecker sind nicht normiert. Sicherste Option: Akku direkt bei DFRobot kaufen.
|
||||
|
||||
## Architektur-Entscheidung: WiFi Light Sleep (Priorität: 250 ms Latenz)
|
||||
|
||||
Die **250 ms Latenz** vom Knopfdruck bis zum API-Call ist das primäre Ziel.
|
||||
|
||||
| Option | Latenz | Ø Strom | Laufzeit (2000 mAh) |
|
||||
|---|---|---|---|
|
||||
| **WiFi Light Sleep (DTIM=10)** | **150–250 ms** ✅ | ~1 mA | **~80 Tage** |
|
||||
| Deep Sleep + Reconnect | 600–1300 ms ❌ | ~0,02 mA | ~mehrere Jahre |
|
||||
|
||||
Deep Sleep scheidet aus: Der WiFi-Reconnect nach dem Aufwachen dauert 600–1300 ms — die 250-ms-Anforderung wird klar verfehlt.
|
||||
|
||||
## WiFi Light Sleep — Funktionsprinzip
|
||||
|
||||
Die CPU schläft, der WiFi-Stack bleibt aktiv. Mit DTIM=10 wacht der ESP32 alle ~1000 ms für 1–2 ms auf, um gepufferte Pakete vom Router abzuholen. Die Verbindungsassoziation bleibt erhalten.
|
||||
|
||||
Ein **GPIO-Interrupt** (Leitung auf GND) weckt den ESP32 in **1–5 ms** — der API-Call kann sofort abgesetzt werden, weil WiFi bereits verbunden ist.
|
||||
|
||||
## Latenzbudget
|
||||
|
||||
| Schritt | Zeit |
|
||||
|---|---|
|
||||
| Wakeup aus Light Sleep | 1–5 ms |
|
||||
| WiFi-Verbindung prüfen (bereits aktiv) | 0 ms |
|
||||
| HTTP-Request aufbauen | 20–50 ms |
|
||||
| TLS-Handshake (HTTPS) | 50–150 ms |
|
||||
| Server-Antwort | 20–50 ms |
|
||||
| **Gesamt** | **~100–250 ms** ✅ |
|
||||
|
||||
## Akkulaufzeit (WiFi Light Sleep, 2000 mAh)
|
||||
|
||||
```
|
||||
Durchschnittsstrom (DTIM=10, Taster selten gedrückt): ~1 mA
|
||||
Nutzbare Kapazität (80 %): 1600 mAh
|
||||
Selbstentladung LiPo: ~2 mAh/Tag
|
||||
|
||||
Laufzeit ≈ 1600 mAh / 1 mA ≈ 1600 h ≈ 67–80 Tage
|
||||
```
|
||||
|
||||
> Zum Vergleich: Mit 1000 mAh (alter Stand) waren es ~40 Tage.
|
||||
|
||||
## GPIO Wake-Up — technische Details
|
||||
|
||||
- **Pegel-Trigger** (kein Flanken-Trigger): die Leitung muss >ein paar ms auf GND bleiben.
|
||||
- **Pull-Up intern** aktivieren: im Ruhezustand HIGH, Ereignis zieht auf GND.
|
||||
- **Wake-fähige Pins** sind nur RTC/LP-GPIOs — im FireBeetle-2-Datenblatt prüfen.
|
||||
- API: `esp_sleep_enable_gpio_wakeup()` / `esp_light_sleep_start()`
|
||||
|
||||
## Vergleich: Alternative Deep-Sleep-Architektur (nicht für E-Stop geeignet)
|
||||
|
||||
Falls in einem anderen Projekt Latenz < 1 s ausreicht und Akkulaufzeit Monate betragen soll:
|
||||
|
||||
- Deep Sleep, WLAN nur 8–18 Uhr alle 30 min (20 WLAN-Verbindungen/Tag)
|
||||
- Zusätzlich GPIO-Wake für Ereignisse (innerhalb ~300 ms nach Aufwachen + Reconnect)
|
||||
- Laufzeit 2000 mAh: **~8–10 Monate** (Selbstentladung dominant)
|
||||
|
||||
Für den Emergency Stop Button ist diese Option **nicht geeignet**, da die WiFi-Reconnect-Zeit die 250-ms-Anforderung überschreitet.
|
||||
28
EmergencyStopButton/eStopESP32.aux
Normal file
28
EmergencyStopButton/eStopESP32.aux
Normal 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}
|
||||
529
EmergencyStopButton/eStopESP32.log
Normal file
529
EmergencyStopButton/eStopESP32.log
Normal 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)
|
||||
|
||||
19
EmergencyStopButton/eStopESP32.out
Normal file
19
EmergencyStopButton/eStopESP32.out
Normal 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
|
||||
BIN
EmergencyStopButton/eStopESP32.pdf
Normal file
BIN
EmergencyStopButton/eStopESP32.pdf
Normal file
Binary file not shown.
BIN
EmergencyStopButton/eStopESP32.synctex.gz
Normal file
BIN
EmergencyStopButton/eStopESP32.synctex.gz
Normal file
Binary file not shown.
262
EmergencyStopButton/eStopESP32.tex
Normal file
262
EmergencyStopButton/eStopESP32.tex
Normal 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}
|
||||
20
EmergencyStopButton/eStopESP32.toc
Normal file
20
EmergencyStopButton/eStopESP32.toc
Normal 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}%
|
||||
13
README.md
13
README.md
@@ -2,6 +2,17 @@
|
||||
|
||||
Dieses Projekt empfängt G-Code und Robotersteuerbefehle, berechnet Inverse Kinematik für einen mehrgliedrigen Roboterarm und leitet die resultierenden Achsenbefehle an mehrere GRBL/FluidNC-Telnet-Sender weiter.
|
||||
|
||||
## Einbindung ins Projekt
|
||||
|
||||
Der Driver steht zwischen Eingabe-Programmen (appInput oder appAutomasiation oder jede beliebige andere Eingabeform)
|
||||
welche die Eingabe steuert. Die Eingabe wird eben an den Driver weitergegeben. Hier im Driver werden die Welt-Koordinaten
|
||||
in Motor-Koordinaten umgerechnet, es werden die Motoren angesteuert.
|
||||
|
||||
<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:
|
||||
@@ -28,7 +39,7 @@ Die Eingaben kommen per WebSocket an den HTTPS-Server und werden in `server/Inpu
|
||||
- Antwort: Positionsdaten des Roboters im JSON-Format.
|
||||
- G-Code-Befehle:
|
||||
- `G90`, `G91`, `G1`, `G28`
|
||||
- `G92` (wird intern als `M92` verarbeitet — setzt Motorposition ohne Bewegung)
|
||||
- `G92` setzt die Motorposition ohne Bewegung. Winkel (`Y`,`Z`,`A`,`B`,`C`) in **Grad** (G-Code-Konvention, wie FluidNC); `X` in mm; `E` = Greifer-Öffnung in mm (ab Null-Position eines Fingers), wird intern über die Greifer-Kopplung in `eMotor` umgerechnet. (`M92` macht dasselbe, erwartet die Winkel aber roh in **Radiant**.)
|
||||
- Messungen in `X`, `Y`, `Z`, `A`, `B`, `C`, `E`, `F`
|
||||
- `M1` für direkte Motor-Koordinaten
|
||||
- FCodes (Datei-/Programm-Befehle) — werden durch den Driver an `appRobotFileservice` weitergeleitet:
|
||||
|
||||
208
doc/Info_G92.md
Normal file
208
doc/Info_G92.md
Normal 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 = c−b → 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).
|
||||
248
doc/Info_Koordinaten.md
Normal file
248
doc/Info_Koordinaten.md
Normal file
@@ -0,0 +1,248 @@
|
||||
# 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.
|
||||
>
|
||||
> **Phase 2 B-Konvention: Entscheidung gefallen (2026-06-26).** Ein Versuch, gerade Hand
|
||||
> auf B=0 umzustellen, führte zu einem Hardware-Crash (G28 fuhr von B≈179 nach B=0 →
|
||||
> Hand schlug in den Arm). Die Konvention **bleibt bei b=π (180°) = gerade Hand**.
|
||||
> Der Homing-Export-Fehler (`180−b` statt `180+b`) ist in `appRobotHoming` behoben;
|
||||
> der Driver selbst ist korrekt und unverändert. Siehe
|
||||
> [`appRobotHoming/doc/Homing_8_G92_B_Flip.md`](../../appRobotHoming/doc/Homing_8_G92_B_Flip.md).
|
||||
>
|
||||
> Weiterhin offen: **C-Nullpunkt** und **Greifer-Kopplung**.
|
||||
|
||||
---
|
||||
|
||||
## 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) | **180°** | Hand gerade (b=π = gerade Hand, Hardware-verifiziert) | ✅ **endgültig** |
|
||||
| C (c) | 0° (Ziel) | kein Hand-Roll — **derzeit neutral ≠ 0** | ⏳ offen |
|
||||
| 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°** ✅ (endgültig) | b = 180° |
|
||||
| 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)
|
||||
|
||||
> **a-Achse ist korrekt** (a=0 → Knick-Achse ∥ x). Phase 2 betrifft
|
||||
> **B (Knick)**, **C (Roll)** und die **Greifer-Kopplung**.
|
||||
|
||||
**B-Konvention: ENTSCHIEDEN, KEINE ÄNDERUNG.** Die gerade Hand bleibt **b = 180° (π rad)**.
|
||||
Ein Umstellungs-Versuch auf B=0 wurde hardwaregetestet und hat zu einem Crash geführt (s.o.).
|
||||
Der Fehler lag ausschließlich im Homing-Export — **der Driver ist korrekt**.
|
||||
|
||||
**C und Greifer: offen.**
|
||||
|
||||
#### 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 = (c−b)·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 = e−b−c = −π → −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: ✅ ABGESCHLOSSEN / KEINE ÄNDERUNG.**
|
||||
Die gerade Hand ist und bleibt **b = 180° (π rad)**. Der Homing-Export-Bug
|
||||
(`180−b` → `180+b`) ist in `appRobotHoming/server/fkStateToDriverG92.cjs` behoben.
|
||||
Im Driver (`Arm3SegmentLinearX`, `RobotController`, `portInverse.js`) gibt es nichts
|
||||
zu ändern.
|
||||
|
||||
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 B (abgeschlossen):** B=180° = gerade Hand — endgültige Konvention, Hardware-
|
||||
verifiziert. Homing-Export korrigiert. Driver unverändert korrekt.
|
||||
- **Phase 2 C+Greifer (offen):** Grundstellung mit C=0; Greifer-Kopplung gegen echte Mechanik
|
||||
kalibriert; 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) → `robot.b = Math.PI` (gerade Hand), Finger sauber entlang −y.
|
||||
- **B-Konvention (endgültig):** `b = π (180°)` = gerade Hand. Hardware-verifiziert.
|
||||
Kein Umbau geplant. Homing-Export-Bug (`180−b` → `180+b`) in `appRobotHoming` behoben;
|
||||
Doku: [`appRobotHoming/doc/Homing_8_G92_B_Flip.md`](../../appRobotHoming/doc/Homing_8_G92_B_Flip.md).
|
||||
- **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
553
doc/Server_to_Robot.svg
Normal 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 |
321
doc/SoftwareModularisation.svg
Normal file
321
doc/SoftwareModularisation.svg
Normal 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">& 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 |
271
doc/ToDo_15_AccelerationSensor.md
Normal file
271
doc/ToDo_15_AccelerationSensor.md
Normal file
@@ -0,0 +1,271 @@
|
||||
# ToDo 15 — Beschleunigungssensor LIS3DH (Unterarm / Hand-Controller)
|
||||
|
||||
## Kontext
|
||||
|
||||
Der „Hand"-FluidNC-Controller (`fluidNcHand.local`) hat einen LIS3DH I²C-Sensor
|
||||
an Adresse `0x18` (dezimal 24). Die angepasste FluidNC-Firmware unterstützt M260/M261
|
||||
für I²C-Zugriff (siehe `FluidNC/docs/i2c-gcode-integration.md`).
|
||||
|
||||
Ziel: X/Y/Z-Beschleunigungsdaten periodisch auslesen, in der Info-Page anzeigen,
|
||||
und später den daraus berechneten Winkel mit dem Position-Driver-Winkel `b` vergleichen.
|
||||
|
||||
---
|
||||
|
||||
## Designentscheidung: WSSenderGrbl statt TelnetSenderGRBL
|
||||
|
||||
**Gewählt:** `WSSenderGrbl` (WebSocket, Port 81 = FluidNC WebUI-Protokoll).
|
||||
|
||||
**Begründung:**
|
||||
- Genau **eine** Verbindung pro Controller, die G-Code-Moves und Sensordaten gemeinsam
|
||||
trägt — kein zweiter paralleler Socket.
|
||||
- Die WebSocket-Verbindung empfängt dieselben Ausgaben wie Telnet: Status-Reports
|
||||
`<Idle|MPos:…>`, `ok`/`error`-Zeilen, und `log_info`-Nachrichten wie `I2CRESP`.
|
||||
- Der `grblState === 'Idle'`-Check (aus den Status-Reports) verhindert I²C-Polling
|
||||
während eines laufenden Moves — genau wie TelnetSenderGRBL es macht.
|
||||
|
||||
**Nicht gewählt:** TelnetSenderGRBL — hätte zwar weniger Umbauaufwand (Response-Parser
|
||||
existiert dort bereits), aber der Umbau von WSSenderGrbl ist sinnvoll, weil die
|
||||
Response-Infrastruktur dort früher oder später sowieso gebraucht wird.
|
||||
|
||||
**Hinweis:** FluidNC sendet auf Port 81 pro WebSocket-Frame eine vollständige Zeile
|
||||
(kein TCP-Fragmentierungsproblem). Das vereinfacht den Parser gegenüber Telnet.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Sensor auslesen (Backend)
|
||||
|
||||
### 1.1 Initialisierung des LIS3DH (einmalig nach Connect)
|
||||
|
||||
```
|
||||
M260 P24 Q32 R87 → CTRL_REG1 0x20 = 0x57 (100 Hz, alle drei Achsen aktiv)
|
||||
```
|
||||
|
||||
Muss gesendet werden, sobald der Hand-Controller verbunden ist (nach dem ersten Connect
|
||||
oder Reconnect). Ohne diesen Schritt liefern OUT_X/Y/Z nur Nullen.
|
||||
|
||||
### 1.2 Lesesequenz (wiederholt, z. B. alle 500 ms)
|
||||
|
||||
```
|
||||
M260 P24 Q168 → Registerzeiger auf 0xA8 (= OUT_X_L | 0x80 Auto-Increment)
|
||||
M261 P24 L6 → 6 Bytes lesen: X_L X_H Y_L Y_H Z_L Z_H
|
||||
```
|
||||
|
||||
Antwortformat in der WebSocket-Ausgabe (via FluidNC `log_info`):
|
||||
```
|
||||
I2CRESP B0 A0x18: C0 01 40 F8 80 3F
|
||||
```
|
||||
|
||||
### 1.3 Byte → g-Wert Umrechnung (JavaScript)
|
||||
|
||||
```js
|
||||
function parseLIS3DH(hexBytes) { // z. B. ['C0','01','40','F8','80','3F']
|
||||
function toSigned16(lo, hi) {
|
||||
const raw = (parseInt(hi, 16) << 8) | parseInt(lo, 16);
|
||||
return raw > 32767 ? raw - 65536 : raw;
|
||||
}
|
||||
const x = toSigned16(hexBytes[0], hexBytes[1]) >> 4; // 12-bit rechtsbündig
|
||||
const y = toSigned16(hexBytes[2], hexBytes[3]) >> 4;
|
||||
const z = toSigned16(hexBytes[4], hexBytes[5]) >> 4;
|
||||
// ±2 g Bereich: 1 g ≈ 1024 LSB
|
||||
return { x: x / 1024, y: y / 1024, z: z / 1024 };
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Winkelberechnung (später, wenn Achsenorientierung bekannt)
|
||||
|
||||
Wenn Schwerkraft = einzige Beschleunigung (Arm ruhig):
|
||||
|
||||
```js
|
||||
const roll = Math.atan2(y, z); // Rotation um X-Achse
|
||||
const pitch = Math.atan2(-x, Math.hypot(y, z)); // Rotation um Y-Achse
|
||||
```
|
||||
|
||||
Welche Winkelachse dem Motor `b` entspricht, muss durch Ausprobieren (Arm drehen,
|
||||
Sensor beobachten) bestimmt werden.
|
||||
|
||||
Vergleich mit Position Driver:
|
||||
- Sensorwinkel (rad) ↔ `robot.b` (rad, Konvention: gerade Hand = π)
|
||||
- Differenz = Schlupf / Kalibrierungsfehler
|
||||
|
||||
---
|
||||
|
||||
## ToDo-Liste: Programmänderungen
|
||||
|
||||
### A — `robot/WSSenderGrbl.js` — Response-Infrastruktur ✅ erledigt
|
||||
|
||||
**A1 — EventEmitter einbinden** ✅
|
||||
- `SenderInterface` erbt jetzt von `EventEmitter` → WSSenderGrbl und TelnetSenderGRBL
|
||||
bekommen die Fähigkeit automatisch, ohne eigene `class`-Zeile zu ändern.
|
||||
|
||||
**A2 — Eingehende Nachrichten empfangen** ✅
|
||||
- `ws.on('message', (data) => this._handleMessage(data))` in `_tryConnect()`
|
||||
- `_handleMessage(data)`: trimmt, verwirft Leerzeilen, ruft `_handleResponseLine()`
|
||||
|
||||
**A3 — Response-Zeilen auswerten** ✅
|
||||
- `_handleResponseLine(line)`: dispatcht auf `_parseStatusReport`, `_handleI2CResp`,
|
||||
oder setzt `lastError` + emittiert `'error-report'`
|
||||
- Neue Felder im Konstruktor: `grblState`, `machinePosition`, `lastReportAt`, `lastError`,
|
||||
`_i2cRespResolve`, `_i2cRespReject`
|
||||
|
||||
**A4 — Status-Reports parsen** ✅
|
||||
- `_parseStatusReport(line)`: extrahiert `grblState` + `machinePosition`, setzt
|
||||
`lastReportAt`, emittiert `'status'`
|
||||
- `getStatus()` gibt jetzt auch `grblState`, `machinePosition`, `lastReportAt`,
|
||||
`lastError` zurück
|
||||
|
||||
**A5 — Heartbeat (Idle-Erkennung)** ✅
|
||||
- `_startHeartbeat()` / `_stopHeartbeat()` via injizierbare `setIntervalFn` /
|
||||
`clearIntervalFn`; sendet `?` im Intervall (default 10 s)
|
||||
- Startet in `ws.on('open')`, stoppt in `ws.on('close')` und `disconnect()`
|
||||
|
||||
**Unit-Tests:** `test/Sender.WS.responseParsing.test.js` — alle Tests grün.
|
||||
|
||||
---
|
||||
|
||||
### B — `robot/WSSenderGrbl.js` — I²C / LIS3DH
|
||||
|
||||
(Baut auf Abschnitt A auf; `grblState` muss verfügbar sein.)
|
||||
|
||||
**B1 — I2CRESP-Handler**
|
||||
- [ ] Neue Felder:
|
||||
```js
|
||||
this._i2cRespResolve = null;
|
||||
this._i2cRespReject = null;
|
||||
```
|
||||
- [ ] Methode `_handleI2CResp(line)`:
|
||||
- Bytes-String extrahieren (nach dem letzten `:`)
|
||||
- als Array von Hex-Strings splitten
|
||||
- `_i2cRespResolve(bytes)` aufrufen, Felder auf `null` zurücksetzen
|
||||
|
||||
**B2 — Async-Helfer**
|
||||
- [ ] Methode `async _waitI2CResp(timeoutMs = 500)` → Promise:
|
||||
- setzt `_i2cRespResolve` / `_i2cRespReject`
|
||||
- Timeout löst mit `null` auf (kein Fehler-Throw, damit Polling weiterläuft)
|
||||
- [ ] Methode `async sendAndWaitI2CResp(gcode, timeoutMs = 500)`:
|
||||
- prüft `grblState === 'Idle'` → bei nicht-Idle: return `null`
|
||||
- startet `_waitI2CResp()`
|
||||
- sendet `gcode` via `this.send(gcode)`
|
||||
- `await`-et das Promise, gibt Bytes-Array zurück
|
||||
|
||||
**B3 — Sensor-Methoden**
|
||||
- [ ] Methode `async initLIS3DH()`:
|
||||
```
|
||||
M260 P24 Q32 R87
|
||||
```
|
||||
Sendet Konfigurationsbefehl, wartet auf `ok` (oder kurzes Delay).
|
||||
- [ ] Methode `async readLIS3DH()` → `{ x, y, z }` oder `null`:
|
||||
1. `M260 P24 Q168` senden (Registerzeiger), auf `ok` warten
|
||||
2. `M261 P24 L6` senden, auf `I2CRESP` warten
|
||||
3. Bytes mit `parseLIS3DH()` umrechnen
|
||||
|
||||
**B4 — Polling-Loop**
|
||||
- [ ] Methode `startAccelerometerPolling(intervalMs = 1000)`:
|
||||
- ruft `initLIS3DH()` einmalig auf
|
||||
- startet `setInterval` mit `readLIS3DH()`
|
||||
- speichert Ergebnis in `this.accelerometer = { x, y, z, timestamp }`
|
||||
- `emit('accelerometer', this.accelerometer)`
|
||||
- [ ] Methode `stopAccelerometerPolling()`: löscht Interval
|
||||
- [ ] `startAccelerometerPolling()` in `ws.on('open', …)` nach `_startHeartbeat()` aufrufen,
|
||||
nur wenn `this.controllerRole === 'hand'`
|
||||
- [ ] Bei `ws.on('close', …)`: `stopAccelerometerPolling()` aufrufen
|
||||
|
||||
---
|
||||
|
||||
### C — `startRobot.js` — Hand-Controller auf WSSenderGrbl umstellen
|
||||
|
||||
Aktuell verwendet `startRobot.js` für alle Controller denselben `TelnetSenderClass`.
|
||||
|
||||
- [ ] `WSSenderGrbl` importieren:
|
||||
```js
|
||||
const WSSender = require('./robot/WSSenderGrbl');
|
||||
```
|
||||
- [ ] Beim Erstellen der Sender-Instanzen: für den Hand-Controller `WSSenderGrbl`
|
||||
verwenden, für Base und Elbow weiterhin `TelnetSenderClass`:
|
||||
```js
|
||||
const SenderClass = key === 'hand' ? WSSender : TelnetSenderClass;
|
||||
const instance = new SenderClass(ctrl.ip, ctrl.port, ...axes7, { … });
|
||||
```
|
||||
(WSSenderGrbl ignoriert `port` im Konstruktor und nutzt immer Port 81 — ggf.
|
||||
`wsPort` als separates Option-Feld in `robot.json` ergänzen.)
|
||||
- [ ] Alternativ: `WSSenderClass` als injizierbaren Parameter in `startRobot()` ergänzen
|
||||
(analog zu `TelnetSenderClass`), damit Tests mocken können.
|
||||
|
||||
---
|
||||
|
||||
### D — `robot/AccelerometerService.js` (neue Datei)
|
||||
|
||||
- [ ] Klasse `AccelerometerService` anlegen
|
||||
- [ ] Konstruktor nimmt `handSender` (WSSenderGrbl-Instanz)
|
||||
- [ ] `start()`: lauscht auf `handSender.on('accelerometer', …)` und speichert Daten
|
||||
- [ ] `getLatest()`: gibt `{ x, y, z, timestamp }` zurück (oder `null`)
|
||||
- [ ] (Phase 2) `getAngle()`: berechnet Roll/Pitch, gibt `{ roll, pitch }` zurück
|
||||
|
||||
---
|
||||
|
||||
### E — `server/InfoServer.js`
|
||||
|
||||
- [ ] `AccelerometerService` importieren, Instanz mit Hand-Sender erzeugen, `start()` aufrufen
|
||||
- [ ] Neuer Endpunkt `GET /api/acceleration`:
|
||||
```js
|
||||
app.get('/api/acceleration', (req, res) => {
|
||||
res.json(accelerometerService.getLatest() ?? { x: null, y: null, z: null });
|
||||
});
|
||||
```
|
||||
- [ ] (Phase 2) Endpunkt um `angle` erweitern
|
||||
|
||||
---
|
||||
|
||||
### F — `public/index.html`
|
||||
|
||||
- [ ] Neuen Abschnitt **Acceleration** in der Info-Page einfügen (nach „Position Driver"):
|
||||
```html
|
||||
<div class="section">
|
||||
<h3>Acceleration (LIS3DH)</h3>
|
||||
<table>
|
||||
<tr><td>X</td><td id="acc-x">—</td><td>g</td></tr>
|
||||
<tr><td>Y</td><td id="acc-y">—</td><td>g</td></tr>
|
||||
<tr><td>Z</td><td id="acc-z">—</td><td>g</td></tr>
|
||||
</table>
|
||||
<!-- Phase 2 -->
|
||||
<table id="acc-angle-table" style="display:none">
|
||||
<tr><td>Winkel (Sensor)</td><td id="acc-angle">—</td><td>°</td></tr>
|
||||
<tr><td>Winkel (Driver b)</td><td id="driver-b">—</td><td>°</td></tr>
|
||||
<tr><td>Differenz</td><td id="acc-diff">—</td><td>°</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### G — `public/app.js`
|
||||
|
||||
- [ ] Neue Funktion `updateAcceleration()`:
|
||||
- `fetch('/api/acceleration')` → JSON
|
||||
- Felder `#acc-x`, `#acc-y`, `#acc-z` setzen (auf 3 Dezimalstellen)
|
||||
- Fehlerfall: Felder auf `—` setzen
|
||||
- [ ] `updateAcceleration()` in den 1-Sekunden-Poll-Zyklus aufnehmen
|
||||
(neben `updateStatus()`, `updatePosition()`)
|
||||
- [ ] (Phase 2) Winkel-Zeilen einblenden und befüllen, sobald Achse bekannt
|
||||
|
||||
---
|
||||
|
||||
## Offene Fragen / nächste Schritte
|
||||
|
||||
1. **WebSocket-Nachrichtenformat**: Verifizieren, dass FluidNC Port 81 `log_info`-Nachrichten
|
||||
(`I2CRESP …`) als plain-text WebSocket-Frames sendet (und nicht JSON-gewrappt).
|
||||
→ Testbefehl: `M260` (Scan) manuell über FluidNC WebUI senden und WS-Traffic beobachten.
|
||||
|
||||
2. **Achsenorientierung**: Welche LIS3DH-Achse (x/y/z) entspricht dem Motor-`b`-Winkel?
|
||||
→ Arm langsam kippen, Sensorwerte in der Info-Page beobachten.
|
||||
|
||||
3. **Polling-Konflikt**: I²C-Befehle dürfen nicht während eines Moves gesendet werden.
|
||||
Prüfen ob `grblState === 'Idle'` ausreicht, oder ob ein Queue-Mechanismus nötig ist.
|
||||
|
||||
4. **`ok`-Synchronisation**: Für `initLIS3DH()` und den Zeiger-Befehl vor dem Lesen wird
|
||||
ein `ok`-Wait benötigt. Prüfen, ob ein einfaches `await delay(50)` reicht oder ob
|
||||
ein echter `ok`-Promise nötig ist (dann Abschnitt A3 erweitern).
|
||||
|
||||
5. **Kalibrierung**: Nullpunkt und Skalierung des Sensors ggf. gegen bekannte Positionen
|
||||
abgleichen (Arm senkrecht = b=0, Arm waagrecht = b=π/2 usw.).
|
||||
@@ -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
|
||||
@@ -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 54–57
|
||||
|
||||
**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 66–67 und 77–78
|
||||
|
||||
**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.
|
||||
@@ -10734,3 +10734,739 @@
|
||||
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
|
||||
2026-06-26T18:38:26.674Z ::ffff:127.0.0.1: FList
|
||||
2026-06-26T18:38:26.718Z ::ffff:127.0.0.1: FPlus
|
||||
2026-06-26T18:38:26.751Z ::ffff:127.0.0.1: FLoad nichtda
|
||||
2026-06-26T18:38:26.778Z ::ffff:127.0.0.1: FShow
|
||||
2026-06-26T18:38:26.909Z ::ffff:127.0.0.1: M114
|
||||
2026-06-26T18:38:26.979Z ::ffff:127.0.0.1: M114
|
||||
2026-06-26T18:38:26.991Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-26T18:38:27.132Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-26T18:38:27.362Z ::ffff:127.0.0.1: G1 X1
|
||||
2026-06-26T21:14:13.178Z ::ffff:127.0.0.1: FList
|
||||
2026-06-26T21:14:13.213Z ::ffff:127.0.0.1: FPlus
|
||||
2026-06-26T21:14:13.241Z ::ffff:127.0.0.1: FLoad nichtda
|
||||
2026-06-26T21:14:13.261Z ::ffff:127.0.0.1: FShow
|
||||
2026-06-26T21:14:13.320Z ::ffff:127.0.0.1: M114
|
||||
2026-06-26T21:14:13.562Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-26T21:14:13.602Z ::ffff:127.0.0.1: M114
|
||||
2026-06-26T21:14:13.620Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-26T21:14:13.799Z ::ffff:127.0.0.1: G1 X1
|
||||
|
||||
164
logs/pings.log
164
logs/pings.log
@@ -14746,3 +14746,167 @@
|
||||
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
|
||||
2026-06-26T18:38:26.680Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-26T18:38:26.965Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-26T21:14:13.068Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-26T21:14:13.575Z ::ffff:127.0.0.1 : Ping
|
||||
|
||||
@@ -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);
|
||||
|
||||
})
|
||||
|
||||
@@ -79,15 +79,31 @@ async function _req(method, path, body) {
|
||||
opts.headers = { 'content-type': 'application/json' };
|
||||
opts.body = JSON.stringify(body);
|
||||
}
|
||||
const res = await fetch(url, opts);
|
||||
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;
|
||||
}
|
||||
return res.json();
|
||||
|
||||
const data = await res.json();
|
||||
console.log(`[FCode] ← ${res.status} ${method} ${path} ${JSON.stringify(data).slice(0, 160)}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
module.exports = { isFCode, handle };
|
||||
|
||||
@@ -40,20 +40,25 @@ class GCode{
|
||||
|
||||
|
||||
static getM114(robot){
|
||||
// position = Workspace (x/y/z in mm, a/b/c als Euler-Winkel phi/theta/psi in rad,
|
||||
// e = Greifer-Öffnung in mm).
|
||||
// motorCounts = die 7 Motor-Slots, inkl. e = eMotor (abgeleiteter Greifer-Motorwert,
|
||||
// NICHT die mm-Öffnung — die steht in position.e).
|
||||
let text = '{"position":{ "x":'+robot.x+
|
||||
', "y":'+robot.y+
|
||||
', "z":'+robot.z+
|
||||
', "a":' +robot.phi +
|
||||
', "b":' +robot.theta +
|
||||
', "c":' +robot.psi + '},' +
|
||||
', "a":' +robot.phi +
|
||||
', "b":' +robot.theta +
|
||||
', "c":' +robot.psi +
|
||||
', "e":' +(robot.e ?? 0) + '},' +
|
||||
'"motorCounts":{ "x":'+ robot.xMotor +
|
||||
', "y":'+ robot.alpha +
|
||||
', "y":'+ robot.alpha +
|
||||
', "z":'+ robot.beta +
|
||||
', "a":'+ robot.a +
|
||||
', "b":'+ robot.b +
|
||||
', "a":'+ robot.a +
|
||||
', "b":'+ robot.b +
|
||||
', "c":'+ robot.c +
|
||||
', "e":'+ (robot.e ?? 0) +
|
||||
'}}';
|
||||
', "e":'+ (robot.eMotor ?? 0) +
|
||||
'}}';
|
||||
return text;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
class SenderInterface {
|
||||
const EventEmitter = require('events');
|
||||
|
||||
class SenderInterface extends EventEmitter {
|
||||
async connect() {
|
||||
throw new Error('connect() must be implemented by sender classes');
|
||||
}
|
||||
|
||||
@@ -30,11 +30,22 @@ module.exports = class WSSenderGrbl extends SenderInterface {
|
||||
this.WebSocketClass = options.WebSocketClass || WebSocket;
|
||||
this.setTimeoutFn = options.setTimeoutFn || setTimeout;
|
||||
this.clearTimeoutFn = options.clearTimeoutFn || clearTimeout;
|
||||
this.setIntervalFn = options.setIntervalFn || setInterval;
|
||||
this.clearIntervalFn = options.clearIntervalFn || clearInterval;
|
||||
this.reconnectDelay = Number.isFinite(options.reconnectDelay) ? options.reconnectDelay : 2000;
|
||||
this.maxReconnectDelay = Number.isFinite(options.maxReconnectDelay) ? options.maxReconnectDelay : 30000;
|
||||
this.heartbeatInterval = Number.isFinite(options.heartbeatInterval) ? options.heartbeatInterval : 10000;
|
||||
this.wsPort = options.wsPort || 81;
|
||||
this.autoConnect = options.autoConnect !== false;
|
||||
|
||||
this._heartbeatTimer = null;
|
||||
this.grblState = null;
|
||||
this.machinePosition = null;
|
||||
this.lastReportAt = null;
|
||||
this.lastError = null;
|
||||
this._i2cRespResolve = null;
|
||||
this._i2cRespReject = null;
|
||||
|
||||
if (urlGRBL === "test.test") {
|
||||
this.isTestMode = true;
|
||||
this.state = 'connected';
|
||||
@@ -105,10 +116,12 @@ module.exports = class WSSenderGrbl extends SenderInterface {
|
||||
this.connectRejecter = null;
|
||||
}
|
||||
this.connectPromise = null;
|
||||
this._startHeartbeat();
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
console.log("WS Closed " + this.urlGRBLstr);
|
||||
this._stopHeartbeat();
|
||||
this.ws = null;
|
||||
if (this.shouldReconnect) {
|
||||
this.state = 'reconnecting';
|
||||
@@ -131,6 +144,8 @@ module.exports = class WSSenderGrbl extends SenderInterface {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('message', (data) => this._handleMessage(data));
|
||||
}
|
||||
|
||||
_scheduleReconnect() {
|
||||
@@ -161,11 +176,16 @@ module.exports = class WSSenderGrbl extends SenderInterface {
|
||||
error: this.error,
|
||||
isTestMode: !!this.isTestMode,
|
||||
reconnectAttempt: this.reconnectAttempt,
|
||||
reconnectTimer: !!this.reconnectTimer
|
||||
reconnectTimer: !!this.reconnectTimer,
|
||||
grblState: this.grblState,
|
||||
machinePosition: this.machinePosition,
|
||||
lastReportAt: this.lastReportAt,
|
||||
lastError: this.lastError,
|
||||
};
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this._stopHeartbeat();
|
||||
this.shouldReconnect = false;
|
||||
|
||||
if (this.reconnectTimer) {
|
||||
@@ -189,6 +209,59 @@ module.exports = class WSSenderGrbl extends SenderInterface {
|
||||
this.error = null;
|
||||
}
|
||||
|
||||
_handleMessage(data) {
|
||||
const line = data.toString().trim();
|
||||
if (line) this._handleResponseLine(line);
|
||||
}
|
||||
|
||||
_handleResponseLine(line) {
|
||||
if (line.startsWith('<')) {
|
||||
this._parseStatusReport(line);
|
||||
} else if (line.startsWith('I2CRESP')) {
|
||||
this._handleI2CResp(line);
|
||||
} else if (line.startsWith('error:') || line.startsWith('ALARM:')) {
|
||||
this.lastError = line;
|
||||
this.emit('error-report', line);
|
||||
}
|
||||
}
|
||||
|
||||
_parseStatusReport(line) {
|
||||
// <Idle|MPos:0.00,0.00,0.00|Bf:15,128>
|
||||
const stateMatch = line.match(/^<([^|>]+)/);
|
||||
if (stateMatch) this.grblState = stateMatch[1];
|
||||
|
||||
const mposMatch = line.match(/MPos:([-\d.,]+)/);
|
||||
if (mposMatch) this.machinePosition = mposMatch[1].split(',').map(Number);
|
||||
|
||||
this.lastReportAt = Date.now();
|
||||
this.emit('status', { grblState: this.grblState, machinePosition: this.machinePosition });
|
||||
}
|
||||
|
||||
_handleI2CResp(line) {
|
||||
// I2CRESP B0 A0x18: C0 01 40 F8 80 3F
|
||||
const colonIdx = line.lastIndexOf(':');
|
||||
if (colonIdx === -1) return;
|
||||
const bytes = line.slice(colonIdx + 1).trim().split(/\s+/);
|
||||
if (this._i2cRespResolve) {
|
||||
this._i2cRespResolve(bytes);
|
||||
this._i2cRespResolve = null;
|
||||
this._i2cRespReject = null;
|
||||
}
|
||||
this.emit('i2cresp', bytes);
|
||||
}
|
||||
|
||||
_startHeartbeat() {
|
||||
this._stopHeartbeat();
|
||||
this._heartbeatTimer = this.setIntervalFn(() => this.send('?'), this.heartbeatInterval);
|
||||
}
|
||||
|
||||
_stopHeartbeat() {
|
||||
if (this._heartbeatTimer) {
|
||||
this.clearIntervalFn(this._heartbeatTimer);
|
||||
this._heartbeatTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
moveTo(mOld, mNew) {
|
||||
this.execCommand("G1", mOld, mNew);
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -76,10 +76,12 @@ function initInputWS(server, robot, GCode, sharedState) {
|
||||
* 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);
|
||||
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);
|
||||
}
|
||||
@@ -88,7 +90,10 @@ function initInputWS(server, robot, GCode, sharedState) {
|
||||
broadcast(wss, result.data);
|
||||
}
|
||||
})
|
||||
.catch(err => sendError(ws, 'FILE_ERROR', err.message, message));
|
||||
.catch(err => {
|
||||
console.error("📁 FCode FEHLER (" + message + "): " + err.message);
|
||||
sendError(ws, err.code || 'FILE_ERROR', err.message, message);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -113,5 +113,50 @@ 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);
|
||||
});
|
||||
|
||||
test("G92 E: Greifer-Port (y) geht real an den Hand-Controller raus (nicht nur ins Modell)", () => {
|
||||
// Regression: G92 mit E muss nicht nur robot.eMotor füllen, sondern den Greifer-Port
|
||||
// auch tatsächlich an FluidNC senden (Kalibrierung nach dem Foto-Homing). Hand-
|
||||
// Verkabelung x=c, y=e, z=b → Greifer liegt auf dem y-Port, gesendet wird eMotor·(180/π).
|
||||
// Schützt davor, dass das E-Senden bei G92 still deaktiviert wird (war zeitweise vermutet).
|
||||
const robot = new Robot(300, 300, 20);
|
||||
const D = 180 / Math.PI;
|
||||
|
||||
const base = new TenetSender("test.test", 2300, "x", "y", "z");
|
||||
const elbow = new TenetSender("test.test", 5000, "a", null, null);
|
||||
const hand = new TenetSender("test.test", 5000, "c", "e", "b");
|
||||
robot.cmdReceivers.push(base, elbow, hand);
|
||||
|
||||
GCode.receiveGCode(robot, "G92 B30 C-45 E10");
|
||||
|
||||
const handSent = hand.tSocket.written;
|
||||
|
||||
// 1) Greifer-Port muss überhaupt rausgehen (sonst wäre das E-Senden deaktiviert).
|
||||
const yMatch = handSent.match(/ y(-?\d+\.\d+)/);
|
||||
expect(yMatch).not.toBeNull();
|
||||
|
||||
// 2) Wert = eMotor·D (= (e − b − c)·D), konsistent mit dem G1/Gamepad-Pfad.
|
||||
expect(parseFloat(yMatch[1])).toBeCloseTo(robot.eMotor * D, 1);
|
||||
|
||||
// 3) Es ist ein Setz-Befehl (G92), keine Bewegung (G1).
|
||||
expect(handSent.startsWith("G92")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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 e−b−c-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)
|
||||
})
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -81,7 +81,7 @@ describe('InputWS FCode-Routing', () => {
|
||||
expect(robot.sendCommand).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('Fileservice-Fehler → machine-readable FILE_ERROR an Sender', async () => {
|
||||
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();
|
||||
@@ -89,7 +89,17 @@ describe('InputWS FCode-Routing', () => {
|
||||
ws.send('FLoad nichtda');
|
||||
const msg = JSON.parse(await replyP);
|
||||
expect(msg.type).toBe('error');
|
||||
expect(msg.code).toBe('FILE_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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
107
test/Robot.Kinematics.NegativeY.test.js
Normal file
107
test/Robot.Kinematics.NegativeY.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
284
test/Sender.WS.responseParsing.test.js
Normal file
284
test/Sender.WS.responseParsing.test.js
Normal file
@@ -0,0 +1,284 @@
|
||||
const WSSenderGrbl = require('../robot/WSSenderGrbl');
|
||||
const EventEmitter = require('events');
|
||||
|
||||
// Minimal mock WebSocket whose events can be triggered manually
|
||||
class MockWS extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this.readyState = 1;
|
||||
this.sent = [];
|
||||
}
|
||||
send(data) { this.sent.push(data); }
|
||||
close() { this.readyState = 3; this.emit('close'); }
|
||||
}
|
||||
|
||||
// Creates a sender with a controllable WS connection (not test-mode shortcut)
|
||||
function makeConnectedSender(options = {}) {
|
||||
let mockWs;
|
||||
const WebSocketClass = function () {
|
||||
mockWs = new MockWS();
|
||||
return mockWs;
|
||||
};
|
||||
const sender = new WSSenderGrbl('robot.local', 2300, 'x', 'y', 'z', null, null, null, null, {
|
||||
WebSocketClass,
|
||||
autoConnect: false,
|
||||
setIntervalFn: options.setIntervalFn || jest.fn(() => 99),
|
||||
clearIntervalFn: options.clearIntervalFn || jest.fn(),
|
||||
setTimeoutFn: jest.fn(),
|
||||
clearTimeoutFn: jest.fn(),
|
||||
...options,
|
||||
});
|
||||
sender._tryConnect();
|
||||
mockWs.emit('open');
|
||||
return { sender, mockWs };
|
||||
}
|
||||
|
||||
// ─── Status-Report Parsing ────────────────────────────────────────────────────
|
||||
|
||||
describe('WSSenderGrbl._parseStatusReport (via _handleMessage)', () => {
|
||||
let sender;
|
||||
beforeEach(() => { sender = new WSSenderGrbl('test.test', 2300, 'x', 'y', 'z'); });
|
||||
|
||||
test('sets grblState', () => {
|
||||
sender._handleMessage('<Idle|MPos:1.00,2.00,3.00|Bf:15,128>');
|
||||
expect(sender.grblState).toBe('Idle');
|
||||
});
|
||||
|
||||
test('sets grblState for Run', () => {
|
||||
sender._handleMessage('<Run|MPos:0.00,0.00,0.00>');
|
||||
expect(sender.grblState).toBe('Run');
|
||||
});
|
||||
|
||||
test('sets machinePosition from MPos', () => {
|
||||
sender._handleMessage('<Idle|MPos:10.50,-3.25,0.75>');
|
||||
expect(sender.machinePosition).toEqual([10.50, -3.25, 0.75]);
|
||||
});
|
||||
|
||||
test('sets lastReportAt to current timestamp', () => {
|
||||
const before = Date.now();
|
||||
sender._handleMessage('<Idle|MPos:0.00,0.00,0.00>');
|
||||
expect(sender.lastReportAt).toBeGreaterThanOrEqual(before);
|
||||
expect(sender.lastReportAt).toBeLessThanOrEqual(Date.now());
|
||||
});
|
||||
|
||||
test('emits status event with grblState and machinePosition', () => {
|
||||
const listener = jest.fn();
|
||||
sender.on('status', listener);
|
||||
sender._handleMessage('<Idle|MPos:1.00,2.00,3.00>');
|
||||
expect(listener).toHaveBeenCalledWith({
|
||||
grblState: 'Idle',
|
||||
machinePosition: [1.00, 2.00, 3.00],
|
||||
});
|
||||
});
|
||||
|
||||
test('getStatus reflects parsed values', () => {
|
||||
sender._handleMessage('<Alarm|MPos:5.00,6.00,7.00>');
|
||||
const s = sender.getStatus();
|
||||
expect(s.grblState).toBe('Alarm');
|
||||
expect(s.machinePosition).toEqual([5.00, 6.00, 7.00]);
|
||||
expect(s.lastReportAt).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Error / Alarm ────────────────────────────────────────────────────────────
|
||||
|
||||
describe('WSSenderGrbl error and alarm handling', () => {
|
||||
let sender;
|
||||
beforeEach(() => { sender = new WSSenderGrbl('test.test', 2300, 'x', 'y', 'z'); });
|
||||
|
||||
test('sets lastError on error: line', () => {
|
||||
sender._handleMessage('error:15');
|
||||
expect(sender.lastError).toBe('error:15');
|
||||
});
|
||||
|
||||
test('sets lastError on ALARM: line', () => {
|
||||
sender._handleMessage('ALARM:1');
|
||||
expect(sender.lastError).toBe('ALARM:1');
|
||||
});
|
||||
|
||||
test('emits error-report event for error:', () => {
|
||||
const listener = jest.fn();
|
||||
sender.on('error-report', listener);
|
||||
sender._handleMessage('error:15');
|
||||
expect(listener).toHaveBeenCalledWith('error:15');
|
||||
});
|
||||
|
||||
test('emits error-report event for ALARM:', () => {
|
||||
const listener = jest.fn();
|
||||
sender.on('error-report', listener);
|
||||
sender._handleMessage('ALARM:1');
|
||||
expect(listener).toHaveBeenCalledWith('ALARM:1');
|
||||
});
|
||||
|
||||
test('getStatus reflects lastError', () => {
|
||||
sender._handleMessage('error:22');
|
||||
expect(sender.getStatus().lastError).toBe('error:22');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── I2CRESP Handling ─────────────────────────────────────────────────────────
|
||||
|
||||
describe('WSSenderGrbl._handleI2CResp (via _handleMessage)', () => {
|
||||
let sender;
|
||||
beforeEach(() => { sender = new WSSenderGrbl('test.test', 2300, 'x', 'y', 'z'); });
|
||||
|
||||
test('emits i2cresp with parsed byte array', () => {
|
||||
const listener = jest.fn();
|
||||
sender.on('i2cresp', listener);
|
||||
sender._handleMessage('I2CRESP B0 A0x18: C0 01 40 F8 80 3F');
|
||||
expect(listener).toHaveBeenCalledWith(['C0', '01', '40', 'F8', '80', '3F']);
|
||||
});
|
||||
|
||||
test('resolves pending _i2cRespResolve and clears it', () => {
|
||||
const resolve = jest.fn();
|
||||
sender._i2cRespResolve = resolve;
|
||||
sender._i2cRespReject = jest.fn();
|
||||
sender._handleMessage('I2CRESP B0 A0x18: C0 01 40 F8 80 3F');
|
||||
expect(resolve).toHaveBeenCalledWith(['C0', '01', '40', 'F8', '80', '3F']);
|
||||
expect(sender._i2cRespResolve).toBeNull();
|
||||
expect(sender._i2cRespReject).toBeNull();
|
||||
});
|
||||
|
||||
test('WROTE response (no colon) does not emit i2cresp', () => {
|
||||
const listener = jest.fn();
|
||||
sender.on('i2cresp', listener);
|
||||
sender._handleMessage('I2CRESP B0 A0x18 WROTE 2');
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('WROTE response does not call pending resolve', () => {
|
||||
const resolve = jest.fn();
|
||||
sender._i2cRespResolve = resolve;
|
||||
sender._handleMessage('I2CRESP B0 A0x18 WROTE 2');
|
||||
expect(resolve).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Initial getStatus fields ─────────────────────────────────────────────────
|
||||
|
||||
describe('WSSenderGrbl getStatus new fields', () => {
|
||||
test('new fields are null initially', () => {
|
||||
const sender = new WSSenderGrbl('test.test', 2300, 'x', 'y', 'z');
|
||||
const s = sender.getStatus();
|
||||
expect(s.grblState).toBeNull();
|
||||
expect(s.machinePosition).toBeNull();
|
||||
expect(s.lastReportAt).toBeNull();
|
||||
expect(s.lastError).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Heartbeat ────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('WSSenderGrbl heartbeat', () => {
|
||||
test('_startHeartbeat registers interval with correct delay', () => {
|
||||
const setIntervalFn = jest.fn(() => 42);
|
||||
const sender = new WSSenderGrbl('test.test', 2300, 'x', 'y', 'z', null, null, null, null, {
|
||||
setIntervalFn,
|
||||
clearIntervalFn: jest.fn(),
|
||||
});
|
||||
sender._startHeartbeat();
|
||||
expect(setIntervalFn).toHaveBeenCalledWith(expect.any(Function), 10000);
|
||||
expect(sender._heartbeatTimer).toBe(42);
|
||||
});
|
||||
|
||||
test('heartbeat callback sends ?', () => {
|
||||
let callback;
|
||||
const setIntervalFn = jest.fn((fn) => { callback = fn; return 1; });
|
||||
const sender = new WSSenderGrbl('test.test', 2300, 'x', 'y', 'z', null, null, null, null, {
|
||||
setIntervalFn,
|
||||
clearIntervalFn: jest.fn(),
|
||||
});
|
||||
sender._startHeartbeat();
|
||||
callback();
|
||||
expect(sender.ws.written).toBe('?\n');
|
||||
});
|
||||
|
||||
test('_stopHeartbeat clears interval and nulls timer', () => {
|
||||
const clearIntervalFn = jest.fn();
|
||||
const sender = new WSSenderGrbl('test.test', 2300, 'x', 'y', 'z', null, null, null, null, {
|
||||
setIntervalFn: jest.fn(() => 42),
|
||||
clearIntervalFn,
|
||||
});
|
||||
sender._startHeartbeat();
|
||||
sender._stopHeartbeat();
|
||||
expect(clearIntervalFn).toHaveBeenCalledWith(42);
|
||||
expect(sender._heartbeatTimer).toBeNull();
|
||||
});
|
||||
|
||||
test('_stopHeartbeat is safe when no timer is running', () => {
|
||||
const clearIntervalFn = jest.fn();
|
||||
const sender = new WSSenderGrbl('test.test', 2300, 'x', 'y', 'z', null, null, null, null, {
|
||||
clearIntervalFn,
|
||||
});
|
||||
expect(() => sender._stopHeartbeat()).not.toThrow();
|
||||
expect(clearIntervalFn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('custom heartbeatInterval is used', () => {
|
||||
const setIntervalFn = jest.fn(() => 1);
|
||||
const sender = new WSSenderGrbl('test.test', 2300, 'x', 'y', 'z', null, null, null, null, {
|
||||
setIntervalFn,
|
||||
clearIntervalFn: jest.fn(),
|
||||
heartbeatInterval: 5000,
|
||||
});
|
||||
sender._startHeartbeat();
|
||||
expect(setIntervalFn).toHaveBeenCalledWith(expect.any(Function), 5000);
|
||||
});
|
||||
|
||||
test('disconnect() stops a running heartbeat', () => {
|
||||
const clearIntervalFn = jest.fn();
|
||||
const sender = new WSSenderGrbl('test.test', 2300, 'x', 'y', 'z', null, null, null, null, {
|
||||
setIntervalFn: jest.fn(() => 7),
|
||||
clearIntervalFn,
|
||||
});
|
||||
sender._startHeartbeat();
|
||||
sender.disconnect();
|
||||
expect(clearIntervalFn).toHaveBeenCalledWith(7);
|
||||
expect(sender._heartbeatTimer).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Integration: WS open/close triggers heartbeat ───────────────────────────
|
||||
|
||||
describe('WSSenderGrbl heartbeat integration with WS lifecycle', () => {
|
||||
test('heartbeat starts on WS open', () => {
|
||||
const setIntervalFn = jest.fn(() => 99);
|
||||
const { sender } = makeConnectedSender({ setIntervalFn });
|
||||
expect(setIntervalFn).toHaveBeenCalled();
|
||||
expect(sender._heartbeatTimer).toBe(99);
|
||||
});
|
||||
|
||||
test('heartbeat stops on WS close', () => {
|
||||
const clearIntervalFn = jest.fn();
|
||||
const { sender, mockWs } = makeConnectedSender({ clearIntervalFn });
|
||||
mockWs.emit('close');
|
||||
expect(clearIntervalFn).toHaveBeenCalled();
|
||||
expect(sender._heartbeatTimer).toBeNull();
|
||||
});
|
||||
|
||||
test('incoming WS message updates grblState end-to-end', () => {
|
||||
const { sender, mockWs } = makeConnectedSender();
|
||||
mockWs.emit('message', '<Idle|MPos:1.00,2.00,3.00>');
|
||||
expect(sender.grblState).toBe('Idle');
|
||||
expect(sender.machinePosition).toEqual([1.00, 2.00, 3.00]);
|
||||
});
|
||||
|
||||
test('incoming WS message emits status event end-to-end', () => {
|
||||
const { sender, mockWs } = makeConnectedSender();
|
||||
const listener = jest.fn();
|
||||
sender.on('status', listener);
|
||||
mockWs.emit('message', '<Run|MPos:5.00,0.00,0.00>');
|
||||
expect(listener).toHaveBeenCalledWith({
|
||||
grblState: 'Run',
|
||||
machinePosition: [5.00, 0.00, 0.00],
|
||||
});
|
||||
});
|
||||
|
||||
test('incoming WS I2CRESP emits i2cresp event end-to-end', () => {
|
||||
const { sender, mockWs } = makeConnectedSender();
|
||||
const listener = jest.fn();
|
||||
sender.on('i2cresp', listener);
|
||||
mockWs.emit('message', 'I2CRESP B0 A0x18: C0 01 40 F8 80 3F');
|
||||
expect(listener).toHaveBeenCalledWith(['C0', '01', '40', 'F8', '80', '3F']);
|
||||
});
|
||||
});
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user