Neubau auf Abrufe

This commit is contained in:
chk
2026-06-08 16:53:14 +02:00
parent f3a9f62ecd
commit 204e050ae4
34 changed files with 446 additions and 942 deletions

View File

@@ -1,57 +1,49 @@
# appRobotHoming # appRobotHoming
Eine kleine Node.js-App mit HTTPS-Frontend (einige Buttons + Textfeld) und Backend, das sich mit einem konfigurierbaren **WSS** (WebSocket Secure) verbindet. Die Buttons senden Befehle an den WSS, und das Textfeld zeigt eingehende Nachrichten/Logs an. `appRobotHoming` ist eine browserbasierte Benutzeroberfläche für die
WebCam-gestützte Ermittlung der Roboterpose. Der Einstieg bleibt als einfaches
Frontend erhalten, während die Auswertung künftig an den BodyTracker weitergeleitet
wird.
## Features ## Was das Projekt jetzt macht
- **HTTPS**-Server wird automatisch mit **selbstsignierten Zertifikaten** betrieben. - Holt aus der WebCam alle 3 bis 10 Bilder ab (siehe `doc/README_WebCam.md`).
- **Postinstall-Task** erstellt bei `npm install` die Zertifikate unter `./certs`. - Zeigt ausgewählte Bilder und die zugehörigen `.npz`-Daten in einer Auswertungsansicht.
- **WSS-Client** mit Auto-Reconnect und optionaler TLS-Validierung (in `.env` steuerbar). - Übergibt diese Daten an den BodyTracker (`doc/README_BodyTracker.md`).
- **SSE** (Server-Sent Events) für Live-Logs im Browser. - Ermittelt daraus die Roboterpose und gibt sie aus.
## Schnellstart ## Aktueller Fokus
```bash - Benutzeroberfläche bleibt der Einstieg.
# 1) Abhängigkeiten installieren und Zertifikate erzeugen - Bildanzeige und Poseausgabe sind zentral.
npm install - Der alte HTTPS/WSS-Server wurde entfernt.
- `certs/`, `scripts/` und `server/` sind nicht mehr Teil des aktuellen Projekts.
# 2) (Optional) .env anlegen, basierend auf .env.sample ## Integration
cp .env.sample .env - Die WebCam- und BodyTracker-Aufrufe laufen über das Backend, nicht direkt aus dem Browser.
# Werte nach Bedarf anpassen - Das Frontend lädt Snapshot-Daten über `/api/latest-snapshot`.
- Der Browser sendet Pose-Anfragen an `/api/estimate`.
- Das Backend kann dann auf interne Docker-Container zugreifen, z. B. auf den WebCam-Service und den BodyTracker-Service.
- Als Fallback verwendet das Backend lokale `public/snapshots`, wenn keine externe WebCam verfügbar ist.
- Konfigurierbare Umgebungsvariablen:
- `WEBCAM_URL` Basis-URL des internen Webcam-Services.
- `BODYTRACKER_URL` Basis-URL des internen BodyTracker-Services.
# 3) Starten ## Geplante Erweiterungen
npm run dev # mit Nodemon 1. Pose an `appRobotDriver` weitergeben.
# oder 2. Wenn die Hand nicht erkannt wird: Vorschlag für eine bessere Arm-/Foto-Position.
npm start # ohne Nodemon 3. Manuelle Eingabe von `x, y, z, a, b, c, e`.
``` 4. Erkennungsergebnis und erkannte Pose klar im UI ausgeben.
Öffne danach: https://localhost:8443 ## Dateien & Struktur
(Da selbstsigniert, musst du dem Zertifikat im Browser einmalig vertrauen.) - `public/` Frontend, UI, Client-Logik und Anzeige.
- `doc/README_WebCam.md` Details zur Webcam-Architektur und Bildabholung.
- `doc/README_BodyTracker.md` BodyTracker-Integration und Poseermittlung.
- `test/` bestehende Tests für die Berechnung und Auswertung.
## Konfiguration (`.env`) ## Nutzung
Siehe `.env.sample` für alle verfügbaren Variablen: 1. `npm install`
- `HTTPS_PORT` (Standard: `8443`) 2. `npm test`
- `WSS_URL` (z. B. `wss://localhost:9001`) 3. Öffne `public/index.html` im Browser oder nutze einen beliebigen statischen Server.
- `WSS_INSECURE_TLS` (`true|false`) bei selbstsignierten Upstream-Zertifikaten oft `true`
- `HTTPS_HOST` (CN für das Zertifikat, Standard: `localhost`)
- `HTTPS_CERT_DAYS` (Gültigkeitsdauer des selbstsignierten Zertifikats in Tagen)
- `ALLOWED_COMMANDS` (kommasepariert; nur diese Kommandos akzeptiert das Backend)
## Sicherheitshinweise > Hinweis: Die Anwendung ist aktuell als Frontend/Analyse-UI aufgebaut. Die
- Die Inhalte des Verzeichnisses `certs/` sowie `.env` sind **absichtlich** in `.gitignore` eingetragen und werden nicht in Gitea eingecheckt. > Backend-Serverlogik aus früheren Versionen wurde bereinigt, um das Projekt zu
- In Entwicklungsumgebungen kann `WSS_INSECURE_TLS=true` nötig sein. In Produktion **deaktivieren** und echte Zertifikate verwenden. > fokussieren.
## Ordnerstruktur
```
appRobotHoming/
├─ public/ # Statisches Frontend (HTML/JS/CSS)
├─ src/ # Backend-Quellcode
├─ scripts/ # Utility-Skripte (z. B. Zertifikatserzeugung)
├─ certs/ # (auto-generiert) selbstsignierte Zertifikate
├─ .gitignore
├─ .env.sample
├─ package.json
└─ README.md
```
## Gitea-Upload
- Committe den Code **ohne** `certs/` und **ohne** `.env`.
- Nach dem Klonen auf einem anderen System einfach `npm install` ausführen die Zertifikate werden wieder neu erzeugt.

149
doc/README_BodyTracker.md Normal file
View File

@@ -0,0 +1,149 @@
# appRobotBodyTrack
3D-Body-Tracking für Roboter aus Mehrkamera-ArUco-Bildern.
**Input**
- Bilder: `render_*.png`
- Intrinsics: `render_*.npz`
- Konfiguration: `robot.json`
**Output**
- Gelenke **R⁷**`{x, y, z, a, b, c, e}` (mm / Grad)
---
## Interfaces
Eine Logik, drei Zugänge:
- **Python**
- **CLI**
- **REST (FastAPI)**
---
## Quickstart
### Python
```python
from scripts import estimate_from_dir
result = estimate_from_dir("data/Scene8", robot_json="robot.json")
print(result.joints)
print(result.confidence)
```
---
### CLI
```bash
pip install -e .
python -m scripts data/Scene8 --robot robot.json
python -m scripts data/Scene8 --robot robot.json --cameras a,b,d
```
---
### REST API
```bash
docker compose up
```
**Request:**
```python
import requests
resp = requests.post(
"http://localhost:8446/v1/estimate",
files=[
("images", ("render_a.png", open("render_a.png", "rb"))),
("intrinsics", ("render_a.npz", open("render_a.npz", "rb"))),
],
)
print(resp.json()["joints"])
```
---
## API
| Endpoint | Methode | Zweck |
|----------|--------|------|
| `/v1/estimate` | POST | Bilder → Gelenke |
| `/v1/health` | GET | Status |
| `/v1/config` | GET | aktive Konfiguration |
**Response:**
```json
{
"joints": {"x": 50.2, "y": -2.1, "z": 94.8, "a": 20.1},
"confidence": {"x": "high", "b": "low"},
"residual_rms": 1.45,
"n_markers": 56,
"processing_ms": 1240
}
```
---
## Struktur
```
.
├── scripts/
├── config/robot.json
├── tests/
└── docker-compose.yaml
```
---
## Deployment (Docker / Portainer)
**Volume:**
```yaml
- /opt/approbot/config/robot.json:/config/robot.json:ro
```
**Healthcheck:**
```bash
curl http://<host>:8446/v1/health
```
---
## Konfiguration
Zentrale Datei: **`robot.json`**
Verwendete Bereiche:
- `links`
- `pose_estimation`
- `vision_config`
- `movements`
- `units`
---
## Stack (minimal)
- numpy
- scipy
- opencv (aruco)
- fastapi + uvicorn
---
## Naming
- **BodyTrack** → Tracking (dynamisch) ✅
- **BodyMap** → Modell / Repräsentation
- **BodySense** → Wahrnehmung (low-level)

65
doc/README_WebCam.md Normal file
View File

@@ -0,0 +1,65 @@
# AppRobotWebcam
Webcam-Service für den AppRobot. Liefert Live-MJPEG-Streams und HD-Standbilder
über einen einzelnen HTTP-Port — als Docker-Container, ohne externe Streaming-Server.
## Was es tut
| | |
|---|---|
| **Live-Stream** | MJPEG multipart im Browser `<img>`, ~139 ms Latenz |
| **HD-Snapshot** | Ein JPEG pro Kamera auf Knopfdruck oder per HTTP GET |
| **Snapshot alle** | Alle Kameras parallel in einem Schritt |
| **REST-API** | Kameraliste, Snapshots, Streams — für andere Container nutzbar |
## Kameras (aktuell)
| ID | Modell | Live | HD-Grab |
|---|---|---|---|
| cam0 | Logitech C270 | 640×480 | 1280×960 |
| cam1 | Logitech C270 | 640×480 | 1280×960 |
| cam2 | Logitech C920 | 640×480 | 1920×1080 |
Konfiguration ausschliesslich über `cameras.json` — kein Redeploy bei Kamera-Änderungen.
## Zugriff
```
http://<host>:8444/ Viewer
http://<host>:8444/api/stream/cam0 Live-MJPEG
http://<host>:8444/api/snapshot/cam0 640er JPEG
http://<host>:8444/api/snapshot/cam0/hires HD-JPEG
http://<host>:8444/api/cameras Kamera-Metadaten (JSON)
http://<host>:8444/health Status
```
## Deploy (Portainer)
1. Portainer → Stacks → Web editor → `docker-compose.yaml` einfügen
2. `APP_PATH` auf den absoluten Pfad des Projektverzeichnisses setzen
3. Deploy — der Container baut sich selbst (Node + FFmpeg)
```yaml
# Minimal-Konfiguration:
APP_PATH=/home/user/appRobotWebcam
```
## Architektur
```
cameras.json → server.js → CameraSwitch (/dev/videoN)
├── Live: ffmpeg → MJPEG → Browser
└── Grab: Live stoppen → hires → zurück
```
Ein FFmpeg pro Kamera, nie zwei gleichzeitig. Das `close`-Event ist der harte Beweis
„Gerät frei" — kein Race, kein 106%-CPU-Bug (der mit go2rtc aufgetreten war).
## Dokumentation
| Datei | Inhalt |
|---|---|
| `doc/01_WebcamRoadmap.md` | Ziel, Architektur, Entwicklungsgeschichte |
| `doc/05_screenShot_roadmap.md` | HD-Grab, Encode-Qualität, Kamera-Eigenheiten |
| `doc/07_multipleCam_roadmap.md` | cameras.json-Referenz, Multi-Kamera-Setup |
| `doc/09_Bug_reports.md` | Bug-Dokumentation |

View File

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 154 KiB

BIN
doc/pic/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

Before

Width:  |  Height:  |  Size: 274 KiB

After

Width:  |  Height:  |  Size: 274 KiB

View File

Before

Width:  |  Height:  |  Size: 321 KiB

After

Width:  |  Height:  |  Size: 321 KiB

View File

Before

Width:  |  Height:  |  Size: 354 KiB

After

Width:  |  Height:  |  Size: 354 KiB

View File

Before

Width:  |  Height:  |  Size: 544 KiB

After

Width:  |  Height:  |  Size: 544 KiB

View File

Before

Width:  |  Height:  |  Size: 575 KiB

After

Width:  |  Height:  |  Size: 575 KiB

View File

Before

Width:  |  Height:  |  Size: 747 KiB

After

Width:  |  Height:  |  Size: 747 KiB

View File

Before

Width:  |  Height:  |  Size: 626 KiB

After

Width:  |  Height:  |  Size: 626 KiB

View File

Before

Width:  |  Height:  |  Size: 588 KiB

After

Width:  |  Height:  |  Size: 588 KiB

View File

Before

Width:  |  Height:  |  Size: 506 KiB

After

Width:  |  Height:  |  Size: 506 KiB

View File

Before

Width:  |  Height:  |  Size: 554 KiB

After

Width:  |  Height:  |  Size: 554 KiB

573
package-lock.json generated
View File

@@ -7,15 +7,11 @@
"": { "": {
"name": "approbothoming", "name": "approbothoming",
"version": "0.1.0", "version": "0.1.0",
"hasInstallScript": true,
"dependencies": { "dependencies": {
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.19.2", "express": "^4.19.2"
"selfsigned": "^2.4.1",
"ws": "^8.18.0"
}, },
"devDependencies": { "devDependencies": {
"esbuild": "^0.28.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
"nodemon": "^3.0.2" "nodemon": "^3.0.2"
@@ -567,448 +563,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
"integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
"integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
"integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
"integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
"integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
"integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
"integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
"integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
"integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
"integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
"integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
"integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
"integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
"integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
"integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
"integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
"integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
"integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
"integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
"integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
"integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
"integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
"integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
"integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
"integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@istanbuljs/load-nyc-config": { "node_modules/@istanbuljs/load-nyc-config": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@@ -1513,20 +1067,12 @@
"version": "25.6.0", "version": "25.6.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~7.19.0" "undici-types": "~7.19.0"
} }
}, },
"node_modules/@types/node-forge": {
"version": "1.3.14",
"resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz",
"integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/stack-utils": { "node_modules/@types/stack-utils": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
@@ -1906,21 +1452,6 @@
"npm": "1.2.8000 || >= 1.4.16" "npm": "1.2.8000 || >= 1.4.16"
} }
}, },
"node_modules/body-parser/node_modules/qs": {
"version": "6.15.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
"integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.14", "version": "1.1.14",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
@@ -2586,48 +2117,6 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/esbuild": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.28.0",
"@esbuild/android-arm": "0.28.0",
"@esbuild/android-arm64": "0.28.0",
"@esbuild/android-x64": "0.28.0",
"@esbuild/darwin-arm64": "0.28.0",
"@esbuild/darwin-x64": "0.28.0",
"@esbuild/freebsd-arm64": "0.28.0",
"@esbuild/freebsd-x64": "0.28.0",
"@esbuild/linux-arm": "0.28.0",
"@esbuild/linux-arm64": "0.28.0",
"@esbuild/linux-ia32": "0.28.0",
"@esbuild/linux-loong64": "0.28.0",
"@esbuild/linux-mips64el": "0.28.0",
"@esbuild/linux-ppc64": "0.28.0",
"@esbuild/linux-riscv64": "0.28.0",
"@esbuild/linux-s390x": "0.28.0",
"@esbuild/linux-x64": "0.28.0",
"@esbuild/netbsd-arm64": "0.28.0",
"@esbuild/netbsd-x64": "0.28.0",
"@esbuild/openbsd-arm64": "0.28.0",
"@esbuild/openbsd-x64": "0.28.0",
"@esbuild/openharmony-arm64": "0.28.0",
"@esbuild/sunos-x64": "0.28.0",
"@esbuild/win32-arm64": "0.28.0",
"@esbuild/win32-ia32": "0.28.0",
"@esbuild/win32-x64": "0.28.0"
}
},
"node_modules/escalade": { "node_modules/escalade": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -2770,14 +2259,14 @@
} }
}, },
"node_modules/express": { "node_modules/express": {
"version": "4.22.1", "version": "4.22.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"accepts": "~1.3.8", "accepts": "~1.3.8",
"array-flatten": "1.1.1", "array-flatten": "1.1.1",
"body-parser": "~1.20.3", "body-parser": "~1.20.5",
"content-disposition": "~0.5.4", "content-disposition": "~0.5.4",
"content-type": "~1.0.4", "content-type": "~1.0.4",
"cookie": "~0.7.1", "cookie": "~0.7.1",
@@ -2796,7 +2285,7 @@
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"path-to-regexp": "~0.1.12", "path-to-regexp": "~0.1.12",
"proxy-addr": "~2.0.7", "proxy-addr": "~2.0.7",
"qs": "~6.14.0", "qs": "~6.15.1",
"range-parser": "~1.2.1", "range-parser": "~1.2.1",
"safe-buffer": "5.2.1", "safe-buffer": "5.2.1",
"send": "~0.19.0", "send": "~0.19.0",
@@ -4505,15 +3994,6 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/node-forge": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz",
"integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==",
"license": "(BSD-3-Clause OR GPL-2.0)",
"engines": {
"node": ">= 6.13.0"
}
},
"node_modules/node-int64": { "node_modules/node-int64": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@@ -4568,9 +4048,9 @@
} }
}, },
"node_modules/nodemon/node_modules/brace-expansion": { "node_modules/nodemon/node_modules/brace-expansion": {
"version": "5.0.5", "version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -4632,9 +4112,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/nodemon/node_modules/semver": { "node_modules/nodemon/node_modules/semver": {
"version": "7.7.4", "version": "7.8.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
@@ -5022,9 +4502,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/qs": { "node_modules/qs": {
"version": "6.14.2", "version": "6.15.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
"integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"side-channel": "^1.1.0" "side-channel": "^1.1.0"
@@ -5198,19 +4678,6 @@
"node": ">=v12.22.7" "node": ">=v12.22.7"
} }
}, },
"node_modules/selfsigned": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz",
"integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==",
"license": "MIT",
"dependencies": {
"@types/node-forge": "^1.3.0",
"node-forge": "^1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/semver": { "node_modules/semver": {
"version": "6.3.1", "version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -5388,9 +4855,9 @@
} }
}, },
"node_modules/simple-update-notifier/node_modules/semver": { "node_modules/simple-update-notifier/node_modules/semver": {
"version": "7.7.4", "version": "7.8.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
@@ -5705,6 +5172,7 @@
"version": "7.19.2", "version": "7.19.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/universalify": { "node_modules/universalify": {
@@ -5944,6 +5412,7 @@
"version": "8.20.0", "version": "8.20.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"

View File

@@ -7,8 +7,6 @@
"scripts": { "scripts": {
"start": "node server/server.js", "start": "node server/server.js",
"dev": "nodemon server/server.js", "dev": "nodemon server/server.js",
"postinstall": "node scripts/generate-certs.js || true",
"create": "node scripts/generate-certs.js",
"test": "jest", "test": "jest",
"test:coverage": "jest --coverage" "test:coverage": "jest --coverage"
}, },
@@ -17,12 +15,9 @@
}, },
"dependencies": { "dependencies": {
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.19.2", "express": "^4.19.2"
"selfsigned": "^2.4.1",
"ws": "^8.18.0"
}, },
"devDependencies": { "devDependencies": {
"esbuild": "^0.28.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
"nodemon": "^3.0.2" "nodemon": "^3.0.2"

View File

@@ -148,6 +148,34 @@ async function fetchCSV_fromServer() {
return { data, headers, rows }; return { data, headers, rows };
} }
async function fetchWebcamSnapshotData() {
const { data, headers, rows } = await fetchCSV();
return {
filename: data.filename,
mtime: data.mtime,
headers,
rows,
robotIntrinsics: jsonCache,
imageFile: data.imageFile,
image2: data.image2
};
}
async function sendToBodyTracker({imageFile, image2, robotIntrinsics}) {
const response = await fetch('/api/estimate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ imageFile, image2, robotIntrinsics })
});
if (!response.ok) {
const message = await response.text();
throw new Error(`BodyTracker fehlgeschlagen (${response.status}): ${message}`);
}
return await response.json();
}
async function renderSnapshot() { async function renderSnapshot() {
const table = document.getElementById("snapshot-table"); const table = document.getElementById("snapshot-table");
const pictureEl = document.getElementById("snapshot-info-picture"); const pictureEl = document.getElementById("snapshot-info-picture");
@@ -227,14 +255,24 @@ async function onCalculateClick() {
try { try {
const response = await fetch("robot.json"); const response = await fetch("robot.json");
await fetchCSV();
//console.log("Data:", dataCache);
//console.log("json: ", JSON.stringify(jsonCache));
//console.log("Keys:", Object.keys(dataCache));
const robot = await response.json(); const robot = await response.json();
const result = await window.calculate(jsonCache, robot); const snapshot = await fetchWebcamSnapshotData();
appendLog(`Snapshot geladen: ${snapshot.filename} (${snapshot.rows.length} Zeilen)`);
let result = null;
try {
result = await sendToBodyTracker(snapshot);
appendLog("BodyTracker wurde aufgerufen.");
} catch (err) {
appendLog(`BodyTracker nicht aufgerufen: ${err.message}`);
}
if (!result) {
appendLog("Fallback: lokale Pose-Berechnung mit calculateAngles.js");
result = await window.calculate(snapshot.robotIntrinsics, robot);
}
renderResult(result); renderResult(result);
await renderSnapshot(); await renderSnapshot();
appendLog("Result angezeigt."); appendLog("Result angezeigt.");

View File

@@ -1,63 +0,0 @@
// Generiert selbstsignierte Zertifikate bei npm install
import fs from 'fs';
import path from 'path';
import selfsigned from 'selfsigned';
const CERT_DIR = path.resolve('certs');
const KEY_PATH = path.join(CERT_DIR, 'localhost.key');
const CRT_PATH = path.join(CERT_DIR, 'localhost.crt');
function ensureDir(p) {
if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true });
}
function generateIfMissing() {
ensureDir(CERT_DIR);
const host = process.env.HTTPS_HOST || 'localhost';
const days = parseInt(process.env.HTTPS_CERT_DAYS || '3650', 10);
const needKey = !fs.existsSync(KEY_PATH);
const needCrt = !fs.existsSync(CRT_PATH);
if (!needKey && !needCrt) {
console.log(`[certs] Zertifikate existieren bereits in ${CERT_DIR}`);
return;
}
console.log(`[certs] Erzeuge selbstsigniertes Zertifikat für CN=${host}, ${days} Tage gültig...`);
const attrs = [{ name: 'commonName', value: host }];
const pems = selfsigned.generate(attrs, {
keySize: 2048,
days,
algorithm: 'sha256',
extensions: [
{ name: 'basicConstraints', cA: true },
{ name: 'keyUsage', keyCertSign: true, digitalSignature: true, nonRepudiation: true, keyEncipherment: true },
{ name: 'extKeyUsage', serverAuth: true, clientAuth: true },
{ name: 'subjectAltName', altNames: [ { type: 2, value: host }, { type: 7, ip: '127.0.0.1' } ] }
]
});
fs.writeFileSync(KEY_PATH, pems.private, { mode: 0o600 });
fs.writeFileSync(CRT_PATH, pems.cert, { mode: 0o644 });
const readme = `Diese Zertifikate sind nur für lokale Entwicklung gedacht.
` +
`Dateien:
- ${KEY_PATH}
- ${CRT_PATH}
` +
`Nicht committen! Siehe .gitignore.`;
fs.writeFileSync(path.join(CERT_DIR, 'README.txt'), readme);
console.log(`[certs] Zertifikate erzeugt unter ${CERT_DIR}`);
}
try {
generateIfMissing();
} catch (err) {
console.error('[certs] Fehler beim Erzeugen der Zertifikate:', err?.message || err);
process.exit(0); // nicht als harter Fehler werten
}

View File

@@ -1,288 +1,147 @@
import fs from 'fs';
import path from 'path';
import https from 'https';
import express from 'express'; import express from 'express';
import dotenv from 'dotenv'; import path from 'path';
import { WebSocket } from 'ws'; import fs from 'fs/promises';
import { EventEmitter } from 'events'; import { fileURLToPath } from 'url';
import process from 'process';
dotenv.config(); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express(); const app = express();
app.use(express.json()); app.use(express.json({ limit: '20mb' }));
const CERT_DIR = path.resolve('certs'); const PORT = parseInt(process.env.PORT || process.env.HTTPS_PORT || '2093', 10);
const KEY_PATH = path.join(CERT_DIR, 'localhost.key'); const publicDir = path.join(__dirname, '..', 'public');
const CRT_PATH = path.join(CERT_DIR, 'localhost.crt'); const snapshotsDir = path.join(publicDir, 'snapshots');
const WEBCAM_URL = process.env.WEBCAM_URL || '';
const BODYTRACKER_URL = process.env.BODYTRACKER_URL || '';
function loadHttpsCredentials() { app.use(express.static(publicDir));
if (!fs.existsSync(KEY_PATH) || !fs.existsSync(CRT_PATH)) {
console.error(`HTTPS-Zertifikate fehlen in ${CERT_DIR}. Bitte 'npm install' ausführen (Postinstall generiert Zertifikate).`);
process.exit(1);
}
return { key: fs.readFileSync(KEY_PATH), cert: fs.readFileSync(CRT_PATH) };
}
const HTTPS_PORT = parseInt(process.env.HTTPS_PORT || '2033', 10); app.get('/api/health', (req, res) => {
const WSS_URL = process.env.WSS_URL || 'wss://localhost:2096'; res.json({ ok: true, mode: 'backend-proxy', webcamUrl: WEBCAM_URL || null, bodyTrackerUrl: BODYTRACKER_URL || null });
const WSS_INSECURE_TLS = String(process.env.WSS_INSECURE_TLS || 'true').toLowerCase() === 'true';
// Nur bestimmte Kommandos erlauben (aus .env)
const allowedCommands = new Set(
(process.env.ALLOWED_COMMANDS || 'HOME,STOP,STATUS,RESET,PING,GCODEMOTOR')
.split(',')
.map(s => s.trim())
.filter(Boolean)
);
// Broadcaster für Server-Sent Events
const bus = new EventEmitter();
let wsDriver = null;
let wsState = {
connected: false,
lastError: null,
reconnectAttempts: 0,
};
function logAndBroadcast(level, message, data) {
const payload = { ts: new Date().toISOString(), level, message, data };
// Konsole
const line = `[${payload.ts}] [${level}] ${message}`;
//console.log(line, data ? data : '');
// SSE an Clients
bus.emit('event', JSON.stringify(payload));
}
function connectWss() {
if (wsDriver && (wsDriver.readyState === wsDriver.OPEN || wsDriver.readyState === wsDriver.CONNECTING)) {
return;
}
const tlsOptions = { rejectUnauthorized: !WSS_INSECURE_TLS };
logAndBroadcast('info', `Verbinde zu WSS: ${WSS_URL} (rejectUnauthorized=${tlsOptions.rejectUnauthorized})`);
wsDriver = new WebSocket(WSS_URL, tlsOptions);
wsDriver.on('open', () => {
wsState.connected = true;
wsState.lastError = null;
wsState.reconnectAttempts = 0;
logAndBroadcast('info', 'WSS Driver verbunden');
});
wsDriver.on('message', (data) => {
let text = '';
try { text = typeof data === 'string' ? data : data.toString('utf8'); } catch { text = '[binary data]'; }
logAndBroadcast('msg', 'Eingang von WSS', { text });
});
wsDriver.on('close', (code, reason) => {
wsState.connected = false;
logAndBroadcast('warn', `WSS getrennt (code=${code}, reason=${reason?.toString?.() || ''})`);
scheduleReconnect();
});
wsDriver.on('error', (err) => {
wsState.lastError = err?.message || String(err);
logAndBroadcast('error', 'WSS Fehler', { error: wsState.lastError });
});
}
function scheduleReconnect() {
wsState.reconnectAttempts += 1;
const delay = 10000; // 10s
logAndBroadcast('info', `Reconnecting in ${Math.round(delay/1000)}s...`);
setTimeout(connectWss, delay);
}
// HTTP API
app.get('/api/status', (req, res) => {
wsDriver.send("M114");
console.log("M114 gesendet, warte auf Antwort...");
res.json({
httpsPort: HTTPS_PORT,
wssUrl: WSS_URL,
connected: wsState.connected,
wsDriver: wsDriver ? wsDriver.readyState : null,
reconnectAttempts: wsState.reconnectAttempts,
lastError: wsState.lastError,
allowedCommands: Array.from(allowedCommands)
});
}); });
app.post('/api/send', (req, res) => { async function findLatestSnapshotFile() {
const { cmd, payload } = req.body || {}; const files = await fs.readdir(snapshotsDir);
if (!cmd || !allowedCommands.has(String(cmd).trim())) { const entries = await Promise.all(
return res.status(400).json({ ok: false, error: 'Ungültiges oder nicht erlaubtes Kommando', allowed: Array.from(allowedCommands) }); files
} .filter((name) => name.endsWith('.csv'))
if (!wsDriver || wsDriver.readyState !== wsDriver.OPEN) { .map(async (name) => ({
return res.status(503).json({ ok: false, error: 'WSS nicht verbunden' }); name,
} mtime: (await fs.stat(path.join(snapshotsDir, name))).mtime.valueOf()
const msg = { type: String(cmd).trim(), payload: payload ?? null }; }))
);
if (entries.length === 0) return null;
entries.sort((a, b) => b.mtime - a.mtime);
return entries[0].name;
}
if(msg.type==="STATUS"){ app.get('/api/latest-snapshot', async (req, res) => {
wsDriver.send("M114"); try {
logAndBroadcast('tx', 'Sende STATUS (M114) an WSS'); if (WEBCAM_URL) {
return res.json({ ok: true, sent: msg }); const url = new URL('/api/latest-snapshot', WEBCAM_URL).toString();
const fetchRes = await fetch(url);
const contentType = fetchRes.headers.get('content-type') || '';
if (!fetchRes.ok) {
const text = await fetchRes.text();
return res.status(fetchRes.status).type('text/plain').send(text);
} }
if(msg.type==="GCODEMOTOR"){ if (contentType.includes('application/json')) {
if(typeof msg.payload !== 'string' || !msg.payload.trim()){ const body = await fetchRes.json();
return res.status(400).json({ ok: false, error: 'Ungültiger Payload für GCODEMOTOR. Erwartet: String mit G-Code Befehl.' }); return res.json(body);
} }
wsDriver.send(msg.payload); const text = await fetchRes.text();
console.log(`G-Code gesendet: ${msg.payload}`); return res.json({ filename: 'latest.csv', mtime: new Date().toISOString(), content: text });
/*
msg.payload = msg.payload.trim();
var arrayMsg = msg.payload.split(' ').filter(s => s.trim());
if(arrayMsg.length === 0 || !['G0','G1','G28', 'M0', 'M1', 'M114'].includes(arrayMsg[0].toUpperCase())){
return res.status(400).json({ ok: false, error: 'Ungültiger G-Code Befehl. Nur G0, G1 und G28 sind erlaubt.' });
} }
if(arrayMsg[1].toUpperCase().startsWith('X')){
wsDriver.send(`G0 ${arrayMsg[1].toUpperCase()} F1000`); // Schnelles Verfahren zu X-Position const latestFile = await findLatestSnapshotFile();
console.log(`G0 ${arrayMsg[1].toUpperCase()} F1000 gesendet`); if (!latestFile) {
return res.status(404).json({ error: 'Keine Snapshot-CSV-Datei gefunden' });
} }
*/
return res.json({ ok: true, sent: msg.payload}); const baseName = path.basename(latestFile, path.extname(latestFile));
const csvPath = path.join(snapshotsDir, latestFile);
const jsonPath = path.join(snapshotsDir, `${baseName}.json`);
const imagePath = path.join(snapshotsDir, `${baseName}_annotated.jpg`);
const imagePath2 = path.join(snapshotsDir, `${baseName}_annotated2.jpg`);
const content = await fs.readFile(csvPath, 'utf8');
const result = { filename: latestFile, mtime: (await fs.stat(csvPath)).mtime.toISOString(), content };
try {
result.jsonFile = { filename: `${baseName}.json`, content: await fs.readFile(jsonPath, 'utf8') };
} catch {}
try {
const jpg = await fs.readFile(imagePath);
result.imageFile = {
filename: path.basename(imagePath),
mimeType: 'image/jpeg',
contentBase64: jpg.toString('base64')
};
} catch {}
try {
const jpg2 = await fs.readFile(imagePath2);
result.image2 = {
filename: path.basename(imagePath2),
mimeType: 'image/jpeg',
contentBase64: jpg2.toString('base64')
};
} catch {}
return res.json(result);
} catch (err) {
console.error('latest-snapshot error:', err);
return res.status(500).json({ error: 'Fehler beim Laden des Snapshots', details: String(err) });
}
});
app.post('/api/estimate', async (req, res) => {
if (!BODYTRACKER_URL) {
return res.status(501).json({ error: 'BODYTRACKER_URL ist nicht konfiguriert' });
} }
try { try {
wsDriver.send(JSON.stringify(msg)); const { imageFile, image2, robotIntrinsics } = req.body;
logAndBroadcast('tx', 'Sende an WSS', msg); const formData = new FormData();
return res.json({ ok: true, sent: msg });
if (imageFile?.contentBase64) {
const buffer = Buffer.from(imageFile.contentBase64, 'base64');
formData.append('images', new Blob([buffer], { type: imageFile.mimeType || 'image/jpeg' }), imageFile.filename || 'snapshot.jpg');
}
if (image2?.contentBase64) {
const buffer2 = Buffer.from(image2.contentBase64, 'base64');
formData.append('images', new Blob([buffer2], { type: image2.mimeType || 'image/jpeg' }), image2.filename || 'snapshot2.jpg');
}
if (robotIntrinsics) {
formData.append('intrinsics', new Blob([JSON.stringify(robotIntrinsics)], { type: 'application/json' }), 'intrinsics.json');
}
const estimateUrl = new URL('/v1/estimate', BODYTRACKER_URL).toString();
const fetchRes = await fetch(estimateUrl, { method: 'POST', body: formData });
if (!fetchRes.ok) {
const message = await fetchRes.text();
return res.status(fetchRes.status).json({ error: 'BodyTracker-Fehler', details: message });
}
const body = await fetchRes.json();
return res.json(body);
} catch (err) { } catch (err) {
logAndBroadcast('error', 'Senden an WSS fehlgeschlagen', { error: err?.message || String(err) }); console.error('estimate error:', err);
return res.status(500).json({ ok: false, error: 'Senden fehlgeschlagen' }); return res.status(500).json({ error: 'Fehler beim Aufruf des BodyTracker', details: String(err) });
} }
}); });
// SSE-Endpoint app.listen(PORT, () => {
app.get('/api/events', (req, res) => { console.log(`appRobotHoming backend listening on port ${PORT}`);
res.setHeader('Content-Type', 'text/event-stream'); console.log(`WEBCAM_URL=${WEBCAM_URL || '<lokal>'}`);
res.setHeader('Cache-Control', 'no-cache'); console.log(`BODYTRACKER_URL=${BODYTRACKER_URL || '<nicht konfiguriert>'}`);
res.setHeader('Connection', 'keep-alive');
res.flushHeaders?.();
const send = (data) => {
res.write(`data: ${data}
`);
};
const listener = (data) => send(data);
bus.on('event', listener);
// Initialstatus schicken
send(JSON.stringify({ ts: new Date().toISOString(), level: 'info', message: 'SSE verbunden' }));
req.on('close', () => {
bus.off('event', listener);
res.end();
});
});
//snapshot_video0_1775319258906_two_cam.csv
//snapshot_video0_1775319258906_two_cam_annotated.jpg
// Neuester Snapshot-Endpunkt
app.get('/api/latest-snapshot', (req, res) => {
const snapshotsDir = path.join(path.resolve('public'), 'snapshots');
fs.readdir(snapshotsDir, (err, files) => {
if (err) {
return res.status(500).json({ error: 'Fehler beim Lesen des Snapshots-Verzeichnisses' });
}
const csvFiles = files.filter(file => file.endsWith('.csv')).map(file => ({
name: file,
path: path.join(snapshotsDir, file),
mtime: fs.statSync(path.join(snapshotsDir, file)).mtime
})).sort((a, b) => b.mtime - a.mtime);
if (csvFiles.length === 0) {
return res.status(404).json({ error: 'Keine CSV-Dateien gefunden' });
}
const latestFile = csvFiles[0];
const baseName = path.basename(latestFile.name, path.extname(latestFile.name));
const jsonFilename = `${baseName}.json`;
const jsonPath = path.join(snapshotsDir, jsonFilename);
console.log("JSON Pfad:", jsonPath);
console.log("Existiert JSON:", fs.existsSync(jsonPath));
const imageFilename = `${baseName}_annotated.jpg`;
const imagePath = path.join(snapshotsDir, imageFilename);
const imatePath2 = imagePath.includes('video0') ? imagePath.replace('video0', 'video1') : imagePath.replace('video1', 'video0');
//--
fs.readFile(latestFile.path, 'utf8', (err, data) => {
if (err) {
return res.status(500).json({ error: 'Fehler beim Lesen der Datei' });
}
const response = {
filename: latestFile.name,
mtime: latestFile.mtime.toISOString(),
content: data
};
const jsonPath = path.join(snapshotsDir, jsonFilename);
// ✅ JSON FIRST, dann alles andere
fs.readFile(jsonPath, 'utf8', (jsonErr, jsonData) => {
if (!jsonErr && jsonData) {
response.jsonFile = {
filename: jsonFilename,
content: jsonData
};
}
// Bild 1
fs.readFile(imagePath, { encoding: 'base64' }, (jpgErr, jpgBase64) => {
if (!jpgErr && jpgBase64) {
response.imageFile = {
filename: imageFilename,
mimeType: 'image/jpeg',
contentBase64: jpgBase64
};
}
// Bild 2
fs.readFile(imatePath2, { encoding: 'base64' }, (jpgErr2, jpgBase642) => {
if (!jpgErr2 && jpgBase642) {
response.image2 = {
filename: path.basename(imatePath2),
mimeType: 'image/jpeg',
contentBase64: jpgBase642
};
}
// ✅ jetzt erst senden
res.json(response);
});
});
});
});
//--
});
});
// Statisches Frontend
app.use('/', express.static(path.resolve('public')));
// HTTPS-Server starten
const creds = loadHttpsCredentials();
const server = https.createServer({
key: creds.key,
cert: creds.cert,
}, app);
server.listen(HTTPS_PORT, () => {
logAndBroadcast('info', `HTTPS Server läuft auf https://localhost:${HTTPS_PORT}`);
// Nach Start WSS verbinden
connectWss();
}); });