Skip to content

Commit 602d475

Browse files
authored
306 - Add support for other links and orientations. (#309)
* Add support for other links and orientations. Signed-off-by: Jelmer de Wolde <[email protected]> * Create consistent orientations using Scipy transformations. Signed-off-by: Jelmer de Wolde <[email protected]> * Add configuration of separate Panther and Franka. Signed-off-by: Jelmer de Wolde <[email protected]> * Use default GazeboSimSystem for Panther to avoid crashes when spawning multiple robots in Gazebo. Signed-off-by: Jelmer de Wolde <[email protected]> * Fix tests. Signed-off-by: Jelmer de Wolde <[email protected]> * Fix ruff. Signed-off-by: Jelmer de Wolde <[email protected]> * Small improvements. Signed-off-by: Jelmer de Wolde <[email protected]> * Add orientation to GPS platform. Signed-off-by: Jelmer de Wolde <[email protected]> * Remove unnecessary if statement. Signed-off-by: Jelmer de Wolde <[email protected]> * Update documentation. Signed-off-by: Jelmer de Wolde <[email protected]> --------- Signed-off-by: Jelmer de Wolde <[email protected]>
1 parent 2365a94 commit 602d475

File tree

24 files changed

+647
-134
lines changed

24 files changed

+647
-134
lines changed

.github/workflows/reusable-workspace.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ jobs:
6060
--entrypoint /bin/bash "${{ inputs.container-image }}" -i -c "
6161
source /home/rcdt/.bashrc &&
6262
source install/setup.bash &&
63-
uv run pytest -s src/
63+
uv run pytest -s -rsxf src/
6464
"
6565
6666
- name: Show disk usage after job

docs/content/system.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ When we directly launch this file without passing a desired configuration, the s
2020

2121
### Modular Configuration
2222

23-
Each platform class will be initialized by at least defining the specific **platform** type. For example, for a camera we support the types *realsense* or *Zed*, related to the different camera types we support. Furthermore we need to specify the **position** of the platform. Next, we can choose to pass a desired **namespace** for the platform, but when not given, a unique namespace will be defined automatically. Finally, we can pass another created platform as a **parent**, where our system will automatically make the required links to combine the two platforms. Some platforms have additional parameters the can be used during initialization. For example: we can define whether we like to use MoveIt or Navigation for an arm or a vehicle respectively.
23+
Each platform class will be initialized by at least defining the specific **platform** type. For example, for a camera we support the types *realsense* or *Zed*, related to the different camera types we support. Furthermore we need to specify the **position** of the platform. Optionally, we can also specify an **orientation** as `[roll, pitch, yaw]` in degrees. Next, we can choose to pass a desired **namespace** for the platform, but when not given, a unique namespace will be defined automatically. Finally, we can pass another created platform as a **parent**, where our system will automatically make the required links to combine the two platforms. Some platforms have additional parameters the can be used during initialization. For example: we can define whether we like to use MoveIt or Navigation for an arm or a vehicle respectively.
2424

2525
### Nodes Started
2626

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ dependencies = [
3131
"pyrealsense2>=2.56.5.9235",
3232
"pytest<8.4.0",
3333
"pytest-timeout>=2.4.0",
34+
"scipy>=1.16.2",
35+
"scipy-stubs>=1.16.2.3",
3436
"simplejpeg>=1.8.2",
3537
"sphinx>=8.1.3",
3638
"sphinx-autobuild>=2024.10.3",

ros2_ws/src/rcdt_franka/urdf/franka.urdf.xacro

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,13 @@ SPDX-License-Identifier: Apache-2.0
4040
parent="$(arg parent)"
4141
/>
4242

43-
<!-- Create detachable joints between base_link and childs: -->
43+
<!-- Preserve joint so that child link can be used for detachable joints: -->
44+
<gazebo reference="fr3_hand_joint">
45+
<preserveFixedJoint>true</preserveFixedJoint>
46+
</gazebo>
47+
48+
<!-- Create detachable joints with given childs: -->
4449
<xacro:include filename="$(find rcdt_utilities)/urdf/detachable_joints.urdf.xacro" />
45-
<xacro:detachable_joints parent_link="fr3_link0" childs="${$(arg childs)}" />
50+
<xacro:detachable_joints childs="${$(arg childs)}" />
4651

4752
</robot>

ros2_ws/src/rcdt_gazebo/CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ project(rcdt_gazebo)
99
find_package(ament_cmake REQUIRED)
1010
find_package(ament_cmake_python REQUIRED)
1111

12+
# Python executables:
13+
install(
14+
DIRECTORY src_py/
15+
DESTINATION lib/${PROJECT_NAME}
16+
)
17+
1218
# Python package:
1319
ament_python_install_package(${PROJECT_NAME})
1420

ros2_ws/src/rcdt_gazebo/launch/gazebo_robot.launch.py

Lines changed: 27 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@
1414

1515
load_gazebo_ui_arg = LaunchArgument("load_gazebo_ui", False, [True, False])
1616
world_arg = LaunchArgument("world", "walls.sdf")
17-
robots_arg = LaunchArgument("robots", "")
17+
platforms_arg = LaunchArgument("platforms", "")
1818
positions_arg = LaunchArgument("positions", "")
19+
orientations_arg = LaunchArgument("orientations", "")
20+
parents_arg = LaunchArgument("parents", "")
21+
parent_links_arg = LaunchArgument("parent_links", "")
1922
bridge_topics_arg = LaunchArgument("bridge_topics", "")
2023

2124

2225
def launch_setup(context: LaunchContext) -> list:
23-
"""Setup the launch description for the Gazebo simulation with robots.
26+
"""Setup the launch description for the Gazebo simulation with platforms.
2427
2528
Args:
2629
context (LaunchContext): The launch context.
@@ -33,8 +36,11 @@ def launch_setup(context: LaunchContext) -> list:
3336
"""
3437
load_gazebo_ui = load_gazebo_ui_arg.bool_value(context)
3538
world = world_arg.string_value(context)
36-
robots = robots_arg.string_value(context).split()
37-
positions = positions_arg.string_value(context).split()
39+
platforms = platforms_arg.string_value(context)
40+
positions = positions_arg.string_value(context)
41+
orientations = orientations_arg.string_value(context)
42+
parents = parents_arg.string_value(context)
43+
parent_links = parent_links_arg.string_value(context)
3844
bridge_topics = bridge_topics_arg.string_value(context).split()
3945

4046
sdf_file = get_file_path("rcdt_gazebo", ["worlds"], world)
@@ -60,28 +66,20 @@ def launch_setup(context: LaunchContext) -> list:
6066
arguments=bridge_topics,
6167
)
6268

63-
spawn_robots: list[Node] = []
64-
for robot, position in zip(robots, positions, strict=False):
65-
namespace = "" if not robot else f"/{robot}"
66-
x, y, z = position.split(",")
67-
spawn_robot = Node(
68-
package="ros_gz_sim",
69-
executable="create",
70-
arguments=[
71-
"-topic",
72-
f"{namespace}/robot_description",
73-
"-name",
74-
robot,
75-
"-x",
76-
str(x),
77-
"-y",
78-
str(y),
79-
"-z",
80-
str(z),
81-
],
82-
output="screen",
83-
)
84-
spawn_robots.append(spawn_robot)
69+
spawn_platforms = Node(
70+
package="rcdt_gazebo",
71+
executable="spawn_platforms.py",
72+
parameters=[
73+
{
74+
"platforms": platforms,
75+
"positions": positions,
76+
"orientations": orientations,
77+
"parents": parents,
78+
"parent_links": parent_links,
79+
},
80+
],
81+
output="screen",
82+
)
8583

8684
unpause_sim = ExecuteProcess(
8785
cmd=[
@@ -108,13 +106,13 @@ def launch_setup(context: LaunchContext) -> list:
108106
"Creating GZ->ROS Bridge: [/clock (gz.msgs.Clock) -> /clock (rosgraph_msgs/msg/Clock)]",
109107
context,
110108
),
111-
*[Register.on_exit(spawn_robot, context) for spawn_robot in spawn_robots],
109+
Register.on_log(spawn_platforms, "All platforms spawned!", context),
112110
Register.on_start(unpause_sim, context),
113111
]
114112

115113

116114
def generate_launch_description() -> LaunchDescription:
117-
"""Generate the launch description for the Gazebo simulation with robots.
115+
"""Generate the launch description for the Gazebo simulation with platforms.
118116
119117
Returns:
120118
LaunchDescription: The launch description containing the actions to be executed.
@@ -123,7 +121,7 @@ def generate_launch_description() -> LaunchDescription:
123121
[
124122
load_gazebo_ui_arg.declaration,
125123
world_arg.declaration,
126-
robots_arg.declaration,
124+
platforms_arg.declaration,
127125
positions_arg.declaration,
128126
bridge_topics_arg.declaration,
129127
OpaqueFunction(function=launch_setup),
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
#!/usr/bin/env python3
2+
# SPDX-FileCopyrightText: Alliander N. V.
3+
#
4+
# SPDX-License-Identifier: Apache-2.0
5+
6+
import re
7+
import subprocess
8+
9+
import numpy as np
10+
import rclpy
11+
from rclpy.node import Node
12+
from scipy.spatial.transform import RigidTransform, Rotation
13+
14+
15+
class SpawnPlatform(Node):
16+
"""Node to spawn platforms in the Gazebo simulation."""
17+
18+
def __init__(self):
19+
"""Initialize the node."""
20+
super().__init__("spawn_platforms")
21+
self.declare_parameter("platforms", "")
22+
self.declare_parameter("positions", "")
23+
self.declare_parameter("orientations", "")
24+
self.declare_parameter("parents", "")
25+
self.declare_parameter("parent_links", "")
26+
platforms = (
27+
self.get_parameter("platforms").get_parameter_value().string_value.split()
28+
)
29+
positions = (
30+
self.get_parameter("positions").get_parameter_value().string_value.split()
31+
)
32+
orientations = (
33+
self.get_parameter("orientations")
34+
.get_parameter_value()
35+
.string_value.split()
36+
)
37+
parents = (
38+
self.get_parameter("parents").get_parameter_value().string_value.split()
39+
)
40+
parent_links = (
41+
self.get_parameter("parent_links")
42+
.get_parameter_value()
43+
.string_value.split()
44+
)
45+
46+
self.get_logger().info("Spawning platforms...")
47+
self.spawn_platforms(platforms, positions, orientations, parents, parent_links)
48+
self.get_logger().info("All platforms spawned!")
49+
50+
def spawn_platforms(
51+
self,
52+
platforms: list[str],
53+
positions: list[str],
54+
orientations: list[str],
55+
parents: list[str],
56+
parent_links: list[str],
57+
) -> None:
58+
"""Spawn platforms in the Gazebo simulation at specified positions.
59+
60+
Args:
61+
platforms (list[str]): List of the platforms.
62+
positions (list[str]): List of positions in the format "x,y,z".
63+
orientations (list[str]): List of orientations in the format "roll,pitch,yaw".
64+
parents (list[str]): List of parent names or "none" when the platform has no parent.
65+
parent_links (list[str]): List of parent link names or "none" when the platform has no parent.
66+
67+
"""
68+
for n in range(len(platforms)):
69+
platform = platforms[n]
70+
position = np.array(list(map(float, positions[n].split(","))))
71+
orientation = np.array(list(map(float, orientations[n].split(","))))
72+
parent = parents[n]
73+
parent_link = parent_links[n]
74+
75+
if parent != "none":
76+
# First define the transform from world to model:
77+
model_pose = get_pose(parent)
78+
model_tf = RigidTransform.from_components(
79+
model_pose["position"],
80+
Rotation.from_euler("xyz", model_pose["orientation"]),
81+
)
82+
83+
# Next define the transform from model to link:
84+
link_pose = get_pose(parent, parent_link)
85+
link_tf = RigidTransform.from_components(
86+
link_pose["position"],
87+
Rotation.from_euler("xyz", link_pose["orientation"]),
88+
)
89+
90+
# Finally, combine the transforms and apply the given position and orientation:
91+
tf = model_tf * link_tf
92+
position = tf.apply(position)
93+
rotation = tf.rotation * Rotation.from_euler("xyz", orientation)
94+
orientation = rotation.as_euler("xyz")
95+
96+
self.spawn_platform(platform, position, orientation)
97+
98+
def spawn_platform(
99+
self, namespace: str, position: np.ndarray, orientation: np.ndarray
100+
) -> None:
101+
"""Spawn a platform in the Gazebo simulation with a specified position and orientation.
102+
103+
Args:
104+
namespace (str): The namespace of the platform.
105+
position (np.ndarray): The position [x, y, z] of the platform.
106+
orientation (np.ndarray): The orientation [roll, pitch, yaw] of the platform.
107+
"""
108+
self.get_logger().info(f"Spawn: {namespace} {position} {orientation}")
109+
x, y, z = position
110+
roll, pitch, yaw = orientation
111+
subprocess.run(
112+
[
113+
"ros2",
114+
"run",
115+
"ros_gz_sim",
116+
"create",
117+
"-topic",
118+
f"/{namespace}/robot_description",
119+
"-name",
120+
namespace,
121+
"-x",
122+
str(x),
123+
"-y",
124+
str(y),
125+
"-z",
126+
str(z),
127+
"-R",
128+
str(roll),
129+
"-P",
130+
str(pitch),
131+
"-Y",
132+
str(yaw),
133+
],
134+
check=True,
135+
)
136+
137+
138+
def get_pose(model: str, link: str | None = None) -> dict:
139+
"""Get the pose of a model or a specific link of a model in the Gazebo simulation.
140+
141+
Args:
142+
model (str): The name of the model.
143+
link (str | None): The name of the link.
144+
145+
Returns:
146+
dict: A dictionary with the position and orientation of the link.
147+
148+
Raises:
149+
RuntimeError: If the link info could not be retrieved or parsed.
150+
"""
151+
command = ["gz", "model", "-m", model]
152+
if link:
153+
command.extend(["-l", link])
154+
message = subprocess.check_output(command, stderr=subprocess.DEVNULL).decode(
155+
"utf-8"
156+
)
157+
158+
lines = message.splitlines()
159+
line_of_interest = None
160+
for n, line in enumerate(lines):
161+
if line == " - Pose [ XYZ (m) ] [ RPY (rad) ]:":
162+
if line_of_interest is not None:
163+
raise RuntimeError("Found multiple lines containing pose information.")
164+
line_of_interest = n
165+
if line_of_interest is None:
166+
raise RuntimeError(f"Could not find pose information for {model} - {link}.")
167+
position = lines[line_of_interest + 1]
168+
orientation = lines[line_of_interest + 2]
169+
return {
170+
"position": process_string(position),
171+
"orientation": process_string(orientation),
172+
}
173+
174+
175+
def process_string(info_string: str) -> np.ndarray:
176+
"""Process the info string to extract the position or orientation values.
177+
178+
Args:
179+
info_string (str): The info string.
180+
181+
Returns:
182+
np.ndarray: An array of float values extracted from the info string.
183+
184+
Raises:
185+
RuntimeError: If the info string is not in the expected format.
186+
"""
187+
values_string = re.search(r"\[(.*?)\]", info_string)
188+
189+
if not values_string:
190+
raise RuntimeError("Could not parse message.")
191+
192+
group = values_string.group(1)
193+
return np.array(list(map(float, group.split())))
194+
195+
196+
def main(args: list | None = None) -> None:
197+
"""Main function to initialize the ROS 2 node and set the thresholds.
198+
199+
Args:
200+
args (list | None): Command line arguments, defaults to None.
201+
"""
202+
rclpy.init(args=args)
203+
spawn_platform = SpawnPlatform()
204+
spawn_platform.destroy_node()
205+
rclpy.shutdown()
206+
207+
208+
if __name__ == "__main__":
209+
main()

0 commit comments

Comments
 (0)