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