`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.
The manifest is the only contract a consumer has, since no source package
is shared between node repositories, and dora validate checks a dataflow
against it without running the node — so CI needs no robot.
request and response are deliberately untyped. They carry request_id, and
dora defines that as the marker for a pattern-correlated port where
several Arrow schemas legitimately flow, one per service; the runtime
skips type checks on those messages, so annotating them would claim a
contract neither side keeps.
examples/ carries its own manifest because dora resolves a node's
manifest by walking up from its path, and a manifest applies only when
<manifest dir>/<entrypoint> names the same file as the dataflow's path.
Without it the example caller would be matched against the arm node's
manifest and strict validation would fail on the entrypoint mismatch.