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}"
+ )