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.
- airo_robots-2025.4.0/PKG-INFO +16 -0
- airo_robots-2025.4.0/README.md +87 -0
- airo_robots-2025.4.0/airo_robots/__init__.py +1 -0
- airo_robots-2025.4.0/airo_robots/awaitable_action.py +85 -0
- airo_robots-2025.4.0/airo_robots/drives/__init__.py +0 -0
- airo_robots-2025.4.0/airo_robots/drives/hardware/__init__.py +0 -0
- airo_robots-2025.4.0/airo_robots/drives/hardware/kelo_robile.py +133 -0
- airo_robots-2025.4.0/airo_robots/drives/hardware/manual_robot_testing.py +31 -0
- airo_robots-2025.4.0/airo_robots/drives/mobile_robot.py +82 -0
- airo_robots-2025.4.0/airo_robots/grippers/__init__.py +5 -0
- airo_robots-2025.4.0/airo_robots/grippers/hardware/__init__.py +0 -0
- airo_robots-2025.4.0/airo_robots/grippers/hardware/manual_gripper_testing.py +32 -0
- airo_robots-2025.4.0/airo_robots/grippers/hardware/robotiq_2f85_urcap.py +253 -0
- airo_robots-2025.4.0/airo_robots/grippers/hardware/schunk_egk40_usb.py +270 -0
- airo_robots-2025.4.0/airo_robots/grippers/parallel_position_gripper.py +94 -0
- airo_robots-2025.4.0/airo_robots/hardware_interaction_utils.py +45 -0
- airo_robots-2025.4.0/airo_robots/manipulators/__init__.py +2 -0
- airo_robots-2025.4.0/airo_robots/manipulators/bimanual_position_manipulator.py +271 -0
- airo_robots-2025.4.0/airo_robots/manipulators/force_torque_sensor.py +22 -0
- airo_robots-2025.4.0/airo_robots/manipulators/hardware/__init__.py +0 -0
- airo_robots-2025.4.0/airo_robots/manipulators/hardware/manual_manipulator_testing.py +101 -0
- airo_robots-2025.4.0/airo_robots/manipulators/hardware/ur_rtde.py +253 -0
- airo_robots-2025.4.0/airo_robots/manipulators/position_manipulator.py +233 -0
- airo_robots-2025.4.0/airo_robots/py.typed +0 -0
- airo_robots-2025.4.0/airo_robots.egg-info/PKG-INFO +16 -0
- airo_robots-2025.4.0/airo_robots.egg-info/SOURCES.txt +32 -0
- airo_robots-2025.4.0/airo_robots.egg-info/dependency_links.txt +1 -0
- airo_robots-2025.4.0/airo_robots.egg-info/requires.txt +6 -0
- airo_robots-2025.4.0/airo_robots.egg-info/top_level.txt +1 -0
- airo_robots-2025.4.0/setup.cfg +4 -0
- airo_robots-2025.4.0/setup.py +22 -0
- airo_robots-2025.4.0/test/test_async_executor.py +17 -0
- airo_robots-2025.4.0/test/test_awaitable_action.py +22 -0
- 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
|
|
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."""
|
|
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()=}")
|