urkit 0.1.0__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.
@@ -0,0 +1,113 @@
1
+ """Gripper presets and configuration dataclasses.
2
+
3
+ Built-in presets for common Robotiq grippers. Each preset carries the
4
+ mass, center of gravity, TCP offset, and backend type — everything
5
+ needed to configure the robot for that gripper in a single argument.
6
+
7
+ Usage::
8
+
9
+ from urkit import URRobot
10
+ from urkit.gripper.presets import ROBOTIQ_2F_85
11
+
12
+ # One arg — specs + backend
13
+ robot = URRobot(ip="192.168.1.100", gripper=ROBOTIQ_2F_85)
14
+
15
+ # Digital I/O gripper
16
+ from urkit.gripper.presets import DigitalGripperConfig
17
+
18
+ robot = URRobot(ip="192.168.1.100", gripper=DigitalGripperConfig(pin=3))
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from dataclasses import dataclass
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class GripperPreset:
28
+ """Gripper preset with physical specs and backend type.
29
+
30
+ Args:
31
+ name: Human-readable identifier (e.g. ``"2F-85"``).
32
+ mass: Mass in kg.
33
+ center_of_gravity: Center of gravity in tool coordinates [x, y, z], meters.
34
+ tcp_offset: TCP offset [x, y, z, rx, ry, rz], meters/radians.
35
+ backend: Gripper backend name (e.g. ``"robotiq"``).
36
+ max_mm: Maximum finger travel in mm (for ``set_position()``).
37
+ force: Gripper force 0-100 (default 100).
38
+ speed: Gripper speed 0-100 (default 100).
39
+ """
40
+
41
+ name: str
42
+ mass: float
43
+ center_of_gravity: list[float]
44
+ tcp_offset: list[float]
45
+ backend: str
46
+ max_mm: int = 50
47
+ force: int = 100
48
+ speed: int = 100
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class DigitalGripperConfig:
53
+ """Configuration for a digital I/O gripper.
54
+
55
+ Digital grippers have no standard physical specs — just a pin
56
+ and polarity. Pass this instead of a ``GripperPreset`` when
57
+ using a suction cup, solenoid gripper, or custom actuator.
58
+
59
+ Args:
60
+ pin: Digital output pin index (0-7).
61
+ close_on_high: If True, HIGH signal closes the gripper
62
+ (default True — suction cups, electromagnetic grippers).
63
+ If False, LOW signal closes it (normally-closed solenoid).
64
+ """
65
+
66
+ pin: int
67
+ close_on_high: bool = True
68
+
69
+
70
+ # ---------------------------------------------------------------------------
71
+ # Robotiq 2-Finger Adaptive Grippers
72
+ # ---------------------------------------------------------------------------
73
+
74
+ ROBOTIQ_2F_85 = GripperPreset(
75
+ name="2F-85",
76
+ mass=0.921,
77
+ center_of_gravity=[0.0, 0.0, 0.060],
78
+ tcp_offset=[0.0, 0.0, 0.174, 0.0, 0.0, 0.0],
79
+ backend="robotiq",
80
+ max_mm=85,
81
+ )
82
+
83
+ ROBOTIQ_2F_140 = GripperPreset(
84
+ name="2F-140",
85
+ mass=1.013,
86
+ center_of_gravity=[0.0, 0.0, 0.0755],
87
+ tcp_offset=[0.0, 0.0, 0.244, 0.0, 0.0, 0.0],
88
+ backend="robotiq",
89
+ max_mm=140,
90
+ )
91
+
92
+ # ---------------------------------------------------------------------------
93
+ # Robotiq 2F-140-E (Hand-E series)
94
+ # ---------------------------------------------------------------------------
95
+
96
+ ROBOTIQ_HAND_E = GripperPreset(
97
+ name="Hand-E",
98
+ mass=1.068,
99
+ center_of_gravity=[0.0, 0.0, 0.059],
100
+ tcp_offset=[0.0, 0.0, 0.157, 0.0, 0.0, 0.0],
101
+ backend="robotiq",
102
+ max_mm=50,
103
+ )
104
+
105
+ # ---------------------------------------------------------------------------
106
+ # Registry — all built-in presets
107
+ # ---------------------------------------------------------------------------
108
+
109
+ PRESETS: dict[str, GripperPreset] = {
110
+ p.name.upper(): p
111
+ for p in (ROBOTIQ_2F_85, ROBOTIQ_2F_140, ROBOTIQ_HAND_E)
112
+ }
113
+ """Lookup dict keyed by uppercase preset name."""
@@ -0,0 +1,322 @@
1
+ """Robotiq 2F gripper backend via RTDE preamble-based scripting.
2
+
3
+ Controls the gripper by sending URScript commands through RTDE's
4
+ ``sendCustomScriptFunction()`` method, using the official Robotiq
5
+ preamble (``rq_*`` function library) for all communication.
6
+
7
+ The preamble is prepended to every command so the robot has the
8
+ function definitions and a live socket context. This enables
9
+ synchronous motion: ``rq_move_and_wait`` polls the gripper's OBJ
10
+ register via ``socket_read_byte_list`` until the move completes or
11
+ contact is detected — all running on the robot side.
12
+
13
+ Each ``sendCustomScriptFunction`` call blocks until the full script
14
+ (preamble + function) finishes executing on the robot, making all
15
+ gripper operations synchronous by default.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+ import threading
22
+ from typing import TYPE_CHECKING
23
+
24
+ from urkit.exceptions import GripperError
25
+ from urkit.gripper.base import Gripper
26
+ from urkit.gripper.robotiq_preamble import ROBOTIQ_PREAMBLE
27
+
28
+ if TYPE_CHECKING:
29
+ from rtde_control import RTDEControlInterface
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ class RobotiqGripper(Gripper):
35
+ """Robotiq 2F gripper via RTDE preamble-based scripting.
36
+
37
+ Uses the official Robotiq preamble (``rq_*`` URScript functions)
38
+ for all gripper communication. Every command prepends the preamble
39
+ so the robot has function definitions and a persistent socket
40
+ context, enabling synchronous motion with built-in polling.
41
+
42
+ Args:
43
+ rtde_control: RTDEControlInterface (required).
44
+ max_mm: Maximum finger travel in mm (default 50).
45
+ force: Gripper force, 0-100 (default 100).
46
+ speed: Gripper speed, 0-100 (default 100).
47
+
48
+ Raises:
49
+ GripperError: If rtde_control is not provided or force/speed
50
+ are out of range.
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ *,
56
+ rtde_control: "RTDEControlInterface",
57
+ max_mm: int = 50,
58
+ force: int = 100,
59
+ speed: int = 100,
60
+ **kwargs, # rtde_receive, robot_ip passed by factory but not used
61
+ ) -> None:
62
+ if rtde_control is None:
63
+ raise GripperError(
64
+ "RobotiqGripper requires rtde_control (RTDEControlInterface)."
65
+ )
66
+ if not 0 <= force <= 100:
67
+ raise GripperError(
68
+ f"Robotiq gripper force must be 0-100, got {force}."
69
+ )
70
+ if not 0 <= speed <= 100:
71
+ raise GripperError(
72
+ f"Robotiq gripper speed must be 0-100, got {speed}."
73
+ )
74
+
75
+ self._rtde = rtde_control
76
+ self._max_mm = max_mm
77
+ self._force = force
78
+ self._speed = speed
79
+ self._activated = False
80
+
81
+ logger.info(
82
+ "RobotiqGripper initialized (preamble, max_mm=%d)", max_mm
83
+ )
84
+
85
+ # ------------------------------------------------------------------
86
+ # Internal helpers
87
+ # ------------------------------------------------------------------
88
+
89
+ def _require_activated(self) -> None:
90
+ """Raise an error if the gripper has not been activated."""
91
+ if not self._activated:
92
+ raise GripperError(
93
+ "Gripper is not activated. Call activate() before "
94
+ "using open(), close(), or set_position()."
95
+ )
96
+
97
+ def _build_script(self, function_call: str) -> str:
98
+ """Build a complete URScript: preamble + config + function call.
99
+
100
+ The preamble defines all ``rq_*`` functions and initializes
101
+ socket state. Two config lines override the mm range for
102
+ the current gripper model before the function executes.
103
+
104
+ Args:
105
+ function_call: A preamble function call (e.g. ``rq_open_and_wait()``).
106
+
107
+ Returns:
108
+ Complete URScript code string.
109
+ """
110
+ return (
111
+ ROBOTIQ_PREAMBLE
112
+ + f"set_closed_mm(0.0, 1)\n"
113
+ + f"set_open_mm({self._max_mm}.0, 1)\n"
114
+ + f"{function_call}\n"
115
+ )
116
+
117
+ def _send_script(self, code: str) -> None:
118
+ """Send URScript code via sendCustomScriptFunction.
119
+
120
+ Blocks until the script finishes executing on the robot.
121
+
122
+ Args:
123
+ code: URScript code to execute on the robot.
124
+
125
+ Raises:
126
+ GripperError: If the script fails to send.
127
+ """
128
+ try:
129
+ ok = self._rtde.sendCustomScriptFunction("_gripper_cmd", code)
130
+ except Exception as e:
131
+ raise GripperError(f"Failed to send gripper script: {e}") from e
132
+ if not ok:
133
+ raise GripperError(
134
+ "sendCustomScriptFunction returned False — "
135
+ "RTDE custom script client may not be available."
136
+ )
137
+
138
+ # ------------------------------------------------------------------
139
+ # Public interface
140
+ # ------------------------------------------------------------------
141
+
142
+ def activate(self, *, timeout: float = 5.0) -> None:
143
+ """Activate the gripper and open it to a known safe state.
144
+
145
+ Checks the gripper's actual activation state on the robot using
146
+ the preamble's ``rq_is_gripper_activated()``. Only sends the
147
+ activation command if the gripper is not already activated. If
148
+ already activated, opens the gripper to a known safe state.
149
+
150
+ Safe to call multiple times — skips activation if the gripper
151
+ is already active on the robot.
152
+
153
+ Args:
154
+ timeout: Maximum seconds to wait for activation (default
155
+ 5.0). The Robotiq preamble blocks indefinitely if no
156
+ gripper is connected, so this prevents hanging.
157
+
158
+ Raises:
159
+ GripperError: If activation fails or times out.
160
+ """
161
+ if self._activated:
162
+ return
163
+ # Check activation state and activate+open if needed, all in one script.
164
+ # rq_activate_and_wait() sends ACT then polls until the gripper
165
+ # reports activated (~3-5s for internal initialization).
166
+ activation_script = (
167
+ ROBOTIQ_PREAMBLE
168
+ + f"set_closed_mm(0.0, 1)\n"
169
+ + f"set_open_mm({self._max_mm}.0, 1)\n"
170
+ + "if (not rq_is_gripper_activated()):\n"
171
+ + " rq_activate_and_wait()\n"
172
+ + "end\n"
173
+ + "rq_open_and_wait()\n"
174
+ )
175
+ # Run in a thread with a timeout — the preamble blocks indefinitely
176
+ # if no gripper is physically connected (2000-iteration loop).
177
+ _ready = threading.Event()
178
+ _err: Exception | None = None
179
+
180
+ def _do_activate() -> None:
181
+ try:
182
+ self._send_script(activation_script)
183
+ except Exception as e:
184
+ _err = e
185
+ finally:
186
+ _ready.set()
187
+
188
+ threading.Thread(target=_do_activate, daemon=True).start()
189
+ if not _ready.wait(timeout=timeout):
190
+ raise GripperError(
191
+ f"Gripper activation timed out after {timeout:.0f}s — "
192
+ "check that the gripper is physically connected and powered."
193
+ )
194
+ if _err is not None:
195
+ raise GripperError(f"Gripper activation failed: {_err}") from _err
196
+
197
+ self._activated = True
198
+ logger.info("Robotiq gripper activated (checked robot state)")
199
+
200
+ def is_activated(self) -> bool:
201
+ """Check if the gripper has been activated."""
202
+ return self._activated
203
+
204
+ def deactivate(self) -> None:
205
+ """Deactivate the gripper (send DEACT command).
206
+
207
+ Safe to call when not activated — becomes a no-op.
208
+ Use this before enabling freedrive to prevent the
209
+ gripper's script from interfering with manual movement.
210
+ Call activate() again after freedrive is disabled.
211
+ """
212
+ if not self._activated:
213
+ return
214
+ deact_script = (
215
+ 'socket_open("127.0.0.1", 63352, "deact_sock")\n'
216
+ "sync()\n"
217
+ 'socket_set_var("DEACT", 1, "deact_sock")\n'
218
+ "sync()\n"
219
+ )
220
+ self._send_script(deact_script)
221
+ self._activated = False
222
+ logger.info("Robotiq gripper deactivated")
223
+
224
+ def open(self, *, wait: bool = True) -> None:
225
+ """Open the gripper (fully open).
226
+
227
+ Args:
228
+ wait: If True, block until the gripper reaches the open
229
+ position (default True). If False, return immediately.
230
+
231
+ Raises:
232
+ GripperError: If the gripper has not been activated.
233
+ """
234
+ self._require_activated()
235
+ func = "rq_open_and_wait()" if wait else "rq_open()"
236
+ self._send_script(self._build_script(func))
237
+ logger.debug("Robotiq gripper opened (wait=%s)", wait)
238
+
239
+ def close(self, *, wait: bool = True) -> None:
240
+ """Close the gripper (fully closed).
241
+
242
+ Args:
243
+ wait: If True, block until the gripper reaches the closed
244
+ position or detects contact (default True). If False,
245
+ return immediately.
246
+
247
+ Raises:
248
+ GripperError: If the gripper has not been activated.
249
+ """
250
+ self._require_activated()
251
+ func = "rq_close_and_wait()" if wait else "rq_close()"
252
+ self._send_script(self._build_script(func))
253
+ logger.debug("Robotiq gripper closed (wait=%s)", wait)
254
+
255
+ def set_position(self, mm: float, *, wait: bool = True) -> None:
256
+ """Set the gripper to a specific opening in millimeters.
257
+
258
+ Args:
259
+ mm: Opening in mm (0 = fully closed, ``max_mm`` = fully open).
260
+ wait: If True, block until the gripper reaches the target
261
+ position or detects contact (default True). If False,
262
+ return immediately.
263
+
264
+ Raises:
265
+ GripperError: If the gripper has not been activated or
266
+ position is out of range.
267
+ """
268
+ if not 0 <= mm <= self._max_mm:
269
+ raise GripperError(
270
+ f"Robotiq gripper position must be 0-{self._max_mm} mm, got {mm}."
271
+ )
272
+ self._require_activated()
273
+ if wait:
274
+ func = f"rq_move_and_wait_mm({mm})"
275
+ else:
276
+ func = f"rq_move_mm({mm})"
277
+ self._send_script(self._build_script(func))
278
+ logger.debug("Robotiq gripper set to %.1f mm (wait=%s)", mm, wait)
279
+
280
+ def set_force(self, force: int) -> None:
281
+ """Set gripper force for subsequent movements.
282
+
283
+ Args:
284
+ force: Force 0-100.
285
+ """
286
+ if not 0 <= force <= 100:
287
+ raise GripperError(
288
+ f"Robotiq gripper force must be 0-100, got {force}."
289
+ )
290
+ self._force = force
291
+ logger.debug("Robotiq gripper force set to %d", force)
292
+
293
+ def set_speed(self, speed: int) -> None:
294
+ """Set gripper speed for subsequent movements.
295
+
296
+ Args:
297
+ speed: Speed 0-100.
298
+ """
299
+ if not 0 <= speed <= 100:
300
+ raise GripperError(
301
+ f"Robotiq gripper speed must be 0-100, got {speed}."
302
+ )
303
+ self._speed = speed
304
+ logger.debug("Robotiq gripper speed set to %d", speed)
305
+
306
+ def is_connected(self) -> bool:
307
+ """Check if the gripper can send commands via RTDE."""
308
+ try:
309
+ return self._rtde.sendCustomScriptFunction(
310
+ "_grip_ping", 'textmsg("gripper", "ok")'
311
+ )
312
+ except Exception:
313
+ return False
314
+
315
+ def disconnect(self) -> None:
316
+ """Disconnect the gripper.
317
+
318
+ Resets the activation flag. Call activate() again before
319
+ using the gripper.
320
+ """
321
+ self._activated = False
322
+ logger.debug("Robotiq gripper disconnected")