234 lines
11 KiB
JavaScript
234 lines
11 KiB
JavaScript
/***
|
|
* RobotController — Steuerlogik (ToDo_6).
|
|
*
|
|
* Verarbeitet bereits geparste G-Code-/Befehlsobjekte (aus GCodeParser) und wendet
|
|
* sie auf das Robotermodell an: Bewegungsmodus, Kinematik, sendCommand().
|
|
*
|
|
* Bewusst getrennt vom Parsing (GCodeParser) und vom Datei-Handling (GCode):
|
|
* der Controller kennt nur strukturierte Befehle, keine rohen Textstrings.
|
|
*/
|
|
const GCodeParser = require('./GCodeParser');
|
|
const { motorStateFromPorts, D } = require('./portInverse');
|
|
|
|
class RobotController {
|
|
|
|
/**
|
|
* Parst eine rohe Nachricht und wendet alle enthaltenen Befehle der Reihe nach an.
|
|
*
|
|
* Rückgabe: `undefined` (synchron, wie bisher) für alle gewöhnlichen Befehle.
|
|
* Nur wenn ein asynchroner Befehl enthalten war (Hardware-Sync, ToDo_9 Paket 4)
|
|
* wird ein Promise zurückgegeben, das auf dessen Abschluss wartet.
|
|
* @param {object} robot Robotermodell
|
|
* @param {string|Buffer} message rohe G-Code-Nachricht
|
|
*/
|
|
static receive(robot, message) {
|
|
const commands = GCodeParser.parse(message);
|
|
if (!commands.length) return;
|
|
const results = commands.map(parsed => this.applyCommand(robot, parsed));
|
|
const pending = results.filter(r => r && typeof r.then === 'function');
|
|
if (pending.length === 0) return; // synchroner Pfad unverändert
|
|
return Promise.all(pending).then(arr => arr[arr.length - 1]);
|
|
}
|
|
|
|
/**
|
|
* Wendet einen einzelnen, bereits geparsten Befehl auf das Robotermodell an.
|
|
* @param {object} robot
|
|
* @param {{command: string, params: object}} parsed
|
|
*/
|
|
static applyCommand(robot, parsed) {
|
|
if (!parsed || !parsed.command) return;
|
|
|
|
const cmd = parsed.command;
|
|
const params = parsed.params || {};
|
|
|
|
if (cmd === 'G90') {
|
|
robot.moveRelative = false;
|
|
return;
|
|
}
|
|
|
|
if (cmd === 'G91') {
|
|
robot.moveRelative = true;
|
|
return;
|
|
}
|
|
|
|
if (cmd === 'G28') {
|
|
// Home = Grundstellung: Arm voll ausgestreckt entlang -y (siehe
|
|
// doc/Info_Koordinaten.md). y und phi in der -y-Konvention.
|
|
robot.x = 0;
|
|
robot.y = -(robot.l1 + robot.l2 + robot.l3);
|
|
robot.z = 0;
|
|
robot.phi = Math.PI / 2;
|
|
robot.theta = Math.PI / 2;
|
|
robot.psi = 0;
|
|
robot.e = 0;
|
|
robot.calculateAngles3D();
|
|
robot.sendCommand();
|
|
return;
|
|
}
|
|
|
|
if (cmd === 'G1') {
|
|
if (robot.moveRelative) {
|
|
if (Number.isFinite(params.X)) { robot.x += params.X; robot.xMotorChanged = true; }
|
|
if (Number.isFinite(params.Y)) { robot.y += params.Y; robot.yMotorChanged = true; }
|
|
if (Number.isFinite(params.Z)) { robot.z += params.Z; robot.zMotorChanged = true; }
|
|
if (Number.isFinite(params.A)) { robot.phi += params.A; robot.aMotorChanged = true; }
|
|
if (Number.isFinite(params.B)) { robot.theta += params.B; robot.bMotorChanged = true; }
|
|
if (Number.isFinite(params.C)) { robot.psi += params.C; robot.cMotorChanged = true; }
|
|
if (Number.isFinite(params.E)) { robot.e += params.E; robot.eMotorChanged = true; }
|
|
if (Number.isFinite(params.F)) { robot.feedrate = params.F; }
|
|
} else {
|
|
if (Number.isFinite(params.X)) { robot.x = params.X; robot.xMotorChanged = true; }
|
|
if (Number.isFinite(params.Y)) { robot.y = params.Y; robot.yMotorChanged = true; }
|
|
if (Number.isFinite(params.Z)) { robot.z = params.Z; robot.zMotorChanged = true; }
|
|
if (Number.isFinite(params.A)) { robot.phi = params.A; robot.aMotorChanged = true; }
|
|
if (Number.isFinite(params.B)) { robot.theta = params.B; robot.bMotorChanged = true; }
|
|
if (Number.isFinite(params.C)) { robot.psi = params.C; robot.cMotorChanged = true; }
|
|
if (Number.isFinite(params.E)) { robot.e = params.E; robot.eMotorChanged = true; }
|
|
if (Number.isFinite(params.F)) { robot.feedrate = params.F; }
|
|
}
|
|
|
|
robot.calculateAngles3D();
|
|
robot.sendCommand();
|
|
return;
|
|
}
|
|
|
|
if (cmd === 'M1') {
|
|
if (robot.moveRelative) {
|
|
if (Number.isFinite(params.X)) { robot.xMotor += params.X; robot.xMotorChanged = true; }
|
|
if (Number.isFinite(params.Y)) { robot.alpha += params.Y; robot.yMotorChanged = true; }
|
|
if (Number.isFinite(params.Z)) { robot.beta += params.Z; robot.zMotorChanged = true; }
|
|
if (Number.isFinite(params.A)) { robot.a += params.A; robot.aMotorChanged = true; }
|
|
if (Number.isFinite(params.B)) { robot.b += params.B; robot.bMotorChanged = true; }
|
|
if (Number.isFinite(params.C)) { robot.c += params.C; robot.cMotorChanged = true; }
|
|
if (Number.isFinite(params.E)) { robot.e += params.E; robot.eMotorChanged = true; }
|
|
} else {
|
|
if (Number.isFinite(params.X)) { robot.xMotor = params.X; robot.xMotorChanged = true; }
|
|
if (Number.isFinite(params.Y)) { robot.alpha = params.Y; robot.yMotorChanged = true; }
|
|
if (Number.isFinite(params.Z)) { robot.beta = params.Z; robot.zMotorChanged = true; }
|
|
if (Number.isFinite(params.A)) { robot.a = params.A; robot.aMotorChanged = true; }
|
|
if (Number.isFinite(params.B)) { robot.b = params.B; robot.bMotorChanged = true; }
|
|
if (Number.isFinite(params.C)) { robot.c = params.C; robot.cMotorChanged = true; }
|
|
if (Number.isFinite(params.E)) { robot.e = params.E; robot.eMotorChanged = true; }
|
|
}
|
|
|
|
robot.calculatePositionFromMotorAngles();
|
|
robot.sendCommand();
|
|
return;
|
|
}
|
|
|
|
if (cmd === 'M114' && params.R === true) {
|
|
// Hardware-Sync (ToDo_9 Paket 4): liest die echten MPos aller Controller
|
|
// und übernimmt sie als Soll-Zustand. Asynchron → gibt ein Promise zurück.
|
|
return this.syncFromHardware(robot);
|
|
}
|
|
|
|
if (cmd === 'M92' || cmd === 'G92') {
|
|
// Beide setzen die Motorposition ohne Bewegung, unterscheiden sich aber in den
|
|
// Winkel-EINHEITEN:
|
|
// G92 → GRAD (G-Code-Konvention für Rotationsachsen, wie FluidNC und die
|
|
// "Position Motoren"-Anzeige in public/app.js). Intern sind die
|
|
// Winkel-Slots in Radiant → Grad/D umrechnen (D = 180/π).
|
|
// M92 → RADIANT, roh in die internen Slots (interne/Test-Variante).
|
|
// X ist die lineare mm-Schiene, E die Greifer-Öffnung in mm (ab Null-Position
|
|
// eines Fingers) — beide ohne Winkel-Umrechnung.
|
|
const angScale = (cmd === 'G92') ? 1 / D : 1;
|
|
robot.createMotorPosition();
|
|
if (Number.isFinite(params.X)) { robot.xMotor = params.X; robot.xMotorChanged = true; }
|
|
if (Number.isFinite(params.Y)) { robot.alpha = params.Y * angScale; robot.yMotorChanged = true; }
|
|
if (Number.isFinite(params.Z)) { robot.beta = params.Z * angScale; robot.zMotorChanged = true; }
|
|
if (Number.isFinite(params.A)) { robot.a = params.A * angScale; robot.aMotorChanged = true; }
|
|
if (Number.isFinite(params.B)) { robot.b = params.B * angScale; robot.bMotorChanged = true; }
|
|
if (Number.isFinite(params.C)) { robot.c = params.C * angScale; robot.cMotorChanged = true; }
|
|
// E nach B/C setzen: der Greifer-Motorwert hängt über die Kinematik-Kopplung
|
|
// von b und c ab. robot.e = Finger-Öffnung (mm), eMotor = abgeleiteter Motorwert.
|
|
// Ohne diese eMotor-Ableitung bliebe der Greiferwert stale (alte E-Inkonsistenz):
|
|
// sendCommand() verschickt eMotor, nicht e.
|
|
if (Number.isFinite(params.E)) {
|
|
robot.e = params.E;
|
|
robot.eMotor = robot.gripperMotorFromOpening(robot.e);
|
|
robot.eMotorChanged = true;
|
|
}
|
|
|
|
robot.calculatePositionFromMotorAngles();
|
|
robot.sendCommand('G92');
|
|
return;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hardware-Sync (ToDo_9 Paket 4): liest die echten Achs-Positionen (`MPos`) der
|
|
* drei Controller, rekonstruiert daraus die sieben Motorwerte und übernimmt sie
|
|
* als neuen Soll-Zustand. Danach Vorwärtskinematik → Pose.
|
|
*
|
|
* Bewegt den Roboter NICHT — es wird kein `sendCommand()`/Move ausgelöst, nur der
|
|
* interne Zustand an die Realität angeglichen (nach Homing/Jog/Stall/Reconnect).
|
|
*
|
|
* @param {object} robot
|
|
* @param {{timeoutMs?: number}} [options]
|
|
* @returns {Promise<{x,y,z,phi,theta,psi}>} die übernommene Pose
|
|
*/
|
|
static async syncFromHardware(robot, options = {}) {
|
|
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 1000;
|
|
const receivers = (robot && robot.cmdReceivers) || [];
|
|
|
|
// Sender nach Rolle zuordnen (controllerRole wird in startRobot.js gesetzt).
|
|
const byRole = {};
|
|
for (const s of receivers) {
|
|
if (s && s.controllerRole) byRole[s.controllerRole] = s;
|
|
}
|
|
for (const role of ['base', 'elbow', 'hand']) {
|
|
if (!byRole[role]) {
|
|
throw new Error(`Sync: Controller '${role}' fehlt (kein Sender mit controllerRole='${role}')`);
|
|
}
|
|
}
|
|
|
|
// Frische Reports von allen drei Controllern anfordern (aktiv '?', mit await).
|
|
const [baseSnap, elbowSnap, handSnap] = await Promise.all([
|
|
byRole.base.requestStatusReport(timeoutMs),
|
|
byRole.elbow.requestStatusReport(timeoutMs),
|
|
byRole.hand.requestStatusReport(timeoutMs),
|
|
]);
|
|
|
|
// MPos-Arrays validieren (FluidNC meldet ggf. mehr Achsen; nur die nötigen lesen).
|
|
const need = (snap, role, n) => {
|
|
const mp = snap && snap.machinePosition;
|
|
if (!Array.isArray(mp) || mp.length < n || !mp.slice(0, n).every(Number.isFinite)) {
|
|
throw new Error(`Sync: ${role} lieferte keine gültige MPos (${JSON.stringify(mp)})`);
|
|
}
|
|
return mp;
|
|
};
|
|
const b = need(baseSnap, 'base', 3);
|
|
const e = need(elbowSnap, 'elbow', 1);
|
|
const h = need(handSnap, 'hand', 3);
|
|
|
|
// Port → Motorwerte (linear/eindeutig, ToDo_9a) → auf den Roboter schreiben.
|
|
const m = motorStateFromPorts({
|
|
base: { x: b[0], y: b[1], z: b[2] },
|
|
elbow: { x: e[0] },
|
|
hand: { x: h[0], y: h[1], z: h[2] },
|
|
});
|
|
robot.xMotor = m.xMotor;
|
|
robot.alpha = m.alpha;
|
|
robot.beta = m.beta;
|
|
robot.a = m.a;
|
|
robot.b = m.b;
|
|
robot.c = m.c;
|
|
robot.eMotor = m.eMotor;
|
|
|
|
// Vorwärtskinematik: füllt x/y/z + phi/theta/psi aus den Hardwarewerten.
|
|
robot.calculatePositionFromMotorAngles();
|
|
|
|
// motorPosition zurücksetzen, damit der nächste Move den Speed-Delta von der
|
|
// echten Position aus rechnet (sonst falscher Feedrate im Korrekt-Modus).
|
|
robot.motorPosition = null;
|
|
robot.motorPositionOld = null;
|
|
|
|
return {
|
|
x: robot.x, y: robot.y, z: robot.z,
|
|
phi: robot.phi, theta: robot.theta, psi: robot.psi,
|
|
};
|
|
}
|
|
}
|
|
|
|
module.exports = RobotController;
|