Phase 0, 1, 2
This commit is contained in:
295
server/server.js
295
server/server.js
@@ -9,6 +9,7 @@ import process from 'process';
|
||||
import { spawn } from 'child_process';
|
||||
import { WebcamClient } from './webcamClient.js';
|
||||
import { assignByZRange, removeMarkerAssignment, alignSetToMeasured, assignMarkerId, adoptXAxis, assignFixedMarkersToLink, setJointOriginYZ } from './editRobot.js';
|
||||
import { runHoming } from './homingOrchestrator.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@@ -19,8 +20,9 @@ app.use(express.json({ limit: '20mb' }));
|
||||
const PORT = parseInt(process.env.PORT || process.env.HTTPS_PORT || '2093', 10);
|
||||
const publicDir = path.join(__dirname, '..', 'public');
|
||||
const snapshotsDir = path.join(publicDir, 'snapshots');
|
||||
const WEBCAM_URL = process.env.WEBCAM_URL || '';
|
||||
const WEBCAM_URL = process.env.WEBCAM_URL || '';
|
||||
const BODYTRACKER_URL = process.env.BODYTRACKER_URL || '';
|
||||
const ROBOT_URL = process.env.ROBOT_URL || '';
|
||||
const HTTPS_KEY_PATH = process.env.HTTPS_KEY_PATH || path.join(__dirname, '..', 'https', 'localhost.key');
|
||||
const HTTPS_CERT_PATH = process.env.HTTPS_CERT_PATH || path.join(__dirname, '..', 'https', 'localhost.pem');
|
||||
const HTTPS_PASSPHRASE = process.env.HTTPS_PASSPHRASE || 'abcd';
|
||||
@@ -430,11 +432,13 @@ app.post('/api/calibration/compute', async (req, res) => {
|
||||
// ── Board-Erkennung ───────────────────────────────────────────────────────────
|
||||
|
||||
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');
|
||||
const SCRIPT_4B = path.join(__dirname, '..', 'scripts', '4b_revolute_angle.py');
|
||||
|
||||
/**
|
||||
* Führt ein Python-Script aus und leitet stdout/stderr zeilenweise an `send` weiter.
|
||||
@@ -473,6 +477,103 @@ function runScript(args, send) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Board-Pipeline: Snapshot + Script 1 + Script 2 (pro Kamera) + Script 3b.
|
||||
* Schreibt Ergebnisse nach runDir (muss bereits existieren).
|
||||
* Wird von /api/board/run UND /api/homing/run genutzt.
|
||||
*
|
||||
* @param {string} runDir – Zielverzeichnis (bereits erstellt)
|
||||
* @param {Function} send – SSE-Send-Funktion (obj => void)
|
||||
* @param {{ refSet?: string }} [opts]
|
||||
*/
|
||||
async function runBoardPipeline(runDir, send, { refSet } = {}) {
|
||||
// Kameras ermitteln
|
||||
if (!WEBCAM_URL) throw new Error('WEBCAM_URL nicht konfiguriert');
|
||||
const camData = await new WebcamClient(WEBCAM_URL).getCameras();
|
||||
const cameraIds = (camData.cameras ?? []).map(c => c.id);
|
||||
send({ type: 'log', text: `▶ Kameras: ${cameraIds.join(', ')}` });
|
||||
send({ type: 'log', text: '' });
|
||||
|
||||
// Pro Kamera: Foto → Script 1 → Script 2
|
||||
for (const camId of cameraIds) {
|
||||
send({ type: 'log', text: `─── ${camId} ${'─'.repeat(40 - camId.length)}` });
|
||||
|
||||
// Snapshot
|
||||
send({ type: 'log', text: 'Foto aufnehmen …' });
|
||||
let snapResp;
|
||||
for (let attempt = 1; attempt <= 2; attempt++) {
|
||||
snapResp = await new WebcamClient(WEBCAM_URL).getSnapshot(camId, true);
|
||||
if (snapResp.status !== 503) break;
|
||||
if (attempt < 2) await new Promise(r => setTimeout(r, 2000));
|
||||
}
|
||||
if (!snapResp.ok) {
|
||||
send({ type: 'log', text: `⚠ HTTP ${snapResp.status} – Kamera übersprungen` });
|
||||
continue;
|
||||
}
|
||||
const imgPath = path.join(runDir, `${camId}.jpg`);
|
||||
await fsPromises.writeFile(imgPath, Buffer.from(await snapResp.arrayBuffer()));
|
||||
send({ type: 'log', text: `✅ Foto: ${camId}.jpg` });
|
||||
|
||||
// NPZ suchen – neueste Session, die eine NPZ für diese Kamera enthält
|
||||
const npzInfo = await findLatestNpzForCamera(camId);
|
||||
if (!npzInfo) {
|
||||
send({ type: 'log', text: `⚠ Keine NPZ für ${camId} gefunden – übersprungen` });
|
||||
continue;
|
||||
}
|
||||
const npzPath = npzInfo.npzPath;
|
||||
send({ type: 'log', text: `▶ NPZ: data/calibration/${npzInfo.session}/${camId}_calibration.npz` });
|
||||
|
||||
// Script 1 – ArUco-Erkennung
|
||||
send({ type: 'log', text: '\n▷ 1_detect_aruco_observations' });
|
||||
const exit1 = await runScript([
|
||||
SCRIPT_1,
|
||||
'-i', imgPath,
|
||||
'-npz', npzPath,
|
||||
'-robot', ROBOT_JSON,
|
||||
'-cameraId', camId,
|
||||
'-outDir', runDir,
|
||||
'--saveDebugImage',
|
||||
], send);
|
||||
if (exit1 !== 0) {
|
||||
send({ type: 'log', text: `❌ Script 1 Exit ${exit1}` });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Script 2 – Kamera-Pose schätzen
|
||||
const detJson = path.join(runDir, `${camId}_aruco_detection.json`);
|
||||
try { await fsPromises.access(detJson); }
|
||||
catch {
|
||||
send({ type: 'log', text: '⚠ Detection-JSON fehlt – Script 2 übersprungen' });
|
||||
continue;
|
||||
}
|
||||
send({ type: 'log', text: '\n▷ 2_estimate_camera_from_observations' });
|
||||
const script2Args = [SCRIPT_2, '-i', detJson, '-robot', ROBOT_JSON, '-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}` });
|
||||
|
||||
send({ type: 'log', text: '' });
|
||||
}
|
||||
|
||||
// Script 3b: Marker-Triangulierung (benötigt ≥2 Kamera-Posen)
|
||||
send({ type: 'log', text: '' });
|
||||
send({ type: 'log', text: '─── 3b: Marker-Triangulierung ────────────────────────────' });
|
||||
const runFiles3b = await fsPromises.readdir(runDir);
|
||||
const numPoses = runFiles3b.filter(f => f.endsWith('_camera_pose.json')).length;
|
||||
if (numPoses >= 2) {
|
||||
send({ type: 'log', text: `▷ 3b_corner_marker_poses (${numPoses} Kamera-Posen)` });
|
||||
const exit3b = await runScript([
|
||||
SCRIPT_3B,
|
||||
'--evalDir', runDir,
|
||||
'--robot', ROBOT_JSON,
|
||||
], send);
|
||||
if (exit3b !== 0) send({ type: 'log', text: `❌ Script 3b Exit ${exit3b}` });
|
||||
} else {
|
||||
send({ type: 'log', text: `⚠ Nur ${numPoses} Kamera-Pose(n) – Script 3b braucht ≥2 Kameras` });
|
||||
}
|
||||
send({ type: 'log', text: '' });
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/board/run
|
||||
* 1. Erstellt data/board/{timestamp}/
|
||||
@@ -513,94 +614,8 @@ app.post('/api/board/run', async (req, res) => {
|
||||
send({ type: 'log', text: `▶ Referenz-Set: ${refSet ? `"${refSet}" (${refMarkerCount} Marker)` : 'alle'}` });
|
||||
send({ type: 'log', text: '' });
|
||||
|
||||
// 2. Kameras ermitteln
|
||||
if (!WEBCAM_URL) throw new Error('WEBCAM_URL nicht konfiguriert');
|
||||
const camData = await new WebcamClient(WEBCAM_URL).getCameras();
|
||||
const cameraIds = (camData.cameras ?? []).map(c => c.id);
|
||||
send({ type: 'log', text: `▶ Kameras: ${cameraIds.join(', ')}` });
|
||||
send({ type: 'log', text: '' });
|
||||
|
||||
// 3. Pro Kamera: Foto → Script 1 → Script 2
|
||||
for (const camId of cameraIds) {
|
||||
send({ type: 'log', text: `─── ${camId} ${'─'.repeat(40 - camId.length)}` });
|
||||
|
||||
// Snapshot
|
||||
send({ type: 'log', text: 'Foto aufnehmen …' });
|
||||
let snapResp;
|
||||
for (let attempt = 1; attempt <= 2; attempt++) {
|
||||
snapResp = await new WebcamClient(WEBCAM_URL).getSnapshot(camId, true);
|
||||
if (snapResp.status !== 503) break;
|
||||
if (attempt < 2) await new Promise(r => setTimeout(r, 2000));
|
||||
}
|
||||
if (!snapResp.ok) {
|
||||
send({ type: 'log', text: `⚠ HTTP ${snapResp.status} – Kamera übersprungen` });
|
||||
continue;
|
||||
}
|
||||
const imgPath = path.join(runDir, `${camId}.jpg`);
|
||||
await fsPromises.writeFile(imgPath, Buffer.from(await snapResp.arrayBuffer()));
|
||||
send({ type: 'log', text: `✅ Foto: ${camId}.jpg` });
|
||||
|
||||
// NPZ suchen – neueste Session, die eine NPZ für diese Kamera enthält
|
||||
const npzInfo = await findLatestNpzForCamera(camId);
|
||||
if (!npzInfo) {
|
||||
send({ type: 'log', text: `⚠ Keine NPZ für ${camId} gefunden (in keiner Kalibrierungs-Session) – übersprungen` });
|
||||
continue;
|
||||
}
|
||||
const npzPath = npzInfo.npzPath;
|
||||
send({ type: 'log', text: `▶ NPZ: data/calibration/${npzInfo.session}/${camId}_calibration.npz` });
|
||||
|
||||
// Script 1 – ArUco-Erkennung
|
||||
send({ type: 'log', text: '\n▷ 1_detect_aruco_observations' });
|
||||
const exit1 = await runScript([
|
||||
SCRIPT_1,
|
||||
'-i', imgPath,
|
||||
'-npz', npzPath,
|
||||
'-robot', ROBOT_JSON,
|
||||
'-cameraId', camId,
|
||||
'-outDir', runDir,
|
||||
'--saveDebugImage',
|
||||
], send);
|
||||
if (exit1 !== 0) {
|
||||
send({ type: 'log', text: `❌ Script 1 Exit ${exit1}` });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Script 2 – Kamera-Pose schätzen
|
||||
const detJson = path.join(runDir, `${camId}_aruco_detection.json`);
|
||||
try { await fsPromises.access(detJson); }
|
||||
catch {
|
||||
send({ type: 'log', text: '⚠ Detection-JSON fehlt – Script 2 übersprungen' });
|
||||
continue;
|
||||
}
|
||||
|
||||
send({ type: 'log', text: '\n▷ 2_estimate_camera_from_observations' });
|
||||
const script2Args = [SCRIPT_2, '-i', detJson, '-robot', ROBOT_JSON, '-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}` });
|
||||
}
|
||||
|
||||
send({ type: 'log', text: '' });
|
||||
}
|
||||
|
||||
// ── Script 3b: Marker-Triangulierung (benötigt ≥2 Kamera-Posen) ──
|
||||
send({ type: 'log', text: '' });
|
||||
send({ type: 'log', text: '─── 3b: Marker-Triangulierung ────────────────────────────' });
|
||||
const runFiles3b = await fsPromises.readdir(runDir);
|
||||
const numPoses = runFiles3b.filter(f => f.endsWith('_camera_pose.json')).length;
|
||||
if (numPoses >= 2) {
|
||||
send({ type: 'log', text: `▷ 3b_corner_marker_poses (${numPoses} Kamera-Posen)` });
|
||||
const exit3b = await runScript([
|
||||
SCRIPT_3B,
|
||||
'--evalDir', runDir,
|
||||
'--robot', ROBOT_JSON,
|
||||
], send);
|
||||
if (exit3b !== 0) send({ type: 'log', text: `❌ Script 3b Exit ${exit3b}` });
|
||||
} else {
|
||||
send({ type: 'log', text: `⚠ Nur ${numPoses} Kamera-Pose(n) vorhanden – Script 3b braucht ≥2 Kameras für Triangulierung, wird übersprungen.` });
|
||||
}
|
||||
send({ type: 'log', text: '' });
|
||||
// 2–3b: Board-Pipeline (Foto + Scripts 1, 2, 3b)
|
||||
await runBoardPipeline(runDir, send, { refSet });
|
||||
|
||||
send({ type: 'log', text: `✅ Board-Run abgeschlossen: ${ts}` });
|
||||
send({ type: 'done', exitCode: 0, runDir: ts });
|
||||
@@ -714,6 +729,108 @@ app.get('/api/board/latest', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ── Homing ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* POST /api/homing/run
|
||||
* Vollständiger Homing-Ablauf: Board-Pipeline + 4b-Kette (SSE-Stream).
|
||||
*/
|
||||
app.post('/api/homing/run', async (req, res) => {
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
res.flushHeaders();
|
||||
|
||||
const send = (obj) => {
|
||||
if (!res.writableEnded) res.write(`data: ${JSON.stringify(obj)}\n\n`);
|
||||
};
|
||||
|
||||
try {
|
||||
await fsPromises.mkdir(homingDataDir, { recursive: true });
|
||||
await runHoming({
|
||||
robotJsonPath: ROBOT_JSON,
|
||||
homingDir: homingDataDir,
|
||||
send,
|
||||
runScript,
|
||||
runBoardPipeline,
|
||||
SCRIPT_4B,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('homing/run error:', err);
|
||||
try {
|
||||
send({ type: 'error', text: String(err) });
|
||||
send({ type: 'done', exitCode: -1 });
|
||||
} catch {}
|
||||
}
|
||||
if (!res.writableEnded) res.end();
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/homing/send-state
|
||||
* Sendet { state: { x, y, z, a, b, c, e } } an ROBOT_URL/api/state.
|
||||
*/
|
||||
app.post('/api/homing/send-state', async (req, res) => {
|
||||
try {
|
||||
const { state } = req.body ?? {};
|
||||
if (!state) return res.status(400).json({ error: '"state" fehlt' });
|
||||
if (!ROBOT_URL) return res.status(501).json({ error: 'ROBOT_URL ist nicht konfiguriert' });
|
||||
|
||||
const url = new URL('/api/state', ROBOT_URL).toString();
|
||||
const upstream = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(state),
|
||||
});
|
||||
if (!upstream.ok) {
|
||||
const text = await upstream.text();
|
||||
return res.status(upstream.status).json({ error: `Robot-Fehler: ${text}` });
|
||||
}
|
||||
const result = await upstream.json().catch(() => ({}));
|
||||
return res.json({ ok: true, result });
|
||||
} catch (err) {
|
||||
console.error('homing/send-state error:', err);
|
||||
return res.status(500).json({ error: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/homing/run-data?run=<timestamp>
|
||||
* Gibt Bilder (base64) und JSON-Dateien eines Homing-Runs zurück.
|
||||
*/
|
||||
app.get('/api/homing/run-data', async (req, res) => {
|
||||
try {
|
||||
const runName = req.query.run;
|
||||
if (!runName) return res.status(400).json({ error: '"run" parameter fehlt' });
|
||||
const runDir = path.join(homingDataDir, runName);
|
||||
let files = [];
|
||||
try { files = await fsPromises.readdir(runDir); } catch {}
|
||||
|
||||
const images = [];
|
||||
for (const f of files.sort()) {
|
||||
if (/\.(jpg|jpeg|png)$/i.test(f)) {
|
||||
try {
|
||||
const buf = await fsPromises.readFile(path.join(runDir, f));
|
||||
images.push({ filename: f, contentBase64: buf.toString('base64'), mimeType: 'image/jpeg' });
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
// Letzten accumulated_state zurückgeben
|
||||
let finalState = null;
|
||||
const stateFiles = files.filter(f => f.startsWith('state_') && f.endsWith('.json')).sort();
|
||||
if (stateFiles.length > 0) {
|
||||
try {
|
||||
const raw = await fsPromises.readFile(path.join(runDir, stateFiles[stateFiles.length - 1]), 'utf8');
|
||||
finalState = JSON.parse(raw).accumulated_state ?? null;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return res.json({ runDir: runName, images, finalState });
|
||||
} catch (err) {
|
||||
return res.status(500).json({ error: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
// ── Robot-JSON bearbeiten ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user