griip-sdk-dev 0.4.0.dev24238__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- griip_sdk/__init__.py +77 -0
- griip_sdk/calibration/__init__.py +0 -0
- griip_sdk/calibration/app.py +638 -0
- griip_sdk/calibration/apriltag_pose.py +101 -0
- griip_sdk/calibration/camera_factory.py +105 -0
- griip_sdk/calibration/cartesian_pose.py +220 -0
- griip_sdk/calibration/config_store.py +131 -0
- griip_sdk/calibration/hand_eye_calib/README.md +109 -0
- griip_sdk/calibration/hand_eye_calib/__init__.py +40 -0
- griip_sdk/calibration/hand_eye_calib/config/routine.yaml +7 -0
- griip_sdk/calibration/hand_eye_calib/core.py +692 -0
- griip_sdk/calibration/hand_eye_calib/main.py +596 -0
- griip_sdk/calibration/hand_eye_calib/planning.py +122 -0
- griip_sdk/calibration/hand_eye_calib/routines.py +223 -0
- griip_sdk/calibration/hand_eye_calib/run_hand_eye_calibration.py +131 -0
- griip_sdk/calibration/hand_eye_calib/transforms.py +57 -0
- griip_sdk/calibration/mqtt_publisher.py +98 -0
- griip_sdk/calibration/mqtt_topics.py +15 -0
- griip_sdk/calibration/routines.py +36 -0
- griip_sdk/calibration/scanning/__init__.py +11 -0
- griip_sdk/calibration/scanning/environment_scanning.py +381 -0
- griip_sdk/calibration/scanning/run_environment_scanning.py +142 -0
- griip_sdk/calibration/tests/__init__.py +0 -0
- griip_sdk/calibration/tests/conftest.py +33 -0
- griip_sdk/calibration/tests/test_calibration_app.py +822 -0
- griip_sdk/calibration/tests/test_camera_factory.py +126 -0
- griip_sdk/calibration/tests/test_environment_scanning.py +547 -0
- griip_sdk/calibration/tests/test_hand_eye_calibration_service.py +524 -0
- griip_sdk/calibration/tests/test_hand_eye_helpers.py +211 -0
- griip_sdk/calibration/tests/test_transforms.py +126 -0
- griip_sdk/calibration/types.py +21 -0
- griip_sdk/calibration/units.py +104 -0
- griip_sdk/cell/__init__.py +22 -0
- griip_sdk/cell/builders.py +631 -0
- griip_sdk/cell/config.py +505 -0
- griip_sdk/cell/end_effector_collision_model.py +160 -0
- griip_sdk/cell/initialize.py +282 -0
- griip_sdk/cell/loader.py +102 -0
- griip_sdk/cell/mock_builders.py +114 -0
- griip_sdk/cell/parts.py +63 -0
- griip_sdk/cell/recipes.py +872 -0
- griip_sdk/cell/tests/__init__.py +0 -0
- griip_sdk/cell/tests/conftest.py +7 -0
- griip_sdk/cell/tests/test_builders.py +13 -0
- griip_sdk/cell/tests/test_end_effector_collision_model.py +118 -0
- griip_sdk/cell/tests/test_initialize.py +458 -0
- griip_sdk/cell/tests/test_loader.py +70 -0
- griip_sdk/cell/tests/test_mock_cell_smoke.py +54 -0
- griip_sdk/cell/tests/test_parts.py +55 -0
- griip_sdk/cell/tests/test_recipes.py +156 -0
- griip_sdk/cell/tests/test_recording_config.py +113 -0
- griip_sdk/errors.py +16 -0
- griip_sdk/grpc_adapters/__init__.py +0 -0
- griip_sdk/grpc_adapters/servicer_as_stub.py +166 -0
- griip_sdk/hardware/__init__.py +0 -0
- griip_sdk/hardware/bin_shaker.py +86 -0
- griip_sdk/hardware/camera.py +300 -0
- griip_sdk/hardware/fanuc_robot_client.py +266 -0
- griip_sdk/hardware/ft_sensor.py +352 -0
- griip_sdk/hardware/gripper.py +339 -0
- griip_sdk/hardware/grpc_vision_backend.py +123 -0
- griip_sdk/hardware/io_client.py +194 -0
- griip_sdk/hardware/mmai_vision_backend.py +265 -0
- griip_sdk/hardware/mocks/__init__.py +0 -0
- griip_sdk/hardware/mocks/camera_mock.py +62 -0
- griip_sdk/hardware/mocks/gripper_client_mock.py +135 -0
- griip_sdk/hardware/mocks/io_client_mock.py +28 -0
- griip_sdk/hardware/mocks/robot_client_mock.py +584 -0
- griip_sdk/hardware/onrobot_3fg15_gripper.py +103 -0
- griip_sdk/hardware/robot_client.py +1692 -0
- griip_sdk/hardware/robotiq_2f140_gripper.py +327 -0
- griip_sdk/hardware/sim_gripper.py +365 -0
- griip_sdk/hardware/sim_hand_eye.py +138 -0
- griip_sdk/hardware/sim_robot_client.py +189 -0
- griip_sdk/hardware/suction_gripper.py +112 -0
- griip_sdk/hardware/tests/__init__.py +0 -0
- griip_sdk/hardware/tests/conftest.py +8 -0
- griip_sdk/hardware/tests/test_call_with_timeout.py +56 -0
- griip_sdk/hardware/tests/test_command_state_fanout.py +131 -0
- griip_sdk/hardware/tests/test_gripper_command_listeners.py +90 -0
- griip_sdk/hardware/tests/test_grpc_vision_backend.py +220 -0
- griip_sdk/hardware/tests/test_mmai_vision_backend.py +312 -0
- griip_sdk/hardware/tests/test_reset_to_normal.py +101 -0
- griip_sdk/hardware/tests/test_robot_client_base.py +48 -0
- griip_sdk/hardware/tests/test_robot_client_lifecycle.py +323 -0
- griip_sdk/hardware/tests/test_robot_feedback_fanout.py +154 -0
- griip_sdk/hardware/tests/test_robotiq_2f140_gripper.py +86 -0
- griip_sdk/hardware/tests/test_robotiq_gripper_bus_lock.py +78 -0
- griip_sdk/hardware/tests/test_sim_gripper.py +230 -0
- griip_sdk/hardware/tests/test_sim_hand_eye.py +226 -0
- griip_sdk/hardware/tests/test_sim_robot_client.py +196 -0
- griip_sdk/hardware/tests/test_suction_gripper.py +82 -0
- griip_sdk/hardware/tests/test_tower_light.py +179 -0
- griip_sdk/hardware/tests/test_vision_codec.py +73 -0
- griip_sdk/hardware/tests/test_vrmcs_tcp_guard.py +81 -0
- griip_sdk/hardware/torquing_client.py +23 -0
- griip_sdk/hardware/tower_light.py +84 -0
- griip_sdk/hardware/ur_robot_client.py +88 -0
- griip_sdk/hardware/vision_codec.py +77 -0
- griip_sdk/log_utils.py +42 -0
- griip_sdk/modeling/__init__.py +0 -0
- griip_sdk/modeling/sphere_generation_tool/README.md +187 -0
- griip_sdk/modeling/sphere_generation_tool/__init__.py +8 -0
- griip_sdk/modeling/sphere_generation_tool/__main__.py +242 -0
- griip_sdk/modeling/sphere_generation_tool/_actors.py +278 -0
- griip_sdk/modeling/sphere_generation_tool/_geometry.py +55 -0
- griip_sdk/modeling/sphere_generation_tool/editor.py +1142 -0
- griip_sdk/modeling/sphere_generation_tool/object_io.py +53 -0
- griip_sdk/modeling/sphere_generation_tool/urdf_io.py +315 -0
- griip_sdk/modeling/tests/__init__.py +0 -0
- griip_sdk/modeling/tests/conftest.py +123 -0
- griip_sdk/modeling/tests/test_editor_math.py +80 -0
- griip_sdk/modeling/tests/test_editor_smoke.py +471 -0
- griip_sdk/modeling/tests/test_mesh_loading.py +66 -0
- griip_sdk/modeling/tests/test_object_io.py +72 -0
- griip_sdk/modeling/tests/test_urdf_io.py +367 -0
- griip_sdk/orchestration/__init__.py +129 -0
- griip_sdk/orchestration/bin_cuboid_extraction.py +77 -0
- griip_sdk/orchestration/bin_picking_manager.py +580 -0
- griip_sdk/orchestration/bin_state.py +85 -0
- griip_sdk/orchestration/callbacks.py +60 -0
- griip_sdk/orchestration/cycle_pause_gate.py +92 -0
- griip_sdk/orchestration/errors.py +27 -0
- griip_sdk/orchestration/execution/__init__.py +18 -0
- griip_sdk/orchestration/execution/grasp_executor.py +105 -0
- griip_sdk/orchestration/execution/parallel_jaw_executor.py +132 -0
- griip_sdk/orchestration/execution/suction_executor.py +53 -0
- griip_sdk/orchestration/execution/tests/__init__.py +0 -0
- griip_sdk/orchestration/execution/tests/test_parallel_jaw_executor.py +164 -0
- griip_sdk/orchestration/execution/three_finger_executor.py +144 -0
- griip_sdk/orchestration/grasp_provider.py +258 -0
- griip_sdk/orchestration/grasp_queue.py +107 -0
- griip_sdk/orchestration/part_setup.py +52 -0
- griip_sdk/orchestration/perception_inputs.py +187 -0
- griip_sdk/orchestration/pick_and_place_manager.py +1514 -0
- griip_sdk/orchestration/pick_generator.py +230 -0
- griip_sdk/orchestration/placement_planner.py +365 -0
- griip_sdk/orchestration/planning.py +59 -0
- griip_sdk/orchestration/proto_conversions.py +113 -0
- griip_sdk/orchestration/roi_scheduler.py +150 -0
- griip_sdk/orchestration/scoop_executor.py +122 -0
- griip_sdk/orchestration/tests/__init__.py +0 -0
- griip_sdk/orchestration/tests/conftest.py +156 -0
- griip_sdk/orchestration/tests/test_bin_picking_manager.py +485 -0
- griip_sdk/orchestration/tests/test_bin_state.py +160 -0
- griip_sdk/orchestration/tests/test_cycle_pause_gate.py +130 -0
- griip_sdk/orchestration/tests/test_grasp_provider.py +307 -0
- griip_sdk/orchestration/tests/test_part_setup.py +69 -0
- griip_sdk/orchestration/tests/test_perception_inputs.py +122 -0
- griip_sdk/orchestration/tests/test_phase_collision_spheres.py +57 -0
- griip_sdk/orchestration/tests/test_pick_and_place_manager.py +760 -0
- griip_sdk/orchestration/tests/test_pick_generator.py +130 -0
- griip_sdk/orchestration/tests/test_placement_planner.py +333 -0
- griip_sdk/orchestration/tests/test_planning.py +64 -0
- griip_sdk/orchestration/tests/test_proto_conversions.py +96 -0
- griip_sdk/orchestration/tests/test_scoop_executor.py +106 -0
- griip_sdk/orchestration/trajectory_store.py +170 -0
- griip_sdk/orchestration/types.py +466 -0
- griip_sdk/prototyping/__init__.py +0 -0
- griip_sdk/prototyping/config/__init__.py +20 -0
- griip_sdk/prototyping/config/registry.py +22 -0
- griip_sdk/prototyping/in_process_api.py +59 -0
- griip_sdk/prototyping/read_tcp.py +67 -0
- griip_sdk/prototyping/run_environment_scanning.py +99 -0
- griip_sdk/prototyping/run_hand_eye_calibration.py +104 -0
- griip_sdk/prototyping/run_linear_shuttle.py +556 -0
- griip_sdk/recording/__init__.py +71 -0
- griip_sdk/recording/foxglove_events.py +553 -0
- griip_sdk/recording/foxglove_joint_states.py +115 -0
- griip_sdk/recording/gripper_state_logger.py +203 -0
- griip_sdk/recording/gripper_state_mcap_recording.py +69 -0
- griip_sdk/recording/h264_video_logger.py +128 -0
- griip_sdk/recording/h264_video_mcap_recording.py +217 -0
- griip_sdk/recording/kpi_logger.py +186 -0
- griip_sdk/recording/kpi_mcap_recording.py +143 -0
- griip_sdk/recording/kpi_timer.py +148 -0
- griip_sdk/recording/mcap_recording.py +209 -0
- griip_sdk/recording/memory_utils.py +19 -0
- griip_sdk/recording/robot_state_logger.py +133 -0
- griip_sdk/recording/rotating_mcap_writer.py +326 -0
- griip_sdk/recording/tests/test_foxglove_event_publisher.py +324 -0
- griip_sdk/recording/tests/test_foxglove_joint_states.py +26 -0
- griip_sdk/recording/tests/test_kpi_timer.py +346 -0
- griip_sdk/recording/tests/test_mcap_foxglove_event_publisher.py +355 -0
- griip_sdk/recording/tests/test_mcap_gripper_state_logger.py +334 -0
- griip_sdk/recording/tests/test_mcap_h264_video_logger.py +118 -0
- griip_sdk/recording/tests/test_mcap_kpi_logger.py +72 -0
- griip_sdk/recording/tests/test_mcap_robot_state_logger.py +253 -0
- griip_sdk/recording/tests/test_session.py +87 -0
- griip_sdk/tests/__init__.py +0 -0
- griip_sdk/tests/test_log_utils.py +81 -0
- griip_sdk/tests/test_servicer_as_stub.py +69 -0
- griip_sdk_dev-0.4.0.dev24238.dist-info/METADATA +513 -0
- griip_sdk_dev-0.4.0.dev24238.dist-info/RECORD +195 -0
- griip_sdk_dev-0.4.0.dev24238.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Three-finger grasp executor (stub).
|
|
2
|
+
|
|
3
|
+
A single executor class handles both external and internal grasp modes,
|
|
4
|
+
selected by the ``mode`` field on the parameters dataclass. External and
|
|
5
|
+
internal are exact inverses for a three-finger gripper: external opens
|
|
6
|
+
to engage, internal closes to engage.
|
|
7
|
+
|
|
8
|
+
End-to-end support is out of scope for the initial refactor; the stub
|
|
9
|
+
exists so the proto ``oneof`` is exercised and the architecture is
|
|
10
|
+
provably general.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
from griip_sdk.hardware.gripper import IO3FingerGripper, ThreeFingerGripper
|
|
19
|
+
from griip_sdk.orchestration.execution.grasp_executor import GraspExecutor, GraspMode
|
|
20
|
+
|
|
21
|
+
PREP_FOR_APPROACH_NARROW_IO_STATE = 0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class ThreeFingerExecutorParameters:
|
|
26
|
+
"""SDK-side per-grasp execution parameters for the three-finger executor."""
|
|
27
|
+
|
|
28
|
+
mode: GraspMode
|
|
29
|
+
grip_force: int
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class IOThreeFingerParameters:
|
|
34
|
+
"""IO based three-finger grasp execution parameters (stub)."""
|
|
35
|
+
|
|
36
|
+
mode: GraspMode
|
|
37
|
+
prep_io_state: int
|
|
38
|
+
engaged_io_state: int
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ThreeFingerExecutor(GraspExecutor):
|
|
42
|
+
"""Three-finger grasp executor.
|
|
43
|
+
|
|
44
|
+
External: open fingers around object, then close to engage.
|
|
45
|
+
Internal: close fingers inside cavity, then open to engage.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, gripper: ThreeFingerGripper, parameters: ThreeFingerExecutorParameters) -> None:
|
|
49
|
+
super().__init__(gripper)
|
|
50
|
+
self._params = parameters
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def gripper(self) -> ThreeFingerGripper:
|
|
54
|
+
return self._gripper # type: ignore[return-value]
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def parameters(self) -> ThreeFingerExecutorParameters:
|
|
58
|
+
return self._params
|
|
59
|
+
|
|
60
|
+
def prepare_to_grasp(self, block: bool = True) -> None:
|
|
61
|
+
if self._params.mode is GraspMode.EXTERNAL:
|
|
62
|
+
self.gripper.open(block=block)
|
|
63
|
+
else:
|
|
64
|
+
self.gripper.close(force_value=self._params.grip_force, block=block)
|
|
65
|
+
|
|
66
|
+
def grasp_object(self, block: bool = True, timeout: Optional[float] = None) -> None:
|
|
67
|
+
if self._params.mode is GraspMode.EXTERNAL:
|
|
68
|
+
self.gripper.close(force_value=self._params.grip_force, block=block)
|
|
69
|
+
else:
|
|
70
|
+
self.gripper.open(block=block)
|
|
71
|
+
|
|
72
|
+
def release_object(self, block: bool = True) -> None:
|
|
73
|
+
if self._params.mode is GraspMode.EXTERNAL:
|
|
74
|
+
self.gripper.open(block=block)
|
|
75
|
+
else:
|
|
76
|
+
self.gripper.close(force_value=self._params.grip_force, block=block)
|
|
77
|
+
|
|
78
|
+
def is_object_engaged(self) -> bool:
|
|
79
|
+
raise NotImplementedError("Not implemented, will be implemented in the next pr")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class IOThreeFingerExecutor(GraspExecutor):
|
|
83
|
+
"""IO-based three-finger grasp executor.
|
|
84
|
+
|
|
85
|
+
External: open fingers around object, then close to engage.
|
|
86
|
+
Internal: close fingers inside cavity, then open to engage.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(self, gripper: IO3FingerGripper, parameters: IOThreeFingerParameters) -> None:
|
|
90
|
+
super().__init__(gripper)
|
|
91
|
+
self._params = parameters
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def gripper(self) -> IO3FingerGripper:
|
|
95
|
+
return self._gripper # type: ignore[return-value]
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def parameters(self) -> IOThreeFingerParameters:
|
|
99
|
+
return self._params
|
|
100
|
+
|
|
101
|
+
def prepare_to_approach(self, block: bool = False) -> None:
|
|
102
|
+
"""Pre-position the fingers to a safe narrow width while the arm travels to prep."""
|
|
103
|
+
narrow_target = min(PREP_FOR_APPROACH_NARROW_IO_STATE, max(self._params.prep_io_state, 0))
|
|
104
|
+
self.gripper.move_gripper(io_state=narrow_target, block=block)
|
|
105
|
+
|
|
106
|
+
def prepare_for_plunge(self, block: bool = True) -> None:
|
|
107
|
+
"""Close the fingers fully so the gripper can plunge through a narrow aperture."""
|
|
108
|
+
self.gripper.close(block=block)
|
|
109
|
+
|
|
110
|
+
def prepare_to_grasp(self, block: bool = True) -> None:
|
|
111
|
+
self.gripper.move_gripper(io_state=self._params.prep_io_state, block=block)
|
|
112
|
+
|
|
113
|
+
def grasp_object(self, block: bool = True, timeout: Optional[float] = None) -> None:
|
|
114
|
+
self.gripper.move_gripper(io_state=self._params.engaged_io_state, block=block)
|
|
115
|
+
|
|
116
|
+
def release_object(self, block: bool = True) -> None:
|
|
117
|
+
if self._params.mode is GraspMode.EXTERNAL:
|
|
118
|
+
self.gripper.open_gripper(block=block)
|
|
119
|
+
else:
|
|
120
|
+
self.gripper.close_gripper(block=block)
|
|
121
|
+
|
|
122
|
+
def is_object_engaged(self) -> bool:
|
|
123
|
+
if self._params.mode is GraspMode.EXTERNAL:
|
|
124
|
+
return self._external_is_object_engaged()
|
|
125
|
+
else:
|
|
126
|
+
return self._internal_is_object_engaged()
|
|
127
|
+
|
|
128
|
+
def return_to_idle(self, block: bool = True) -> None:
|
|
129
|
+
self.gripper.release(block=block)
|
|
130
|
+
self.gripper.close(block=block)
|
|
131
|
+
|
|
132
|
+
def _external_is_object_engaged(self) -> bool:
|
|
133
|
+
raise NotImplementedError("Not implemented")
|
|
134
|
+
|
|
135
|
+
def _internal_is_object_engaged(self) -> bool:
|
|
136
|
+
io_sensor_state = self.gripper.get_io_sensor()
|
|
137
|
+
if io_sensor_state == 1:
|
|
138
|
+
return True
|
|
139
|
+
|
|
140
|
+
self.gripper.move_gripper(io_state=1, block=True, timeout=0.1)
|
|
141
|
+
io_sensor_state = self.gripper.get_io_sensor()
|
|
142
|
+
if io_sensor_state == 1:
|
|
143
|
+
return True
|
|
144
|
+
return False
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Grasp targets for an app that moves the robot itself.
|
|
2
|
+
|
|
3
|
+
The SDK takes and stores pictures, runs perception on a stored picture when
|
|
4
|
+
asked, and keeps one queue of ranked grasp poses. It never commands motion:
|
|
5
|
+
the app parks the robot before ``capture`` and executes the pose it pops.
|
|
6
|
+
|
|
7
|
+
One perception run at a time, on the same pipeline the pick managers use.
|
|
8
|
+
The results of a run replace the queue, so pop before starting the next run.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Any, Optional, cast
|
|
18
|
+
|
|
19
|
+
import grpc
|
|
20
|
+
from spatialmath import SE3
|
|
21
|
+
|
|
22
|
+
from griip_sdk.cell.end_effector_collision_model import EndEffectorCollisionModel
|
|
23
|
+
from griip_sdk.hardware.camera import CameraManager, GrabResult
|
|
24
|
+
from griip_sdk.hardware.robot_client import VrmcsRobotClient
|
|
25
|
+
from griip_sdk.orchestration.perception_inputs import build_generate_picks_request
|
|
26
|
+
from griip_sdk.orchestration.proto_conversions import bin_wall_to_proto, bin_walls_to_proto
|
|
27
|
+
from griip_sdk.orchestration.types import CaptureInfo, GraspTarget
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
_RUN_JOIN_TIMEOUT_SECONDS = 5.0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class _StoredCapture:
|
|
36
|
+
tag: str
|
|
37
|
+
bin_number: Optional[int]
|
|
38
|
+
grab_result: GrabResult
|
|
39
|
+
tcp_pose: SE3
|
|
40
|
+
joint_angles: list[float]
|
|
41
|
+
state: str = "captured"
|
|
42
|
+
detail: Optional[str] = None
|
|
43
|
+
targets: list[GraspTarget] = field(default_factory=list)
|
|
44
|
+
|
|
45
|
+
def info(self) -> CaptureInfo:
|
|
46
|
+
return CaptureInfo(
|
|
47
|
+
tag=self.tag,
|
|
48
|
+
bin_number=self.bin_number,
|
|
49
|
+
image_id=self.grab_result.image_id,
|
|
50
|
+
capture_timestamp_ns=self.grab_result.timestamp_nanoseconds,
|
|
51
|
+
state=self.state,
|
|
52
|
+
detail=self.detail,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class GraspProvider:
|
|
57
|
+
"""Store pictures, process them on demand, pop grasp poses from one queue."""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
*,
|
|
62
|
+
picking_stub: object,
|
|
63
|
+
camera_manager: CameraManager,
|
|
64
|
+
robot_client: VrmcsRobotClient,
|
|
65
|
+
tcp_to_camera: SE3,
|
|
66
|
+
tcp_to_flange: SE3,
|
|
67
|
+
camera_intrinsics_flat: list[float],
|
|
68
|
+
roi: list[int],
|
|
69
|
+
max_detections: int,
|
|
70
|
+
max_picks_per_object: int,
|
|
71
|
+
min_depth_for_perception_mm: float,
|
|
72
|
+
end_effector_collision_model: EndEffectorCollisionModel,
|
|
73
|
+
gripper_open_width_m: Optional[float],
|
|
74
|
+
gripper_closed_width_m: float,
|
|
75
|
+
all_bin_walls: dict[int, list[dict[str, Any]]],
|
|
76
|
+
all_bin_floors: dict[int, dict[str, Any]],
|
|
77
|
+
) -> None:
|
|
78
|
+
self._picking_stub = picking_stub
|
|
79
|
+
self._camera_manager = camera_manager
|
|
80
|
+
self._robot_client = robot_client
|
|
81
|
+
self._tcp_to_camera = tcp_to_camera
|
|
82
|
+
self._tcp_to_flange = tcp_to_flange
|
|
83
|
+
self._camera_intrinsics_flat = camera_intrinsics_flat
|
|
84
|
+
self._roi = list(roi)
|
|
85
|
+
self._max_detections = max_detections
|
|
86
|
+
self._max_picks_per_object = max_picks_per_object
|
|
87
|
+
self._min_depth_for_perception_mm = min_depth_for_perception_mm
|
|
88
|
+
self._end_effector_collision_model = end_effector_collision_model
|
|
89
|
+
self._gripper_open_width_m = gripper_open_width_m
|
|
90
|
+
self._gripper_closed_width_m = gripper_closed_width_m
|
|
91
|
+
self._all_bin_walls = all_bin_walls
|
|
92
|
+
self._all_bin_floors = all_bin_floors
|
|
93
|
+
|
|
94
|
+
self._captures: dict[str, _StoredCapture] = {}
|
|
95
|
+
self._queue: list[GraspTarget] = []
|
|
96
|
+
self._runs_in_flight = 0
|
|
97
|
+
self._threads: list[threading.Thread] = []
|
|
98
|
+
self._closed = False
|
|
99
|
+
self._state = threading.Condition()
|
|
100
|
+
# One GeneratePicks stream at a time, the same constraint PickGenerator has.
|
|
101
|
+
self._stream_lock = threading.Lock()
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def bin_numbers(self) -> list[int]:
|
|
105
|
+
return sorted(self._all_bin_walls)
|
|
106
|
+
|
|
107
|
+
# ------------------------------------------------------------------
|
|
108
|
+
# Picture store
|
|
109
|
+
# ------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
def capture(self, tag: str, *, bin_number: Optional[int] = None) -> CaptureInfo:
|
|
112
|
+
"""Take a picture at the robot's current pose and store it under ``tag``.
|
|
113
|
+
|
|
114
|
+
Never moves the robot; park it first. Replaces any picture stored
|
|
115
|
+
under the same tag. Does not start processing.
|
|
116
|
+
"""
|
|
117
|
+
if bin_number is not None and bin_number not in self._all_bin_walls:
|
|
118
|
+
raise ValueError(f"bin_number {bin_number} is not in runtime.bins.available ({self.bin_numbers})")
|
|
119
|
+
joint_angles = list(self._robot_client.get_joint_angles())
|
|
120
|
+
tcp_pose = self._robot_client.get_TCP_pose()
|
|
121
|
+
grab_result = self._camera_manager.grab()
|
|
122
|
+
stored = _StoredCapture(
|
|
123
|
+
tag=tag,
|
|
124
|
+
bin_number=bin_number,
|
|
125
|
+
grab_result=grab_result,
|
|
126
|
+
tcp_pose=tcp_pose,
|
|
127
|
+
joint_angles=joint_angles,
|
|
128
|
+
)
|
|
129
|
+
with self._state:
|
|
130
|
+
self._captures[tag] = stored
|
|
131
|
+
logger.info("Stored picture %r (bin=%s, image_id=%s)", tag, bin_number, grab_result.image_id)
|
|
132
|
+
return stored.info()
|
|
133
|
+
|
|
134
|
+
def captures(self) -> list[CaptureInfo]:
|
|
135
|
+
with self._state:
|
|
136
|
+
return [stored.info() for stored in self._captures.values()]
|
|
137
|
+
|
|
138
|
+
def delete(self, tag: str) -> None:
|
|
139
|
+
with self._state:
|
|
140
|
+
self._captures.pop(tag, None)
|
|
141
|
+
|
|
142
|
+
# ------------------------------------------------------------------
|
|
143
|
+
# Processing and the queue
|
|
144
|
+
# ------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
def process(self, tag: str) -> None:
|
|
147
|
+
"""Run perception on the stored picture ``tag`` in the background.
|
|
148
|
+
|
|
149
|
+
When the run finishes its poses replace the queue. A picture replaced
|
|
150
|
+
or deleted while its run is in flight is dropped on completion.
|
|
151
|
+
"""
|
|
152
|
+
with self._state:
|
|
153
|
+
if self._closed:
|
|
154
|
+
raise RuntimeError("GraspProvider is closed")
|
|
155
|
+
stored = self._captures.get(tag)
|
|
156
|
+
if stored is None:
|
|
157
|
+
raise ValueError(f"no stored picture under tag {tag!r}")
|
|
158
|
+
stored.state = "processing"
|
|
159
|
+
stored.detail = None
|
|
160
|
+
self._runs_in_flight += 1
|
|
161
|
+
thread = threading.Thread(target=self._run, args=(stored,), name=f"grasp-provider-{tag}", daemon=True)
|
|
162
|
+
self._threads.append(thread)
|
|
163
|
+
thread.start()
|
|
164
|
+
|
|
165
|
+
def next_target(self, timeout: Optional[float] = None) -> Optional[GraspTarget]:
|
|
166
|
+
"""Wait for in-flight runs, then pop the best pose. None when nothing is left."""
|
|
167
|
+
deadline = None if timeout is None else time.monotonic() + timeout
|
|
168
|
+
with self._state:
|
|
169
|
+
while self._runs_in_flight > 0:
|
|
170
|
+
remaining = None if deadline is None else deadline - time.monotonic()
|
|
171
|
+
if remaining is not None and remaining <= 0.0:
|
|
172
|
+
return None
|
|
173
|
+
self._state.wait(remaining)
|
|
174
|
+
if not self._queue:
|
|
175
|
+
return None
|
|
176
|
+
return self._queue.pop(0)
|
|
177
|
+
|
|
178
|
+
def close(self) -> None:
|
|
179
|
+
with self._state:
|
|
180
|
+
self._closed = True
|
|
181
|
+
threads = list(self._threads)
|
|
182
|
+
for thread in threads:
|
|
183
|
+
thread.join(timeout=_RUN_JOIN_TIMEOUT_SECONDS)
|
|
184
|
+
|
|
185
|
+
# ------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
def _run(self, stored: _StoredCapture) -> None:
|
|
188
|
+
ranked: list[tuple[int, float, Any]] = []
|
|
189
|
+
detail: Optional[str] = None
|
|
190
|
+
failed = False
|
|
191
|
+
try:
|
|
192
|
+
request = build_generate_picks_request(
|
|
193
|
+
stored.grab_result,
|
|
194
|
+
stored.tcp_pose,
|
|
195
|
+
stored.joint_angles,
|
|
196
|
+
tcp_to_camera=self._tcp_to_camera,
|
|
197
|
+
camera_intrinsics_flat=self._camera_intrinsics_flat,
|
|
198
|
+
min_depth_for_perception_mm=self._min_depth_for_perception_mm,
|
|
199
|
+
max_detections=self._max_detections,
|
|
200
|
+
max_picks_per_object=self._max_picks_per_object,
|
|
201
|
+
end_effector_collision_model=self._end_effector_collision_model,
|
|
202
|
+
gripper_open_width_m=self._gripper_open_width_m,
|
|
203
|
+
gripper_closed_width_m=self._gripper_closed_width_m,
|
|
204
|
+
extra_kwargs=self._request_extras(stored.bin_number),
|
|
205
|
+
)
|
|
206
|
+
with self._stream_lock:
|
|
207
|
+
for response in cast(Any, self._picking_stub).GeneratePicks(request):
|
|
208
|
+
if response.HasField("pick_result"):
|
|
209
|
+
pick_result = response.pick_result
|
|
210
|
+
ranked.append((-int(pick_result.priority), float(pick_result.distance_to_home), pick_result))
|
|
211
|
+
if response.HasField("terminal_message"):
|
|
212
|
+
terminal = response.terminal_message
|
|
213
|
+
if terminal.no_parts_detected:
|
|
214
|
+
detail = "no parts detected"
|
|
215
|
+
elif terminal.has_only_ungraspable_objects:
|
|
216
|
+
detail = "objects detected, none graspable"
|
|
217
|
+
except grpc.RpcError as rpc_error:
|
|
218
|
+
failed = True
|
|
219
|
+
detail = f"gRPC {rpc_error.code().name}: {rpc_error.details()}"
|
|
220
|
+
logger.error("Perception failed for picture %r: %s", stored.tag, detail)
|
|
221
|
+
except Exception as run_error:
|
|
222
|
+
failed = True
|
|
223
|
+
detail = f"{type(run_error).__name__}: {run_error}"
|
|
224
|
+
logger.error("Perception failed for picture %r: %s", stored.tag, detail, exc_info=True)
|
|
225
|
+
|
|
226
|
+
ranked.sort(key=lambda entry: (entry[0], entry[1]))
|
|
227
|
+
targets = [
|
|
228
|
+
GraspTarget.from_proto(
|
|
229
|
+
pick_result,
|
|
230
|
+
capture_tag=stored.tag,
|
|
231
|
+
bin_number=stored.bin_number,
|
|
232
|
+
image_id=stored.grab_result.image_id,
|
|
233
|
+
tcp_to_flange=self._tcp_to_flange,
|
|
234
|
+
)
|
|
235
|
+
for _, _, pick_result in ranked
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
with self._state:
|
|
239
|
+
self._runs_in_flight -= 1
|
|
240
|
+
if self._captures.get(stored.tag) is stored:
|
|
241
|
+
stored.state = "failed" if failed else "done"
|
|
242
|
+
stored.detail = detail
|
|
243
|
+
stored.targets = targets
|
|
244
|
+
self._queue = list(targets)
|
|
245
|
+
logger.info("Picture %r processed: %d targets%s", stored.tag, len(targets), f" ({detail})" if detail else "")
|
|
246
|
+
else:
|
|
247
|
+
logger.info("Picture %r was replaced during processing; %d targets dropped", stored.tag, len(targets))
|
|
248
|
+
self._state.notify_all()
|
|
249
|
+
|
|
250
|
+
def _request_extras(self, bin_number: Optional[int]) -> dict[str, Any]:
|
|
251
|
+
extras: dict[str, Any] = {"roi": self._roi}
|
|
252
|
+
if bin_number is None:
|
|
253
|
+
return extras
|
|
254
|
+
extras["bin_walls"] = bin_walls_to_proto(self._all_bin_walls[bin_number])
|
|
255
|
+
bin_floor = self._all_bin_floors.get(bin_number)
|
|
256
|
+
if bin_floor is not None:
|
|
257
|
+
extras["bin_floor"] = bin_wall_to_proto(bin_floor)
|
|
258
|
+
return extras
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import threading
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from griip_sdk.orchestration.types import PickResultAdapter
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GraspQueue:
|
|
13
|
+
"""Thread-safe queue for planned grasps, gated by a generation epoch.
|
|
14
|
+
|
|
15
|
+
A grasp is only valid while the planning assumptions it was generated under
|
|
16
|
+
still hold (the captured scene and the capture pose it was planned from). The
|
|
17
|
+
epoch is a single monotonic token for those assumptions: every fresh capture
|
|
18
|
+
or invalidation advances it and clears the queue in the same locked step.
|
|
19
|
+
Producers stamp grasps with the epoch they were generated under and ``add``
|
|
20
|
+
rejects any stale epoch, so everything queued is always current.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
self._queue: list[PickResultAdapter] = []
|
|
25
|
+
self._lock = threading.Lock()
|
|
26
|
+
self._current_epoch = 0
|
|
27
|
+
self._current_image_id: Optional[str] = None
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def current_epoch(self) -> int:
|
|
31
|
+
with self._lock:
|
|
32
|
+
return self._current_epoch
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def current_image_id(self) -> Optional[str]:
|
|
36
|
+
with self._lock:
|
|
37
|
+
return self._current_image_id
|
|
38
|
+
|
|
39
|
+
def reset(self, new_image_id: str) -> int:
|
|
40
|
+
"""Open a new generation epoch for a freshly captured image.
|
|
41
|
+
|
|
42
|
+
Clears the queue and returns the epoch the producer must stamp its grasps
|
|
43
|
+
with via :meth:`add`.
|
|
44
|
+
"""
|
|
45
|
+
with self._lock:
|
|
46
|
+
self._current_epoch += 1
|
|
47
|
+
self._current_image_id = new_image_id
|
|
48
|
+
self._queue.clear()
|
|
49
|
+
logger.info(f"Queue reset. Epoch {self._current_epoch}, image ID: {new_image_id}")
|
|
50
|
+
return self._current_epoch
|
|
51
|
+
|
|
52
|
+
def invalidate(self) -> None:
|
|
53
|
+
"""Drop every queued grasp and advance the epoch.
|
|
54
|
+
|
|
55
|
+
Call whenever a queued grasp's planning assumptions change (capture pose,
|
|
56
|
+
active part, bin). In-flight grasps from the previous epoch are rejected on
|
|
57
|
+
``add``, and nothing stale survives the call.
|
|
58
|
+
"""
|
|
59
|
+
with self._lock:
|
|
60
|
+
dropped = len(self._queue)
|
|
61
|
+
self._current_epoch += 1
|
|
62
|
+
self._current_image_id = None
|
|
63
|
+
self._queue.clear()
|
|
64
|
+
logger.info(f"Queue invalidated. Epoch {self._current_epoch}, dropped {dropped} grasps.")
|
|
65
|
+
|
|
66
|
+
def add(self, grasp: PickResultAdapter, epoch: int) -> bool:
|
|
67
|
+
"""Add a grasp produced under ``epoch`` if that epoch is still current."""
|
|
68
|
+
with self._lock:
|
|
69
|
+
if epoch != self._current_epoch:
|
|
70
|
+
logger.warning(f"GRASP REJECTED (stale epoch {epoch}, current {self._current_epoch})")
|
|
71
|
+
return False
|
|
72
|
+
self._queue.append(grasp)
|
|
73
|
+
logger.info(f"GRASP ADDED. Queue size: {len(self._queue)}, epoch: {epoch}")
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
def get_next(self) -> Optional[PickResultAdapter]:
|
|
77
|
+
"""Pop the next grasp, or None if the queue is empty."""
|
|
78
|
+
with self._lock:
|
|
79
|
+
if not self._queue:
|
|
80
|
+
return None
|
|
81
|
+
best_index = min(
|
|
82
|
+
range(len(self._queue)),
|
|
83
|
+
key=lambda queue_index: (
|
|
84
|
+
-self._queue[queue_index].priority,
|
|
85
|
+
self._queue[queue_index].distance_to_home,
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
grasp = self._queue.pop(best_index)
|
|
89
|
+
logger.info(f"Retrieved grasp {grasp.name}. Remaining: {len(self._queue)}")
|
|
90
|
+
return grasp
|
|
91
|
+
|
|
92
|
+
def has_grasps(self) -> bool:
|
|
93
|
+
return self.grasps_count() > 0
|
|
94
|
+
|
|
95
|
+
def grasps_count(self) -> int:
|
|
96
|
+
with self._lock:
|
|
97
|
+
return len(self._queue)
|
|
98
|
+
|
|
99
|
+
def clear_for_object(self, object_index: int) -> int:
|
|
100
|
+
"""Remove all grasps for a specific object from the queue."""
|
|
101
|
+
with self._lock:
|
|
102
|
+
original_count = len(self._queue)
|
|
103
|
+
self._queue = [grasp for grasp in self._queue if grasp.obj_ref.idx != object_index]
|
|
104
|
+
removed_count = original_count - len(self._queue)
|
|
105
|
+
if removed_count > 0:
|
|
106
|
+
logger.info(f"Cleared {removed_count} grasps for object {object_index}. Remaining: {len(self._queue)}")
|
|
107
|
+
return removed_count
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Per-part resolution and Initialize plumbing. SDK-internal free functions.
|
|
2
|
+
|
|
3
|
+
Validates a ``part_id`` against the cell config and (re-)sends the per-part
|
|
4
|
+
Initialize request. Used by both the manager's ``set_part`` and the cell
|
|
5
|
+
builders, so it lives outside the manager class.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from griip_sdk.cell.builders import get_effective_tool_config
|
|
14
|
+
from griip_sdk.cell.initialize import build_initialize_request, send_initialize
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def resolve_part_inputs(cell_config: dict[str, Any], part_id: str) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]:
|
|
18
|
+
"""Validate ``part_id`` against the cell config and return the part,
|
|
19
|
+
effective tool, and gripper config blocks. Raises ``ValueError`` if
|
|
20
|
+
``part_id`` isn't defined. SDK-internal.
|
|
21
|
+
"""
|
|
22
|
+
parts = cell_config.get("parts", {})
|
|
23
|
+
if part_id not in parts:
|
|
24
|
+
raise ValueError(f"Part '{part_id}' not defined in cell config. Known parts: {sorted(parts.keys())}")
|
|
25
|
+
part_config = parts[part_id]
|
|
26
|
+
tool_config = get_effective_tool_config(cell_config, part_config)
|
|
27
|
+
gripper_config = tool_config.get("gripper", {})
|
|
28
|
+
return part_config, tool_config, gripper_config
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def send_initialize_for_part(
|
|
32
|
+
admin_stub: Any,
|
|
33
|
+
cell_config: dict[str, Any],
|
|
34
|
+
part_config: dict[str, Any],
|
|
35
|
+
part_id: str,
|
|
36
|
+
base_path: Path,
|
|
37
|
+
*,
|
|
38
|
+
camera_intrinsics_flat: list[float],
|
|
39
|
+
image_width: int,
|
|
40
|
+
image_height: int,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Send the per-part Initialize request over the supplied admin stub. SDK-internal."""
|
|
43
|
+
initialize_request = build_initialize_request(
|
|
44
|
+
cell_config=cell_config,
|
|
45
|
+
part_config=part_config,
|
|
46
|
+
part_id=part_id,
|
|
47
|
+
base_path=base_path,
|
|
48
|
+
camera_intrinsics_flat=camera_intrinsics_flat,
|
|
49
|
+
image_width=image_width,
|
|
50
|
+
image_height=image_height,
|
|
51
|
+
)
|
|
52
|
+
send_initialize(admin_stub, initialize_request)
|