diff --git a/rosbot_xl_controller/CMakeLists.txt b/rosbot_xl_controller/CMakeLists.txt deleted file mode 100644 index 73a7a395..00000000 --- a/rosbot_xl_controller/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ -cmake_minimum_required(VERSION 3.10.2) -project(rosbot_xl_controller) - -find_package(ament_cmake REQUIRED) - -install(DIRECTORY - launch - config - DESTINATION share/${PROJECT_NAME} -) - -ament_package() diff --git a/rosbot_xl_controller/launch/controller.launch.py b/rosbot_xl_controller/launch/controller.launch.py index 1f075d23..d2a6f01d 100644 --- a/rosbot_xl_controller/launch/controller.launch.py +++ b/rosbot_xl_controller/launch/controller.launch.py @@ -36,7 +36,10 @@ def generate_launch_description(): declare_mecanum_arg = DeclareLaunchArgument( "mecanum", default_value="False", - description="Whether to use mecanum drive controller (otherwise diff drive controller is used)", + description=( + "Whether to use mecanum drive controller" + "(otherwise diff drive controller is used)", + ) ) lidar_model = LaunchConfiguration("lidar_model") diff --git a/rosbot_xl_controller/package.xml b/rosbot_xl_controller/package.xml index 0a2e4cb1..f11e5a0b 100644 --- a/rosbot_xl_controller/package.xml +++ b/rosbot_xl_controller/package.xml @@ -9,14 +9,13 @@ Maciej Stepien Krzysztof Wojciechowski + Jakub Delicat Husarion https://husarion.com/ https://github.com/husarion/rosbot_xl_ros https://github.com/husarion/rosbot_xl_ros/issues - ament_cmake - rosbot_xl_description launch @@ -34,6 +33,6 @@ rosbot_hardware_interfaces - ament_cmake + ament_python diff --git a/rosbot_xl_controller/resource/rosbot_xl_controller b/rosbot_xl_controller/resource/rosbot_xl_controller new file mode 100644 index 00000000..e69de29b diff --git a/rosbot_xl_controller/setup.cfg b/rosbot_xl_controller/setup.cfg new file mode 100644 index 00000000..8878b79a --- /dev/null +++ b/rosbot_xl_controller/setup.cfg @@ -0,0 +1,4 @@ +[develop] +script_dir=$base/lib/rosbot_xl_controller +[install] +install_scripts=$base/lib/rosbot_xl_controller diff --git a/rosbot_xl_controller/setup.py b/rosbot_xl_controller/setup.py new file mode 100644 index 00000000..74d649c6 --- /dev/null +++ b/rosbot_xl_controller/setup.py @@ -0,0 +1,27 @@ +import os +from glob import glob +from setuptools import find_packages, setup + +package_name = "rosbot_xl_controller" + +setup( + name=package_name, + version="0.8.2", + packages=find_packages(exclude=["test"]), + data_files=[ + ("share/ament_index/resource_index/packages", ["resource/" + package_name]), + ("share/" + package_name, ["package.xml"]), + (os.path.join("share", package_name, "launch"), glob("launch/*.launch.py")), + (os.path.join("share", package_name, "config"), glob("config/*.yaml")), + ], + install_requires=["setuptools"], + zip_safe=True, + maintainer="Husarion", + maintainer_email="contact@husarion.com", + description="Hardware configuration for ROSbot XL", + license="Apache License 2.0", + tests_require=["pytest"], + entry_points={ + "console_scripts": [], + }, +) diff --git a/rosbot_xl_controller/test/controllers_test_node.py b/rosbot_xl_controller/test/controllers_test_node.py new file mode 100644 index 00000000..496da1ef --- /dev/null +++ b/rosbot_xl_controller/test/controllers_test_node.py @@ -0,0 +1,97 @@ +# Copyright 2021 Open Source Robotics Foundation, Inc. +# Copyright 2023 Husarion +# +# 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 rclpy + +from threading import Event +from threading import Thread + +from rclpy.node import Node + +from sensor_msgs.msg import JointState, Imu +from nav_msgs.msg import Odometry + + +class ControllersTestNode(Node): + ROSBOT_HARDWARE_PUBLISHERS_RATE = 10.0 + + __test__ = False + + def __init__(self, name="test_node"): + super().__init__(name) + self.joint_state_msg_event = Event() + self.odom_msg_event = Event() + self.imu_msg_event = Event() + + def create_test_subscribers_and_publishers(self): + self.joint_state_sub = self.create_subscription( + JointState, "/joint_states", self.joint_states_callback, 10 + ) + + self.odom_sub = self.create_subscription( + Odometry, "/rosbot_xl_base_controller/odom", self.odometry_callback, 10 + ) + + self.imu_sub = self.create_subscription( + Imu, "/imu_broadcaster/imu", self.imu_callback, 10 + ) + + self.imu_publisher = self.create_publisher(Imu, "_imu/data_raw", 10) + + self.joint_states_publisher = self.create_publisher( + JointState, "_motors_response", 10 + ) + + self.timer = None + + def start_node_thread(self): + self.ros_spin_thread = Thread( + target=lambda node: rclpy.spin(node), args=(self,) + ) + self.ros_spin_thread.start() + + def joint_states_callback(self, data): + self.joint_state_msg_event.set() + + def odometry_callback(self, data): + self.odom_msg_event.set() + + def imu_callback(self, data): + self.imu_msg_event.set() + + def start_publishing_fake_hardware(self): + self.timer = self.create_timer( + 1.0 / self.ROSBOT_HARDWARE_PUBLISHERS_RATE, + self.publish_fake_hardware_messages, + ) + + def publish_fake_hardware_messages(self): + imu_msg = Imu() + imu_msg.header.stamp = self.get_clock().now().to_msg() + imu_msg.header.frame_id = "imu_link" + + joint_state_msg = JointState() + joint_state_msg.header.stamp = self.get_clock().now().to_msg() + joint_state_msg.name = [ + "fl_wheel_joint", + "fr_wheel_joint", + "rl_wheel_joint", + "rr_wheel_joint", + ] + joint_state_msg.position = [0.0, 0.0, 0.0, 0.0] + joint_state_msg.velocity = [0.0, 0.0, 0.0, 0.0] + + self.imu_publisher.publish(imu_msg) + self.joint_states_publisher.publish(joint_state_msg) diff --git a/rosbot_xl_controller/test/test_copyright.py b/rosbot_xl_controller/test/test_copyright.py new file mode 100644 index 00000000..f46f861d --- /dev/null +++ b/rosbot_xl_controller/test/test_copyright.py @@ -0,0 +1,23 @@ +# Copyright 2015 Open Source Robotics Foundation, 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. + +from ament_copyright.main import main +import pytest + + +@pytest.mark.copyright +@pytest.mark.linter +def test_copyright(): + rc = main(argv=[".", "test"]) + assert rc == 0, "Found errors" diff --git a/rosbot_xl_controller/test/test_diff_drive_controllers.py b/rosbot_xl_controller/test/test_diff_drive_controllers.py new file mode 100644 index 00000000..547c60fc --- /dev/null +++ b/rosbot_xl_controller/test/test_diff_drive_controllers.py @@ -0,0 +1,98 @@ +# Copyright 2021 Open Source Robotics Foundation, Inc. +# Copyright 2023 Intel Corporation. All Rights Reserved. +# Copyright 2023 Husarion +# +# 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 launch_pytest +import pytest +import rclpy + +from ament_index_python.packages import get_package_share_directory +from launch import LaunchDescription +from launch.actions import IncludeLaunchDescription +from launch.substitutions import PathJoinSubstitution +from launch.launch_description_sources import PythonLaunchDescriptionSource +from controllers_test_node import ControllersTestNode + + +@launch_pytest.fixture +def generate_test_description(): + rosbot_xl_controller = get_package_share_directory("rosbot_xl_controller") + bringup_launch = IncludeLaunchDescription( + PythonLaunchDescriptionSource( + PathJoinSubstitution( + [ + rosbot_xl_controller, + "launch", + "controller.launch.py", + ] + ) + ), + launch_arguments={ + "use_sim": "False", + "mecanum": "False", + "use_gpu": "False", + }.items(), + ) + + return LaunchDescription([bringup_launch]) + + +@pytest.mark.launch(fixture=generate_test_description) +def test_controllers_startup_fail(): + rclpy.init() + try: + node = ControllersTestNode("test_controllers_bringup") + node.create_test_subscribers_and_publishers() + + node.start_node_thread() + msgs_received_flag = node.joint_state_msg_event.wait(timeout=10.0) + assert ( + not msgs_received_flag + ), "Expected JointStates message not received. Check joint_state_broadcaster!" + msgs_received_flag = node.odom_msg_event.wait(timeout=10.0) + assert ( + not msgs_received_flag + ), "Expected Odom message not received. Check rosbot_base_controller!" + msgs_received_flag = node.imu_msg_event.wait(timeout=10.0) + assert ( + not msgs_received_flag + ), "Expected Imu message not received. Check imu_broadcaster!" + finally: + rclpy.shutdown() + + +@pytest.mark.launch(fixture=generate_test_description) +def test_controllers_startup_success(): + rclpy.init() + try: + node = ControllersTestNode("test_controllers_bringup") + node.create_test_subscribers_and_publishers() + node.start_publishing_fake_hardware() + + node.start_node_thread() + msgs_received_flag = node.joint_state_msg_event.wait(timeout=10.0) + assert ( + msgs_received_flag + ), "Expected JointStates message but it was not received. Check joint_state_broadcaster!" + msgs_received_flag = node.odom_msg_event.wait(timeout=10.0) + assert ( + msgs_received_flag + ), "Expected Odom message but it was not received. Check rosbot_base_controller!" + msgs_received_flag = node.imu_msg_event.wait(timeout=10.0) + assert ( + msgs_received_flag + ), "Expected Imu message but it was not received. Check imu_broadcaster!" + finally: + rclpy.shutdown() diff --git a/rosbot_xl_controller/test/test_flake8.py b/rosbot_xl_controller/test/test_flake8.py new file mode 100644 index 00000000..ee79f31a --- /dev/null +++ b/rosbot_xl_controller/test/test_flake8.py @@ -0,0 +1,25 @@ +# Copyright 2017 Open Source Robotics Foundation, 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. + +from ament_flake8.main import main_with_errors +import pytest + + +@pytest.mark.flake8 +@pytest.mark.linter +def test_flake8(): + rc, errors = main_with_errors(argv=[]) + assert rc == 0, "Found %d code style errors / warnings:\n" % len( + errors + ) + "\n".join(errors) diff --git a/rosbot_xl_controller/test/test_mecanum_controllers.py b/rosbot_xl_controller/test/test_mecanum_controllers.py new file mode 100644 index 00000000..6fc38e98 --- /dev/null +++ b/rosbot_xl_controller/test/test_mecanum_controllers.py @@ -0,0 +1,98 @@ +# Copyright 2021 Open Source Robotics Foundation, Inc. +# Copyright 2023 Intel Corporation. All Rights Reserved. +# Copyright 2023 Husarion +# +# 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 launch_pytest +import pytest +import rclpy + +from ament_index_python.packages import get_package_share_directory +from launch import LaunchDescription +from launch.actions import IncludeLaunchDescription +from launch.substitutions import PathJoinSubstitution +from launch.launch_description_sources import PythonLaunchDescriptionSource +from controllers_test_node import ControllersTestNode + + +@launch_pytest.fixture +def generate_test_description(): + rosbot_xl_controller = get_package_share_directory("rosbot_xl_controller") + bringup_launch = IncludeLaunchDescription( + PythonLaunchDescriptionSource( + PathJoinSubstitution( + [ + rosbot_xl_controller, + "launch", + "controller.launch.py", + ] + ) + ), + launch_arguments={ + "use_sim": "False", + "mecanum": "True", + "use_gpu": "False", + }.items(), + ) + + return LaunchDescription([bringup_launch]) + + +@pytest.mark.launch(fixture=generate_test_description) +def test_controllers_startup_fail(): + rclpy.init() + try: + node = ControllersTestNode("test_controllers_bringup") + node.create_test_subscribers_and_publishers() + + node.start_node_thread() + msgs_received_flag = node.joint_state_msg_event.wait(timeout=10.0) + assert ( + not msgs_received_flag + ), "Expected JointStates message not received. Check joint_state_broadcaster!" + msgs_received_flag = node.odom_msg_event.wait(timeout=10.0) + assert ( + not msgs_received_flag + ), "Expected Odom message not received. Check rosbot_base_controller!" + msgs_received_flag = node.imu_msg_event.wait(timeout=10.0) + assert ( + not msgs_received_flag + ), "Expected Imu message not received. Check imu_broadcaster!" + finally: + rclpy.shutdown() + + +@pytest.mark.launch(fixture=generate_test_description) +def test_controllers_startup_success(): + rclpy.init() + try: + node = ControllersTestNode("test_controllers_bringup") + node.create_test_subscribers_and_publishers() + node.start_publishing_fake_hardware() + + node.start_node_thread() + msgs_received_flag = node.joint_state_msg_event.wait(timeout=10.0) + assert ( + msgs_received_flag + ), "Expected JointStates message but it was not received. Check joint_state_broadcaster!" + msgs_received_flag = node.odom_msg_event.wait(timeout=10.0) + assert ( + msgs_received_flag + ), "Expected Odom message but it was not received. Check rosbot_base_controller!" + msgs_received_flag = node.imu_msg_event.wait(timeout=10.0) + assert ( + msgs_received_flag + ), "Expected Imu message but it was not received. Check imu_broadcaster!" + finally: + rclpy.shutdown() diff --git a/rosbot_xl_controller/test/test_pep257.py b/rosbot_xl_controller/test/test_pep257.py new file mode 100644 index 00000000..a2c3deb8 --- /dev/null +++ b/rosbot_xl_controller/test/test_pep257.py @@ -0,0 +1,23 @@ +# Copyright 2015 Open Source Robotics Foundation, 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. + +from ament_pep257.main import main +import pytest + + +@pytest.mark.linter +@pytest.mark.pep257 +def test_pep257(): + rc = main(argv=[".", "test"]) + assert rc == 0, "Found code style errors / warnings" diff --git a/rosbot_xl_controller/test/test_xacro.py b/rosbot_xl_controller/test/test_xacro.py new file mode 100644 index 00000000..42a9a9c1 --- /dev/null +++ b/rosbot_xl_controller/test/test_xacro.py @@ -0,0 +1,55 @@ +# Copyright 2023 Husarion +# +# 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 os +import xacro +import itertools +from ament_index_python.packages import get_package_share_directory + + +def test_rosbot_description_parsing(): + mecanum_values = ["true", "false"] + use_sim_values = ["true", "false"] + use_gpu_values = ["true", "false"] + simulation_engine_values = ["ignition-gazebo", "webots"] # 'gazebo-classic' + lidar_model = ["slamtec_rplidar_s1", "slamtec_rplidar_a2", "slamtec_rplidar_a3"] + camera_model = ["intel_realsense_d435"] + + all_combinations = list( + itertools.product( + mecanum_values, use_sim_values, use_gpu_values, simulation_engine_values, + lidar_model, camera_model + ) + ) + + for combination in all_combinations: + mecanum, use_sim, use_gpu, simulation_engine, lidar_model, camera_model = combination + mappings = { + "mecanum": mecanum, + "use_sim": use_sim, + "use_gpu": use_gpu, + "simulation_engine": simulation_engine, + "lidar_model": lidar_model, + "camera_model": camera_model + } + rosbot_xl_description = get_package_share_directory("rosbot_xl_description") + xacro_path = os.path.join(rosbot_xl_description, "urdf/rosbot_xl.urdf.xacro") + try: + xacro.process_file(xacro_path, mappings=mappings) + except xacro.XacroException as e: + assert False, ( + f"xacro parsing failed: {str(e)} for mecanum: {mecanum}, " + f"use_sim: {use_sim}, use_gpu: {use_gpu}, simulation_engine: {simulation_engine}, " + f"lidar_model: {lidar_model}, camera_model{camera_model}" + )