From d36ef6189d42f26df669ed9e11405bf902196bd3 Mon Sep 17 00:00:00 2001 From: chk <79915315+ChKendel@users.noreply.github.com> Date: Wed, 17 Jun 2026 23:23:55 +0200 Subject: [PATCH] Homing API --- doc/Homing.md | 2 +- doc/accessRobotAPI.md | 8 +++++-- server/editRobot.js | 10 ++++----- server/robotConfig.js | 44 ++++++++++++++++++++++++++++++++++++ server/server.js | 52 ++++++++++++++++++++++++++----------------- 5 files changed, 88 insertions(+), 28 deletions(-) create mode 100644 server/robotConfig.js diff --git a/doc/Homing.md b/doc/Homing.md index 567ccb2..dd63f27 100644 --- a/doc/Homing.md +++ b/doc/Homing.md @@ -159,4 +159,4 @@ die aktuelle Konfiguration. - [x] **X-Schätzung verfeinern** (2026-06-14): `estimateXFromMarkers()` rechnet den kinematischen Gelenk-Offset heraus statt rohem Mittelwert — behebt den ~110 mm Versatz der Modell-Marker - [x] **Unit-Test für X-Schätzung** (2026-06-14): reine Geometrie nach `server/homingXEstimate.cjs` ausgelöst, `test/homingXEstimate.test.js` (9 Tests, inkl. Regression gegen den Offset-Bug) - [ ] **y-Restfehler** (~2°): erkannt 30° → ausgegeben 28°; vermutlich X-Rest-Rauschen + 4b-Fit-Residuum, noch zu untersuchen -- [ ] **robot.json via Driver-API** (optional): wenn Driver `GET ROBOT_URL/api/robot/config` bereitstellt +- [x] **robot.json via Driver-API** (2026-06-17): `server/robotConfig.js` — `fetchRobot()`/`pushRobot()`/`robotCachePath`; automatischer Fallback auf lokale Datei wenn `ROBOT_URL` nicht gesetzt diff --git a/doc/accessRobotAPI.md b/doc/accessRobotAPI.md index dc820f0..db4be8e 100644 --- a/doc/accessRobotAPI.md +++ b/doc/accessRobotAPI.md @@ -247,9 +247,13 @@ try { --- +## Status: Umgesetzt (2026-06-17) + +`server/robotConfig.js` erstellt. `server/editRobot.js` und `server/server.js` angepasst. + ## Offene Fragen -- [ ] Genaue Endpoints des appRobotDriver für GET / POST robot.json bestätigen +- [ ] Genaue Endpoints des appRobotDriver für GET / POST robot.json bestätigen (aktuell: `/api/robot/config`) - [ ] Soll der Driver eine Versions-/Konflikterkennung haben (z.B. ETag / `updatedAt`)? -- [ ] Soll `pushRobot()` bei Driver-Fehler still auf lokal-only zurückfallen, oder hard fail? +- [ ] `pushRobot()` bei Driver-Fehler: aktuell hard fail → Kalibrierungs-Endpoint antwortet 502 - [ ] Authentifizierung zwischen appRobotHoming und appRobotDriver nötig? diff --git a/server/editRobot.js b/server/editRobot.js index 7a86740..bef3539 100644 --- a/server/editRobot.js +++ b/server/editRobot.js @@ -5,18 +5,18 @@ * atomisches Write per Temp-Datei ist hier nicht nötig – die Datei wird direkt * überschrieben; bei Bedarf Backup-Strategie ergänzen). */ -import fsPromises from 'fs/promises'; import { createRequire } from 'module'; +import { fetchRobot, pushRobot } from './robotConfig.js'; const { normalizeSpinDeg } = createRequire(import.meta.url)('./spinNormalize.cjs'); // ── I/O ─────────────────────────────────────────────────────────────────────── -async function readRobot(robotPath) { - return JSON.parse(await fsPromises.readFile(robotPath, 'utf8')); +async function readRobot(_robotPath) { + return fetchRobot(); } -async function writeRobot(robotPath, data) { - await fsPromises.writeFile(robotPath, JSON.stringify(data, null, 2), 'utf8'); +async function writeRobot(_robotPath, data) { + return pushRobot(data); } // ── Aktion 1: Marker nach Z-Bereich zuordnen ───────────────────────────────── diff --git a/server/robotConfig.js b/server/robotConfig.js new file mode 100644 index 0000000..2fca367 --- /dev/null +++ b/server/robotConfig.js @@ -0,0 +1,44 @@ +import fsPromises from 'fs/promises'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const ROBOT_URL = process.env.ROBOT_URL || ''; +const ROBOT_JSON = process.env.ROBOT_JSON + || path.join(__dirname, '..', 'scripts', 'robot_1781069752019.json'); + +/** + * Lädt robot.json. + * Reihenfolge: (1) ROBOT_URL/api/robot/config, (2) lokale Datei als Fallback. + * Schreibt das Ergebnis immer in die lokale Cache-Datei (für Python-Skripte). + */ +export async function fetchRobot() { + if (ROBOT_URL) { + const res = await fetch(new URL('/api/robot/config', ROBOT_URL)); + if (!res.ok) throw new Error(`Driver ${res.status}: ${await res.text()}`); + const data = await res.json(); + await fsPromises.writeFile(ROBOT_JSON, JSON.stringify(data, null, 2), 'utf8'); + return data; + } + return JSON.parse(await fsPromises.readFile(ROBOT_JSON, 'utf8')); +} + +/** + * Speichert robot.json. + * Schreibt immer in lokale Cache-Datei; sendet zusätzlich an Driver wenn konfiguriert. + */ +export async function pushRobot(data) { + await fsPromises.writeFile(ROBOT_JSON, JSON.stringify(data, null, 2), 'utf8'); + if (ROBOT_URL) { + const res = await fetch(new URL('/api/robot/config', ROBOT_URL), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); + if (!res.ok) throw new Error(`Driver ${res.status}: ${await res.text()}`); + } +} + +/** Pfad zur lokalen Cache-Datei – wird an Python-Skripte als -robot-Argument übergeben. */ +export const robotCachePath = ROBOT_JSON; diff --git a/server/server.js b/server/server.js index b5b884b..03a0a78 100755 --- a/server/server.js +++ b/server/server.js @@ -10,6 +10,7 @@ import { spawn } from 'child_process'; import { WebcamClient } from './webcamClient.js'; import { assignByZRange, removeMarkerAssignment, alignSetToMeasured, assignMarkerId, adoptXAxis, assignFixedMarkersToLink, setJointOriginYZ, setArmMarkerSpin } from './editRobot.js'; import { runHoming } from './homingOrchestrator.js'; +import { fetchRobot, robotCachePath } from './robotConfig.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -439,8 +440,6 @@ app.post('/api/calibration/compute', async (req, res) => { const boardDataDir = path.join(__dirname, '..', 'data', 'board'); const homingDataDir = path.join(__dirname, '..', 'data', 'homing'); -const ROBOT_JSON = process.env.ROBOT_JSON - || path.join(__dirname, '..', 'scripts', 'robot_1781069752019.json'); const SCRIPT_1 = path.join(__dirname, '..', 'scripts', '1_detect_aruco_observations.py'); const SCRIPT_2 = path.join(__dirname, '..', 'scripts', '2_estimate_camera_from_observations.py'); const SCRIPT_3B = path.join(__dirname, '..', 'scripts', '3b_corner_marker_poses.py'); @@ -496,6 +495,12 @@ function runScript(args, send) { * @param {{ refSet?: string }} [opts] */ async function runBoardPipeline(runDir, send, { refSet } = {}) { + try { + await fetchRobot(); + } catch (err) { + send({ type: 'log', text: `⚠ robot.json-Cache: Driver nicht erreichbar – nutze lokale Datei (${err.message})` }); + } + // Kameras ermitteln if (!WEBCAM_URL) throw new Error('WEBCAM_URL nicht konfiguriert'); const camData = await new WebcamClient(WEBCAM_URL).getCameras(); @@ -538,7 +543,7 @@ async function runBoardPipeline(runDir, send, { refSet } = {}) { SCRIPT_1, '-i', imgPath, '-npz', npzPath, - '-robot', ROBOT_JSON, + '-robot', robotCachePath, '-cameraId', camId, '-outDir', runDir, '--saveDebugImage', @@ -556,7 +561,7 @@ async function runBoardPipeline(runDir, send, { refSet } = {}) { continue; } send({ type: 'log', text: '\n▷ 2_estimate_camera_from_observations' }); - const script2Args = [SCRIPT_2, '-i', detJson, '-robot', ROBOT_JSON, '-outDir', runDir]; + const script2Args = [SCRIPT_2, '-i', detJson, '-robot', robotCachePath, '-outDir', runDir]; if (refSet) script2Args.push('--refSet', refSet); const exit2 = await runScript(script2Args, send); if (exit2 !== 0) send({ type: 'log', text: `❌ Script 2 Exit ${exit2}` }); @@ -574,7 +579,7 @@ async function runBoardPipeline(runDir, send, { refSet } = {}) { const exit3b = await runScript([ SCRIPT_3B, '--evalDir', runDir, - '--robot', ROBOT_JSON, + '--robot', robotCachePath, ], send); if (exit3b !== 0) send({ type: 'log', text: `❌ Script 3b Exit ${exit3b}` }); } else { @@ -612,13 +617,13 @@ app.post('/api/board/run', async (req, res) => { // Robot-JSON laden und Marker-Anzahl loggen let robotData = null; - try { robotData = JSON.parse(await fsPromises.readFile(ROBOT_JSON, 'utf8')); } catch {} + try { robotData = JSON.parse(await fsPromises.readFile(robotCachePath, 'utf8')); } catch {} const boardMarkers = robotData?.links?.Board?.markers ?? []; const boardMarkerCount = boardMarkers.length; const refMarkerCount = refSet ? boardMarkers.filter(m => m.set === refSet).length : boardMarkerCount; - send({ type: 'log', text: `▶ Robot-JSON: ${ROBOT_JSON}` }); + send({ type: 'log', text: `▶ Robot-JSON: ${robotCachePath}` }); send({ type: 'log', text: `▶ Board-Marker: ${boardMarkerCount} (links.Board.markers)` }); send({ type: 'log', text: `▶ Referenz-Set: ${refSet ? `"${refSet}" (${refMarkerCount} Marker)` : 'alle'}` }); send({ type: 'log', text: '' }); @@ -703,7 +708,7 @@ app.get('/api/board/latest', async (req, res) => { const runDir = path.join(dataDir, runName); let robot = null; - try { robot = JSON.parse(await fsPromises.readFile(ROBOT_JSON, 'utf8')); } catch {} + try { robot = JSON.parse(await fsPromises.readFile(robotCachePath, 'utf8')); } catch {} let files = []; try { files = await fsPromises.readdir(runDir); } catch {} @@ -746,7 +751,7 @@ app.get('/api/board/latest', async (req, res) => { measuredMarkers = JSON.parse(raw); } catch {} - return res.json({ runDir: runName, robotFile: path.basename(ROBOT_JSON), robot, detections, cameraPoses, measuredMarkers }); + return res.json({ runDir: runName, robotFile: path.basename(robotCachePath), robot, detections, cameraPoses, measuredMarkers }); } catch (err) { return res.status(500).json({ error: String(err) }); } @@ -771,7 +776,7 @@ app.post('/api/homing/run', async (req, res) => { try { await fsPromises.mkdir(homingDataDir, { recursive: true }); await runHoming({ - robotJsonPath: ROBOT_JSON, + robotJsonPath: robotCachePath, homingDir: homingDataDir, send, runScript, @@ -890,7 +895,7 @@ app.post('/api/robot/assign-by-z', async (req, res) => { } } catch { /* kein 3b-Output vorhanden – nur bestehende robot.json-Marker bearbeiten */ } - const result = await assignByZRange(ROBOT_JSON, { zMin, zMax, set, link, extraMarkers }); + const result = await assignByZRange(robotCachePath, { zMin, zMax, set, link, extraMarkers }); const added = result.changes.filter(c => c.action === 'added').length; const updated = result.changes.filter(c => c.action === 'updated').length; console.log(`robot/assign-by-z z=[${zMin}..${zMax}] set="${set}" link="${link}" → ${updated} aktualisiert, ${added} neu (von ${extraMarkers.length} 3b-Markern)`); @@ -915,7 +920,7 @@ app.post('/api/robot/remove-marker', async (req, res) => { if (!['set', 'link'].includes(removeFrom)) { return res.status(400).json({ error: 'removeFrom muss "set" oder "link" sein' }); } - const result = await removeMarkerAssignment(ROBOT_JSON, { markerId, removeFrom }); + const result = await removeMarkerAssignment(robotCachePath, { markerId, removeFrom }); console.log(`robot/remove-marker id=${markerId} from=${removeFrom} → changed=${result.changed}`); return res.json(result); } catch (err) { @@ -930,7 +935,7 @@ app.post('/api/robot/remove-marker', async (req, res) => { */ app.get('/api/robot', async (req, res) => { try { - const robot = JSON.parse(await fsPromises.readFile(ROBOT_JSON, 'utf8')); + const robot = await fetchRobot(); return res.json(robot); } catch (err) { return res.status(500).json({ error: String(err) }); @@ -944,7 +949,7 @@ app.get('/api/robot', async (req, res) => { */ app.get('/api/robot/board-sets', async (req, res) => { try { - const robot = JSON.parse(await fsPromises.readFile(ROBOT_JSON, 'utf8')); + const robot = await fetchRobot(); const markers = robot?.links?.Board?.markers ?? []; const sets = [...new Set(markers.map(m => m.set).filter(Boolean))].sort(); return res.json({ sets }); @@ -975,7 +980,7 @@ app.post('/api/robot/align-sets', async (req, res) => { } } catch { /* kein 3b-Output vorhanden */ } - const result = await alignSetToMeasured(ROBOT_JSON, { setToMove, extraMarkers }); + const result = await alignSetToMeasured(robotCachePath, { setToMove, extraMarkers }); if (result.error) return res.status(400).json(result); console.log( @@ -1010,7 +1015,7 @@ app.post('/api/robot/assign-id', async (req, res) => { } } catch { /* kein 3b-Output vorhanden */ } - const result = await assignMarkerId(ROBOT_JSON, { markerId, set, link, extraMarkers }); + const result = await assignMarkerId(robotCachePath, { markerId, set, link, extraMarkers }); if (!result.changed && result.error) return res.status(400).json(result); console.log( @@ -1036,7 +1041,7 @@ app.post('/api/robot/adopt-x-axis', async (req, res) => { if (!Array.isArray(direction) || direction.length < 3) { return res.status(400).json({ error: '"direction" muss ein Array [vx,vy,vz] sein.' }); } - const result = await adoptXAxis(ROBOT_JSON, { direction }); + const result = await adoptXAxis(robotCachePath, { direction }); console.log( `robot/adopt-x-axis dir=[${direction.map(v => Number(v).toFixed(4)).join(', ')}]` + ` → ${result.numChanged} Marker, Ursprung=[${result.origin.join(', ')}]` + @@ -1064,7 +1069,7 @@ app.post('/api/robot/assign-fixed-markers', async (req, res) => { if (!targetLink) { return res.status(400).json({ error: '"targetLink" muss angegeben werden.' }); } - const result = await assignFixedMarkersToLink(ROBOT_JSON, { markerIds, targetLink, measuredPositions }); + const result = await assignFixedMarkersToLink(robotCachePath, { markerIds, targetLink, measuredPositions }); console.log( `robot/assign-fixed-markers [${markerIds.join(',')}] → ${targetLink}` + ` added=${result.numAdded} alreadyPresent=${result.numAlreadyPresent}`, @@ -1089,7 +1094,7 @@ app.post('/api/robot/set-joint-origin', async (req, res) => { if (!Number.isFinite(Number(y)) || !Number.isFinite(Number(z))) { return res.status(400).json({ error: '"y" und "z" müssen Zahlen sein.' }); } - const result = await setJointOriginYZ(ROBOT_JSON, { linkName, y: Number(y), z: Number(z) }); + const result = await setJointOriginYZ(robotCachePath, { linkName, y: Number(y), z: Number(z) }); if (!result.changed) { return res.status(400).json({ error: result.error }); } @@ -1115,7 +1120,7 @@ app.post('/api/robot/set-arm-marker-spin', async (req, res) => { if (!linkName) return res.status(400).json({ error: '"linkName" muss angegeben werden.' }); if (markerId == null) return res.status(400).json({ error: '"markerId" muss angegeben werden.' }); if (!Number.isFinite(Number(spin))) return res.status(400).json({ error: '"spin" muss eine Zahl sein.' }); - const result = await setArmMarkerSpin(ROBOT_JSON, { linkName, markerId, spin: Number(spin) }); + const result = await setArmMarkerSpin(robotCachePath, { linkName, markerId, spin: Number(spin) }); if (!result.changed) return res.status(400).json({ error: result.error }); console.log(`robot/set-arm-marker-spin ${linkName}#${markerId}: ${result.oldSpin}° → ${result.newSpin}°`); return res.json(result); @@ -1268,6 +1273,13 @@ async function startServer() { await checkServiceReachability('BODYTRACKER_URL', new URL('/v1/health', BODYTRACKER_URL).toString()); } + try { + await fetchRobot(); + console.log('✅ robot.json geladen und gecacht.'); + } catch (err) { + console.warn(`⚠ robot.json: Driver nicht erreichbar – nutze lokale Datei: ${err.message}`); + } + const server = await createHttpsServer(); const isHttps = Boolean(server); const listenServer = server || app;