From d27b0532993663809cd1322dbc5eba1a20769ee5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Nov 2025 08:26:50 +0000 Subject: [PATCH 1/6] Initial plan From ba30bc066cd506fdd58419f02cd1c591ad46a352 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Nov 2025 08:35:34 +0000 Subject: [PATCH 2/6] Add Gazebo Sim support with backward compatibility for Gazebo Classic Co-authored-by: marc-hanheide <1153084+marc-hanheide@users.noreply.github.com> --- README.md | 12 +- .../description/ros2_control.xacro | 8 ++ hunter_gazebo/launch/launch_gz_sim.launch.py | 126 ++++++++++++++++++ hunter_gazebo/package.xml | 3 + 4 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 hunter_gazebo/launch/launch_gz_sim.launch.py diff --git a/README.md b/README.md index f174e04..ff1e891 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,17 @@ This launch file starts Rviz and loads the Hunter V2 model. ### 2. Simulate the Robot in Gazebo -To load the Hunter V2 in a Gazebo simulation environment: +#### Option A: Gazebo Sim (Recommended for new installations) + +To load the Hunter V2 in Gazebo Sim (formerly Ignition Gazebo): + +```bash +ros2 launch hunter_gazebo launch_gz_sim.launch.py +``` + +#### Option B: Gazebo Classic (Legacy) + +To load the Hunter V2 in Gazebo Classic: ```bash ros2 launch hunter_gazebo launch_sim.launch.py diff --git a/hunter_description/description/ros2_control.xacro b/hunter_description/description/ros2_control.xacro index 12c1fd3..892f5c4 100644 --- a/hunter_description/description/ros2_control.xacro +++ b/hunter_description/description/ros2_control.xacro @@ -38,10 +38,18 @@ + $(find hunter_description)/config/ackermann_like_controller.yaml + + + + $(find hunter_description)/config/ackermann_like_controller.yaml + + + diff --git a/hunter_gazebo/launch/launch_gz_sim.launch.py b/hunter_gazebo/launch/launch_gz_sim.launch.py new file mode 100644 index 0000000..47c7165 --- /dev/null +++ b/hunter_gazebo/launch/launch_gz_sim.launch.py @@ -0,0 +1,126 @@ +import os + +from launch_ros.actions import Node + +from launch import LaunchDescription +from launch.event_handlers import OnProcessExit + +from launch_ros.substitutions import FindPackageShare +from launch_ros.parameter_descriptions import ParameterValue + +from launch.launch_description_sources import PythonLaunchDescriptionSource +from launch.substitutions import Command, FindExecutable, PathJoinSubstitution, LaunchConfiguration +from launch.actions import IncludeLaunchDescription, RegisterEventHandler, DeclareLaunchArgument + +from ament_index_python.packages import get_package_share_directory + +def generate_launch_description(): + # Declare the use_sim_time argument + use_sim_time = DeclareLaunchArgument( + 'use_sim_time', + default_value='true', + description='Use simulation time' + ) + + # Launch configuration for use_sim_time + use_sim_time_config = LaunchConfiguration('use_sim_time') + + # Include the Gazebo Sim launch file + gazebo = IncludeLaunchDescription( + PythonLaunchDescriptionSource([ + PathJoinSubstitution([ + FindPackageShare('ros_gz_sim'), + 'launch', + 'gz_sim.launch.py' + ]) + ]), + launch_arguments={'gz_args': '-r -v 4'}.items() + ) + + hunter_description_path = os.path.join( + get_package_share_directory('hunter_description')) + + # Get URDF via xacro + robot_description_content = Command( + [ + PathJoinSubstitution([FindExecutable(name="xacro")]), + " ", + PathJoinSubstitution( + [FindPackageShare("hunter_description"), "description", 'robot.urdf.xacro'] + ), + ] + ) + robot_description = { + "robot_description": ParameterValue(robot_description_content, value_type=str) + } + + node_robot_state_publisher = Node( + package='robot_state_publisher', + executable='robot_state_publisher', + output='screen', + parameters=[robot_description, {'use_sim_time': use_sim_time_config}] + ) + + # Spawn entity in Gazebo Sim + spawn_entity = Node( + package='ros_gz_sim', + executable='create', + arguments=['-topic', 'robot_description', '-name', 'hunter', '-allow_renaming', 'true'], + output='screen' + ) + + # Bridge for clock + bridge = Node( + package='ros_gz_bridge', + executable='parameter_bridge', + arguments=['/clock@rosgraph_msgs/msg/Clock[gz.msgs.Clock'], + output='screen' + ) + + # Load joint state broadcaster using spawner + load_joint_state_broadcaster = Node( + package='controller_manager', + executable='spawner', + arguments=['joint_state_broadcaster'], + output='screen' + ) + + # Load ackermann controller using spawner + load_ackermann_controller = Node( + package='controller_manager', + executable='spawner', + arguments=['ackermann_like_controller'], + output='screen' + ) + + rviz = Node( + package='rviz2', + executable='rviz2', + arguments=[ + '-d', + os.path.join(hunter_description_path, 'rviz/robot_view.rviz'), + ], + output='screen', + parameters=[{'use_sim_time': use_sim_time_config}] + ) + + return LaunchDescription([ + use_sim_time, + RegisterEventHandler( + event_handler=OnProcessExit( + target_action=spawn_entity, + on_exit=[load_joint_state_broadcaster], + ) + ), + RegisterEventHandler( + event_handler=OnProcessExit( + target_action=load_joint_state_broadcaster, + on_exit=[load_ackermann_controller], + ) + ), + gazebo, + bridge, + rviz, + node_robot_state_publisher, + spawn_entity, + ]) diff --git a/hunter_gazebo/package.xml b/hunter_gazebo/package.xml index 67ac927..064b8ea 100755 --- a/hunter_gazebo/package.xml +++ b/hunter_gazebo/package.xml @@ -17,6 +17,9 @@ hunter_description gazebo_ros gazebo_ros2_control + ros_gz_sim + ros_gz_bridge + gz_ros2_control controller_manager teleop_twist_keyboard From f54ca59f19d208642a3596c089d78d083d49e2cb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Nov 2025 08:38:06 +0000 Subject: [PATCH 3/6] Fix xacro syntax to use ROS2 find-pkg-share instead of ROS1 find Co-authored-by: marc-hanheide <1153084+marc-hanheide@users.noreply.github.com> --- .../description/ros2_control.xacro | 4 ++-- .../launch_gz_sim.launch.cpython-312.pyc | Bin 0 -> 3295 bytes 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 hunter_gazebo/launch/__pycache__/launch_gz_sim.launch.cpython-312.pyc diff --git a/hunter_description/description/ros2_control.xacro b/hunter_description/description/ros2_control.xacro index 892f5c4..278baf0 100644 --- a/hunter_description/description/ros2_control.xacro +++ b/hunter_description/description/ros2_control.xacro @@ -41,14 +41,14 @@ - $(find hunter_description)/config/ackermann_like_controller.yaml + $(find-pkg-share hunter_description)/config/ackermann_like_controller.yaml - $(find hunter_description)/config/ackermann_like_controller.yaml + $(find-pkg-share hunter_description)/config/ackermann_like_controller.yaml diff --git a/hunter_gazebo/launch/__pycache__/launch_gz_sim.launch.cpython-312.pyc b/hunter_gazebo/launch/__pycache__/launch_gz_sim.launch.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..54f7e7254f0a1e74565ffcd2574958f034129d13 GIT binary patch literal 3295 zcmb7G&2JmW6`v)S%O$m>R$S_X`mm@k+YBtKaO1>D(=>_f#A#Glh>-wgfNoaY^=j>1 zE-^clErJ4k=q0BBy#xUYBq(}FAsu?`|B%ZDGLXeoK?*cF^hQAJ#izd650U=pp-XV) z&3kX&%$s@d_vYI|A&=m@`k#_DC?fPPHb}lyOq~4@h%bECRFw`&~bZ@any{lu2mX*G9m7+w=w zZDP4CU>1Jc+Qx3v@VuK3EdmVrU8|*UYt8$bY24Y>Fq>K2*0AOn#K0eGc875mw)e@d z+j`F89k+vmQIVz-pz+bvqQ8U+o0ubjqLu~wOV&NJ07t}hi!x^k)=(y zb=NXGSPOTTrZexins!Gwo?C~OKQv6sgB{)6Gg{;ptYjOQPTnw@Htadle;b<}2c)py zIny9&JMv2Pm|LoDVWUZ0yx)v(GVIuer{RA#5A7F-aEVao^pu`H;t!b=z5_&J3c=MM z#(D=F$wYF5NcNn{ol|28rDu{kx}ayxbTTr}ZRw((v$%VOWX7;RjLDc9<9al#xA$olI2ty+Zgw44*Y~O~Gq8%pj2~vYM@ze%7NCMUYCrQ( zYhoAF`4*tq6RwI`g6SFj~@pu_J%&wf~&Rxf=Y+Q)XatB+wX;ArmNj2hhuWU4Jw|W09 z&@-{t-c=pX^fsW`*kt@q%x;}=>fwl%nr@3=*9LG^0nVW&V4C&X+JhDl)AsVEAf3tO2NKFHgre7j%2j{n=R;8+MB{;23`8jqm%&JXO}QYPAw|me zk&?-tK|G*Z1-}d(EyY0tpj3}?+lx|;Jwn2~qmz+J{i!_n!c3$yBh$T+XQdY<`_hPb zh=(Hq>P49z>%~z$S_V`veAjv*jL>qtm8X{)!bS))ET3pG)=Yc~p(sQU22XSjupU4Q z9>~T+wH>~w>u~|`A}nH=^H+p#uw!@!nkV8Lj;nWU<4t@UhV0epZ9vmca~$^%lsiQ$ or)c#Dv~h~A{ea#$MIZc#n#ZX5Bl`3^^yw+uJmVdX%Z3L33nKoB Date: Tue, 11 Nov 2025 08:38:42 +0000 Subject: [PATCH 4/6] Add Python cache to gitignore and remove committed pycache Co-authored-by: marc-hanheide <1153084+marc-hanheide@users.noreply.github.com> --- .gitignore | 6 ++++++ .../launch_gz_sim.launch.cpython-312.pyc | Bin 3295 -> 0 bytes 2 files changed, 6 insertions(+) delete mode 100644 hunter_gazebo/launch/__pycache__/launch_gz_sim.launch.cpython-312.pyc diff --git a/.gitignore b/.gitignore index 259148f..0854c54 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,9 @@ *.exe *.out *.app + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.pyc diff --git a/hunter_gazebo/launch/__pycache__/launch_gz_sim.launch.cpython-312.pyc b/hunter_gazebo/launch/__pycache__/launch_gz_sim.launch.cpython-312.pyc deleted file mode 100644 index 54f7e7254f0a1e74565ffcd2574958f034129d13..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3295 zcmb7G&2JmW6`v)S%O$m>R$S_X`mm@k+YBtKaO1>D(=>_f#A#Glh>-wgfNoaY^=j>1 zE-^clErJ4k=q0BBy#xUYBq(}FAsu?`|B%ZDGLXeoK?*cF^hQAJ#izd650U=pp-XV) z&3kX&%$s@d_vYI|A&=m@`k#_DC?fPPHb}lyOq~4@h%bECRFw`&~bZ@any{lu2mX*G9m7+w=w zZDP4CU>1Jc+Qx3v@VuK3EdmVrU8|*UYt8$bY24Y>Fq>K2*0AOn#K0eGc875mw)e@d z+j`F89k+vmQIVz-pz+bvqQ8U+o0ubjqLu~wOV&NJ07t}hi!x^k)=(y zb=NXGSPOTTrZexins!Gwo?C~OKQv6sgB{)6Gg{;ptYjOQPTnw@Htadle;b<}2c)py zIny9&JMv2Pm|LoDVWUZ0yx)v(GVIuer{RA#5A7F-aEVao^pu`H;t!b=z5_&J3c=MM z#(D=F$wYF5NcNn{ol|28rDu{kx}ayxbTTr}ZRw((v$%VOWX7;RjLDc9<9al#xA$olI2ty+Zgw44*Y~O~Gq8%pj2~vYM@ze%7NCMUYCrQ( zYhoAF`4*tq6RwI`g6SFj~@pu_J%&wf~&Rxf=Y+Q)XatB+wX;ArmNj2hhuWU4Jw|W09 z&@-{t-c=pX^fsW`*kt@q%x;}=>fwl%nr@3=*9LG^0nVW&V4C&X+JhDl)AsVEAf3tO2NKFHgre7j%2j{n=R;8+MB{;23`8jqm%&JXO}QYPAw|me zk&?-tK|G*Z1-}d(EyY0tpj3}?+lx|;Jwn2~qmz+J{i!_n!c3$yBh$T+XQdY<`_hPb zh=(Hq>P49z>%~z$S_V`veAjv*jL>qtm8X{)!bS))ET3pG)=Yc~p(sQU22XSjupU4Q z9>~T+wH>~w>u~|`A}nH=^H+p#uw!@!nkV8Lj;nWU<4t@UhV0epZ9vmca~$^%lsiQ$ or)c#Dv~h~A{ea#$MIZc#n#ZX5Bl`3^^yw+uJmVdX%Z3L33nKoB Date: Tue, 11 Nov 2025 08:41:03 +0000 Subject: [PATCH 5/6] Add comprehensive migration guide documentation Co-authored-by: marc-hanheide <1153084+marc-hanheide@users.noreply.github.com> --- MIGRATION_GUIDE.md | 155 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 MIGRATION_GUIDE.md diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md new file mode 100644 index 0000000..02bb8fa --- /dev/null +++ b/MIGRATION_GUIDE.md @@ -0,0 +1,155 @@ +# Gazebo Classic to Gazebo Sim Migration Guide + +This document describes the migration from Gazebo Classic to Gazebo Sim (formerly Ignition Gazebo) for the hunter_robot package. + +## Overview + +The hunter_gazebo package now supports both Gazebo Classic and Gazebo Sim, allowing users to choose their preferred simulation environment. + +## What Changed + +### 1. Package Dependencies (hunter_gazebo/package.xml) + +Added new dependencies for Gazebo Sim while keeping Gazebo Classic dependencies: + +```xml +ros_gz_sim +ros_gz_bridge +gz_ros2_control +``` + +### 2. Launch Files + +- **launch_sim.launch.py** (unchanged): For Gazebo Classic +- **launch_gz_sim.launch.py** (new): For Gazebo Sim + +Key differences in the Gazebo Sim launch file: +- Uses `ros_gz_sim/gz_sim.launch.py` instead of `gazebo_ros/gazebo.launch.py` +- Uses `ros_gz_sim/create` instead of `gazebo_ros/spawn_entity.py` +- Adds clock bridge: `/clock@rosgraph_msgs/msg/Clock[gz.msgs.Clock` +- Uses `controller_manager/spawner` nodes instead of `ExecuteProcess` with ros2 CLI + +### 3. URDF/Xacro (hunter_description/description/ros2_control.xacro) + +Added Gazebo Sim plugin while keeping the Gazebo Classic plugin: + +```xml + + + + $(find-pkg-share hunter_description)/config/ackermann_like_controller.yaml + + + + + + + $(find-pkg-share hunter_description)/config/ackermann_like_controller.yaml + + +``` + +Also updated the parameter path syntax from `$(find ...)` (ROS1 style) to `$(find-pkg-share ...)` (ROS2 style). + +## Usage + +### Gazebo Sim (Recommended) + +```bash +ros2 launch hunter_gazebo launch_gz_sim.launch.py +``` + +### Gazebo Classic (Legacy) + +```bash +ros2 launch hunter_gazebo launch_sim.launch.py +``` + +### Teleop Control (works with both) + +```bash +ros2 run teleop_twist_keyboard teleop_twist_keyboard --ros-args --remap cmd_vel:=/ackermann_like_controller/cmd_vel +``` + +## Technical Details + +### Controller Loading + +**Gazebo Classic approach** (ExecuteProcess): +```python +load_joint_state_broadcaster = ExecuteProcess( + cmd=['ros2', 'control', 'load_controller', '--set-state', 'active', + 'joint_state_broadcaster'], + output='screen' +) +``` + +**Gazebo Sim approach** (spawner node): +```python +load_joint_state_broadcaster = Node( + package='controller_manager', + executable='spawner', + arguments=['joint_state_broadcaster'], + output='screen' +) +``` + +### Clock Synchronization + +Gazebo Sim requires explicit clock bridging: +```python +bridge = Node( + package='ros_gz_bridge', + executable='parameter_bridge', + arguments=['/clock@rosgraph_msgs/msg/Clock[gz.msgs.Clock'], + output='screen' +) +``` + +## Backward Compatibility + +All changes maintain backward compatibility: +- Original Gazebo Classic launch file remains unchanged +- Both Gazebo plugins are present in the URDF +- Original dependencies are kept alongside new ones + +## Testing + +To test the migration: + +1. **Build the package**: + ```bash + colcon build --packages-select hunter_gazebo hunter_description + source install/setup.bash + ``` + +2. **Test Gazebo Sim**: + ```bash + ros2 launch hunter_gazebo launch_gz_sim.launch.py + ``` + +3. **Test Gazebo Classic** (ensure backward compatibility): + ```bash + ros2 launch hunter_gazebo launch_sim.launch.py + ``` + +4. **Verify controllers**: + ```bash + ros2 control list_controllers + ``` + + Expected output: + - `joint_state_broadcaster[joint_state_broadcaster/JointStateBroadcaster] active` + - `ackermann_like_controller[tricycle_controller/TricycleController] active` + +5. **Test robot control**: + ```bash + ros2 run teleop_twist_keyboard teleop_twist_keyboard --ros-args --remap cmd_vel:=/ackermann_like_controller/cmd_vel + ``` + +## References + +- [LCAS Migration Guide](https://github.com/LCAS/aoc_distro/wiki/Migration-to-new-Gazebo) +- [ros_gz_sim Documentation](https://github.com/gazebosim/ros_gz) +- [gz_ros2_control Documentation](https://github.com/ros-controls/gz_ros2_control) +- [RBT1001 Reference Implementation](https://github.com/LCAS/RBT1001) From 3fd21062eea2eb31425da70ef355c1fbf9fc9d76 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Nov 2025 08:55:56 +0000 Subject: [PATCH 6/6] Use xacro conditionals to prevent loading both plugins simultaneously Co-authored-by: marc-hanheide <1153084+marc-hanheide@users.noreply.github.com> --- MIGRATION_GUIDE.md | 48 ++++++++++++------- .../description/ros2_control.xacro | 28 +++++++---- hunter_gazebo/launch/launch_gz_sim.launch.py | 4 +- hunter_gazebo/launch/launch_sim.launch.py | 4 +- 4 files changed, 56 insertions(+), 28 deletions(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 02bb8fa..84c455d 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -31,25 +31,32 @@ Key differences in the Gazebo Sim launch file: ### 3. URDF/Xacro (hunter_description/description/ros2_control.xacro) -Added Gazebo Sim plugin while keeping the Gazebo Classic plugin: +Added conditional plugin loading using xacro arguments to prevent both plugins from loading simultaneously: ```xml - - - - $(find-pkg-share hunter_description)/config/ackermann_like_controller.yaml - - - - - - - $(find-pkg-share hunter_description)/config/ackermann_like_controller.yaml - - + + + + + + + + $(find-pkg-share hunter_description)/config/ackermann_like_controller.yaml + + + + + + + + + $(find-pkg-share hunter_description)/config/ackermann_like_controller.yaml + + + ``` -Also updated the parameter path syntax from `$(find ...)` (ROS1 style) to `$(find-pkg-share ...)` (ROS2 style). +The launch files pass the appropriate `sim_gazebo` argument to ensure only the correct plugin is loaded for each simulator. Also updated the parameter path syntax from `$(find ...)` (ROS1 style) to `$(find-pkg-share ...)` (ROS2 style). ## Usage @@ -106,12 +113,21 @@ bridge = Node( ) ``` +### Plugin Selection + +The URDF uses xacro conditional logic to load only one plugin at a time based on the `sim_gazebo` argument: +- `sim_gazebo:=classic` (default) loads `libgazebo_ros2_control.so` for Gazebo Classic +- `sim_gazebo:=sim` loads `libgz_ros2_control-system.so` for Gazebo Sim + +This prevents both plugins from attempting to load simultaneously. + ## Backward Compatibility All changes maintain backward compatibility: - Original Gazebo Classic launch file remains unchanged -- Both Gazebo plugins are present in the URDF +- Only the appropriate plugin loads based on which simulator is running (controlled by xacro argument) - Original dependencies are kept alongside new ones +- Default behavior (when no argument is passed) is Gazebo Classic ## Testing diff --git a/hunter_description/description/ros2_control.xacro b/hunter_description/description/ros2_control.xacro index 278baf0..feb1a74 100644 --- a/hunter_description/description/ros2_control.xacro +++ b/hunter_description/description/ros2_control.xacro @@ -1,6 +1,9 @@ + + + gazebo_ros2_control/GazeboSystem @@ -38,18 +41,23 @@ + - - - $(find-pkg-share hunter_description)/config/ackermann_like_controller.yaml - - + + + + $(find-pkg-share hunter_description)/config/ackermann_like_controller.yaml + + + - - - $(find-pkg-share hunter_description)/config/ackermann_like_controller.yaml - - + + + + $(find-pkg-share hunter_description)/config/ackermann_like_controller.yaml + + + diff --git a/hunter_gazebo/launch/launch_gz_sim.launch.py b/hunter_gazebo/launch/launch_gz_sim.launch.py index 47c7165..dd989d3 100644 --- a/hunter_gazebo/launch/launch_gz_sim.launch.py +++ b/hunter_gazebo/launch/launch_gz_sim.launch.py @@ -40,7 +40,7 @@ def generate_launch_description(): hunter_description_path = os.path.join( get_package_share_directory('hunter_description')) - # Get URDF via xacro + # Get URDF via xacro with sim_gazebo argument set to 'sim' robot_description_content = Command( [ PathJoinSubstitution([FindExecutable(name="xacro")]), @@ -48,6 +48,8 @@ def generate_launch_description(): PathJoinSubstitution( [FindPackageShare("hunter_description"), "description", 'robot.urdf.xacro'] ), + " ", + "sim_gazebo:=sim", ] ) robot_description = { diff --git a/hunter_gazebo/launch/launch_sim.launch.py b/hunter_gazebo/launch/launch_sim.launch.py index 0b26780..e513be3 100644 --- a/hunter_gazebo/launch/launch_sim.launch.py +++ b/hunter_gazebo/launch/launch_sim.launch.py @@ -37,7 +37,7 @@ def generate_launch_description(): hunter_description_path = os.path.join( get_package_share_directory('hunter_description')) - # Get URDF via xacro + # Get URDF via xacro with sim_gazebo argument set to 'classic' robot_description_content = Command( [ PathJoinSubstitution([FindExecutable(name="xacro")]), @@ -45,6 +45,8 @@ def generate_launch_description(): PathJoinSubstitution( [FindPackageShare("hunter_description"), "description", 'robot.urdf.xacro'] ), + " ", + "sim_gazebo:=classic", ] ) robot_description = {