diff --git a/README.md b/README.md index 557b4b3..3763a95 100644 --- a/README.md +++ b/README.md @@ -55,21 +55,53 @@ Once the package is built and sourced, you can start a simulation. _Note: You can use `--world_name` flag to indicate other [world](andino_gz/worlds/) to use. (For example: `depot.sdf`(default), `empty.sdf`)_ -If you'd like to work from ROS you can launch the ros bridge by adding the corresponding flag +By default the ros bridge and rviz is initialized. In case you prefer to disable any of those you can do it via its flags: ```sh - ros2 launch andino_gz andino_gz.launch.py ros_bridge:=true + ros2 launch andino_gz andino_gz.launch.py ros_bridge:=false rviz:=false ``` -(Optional) Or launching it separately via: - +To see a complete list of available arguments for the launch file do: ```sh - ros2 launch andino_gz gz_ros_bridge.launch.py + ros2 launch andino_gz andino_gz.launch.py --show-args ``` Make sure to review the required topics using `ign topics` and `ros2 topic` CLI tools. Also, consider using looking at the translation entries under `andino_gz/config/bridge_config.yaml`. +### Multi robot simulation + + This simulation also support multi robot simulation. + + ```sh + ros2 launch andino_gz andino_gz.launch.py robots:=" + andino1={x: 0.0, y: 0.0, z: 0.1, yaw: 0.}; + andino2={x: -0.4, y: 0.1, z: 0.1, yaw: 0.}; + andino3={x: -0.4, y: -0.1, z: 0.1, yaw: 0.}; + andino4={x: -0.8, y: 0.2, z: 0.1, yaw: 0.}; + andino5={x: -0.8, y: -0.2, z: 0.1, yaw: 0.}; + andino6={x: -0.8, y: 0.0, z: 0.1, yaw: 0.};" + ``` + + _Note: You can add as many as you want_ + + + + The launch file is in charge of: + - Start Gazebo simulator with a defined world (See '--world_name' flag) + - Spawn as many robots as commanded. + - Launch ros bridge for each robot. + - Launch rviz visualization for each robot. + + The simulation allows spawn as many robots as you want via the `--robots` flags. + For that you can pass the information of the robots in some sort of YAML format via ROS2 cli: + ```yaml + ={x: 0.0, y: 0.0, yaw: 0.0, roll: 0.0, pitch: 0.0, yaw: 0.0}; + ``` + + Note a ROS Namespace is pushed for each robot so all the topics and nodes are called the same with a difference of a `` prefix. + + ### SLAM @@ -77,7 +109,7 @@ Also, consider using looking at the translation entries under `andino_gz/config/ 1. Run simulation with ros bridge and RViz. ```sh - ros2 launch andino_gz andino_gz.launch.py ros_bridge:=true rviz:=true + ros2 launch andino_gz andino_gz.launch.py ``` 2. Run slam toolbox @@ -93,22 +125,6 @@ Also, consider using looking at the translation entries under `andino_gz/config/ 3. Visualize in RViz: Add `map` panel to RViz and see how the map is being generated. -### Spawn multiple Andinos - -Launch simulation as before: - ```sh - ros2 launch andino_gz andino_gz.launch.py - ``` - -This will spawn only one Andino in the simulation - -For spawning more Andinos you can use the `spawn_robot` launch file. Make sure a different `entity` name is passed as argument as well as initial positions. - ```sh - ros2 launch andino_gz spawn_robot.launch.py entity:=andino_n initial_pose_x:=1 initial_pose_y:=1 - ``` - -_Note: Andino is spawned but no bridge to ROS2 bridge are spawned for those robots._ - ## :raised_hands: Contributing Issues or PRs are always welcome! Please refer to [CONTRIBUTING](CONTRIBUTING.md) doc. diff --git a/andino_gz/config/bridge_config.yaml b/andino_gz/config/bridge_config.yaml index 7519d5d..7f1f3ed 100644 --- a/andino_gz/config/bridge_config.yaml +++ b/andino_gz/config/bridge_config.yaml @@ -1,46 +1,44 @@ -- ros_topic_name: "/clock" - gz_topic_name: "/clock" - ros_type_name: "rosgraph_msgs/msg/Clock" - gz_type_name: "gz.msgs.Clock" - direction: GZ_TO_ROS -- ros_topic_name: "/odom" - gz_topic_name: "/model/andino/odometry" +# Placeholders to be rewritten by launch file: +# - : Should be replaced by entity name: For example "andino", "andino_1", "andino_2". + +- ros_topic_name: "odom" + gz_topic_name: "/model//odometry" ros_type_name: "nav_msgs/msg/Odometry" gz_type_name: "gz.msgs.Odometry" direction: GZ_TO_ROS # odom <-> base_link transform -- ros_topic_name: "/tf" - gz_topic_name: "/model/andino/pose" +- ros_topic_name: "tf" + gz_topic_name: "/model//pose" ros_type_name: "tf2_msgs/msg/TFMessage" gz_type_name: "gz.msgs.Pose_V" direction: GZ_TO_ROS -- ros_topic_name: "/joint_states" - gz_topic_name: "/world/gazebo_world/model/andino/joint_state" +- ros_topic_name: "joint_states" + gz_topic_name: "/world/gazebo_world/model//joint_state" ros_type_name: "sensor_msgs/msg/JointState" gz_type_name: "gz.msgs.Model" direction: GZ_TO_ROS -- ros_topic_name: "/camera_info" - gz_topic_name: "/world/gazebo_world/model/andino/link/base_link/sensor/camera/camera_info" +- ros_topic_name: "camera_info" + gz_topic_name: "/world/gazebo_world/model//link/base_link/sensor/camera/camera_info" ros_type_name: "sensor_msgs/msg/CameraInfo" gz_type_name: "gz.msgs.CameraInfo" direction: GZ_TO_ROS -- ros_topic_name: "/image_raw" - gz_topic_name: "/world/gazebo_world/model/andino/link/base_link/sensor/camera/image" +- ros_topic_name: "image_raw" + gz_topic_name: "/world/gazebo_world/model//link/base_link/sensor/camera/image" ros_type_name: "sensor_msgs/msg/Image" gz_type_name: "gz.msgs.Image" direction: GZ_TO_ROS -- ros_topic_name: "/scan" - gz_topic_name: "/world/gazebo_world/model/andino/link/base_link/sensor/sensor_ray_front/scan" +- ros_topic_name: "scan" + gz_topic_name: "/world/gazebo_world/model//link/base_link/sensor/sensor_ray_front/scan" ros_type_name: "sensor_msgs/msg/LaserScan" gz_type_name: "gz.msgs.LaserScan" direction: GZ_TO_ROS -- ros_topic_name: "/scan/points" - gz_topic_name: "/world/gazebo_world/model/andino/link/base_link/sensor/sensor_ray_front/scan/points" +- ros_topic_name: "scan/points" + gz_topic_name: "/world/gazebo_world/model//link/base_link/sensor/sensor_ray_front/scan/points" ros_type_name: "sensor_msgs/msg/PointCloud2" gz_type_name: "gz.msgs.PointCloudPacked" direction: GZ_TO_ROS -- ros_topic_name: "/cmd_vel" - gz_topic_name: "/model/andino/cmd_vel" +- ros_topic_name: "cmd_vel" + gz_topic_name: "/model//cmd_vel" ros_type_name: "geometry_msgs/msg/Twist" gz_type_name: "gz.msgs.Twist" direction: ROS_TO_GZ diff --git a/andino_gz/launch/andino_gz.launch.py b/andino_gz/launch/andino_gz.launch.py index 8c44464..e6b77b9 100644 --- a/andino_gz/launch/andino_gz.launch.py +++ b/andino_gz/launch/andino_gz.launch.py @@ -3,75 +3,131 @@ import os from ament_index_python.packages import get_package_share_directory from launch import LaunchDescription -from launch.actions import DeclareLaunchArgument, ExecuteProcess, IncludeLaunchDescription +from launch.actions import DeclareLaunchArgument, GroupAction, IncludeLaunchDescription, LogInfo from launch.conditions import IfCondition from launch.launch_description_sources import PythonLaunchDescriptionSource -from launch.substitutions import LaunchConfiguration, PathJoinSubstitution -from launch_ros.actions import Node -import xacro +from launch.substitutions import LaunchConfiguration, PathJoinSubstitution, PythonExpression, TextSubstitution +from launch_ros.actions import Node, PushRosNamespace + +from nav2_common.launch import ParseMultiRobotPose def generate_launch_description(): pkg_andino_gz = get_package_share_directory('andino_gz') ros_bridge_arg = DeclareLaunchArgument( - 'ros_bridge', default_value='false', description = 'Run ROS bridge node.') - rviz_arg = DeclareLaunchArgument('rviz', default_value='false', description='Start RViz.') - world_name_arg = DeclareLaunchArgument('world_name', default_value='depot.sdf', description='Name of the world to load.') + 'ros_bridge', default_value='true', description='Run ROS bridge node.') + rviz_arg = DeclareLaunchArgument('rviz', default_value='true', description='Start RViz.') + world_name_arg = DeclareLaunchArgument( + 'world_name', default_value='depot.sdf', description='Name of the world to load.') + robots_arg = DeclareLaunchArgument( + 'robots', default_value="andino={x: 0., y: 0., z: 0.1, yaw: 0.};", + description='Robots to spawn, multiple robots can be stated separated by a ; ') + # Variables of launch file. + rviz = LaunchConfiguration('rviz') + ros_bridge = LaunchConfiguration('ros_bridge') world_name = LaunchConfiguration('world_name') - world_path = PathJoinSubstitution([pkg_andino_gz, 'worlds', world_name]) - # Gazebo Sim - gazebo = IncludeLaunchDescription( - PythonLaunchDescriptionSource( - os.path.join(get_package_share_directory('ros_gz_sim'), 'launch', 'gz_sim.launch.py') - ), - launch_arguments={'gz_args': world_path}.items(), - ) - - # Spawn the robot and the Robot State Publisher node. - spawn_robot_and_rsp = IncludeLaunchDescription( - PythonLaunchDescriptionSource( - os.path.join(pkg_andino_gz, 'launch', 'spawn_robot.launch.py') - ), - launch_arguments={ - 'entity': 'andino', - 'initial_pose_x': '0', - 'initial_pose_y': '0', - 'initial_pose_z': '0.1', - 'initial_pose_yaw': '0', - 'robot_description_topic': '/robot_description', - 'use_sim_time': 'true', - }.items(), - ) + # Obtains world path. + world_path = PathJoinSubstitution([pkg_andino_gz, 'worlds', world_name]) - # RViz - rviz = Node( - package='rviz2', - executable='rviz2', - arguments=['-d', os.path.join(pkg_andino_gz, 'rviz', 'andino_gz.rviz')], - parameters=[{'use_sim_time': True}], - condition=IfCondition(LaunchConfiguration('rviz')) + base_group = GroupAction( + scoped=True, forwarding=False, + launch_configurations={ + 'ros_bridge': ros_bridge, + 'world_name': world_name + }, + actions=[ + # Gazebo Sim + IncludeLaunchDescription( + PythonLaunchDescriptionSource( + os.path.join(get_package_share_directory('ros_gz_sim'), 'launch', 'gz_sim.launch.py') + ), + launch_arguments={'gz_args': world_path}.items(), + ), + # ROS Bridge for generic Gazebo stuff + Node( + package='ros_gz_bridge', + executable='parameter_bridge', + arguments=['/clock@rosgraph_msgs/msg/Clock[ignition.msgs.Clock'], + output='screen', + namespace='andino_gz_sim', + condition=IfCondition(ros_bridge), + ), + ] ) - # Run ros_gz bridge - ros_bridge = IncludeLaunchDescription( - PythonLaunchDescriptionSource( - os.path.join(pkg_andino_gz, 'launch', 'gz_ros_bridge.launch.py') - ), - condition=IfCondition(LaunchConfiguration('ros_bridge')) - ) + robots_list = ParseMultiRobotPose('robots').value() + # When no robots are specified, spawn a single robot at the origin. + # The default value isn't getting parsed correctly, so we need to check for an empty dictionary. + if (robots_list == {}): + robots_list = {"andino": {"x": 0., "y": 0., "z": 0.1, "yaw": 0.}} + log_number_robots = LogInfo(msg="Robots to spawn: " + str(robots_list)) + spawn_robots_group = [] + for robot_name in robots_list: + init_pose = robots_list[robot_name] + # As it is scoped and not forwarding, the launch configuration in this context gets cleared. + group = GroupAction( + scoped=True, forwarding=False, + launch_configurations={ + 'rviz': rviz, + 'ros_bridge': ros_bridge + }, + actions=[ + LogInfo(msg="Group for robot: " + robot_name), + PushRosNamespace( + condition=IfCondition( + PythonExpression([TextSubstitution(text=str(len(robots_list.keys()))), ' > 1'])), + namespace=robot_name), + # Spawn the robot and the Robot State Publisher node. + IncludeLaunchDescription( + PythonLaunchDescriptionSource( + os.path.join(pkg_andino_gz, 'launch', 'include', 'spawn_robot.launch.py') + ), + launch_arguments={ + 'entity': robot_name, + 'initial_pose_x': str(init_pose['x']), + 'initial_pose_y': str(init_pose['y']), + 'initial_pose_z': str(init_pose['z']), + 'initial_pose_yaw': str(init_pose['yaw']), + 'robot_description_topic': 'robot_description', + 'use_sim_time': 'true', + }.items(), + ), + # RViz + Node( + condition=IfCondition(rviz), + package='rviz2', + executable='rviz2', + arguments=['-d', os.path.join(pkg_andino_gz, 'rviz', 'andino_gz.rviz')], + parameters=[{'use_sim_time': True}], + remappings=[ + ('/tf', 'tf'), + ('/tf_static', 'tf_static'), + ], + ), + # Run ros_gz bridge + IncludeLaunchDescription( + PythonLaunchDescriptionSource( + os.path.join(pkg_andino_gz, 'launch', 'include', 'gz_ros_bridge.launch.py') + ), + launch_arguments={ + 'entity': robot_name, + }.items(), + condition=IfCondition(LaunchConfiguration('ros_bridge')), + ) + ] + ) + spawn_robots_group.append(group) - return LaunchDescription( - [ - # Arguments and Nodes - ros_bridge_arg, - rviz_arg, - world_name_arg, - gazebo, - ros_bridge, - spawn_robot_and_rsp, - rviz, - ] - ) + ld = LaunchDescription() + ld.add_action(log_number_robots) + ld.add_action(ros_bridge_arg) + ld.add_action(rviz_arg) + ld.add_action(world_name_arg) + ld.add_action(robots_arg) + ld.add_action(base_group) + for group in spawn_robots_group: + ld.add_action(group) + return ld diff --git a/andino_gz/launch/gz_ros_bridge.launch.py b/andino_gz/launch/gz_ros_bridge.launch.py deleted file mode 100644 index 01aa0f5..0000000 --- a/andino_gz/launch/gz_ros_bridge.launch.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 - -import os -from ament_index_python.packages import get_package_share_directory -from launch import LaunchDescription -from launch.actions import ExecuteProcess - -def generate_launch_description(): - pkg_andino_gz = get_package_share_directory('andino_gz') - bridge_config_file_path = os.path.join(pkg_andino_gz, 'config', 'bridge_config.yaml') - - bridge_process = ExecuteProcess( - cmd=[ - 'ros2', - 'run', - 'ros_gz_bridge', - 'parameter_bridge', - '--ros-args', - '-p', - f'config_file:={bridge_config_file_path}' - ], - shell=True, - output='screen', - ) - - return LaunchDescription([bridge_process]) diff --git a/andino_gz/launch/include/gz_ros_bridge.launch.py b/andino_gz/launch/include/gz_ros_bridge.launch.py new file mode 100644 index 0000000..97fa3ae --- /dev/null +++ b/andino_gz/launch/include/gz_ros_bridge.launch.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 + +import os +from ament_index_python.packages import get_package_share_directory +from launch import LaunchDescription +from launch.actions import DeclareLaunchArgument +from launch_ros.actions import Node +from launch.substitutions import LaunchConfiguration + +from nav2_common.launch import ReplaceString + + +def generate_launch_description(): + pkg_andino_gz = get_package_share_directory('andino_gz') + bridge_config_file_path = os.path.join(pkg_andino_gz, 'config', 'bridge_config.yaml') + + entity_arg = DeclareLaunchArgument( + 'entity', default_value='andino', description='Name of the entity to bridge with Gazebo.') + + # A placeholder is used in the bridge config file to be replaced by the entity name. + bridge_config = ReplaceString( + source_file=bridge_config_file_path, + replacements={'': LaunchConfiguration('entity')}, + ) + + bridge_node = Node( + package='ros_gz_bridge', + executable='parameter_bridge', + output='screen', + parameters=[{ + 'config_file': bridge_config + }], + ) + + return LaunchDescription([ + entity_arg, + bridge_node, + ]) diff --git a/andino_gz/launch/spawn_robot.launch.py b/andino_gz/launch/include/spawn_robot.launch.py similarity index 96% rename from andino_gz/launch/spawn_robot.launch.py rename to andino_gz/launch/include/spawn_robot.launch.py index fc52081..01b7829 100644 --- a/andino_gz/launch/spawn_robot.launch.py +++ b/andino_gz/launch/include/spawn_robot.launch.py @@ -5,15 +5,14 @@ from ament_index_python.packages import get_package_share_directory from launch import LaunchDescription from launch.actions import DeclareLaunchArgument -from launch.conditions import IfCondition from launch.substitutions import LaunchConfiguration -from launch.substitutions import PythonExpression from launch_ros.actions import Node from xacro import process_file PKG_ANDINO_DESCRIPTION = get_package_share_directory('andino_description') PKG_ANDINO_GZ = get_package_share_directory('andino_gz') + def get_robot_description() -> str: """ Obtain the URDF from the xacro file. @@ -78,7 +77,7 @@ def generate_launch_description(): ) robot_desc_argument = DeclareLaunchArgument( 'robot_description_topic', - default_value='/robot_description', + default_value='robot_description', description='Robot description topic.', ) rsp_frequency_argument = DeclareLaunchArgument( @@ -105,6 +104,10 @@ def generate_launch_description(): 'robot_description': get_robot_description(), } ], + remappings=[ + ('/tf', 'tf'), + ('/tf_static', 'tf_static'), + ], ) # Spawn the robot model. diff --git a/andino_gz/package.xml b/andino_gz/package.xml index e7d0663..409dabe 100644 --- a/andino_gz/package.xml +++ b/andino_gz/package.xml @@ -20,6 +20,7 @@ ros_gz_sim ros2launch + nav2_common rviz2 xacro diff --git a/andino_gz/rviz/andino_gz.rviz b/andino_gz/rviz/andino_gz.rviz index 9181d77..dbce4ae 100644 --- a/andino_gz/rviz/andino_gz.rviz +++ b/andino_gz/rviz/andino_gz.rviz @@ -9,7 +9,7 @@ Panels: - /RobotModel1 - /RobotModel1/Description Topic1 - /TF1 - - /LaserScan1 + - /Image1 Splitter Ratio: 0.5 Tree Height: 2329 - Class: rviz_common/Selection @@ -61,7 +61,7 @@ Visualization Manager: Durability Policy: Volatile History Policy: Keep Last Reliability Policy: Reliable - Value: /robot_description + Value: robot_description Enabled: true Links: All Links Enabled: true @@ -228,10 +228,24 @@ Visualization Manager: Filter size: 10 History Policy: Keep Last Reliability Policy: Reliable - Value: /scan + Value: scan Use Fixed Frame: true Use rainbow: true Value: true + - Class: rviz_default_plugins/Image + Enabled: true + Max Value: 1 + Median window: 5 + Min Value: 0 + Name: Image + Normalize Range: true + Topic: + Depth: 5 + Durability Policy: Volatile + History Policy: Keep Last + Reliability Policy: Reliable + Value: image_raw + Value: true Enabled: true Global Options: Background Color: 48; 48; 48 @@ -296,15 +310,17 @@ Visualization Manager: Pitch: 0.35039839148521423 Target Frame: Value: Orbit (rviz) - Yaw: 1.0153923034667969 + Yaw: 1.0203922986984253 Saved: ~ Window Geometry: Displays: - collapsed: false - Height: 2806 - Hide Left Dock: false + collapsed: true + Height: 1113 + Hide Left Dock: true Hide Right Dock: false - QMainWindow State: 000000ff00000000fd0000000400000000000001da00000a07fc0200000008fb0000001200530065006c0065006300740069006f006e00000001e10000009b000000ab00fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c006100790073010000006900000a070000017800fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261000000010000014d00000a07fc0200000003fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000000a00560069006500770073010000006900000a070000012300fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e100000197000000030000137c00000051fc0100000002fb0000000800540069006d006501000000000000137c0000046000fffffffb0000000800540069006d006501000000000000045000000000000000000000103d00000a0700000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000 + Image: + collapsed: false + QMainWindow State: 000000ff00000000fd0000000400000000000002eb00000a07fc020000000afb0000001200530065006c0065006300740069006f006e00000001e10000009b000000ab00fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c006100790073000000006900000a070000017800fffffffb0000000a0056006900650077007300000009410000012f0000012300fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261fb0000000c00430061006d006500720061010000028000000153000000000000000000000001000001ca0000036afc0200000003fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fc000000690000036a0000004500fffffffa000000000100000002fb0000000a0049006d0061006700650100000000ffffffff0000008c00fffffffb0000000a0049006d0061006700650000000000ffffffff0000000000000000fb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e10000019700000003000005fc00000051fc0100000002fb0000000800540069006d00650100000000000005fc0000046000fffffffb0000000800540069006d00650100000000000004500000000000000000000004260000036a00000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000 Selection: collapsed: false Time: @@ -313,6 +329,6 @@ Window Geometry: collapsed: false Views: collapsed: false - Width: 4988 - X: 132 - Y: 0 + Width: 1532 + X: 2945 + Y: 441 diff --git a/andino_gz/urdf/andino_gz.urdf.xacro b/andino_gz/urdf/andino_gz.urdf.xacro index e91c712..9ca2dea 100644 --- a/andino_gz/urdf/andino_gz.urdf.xacro +++ b/andino_gz/urdf/andino_gz.urdf.xacro @@ -80,6 +80,7 @@ + camera_link 1.047 diff --git a/andino_gz/worlds/depot.sdf b/andino_gz/worlds/depot.sdf index 9796086..dfd4719 100644 --- a/andino_gz/worlds/depot.sdf +++ b/andino_gz/worlds/depot.sdf @@ -34,7 +34,7 @@ scene 0.4 0.4 0.4 0.8 0.8 0.8 - -0.7 0.7 0.7 0 0.3 -0.3 + -1.7 0.7 0.7 0 0.3 -0.3 diff --git a/docker/requirements.txt b/docker/requirements.txt index 5594dda..2eb6eed 100644 --- a/docker/requirements.txt +++ b/docker/requirements.txt @@ -20,6 +20,8 @@ python3 python3-pip python3-setuptools ros-humble-andino-description +ros-humble-andino-slam +ros-humble-nav2-common software-properties-common sudo tmux diff --git a/docs/media/andino_gz_multi_robot.png b/docs/media/andino_gz_multi_robot.png new file mode 100644 index 0000000..2d3165e Binary files /dev/null and b/docs/media/andino_gz_multi_robot.png differ