From dae3be85e1cc2abaf519ae27a26666d4836e5bfd Mon Sep 17 00:00:00 2001 From: 85vmh Date: Sat, 12 Sep 2026 20:52:51 +0200 Subject: [PATCH] Add spindle angle offset (D word) for G33 and G76 threading Why --- Spindle synchronized motion always starts at the spindle index pulse, so every threading pass begins at the same angular position. Cutting a multi-start thread means starting successive passes at a fixed angle past the index instead, and there was no way to ask for that. D on G33 and G76 now gives that start angle in degrees. The synchronized move is held at rest after the index until the spindle has turned through it. D is optional and defaults to zero, which is the existing behaviour of starting at the index pulse; a two-start thread is cut by running the program twice, with D0 and D180. What changed ------------ Interpreter: interp_check.cc accepts D on G33 and G76, which previously rejected it as "D word with no G41 ... to use it". convert_straight() and convert_threading_cycle() read it and pass it to the canon layer. A negative D is taken as its magnitude rather than rejected -- the value is a delay past the index and has no direction. Canon and NML: START_SPEED_FEED_SYNCH() gains an angle_degrees parameter, defaulted to 0.0 so the interface and its non-task implementations stay source compatible. EMC_TRAJ_SET_SPINDLESYNC carries angular_offset_degrees, which reaches the planner through emcTrajSetSpindleSync() and EMCMOT_SET_SPINDLESYNC. Planner: tpSetSpindleSync() converts degrees to revolutions once and stores it in tp->spindle.pending_offset; tcSetupState() copies that into each TC as it is queued, so the passes of a G76 cycle each carry their own value rather than reading whatever the TP holds when they run. The hold itself is in tpSyncPositionMode(): spindle.revs resets to 0 at the index pulse, so while it is below the requested offset the move is held at target_vel 0. On reaching the angle, spindle.offset is set to the current revs so position mode starts tracking from zero error there. tpCheckAtSpeed() skips the sync_accel ramp when an offset is requested, because that ramp exists to catch the axis up to an already-turning spindle, which is what the hold is there to prevent. The hold is scoped to one index pulse by tp->spindle.angle_hold_pending, armed where the index wait is armed and cleared once the angle is reached. This matters for G76 with an entry taper: such a pass emits three synchronized moves, all carrying the same angle_offset, and only the first of them follows an index pulse. Without the flag the later segments also reset spindle.offset, discarding the value tpCompleteSegment() accumulates and shifting the thread phase part way through a pass. Compatibility: the canon signature change is a break in the public interface tests/interp/compile guards, so use-rs274.cc is updated -- per that test's README this is noted deliberately. canonmodule.cc spells out its argument list so the C++ default survives into Python; a bare function pointer would have made the new argument mandatory and broken every existing three-argument caller. Docs: the D word is documented for both G33 and G76, including that it defaults to zero, that a negative value is used as its magnitude, and that values of 360 or more are not reduced. The letter table in overview.adoc gains the second meaning of D. A note under G33 Technical Info records that a D offset holds the axis at rest instead of using the usual post-index acceleration compensation. Testing ------- tests/interp/g76-d-word (new) runs the interpreter over G33 and G76 with no D, with D, and with a negative D, and checks what reaches the canon layer. saicanon prints the angle only when one was asked for, so the expected output of the existing g33.1 and g76 tests is unchanged. tests/motion/spindle-angle-offset (new) runs a sim machine with sim_spindle, whose index pulse resets spindle.0.revs -- the same reset D is measured against. It samples Z, X, revs and the index pin every servo period and checks that G33 without D starts at the index, that D180 waits half a turn longer, that D-180 behaves identically to D180, that a G76 cycle with D180 starts half a turn later than an otherwise identical cycle without it, and that both G76 cycles still cut the programmed pitch. Each angle check is a difference between two passes, because detecting that the tool has started moving costs a fixed amount of rotation that only cancels that way. The new test was confirmed to fail when the hold is disabled. tests/interp and tests/motion pass in full, 90 tests, with tests/interp/compile skipped as it already was. Not covered by the tests ------------------------ The segment scoping described above is not caught by any automated test. It was confirmed by restoring the unscoped version and rerunning: the test still passes, because core_sim.hal feeds motor-pos-cmd straight back as motor-pos-fb, so the simulated axis tracks perfectly and the discrepancy the fault feeds on is essentially zero. Reproducing it needs a machine, or a sim with a servo model that has real following error. Reviewers with a lathe are asked to look at a G76 pass with an entry taper (L1 or L3) and a D word. tests/interp/compile is still disabled and still does not link, for the unrelated and pre-existing reason that it is also missing SET_MOTION_CONTROL_MODE. Only the START_SPEED_FEED_SYNCH signature was brought up to date here. Co-Authored-By: Claude Opus 5 (1M context) --- docs/src/gcode/g-code.adoc | 37 ++- docs/src/gcode/overview.adoc | 3 +- src/emc/motion/command.c | 2 +- src/emc/motion/motion.h | 1 + src/emc/nml_intf/canon.hh | 2 +- src/emc/nml_intf/emc.cc | 1 + src/emc/nml_intf/emc.hh | 2 +- src/emc/nml_intf/emc_nml.hh | 4 +- src/emc/rs274ngc/canonmodule.cc | 8 +- src/emc/rs274ngc/gcodemodule.cc | 2 +- src/emc/rs274ngc/interp_check.cc | 5 +- src/emc/rs274ngc/interp_convert.cc | 21 +- src/emc/sai/saicanon.cc | 11 +- src/emc/task/emccanon.cc | 3 +- src/emc/task/emctaskmain.cc | 2 +- src/emc/task/taskintf.cc | 3 +- src/emc/tp/tc.c | 1 + src/emc/tp/tc_types.h | 1 + src/emc/tp/tp.c | 46 ++- src/emc/tp/tp.h | 2 +- src/emc/tp/tp_types.h | 8 + tests/interp/compile/use-rs274.cc | 2 +- tests/interp/g76-d-word/README | 10 + tests/interp/g76-d-word/d-word.ngc | 27 ++ tests/interp/g76-d-word/expected | 7 + tests/interp/g76-d-word/test.sh | 5 + .../spindle-angle-offset/angle-offset.ngc | 39 +++ tests/motion/spindle-angle-offset/expected | 1 + tests/motion/spindle-angle-offset/sample.hal | 24 ++ tests/motion/spindle-angle-offset/test-ui.py | 313 ++++++++++++++++++ tests/motion/spindle-angle-offset/test.ini | 97 ++++++ tests/motion/spindle-angle-offset/test.sh | 4 + 32 files changed, 666 insertions(+), 28 deletions(-) create mode 100644 tests/interp/g76-d-word/README create mode 100644 tests/interp/g76-d-word/d-word.ngc create mode 100644 tests/interp/g76-d-word/expected create mode 100755 tests/interp/g76-d-word/test.sh create mode 100644 tests/motion/spindle-angle-offset/angle-offset.ngc create mode 100644 tests/motion/spindle-angle-offset/expected create mode 100644 tests/motion/spindle-angle-offset/sample.hal create mode 100755 tests/motion/spindle-angle-offset/test-ui.py create mode 100644 tests/motion/spindle-angle-offset/test.ini create mode 100755 tests/motion/spindle-angle-offset/test.sh diff --git a/docs/src/gcode/g-code.adoc b/docs/src/gcode/g-code.adoc index 6253e57afb2..6ae25c5bd9f 100644 --- a/docs/src/gcode/g-code.adoc +++ b/docs/src/gcode/g-code.adoc @@ -1041,10 +1041,11 @@ It is an error if : [source,ngc] ---- -G33 X- Y- Z- K- $- +G33 X- Y- Z- K- $- D- ---- * 'K' - distance per revolution +* 'D' - (optional) start angle offset from the spindle index pulse, in degrees For spindle-synchronized motion in one direction, code 'G33 X- Y- Z- K-' where K gives the distance moved in XYZ for each revolution of the @@ -1064,6 +1065,20 @@ speed pins, so multiple passes line up. 'G33' moves end at the programmed endpoint. G33 could be used to cut tapered threads or a fusee. +The (optional) 'D' argument delays the start of the motion until the spindle +has turned 'D' degrees past the index pulse, instead of starting at the index +pulse itself. If 'D' is omitted the default is zero, which starts at the index +pulse as before. This is how successive starts of a multi-start thread are cut: +the same thread program is run once per start, each with a different 'D'. +For example, a two-start thread is cut with 'D0' and then 'D180'; a three-start +thread with 'D0', 'D120' and 'D240'. + +[NOTE] +'D' is an angle past the index pulse and so is only meaningful as a positive +number. A negative value is not an error: its magnitude is used, so 'D-90' +behaves exactly as 'D90'. Values of 360 or more are not reduced, so 'D450' +waits a full turn longer than 'D90'. + All the axis words are optional, except that at least one must be used. [NOTE] @@ -1082,6 +1097,10 @@ angle. That means that Z will reach the correct position just as it finishes accelerating to the proper speed, and can immediately begin cutting a good thread. +When a 'D' offset is programmed the axis is instead held at rest until the +spindle reaches the requested angle, and tracking begins from there. Allow +enough clearance before the thread for the axis to accelerate from that point. + .HAL Connections The pin 'spindle.N.at-speed' must be set or driven true for the motion to start. Additionally spindle.N.revs must increase by 1 for each revolution @@ -1960,7 +1979,7 @@ In example S100 with 1.25MM per revolution thread pitch gives a feed of F125. [source,ngc] ---- -G76 P- Z- I- J- R- K- Q- H- E- L- $- +G76 P- Z- I- J- R- K- Q- H- E- L- $- D- ---- .G76 Threading @@ -2001,6 +2020,20 @@ Unnecessarily high degression values will produce an unnecessarily high number o * 'H-' - The number of 'spring passes'. Spring passes are additional passes at full thread depth. If no additional passes are desired, program 'H0'. +* 'D-' - The 'start angle offset' in degrees from the spindle index pulse. + Every pass of the cycle begins once the spindle has turned 'D' degrees past + the index pulse, instead of at the index pulse itself. + If 'D' is omitted the default is zero, which starts every pass at the index + pulse as before. + This cuts one start of a multi-start thread: run the same 'G76' cycle once per + start, each with a different 'D'. A two-start thread is cut with 'D0' and then + 'D180', a three-start thread with 'D0', 'D120' and 'D240'. + +[NOTE] +'D' is an angle past the index pulse and so is only meaningful as a positive +number. A negative value is not an error: its magnitude is used, so 'D-90' +behaves exactly as 'D90'. Values of 360 or more are not reduced, so 'D450' +waits a full turn longer than 'D90'. Thread entries and exits can be programmed tapered with the 'E' and 'L' values. diff --git a/docs/src/gcode/overview.adoc b/docs/src/gcode/overview.adoc index 683f72cc154..7b31ab77581 100644 --- a/docs/src/gcode/overview.adoc +++ b/docs/src/gcode/overview.adoc @@ -116,7 +116,8 @@ The table includes N and O for completeness, even though, as defined above, line |A | A axis of machine |B | B axis of machine |C | C axis of machine -|D | Tool radius compensation number +.2+|D | Tool radius compensation number. +<| Start angle offset from the spindle index pulse, in degrees, for G33 and G76 threading. |F | Feed rate |G | General function (See table <>) |H | Tool length offset index diff --git a/src/emc/motion/command.c b/src/emc/motion/command.c index 8905ad05d13..2fe135e21aa 100644 --- a/src/emc/motion/command.c +++ b/src/emc/motion/command.c @@ -1051,7 +1051,7 @@ void emcmotCommandHandler_locked(void *arg, long servo_period) break; case EMCMOT_SET_SPINDLESYNC: - tpSetSpindleSync(&emcmotInternal->coord_tp, emcmotCommand->spindle, emcmotCommand->spindlesync, emcmotCommand->flags); + tpSetSpindleSync(&emcmotInternal->coord_tp, emcmotCommand->spindle, emcmotCommand->spindlesync, emcmotCommand->flags, emcmotCommand->angular_offset_degrees); break; case EMCMOT_SET_LINE: diff --git a/src/emc/motion/motion.h b/src/emc/motion/motion.h index 1312b5e45dd..59ad0672b16 100644 --- a/src/emc/motion/motion.h +++ b/src/emc/motion/motion.h @@ -219,6 +219,7 @@ extern "C" { constraints (the INI file) */ int motion_type; /* this move is because of traverse, feed, arc, or toolchange */ double spindlesync; /* user units per spindle revolution, 0 = no sync */ + double angular_offset_degrees; /* spindle angle offset for threading start (D word) */ double acc; /* max acceleration */ double jerk; /* jerk for traj */ double ini_maxjerk; diff --git a/src/emc/nml_intf/canon.hh b/src/emc/nml_intf/canon.hh index 916b3e92971..2925cd17fca 100644 --- a/src/emc/nml_intf/canon.hh +++ b/src/emc/nml_intf/canon.hh @@ -432,7 +432,7 @@ extern void STOP_CUTTER_RADIUS_COMPENSATION(); translation commands. */ /* used for threading */ -extern void START_SPEED_FEED_SYNCH(int spindle, double feed_per_revolution, bool velocity_mode); +extern void START_SPEED_FEED_SYNCH(int spindle, double feed_per_revolution, bool velocity_mode, double angle_degrees = 0.0); extern void STOP_SPEED_FEED_SYNCH(); diff --git a/src/emc/nml_intf/emc.cc b/src/emc/nml_intf/emc.cc index 3eb7360bdd3..e8a7542f4e3 100644 --- a/src/emc/nml_intf/emc.cc +++ b/src/emc/nml_intf/emc.cc @@ -1115,6 +1115,7 @@ void EMC_TRAJ_SET_SPINDLESYNC::update(CMS * cms) EMC_TRAJ_CMD_MSG::update(cms); cms->update(feed_per_revolution); cms->update(velocity_mode); + cms->update(angular_offset_degrees); } /* diff --git a/src/emc/nml_intf/emc.hh b/src/emc/nml_intf/emc.hh index 2738b34144b..9e5b60200ec 100644 --- a/src/emc/nml_intf/emc.hh +++ b/src/emc/nml_intf/emc.hh @@ -374,7 +374,7 @@ extern int emcTrajLinearMove(const EmcPose& end, int type, double vel, extern int emcTrajCircularMove(const EmcPose& end, const PM_CARTESIAN& center, const PM_CARTESIAN& normal, int turn, int type, double vel, double ini_maxvel, double acc, double ini_maxjerk); extern int emcTrajSetTermCond(int cond, double tolerance); -extern int emcTrajSetSpindleSync(int spindle, double feed_per_revolution, bool wait_for_index); +extern int emcTrajSetSpindleSync(int spindle, double feed_per_revolution, bool wait_for_index, double angular_offset_degrees = 0.0); extern int emcTrajSetOffset(const EmcPose& tool_offset); extern int emcTrajSetHome(const EmcPose& home); extern int emcTrajClearProbeTrippedFlag(); diff --git a/src/emc/nml_intf/emc_nml.hh b/src/emc/nml_intf/emc_nml.hh index 5cede52b09f..1aa377e2562 100644 --- a/src/emc/nml_intf/emc_nml.hh +++ b/src/emc/nml_intf/emc_nml.hh @@ -815,7 +815,8 @@ class EMC_TRAJ_SET_SPINDLESYNC:public EMC_TRAJ_CMD_MSG { : EMC_TRAJ_CMD_MSG(EMC_TRAJ_SET_SPINDLESYNC_TYPE, sizeof(EMC_TRAJ_SET_SPINDLESYNC)), spindle(0), feed_per_revolution(0.0), - velocity_mode(false) + velocity_mode(false), + angular_offset_degrees(0.0) {}; // Sub-class update() calls base-class update() @@ -825,6 +826,7 @@ class EMC_TRAJ_SET_SPINDLESYNC:public EMC_TRAJ_CMD_MSG { int spindle; double feed_per_revolution; bool velocity_mode; + double angular_offset_degrees; // spindle angle offset for threading start (D word, degrees) }; class EMC_TRAJ_SET_OFFSET:public EMC_TRAJ_CMD_MSG { diff --git a/src/emc/rs274ngc/canonmodule.cc b/src/emc/rs274ngc/canonmodule.cc index 3399fb2fc91..bc6417bb4af 100644 --- a/src/emc/rs274ngc/canonmodule.cc +++ b/src/emc/rs274ngc/canonmodule.cc @@ -18,6 +18,7 @@ */ #define BOOST_PYTHON_MAX_ARITY 13 #include +#include #include #include #include @@ -226,7 +227,12 @@ BOOST_PYTHON_MODULE(emccanon) { def("SPINDLE_RETRACT",&SPINDLE_RETRACT); def("SPINDLE_RETRACT_TRAVERSE",&SPINDLE_RETRACT_TRAVERSE); def("START_CUTTER_RADIUS_COMPENSATION",&START_CUTTER_RADIUS_COMPENSATION); - def("START_SPEED_FEED_SYNCH",&START_SPEED_FEED_SYNCH); + // spell the arguments out so the C++ default for angle_degrees survives into + // Python: a bare function pointer would make the new argument mandatory and + // break every existing three-argument caller + def("START_SPEED_FEED_SYNCH",&START_SPEED_FEED_SYNCH, + (arg("spindle"), arg("feed_per_revolution"), arg("velocity_mode"), + arg("angle_degrees")=0.0)); def("START_SPINDLE_CLOCKWISE",&START_SPINDLE_CLOCKWISE); def("START_SPINDLE_COUNTERCLOCKWISE",&START_SPINDLE_COUNTERCLOCKWISE); def("STOP_CUTTER_RADIUS_COMPENSATION",&STOP_CUTTER_RADIUS_COMPENSATION); diff --git a/src/emc/rs274ngc/gcodemodule.cc b/src/emc/rs274ngc/gcodemodule.cc index 3b15edea612..d6ab6424812 100644 --- a/src/emc/rs274ngc/gcodemodule.cc +++ b/src/emc/rs274ngc/gcodemodule.cc @@ -869,7 +869,7 @@ void SET_CUTTER_RADIUS_COMPENSATION(double /*radius*/) {} void START_CUTTER_RADIUS_COMPENSATION(int /*direction*/) {} void STOP_CUTTER_RADIUS_COMPENSATION(int /*direction*/) {} void START_SPEED_FEED_SYNCH() {} -void START_SPEED_FEED_SYNCH(int /*spindle*/, double /*sync*/, bool /*vel*/) {} +void START_SPEED_FEED_SYNCH(int /*spindle*/, double /*sync*/, bool /*vel*/, double /*angle*/) {} void STOP_SPEED_FEED_SYNCH() {} void START_SPINDLE_COUNTERCLOCKWISE(int /*spindle*/, int /*wait_for_at_speed*/) {} void START_SPINDLE_CLOCKWISE(int /*spindle*/, int /*wait_for_at_speed*/) {} diff --git a/src/emc/rs274ngc/interp_check.cc b/src/emc/rs274ngc/interp_check.cc index 196f2772763..07a625d55e3 100644 --- a/src/emc/rs274ngc/interp_check.cc +++ b/src/emc/rs274ngc/interp_check.cc @@ -257,8 +257,9 @@ int Interp::check_other_codes(block_pointer block) //!< pointer to a block (motion != G_70) && (motion != G_71) && (motion != G_71_1) && (motion != G_71_2) && (motion != G_72) && (motion != G_72_1) && (motion != G_72_2) && (motion != G_73) && (motion != G_83) && - (block->g_modes[14] != G_96)), - _("D word with no G41, G41.1, G42, G42.1, G71, G71.1, G71.2 G73, G83 or G96 to use it")); + (block->g_modes[14] != G_96) && + (motion != G_33) && (motion != G_76)), + _("D word with no G41, G41.1, G42, G42.1, G71, G71.1, G71.2 G73, G83, G96, G33 or G76 to use it")); } if (block->dollar_flag) { diff --git a/src/emc/rs274ngc/interp_convert.cc b/src/emc/rs274ngc/interp_convert.cc index 8156c90515f..049f6f73e02 100644 --- a/src/emc/rs274ngc/interp_convert.cc +++ b/src/emc/rs274ngc/interp_convert.cc @@ -5528,7 +5528,10 @@ int Interp::convert_straight(int move, //!< either G_0 or G_1 CHKS(((settings->spindle_turning[settings->active_spindle] != CANON_CLOCKWISE) && (settings->spindle_turning[settings->active_spindle] != CANON_COUNTERCLOCKWISE)), _("Spindle not turning in G33")); - START_SPEED_FEED_SYNCH(settings->active_spindle, block->k_number, 0); + // the offset is a direction-less angle past the index pulse, so a negative + // D is taken as its magnitude rather than rejected + double g33_angle = block->d_flag ? fabs(block->d_number_float) : 0.0; + START_SPEED_FEED_SYNCH(settings->active_spindle, block->k_number, 0, g33_angle); STRAIGHT_FEED(block->line_number, end_x, end_y, end_z, AA_end, BB_end, CC_end, u_end, v_end, w_end); STOP_SPEED_FEED_SYNCH(); settings->current_x = end_x; @@ -5660,26 +5663,27 @@ threading_pass(setup_pointer settings, block_pointer block, int boring, double safe_x, double depth, double end_depth, double start_y, double start_z, double zoff, double taper_dist, int entry_taper, int exit_taper, double taper_pitch, - double pitch, double full_threadheight, double target_z) { + double pitch, double full_threadheight, double target_z, + double angle_offset) { STRAIGHT_TRAVERSE(block->line_number, boring? safe_x + depth - end_depth: safe_x - depth + end_depth, start_y, start_z - zoff, AABBCC); //back if(taper_dist && entry_taper) { DISABLE_FEED_OVERRIDE(); - START_SPEED_FEED_SYNCH(settings->active_spindle, taper_pitch, 0); + START_SPEED_FEED_SYNCH(settings->active_spindle, taper_pitch, 0, angle_offset); STRAIGHT_FEED(block->line_number, boring? safe_x + depth - full_threadheight: safe_x - depth + full_threadheight, start_y, start_z - zoff, AABBCC); //in STRAIGHT_FEED(block->line_number, boring? safe_x + depth: safe_x - depth, //angled in start_y, start_z - zoff - taper_dist, AABBCC); - START_SPEED_FEED_SYNCH(settings->active_spindle, pitch, 0); + START_SPEED_FEED_SYNCH(settings->active_spindle, pitch, 0, angle_offset); } else { STRAIGHT_TRAVERSE(block->line_number, boring? safe_x + depth: safe_x - depth, start_y, start_z - zoff, AABBCC); //in DISABLE_FEED_OVERRIDE(); - START_SPEED_FEED_SYNCH(settings->active_spindle, pitch, 0); + START_SPEED_FEED_SYNCH(settings->active_spindle, pitch, 0, angle_offset); } if(taper_dist && exit_taper) { @@ -5768,6 +5772,9 @@ int Interp::convert_threading_cycle(block_pointer block, int entry_taper = taper_flags & 1; int exit_taper = taper_flags & 2; + // as in G33: a negative D is taken as its magnitude + double angle_offset = block->d_flag ? fabs(block->d_number_float) : 0.0; + double depth, zoff; int pass = 1; @@ -5778,7 +5785,7 @@ int Interp::convert_threading_cycle(block_pointer block, while (depth < end_depth) { threading_pass(settings, block, boring, safe_x, depth, end_depth, start_y, start_z, zoff, taper_dist, entry_taper, exit_taper, - taper_pitch, pitch, full_threadheight, target_z); + taper_pitch, pitch, full_threadheight, target_z, angle_offset); depth = full_dia_depth + cut_increment * pow(++pass, 1.0/degression); zoff = (depth - full_dia_depth) * tan(compound_angle); } @@ -5789,7 +5796,7 @@ int Interp::convert_threading_cycle(block_pointer block, for(int i = 0; iline_number, end_x, end_y, end_z, AABBCC); settings->current_x = end_x; diff --git a/src/emc/sai/saicanon.cc b/src/emc/sai/saicanon.cc index 169e73a8a39..c1254b7ac3a 100644 --- a/src/emc/sai/saicanon.cc +++ b/src/emc/sai/saicanon.cc @@ -737,8 +737,15 @@ int GET_EXTERNAL_AXIS_MASK() {return 0x3f;} // XYZABC machine double GET_EXTERNAL_ANGLE_UNITS() {return 1.0;} int GET_EXTERNAL_SELECTED_TOOL_SLOT() { return 0; } int GET_EXTERNAL_SPINDLE_OVERRIDE_ENABLE(int /*spindle*/) {return so_enable;} -void START_SPEED_FEED_SYNCH(int /*spindle*/, double sync, bool vel) -{PRINT("START_SPEED_FEED_SYNC(%f,%d)\n", sync, vel);} +void START_SPEED_FEED_SYNCH(int /*spindle*/, double sync, bool vel, double angle_degrees) +{ + // only print the angle when one was asked for, so that the expected output + // of tests predating the D word stays valid + if (angle_degrees != 0.0) + PRINT("START_SPEED_FEED_SYNC(%f,%d,%f)\n", sync, vel, angle_degrees); + else + PRINT("START_SPEED_FEED_SYNC(%f,%d)\n", sync, vel); +} CANON_MOTION_MODE motion_mode; int GET_EXTERNAL_DIGITAL_INPUT(int /*index*/, int def) { return def; } diff --git a/src/emc/task/emccanon.cc b/src/emc/task/emccanon.cc index a5f45837c99..2365c4470a8 100644 --- a/src/emc/task/emccanon.cc +++ b/src/emc/task/emccanon.cc @@ -1503,13 +1503,14 @@ void STOP_CUTTER_RADIUS_COMPENSATION() -void START_SPEED_FEED_SYNCH(int spindle, double feed_per_revolution, bool velocity_mode) +void START_SPEED_FEED_SYNCH(int spindle, double feed_per_revolution, bool velocity_mode, double angle_degrees) { flush_segments(); auto spindleSyncMsg = std::make_unique(); spindleSyncMsg->spindle = spindle; spindleSyncMsg->feed_per_revolution = TO_EXT_LEN(FROM_PROG_LEN(feed_per_revolution)); spindleSyncMsg->velocity_mode = velocity_mode; + spindleSyncMsg->angular_offset_degrees = angle_degrees; interp_list.append(std::move(spindleSyncMsg)); canon.spindle[spindle].synched = 1; } diff --git a/src/emc/task/emctaskmain.cc b/src/emc/task/emctaskmain.cc index ff0978fe922..76c7adbb4e8 100644 --- a/src/emc/task/emctaskmain.cc +++ b/src/emc/task/emctaskmain.cc @@ -1887,7 +1887,7 @@ static int emcTaskIssueCommand(NMLmsg * cmd) case EMC_TRAJ_SET_SPINDLESYNC_TYPE: emcTrajSetSpindlesyncMsg = reinterpret_cast(cmd); - retval = emcTrajSetSpindleSync(emcTrajSetSpindlesyncMsg->spindle, emcTrajSetSpindlesyncMsg->feed_per_revolution, emcTrajSetSpindlesyncMsg->velocity_mode); + retval = emcTrajSetSpindleSync(emcTrajSetSpindlesyncMsg->spindle, emcTrajSetSpindlesyncMsg->feed_per_revolution, emcTrajSetSpindlesyncMsg->velocity_mode, emcTrajSetSpindlesyncMsg->angular_offset_degrees); break; case EMC_TRAJ_SET_OFFSET_TYPE: diff --git a/src/emc/task/taskintf.cc b/src/emc/task/taskintf.cc index 482d8bf8afe..4eac12ffee9 100644 --- a/src/emc/task/taskintf.cc +++ b/src/emc/task/taskintf.cc @@ -1505,12 +1505,13 @@ int emcTrajSetOffset(const EmcPose& tool_offset) return usrmotWriteEmcmotCommand(&emcmotCommand); } -int emcTrajSetSpindleSync(int spindle, double fpr, bool wait_for_index) +int emcTrajSetSpindleSync(int spindle, double fpr, bool wait_for_index, double angular_offset_degrees) { emcmotCommand.command = EMCMOT_SET_SPINDLESYNC; emcmotCommand.spindle = spindle; emcmotCommand.spindlesync = fpr; emcmotCommand.flags = wait_for_index; + emcmotCommand.angular_offset_degrees = angular_offset_degrees; return usrmotWriteEmcmotCommand(&emcmotCommand); } diff --git a/src/emc/tp/tc.c b/src/emc/tp/tc.c index e1c9fe5ffe3..60e5b5c5f09 100644 --- a/src/emc/tp/tc.c +++ b/src/emc/tp/tc.c @@ -792,6 +792,7 @@ int tcSetupState(TC_STRUCT * const tc, TP_STRUCT const * const tp) tc->tolerance = tp->tolerance; tc->synchronized = tp->synchronized; tc->uu_per_rev = tp->uu_per_rev; + tc->angle_offset = tp->spindle.pending_offset; return TP_ERR_OK; } diff --git a/src/emc/tp/tc_types.h b/src/emc/tp/tc_types.h index 135b34586ae..b0b3eb40313 100644 --- a/src/emc/tp/tc_types.h +++ b/src/emc/tp/tc_types.h @@ -176,6 +176,7 @@ typedef struct { // stay within this distance from the path. int synchronized; // spindle sync state double uu_per_rev; // for sync, user units per rev (e.g. 0.0625 for 16tpi) + double angle_offset; // spindle angle offset from index (revolutions) for D word double vel_at_blend_start; int sync_accel; // we're accelerating up to sync with the spindle unsigned char enables; // Feed scale, etc, enable bits for this move diff --git a/src/emc/tp/tp.c b/src/emc/tp/tp.c index 7e63d467aba..41814556e60 100644 --- a/src/emc/tp/tp.c +++ b/src/emc/tp/tp.c @@ -500,6 +500,8 @@ int tpInit(TP_STRUCT * const tp) tp->spindle.revs = 0.0; tp->spindle.waiting_for_index = MOTION_INVALID_ID; tp->spindle.waiting_for_atspeed = MOTION_INVALID_ID; + tp->spindle.pending_offset = 0.0; + tp->spindle.angle_hold_pending = 0; tp->reverse_run = TC_DIR_FORWARD; tp->termCond = TC_TERM_COND_PARABOLIC; @@ -3476,8 +3478,18 @@ STATIC tp_err_t tpCheckAtSpeed(TP_STRUCT * const tp, TC_STRUCT * const tc) /* passed index, start the move */ emcmotStatus->spindleSync = 1; tp->spindle.waiting_for_index = MOTION_INVALID_ID; - tc->sync_accel = 1; tp->spindle.revs = 0; + tp->spindle.offset = 0.0; + /* gate on > 0.0, matching tpSyncPositionMode(): a value that does + * not request a hold there must not suppress the ramp here, or the + * move would start with neither */ + if (!(tc->angle_offset > 0.0)) { + /* no angle offset: use sync_accel to ramp up to spindle speed */ + tc->sync_accel = 1; + } + /* if angle_offset > 0: tpSyncPositionMode() will hold Z at rest + * until the spindle reaches angle_offset revolutions past the + * index pulse, then release to tracking mode */ } } return TP_ERR_OK; @@ -3572,6 +3584,9 @@ STATIC tp_err_t tpActivateSegment(TP_STRUCT * const tp, TC_STRUCT * const tc) { // ask for an index reset emcmotStatus->spindle_status[tp->spindle.spindle_num].spindle_index_enable = 1; tp->spindle.offset = 0.0; + // a fresh index sync: any angle offset on the segment that follows it + // has yet to be waited out + tp->spindle.angle_hold_pending = 1; rtapi_print_msg(RTAPI_MSG_DBG, "Waiting on sync. spindle_num %d..\n", tp->spindle.spindle_num); return TP_ERR_WAITING; } @@ -3624,6 +3639,25 @@ STATIC void tpSyncPositionMode(TP_STRUCT * const tp, TC_STRUCT * const tc, tp->spindle.revs = spindle_pos; } + /* Angle-offset hold: after index, keep the axis at rest until the spindle + * has advanced angle_offset revolutions (spindle.revs resets to 0 at the + * index pulse). angle_hold_pending scopes this to the first segment after + * the index -- the later segments of a threading pass carry the same + * angle_offset, and must keep tracking against the offset accumulated by + * tpCompleteSegment() rather than re-zeroing it here. */ + if (tp->spindle.angle_hold_pending && tc->angle_offset > 0.0) { + if (tp->spindle.revs < tc->angle_offset) { + tc->target_vel = 0.0; + return; + } + /* Spindle reached target angle: set offset so pos_desired = 0 now, + * then fall through to normal tracking (sync_accel stays 0). The TC's + * angle_offset is left intact so the segment still describes what was + * programmed, which a reverse run over it depends on. */ + tp->spindle.offset = tp->spindle.revs; + tp->spindle.angle_hold_pending = 0; + } + double pos_desired = (tp->spindle.revs - tp->spindle.offset) * tc->uu_per_rev; double pos_error = pos_desired - tc->progress; @@ -4234,7 +4268,7 @@ int tpRunCycle(TP_STRUCT * const tp, long period) return TP_ERR_OK; } -int tpSetSpindleSync(TP_STRUCT * const tp, int spindle, double sync, int mode) { +int tpSetSpindleSync(TP_STRUCT * const tp, int spindle, double sync, int mode, double angular_offset_degrees) { if(sync) { if (mode) { tp->synchronized = TC_SYNC_VELOCITY; @@ -4243,8 +4277,14 @@ int tpSetSpindleSync(TP_STRUCT * const tp, int spindle, double sync, int mode) { } tp->uu_per_rev = sync; tp->spindle.spindle_num = spindle; - } else + /* the offset is a direction-less angle past the index, so take the + * magnitude -- the interpreter does the same with a negative D word, + * and this keeps the guarantee for any other caller of this API */ + tp->spindle.pending_offset = fabs(angular_offset_degrees) / 360.0; + } else { tp->synchronized = 0; + tp->spindle.pending_offset = 0.0; + } return TP_ERR_OK; } diff --git a/src/emc/tp/tp.h b/src/emc/tp/tp.h index e00b457ad23..9ed9baa4942 100644 --- a/src/emc/tp/tp.h +++ b/src/emc/tp/tp.h @@ -67,7 +67,7 @@ int tpIsDone(TP_STRUCT * const tp); int tpQueueDepth(TP_STRUCT * const tp); int tpActiveDepth(TP_STRUCT * const tp); int tpGetMotionType(TP_STRUCT * const tp); -int tpSetSpindleSync(TP_STRUCT * const tp, int spindle, double sync, int wait); +int tpSetSpindleSync(TP_STRUCT * const tp, int spindle, double sync, int wait, double angular_offset_degrees); int tpSetAout(TP_STRUCT * const tp, unsigned char index, double start, double end); int tpSetDout(TP_STRUCT * const tp, int index, unsigned char start, unsigned char end); //gets called to place DIO toggles on the TC queue diff --git a/src/emc/tp/tp_types.h b/src/emc/tp/tp_types.h index 6687ef3a2d1..9421d90540d 100644 --- a/src/emc/tp/tp_types.h +++ b/src/emc/tp/tp_types.h @@ -83,6 +83,14 @@ typedef struct { double revs; int waiting_for_index; int waiting_for_atspeed; + double pending_offset; // requested angle offset in revolutions (set by tpSetSpindleSync, applied at index) + int angle_hold_pending; // an angle offset is still to be waited out for the + // current index sync; armed when the index wait is + // armed, cleared once the angle has been reached. + // Scopes the hold to the first segment after the + // index, so later segments of the same pass keep + // the spindle offset accumulated by + // tpCompleteSegment() instead of resetting it. } tp_spindle_t; /** diff --git a/tests/interp/compile/use-rs274.cc b/tests/interp/compile/use-rs274.cc index 2d388b5fcec..45e75382484 100644 --- a/tests/interp/compile/use-rs274.cc +++ b/tests/interp/compile/use-rs274.cc @@ -86,7 +86,7 @@ void SET_NAIVECAM_TOLERANCE(double tolerance) {} void SET_CUTTER_RADIUS_COMPENSATION(double radius) {} void START_CUTTER_RADIUS_COMPENSATION(int direction) {} void STOP_CUTTER_RADIUS_COMPENSATION() {} -void START_SPEED_FEED_SYNCH(int spindle, double feed_per_revolution, bool velocity_mode) {} +void START_SPEED_FEED_SYNCH(int spindle, double feed_per_revolution, bool velocity_mode, double angle_degrees) {} void STOP_SPEED_FEED_SYNCH() {} void ARC_FEED(int lineno, diff --git a/tests/interp/g76-d-word/README b/tests/interp/g76-d-word/README new file mode 100644 index 00000000000..a7f67ce2b91 --- /dev/null +++ b/tests/interp/g76-d-word/README @@ -0,0 +1,10 @@ +Checks the D word start angle offset for spindle synchronized motion. + +Covers that the angle reaches the canon layer for both G33 and G76, that it is +absent (and so defaults to zero) when no D is programmed, and that a negative D +is passed on as its magnitude rather than being dropped. + +The G76 case uses an entry taper (L1) on purpose: that makes a single threading +pass emit more than one synchronized move, which is the case where the offset +has to be applied only once, at the index pulse, rather than at the start of +every segment of the pass. diff --git a/tests/interp/g76-d-word/d-word.ngc b/tests/interp/g76-d-word/d-word.ngc new file mode 100644 index 00000000000..ffdac8b7d07 --- /dev/null +++ b/tests/interp/g76-d-word/d-word.ngc @@ -0,0 +1,27 @@ +(Checks that the D word start angle offset reaches the canon layer on G33 and) +(G76, that it defaults to zero when absent, and that a negative D is passed on) +(as its magnitude.) + +g20 g18 +s800 m3 +g0 z.2 x.2 + +(G33 with no D: START_SPEED_FEED_SYNC prints without an angle) +g0 x.2 z0 +g33 z-.5 k.05 + +(G33 with D: the angle is carried through) +g0 x.2 z0 +g33 z-.5 k.05 d90 + +(G33 with a negative D: reported as +90, not -90 and not dropped) +g0 x.2 z0 +g33 z-.5 k.05 d-90 + +(G76 with D and an entry taper. The entry taper makes the pass emit more than) +(one synchronized move, so the offset appears on each START_SPEED_FEED_SYNC of) +(the pass except the exit taper's.) +g0 x.2 z0 +g76 p.05 z-.5 i-.05 j.01 k.02 h0 e.02 l1 d120 + +m2 diff --git a/tests/interp/g76-d-word/expected b/tests/interp/g76-d-word/expected new file mode 100644 index 00000000000..745ad161f55 --- /dev/null +++ b/tests/interp/g76-d-word/expected @@ -0,0 +1,7 @@ + N..... START_SPEED_FEED_SYNC(0.050000,0) + N..... START_SPEED_FEED_SYNC(0.050000,0,90.000000) + N..... START_SPEED_FEED_SYNC(0.050000,0,90.000000) + N..... START_SPEED_FEED_SYNC(0.070711,0,120.000000) + N..... START_SPEED_FEED_SYNC(0.050000,0,120.000000) + N..... START_SPEED_FEED_SYNC(0.070711,0,120.000000) + N..... START_SPEED_FEED_SYNC(0.050000,0,120.000000) diff --git a/tests/interp/g76-d-word/test.sh b/tests/interp/g76-d-word/test.sh new file mode 100755 index 00000000000..894a30c4601 --- /dev/null +++ b/tests/interp/g76-d-word/test.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# Only the spindle-sync calls matter here; the surrounding motion is covered by +# the g33 and g76 tests. +rs274 -g d-word.ngc | awk '{$1=""; print}' | grep 'SPEED_FEED_SYNC(' +exit "${PIPESTATUS[0]}" diff --git a/tests/motion/spindle-angle-offset/angle-offset.ngc b/tests/motion/spindle-angle-offset/angle-offset.ngc new file mode 100644 index 00000000000..22e56d4a05b --- /dev/null +++ b/tests/motion/spindle-angle-offset/angle-offset.ngc @@ -0,0 +1,39 @@ +(Spindle synchronized motion with a D word start angle offset.) +(Each G33 below is a separate synchronized move, so each one waits for its own) +(index pulse -- that reset is what D is measured from.) + +g21 g18 g90 g8 +s600 m3 +g4 p2 (let the simulated spindle reach speed) + +(Reference pass: no D, so motion starts at the index pulse.) +g0 x10 z5 +g33 z-10 k2 + +(D180: must start half a turn later than the reference pass.) +g0 x10 z5 +g33 z-10 k2 d180 + +(Negative D: the magnitude is used, so this must match the D180 pass.) +g0 x10 z5 +g33 z-10 k2 d-180 + +(G76 with an entry taper, run twice with identical geometry and speed: once) +(without D and once with. The entry taper makes one pass emit three) +(synchronized moves instead of one, which is the case where the offset must be) +(applied once at the index rather than at the start of every segment.) +(The pair is compared against itself because the angle it costs to detect that) +(the tool has started moving depends on the spindle speed and on which axis) +(leads, so only two otherwise identical cycles can be differenced.) +s600 +g4 p2 + +(reference cycle, no D) +g0 x10 z5 +g76 p2 z-10 i-1 j0.4 k0.6 r1 q29.5 h0 e1 l1 + +(same cycle offset half a turn) +g0 x10 z5 +g76 p2 z-10 i-1 j0.4 k0.6 r1 q29.5 h0 e1 l1 d180 + +m2 diff --git a/tests/motion/spindle-angle-offset/expected b/tests/motion/spindle-angle-offset/expected new file mode 100644 index 00000000000..8c0382aa4aa --- /dev/null +++ b/tests/motion/spindle-angle-offset/expected @@ -0,0 +1 @@ +Completed successfully diff --git a/tests/motion/spindle-angle-offset/sample.hal b/tests/motion/spindle-angle-offset/sample.hal new file mode 100644 index 00000000000..1f658e3db1e --- /dev/null +++ b/tests/motion/spindle-angle-offset/sample.hal @@ -0,0 +1,24 @@ +# HAL file for sampling spindle-synchronized motion against the spindle angle. +# +# The simulated spindle encoder (LIB:sim_spindle_encoder.hal, loaded from the +# ini) provides the two things this test needs: a revolution count in +# spindle.0.revs, and an index pulse that resets it to zero. The D word start +# angle offset is defined against exactly that reset, so sampling revs next to +# the Z position is enough to see whether the axis was held for the right angle. + +loadrt sampler depth=8000 cfg=ssfffb + +# disable sampler before adding the function +setp sampler.0.enable 0 + +addf sampler.0 servo-thread + +net line-number motion.interp.line-number => sampler.0.pin.0 +net motion-type motion.interp.motion-type => sampler.0.pin.1 +# spindle-pos and spindle-index-enable are the nets made by sim_spindle_encoder.hal +net spindle-pos => sampler.0.pin.2 +net Zpos => sampler.0.pin.3 +# X too: a G76 pass with an entry taper begins with a radial feed, so the move +# that actually waits out the offset is an X move, not a Z move +net Xpos => sampler.0.pin.4 +net spindle-index-enable => sampler.0.pin.5 diff --git a/tests/motion/spindle-angle-offset/test-ui.py b/tests/motion/spindle-angle-offset/test-ui.py new file mode 100755 index 00000000000..115039fca48 --- /dev/null +++ b/tests/motion/spindle-angle-offset/test-ui.py @@ -0,0 +1,313 @@ +#!/usr/bin/env python3 +# +# Checks the D word start angle offset for spindle synchronized motion. +# +# The offset is defined against the spindle index pulse, and the simulated +# spindle encoder resets spindle.0.revs to zero at exactly that pulse. So the +# angle the axis actually waited for can be read straight off the samples: find +# the index reset, then find the first sample where Z starts to move, and look +# at how far the spindle had turned in between. +# +# Detecting "Z started moving" costs a small, fixed amount of spindle rotation +# (the axis has to accelerate far enough to clear the threshold), so no single +# measurement is exact. Every check below is therefore a *difference* between +# two passes that carry that same lag, which cancels it. + +import linuxcnc +import linuxcnc_util +import hal + +import time +import sys +import os + +INTERPTIMEOUT = 5 # Max seconds to wait for the interpreter to start +GCODETIMEOUT = 240 # Max seconds for the gcode program to run + +PITCH = 2.0 # K/P word of every synchronized move in the program, mm/rev +ZMOVED = 0.02 # mm of Z travel counted as "the axis has been released" +REVTOL = 0.08 # rev, tolerance on a measured angle difference (~29 deg) + +# sampler field order, see sample.hal +LINE, MTYPE, REVS, ZPOS, XPOS, INDEX = range(6) + +samples = [] +failures = [] + + +def fail(msg): + print("FAIL: {}".format(msg)) + failures.append(msg) + + +def index_resets(data): + """Split the samples at each index pulse. + + sim_spindle clears index-enable at the moment it zeroes its position, so a + falling edge of that pin is the index reset. Returns a list of sample + lists, one per synchronized move that waited for an index. + """ + groups = [] + start = None + for i in range(1, len(data)): + if data[i - 1][INDEX] and not data[i][INDEX]: + if start is not None: + groups.append(data[start:i]) + start = i + if start is not None: + groups.append(data[start:]) + return groups + + +def release_index(group): + """Position in the group at which the tool began to move, or None. + + Either axis counts: a G33 pass is released into a Z move, but a G76 pass + with an entry taper is released into a radial X feed. + """ + z0, x0 = group[0][ZPOS], group[0][XPOS] + for i, s in enumerate(group): + if abs(s[ZPOS] - z0) > ZMOVED or abs(s[XPOS] - x0) > ZMOVED: + return i + return None + + +def release_angle(group): + """Revolutions past the index at which the tool began to move, or None.""" + i = release_index(group) + return None if i is None else group[i][REVS] + + +def cut_samples(group): + """The synchronized cut itself: from release until Z stops advancing. + + A group runs to the next index pulse, so it also contains the rapids that + reposition for the following pass. Cutting it off where Z stops going + negative keeps those out of the pitch check. + """ + i = release_index(group) + if i is None: + return [] + cut = [group[i]] + stalled = 0 + for s in group[i + 1:]: + if s[ZPOS] > cut[-1][ZPOS] + 0.001: # Z turned round: the cut is over + break + if s[ZPOS] > cut[-1][ZPOS] - 1e-9: + # Z has stopped advancing. The cut is over even though Z has not + # moved back yet: the program dwells here, and the line number is + # no help because a dwell keeps reporting the previous motion line. + stalled += 1 + if stalled > 20: + break + else: + stalled = 0 + cut.append(s) + return cut[:len(cut) - stalled] + + +def cut_pitch(group, z1, z2): + """Measured mm per revolution between two Z positions of a cut. + + Sampled well inside the thread so that neither the entry taper nor the + acceleration at either end is included. + """ + def revs_at(zt): + prev = None + for s in group: + if prev and prev[ZPOS] > zt >= s[ZPOS]: + span = prev[ZPOS] - s[ZPOS] + f = (prev[ZPOS] - zt) / span if span else 0.0 + return prev[REVS] + f * (s[REVS] - prev[REVS]) + prev = s + return None + + a, b = revs_at(z1), revs_at(z2) + if a is None or b is None or b <= a: + return None + return (z1 - z2) / (b - a) + + +def pitch_error(group): + """Largest departure from 'Z advances PITCH per revolution' during the cut.""" + cut = cut_samples(group) + if len(cut) < 20: + return None + # drop the acceleration at each end, where Z is not yet tracking the spindle + cut = cut[len(cut) // 4: -(len(cut) // 4)] + if len(cut) < 5: + return None + zr, rr = cut[0][ZPOS], cut[0][REVS] + return max(abs((zr - s[ZPOS]) - PITCH * (s[REVS] - rr)) for s in cut) + + +# +# Command line: -ngc +# +ngcfile = None +for i in range(1, len(sys.argv) - 1): + if "-ngc" == sys.argv[i]: + ngcfile = sys.argv[i + 1] + break + +if not ngcfile: + print("Missing NGC-file; run with: test-ui.py -ngc ngcfile.ngc") + sys.exit(1) + +if not os.path.exists(ngcfile): + print("NGC-file '{}' does not exist".format(ngcfile)) + sys.exit(1) + +# +# Connect to the sampler stream +# +h = hal.component("python-ui") +sampler = hal.stream(h, hal.sampler_base, "ssfffb") +h.ready() + +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]) + +c.mode(linuxcnc.MODE_AUTO) +c.program_open(ngcfile) + +hal.set_p("sampler.0.enable", "1") +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) + + +def drain(): + while sampler.readable: + sample = sampler.read() + if sample is None: + print("Error: sampler read None") + sys.exit(1) + samples.append(sample) + + +start = time.time() +s.poll() +while s.interp_state != linuxcnc.INTERP_IDLE and time.time() - start < GCODETIMEOUT: + drain() + time.sleep(0.005) + s.poll() + +if s.interp_state != linuxcnc.INTERP_IDLE: + print("Timed out running the GCode program") + sys.exit(1) + +hal.set_p("sampler.0.enable", "0") +time.sleep(0.05) +drain() + +print("collected {} samples".format(len(samples))) + +# +# Analysis +# +groups = index_resets(samples) +print("{} synchronized moves waited for an index pulse".format(len(groups))) + +# The program is three G33 passes (no D, D180, D-180) followed by two G76 +# cycles of identical geometry, the second one offset. The pass count of a G76 +# cycle depends on its depth degression, so the two cycles are not assumed to be +# any particular length -- only that they are the same length as each other. +if len(groups) < 5 or (len(groups) - 3) % 2: + fail("expected 3 G33 passes and two G76 cycles of equal length, " + "got {} synchronized moves".format(len(groups))) + print("Test Failed") + sys.exit(1) + +angles = [release_angle(g) for g in groups] +for i, a in enumerate(angles): + print("move {}: released {} rev past index".format( + i, "never" if a is None else "{:.4f}".format(a))) + +npasses = (len(groups) - 3) // 2 +ref, d180, dneg180 = angles[0], angles[1], angles[2] +g76ref, g76d180 = angles[3], angles[3 + npasses] + +for i, a in enumerate(angles): + if a is None: + fail("move {} never moved after its index pulse".format(i)) +if failures: + print("Test Failed") + sys.exit(1) + +# D180 holds for half a turn longer than no D at all. +if abs((d180 - ref) - 0.5) > REVTOL: + fail("D180 released {:.4f} rev after the reference pass, expected 0.5" + .format(d180 - ref)) + +# A negative D is used as its magnitude, so D-180 matches D180. This is the +# check that a negative value is neither rejected nor silently dropped: if it +# were dropped this difference would be the full 0.5 rev. +if abs(dneg180 - d180) > REVTOL: + fail("D-180 released {:.4f} rev past index, D180 released {:.4f}; " + "a negative D should be used as its magnitude" + .format(dneg180, d180)) + +# The reference pass has no offset, so it starts at the index pulse. Only the +# detection lag should separate it from zero. +if ref > REVTOL: + fail("pass without a D word released {:.4f} rev past index, expected ~0" + .format(ref)) + +# G76 honours D too: the offset cycle waits half a turn longer than the +# otherwise identical reference cycle. +if abs((g76d180 - g76ref) - 0.5) > REVTOL: + fail("G76 with D180 released {:.4f} rev past index and the reference " + "cycle {:.4f}, a difference of {:.4f}; expected 0.5" + .format(g76d180, g76ref, g76d180 - g76ref)) + +# Whatever the offset did, the cut itself must still advance one pitch per +# revolution -- a hold that released into the wrong tracking state would show +# up here rather than in the angles above. Only the G33 passes are checked: +# a G76 pass cuts its entry taper at a different pitch by design. +for i, g in enumerate(groups[:3]): + err = pitch_error(g) + if err is None: + continue + print("move {}: pitch tracking error {:.4f} mm".format(i, err)) + if err > 0.5: + fail("move {} drifted {:.4f} mm from {} mm/rev".format(i, err, PITCH)) + +# The same for the body of a G76 pass, measured between two Z positions well +# inside the thread so the entry taper is excluded. Both cycles must cut the +# programmed pitch: an offset that resynchronised the spindle reference part way +# through a pass, or one that dropped the axis out of position tracking, would +# show up as a pitch that is not P. +for i, g in ((3, groups[3]), (3 + npasses, groups[3 + npasses])): + measured = cut_pitch(g, -2.0, -8.0) + if measured is None: + fail("move {} never cut through the Z range the pitch is measured over" + .format(i)) + continue + print("move {}: cut {:.4f} mm/rev".format(i, measured)) + if abs(measured - PITCH) > 0.02: + fail("move {} cut {:.4f} mm/rev, expected {}".format(i, measured, PITCH)) + +if failures: + print("Test Failed") + sys.exit(1) + +print("Completed successfully") +sys.exit(0) diff --git a/tests/motion/spindle-angle-offset/test.ini b/tests/motion/spindle-angle-offset/test.ini new file mode 100644 index 00000000000..f1e9ab76e3b --- /dev/null +++ b/tests/motion/spindle-angle-offset/test.ini @@ -0,0 +1,97 @@ +[EMC] +DEBUG = 0 +VERSION = 1.1 + +[DISPLAY] +DISPLAY = ./test-ui.py -ngc ./angle-offset.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 +HALFILE = ./sample.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-angle-offset/test.sh b/tests/motion/spindle-angle-offset/test.sh new file mode 100755 index 00000000000..87a3bca3260 --- /dev/null +++ b/tests/motion/spindle-angle-offset/test.sh @@ -0,0 +1,4 @@ +#!/bin/bash +if linuxcnc -r test.ini; then + echo "Completed successfully" > result +fi