From 0a284b4db8972641ab8deee45b69b54cfd102c74 Mon Sep 17 00:00:00 2001 From: Alexey Presnyakov <309782758+alex-pres@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:25:06 +0400 Subject: [PATCH 1/2] tooldata: notify db_program of G10 tool offsets In db mode no tool table file is written, so offsets set with G10 L1/L10/L11 were lost at restart without the TOOL_OFFSET notification. --- src/emc/task/taskclass.cc | 17 ++-- tests/tooldb/db-g10-offset/.gitignore | 3 + tests/tooldb/db-g10-offset/README | 6 ++ tests/tooldb/db-g10-offset/base.inc | 107 +++++++++++++++++++++++ tests/tooldb/db-g10-offset/checkresult | 2 + tests/tooldb/db-g10-offset/core_sim.hal | 54 ++++++++++++ tests/tooldb/db-g10-offset/db_test.py | 80 +++++++++++++++++ tests/tooldb/db-g10-offset/dbtest_lib.py | 82 +++++++++++++++++ tests/tooldb/db-g10-offset/test-ui.py | 50 +++++++++++ tests/tooldb/db-g10-offset/test.ini | 5 ++ tests/tooldb/db-g10-offset/test.sh | 11 +++ tests/tooldb/db-g10-offset/verify-ui.py | 16 ++++ tests/tooldb/db-g10-offset/verify.ini | 5 ++ 13 files changed, 430 insertions(+), 8 deletions(-) create mode 100644 tests/tooldb/db-g10-offset/.gitignore create mode 100644 tests/tooldb/db-g10-offset/README create mode 100644 tests/tooldb/db-g10-offset/base.inc create mode 100755 tests/tooldb/db-g10-offset/checkresult create mode 100644 tests/tooldb/db-g10-offset/core_sim.hal create mode 100755 tests/tooldb/db-g10-offset/db_test.py create mode 100644 tests/tooldb/db-g10-offset/dbtest_lib.py create mode 100755 tests/tooldb/db-g10-offset/test-ui.py create mode 100644 tests/tooldb/db-g10-offset/test.ini create mode 100755 tests/tooldb/db-g10-offset/test.sh create mode 100755 tests/tooldb/db-g10-offset/verify-ui.py create mode 100644 tests/tooldb/db-g10-offset/verify.ini diff --git a/src/emc/task/taskclass.cc b/src/emc/task/taskclass.cc index c657448c79f..25dc5d47fca 100644 --- a/src/emc/task/taskclass.cc +++ b/src/emc/task/taskclass.cc @@ -587,14 +587,15 @@ int Task::emcToolSetOffset(int idx, int toolno, const EmcPose& offset, double di if (0 != tooldata_save(tooltable_filename)) { emcioStatus.status = RCS_STATUS::ERROR; } - //TODO - // if (io_db_mode == DB_ACTIVE) { - // int pno = idx; // for random_toolchanger - // if (!random_toolchanger) { pno = tdata.pocketno; } - // if (tooldata_db_notify(TOOL_OFFSET,toolno,pno,tdata)) { - // UNEXPECTED_MSG; - // } - // } + if (db_mode == tooldb_t::DB_ACTIVE) { + // tooldata_save() does not write tool table entries in db mode, + // so the db_program must be notified to persist the new offsets + int pno = idx; // for random_toolchanger + if (!random_toolchanger) { pno = tdata.pocketno; } + if (tooldata_db_notify(TOOL_OFFSET,toolno,pno,tdata)) { + UNEXPECTED_MSG; + } + } return 0; } diff --git a/tests/tooldb/db-g10-offset/.gitignore b/tests/tooldb/db-g10-offset/.gitignore new file mode 100644 index 00000000000..f9a4fc41d40 --- /dev/null +++ b/tests/tooldb/db-g10-offset/.gitignore @@ -0,0 +1,3 @@ +sim.var* +db_tools.txt +db_cmds.log diff --git a/tests/tooldb/db-g10-offset/README b/tests/tooldb/db-g10-offset/README new file mode 100644 index 00000000000..e22c5da186c --- /dev/null +++ b/tests/tooldb/db-g10-offset/README @@ -0,0 +1,6 @@ +Verify that tool offsets set with G10 L1/L10/L11 are persisted when an +[EMCIO]DB_PROGRAM is configured. + +In db mode with a nonrandom toolchanger no tool table file is written, so the +db program must be notified of the new offsets ('p' command). Without that +notification the offsets only live in memory and are lost on the next start. diff --git a/tests/tooldb/db-g10-offset/base.inc b/tests/tooldb/db-g10-offset/base.inc new file mode 100644 index 00000000000..e65b4b36816 --- /dev/null +++ b/tests/tooldb/db-g10-offset/base.inc @@ -0,0 +1,107 @@ +[EMC] +# The version string for this INI file. +VERSION = 1.1 + +DEBUG = 0x0 + + +[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 +COORDINATES = XYZ +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] +# no TOOL_TABLE: tool data is provided by the db program +TOOL_CHANGE_QUILL_UP = 1 +RANDOM_TOOLCHANGER = 0 +DB_PROGRAM = ./db_test.py + + + +[KINS] +KINEMATICS = trivkins coordinates=XYZ +#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/tooldb/db-g10-offset/checkresult b/tests/tooldb/db-g10-offset/checkresult new file mode 100755 index 00000000000..24dc9aa53e3 --- /dev/null +++ b/tests/tooldb/db-g10-offset/checkresult @@ -0,0 +1,2 @@ +#!/bin/sh +exit 0 # test failure is indicated by test.sh exit value diff --git a/tests/tooldb/db-g10-offset/core_sim.hal b/tests/tooldb/db-g10-offset/core_sim.hal new file mode 100644 index 00000000000..da1597c8924 --- /dev/null +++ b/tests/tooldb/db-g10-offset/core_sim.hal @@ -0,0 +1,54 @@ +# 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 + +# tool loading loopback +net tool-prepare iocontrol.0.tool-prepare => iocontrol.0.tool-prepared +net tool-change iocontrol.0.tool-change => iocontrol.0.tool-changed + diff --git a/tests/tooldb/db-g10-offset/db_test.py b/tests/tooldb/db-g10-offset/db_test.py new file mode 100755 index 00000000000..7cfef350941 --- /dev/null +++ b/tests/tooldb/db-g10-offset/db_test.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +# Minimal [EMCIO]DB_PROGRAM used by this test. +# Tool data is kept in a flat file (db_tools.txt) so that it survives a +# restart of LinuxCNC, and every received command is appended to db_cmds.log. +# Note: stdout is the pipe to LinuxCNC, so logging goes to a file. + +import os + +from tooldb import tooldb_callbacks +from tooldb import tooldb_tools +from tooldb import tooldb_loop + +savefile = "db_tools.txt" +logfile = "db_cmds.log" + +initial_tools = { + 1: "T1 P1 D0.1 Z0.5", + 2: "T2 P2 D0.2 Z1.5", + 3: "T3 P3 D0.3 Z2.5", +} + +tools = {} + +def log(msg): + with open(logfile,"a") as f: + f.write("%s\n"%msg) + +def save(): + with open(savefile,"w") as f: + for tno in sorted(tools): + f.write("%s\n"%tools[tno]) + +def load(): + if not os.path.exists(savefile): + tools.update(initial_tools) + save() + return + with open(savefile) as f: + for line in f: + line = line.strip() + if not line: continue + tools[toolno_of(line)] = line + +def toolno_of(toolline): + for item in toolline.upper().split(): + if item.startswith("T"): return int(item[1:]) + raise ValueError("no toolno in <%s>"%toolline) + +def merge(toolline,params): + # apply the letter parameters of params to toolline + D = dict((item[0],item[1:]) for item in toolline.upper().split()) + for item in params.upper().split(): + if item.startswith(";"): break + D[item[0]] = item[1:] + letters = ["T","P"] + sorted(set(D) - set(["T","P"])) + return " ".join("%s%s"%(letter,D[letter]) for letter in letters) + +# 'g' interface command +def get_tool(tno): + log("g %s"%tools[tno]) + return tools[tno] + +# 'p' interface command +def put_tool(tno,params): + log("p %s"%params) + tools[tno] = merge(tools.get(tno,"T%d P%d"%(tno,tno)),params) + save() + +# 'l' interface command +def load_spindle(tno,params): + log("l %s"%params) + +# 'u' interface command +def unload_spindle(tno,params): + log("u %s"%params) + +load() +tooldb_callbacks(get_tool,put_tool,load_spindle,unload_spindle) +tooldb_tools(sorted(tools)) +tooldb_loop() diff --git a/tests/tooldb/db-g10-offset/dbtest_lib.py b/tests/tooldb/db-g10-offset/dbtest_lib.py new file mode 100644 index 00000000000..fbb6e18af10 --- /dev/null +++ b/tests/tooldb/db-g10-offset/dbtest_lib.py @@ -0,0 +1,82 @@ +# helpers shared by the ui scripts of this test + +import sys +import time + +import linuxcnc + +savefile = "db_tools.txt" + + +def wait_for_linuxcnc_startup(status, timeout=10.0): + """Poll the Status buffer waiting for it to look initialized, rather + than just allocated (all-zero).""" + start_time = time.time() + while time.time() - start_time < timeout: + status.poll() + if (status.angular_units == 0.0) \ + or (status.axis_mask == 0) \ + or (status.cycle_time == 0.0) \ + or (status.exec_state != linuxcnc.EXEC_DONE) \ + or (status.interp_state != linuxcnc.INTERP_IDLE) \ + or (status.inpos is False) \ + or (status.linear_units == 0.0) \ + or (status.max_acceleration == 0.0) \ + or (status.max_velocity == 0.0) \ + or (status.program_units == 0.0) \ + or (status.rapidrate == 0.0) \ + or (status.state != linuxcnc.RCS_DONE) \ + or (status.task_state != linuxcnc.STATE_ESTOP): + time.sleep(0.1) + else: + return + raise RuntimeError("timeout waiting for linuxcnc startup") + + +def start_machine(): + c = linuxcnc.command() + s = linuxcnc.stat() + wait_for_linuxcnc_startup(s) + c.state(linuxcnc.STATE_ESTOP_RESET) + c.state(linuxcnc.STATE_ON) + c.home(-1) + c.wait_complete() + c.mode(linuxcnc.MODE_MDI) + c.wait_complete() + return (c, s) + + +def fail(msg): + print("FAIL: %s" % msg) + sys.exit(1) + + +def check(what, got, expected): + if abs(got - expected) > 1e-6: + fail("%s: expected %.6f, got %.6f" % (what, expected, got)) + print("ok: %s = %.6f" % (what, got)) + + +def tool_table_zoffset(s, idx): + s.poll() + return s.tool_table[idx].zoffset + + +def db_zoffset(toolno, timeout=0): + """Z offset recorded by the db program for toolno, or None.""" + deadline = time.time() + timeout + while True: + try: + with open(savefile) as f: + for line in f: + items = line.upper().split() + if "T%d" % toolno not in items: + continue + for item in items: + if item.startswith("Z"): + return float(item[1:]) + except IOError: + pass + if time.time() >= deadline: + return None + time.sleep(0.1) diff --git a/tests/tooldb/db-g10-offset/test-ui.py b/tests/tooldb/db-g10-offset/test-ui.py new file mode 100755 index 00000000000..28cfded060b --- /dev/null +++ b/tests/tooldb/db-g10-offset/test-ui.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 + +# With an [EMCIO]DB_PROGRAM configured and a nonrandom toolchanger, no tool +# table file is written, so G10 L1/L10/L11 offsets are persisted only if the +# db program is notified ('p' command). + +import sys + +from dbtest_lib import start_machine, check, fail, tool_table_zoffset, db_zoffset + +(c, s) = start_machine() + +# initial values come from the db program +check("tool 3 zoffset at startup", tool_table_zoffset(s, 3), 2.5) +check("db zoffset for tool 3 at startup", db_zoffset(3), 2.5) + +# 1) G10 L1 on a tool that is not in the spindle +c.mdi("G10 L1 P3 Z-12.345") +c.wait_complete() + +check("tool 3 zoffset after G10 L1", tool_table_zoffset(s, 3), -12.345) + +got = db_zoffset(3, timeout=5) +if got is None: + fail("db program was not notified of the G10 L1 offset for tool 3") +check("db zoffset for tool 3 after G10 L1", got, -12.345) + +# 2) G10 L1 on the tool that is in the spindle +c.mdi("T2 M6") +c.wait_complete() +s.poll() +if s.tool_in_spindle != 2: + fail("expected tool 2 in spindle, got %d" % s.tool_in_spindle) + +c.mdi("G10 L1 P2 Z-6.789") +c.wait_complete() + +check("tool 2 zoffset after G10 L1", tool_table_zoffset(s, 2), -6.789) +check("spindle zoffset after G10 L1", tool_table_zoffset(s, 0), -6.789) + +got = db_zoffset(2, timeout=5) +if got is None: + fail("db program was not notified of the G10 L1 offset for tool 2") +check("db zoffset for tool 2 after G10 L1", got, -6.789) + +# unload the tool so the db program sees a clean shutdown +c.mdi("T0 M6") +c.wait_complete() + +sys.exit(0) diff --git a/tests/tooldb/db-g10-offset/test.ini b/tests/tooldb/db-g10-offset/test.ini new file mode 100644 index 00000000000..2f2a135c371 --- /dev/null +++ b/tests/tooldb/db-g10-offset/test.ini @@ -0,0 +1,5 @@ +# apply G10 L1 tool offsets with an [EMCIO]DB_PROGRAM configured +[DISPLAY] +DISPLAY = ./test-ui.py + +#INCLUDE base.inc diff --git a/tests/tooldb/db-g10-offset/test.sh b/tests/tooldb/db-g10-offset/test.sh new file mode 100755 index 00000000000..d67ba664c8d --- /dev/null +++ b/tests/tooldb/db-g10-offset/test.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -x +set -e + +rm -f sim.var* db_tools.txt db_cmds.log + +# run 1: apply G10 L1 offsets (db program creates db_tools.txt) +linuxcnc -r test.ini + +# run 2: the offsets must be restored from the db program +linuxcnc -r verify.ini diff --git a/tests/tooldb/db-g10-offset/verify-ui.py b/tests/tooldb/db-g10-offset/verify-ui.py new file mode 100755 index 00000000000..99124db2143 --- /dev/null +++ b/tests/tooldb/db-g10-offset/verify-ui.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 + +# Second run: the tool data now comes from the db program, and must carry the +# offsets written by test-ui.py in the previous run. + +import sys + +from dbtest_lib import start_machine, check, tool_table_zoffset + +(c, s) = start_machine() + +check("tool 3 zoffset after restart", tool_table_zoffset(s, 3), -12.345) +check("tool 2 zoffset after restart", tool_table_zoffset(s, 2), -6.789) +check("tool 1 zoffset after restart", tool_table_zoffset(s, 1), 0.5) + +sys.exit(0) diff --git a/tests/tooldb/db-g10-offset/verify.ini b/tests/tooldb/db-g10-offset/verify.ini new file mode 100644 index 00000000000..ac5c4245e00 --- /dev/null +++ b/tests/tooldb/db-g10-offset/verify.ini @@ -0,0 +1,5 @@ +# restart: the offsets applied by test.ini must come back from the db program +[DISPLAY] +DISPLAY = ./verify-ui.py + +#INCLUDE base.inc From cc35a39f9b5a7289054c6892cadbd97018758b9f Mon Sep 17 00:00:00 2001 From: Alexey Presnyakov <309782758+alex-pres@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:34:03 +0400 Subject: [PATCH 2/2] tests: use LIB:core_sim.hal in the tooldb test It already provides the estop and toolchange loopbacks, so the local copy is not needed; drop the .gitignore entries covered by tests/.gitignore. --- tests/tooldb/db-g10-offset/.gitignore | 1 - tests/tooldb/db-g10-offset/base.inc | 15 ++----- tests/tooldb/db-g10-offset/core_sim.hal | 54 ------------------------- 3 files changed, 3 insertions(+), 67 deletions(-) delete mode 100644 tests/tooldb/db-g10-offset/core_sim.hal diff --git a/tests/tooldb/db-g10-offset/.gitignore b/tests/tooldb/db-g10-offset/.gitignore index f9a4fc41d40..211e02568a8 100644 --- a/tests/tooldb/db-g10-offset/.gitignore +++ b/tests/tooldb/db-g10-offset/.gitignore @@ -1,3 +1,2 @@ -sim.var* db_tools.txt db_cmds.log diff --git a/tests/tooldb/db-g10-offset/base.inc b/tests/tooldb/db-g10-offset/base.inc index e65b4b36816..70e31728ae5 100644 --- a/tests/tooldb/db-g10-offset/base.inc +++ b/tests/tooldb/db-g10-offset/base.inc @@ -1,14 +1,12 @@ [EMC] # The version string for this INI file. VERSION = 1.1 - DEBUG = 0x0 - [RS274NGC] PARAMETER_FILE = sim.var -[EMCMOT] +[EMCMOT] EMCMOT = motmod COMM_TIMEOUT = 4.0 BASE_PERIOD = 0 @@ -19,14 +17,10 @@ TASK = milltask CYCLE_TIME = 0.001 [HAL] -HALUI = halui -HALFILE = core_sim.hal - -[HALUI] -#No Content +# the library file provides the estop and toolchange loopbacks needed here +HALFILE = LIB:core_sim.hal [TRAJ] - NO_FORCE_HOMING=1 COORDINATES = XYZ HOME = 0 0 0 @@ -42,11 +36,8 @@ TOOL_CHANGE_QUILL_UP = 1 RANDOM_TOOLCHANGER = 0 DB_PROGRAM = ./db_test.py - - [KINS] KINEMATICS = trivkins coordinates=XYZ -#This is a best-guess at the number of joints, it should be checked JOINTS = 3 [AXIS_X] diff --git a/tests/tooldb/db-g10-offset/core_sim.hal b/tests/tooldb/db-g10-offset/core_sim.hal deleted file mode 100644 index da1597c8924..00000000000 --- a/tests/tooldb/db-g10-offset/core_sim.hal +++ /dev/null @@ -1,54 +0,0 @@ -# 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 - -# tool loading loopback -net tool-prepare iocontrol.0.tool-prepare => iocontrol.0.tool-prepared -net tool-change iocontrol.0.tool-change => iocontrol.0.tool-changed -