From d0d35a7b109e29f61c9ca19689febaa6cb30bef8 Mon Sep 17 00:00:00 2001 From: Luca Toniolo Date: Sun, 26 Apr 2026 18:07:40 +0800 Subject: [PATCH 1/7] task: tighten AUTO read-ahead wait-clear to match MDI Auto mode cleared the read-ahead wait on execState==DONE alone, so a G38 issued just before an emcMotionUpdate snapshot could let the next read fire while motion.traj.queue was still non-zero, tripping NCE_QUEUE_IS_NOT_EMPTY_AFTER_PROBING. Add the queue==0 + io.status ==DONE conjuncts already used by mdi_execute_hook. Closes #3650, #662, #263. --- src/emc/task/emctaskmain.cc | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/emc/task/emctaskmain.cc b/src/emc/task/emctaskmain.cc index 7292a172023..99c70fe7c87 100644 --- a/src/emc/task/emctaskmain.cc +++ b/src/emc/task/emctaskmain.cc @@ -505,11 +505,18 @@ void readahead_reading(void) int count = 0; interpret_again: if (emcTaskPlanIsWait()) { - // delay reading of next line until all is done + // delay reading of next line until all is done. + // Match the stricter check used by mdi_execute_hook: + // trusting only execState==DONE can race when motion + // hasn't yet processed a just-issued command (e.g. a + // G38 probe), letting the next read see a non-empty + // queue. if (interp_list.len() == 0 && emcTaskCommand == 0 && emcStatus->task.execState == - EMC_TASK_EXEC_DONE) { + EMC_TASK_EXEC_DONE && + emcStatus->motion.traj.queue == 0 && + emcStatus->io.status == RCS_DONE) { emcTaskPlanClearWait(); } } else { From 4d45f0cd2089e4ad50337f360791400ef3129972 Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Mon, 5 Oct 2026 16:24:46 +0800 Subject: [PATCH 2/7] gcodemodule: report the actual exception from failed canon callbacks When a Python callback invoked during parse_file raises (for example a canon object missing get_external_length_units), the error exit replaced the pending exception with RuntimeError("parse_file interp_error"), so AXIS and friends could only report that generic message instead of the actual error. The exception is already on the indicator; keep it. Fixes #2492. --- src/emc/rs274ngc/gcodemodule.cc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/emc/rs274ngc/gcodemodule.cc b/src/emc/rs274ngc/gcodemodule.cc index 1791a498f52..3463615514a 100644 --- a/src/emc/rs274ngc/gcodemodule.cc +++ b/src/emc/rs274ngc/gcodemodule.cc @@ -808,9 +808,9 @@ static PyObject *parse_file(PyObject *self, PyObject *args) { PyErr_Format(PyExc_RuntimeError, "interp_error > 0 but no Python exception set"); } else { - // seems a PyErr_Ocurred(), but no exception was set ? - // so return error info that can be caught and handled - PyErr_Format(PyExc_RuntimeError,"parse_file interp_error"); + // Keep the exception that the failing callback set: overwriting + // it here with a generic RuntimeError hides the actual error + // from the caller (#2492). fprintf(stderr,"!!!%s: parse_file() f=%s\n" "!!!interp_error=%d result=%d last_sequence_number=%d\n", __FILE__,f,interp_error,result,last_sequence_number); From 9dc0eedac17b30cbd2efe33acc4792e84e955b4b Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Mon, 5 Oct 2026 18:09:59 +0800 Subject: [PATCH 3/7] configure: fix libtirpc pkg-config detection The conditional tested whether the string "pkg-config libtirpc" is empty, which it never is, so the fallback block ran unconditionally. Use $PKG_CONFIG --exists as intended, and route the block's other pkg-config calls through $PKG_CONFIG too so a cross pkg-config is honored. Behavior is unchanged in practice: without libtirpc.pc the block is now skipped and configure fails later with the same explicit error. Refs #652. --- src/configure.ac | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/configure.ac b/src/configure.ac index a937904e6be..16e3c878230 100644 --- a/src/configure.ac +++ b/src/configure.ac @@ -89,15 +89,15 @@ fi # check for rpc/rpc.h # AC_CHECK_HEADER([rpc/rpc.h],[], [ - if ! test -z "$PKG_CONFIG libtirpc"; then + if $PKG_CONFIG --exists libtirpc; then - LIBTIRPC_CFLAGS=`pkg-config libtirpc --cflags` + LIBTIRPC_CFLAGS=`$PKG_CONFIG libtirpc --cflags` AC_SUBST([LIBTIRPC_CFLAGS]) - LIBTIRPC_LIBS=`pkg-config libtirpc --libs` + LIBTIRPC_LIBS=`$PKG_CONFIG libtirpc --libs` AC_SUBST([LIBTIRPC_LIBS]) - CFLAGS="${CFLAGS:+$CFLAGS }$(pkg-config libtirpc --cflags)" + CFLAGS="${CFLAGS:+$CFLAGS }$($PKG_CONFIG libtirpc --cflags)" fi # Update path From a33293f46575c809e082e0806e79ab996f982513 Mon Sep 17 00:00:00 2001 From: Cmorley Date: Fri, 16 Oct 2026 18:48:01 -0700 Subject: [PATCH 4/7] qtvcp -qt_action: fix spindle_slower command at min speed if step was bigger then minimum speed, the math would set the speed higher not stay at minimum. Still an issue if minimuim is set to 0, next press will start the spindle at default speed. issue report (#4688) and code from Martin-HC on github --- lib/python/qtvcp/qt_action.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/python/qtvcp/qt_action.py b/lib/python/qtvcp/qt_action.py index fe0b369842d..8889d3eb42a 100644 --- a/lib/python/qtvcp/qt_action.py +++ b/lib/python/qtvcp/qt_action.py @@ -486,11 +486,14 @@ def SET_SPINDLE_SLOWER(self, number=0): dir = 1 else: dir = -1 - if abs(cur - (INFO.SPINDLE_INCREMENT * dir)) <= INFO['MIN_SPINDLE_{}_SPEED'.format(i)]: - self.cmd.spindle(dir, INFO['MIN_SPINDLE_{}_SPEED'.format(i)], i) - continue - else: - self.cmd.spindle(dir, abs(cur - (INFO.SPINDLE_INCREMENT * dir)), i) + + min_speed = INFO['MIN_SPINDLE_{}_SPEED'.format(i)] + next_speed = abs(cur) - INFO.SPINDLE_INCREMENT + + if next_speed <= min_speed: + next_speed = min_speed + + self.cmd.spindle(dir, next_speed, i) def SET_SPINDLE_STOP(self, number=0): self.cmd.spindle(linuxcnc.SPINDLE_OFF, number) From ccbeb79495b9de25e2f150c16cd8012432473528 Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Sat, 17 Oct 2026 15:50:12 +1100 Subject: [PATCH 5/7] axis: send the startup slider values before the POSTGUI_HALFILE runs A Tk scale's set widget command defers the scale's -command until the widget is drawn. axis initialized the max velocity slider and the feed, rapid and spindle override sliders with set, while the window is still withdrawn, and only maps it after the POSTGUI_HALFILE has run. The deferred commands then re-sent the INI max velocity and 100% overrides after postgui, overriding anything halui had been told there, e.g. halui.max-velocity.counts or halui.feed-override.counts set or connected in the postgui file. Later changes of those pins worked because halui only sends on a change. Set the slider variables instead, which never invoke -command, and send the max velocity explicitly next to the override commands that are already sent at startup. What axis sends at startup is unchanged, it is only sent before the postgui file instead of after it. Fixes #4690 --- src/emc/usr_intf/axis/scripts/axis.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/emc/usr_intf/axis/scripts/axis.py b/src/emc/usr_intf/axis/scripts/axis.py index c61cbc9c919..2b74d4a2463 100755 --- a/src/emc/usr_intf/axis/scripts/axis.py +++ b/src/emc/usr_intf/axis/scripts/axis.py @@ -3487,7 +3487,7 @@ def units(s, d=1.0): root_window.tk.eval("${pane_top}.jogspeed.s set [setval $jog_speed $max_speed]") root_window.tk.eval("${pane_top}.ajogspeed.s set [setval $jog_aspeed $max_aspeed]") -root_window.tk.eval("${pane_top}.maxvel.s set [setval $maxvel_speed $max_maxvel]") +root_window.tk.eval("set maxvel_slider_val [setval $maxvel_speed $max_maxvel]") widgets.feedoverride.configure(to=max_feed_override) widgets.rapidoverride.configure(to=100) widgets.spinoverride.configure(to=max_spindle_override) @@ -4163,12 +4163,16 @@ def balance_ja(): else: widgets.view_y2.pack_forget() -widgets.feedoverride.set(100) +# Set the slider variables, not the scales: a scale's set defers its +# -command until the window is mapped, after the POSTGUI_HALFILE ran, +# and would override what halui was told there. +vars.feedrate.set(100) commands.set_feedrate(100) -widgets.rapidoverride.set(100) +vars.rapidrate.set(100) commands.set_rapidrate(100) -widgets.spinoverride.set(100) +vars.spindlerate.set(100) commands.set_spindlerate(100) +root_window.tk.eval("update_maxvel_slider_vel $maxvel_slider_val") def forget(widget, *pins): if "AXIS_NO_AUTOCONFIGURE" in os.environ: return From 926053da28c485228ad6e08c692e648a70b9a47e Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Sun, 1 Nov 2026 17:51:53 +0800 Subject: [PATCH 6/7] gremlin, qt5_graphics: a stopped parse now raises KeyboardInterrupt Since #4636 the gcodemodule lets the callback's own exception propagate instead of masking it as RuntimeError. An (AXIS,stop) program raises KeyboardInterrupt, which escaped these Exception-only handlers on file load. AXIS already catches it. Refs #2492. --- src/emc/usr_intf/gremlin/gremlin.py | 4 +++- src/emc/usr_intf/gremlin/qt5_graphics.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/emc/usr_intf/gremlin/gremlin.py b/src/emc/usr_intf/gremlin/gremlin.py index 9430bc953f7..9248a18699d 100755 --- a/src/emc/usr_intf/gremlin/gremlin.py +++ b/src/emc/usr_intf/gremlin/gremlin.py @@ -339,7 +339,9 @@ def load(self,filename = None): if result > gcode.MIN_ERROR: self.report_gcode_error(result, seq, filename) self.calculate_gcode_properties(canon) - except Exception as e: + except (Exception, KeyboardInterrupt) as e: + # (AXIS,stop) raises KeyboardInterrupt out of the parse, and it + # is no longer masked as RuntimeError (#2492). print (e) self.gcode_properties = None diff --git a/src/emc/usr_intf/gremlin/qt5_graphics.py b/src/emc/usr_intf/gremlin/qt5_graphics.py index 1bcae3db309..c442df4fd97 100644 --- a/src/emc/usr_intf/gremlin/qt5_graphics.py +++ b/src/emc/usr_intf/gremlin/qt5_graphics.py @@ -395,7 +395,9 @@ def load(self,filename = None): self.logger.set_depth(self.from_internal_linear_unit(self.get_foam_z()), self.from_internal_linear_unit(self.get_foam_w())) self.calculate_gcode_properties(canon) - except Exception as e: + except (Exception, KeyboardInterrupt) as e: + # (AXIS,stop) raises KeyboardInterrupt out of the parse, and it + # is no longer masked as RuntimeError (#2492). print (e) self.gcode_properties = None finally: From 1d303dcb5af85c529572ec2f4ef9cd056f3b94c5 Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Sun, 1 Nov 2026 17:43:02 +0800 Subject: [PATCH 7/7] tests/gcode-renderer: a stopped parse now raises KeyboardInterrupt The 2.9 gcodemodule fix in this merge lets the callback's own exception propagate instead of masking it as RuntimeError. parse_failing caught only Exception, which let KeyboardInterrupt escape the stopped-parse tests. --- tests/gcode-renderer/canon.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/gcode-renderer/canon.py b/tests/gcode-renderer/canon.py index 8e20be946fe..5f609bd15bc 100644 --- a/tests/gcode-renderer/canon.py +++ b/tests/gcode-renderer/canon.py @@ -208,7 +208,10 @@ def parse_failing(text, geometry="XYZ", cls=None, limits=None, **kw): canon.parameter_file = var.name try: result = gcode.parse(path, canon, "", "") - except Exception: + except (Exception, KeyboardInterrupt): + # The parse now propagates the callback's own exception + # (#2492); a stopped parse raises KeyboardInterrupt, which + # is not an Exception subclass. pass finally: os.unlink(path)