Files
appRobotHoming/test/homingXEstimate.test.js
2026-06-14 22:35:44 +02:00

137 lines
5.8 KiB
JavaScript
Raw Permalink 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.
/**
* homingXEstimate.test.js
* =======================
* Unit-Test für server/homingXEstimate.cjs (reine X-Schätzungs-Geometrie).
*
* Sichert insbesondere den Bug vom 2026-06-14 ab: Vorher setzte die Schätzung
* x_slider = Mittelwert der beobachteten world-x und vergaß den kinematischen
* Gelenk-Offset (Arm1.origin.x = 110). Dadurch lagen die Modell-Marker ~110 mm
* zu weit in +x. Korrekt: slider_i = beobachtetes_x Modell_x(slider=0).
*/
const fs = require('fs');
const path = require('path');
const { modelWorldXAtSliderZero, estimateXFromParsed } =
require('../server/homingXEstimate.cjs');
// ── Synthetische Kinematik (Struktur wie robot.json, minimal) ──────────────────
function makeLinks() {
return {
Board: {}, // Root, kein jointToParent
Base: { parent: 'Board', jointToParent: { type: 'linear', axis: [1, 0, 0], origin: [0, 0, 16] }, markers: [] },
Arm1: {
parent: 'Base',
jointToParent: { type: 'revolute', axis: [-1, 0, 0], origin: [110, 108, 45] },
markers: [
{ id: 198, position: [0, -160, 35] },
{ id: 229, position: [0, -250, 35] },
{ id: 197, position: [-35, -250, 0] },
],
},
Ellbow: {
parent: 'Arm1',
jointToParent: { type: 'revolute', axis: [-1, 0, 0], origin: [0, -250, 0] },
markers: [ { id: 244, position: [125, 0, 0] } ],
},
Arm2: {
parent: 'Ellbow',
jointToParent: { type: 'revolute', axis: [0, -1, 0], origin: [90, 0, 0] }, // dreht um y → NICHT x-sicher
markers: [ { id: 300, position: [0, -100, 0] } ],
},
};
}
describe('modelWorldXAtSliderZero', () => {
const links = makeLinks();
test('summiert origin.x entlang x-Rotations-Kette (Arm1)', () => {
// Arm1.origin.x (110) + local x (0)
expect(modelWorldXAtSliderZero(links, 'Arm1', [0, -160, 35]))
.toEqual({ worldX: 110, xSafe: true });
// local x (-35) wird mitgenommen
expect(modelWorldXAtSliderZero(links, 'Arm1', [-35, -250, 0]))
.toEqual({ worldX: 75, xSafe: true });
});
test('Ellbow bleibt x-sicher (gesamte Kette dreht um x)', () => {
// Ellbow.origin.x (0) + Arm1.origin.x (110) + local x (125)
expect(modelWorldXAtSliderZero(links, 'Ellbow', [125, 0, 0]))
.toEqual({ worldX: 235, xSafe: true });
});
test('Arm2 ist NICHT x-sicher (dreht um y)', () => {
const r = modelWorldXAtSliderZero(links, 'Arm2', [0, -100, 0]);
expect(r.xSafe).toBe(false);
});
test('unbekannter Link → worldX = local x, xSafe bleibt true', () => {
expect(modelWorldXAtSliderZero(links, 'DoesNotExist', [7, 0, 0]))
.toEqual({ worldX: 7, xSafe: true });
});
});
describe('estimateXFromParsed', () => {
const links = makeLinks();
test('rechnet den Gelenk-Offset heraus (Arm1 + Ellbow), nicht roher Mittelwert', () => {
// Alle Marker sind so gewählt, dass der implizierte Slider exakt 40 ist.
const aruco = { markers: [
{ marker_id: 198, link: 'Arm1', position_mm: [150, 0, 0] }, // 150 - 110 = 40
{ marker_id: 229, link: 'Arm1', position_mm: [150, 0, 0] }, // 150 - 110 = 40
{ marker_id: 197, link: 'Arm1', position_mm: [115, 0, 0] }, // 115 - 75 = 40
{ marker_id: 244, link: 'Ellbow', position_mm: [275, 0, 0] }, // 275 - 235 = 40
]};
expect(estimateXFromParsed(aruco, links)).toBeCloseTo(40, 6);
// Roher Mittelwert (alter Bug) wäre (150+150+115+275)/4 = 172.5 → NICHT das.
expect(estimateXFromParsed(aruco, links)).not.toBeCloseTo(172.5, 1);
});
test('überspringt Board, unbekannte IDs, x-unsichere Ketten und fehlende Positionen', () => {
const aruco = { markers: [
{ marker_id: 198, link: 'Arm1', position_mm: [150, 0, 0] }, // zählt → 40
{ marker_id: 229, link: 'Arm1', position_mm: [150, 0, 0] }, // zählt → 40
{ marker_id: 5, link: 'Board', position_mm: [999, 0, 0] }, // Board → skip
{ marker_id: 999, link: 'Arm1', position_mm: [500, 0, 0] }, // unbekannte ID → skip
{ marker_id: 300, link: 'Arm2', position_mm: [500, 0, 0] }, // x-unsicher → skip
{ marker_id: 197, link: 'Arm1' }, // kein position_mm → skip
]};
expect(estimateXFromParsed(aruco, links)).toBeCloseTo(40, 6);
});
test('Fallback ohne robot.json: roher Mittelwert der Nicht-Board-Marker', () => {
const aruco = { markers: [
{ marker_id: 198, link: 'Arm1', position_mm: [150, 0, 0] },
{ marker_id: 229, link: 'Arm1', position_mm: [150, 0, 0] },
{ marker_id: 197, link: 'Arm1', position_mm: [115, 0, 0] },
{ marker_id: 5, link: 'Board', position_mm: [999, 0, 0] }, // Board zählt nicht
]};
// (150 + 150 + 115) / 3 = 138.333…
expect(estimateXFromParsed(aruco, {})).toBeCloseTo(138.3333, 3);
});
test('keine Marker → 0.0', () => {
expect(estimateXFromParsed({ markers: [] }, links)).toBe(0.0);
expect(estimateXFromParsed({}, links)).toBe(0.0);
});
});
describe('Regression gegen echte robot.json', () => {
test('Arm1-Marker bei beobachtetem x=150/150/115 → Slider ≈ 40 (nicht ~138)', () => {
const robotPath = path.resolve(__dirname, '../scripts/robot_1781069752019.json');
const links = JSON.parse(fs.readFileSync(robotPath, 'utf8')).links;
// Marker-IDs/locals stammen aus robot.json (Arm1: 198 [0,..], 229 [0,..], 197 [-35,..]);
// Arm1.origin.x = 110, Base.origin.x = 0.
const aruco = { markers: [
{ marker_id: 198, link: 'Arm1', position_mm: [150, 12, 60] },
{ marker_id: 229, link: 'Arm1', position_mm: [150, -8, 60] },
{ marker_id: 197, link: 'Arm1', position_mm: [115, 3, 25] },
]};
const x = estimateXFromParsed(aruco, links);
expect(x).toBeCloseTo(40, 6); // korrekt: Offset herausgerechnet
expect(x).not.toBeCloseTo(138.3, 1); // alter Bug (roher Mittelwert) ausgeschlossen
});
});