Konfig in robot.json
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,5 +1,9 @@
|
||||
# Node
|
||||
node_modules
|
||||
|
||||
# Robot-Config: Snapshots und generierter API-Key (nicht ins Repo)
|
||||
data/robot/robot_*.json
|
||||
data/robot/.apikey
|
||||
logs
|
||||
npm-debug.log
|
||||
yarn-debug.log
|
||||
|
||||
79
README.md
79
README.md
@@ -11,6 +11,8 @@ Dieses Projekt empfängt G-Code und Robotersteuerbefehle, berechnet Inverse Kine
|
||||
- `robot/GCode.js` verarbeitet G-Code, übersetzt ihn in Roboter-Koordinaten und triggert `robot.sendCommand()`.
|
||||
- `robot/RobotBase.js` ist die abstrakte Basisklasse / der Interface-Vertrag: generische Infrastruktur (Zustand, `sendCommand`, Motor-Geschwindigkeiten) plus die zwei abstrakten Kinematik-Methoden.
|
||||
- `robot/kinematics/Arm3SegmentLinearX.js` ist die konkrete Kinematik (Inverse + Vorwärts) für den aktuellen Arm. Die Auswahl der Kinematik erfolgt über `robot/KinematicsFactory.js` (Umgebungsvariablen `ROBOT_KINEMATICS` / `ROBOT_KINEMATICS_PARAMS`). Siehe `doc/ToDo_12_InverseKinematikConfig_ROADMAP.md`.
|
||||
- `robot/RobotConfig.js` liest `data/robot/robot.json` beim Start synchron und gibt einen typisierten Konfigurations-Record zurück (Kinematik-Parameter, Bewegungs-Defaults, Controller-Endpunkte). Env-Variablen überschreiben die JSON-Werte.
|
||||
- `server/RobotConfigService.js` stellt `GET/PUT /api/robot` und `GET /api/robot/history` über den InfoServer bereit (Single Source of Truth für alle Apps). Schreibzugriffe erfordern `Authorization: Bearer <ROBOT_API_KEY>`.
|
||||
- `robot/TelnetSenderGRBL.js` formatiert die Motor-Positionen in GRBL-kompatible Befehle und sendet sie per Telnet an einen Zielcontroller.
|
||||
|
||||
## Eingaben
|
||||
@@ -89,22 +91,65 @@ Die Eingaben kommen per WebSocket an den HTTPS-Server und werden in `server/Inpu
|
||||
- Interner Schalter, ob `robot.calculateSpeeds()` rechnet. Ändert allein **nicht** die
|
||||
Sender-Ausgabe — dafür ist `ROBOT_SPEED_MODE=correct` nötig. Vom Korrekt-Modus
|
||||
automatisch aktiviert.
|
||||
- `ROBOT_KINEMATICS`
|
||||
- Standard: `arm3segmentlinearx`
|
||||
- Bezeichner der Kinematik-Klasse (case-insensitive). Bekannte Werte: `arm3segmentlinearx`,
|
||||
`grabit` / `robot02` (Joy-IT Grab-It). Unbekannter Bezeichner → Fehler beim Start.
|
||||
- `ROBOT_KINEMATICS_PARAMS`
|
||||
- JSON-Objekt mit Konstruktor-Parametern, z. B. `{"l1":250,"l2":264,"l3":100}`.
|
||||
Überschreibt die Werte aus `robot.json`. Wird von der Factory als viertes Argument
|
||||
an den Konstruktor weitergereicht (erlaubt kinematik-spezifische Parameter wie `baseHeight`).
|
||||
- `ROBOT_API_KEY`
|
||||
- Statischer Bearer-Token für `PUT /api/robot`. Fehlt die Variable, generiert
|
||||
`RobotConfigService` beim ersten Start einen zufälligen Key und speichert ihn in
|
||||
`data/robot/.apikey` (nicht im Repo). Der Key wird beim Start einmalig geloggt.
|
||||
|
||||
### HTTPS-Konfiguration
|
||||
|
||||
- `https/localhost.key`
|
||||
- `https/localhost.pem`
|
||||
- Passphrase aktuell hart codiert als `abcd`
|
||||
- Passphrase: Env-Variable `HTTPS_PASSPHRASE`, Default `abcd`
|
||||
|
||||
### robot.json
|
||||
|
||||
`data/robot/robot.json` ist die zentrale Konfigurationsdatei für einen konkreten Roboter.
|
||||
Sie wird von `robot/RobotConfig.js` beim Start synchron gelesen. Fehlt die Datei, startet
|
||||
der Driver mit Fallback-Defaults und loggt eine Warnung.
|
||||
|
||||
Relevante Abschnitte für den Driver:
|
||||
|
||||
```json
|
||||
{
|
||||
"kinematics": { "type": "arm3segmentlinearx" },
|
||||
"motion": { "defaultFeedrate": 1000, "speedMode": "legacy" },
|
||||
"controllers": {
|
||||
"base": { "ip": "fluidNcBase.local", "port": 2300, "protocol": "telnet", "axes": ["x","y","z"] },
|
||||
"elbow": { "ip": "fluidNcEllbow.local", "port": 5000, "protocol": "telnet", "axes": ["a",null,null] },
|
||||
"hand": { "ip": "fluidNcHand.local", "port": 5000, "protocol": "telnet", "axes": ["c","e","b"] }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Armlängen werden aus dem `links`-Abschnitt abgeleitet (`Arm1.skeleton.to[1]` → l1 usw.).
|
||||
Env-Variablen haben Vorrang vor robot.json (nützlich für Tests und schnelle Korrekturen).
|
||||
|
||||
Snapshots werden automatisch vor jedem PUT angelegt (`data/robot/robot_YYYYMMDD_HHmmss.json`).
|
||||
Snapshots sind nicht im Repo (`.gitignore`), `robot.json` selbst schon.
|
||||
|
||||
### Telnet-Sender-Konfiguration
|
||||
|
||||
In `startRobot.js` werden drei `TelnetSenderGRBL` Instanzen erzeugt:
|
||||
`startRobot.js` erzeugt die `TelnetSenderGRBL`-Instanzen dynamisch aus `cfg.controllers`
|
||||
(geladen von `robot/RobotConfig.js`). Die Defaults entsprechen drei Controllern:
|
||||
|
||||
- `telnetSender1` → Basisachse(n): `x`, `y`, `z`
|
||||
- `telnetSender2` → Ellbogenachse: `a`
|
||||
- `telnetSender3` → Handachsen: `c`, `e`, `b`
|
||||
| Key | Default-IP | Port | Achsen |
|
||||
|-----|-----------|------|--------|
|
||||
| `base` | `fluidNcBase.local` | 2300 | `x, y, z` |
|
||||
| `elbow` | `fluidNcEllbow.local` | 5000 | `a` |
|
||||
| `hand` | `fluidNcHand.local` | 5000 | `c, e, b` |
|
||||
|
||||
Die Achszuordnung kann in `robot/TelnetSenderGRBL.js` durch Anpassung der Konstruktorparameter geändert werden.
|
||||
IPs können per Env-Variable überschrieben werden (`GRBL_BASE_IP`, `GRBL_ELLBOW_IP`,
|
||||
`GRBL_HAND_IP`). Alles andere (Port, Achsen, Controller-Anzahl) wird in `robot.json`
|
||||
konfiguriert.
|
||||
|
||||
## Serverschnittstellen
|
||||
|
||||
@@ -124,6 +169,9 @@ Die Achszuordnung kann in `robot/TelnetSenderGRBL.js` durch Anpassung der Konstr
|
||||
- API-Endpunkte:
|
||||
- `/api/status`
|
||||
- `/api/position`
|
||||
- `/api/robot` — `GET`: aktuelle `robot.json`; `PUT`: überschreibt sie (Auth erforderlich)
|
||||
- `/api/robot/history` — Liste aller Snapshots
|
||||
- `/api/robot/history/:ts` — einen bestimmten Snapshot abrufen
|
||||
|
||||
## Wichtige Dateien
|
||||
|
||||
@@ -134,6 +182,9 @@ Die Achszuordnung kann in `robot/TelnetSenderGRBL.js` durch Anpassung der Konstr
|
||||
- `robot/kinematics/Arm3SegmentLinearX.js` — konkrete Kinematik (Modell + Inverse/Vorwärts), Default-Arm
|
||||
- `robot/kinematics/Arm3SegmentRotaryBase.js` — Kinematik für den Joy-IT „Grab-It" (Robot02), 5 Achsen + Greifer mit Drehbasis (`ROBOT_KINEMATICS=grabit`)
|
||||
- `robot/KinematicsFactory.js` — wählt die Kinematik per Umgebungsvariable
|
||||
- `robot/RobotConfig.js` — liest `data/robot/robot.json`, gibt typisierten Konfigurations-Record zurück
|
||||
- `server/RobotConfigService.js` — REST-Endpunkte `/api/robot*` (lesen/schreiben, Snapshots, Auth)
|
||||
- `data/robot/robot.json` — zentrale Roboter-Konfiguration (Single Source of Truth)
|
||||
- `robot/GCodeParser.js` — wandelt rohe Nachrichten in strukturierte Befehlsobjekte
|
||||
- `robot/RobotController.js` — wendet geparste Befehle auf das Modell an (Steuerlogik)
|
||||
- `robot/GCode.js` — Fassade + Datei-Befehle
|
||||
@@ -143,9 +194,10 @@ Die Achszuordnung kann in `robot/TelnetSenderGRBL.js` durch Anpassung der Konstr
|
||||
|
||||
## Laufzeitvoraussetzungen
|
||||
|
||||
- Das Verzeichnis `logs/` muss im Arbeitsverzeichnis existieren, da `InputWS.js` dort `pings.log` und `gcode_commands.log` schreibt.
|
||||
- HTTPS-Zertifikate: `https/localhost.key` und `https/localhost.pem` (Passphrase aktuell hardcoded `abcd`).
|
||||
- Die Telnet-Sender werden erst nach 5 Sekunden zum Roboter hinzugefügt, damit die Verbindungen Zeit haben aufzubauen.
|
||||
- HTTPS-Zertifikate: `https/localhost.key` und `https/localhost.pem` (Passphrase via `HTTPS_PASSPHRASE`, Default `abcd`).
|
||||
- `data/robot/robot.json` — wird beim Start eingelesen; fehlt die Datei, startet der Driver mit Defaults + Warnung.
|
||||
- `logs/` wird beim Start automatisch angelegt (`fs.mkdirSync('logs', { recursive: true })` in `startRobot.js`).
|
||||
- Telnet-Sender werden sofort beim Start als `cmdReceivers` registriert; interne Reconnect-Logik überbrückt verzögerte Controller-Verbindungen automatisch.
|
||||
|
||||
## ToDo / Open Tasks
|
||||
|
||||
@@ -163,9 +215,10 @@ Architektur- und Refactoring-Aufgaben sind in `doc/ToDo_*.md` dokumentiert:
|
||||
| `doc/ToDo_6b_FileHandling.md` | File-Handling: fehlende Befehle, Cursor im Speicher, Fehler-Feedback | offen |
|
||||
| `doc/ToDo_7_Tests.md` | Testabdeckung und Stabilität | teilweise |
|
||||
| `doc/ToDo_8_Bugs.md` | Bekannte konkrete Bugs | teilweise |
|
||||
| `doc/ToDo_9_HardwareFeedback.md` | Hardware-Feedback-Loop (GRBL-Antworten, Command-Queue, Positionsabgleich) | offen |
|
||||
| `doc/ToDo_9_HardwareFeedback.md` | Hardware-Feedback-Loop (GRBL-Antworten, Command-Queue, Positionsabgleich) | teilweise (Baustein Port→Motor ✅, Pakete 1–6 offen) |
|
||||
| `doc/ToDo_10_VerbindungsVerlust.md` | Verbindungsverlust erkennen, Watchdog, UI-Statusanzeige | offen |
|
||||
| `doc/ToDo_12_InverseKinematikConfig_ROADMAP.md` | Austauschbare Kinematik: RobotBase + Robot7M, Env-Konfiguration | offen |
|
||||
| `doc/ToDo_12_InverseKinematikConfig_ROADMAP.md` | Austauschbare Kinematik: RobotBase, KinematicsFactory, Grab-It | ✅ erledigt |
|
||||
| `doc/ToDo_14_robot_json_service.md` | robot.json als REST-Service, RobotConfigService, RobotConfig | teilweise (Schritte 1–4 in appRobotDriver ✅, Schritte 5–7 offen) |
|
||||
| `doc/ToDo_49_Cleanup.md` | Pre-Release-Cleanup: tote Code, Zertifikate, ToDos, README | offen |
|
||||
|
||||
### Empfohlene Bearbeitungsreihenfolge
|
||||
@@ -191,5 +244,5 @@ Kurzübersicht weiterer offener Punkte:
|
||||
- [ ] `FFirst`/`FLast`-Befehle in `GCode.receiveFC()` implementieren
|
||||
- [ ] `ROBOT_USE_SPEED_CALC` und `motorSpeeds` im echten Betrieb prüfen
|
||||
- [ ] `FluidNCClient.js` evaluieren: als Ersatz oder Ergänzung zu `TelnetSenderGRBL`?
|
||||
- [ ] HTTPS-Konfiguration und Zertifikatsverwaltung verbessern (Passphrase aus Env-Variable)
|
||||
- [ ] `logs/`-Verzeichnis beim Start automatisch anlegen
|
||||
- [x] HTTPS-Passphrase aus Env-Variable (`HTTPS_PASSPHRASE`) — erledigt
|
||||
- [x] `logs/`-Verzeichnis beim Start automatisch anlegen — erledigt (`startRobot.js`)
|
||||
|
||||
0
data/robot/.gitkeep
Normal file
0
data/robot/.gitkeep
Normal file
502
data/robot/robot.json
Normal file
502
data/robot/robot.json
Normal file
@@ -0,0 +1,502 @@
|
||||
{
|
||||
"_label": "todo3_2026-06-11",
|
||||
"coordinateSystem": {"handedness": "right", "x": "right", "y": "backward", "z": "up"},
|
||||
"units": {"_owner": "appRobotDriver", "length": "mm", "rotation": "degree"},
|
||||
"kinematics": {
|
||||
"_owner": "appRobotDriver",
|
||||
"type": "arm3segmentlinearx"
|
||||
},
|
||||
"motion": {
|
||||
"_owner": "appRobotDriver",
|
||||
"defaultFeedrate": 2300,
|
||||
"speedMode": "legacy",
|
||||
"speedModeOptions": ["legacy", "correct"]
|
||||
},
|
||||
"controllers": {
|
||||
"_owner": "appRobotDriver",
|
||||
"base": { "ip": "fluidNcBase.local", "port": 2300, "protocol": "telnet", "axes": ["x", "y", "z"] },
|
||||
"elbow": { "ip": "fluidNcEllbow.local", "port": 5000, "protocol": "telnet", "axes": ["a", null, null] },
|
||||
"hand": { "ip": "fluidNcHand.local", "port": 5000, "protocol": "telnet", "axes": ["c", "e", "b"] }
|
||||
},
|
||||
"vision_config": {"MarkerType": "DICT_4X4_250", "MarkerSize": 0.025},
|
||||
"renderingInfo": {
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"renderDefaults": {"width": 1280, "height": 720, "dofFStop": 11},
|
||||
"cameraPosition__1": [-10, -800, 500],
|
||||
"cameraPosition__2": [-500, 300, 1200],
|
||||
"cameraPosition__3": [-200, -900, 200],
|
||||
"cameraPosition__4": [1200, 200, 300],
|
||||
"cameraPosition_a": [-300, -800, 500],
|
||||
"cameraPosition": [-200, 200, 1400],
|
||||
"cameraPosition_c": [600, -500, 600],
|
||||
"cameraTarget": [200, -200, 180],
|
||||
"cameraUpVector": [0, 0, 1],
|
||||
"lightPosition": [-500, -500, 500],
|
||||
"lightTarget": [0, 0, 0],
|
||||
"lightUpVector": [0, 0, 1],
|
||||
"metric": "mm",
|
||||
"showSkeleton": true,
|
||||
"showMarkers": true,
|
||||
"backgroundColor": [0.7, 0.85, 1.0],
|
||||
"backgroundStrength": 0.2,
|
||||
"sunEnergy": 0.35,
|
||||
"areaEnergy": 120,
|
||||
"exposure": -1.5,
|
||||
"lensDirt": true,
|
||||
"lensDirtStrength": 0.08,
|
||||
"dofEnabled": true,
|
||||
"dofFStop": 11.0,
|
||||
"arucoDust": true,
|
||||
"arucoDustStrength": 1.6,
|
||||
"markerOffsetMaxMm": 4.0,
|
||||
"markerOffsetSeed": 0,
|
||||
"markerRotationMaxDeg": 3,
|
||||
"motionBlur": true,
|
||||
"motionBlurMaxPx": 5.5,
|
||||
"focalErrorPct": 0.5,
|
||||
"principalErrorPx": 3.0,
|
||||
"residualDistortion": [0.02, -0.01],
|
||||
"localizedBlur": false,
|
||||
"localizedBlurStrength": 0.15,
|
||||
"vignette": true,
|
||||
"vignetteStrength": 0.08,
|
||||
"sensorNoise": true,
|
||||
"sensorNoiseStrength": 0.01,
|
||||
"lensDistortion": true,
|
||||
"lensDistortionStrength": 0.002,
|
||||
"materials": {
|
||||
"wood": {"baseColor": [0.72, 0.52, 0.33], "roughness": 0.85, "metallic": 0.0},
|
||||
"plaWhite": {"baseColor": [0.95, 0.95, 0.95], "roughness": 0.45, "metallic": 0.0},
|
||||
"steel": {"baseColor": [0.72, 0.72, 0.75], "roughness": 0.25, "metallic": 1.0},
|
||||
"powderCoatBlue": {"baseColor": [0.15, 0.25, 0.7], "roughness": 0.55, "metallic": 0.0},
|
||||
"defaultPlastic": {"baseColor": [0.95, 0.95, 0.95], "roughness": 0.4, "metallic": 0.0},
|
||||
"skeletonRed": {"baseColor": [0.85, 0.2, 0.2], "roughness": 0.35, "metallic": 0.0},
|
||||
"markerBlack": {"baseColor": [0.04, 0.04, 0.04], "roughness": 0.8, "metallic": 0.0}
|
||||
},
|
||||
"skeletonDefaults": {"radius": 4, "color": [0.85, 0.2, 0.2]},
|
||||
"markerDefaults": {"size": 25, "thickness": 1, "color": [0.04, 0.04, 0.04]},
|
||||
"defaultPosition": {"x": 80, "y": 20, "z": 80, "a": -120, "b": 23, "c": 9, "e": 3}
|
||||
},
|
||||
"defaultPosition__": {"x": 10, "y": 4, "z": 20, "a": 10, "b": 2, "c": 9, "e": 1},
|
||||
"defaultPosition": {"x": 50, "y": 4, "z": 176, "a": 20, "b": 60, "c": 9, "e": 5},
|
||||
"recognized": {"x": null, "y": null, "z": null, "a": null, "b": null, "c": null, "e": null},
|
||||
"constraint_rules": {
|
||||
"rigid_distance": {"enabled": true, "mode": "mst", "weight": 1.0},
|
||||
"joint_axis_projection": {"enabled": true, "max_pairs": 2, "weight": 0.35},
|
||||
"chain_axis_projection": {"enabled": false, "max_depth": 3, "max_pairs": 2, "weight": 0.15},
|
||||
"axis_alignment_threshold": 0.95
|
||||
},
|
||||
"observation_weighting": {"enabled": true, "distance_weight": true, "marker_size_weight": true, "view_angle_weight": true},
|
||||
"multiview_calculation": {
|
||||
"combine_mode": "mean",
|
||||
"size_ref_px": 50.0,
|
||||
"border_ref_px": 120.0,
|
||||
"center_ref_norm": 0.01,
|
||||
"sharpness_ref": 2500.0,
|
||||
"homography_ref": 0.18,
|
||||
"size_factor": 0.3,
|
||||
"aspect_factor": 0.3,
|
||||
"border_factor": 0.01,
|
||||
"center_factor": 0.01,
|
||||
"sharpness_factor": 0.5,
|
||||
"homography_factor": 0.2,
|
||||
"normal_visibility_factor": 0.01,
|
||||
"spin_factor": 0.3,
|
||||
"weight_floor": 0.3
|
||||
},
|
||||
"pose_estimation": {
|
||||
"method": "hybrid",
|
||||
"marker_observation": "corner_pose",
|
||||
"use_normals": true,
|
||||
"normal_weight": 100.0,
|
||||
"robust_loss": "huber",
|
||||
"huber_delta_mm": 8.0,
|
||||
"max_iterations": 200,
|
||||
"min_cameras_per_marker": 2,
|
||||
"finger_block_joints": ["b", "c", "e"],
|
||||
"per_link_method": {}
|
||||
},
|
||||
"robot_test_poses": {
|
||||
"4": {"x": 70, "y": 50, "z": -70, "a": 120, "b": 50, "c": 30, "e": 20},
|
||||
"5": {"x": 180, "y": 86, "z": -120, "a": -60, "b": 22, "c": 91, "e": 10},
|
||||
"6": {"x": 80, "y": 20, "z": 80, "a": -120, "b": 23, "c": 9, "e": 3},
|
||||
"7": {"x": 30, "y": -2, "z": 95, "a": 20, "b": 23, "c": 9, "e": 9},
|
||||
"8": {"x": 50, "y": -2, "z": 95, "a": 20, "b": 60, "c": 9, "e": 3},
|
||||
"9": {"x": 60, "y": -2, "z": 95, "a": 200, "b": 60, "c": 9, "e": 8},
|
||||
"9a": {
|
||||
"x": 60,
|
||||
"y": -2,
|
||||
"z": 95,
|
||||
"a": 200,
|
||||
"b": 60,
|
||||
"c": 9,
|
||||
"e": 8,
|
||||
"rendering": {"width": 1440, "height": 1080, "dofFStop": 11}
|
||||
},
|
||||
"9b": {
|
||||
"x": 60,
|
||||
"y": -2,
|
||||
"z": 95,
|
||||
"a": 200,
|
||||
"b": 60,
|
||||
"c": 9,
|
||||
"e": 8,
|
||||
"rendering": {"width": 4896, "height": 3264, "dofFStop": 5.6}
|
||||
},
|
||||
"10": {"x": 120, "y": 60, "z": -110, "a": 20, "b": 30, "c": 180, "e": 4},
|
||||
"11": {"x": 50, "y": 4, "z": 176, "a": 20, "b": 60, "c": 9, "e": 5},
|
||||
"12": {"x": 50, "y": 0, "z": 178, "a": 210, "b": 80, "c": 90, "e": 6}
|
||||
},
|
||||
"test_camera_positions": {
|
||||
"a": [-300, -800, 800],
|
||||
"b": [300, -900, 1200],
|
||||
"c": [300, -900, 400],
|
||||
"d": [700, -800, 400],
|
||||
"e": [1200, -900, 400],
|
||||
"f": [500, -300, 1400],
|
||||
"g": [-200, 200, 1400]
|
||||
},
|
||||
"test_camera_targets": {
|
||||
"a": [210, -100, 180],
|
||||
"b": [310, -80, 180],
|
||||
"c": [210, -100, 150],
|
||||
"d": [210, -100, 150],
|
||||
"e": [210, -100, 50],
|
||||
"f": [200, -200, 180],
|
||||
"g": [200, -200, 180]
|
||||
},
|
||||
"movements": {"x": null, "y": null, "z": null, "a": null, "b": null, "c": null, "e": null},
|
||||
"state_pose_params": {
|
||||
"numbers_of_Elements_to_consider_start": 3,
|
||||
"numbers_of_Elements_to_consider_final": 5,
|
||||
"solver_in_between_geometrical": false,
|
||||
"solver_after_geometrical": false,
|
||||
"geometric_passes_per_stage": 2,
|
||||
"revolute_search_coarse_deg": 5.0,
|
||||
"revolute_search_fine_deg": 1.0,
|
||||
"root_pose_min_markers": 3,
|
||||
"use_marker_normals_flip_tiebreak": true,
|
||||
"normal_flip_weight": 0.05
|
||||
},
|
||||
"links": {
|
||||
"_owner": "appRobotDriver",
|
||||
"Board": {
|
||||
"parent": null,
|
||||
"size": [1000, 200, 25],
|
||||
"mountPosition": [0, 0, 0],
|
||||
"mountRotation": [0, 0, 0],
|
||||
"skeleton": {"from": [0, 0, 16], "to": [1000, 0, 16], "radius": 4, "color": [0.85, 0.2, 0.2]},
|
||||
"markers": [
|
||||
{"id": 210, "set": "Brett", "position": [20, -20, 0.3], "normal": [0, 0, 1]},
|
||||
{"id": 211, "set": "Brett", "position": [250, -10, 0.3], "normal": [0, 0, 1]},
|
||||
{"id": 215, "set": "Brett", "position": [250, -90, 0.3], "normal": [0, 0, 1]},
|
||||
{"id": 214, "set": "Brett", "position": [350, -10, 0.3], "normal": [0, 0, 1]},
|
||||
{"id": 208, "set": "Brett", "position": [350, -90, 0.3], "normal": [0, 0, 1]},
|
||||
{"id": 206, "set": "Brett", "position": [650, -10, 0.3], "normal": [0, 0, 1]},
|
||||
{"id": 205, "set": "Brett", "position": [750, -90, 0.3], "normal": [0, 0, 1]},
|
||||
{"id": 207, "set": "Brett", "position": [750, -10, 0.3], "normal": [0, 0, 1]},
|
||||
{"id": 217, "set": "Brett", "position": [650, -90, 0.3], "normal": [0, 0, 1]},
|
||||
{
|
||||
"id": 46,
|
||||
"set": "A0",
|
||||
"position": [536.71, 185.44, -27.3],
|
||||
"normal": [0, 0, 1],
|
||||
"spin": 90,
|
||||
"info": "is placed on a white paper, A0_60Arucos_25mm_Seet223.pdf, with the following marker placements:"
|
||||
},
|
||||
{"id": 47, "set": "A0", "position": [344.23, -286.54, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 48, "set": "A0", "position": [688.69, -320.72, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 49, "set": "A0", "position": [1006.0, 158.33, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 50, "set": "A0", "position": [573.41, 211.86, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 51, "set": "A0", "position": [167.8, -172.08, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 52, "set": "A0", "position": [94.68, 208.66, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 53, "set": "A0", "position": [486.25, 212.24, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 54, "set": "A0", "position": [342.27, -330.59, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 55, "set": "A0", "position": [283.72, -262.58, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 56, "set": "A0", "position": [498.68, 168.67, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 57, "set": "A0", "position": [602.86, -364.05, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 58, "set": "A0", "position": [50.09, -218.11, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 59, "set": "A0", "position": [626.21, -278.75, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 60, "set": "A0", "position": [434.36, 283.81, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 61, "set": "A0", "position": [-22.42, 335.83, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 62, "set": "A0", "position": [404.7, -175.1, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 63, "set": "A0", "position": [777.4, -236.15, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 64, "set": "A0", "position": [-21.27, -188.23, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 65, "set": "A0", "position": [803.39, -297.37, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 66, "set": "A0", "position": [209.75, -363.23, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 67, "set": "A0", "position": [523.07, 267.04, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 68, "set": "A0", "position": [573.73, 170.64, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 69, "set": "A0", "position": [7.61, -281.21, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 70, "set": "A0", "position": [601.87, 300.33, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 71, "set": "A0", "position": [749.75, -284.01, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 72, "set": "A0", "position": [440.99, 194.32, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 73, "set": "A0", "position": [221.73, 333.11, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 74, "set": "A0", "position": [93.78, 144.5, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 75, "set": "A0", "position": [-25.7, 194.58, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 76, "set": "A0", "position": [685.21, 166.8, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 77, "set": "A0", "position": [18.19, 191.57, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 78, "set": "A0", "position": [823.11, -344.38, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 79, "set": "A0", "position": [312.3, -159.11, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 80, "set": "A0", "position": [863.59, -335.92, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 81, "set": "A0", "position": [132.14, 169.03, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 82, "set": "A0", "position": [219.16, 297.24, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 83, "set": "A0", "position": [44.16, 339.22, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 84, "set": "A0", "position": [407.49, 258.42, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 85, "set": "A0", "position": [504.58, -312.75, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 86, "set": "A0", "position": [362.89, 292.01, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 87, "set": "A0", "position": [943.63, -245.76, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 88, "set": "A0", "position": [765.87, 316.04, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 89, "set": "A0", "position": [988.02, -369.14, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 90, "set": "A0", "position": [643.17, 316.43, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 91, "set": "A0", "position": [723.35, 328.05, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 92, "set": "A0", "position": [645.09, -184.84, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 93, "set": "A0", "position": [934.88, 143.6, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 94, "set": "A0", "position": [875.7, 173.65, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 95, "set": "A0", "position": [186.04, -274.07, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 96, "set": "A0", "position": [369.77, -186.49, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 97, "set": "A0", "position": [304.35, -359.67, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 98, "set": "A0", "position": [575.27, 315.06, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 99, "set": "A0", "position": [959.16, -321.55, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 100, "set": "A0", "position": [803.25, 172.36, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 101, "set": "A0", "position": [117.7, 298.66, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 102, "set": "A0", "position": [649.69, -223.0, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 103, "set": "A0", "position": [105.71, -187.71, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 104, "set": "A0", "position": [826.71, 239.16, -27.3], "normal": [0, 0, 1], "spin": 90},
|
||||
{"id": 105, "set": "A0", "position": [524.84, -266.25, -27.3], "normal": [0, 0, 1], "spin": 90}
|
||||
],
|
||||
"model": [
|
||||
{
|
||||
"stlFile": "surfaces/Board.stl",
|
||||
"originOfModel": [0, 0, 0],
|
||||
"rotationOfModelDegree": [0, 0, -90],
|
||||
"material": "wood"
|
||||
},
|
||||
{
|
||||
"stlFile": "surfaces/BoardRail.stl",
|
||||
"originOfModel": [0, 0, 0],
|
||||
"rotationOfModelDegree": [0, 0, -90],
|
||||
"material": "steel"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Base": {
|
||||
"parent": "Board",
|
||||
"size": [150, 200, 150],
|
||||
"mountPosition": [0, 0, 0],
|
||||
"mountRotation": [0, 0, 0],
|
||||
"jointToParent": {
|
||||
"name": "Slider",
|
||||
"type": "linear",
|
||||
"axis": [1, 0, 0],
|
||||
"origin": [0, 0, 16],
|
||||
"rotation": [0, 0, 0],
|
||||
"variable": "x",
|
||||
"feedrate": 2000,
|
||||
"controller": "base"
|
||||
},
|
||||
"skeleton": {"from": [0, 108, 45], "to": [110, 108, 45], "radius": 4, "color": [0.2, 0.8, 0.2]},
|
||||
"markers": [],
|
||||
"model": [
|
||||
{
|
||||
"stlFile": "surfaces/Base.stl",
|
||||
"originOfModel": [-30, 0, -35],
|
||||
"rotationOfModelDegree": [0, 0, 0],
|
||||
"material": "plaWhite"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Arm1": {
|
||||
"parent": "Base",
|
||||
"size": [70, 250, 70],
|
||||
"mountPosition": [0, 0, 0],
|
||||
"mountRotation": [0, 0, 0],
|
||||
"jointToParent": {
|
||||
"name": "Joint1",
|
||||
"type": "revolute",
|
||||
"axis": [-1, 0, 0],
|
||||
"origin": [110, 108, 45],
|
||||
"rotation": [0, 0, 0],
|
||||
"variable": "y",
|
||||
"feedrate": 2300,
|
||||
"controller": "base"
|
||||
},
|
||||
"skeleton": {"from": [0, 0, 0], "to": [0, -250, 0], "radius": 4, "color": [0.2, 0.2, 0.9]},
|
||||
"markers": [
|
||||
{"id": 198, "name": "aruco_198", "position": [0, -160, 35], "normal": [0, 0, 1], "size": 25, "spin": 0},
|
||||
{"id": 229, "name": "aruco_229", "position": [0, -250, 35], "normal": [0, 0, 1], "size": 25, "spin": 0},
|
||||
{"id": 242, "name": "aruco_242", "position": [0, -250, -35], "normal": [0, 0, -1], "size": 25, "spin": 0},
|
||||
{"id": 243, "name": "aruco_243", "position": [0, -285, 0], "normal": [0, -1, 0], "size": 25, "spin": 0}
|
||||
],
|
||||
"model": [
|
||||
{
|
||||
"stlFile": "surfaces/Holm.stl",
|
||||
"originOfModel__": [-25, 29, -28.5],
|
||||
"originOfModel": [-29, 25, 28.5],
|
||||
"rotationOfModelDegree__": [0, 0, 0],
|
||||
"rotationOfModelDegree": [180, 0, -90],
|
||||
"material": "powderCoatBlue"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Ellbow": {
|
||||
"parent": "Arm1",
|
||||
"mountPosition": [0, 0, 0],
|
||||
"mountRotation": [0, 0, 0],
|
||||
"jointToParent": {
|
||||
"name": "Joint2",
|
||||
"type": "revolute",
|
||||
"axis": [-1, 0, 0],
|
||||
"origin": [0, -250, 0],
|
||||
"rotation": [0, 0, 0],
|
||||
"variable": "z",
|
||||
"feedrate": 2300,
|
||||
"controller": "base"
|
||||
},
|
||||
"skeleton": {"from": [0, 0, 0], "to": [90, 0, 0], "radius": 4, "color": [0.9, 0.2, 0.2]},
|
||||
"model": [
|
||||
{
|
||||
"stlFile": "surfaces/Ellebogen.stl",
|
||||
"originOfModel": [90, 0, 0],
|
||||
"rotationOfModelDegree": [0, -90, -90],
|
||||
"material": "defaultPlastic"
|
||||
}
|
||||
],
|
||||
"markers": [
|
||||
{"id": 244, "name": "aruco_244", "position": [125, 0, 0], "normal": [1, 0, 0], "size": 25, "spin": 0},
|
||||
{"id": 245, "name": "aruco_245", "position": [90, 0, -35], "normal": [0, 0, -1], "size": 25, "spin": 0},
|
||||
{"id": 246, "name": "aruco_246", "position": [90, 0, 35], "normal": [0, 0, 1], "size": 25},
|
||||
{"id": 247, "name": "aruco_247", "position": [52.5, 0, 35], "normal": [0, 0, 1], "size": 25},
|
||||
{"id": 248, "name": "aruco_248", "position": [52.5, 0, -35], "normal": [0, 0, -1], "size": 25},
|
||||
{"id": 232, "name": "aruco_232", "position": [90, 24.75, -24.75], "normal": [0, 1, -1], "size": 25},
|
||||
{"id": 231, "name": "aruco_231", "position": [90, 24.75, 24.75], "normal": [0, 1, 1], "size": 25}
|
||||
]
|
||||
},
|
||||
"Arm2": {
|
||||
"parent": "Ellbow",
|
||||
"mountPosition": [0, 0, 0],
|
||||
"mountRotation": [0, 0, 0],
|
||||
"jointToParent": {
|
||||
"name": "Joint3",
|
||||
"type": "revolute",
|
||||
"axis": [0, -1, 0],
|
||||
"origin": [90, 0, 0],
|
||||
"rotation": [0, 0, 0],
|
||||
"variable": "a",
|
||||
"feedrate": 2300,
|
||||
"controller": "elbow"
|
||||
},
|
||||
"skeleton": {"from": [0, 0, 0], "to": [0, -250, 0], "radius": 4, "color": [0.95, 0.85, 0.2]},
|
||||
"model": [
|
||||
{
|
||||
"stlFile": "surfaces/Unterarm.stl",
|
||||
"originOfModel": [0, -250, 0],
|
||||
"rotationOfModelDegree": [180, 0, -90],
|
||||
"material": "defaultPlastic"
|
||||
}
|
||||
],
|
||||
"markers": [
|
||||
{"id": 120, "position": [24.75, -112, -24.75], "normal": [1, 0, -1]},
|
||||
{"id": 122, "name": "aruco_122", "position": [-35, -112, 0], "normal": [-1, 0, 0]},
|
||||
{"id": 218, "name": "aruco_218", "position": [35, -112, 0], "normal": [1, 0, 0]},
|
||||
{"id": 113, "name": "aruco_113", "position": [0, -182, 30], "normal": [0, 0, 1]},
|
||||
{"id": 114, "name": "aruco_114", "position": [24.75, -182, -24.75], "normal": [1, 0, -1]},
|
||||
{"id": 115, "name": "aruco_115", "position": [-24.75, -182, -24.75], "normal": [-1, 0, -1]},
|
||||
{"id": 124, "name": "aruco_124", "position": [-35, -219, 0], "normal": [-1, 0, 0]},
|
||||
{"id": 219, "name": "aruco_219", "position": [35, -219, 0], "normal": [1, 0, 0]}
|
||||
]
|
||||
},
|
||||
"Hand": {
|
||||
"parent": "Arm2",
|
||||
"mountPosition": [0, 0, 0],
|
||||
"mountRotation": [0, 0, 0],
|
||||
"jointToParent": {
|
||||
"name": "Joint4",
|
||||
"type": "revolute",
|
||||
"axis": [1, 0, 0],
|
||||
"origin": [0, -250, 0],
|
||||
"rotation": [0, 0, 0],
|
||||
"variable": "b",
|
||||
"feedrate": 2300,
|
||||
"controller": "hand"
|
||||
},
|
||||
"skeleton": {"from": [0, 0, 0], "to": [0, -35, 0], "radius": 4, "color": [0.95, 0.55, 0.15]}
|
||||
},
|
||||
"Palm": {
|
||||
"parent": "Hand",
|
||||
"mountPosition": [0, 0, 0],
|
||||
"mountRotation": [0, 0, 0],
|
||||
"jointToParent": {
|
||||
"name": "Joint3",
|
||||
"type": "revolute",
|
||||
"axis": [0, -1, 0],
|
||||
"origin": [0, 0, 0],
|
||||
"rotation": [0, 0, 0],
|
||||
"variable": "c",
|
||||
"feedrate": 2300,
|
||||
"controller": "hand"
|
||||
},
|
||||
"skeleton": {"from": [-50, -35, 0], "to": [50, -35, 0], "radius": 7, "color": [0.95, 0.2, 0.2]}
|
||||
},
|
||||
"FingerA": {
|
||||
"parent": "Palm",
|
||||
"size": [80, 60, 20],
|
||||
"mountPosition": [0, 0, 0],
|
||||
"mountRotation": [0, 0, 0],
|
||||
"jointToParent": {
|
||||
"name": "Slider",
|
||||
"type": "linear",
|
||||
"axis": [1, 0, 0],
|
||||
"origin": [4, -35, 0],
|
||||
"rotation": [0, 0, 0],
|
||||
"variable": "e",
|
||||
"feedrate": 2000,
|
||||
"controller": "hand"
|
||||
},
|
||||
"skeleton": {"from": [0, 0, 0], "to": [0, -60, 0], "radius": 4, "color": [0.2, 0.8, 0.2]},
|
||||
"markers": [
|
||||
{"id": 40, "position": [12, -24, -17.1], "normal": [-10.98, 0, -23.56]},
|
||||
{"id": 41, "position": [1.5, -2.2, 25.8], "normal": [0, -25.6, 9.5]},
|
||||
{"id": 42, "position": [13.9, -40, 0], "normal": [1, -0.35, 0.4], "spin": 27}
|
||||
],
|
||||
"model": [
|
||||
{
|
||||
"stlFile": "surfaces/Finger.stl",
|
||||
"originOfModel": [24, 0, -9.1],
|
||||
"rotationOfModelDegree": [90, -90, 0],
|
||||
"material": "defaultPlastic"
|
||||
}
|
||||
]
|
||||
},
|
||||
"FingerB": {
|
||||
"parent": "Palm",
|
||||
"size": [80, 60, 20],
|
||||
"mountPosition": [0, 0, 0],
|
||||
"mountRotation": [0, 0, 0],
|
||||
"jointToParent": {
|
||||
"name": "Slider",
|
||||
"type": "linear",
|
||||
"axis": [-1, 0, 0],
|
||||
"origin": [-4, -35, 0],
|
||||
"rotation": [0, 0, 0],
|
||||
"variable": "e",
|
||||
"feedrate": 2000,
|
||||
"controller": "hand"
|
||||
},
|
||||
"skeleton": {"from": [0, 0, 0], "to": [0, -60, 0], "radius": 4, "color": [0.2, 0.8, 0.2]},
|
||||
"markers": [
|
||||
{"id": 43, "position": [-12, -24, 17.1], "normal": [10.98, 0, 23.56], "spin": 90},
|
||||
{"id": 44, "position": [-1.5, -2.2, -25.8], "normal": [0, -25.6, -9.5], "spin": 90},
|
||||
{"id": 45, "position": [-13.9, -40, 0], "normal": [-1, -0.35, -0.4], "spin": -27}
|
||||
],
|
||||
"model": [
|
||||
{
|
||||
"stlFile": "surfaces/Finger.stl",
|
||||
"originOfModel": [-24, 0, 9.1],
|
||||
"rotationOfModelDegree": [90, 90, 0],
|
||||
"material": "defaultPlastic"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
172
doc/ToDo_14_robot_json_service.md
Normal file
172
doc/ToDo_14_robot_json_service.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# Roadmap: robot.json als zentraler Service
|
||||
|
||||
## Ist-Zustand (Problem)
|
||||
|
||||
`robot.json` existiert aktuell an mindestens zwei unabhängigen Orten:
|
||||
|
||||
| App | Pfad | Rolle |
|
||||
|-----|------|-------|
|
||||
| `appRobotHoming` | `scripts/robot_<ts>.json` (Env: `ROBOT_JSON`) | Lesen + Schreiben (editRobot.js) |
|
||||
| `appRobotRendering` | `data/robot/robot.json` | Lesen (Rendering) |
|
||||
| `appRobotDriver` | — | Armlängen hardcodiert (`250, 264, 100`) |
|
||||
|
||||
Es gibt keine Versionierung, keinen Single Point of Truth, und wenn Homing die Geometrie kalibriert, bekommen Driver und Rendering davon nichts mit.
|
||||
|
||||
---
|
||||
|
||||
## Ziel-Architektur
|
||||
|
||||
`appRobotDriver` ist das **Zentrum des Systems** (er steuert die Hardware). Er besitzt daher auch die `robot.json` und stellt sie über einen REST-Endpunkt bereit. Alle anderen Apps lesen/schreiben ausschließlich über diesen Endpunkt.
|
||||
|
||||
```
|
||||
appRobotDriver
|
||||
└── data/robot/
|
||||
├── robot.json ← aktueller Stand (Single Source of Truth)
|
||||
├── robot_20260101_143522.json ← Snapshot vor jeder Änderung
|
||||
└── robot_20260115_091010.json
|
||||
|
||||
appRobotHoming ──GET/PUT──► appRobotDriver :2098/api/robot
|
||||
appRobotRendering ──GET──────► appRobotDriver :2098/api/robot
|
||||
appRobotSimulation ──GET────► appRobotDriver :2098/api/robot
|
||||
```
|
||||
|
||||
Der InfoServer (Port 2098) bekommt die neuen Endpunkte. Der WebSocket-Server (Port 2095) bleibt unverändert.
|
||||
|
||||
---
|
||||
|
||||
## API-Design
|
||||
|
||||
Alle Endpunkte unter dem bereits laufenden InfoServer auf Port 2098.
|
||||
|
||||
```
|
||||
GET /api/robot → robot.json als JSON (kein Auth nötig)
|
||||
PUT /api/robot → ersetzt robot.json, legt vorher Snapshot an (Auth nötig)
|
||||
GET /api/robot/history → Liste aller Snapshots [{ filename, timestamp }]
|
||||
GET /api/robot/history/:ts → einen bestimmten Snapshot abrufen
|
||||
```
|
||||
|
||||
### Sicherheit (trivial, da Netz sicher)
|
||||
|
||||
PUT-Requests brauchen einen statischen API-Key als HTTP-Header:
|
||||
|
||||
```
|
||||
Authorization: Bearer <ROBOT_API_KEY>
|
||||
```
|
||||
|
||||
Key wird per Umgebungsvariable konfiguriert (`ROBOT_API_KEY`). Fehlt die Variable, wird ein zufälliger Key generiert und beim Start geloggt. Für GET braucht es keinen Key — Lesen ist überall erlaubt.
|
||||
|
||||
Das ist die einzige Absicherung. Kein JWT, keine Sessions, kein Rate-Limiting — Netz ist sicher, und ein versehentlicher Schreib-Request aus einem Browser soll trotzdem nicht funktionieren.
|
||||
|
||||
---
|
||||
|
||||
## Status
|
||||
|
||||
| Schritt | Bereich | Status |
|
||||
|---------|---------|--------|
|
||||
| 1 — Datei anlegen | appRobotDriver | ✅ erledigt |
|
||||
| 2 — RobotConfigService | appRobotDriver | ✅ erledigt |
|
||||
| 3 — Registrierung InfoServer | appRobotDriver | ✅ erledigt |
|
||||
| 4 — Driver liest Armlängen | appRobotDriver | ✅ erledigt |
|
||||
| 5 — appRobotHoming umstellen | appRobotHoming | ⬜ offen |
|
||||
| 6 — appRobotRendering umstellen | appRobotRendering | ⬜ offen |
|
||||
| 7 — Aufräumen | alle Repos | ⬜ offen |
|
||||
|
||||
Die Schritte 1–4 betreffen ausschließlich `appRobotDriver` und sind vollständig umgesetzt.
|
||||
Schritte 5–7 betreffen `appRobotHoming` und `appRobotRendering` — separate Tickets/Sessions.
|
||||
|
||||
---
|
||||
|
||||
## Umsetzungsschritte
|
||||
|
||||
### Schritt 1 — Datei anlegen (appRobotDriver) ✅
|
||||
|
||||
- Verzeichnis `data/robot/` anlegen
|
||||
- Default-`robot.json` wird vom Nutzer geliefert und dort abgelegt
|
||||
- `.gitignore`-Eintrag für `data/robot/robot_*.json` (Snapshots gehören nicht ins Repo, `robot.json` selbst schon)
|
||||
- Driver startet mit Defaults (`l1: 250, l2: 264, l3: 100`) wenn Datei fehlt, loggt Warnung
|
||||
|
||||
### Schritt 2 — RobotConfigService (appRobotDriver) ✅
|
||||
|
||||
Neue, **in sich geschlossene** Datei `server/RobotConfigService.js`. Sie hat keine Abhängigkeiten auf andere Teile des Drivers und kann in jeden Express- oder `https.createServer`-basierten Server mit einer Zeile eingehängt werden:
|
||||
|
||||
```js
|
||||
const robotConfigService = require('./server/RobotConfigService');
|
||||
robotConfigService.register(app, { apiKey: process.env.ROBOT_API_KEY });
|
||||
// fertig — alle /api/robot*-Routen sind registriert
|
||||
```
|
||||
|
||||
Das Modul kapselt intern:
|
||||
|
||||
```
|
||||
readRobotJson() → Promise<object>
|
||||
writeRobotJson(data) → Promise<{ snapshotFile }> // legt robot_<ts>.json an + pruning
|
||||
listHistory() → Promise<{ filename, day, timestamp }[]>
|
||||
readSnapshot(ts) → Promise<object>
|
||||
pruneSnapshots() → löscht überschüssige Snapshots (s. Regel unten)
|
||||
```
|
||||
|
||||
**Snapshot-Pruning-Regel:** Pro Tag maximal 100 Snapshots. Sind es mehr, wird nur der neueste des Tages behalten. Diese Bereinigung läuft automatisch nach jedem Schreibvorgang.
|
||||
|
||||
Timestamp-Format: `YYYYMMDD_HHmmss` (konsistent mit appRobotHoming).
|
||||
|
||||
**API-Key:** Wird per Option übergeben. Fehlt der Key (undefined), generiert das Modul beim ersten Start einen zufälligen Key, loggt ihn einmalig und speichert ihn in `data/robot/.apikey` (nicht im Repo). So funktioniert es ohne Konfiguration, ist aber trotzdem nicht offen.
|
||||
|
||||
### Schritt 3 — Registrierung in InfoServer.js (appRobotDriver) ✅
|
||||
|
||||
Eine Zeile in `server/InfoServer.js` am Anfang der Request-Handler:
|
||||
|
||||
```js
|
||||
robotConfigService.register(httpsServer, { apiKey });
|
||||
```
|
||||
|
||||
Da `InfoServer.js` kein Express nutzt (rohes `https.createServer`), bekommt `RobotConfigService` intern einen minimalen Router, der url-Matching selbst macht — oder `InfoServer.js` wird auf Express umgestellt (kleiner Schritt, bringt mehr Flexibilität für spätere Endpunkte).
|
||||
|
||||
### Schritt 4 — Driver liest Armlängen aus robot.json ✅
|
||||
|
||||
`startRobot.js` liest beim Start arm-lengths aus `data/robot/robot.json`.
|
||||
Fallback auf `{ l1: 250, l2: 264, l3: 100 }` mit Log-Warnung wenn Datei fehlt oder Keys fehlen.
|
||||
|
||||
```js
|
||||
// links.Arm1.size[1] → l1
|
||||
// links.Arm2.size[1] → l2
|
||||
// links.Ellbow.size[0] → l3
|
||||
```
|
||||
|
||||
### Schritt 5 — appRobotHoming auf Driver-API umstellen ⬜
|
||||
|
||||
`server/server.js` in appRobotHoming:
|
||||
|
||||
- `ROBOT_JSON`-Env-Variable durch `ROBOT_DRIVER_URL` ersetzen (z.B. `https://appRobotDriver:2098`)
|
||||
- Neue Hilfsfunktionen `fetchRobotJson()` / `pushRobotJson(data)` (HTTP GET/PUT mit API-Key)
|
||||
- Alle `fsPromises.readFile(ROBOT_JSON)`-Aufrufe durch `await fetchRobotJson()` ersetzen
|
||||
- Nach jeder editRobot.js-Transformation: `await pushRobotJson(updated)`
|
||||
- `editRobot.js` bleibt unverändert (pure Transformationen, kein File-IO)
|
||||
|
||||
### Schritt 6 — appRobotRendering auf Driver-API umstellen ⬜
|
||||
|
||||
`robot.json` wird einmalig beim Start vom Driver geholt und gecacht. Cache wird per `GET /api/robot` aktualisierbar. Env-Variable `ROBOT_DRIVER_URL`.
|
||||
|
||||
### Schritt 7 — Aufräumen ⬜
|
||||
|
||||
- Lokale `robot.json`-Kopien in appRobotHoming und appRobotRendering entfernen
|
||||
- `.gitignore`-Einträge in den betroffenen Repos anpassen
|
||||
- `ROBOT_JSON`-Env-Variable aus docker-compose-Dateien entfernen, `ROBOT_DRIVER_URL` hinzufügen
|
||||
|
||||
---
|
||||
|
||||
## Entschiedene Punkte
|
||||
|
||||
| Frage | Entscheidung |
|
||||
|-------|-------------|
|
||||
| Driver-Start ohne robot.json | Mit Defaults starten + Warnung loggen |
|
||||
| Snapshot-Limit | Max. 100 pro Tag; danach pro Tag nur die letzte Version behalten |
|
||||
| Caching in Clients | Ja, erlaubt — Ziel ist API-only-Zugriff, kein direktes File-IO |
|
||||
| Port InfoServer | 2098 bleibt; weitere Endpunkte werden dort angehängt |
|
||||
|
||||
---
|
||||
|
||||
## Nicht in dieser Roadmap
|
||||
|
||||
- Konflikte bei gleichzeitigen Schreibzugriffen (mutex) — vorerst nicht nötig, Homing ist der einzige Schreiber
|
||||
- Diff-Anzeige zwischen Snapshots
|
||||
- Rollback-Endpunkt (kann manuell über `GET /api/robot/history/:ts` + `PUT /api/robot` gemacht werden)
|
||||
@@ -1,20 +1,194 @@
|
||||
# ToDo 3 — Konfiguration
|
||||
|
||||
## Ziel der Verbesserung
|
||||
## Ausgangslage
|
||||
|
||||
Zentralisierte Konfiguration statt verstreuter Hardcodierung. Konfiguration soll transparent, testbar und leicht anpassbar sein.
|
||||
Konfiguration ist aktuell über drei Orte verstreut:
|
||||
|
||||
## Aufgaben
|
||||
| Ort | Beispiele |
|
||||
|-----|-----------|
|
||||
| Hardcodiert im Code | Controller-Ports (2300, 5000), Passphrase `'abcd'` |
|
||||
| Env-Variablen | `GRBL_BASE_IP`, `ROBOT_DEFAULT_FEEDRATE`, `PORT`, … |
|
||||
| `robot.json` | Geometrie, Rendering-Parameter, Pose-Estimation-Optionen |
|
||||
|
||||
- [ ] `config.js` oder ein zentrales Config-Modul anlegen
|
||||
- [ ] Alle Umgebungsvariablen an einer Stelle lesen und validieren
|
||||
- `PORT`
|
||||
- `GRBL_BASE_IP`, `GRBL_ELLBOW_IP`, `GRBL_HAND_IP`
|
||||
- `ROBOT_DEFAULT_FEEDRATE`
|
||||
- `ROBOT_USE_SPEED_CALC`
|
||||
- HTTPS-Zertifikatpfade und Passphrase
|
||||
- [ ] `startRobot.js`, `TelnetSenderGRBL`, `InfoServer.js` und weitere Module mit dem Config-Modul arbeiten lassen
|
||||
- [ ] Optional: `config/default.json` oder `.env` als Konfigurationsbasis bereitstellen
|
||||
- [ ] Fehlende oder ungültige Konfiguration frühzeitig mit klarer Fehlermeldung melden
|
||||
- [ ] HTTPS-Passphrase aus Umgebungsvariable lesen statt hardcoded `'abcd'` in `startRobot.js`
|
||||
- [ ] `logs/`-Verzeichnis beim Start automatisch anlegen (aktuell crash wenn nicht vorhanden — siehe `doc/ToDo_8_Bugs.md` Bug 4)
|
||||
Ergebnis: Wer einen Roboter in einer anderen Umgebung betreibt, muss Code lesen, um zu wissen was anzupassen ist. Und es ist unklar, welches Programm für welchen Abschnitt zuständig ist.
|
||||
|
||||
---
|
||||
|
||||
## Architektur-Entscheidung
|
||||
|
||||
**`robot.json` ist die einzige Wahrheitsquelle für alles, was roboter-spezifisch ist.**
|
||||
Eine robot.json beschreibt einen konkreten Roboter vollständig — sie kann weitergegeben werden, wenn der Roboter in einer anderen Umgebung eingesetzt wird.
|
||||
|
||||
**Env-Variablen bleiben nur für deployment-spezifische Werte,** die nichts mit dem Roboter selbst zu tun haben:
|
||||
|
||||
| Variable | Grund |
|
||||
|----------|-------|
|
||||
| `PORT` (2095/2098) | Server-Port ändert sich je Deployment, nicht je Roboter |
|
||||
| `HTTPS_KEY_PATH`, `HTTPS_CERT_PATH`, `HTTPS_PASSPHRASE` | Sicherheits-Infrastruktur |
|
||||
| `ROBOT_API_KEY` | Geheimnis, darf nie in einer weitergebbaren Datei stehen |
|
||||
|
||||
---
|
||||
|
||||
## Änderungen an `robot.json`
|
||||
|
||||
### 1 — Kinematik-Typ (`kinematics`)
|
||||
|
||||
Die Kinematik-Parameter (Armlängen, Achsen, Gelenk-Kette) sind bereits vollständig in `links` enthalten und werden von `robot/RobotConfig.js` daraus abgeleitet. Einzig der Name des Solver-Algorithmus fehlt noch:
|
||||
|
||||
```json
|
||||
"kinematics": {
|
||||
"_owner": "appRobotDriver",
|
||||
"type": "arm3segmentlinearx"
|
||||
}
|
||||
```
|
||||
|
||||
Kein Duplizieren von `links`-Daten. `RobotConfig.js` liest z.B.:
|
||||
- `links.Arm1.skeleton.to[1]` → l1
|
||||
- `links.Ellbow.skeleton.to[0]` → l3
|
||||
|
||||
### 2 — Feedrate und Controller-Zuordnung in `jointToParent`
|
||||
|
||||
Beide Infos gehören direkt zum Gelenk, weil sie die Hardware-Eigenschaft eines konkreten Joints beschreiben:
|
||||
|
||||
```json
|
||||
"jointToParent": {
|
||||
"name": "Slider",
|
||||
"type": "linear",
|
||||
"axis": [1, 0, 0],
|
||||
"origin": [0, 0, 16],
|
||||
"variable": "x",
|
||||
"feedrate": 2000,
|
||||
"controller": "base"
|
||||
}
|
||||
```
|
||||
|
||||
- `feedrate` — maximale Vorschubgeschwindigkeit dieses Joints in mm/min (linear) oder °/min (revolute). Überschreibt den globalen Default aus `motion.defaultFeedrate`.
|
||||
- `controller` — Verweis auf den Schlüssel in `controllers`. Darüber weiss der Driver, welche Variable auf welchem Controller-Kanal liegt. Die heutigen hardcodierten Achszuordnungen (`'x','y','z'` / `'a',null,null` / …) werden damit überflüssig.
|
||||
|
||||
### 3 — Controller-Endpunkte (`controllers`)
|
||||
|
||||
Top-Level-Abschnitt, nur IP und Port. Die Achszuordnung ergibt sich aus den `controller`-Verweisen in den Joints (kein Duplikat).
|
||||
|
||||
```json
|
||||
"controllers": {
|
||||
"_owner": "appRobotDriver",
|
||||
"base": { "ip": "fluidNcBase.local", "port": 2300, "protocol": "telnet" },
|
||||
"elbow": { "ip": "fluidNcEllbow.local", "port": 5000, "protocol": "telnet" },
|
||||
"hand": { "ip": "fluidNcHand.local", "port": 5000, "protocol": "telnet" }
|
||||
}
|
||||
```
|
||||
|
||||
Der Driver liest `controllers` für IP/Port, scannt dann `links` nach `jointToParent.controller === "base"` und bekommt so `["x", "y", "z"]` als Achsliste.
|
||||
|
||||
### 4 — Globale Bewegungs-Defaults (`motion`)
|
||||
|
||||
Werte, die für alle Joints gelten, sofern kein `feedrate` im Joint hinterlegt ist. Ausserdem Software-Flags, die keine natürliche Heimat in einem einzelnen Joint haben:
|
||||
|
||||
```json
|
||||
"motion": {
|
||||
"_owner": "appRobotDriver",
|
||||
"defaultFeedrate": 2300,
|
||||
"speedMode": "legacy",
|
||||
"speedModeOptions": ["legacy", "correct"]
|
||||
}
|
||||
```
|
||||
|
||||
`speedMode` und `defaultFeedrate` lösen die Env-Vars `ROBOT_DEFAULT_FEEDRATE` und `ROBOT_SPEED_MODE` ab.
|
||||
|
||||
---
|
||||
|
||||
## Interne Abhängigkeiten in `robot.json`
|
||||
|
||||
Mit `controller`-Verweisen in den Joints entstehen interne Cross-References. Die robot.json darf solche haben — das ist bewusste Design-Entscheidung, die Redundanz vermeidet. Ein späterer `validator.py` muss diese Konsistenz prüfen:
|
||||
|
||||
| Referenz | Regel |
|
||||
|----------|-------|
|
||||
| `jointToParent.controller` → `controllers.*` | Jeder Verweis muss auf einen existierenden Controller-Schlüssel zeigen |
|
||||
| Marker-IDs | Jede ID darf global nur einmal vergeben sein |
|
||||
| `parent` in `links` | Muss auf einen existierenden Link zeigen, kein Zyklus |
|
||||
|
||||
---
|
||||
|
||||
## Verantwortlichkeits-Tabelle
|
||||
|
||||
Jeder Abschnitt hat genau einen **Eigentümer** (das Programm, das diesen Abschnitt schreiben darf). Konsumenten lesen nur.
|
||||
|
||||
| Abschnitt | Eigentümer (`_owner`) | Konsumenten |
|
||||
|-----------|----------------------|-------------|
|
||||
| `kinematics` | **appRobotDriver** | appRobotDriver (via RobotConfig.js) |
|
||||
| `motion` | **appRobotDriver** | appRobotDriver |
|
||||
| `controllers` | **appRobotDriver** | appRobotDriver |
|
||||
| `units` | **appRobotDriver** | alle |
|
||||
| `links` (inkl. `jointToParent.feedrate`, `.controller`) | **appRobotDriver** — Homing darf Marker-Positionen/-Normalen aktualisieren (via PUT /api/robot), aber keine Driver-Felder (`feedrate`, `controller`) verändern | alle |
|
||||
| `vision_config` | — | appRobotHoming |
|
||||
| `constraint_rules`, `observation_weighting`, `multiview_calculation`, `pose_estimation`, `state_pose_params` | — | appRobotHoming |
|
||||
| `renderingInfo` | — | appRobotRendering |
|
||||
| `robot_test_poses`, `test_camera_positions/targets` | — | appRobotHoming, appRobotRendering |
|
||||
| `defaultPosition` | — | appRobotDriver, appRobotRendering |
|
||||
| `coordinateSystem` | — | alle |
|
||||
|
||||
Sektionen ohne `_owner` sind manuell konfiguriert und werden von keinem Programm automatisch überschrieben. Der Validator prüft, dass Homing in `links` nur die erlaubten Felder (`position`, `normal`, `spin`, `size` in Markern) verändert.
|
||||
|
||||
---
|
||||
|
||||
## Zugriffsmuster
|
||||
|
||||
### appRobotDriver — neues Modul `robot/RobotConfig.js`
|
||||
|
||||
Der Driver liest robot.json synchron beim Start (direkt vom Disk, vor dem HTTP-Server-Start). `RobotConfig.js` ist der einzige Ort im Driver-Code, der robot.json kennt:
|
||||
|
||||
```js
|
||||
const cfg = RobotConfig.load(); // synchron, gibt typisierten Record zurück
|
||||
|
||||
cfg.kinematics.type // → 'arm3segmentlinearx'
|
||||
cfg.kinematics.l1 // → 250 (abgeleitet aus links.Arm1.skeleton)
|
||||
cfg.motion.defaultFeedrate // → 2300
|
||||
cfg.controllers // → { base: {ip, port}, elbow: {ip, port}, hand: {ip, port} }
|
||||
cfg.axesByController('base') // → ['x', 'y', 'z'] (abgeleitet aus links)
|
||||
```
|
||||
|
||||
Alle `process.env`-Lesungen für roboter-spezifische Werte wandern hierher. Env-Vars bleiben als Override-Ebene (Env hat Vorrang vor robot.json — nützlich für Tests und schnelle Korrekturen ohne Datei-Änderung).
|
||||
|
||||
### Alle anderen Apps — HTTP GET
|
||||
|
||||
```js
|
||||
const robot = await fetch('https://appRobotDriver:2098/api/robot').then(r => r.json());
|
||||
```
|
||||
|
||||
Kein eigenes Zugriffs-Modul nötig.
|
||||
|
||||
---
|
||||
|
||||
## Umsetzungsschritte
|
||||
|
||||
### ✅ Schritt 1 — `robot.json` erweitern
|
||||
- Abschnitte `kinematics`, `motion`, `controllers` eintragen
|
||||
- In jedem `jointToParent`: `feedrate` und `controller` ergänzen
|
||||
- Hinweis: `controllers` enthält explizite `axes`-Reihenfolge (Baum-Traversal liefert für Hand-Controller falsche Reihenfolge)
|
||||
- Beide Kopien aktualisiert: `appRobotRendering/data/robot/robot.json` und `appRobotDriver/data/robot/robot.json`
|
||||
|
||||
### ✅ Schritt 2 — `robot/RobotConfig.js` anlegen
|
||||
- Liest robot.json synchron
|
||||
- Leitet l1/l2/l3 aus `links.*.skeleton.to` ab (behebt alten Bug: `Ellbow.size[0]` existierte nicht)
|
||||
- Gibt `axesByController(key)` aus `controllers[key].axes` zurück
|
||||
- Gibt typisierten Record mit Fallbacks zurück
|
||||
- Env-Override-Ebene: GRBL_*_IP, ROBOT_DEFAULT_FEEDRATE, ROBOT_SPEED_MODE, ROBOT_USE_SPEED_CALC
|
||||
|
||||
### ✅ Schritt 3 — `startRobot.js` und `RobotBase.js` umstellen
|
||||
- `startRobot.js`: `readArmLengthsFromConfig` entfernt, ersetzt durch `RobotConfig.load()`; Controller-Setup config-getrieben (keine hardcodierten IPs/Ports/Achsen mehr)
|
||||
- `RobotBase.js`: akzeptiert optionalen `config`-Parameter im Konstruktor; Env-Vars bleiben als Fallback
|
||||
- `Arm3SegmentLinearX` und `Arm3SegmentRotaryBase`: reichen `config`/`params` an `super()` durch
|
||||
|
||||
### ✅ Schritt 4 — HTTPS-Passphrase aus Env-Variable
|
||||
`HTTPS_PASSPHRASE` lesen statt hardcodiertem `'abcd'`. Default bleibt `'abcd'` für lokale Entwicklung.
|
||||
|
||||
### ✅ Schritt 5 — `logs/`-Verzeichnis automatisch anlegen
|
||||
`fsModule.mkdirSync?.('logs', { recursive: true })` in `startRobot.js` nach erfolgreichem HTTPS-Load. Bestehende `ensureLogDir()` in `InputWS.js` bleibt als zweite Absicherung.
|
||||
|
||||
---
|
||||
|
||||
## Nicht in dieser ToDo
|
||||
|
||||
- JSON-Schema-Validierung (separates Ticket, koordiniert mit dem geplanten `validator.py`)
|
||||
- Laufzeit-Reload von robot.json ohne Neustart
|
||||
- `speedMode` Details → ToDo 6a
|
||||
|
||||
@@ -10354,3 +10354,18 @@
|
||||
2026-06-10T21:18:23.947Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-10T21:18:24.131Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-10T21:18:24.377Z ::ffff:127.0.0.1: G1 X1
|
||||
2026-06-11T18:35:07.352Z ::ffff:127.0.0.1: M114
|
||||
2026-06-11T18:35:07.382Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-11T18:35:07.818Z ::ffff:127.0.0.1: M114
|
||||
2026-06-11T18:35:08.029Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-11T18:35:08.254Z ::ffff:127.0.0.1: G1 X1
|
||||
2026-06-11T18:37:29.305Z ::ffff:127.0.0.1: M114
|
||||
2026-06-11T18:37:29.323Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-11T18:37:29.955Z ::ffff:127.0.0.1: M114
|
||||
2026-06-11T18:37:30.191Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-11T18:37:30.431Z ::ffff:127.0.0.1: G1 X1
|
||||
2026-06-11T19:43:39.299Z ::ffff:127.0.0.1: M114
|
||||
2026-06-11T19:43:39.339Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-11T19:43:39.506Z ::ffff:127.0.0.1: M114
|
||||
2026-06-11T19:43:39.745Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
|
||||
2026-06-11T19:43:40.028Z ::ffff:127.0.0.1: G1 X1
|
||||
|
||||
@@ -14616,3 +14616,9 @@
|
||||
2026-06-10T21:03:42.257Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-10T21:18:23.643Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-10T21:18:23.899Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-11T18:35:07.333Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-11T18:35:07.597Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-11T18:37:29.284Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-11T18:37:29.724Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-11T19:43:39.197Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-11T19:43:39.241Z ::ffff:127.0.0.1 : Ping
|
||||
|
||||
@@ -21,18 +21,19 @@ const MotorPosition = require('./RobotMotorPosition.js')
|
||||
class RobotBase{
|
||||
|
||||
|
||||
constructor() {
|
||||
// Umgebungsvariablen-Logik
|
||||
const DEFAULT_FEEDRATE = process.env.ROBOT_DEFAULT_FEEDRATE ?
|
||||
Number(process.env.ROBOT_DEFAULT_FEEDRATE) : 1000;
|
||||
constructor(config = {}) {
|
||||
// config-Werte haben Vorrang, Env-Variablen als Fallback (Kompatibilität).
|
||||
const DEFAULT_FEEDRATE = config.defaultFeedrate
|
||||
?? (process.env.ROBOT_DEFAULT_FEEDRATE ? Number(process.env.ROBOT_DEFAULT_FEEDRATE) : 1000);
|
||||
// Speed-Regelung-Schalter: 'legacy' (Default — exakt wie bisher) oder 'correct'.
|
||||
// Siehe doc/ToDo_6a_Speed.md.
|
||||
this.speedMode = (process.env.ROBOT_SPEED_MODE || 'legacy').toLowerCase();
|
||||
this.speedMode = (config.speedMode || process.env.ROBOT_SPEED_MODE || 'legacy').toLowerCase();
|
||||
// ROBOT_USE_SPEED_CALC bleibt der interne Schalter für calculateSpeeds();
|
||||
// der Korrekt-Modus aktiviert die Berechnung automatisch.
|
||||
this.useSpeedCalc = this.speedMode === 'correct' ||
|
||||
process.env.ROBOT_USE_SPEED_CALC === 'true' ||
|
||||
process.env.ROBOT_USE_SPEED_CALC === '1';
|
||||
this.useSpeedCalc = config.useSpeedCalc
|
||||
?? (this.speedMode === 'correct' ||
|
||||
process.env.ROBOT_USE_SPEED_CALC === 'true' ||
|
||||
process.env.ROBOT_USE_SPEED_CALC === '1');
|
||||
/** @type {number} Bewegungszeit des letzten Schritts in Minuten (für koordinierte Feedrate) */
|
||||
this.lastMoveTime = 0;
|
||||
|
||||
|
||||
101
robot/RobotConfig.js
Normal file
101
robot/RobotConfig.js
Normal file
@@ -0,0 +1,101 @@
|
||||
'use strict';
|
||||
|
||||
const ROBOT_JSON_PATH = 'data/robot/robot.json';
|
||||
|
||||
// Backward-compat env-var names für Controller-IPs (werden in Env als GRBL_ELLBOW_IP gespeichert)
|
||||
const ENV_IP_MAP = {
|
||||
base: 'GRBL_BASE_IP',
|
||||
elbow: 'GRBL_ELLBOW_IP',
|
||||
hand: 'GRBL_HAND_IP'
|
||||
};
|
||||
|
||||
const DEFAULTS = {
|
||||
kinematics: { type: 'arm3segmentlinearx', l1: 250, l2: 264, l3: 100 },
|
||||
motion: { defaultFeedrate: 1000, speedMode: 'legacy', useSpeedCalc: false },
|
||||
controllers: {
|
||||
base: { ip: 'fluidNcBase.local', port: 2300, protocol: 'telnet', axes: ['x', 'y', 'z'] },
|
||||
elbow: { ip: 'fluidNcEllbow.local', port: 5000, protocol: 'telnet', axes: ['a', null, null] },
|
||||
hand: { ip: 'fluidNcHand.local', port: 5000, protocol: 'telnet', axes: ['c', 'e', 'b'] }
|
||||
}
|
||||
};
|
||||
|
||||
function deriveKinematicParams(links) {
|
||||
if (!links) return {};
|
||||
const result = {};
|
||||
const l1raw = links.Arm1?.skeleton?.to?.[1];
|
||||
const l2raw = links.Arm2?.skeleton?.to?.[1];
|
||||
const l3raw = links.Ellbow?.skeleton?.to?.[0];
|
||||
if (l1raw != null) result.l1 = Math.abs(l1raw);
|
||||
if (l2raw != null) result.l2 = Math.abs(l2raw);
|
||||
if (l3raw != null) result.l3 = l3raw;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Liest robot.json synchron und gibt einen typisierten Config-Record zurück.
|
||||
* Env-Variablen haben Vorrang vor robot.json (nützlich für Tests und Notfall-Overrides).
|
||||
*
|
||||
* @param {Object} [fsModule] - Abhängigkeit (Default: require('fs'))
|
||||
* @param {Object} [processEnv] - Abhängigkeit (Default: process.env)
|
||||
* @param {Object} [consoleObj] - Abhängigkeit (Default: console)
|
||||
* @returns {{ kinematics, motion, controllers, axesByController }}
|
||||
*/
|
||||
function load(fsModule, processEnv, consoleObj) {
|
||||
const fs_ = fsModule ?? require('fs');
|
||||
const env_ = processEnv ?? process.env;
|
||||
const log_ = consoleObj ?? console;
|
||||
|
||||
let json = null;
|
||||
try {
|
||||
const raw = fs_.readFileSync(ROBOT_JSON_PATH, 'utf8');
|
||||
json = JSON.parse(raw);
|
||||
} catch {
|
||||
log_.warn('[RobotConfig] data/robot/robot.json nicht lesbar — nutze Defaults');
|
||||
}
|
||||
|
||||
// Kinematik-Typ und Armlängen (aus links abgeleitet)
|
||||
const linkParams = deriveKinematicParams(json?.links);
|
||||
const kinematics = {
|
||||
type: json?.kinematics?.type ?? DEFAULTS.kinematics.type,
|
||||
l1: linkParams.l1 ?? DEFAULTS.kinematics.l1,
|
||||
l2: linkParams.l2 ?? DEFAULTS.kinematics.l2,
|
||||
l3: linkParams.l3 ?? DEFAULTS.kinematics.l3
|
||||
};
|
||||
|
||||
// Bewegungs-Defaults — Env hat Vorrang, dann robot.json, dann Hard-Default
|
||||
const jsonMotion = json?.motion ?? {};
|
||||
const rawFeedrate = env_.ROBOT_DEFAULT_FEEDRATE;
|
||||
const rawMode = env_.ROBOT_SPEED_MODE;
|
||||
const rawCalc = env_.ROBOT_USE_SPEED_CALC;
|
||||
const speedMode = (rawMode || jsonMotion.speedMode || DEFAULTS.motion.speedMode).toLowerCase();
|
||||
const motion = {
|
||||
defaultFeedrate: rawFeedrate
|
||||
? Number(rawFeedrate)
|
||||
: (jsonMotion.defaultFeedrate ?? DEFAULTS.motion.defaultFeedrate),
|
||||
speedMode,
|
||||
useSpeedCalc: speedMode === 'correct' || rawCalc === 'true' || rawCalc === '1'
|
||||
};
|
||||
|
||||
// Controller-Endpunkte — robot.json überschreibt Defaults, Env überschreibt IPs
|
||||
const jsonControllers = json?.controllers ?? {};
|
||||
const controllers = {};
|
||||
for (const key of Object.keys(DEFAULTS.controllers)) {
|
||||
const def = DEFAULTS.controllers[key];
|
||||
const cfg = jsonControllers[key] ?? {};
|
||||
const envIpKey = ENV_IP_MAP[key];
|
||||
controllers[key] = {
|
||||
ip: env_[envIpKey] ?? cfg.ip ?? def.ip,
|
||||
port: cfg.port ?? def.port,
|
||||
protocol: cfg.protocol ?? def.protocol,
|
||||
axes: cfg.axes ?? def.axes
|
||||
};
|
||||
}
|
||||
|
||||
function axesByController(key) {
|
||||
return controllers[key]?.axes ?? [];
|
||||
}
|
||||
|
||||
return { kinematics, motion, controllers, axesByController };
|
||||
}
|
||||
|
||||
module.exports = { load, DEFAULTS };
|
||||
@@ -20,9 +20,10 @@ class Arm3SegmentLinearX extends RobotBase {
|
||||
* @param {number} l1 Länge des Oberarms in mm
|
||||
* @param {number} l2 Länge des Unterarms in mm
|
||||
* @param {number} l3 Länge der Hand (Endeffector) in mm
|
||||
* @param {Object} [config] Bewegungs-Config (defaultFeedrate, speedMode, …)
|
||||
*/
|
||||
constructor(l1, l2, l3) {
|
||||
super();
|
||||
constructor(l1, l2, l3, config = {}) {
|
||||
super(config);
|
||||
|
||||
/** @type {number} Länge des Oberarms in mm */
|
||||
this.l1 = l1;
|
||||
|
||||
@@ -61,7 +61,7 @@ class Arm3SegmentRotaryBase extends RobotBase {
|
||||
* @param {number} [params.baseHeight=110] Höhe der Schulterachse über der Basis in mm
|
||||
*/
|
||||
constructor(l1 = 105, l2 = 98, l3 = 100, params = {}) {
|
||||
super();
|
||||
super(params);
|
||||
|
||||
/** @type {number} Länge des Oberarms in mm */
|
||||
this.l1 = l1;
|
||||
|
||||
@@ -1,102 +1,86 @@
|
||||
// server/InfoServer.js
|
||||
const fs = require('fs');
|
||||
const https = require('https');
|
||||
'use strict';
|
||||
|
||||
function createInfoServer(httpsOptions, sharedState, robot, GCode, senders) {
|
||||
return https.createServer(httpsOptions, (req, res) => {
|
||||
const express = require('express');
|
||||
const https = require('https');
|
||||
const path = require('path');
|
||||
const robotConfigService = require('./RobotConfigService');
|
||||
|
||||
if (req.url === '/') {
|
||||
return serveFile(res, './public/index.html', 'text/html');
|
||||
}
|
||||
const PUBLIC_DIR = path.join(__dirname, '..', 'public');
|
||||
|
||||
if (req.url === '/app.js') {
|
||||
return serveFile(res, './public/app.js', 'application/javascript');
|
||||
}
|
||||
function createInfoServer(httpsOptions, sharedState, robot, GCode, senders, options = {}) {
|
||||
const app = express();
|
||||
|
||||
if (req.url === '/style.css') {
|
||||
return serveFile(res, './public/style.css', 'text/css');
|
||||
}
|
||||
// ── Statische Dateien ────────────────────────────────────────────────────
|
||||
const staticFile = (file) => (req, res) =>
|
||||
res.sendFile(path.join(PUBLIC_DIR, file), err => {
|
||||
if (err) res.status(404).end('Not found');
|
||||
});
|
||||
|
||||
if (req.url === '/allApps.css') {
|
||||
return serveFile(res, './public/allApps.css', 'text/css');
|
||||
}
|
||||
app.get('/', staticFile('index.html'));
|
||||
app.get('/app.js', staticFile('app.js'));
|
||||
app.get('/style.css', staticFile('style.css'));
|
||||
app.get('/allApps.css', staticFile('allApps.css'));
|
||||
|
||||
/* ---------- API ---------- */
|
||||
if (req.url === '/api/status') {
|
||||
const sendersStatus = senders.map(({ name, instance }) => {
|
||||
const status = instance?.getStatus ? instance.getStatus() : {
|
||||
state: instance?.isTestMode ? 'connected' : instance?.tSocket ? 'connected' : 'disconnected',
|
||||
url: instance?.url || null,
|
||||
error: instance?.error || null,
|
||||
isTestMode: !!instance?.isTestMode,
|
||||
reconnectAttempt: instance?.reconnectAttempt || 0,
|
||||
reconnectTimer: !!instance?.reconnectTimer
|
||||
};
|
||||
|
||||
const state = status.state || (instance?.tSocket ? 'connected' : 'disconnected');
|
||||
const health = state === 'connected'
|
||||
? 'ok'
|
||||
: state === 'reconnecting'
|
||||
? 'warning'
|
||||
: 'disconnected';
|
||||
const reason = state === 'disconnected'
|
||||
? status.error || 'no active socket connection'
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
name,
|
||||
state,
|
||||
url: status.url || null,
|
||||
isTestMode: !!status.isTestMode,
|
||||
error: status.error || null,
|
||||
reconnectAttempt: status.reconnectAttempt || 0,
|
||||
reconnectTimer: !!status.reconnectTimer,
|
||||
health,
|
||||
reason
|
||||
};
|
||||
});
|
||||
|
||||
const connectedSenders = sendersStatus.filter(s => s.health === 'ok').length;
|
||||
const health = {
|
||||
ok: sendersStatus.length > 0 && sendersStatus.every(s => s.health === 'ok'),
|
||||
connectedSenders,
|
||||
totalSenders: sendersStatus.length
|
||||
// ── API ──────────────────────────────────────────────────────────────────
|
||||
app.get('/api/status', (req, res) => {
|
||||
const sendersStatus = senders.map(({ name, instance }) => {
|
||||
const status = instance?.getStatus ? instance.getStatus() : {
|
||||
state: instance?.isTestMode ? 'connected' : instance?.tSocket ? 'connected' : 'disconnected',
|
||||
url: instance?.url || null,
|
||||
error: instance?.error || null,
|
||||
isTestMode: !!instance?.isTestMode,
|
||||
reconnectAttempt: instance?.reconnectAttempt || 0,
|
||||
reconnectTimer: !!instance?.reconnectTimer
|
||||
};
|
||||
|
||||
const status = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
const state = status.state || (instance?.tSocket ? 'connected' : 'disconnected');
|
||||
const health = state === 'connected' ? 'ok'
|
||||
: state === 'reconnecting' ? 'warning'
|
||||
: 'disconnected';
|
||||
const reason = state === 'disconnected'
|
||||
? status.error || 'no active socket connection'
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
name,
|
||||
state,
|
||||
url: status.url || null,
|
||||
isTestMode: !!status.isTestMode,
|
||||
error: status.error || null,
|
||||
reconnectAttempt: status.reconnectAttempt || 0,
|
||||
reconnectTimer: !!status.reconnectTimer,
|
||||
health,
|
||||
clients: sharedState.connectedClients,
|
||||
senders: sendersStatus,
|
||||
lastCommands: sharedState.lastCommands,
|
||||
lastPings: sharedState.lastPings
|
||||
reason
|
||||
};
|
||||
});
|
||||
|
||||
res.writeHead(200, {'Content-Type': 'application/json'});
|
||||
return res.end(JSON.stringify(status));
|
||||
}
|
||||
|
||||
if (req.url === '/api/position') {
|
||||
res.writeHead(200, {'Content-Type': 'application/json'});
|
||||
return res.end(GCode.getM114(robot));
|
||||
}
|
||||
|
||||
res.writeHead(404);
|
||||
res.end('Not found');
|
||||
const connectedSenders = sendersStatus.filter(s => s.health === 'ok').length;
|
||||
res.json({
|
||||
generatedAt: new Date().toISOString(),
|
||||
health: {
|
||||
ok: sendersStatus.length > 0 && sendersStatus.every(s => s.health === 'ok'),
|
||||
connectedSenders,
|
||||
totalSenders: sendersStatus.length
|
||||
},
|
||||
clients: sharedState.connectedClients,
|
||||
senders: sendersStatus,
|
||||
lastCommands: sharedState.lastCommands,
|
||||
lastPings: sharedState.lastPings
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/position', (req, res) => {
|
||||
res.json(JSON.parse(GCode.getM114(robot)));
|
||||
});
|
||||
|
||||
// ── Robot-Config-Service ─────────────────────────────────────────────────
|
||||
robotConfigService.register(app, { apiKey: options.apiKey });
|
||||
|
||||
// ── 404 ──────────────────────────────────────────────────────────────────
|
||||
app.use((req, res) => res.status(404).end('Not found'));
|
||||
|
||||
return https.createServer(httpsOptions, app);
|
||||
}
|
||||
|
||||
/* ---------- Helper ---------- */
|
||||
|
||||
function serveFile(res, path, type) {
|
||||
fs.readFile(path, (err, data) => {
|
||||
if (err) {
|
||||
res.writeHead(404);
|
||||
return res.end('Not found');
|
||||
}
|
||||
res.writeHead(200, {'Content-Type': type});
|
||||
res.end(data);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = createInfoServer;
|
||||
module.exports = createInfoServer;
|
||||
|
||||
176
server/RobotConfigService.js
Normal file
176
server/RobotConfigService.js
Normal file
@@ -0,0 +1,176 @@
|
||||
// server/RobotConfigService.js
|
||||
//
|
||||
// Self-contained Express-Modul für die robot.json-Verwaltung.
|
||||
// Einbinden mit einer Zeile:
|
||||
//
|
||||
// const robotConfigService = require('./RobotConfigService');
|
||||
// robotConfigService.register(app, { apiKey: process.env.ROBOT_API_KEY });
|
||||
//
|
||||
// Routen:
|
||||
// GET /api/robot → aktuelle robot.json
|
||||
// PUT /api/robot → überschreibt robot.json, legt Snapshot an (Auth)
|
||||
// GET /api/robot/history → Liste aller Snapshots
|
||||
// GET /api/robot/history/:ts → einen bestimmten Snapshot abrufen
|
||||
|
||||
'use strict';
|
||||
|
||||
const express = require('express');
|
||||
const fs = require('fs');
|
||||
const fsp = require('fs/promises');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const DATA_DIR = path.join(__dirname, '..', 'data', 'robot');
|
||||
const ROBOT_JSON = path.join(DATA_DIR, 'robot.json');
|
||||
const KEY_FILE = path.join(DATA_DIR, '.apikey');
|
||||
|
||||
// ── Timestamp ────────────────────────────────────────────────────────────────
|
||||
|
||||
function makeTimestamp() {
|
||||
const now = new Date();
|
||||
const p = (n, w = 2) => String(n).padStart(w, '0');
|
||||
return `${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}_${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`;
|
||||
}
|
||||
|
||||
// ── API-Key-Auflösung ────────────────────────────────────────────────────────
|
||||
|
||||
function resolveApiKey(provided) {
|
||||
if (provided) return provided;
|
||||
try {
|
||||
return fs.readFileSync(KEY_FILE, 'utf8').trim();
|
||||
} catch {
|
||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
const key = crypto.randomBytes(24).toString('hex');
|
||||
fs.writeFileSync(KEY_FILE, key, 'utf8');
|
||||
console.log(`[RobotConfigService] Kein ROBOT_API_KEY gesetzt — neuer Key generiert: ${key}`);
|
||||
console.log(`[RobotConfigService] Gespeichert in: ${KEY_FILE}`);
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Datei-Operationen ────────────────────────────────────────────────────────
|
||||
|
||||
async function readRobotJson() {
|
||||
const raw = await fsp.readFile(ROBOT_JSON, 'utf8');
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
|
||||
async function writeRobotJson(data) {
|
||||
await fsp.mkdir(DATA_DIR, { recursive: true });
|
||||
const ts = makeTimestamp();
|
||||
const snapshotFile = `robot_${ts}.json`;
|
||||
|
||||
try {
|
||||
const current = await fsp.readFile(ROBOT_JSON, 'utf8');
|
||||
await fsp.writeFile(path.join(DATA_DIR, snapshotFile), current, 'utf8');
|
||||
} catch {
|
||||
// Noch kein robot.json vorhanden — kein Snapshot nötig
|
||||
}
|
||||
|
||||
await fsp.writeFile(ROBOT_JSON, JSON.stringify(data, null, 2), 'utf8');
|
||||
await pruneSnapshots();
|
||||
return { snapshotFile };
|
||||
}
|
||||
|
||||
async function listHistory() {
|
||||
try {
|
||||
const files = await fsp.readdir(DATA_DIR);
|
||||
return files
|
||||
.filter(f => /^robot_\d{8}_\d{6}\.json$/.test(f))
|
||||
.sort()
|
||||
.reverse()
|
||||
.map(f => ({ filename: f, day: f.slice(6, 14), timestamp: f.slice(15, 21) }));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function readSnapshot(ts) {
|
||||
const raw = await fsp.readFile(path.join(DATA_DIR, `robot_${ts}.json`), 'utf8');
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
|
||||
// ── Pruning ──────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Regel: Pro Tag maximal 100 Snapshots.
|
||||
// Hat ein Tag mehr als 100, wird nur der neueste behalten.
|
||||
|
||||
async function pruneSnapshots() {
|
||||
let files;
|
||||
try { files = await fsp.readdir(DATA_DIR); } catch { return; }
|
||||
|
||||
const byDay = {};
|
||||
for (const f of files) {
|
||||
if (!/^robot_\d{8}_\d{6}\.json$/.test(f)) continue;
|
||||
const day = f.slice(6, 14);
|
||||
if (!byDay[day]) byDay[day] = [];
|
||||
byDay[day].push(f);
|
||||
}
|
||||
|
||||
for (const [day, dayFiles] of Object.entries(byDay)) {
|
||||
if (dayFiles.length <= 100) continue;
|
||||
dayFiles.sort(); // älteste zuerst
|
||||
const toDelete = dayFiles.slice(0, -1); // neueste behalten
|
||||
for (const f of toDelete) {
|
||||
try { await fsp.unlink(path.join(DATA_DIR, f)); } catch { /* ignorieren */ }
|
||||
}
|
||||
console.log(`[RobotConfigService] Tag ${day}: ${toDelete.length} Snapshots bereinigt, behalten: ${dayFiles[dayFiles.length - 1]}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Auth ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
function isAuthorized(req, key) {
|
||||
return (req.headers['authorization'] ?? '') === `Bearer ${key}`;
|
||||
}
|
||||
|
||||
// ── Registrierung ─────────────────────────────────────────────────────────────
|
||||
|
||||
function register(app, options = {}) {
|
||||
const apiKey = resolveApiKey(options.apiKey);
|
||||
const router = express.Router();
|
||||
router.use(express.json({ limit: '5mb' }));
|
||||
|
||||
router.get('/api/robot/history', async (req, res) => {
|
||||
try {
|
||||
return res.json({ history: await listHistory() });
|
||||
} catch (err) {
|
||||
return res.status(500).json({ error: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/api/robot/history/:ts', async (req, res) => {
|
||||
try {
|
||||
return res.json(await readSnapshot(req.params.ts));
|
||||
} catch {
|
||||
return res.status(404).json({ error: 'Snapshot nicht gefunden' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/api/robot', async (req, res) => {
|
||||
try {
|
||||
return res.json(await readRobotJson());
|
||||
} catch {
|
||||
return res.status(404).json({ error: 'robot.json nicht gefunden' });
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/api/robot', async (req, res) => {
|
||||
if (!isAuthorized(req, apiKey)) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
if (!req.body || typeof req.body !== 'object' || Array.isArray(req.body)) {
|
||||
return res.status(400).json({ error: 'Body muss ein JSON-Objekt sein' });
|
||||
}
|
||||
try {
|
||||
const result = await writeRobotJson(req.body);
|
||||
return res.json({ ok: true, ...result });
|
||||
} catch (err) {
|
||||
return res.status(500).json({ error: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
app.use(router);
|
||||
}
|
||||
|
||||
module.exports = { register, readRobotJson, writeRobotJson, listHistory, readSnapshot };
|
||||
@@ -3,17 +3,18 @@ const https = require('https');
|
||||
const { createRobotFromEnv } = require('./robot/KinematicsFactory');
|
||||
const GCode = require('./robot/GCode');
|
||||
const TelnetSender = require('./robot/TelnetSenderGRBL');
|
||||
const RobotConfig = require('./robot/RobotConfig');
|
||||
|
||||
const initInputWS = require('./server/InputWS');
|
||||
const createInfoServer = require('./server/InfoServer');
|
||||
|
||||
function loadHttpsOptions(fsModule) {
|
||||
function loadHttpsOptions(fsModule, processEnv) {
|
||||
try {
|
||||
return {
|
||||
enable: true,
|
||||
key: fsModule.readFileSync('https/localhost.key'),
|
||||
cert: fsModule.readFileSync('https/localhost.pem'),
|
||||
passphrase: 'abcd'
|
||||
passphrase: processEnv.HTTPS_PASSPHRASE ?? 'abcd'
|
||||
};
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to load HTTPS certificate/key: ${err.message}`);
|
||||
@@ -59,7 +60,7 @@ function createApp(options = {}) {
|
||||
|
||||
let httpsOptions;
|
||||
try {
|
||||
httpsOptions = loadHttpsOptions(fsModule);
|
||||
httpsOptions = loadHttpsOptions(fsModule, processEnv);
|
||||
startupStatus.https = { ok: true };
|
||||
} catch (err) {
|
||||
startupStatus.https = { ok: false, error: err.message };
|
||||
@@ -67,15 +68,18 @@ function createApp(options = {}) {
|
||||
return { startupStatus };
|
||||
}
|
||||
|
||||
// logs/-Verzeichnis sicherstellen (idempotent)
|
||||
fsModule.mkdirSync?.('logs', { recursive: true });
|
||||
|
||||
const httpsServer = httpsModule.createServer(httpsOptions);
|
||||
|
||||
// Kinematik wählen: explizit injizierte RobotClass (Tests) hat Vorrang,
|
||||
// sonst per Umgebungsvariable (ROBOT_KINEMATICS / ROBOT_KINEMATICS_PARAMS).
|
||||
// Die Armlängen der Standard-Hardware dienen als Default, wenn keine Params
|
||||
// gesetzt sind — siehe doc/ToDo_12_InverseKinematikConfig_ROADMAP.md.
|
||||
// robot.json lesen: Kinematik-Params, Bewegungs-Defaults, Controller-Endpunkte.
|
||||
// Env-Variablen überschreiben robot.json (Override-Ebene).
|
||||
const cfg = RobotConfig.load(fsModule, processEnv, consoleObj);
|
||||
|
||||
const robot = RobotClass
|
||||
? new RobotClass(250, 264, 100)
|
||||
: createRobotFromEnv(processEnv, { l1: 250, l2: 264, l3: 100 });
|
||||
? new RobotClass(cfg.kinematics.l1, cfg.kinematics.l2, cfg.kinematics.l3, cfg.motion)
|
||||
: createRobotFromEnv(processEnv, { ...cfg.kinematics, ...cfg.motion });
|
||||
|
||||
const sharedState = {
|
||||
connectedClients: [],
|
||||
@@ -85,19 +89,12 @@ function createApp(options = {}) {
|
||||
|
||||
initInputWSFn(httpsServer, robot, GCodeModule, sharedState);
|
||||
|
||||
const baseIP = processEnv.GRBL_BASE_IP ?? 'fluidNcBase.local';
|
||||
const elbowIP = processEnv.GRBL_ELLBOW_IP ?? 'fluidNcEllbow.local';
|
||||
const handIP = processEnv.GRBL_HAND_IP ?? 'fluidNcHand.local';
|
||||
|
||||
const telnetSender1 = new TelnetSenderClass(baseIP, 2300, 'x', 'y', 'z');
|
||||
const telnetSender2 = new TelnetSenderClass(elbowIP, 5000, 'a', null, null);
|
||||
const telnetSender3 = new TelnetSenderClass(handIP, 5000, 'c', 'e', 'b');
|
||||
|
||||
const senders = [
|
||||
{ name: 'Base', instance: telnetSender1 },
|
||||
{ name: 'Elbow', instance: telnetSender2 },
|
||||
{ name: 'Hand', instance: telnetSender3 }
|
||||
];
|
||||
const senders = [];
|
||||
for (const [key, ctrl] of Object.entries(cfg.controllers)) {
|
||||
const name = key.charAt(0).toUpperCase() + key.slice(1);
|
||||
const instance = new TelnetSenderClass(ctrl.ip, ctrl.port, ...ctrl.axes);
|
||||
senders.push({ name, instance });
|
||||
}
|
||||
|
||||
startupStatus.senders = senders.map(getSenderConnectionStatus);
|
||||
const disconnectedSenders = startupStatus.senders.filter(s => s.status === 'disconnected');
|
||||
@@ -122,7 +119,8 @@ function createApp(options = {}) {
|
||||
sharedState,
|
||||
robot,
|
||||
GCodeModule,
|
||||
senders
|
||||
senders,
|
||||
{ apiKey: processEnv.ROBOT_API_KEY }
|
||||
);
|
||||
|
||||
const infoPort = 2098;
|
||||
|
||||
150
test/RobotConfig.test.js
Normal file
150
test/RobotConfig.test.js
Normal file
@@ -0,0 +1,150 @@
|
||||
'use strict';
|
||||
const { load, DEFAULTS } = require('../robot/RobotConfig');
|
||||
|
||||
function makeFs(content) {
|
||||
return {
|
||||
readFileSync: jest.fn(() => content)
|
||||
};
|
||||
}
|
||||
|
||||
function makeFailFs() {
|
||||
return {
|
||||
readFileSync: jest.fn(() => { throw new Error('ENOENT'); })
|
||||
};
|
||||
}
|
||||
|
||||
const log = { warn: jest.fn(), log: jest.fn(), error: jest.fn() };
|
||||
|
||||
const FULL_ROBOT_JSON = {
|
||||
kinematics: { type: 'arm3segmentlinearx' },
|
||||
motion: { defaultFeedrate: 2300, speedMode: 'legacy', speedModeOptions: ['legacy', 'correct'] },
|
||||
controllers: {
|
||||
base: { ip: 'fluidNcBase.local', port: 2300, protocol: 'telnet', axes: ['x', 'y', 'z'] },
|
||||
elbow: { ip: 'fluidNcEllbow.local', port: 5000, protocol: 'telnet', axes: ['a', null, null] },
|
||||
hand: { ip: 'fluidNcHand.local', port: 5000, protocol: 'telnet', axes: ['c', 'e', 'b'] }
|
||||
},
|
||||
links: {
|
||||
Arm1: { skeleton: { from: [0,0,0], to: [0,-250,0] } },
|
||||
Arm2: { skeleton: { from: [0,0,0], to: [0,-250,0] } },
|
||||
Ellbow: { skeleton: { from: [0,0,0], to: [90,0,0] } }
|
||||
}
|
||||
};
|
||||
|
||||
describe('RobotConfig.load — Vollständige robot.json', () => {
|
||||
let cfg;
|
||||
beforeEach(() => {
|
||||
cfg = load(makeFs(JSON.stringify(FULL_ROBOT_JSON)), {}, log);
|
||||
});
|
||||
|
||||
test('kinematics.type aus robot.json', () => {
|
||||
expect(cfg.kinematics.type).toBe('arm3segmentlinearx');
|
||||
});
|
||||
|
||||
test('l1/l2 aus links.Arm1/Arm2.skeleton.to[1] (Betrag)', () => {
|
||||
expect(cfg.kinematics.l1).toBe(250);
|
||||
expect(cfg.kinematics.l2).toBe(250);
|
||||
});
|
||||
|
||||
test('l3 aus links.Ellbow.skeleton.to[0]', () => {
|
||||
expect(cfg.kinematics.l3).toBe(90);
|
||||
});
|
||||
|
||||
test('motion.defaultFeedrate aus robot.json', () => {
|
||||
expect(cfg.motion.defaultFeedrate).toBe(2300);
|
||||
});
|
||||
|
||||
test('motion.speedMode aus robot.json', () => {
|
||||
expect(cfg.motion.speedMode).toBe('legacy');
|
||||
});
|
||||
|
||||
test('motion.useSpeedCalc false für legacy', () => {
|
||||
expect(cfg.motion.useSpeedCalc).toBe(false);
|
||||
});
|
||||
|
||||
test('controllers enthält alle drei Endpunkte', () => {
|
||||
expect(cfg.controllers.base.ip).toBe('fluidNcBase.local');
|
||||
expect(cfg.controllers.base.port).toBe(2300);
|
||||
expect(cfg.controllers.base.protocol).toBe('telnet');
|
||||
expect(cfg.controllers.elbow.port).toBe(5000);
|
||||
expect(cfg.controllers.hand.ip).toBe('fluidNcHand.local');
|
||||
});
|
||||
|
||||
test('axesByController gibt korrektes Array zurück', () => {
|
||||
expect(cfg.axesByController('base')).toEqual(['x', 'y', 'z']);
|
||||
expect(cfg.axesByController('elbow')).toEqual(['a', null, null]);
|
||||
expect(cfg.axesByController('hand')).toEqual(['c', 'e', 'b']);
|
||||
});
|
||||
|
||||
test('axesByController gibt [] für unbekannten Key zurück', () => {
|
||||
expect(cfg.axesByController('unknown')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RobotConfig.load — Fehlerbehandlung', () => {
|
||||
test('fehlende robot.json → Defaults', () => {
|
||||
const cfg = load(makeFailFs(), {}, log);
|
||||
expect(cfg.kinematics.l1).toBe(DEFAULTS.kinematics.l1);
|
||||
expect(cfg.motion.defaultFeedrate).toBe(DEFAULTS.motion.defaultFeedrate);
|
||||
expect(cfg.controllers.base.ip).toBe(DEFAULTS.controllers.base.ip);
|
||||
});
|
||||
|
||||
test('ungültiges JSON → Defaults', () => {
|
||||
const cfg = load(makeFs('{ not valid json }'), {}, log);
|
||||
expect(cfg.kinematics.type).toBe(DEFAULTS.kinematics.type);
|
||||
});
|
||||
|
||||
test('fehlende links → kinematics-Defaults', () => {
|
||||
const json = { ...FULL_ROBOT_JSON };
|
||||
delete json.links;
|
||||
const cfg = load(makeFs(JSON.stringify(json)), {}, log);
|
||||
expect(cfg.kinematics.l1).toBe(DEFAULTS.kinematics.l1);
|
||||
expect(cfg.kinematics.l3).toBe(DEFAULTS.kinematics.l3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RobotConfig.load — Env-Override', () => {
|
||||
test('ROBOT_DEFAULT_FEEDRATE überschreibt robot.json', () => {
|
||||
const env = { ROBOT_DEFAULT_FEEDRATE: '5000' };
|
||||
const cfg = load(makeFs(JSON.stringify(FULL_ROBOT_JSON)), env, log);
|
||||
expect(cfg.motion.defaultFeedrate).toBe(5000);
|
||||
});
|
||||
|
||||
test('ROBOT_SPEED_MODE=correct setzt useSpeedCalc=true', () => {
|
||||
const env = { ROBOT_SPEED_MODE: 'correct' };
|
||||
const cfg = load(makeFs(JSON.stringify(FULL_ROBOT_JSON)), env, log);
|
||||
expect(cfg.motion.speedMode).toBe('correct');
|
||||
expect(cfg.motion.useSpeedCalc).toBe(true);
|
||||
});
|
||||
|
||||
test('GRBL_BASE_IP überschreibt controller.base.ip', () => {
|
||||
const env = { GRBL_BASE_IP: '192.168.1.10' };
|
||||
const cfg = load(makeFs(JSON.stringify(FULL_ROBOT_JSON)), env, log);
|
||||
expect(cfg.controllers.base.ip).toBe('192.168.1.10');
|
||||
});
|
||||
|
||||
test('GRBL_ELLBOW_IP überschreibt controller.elbow.ip', () => {
|
||||
const env = { GRBL_ELLBOW_IP: '192.168.1.11' };
|
||||
const cfg = load(makeFs(JSON.stringify(FULL_ROBOT_JSON)), env, log);
|
||||
expect(cfg.controllers.elbow.ip).toBe('192.168.1.11');
|
||||
});
|
||||
|
||||
test('GRBL_HAND_IP überschreibt controller.hand.ip', () => {
|
||||
const env = { GRBL_HAND_IP: '192.168.1.12' };
|
||||
const cfg = load(makeFs(JSON.stringify(FULL_ROBOT_JSON)), env, log);
|
||||
expect(cfg.controllers.hand.ip).toBe('192.168.1.12');
|
||||
});
|
||||
});
|
||||
|
||||
describe('RobotConfig.load — speedMode correct', () => {
|
||||
test('speedMode correct aus robot.json setzt useSpeedCalc=true', () => {
|
||||
const json = { ...FULL_ROBOT_JSON, motion: { ...FULL_ROBOT_JSON.motion, speedMode: 'correct' } };
|
||||
const cfg = load(makeFs(JSON.stringify(json)), {}, log);
|
||||
expect(cfg.motion.useSpeedCalc).toBe(true);
|
||||
});
|
||||
|
||||
test('ROBOT_USE_SPEED_CALC=true setzt useSpeedCalc', () => {
|
||||
const env = { ROBOT_USE_SPEED_CALC: 'true' };
|
||||
const cfg = load(makeFs(JSON.stringify(FULL_ROBOT_JSON)), env, log);
|
||||
expect(cfg.motion.useSpeedCalc).toBe(true);
|
||||
});
|
||||
});
|
||||
272
test/RobotConfigService.test.js
Normal file
272
test/RobotConfigService.test.js
Normal file
@@ -0,0 +1,272 @@
|
||||
const fs = require('fs');
|
||||
const https = require('https');
|
||||
const path = require('path');
|
||||
const express = require('express');
|
||||
|
||||
const service = require('../server/RobotConfigService');
|
||||
|
||||
const DATA_DIR = path.join(__dirname, '..', 'data', 'robot');
|
||||
const ROBOT_JSON = path.join(DATA_DIR, 'robot.json');
|
||||
const TEST_KEY = 'test-api-key-12345';
|
||||
|
||||
const SAMPLE_ROBOT = {
|
||||
links: {
|
||||
Arm1: { size: [70, 250, 70] },
|
||||
Arm2: { size: [70, 250, 70] },
|
||||
Ellbow: { size: [90, 0, 0] }
|
||||
}
|
||||
};
|
||||
|
||||
// ── HTTPS-Test-Server ─────────────────────────────────────────────────────────
|
||||
|
||||
function makeServer(apiKey) {
|
||||
const app = express();
|
||||
service.register(app, { apiKey });
|
||||
const key = fs.readFileSync('https/localhost.key');
|
||||
const cert = fs.readFileSync('https/localhost.pem');
|
||||
return https.createServer({ key, cert, passphrase: 'abcd' }, app);
|
||||
}
|
||||
|
||||
function listen(server) {
|
||||
return new Promise((resolve, reject) => {
|
||||
server.listen(0, () => {
|
||||
const addr = server.address();
|
||||
addr ? resolve(addr.port) : reject(new Error('no port'));
|
||||
});
|
||||
server.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
function closeServer(server) {
|
||||
return new Promise(resolve => { server.close(resolve); });
|
||||
}
|
||||
|
||||
function request(method, url, { body, token } = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const agent = new https.Agent({ rejectUnauthorized: false });
|
||||
const payload = body ? JSON.stringify(body) : undefined;
|
||||
const headers = {
|
||||
...(payload ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) } : {}),
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||||
};
|
||||
const req = https.request(url, { method, agent, headers }, (res) => {
|
||||
let data = '';
|
||||
res.on('data', chunk => { data += chunk; });
|
||||
res.on('end', () => resolve({ statusCode: res.statusCode, body: data }));
|
||||
});
|
||||
req.on('error', reject);
|
||||
if (payload) req.write(payload);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// ── Setup / Teardown ──────────────────────────────────────────────────────────
|
||||
|
||||
function cleanupTestFiles() {
|
||||
for (const f of fs.readdirSync(DATA_DIR)) {
|
||||
if (f === '.gitkeep' || f === '.apikey') continue;
|
||||
fs.unlinkSync(path.join(DATA_DIR, f));
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
cleanupTestFiles();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanupTestFiles();
|
||||
});
|
||||
|
||||
// ── Einzel-Funktionen ─────────────────────────────────────────────────────────
|
||||
|
||||
describe('readRobotJson / writeRobotJson', () => {
|
||||
test('writeRobotJson schreibt Datei und legt Snapshot an', async () => {
|
||||
fs.writeFileSync(ROBOT_JSON, JSON.stringify({ version: 1 }), 'utf8');
|
||||
const { snapshotFile } = await service.writeRobotJson({ version: 2 });
|
||||
|
||||
expect(snapshotFile).toMatch(/^robot_\d{8}_\d{6}\.json$/);
|
||||
expect(fs.existsSync(path.join(DATA_DIR, snapshotFile))).toBe(true);
|
||||
|
||||
const written = JSON.parse(fs.readFileSync(ROBOT_JSON, 'utf8'));
|
||||
expect(written).toEqual({ version: 2 });
|
||||
|
||||
const snapshot = JSON.parse(fs.readFileSync(path.join(DATA_DIR, snapshotFile), 'utf8'));
|
||||
expect(snapshot).toEqual({ version: 1 });
|
||||
});
|
||||
|
||||
test('kein Snapshot wenn robot.json noch nicht existiert', async () => {
|
||||
const { snapshotFile } = await service.writeRobotJson({ fresh: true });
|
||||
expect(fs.existsSync(path.join(DATA_DIR, snapshotFile))).toBe(false);
|
||||
});
|
||||
|
||||
test('readRobotJson liest die aktuelle Datei', async () => {
|
||||
fs.writeFileSync(ROBOT_JSON, JSON.stringify(SAMPLE_ROBOT), 'utf8');
|
||||
const data = await service.readRobotJson();
|
||||
expect(data).toEqual(SAMPLE_ROBOT);
|
||||
});
|
||||
|
||||
test('readRobotJson wirft, wenn robot.json fehlt', async () => {
|
||||
await expect(service.readRobotJson()).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('listHistory / readSnapshot', () => {
|
||||
test('listHistory gibt leere Liste zurück wenn kein Snapshot', async () => {
|
||||
const list = await service.listHistory();
|
||||
expect(list).toEqual([]);
|
||||
});
|
||||
|
||||
test('listHistory listet vorhandene Snapshots, neueste zuerst', async () => {
|
||||
fs.writeFileSync(path.join(DATA_DIR, 'robot_20260101_120000.json'), '{}', 'utf8');
|
||||
fs.writeFileSync(path.join(DATA_DIR, 'robot_20260102_083000.json'), '{}', 'utf8');
|
||||
|
||||
const list = await service.listHistory();
|
||||
expect(list.map(e => e.filename)).toEqual([
|
||||
'robot_20260102_083000.json',
|
||||
'robot_20260101_120000.json'
|
||||
]);
|
||||
expect(list[0]).toEqual({ filename: 'robot_20260102_083000.json', day: '20260102', timestamp: '083000' });
|
||||
});
|
||||
|
||||
test('readSnapshot liest einen bestimmten Snapshot', async () => {
|
||||
const ts = '20260101_120000';
|
||||
fs.writeFileSync(path.join(DATA_DIR, `robot_${ts}.json`), JSON.stringify({ x: 42 }), 'utf8');
|
||||
const data = await service.readSnapshot(ts);
|
||||
expect(data).toEqual({ x: 42 });
|
||||
});
|
||||
|
||||
test('readSnapshot wirft bei unbekanntem Timestamp', async () => {
|
||||
await expect(service.readSnapshot('99999999_999999')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pruning', () => {
|
||||
test('≤ 100 Snapshots pro Tag bleiben alle erhalten', async () => {
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
fs.writeFileSync(path.join(DATA_DIR, `robot_2026061${i}_120000.json`), '{}', 'utf8');
|
||||
}
|
||||
fs.writeFileSync(ROBOT_JSON, '{}', 'utf8');
|
||||
await service.writeRobotJson({ pruned: true });
|
||||
|
||||
const list = await service.listHistory();
|
||||
// 5 bestehende (verschiedene Tage) + 1 neuer Snapshot = 6
|
||||
expect(list.length).toBe(6);
|
||||
});
|
||||
|
||||
test('> 100 Snapshots eines Tages: nur neuester bleibt', async () => {
|
||||
const day = '20260101';
|
||||
for (let i = 0; i < 102; i++) {
|
||||
const hh = String(Math.floor(i / 3600)).padStart(2, '0');
|
||||
const mm = String(Math.floor((i % 3600) / 60)).padStart(2, '0');
|
||||
const ss = String(i % 60).padStart(2, '0');
|
||||
fs.writeFileSync(path.join(DATA_DIR, `robot_${day}_${hh}${mm}${ss}.json`), `{"i":${i}}`, 'utf8');
|
||||
}
|
||||
// Neuester (lexikographisch letzter) vorher anlegen
|
||||
const newest = `robot_${day}_235959.json`;
|
||||
fs.writeFileSync(path.join(DATA_DIR, newest), '{"newest":true}', 'utf8');
|
||||
|
||||
fs.writeFileSync(ROBOT_JSON, '{}', 'utf8');
|
||||
await service.writeRobotJson({ trigger: true });
|
||||
|
||||
const list = await service.listHistory();
|
||||
const daySnapshots = list.filter(e => e.day === day);
|
||||
expect(daySnapshots.length).toBe(1);
|
||||
expect(daySnapshots[0].filename).toBe(newest);
|
||||
});
|
||||
});
|
||||
|
||||
// ── HTTP-Routen ───────────────────────────────────────────────────────────────
|
||||
|
||||
describe('HTTP GET /api/robot', () => {
|
||||
let server, port;
|
||||
|
||||
beforeEach(async () => { server = makeServer(TEST_KEY); port = await listen(server); });
|
||||
afterEach(async () => { await closeServer(server); });
|
||||
|
||||
test('404 wenn robot.json nicht existiert', async () => {
|
||||
const { statusCode } = await request('GET', `https://127.0.0.1:${port}/api/robot`);
|
||||
expect(statusCode).toBe(404);
|
||||
});
|
||||
|
||||
test('200 mit robot.json-Inhalt', async () => {
|
||||
fs.writeFileSync(ROBOT_JSON, JSON.stringify(SAMPLE_ROBOT), 'utf8');
|
||||
const { statusCode, body } = await request('GET', `https://127.0.0.1:${port}/api/robot`);
|
||||
expect(statusCode).toBe(200);
|
||||
expect(JSON.parse(body)).toEqual(SAMPLE_ROBOT);
|
||||
});
|
||||
});
|
||||
|
||||
describe('HTTP PUT /api/robot', () => {
|
||||
let server, port;
|
||||
|
||||
beforeEach(async () => { server = makeServer(TEST_KEY); port = await listen(server); });
|
||||
afterEach(async () => { await closeServer(server); });
|
||||
|
||||
test('401 ohne Authorization-Header', async () => {
|
||||
const { statusCode } = await request('PUT', `https://127.0.0.1:${port}/api/robot`, { body: SAMPLE_ROBOT });
|
||||
expect(statusCode).toBe(401);
|
||||
});
|
||||
|
||||
test('401 mit falschem Key', async () => {
|
||||
const { statusCode } = await request('PUT', `https://127.0.0.1:${port}/api/robot`, {
|
||||
body: SAMPLE_ROBOT, token: 'wrong-key'
|
||||
});
|
||||
expect(statusCode).toBe(401);
|
||||
});
|
||||
|
||||
test('200 mit korrektem Key — schreibt Datei', async () => {
|
||||
const { statusCode, body } = await request('PUT', `https://127.0.0.1:${port}/api/robot`, {
|
||||
body: SAMPLE_ROBOT, token: TEST_KEY
|
||||
});
|
||||
expect(statusCode).toBe(200);
|
||||
const result = JSON.parse(body);
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.snapshotFile).toMatch(/^robot_\d{8}_\d{6}\.json$/);
|
||||
|
||||
const written = JSON.parse(fs.readFileSync(ROBOT_JSON, 'utf8'));
|
||||
expect(written).toEqual(SAMPLE_ROBOT);
|
||||
});
|
||||
});
|
||||
|
||||
describe('HTTP GET /api/robot/history', () => {
|
||||
let server, port;
|
||||
|
||||
beforeEach(async () => { server = makeServer(TEST_KEY); port = await listen(server); });
|
||||
afterEach(async () => { await closeServer(server); });
|
||||
|
||||
test('200 mit leerer History', async () => {
|
||||
const { statusCode, body } = await request('GET', `https://127.0.0.1:${port}/api/robot/history`);
|
||||
expect(statusCode).toBe(200);
|
||||
expect(JSON.parse(body)).toEqual({ history: [] });
|
||||
});
|
||||
|
||||
test('200 listet vorhandene Snapshots', async () => {
|
||||
fs.writeFileSync(path.join(DATA_DIR, 'robot_20260611_100000.json'), '{}', 'utf8');
|
||||
const { statusCode, body } = await request('GET', `https://127.0.0.1:${port}/api/robot/history`);
|
||||
expect(statusCode).toBe(200);
|
||||
const { history } = JSON.parse(body);
|
||||
expect(history).toHaveLength(1);
|
||||
expect(history[0].filename).toBe('robot_20260611_100000.json');
|
||||
});
|
||||
});
|
||||
|
||||
describe('HTTP GET /api/robot/history/:ts', () => {
|
||||
let server, port;
|
||||
|
||||
beforeEach(async () => { server = makeServer(TEST_KEY); port = await listen(server); });
|
||||
afterEach(async () => { await closeServer(server); });
|
||||
|
||||
test('200 für vorhandenen Snapshot', async () => {
|
||||
const ts = '20260611_100000';
|
||||
fs.writeFileSync(path.join(DATA_DIR, `robot_${ts}.json`), JSON.stringify({ snap: true }), 'utf8');
|
||||
const { statusCode, body } = await request('GET', `https://127.0.0.1:${port}/api/robot/history/${ts}`);
|
||||
expect(statusCode).toBe(200);
|
||||
expect(JSON.parse(body)).toEqual({ snap: true });
|
||||
});
|
||||
|
||||
test('404 für unbekannten Snapshot', async () => {
|
||||
const { statusCode } = await request('GET', `https://127.0.0.1:${port}/api/robot/history/99991231_235959`);
|
||||
expect(statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
@@ -43,7 +43,7 @@ describe('startRobot orchestrator', () => {
|
||||
consoleObj: { log: jest.fn(), warn: jest.fn(), error: jest.fn() }
|
||||
});
|
||||
|
||||
expect(readFileSync).toHaveBeenCalledTimes(2);
|
||||
expect(readFileSync).toHaveBeenCalledTimes(3); // key, cert, data/robot/robot.json
|
||||
expect(httpsModuleMock.createServer).toHaveBeenCalledWith({
|
||||
enable: true,
|
||||
key: 'fake-key',
|
||||
@@ -61,7 +61,8 @@ describe('startRobot orchestrator', () => {
|
||||
expect.objectContaining({ name: 'Base', instance: expect.any(Object) }),
|
||||
expect.objectContaining({ name: 'Elbow', instance: expect.any(Object) }),
|
||||
expect.objectContaining({ name: 'Hand', instance: expect.any(Object) })
|
||||
])
|
||||
]),
|
||||
expect.objectContaining({}) // options: { apiKey }
|
||||
);
|
||||
|
||||
expect(httpsServerMock.listen).toHaveBeenCalledWith(2095);
|
||||
|
||||
Reference in New Issue
Block a user