From 12935bdba3b57fc3cf3de5f8316464219d556c6a Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Mon, 9 Nov 2026 22:19:00 +1100 Subject: [PATCH] tests: bring the multiaxis tests from #4730 under 10 seconds each Issue #4805 (BsAtHome): twp-table took over five minutes and twp-native over one, and with ptp-machine-frame, ptp-robot and kins-switch at 16 to 26 seconds they added about eight minutes to CI. None of them waits on anything but motion and polling: the sims moved at 90 deg/s on the rotaries and 120 mm/s on the slides, often under TCP, where the slides swing around a pivot to keep the tip still; and every step waited out a 10 ms task cycle, the 10 ms poll of wait_complete() and two 50 ms status polls. The sims now move fast. Most checks look only at where a move ends. twp-native reads paths from a sampler once a servo cycle: its G30.5 check now allows the a * dt^2 / 2 the turn can fall short between two samples from the joint's own acceleration, where it had the figure for 700 mm/s^2 written in. kins-switch samples status while its program strokes W, and W rests on a dwell at the end of the stroke. The tests wait on what wait_complete() waits for, polled every 2 ms, and twp-native, twp-table and the ptp tests run task on a 1 ms cycle. Faster polls showed three races the slow ones had hidden. A move counted as ended when motion was in position with an empty queue, which is also true before task has sent the move on: the tests now also wait for task's interpreter to be idle. Status is a copy task writes once a cycle, so two polls can read the same copy and see still joints that are moving: a move now counts as ended only on a copy task wrote two cycles after the command, and still joints only over copies with motion's heartbeat apart. And twp-native counted the sampler log's lines before a move while the log, block buffered, could still lack the end of the previous one: it now waits for the log to reach the machine at rest before a move as well as after, and reads only the lines added since. kins-switch keeps its 10 ms task cycle. At 1 ms its program's G12.1 after a G0 W0 sometimes switches while W is still about 25 um from the end of its move, and W stays there; that is for its own issue, not for this test. Two tests are split, so that each half starts LinuxCNC once and runs beside the other: twp-table-one-sided takes the one-sided A of twp-table's second run, and twp-native-ptp the point-to-point moves of twp-native, sharing its INI and HAL files and, through twp-native/native.py, its helpers. twp-table starts each case with one joint-space G69 G53.5 move rather than a G0 of the tip, since G53.1 chooses its pose from the rotaries alone; it checks the pose with no P against the P1 and P2 poses it already has from the same start rather than driving there again; and it keeps three planes of five, one in general position, one with J negative and one whose C sits near the +-180 wrap. twp-native's timed point-to-point feeds take half a second each rather than one and two. Here, each alone: twp-native 70 s => 7.1 s twp-native-ptp 7.2 s twp-table 338 s => 9.0 s twp-table-one-sided 4.1 s ptp-machine-frame 26 s => 5.5 s ptp-robot 22 s => 4.2 s kins-switch 16 s => 8.4 s All of them pass with six copies of each running beside four busy loops, and pinned to one CPU. About 3.6 s of each is LinuxCNC starting and stopping. Fixes #4805 --- tests/kins-switch/test-ui.py | 56 ++-- tests/kins-switch/test.ini | 58 ++-- tests/ptp-machine-frame/test-ui.py | 35 ++- tests/ptp-machine-frame/test.ini | 52 ++-- tests/ptp-robot/test-ui.py | 41 ++- tests/ptp-robot/test.ini | 60 ++-- tests/twp-native-ptp/README | 16 ++ tests/twp-native-ptp/checkresult | 3 + tests/twp-native-ptp/sim.hal | 1 + tests/twp-native-ptp/test-ui.py | 196 +++++++++++++ tests/twp-native-ptp/test.ini | 1 + tests/twp-native-ptp/test.sh | 4 + tests/twp-native-ptp/tool.tbl | 1 + tests/twp-native/README | 9 +- tests/twp-native/native.py | 231 +++++++++++++++ tests/twp-native/test-ui.py | 390 +------------------------- tests/twp-native/test.ini | 72 ++--- tests/twp-table-one-sided/README | 5 + tests/twp-table-one-sided/checkresult | 3 + tests/twp-table-one-sided/sim.hal | 1 + tests/twp-table-one-sided/test-ui.py | 1 + tests/twp-table-one-sided/test.ini | 124 ++++++++ tests/twp-table-one-sided/test.sh | 4 + tests/twp-table-one-sided/tool.tbl | 1 + tests/twp-table/README | 7 +- tests/twp-table/checkresult | 4 +- tests/twp-table/test-ui.py | 67 +++-- tests/twp-table/test.ini | 62 ++-- tests/twp-table/test.sh | 7 - 29 files changed, 916 insertions(+), 596 deletions(-) create mode 100644 tests/twp-native-ptp/README create mode 100755 tests/twp-native-ptp/checkresult create mode 120000 tests/twp-native-ptp/sim.hal create mode 100755 tests/twp-native-ptp/test-ui.py create mode 120000 tests/twp-native-ptp/test.ini create mode 100755 tests/twp-native-ptp/test.sh create mode 120000 tests/twp-native-ptp/tool.tbl create mode 100644 tests/twp-native/native.py create mode 100644 tests/twp-table-one-sided/README create mode 100755 tests/twp-table-one-sided/checkresult create mode 120000 tests/twp-table-one-sided/sim.hal create mode 120000 tests/twp-table-one-sided/test-ui.py create mode 100644 tests/twp-table-one-sided/test.ini create mode 100755 tests/twp-table-one-sided/test.sh create mode 120000 tests/twp-table-one-sided/tool.tbl diff --git a/tests/kins-switch/test-ui.py b/tests/kins-switch/test-ui.py index 410394f5979..d245113f5ab 100755 --- a/tests/kins-switch/test-ui.py +++ b/tests/kins-switch/test-ui.py @@ -44,40 +44,64 @@ def drain(): return said said.append(m) +# what c.wait_complete() waits for, task done with the last command sent, +# looked at every 2 ms rather than every 10: a step of several commands +# pays that wait on each +def done(timeout=60): + deadline = time.time() + timeout + while time.time() < deadline: + s.poll() + if s.echo_serial_number >= c.serial and s.state != linuxcnc.RCS_EXEC: + return + time.sleep(0.002) + error("timed out waiting for task") + +# the move has ended when task has nothing left to run and motion is at +# rest; in position with an empty queue alone is also true before task has +# sent the move on. Status is a copy task writes once a cycle, so it only +# counts from a copy written two task cycles after the command, and the +# joints only count as still over two copies a servo cycle or more apart def settled(): deadline = time.time() + 60 + s.poll() + beat = s.taskbeat + 2 last = None + last_hb = None while time.time() < deadline: - now = joints() - if s.inpos and not s.queue and now == last: - return now - last = now - time.sleep(0.05) + s.poll() + if s.taskbeat >= beat: + now = joints() + if (s.interp_state == linuxcnc.INTERP_IDLE and s.inpos and not s.queue + and now == last and s.heartbeat != last_hb): + return now + last = now + last_hb = s.heartbeat + time.sleep(0.002) error("timed out waiting for the move") return last def mdi(cmd): c.mdi(cmd) - c.wait_complete(60) + done(60) return settled() c.state(linuxcnc.STATE_ESTOP_RESET) c.state(linuxcnc.STATE_ON) -c.wait_complete(30) +done(30) c.home(-1) -c.wait_complete(60) +done(60) # start in identity every run, the abort's G13.1 may or may not have run yet c.mode(linuxcnc.MODE_MDI) -c.wait_complete(30) +done(30) mdi("G13.1") c.mode(linuxcnc.MODE_AUTO) -c.wait_complete(30) +done(30) drain() # ---- the program, and what the joints do while it runs ------------------- c.program_open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "test.ngc")) -c.wait_complete(30) +done(30) last = kins_type() c.auto(linuxcnc.AUTO_RUN, 0) @@ -181,7 +205,7 @@ def mdi(cmd): import math c.mode(linuxcnc.MODE_MDI) -c.wait_complete(30) +done(30) drain() def pose(): @@ -283,7 +307,7 @@ def tool_length_check(what, before_joints, before_pose, after_joints, after_pose def refused(cmd, needle): drain() c.mdi(cmd) - c.wait_complete(30) + done(30) m = e.poll() if not m or m[0] not in (linuxcnc.NML_ERROR, linuxcnc.OPERATOR_ERROR): error("%s was accepted" % cmd) @@ -349,7 +373,7 @@ def refused(cmd, needle): drain() c.mdi("G12.1 P-1") -c.wait_complete(30) +done(30) m = poll() if not m or m[0] not in (linuxcnc.NML_ERROR, linuxcnc.OPERATOR_ERROR): error("G12.1 P-1 was accepted") @@ -362,7 +386,7 @@ def refused(cmd, needle): # ---- a kinematics the module does not provide is refused at read time ---- c.mdi("G12.1 P7") -c.wait_complete(30) +done(30) m = poll() if not m or m[0] not in (linuxcnc.NML_ERROR, linuxcnc.OPERATOR_ERROR): error("G12.1 P7 was accepted") @@ -384,7 +408,7 @@ def refused(cmd, needle): if s.g5x_index != 2: continue c.abort() - c.wait_complete(30) + done(30) deadline = time.time() + 10 while time.time() < deadline: s.poll() diff --git a/tests/kins-switch/test.ini b/tests/kins-switch/test.ini index e84afcc4ea4..59e2c6d151a 100644 --- a/tests/kins-switch/test.ini +++ b/tests/kins-switch/test.ini @@ -20,13 +20,15 @@ JOINTS = 6 HALFILE = sim.hal [TRAJ] +# A sim that moves fast, to keep the run short: the checks on the program's +# W stroke sample status, and W rests on a dwell at its end. COORDINATES = XYZBCW LINEAR_UNITS = mm ANGULAR_UNITS = deg -DEFAULT_LINEAR_VELOCITY = 200 -MAX_LINEAR_VELOCITY = 346 -MAX_LINEAR_ACCELERATION = 800 -DEFAULT_LINEAR_ACCELERATION = 800 +DEFAULT_LINEAR_VELOCITY = 10000 +MAX_LINEAR_VELOCITY = 10000 +MAX_LINEAR_ACCELERATION = 100000 +DEFAULT_LINEAR_ACCELERATION = 100000 MAX_ANGULAR_VELOCITY = 360 [EMCMOT] @@ -44,45 +46,45 @@ TOOL_TABLE = tool.tbl [AXIS_X] MIN_LIMIT = -500 MAX_LIMIT = 500 -MAX_VELOCITY = 200 -MAX_ACCELERATION = 800 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 [AXIS_Y] MIN_LIMIT = -500 MAX_LIMIT = 500 -MAX_VELOCITY = 200 -MAX_ACCELERATION = 800 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 [AXIS_Z] MIN_LIMIT = -500 MAX_LIMIT = 500 -MAX_VELOCITY = 200 -MAX_ACCELERATION = 800 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 [AXIS_B] MIN_LIMIT = -120 MAX_LIMIT = 120 -MAX_VELOCITY = 60 -MAX_ACCELERATION = 200 +MAX_VELOCITY = 36000 +MAX_ACCELERATION = 360000 [AXIS_C] MIN_LIMIT = -36000 MAX_LIMIT = 36000 -MAX_VELOCITY = 60 -MAX_ACCELERATION = 200 +MAX_VELOCITY = 36000 +MAX_ACCELERATION = 360000 [AXIS_W] MIN_LIMIT = -100 MAX_LIMIT = 500 -MAX_VELOCITY = 200 -MAX_ACCELERATION = 800 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 [JOINT_0] TYPE = LINEAR MIN_LIMIT = -500 MAX_LIMIT = 500 -MAX_VELOCITY = 200 -MAX_ACCELERATION = 800 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 HOME_SEARCH_VEL = 0 HOME_SEQUENCE = 0 @@ -90,8 +92,8 @@ HOME_SEQUENCE = 0 TYPE = LINEAR MIN_LIMIT = -500 MAX_LIMIT = 500 -MAX_VELOCITY = 200 -MAX_ACCELERATION = 800 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 HOME_SEARCH_VEL = 0 HOME_SEQUENCE = 0 @@ -99,8 +101,8 @@ HOME_SEQUENCE = 0 TYPE = LINEAR MIN_LIMIT = -500 MAX_LIMIT = 500 -MAX_VELOCITY = 200 -MAX_ACCELERATION = 800 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 HOME_SEARCH_VEL = 0 HOME_SEQUENCE = 0 @@ -108,8 +110,8 @@ HOME_SEQUENCE = 0 TYPE = ANGULAR MIN_LIMIT = -120 MAX_LIMIT = 120 -MAX_VELOCITY = 60 -MAX_ACCELERATION = 200 +MAX_VELOCITY = 36000 +MAX_ACCELERATION = 360000 HOME_SEARCH_VEL = 0 HOME_SEQUENCE = 0 @@ -117,8 +119,8 @@ HOME_SEQUENCE = 0 TYPE = ANGULAR MIN_LIMIT = -36000 MAX_LIMIT = 36000 -MAX_VELOCITY = 60 -MAX_ACCELERATION = 200 +MAX_VELOCITY = 36000 +MAX_ACCELERATION = 360000 HOME_SEARCH_VEL = 0 HOME_SEQUENCE = 0 @@ -126,7 +128,7 @@ HOME_SEQUENCE = 0 TYPE = LINEAR MIN_LIMIT = -100 MAX_LIMIT = 500 -MAX_VELOCITY = 200 -MAX_ACCELERATION = 800 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 HOME_SEARCH_VEL = 0 HOME_SEQUENCE = 0 diff --git a/tests/ptp-machine-frame/test-ui.py b/tests/ptp-machine-frame/test-ui.py index 756e76b90bd..5110681625d 100755 --- a/tests/ptp-machine-frame/test-ui.py +++ b/tests/ptp-machine-frame/test-ui.py @@ -42,23 +42,46 @@ def drain(): if m[0] in (linuxcnc.NML_ERROR, linuxcnc.OPERATOR_ERROR): error("reported: %s" % m[1]) +# what c.wait_complete() waits for, task done with the last command sent, +# looked at every 2 ms rather than every 10: a step of several commands +# pays that wait on each +def done(timeout=60): + deadline = time.time() + timeout + while time.time() < deadline: + s.poll() + if s.echo_serial_number >= c.serial and s.state != linuxcnc.RCS_EXEC: + return + time.sleep(0.002) + error("timed out waiting for task") + +# the move has ended when task has nothing left to run and motion is at +# rest; in position with an empty queue alone is also true before task has +# sent the move on. Status is a copy task writes once a cycle, so it only +# counts from a copy written two task cycles after the command, and the +# joints only count as still over two copies a servo cycle or more apart def settled(): deadline = time.time() + 60 + s.poll() + beat = s.taskbeat + 2 last = None + last_hb = None while time.time() < deadline: s.poll() - now = [s.joint_position[i] for i in range(JOINTS)] - if s.inpos and not s.queue and now == last: - return now - last = now - time.sleep(0.05) + if s.taskbeat >= beat: + now = [s.joint_position[i] for i in range(JOINTS)] + if (s.interp_state == linuxcnc.INTERP_IDLE and s.inpos and not s.queue + and now == last and s.heartbeat != last_hb): + return now + last = now + last_hb = s.heartbeat + time.sleep(0.002) error("timed out waiting for the move") return last def mdi(*cmds): for cmd in cmds: c.mdi(cmd) - c.wait_complete(60) + done(60) return settled() def kins_type(): diff --git a/tests/ptp-machine-frame/test.ini b/tests/ptp-machine-frame/test.ini index 0d273a3d2ba..8f22c847154 100644 --- a/tests/ptp-machine-frame/test.ini +++ b/tests/ptp-machine-frame/test.ini @@ -19,13 +19,15 @@ JOINTS = 5 HALFILE = sim.hal [TRAJ] +# A sim that moves fast: the test checks where each move ends, not how it +# gets there, and slower limits only make it long. COORDINATES = XYZBC LINEAR_UNITS = mm ANGULAR_UNITS = deg -DEFAULT_LINEAR_VELOCITY = 100 -MAX_LINEAR_VELOCITY = 120 -MAX_LINEAR_ACCELERATION = 700 -DEFAULT_LINEAR_ACCELERATION = 300 +DEFAULT_LINEAR_VELOCITY = 10000 +MAX_LINEAR_VELOCITY = 10000 +MAX_LINEAR_ACCELERATION = 100000 +DEFAULT_LINEAR_ACCELERATION = 100000 NO_FORCE_HOMING = 1 [EMCMOT] @@ -35,7 +37,7 @@ COMM_TIMEOUT = 4 [TASK] TASK = milltask -CYCLE_TIME = 0.010 +CYCLE_TIME = 0.001 [EMCIO] TOOL_TABLE = tool.tbl @@ -43,38 +45,38 @@ TOOL_TABLE = tool.tbl [AXIS_X] MIN_LIMIT = -5000 MAX_LIMIT = 5000 -MAX_VELOCITY = 200 -MAX_ACCELERATION = 700 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 [AXIS_Y] MIN_LIMIT = -5000 MAX_LIMIT = 5000 -MAX_VELOCITY = 200 -MAX_ACCELERATION = 700 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 [AXIS_Z] MIN_LIMIT = -5000 MAX_LIMIT = 5000 -MAX_VELOCITY = 200 -MAX_ACCELERATION = 700 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 [AXIS_B] MIN_LIMIT = -185 MAX_LIMIT = 185 -MAX_VELOCITY = 90 -MAX_ACCELERATION = 900 +MAX_VELOCITY = 36000 +MAX_ACCELERATION = 360000 [AXIS_C] MIN_LIMIT = -360 MAX_LIMIT = 360 -MAX_VELOCITY = 90 -MAX_ACCELERATION = 900 +MAX_VELOCITY = 36000 +MAX_ACCELERATION = 360000 [JOINT_0] TYPE = LINEAR HOME = 0 -MAX_VELOCITY = 200 -MAX_ACCELERATION = 700 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 MIN_LIMIT = -5000 MAX_LIMIT = 5000 HOME_SEQUENCE = 0 @@ -82,8 +84,8 @@ HOME_SEQUENCE = 0 [JOINT_1] TYPE = LINEAR HOME = 0 -MAX_VELOCITY = 200 -MAX_ACCELERATION = 700 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 MIN_LIMIT = -5000 MAX_LIMIT = 5000 HOME_SEQUENCE = 0 @@ -91,8 +93,8 @@ HOME_SEQUENCE = 0 [JOINT_2] TYPE = LINEAR HOME = 0 -MAX_VELOCITY = 200 -MAX_ACCELERATION = 700 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 MIN_LIMIT = -5000 MAX_LIMIT = 5000 HOME_SEQUENCE = 0 @@ -100,8 +102,8 @@ HOME_SEQUENCE = 0 [JOINT_3] TYPE = ANGULAR HOME = 0 -MAX_VELOCITY = 90 -MAX_ACCELERATION = 900 +MAX_VELOCITY = 36000 +MAX_ACCELERATION = 360000 MIN_LIMIT = -185 MAX_LIMIT = 185 HOME_SEQUENCE = 0 @@ -109,8 +111,8 @@ HOME_SEQUENCE = 0 [JOINT_4] TYPE = ANGULAR HOME = 0 -MAX_VELOCITY = 90 -MAX_ACCELERATION = 900 +MAX_VELOCITY = 36000 +MAX_ACCELERATION = 360000 MIN_LIMIT = -360 MAX_LIMIT = 360 HOME_SEQUENCE = 0 diff --git a/tests/ptp-robot/test-ui.py b/tests/ptp-robot/test-ui.py index 375de49eaf2..93d77c1ea8a 100755 --- a/tests/ptp-robot/test-ui.py +++ b/tests/ptp-robot/test-ui.py @@ -20,16 +20,39 @@ def error(what): errors += 1 print("*** ERROR %s" % what) +# what c.wait_complete() waits for, task done with the last command sent, +# looked at every 2 ms rather than every 10: a step of several commands +# pays that wait on each +def done(timeout=60): + deadline = time.time() + timeout + while time.time() < deadline: + s.poll() + if s.echo_serial_number >= c.serial and s.state != linuxcnc.RCS_EXEC: + return + time.sleep(0.002) + error("timed out waiting for task") + +# the move has ended when task has nothing left to run and motion is at +# rest; in position with an empty queue alone is also true before task has +# sent the move on. Status is a copy task writes once a cycle, so it only +# counts from a copy written two task cycles after the command, and the +# joints only count as still over two copies a servo cycle or more apart def settled(): deadline = time.time() + 60 + s.poll() + beat = s.taskbeat + 2 last = None + last_hb = None while time.time() < deadline: s.poll() - now = [s.joint_position[i] for i in range(JOINTS)] - if s.inpos and not s.queue and now == last: - return now - last = now - time.sleep(0.05) + if s.taskbeat >= beat: + now = [s.joint_position[i] for i in range(JOINTS)] + if (s.interp_state == linuxcnc.INTERP_IDLE and s.inpos and not s.queue + and now == last and s.heartbeat != last_hb): + return now + last = now + last_hb = s.heartbeat + time.sleep(0.002) error("timed out waiting for the move") return last @@ -39,16 +62,16 @@ def drain(): def mdi(cmd): c.mdi(cmd) - c.wait_complete(60) + done(60) return settled() c.state(linuxcnc.STATE_ESTOP_RESET) c.state(linuxcnc.STATE_ON) -c.wait_complete(30) +done(30) c.home(-1) -c.wait_complete(60) +done(60) c.mode(linuxcnc.MODE_MDI) -c.wait_complete(30) +done(30) drain() # the joint form moves the joints it names and leaves the rest alone diff --git a/tests/ptp-robot/test.ini b/tests/ptp-robot/test.ini index 4570f1389e8..27aa30a1827 100644 --- a/tests/ptp-robot/test.ini +++ b/tests/ptp-robot/test.ini @@ -22,13 +22,15 @@ HALCMD = setp pumakins.D4 400 HALCMD = setp pumakins.D6 80 [TRAJ] +# A sim that moves fast: the test checks where each move ends, not how it +# gets there, and slower limits only make it long. COORDINATES = XYZABC LINEAR_UNITS = mm ANGULAR_UNITS = deg -DEFAULT_LINEAR_VELOCITY = 100 -MAX_LINEAR_VELOCITY = 120 -MAX_LINEAR_ACCELERATION = 700 -DEFAULT_LINEAR_ACCELERATION = 300 +DEFAULT_LINEAR_VELOCITY = 10000 +MAX_LINEAR_VELOCITY = 10000 +MAX_LINEAR_ACCELERATION = 100000 +DEFAULT_LINEAR_ACCELERATION = 100000 NO_FORCE_HOMING = 1 [EMCMOT] @@ -38,7 +40,7 @@ COMM_TIMEOUT = 4 [TASK] TASK = milltask -CYCLE_TIME = 0.010 +CYCLE_TIME = 0.001 [EMCIO] TOOL_TABLE = tool.tbl @@ -46,44 +48,44 @@ TOOL_TABLE = tool.tbl [AXIS_X] MIN_LIMIT = -5000 MAX_LIMIT = 5000 -MAX_VELOCITY = 200 -MAX_ACCELERATION = 700 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 [AXIS_Y] MIN_LIMIT = -5000 MAX_LIMIT = 5000 -MAX_VELOCITY = 200 -MAX_ACCELERATION = 700 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 [AXIS_Z] MIN_LIMIT = -5000 MAX_LIMIT = 5000 -MAX_VELOCITY = 200 -MAX_ACCELERATION = 700 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 [AXIS_A] MIN_LIMIT = -360 MAX_LIMIT = 360 -MAX_VELOCITY = 90 -MAX_ACCELERATION = 900 +MAX_VELOCITY = 36000 +MAX_ACCELERATION = 360000 [AXIS_B] MIN_LIMIT = -185 MAX_LIMIT = 185 -MAX_VELOCITY = 90 -MAX_ACCELERATION = 900 +MAX_VELOCITY = 36000 +MAX_ACCELERATION = 360000 [AXIS_C] MIN_LIMIT = -320 MAX_LIMIT = 320 -MAX_VELOCITY = 90 -MAX_ACCELERATION = 900 +MAX_VELOCITY = 36000 +MAX_ACCELERATION = 360000 [JOINT_0] TYPE = ANGULAR HOME = 0 -MAX_VELOCITY = 90 -MAX_ACCELERATION = 900 +MAX_VELOCITY = 36000 +MAX_ACCELERATION = 360000 MIN_LIMIT = -360 MAX_LIMIT = 360 HOME_SEQUENCE = 0 @@ -91,8 +93,8 @@ HOME_SEQUENCE = 0 [JOINT_1] TYPE = ANGULAR HOME = 0 -MAX_VELOCITY = 90 -MAX_ACCELERATION = 900 +MAX_VELOCITY = 36000 +MAX_ACCELERATION = 360000 MIN_LIMIT = -360 MAX_LIMIT = 360 HOME_SEQUENCE = 0 @@ -100,8 +102,8 @@ HOME_SEQUENCE = 0 [JOINT_2] TYPE = ANGULAR HOME = 0 -MAX_VELOCITY = 90 -MAX_ACCELERATION = 900 +MAX_VELOCITY = 36000 +MAX_ACCELERATION = 360000 MIN_LIMIT = -360 MAX_LIMIT = 360 HOME_SEQUENCE = 0 @@ -109,8 +111,8 @@ HOME_SEQUENCE = 0 [JOINT_3] TYPE = ANGULAR HOME = 0 -MAX_VELOCITY = 90 -MAX_ACCELERATION = 900 +MAX_VELOCITY = 36000 +MAX_ACCELERATION = 360000 MIN_LIMIT = -360 MAX_LIMIT = 360 HOME_SEQUENCE = 0 @@ -118,8 +120,8 @@ HOME_SEQUENCE = 0 [JOINT_4] TYPE = ANGULAR HOME = 0 -MAX_VELOCITY = 90 -MAX_ACCELERATION = 900 +MAX_VELOCITY = 36000 +MAX_ACCELERATION = 360000 MIN_LIMIT = -360 MAX_LIMIT = 360 HOME_SEQUENCE = 0 @@ -127,8 +129,8 @@ HOME_SEQUENCE = 0 [JOINT_5] TYPE = ANGULAR HOME = 0 -MAX_VELOCITY = 90 -MAX_ACCELERATION = 900 +MAX_VELOCITY = 36000 +MAX_ACCELERATION = 360000 MIN_LIMIT = -360 MAX_LIMIT = 360 HOME_SEQUENCE = 0 diff --git a/tests/twp-native-ptp/README b/tests/twp-native-ptp/README new file mode 100644 index 00000000000..4b08bffbb00 --- /dev/null +++ b/tests/twp-native-ptp/README @@ -0,0 +1,16 @@ +The point-to-point moves on the xyzacb nutating-head sim of twp-native, +whose INI and HAL files and helpers (twp-native/native.py) it shares. + +G53.4 G0 to a program point, G53.5 and G53.7 G0 to a slide position with +the head tilted, G28.5 and G30.5 to a slide position stored on the identity +kinematics, through a slide waypoint when given one, G53.4 G1 taking the +time the straight move would in G94 and in G93, and what they refuse. The +config sets MACHINE_MOVES_IN_MACHINE_FRAME, so under TCP G53, G28 and G30 land +where G53.5, G28.5 and G30.5 do, G28.1 and G30.1 storing the slides, and on +the identity kinematics they are the world's. + +The path of a move is read from a sampler on the servo thread, every cycle +of the joint commands and the world position, logged by halsampler to +samples.log while the test runs: a status snapshot per task cycle misses the +turn of a move that stops and comes back. The log is left behind when a +check fails. diff --git a/tests/twp-native-ptp/checkresult b/tests/twp-native-ptp/checkresult new file mode 100755 index 00000000000..9d48d3f180e --- /dev/null +++ b/tests/twp-native-ptp/checkresult @@ -0,0 +1,3 @@ +#!/bin/sh +# the test script counts its own failures +grep -q "^Exiting with 0 errors" "$1" diff --git a/tests/twp-native-ptp/sim.hal b/tests/twp-native-ptp/sim.hal new file mode 120000 index 00000000000..8ea49d067a8 --- /dev/null +++ b/tests/twp-native-ptp/sim.hal @@ -0,0 +1 @@ +../twp-native/sim.hal \ No newline at end of file diff --git a/tests/twp-native-ptp/test-ui.py b/tests/twp-native-ptp/test-ui.py new file mode 100755 index 00000000000..2e9abe003eb --- /dev/null +++ b/tests/twp-native-ptp/test-ui.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +# The point-to-point moves on the xyzacb nutating-head sim: see README. + +import os +import sys +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "twp-native")) +from native import * + +# from where twp-native's G53.1 Q1 leaves the machine: every rotary turned, +# the table too, under a plane +mdi("G12.1 P1", "G0 X0 Y0 Z0 A0 B0 C0", "G68.2 P1 Q123 I30 J0 K0", "G53.1 Q1") + +# --- G53.4, G53.5 and G69 ------------------------------------------------ +before = mdi("G69") +after, samples = sampled("G53.4 G0 X0 Y0 Z0 A0 B0 C0") +show("G53.4 G0", after) +drain() +joint_line("G53.4 G0", samples, before, after) +s.poll() +if s.g68_active: + error("G69 left the plane active") +if not close(list(s.g68_rotation), [1, 0, 0, 0, 1, 0, 0, 0, 1], 1e-12): + error("G69 left a rotation in status") +if not close(list(s.position[:3]), [0, 0, 0], 1e-6) or any(abs(after[j]) > 1e-6 for j in (TABLE, SECONDARY, PRIMARY)): + error("G53.4 G0 did not bring the tool and the rotaries back to zero") + +# G53.7 takes joint values by joint number: with the head tilted, J2=-5 +# puts joint 2 at -5 whatever that does to the tool tip, and touches no +# other joint; the value is the joint's own, untouched by G20 +before = mdi("G0 B30") +show("before G53.7", before) +after = mdi("G20 G53.7 G0 J2=-5") +mdi("G21") +show("G53.7 G0 J2=-5", after) +drain() +if abs(after[2] + 5) > 1e-6: + error("G53.7 J2=-5 left joint 2 at %.6f" % after[2]) +for j in (0, 1, 3, 4, 5): + if abs(after[j] - before[j]) > 1e-6: + error("G53.7 J2=-5 moved joint %d from %.9f to %.9f" % (j, before[j], after[j])) +after = mdi("G53.7 G0 J2=0 J[2+2]=0") +if abs(after[2]) > 1e-6 or abs(after[SECONDARY]) > 1e-6: + error("G53.7 J2=0 J4=0 did not put joints 2 and 4 at zero") + +# G53.5 takes the same destination by axis letter, in program units: Z-5 +# is joint 2 in millimetres, and under G20 the same words are inches +before = mdi("G0 B30") +after = mdi("G53.5 G0 Z-5") +show("G53.5 G0 Z-5", after) +drain() +if abs(after[2] + 5) > 1e-6: + error("G53.5 Z-5 left joint 2 at %.6f" % after[2]) +for j in (0, 1, 3, 4, 5): + if abs(after[j] - before[j]) > 1e-6: + error("G53.5 Z-5 moved joint %d from %.9f to %.9f" % (j, before[j], after[j])) +after = mdi("G20 G53.5 G0 Z-1") +mdi("G21") +show("G20 G53.5 G0 Z-1", after) +if abs(after[2] + 25.4) > 1e-6: + error("an inch of G53.5 Z left joint 2 at %.6f, not -25.4" % after[2]) +after = mdi("G53.5 G0 Z0 B0") +if abs(after[2]) > 1e-6 or abs(after[SECONDARY]) > 1e-6: + error("G53.5 Z0 B0 did not put joints 2 and 4 at zero") + +# --- G28.5 and G30.5 ------------------------------------------------------ +# the stored positions are saved on the identity kinematics, where the world +# is the slides: G28.1 at X10 Y20 Z-5 with the head straight, G30.1 at the +# same slides with the head at B10 +mdi("G12.1 P0", "G0 X10 Y20 Z-5 A0 B0 C0", "G28.1", "G0 B10", "G30.1", + "G0 X0 Y0 Z0 B0", "G12.1 P1") +# with the head tilted under TCP, G28.5 alone takes every slide to the +# stored position, on one joint-space move +before = mdi("G0 X0 Y0 Z0 A0 B-30 C0") +after, samples = sampled("G28.5") +show("G28.5", after) +drain() +joint_line("G28.5", samples, before, after) +if not close(after, [10, 20, -5, 0, 0, 0], 1e-6): + error("G28.5 put the joints at %s, not at the stored slides" % (after,)) +# an axis word is a slide to pass through first, and then only that letter +# goes to its stored value: Z3 on the way, then Z-5, the rest untouched +before = mdi("G0 X0 Y0 Z0 A0 B-30 C0") +after, samples = sampled("G30.5 Z3") +show("G30.5 Z3", after) +drain() +if abs(after[2] + 5) > 1e-6: + error("G30.5 Z3 left joint 2 at %.6f, not at the stored -5" % after[2]) +for j in (0, 1, 3, 4, 5): + if abs(after[j] - before[j]) > 1e-6: + error("G30.5 Z3 moved joint %d from %.9f to %.9f" % (j, before[j], after[j])) +# the first move ends on Z3 and the second starts there inside the same +# servo cycle, so the turn falls between two samples: joint 2 decelerates +# into it at its MAX_ACCELERATION, which over one servo cycle leaves it at +# most a * dt^2 / 2 short of it, and it never goes past it +cfg = linuxcnc.ini(os.environ["INI_FILE_NAME"]) +dt = float(cfg.find("EMCMOT", "SERVO_PERIOD")) * 1e-9 +short = float(cfg.find("JOINT_2", "MAX_ACCELERATION")) * dt * dt / 2 +peak = max(j[2] for j, p in samples) +if peak > 3 + 1e-6 or peak < 3 - short - 1e-6: + error("G30.5 Z3 did not pass through the Z3 slide (joint 2 peaked at %.6f)" % peak) +after = mdi("G30.5") +if not close(after, [10, 20, -5, 0, 10, 0], 1e-6): + error("G30.5 put the joints at %s, not at the stored slides with B10" % (after,)) +mdi("G0 X0 Y0 Z0 A0 B0 C0") + +# a point-to-point feed takes the time the straight move would: 5 mm at +# F600 is half a second, and F120 in G93 is half a second too +def timed(cmd): + c.mdi(cmd) + first = last = None + t0 = time.time() + s.poll(); start = [s.joint_position[i] for i in range(JOINTS)] + while time.time() - t0 < 60: + s.poll() + now = [s.joint_position[i] for i in range(JOINTS)] + if now != start: + if first is None: + first = time.time() + last = time.time() + start = now + elif (first is not None and s.interp_state == linuxcnc.INTERP_IDLE + and s.inpos and not s.queue): + break + time.sleep(0.005) + done() + settled() + return (last - first) if first else 0.0 +mdi("G0 X0 Y0 Z0 A0 B0 C0") +took = timed("G53.4 G1 X5 F600") +print("G53.4 G1 X5 F600 took %.3f s" % took) +if not 0.35 < took < 0.75: + error("a 5 mm point-to-point feed at F600 took %.3f s, not about half a second" % took) +took = timed("G93 G53.4 G1 X0 F120") +mdi("G94") +print("G93 G53.4 G1 X0 F120 took %.3f s" % took) +if not 0.35 < took < 0.75: + error("a point-to-point feed at G93 F120 took %.3f s, not about half a second" % took) +drain() + +# what the point-to-point codes refuse +for cmd in ("G53.4 G2 X1 I1", "G91 G53.7 G0 J0=1", "G53.7 G0 J9=1", "G53.7 G0 X1", + "G53.7 G0 J1", "G53.7 G0", "G0 J0=1", "G53.7 G0 J0.5=1", "G53.7 G0 J0=1 J0=2", + "G53.5 G0 J0=1", "G53.5 G0", "G91 G53.5 G0 X1", "G53.4 G1 F0 X1", + "G91 G28.5 X1", "G30.5 G1 X1"): + c.mdi(cmd) + done(30) + m = e.poll() + if not m or m[0] not in (linuxcnc.NML_ERROR, linuxcnc.OPERATOR_ERROR): + error("%s was accepted" % cmd) + else: + print("refused as expected:", m[1]) + c.mode(linuxcnc.MODE_MDI) +mdi("G90 G94 G0 X0 Y0 Z0 A0 B0 C0") + +# The config sets MACHINE_MOVES_IN_MACHINE_FRAME: under TCP G53 is G53.5, the +# slides at the words whatever the head is doing, on one joint-space move, +# where the tip's G53 would move every slide to keep the tip on machine Z +before = mdi("G12.1 P1", "G0 B30") +after, samples = sampled("G53 G0 Z-5") +show("G53 G0 Z-5 under TCP", after) +drain() +joint_line("G53 G0 Z-5", samples, before, after) +if abs(after[2] + 5) > 1e-6: + error("G53 Z-5 under TCP left joint 2 at %.6f" % after[2]) +for j in (0, 1, 3, 4, 5): + if abs(after[j] - before[j]) > 1e-6: + error("G53 Z-5 under TCP moved joint %d from %.9f to %.9f" % (j, before[j], after[j])) +# G28.1 and G30.1 store where the slides stand with the head tilted, and G28 +# and G30 go back to them after a plain move has taken the slides along +mdi("G53.5 G0 X10 Y20 Z-5 B-30", "G28.1", "G30.1") +here = mdi("G0 X0 Y0 Z0 B0") +after, samples = sampled("G28") +show("G28 under TCP", after) +drain() +joint_line("G28", samples, here, after) +if not close(after, [10, 20, -5, 0, -30, 0], 1e-6): + error("G28 under TCP put the joints at %s, not at the slides G28.1 stored" % (after,)) +before = mdi("G0 X0 Y0 Z0 B0") +after = mdi("G30 Z3") +if abs(after[2] + 5) > 1e-6: + error("G30 Z3 under TCP left joint 2 at %.6f, not at the stored -5" % after[2]) +for j in (0, 1, 3, 4, 5): + if abs(after[j] - before[j]) > 1e-6: + error("G30 Z3 under TCP moved joint %d from %.9f to %.9f" % (j, before[j], after[j])) +# on the identity kinematics the world is the slides and G53 its straight line +mdi("G0 X0 Y0 Z0 B0", "G12.1 P0") +before = mdi("G53 G0 X10 Y0 Z-5") +if not close(before, [10, 0, -5, 0, 0, 0], 1e-6): + error("G53 X10 Z-5 on the identity put the joints at %s" % (before,)) +mdi("G28.1", "G0 X0 Z0", "G28", "G12.1 P1") +if not close(settled(), [10, 0, -5, 0, 0, 0], 1e-6): + error("G28 on the identity did not go back to the G28.1 store") +mdi("G0 X0 Y0 Z0 A0 B0 C0") +drain() + +finish() diff --git a/tests/twp-native-ptp/test.ini b/tests/twp-native-ptp/test.ini new file mode 120000 index 00000000000..ea0cd84157c --- /dev/null +++ b/tests/twp-native-ptp/test.ini @@ -0,0 +1 @@ +../twp-native/test.ini \ No newline at end of file diff --git a/tests/twp-native-ptp/test.sh b/tests/twp-native-ptp/test.sh new file mode 100755 index 00000000000..d27cc469eb5 --- /dev/null +++ b/tests/twp-native-ptp/test.sh @@ -0,0 +1,4 @@ +#!/bin/bash -e +# a failed run leaves the var file behind, and it carries offsets +rm -f sim.var sim.var.bak samples.log +linuxcnc -r test.ini diff --git a/tests/twp-native-ptp/tool.tbl b/tests/twp-native-ptp/tool.tbl new file mode 120000 index 00000000000..c277ad64859 --- /dev/null +++ b/tests/twp-native-ptp/tool.tbl @@ -0,0 +1 @@ +../twp-native/tool.tbl \ No newline at end of file diff --git a/tests/twp-native/README b/tests/twp-native/README index 3280aceebea..eb150693c68 100644 --- a/tests/twp-native/README +++ b/tests/twp-native/README @@ -7,14 +7,7 @@ angle pairs and leaves the linear joints where they were all through the move; G53.6 leaves the tool tip where it was; G53.3 ends at the point asked for in the plane; a move along plane X goes along plane X in the world; G68.3 reads the plane back off the oriented tool; G69 cancels. -G53.6 standing near the end of C's travel reaches the plane by the nearest pose inside it, never running C past its limit. Q1 lets the table take part. The point-to-point moves are checked too: -G53.4 G0 to a program point, G53.5 and G53.7 G0 to a slide position with -the head tilted, G28.5 and G30.5 to a slide position stored on the identity -kinematics, through a slide waypoint when given one, G53.4 G1 taking the -time the straight move would in G94 and in G93, and what they refuse. The -config sets MACHINE_MOVES_IN_MACHINE_FRAME, so under TCP G53, G28 and G30 land -where G53.5, G28.5 and G30.5 do, G28.1 and G30.1 storing the slides, and on -the identity kinematics they are the world's. +G53.6 standing near the end of C's travel reaches the plane by the nearest pose inside it, never running C past its limit. Q1 lets the table take part. A plane refuses what would move the ground under it, and a program stopped with a plane leaves none behind. The point-to-point moves on the same machine are twp-native-ptp, which shares native.py with this test. The path of a move is read from a sampler on the servo thread, every cycle of the joint commands and the world position, logged by halsampler to diff --git a/tests/twp-native/native.py b/tests/twp-native/native.py new file mode 100644 index 00000000000..9f312e42d51 --- /dev/null +++ b/tests/twp-native/native.py @@ -0,0 +1,231 @@ +# The machine, the helpers and the finish that twp-native and +# twp-native-ptp share: see their READMEs. + + +import linuxcnc +import hal +import sys +import os +import time +import math +import numpy as np + +TOPDIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +sys.path.insert(0, os.path.join(TOPDIR, "tests", "kins-twp", "xyzacb")) +import remap_funcs_twp as twp + +JOINTS = 6 +TABLE, SECONDARY, PRIMARY = 3, 4, 5 # A, B, C +I4 = np.asmatrix(np.identity(4)) + +class Log: + def debug(self, *a): pass + def error(self, *a): print("oracle:", a) +log = Log() + +c = linuxcnc.command() +s = linuxcnc.stat() +e = linuxcnc.error_channel() + +c.state(linuxcnc.STATE_ESTOP_RESET) +c.state(linuxcnc.STATE_ON) +c.home(-1) +c.wait_complete() +c.mode(linuxcnc.MODE_MDI) + +errors = 0 + +def error(msg): + global errors + errors += 1 + print("*** ERROR " + msg) + +def drain(): + while True: + m = e.poll() + if not m: + return + print("channel:", m) + if m[0] in (linuxcnc.NML_ERROR, linuxcnc.OPERATOR_ERROR): + error("reported: %s" % m[1]) + +# what c.wait_complete() waits for, task done with the last command sent, +# looked at every 2 ms rather than every 10: a step of several commands +# pays that wait on each +def done(timeout=60): + deadline = time.time() + timeout + while time.time() < deadline: + s.poll() + if s.echo_serial_number >= c.serial and s.state != linuxcnc.RCS_EXEC: + return + time.sleep(0.002) + error("timed out waiting for task") + +# the move has ended when task has nothing left to run and motion is at +# rest; in position with an empty queue alone is also true before task has +# sent the move on. Status is a copy task writes once a cycle, so it only +# counts from a copy written two task cycles after the command, and the +# joints only count as still over two copies a servo cycle or more apart +def settled(): + deadline = time.time() + 60 + s.poll() + beat = s.taskbeat + 2 + last = None + last_hb = None + while time.time() < deadline: + s.poll() + if s.taskbeat >= beat: + now = [s.joint_position[i] for i in range(JOINTS)] + if (s.interp_state == linuxcnc.INTERP_IDLE and s.inpos and not s.queue + and now == last and s.heartbeat != last_hb): + return now + last = now + last_hb = s.heartbeat + time.sleep(0.002) + error("timed out waiting for the move") + return last + +def mdi(*cmds): + for cmd in cmds: + c.mdi(cmd) + done() + return settled() + +# the path of a move is read from the sampler log, every servo cycle of the +# joint commands and the world position, which halsampler writes to +# samples.log as it runs; status would give a snapshot per task cycle, which +# misses the turn of a move that stops and comes back +LOG = "samples.log" + +# the samples read so far, and how far into the file they reach: each read +# takes only the lines written since +sampler = {"samples": [], "at": 0} + +def log_samples(): + with open(LOG) as f: + f.seek(sampler["at"]) + text = f.read() + end = text.rfind("\n") + 1 # the rest is a line still being written + sampler["at"] += len(text[:end].encode()) + for line in text[:end].split("\n")[:-1]: + v = line.split() + if len(v) != 1 + JOINTS + 3: + continue # an "overrun" line, counted from the pin at the end + v = [float(x) for x in v[1:]] + sampler["samples"].append((v[:JOINTS], v[JOINTS:])) + return sampler["samples"] + +# the log is block buffered: read it until its last line has the machine +# at rest where it is, after n0 lines +def caught_up(what, at, n0=0): + deadline = time.time() + 10 + while True: + samples = log_samples() + if len(samples) > n0 and close(samples[-1][0], at, 2e-6): + return samples + if time.time() > deadline: + error("%s: the sampler log did not catch up with the machine" % what) + return samples + time.sleep(0.01) + +# run one command and return the settled joints and the log of the way there, +# from a log that has caught up with the machine before the move as well +def sampled(cmd): + s.poll() + n0 = len(caught_up(cmd, [s.joint_position[i] for i in range(JOINTS)])) + c.mdi(cmd) + done() + end = settled() + samples = caught_up(cmd, end, n0) + return end, samples[n0:] + +# a point-to-point move runs every joint on a straight line in joint space, +# all together: the fraction of the way each moving joint has gone is the +# same for all of them at every sample, never goes back, and reaches one; +# the log prints six decimals, which on a short move is 1e-5 of the way +def joint_line(what, samples, start, end): + moving = [i for i in range(JOINTS) if abs(end[i] - start[i]) > 1e-6] + if not moving: + error("%s: no joint moved" % what) + return + last = 0.0 + for n, (j, p) in enumerate(samples): + fs = [(j[i] - start[i]) / (end[i] - start[i]) for i in moving] + f = sum(fs) / len(fs) + if max(abs(x - f) for x in fs) > 1e-5: + error("%s: joints out of step at sample %d of %d: %s" % (what, n, len(samples), fs)) + return + if f < last - 1e-6: + error("%s: the joint path ran backwards at sample %d of %d (%.4f after %.4f)" % (what, n, len(samples), f, last)) + return + if f < -1e-6 or f > 1 + 1e-6: + error("%s: a joint left its segment at sample %d of %d (fraction %.4f)" % (what, n, len(samples), f)) + return + last = f + if last < 1 - 1e-6: + error("%s: the last sample is short of the end (fraction %.4f)" % (what, last)) + print("%s: %d joints on one line in joint space through %d samples" % (what, len(moving), len(samples))) + +def show(what, j): + print("%-26s %s" % (what, " ".join("%.4f" % v for v in j))) + +def wrap(d): + return (d + 180.0) % 360.0 - 180.0 + +def close(a, b, tol=1e-6): + return all(abs(x - y) <= tol for x, y in zip(a, b)) + +# the pairs the oracle would keep for a tool axis, in machine coordinates, +# as (b, c) in degrees, and which is nearest to the head's present angles +def oracle_pairs(z): + t1s, t2s = twp.kins_calc_possible_joint_angles(log, list(z), None) + pairs = [] + for t1 in t1s or []: + for t2 in t2s or []: + m = twp.kins_calc_transformation_matrix(t1, t2, 0, I4, 'inv') + got = [m[0, 2], m[1, 2], m[2, 2]] + if close(got, z, 1e-6): + pairs.append((math.degrees(t2), math.degrees(t1))) + return pairs + +def nearest_pair(pairs, b_now, c_now): + return min(pairs, key=lambda p: abs(wrap(p[0] - b_now)) + abs(wrap(p[1] - c_now))) + +def tool_axis(joints): + # the tool axis in work coordinates: the head as the oracle models it, + # brought into the table's frame the way the module reports the work + m = twp.kins_calc_transformation_matrix(math.radians(joints[PRIMARY]), + math.radians(joints[SECONDARY]), 0, I4, 'inv') + zm = np.array([m[0, 2], m[1, 2], m[2, 2]]) + a = math.radians(joints[TABLE]) + W = np.array([[1, 0, 0], [0, math.cos(a), math.sin(a)], [0, -math.sin(a), math.cos(a)]]) + return W.T.dot(zm) + +def plane_axes(): + s.poll() + r = s.g68_rotation + return ([r[0], r[3], r[6]], [r[1], r[4], r[7]], [r[2], r[5], r[8]]) + +def rot_x(d): + r = math.radians(d) + return np.array([[1, 0, 0], [0, math.cos(r), -math.sin(r)], [0, math.sin(r), math.cos(r)]]) + +def rot_y(d): + r = math.radians(d) + return np.array([[math.cos(r), 0, math.sin(r)], [0, 1, 0], [-math.sin(r), 0, math.cos(r)]]) + +# the sampler kept up, and the count of failures, for checkresult +def finish(): + for f in ("sim.var", "sim.var.bak"): + try: + os.unlink(f) + except OSError: + pass + + overruns = int(hal.get_value("sampler.0.overruns")) + if overruns: + error("the sampler lost %d samples" % overruns) + if not errors: + os.unlink(LOG) + print("Exiting with %d errors" % errors) + sys.exit(1 if errors else 0) diff --git a/tests/twp-native/test-ui.py b/tests/twp-native/test-ui.py index ae5acc6b8ca..85e4f2889d8 100755 --- a/tests/twp-native/test-ui.py +++ b/tests/twp-native/test-ui.py @@ -1,188 +1,15 @@ #!/usr/bin/env python3 # The tilted work plane on the xyzacb nutating-head sim, natively: see README. -import linuxcnc -import hal -import sys import os -import time -import math -import numpy as np - -TOPDIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) -sys.path.insert(0, os.path.join(TOPDIR, "tests", "kins-twp", "xyzacb")) -import remap_funcs_twp as twp - -JOINTS = 6 -TABLE, SECONDARY, PRIMARY = 3, 4, 5 # A, B, C -I4 = np.asmatrix(np.identity(4)) - -class Log: - def debug(self, *a): pass - def error(self, *a): print("oracle:", a) -log = Log() - -c = linuxcnc.command() -s = linuxcnc.stat() -e = linuxcnc.error_channel() - -c.state(linuxcnc.STATE_ESTOP_RESET) -c.state(linuxcnc.STATE_ON) -c.home(-1) -c.wait_complete() -c.mode(linuxcnc.MODE_MDI) - -errors = 0 - -def error(msg): - global errors - errors += 1 - print("*** ERROR " + msg) - -def drain(): - while True: - m = e.poll() - if not m: - return - print("channel:", m) - if m[0] in (linuxcnc.NML_ERROR, linuxcnc.OPERATOR_ERROR): - error("reported: %s" % m[1]) - -def settled(): - deadline = time.time() + 60 - last = None - while time.time() < deadline: - s.poll() - now = [s.joint_position[i] for i in range(JOINTS)] - if s.inpos and not s.queue and now == last: - return now - last = now - time.sleep(0.05) - error("timed out waiting for the move") - return last - -def mdi(*cmds): - for cmd in cmds: - c.mdi(cmd) - c.wait_complete(60) - return settled() - -# the path of a move is read from the sampler log, every servo cycle of the -# joint commands and the world position, which halsampler writes to -# samples.log as it runs; status would give a snapshot per task cycle, which -# misses the turn of a move that stops and comes back -LOG = "samples.log" - -def log_samples(): - with open(LOG) as f: - lines = f.read().split("\n") - out = [] - for line in lines[:-1]: # the last piece is a line still being written - v = line.split() - if len(v) != 1 + JOINTS + 3: - continue # an "overrun" line, counted from the pin at the end - v = [float(x) for x in v[1:]] - out.append((v[:JOINTS], v[JOINTS:])) - return out - -# run one command and return the settled joints and the log of the way there: -# the log is block buffered, so it is read until it has caught up with the -# machine at rest -def sampled(cmd): - n0 = len(log_samples()) - c.mdi(cmd) - c.wait_complete(60) - end = settled() - deadline = time.time() + 10 - while True: - samples = log_samples() - if len(samples) > n0 and close(samples[-1][0], end, 2e-6): - break - if time.time() > deadline: - error("%s: the sampler log did not catch up with the machine" % cmd) - break - time.sleep(0.02) - return end, samples[n0:] - -# a point-to-point move runs every joint on a straight line in joint space, -# all together: the fraction of the way each moving joint has gone is the -# same for all of them at every sample, never goes back, and reaches one; -# the log prints six decimals, which on a short move is 1e-5 of the way -def joint_line(what, samples, start, end): - moving = [i for i in range(JOINTS) if abs(end[i] - start[i]) > 1e-6] - if not moving: - error("%s: no joint moved" % what) - return - last = 0.0 - for n, (j, p) in enumerate(samples): - fs = [(j[i] - start[i]) / (end[i] - start[i]) for i in moving] - f = sum(fs) / len(fs) - if max(abs(x - f) for x in fs) > 1e-5: - error("%s: joints out of step at sample %d of %d: %s" % (what, n, len(samples), fs)) - return - if f < last - 1e-6: - error("%s: the joint path ran backwards at sample %d of %d (%.4f after %.4f)" % (what, n, len(samples), f, last)) - return - if f < -1e-6 or f > 1 + 1e-6: - error("%s: a joint left its segment at sample %d of %d (fraction %.4f)" % (what, n, len(samples), f)) - return - last = f - if last < 1 - 1e-6: - error("%s: the last sample is short of the end (fraction %.4f)" % (what, last)) - print("%s: %d joints on one line in joint space through %d samples" % (what, len(moving), len(samples))) - -def show(what, j): - print("%-26s %s" % (what, " ".join("%.4f" % v for v in j))) - -def wrap(d): - return (d + 180.0) % 360.0 - 180.0 - -def close(a, b, tol=1e-6): - return all(abs(x - y) <= tol for x, y in zip(a, b)) - -# the pairs the oracle would keep for a tool axis, in machine coordinates, -# as (b, c) in degrees, and which is nearest to the head's present angles -def oracle_pairs(z): - t1s, t2s = twp.kins_calc_possible_joint_angles(log, list(z), None) - pairs = [] - for t1 in t1s or []: - for t2 in t2s or []: - m = twp.kins_calc_transformation_matrix(t1, t2, 0, I4, 'inv') - got = [m[0, 2], m[1, 2], m[2, 2]] - if close(got, z, 1e-6): - pairs.append((math.degrees(t2), math.degrees(t1))) - return pairs - -def nearest_pair(pairs, b_now, c_now): - return min(pairs, key=lambda p: abs(wrap(p[0] - b_now)) + abs(wrap(p[1] - c_now))) - -def tool_axis(joints): - # the tool axis in work coordinates: the head as the oracle models it, - # brought into the table's frame the way the module reports the work - m = twp.kins_calc_transformation_matrix(math.radians(joints[PRIMARY]), - math.radians(joints[SECONDARY]), 0, I4, 'inv') - zm = np.array([m[0, 2], m[1, 2], m[2, 2]]) - a = math.radians(joints[TABLE]) - W = np.array([[1, 0, 0], [0, math.cos(a), math.sin(a)], [0, -math.sin(a), math.cos(a)]]) - return W.T.dot(zm) - -def plane_axes(): - s.poll() - r = s.g68_rotation - return ([r[0], r[3], r[6]], [r[1], r[4], r[7]], [r[2], r[5], r[8]]) - -def rot_x(d): - r = math.radians(d) - return np.array([[1, 0, 0], [0, math.cos(r), -math.sin(r)], [0, math.sin(r), math.cos(r)]]) - -def rot_y(d): - r = math.radians(d) - return np.array([[math.cos(r), 0, math.sin(r)], [0, 1, 0], [-math.sin(r), 0, math.cos(r)]]) +import sys +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "twp-native")) +from native import * # --- the plane, and G53.1 with the table held --------------------------- # no G53.2 has run yet, so the pose parameters must refuse to be read c.mdi("G0 X#<_orient_x>") -c.wait_complete(30) +done(30) m = e.poll() if not m or m[0] not in (linuxcnc.NML_ERROR, linuxcnc.OPERATOR_ERROR): error("reading #<_orient_x> before the first G53.2 was accepted") @@ -251,7 +78,7 @@ def rot_y(d): % (poses["P1"][0][SECONDARY], poses["P2"][0][SECONDARY], poses[""][0][SECONDARY], poses[""][1][SECONDARY])) c.mdi("G53.1 P3") -c.wait_complete(30) +done(30) m = e.poll() if not m or m[0] not in (linuxcnc.NML_ERROR, linuxcnc.OPERATOR_ERROR): error("G53.1 P3 was accepted") @@ -326,13 +153,13 @@ def in_plane(): error("#5075 #5076 held (%.4f, %.4f), G53.6 had reached (%.4f, %.4f)" % (after4[SECONDARY], after4[PRIMARY], stay[SECONDARY], stay[PRIMARY])) c.mdi("#5075 = 0") -c.wait_complete(30) +done(30) m = e.poll() if not m or m[0] not in (linuxcnc.NML_ERROR, linuxcnc.OPERATOR_ERROR): error("writing #5075 was accepted; the G53.2 pose is not read-only") drain() c.mdi("G0 A#<_orient_a>") -c.wait_complete(30) +done(30) m = e.poll() if m and m[0] in (linuxcnc.NML_ERROR, linuxcnc.OPERATOR_ERROR): error("#<_orient_a> after G53.2: %s" % m[1]) @@ -430,7 +257,7 @@ def pose_near(pairs, b_now, c_now, limited): for cmd in ("G68.3 I1 J0 K0", "G68.3 P1", "G68.3 Q123", "G69 R45", "G69 I1", "G69 P1"): mdi("G69") c.mdi(cmd) - c.wait_complete(30) + done(30) m = e.poll() if not m or m[0] not in (linuxcnc.NML_ERROR, linuxcnc.OPERATOR_ERROR): error("%s was accepted" % cmd) @@ -458,192 +285,13 @@ def pose_near(pairs, b_now, c_now, limited): if not close(list(W.T.dot(xm)), list(R4[:, 0]), 1e-6): error("with Q1 the machine did not place the plane's X") -# --- G53.4, G53.5 and G69 ------------------------------------------------ -before = mdi("G69") -after, samples = sampled("G53.4 G0 X0 Y0 Z0 A0 B0 C0") -show("G53.4 G0", after) -drain() -joint_line("G53.4 G0", samples, before, after) -s.poll() -if s.g68_active: - error("G69 left the plane active") -if not close(list(s.g68_rotation), [1, 0, 0, 0, 1, 0, 0, 0, 1], 1e-12): - error("G69 left a rotation in status") -if not close(list(s.position[:3]), [0, 0, 0], 1e-6) or any(abs(after[j]) > 1e-6 for j in (TABLE, SECONDARY, PRIMARY)): - error("G53.4 G0 did not bring the tool and the rotaries back to zero") - -# G53.7 takes joint values by joint number: with the head tilted, J2=-5 -# puts joint 2 at -5 whatever that does to the tool tip, and touches no -# other joint; the value is the joint's own, untouched by G20 -before = mdi("G0 B30") -show("before G53.7", before) -after = mdi("G20 G53.7 G0 J2=-5") -mdi("G21") -show("G53.7 G0 J2=-5", after) -drain() -if abs(after[2] + 5) > 1e-6: - error("G53.7 J2=-5 left joint 2 at %.6f" % after[2]) -for j in (0, 1, 3, 4, 5): - if abs(after[j] - before[j]) > 1e-6: - error("G53.7 J2=-5 moved joint %d from %.9f to %.9f" % (j, before[j], after[j])) -after = mdi("G53.7 G0 J2=0 J[2+2]=0") -if abs(after[2]) > 1e-6 or abs(after[SECONDARY]) > 1e-6: - error("G53.7 J2=0 J4=0 did not put joints 2 and 4 at zero") - -# G53.5 takes the same destination by axis letter, in program units: Z-5 -# is joint 2 in millimetres, and under G20 the same words are inches -before = mdi("G0 B30") -after = mdi("G53.5 G0 Z-5") -show("G53.5 G0 Z-5", after) -drain() -if abs(after[2] + 5) > 1e-6: - error("G53.5 Z-5 left joint 2 at %.6f" % after[2]) -for j in (0, 1, 3, 4, 5): - if abs(after[j] - before[j]) > 1e-6: - error("G53.5 Z-5 moved joint %d from %.9f to %.9f" % (j, before[j], after[j])) -after = mdi("G20 G53.5 G0 Z-1") -mdi("G21") -show("G20 G53.5 G0 Z-1", after) -if abs(after[2] + 25.4) > 1e-6: - error("an inch of G53.5 Z left joint 2 at %.6f, not -25.4" % after[2]) -after = mdi("G53.5 G0 Z0 B0") -if abs(after[2]) > 1e-6 or abs(after[SECONDARY]) > 1e-6: - error("G53.5 Z0 B0 did not put joints 2 and 4 at zero") - -# --- G28.5 and G30.5 ------------------------------------------------------ -# the stored positions are saved on the identity kinematics, where the world -# is the slides: G28.1 at X10 Y20 Z-5 with the head straight, G30.1 at the -# same slides with the head at B10 -mdi("G12.1 P0", "G0 X10 Y20 Z-5 A0 B0 C0", "G28.1", "G0 B10", "G30.1", - "G0 X0 Y0 Z0 B0", "G12.1 P1") -# with the head tilted under TCP, G28.5 alone takes every slide to the -# stored position, on one joint-space move -before = mdi("G0 X0 Y0 Z0 A0 B-30 C0") -after, samples = sampled("G28.5") -show("G28.5", after) -drain() -joint_line("G28.5", samples, before, after) -if not close(after, [10, 20, -5, 0, 0, 0], 1e-6): - error("G28.5 put the joints at %s, not at the stored slides" % (after,)) -# an axis word is a slide to pass through first, and then only that letter -# goes to its stored value: Z3 on the way, then Z-5, the rest untouched -before = mdi("G0 X0 Y0 Z0 A0 B-30 C0") -after, samples = sampled("G30.5 Z3") -show("G30.5 Z3", after) -drain() -if abs(after[2] + 5) > 1e-6: - error("G30.5 Z3 left joint 2 at %.6f, not at the stored -5" % after[2]) -for j in (0, 1, 3, 4, 5): - if abs(after[j] - before[j]) > 1e-6: - error("G30.5 Z3 moved joint %d from %.9f to %.9f" % (j, before[j], after[j])) -# the first move ends on Z3 and the second starts there inside the same -# servo cycle, so the turn falls between two samples: joint 2 decelerates -# into it at 700 mm/s^2, which over one 1 ms cycle is 3.5e-4 short of it, -# and it never goes past it -peak = max(j[2] for j, p in samples) -if peak > 3 + 1e-6 or peak < 3 - 1e-3: - error("G30.5 Z3 did not pass through the Z3 slide (joint 2 peaked at %.6f)" % peak) -after = mdi("G30.5") -if not close(after, [10, 20, -5, 0, 10, 0], 1e-6): - error("G30.5 put the joints at %s, not at the stored slides with B10" % (after,)) -mdi("G0 X0 Y0 Z0 A0 B0 C0") - -# a point-to-point feed takes the time the straight move would: 10 mm at -# F600 is one second, and F30 in G93 is two -def timed(cmd): - c.mdi(cmd) - first = last = None - t0 = time.time() - s.poll(); start = [s.joint_position[i] for i in range(JOINTS)] - while time.time() - t0 < 60: - s.poll() - now = [s.joint_position[i] for i in range(JOINTS)] - if now != start: - if first is None: - first = time.time() - last = time.time() - start = now - elif first is not None and s.inpos and not s.queue and time.time() - last > 0.3: - break - time.sleep(0.005) - c.wait_complete(60) - settled() - return (last - first) if first else 0.0 -mdi("G0 X0 Y0 Z0 A0 B0 C0") -took = timed("G53.4 G1 X10 F600") -print("G53.4 G1 X10 F600 took %.3f s" % took) -if not 0.7 < took < 1.5: - error("a 10 mm point-to-point feed at F600 took %.3f s, not about one" % took) -took = timed("G93 G53.4 G1 X0 F30") -mdi("G94") -print("G93 G53.4 G1 X0 F30 took %.3f s" % took) -if not 1.6 < took < 2.6: - error("a point-to-point feed at G93 F30 took %.3f s, not about two" % took) -drain() - -# what the point-to-point codes refuse -for cmd in ("G53.4 G2 X1 I1", "G91 G53.7 G0 J0=1", "G53.7 G0 J9=1", "G53.7 G0 X1", - "G53.7 G0 J1", "G53.7 G0", "G0 J0=1", "G53.7 G0 J0.5=1", "G53.7 G0 J0=1 J0=2", - "G53.5 G0 J0=1", "G53.5 G0", "G91 G53.5 G0 X1", "G53.4 G1 F0 X1", - "G91 G28.5 X1", "G30.5 G1 X1"): - c.mdi(cmd) - c.wait_complete(30) - m = e.poll() - if not m or m[0] not in (linuxcnc.NML_ERROR, linuxcnc.OPERATOR_ERROR): - error("%s was accepted" % cmd) - else: - print("refused as expected:", m[1]) - c.mode(linuxcnc.MODE_MDI) -mdi("G90 G94 G0 X0 Y0 Z0 A0 B0 C0") - -# The config sets MACHINE_MOVES_IN_MACHINE_FRAME: under TCP G53 is G53.5, the -# slides at the words whatever the head is doing, on one joint-space move, -# where the tip's G53 would move every slide to keep the tip on machine Z -before = mdi("G12.1 P1", "G0 B30") -after, samples = sampled("G53 G0 Z-5") -show("G53 G0 Z-5 under TCP", after) -drain() -joint_line("G53 G0 Z-5", samples, before, after) -if abs(after[2] + 5) > 1e-6: - error("G53 Z-5 under TCP left joint 2 at %.6f" % after[2]) -for j in (0, 1, 3, 4, 5): - if abs(after[j] - before[j]) > 1e-6: - error("G53 Z-5 under TCP moved joint %d from %.9f to %.9f" % (j, before[j], after[j])) -# G28.1 and G30.1 store where the slides stand with the head tilted, and G28 -# and G30 go back to them after a plain move has taken the slides along -mdi("G53.5 G0 X10 Y20 Z-5 B-30", "G28.1", "G30.1") -here = mdi("G0 X0 Y0 Z0 B0") -after, samples = sampled("G28") -show("G28 under TCP", after) -drain() -joint_line("G28", samples, here, after) -if not close(after, [10, 20, -5, 0, -30, 0], 1e-6): - error("G28 under TCP put the joints at %s, not at the slides G28.1 stored" % (after,)) -before = mdi("G0 X0 Y0 Z0 B0") -after = mdi("G30 Z3") -if abs(after[2] + 5) > 1e-6: - error("G30 Z3 under TCP left joint 2 at %.6f, not at the stored -5" % after[2]) -for j in (0, 1, 3, 4, 5): - if abs(after[j] - before[j]) > 1e-6: - error("G30 Z3 under TCP moved joint %d from %.9f to %.9f" % (j, before[j], after[j])) -# on the identity kinematics the world is the slides and G53 its straight line -mdi("G0 X0 Y0 Z0 B0", "G12.1 P0") -before = mdi("G53 G0 X10 Y0 Z-5") -if not close(before, [10, 0, -5, 0, 0, 0], 1e-6): - error("G53 X10 Z-5 on the identity put the joints at %s" % (before,)) -mdi("G28.1", "G0 X0 Z0", "G28", "G12.1 P1") -if not close(settled(), [10, 0, -5, 0, 0, 0], 1e-6): - error("G28 on the identity did not go back to the G28.1 store") -mdi("G0 X0 Y0 Z0 A0 B0 C0") -drain() - # --- a plane refuses what would move the ground under it --------------- # each refusal is an interpreter error, and the abort that follows cancels # the plane, so it is defined afresh before every one for cmd in ("G92 X1", "G52 A1", "G10 L2 P1 X1"): mdi("G68.2 P1 Q123 I30 J0 K0") c.mdi(cmd) - c.wait_complete(30) + done(30) m = e.poll() if not m or m[0] not in (linuxcnc.NML_ERROR, linuxcnc.OPERATOR_ERROR): error("%s was accepted while a plane is active" % cmd) @@ -661,7 +309,7 @@ def timed(cmd): # the queue away. The plane in status has to end up cancelled all the same, # and a G69 typed afterwards has to be able to say so again. c.mode(linuxcnc.MODE_AUTO) -c.wait_complete(30) +done(30) c.program_open("abort.ngc") c.auto(linuxcnc.AUTO_RUN, 0) deadline = time.time() + 30 @@ -674,9 +322,9 @@ def timed(cmd): if not s.g68_active: error("the program never reported a plane to stop in the middle of") c.abort() -c.wait_complete(30) +done(30) c.mode(linuxcnc.MODE_MDI) -c.wait_complete(30) +done(30) drain() s.poll() if s.g68_active: @@ -691,16 +339,4 @@ def timed(cmd): mdi("G0 X0 Y0 Z0 A0 B0 C0") drain() -for f in ("sim.var", "sim.var.bak"): - try: - os.unlink(f) - except OSError: - pass - -overruns = int(hal.get_value("sampler.0.overruns")) -if overruns: - error("the sampler lost %d samples" % overruns) -if not errors: - os.unlink(LOG) -print("Exiting with %d errors" % errors) -sys.exit(1 if errors else 0) +finish() diff --git a/tests/twp-native/test.ini b/tests/twp-native/test.ini index a8060670ed6..3f233cebdd7 100644 --- a/tests/twp-native/test.ini +++ b/tests/twp-native/test.ini @@ -31,13 +31,15 @@ HALCMD = setp xyzacb_trsrn_kins.y-rot-axis 300 HALCMD = setp xyzacb_trsrn_kins.z-rot-axis 400 [TRAJ] +# A sim that moves fast, to keep the run short: the checks on a path read +# it from the sampler, every servo cycle, and allow for the cycle. COORDINATES = XYZABC LINEAR_UNITS = mm ANGULAR_UNITS = deg -DEFAULT_LINEAR_VELOCITY = 100 -MAX_LINEAR_VELOCITY = 120 -MAX_LINEAR_ACCELERATION = 700 -DEFAULT_LINEAR_ACCELERATION = 300 +DEFAULT_LINEAR_VELOCITY = 10000 +MAX_LINEAR_VELOCITY = 10000 +MAX_LINEAR_ACCELERATION = 100000 +DEFAULT_LINEAR_ACCELERATION = 100000 NO_FORCE_HOMING = 1 [EMCMOT] @@ -47,7 +49,7 @@ COMM_TIMEOUT = 4 [TASK] TASK = milltask -CYCLE_TIME = 0.010 +CYCLE_TIME = 0.001 [EMCIO] TOOL_TABLE = tool.tbl @@ -55,45 +57,45 @@ TOOL_TABLE = tool.tbl [AXIS_X] MIN_LIMIT = -5000 MAX_LIMIT = 5000 -MAX_VELOCITY = 200 -MAX_ACCELERATION = 700 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 [AXIS_Y] MIN_LIMIT = -5000 MAX_LIMIT = 5000 -MAX_VELOCITY = 200 -MAX_ACCELERATION = 700 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 [AXIS_Z] MIN_LIMIT = -5000 MAX_LIMIT = 5000 -MAX_VELOCITY = 200 -MAX_ACCELERATION = 700 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 [AXIS_A] MIN_LIMIT = -360 MAX_LIMIT = 360 -MAX_VELOCITY = 90 -MAX_ACCELERATION = 900 +MAX_VELOCITY = 36000 +MAX_ACCELERATION = 360000 [AXIS_B] MIN_LIMIT = -185 MAX_LIMIT = 185 -MAX_VELOCITY = 90 -MAX_ACCELERATION = 900 +MAX_VELOCITY = 36000 +MAX_ACCELERATION = 360000 [AXIS_C] MIN_LIMIT = -320 MAX_LIMIT = 320 -MAX_VELOCITY = 90 -MAX_ACCELERATION = 900 +MAX_VELOCITY = 36000 +MAX_ACCELERATION = 360000 [JOINT_0] TYPE = LINEAR HOME = 0 -MAX_VELOCITY = 200 -MAX_ACCELERATION = 700 -MAX_JERK = 7000 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 +MAX_JERK = 10000000 MIN_LIMIT = -5000 MAX_LIMIT = 5000 HOME_SEQUENCE = 0 @@ -101,9 +103,9 @@ HOME_SEQUENCE = 0 [JOINT_1] TYPE = LINEAR HOME = 0 -MAX_VELOCITY = 200 -MAX_ACCELERATION = 700 -MAX_JERK = 7000 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 +MAX_JERK = 10000000 MIN_LIMIT = -5000 MAX_LIMIT = 5000 HOME_SEQUENCE = 0 @@ -111,9 +113,9 @@ HOME_SEQUENCE = 0 [JOINT_2] TYPE = LINEAR HOME = 0 -MAX_VELOCITY = 200 -MAX_ACCELERATION = 700 -MAX_JERK = 7000 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 +MAX_JERK = 10000000 MIN_LIMIT = -5000 MAX_LIMIT = 5000 HOME_SEQUENCE = 0 @@ -121,9 +123,9 @@ HOME_SEQUENCE = 0 [JOINT_3] TYPE = ANGULAR HOME = 0 -MAX_VELOCITY = 90 -MAX_ACCELERATION = 900 -MAX_JERK = 9000 +MAX_VELOCITY = 36000 +MAX_ACCELERATION = 360000 +MAX_JERK = 36000000 MIN_LIMIT = -360 MAX_LIMIT = 360 HOME_SEQUENCE = 0 @@ -131,9 +133,9 @@ HOME_SEQUENCE = 0 [JOINT_4] TYPE = ANGULAR HOME = 0 -MAX_VELOCITY = 90 -MAX_ACCELERATION = 900 -MAX_JERK = 9000 +MAX_VELOCITY = 36000 +MAX_ACCELERATION = 360000 +MAX_JERK = 36000000 MIN_LIMIT = -185 MAX_LIMIT = 185 HOME_SEQUENCE = 0 @@ -141,9 +143,9 @@ HOME_SEQUENCE = 0 [JOINT_5] TYPE = ANGULAR HOME = 0 -MAX_VELOCITY = 90 -MAX_ACCELERATION = 900 -MAX_JERK = 9000 +MAX_VELOCITY = 36000 +MAX_ACCELERATION = 360000 +MAX_JERK = 36000000 MIN_LIMIT = -320 MAX_LIMIT = 320 HOME_SEQUENCE = 0 diff --git a/tests/twp-table-one-sided/README b/tests/twp-table-one-sided/README new file mode 100644 index 00000000000..5dbd0056cd5 --- /dev/null +++ b/tests/twp-table-one-sided/README @@ -0,0 +1,5 @@ +G53.1 on the twp-table machine with A tilting one way only, [AXIS_A] 0 to +100 while [JOINT_3] runs on to 9999: G53.1 to each side plane goes to A90 +with C carrying the side round, never to A270, a whole turn the joint has +travel for and the axis has not. The script and the HAL file are +twp-table's; the script sees the one-sided A in the INI file. diff --git a/tests/twp-table-one-sided/checkresult b/tests/twp-table-one-sided/checkresult new file mode 100755 index 00000000000..9d48d3f180e --- /dev/null +++ b/tests/twp-table-one-sided/checkresult @@ -0,0 +1,3 @@ +#!/bin/sh +# the test script counts its own failures +grep -q "^Exiting with 0 errors" "$1" diff --git a/tests/twp-table-one-sided/sim.hal b/tests/twp-table-one-sided/sim.hal new file mode 120000 index 00000000000..62bc98ee865 --- /dev/null +++ b/tests/twp-table-one-sided/sim.hal @@ -0,0 +1 @@ +../twp-table/sim.hal \ No newline at end of file diff --git a/tests/twp-table-one-sided/test-ui.py b/tests/twp-table-one-sided/test-ui.py new file mode 120000 index 00000000000..f0f197973be --- /dev/null +++ b/tests/twp-table-one-sided/test-ui.py @@ -0,0 +1 @@ +../twp-table/test-ui.py \ No newline at end of file diff --git a/tests/twp-table-one-sided/test.ini b/tests/twp-table-one-sided/test.ini new file mode 100644 index 00000000000..d08c9197039 --- /dev/null +++ b/tests/twp-table-one-sided/test.ini @@ -0,0 +1,124 @@ +[EMC] +VERSION = 1.1 +DEBUG = 0 + +[DISPLAY] +DISPLAY = ./test-ui.py + +[RS274NGC] +RS274NGC_STARTUP_CODE = G17 G21 G40 G49 G54 G64 P0.001 G80 G90 G92.1 G94 G97 G98 +PARAMETER_FILE = sim.var + +[KINS] +KINEMATICS = xyzac-trt-kins sparm=identityfirst +JOINTS = 5 + +[HAL] +HALFILE = sim.hal +HALCMD = setp xyzac-trt-kins.x-rot-point 100 +HALCMD = setp xyzac-trt-kins.y-rot-point -50 +HALCMD = setp xyzac-trt-kins.z-rot-point -80 + +[TRAJ] +# A sim that moves fast: the test checks where each move ends, not how it +# gets there, and slower limits only make it long. +COORDINATES = XYZAC +LINEAR_UNITS = mm +ANGULAR_UNITS = deg +DEFAULT_LINEAR_VELOCITY = 10000 +MAX_LINEAR_VELOCITY = 10000 +MAX_LINEAR_ACCELERATION = 100000 +DEFAULT_LINEAR_ACCELERATION = 100000 +NO_FORCE_HOMING = 1 + +[EMCMOT] +EMCMOT = motmod +SERVO_PERIOD = 1000000 +COMM_TIMEOUT = 4 + +[TASK] +TASK = milltask +CYCLE_TIME = 0.001 + +[EMCIO] +TOOL_TABLE = tool.tbl + +[AXIS_X] +MIN_LIMIT = -5000 +MAX_LIMIT = 5000 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 + +[AXIS_Y] +MIN_LIMIT = -5000 +MAX_LIMIT = 5000 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 + +[AXIS_Z] +MIN_LIMIT = -5000 +MAX_LIMIT = 5000 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 + +[AXIS_A] +MIN_LIMIT = 0 +MAX_LIMIT = 100 +MAX_VELOCITY = 36000 +MAX_ACCELERATION = 360000 + +[AXIS_C] +MIN_LIMIT = -9999 +MAX_LIMIT = 9999 +MAX_VELOCITY = 36000 +MAX_ACCELERATION = 360000 + +[JOINT_0] +TYPE = LINEAR +HOME = 0 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 +MAX_JERK = 10000000 +MIN_LIMIT = -5000 +MAX_LIMIT = 5000 +HOME_SEQUENCE = 0 + +[JOINT_1] +TYPE = LINEAR +HOME = 0 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 +MAX_JERK = 10000000 +MIN_LIMIT = -5000 +MAX_LIMIT = 5000 +HOME_SEQUENCE = 0 + +[JOINT_2] +TYPE = LINEAR +HOME = 0 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 +MAX_JERK = 10000000 +MIN_LIMIT = -5000 +MAX_LIMIT = 5000 +HOME_SEQUENCE = 0 + +[JOINT_3] +TYPE = ANGULAR +HOME = 0 +MAX_VELOCITY = 36000 +MAX_ACCELERATION = 360000 +MAX_JERK = 36000000 +MIN_LIMIT = 0 +MAX_LIMIT = 9999 +HOME_SEQUENCE = 0 + +[JOINT_4] +TYPE = ANGULAR +HOME = 0 +MAX_VELOCITY = 36000 +MAX_ACCELERATION = 360000 +MAX_JERK = 36000000 +MIN_LIMIT = -9999 +MAX_LIMIT = 9999 +HOME_SEQUENCE = 0 diff --git a/tests/twp-table-one-sided/test.sh b/tests/twp-table-one-sided/test.sh new file mode 100755 index 00000000000..765cf14fed6 --- /dev/null +++ b/tests/twp-table-one-sided/test.sh @@ -0,0 +1,4 @@ +#!/bin/bash -e +# a failed run leaves the var file behind, and it carries offsets +rm -f sim.var sim.var.bak +linuxcnc -r test.ini diff --git a/tests/twp-table-one-sided/tool.tbl b/tests/twp-table-one-sided/tool.tbl new file mode 120000 index 00000000000..4d0623410ab --- /dev/null +++ b/tests/twp-table-one-sided/tool.tbl @@ -0,0 +1 @@ +../twp-table/tool.tbl \ No newline at end of file diff --git a/tests/twp-table/README b/tests/twp-table/README index f9b61239e6e..92cf086a12f 100644 --- a/tests/twp-table/README +++ b/tests/twp-table/README @@ -6,7 +6,6 @@ P1 always the one with A positive and P2 the one with A negative, the same pose from every start; each leaves the tool along the plane's normal; a move in the plane after the turn ends where the plane puts it. -Then again with A tilting one way only, [AXIS_A] 0 to 100 while [JOINT_3] -runs on to 9999: G53.1 to each side plane goes to A90 with C carrying the -side round, never to A270, a whole turn the joint has travel for and the -axis has not. +The planes are one in general position, one with J negative, and one +whose C solutions sit near the +-180 wrap. twp-table-one-sided runs the +same script with A tilting one way only. diff --git a/tests/twp-table/checkresult b/tests/twp-table/checkresult index a9e60354d4f..9d48d3f180e 100755 --- a/tests/twp-table/checkresult +++ b/tests/twp-table/checkresult @@ -1,3 +1,3 @@ #!/bin/sh -# the test script counts its own failures, once per configuration -[ "$(grep -c "^Exiting with 0 errors" "$1")" = 2 ] +# the test script counts its own failures +grep -q "^Exiting with 0 errors" "$1" diff --git a/tests/twp-table/test-ui.py b/tests/twp-table/test-ui.py index 3f4c6f95207..16a801658e9 100755 --- a/tests/twp-table/test-ui.py +++ b/tests/twp-table/test-ui.py @@ -36,23 +36,46 @@ def drain(): if m[0] in (linuxcnc.NML_ERROR, linuxcnc.OPERATOR_ERROR): error("reported: %s" % m[1]) +# what c.wait_complete() waits for, task done with the last command sent, +# looked at every 2 ms rather than every 10: a step of several commands +# pays that wait on each +def done(timeout=60): + deadline = time.time() + timeout + while time.time() < deadline: + s.poll() + if s.echo_serial_number >= c.serial and s.state != linuxcnc.RCS_EXEC: + return + time.sleep(0.002) + error("timed out waiting for task") + +# the move has ended when task has nothing left to run and motion is at +# rest; in position with an empty queue alone is also true before task has +# sent the move on. Status is a copy task writes once a cycle, so it only +# counts from a copy written two task cycles after the command, and the +# joints only count as still over two copies a servo cycle or more apart def settled(): deadline = time.time() + 60 + s.poll() + beat = s.taskbeat + 2 last = None + last_hb = None while time.time() < deadline: s.poll() - now = [s.joint_position[i] for i in range(JOINTS)] - if s.inpos and not s.queue and now == last: - return now - last = now - time.sleep(0.05) + if s.taskbeat >= beat: + now = [s.joint_position[i] for i in range(JOINTS)] + if (s.interp_state == linuxcnc.INTERP_IDLE and s.inpos and not s.queue + and now == last and s.heartbeat != last_hb): + return now + last = now + last_hb = s.heartbeat + time.sleep(0.002) error("timed out waiting for the move") return last def mdi(*cmds): for cmd in cmds: c.mdi(cmd) - c.wait_complete(60) + done() j = settled() drain() return j @@ -76,13 +99,18 @@ def travel(j, start): mdi("G12.1 P1", "G0 X0 Y0 Z60 A0 C0") -# half.ini: A tilts one way only, its [AXIS_A] travel 0 to 100 while +# Each case starts the rotaries where it says, with one joint-space move to +# the slides: G53.1 chooses its pose from the rotaries alone, and a G0 of +# the tip would swing the slides around the table's pivot to get there. +START = "G69 G53.5 G0 X0 Y0 Z60 A%g C%g" + +# twp-table-one-sided: A tilts one way only, its [AXIS_A] travel 0 to 100 while # [JOINT_3] runs on to 9999, so a pose turned whole turns along to fit the # joint must still fit the axis: each side plane goes to A90, C carrying it one_sided = linuxcnc.ini(os.environ["INI_FILE_NAME"]).find("AXIS_A", "MIN_LIMIT") == "0" if one_sided: for i in (0, 90, -90, 180): - mdi("G69", "G0 X0 Y0 Z60 A0 C0", "G68.2 X0 Y0 Z0 I%g J90 K0" % i) + mdi(START % (0, 0), "G68.2 X0 Y0 Z0 I%g J90 K0" % i) j = mdi("G53.1") off = angle(tool_axis(j), normal(i, 90)) if off > 1e-6 or abs(j[A] - 90.0) > 1e-6: @@ -90,15 +118,16 @@ def travel(j, start): % (i, j[A], j[C], off)) print("I%g J90 G53.1 on the one-sided A: A%.3f C%.3f" % (i, j[A], j[C])) -planes = [] if one_sided else [(0, 30), (60, 30), (-90, 45), (45, -30), (150, 20)] +# a plane in general position, one with J negative, which flips the sign of +# the A that reaches it, and one whose C sits near the +-180 wrap +planes = [] if one_sided else [(60, 30), (45, -30), (150, 20)] starts = [(0, 0), (-40, 200), (40, -100)] for i, jj in planes: - nearest = [] + poses = {} for word, sign in (("", 0), ("P1", 1), ("P2", -1)): - landed = [] + landed = poses[word] = [] for start in starts: - mdi("G69", "G0 X0 Y0 Z60 A%g C%g" % start, - "G68.2 X0 Y0 Z0 I%g J%g K0" % (i, jj)) + mdi(START % start, "G68.2 X0 Y0 Z0 I%g J%g K0" % (i, jj)) j = mdi("G53.1 %s" % word) off = angle(tool_axis(j), normal(i, jj)) if off > 1e-6: @@ -108,8 +137,6 @@ def travel(j, start): error("G53.1 %s on I%g J%g from A%g C%g put A at %.4f" % (word, i, jj, start[0], start[1], j[A])) landed.append(j) - if not sign: - nearest.append((j, start)) if sign: # one pose, C on whichever turn is nearest the start for j in landed[1:]: @@ -119,10 +146,10 @@ def travel(j, start): % (word, i, jj, landed[0], j)) print("I%g J%g G53.1 %s: %s" % (i, jj, word, ", ".join("A%.3f C%.3f" % (j[A], j[C]) for j in landed))) - # no P: the nearer of the two poses, by rotary travel - for (j, start) in nearest: - mdi("G69", "G0 X0 Y0 Z60 A%g C%g" % start, "G68.2 X0 Y0 Z0 I%g J%g K0" % (i, jj)) - other = mdi("G53.1 %s" % ("P2" if j[A] > 0 else "P1")) + # no P: the nearer of the two poses, by rotary travel, against the + # other one P1 or P2 gave from the same start + for j, start, p1, p2 in zip(poses[""], starts, poses["P1"], poses["P2"]): + other = p2 if j[A] > 0 else p1 if travel(other, start) < travel(j, start) - 1e-6: error("G53.1 on I%g J%g from A%g C%g went to A%.3f C%.3f, A%.3f C%.3f is nearer" % (i, jj, start[0], start[1], j[A], j[C], other[A], other[C])) @@ -131,7 +158,7 @@ def travel(j, start): turns = () if one_sided else ((60, 30, "P1"), (60, 30, "P2"), (-90, 45, "P1")) for i, jj, word in turns: mdi("G69", "G0 X0 Y0 Z60 A0 C0", "G68.2 X0 Y0 Z0 I%g J%g K0" % (i, jj), - "G53.1 %s" % word, "G0 X10 Y5 Z20", "G1 X-10 F2000") + "G53.1 %s" % word, "G0 X10 Y5 Z20", "G1 X-10 F60000") s.poll() ri, rj = math.radians(i), math.radians(jj) px, py, pz = -10.0, 5.0, 20.0 diff --git a/tests/twp-table/test.ini b/tests/twp-table/test.ini index 44fb0768e9f..1eeacaeb6b8 100644 --- a/tests/twp-table/test.ini +++ b/tests/twp-table/test.ini @@ -20,13 +20,15 @@ HALCMD = setp xyzac-trt-kins.y-rot-point -50 HALCMD = setp xyzac-trt-kins.z-rot-point -80 [TRAJ] +# A sim that moves fast: the test checks where each move ends, not how it +# gets there, and slower limits only make it long. COORDINATES = XYZAC LINEAR_UNITS = mm ANGULAR_UNITS = deg -DEFAULT_LINEAR_VELOCITY = 100 -MAX_LINEAR_VELOCITY = 120 -MAX_LINEAR_ACCELERATION = 700 -DEFAULT_LINEAR_ACCELERATION = 300 +DEFAULT_LINEAR_VELOCITY = 10000 +MAX_LINEAR_VELOCITY = 10000 +MAX_LINEAR_ACCELERATION = 100000 +DEFAULT_LINEAR_ACCELERATION = 100000 NO_FORCE_HOMING = 1 [EMCMOT] @@ -36,7 +38,7 @@ COMM_TIMEOUT = 4 [TASK] TASK = milltask -CYCLE_TIME = 0.010 +CYCLE_TIME = 0.001 [EMCIO] TOOL_TABLE = tool.tbl @@ -44,39 +46,39 @@ TOOL_TABLE = tool.tbl [AXIS_X] MIN_LIMIT = -5000 MAX_LIMIT = 5000 -MAX_VELOCITY = 200 -MAX_ACCELERATION = 700 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 [AXIS_Y] MIN_LIMIT = -5000 MAX_LIMIT = 5000 -MAX_VELOCITY = 200 -MAX_ACCELERATION = 700 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 [AXIS_Z] MIN_LIMIT = -5000 MAX_LIMIT = 5000 -MAX_VELOCITY = 200 -MAX_ACCELERATION = 700 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 [AXIS_A] MIN_LIMIT = -120 MAX_LIMIT = 120 -MAX_VELOCITY = 90 -MAX_ACCELERATION = 900 +MAX_VELOCITY = 36000 +MAX_ACCELERATION = 360000 [AXIS_C] MIN_LIMIT = -9999 MAX_LIMIT = 9999 -MAX_VELOCITY = 90 -MAX_ACCELERATION = 900 +MAX_VELOCITY = 36000 +MAX_ACCELERATION = 360000 [JOINT_0] TYPE = LINEAR HOME = 0 -MAX_VELOCITY = 200 -MAX_ACCELERATION = 700 -MAX_JERK = 7000 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 +MAX_JERK = 10000000 MIN_LIMIT = -5000 MAX_LIMIT = 5000 HOME_SEQUENCE = 0 @@ -84,9 +86,9 @@ HOME_SEQUENCE = 0 [JOINT_1] TYPE = LINEAR HOME = 0 -MAX_VELOCITY = 200 -MAX_ACCELERATION = 700 -MAX_JERK = 7000 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 +MAX_JERK = 10000000 MIN_LIMIT = -5000 MAX_LIMIT = 5000 HOME_SEQUENCE = 0 @@ -94,9 +96,9 @@ HOME_SEQUENCE = 0 [JOINT_2] TYPE = LINEAR HOME = 0 -MAX_VELOCITY = 200 -MAX_ACCELERATION = 700 -MAX_JERK = 7000 +MAX_VELOCITY = 10000 +MAX_ACCELERATION = 100000 +MAX_JERK = 10000000 MIN_LIMIT = -5000 MAX_LIMIT = 5000 HOME_SEQUENCE = 0 @@ -104,9 +106,9 @@ HOME_SEQUENCE = 0 [JOINT_3] TYPE = ANGULAR HOME = 0 -MAX_VELOCITY = 90 -MAX_ACCELERATION = 900 -MAX_JERK = 9000 +MAX_VELOCITY = 36000 +MAX_ACCELERATION = 360000 +MAX_JERK = 36000000 MIN_LIMIT = -120 MAX_LIMIT = 120 HOME_SEQUENCE = 0 @@ -114,9 +116,9 @@ HOME_SEQUENCE = 0 [JOINT_4] TYPE = ANGULAR HOME = 0 -MAX_VELOCITY = 90 -MAX_ACCELERATION = 900 -MAX_JERK = 9000 +MAX_VELOCITY = 36000 +MAX_ACCELERATION = 360000 +MAX_JERK = 36000000 MIN_LIMIT = -9999 MAX_LIMIT = 9999 HOME_SEQUENCE = 0 diff --git a/tests/twp-table/test.sh b/tests/twp-table/test.sh index 1a5d8c386c2..765cf14fed6 100755 --- a/tests/twp-table/test.sh +++ b/tests/twp-table/test.sh @@ -2,10 +2,3 @@ # a failed run leaves the var file behind, and it carries offsets rm -f sim.var sim.var.bak linuxcnc -r test.ini -# A tilting one way only, its joint left free to turn on -sed -e '/^\[AXIS_A\]/,/^\[/s/^MIN_LIMIT = .*/MIN_LIMIT = 0/' -e '/^\[AXIS_A\]/,/^\[/s/^MAX_LIMIT = .*/MAX_LIMIT = 100/' \ - -e '/^\[JOINT_3\]/,/^\[/s/^MIN_LIMIT = .*/MIN_LIMIT = 0/' -e '/^\[JOINT_3\]/,/^\[/s/^MAX_LIMIT = .*/MAX_LIMIT = 9999/' \ - test.ini > half.ini -rm -f sim.var sim.var.bak -linuxcnc -r half.ini -rm -f half.ini