From 9f354dc6bc1956bf09d97239a9f9a069b3721ce8 Mon Sep 17 00:00:00 2001 From: Julia Jia Date: Wed, 29 Jan 2025 21:19:18 -0800 Subject: [PATCH 01/20] Initial commit for chaining diff_drive_controller with pid_controller --- example_16/CMakeLists.txt | 84 +++++++ example_16/README.md | 6 + .../config/diffbot_chained_controllers.yaml | 106 +++++++++ example_16/bringup/launch/diffbot.launch.py | 159 +++++++++++++ .../description/launch/view_robot.launch.py | 111 +++++++++ .../ros2_control/diffbot.ros2_control.xacro | 34 +++ .../description/urdf/diffbot.urdf.xacro | 20 ++ example_16/doc/diffbot.png | Bin 0 -> 19716 bytes example_16/doc/userdoc.rst | 199 ++++++++++++++++ example_16/hardware/diffbot_system.cpp | 220 ++++++++++++++++++ .../diffbot_system.hpp | 66 ++++++ example_16/package.xml | 46 ++++ example_16/ros2_control_demo_example_16.xml | 9 + example_16/test/test_diffbot_launch.py | 104 +++++++++ example_16/test/test_urdf_xacro.py | 77 ++++++ example_16/test/test_view_robot_launch.py | 54 +++++ 16 files changed, 1295 insertions(+) create mode 100644 example_16/CMakeLists.txt create mode 100644 example_16/README.md create mode 100644 example_16/bringup/config/diffbot_chained_controllers.yaml create mode 100644 example_16/bringup/launch/diffbot.launch.py create mode 100644 example_16/description/launch/view_robot.launch.py create mode 100644 example_16/description/ros2_control/diffbot.ros2_control.xacro create mode 100644 example_16/description/urdf/diffbot.urdf.xacro create mode 100644 example_16/doc/diffbot.png create mode 100644 example_16/doc/userdoc.rst create mode 100644 example_16/hardware/diffbot_system.cpp create mode 100644 example_16/hardware/include/ros2_control_demo_example_16/diffbot_system.hpp create mode 100644 example_16/package.xml create mode 100644 example_16/ros2_control_demo_example_16.xml create mode 100644 example_16/test/test_diffbot_launch.py create mode 100644 example_16/test/test_urdf_xacro.py create mode 100644 example_16/test/test_view_robot_launch.py diff --git a/example_16/CMakeLists.txt b/example_16/CMakeLists.txt new file mode 100644 index 000000000..81ad5b39a --- /dev/null +++ b/example_16/CMakeLists.txt @@ -0,0 +1,84 @@ +cmake_minimum_required(VERSION 3.16) +project(ros2_control_demo_example_16 LANGUAGES CXX) + +if(CMAKE_CXX_COMPILER_ID MATCHES "(GNU|Clang)") + add_compile_options(-Wall -Wextra) +endif() + +# set the same behavior for windows as it is on linux +set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) + +# find dependencies +set(THIS_PACKAGE_INCLUDE_DEPENDS + hardware_interface + pluginlib + rclcpp + rclcpp_lifecycle +) + +# Specify the required version of ros2_control +find_package(controller_manager 4.0.0) +# Handle the case where the required version is not found +if(NOT controller_manager_FOUND) + message(FATAL_ERROR "ros2_control version 4.0.0 or higher is required. " + "Are you using the correct branch of the ros2_control_demos repository?") +endif() + +# find dependencies +find_package(backward_ros REQUIRED) +find_package(ament_cmake REQUIRED) +foreach(Dependency IN ITEMS ${THIS_PACKAGE_INCLUDE_DEPENDS}) + find_package(${Dependency} REQUIRED) +endforeach() + +## COMPILE +add_library( + ros2_control_demo_example_16 + SHARED + hardware/diffbot_system.cpp +) +target_compile_features(ros2_control_demo_example_16 PUBLIC cxx_std_17) +target_include_directories(ros2_control_demo_example_16 PUBLIC +$ +$ +) +ament_target_dependencies( + ros2_control_demo_example_16 PUBLIC + ${THIS_PACKAGE_INCLUDE_DEPENDS} +) + +# Export hardware plugins +pluginlib_export_plugin_description_file(hardware_interface ros2_control_demo_example_16.xml) + +# INSTALL +install( + DIRECTORY hardware/include/ + DESTINATION include/ros2_control_demo_example_16 +) +install( + DIRECTORY description/launch description/ros2_control description/urdf + DESTINATION share/ros2_control_demo_example_16 +) +install( + DIRECTORY bringup/launch bringup/config + DESTINATION share/ros2_control_demo_example_16 +) +install(TARGETS ros2_control_demo_example_16 + EXPORT export_ros2_control_demo_example_16 + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin +) + +if(BUILD_TESTING) + find_package(ament_cmake_pytest REQUIRED) + + ament_add_pytest_test(example_16_urdf_xacro test/test_urdf_xacro.py) + ament_add_pytest_test(view_example_16_launch test/test_view_robot_launch.py) + ament_add_pytest_test(run_example_16_launch test/test_diffbot_launch.py) +endif() + +## EXPORTS +ament_export_targets(export_ros2_control_demo_example_16 HAS_LIBRARY_TARGET) +ament_export_dependencies(${THIS_PACKAGE_INCLUDE_DEPENDS}) +ament_package() diff --git a/example_16/README.md b/example_16/README.md new file mode 100644 index 000000000..fe2445172 --- /dev/null +++ b/example_16/README.md @@ -0,0 +1,6 @@ +# ros2_control_demo_example_16 + + *DiffBot*, or ''Differential Mobile Robot'', is a simple mobile base with differential drive. + The robot is basically a box moving according to differential drive kinematics. + +Find the documentation in [doc/userdoc.rst](doc/userdoc.rst) or on [control.ros.org](https://control.ros.org/master/doc/ros2_control_demos/example_16/doc/userdoc.html). diff --git a/example_16/bringup/config/diffbot_chained_controllers.yaml b/example_16/bringup/config/diffbot_chained_controllers.yaml new file mode 100644 index 000000000..063c9f2db --- /dev/null +++ b/example_16/bringup/config/diffbot_chained_controllers.yaml @@ -0,0 +1,106 @@ +controller_manager: + ros__parameters: + update_rate: 10 # Hz + + joint_state_broadcaster: + type: joint_state_broadcaster/JointStateBroadcaster + + pid_controller_left_wheel_joint: + type: pid_controller/PidController + + pid_controller_right_wheel_joint: + type: pid_controller/PidController + + diffbot_base_controller: + type: diff_drive_controller/DiffDriveController + + forward_velocity_controller_for_debug: + type: forward_command_controller/ForwardCommandController + + +pid_controller_left_wheel_joint: + ros__parameters: + + dof_names: + - left_wheel_joint + + command_interface: velocity + + reference_and_state_interfaces: + - position + - velocity + + gains: + left_wheel_joint: {"p": 1.0, "i": 0.5, "d": 0.2, "i_clamp_min": -2.0, "i_clamp_max": 2.0, "antiwindup": true} + + +pid_controller_right_wheel_joint: + ros__parameters: + + dof_names: + - right_wheel_joint + + command_interface: velocity + + reference_and_state_interfaces: + - position + - velocity + + gains: + right_wheel_joint: {"p": 1.0, "i": 0.5, "d": 0.2, "i_clamp_min": -2.0, "i_clamp_max": 2.0, "antiwindup": true} + + +diffbot_base_controller: + ros__parameters: + + left_wheel_names: ["pid_controller_left_wheel_joint/left_wheel_joint"] + right_wheel_names: ["pid_controller_right_wheel_joint/right_wheel_joint"] + + wheel_separation: 0.10 + #wheels_per_side: 1 # actually 2, but both are controlled by 1 signal + wheel_radius: 0.015 + + wheel_separation_multiplier: 1.0 + left_wheel_radius_multiplier: 1.0 + right_wheel_radius_multiplier: 1.0 + + publish_rate: 50.0 + odom_frame_id: odom + base_frame_id: base_link + pose_covariance_diagonal : [0.001, 0.001, 0.001, 0.001, 0.001, 0.01] + twist_covariance_diagonal: [0.001, 0.001, 0.001, 0.001, 0.001, 0.01] + + open_loop: true + enable_odom_tf: true + + cmd_vel_timeout: 0.5 + #publish_limited_velocity: true + #velocity_rolling_window_size: 10 + + # Velocity and acceleration limits + # Whenever a min_* is unspecified, default to -max_* + linear.x.has_velocity_limits: true + linear.x.has_acceleration_limits: true + linear.x.has_jerk_limits: false + linear.x.max_velocity: 1.0 + linear.x.min_velocity: -1.0 + linear.x.max_acceleration: 1.0 + linear.x.max_jerk: 0.0 + linear.x.min_jerk: 0.0 + + angular.z.has_velocity_limits: true + angular.z.has_acceleration_limits: true + angular.z.has_jerk_limits: false + angular.z.max_velocity: 1.0 + angular.z.min_velocity: -1.0 + angular.z.max_acceleration: 1.0 + angular.z.min_acceleration: -1.0 + angular.z.max_jerk: 0.0 + angular.z.min_jerk: 0.0 + +forward_velocity_controller_for_debug: + ros__parameters: + joints: + - pid_controller_left_wheel_joint/left_wheel_joint + - pid_controller_right_wheel_joint/right_wheel_joint + interface_name: velocity diff --git a/example_16/bringup/launch/diffbot.launch.py b/example_16/bringup/launch/diffbot.launch.py new file mode 100644 index 000000000..d6f9cf9c5 --- /dev/null +++ b/example_16/bringup/launch/diffbot.launch.py @@ -0,0 +1,159 @@ +# Copyright 2020 ros2_control Development Team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from launch import LaunchDescription +from launch.actions import DeclareLaunchArgument, RegisterEventHandler +from launch.conditions import IfCondition +from launch.event_handlers import OnProcessExit +from launch.substitutions import Command, FindExecutable, PathJoinSubstitution, LaunchConfiguration + +from launch_ros.actions import Node +from launch_ros.substitutions import FindPackageShare + + +def generate_launch_description(): + # Declare arguments + declared_arguments = [] + declared_arguments.append( + DeclareLaunchArgument( + "gui", + default_value="false", + description="Start RViz2 automatically with this launch file.", + ) + ) + declared_arguments.append( + DeclareLaunchArgument( + "use_mock_hardware", + default_value="false", + description="Start robot with mock hardware mirroring command to its states.", + ) + ) + + # Initialize Arguments + gui = LaunchConfiguration("gui") + use_mock_hardware = LaunchConfiguration("use_mock_hardware") + + # Get URDF via xacro + robot_description_content = Command( + [ + PathJoinSubstitution([FindExecutable(name="xacro")]), + " ", + PathJoinSubstitution( + [FindPackageShare("ros2_control_demo_example_16"), "urdf", "diffbot.urdf.xacro"] + ), + " ", + "use_mock_hardware:=", + use_mock_hardware, + ] + ) + robot_description = {"robot_description": robot_description_content} + + robot_controllers = PathJoinSubstitution( + [ + FindPackageShare("ros2_control_demo_example_16"), + "config", + "diffbot_chained_controllers.yaml", + ] + ) + rviz_config_file = PathJoinSubstitution( + [FindPackageShare("ros2_control_demo_description"), "diffbot/rviz", "diffbot.rviz"] + ) + + control_node = Node( + package="controller_manager", + executable="ros2_control_node", + parameters=[robot_controllers], + output="both", + ) + robot_state_pub_node = Node( + package="robot_state_publisher", + executable="robot_state_publisher", + output="both", + parameters=[robot_description], + ) + rviz_node = Node( + package="rviz2", + executable="rviz2", + name="rviz2", + output="log", + arguments=["-d", rviz_config_file], + condition=IfCondition(gui), + ) + + joint_state_broadcaster_spawner = Node( + package="controller_manager", + executable="spawner", + arguments=["joint_state_broadcaster"], + ) + + pid_controller_left_wheel_joint_spawner = Node( + package="controller_manager", + executable="spawner", + arguments=["pid_controller_left_wheel_joint", "--param-file", robot_controllers], + ) + + pid_controller_right_wheel_joint_spawner = Node( + package="controller_manager", + executable="spawner", + arguments=["pid_controller_right_wheel_joint", "--param-file", robot_controllers], + ) + + robot_base_controller_spawner = Node( + package="controller_manager", + executable="spawner", + arguments=[ + "diffbot_base_controller", + # "forward_velocity_controller", + "--param-file", + robot_controllers, + "--controller-ros-args", + "-r /diffbot_base_controller/cmd_vel:=/cmd_vel", + ], + ) + + # Delay rviz start after `joint_state_broadcaster` + delay_rviz_after_joint_state_broadcaster_spawner = RegisterEventHandler( + event_handler=OnProcessExit( + target_action=joint_state_broadcaster_spawner, + on_exit=[rviz_node], + ) + ) + + delay_robot_base_after_pid_controller_spawner = RegisterEventHandler( + event_handler=OnProcessExit( + target_action=pid_controller_right_wheel_joint_spawner, + on_exit=[robot_base_controller_spawner], + ) + ) + + # Delay start of joint_state_broadcaster after `robot_controller` + # TODO(anyone): This is a workaround for flaky tests. Remove when fixed. + delay_joint_state_broadcaster_after_robot_base_controller_spawner = RegisterEventHandler( + event_handler=OnProcessExit( + target_action=robot_base_controller_spawner, + on_exit=[joint_state_broadcaster_spawner], + ) + ) + + nodes = [ + control_node, + robot_state_pub_node, + pid_controller_left_wheel_joint_spawner, + pid_controller_right_wheel_joint_spawner, + delay_robot_base_after_pid_controller_spawner, + delay_rviz_after_joint_state_broadcaster_spawner, + delay_joint_state_broadcaster_after_robot_base_controller_spawner, + ] + + return LaunchDescription(declared_arguments + nodes) diff --git a/example_16/description/launch/view_robot.launch.py b/example_16/description/launch/view_robot.launch.py new file mode 100644 index 000000000..41589297f --- /dev/null +++ b/example_16/description/launch/view_robot.launch.py @@ -0,0 +1,111 @@ +# Copyright 2021 Stogl Robotics Consulting UG (haftungsbeschränkt) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from launch import LaunchDescription +from launch.actions import DeclareLaunchArgument +from launch.conditions import IfCondition +from launch.substitutions import Command, FindExecutable, LaunchConfiguration, PathJoinSubstitution + +from launch_ros.actions import Node +from launch_ros.substitutions import FindPackageShare + + +def generate_launch_description(): + # Declare arguments + declared_arguments = [] + declared_arguments.append( + DeclareLaunchArgument( + "description_package", + default_value="ros2_control_demo_description", + description="Description package with robot URDF/xacro files. Usually the argument \ + is not set, it enables use of a custom description.", + ) + ) + declared_arguments.append( + DeclareLaunchArgument( + "description_file", + default_value="diffbot.urdf.xacro", + description="URDF/XACRO description file with the robot.", + ) + ) + declared_arguments.append( + DeclareLaunchArgument( + "gui", + default_value="true", + description="Start Rviz2 and Joint State Publisher gui automatically \ + with this launch file.", + ) + ) + declared_arguments.append( + DeclareLaunchArgument( + "prefix", + default_value='""', + description="Prefix of the joint names, useful for \ + multi-robot setup. If changed than also joint names in the controllers' configuration \ + have to be updated.", + ) + ) + + # Initialize Arguments + description_package = LaunchConfiguration("description_package") + description_file = LaunchConfiguration("description_file") + gui = LaunchConfiguration("gui") + prefix = LaunchConfiguration("prefix") + + # Get URDF via xacro + robot_description_content = Command( + [ + PathJoinSubstitution([FindExecutable(name="xacro")]), + " ", + PathJoinSubstitution( + [FindPackageShare("ros2_control_demo_example_16"), "urdf", description_file] + ), + " ", + "prefix:=", + prefix, + ] + ) + robot_description = {"robot_description": robot_description_content} + + rviz_config_file = PathJoinSubstitution( + [FindPackageShare(description_package), "diffbot/rviz", "diffbot_view.rviz"] + ) + + joint_state_publisher_node = Node( + package="joint_state_publisher_gui", + executable="joint_state_publisher_gui", + condition=IfCondition(gui), + ) + robot_state_publisher_node = Node( + package="robot_state_publisher", + executable="robot_state_publisher", + output="both", + parameters=[robot_description], + ) + rviz_node = Node( + package="rviz2", + executable="rviz2", + name="rviz2", + output="log", + arguments=["-d", rviz_config_file], + condition=IfCondition(gui), + ) + + nodes = [ + joint_state_publisher_node, + robot_state_publisher_node, + rviz_node, + ] + + return LaunchDescription(declared_arguments + nodes) diff --git a/example_16/description/ros2_control/diffbot.ros2_control.xacro b/example_16/description/ros2_control/diffbot.ros2_control.xacro new file mode 100644 index 000000000..341df177e --- /dev/null +++ b/example_16/description/ros2_control/diffbot.ros2_control.xacro @@ -0,0 +1,34 @@ + + + + + + + + + ros2_control_demo_example_16/DiffBotSystemHardware + 0 + 3.0 + + + + + mock_components/GenericSystem + true + + + + + + + + + + + + + + + + + diff --git a/example_16/description/urdf/diffbot.urdf.xacro b/example_16/description/urdf/diffbot.urdf.xacro new file mode 100644 index 000000000..f11dc0844 --- /dev/null +++ b/example_16/description/urdf/diffbot.urdf.xacro @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + diff --git a/example_16/doc/diffbot.png b/example_16/doc/diffbot.png new file mode 100644 index 0000000000000000000000000000000000000000..1c4fc2476dba992c2984f32d8225fadf4cb9eba2 GIT binary patch literal 19716 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 literal 0 HcmV?d00001 diff --git a/example_16/doc/userdoc.rst b/example_16/doc/userdoc.rst new file mode 100644 index 000000000..37027a92b --- /dev/null +++ b/example_16/doc/userdoc.rst @@ -0,0 +1,199 @@ +:github_url: https://github.com/ros-controls/ros2_control_demos/blob/{REPOS_FILE_BRANCH}/example_16/doc/userdoc.rst + +.. _ros2_control_demos_example_16_userdoc: + +******************************** +DiffBot with Chained Controllers +******************************** + +*DiffBot*, or ''Differential Mobile Robot'', is a simple mobile base with differential drive. The robot is basically a box moving according to differential drive kinematics. + +*example_16* is based of *example_2* which the hardware interface plugin is implemented having only one interface. This example demonstrates using a chaininged diff_drive_controller and pid_controller for each wheel to control the robot. The controllers are chained together to control the robot's velocity. + +* The communication is done using proprietary API to communicate with the robot control box. +* Data for all joints is exchanged at once. + +The *DiffBot* URDF files can be found in ``description/urdf`` folder. + +.. include:: ../../doc/run_from_docker.rst + + + +Tutorial steps +-------------------------- + +1. To check that *DiffBot* description is working properly use following launch commands + + .. code-block:: shell + + ros2 launch ros2_control_demo_example_16 view_robot.launch.py + + .. warning:: + Getting the following output in terminal is OK: ``Warning: Invalid frame ID "odom" passed to canTransform argument target_frame - frame does not exist``. + This happens because ``joint_state_publisher_gui`` node need some time to start. + + .. image:: diffbot.png + :width: 400 + :alt: Differential Mobile Robot + +2. To start *DiffBot* example open a terminal, source your ROS2-workspace and execute its launch file with + + .. code-block:: shell + + ros2 launch ros2_control_demo_example_16 diffbot.launch.py + + The launch file loads and starts the robot hardware, controllers and opens *RViz*. + In the starting terminal you will see a lot of output from the hardware implementation showing its internal states. + This excessive printing is only added for demonstration. In general, printing to the terminal should be avoided as much as possible in a hardware interface implementation. + + If you can see an orange box in *RViz* everything has started properly. + Still, to be sure, let's introspect the control system before moving *DiffBot*. + +3. Check if the hardware interface loaded properly, by opening another terminal and executing + + .. code-block:: shell + + ros2 control list_hardware_interfaces + + You should get + + .. code-block:: shell + + command interfaces + left_wheel_joint/velocity [available] [claimed] + right_wheel_joint/velocity [available] [claimed] + state interfaces + left_wheel_joint/position + left_wheel_joint/velocity + right_wheel_joint/position + right_wheel_joint/velocity + + The ``[claimed]`` marker on command interfaces means that a controller has access to command *DiffBot*. + + Furthermore, we can see that the command interface is of type ``velocity``, which is typical for a differential drive robot. + +4. Check if controllers are running + + .. code-block:: shell + + ros2 control list_controllers + + You should get + + .. code-block:: shell + + diffbot_base_controller[diff_drive_controller/DiffDriveController] active + joint_state_broadcaster[joint_state_broadcaster/JointStateBroadcaster] active + +5. If everything is fine, now you can send a command to *Diff Drive Controller* using ROS 2 CLI interface: + + .. code-block:: shell + + ros2 topic pub --rate 10 /cmd_vel geometry_msgs/msg/TwistStamped " + twist: + linear: + x: 0.7 + y: 0.0 + z: 0.0 + angular: + x: 0.0 + y: 0.0 + z: 1.0" + + You should now see an orange box circling in *RViz*. + Also, you should see changing states in the terminal where launch file is started. + + .. code-block:: shell + + [ros2_control_node-1] [INFO] [1721762311.808415917] [controller_manager.resource_manager.hardware_component.system.DiffBot]: Writing commands: + [ros2_control_node-1] command 43.33 for 'left_wheel_joint'! + [ros2_control_node-1] command 50.00 for 'right_wheel_joint'! + +6. Let's introspect the ros2_control hardware component. Calling + + .. code-block:: shell + + ros2 control list_hardware_components + + should give you + + .. code-block:: shell + + Hardware Component 1 + name: DiffBot + type: system + plugin name: ros2_control_demo_example_16/DiffBotSystemHardware + state: id=3 label=active + command interfaces + left_wheel_joint/velocity [available] [claimed] + right_wheel_joint/velocity [available] [claimed] + + This shows that the custom hardware interface plugin is loaded and running. If you work on a real + robot and don't have a simulator running, it is often faster to use the ``mock_components/GenericSystem`` + hardware component instead of writing a custom one. Stop the launch file and start it again with + an additional parameter + + .. code-block:: shell + + ros2 launch ros2_control_demo_example_16 diffbot.launch.py use_mock_hardware:=True + + Calling + + .. code-block:: shell + + ros2 control list_hardware_components + + now should give you + + .. code-block:: shell + + Hardware Component 1 + name: DiffBot + type: system + plugin name: mock_components/GenericSystem + state: id=3 label=active + command interfaces + left_wheel_joint/velocity [available] [claimed] + right_wheel_joint/velocity [available] [claimed] + + You see that a different plugin was loaded. Having a look into the `diffbot.ros2_control.xacro `__, one can find the + instructions to load this plugin together with the parameter ``calculate_dynamics``. + + .. code-block:: xml + + + mock_components/GenericSystem + true + + + This enables the integration of the velocity commands to the position state interface, which can be + checked by means of ``ros2 topic echo /joint_states``: The position values are increasing over time if the robot is moving. + You now can test the setup with the commands from above, it should work identically as the custom hardware component plugin. + + More information on mock_components can be found in the :ref:`ros2_control documentation `. + +Files used for this demos +-------------------------- + +* Launch file: `diffbot.launch.py `__ +* Controllers yaml: `diffbot_controllers.yaml `__ +* URDF file: `diffbot.urdf.xacro `__ + + * Description: `diffbot_description.urdf.xacro `__ + * ``ros2_control`` tag: `diffbot.ros2_control.xacro `__ + +* RViz configuration: `diffbot.rviz `__ + +* Hardware interface plugin: `diffbot_system.cpp `__ + + +Controllers from this demo +-------------------------- + +* ``Joint State Broadcaster`` (`ros2_controllers repository `__): :ref:`doc ` +* ``Diff Drive Controller`` (`ros2_controllers repository `__): :ref:`doc ` +* ``pid_controller`` (`ros2_controllers repository `__): :ref:`doc ` + +References +-------------------------- +https://github.com/ros-controls/roscon_advanced_workshop/blob/9-chaining-controllers/solution/controlko_bringup/config/rrbot_chained_controllers.yaml diff --git a/example_16/hardware/diffbot_system.cpp b/example_16/hardware/diffbot_system.cpp new file mode 100644 index 000000000..8cae96625 --- /dev/null +++ b/example_16/hardware/diffbot_system.cpp @@ -0,0 +1,220 @@ +// Copyright 2021 ros2_control Development Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ros2_control_demo_example_16/diffbot_system.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "hardware_interface/lexical_casts.hpp" +#include "hardware_interface/types/hardware_interface_type_values.hpp" +#include "rclcpp/rclcpp.hpp" + +namespace ros2_control_demo_example_16 +{ +hardware_interface::CallbackReturn DiffBotSystemHardware::on_init( + const hardware_interface::HardwareInfo & info) +{ + if ( + hardware_interface::SystemInterface::on_init(info) != + hardware_interface::CallbackReturn::SUCCESS) + { + return hardware_interface::CallbackReturn::ERROR; + } + + // BEGIN: This part here is for exemplary purposes - Please do not copy to your production code + hw_start_sec_ = + hardware_interface::stod(info_.hardware_parameters["example_param_hw_start_duration_sec"]); + hw_stop_sec_ = + hardware_interface::stod(info_.hardware_parameters["example_param_hw_stop_duration_sec"]); + // END: This part here is for exemplary purposes - Please do not copy to your production code + + for (const hardware_interface::ComponentInfo & joint : info_.joints) + { + // DiffBotSystem has exactly two states and one command interface on each joint + if (joint.command_interfaces.size() != 1) + { + RCLCPP_FATAL( + get_logger(), "Joint '%s' has %zu command interfaces found. 1 expected.", + joint.name.c_str(), joint.command_interfaces.size()); + return hardware_interface::CallbackReturn::ERROR; + } + + if (joint.command_interfaces[0].name != hardware_interface::HW_IF_VELOCITY) + { + RCLCPP_FATAL( + get_logger(), "Joint '%s' have %s command interfaces found. '%s' expected.", + joint.name.c_str(), joint.command_interfaces[0].name.c_str(), + hardware_interface::HW_IF_VELOCITY); + return hardware_interface::CallbackReturn::ERROR; + } + + if (joint.state_interfaces.size() != 2) + { + RCLCPP_FATAL( + get_logger(), "Joint '%s' has %zu state interface. 2 expected.", joint.name.c_str(), + joint.state_interfaces.size()); + return hardware_interface::CallbackReturn::ERROR; + } + + if (joint.state_interfaces[0].name != hardware_interface::HW_IF_POSITION) + { + RCLCPP_FATAL( + get_logger(), "Joint '%s' have '%s' as first state interface. '%s' expected.", + joint.name.c_str(), joint.state_interfaces[0].name.c_str(), + hardware_interface::HW_IF_POSITION); + return hardware_interface::CallbackReturn::ERROR; + } + + if (joint.state_interfaces[1].name != hardware_interface::HW_IF_VELOCITY) + { + RCLCPP_FATAL( + get_logger(), "Joint '%s' have '%s' as second state interface. '%s' expected.", + joint.name.c_str(), joint.state_interfaces[1].name.c_str(), + hardware_interface::HW_IF_VELOCITY); + return hardware_interface::CallbackReturn::ERROR; + } + } + + return hardware_interface::CallbackReturn::SUCCESS; +} + +hardware_interface::CallbackReturn DiffBotSystemHardware::on_configure( + const rclcpp_lifecycle::State & /*previous_state*/) +{ + // BEGIN: This part here is for exemplary purposes - Please do not copy to your production code + RCLCPP_INFO(get_logger(), "Configuring ...please wait..."); + + for (int i = 0; i < hw_start_sec_; i++) + { + rclcpp::sleep_for(std::chrono::seconds(1)); + RCLCPP_INFO(get_logger(), "%.1f seconds left...", hw_start_sec_ - i); + } + // END: This part here is for exemplary purposes - Please do not copy to your production code + + // reset values always when configuring hardware + for (const auto & [name, descr] : joint_state_interfaces_) + { + set_state(name, 0.0); + } + for (const auto & [name, descr] : joint_command_interfaces_) + { + set_command(name, 0.0); + } + RCLCPP_INFO(get_logger(), "Successfully configured!"); + + return hardware_interface::CallbackReturn::SUCCESS; +} + +hardware_interface::CallbackReturn DiffBotSystemHardware::on_activate( + const rclcpp_lifecycle::State & /*previous_state*/) +{ + // BEGIN: This part here is for exemplary purposes - Please do not copy to your production code + RCLCPP_INFO(get_logger(), "Activating ...please wait..."); + + for (auto i = 0; i < hw_start_sec_; i++) + { + rclcpp::sleep_for(std::chrono::seconds(1)); + RCLCPP_INFO(get_logger(), "%.1f seconds left...", hw_start_sec_ - i); + } + // END: This part here is for exemplary purposes - Please do not copy to your production code + + // command and state should be equal when starting + for (const auto & [name, descr] : joint_command_interfaces_) + { + set_command(name, get_state(name)); + } + + RCLCPP_INFO(get_logger(), "Successfully activated!"); + + return hardware_interface::CallbackReturn::SUCCESS; +} + +hardware_interface::CallbackReturn DiffBotSystemHardware::on_deactivate( + const rclcpp_lifecycle::State & /*previous_state*/) +{ + // BEGIN: This part here is for exemplary purposes - Please do not copy to your production code + RCLCPP_INFO(get_logger(), "Deactivating ...please wait..."); + + for (auto i = 0; i < hw_stop_sec_; i++) + { + rclcpp::sleep_for(std::chrono::seconds(1)); + RCLCPP_INFO(get_logger(), "%.1f seconds left...", hw_stop_sec_ - i); + } + // END: This part here is for exemplary purposes - Please do not copy to your production code + + RCLCPP_INFO(get_logger(), "Successfully deactivated!"); + + return hardware_interface::CallbackReturn::SUCCESS; +} + +hardware_interface::return_type DiffBotSystemHardware::read( + const rclcpp::Time & /*time*/, const rclcpp::Duration & period) +{ + // BEGIN: This part here is for exemplary purposes - Please do not copy to your production code + std::stringstream ss; + ss << "Reading states:"; + ss << std::fixed << std::setprecision(2); + for (const auto & [name, descr] : joint_state_interfaces_) + { + if (descr.get_interface_name() == hardware_interface::HW_IF_POSITION) + { + // Simulate DiffBot wheels's movement as a first-order system + // Update the joint status: this is a revolute joint without any limit. + // Simply integrates + auto velo = get_command(descr.get_prefix_name() + "/" + hardware_interface::HW_IF_VELOCITY); + set_state(name, get_state(name) + period.seconds() * velo); + + ss << std::endl + << "\t position " << get_state(name) << " and velocity " << velo << " for '" << name + << "'!"; + } + } + RCLCPP_INFO_THROTTLE(get_logger(), *get_clock(), 500, "%s", ss.str().c_str()); + // END: This part here is for exemplary purposes - Please do not copy to your production code + + return hardware_interface::return_type::OK; +} + +hardware_interface::return_type ros2_control_demo_example_16 ::DiffBotSystemHardware::write( + const rclcpp::Time & /*time*/, const rclcpp::Duration & /*period*/) +{ + // BEGIN: This part here is for exemplary purposes - Please do not copy to your production code + std::stringstream ss; + ss << "Writing commands:"; + for (const auto & [name, descr] : joint_command_interfaces_) + { + // Simulate sending commands to the hardware + set_state(name, get_command(name)); + + ss << std::fixed << std::setprecision(2) << std::endl + << "\t" << "command " << get_command(name) << " for '" << name << "'!"; + } + RCLCPP_INFO_THROTTLE(get_logger(), *get_clock(), 500, "%s", ss.str().c_str()); + // END: This part here is for exemplary purposes - Please do not copy to your production code + + return hardware_interface::return_type::OK; +} + +} // namespace ros2_control_demo_example_16 + +#include "pluginlib/class_list_macros.hpp" +PLUGINLIB_EXPORT_CLASS( + ros2_control_demo_example_16::DiffBotSystemHardware, hardware_interface::SystemInterface) diff --git a/example_16/hardware/include/ros2_control_demo_example_16/diffbot_system.hpp b/example_16/hardware/include/ros2_control_demo_example_16/diffbot_system.hpp new file mode 100644 index 000000000..0eeb99f70 --- /dev/null +++ b/example_16/hardware/include/ros2_control_demo_example_16/diffbot_system.hpp @@ -0,0 +1,66 @@ +// Copyright 2021 ros2_control Development Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef ROS2_CONTROL_DEMO_EXAMPLE_16__DIFFBOT_SYSTEM_HPP_ +#define ROS2_CONTROL_DEMO_EXAMPLE_16__DIFFBOT_SYSTEM_HPP_ + +#include +#include +#include + +#include "hardware_interface/handle.hpp" +#include "hardware_interface/hardware_info.hpp" +#include "hardware_interface/system_interface.hpp" +#include "hardware_interface/types/hardware_interface_return_values.hpp" +#include "rclcpp/clock.hpp" +#include "rclcpp/duration.hpp" +#include "rclcpp/macros.hpp" +#include "rclcpp/time.hpp" +#include "rclcpp_lifecycle/node_interfaces/lifecycle_node_interface.hpp" +#include "rclcpp_lifecycle/state.hpp" + +namespace ros2_control_demo_example_16 +{ +class DiffBotSystemHardware : public hardware_interface::SystemInterface +{ +public: + RCLCPP_SHARED_PTR_DEFINITIONS(DiffBotSystemHardware); + + hardware_interface::CallbackReturn on_init( + const hardware_interface::HardwareInfo & info) override; + + hardware_interface::CallbackReturn on_configure( + const rclcpp_lifecycle::State & previous_state) override; + + hardware_interface::CallbackReturn on_activate( + const rclcpp_lifecycle::State & previous_state) override; + + hardware_interface::CallbackReturn on_deactivate( + const rclcpp_lifecycle::State & previous_state) override; + + hardware_interface::return_type read( + const rclcpp::Time & time, const rclcpp::Duration & period) override; + + hardware_interface::return_type write( + const rclcpp::Time & time, const rclcpp::Duration & period) override; + +private: + // Parameters for the DiffBot simulation + double hw_start_sec_; + double hw_stop_sec_; +}; + +} // namespace ros2_control_demo_example_16 + +#endif // ROS2_CONTROL_DEMO_EXAMPLE_16__DIFFBOT_SYSTEM_HPP_ diff --git a/example_16/package.xml b/example_16/package.xml new file mode 100644 index 000000000..f802f4237 --- /dev/null +++ b/example_16/package.xml @@ -0,0 +1,46 @@ + + + + ros2_control_demo_example_16 + 0.0.0 + Demo package of `ros2_control` hardware for DiffBot. + + Dr.-Ing. Denis Štogl + Bence Magyar + Christoph Froehlich + + Dr.-Ing. Denis Štogl + + Apache-2.0 + + ament_cmake + + backward_ros + hardware_interface + pluginlib + rclcpp + rclcpp_lifecycle + + controller_manager + diff_drive_controller + pid_controller + joint_state_broadcaster + joint_state_publisher_gui + robot_state_publisher + ros2_control_demo_description + ros2_controllers_test_nodes + ros2controlcli + ros2launch + rviz2 + xacro + + ament_cmake_pytest + controller_manager + launch_testing_ros + liburdfdom-tools + xacro + + + ament_cmake + + diff --git a/example_16/ros2_control_demo_example_16.xml b/example_16/ros2_control_demo_example_16.xml new file mode 100644 index 000000000..4508456d1 --- /dev/null +++ b/example_16/ros2_control_demo_example_16.xml @@ -0,0 +1,9 @@ + + + + The ros2_control DiffBot example using a system hardware interface-type. It uses velocity command and position state interface. The example is the starting point to implement a hardware interface for differential-drive mobile robots. + + + diff --git a/example_16/test/test_diffbot_launch.py b/example_16/test/test_diffbot_launch.py new file mode 100644 index 000000000..6ab19d54b --- /dev/null +++ b/example_16/test/test_diffbot_launch.py @@ -0,0 +1,104 @@ +# Copyright (c) 2024 AIT - Austrian Institute of Technology GmbH +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the {copyright_holder} nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +# Author: Christoph Froehlich + +import os +import pytest +import unittest + +from ament_index_python.packages import get_package_share_directory +from launch import LaunchDescription +from launch.actions import IncludeLaunchDescription +from launch.launch_description_sources import PythonLaunchDescriptionSource +from launch_testing.actions import ReadyToTest + +# import launch_testing.markers +import rclpy +from controller_manager.test_utils import ( + check_controllers_running, + check_if_js_published, + check_node_running, +) + + +# Executes the given launch file and checks if all nodes can be started +@pytest.mark.rostest +def generate_test_description(): + launch_include = IncludeLaunchDescription( + PythonLaunchDescriptionSource( + os.path.join( + get_package_share_directory("ros2_control_demo_example_16"), + "launch/diffbot.launch.py", + ) + ), + launch_arguments={"gui": "False"}.items(), + ) + + return LaunchDescription([launch_include, ReadyToTest()]) + + +# This is our test fixture. Each method is a test case. +# These run alongside the processes specified in generate_test_description() +class TestFixture(unittest.TestCase): + @classmethod + def setUpClass(cls): + rclpy.init() + + @classmethod + def tearDownClass(cls): + rclpy.shutdown() + + def setUp(self): + self.node = rclpy.create_node("test_node") + + def tearDown(self): + self.node.destroy_node() + + def test_node_start(self, proc_output): + check_node_running(self.node, "robot_state_publisher") + + def test_controller_running(self, proc_output): + # disable this test for now, as the activation of the diffbot_base_controller fails + # cnames = ["diffbot_base_controller", "joint_state_broadcaster"] + cnames = ["joint_state_broadcaster"] + + check_controllers_running(self.node, cnames) + + def test_check_if_msgs_published(self): + check_if_js_published("/joint_states", ["left_wheel_joint", "right_wheel_joint"]) + + +# TODO(anyone): enable this if shutdown of ros2_control_node does not fail anymore +# @launch_testing.post_shutdown_test() +# # These tests are run after the processes in generate_test_description() have shutdown. +# class TestDescriptionCraneShutdown(unittest.TestCase): + +# def test_exit_codes(self, proc_info): +# """Check if the processes exited normally.""" +# launch_testing.asserts.assertExitCodes(proc_info) diff --git a/example_16/test/test_urdf_xacro.py b/example_16/test/test_urdf_xacro.py new file mode 100644 index 000000000..744e0e0d4 --- /dev/null +++ b/example_16/test/test_urdf_xacro.py @@ -0,0 +1,77 @@ +# Copyright (c) 2022 FZI Forschungszentrum Informatik +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the {copyright_holder} nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +# Author: Lukas Sackewitz + +import os +import shutil +import subprocess +import tempfile + +from ament_index_python.packages import get_package_share_directory + + +def test_urdf_xacro(): + # General Arguments + description_package = "ros2_control_demo_example_16" + description_file = "diffbot.urdf.xacro" + + description_file_path = os.path.join( + get_package_share_directory(description_package), "urdf", description_file + ) + + (_, tmp_urdf_output_file) = tempfile.mkstemp(suffix=".urdf") + + # Compose `xacro` and `check_urdf` command + xacro_command = ( + f"{shutil.which('xacro')}" f" {description_file_path}" f" > {tmp_urdf_output_file}" + ) + check_urdf_command = f"{shutil.which('check_urdf')} {tmp_urdf_output_file}" + + # Try to call processes but finally remove the temp file + try: + xacro_process = subprocess.run( + xacro_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True + ) + + assert xacro_process.returncode == 0, " --- XACRO command failed ---" + + check_urdf_process = subprocess.run( + check_urdf_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True + ) + + assert ( + check_urdf_process.returncode == 0 + ), "\n --- URDF check failed! --- \nYour xacro does not unfold into a proper urdf robot description. Please check your xacro file." + + finally: + os.remove(tmp_urdf_output_file) + + +if __name__ == "__main__": + test_urdf_xacro() diff --git a/example_16/test/test_view_robot_launch.py b/example_16/test/test_view_robot_launch.py new file mode 100644 index 000000000..bd1e87ff6 --- /dev/null +++ b/example_16/test/test_view_robot_launch.py @@ -0,0 +1,54 @@ +# Copyright (c) 2022 FZI Forschungszentrum Informatik +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the {copyright_holder} nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +# Author: Lukas Sackewitz + +import os +import pytest + +from ament_index_python.packages import get_package_share_directory +from launch import LaunchDescription +from launch.actions import IncludeLaunchDescription +from launch.launch_description_sources import PythonLaunchDescriptionSource +from launch_testing.actions import ReadyToTest + + +# Executes the given launch file and checks if all nodes can be started +@pytest.mark.rostest +def generate_test_description(): + launch_include = IncludeLaunchDescription( + PythonLaunchDescriptionSource( + os.path.join( + get_package_share_directory("ros2_control_demo_example_16"), + "launch/view_robot.launch.py", + ) + ), + launch_arguments={"gui": "true"}.items(), + ) + + return LaunchDescription([launch_include, ReadyToTest()]) From 66fde02b25d4f31919f11a05564f6b5e0223c6d7 Mon Sep 17 00:00:00 2001 From: Christoph Froehlich Date: Fri, 31 Jan 2025 19:04:21 +0000 Subject: [PATCH 02/20] Fix speed_limiter parameters --- .../config/diffbot_chained_controllers.yaml | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/example_16/bringup/config/diffbot_chained_controllers.yaml b/example_16/bringup/config/diffbot_chained_controllers.yaml index 063c9f2db..db0518243 100644 --- a/example_16/bringup/config/diffbot_chained_controllers.yaml +++ b/example_16/bringup/config/diffbot_chained_controllers.yaml @@ -79,24 +79,18 @@ diffbot_base_controller: # Velocity and acceleration limits # Whenever a min_* is unspecified, default to -max_* - linear.x.has_velocity_limits: true - linear.x.has_acceleration_limits: true - linear.x.has_jerk_limits: false linear.x.max_velocity: 1.0 linear.x.min_velocity: -1.0 linear.x.max_acceleration: 1.0 - linear.x.max_jerk: 0.0 - linear.x.min_jerk: 0.0 + linear.x.max_jerk: .NAN + linear.x.min_jerk: .NAN - angular.z.has_velocity_limits: true - angular.z.has_acceleration_limits: true - angular.z.has_jerk_limits: false angular.z.max_velocity: 1.0 angular.z.min_velocity: -1.0 angular.z.max_acceleration: 1.0 angular.z.min_acceleration: -1.0 - angular.z.max_jerk: 0.0 - angular.z.min_jerk: 0.0 + angular.z.max_jerk: .NAN + angular.z.min_jerk: .NAN forward_velocity_controller_for_debug: ros__parameters: From 96c7211b0c835aa4884e1d7e9099b5e2051d73d9 Mon Sep 17 00:00:00 2001 From: Christoph Froehlich Date: Fri, 31 Jan 2025 19:05:06 +0000 Subject: [PATCH 03/20] Spawn the two PID controllers at once --- example_16/bringup/launch/diffbot.launch.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/example_16/bringup/launch/diffbot.launch.py b/example_16/bringup/launch/diffbot.launch.py index d6f9cf9c5..33e94ff26 100644 --- a/example_16/bringup/launch/diffbot.launch.py +++ b/example_16/bringup/launch/diffbot.launch.py @@ -97,16 +97,15 @@ def generate_launch_description(): arguments=["joint_state_broadcaster"], ) - pid_controller_left_wheel_joint_spawner = Node( + pid_controllers_spawner = Node( package="controller_manager", executable="spawner", - arguments=["pid_controller_left_wheel_joint", "--param-file", robot_controllers], - ) - - pid_controller_right_wheel_joint_spawner = Node( - package="controller_manager", - executable="spawner", - arguments=["pid_controller_right_wheel_joint", "--param-file", robot_controllers], + arguments=[ + "pid_controller_left_wheel_joint", + "pid_controller_right_wheel_joint", + "--param-file", + robot_controllers, + ], ) robot_base_controller_spawner = Node( @@ -132,7 +131,7 @@ def generate_launch_description(): delay_robot_base_after_pid_controller_spawner = RegisterEventHandler( event_handler=OnProcessExit( - target_action=pid_controller_right_wheel_joint_spawner, + target_action=pid_controllers_spawner, on_exit=[robot_base_controller_spawner], ) ) @@ -149,8 +148,7 @@ def generate_launch_description(): nodes = [ control_node, robot_state_pub_node, - pid_controller_left_wheel_joint_spawner, - pid_controller_right_wheel_joint_spawner, + pid_controllers_spawner, delay_robot_base_after_pid_controller_spawner, delay_rviz_after_joint_state_broadcaster_spawner, delay_joint_state_broadcaster_after_robot_base_controller_spawner, From fd2332dedce8c1c0d638ce13a856f9d6332bc6ae Mon Sep 17 00:00:00 2001 From: Christoph Froehlich Date: Fri, 31 Jan 2025 19:05:36 +0000 Subject: [PATCH 04/20] Use PID with velocity control loop --- example_16/bringup/config/diffbot_chained_controllers.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/example_16/bringup/config/diffbot_chained_controllers.yaml b/example_16/bringup/config/diffbot_chained_controllers.yaml index db0518243..a86ac29ba 100644 --- a/example_16/bringup/config/diffbot_chained_controllers.yaml +++ b/example_16/bringup/config/diffbot_chained_controllers.yaml @@ -27,7 +27,6 @@ pid_controller_left_wheel_joint: command_interface: velocity reference_and_state_interfaces: - - position - velocity gains: @@ -43,7 +42,6 @@ pid_controller_right_wheel_joint: command_interface: velocity reference_and_state_interfaces: - - position - velocity gains: @@ -60,6 +58,9 @@ diffbot_base_controller: #wheels_per_side: 1 # actually 2, but both are controlled by 1 signal wheel_radius: 0.015 + # we have velocity feedback + position_feedback: false + wheel_separation_multiplier: 1.0 left_wheel_radius_multiplier: 1.0 right_wheel_radius_multiplier: 1.0 From 164dabdb57c424752c48d8a35bb8330e9a765dd0 Mon Sep 17 00:00:00 2001 From: Christoph Froehlich Date: Fri, 31 Jan 2025 19:08:15 +0000 Subject: [PATCH 05/20] Some updates to the docs --- example_16/doc/userdoc.rst | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/example_16/doc/userdoc.rst b/example_16/doc/userdoc.rst index 37027a92b..1995a8b2c 100644 --- a/example_16/doc/userdoc.rst +++ b/example_16/doc/userdoc.rst @@ -8,10 +8,7 @@ DiffBot with Chained Controllers *DiffBot*, or ''Differential Mobile Robot'', is a simple mobile base with differential drive. The robot is basically a box moving according to differential drive kinematics. -*example_16* is based of *example_2* which the hardware interface plugin is implemented having only one interface. This example demonstrates using a chaininged diff_drive_controller and pid_controller for each wheel to control the robot. The controllers are chained together to control the robot's velocity. - -* The communication is done using proprietary API to communicate with the robot control box. -* Data for all joints is exchanged at once. +*example_16* is based on *example_2*. This example demonstrates using a chained diff_drive_controller and pid_controller for each wheel to control the robot. The controllers are chained together to control the robot's twist. The *DiffBot* URDF files can be found in ``description/urdf`` folder. From 30017bd2b2a8986ce12a2ed9a141fd6e7c880c1d Mon Sep 17 00:00:00 2001 From: Julia Jia Date: Sat, 1 Feb 2025 23:32:05 -0800 Subject: [PATCH 06/20] Update launch.py and add steps to documentation --- example_16/bringup/launch/diffbot.launch.py | 32 +++- .../description/launch/view_robot.launch.py | 8 +- example_16/doc/userdoc.rst | 137 ++++++++++++++++-- example_16/hardware/diffbot_system.cpp | 2 +- .../diffbot_system.hpp | 2 +- example_16/test/test_diffbot_launch.py | 37 ++--- 6 files changed, 166 insertions(+), 52 deletions(-) diff --git a/example_16/bringup/launch/diffbot.launch.py b/example_16/bringup/launch/diffbot.launch.py index 33e94ff26..88679fa08 100644 --- a/example_16/bringup/launch/diffbot.launch.py +++ b/example_16/bringup/launch/diffbot.launch.py @@ -1,4 +1,4 @@ -# Copyright 2020 ros2_control Development Team +# Copyright 2025 ros2_control Development Team # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,11 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. +# +# Author: Julia Jia + from launch import LaunchDescription from launch.actions import DeclareLaunchArgument, RegisterEventHandler -from launch.conditions import IfCondition +from launch.conditions import IfCondition, UnlessCondition from launch.event_handlers import OnProcessExit -from launch.substitutions import Command, FindExecutable, PathJoinSubstitution, LaunchConfiguration +from launch.substitutions import Command, FindExecutable, PathJoinSubstitution, LaunchConfiguration, TextSubstitution from launch_ros.actions import Node from launch_ros.substitutions import FindPackageShare @@ -28,7 +31,7 @@ def generate_launch_description(): declared_arguments.append( DeclareLaunchArgument( "gui", - default_value="false", + default_value="true", description="Start RViz2 automatically with this launch file.", ) ) @@ -39,10 +42,18 @@ def generate_launch_description(): description="Start robot with mock hardware mirroring command to its states.", ) ) + declared_arguments.append( + DeclareLaunchArgument( + "fixed_frame_id", + default_value="odom", + description="Fixed frame id of the robot.", + ) + ) # Initialize Arguments gui = LaunchConfiguration("gui") use_mock_hardware = LaunchConfiguration("use_mock_hardware") + fixed_frame_id = LaunchConfiguration("fixed_frame_id") # Get URDF via xacro robot_description_content = Command( @@ -82,12 +93,13 @@ def generate_launch_description(): output="both", parameters=[robot_description], ) + rviz_node = Node( package="rviz2", executable="rviz2", name="rviz2", output="log", - arguments=["-d", rviz_config_file], + arguments=["-d", rviz_config_file, "-f", fixed_frame_id], condition=IfCondition(gui), ) @@ -107,17 +119,21 @@ def generate_launch_description(): robot_controllers, ], ) - + + # start the base controller in inactive mode robot_base_controller_spawner = Node( package="controller_manager", executable="spawner", arguments=[ "diffbot_base_controller", - # "forward_velocity_controller", "--param-file", robot_controllers, "--controller-ros-args", "-r /diffbot_base_controller/cmd_vel:=/cmd_vel", + # "--inactive", + "--ros-args", + "--log-level", + "debug", ], ) @@ -136,7 +152,7 @@ def generate_launch_description(): ) ) - # Delay start of joint_state_broadcaster after `robot_controller` + # Delay start of joint_state_broadcaster after `robot_base_controller` # TODO(anyone): This is a workaround for flaky tests. Remove when fixed. delay_joint_state_broadcaster_after_robot_base_controller_spawner = RegisterEventHandler( event_handler=OnProcessExit( diff --git a/example_16/description/launch/view_robot.launch.py b/example_16/description/launch/view_robot.launch.py index 41589297f..49f510120 100644 --- a/example_16/description/launch/view_robot.launch.py +++ b/example_16/description/launch/view_robot.launch.py @@ -1,4 +1,4 @@ -# Copyright 2021 Stogl Robotics Consulting UG (haftungsbeschränkt) +# Copyright 2025 ros2_control Development Team # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -77,7 +77,7 @@ def generate_launch_description(): ] ) robot_description = {"robot_description": robot_description_content} - + rviz_config_file = PathJoinSubstitution( [FindPackageShare(description_package), "diffbot/rviz", "diffbot_view.rviz"] ) @@ -93,12 +93,14 @@ def generate_launch_description(): output="both", parameters=[robot_description], ) + + # start rviz2 with intial fixed frame id as base_link rviz_node = Node( package="rviz2", executable="rviz2", name="rviz2", output="log", - arguments=["-d", rviz_config_file], + arguments=["-d", rviz_config_file, "-f", "base_link"], condition=IfCondition(gui), ) diff --git a/example_16/doc/userdoc.rst b/example_16/doc/userdoc.rst index 1995a8b2c..cd7a82423 100644 --- a/example_16/doc/userdoc.rst +++ b/example_16/doc/userdoc.rst @@ -8,18 +8,33 @@ DiffBot with Chained Controllers *DiffBot*, or ''Differential Mobile Robot'', is a simple mobile base with differential drive. The robot is basically a box moving according to differential drive kinematics. -*example_16* is based on *example_2*. This example demonstrates using a chained diff_drive_controller and pid_controller for each wheel to control the robot. The controllers are chained together to control the robot's twist. +*example_16* extends *example_2* by demonstrating controller chaining. It shows how to chain a diff_drive_controller with two pid_controllers (one for each wheel) to achieve coordinated robot motion. The pid_controllers directly control wheel velocities, while the diff_drive_controller converts desired robot twist into wheel velocity commands. The *DiffBot* URDF files can be found in ``description/urdf`` folder. .. include:: ../../doc/run_from_docker.rst +Inspired by the scenario outlined in `ROS2 controller manager chaining documentation `__. +We'll implement a segament of the scenario and cover the chain of 'diff_drive_controller' with two PID controllers. Along with the process, we call out the pattern of contructing virtual interfaces in terms of controllers with details on what is happening behind the scenes. +Two flows are demonstrated: activation/execution flow and deactivation flow. + +In the activation and execution flow, we follow these steps: + + 1. First, we activate only the PID controllers to verify proper motor velocity control. The PID controllers accept commands through topics and provide virtual interfaces for chaining. + + 2. Next, we activate the diff_drive_controller, which connects to the PID controllers' virtual interfaces. When chained, the PID controllers switch to chained mode and disable their external command topics. This allows us to verify the differential drive kinematics. + + 3. Upon activation, the diff_drive_controller provides odometry state interfaces. + + 4. Finally, we send velocity commands to test robot movement, with dynamics enabled to demonstrate the PID controllers' behavior. + +For the deactivation flow, controllers must be deactivated in the reverse order of their chain. When a controller is deactivated, all controllers that depend on it must also be deactivated. We demonstrate this process step by step and examine the controller states at each stage. Tutorial steps -------------------------- -1. To check that *DiffBot* description is working properly use following launch commands +1. The first step is to check that *DiffBot* description is working properly use following launch commands .. code-block:: shell @@ -37,7 +52,7 @@ Tutorial steps .. code-block:: shell - ros2 launch ros2_control_demo_example_16 diffbot.launch.py + ros2 launch ros2_control_demo_example_16 diffbot.launch.py inactive_mode:=true fixed_frame_id:=base_link The launch file loads and starts the robot hardware, controllers and opens *RViz*. In the starting terminal you will see a lot of output from the hardware implementation showing its internal states. @@ -55,19 +70,34 @@ Tutorial steps You should get .. code-block:: shell - + command interfaces - left_wheel_joint/velocity [available] [claimed] - right_wheel_joint/velocity [available] [claimed] + diffbot_base_controller/angular/velocity [unavailable] [unclaimed] + diffbot_base_controller/linear/velocity [unavailable] [unclaimed] + left_wheel_joint/velocity [available] [claimed] + pid_controller_left_wheel_joint/left_wheel_joint/velocity [available] [unclaimed] + pid_controller_right_wheel_joint/right_wheel_joint/velocity [available] [unclaimed] + right_wheel_joint/velocity [available] [claimed] state interfaces - left_wheel_joint/position - left_wheel_joint/velocity - right_wheel_joint/position - right_wheel_joint/velocity + left_wheel_joint/position + left_wheel_joint/velocity + pid_controller_left_wheel_joint/left_wheel_joint/velocity + pid_controller_right_wheel_joint/right_wheel_joint/velocity + right_wheel_joint/position + right_wheel_joint/velocity The ``[claimed]`` marker on command interfaces means that a controller has access to command *DiffBot*. - Furthermore, we can see that the command interface is of type ``velocity``, which is typical for a differential drive robot. + In this example, diff_drive_controller/DiffDriveController is chinable after controller which another controllre can reference following command interfaces. Both intrfaces are in ``unclaimed`` state, which means that no controller is using them. + + .. code-block:: shell + command interfaces + diffbot_base_controller/angular/velocity [available] [unclaimed] + diffbot_base_controller/linear/velocity [available] [unclaimed] + + + + more, we can see that the command interface is of type ``velocity``, which is typical for a differential drive robot. 4. Check if controllers are running @@ -79,10 +109,89 @@ Tutorial steps .. code-block:: shell - diffbot_base_controller[diff_drive_controller/DiffDriveController] active - joint_state_broadcaster[joint_state_broadcaster/JointStateBroadcaster] active + joint_state_broadcaster joint_state_broadcaster/JointStateBroadcaster active + diffbot_base_controller diff_drive_controller/DiffDriveController inactive + pid_controller_right_wheel_joint pid_controller/PidController active + pid_controller_left_wheel_joint pid_controller/PidController active + + +5. Activate the chained diff_drive_controller + + .. code-block:: shell + + ros2 control switch_controllers --activate diffbot_base_controller + + You should see the following output: + + .. code-block:: shell + + Successfully switched controllers + +6. Check the hardware interfaces as well as the controllers + + .. code-block:: shell + + ros2 control list_hardware_interfaces + ros2 control list_controllers + + You should see the following output: + + .. code-block:: shell + + command interfaces + diffbot_base_controller/angular/velocity [available] [unclaimed] + diffbot_base_controller/linear/velocity [available] [unclaimed] + left_wheel_joint/velocity [available] [claimed] + pid_controller_left_wheel_joint/left_wheel_joint/velocity [available] [claimed] + pid_controller_right_wheel_joint/right_wheel_joint/velocity [available] [claimed] + right_wheel_joint/velocity [available] [claimed] + state interfaces + left_wheel_joint/position + left_wheel_joint/velocity + pid_controller_left_wheel_joint/left_wheel_joint/velocity + pid_controller_right_wheel_joint/right_wheel_joint/velocity + right_wheel_joint/position + right_wheel_joint/velocity + + + .. code-block:: shell + + joint_state_broadcaster joint_state_broadcaster/JointStateBroadcaster active + diffbot_base_controller diff_drive_controller/DiffDriveController active + pid_controller_right_wheel_joint pid_controller/PidController active + pid_controller_left_wheel_joint pid_controller/PidController active + + + +7. Send a command to the left wheel + +.. code-block:: shell + ros2 topic pub -1 /pid_controller_left_wheel_joint/reference control_msgs/msg/MultiDOFCommand "{ + dof_names: ['left_wheel_joint/velocity'], + values: [1.0], + values_dot: [0.0] + }" + + +8. Send a command to the right wheel + + .. code-block:: shell + + ros2 topic pub -1 /pid_controller_right_wheel_joint/reference control_msgs/msg/MultiDOFCommand "{ + dof_names: ['right_wheel_joint/velocity'], + values: [1.0], + values_dot: [0.0] + }" + + +7. Change the fixed frame id for rviz to odom + + + Look for "Fixed Frame" in the "Global Options" section + Click on the frame name + Type or select the new frame ID and change it to odom then click on reset button -5. If everything is fine, now you can send a command to *Diff Drive Controller* using ROS 2 CLI interface: +8. If everything is fine, now you can send a command to *Diff Drive Controller* using ROS 2 CLI interface: .. code-block:: shell diff --git a/example_16/hardware/diffbot_system.cpp b/example_16/hardware/diffbot_system.cpp index 8cae96625..95867f923 100644 --- a/example_16/hardware/diffbot_system.cpp +++ b/example_16/hardware/diffbot_system.cpp @@ -1,4 +1,4 @@ -// Copyright 2021 ros2_control Development Team +// Copyright 2025 ros2_control Development Team // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/example_16/hardware/include/ros2_control_demo_example_16/diffbot_system.hpp b/example_16/hardware/include/ros2_control_demo_example_16/diffbot_system.hpp index 0eeb99f70..495098a6e 100644 --- a/example_16/hardware/include/ros2_control_demo_example_16/diffbot_system.hpp +++ b/example_16/hardware/include/ros2_control_demo_example_16/diffbot_system.hpp @@ -1,4 +1,4 @@ -// Copyright 2021 ros2_control Development Team +// Copyright 2025 ros2_control Development Team // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/example_16/test/test_diffbot_launch.py b/example_16/test/test_diffbot_launch.py index 6ab19d54b..be1e0a4b8 100644 --- a/example_16/test/test_diffbot_launch.py +++ b/example_16/test/test_diffbot_launch.py @@ -1,32 +1,19 @@ -# Copyright (c) 2024 AIT - Austrian Institute of Technology GmbH +# Copyright 2025 ros2_control Development Team # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# http://www.apache.org/licenses/LICENSE-2.0 # -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# * Neither the name of the {copyright_holder} nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + # -# Author: Christoph Froehlich +# Author: Julia Jia import os import pytest From aea8f7841b08b38235c27c8cd358d12330ffb6e7 Mon Sep 17 00:00:00 2001 From: Julia Jia Date: Mon, 3 Feb 2025 22:14:21 -0800 Subject: [PATCH 07/20] Make a working example --- .gitignore | 2 + README.md | 4 + .../config/diffbot_chained_controllers.yaml | 8 +- example_16/bringup/launch/diffbot.launch.py | 13 +- .../description/launch/view_robot.launch.py | 7 +- example_16/doc/userdoc.rst | 283 ++++++------------ example_16/hardware/diffbot_system.cpp | 4 +- example_16/test/test_diffbot_launch.py | 10 +- 8 files changed, 116 insertions(+), 215 deletions(-) diff --git a/.gitignore b/.gitignore index 02e922e35..bb56e221f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .ccache .work +/.vscode +*.pyc diff --git a/README.md b/README.md index fa7a539c9..8633cd050 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,10 @@ The following examples are part of this demo repository: This example shows how to integrate multiple robots under different controller manager instances. +* Example 16: ["DiffBot with Chained Controllers"](example_16) + + This example shows how to create chained controllers using diff_drive_controller and two pid_controllers to control a differential drive robot. + ## Structure The repository is structured into `example_XY` folders that fully contained packages with names `ros2_control_demos_example_XY`. diff --git a/example_16/bringup/config/diffbot_chained_controllers.yaml b/example_16/bringup/config/diffbot_chained_controllers.yaml index a86ac29ba..c73f4a9e0 100644 --- a/example_16/bringup/config/diffbot_chained_controllers.yaml +++ b/example_16/bringup/config/diffbot_chained_controllers.yaml @@ -30,8 +30,8 @@ pid_controller_left_wheel_joint: - velocity gains: - left_wheel_joint: {"p": 1.0, "i": 0.5, "d": 0.2, "i_clamp_min": -2.0, "i_clamp_max": 2.0, "antiwindup": true} - + # control the velocity, no d term + left_wheel_joint: {"p": 0.15, "i": 0.05, "d": 0.0, "i_clamp_min": -2.0, "i_clamp_max": 2.0, "antiwindup": true, "feedforward_gain": 0.95} pid_controller_right_wheel_joint: ros__parameters: @@ -45,8 +45,8 @@ pid_controller_right_wheel_joint: - velocity gains: - right_wheel_joint: {"p": 1.0, "i": 0.5, "d": 0.2, "i_clamp_min": -2.0, "i_clamp_max": 2.0, "antiwindup": true} - + # control the velocity, no d term + right_wheel_joint: {"p": 0.15, "i": 0.05, "d": 0.0, "i_clamp_min": -2.0, "i_clamp_max": 2.0, "antiwindup": true, "feedforward_gain": 0.95} diffbot_base_controller: ros__parameters: diff --git a/example_16/bringup/launch/diffbot.launch.py b/example_16/bringup/launch/diffbot.launch.py index 88679fa08..f9fb705da 100644 --- a/example_16/bringup/launch/diffbot.launch.py +++ b/example_16/bringup/launch/diffbot.launch.py @@ -12,14 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -# -# Author: Julia Jia from launch import LaunchDescription from launch.actions import DeclareLaunchArgument, RegisterEventHandler -from launch.conditions import IfCondition, UnlessCondition +from launch.conditions import IfCondition from launch.event_handlers import OnProcessExit -from launch.substitutions import Command, FindExecutable, PathJoinSubstitution, LaunchConfiguration, TextSubstitution +from launch.substitutions import ( + Command, + FindExecutable, + PathJoinSubstitution, + LaunchConfiguration, +) from launch_ros.actions import Node from launch_ros.substitutions import FindPackageShare @@ -119,7 +122,7 @@ def generate_launch_description(): robot_controllers, ], ) - + # start the base controller in inactive mode robot_base_controller_spawner = Node( package="controller_manager", diff --git a/example_16/description/launch/view_robot.launch.py b/example_16/description/launch/view_robot.launch.py index 49f510120..a48efbec7 100644 --- a/example_16/description/launch/view_robot.launch.py +++ b/example_16/description/launch/view_robot.launch.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. + from launch import LaunchDescription from launch.actions import DeclareLaunchArgument from launch.conditions import IfCondition @@ -77,7 +78,7 @@ def generate_launch_description(): ] ) robot_description = {"robot_description": robot_description_content} - + rviz_config_file = PathJoinSubstitution( [FindPackageShare(description_package), "diffbot/rviz", "diffbot_view.rviz"] ) @@ -93,8 +94,8 @@ def generate_launch_description(): output="both", parameters=[robot_description], ) - - # start rviz2 with intial fixed frame id as base_link + + # start rviz2 with initial fixed frame id as base_link rviz_node = Node( package="rviz2", executable="rviz2", diff --git a/example_16/doc/userdoc.rst b/example_16/doc/userdoc.rst index cd7a82423..c333d58da 100644 --- a/example_16/doc/userdoc.rst +++ b/example_16/doc/userdoc.rst @@ -8,142 +8,61 @@ DiffBot with Chained Controllers *DiffBot*, or ''Differential Mobile Robot'', is a simple mobile base with differential drive. The robot is basically a box moving according to differential drive kinematics. -*example_16* extends *example_2* by demonstrating controller chaining. It shows how to chain a diff_drive_controller with two pid_controllers (one for each wheel) to achieve coordinated robot motion. The pid_controllers directly control wheel velocities, while the diff_drive_controller converts desired robot twist into wheel velocity commands. +This example extends *example_2* by demonstrating controller chaining with a diff_drive_controller and two pid_controllers (one for each wheel) to achieve coordinated robot motion. If you haven't already, you can find the instructions for *example_2* in :ref:`ros2_control_demos_example_2_userdoc`. It is recommended to follow the steps given in that tutorial first before proceeding with this one. + +This example demonstrates controller chaining as described in the `ROS2 controller manager chaining documentation `__. The control chain flows from the diff_drive_controller through two PID controllers to the DiffBot hardware. The diff_drive_controller converts desired robot twist into wheel velocity commands, which are then processed by the PID controllers to directly control the wheel velocities. Additionally, this example shows how to enable the feedforward mode for the PID controllers. The *DiffBot* URDF files can be found in ``description/urdf`` folder. .. include:: ../../doc/run_from_docker.rst -Inspired by the scenario outlined in `ROS2 controller manager chaining documentation `__. -We'll implement a segament of the scenario and cover the chain of 'diff_drive_controller' with two PID controllers. Along with the process, we call out the pattern of contructing virtual interfaces in terms of controllers with details on what is happening behind the scenes. - -Two flows are demonstrated: activation/execution flow and deactivation flow. - -In the activation and execution flow, we follow these steps: - - 1. First, we activate only the PID controllers to verify proper motor velocity control. The PID controllers accept commands through topics and provide virtual interfaces for chaining. - - 2. Next, we activate the diff_drive_controller, which connects to the PID controllers' virtual interfaces. When chained, the PID controllers switch to chained mode and disable their external command topics. This allows us to verify the differential drive kinematics. - - 3. Upon activation, the diff_drive_controller provides odometry state interfaces. - - 4. Finally, we send velocity commands to test robot movement, with dynamics enabled to demonstrate the PID controllers' behavior. - -For the deactivation flow, controllers must be deactivated in the reverse order of their chain. When a controller is deactivated, all controllers that depend on it must also be deactivated. We demonstrate this process step by step and examine the controller states at each stage. Tutorial steps -------------------------- -1. The first step is to check that *DiffBot* description is working properly use following launch commands +1. To start *DiffBot* example open a terminal, source your ROS2-workspace and execute its launch file with .. code-block:: shell - ros2 launch ros2_control_demo_example_16 view_robot.launch.py - - .. warning:: - Getting the following output in terminal is OK: ``Warning: Invalid frame ID "odom" passed to canTransform argument target_frame - frame does not exist``. - This happens because ``joint_state_publisher_gui`` node need some time to start. - - .. image:: diffbot.png - :width: 400 - :alt: Differential Mobile Robot - -2. To start *DiffBot* example open a terminal, source your ROS2-workspace and execute its launch file with - - .. code-block:: shell - - ros2 launch ros2_control_demo_example_16 diffbot.launch.py inactive_mode:=true fixed_frame_id:=base_link + ros2 launch ros2_control_demo_example_16 diffbot.launch.py The launch file loads and starts the robot hardware, controllers and opens *RViz*. In the starting terminal you will see a lot of output from the hardware implementation showing its internal states. This excessive printing is only added for demonstration. In general, printing to the terminal should be avoided as much as possible in a hardware interface implementation. - If you can see an orange box in *RViz* everything has started properly. - Still, to be sure, let's introspect the control system before moving *DiffBot*. - -3. Check if the hardware interface loaded properly, by opening another terminal and executing - - .. code-block:: shell - - ros2 control list_hardware_interfaces - - You should get - - .. code-block:: shell - - command interfaces - diffbot_base_controller/angular/velocity [unavailable] [unclaimed] - diffbot_base_controller/linear/velocity [unavailable] [unclaimed] - left_wheel_joint/velocity [available] [claimed] - pid_controller_left_wheel_joint/left_wheel_joint/velocity [available] [unclaimed] - pid_controller_right_wheel_joint/right_wheel_joint/velocity [available] [unclaimed] - right_wheel_joint/velocity [available] [claimed] - state interfaces - left_wheel_joint/position - left_wheel_joint/velocity - pid_controller_left_wheel_joint/left_wheel_joint/velocity - pid_controller_right_wheel_joint/right_wheel_joint/velocity - right_wheel_joint/position - right_wheel_joint/velocity + If you can see an orange box in *RViz* everything has started properly. Let's introspect the control system before moving *DiffBot*. - The ``[claimed]`` marker on command interfaces means that a controller has access to command *DiffBot*. - - In this example, diff_drive_controller/DiffDriveController is chinable after controller which another controllre can reference following command interfaces. Both intrfaces are in ``unclaimed`` state, which means that no controller is using them. +2. Check controllers .. code-block:: shell - command interfaces - diffbot_base_controller/angular/velocity [available] [unclaimed] - diffbot_base_controller/linear/velocity [available] [unclaimed] - - - more, we can see that the command interface is of type ``velocity``, which is typical for a differential drive robot. - -4. Check if controllers are running - - .. code-block:: shell - - ros2 control list_controllers + $ ros2 control list_controllers You should get .. code-block:: shell - joint_state_broadcaster joint_state_broadcaster/JointStateBroadcaster active - diffbot_base_controller diff_drive_controller/DiffDriveController inactive - pid_controller_right_wheel_joint pid_controller/PidController active - pid_controller_left_wheel_joint pid_controller/PidController active - - -5. Activate the chained diff_drive_controller - - .. code-block:: shell - - ros2 control switch_controllers --activate diffbot_base_controller - - You should see the following output: - - .. code-block:: shell - - Successfully switched controllers + joint_state_broadcaster joint_state_broadcaster/JointStateBroadcaster active + diffbot_base_controller diff_drive_controller/DiffDriveController active + pid_controller_right_wheel_joint pid_controller/PidController active + pid_controller_left_wheel_joint pid_controller/PidController active -6. Check the hardware interfaces as well as the controllers +3. Check the hardware interface loaded by opening another terminal and executing .. code-block:: shell - ros2 control list_hardware_interfaces - ros2 control list_controllers + $ ros2 control list_hardware_interfaces - You should see the following output: + You should get - .. code-block:: shell + .. code-block:: shell command interfaces - diffbot_base_controller/angular/velocity [available] [unclaimed] - diffbot_base_controller/linear/velocity [available] [unclaimed] + diffbot_base_controller/angular/velocity [unavailable] [unclaimed] + diffbot_base_controller/linear/velocity [unavailable] [unclaimed] left_wheel_joint/velocity [available] [claimed] - pid_controller_left_wheel_joint/left_wheel_joint/velocity [available] [claimed] - pid_controller_right_wheel_joint/right_wheel_joint/velocity [available] [claimed] + pid_controller_left_wheel_joint/left_wheel_joint/velocity [available] [unclaimed] + pid_controller_right_wheel_joint/right_wheel_joint/velocity [available] [unclaimed] right_wheel_joint/velocity [available] [claimed] state interfaces left_wheel_joint/position @@ -154,48 +73,35 @@ Tutorial steps right_wheel_joint/velocity - .. code-block:: shell - - joint_state_broadcaster joint_state_broadcaster/JointStateBroadcaster active - diffbot_base_controller diff_drive_controller/DiffDriveController active - pid_controller_right_wheel_joint pid_controller/PidController active - pid_controller_left_wheel_joint pid_controller/PidController active - + The ``[claimed]`` marker on command interfaces means that a controller has access to command *DiffBot*. There are two ``[claimed]`` interfaces from pid_controller, one for left wheel and one for right wheel. These interfaces are referenced by diff_drive_controller. By referencing them, diff_drive_controller can send commands to these interfaces. If you see these, we've successfully chained the controllers. + There are also two ``[unclaimed]`` interfaces from diff_drive_controller, one for angular velocity and one for linear velocity. These are provided by the diff_drive_controller because it is chainable. You can ignore them since we don't use them in this example. -7. Send a command to the left wheel - -.. code-block:: shell - ros2 topic pub -1 /pid_controller_left_wheel_joint/reference control_msgs/msg/MultiDOFCommand "{ - dof_names: ['left_wheel_joint/velocity'], - values: [1.0], - values_dot: [0.0] - }" +4. We specified ``feedforward_gain`` as part of ``gains`` in diffbot_chained_controllers.yaml. To actually enable feedforward mode for the pid_controller, we need to use a service provided by pid_controller. Let's enable it. + .. code-block:: shell -8. Send a command to the right wheel + $ ros2 service call /pid_controller_left_wheel_joint/set_feedforward_control std_srvs/srv/SetBool "data: true" + $ ros2 service call /pid_controller_right_wheel_joint/set_feedforward_control std_srvs/srv/SetBool "data: true" - .. code-block:: shell + You should get - ros2 topic pub -1 /pid_controller_right_wheel_joint/reference control_msgs/msg/MultiDOFCommand "{ - dof_names: ['right_wheel_joint/velocity'], - values: [1.0], - values_dot: [0.0] - }" + .. code-block:: shell + response: + std_srvs.srv.SetBool_Response(success=True, message='') -7. Change the fixed frame id for rviz to odom +5. To see the pid_controller in action, let's subscribe to the controler_state topic, e.g. pid_controller_left_wheel_joint/controller_state topic. + .. code-block:: shell - Look for "Fixed Frame" in the "Global Options" section - Click on the frame name - Type or select the new frame ID and change it to odom then click on reset button + $ ros2 topic echo /pid_controller_left_wheel_joint/controller_state -8. If everything is fine, now you can send a command to *Diff Drive Controller* using ROS 2 CLI interface: +6. Now we are ready to send a command to move the robot. Send a command to *Diff Drive Controller* by opening another terminal and executing .. code-block:: shell - ros2 topic pub --rate 10 /cmd_vel geometry_msgs/msg/TwistStamped " + $ ros2 topic pub --rate 10 /cmd_vel geometry_msgs/msg/TwistStamped " twist: linear: x: 0.7 @@ -206,91 +112,78 @@ Tutorial steps y: 0.0 z: 1.0" - You should now see an orange box circling in *RViz*. - Also, you should see changing states in the terminal where launch file is started. - - .. code-block:: shell - - [ros2_control_node-1] [INFO] [1721762311.808415917] [controller_manager.resource_manager.hardware_component.system.DiffBot]: Writing commands: - [ros2_control_node-1] command 43.33 for 'left_wheel_joint'! - [ros2_control_node-1] command 50.00 for 'right_wheel_joint'! - -6. Let's introspect the ros2_control hardware component. Calling + You should now see robot is moving in circles in *RViz*. - .. code-block:: shell - - ros2 control list_hardware_components - - should give you +7. In the terminal where launch file is started, you should see the commands being sent to the wheels and how they are gradually stabilizing to the target velocity. .. code-block:: shell - Hardware Component 1 - name: DiffBot - type: system - plugin name: ros2_control_demo_example_16/DiffBotSystemHardware - state: id=3 label=active - command interfaces - left_wheel_joint/velocity [available] [claimed] - right_wheel_joint/velocity [available] [claimed] + [ros2_control_node-1] [INFO] [1738648404.508385200] [controller_manager.resource_manager.hardware_component.system.DiffBot]: Writing commands: + [ros2_control_node-1] command 0.00 for 'right_wheel_joint/velocity'! + [ros2_control_node-1] command 0.00 for 'left_wheel_joint/velocity'! - This shows that the custom hardware interface plugin is loaded and running. If you work on a real - robot and don't have a simulator running, it is often faster to use the ``mock_components/GenericSystem`` - hardware component instead of writing a custom one. Stop the launch file and start it again with - an additional parameter + [ros2_control_node-1] [INFO] [1738648405.008399450] [controller_manager.resource_manager.hardware_component.system.DiffBot]: Writing commands: + [ros2_control_node-1] command 14.55 for 'right_wheel_joint/velocity'! + [ros2_control_node-1] command 13.17 for 'left_wheel_joint/velocity'! - .. code-block:: shell + [ros2_control_node-1] [INFO] [1738648405.508445448] [controller_manager.resource_manager.hardware_component.system.DiffBot]: Writing commands: + [ros2_control_node-1] command 49.21 for 'right_wheel_joint/velocity'! + [ros2_control_node-1] command 44.52 for 'left_wheel_joint/velocity'! - ros2 launch ros2_control_demo_example_16 diffbot.launch.py use_mock_hardware:=True + [ros2_control_node-1] [INFO] [1738648406.108246536] [controller_manager.resource_manager.hardware_component.system.DiffBot]: Writing commands: + [ros2_control_node-1] command 49.73 for 'right_wheel_joint/velocity'! + [ros2_control_node-1] command 43.11 for 'left_wheel_joint/velocity'! - Calling +8. Let's go back to the terminal where we subscribed to the controller_state topic and see the changing states. .. code-block:: shell - ros2 control list_hardware_components + --- + header: + stamp: + sec: 1738639255 + nanosec: 743875549 + frame_id: '' + dof_states: + - name: left_wheel_joint + reference: 0.0 + feedback: 0.0 + feedback_dot: 0.0 + error: 0.0 + error_dot: 0.0 + time_step: 0.09971356 + output: 0.0 + + --- + header: + stamp: + sec: 1738639255 + nanosec: 844169802 + frame_id: '' + dof_states: + - name: left_wheel_joint + reference: 6.3405774 + feedback: 0.0 + feedback_dot: 0.0 + error: 6.3405774 + error_dot: 0.0 + time_step: 0.100294253 + output: 7.006431313696083 - now should give you - - .. code-block:: shell - - Hardware Component 1 - name: DiffBot - type: system - plugin name: mock_components/GenericSystem - state: id=3 label=active - command interfaces - left_wheel_joint/velocity [available] [claimed] - right_wheel_joint/velocity [available] [claimed] - - You see that a different plugin was loaded. Having a look into the `diffbot.ros2_control.xacro `__, one can find the - instructions to load this plugin together with the parameter ``calculate_dynamics``. - - .. code-block:: xml - - - mock_components/GenericSystem - true - - - This enables the integration of the velocity commands to the position state interface, which can be - checked by means of ``ros2 topic echo /joint_states``: The position values are increasing over time if the robot is moving. - You now can test the setup with the commands from above, it should work identically as the custom hardware component plugin. - - More information on mock_components can be found in the :ref:`ros2_control documentation `. Files used for this demos -------------------------- -* Launch file: `diffbot.launch.py `__ -* Controllers yaml: `diffbot_controllers.yaml `__ -* URDF file: `diffbot.urdf.xacro `__ +* Launch file: `diffbot.launch.py `__ +* Controllers yaml: `diffbot_chained_controllers.yaml `__ +* URDF file: `diffbot.urdf.xacro `__ * Description: `diffbot_description.urdf.xacro `__ - * ``ros2_control`` tag: `diffbot.ros2_control.xacro `__ + * ``ros2_control`` tag: `diffbot.ros2_control.xacro `__ * RViz configuration: `diffbot.rviz `__ -* Hardware interface plugin: `diffbot_system.cpp `__ +* Hardware interface plugin: `diffbot_system.cpp `__ Controllers from this demo @@ -299,7 +192,3 @@ Controllers from this demo * ``Joint State Broadcaster`` (`ros2_controllers repository `__): :ref:`doc ` * ``Diff Drive Controller`` (`ros2_controllers repository `__): :ref:`doc ` * ``pid_controller`` (`ros2_controllers repository `__): :ref:`doc ` - -References --------------------------- -https://github.com/ros-controls/roscon_advanced_workshop/blob/9-chaining-controllers/solution/controlko_bringup/config/rrbot_chained_controllers.yaml diff --git a/example_16/hardware/diffbot_system.cpp b/example_16/hardware/diffbot_system.cpp index 95867f923..a5acf0ffd 100644 --- a/example_16/hardware/diffbot_system.cpp +++ b/example_16/hardware/diffbot_system.cpp @@ -201,8 +201,8 @@ hardware_interface::return_type ros2_control_demo_example_16 ::DiffBotSystemHard ss << "Writing commands:"; for (const auto & [name, descr] : joint_command_interfaces_) { - // Simulate sending commands to the hardware - set_state(name, get_command(name)); + // Simulate sending commands to the hardware with a slow down factor + set_state(name, get_command(name) * 0.8); ss << std::fixed << std::setprecision(2) << std::endl << "\t" << "command " << get_command(name) << " for '" << name << "'!"; diff --git a/example_16/test/test_diffbot_launch.py b/example_16/test/test_diffbot_launch.py index be1e0a4b8..4f17b5d06 100644 --- a/example_16/test/test_diffbot_launch.py +++ b/example_16/test/test_diffbot_launch.py @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -# -# Author: Julia Jia import os import pytest @@ -72,8 +70,12 @@ def test_node_start(self, proc_output): def test_controller_running(self, proc_output): # disable this test for now, as the activation of the diffbot_base_controller fails - # cnames = ["diffbot_base_controller", "joint_state_broadcaster"] - cnames = ["joint_state_broadcaster"] + cnames = [ + "pid_controller_left_wheel_joint", + "pid_controller_right_wheel_joint", + "diffbot_base_controller", + "joint_state_broadcaster", + ] check_controllers_running(self.node, cnames) From 64a2b6ce91c07daca61b80c01c059a71a152ab02 Mon Sep 17 00:00:00 2001 From: Julia Jia Date: Wed, 5 Feb 2025 11:17:49 -0800 Subject: [PATCH 08/20] Update per review feedback. --- .gitignore | 2 -- example_16/bringup/launch/diffbot.launch.py | 5 ----- 2 files changed, 7 deletions(-) diff --git a/.gitignore b/.gitignore index bb56e221f..02e922e35 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,2 @@ .ccache .work -/.vscode -*.pyc diff --git a/example_16/bringup/launch/diffbot.launch.py b/example_16/bringup/launch/diffbot.launch.py index f9fb705da..8552eb55a 100644 --- a/example_16/bringup/launch/diffbot.launch.py +++ b/example_16/bringup/launch/diffbot.launch.py @@ -123,7 +123,6 @@ def generate_launch_description(): ], ) - # start the base controller in inactive mode robot_base_controller_spawner = Node( package="controller_manager", executable="spawner", @@ -133,10 +132,6 @@ def generate_launch_description(): robot_controllers, "--controller-ros-args", "-r /diffbot_base_controller/cmd_vel:=/cmd_vel", - # "--inactive", - "--ros-args", - "--log-level", - "debug", ], ) From bb45f91551b7d3e88620898180480f3cea4c688a Mon Sep 17 00:00:00 2001 From: Julia Jia Date: Wed, 5 Feb 2025 14:38:21 -0800 Subject: [PATCH 09/20] Update userdoc.rst --- example_16/doc/userdoc.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/example_16/doc/userdoc.rst b/example_16/doc/userdoc.rst index c333d58da..c45aeeca4 100644 --- a/example_16/doc/userdoc.rst +++ b/example_16/doc/userdoc.rst @@ -36,7 +36,7 @@ Tutorial steps .. code-block:: shell - $ ros2 control list_controllers + ros2 control list_controllers You should get @@ -51,7 +51,7 @@ Tutorial steps .. code-block:: shell - $ ros2 control list_hardware_interfaces + ros2 control list_hardware_interfaces You should get @@ -81,8 +81,8 @@ Tutorial steps .. code-block:: shell - $ ros2 service call /pid_controller_left_wheel_joint/set_feedforward_control std_srvs/srv/SetBool "data: true" - $ ros2 service call /pid_controller_right_wheel_joint/set_feedforward_control std_srvs/srv/SetBool "data: true" + ros2 service call /pid_controller_left_wheel_joint/set_feedforward_control std_srvs/srv/SetBool "data: true" \ + ros2 service call /pid_controller_right_wheel_joint/set_feedforward_control std_srvs/srv/SetBool "data: true" You should get @@ -95,13 +95,13 @@ Tutorial steps .. code-block:: shell - $ ros2 topic echo /pid_controller_left_wheel_joint/controller_state + ros2 topic echo /pid_controller_left_wheel_joint/controller_state 6. Now we are ready to send a command to move the robot. Send a command to *Diff Drive Controller* by opening another terminal and executing .. code-block:: shell - $ ros2 topic pub --rate 10 /cmd_vel geometry_msgs/msg/TwistStamped " + ros2 topic pub --rate 10 /cmd_vel geometry_msgs/msg/TwistStamped " twist: linear: x: 0.7 From 191438fc3d51b73cd6e0ae8f49b5b25a92948636 Mon Sep 17 00:00:00 2001 From: Julia Jia Date: Wed, 5 Feb 2025 14:41:55 -0800 Subject: [PATCH 10/20] Update userdoc.rst --- example_16/doc/userdoc.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example_16/doc/userdoc.rst b/example_16/doc/userdoc.rst index c45aeeca4..2a1e89a7e 100644 --- a/example_16/doc/userdoc.rst +++ b/example_16/doc/userdoc.rst @@ -81,7 +81,7 @@ Tutorial steps .. code-block:: shell - ros2 service call /pid_controller_left_wheel_joint/set_feedforward_control std_srvs/srv/SetBool "data: true" \ + ros2 service call /pid_controller_left_wheel_joint/set_feedforward_control std_srvs/srv/SetBool "data: true" && \ ros2 service call /pid_controller_right_wheel_joint/set_feedforward_control std_srvs/srv/SetBool "data: true" You should get From 0db71194170e794180cee5446f8a8ef4c3622cb9 Mon Sep 17 00:00:00 2001 From: Julia Jia Date: Mon, 10 Feb 2025 15:33:30 -0800 Subject: [PATCH 11/20] Update per review feedback. --- doc/index.rst | 2 ++ .../bringup/config/diffbot_chained_controllers.yaml | 10 ---------- example_16/doc/userdoc.rst | 6 ++---- example_16/hardware/diffbot_system.cpp | 2 +- example_16/test/test_diffbot_launch.py | 1 - 5 files changed, 5 insertions(+), 16 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index 1a08611ed..ba7bc8111 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -85,6 +85,8 @@ Example 14: "Modular robots with actuators not providing states and with additio Example 15: "Using multiple controller managers" This example shows how to integrate multiple robots under different controller manager instances. +Example 16: "DiffBot with chained controllers" + This example shows how to create chained controllers using diff_drive_controller and pid_controllers to control a differential drive robot. .. _ros2_control_demos_install: diff --git a/example_16/bringup/config/diffbot_chained_controllers.yaml b/example_16/bringup/config/diffbot_chained_controllers.yaml index c73f4a9e0..38dd8d9e1 100644 --- a/example_16/bringup/config/diffbot_chained_controllers.yaml +++ b/example_16/bringup/config/diffbot_chained_controllers.yaml @@ -14,9 +14,6 @@ controller_manager: diffbot_base_controller: type: diff_drive_controller/DiffDriveController - forward_velocity_controller_for_debug: - type: forward_command_controller/ForwardCommandController - pid_controller_left_wheel_joint: ros__parameters: @@ -92,10 +89,3 @@ diffbot_base_controller: angular.z.min_acceleration: -1.0 angular.z.max_jerk: .NAN angular.z.min_jerk: .NAN - -forward_velocity_controller_for_debug: - ros__parameters: - joints: - - pid_controller_left_wheel_joint/left_wheel_joint - - pid_controller_right_wheel_joint/right_wheel_joint - interface_name: velocity diff --git a/example_16/doc/userdoc.rst b/example_16/doc/userdoc.rst index 2a1e89a7e..434e500ac 100644 --- a/example_16/doc/userdoc.rst +++ b/example_16/doc/userdoc.rst @@ -6,11 +6,9 @@ DiffBot with Chained Controllers ******************************** -*DiffBot*, or ''Differential Mobile Robot'', is a simple mobile base with differential drive. The robot is basically a box moving according to differential drive kinematics. +This example shows how to create chained controllers using diff_drive_controller and pid_controllers to control a differential drive robot. It extends *example_2*. If you haven't already, you can find the instructions for *example_2* in :ref:`ros2_control_demos_example_2_userdoc`. It is recommended to follow the steps given in that tutorial first before proceeding with this one. -This example extends *example_2* by demonstrating controller chaining with a diff_drive_controller and two pid_controllers (one for each wheel) to achieve coordinated robot motion. If you haven't already, you can find the instructions for *example_2* in :ref:`ros2_control_demos_example_2_userdoc`. It is recommended to follow the steps given in that tutorial first before proceeding with this one. - -This example demonstrates controller chaining as described in the `ROS2 controller manager chaining documentation `__. The control chain flows from the diff_drive_controller through two PID controllers to the DiffBot hardware. The diff_drive_controller converts desired robot twist into wheel velocity commands, which are then processed by the PID controllers to directly control the wheel velocities. Additionally, this example shows how to enable the feedforward mode for the PID controllers. +This example demonstrates controller chaining as described in :ref:`controller_chaining`. The control chain flows from the diff_drive_controller through two PID controllers to the DiffBot hardware. The diff_drive_controller converts desired robot twist into wheel velocity commands, which are then processed by the PID controllers to directly control the wheel velocities. Additionally, this example shows how to enable the feedforward mode for the PID controllers. The *DiffBot* URDF files can be found in ``description/urdf`` folder. diff --git a/example_16/hardware/diffbot_system.cpp b/example_16/hardware/diffbot_system.cpp index a5acf0ffd..6415a7d5a 100644 --- a/example_16/hardware/diffbot_system.cpp +++ b/example_16/hardware/diffbot_system.cpp @@ -176,7 +176,6 @@ hardware_interface::return_type DiffBotSystemHardware::read( { if (descr.get_interface_name() == hardware_interface::HW_IF_POSITION) { - // Simulate DiffBot wheels's movement as a first-order system // Update the joint status: this is a revolute joint without any limit. // Simply integrates auto velo = get_command(descr.get_prefix_name() + "/" + hardware_interface::HW_IF_VELOCITY); @@ -202,6 +201,7 @@ hardware_interface::return_type ros2_control_demo_example_16 ::DiffBotSystemHard for (const auto & [name, descr] : joint_command_interfaces_) { // Simulate sending commands to the hardware with a slow down factor + // to show-case the PID action set_state(name, get_command(name) * 0.8); ss << std::fixed << std::setprecision(2) << std::endl diff --git a/example_16/test/test_diffbot_launch.py b/example_16/test/test_diffbot_launch.py index 4f17b5d06..15ce71781 100644 --- a/example_16/test/test_diffbot_launch.py +++ b/example_16/test/test_diffbot_launch.py @@ -69,7 +69,6 @@ def test_node_start(self, proc_output): check_node_running(self.node, "robot_state_publisher") def test_controller_running(self, proc_output): - # disable this test for now, as the activation of the diffbot_base_controller fails cnames = [ "pid_controller_left_wheel_joint", "pid_controller_right_wheel_joint", From f22a46fc3f0462d247dc5cc9651e855ea842cd9f Mon Sep 17 00:00:00 2001 From: Christoph Froehlich Date: Sun, 16 Feb 2025 19:25:15 +0000 Subject: [PATCH 12/20] Add example_16 to TOC --- doc/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/index.rst b/doc/index.rst index ba7bc8111..79fa88a95 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -282,3 +282,4 @@ Examples Example 13: Multiple robots <../example_13/doc/userdoc.rst> Example 14: Modular robots with actuators not providing states <../example_14/doc/userdoc.rst> Example 15: Using multiple controller managers <../example_15/doc/userdoc.rst> + Example 16: DiffBot with chained controllers <../example_16/doc/userdoc.rst> From a199348b6857090ea6a81397c2d5f88a1904e71e Mon Sep 17 00:00:00 2001 From: Julia Jia Date: Mon, 17 Feb 2025 23:46:15 -0800 Subject: [PATCH 13/20] Update per review and add python script for visualization experience --- .../config/diffbot_chained_controllers.yaml | 7 +- example_16/bringup/config/plotjuggler.xml | 84 ++++++++++++ example_16/bringup/launch/demo_test.launch.py | 28 ++++ example_16/bringup/launch/demo_test_helper.py | 72 ++++++++++ .../bringup/launch/plotjuggler.launch.py | 41 ++++++ example_16/doc/diffbot.png | Bin 19716 -> 0 bytes example_16/doc/userdoc.rst | 123 +++++++++++++----- example_16/test/test_diffbot_launch.py | 15 +-- 8 files changed, 323 insertions(+), 47 deletions(-) create mode 100644 example_16/bringup/config/plotjuggler.xml create mode 100644 example_16/bringup/launch/demo_test.launch.py create mode 100644 example_16/bringup/launch/demo_test_helper.py create mode 100644 example_16/bringup/launch/plotjuggler.launch.py delete mode 100644 example_16/doc/diffbot.png diff --git a/example_16/bringup/config/diffbot_chained_controllers.yaml b/example_16/bringup/config/diffbot_chained_controllers.yaml index 38dd8d9e1..991f517d0 100644 --- a/example_16/bringup/config/diffbot_chained_controllers.yaml +++ b/example_16/bringup/config/diffbot_chained_controllers.yaml @@ -28,7 +28,7 @@ pid_controller_left_wheel_joint: gains: # control the velocity, no d term - left_wheel_joint: {"p": 0.15, "i": 0.05, "d": 0.0, "i_clamp_min": -2.0, "i_clamp_max": 2.0, "antiwindup": true, "feedforward_gain": 0.95} + left_wheel_joint: {"p": 0.5, "i": 2.5, "d": 0.0, "i_clamp_min": -20.0, "i_clamp_max": 20.0, "antiwindup": true, "feedforward_gain": 0.95} pid_controller_right_wheel_joint: ros__parameters: @@ -43,7 +43,7 @@ pid_controller_right_wheel_joint: gains: # control the velocity, no d term - right_wheel_joint: {"p": 0.15, "i": 0.05, "d": 0.0, "i_clamp_min": -2.0, "i_clamp_max": 2.0, "antiwindup": true, "feedforward_gain": 0.95} + right_wheel_joint: {"p": 0.5, "i": 2.5, "d": 0.0, "i_clamp_min": -20.0, "i_clamp_max": 20.0, "antiwindup": true, "feedforward_gain": 0.95} diffbot_base_controller: ros__parameters: @@ -72,7 +72,8 @@ diffbot_base_controller: enable_odom_tf: true cmd_vel_timeout: 0.5 - #publish_limited_velocity: true + # set publish_limited_velocity to true for visualization + publish_limited_velocity: true #velocity_rolling_window_size: 10 # Velocity and acceleration limits diff --git a/example_16/bringup/config/plotjuggler.xml b/example_16/bringup/config/plotjuggler.xml new file mode 100644 index 000000000..0012e8c11 --- /dev/null +++ b/example_16/bringup/config/plotjuggler.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example_16/bringup/launch/demo_test.launch.py b/example_16/bringup/launch/demo_test.launch.py new file mode 100644 index 000000000..64343591f --- /dev/null +++ b/example_16/bringup/launch/demo_test.launch.py @@ -0,0 +1,28 @@ +# Copyright 2025 ros2_control Development Team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from launch import LaunchDescription +from launch.substitutions import PathJoinSubstitution +from launch_ros.substitutions import FindPackageShare +from launch.actions import ExecuteProcess + +def generate_launch_description(): + + return LaunchDescription([ + ExecuteProcess( + cmd=['python3', PathJoinSubstitution([FindPackageShare("ros2_control_demo_example_16"), "launch", "demo_test_helper.py"])], + output='screen' + ) + ]) diff --git a/example_16/bringup/launch/demo_test_helper.py b/example_16/bringup/launch/demo_test_helper.py new file mode 100644 index 000000000..bd606828d --- /dev/null +++ b/example_16/bringup/launch/demo_test_helper.py @@ -0,0 +1,72 @@ +# Copyright 2025 ros2_control Development Team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +import rclpy +from rclpy.node import Node +from geometry_msgs.msg import TwistStamped +from std_srvs.srv import SetBool + + +class DiffbotChainedControllersTest(Node): + def __init__(self): + super().__init__('diffbot_chained_controllers_demo_helper_node') + # Enable feedforward control via service call + self.client_left_ = self.create_client(SetBool, '/pid_controller_left_wheel_joint/set_feedforward_control') + self.client_right_ = self.create_client(SetBool, '/pid_controller_right_wheel_joint/set_feedforward_control') + self.publisher_ = self.create_publisher(TwistStamped, '/cmd_vel', 10) + + def set_feedforward_control(self): + while not self.client_left_.wait_for_service(timeout_sec=1.0): + self.get_logger().info('Waiting for left feedforward control service to be available...') + while not self.client_right_.wait_for_service(timeout_sec=1.0): + self.get_logger().info('Waiting for right feedforward control service to be available...') + + request_left = SetBool.Request() + request_left.data = True + future_left = self.client_left_.call_async(request_left) + + request_right = SetBool.Request() + request_right.data = True + future_right = self.client_right_ .call_async(request_right) + + rclpy.spin_until_future_complete(self, future_left) + rclpy.spin_until_future_complete(self, future_right) + + self.get_logger().info('Enabled feedforward control for both wheels.') + + def publish_cmd_vel(self, delay=0.1): + + twist_msg = TwistStamped() + twist_msg.twist.linear.x = 0.7 + twist_msg.twist.linear.y = 0.0 + twist_msg.twist.linear.z = 0.0 + twist_msg.twist.angular.x = 0.0 + twist_msg.twist.angular.y = 0.0 + twist_msg.twist.angular.z = 1.0 + + while rclpy.ok(): + self.get_logger().info(f"Publishing twist message to cmd_vel: {twist_msg}") + self.publisher_.publish(twist_msg) + time.sleep(delay) + + +if __name__ == '__main__': + rclpy.init() + test_node = DiffbotChainedControllersTest() + test_node.set_feedforward_control() + test_node.publish_cmd_vel(delay=0.1) + rclpy.spin(test_node) + test_node.destroy_node() + rclpy.shutdown() diff --git a/example_16/bringup/launch/plotjuggler.launch.py b/example_16/bringup/launch/plotjuggler.launch.py new file mode 100644 index 000000000..1b4741b2a --- /dev/null +++ b/example_16/bringup/launch/plotjuggler.launch.py @@ -0,0 +1,41 @@ +# Copyright 2025 ros2_control Development Team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from launch import LaunchDescription +from launch.substitutions import PathJoinSubstitution +from launch_ros.actions import Node +from launch_ros.substitutions import FindPackageShare + +def generate_launch_description(): + + # Find plotjuggler layout file + plotjuggler_layout = PathJoinSubstitution( + [ + FindPackageShare("ros2_control_demo_example_16"), + "config", + "plotjuggler.xml", + ] + ) + + return LaunchDescription([ + Node( + package="plotjuggler", + executable="plotjuggler", + name="plotjuggler", + output="screen", + arguments=["--layout", plotjuggler_layout], + ) + ]) + diff --git a/example_16/doc/diffbot.png b/example_16/doc/diffbot.png deleted file mode 100644 index 1c4fc2476dba992c2984f32d8225fadf4cb9eba2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19716 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 diff --git a/example_16/doc/userdoc.rst b/example_16/doc/userdoc.rst index 434e500ac..ac882713b 100644 --- a/example_16/doc/userdoc.rst +++ b/example_16/doc/userdoc.rst @@ -10,6 +10,8 @@ This example shows how to create chained controllers using diff_drive_controller This example demonstrates controller chaining as described in :ref:`controller_chaining`. The control chain flows from the diff_drive_controller through two PID controllers to the DiffBot hardware. The diff_drive_controller converts desired robot twist into wheel velocity commands, which are then processed by the PID controllers to directly control the wheel velocities. Additionally, this example shows how to enable the feedforward mode for the PID controllers. +Furthermore, this example shows how to use plotjuggler to visualize the controller states. + The *DiffBot* URDF files can be found in ``description/urdf`` folder. .. include:: ../../doc/run_from_docker.rst @@ -24,11 +26,11 @@ Tutorial steps ros2 launch ros2_control_demo_example_16 diffbot.launch.py - The launch file loads and starts the robot hardware, controllers and opens *RViz*. - In the starting terminal you will see a lot of output from the hardware implementation showing its internal states. - This excessive printing is only added for demonstration. In general, printing to the terminal should be avoided as much as possible in a hardware interface implementation. + The launch file loads and starts the robot hardware, controllers and opens *RViz*. + In the starting terminal you will see a lot of output from the hardware implementation showing its internal states. + This excessive printing is only added for demonstration. In general, printing to the terminal should be avoided as much as possible in a hardware interface implementation. - If you can see an orange box in *RViz* everything has started properly. Let's introspect the control system before moving *DiffBot*. + If you can see an orange box in *RViz* everything has started properly. Let's introspect the control system before moving *DiffBot*. 2. Check controllers @@ -36,7 +38,7 @@ Tutorial steps ros2 control list_controllers - You should get + You should get .. code-block:: shell @@ -112,25 +114,24 @@ Tutorial steps You should now see robot is moving in circles in *RViz*. -7. In the terminal where launch file is started, you should see the commands being sent to the wheels and how they are gradually stabilizing to the target velocity. +7. In the terminal where launch file is started, you should see the commands being sent to the wheels and how they are gradually stabilizing to the target velocity similar to following output. .. code-block:: shell - [ros2_control_node-1] [INFO] [1738648404.508385200] [controller_manager.resource_manager.hardware_component.system.DiffBot]: Writing commands: - [ros2_control_node-1] command 0.00 for 'right_wheel_joint/velocity'! - [ros2_control_node-1] command 0.00 for 'left_wheel_joint/velocity'! - - [ros2_control_node-1] [INFO] [1738648405.008399450] [controller_manager.resource_manager.hardware_component.system.DiffBot]: Writing commands: - [ros2_control_node-1] command 14.55 for 'right_wheel_joint/velocity'! - [ros2_control_node-1] command 13.17 for 'left_wheel_joint/velocity'! + [ros2_control_node-1] [WARN] [1739860498.298085087] [diffbot_base_controller]: Received TwistStamped with zero timestamp, setting it to current time, this message will only be shown once + [ros2_control_node-1] [INFO] [1739860498.634431841] [controller_manager.resource_manager.hardware_component.system.DiffBot]: Reading states: + [ros2_control_node-1] position 5.78 and velocity 27.52 for 'right_wheel_joint/position'! + [ros2_control_node-1] position 5.23 and velocity 24.90 for 'left_wheel_joint/position'! + [ros2_control_node-1] [INFO] [1739860498.634800954] [controller_manager.resource_manager.hardware_component.system.DiffBot]: Writing commands: + [ros2_control_node-1] command 35.55 for 'right_wheel_joint/velocity'! + [ros2_control_node-1] command 32.16 for 'left_wheel_joint/velocity'! + [ros2_control_node-1] [INFO] [1739860499.234393780] [controller_manager.resource_manager.hardware_component.system.DiffBot]: Reading states: + [ros2_control_node-1] position 36.99 and velocity 60.61 for 'right_wheel_joint/position'! + [ros2_control_node-1] position 33.19 and velocity 53.17 for 'left_wheel_joint/position'! + [ros2_control_node-1] [INFO] [1739860499.234812092] [controller_manager.resource_manager.hardware_component.system.DiffBot]: Writing commands: + [ros2_control_node-1] command 60.53 for 'right_wheel_joint/velocity'! + [ros2_control_node-1] command 52.27 for 'left_wheel_joint/velocity'! - [ros2_control_node-1] [INFO] [1738648405.508445448] [controller_manager.resource_manager.hardware_component.system.DiffBot]: Writing commands: - [ros2_control_node-1] command 49.21 for 'right_wheel_joint/velocity'! - [ros2_control_node-1] command 44.52 for 'left_wheel_joint/velocity'! - - [ros2_control_node-1] [INFO] [1738648406.108246536] [controller_manager.resource_manager.hardware_component.system.DiffBot]: Writing commands: - [ros2_control_node-1] command 49.73 for 'right_wheel_joint/velocity'! - [ros2_control_node-1] command 43.11 for 'left_wheel_joint/velocity'! 8. Let's go back to the terminal where we subscribed to the controller_state topic and see the changing states. @@ -139,37 +140,87 @@ Tutorial steps --- header: stamp: - sec: 1738639255 - nanosec: 743875549 + sec: 1739860821 + nanosec: 314185285 frame_id: '' dof_states: - name: left_wheel_joint - reference: 0.0 - feedback: 0.0 + reference: 18.967273133333336 + feedback: 13.294059091678035 feedback_dot: 0.0 - error: 0.0 + error: 5.673214041655301 error_dot: 0.0 - time_step: 0.09971356 - output: 0.0 - + time_step: 0.100077098 + output: 24.857442270936623 --- header: stamp: - sec: 1738639255 - nanosec: 844169802 + sec: 1739860821 + nanosec: 414126639 frame_id: '' dof_states: - name: left_wheel_joint - reference: 6.3405774 - feedback: 0.0 + reference: 25.296198783333335 + feedback: 19.8859538167493 feedback_dot: 0.0 - error: 6.3405774 + error: 5.410244966584035 error_dot: 0.0 - time_step: 0.100294253 - output: 7.006431313696083 + time_step: 0.099941354 + output: 32.090205119481226 + + +Visualize the convergence of DiffBot's wheel velocities and commands +--------------------------------------------------------------------- + +In the section below, we will use *plotjuggler* to observe the convergence of DiffBot's wheel velocities and commands from PID controllers. + +*plotjuggler* is an open-source data visualization tool and widely embraced by ROS2 community. If you don't have it installed, you can find the instructions from `plotjuggler website `__. + + +Before we proceed, we stop all previous steps from terminal and start from the beginning. + +1. To start *DiffBot* example open a terminal, source your ROS2-workspace and execute its launch file with + + .. code-block:: shell + + ros2 launch ros2_control_demo_example_16 diffbot.launch.py + +Like before, if you can see an orange box in *RViz*, everything has started properly. + +2. To start the plotjuggler with a provided layout file, open another terminal and execute + + .. code-block:: shell + + ros2 launch ros2_control_demo_example_16 plotjuggler.launch.py + +After this, you will see a few dialogs popping up. For example: + + .. code-block:: shell + + Start the previously used streaming plugin? + + ROS2 Topic Subscriber + +Click 'Yes' for the first dialog and 'OK" to the follow two dialogs, then you will see the plotjuggler window. + +3. To enable feedforward mode and published a command to move the robot, instead of doing these manually, we will use the demo_test.launch.py. Open another terminal and execute + + .. code-block:: shell + + ros2 launch ros2_control_demo_example_16 demo_test.launch.py + +4. From the plotjuggler, you can see the controllers' states and commands being plotted, similar to following figure. From the figure, the DiffBot's wheel velocities and commands from PID controllers are converged to the target velocity fairly quickly. + + .. image:: /images/example_16/plotjuggler_controller_states.png + :align: center + :alt: Visualization via Plotjuggler + + TODO: add png here. + +5. Change the ``gains`` in the ``diffbot_chained_controllers.yaml`` file with different p, i, d values, repeat above steps to see the effect of feedforward mode. -Files used for this demos +Files used for this demo -------------------------- * Launch file: `diffbot.launch.py `__ diff --git a/example_16/test/test_diffbot_launch.py b/example_16/test/test_diffbot_launch.py index 15ce71781..b2fc0c2bb 100644 --- a/example_16/test/test_diffbot_launch.py +++ b/example_16/test/test_diffbot_launch.py @@ -23,7 +23,7 @@ from launch.launch_description_sources import PythonLaunchDescriptionSource from launch_testing.actions import ReadyToTest -# import launch_testing.markers +import launch_testing.markers import rclpy from controller_manager.test_utils import ( check_controllers_running, @@ -82,11 +82,10 @@ def test_check_if_msgs_published(self): check_if_js_published("/joint_states", ["left_wheel_joint", "right_wheel_joint"]) -# TODO(anyone): enable this if shutdown of ros2_control_node does not fail anymore -# @launch_testing.post_shutdown_test() -# # These tests are run after the processes in generate_test_description() have shutdown. -# class TestDescriptionCraneShutdown(unittest.TestCase): +@launch_testing.post_shutdown_test() +# These tests are run after the processes in generate_test_description() have shutdown. +class TestDescriptionCraneShutdown(unittest.TestCase): -# def test_exit_codes(self, proc_info): -# """Check if the processes exited normally.""" -# launch_testing.asserts.assertExitCodes(proc_info) + def test_exit_codes(self, proc_info): + """Check if the processes exited normally.""" + launch_testing.asserts.assertExitCodes(proc_info) From 8bc64213cc0b00ffe025d8ea06324f5bbe92bafb Mon Sep 17 00:00:00 2001 From: Julia Jia Date: Tue, 18 Feb 2025 19:26:48 -0800 Subject: [PATCH 14/20] Add plotjuggler diagram and fix pre-commit issues --- example_16/bringup/config/plotjuggler.xml | 2 +- example_16/bringup/launch/demo_test.launch.py | 24 +++++++--- example_16/bringup/launch/demo_test_helper.py | 36 +++++++++------ .../bringup/launch/plotjuggler.launch.py | 41 ------------------ example_16/doc/diffbot_velocities.png | Bin 0 -> 179617 bytes example_16/doc/userdoc.rst | 15 ++++--- 6 files changed, 51 insertions(+), 67 deletions(-) delete mode 100644 example_16/bringup/launch/plotjuggler.launch.py create mode 100644 example_16/doc/diffbot_velocities.png diff --git a/example_16/bringup/config/plotjuggler.xml b/example_16/bringup/config/plotjuggler.xml index 0012e8c11..0836de567 100644 --- a/example_16/bringup/config/plotjuggler.xml +++ b/example_16/bringup/config/plotjuggler.xml @@ -68,7 +68,7 @@ - + diff --git a/example_16/bringup/launch/demo_test.launch.py b/example_16/bringup/launch/demo_test.launch.py index 64343591f..21d826c23 100644 --- a/example_16/bringup/launch/demo_test.launch.py +++ b/example_16/bringup/launch/demo_test.launch.py @@ -18,11 +18,23 @@ from launch_ros.substitutions import FindPackageShare from launch.actions import ExecuteProcess + def generate_launch_description(): - return LaunchDescription([ - ExecuteProcess( - cmd=['python3', PathJoinSubstitution([FindPackageShare("ros2_control_demo_example_16"), "launch", "demo_test_helper.py"])], - output='screen' - ) - ]) + return LaunchDescription( + [ + ExecuteProcess( + cmd=[ + "python3", + PathJoinSubstitution( + [ + FindPackageShare("ros2_control_demo_example_16"), + "launch", + "demo_test_helper.py", + ] + ), + ], + output="screen", + ) + ] + ) diff --git a/example_16/bringup/launch/demo_test_helper.py b/example_16/bringup/launch/demo_test_helper.py index bd606828d..7bfc10b1d 100644 --- a/example_16/bringup/launch/demo_test_helper.py +++ b/example_16/bringup/launch/demo_test_helper.py @@ -21,17 +21,25 @@ class DiffbotChainedControllersTest(Node): def __init__(self): - super().__init__('diffbot_chained_controllers_demo_helper_node') + super().__init__("diffbot_chained_controllers_demo_helper_node") # Enable feedforward control via service call - self.client_left_ = self.create_client(SetBool, '/pid_controller_left_wheel_joint/set_feedforward_control') - self.client_right_ = self.create_client(SetBool, '/pid_controller_right_wheel_joint/set_feedforward_control') - self.publisher_ = self.create_publisher(TwistStamped, '/cmd_vel', 10) - + self.client_left_ = self.create_client( + SetBool, "/pid_controller_left_wheel_joint/set_feedforward_control" + ) + self.client_right_ = self.create_client( + SetBool, "/pid_controller_right_wheel_joint/set_feedforward_control" + ) + self.publisher_ = self.create_publisher(TwistStamped, "/cmd_vel", 10) + def set_feedforward_control(self): - while not self.client_left_.wait_for_service(timeout_sec=1.0): - self.get_logger().info('Waiting for left feedforward control service to be available...') + while not self.client_left_.wait_for_service(timeout_sec=1.0): + self.get_logger().info( + "Waiting for left feedforward control service to be available..." + ) while not self.client_right_.wait_for_service(timeout_sec=1.0): - self.get_logger().info('Waiting for right feedforward control service to be available...') + self.get_logger().info( + "Waiting for right feedforward control service to be available..." + ) request_left = SetBool.Request() request_left.data = True @@ -39,15 +47,15 @@ def set_feedforward_control(self): request_right = SetBool.Request() request_right.data = True - future_right = self.client_right_ .call_async(request_right) + future_right = self.client_right_.call_async(request_right) rclpy.spin_until_future_complete(self, future_left) rclpy.spin_until_future_complete(self, future_right) - self.get_logger().info('Enabled feedforward control for both wheels.') + self.get_logger().info("Enabled feedforward control for both wheels.") def publish_cmd_vel(self, delay=0.1): - + twist_msg = TwistStamped() twist_msg.twist.linear.x = 0.7 twist_msg.twist.linear.y = 0.0 @@ -62,9 +70,9 @@ def publish_cmd_vel(self, delay=0.1): time.sleep(delay) -if __name__ == '__main__': - rclpy.init() - test_node = DiffbotChainedControllersTest() +if __name__ == "__main__": + rclpy.init() + test_node = DiffbotChainedControllersTest() test_node.set_feedforward_control() test_node.publish_cmd_vel(delay=0.1) rclpy.spin(test_node) diff --git a/example_16/bringup/launch/plotjuggler.launch.py b/example_16/bringup/launch/plotjuggler.launch.py deleted file mode 100644 index 1b4741b2a..000000000 --- a/example_16/bringup/launch/plotjuggler.launch.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2025 ros2_control Development Team -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from launch import LaunchDescription -from launch.substitutions import PathJoinSubstitution -from launch_ros.actions import Node -from launch_ros.substitutions import FindPackageShare - -def generate_launch_description(): - - # Find plotjuggler layout file - plotjuggler_layout = PathJoinSubstitution( - [ - FindPackageShare("ros2_control_demo_example_16"), - "config", - "plotjuggler.xml", - ] - ) - - return LaunchDescription([ - Node( - package="plotjuggler", - executable="plotjuggler", - name="plotjuggler", - output="screen", - arguments=["--layout", plotjuggler_layout], - ) - ]) - diff --git a/example_16/doc/diffbot_velocities.png b/example_16/doc/diffbot_velocities.png new file mode 100644 index 0000000000000000000000000000000000000000..c4991b8e349ea6ec06325b51768f6d0a7c03b59d GIT binary patch literal 179617 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 OdggO~*!laHSN|XInkLu) literal 0 HcmV?d00001 diff --git a/example_16/doc/userdoc.rst b/example_16/doc/userdoc.rst index ac882713b..6a3c26fc4 100644 --- a/example_16/doc/userdoc.rst +++ b/example_16/doc/userdoc.rst @@ -187,11 +187,12 @@ Before we proceed, we stop all previous steps from terminal and start from the b Like before, if you can see an orange box in *RViz*, everything has started properly. -2. To start the plotjuggler with a provided layout file, open another terminal and execute +2. To start the plotjuggler with a provided layout file(plotjuggler.xml), open another terminal and run following command. .. code-block:: shell - ros2 launch ros2_control_demo_example_16 plotjuggler.launch.py + # replace {PARENT_DIR_OF_LAYOUT_FILE} with actual parent directory of the layout file + ros2 run plotjuggler plotjuggler --layout {PARENT_DIR_OF_LAYOUT_FILE}/plotjuggler.xml After this, you will see a few dialogs popping up. For example: @@ -211,9 +212,9 @@ Click 'Yes' for the first dialog and 'OK" to the follow two dialogs, then you wi 4. From the plotjuggler, you can see the controllers' states and commands being plotted, similar to following figure. From the figure, the DiffBot's wheel velocities and commands from PID controllers are converged to the target velocity fairly quickly. - .. image:: /images/example_16/plotjuggler_controller_states.png - :align: center - :alt: Visualization via Plotjuggler + .. image:: diffbot_velocities.png + :width: 400 + :alt: Plotjuggler visualization of Diffbot velocities and commands TODO: add png here. @@ -234,6 +235,10 @@ Files used for this demo * Hardware interface plugin: `diffbot_system.cpp `__ +* Demo helper utility: + + + demo test helper node: `demo_test_helper.py `__ + + demo test launch file: `demo_test.launch.py `__ Controllers from this demo -------------------------- From 0cc375ce786fd17f808730f149dc16b9cacc2019 Mon Sep 17 00:00:00 2001 From: Julia Jia Date: Thu, 20 Feb 2025 02:33:10 -0800 Subject: [PATCH 15/20] Update example_16/doc/userdoc.rst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Christoph Fröhlich --- example_16/doc/userdoc.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/example_16/doc/userdoc.rst b/example_16/doc/userdoc.rst index 6a3c26fc4..42ce88e0e 100644 --- a/example_16/doc/userdoc.rst +++ b/example_16/doc/userdoc.rst @@ -214,9 +214,7 @@ Click 'Yes' for the first dialog and 'OK" to the follow two dialogs, then you wi .. image:: diffbot_velocities.png :width: 400 - :alt: Plotjuggler visualization of Diffbot velocities and commands - - TODO: add png here. + :alt: Plotjuggler visualization of DiffBot velocities and commands 5. Change the ``gains`` in the ``diffbot_chained_controllers.yaml`` file with different p, i, d values, repeat above steps to see the effect of feedforward mode. From 430d70c197c6890542fa2682258bc242f1ee1145 Mon Sep 17 00:00:00 2001 From: Julia Jia Date: Thu, 20 Feb 2025 02:33:45 -0800 Subject: [PATCH 16/20] Update example_16/doc/userdoc.rst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Christoph Fröhlich --- example_16/doc/userdoc.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/example_16/doc/userdoc.rst b/example_16/doc/userdoc.rst index 42ce88e0e..3cdad1c16 100644 --- a/example_16/doc/userdoc.rst +++ b/example_16/doc/userdoc.rst @@ -191,8 +191,7 @@ Like before, if you can see an orange box in *RViz*, everything has started prop .. code-block:: shell - # replace {PARENT_DIR_OF_LAYOUT_FILE} with actual parent directory of the layout file - ros2 run plotjuggler plotjuggler --layout {PARENT_DIR_OF_LAYOUT_FILE}/plotjuggler.xml + ros2 run plotjuggler plotjuggler --layout $(ros2 pkg prefix ros2_control_demo_example_16 --share)/config/plotjuggler.xml After this, you will see a few dialogs popping up. For example: From 288a358f73d79f5d778b1059020f63985c4f7575 Mon Sep 17 00:00:00 2001 From: Julia Jia Date: Thu, 20 Feb 2025 02:34:14 -0800 Subject: [PATCH 17/20] Update example_16/doc/userdoc.rst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Christoph Fröhlich --- example_16/doc/userdoc.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example_16/doc/userdoc.rst b/example_16/doc/userdoc.rst index 3cdad1c16..a92f67e0e 100644 --- a/example_16/doc/userdoc.rst +++ b/example_16/doc/userdoc.rst @@ -201,7 +201,7 @@ After this, you will see a few dialogs popping up. For example: ROS2 Topic Subscriber -Click 'Yes' for the first dialog and 'OK" to the follow two dialogs, then you will see the plotjuggler window. +Click 'Yes' for the first dialog and 'OK" to the following two dialogs, then you will see the plotjuggler window. 3. To enable feedforward mode and published a command to move the robot, instead of doing these manually, we will use the demo_test.launch.py. Open another terminal and execute From 8e74ebc7bf578976d97b55458c9bbfdfea3facaa Mon Sep 17 00:00:00 2001 From: Julia Jia Date: Thu, 20 Feb 2025 02:49:47 -0800 Subject: [PATCH 18/20] Update doc with command to change parameter online --- example_16/doc/userdoc.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/example_16/doc/userdoc.rst b/example_16/doc/userdoc.rst index a92f67e0e..0ef6b3740 100644 --- a/example_16/doc/userdoc.rst +++ b/example_16/doc/userdoc.rst @@ -215,7 +215,11 @@ Click 'Yes' for the first dialog and 'OK" to the following two dialogs, then you :width: 400 :alt: Plotjuggler visualization of DiffBot velocities and commands -5. Change the ``gains`` in the ``diffbot_chained_controllers.yaml`` file with different p, i, d values, repeat above steps to see the effect of feedforward mode. +5. Change the ``gains`` in the ``diffbot_chained_controllers.yaml`` file with some different values, repeat above steps and observe its effect to the pid_controller commands. For example, to change the ``feedforward_gain`` of the right wheel to 0.50, you can use the following command: + + .. code-block:: shell + + ros2 param set /pid_controller_right_wheel_joint gains.right_wheel_joint.feedforward_gain 0.50 Files used for this demo From 3f3f7749cb43ea9f107a792765fe34c63c8066f0 Mon Sep 17 00:00:00 2001 From: Christoph Froehlich Date: Wed, 26 Feb 2025 12:18:51 +0000 Subject: [PATCH 19/20] Fix test_dependencies --- example_16/package.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/example_16/package.xml b/example_16/package.xml index f802f4237..af45e5808 100644 --- a/example_16/package.xml +++ b/example_16/package.xml @@ -35,10 +35,10 @@ xacro ament_cmake_pytest - controller_manager - launch_testing_ros + launch_testing + launch liburdfdom-tools - xacro + rclpy ament_cmake From 7ea984d48caacc986ca9fa0736f1a8e48217af35 Mon Sep 17 00:00:00 2001 From: Christoph Froehlich Date: Wed, 26 Feb 2025 12:19:57 +0000 Subject: [PATCH 20/20] Update meta information --- example_16/package.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/example_16/package.xml b/example_16/package.xml index af45e5808..b4901ad25 100644 --- a/example_16/package.xml +++ b/example_16/package.xml @@ -8,11 +8,12 @@ Dr.-Ing. Denis Štogl Bence Magyar Christoph Froehlich - - Dr.-Ing. Denis Štogl + Sai Kishor Kothakota Apache-2.0 + Julia Jia + ament_cmake backward_ros