Claude: Umbau Robot

This commit is contained in:
chk
2026-06-09 15:51:30 +02:00
parent 2da589dfa3
commit 78185b8bdb
14 changed files with 1017 additions and 221 deletions

View File

@@ -0,0 +1,128 @@
const Robot = require('../robot/Robot');
const MotorPosition = require('../robot/RobotMotorPosition');
describe('Robot.calculateSpeeds (ToDo_6a)', () => {
beforeAll(() => {
jest.spyOn(console, 'log').mockImplementation(() => {});
});
afterAll(() => {
jest.restoreAllMocks();
});
function makeRobot() {
const robot = new Robot(250, 264, 100);
robot.useSpeedCalc = true;
robot.feedrate = 1000;
return robot;
}
test('erzeugt keine NaN-Werte (Property-Bug behoben) und setzt moveTime', () => {
const robot = makeRobot();
robot.xMotor = 10; robot.alpha = 1; robot.beta = 1;
robot.a = 0.5; robot.b = 0.5; robot.c = 0.5; robot.eMotor = 0.2;
const oldPos = new MotorPosition(0, 0, 0, 0, 0, 0, 0);
const newPos = new MotorPosition(10, 1, 1, 0.5, 0.5, 0.5, 0.2);
robot.calculateSpeeds(oldPos, newPos);
for (const k of ['x', 'y', 'z', 'a', 'b', 'c', 'e']) {
expect(Number.isFinite(robot.motorSpeeds[k])).toBe(true);
}
expect(robot.motorSpeeds.x).toBeGreaterThan(0);
expect(robot.lastMoveTime).toBeGreaterThan(0);
});
test('exakte Werte: motorSpeeds = Delta/Zeit, moveTime = Distanz/Feedrate', () => {
const robot = makeRobot();
// reine X-Motor-Bewegung um 30 mm
robot.xMotor = 30; robot.alpha = 0; robot.beta = 0;
const oldPos = new MotorPosition(0, 0, 0, 0, 0, 0, 0);
const newPos = new MotorPosition(30, 0, 0, 0, 0, 0, 0);
robot.calculateSpeeds(oldPos, newPos);
// Distanz = 30, feedrate = 1000 → moveTime = 0.03 min
expect(robot.lastMoveTime).toBeCloseTo(0.03, 6);
// motorSpeeds.x = 30 / 0.03 = 1000
expect(robot.motorSpeeds.x).toBeCloseTo(1000, 4);
// unbewegte Achsen → 0
expect(robot.motorSpeeds.y).toBeCloseTo(0, 6);
expect(robot.motorSpeeds.z).toBeCloseTo(0, 6);
});
test('Handgelenk-Zweig: xyz-Motoren unverändert, Handgelenk-Punkt bewegt sich', () => {
const robot = makeRobot();
robot.xMotor = 0; robot.alpha = 0; robot.beta = 0;
const oldPos = new MotorPosition(0, 0, 0, 0, 0, 0, 0);
oldPos.pX = 0; oldPos.pY = 0; oldPos.pZ = 0;
const newPos = new MotorPosition(0, 0, 0, 0, 0, 0, 0);
newPos.pX = 4; newPos.pY = 0; newPos.pZ = 3; // Handgelenk-Distanz = 5
robot.calculateSpeeds(oldPos, newPos);
// xyz-Distanz ist 0 → der Handgelenk-Zweig greift: moveTime = 5 / 1000
expect(robot.lastMoveTime).toBeCloseTo(0.005, 6);
});
test('Finger-Zweig: nur e bewegt sich', () => {
const robot = makeRobot();
robot.eMotor = 2;
const oldPos = new MotorPosition(0, 0, 0, 0, 0, 0, 0);
const newPos = new MotorPosition(0, 0, 0, 0, 0, 0, 2);
robot.calculateSpeeds(oldPos, newPos);
expect(Number.isFinite(robot.motorSpeeds.e)).toBe(true);
expect(robot.lastMoveTime).toBeCloseTo(0.002, 6); // 2 / 1000
});
test('Guard: useSpeedCalc deaktiviert → keine Änderung (Legacy)', () => {
const robot = makeRobot();
robot.useSpeedCalc = false;
robot.xMotor = 10;
const oldPos = new MotorPosition(0, 0, 0, 0, 0, 0, 0);
const newPos = new MotorPosition(10, 0, 0, 0, 0, 0, 0);
robot.calculateSpeeds(oldPos, newPos);
expect(robot.motorSpeeds.x).toBe(0);
expect(robot.lastMoveTime).toBe(0);
});
test('Guard: feedrate <= 0 → keine Berechnung', () => {
const robot = makeRobot();
robot.feedrate = 0;
robot.xMotor = 10;
const oldPos = new MotorPosition(0, 0, 0, 0, 0, 0, 0);
const newPos = new MotorPosition(10, 0, 0, 0, 0, 0, 0);
robot.calculateSpeeds(oldPos, newPos);
expect(robot.motorSpeeds.x).toBe(0);
expect(robot.lastMoveTime).toBe(0);
});
test('Guard: fehlende Positionen → kein Fehler, keine Änderung', () => {
const robot = makeRobot();
expect(() => robot.calculateSpeeds(null, null)).not.toThrow();
expect(robot.motorSpeeds.x).toBe(0);
});
test('keine Bewegung (alle Distanzen 0) → moveTime bleibt 0', () => {
const robot = makeRobot();
const pos = new MotorPosition(5, 0.1, 0.2, 0, 0, 0, 0);
robot.xMotor = 5; robot.alpha = 0.1; robot.beta = 0.2;
robot.calculateSpeeds(pos, pos);
expect(robot.lastMoveTime).toBe(0);
});
});

View File

@@ -0,0 +1,72 @@
const RobotController = require('../robot/RobotController');
const createDummyRobot = require('./helpers/createDummyRobot');
describe('RobotController (ToDo_6)', () => {
test('applyCommand: G90 setzt Absolutmodus, ohne Bewegung', () => {
const robot = createDummyRobot();
robot.moveRelative = true;
RobotController.applyCommand(robot, { command: 'G90', params: {} });
expect(robot.moveRelative).toBe(false);
expect(robot.calculateAngles3D).not.toHaveBeenCalled();
expect(robot.sendCommand).not.toHaveBeenCalled();
});
test('applyCommand: G1 absolut setzt Koordinaten und bewegt', () => {
const robot = createDummyRobot();
robot.moveRelative = false;
RobotController.applyCommand(robot, { command: 'G1', params: { X: 10, Y: 20, Z: 30 } });
expect(robot.x).toBe(10);
expect(robot.y).toBe(20);
expect(robot.z).toBe(30);
expect(robot.calculateAngles3D).toHaveBeenCalledTimes(1);
expect(robot.sendCommand).toHaveBeenCalledTimes(1);
});
test('applyCommand: G1 relativ addiert', () => {
const robot = createDummyRobot();
robot.moveRelative = true;
robot.x = 5;
RobotController.applyCommand(robot, { command: 'G1', params: { X: 2 } });
expect(robot.x).toBe(7);
});
test('applyCommand: M1 verändert Motorwerte und rechnet vorwärts', () => {
const robot = createDummyRobot();
robot.moveRelative = false;
RobotController.applyCommand(robot, { command: 'M1', params: { X: 1, Y: 2, Z: 3, A: 4, B: 5, C: 6 } });
expect(robot.xMotor).toBe(1);
expect(robot.alpha).toBe(2);
expect(robot.beta).toBe(3);
expect(robot.a).toBe(4);
expect(robot.calculatePositionFromMotorAngles).toHaveBeenCalled();
expect(robot.sendCommand).toHaveBeenCalled();
});
test('applyCommand: ungültiger Befehl wird ignoriert', () => {
const robot = createDummyRobot();
RobotController.applyCommand(robot, null);
RobotController.applyCommand(robot, { command: 'G99', params: {} });
expect(robot.sendCommand).not.toHaveBeenCalled();
});
test('receive: parst rohe Nachricht und wendet mehrere Befehle an', () => {
const robot = createDummyRobot();
robot.moveRelative = true;
RobotController.receive(robot, 'G90 G1 X12 Y34');
expect(robot.moveRelative).toBe(false);
expect(robot.x).toBe(12);
expect(robot.y).toBe(34);
expect(robot.sendCommand).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,175 @@
const TelnetSender = require('../robot/TelnetSenderGRBL.js');
describe('TelnetSenderGRBL — ROBOT_SPEED_MODE (ToDo_6a)', () => {
beforeAll(() => {
jest.spyOn(console, 'log').mockImplementation(() => {});
});
afterAll(() => {
jest.restoreAllMocks();
});
function makeSender() {
const sender = new TelnetSender('test.test', 2300, 'x', 'y', 'z');
sender.tSocket = { written: '', write(txt) { this.written = txt; } };
return sender;
}
// Gemeinsame Bewegungsdaten: x, alpha(=90°), beta bleibt
function makeMotion(extra) {
const mOld = { x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, e: 0 };
const mNew = {
x: 12.34, y: Math.PI / 2, z: 0, a: 0, b: 0, c: 0, e: 0,
xMotorChanged: true, yMotorChanged: true, zMotorChanged: true,
feedrate: 999,
...extra
};
return { mOld, mNew };
}
test('legacy (Default): kartesische Feedrate, exakt wie bisher', () => {
const sender = makeSender();
const { mOld, mNew } = makeMotion({ speedMode: 'legacy' });
sender.execCommand('G1', mOld, mNew);
// Achsenwerte wie gehabt, Feedrate = mNew.feedrate
expect(sender.tSocket.written).toBe('G90 G1 x12.34 y90.00 z-90.00 f999.00\r\n');
});
test('ohne speedMode-Feld → ebenfalls Legacy-Verhalten', () => {
const sender = makeSender();
const { mOld, mNew } = makeMotion(); // kein speedMode
sender.execCommand('G1', mOld, mNew);
expect(sender.tSocket.written).toContain('f999.00');
});
test('correct: koordinierte Feedrate ersetzt die kartesische', () => {
const sender = makeSender();
const { mOld, mNew } = makeMotion({ speedMode: 'correct', moveTime: 0.5 });
const expectedF = sender.computeCoordinatedFeedrate(mOld, mNew);
sender.execCommand('G1', mOld, mNew);
// Achsenwerte identisch zu Legacy — nur die Feedrate unterscheidet sich
expect(sender.tSocket.written).toContain('x12.34 y90.00 z-90.00');
expect(sender.tSocket.written).toContain('f' + expectedF.toFixed(2));
// und sie ist NICHT die kartesische 999
expect(sender.tSocket.written).not.toContain('f999.00');
});
test('correct ohne moveTime → Fallback auf Legacy-Feedrate', () => {
const sender = makeSender();
const { mOld, mNew } = makeMotion({ speedMode: 'correct' }); // kein moveTime
sender.execCommand('G1', mOld, mNew);
expect(sender.tSocket.written).toContain('f999.00');
});
test('correct ohne Bewegung (alt == neu) → null → Fallback', () => {
const sender = makeSender();
const { mNew } = makeMotion({ speedMode: 'correct', moveTime: 0.5 });
// gleiche Position als alt UND neu → Strecke 0
expect(sender.computeCoordinatedFeedrate(mNew, mNew)).toBeNull();
sender.execCommand('G1', mNew, mNew);
expect(sender.tSocket.written).toContain('f999.00');
});
test('computeCoordinatedFeedrate = Sender-Strecke / moveTime', () => {
const sender = makeSender();
const { mOld, mNew } = makeMotion({ speedMode: 'correct', moveTime: 0.5 });
// erwartete Strecke: x-Port=12.34, y-Port=90, z-Port=(0-90)=-90
const dist = Math.sqrt(12.34 ** 2 + 90 ** 2 + (-90) ** 2);
const expected = dist / 0.5;
expect(sender.computeCoordinatedFeedrate(mOld, mNew)).toBeCloseTo(expected, 4);
});
test('Hand-Sender (c,e,b) liefert im Korrekt-Modus koordinierte Feedrate', () => {
const hand = new TelnetSender('test.test', 5000, 'c', 'e', 'b');
hand.tSocket = { written: '', write(t) { this.written = t; } };
const mOld = { x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, e: 0 };
const mNew = {
x: 0, y: 0, z: 0, a: 0, b: 0.5, c: 0.8, e: 0.3,
bMotorChanged: true, cMotorChanged: true, eMotorChanged: true,
feedrate: 1234, speedMode: 'correct', moveTime: 0.2
};
const expectedF = hand.computeCoordinatedFeedrate(mOld, mNew);
expect(expectedF).toBeGreaterThan(0);
hand.execCommand('G1', mOld, mNew);
expect(hand.tSocket.written).toContain('f' + expectedF.toFixed(2));
expect(hand.tSocket.written).not.toContain('f1234.00');
});
});
/**
* Kreuzprobe: portValue() muss exakt das liefern, was der echte Sende-Pfad
* (execCommand, Legacy) an den jeweiligen GRBL-Port schreibt. Schützt die in
* portValue dupliziert gehaltenen Formeln gegen Drift.
*/
describe('TelnetSenderGRBL.portValue ↔ Sende-Pfad (Kreuzprobe)', () => {
beforeAll(() => {
jest.spyOn(console, 'log').mockImplementation(() => {});
});
afterAll(() => {
jest.restoreAllMocks();
});
const POS = { x: 1.1, y: 0.3, z: 0.7, a: 0.9, b: 1.3, c: 0.5, e: 0.4 };
function mNewAllChanged() {
return {
...POS,
xMotorChanged: true, yMotorChanged: true, zMotorChanged: true,
aMotorChanged: true, bMotorChanged: true, cMotorChanged: true, eMotorChanged: true,
feedrate: 100
};
}
function axisVal(line, axis) {
const m = line.match(new RegExp(axis + '(-?\\d+(?:\\.\\d+)?)'));
return m ? parseFloat(m[1]) : undefined;
}
// Baut einen Sender, der genau EINEN GRBL-Port auf eine Roboter-Achse abbildet,
// führt execCommand aus und vergleicht den gesendeten Wert mit portValue().
function crossCheck(grblPort, robotAxis) {
const ctorArgs = ['test.test', 100, null, null, null, null, null];
// Position des Ports in der Konstruktor-Signatur: x=2,y=3,z=4,a=5,b=6
const idx = { x: 2, y: 3, z: 4, a: 5, b: 6 }[grblPort];
ctorArgs[idx] = robotAxis;
const sender = new TelnetSender(...ctorArgs);
sender.tSocket = { written: '', write(t) { this.written = t; } };
const mNew = mNewAllChanged();
const mOld = { x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, e: 0 };
sender.execCommand('G1', mOld, mNew);
const emitted = axisVal(sender.tSocket.written, grblPort);
expect(emitted).toBeDefined();
expect(emitted).toBeCloseTo(sender.portValue(grblPort, robotAxis, mNew), 2);
}
// x-Port deckt alle sieben Roboter-Achsen ab (inkl. der kniffligen c/e-Formeln)
for (const r of ['x', 'y', 'z', 'a', 'b', 'c', 'e']) {
test(`x-Port ← robotAxis '${r}'`, () => crossCheck('x', r));
}
// Port-abhängige Formel-Varianten gezielt prüfen
test("y-Port ← 'c' (factorTurnLift-Variante)", () => crossCheck('y', 'c'));
test("z-Port ← 'c' (c+b+z-y-Variante)", () => crossCheck('z', 'c'));
test("z-Port ← 'b' (reines b, abweichend vom x-Port)", () => crossCheck('z', 'b'));
test("a-Port ← 'x' (Quirk: nutzt y)", () => crossCheck('a', 'x'));
});

View File

@@ -0,0 +1,102 @@
/**
* Integrationstest der Speed-Koordination (ToDo_6a).
*
* Kernaussage des Korrekt-Modus: Alle Controller müssen den Bewegungsschritt in
* DERSELBEN Zeit beenden. Dieser Test prüft genau das — komplett unabhängig von der
* internen Berechnung: er liest die tatsächlich gesendeten G-Code-Zeilen, rechnet aus
* den absoluten Achswerten die Strecke jedes Senders aus und teilt durch die gesendete
* Feedrate. Das Ergebnis (= Zeit) muss für jeden Sender gleich der robot-moveTime sein.
*
* Damit werden gleichzeitig abgesichert: moveTime-Propagation Robot→Sender, portValue
* gegen den echten Sende-Pfad, und die Feedrate-Auswahl im Korrekt-Modus.
*/
const Robot = require('../robot/Robot');
const GCode = require('../robot/GCode');
const TelnetSender = require('../robot/TelnetSenderGRBL');
function parseLine(line) {
const out = {};
const re = /([a-z])(-?\d+(?:\.\d+)?)/g;
let m;
while ((m = re.exec(line)) !== null) out[m[1]] = parseFloat(m[2]);
return out;
}
function makeSender(...args) {
const s = new TelnetSender(...args); // "test.test" → isTestMode
s.writes = [];
s.tSocket = { write(t) { s.writes.push(t); } };
return s;
}
// Produktiv-Verkabelung wie in startRobot.js
function buildRobot(speedMode) {
const robot = new Robot(250, 264, 100);
robot.speedMode = speedMode;
robot.useSpeedCalc = (speedMode === 'correct');
robot.moveRelative = false; // M1 absolut
const base = makeSender('test.test', 2300, 'x', 'y', 'z');
const elbow = makeSender('test.test', 5000, 'a', null, null);
const hand = makeSender('test.test', 5000, 'c', 'e', 'b');
robot.cmdReceivers.push(base, elbow, hand);
return { robot, base, elbow, hand };
}
// drei absolute Motor-Stellungen; Schritt 0 = Warmup (erster Aufruf → Fallback)
function runMoves(robot) {
GCode.receiveGCode(robot, 'M1 X5 Y0.10 Z0.20 A0.30 B0.40 C0.50'); // 0 warmup
GCode.receiveGCode(robot, 'M1 X10 Y0.20 Z0.30 A0.40 B0.50 C0.60'); // 1 baseline
GCode.receiveGCode(robot, 'M1 X22 Y0.45 Z0.50 A0.65 B0.85 C0.90'); // 2 gemessen
}
describe('Speed-Koordination (ToDo_6a)', () => {
beforeAll(() => { jest.spyOn(console, 'log').mockImplementation(() => {}); });
afterAll(() => { jest.restoreAllMocks(); });
test('correct: alle Sender beenden den Schritt in gleicher Zeit (= moveTime)', () => {
const { robot, base, elbow, hand } = buildRobot('correct');
runMoves(robot);
const moveTime = robot.motorPosition.moveTime;
expect(moveTime).toBeGreaterThan(0);
for (const s of [base, elbow, hand]) {
const v1 = parseLine(s.writes[1]); // baseline
const v2 = parseLine(s.writes[2]); // gemessen
const axes = Object.keys(v2).filter(k => k !== 'f');
let sumSq = 0;
for (const ax of axes) sumSq += (v2[ax] - v1[ax]) ** 2;
const dist = Math.sqrt(sumSq);
const time = dist / v2.f; // unabhängig aus Output berechnet
// jeder Controller braucht dieselbe Zeit (Rundung auf 2 Dezimalen → 2% Toleranz)
expect(Math.abs(time - moveTime) / moveTime).toBeLessThan(0.02);
}
});
test('correct: die Sender bekommen UNTERSCHIEDLICHE Feedrates', () => {
const { robot, base, elbow, hand } = buildRobot('correct');
runMoves(robot);
const fBase = parseLine(base.writes[2]).f;
const fElbow = parseLine(elbow.writes[2]).f;
const fHand = parseLine(hand.writes[2]).f;
// Genau das ist der Unterschied zu legacy: nicht alle gleich.
expect(fBase).not.toBeCloseTo(fElbow, 2);
expect(fBase).not.toBeCloseTo(fHand, 2);
});
test('legacy (Kontrast): alle Sender bekommen dieselbe kartesische Feedrate', () => {
const { robot, base, elbow, hand } = buildRobot('legacy');
runMoves(robot);
// robot.feedrate ist 1000 (kein F in M1) → alle Sender f1000.00
expect(parseLine(base.writes[2]).f).toBeCloseTo(1000, 2);
expect(parseLine(elbow.writes[2]).f).toBeCloseTo(1000, 2);
expect(parseLine(hand.writes[2]).f).toBeCloseTo(1000, 2);
});
});