airo-robots 2025.4.0__tar.gz

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 (34) hide show
  1. airo_robots-2025.4.0/PKG-INFO +16 -0
  2. airo_robots-2025.4.0/README.md +87 -0
  3. airo_robots-2025.4.0/airo_robots/__init__.py +1 -0
  4. airo_robots-2025.4.0/airo_robots/awaitable_action.py +85 -0
  5. airo_robots-2025.4.0/airo_robots/drives/__init__.py +0 -0
  6. airo_robots-2025.4.0/airo_robots/drives/hardware/__init__.py +0 -0
  7. airo_robots-2025.4.0/airo_robots/drives/hardware/kelo_robile.py +133 -0
  8. airo_robots-2025.4.0/airo_robots/drives/hardware/manual_robot_testing.py +31 -0
  9. airo_robots-2025.4.0/airo_robots/drives/mobile_robot.py +82 -0
  10. airo_robots-2025.4.0/airo_robots/grippers/__init__.py +5 -0
  11. airo_robots-2025.4.0/airo_robots/grippers/hardware/__init__.py +0 -0
  12. airo_robots-2025.4.0/airo_robots/grippers/hardware/manual_gripper_testing.py +32 -0
  13. airo_robots-2025.4.0/airo_robots/grippers/hardware/robotiq_2f85_urcap.py +253 -0
  14. airo_robots-2025.4.0/airo_robots/grippers/hardware/schunk_egk40_usb.py +270 -0
  15. airo_robots-2025.4.0/airo_robots/grippers/parallel_position_gripper.py +94 -0
  16. airo_robots-2025.4.0/airo_robots/hardware_interaction_utils.py +45 -0
  17. airo_robots-2025.4.0/airo_robots/manipulators/__init__.py +2 -0
  18. airo_robots-2025.4.0/airo_robots/manipulators/bimanual_position_manipulator.py +271 -0
  19. airo_robots-2025.4.0/airo_robots/manipulators/force_torque_sensor.py +22 -0
  20. airo_robots-2025.4.0/airo_robots/manipulators/hardware/__init__.py +0 -0
  21. airo_robots-2025.4.0/airo_robots/manipulators/hardware/manual_manipulator_testing.py +101 -0
  22. airo_robots-2025.4.0/airo_robots/manipulators/hardware/ur_rtde.py +253 -0
  23. airo_robots-2025.4.0/airo_robots/manipulators/position_manipulator.py +233 -0
  24. airo_robots-2025.4.0/airo_robots/py.typed +0 -0
  25. airo_robots-2025.4.0/airo_robots.egg-info/PKG-INFO +16 -0
  26. airo_robots-2025.4.0/airo_robots.egg-info/SOURCES.txt +32 -0
  27. airo_robots-2025.4.0/airo_robots.egg-info/dependency_links.txt +1 -0
  28. airo_robots-2025.4.0/airo_robots.egg-info/requires.txt +6 -0
  29. airo_robots-2025.4.0/airo_robots.egg-info/top_level.txt +1 -0
  30. airo_robots-2025.4.0/setup.cfg +4 -0
  31. airo_robots-2025.4.0/setup.py +22 -0
  32. airo_robots-2025.4.0/test/test_async_executor.py +17 -0
  33. airo_robots-2025.4.0/test/test_awaitable_action.py +22 -0
  34. airo_robots-2025.4.0/test/test_parallel_position_gripper.py +75 -0
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: airo_robots
3
+ Version: 2025.4.0
4
+ Summary: Interfaces, hardware implementations of those interfaces and other functionalities to control robot manipulators and grippers at the Ghent University AI and Robotics Lab
5
+ Author: Thomas Lips
6
+ Author-email: thomas.lips@ugent.be
7
+ Requires-Dist: numpy<2.0
8
+ Requires-Dist: ur-rtde>=1.5.7
9
+ Requires-Dist: click
10
+ Requires-Dist: airo-typing==2025.4.0
11
+ Requires-Dist: airo-spatial-algebra==2025.4.0
12
+ Requires-Dist: airo-tulip==0.2.2
13
+ Dynamic: author
14
+ Dynamic: author-email
15
+ Dynamic: requires-dist
16
+ Dynamic: summary
@@ -0,0 +1,87 @@
1
+ # Airo-robots
2
+ This package contains code for controlling robot manipulators and grippers, specifically:
3
+ - base classes (interfaces) for the different hardware types (grippers, manipulators, F/T sensors), i.e. defines the 'driver' interfaces.
4
+ - implementations of those interfaces for some of the hardware used at AIRO.
5
+ - code to manually test these implementations, when the hardware is attached to the computer.
6
+
7
+ The following combinations of hardware and communication options are currently implemented:
8
+ | Hardware | Communication | Implementation |
9
+ |----------|:----------|----------------|
10
+ | UR robots | RTDE | [ur_rtde.py](airo_robots/manipulators/hardware/ur_rtde.py) |
11
+ | Robotiq 2F85 gripper | URCap web API | [robotiq_2f85_urcap.py](airo_robots/grippers/hardware/robotiq_2f85_urcap.py) |
12
+
13
+ Each hardware implementation module will have a `__main__` codeblock that runs the tests for that hardware implementation. This is useful to check if the hardware is connected correctly and the implementation is working as expected. But it is also the place to be to get an idea of how to use the implementation.
14
+
15
+ ## Async interactions
16
+ Certain hardware commands can require some time before completion. Think about sending a move command to a robot, the exact time will depend on the velocity (profile) and the distance between the current pose and target pose, but it can easily be multiple seconds. You might nog want to be busy waiting on that command to complete before the method returns. For this reason, certain commands will send the command to the hardware, formulate a condition that signals when the command has finished and then return an [`AwaitableAction`](airo_robots/awaitable_action.py). You can then decide when (and if) you want to start busy waiting on the command to finish (according to the specified condition). This allows you to do some other useful things in the meantime.
17
+
18
+ When you decide to wait you have to specify the max waiting time (the timeout value) and the granularity of the waiting. Make sure to take a look in the code so that you understand these values and can use appropriate values, especially have a look at the part about the `time.sleep` accuracy. An example interaction might look like this:
19
+
20
+ ```python
21
+ action = robot.move(target_pose)
22
+ # do other stuff, such as computing the next action
23
+ new_action = compute_new_action()
24
+ action.wait(timeout=2,resolution=0.2)
25
+ # send new action
26
+ robot.do_new_action(new_action)
27
+ ```
28
+
29
+ **This is only guaranteed to work when you explicitly wait on a previous command before sending a new command**. If you send multiple commands, it is up to the robot controller to decide in what order the commands are executed (if they are not preempted). At that point, the termination condition in the `AwaitableAction` will probably make no sense anymore and result in a timeout. So make sure you know what you are doing if you send multiple actions and don't use the `wait()` method on them unless you know very well what you are doing.
30
+
31
+ See [below](#notes-on-the-different-types-of-interfaces-for-hardware-interaction) for a lengthier discussion of (async) hardware interactions and why the interface is implemented like this.
32
+ ## Installation
33
+ You can simply pip install this package. Note that some hardware implementations have additional dependencies, which are for now included in the setup.py but might be separated later on.
34
+ ## Structure
35
+ a more detailled overview of the structure and content of this package:
36
+
37
+ ```
38
+ airo_robots/
39
+ manipulators/
40
+ position_manipulator.py # base classes for position-controlled manipulators
41
+ force_torque_sensor.py # base class for FT sensors
42
+ bimanual_position_manipulator.py # base class for bimanual manipulators
43
+ hardware/ # contains the implementations of the inferfaces
44
+ manual_gripper_testing.py # code for manually testing hw implementations
45
+ ur_rtde.py # implementation of the interfaces for UR robots over the RTDE interface
46
+
47
+ grippers/
48
+ parallel_position_gripper.py # base classes for parallel-finger, position-controlled grippers
49
+ hardware/
50
+ manual_gripper_testing.py # code for manually testing hw implementations
51
+ robotiq_2f85_urcap.py # implementations for robotiq_2F85 gripper over the URscript TCP API
52
+ ```
53
+
54
+ ## Adding new hardware
55
+
56
+ ### Adding new hardware implementations
57
+ If an interface already exists for the hardware you want to use, you can simply use that interface and implement the hardware-specific code in a new module under the appriate `hardware/` folder. For the async methods, use an appropriate condition in the Awaitable Action object to signal when the command has finished.
58
+
59
+ Don't forget to add a `__main__` codeblock to the new module that runs the tests for that hardware implementation.
60
+ Also don't forget to add the implementation in the table above and import it in the `__init__.py` of the appropriate submodule.
61
+
62
+
63
+ ### Adding new interfaces
64
+ Best to look at the existing interfaces for inspiration. The main thing to keep in mind is that the interface should be as general as possible, while still being useful. So don't add too many methods that are specific to a single hardware implementation. Methods for which it makes sense should return an Awaitable Action object.
65
+
66
+ Dont forget to add the interface in the `__init__.py` of the appropriate submodule.
67
+ ## Notes on the different types of interfaces for Hardware interaction
68
+
69
+ There are basically three ways to send commands to hardware:
70
+ - send a command and never look back (asynchronous)
71
+ - send a command, continue and later on check if it was succesful (asynchronous + awaitable)
72
+ - send a command and wait for it to finish (synchronous)
73
+
74
+ In ROS these are respectively sending to a topic, sending to an action server and sending + waiting on the action server.
75
+
76
+ A first thing to note is that not all commands have a clear 'termination condition'. E.g. if you send a new waypoint to an admittance controller, when has has the controller succesfully handled your request? There is no clear 'end' as with a MoveL signal that has a target pose and really has to get there.
77
+ So for these commands only the first option is available.
78
+
79
+ Another complication with async commands is about what should happen when you send multiple commands in a row. Should the robot execute them in that order, or should the robot simply take the most recent command and ignore the previous ones? This is a design choice that should be made by the user of the interface. ROS (and most hardware controllers) take the last option, i.e. the robot will execute the most recent command and ignore the previous ones. These would be marked as *preempted* in ROS.
80
+
81
+ To implement the awaitable behavior in python, there are a number of options:
82
+
83
+ 1) use AsyncIO. This might actually be the most pythonic way, but it requires all downstream code to also be async. This is not always possible, e.g. when using this code in ROS. Even when it is possible, it is not desirable imo.
84
+
85
+ 2) Have a custom return object on which you can wait actively when desired. This is the approach we have taken in this package.
86
+
87
+ To signal preemption of commands to the user, you have to manually queue all commands and mark all other commands as preempted when popping the most recent command from the queue. This in turn requires a separate controll process (otherwise your single thread (mind the GIL), would spend lots of times checking if the current command is finished, even if you don't care about that at all). We considered this complexity unjustified and hence do not detect/enforce/control preemptions. It is up to the user to either avoid sending multiple commands or to check how the hardware controllers deals with them. Note that waiting on preempted or stale commands will result in a timeout.
@@ -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
@@ -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
+ )
@@ -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()=}")