Homing API

This commit is contained in:
chk
2026-06-17 23:23:55 +02:00
parent eb403dab36
commit d36ef6189d
5 changed files with 88 additions and 28 deletions

View File

@@ -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

View File

@@ -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?

View File

@@ -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 ─────────────────────────────────

44
server/robotConfig.js Normal file
View File

@@ -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;

View File

@@ -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;