Files
appRobotDriver/robot/kinematics/Arm3SegmentLinearX.js
2026-06-26 15:53:50 +02:00

177 lines
7.5 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.
const RobotBase = require('../RobotBase.js')
/**
* Arm3SegmentLinearX — 6-DOF-Arm mit linearer X-Schiene und drei Segmenten
* (Oberarm `l1`, Unterarm `l2`, Hand/Endeffector `l3`) plus Greifer.
*
* Erste konkrete Kinematik-Implementierung des Frameworks. Implementiert den
* Interface-Vertrag von {@link RobotBase} — die gesamte generische Infrastruktur
* (Zustand, sendCommand, Speed-Berechnung, ...) wird geerbt.
*
* Physikalische Struktur:
* - X-Achse: lineare Schiene (Schulterposition `xMotor`)
* - Schulter/Ellbogen: 2D-Arm in der Y-Z-Ebene (`alpha`, `beta`)
* - Handgelenk: drei Winkel `a` (Ellbogen-Dreher), `b` (Knicker), `c` (Hand-Dreher)
* - Greifer: `e`
*/
class Arm3SegmentLinearX extends RobotBase {
/**
* @param {number} l1 Länge des Oberarms in mm
* @param {number} l2 Länge des Unterarms in mm
* @param {number} l3 Länge der Hand (Endeffector) in mm
* @param {Object} [config] Bewegungs-Config (defaultFeedrate, speedMode, …)
*/
constructor(l1, l2, l3, config = {}) {
super(config);
/** @type {number} Länge des Oberarms in mm */
this.l1 = l1;
/** @type {number} Länge des Unterarms in mm */
this.l2 = l2;
/** @type {number} Länge der Hand (Endeffector) in mm */
this.l3 = l3;
}
// ─────────────────────────────────────────────────────────────────────
// Y-Konvention: Der reale Roboter steht/arbeitet in -y (robot.json:
// Arm1 -> [0,-250,0], coordinateSystem.y = "backward"). Die interne
// Kinematik (_ikPlusY/_fkPlusY) rechnet historisch in +y. Beide
// öffentlichen Methoden spiegeln daher die Workspace-Pose an der x-z-Ebene
// (y, pY, phi, psi; theta bleibt), sodass alpha=0 nach -y zeigt.
// Siehe doc/Info_Koordinaten.md (Weg 2, Phase 1).
// ─────────────────────────────────────────────────────────────────────
/** Reflexion der Workspace-Pose an der x-z-Ebene (Involution: zweimal = Identität). */
_mirrorWorkspaceY() {
this.y = -this.y;
this.pY = -this.pY;
this.phi = -this.phi;
this.psi = -this.psi;
}
calculateAngles3D(verbose) {
// -y-Eingabe in den internen +y-Frame spiegeln, rechnen, dann die
// Workspace-Felder zurückspiegeln (Motorwerte bleiben unberührt).
this._mirrorWorkspaceY();
this._ikPlusY(verbose);
this._mirrorWorkspaceY();
}
calculatePositionFromMotorAngles(verbose = false) {
this._fkPlusY(verbose);
this._mirrorWorkspaceY(); // +y-Ergebnis -> -y Workspace
}
// Berechnet aus XYZ die Motor-Winkel für den GCode (interne +y-Mathematik)
_ikPlusY(verbose){
while(this.phi > Math.PI){this.phi -= 2*Math.PI}
while(this.phi < -Math.PI){this.phi += 2*Math.PI}
while(this.theta > Math.PI){this.theta -= 2*Math.PI}
while(this.theta < -Math.PI){this.theta += 2*Math.PI}
// Handgelenk-Punkt ausrechnen:
this.pX = this.x + this.l3*Math.sin(this.theta)*Math.cos(this.phi);
this.pY = this.y + this.l3*Math.sin(this.theta)*Math.sin(this.phi);
this.pZ = this.z + this.l3*Math.cos(this.theta);
var pX = this.pX;
var pY = this.pY;
var pZ = this.pZ;
this.xMotor = pX;
// Ziel-Punkt ausrechnen ==> 2D Rechnung Arm
var r = Math.sqrt(pY * pY + pZ * pZ);
if (r > (this.l1 + this.l2)) { return; }
if (r == 0) { return; }
var gamma = Math.atan2(pZ, pY);
var delta = Math.acos((this.l1 * this.l1 + this.l2 * this.l2 - r * r) / (2 * this.l1 * this.l2));
this.alpha = Math.acos((this.l1 * this.l1 + r * r - this.l2 * this.l2) / (2 * r * this.l1)) + gamma;
this.beta = -Math.PI + (this.alpha + delta);
// Ende <== 2D Rechnung Arm
// Richtung der Hand ausgerechnet
// Arm = (0, cos(beta), sin(beta)) Punkt = (sin(theta)cos(phi), sin(theta)sin(phi), cos(theta))
//
// Unterarm muss gedreht werden. Aus der Y-Z-Ebene raus. Hin in die Ebene n x r
// wobei n = Unterarm x (P-O) ist
var nX = Math.cos(this.beta)*Math.cos(this.theta) - Math.sin(this.theta)*Math.sin(this.phi)*Math.sin(this.beta);
var nY = Math.sin(this.beta)*Math.sin(this.theta)*Math.cos(this.phi);
var nZ = -1.0*Math.sin(this.theta)*Math.cos(this.phi)*Math.cos(this.beta);
var nBetrag = Math.sqrt(nX*nX + nY*nY + nZ*nZ);
if(verbose) console.log("Richtung: > ", nX/nBetrag, nY/nBetrag, nZ/nBetrag);
var cosA = (nX)/nBetrag;
this.a = Math.acos(cosA)
if(Math.cos(this.phi) > 0){this.a = -this.a}
if(Math.sin(this.theta) < 0) {this.a = -this.a}
// Handgelenk-Knick-Winkel: b=0 = gerade (Phase 2). cosB ist cos des internen
// Winkels zwischen Unterarm und Hand; acos liefert [0,π], also b = πacos(cosB) ∈ [0,π].
var cosB = Math.cos(this.beta)*Math.sin(this.theta)*Math.sin(this.phi) + Math.sin(this.beta)*Math.cos(this.theta);
this.b = Math.PI - Math.acos(cosB);
// Winkel zwischen n und z muss rumgedreht werden.
var cosC = - nZ / nBetrag;
this.c = Math.acos(cosC);
this.c += this.psi;
// a um 180° drehen
this.a += Math.PI;
while(this.a > Math.PI){this.a -= 2*Math.PI}
while(this.a < -Math.PI){this.a += 2*Math.PI}
this.eMotor = this.gripperMotorFromOpening(this.e);
}
/**
* Greifer-Kopplung dieses Arms: die Finger-Sehne läuft durchs Handgelenk, daher
* ziehen Knick (`b`) und Dreh (`c`) am Greifer mit. `eMotor` kompensiert das, damit
* die Finger-Öffnung `e` (mm, ab Null-Position eines Fingers) unabhängig von der
* Handstellung bleibt. Einzige Quelle für diese Kopplung (auch via G92/M92 genutzt).
*/
gripperMotorFromOpening(e) {
return e - this.b - this.c;
}
_fkPlusY(verbose = false) {
const vecBizeps = {x: this.xMotor, y: this.l1 * Math.cos(this.alpha), z: this.l1 * Math.sin(this.alpha)}
const vecUnterarm = {x: 0, y: Math.cos(this.beta), z: Math.sin(this.beta)}
// der Handgelenk-Punkt
this.pX = vecBizeps.x + this.l2 * vecUnterarm.x;
this.pY = vecBizeps.y + this.l2 * vecUnterarm.y;
this.pZ = vecBizeps.z + this.l2 * vecUnterarm.z;
// n: Die Handgelenk-Unterarm-Knick-Achse. X-Achse wird um den Unterarm gedreht.
const n = { x: -Math.cos(this.a), y: vecUnterarm.z * Math.sin(this.a), z: -vecUnterarm.y * Math.sin(this.a) };
if(verbose) console.log("n inverse:", n.x, n.y, n.z);
// b=0 = gerade (Phase 2): interne Rotation um πb (bei b=0 → π → Hand gestreckt).
const vHand = this.rotateAroundAxis(vecUnterarm, n, Math.PI - this.b);
this.x = this.pX - this.l3 * vHand.x;
this.y = this.pY - this.l3 * vHand.y;
this.z = this.pZ - this.l3 * vHand.z;
this.theta = Math.atan2(Math.sqrt(vHand.x*vHand.x + vHand.y*vHand.y),vHand.z);
this.phi = Math.atan2(vHand.y, vHand.x);
this.psi = this.c - Math.acos(-n.z);
while(this.phi > Math.PI){this.phi -= 2*Math.PI}
while(this.phi < -Math.PI){this.phi += 2*Math.PI}
while(this.theta > Math.PI){this.theta -= 2*Math.PI}
while(this.theta < -Math.PI){this.theta += 2*Math.PI}
}
}
module.exports = Arm3SegmentLinearX