diff --git a/.docker/COLCON_IGNORE b/.docker/COLCON_IGNORE new file mode 100644 index 0000000..e69de29 diff --git a/.docker/Dockerfile b/.docker/Dockerfile new file mode 100644 index 0000000..71f152e --- /dev/null +++ b/.docker/Dockerfile @@ -0,0 +1,14 @@ +FROM ghcr.io/reazon-research/ros2:humble + +COPY ros_entrypoint.sh /ros_entrypoint.sh +RUN . /opt/ros/humble/setup.sh && \ + apt update && \ + apt install nlohmann-json3-dev -y && \ + mkdir --parents ~/ros2_ws/src && \ + git clone --branch main https://github.com/reazon-research/openarm_ros2.git ~/ros2_ws/src/openarm_ros2 && \ + cd ~/ros2_ws && \ + rosdep install --from-paths src --ignore-src -r -y && \ + colcon build --symlink-install && \ + chmod +x /ros_entrypoint.sh + +ENTRYPOINT [ "/ros_entrypoint.sh" ] diff --git a/.docker/README.md b/.docker/README.md new file mode 100644 index 0000000..dc1279b --- /dev/null +++ b/.docker/README.md @@ -0,0 +1,27 @@ +# Docker GUI Forwarding + +On Linux: +```sh +host +local:root +``` + +```sh +docker run --env DISPLAY=$DISPLAY \ +--volume /tmp/.X11-unix:/tmp/.X11-unix \ +--network=host \ +-it ghcr.io/reazon-research/openarm:v0.3 \ +/bin/bash +``` + +Open the MuJoCo sim at +[https://thomasonzhou.github.io/mujoco_anywhere/](https://thomasonzhou.github.io/mujoco_anywhere/) + +```sh +. ~/ros2_ws/install/setup.bash && \ +ros2 launch -d openarm_bimanual_moveit_config demo.launch.py hardware_type:=sim +``` + +# To build the latest image (v0.3) +```sh +docker build --no-cache -t ghcr.io/reazon-research/openarm:v0.3 . +``` diff --git a/.docker/ros_entrypoint.sh b/.docker/ros_entrypoint.sh new file mode 100644 index 0000000..c97596f --- /dev/null +++ b/.docker/ros_entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -e + +source /opt/ros/humble/setup.sh +source ~/ros2_ws/install/setup.bash + +exec "$@" \ No newline at end of file diff --git a/openarm/CMakeLists.txt b/openarm/CMakeLists.txt index 99a2410..da30a8f 100644 --- a/openarm/CMakeLists.txt +++ b/openarm/CMakeLists.txt @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -cmake_minimum_required(VERSION 3.8) +cmake_minimum_required(VERSION 3.22) project(openarm) if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") diff --git a/openarm/package.xml b/openarm/package.xml index b1ab500..98ad247 100644 --- a/openarm/package.xml +++ b/openarm/package.xml @@ -30,7 +30,8 @@ openarm_bringup openarm_description openarm_hardware - + openarm_mujoco_hardware + ament_lint_auto ament_lint_common diff --git a/openarm_bimanual_bringup/CMakeLists.txt b/openarm_bimanual_bringup/CMakeLists.txt index 8669756..d3ddb10 100644 --- a/openarm_bimanual_bringup/CMakeLists.txt +++ b/openarm_bimanual_bringup/CMakeLists.txt @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -cmake_minimum_required(VERSION 3.8) +cmake_minimum_required(VERSION 3.22) project(openarm_bimanual_bringup) if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") diff --git a/openarm_bimanual_bringup/launch/depth_camera.launch.py b/openarm_bimanual_bringup/launch/depth_camera.launch.py index 9ea8caf..c93ab97 100644 --- a/openarm_bimanual_bringup/launch/depth_camera.launch.py +++ b/openarm_bimanual_bringup/launch/depth_camera.launch.py @@ -22,9 +22,11 @@ def generate_launch_description(): realsense_share_dir = FindPackageShare("realsense2_camera") rs_launch_path = [realsense_share_dir, "/launch/rs_launch.py"] - return LaunchDescription([ - IncludeLaunchDescription( - PythonLaunchDescriptionSource(rs_launch_path), - launch_arguments={"pointcloud.enable": "true"}.items() - ) - ]) + return LaunchDescription( + [ + IncludeLaunchDescription( + PythonLaunchDescriptionSource(rs_launch_path), + launch_arguments={"pointcloud.enable": "true"}.items(), + ) + ] + ) diff --git a/openarm_bimanual_bringup/launch/start_teleop.launch.py b/openarm_bimanual_bringup/launch/start_teleop.launch.py index 5989f96..9efae86 100644 --- a/openarm_bimanual_bringup/launch/start_teleop.launch.py +++ b/openarm_bimanual_bringup/launch/start_teleop.launch.py @@ -38,7 +38,8 @@ def generate_launch_description(): use_sim_time = LaunchConfiguration("use_sim_time") use_sim_time_launch_arg = DeclareLaunchArgument( - name="use_sim_time", default_value="false") + name="use_sim_time", default_value="false" + ) robot_state_publisher_node = IncludeLaunchDescription( PythonLaunchDescriptionSource( @@ -63,8 +64,11 @@ def generate_launch_description(): } controller_params = PathJoinSubstitution( - [FindPackageShare(package="openarm_bimanual_bringup"), - "config", "controllers.yaml"] + [ + FindPackageShare(package="openarm_bimanual_bringup"), + "config", + "controllers.yaml", + ] ) controller_manager = Node( diff --git a/openarm_bimanual_description/CMakeLists.txt b/openarm_bimanual_description/CMakeLists.txt index 8ffbad0..8a260c2 100644 --- a/openarm_bimanual_description/CMakeLists.txt +++ b/openarm_bimanual_description/CMakeLists.txt @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -cmake_minimum_required(VERSION 3.8) +cmake_minimum_required(VERSION 3.22) project(openarm_bimanual_description) find_package(ament_cmake REQUIRED) diff --git a/openarm_bimanual_description/launch/description.launch.py b/openarm_bimanual_description/launch/description.launch.py index 3d9e77b..658629f 100644 --- a/openarm_bimanual_description/launch/description.launch.py +++ b/openarm_bimanual_description/launch/description.launch.py @@ -25,15 +25,16 @@ from launch_ros.parameter_descriptions import ParameterValue def generate_launch_description(): pkg_share = Path( - launch_ros.substitutions.FindPackageShare(package="openarm_bimanual_description").find( - "openarm_bimanual_description" - ) + launch_ros.substitutions.FindPackageShare( + package="openarm_bimanual_description" + ).find("openarm_bimanual_description") ) default_model_path = pkg_share / "urdf/openarm_bimanual.urdf.xacro" use_sim_time = LaunchConfiguration("use_sim_time") use_sim_time_launch_arg = DeclareLaunchArgument( - "use_sim_time", default_value="true") + "use_sim_time", default_value="true" + ) robot_state_publisher_node = launch_ros.actions.Node( package="robot_state_publisher", diff --git a/openarm_bimanual_description/launch/display.launch.py b/openarm_bimanual_description/launch/display.launch.py index ad3fd5a..8e863b1 100644 --- a/openarm_bimanual_description/launch/display.launch.py +++ b/openarm_bimanual_description/launch/display.launch.py @@ -25,9 +25,9 @@ from launch.launch_description_sources import PythonLaunchDescriptionSource def generate_launch_description(): pkg_share = Path( - launch_ros.substitutions.FindPackageShare(package="openarm_bimanual_description").find( - "openarm_bimanual_description" - ) + launch_ros.substitutions.FindPackageShare( + package="openarm_bimanual_description" + ).find("openarm_bimanual_description") ) default_model_path = pkg_share / "urdf/openarm_bimanual.urdf.xacro" default_rviz_config_path = pkg_share / "rviz/robot_description.rviz" diff --git a/openarm_bimanual_description/launch/gazebo.launch.py b/openarm_bimanual_description/launch/gazebo.launch.py index 52f985c..240b9f7 100644 --- a/openarm_bimanual_description/launch/gazebo.launch.py +++ b/openarm_bimanual_description/launch/gazebo.launch.py @@ -37,7 +37,8 @@ def generate_launch_description(): # Make path to resources dir without last package_name fragment. path_to_share_dir_clipped = "".join( get_package_share_directory(resources_package).rsplit( - "/" + resources_package, 1) + "/" + resources_package, 1 + ) ) # Gazebo hint for resources. @@ -57,10 +58,12 @@ def generate_launch_description(): use_custom_world = LaunchConfiguration("use_custom_world") use_custom_world_launch_arg = DeclareLaunchArgument( - "use_custom_world", default_value="true") + "use_custom_world", default_value="true" + ) gazebo_world = LaunchConfiguration("gazebo_world") gazebo_world_launch_arg = DeclareLaunchArgument( - "gazebo_world", default_value="empty.sdf") + "gazebo_world", default_value="empty.sdf" + ) # prepare custom world world = os.getenv("GZ_SIM_WORLD", "empty") @@ -78,9 +81,11 @@ def generate_launch_description(): PythonLaunchDescriptionSource( os.path.join(pkg_ros_gz_sim, "launch", "gz_sim.launch.py"), ), - launch_arguments=launch_arguments - if use_custom_world - else dict(gz_args="-r " + gazebo_world + " --verbose").items(), + launch_arguments=( + launch_arguments + if use_custom_world + else dict(gz_args="-r " + gazebo_world + " --verbose").items() + ), ) # Spawn @@ -104,7 +109,8 @@ def generate_launch_description(): use_sim_time = LaunchConfiguration("use_sim_time") use_sim_time_launch_arg = DeclareLaunchArgument( - "use_sim_time", default_value="true") + "use_sim_time", default_value="true" + ) use_rviz = LaunchConfiguration("use_rviz") use_rviz_arg = DeclareLaunchArgument("use_rviz", default_value="true") diff --git a/openarm_bimanual_description/urdf/openarm_bimanual.urdf b/openarm_bimanual_description/urdf/openarm_bimanual.urdf index fa82676..dbe78c4 100644 --- a/openarm_bimanual_description/urdf/openarm_bimanual.urdf +++ b/openarm_bimanual_description/urdf/openarm_bimanual.urdf @@ -1,6 +1,6 @@ - + + @@ -426,20 +427,21 @@ - + + - + - + @@ -454,6 +456,7 @@ + @@ -709,20 +712,21 @@ - + + - + - + @@ -743,64 +747,69 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + @@ -811,63 +820,68 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + diff --git a/openarm_bimanual_description/urdf/openarm_bimanual.urdf.xacro b/openarm_bimanual_description/urdf/openarm_bimanual.urdf.xacro index 096607a..3c3cd9e 100644 --- a/openarm_bimanual_description/urdf/openarm_bimanual.urdf.xacro +++ b/openarm_bimanual_description/urdf/openarm_bimanual.urdf.xacro @@ -15,14 +15,15 @@  limitations under the License. --> + - + - + @@ -54,8 +55,8 @@ - - - + + + diff --git a/openarm_bimanual_moveit_config/config/initial_positions.yaml b/openarm_bimanual_moveit_config/config/initial_positions.yaml index 62bcdf0..4740b89 100644 --- a/openarm_bimanual_moveit_config/config/initial_positions.yaml +++ b/openarm_bimanual_moveit_config/config/initial_positions.yaml @@ -15,7 +15,7 @@ # Default initial positions for openarm_bimanual's ros2_control fake system initial_positions: - left_left_pris1: 0 + left_gripper: 0 left_rev1: 0 left_rev2: 0 left_rev3: 0 @@ -23,7 +23,7 @@ initial_positions: left_rev5: 0 left_rev6: 0 left_rev7: 0 - right_left_pris1: 0 + right_gripper: 0 right_rev1: 0 right_rev2: 0 right_rev3: 0 diff --git a/openarm_bimanual_moveit_config/config/joint_limits.yaml b/openarm_bimanual_moveit_config/config/joint_limits.yaml index e67c8fe..69ce9b0 100644 --- a/openarm_bimanual_moveit_config/config/joint_limits.yaml +++ b/openarm_bimanual_moveit_config/config/joint_limits.yaml @@ -22,7 +22,7 @@ default_acceleration_scaling_factor: 0.1 # Specific joint properties can be changed with the keys [max_position, min_position, max_velocity, max_acceleration] # Joint limits can be turned off with [has_velocity_limits, has_acceleration_limits] joint_limits: - left_left_pris1: + left_gripper: has_velocity_limits: true max_velocity: 0.10000000000000001 has_acceleration_limits: false @@ -62,7 +62,7 @@ joint_limits: max_velocity: 0 has_acceleration_limits: false max_acceleration: 0 - right_left_pris1: + right_gripper: has_velocity_limits: true max_velocity: 0.10000000000000001 has_acceleration_limits: false diff --git a/openarm_bimanual_moveit_config/config/moveit_controllers.yaml b/openarm_bimanual_moveit_config/config/moveit_controllers.yaml index 521b037..676b2fe 100644 --- a/openarm_bimanual_moveit_config/config/moveit_controllers.yaml +++ b/openarm_bimanual_moveit_config/config/moveit_controllers.yaml @@ -50,12 +50,12 @@ moveit_simple_controller_manager: left_gripper_controller: type: GripperCommand joints: - - left_left_pris1 + - left_gripper action_ns: gripper_cmd default: true right_gripper_controller: type: GripperCommand joints: - - right_left_pris1 + - right_gripper action_ns: gripper_cmd default: true diff --git a/openarm_bimanual_moveit_config/config/openarm_bimanual.ros2_control.xacro b/openarm_bimanual_moveit_config/config/openarm_bimanual.ros2_control.xacro deleted file mode 100644 index 0e3b8dc..0000000 --- a/openarm_bimanual_moveit_config/config/openarm_bimanual.ros2_control.xacro +++ /dev/null @@ -1,157 +0,0 @@ - - - - - - - - - - mock_components/GenericSystem - - - - - - ${initial_positions['left_rev1']} - - - - - - - - ${initial_positions['left_rev2']} - - - - - - - - ${initial_positions['left_rev3']} - - - - - - - - ${initial_positions['left_rev4']} - - - - - - - - ${initial_positions['left_rev5']} - - - - - - - - ${initial_positions['left_rev6']} - - - - - - - - ${initial_positions['left_rev7']} - - - - - - - - ${initial_positions['right_rev1']} - - - - - - - - ${initial_positions['right_rev2']} - - - - - - - - ${initial_positions['right_rev3']} - - - - - - - - ${initial_positions['right_rev4']} - - - - - - - - ${initial_positions['right_rev5']} - - - - - - - - ${initial_positions['right_rev6']} - - - - - - - - ${initial_positions['right_rev7']} - - - - - - - - ${initial_positions['left_left_pris1']} - - - - - - - - ${initial_positions['right_left_pris1']} - - - - - - - diff --git a/openarm_bimanual_moveit_config/config/openarm_bimanual.srdf b/openarm_bimanual_moveit_config/config/openarm_bimanual.srdf index e58b8c5..e2b3f30 100644 --- a/openarm_bimanual_moveit_config/config/openarm_bimanual.srdf +++ b/openarm_bimanual_moveit_config/config/openarm_bimanual.srdf @@ -66,11 +66,11 @@ - + - + @@ -86,7 +86,7 @@ - + @@ -94,7 +94,7 @@ - + @@ -104,7 +104,7 @@ - + @@ -112,7 +112,7 @@ - + @@ -122,22 +122,22 @@ - + - + - + - + - + - + @@ -145,7 +145,7 @@ - + @@ -155,25 +155,25 @@ - + - + - + - + - + @@ -181,7 +181,7 @@ - + @@ -191,7 +191,7 @@ - + @@ -199,7 +199,7 @@ - + @@ -209,7 +209,7 @@ - + @@ -217,7 +217,7 @@ - + @@ -227,7 +227,7 @@ - + @@ -235,7 +235,7 @@ - + @@ -245,7 +245,7 @@ - + @@ -253,7 +253,7 @@ - + @@ -263,7 +263,7 @@ - + @@ -271,7 +271,7 @@ - + @@ -286,8 +286,8 @@ - - + + diff --git a/openarm_bimanual_moveit_config/config/openarm_bimanual.urdf.xacro b/openarm_bimanual_moveit_config/config/openarm_bimanual.urdf.xacro index 83826f3..c87d773 100644 --- a/openarm_bimanual_moveit_config/config/openarm_bimanual.urdf.xacro +++ b/openarm_bimanual_moveit_config/config/openarm_bimanual.urdf.xacro @@ -16,6 +16,7 @@ --> + @@ -26,10 +27,5 @@ - - diff --git a/openarm_bimanual_moveit_config/config/ros2_controllers.yaml b/openarm_bimanual_moveit_config/config/ros2_controllers.yaml index 24b5d3a..3a18e76 100644 --- a/openarm_bimanual_moveit_config/config/ros2_controllers.yaml +++ b/openarm_bimanual_moveit_config/config/ros2_controllers.yaml @@ -82,10 +82,10 @@ right_arm_controller: - velocity left_gripper_controller: ros__parameters: - joint: left_left_pris1 + joint: left_gripper right_gripper_controller: ros__parameters: - joint: right_left_pris1 + joint: right_gripper left_side_controller: ros__parameters: joints: @@ -96,7 +96,7 @@ left_side_controller: - left_rev5 - left_rev6 - left_rev7 - - left_left_pris1 + - left_gripper command_interfaces: - position - velocity @@ -113,7 +113,7 @@ right_side_controller: - right_rev5 - right_rev6 - right_rev7 - - right_left_pris1 + - right_gripper command_interfaces: - position - velocity @@ -130,7 +130,7 @@ upper_body_controller: - left_rev5 - left_rev6 - left_rev7 - - left_left_pris1 + - left_gripper - right_rev1 - right_rev2 - right_rev3 @@ -138,7 +138,7 @@ upper_body_controller: - right_rev5 - right_rev6 - right_rev7 - - right_left_pris1 + - right_gripper command_interfaces: - position - velocity diff --git a/openarm_bimanual_moveit_config/launch/demo.launch.py b/openarm_bimanual_moveit_config/launch/demo.launch.py index ac1a259..08d8b80 100644 --- a/openarm_bimanual_moveit_config/launch/demo.launch.py +++ b/openarm_bimanual_moveit_config/launch/demo.launch.py @@ -14,9 +14,53 @@ from moveit_configs_utils import MoveItConfigsBuilder from moveit_configs_utils.launches import generate_demo_launch +from launch import LaunchDescription +from launch.actions import DeclareLaunchArgument +from launch.substitutions import LaunchConfiguration, Command, PathJoinSubstitution, FindExecutable +from launch_ros.substitutions import FindPackageShare def generate_launch_description(): - moveit_config = MoveItConfigsBuilder( - "openarm_bimanual", package_name="openarm_bimanual_moveit_config").to_moveit_configs() - return generate_demo_launch(moveit_config) + declared_arguments = [] + declared_arguments.append( + DeclareLaunchArgument( + "hardware_type", + default_value="real", + description="Hardware interface type: 'real', 'sim' (MuJoCo), or 'mock'", + ) + ) + declared_arguments.append( + DeclareLaunchArgument( + "mock_sensor_commands", + default_value="false", + description="Enable writable sensor interfaces when using mock hardware", + ) + ) + + hardware_type = LaunchConfiguration("hardware_type") + mock_sensor_commands = LaunchConfiguration("mock_sensor_commands") + + moveit_config = ( + MoveItConfigsBuilder("openarm_bimanual", + package_name="openarm_bimanual_moveit_config") + .robot_description( + file_path="config/openarm_bimanual.urdf.xacro", + mappings={ + "hardware_type": hardware_type, + "mock_sensor_commands": mock_sensor_commands, + }, + ) + .to_moveit_configs() + ) + + demo_ld = generate_demo_launch(moveit_config) + + ld = LaunchDescription() + + for arg in declared_arguments: + ld.add_action(arg) + + for entity in demo_ld.entities: + ld.add_action(entity) + + return ld diff --git a/openarm_bimanual_moveit_config/launch/move_group.launch.py b/openarm_bimanual_moveit_config/launch/move_group.launch.py index 5072d9e..1ea72bf 100644 --- a/openarm_bimanual_moveit_config/launch/move_group.launch.py +++ b/openarm_bimanual_moveit_config/launch/move_group.launch.py @@ -18,5 +18,6 @@ from moveit_configs_utils.launches import generate_move_group_launch def generate_launch_description(): moveit_config = MoveItConfigsBuilder( - "openarm_bimanual", package_name="openarm_bimanual_moveit_config").to_moveit_configs() + "openarm_bimanual", package_name="openarm_bimanual_moveit_config" + ).to_moveit_configs() return generate_move_group_launch(moveit_config) diff --git a/openarm_bimanual_moveit_config/launch/moveit_rviz.launch.py b/openarm_bimanual_moveit_config/launch/moveit_rviz.launch.py index 2061e83..6ba122d 100644 --- a/openarm_bimanual_moveit_config/launch/moveit_rviz.launch.py +++ b/openarm_bimanual_moveit_config/launch/moveit_rviz.launch.py @@ -18,5 +18,6 @@ from moveit_configs_utils.launches import generate_moveit_rviz_launch def generate_launch_description(): moveit_config = MoveItConfigsBuilder( - "openarm_bimanual", package_name="openarm_bimanual_moveit_config").to_moveit_configs() + "openarm_bimanual", package_name="openarm_bimanual_moveit_config" + ).to_moveit_configs() return generate_moveit_rviz_launch(moveit_config) diff --git a/openarm_bimanual_moveit_config/launch/rsp.launch.py b/openarm_bimanual_moveit_config/launch/rsp.launch.py index 2561327..72beea6 100644 --- a/openarm_bimanual_moveit_config/launch/rsp.launch.py +++ b/openarm_bimanual_moveit_config/launch/rsp.launch.py @@ -18,5 +18,6 @@ from moveit_configs_utils.launches import generate_rsp_launch def generate_launch_description(): moveit_config = MoveItConfigsBuilder( - "openarm_bimanual", package_name="openarm_bimanual_moveit_config").to_moveit_configs() + "openarm_bimanual", package_name="openarm_bimanual_moveit_config" + ).to_moveit_configs() return generate_rsp_launch(moveit_config) diff --git a/openarm_bimanual_moveit_config/launch/setup_assistant.launch.py b/openarm_bimanual_moveit_config/launch/setup_assistant.launch.py index 0452258..fc158b4 100644 --- a/openarm_bimanual_moveit_config/launch/setup_assistant.launch.py +++ b/openarm_bimanual_moveit_config/launch/setup_assistant.launch.py @@ -18,5 +18,6 @@ from moveit_configs_utils.launches import generate_setup_assistant_launch def generate_launch_description(): moveit_config = MoveItConfigsBuilder( - "openarm_bimanual", package_name="openarm_bimanual_moveit_config").to_moveit_configs() + "openarm_bimanual", package_name="openarm_bimanual_moveit_config" + ).to_moveit_configs() return generate_setup_assistant_launch(moveit_config) diff --git a/openarm_bimanual_moveit_config/launch/spawn_controllers.launch.py b/openarm_bimanual_moveit_config/launch/spawn_controllers.launch.py index ef63751..95c7565 100644 --- a/openarm_bimanual_moveit_config/launch/spawn_controllers.launch.py +++ b/openarm_bimanual_moveit_config/launch/spawn_controllers.launch.py @@ -18,5 +18,6 @@ from moveit_configs_utils.launches import generate_spawn_controllers_launch def generate_launch_description(): moveit_config = MoveItConfigsBuilder( - "openarm_bimanual", package_name="openarm_bimanual_moveit_config").to_moveit_configs() + "openarm_bimanual", package_name="openarm_bimanual_moveit_config" + ).to_moveit_configs() return generate_spawn_controllers_launch(moveit_config) diff --git a/openarm_bimanual_moveit_config/launch/static_virtual_joint_tfs.launch.py b/openarm_bimanual_moveit_config/launch/static_virtual_joint_tfs.launch.py index bfc4ca5..6cfa2e7 100644 --- a/openarm_bimanual_moveit_config/launch/static_virtual_joint_tfs.launch.py +++ b/openarm_bimanual_moveit_config/launch/static_virtual_joint_tfs.launch.py @@ -18,5 +18,6 @@ from moveit_configs_utils.launches import generate_static_virtual_joint_tfs_laun def generate_launch_description(): moveit_config = MoveItConfigsBuilder( - "openarm_bimanual", package_name="openarm_bimanual_moveit_config").to_moveit_configs() + "openarm_bimanual", package_name="openarm_bimanual_moveit_config" + ).to_moveit_configs() return generate_static_virtual_joint_tfs_launch(moveit_config) diff --git a/openarm_bimanual_moveit_config/launch/warehouse_db.launch.py b/openarm_bimanual_moveit_config/launch/warehouse_db.launch.py index f6ef5d1..5886bb7 100644 --- a/openarm_bimanual_moveit_config/launch/warehouse_db.launch.py +++ b/openarm_bimanual_moveit_config/launch/warehouse_db.launch.py @@ -18,5 +18,6 @@ from moveit_configs_utils.launches import generate_warehouse_db_launch def generate_launch_description(): moveit_config = MoveItConfigsBuilder( - "openarm_bimanual", package_name="openarm_bimanual_moveit_config").to_moveit_configs() + "openarm_bimanual", package_name="openarm_bimanual_moveit_config" + ).to_moveit_configs() return generate_warehouse_db_launch(moveit_config) diff --git a/openarm_bringup/CMakeLists.txt b/openarm_bringup/CMakeLists.txt index e59468f..22ca5a1 100644 --- a/openarm_bringup/CMakeLists.txt +++ b/openarm_bringup/CMakeLists.txt @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -cmake_minimum_required(VERSION 3.8) +cmake_minimum_required(VERSION 3.22) project(openarm_bringup) if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") diff --git a/openarm_bringup/launch/openarm.launch.py b/openarm_bringup/launch/openarm.launch.py index 158ec35..3a20d09 100644 --- a/openarm_bringup/launch/openarm.launch.py +++ b/openarm_bringup/launch/openarm.launch.py @@ -23,7 +23,12 @@ from launch import LaunchDescription from launch.actions import DeclareLaunchArgument, RegisterEventHandler, TimerAction from launch.event_handlers import OnProcessExit, OnProcessStart -from launch.substitutions import Command, FindExecutable, LaunchConfiguration, PathJoinSubstitution +from launch.substitutions import ( + Command, + FindExecutable, + LaunchConfiguration, + PathJoinSubstitution, +) from launch_ros.actions import Node from launch_ros.substitutions import FindPackageShare @@ -72,9 +77,9 @@ def generate_launch_description(): ) declared_arguments.append( DeclareLaunchArgument( - "use_mock_hardware", - default_value="false", - description="Start robot with mock hardware mirroring command to its states.", + "hardware_type", + default_value="real", + description="Hardware interface type: 'real', 'sim' (MuJoCo), or 'mock'", ) ) declared_arguments.append( @@ -82,7 +87,7 @@ def generate_launch_description(): "mock_sensor_commands", default_value="false", description="Enable mock command interfaces for sensors used for simple simulations. \ - Used only if 'use_mock_hardware' parameter is true.", + Used only if 'hardware_type' parameter is 'mock'.", ) ) declared_arguments.append( @@ -101,7 +106,7 @@ def generate_launch_description(): description_package = LaunchConfiguration("description_package") description_file = LaunchConfiguration("description_file") prefix = LaunchConfiguration("prefix") - use_mock_hardware = LaunchConfiguration("use_mock_hardware") + hardware_type = LaunchConfiguration("hardware_type") mock_sensor_commands = LaunchConfiguration("mock_sensor_commands") robot_controller = LaunchConfiguration("robot_controller") @@ -118,8 +123,8 @@ def generate_launch_description(): "prefix:=", prefix, " ", - "use_mock_hardware:=", - use_mock_hardware, + "hardware_type:=", + hardware_type, " ", "mock_sensor_commands:=", mock_sensor_commands, @@ -133,8 +138,7 @@ def generate_launch_description(): [FindPackageShare(runtime_config_package), "config", controllers_file] ) rviz_config_file = PathJoinSubstitution( - [FindPackageShare(description_package), "rviz", - "robot_description.rviz"] + [FindPackageShare(description_package), "rviz", "openarm.rviz"] ) control_node = Node( @@ -160,8 +164,11 @@ def generate_launch_description(): joint_state_broadcaster_spawner = Node( package="controller_manager", executable="spawner", - arguments=["joint_state_broadcaster", - "--controller-manager", "/controller_manager"], + arguments=[ + "joint_state_broadcaster", + "--controller-manager", + "/controller_manager", + ], ) robot_controller_names = [robot_controller] @@ -188,15 +195,17 @@ def generate_launch_description(): ] # Delay loading and activation of `joint_state_broadcaster` after start of ros2_control_node - delay_joint_state_broadcaster_spawner_after_ros2_control_node = RegisterEventHandler( - event_handler=OnProcessStart( - target_action=control_node, - on_start=[ - TimerAction( - period=3.0, - actions=[joint_state_broadcaster_spawner], - ), - ], + delay_joint_state_broadcaster_spawner_after_ros2_control_node = ( + RegisterEventHandler( + event_handler=OnProcessStart( + target_action=control_node, + on_start=[ + TimerAction( + period=3.0, + actions=[joint_state_broadcaster_spawner], + ), + ], + ) ) ) diff --git a/openarm_bringup/launch/test_forward_position_controller.launch.py b/openarm_bringup/launch/test_forward_position_controller.launch.py index 72d1c1b..da1f888 100644 --- a/openarm_bringup/launch/test_forward_position_controller.launch.py +++ b/openarm_bringup/launch/test_forward_position_controller.launch.py @@ -28,8 +28,11 @@ from launch_ros.substitutions import FindPackageShare def generate_launch_description(): position_goals = PathJoinSubstitution( - [FindPackageShare("openarm_bringup"), "config", - "test_goal_publishers_config.yaml"] + [ + FindPackageShare("openarm_bringup"), + "config", + "test_goal_publishers_config.yaml", + ] ) return LaunchDescription( diff --git a/openarm_bringup/launch/test_joint_trajectory_controller.launch.py b/openarm_bringup/launch/test_joint_trajectory_controller.launch.py index c72189a..3e68ef2 100644 --- a/openarm_bringup/launch/test_joint_trajectory_controller.launch.py +++ b/openarm_bringup/launch/test_joint_trajectory_controller.launch.py @@ -28,8 +28,11 @@ from launch_ros.substitutions import FindPackageShare def generate_launch_description(): position_goals = PathJoinSubstitution( - [FindPackageShare("openarm_bringup"), "config", - "test_goal_publishers_config.yaml"] + [ + FindPackageShare("openarm_bringup"), + "config", + "test_goal_publishers_config.yaml", + ] ) return LaunchDescription( diff --git a/openarm_description/CMakeLists.txt b/openarm_description/CMakeLists.txt index 67c236a..30282f4 100644 --- a/openarm_description/CMakeLists.txt +++ b/openarm_description/CMakeLists.txt @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -cmake_minimum_required(VERSION 3.8) +cmake_minimum_required(VERSION 3.22) project(openarm_description) find_package(ament_cmake REQUIRED) diff --git a/openarm_description/config/initial_positions.yaml b/openarm_description/config/initial_positions.yaml index 3db675f..759a2fe 100644 --- a/openarm_description/config/initial_positions.yaml +++ b/openarm_description/config/initial_positions.yaml @@ -15,8 +15,8 @@ # Default initial positions for openarm's ros2_control fake system initial_positions: - left_pris1: 0 - right_pris2: 0 + gripper: 0 + gripper_mimic: 0 rev1: 0 rev2: 0 rev3: 0 diff --git a/openarm_description/launch/description.launch.py b/openarm_description/launch/description.launch.py index 7f07224..93d8127 100644 --- a/openarm_description/launch/description.launch.py +++ b/openarm_description/launch/description.launch.py @@ -37,34 +37,52 @@ def generate_launch_description(): description="Absolute path to the robot's URDF file", ) side_arg = DeclareLaunchArgument( - name="side", default_value="right", # Use "left" to test left arm. - description="Select arm side: 'left' or 'right'" + name="side", + default_value="right", # Use "left" to test left arm. + description="Select arm side: 'left' or 'right'", ) zero_pos_arg = DeclareLaunchArgument( - # Use "arm" to test alternative configuration. - name="zero_pos", default_value="up", - description="Specify zero position: 'up' or 'arm'" + name="zero_pos", + default_value="up", # Use "arm" to test alternative configuration. + description="Specify zero position: 'up' or 'arm'", ) prefix_arg = DeclareLaunchArgument( - name="prefix", default_value="", - description="Prefix for link and joint names (e.g., left_, right_)" + name="prefix", + default_value="", + description="Prefix for link and joint names (e.g., left_, right_)", ) can_device_arg = DeclareLaunchArgument( - name="can_device", default_value="can0", - description="CAN device identifier to use" + name="can_device", + default_value="can0", + description="CAN device identifier to use", + ) + hardware_type_arg = DeclareLaunchArgument( + name="hardware_type", + default_value="real", + description="Hardware interface type: 'real', 'sim' (MuJoCo), or 'mock'", ) use_sim_time = LaunchConfiguration("use_sim_time") use_sim_time_launch_arg = DeclareLaunchArgument( - "use_sim_time", default_value="true") + "use_sim_time", default_value="true" + ) - robot_description_command = Command([ - "xacro ", LaunchConfiguration("model"), - " side:=", LaunchConfiguration("side"), - " zero_pos:=", LaunchConfiguration("zero_pos"), - " prefix:=", LaunchConfiguration("prefix"), - " can_device:=", LaunchConfiguration("can_device") - ]) + robot_description_command = Command( + [ + "xacro ", + LaunchConfiguration("model"), + " side:=", + LaunchConfiguration("side"), + " zero_pos:=", + LaunchConfiguration("zero_pos"), + " prefix:=", + LaunchConfiguration("prefix"), + " can_device:=", + LaunchConfiguration("can_device"), + " hardware_type:=", + LaunchConfiguration("hardware_type"), + ] + ) robot_state_publisher_node = launch_ros.actions.Node( package="robot_state_publisher", @@ -87,6 +105,7 @@ def generate_launch_description(): zero_pos_arg, prefix_arg, can_device_arg, + hardware_type_arg, use_sim_time_launch_arg, robot_state_publisher_node, ] diff --git a/openarm_description/launch/display.launch.py b/openarm_description/launch/display.launch.py index 98bdc13..22d80cd 100644 --- a/openarm_description/launch/display.launch.py +++ b/openarm_description/launch/display.launch.py @@ -46,7 +46,10 @@ def generate_launch_description(): ), ] ), - launch_arguments=dict(use_sim_time=use_sim_time).items(), + launch_arguments=dict( + use_sim_time=use_sim_time, + hardware_type=LaunchConfiguration("hardware_type") + ).items(), ) joint_state_publisher_node = launch_ros.actions.Node( @@ -107,6 +110,11 @@ def generate_launch_description(): default_value=str(default_rviz_config_path), description="Absolute path to rviz config file", ), + launch.actions.DeclareLaunchArgument( + name="hardware_type", + default_value="real", + description="Hardware interface type: 'real', 'sim' (MuJoCo), or 'mock'", + ), joint_state_publisher_node, joint_state_publisher_gui_node, robot_state_publisher_node, diff --git a/openarm_description/launch/gazebo.launch.py b/openarm_description/launch/gazebo.launch.py index 7703d62..6220edf 100644 --- a/openarm_description/launch/gazebo.launch.py +++ b/openarm_description/launch/gazebo.launch.py @@ -37,7 +37,8 @@ def generate_launch_description(): # Make path to resources dir without last package_name fragment. path_to_share_dir_clipped = "".join( get_package_share_directory(resources_package).rsplit( - "/" + resources_package, 1) + "/" + resources_package, 1 + ) ) # Gazebo hint for resources. @@ -57,10 +58,12 @@ def generate_launch_description(): use_custom_world = LaunchConfiguration("use_custom_world") use_custom_world_launch_arg = DeclareLaunchArgument( - "use_custom_world", default_value="true") + "use_custom_world", default_value="true" + ) gazebo_world = LaunchConfiguration("gazebo_world") gazebo_world_launch_arg = DeclareLaunchArgument( - "gazebo_world", default_value="empty.sdf") + "gazebo_world", default_value="empty.sdf" + ) # prepare custom world world = os.getenv("GZ_SIM_WORLD", "empty") @@ -78,9 +81,11 @@ def generate_launch_description(): PythonLaunchDescriptionSource( os.path.join(pkg_ros_gz_sim, "launch", "gz_sim.launch.py"), ), - launch_arguments=launch_arguments - if use_custom_world - else dict(gz_args="-r " + gazebo_world + " --verbose").items(), + launch_arguments=( + launch_arguments + if use_custom_world + else dict(gz_args="-r " + gazebo_world + " --verbose").items() + ), ) # Spawn @@ -104,7 +109,8 @@ def generate_launch_description(): use_sim_time = LaunchConfiguration("use_sim_time") use_sim_time_launch_arg = DeclareLaunchArgument( - "use_sim_time", default_value="true") + "use_sim_time", default_value="true" + ) use_rviz = LaunchConfiguration("use_rviz") use_rviz_arg = DeclareLaunchArgument("use_rviz", default_value="true") diff --git a/openarm_description/urdf/openarm.robot.xacro b/openarm_description/urdf/openarm.robot.xacro index fdb687e..3239c21 100644 --- a/openarm_description/urdf/openarm.robot.xacro +++ b/openarm_description/urdf/openarm.robot.xacro @@ -1,312 +1,315 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/openarm_description/urdf/openarm.ros2_control.xacro b/openarm_description/urdf/openarm.ros2_control.xacro index 069fcc8..3c483d7 100644 --- a/openarm_description/urdf/openarm.ros2_control.xacro +++ b/openarm_description/urdf/openarm.ros2_control.xacro @@ -15,83 +15,104 @@  limitations under the License. --> - + + + + + + + + + - + mock_components/GenericSystem ${mock_sensor_commands} - + + openarm_mujoco_hardware/MujocoHardware + ${prefix} + + ${computed_websocket_port} + + + openarm_hardware/OpenArmHW ${prefix} ${can_device} - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + diff --git a/openarm_description/urdf/openarm.urdf b/openarm_description/urdf/openarm.urdf index a38f872..3117469 100644 --- a/openarm_description/urdf/openarm.urdf +++ b/openarm_description/urdf/openarm.urdf @@ -1,6 +1,6 @@ - + + @@ -283,20 +284,21 @@ - + + - + - + @@ -305,69 +307,75 @@ - mock_components/GenericSystem - False + openarm_hardware/OpenArmHW + + can0 - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + diff --git a/openarm_description/urdf/openarm.urdf.xacro b/openarm_description/urdf/openarm.urdf.xacro index 98f2e0a..37aeffe 100644 --- a/openarm_description/urdf/openarm.urdf.xacro +++ b/openarm_description/urdf/openarm.urdf.xacro @@ -19,19 +19,19 @@ - + - + - + - - + + diff --git a/openarm_hardware/CMakeLists.txt b/openarm_hardware/CMakeLists.txt index c788555..af0ab4d 100644 --- a/openarm_hardware/CMakeLists.txt +++ b/openarm_hardware/CMakeLists.txt @@ -13,7 +13,7 @@ # limitations under the License. -cmake_minimum_required(VERSION 3.8) +cmake_minimum_required(VERSION 3.22) project(openarm_hardware) if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") diff --git a/openarm_hardware/test/test_openarm_hardware.cpp b/openarm_hardware/test/test_openarm_hardware.cpp index 1f64b27..c2cc4f7 100644 --- a/openarm_hardware/test/test_openarm_hardware.cpp +++ b/openarm_hardware/test/test_openarm_hardware.cpp @@ -102,14 +102,14 @@ class TestOpenArmHW : public ::testing::Test { - + 0 - + 0 diff --git a/openarm_mujoco_hardware/CMakeLists.txt b/openarm_mujoco_hardware/CMakeLists.txt new file mode 100644 index 0000000..7069ef8 --- /dev/null +++ b/openarm_mujoco_hardware/CMakeLists.txt @@ -0,0 +1,98 @@ +# Copyright 2025 Reazon Holdings, Inc. +# +# 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. + +cmake_minimum_required(VERSION 3.22) +project(openarm_mujoco_hardware) + +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wall -Wextra -Wpedantic) +endif() + +# find dependencies +find_package(ament_cmake REQUIRED) +find_package(hardware_interface REQUIRED) +find_package(pluginlib REQUIRED) +find_package(rclcpp REQUIRED) +find_package(rclcpp_lifecycle REQUIRED) +find_package(Boost REQUIRED) +find_package(nlohmann_json REQUIRED) + +add_library(${PROJECT_NAME} SHARED + src/openarm_mujoco_hardware.cpp +) + +target_include_directories( + ${PROJECT_NAME} + PRIVATE + include +) + +ament_target_dependencies(${PROJECT_NAME} PUBLIC + hardware_interface + pluginlib + rclcpp + rclcpp_lifecycle +) + +pluginlib_export_plugin_description_file(hardware_interface openarm_mujoco_hardware.xml) + +target_link_libraries(${PROJECT_NAME} PRIVATE Boost::headers nlohmann_json::nlohmann_json) + + +install( + TARGETS ${PROJECT_NAME} + DESTINATION lib + ) + +install(DIRECTORY include/ +DESTINATION include +) + + +if(BUILD_TESTING) + find_package(ament_lint_auto REQUIRED) + find_package(ament_cmake_gmock REQUIRED) + find_package(hardware_interface REQUIRED) + find_package(ros2_control_test_assets REQUIRED) + + ament_add_gmock(test_openarm_mujoco_hardware + test/test_load_openarm_mujoco_hardware.cpp + ) + target_link_libraries(test_openarm_mujoco_hardware + ${PROJECT_NAME} + ) + ament_target_dependencies(test_openarm_mujoco_hardware + hardware_interface + ros2_control_test_assets + ) + + set(ament_cmake_copyright_FOUND TRUE) + set(ament_cmake_cpplint_FOUND TRUE) + ament_lint_auto_find_test_dependencies() +endif() + +ament_export_include_directories( + include +) + +ament_export_libraries( + ${PROJECT_NAME} +) +ament_export_dependencies( + hardware_interface + pluginlib + rclcpp +) + +ament_package() diff --git a/openarm_mujoco_hardware/LICENSE b/openarm_mujoco_hardware/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/openarm_mujoco_hardware/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/openarm_mujoco_hardware/README.md b/openarm_mujoco_hardware/README.md new file mode 100644 index 0000000..cfb1836 --- /dev/null +++ b/openarm_mujoco_hardware/README.md @@ -0,0 +1,42 @@ +# OpenArm MuJoCo Hardware Interface + +This package provides a ros2_control hardware interface for simulating OpenArm using the MuJoCo physics engine in place of physical hardware. It connects to a WebAssembly instance of MuJoCo through WebSockets. + +## Usage + +Certain OpenArm packages have been configured to use this interface by specifying the `hardware_type` flag. +This flag defaults to `real`, which assumes the use of physical hardware. +Setting the flag to `mock` uses an interface that writes commands to states directly (with some delays). +Setting the flag to `sim` uses the MuJoCo hardware interface. + +For example, to use MoveIt2 with simulated bimanual hardware, first run MuJoCo by visiting: + +[github.com/thomasonzhou/mujoco_anywhere](https://github.com/thomasonzhou/mujoco_anywhere) + +Then run the original command with the `hardware_type` flag: +```sh +ros2 launch -d openarm_bimanual_moveit_config demo.launch.py hardware_type:=sim +``` + +*It may be necessary to install the nlohmann-json-dev library before building* + +Please note that running multiple instances of the website will cause conflicting signals. Future configurations will allow for multiple instances to run simultaneously. + +## Configuration + +### Hardware Plugin Config + +The hardware plugin is specified in `openarm_description/openarm.ros2_control.xacro` as follows: + +```xml + + + openarm_mujoco_hardware/MujocoHardware + left_ + 1337 + + + +``` + +When using OpenArm in a bimanual configuration, the WebSocket ports default to 1337 for right arm and 1338 for left arm commands. However, in practice commands can be sent and states can be received through any connected port. diff --git a/openarm_mujoco_hardware/include/openarm_mujoco_hardware/openarm_mujoco_hardware.hpp b/openarm_mujoco_hardware/include/openarm_mujoco_hardware/openarm_mujoco_hardware.hpp new file mode 100644 index 0000000..db19104 --- /dev/null +++ b/openarm_mujoco_hardware/include/openarm_mujoco_hardware/openarm_mujoco_hardware.hpp @@ -0,0 +1,123 @@ +// Copyright 2025 Reazon Holdings, Inc. +// +// 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. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "hardware_interface/system_interface.hpp" +#include "hardware_interface/types/hardware_interface_type_values.hpp" + +namespace openarm_mujoco_hardware { + +class WebSocketSession; + +class MujocoHardware : public hardware_interface::SystemInterface { + public: + MujocoHardware() = default; + + 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_cleanup( + const rclcpp_lifecycle::State& /*previous_state*/) override; + hardware_interface::CallbackReturn on_shutdown( + 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::CallbackReturn on_error( + const rclcpp_lifecycle::State& /*previous_state*/) override; + + std::vector export_state_interfaces() + override; + std::vector export_command_interfaces() + 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; + + friend class WebSocketSession; + + private: + static constexpr size_t TOTAL_DOF = + 8; // Total degrees of freedom, including gripper + inline static constexpr std::array KP_ = { + 180.0, 180.0, 140.0, 155.0, 115.0, 115.0, 115.0, 25.0}; + inline static constexpr std::array KD_ = { + 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01}; + static constexpr double MAX_MOTOR_TORQUE = 100.0; + + std::vector qpos_; + std::vector qvel_; + std::vector qtau_; + + std::vector cmd_qpos_; + std::vector cmd_qvel_; + std::vector cmd_qtau_ff_; + + std::mutex state_mutex_; + + void clear_cmd_torque(); + + // websocket connection to mujoco + boost::asio::ip::tcp::endpoint endpoint_; + boost::asio::ip::address address_; + static constexpr double kDefaultWebsocketPort = 1337; + unsigned short websocket_port_; + boost::asio::io_context ioc_{}; + boost::asio::ip::tcp::acceptor acceptor_{ioc_}; + std::thread ioc_thread_; + + std::shared_ptr ws_session_; + void start_accept(); + + const std::string kMuJoCoWebSocketURL_ = + "https://thomasonzhou.github.io/mujoco_anywhere/"; +}; + +class WebSocketSession : public std::enable_shared_from_this { + public: + static std::shared_ptr create( + boost::asio::ip::tcp::socket socket, MujocoHardware* hw); + void run(); + WebSocketSession(boost::asio::ip::tcp::socket socket, MujocoHardware* hw); + + void send_json(const nlohmann::json& j); + + private: + void flush(); + void do_handshake(); + void on_accept(boost::beast::error_code ec); + void do_read(); + void on_read(boost::beast::error_code ec, std::size_t); + + boost::beast::websocket::stream ws_; + boost::beast::flat_buffer buffer_; + MujocoHardware* hw_; + std::deque> send_queue_; + bool write_in_progress_; +}; +}; // namespace openarm_mujoco_hardware diff --git a/openarm_mujoco_hardware/openarm_mujoco_hardware.xml b/openarm_mujoco_hardware/openarm_mujoco_hardware.xml new file mode 100644 index 0000000..cdc3b57 --- /dev/null +++ b/openarm_mujoco_hardware/openarm_mujoco_hardware.xml @@ -0,0 +1,25 @@ + + + + + + ros2_control hardware interface. + + + diff --git a/openarm_mujoco_hardware/package.xml b/openarm_mujoco_hardware/package.xml new file mode 100644 index 0000000..fc27051 --- /dev/null +++ b/openarm_mujoco_hardware/package.xml @@ -0,0 +1,39 @@ + + + + + openarm_mujoco_hardware + 1.0.0 + MuJoCo hardware interface for ros2_control + Thomason Zhou + Apache-2.0 + + ament_cmake + + nlohmann-json-dev + rclcpp + hardware_interface + pluginlib + + ament_lint_auto + ament_lint_common + ros2_control_test_assets + + + ament_cmake + + diff --git a/openarm_mujoco_hardware/src/openarm_mujoco_hardware.cpp b/openarm_mujoco_hardware/src/openarm_mujoco_hardware.cpp new file mode 100644 index 0000000..3fceb4f --- /dev/null +++ b/openarm_mujoco_hardware/src/openarm_mujoco_hardware.cpp @@ -0,0 +1,322 @@ +// Copyright 2025 Reazon Holdings, Inc. +// +// 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 + +namespace openarm_mujoco_hardware { + +hardware_interface::CallbackReturn MujocoHardware::on_init( + const hardware_interface::HardwareInfo& info) { + if (hardware_interface::SystemInterface::on_init(info) != + CallbackReturn::SUCCESS) { + return CallbackReturn::ERROR; + } + + if (info.hardware_parameters.find("websocket_port") != + info.hardware_parameters.end()) { + const double val = std::stoi(info.hardware_parameters.at("websocket_port")); + if (val <= 0) { + return hardware_interface::CallbackReturn::FAILURE; + } + websocket_port_ = val; + } else { + websocket_port_ = kDefaultWebsocketPort; + } + std::cerr << "websocket port: " << websocket_port_ << std::endl; + address_ = boost::asio::ip::make_address("0.0.0.0"); + endpoint_ = boost::asio::ip::tcp::endpoint(address_, websocket_port_); + + // allocate space for joint states + const size_t DOF = info_.joints.size(); + + qpos_.resize(DOF, 0.0); + qvel_.resize(DOF, 0.0); + qtau_.resize(DOF, 0.0); + + cmd_qpos_.resize(DOF, 0.0); + cmd_qvel_.resize(DOF, 0.0); + cmd_qtau_ff_.resize(DOF, 0.0); + + return hardware_interface::CallbackReturn::SUCCESS; +} + +void MujocoHardware::start_accept() { + acceptor_.async_accept([this](boost::beast::error_code ec, + boost::asio::ip::tcp::socket socket) { + if (ec) { + std::cerr << "error accepting connection: " << ec.message() << std::endl; + } + std::cout << "new connection accepted." << std::endl; + ws_session_ = WebSocketSession::create(std::move(socket), this); + ws_session_->run(); + this->start_accept(); + }); +} + +hardware_interface::CallbackReturn MujocoHardware::on_configure( + const rclcpp_lifecycle::State& /*previous_state*/) { + boost::beast::error_code ec; + + if (acceptor_.is_open()) { + return hardware_interface::CallbackReturn::FAILURE; + } else { + acceptor_.open(endpoint_.protocol(), ec); + if (ec) { + throw std::runtime_error("open error: " + ec.message()); + } + } + + acceptor_.set_option(boost::asio::socket_base::reuse_address(true), ec); + if (ec) { + throw std::runtime_error("enable address reuse error: " + ec.message()); + } + + acceptor_.bind(endpoint_, ec); + if (ec) { + throw std::runtime_error("bind error: " + ec.message()); + } + + acceptor_.listen(boost::asio::socket_base::max_listen_connections, ec); + if (ec) { + throw std::runtime_error("listen error: " + ec.message()); + } + + start_accept(); + + ioc_thread_ = std::thread([this]() { + try { + ioc_.run(); + } catch (const std::exception& e) { + std::cerr << "error in io_context thread: " << e.what() << std::endl; + } + }); + + return hardware_interface::CallbackReturn::SUCCESS; +} + +hardware_interface::CallbackReturn MujocoHardware::on_cleanup( + const rclcpp_lifecycle::State& /*previous_state*/) { + return hardware_interface::CallbackReturn::SUCCESS; +} + +hardware_interface::CallbackReturn MujocoHardware::on_shutdown( + const rclcpp_lifecycle::State& /*previous_state*/) { + clear_cmd_torque(); + ioc_.stop(); + if (ioc_thread_.joinable()) ioc_thread_.join(); + return hardware_interface::CallbackReturn::SUCCESS; +} + +hardware_interface::CallbackReturn MujocoHardware::on_activate( + const rclcpp_lifecycle::State& /*previous_state*/) { + return hardware_interface::CallbackReturn::SUCCESS; +} + +void MujocoHardware::clear_cmd_torque() { + nlohmann::json cmd_msg; + nlohmann::json& cmd = cmd_msg["cmd"]; + for (size_t i = 0; i < cmd_qpos_.size(); ++i) { + cmd[info_.joints[i].name] = 0.0; + } + if (ws_session_) { + ws_session_->send_json(cmd_msg); + } +} + +hardware_interface::CallbackReturn MujocoHardware::on_deactivate( + const rclcpp_lifecycle::State& /*previous_state*/) { + clear_cmd_torque(); + return hardware_interface::CallbackReturn::SUCCESS; +} + +hardware_interface::CallbackReturn MujocoHardware::on_error( + const rclcpp_lifecycle::State& /*previous_state*/) { + clear_cmd_torque(); + return hardware_interface::CallbackReturn::SUCCESS; +} +std::vector +MujocoHardware::export_state_interfaces() { + std::vector state_interfaces; + for (size_t i = 0; i < qpos_.size(); ++i) { + state_interfaces.emplace_back(hardware_interface::StateInterface( + info_.joints.at(i).name, hardware_interface::HW_IF_POSITION, + &qpos_[i])); + state_interfaces.emplace_back(hardware_interface::StateInterface( + info_.joints.at(i).name, hardware_interface::HW_IF_VELOCITY, + &qvel_[i])); + // state_interfaces.emplace_back(hardware_interface::StateInterface(info_.joints.at(i).name, + // hardware_interface::HW_IF_EFFORT, &qtau_[i])); + } + return state_interfaces; +} +std::vector +MujocoHardware::export_command_interfaces() { + std::vector command_interfaces; + for (size_t i = 0; i < qpos_.size(); ++i) { + command_interfaces.emplace_back(hardware_interface::CommandInterface( + info_.joints.at(i).name, hardware_interface::HW_IF_POSITION, + &cmd_qpos_[i])); + command_interfaces.emplace_back(hardware_interface::CommandInterface( + info_.joints.at(i).name, hardware_interface::HW_IF_VELOCITY, + &cmd_qvel_[i])); + command_interfaces.emplace_back(hardware_interface::CommandInterface( + info_.joints.at(i).name, hardware_interface::HW_IF_EFFORT, + &cmd_qtau_ff_[i])); + } + return command_interfaces; +} +hardware_interface::return_type MujocoHardware::read( + const rclcpp::Time& /*time*/, const rclcpp::Duration& /*period*/) { + // Read state from the last recieived websocket message + // Why decouple this? The simulation might be paused, and we want to read the + // last state + + // right now this is optional as state is updated by listening to messages + // from MuJoCo + return hardware_interface::return_type::OK; +} +hardware_interface::return_type MujocoHardware::write( + const rclcpp::Time& /*time*/, const rclcpp::Duration& /*period*/) { + // send a websocket message + + nlohmann::json cmd_msg; + nlohmann::json& cmd = cmd_msg["cmd"]; + + for (size_t i = 0; i < cmd_qpos_.size(); ++i) { + const double qpos_error = cmd_qpos_[i] - qpos_[i]; + const double qvel_error = cmd_qvel_[i] - qvel_[i]; + const double qtau_ff = cmd_qtau_ff_[i]; + + double cmd_torque = KP_[i] * qpos_error + KD_[i] * qvel_error + qtau_ff; + + // if (cmd_torque > MAX_MOTOR_TORQUE) { + // cmd_torque = MAX_MOTOR_TORQUE; + // } else if (cmd_torque < -MAX_MOTOR_TORQUE) { + // cmd_torque = -MAX_MOTOR_TORQUE; + // } + cmd[info_.joints[i].name] = cmd_torque; + } + if (ws_session_) { + ws_session_->send_json(cmd_msg); + } else { + std::cerr << "MuJoCo WebSocket session is not active, please connect at " + << kMuJoCoWebSocketURL_ << std::endl; + } + + return hardware_interface::return_type::OK; +} + +WebSocketSession::WebSocketSession(boost::asio::ip::tcp::socket socket, + MujocoHardware* hw) + : ws_(std::move(socket)), hw_(hw), write_in_progress_(false) {} + +std::shared_ptr WebSocketSession::create( + boost::asio::ip::tcp::socket socket, MujocoHardware* hw) { + return std::make_shared(std::move(socket), hw); +} + +void WebSocketSession::send_json(const nlohmann::json& j) { + std::shared_ptr msg = std::make_shared(j.dump()); + boost::asio::post(ws_.get_executor(), [self = shared_from_this(), msg] { + self->send_queue_.push_back(msg); + if (!self->write_in_progress_) { + self->flush(); + } + }); +} + +void WebSocketSession::flush() { + if (send_queue_.empty()) { + write_in_progress_ = false; + return; + } + + write_in_progress_ = true; + auto msg = send_queue_.front(); + send_queue_.pop_front(); + + ws_.async_write(boost::asio::buffer(*msg), + [self = shared_from_this(), msg](boost::beast::error_code ec, + std::size_t) { + if (ec) { + std::cerr << "send error: " << ec.message() << std::endl; + } + self->write_in_progress_ = false; + self->flush(); + }); +} + +void WebSocketSession::run() { do_handshake(); } + +void WebSocketSession::do_handshake() { + ws_.set_option(boost::beast::websocket::stream_base::timeout::suggested( + boost::beast::role_type::server)); + ws_.async_accept(boost::beast::bind_front_handler( + &WebSocketSession::on_accept, shared_from_this())); +} + +void WebSocketSession::on_accept(boost::beast::error_code ec) { + if (ec) { + std::cerr << "handshake failed: " << ec.message() << std::endl; + } + do_read(); +} + +void WebSocketSession::do_read() { + ws_.async_read(buffer_, boost::beast::bind_front_handler( + &WebSocketSession::on_read, shared_from_this())); +} + +void WebSocketSession::on_read(boost::beast::error_code ec, + std::size_t bytes_transferred) { + if (ec) { + std::cerr << "read error: " << ec.message() << std::endl; + return; + } + std::string data = boost::beast::buffers_to_string(buffer_.data()); + { + std::lock_guard(hw_->state_mutex_); + try { + nlohmann::json j = nlohmann::json::parse(data); + // if "state" key exists, update the hardware state + if (j.contains("state")) { + const nlohmann::json& state = j["state"]; + for (size_t i = 0; i < hw_->info_.joints.size(); ++i) { + const auto& joint = hw_->info_.joints[i]; + if (state.contains(joint.name)) { + const nlohmann::json& joint_data = state.at(joint.name); + if (joint_data.contains("qpos")) { + hw_->qpos_[i] = joint_data.at("qpos").get(); + } + if (joint_data.contains("qvel")) { + hw_->qvel_[i] = joint_data.at("qvel").get(); + } + } + } + } + } catch (const nlohmann::json::parse_error& e) { + std::cerr << "json parse error: " << e.what() << std::endl; + } + }; + + buffer_.consume(bytes_transferred); + do_read(); +} + +}; // namespace openarm_mujoco_hardware + +#include "pluginlib/class_list_macros.hpp" + +PLUGINLIB_EXPORT_CLASS(openarm_mujoco_hardware::MujocoHardware, + hardware_interface::SystemInterface) \ No newline at end of file diff --git a/openarm_mujoco_hardware/test/test_load_openarm_mujoco_hardware.cpp b/openarm_mujoco_hardware/test/test_load_openarm_mujoco_hardware.cpp new file mode 100644 index 0000000..0a72a56 --- /dev/null +++ b/openarm_mujoco_hardware/test/test_load_openarm_mujoco_hardware.cpp @@ -0,0 +1,31 @@ +// Copyright 2025 Reazon Holdings, Inc. +// +// 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 + +#include + +#include "hardware_interface/system_interface.hpp" + +static constexpr const char* kPluginName = + "openarm_mujoco_hardware/MujocoHardware"; + +TEST(TestLoadMujocoOpenarmHardware, can_load_plugin) { + pluginlib::ClassLoader loader( + "openarm_mujoco_hardware", "hardware_interface::SystemInterface"); + std::shared_ptr instance; + + ASSERT_NO_THROW(instance = loader.createSharedInstance(kPluginName)); + EXPECT_NE(instance, nullptr); +}