From c25b06a8ee68aefc7da0cfa2560421b693686711 Mon Sep 17 00:00:00 2001 From: jepson2k <55201008+Jepson2k@users.noreply.github.com> Date: Fri, 4 Sep 2026 21:31:30 -0400 Subject: [PATCH 1/4] Carry the seventh shape wire element and refuse physics explicitly waldoctl's Shape.to_wire() now emits [kind, params, pose, collision, margin, name, physics]. ShapeWire carries the seventh element (defaulted, so pre-physics peers still decode) and hands it to shape_from_wire, and the set-level validation refuses any shape that declares physics: this backend has no contact simulation, and silently flattening a body to its geometry would let a program believe it was being simulated. Co-Authored-By: Claude Opus 5 --- parol6/PAROL6_ROBOT.py | 7 +++++++ parol6/client/async_client.py | 8 ++++++-- parol6/commands/shape_commands.py | 4 +++- parol6/protocol/wire.py | 9 ++++++++- tests/integration/test_shapes_e2e.py | 15 ++++++++++++++- 5 files changed, 38 insertions(+), 5 deletions(-) diff --git a/parol6/PAROL6_ROBOT.py b/parol6/PAROL6_ROBOT.py index a505535..f22bf2b 100644 --- a/parol6/PAROL6_ROBOT.py +++ b/parol6/PAROL6_ROBOT.py @@ -294,6 +294,13 @@ def _validate_shapes(shapes: "Iterable[Any]") -> "list[Any]": for s in shapes: if s.kind not in _SHAPE_KINDS: raise ValueError(f"Shape {s.name!r}: unknown kind {s.kind!r}") + # Refused, never flattened to geometry: a caller declaring a body + # expects it to be simulated, and this backend simulates no contact. + if s.physics is not None: + raise ValueError( + f"Shape {s.name!r}: physics is not supported by the parol6 " + "backend (no contact simulation); declare geometry only" + ) return shapes diff --git a/parol6/client/async_client.py b/parol6/client/async_client.py index ac41cd1..43c8953 100644 --- a/parol6/client/async_client.py +++ b/parol6/client/async_client.py @@ -1003,11 +1003,15 @@ async def shapes(self) -> ShapeWorld | None: return None return ShapeWorld( installation=tuple( - shape_from_wire(w.kind, w.params, w.pose, w.collision, w.margin, w.name) + shape_from_wire( + w.kind, w.params, w.pose, w.collision, w.margin, w.name, w.physics + ) for w in resp.installation ), program=tuple( - shape_from_wire(w.kind, w.params, w.pose, w.collision, w.margin, w.name) + shape_from_wire( + w.kind, w.params, w.pose, w.collision, w.margin, w.name, w.physics + ) for w in resp.program ), ) diff --git a/parol6/commands/shape_commands.py b/parol6/commands/shape_commands.py index b16e8e1..362555f 100644 --- a/parol6/commands/shape_commands.py +++ b/parol6/commands/shape_commands.py @@ -41,7 +41,9 @@ class SetShapesCommand(SystemCommand[SetShapesCmd]): def execute_step(self, state: ControllerState) -> ExecutionStatusCode: shapes = [ - shape_from_wire(w.kind, w.params, w.pose, w.collision, w.margin, w.name) + shape_from_wire( + w.kind, w.params, w.pose, w.collision, w.margin, w.name, w.physics + ) for w in self.p.shapes ] state.set_shapes(shapes) diff --git a/parol6/protocol/wire.py b/parol6/protocol/wire.py index 772e882..8e40a07 100644 --- a/parol6/protocol/wire.py +++ b/parol6/protocol/wire.py @@ -638,7 +638,13 @@ class SetTcpOffsetCmd( class ShapeWire(msgspec.Struct, array_like=True, frozen=True, gc=False): - """One workspace shape — mirrors waldoctl ``Shape.to_wire()``.""" + """One workspace shape — mirrors waldoctl ``Shape.to_wire()``. + + ``physics`` (``[mass | None, [slide, spin, roll]]``) is carried so the + codec matches waldoctl's, and refused at apply: this backend has no + contact simulation. Pre-physics peers send six elements; the default + fills the seventh. + """ kind: str params: list[float] @@ -646,6 +652,7 @@ class ShapeWire(msgspec.Struct, array_like=True, frozen=True, gc=False): collision: bool margin: float | None name: str + physics: tuple[float | None, list[float]] | None = None class SetShapesCmd( diff --git a/tests/integration/test_shapes_e2e.py b/tests/integration/test_shapes_e2e.py index 32856fe..17f5a3f 100644 --- a/tests/integration/test_shapes_e2e.py +++ b/tests/integration/test_shapes_e2e.py @@ -17,7 +17,7 @@ import pytest from parol6 import MotionError, RobotClient -from waldoctl import Box +from waldoctl import Box, Physical pytestmark = pytest.mark.integration @@ -74,6 +74,19 @@ def test_set_shapes_ack_readback_rejection_and_timeout( assert world is not None assert tuple(s.name for s in world.program) == ("table",) + # No contact simulation here: a shape declaring physics is refused + # by name, never silently flattened to its geometry. + with pytest.raises(MotionError, match="physics"): + client.set_shapes( + [ + box, + Box(name="brick", x=0.1, y=0.1, z=0.1, physics=Physical(mass=0.2)), + ] + ) + world = client.shapes() + assert world is not None + assert tuple(s.name for s in world.program) == ("table",) + # Unreachable controller → unconfirmed (0), never a fake success. dead = RobotClient( host=ports.server_ip, port=ports.server_port + 91, timeout=0.3 From 49f464d5899ea56859c9eeb8281a95021f84cf4b Mon Sep 17 00:00:00 2001 From: jepson2k <55201008+Jepson2k@users.noreply.github.com> Date: Sun, 6 Sep 2026 11:39:20 -0400 Subject: [PATCH 2/4] Run the examples in CI, and fix the four that had rotted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `--examples` is opt-in and CI ran a bare `pytest`, so nothing had executed these scripts in a long time — and they are the code a user copies. Half of them failed on their own happy path against a stock simulator. Three never referenced the robot before commanding planned motion, which is refused outright. `precision.py` looked like it did: it homed only when the arm was not already NEAR the home angles, but a freshly started robot reports positions it has not homed to, so the guard skipped the very step the moves below needed. Proximity is not referencing; all three now home first, as the refusal's own remedy says. `demo_showcase.py` swept the wrist +/-20 degrees about each tool axis, which at that pose leaves the reachable set part-way through and comes back as "IK: partial path failure". Ten degrees clears it and still shows what the sweep is there to show — the pencil tip holding still while the wrist turns. CI now runs them after the suite, so the next one to rot says so. Co-Authored-By: Claude Opus 5 --- .github/workflows/tests.yml | 9 +++++++++ examples/demo_showcase.py | 5 ++++- examples/manage_server_demo.py | 3 +++ examples/precision.py | 13 +++++-------- examples/sync_client_quickstart.py | 4 ++++ 5 files changed, 25 insertions(+), 9 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index af955d3..8bba775 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -144,3 +144,12 @@ jobs: PYTHONUTF8: '1' run: | pytest + # The examples are the scripts a user copies, and `--examples` is opt-in, + # so nothing was running them: four had rotted into refusals on their + # own happy path. They run each script as a subprocess against the + # simulator, so they belong after the suite rather than inside it. + - name: Run examples + env: + PYTHONUNBUFFERED: '1' + PYTHONUTF8: '1' + run: pytest tests/test_examples.py --examples diff --git a/examples/demo_showcase.py b/examples/demo_showcase.py index 547e2c4..6fa53ff 100644 --- a/examples/demo_showcase.py +++ b/examples/demo_showcase.py @@ -134,7 +134,10 @@ def circle_pt(cx, cz, angle_deg): rbt.move_l([0, 60, 0, 0, 0, 0], speed=0.3, frame="TRF", rel=True, wait=True) # Precision TRF rotations — pencil tip stays stationary while wrist rotates - SWEEP = 20 + # 10 degrees, not more: at this pose a wider sweep takes the wrist out of + # the reachable set part-way through and the move is refused with + # "IK: partial path failure". + SWEEP = 10 for axis in range(3): delta = [0, 0, 0, 0, 0, 0] delta[3 + axis] = -SWEEP diff --git a/examples/manage_server_demo.py b/examples/manage_server_demo.py index f5192eb..a450941 100644 --- a/examples/manage_server_demo.py +++ b/examples/manage_server_demo.py @@ -32,6 +32,9 @@ def main() -> None: print("simulator(True):", sim_on) if sim_on: + # Reference the robot first: planned motion is refused until + # it is homed, whatever its reported angles look like. + client.home(wait=True) # Small relative move: +3mm in Z over 0.8s moved = client.move_l([0, 0, 3, 0, 0, 0], rel=True, duration=0.8) print("move_l ->", moved) diff --git a/examples/precision.py b/examples/precision.py index 91b9b83..5370870 100644 --- a/examples/precision.py +++ b/examples/precision.py @@ -14,22 +14,19 @@ PORT = 5001 HOME_ANGLES = [90.0, -90.0, 180.0, 0.0, 0.0, 180.0] -HOME_TOLERANCE_DEG = 2.0 with Robot(host=HOST, port=PORT, normalize_logs=True) as robot: rbt = robot.create_sync_client(timeout=2.0) rbt.wait_ready(timeout=5.0) rbt.simulator(True) - # Select tool, and home only if not already near the home pose 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(wait=True) + # Home unconditionally. Being NEAR the home angles is not the same as + # being referenced: a freshly started robot reports positions it has not + # homed to, so a proximity check skips the homing the moves below require + # and they are refused with "Robot not homed". + rbt.home(wait=True) PRECISION_POSE = [0, -250, 350, -90, 0, -90] rbt.move_j(pose=PRECISION_POSE, speed=0.5, wait=True) diff --git a/examples/sync_client_quickstart.py b/examples/sync_client_quickstart.py index 8492b0c..02ad6d7 100644 --- a/examples/sync_client_quickstart.py +++ b/examples/sync_client_quickstart.py @@ -23,6 +23,10 @@ def main() -> None: raise SystemExit(1) client.simulator(True) + # Planned motion is refused until the robot is referenced, and a + # freshly started one is not — however sensible its reported + # angles look. + client.home(wait=True) print("ping:", client.ping()) print("pose xyz:", client.pose()[:3]) print("angles:", client.angles()) From b9b01e3430821ac53b9ac96434c148b5363d23e8 Mon Sep 17 00:00:00 2001 From: jepson2k <55201008+Jepson2k@users.noreply.github.com> Date: Sun, 6 Sep 2026 13:24:40 -0400 Subject: [PATCH 3/4] demo_showcase: take the version Waldo Commander runs, verbatim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The example and the program loaded in Waldo Commander had drifted apart, and the WC one is the copy that gets run and watched. It sweeps 40 degrees with the pencil offset on tool X; the example had been changed to offset on tool Z by b1fe085 and nobody re-checked the sweep against it, so at 20 degrees the rotation path left the workspace and the move was refused with "IK: partial path failure" (190/200 poses reachable). Copied across as-is rather than reconciled by hand, so the script that ships as the example is the script that actually gets demonstrated. It runs standalone too — `pytest tests/test_examples.py --examples` drives it as a subprocess against the simulator. Note b1fe085's reasoning still stands on its own terms ("SSG-48 tool transform is along Z"), and precision.py still carries the Z offset; if that one is wrong too it wants the same treatment. Co-Authored-By: Claude Opus 5 --- examples/demo_showcase.py | 295 ++++++++++++++++++-------------------- 1 file changed, 141 insertions(+), 154 deletions(-) diff --git a/examples/demo_showcase.py b/examples/demo_showcase.py index 6fa53ff..31f5ce9 100644 --- a/examples/demo_showcase.py +++ b/examples/demo_showcase.py @@ -2,160 +2,147 @@ Exercises move_j, move_l, move_c, move_p, move_s, blended zig-zag, tool actions, TCP offset, and precision TRF rotations. - -Run: - python examples/demo_showcase.py """ import math - -from parol6 import Robot - -HOST = "127.0.0.1" -PORT = 5001 - -with Robot(host=HOST, port=PORT, normalize_logs=True) as robot: - rbt = robot.create_sync_client(timeout=2.0) - rbt.wait_ready(timeout=5.0) - rbt.simulator(True) - - # Select tool and home - rbt.select_tool("SSG-48") - rbt.tool.calibrate() - rbt.home(wait=True) - - # move_j vs move_l (joint-space then linear-cartesian to nearby pose) - rbt.move_j(pose=[100, 240, 334, 90, 0, 90], speed=0.5, wait=True) - rbt.move_l([-50, 240, 334, 90, 0, 90], speed=0.5, wait=True) - - # ── Curved motion: three vertical circles + sine-wave spline ────────── - RADIUS = 30 - SPEED = 0.4 - CIRCLE_Y = 340 - ORIENTATION = [90, 0, 90] - CENTERS = [(0, CIRCLE_Y, 280), (0, CIRCLE_Y, 210), (0, CIRCLE_Y, 140)] - - def circle_pt(cx, cz, angle_deg): - """Circle in the XZ plane (vertical) at fixed Y.""" - a = math.radians(angle_deg) - return [ - cx + RADIUS * math.cos(a), - CIRCLE_Y, - cz + RADIUS * math.sin(a), - ] + ORIENTATION - - # Circle 1: full circle with a single move_c (start = end) - 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 - ) - - # Circle 2: two half-circle move_c arcs - cx, _, cz = CENTERS[1] - rbt.move_l(circle_pt(cx, cz, 0), speed=SPEED, wait=True) - rbt.move_c( - via=circle_pt(cx, cz, 90), end=circle_pt(cx, cz, 180), speed=SPEED, wait=True - ) - rbt.move_c( - via=circle_pt(cx, cz, 270), end=circle_pt(cx, cz, 0), speed=SPEED, wait=True - ) - - # Circle 3: computed waypoints with move_p - cx, _, cz = CENTERS[2] - waypoints = [circle_pt(cx, cz, i * 30) for i in range(12)] - waypoints.append(waypoints[0]) - rbt.move_l(waypoints[0], speed=SPEED, wait=True) - rbt.move_p(waypoints, speed=SPEED, wait=True) - - # Sine wave through all three circle centers (bottom to top) using move_s - SINE_POINTS = 36 - z_min, z_max = CENTERS[2][2], CENTERS[0][2] - spline = [] - for i in range(SINE_POINTS + 1): - t = i / SINE_POINTS - z = z_min + t * (z_max - z_min) - x = RADIUS * math.cos(t * 3 * 2 * math.pi) - spline.append([x, CIRCLE_Y, z] + ORIENTATION) - rbt.move_s(spline, speed=SPEED, wait=True) - - # ── Zig-zag scan ───────────────────────────────────────────────────── - ZZ_ORI = [-180, -90, -180] - ROWS = 6 - Y_MIN, Y_MAX = 0, 160 - Z_MIN, Z_MAX = 200, 300 - X = 280 - BLEND = 15 - - rbt.move_j(pose=[X, 0, 334] + ZZ_ORI, speed=0.5, wait=True) - rbt.move_l([X, Y_MIN, Z_MAX + 30] + ZZ_ORI, speed=0.5, wait=True) - z_step = (Z_MAX - Z_MIN) / (ROWS - 1) - for row in range(ROWS): - z = Z_MAX - row * z_step - 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( - [X, y_end, z] + ZZ_ORI, speed=0.5, r=0 if is_last else BLEND, wait=False - ) - rbt.wait_motion() - - # ── Precision demo: pencil pick-up and TCP-offset rotations ────────── - # Home first — orientation flip from zigzag end requires fresh joint config. - rbt.home(wait=True) - PRECISION_POSE = [0, -250, 350, -90, 0, -90] - rbt.move_j(pose=PRECISION_POSE, speed=0.5, wait=True) - - # Test gripper: two quick close/open cycles - rbt.tool.close(speed=1.0) - rbt.tool.open(speed=1.0) - rbt.tool.close(speed=1.0) - rbt.tool.open(speed=1.0) - - # Approach pencil: move_j to 100mm above, descend linearly, grab, retract - PENCIL_ABOVE = [-90, -81.6, 161.8, 0, -69.4, 180] - rbt.move_j(angles=PENCIL_ABOVE, speed=0.3, wait=True) - rbt.move_l([0, 0, -100, 0, 0, 0], rel=True, speed=0.2, wait=True) - rbt.tool.close(wait=True) - rbt.move_l([0, 0, 100, 0, 0, 0], rel=True, speed=0.2, wait=True) - rbt.move_j(pose=PRECISION_POSE, speed=0.3, wait=True) - - # Offset TCP to pencil tip (~100mm exposed below gripper) - rbt.set_tcp_offset(0, 0, -100) - - # Pencil tip traces straight lines (linear precision demo) - # Forward/back (tool Z = world -Y at this pose) - rbt.move_l([0, 0, 100, 0, 0, 0], speed=0.3, frame="TRF", rel=True, wait=True) - rbt.move_l([0, 0, -200, 0, 0, 0], speed=0.3, frame="TRF", rel=True, wait=True) - rbt.move_l([0, 0, 100, 0, 0, 0], speed=0.3, frame="TRF", rel=True, wait=True) - # Side to side (tool Y = world -X at this pose) - rbt.move_l([0, 60, 0, 0, 0, 0], speed=0.3, frame="TRF", rel=True, wait=True) - rbt.move_l([0, -120, 0, 0, 0, 0], speed=0.3, frame="TRF", rel=True, wait=True) - rbt.move_l([0, 60, 0, 0, 0, 0], speed=0.3, frame="TRF", rel=True, wait=True) - - # Precision TRF rotations — pencil tip stays stationary while wrist rotates - # 10 degrees, not more: at this pose a wider sweep takes the wrist out of - # the reachable set part-way through and the move is refused with - # "IK: partial path failure". - SWEEP = 10 - for axis in range(3): - delta = [0, 0, 0, 0, 0, 0] - delta[3 + axis] = -SWEEP - rbt.move_l(delta, speed=0.5, frame="TRF", rel=True, wait=True) - delta[3 + axis] = SWEEP - rbt.move_l(delta, speed=0.5, frame="TRF", rel=True, wait=True) - rbt.move_l(delta, speed=0.5, frame="TRF", rel=True, wait=True) - delta[3 + axis] = -SWEEP - rbt.move_l(delta, speed=0.5, frame="TRF", rel=True, wait=True) - - # Place pencil back: descend linearly, release, retract - rbt.set_tcp_offset(0, 0, 0) - rbt.move_j(angles=PENCIL_ABOVE, speed=0.3, wait=True) - rbt.move_l([0, 0, -100, 0, 0, 0], rel=True, speed=0.2, wait=True) - rbt.tool.open(wait=True) - rbt.move_l([0, 0, 100, 0, 0, 0], rel=True, speed=0.2, wait=True) - - # Return and finish - rbt.move_j(pose=PRECISION_POSE, speed=0.3, wait=True) - rbt.home(wait=True) - print("Done!") +from parol6 import RobotClient + +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 +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() + +# 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) +rbt.move_l([-50, 340, 334, 90, 0, 90], speed=0.5) + + +# ── Curved motion: three vertical circles + sine-wave spline ────────── +RADIUS = 30 +SPEED = 0.8 +CIRCLE_Y = 340 +ORIENTATION = [90, 0, 90] +CENTERS = [(0, CIRCLE_Y, 280), (0, CIRCLE_Y, 210), (0, CIRCLE_Y, 140)] + + +def circle_pt(cx, cz, angle_deg): + """Circle in the XZ plane (vertical) at fixed Y.""" + a = math.radians(angle_deg) + return [ + cx + RADIUS * math.cos(a), + CIRCLE_Y, + cz + RADIUS * math.sin(a), + ] + ORIENTATION + + +# Circle 1: full circle with a single move_c (start = end) +cx, _, cz = CENTERS[0] +rbt.move_j(pose=circle_pt(cx, cz, 0), speed=0.5) +rbt.move_c(via=circle_pt(cx, cz, 180), end=circle_pt(cx, cz, 0), speed=SPEED) + +# Circle 2: two half-circle move_c arcs +cx, _, cz = CENTERS[1] +rbt.move_l(circle_pt(cx, cz, 0), speed=SPEED) +rbt.move_c(via=circle_pt(cx, cz, 90), end=circle_pt(cx, cz, 180), speed=SPEED) +rbt.move_c(via=circle_pt(cx, cz, 270), end=circle_pt(cx, cz, 0), speed=SPEED) + +# Circle 3: computed waypoints with move_p +cx, _, cz = CENTERS[2] +waypoints = [circle_pt(cx, cz, i * 30) for i in range(12)] +waypoints.append(waypoints[0]) +rbt.move_l(waypoints[0], speed=SPEED) +rbt.move_p(waypoints, speed=SPEED) + +# Sine wave through all three circle centers (bottom to top) using move_s +SINE_POINTS = 36 +z_min, z_max = CENTERS[2][2], CENTERS[0][2] +spline = [] +for i in range(SINE_POINTS + 1): + t = i / SINE_POINTS + z = z_min + t * (z_max - z_min) + x = RADIUS * math.cos(t * 3 * 2 * math.pi) + spline.append([x, CIRCLE_Y, z] + ORIENTATION) +rbt.move_s(spline, speed=SPEED) + +# ── Zig-zag scan ───────────────────────────────────────────────────── +ZZ_ORI = [-180, -90, -180] +ROWS = 6 +Y_MIN, Y_MAX = 0, 160 +Z_MIN, Z_MAX = 200, 300 +X = 280 +BLEND = 15 + +rbt.move_j(pose=[X, 0, 334] + ZZ_ORI, speed=0.5) +rbt.move_l([X, Y_MIN, Z_MAX + 30] + ZZ_ORI, speed=1.0) +z_step = (Z_MAX - Z_MIN) / (ROWS - 1) +for row in range(ROWS): + z = Z_MAX - row * z_step + 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() + +# ── Precision demo: pencil pick-up and TCP-offset rotations ────────── +PRECISION_POSE = [0, -250, 350, -90, 0, -90] +rbt.move_j(pose=PRECISION_POSE, speed=0.5) + +# Test gripper: two quick close/open cycles +rbt.tool.close(speed=1.0) +rbt.tool.open(speed=1.0) +rbt.tool.close(speed=1.0) +rbt.tool.open(speed=1.0) + +# Approach pencil: move_j to 100mm above, descend linearly, grab, retract +PENCIL_ABOVE = [-90, -81.6, 161.8, 0, -69.4, 180] +rbt.move_j(angles=PENCIL_ABOVE, speed=0.8) +rbt.move_l([0, 0, -93, 0, 0, 0], rel=True, speed=0.4) +rbt.tool.close(wait=True) +rbt.move_l([0, 0, 93, 0, 0, 0], rel=True, speed=0.4) +rbt.move_j(pose=PRECISION_POSE, speed=0.8) + +# Offset TCP to pencil tip (~100mm exposed below gripper). The pencil is +# clamped perpendicular to the gripper's jaw-closing direction, hanging +# along tool -X — that's the axis the offset goes on, not Z. +rbt.set_tcp_offset(-100, 0, 0) + +# Pencil tip traces straight lines (linear precision demo) +rbt.move_l([0, 0, 100, 0, 0, 0], speed=0.8, frame="TRF", rel=True) +rbt.move_l([0, 0, -200, 0, 0, 0], speed=0.8, frame="TRF", rel=True) +rbt.move_l([0, 0, 100, 0, 0, 0], speed=0.8, frame="TRF", rel=True) + +# Precision TRF rotations — pencil tip stays stationary while wrist rotates. +# 40° is the largest sweep that keeps every axis IK-reachable from this pose +# with the 100mm pencil offset. +SWEEP = 40 +for axis in range(3): + delta = [0, 0, 0, 0, 0, 0] + delta[3 + axis] = -SWEEP + rbt.move_l(delta, speed=0.8, frame="TRF", rel=True) + delta[3 + axis] = SWEEP + rbt.move_l(delta, speed=0.8, frame="TRF", rel=True) + rbt.move_l(delta, speed=0.8, frame="TRF", rel=True) + delta[3 + axis] = -SWEEP + rbt.move_l(delta, speed=0.8, frame="TRF", rel=True) + +# Place pencil back: descend linearly, release, retract +rbt.set_tcp_offset(0, 0, 0) +rbt.move_j(angles=PENCIL_ABOVE, speed=0.8) +rbt.move_l([0, 0, -93, 0, 0, 0], rel=True, speed=0.4) +rbt.tool.open(wait=True) +rbt.move_l([0, 0, 93, 0, 0, 0], rel=True, speed=0.4) + +# Return to home position (joint move, not the full homing sequence) +rbt.move_j(pose=PRECISION_POSE, speed=0.8) +rbt.move_j(angles=HOME_ANGLES, speed=0.8) +print("Done!") From a3a2cd2aa191951e1659f6b37383f580adc4bc29 Mon Sep 17 00:00:00 2001 From: jepson2k <55201008+Jepson2k@users.noreply.github.com> Date: Sun, 6 Sep 2026 14:51:39 -0400 Subject: [PATCH 4/4] precision: take the version Waldo Commander runs, verbatim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same treatment as demo_showcase, and the same divergence: b1fe085 moved the pencil offset to tool Z in the example while the program actually run and watched in Waldo Commander kept it on tool X and sweeps 40 degrees. Copied across as-is so the script that ships as the example is the script that gets demonstrated. Verified standalone — `pytest tests/test_examples.py::test_example_runs[precision.py] --examples` drives it as a subprocess against the simulator. Co-Authored-By: Claude Opus 5 --- examples/precision.py | 120 ++++++++++++++++++++---------------------- 1 file changed, 57 insertions(+), 63 deletions(-) diff --git a/examples/precision.py b/examples/precision.py index 5370870..36c493d 100644 --- a/examples/precision.py +++ b/examples/precision.py @@ -3,80 +3,74 @@ Picks up a pencil, offsets the TCP to the pencil tip, then demonstrates linear and rotational moves in the Tool Reference Frame (TRF). The pencil tip stays at a fixed point while the wrist rotates around it. - -Run: - python examples/precision.py """ -from parol6 import Robot +from parol6 import RobotClient -HOST = "127.0.0.1" -PORT = 5001 +rbt = RobotClient() HOME_ANGLES = [90.0, -90.0, 180.0, 0.0, 0.0, 180.0] +HOME_TOLERANCE_DEG = 2.0 -with Robot(host=HOST, port=PORT, normalize_logs=True) as robot: - rbt = robot.create_sync_client(timeout=2.0) - rbt.wait_ready(timeout=5.0) - rbt.simulator(True) - - rbt.select_tool("SSG-48") - rbt.tool.calibrate() - # Home unconditionally. Being NEAR the home angles is not the same as - # being referenced: a freshly started robot reports positions it has not - # homed to, so a proximity check skips the homing the moves below require - # and they are refused with "Robot not homed". - rbt.home(wait=True) +# Select tool, and home only if not already near the home pose +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() - PRECISION_POSE = [0, -250, 350, -90, 0, -90] - rbt.move_j(pose=PRECISION_POSE, speed=0.5, wait=True) +PRECISION_POSE = [0, -250, 350, -90, 0, -90] +rbt.move_j(pose=PRECISION_POSE, speed=0.5) - # Test gripper: two quick close/open cycles - rbt.tool.close(speed=1.0) - rbt.tool.open(speed=1.0) - rbt.tool.close(speed=1.0) - rbt.tool.open(speed=1.0) +# Test gripper: two quick close/open cycles +rbt.tool.close(speed=1.0) +rbt.tool.open(speed=1.0) +rbt.tool.close(speed=1.0) +rbt.tool.open(speed=1.0) - # Approach pencil: move_j to 100mm above, descend linearly, grab, retract - PENCIL_ABOVE = [-90, -81.6, 161.8, 0, -69.4, 180] - rbt.move_j(angles=PENCIL_ABOVE, speed=0.8, wait=True) - rbt.move_l([0, 0, -93, 0, 0, 0], rel=True, speed=0.4, wait=True) - rbt.tool.close(wait=True) - rbt.move_l([0, 0, 93, 0, 0, 0], rel=True, speed=0.4, wait=True) - rbt.move_j(pose=PRECISION_POSE, speed=0.8, wait=True) +# Approach pencil: move_j to 100mm above, descend linearly, grab, retract +PENCIL_ABOVE = [-90, -81.6, 161.8, 0, -69.4, 180] +rbt.move_j(angles=PENCIL_ABOVE, speed=0.8) +rbt.move_l([0, 0, -93, 0, 0, 0], rel=True, speed=0.4) +rbt.tool.close(wait=True) +rbt.move_l([0, 0, 93, 0, 0, 0], rel=True, speed=0.4) +rbt.move_j(pose=PRECISION_POSE, speed=0.8) - # Offset TCP to pencil tip (~100mm exposed below gripper). The pencil is - # clamped perpendicular to the gripper's jaw-closing direction, hanging - # along tool -X — that's the axis the offset goes on, not Z. - rbt.set_tcp_offset(-100, 0, 0) +# Offset TCP to pencil tip (~100mm exposed below gripper). The pencil is +# clamped perpendicular to the gripper's jaw-closing direction, hanging +# along tool -X — that's the axis the offset goes on, not Z. +rbt.set_tcp_offset(-100, 0, 0) - # Pencil tip traces straight lines (linear precision demo) - rbt.move_l([0, 0, 100, 0, 0, 0], speed=0.8, frame="TRF", rel=True, wait=True) - rbt.move_l([0, 0, -200, 0, 0, 0], speed=0.8, frame="TRF", rel=True, wait=True) - rbt.move_l([0, 0, 100, 0, 0, 0], speed=0.8, frame="TRF", rel=True, wait=True) +# Pencil tip traces straight lines (linear precision demo) +rbt.move_l([0, 0, 100, 0, 0, 0], speed=0.8, frame="TRF", rel=True) +rbt.move_l([0, 0, -200, 0, 0, 0], speed=0.8, frame="TRF", rel=True) +rbt.move_l([0, 0, 100, 0, 0, 0], speed=0.8, frame="TRF", rel=True) - # Precision TRF rotations — pencil tip stays stationary while wrist rotates. - # 40° is the largest sweep that keeps every axis IK-reachable from this pose - # with the 100mm pencil offset. - SWEEP = 40 - for axis in range(3): - delta = [0, 0, 0, 0, 0, 0] - delta[3 + axis] = -SWEEP - rbt.move_l(delta, speed=0.8, frame="TRF", rel=True, wait=True) - delta[3 + axis] = SWEEP - rbt.move_l(delta, speed=0.8, frame="TRF", rel=True, wait=True) - rbt.move_l(delta, speed=0.8, frame="TRF", rel=True, wait=True) - delta[3 + axis] = -SWEEP - rbt.move_l(delta, speed=0.8, frame="TRF", rel=True, wait=True) +# Precision TRF rotations — pencil tip stays stationary while wrist rotates. +# 40° is the largest sweep that keeps every axis IK-reachable from this pose +# with the 100mm pencil offset. +SWEEP = 40 +for axis in range(3): + delta = [0, 0, 0, 0, 0, 0] + delta[3 + axis] = -SWEEP + rbt.move_l(delta, speed=0.8, frame="TRF", rel=True) + delta[3 + axis] = SWEEP + rbt.move_l(delta, speed=0.8, frame="TRF", rel=True) + rbt.move_l(delta, speed=0.8, frame="TRF", rel=True) + delta[3 + axis] = -SWEEP + rbt.move_l(delta, speed=0.8, frame="TRF", rel=True) - # Place pencil back: descend linearly, release, retract - rbt.set_tcp_offset(0, 0, 0) - rbt.move_j(angles=PENCIL_ABOVE, speed=0.8, wait=True) - rbt.move_l([0, 0, -93, 0, 0, 0], rel=True, speed=0.4, wait=True) - rbt.tool.open(wait=True) - rbt.move_l([0, 0, 93, 0, 0, 0], rel=True, speed=0.4, wait=True) +# Place pencil back: descend linearly, release, retract +rbt.set_tcp_offset(0, 0, 0) +rbt.move_j(angles=PENCIL_ABOVE, speed=0.8) +rbt.move_l([0, 0, -93, 0, 0, 0], rel=True, speed=0.4) +rbt.tool.open(wait=True) +rbt.move_l([0, 0, 93, 0, 0, 0], rel=True, speed=0.4) - # Return to home position (joint move, not the full homing sequence) - rbt.move_j(pose=PRECISION_POSE, speed=0.8, wait=True) - rbt.move_j(angles=HOME_ANGLES, speed=0.8, wait=True) - print("Done!") +# Return to home position (joint move, not the full homing sequence) +rbt.move_j(pose=PRECISION_POSE, speed=0.8) +rbt.move_j(angles=HOME_ANGLES, speed=0.8) +print("Done!")