diff --git a/docs/src/gcode/g-code.adoc b/docs/src/gcode/g-code.adoc index 4670688041d..87edf8035dd 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 from G-code |<> |Go to Predefined Position |<> |Spindle Synchronized Motion |<> |Rigid Tapping @@ -997,6 +998,48 @@ 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` and takes 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.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. On a synchronized (negative `HOME_SEQUENCE`) joint pair, + Pn on either joint homes both. + +.G28.2 Example Lines +[source,ngc] +---- +G28.2 (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] +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 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 8905ad05d13..c89f8835c98 100644 --- a/src/emc/motion/command.c +++ b/src/emc/motion/command.c @@ -1494,9 +1494,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 (hal_get_bool(emcmot_hal_data->homing_inhibit)) { diff --git a/src/emc/motion/control.c b/src/emc/motion/control.c index 593c337e071..4c4326c19e5 100644 --- a/src/emc/motion/control.c +++ b/src/emc/motion/control.c @@ -2196,6 +2196,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 aad1d345730..2f198ad1122 100644 --- a/src/emc/motion/motion.h +++ b/src/emc/motion/motion.h @@ -667,6 +667,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. */ } emcmot_status_t; /********************************* diff --git a/src/emc/nml_intf/canon.hh b/src/emc/nml_intf/canon.hh index 31cbb3cb3c8..ecfd800db2f 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 0374a196a1f..d49609e299f 100644 --- a/src/emc/nml_intf/emc.cc +++ b/src/emc/nml_intf/emc.cc @@ -1850,6 +1850,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 b70fc4504a7..1739308b793 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 717de3ce619..b604ad5a915 100644 --- a/src/emc/nml_intf/emc_nml.hh +++ b/src/emc/nml_intf/emc_nml.hh @@ -1166,6 +1166,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 d58c87c637a..ffe1db74c5d 100644 --- a/src/emc/nml_intf/emcops.cc +++ b/src/emc/nml_intf/emcops.cc @@ -112,6 +112,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 a6114cf1808..564126e9a3f 100644 --- a/src/emc/rs274ngc/gcodemodule.cc +++ b/src/emc/rs274ngc/gcodemodule.cc @@ -707,6 +707,9 @@ void SELECT_PLANE(CANON_PLANE pl) { Py_XDECREF(result); } +void HOME_CYCLE(void) {} +void HOME_CYCLE_JOINT(int) {} + 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 e33cb4b565f..8e3ad61d726 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,-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 196f2772763..f95f29d0bd5 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)), @@ -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->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" " 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 638e87eb0c3..d9e9b78a789 100644 --- a/src/emc/rs274ngc/interp_convert.cc +++ b/src/emc/rs274ngc/interp_convert.cc @@ -3168,6 +3168,63 @@ 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 bare form homes all joints, in HOME_SEQUENCE order. + +An optional Pn word homes 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 '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. + +There is deliberately no G-code unhome. A G28.3 was part of the original +proposal and was dropped during review of PR #4172: neither reviewer could +name a use for it that a numbered parameter would not serve better, and it +was the one operation able to leave a running program on an unreferenced +machine -- the state behind the real-hardware failure Sigma1912 reported. +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 the bare form 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"); + + 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 must be a non-negative whole joint number" + " (omit P to home every joint)"); + joint = round_to_int(block->p_number); + } + + if (joint < 0) { + HOME_CYCLE(); + } else { + HOME_CYCLE_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 @@ -4347,6 +4404,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_internal.hh b/src/emc/rs274ngc/interp_internal.hh index 22268854211..864991b39e8 100644 --- a/src/emc/rs274ngc/interp_internal.hh +++ b/src/emc/rs274ngc/interp_internal.hh @@ -221,6 +221,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, diff --git a/src/emc/rs274ngc/rs274ngc_interp.hh b/src/emc/rs274ngc/rs274ngc_interp.hh index ebf77786fd9..2a388bdf6fc 100644 --- a/src/emc/rs274ngc/rs274ngc_interp.hh +++ b/src/emc/rs274ngc/rs274ngc_interp.hh @@ -323,6 +323,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/sai/saicanon.cc b/src/emc/sai/saicanon.cc index 8bbc3a6387c..0576ac4ae1f 100644 --- a/src/emc/sai/saicanon.cc +++ b/src/emc/sai/saicanon.cc @@ -113,6 +113,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 21815adad51..6d6ba6c6469 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 ff0978fe922..7bed7633e09 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; @@ -1605,6 +1638,12 @@ 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 + // 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) { @@ -1669,10 +1708,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; @@ -2527,6 +2610,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; @@ -2656,7 +2746,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 { @@ -2758,6 +2854,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 3d0b7bc05fd..c7caf03cbb6 100644 --- a/src/emc/task/taskintf.cc +++ b/src/emc/task/taskintf.cc @@ -795,8 +795,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. The negative sentinels (-1 all, -2 volatile) + // are an internal NML convention used by the GUI buttons, halui and + // linuxcncrsh; no operator types them, and G-code cannot express them + // at all -- convert_home_cycle() refuses a negative P word. Naming + // "-1 for all" here told a G28.2 user to try something the + // interpreter then rejected (PR #4172, Sigma1912). + emcOperatorError("Cannot home invalid joint %d (this machine has " + "joints 0..%d; omit the joint to home them all)", + joint, TrajConfig.Joints - 1); + return EMCMOT_COMM_ERROR_COMMAND; } emcmotCommand.command = EMCMOT_JOINT_HOME; @@ -807,8 +834,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; @@ -2147,6 +2186,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/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..5666c1534af --- /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()/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/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/immediate-unhome-mode/checkresult b/tests/interp/gcode-homing/immediate-unhome-mode/checkresult new file mode 100755 index 00000000000..24dc9aa53e3 --- /dev/null +++ b/tests/interp/gcode-homing/immediate-unhome-mode/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/immediate-unhome-mode/sim.tbl b/tests/interp/gcode-homing/immediate-unhome-mode/sim.tbl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/interp/gcode-homing/immediate-unhome-mode/test-ui.py b/tests/interp/gcode-homing/immediate-unhome-mode/test-ui.py new file mode 100755 index 00000000000..3a331b9f190 --- /dev/null +++ b/tests/interp/gcode-homing/immediate-unhome-mode/test-ui.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 + +""" +Regression test: an immediate (GUI-path) home or unhome must not disturb the +trajectory mode, and must not quietly gain permissions it never had. + +The G28.2 sequencing dips motion into FREE for the duration of a queued home +(do_homing() only advances there) and restores the previous mode afterwards, +from the EMC_TASK_EXEC::WAITING_FOR_HOMING poll. + +That poll is only ever reached through emcTaskCheckPostconditions(), which +task calls only for commands taken off the interp_list. The GUI's Home and +Unhome buttons, halui and linuxcncrsh all send *immediate* commands: those +reach emcTaskIssueCommand() but are never followed by +emcTaskCheckPostconditions(). An earlier revision of this branch dipped for +those too, which had two effects, both regressions against the pre-G28 +behaviour: + + 1. the mode was dipped to FREE and never restored, silently stranding the + machine in joint mode; and + 2. because the dip ran *before* the command was issued, an immediate unhome + started succeeding from teleop, where motion deliberately refuses it + ("must be in joint mode or disabled to unhome", the EMCMOT_JOINT_UNHOME + case in command.c). + +The sequencing is now scoped to the queued path, and with G28.3 dropped from +this PR an unhome has no queued path at all. Both are checked here, because +neither the scoping nor the drop is visible from the outside: motion does not +change the trajectory mode by itself for a single-joint home or unhome, so +any mode change observed here comes from task. +""" + +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 the stray dip destroyed. +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. Motion permits this when idle (see the +# EMCMOT_JOINT_HOME guard), so it must succeed -- but still without task +# touching the trajectory mode. +c.home(0) +if not wait_homed([1, 1, 1]): + fail("immediate home(0) did not complete") +time.sleep(0.8) +poll() +if s.motion_mode != mode_before: + fail( + "immediate home changed the trajectory mode {} -> {} and never " + "restored it".format(mode_before, s.motion_mode) + ) +print("PASS: an immediate home leaves the trajectory mode untouched") + +print("done! it all worked") +sys.exit(0) diff --git a/tests/interp/gcode-homing/immediate-unhome-mode/test.ini b/tests/interp/gcode-homing/immediate-unhome-mode/test.ini new file mode 100644 index 00000000000..eb1ecd04ca9 --- /dev/null +++ b/tests/interp/gcode-homing/immediate-unhome-mode/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/immediate-unhome-mode/test.sh b/tests/interp/gcode-homing/immediate-unhome-mode/test.sh new file mode 100755 index 00000000000..a16f6fa8522 --- /dev/null +++ b/tests/interp/gcode-homing/immediate-unhome-mode/test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec linuxcnc -r test.ini diff --git a/tests/interp/gcode-homing/invalid-pword/checkresult b/tests/interp/gcode-homing/invalid-pword/checkresult new file mode 100755 index 00000000000..24dc9aa53e3 --- /dev/null +++ b/tests/interp/gcode-homing/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/gcode-homing/invalid-pword/sim.tbl b/tests/interp/gcode-homing/invalid-pword/sim.tbl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/interp/gcode-homing/invalid-pword/test-ui.py b/tests/interp/gcode-homing/invalid-pword/test-ui.py new file mode 100755 index 00000000000..66529d5e237 --- /dev/null +++ b/tests/interp/gcode-homing/invalid-pword/test-ui.py @@ -0,0 +1,222 @@ +#!/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. +""" + +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 not advertise a value the interpreter then refuses. + # -1/-2 are the internal NML sentinels (all / volatile) used by the GUI, + # halui and linuxcncrsh; a G28.2 P word cannot carry them, so naming them + # here sends the operator to a second error (PR #4172, Sigma1912). + if "-1" in joined or "-2" in joined: + fail("{} error offers a negative sentinel a P word cannot express: " + "{!r}".format(cmd, msgs[0][:120])) + print("PASS: {} error offers no sentinel the P word cannot express".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))) + +# A negative P word is refused by the interpreter, and the message has to point +# at the spelling that does what the operator wanted rather than just say no. +drain_errors() +c.mdi("G28.2 P-1") +wait_idle() +time.sleep(0.3) +poll() +msgs = drain_errors() +joined = " ".join(msgs) +if not msgs: + fail("G28.2 P-1 was accepted silently") +if "non-negative" not in joined: + fail("G28.2 P-1 gave an unexpected error: {!r}".format(msgs[0][:120])) +if "omit" not in joined.lower(): + fail("G28.2 P-1 error does not point at the bare form: {!r}".format(msgs[0][:120])) +print("PASS: G28.2 P-1 refused, and the error names the bare form") +if list(s.homed[:3]) != [1, 1, 1]: + fail("G28.2 P-1 changed the homed state: {}".format(list(s.homed[:3]))) + +# 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/gcode-homing/invalid-pword/test.ini b/tests/interp/gcode-homing/invalid-pword/test.ini new file mode 100644 index 00000000000..c0b3db5b31a --- /dev/null +++ b/tests/interp/gcode-homing/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/gcode-homing/invalid-pword/test.sh b/tests/interp/gcode-homing/invalid-pword/test.sh new file mode 100755 index 00000000000..a16f6fa8522 --- /dev/null +++ b/tests/interp/gcode-homing/invalid-pword/test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec linuxcnc -r test.ini diff --git a/tests/interp/gcode-homing/joint-pword/expected b/tests/interp/gcode-homing/joint-pword/expected new file mode 100644 index 00000000000..4f2ebb97604 --- /dev/null +++ b/tests/interp/gcode-homing/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/gcode-homing/joint-pword/test.ngc b/tests/interp/gcode-homing/joint-pword/test.ngc new file mode 100644 index 00000000000..524a0fdb792 --- /dev/null +++ b/tests/interp/gcode-homing/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 all joints. +; Bare G28.2 (no P) is unaffected and still homes every joint. +g28.2 p1 ; home joint 1 only +g28.2 ; home 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..a0da0429302 --- /dev/null +++ b/tests/interp/gcode-homing/joint-pword/test.sh @@ -0,0 +1,4 @@ +#!/bin/bash +# G28.2 Pn homes a single joint (no INI flag needed, same as bare G28.2). +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..f2f5eefac56 --- /dev/null +++ b/tests/interp/gcode-homing/sequencing/test-ui.py @@ -0,0 +1,104 @@ +#!/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 passes the NO_FORCE_HOMING 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