crisp-python 1.3.1__tar.gz → 1.5.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.
- {crisp_python-1.3.1/crisp_python.egg-info → crisp_python-1.5.0}/PKG-INFO +1 -1
- crisp_python-1.5.0/crisp_py/camera/__init__.py +6 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/camera/camera_config.py +0 -13
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/trigger_left.yaml +1 -1
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/control/controller_switcher.py +3 -5
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/robot.py +46 -62
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/robot_config.py +31 -1
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/sensors/sensor.py +39 -12
- crisp_python-1.5.0/crisp_py/sensors/sensor_config.py +61 -0
- crisp_python-1.5.0/crisp_py/utils/geometry.py +107 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0/crisp_python.egg-info}/PKG-INFO +1 -1
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_python.egg-info/SOURCES.txt +1 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/pyproject.toml +1 -1
- crisp_python-1.3.1/crisp_py/camera/__init__.py +0 -6
- crisp_python-1.3.1/crisp_py/sensors/sensor_config.py +0 -31
- {crisp_python-1.3.1 → crisp_python-1.5.0}/LICENSE.md +0 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/MANIFEST.in +0 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/README.md +0 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/__init__.py +0 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/camera/camera.py +0 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/control/clipped_cartesian_impedance.yaml +0 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/control/default_cartesian_impedance.yaml +0 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/control/default_operational_space_controller.yaml +0 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/control/gravity_compensation.yaml +0 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/control/gravity_compensation_on_plane.yaml +0 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/control/joint_control.yaml +0 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/control/joint_velocity_control.yaml +0 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/control/no_friction_cartesian_impedance.yaml +0 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/gripper_franka.yaml +0 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/gripper_left.yaml +0 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/gripper_left_config.yaml +0 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/gripper_right.yaml +0 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/gripper_right_config.yaml +0 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/gripper_uncalibrated.yaml +0 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/gripper_with_trigger.yaml +0 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/control/__init__.py +0 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/control/joint_trajectory_controller_client.py +0 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/control/parameters_client.py +0 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/gripper/__init__.py +0 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/gripper/gripper.py +0 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/utils/__init__.py +0 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/utils/callback_monitor.py +0 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/utils/diagnostics_viewer.py +0 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_python.egg-info/dependency_links.txt +0 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_python.egg-info/requires.txt +0 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_python.egg-info/top_level.txt +0 -0
- {crisp_python-1.3.1 → crisp_python-1.5.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crisp-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: Simple python interface to control robots that use crisp_controllers and other ROS2 components.
|
|
5
5
|
Project-URL: Homepage, https://utiasDSL.github.io/crisp_controllers/
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -16,16 +16,3 @@ class CameraConfig:
|
|
|
16
16
|
camera_color_info_topic: str | None = None
|
|
17
17
|
|
|
18
18
|
max_image_delay: float = 1.0
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@dataclass
|
|
22
|
-
class FrankaCameraConfig(CameraConfig):
|
|
23
|
-
"""Example camera configuration used with Franka."""
|
|
24
|
-
|
|
25
|
-
camera_name: str = "franka"
|
|
26
|
-
camera_frame: str = "franka_link"
|
|
27
|
-
|
|
28
|
-
resolution: tuple[int, int] | None = (256, 256)
|
|
29
|
-
|
|
30
|
-
camera_color_image_topic: str | None = "franka/color/image_raw"
|
|
31
|
-
camera_color_info_topic: str | None = "franka/color/camera_info"
|
|
@@ -15,18 +15,16 @@ class ControllerSwitcherClient:
|
|
|
15
15
|
"""ControllerSwitcher class allows user to communicate with the controller_manager and manage controllers in an easy way."""
|
|
16
16
|
|
|
17
17
|
def __init__(
|
|
18
|
-
self,
|
|
18
|
+
self,
|
|
19
|
+
node: Node,
|
|
19
20
|
):
|
|
20
21
|
"""Initialize the ControllerSwitcher.
|
|
21
22
|
|
|
22
23
|
Args:
|
|
23
24
|
node (Node): Node used for the communication with the controller_manager.
|
|
24
|
-
always_active (list[str], optional): List of controllers that are always active. Defaults to ["joint_state_broadcaster", "franka_robot_state_broadcaster"].
|
|
25
25
|
"""
|
|
26
26
|
self.node = node
|
|
27
27
|
|
|
28
|
-
self.always_active = always_active
|
|
29
|
-
|
|
30
28
|
self.load_client = node.create_client(
|
|
31
29
|
LoadController,
|
|
32
30
|
"controller_manager/load_controller",
|
|
@@ -160,7 +158,7 @@ class ControllerSwitcherClient:
|
|
|
160
158
|
|
|
161
159
|
to_deactivate = []
|
|
162
160
|
for active_controller in active_controllers:
|
|
163
|
-
if active_controller not
|
|
161
|
+
if not active_controller.endswith("broadcaster"): # Do not deactivate broadcasters
|
|
164
162
|
to_deactivate.append(active_controller)
|
|
165
163
|
|
|
166
164
|
to_activate = [controller_name]
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
"""Provides a client to control the franka robot. It is the easiest way to control the robot using ROS2."""
|
|
2
2
|
|
|
3
3
|
import threading
|
|
4
|
-
from dataclasses import dataclass
|
|
5
4
|
from typing import List
|
|
6
5
|
|
|
7
6
|
import numpy as np
|
|
8
7
|
import rclpy
|
|
9
8
|
import rclpy.executors
|
|
10
|
-
from geometry_msgs.msg import PoseStamped, WrenchStamped
|
|
9
|
+
from geometry_msgs.msg import PoseStamped, TwistStamped, WrenchStamped
|
|
11
10
|
from numpy.typing import NDArray
|
|
12
11
|
from rclpy.callback_groups import ReentrantCallbackGroup
|
|
13
12
|
from rclpy.node import Node
|
|
@@ -20,36 +19,7 @@ from crisp_py.control.joint_trajectory_controller_client import JointTrajectoryC
|
|
|
20
19
|
from crisp_py.control.parameters_client import ParametersClient
|
|
21
20
|
from crisp_py.robot_config import FrankaConfig, RobotConfig
|
|
22
21
|
from crisp_py.utils.callback_monitor import CallbackMonitor
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@dataclass
|
|
26
|
-
class Pose:
|
|
27
|
-
"""Compact representation of an SE3 object."""
|
|
28
|
-
|
|
29
|
-
position: np.ndarray
|
|
30
|
-
orientation: Rotation
|
|
31
|
-
|
|
32
|
-
def copy(self) -> "Pose":
|
|
33
|
-
"""Create a copy of this pose."""
|
|
34
|
-
return Pose(self.position.copy(), Rotation.from_quat(self.orientation.as_quat()))
|
|
35
|
-
|
|
36
|
-
def __str__(self) -> str:
|
|
37
|
-
"""Return a string representation of a Pose."""
|
|
38
|
-
return f"Pos: {np.array2string(self.position, suppress_small=True, precision=2, floatmode='fixed')},\n Orientation: {np.array2string(self.orientation.as_matrix(), suppress_small=True, precision=2, floatmode='fixed')}"
|
|
39
|
-
|
|
40
|
-
def __sub__(self, other: "Pose") -> "Pose":
|
|
41
|
-
"""Subtract another pose from this pose, i.e. compute the relative pose."""
|
|
42
|
-
return Pose(
|
|
43
|
-
self.position - other.position,
|
|
44
|
-
self.orientation * other.orientation.inv(),
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
def __add__(self, other: "Pose") -> "Pose":
|
|
48
|
-
"""Add another pose to this pose, i.e. add a relative pose."""
|
|
49
|
-
return Pose(
|
|
50
|
-
self.position + other.position,
|
|
51
|
-
other.orientation * self.orientation,
|
|
52
|
-
)
|
|
22
|
+
from crisp_py.utils.geometry import Pose, Twist
|
|
53
23
|
|
|
54
24
|
|
|
55
25
|
class Robot:
|
|
@@ -111,6 +81,7 @@ class Robot:
|
|
|
111
81
|
self._current_joint = None
|
|
112
82
|
self._target_joint = None
|
|
113
83
|
self._target_wrench = None
|
|
84
|
+
self._current_twist = None
|
|
114
85
|
|
|
115
86
|
self._callback_monitor = CallbackMonitor(
|
|
116
87
|
node=self.node,
|
|
@@ -145,6 +116,16 @@ class Robot:
|
|
|
145
116
|
callback_group=ReentrantCallbackGroup(),
|
|
146
117
|
)
|
|
147
118
|
|
|
119
|
+
self.node.create_subscription(
|
|
120
|
+
TwistStamped,
|
|
121
|
+
self.config.current_twist_topic,
|
|
122
|
+
self._callback_monitor.monitor(
|
|
123
|
+
f"{namespace.capitalize()} Current Twist", self._callback_current_twist
|
|
124
|
+
),
|
|
125
|
+
qos_profile_sensor_data,
|
|
126
|
+
callback_group=ReentrantCallbackGroup(),
|
|
127
|
+
)
|
|
128
|
+
|
|
148
129
|
self.node.create_timer(
|
|
149
130
|
1.0 / self.config.publish_frequency,
|
|
150
131
|
self._callback_monitor.monitor(
|
|
@@ -237,6 +218,22 @@ class Robot:
|
|
|
237
218
|
)
|
|
238
219
|
return self._target_joint.copy()
|
|
239
220
|
|
|
221
|
+
@property
|
|
222
|
+
def end_effector_twist(self) -> Twist:
|
|
223
|
+
"""Get the current twist of the end effector.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Twist: Current end-effector twist message.
|
|
227
|
+
|
|
228
|
+
Raises:
|
|
229
|
+
RuntimeError: If no twist messages have been received yet.
|
|
230
|
+
"""
|
|
231
|
+
if self._current_twist is None:
|
|
232
|
+
raise RuntimeError(
|
|
233
|
+
f"The robot has not received any twists yet. Is the current_twist_topic {self.config.current_twist_topic} correct?"
|
|
234
|
+
)
|
|
235
|
+
return self._current_twist.copy()
|
|
236
|
+
|
|
240
237
|
def is_ready(self) -> bool:
|
|
241
238
|
"""Check if the robot is ready for operation.
|
|
242
239
|
|
|
@@ -311,7 +308,11 @@ class Robot:
|
|
|
311
308
|
"""
|
|
312
309
|
if self._target_pose is None or not rclpy.ok():
|
|
313
310
|
return
|
|
314
|
-
self._target_pose_publisher.publish(
|
|
311
|
+
self._target_pose_publisher.publish(
|
|
312
|
+
self._target_pose.to_ros_msg(
|
|
313
|
+
self.config.base_frame, self.node.get_clock().now().to_msg()
|
|
314
|
+
)
|
|
315
|
+
)
|
|
315
316
|
|
|
316
317
|
def _callback_publish_target_joint(self):
|
|
317
318
|
"""Publish the current target joint configuration if one exists.
|
|
@@ -384,10 +385,21 @@ class Robot:
|
|
|
384
385
|
Args:
|
|
385
386
|
msg (PoseStamped): ROS message containing the current pose.
|
|
386
387
|
"""
|
|
387
|
-
self._current_pose =
|
|
388
|
+
self._current_pose = Pose.from_ros_msg(msg)
|
|
388
389
|
if self._target_pose is None:
|
|
389
390
|
self._target_pose = self._current_pose.copy()
|
|
390
391
|
|
|
392
|
+
def _callback_current_twist(self, msg: TwistStamped):
|
|
393
|
+
"""Update the current twist from a ROS message.
|
|
394
|
+
|
|
395
|
+
This callback is triggered when a new twist message is received. It updates
|
|
396
|
+
the current twist.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
msg (TwistStamped): ROS message containing the current twist.
|
|
400
|
+
"""
|
|
401
|
+
self._current_twist = Twist.from_ros_msg(msg)
|
|
402
|
+
|
|
391
403
|
def _callback_current_joint(self, msg: JointState):
|
|
392
404
|
"""Update the current joint state from a ROS message.
|
|
393
405
|
|
|
@@ -467,34 +479,6 @@ class Robot:
|
|
|
467
479
|
# if switch_to_default_controller:
|
|
468
480
|
# self.controller_switcher_client.switch_controller(self.config.default_controller)
|
|
469
481
|
|
|
470
|
-
def _pose_msg_to_pose(self, pose: PoseStamped) -> Pose:
|
|
471
|
-
"""Convert a ROS2 pose msg to a pose."""
|
|
472
|
-
position = np.array([pose.pose.position.x, pose.pose.position.y, pose.pose.position.z])
|
|
473
|
-
orientation = Rotation.from_quat(
|
|
474
|
-
[
|
|
475
|
-
pose.pose.orientation.x,
|
|
476
|
-
pose.pose.orientation.y,
|
|
477
|
-
pose.pose.orientation.z,
|
|
478
|
-
pose.pose.orientation.w,
|
|
479
|
-
]
|
|
480
|
-
)
|
|
481
|
-
return Pose(position, orientation)
|
|
482
|
-
|
|
483
|
-
def _pose_to_pose_msg(self, pose: Pose) -> PoseStamped:
|
|
484
|
-
"""Convert a pose to a pose message."""
|
|
485
|
-
msg = PoseStamped()
|
|
486
|
-
msg.header.frame_id = self.config.base_frame
|
|
487
|
-
msg.header.stamp = self.node.get_clock().now().to_msg()
|
|
488
|
-
msg.pose.position.x, msg.pose.position.y, msg.pose.position.z = pose.position
|
|
489
|
-
q = pose.orientation.as_quat()
|
|
490
|
-
(
|
|
491
|
-
msg.pose.orientation.x,
|
|
492
|
-
msg.pose.orientation.y,
|
|
493
|
-
msg.pose.orientation.z,
|
|
494
|
-
msg.pose.orientation.w,
|
|
495
|
-
) = q
|
|
496
|
-
return msg
|
|
497
|
-
|
|
498
482
|
def _joint_to_joint_msg(
|
|
499
483
|
self, q: NDArray, dq: NDArray | None = None, tau: NDArray | None = None
|
|
500
484
|
) -> JointState:
|
|
@@ -5,7 +5,7 @@ from dataclasses import dataclass, field
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
@dataclass
|
|
8
|
+
@dataclass(kw_only=True)
|
|
9
9
|
class RobotConfig:
|
|
10
10
|
"""Configuration class for robot parameters.
|
|
11
11
|
|
|
@@ -41,6 +41,7 @@ class RobotConfig:
|
|
|
41
41
|
target_joint_topic: str = "target_joint"
|
|
42
42
|
current_pose_topic: str = "current_pose"
|
|
43
43
|
current_joint_topic: str = "joint_states"
|
|
44
|
+
current_twist_topic: str = "current_twist"
|
|
44
45
|
|
|
45
46
|
publish_frequency: float = 50.0
|
|
46
47
|
time_to_home: float = 5.0
|
|
@@ -184,3 +185,32 @@ class SO101Config(RobotConfig):
|
|
|
184
185
|
)
|
|
185
186
|
base_frame: str = "Base"
|
|
186
187
|
target_frame: str = "Fixed_Gripper"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def make_robot_config(robot_type: str, **kwargs) -> RobotConfig: # noqa: ANN003
|
|
191
|
+
"""Factory function to create robot configuration objects.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
robot_type (str): Type of robot ('franka', 'kinova', 'iiwa', 'so101')
|
|
195
|
+
**kwargs: Additional keyword arguments to override default configuration
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
RobotConfig: Configured robot configuration object
|
|
199
|
+
|
|
200
|
+
Raises:
|
|
201
|
+
ValueError: If robot_type is not supported
|
|
202
|
+
"""
|
|
203
|
+
robot_type = robot_type.lower()
|
|
204
|
+
|
|
205
|
+
if robot_type == "franka":
|
|
206
|
+
return FrankaConfig(**kwargs)
|
|
207
|
+
elif robot_type == "kinova":
|
|
208
|
+
return KinovaConfig(**kwargs)
|
|
209
|
+
elif robot_type == "iiwa":
|
|
210
|
+
return IiwaConfig(**kwargs)
|
|
211
|
+
elif robot_type == "so101":
|
|
212
|
+
return SO101Config(**kwargs)
|
|
213
|
+
else:
|
|
214
|
+
raise ValueError(
|
|
215
|
+
f"Unsupported robot type: {robot_type}. Supported types: franka, kinova, iiwa, so101"
|
|
216
|
+
)
|
|
@@ -5,11 +5,11 @@ from abc import ABC, abstractmethod
|
|
|
5
5
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
import rclpy
|
|
8
|
+
from geometry_msgs.msg import WrenchStamped
|
|
8
9
|
from rclpy.callback_groups import ReentrantCallbackGroup
|
|
9
10
|
from rclpy.executors import MultiThreadedExecutor, SingleThreadedExecutor
|
|
10
11
|
from rclpy.node import Node
|
|
11
12
|
from rclpy.qos import qos_profile_sensor_data
|
|
12
|
-
from sensor_msgs.msg import JointState
|
|
13
13
|
from std_msgs.msg import Float32MultiArray
|
|
14
14
|
|
|
15
15
|
from crisp_py.sensors.sensor_config import SensorConfig
|
|
@@ -54,7 +54,7 @@ class Sensor(ABC):
|
|
|
54
54
|
self._value: np.ndarray | None = None
|
|
55
55
|
self._baseline: np.ndarray | None = None
|
|
56
56
|
self._callback_monitor = CallbackMonitor(
|
|
57
|
-
self.node,
|
|
57
|
+
self.node, stale_threshold=self.config.max_data_delay
|
|
58
58
|
)
|
|
59
59
|
|
|
60
60
|
self._create_subscription()
|
|
@@ -72,7 +72,6 @@ class Sensor(ABC):
|
|
|
72
72
|
"""Get the latest calibrated sensor value."""
|
|
73
73
|
if self._value is None or self._baseline is None:
|
|
74
74
|
raise ValueError("Sensor value is not available yet.")
|
|
75
|
-
self._callback_monitor.check_callback_health()
|
|
76
75
|
return self._value - self._baseline
|
|
77
76
|
|
|
78
77
|
def _spin_node(self):
|
|
@@ -132,7 +131,7 @@ class Float32ArraySensor(Sensor):
|
|
|
132
131
|
self.node.create_subscription(
|
|
133
132
|
Float32MultiArray,
|
|
134
133
|
self.config.data_topic,
|
|
135
|
-
self._callback_monitor.monitor(self._callback_sensor_data),
|
|
134
|
+
self._callback_monitor.monitor(name="float32", func=self._callback_sensor_data),
|
|
136
135
|
qos_profile_sensor_data,
|
|
137
136
|
callback_group=ReentrantCallbackGroup(),
|
|
138
137
|
)
|
|
@@ -144,21 +143,49 @@ class Float32ArraySensor(Sensor):
|
|
|
144
143
|
self._baseline = np.zeros_like(self._value)
|
|
145
144
|
|
|
146
145
|
|
|
147
|
-
class
|
|
148
|
-
"""Torque sensor that subscribes to
|
|
146
|
+
class ForceTorqueSensor(Sensor):
|
|
147
|
+
"""Torque sensor that subscribes to WrenchStamped messages."""
|
|
149
148
|
|
|
150
149
|
def _create_subscription(self):
|
|
151
|
-
"""Create the ROS2 subscription for
|
|
150
|
+
"""Create the ROS2 subscription for WrenchStamped messages."""
|
|
152
151
|
self.node.create_subscription(
|
|
153
|
-
|
|
152
|
+
WrenchStamped,
|
|
154
153
|
self.config.data_topic,
|
|
155
|
-
self._callback_monitor(self.
|
|
154
|
+
self._callback_monitor.monitor(name="force_torque_monitor", func=self._callback_wrench),
|
|
156
155
|
qos_profile_sensor_data,
|
|
157
156
|
callback_group=ReentrantCallbackGroup(),
|
|
158
157
|
)
|
|
159
158
|
|
|
160
|
-
def
|
|
161
|
-
"""Callback for
|
|
162
|
-
self._value = np.array(
|
|
159
|
+
def _callback_wrench(self, msg: WrenchStamped):
|
|
160
|
+
"""Callback for wrench data."""
|
|
161
|
+
self._value = np.array(
|
|
162
|
+
[
|
|
163
|
+
msg.wrench.force.x,
|
|
164
|
+
msg.wrench.force.y,
|
|
165
|
+
msg.wrench.force.z,
|
|
166
|
+
msg.wrench.torque.x,
|
|
167
|
+
msg.wrench.torque.y,
|
|
168
|
+
msg.wrench.torque.z,
|
|
169
|
+
],
|
|
170
|
+
dtype=np.float32,
|
|
171
|
+
)
|
|
163
172
|
if self._baseline is None:
|
|
164
173
|
self._baseline = np.zeros_like(self._value)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def make_sensor(
|
|
177
|
+
sensor_config: SensorConfig,
|
|
178
|
+
**kwargs, # noqa: ANN003
|
|
179
|
+
) -> Sensor:
|
|
180
|
+
"""Factory function to create a sensor based on the configuration."""
|
|
181
|
+
if sensor_config.sensor_type == "float32":
|
|
182
|
+
return Float32ArraySensor(
|
|
183
|
+
sensor_config=sensor_config,
|
|
184
|
+
**kwargs,
|
|
185
|
+
)
|
|
186
|
+
elif sensor_config.sensor_type == "force_torque":
|
|
187
|
+
return ForceTorqueSensor(
|
|
188
|
+
sensor_config=sensor_config,
|
|
189
|
+
**kwargs,
|
|
190
|
+
)
|
|
191
|
+
raise ValueError(f"Unknown sensor type: {sensor_config.sensor_type}")
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Sensor configuration classes for sensor objects."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(kw_only=True)
|
|
8
|
+
class SensorConfig:
|
|
9
|
+
"""Default sensor configuration."""
|
|
10
|
+
|
|
11
|
+
shape: tuple[int, ...]
|
|
12
|
+
sensor_type: Literal["empty", "float32", "force_torque"]
|
|
13
|
+
|
|
14
|
+
name: str = "sensor"
|
|
15
|
+
data_topic: str = "sensor_data"
|
|
16
|
+
max_data_delay: float = 1.0
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class EmptySensorConfig(SensorConfig):
|
|
21
|
+
"""Configuration for an empty sensor, used when using no sensor."""
|
|
22
|
+
|
|
23
|
+
shape: tuple[int, ...] = (0,)
|
|
24
|
+
sensor_type: Literal["empty", "float32", "force_torque"] = "empty"
|
|
25
|
+
|
|
26
|
+
name: str = "empty_sensor"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class AnySkinSensorConfig(SensorConfig):
|
|
31
|
+
"""Configuration for the AnySkin sensor broadcasted by a node using the anyskin_ros2 wrapper: https://github.com/danielsanjosepro/anyskin_ros2."""
|
|
32
|
+
|
|
33
|
+
shape: tuple[int, ...] = (5,)
|
|
34
|
+
name: str = "anyskin_sensor"
|
|
35
|
+
data_topic: str = "tactile_data"
|
|
36
|
+
sensor_type: Literal["empty", "float32", "force_torque"] = "float32"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class ForceTorqueSensorConfig(SensorConfig):
|
|
41
|
+
"""Configuration for a force-torque (i.e. wrench) sensor."""
|
|
42
|
+
|
|
43
|
+
shape: tuple[int, ...] = (6,)
|
|
44
|
+
name: str = "ft_sensor"
|
|
45
|
+
data_topic: str = "external_wrench"
|
|
46
|
+
sensor_type: Literal["empty", "float32", "force_torque"] = "force_torque"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def make_sensor_config(
|
|
50
|
+
sensor_type: Literal["empty", "float32", "force_torque"],
|
|
51
|
+
**kwargs, # noqa: ANN003
|
|
52
|
+
) -> SensorConfig:
|
|
53
|
+
"""Factory function to create a sensor configuration based on the type."""
|
|
54
|
+
if sensor_type == "empty":
|
|
55
|
+
return EmptySensorConfig(**kwargs)
|
|
56
|
+
elif sensor_type == "float32":
|
|
57
|
+
return AnySkinSensorConfig(**kwargs)
|
|
58
|
+
elif sensor_type == "force_torque":
|
|
59
|
+
return ForceTorqueSensorConfig(**kwargs)
|
|
60
|
+
else:
|
|
61
|
+
raise ValueError(f"Unknown sensor type: {sensor_type}")
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Utility functions for geometry operations."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from builtin_interfaces.msg import Time as TimeMsg
|
|
7
|
+
from geometry_msgs.msg import PoseStamped, TwistStamped
|
|
8
|
+
from scipy.spatial.transform import Rotation
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Pose:
|
|
13
|
+
"""Compact representation of an SE3 object."""
|
|
14
|
+
|
|
15
|
+
position: np.ndarray
|
|
16
|
+
orientation: Rotation
|
|
17
|
+
|
|
18
|
+
def copy(self) -> "Pose":
|
|
19
|
+
"""Create a copy of this pose."""
|
|
20
|
+
return Pose(self.position.copy(), Rotation.from_quat(self.orientation.as_quat()))
|
|
21
|
+
|
|
22
|
+
def __str__(self) -> str:
|
|
23
|
+
"""Return a string representation of a Pose."""
|
|
24
|
+
return f"Pos: {np.array2string(self.position, suppress_small=True, precision=2, floatmode='fixed')},\n Orientation: {np.array2string(self.orientation.as_matrix(), suppress_small=True, precision=2, floatmode='fixed')}"
|
|
25
|
+
|
|
26
|
+
def __sub__(self, other: "Pose") -> "Pose":
|
|
27
|
+
"""Subtract another pose from this pose, i.e. compute the relative pose."""
|
|
28
|
+
return Pose(
|
|
29
|
+
self.position - other.position,
|
|
30
|
+
self.orientation * other.orientation.inv(),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def __add__(self, other: "Pose") -> "Pose":
|
|
34
|
+
"""Add another pose to this pose, i.e. add a relative pose."""
|
|
35
|
+
return Pose(
|
|
36
|
+
self.position + other.position,
|
|
37
|
+
other.orientation * self.orientation,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def from_ros_msg(cls, msg: PoseStamped) -> "Pose":
|
|
42
|
+
"""Create Pose from ROS PoseStamped message."""
|
|
43
|
+
position = np.array([msg.pose.position.x, msg.pose.position.y, msg.pose.position.z])
|
|
44
|
+
orientation = Rotation.from_quat(
|
|
45
|
+
[
|
|
46
|
+
msg.pose.orientation.x,
|
|
47
|
+
msg.pose.orientation.y,
|
|
48
|
+
msg.pose.orientation.z,
|
|
49
|
+
msg.pose.orientation.w,
|
|
50
|
+
]
|
|
51
|
+
)
|
|
52
|
+
return cls(position, orientation)
|
|
53
|
+
|
|
54
|
+
def to_ros_msg(self, frame_id: str, stamp: TimeMsg) -> PoseStamped:
|
|
55
|
+
"""Convert Pose to ROS PoseStamped message."""
|
|
56
|
+
msg = PoseStamped()
|
|
57
|
+
msg.header.frame_id = frame_id
|
|
58
|
+
msg.header.stamp = stamp
|
|
59
|
+
msg.pose.position.x, msg.pose.position.y, msg.pose.position.z = self.position
|
|
60
|
+
q = self.orientation.as_quat()
|
|
61
|
+
(
|
|
62
|
+
msg.pose.orientation.x,
|
|
63
|
+
msg.pose.orientation.y,
|
|
64
|
+
msg.pose.orientation.z,
|
|
65
|
+
msg.pose.orientation.w,
|
|
66
|
+
) = q
|
|
67
|
+
return msg
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class Twist:
|
|
72
|
+
"""Compact representation of a twist (linear and angular velocity)."""
|
|
73
|
+
|
|
74
|
+
linear: np.ndarray
|
|
75
|
+
angular: np.ndarray
|
|
76
|
+
|
|
77
|
+
def copy(self) -> "Twist":
|
|
78
|
+
"""Create a copy of this twist."""
|
|
79
|
+
return Twist(self.linear.copy(), self.angular.copy())
|
|
80
|
+
|
|
81
|
+
def __str__(self) -> str:
|
|
82
|
+
"""Return a string representation of a Twist."""
|
|
83
|
+
return f"Linear: {np.array2string(self.linear, suppress_small=True, precision=2, floatmode='fixed')},\n Angular: {np.array2string(self.angular, suppress_small=True, precision=2, floatmode='fixed')}"
|
|
84
|
+
|
|
85
|
+
def magnitude(self) -> float:
|
|
86
|
+
"""Return the magnitude of the linear velocity."""
|
|
87
|
+
return np.linalg.norm(self.linear).astype(float)
|
|
88
|
+
|
|
89
|
+
def angular_magnitude(self) -> float:
|
|
90
|
+
"""Return the magnitude of the angular velocity."""
|
|
91
|
+
return np.linalg.norm(self.angular).astype(float)
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def from_ros_msg(cls, msg: TwistStamped) -> "Twist":
|
|
95
|
+
"""Create Twist from ROS TwistStamped message."""
|
|
96
|
+
linear = np.array([msg.twist.linear.x, msg.twist.linear.y, msg.twist.linear.z])
|
|
97
|
+
angular = np.array([msg.twist.angular.x, msg.twist.angular.y, msg.twist.angular.z])
|
|
98
|
+
return cls(linear, angular)
|
|
99
|
+
|
|
100
|
+
def to_ros_msg(self, frame_id: str, stamp: TimeMsg) -> TwistStamped:
|
|
101
|
+
"""Convert Twist to ROS TwistStamped message."""
|
|
102
|
+
msg = TwistStamped()
|
|
103
|
+
msg.header.frame_id = frame_id
|
|
104
|
+
msg.header.stamp = stamp
|
|
105
|
+
msg.twist.linear.x, msg.twist.linear.y, msg.twist.linear.z = self.linear
|
|
106
|
+
msg.twist.angular.x, msg.twist.angular.y, msg.twist.angular.z = self.angular
|
|
107
|
+
return msg
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crisp-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: Simple python interface to control robots that use crisp_controllers and other ROS2 components.
|
|
5
5
|
Project-URL: Homepage, https://utiasDSL.github.io/crisp_controllers/
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -35,6 +35,7 @@ crisp_py/sensors/sensor_config.py
|
|
|
35
35
|
crisp_py/utils/__init__.py
|
|
36
36
|
crisp_py/utils/callback_monitor.py
|
|
37
37
|
crisp_py/utils/diagnostics_viewer.py
|
|
38
|
+
crisp_py/utils/geometry.py
|
|
38
39
|
crisp_python.egg-info/PKG-INFO
|
|
39
40
|
crisp_python.egg-info/SOURCES.txt
|
|
40
41
|
crisp_python.egg-info/dependency_links.txt
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
"""Sensor configuration classes for sensor objects."""
|
|
2
|
-
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
@dataclass
|
|
7
|
-
class SensorConfig:
|
|
8
|
-
"""Default sensor configuration."""
|
|
9
|
-
|
|
10
|
-
shape: tuple[int, ...]
|
|
11
|
-
|
|
12
|
-
name: str = "sensor"
|
|
13
|
-
data_topic: str = "sensor_data"
|
|
14
|
-
max_data_delay: float = 1.0
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@dataclass
|
|
18
|
-
class EmptySensorConfig(SensorConfig):
|
|
19
|
-
"""Configuration for an empty sensor, used when using no sensor."""
|
|
20
|
-
|
|
21
|
-
shape: tuple[int, ...] = (0,)
|
|
22
|
-
name: str = "empty_sensor"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@dataclass
|
|
26
|
-
class AnySkinSensorConfig(SensorConfig):
|
|
27
|
-
"""Configuration for the AnySkin sensor broadcasted by a node using the anyskin_ros2 wrapper: https://github.com/danielsanjosepro/anyskin_ros2."""
|
|
28
|
-
|
|
29
|
-
shape: tuple[int, ...] = (5,)
|
|
30
|
-
name: str = "anyskin_sensor"
|
|
31
|
-
data_topic: str = "tactile_data"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/control/clipped_cartesian_impedance.yaml
RENAMED
|
File without changes
|
{crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/control/default_cartesian_impedance.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/control/gravity_compensation_on_plane.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/control/joint_velocity_control.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/control/joint_trajectory_controller_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|