Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
32574c7
Behave as par6 does on every waldoctl method the two backends share
claude Sep 23, 2026
481ad0d
Finish the parity pass: zero-length moves, the jog_l brake gate, tcp_…
claude Sep 23, 2026
23e03fa
Close the review findings on the parity pass
claude Sep 24, 2026
cd24ba7
Default planned moves to half speed, grip current as a fraction, join…
Jepson2k Sep 26, 2026
53ed97c
Plan out of the singularity continuously, and make the timing tests r…
claude Sep 26, 2026
3713d53
Warm the strided row layout; start the process move clear of the wrist
claude Sep 26, 2026
236551d
Draw a full circle as one; time stream tests on ticks; run every example
claude Sep 27, 2026
a2f3256
Wait on the examples' long moves long enough
claude Sep 27, 2026
7280109
Pin waldoctl v0.15.0
claude Sep 27, 2026
1a3ba85
Make the blend hold configurable, and hold longer in tests
claude Sep 27, 2026
f6435ee
Wait for the held move_j by its index in the mixed-type blend test
Jepson2k Sep 27, 2026
a5ef533
Measure the full-circle move_c against the circle, not its sample chords
Jepson2k Sep 27, 2026
49eb560
Fail, attribute and resync every drop the pipeline makes
Jepson2k Sep 27, 2026
9f77ff3
Plan every path along what it actually traces, tick for tick
Jepson2k Sep 27, 2026
5ebc576
Restart every stream from the arm, and hold it to its limits and keep…
Jepson2k Sep 27, 2026
33cc9b6
Queue tool actions with motion; only a tool stop jumps the queue
Jepson2k Sep 27, 2026
624a052
Report a trajectory's queue time as the ticks it plays; isolate in-pr…
Jepson2k Sep 27, 2026
3594743
Wait out loopback delivery in the harness tests macOS runs late
Jepson2k Sep 27, 2026
f4f2bfd
Match dry-run tool and cancellation behavior to the controller
Jepson2k Sep 28, 2026
6946ca9
Wait for accepted commands before reporting settled motion
Jepson2k Sep 28, 2026
d530835
Give the IK worker's first answer the time its cold start takes
Jepson2k Oct 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 48 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,12 +301,15 @@ Note: RUCKIG is point-to-point only and cannot follow Cartesian paths. When RUCK
### Speed and acceleration

```python
client.moveJ(target, speed=0.5, accel=0.5) # 50% of joint limits
client.moveL(target, speed=0.25, accel=1.0) # 25% cart speed, full accel
client.moveL(target, duration=2.0) # Fixed duration (uses TOPPRA)
client.move_j(target) # default speed=0.5, accel=0.5
client.move_l(target, speed=0.25, accel=1.0) # 25% cart speed, full accel
client.move_l(target, duration=2.0) # Fixed duration; speed is unused
```

Speed and accel are fractions of maximum (0.0–1.0), not percentages.
Speed and accel are fractions of maximum in (0, 1], not percentages. A
`duration` > 0 times the move and `speed` is ignored; otherwise `speed` times
it. `servo_j`/`servo_l` default to `speed=0.5, accel=0.5`, and jogs to
`accel=0.5`.

For Cartesian moves, joint limits stay at 100% as hard bounds—the speed fraction only affects the Cartesian velocity constraint.

Expand All @@ -330,17 +333,22 @@ the pause request can be acknowledged while still decelerating. Queued delays
retain their remaining time while paused; positive speed changes do not retime
delays, tool actuators or homing routines already in progress.

Completion waits query the requested command's exact success. Tool actions run
concurrently with arm motion, so the highest completed index alone cannot prove
that an earlier command finished. The controller retains its latest 1024
successful completions; an unknown, cancelled, or expired result remains
unconfirmed. A controller-session change during a wait raises `ConnectionError`.
This requires matching client and controller versions supporting the completion
query.
Completion waits query the requested command's exact success. A jog, a servo
stream or a `tool.stop()` can finish ahead of commands queued before it, so the
highest completed index alone cannot prove that an earlier command finished.
The controller retains its latest 1024
successful completions and 1024 failures. A wait on a command that was
discarded — by `stop()`, an E-stop, `reset_state()`, a teleport or a failure
queued ahead of it — raises `MotionError` (`MOTN_CANCELLED`) at once, and one
on a command the pipeline failed raises `MotionError` with that failure. An
unknown or expired result remains unconfirmed. A controller-session change
during a wait raises `ConnectionError`. This requires matching client and
controller versions supporting the completion query.

Standalone `wait_command()` keeps its wall-clock timeout and returns false if
completion is unconfirmed. Blocking motion calls raise `TimeoutError` in that
case. A timed-out wait leaves the motion queued; `stop()` cancels it. Planning
completion is still unconfirmed when it runs out. Blocking motion calls raise
`TimeoutError` in that case. A timed-out wait leaves the motion queued;
`stop()` cancels it. Planning
preview retimes trajectories and reports paused queued operations as
`UnresolvedPreview` instead of claiming completion.

Expand All @@ -360,7 +368,7 @@ checks; no continuous recorded-trajectory command is added.

## Command system

Jog and servo commands (JogJ, JogL, ServoJ, ServoL) automatically use the streaming fast-path — the server de-duplicates stale inputs, reduces ACK chatter, and reuses the active command. Use jog/servo for UI-driven motion or teleoperation; use planned moves (MoveJ, MoveL, etc.) for discrete motions and queued programs.
Jog and servo commands (JogJ, JogL, ServoJ, ServoL) automatically use the streaming fast-path — the server de-duplicates stale inputs, reduces ACK chatter, and reuses the active command. A servo stream stops about 0.25 s after its last target arrives: the arm brakes to rest and holds. Use jog/servo for UI-driven motion or teleoperation; use planned moves (MoveJ, MoveL, etc.) for discrete motions and queued programs.

### Command categories

Expand Down Expand Up @@ -411,6 +419,32 @@ with RobotClient() as c:

Add a new tool by creating a `ToolConfig` subclass (or using `ToolConfig` directly) and calling `register_tool("KEY", config)` in `parol6/tools.py`.

### Tool actions

Tool actions (`tool.open()`, `close()`, `set_position()`, `calibrate()`,
`release()`, `tool_action(...)`) are queued work, in the same queue as planned
motion:

- **They take their turn.** A tool action waits for the motion queued ahead of
it, the arm holds still while it runs, and the motion queued after it waits
for it. A tool action sent after a blended move ends the blend there. A wait
on a tool action therefore covers the motion queued ahead of it, and
`queued_duration` counts the tool's estimated travel.
- **A failure cancels what follows.** A `move` on a gripper never calibrated is
refused when its turn comes, under its own index; like any command that
fails, it cancels the commands queued behind it (`MOTN_CANCELLED`).
- **What discards the queue discards them.** A pause holds them. `stop()`,
`estop()`, `reset_state()`, a teleport, or a jog or servo stream taking the
arm fails the queued ones as cancelled and halts the one running where the
jaws are, keeping the grip.
- **`tool.stop()` is immediate.** It is not queued: it halts the tool action
running, failing it as cancelled by a tool stop, keeps everything queued
behind it, and holds the next queued tool action until the jaws are still.

`release()` drops the grip without moving the jaws. A pneumatic action lasts
its estimated stroke; an electric `calibrate` lasts 200 control ticks (two
seconds at the default 100 Hz).


**Security note:** The controller has no authentication — it accepts any correctly parsed command on its UDP port. Multiple senders are supported by design (e.g., GUI + orchestrator), but deploy only on trusted networks.

Expand Down
13 changes: 10 additions & 3 deletions examples/async_client_quickstart.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ async def run_client() -> int:
ok = await client.simulator(True)
print(f"simulator(True): {ok}")

# Planned motion is refused until the robot is referenced, and a
# freshly started one is not — however sensible its reported
# angles look.
await client.home(wait=True)

print("ping:", await client.ping())
pose_xyz = (await client.pose())[:3]
print("pose xyz:", pose_xyz)
Expand All @@ -36,9 +41,11 @@ async def run_client() -> int:
print(status.speeds)
break

# Small relative move (safe in simulator)
# Move +5mm in Z over 1.0s
moved = await client.move_l([0, 0, 5, 0, 0, 0], rel=True, duration=1.0)
# Small relative move (safe in simulator): +5mm in Z over 1.0s,
# waited on so a refusal raises instead of passing unnoticed
moved = await client.move_l(
[0, 0, 5, 0, 0, 0], rel=True, duration=1.0, wait=True
)
print("move_l ->", moved)

return 0
Expand Down
18 changes: 8 additions & 10 deletions examples/demo_showcase.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,12 @@
rbt = RobotClient()

HOME_ANGLES = [90.0, -90.0, 180.0, 0.0, 0.0, 180.0]
HOME_TOLERANCE_DEG = 2.0

# Select tool, and home only if not already near the home pose
# Select the tool and home: an arm that is already homed returns to the
# home pose with a planned move, and one already there does not move.
rbt.select_tool("SSG-48")
rbt.tool.calibrate()
current = rbt.angles()
if (
current is None
or max(abs(a - h) for a, h in zip(current, HOME_ANGLES)) > HOME_TOLERANCE_DEG
):
rbt.home()
rbt.home()

# move_j vs move_l (joint-space then linear-cartesian to nearby pose)
rbt.move_j(pose=[100, 340, 334, 90, 0, 90], speed=0.5)
Expand Down Expand Up @@ -90,8 +85,11 @@ def circle_pt(cx, cz, angle_deg):
is_last = row == ROWS - 1
y_start, y_end = (Y_MIN, Y_MAX) if row % 2 == 0 else (Y_MAX, Y_MIN)
rbt.move_l([X, y_start, z] + ZZ_ORI, speed=1.0, r=BLEND, wait=False)
rbt.move_l([X, y_end, z] + ZZ_ORI, speed=1.0, r=0 if is_last else BLEND, wait=False)
rbt.wait_motion()
last = rbt.move_l(
[X, y_end, z] + ZZ_ORI, speed=1.0, r=0 if is_last else BLEND, wait=False
)
# The whole blended scan runs as one path: wait for its last move.
rbt.wait_command(last, timeout=30)

# ── Precision demo: pencil pick-up and TCP-offset rotations ──────────
PRECISION_POSE = [0, -250, 350, -90, 0, -90]
Expand Down
10 changes: 8 additions & 2 deletions examples/draw_circle.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
SPEED = 0.4
CIRCLE_Y = 340
ORIENTATION = [90, 0, 90]
CENTERS = [(0, CIRCLE_Y, 280), (0, CIRCLE_Y, 210), (0, CIRCLE_Y, 140)]
# The top circle sits just below where following it would need the wrist
# to flip mid-arc, which a cartesian move refuses.
CENTERS = [(0, CIRCLE_Y, 270), (0, CIRCLE_Y, 210), (0, CIRCLE_Y, 140)]


def circle_pt(cx, cz, angle_deg):
Expand All @@ -46,7 +48,11 @@ def circle_pt(cx, cz, angle_deg):
cx, _, cz = CENTERS[0]
rbt.move_j(pose=circle_pt(cx, cz, 0), speed=0.5, wait=True)
rbt.move_c(
via=circle_pt(cx, cz, 180), end=circle_pt(cx, cz, 0), speed=SPEED, wait=True
via=circle_pt(cx, cz, 180),
end=circle_pt(cx, cz, 0),
speed=SPEED,
wait=True,
timeout=30,
)

# Circle 2: two half-circle move_c arcs
Expand Down
16 changes: 6 additions & 10 deletions examples/precision.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,16 @@
rbt = RobotClient()

HOME_ANGLES = [90.0, -90.0, 180.0, 0.0, 0.0, 180.0]
HOME_TOLERANCE_DEG = 2.0

# Select tool, and home only if not already near the home pose
# Select the tool and home: an arm that is already homed returns to the
# home pose with a planned move, and one already there does not move.
rbt.select_tool("SSG-48")
rbt.tool.calibrate()
current = rbt.angles()
if (
current is None
or max(abs(a - h) for a, h in zip(current, HOME_ANGLES)) > HOME_TOLERANCE_DEG
):
rbt.home()
rbt.tool.calibrate(wait=True, timeout=30.0)
rbt.home(wait=True, timeout=30.0)

PRECISION_POSE = [0, -250, 350, -90, 0, -90]
rbt.move_j(pose=PRECISION_POSE, speed=0.5)
# The first Cartesian target also warms the controller's planner.
rbt.move_j(pose=PRECISION_POSE, speed=0.5, timeout=30.0)

# Test gripper: two quick close/open cycles
rbt.tool.close(speed=1.0)
Expand Down
7 changes: 5 additions & 2 deletions examples/zigzag_scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,12 @@
is_last = row == ROWS - 1
y_start, y_end = (Y_MIN, Y_MAX) if row % 2 == 0 else (Y_MAX, Y_MIN)
rbt.move_l([X, y_start, z] + ZZ_ORI, speed=0.5, r=BLEND, wait=False)
rbt.move_l(
last = rbt.move_l(
[X, y_end, z] + ZZ_ORI, speed=0.5, r=0 if is_last else BLEND, wait=False
)

rbt.wait_motion()
# The blended scan runs as one path: wait for its last move. A move that
# fails anywhere in the scan fails the ones behind it too, and raises.
if not rbt.wait_command(last, timeout=60.0):
raise SystemExit("the scan did not finish")
print("Done!")
5 changes: 3 additions & 2 deletions parol6/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from . import PAROL6_ROBOT

__version__: str = _pkg_version("parol6")
from .client.async_client import AsyncRobotClient
from .client.async_client import AsyncRobotClient, StatusSnapshot
from .client.dry_run_client import DryRunRobotClient
from .client.sync_client import RobotClient
from .protocol.wire import (
Expand All @@ -33,7 +33,7 @@
# Type aliases for backward compatibility
CurrentActionResult = CurrentActionResultStruct
LoopStatsResult = LoopStatsResultStruct
StatusResult = StatusResultStruct
StatusResult = StatusSnapshot
ToolResult = ToolResultStruct

__all__ = [
Expand All @@ -43,6 +43,7 @@
"DryRunRobotClient",
"RobotClient",
"PAROL6_ROBOT",
"StatusSnapshot",
# Result types (msgspec structs)
"CurrentActionResultStruct",
"LoopStatsResultStruct",
Expand Down
2 changes: 1 addition & 1 deletion parol6/ack_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
CmdType.SET_STATUS_RATE,
CmdType.SET_EXECUTION_SPEED,
CmdType.PAUSE,
CmdType.TELEPORT,
}

# Query command types (use request/response, not ACK)
Expand Down Expand Up @@ -50,7 +51,6 @@
CmdType.SERVOL,
CmdType.JOGJ,
CmdType.JOGL,
CmdType.TELEPORT,
CmdType.RESET_LOOP_STATS,
}

Expand Down
Loading
Loading