From 85993e539d53dc3060b6676082ff43133b0e5a30 Mon Sep 17 00:00:00 2001 From: 85vmh Date: Sat, 7 Nov 2026 19:06:20 +0200 Subject: [PATCH] motion: report the spindle synch of the move in progress A GUI cannot tell from stat whether the move in progress is a thread, nor its pitch: a G33 or G76 pass reports EMC_MOTION_TYPE_FEED like any G1, and the live backplot draws it as a plain feed. Showing the threads as threads in the backplot, or the pitch being cut while a program runs, needs the spindle synch of each move, which the planner already has in the TC in progress. stat gets three fields: - spindle_sync_mode: how the move in progress follows the spindle, SPINDLE_SYNC_NONE, SPINDLE_SYNC_VELOCITY (G95 feed per revolution) or SPINDLE_SYNC_POSITION (G33, G33.1, G76). - spindle_sync_pitch: machine units the move advances per spindle revolution, 0 when it is not synchronized. - spindle_sync_index_pulse_angle_offset: the angle in degrees past the spindle index pulse the synch started at, the D word of G33 or G76, 0 when there is none. The planner fills them from the TC in progress in tpUpdateMovementStatus() and clears them when there is none; task copies them into EMC_TRAJ_STAT. TC_SYNC_* now take their values from the new EMC_SPINDLE_SYNC_* in motion_types.h, so the mode is reported as is, and the linuxcnc module exports the three constants. linuxcnc.positionlogger takes a keyword-only sync=True, for the live backplot. Such a logger keeps a 12 byte logger_sync (mode, pitch, index_pulse_angle_offset) beside each point, returned by the new sync_points() with the same indices as points(), and starts a new point whenever the synch changes, so a synched move is not merged into a colinear move before it. Without sync=True, which is what every existing GUI does, it logs exactly as before and sync_points() returns None. The extra buffer costs 12 bytes a point, 1.2 MB at the 100000 point limit. The fields and sync_points() are documented in python-interface.adoc. Test: tests/motion/spindle-sync-status runs one move of each synch kind (G94 G1, G95 G1, G33 with D, G76 with D and both tapers), checks stat while it runs, and checks two loggers made with and without sync=True. --- docs/src/config/python-interface.adoc | 36 ++++ include/motion_types.h | 6 + src/emc/motion/motion.h | 5 + src/emc/nml_intf/emc.cc | 3 + src/emc/nml_intf/emc_nml.hh | 6 + src/emc/nml_intf/emcops.cc | 3 + src/emc/task/taskintf.cc | 4 + src/emc/tp/tc_types.h | 8 +- src/emc/tp/tp.c | 11 + src/emc/usr_intf/axis/extensions/emcmodule.cc | 97 ++++++++- tests/motion/spindle-sync-status/expected | 1 + tests/motion/spindle-sync-status/sync.ngc | 13 ++ tests/motion/spindle-sync-status/test-ui.py | 192 ++++++++++++++++++ tests/motion/spindle-sync-status/test.ini | 96 +++++++++ tests/motion/spindle-sync-status/test.sh | 4 + 15 files changed, 481 insertions(+), 4 deletions(-) create mode 100644 tests/motion/spindle-sync-status/expected create mode 100644 tests/motion/spindle-sync-status/sync.ngc create mode 100755 tests/motion/spindle-sync-status/test-ui.py create mode 100644 tests/motion/spindle-sync-status/test.ini create mode 100755 tests/motion/spindle-sync-status/test.sh diff --git a/docs/src/config/python-interface.adoc b/docs/src/config/python-interface.adoc index f55ccb764a2..b90c4d1dc09 100644 --- a/docs/src/config/python-interface.adoc +++ b/docs/src/config/python-interface.adoc @@ -345,6 +345,22 @@ see <> for an example. returns the current spindle status, see <>. +*spindle_sync_mode*:: '(returns integer)' - + how the motion in progress follows the spindle. One of: + * SPINDLE_SYNC_NONE - not synchronized, or no motion in progress. + * SPINDLE_SYNC_VELOCITY - feed per revolution, G95. + * SPINDLE_SYNC_POSITION - locked to the spindle position, G33, G33.1 and G76. + +*spindle_sync_index_pulse_angle_offset*:: '(returns float)' - + for a synchronized motion in progress, the angle in degrees past the + spindle index pulse at which the synch started: the D word of G33 or G76. + 0 when the motion in progress is not synchronized. + +*spindle_sync_pitch*:: '(returns float)' - + machine units the motion in progress advances per spindle revolution + (the K word of G33, the F word under G95). 0 when the motion in progress + is not synchronized. + *spindles*:: '(returns integer)' - number of spindles. Reflects `[TRAJ]SPINDLES` INI value. @@ -1078,6 +1094,10 @@ inifile = linuxcnc.ini(stat.ini_filename) == The `linuxcnc.positionlogger` type +`linuxcnc.positionlogger(stat, color0, ..., color5, geometry[, is_xyuv], sync=False)` +makes a logger; `sync=True` also keeps the spindle synch of each point, see +`sync_points()`. + Some usage hints can be gleaned from `src/emc/usr_intf/gremlin/gremlin.py`. @@ -1111,4 +1131,20 @@ Some usage hints can be gleaned from `last([int])`:: Return the most recent point on the plot or None +`sync_points()`:: + For a logger made with the keyword argument `sync=True`, return + `(bytes, npts)`: the spindle synch each point of `points()` was taken under, + same indices, copied under the same lock. Each entry is a C `logger_sync`, + 12 bytes -- uint8 mode (`stat.spindle_sync_mode`), 3 pad bytes, float32 + pitch (`stat.spindle_sync_pitch`), float32 index_pulse_angle_offset + (`stat.spindle_sync_index_pulse_angle_offset`); numpy + `[('mode', 'u1'), ('pad', 'u1', 3), ('pitch', 'f4'), + ('index_pulse_angle_offset', 'f4')]`. An entry describes the line that + ends at its point, so the point before the first entry of a synched run is + where that synch started. Such a logger + also starts a new point whenever the synch changes, so a synched move + colinear with the move before it is not merged into it. A logger made + without `sync=True` (the default) returns None and logs exactly as before; + the extra buffer costs 12 bytes a point, 1.2 MB at the 100000 point limit. + // vim: set syntax=asciidoc: diff --git a/include/motion_types.h b/include/motion_types.h index 60cadaa05ed..1de2475fe07 100644 --- a/include/motion_types.h +++ b/include/motion_types.h @@ -22,6 +22,12 @@ #define EMC_MOTION_TYPE_PROBING 5 #define EMC_MOTION_TYPE_INDEXROTARY 6 +/* spindle synch of the move in progress, as stat.spindle_sync_mode; + the same values as the planner's TC_SYNC_* */ +#define EMC_SPINDLE_SYNC_NONE 0 +#define EMC_SPINDLE_SYNC_VELOCITY 1 /* G95, feed per revolution */ +#define EMC_SPINDLE_SYNC_POSITION 2 /* G33, G33.1, G76 */ + /* modes of EMC_TRAJ_SET_SO_ENABLE, passed on as EMCMOT_SS_ENABLE */ #define EMC_SO_OVERRIDE_OFF 0 #define EMC_SO_OVERRIDE_ON 1 diff --git a/src/emc/motion/motion.h b/src/emc/motion/motion.h index 1a48aa02f57..e10580d2c72 100644 --- a/src/emc/motion/motion.h +++ b/src/emc/motion/motion.h @@ -667,6 +667,11 @@ Suggestion: Split this in to an Error and a Status flag register.. EmcPose dtg; double current_vel; double requested_vel; + /* spindle synch of the move in progress, 0 when there is none */ + int sync_mode; /* TC_SYNC_*: 0 none, 1 velocity (G95), 2 position (G33/G76) */ + double sync_uu_per_rev; /* units advanced per spindle revolution */ + double sync_index_pulse_angle_offset; /* degrees past the spindle index pulse + the synch started at (D word) */ /* S-curve motion state - for accurate jerk output */ double current_acc; /* current path acceleration */ diff --git a/src/emc/nml_intf/emc.cc b/src/emc/nml_intf/emc.cc index dee2d5699fc..768fee78944 100644 --- a/src/emc/nml_intf/emc.cc +++ b/src/emc/nml_intf/emc.cc @@ -1720,6 +1720,9 @@ void EMC_TRAJ_STAT::update(CMS * cms) cms->update(switchkins_seq); cms->update(switchkins_changed); cms->update(switchkins_flags, SWITCHKINS_MAX_TYPES); + cms->update(spindle_sync_mode); + cms->update(spindle_sync_pitch); + cms->update(spindle_sync_index_pulse_angle_offset); } /* diff --git a/src/emc/nml_intf/emc_nml.hh b/src/emc/nml_intf/emc_nml.hh index f71030156bb..cf96ad3f6de 100644 --- a/src/emc/nml_intf/emc_nml.hh +++ b/src/emc/nml_intf/emc_nml.hh @@ -1043,6 +1043,12 @@ class EMC_TRAJ_STAT:public EMC_TRAJ_STAT_MSG { // (KINSTYPE_*), as the module declares it; // -1 for a type the module does not provide + // spindle synch of the move in progress, 0 when there is none + int spindle_sync_mode; // EMC_SPINDLE_SYNC_NONE/_VELOCITY/_POSITION + double spindle_sync_pitch; // machine units per spindle revolution + // degrees past the spindle index pulse the synch started at (G33/G76 D) + double spindle_sync_index_pulse_angle_offset; + StateTag tag; }; diff --git a/src/emc/nml_intf/emcops.cc b/src/emc/nml_intf/emcops.cc index e40a1818cf9..2157e9c0ba0 100644 --- a/src/emc/nml_intf/emcops.cc +++ b/src/emc/nml_intf/emcops.cc @@ -106,6 +106,9 @@ EMC_TRAJ_STAT::EMC_TRAJ_STAT() switchkins_type(0), switchkins_seq(0), switchkins_changed(false), + spindle_sync_mode(0), + spindle_sync_pitch(0.0), + spindle_sync_index_pulse_angle_offset(0.0), tag() { // -1 = nothing known yet; a zero here would read as "types exist diff --git a/src/emc/task/taskintf.cc b/src/emc/task/taskintf.cc index 2a429b0f66b..6b7bf9c8fb8 100644 --- a/src/emc/task/taskintf.cc +++ b/src/emc/task/taskintf.cc @@ -1678,6 +1678,10 @@ int emcTrajUpdate(EMC_TRAJ_STAT * stat) stat->distance_to_go = emcmotStatus.distance_to_go; stat->dtg = emcmotStatus.dtg; stat->current_vel = emcmotStatus.current_vel; + stat->spindle_sync_mode = emcmotStatus.sync_mode; + stat->spindle_sync_pitch = emcmotStatus.sync_uu_per_rev; + stat->spindle_sync_index_pulse_angle_offset = + emcmotStatus.sync_index_pulse_angle_offset; if (EMC_DEBUG_MOTION_TIME & emc_debug) { if (stat->id != last_id) { if (last_id != last_id_printed) { diff --git a/src/emc/tp/tc_types.h b/src/emc/tp/tc_types.h index c7406ecf801..98bafab5d05 100644 --- a/src/emc/tp/tc_types.h +++ b/src/emc/tp/tc_types.h @@ -16,6 +16,7 @@ #include #include #include +#include #include "spherical_arc.h" #include "../nml_intf/state_tag.h" @@ -36,10 +37,11 @@ typedef enum { TC_SPHERICAL = 4 } tc_motion_type_t; +/* reported as is in stat.spindle_sync_mode */ typedef enum { - TC_SYNC_NONE = 0, - TC_SYNC_VELOCITY, - TC_SYNC_POSITION + TC_SYNC_NONE = EMC_SPINDLE_SYNC_NONE, + TC_SYNC_VELOCITY = EMC_SPINDLE_SYNC_VELOCITY, + TC_SYNC_POSITION = EMC_SPINDLE_SYNC_POSITION } tc_spindle_sync_t; typedef enum { diff --git a/src/emc/tp/tp.c b/src/emc/tp/tp.c index 2b4024c9ab1..705f5665945 100644 --- a/src/emc/tp/tp.c +++ b/src/emc/tp/tp.c @@ -3217,6 +3217,9 @@ STATIC int tpUpdateMovementStatus(TP_STRUCT * const tp, TC_STRUCT const * const emcmotStatus->requested_vel = 0; emcmotStatus->current_vel = 0; emcmotStatus->spindleSync = 0; + emcmotStatus->sync_mode = TC_SYNC_NONE; + emcmotStatus->sync_uu_per_rev = 0; + emcmotStatus->sync_index_pulse_angle_offset = 0; // Clear S-curve motion state emcmotStatus->current_acc = 0; @@ -3245,6 +3248,14 @@ STATIC int tpUpdateMovementStatus(TP_STRUCT * const tp, TC_STRUCT const * const tp->execId = tc->id; emcmotStatus->requested_vel = tc->reqvel; emcmotStatus->current_vel = tc->currentvel; + emcmotStatus->sync_mode = tc->synchronized; + if (tc->synchronized != TC_SYNC_NONE) { + emcmotStatus->sync_uu_per_rev = tc->uu_per_rev; + emcmotStatus->sync_index_pulse_angle_offset = tc->angle_offset * 360.0; + } else { + emcmotStatus->sync_uu_per_rev = 0; + emcmotStatus->sync_index_pulse_angle_offset = 0; + } // Output accurate S-curve motion state (for accurate jerk calculation) emcmotStatus->current_acc = tc->currentacc; diff --git a/src/emc/usr_intf/axis/extensions/emcmodule.cc b/src/emc/usr_intf/axis/extensions/emcmodule.cc index 635726e7cd7..d8a3968e7c2 100644 --- a/src/emc/usr_intf/axis/extensions/emcmodule.cc +++ b/src/emc/usr_intf/axis/extensions/emcmodule.cc @@ -1198,6 +1198,17 @@ static PyMemberDef Stat_members[] = { "currently taking place."}, { "distance_to_go", T_DOUBLE, O(motion.traj.distance_to_go), READONLY, NULL}, { "current_vel", T_DOUBLE, O(motion.traj.current_vel), READONLY, NULL}, + { "spindle_sync_mode", T_INT, O(motion.traj.spindle_sync_mode), READONLY, + "How the motion in progress follows the spindle: SPINDLE_SYNC_NONE,\n" + "SPINDLE_SYNC_VELOCITY (G95 feed per revolution) or SPINDLE_SYNC_POSITION\n" + "(G33, G33.1, G76)."}, + { "spindle_sync_pitch", T_DOUBLE, O(motion.traj.spindle_sync_pitch), READONLY, + "Machine units the motion in progress advances per spindle revolution,\n" + "0 when it is not synchronized."}, + { "spindle_sync_index_pulse_angle_offset", T_DOUBLE, + O(motion.traj.spindle_sync_index_pulse_angle_offset), READONLY, + "Degrees past the spindle index pulse the synchronized motion in\n" + "progress started at (the D word of G33 or G76), 0 when there is none."}, { "feed_override_enabled", T_BOOL, O(motion.traj.feed_override_enabled), READONLY, NULL}, { "adaptive_feed_enabled", T_BOOL, O(motion.traj.adaptive_feed_enabled), READONLY, NULL}, { "feed_hold_enabled", T_BOOL, O(motion.traj.feed_hold_enabled), READONLY, NULL}, @@ -2882,12 +2893,23 @@ struct logger_point { struct color c2; }; +// The spindle synch a logged point was taken under, kept beside p (same +// index) when the logger is made with sync=True; see Logger_get_sync_points. +struct logger_sync { + unsigned char mode; // stat.spindle_sync_mode + unsigned char pad[3]; + float pitch; // stat.spindle_sync_pitch + float index_pulse_angle_offset; // stat.spindle_sync_index_pulse_angle_offset +}; +static_assert(sizeof(struct logger_sync) == 12, "logger_sync layout is documented"); + #define NUMCOLORS (6) #define MAX_POINTS (100000) typedef struct { PyObject_HEAD int npts, mpts, lpts; struct logger_point *p; + struct logger_sync *sp; // NULL unless made with sync=True struct color colors[NUMCOLORS]; bool exit, clear, changed; char *geometry; @@ -2916,10 +2938,27 @@ static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; static void LOCK() { pthread_mutex_lock(&mutex); } static void UNLOCK() { pthread_mutex_unlock(&mutex); } -static int Logger_init(pyPositionLogger *self, PyObject *a, PyObject * /*k*/) { +static int Logger_init(pyPositionLogger *self, PyObject *a, PyObject *k) { char *geometry; struct color *c = self->colors; + // The positional arguments stay as they were; sync is keyword only. + bool sync = false; + if(k) { + PyObject *key, *value; + Py_ssize_t pos = 0; + while(PyDict_Next(k, &pos, &key, &value)) { + if(!PyUnicode_Check(key) || PyUnicode_CompareWithASCIIString(key, "sync")) { + PyErr_Format(PyExc_TypeError, + "positionlogger() got an unexpected keyword argument '%S'", key); + return -1; + } + int t = PyObject_IsTrue(value); + if(t < 0) return -1; + sync = t; + } + } self->p = (logger_point*)malloc(sizeof(self->p[0])); // Will be realloc'ed + self->sp = sync ? (logger_sync*)malloc(sizeof(self->sp[0])) : NULL; self->npts = self->mpts = 0; self->exit = self->clear = 0; self->changed = 1; @@ -2945,6 +2984,7 @@ static int Logger_init(pyPositionLogger *self, PyObject *a, PyObject * /*k*/) { static void Logger_dealloc(pyPositionLogger *s) { free(s->p); + free(s->sp); Py_XDECREF(s->st); free(s->geometry); PyObject_Del(s); @@ -3023,6 +3063,22 @@ static PyObject *Logger_start(pyPositionLogger *s, PyObject *o) { struct logger_point *op = &s->p[s->npts-1]; struct logger_point *oop = &s->p[s->npts-2]; bool add_point = s->npts < 2 || c != op->c; + struct logger_sync sy = {}; + if(s->sp) { + sy.mode = status->motion.traj.spindle_sync_mode; + sy.pitch = status->motion.traj.spindle_sync_pitch; + sy.index_pulse_angle_offset = + status->motion.traj.spindle_sync_index_pulse_angle_offset; + // A synch starting or changing starts a point of its own, + // so the start of a synched move, where its phase is set, + // is not merged into a colinear move before it. + if(s->npts) { + const struct logger_sync &osy = s->sp[s->npts-1]; + add_point = add_point || sy.mode != osy.mode + || sy.pitch != osy.pitch + || sy.index_pulse_angle_offset != osy.index_pulse_angle_offset; + } + } double x, y, z, rx, ry, rz; if(s->is_xyuv) { x = status->motion.traj.position.tran.x - status->task.toolOffset.tran.x, @@ -3076,11 +3132,17 @@ static PyObject *Logger_start(pyPositionLogger *s, PyObject *o) { s->npts -= adjust; memmove(s->p, s->p + adjust, sizeof(struct logger_point) * s->npts); + if(s->sp) + memmove(s->sp, s->sp + adjust, + sizeof(struct logger_sync) * s->npts); } else { s->mpts = 2 * s->mpts + 2; s->changed = 1; s->p = (struct logger_point*) realloc(s->p, sizeof(struct logger_point) * s->mpts); + if(s->sp) + s->sp = (struct logger_sync*) realloc(s->sp, + sizeof(struct logger_sync) * s->mpts); } UNLOCK(); op = &s->p[s->npts-1]; @@ -3099,12 +3161,14 @@ static PyObject *Logger_start(pyPositionLogger *s, PyObject *o) { np.rx = rx; np.ry = ry; np.rz = rz; np.c = np.c2 = c; } + if(s->sp) s->sp[s->npts] = s->sp[s->npts+1] = sy; s->npts += 2; } else { struct logger_point &np = s->p[s->npts]; np.x = x; np.y = y; np.z = z; np.rx = rx; np.ry = ry; np.rz = rz; np.c = np.c2 = c; + if(s->sp) s->sp[s->npts] = sy; s->npts++; } } else { @@ -3200,6 +3264,28 @@ static PyObject *Logger_get_points(pyPositionLogger *s, PyObject * /*o*/) { return Py_BuildValue("Nii", buf, npts, is_xyuv); } +// The spindle synch of each point points() returns, same indices, for a +// logger made with sync=True; None otherwise. Returns (bytes, npts); each +// entry is a `struct logger_sync`, 12 bytes: uint8 mode, 3 pad bytes, +// float32 pitch, float32 index_pulse_angle_offset (numpy: [('mode','u1'), +// ('pad','u1',3), ('pitch','f4'), ('index_pulse_angle_offset','f4')]). An +// entry is the state the point was taken under, so it describes the line +// that ends at that point. Does not advance lpts: points() does that. +static PyObject *Logger_get_sync_points(pyPositionLogger *s, PyObject * /*o*/) { + LOCK(); + if(!s->sp) { + UNLOCK(); + Py_RETURN_NONE; + } + int npts = s->npts; + if(npts < 0) npts = 0; + Py_ssize_t nbytes = (Py_ssize_t)npts * (Py_ssize_t)sizeof(struct logger_sync); + PyObject *buf = PyBytes_FromStringAndSize((const char*)s->sp, nbytes); + UNLOCK(); + if(!buf) return NULL; + return Py_BuildValue("Ni", buf, npts); +} + static PyMemberDef Logger_members[] = { {(char*)"npts", T_INT, offsetof(pyPositionLogger, npts), READONLY, NULL}, {}, @@ -3217,6 +3303,11 @@ static PyMethodDef Logger_methods[] = { {"points", (PyCFunction)Logger_get_points, METH_NOARGS, "Return (bytes, npts, is_xyuv): a copy of the logged point buffer for " "VBO upload by the core renderer"}, + {"sync_points", (PyCFunction)Logger_get_sync_points, METH_NOARGS, + "Return (bytes, npts): the spindle synch of each point of points(), " + "12 bytes each (uint8 mode, 3 pad, float32 pitch, " + "float32 index_pulse_angle_offset), " + "or None unless the logger was made with sync=True"}, {"set_depth", (PyCFunction)Logger_set_depth, METH_VARARGS, "set the Z and W depths for foam cutter"}, {"set_colors", (PyCFunction)Logger_set_colors, METH_VARARGS, @@ -3418,6 +3509,10 @@ PyMODINIT_FUNC PyInit_linuxcnc(void) ENUMX(4, EMC_MOTION_TYPE_PROBING); ENUMX(4, EMC_MOTION_TYPE_INDEXROTARY); + ENUMX(4, EMC_SPINDLE_SYNC_NONE); + ENUMX(4, EMC_SPINDLE_SYNC_VELOCITY); + ENUMX(4, EMC_SPINDLE_SYNC_POSITION); + ENUM(KINEMATICS_IDENTITY); ENUM(KINEMATICS_FORWARD_ONLY); ENUM(KINEMATICS_INVERSE_ONLY); diff --git a/tests/motion/spindle-sync-status/expected b/tests/motion/spindle-sync-status/expected new file mode 100644 index 00000000000..8c0382aa4aa --- /dev/null +++ b/tests/motion/spindle-sync-status/expected @@ -0,0 +1 @@ +Completed successfully diff --git a/tests/motion/spindle-sync-status/sync.ngc b/tests/motion/spindle-sync-status/sync.ngc new file mode 100644 index 00000000000..7b0425ffc80 --- /dev/null +++ b/tests/motion/spindle-sync-status/sync.ngc @@ -0,0 +1,13 @@ +(One move of each kind of spindle synch, for stat.spindle_sync_* and the) +(position logger's sync_points.) +g21 g18 g90 g8 +s600 m3 +g4 p1 (let the simulated spindle reach speed) +g94 g1 x10 z5 f500 +g95 g1 z0 f0.2 +g94 g0 x10 z5 +g33 z-10 k1.5 d90 +g0 x10 z5 +g76 p2 z-10 i-1 j0.4 k0.6 r1 q29.5 h0 e1 l3 d45 +m5 +m2 diff --git a/tests/motion/spindle-sync-status/test-ui.py b/tests/motion/spindle-sync-status/test-ui.py new file mode 100755 index 00000000000..33ea55fe66d --- /dev/null +++ b/tests/motion/spindle-sync-status/test-ui.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 +# +# Checks stat.spindle_sync_mode, _pitch and _index_pulse_angle_offset, and +# positionlogger.sync_points() +# while a program runs one move of each kind of spindle synch: none (G94 G1), +# velocity (G95 G1), position (G33 with D, G76 with D and both tapers). +# +# stat is polled every few milliseconds and each sample filed under the +# motion line it was taken on; two position loggers run at the same time, +# one made with sync=True and one without. + +import linuxcnc +import linuxcnc_util + +import os +import struct +import sys +import threading +import time + +INTERPTIMEOUT = 5 # Max seconds to wait for the interpreter to start +GCODETIMEOUT = 120 # Max seconds for the gcode program to run + +# line numbers in sync.ngc +L_G94, L_G95, L_G33, L_G76 = 6, 7, 9, 11 + +failures = [] + + +def check(what, cond, detail=""): + print(("ok " if cond else "FAIL ") + what + + (": " + str(detail) if detail and not cond else "")) + if not cond: + failures.append(what) + + +def close(a, b, tol=1e-6): + return abs(a - b) < tol + + +ngcfile = None +for i in range(len(sys.argv) - 1): + if sys.argv[i] == "-ngc": + ngcfile = sys.argv[i + 1] + break +if not ngcfile or not os.path.exists(ngcfile): + print("Missing NGC-file; run with: test-ui.py -ngc ngcfile.ngc") + sys.exit(1) + +c = linuxcnc.command() +s = linuxcnc.stat() +e = linuxcnc.error_channel() + +l = linuxcnc_util.LinuxCNC(command=c, status=s, error=e) +c.state(linuxcnc.STATE_ESTOP_RESET) +c.state(linuxcnc.STATE_ON) + +c.home(-1) +c.wait_complete() +l.wait_for_home([1, 1, 1, 0, 0, 0, 0, 0, 0]) + +s.poll() +check("constants: SPINDLE_SYNC_NONE/_VELOCITY/_POSITION are 0, 1, 2", + (linuxcnc.SPINDLE_SYNC_NONE, linuxcnc.SPINDLE_SYNC_VELOCITY, + linuxcnc.SPINDLE_SYNC_POSITION) == (0, 1, 2)) +check("idle: no synch", + (s.spindle_sync_mode, s.spindle_sync_pitch, + s.spindle_sync_index_pulse_angle_offset) == (0, 0.0, 0.0)) + +COLORS = [(255, 255, 255, 255)] * 6 +logger = linuxcnc.positionlogger(linuxcnc.stat(), *COLORS, "XYZ", sync=True) +plain = linuxcnc.positionlogger(linuxcnc.stat(), *COLORS, "XYZ") +try: + linuxcnc.positionlogger(linuxcnc.stat(), *COLORS, "XYZ", bogus=1) + check("positionlogger rejects an unknown keyword", False) +except TypeError: + check("positionlogger rejects an unknown keyword", True) +threads = [threading.Thread(target=lg.start, args=(0.002,)) + for lg in (logger, plain)] +for t in threads: + t.start() + +c.mode(linuxcnc.MODE_AUTO) +c.program_open(ngcfile) +c.auto(linuxcnc.AUTO_RUN, 1) + +start = time.time() +while time.time() - start < INTERPTIMEOUT: + s.poll() + if s.interp_state != linuxcnc.INTERP_IDLE: + break + time.sleep(0.01) +if s.interp_state == linuxcnc.INTERP_IDLE: + print("Timed out starting interpreter") + sys.exit(1) + +# motion line -> set of (mode, pitch, index pulse angle offset) seen on it +seen = {} +start = time.time() +s.poll() +while s.interp_state != linuxcnc.INTERP_IDLE and time.time() - start < GCODETIMEOUT: + seen.setdefault(s.motion_line, set()).add( + (s.spindle_sync_mode, round(s.spindle_sync_pitch, 6), + round(s.spindle_sync_index_pulse_angle_offset, 6))) + time.sleep(0.002) + s.poll() +if s.interp_state != linuxcnc.INTERP_IDLE: + print("Timed out running the GCode program") + sys.exit(1) + +for lg in (logger, plain): + lg.stop() +for t in threads: + t.join() + +NONE = linuxcnc.SPINDLE_SYNC_NONE +VEL = linuxcnc.SPINDLE_SYNC_VELOCITY +POS = linuxcnc.SPINDLE_SYNC_POSITION + +# --- stat ---------------------------------------------------------------- + +check("G94 G1: never synched", seen.get(L_G94) == {(NONE, 0.0, 0.0)}, + seen.get(L_G94)) +g95 = seen.get(L_G95, set()) +check("G95 G1 F0.2: velocity synch at 0.2 mm/rev, angle offset 0", + (VEL, 0.2, 0.0) in g95 and not any(m == POS for m, _, _ in g95), g95) +g33 = seen.get(L_G33, set()) +check("G33 K1.5 D90: position synch at 1.5 mm/rev, 90 degrees", + (POS, 1.5, 90.0) in g33 and not any(m == VEL for m, _, _ in g33), g33) +g76 = {x for x in seen.get(L_G76, set()) if x[0] == POS} +check("G76 P2 D45: position synch at 2 mm/rev and 45 degrees on the thread", + (POS, 2.0, 45.0) in g76, g76) +check("... D45 on the entry taper, 0 on the exit taper, as the interpreter " + "passes them", {a for _, _, a in g76} == {45.0, 0.0}, g76) +check("... no velocity synch in a G76", + not any(x[0] == VEL for x in seen.get(L_G76, set())), seen.get(L_G76)) +s.poll() +check("program end: no synch", + (s.spindle_sync_mode, s.spindle_sync_pitch, + s.spindle_sync_index_pulse_angle_offset) == (0, 0.0, 0.0)) + +# --- position logger ----------------------------------------------------- + +check("a logger made without sync=True has no sync_points", + plain.sync_points() is None) +pbuf, npts, _ = logger.points() +sbuf, snpts = logger.sync_points() +check("sync_points has one 12 byte entry per point", + snpts == npts and len(sbuf) == 12 * npts and npts > 0, (snpts, npts, len(sbuf))) +check("points() keeps its 32 byte layout", len(pbuf) == 32 * npts, len(pbuf)) +syncs = [struct.unpack_from("=B3xff", sbuf, 12 * i) for i in range(snpts)] +pts = [struct.unpack_from("=fff", pbuf, 32 * i) for i in range(npts)] + +check("the logged velocity synch is G95's 0.2 mm/rev", + any(m == VEL for m, _, _ in syncs) + and all(close(p, 0.2) and a == 0.0 for m, p, a in syncs if m == VEL), + {x for x in syncs if x[0] == VEL}) +pos = {(round(p, 4), round(a, 4)) for m, p, a in syncs if m == POS} +check("the logged position synchs are the G33's and the G76's", + (1.5, 90.0) in pos and (2.0, 45.0) in pos, pos) +check("no synch is logged with a pitch or angle offset", all( + p == 0.0 and a == 0.0 for m, p, a in syncs if m == NONE)) + +# Every synch start has a point of its own. An entry describes the line that +# ends at its point, so the G33's first entry is the line from where the G33 +# starts, X10 Z5 (the G0 before it ends there), to where it ends, Z-10. +starts = [i for i in range(1, snpts) if syncs[i] != syncs[i - 1]] +g33start = [i for i in starts if syncs[i][0] == POS and close(syncs[i][1], 1.5, 1e-4)] +check("the G33 starts a point of its own", len(g33start) == 1, g33start) +if g33start: + i = g33start[0] + x0, _, z0 = pts[i - 1] + x1, _, z1 = pts[i] + check("... its line runs from X10 Z5 to X10 Z-10", + close(x0, 10, 0.05) and close(z0, 5, 0.05) + and close(x1, 10, 0.05) and close(z1, -10, 0.05), + (pts[i - 1], pts[i])) +# G76 L3: each pass starts with the entry taper, so each pass is a synch +# start; with E and L3 there are at least two passes in this cycle. +g76starts = [i for i in starts if syncs[i][0] == POS and syncs[i - 1][0] == NONE + and not close(syncs[i][1], 1.5, 1e-4)] +check("each G76 pass starts a point of its own", len(g76starts) >= 2, + len(g76starts)) + +_, pnpts, _ = plain.points() +check("the logger without sync has no more points than the one with it", + 0 < pnpts <= npts, (pnpts, npts)) + +if failures: + print("%d check(s) failed" % len(failures)) + sys.exit(1) +sys.exit(0) diff --git a/tests/motion/spindle-sync-status/test.ini b/tests/motion/spindle-sync-status/test.ini new file mode 100644 index 00000000000..8de87127bab --- /dev/null +++ b/tests/motion/spindle-sync-status/test.ini @@ -0,0 +1,96 @@ +[EMC] +DEBUG = 0 +VERSION = 1.1 + +[DISPLAY] +DISPLAY = ./test-ui.py -ngc ./sync.ngc + +[TASK] +TASK = milltask +CYCLE_TIME = 0.001 + +[RS274NGC] +PARAMETER_FILE = sim.var + +[EMCMOT] +EMCMOT = motmod +BASE_PERIOD = 0 +SERVO_PERIOD = 1000000 + +[HAL] +HALFILE = LIB:core_sim.hal +HALFILE = LIB:sim_spindle_encoder.hal + +[TRAJ] +NO_FORCE_HOMING = 1 +AXES = 3 +COORDINATES = X Y Z +HOME = 0 0 0 +LINEAR_UNITS = mm +ANGULAR_UNITS = degree +DEFAULT_LINEAR_VELOCITY = 100 +MAX_LINEAR_VELOCITY = 500 + +[KINS] +KINEMATICS = trivkins +JOINTS = 3 + +[AXIS_X] +MIN_LIMIT = -1000.0 +MAX_LIMIT = 1000.0 +MAX_VELOCITY = 500 +MAX_ACCELERATION = 3000 + +[JOINT_0] +TYPE = LINEAR +HOME = 0.000 +MAX_VELOCITY = 500 +MAX_ACCELERATION = 3000 +BACKLASH = 0.000 +INPUT_SCALE = 4000 +OUTPUT_SCALE = 1.000 +MIN_LIMIT = -1000.0 +MAX_LIMIT = 1000.0 +FERROR = 0.050 +MIN_FERROR = 0.010 +HOME_SEQUENCE = 0 + +[AXIS_Y] +MIN_LIMIT = -1000.0 +MAX_LIMIT = 1000.0 +MAX_VELOCITY = 500 +MAX_ACCELERATION = 3000 + +[JOINT_1] +TYPE = LINEAR +HOME = 0.000 +MAX_VELOCITY = 500 +MAX_ACCELERATION = 3000 +BACKLASH = 0.000 +INPUT_SCALE = 4000 +OUTPUT_SCALE = 1.000 +MIN_LIMIT = -1000.0 +MAX_LIMIT = 1000.0 +FERROR = 0.050 +MIN_FERROR = 0.010 +HOME_SEQUENCE = 0 + +[AXIS_Z] +MIN_LIMIT = -500.0 +MAX_LIMIT = 500.0 +MAX_VELOCITY = 500 +MAX_ACCELERATION = 3000 + +[JOINT_2] +TYPE = LINEAR +HOME = 0.0 +MAX_VELOCITY = 500 +MAX_ACCELERATION = 3000 +BACKLASH = 0.000 +INPUT_SCALE = 4000 +OUTPUT_SCALE = 1.000 +MIN_LIMIT = -500.0 +MAX_LIMIT = 500.0 +FERROR = 0.050 +MIN_FERROR = 0.010 +HOME_SEQUENCE = 0 diff --git a/tests/motion/spindle-sync-status/test.sh b/tests/motion/spindle-sync-status/test.sh new file mode 100755 index 00000000000..87a3bca3260 --- /dev/null +++ b/tests/motion/spindle-sync-status/test.sh @@ -0,0 +1,4 @@ +#!/bin/bash +if linuxcnc -r test.ini; then + echo "Completed successfully" > result +fi