From 204e050ae420e15a05b1722102a57729ed25270a Mon Sep 17 00:00:00 2001 From: chk <79915315+ChKendel@users.noreply.github.com> Date: Mon, 8 Jun 2026 16:53:14 +0200 Subject: [PATCH] Neubau auf Abrufe --- README.md | 88 ++- doc/README_BodyTracker.md | 149 +++++ doc/README_WebCam.md | 65 ++ {docs => doc}/pic/code_robot_definition.pdf | Bin {docs => doc}/pic/code_robot_definition.svg | 0 doc/pic/image.png | Bin 0 -> 29248 bytes {docs => doc}/pic/robot_frontView_forearm.pdf | Bin {docs => doc}/pic/robot_frontView_forearm.svg | 0 .../pic/robot_frontView_forearm_0.pdf | Bin .../pic/robot_frontView_forearm_1.pdf | Bin {docs => doc}/pic/robot_hand_sideView.pdf | Bin {docs => doc}/pic/robot_hand_sideView.svg | 0 {docs => doc}/pic/robot_hand_sideView_0.pdf | Bin {docs => doc}/pic/robot_hand_topView.pdf | Bin {docs => doc}/pic/robot_hand_topView.svg | 0 {docs => doc}/pic/robot_hand_topView_0.pdf | Bin {docs => doc}/pic/robot_image_a.png | Bin {docs => doc}/pic/robot_image_b.png | Bin {docs => doc}/pic/robot_image_c.png | Bin {docs => doc}/pic/robot_image_d.png | Bin {docs => doc}/pic/robot_image_e.png | Bin {docs => doc}/pic/robot_sideView_forearm.pdf | Bin {docs => doc}/pic/robot_sideView_forearm.svg | 0 .../pic/robot_sideView_forearm_0.pdf | Bin .../pic/robot_sideView_measurements.pdf | Bin .../pic/robot_sideView_measurements.svg | 0 .../pic/robot_sideView_measurements_0.pdf | Bin {docs => doc}/position.pdf | Bin {docs => doc}/position.tex | 0 package-lock.json | 573 +----------------- package.json | 7 +- public/client.js | 52 +- scripts/generate-certs.js | 63 -- server/server.js | 391 ++++-------- 34 files changed, 446 insertions(+), 942 deletions(-) create mode 100644 doc/README_BodyTracker.md create mode 100644 doc/README_WebCam.md rename {docs => doc}/pic/code_robot_definition.pdf (100%) rename {docs => doc}/pic/code_robot_definition.svg (100%) create mode 100644 doc/pic/image.png rename {docs => doc}/pic/robot_frontView_forearm.pdf (100%) rename {docs => doc}/pic/robot_frontView_forearm.svg (100%) rename {docs => doc}/pic/robot_frontView_forearm_0.pdf (100%) rename {docs => doc}/pic/robot_frontView_forearm_1.pdf (100%) rename {docs => doc}/pic/robot_hand_sideView.pdf (100%) rename {docs => doc}/pic/robot_hand_sideView.svg (100%) rename {docs => doc}/pic/robot_hand_sideView_0.pdf (100%) rename {docs => doc}/pic/robot_hand_topView.pdf (100%) rename {docs => doc}/pic/robot_hand_topView.svg (100%) rename {docs => doc}/pic/robot_hand_topView_0.pdf (100%) rename {docs => doc}/pic/robot_image_a.png (100%) rename {docs => doc}/pic/robot_image_b.png (100%) rename {docs => doc}/pic/robot_image_c.png (100%) rename {docs => doc}/pic/robot_image_d.png (100%) rename {docs => doc}/pic/robot_image_e.png (100%) rename {docs => doc}/pic/robot_sideView_forearm.pdf (100%) rename {docs => doc}/pic/robot_sideView_forearm.svg (100%) rename {docs => doc}/pic/robot_sideView_forearm_0.pdf (100%) rename {docs => doc}/pic/robot_sideView_measurements.pdf (100%) rename {docs => doc}/pic/robot_sideView_measurements.svg (100%) rename {docs => doc}/pic/robot_sideView_measurements_0.pdf (100%) rename {docs => doc}/position.pdf (100%) rename {docs => doc}/position.tex (100%) delete mode 100755 scripts/generate-certs.js 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 0000000000000000000000000000000000000000..37410ff53a90f3aebb59f6c100140cdbd939dd0b GIT binary patch literal 29248 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 literal 0 HcmV?d00001 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 || ''}`); });