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.
Files changed (47) hide show
  1. {crisp_python-1.3.1/crisp_python.egg-info → crisp_python-1.5.0}/PKG-INFO +1 -1
  2. crisp_python-1.5.0/crisp_py/camera/__init__.py +6 -0
  3. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/camera/camera_config.py +0 -13
  4. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/trigger_left.yaml +1 -1
  5. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/control/controller_switcher.py +3 -5
  6. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/robot.py +46 -62
  7. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/robot_config.py +31 -1
  8. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/sensors/sensor.py +39 -12
  9. crisp_python-1.5.0/crisp_py/sensors/sensor_config.py +61 -0
  10. crisp_python-1.5.0/crisp_py/utils/geometry.py +107 -0
  11. {crisp_python-1.3.1 → crisp_python-1.5.0/crisp_python.egg-info}/PKG-INFO +1 -1
  12. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_python.egg-info/SOURCES.txt +1 -0
  13. {crisp_python-1.3.1 → crisp_python-1.5.0}/pyproject.toml +1 -1
  14. crisp_python-1.3.1/crisp_py/camera/__init__.py +0 -6
  15. crisp_python-1.3.1/crisp_py/sensors/sensor_config.py +0 -31
  16. {crisp_python-1.3.1 → crisp_python-1.5.0}/LICENSE.md +0 -0
  17. {crisp_python-1.3.1 → crisp_python-1.5.0}/MANIFEST.in +0 -0
  18. {crisp_python-1.3.1 → crisp_python-1.5.0}/README.md +0 -0
  19. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/__init__.py +0 -0
  20. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/camera/camera.py +0 -0
  21. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/control/clipped_cartesian_impedance.yaml +0 -0
  22. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/control/default_cartesian_impedance.yaml +0 -0
  23. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/control/default_operational_space_controller.yaml +0 -0
  24. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/control/gravity_compensation.yaml +0 -0
  25. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/control/gravity_compensation_on_plane.yaml +0 -0
  26. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/control/joint_control.yaml +0 -0
  27. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/control/joint_velocity_control.yaml +0 -0
  28. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/control/no_friction_cartesian_impedance.yaml +0 -0
  29. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/gripper_franka.yaml +0 -0
  30. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/gripper_left.yaml +0 -0
  31. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/gripper_left_config.yaml +0 -0
  32. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/gripper_right.yaml +0 -0
  33. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/gripper_right_config.yaml +0 -0
  34. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/gripper_uncalibrated.yaml +0 -0
  35. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/config/gripper_with_trigger.yaml +0 -0
  36. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/control/__init__.py +0 -0
  37. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/control/joint_trajectory_controller_client.py +0 -0
  38. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/control/parameters_client.py +0 -0
  39. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/gripper/__init__.py +0 -0
  40. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/gripper/gripper.py +0 -0
  41. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/utils/__init__.py +0 -0
  42. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/utils/callback_monitor.py +0 -0
  43. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_py/utils/diagnostics_viewer.py +0 -0
  44. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_python.egg-info/dependency_links.txt +0 -0
  45. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_python.egg-info/requires.txt +0 -0
  46. {crisp_python-1.3.1 → crisp_python-1.5.0}/crisp_python.egg-info/top_level.txt +0 -0
  47. {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.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
@@ -0,0 +1,6 @@
1
+ """Initialize the camera module."""
2
+
3
+ from crisp_py.camera.camera import Camera
4
+ from crisp_py.camera.camera_config import CameraConfig
5
+
6
+ __import__ = [Camera, CameraConfig]
@@ -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"
@@ -1,4 +1,4 @@
1
- joint_state_topic: gripper_state_broadcaster/joint_states
1
+ joint_state_topic: trigger_state_broadcaster/joint_states
2
2
  max_value: 1.10193625925745
3
3
  min_value: 1.8907875646311676
4
4
  index: 1
@@ -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, node: Node, always_active: list[str] = ["joint_state_broadcaster", "pose_broadcaster"]
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 in self.always_active:
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(self._pose_to_pose_msg(self._target_pose))
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 = self._pose_msg_to_pose(msg)
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, f"Sensor '{self.config.name}'", self.config.max_data_delay
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 TorqueSensor(Sensor):
148
- """Torque sensor that subscribes to JointState messages."""
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 JointState messages."""
150
+ """Create the ROS2 subscription for WrenchStamped messages."""
152
151
  self.node.create_subscription(
153
- JointState,
152
+ WrenchStamped,
154
153
  self.config.data_topic,
155
- self._callback_monitor(self._callback_joint_state),
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 _callback_joint_state(self, msg: JointState):
161
- """Callback for joint state data."""
162
- self._value = np.array(msg.effort, dtype=np.float32)
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.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,6 +1,6 @@
1
1
  [project]
2
2
  name = "crisp-python"
3
- version = "1.3.1"
3
+ version = "1.5.0"
4
4
  description = "Simple python interface to control robots that use crisp_controllers and other ROS2 components."
5
5
  readme = "README.md"
6
6
  license-files = ["LICENSE.md"]
@@ -1,6 +0,0 @@
1
- """Initialize the camera module."""
2
-
3
- from crisp_py.camera.camera import Camera
4
- from crisp_py.camera.camera_config import CameraConfig, FrankaCameraConfig
5
-
6
- __import__ = [Camera, CameraConfig, FrankaCameraConfig]
@@ -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