diff --git a/docs/src/config/ini-config.adoc b/docs/src/config/ini-config.adoc index 8f1e8d09d56..8ff2083f814 100644 --- a/docs/src/config/ini-config.adoc +++ b/docs/src/config/ini-config.adoc @@ -633,6 +633,14 @@ The maximum number of `USER_M_PATH` directories is defined at compile time (typ: Preserve case in O-word names within comments if set, enables reading of mixed-case HAL items in structured comments like `(debug, #<_hal[MixedCaseItem])`. * `OWORD_WARNONLY = 0` (Default: 0) + Warn rather than error in case of errors in O-word subroutines. +* `GCODE_HOMING = 0` (bool, Default: 0) + + When set, a plain `G28` (no axis words) references the machine before its + return move: it runs the homing cycle on all joints when the machine is not + already fully homed, then performs the normal `G28` return. An already-homed + machine skips the homing step, so `G28` keeps its stock behavior. The option + affects only the bare `G28`; `G28 axes`, `G28.1`, `G30`, `G30.1`, `G28.2` + and `G28.3` are unchanged. With the default of 0, behavior is identical to + stock LinuxCNC. See <> and <>. * `DISABLE_G92_PERSISTENCE = 0` (bool, Default: 0) Allow to clear the G92 offset automatically when config start-up. * `DISABLE_AUTO_G54 = 0` (bool, Default: 0) + diff --git a/docs/src/gcode/g-code.adoc b/docs/src/gcode/g-code.adoc index 3d9063339b9..03aaf7d6883 100644 --- a/docs/src/gcode/g-code.adoc +++ b/docs/src/gcode/g-code.adoc @@ -73,6 +73,7 @@ as the 'L number', and so on for any other letter. |<> |Plane Select |<> |Set Units of Measure |<> |Go to Predefined Position +|<> |Home / Unhome from G-code |<> |Go to Predefined Position |<> |Spindle Synchronized Motion |<> |Rigid Tapping @@ -997,6 +998,64 @@ It is an error if : * Cutter Compensation is turned on +[NOTE] +When `<>` is set to 1, a plain +`G28` (no axis words) first references the machine -- it runs the homing +cycle on all joints when the machine is not already fully homed -- and then +performs the normal return move. On an already-homed machine the homing step +is skipped, so `G28` behaves exactly as described above. The flag has no +effect on `G28 axes`, `G28.1`, `G30`, `G30.1`, `G28.2` or `G28.3`, and it is +off by default (stock behavior). See also `G28.2` below. + +[[gcode:g28.2-g28.3]] +== G28.2, G28.3 Home, Unhome from G-code(((G28.2 Home from G-code)))(((G28.3 Unhome from G-code))) + +These non-modal codes let a program or MDI line reference the machine +instead of requiring the operator to use the GUI's *Home All* button. They +follow the same modal-group-0 pattern as `G28.1`/`G30.1` and take no axis +words. + +* 'G28.2' - runs the homing cycle on all joints, in `HOME_SEQUENCE` order + (the same operation as the GUI *Home All*). +* 'G28.3' - unhomes all joints. +* '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 `G28.2`'s `HOME_SEQUENCE`, and by `G28.6`/joint jogging). + Other joints are left as they are. +* 'G28.3 Pn' - unhomes joint 'n' only, leaving other joints as they are. + +.G28.2/G28.3 Example Lines +[source,ngc] +---- +G28.2 (home all joints, in HOME_SEQUENCE order) +G28.2 P1 (home joint 1 only) +G28.3 P1 (unhome joint 1 only) +G28.3 (unhome all joints) +---- + +A queued `G28.2`/`G28.3` 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. + +[IMPORTANT] +When `[TRAJ]NO_FORCE_HOMING` is not set (the default), unhoming a joint that +leaves the machine not fully homed blocks any further `AUTO` or `MDI` +command until the machine is fully re-homed, exactly as if the machine had +never been homed in the first place -- closing the gap where `G28.3` could +otherwise be used to bypass the homed-before-running requirement. + +[NOTE] +`G28.2` and `G28.3` are LinuxCNC extensions; there is no standard Fanuc +equivalent. They are available regardless of the `GCODE_HOMING` INI setting. + +It is an error if : + +* Cutter Compensation is turned on +* 'Pn' names a joint number that does not exist on the machine + [[gcode:g30-g30.1]] == G30, G30.1 Go/Set Predefined Position(((G30 Go/Set Predefined Position))) diff --git a/src/emc/motion/command.c b/src/emc/motion/command.c index 2f4509d0987..98dc11038f5 100644 --- a/src/emc/motion/command.c +++ b/src/emc/motion/command.c @@ -1416,9 +1416,12 @@ 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 */ - reportError(_("must be in joint mode to home")); + /* Normally homing requires free (joint) mode. Allow it also when + * motion is otherwise IDLE (in position, nothing queued) so a + * G-code-triggered home (G28.2) works from MDI / a program. */ + if (emcmotStatus->motion_state != EMCMOT_MOTION_FREE + && !(GET_MOTION_INPOS_FLAG() && emcmotStatus->depth == 0)) { + reportError(_("must be in joint mode (or idle) to home")); return; } if (*(emcmot_hal_data->homing_inhibit)) { diff --git a/src/emc/nml_intf/canon.hh b/src/emc/nml_intf/canon.hh index 93a7075f20f..d167f1b1c04 100644 --- a/src/emc/nml_intf/canon.hh +++ b/src/emc/nml_intf/canon.hh @@ -243,6 +243,21 @@ extern void SET_G92_OFFSET(double x, double y, double z, extern void SET_XY_ROTATION(double t); +/* G28.2 / G28.3: trigger the machine homing cycle / unhome from G-code + * (bare form = all joints). Maps to EMC_JOINT_HOME/UNHOME(-1). */ +extern void HOME_CYCLE(void); +extern void UNHOME_AXES(void); +/* G28.2 Pn / G28.3 Pn: home/unhome a single joint by its 0-based joint + * number (matching [JOINT_n] INI section numbering). Maps to + * EMC_JOINT_HOME/UNHOME(joint). */ +extern void HOME_CYCLE_JOINT(int joint); +extern void UNHOME_JOINT(int joint); +/* GCODE_HOMING (plain G28 with [RS274NGC]GCODE_HOMING=1): reference the + * machine before the G28 return move, but only when it is not already fully + * homed (a homed machine sees a pure legacy G28). Emits EMC_JOINT_HOME with + * the EMC_HOME_ALL_IF_UNHOMED sentinel; task skips it when all_homed(). */ +extern void HOME_CYCLE_IF_UNHOMED(void); + /* 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.hh b/src/emc/nml_intf/emc.hh index 0b500f185bf..4c5bdab185f 100644 --- a/src/emc/nml_intf/emc.hh +++ b/src/emc/nml_intf/emc.hh @@ -214,7 +214,8 @@ enum class EMC_TASK_EXEC { WAITING_FOR_MOTION_AND_IO = 7, WAITING_FOR_DELAY = 8, WAITING_FOR_SYSTEM_CMD = 9, - WAITING_FOR_SPINDLE_ORIENTED = 10 + WAITING_FOR_SPINDLE_ORIENTED = 10, + WAITING_FOR_HOMING = 11 }; // types for EMC_TASK interpState diff --git a/src/emc/nml_intf/emc_nml.hh b/src/emc/nml_intf/emc_nml.hh index ba1846771a0..d79d5076ac7 100644 --- a/src/emc/nml_intf/emc_nml.hh +++ b/src/emc/nml_intf/emc_nml.hh @@ -327,6 +327,13 @@ class EMC_JOINT_HALT:public EMC_JOINT_CMD_MSG { void update(CMS * cms); }; +// GCODE_HOMING ([RS274NGC]GCODE_HOMING=1): a plain G28 emits EMC_JOINT_HOME +// carrying this sentinel in 'joint' to mean "home all joints, but only if the +// machine is not already fully homed". Task resolves it via all_homed(): a +// fully-homed machine sees a pure legacy G28 return (no homing). Distinct from +// joint = -1 (unconditional home-all) and joint = -2 (UNHOME volatile). +#define EMC_HOME_ALL_IF_UNHOMED (-3) + class EMC_JOINT_HOME:public EMC_JOINT_CMD_MSG { public: EMC_JOINT_HOME() diff --git a/src/emc/rs274ngc/gcodemodule.cc b/src/emc/rs274ngc/gcodemodule.cc index a5a200ba5e8..5a1fca58c35 100644 --- a/src/emc/rs274ngc/gcodemodule.cc +++ b/src/emc/rs274ngc/gcodemodule.cc @@ -432,6 +432,12 @@ void SELECT_PLANE(CANON_PLANE pl) { Py_XDECREF(result); } +void HOME_CYCLE(void) {} +void UNHOME_AXES(void) {} +void HOME_CYCLE_JOINT(int) {} +void UNHOME_JOINT(int) {} +void HOME_CYCLE_IF_UNHOMED(void) {} + void SET_TRAVERSE_RATE(double rate) { maybe_new_line(); if(interp_error) return; diff --git a/src/emc/rs274ngc/interp_array.cc b/src/emc/rs274ngc/interp_array.cc index ba233a1d88b..ac830c3ee54 100644 --- a/src/emc/rs274ngc/interp_array.cc +++ b/src/emc/rs274ngc/interp_array.cc @@ -85,7 +85,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, 0,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, // 282=G28.2 283=G28.3 /* 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 b5c9749c545..1f00ba3d070 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 || mode0 == G_28_3) { // 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)), @@ -326,12 +327,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->g_modes[GM_MODAL_0] != G_28_3) && (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 G64 G5 G5.2 G6, G6.2, G76 G82 G86 G88 G89" + " G28.2 G28.3" " 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 8f8573cab2f..d78c1dab91a 100644 --- a/src/emc/rs274ngc/interp_convert.cc +++ b/src/emc/rs274ngc/interp_convert.cc @@ -3111,6 +3111,64 @@ Called by: convert_modal_0. */ +/*! convert_home_cycle + +Handles G28.2 (run the homing cycle on all joints) and G28.3 (unhome all +joints) from a G-code line, so machines can reference / clear references +from MDI or a program instead of only from the GUI. Bare form (no P word) +acts on all joints, in HOME_SEQUENCE order. + +An optional Pn word homes/unhomes a single joint by its 0-based joint number +(matching [JOINT_n] INI section numbering, e.g. P1 -> JOINT_1). This is the +primitive Sigma1912 asked for 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/UNHOME 'joint' field, so it needs no NML change and works +identically on any kinematics (per grandixximo's review comment on that PR). +Axis-letter forms (G28.2 X) are deliberately NOT supported: resolving an +axis letter to a joint needs the kinematics coordinate map and isn't +trivial even on trivkins (duplicate letters on gantries) -- andypugh's +review also objected that homing is a joint concept, not an axis one. + +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 the bare form to home both. + +Motion still enforces its own safety (idle / not on limits) and does the +final range check of the joint number against the machine's actual joint +count. +*/ +int Interp::convert_home_cycle(int move, + block_pointer block, + setup_pointer settings) +{ + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), + "Cannot home (G28.2/G28.3) with cutter radius compensation on"); + + int joint = -1; + if (block->p_flag) { + CHKS(((block->p_number < 0.0) || + (block->p_number != round_to_int(block->p_number))), + "P value for G28.2/G28.3 must be a non-negative whole joint number"); + joint = round_to_int(block->p_number); + } + + if (move == G_28_2) { + if (joint < 0) { + HOME_CYCLE(); + } else { + HOME_CYCLE_JOINT(joint); + } + } else { + if (joint < 0) { + UNHOME_AXES(); + } else { + UNHOME_JOINT(joint); + } + } + 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 @@ -3143,6 +3201,16 @@ int Interp::convert_home(int move, //!< G-code, must be G_28 or G_30 CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), NCE_CANNOT_USE_G28_OR_G30_WITH_CUTTER_RADIUS_COMP); + /* GCODE_HOMING ([RS274NGC]GCODE_HOMING=1): a plain G28 references the + * machine (runs the homing cycle on all joints) BEFORE the waypoint + + * return moves below, but only when it is not already fully homed - task + * drops the home when all_homed(), so a homed machine sees a pure legacy + * G28. The home is emitted first so it completes (motion stays busy, the + * return waits) while the joint positions are still unknown. Flag-gated and + * G28-only; G30/G28.2/G28.3 are unchanged. */ + if (FEATURE(GCODE_HOMING) && move == G_28) + HOME_CYCLE_IF_UNHOMED(); + // waypoint is in currently active coordinate system // move indexers first, one at a time @@ -4290,6 +4358,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) || (code == G_28_3)) { + CHP(convert_home_cycle(code, 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_internal.hh b/src/emc/rs274ngc/interp_internal.hh index 22268854211..f9cf803fd38 100644 --- a/src/emc/rs274ngc/interp_internal.hh +++ b/src/emc/rs274ngc/interp_internal.hh @@ -221,6 +221,8 @@ enum GCodes G_21 = 210, G_28 = 280, G_28_1 = 281, + G_28_2 = 282, /* G-code homing cycle (home worded/all joints) */ + G_28_3 = 283, /* G-code unhome */ G_30 = 300, G_30_1 = 301, G_33 = 330, @@ -844,6 +846,11 @@ struct setup // do not lowercase named params inside comments - for #<_hal[PinName]> #define FEATURE_NO_DOWNCASE_OWORD 0x00000010 #define FEATURE_OWORD_WARNONLY 0x00000020 + // [RS274NGC]GCODE_HOMING=1: a plain G28 references the machine (runs the + // homing cycle) before its return move when the machine is not already + // fully homed; a fully-homed machine sees a pure legacy G28. G28.2/G28.3 + // are flag-independent. +#define FEATURE_GCODE_HOMING 0x00000040 boost::python::object *pythis; // boost::cref to 'this' const char *on_abort_command; diff --git a/src/emc/rs274ngc/rs274ngc_interp.hh b/src/emc/rs274ngc/rs274ngc_interp.hh index f02d334a810..10e8009b63e 100644 --- a/src/emc/rs274ngc/rs274ngc_interp.hh +++ b/src/emc/rs274ngc/rs274ngc_interp.hh @@ -321,6 +321,8 @@ public: setup_pointer settings); int convert_savehome(int move, block_pointer block, setup_pointer settings); + int convert_home_cycle(int move, block_pointer block, // G28.2/G28.3 + 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 f843fc82985..d3e3e67a8cf 100644 --- a/src/emc/rs274ngc/rs274ngc_pre.cc +++ b/src/emc/rs274ngc/rs274ngc_pre.cc @@ -909,6 +909,8 @@ int Interp::init() _setup.feature_set |= FEATURE_NO_DOWNCASE_OWORD; if (inifile.findBoolV("OWORD_WARNONLY", "RS274NGC", false)) _setup.feature_set |= FEATURE_OWORD_WARNONLY; + if (inifile.findBoolV("GCODE_HOMING", "RS274NGC", false)) + _setup.feature_set |= FEATURE_GCODE_HOMING; if (auto inival = inifile.findInt("LOCKING_INDEXER_JOINT", "AXIS_A")) { _setup.a_indexer_jnum = *inival; diff --git a/src/emc/sai/saicanon.cc b/src/emc/sai/saicanon.cc index 23184d7f33a..9ae66bb38ff 100644 --- a/src/emc/sai/saicanon.cc +++ b/src/emc/sai/saicanon.cc @@ -113,6 +113,12 @@ void SET_XY_ROTATION(double t) { ECHO_WITH_ARGS("%.4f", t); } +void HOME_CYCLE(void) { ECHO_WITH_ARGS(""); } +void UNHOME_AXES(void) { ECHO_WITH_ARGS(""); } +void HOME_CYCLE_JOINT(int joint) { ECHO_WITH_ARGS("%d", joint); } +void UNHOME_JOINT(int joint) { ECHO_WITH_ARGS("%d", joint); } +void HOME_CYCLE_IF_UNHOMED(void) { ECHO_WITH_ARGS(""); } + 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 2fa4df47344..5bdf6e47a66 100644 --- a/src/emc/task/emccanon.cc +++ b/src/emc/task/emccanon.cc @@ -478,6 +478,59 @@ 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)); +} + +void UNHOME_AXES(void) +{ + flush_segments(); + auto msg = std::make_unique(); + msg->joint = -1; + interp_list.append(std::move(msg)); +} + +/* G28.2 Pn / G28.3 Pn -- home/unhome a single joint. joint is the interp's + * already-validated (non-negative) P value; motion does the final + * range check against the machine's actual joint count. */ +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 UNHOME_JOINT(int joint) +{ + flush_segments(); // see HOME_CYCLE + auto msg = std::make_unique(); + msg->joint = joint; + interp_list.append(std::move(msg)); +} + +/* GCODE_HOMING plain G28: home all joints, but only if the machine is not + * already fully homed. The sentinel joint value defers the all-homed test to + * task (execution time), so a homed machine drops the home and runs a pure + * legacy G28 return. */ +void HOME_CYCLE_IF_UNHOMED(void) +{ + flush_segments(); // see HOME_CYCLE + auto msg = std::make_unique(); + msg->joint = EMC_HOME_ALL_IF_UNHOMED; + 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 9ea6607234a..5d209c86f06 100644 --- a/src/emc/task/emctaskmain.cc +++ b/src/emc/task/emctaskmain.cc @@ -392,6 +392,30 @@ static EMC_TRAJ_SET_SPINDLESYNC *emcTrajSetSpindlesyncMsg; //static EMC_MOTION_SET_AOUT *emcMotionSetAoutMsg; //static EMC_MOTION_SET_DOUT *emcMotionSetDoutMsg; +// G28.2/G28.3 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/unhome 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 homingIsUnhome = false; // which completion criterion to check +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 + 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; @@ -1605,6 +1629,13 @@ static EMC_TASK_EXEC emcTaskCheckPreconditions(NMLmsg * cmd) return EMC_TASK_EXEC::WAITING_FOR_MOTION; break; + case EMC_JOINT_HOME_TYPE: // G28.2: program-order homing + case EMC_JOINT_UNHOME_TYPE: // G28.3: program-order unhome + // drain prior motion before (un)homing; without these cases a + // queued home/unhome 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) { @@ -1669,12 +1700,71 @@ 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 + { + int target_joint = home_msg->joint; + if (target_joint == EMC_HOME_ALL_IF_UNHOMED) { + // GCODE_HOMING plain G28: reference the machine only if it is + // not already fully homed; otherwise drop the home so the + // queued G28 return alone runs (pure legacy G28) -- no mode + // dip needed since nothing is actually commanded. + if (all_homed()) { + retval = 0; + break; + } + target_joint = -1; + } + // 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; + homingIsUnhome = false; + 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: unhome_msg = reinterpret_cast(cmd); + homingPriorMode = emcStatus->motion.traj.mode; + if (homingPriorMode != EMC_TRAJ_MODE::FREE) { + emcTrajSetMode(EMC_TRAJ_MODE::FREE); + } + homingWaitJoint = unhome_msg->joint; + homingIsUnhome = true; + homingStarted = false; + homingIssueTime = etime(); + homingWaiting = true; retval = emcJointUnhome(unhome_msg->joint); + if (retval != 0) { + // See the matching comment in EMC_JOINT_HOME_TYPE above. + homingWaiting = false; + if (homingPriorMode != EMC_TRAJ_MODE::FREE) { + emcTrajSetMode(homingPriorMode); + } + } break; case EMC_JOG_CONT_TYPE: @@ -2506,6 +2596,14 @@ static EMC_TASK_EXEC emcTaskCheckPostconditions(NMLmsg * cmd) return EMC_TASK_EXEC::WAITING_FOR_SPINDLE_ORIENTED; break; + case EMC_JOINT_HOME_TYPE: + case EMC_JOINT_UNHOME_TYPE: + // homingWaiting is false when EMC_JOINT_HOME_TYPE resolved to a no-op + // (GCODE_HOMING dropped because all_homed() was already true) -- + // nothing was issued, so there's 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; @@ -2737,6 +2835,114 @@ static int emcTaskExecute(void) } break; + case EMC_TASK_EXEC::WAITING_FOR_HOMING: + // G28.2/G28.3 sequencing: wait for the joint home/unhome 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. + // + // HOME and UNHOME are NOT symmetric at the motion level: EMCMOT_JOINT_HOME + // (control.c/do_home_joint) is a genuine state machine -- .homing goes + // true while it runs, false (with .homed set) when it finishes -- but + // EMCMOT_JOINT_UNHOME (command.c) is synchronous: set_unhomed() just + // clears .homed immediately in the same cycle it's issued, and .homing + // is never touched. So UNHOME must be checked directly, with no "wait + // for .homing to start" phase -- that phase would never end for it. + STEPPING_CHECK(); + { + bool any_homing = false; + bool all_target_homed = true; // success criterion for HOME + bool any_target_homed = false; // success criterion for UNHOME (want none) + 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) { + any_target_homed = true; + } else { + all_target_homed = false; + } + } + + bool success; + if (homingIsUnhome) { + // Synchronous: whatever it did, it already did by now. + success = !any_target_homed; + } else { + if (any_homing) { + homingStarted = true; + break; // still running; no timeout once started (see HOMING_START_TIMEOUT comment) + } + if (!homingStarted) { + // Never observed .homing go true: 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; + if (success && homingIsUnhome && !all_homed() && !no_force_homing) { + // Close the hole a per-move check would be expensive to plug: + // [TRAJ]NO_FORCE_HOMING=0 (the default) already refuses to + // *start* MDI/AUTO on an unhomed machine, but that check only + // fires at program/MDI start, not per line -- so a mid-program + // G28.3 with no matching re-home before further motion would + // otherwise slip through unreferenced (see PR #4172, Sigma's + // "G28.3 then G1 x2" example). Re-apply the exact same policy + // here, at the sync point this command already forces, with + // no cost added to the motion path itself. + emcOperatorError(_("Can't continue unhomed after G28.3 " + "(NO_FORCE_HOMING=0) -- re-home before the next move")); + emcStatus->task.execState = EMC_TASK_EXEC::ERROR; + // Same reasoning as the failure path below: leave it in FREE, + // don't snap back to a mode an unhomed machine can't legally + // run coordinated motion in. + } else if (success) { + emcStatus->task.execState = EMC_TASK_EXEC::DONE; + if (homingPriorMode != EMC_TRAJ_MODE::FREE) { + emcTrajSetMode(homingPriorMode); + } + } else { + // Homing/unhoming stopped without reaching the target state + // (aborted, faulted, ESTOP mid-cycle, a mode guard rejected an + // unhome, ...) -- 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("%s did not complete for joint %s", + homingIsUnhome ? "G28.3 unhome" : "G28.2 home", + 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 cbad6eab786..f11cf380ba7 100644 --- a/src/emc/task/taskintf.cc +++ b/src/emc/task/taskintf.cc @@ -796,7 +796,11 @@ int emcJointOverrideLimits(int joint) int emcJointHome(int joint) { if (joint < -1 || joint >= EMCMOT_MAX_JOINTS) { - return 0; + // Was silently "succeeding" here (return 0 == EMCMOT_COMM_OK to every + // caller), so an out-of-range G28.2 Pn joint number looked like a + // completed home instead of surfacing as an error. + rcs_print("emcJointHome: invalid joint number %d\n", joint); + return EMCMOT_COMM_ERROR_COMMAND; } emcmotCommand.command = EMCMOT_JOINT_HOME; @@ -808,7 +812,10 @@ int emcJointHome(int joint) int emcJointUnhome(int joint) { if (joint < -2 || joint >= EMCMOT_MAX_JOINTS) { - return 0; + // See emcJointHome: don't silently report success for an + // out-of-range joint number (e.g. from G28.3 Pn). + rcs_print("emcJointUnhome: invalid joint number %d\n", joint); + return EMCMOT_COMM_ERROR_COMMAND; } emcmotCommand.command = EMCMOT_JOINT_UNHOME; diff --git a/tests/interp/gcode-homing/flush-order/checkresult b/tests/interp/gcode-homing/flush-order/checkresult new file mode 100755 index 00000000000..24dc9aa53e3 --- /dev/null +++ b/tests/interp/gcode-homing/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/gcode-homing/flush-order/sim.tbl b/tests/interp/gcode-homing/flush-order/sim.tbl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/interp/gcode-homing/flush-order/test-ui.py b/tests/interp/gcode-homing/flush-order/test-ui.py new file mode 100755 index 00000000000..3f7fbf9dc1f --- /dev/null +++ b/tests/interp/gcode-homing/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_JOINT()/UNHOME_AXES()/UNHOME_JOINT()/HOME_CYCLE_IF_UNHOMED() +(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/unhome canon functions, a queued move immediately +before a G28.2/G28.3 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/gcode-homing/flush-order/test.ini b/tests/interp/gcode-homing/flush-order/test.ini new file mode 100644 index 00000000000..38b6a739acb --- /dev/null +++ b/tests/interp/gcode-homing/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/gcode-homing/flush-order/test.ngc b/tests/interp/gcode-homing/flush-order/test.ngc new file mode 100644 index 00000000000..0682100733b --- /dev/null +++ b/tests/interp/gcode-homing/flush-order/test.ngc @@ -0,0 +1,6 @@ +G20 +G94 +F60 +G1 X2 +G28.2 P0 +M2 diff --git a/tests/interp/gcode-homing/flush-order/test.sh b/tests/interp/gcode-homing/flush-order/test.sh new file mode 100755 index 00000000000..a16f6fa8522 --- /dev/null +++ b/tests/interp/gcode-homing/flush-order/test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec linuxcnc -r test.ini diff --git a/tests/interp/gcode-homing/homing-off/expected b/tests/interp/gcode-homing/homing-off/expected new file mode 100644 index 00000000000..9631897fdc5 --- /dev/null +++ b/tests/interp/gcode-homing/homing-off/expected @@ -0,0 +1,19 @@ + 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..... STRAIGHT_TRAVERSE(0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000) + N..... STRAIGHT_TRAVERSE(0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000) + N..... HOME_CYCLE() + N..... UNHOME_AXES() + 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/gcode-homing/homing-off/test.ngc b/tests/interp/gcode-homing/homing-off/test.ngc new file mode 100644 index 00000000000..e9b0e82ad66 --- /dev/null +++ b/tests/interp/gcode-homing/homing-off/test.ngc @@ -0,0 +1,6 @@ +; Default (GCODE_HOMING off): a plain G28 is a pure return move and emits NO +; homing op. G28.2 / G28.3 still home / unhome (they are flag-independent). +g28 ; default: return only, no HOME_CYCLE_IF_UNHOMED +g28.2 ; home all joints (flag-independent) +g28.3 ; unhome all joints (flag-independent) +m2 diff --git a/tests/interp/gcode-homing/homing-off/test.sh b/tests/interp/gcode-homing/homing-off/test.sh new file mode 100755 index 00000000000..02f05dabf1e --- /dev/null +++ b/tests/interp/gcode-homing/homing-off/test.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# No -i INI, so GCODE_HOMING defaults to off: the plain G28 must NOT emit a +# homing op, while G28.2/G28.3 still do. +rs274 -g test.ngc | awk '{$1=""; print}' | sed 's/-0\.0000/0.0000/g' +exit "${PIPESTATUS[0]}" diff --git a/tests/interp/gcode-homing/homing-on/expected b/tests/interp/gcode-homing/homing-on/expected new file mode 100644 index 00000000000..63455f07ed8 --- /dev/null +++ b/tests/interp/gcode-homing/homing-on/expected @@ -0,0 +1,19 @@ + 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() + N..... UNHOME_AXES() + N..... HOME_CYCLE_IF_UNHOMED() + N..... STRAIGHT_TRAVERSE(0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000) + N..... STRAIGHT_TRAVERSE(0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000) + 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() diff --git a/tests/interp/gcode-homing/homing-on/test.ini b/tests/interp/gcode-homing/homing-on/test.ini new file mode 100644 index 00000000000..79280defa62 --- /dev/null +++ b/tests/interp/gcode-homing/homing-on/test.ini @@ -0,0 +1,5 @@ +[RS274NGC] +GCODE_HOMING = 1 + +[TRAJ] +LINEAR_UNITS = mm diff --git a/tests/interp/gcode-homing/homing-on/test.ngc b/tests/interp/gcode-homing/homing-on/test.ngc new file mode 100644 index 00000000000..c969964df50 --- /dev/null +++ b/tests/interp/gcode-homing/homing-on/test.ngc @@ -0,0 +1,8 @@ +; GCODE_HOMING=1: a plain G28 (no axis words) references the machine first +; (HOME_CYCLE_IF_UNHOMED) and then performs its normal return move. +; G28.2 / G28.3 home / unhome explicitly and are flag-independent. +g28.2 ; home all joints +g28.3 ; unhome all joints +g28 ; with GCODE_HOMING=1: home-if-unhomed, then return +g28.1 ; unaffected by the flag (store predefined position) +m2 diff --git a/tests/interp/gcode-homing/homing-on/test.sh b/tests/interp/gcode-homing/homing-on/test.sh new file mode 100755 index 00000000000..cf21d8506dc --- /dev/null +++ b/tests/interp/gcode-homing/homing-on/test.sh @@ -0,0 +1,3 @@ +#!/bin/bash +rs274 -i test.ini -g test.ngc | awk '{$1=""; print}' | sed 's/-0\.0000/0.0000/g' +exit "${PIPESTATUS[0]}" diff --git a/tests/interp/gcode-homing/joint-pword/expected b/tests/interp/gcode-homing/joint-pword/expected new file mode 100644 index 00000000000..01f1228ac26 --- /dev/null +++ b/tests/interp/gcode-homing/joint-pword/expected @@ -0,0 +1,19 @@ + 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..... UNHOME_JOINT(0) + N..... HOME_CYCLE() + N..... UNHOME_AXES() + 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/gcode-homing/joint-pword/test.ngc b/tests/interp/gcode-homing/joint-pword/test.ngc new file mode 100644 index 00000000000..bf8101c8790 --- /dev/null +++ b/tests/interp/gcode-homing/joint-pword/test.ngc @@ -0,0 +1,8 @@ +; G28.2 Pn / G28.3 Pn: home/unhome a single joint by its 0-based joint +; number (matching [JOINT_n] INI section numbering), instead of all joints. +; Bare G28.2/G28.3 (no P) are unaffected and still act on all joints. +g28.2 p1 ; home joint 1 only +g28.3 p0 ; unhome joint 0 only +g28.2 ; home all joints (unaffected by P support) +g28.3 ; unhome all joints (unaffected by P support) +m2 diff --git a/tests/interp/gcode-homing/joint-pword/test.sh b/tests/interp/gcode-homing/joint-pword/test.sh new file mode 100755 index 00000000000..f8e8981531b --- /dev/null +++ b/tests/interp/gcode-homing/joint-pword/test.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# G28.2 Pn / G28.3 Pn home/unhome a single joint (no INI flag needed -- +# independent of GCODE_HOMING, same as bare G28.2/G28.3). +rs274 -g test.ngc | awk '{$1=""; print}' | sed 's/-0\.0000/0.0000/g' +exit "${PIPESTATUS[0]}" diff --git a/tests/interp/gcode-homing/sequencing/checkresult b/tests/interp/gcode-homing/sequencing/checkresult new file mode 100755 index 00000000000..24dc9aa53e3 --- /dev/null +++ b/tests/interp/gcode-homing/sequencing/checkresult @@ -0,0 +1,2 @@ +#!/bin/sh +exit 0 # test failure is indicated by test.sh exit value diff --git a/tests/interp/gcode-homing/sequencing/sim.tbl b/tests/interp/gcode-homing/sequencing/sim.tbl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/interp/gcode-homing/sequencing/test-ui.py b/tests/interp/gcode-homing/sequencing/test-ui.py new file mode 100755 index 00000000000..b9068cf53d9 --- /dev/null +++ b/tests/interp/gcode-homing/sequencing/test-ui.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 + +""" +Regression test for the G28.2/G28.3 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. With NO_FORCE_HOMING unset (default, force homing required), a G28.3 Pn + that leaves the machine not fully homed must trip the pre-existing + NO_FORCE_HOMING gate (checked at commit 2a5eabf581, re-applied at this + sync point by commit 3d238dbb46) and block further MDI commands, exactly + as if never homed -- note this fires on the *current* homed state, not + on whether this particular G28.3 changed anything, so even a redundant + G28.3 Pn on an already-unhomed joint trips it as long as the machine + isn't fully homed. Recovery has to go through the classic direct + joint-home NML call (c.home(), the same one the GUI's Home button uses) + rather than MDI text, since that gate has no exemption for a homing + command that is itself submitted as MDI text. + 3. A G28.2/G28.3 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/unhome 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") + +# This G28.3 P1 leaves the machine not fully homed -- with the default +# NO_FORCE_HOMING=0, that must trip the gate: task_mode no longer reads the +# MDI/AUTO choice (determineMode() derives MANUAL from the traj mode being +# left at FREE), and further MDI motion is blocked. +c.mdi("G28.3 P1") +if not wait_idle(): + fail("G28.3 P1 did not settle") +poll() +if list(s.homed[:3]) == [1, 1, 1]: + fail("G28.3 P1 did not actually unhome joint 1") +if s.task_mode == mode_before: + fail("NO_FORCE_HOMING gate did not trip after G28.3 Pn left the machine not fully homed (task_mode still {})".format(s.task_mode)) +print("PASS: NO_FORCE_HOMING gate trips after G28.3 Pn leaves the machine not fully homed") + +c.mdi("G0 X1") +if not wait_idle(): + fail("gate-check MDI command did not settle") +poll() +if not near(s.position[0], 0.0): + fail("NO_FORCE_HOMING gate did not block MDI after G28.3 Pn left the machine not fully homed (X moved to {})".format(s.position[0])) +print("PASS: NO_FORCE_HOMING gate blocks MDI after G28.3 Pn leaves the machine not fully homed") + +# Recovery goes through the classic direct joint-home call (same path the +# GUI's Home button uses), not MDI text -- the gate has no exemption for a +# homing command submitted as MDI text. +c.home(1) +if not wait_homed([1, 1, 1]): + fail("recovery home of joint 1 did not complete") + +c.mode(linuxcnc.MODE_MDI) +time.sleep(0.2) +poll() +if s.task_mode != mode_before: + fail("task_mode did not recover to MDI after re-homing (still {})".format(s.task_mode)) + +c.mdi("G0 X1") +if not wait_idle(): + fail("post-recovery MDI command did not settle") +poll() +if not near(s.position[0], 1.0): + fail("MDI still blocked after recovering full homed state (X stayed at {})".format(s.position[0])) +print("PASS: MDI works again once fully homed") + +# Machine is fully homed again here, so this passes the gate and reaches +# the actual Pn validation, which must reject joint 99 without leaving +# task_mode stuck (regression test for the fix in 65447329e9). +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/gcode-homing/sequencing/test.ini b/tests/interp/gcode-homing/sequencing/test.ini new file mode 100644 index 00000000000..eb1ecd04ca9 --- /dev/null +++ b/tests/interp/gcode-homing/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/gcode-homing/sequencing/test.sh b/tests/interp/gcode-homing/sequencing/test.sh new file mode 100755 index 00000000000..a16f6fa8522 --- /dev/null +++ b/tests/interp/gcode-homing/sequencing/test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec linuxcnc -r test.ini