From 46ceae11d34d9f848704d9ea3828d95937e6666f Mon Sep 17 00:00:00 2001 From: openhands Date: Mon, 24 Aug 2026 19:19:34 +0000 Subject: [PATCH] Document node parameters and switch to config-file based launches - Rename launch/sas_robot_driver_ros_composer_example.py to launch/composer_example_launch.py and convert it to load all parameters from config/config.yaml (config_file launch argument). - Add launch/composer_launch.py (single composer node) and launch/watchdog_launch.py (watchdog commander node). - Add config/config.yaml with parameter blocks for the three nodes (sas_robot_driver_ros_composer_node, sas_robot_watchdog_commander_node, and the two sas_robot_driver_ros_example instances named robot_1 and robot_2). - Add a 'ROS 2 Nodes & Parameters' section to the README (and update AGENTS.md) documenting every parameter of each node. - Install the config directory into the package share directory. Co-authored-by: openhands --- AGENTS.md | 4 +- CMakeLists.txt | 1 + README.md | 76 +++++++++++++++++-- config/config.yaml | 39 ++++++++++ launch/composer_example_launch.py | 41 ++++++++++ launch/composer_launch.py | 34 +++++++++ .../sas_robot_driver_ros_composer_example.py | 43 ----------- launch/watchdog_launch.py | 28 +++++++ 8 files changed, 213 insertions(+), 53 deletions(-) create mode 100644 config/config.yaml create mode 100644 launch/composer_example_launch.py create mode 100644 launch/composer_launch.py delete mode 100644 launch/sas_robot_driver_ros_composer_example.py create mode 100644 launch/watchdog_launch.py diff --git a/AGENTS.md b/AGENTS.md index fd2f223..b306a7e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,7 +42,7 @@ Quick check (what CI runs): docker compose build -f docker/compose.yml docker compose up -f docker/compose.yml # or the one-shot version from the README: -docker run --rm murilomarinho/sas:jazzy /bin/bash -c "ros2 launch sas_robot_driver sas_robot_driver_ros_composer_example.py" +docker run --rm murilomarinho/sas:jazzy /bin/bash -c "ros2 launch sas_robot_driver composer_example_launch.py" ``` Building outside docker (requires a ROS 2 jazzy workspace with the sibling @@ -58,7 +58,7 @@ source install/setup.bash Examples: ```bash -ros2 launch sas_robot_driver sas_robot_driver_ros_composer_example.py +ros2 launch sas_robot_driver composer_example_launch.py ros2 run sas_robot_driver sas_robot_driver_interface_example.py ``` diff --git a/CMakeLists.txt b/CMakeLists.txt index d4e6922..a439241 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -137,6 +137,7 @@ install(TARGETS # According to https://github.com/SmartArmStack/sas_robot_driver/blob/ros2/CMakeLists.txt install(DIRECTORY launch + config DESTINATION share/${PROJECT_NAME}/ ) # ^^^^^^^^^^^^^^^^^^ # diff --git a/README.md b/README.md index 7398823..b33aaa7 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ## Quick check ```bash -docker run --rm murilomarinho/sas:jazzy /bin/bash -c "ros2 launch sas_robot_driver sas_robot_driver_ros_composer_example.py" +docker run --rm murilomarinho/sas:jazzy /bin/bash -c "ros2 launch sas_robot_driver composer_example_launch.py" ``` ## Contents @@ -16,6 +16,7 @@ docker run --rm murilomarinho/sas:jazzy /bin/bash -c "ros2 launch sas_robot_driv - `src/` — library and node implementations. - `scripts/` — Python example scripts. - `launch/` — example launch files. +- `config/` — example parameter configuration file. ## Client–Server pair @@ -41,18 +42,77 @@ from sas_robot_driver import ( ) ``` -## Nodes +## ROS 2 Nodes & Parameters -- `sas_robot_driver_ros_composer_node` — Composes multiple RobotDriver clients. -- `sas_robot_watchdog_commander_node` — Sends periodic watchdog triggers to clients. -- `sas_robot_driver_ros_example` — Minimal example robot driver node. +Each node loads its parameters from a YAML configuration file. The default is +`config/config.yaml` in this package; pass a different file with the +`config_file:=` launch argument of the corresponding launch file. -## Launch file +### Node: `sas_robot_driver_ros_composer_node` -An example to compose multiple `RobotDriver` clients serially. +| Property | Value | +|---|---| +| **Executable** | `sas_robot_driver_ros_composer_node` | +| **ROS node name** | `robot_composed` (set by the `name` launch argument of `composer_launch.py`) | +| **Description** | Composes multiple `RobotDriver` clients serially into a single robot driver, exposing one `RobotDriverROS` control loop for all of them. | + +#### Parameters + +| Parameter | Type | Mandatory / Optional | Default | Purpose | +|---|---|---|---|---| +| `robot_driver_client_names` | array of strings | **Mandatory** | none — must be provided | Topic prefixes of the robot-driver clients to compose | +| `override_joint_limits_with_robot_parameter_file` | bool | **Mandatory** | none — must be provided | Whether to take the joint limits from a robot parameter file instead of concatenating the clients' limits | +| `robot_parameter_file_path` | string | **Mandatory** if `override_joint_limits_with_robot_parameter_file` is `true` | none — must be provided | Path of the robot parameter file with the joint limits | +| `thread_sampling_time_sec` | double | **Mandatory** | none — must be provided | Sampling period of the robot control-loop thread | + +### Node: `sas_robot_watchdog_commander_node` + +| Property | Value | +|---|---| +| **Executable** | `sas_robot_watchdog_commander_node` | +| **ROS node name** | `sas_robot_watchdog_commander_node` | +| **Description** | Sends periodic watchdog triggers to a robot-driver client (joint control is blacklisted). | + +#### Parameters + +| Parameter | Type | Mandatory / Optional | Default | Purpose | +|---|---|---|---|---| +| `thread_sampling_time_sec` | double | **Mandatory** | none — must be provided | Sampling period of the watchdog loop | +| `watchdog_period` | double | **Mandatory** | none — must be provided | Watchdog trigger period | +| `watchdog_maximum_acceptable_delay` | double | **Mandatory** | none — must be provided | Maximum acceptable delay before the watchdog is triggered | +| `robot_name` | string | **Mandatory** | none — must be provided | Topic prefix of the robot-driver client | + +### Node: `sas_robot_driver_ros_example` + +| Property | Value | +|---|---| +| **Executable** | `sas_robot_driver_ros_example` | +| **ROS node name** | `sas_robot_driver_ros_example` (the launch example runs two instances named `robot_1` and `robot_2`) | +| **Description** | Minimal example robot driver node. | + +#### Parameters + +| Parameter | Type | Mandatory / Optional | Default | Purpose | +|---|---|---|---|---| +| `robot_name` | string | **Mandatory** | none — must be provided | Name of the robot | +| `initial_joint_positions` | array of doubles (radians) | **Mandatory** | none — must be provided | Initial joint positions | +| `joint_limits_min` | array of doubles (radians) | **Mandatory** | none — must be provided | Minimum joint limits | +| `joint_limits_max` | array of doubles (radians) | **Mandatory** | none — must be provided | Maximum joint limits | +| `thread_sampling_time_sec` | double | **Mandatory** | none — must be provided | Sampling period of the robot control-loop thread | + +**How mandatory/optional is determined in code:** +- **Mandatory** params are read with `sas::get_ros_parameter(...)` — if missing, the node throws and fails to start. +- **Optional** params are read with `sas::get_ros_optional_parameter(..., )` — they carry in-code defaults. + +#### Sample launches ```bash -ros2 launch sas_robot_driver sas_robot_driver_ros_composer_example.py +# Compose two example robots plus a composer node +ros2 launch sas_robot_driver composer_example_launch.py +# Single composer node +ros2 launch sas_robot_driver composer_launch.py +# Watchdog commander +ros2 launch sas_robot_driver watchdog_launch.py ``` ## Script diff --git a/config/config.yaml b/config/config.yaml new file mode 100644 index 0000000..0fc608f --- /dev/null +++ b/config/config.yaml @@ -0,0 +1,39 @@ +# Parameters for the sas_robot_driver nodes. +# +# Each top-level block is keyed by the ROS node name and is loaded by the +# corresponding launch file via its `config_file` argument. The two +# `sas_robot_driver_ros_example` instances are started with the node names +# `robot_1` and `robot_2` (see composer_example_launch.py). + +# sas_robot_driver_ros_example instance 1 +robot_1: + ros__parameters: + robot_name: "robot_1" + initial_joint_positions: [0., 0., 0., 0.] + joint_limits_min: [-1., -2., -3., -4.] + joint_limits_max: [1., 2., 3., 4.] + thread_sampling_time_sec: 0.001 # Robot thread is at 1000 Hz + +# sas_robot_driver_ros_example instance 2 +robot_2: + ros__parameters: + robot_name: "robot_2" + initial_joint_positions: [0., 0., 0., 0., 0., 0.] + joint_limits_min: [-5., -6., -7., -8., -9., -10.] + joint_limits_max: [5., 6., 7., 8., 9., 10.] + thread_sampling_time_sec: 0.001 # Robot thread is at 1000 Hz + +# sas_robot_driver_ros_composer_node +robot_composed: + ros__parameters: + robot_driver_client_names: ["robot_1", "robot_2"] + override_joint_limits_with_robot_parameter_file: false + thread_sampling_time_sec: 0.01 + +# sas_robot_watchdog_commander_node +sas_robot_watchdog_commander_node: + ros__parameters: + robot_name: "robot_composed" + thread_sampling_time_sec: 0.001 + watchdog_period: 0.01 + watchdog_maximum_acceptable_delay: 0.05 diff --git a/launch/composer_example_launch.py b/launch/composer_example_launch.py new file mode 100644 index 0000000..cfcf60d --- /dev/null +++ b/launch/composer_example_launch.py @@ -0,0 +1,41 @@ +import os.path + +from ament_index_python.packages import get_package_share_directory +from launch import LaunchDescription +from launch.actions import DeclareLaunchArgument +from launch.substitutions import LaunchConfiguration +from launch_ros.actions import Node + + +def generate_launch_description(): + """Example composing two example robots and a RobotDriverROSComposer node. + + All parameters are loaded from a YAML configuration file. Pass a + different file with ``config_file:=/path/to/config.yaml``. + """ + config_file = LaunchConfiguration('config_file') + + return LaunchDescription([ + DeclareLaunchArgument( + 'config_file', + default_value=os.path.join(get_package_share_directory('sas_robot_driver'), 'config', 'config.yaml') + ), + Node( + package='sas_robot_driver', + executable='sas_robot_driver_ros_example', + name='robot_1', + parameters=[config_file] + ), + Node( + package='sas_robot_driver', + executable='sas_robot_driver_ros_example', + name='robot_2', + parameters=[config_file] + ), + Node( + package='sas_robot_driver', + executable='sas_robot_driver_ros_composer_node', + name='robot_composed', + parameters=[config_file] + ) + ]) diff --git a/launch/composer_launch.py b/launch/composer_launch.py new file mode 100644 index 0000000..851f5e0 --- /dev/null +++ b/launch/composer_launch.py @@ -0,0 +1,34 @@ +import os.path + +from ament_index_python.packages import get_package_share_directory +from launch import LaunchDescription +from launch.actions import DeclareLaunchArgument +from launch.substitutions import LaunchConfiguration +from launch_ros.actions import Node + + +def generate_launch_description(): + """Launch the RobotDriverROSComposer node. + + Parameters are loaded from a YAML configuration file. Pass a different + file with ``config_file:=/path/to/config.yaml``. + """ + name = LaunchConfiguration('name') + config_file = LaunchConfiguration('config_file') + + return LaunchDescription([ + DeclareLaunchArgument( + 'name', + default_value='robot_composed' + ), + DeclareLaunchArgument( + 'config_file', + default_value=os.path.join(get_package_share_directory('sas_robot_driver'), 'config', 'config.yaml') + ), + Node( + package='sas_robot_driver', + executable='sas_robot_driver_ros_composer_node', + name=name, + parameters=[config_file] + ) + ]) diff --git a/launch/sas_robot_driver_ros_composer_example.py b/launch/sas_robot_driver_ros_composer_example.py deleted file mode 100644 index 8ea9e5f..0000000 --- a/launch/sas_robot_driver_ros_composer_example.py +++ /dev/null @@ -1,43 +0,0 @@ -from launch import LaunchDescription -from launch_ros.actions import Node - - -def generate_launch_description(): - return LaunchDescription([ - Node( - package='sas_robot_driver', - executable='sas_robot_driver_ros_example', - name='robot_1', - parameters=[{ - "robot_name": "robot_1", - "joint_limits_min": [-1., -2., -3., -4.], - "joint_limits_max": [1., 2., 3., 4], - "initial_joint_positions": [0., 0., 0., 0.], - "thread_sampling_time_sec": 0.001 - }] - ), - Node( - package='sas_robot_driver', - executable='sas_robot_driver_ros_example', - name='robot_2', - parameters=[{ - "robot_name": "robot_2", - "joint_limits_min": [-5., -6., -7., -8., -9., -10.], - "joint_limits_max": [5., 6., 7., 8., 9., 10.], - "initial_joint_positions": [0., 0., 0., 0., 0., 0.], - "thread_sampling_time_sec": 0.001 - }] - ), - Node( - package='sas_robot_driver', - executable='sas_robot_driver_ros_composer_node', - name='robot_composed', - parameters=[{ - "use_real_robot": True, - "use_coppeliasim": False, - "robot_driver_client_names": ["robot_1", "robot_2"], - "override_joint_limits_with_robot_parameter_file": False, - "thread_sampling_time_sec": 0.01 - }] - ) - ]) diff --git a/launch/watchdog_launch.py b/launch/watchdog_launch.py new file mode 100644 index 0000000..480bbec --- /dev/null +++ b/launch/watchdog_launch.py @@ -0,0 +1,28 @@ +import os.path + +from ament_index_python.packages import get_package_share_directory +from launch import LaunchDescription +from launch.actions import DeclareLaunchArgument +from launch.substitutions import LaunchConfiguration +from launch_ros.actions import Node + + +def generate_launch_description(): + """Launch the watchdog commander node. + + Parameters are loaded from a YAML configuration file. Pass a different + file with ``config_file:=/path/to/config.yaml``. + """ + config_file = LaunchConfiguration('config_file') + + return LaunchDescription([ + DeclareLaunchArgument( + 'config_file', + default_value=os.path.join(get_package_share_directory('sas_robot_driver'), 'config', 'config.yaml') + ), + Node( + package='sas_robot_driver', + executable='sas_robot_watchdog_commander_node', + parameters=[config_file] + ) + ])