Files
appRobotDriver/robot/kinematics/Arm3SegmentRotaryBase.js
2026-06-11 22:05:45 +02:00

188 lines
9.1 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.
const RobotBase = require('../RobotBase.js')
/**
* Arm3SegmentRotaryBase — Kinematik für den **Joy-IT „Grab-It" (Robot02)**.
*
* Strukturell wie {@link Arm3SegmentLinearX} (drei Arm-Segmente: Oberarm `l1`,
* Unterarm `l2`, Hand `l3`), aber statt der linearen X-Schiene sitzt der Arm auf
* einer **drehbaren Basis** (Yaw). Dazu kommt eine Handgelenk-Rotation.
*
* ───────────────────────────────────────────────────────────────────────────
* Physische Spezifikation (recherchiert aus dem Joy-IT-Datenblatt / Handbuch):
* - 6 PWM-Servos (20 kg, Metallgetriebe), 360° mech. / 180° Arbeitsbereich
* - 5 Bewegungsachsen + Greifer
* - Reichweite ca. 300 mm ab Mitte Drehteller, Bauhöhe bis 420 mm
*
* Gelenkkette (Basis → Greifer):
* q1 Basis-Drehung (Yaw um die vertikale Z-Achse) → this.xMotor
* q2 Schulter (Pitch) → this.alpha
* q3 Ellbogen (Pitch, relativ zum Oberarm) → this.beta
* q4 Handgelenk (Pitch, relativ zum Unterarm) → this.a
* q5 Handgelenk-Roll (Drehung um die Hand-/Anflugachse)→ this.b
* (ungenutzt) → this.c (= 0)
* q6 Greifer (Öffnungsweite) → this.eMotor
*
* Damit reichen die 7 Motor-Slots des Frameworks aus; ein Slot (`c`) bleibt frei.
*
* ───────────────────────────────────────────────────────────────────────────
* 5-DOF-Einschränkung (wichtig!):
* Ein 5-Achs-Arm kann **nicht** jede beliebige 6-DOF-Pose (x,y,z,phi,theta,psi)
* erreichen. Da die Basis-Drehung den Azimut bestimmt, liegt der gesamte Arm in
* **einer** vertikalen Ebene durch die Basisachse. Folge:
* - `phi` (Hand-Azimut) ist **nicht frei** — er ist an die Position gekoppelt
* und wird in der Inversen aus `atan2(y, x)` abgeleitet. Ein als Eingabe
* gesetztes `this.phi` wird in {@link calculateAngles3D} **ignoriert** und in
* {@link calculatePositionFromMotorAngles} als Ergebnis überschrieben.
* - Frei wählbar sind: Position `x,y,z`, Hand-Neigung `theta` (Polarwinkel von
* +Z, gleiche Konvention wie Arm3SegmentLinearX) und Hand-Rotation `psi`.
*
* ───────────────────────────────────────────────────────────────────────────
* Geometrie-Modell / Annahmen:
* - Schultergelenk sitzt auf der Basis-Drehachse, Höhe `baseHeight` über der
* Grundplatte (radialer Versatz = 0 — vereinfachende Annahme).
* - Winkel `alpha`, `beta`, `a` werden intern als Elevation gegen die
* Horizontale geführt; `alpha` ist absolut, `beta`/`a` relativ zum
* vorhergehenden Segment.
* - Es wird genau **ein** Ellbogen-Zweig gewählt (`beta` ∈ [0, π]).
* - Gelenk-Endanschläge werden **nicht** geprüft (wie bei Arm3SegmentLinearX).
*
* ⚠️ Die Default-Längen sind **Schätzwerte**, abgeleitet aus den
* veröffentlichten Maßen (Σ Segmente ≈ Reichweite 300 mm, `baseHeight` ≈
* Höhe 420 mm Reichweite). Für den Echtbetrieb am physischen Arm sollten sie
* **kalibriert** und per `ROBOT_KINEMATICS_PARAMS` gesetzt werden.
*/
class Arm3SegmentRotaryBase extends RobotBase {
/**
* @param {number} [l1=105] Länge des Oberarms (Schulter→Ellbogen) in mm
* @param {number} [l2=98] Länge des Unterarms (Ellbogen→Handgelenk) in mm
* @param {number} [l3=100] Länge der Hand (Handgelenk→Greiferspitze) in mm
* @param {Object} [params] zusätzliche Parameter
* @param {number} [params.baseHeight=110] Höhe der Schulterachse über der Basis in mm
*/
constructor(l1 = 105, l2 = 98, l3 = 100, params = {}) {
super(params);
/** @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;
/** @type {number} Höhe der Schulter-Drehachse über der Grundplatte in mm */
this.baseHeight = (params && params.baseHeight != null) ? params.baseHeight : 110;
// Neutrale Default-Pose: Hand horizontal nach vorn (theta = π/2 = Polarwinkel).
this.theta = Math.PI / 2;
}
/**
* Inverse Kinematik: Workspace (x, y, z, theta, psi, e) → Gelenkwinkel.
* `phi` wird ignoriert und aus der Position abgeleitet (5-DOF, siehe Klassendoc).
*/
calculateAngles3D(verbose) {
// Winkel normalisieren (Konsistenz mit Arm3SegmentLinearX)
while (this.theta > Math.PI) { this.theta -= 2 * Math.PI }
while (this.theta < -Math.PI) { this.theta += 2 * Math.PI }
// 1) Basis-Drehung aus dem Azimut der Zielposition (phi ist gekoppelt)
const q1 = Math.atan2(this.y, this.x);
this.xMotor = q1;
this.phi = q1;
// 2) Hand-Vektor abziehen → Handgelenk-Punkt
// theta = Polarwinkel von +Z. Hand-Radial = l3·sin(theta), Hand-Z = l3·cos(theta).
const st = Math.sqrt(this.x * this.x + this.y * this.y); // radialer Abstand der Spitze
const zt = this.z;
const sw = st - this.l3 * Math.sin(this.theta); // radial: Handgelenk
const zw = zt - this.l3 * Math.cos(this.theta); // Höhe: Handgelenk
// Handgelenk-Punkt in 3D (für Speed-Berechnung)
this.pX = sw * Math.cos(q1);
this.pY = sw * Math.sin(q1);
this.pZ = zw;
// 3) 2-Gelenk-IK in der Armebene: von Schulter (0, baseHeight) zum Handgelenk (sw, zw)
const dx = sw;
const dz = zw - this.baseHeight;
const R2 = dx * dx + dz * dz;
const R = Math.sqrt(R2);
if (R > (this.l1 + this.l2)) { return; } // unerreichbar — Slots unverändert lassen
if (R === 0) { return; }
// Ellbogen-Relativwinkel (ein Zweig: qe ∈ [0, π])
let cosE = (R2 - this.l1 * this.l1 - this.l2 * this.l2) / (2 * this.l1 * this.l2);
cosE = Math.max(-1, Math.min(1, cosE));
const qe = Math.atan2(Math.sqrt(1 - cosE * cosE), cosE);
// Absoluter Oberarmwinkel (Elevation gegen Horizontale)
const A2 = Math.atan2(dz, dx) - Math.atan2(this.l2 * Math.sin(qe), this.l1 + this.l2 * Math.cos(qe));
const A3 = A2 + qe; // absoluter Unterarmwinkel
const A4 = Math.PI / 2 - this.theta; // absolute Hand-Elevation aus Polarwinkel
// 4) Motor-Slots schreiben
this.alpha = A2; // Schulter (absolut)
this.beta = qe; // Ellbogen (relativ) = A3 A2
this.a = A4 - A3; // Handgelenk-Pitch (relativ)
this.b = this.psi; // Handgelenk-Roll
this.c = 0; // ungenutzt
this.eMotor = this.e; // Greifer
// a auf [-π, π] normalisieren
while (this.a > Math.PI) { this.a -= 2 * Math.PI }
while (this.a < -Math.PI) { this.a += 2 * Math.PI }
if (verbose) {
console.log("IK GrabIt: q1=", q1.toFixed(3), "alpha=", this.alpha.toFixed(3),
"beta=", this.beta.toFixed(3), "a=", this.a.toFixed(3), "b=", this.b.toFixed(3));
}
}
/**
* Vorwärts-Kinematik: Gelenkwinkel → Workspace (x, y, z, phi, theta, psi, e).
* Exakte Umkehrung von {@link calculateAngles3D}.
*/
calculatePositionFromMotorAngles(verbose = false) {
const q1 = this.xMotor; // Basis-Yaw
const A2 = this.alpha; // Oberarm absolut
const A3 = this.alpha + this.beta; // Unterarm absolut
const A4 = this.alpha + this.beta + this.a; // Hand absolut (Elevation)
// Handgelenk-Punkt in der Armebene
const sw = this.l1 * Math.cos(A2) + this.l2 * Math.cos(A3);
const zw = this.baseHeight + this.l1 * Math.sin(A2) + this.l2 * Math.sin(A3);
// Greiferspitze in der Armebene (Hand-Elevation A4)
const st = sw + this.l3 * Math.cos(A4);
const zt = zw + this.l3 * Math.sin(A4);
// In den 3D-Raum drehen
this.pX = sw * Math.cos(q1);
this.pY = sw * Math.sin(q1);
this.pZ = zw;
this.x = st * Math.cos(q1);
this.y = st * Math.sin(q1);
this.z = zt;
this.phi = q1; // Azimut = Basis-Drehung
this.theta = Math.PI / 2 - A4; // Polarwinkel von +Z
this.psi = this.b; // Hand-Roll
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 }
if (verbose) {
console.log("FK GrabIt: x=", this.x.toFixed(2), "y=", this.y.toFixed(2),
"z=", this.z.toFixed(2), "phi=", this.phi.toFixed(3), "theta=", this.theta.toFixed(3));
}
}
}
module.exports = Arm3SegmentRotaryBase