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.
Files changed (195) hide show
  1. griip_sdk/__init__.py +77 -0
  2. griip_sdk/calibration/__init__.py +0 -0
  3. griip_sdk/calibration/app.py +638 -0
  4. griip_sdk/calibration/apriltag_pose.py +101 -0
  5. griip_sdk/calibration/camera_factory.py +105 -0
  6. griip_sdk/calibration/cartesian_pose.py +220 -0
  7. griip_sdk/calibration/config_store.py +131 -0
  8. griip_sdk/calibration/hand_eye_calib/README.md +109 -0
  9. griip_sdk/calibration/hand_eye_calib/__init__.py +40 -0
  10. griip_sdk/calibration/hand_eye_calib/config/routine.yaml +7 -0
  11. griip_sdk/calibration/hand_eye_calib/core.py +692 -0
  12. griip_sdk/calibration/hand_eye_calib/main.py +596 -0
  13. griip_sdk/calibration/hand_eye_calib/planning.py +122 -0
  14. griip_sdk/calibration/hand_eye_calib/routines.py +223 -0
  15. griip_sdk/calibration/hand_eye_calib/run_hand_eye_calibration.py +131 -0
  16. griip_sdk/calibration/hand_eye_calib/transforms.py +57 -0
  17. griip_sdk/calibration/mqtt_publisher.py +98 -0
  18. griip_sdk/calibration/mqtt_topics.py +15 -0
  19. griip_sdk/calibration/routines.py +36 -0
  20. griip_sdk/calibration/scanning/__init__.py +11 -0
  21. griip_sdk/calibration/scanning/environment_scanning.py +381 -0
  22. griip_sdk/calibration/scanning/run_environment_scanning.py +142 -0
  23. griip_sdk/calibration/tests/__init__.py +0 -0
  24. griip_sdk/calibration/tests/conftest.py +33 -0
  25. griip_sdk/calibration/tests/test_calibration_app.py +822 -0
  26. griip_sdk/calibration/tests/test_camera_factory.py +126 -0
  27. griip_sdk/calibration/tests/test_environment_scanning.py +547 -0
  28. griip_sdk/calibration/tests/test_hand_eye_calibration_service.py +524 -0
  29. griip_sdk/calibration/tests/test_hand_eye_helpers.py +211 -0
  30. griip_sdk/calibration/tests/test_transforms.py +126 -0
  31. griip_sdk/calibration/types.py +21 -0
  32. griip_sdk/calibration/units.py +104 -0
  33. griip_sdk/cell/__init__.py +22 -0
  34. griip_sdk/cell/builders.py +631 -0
  35. griip_sdk/cell/config.py +505 -0
  36. griip_sdk/cell/end_effector_collision_model.py +160 -0
  37. griip_sdk/cell/initialize.py +282 -0
  38. griip_sdk/cell/loader.py +102 -0
  39. griip_sdk/cell/mock_builders.py +114 -0
  40. griip_sdk/cell/parts.py +63 -0
  41. griip_sdk/cell/recipes.py +872 -0
  42. griip_sdk/cell/tests/__init__.py +0 -0
  43. griip_sdk/cell/tests/conftest.py +7 -0
  44. griip_sdk/cell/tests/test_builders.py +13 -0
  45. griip_sdk/cell/tests/test_end_effector_collision_model.py +118 -0
  46. griip_sdk/cell/tests/test_initialize.py +458 -0
  47. griip_sdk/cell/tests/test_loader.py +70 -0
  48. griip_sdk/cell/tests/test_mock_cell_smoke.py +54 -0
  49. griip_sdk/cell/tests/test_parts.py +55 -0
  50. griip_sdk/cell/tests/test_recipes.py +156 -0
  51. griip_sdk/cell/tests/test_recording_config.py +113 -0
  52. griip_sdk/errors.py +16 -0
  53. griip_sdk/grpc_adapters/__init__.py +0 -0
  54. griip_sdk/grpc_adapters/servicer_as_stub.py +166 -0
  55. griip_sdk/hardware/__init__.py +0 -0
  56. griip_sdk/hardware/bin_shaker.py +86 -0
  57. griip_sdk/hardware/camera.py +300 -0
  58. griip_sdk/hardware/fanuc_robot_client.py +266 -0
  59. griip_sdk/hardware/ft_sensor.py +352 -0
  60. griip_sdk/hardware/gripper.py +339 -0
  61. griip_sdk/hardware/grpc_vision_backend.py +123 -0
  62. griip_sdk/hardware/io_client.py +194 -0
  63. griip_sdk/hardware/mmai_vision_backend.py +265 -0
  64. griip_sdk/hardware/mocks/__init__.py +0 -0
  65. griip_sdk/hardware/mocks/camera_mock.py +62 -0
  66. griip_sdk/hardware/mocks/gripper_client_mock.py +135 -0
  67. griip_sdk/hardware/mocks/io_client_mock.py +28 -0
  68. griip_sdk/hardware/mocks/robot_client_mock.py +584 -0
  69. griip_sdk/hardware/onrobot_3fg15_gripper.py +103 -0
  70. griip_sdk/hardware/robot_client.py +1692 -0
  71. griip_sdk/hardware/robotiq_2f140_gripper.py +327 -0
  72. griip_sdk/hardware/sim_gripper.py +365 -0
  73. griip_sdk/hardware/sim_hand_eye.py +138 -0
  74. griip_sdk/hardware/sim_robot_client.py +189 -0
  75. griip_sdk/hardware/suction_gripper.py +112 -0
  76. griip_sdk/hardware/tests/__init__.py +0 -0
  77. griip_sdk/hardware/tests/conftest.py +8 -0
  78. griip_sdk/hardware/tests/test_call_with_timeout.py +56 -0
  79. griip_sdk/hardware/tests/test_command_state_fanout.py +131 -0
  80. griip_sdk/hardware/tests/test_gripper_command_listeners.py +90 -0
  81. griip_sdk/hardware/tests/test_grpc_vision_backend.py +220 -0
  82. griip_sdk/hardware/tests/test_mmai_vision_backend.py +312 -0
  83. griip_sdk/hardware/tests/test_reset_to_normal.py +101 -0
  84. griip_sdk/hardware/tests/test_robot_client_base.py +48 -0
  85. griip_sdk/hardware/tests/test_robot_client_lifecycle.py +323 -0
  86. griip_sdk/hardware/tests/test_robot_feedback_fanout.py +154 -0
  87. griip_sdk/hardware/tests/test_robotiq_2f140_gripper.py +86 -0
  88. griip_sdk/hardware/tests/test_robotiq_gripper_bus_lock.py +78 -0
  89. griip_sdk/hardware/tests/test_sim_gripper.py +230 -0
  90. griip_sdk/hardware/tests/test_sim_hand_eye.py +226 -0
  91. griip_sdk/hardware/tests/test_sim_robot_client.py +196 -0
  92. griip_sdk/hardware/tests/test_suction_gripper.py +82 -0
  93. griip_sdk/hardware/tests/test_tower_light.py +179 -0
  94. griip_sdk/hardware/tests/test_vision_codec.py +73 -0
  95. griip_sdk/hardware/tests/test_vrmcs_tcp_guard.py +81 -0
  96. griip_sdk/hardware/torquing_client.py +23 -0
  97. griip_sdk/hardware/tower_light.py +84 -0
  98. griip_sdk/hardware/ur_robot_client.py +88 -0
  99. griip_sdk/hardware/vision_codec.py +77 -0
  100. griip_sdk/log_utils.py +42 -0
  101. griip_sdk/modeling/__init__.py +0 -0
  102. griip_sdk/modeling/sphere_generation_tool/README.md +187 -0
  103. griip_sdk/modeling/sphere_generation_tool/__init__.py +8 -0
  104. griip_sdk/modeling/sphere_generation_tool/__main__.py +242 -0
  105. griip_sdk/modeling/sphere_generation_tool/_actors.py +278 -0
  106. griip_sdk/modeling/sphere_generation_tool/_geometry.py +55 -0
  107. griip_sdk/modeling/sphere_generation_tool/editor.py +1142 -0
  108. griip_sdk/modeling/sphere_generation_tool/object_io.py +53 -0
  109. griip_sdk/modeling/sphere_generation_tool/urdf_io.py +315 -0
  110. griip_sdk/modeling/tests/__init__.py +0 -0
  111. griip_sdk/modeling/tests/conftest.py +123 -0
  112. griip_sdk/modeling/tests/test_editor_math.py +80 -0
  113. griip_sdk/modeling/tests/test_editor_smoke.py +471 -0
  114. griip_sdk/modeling/tests/test_mesh_loading.py +66 -0
  115. griip_sdk/modeling/tests/test_object_io.py +72 -0
  116. griip_sdk/modeling/tests/test_urdf_io.py +367 -0
  117. griip_sdk/orchestration/__init__.py +129 -0
  118. griip_sdk/orchestration/bin_cuboid_extraction.py +77 -0
  119. griip_sdk/orchestration/bin_picking_manager.py +580 -0
  120. griip_sdk/orchestration/bin_state.py +85 -0
  121. griip_sdk/orchestration/callbacks.py +60 -0
  122. griip_sdk/orchestration/cycle_pause_gate.py +92 -0
  123. griip_sdk/orchestration/errors.py +27 -0
  124. griip_sdk/orchestration/execution/__init__.py +18 -0
  125. griip_sdk/orchestration/execution/grasp_executor.py +105 -0
  126. griip_sdk/orchestration/execution/parallel_jaw_executor.py +132 -0
  127. griip_sdk/orchestration/execution/suction_executor.py +53 -0
  128. griip_sdk/orchestration/execution/tests/__init__.py +0 -0
  129. griip_sdk/orchestration/execution/tests/test_parallel_jaw_executor.py +164 -0
  130. griip_sdk/orchestration/execution/three_finger_executor.py +144 -0
  131. griip_sdk/orchestration/grasp_provider.py +258 -0
  132. griip_sdk/orchestration/grasp_queue.py +107 -0
  133. griip_sdk/orchestration/part_setup.py +52 -0
  134. griip_sdk/orchestration/perception_inputs.py +187 -0
  135. griip_sdk/orchestration/pick_and_place_manager.py +1514 -0
  136. griip_sdk/orchestration/pick_generator.py +230 -0
  137. griip_sdk/orchestration/placement_planner.py +365 -0
  138. griip_sdk/orchestration/planning.py +59 -0
  139. griip_sdk/orchestration/proto_conversions.py +113 -0
  140. griip_sdk/orchestration/roi_scheduler.py +150 -0
  141. griip_sdk/orchestration/scoop_executor.py +122 -0
  142. griip_sdk/orchestration/tests/__init__.py +0 -0
  143. griip_sdk/orchestration/tests/conftest.py +156 -0
  144. griip_sdk/orchestration/tests/test_bin_picking_manager.py +485 -0
  145. griip_sdk/orchestration/tests/test_bin_state.py +160 -0
  146. griip_sdk/orchestration/tests/test_cycle_pause_gate.py +130 -0
  147. griip_sdk/orchestration/tests/test_grasp_provider.py +307 -0
  148. griip_sdk/orchestration/tests/test_part_setup.py +69 -0
  149. griip_sdk/orchestration/tests/test_perception_inputs.py +122 -0
  150. griip_sdk/orchestration/tests/test_phase_collision_spheres.py +57 -0
  151. griip_sdk/orchestration/tests/test_pick_and_place_manager.py +760 -0
  152. griip_sdk/orchestration/tests/test_pick_generator.py +130 -0
  153. griip_sdk/orchestration/tests/test_placement_planner.py +333 -0
  154. griip_sdk/orchestration/tests/test_planning.py +64 -0
  155. griip_sdk/orchestration/tests/test_proto_conversions.py +96 -0
  156. griip_sdk/orchestration/tests/test_scoop_executor.py +106 -0
  157. griip_sdk/orchestration/trajectory_store.py +170 -0
  158. griip_sdk/orchestration/types.py +466 -0
  159. griip_sdk/prototyping/__init__.py +0 -0
  160. griip_sdk/prototyping/config/__init__.py +20 -0
  161. griip_sdk/prototyping/config/registry.py +22 -0
  162. griip_sdk/prototyping/in_process_api.py +59 -0
  163. griip_sdk/prototyping/read_tcp.py +67 -0
  164. griip_sdk/prototyping/run_environment_scanning.py +99 -0
  165. griip_sdk/prototyping/run_hand_eye_calibration.py +104 -0
  166. griip_sdk/prototyping/run_linear_shuttle.py +556 -0
  167. griip_sdk/recording/__init__.py +71 -0
  168. griip_sdk/recording/foxglove_events.py +553 -0
  169. griip_sdk/recording/foxglove_joint_states.py +115 -0
  170. griip_sdk/recording/gripper_state_logger.py +203 -0
  171. griip_sdk/recording/gripper_state_mcap_recording.py +69 -0
  172. griip_sdk/recording/h264_video_logger.py +128 -0
  173. griip_sdk/recording/h264_video_mcap_recording.py +217 -0
  174. griip_sdk/recording/kpi_logger.py +186 -0
  175. griip_sdk/recording/kpi_mcap_recording.py +143 -0
  176. griip_sdk/recording/kpi_timer.py +148 -0
  177. griip_sdk/recording/mcap_recording.py +209 -0
  178. griip_sdk/recording/memory_utils.py +19 -0
  179. griip_sdk/recording/robot_state_logger.py +133 -0
  180. griip_sdk/recording/rotating_mcap_writer.py +326 -0
  181. griip_sdk/recording/tests/test_foxglove_event_publisher.py +324 -0
  182. griip_sdk/recording/tests/test_foxglove_joint_states.py +26 -0
  183. griip_sdk/recording/tests/test_kpi_timer.py +346 -0
  184. griip_sdk/recording/tests/test_mcap_foxglove_event_publisher.py +355 -0
  185. griip_sdk/recording/tests/test_mcap_gripper_state_logger.py +334 -0
  186. griip_sdk/recording/tests/test_mcap_h264_video_logger.py +118 -0
  187. griip_sdk/recording/tests/test_mcap_kpi_logger.py +72 -0
  188. griip_sdk/recording/tests/test_mcap_robot_state_logger.py +253 -0
  189. griip_sdk/recording/tests/test_session.py +87 -0
  190. griip_sdk/tests/__init__.py +0 -0
  191. griip_sdk/tests/test_log_utils.py +81 -0
  192. griip_sdk/tests/test_servicer_as_stub.py +69 -0
  193. griip_sdk_dev-0.4.0.dev24238.dist-info/METADATA +513 -0
  194. griip_sdk_dev-0.4.0.dev24238.dist-info/RECORD +195 -0
  195. 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)