Neubau auf Abrufe
88
README.md
@@ -1,57 +1,49 @@
|
|||||||
# appRobotHoming
|
# appRobotHoming
|
||||||
|
|
||||||
Eine kleine Node.js-App mit HTTPS-Frontend (einige Buttons + Textfeld) und Backend, das sich mit einem konfigurierbaren **WSS** (WebSocket Secure) verbindet. Die Buttons senden Befehle an den WSS, und das Textfeld zeigt eingehende Nachrichten/Logs an.
|
`appRobotHoming` ist eine browserbasierte Benutzeroberfläche für die
|
||||||
|
WebCam-gestützte Ermittlung der Roboterpose. Der Einstieg bleibt als einfaches
|
||||||
|
Frontend erhalten, während die Auswertung künftig an den BodyTracker weitergeleitet
|
||||||
|
wird.
|
||||||
|
|
||||||
## Features
|
## Was das Projekt jetzt macht
|
||||||
- **HTTPS**-Server wird automatisch mit **selbstsignierten Zertifikaten** betrieben.
|
- Holt aus der WebCam alle 3 bis 10 Bilder ab (siehe `doc/README_WebCam.md`).
|
||||||
- **Postinstall-Task** erstellt bei `npm install` die Zertifikate unter `./certs`.
|
- Zeigt ausgewählte Bilder und die zugehörigen `.npz`-Daten in einer Auswertungsansicht.
|
||||||
- **WSS-Client** mit Auto-Reconnect und optionaler TLS-Validierung (in `.env` steuerbar).
|
- Übergibt diese Daten an den BodyTracker (`doc/README_BodyTracker.md`).
|
||||||
- **SSE** (Server-Sent Events) für Live-Logs im Browser.
|
- Ermittelt daraus die Roboterpose und gibt sie aus.
|
||||||
|
|
||||||
## Schnellstart
|
## Aktueller Fokus
|
||||||
```bash
|
- Benutzeroberfläche bleibt der Einstieg.
|
||||||
# 1) Abhängigkeiten installieren und Zertifikate erzeugen
|
- Bildanzeige und Poseausgabe sind zentral.
|
||||||
npm install
|
- Der alte HTTPS/WSS-Server wurde entfernt.
|
||||||
|
- `certs/`, `scripts/` und `server/` sind nicht mehr Teil des aktuellen Projekts.
|
||||||
|
|
||||||
# 2) (Optional) .env anlegen, basierend auf .env.sample
|
## Integration
|
||||||
cp .env.sample .env
|
- Die WebCam- und BodyTracker-Aufrufe laufen über das Backend, nicht direkt aus dem Browser.
|
||||||
# Werte nach Bedarf anpassen
|
- Das Frontend lädt Snapshot-Daten über `/api/latest-snapshot`.
|
||||||
|
- Der Browser sendet Pose-Anfragen an `/api/estimate`.
|
||||||
|
- Das Backend kann dann auf interne Docker-Container zugreifen, z. B. auf den WebCam-Service und den BodyTracker-Service.
|
||||||
|
- Als Fallback verwendet das Backend lokale `public/snapshots`, wenn keine externe WebCam verfügbar ist.
|
||||||
|
- Konfigurierbare Umgebungsvariablen:
|
||||||
|
- `WEBCAM_URL` – Basis-URL des internen Webcam-Services.
|
||||||
|
- `BODYTRACKER_URL` – Basis-URL des internen BodyTracker-Services.
|
||||||
|
|
||||||
# 3) Starten
|
## Geplante Erweiterungen
|
||||||
npm run dev # mit Nodemon
|
1. Pose an `appRobotDriver` weitergeben.
|
||||||
# oder
|
2. Wenn die Hand nicht erkannt wird: Vorschlag für eine bessere Arm-/Foto-Position.
|
||||||
npm start # ohne Nodemon
|
3. Manuelle Eingabe von `x, y, z, a, b, c, e`.
|
||||||
```
|
4. Erkennungsergebnis und erkannte Pose klar im UI ausgeben.
|
||||||
|
|
||||||
Öffne danach: https://localhost:8443
|
## Dateien & Struktur
|
||||||
(Da selbstsigniert, musst du dem Zertifikat im Browser einmalig vertrauen.)
|
- `public/` – Frontend, UI, Client-Logik und Anzeige.
|
||||||
|
- `doc/README_WebCam.md` – Details zur Webcam-Architektur und Bildabholung.
|
||||||
|
- `doc/README_BodyTracker.md` – BodyTracker-Integration und Poseermittlung.
|
||||||
|
- `test/` – bestehende Tests für die Berechnung und Auswertung.
|
||||||
|
|
||||||
## Konfiguration (`.env`)
|
## Nutzung
|
||||||
Siehe `.env.sample` für alle verfügbaren Variablen:
|
1. `npm install`
|
||||||
- `HTTPS_PORT` (Standard: `8443`)
|
2. `npm test`
|
||||||
- `WSS_URL` (z. B. `wss://localhost:9001`)
|
3. Öffne `public/index.html` im Browser oder nutze einen beliebigen statischen Server.
|
||||||
- `WSS_INSECURE_TLS` (`true|false`) – bei selbstsignierten Upstream-Zertifikaten oft `true`
|
|
||||||
- `HTTPS_HOST` (CN für das Zertifikat, Standard: `localhost`)
|
|
||||||
- `HTTPS_CERT_DAYS` (Gültigkeitsdauer des selbstsignierten Zertifikats in Tagen)
|
|
||||||
- `ALLOWED_COMMANDS` (kommasepariert; nur diese Kommandos akzeptiert das Backend)
|
|
||||||
|
|
||||||
## Sicherheitshinweise
|
> Hinweis: Die Anwendung ist aktuell als Frontend/Analyse-UI aufgebaut. Die
|
||||||
- Die Inhalte des Verzeichnisses `certs/` sowie `.env` sind **absichtlich** in `.gitignore` eingetragen und werden nicht in Gitea eingecheckt.
|
> Backend-Serverlogik aus früheren Versionen wurde bereinigt, um das Projekt zu
|
||||||
- In Entwicklungsumgebungen kann `WSS_INSECURE_TLS=true` nötig sein. In Produktion **deaktivieren** und echte Zertifikate verwenden.
|
> fokussieren.
|
||||||
|
|
||||||
## Ordnerstruktur
|
|
||||||
```
|
|
||||||
appRobotHoming/
|
|
||||||
├─ public/ # Statisches Frontend (HTML/JS/CSS)
|
|
||||||
├─ src/ # Backend-Quellcode
|
|
||||||
├─ scripts/ # Utility-Skripte (z. B. Zertifikatserzeugung)
|
|
||||||
├─ certs/ # (auto-generiert) selbstsignierte Zertifikate
|
|
||||||
├─ .gitignore
|
|
||||||
├─ .env.sample
|
|
||||||
├─ package.json
|
|
||||||
└─ README.md
|
|
||||||
```
|
|
||||||
|
|
||||||
## Gitea-Upload
|
|
||||||
- Committe den Code **ohne** `certs/` und **ohne** `.env`.
|
|
||||||
- Nach dem Klonen auf einem anderen System einfach `npm install` ausführen – die Zertifikate werden wieder neu erzeugt.
|
|
||||||
|
|||||||
149
doc/README_BodyTracker.md
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
# appRobotBodyTrack
|
||||||
|
|
||||||
|
3D-Body-Tracking für Roboter aus Mehrkamera-ArUco-Bildern.
|
||||||
|
|
||||||
|
**Input**
|
||||||
|
- Bilder: `render_*.png`
|
||||||
|
- Intrinsics: `render_*.npz`
|
||||||
|
- Konfiguration: `robot.json`
|
||||||
|
|
||||||
|
**Output**
|
||||||
|
- Gelenke **R⁷** → `{x, y, z, a, b, c, e}` (mm / Grad)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Interfaces
|
||||||
|
|
||||||
|
Eine Logik, drei Zugänge:
|
||||||
|
|
||||||
|
- **Python**
|
||||||
|
- **CLI**
|
||||||
|
- **REST (FastAPI)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
from scripts import estimate_from_dir
|
||||||
|
|
||||||
|
result = estimate_from_dir("data/Scene8", robot_json="robot.json")
|
||||||
|
|
||||||
|
print(result.joints)
|
||||||
|
print(result.confidence)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -e .
|
||||||
|
|
||||||
|
python -m scripts data/Scene8 --robot robot.json
|
||||||
|
python -m scripts data/Scene8 --robot robot.json --cameras a,b,d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### REST API
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
resp = requests.post(
|
||||||
|
"http://localhost:8446/v1/estimate",
|
||||||
|
files=[
|
||||||
|
("images", ("render_a.png", open("render_a.png", "rb"))),
|
||||||
|
("intrinsics", ("render_a.npz", open("render_a.npz", "rb"))),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
print(resp.json()["joints"])
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
| Endpoint | Methode | Zweck |
|
||||||
|
|----------|--------|------|
|
||||||
|
| `/v1/estimate` | POST | Bilder → Gelenke |
|
||||||
|
| `/v1/health` | GET | Status |
|
||||||
|
| `/v1/config` | GET | aktive Konfiguration |
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"joints": {"x": 50.2, "y": -2.1, "z": 94.8, "a": 20.1},
|
||||||
|
"confidence": {"x": "high", "b": "low"},
|
||||||
|
"residual_rms": 1.45,
|
||||||
|
"n_markers": 56,
|
||||||
|
"processing_ms": 1240
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Struktur
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── scripts/
|
||||||
|
├── config/robot.json
|
||||||
|
├── tests/
|
||||||
|
└── docker-compose.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deployment (Docker / Portainer)
|
||||||
|
|
||||||
|
**Volume:**
|
||||||
|
```yaml
|
||||||
|
- /opt/approbot/config/robot.json:/config/robot.json:ro
|
||||||
|
```
|
||||||
|
|
||||||
|
**Healthcheck:**
|
||||||
|
```bash
|
||||||
|
curl http://<host>:8446/v1/health
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Konfiguration
|
||||||
|
|
||||||
|
Zentrale Datei: **`robot.json`**
|
||||||
|
|
||||||
|
Verwendete Bereiche:
|
||||||
|
- `links`
|
||||||
|
- `pose_estimation`
|
||||||
|
- `vision_config`
|
||||||
|
- `movements`
|
||||||
|
- `units`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stack (minimal)
|
||||||
|
|
||||||
|
- numpy
|
||||||
|
- scipy
|
||||||
|
- opencv (aruco)
|
||||||
|
- fastapi + uvicorn
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Naming
|
||||||
|
|
||||||
|
- **BodyTrack** → Tracking (dynamisch) ✅
|
||||||
|
- **BodyMap** → Modell / Repräsentation
|
||||||
|
- **BodySense** → Wahrnehmung (low-level)
|
||||||
65
doc/README_WebCam.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# AppRobotWebcam
|
||||||
|
|
||||||
|
Webcam-Service für den AppRobot. Liefert Live-MJPEG-Streams und HD-Standbilder
|
||||||
|
über einen einzelnen HTTP-Port — als Docker-Container, ohne externe Streaming-Server.
|
||||||
|
|
||||||
|
## Was es tut
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Live-Stream** | MJPEG multipart im Browser `<img>`, ~139 ms Latenz |
|
||||||
|
| **HD-Snapshot** | Ein JPEG pro Kamera auf Knopfdruck oder per HTTP GET |
|
||||||
|
| **Snapshot alle** | Alle Kameras parallel in einem Schritt |
|
||||||
|
| **REST-API** | Kameraliste, Snapshots, Streams — für andere Container nutzbar |
|
||||||
|
|
||||||
|
## Kameras (aktuell)
|
||||||
|
|
||||||
|
| ID | Modell | Live | HD-Grab |
|
||||||
|
|---|---|---|---|
|
||||||
|
| cam0 | Logitech C270 | 640×480 | 1280×960 |
|
||||||
|
| cam1 | Logitech C270 | 640×480 | 1280×960 |
|
||||||
|
| cam2 | Logitech C920 | 640×480 | 1920×1080 |
|
||||||
|
|
||||||
|
Konfiguration ausschliesslich über `cameras.json` — kein Redeploy bei Kamera-Änderungen.
|
||||||
|
|
||||||
|
## Zugriff
|
||||||
|
|
||||||
|
```
|
||||||
|
http://<host>:8444/ Viewer
|
||||||
|
http://<host>:8444/api/stream/cam0 Live-MJPEG
|
||||||
|
http://<host>:8444/api/snapshot/cam0 640er JPEG
|
||||||
|
http://<host>:8444/api/snapshot/cam0/hires HD-JPEG
|
||||||
|
http://<host>:8444/api/cameras Kamera-Metadaten (JSON)
|
||||||
|
http://<host>:8444/health Status
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deploy (Portainer)
|
||||||
|
|
||||||
|
1. Portainer → Stacks → Web editor → `docker-compose.yaml` einfügen
|
||||||
|
2. `APP_PATH` auf den absoluten Pfad des Projektverzeichnisses setzen
|
||||||
|
3. Deploy — der Container baut sich selbst (Node + FFmpeg)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Minimal-Konfiguration:
|
||||||
|
APP_PATH=/home/user/appRobotWebcam
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architektur
|
||||||
|
|
||||||
|
```
|
||||||
|
cameras.json → server.js → CameraSwitch (/dev/videoN)
|
||||||
|
├── Live: ffmpeg → MJPEG → Browser
|
||||||
|
└── Grab: Live stoppen → hires → zurück
|
||||||
|
```
|
||||||
|
|
||||||
|
Ein FFmpeg pro Kamera, nie zwei gleichzeitig. Das `close`-Event ist der harte Beweis
|
||||||
|
„Gerät frei" — kein Race, kein 106%-CPU-Bug (der mit go2rtc aufgetreten war).
|
||||||
|
|
||||||
|
## Dokumentation
|
||||||
|
|
||||||
|
| Datei | Inhalt |
|
||||||
|
|---|---|
|
||||||
|
| `doc/01_WebcamRoadmap.md` | Ziel, Architektur, Entwicklungsgeschichte |
|
||||||
|
| `doc/05_screenShot_roadmap.md` | HD-Grab, Encode-Qualität, Kamera-Eigenheiten |
|
||||||
|
| `doc/07_multipleCam_roadmap.md` | cameras.json-Referenz, Multi-Kamera-Setup |
|
||||||
|
| `doc/09_Bug_reports.md` | Bug-Dokumentation |
|
||||||
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 154 KiB |
BIN
doc/pic/image.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 274 KiB After Width: | Height: | Size: 274 KiB |
|
Before Width: | Height: | Size: 321 KiB After Width: | Height: | Size: 321 KiB |
|
Before Width: | Height: | Size: 354 KiB After Width: | Height: | Size: 354 KiB |
|
Before Width: | Height: | Size: 544 KiB After Width: | Height: | Size: 544 KiB |
|
Before Width: | Height: | Size: 575 KiB After Width: | Height: | Size: 575 KiB |
|
Before Width: | Height: | Size: 747 KiB After Width: | Height: | Size: 747 KiB |
|
Before Width: | Height: | Size: 626 KiB After Width: | Height: | Size: 626 KiB |
|
Before Width: | Height: | Size: 588 KiB After Width: | Height: | Size: 588 KiB |
|
Before Width: | Height: | Size: 506 KiB After Width: | Height: | Size: 506 KiB |
|
Before Width: | Height: | Size: 554 KiB After Width: | Height: | Size: 554 KiB |
573
package-lock.json
generated
@@ -7,15 +7,11 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "approbothoming",
|
"name": "approbothoming",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"hasInstallScript": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2"
|
||||||
"selfsigned": "^2.4.1",
|
|
||||||
"ws": "^8.18.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"esbuild": "^0.28.0",
|
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"nodemon": "^3.0.2"
|
"nodemon": "^3.0.2"
|
||||||
@@ -567,448 +563,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
|
||||||
"version": "0.28.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
|
|
||||||
"integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
|
|
||||||
"cpu": [
|
|
||||||
"ppc64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"aix"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/android-arm": {
|
|
||||||
"version": "0.28.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
|
|
||||||
"integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/android-arm64": {
|
|
||||||
"version": "0.28.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
|
|
||||||
"integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/android-x64": {
|
|
||||||
"version": "0.28.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
|
|
||||||
"integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/darwin-arm64": {
|
|
||||||
"version": "0.28.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
|
|
||||||
"integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/darwin-x64": {
|
|
||||||
"version": "0.28.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
|
|
||||||
"integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/freebsd-arm64": {
|
|
||||||
"version": "0.28.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
|
|
||||||
"integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"freebsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/freebsd-x64": {
|
|
||||||
"version": "0.28.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
|
|
||||||
"integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"freebsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-arm": {
|
|
||||||
"version": "0.28.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
|
|
||||||
"integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-arm64": {
|
|
||||||
"version": "0.28.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
|
|
||||||
"integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-ia32": {
|
|
||||||
"version": "0.28.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
|
|
||||||
"integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
|
|
||||||
"cpu": [
|
|
||||||
"ia32"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-loong64": {
|
|
||||||
"version": "0.28.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
|
|
||||||
"integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
|
|
||||||
"cpu": [
|
|
||||||
"loong64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-mips64el": {
|
|
||||||
"version": "0.28.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
|
|
||||||
"integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
|
|
||||||
"cpu": [
|
|
||||||
"mips64el"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-ppc64": {
|
|
||||||
"version": "0.28.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
|
|
||||||
"integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
|
|
||||||
"cpu": [
|
|
||||||
"ppc64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-riscv64": {
|
|
||||||
"version": "0.28.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
|
|
||||||
"integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
|
|
||||||
"cpu": [
|
|
||||||
"riscv64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-s390x": {
|
|
||||||
"version": "0.28.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
|
|
||||||
"integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
|
|
||||||
"cpu": [
|
|
||||||
"s390x"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-x64": {
|
|
||||||
"version": "0.28.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
|
|
||||||
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/netbsd-arm64": {
|
|
||||||
"version": "0.28.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
|
|
||||||
"integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"netbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/netbsd-x64": {
|
|
||||||
"version": "0.28.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
|
|
||||||
"integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"netbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/openbsd-arm64": {
|
|
||||||
"version": "0.28.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
|
|
||||||
"integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"openbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/openbsd-x64": {
|
|
||||||
"version": "0.28.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
|
|
||||||
"integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"openbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/openharmony-arm64": {
|
|
||||||
"version": "0.28.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
|
|
||||||
"integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"openharmony"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/sunos-x64": {
|
|
||||||
"version": "0.28.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
|
|
||||||
"integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"sunos"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/win32-arm64": {
|
|
||||||
"version": "0.28.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
|
|
||||||
"integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/win32-ia32": {
|
|
||||||
"version": "0.28.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
|
|
||||||
"integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
|
|
||||||
"cpu": [
|
|
||||||
"ia32"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/win32-x64": {
|
|
||||||
"version": "0.28.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
|
|
||||||
"integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@istanbuljs/load-nyc-config": {
|
"node_modules/@istanbuljs/load-nyc-config": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
|
||||||
@@ -1513,20 +1067,12 @@
|
|||||||
"version": "25.6.0",
|
"version": "25.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
|
||||||
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
|
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.19.0"
|
"undici-types": "~7.19.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node-forge": {
|
|
||||||
"version": "1.3.14",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz",
|
|
||||||
"integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/stack-utils": {
|
"node_modules/@types/stack-utils": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
|
||||||
@@ -1906,21 +1452,6 @@
|
|||||||
"npm": "1.2.8000 || >= 1.4.16"
|
"npm": "1.2.8000 || >= 1.4.16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/body-parser/node_modules/qs": {
|
|
||||||
"version": "6.15.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
|
|
||||||
"integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"side-channel": "^1.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.6"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.14",
|
"version": "1.1.14",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
||||||
@@ -2586,48 +2117,6 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/esbuild": {
|
|
||||||
"version": "0.28.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
|
|
||||||
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
|
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"esbuild": "bin/esbuild"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@esbuild/aix-ppc64": "0.28.0",
|
|
||||||
"@esbuild/android-arm": "0.28.0",
|
|
||||||
"@esbuild/android-arm64": "0.28.0",
|
|
||||||
"@esbuild/android-x64": "0.28.0",
|
|
||||||
"@esbuild/darwin-arm64": "0.28.0",
|
|
||||||
"@esbuild/darwin-x64": "0.28.0",
|
|
||||||
"@esbuild/freebsd-arm64": "0.28.0",
|
|
||||||
"@esbuild/freebsd-x64": "0.28.0",
|
|
||||||
"@esbuild/linux-arm": "0.28.0",
|
|
||||||
"@esbuild/linux-arm64": "0.28.0",
|
|
||||||
"@esbuild/linux-ia32": "0.28.0",
|
|
||||||
"@esbuild/linux-loong64": "0.28.0",
|
|
||||||
"@esbuild/linux-mips64el": "0.28.0",
|
|
||||||
"@esbuild/linux-ppc64": "0.28.0",
|
|
||||||
"@esbuild/linux-riscv64": "0.28.0",
|
|
||||||
"@esbuild/linux-s390x": "0.28.0",
|
|
||||||
"@esbuild/linux-x64": "0.28.0",
|
|
||||||
"@esbuild/netbsd-arm64": "0.28.0",
|
|
||||||
"@esbuild/netbsd-x64": "0.28.0",
|
|
||||||
"@esbuild/openbsd-arm64": "0.28.0",
|
|
||||||
"@esbuild/openbsd-x64": "0.28.0",
|
|
||||||
"@esbuild/openharmony-arm64": "0.28.0",
|
|
||||||
"@esbuild/sunos-x64": "0.28.0",
|
|
||||||
"@esbuild/win32-arm64": "0.28.0",
|
|
||||||
"@esbuild/win32-ia32": "0.28.0",
|
|
||||||
"@esbuild/win32-x64": "0.28.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/escalade": {
|
"node_modules/escalade": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||||
@@ -2770,14 +2259,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "4.22.1",
|
"version": "4.22.2",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
|
||||||
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
|
"integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "~1.3.8",
|
"accepts": "~1.3.8",
|
||||||
"array-flatten": "1.1.1",
|
"array-flatten": "1.1.1",
|
||||||
"body-parser": "~1.20.3",
|
"body-parser": "~1.20.5",
|
||||||
"content-disposition": "~0.5.4",
|
"content-disposition": "~0.5.4",
|
||||||
"content-type": "~1.0.4",
|
"content-type": "~1.0.4",
|
||||||
"cookie": "~0.7.1",
|
"cookie": "~0.7.1",
|
||||||
@@ -2796,7 +2285,7 @@
|
|||||||
"parseurl": "~1.3.3",
|
"parseurl": "~1.3.3",
|
||||||
"path-to-regexp": "~0.1.12",
|
"path-to-regexp": "~0.1.12",
|
||||||
"proxy-addr": "~2.0.7",
|
"proxy-addr": "~2.0.7",
|
||||||
"qs": "~6.14.0",
|
"qs": "~6.15.1",
|
||||||
"range-parser": "~1.2.1",
|
"range-parser": "~1.2.1",
|
||||||
"safe-buffer": "5.2.1",
|
"safe-buffer": "5.2.1",
|
||||||
"send": "~0.19.0",
|
"send": "~0.19.0",
|
||||||
@@ -4505,15 +3994,6 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/node-forge": {
|
|
||||||
"version": "1.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz",
|
|
||||||
"integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==",
|
|
||||||
"license": "(BSD-3-Clause OR GPL-2.0)",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 6.13.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/node-int64": {
|
"node_modules/node-int64": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||||
@@ -4568,9 +4048,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nodemon/node_modules/brace-expansion": {
|
"node_modules/nodemon/node_modules/brace-expansion": {
|
||||||
"version": "5.0.5",
|
"version": "5.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -4632,9 +4112,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/nodemon/node_modules/semver": {
|
"node_modules/nodemon/node_modules/semver": {
|
||||||
"version": "7.7.4",
|
"version": "7.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz",
|
||||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
"integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -5022,9 +4502,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.14.2",
|
"version": "6.15.2",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
|
||||||
"integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
|
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"side-channel": "^1.1.0"
|
"side-channel": "^1.1.0"
|
||||||
@@ -5198,19 +4678,6 @@
|
|||||||
"node": ">=v12.22.7"
|
"node": ">=v12.22.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/selfsigned": {
|
|
||||||
"version": "2.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz",
|
|
||||||
"integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node-forge": "^1.3.0",
|
|
||||||
"node-forge": "^1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
@@ -5388,9 +4855,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/simple-update-notifier/node_modules/semver": {
|
"node_modules/simple-update-notifier/node_modules/semver": {
|
||||||
"version": "7.7.4",
|
"version": "7.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz",
|
||||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
"integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -5705,6 +5172,7 @@
|
|||||||
"version": "7.19.2",
|
"version": "7.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
|
||||||
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
|
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/universalify": {
|
"node_modules/universalify": {
|
||||||
@@ -5944,6 +5412,7 @@
|
|||||||
"version": "8.20.0",
|
"version": "8.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
|
||||||
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
|
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
|
|||||||
@@ -7,8 +7,6 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node server/server.js",
|
"start": "node server/server.js",
|
||||||
"dev": "nodemon server/server.js",
|
"dev": "nodemon server/server.js",
|
||||||
"postinstall": "node scripts/generate-certs.js || true",
|
|
||||||
"create": "node scripts/generate-certs.js",
|
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:coverage": "jest --coverage"
|
"test:coverage": "jest --coverage"
|
||||||
},
|
},
|
||||||
@@ -17,12 +15,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2"
|
||||||
"selfsigned": "^2.4.1",
|
|
||||||
"ws": "^8.18.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"esbuild": "^0.28.0",
|
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"nodemon": "^3.0.2"
|
"nodemon": "^3.0.2"
|
||||||
|
|||||||
@@ -148,6 +148,34 @@ async function fetchCSV_fromServer() {
|
|||||||
return { data, headers, rows };
|
return { data, headers, rows };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchWebcamSnapshotData() {
|
||||||
|
const { data, headers, rows } = await fetchCSV();
|
||||||
|
return {
|
||||||
|
filename: data.filename,
|
||||||
|
mtime: data.mtime,
|
||||||
|
headers,
|
||||||
|
rows,
|
||||||
|
robotIntrinsics: jsonCache,
|
||||||
|
imageFile: data.imageFile,
|
||||||
|
image2: data.image2
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendToBodyTracker({imageFile, image2, robotIntrinsics}) {
|
||||||
|
const response = await fetch('/api/estimate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ imageFile, image2, robotIntrinsics })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const message = await response.text();
|
||||||
|
throw new Error(`BodyTracker fehlgeschlagen (${response.status}): ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
async function renderSnapshot() {
|
async function renderSnapshot() {
|
||||||
const table = document.getElementById("snapshot-table");
|
const table = document.getElementById("snapshot-table");
|
||||||
const pictureEl = document.getElementById("snapshot-info-picture");
|
const pictureEl = document.getElementById("snapshot-info-picture");
|
||||||
@@ -227,14 +255,24 @@ async function onCalculateClick() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("robot.json");
|
const response = await fetch("robot.json");
|
||||||
await fetchCSV();
|
|
||||||
|
|
||||||
//console.log("Data:", dataCache);
|
|
||||||
//console.log("json: ", JSON.stringify(jsonCache));
|
|
||||||
//console.log("Keys:", Object.keys(dataCache));
|
|
||||||
|
|
||||||
const robot = await response.json();
|
const robot = await response.json();
|
||||||
const result = await window.calculate(jsonCache, robot);
|
const snapshot = await fetchWebcamSnapshotData();
|
||||||
|
|
||||||
|
appendLog(`Snapshot geladen: ${snapshot.filename} (${snapshot.rows.length} Zeilen)`);
|
||||||
|
|
||||||
|
let result = null;
|
||||||
|
try {
|
||||||
|
result = await sendToBodyTracker(snapshot);
|
||||||
|
appendLog("BodyTracker wurde aufgerufen.");
|
||||||
|
} catch (err) {
|
||||||
|
appendLog(`BodyTracker nicht aufgerufen: ${err.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
appendLog("Fallback: lokale Pose-Berechnung mit calculateAngles.js");
|
||||||
|
result = await window.calculate(snapshot.robotIntrinsics, robot);
|
||||||
|
}
|
||||||
|
|
||||||
renderResult(result);
|
renderResult(result);
|
||||||
await renderSnapshot();
|
await renderSnapshot();
|
||||||
appendLog("Result angezeigt.");
|
appendLog("Result angezeigt.");
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
387
server/server.js
@@ -1,288 +1,147 @@
|
|||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
import https from 'https';
|
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import dotenv from 'dotenv';
|
import path from 'path';
|
||||||
import { WebSocket } from 'ws';
|
import fs from 'fs/promises';
|
||||||
import { EventEmitter } from 'events';
|
import { fileURLToPath } from 'url';
|
||||||
|
import process from 'process';
|
||||||
|
|
||||||
dotenv.config();
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json({ limit: '20mb' }));
|
||||||
|
|
||||||
const CERT_DIR = path.resolve('certs');
|
const PORT = parseInt(process.env.PORT || process.env.HTTPS_PORT || '2093', 10);
|
||||||
const KEY_PATH = path.join(CERT_DIR, 'localhost.key');
|
const publicDir = path.join(__dirname, '..', 'public');
|
||||||
const CRT_PATH = path.join(CERT_DIR, 'localhost.crt');
|
const snapshotsDir = path.join(publicDir, 'snapshots');
|
||||||
|
const WEBCAM_URL = process.env.WEBCAM_URL || '';
|
||||||
|
const BODYTRACKER_URL = process.env.BODYTRACKER_URL || '';
|
||||||
|
|
||||||
function loadHttpsCredentials() {
|
app.use(express.static(publicDir));
|
||||||
if (!fs.existsSync(KEY_PATH) || !fs.existsSync(CRT_PATH)) {
|
|
||||||
console.error(`HTTPS-Zertifikate fehlen in ${CERT_DIR}. Bitte 'npm install' ausführen (Postinstall generiert Zertifikate).`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
return { key: fs.readFileSync(KEY_PATH), cert: fs.readFileSync(CRT_PATH) };
|
|
||||||
}
|
|
||||||
|
|
||||||
const HTTPS_PORT = parseInt(process.env.HTTPS_PORT || '2033', 10);
|
app.get('/api/health', (req, res) => {
|
||||||
const WSS_URL = process.env.WSS_URL || 'wss://localhost:2096';
|
res.json({ ok: true, mode: 'backend-proxy', webcamUrl: WEBCAM_URL || null, bodyTrackerUrl: BODYTRACKER_URL || null });
|
||||||
const WSS_INSECURE_TLS = String(process.env.WSS_INSECURE_TLS || 'true').toLowerCase() === 'true';
|
|
||||||
|
|
||||||
// Nur bestimmte Kommandos erlauben (aus .env)
|
|
||||||
const allowedCommands = new Set(
|
|
||||||
(process.env.ALLOWED_COMMANDS || 'HOME,STOP,STATUS,RESET,PING,GCODEMOTOR')
|
|
||||||
.split(',')
|
|
||||||
.map(s => s.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Broadcaster für Server-Sent Events
|
|
||||||
const bus = new EventEmitter();
|
|
||||||
|
|
||||||
let wsDriver = null;
|
|
||||||
let wsState = {
|
|
||||||
connected: false,
|
|
||||||
lastError: null,
|
|
||||||
reconnectAttempts: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
function logAndBroadcast(level, message, data) {
|
|
||||||
const payload = { ts: new Date().toISOString(), level, message, data };
|
|
||||||
// Konsole
|
|
||||||
const line = `[${payload.ts}] [${level}] ${message}`;
|
|
||||||
//console.log(line, data ? data : '');
|
|
||||||
// SSE an Clients
|
|
||||||
bus.emit('event', JSON.stringify(payload));
|
|
||||||
}
|
|
||||||
|
|
||||||
function connectWss() {
|
|
||||||
if (wsDriver && (wsDriver.readyState === wsDriver.OPEN || wsDriver.readyState === wsDriver.CONNECTING)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tlsOptions = { rejectUnauthorized: !WSS_INSECURE_TLS };
|
|
||||||
logAndBroadcast('info', `Verbinde zu WSS: ${WSS_URL} (rejectUnauthorized=${tlsOptions.rejectUnauthorized})`);
|
|
||||||
|
|
||||||
wsDriver = new WebSocket(WSS_URL, tlsOptions);
|
|
||||||
|
|
||||||
wsDriver.on('open', () => {
|
|
||||||
wsState.connected = true;
|
|
||||||
wsState.lastError = null;
|
|
||||||
wsState.reconnectAttempts = 0;
|
|
||||||
logAndBroadcast('info', 'WSS Driver verbunden');
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
wsDriver.on('message', (data) => {
|
|
||||||
let text = '';
|
|
||||||
try { text = typeof data === 'string' ? data : data.toString('utf8'); } catch { text = '[binary data]'; }
|
|
||||||
|
|
||||||
logAndBroadcast('msg', 'Eingang von WSS', { text });
|
|
||||||
});
|
|
||||||
|
|
||||||
wsDriver.on('close', (code, reason) => {
|
|
||||||
wsState.connected = false;
|
|
||||||
logAndBroadcast('warn', `WSS getrennt (code=${code}, reason=${reason?.toString?.() || ''})`);
|
|
||||||
scheduleReconnect();
|
|
||||||
});
|
|
||||||
|
|
||||||
wsDriver.on('error', (err) => {
|
|
||||||
wsState.lastError = err?.message || String(err);
|
|
||||||
logAndBroadcast('error', 'WSS Fehler', { error: wsState.lastError });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function scheduleReconnect() {
|
|
||||||
wsState.reconnectAttempts += 1;
|
|
||||||
const delay = 10000; // 10s
|
|
||||||
logAndBroadcast('info', `Reconnecting in ${Math.round(delay/1000)}s...`);
|
|
||||||
setTimeout(connectWss, delay);
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTP API
|
|
||||||
app.get('/api/status', (req, res) => {
|
|
||||||
wsDriver.send("M114");
|
|
||||||
console.log("M114 gesendet, warte auf Antwort...");
|
|
||||||
res.json({
|
|
||||||
httpsPort: HTTPS_PORT,
|
|
||||||
wssUrl: WSS_URL,
|
|
||||||
connected: wsState.connected,
|
|
||||||
wsDriver: wsDriver ? wsDriver.readyState : null,
|
|
||||||
reconnectAttempts: wsState.reconnectAttempts,
|
|
||||||
lastError: wsState.lastError,
|
|
||||||
allowedCommands: Array.from(allowedCommands)
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/send', (req, res) => {
|
async function findLatestSnapshotFile() {
|
||||||
const { cmd, payload } = req.body || {};
|
const files = await fs.readdir(snapshotsDir);
|
||||||
if (!cmd || !allowedCommands.has(String(cmd).trim())) {
|
const entries = await Promise.all(
|
||||||
return res.status(400).json({ ok: false, error: 'Ungültiges oder nicht erlaubtes Kommando', allowed: Array.from(allowedCommands) });
|
files
|
||||||
}
|
.filter((name) => name.endsWith('.csv'))
|
||||||
if (!wsDriver || wsDriver.readyState !== wsDriver.OPEN) {
|
.map(async (name) => ({
|
||||||
return res.status(503).json({ ok: false, error: 'WSS nicht verbunden' });
|
name,
|
||||||
}
|
mtime: (await fs.stat(path.join(snapshotsDir, name))).mtime.valueOf()
|
||||||
const msg = { type: String(cmd).trim(), payload: payload ?? null };
|
}))
|
||||||
|
);
|
||||||
|
if (entries.length === 0) return null;
|
||||||
|
entries.sort((a, b) => b.mtime - a.mtime);
|
||||||
|
return entries[0].name;
|
||||||
|
}
|
||||||
|
|
||||||
if(msg.type==="STATUS"){
|
app.get('/api/latest-snapshot', async (req, res) => {
|
||||||
wsDriver.send("M114");
|
try {
|
||||||
logAndBroadcast('tx', 'Sende STATUS (M114) an WSS');
|
if (WEBCAM_URL) {
|
||||||
return res.json({ ok: true, sent: msg });
|
const url = new URL('/api/latest-snapshot', WEBCAM_URL).toString();
|
||||||
|
const fetchRes = await fetch(url);
|
||||||
|
const contentType = fetchRes.headers.get('content-type') || '';
|
||||||
|
|
||||||
|
if (!fetchRes.ok) {
|
||||||
|
const text = await fetchRes.text();
|
||||||
|
return res.status(fetchRes.status).type('text/plain').send(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(msg.type==="GCODEMOTOR"){
|
if (contentType.includes('application/json')) {
|
||||||
if(typeof msg.payload !== 'string' || !msg.payload.trim()){
|
const body = await fetchRes.json();
|
||||||
return res.status(400).json({ ok: false, error: 'Ungültiger Payload für GCODEMOTOR. Erwartet: String mit G-Code Befehl.' });
|
return res.json(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
wsDriver.send(msg.payload);
|
const text = await fetchRes.text();
|
||||||
console.log(`G-Code gesendet: ${msg.payload}`);
|
return res.json({ filename: 'latest.csv', mtime: new Date().toISOString(), content: text });
|
||||||
/*
|
|
||||||
msg.payload = msg.payload.trim();
|
|
||||||
var arrayMsg = msg.payload.split(' ').filter(s => s.trim());
|
|
||||||
if(arrayMsg.length === 0 || !['G0','G1','G28', 'M0', 'M1', 'M114'].includes(arrayMsg[0].toUpperCase())){
|
|
||||||
return res.status(400).json({ ok: false, error: 'Ungültiger G-Code Befehl. Nur G0, G1 und G28 sind erlaubt.' });
|
|
||||||
}
|
}
|
||||||
if(arrayMsg[1].toUpperCase().startsWith('X')){
|
|
||||||
wsDriver.send(`G0 ${arrayMsg[1].toUpperCase()} F1000`); // Schnelles Verfahren zu X-Position
|
const latestFile = await findLatestSnapshotFile();
|
||||||
console.log(`G0 ${arrayMsg[1].toUpperCase()} F1000 gesendet`);
|
if (!latestFile) {
|
||||||
|
return res.status(404).json({ error: 'Keine Snapshot-CSV-Datei gefunden' });
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
return res.json({ ok: true, sent: msg.payload});
|
const baseName = path.basename(latestFile, path.extname(latestFile));
|
||||||
|
const csvPath = path.join(snapshotsDir, latestFile);
|
||||||
|
const jsonPath = path.join(snapshotsDir, `${baseName}.json`);
|
||||||
|
const imagePath = path.join(snapshotsDir, `${baseName}_annotated.jpg`);
|
||||||
|
const imagePath2 = path.join(snapshotsDir, `${baseName}_annotated2.jpg`);
|
||||||
|
|
||||||
|
const content = await fs.readFile(csvPath, 'utf8');
|
||||||
|
const result = { filename: latestFile, mtime: (await fs.stat(csvPath)).mtime.toISOString(), content };
|
||||||
|
|
||||||
|
try {
|
||||||
|
result.jsonFile = { filename: `${baseName}.json`, content: await fs.readFile(jsonPath, 'utf8') };
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const jpg = await fs.readFile(imagePath);
|
||||||
|
result.imageFile = {
|
||||||
|
filename: path.basename(imagePath),
|
||||||
|
mimeType: 'image/jpeg',
|
||||||
|
contentBase64: jpg.toString('base64')
|
||||||
|
};
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const jpg2 = await fs.readFile(imagePath2);
|
||||||
|
result.image2 = {
|
||||||
|
filename: path.basename(imagePath2),
|
||||||
|
mimeType: 'image/jpeg',
|
||||||
|
contentBase64: jpg2.toString('base64')
|
||||||
|
};
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return res.json(result);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('latest-snapshot error:', err);
|
||||||
|
return res.status(500).json({ error: 'Fehler beim Laden des Snapshots', details: String(err) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/estimate', async (req, res) => {
|
||||||
|
if (!BODYTRACKER_URL) {
|
||||||
|
return res.status(501).json({ error: 'BODYTRACKER_URL ist nicht konfiguriert' });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
wsDriver.send(JSON.stringify(msg));
|
const { imageFile, image2, robotIntrinsics } = req.body;
|
||||||
logAndBroadcast('tx', 'Sende an WSS', msg);
|
const formData = new FormData();
|
||||||
return res.json({ ok: true, sent: msg });
|
|
||||||
|
if (imageFile?.contentBase64) {
|
||||||
|
const buffer = Buffer.from(imageFile.contentBase64, 'base64');
|
||||||
|
formData.append('images', new Blob([buffer], { type: imageFile.mimeType || 'image/jpeg' }), imageFile.filename || 'snapshot.jpg');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (image2?.contentBase64) {
|
||||||
|
const buffer2 = Buffer.from(image2.contentBase64, 'base64');
|
||||||
|
formData.append('images', new Blob([buffer2], { type: image2.mimeType || 'image/jpeg' }), image2.filename || 'snapshot2.jpg');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (robotIntrinsics) {
|
||||||
|
formData.append('intrinsics', new Blob([JSON.stringify(robotIntrinsics)], { type: 'application/json' }), 'intrinsics.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
const estimateUrl = new URL('/v1/estimate', BODYTRACKER_URL).toString();
|
||||||
|
const fetchRes = await fetch(estimateUrl, { method: 'POST', body: formData });
|
||||||
|
|
||||||
|
if (!fetchRes.ok) {
|
||||||
|
const message = await fetchRes.text();
|
||||||
|
return res.status(fetchRes.status).json({ error: 'BodyTracker-Fehler', details: message });
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await fetchRes.json();
|
||||||
|
return res.json(body);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logAndBroadcast('error', 'Senden an WSS fehlgeschlagen', { error: err?.message || String(err) });
|
console.error('estimate error:', err);
|
||||||
return res.status(500).json({ ok: false, error: 'Senden fehlgeschlagen' });
|
return res.status(500).json({ error: 'Fehler beim Aufruf des BodyTracker', details: String(err) });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// SSE-Endpoint
|
app.listen(PORT, () => {
|
||||||
app.get('/api/events', (req, res) => {
|
console.log(`appRobotHoming backend listening on port ${PORT}`);
|
||||||
res.setHeader('Content-Type', 'text/event-stream');
|
console.log(`WEBCAM_URL=${WEBCAM_URL || '<lokal>'}`);
|
||||||
res.setHeader('Cache-Control', 'no-cache');
|
console.log(`BODYTRACKER_URL=${BODYTRACKER_URL || '<nicht konfiguriert>'}`);
|
||||||
res.setHeader('Connection', 'keep-alive');
|
|
||||||
res.flushHeaders?.();
|
|
||||||
|
|
||||||
const send = (data) => {
|
|
||||||
res.write(`data: ${data}
|
|
||||||
|
|
||||||
`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const listener = (data) => send(data);
|
|
||||||
bus.on('event', listener);
|
|
||||||
|
|
||||||
// Initialstatus schicken
|
|
||||||
send(JSON.stringify({ ts: new Date().toISOString(), level: 'info', message: 'SSE verbunden' }));
|
|
||||||
|
|
||||||
req.on('close', () => {
|
|
||||||
bus.off('event', listener);
|
|
||||||
res.end();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
//snapshot_video0_1775319258906_two_cam.csv
|
|
||||||
//snapshot_video0_1775319258906_two_cam_annotated.jpg
|
|
||||||
|
|
||||||
// Neuester Snapshot-Endpunkt
|
|
||||||
app.get('/api/latest-snapshot', (req, res) => {
|
|
||||||
const snapshotsDir = path.join(path.resolve('public'), 'snapshots');
|
|
||||||
fs.readdir(snapshotsDir, (err, files) => {
|
|
||||||
if (err) {
|
|
||||||
return res.status(500).json({ error: 'Fehler beim Lesen des Snapshots-Verzeichnisses' });
|
|
||||||
}
|
|
||||||
const csvFiles = files.filter(file => file.endsWith('.csv')).map(file => ({
|
|
||||||
name: file,
|
|
||||||
path: path.join(snapshotsDir, file),
|
|
||||||
mtime: fs.statSync(path.join(snapshotsDir, file)).mtime
|
|
||||||
})).sort((a, b) => b.mtime - a.mtime);
|
|
||||||
|
|
||||||
if (csvFiles.length === 0) {
|
|
||||||
return res.status(404).json({ error: 'Keine CSV-Dateien gefunden' });
|
|
||||||
}
|
|
||||||
const latestFile = csvFiles[0];
|
|
||||||
const baseName = path.basename(latestFile.name, path.extname(latestFile.name));
|
|
||||||
const jsonFilename = `${baseName}.json`;
|
|
||||||
const jsonPath = path.join(snapshotsDir, jsonFilename);
|
|
||||||
|
|
||||||
console.log("JSON Pfad:", jsonPath);
|
|
||||||
console.log("Existiert JSON:", fs.existsSync(jsonPath));
|
|
||||||
|
|
||||||
const imageFilename = `${baseName}_annotated.jpg`;
|
|
||||||
const imagePath = path.join(snapshotsDir, imageFilename);
|
|
||||||
const imatePath2 = imagePath.includes('video0') ? imagePath.replace('video0', 'video1') : imagePath.replace('video1', 'video0');
|
|
||||||
|
|
||||||
//--
|
|
||||||
fs.readFile(latestFile.path, 'utf8', (err, data) => {
|
|
||||||
if (err) {
|
|
||||||
return res.status(500).json({ error: 'Fehler beim Lesen der Datei' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = {
|
|
||||||
filename: latestFile.name,
|
|
||||||
mtime: latestFile.mtime.toISOString(),
|
|
||||||
content: data
|
|
||||||
};
|
|
||||||
|
|
||||||
const jsonPath = path.join(snapshotsDir, jsonFilename);
|
|
||||||
|
|
||||||
// ✅ JSON FIRST, dann alles andere
|
|
||||||
fs.readFile(jsonPath, 'utf8', (jsonErr, jsonData) => {
|
|
||||||
if (!jsonErr && jsonData) {
|
|
||||||
response.jsonFile = {
|
|
||||||
filename: jsonFilename,
|
|
||||||
content: jsonData
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bild 1
|
|
||||||
fs.readFile(imagePath, { encoding: 'base64' }, (jpgErr, jpgBase64) => {
|
|
||||||
if (!jpgErr && jpgBase64) {
|
|
||||||
response.imageFile = {
|
|
||||||
filename: imageFilename,
|
|
||||||
mimeType: 'image/jpeg',
|
|
||||||
contentBase64: jpgBase64
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bild 2
|
|
||||||
fs.readFile(imatePath2, { encoding: 'base64' }, (jpgErr2, jpgBase642) => {
|
|
||||||
if (!jpgErr2 && jpgBase642) {
|
|
||||||
response.image2 = {
|
|
||||||
filename: path.basename(imatePath2),
|
|
||||||
mimeType: 'image/jpeg',
|
|
||||||
contentBase64: jpgBase642
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ jetzt erst senden
|
|
||||||
res.json(response);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
//--
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Statisches Frontend
|
|
||||||
app.use('/', express.static(path.resolve('public')));
|
|
||||||
|
|
||||||
// HTTPS-Server starten
|
|
||||||
const creds = loadHttpsCredentials();
|
|
||||||
const server = https.createServer({
|
|
||||||
key: creds.key,
|
|
||||||
cert: creds.cert,
|
|
||||||
}, app);
|
|
||||||
|
|
||||||
server.listen(HTTPS_PORT, () => {
|
|
||||||
logAndBroadcast('info', `HTTPS Server läuft auf https://localhost:${HTTPS_PORT}`);
|
|
||||||
// Nach Start WSS verbinden
|
|
||||||
connectWss();
|
|
||||||
});
|
});
|
||||||
|
|||||||