airo-robots 2025.4.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 @@
1
+ # keep mypy happy
@@ -0,0 +1,85 @@
1
+ import enum
2
+ import time
3
+ import warnings
4
+ from typing import Callable, Optional
5
+
6
+
7
+ class ACTION_STATUS_ENUM(enum.Enum):
8
+ EXECUTING = 1
9
+ SUCCEEDED = 2
10
+ TIMEOUT = 3
11
+
12
+
13
+ class AwaitableAction:
14
+ def __init__(
15
+ self,
16
+ termination_condition: Callable[..., bool],
17
+ default_timeout: float = 30.0,
18
+ default_sleep_resolution: float = 0.1,
19
+ ):
20
+ """
21
+
22
+ Args:
23
+ termination_condition (Callable[..., bool]): Any callable that returns True when the action is completed.
24
+ In the simplest case, it can be a lambda that returns True.
25
+ It can also be true when the gripper is in the desired position.
26
+ Or it could be true after a certain amount of time has passed since the action was started.
27
+
28
+ default_timeout (float, optional): The max waiting time before the wait returns and raises a warning.
29
+ Select an appropriate default value for the command you are creating this AwaitableAction for.
30
+
31
+ default_sleep_resolution (float, optional): The length of the time.sleep() in each iteration.
32
+ Select an appropriate default value for the command you are creating this AwaitableAction for.
33
+
34
+ Note that the scope of this action is to send 1 command, do some other things, then wait for the command to finish.
35
+ If you send multiple commands to the gripper, there is no guarantee that the gripper will execute them in the order you send them,
36
+ as the gripper might preempt intermediate commands after it has finished the current command.
37
+ Such preemption will also not be detected by this action, and hence the wait of a preempted action will either timeout or succeed by accident.
38
+
39
+ """
40
+ self.status = ACTION_STATUS_ENUM.EXECUTING
41
+ self.is_action_done = termination_condition
42
+ self._default_timeout = default_timeout
43
+ self._default_sleep_resolution = default_sleep_resolution
44
+
45
+ def wait(self, timeout: Optional[float] = None, sleep_resolution: Optional[float] = None) -> ACTION_STATUS_ENUM:
46
+ """Busy waiting until the termination condition returns true, or until timeout.
47
+
48
+ Args:
49
+ timeout (float, optional): The max waiting time before the wait returns and raises a warning.
50
+ This prevents infinite loops. Defaults to the value set during creation of the awaitable, which is usually an appropriate value for the command.
51
+
52
+ sleep_resolution (float, optional): The length of the time.sleep() in each iteration.
53
+ higher values will take up less CPU resources but will also cause more latency between the action finishing and
54
+ this method to return. Defaults to the value set during creation of the awaitable, which is usually an appropriate value for the command.
55
+
56
+ Keep in mind that the time.sleep() function has limited accuracy, so the actual sleeping time will be usually higher
57
+ due to scheduling activities of the OS. Take a look [here](https://github.com/airo-ugent/airo-mono/pull/21#discussion_r1132520057)
58
+ for some realistic numbers on the error. The error is independent of the sleep time and is in the order of 0.2ms.
59
+ So the lower the sleep time, the higher the relative error becomes.
60
+
61
+
62
+ Returns:
63
+ ACTION_STATUS_ENUM: _description_
64
+ """
65
+ # see #airo-robots/scripts/measure_sleep_accuracy.py for a script that measures the sleep accuracy and
66
+ # the result of some measurements.
67
+ sleep_resolution = sleep_resolution or self._default_sleep_resolution
68
+ timeout = timeout or self._default_timeout
69
+ assert (
70
+ sleep_resolution > 0.001
71
+ ), "sleep resolution must be at least 1 ms, otherwise the relative error of a sleep becomes too large to be meaningful"
72
+ if not self.status == ACTION_STATUS_ENUM.EXECUTING:
73
+ return self.status
74
+ while True:
75
+ time.sleep(sleep_resolution)
76
+ timeout -= sleep_resolution
77
+ if self.is_action_done():
78
+ self.status = ACTION_STATUS_ENUM.SUCCEEDED
79
+ return self.status
80
+ if timeout < 0:
81
+ warnings.warn("Action timed out. Make sure this was expected.")
82
+ return ACTION_STATUS_ENUM.TIMEOUT
83
+
84
+ def is_done(self) -> bool:
85
+ return self.status == ACTION_STATUS_ENUM.SUCCEEDED
File without changes
File without changes
@@ -0,0 +1,133 @@
1
+ import math
2
+ import time
3
+ from threading import Thread
4
+
5
+ import numpy as np
6
+ from airo_robots.awaitable_action import AwaitableAction
7
+ from airo_robots.drives.mobile_robot import CompliantLevel, MobileRobot
8
+ from airo_tulip.api.client import KELORobile as KELORobileClient # type: ignore
9
+ from airo_tulip.hardware.platform_driver import PlatformDriverType # type: ignore
10
+ from airo_typing import Vector3DType
11
+
12
+
13
+ class KELORobile(MobileRobot):
14
+ """KELO Robile platform implementation of the MobileRobot interface.
15
+
16
+ The KELO Robile platform consists of several drives with castor wheels which are connected via EtherCAT and can
17
+ be controlled individually. The airo-tulip API, which is used here, provides a higher level interface which
18
+ controls the entire platform."""
19
+
20
+ def __init__(self, robot_ip: str, robot_port: int = 49789):
21
+ """Connect to the KELO robot.
22
+
23
+ The KELO robot should already be running the airo-tulip server. If this is not the case, any messages
24
+ sent from the client may be queued and executed once the server is started, resulting in unexpected
25
+ and/or sudden movements.
26
+
27
+ Args:
28
+ robot_ip: IP address of the KELO CPU brick.
29
+ robot_port: Port to connect on (default: 49789)."""
30
+
31
+ self._kelo_robile = KELORobileClient(robot_ip, robot_port)
32
+
33
+ # Position control.
34
+ self._control_loop_done = True
35
+
36
+ def align_drives(self, x: float, y: float, a: float, timeout: float = 1.0) -> AwaitableAction:
37
+ """Align all drives for driving in a direction given by the linear and angular velocities.
38
+
39
+ Beware that sending any other velocity commands before the awaitable is done may cause unexpected behaviour,
40
+ as this affects the "aligned" condition.
41
+
42
+ Args:
43
+ x: The x velocity (linear, m/s).
44
+ y: The y velocity (linear, m/s)
45
+ a: The angular velocity (rad/s).
46
+ timeout: The awaitable will finish after this time at the latest.
47
+
48
+ Returns:
49
+ An AwaitableAction which will check if the drives are aligned, or if the timeout has expired."""
50
+ self._kelo_robile.align_drives(x, y, a, timeout=timeout)
51
+
52
+ action_sent_time = time.time_ns()
53
+ return AwaitableAction(
54
+ lambda: self._kelo_robile.are_drives_aligned() or time.time_ns() - action_sent_time > timeout * 1e9,
55
+ default_timeout=2 * timeout,
56
+ default_sleep_resolution=0.002,
57
+ )
58
+
59
+ def set_platform_velocity_target(self, x: float, y: float, a: float, timeout: float) -> AwaitableAction:
60
+ self._kelo_robile.set_platform_velocity_target(x, y, a, timeout=timeout)
61
+
62
+ action_sent_time = time.time_ns()
63
+ return AwaitableAction(
64
+ lambda: time.time_ns() - action_sent_time > timeout * 1e9,
65
+ default_timeout=2 * timeout,
66
+ default_sleep_resolution=0.002,
67
+ )
68
+
69
+ def _move_platform_to_pose_control_loop(
70
+ self, target_pose: Vector3DType, action_start_time: float, action_timeout_time: float, timeout: float
71
+ ) -> None:
72
+ stop = False
73
+ while not stop:
74
+ current_pose = self._kelo_robile.get_odometry()
75
+ delta_pose = target_pose - current_pose
76
+
77
+ vel_vec_angle = np.arctan2(delta_pose[1], delta_pose[0]) - current_pose[2]
78
+ vel_vec_norm = min(np.linalg.norm(delta_pose[:2]), 0.5)
79
+ vel_x = vel_vec_norm * np.cos(vel_vec_angle)
80
+ vel_y = vel_vec_norm * np.sin(vel_vec_angle)
81
+
82
+ delta_angle = np.arctan2(np.sin(delta_pose[2]), np.cos(delta_pose[2]))
83
+ vel_a = max(min(delta_angle, math.pi / 4), -math.pi / 4)
84
+
85
+ command_timeout = (action_timeout_time - time.time_ns()) * 1e-9
86
+ if command_timeout >= 0.0:
87
+ self._kelo_robile.set_platform_velocity_target(vel_x, vel_y, vel_a, timeout=command_timeout)
88
+
89
+ at_target_pose = bool(np.linalg.norm(delta_pose) < 0.01)
90
+ stop = at_target_pose or time.time_ns() - action_start_time > timeout * 1e9
91
+
92
+ self._kelo_robile.set_platform_velocity_target(0.0, 0.0, 0.0)
93
+ self._control_loop_done = True
94
+
95
+ def move_platform_to_pose(self, x: float, y: float, a: float, timeout: float) -> AwaitableAction:
96
+ target_pose = np.array([x, y, a])
97
+ action_start_time = time.time_ns()
98
+ action_timeout_time = action_start_time + timeout * 1e9
99
+
100
+ self._control_loop_done = False # Will be set to True by the below thread once it's finished.
101
+ thread = Thread(
102
+ target=self._move_platform_to_pose_control_loop,
103
+ args=(target_pose, action_start_time, action_timeout_time, timeout),
104
+ )
105
+ thread.start()
106
+
107
+ return AwaitableAction(
108
+ lambda: self._control_loop_done,
109
+ default_timeout=2 * timeout,
110
+ default_sleep_resolution=0.002,
111
+ )
112
+
113
+ def enable_compliant_mode(
114
+ self, enabled: bool, compliant_level: CompliantLevel = CompliantLevel.COMPLIANT_WEAK
115
+ ) -> None:
116
+ if enabled:
117
+ if compliant_level == CompliantLevel.COMPLIANT_WEAK:
118
+ self._kelo_robile.set_driver_type(PlatformDriverType.COMPLIANT_WEAK)
119
+ elif compliant_level == CompliantLevel.COMPLIANT_MODERATE:
120
+ self._kelo_robile.set_driver_type(PlatformDriverType.COMPLIANT_MODERATE)
121
+ else:
122
+ self._kelo_robile.set_driver_type(PlatformDriverType.COMPLIANT_STRONG)
123
+ else:
124
+ self._kelo_robile.set_driver_type(PlatformDriverType.VELOCITY)
125
+
126
+ def get_odometry(self) -> Vector3DType:
127
+ return self._kelo_robile.get_odometry()
128
+
129
+ def get_velocity(self) -> Vector3DType:
130
+ return self._kelo_robile.get_velocity()
131
+
132
+ def reset_odometry(self) -> None:
133
+ self._kelo_robile.reset_odometry()
@@ -0,0 +1,31 @@
1
+ """code for manual testing of mobile robot base class implementations.
2
+ """
3
+ from airo_robots.drives.hardware.kelo_robile import KELORobile
4
+ from airo_robots.drives.mobile_robot import MobileRobot
5
+
6
+
7
+ def manually_test_robot_implementation(robot: MobileRobot) -> None:
8
+ if isinstance(robot, KELORobile):
9
+ input("robot will rotate drives")
10
+ robot.align_drives(0.1, 0.0, 0.0, 1.0).wait()
11
+
12
+ input("robot will now move forward 10cm")
13
+ robot.set_platform_velocity_target(0.1, 0.0, 0.0, 1.0).wait()
14
+
15
+ input("robot will now move left 10cm")
16
+ robot.set_platform_velocity_target(0.0, 0.1, 0.0, 1.0).wait()
17
+
18
+ input("robot will now make two short rotations")
19
+ robot.set_platform_velocity_target(0.0, 0.0, 0.1, 1.0).wait()
20
+ robot.set_platform_velocity_target(0.0, 0.0, -0.1, 1.0).wait()
21
+
22
+ input("robot will now return to original position")
23
+ robot.set_platform_velocity_target(-0.1, -0.1, 0.0, 1.0).wait()
24
+
25
+ input("robot will now move to 10 cm along +X relative from its starting position with position control")
26
+ robot.move_platform_to_pose(0.1, 0.0, 0.0, 10.0).wait()
27
+
28
+
29
+ if __name__ == "__main__":
30
+ mobi = KELORobile("10.10.129.21")
31
+ manually_test_robot_implementation(mobi)
@@ -0,0 +1,82 @@
1
+ from abc import ABC, abstractmethod
2
+ from enum import Enum
3
+
4
+ from airo_robots.awaitable_action import AwaitableAction
5
+ from airo_typing import Vector3DType
6
+
7
+
8
+ class CompliantLevel(Enum):
9
+ """The level of compliance expected from the mobile robot.
10
+
11
+ Values may not correspond to identical behaviour on different mobile platforms, but are merely an indication.
12
+ A value of weak means a very compliant robot, whereas a value of strong means a slightly compliant robot."""
13
+
14
+ COMPLIANT_WEAK = 1
15
+ COMPLIANT_MODERATE = 2
16
+ COMPLIANT_STRONG = 3
17
+
18
+
19
+ class MobileRobot(ABC):
20
+ """
21
+ Base class for a mobile robot.
22
+
23
+ Mobile robots typically allow to set a target velocity for the entire platform and apply torques to their wheels
24
+ to achieve the desired velocity.
25
+
26
+ All values are in metric units:
27
+ - Linear velocities are expressed in meters/second
28
+ - Angular velocities are expressed in radians/second
29
+ """
30
+
31
+ @abstractmethod
32
+ def set_platform_velocity_target(self, x: float, y: float, a: float, timeout: float) -> AwaitableAction:
33
+ """Set the desired platform velocity.
34
+
35
+ Args:
36
+ x: Linear velocity along the X axis.
37
+ y: Linear velocity along the Y axis.
38
+ a: Angular velocity.
39
+ timeout: After this time, the platform will automatically stop.
40
+
41
+ Returns:
42
+ An awaitable action."""
43
+
44
+ @abstractmethod
45
+ def move_platform_to_pose(self, x: float, y: float, a: float, timeout: float) -> AwaitableAction:
46
+ """Move the platform to the given pose, without guarantees about the followed path.
47
+
48
+ Args:
49
+ x: Position along the startup pose's X axis.
50
+ y: Position along the startup pose's Y axis.
51
+ a: Orientation around the startup pose's Z axis.
52
+ timeout: After this time, the platform will automatically stop.
53
+
54
+ Returns:
55
+ An awaitable action."""
56
+
57
+ @abstractmethod
58
+ def enable_compliant_mode(self, enabled: bool, compliant_level: CompliantLevel) -> None:
59
+ """Enable compliant mode on the robot.
60
+
61
+ Args:
62
+ enabled: If true, will enable compliant mode. Else, will disable compliant mode.
63
+ compliant_level: The level of compliance to be expected from the robot. Ignored if `enabled` is `False`."""
64
+
65
+ @abstractmethod
66
+ def get_odometry(self) -> Vector3DType:
67
+ """Get the estimated robot pose as a 3D vector comprising the `x`, `y`, and `theta` values relative to the
68
+ robot's starting pose.
69
+
70
+ Returns: A 3D vector with a value for the `x` position, `y` position, and `theta` angle of the robot."""
71
+
72
+ @abstractmethod
73
+ def reset_odometry(self) -> None:
74
+ """Reset the robot's odometry to `(0, 0, 0)`."""
75
+
76
+ @abstractmethod
77
+ def get_velocity(self) -> Vector3DType:
78
+ """Get the estimated robot's velocity as a 3D vector comprising the `x`, `y` and `theta` values relative to the
79
+ robot's starting pose.
80
+
81
+ Returns:
82
+ A 3D vector with a value for the `x` velocity, `y` velocity, and `theta` angular velocity of the robot."""
@@ -0,0 +1,5 @@
1
+ from airo_robots.grippers.hardware.robotiq_2f85_urcap import Robotiq2F85 # noqa: F401 - imported but unused
2
+ from airo_robots.grippers.parallel_position_gripper import ( # noqa: F401 - imported but unused
3
+ ParallelPositionGripper,
4
+ ParallelPositionGripperSpecs,
5
+ )
File without changes
@@ -0,0 +1,32 @@
1
+ """code for manual testing of gripper base class implementations.
2
+ """
3
+ from airo_robots.grippers.parallel_position_gripper import ParallelPositionGripper, ParallelPositionGripperSpecs
4
+
5
+
6
+ def manually_test_gripper_implementation(
7
+ gripper: ParallelPositionGripper, specs: ParallelPositionGripperSpecs
8
+ ) -> None:
9
+ input("gripper will now open")
10
+ gripper.move(specs.max_width).wait()
11
+ assert abs(gripper.get_current_width() - specs.max_width) < 0.003
12
+ print(f"gripper width {gripper.get_current_width()} should be close to the requested {specs.max_width}")
13
+
14
+ input("set speed and check that this happens synchronously")
15
+ gripper.speed = 0.02
16
+ print(f"{gripper.speed=}")
17
+ assert abs(gripper.speed - 0.02) < 0.005
18
+
19
+ input("gripper wil now move slowly to 2cm opening ")
20
+ res = gripper.move(0.02)
21
+ print("move started and returned awaitable")
22
+ res.wait()
23
+ print("move completed, gripper should have reached 2cm")
24
+ assert abs(gripper.get_current_width() - 0.02) < 0.003
25
+
26
+ input("reopen gripper fast")
27
+ gripper.move(0.08, 0.15)
28
+ input("close with high force, you can put an object between the fingers to test the force and the grasp detection")
29
+ gripper.max_grasp_force = 200
30
+ gripper.speed = 0.02
31
+ gripper.move(0.02).wait()
32
+ print(f"{gripper.is_an_object_grasped()=}")
@@ -0,0 +1,253 @@
1
+ import asyncio
2
+ import socket
3
+ import time
4
+ from typing import Optional
5
+
6
+ import numpy as np
7
+ from airo_robots.awaitable_action import AwaitableAction
8
+ from airo_robots.grippers.hardware.manual_gripper_testing import manually_test_gripper_implementation
9
+ from airo_robots.grippers.parallel_position_gripper import ParallelPositionGripper, ParallelPositionGripperSpecs
10
+ from airo_robots.hardware_interaction_utils import wait_for_condition_with_timeout
11
+
12
+
13
+ def rescale_range(x: float, from_min: float, from_max: float, to_min: float, to_max: float) -> float:
14
+ return to_min + (x - from_min) / (from_max - from_min) * (to_max - to_min)
15
+
16
+
17
+ class Robotiq2F85(ParallelPositionGripper):
18
+ """
19
+ Implementation of the gripper interface for a Robotiq 2F-85 gripper that is connected to a UR robot and is controlled with the Robotiq URCap.
20
+
21
+ The API is available at TCP port 63352 of the UR controller and wraps the Modbus registers of the gripper, as described in
22
+ https://dof.robotiq.com/discussion/2420/control-robotiq-gripper-mounted-on-ur-robot-via-socket-communication-python.
23
+ The control sequence is gripper motor <---- gripper registers<--ModbusSerial(rs485)-- UR controller <--TCP-- remote control
24
+
25
+ This class does 2 things:
26
+ - it communicates over TCP using the above mentioned API to read/write values from/to the gripper's registers.
27
+ - it rescales all those register values into metric units, as required by the gripper interface
28
+
29
+ For more info on how to install the URCap and connecting the gripper's RS-485 connection to a UR robot, see the manual, section 4.8
30
+ https://assets.robotiq.com/website-assets/support_documents/document/2F-85_2F-140_Instruction_Manual_e-Series_PDF_20190206.pdf
31
+
32
+ Make sure you can control the gripper using the robot controller (polyscope) before using this script.
33
+ """
34
+
35
+ # values obtained from the manual
36
+ # see https://assets.robotiq.com/website-assets/support_documents/document/2F-85_2F-140_Instruction_Manual_e-Series_PDF_20190206.pdf
37
+ ROBOTIQ_2F85_DEFAULT_SPECS = ParallelPositionGripperSpecs(0.085, 0.0, 220, 25, 0.15, 0.02)
38
+
39
+ def __init__(self, host_ip: str, port: int = 63352, fingers_max_stroke: Optional[float] = None) -> None:
40
+ """
41
+ host_ip: the IP adress of the robot to which the gripper is connected.
42
+
43
+ fingers_max_stroke:
44
+ allow for custom max stroke width (if you have fingertips that are closer together).
45
+ the robotiq will always calibrate such that max opening = 0 and min opening = 255 for its register.
46
+ see manual p42
47
+ """
48
+ self._gripper_specs = self.ROBOTIQ_2F85_DEFAULT_SPECS
49
+ if fingers_max_stroke:
50
+ self._gripper_specs.max_width = fingers_max_stroke
51
+ self.host_ip = host_ip
52
+ self.port = port
53
+
54
+ self._check_connection()
55
+
56
+ if not self.gripper_is_active():
57
+ self._activate_gripper()
58
+
59
+ super().__init__(self.gripper_specs)
60
+
61
+ def get_current_width(self) -> float:
62
+ register_value = int(self._communicate("GET POS").split(" ")[1])
63
+ width = rescale_range(register_value, 0, 230, self._gripper_specs.max_width, self._gripper_specs.min_width)
64
+ return width
65
+
66
+ def move(self, width: float, speed: Optional[float] = None, force: Optional[float] = None) -> AwaitableAction:
67
+ if speed:
68
+ self.speed = speed
69
+ if force:
70
+ self.max_grasp_force = force
71
+ self._set_target_width(width)
72
+
73
+ # this sleep is required to make sure that the OBJ STATUS
74
+ # of the gripper is already in 'moving' before entering the wait loop.
75
+ time.sleep(0.01)
76
+
77
+ def move_done_condition() -> bool:
78
+ done = abs(self.get_current_width() - width) < 0.002
79
+ done = done or self.is_an_object_grasped()
80
+ return done
81
+
82
+ return AwaitableAction(move_done_condition)
83
+
84
+ def is_an_object_grasped(self) -> bool:
85
+ return int(self._communicate("GET OBJ").split(" ")[1]) == 2
86
+
87
+ @property
88
+ def speed(self) -> float:
89
+ speed_register_value = self._read_speed_register()
90
+ return rescale_range(
91
+ speed_register_value, 0, 255, self._gripper_specs.min_speed, self._gripper_specs.max_speed
92
+ )
93
+
94
+ @speed.setter
95
+ def speed(self, value: float) -> None:
96
+ speed = np.clip(value, self.gripper_specs.min_speed, self.gripper_specs.max_speed)
97
+ speed_register_value = int(
98
+ rescale_range(speed, self._gripper_specs.min_speed, self._gripper_specs.max_speed, 0, 255)
99
+ )
100
+ self._communicate(f"SET SPE {speed_register_value}")
101
+
102
+ def is_value_set() -> bool:
103
+ return self._is_target_value_set(self._read_speed_register(), speed_register_value)
104
+
105
+ wait_for_condition_with_timeout(is_value_set)
106
+
107
+ @property
108
+ def max_grasp_force(self) -> float:
109
+ force_register_value = self._read_force_register()
110
+ # 0 force has a special meaning, cf manual.
111
+ return rescale_range(
112
+ force_register_value, 1, 255, self._gripper_specs.min_force, self._gripper_specs.max_force
113
+ )
114
+
115
+ @max_grasp_force.setter
116
+ def max_grasp_force(self, value: float) -> None:
117
+ force = np.clip(value, self.gripper_specs.min_force, self.gripper_specs.max_force)
118
+ force_register_value = int(
119
+ rescale_range(force, self._gripper_specs.min_force, self._gripper_specs.max_force, 1, 255)
120
+ )
121
+ self._communicate(f"SET FOR {force_register_value}")
122
+
123
+ def is_value_set() -> bool:
124
+ return self._is_target_value_set(force_register_value, self._read_force_register())
125
+
126
+ wait_for_condition_with_timeout(is_value_set)
127
+
128
+ ###########################
129
+ ## non-interface classes ##
130
+ ###########################
131
+ async def asyncio_move(self, width: float, speed: Optional[float] = None, force: Optional[float] = None) -> None:
132
+ """Asyncio (async) move"""
133
+ if speed:
134
+ self.speed = speed
135
+ if force:
136
+ self.max_grasp_force = force
137
+ self._set_target_width(width)
138
+
139
+ while self.is_gripper_moving():
140
+ await asyncio.sleep(0.05)
141
+
142
+ ####################
143
+ # Private methods #
144
+ ####################
145
+
146
+ def _set_target_width(self, target_width_in_meters: float) -> None:
147
+ """Sends target width to gripper"""
148
+ target_width_in_meters = np.clip(
149
+ target_width_in_meters, self._gripper_specs.min_width, self._gripper_specs.max_width
150
+ )
151
+ # 230 is 'force closed', cf _write_target_width_to_register.
152
+ target_width_register_value = round(
153
+ rescale_range(target_width_in_meters, self._gripper_specs.min_width, self._gripper_specs.max_width, 230, 0)
154
+ )
155
+ self._write_target_width_to_register(target_width_register_value)
156
+
157
+ def _write_target_width_to_register(self, target_width_register_value: int) -> None:
158
+ """
159
+ Takes values in range 0 -255
160
+ 3 is actually fully open, 230 is fully closed in operating mode (straight fingers) and 255 is fully closed in encompassed mode.
161
+ For the default fingertips, the range 3 - 230 maps approximately to 0mm - 85mm with a quasi linear relation of 0.4mm / unit
162
+ (experimental findings, but manual also mentions this 0.4mm/unit relation)
163
+
164
+ For custom finger tips with a different stroke, it won't be 0.4mm/unit anymore,
165
+ but 230 will still be fully closed due to the self-calibration of the gripper.
166
+
167
+ """
168
+ self._communicate(f"SET POS {target_width_register_value}")
169
+
170
+ def is_value_set() -> bool:
171
+ return self._is_target_value_set(target_width_register_value, self._read_target_width_register())
172
+
173
+ wait_for_condition_with_timeout(is_value_set)
174
+
175
+ def _read_target_width_register(self) -> int:
176
+ return int(self._communicate("GET PRE").split(" ")[1])
177
+
178
+ def _read_speed_register(self) -> int:
179
+ return int(self._communicate("GET SPE").split(" ")[1])
180
+
181
+ def _read_force_register(self) -> int:
182
+ return int(self._communicate("GET FOR").split(" ")[1])
183
+
184
+ def is_gripper_moving(self) -> bool:
185
+ # Moving == 0 => detected OR position reached
186
+ return int(self._communicate("GET OBJ").split(" ")[1]) == 0
187
+
188
+ def _check_connection(self) -> None:
189
+ """validate communication with gripper is possible.
190
+ Raises:
191
+ ConnectionError
192
+ """
193
+ if not self._communicate("GET STA").startswith("STA"):
194
+ raise ConnectionError("Could not connect to gripper")
195
+
196
+ def _communicate(self, command: str) -> str:
197
+ """Helper function to communicate with gripper over a tcp socket.
198
+ Args:
199
+ command (str): The GET/SET command string.
200
+ """
201
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
202
+ try:
203
+ s.connect((self.host_ip, self.port))
204
+ s.sendall(("" + str.strip(command) + "\n").encode())
205
+
206
+ data = s.recv(2**10)
207
+ return data.decode()[:-1]
208
+ except Exception as e:
209
+ raise (e)
210
+
211
+ def _activate_gripper(self) -> None:
212
+ """Activates the gripper, sets target position to "Open" and sets GTO flag."""
213
+ self._communicate("SET ACT 1")
214
+ wait_for_condition_with_timeout(self.gripper_is_active)
215
+ # initialize gripper
216
+ self._communicate("SET GTO 1") # enable Gripper
217
+ self.speed = self._gripper_specs.max_speed
218
+ self.force = self._gripper_specs.min_force
219
+
220
+ def _deactivate_gripper(self) -> None:
221
+ self._communicate("SET ACT 0")
222
+ wait_for_condition_with_timeout(lambda: self._communicate("GET STA") == "STA 0")
223
+
224
+ def gripper_is_active(self) -> bool:
225
+ return self._communicate("GET STA") == "STA 3"
226
+
227
+ @staticmethod
228
+ def _is_target_value_set(target: int, value: int) -> bool:
229
+ """helper to compare target value to current value and make the force / speed request synchronous"""
230
+ return abs(target - value) < 5
231
+
232
+
233
+ def get_empirical_data_on_opening_angles(robot_ip: str) -> None:
234
+ """used to verify the relation between finger width and register values."""
235
+ gripper = Robotiq2F85(robot_ip)
236
+ gripper._write_target_width_to_register(230)
237
+ input("press")
238
+ for position in [0, 50, 100, 150, 200, 250]:
239
+ gripper._write_target_width_to_register(position)
240
+ input(f"press key for moving to next position {position}")
241
+
242
+
243
+ if __name__ == "__main__":
244
+ robot_ip = "10.42.0.162" # hardcoded IP of Victor UR3e
245
+ import click
246
+
247
+ @click.command()
248
+ @click.option("--robot_ip", default=robot_ip, help="IP of UR to which the gripper is connected.")
249
+ def test_robotiq(robot_ip: str) -> None:
250
+ gripper = Robotiq2F85(robot_ip)
251
+ manually_test_gripper_implementation(gripper, gripper._gripper_specs)
252
+
253
+ test_robotiq()