diff --git a/README.md b/README.md index 02822cd..40f8324 100755 --- a/README.md +++ b/README.md @@ -1,57 +1,49 @@ # 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 -- **HTTPS**-Server wird automatisch mit **selbstsignierten Zertifikaten** betrieben. -- **Postinstall-Task** erstellt bei `npm install` die Zertifikate unter `./certs`. -- **WSS-Client** mit Auto-Reconnect und optionaler TLS-Validierung (in `.env` steuerbar). -- **SSE** (Server-Sent Events) für Live-Logs im Browser. +## Was das Projekt jetzt macht +- Holt aus der WebCam alle 3 bis 10 Bilder ab (siehe `doc/README_WebCam.md`). +- Zeigt ausgewählte Bilder und die zugehörigen `.npz`-Daten in einer Auswertungsansicht. +- Übergibt diese Daten an den BodyTracker (`doc/README_BodyTracker.md`). +- Ermittelt daraus die Roboterpose und gibt sie aus. -## Schnellstart -```bash -# 1) Abhängigkeiten installieren und Zertifikate erzeugen -npm install +## Aktueller Fokus +- Benutzeroberfläche bleibt der Einstieg. +- Bildanzeige und Poseausgabe sind zentral. +- 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 -cp .env.sample .env -# Werte nach Bedarf anpassen +## Integration +- Die WebCam- und BodyTracker-Aufrufe laufen über das Backend, nicht direkt aus dem Browser. +- 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 -npm run dev # mit Nodemon -# oder -npm start # ohne Nodemon -``` +## Geplante Erweiterungen +1. Pose an `appRobotDriver` weitergeben. +2. Wenn die Hand nicht erkannt wird: Vorschlag für eine bessere Arm-/Foto-Position. +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 -(Da selbstsigniert, musst du dem Zertifikat im Browser einmalig vertrauen.) +## Dateien & Struktur +- `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`) -Siehe `.env.sample` für alle verfügbaren Variablen: -- `HTTPS_PORT` (Standard: `8443`) -- `WSS_URL` (z. B. `wss://localhost:9001`) -- `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) +## Nutzung +1. `npm install` +2. `npm test` +3. Öffne `public/index.html` im Browser oder nutze einen beliebigen statischen Server. -## Sicherheitshinweise -- Die Inhalte des Verzeichnisses `certs/` sowie `.env` sind **absichtlich** in `.gitignore` eingetragen und werden nicht in Gitea eingecheckt. -- In Entwicklungsumgebungen kann `WSS_INSECURE_TLS=true` nötig sein. In Produktion **deaktivieren** und echte Zertifikate verwenden. - -## 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. +> Hinweis: Die Anwendung ist aktuell als Frontend/Analyse-UI aufgebaut. Die +> Backend-Serverlogik aus früheren Versionen wurde bereinigt, um das Projekt zu +> fokussieren. diff --git a/doc/README_BodyTracker.md b/doc/README_BodyTracker.md new file mode 100644 index 0000000..b246c6c --- /dev/null +++ b/doc/README_BodyTracker.md @@ -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://: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) diff --git a/doc/README_WebCam.md b/doc/README_WebCam.md new file mode 100644 index 0000000..d6e2e47 --- /dev/null +++ b/doc/README_WebCam.md @@ -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 ``, ~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://:8444/ Viewer +http://:8444/api/stream/cam0 Live-MJPEG +http://:8444/api/snapshot/cam0 640er JPEG +http://:8444/api/snapshot/cam0/hires HD-JPEG +http://:8444/api/cameras Kamera-Metadaten (JSON) +http://: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 | diff --git a/docs/pic/code_robot_definition.pdf b/doc/pic/code_robot_definition.pdf similarity index 100% rename from docs/pic/code_robot_definition.pdf rename to doc/pic/code_robot_definition.pdf diff --git a/docs/pic/code_robot_definition.svg b/doc/pic/code_robot_definition.svg similarity index 100% rename from docs/pic/code_robot_definition.svg rename to doc/pic/code_robot_definition.svg diff --git a/doc/pic/image.png b/doc/pic/image.png new file mode 100644 index 0000000..37410ff Binary files /dev/null and b/doc/pic/image.png differ diff --git a/docs/pic/robot_frontView_forearm.pdf b/doc/pic/robot_frontView_forearm.pdf similarity index 100% rename from docs/pic/robot_frontView_forearm.pdf rename to doc/pic/robot_frontView_forearm.pdf diff --git a/docs/pic/robot_frontView_forearm.svg b/doc/pic/robot_frontView_forearm.svg similarity index 100% rename from docs/pic/robot_frontView_forearm.svg rename to doc/pic/robot_frontView_forearm.svg diff --git a/docs/pic/robot_frontView_forearm_0.pdf b/doc/pic/robot_frontView_forearm_0.pdf similarity index 100% rename from docs/pic/robot_frontView_forearm_0.pdf rename to doc/pic/robot_frontView_forearm_0.pdf diff --git a/docs/pic/robot_frontView_forearm_1.pdf b/doc/pic/robot_frontView_forearm_1.pdf similarity index 100% rename from docs/pic/robot_frontView_forearm_1.pdf rename to doc/pic/robot_frontView_forearm_1.pdf diff --git a/docs/pic/robot_hand_sideView.pdf b/doc/pic/robot_hand_sideView.pdf similarity index 100% rename from docs/pic/robot_hand_sideView.pdf rename to doc/pic/robot_hand_sideView.pdf diff --git a/docs/pic/robot_hand_sideView.svg b/doc/pic/robot_hand_sideView.svg similarity index 100% rename from docs/pic/robot_hand_sideView.svg rename to doc/pic/robot_hand_sideView.svg diff --git a/docs/pic/robot_hand_sideView_0.pdf b/doc/pic/robot_hand_sideView_0.pdf similarity index 100% rename from docs/pic/robot_hand_sideView_0.pdf rename to doc/pic/robot_hand_sideView_0.pdf diff --git a/docs/pic/robot_hand_topView.pdf b/doc/pic/robot_hand_topView.pdf similarity index 100% rename from docs/pic/robot_hand_topView.pdf rename to doc/pic/robot_hand_topView.pdf diff --git a/docs/pic/robot_hand_topView.svg b/doc/pic/robot_hand_topView.svg similarity index 100% rename from docs/pic/robot_hand_topView.svg rename to doc/pic/robot_hand_topView.svg diff --git a/docs/pic/robot_hand_topView_0.pdf b/doc/pic/robot_hand_topView_0.pdf similarity index 100% rename from docs/pic/robot_hand_topView_0.pdf rename to doc/pic/robot_hand_topView_0.pdf diff --git a/docs/pic/robot_image_a.png b/doc/pic/robot_image_a.png similarity index 100% rename from docs/pic/robot_image_a.png rename to doc/pic/robot_image_a.png diff --git a/docs/pic/robot_image_b.png b/doc/pic/robot_image_b.png similarity index 100% rename from docs/pic/robot_image_b.png rename to doc/pic/robot_image_b.png diff --git a/docs/pic/robot_image_c.png b/doc/pic/robot_image_c.png similarity index 100% rename from docs/pic/robot_image_c.png rename to doc/pic/robot_image_c.png diff --git a/docs/pic/robot_image_d.png b/doc/pic/robot_image_d.png similarity index 100% rename from docs/pic/robot_image_d.png rename to doc/pic/robot_image_d.png diff --git a/docs/pic/robot_image_e.png b/doc/pic/robot_image_e.png similarity index 100% rename from docs/pic/robot_image_e.png rename to doc/pic/robot_image_e.png diff --git a/docs/pic/robot_sideView_forearm.pdf b/doc/pic/robot_sideView_forearm.pdf similarity index 100% rename from docs/pic/robot_sideView_forearm.pdf rename to doc/pic/robot_sideView_forearm.pdf diff --git a/docs/pic/robot_sideView_forearm.svg b/doc/pic/robot_sideView_forearm.svg similarity index 100% rename from docs/pic/robot_sideView_forearm.svg rename to doc/pic/robot_sideView_forearm.svg diff --git a/docs/pic/robot_sideView_forearm_0.pdf b/doc/pic/robot_sideView_forearm_0.pdf similarity index 100% rename from docs/pic/robot_sideView_forearm_0.pdf rename to doc/pic/robot_sideView_forearm_0.pdf diff --git a/docs/pic/robot_sideView_measurements.pdf b/doc/pic/robot_sideView_measurements.pdf similarity index 100% rename from docs/pic/robot_sideView_measurements.pdf rename to doc/pic/robot_sideView_measurements.pdf diff --git a/docs/pic/robot_sideView_measurements.svg b/doc/pic/robot_sideView_measurements.svg similarity index 100% rename from docs/pic/robot_sideView_measurements.svg rename to doc/pic/robot_sideView_measurements.svg diff --git a/docs/pic/robot_sideView_measurements_0.pdf b/doc/pic/robot_sideView_measurements_0.pdf similarity index 100% rename from docs/pic/robot_sideView_measurements_0.pdf rename to doc/pic/robot_sideView_measurements_0.pdf diff --git a/docs/position.pdf b/doc/position.pdf similarity index 100% rename from docs/position.pdf rename to doc/position.pdf diff --git a/docs/position.tex b/doc/position.tex similarity index 100% rename from docs/position.tex rename to doc/position.tex diff --git a/package-lock.json b/package-lock.json index af1ed9b..530d495 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,15 +7,11 @@ "": { "name": "approbothoming", "version": "0.1.0", - "hasInstallScript": true, "dependencies": { "dotenv": "^16.4.5", - "express": "^4.19.2", - "selfsigned": "^2.4.1", - "ws": "^8.18.0" + "express": "^4.19.2" }, "devDependencies": { - "esbuild": "^0.28.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "nodemon": "^3.0.2" @@ -567,448 +563,6 @@ "dev": true, "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": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1513,20 +1067,12 @@ "version": "25.6.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "dev": true, "license": "MIT", "dependencies": { "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": { "version": "2.0.3", "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" } }, - "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": { "version": "1.1.14", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", @@ -2586,48 +2117,6 @@ "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": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2770,14 +2259,14 @@ } }, "node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "~1.20.3", + "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", @@ -2796,7 +2285,7 @@ "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "~6.14.0", + "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", @@ -4505,15 +3994,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": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -4568,9 +4048,9 @@ } }, "node_modules/nodemon/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -4632,9 +4112,9 @@ "license": "MIT" }, "node_modules/nodemon/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", "dev": true, "license": "ISC", "bin": { @@ -5022,9 +4502,9 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -5198,19 +4678,6 @@ "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": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -5388,9 +4855,9 @@ } }, "node_modules/simple-update-notifier/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", "dev": true, "license": "ISC", "bin": { @@ -5705,6 +5172,7 @@ "version": "7.19.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "dev": true, "license": "MIT" }, "node_modules/universalify": { @@ -5944,6 +5412,7 @@ "version": "8.20.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/package.json b/package.json index 679e5a4..6a7ace8 100755 --- a/package.json +++ b/package.json @@ -7,8 +7,6 @@ "scripts": { "start": "node server/server.js", "dev": "nodemon server/server.js", - "postinstall": "node scripts/generate-certs.js || true", - "create": "node scripts/generate-certs.js", "test": "jest", "test:coverage": "jest --coverage" }, @@ -17,12 +15,9 @@ }, "dependencies": { "dotenv": "^16.4.5", - "express": "^4.19.2", - "selfsigned": "^2.4.1", - "ws": "^8.18.0" + "express": "^4.19.2" }, "devDependencies": { - "esbuild": "^0.28.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "nodemon": "^3.0.2" diff --git a/public/client.js b/public/client.js index f66b597..97e4b0c 100755 --- a/public/client.js +++ b/public/client.js @@ -148,6 +148,34 @@ async function fetchCSV_fromServer() { 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() { const table = document.getElementById("snapshot-table"); const pictureEl = document.getElementById("snapshot-info-picture"); @@ -227,14 +255,24 @@ async function onCalculateClick() { try { 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 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); await renderSnapshot(); appendLog("Result angezeigt."); diff --git a/scripts/generate-certs.js b/scripts/generate-certs.js deleted file mode 100755 index 4750392..0000000 --- a/scripts/generate-certs.js +++ /dev/null @@ -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 -} diff --git a/server/server.js b/server/server.js index f07431f..d70b4ab 100755 --- a/server/server.js +++ b/server/server.js @@ -1,288 +1,147 @@ -import fs from 'fs'; -import path from 'path'; -import https from 'https'; import express from 'express'; -import dotenv from 'dotenv'; -import { WebSocket } from 'ws'; -import { EventEmitter } from 'events'; +import path from 'path'; +import fs from 'fs/promises'; +import { fileURLToPath } from 'url'; +import process from 'process'; -dotenv.config(); +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); const app = express(); -app.use(express.json()); +app.use(express.json({ limit: '20mb' })); -const CERT_DIR = path.resolve('certs'); -const KEY_PATH = path.join(CERT_DIR, 'localhost.key'); -const CRT_PATH = path.join(CERT_DIR, 'localhost.crt'); +const PORT = parseInt(process.env.PORT || process.env.HTTPS_PORT || '2093', 10); +const publicDir = path.join(__dirname, '..', 'public'); +const snapshotsDir = path.join(publicDir, 'snapshots'); +const WEBCAM_URL = process.env.WEBCAM_URL || ''; +const BODYTRACKER_URL = process.env.BODYTRACKER_URL || ''; -function loadHttpsCredentials() { - 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) }; -} +app.use(express.static(publicDir)); -const HTTPS_PORT = parseInt(process.env.HTTPS_PORT || '2033', 10); -const WSS_URL = process.env.WSS_URL || 'wss://localhost:2096'; -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.get('/api/health', (req, res) => { + res.json({ ok: true, mode: 'backend-proxy', webcamUrl: WEBCAM_URL || null, bodyTrackerUrl: BODYTRACKER_URL || null }); }); -app.post('/api/send', (req, res) => { - const { cmd, payload } = req.body || {}; - if (!cmd || !allowedCommands.has(String(cmd).trim())) { - return res.status(400).json({ ok: false, error: 'Ungültiges oder nicht erlaubtes Kommando', allowed: Array.from(allowedCommands) }); - } - if (!wsDriver || wsDriver.readyState !== wsDriver.OPEN) { - return res.status(503).json({ ok: false, error: 'WSS nicht verbunden' }); - } - const msg = { type: String(cmd).trim(), payload: payload ?? null }; +async function findLatestSnapshotFile() { + const files = await fs.readdir(snapshotsDir); + const entries = await Promise.all( + files + .filter((name) => name.endsWith('.csv')) + .map(async (name) => ({ + name, + mtime: (await fs.stat(path.join(snapshotsDir, name))).mtime.valueOf() + })) + ); + if (entries.length === 0) return null; + entries.sort((a, b) => b.mtime - a.mtime); + return entries[0].name; +} - if(msg.type==="STATUS"){ - wsDriver.send("M114"); - logAndBroadcast('tx', 'Sende STATUS (M114) an WSS'); - return res.json({ ok: true, sent: msg }); - } - - if(msg.type==="GCODEMOTOR"){ - if(typeof msg.payload !== 'string' || !msg.payload.trim()){ - return res.status(400).json({ ok: false, error: 'Ungültiger Payload für GCODEMOTOR. Erwartet: String mit G-Code Befehl.' }); - } - - wsDriver.send(msg.payload); - console.log(`G-Code gesendet: ${msg.payload}`); - /* - 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 - console.log(`G0 ${arrayMsg[1].toUpperCase()} F1000 gesendet`); - } - */ - return res.json({ ok: true, sent: msg.payload}); - } - +app.get('/api/latest-snapshot', async (req, res) => { try { - wsDriver.send(JSON.stringify(msg)); - logAndBroadcast('tx', 'Sende an WSS', msg); - return res.json({ ok: true, sent: msg }); - } catch (err) { - logAndBroadcast('error', 'Senden an WSS fehlgeschlagen', { error: err?.message || String(err) }); - return res.status(500).json({ ok: false, error: 'Senden fehlgeschlagen' }); - } -}); + if (WEBCAM_URL) { + const url = new URL('/api/latest-snapshot', WEBCAM_URL).toString(); + const fetchRes = await fetch(url); + const contentType = fetchRes.headers.get('content-type') || ''; -// SSE-Endpoint -app.get('/api/events', (req, res) => { - res.setHeader('Content-Type', 'text/event-stream'); - res.setHeader('Cache-Control', 'no-cache'); - 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' }); + if (!fetchRes.ok) { + const text = await fetchRes.text(); + return res.status(fetchRes.status).type('text/plain').send(text); } - const response = { - filename: latestFile.name, - mtime: latestFile.mtime.toISOString(), - content: data + if (contentType.includes('application/json')) { + const body = await fetchRes.json(); + return res.json(body); + } + + const text = await fetchRes.text(); + return res.json({ filename: 'latest.csv', mtime: new Date().toISOString(), content: text }); + } + + const latestFile = await findLatestSnapshotFile(); + if (!latestFile) { + return res.status(404).json({ error: 'Keine Snapshot-CSV-Datei gefunden' }); + } + + 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 {} - const jsonPath = path.join(snapshotsDir, jsonFilename); + try { + const jpg2 = await fs.readFile(imagePath2); + result.image2 = { + filename: path.basename(imagePath2), + mimeType: 'image/jpeg', + contentBase64: jpg2.toString('base64') + }; + } catch {} - // ✅ 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); - }); - }); - }); - }); - - //-- - }); + 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) }); + } }); -// Statisches Frontend -app.use('/', express.static(path.resolve('public'))); +app.post('/api/estimate', async (req, res) => { + if (!BODYTRACKER_URL) { + return res.status(501).json({ error: 'BODYTRACKER_URL ist nicht konfiguriert' }); + } -// HTTPS-Server starten -const creds = loadHttpsCredentials(); -const server = https.createServer({ - key: creds.key, - cert: creds.cert, -}, app); + try { + const { imageFile, image2, robotIntrinsics } = req.body; + const formData = new FormData(); -server.listen(HTTPS_PORT, () => { - logAndBroadcast('info', `HTTPS Server läuft auf https://localhost:${HTTPS_PORT}`); - // Nach Start WSS verbinden - connectWss(); + 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) { + console.error('estimate error:', err); + return res.status(500).json({ error: 'Fehler beim Aufruf des BodyTracker', details: String(err) }); + } +}); + +app.listen(PORT, () => { + console.log(`appRobotHoming backend listening on port ${PORT}`); + console.log(`WEBCAM_URL=${WEBCAM_URL || ''}`); + console.log(`BODYTRACKER_URL=${BODYTRACKER_URL || ''}`); });