From 36de38964b7a08cf093cb430ea857adb3cbe4f76 Mon Sep 17 00:00:00 2001 From: tzuohann <2057796+tzuohann@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:28:35 -0400 Subject: [PATCH 1/5] task: run from line -- nothing a skipped line produces leaves the queue (#246) A wait (M66, G38, M6 epilog, M61, user M-codes) inside a subroutine or remap on a skipped line sent the queued commands to motion before the skip discarded them, so skipped code half-executed. While skipping, discard at every call level and let a wait neither send nor wait. The skip ends before the first main-program line at or after the start line (this also fixes run from line 1). #<_task_skipping> lets remaps skip their own work. Co-Authored-By: Claude Opus 5.5 --- src/emc/rs274ngc/interp_namedparams.cc | 11 ++ src/emc/task/emctaskmain.cc | 130 ++++++++++-------- .../run-from-line-first/checkresult | 8 ++ .../run-from-line-first/mock-motion.hal | 3 + .../run-from-line-first/subs/m442.ngc | 7 + .../run-from-line-first/test-ui.py | 17 +++ .../run-from-line-first/test.ini | 103 ++++++++++++++ .../run-from-line-first/test.ngc | 3 + .../motion-logger/run-from-line-first/test.sh | 5 + .../run-from-line-wait/checkresult | 9 ++ .../run-from-line-wait/mock-motion.hal | 3 + .../run-from-line-wait/subs/m442.ngc | 7 + .../run-from-line-wait/test-ui.py | 17 +++ .../motion-logger/run-from-line-wait/test.ini | 103 ++++++++++++++ .../motion-logger/run-from-line-wait/test.ngc | 3 + .../motion-logger/run-from-line-wait/test.sh | 5 + 16 files changed, 380 insertions(+), 54 deletions(-) create mode 100755 tests/motion-logger/run-from-line-first/checkresult create mode 100644 tests/motion-logger/run-from-line-first/mock-motion.hal create mode 100644 tests/motion-logger/run-from-line-first/subs/m442.ngc create mode 100755 tests/motion-logger/run-from-line-first/test-ui.py create mode 100644 tests/motion-logger/run-from-line-first/test.ini create mode 100644 tests/motion-logger/run-from-line-first/test.ngc create mode 100755 tests/motion-logger/run-from-line-first/test.sh create mode 100755 tests/motion-logger/run-from-line-wait/checkresult create mode 100644 tests/motion-logger/run-from-line-wait/mock-motion.hal create mode 100644 tests/motion-logger/run-from-line-wait/subs/m442.ngc create mode 100755 tests/motion-logger/run-from-line-wait/test-ui.py create mode 100644 tests/motion-logger/run-from-line-wait/test.ini create mode 100644 tests/motion-logger/run-from-line-wait/test.ngc create mode 100755 tests/motion-logger/run-from-line-wait/test.sh diff --git a/src/emc/rs274ngc/interp_namedparams.cc b/src/emc/rs274ngc/interp_namedparams.cc index b00c7206c2d..8e230b76ade 100644 --- a/src/emc/rs274ngc/interp_namedparams.cc +++ b/src/emc/rs274ngc/interp_namedparams.cc @@ -55,6 +55,10 @@ namespace bp = boost::python; using namespace linuxcnc; +// 1 while task skips lines before a run-from-line start line (issue #246). +// Only milltask sets it; the preview and standalone rs274 leave it 0. +int _task_skipping = 0; + enum predefined_named_parameters { NP_LINE, NP_MOTION_MODE, @@ -114,6 +118,7 @@ enum predefined_named_parameters { NP_SELECTED_TOOL, NP_VALUE_RETURNED, NP_TASK, + NP_TASK_SKIPPING, }; /****************************************************************************/ @@ -859,6 +864,11 @@ int Interp::lookup_named_param(const char *nameBuf, *value = _task; break; + case NP_TASK_SKIPPING: + // 1 while task skips lines before a run-from-line start line (issue #246) + *value = _task_skipping; + break; + default: ERS(_("BUG: lookup_named_param(%s): unhandled index=%fn"), nameBuf,index); @@ -1031,6 +1041,7 @@ int Interp::init_named_parameters() // predicate: 1 in milltask instance, 0 in UI - control preview behaviour init_readonly_param("_task", NP_TASK, PA_USE_LOOKUP); + init_readonly_param("_task_skipping", NP_TASK_SKIPPING, PA_USE_LOOKUP); // debugging aids init_readonly_param("_call_level", NP_CALL_LEVEL, PA_USE_LOOKUP); diff --git a/src/emc/task/emctaskmain.cc b/src/emc/task/emctaskmain.cc index 96fa4951eab..01792e1badc 100644 --- a/src/emc/task/emctaskmain.cc +++ b/src/emc/task/emctaskmain.cc @@ -474,6 +474,43 @@ extern void emcTaskQueueTaskPlanSynchCmd(); static EMC_TASK_INTERP interpResumeState = EMC_TASK_INTERP::IDLE; static int programStartLine = 0; // which line to run program from +// RUN FROM LINE (issue #246): 1 from an AUTO_RUN with a start line until the +// skip ends. While it is set, nothing a skipped line produces -- at any call +// level -- may leave interp_list: a queue-busting wait inside a remap or +// subroutine used to send the queued commands to the machine before the skip +// discarded them, so skipped code half-executed. Read by the interpreter as +// #<_task_skipping> so a remap can skip its own side effects. +extern int _task_skipping; // defined in the interpreter library + +static void skip_end(void) +{ + if (!_task_skipping) return; + _task_skipping = 0; + programStartLine = 0; +} + +static void skip_reset(void) +{ + _task_skipping = 0; +} + +// Discard what a skipped line queued: report operator messages (as the skip +// always did), then clear. +static void skip_discard(void) +{ + while (interp_list.len() > 0) { + auto cmd = interp_list.get(); + if (!cmd) break; + switch (cmd->_type) { + case EMC_OPERATOR_ERROR_TYPE: + emcOperatorError("%s", static_cast(cmd.get())->error); + break; + default: + break; + } + } + interp_list.clear(); +} // how long the interp list can be int stepping = 0; @@ -495,48 +532,13 @@ static NML_INTERP_LIST mdi_input_queue; #define MAX_MDI_QUEUE 10 static int max_mdi_queued_commands = MAX_MDI_QUEUE; -/* - checkInterpList(NML_INTERP_LIST *il, EMC_STAT *stat) takes a pointer - to an interpreter list and a pointer to the EMC status, pops each NML - message off the list, and checks it against limits, resource availability, - etc. in the status. - - It returns 0 if all messages check out, -1 if any of them fail. If one - fails, the rest of the list is not checked. - */ -static int checkInterpList(NML_INTERP_LIST * il, EMC_STAT * /*stat*/) -{ - while (il->len() > 0) { - auto cmd = il->get(); - - switch (cmd->_type) { - - case EMC_OPERATOR_ERROR_TYPE: { - auto error_msg = static_cast(cmd.get()); - emcOperatorError("%s", error_msg->error); - break; - } - - //FIXME: there was limit checking tests below, see if they were needed - case EMC_TRAJ_LINEAR_MOVE_TYPE: - break; - - case EMC_TRAJ_CIRCULAR_MOVE_TYPE: - break; - - default: - break; - } - } - - return 0; -} extern int emcTaskMopup(); void readahead_reading(void) { int readRetval; int execRetval; + int skipYield = 0; // a wait was met while skipping: stop reading this cycle if (interp_list.len() <= emc_task_interp_max_len) { int count = 0; @@ -582,6 +584,25 @@ void readahead_reading(void) emcTaskPlanCommand((char *) &emcStatus->task. command); + // RUN FROM LINE: the skip ends before the first + // main-program line at or after the start line runs -- + // also when the line before it never ran at level 0 + // (start line 1, a %-headed file). + if (_task_skipping && programStartLine > 0 + && emcTaskPlanLevel() == 0 + && emcStatus->task.readLine >= programStartLine) { + CANON_UPDATE_END_POINT(emcStatus->motion.traj.actualPosition.tran.x, + emcStatus->motion.traj.actualPosition.tran.y, + emcStatus->motion.traj.actualPosition.tran.z, + emcStatus->motion.traj.actualPosition.a, + emcStatus->motion.traj.actualPosition.b, + emcStatus->motion.traj.actualPosition.c, + emcStatus->motion.traj.actualPosition.u, + emcStatus->motion.traj.actualPosition.v, + emcStatus->motion.traj.actualPosition.w); + emcTaskPlanSynch(); + skip_end(); + } // and execute it execRetval = emcTaskPlanExecute(NULL); // line number may need update after @@ -598,6 +619,12 @@ void readahead_reading(void) || execRetval == INTERP_EXIT ) { emcStatus->task.interpState = EMC_TASK_INTERP::WAITING; + } else if (execRetval == INTERP_EXECUTE_FINISH && _task_skipping) { + // RUN FROM LINE: a wait inside skipped code must not send + // the skipped commands to the machine. Discard below, do + // not wait, re-read nothing; one wait per task cycle. + emcStatus->task.input_timeout = 0; + skipYield = 1; } else if (execRetval == INTERP_EXECUTE_FINISH) { // INTERP_EXECUTE_FINISH signifies // that no more reading should be done until @@ -621,24 +648,13 @@ void readahead_reading(void) // throw the results away if we're supposed to // read // through it - if ( programStartLine != 0 && - emcTaskPlanLevel() == 0 && - ( programStartLine < 0 || - emcTaskPlanLine() <= programStartLine )) { - // we're stepping over lines, so check them - // for - // limits, etc. and clear then out - if (0 != checkInterpList(&interp_list, - emcStatus)) { - // problem with actions, so do same as we - // did - // for a bad read from emcTaskPlanRead() - // above - emcStatus->task.interpState = - EMC_TASK_INTERP::WAITING; - } - // and clear it regardless - interp_list.clear(); + if (_task_skipping) { + // RUN FROM LINE: every block executed while skipping, + // at any call level, is discarded before anything can + // send it (formerly: only main-program lines, after the + // whole line -- remap/subroutine bodies leaked out at + // their waits). Operator messages are still shown. + skip_discard(); } if (emcStatus->task.readLine < programStartLine && @@ -663,10 +679,12 @@ void readahead_reading(void) // reset programStartLine so we don't fall into our stepping routines // if we happen to execute lines before the current point later (due to subroutines). programStartLine = 0; + skip_end(); } } if (count++ < emc_task_interp_max_len + && !skipYield && emcStatus->task.interpState == EMC_TASK_INTERP::READING && interp_list.len() <= emc_task_interp_max_len * 2/3) { goto interpret_again; @@ -777,6 +795,7 @@ void readahead_waiting(void) emcStatus->task.interpState = EMC_TASK_INTERP::IDLE; } emcStatus->task.readLine = 0; + skip_reset(); // run over: never carry "skipping" into MDI or the next run } else { // still executing } @@ -2188,6 +2207,7 @@ static int emcTaskIssueCommand(NMLmsg * cmd) case EMC_TASK_ABORT_TYPE: // abort everything + skip_reset(); emcTaskAbort(); // KLUDGE call motion abort before state restore to make absolutely sure no // stray restore commands make it down to motion @@ -2440,6 +2460,8 @@ static int emcTaskIssueCommand(NMLmsg * cmd) } run_msg = reinterpret_cast(cmd); programStartLine = run_msg->line; + skip_reset(); + _task_skipping = (programStartLine != 0); emcStatus->task.interpState = EMC_TASK_INTERP::READING; emcStatus->task.task_paused = 0; retval = 0; diff --git a/tests/motion-logger/run-from-line-first/checkresult b/tests/motion-logger/run-from-line-first/checkresult new file mode 100755 index 00000000000..2a7ec968c72 --- /dev/null +++ b/tests/motion-logger/run-from-line-first/checkresult @@ -0,0 +1,8 @@ +#!/bin/sh +# Run from line 1: nothing is skipped, the M442 call after it runs in full. +cd "$(dirname "$1")" || exit 1 +fail=0 +grep -q "z=1," out.motion-logger || { echo "FAIL: G0 Z1 did not run"; fail=1; } +grep -q "SET_LINE x=7," out.motion-logger || { echo "FAIL: X7 of the called subroutine did not run"; fail=1; } +grep -q "y=7," out.motion-logger || { echo "FAIL: Y7 of the called subroutine did not run"; fail=1; } +exit $fail diff --git a/tests/motion-logger/run-from-line-first/mock-motion.hal b/tests/motion-logger/run-from-line-first/mock-motion.hal new file mode 100644 index 00000000000..386f1d656a2 --- /dev/null +++ b/tests/motion-logger/run-from-line-first/mock-motion.hal @@ -0,0 +1,3 @@ +loadusr -W motion-logger out.motion-logger +setp iocontrol.0.emc-enable-in 1 + diff --git a/tests/motion-logger/run-from-line-first/subs/m442.ngc b/tests/motion-logger/run-from-line-first/subs/m442.ngc new file mode 100644 index 00000000000..bbb7b6f58f0 --- /dev/null +++ b/tests/motion-logger/run-from-line-first/subs/m442.ngc @@ -0,0 +1,7 @@ +o sub +(a skipped call: one move before a wait, one after) +G0 X7 +M66 P0 L0 +G0 Y7 +o endsub +m2 diff --git a/tests/motion-logger/run-from-line-first/test-ui.py b/tests/motion-logger/run-from-line-first/test-ui.py new file mode 100755 index 00000000000..a16aed0ec25 --- /dev/null +++ b/tests/motion-logger/run-from-line-first/test-ui.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +# Run-from-line with a remapped code whose subroutine waits (M66): issue #246. +import linuxcnc, sys, time +c = linuxcnc.command(); s = linuxcnc.stat(); e = linuxcnc.error_channel() +c.state(linuxcnc.STATE_ESTOP_RESET) +c.state(linuxcnc.STATE_ON) +c.mode(linuxcnc.MODE_AUTO) +c.program_open('test.ngc') +c.auto(linuxcnc.AUTO_RUN, 1) +c.wait_complete() +start = time.time() +while time.time() - start < 10: + s.poll() + if s.interp_state == linuxcnc.INTERP_IDLE and s.exec_state == linuxcnc.EXEC_DONE: + break + time.sleep(0.1) +sys.exit(0) diff --git a/tests/motion-logger/run-from-line-first/test.ini b/tests/motion-logger/run-from-line-first/test.ini new file mode 100644 index 00000000000..674564ea354 --- /dev/null +++ b/tests/motion-logger/run-from-line-first/test.ini @@ -0,0 +1,103 @@ +[EMC] +VERSION = 1.1 +DEBUG = 0xffffffff +RCS_DEBUG = 0x05400f01 + +[DISPLAY] +DISPLAY = ./test-ui.py + +[TASK] +TASK = milltask +CYCLE_TIME = 0.001 + +[RS274NGC] +SUBROUTINE_PATH = ./subs +LOG_LEVEL = 999 +REMAP=M442 modalgroup=5 ngc=m442 + +[EMCMOT] +#EMCMOT = motmod +COMM_TIMEOUT = 4.0 +BASE_PERIOD = 0 +SERVO_PERIOD = 1000000 + +[EMCIO] +TOOL_TABLE = simpockets.tbl +TOOL_CHANGE_QUILL_UP = 1 +RANDOM_TOOLCHANGER = 0 + +[HAL] +HALFILE = mock-motion.hal +#POSTGUI_HALFILE = postgui.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 = 120 +MAX_LINEAR_VELOCITY = 400 + +[KINS] +KINEMATICS = trivkins +JOINTS = 3 + +[AXIS_X] +MIN_LIMIT = -40.0 +MAX_LIMIT = 40.0 +MAX_VELOCITY = 400 +MAX_ACCELERATION = 1000.0 + +[JOINT_0] +TYPE = LINEAR +HOME = 0.000 +MAX_VELOCITY = 400 +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 + +[AXIS_Y] +MIN_LIMIT = -40.0 +MAX_LIMIT = 40.0 +MAX_VELOCITY = 400 +MAX_ACCELERATION = 1000.0 + +[JOINT_1] +TYPE = LINEAR +HOME = 0.000 +MAX_VELOCITY = 400 +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 + +[AXIS_Z] +MIN_LIMIT = -40 +MAX_LIMIT = 40 +MAX_VELOCITY = 400 +MAX_ACCELERATION = 1000.0 + +[JOINT_2] +TYPE = LINEAR +HOME = 0.0 +MAX_VELOCITY = 400 +MAX_ACCELERATION = 1000.0 +BACKLASH = 0.000 +INPUT_SCALE = 4000 +OUTPUT_SCALE = 1.000 +MIN_LIMIT = -40 +MAX_LIMIT = 40 +FERROR = 0.050 +MIN_FERROR = 0.010 + diff --git a/tests/motion-logger/run-from-line-first/test.ngc b/tests/motion-logger/run-from-line-first/test.ngc new file mode 100644 index 00000000000..e3839c72109 --- /dev/null +++ b/tests/motion-logger/run-from-line-first/test.ngc @@ -0,0 +1,3 @@ +G0 Z1 +M442 (called after the start line: must run in full) +M2 diff --git a/tests/motion-logger/run-from-line-first/test.sh b/tests/motion-logger/run-from-line-first/test.sh new file mode 100755 index 00000000000..21395bb5efd --- /dev/null +++ b/tests/motion-logger/run-from-line-first/test.sh @@ -0,0 +1,5 @@ +#!/bin/bash -e + +rm -f out.motion-logger + +linuxcnc -r test.ini diff --git a/tests/motion-logger/run-from-line-wait/checkresult b/tests/motion-logger/run-from-line-wait/checkresult new file mode 100755 index 00000000000..5b4c397de37 --- /dev/null +++ b/tests/motion-logger/run-from-line-wait/checkresult @@ -0,0 +1,9 @@ +#!/bin/sh +# Run from line 2: the M442 call on line 1 is skipped. Before the fix the M66 in its +# subroutine sent the queued X7 move to motion; the Y7 after it was dropped. +cd "$(dirname "$1")" || exit 1 +fail=0 +grep -q "SET_LINE x=7," out.motion-logger && { echo "FAIL: X7 from the skipped subroutine reached motion"; fail=1; } +grep -q "y=7," out.motion-logger && { echo "FAIL: Y7 from the skipped subroutine reached motion"; fail=1; } +grep -q "z=1," out.motion-logger || { echo "FAIL: the start line (G0 Z1) did not run"; fail=1; } +exit $fail diff --git a/tests/motion-logger/run-from-line-wait/mock-motion.hal b/tests/motion-logger/run-from-line-wait/mock-motion.hal new file mode 100644 index 00000000000..386f1d656a2 --- /dev/null +++ b/tests/motion-logger/run-from-line-wait/mock-motion.hal @@ -0,0 +1,3 @@ +loadusr -W motion-logger out.motion-logger +setp iocontrol.0.emc-enable-in 1 + diff --git a/tests/motion-logger/run-from-line-wait/subs/m442.ngc b/tests/motion-logger/run-from-line-wait/subs/m442.ngc new file mode 100644 index 00000000000..bbb7b6f58f0 --- /dev/null +++ b/tests/motion-logger/run-from-line-wait/subs/m442.ngc @@ -0,0 +1,7 @@ +o sub +(a skipped call: one move before a wait, one after) +G0 X7 +M66 P0 L0 +G0 Y7 +o endsub +m2 diff --git a/tests/motion-logger/run-from-line-wait/test-ui.py b/tests/motion-logger/run-from-line-wait/test-ui.py new file mode 100755 index 00000000000..04a0afa478e --- /dev/null +++ b/tests/motion-logger/run-from-line-wait/test-ui.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +# Run-from-line with a remapped code whose subroutine waits (M66): issue #246. +import linuxcnc, sys, time +c = linuxcnc.command(); s = linuxcnc.stat(); e = linuxcnc.error_channel() +c.state(linuxcnc.STATE_ESTOP_RESET) +c.state(linuxcnc.STATE_ON) +c.mode(linuxcnc.MODE_AUTO) +c.program_open('test.ngc') +c.auto(linuxcnc.AUTO_RUN, 2) +c.wait_complete() +start = time.time() +while time.time() - start < 10: + s.poll() + if s.interp_state == linuxcnc.INTERP_IDLE and s.exec_state == linuxcnc.EXEC_DONE: + break + time.sleep(0.1) +sys.exit(0) diff --git a/tests/motion-logger/run-from-line-wait/test.ini b/tests/motion-logger/run-from-line-wait/test.ini new file mode 100644 index 00000000000..674564ea354 --- /dev/null +++ b/tests/motion-logger/run-from-line-wait/test.ini @@ -0,0 +1,103 @@ +[EMC] +VERSION = 1.1 +DEBUG = 0xffffffff +RCS_DEBUG = 0x05400f01 + +[DISPLAY] +DISPLAY = ./test-ui.py + +[TASK] +TASK = milltask +CYCLE_TIME = 0.001 + +[RS274NGC] +SUBROUTINE_PATH = ./subs +LOG_LEVEL = 999 +REMAP=M442 modalgroup=5 ngc=m442 + +[EMCMOT] +#EMCMOT = motmod +COMM_TIMEOUT = 4.0 +BASE_PERIOD = 0 +SERVO_PERIOD = 1000000 + +[EMCIO] +TOOL_TABLE = simpockets.tbl +TOOL_CHANGE_QUILL_UP = 1 +RANDOM_TOOLCHANGER = 0 + +[HAL] +HALFILE = mock-motion.hal +#POSTGUI_HALFILE = postgui.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 = 120 +MAX_LINEAR_VELOCITY = 400 + +[KINS] +KINEMATICS = trivkins +JOINTS = 3 + +[AXIS_X] +MIN_LIMIT = -40.0 +MAX_LIMIT = 40.0 +MAX_VELOCITY = 400 +MAX_ACCELERATION = 1000.0 + +[JOINT_0] +TYPE = LINEAR +HOME = 0.000 +MAX_VELOCITY = 400 +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 + +[AXIS_Y] +MIN_LIMIT = -40.0 +MAX_LIMIT = 40.0 +MAX_VELOCITY = 400 +MAX_ACCELERATION = 1000.0 + +[JOINT_1] +TYPE = LINEAR +HOME = 0.000 +MAX_VELOCITY = 400 +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 + +[AXIS_Z] +MIN_LIMIT = -40 +MAX_LIMIT = 40 +MAX_VELOCITY = 400 +MAX_ACCELERATION = 1000.0 + +[JOINT_2] +TYPE = LINEAR +HOME = 0.0 +MAX_VELOCITY = 400 +MAX_ACCELERATION = 1000.0 +BACKLASH = 0.000 +INPUT_SCALE = 4000 +OUTPUT_SCALE = 1.000 +MIN_LIMIT = -40 +MAX_LIMIT = 40 +FERROR = 0.050 +MIN_FERROR = 0.010 + diff --git a/tests/motion-logger/run-from-line-wait/test.ngc b/tests/motion-logger/run-from-line-wait/test.ngc new file mode 100644 index 00000000000..652950259e9 --- /dev/null +++ b/tests/motion-logger/run-from-line-wait/test.ngc @@ -0,0 +1,3 @@ +M442 (called on a line before the start line: must do nothing) +G0 Z1 (START LINE) +M2 diff --git a/tests/motion-logger/run-from-line-wait/test.sh b/tests/motion-logger/run-from-line-wait/test.sh new file mode 100755 index 00000000000..21395bb5efd --- /dev/null +++ b/tests/motion-logger/run-from-line-wait/test.sh @@ -0,0 +1,5 @@ +#!/bin/bash -e + +rm -f out.motion-logger + +linuxcnc -r test.ini From 9a067946750a55cc4df14d2cd38e8743c18c1b03 Mon Sep 17 00:00:00 2001 From: tzuohann <2057796+tzuohann@users.noreply.github.com> Date: Sun, 11 Oct 2026 21:45:23 -0400 Subject: [PATCH 2/5] tests: remap/introspect lists the new read-only #<_task_skipping> Co-Authored-By: Claude Opus 5.5 --- tests/remap/introspect/expected | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/remap/introspect/expected b/tests/remap/introspect/expected index 2f33b4bbe08..4c4a0daf2c4 100644 --- a/tests/remap/introspect/expected +++ b/tests/remap/introspect/expected @@ -29,8 +29,8 @@ speed= 3000.0 global parameter set in test.ngc: 47.11 parameter set via test.ini: 3.14159 locals: ['a_new_local'] -globals: ['_a', '_a_global_set_in_test_dot_ngc', '_a_new_global', '_abs_a', '_abs_b', '_abs_c', '_abs_u', '_abs_v', '_abs_w', '_abs_x', '_abs_y', '_abs_z', '_absolute', '_adaptive_feed', '_b', '_c', '_call_level', '_ccomp', '_coord_system', '_current_pocket', '_current_tool', '_feed', '_feed_hold', '_feed_override', '_flood', '_ijk_absolute_mode', '_imperial', '_incremental', '_ini[example]variable', '_inverse_time', '_kins_type', '_lathe_diameter_mode', '_lathe_radius_mode', '_line', '_metric', '_metric_machine', '_mist', '_motion_mode', '_plane', '_remap_level', '_retract_old_z', '_retract_r_plane', '_rpm', '_selected_pocket', '_selected_tool', '_speed_override', '_spindle_css_mode', '_spindle_cw', '_spindle_on', '_spindle_rpm_mode', '_task', '_tool_offset', '_u', '_units_per_minute', '_units_per_rev', '_v', '_value', '_value_returned', '_vmajor', '_vminor', '_w', '_x', '_y', '_z', 'foo'] -params(): ['a_new_local', '_a', '_a_global_set_in_test_dot_ngc', '_a_new_global', '_abs_a', '_abs_b', '_abs_c', '_abs_u', '_abs_v', '_abs_w', '_abs_x', '_abs_y', '_abs_z', '_absolute', '_adaptive_feed', '_b', '_c', '_call_level', '_ccomp', '_coord_system', '_current_pocket', '_current_tool', '_feed', '_feed_hold', '_feed_override', '_flood', '_ijk_absolute_mode', '_imperial', '_incremental', '_ini[example]variable', '_inverse_time', '_kins_type', '_lathe_diameter_mode', '_lathe_radius_mode', '_line', '_metric', '_metric_machine', '_mist', '_motion_mode', '_plane', '_remap_level', '_retract_old_z', '_retract_r_plane', '_rpm', '_selected_pocket', '_selected_tool', '_speed_override', '_spindle_css_mode', '_spindle_cw', '_spindle_on', '_spindle_rpm_mode', '_task', '_tool_offset', '_u', '_units_per_minute', '_units_per_rev', '_v', '_value', '_value_returned', '_vmajor', '_vminor', '_w', '_x', '_y', '_z', 'foo'] +globals: ['_a', '_a_global_set_in_test_dot_ngc', '_a_new_global', '_abs_a', '_abs_b', '_abs_c', '_abs_u', '_abs_v', '_abs_w', '_abs_x', '_abs_y', '_abs_z', '_absolute', '_adaptive_feed', '_b', '_c', '_call_level', '_ccomp', '_coord_system', '_current_pocket', '_current_tool', '_feed', '_feed_hold', '_feed_override', '_flood', '_ijk_absolute_mode', '_imperial', '_incremental', '_ini[example]variable', '_inverse_time', '_kins_type', '_lathe_diameter_mode', '_lathe_radius_mode', '_line', '_metric', '_metric_machine', '_mist', '_motion_mode', '_plane', '_remap_level', '_retract_old_z', '_retract_r_plane', '_rpm', '_selected_pocket', '_selected_tool', '_speed_override', '_spindle_css_mode', '_spindle_cw', '_spindle_on', '_spindle_rpm_mode', '_task', '_task_skipping', '_tool_offset', '_u', '_units_per_minute', '_units_per_rev', '_v', '_value', '_value_returned', '_vmajor', '_vminor', '_w', '_x', '_y', '_z', 'foo'] +params(): ['a_new_local', '_a', '_a_global_set_in_test_dot_ngc', '_a_new_global', '_abs_a', '_abs_b', '_abs_c', '_abs_u', '_abs_v', '_abs_w', '_abs_x', '_abs_y', '_abs_z', '_absolute', '_adaptive_feed', '_b', '_c', '_call_level', '_ccomp', '_coord_system', '_current_pocket', '_current_tool', '_feed', '_feed_hold', '_feed_override', '_flood', '_ijk_absolute_mode', '_imperial', '_incremental', '_ini[example]variable', '_inverse_time', '_kins_type', '_lathe_diameter_mode', '_lathe_radius_mode', '_line', '_metric', '_metric_machine', '_mist', '_motion_mode', '_plane', '_remap_level', '_retract_old_z', '_retract_r_plane', '_rpm', '_selected_pocket', '_selected_tool', '_speed_override', '_spindle_css_mode', '_spindle_cw', '_spindle_on', '_spindle_rpm_mode', '_task', '_task_skipping', '_tool_offset', '_u', '_units_per_minute', '_units_per_rev', '_v', '_value', '_value_returned', '_vmajor', '_vminor', '_w', '_x', '_y', '_z', 'foo'] 14 N..... MESSAGE(" after introspect: return value=2.718280 call_level= 0.000000") 15 N..... SET_G5X_OFFSET(1, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000) 16 N..... SET_XY_ROTATION(0.0000) From 348fa2e8fa869a80570ee0e180721db6c7751923 Mon Sep 17 00:00:00 2001 From: tzuohann <2057796+tzuohann@users.noreply.github.com> Date: Sun, 11 Oct 2026 22:20:00 -0400 Subject: [PATCH 3/5] tests: ignore the run-from-line tests' out.motion-logger, as mountaindew does Co-Authored-By: Claude Opus 5.5 --- tests/motion-logger/run-from-line-first/.gitignore | 3 +++ tests/motion-logger/run-from-line-wait/.gitignore | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 tests/motion-logger/run-from-line-first/.gitignore create mode 100644 tests/motion-logger/run-from-line-wait/.gitignore diff --git a/tests/motion-logger/run-from-line-first/.gitignore b/tests/motion-logger/run-from-line-first/.gitignore new file mode 100644 index 00000000000..1a4dabbd2ff --- /dev/null +++ b/tests/motion-logger/run-from-line-first/.gitignore @@ -0,0 +1,3 @@ +out.motion-logger +rs274ngc.var +rs274ngc.var.bak diff --git a/tests/motion-logger/run-from-line-wait/.gitignore b/tests/motion-logger/run-from-line-wait/.gitignore new file mode 100644 index 00000000000..1a4dabbd2ff --- /dev/null +++ b/tests/motion-logger/run-from-line-wait/.gitignore @@ -0,0 +1,3 @@ +out.motion-logger +rs274ngc.var +rs274ngc.var.bak From c3bd72361e88f720baaf624c863fc475d844554e Mon Sep 17 00:00:00 2001 From: tzuohann <2057796+tzuohann@users.noreply.github.com> Date: Mon, 12 Oct 2026 09:21:27 -0400 Subject: [PATCH 4/5] run from line: skipped tool-probe M6 remaps do the bookkeeping only; document #<_task_skipping> A remapped M6 that probes tool length ran its G38 in the interpreter while task skipped the line; the probe is discarded, #5070 stays 0 in a fresh session, and the remap aborted the run. tool_probe_m6 (both stdglue copies) and qt_auto_probe_tool.ngc now return after the tool bookkeeping when #<_task_skipping> is set, before any unit switch, G49 or motion. #<_task_skipping> is documented as a "no side effects while skipping" switch for remaps only; it does not say what state the start line needs. Co-Authored-By: Claude Fable 5.1 --- configs/sim/woodpecker/python/stdglue.py | 19 +++++++++++++++++++ docs/src/gcode/overview.adoc | 8 ++++++++ docs/src/remap/remap.adoc | 10 ++++++++++ .../remap-subroutines/qt_auto_probe_tool.ngc | 8 ++++++++ nc_files/remap_lib/python-stdglue/stdglue.py | 19 +++++++++++++++++++ 5 files changed, 64 insertions(+) diff --git a/configs/sim/woodpecker/python/stdglue.py b/configs/sim/woodpecker/python/stdglue.py index 12cf833a7ec..3fb74bfae15 100644 --- a/configs/sim/woodpecker/python/stdglue.py +++ b/configs/sim/woodpecker/python/stdglue.py @@ -528,6 +528,25 @@ def tool_probe_m6(self, **words): if not self.task: yield INTERP_OK + # run from line: this M6 sits on a line before the start line. Task + # discards everything a skipped line queues (issue #246), so the probe + # below would hand back a stale #5070 and abort the run. Do the tool + # bookkeeping and nothing else: no unit switch, no G49, no motion, so + # the interpreter state the start line sees is the program's own. + if self.params["_task_skipping"]: + try: + emccanon.CHANGE_TOOL() + self.current_pocket = self.selected_pocket + self.selected_pocket = -1 + self.selected_tool = -1 + # cause a sync() + self.set_tool_parameters() + self.toolchange_flag = True + except InterpreterException as e: + self.set_errormsg("tool_probe_m6 remap error: %s" % (e)) + yield INTERP_ERROR + yield INTERP_OK + IMPERIAL_BASED = not(bool(self.params['_metric_machine'])) try: diff --git a/docs/src/gcode/overview.adoc b/docs/src/gcode/overview.adoc index 3aeb7ba083e..5b657fefc15 100644 --- a/docs/src/gcode/overview.adoc +++ b/docs/src/gcode/overview.adoc @@ -617,6 +617,14 @@ can be added easily without changes to the source code. to retain proper preview, for instance when testing the success of a probe (G38.n) by inspecting #5070, which will always fail in the preview interpreter (e.g. Axis). +* '#<_task_skipping>' - 1.0 while task is skipping the lines before a + run-from-line start line, 0.0 otherwise (always 0.0 in a preview + interpreter). Nothing a skipped line queues reaches the machine. This + is only a "no side effects while skipping" switch for remaps whose work + is not a queued command: a probe whose result would then be stale, an + operator wait, or Python that tracks a tool changer's state. It does + not say what state the start line needs; that depends on the start + line, which a remap cannot know from this flag. * '#<_call_level>' - current nesting level of O-code procedures. For debugging. * '#<_remap_level>' - current level of the remap stack. Each remap in a block adds one to the remap level. For debugging. diff --git a/docs/src/remap/remap.adoc b/docs/src/remap/remap.adoc index 75e2a0980fa..dd161905876 100644 --- a/docs/src/remap/remap.adoc +++ b/docs/src/remap/remap.adoc @@ -1777,6 +1777,16 @@ See 'configs/sim/axis/remap/manual-toolchange-with-tool-length-switch/nc_subrout Within Embedded Python, the `task` instance can be checked for by testing 'self.task' - this will be 1 in the milltask instance, and 0 in the preview instance(s). +A related case is run from line: task reads the lines before the start line to +rebuild the modal state and discards what they queue, so a remapped code on +such a line is executed by the interpreter but nothing it queues reaches the +machine. A remap whose work is not a queued command - a probe (its result would +be stale), an operator wait, Python that tracks a changer's state - can test +`#<_task_skipping>` (`self.params["_task_skipping"]` in Python), which is 1 +while task is skipping, and return early. It is only that: it does not tell a +remap what state the start line needs. See 'nc_files/remap-subroutines/qt_auto_probe_tool.ngc' +and `tool_probe_m6` in 'nc_files/remap_lib/python-stdglue/stdglue.py'. + [[remap:remappable-codes]] == Remappable Codes diff --git a/nc_files/remap-subroutines/qt_auto_probe_tool.ngc b/nc_files/remap-subroutines/qt_auto_probe_tool.ngc index be95048840b..af6b631b80c 100644 --- a/nc_files/remap-subroutines/qt_auto_probe_tool.ngc +++ b/nc_files/remap-subroutines/qt_auto_probe_tool.ngc @@ -68,6 +68,14 @@ o100 if [#<_task> EQ 0] o100 return [999] o100 endif +; run from line: this M6 is on a line before the start line. Task discards +; everything a skipped line queues (issue #246), so the probe below would +; read a stale #5070 and abort the run; a positive return lets the epilog +; do the tool bookkeeping and nothing moves. +o101 if [#<_task_skipping> EQ 1] +o101 return [3] +o101 endif + ; check we are in right mode o110 if [#<_metric_machine>] o115 if [#<_imperial>] diff --git a/nc_files/remap_lib/python-stdglue/stdglue.py b/nc_files/remap_lib/python-stdglue/stdglue.py index 8457e400f38..43294cac63c 100644 --- a/nc_files/remap_lib/python-stdglue/stdglue.py +++ b/nc_files/remap_lib/python-stdglue/stdglue.py @@ -528,6 +528,25 @@ def tool_probe_m6(self, **words): if not self.task: yield INTERP_OK + # run from line: this M6 sits on a line before the start line. Task + # discards everything a skipped line queues (issue #246), so the probe + # below would hand back a stale #5070 and abort the run. Do the tool + # bookkeeping and nothing else: no unit switch, no G49, no motion, so + # the interpreter state the start line sees is the program's own. + if self.params["_task_skipping"]: + try: + emccanon.CHANGE_TOOL() + self.current_pocket = self.selected_pocket + self.selected_pocket = -1 + self.selected_tool = -1 + # cause a sync() + self.set_tool_parameters() + self.toolchange_flag = True + except InterpreterException as e: + self.set_errormsg("tool_probe_m6 remap error: %s" % (e)) + yield INTERP_ERROR + yield INTERP_OK + IMPERIAL_BASED = not(bool(self.params['_metric_machine'])) try: From dad276e4adecc76827939df3400141dfb8877495 Mon Sep 17 00:00:00 2001 From: tzuohann <2057796+tzuohann@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:18:02 -0400 Subject: [PATCH 5/5] run from line: guard manual_change.ngc; correct the stdglue comment on the current tool manual_change.ngc is the one shipped M6 remap that still aborts a skipped M6 (its M66 wait and G38.3 read stale values, return -2). It now returns before any of it while #<_task_skipping> is set. In stdglue, the tool table reload at the end of the skip sets the current tool to the one in the spindle, so the comment no longer claims the program's own tool. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../nc_subroutines/manual_change.ngc | 8 ++++++++ configs/sim/woodpecker/python/stdglue.py | 5 +++-- nc_files/remap_lib/python-stdglue/stdglue.py | 5 +++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/configs/sim/axis/remap/manual-toolchange-with-tool-length-switch/nc_subroutines/manual_change.ngc b/configs/sim/axis/remap/manual-toolchange-with-tool-length-switch/nc_subroutines/manual_change.ngc index e055ed26859..12867745567 100644 --- a/configs/sim/axis/remap/manual-toolchange-with-tool-length-switch/nc_subroutines/manual_change.ngc +++ b/configs/sim/axis/remap/manual-toolchange-with-tool-length-switch/nc_subroutines/manual_change.ngc @@ -9,6 +9,14 @@ o sub # = #<_absolute> # = #<_feed> +; run from line: this M6 is on a line before the start line. Task discards +; everything a skipped line queues (issue #246), so the M66 wait and the probe +; below would read stale values and abort the run. Return before any of it; +; a positive value lets change_epilog do the tool bookkeeping. +O50 if [#<_task_skipping> EQ 1] +O50 return [1] +O50 endif + G21 (TLO and toolchange position is in mm as per ini) G90 (absolute) diff --git a/configs/sim/woodpecker/python/stdglue.py b/configs/sim/woodpecker/python/stdglue.py index 3fb74bfae15..4ed7f706e35 100644 --- a/configs/sim/woodpecker/python/stdglue.py +++ b/configs/sim/woodpecker/python/stdglue.py @@ -531,8 +531,9 @@ def tool_probe_m6(self, **words): # run from line: this M6 sits on a line before the start line. Task # discards everything a skipped line queues (issue #246), so the probe # below would hand back a stale #5070 and abort the run. Do the tool - # bookkeeping and nothing else: no unit switch, no G49, no motion, so - # the interpreter state the start line sees is the program's own. + # bookkeeping and nothing else: no unit switch, no G49, no motion. (The + # tool table reload at the end of the skip then sets the current tool to + # the one actually in the spindle, not the program's.) if self.params["_task_skipping"]: try: emccanon.CHANGE_TOOL() diff --git a/nc_files/remap_lib/python-stdglue/stdglue.py b/nc_files/remap_lib/python-stdglue/stdglue.py index 43294cac63c..fb7048dd5b0 100644 --- a/nc_files/remap_lib/python-stdglue/stdglue.py +++ b/nc_files/remap_lib/python-stdglue/stdglue.py @@ -531,8 +531,9 @@ def tool_probe_m6(self, **words): # run from line: this M6 sits on a line before the start line. Task # discards everything a skipped line queues (issue #246), so the probe # below would hand back a stale #5070 and abort the run. Do the tool - # bookkeeping and nothing else: no unit switch, no G49, no motion, so - # the interpreter state the start line sees is the program's own. + # bookkeeping and nothing else: no unit switch, no G49, no motion. (The + # tool table reload at the end of the skip then sets the current tool to + # the one actually in the spindle, not the program's.) if self.params["_task_skipping"]: try: emccanon.CHANGE_TOOL()