Konfig in robot.json
This commit is contained in:
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
|
||||
|
||||
Reference in New Issue
Block a user