From 67da567f20dab5a2855852fa74fe2456af4a8272 Mon Sep 17 00:00:00 2001 From: tzuohann <2057796+tzuohann@users.noreply.github.com> Date: Mon, 12 Oct 2026 14:38:09 -0400 Subject: [PATCH] task: program_units follows the executing G20/G21, like gcodes (#3627) program_units was only written by canon USE_LENGTH_UNITS, when the interpreter read a G20/G21 -- often far ahead of motion -- while gcodes reports the executing move. emcTaskUpdate now sets programUnits from the active G20/G21 it has just computed. Canon's write stays for interpreters that report no G20/G21 (canterp). tests/program-units runs a 5 s G21 move followed by G20 on simulated motion and fails if program_units disagrees with gcodes during the move. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/src/config/python-interface.adoc | 5 +- src/emc/task/emctask.cc | 12 +++ tests/program-units/.gitignore | 1 + tests/program-units/README | 4 + tests/program-units/checkresult | 2 + tests/program-units/core_sim.hal | 61 ++++++++++++++ tests/program-units/test-ui.py | 63 ++++++++++++++ tests/program-units/test.ini | 117 ++++++++++++++++++++++++++ tests/program-units/test.ngc | 5 ++ tests/program-units/test.sh | 5 ++ 10 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 tests/program-units/.gitignore create mode 100644 tests/program-units/README create mode 100755 tests/program-units/checkresult create mode 100644 tests/program-units/core_sim.hal create mode 100755 tests/program-units/test-ui.py create mode 100644 tests/program-units/test.ini create mode 100644 tests/program-units/test.ngc create mode 100755 tests/program-units/test.sh diff --git a/docs/src/config/python-interface.adoc b/docs/src/config/python-interface.adoc index 0fce1c760ac..f55ccb764a2 100644 --- a/docs/src/config/python-interface.adoc +++ b/docs/src/config/python-interface.adoc @@ -313,7 +313,10 @@ see <> for an example. flag, True if a probe operation is in progress. *program_units*:: '(returns integer)' - - one of CANON_UNITS_INCHES=1, CANON_UNITS_MM=2, CANON_UNITS_CM=3 + one of CANON_UNITS_INCHES=1, CANON_UNITS_MM=2, CANON_UNITS_CM=3. + Follows the active G20/G21 reported in *gcodes*: the executing line while + a program runs, the interpreter otherwise. CANON_UNITS_CM is never reported: + G-code cannot select it and canterp maps it to mm. *queue*:: '(returns integer)' - current size of the trajectory planner queue. diff --git a/src/emc/task/emctask.cc b/src/emc/task/emctask.cc index 6e88b359372..c9772a266db 100644 --- a/src/emc/task/emctask.cc +++ b/src/emc/task/emctask.cc @@ -29,6 +29,7 @@ #include "nml_intf/interpl_nml.hh" // NML_INTERP_LIST, interp_list #include "nml_intf/canon.hh" // CANON_VECTOR, GET_PROGRAM_ORIGIN() #include "rs274ngc/rs274ngc_interp.hh" // the interpreter +#include "rs274ngc/interp_internal.hh" // G_20, G_21 #include "nml_intf/interp_return.hh" // INTERP_FILE_NOT_OPEN #include @@ -763,6 +764,17 @@ int emcTaskUpdate(EMC_TASK_STAT * stat) interp.active_settings(&stat->activeSettings[0]); } + // program_units follows the active G20/G21 above, so it describes the + // same point in the program as gcodes does: the executing move while a + // program runs, the interpreter otherwise. Canon also sets it as the + // interpreter reads ahead, which alone reported a G20 read ahead of a + // still-running G21 move (issue #3627). Canon's value stays when the + // interpreter reports no G20/G21 at all (canterp). + if (stat->activeGCodes[5] == G_20) + stat->programUnits = CANON_UNITS_INCHES; + else if (stat->activeGCodes[5] == G_21) + stat->programUnits = CANON_UNITS_MM; + //update state of optional stop stat->optional_stop_state = GET_OPTIONAL_PROGRAM_STOP(); diff --git a/tests/program-units/.gitignore b/tests/program-units/.gitignore new file mode 100644 index 00000000000..b4bf49eb08f --- /dev/null +++ b/tests/program-units/.gitignore @@ -0,0 +1 @@ +sim.var* diff --git a/tests/program-units/README b/tests/program-units/README new file mode 100644 index 00000000000..ecdc33887f1 --- /dev/null +++ b/tests/program-units/README @@ -0,0 +1,4 @@ +Issue #3627: linuxcnc.stat.program_units followed the interpreter's +read-ahead, so it reported a G20 while a G21 move was still executing, and +disagreed with gcodes. This runs a 5 s G21 move followed by G20 and checks, +while the move executes, that program_units says mm whenever gcodes says G21. diff --git a/tests/program-units/checkresult b/tests/program-units/checkresult new file mode 100755 index 00000000000..24dc9aa53e3 --- /dev/null +++ b/tests/program-units/checkresult @@ -0,0 +1,2 @@ +#!/bin/sh +exit 0 # test failure is indicated by test.sh exit value diff --git a/tests/program-units/core_sim.hal b/tests/program-units/core_sim.hal new file mode 100644 index 00000000000..cb2648e9733 --- /dev/null +++ b/tests/program-units/core_sim.hal @@ -0,0 +1,61 @@ +# core HAL config file for simulation + +# first load all the RT modules that will be needed +# kinematics +loadrt [KINS]KINEMATICS +#autoconverted trivkins +# motion controller, get name and thread periods from INI file +loadrt [EMCMOT]EMCMOT base_period_nsec=[EMCMOT]BASE_PERIOD servo_period_nsec=[EMCMOT]SERVO_PERIOD num_joints=[KINS]JOINTS +# load 6 differentiators (for velocity and accel signals +loadrt ddt count=6 +# load additional blocks +loadrt hypot count=2 +loadrt comp count=3 +loadrt or2 count=1 + +# add motion controller functions to servo thread +addf motion-command-handler servo-thread +addf motion-controller servo-thread +# link the differentiator functions into the code +addf ddt.0 servo-thread +addf ddt.1 servo-thread +addf ddt.2 servo-thread +addf ddt.3 servo-thread +addf ddt.4 servo-thread +addf ddt.5 servo-thread +addf hypot.0 servo-thread +addf hypot.1 servo-thread + +# create HAL signals for position commands from motion module +# loop position commands back to motion module feedback +net Xpos joint.0.motor-pos-cmd => joint.0.motor-pos-fb ddt.0.in +net Ypos joint.1.motor-pos-cmd => joint.1.motor-pos-fb ddt.2.in +net Zpos joint.2.motor-pos-cmd => joint.2.motor-pos-fb ddt.4.in + +# send the position commands thru differentiators to +# generate velocity and accel signals +net Xvel ddt.0.out => ddt.1.in hypot.0.in0 +net Xacc <= ddt.1.out +net Yvel ddt.2.out => ddt.3.in hypot.0.in1 +net Yacc <= ddt.3.out +net Zvel ddt.4.out => ddt.5.in hypot.1.in0 +net Zacc <= ddt.5.out + +# Cartesian 2- and 3-axis velocities +net XYvel hypot.0.out => hypot.1.in1 +net XYZvel <= hypot.1.out + +# estop loopback +net estop-loop iocontrol.0.user-enable-out iocontrol.0.emc-enable-in + +# create signals for tool loading loopback +net tool-prepare <= iocontrol.0.tool-prepare +net tool-prepared => iocontrol.0.tool-prepared + +net tool-change <= iocontrol.0.tool-change +net tool-changed => iocontrol.0.tool-changed + +net tool-number <= iocontrol.0.tool-number +net tool-prep-number <= iocontrol.0.tool-prep-number +net tool-prep-pocket <= iocontrol.0.tool-prep-pocket + diff --git a/tests/program-units/test-ui.py b/tests/program-units/test-ui.py new file mode 100755 index 00000000000..53cf527917d --- /dev/null +++ b/tests/program-units/test-ui.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 + +import linuxcnc +import linuxcnc_util +import time +import sys + + +l = linuxcnc_util.LinuxCNC() +c, s = l.command, l.status +l.wait_for_linuxcnc_startup() + +c.state(linuxcnc.STATE_ESTOP_RESET) +c.state(linuxcnc.STATE_ON) +c.home(-1) +c.wait_complete() +c.mode(linuxcnc.MODE_AUTO) +c.wait_complete() +c.program_open('test.ngc') +c.wait_complete() +s.poll() +if s.task_mode != linuxcnc.MODE_AUTO or not s.file.endswith('test.ngc'): + print('FAIL: not in AUTO with test.ngc open (mode %d, file %r)' % (s.task_mode, s.file)) + sys.exit(1) +e = linuxcnc.error_channel() +c.auto(linuxcnc.AUTO_RUN, 0) + +# While the G21 move executes, gcodes[5] (from the executing move) says G21. +# program_units must agree: before the fix it already showed the G20 that the +# interpreter had read ahead. +in_g21 = mismatch = 0 +start = time.time() +while time.time() - start < 30: + s.poll() + if s.interp_state == linuxcnc.INTERP_IDLE and time.time() - start > 1: + break + if s.gcodes[5] == 210 and s.interp_state != linuxcnc.INTERP_IDLE: + in_g21 += 1 + if s.program_units != 2: # CANON_UNITS_MM (no Python constant for it) + mismatch += 1 + time.sleep(0.005) + +s.poll() +while True: + msg = e.poll() + if not msg: + break + print('error channel: %s' % (msg[1],)) +if s.interp_state != linuxcnc.INTERP_IDLE: + print('FAIL: the program did not finish within 30 s') + sys.exit(1) +print('samples while G21 executed: %d, program_units not mm in them: %d, end gcodes[5]=%d program_units=%d' + % (in_g21, mismatch, s.gcodes[5], s.program_units)) +if in_g21 < 100: + print('FAIL: the G21 move was not seen executing') + sys.exit(1) +if mismatch: + print('FAIL: program_units disagreed with gcodes while the G21 move executed') + sys.exit(1) +if (s.gcodes[5], s.program_units) != (200, 1): + print('FAIL: after the program, G20 should be active in both') + sys.exit(1) +sys.exit(0) diff --git a/tests/program-units/test.ini b/tests/program-units/test.ini new file mode 100644 index 00000000000..2deecdc8974 --- /dev/null +++ b/tests/program-units/test.ini @@ -0,0 +1,117 @@ +# Simulated machine for tests/program-units (issue #3627), based on tests/abort/feed-rate. + +[EMC] +# The version string for this INI file. +VERSION = 1.1 + +DEBUG = 0 +RCS_DEBUG = 0x05400f01 + +[DISPLAY] +DISPLAY = ./test-ui.py + +[FILTER] +#No Content + +[RS274NGC] +PARAMETER_FILE = sim.var + +[EMCMOT] +EMCMOT = motmod +COMM_TIMEOUT = 4.0 +BASE_PERIOD = 0 +SERVO_PERIOD = 1000000 + +[TASK] +TASK = milltask +CYCLE_TIME = 0.001 + +[HAL] +HALUI = halui +HALFILE = core_sim.hal + +[HALUI] +#No Content +[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_ACCELERATION = 123.45 +MAX_LINEAR_VELOCITY = 45.67 + +[EMCIO] +TOOL_TABLE = simpockets.tbl +TOOL_CHANGE_QUILL_UP = 1 +RANDOM_TOOLCHANGER = 0 + + + +[KINS] +KINEMATICS = trivkins +#This is a best-guess at the number of joints, it should be checked +JOINTS = 3 + +[AXIS_X] +MIN_LIMIT = -40.0 +MAX_LIMIT = 40.0 +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.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 + +[AXIS_Y] +MIN_LIMIT = -40.0 +MAX_LIMIT = 40.0 +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 + +[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 + +[AXIS_Z] +MIN_LIMIT = -40.0 +MAX_LIMIT = 40.0 +MAX_VELOCITY = 4 +MAX_ACCELERATION = 1000.0 + +[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 = -40.0 +MAX_LIMIT = 40.0 +FERROR = 0.050 +MIN_FERROR = 0.010 diff --git a/tests/program-units/test.ngc b/tests/program-units/test.ngc new file mode 100644 index 00000000000..8cada2fe78a --- /dev/null +++ b/tests/program-units/test.ngc @@ -0,0 +1,5 @@ +G21 (this move runs in mm) +G1 X25 F300 (about 5 s: the interpreter reads past it to the G20 long before it ends) +G20 +G1 X0 F20 +M2 diff --git a/tests/program-units/test.sh b/tests/program-units/test.sh new file mode 100755 index 00000000000..41a4807ac77 --- /dev/null +++ b/tests/program-units/test.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +rm -f sim.var +linuxcnc -r test.ini +