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..31f5ce9 100644 --- a/examples/demo_showcase.py +++ b/examples/demo_showcase.py @@ -2,157 +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 - SWEEP = 20 - 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!") 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..36c493d 100644 --- a/examples/precision.py +++ b/examples/precision.py @@ -3,83 +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) - - # 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) +# 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!") 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()) 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 8a821f7..bcab3d3 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 e602165..879078b 100644 --- a/tests/integration/test_shapes_e2e.py +++ b/tests/integration/test_shapes_e2e.py @@ -19,7 +19,7 @@ from parol6 import MotionError, RobotClient from tests.conftest import free_udp_port -from waldoctl import Box +from waldoctl import Box, Physical pytestmark = pytest.mark.integration @@ -76,6 +76,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. # A port the kernel just handed out and nothing bound: arithmetic on # the live port runs past 65535 whenever the ephemeral range hands