fix(experement): harden tool transfers and tightening at wrist limits

Exit the rack with a graded +Y move after taking or docking a tool, continue through home after a dock, and fall back from work to home when a joint plan is rejected. Wait for fresh zero-velocity joint states before planning and after joint motion, restore a screw to the planning scene before a pick retry, and twist only the flange joint while simulating tightening so the arm does not leave a reachable IK branch.
This commit is contained in:
Даниил Грабарь
2026-11-09 01:29:36 +10:00
parent a6c6474db9
commit 41144ecd2a
6 changed files with 215 additions and 70 deletions
+28 -11
View File
@@ -1,4 +1,5 @@
"""Operation sequences are visible, ordered and fail closed."""
import math
from pathlib import Path
import pytest
@@ -75,6 +76,9 @@ class FakeMotion:
self.moves = []
self.named = []
self.fail_labels = set()
self.joint_moves = []
self.joint_values = (0.0,) * 7
self.fail_positive_joint = False
def move_pose(self, goal, planner='ptp', speed=0.2, label=''):
self.moves.append((label, planner, speed, goal))
@@ -86,9 +90,22 @@ class FakeMotion:
self.named.append((name, speed))
return MotionResult(True, 'ok', f'move_named({name})')
def joint_positions(self):
return self.joint_values
def move_joints(self, joints, speed=0.2, label=''):
self.joint_moves.append((label, tuple(joints)))
if self.fail_positive_joint and joints[6] > 0:
return MotionResult(False, 'injected joint failure', label)
self.joint_values = tuple(joints)
return MotionResult(True, 'ok', label)
def reset_stop(self):
return None
def wait_stationary(self, *args, **kwargs):
return True
def make_operations(experiment_id):
spec = load_scenario(experiment_id, SCENES)
@@ -122,6 +139,7 @@ def test_take_tool_follows_the_visible_axial_route():
'take_tool(vacuum):approach',
'take_tool(vacuum):contact',
'take_tool(vacuum):lift',
'take_tool(vacuum):exit',
]
assert supervisor.commands[0][:2] == ('attach_tool', 'vacuum')
assert bridge.attached == []
@@ -138,7 +156,7 @@ def test_dock_tool_returns_the_tool_to_its_socket():
assert result.success
assert supervisor.commands[0][:2] == ('dock_tool', 'vacuum')
assert bridge.added_world[0][0] == 'grant_vacuum'
assert labels(motion)[-1] == 'dock_tool(vacuum):clear'
assert labels(motion)[-1] == 'dock_tool(vacuum):exit'
def test_cap_pick_and_place_use_the_hatch_node_transform():
@@ -213,11 +231,11 @@ def test_tighten_hole_is_a_visible_revolution_without_a_screw():
'tighten_hole(hole_1):approach',
'tighten_hole(hole_1):contact',
]
twist = motion.moves[names.index('tighten_hole(hole_1):twist:positive')][3]
contact = motion.moves[names.index('tighten_hole(hole_1):contact')][3]
assert twist.xyz == pytest.approx(contact.xyz)
relative = contact.inverse().compose_relaxed(twist)
assert abs(relative.rotation[2][2] - 1.0) < 1e-9
assert motion.joint_moves
label, joints = motion.joint_moves[0]
assert label == 'tighten_hole(hole_1):joint_twist'
assert joints[:6] == (0.0,) * 6
assert joints[6] == pytest.approx(math.radians(45.0))
# The operation is a simulation only: no supervisor command changes state.
assert supervisor.commands == []
assert bridge.attached == []
@@ -232,13 +250,12 @@ def test_screw_pick_reconfigures_to_home_when_work_is_blocked():
assert supervisor.commands[0][:2] == ('grab_screw', 'screw_1')
def test_tighten_hole_reverses_the_twist_when_one_direction_is_blocked():
def test_tighten_hole_reverses_when_the_flange_limit_blocks_one_direction():
operations, supervisor, bridge, motion, geometry = make_operations(14)
motion.fail_labels.add('tighten_hole(hole_1):twist:positive')
motion.fail_positive_joint = True
assert operations.tighten_hole('hole_1').success
names = labels(motion)
assert 'tighten_hole(hole_1):twist:positive' in names
assert 'tighten_hole(hole_1):twist:negative' in names
assert len(motion.joint_moves) == 2
assert motion.joint_moves[1][1][6] == pytest.approx(math.radians(-45.0))
def test_failed_motion_stops_the_operation_and_emits_failure():