/** * Verifikation der Port→Motor-Rückrechnung für ToDo_9 / Paket 4 (Sync-Command). * * Herleitung: doc/ToDo_9a_PortRueckrechnung.md * * Kernaussage: Für die PRODUKTIV-Verkabelung (startRobot.js) ist die Abbildung * der sieben Motorwerte auf die sieben gesendeten GRBL-Achswerte LINEAR und * EINDEUTIG umkehrbar. `motorStateFromPorts()` ist diese Umkehrung; die Tests * beweisen sie gegen den echten Sende-Pfad (`execCommand`) und gegen `portValue`. * * Damit ist klar: die B3-Mehrdeutigkeit (Ellbogen oben/unten) betrifft nur die * kartesische Inverskinematik (`calculateAngles3D`), die der Sync gar nicht nutzt. */ const TelnetSender = require('../robot/TelnetSenderGRBL'); const MotorPosition = require('../robot/RobotMotorPosition'); const Robot = require('../robot/kinematics/Arm3SegmentLinearX'); // Produktiv-Funktion unter Test (vorher inline in dieser Datei, jetzt extrahiert). const { motorStateFromPorts } = require('../robot/portInverse'); const D = 180 / Math.PI; /* ---------- Produktiv-Verkabelung (1:1 wie startRobot.js) ---------- */ function buildSenders() { const mk = (...args) => new TelnetSender('test.test', ...args); // test.test → Fake-tSocket return { base: mk(2300, 'x', 'y', 'z'), // GRBL x←xMotor, y←alpha, z←(beta-alpha) elbow: mk(5000, 'a', null, null), // GRBL x←a hand: mk(5000, 'c', 'e', 'b'), // GRBL x←(c-b), y←eMotor, z←b }; } /* ---------- Die Umkehrung kommt jetzt aus robot/portInverse.js ---------- * (vorher hier inline; extrahiert für ToDo_9 Paket 4, damit Produktivcode und * dieser Verifikations-Test dieselbe Funktion nutzen.) */ /* ---------- Hilfen ---------- */ // {x,y,z,a,b,c,e}-pos aus Motorwerten (Feld-Mapping der RobotMotorPosition) function posFromMotors(m) { return { x: m.xMotor, y: m.alpha, z: m.beta, a: m.a, b: m.b, c: m.c, e: m.eMotor }; } // Port-Readings über die reine Funktion portValue (volle Präzision) function portsViaPortValue(S, pos) { return { base: { x: S.base.portValue('x', 'x', pos), y: S.base.portValue('y', 'y', pos), z: S.base.portValue('z', 'z', pos) }, elbow: { x: S.elbow.portValue('x', 'a', pos) }, hand: { x: S.hand.portValue('x', 'c', pos), y: S.hand.portValue('y', 'e', pos), z: S.hand.portValue('z', 'b', pos) }, }; } // Achswerte aus einer real gesendeten G-Code-Zeile (z. B. "G1 x12.34 y90.00 z-90.00") function parseAxes(line) { const out = {}; const re = /([xyz])(-?\d+(?:\.\d+)?)/g; let m; while ((m = re.exec(line)) !== null) out[m[1]] = parseFloat(m[2]); return out; } // Port-Readings über den ECHTEN Sende-Pfad execCommand (inkl. 2-Dezimal-Rundung) function portsViaExecCommand(S, m) { const mNew = new MotorPosition(m.xMotor, m.alpha, m.beta, m.a, m.b, m.c, m.eMotor); for (const f of ['xMotorChanged','yMotorChanged','zMotorChanged','aMotorChanged','bMotorChanged','cMotorChanged','eMotorChanged']) mNew[f] = true; const mOld = new MotorPosition(0, 0, 0, 0, 0, 0, 0); S.base.execCommand('G1', mOld, mNew); S.elbow.execCommand('G1', mOld, mNew); S.hand.execCommand('G1', mOld, mNew); return { base: parseAxes(S.base.tSocket.written), elbow: parseAxes(S.elbow.tSocket.written), hand: parseAxes(S.hand.tSocket.written), }; } /* ---------- Repräsentative Motorzustände (plausible Bereiche) ---------- */ const CASES = [ { name: 'Nullstellung', xMotor: 0, alpha: 0, beta: 0, a: 0, b: 0, c: 0, eMotor: 0 }, { name: 'gemischt klein', xMotor: 100, alpha: 0.30, beta: 0.50, a: 0.20, b: 0.40, c: 0.90, eMotor: 1.0 }, { name: 'negative Winkel', xMotor: -55, alpha:-0.70, beta:-0.25, a:-1.10, b:-0.60, c:-0.30, eMotor:-0.5 }, { name: 'große Winkel', xMotor: 250, alpha: 1.20, beta: 2.00, a: 2.90, b: 1.50, c: 3.00, eMotor: 2.5 }, { name: 'gekoppelt c≈b', xMotor: 42, alpha: 0.10, beta: 0.10, a: 0.05, b: 1.20, c: 1.25, eMotor: 0.0 }, ]; beforeAll(() => { jest.spyOn(console, 'log').mockImplementation(() => {}); }); afterAll(() => { jest.restoreAllMocks(); }); describe('Port→Motor-Rückrechnung (ToDo_9, Baustein für Paket 4)', () => { describe('A) exakte Umkehrung gegen portValue (volle Präzision)', () => { for (const c of CASES) { test(c.name, () => { const S = buildSenders(); const ports = portsViaPortValue(S, posFromMotors(c)); const rec = motorStateFromPorts(ports); for (const k of ['xMotor','alpha','beta','a','b','c','eMotor']) { expect(rec[k]).toBeCloseTo(c[k], 9); } }); } }); describe('B) Umkehrung gegen den echten Sende-Pfad execCommand (mit Rundung)', () => { for (const c of CASES) { test(c.name, () => { const S = buildSenders(); const ports = portsViaExecCommand(S, c); const rec = motorStateFromPorts(ports); // xMotor in mm (Rundung 0.01) → 0.02 Toleranz; Winkel in rad (Rundung 0.01° je Port) → 1e-3 expect(rec.xMotor).toBeCloseTo(c.xMotor, 2); for (const k of ['alpha','beta','a','b','c','eMotor']) { expect(Math.abs(rec[k] - c[k])).toBeLessThan(1e-3); } }); } }); describe('C) Voll-Kette Sync: Ports → Motoren → Vorwärtskinematik → Pose', () => { // Beweist, dass die rekonstruierten Motorwerte über die Vorwärtskinematik // exakt dieselbe Pose liefern wie die Originalwerte (= was Sync dem Client meldet). for (const c of CASES) { test(c.name, () => { const S = buildSenders(); // Referenz-Pose direkt aus den Original-Motorwerten const ref = new Robot(250, 264, 100); Object.assign(ref, { xMotor: c.xMotor, alpha: c.alpha, beta: c.beta, a: c.a, b: c.b, c: c.c, eMotor: c.eMotor }); ref.calculatePositionFromMotorAngles(); // Sync-Pfad: Ports → Inverse → Motoren setzen → Vorwärtskinematik const rec = motorStateFromPorts(portsViaPortValue(S, posFromMotors(c))); const sync = new Robot(250, 264, 100); Object.assign(sync, { xMotor: rec.xMotor, alpha: rec.alpha, beta: rec.beta, a: rec.a, b: rec.b, c: rec.c, eMotor: rec.eMotor }); sync.calculatePositionFromMotorAngles(); for (const k of ['x','y','z','phi','theta','psi']) { expect(sync[k]).toBeCloseTo(ref[k], 6); } }); } }); });