From 9196617d0d49c751897bf23d5e30b8a677303fb9 Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Tue, 10 Nov 2026 16:13:04 +1100 Subject: [PATCH] task: abort once after a motion error, not again on an old status copy After a motion error task aborts. If its next status read from motion failed, task kept its old copy, still showing the error, and aborted a second time, dropping the command that had just come in. A failed read now reports an old copy as busy, not as an error, as it already did for done. tests/stale-motion-status slow-controller checks one abort per failed probe and that the next move runs. --- src/emc/task/taskintf.cc | 7 ++- tests/stale-motion-status/README | 2 + .../slow-controller/on_abort.ngc | 5 ++ .../slow-controller/probe.hal | 1 + .../slow-controller/slowkins.comp | 38 +++++++++++--- .../slow-controller/test.ini | 2 + tests/stale-motion-status/test-ui.py | 50 +++++++++++++++++++ 7 files changed, 96 insertions(+), 9 deletions(-) create mode 100644 tests/stale-motion-status/slow-controller/on_abort.ngc diff --git a/src/emc/task/taskintf.cc b/src/emc/task/taskintf.cc index c291547cf39..d49ba291a26 100644 --- a/src/emc/task/taskintf.cc +++ b/src/emc/task/taskintf.cc @@ -2098,10 +2098,13 @@ int emcSpindleUpdate(EMC_SPINDLE_STAT stat[], int num_spindles){ // A failed read leaves stat as it was. If that copy is older than the // last command sent, motion may already be running it, so do not report -// it done: a probe just queued would look finished. +// it done: a probe just queued would look finished. Nor in error: an +// abort sent since has cleared it, and task would abort again, along +// with whatever command came next. An error that stands shows on the +// next read, as only an abort clears it. static int emcMotionUpdateFailed(EMC_MOTION_STAT * stat) { - if (stat->status == RCS_STATUS::DONE && + if (stat->status != RCS_STATUS::EXEC && stat->echo_serial_number != emcmotCommand.commandNum) { stat->status = RCS_STATUS::EXEC; } diff --git a/tests/stale-motion-status/README b/tests/stale-motion-status/README index 993a1643a0d..acfe60388c9 100644 --- a/tests/stale-motion-status/README +++ b/tests/stale-motion-status/README @@ -21,3 +21,5 @@ slow-controller uses kinematics that busy wait, so motion-controller takes most of the servo period and status reads by task often fail while it runs. The wait starts once task is up: its startup reads the status only once a second and gives up after ten tries. + +After the program, slow-controller runs five G38.2 in MDI that end without contact. A little after each probe move ends, slowkins busy waits nearly all of each servo period for a while, so that status reads by task fail but the command echoes it waits for get through. Task keeps its copy from before its abort, still in error; it must not abort again on it, which on_abort.ngc counts, nor drop the move sent as soon as wait_complete() returns. diff --git a/tests/stale-motion-status/slow-controller/on_abort.ngc b/tests/stale-motion-status/slow-controller/on_abort.ngc new file mode 100644 index 00000000000..c49fb1be255 --- /dev/null +++ b/tests/stale-motion-status/slow-controller/on_abort.ngc @@ -0,0 +1,5 @@ +o sub +; count the aborts, for test-ui.py +#4000 = [#4000 + 1] +o endsub +m2 diff --git a/tests/stale-motion-status/slow-controller/probe.hal b/tests/stale-motion-status/slow-controller/probe.hal index 0c0e3c6ba81..c182677bd6d 100644 --- a/tests/stale-motion-status/slow-controller/probe.hal +++ b/tests/stale-motion-status/slow-controller/probe.hal @@ -9,3 +9,4 @@ net probe probe-comp.out => motion.probe-input # by task often fail while it runs. test-ui.py sets slowkins.wait-ns once # task is up: at startup task reads the status only once a second. addf slowkins.arm servo-thread 1 +net motion-type motion.motion-type => slowkins.motion-type diff --git a/tests/stale-motion-status/slow-controller/slowkins.comp b/tests/stale-motion-status/slow-controller/slowkins.comp index 0025d944fec..6495c9bcdb4 100644 --- a/tests/stale-motion-status/slow-controller/slowkins.comp +++ b/tests/stale-motion-status/slow-controller/slowkins.comp @@ -1,5 +1,12 @@ component slowkins "Identity kinematics for X Y Z that busy wait in kinematicsForward once per servo period"; pin out bool dummy = 1; // halcompile requires at least one pin +pin in sint motion_type "Wire to motion.motion-type: the end of a probe move starts a burst"; +param rw uint burst_delay = 0 "Servo periods from the end of a probe move to its burst"; +param rw uint burst_periods = 0 "Servo periods of a burst; 0 for none"; +param rw uint burst_ns = 0 "Busy wait in each servo period of a burst"; +variable rtapi_sint was_probing = 0; +variable rtapi_uint delay_left = 0; +variable rtapi_uint burst_left = 0; function arm "Let the next kinematicsForward call busy wait"; option singleton yes; option extra_setup; @@ -8,10 +15,11 @@ license "GPL"; author "Luca Toniolo"; ;; #include +#include static struct haldata { hal_uint_t wait_ns; - int armed; + rtapi_uint armed_ns; } *haldata; EXTRA_SETUP() { @@ -29,10 +37,26 @@ EXTRA_SETUP() { } /* motion calls kinematicsForward twice per period in free mode; one wait - per period keeps the controller inside the period */ + per period keeps the controller inside the period. A burst, a little + after a probe move ends, makes nearly every status read by task fail + while the command echoes it waits for still get through. */ FUNCTION(arm) { - (void)__comp_inst; - haldata->armed = 1; + rtapi_sint probing = motion_type == EMC_MOTION_TYPE_PROBING; + + if (was_probing && !probing && burst_periods) { + delay_left = burst_delay; + burst_left = burst_periods; + } + was_probing = probing; + if (delay_left) { + delay_left--; + haldata->armed_ns = hal_get_uint(haldata->wait_ns); + } else if (burst_left) { + burst_left--; + haldata->armed_ns = burst_ns; + } else { + haldata->armed_ns = hal_get_uint(haldata->wait_ns); + } } KINS_NOT_SWITCHABLE @@ -52,9 +76,9 @@ int kinematicsForward(const double *j, EmcPose *pos, { (void)fflags; (void)iflags; - if (haldata->armed) { - rtapi_sint end = rtapi_get_time() + (rtapi_sint)hal_get_uint(haldata->wait_ns); - haldata->armed = 0; + if (haldata->armed_ns) { + rtapi_sint end = rtapi_get_time() + (rtapi_sint)haldata->armed_ns; + haldata->armed_ns = 0; while (rtapi_get_time() < end) ; } diff --git a/tests/stale-motion-status/slow-controller/test.ini b/tests/stale-motion-status/slow-controller/test.ini index d4644a2b791..04fe41e073a 100644 --- a/tests/stale-motion-status/slow-controller/test.ini +++ b/tests/stale-motion-status/slow-controller/test.ini @@ -15,6 +15,8 @@ CYCLE_TIME = 0.001 [RS274NGC] PARAMETER_FILE = sim.var +SUBROUTINE_PATH = . +ON_ABORT_COMMAND = O call [EMCMOT] EMCMOT = motmod diff --git a/tests/stale-motion-status/test-ui.py b/tests/stale-motion-status/test-ui.py index edd6e09b2d2..92c65edb74b 100755 --- a/tests/stale-motion-status/test-ui.py +++ b/tests/stale-motion-status/test-ui.py @@ -97,6 +97,56 @@ def error(err): error(err) err = e.poll() +# slow-controller: a probe that ends without contact makes task abort, once. +# Once task has seen the probe fail and aborted, slowkins makes nearly every +# status read by task fail for a while, so its copy from before the abort, +# still in error, is all it has; it must not abort again on it, nor abort +# the move sent as soon as wait_complete() returns. on_abort.ngc counts the +# aborts in #4000. +def aborts(): + c.mdi("(debug,aborts #4000)") + c.wait_complete(60) + start = time.time() + while True: + err = e.poll() + if err and err[1].startswith("aborts "): + return int(float(err[1].split()[1])) + if time.time() - start > 60: + sys.stderr.write("timeout reading the abort count\n") + sys.exit(1) + time.sleep(0.01) + +if hal.component_exists("slowkins"): + c.mode(linuxcnc.MODE_MDI) + c.wait_complete(60) + settle("MDI mode was on", lambda: s.task_mode == linuxcnc.MODE_MDI) + before = aborts() + hal.set_p("slowkins.burst-ns", "995000") + hal.set_p("slowkins.burst-delay", "20") + hal.set_p("slowkins.burst-periods", "200") + for n in range(5): + c.mdi("G38.2 Z-0.01 F6") + c.wait_complete(60) + c.mdi("G0 Z0") + c.wait_complete(60) + start = time.time() + while True: + s.poll() + if s.state == linuxcnc.RCS_DONE and s.interp_state == linuxcnc.INTERP_IDLE: + break + if time.time() - start > 60: + sys.stderr.write("timeout waiting for the move after probe %d\n" % n) + sys.exit(1) + time.sleep(0.05) + if abs(s.position[2]) > 1e-6: + failures.append("the move after probe %d was lost, Z%.4f" % (n, s.position[2])) + break + count = aborts() - before + if count != n + 1: + failures.append("%d aborts after probe %d, want %d" % (count, n, n + 1)) + break + hal.set_p("slowkins.burst-periods", "0") + for msg in failures + errors: sys.stderr.write("error: %s\n" % msg) if h["finished"] != 1: