Skip to content

Behave as par6 does on every shared waldoctl method - #52

Open
Jepson2k wants to merge 18 commits into
mainfrom
claude/tender-darwin-9dq67v
Open

Jepson2k wants to merge 18 commits into
mainfrom
claude/tender-darwin-9dq67v

Conversation

@Jepson2k

Copy link
Copy Markdown
Collaborator

Makes parol6 behave as par6 does on every waldoctl method the two backends share, then closes the review findings from that pass. Each commit message has the full breakdown; the main points:

Stop and completions. Every command that a stop, e-stop, reset_state, simulator toggle, teleport or preempting stream discards completes as failed with MOTN_CANCELLED, and wait_command raises it. MotionError is a waldoctl RobotError, so a frontend catches one type on either backend. reset_state resets the program, not the controller.

Safety and limits. Targets are checked for finiteness and joint limits at the wire, including every target in a blended move_j chain with relative steps. jog_j brakes each joint on its own profile, so a joint joining a jog cannot carry another past its limit. The streaming executors' graceful stop now actually brakes. Before, ruckig handed out a copy of its targets, so the writes to them changed nothing and every jog_l release, IK stop and servo_l brake ended abruptly.

Streams. jog_l is a 6-DOF twist that integrates translation and rotation separately, so the TCP stays on a straight line. jog_l and servo streams are refused unhomed, and the refusal is latched as the standing error. Servo streams honour their speed fraction, brake and hold on an unreachable target, and hold after 0.25 s of client silence. A jog_l braked short of a keep-out ends FAILED with SYS_SELF_COLLISION.

Planned motion. move_p rounds each corner at one tool speed. move_s is a natural cubic spline, and it slerps rotations as the wire names them (intrinsic XYZ). move_c takes a blend radius, so move_l(r) → move_c(r) → move_l is one continuous path. A path leaving a wrist singularity turns the wrist first, and that turn counts toward the move's duration and is collision-checked. An IK branch hop above 0.35 rad refuses the move with IK_PARTIAL_PATH instead of smoothing it over. A move to within a motor step of where the arm stands is done in place.

Tools and queries. Electric grippers speak move/calibrate/stop/idle and are validated at the wire. A move before a calibrate is refused. The default grip current comes from the tool config. joint_speeds is rad/s. tcp_speed differentiates across frame-stamped samples, so it no longer depends on the broadcast period. status() carries a ToolStatus. The dry run honours select_profile and previews jog_j in the right units.

Companion branches

This depends on the waldoctl changes on Jepson2k/waldoctl@claude/tender-darwin-9dq67v (MOTN_CANCELLED / RobotError.cancelled, default_current, and the contract docs). CI picks that branch up by name. The waldoctl pin here is still v0.14.0, so the waldoctl branch needs to be merged and tagged, and this PR's pin bumped to that tag, before this merges. A matching Waldo-Commander branch also exists.

— written by Claude on behalf of @Jepson2k

🤖 Generated with Claude Code

claude and others added 18 commits September 23, 2026 07:24
Stop and completions:
- Every command a stop, e-stop, reset_state, simulator toggle or teleport
  discards completes as failed with MOTN_CANCELLED, and wait_command
  raises it; MotionError is a waldoctl RobotError, so a frontend catches
  one type on either backend.
- A tool action stops in place on stop/e-stop; the tool side channel is
  a FIFO so tool actions run in queue order with the moves they follow.
- reset_state resets the program, not the controller: the protective
  stop, homed state, outputs and gripper command stay.

Tools:
- Electric grippers speak move/calibrate/stop/idle, validated at the
  wire (exactly [position, speed, current_ma], in range); a move before
  a calibrate is refused; a tool action names the selected tool; the
  default grip current comes from the tool config.

Streams:
- jog_l is a 6-DOF twist; jog_j stops one joint short of its soft
  limit and leaves the others running; jog_l and every servo stream are
  refused unhomed; servo streams honour their speed fraction, brake on
  an unreachable target and hold after 0.25 s of client silence.

Planned motion:
- move_p rounds each corner by a quarter of the shorter leg at one tool
  speed; move_s is a natural cubic spline on chord-length knots; TRF is
  an offset in the tool frame at the start of the move; a WRF rel pose
  rotates about the TCP.
- move_c takes a blend radius: a Line|Arc chain shares one cubic corner
  with move_l, so move_l(r) → move_c(r) → move_l is one continuous path.
- LINEAR ramps at the hardware acceleration limit over the path's own
  length; a TOPPRA failure refuses the move; an IK branch hop above
  0.35 rad refuses it as IK_PARTIAL_PATH instead of being smoothed over.
  A path leaving a wrist singularity (the standby pose is one) turns
  the wrist first, as a joint move the tool never sees, then runs the
  cartesian path from the turned wrist.
- Targets are checked for finiteness and joint limits at the wire.

Queries and control:
- joint_speeds is rad/s (is_robot_stopped defaults to 0.01 rad/s);
  queue()/activity() report snake_case command names; status() carries
  a ToolStatus; teleport is an acked system command that validates its
  pose, cancels what was driving the arm and references it.
- tcp_speed differentiates across a window of frame-stamped samples
  instead of assuming the broadcast period, which a client polling
  between broadcasts halved.
- A stream refused in setup latches the refusal as the standing error,
  so a jog_l or servo sent unhomed is answered in STATUS.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HsNxn193Lto6BF7RZXuc6
…speed

- A planned move to within a motor step of where the arm stands is done
  where it stands, instead of being handed to a timing solver that has
  no distance to spread and refuses it.
- The brake a jog_l runs out after a predicted collision withholds the
  configurations that would reach the contact and holds the last clear
  one, so escaping one keep-out never streams into the next.
- tcp_speed differentiates across a window of frame-stamped samples
  rather than over the broadcast period, which a client polling between
  broadcasts halved and the loop's jitter scattered.
- Tests: a servo_l stream is driven the way a client drives it, refreshed
  until the tool is on target, since a silent stream now brakes and holds;
  status() is a StatusSnapshot with a ToolStatus; hand-built jog_l states
  are referenced.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HsNxn193Lto6BF7RZXuc6
Safety and limits:
- A blended move_j chain checks every target against the joint limits,
  relative moves included, so a chain of rel steps cannot plan past one.
- jog_j brakes each joint on its own profile (no Ruckig time sync) and
  keeps the lookahead's measured speed across the datagrams that stream
  it, so a joint joining the jog cannot carry another past its limit.
- Servo targets are checked for finiteness, and servo_j targets against
  the joint limits, where the controller decodes them.
- The streaming executors' graceful stop brakes: ruckig hands out a copy
  of its targets, so the per-element writes it replaced changed nothing
  and every jog_l release, IK stop and servo_l brake ended abruptly.

Completions and errors:
- A stream that preempts planned motion fails every index it discarded
  with MOTN_CANCELLED, as stop does.
- A jog_l braked short of a keep-out ends FAILED with SYS_SELF_COLLISION
  latched as the error error() reads; the executor latches every failed
  command's error and records it against its index.
- servo_l through an unreachable pose brakes and holds, then resumes from
  the arm when the stream moves on to a pose it can reach.
- A tool action sent right behind a select_tool is judged against that
  selection, and the teleport's tool check uses it too.
- A planned home reports no referencing progress; the seek's step is
  cleared when the seek ends.
- tcp_speed reads zero once the arm has not moved for a window of control
  ticks, whatever the status rate.

Paths:
- jog_l integrates translation and rotation separately, so a twist with
  both keeps the TCP on a straight line; the dry run applies the same
  speed ceiling to a diagonal.
- A move that starts with a wrist turn takes the duration it names, turn
  included; the turn's interior rows are collision-checked; the dry run
  takes the same turn instead of calling the move unreachable.
- LINEAR, TRAPEZOID and QUINTIC time a cartesian path along the tool's
  distance, and hold a process move to one tool speed under the tool
  ceiling, as TOPPRA does.
- move_s slerps between the waypoints' rotations read as the wire names
  them (intrinsic XYZ); rotation angles use atan2, so a repeated pose
  measures zero.

Client and dry run:
- Pose moves report as move_j and servo_j; move_j(pose=..., rel=True)
  raises; the dry run honours select_profile, previews jog_j distance in
  the right units and answers select_tool/set_tcp_offset with an index.
- A gripper move that names no current grips at the middle of the tool's
  current range (waldoctl's default_current).

Simplified: one outcome-ring size, the attachment-changed message as a
constant, pose6_to_se3, blend setup on the chain-link mixin, the scaled
velocity limit, typed tool side channel, the servo step folded, the
teleport refusals in the controller; dead segment accessors, jog minima
and motion re-exports removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HsNxn193Lto6BF7RZXuc6
…t speeds in deg/s

Planned moves take duration: float = 0.0, speed: float = 0.5 and
accel: float = 0.5, as waldoctl now states. planned_move_timing, shared
by the live, sync and dry-run clients, checks them before anything is
sent: a duration above 0 times the move, otherwise speed does, and a
negative or non-finite duration, or a speed or accel outside (0, 1],
raises ValueError. The wire is unchanged: the unused field goes out as
it already did. Servo streams default to half speed and half accel,
jogs to half accel.

An electric gripper's move carries current as a fraction of the tool's
current range, as it already carried speed, and the controller turns it
into the gripper's mA where it builds the hardware command.
set_position/open/close take speed and current as typed fractions, and
adjust_step is 10 percent points.

The status cache publishes joint speeds in deg/s, converted in place
when they change, so joint_speeds(), STATUS and is_robot_stopped and
wait_motion's thresholds (now 0.5 deg/s) read in degrees like angles.

Two tests pass accel=1.0 explicitly where they compare against a
preview that does not model the ramps or need a profile margin that
half accel would eat.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ead the plan

The parity pass failed CI on every runner. Two planner faults and eight
tests that asserted wall-clock timing, which the macOS runners run at
40-49 Hz.

Planner:
- A cartesian joint path is the IK solver's own array, and the solver
  hands it back Fortran-ordered: its rows were a layout the warmup never
  compiled, so the first cartesian plan in a process compiled the
  rad-to-steps kernel for seconds in the middle of a chain. The path is
  made contiguous where it is built.
- The turn a chain takes out of the wrist singularity was the first one
  in the list that left a chain on one branch; with the split between J4
  and J6 settled a hair wrong, the solver snapped the wrist 12 degrees
  in one row once the tool had left the singularity, which TOPP-RA
  absorbed and the row-timed profiles stretched a 5 mm move to 8 s over.
  The first turn whose chain steps no joint further between two rows
  than the turn's own rows do is taken now, the fixed turns before the
  solver's hint, so the choice stays the same from run to run.

Status:
- tcp_speed differentiates over perf_counter stamps; monotonic ticks
  every 15.6 ms on Windows before Python 3.13 and scattered the readout
  by a fifth.

Tests:
- The recompile test plans a cartesian move; a move out of the
  singularity has to plan under 3 s on LINEAR and TRAPEZOID; a coarse
  monotonic clock must not scatter tcp_speed.
- The profile timing test reads the duration the controller planned
  from the paused queue; the corner test reads the one tool speed off
  the plan the arm plays and the geometry off the arm; a spline passes
  its waypoints within the polyline the status samples draw; the servo
  and jog tests tick the in-process controller at the control rate on
  every OS, and the jog_l preview test runs in-process; the keep-out jog
  ticks through the datagram still in flight before asserting the latch;
  the process move from home, near-singular at both ends, waits 20 s.
- Grip current on the raw calibration-gate move is a fraction.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HsNxn193Lto6BF7RZXuc6
The IK solver returns a cartesian joint path Fortran-ordered, and the
planner converts its rows to steps: the warmup compiles the conversion
for that strided layout, so the first cartesian plan in a process no
longer compiles it in the middle of a move. The path keeps the solver's
layout.

test_move_p_basic starts clear of the home pose's wrist singularity: a
process move runs at the one speed its slowest row allows, and at the
singularity that row is the wrist's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HsNxn193Lto6BF7RZXuc6
- A move_c whose end is its start sweeps the whole circle. The arc took
  the sweep from the angle between start and end, which the arm's settle
  error leaves a hair above zero, and a full circle ran as a nudge or was
  refused; it now takes a returned end as a full circle, as ArcSegment
  and par6 do.
- tcp_speed and the stale-broadcast check read one clock, perf_counter.
- The servo grace and jog duration tests run the commands' timers on a
  virtual clock advanced one control interval per tick, so they count
  ticks on any runner. The keep-out latch test pings the controller
  after the stop: once the ping is answered, no jog sent before it is
  still in flight to clear the error.
- The blend-chain test waits on the last move's completion rather than
  on the arm keeping still.
- The wrist-turn collision test clears the keep-out it gave its preview:
  a preview's shapes are the process's, and every later preview planned
  around it.
- The examples step runs demo_showcase and precision against a
  controller the test starts, as a Waldo Commander session provides one;
  they home unconditionally, since the arm boots unhomed at the home
  angles. draw_circle's top circle moves down to 270 mm: at 280 mm
  following it needs the wrist to flip mid-arc, which a cartesian move
  refuses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HsNxn193Lto6BF7RZXuc6
demo_showcase waited on its blended zig-zag with wait_motion, which gives
up after 10 s without saying so; the chain runs as one 6.6 s path, and
on a slow loop the next move timed out behind it. It waits on the scan's
last move instead, for up to 30 s. draw_circle's full circle, a 4.9 s
path, gets the same 30 s its spline already has 60 s for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HsNxn193Lto6BF7RZXuc6
The contract this branch implements is released as waldoctl v0.15.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HsNxn193Lto6BF7RZXuc6
The planner plans a blend chain once no command has arrived for the
blend hold, 0.1 s as in par6's blend_hold. A test sends a chain one
acknowledged command at a time, and on a degraded CI loop (macOS runs
~40 Hz) two of them can arrive further apart than that: the chain is
planned in pieces, its corners are not rounded, and from the standby
wrist singularity the piece after the split needs a mid-path wrist flip
and is refused. PAROL6_BLEND_HOLD_S sets the hold; the test server uses
0.5 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HsNxn193Lto6BF7RZXuc6
With the 0.5 s blend hold the planner keeps move_j(r=20) waiting for a
blend partner, and on a slow runner wait_motion gave up waiting for it
to start: mid_pose was the pose before the move, move_l reached the
planner alongside the held move_j, and its line back across the wrist
singularity failed IK. The move's own completion is what the test
needs before reading the pose.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QGctXgWU5nqo9ERf6t5A4D
The via-point check measured the via point's distance to the straight
chord between consecutive TCP samples. On the macOS runner the loop
runs near 47 Hz with ticks up to 84 ms late, so samples land ~36° apart
and a chord cuts 1.5 mm inside the arc while the arm stays on it. The
test now holds every sample to the circle and asks for one whole turn
about its centre, which passes the via point opposite the start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QGctXgWU5nqo9ERf6t5A4D
A queued command the pipeline drops (a stream preempting the queue, a plan
that fails, a failed inline command, a world-guard refusal, reconnect,
attachment change, simulator switch) now goes through one drop point: the
planner is resynced to the tool, TCP, shapes and profile the controller
actually holds, and each failing command records its own attributed error
before its followers fail MOTN_CANCELLED. A wait on the failed command
raises its error after a later command clears the standing one.

Stream refusals are attributed to the stream's own index and latch the
standing error only when nothing else is in flight; an unhomed jog_l or
servo is refused before it can preempt a running home. A teleport clears
the standing error and collision, lands before motion read in the same
batch, accepts the tool positions status reports, and the pneumatic jaw
reads back where it was put. The completion ring looks indices up in
O(1). PAROL6_BLEND_HOLD_S must be a positive duration, and a blend chain
is held while earlier motion plays, as par6 holds it.

The integration fixture clears a latched protective stop before resetting,
so one failed test no longer disables the server for every later one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QGctXgWU5nqo9ERf6t5A4D
- A joint parked on its limit step reads back up to half a step past the
  radian limit; the limit checks (move_j, blend links, wire, IK, the wrist
  turn) allow that half step, so relative moves and replayed rows are no
  longer refused.
- TRAPEZOID, QUINTIC and RUCKIG size multi-row paths by path length and
  time blended move_j chains along the chain, so a closed circle and an
  out-and-back chain run instead of collapsing to their endpoints; the
  cartesian speed ceiling applies under every profile, and rows are one
  tick apart so a move lasts the time it was planned for.
- An IK step too big to take is bisected and re-solved before it is
  refused, so a turn a hair off the wrist singularity plans; a genuine
  branch flip is still refused.
- Lines interpolate position and rotation separately (no screw bow);
  move_p and move_s are sampled by geometry on the combined metric; a
  spline turns the tool at a waypoint that only turns it without swinging
  its position.
- One arc and one TRF resolver: a collinear via fails the move_c on its
  own index whether or not it follows a blend. A failing chain previews
  its first move before the second.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QGctXgWU5nqo9ERf6t5A4D
…-outs

- A cancelled stream (stop, estop, teleport, a planned move, a change of
  stream type, a failed or raising command) resets both streaming
  executors, so the next jog or servo starts from where the arm is rather
  than resuming the discarded stream's position and velocity, and a jog_l
  never solves from zero seeds.
- Every servo_l and jog_l joint step, brakes included, is clamped to the
  joint speeds, so a stream going silent or losing IK never sends the lag
  the clamp was holding back in one tick; servo_l resumes when its client
  resends the target it braked short of.
- jog_j releases a joint's limit latch once it leaves the jog or turns
  back, never commands a joint across its limit whatever the accel does
  mid-brake, and stops a joint the arm reports past it.
- servo_j, servo_j_pose and servo_l refuse a colliding target and brake to
  a SYS_SELF_COLLISION failure on a predicted contact, as jog_l does.
- jog_l resolves its twist against the tool as it stands, and servo_l
  takes each target from where the stream is, so axes follow the tool and
  a steady turn never wraps back at half a turn; the preview follows.
- Jog durations count control ticks; the hot paths reuse their buffers
  and skip re-solving and re-applying what has not changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QGctXgWU5nqo9ERf6t5A4D
waldoctl marks tool_action QUEUED: it enters the controller's command
queue with the motion around it. The side channel that ran tool actions
beside the motion is gone. move_l(A); close(); move_l(B) closes the jaws
once the arm rests at A, holds the arm while they close, and leaves A
once they have closed, with no blend across the close. A tool action
waits under pause, counts in queued_duration and shows in queue(), and a
wait on it covers the motion queued ahead of it.

A tool action refused when its turn comes (not the fitted tool, not
calibrated) fails on its own index and cancels what is queued behind it.
Stop, estop, reset_state, teleport and a stream preempting the queue drop
queued tool actions and halt a running one where it is, keeping its
grip; a serial reconnect halts the tool before it forgets the
calibration. tool.stop() stays immediate: it halts the running action
("a tool stop"), completes when the jaws are still, and keeps what is
queued behind it. The preview orders tool actions the same way, a
release keeps the jaws where they are, and a calibration takes its two
seconds; the pneumatic valve dwells its travel. The sync tool_action
defaults to wait=False, as the contract has it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QGctXgWU5nqo9ERf6t5A4D
…ocess tests

A trajectory's duration is now the time the player spends on it, one tick
per row including the start row, so the preview and the controller's
queued_duration agree to the tick.

The in-process controller fixture puts the process-wide robot model back
(no tool, no program shapes) when it closes: a shape one test attached
had every later in-process plan refusing a self-collision with it. The
r=0 blend test waits on its last move's own index, since that move's
radius holds it for a partner while the arm rests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QGctXgWU5nqo9ERf6t5A4D
On macOS the loopback socket can hand a datagram over a tick or two after
it was sent. The jog_l-after-a-cancel test now drains the controller's
socket before waiting for the jog to end, so it never mistakes a jog not
yet read for one already finished; the teleport tests wait up to two
seconds for an acknowledgement the controller has already sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QGctXgWU5nqo9ERf6t5A4D
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants