Files
appRobotHoming/server/editRobot.js
2026-06-10 17:57:05 +02:00

392 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* editRobot.js
* Hilfsfunktionen zum Bearbeiten von robot_xxx.json.
* Alle Schreibvorgänge machen ein Backup-Kommentar in der Datei (nein, aber
* 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';
// ── I/O ───────────────────────────────────────────────────────────────────────
async function readRobot(robotPath) {
return JSON.parse(await fsPromises.readFile(robotPath, 'utf8'));
}
async function writeRobot(robotPath, data) {
await fsPromises.writeFile(robotPath, JSON.stringify(data, null, 2), 'utf8');
}
// ── Aktion 1: Marker nach Z-Bereich zuordnen ─────────────────────────────────
/**
* Weist allen Markern, deren z-Position (mm) zwischen zMin und zMax liegt,
* das angegebene Set und/oder den angegebenen Link zu.
*
* - set (optional): Setzt den set-Wert des Markers.
* - link (optional): Verschiebt den Marker in diesen Link (wird ggf. angelegt).
* - extraMarkers (optional): Triangulierte Marker aus aruco_marker_poses.json
* werden als neue Einträge in robot.json hinzugefügt,
* wenn sie noch nicht vorhanden sind.
*
* Gibt { numChanged, changes[] } zurück.
* changes[]: { markerId, action, oldLink, newLink, oldSet, newSet }
* action: 'updated' | 'added'
*/
export async function assignByZRange(robotPath, { zMin, zMax, set, link, extraMarkers = [] }) {
const robot = await readRobot(robotPath);
const links = robot.links ?? {};
const changes = [];
const zLo = Number(zMin);
const zHi = Number(zMax);
// ── Teil 1: Bestehende robot.json-Marker aktualisieren / verschieben ──────────
const snapshot = [];
for (const [linkName, linkData] of Object.entries(links)) {
for (const marker of (linkData.markers ?? [])) {
if (Array.isArray(marker.position)) snapshot.push({ id: marker.id, currentLink: linkName });
}
}
for (const { id, currentLink } of snapshot) {
const srcLinkData = links[currentLink];
const idx = (srcLinkData?.markers ?? []).findIndex(m => Number(m.id) === id);
if (idx === -1) continue;
const marker = srcLinkData.markers[idx];
const z = Number(marker.position[2]);
if (z < zLo || z > zHi) continue;
const change = {
action: 'updated',
markerId: marker.id,
oldLink: currentLink,
oldSet: marker.set ?? '',
newLink: currentLink,
newSet: marker.set ?? '',
};
if (set !== undefined && set !== '') { marker.set = set; change.newSet = set; }
if (link && link !== currentLink) {
srcLinkData.markers.splice(idx, 1);
if (!links[link]) links[link] = { markers: [] };
if (!links[link].markers) links[link].markers = [];
links[link].markers.push(marker);
change.newLink = link;
}
changes.push(change);
}
// ── Teil 2: Neue Marker aus 3b-Triangulation einfügen (noch nicht in robot.json) ──
// Diese haben keine Position in robot.json wir übernehmen die gemessene Position.
if (link && extraMarkers.length > 0) {
const knownIds = new Set();
for (const ld of Object.values(links)) {
for (const m of (ld.markers ?? [])) knownIds.add(Number(m.id));
}
for (const em of extraMarkers) {
const emId = Number(em.marker_id);
const emPos = em.position_mm; // [x_mm, y_mm, z_mm] robot-Koordinaten
if (knownIds.has(emId)) continue; // bereits in robot.json (evtl. gerade hinzugefügt)
if (!Array.isArray(emPos) || emPos.length < 3) continue;
const z = Number(emPos[2]);
if (z < zLo || z > zHi) continue;
const newMarker = {
id: emId,
position: emPos.map(v => Math.round(Number(v) * 100) / 100), // 2 Dezimalstellen
};
if (set) newMarker.set = set;
if (!links[link]) links[link] = { markers: [] };
if (!links[link].markers) links[link].markers = [];
links[link].markers.push(newMarker);
knownIds.add(emId);
changes.push({
action: 'added',
markerId: emId,
oldLink: null,
oldSet: '',
newLink: link,
newSet: set ?? '',
});
}
}
if (changes.length > 0) {
robot.links = links;
await writeRobot(robotPath, robot);
}
return { numChanged: changes.length, changes };
}
// ── Aktion 2: Set oder Link-Zuordnung entfernen ───────────────────────────────
/**
* Entfernt die Set- oder Link-Zuordnung eines Markers.
*
* removeFrom: 'set' → löscht nur den set-Wert (Marker bleibt im Link)
* removeFrom: 'link' → entfernt Marker komplett aus dem Link
* (nur wenn set bereits leer/nicht gesetzt)
*
* Gibt { changed, markerId, action, link, [oldSet], [error] } zurück.
*/
export async function removeMarkerAssignment(robotPath, { markerId, removeFrom }) {
const id = Number(markerId);
const robot = await readRobot(robotPath);
const links = robot.links ?? {};
for (const [linkName, linkData] of Object.entries(links)) {
const markers = linkData.markers ?? [];
const idx = markers.findIndex(m => Number(m.id) === id);
if (idx === -1) continue;
const marker = markers[idx];
if (removeFrom === 'set') {
const oldSet = marker.set ?? '';
delete marker.set;
await writeRobot(robotPath, robot);
return { changed: true, markerId: id, action: 'set-removed', link: linkName, oldSet };
}
if (removeFrom === 'link') {
if (marker.set) {
return {
changed: false,
markerId: id,
error: `Marker ${id} hat noch Set "${marker.set}". Bitte zuerst Set entfernen.`,
};
}
markers.splice(idx, 1);
await writeRobot(robotPath, robot);
return { changed: true, markerId: id, action: 'link-removed', link: linkName };
}
return { changed: false, error: `Unbekannte Aktion: "${removeFrom}"` };
}
return { changed: false, error: `Marker-ID ${id} nicht gefunden.` };
}
// ── Aktion 3: Sets untereinander justieren (2D+Z Kabsch) ─────────────────────
/**
* Optimale 2D-Rotation (um Z-Achse) + 3D-Translation, die model→measured minimiert.
* Gibt { tx, ty, tz, theta, thetaDeg } zurück.
* modelPts / measuredPts: Arrays von [x, y, z] in mm.
*/
function computeRigid2DZ(modelPts, measuredPts) {
const n = modelPts.length;
if (n === 0) return { tx: 0, ty: 0, tz: 0, theta: 0, thetaDeg: 0 };
// Schwerpunkte
let mCx = 0, mCy = 0, mCz = 0, pCx = 0, pCy = 0, pCz = 0;
for (let i = 0; i < n; i++) {
mCx += modelPts[i][0]; mCy += modelPts[i][1]; mCz += modelPts[i][2];
pCx += measuredPts[i][0]; pCy += measuredPts[i][1]; pCz += measuredPts[i][2];
}
mCx /= n; mCy /= n; mCz /= n;
pCx /= n; pCy /= n; pCz /= n;
// 2D-Kreuzkovarianz für Kabsch-Rotation in XY-Ebene
let H00 = 0, H01 = 0, H10 = 0, H11 = 0;
for (let i = 0; i < n; i++) {
const ax = modelPts[i][0] - mCx, ay = modelPts[i][1] - mCy;
const bx = measuredPts[i][0] - pCx, by = measuredPts[i][1] - pCy;
H00 += ax * bx; H01 += ax * by;
H10 += ay * bx; H11 += ay * by;
}
// Optimaler Drehwinkel (2D-Sonderfall von Kabsch / SVD → atan2)
const theta = Math.atan2(H10 - H01, H00 + H11);
const cos = Math.cos(theta);
const sin = Math.sin(theta);
// Translation: gemessener Schwerpunkt R × Modell-Schwerpunkt
const tx = pCx - (cos * mCx - sin * mCy);
const ty = pCy - (sin * mCx + cos * mCy);
const tz = pCz - mCz;
return { tx, ty, tz, theta, thetaDeg: theta * 180 / Math.PI };
}
function applyRigid2DZ([x, y, z], { tx, ty, tz, theta }) {
const cos = Math.cos(theta);
const sin = Math.sin(theta);
return [
Math.round((cos * x - sin * y + tx) * 100) / 100,
Math.round((sin * x + cos * y + ty) * 100) / 100,
Math.round((z + tz) * 100) / 100,
];
}
/**
* Richtet alle Marker des Sets `setToMove` rigid an ihren triangulieren 3b-Positionen aus.
* Das andere Set bleibt unberührt die Transformation wird aus den Paaren
* (Modellposition, Messposition) der matching Marker berechnet.
*
* setToMove: Name des zu verschiebenden Sets (z.B. "rail")
* extraMarkers: Marker aus aruco_marker_poses.json (mit marker_id, position_mm)
*
* Gibt { numChanged, numMatchingPts, transform: {tx, ty, tz, thetaDeg} } zurück,
* oder { error } bei Fehler.
*/
export async function alignSetToMeasured(robotPath, { setToMove, extraMarkers = [] }) {
const robot = await readRobot(robotPath);
const links = robot.links ?? {};
// Alle Marker des zu verschiebenden Sets sammeln
const setMarkers = [];
for (const [linkName, linkData] of Object.entries(links)) {
for (const marker of (linkData.markers ?? [])) {
if (marker.set === setToMove) {
setMarkers.push({ marker, linkName });
}
}
}
if (setMarkers.length === 0) {
return { error: `Set "${setToMove}" nicht gefunden oder leer.` };
}
// Gemessene Positionen aus 3b indizieren
const measuredMap = new Map();
for (const em of extraMarkers) {
if (Array.isArray(em.position_mm) && em.position_mm.length >= 3) {
measuredMap.set(Number(em.marker_id), em.position_mm.map(Number));
}
}
// Matching-Paare für Kabsch (nur Marker, die sowohl Modellposition als auch Messung haben)
const modelPts = [], measuredPts = [];
for (const { marker } of setMarkers) {
if (!Array.isArray(marker.position) || marker.position.length < 3) continue;
const mpos = measuredMap.get(Number(marker.id));
if (!mpos) continue;
modelPts.push(marker.position.map(Number));
measuredPts.push(mpos);
}
if (modelPts.length < 2) {
return {
error: `Zu wenig Messpunkte für Set "${setToMove}" (${modelPts.length} von ${setMarkers.length} Markern gemessen). ` +
`Bitte Board-Run mit ≥2 Kameras durchführen.`,
};
}
const transform = computeRigid2DZ(modelPts, measuredPts);
// Transformation auf ALLE Marker des Sets anwenden (auch nicht gemessene)
let numChanged = 0;
for (const { marker } of setMarkers) {
if (!Array.isArray(marker.position) || marker.position.length < 3) continue;
marker.position = applyRigid2DZ(marker.position.map(Number), transform);
numChanged++;
}
robot.links = links;
await writeRobot(robotPath, robot);
return {
numChanged,
numMatchingPts: modelPts.length,
transform: {
tx: Math.round(transform.tx * 100) / 100,
ty: Math.round(transform.ty * 100) / 100,
tz: Math.round(transform.tz * 100) / 100,
thetaDeg: Math.round(transform.thetaDeg * 100) / 100,
},
};
}
// ── Aktion 4: Einzelnen Marker per ID hinzufügen / aktualisieren ──────────────
/**
* Fügt einen Marker per ID zu Set und Link hinzu oder aktualisiert einen bestehenden.
*
* - Existiert der Marker bereits in robot.json: set und/oder link werden aktualisiert.
* - Existiert er noch nicht: Position wird aus extraMarkers (3b-Output) geholt und
* der Marker wird dem angegebenen Link hinzugefügt.
*
* Gibt { changed, change } oder { changed: false, error } zurück.
*/
export async function assignMarkerId(robotPath, { markerId, set, link, extraMarkers = [] }) {
const id = Number(markerId);
if (!Number.isFinite(id) || id < 0) return { changed: false, error: 'Ungültige Marker-ID.' };
const robot = await readRobot(robotPath);
const links = robot.links ?? {};
// Marker in robot.json suchen
let foundMarker = null, foundLink = null;
outer: for (const [linkName, linkData] of Object.entries(links)) {
for (const m of (linkData.markers ?? [])) {
if (Number(m.id) === id) { foundMarker = m; foundLink = linkName; break outer; }
}
}
if (foundMarker) {
// Bestehenden Marker: Set und ggf. Link aktualisieren
const change = {
action: 'updated',
markerId: id,
oldLink: foundLink,
oldSet: foundMarker.set ?? '',
newLink: foundLink,
newSet: foundMarker.set ?? '',
};
if (set !== undefined && set !== '') { foundMarker.set = set; change.newSet = set; }
if (link && link !== foundLink) {
// In neuen Link verschieben
links[foundLink].markers = links[foundLink].markers.filter(m => Number(m.id) !== id);
if (!links[link]) links[link] = { markers: [] };
if (!links[link].markers) links[link].markers = [];
links[link].markers.push(foundMarker);
change.newLink = link;
}
robot.links = links;
await writeRobot(robotPath, robot);
return { changed: true, change };
}
// Neuer Marker: Position aus 3b-Output holen
const em = extraMarkers.find(m => Number(m.marker_id) === id);
if (!em) {
return {
changed: false,
error: `Marker ${id} ist nicht in robot.json und nicht im letzten 3b-Run vorhanden.`,
};
}
if (!link) {
return { changed: false, error: 'Link muss angegeben werden, um einen neuen Marker hinzuzufügen.' };
}
const newMarker = {
id,
position: em.position_mm.map(v => Math.round(Number(v) * 100) / 100),
};
if (set) newMarker.set = set;
if (!links[link]) links[link] = { markers: [] };
if (!links[link].markers) links[link].markers = [];
links[link].markers.push(newMarker);
robot.links = links;
await writeRobot(robotPath, robot);
return {
changed: true,
change: { action: 'added', markerId: id, oldLink: null, oldSet: '', newLink: link, newSet: set ?? '' },
};
}