A comprehensive mobile robot platform for ROS 2 Jazzy with differential drive, LIDAR-based SLAM, and autonomous navigation capabilities.
Author: Aman Mishra
MISIS: M00983641
Email: am3971@live.mdx.ac.uk
License: Apache-2.0
ROS Distribution: Jazzy
Simulator: Gazebo Harmonic
Lecturer: Dr. Sameer Kishore
Program: MSc Robotics
University: Middlesex University Dubai
Ballbot is a differential-drive mobile robot designed for educational and research purposes in robotics. The project demonstrates a complete robotics software stack including:
| Component | Dimension | Description |
|---|---|---|
| Chassis | 0.30m × 0.65m × 0.30m | Main body (blue) |
| Drive Wheels | 0.05m radius, 0.04m width | Left and right powered wheels |
| Wheel Separation | 0.54m | Distance between wheel centers |
| Castor Wheel | 0.04m radius | Passive rear support |
| Collector Arms | 0.60m length | Fixed arms extending forward |
| Flaps | 0.25m length | Articulated end-effectors on arms |
| LIDAR | 360° scan, 10m range | Mounted on vertical pole |
map
└── odom
└── base_footprint
└── base_link
├── left_wheel
├── right_wheel
├── castor_wheel
├── left_arm
│ └── left_flap
├── right_arm
│ └── right_flap
├── vertical_pole
│ └── lidar_link
└── imu_link
ballbot_ws/src/
├── ballbot_bringup/ # Top-level launch orchestration
├── ballbot_control/ # ros2_control configuration
├── ballbot_description/ # URDF/Xacro and meshes
├── ballbot_gazebo/ # Gazebo simulation launch
├── ballbot_imu/ # IMU integration (experimental)
├── ballbot_nav2/ # Nav2 configuration and launch
├── ballbot_slam/ # SLAM toolbox configuration
├── ballbot_teleop/ # Teleoperation nodes
└── ros2_assessment_world/ # Assessment environment
# Development tools
sudo apt install -y python3-colcon-common-extensions python3-rosdep python3-vcstool
# Gazebo and ROS-Gazebo integration
sudo apt install -y ros-jazzy-ros-gz ros-jazzy-gz-ros2-control
# Control packages
sudo apt install -y ros-jazzy-ros2-control ros-jazzy-ros2-controllers
# Navigation and SLAM
sudo apt install -y ros-jazzy-navigation2 ros-jazzy-nav2-bringup
sudo apt install -y ros-jazzy-slam-toolbox
# Visualization
sudo apt install -y ros-jazzy-rviz2 ros-jazzy-joint-state-publisher
# Initialize rosdep
sudo rosdep init || true
rosdep update
# Create workspace
mkdir -p ~/ros2/ballbot_ws/src
cd ~/ros2/ballbot_ws/src
# Clone repository (replace with your repository URL)
git clone [https://github.com/mrrox1337/ballbot.git](https://github.com/mrrox1337/ballbot.git) .
# Install package dependencies
cd ~/ros2/ballbot_ws
rosdep install --from-paths src --ignore-src -r -y
# Build workspace
colcon build --symlink-install
# Source workspace
source install/setup.bash
echo "source /opt/ros/jazzy/setup.bash" >> ~/.bashrc
echo "source ~/ros2/ballbot_ws/install/setup.bash" >> ~/.bashrc
The simplest way to run the complete system:
# Terminal 1: Launch simulation with SLAM
ros2 launch ballbot_bringup bringup.launch.py slam:=true teleop:=true
# Drive around to create a map, then save it
ros2 run nav2_map_server map_saver_cli -f ~/my_map
# Terminal 1: Launch simulation with navigation
ros2 launch ballbot_bringup bringup.launch.py navigation:=true
# Terminal 2: After setting 2D Pose Estimate in RViz, run commander
ros2 run ballbot_nav2 simple_commander
Purpose: Defines the robot’s physical structure, visual appearance, and sensor configuration.
Key Files:
urdf/ballbot.urdf.xacro - Main robot description filerviz/rviz.rviz - RViz visualization configurationlaunch/display.launch.py - Standalone visualization launchTopics Published:
/robot_description (std_msgs/String) - URDF as string parameterStandalone Usage:
# View robot in RViz (no simulation)
ros2 launch ballbot_description display.launch.py
Purpose: Integrates the robot with Gazebo Harmonic simulation environment.
Key Files:
launch/ballbot_gazebo.launch.py - Simulation launch fileLaunch Arguments:
| Argument | Default | Description |
|———-|———|————-|
| world_type | empty | World to load: empty or assessment |
| use_sim_time | true | Use simulation clock |
Topics Bridged:
/clock - Simulation time/scan - LIDAR data (bridged separately in SLAM/Nav launches)Standalone Usage:
# Launch with empty world
ros2 launch ballbot_gazebo ballbot_gazebo.launch.py world_type:=empty
# Launch with assessment world
ros2 launch ballbot_gazebo ballbot_gazebo.launch.py world_type:=assessment
Purpose: Configures ros2_control for differential drive and flap position control.
Key Files:
config/ballbot_controllers.yaml - Controller parameterslaunch/ballbot_control.launch.py - Controller spawning launchControllers:
| Controller | Type | Interface |
|————|——|———–|
| joint_state_broadcaster | JointStateBroadcaster | State publishing |
| diff_drive_base_controller | DiffDriveController | Velocity command |
| flap_controller | ForwardCommandController | Position command |
Command Topics:
/diff_drive_base_controller/cmd_vel (geometry_msgs/TwistStamped) - Drive commands/flap_controller/commands (std_msgs/Float64MultiArray) - Flap positionsState Topics:
/joint_states (sensor_msgs/JointState) - All joint states/odom (nav_msgs/Odometry) - Wheel odometryStandalone Usage:
# Launch Gazebo + Controllers
ros2 launch ballbot_control ballbot_control.launch.py world_type:=assessment
Purpose: Provides keyboard-based teleoperation for driving and flap control.
Key Files:
ballbot_teleop/teleop_flap_node.py - Main teleoperation nodeControls:
Moving:
w
a s d
x
q/z : increase/decrease max speeds by 10%
w/x : linear movement (forward/back)
a/d : angular movement (left/right)
s : force stop
Flap Controls:
1 : Reset Flaps (0.0, 0.0)
2 : Open Flaps (-0.5, 0.5)
3 : Close/Grip (-1.57, 1.57)
CTRL-C to quit
Standalone Usage:
# Run teleop node (requires controllers to be running)
ros2 run ballbot_teleop teleop_flap_node
Purpose: Performs simultaneous localization and mapping using slam_toolbox.
Key Files:
config/slam_async_config.yaml - SLAM parameterslaunch/online_async_slam.launch.py - Complete SLAM launchmaps/assessment_map.yaml - Pre-built assessment world mapmaps/assessment_map.pgm - Map image fileSLAM Configuration:
Topics:
/map (nav_msgs/OccupancyGrid) - Generated map/scan (sensor_msgs/LaserScan) - LIDAR inputStandalone Usage:
# Launch complete SLAM system
ros2 launch ballbot_slam online_async_slam.launch.py world_type:=assessment
# Save the map after exploring
ros2 run nav2_map_server map_saver_cli -f ~/ros2/ballbot_ws/src/ballbot_slam/maps/my_map
Purpose: Provides autonomous navigation using the Nav2 stack.
Key Files:
config/ballbot_nav2_params.yaml - Complete Nav2 parameterslaunch/navigation.launch.py - Navigation stack launchballbot_nav2/cmd_vel_relay.py - Twist to TwistStamped converterballbot_nav2/simple_commander.py - Example goal senderNav2 Components:
Robot Footprint:
[[1.0, 0.35], [1.0, -0.35], [-0.27, -0.35], [-0.27, 0.35]]
This accounts for the extended arms and flaps.
Important: After launching navigation, you must set a 2D Pose Estimate in RViz before the robot can navigate. The AMCL localization needs an initial pose to start.
Standalone Usage:
# Terminal 1: Launch navigation
ros2 launch ballbot_nav2 navigation.launch.py
# In RViz: Click "2D Pose Estimate" and set robot's approximate position
# Terminal 2: Send navigation goal (after setting pose estimate)
ros2 run ballbot_nav2 simple_commander
Purpose: Unified launch system for all robot configurations.
Key Files:
launch/bringup.launch.py - Master launch fileLaunch Arguments:
| Argument | Default | Description |
|---|---|---|
world |
assessment |
World to load: assessment or empty |
teleop |
false |
Launch teleoperation in new terminal |
slam |
false |
Enable SLAM mapping |
navigation |
false |
Enable Nav2 navigation stack |
spawn_balls |
false |
Spawn balls in assessment world |
use_sim_time |
true |
Use simulation time |
Usage Examples:
# Basic simulation (just robot in assessment world)
ros2 launch ballbot_bringup bringup.launch.py
# Simulation with balls spawned (Assessment world only)
ros2 launch ballbot_bringup bringup.launch.py spawn_balls:=true
# Simulation with teleoperation
ros2 launch ballbot_bringup bringup.launch.py teleop:=true
# SLAM mapping session
ros2 launch ballbot_bringup bringup.launch.py slam:=true teleop:=true
# Autonomous navigation (remember to set 2D Pose Estimate!)
ros2 launch ballbot_bringup bringup.launch.py navigation:=true
# Empty world for testing
ros2 launch ballbot_bringup bringup.launch.py world:=empty teleop:=true
Important Notes:
slam and navigation are mutually exclusive - don’t enable bothnavigation, manually set 2D Pose Estimate in RViz before sending goalssimple_commander node should be run manually after navigation is readyStatus: ⚠️ Experimental - Not integrated into main system
Purpose: Attempted integration of IMU sensor fusion for improved localization.
This package was developed to emulate a BNO055 absolute orientation sensor and fuse IMU data with wheel odometry for more robust localization. While the sensor simulation works, the full integration with the localization stack was not completed.
What Works:
What’s Not Integrated:
For Future Development:
The ballbot_imu package can serve as a starting point for:
Refer to the node_graphs sub-folder for detailed RQT visualization while teleoperating, mapping, and navigating.
Refer to the tf2_frames sub-folder for detailed robot transform visualization while navigating notice how the robot listens to the /map topic to the /odom and then the pose information is distributed to the rest of the robot body.
The ros2_assessment_world package provides the PDE4430 assessment environment.
Three colored spheres spawn at random locations:
# Via bringup (recommended)
ros2 launch ballbot_bringup bringup.launch.py world:=assessment
# Standalone (world only, no robot)
ros2 launch assessment_world assessment_complete.launch.py
# Terminal 1: Launch basic simulation with balls spawned
ros2 launch ballbot_bringup bringup.launch.py spawn_balls:=true teleop:=true
# Use WASD keys to drive, 1/2/3 to control flaps
# Press Ctrl+C to exit
# Terminal 1: Launch SLAM
ros2 launch ballbot_bringup bringup.launch.py slam:=true teleop:=true
# Drive around the entire environment to build a complete map
# Watch the map build in RViz
# Terminal 2: Save the map when done
ros2 run nav2_map_server map_saver_cli -f ~/ros2/ballbot_ws/src/ballbot_slam/maps/new_map
# Update ballbot_nav2/config/ballbot_nav2_params.yaml with new map path if needed
# Terminal 1: Launch navigation
ros2 launch ballbot_bringup bringup.launch.py navigation:=true
# In RViz:
# 1. Click "2D Pose Estimate" button
# 2. Click and drag on the map where the robot actually is
# 3. Wait for AMCL particle cloud to converge
# Terminal 2: Send a navigation goal
ros2 run ballbot_nav2 simple_commander
# Or use RViz "2D Goal Pose" button to send goals interactively
# After navigation is running and localized:
ros2 action send_goal /navigate_to_pose nav2_msgs/action/NavigateToPose \
"{pose: {header: {frame_id: 'map'}, pose: {position: {x: 2.0, y: 1.0, z: 0.0}, orientation: {w: 1.0}}}}"
The ballbot_nav2 package provides two commander scripts for autonomous navigation using the Nav2 Simple Commander API:
| Script | Purpose |
|---|---|
simple_commander |
Navigate to a single goal position |
waypoint_commander |
Navigate through multiple waypoints in sequence |
Before running either commander, you must:
Step 1: Launch Navigation Stack
ros2 launch ballbot_bringup bringup.launch.py navigation:=true
Step 2: Set Initial Pose in RViz
AMCL needs to know where the robot is before it can navigate:
You’ll know localization is working when the green particle cloud tightens around the robot’s position.
Sends the robot to a single predefined goal position.
Run:
ros2 run ballbot_nav2 simple_commander
Default Goal: x=1.0, y=0.0 (1 meter forward from origin)
File Location: ~/ros2/ballbot_ws/src/ballbot_nav2/ballbot_nav2/simple_commander.py
Sends the robot through a sequence of waypoints.
Run:
ros2 run ballbot_nav2 waypoint_commander
File Location: ~/ros2/ballbot_ws/src/ballbot_nav2/ballbot_nav2/waypoint_commander.py
Open waypoint_commander.py and locate the waypoints list:
waypoints = [
{'x': 1.0, 'y': 0.0, 'yaw': 0.0}, # Waypoint 1
{'x': 2.0, 'y': 1.0, 'yaw': 1.57}, # Waypoint 2
{'x': 0.0, 'y': 0.0, 'yaw': 0.0}, # Waypoint 3: return to start
]
Each waypoint is a dictionary with three values:
| Parameter | Type | Description |
|---|---|---|
x |
float | X coordinate in meters (map frame) |
y |
float | Y coordinate in meters (map frame) |
yaw |
float | Orientation in radians |
Yaw (Orientation) Reference:
| Yaw Value | Direction |
|---|---|
0.0 |
Facing +X (right on map) |
1.57 (π/2) |
Facing +Y (up on map) |
3.14 (π) |
Facing -X (left on map) |
-1.57 (-π/2) |
Facing -Y (down on map) |
Y+
↑
(-4,4) _____|_____ (4,4)
| | | |
| |Pen| |
| | | |
| |
-X ← | (0,0) | → +X
| |
|___________|
(-4,-4) (4,-4)
↓
Y-
Key Locations:
Navigate to Pen Areas:
waypoints = [
{'x': 0.0, 'y': 1.0, 'yaw': 1.57}, # Move forward, face pen
{'x': -0.6, 'y': 3.0, 'yaw': 1.57}, # Go to left pen
{'x': 0.6, 'y': 3.0, 'yaw': 1.57}, # Go to right pen
{'x': 0.0, 'y': 0.0, 'yaw': 0.0}, # Return to center
]
Patrol the Perimeter:
waypoints = [
{'x': 3.0, 'y': 0.0, 'yaw': 1.57}, # Right side
{'x': 3.0, 'y': 3.0, 'yaw': 3.14}, # Top-right corner
{'x': -3.0, 'y': 3.0, 'yaw': -1.57}, # Top-left corner
{'x': -3.0, 'y': -3.0, 'yaw': 0.0}, # Bottom-left corner
{'x': 3.0, 'y': -3.0, 'yaw': 1.57}, # Bottom-right corner
{'x': 0.0, 'y': 0.0, 'yaw': 0.0}, # Return to center
]
If you used --symlink-install during the initial build, Python file changes take effect immediately - no rebuild needed.
Otherwise, rebuild the package:
cd ~/ros2/ballbot_ws
colcon build --symlink-install --packages-select ballbot_nav2
source install/setup.bash
The following are known limitations of the current implementation:
Before running any navigation script (simple_commander or waypoint_commander), the robot’s initial pose must be set manually in RViz using the “2D Pose Estimate” tool. Programmatic initialization of the initial pose has not been implemented yet.
Workaround: Always set the 2D Pose Estimate in RViz and wait for the AMCL particle cloud to converge before running navigation commands.
The robot currently relies solely on wheel odometry for pose estimation. The IMU sensor integration (ballbot_imu package) was attempted but not fully integrated into the localization stack.
Impact: When the robot turns, it calculates its orientation based on how much the wheels have rotated. If the robot collides with an obstacle or experiences wheel slip, the odometry becomes inaccurate. This causes:
Workaround:
Due to the odometry drift issue described above, the waypoint_commander does not work reliably for longer waypoint sequences. As the robot navigates through multiple waypoints, accumulated orientation error causes it to increasingly miss target positions.
Workaround:
simple_commander for single-goal navigation which is more reliableThe robot has no awareness of the colored spheres that spawn in the assessment world. It cannot:
Current capability: The flaps can be controlled manually via teleoperation (keys 1, 2, 3) to push or grip spheres, but all ball collection must be done through manual teleoperation.
Future improvement: This would require:
# Check Gazebo installation
gz sim --version
# Try with OGRE rendering (more compatible)
# This is already set in the launch files for assessment world
ros2 control list_controllers
ros2 topic list | grep cmd_vel
ros2 topic echo /diff_drive_base_controller/cmd_vel
ros2 topic echo /scan --once
ros2 run tf2_tools view_frames
ros2 lifecycle list /map_server
ros2 lifecycle list /amcl
ros2 topic echo /odom
# List all active nodes
ros2 node list
# List all topics
ros2 topic list
# Check topic frequency
ros2 topic hz /scan
# View TF tree
ros2 run tf2_tools view_frames
# Monitor transforms
ros2 run tf2_ros tf2_echo map base_footprint
# Check controller status
ros2 control list_controllers
ros2 control list_hardware_interfaces
--symlink-install for faster iteration:
colcon build --symlink-install --packages-select <package_name>
ament_python build typepackage.xmlsetup.py data_filesThis project is licensed under the Apache License 2.0. See individual package LICENSE files for details.
Last Updated: December 2025
ROS 2 Version: Jazzy Jalisco
Gazebo Version: Harmonic