From f36c9bda6f1133bb8c06c13bc7159b1f251cda96 Mon Sep 17 00:00:00 2001 From: chabron94 Date: Sat, 13 Jun 2026 07:11:40 -0400 Subject: [PATCH 1/8] interp+canon: G28.2 (home) / G28.3 (unhome) G-codes Add two non-modal (group 0) G-codes to trigger the machine homing cycle from G-code, so a machine can reference / clear references from MDI or a program instead of only from the GUI: G28.2 run the homing cycle (all joints, in HOME_SEQUENCE order) G28.3 unhome (all joints) - interp: enum G_28_2/G_28_3, modal group 0, accepted in check_g_codes; convert_modal_0 -> convert_home_cycle (cutter-comp guard) - canon: HOME_CYCLE()/UNHOME_AXES() -> EMC_JOINT_HOME/UNHOME(joint=-1); saicanon/gcodemodule stubs Bare form only (no axis words). Useful for unattended power-up and for operators who prefer a typed reference command. --- src/emc/nml_intf/canon.hh | 5 +++++ src/emc/rs274ngc/gcodemodule.cc | 3 +++ src/emc/rs274ngc/interp_array.cc | 2 +- src/emc/rs274ngc/interp_check.cc | 1 + src/emc/rs274ngc/interp_convert.cc | 23 +++++++++++++++++++++++ src/emc/rs274ngc/interp_internal.hh | 2 ++ src/emc/rs274ngc/rs274ngc_interp.hh | 2 ++ src/emc/sai/saicanon.cc | 3 +++ src/emc/task/emccanon.cc | 15 +++++++++++++++ 9 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/emc/nml_intf/canon.hh b/src/emc/nml_intf/canon.hh index 93a7075f20f..a49b2fe1b5a 100644 --- a/src/emc/nml_intf/canon.hh +++ b/src/emc/nml_intf/canon.hh @@ -243,6 +243,11 @@ 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); + /* 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/rs274ngc/gcodemodule.cc b/src/emc/rs274ngc/gcodemodule.cc index a5a200ba5e8..576c1ea9504 100644 --- a/src/emc/rs274ngc/gcodemodule.cc +++ b/src/emc/rs274ngc/gcodemodule.cc @@ -432,6 +432,9 @@ void SELECT_PLANE(CANON_PLANE pl) { Py_XDECREF(result); } +void HOME_CYCLE(void) {} +void UNHOME_AXES(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..5778adb6b59 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)), diff --git a/src/emc/rs274ngc/interp_convert.cc b/src/emc/rs274ngc/interp_convert.cc index 8f8573cab2f..643f793d63b 100644 --- a/src/emc/rs274ngc/interp_convert.cc +++ b/src/emc/rs274ngc/interp_convert.cc @@ -3111,6 +3111,27 @@ 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 axis +words). Motion still enforces its own safety (idle / not on limits). +*/ +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"); + if (move == G_28_2) { + HOME_CYCLE(); + } else { + UNHOME_AXES(); + } + 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 @@ -4290,6 +4311,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..83ae27454db 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, 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/sai/saicanon.cc b/src/emc/sai/saicanon.cc index 23184d7f33a..053f18c359f 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 UNHOME_AXES(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..60d97a4f108 100644 --- a/src/emc/task/emccanon.cc +++ b/src/emc/task/emccanon.cc @@ -478,6 +478,21 @@ void SET_XY_ROTATION(double t) { canon.xy_rotation = t; } + +void HOME_CYCLE(void) +{ + auto msg = std::make_unique(); + msg->joint = -1; // -1 = all joints (HOME_SEQUENCE order) + interp_list.append(std::move(msg)); +} + +void UNHOME_AXES(void) +{ + auto msg = std::make_unique(); + msg->joint = -1; + interp_list.append(std::move(msg)); +} + void SET_G5X_OFFSET(int index, double x, double y, double z, double a, double b, double c, From 60b4aa34a8e2bec918e1eda74a6b426a4aa7c5e4 Mon Sep 17 00:00:00 2001 From: chabron94 Date: Sat, 13 Jun 2026 07:11:52 -0400 Subject: [PATCH 2/8] task: allow queued JOINT_HOME/UNHOME in emcTaskCheckPreconditions EMC_JOINT_HOME_TYPE and EMC_JOINT_UNHOME_TYPE were missing from emcTaskCheckPreconditions(), which is called for every command pulled off the interp_list. A homing command that arrives via the queue (e.g. from a G-code like G28.2, or any front-end that queues it) therefore hit the 'default' case and returned EMC_TASK_EXEC::ERROR - the command was dropped and never reached motion. Add both types returning WAITING_FOR_MOTION (drain prior motion, then home). Bug fix; independent of the G28.2/G28.3 codes (any queued home benefits). --- src/emc/task/emctaskmain.cc | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/emc/task/emctaskmain.cc b/src/emc/task/emctaskmain.cc index 9ea6607234a..02befc78cb9 100644 --- a/src/emc/task/emctaskmain.cc +++ b/src/emc/task/emctaskmain.cc @@ -1605,6 +1605,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) { From 75abd4fe102048ad2f17f26baf1f69c3eec7b1e7 Mon Sep 17 00:00:00 2001 From: chabron94 Date: Sat, 13 Jun 2026 07:11:53 -0400 Subject: [PATCH 3/8] motion: permit homing when idle (not only in free mode) EMCMOT_JOINT_HOME required motion_state == FREE. Allow it also when motion is otherwise idle (in position, queue empty) so a homing command issued from MDI or a program (G28.2) is honored, while still refusing to home mid-motion. No change when already in free mode. --- src/emc/motion/command.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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)) { From 86c58f6cc6a6b89d720caf21f77f50eba80a934b Mon Sep 17 00:00:00 2001 From: chabron94 Date: Tue, 16 Jun 2026 12:21:51 -0400 Subject: [PATCH 4/8] GCODE_HOMING: plain G28 references the machine first ([RS274NGC]GCODE_HOMING=1) With [RS274NGC]GCODE_HOMING=1 (default 0 = stock), a plain G28 runs the machine homing cycle before its natural return move whenever the machine is not already fully homed; a fully-homed machine sees a pure legacy G28 (return only). G30 and G28.2/G28.3 are unchanged. Lets a machine reference itself from MDI or from the top of a program (e.g. unattended power-up) instead of only from the GUI, while a homed machine pays nothing. Flow: interp convert_home (FEATURE_GCODE_HOMING + G_28) emits a new canon op HOME_CYCLE_IF_UNHOMED() before the waypoint/return moves. Canon queues EMC_JOINT_HOME carrying the EMC_HOME_ALL_IF_UNHOMED (-3) sentinel in its 'joint' field. Task resolves the sentinel at execution time: if all_homed() the home is dropped (the queued return alone = pure legacy G28), otherwise it issues the normal home-all (emcJointHome(-1)) - the same path G28.2 and the GUI Home-All already use. No new command field, no motion / homing.c / NML-layout change. Single-channel; built on the G28.2/G28.3 home-cycle G-codes. Verified at the interpreter (rs274 -i): GCODE_HOMING=1 plain G28 emits HOME_CYCLE_IF_UNHOMED() before the return traverse; G30 does not home; G28.2 still homes unconditionally; default-off G28 is bit-identical to stock (no homing emitted). Full build (interp/task/motion) clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/emc/nml_intf/canon.hh | 5 +++++ src/emc/nml_intf/emc_nml.hh | 7 +++++++ src/emc/rs274ngc/gcodemodule.cc | 1 + src/emc/rs274ngc/interp_convert.cc | 10 ++++++++++ src/emc/rs274ngc/interp_internal.hh | 5 +++++ src/emc/rs274ngc/rs274ngc_pre.cc | 2 ++ src/emc/sai/saicanon.cc | 1 + src/emc/task/emccanon.cc | 11 +++++++++++ src/emc/task/emctaskmain.cc | 9 ++++++++- 9 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/emc/nml_intf/canon.hh b/src/emc/nml_intf/canon.hh index a49b2fe1b5a..105775de61c 100644 --- a/src/emc/nml_intf/canon.hh +++ b/src/emc/nml_intf/canon.hh @@ -247,6 +247,11 @@ extern void SET_XY_ROTATION(double t); * (bare form = all joints). Maps to EMC_JOINT_HOME/UNHOME(-1). */ extern void HOME_CYCLE(void); extern void UNHOME_AXES(void); +/* 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 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 576c1ea9504..67fe4dd1714 100644 --- a/src/emc/rs274ngc/gcodemodule.cc +++ b/src/emc/rs274ngc/gcodemodule.cc @@ -434,6 +434,7 @@ void SELECT_PLANE(CANON_PLANE pl) { void HOME_CYCLE(void) {} void UNHOME_AXES(void) {} +void HOME_CYCLE_IF_UNHOMED(void) {} void SET_TRAVERSE_RATE(double rate) { maybe_new_line(); diff --git a/src/emc/rs274ngc/interp_convert.cc b/src/emc/rs274ngc/interp_convert.cc index 643f793d63b..f02a5f2eab0 100644 --- a/src/emc/rs274ngc/interp_convert.cc +++ b/src/emc/rs274ngc/interp_convert.cc @@ -3164,6 +3164,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 diff --git a/src/emc/rs274ngc/interp_internal.hh b/src/emc/rs274ngc/interp_internal.hh index 83ae27454db..f9cf803fd38 100644 --- a/src/emc/rs274ngc/interp_internal.hh +++ b/src/emc/rs274ngc/interp_internal.hh @@ -846,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_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 053f18c359f..eda53c32c57 100644 --- a/src/emc/sai/saicanon.cc +++ b/src/emc/sai/saicanon.cc @@ -115,6 +115,7 @@ void SET_XY_ROTATION(double t) { void HOME_CYCLE(void) { ECHO_WITH_ARGS(""); } void UNHOME_AXES(void) { ECHO_WITH_ARGS(""); } +void HOME_CYCLE_IF_UNHOMED(void) { ECHO_WITH_ARGS(""); } void SET_G5X_OFFSET(int index, double x, double y, double z, diff --git a/src/emc/task/emccanon.cc b/src/emc/task/emccanon.cc index 60d97a4f108..01ca24cfe4c 100644 --- a/src/emc/task/emccanon.cc +++ b/src/emc/task/emccanon.cc @@ -493,6 +493,17 @@ void UNHOME_AXES(void) 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) +{ + 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 02befc78cb9..23c1611d467 100644 --- a/src/emc/task/emctaskmain.cc +++ b/src/emc/task/emctaskmain.cc @@ -1676,7 +1676,14 @@ static int emcTaskIssueCommand(NMLmsg * cmd) case EMC_JOINT_HOME_TYPE: home_msg = reinterpret_cast(cmd); - retval = emcJointHome(home_msg->joint); + if (home_msg->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). + retval = all_homed() ? 0 : emcJointHome(-1); + } else { + retval = emcJointHome(home_msg->joint); + } break; case EMC_JOINT_UNHOME_TYPE: From adbcd95a71c6ed6753c8bc813f93507ae1e773b5 Mon Sep 17 00:00:00 2001 From: chabron94 Date: Tue, 16 Jun 2026 13:31:29 -0400 Subject: [PATCH 5/8] docs+tests: G28.2/G28.3 and GCODE_HOMING Document the G-code machine-homing feature and add interpreter-level regression tests for it. Docs: - g-code.adoc: new "G28.2, G28.3 Home, Unhome from G-code" section, a GCODE_HOMING note in the G28 section, and a quick-reference entry. - ini-config.adoc: GCODE_HOMING entry in the [RS274NGC] section. Tests (tests/interp/gcode-homing, rs274 canon-level): - homing-on: with [RS274NGC]GCODE_HOMING=1, a plain G28 emits HOME_CYCLE_IF_UNHOMED() before its return; G28.2 -> HOME_CYCLE(), G28.3 -> UNHOME_AXES(). - homing-off: default (flag off) a plain G28 emits no homing op, while G28.2/G28.3 still home/unhome (flag-independent). Verified: new tests pass; tests/interp + tests/abort = 86/86 (1 skipped), build clean. Co-authored-by: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/src/config/ini-config.adoc | 8 +++++ docs/src/gcode/g-code.adoc | 35 +++++++++++++++++++ tests/interp/gcode-homing/homing-off/expected | 19 ++++++++++ tests/interp/gcode-homing/homing-off/test.ngc | 6 ++++ tests/interp/gcode-homing/homing-off/test.sh | 5 +++ tests/interp/gcode-homing/homing-on/expected | 19 ++++++++++ tests/interp/gcode-homing/homing-on/test.ini | 5 +++ tests/interp/gcode-homing/homing-on/test.ngc | 8 +++++ tests/interp/gcode-homing/homing-on/test.sh | 3 ++ 9 files changed, 108 insertions(+) create mode 100644 tests/interp/gcode-homing/homing-off/expected create mode 100644 tests/interp/gcode-homing/homing-off/test.ngc create mode 100755 tests/interp/gcode-homing/homing-off/test.sh create mode 100644 tests/interp/gcode-homing/homing-on/expected create mode 100644 tests/interp/gcode-homing/homing-on/test.ini create mode 100644 tests/interp/gcode-homing/homing-on/test.ngc create mode 100755 tests/interp/gcode-homing/homing-on/test.sh 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..2f23f1eb1bc 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,40 @@ 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; they always act on all joints. + +* '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. + +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] +`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 + [[gcode:g30-g30.1]] == G30, G30.1 Go/Set Predefined Position(((G30 Go/Set Predefined Position))) 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]}" From 3d238dbb4634799ac4c9a799aa1095eccb0e0393 Mon Sep 17 00:00:00 2001 From: chabron94 Date: Thu, 2 Jul 2026 01:29:10 -0400 Subject: [PATCH 6/8] G28.2/G28.3: add Pn per-joint home/unhome, fix homing sequencing Implements the direction from PR #4172's review discussion instead of the axis-letter form originally sketched there: - Adds an optional Pn word to G28.2/G28.3 to home/unhome a single joint by its 0-based joint number (matching [JOINT_n] INI numbering), e.g. G28.2 P1. Bare G28.2/G28.3 (no P) are unchanged (home/unhome all joints). Reuses the joint field EMC_JOINT_HOME/EMC_JOINT_UNHOME already carry, so it needs no NML change and works identically on any kinematics -- exactly the primitive grandixximo's review comment argued for. The axis-letter form (G28.2 X) is deliberately NOT implemented: resolving an axis letter to a joint needs the kinematics coordinate map and isn't trivial even on trivkins (duplicate letters on gantries), and andypugh's review also objected that homing is a joint concept, not an axis one. G28.2/G28.3 needed adding to the P-word whitelist in interp_check.cc (checked against g_modes[GM_MODAL_0], since they are modal-group-0 codes like G10/G4, not motion-group codes). - Fixes the real gap grandixximo's review identified: do_homing() (control.c) only ever advances while motion is in FREE mode, so a home/unhome issued from a running program or MDI while in TELEOP/COORD would previously either be rejected by a motion-side guard or silently never progress. Task now sequences it properly: a new EMC_TASK_EXEC::WAITING_FOR_HOMING state (modeled on the existing WAITING_FOR_SPINDLE_ORIENTED state) saves the current trajectory mode, dips into FREE, waits for the actual per-joint .homing/.homed status to reach the expected end state, then restores the prior mode -- invisibly to the task-level MDI/AUTO/MANUAL state, the same principle multichannel-DESIGN.txt uses for the analogous per-channel-homing problem. If homing/unhoming does not reach the expected end state, the program aborts (execState = ERROR) rather than continuing unreferenced, and the machine is left in FREE for an operator to intervene from rather than snapped back to a mode an unhomed machine may not legally run coordinated motion in. HOME and UNHOME are not symmetric at the motion level: EMCMOT_JOINT_HOME is a genuine state machine (.homing goes true while running), but EMCMOT_JOINT_UNHOME (command.c) is synchronous -- set_unhomed() just clears .homed immediately, .homing is never touched. The sequencing wait branches accordingly: UNHOME checks the target .homed state directly, HOME waits for the full start-then-finish cycle. - Re-applies [TRAJ]NO_FORCE_HOMING at the point a home/unhome command already forces a sync, closing the hole Sigma1912 raised in the PR discussion (G28.3 mid-program followed by a move with no re-home). NO_FORCE_HOMING=0 already refuses to *start* MDI/AUTO on an unhomed machine, but only at program/MDI start, not per line, so this specific gap needed its own check -- at no cost to the motion path, since it only runs at a sync point the command already forces. - Fixes two bugs found in the process that predate this commit and affect the base G28.2/G28.3/GCODE_HOMING feature, not just Pn: * HOME_CYCLE()/UNHOME_AXES()/HOME_CYCLE_IF_UNHOMED() (emccanon.cc) never flushed pending chained motion segments before appending their own command. STRAIGHT_FEED/STRAIGHT_TRAVERSE buffer points for arc-blend lookahead and only reach interp_list on a flush, so a queued move immediately before a G28.2/G28.3/homing-G28 could silently execute AFTER the home instead of before it. Fixed by calling flush_segments() first in all five home/unhome canon functions (the three pre-existing ones too). * emcJointHome()/emcJointUnhome() (taskintf.cc) returned 0 (success) for an out-of-range joint number, so an invalid Pn would silently report success instead of an error. - Adds stub implementations of the two new canon calls to gcodemodule.cc (the Python gcode module bindings), the third canon backend alongside emccanon.cc and saicanon.cc. Validated live in headless sim: the exact rehoming-a-shared-joint use case Sigma1912 described (cold-start home-all, move, mid-program unhome one joint, rehome it, move again) completes cleanly with zero errors; a NO_FORCE_HOMING=0 config confirms an unreferenced move after an unhome is correctly blocked with the intended error message; a plain move-then-home-all program confirms the flush_segments() ordering fix. Interp-level regression (tests/interp/gcode-homing/*, tests/interp/rotation/g28) passes 4/4, including a new joint-pword test case for the Pn parsing. Signed-off-by: chabron94 --- src/emc/nml_intf/canon.hh | 5 + src/emc/nml_intf/emc.hh | 3 +- src/emc/rs274ngc/gcodemodule.cc | 2 + src/emc/rs274ngc/interp_check.cc | 2 + src/emc/rs274ngc/interp_convert.cc | 47 ++++- src/emc/sai/saicanon.cc | 2 + src/emc/task/emccanon.cc | 27 +++ src/emc/task/emctaskmain.cc | 186 +++++++++++++++++- src/emc/task/taskintf.cc | 11 +- .../interp/gcode-homing/joint-pword/expected | 19 ++ .../interp/gcode-homing/joint-pword/test.ngc | 8 + tests/interp/gcode-homing/joint-pword/test.sh | 5 + 12 files changed, 302 insertions(+), 15 deletions(-) create mode 100644 tests/interp/gcode-homing/joint-pword/expected create mode 100644 tests/interp/gcode-homing/joint-pword/test.ngc create mode 100755 tests/interp/gcode-homing/joint-pword/test.sh diff --git a/src/emc/nml_intf/canon.hh b/src/emc/nml_intf/canon.hh index 105775de61c..d167f1b1c04 100644 --- a/src/emc/nml_intf/canon.hh +++ b/src/emc/nml_intf/canon.hh @@ -247,6 +247,11 @@ extern void SET_XY_ROTATION(double t); * (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 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/rs274ngc/gcodemodule.cc b/src/emc/rs274ngc/gcodemodule.cc index 67fe4dd1714..5a1fca58c35 100644 --- a/src/emc/rs274ngc/gcodemodule.cc +++ b/src/emc/rs274ngc/gcodemodule.cc @@ -434,6 +434,8 @@ void SELECT_PLANE(CANON_PLANE pl) { 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) { diff --git a/src/emc/rs274ngc/interp_check.cc b/src/emc/rs274ngc/interp_check.cc index 5778adb6b59..1f00ba3d070 100644 --- a/src/emc/rs274ngc/interp_check.cc +++ b/src/emc/rs274ngc/interp_check.cc @@ -327,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 f02a5f2eab0..d78c1dab91a 100644 --- a/src/emc/rs274ngc/interp_convert.cc +++ b/src/emc/rs274ngc/interp_convert.cc @@ -3115,19 +3115,56 @@ Called by: convert_modal_0. 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 axis -words). Motion still enforces its own safety (idle / not on limits). +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*/, + 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) { - HOME_CYCLE(); + if (joint < 0) { + HOME_CYCLE(); + } else { + HOME_CYCLE_JOINT(joint); + } } else { - UNHOME_AXES(); + if (joint < 0) { + UNHOME_AXES(); + } else { + UNHOME_JOINT(joint); + } } return INTERP_OK; } diff --git a/src/emc/sai/saicanon.cc b/src/emc/sai/saicanon.cc index eda53c32c57..9ae66bb38ff 100644 --- a/src/emc/sai/saicanon.cc +++ b/src/emc/sai/saicanon.cc @@ -115,6 +115,8 @@ void SET_XY_ROTATION(double 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, diff --git a/src/emc/task/emccanon.cc b/src/emc/task/emccanon.cc index 01ca24cfe4c..5bdf6e47a66 100644 --- a/src/emc/task/emccanon.cc +++ b/src/emc/task/emccanon.cc @@ -481,6 +481,12 @@ void SET_XY_ROTATION(double 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)); @@ -488,17 +494,38 @@ void HOME_CYCLE(void) 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)); diff --git a/src/emc/task/emctaskmain.cc b/src/emc/task/emctaskmain.cc index 23c1611d467..79a500d53b4 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; @@ -1676,18 +1700,50 @@ static int emcTaskIssueCommand(NMLmsg * cmd) case EMC_JOINT_HOME_TYPE: home_msg = reinterpret_cast(cmd); - if (home_msg->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). - retval = all_homed() ? 0 : emcJointHome(-1); - } else { - 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); } 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); break; @@ -2520,6 +2576,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; @@ -2751,6 +2815,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/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]}" From 65447329e9da5f99db18b3ca24c285d8183f8a37 Mon Sep 17 00:00:00 2001 From: chabron94 Date: Thu, 2 Jul 2026 04:44:59 -0400 Subject: [PATCH 7/8] fix(g28): restore traj mode when G28.2/G28.3 rejects the request outright emcJointHome()/emcJointUnhome() can fail immediately (e.g. an invalid joint number from a bad Pn word) after the FREE-mode dip added for homing sequencing has already been applied. Since homing never starts in that case, the WAITING_FOR_HOMING poll that normally restores the prior traj mode never runs, leaving traj.mode stuck at FREE. Because determineMode() derives task.mode from traj.mode, this makes task.mode read as MANUAL indefinitely -- silently breaking all subsequent MDI and AUTO commands until the operator manually cycles mode again. Found via the same isolated/chained-style critical-review stress testing used on feat/rotary-tangent: G28.2 P followed by any other MDI command reproduced it every time, while a generic interpreter error (e.g. an out-of-range G-code) did not, confirming this was specific to the new homing-sequencing dip rather than general MDI error handling. Fix: check emcJointHome()/emcJointUnhome()'s return value immediately and undo the mode dip and homingWaiting flag right there if the request was rejected outright, instead of leaving them for a resolution path that will never run. Signed-off-by: chabron94 --- src/emc/task/emctaskmain.cc | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/emc/task/emctaskmain.cc b/src/emc/task/emctaskmain.cc index 79a500d53b4..5d209c86f06 100644 --- a/src/emc/task/emctaskmain.cc +++ b/src/emc/task/emctaskmain.cc @@ -1730,6 +1730,19 @@ static int emcTaskIssueCommand(NMLmsg * cmd) 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; @@ -1745,6 +1758,13 @@ static int emcTaskIssueCommand(NMLmsg * cmd) 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: From 9cb056effefbf0a66937a7015b07ed208c31afc4 Mon Sep 17 00:00:00 2001 From: chabron94 Date: Thu, 2 Jul 2026 05:20:07 -0400 Subject: [PATCH 8/8] docs+tests: document G28.2/G28.3 Pn, add sequencing and flush-order tests Docs (docs/src/gcode/g-code.adoc): the G28.2/G28.3 section still said "take no axis words; they always act on all joints", predating the Pn work in 3d238dbb46. Documents the Pn word (0-based joint number, matching [JOINT_n] INI numbering), examples, the NO_FORCE_HOMING gate re-check on unhome, and the invalid-joint error condition. Tests: the existing joint-pword test only exercises canon-call generation at the interpreter level (rs274 -g); nothing covered the actual task-level sequencing behavior added in 3d238dbb46/65447329e9, or the flush_segments() ordering fix (which needs the real emccanon.cc buffering, not reachable via the interp-only saicanon.cc backend rs274 uses). Adds two live headless tests using the DISPLAY-script pattern from writing-tests.adoc: - gcode-homing/sequencing: G28.2 Pn's mode dip is invisible at the task level; a G28.3 Pn that leaves the machine not fully homed trips the pre-existing NO_FORCE_HOMING gate and blocks further MDI (recovery goes through the classic c.home() NML call, since that gate has no exemption for a homing command submitted as MDI text); an invalid Pn is rejected without leaving task_mode stuck (regression test for the bug fixed in 65447329e9). - gcode-homing/flush-order: a queued move immediately before G28.2 must complete before the home, not after. Verified this test actually catches the regression by temporarily reverting the flush_segments() call in HOME_CYCLE_JOINT() and confirming it fails (X still at 0.008 when homed flips true), then restored the fix and confirmed it passes. Verified: tests/interp/gcode-homing (5/5) and the full tests/interp + tests/abort suite (89/89, 1 pre-existing skip) both pass. --- docs/src/gcode/g-code.adoc | 34 +++- .../gcode-homing/flush-order/checkresult | 2 + tests/interp/gcode-homing/flush-order/sim.tbl | 0 .../gcode-homing/flush-order/test-ui.py | 87 ++++++++++ .../interp/gcode-homing/flush-order/test.ini | 99 +++++++++++ .../interp/gcode-homing/flush-order/test.ngc | 6 + tests/interp/gcode-homing/flush-order/test.sh | 2 + .../gcode-homing/sequencing/checkresult | 2 + tests/interp/gcode-homing/sequencing/sim.tbl | 0 .../interp/gcode-homing/sequencing/test-ui.py | 158 ++++++++++++++++++ tests/interp/gcode-homing/sequencing/test.ini | 98 +++++++++++ tests/interp/gcode-homing/sequencing/test.sh | 2 + 12 files changed, 485 insertions(+), 5 deletions(-) create mode 100755 tests/interp/gcode-homing/flush-order/checkresult create mode 100644 tests/interp/gcode-homing/flush-order/sim.tbl create mode 100755 tests/interp/gcode-homing/flush-order/test-ui.py create mode 100644 tests/interp/gcode-homing/flush-order/test.ini create mode 100644 tests/interp/gcode-homing/flush-order/test.ngc create mode 100755 tests/interp/gcode-homing/flush-order/test.sh create mode 100755 tests/interp/gcode-homing/sequencing/checkresult create mode 100644 tests/interp/gcode-homing/sequencing/sim.tbl create mode 100755 tests/interp/gcode-homing/sequencing/test-ui.py create mode 100644 tests/interp/gcode-homing/sequencing/test.ini create mode 100755 tests/interp/gcode-homing/sequencing/test.sh diff --git a/docs/src/gcode/g-code.adoc b/docs/src/gcode/g-code.adoc index 2f23f1eb1bc..03aaf7d6883 100644 --- a/docs/src/gcode/g-code.adoc +++ b/docs/src/gcode/g-code.adoc @@ -1013,16 +1013,39 @@ off by default (stock behavior). See also `G28.2` below. 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; they always act on all joints. +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. -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. +.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 @@ -1031,6 +1054,7 @@ 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/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/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