diff --git a/docs/src/gcode.html.in b/docs/src/gcode.html.in index 6e4c641129f..02353bbcf36 100644 --- a/docs/src/gcode.html.in +++ b/docs/src/gcode.html.in @@ -165,6 +165,7 @@ tr.head td, tr.head th { background: black; color: white; } G10 L2P RSet Coordinate System G10 L20PSet Coordinate System G28, G28.1Go/Set Predefined Position + G28.2PHome from G-code G30, G30.1Go/Set Predefined Position G53Move in Machine Coordinates G52, G92Coordinate System Offset diff --git a/docs/src/gcode/g-code.adoc b/docs/src/gcode/g-code.adoc index 12dbcdf599f..7e27083cfad 100644 --- a/docs/src/gcode/g-code.adoc +++ b/docs/src/gcode/g-code.adoc @@ -74,6 +74,7 @@ as the 'L number', and so on for any other letter. |<> |Plane Select |<> |Set Units of Measure |<> |Go to Predefined Position +|<> |Home from G-code |<> |Go to Predefined Position |<> |Spindle Synchronized Motion |<> |Rigid Tapping @@ -1070,6 +1071,81 @@ It is an error if : * Cutter Compensation is turned on +[[gcode:g28.2]] +== G28.2 Home from G-code(((G28.2 Home from G-code))) + +This non-modal code lets a program or MDI line reference the machine +instead of requiring the operator to use the GUI's *Home All* button. It +follows the same modal-group-0 pattern as `G28.1`/`G30.1`, takes no axis +words, and *requires* a `P` word saying what to home. + +* 'G28.2 P-1' - runs the homing cycle on every joint, in `HOME_SEQUENCE` + order (the same operation as the GUI *Home All*). This form requires the + config to set `[JOINT_n]HOME_SEQUENCE`; exactly as with *Home All*, a + config with no `HOME_SEQUENCE` cannot home all joints at once and the + command fails, leaving the machine in joint mode. +* 'G28.2 Pn' - runs the homing cycle on joint 'n' only, where 'n' is the + 0-based joint number matching its `[JOINT_n]` INI section (the same + numbering used by `HOME_SEQUENCE` and by joint jogging). Other joints are + left as they are, and `HOME_SEQUENCE` is not required for the `Pn` form. + How many joints actually home depends on the `HOME_SEQUENCE` of joint 'n': ++ +-- +** a *negative* (synchronized) sequence homes the whole synchronized group - + `Pn` on either joint of a gantry pair homes both; +** a *positive* sequence shared with other joints homes only joint 'n' - + use `G28.2 P-1` to home the shared group together; +** no sequence, or a sequence not shared with any other joint, homes just + joint 'n'. +-- + +[WARNING] +There is no bare `G28.2`. Homing drives joints onto their switches at +homing speed, ignoring soft limits, from wherever the machine happens to +be, so homing the whole machine has to be asked for explicitly with `P-1` +rather than being what a truncated or mistyped line does. A `G28.2` with no +`P` word is an error and stops the program. + +.G28.2 Example Lines +[source,ngc] +---- +G28.2 P-1 (home all joints, in HOME_SEQUENCE order) +G28.2 P1 (home joint 1 only) +---- + +A queued `G28.2` dips motion into free mode for the duration of the homing +cycle and restores whatever mode (manual/MDI/auto) was active once it +finishes, so the mode dip is invisible at the task level. Motion still +enforces its own safety: the home is honored only when the machine is idle +(in position with no queued motion) or in joint mode, and a home is refused +mid-motion. Homing inhibits and per-joint limit handling are unchanged. + +[NOTE] +When the homing cycle finishes, the interpreter resyncs its model of the +current position from the machine (the same way it does after probing or a +tool change), so a following move made in incremental distance mode +(`G91`), or an arc whose center is given with `I`/`J`/`K`, is computed from +where the machine actually is after homing. `G28.2` is a queue point: read +ahead stops at it and resumes once the cycle has run. `G5x`/`G92` offsets +that were active before the `G28.2` are still applied afterward and now +refer to the newly established machine zero; clear or re-establish them if +that is not what you want. + +[NOTE] +There is no G-code unhome. Clearing a joint's reference is done from the +GUI, halui or linuxcncrsh. + +[NOTE] +`G28.2` is a LinuxCNC extension; there is no standard Fanuc equivalent. + +It is an error if: + +* Cutter radius compensation is active (`G41`, `G41.1`, `G42` or `G42.1`). +* An axis word is present ('G28.2' takes no axis words). +* The 'P' word is missing. +* The 'P' word is not a whole number, or is negative other than `P-1`. +* The 'P' word names a joint number the machine does not have. + [[gcode:g30-g30.1]] == G30, G30.1 Go/Set Predefined Position(((G30 Go/Set Predefined Position))) diff --git a/docs/src/gcode/overview.adoc b/docs/src/gcode/overview.adoc index 62c6fd87198..85ff1a127a5 100644 --- a/docs/src/gcode/overview.adoc +++ b/docs/src/gcode/overview.adoc @@ -964,7 +964,7 @@ The modal groups are shown in the following Table. [width="80%",cols="4,6",options="header"] |=== |Modal Group Meaning | Member Words -|Non-modal codes (Group 0) | G4, G10 G28, G30, G52, G53, G92, G92.1, G92.2, G92.3, +|Non-modal codes (Group 0) | G4, G10 G28, G28.2, G30, G52, G53, G92, G92.1, G92.2, G92.3, |Motion (Group 1) | G0, G1, G2, G3, G33, G38.n, G73, G76, G80, G81 G82, G83, G84, G85, G86, G87, G88, G89 |Plane selection (Group 2) | G17, G18, G19, G17.1, G18.1, G19.1 diff --git a/docs/src/remap/remap.adoc b/docs/src/remap/remap.adoc index 218062048cd..75e2a0980fa 100644 --- a/docs/src/remap/remap.adoc +++ b/docs/src/remap/remap.adoc @@ -1851,7 +1851,7 @@ All the listed G-codes are already defined in the current implementation of Linu |25 | | | | | | | | | | |26 | | | | | | | | | | |27 | | | | | | | | | | -|28 |G28 |G28.1 | | | | | | | | +|28 |G28 |G28.1 |G28.2 | | | | | | | |29 | | | | | | | | | | |=== diff --git a/lib/python/common/hal_glib.py b/lib/python/common/hal_glib.py index a157a3f47c0..d5c49163990 100644 --- a/lib/python/common/hal_glib.py +++ b/lib/python/common/hal_glib.py @@ -287,7 +287,7 @@ class _GStat(GObject.GObject): STATE_ON = linuxcnc.STATE_ON STATE_OFF = linuxcnc.STATE_OFF - group0 = ('G4', 'G10','G28', 'G30', 'G52', 'G53', 'G92', 'G92.1', 'G92.2', 'G92.3') + group0 = ('G4', 'G10','G28', 'G28.2', 'G30', 'G52', 'G53', 'G92', 'G92.1', 'G92.2', 'G92.3') group1 = ('G0', 'G1', 'G2', 'G3', 'G33', 'G38.n', 'G73', 'G76', 'G80', 'G81',\ 'G82', 'G83', 'G84', 'G85', 'G86', 'G87', 'G88', 'G89') group2 = ('G17', 'G18', 'G19', 'G17.1', 'G18.1', 'G19.1') diff --git a/lib/python/qtvcp/lib/mdi_text.py b/lib/python/qtvcp/lib/mdi_text.py index e1a4cdca880..dd477c905c9 100644 --- a/lib/python/qtvcp/lib/mdi_text.py +++ b/lib/python/qtvcp/lib/mdi_text.py @@ -27,6 +27,7 @@ def gcode_titles(): 'G21': 'Set Units to Millimeters', 'G28': 'Go to Predefined Position', 'G28.1': 'Set Predefined Position', + 'G28.2': 'Home from G-code', 'G30': 'Go to Predefined Position', 'G30.1': 'Set Predefined Position', 'G33': 'Spindle Synchronized Motion', @@ -209,6 +210,7 @@ def gcode_descriptions(gcode): 'G21': G21, 'G28': G28, 'G28.1': G28_1, + 'G28.2': G28_2, 'G30': G30, 'G30.1': G30_1, 'G33': G33, @@ -467,6 +469,29 @@ def gcode_descriptions(gcode): G28.1 - stores the current absolute position into parameters 5161-5166. """ + +G28_2 = """G28.2 Home from G-code +Runs the homing cycle, the same operation as the GUI +Home All button, so a program or MDI line can +reference the machine. Takes no axis words, and +requires a P word saying what to home. + +G28.2 P-1 - homes all joints, in HOME_SEQUENCE order +(the config must set HOME_SEQUENCE). + +G28.2 Pn - homes joint n only, where n is the +0-based joint number matching its [JOINT_n] INI +section. On a synchronized (negative HOME_SEQUENCE) +joint pair, Pn on either joint homes both. + +There is no bare G28.2: homing the whole machine has +to be asked for with P-1. + +It is an error if cutter compensation is on, if the P +word is missing or is not -1 or a whole joint number, +or if Pn names a joint the machine does not have. +""" + G30 = """G30 Go to Predefined Position G30 uses the values stored in parameters 5181-5189 as the X Y Z A B C U V W final point to move to. diff --git a/src/emc/motion/command.c b/src/emc/motion/command.c index a938fa5d613..17e9c767a37 100644 --- a/src/emc/motion/command.c +++ b/src/emc/motion/command.c @@ -1505,8 +1505,23 @@ void emcmotCommandHandler_locked(void *arg, long servo_period) rtapi_print_msg(RTAPI_MSG_DBG, "JOINT_HOME"); rtapi_print_msg(RTAPI_MSG_DBG, " %d", joint_num); - if (emcmotStatus->motion_state != EMCMOT_MOTION_FREE) { - /* can't home unless in free mode */ + /* Normally homing requires free (joint) mode. The queued G28.2 path + * (emctaskmain EMC_TASK_EXEC::WAITING_FOR_HOMING) dips motion into + * FREE before issuing this command; while that transition is still + * settling motion_state can read non-FREE for a cycle. Accept the + * command in exactly that window -- in position, nothing queued, and + * a FREE transition already pending (teleop and coord both cleared) + * -- so a G-code-triggered home from MDI / a program is honored. + * + * Do NOT accept it merely because motion is idle: an immediate home + * (halui, linuxcncrsh, c.home(n)) on an already-homed machine sitting + * in TELEOP is idle too, and do_homing() only advances in FREE + * (control.c), so accepting it there would silently drop the + * request (found in review of PR #4172). */ + if (emcmotStatus->motion_state != EMCMOT_MOTION_FREE + && !(GET_MOTION_INPOS_FLAG() && emcmotStatus->depth == 0 + && !emcmotInternal->teleoperating + && !emcmotInternal->coordinating)) { reportError(_("must be in joint mode to home")); return; } diff --git a/src/emc/motion/control.c b/src/emc/motion/control.c index b509a133d3e..43ad5854168 100644 --- a/src/emc/motion/control.c +++ b/src/emc/motion/control.c @@ -2273,6 +2273,7 @@ static void update_status(void) } emcmotStatus->jogging_active = hal_get_bool(emcmot_hal_data->jog_is_active); + emcmotStatus->homing_active = get_homing_is_active(); /*! \todo FIXME - the rest of this function is stuff that was apparently dropped in the initial move from emcmot.c to control.c. I diff --git a/src/emc/motion/motion.h b/src/emc/motion/motion.h index 06ff25eb0ba..b107b232811 100644 --- a/src/emc/motion/motion.h +++ b/src/emc/motion/motion.h @@ -675,6 +675,10 @@ Suggestion: Split this in to an Error and a Status flag register.. int numExtraJoints; int stepping; bool jogging_active; + bool homing_active; /* homing state machine is running (get_homing_is_active()). + Aggregate: stays true across the gap between + HOME_SEQUENCE groups, when every joint's per-joint + .homing flag is momentarily false. */ int switchkins_seq; /* echoes the config counter once acted on */ int switchkins_type; /* switchkins type now in force */ diff --git a/src/emc/nml_intf/canon.hh b/src/emc/nml_intf/canon.hh index 51fce63005f..96e558dbf23 100644 --- a/src/emc/nml_intf/canon.hh +++ b/src/emc/nml_intf/canon.hh @@ -243,6 +243,13 @@ extern void SET_G92_OFFSET(double x, double y, double z, extern void SET_XY_ROTATION(double t); +/* G28.2: trigger the machine homing cycle from G-code (bare form = all + * joints, in HOME_SEQUENCE order). Maps to EMC_JOINT_HOME(-1). */ +extern void HOME_CYCLE(void); +/* G28.2 Pn: home a single joint by its 0-based joint number (matching + * [JOINT_n] INI section numbering). Maps to EMC_JOINT_HOME(joint). */ +extern void HOME_CYCLE_JOINT(int joint); + /* Offset the origin to the point with absolute coordinates x, y, z, a, b, c, u, v, and w. Values of x, y, z, a, b, c, u, v, and w are real numbers. The units are whatever length units are being used at the time diff --git a/src/emc/nml_intf/emc.cc b/src/emc/nml_intf/emc.cc index bf20de160e3..fefc92b602f 100644 --- a/src/emc/nml_intf/emc.cc +++ b/src/emc/nml_intf/emc.cc @@ -1864,6 +1864,7 @@ void EMC_MOTION_STAT::update(CMS * cms) EmcPose_update(cms, &eoffset_pose); cms->update(numExtraJoints); cms->update(jogging_active); + cms->update(homing_active); cms->update(heartbeat); } diff --git a/src/emc/nml_intf/emc.hh b/src/emc/nml_intf/emc.hh index bf4777478e7..dbe30abe555 100644 --- a/src/emc/nml_intf/emc.hh +++ b/src/emc/nml_intf/emc.hh @@ -216,7 +216,8 @@ enum class EMC_TASK_EXEC { WAITING_FOR_DELAY = 8, WAITING_FOR_SYSTEM_CMD = 9, WAITING_FOR_SPINDLE_ORIENTED = 10, - WAITING_FOR_KINS_SWITCH = 11 + WAITING_FOR_KINS_SWITCH = 11, + WAITING_FOR_HOMING = 12 }; // types for EMC_TASK interpState diff --git a/src/emc/nml_intf/emc_nml.hh b/src/emc/nml_intf/emc_nml.hh index 7690268f9ee..2636d9c9302 100644 --- a/src/emc/nml_intf/emc_nml.hh +++ b/src/emc/nml_intf/emc_nml.hh @@ -1189,6 +1189,12 @@ class EMC_MOTION_STAT:public EMC_MOTION_STAT_MSG { EmcPose eoffset_pose; int numExtraJoints; bool jogging_active; + // Aggregate "the homing state machine is running", from + // get_homing_is_active() in motion. Unlike the per-joint EMC_JOINT_STAT + // .homing flags, this stays true across the gap between HOME_SEQUENCE + // groups, where every joint momentarily reads .homing == false while the + // machine is still homing (see the race note in motion/homing.c). + bool homing_active; uint64_t heartbeat; // motion controller's heartbeat counter }; diff --git a/src/emc/nml_intf/emcops.cc b/src/emc/nml_intf/emcops.cc index c6e77f40d9d..98e1eb26786 100644 --- a/src/emc/nml_intf/emcops.cc +++ b/src/emc/nml_intf/emcops.cc @@ -117,6 +117,7 @@ EMC_MOTION_STAT::EMC_MOTION_STAT() eoffset_pose{}, numExtraJoints(0), jogging_active(0), + homing_active(false), heartbeat(0) { } diff --git a/src/emc/rs274ngc/gcodemodule.cc b/src/emc/rs274ngc/gcodemodule.cc index a4bdad6ef16..cb51aee8611 100644 --- a/src/emc/rs274ngc/gcodemodule.cc +++ b/src/emc/rs274ngc/gcodemodule.cc @@ -544,6 +544,9 @@ void SELECT_PLANE(CANON_PLANE pl) { parse_state.canon->set_plane(static_cast(pl)); } +void HOME_CYCLE(void) {} +void HOME_CYCLE_JOINT(int) {} + void SET_TRAVERSE_RATE(double rate) { parse_state.canon->set_traverse_rate(rate); } diff --git a/src/emc/rs274ngc/interp_array.cc b/src/emc/rs274ngc/interp_array.cc index 63f6d20975a..6bfaa513b4e 100644 --- a/src/emc/rs274ngc/interp_array.cc +++ b/src/emc/rs274ngc/interp_array.cc @@ -84,7 +84,7 @@ const int Interp::gees[] = { /* 220 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* 240 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* 260 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -/* 280 */ 0, 0,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 280 */ 0, 0, 0,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, // 282=G28.2 /* 300 */ 0, 0,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* 320 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1, 1, 1,-1,-1,-1,-1,-1,-1,-1,-1, /* 340 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, diff --git a/src/emc/rs274ngc/interp_check.cc b/src/emc/rs274ngc/interp_check.cc index 8be61f9c609..1eeb8625c1a 100644 --- a/src/emc/rs274ngc/interp_check.cc +++ b/src/emc/rs274ngc/interp_check.cc @@ -100,6 +100,7 @@ int Interp::check_g_codes(block_pointer block, //!< pointer to a block to be c } else if (mode1 == G_5_2){ } else if (mode1 == G_6_2){ } else if (mode0 == G_28_1 || mode0 == G_30_1) { + } else if (mode0 == G_28_2) { // G-code homing } else if (mode0 == G_52) { } else if (mode0 == G_53) { CHKS(((block->motion_to_be != G_0) && (block->motion_to_be != G_1)), @@ -331,12 +332,14 @@ int Interp::check_other_codes(block_pointer block) //!< pointer to a block (motion != G_6) && (motion != G_6_2) && (motion != G_2) && (motion != G_3) && (motion != G_74) && (motion != G_84) && + (block->g_modes[GM_MODAL_0] != G_28_2) && (block->m_modes[9] != 50) && (block->m_modes[9] != 51) && (block->m_modes[9] != 52) && (block->m_modes[9] != 53) && (block->m_modes[5] != 62) && (block->m_modes[5] != 63) && (block->m_modes[5] != 64) && (block->m_modes[5] != 65) && (block->m_modes[5] != 66) && (block->m_modes[7] != 19) && (block->user_m != 1) && (block->o_type != M_98)), _("P word with no G2 G3 G4 G10 G12.1 G64 G5 G5.2 G6, G6.2, G76 G82 G86 G88 G89" + " G28.2" " or M50 M51 M52 M53 M62 M63 M64 M65 M66 M98 " "or user M code to use it")); int p_value = round_to_int(block->p_number); diff --git a/src/emc/rs274ngc/interp_convert.cc b/src/emc/rs274ngc/interp_convert.cc index 4ed206d83e7..93577daa852 100644 --- a/src/emc/rs274ngc/interp_convert.cc +++ b/src/emc/rs274ngc/interp_convert.cc @@ -3167,6 +3167,93 @@ Called by: convert_modal_0. */ +/*! convert_home_cycle + +Handles G28.2 (run the homing cycle) from a G-code line, so machines can +reference themselves from MDI or a program instead of only from the GUI's +*Home All* button. + +The P word is mandatory and says what to home: P-1 homes every joint in +HOME_SEQUENCE order, and P0, P1, ... home a single joint by its 0-based +joint number (matching [JOINT_n] INI section numbering, e.g. P1 -> +JOINT_1). The single-joint form is the primitive raised in the PR #4172 +discussion for re-homing a joint that is switched between rotary-axis and +spindle use mid-program +(https://github.com/LinuxCNC/linuxcnc/pull/4172) -- it reuses the existing +EMC_JOINT_HOME 'joint' field, so it needs no NML change and works +identically on any kinematics. + +There is deliberately no bare form. G28.2 alone is refused, and homing +every joint has to be asked for explicitly with P-1 (PR #4172, BsAtHome). +Homing drives joints onto their switches at homing speed, ignoring soft +limits, from wherever the machine happens to be; a G-code word that starts +that on every joint of the machine should not be the one you get by +mistyping or truncating a line, or by leaving a stale G28.2 in a file +edited for a machine with different homing. A refusal, rather than doing +nothing quietly: an operator whose G28.2 silently did nothing would have +no way to tell it from a homing cycle that ran, and would go on to cut +against a reference that was never re-established. + +The negative values the NML EMC_JOINT_HOME field can carry are not all +G-code surface: -1 (all) is now spelled P-1, but -2 (volatile unhome) is +refused here, as are all other negatives. Axis-letter forms (G28.2 X) are +deliberately NOT supported either: resolving an axis letter to a joint +needs the kinematics coordinate map and isn't trivial even on trivkins +(duplicate letters on gantries), and homing is a joint concept, not an +axis one. + +There is deliberately no G-code unhome. A G28.3 was part of the original +proposal and was dropped during review of PR #4172: no use was found for it +that a numbered parameter would not serve better under NO_FORCE_HOMING=1, +and it was the one operation able to leave a running program on an +unreferenced machine. The GUI, halui and linuxcncrsh keep their existing +unhome. + +On a synchronized (negative HOME_SEQUENCE) joint pair, Pn on either joint +homes both (motion's existing gantry-homing behavior); on a positive shared +sequence Pn homes only the named joint -- use P-1 to home both. + +Motion still enforces its own safety (idle / not on limits). The joint +number is range-checked against the machine's configured joint count in +task (emcJointHome(), taskintf.cc), which is where that count is known -- +the interpreter has no joint count in its state. +*/ +int Interp::convert_home_cycle(block_pointer block, + setup_pointer settings) +{ + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), + "Cannot home (G28.2) with cutter radius compensation on"); + + CHKS((!block->p_flag), + "G28.2 requires a P word: P-1 homes every joint, or P0, P1, ..." + " home one joint by its number"); + + CHKS((block->p_number != round_to_int(block->p_number)), + "P value for G28.2 must be a whole joint number, or P-1 to home" + " every joint"); + + int joint = round_to_int(block->p_number); + + CHKS((joint < -1), + "P value for G28.2 must be P-1 (every joint) or a joint number of" + " 0 or more"); + + if (joint < 0) { + HOME_CYCLE(); + } else { + HOME_CYCLE_JOINT(joint); + } + + // Homing re-establishes machine zero and, for an immediate home, + // rewrites the joint coordinate with no physical motion. Stop reading + // ahead here (INTERP_EXECUTE_FINISH, via execute_block) and, once the + // cycle has run, resync current_* from the machine in read_inputs() so + // a following G91 move or I/J/K arc centre is right. Same pattern as + // probing and tool change. + settings->home_flag = true; + return INTERP_OK; +} + int Interp::convert_home(int move, //!< G-code, must be G_28 or G_30 block_pointer block, //!< pointer to a block of RS274 instructions setup_pointer settings) //!< pointer to machine settings @@ -4350,6 +4437,8 @@ int Interp::convert_modal_0(int code, //!< G-code, must be from group 0 CHP(convert_home(code, block, settings)); } else if ((code == G_28_1) || (code == G_30_1)) { CHP(convert_savehome(code, block, settings)); + } else if (code == G_28_2) { + CHP(convert_home_cycle(block, settings)); } else if ((code == G_52) || (code == G_92)) { CHP(convert_axis_offsets(code, block, settings)); } else if ((code == G_5_3)||(code == G_6_3)) { // jjf diff --git a/src/emc/rs274ngc/interp_execute.cc b/src/emc/rs274ngc/interp_execute.cc index 2411aafc0cc..46eb9e1a873 100644 --- a/src/emc/rs274ngc/interp_execute.cc +++ b/src/emc/rs274ngc/interp_execute.cc @@ -199,8 +199,8 @@ Returned Value: int convert_speed convert_stop convert_tool_select - Otherwise, if the probe_flag in the settings is true, - or the input_flag is set to true this returns + Otherwise, if the probe_flag, input_flag, toolchange_flag or home_flag + in the settings is true, this returns INTERP_EXECUTE_FINISH. Otherwise, it returns INTERP_OK. @@ -327,6 +327,9 @@ int Interp::execute_block(block_pointer block, //!< pointer to a block of RS27 if (settings->kinsSwitch_flag) return (INTERP_EXECUTE_FINISH); + if (settings->home_flag) + return (INTERP_EXECUTE_FINISH); + // All changes to settings are complete write_canon_state_tag(block, settings); return INTERP_OK; diff --git a/src/emc/rs274ngc/interp_internal.hh b/src/emc/rs274ngc/interp_internal.hh index b58d40e525f..b833a94e778 100644 --- a/src/emc/rs274ngc/interp_internal.hh +++ b/src/emc/rs274ngc/interp_internal.hh @@ -231,6 +231,7 @@ enum GCodes G_21 = 210, G_28 = 280, G_28_1 = 281, + G_28_2 = 282, /* G-code homing cycle (home one/all joints) */ G_30 = 300, G_30_1 = 301, G_33 = 330, @@ -760,6 +761,7 @@ struct setup bool kinsSwitch_flag; // flag indicating waiting for kinematics switch done int kins_type; // kinematics selected by G12.1/G13.1 bool toolchange_flag; // flag indicating we just had a tool change + bool home_flag; // flag indicating a G28.2 homing cycle just ran int input_index; // channel queried bool input_digital; // input queried was digital (false=analog) bool cutter_comp_firstmove; // this is the first comp move diff --git a/src/emc/rs274ngc/interp_setup.cc b/src/emc/rs274ngc/interp_setup.cc index 3b5445795c3..d0611d9da9f 100644 --- a/src/emc/rs274ngc/interp_setup.cc +++ b/src/emc/rs274ngc/interp_setup.cc @@ -119,6 +119,7 @@ setup::setup() : kinsSwitch_flag(0), kins_type(0), toolchange_flag(0), + home_flag(0), input_index(0), input_digital(0), cutter_comp_firstmove(0), diff --git a/src/emc/rs274ngc/rs274ngc_interp.hh b/src/emc/rs274ngc/rs274ngc_interp.hh index 46491d6a627..cfbc3f34720 100644 --- a/src/emc/rs274ngc/rs274ngc_interp.hh +++ b/src/emc/rs274ngc/rs274ngc_interp.hh @@ -327,6 +327,8 @@ public: setup_pointer settings); int convert_savehome(int move, block_pointer block, setup_pointer settings); + int convert_home_cycle(block_pointer block, // G28.2 + setup_pointer settings); int convert_length_units(int g_code, setup_pointer settings); int convert_m(block_pointer block, setup_pointer settings); int convert_modal_0(int code, block_pointer block, diff --git a/src/emc/rs274ngc/rs274ngc_pre.cc b/src/emc/rs274ngc/rs274ngc_pre.cc index 4163dc99a7c..3b4d2a40f57 100644 --- a/src/emc/rs274ngc/rs274ngc_pre.cc +++ b/src/emc/rs274ngc/rs274ngc_pre.cc @@ -1193,6 +1193,7 @@ int Interp::init() //_setup.plane set in Interp::synch _setup.probe_flag = false; _setup.toolchange_flag = false; + _setup.home_flag = false; _setup.input_flag = false; _setup.kinsSwitch_flag = false; _setup.input_index = -1; @@ -1459,6 +1460,19 @@ int Interp::read_inputs(setup_pointer settings) load_tool_table(); settings->toolchange_flag = false; } + if (settings->home_flag) { + // A G28.2 homing cycle re-establishes machine zero and, for an + // immediate (index/switchless) home, rewrites the joint coordinate + // even when nothing physically moved. Pull the interpreter's model + // of the current position back in line with the machine so a + // following G91 move or an I/J/K arc centre is computed from where + // the tool actually is, not from the pre-home point. Same mechanism + // as the tool-change resync above, without the tool-table reload. + CHKS((GET_EXTERNAL_QUEUE_EMPTY() == 0), + _("Queue is not empty after homing")); + refresh_actual_position(&_setup); + settings->home_flag = false; + } // always track toolchanger-fault and toolchanger-reason codes settings->parameters[5600] = GET_EXTERNAL_TC_FAULT(); settings->parameters[5601] = GET_EXTERNAL_TC_REASON(); @@ -2692,6 +2706,7 @@ int Interp::on_abort(int reason, const char *message) // if set, may cause a "Queue is not empty after tool change" error _setup.toolchange_flag = false; _setup.probe_flag = false; + _setup.home_flag = false; _setup.input_flag = false; _setup.kinsSwitch_flag = false; diff --git a/src/emc/sai/saicanon.cc b/src/emc/sai/saicanon.cc index 87c77b36fe7..fd946d5c3af 100644 --- a/src/emc/sai/saicanon.cc +++ b/src/emc/sai/saicanon.cc @@ -112,6 +112,9 @@ void SET_XY_ROTATION(double t) { ECHO_WITH_ARGS("%.4f", t); } +void HOME_CYCLE(void) { ECHO_WITH_ARGS(""); } +void HOME_CYCLE_JOINT(int joint) { ECHO_WITH_ARGS("%d", joint); } + void SET_G5X_OFFSET(int index, double x, double y, double z, double a, double b, double c, diff --git a/src/emc/task/emccanon.cc b/src/emc/task/emccanon.cc index 2c7e8ea5fa0..bb6c2deafb3 100644 --- a/src/emc/task/emccanon.cc +++ b/src/emc/task/emccanon.cc @@ -478,6 +478,31 @@ void SET_XY_ROTATION(double t) { canon.xy_rotation = t; } + +void HOME_CYCLE(void) +{ + // STRAIGHT_FEED/STRAIGHT_TRAVERSE buffer points into chained_points for + // arc-blend lookahead and only append to interp_list on flush (see + // see_segment()/flush_segments()). Without flushing here first, any + // motion queued just before this G28.2 would get silently reordered to + // execute AFTER the home instead of before it. + flush_segments(); + auto msg = std::make_unique(); + msg->joint = -1; // -1 = all joints (HOME_SEQUENCE order) + interp_list.append(std::move(msg)); +} + +/* G28.2 Pn -- home a single joint. joint is the interp's already-validated + * (non-negative) P value; task range-checks it against the machine's + * configured joint count in emcJointHome() (taskintf.cc). */ +void HOME_CYCLE_JOINT(int joint) +{ + flush_segments(); // see HOME_CYCLE + auto msg = std::make_unique(); + msg->joint = joint; + interp_list.append(std::move(msg)); +} + void SET_G5X_OFFSET(int index, double x, double y, double z, double a, double b, double c, diff --git a/src/emc/task/emctaskmain.cc b/src/emc/task/emctaskmain.cc index 5e41127a5a2..febd808abc6 100644 --- a/src/emc/task/emctaskmain.cc +++ b/src/emc/task/emctaskmain.cc @@ -392,6 +392,39 @@ static EMC_TRAJ_SET_SPINDLESYNC *emcTrajSetSpindlesyncMsg; //static EMC_MOTION_SET_AOUT *emcMotionSetAoutMsg; //static EMC_MOTION_SET_DOUT *emcMotionSetDoutMsg; +// G28.2 sequencing state (see EMC_TASK_EXEC::WAITING_FOR_HOMING): +// homing only actually runs while motion is in FREE mode (control.c only +// calls do_homing() there), so a queued home triggered from a program or +// MDI while running in TELEOP/COORD would otherwise silently stall. We dip +// motion into FREE for the duration and restore whatever mode it was in +// before, invisibly to the task-level MDI/AUTO/MANUAL state +// (mdiOrAuto is untouched) -- same principle as multichannel-DESIGN.txt's +// "channel sessions do NOT flip the global teleop mode" for the analogous +// per-channel-homing problem. +static int homingWaitJoint = -1; // joint (-1 = all) we're waiting on +static bool homingWaiting = false; // true while EMC_TASK_EXEC::WAITING_FOR_HOMING is active +static bool homingStarted = false; // true once we've observed .homing go true at least once +static double homingIssueTime = 0.0; // etime() when issued, for the start-timeout below +// Some motion-side guards (e.g. "must be in joint mode to home", +// motion.homing-inhibit, already-homing) reject with reportError() and a +// bare return, without setting commandStatus to a failure -- so a rejected +// home can look identical to an accepted one at the retval/NML level. Give +// it this long to actually start (.homing go true) before treating it as +// rejected; once started, there is no further timeout (real homing cycles +// vary widely in duration, same as a GUI just watching .homing/.homed). +static const double HOMING_START_TIMEOUT = 2.0; +static EMC_TRAJ_MODE homingPriorMode = EMC_TRAJ_MODE::FREE; // mode to restore on success +// True only while emcTaskExecute() is issuing a command taken off the +// interp_list. emcTaskIssueCommand() serves both that queued path and the +// immediate commands emcTaskPlan() sends straight through (the GUI's Home +// button, halui, linuxcncrsh), and only the queued path is followed by +// emcTaskCheckPostconditions() -- so only the queued path can ever reach +// EMC_TASK_EXEC::WAITING_FOR_HOMING to undo the FREE-mode dip and restore the +// previous trajectory mode. Dipping on an immediate command would therefore +// strand the machine in FREE for good. Gate the sequencing on this flag so +// an immediate home keeps its original pass-through behaviour. +static bool issuingQueuedCommand = false; + static EMC_SPINDLE_SPEED *spindle_speed_msg; static EMC_SPINDLE_ORIENT *spindle_orient_msg; static EMC_SPINDLE_WAIT_ORIENT_COMPLETE *wait_spindle_orient_complete_msg; @@ -1611,6 +1644,12 @@ static EMC_TASK_EXEC emcTaskCheckPreconditions(NMLmsg * cmd) return EMC_TASK_EXEC::WAITING_FOR_MOTION_AND_IO; break; + case EMC_JOINT_HOME_TYPE: // G28.2: program-order homing + // drain prior motion before homing; without this case a queued home + // hit default -> EMC_TASK_EXEC::ERROR and was silently dropped + // (never reached motion). + return EMC_TASK_EXEC::WAITING_FOR_MOTION; + default: // unrecognized command if (emc_debug & EMC_DEBUG_TASK_ISSUE) { @@ -1675,10 +1714,54 @@ static int emcTaskIssueCommand(NMLmsg * cmd) case EMC_JOINT_HOME_TYPE: home_msg = reinterpret_cast(cmd); - retval = emcJointHome(home_msg->joint); + homingWaiting = false; // default; set true below only if we actually issue a home + { + const int target_joint = home_msg->joint; + if (!issuingQueuedCommand) { + // Immediate command (the GUI Home button, halui, linuxcncrsh): + // pass it straight through, exactly as before this sequencing + // existed. Nothing calls emcTaskCheckPostconditions() for an + // immediate command, so a mode dip taken here would never be + // undone. See issuingQueuedCommand. + retval = emcJointHome(target_joint); + break; + } + // do_homing() (control.c) only advances while motion is in FREE + // mode, so a queued home while running in TELEOP/COORD would + // otherwise silently stall. Dip into FREE for the duration and + // restore whatever mode was active once homing finishes (or is + // found to have been rejected), invisibly to the task-level + // MDI/AUTO/MANUAL state. + homingPriorMode = emcStatus->motion.traj.mode; + if (homingPriorMode != EMC_TRAJ_MODE::FREE) { + emcTrajSetMode(EMC_TRAJ_MODE::FREE); + } + homingWaitJoint = target_joint; + homingStarted = false; + homingIssueTime = etime(); + homingWaiting = true; + retval = emcJointHome(target_joint); + if (retval != 0) { + // emcJointHome() rejected the request outright (e.g. an + // invalid joint number) -- homing will never start, so the + // WAITING_FOR_HOMING poll below would never run to undo the + // FREE-mode dip either. Undo it here instead, or traj.mode + // (and therefore task.mode, which determineMode() derives + // from it) stays stuck at FREE/MANUAL until the operator + // manually cycles mode again. + homingWaiting = false; + if (homingPriorMode != EMC_TRAJ_MODE::FREE) { + emcTrajSetMode(homingPriorMode); + } + } + } break; case EMC_JOINT_UNHOME_TYPE: + // No sequencing here: with G28.3 dropped from this PR an unhome can + // only arrive as an immediate command (the GUI, halui, linuxcncrsh), + // never from the interpreter, so there is no queued path whose + // trajectory mode would need dipping and restoring. unhome_msg = reinterpret_cast(cmd); retval = emcJointUnhome(unhome_msg->joint); break; @@ -2538,6 +2621,13 @@ static EMC_TASK_EXEC emcTaskCheckPostconditions(NMLmsg * cmd) return EMC_TASK_EXEC::WAITING_FOR_SPINDLE_ORIENTED; break; + case EMC_JOINT_HOME_TYPE: + // homingWaiting is false when the command was passed straight through + // as an immediate command (see issuingQueuedCommand) -- the sequencing + // did not run, so there is nothing to wait for. + return homingWaiting ? EMC_TASK_EXEC::WAITING_FOR_HOMING : EMC_TASK_EXEC::DONE; + break; + case EMC_TRAJ_DELAY_TYPE: case EMC_AUX_INPUT_WAIT_TYPE: return EMC_TASK_EXEC::WAITING_FOR_DELAY; @@ -2671,7 +2761,13 @@ static int emcTaskExecute(void) } } else { // have an outstanding command - if (0 != emcTaskIssueCommand(emcTaskCommand.get())) { + // This is the one emcTaskIssueCommand() call site followed by + // emcTaskCheckPostconditions(), i.e. the only one whose commands + // can reach a WAITING_FOR_* state. See issuingQueuedCommand. + issuingQueuedCommand = true; + const int issue_retval = emcTaskIssueCommand(emcTaskCommand.get()); + issuingQueuedCommand = false; + if (0 != issue_retval) { emcStatus->task.execState = EMC_TASK_EXEC::ERROR; retval = -1; } else { @@ -2784,6 +2880,155 @@ static int emcTaskExecute(void) break; } + case EMC_TASK_EXEC::WAITING_FOR_HOMING: + // G28.2 sequencing: wait for the joint home issued in + // emcTaskIssueCommand to actually run to completion (do_homing() only + // advances while motion is in FREE, which is why we dipped into it + // there), then restore the prior trajectory mode. See the + // homingWaiting block of static state near the top of this file. + STEPPING_CHECK(); + { + bool any_homing = false; + bool all_target_homed = true; // success criterion + int lo = (homingWaitJoint < 0) ? 0 : homingWaitJoint; + int hi = (homingWaitJoint < 0) ? (emcStatus->motion.traj.joints - 1) : homingWaitJoint; + for (int j = lo; j <= hi; j++) { + if (emcStatus->motion.joint[j].homing) { + any_homing = true; + } + if (!emcStatus->motion.joint[j].homed) { + all_target_homed = false; + } + } + + bool success; + // "Is homing still running?" must come from motion's aggregate + // homing_active, not from OR-ing the per-joint .homing flags. On a + // machine that homes in several HOME_SEQUENCE groups the sequence + // machine finishes one group and can spend a cycle or more before + // the next group raises .homing, so there is a window in which + // every joint reads .homing == false while the machine is still + // homing. Task samples far coarser than the servo cycle, so it can + // land in that window, conclude homing stopped, and score a + // perfectly good home-all as "did not complete". motion/homing.c + // documents the same deassertion lag in its own words ("The homing + // status variable turns false before homing_active state turns + // false") and guards against it internally for the same reason. + // + // Single-joint Pn and single-sequence machines never hit the gap, + // which is why this only shows up on a multi-sequence home-all. + // The per-joint OR is kept as a belt-and-braces term: it can only + // extend the "still running" window, never shorten it. + const bool homing_running = emcStatus->motion.homing_active || any_homing; + if (homing_running) { + homingStarted = true; + break; // still running; no timeout once started (see HOMING_START_TIMEOUT comment) + } + if (!homingStarted) { + // Never observed homing go active: motion silently rejected + // it (a guard like "must be in joint mode", or + // motion.homing-inhibit, reports an operator error but does + // not fail the NML command -- see emcJointHome's caller), + // or this is the same task cycle it was issued in. Give it + // HOMING_START_TIMEOUT before concluding it was rejected. + if (etime() - homingIssueTime < HOMING_START_TIMEOUT) { + break; + } + emcOperatorError("G28.2 home did not start -- check machine mode, " + "motion.homing-inhibit, and whether a homing " + "cycle is already in progress"); + emcStatus->task.execState = EMC_TASK_EXEC::ERROR; + emcTaskEager = 1; + homingWaiting = false; + // Nothing physically moved, so it's safe to restore the + // mode immediately instead of leaving the machine parked + // in FREE. + if (homingPriorMode != EMC_TRAJ_MODE::FREE) { + emcTrajSetMode(homingPriorMode); + } + break; + } + // It ran and has now stopped; did it reach the expected end state? + success = all_target_homed; + + homingWaiting = false; + emcTaskEager = 1; + + // Is it legal to hand the machine back to the coordinated mode it + // was in before the FREE dip? Motion refuses to (re-)enter TELEOP + // or COORD on non-identity kinematics unless *every* joint is + // homed -- switch_to_teleop_mode() (motion.c) and the EMCMOT_COORD + // case (command.c) both gate on + // "kinType != KINEMATICS_IDENTITY && !get_allhomed()". + // + // Restoring unconditionally means motion rejects the request, task + // still reports DONE, and the machine is stranded in FREE with the + // GUI's mode controls greyed out -- recoverable only by cycling the + // controller (PR #4172: Sigma1912's "g28.3 p0" on a gantry gave + // "all joints must be homed before going into coordinated mode", + // then needed F2). Mirror motion's own condition here and abort + // cleanly instead of wedging. + // + // With G28.3 gone from this PR the only command that reaches here + // is a home, which ends with its joints referenced, so on + // non-identity kinematics -- where a coordinated prior mode already + // implies the machine was fully homed -- this gate now guards a + // state no G-code can produce. It is kept because the alternative + // failure is silent: motion defers the COORD/TELEOP transition to + // its controller cycle, so a refused restore leaves no trace task + // could notice, and the operator gets a dead UI with no message. + // + // Mirroring the condition rather than issuing the restore and + // checking whether it took is deliberate: the COORD/TELEOP + // transition is deferred to the controller cycle (see + // "defer transition to controller cycle" in command.c), so an + // immediate read-back would race exactly the way the old + // .homing-based completion test did. + // + // Read the kinematics type from status, not from this file's + // static emcmotConfig: that copy is filled in once just before + // the main loop and never refreshed, while taskintf.cc re-reads + // the motion config whenever config_num changes and republishes + // it as traj.kinematics_type. The two agree today -- kinType is + // written exactly once, in init_comm_buffers() (motion.c), and a + // runtime switchkins change does not alter it (switchkins answers + // KINEMATICS_BOTH for every selectable type) -- so this is a + // matter of reading the copy that is maintained, not of avoiding + // a divergence that exists now. + const bool restore_ok = (homingPriorMode == EMC_TRAJ_MODE::FREE) + || (emcStatus->motion.traj.kinematics_type + == KINEMATICS_IDENTITY) + || all_homed(); + + if (success && !restore_ok) { + // The command itself did what was asked, but it left the + // machine partially referenced and motion will not take + // TELEOP/COORD back in that state. Stay in FREE and fail the + // program rather than report DONE and strand the operator. + emcOperatorError(_("G28.2 home succeeded but left the machine not " + "fully homed -- staying in joint mode, as " + "non-identity kinematics cannot re-enter " + "coordinated motion until every joint is homed")); + emcStatus->task.execState = EMC_TASK_EXEC::ERROR; + } else if (success) { + emcStatus->task.execState = EMC_TASK_EXEC::DONE; + if (homingPriorMode != EMC_TRAJ_MODE::FREE) { + emcTrajSetMode(homingPriorMode); + } + } else { + // Homing stopped without reaching the target state (aborted, + // faulted, ESTOP mid-cycle, ...) -- abort the program rather + // than let it continue unreferenced. Deliberately NOT restored to + // homingPriorMode here: an unhomed/partially-homed machine may + // not legally re-enter TELEOP/COORD, and FREE is the safe + // state to leave it in for an operator to intervene from. + emcOperatorError("G28.2 home did not complete for joint %s", + homingWaitJoint < 0 ? "ALL" : "requested"); + emcStatus->task.execState = EMC_TASK_EXEC::ERROR; + } + } + break; + case EMC_TASK_EXEC::WAITING_FOR_DELAY: STEPPING_CHECK(); // check if delay has passed diff --git a/src/emc/task/taskintf.cc b/src/emc/task/taskintf.cc index 585a22af698..9b6fab24c62 100644 --- a/src/emc/task/taskintf.cc +++ b/src/emc/task/taskintf.cc @@ -796,8 +796,35 @@ int emcJointOverrideLimits(int joint) int emcJointHome(int joint) { - if (joint < -1 || joint >= EMCMOT_MAX_JOINTS) { - return 0; + // Range-check against the machine's *configured* joint count, not against + // EMCMOT_MAX_JOINTS: joints[] is sized for the compile-time maximum, so a + // joint number between the configured count and that maximum passes a + // EMCMOT_MAX_JOINTS check, reaches motion, and is then silently dropped -- + // do_home_joint() has no joint that could start homing, and says nothing. + // Task, waiting for a homing cycle that will never begin, sat out its + // start timeout with the machine parked in the FREE-mode dip and then + // reported the generic "home did not start" (PR #4172: Sigma1912's + // "g28.2 p5" on a 5-joint machine -- two seconds of the GUI jogging in + // joint mode, then a message naming nothing). + // + // Checked here rather than in the interpreter because the joint count is + // not part of interpreter state, and here it covers every caller (G28.2 + // Pn, the GUI Home button, halui, linuxcncrsh) instead of just G-code. + // + // Reported with emcOperatorError(), not rcs_print(): an operator typing + // "G28.2 P5" needs to see it, and only the error channel reaches the GUI. + if (joint < -1 || joint >= TrajConfig.Joints) { + // Report only what the person reading it can act on: the joints this + // machine actually has, and the spelling that homes them all. "-1 for + // all" is the NML convention the GUI buttons, halui and linuxcncrsh + // use, and G28.2 P-1 is how G-code asks for the same thing, so it is + // safe to name here -- unlike -2 (volatile unhome), which no caller of + // this function can use and which convert_home_cycle() refuses + // (PR #4172, Sigma1912). + emcOperatorError("Cannot home invalid joint %d (this machine has " + "joints 0..%d; P-1 homes every joint)", + joint, TrajConfig.Joints - 1); + return EMCMOT_COMM_ERROR_COMMAND; } emcmotCommand.command = EMCMOT_JOINT_HOME; @@ -808,8 +835,20 @@ int emcJointHome(int joint) int emcJointUnhome(int joint) { - if (joint < -2 || joint >= EMCMOT_MAX_JOINTS) { - return 0; + // See emcJointHome: bound by the configured joint count, and report it + // where the operator can see it. Motion does range-check the unhome + // path, but as "jno > all_joints", so the first unconfigured joint + // number slips through to an unrelated complaint about extra joints -- + // and because an unconfigured joint reads as not homed, task's + // synchronous "no joint in range is still homed" test would then score + // that refusal as a *successful* unhome. + if (joint < -2 || joint >= TrajConfig.Joints) { + // See the note in emcJointHome() above on why the internal + // sentinels are not offered to the operator here. + emcOperatorError("Cannot unhome invalid joint %d (this machine " + "has joints 0..%d)", + joint, TrajConfig.Joints - 1); + return EMCMOT_COMM_ERROR_COMMAND; } emcmotCommand.command = EMCMOT_JOINT_UNHOME; @@ -2172,6 +2211,7 @@ int emcMotionUpdate(EMC_MOTION_STAT * stat) } stat->jogging_active = emcmotStatus.jogging_active; + stat->homing_active = emcmotStatus.homing_active; stat->numExtraJoints = emcmotStatus.numExtraJoints; // set the status flag diff --git a/tests/interp/g28.2/flush-order/checkresult b/tests/interp/g28.2/flush-order/checkresult new file mode 100755 index 00000000000..24dc9aa53e3 --- /dev/null +++ b/tests/interp/g28.2/flush-order/checkresult @@ -0,0 +1,2 @@ +#!/bin/sh +exit 0 # test failure is indicated by test.sh exit value diff --git a/tests/interp/g28.2/flush-order/sim.tbl b/tests/interp/g28.2/flush-order/sim.tbl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/interp/g28.2/flush-order/test-ui.py b/tests/interp/g28.2/flush-order/test-ui.py new file mode 100755 index 00000000000..5666c1534af --- /dev/null +++ b/tests/interp/g28.2/flush-order/test-ui.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 + +""" +Regression test for the flush_segments() ordering fix in HOME_CYCLE()/ +HOME_CYCLE()/HOME_CYCLE_JOINT() +(emccanon.cc). + +STRAIGHT_FEED/STRAIGHT_TRAVERSE buffer points into chained_points for +arc-blend lookahead and only reach interp_list on a flush (see +see_segment()/flush_segments()). Without an explicit flush_segments() call +at the start of the home canon functions, a queued move immediately +before a G28.2 could silently get reordered to run *after* the home +instead of before it. + +test.ngc queues "G1 X2" (a multi-cycle move, slow enough to poll mid-flight) +immediately followed by "G28.2 P0". This script polls position and homed +state throughout the run and asserts joint 0 never reports homed=1 before +its position has actually reached the X2 target -- if the move were +silently deferred to after the home (the bug), homed would flip true while +position was still near its starting point. +""" + +import linuxcnc +import hal + +import sys +import time + +h = hal.component("python-ui") +h.ready() + +c = linuxcnc.command() +s = linuxcnc.stat() + + +def poll(): + s.poll() + + +def fail(msg): + print("FAIL: " + msg) + sys.exit(1) + + +c.state(linuxcnc.STATE_ESTOP_RESET) +c.state(linuxcnc.STATE_ON) +c.mode(linuxcnc.MODE_AUTO) +time.sleep(0.2) + +c.program_open("test.ngc") +time.sleep(0.2) +c.auto(linuxcnc.AUTO_RUN, 0) + +saw_position_near_target = False +homed_while_short_of_target = None +t0 = time.time() +while time.time() - t0 < 10.0: + poll() + if s.position[0] > 1.9: + saw_position_near_target = True + if s.homed[0] and not saw_position_near_target: + homed_while_short_of_target = s.position[0] + break + if s.exec_state == linuxcnc.EXEC_DONE and s.interp_state == linuxcnc.INTERP_IDLE: + break + time.sleep(0.001) + +if homed_while_short_of_target is not None: + fail("joint 0 reported homed while X was still at {} (target 2.0) -- " + "the queued move was reordered to run after G28.2 P0".format(homed_while_short_of_target)) + +if not saw_position_near_target: + fail("X never reached its target -- move did not run at all") + +t1 = time.time() +while time.time() - t1 < 5.0: + poll() + if s.exec_state == linuxcnc.EXEC_DONE and s.interp_state == linuxcnc.INTERP_IDLE: + break + time.sleep(0.01) + +if not s.homed[0]: + fail("joint 0 never ended up homed") + +print("PASS: the queued move completed before G28.2 P0 homed the joint") +print("done! it all worked") +sys.exit(0) diff --git a/tests/interp/g28.2/flush-order/test.ini b/tests/interp/g28.2/flush-order/test.ini new file mode 100644 index 00000000000..38b6a739acb --- /dev/null +++ b/tests/interp/g28.2/flush-order/test.ini @@ -0,0 +1,99 @@ +[EMC] +DEBUG = 0 +VERSION = 1.1 + +[DISPLAY] +DISPLAY = ./test-ui.py + +[TASK] +TASK = milltask +CYCLE_TIME = 0.001 + +[RS274NGC] +PARAMETER_FILE = sim.var + +[EMCMOT] +EMCMOT = motmod +COMM_TIMEOUT = 4.0 +BASE_PERIOD = 0 +SERVO_PERIOD = 1000000 + +[HAL] +HALUI = halui +HALFILE = LIB:core_sim.hal + +[TRAJ] +NO_FORCE_HOMING = 1 +AXES = 3 +COORDINATES = X Y Z +HOME = 0 0 0 +LINEAR_UNITS = inch +ANGULAR_UNITS = degree +DEFAULT_LINEAR_VELOCITY = 1.2 +MAX_LINEAR_VELOCITY = 4 + +[KINS] +JOINTS = 3 +KINEMATICS = trivkins + +[AXIS_X] +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +MIN_LIMIT = -40.0 +MAX_LIMIT = 40.0 + +[AXIS_Y] +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +MIN_LIMIT = -40.0 +MAX_LIMIT = 40.0 + +[AXIS_Z] +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +MIN_LIMIT = -4.0 +MAX_LIMIT = 4.0 + +[JOINT_0] +TYPE = LINEAR +HOME = 0.000 +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +BACKLASH = 0.000 +INPUT_SCALE = 4000 +OUTPUT_SCALE = 1.000 +MIN_LIMIT = -40.0 +MAX_LIMIT = 40.0 +FERROR = 0.050 +MIN_FERROR = 0.010 + +[JOINT_1] +TYPE = LINEAR +HOME = 0.000 +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +BACKLASH = 0.000 +INPUT_SCALE = 4000 +OUTPUT_SCALE = 1.000 +MIN_LIMIT = -40.0 +MAX_LIMIT = 40.0 +FERROR = 0.050 +MIN_FERROR = 0.010 + +[JOINT_2] +TYPE = LINEAR +HOME = 0.0 +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +BACKLASH = 0.000 +INPUT_SCALE = 4000 +OUTPUT_SCALE = 1.000 +MIN_LIMIT = -4.0 +MAX_LIMIT = 4.0 +FERROR = 0.050 +MIN_FERROR = 0.010 + +[EMCIO] +TOOL_CHANGE_QUILL_UP = 1 +RANDOM_TOOLCHANGER = 0 +TOOL_TABLE = sim.tbl diff --git a/tests/interp/g28.2/flush-order/test.ngc b/tests/interp/g28.2/flush-order/test.ngc new file mode 100644 index 00000000000..0682100733b --- /dev/null +++ b/tests/interp/g28.2/flush-order/test.ngc @@ -0,0 +1,6 @@ +G20 +G94 +F60 +G1 X2 +G28.2 P0 +M2 diff --git a/tests/interp/g28.2/flush-order/test.sh b/tests/interp/g28.2/flush-order/test.sh new file mode 100755 index 00000000000..a16f6fa8522 --- /dev/null +++ b/tests/interp/g28.2/flush-order/test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec linuxcnc -r test.ini diff --git a/tests/interp/g28.2/home-all-sequence/checkresult b/tests/interp/g28.2/home-all-sequence/checkresult new file mode 100755 index 00000000000..24dc9aa53e3 --- /dev/null +++ b/tests/interp/g28.2/home-all-sequence/checkresult @@ -0,0 +1,2 @@ +#!/bin/sh +exit 0 # test failure is indicated by test.sh exit value diff --git a/tests/interp/g28.2/home-all-sequence/sim.tbl b/tests/interp/g28.2/home-all-sequence/sim.tbl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/interp/g28.2/home-all-sequence/test-ui.py b/tests/interp/g28.2/home-all-sequence/test-ui.py new file mode 100755 index 00000000000..b39441e6bb1 --- /dev/null +++ b/tests/interp/g28.2/home-all-sequence/test-ui.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 + +""" +G28.2 P-1 (home every joint) on a config that sets HOME_SEQUENCE. + +The other tests here only exercise G28.2 Pn on a single joint. P-1 goes +through a different motion path -- do_home_joint(-1) -> do_home_all() -> +the HOME_SEQUENCE state machine -- and needs HOME_SEQUENCE set, exactly +like the GUI Home All button. This config has three single-joint sequence +groups (0, 1, 2). + +Checks: + 1. G28.2 P-1 issued from MDI homes every joint and completes without a + task error. + 2. The task-level mode is not disturbed by the FREE-mode dip. + 3. Homing walks the sequence groups in order (joint 0, then 1, then 2) -- + and the completion poll does not trip on the gap between groups, where + all per-joint .homing flags read false while the machine is still + homing. A false "did not complete" would abort the MDI command and + leave the machine unhomed in FREE. + 4. G28.2 P-1 from a program (AUTO) works the same way. + 5. A bare G28.2 (no P word) is refused outright, homes nothing, and does + not disturb the machine -- homing every joint has to be asked for. +""" + +import linuxcnc +import hal + +import sys +import time + +h = hal.component("python-ui") +h.ready() + +c = linuxcnc.command() +s = linuxcnc.stat() +e = linuxcnc.error_channel() + + +def poll(): + s.poll() + + +def fail(msg): + print("FAIL: " + msg) + sys.exit(1) + + +def drain_errors(): + msgs = [] + while True: + err = e.poll() + if not err: + return msgs + msgs.append(err[1]) + + +def wait_idle(timeout=15.0): + t0 = time.time() + while time.time() - t0 < timeout: + poll() + if s.exec_state == linuxcnc.EXEC_DONE and s.interp_state == linuxcnc.INTERP_IDLE: + return True + time.sleep(0.01) + return False + + +def wait_homed(expected, timeout=15.0): + t0 = time.time() + while time.time() - t0 < timeout: + poll() + if list(s.homed[:3]) == expected: + return True + time.sleep(0.01) + return False + + +c.state(linuxcnc.STATE_ESTOP_RESET) +c.state(linuxcnc.STATE_ON) +poll() +if list(s.homed[:3]) != [0, 0, 0]: + fail("machine came up homed: {}".format(list(s.homed[:3]))) + +c.mode(linuxcnc.MODE_MDI) +time.sleep(0.2) +poll() +mode_before = s.task_mode +drain_errors() + +# --- 1: G28.2 P-1 from MDI homes everything -------------------------------- +c.mdi("G28.2 P-1") + +# Watch the homed flags flip on in sequence order while the cycle runs. +order = [] +t0 = time.time() +while time.time() - t0 < 15.0: + poll() + for j in range(3): + if s.homed[j] and j not in order: + order.append(j) + if list(s.homed[:3]) == [1, 1, 1]: + break + time.sleep(0.005) + +if list(s.homed[:3]) != [1, 1, 1]: + fail("G28.2 P-1 did not home all joints: homed={}".format(list(s.homed[:3]))) +if order != [0, 1, 2]: + fail("joints did not home in HOME_SEQUENCE order: saw {}".format(order)) + +if not wait_idle(): + fail("G28.2 P-1 (MDI) did not return to idle/DONE") + +errs = drain_errors() +if any("did not complete" in m or "did not start" in m for m in errs): + fail("G28.2 P-1 reported a spurious homing failure: {!r}".format(errs)) +poll() +if s.exec_state == linuxcnc.EXEC_ERROR: + fail("G28.2 P-1 left task in EXEC_ERROR (errors={!r})".format(errs)) +if s.task_mode != mode_before: + fail("task_mode changed across G28.2 P-1: {} -> {}".format(mode_before, s.task_mode)) +print("PASS: G28.2 P-1 from MDI homes every joint in sequence order, mode untouched") + +# --- 2: and again from a program ----------------------------------------- +c.mode(linuxcnc.MODE_MANUAL) +c.teleop_enable(0) # to joint mode, so the unhome is allowed +time.sleep(0.3) +c.unhome(-1) +if not wait_homed([0, 0, 0]): + fail("could not unhome for the AUTO re-test: {}".format(list(s.homed[:3]))) + +c.mode(linuxcnc.MODE_AUTO) +time.sleep(0.2) +c.program_open("test.ngc") +time.sleep(0.2) +drain_errors() +c.auto(linuxcnc.AUTO_RUN, 0) + +if not wait_homed([1, 1, 1]): + fail("G28.2 P-1 from a program did not home all joints: {}".format(list(s.homed[:3]))) +if not wait_idle(): + fail("program with G28.2 P-1 did not finish") +errs = drain_errors() +if errs: + fail("program with G28.2 P-1 raised errors: {!r}".format(errs)) +print("PASS: G28.2 P-1 from a program homes every joint") + +# --- 3: a bare G28.2 homes nothing ---------------------------------------- +# Homing every joint must be asked for with P-1. A G28.2 with no P word is +# refused by the interpreter, before any homing cycle or FREE-mode dip, so a +# truncated or mistyped line cannot start the whole machine homing. +c.mode(linuxcnc.MODE_MANUAL) +c.teleop_enable(0) +time.sleep(0.3) +c.unhome(-1) +if not wait_homed([0, 0, 0]): + fail("could not unhome for the bare-G28.2 check: {}".format(list(s.homed[:3]))) + +c.mode(linuxcnc.MODE_MDI) +time.sleep(0.2) +drain_errors() +c.mdi("G28.2") +wait_idle() +time.sleep(0.5) +poll() +msgs = drain_errors() +if not msgs: + fail("bare G28.2 was accepted silently") +joined = " ".join(msgs) +if "P word" not in joined: + fail("bare G28.2 gave an unexpected error: {!r}".format(msgs[0][:120])) +if "P-1" not in joined: + fail("bare G28.2 error does not name P-1: {!r}".format(msgs[0][:120])) +if list(s.homed[:3]) != [0, 0, 0]: + fail("bare G28.2 homed something: {}".format(list(s.homed[:3]))) +print("PASS: bare G28.2 is refused, names P-1, and homes nothing") + +print("done! it all worked") +sys.exit(0) diff --git a/tests/interp/g28.2/home-all-sequence/test.ini b/tests/interp/g28.2/home-all-sequence/test.ini new file mode 100644 index 00000000000..b55ba992d9b --- /dev/null +++ b/tests/interp/g28.2/home-all-sequence/test.ini @@ -0,0 +1,109 @@ +[EMC] +DEBUG = 0 +VERSION = 1.1 + +[DISPLAY] +DISPLAY = ./test-ui.py + +[TASK] +TASK = milltask +CYCLE_TIME = 0.001 + +[RS274NGC] +PARAMETER_FILE = sim.var + +[EMCMOT] +EMCMOT = motmod +COMM_TIMEOUT = 4.0 +BASE_PERIOD = 0 +SERVO_PERIOD = 1000000 + +[HAL] +HALUI = halui +HALFILE = LIB:core_sim.hal + +[TRAJ] +AXES = 3 +COORDINATES = X Y Z +HOME = 0 0 0 +# so the test can issue the G28.2 P-1 as a genuine first-home from MDI / +# a program on an unreferenced machine (the unattended-power-up case), +# not only as a re-home after the GUI has already homed it. +NO_FORCE_HOMING = 1 +LINEAR_UNITS = inch +ANGULAR_UNITS = degree +DEFAULT_LINEAR_VELOCITY = 1.2 +MAX_LINEAR_VELOCITY = 4 + +[KINS] +JOINTS = 3 +KINEMATICS = trivkins + +[AXIS_X] +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +MIN_LIMIT = -40.0 +MAX_LIMIT = 40.0 + +[AXIS_Y] +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +MIN_LIMIT = -40.0 +MAX_LIMIT = 40.0 + +[AXIS_Z] +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +MIN_LIMIT = -4.0 +MAX_LIMIT = 4.0 + +# Three distinct HOME_SEQUENCE groups: G28.2 P-1 has to walk all of +# them, and the completion test has to stay "still homing" across the two +# gaps between groups (where every joint's per-joint .homing flag is +# momentarily false -- see the homing_active term in emctaskmain.cc). +[JOINT_0] +TYPE = LINEAR +HOME = 0.000 +HOME_SEQUENCE = 0 +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +BACKLASH = 0.000 +INPUT_SCALE = 4000 +OUTPUT_SCALE = 1.000 +MIN_LIMIT = -40.0 +MAX_LIMIT = 40.0 +FERROR = 0.050 +MIN_FERROR = 0.010 + +[JOINT_1] +TYPE = LINEAR +HOME = 0.000 +HOME_SEQUENCE = 1 +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +BACKLASH = 0.000 +INPUT_SCALE = 4000 +OUTPUT_SCALE = 1.000 +MIN_LIMIT = -40.0 +MAX_LIMIT = 40.0 +FERROR = 0.050 +MIN_FERROR = 0.010 + +[JOINT_2] +TYPE = LINEAR +HOME = 0.0 +HOME_SEQUENCE = 2 +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +BACKLASH = 0.000 +INPUT_SCALE = 4000 +OUTPUT_SCALE = 1.000 +MIN_LIMIT = -4.0 +MAX_LIMIT = 4.0 +FERROR = 0.050 +MIN_FERROR = 0.010 + +[EMCIO] +TOOL_CHANGE_QUILL_UP = 1 +RANDOM_TOOLCHANGER = 0 +TOOL_TABLE = sim.tbl diff --git a/tests/interp/g28.2/home-all-sequence/test.ngc b/tests/interp/g28.2/home-all-sequence/test.ngc new file mode 100644 index 00000000000..be9b2ce1067 --- /dev/null +++ b/tests/interp/g28.2/home-all-sequence/test.ngc @@ -0,0 +1,3 @@ +(G28.2 P-1 from a program -- home every joint, in HOME_SEQUENCE order) +G28.2 P-1 +M2 diff --git a/tests/interp/g28.2/home-all-sequence/test.sh b/tests/interp/g28.2/home-all-sequence/test.sh new file mode 100755 index 00000000000..a16f6fa8522 --- /dev/null +++ b/tests/interp/g28.2/home-all-sequence/test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec linuxcnc -r test.ini diff --git a/tests/interp/g28.2/immediate-mode-guard/checkresult b/tests/interp/g28.2/immediate-mode-guard/checkresult new file mode 100755 index 00000000000..24dc9aa53e3 --- /dev/null +++ b/tests/interp/g28.2/immediate-mode-guard/checkresult @@ -0,0 +1,2 @@ +#!/bin/sh +exit 0 # test failure is indicated by test.sh exit value diff --git a/tests/interp/g28.2/immediate-mode-guard/sim.tbl b/tests/interp/g28.2/immediate-mode-guard/sim.tbl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/interp/g28.2/immediate-mode-guard/test-ui.py b/tests/interp/g28.2/immediate-mode-guard/test-ui.py new file mode 100755 index 00000000000..6a45e0b7c86 --- /dev/null +++ b/tests/interp/g28.2/immediate-mode-guard/test-ui.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 + +""" +Regression test for the trajectory-mode guards on *immediate* home / unhome +commands -- the ones the GUI Home/Unhome buttons, halui and linuxcncrsh send. +Immediate commands reach emcTaskIssueCommand() but are never followed by +emcTaskCheckPostconditions(), so the G28.2 queued-home sequencing +(EMC_TASK_EXEC::WAITING_FOR_HOMING, which dips motion into FREE and restores +the prior mode) never runs for them. They must therefore behave exactly as +they did before PR #4172. + +Two things are checked, neither visible from outside without provoking it +(motion does not change the trajectory mode by itself for a single-joint +home or unhome, so any mode change seen here came from task): + + 1. An immediate *unhome* from teleop is refused by motion ("must be in + joint mode or disabled to unhome"). Task must not dip into FREE ahead + of it and let it through, and must not touch the mode. + + 2. An immediate *home* from teleop is refused with "must be in joint mode + to home", and the mode is left alone. do_homing() only advances while + motion is in FREE (control.c), and manual mode on an all-homed machine + is TELEOP, so accepting the command here would set the homing state + machine up and then never run it -- a silently-dropped home (found in + PR #4172 review of command.c). Before the fix the relaxed + EMCMOT_JOINT_HOME guard ("... or idle") accepted it. + +Config is trivkins so the machine can sit in TELEOP without being fully +homed; the mode guard under test is kinematics-independent. +""" + +import linuxcnc +import hal + +import sys +import time + +h = hal.component("python-ui") +h.ready() + +c = linuxcnc.command() +s = linuxcnc.stat() +e = linuxcnc.error_channel() + + +def poll(): + s.poll() + + +def fail(msg): + print("FAIL: " + msg) + sys.exit(1) + + +def wait_homed(expected, timeout=10.0): + t0 = time.time() + while time.time() - t0 < timeout: + poll() + if list(s.homed[:3]) == expected: + return True + time.sleep(0.01) + return False + + +def drain_errors(): + msgs = [] + while True: + err = e.poll() + if not err: + return msgs + msgs.append(err[1]) + + +c.state(linuxcnc.STATE_ESTOP_RESET) +c.state(linuxcnc.STATE_ON) +c.mode(linuxcnc.MODE_MANUAL) +c.home(0) +c.home(1) +c.home(2) +if not wait_homed([1, 1, 1]): + fail("initial home-all did not home all joints: {}".format(list(s.homed[:3]))) + +# Get into a coordinated (teleop) mode -- what a stray FREE dip would destroy. +c.teleop_enable(1) +time.sleep(0.5) +poll() +mode_before = s.motion_mode +if mode_before != linuxcnc.TRAJ_MODE_TELEOP: + fail("setup did not reach teleop mode (motion_mode={})".format(mode_before)) +drain_errors() + +# 1. Immediate unhome from teleop. Motion refuses this by design; task must +# not dip into FREE first and thereby let it through. +c.unhome(0) +time.sleep(1.0) +poll() + +if list(s.homed[:3]) != [1, 1, 1]: + fail( + "immediate unhome from teleop went through (homed={}) -- task dipped " + "into FREE before issuing it, bypassing motion's \"must be in joint " + "mode or disabled to unhome\" guard".format(list(s.homed[:3])) + ) +if s.motion_mode != mode_before: + fail( + "immediate unhome changed the trajectory mode {} -> {} and never " + "restored it (1=FREE 2=COORD 3=TELEOP)".format(mode_before, s.motion_mode) + ) +if not drain_errors(): + fail("immediate unhome from teleop was silently ignored -- expected motion's refusal") +print("PASS: an immediate unhome from teleop is refused, mode untouched") + +# 2. Immediate home from teleop. Must be refused with the joint-mode error, +# mode untouched. Before the command.c fix this was silently accepted and +# then never ran (do_homing() only advances in FREE). +drain_errors() +c.home(0) +time.sleep(1.0) +poll() +errs = drain_errors() +if not any("joint mode" in m for m in errs): + fail( + "immediate home from teleop was not refused (errors={!r}). do_homing() " + "only runs in FREE, so an accepted command here is a silently-dropped " + "home".format(errs) + ) +if s.motion_mode != mode_before: + fail( + "immediate home changed the trajectory mode {} -> {}".format( + mode_before, s.motion_mode + ) + ) +homing_flags = [s.joint[j]["homing"] for j in range(3)] +if any(homing_flags): + fail( + "immediate home from teleop left a joint in a homing state " + "(homing={}) -- it was accepted but cannot run".format(homing_flags) + ) +print("PASS: an immediate home from teleop is refused, mode untouched") + +# A legitimate home from joint mode must still work afterwards -- i.e. the +# refused command left nothing stuck (e.g. get_homing_is_active() latched). +c.teleop_enable(0) +time.sleep(0.3) +c.unhome(0) +if not wait_homed([0, 1, 1]): + fail("could not unhome joint 0 from joint mode after the refused home") +c.home(0) +if not wait_homed([1, 1, 1]): + fail("a legitimate home from joint mode did not run after the refused home") +print("PASS: homing still works normally from joint mode") + +print("done! it all worked") +sys.exit(0) diff --git a/tests/interp/g28.2/immediate-mode-guard/test.ini b/tests/interp/g28.2/immediate-mode-guard/test.ini new file mode 100644 index 00000000000..eb1ecd04ca9 --- /dev/null +++ b/tests/interp/g28.2/immediate-mode-guard/test.ini @@ -0,0 +1,98 @@ +[EMC] +DEBUG = 0 +VERSION = 1.1 + +[DISPLAY] +DISPLAY = ./test-ui.py + +[TASK] +TASK = milltask +CYCLE_TIME = 0.001 + +[RS274NGC] +PARAMETER_FILE = sim.var + +[EMCMOT] +EMCMOT = motmod +COMM_TIMEOUT = 4.0 +BASE_PERIOD = 0 +SERVO_PERIOD = 1000000 + +[HAL] +HALUI = halui +HALFILE = LIB:core_sim.hal + +[TRAJ] +AXES = 3 +COORDINATES = X Y Z +HOME = 0 0 0 +LINEAR_UNITS = inch +ANGULAR_UNITS = degree +DEFAULT_LINEAR_VELOCITY = 1.2 +MAX_LINEAR_VELOCITY = 4 + +[KINS] +JOINTS = 3 +KINEMATICS = trivkins + +[AXIS_X] +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +MIN_LIMIT = -40.0 +MAX_LIMIT = 40.0 + +[AXIS_Y] +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +MIN_LIMIT = -40.0 +MAX_LIMIT = 40.0 + +[AXIS_Z] +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +MIN_LIMIT = -4.0 +MAX_LIMIT = 4.0 + +[JOINT_0] +TYPE = LINEAR +HOME = 0.000 +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +BACKLASH = 0.000 +INPUT_SCALE = 4000 +OUTPUT_SCALE = 1.000 +MIN_LIMIT = -40.0 +MAX_LIMIT = 40.0 +FERROR = 0.050 +MIN_FERROR = 0.010 + +[JOINT_1] +TYPE = LINEAR +HOME = 0.000 +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +BACKLASH = 0.000 +INPUT_SCALE = 4000 +OUTPUT_SCALE = 1.000 +MIN_LIMIT = -40.0 +MAX_LIMIT = 40.0 +FERROR = 0.050 +MIN_FERROR = 0.010 + +[JOINT_2] +TYPE = LINEAR +HOME = 0.0 +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +BACKLASH = 0.000 +INPUT_SCALE = 4000 +OUTPUT_SCALE = 1.000 +MIN_LIMIT = -4.0 +MAX_LIMIT = 4.0 +FERROR = 0.050 +MIN_FERROR = 0.010 + +[EMCIO] +TOOL_CHANGE_QUILL_UP = 1 +RANDOM_TOOLCHANGER = 0 +TOOL_TABLE = sim.tbl diff --git a/tests/interp/g28.2/immediate-mode-guard/test.sh b/tests/interp/g28.2/immediate-mode-guard/test.sh new file mode 100755 index 00000000000..a16f6fa8522 --- /dev/null +++ b/tests/interp/g28.2/immediate-mode-guard/test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec linuxcnc -r test.ini diff --git a/tests/interp/g28.2/invalid-pword/checkresult b/tests/interp/g28.2/invalid-pword/checkresult new file mode 100755 index 00000000000..24dc9aa53e3 --- /dev/null +++ b/tests/interp/g28.2/invalid-pword/checkresult @@ -0,0 +1,2 @@ +#!/bin/sh +exit 0 # test failure is indicated by test.sh exit value diff --git a/tests/interp/g28.2/invalid-pword/sim.tbl b/tests/interp/g28.2/invalid-pword/sim.tbl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/interp/g28.2/invalid-pword/test-ui.py b/tests/interp/g28.2/invalid-pword/test-ui.py new file mode 100755 index 00000000000..4237a215b8c --- /dev/null +++ b/tests/interp/g28.2/invalid-pword/test-ui.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 + +""" +Regression test: a home or unhome for a joint number the machine does not have +must be rejected immediately, with an error that names the bad joint, and must +not disturb the trajectory mode. + +Reported on real hardware (Mesa 7I95T gantry) in PR #4172: "g28.2 p5" on a +5-joint machine gave the generic + + G28.2 home did not start -- check machine mode, motion.homing-inhibit, ... + +after a two-second stall, and left the GUI jogging in joint mode until the next +G-code command happened to put it back. + +Two separate defects: emcJointHome()/emcJointUnhome() range-checked against +EMCMOT_MAX_JOINTS (the compile-time maximum, 16) instead of the machine's +configured joint count, so the command went to motion, which silently ignored +it; and the FREE-mode dip taken for homing sequencing then sat there for the +whole start timeout. + +This config has JOINTS = 3, so joint 3 and up are unconfigured. The unhome +half is checked through the immediate NML path (what the GUI's Unhome button +sends) rather than through G-code: G28.3 was dropped from this PR, so the +interpreter can no longer issue an unhome at all. + +It also covers the rest of the P word's surface, which is the whole of what +G28.2 can be asked to do: P-1 homes every joint, a plain joint number homes +one, and everything else -- no P word, P-2, another negative, a fraction -- +is refused up front, homes nothing and leaves the trajectory mode alone. +""" + +import linuxcnc +import hal + +import os +import sys +import time + +h = hal.component("python-ui") +h.ready() + +c = linuxcnc.command() +s = linuxcnc.stat() +e = linuxcnc.error_channel() + + +def poll(): + s.poll() + + +def fail(msg): + print("FAIL: " + msg) + sys.exit(1) + + +def near(a, b, tol=0.001): + return abs(a - b) < tol + + +def wait_idle(timeout=10.0): + t0 = time.time() + while time.time() - t0 < timeout: + poll() + if s.interp_state == linuxcnc.INTERP_IDLE: + return True + time.sleep(0.01) + return False + + +def wait_homed(expected, timeout=10.0): + t0 = time.time() + while time.time() - t0 < timeout: + poll() + if list(s.homed[:3]) == expected: + return True + time.sleep(0.01) + return False + + +def drain_errors(): + msgs = [] + while True: + err = e.poll() + if not err: + return msgs + msgs.append(err[1]) + + +def mode_name(m): + return {linuxcnc.TRAJ_MODE_FREE: "FREE", + linuxcnc.TRAJ_MODE_COORD: "COORD", + linuxcnc.TRAJ_MODE_TELEOP: "TELEOP"}.get(m, str(m)) + + +c.state(linuxcnc.STATE_ESTOP_RESET) +c.state(linuxcnc.STATE_ON) +c.home(0) +c.home(1) +c.home(2) +if not wait_homed([1, 1, 1]): + fail("initial home-all did not home all joints: {}".format(list(s.homed[:3]))) + +# Establish a coordinated mode worth preserving. +c.mode(linuxcnc.MODE_MDI) +c.mdi("G0 X1") +if not wait_idle(): + fail("setup MDI move did not settle") +poll() +if not near(s.position[0], 1.0): + fail("setup MDI move did not run (X at {})".format(s.position[0])) +drain_errors() +poll() +prior_mode = s.motion_mode +print("setup: coordinated motion works, motion_mode={}".format(mode_name(prior_mode))) + +for cmd in ("G28.2 P3",): + drain_errors() + t0 = time.time() + c.mdi(cmd) + if not wait_idle(): + fail("{} never returned to idle".format(cmd)) + elapsed = time.time() - t0 + time.sleep(0.3) + poll() + + msgs = drain_errors() + if not msgs: + fail("{} on an unconfigured joint reported no error at all".format(cmd)) + joined = " ".join(msgs) + if "did not start" in joined: + fail("{} stalled into the generic start-timeout error instead of being " + "rejected up front: {!r}".format(cmd, msgs[0][:120])) + if "joint" not in joined.lower(): + fail("{} error does not mention the joint number: {!r}".format(cmd, msgs[0][:120])) + print("PASS: {} reported {!r}".format(cmd, msgs[0][:90])) + + # The message must only advertise values the interpreter accepts. P-1 + # (home every joint) is real G-code surface, so pointing at it is the + # help the operator wanted. -2 is the volatile-unhome NML sentinel, which + # no caller of this path can use and which G28.2 refuses, so naming it + # would send the operator to a second error (PR #4172, Sigma1912). + if "P-1" not in joined: + fail("{} error does not point at P-1 as the way to home every joint: " + "{!r}".format(cmd, msgs[0][:120])) + if "-2" in joined: + fail("{} error offers the volatile-unhome sentinel, which a P word " + "cannot express: {!r}".format(cmd, msgs[0][:120])) + print("PASS: {} error points at P-1 and offers no unusable sentinel".format(cmd)) + + if elapsed > 1.5: + fail("{} took {:.1f}s to be rejected -- it went to motion and sat out " + "the homing start timeout".format(cmd, elapsed)) + print("PASS: {} was rejected in {:.2f}s, no start-timeout stall".format(cmd, elapsed)) + + if list(s.homed[:3]) != [1, 1, 1]: + fail("{} changed the homed state of a real joint: {}".format(cmd, list(s.homed[:3]))) + + if s.motion_mode != prior_mode: + fail("{} left the machine in {} (was {}) -- the FREE-mode dip taken for " + "homing sequencing was not undone, so the GUI is stuck jogging in " + "joint mode".format(cmd, mode_name(s.motion_mode), mode_name(prior_mode))) + print("PASS: {} left the trajectory mode untouched ({})".format(cmd, mode_name(s.motion_mode))) + +# P-1 (home every joint) is the one negative the P word accepts; it is +# exercised where it belongs, on the HOME_SEQUENCE config in +# ../home-all-sequence -- this config deliberately has no HOME_SEQUENCE, so a +# home-all could not run here anyway. Everything else stays refused, and the +# refusal must name P-1 so the operator is pointed at the spelling that +# works. -2 is the volatile-unhome +# sentinel EMC_JOINT_HOME carries for the GUI/halui/linuxcncrsh; it is not +# G-code surface and must not become it by way of the P word. +for bad, why in (("G28.2 P-2", "the volatile-unhome sentinel"), + ("G28.2 P-3", "a negative that is not -1"), + ("G28.2 P1.5", "a fractional joint number"), + ("G28.2", "no P word at all")): + drain_errors() + c.mdi(bad) + wait_idle() + time.sleep(0.3) + poll() + msgs = drain_errors() + joined = " ".join(msgs) + if not msgs: + fail("{!r} ({}) was accepted silently".format(bad, why)) + if "P-1" not in joined: + fail("{!r} error does not name P-1: {!r}".format(bad, msgs[0][:120])) + if list(s.homed[:3]) != [1, 1, 1]: + fail("{!r} changed the homed state: {}".format(bad, list(s.homed[:3]))) + if s.motion_mode != prior_mode: + fail("{!r} left the machine in {} (was {}) -- the refusal took the " + "FREE-mode dip".format(bad, mode_name(s.motion_mode), + mode_name(prior_mode))) + print("PASS: {!r} ({}) refused, names P-1, nothing homed or moved".format(bad, why)) + +# The same bound applies to an unhome, which since G28.3 was dropped can only +# arrive as an immediate command -- the GUI's Unhome button, halui, +# linuxcncrsh, or c.unhome() here. Motion has its own check on this path, but +# as "jno > all_joints", so joint 3 on a 3-joint machine slips past it into an +# unrelated complaint about extra joints. +drain_errors() +c.mode(linuxcnc.MODE_MANUAL) +time.sleep(0.2) +c.unhome(3) +time.sleep(0.5) +poll() +msgs = drain_errors() +if not msgs: + fail("an immediate unhome of unconfigured joint 3 reported no error at all") +if "extrajoint" in " ".join(msgs): + fail("unhome of joint 3 fell through the off-by-one into the extra-joint " + "branch: {!r}".format(msgs[0][:120])) +if list(s.homed[:3]) != [1, 1, 1]: + fail("an immediate unhome of unconfigured joint 3 disturbed a real joint: {}".format(list(s.homed[:3]))) +print("PASS: immediate unhome of an unconfigured joint reported {!r}".format(msgs[0][:90])) + +c.mode(linuxcnc.MODE_MDI) +time.sleep(0.2) + +# The rejection must not have cost anything: ordinary work continues. +c.mode(linuxcnc.MODE_MDI) +c.mdi("G0 X2") +if not wait_idle(): + fail("MDI move after the rejected G28.2 did not settle") +poll() +if not near(s.position[0], 2.0): + fail("coordinated motion did not survive the rejected Pn (X stayed at {})".format(s.position[0])) +print("PASS: coordinated motion still works after the rejection") + +# A valid Pn on the same machine must still work, so the check is not just +# refusing everything. +drain_errors() +c.mdi("G28.2 P1") +if not wait_idle(): + fail("a valid G28.2 P1 did not settle") +time.sleep(0.3) +poll() +if list(s.homed[:3]) != [1, 1, 1]: + fail("valid G28.2 P1 left joint 1 unhomed: {}".format(list(s.homed[:3]))) +print("PASS: a valid G28.2 P1 still homes on the same machine") + +print("done! it all worked") +sys.exit(0) diff --git a/tests/interp/g28.2/invalid-pword/test.ini b/tests/interp/g28.2/invalid-pword/test.ini new file mode 100644 index 00000000000..c0b3db5b31a --- /dev/null +++ b/tests/interp/g28.2/invalid-pword/test.ini @@ -0,0 +1,99 @@ +[EMC] +DEBUG = 0 +VERSION = 1.1 + +[DISPLAY] +DISPLAY = ./test-ui.py + +[TASK] +TASK = milltask +CYCLE_TIME = 0.001 + +[RS274NGC] +PARAMETER_FILE = sim.var + +[EMCMOT] +EMCMOT = motmod +COMM_TIMEOUT = 4.0 +BASE_PERIOD = 0 +SERVO_PERIOD = 1000000 + +[HAL] +HALUI = halui +HALFILE = LIB:core_sim.hal + +[TRAJ] +AXES = 3 +COORDINATES = X Y Z +HOME = 0 0 0 +LINEAR_UNITS = inch +ANGULAR_UNITS = degree +DEFAULT_LINEAR_VELOCITY = 1.2 +NO_FORCE_HOMING = 1 +MAX_LINEAR_VELOCITY = 4 + +[KINS] +JOINTS = 3 +KINEMATICS = corexykins + +[AXIS_X] +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +MIN_LIMIT = -40.0 +MAX_LIMIT = 40.0 + +[AXIS_Y] +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +MIN_LIMIT = -40.0 +MAX_LIMIT = 40.0 + +[AXIS_Z] +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +MIN_LIMIT = -4.0 +MAX_LIMIT = 4.0 + +[JOINT_0] +TYPE = LINEAR +HOME = 0.000 +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +BACKLASH = 0.000 +INPUT_SCALE = 4000 +OUTPUT_SCALE = 1.000 +MIN_LIMIT = -40.0 +MAX_LIMIT = 40.0 +FERROR = 0.050 +MIN_FERROR = 0.010 + +[JOINT_1] +TYPE = LINEAR +HOME = 0.000 +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +BACKLASH = 0.000 +INPUT_SCALE = 4000 +OUTPUT_SCALE = 1.000 +MIN_LIMIT = -40.0 +MAX_LIMIT = 40.0 +FERROR = 0.050 +MIN_FERROR = 0.010 + +[JOINT_2] +TYPE = LINEAR +HOME = 0.0 +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +BACKLASH = 0.000 +INPUT_SCALE = 4000 +OUTPUT_SCALE = 1.000 +MIN_LIMIT = -4.0 +MAX_LIMIT = 4.0 +FERROR = 0.050 +MIN_FERROR = 0.010 + +[EMCIO] +TOOL_CHANGE_QUILL_UP = 1 +RANDOM_TOOLCHANGER = 0 +TOOL_TABLE = sim.tbl diff --git a/tests/interp/g28.2/invalid-pword/test.sh b/tests/interp/g28.2/invalid-pword/test.sh new file mode 100755 index 00000000000..a16f6fa8522 --- /dev/null +++ b/tests/interp/g28.2/invalid-pword/test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec linuxcnc -r test.ini diff --git a/tests/interp/g28.2/joint-pword/expected b/tests/interp/g28.2/joint-pword/expected new file mode 100644 index 00000000000..4f2ebb97604 --- /dev/null +++ b/tests/interp/g28.2/joint-pword/expected @@ -0,0 +1,17 @@ + N..... USE_LENGTH_UNITS(CANON_UNITS_MM) + N..... SET_G5X_OFFSET(1, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000) + N..... SET_G92_OFFSET(0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000) + N..... SET_XY_ROTATION(0.0000) + N..... SET_FEED_REFERENCE(CANON_XYZ) + N..... ON_RESET() + N..... HOME_CYCLE_JOINT(1) + N..... HOME_CYCLE() + N..... SET_G5X_OFFSET(1, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000) + N..... SET_XY_ROTATION(0.0000) + N..... SET_FEED_MODE(0, 0) + N..... SET_FEED_RATE(0.0000) + N..... STOP_SPINDLE_TURNING(0) + N..... SET_SPINDLE_MODE(0 0.0000) + N..... PROGRAM_END() + N..... ON_RESET() + N..... ON_RESET() diff --git a/tests/interp/g28.2/joint-pword/test.ngc b/tests/interp/g28.2/joint-pword/test.ngc new file mode 100644 index 00000000000..3818cb894d2 --- /dev/null +++ b/tests/interp/g28.2/joint-pword/test.ngc @@ -0,0 +1,6 @@ +; G28.2 Pn: home a single joint by its 0-based joint number (matching +; [JOINT_n] INI section numbering), instead of every joint. +; G28.2 P-1 is the explicit home-every-joint form; there is no bare G28.2. +g28.2 p1 ; home joint 1 only +g28.2 p-1 ; home all joints +m2 diff --git a/tests/interp/g28.2/joint-pword/test.sh b/tests/interp/g28.2/joint-pword/test.sh new file mode 100755 index 00000000000..dc8e7d0164e --- /dev/null +++ b/tests/interp/g28.2/joint-pword/test.sh @@ -0,0 +1,4 @@ +#!/bin/bash +# G28.2 Pn homes a single joint, G28.2 P-1 homes them all (no INI flag needed). +rs274 -g test.ngc | awk '{$1=""; print}' | sed 's/-0\.0000/0.0000/g' +exit "${PIPESTATUS[0]}" diff --git a/tests/interp/g28.2/position-model/checkresult b/tests/interp/g28.2/position-model/checkresult new file mode 100755 index 00000000000..24dc9aa53e3 --- /dev/null +++ b/tests/interp/g28.2/position-model/checkresult @@ -0,0 +1,2 @@ +#!/bin/sh +exit 0 # test failure is indicated by test.sh exit value diff --git a/tests/interp/g28.2/position-model/sim.tbl b/tests/interp/g28.2/position-model/sim.tbl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/interp/g28.2/position-model/sim_extra.hal b/tests/interp/g28.2/position-model/sim_extra.hal new file mode 100644 index 00000000000..be4334b20c6 --- /dev/null +++ b/tests/interp/g28.2/position-model/sim_extra.hal @@ -0,0 +1,2 @@ +# core_sim.hal only loops joints 0..2 back; this test adds a C (joint 3). +net Cpos joint.3.motor-pos-cmd => joint.3.motor-pos-fb diff --git a/tests/interp/g28.2/position-model/test-ui.py b/tests/interp/g28.2/position-model/test-ui.py new file mode 100755 index 00000000000..dd1e78295e0 --- /dev/null +++ b/tests/interp/g28.2/position-model/test-ui.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 + +""" +Regression test: a G28.2 homing cycle resyncs the interpreter's model of +the current position, so a following G91 (incremental) move or an I/J/K +arc centre is computed from where the machine actually is after homing, +not from the stale pre-home point. + +The motivating case (raised in PR #4172 review): a rotary head or a +spindle re-tasked as a C axis is re-homed mid-program with G28.2 Pn. +An immediate home rewrites that joint's coordinate to HOME_OFFSET with no +physical motion (homing.c HOME_SET_INDEX_POSITION), so if the interpreter +keeps its pre-home value a subsequent `G91 C90` targets `stale_C + 90` and +the axis sweeps the difference -- on a wrapped rotary there is no soft +limit to stop it. + +Checked here on joint 3 (C, angular) and joint 0 (X, linear): move the +axis away from zero, re-home it (coordinate jumps back to 0, no motion), +then make an incremental move and confirm it lands at 0 + increment. +""" + +import linuxcnc +import hal + +import sys +import time + +h = hal.component("python-ui") +h.ready() + +c = linuxcnc.command() +s = linuxcnc.stat() + + +def poll(): + s.poll() + + +def wait_idle(timeout=10.0): + t0 = time.time() + while time.time() - t0 < timeout: + poll() + if s.exec_state == linuxcnc.EXEC_DONE and s.interp_state == linuxcnc.INTERP_IDLE: + return True + time.sleep(0.01) + return False + + +def wait_homed(expected, timeout=10.0): + t0 = time.time() + while time.time() - t0 < timeout: + poll() + if list(s.homed[:len(expected)]) == expected: + return True + time.sleep(0.01) + return False + + +def fail(msg): + print("FAIL: " + msg) + sys.exit(1) + + +def near(a, b, tol=0.01): + return abs(a - b) < tol + + +def mdi(cmd): + c.mdi(cmd) + if c.wait_complete(10) == -1: + fail("MDI %r timed out" % cmd) + if not wait_idle(): + fail("MDI %r did not settle" % cmd) + poll() + + +c.state(linuxcnc.STATE_ESTOP_RESET) +c.state(linuxcnc.STATE_ON) +for j in range(4): + c.home(j) +if not wait_homed([1, 1, 1, 1]): + fail("initial home-all did not home all joints: {}".format(list(s.homed[:4]))) + +c.mode(linuxcnc.MODE_MDI) +time.sleep(0.2) + +# --- rotary C (joint 3) --- +mdi("G0 C30") +if not near(s.position[5], 30.0): + fail("C did not reach 30 (got {})".format(s.position[5])) + +# Immediate re-home: C's coordinate snaps back to 0 with no physical move. +mdi("G28.2 P3") +if not near(s.position[5], 0.0): + fail("G28.2 P3 did not put C back to 0 (got {})".format(s.position[5])) + +# The interpreter must now know C is 0. Without the resync it still thinks +# C is 30, so this incremental move targets 120 and the axis sweeps 90 deg +# too far. +mdi("G91 G0 C90") +mdi("G90") +if near(s.position[5], 120.0): + fail("G91 C90 after G28.2 P3 targeted stale_C + 90 = 120 -- " + "interpreter position not resynced after homing") +if not near(s.position[5], 90.0): + fail("G91 C90 after G28.2 P3 landed at {}, expected 90".format(s.position[5])) +print("PASS: G91 rotary move after G28.2 Pn is computed from the homed position") + +# --- linear X (joint 0) --- +mdi("G0 X5") +if not near(s.position[0], 5.0): + fail("X did not reach 5 (got {})".format(s.position[0])) + +mdi("G28.2 P0") +if not near(s.position[0], 0.0): + fail("G28.2 P0 did not put X back to 0 (got {})".format(s.position[0])) + +mdi("G91 G0 X1") +mdi("G90") +if near(s.position[0], 6.0): + fail("G91 X1 after G28.2 P0 targeted stale_X + 1 = 6 -- " + "interpreter position not resynced after homing") +if not near(s.position[0], 1.0): + fail("G91 X1 after G28.2 P0 landed at {}, expected 1".format(s.position[0])) +print("PASS: G91 linear move after G28.2 Pn is computed from the homed position") + +# --- absolute moves are unaffected either way --- +mdi("G0 X0 C0") +if not (near(s.position[0], 0.0) and near(s.position[5], 0.0)): + fail("absolute move back to origin failed: X={} C={}".format(s.position[0], s.position[5])) +print("PASS: absolute moves after G28.2 still land where asked") + +print("done! it all worked") +sys.exit(0) diff --git a/tests/interp/g28.2/position-model/test.ini b/tests/interp/g28.2/position-model/test.ini new file mode 100644 index 00000000000..c725f5bf718 --- /dev/null +++ b/tests/interp/g28.2/position-model/test.ini @@ -0,0 +1,120 @@ +[EMC] +DEBUG = 0 +VERSION = 1.1 + +[DISPLAY] +DISPLAY = ./test-ui.py + +[TASK] +TASK = milltask +CYCLE_TIME = 0.001 + +[RS274NGC] +PARAMETER_FILE = sim.var + +[EMCMOT] +EMCMOT = motmod +COMM_TIMEOUT = 4.0 +BASE_PERIOD = 0 +SERVO_PERIOD = 1000000 + +[HAL] +HALUI = halui +HALFILE = LIB:core_sim.hal +HALFILE = sim_extra.hal + +[TRAJ] +AXES = 4 +COORDINATES = X Y Z C +HOME = 0 0 0 0 +LINEAR_UNITS = inch +ANGULAR_UNITS = degree +DEFAULT_LINEAR_VELOCITY = 1.2 +MAX_LINEAR_VELOCITY = 4 +DEFAULT_ANGULAR_VELOCITY = 45 +MAX_ANGULAR_VELOCITY = 90 + +[KINS] +JOINTS = 4 +KINEMATICS = trivkins coordinates=XYZC + +[AXIS_X] +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +MIN_LIMIT = -40.0 +MAX_LIMIT = 40.0 + +[AXIS_Y] +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +MIN_LIMIT = -40.0 +MAX_LIMIT = 40.0 + +[AXIS_Z] +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +MIN_LIMIT = -4.0 +MAX_LIMIT = 4.0 + +[AXIS_C] +MAX_VELOCITY = 90 +MAX_ACCELERATION = 1000.0 +MIN_LIMIT = -3600.0 +MAX_LIMIT = 3600.0 + +[JOINT_0] +TYPE = LINEAR +HOME = 0.000 +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +BACKLASH = 0.000 +INPUT_SCALE = 4000 +OUTPUT_SCALE = 1.000 +MIN_LIMIT = -40.0 +MAX_LIMIT = 40.0 +FERROR = 0.050 +MIN_FERROR = 0.010 + +[JOINT_1] +TYPE = LINEAR +HOME = 0.000 +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +BACKLASH = 0.000 +INPUT_SCALE = 4000 +OUTPUT_SCALE = 1.000 +MIN_LIMIT = -40.0 +MAX_LIMIT = 40.0 +FERROR = 0.050 +MIN_FERROR = 0.010 + +[JOINT_2] +TYPE = LINEAR +HOME = 0.0 +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +BACKLASH = 0.000 +INPUT_SCALE = 4000 +OUTPUT_SCALE = 1.000 +MIN_LIMIT = -4.0 +MAX_LIMIT = 4.0 +FERROR = 0.050 +MIN_FERROR = 0.010 + +[JOINT_3] +TYPE = ANGULAR +HOME = 0.0 +MAX_VELOCITY = 90 +MAX_ACCELERATION = 1000.0 +BACKLASH = 0.000 +INPUT_SCALE = 1000 +OUTPUT_SCALE = 1.000 +MIN_LIMIT = -3600.0 +MAX_LIMIT = 3600.0 +FERROR = 0.050 +MIN_FERROR = 0.010 + +[EMCIO] +TOOL_CHANGE_QUILL_UP = 1 +RANDOM_TOOLCHANGER = 0 +TOOL_TABLE = sim.tbl diff --git a/tests/interp/g28.2/position-model/test.sh b/tests/interp/g28.2/position-model/test.sh new file mode 100755 index 00000000000..a16f6fa8522 --- /dev/null +++ b/tests/interp/g28.2/position-model/test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec linuxcnc -r test.ini diff --git a/tests/interp/g28.2/sequencing/checkresult b/tests/interp/g28.2/sequencing/checkresult new file mode 100755 index 00000000000..24dc9aa53e3 --- /dev/null +++ b/tests/interp/g28.2/sequencing/checkresult @@ -0,0 +1,2 @@ +#!/bin/sh +exit 0 # test failure is indicated by test.sh exit value diff --git a/tests/interp/g28.2/sequencing/sim.tbl b/tests/interp/g28.2/sequencing/sim.tbl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/interp/g28.2/sequencing/test-ui.py b/tests/interp/g28.2/sequencing/test-ui.py new file mode 100755 index 00000000000..67c05322a67 --- /dev/null +++ b/tests/interp/g28.2/sequencing/test-ui.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 + +""" +Regression test for the G28.2 Pn task-level sequencing behavior: + + 1. G28.2 Pn's FREE-mode dip (needed because do_homing() only advances in + free mode) must be invisible at the task level -- task.mode should be + unaffected by it. + 2. A G28.2 Pn naming a joint that does not exist on the machine must be + rejected without leaving task.mode stuck (regression test for a bug + where an outright-rejected home left the FREE-mode dip unrestored, + which made task.mode read as MANUAL forever). +""" + +import linuxcnc +import hal + +import sys +import time + +h = hal.component("python-ui") +h.ready() + +c = linuxcnc.command() +s = linuxcnc.stat() + + +def poll(): + s.poll() + + +def wait_idle(timeout=5.0): + t0 = time.time() + while time.time() - t0 < timeout: + poll() + if s.exec_state == linuxcnc.EXEC_DONE and s.interp_state == linuxcnc.INTERP_IDLE: + return True + time.sleep(0.01) + return False + + +def wait_homed(expected, timeout=5.0): + t0 = time.time() + while time.time() - t0 < timeout: + poll() + if list(s.homed[:3]) == expected: + return True + time.sleep(0.01) + return False + + +def fail(msg): + print("FAIL: " + msg) + sys.exit(1) + + +def near(a, b, tol=0.001): + return abs(a - b) < tol + + +c.state(linuxcnc.STATE_ESTOP_RESET) +c.state(linuxcnc.STATE_ON) +c.home(0) +c.home(1) +c.home(2) +if not wait_homed([1, 1, 1]): + fail("initial home-all did not home all joints: {}".format(list(s.homed[:3]))) + +c.mode(linuxcnc.MODE_MDI) +time.sleep(0.2) +poll() +mode_before = s.task_mode + +# Machine is still fully homed here, so this MDI G28.2 P1 is a redundant +# re-home -- the NO_FORCE_HOMING gate doesn't apply (all_homed() is true +# throughout), so this exercises the plain mode-dip-and-restore path. +c.mdi("G28.2 P1") +if not wait_idle(): + fail("redundant G28.2 P1 did not complete") +poll() +if s.task_mode != mode_before: + fail("task_mode changed across a redundant G28.2 Pn: {} -> {}".format(mode_before, s.task_mode)) +print("PASS: G28.2 Pn's mode dip is invisible at the task level") + +# The machine is fully homed, so this reaches the actual Pn validation, +# which must reject joint 99 without leaving task_mode stuck: an +# outright-rejected home must still undo the FREE-mode dip (fix(g28): +# "restore traj mode when G28.2 rejects the request outright"). +c.mdi("G28.2 P99") +if not wait_idle(): + fail("invalid-joint G28.2 P99 did not settle") +poll() +if s.task_mode != mode_before: + fail("task_mode after invalid Pn is {}, expected {} (MDI)".format(s.task_mode, mode_before)) + +c.mdi("G0 X2") +if not wait_idle(): + fail("recovery MDI command after invalid Pn did not settle") +poll() +if not near(s.position[0], 2.0): + fail("MDI command after an invalid Pn was rejected -- task_mode stuck (X stayed at {})".format(s.position[0])) +print("PASS: an invalid Pn does not leave task_mode stuck") + +print("done! it all worked") +sys.exit(0) diff --git a/tests/interp/g28.2/sequencing/test.ini b/tests/interp/g28.2/sequencing/test.ini new file mode 100644 index 00000000000..eb1ecd04ca9 --- /dev/null +++ b/tests/interp/g28.2/sequencing/test.ini @@ -0,0 +1,98 @@ +[EMC] +DEBUG = 0 +VERSION = 1.1 + +[DISPLAY] +DISPLAY = ./test-ui.py + +[TASK] +TASK = milltask +CYCLE_TIME = 0.001 + +[RS274NGC] +PARAMETER_FILE = sim.var + +[EMCMOT] +EMCMOT = motmod +COMM_TIMEOUT = 4.0 +BASE_PERIOD = 0 +SERVO_PERIOD = 1000000 + +[HAL] +HALUI = halui +HALFILE = LIB:core_sim.hal + +[TRAJ] +AXES = 3 +COORDINATES = X Y Z +HOME = 0 0 0 +LINEAR_UNITS = inch +ANGULAR_UNITS = degree +DEFAULT_LINEAR_VELOCITY = 1.2 +MAX_LINEAR_VELOCITY = 4 + +[KINS] +JOINTS = 3 +KINEMATICS = trivkins + +[AXIS_X] +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +MIN_LIMIT = -40.0 +MAX_LIMIT = 40.0 + +[AXIS_Y] +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +MIN_LIMIT = -40.0 +MAX_LIMIT = 40.0 + +[AXIS_Z] +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +MIN_LIMIT = -4.0 +MAX_LIMIT = 4.0 + +[JOINT_0] +TYPE = LINEAR +HOME = 0.000 +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +BACKLASH = 0.000 +INPUT_SCALE = 4000 +OUTPUT_SCALE = 1.000 +MIN_LIMIT = -40.0 +MAX_LIMIT = 40.0 +FERROR = 0.050 +MIN_FERROR = 0.010 + +[JOINT_1] +TYPE = LINEAR +HOME = 0.000 +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +BACKLASH = 0.000 +INPUT_SCALE = 4000 +OUTPUT_SCALE = 1.000 +MIN_LIMIT = -40.0 +MAX_LIMIT = 40.0 +FERROR = 0.050 +MIN_FERROR = 0.010 + +[JOINT_2] +TYPE = LINEAR +HOME = 0.0 +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 +BACKLASH = 0.000 +INPUT_SCALE = 4000 +OUTPUT_SCALE = 1.000 +MIN_LIMIT = -4.0 +MAX_LIMIT = 4.0 +FERROR = 0.050 +MIN_FERROR = 0.010 + +[EMCIO] +TOOL_CHANGE_QUILL_UP = 1 +RANDOM_TOOLCHANGER = 0 +TOOL_TABLE = sim.tbl diff --git a/tests/interp/g28.2/sequencing/test.sh b/tests/interp/g28.2/sequencing/test.sh new file mode 100755 index 00000000000..a16f6fa8522 --- /dev/null +++ b/tests/interp/g28.2/sequencing/test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec linuxcnc -r test.ini