fix jogging stepping by holding one velocity instead of re-arming
validate / validate (pull_request) Successful in 58m41s
validate / test (pull_request) Successful in 58m50s
validate / markdownlint (pull_request) Successful in 58m39s

`set_cartesian_velocity` passed `duration=velocity_command_ttl_s` on every
message, so each one re-armed a timed segment and the controller re-planned
the same motion. On a real Lite6 that is not smooth travel: the arm moves in
steps. Reducing how often the velocity is sent does not help — re-arming four
times a second still steps — so it now uses `duration=0`, the SDK's "always
effective, will not stop automatically", and skips a velocity that has not
changed. One command stands until a different one replaces it.

That removes the controller-side expiry, which `_expire_jog` was relying on:
it flipped `_jogging` and reported `ready` without commanding anything,
because the controller's own timer had already stopped the arm. With the
velocity held indefinitely that would leave an arm travelling while the node
reported idle, so `_expire_jog` now queues the stop itself. It goes through
the job queue rather than the driver, since it runs on the reporter thread
and the control thread owns the driver.

`VELOCITY_COMMAND_TTL_S` is unchanged in value and meaning to a caller, but
it is now enforced here rather than by the controller.

Both halves are one commit on purpose: `duration=0` without the stop in
`_expire_jog` is an arm that never stops when its sender dies.

Adds tests/test_driver.py — the driver had no direct coverage, because
FakeArm replaces it everywhere else, which is why this was invisible to the
suite.
This commit is contained in:
2026-09-16 14:27:46 -03:00
parent f29265a860
commit a95d804f0b
8 changed files with 201 additions and 25 deletions
+12 -4
View File
@@ -575,16 +575,24 @@ class Control:
# --- the reporter thread ---
def _expire_jog(self) -> None:
"""Stop calling the arm jogging once the controller has expired the command.
"""Stop the arm once the sender driving it has gone quiet.
A jog lives on the controller's own TTL, so a sender that goes quiet
leaves an arm that stopped moving a moment later. Nothing tells us; the
clock does.
**This is the deadman, not bookkeeping.** The velocity is set on the
controller once and held there indefinitely — see
:meth:`ULite6.set_cartesian_velocity` for why it cannot be given a
duration — so a sender that dies mid-jog leaves an arm that travels
until something says otherwise. Nothing tells us; the clock does, and
this is what says otherwise.
The stop goes through the job queue rather than the arm directly: this
runs on the reporter thread, and the control thread is the only one that
may touch the driver.
"""
with self._lock:
if not self._jogging or time.monotonic() - self._last_jog < self.config.motion.velocity_command_ttl_s:
return
self._jogging = False
self._jobs.put(Jog(velocity=[0.0] * 6))
self._idle_unless_busy()
def _run_reporter(self) -> None: