diff --git a/src/emc/kinematics/maxkins.c b/src/emc/kinematics/maxkins.c index 773525b3566..d2623e0ad62 100644 --- a/src/emc/kinematics/maxkins.c +++ b/src/emc/kinematics/maxkins.c @@ -50,10 +50,6 @@ int kinematicsForward(const double *joints, // B correction const double zb = (pivot_length + joints[8] + tool_length) * cos(d2r(joints[4])); const double xb = (pivot_length + joints[8] + tool_length) * sin(d2r(joints[4])); - - // C correction - const double xyr = hypot(joints[0], joints[1]); - const double xytheta = atan2(joints[1], joints[0]) + d2r(joints[5]); // U correction const double zv = joints[6] * sin(d2r(joints[4])); @@ -61,8 +57,18 @@ int kinematicsForward(const double *joints, // V correction is always in joint 1 only - pos->tran.x = xyr * cos(xytheta) - (con * xb) - xv; - pos->tran.y = xyr * sin(xytheta) - joints[7]; + // B, U and V are all machine frame: the head hangs off the Z slide and + // does not turn with the C table, so they apply before the rotation into + // the workpiece frame rather than after it. + const double mx = joints[0] - (con * xb) - xv; + const double my = joints[1] - joints[7]; + + // C correction + const double xyr = hypot(mx, my); + const double xytheta = atan2(my, mx) + d2r(joints[5]); + + pos->tran.x = xyr * cos(xytheta); + pos->tran.y = xyr * sin(xytheta); pos->tran.z = joints[2] - zb - (con * zv) + pivot_length + tool_length; pos->a = joints[3]; @@ -103,7 +109,7 @@ int kinematicsInverse(const EmcPose * pos, joints[0] = xyr * cos(xytheta) + (con * xb) + xv; joints[1] = xyr * sin(xytheta) + pos->v; - joints[2] = pos->tran.z + zb - (con * zv) - pivot_length - tool_length; + joints[2] = pos->tran.z + zb + (con * zv) - pivot_length - tool_length; joints[3] = pos->a; joints[4] = pos->b; diff --git a/src/emc/kinematics/pumakins.c b/src/emc/kinematics/pumakins.c index 98dc83aa4c5..049d384b424 100644 --- a/src/emc/kinematics/pumakins.c +++ b/src/emc/kinematics/pumakins.c @@ -29,6 +29,16 @@ struct haldata { hal_real_t a2, a3, d3, d4, d6; } *haldata = NULL; +/* the difference of two angles, brought into (-pi, pi] so that a joint a + whole turn from the formula still matches it */ +static double angleDiff(double a, double b) +{ + double d = a - b; + while (d > PM_PI) { d -= 2*PM_PI; } + while (d <= -PM_PI) { d += 2*PM_PI; } + return d; +} + /* The flange orientation for a joint set: the ISO 9787 mechanical interface frame, whose z points out of the interface towards the work. Shared by the forward kinematics and the tool frame so the two cannot drift apart. */ @@ -152,16 +162,16 @@ static int pumaKinematicsForward(const double * joint, *iflags = 0; /* Set shoulder-up flag if necessary */ - if (fabs(joint[0]*PM_PI/180 - atan2(hom.tran.y, hom.tran.x) + - atan2(PUMA_D3, -sqrt(sumSq))) < FLAG_FUZZ) + if (fabs(angleDiff(joint[0]*PM_PI/180, atan2(hom.tran.y, hom.tran.x) - + atan2(PUMA_D3, -sqrt(sumSq)))) < FLAG_FUZZ) { *iflags |= PUMA_SHOULDER_RIGHT; } /* Set elbow down flag if necessary */ - if (fabs(joint[2]*PM_PI/180 - atan2(PUMA_A3, PUMA_D4) + + if (fabs(angleDiff(joint[2]*PM_PI/180, atan2(PUMA_A3, PUMA_D4) - atan2(k, -sqrt(PUMA_A3 * PUMA_A3 + - PUMA_D4 * PUMA_D4 - k * k))) < FLAG_FUZZ) + PUMA_D4 * PUMA_D4 - k * k)))) < FLAG_FUZZ) { *iflags |= PUMA_ELBOW_DOWN; } @@ -177,7 +187,7 @@ static int pumaKinematicsForward(const double * joint, /* if not singular set wrist flip flag if necessary */ else{ - if (! (fabs(joint[3]*PM_PI/180 - atan2(t1, t2)) < FLAG_FUZZ)) + if (! (fabs(angleDiff(joint[3]*PM_PI/180, atan2(t1, t2))) < FLAG_FUZZ)) { *iflags |= PUMA_WRIST_FLIP; } diff --git a/src/emc/kinematics/scarakins.c b/src/emc/kinematics/scarakins.c index 8db57eb7b0c..3b0e69ee7e4 100644 --- a/src/emc/kinematics/scarakins.c +++ b/src/emc/kinematics/scarakins.c @@ -96,8 +96,9 @@ int scaraKinematicsForward(const double * joint, z = D1 + D3 - joint[2] - D5; c = a3; + // the elbow flag: which sign the inverse gives the acos of joint 1 *iflags = 0; - if (joint[1] < 90) + if (joint[1] < 0) *iflags = 1; world->tran.x = x; diff --git a/src/emc/kinematics/three21kins.c b/src/emc/kinematics/three21kins.c index a5a7b5dfa8f..2a3dfe08ee5 100644 --- a/src/emc/kinematics/three21kins.c +++ b/src/emc/kinematics/three21kins.c @@ -32,6 +32,16 @@ struct haldata { hal_real_t a1, a2, a3, d1, d2, d3, d4, d6; } *haldata = NULL; +/* the difference of two angles, brought into (-pi, pi] so that a joint a + whole turn from the formula still matches it */ +static double angleDiff(double a, double b) +{ + double d = a - b; + while (d > PM_PI) { d -= 2*PM_PI; } + while (d <= -PM_PI) { d += 2*PM_PI; } + return d; +} + static int three21KinematicsForward(const double * joint, EmcPose * world, const KINEMATICS_FORWARD_FLAGS * fflags, @@ -132,8 +142,8 @@ static int three21KinematicsForward(const double * joint, *iflags = 0; /* set shoulder flag */ - if (fabs(joint[0]*PM_PI/180 - atan2(hom.tran.y, hom.tran.x) + - atan2(d23, -sqrt(sumSq))) < FLAG_FUZZ) + if (fabs(angleDiff(joint[0]*PM_PI/180, atan2(hom.tran.y, hom.tran.x) - + atan2(d23, -sqrt(sumSq)))) < FLAG_FUZZ) { *iflags |= THREE21_SHOULDER_RIGHT; } @@ -143,8 +153,8 @@ static int three21KinematicsForward(const double * joint, if (discr < 0.0) { discr = 0.0; } - if (fabs(joint[2]*PM_PI/180 - atan2(a3, d4) + - atan2(k, -sqrt(discr))) < FLAG_FUZZ) + if (fabs(angleDiff(joint[2]*PM_PI/180, atan2(a3, d4) - + atan2(k, -sqrt(discr)))) < FLAG_FUZZ) { *iflags |= THREE21_ELBOW_DOWN; } @@ -158,7 +168,7 @@ static int three21KinematicsForward(const double * joint, } else { - if (! (fabs(joint[3]*PM_PI/180 - atan2(t1, t2)) < FLAG_FUZZ)) + if (! (fabs(angleDiff(joint[3]*PM_PI/180, atan2(t1, t2))) < FLAG_FUZZ)) { *iflags |= THREE21_WRIST_FLIP; } diff --git a/src/emc/kinematics/trtfuncs.c b/src/emc/kinematics/trtfuncs.c index 31c9ff1a6de..0cb4b5eb7aa 100644 --- a/src/emc/kinematics/trtfuncs.c +++ b/src/emc/kinematics/trtfuncs.c @@ -362,11 +362,14 @@ int xyzbcKinematicsInverse(const EmcPose * pos, const double dz = hal_get_real(haldata->z_offset) + dt; const double b_rad = pos->b*TO_RAD; const double c_rad = pos->c*TO_RAD; - const double dpx = -cos(b_rad)*dx + sin(b_rad)*dz + dx; - const double dpz = -sin(b_rad)*dx - cos(b_rad)*dz + dz; rtapi_real con = hal_get_bool(haldata->conventional_directions) ? 1.0 : -1.0; + // the offsets seen from the tilted table: the same rotation the + // forward applies to them, in the same sense + const double dpx = -cos(b_rad)*dx + con * sin(b_rad)*dz + dx; + const double dpz = -con * sin(b_rad)*dx - cos(b_rad)*dz + dz; + EmcPose P; // computed position P.tran.x = + cos(c_rad) * cos(b_rad) * (pos->tran.x - x_rot_point) diff --git a/src/emc/motion/control.c b/src/emc/motion/control.c index 50ddadc6c07..b509a133d3e 100644 --- a/src/emc/motion/control.c +++ b/src/emc/motion/control.c @@ -363,10 +363,6 @@ static void handle_kinematicsSwitch(void) { return; // the kinematics in force is unchanged } - switchkins_type = requested_type; - hal_set_real(emcmot_hal_data->kins_type, (double)switchkins_type); - emcmotStatus->switchkins_type = switchkins_type; - KINEMATICS_FORWARD_FLAGS tmpFFlags = fflags; KINEMATICS_INVERSE_FLAGS tmpIFlags = iflags; #ifdef SWITCHKINS_DEBUG @@ -376,15 +372,25 @@ static void handle_kinematicsSwitch(void) { beforePose[anum] = *pcmd_p[anum]; } #endif + /* the joints stay where they are, so a kinematics whose forward cannot + solve them is one the machine cannot run in from here: put the old + one back, or the inverse would run the joints to wherever the pose + we know lands in the new one */ EmcPose poseKinsSwitch = emcmotStatus->carte_pos_cmd; if (kinematicsForward(joint_posKinsSwitch, &poseKinsSwitch, &tmpFFlags, &tmpIFlags)) { - reportError(_("kinematicsForward failed for kinematics type %d"), - switchkins_type); + kinematicsSwitch(switchkins_type); + reportError(_("kinematicsForward failed for kinematics type %d," + " type %d is still in force"), + requested_type, switchkins_type); SET_MOTION_ERROR_FLAG(1); // abort - return; // keep the position we know rather than an unsolved one + return; // the kinematics in force and the position are unchanged } emcmotStatus->carte_pos_cmd = poseKinsSwitch; + + switchkins_type = requested_type; + hal_set_real(emcmot_hal_data->kins_type, (double)switchkins_type); + emcmotStatus->switchkins_type = switchkins_type; #ifdef SWITCHKINS_DEBUG fprintf(stderr,"kswitch type=%d (%s:%d)\n",switchkins_type,__FUNCTION__,__LINE__); for (anum = 0; anum < EMCMOT_MAX_AXIS; anum++) { diff --git a/src/hal/components/matrixkins.comp b/src/hal/components/matrixkins.comp index b12dedc2fcf..aac6c04d913 100644 --- a/src/hal/components/matrixkins.comp +++ b/src/hal/components/matrixkins.comp @@ -176,6 +176,7 @@ the adjustment values should be added to the old values instead of replacing the """; see_also "kins(9)"; pin out bool dummy=1; // halcompile requires at least one pin +option extra_setup; license "GPL"; ;; @@ -191,15 +192,15 @@ static struct haldata { hal_real_t C_zz; } *haldata; -static int matrixkins_setup(void) { +EXTRA_SETUP() { + (void)__comp_inst; + (void)prefix; + (void)extra_arg; int res=0; // inherit comp_id from rtapi_main() if (comp_id < 0) goto error; - res = hal_set_unready(comp_id); - if (res) goto error; - haldata = hal_malloc(sizeof(struct haldata)); if (!haldata) goto error; @@ -215,9 +216,6 @@ static int matrixkins_setup(void) { if (res) goto error; - res = hal_ready(comp_id); - if (res) goto error; - rtapi_print("*** %s setup ok\n",__FILE__); return 0; error: @@ -235,8 +233,6 @@ EXPORT_SYMBOL(kinematicsForward); KINEMATICS_TYPE kinematicsType() { - static bool is_setup=0; - if (!is_setup) matrixkins_setup(); return KINEMATICS_BOTH; } diff --git a/src/hal/components/millturn.comp b/src/hal/components/millturn.comp index d1271de882e..45ec650ad96 100644 --- a/src/hal/components/millturn.comp +++ b/src/hal/components/millturn.comp @@ -27,9 +27,10 @@ chapter (docs/src/motion/switchkins.txt) """; // The fpin pin is not accessible in kinematics functions. -// Use the *_setup() function for pins and params used by kinematics. +// Use EXTRA_SETUP() for pins and params used by kinematics. pin out si32 fpin=0"pin to demonstrate use of a conventional (non-kinematics) function fdemo"; option period no; +option extra_setup; function fdemo; license "GPL"; author "David Mueller"; @@ -59,14 +60,15 @@ FUNCTION(fdemo) { fpin_set(fpin + 1); } -static int millturn_setup(void) { +EXTRA_SETUP() { + (void)__comp_inst; + (void)prefix; + (void)extra_arg; #define HAL_PREFIX "millturn" int res=0; // inherit comp_id from rtapi_main() if (comp_id < 0) goto error; - // set unready to allow creation of pins - if (hal_set_unready(comp_id)) goto error; haldata = hal_malloc(sizeof(*haldata)); if (!haldata) goto error; @@ -85,7 +87,6 @@ static int millturn_setup(void) { res += hal_pin_new_bool(comp_id, HAL_OUT, &haldata->kinstype_is_1, 0, "kinstype.is-1"); if (res) goto error; - hal_ready(comp_id); rtapi_print("*** %s setup ok\n",__FILE__); return 0; error: @@ -142,8 +143,6 @@ int kinematicsSwitch(int new_switchkins_type) KINEMATICS_TYPE kinematicsType() { -static bool is_setup=0; - if (!is_setup) millturn_setup(); return KINEMATICS_BOTH; // set as required // Note: If kinematics are identity, using KINEMATICS_BOTH // may be used in order to allow a gui to display diff --git a/src/hal/components/userkins.comp b/src/hal/components/userkins.comp index a2a25d88c29..a7af5d29a75 100644 --- a/src/hal/components/userkins.comp +++ b/src/hal/components/userkins.comp @@ -53,14 +53,16 @@ change all instances of `userkins` to `mykins`. * The *fpin* pin is included to satisfy the requirements of the halcompile utility but it is not accessible to kinematics functions. * HAL pins and parameters needed in kinematics functions (kinematicsForward(), - kinematicsInverse()) must be setup in a function (*userkins_setup()*) invoked - by the initial motion module call to kinematicsType(). + kinematicsInverse()) must be setup in the *EXTRA_SETUP()* function, which + halcompile runs once when the module is loaded, before the component is + made ready. """; // The fpin pin is not accessible in kinematics functions. -// Use the *_setup() function for pins and params used by kinematics. +// Use EXTRA_SETUP() for pins and params used by kinematics. pin out si32 fpin=0"pin to demonstrate use of a conventional (non-kinematics) function fdemo"; option period no; +option extra_setup; function fdemo; license "GPL"; author "Dewey Garrett"; @@ -91,14 +93,15 @@ FUNCTION(fdemo) { fpin_set(fpin + 1); } -static int userkins_setup(void) { +EXTRA_SETUP() { + (void)__comp_inst; + (void)prefix; + (void)extra_arg; #define HAL_PREFIX "userkins" int res=0; // inherit comp_id from rtapi_main() if (comp_id < 0) goto error; - // set unready to allow creation of pins - if (hal_set_unready(comp_id)) goto error; haldata = hal_malloc(sizeof(struct haldata)); if (!haldata) goto error; @@ -112,7 +115,6 @@ static int userkins_setup(void) { res += hal_param_new_real(comp_id, HAL_RO, &haldata->param_ro, 0.0, "%s.param-ro", HAL_PREFIX); if (res) goto error; - hal_ready(comp_id); rtapi_print("*** %s setup ok\n",__FILE__); return 0; error: @@ -130,8 +132,6 @@ EXPORT_SYMBOL(kinematicsForward); KINEMATICS_TYPE kinematicsType() { -static bool is_setup=0; - if (!is_setup) userkins_setup(); return KINEMATICS_IDENTITY; // set as required // Note: If kinematics are identity, using KINEMATICS_BOTH // may be used in order to allow a gui to display diff --git a/src/hal/components/xyzab_tdr_kins.comp b/src/hal/components/xyzab_tdr_kins.comp index b059f32c103..dd0350e44f7 100644 --- a/src/hal/components/xyzab_tdr_kins.comp +++ b/src/hal/components/xyzab_tdr_kins.comp @@ -31,6 +31,7 @@ chapter (docs/src/motion/switchkins.txt) """; pin out si32 dummy=0"one pin needed to satisfy halcompile requirement"; +option extra_setup; license "GPL"; author "David Mueller"; @@ -54,13 +55,14 @@ static struct haldata { hal_bool_t kinstype_is_1; } *haldata; -static int xyzab_tdr_setup(void) { +EXTRA_SETUP() { + (void)__comp_inst; + (void)prefix; + (void)extra_arg; #define HAL_PREFIX "xyzab_tdr_kins" int res=0; // inherit comp_id from rtapi_main() if (comp_id < 0) goto error; - // set unready to allow creation of pins - if (hal_set_unready(comp_id)) goto error; haldata = hal_malloc(sizeof(*haldata)); if (!haldata) goto error; @@ -80,7 +82,6 @@ static int xyzab_tdr_setup(void) { res += hal_pin_new_bool(comp_id, HAL_OUT, &haldata->kinstype_is_1, 0, "kinstype.is-1"); if (res) goto error; - hal_ready(comp_id); rtapi_print("*** %s setup ok\n",__FILE__); return 0; error: @@ -137,8 +138,6 @@ int kinematicsSwitch(int new_switchkins_type) KINEMATICS_TYPE kinematicsType() { -static bool is_setup=0; - if (!is_setup) xyzab_tdr_setup(); return KINEMATICS_BOTH; // set as required // Note: If kinematics are identity, using KINEMATICS_BOTH // may be used in order to allow a gui to display diff --git a/tests/kins-switch-unsolved/README b/tests/kins-switch-unsolved/README new file mode 100644 index 00000000000..c3f263e4f7f --- /dev/null +++ b/tests/kins-switch-unsolved/README @@ -0,0 +1,9 @@ +A kinematics switch whose forward kinematics cannot solve the current +joint position must leave the type in force, the joints and the position +alone, report the failure and abort; motion goes on working in the +kinematics it kept. + +The hexapod module starts in identity kinematics here, so the joints are +put at a few units each as coordinates. Six struts that short are not a +platform position the hexapod forward can converge to, so G12.1 P1 asks +for exactly that switch. diff --git a/tests/kins-switch-unsolved/checkresult b/tests/kins-switch-unsolved/checkresult new file mode 100755 index 00000000000..9d48d3f180e --- /dev/null +++ b/tests/kins-switch-unsolved/checkresult @@ -0,0 +1,3 @@ +#!/bin/sh +# the test script counts its own failures +grep -q "^Exiting with 0 errors" "$1" diff --git a/tests/kins-switch-unsolved/sim.hal b/tests/kins-switch-unsolved/sim.hal new file mode 100644 index 00000000000..e92c60eb526 --- /dev/null +++ b/tests/kins-switch-unsolved/sim.hal @@ -0,0 +1,16 @@ +loadrt [KINS]KINEMATICS +loadrt [EMCMOT]EMCMOT servo_period_nsec=[EMCMOT]SERVO_PERIOD num_joints=[KINS]JOINTS + +addf motion-command-handler servo-thread +addf motion-controller servo-thread + +net J0 joint.0.motor-pos-cmd => joint.0.motor-pos-fb +net J1 joint.1.motor-pos-cmd => joint.1.motor-pos-fb +net J2 joint.2.motor-pos-cmd => joint.2.motor-pos-fb +net J3 joint.3.motor-pos-cmd => joint.3.motor-pos-fb +net J4 joint.4.motor-pos-cmd => joint.4.motor-pos-fb +net J5 joint.5.motor-pos-cmd => joint.5.motor-pos-fb + +net estop-loop iocontrol.0.user-enable-out iocontrol.0.emc-enable-in +net tool-prep-loop iocontrol.0.tool-prepare iocontrol.0.tool-prepared +net tool-change-loop iocontrol.0.tool-change iocontrol.0.tool-changed diff --git a/tests/kins-switch-unsolved/test-ui.py b/tests/kins-switch-unsolved/test-ui.py new file mode 100755 index 00000000000..1995bcd1e23 --- /dev/null +++ b/tests/kins-switch-unsolved/test-ui.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +# A kinematics switch the new forward cannot solve. Motion must keep the +# position it knows, say so, abort, and take a switch back afterwards. +import hal +import linuxcnc +import sys +import time + +c = linuxcnc.command() +s = linuxcnc.stat() +e = linuxcnc.error_channel() + +errors = 0 + +def error(what): + global errors + errors += 1 + print("*** ERROR %s" % what) + +def kins_type(): + return int(hal.get_value("motion.kins-type")) + +def position(): + s.poll() + return tuple(round(v, 6) for v in s.position[:6]) + +def drain(): + said = [] + while True: + m = e.poll() + if not m: + return said + said.append(m[1]) + +def wait_idle(): + deadline = time.time() + 30 + while time.time() < deadline: + s.poll() + if s.interp_state == linuxcnc.INTERP_IDLE and not s.queue: + return + time.sleep(0.05) + error("timed out waiting for the interpreter") + +def mdi(cmd): + c.mdi(cmd) + c.wait_complete(30) + wait_idle() + time.sleep(0.2) # let the error channel and the status catch up + +c.state(linuxcnc.STATE_ESTOP_RESET) +c.state(linuxcnc.STATE_ON) +c.wait_complete(30) +c.home(-1) +c.wait_complete(60) +c.mode(linuxcnc.MODE_MDI) +c.wait_complete(30) +drain() + +if kins_type() != 0: + error("starts in kinematics type %d, expected identity (0)" % kins_type()) + +def joints(): + s.poll() + return tuple(round(v, 6) for v in s.joint_position[:6]) + +# a known position, in identity: joints and coordinates are the same thing +mdi("G0 X1 Y2 Z3 A4 B5 C6") +before = position() +if before != (1, 2, 3, 4, 5, 6): + error("position before the switch is %s, expected (1, 2, 3, 4, 5, 6)" % (before,)) +drain() + +# the switch the hexapod forward cannot solve: six struts of a few units +# are no platform position. Nothing may change but the report. +mdi("G12.1 P1") +said = drain() +if not any("kinematicsForward failed" in m for m in said): + error("no report of the failed forward kinematics, got %s" % said) +if kins_type() != 0: + error("kinematics type %d after the failed switch, expected 0 still" % kins_type()) +if position() != before: + error("position after the failed switch is %s, expected %s unchanged" % (position(), before)) +if joints() != before: + error("joints after the failed switch are %s, expected %s unchanged" % (joints(), before)) +s.poll() +if s.task_state != linuxcnc.STATE_ON: + error("task state %d after the failed switch, expected still ON" % s.task_state) + +# motion goes on working in the kinematics it kept, and the module is +# back in it too: the joints follow the identity inverse, not the hexapod's +c.mode(linuxcnc.MODE_MDI) +c.wait_complete(30) +mdi("G0 X30 Y30 Z30 A30 B30 C30") +if joints() != (30, 30, 30, 30, 30, 30): + error("joints after the move are %s, expected all 30" % (joints(),)) +said = drain() +if said: + error("unexpected messages after the failed switch: %s" % said) + +# a switch to the kinematics in force is nothing to do +mdi("G13.1") +if kins_type() != 0: + error("kinematics type %d after G13.1, expected identity (0)" % kins_type()) +if position() != (30, 30, 30, 30, 30, 30): + error("position after G13.1 is %s, expected all 30" % (position(),)) +said = drain() +if said: + error("unexpected messages after the switch back: %s" % said) + +print("Exiting with %d errors" % errors) +c.state(linuxcnc.STATE_ESTOP) +sys.exit(1 if errors else 0) diff --git a/tests/kins-switch-unsolved/test.ini b/tests/kins-switch-unsolved/test.ini new file mode 100644 index 00000000000..c93bc09563f --- /dev/null +++ b/tests/kins-switch-unsolved/test.ini @@ -0,0 +1,130 @@ +[EMC] +VERSION = 1.1 +DEBUG = 0 + +[DISPLAY] +DISPLAY = ./test-ui.py + +[RS274NGC] +RS274NGC_STARTUP_CODE = G17 G21 G40 G49 G54 G64 P0.001 G80 G90 G92.1 G94 G97 G98 +PARAMETER_FILE = sim.var + +[KINS] +# switchkins-type 0 is identity, 1 is the hexapod, 2 is the userk template +KINEMATICS = genhexkins sparm=identityfirst +JOINTS = 6 + +[HAL] +HALFILE = sim.hal + +[TRAJ] +COORDINATES = XYZABC +LINEAR_UNITS = mm +ANGULAR_UNITS = deg +DEFAULT_LINEAR_VELOCITY = 200 +MAX_LINEAR_VELOCITY = 346 +MAX_LINEAR_ACCELERATION = 800 +DEFAULT_LINEAR_ACCELERATION = 800 +MAX_ANGULAR_VELOCITY = 360 + +[EMCMOT] +EMCMOT = motmod +SERVO_PERIOD = 1000000 +COMM_TIMEOUT = 4 + +[TASK] +TASK = milltask +CYCLE_TIME = 0.010 + +[EMCIO] +TOOL_TABLE = tool.tbl + +[AXIS_X] +MIN_LIMIT = -500 +MAX_LIMIT = 500 +MAX_VELOCITY = 200 +MAX_ACCELERATION = 800 + +[AXIS_Y] +MIN_LIMIT = -500 +MAX_LIMIT = 500 +MAX_VELOCITY = 200 +MAX_ACCELERATION = 800 + +[AXIS_Z] +MIN_LIMIT = -500 +MAX_LIMIT = 500 +MAX_VELOCITY = 200 +MAX_ACCELERATION = 800 + +[AXIS_A] +MIN_LIMIT = -500 +MAX_LIMIT = 500 +MAX_VELOCITY = 60 +MAX_ACCELERATION = 200 + +[AXIS_B] +MIN_LIMIT = -500 +MAX_LIMIT = 500 +MAX_VELOCITY = 60 +MAX_ACCELERATION = 200 + +[AXIS_C] +MIN_LIMIT = -500 +MAX_LIMIT = 500 +MAX_VELOCITY = 60 +MAX_ACCELERATION = 200 + +[JOINT_0] +TYPE = LINEAR +MIN_LIMIT = -500 +MAX_LIMIT = 500 +MAX_VELOCITY = 200 +MAX_ACCELERATION = 800 +HOME_SEARCH_VEL = 0 +HOME_SEQUENCE = 0 + +[JOINT_1] +TYPE = LINEAR +MIN_LIMIT = -500 +MAX_LIMIT = 500 +MAX_VELOCITY = 200 +MAX_ACCELERATION = 800 +HOME_SEARCH_VEL = 0 +HOME_SEQUENCE = 0 + +[JOINT_2] +TYPE = LINEAR +MIN_LIMIT = -500 +MAX_LIMIT = 500 +MAX_VELOCITY = 200 +MAX_ACCELERATION = 800 +HOME_SEARCH_VEL = 0 +HOME_SEQUENCE = 0 + +[JOINT_3] +TYPE = ANGULAR +MIN_LIMIT = -500 +MAX_LIMIT = 500 +MAX_VELOCITY = 60 +MAX_ACCELERATION = 200 +HOME_SEARCH_VEL = 0 +HOME_SEQUENCE = 0 + +[JOINT_4] +TYPE = ANGULAR +MIN_LIMIT = -500 +MAX_LIMIT = 500 +MAX_VELOCITY = 60 +MAX_ACCELERATION = 200 +HOME_SEARCH_VEL = 0 +HOME_SEQUENCE = 0 + +[JOINT_5] +TYPE = ANGULAR +MIN_LIMIT = -500 +MAX_LIMIT = 500 +MAX_VELOCITY = 60 +MAX_ACCELERATION = 200 +HOME_SEARCH_VEL = 0 +HOME_SEQUENCE = 0 diff --git a/tests/kins-switch-unsolved/test.sh b/tests/kins-switch-unsolved/test.sh new file mode 100755 index 00000000000..765cf14fed6 --- /dev/null +++ b/tests/kins-switch-unsolved/test.sh @@ -0,0 +1,4 @@ +#!/bin/bash -e +# a failed run leaves the var file behind, and it carries offsets +rm -f sim.var sim.var.bak +linuxcnc -r test.ini diff --git a/tests/kins-switch-unsolved/tool.tbl b/tests/kins-switch-unsolved/tool.tbl new file mode 100644 index 00000000000..d793e2d60ed --- /dev/null +++ b/tests/kins-switch-unsolved/tool.tbl @@ -0,0 +1 @@ +T1 P1 D0.0 Z12.5 ; diff --git a/tests/maxkins/README b/tests/maxkins/README new file mode 100644 index 00000000000..cdf964410db --- /dev/null +++ b/tests/maxkins/README @@ -0,0 +1,7 @@ +Tests that maxkins forward and inverse kinematics agree with each other. + +The machine is driven to a series of poses with the pivot length, the tool +length and every one of B, C, U, V and W away from zero, and the position +that comes back through the forward kinematics is compared with the one that +was commanded. A pose that survives the round trip is one where the two +directions of the transform describe the same machine. diff --git a/tests/maxkins/checkresult b/tests/maxkins/checkresult new file mode 100755 index 00000000000..24dc9aa53e3 --- /dev/null +++ b/tests/maxkins/checkresult @@ -0,0 +1,2 @@ +#!/bin/sh +exit 0 # test failure is indicated by test.sh exit value diff --git a/tests/maxkins/core_sim.hal b/tests/maxkins/core_sim.hal new file mode 100644 index 00000000000..daf9b8f4c25 --- /dev/null +++ b/tests/maxkins/core_sim.hal @@ -0,0 +1,28 @@ +# core HAL config file for simulation + +loadrt [KINS]KINEMATICS +loadrt [EMCMOT]EMCMOT servo_period_nsec=[EMCMOT]SERVO_PERIOD num_joints=[KINS]JOINTS + +# add motion controller functions to servo thread +addf motion-command-handler servo-thread +addf motion-controller servo-thread + +# create HAL signals for position commands from motion module +# loop position commands back to motion module feedback +net Xpos joint.0.motor-pos-cmd => joint.0.motor-pos-fb +net Ypos joint.1.motor-pos-cmd => joint.1.motor-pos-fb +net Zpos joint.2.motor-pos-cmd => joint.2.motor-pos-fb +net Apos joint.3.motor-pos-cmd => joint.3.motor-pos-fb +net Bpos joint.4.motor-pos-cmd => joint.4.motor-pos-fb +net Cpos joint.5.motor-pos-cmd => joint.5.motor-pos-fb +net Upos joint.6.motor-pos-cmd => joint.6.motor-pos-fb +net Vpos joint.7.motor-pos-cmd => joint.7.motor-pos-fb +net Wpos joint.8.motor-pos-cmd => joint.8.motor-pos-fb + +# the geometry the kinematics works from. Both are away from zero so that +# the pivot to tip distance is not degenerate. +setp maxkins.pivot-length [MAX]B_PIVOT_LENGTH +setp maxkins.tool-length [MAX]TOOL_LENGTH + +# estop loopback +net estop-loop iocontrol.0.user-enable-out iocontrol.0.emc-enable-in diff --git a/tests/maxkins/test-ui.py b/tests/maxkins/test-ui.py new file mode 100755 index 00000000000..d65e561e516 --- /dev/null +++ b/tests/maxkins/test-ui.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 + +import linuxcnc +import hal + +import time +import sys + + +def wait_for_linuxcnc_startup(status, timeout=10.0): + + """Poll the Status buffer waiting for it to look initialized, + rather than just allocated (all-zero). Returns on success, throws + RuntimeError on failure.""" + + start_time = time.time() + while time.time() - start_time < timeout: + status.poll() + if (status.angular_units == 0.0) \ + or (status.axis_mask == 0) \ + or (status.cycle_time == 0.0) \ + or (status.exec_state != linuxcnc.EXEC_DONE) \ + or (status.interp_state != linuxcnc.INTERP_IDLE) \ + or (status.inpos == False) \ + or (status.linear_units == 0.0) \ + or (status.max_acceleration == 0.0) \ + or (status.max_velocity == 0.0) \ + or (status.program_units == 0.0) \ + or (status.rapidrate == 0.0) \ + or (status.state != linuxcnc.RCS_DONE) \ + or (status.task_state != linuxcnc.STATE_ESTOP): + time.sleep(0.1) + else: + # looks good + return + + # timeout, throw an exception + raise RuntimeError("Timeout") + + +c = linuxcnc.command() +s = linuxcnc.stat() +e = linuxcnc.error_channel() + +# Wait for LinuxCNC to initialize itself so the Status buffer stabilizes. +wait_for_linuxcnc_startup(s) + +# Because the kinematics is non-trivial, a homing is needed. +# HOME_ABSOLUTE_ENCODER = 1 is used in the ini file. +c.state(linuxcnc.STATE_ESTOP_RESET) +c.state(linuxcnc.STATE_ON) +for joint in range(9): + c.home(joint) +c.wait_complete() + +LETTERS = 'XYZABCUVW' + + +def absdelta(a, b): + '''Maximum absolute difference between components of two coordinate points''' + return max(abs(a[i] - b[i]) for i in range(len(a))) + + +def test_pose(pose): + '''Command LinuxCNC to go to 'pose', then check that the position that + comes back matches it. Motion runs the inverse kinematics to turn the + pose into joint values and the forward kinematics to turn the joint + feedback back into a position, so a pose that comes back unchanged is one + the two directions agree on. + ''' + words = ' '.join('%s%0.9f' % (letter, value) + for letter, value in zip(LETTERS, pose)) + + c.mode(linuxcnc.MODE_MDI) + c.mdi('G0 ' + words) + c.wait_complete() + + # The delay seems to be needed for trajectory to fully settle, otherwise + # there is about 1e-5 error between target and actual position. + time.sleep(0.05) + s.poll() + joints = s.joint_actual_position[:9] + position = s.actual_position[:9] + + print("Commanded %s, joints %s, position %s" % (pose, joints, position)) + + # Accuracy limit is set to 1e-9 here. + # For practical purposes, a numerical accuracy of 1e-6 would be perfectly + # acceptable. Current implementation using doubles achieves about 1e-14 + # precision. + if absdelta(position, pose) > 1e-9: + raise RuntimeError( + "Forward and inverse kinematics disagree: commanded %s, joints %s, got %s" + % (pose, joints, position)) + + +# X Y Z A B C U V W +POSES = [ + # The pose everything starts from. + ( 0, 0, 0, 0, 0, 0, 0, 0, 0), + # Linear axes only, with the head upright. + ( 10, 20, 30, 0, 0, 0, 0, 0, 0), + # B alone tilts the head, which moves the tip in X and Z. + ( 10, 20, 30, 0, 40, 0, 0, 0, 0), + # C alone turns the table under the tip. + ( 10, 20, 30, 0, 0, 30, 0, 0, 0), + # B and C together: the head correction is in the machine frame and the + # table rotation is not, so the order the two are applied in matters. + ( 10, 20, 30, 0, 40, 30, 0, 0, 0), + # U alone shifts the head along its own axis, which with B tilted has a + # component in both X and Z. + ( 10, 20, 30, 0, 40, 0, 15, 0, 0), + # V is a plain shift of the saddle. + ( 10, 20, 30, 0, 40, 0, 0, 8, 0), + # W extends the pivot to tip distance. + ( 10, 20, 30, 0, 40, 0, 0, 0, 12), + # Everything at once. + ( 50, 50, 50, 5, 40, 30, 15, 8, 12), + # And with the signs the other way round. + ( -50, -50, -50, -5, -40, -30, -15, -8, -12), + # Back to the origin, where the pose is the same whichever direction the + # axes are taken to run in. + ( 0, 0, 0, 0, 0, 0, 0, 0, 0), +] + +for conventional in (0, 1): + hal.set_p('maxkins.conventional-directions', str(conventional)) + print("conventional-directions %d" % conventional) + for pose in POSES: + test_pose(pose) + +sys.exit(0) diff --git a/tests/maxkins/test.ini b/tests/maxkins/test.ini new file mode 100644 index 00000000000..7cf23c05738 --- /dev/null +++ b/tests/maxkins/test.ini @@ -0,0 +1,210 @@ +[EMC] +# The version string for this INI file. +VERSION = 1.1 + +DEBUG = 0 + +[MAX] +# distance from the end of the reference tool to the centre of the head +# tilt axis, and the length of the tool on top of it +B_PIVOT_LENGTH = 100 +TOOL_LENGTH = 25 + +[DISPLAY] +DISPLAY = ./test-ui.py + +[FILTER] +#No Content + +[RS274NGC] +PARAMETER_FILE = sim.var + +[EMCMOT] +EMCMOT = motmod +COMM_TIMEOUT = 4.0 +BASE_PERIOD = 0 +SERVO_PERIOD = 1000000 + +[TASK] +TASK = milltask +CYCLE_TIME = 0.001 + +[HAL] +HALUI = halui +HALFILE = core_sim.hal + +[HALUI] +#No Content + +[TRAJ] +COORDINATES = X Y Z A B C U V W +HOME = 0 0 0 0 0 0 0 0 0 +LINEAR_UNITS = mm +ANGULAR_UNITS = degree +DEFAULT_LINEAR_VELOCITY = 10 +MAX_LINEAR_VELOCITY = 100 +MAX_LINEAR_ACCELERATION = 1000 +DEFAULT_ANGULAR_VELOCITY = 10 +MAX_ANGULAR_VELOCITY = 100 +MAX_ANGULAR_ACCELERATION = 1000 + +[EMCIO] +TOOL_TABLE = tool.tbl +RANDOM_TOOLCHANGER = 0 + +[KINS] +KINEMATICS = maxkins +JOINTS = 9 + +[AXIS_X] +MIN_LIMIT = -1000 +MAX_LIMIT = 1000 +MAX_VELOCITY = 100 +MAX_ACCELERATION = 1000 + +[JOINT_0] +TYPE = LINEAR +HOME = 0.000 +HOME_ABSOLUTE_ENCODER = 1 +MAX_VELOCITY = 100 +MAX_ACCELERATION = 1000 +BACKLASH = 0.000 +MIN_LIMIT = -1000 +MAX_LIMIT = 1000 +FERROR = 0.001 + +[AXIS_Y] +MIN_LIMIT = -1000 +MAX_LIMIT = 1000 +MAX_VELOCITY = 100 +MAX_ACCELERATION = 1000 + +[JOINT_1] +TYPE = LINEAR +HOME = 0.000 +HOME_ABSOLUTE_ENCODER = 1 +MAX_VELOCITY = 100 +MAX_ACCELERATION = 1000 +BACKLASH = 0.000 +MIN_LIMIT = -1000 +MAX_LIMIT = 1000 +FERROR = 0.001 + +[AXIS_Z] +MIN_LIMIT = -1000 +MAX_LIMIT = 1000 +MAX_VELOCITY = 100 +MAX_ACCELERATION = 1000 + +[JOINT_2] +TYPE = LINEAR +HOME = 0.000 +HOME_ABSOLUTE_ENCODER = 1 +MAX_VELOCITY = 100 +MAX_ACCELERATION = 1000 +BACKLASH = 0.000 +MIN_LIMIT = -1000 +MAX_LIMIT = 1000 +FERROR = 0.001 + +[AXIS_A] +MIN_LIMIT = -360 +MAX_LIMIT = 360 +MAX_VELOCITY = 100 +MAX_ACCELERATION = 1000 + +[JOINT_3] +TYPE = ANGULAR +HOME = 0.000 +HOME_ABSOLUTE_ENCODER = 1 +MAX_VELOCITY = 100 +MAX_ACCELERATION = 1000 +BACKLASH = 0.000 +MIN_LIMIT = -360 +MAX_LIMIT = 360 +FERROR = 0.001 + +[AXIS_B] +MIN_LIMIT = -360 +MAX_LIMIT = 360 +MAX_VELOCITY = 100 +MAX_ACCELERATION = 1000 + +[JOINT_4] +TYPE = ANGULAR +HOME = 0.000 +HOME_ABSOLUTE_ENCODER = 1 +MAX_VELOCITY = 100 +MAX_ACCELERATION = 1000 +BACKLASH = 0.000 +MIN_LIMIT = -360 +MAX_LIMIT = 360 +FERROR = 0.001 + +[AXIS_C] +MIN_LIMIT = -360 +MAX_LIMIT = 360 +MAX_VELOCITY = 100 +MAX_ACCELERATION = 1000 + +[JOINT_5] +TYPE = ANGULAR +HOME = 0.000 +HOME_ABSOLUTE_ENCODER = 1 +MAX_VELOCITY = 100 +MAX_ACCELERATION = 1000 +BACKLASH = 0.000 +MIN_LIMIT = -360 +MAX_LIMIT = 360 +FERROR = 0.001 + +[AXIS_U] +MIN_LIMIT = -1000 +MAX_LIMIT = 1000 +MAX_VELOCITY = 100 +MAX_ACCELERATION = 1000 + +[JOINT_6] +TYPE = LINEAR +HOME = 0.000 +HOME_ABSOLUTE_ENCODER = 1 +MAX_VELOCITY = 100 +MAX_ACCELERATION = 1000 +BACKLASH = 0.000 +MIN_LIMIT = -1000 +MAX_LIMIT = 1000 +FERROR = 0.001 + +[AXIS_V] +MIN_LIMIT = -1000 +MAX_LIMIT = 1000 +MAX_VELOCITY = 100 +MAX_ACCELERATION = 1000 + +[JOINT_7] +TYPE = LINEAR +HOME = 0.000 +HOME_ABSOLUTE_ENCODER = 1 +MAX_VELOCITY = 100 +MAX_ACCELERATION = 1000 +BACKLASH = 0.000 +MIN_LIMIT = -1000 +MAX_LIMIT = 1000 +FERROR = 0.001 + +[AXIS_W] +MIN_LIMIT = -1000 +MAX_LIMIT = 1000 +MAX_VELOCITY = 100 +MAX_ACCELERATION = 1000 + +[JOINT_8] +TYPE = LINEAR +HOME = 0.000 +HOME_ABSOLUTE_ENCODER = 1 +MAX_VELOCITY = 100 +MAX_ACCELERATION = 1000 +BACKLASH = 0.000 +MIN_LIMIT = -1000 +MAX_LIMIT = 1000 +FERROR = 0.001 diff --git a/tests/maxkins/test.sh b/tests/maxkins/test.sh new file mode 100755 index 00000000000..6edf34d77ca --- /dev/null +++ b/tests/maxkins/test.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +rm -f sim.var +linuxcnc -r test.ini diff --git a/tests/maxkins/tool.tbl b/tests/maxkins/tool.tbl new file mode 100644 index 00000000000..e69de29bb2d