TurtleBot State Estimation: From Software to Gazebo Simulation
In the TurtleBot ROS Deployment tutorial, we ran our navigation system using a software simulator node. Now we’ll integrate with Gazebo, replacing our simple simulator with a full physics engine.
Key Insight: Gazebo is not a visualization tool—it’s a ROS node ecosystem!
When you launch Gazebo with a robot model, it automatically creates:
Physics simulation node
Sensor publisher nodes (
/odom,/scan,/imu, etc.)Robot state publisher
Transform (TF) broadcaster
Our task: Integrate our control nodes with Gazebo’s existing nodes.
Architecture Evolution: Software → Gazebo
Software-Only Simulation (previous notebook):
waypoint_generator → /reference
↓
velocity_controller → /cmd_vel
↓
turtlebot_simulator → /odom ← WE IMPLEMENTED THIS
↓
kalman_filter → /estimate
Gazebo Integration (this notebook):
waypoint_generator → /reference
↓
velocity_controller → /cmd_vel
↓
GAZEBO (physics engine) → /odom ← GAZEBO PROVIDES THIS
↓
kalman_filter → /estimate
What Changed:
❌ Remove:
turtlebot_simulatornode✓ Add: Gazebo launch
✓ Unchanged: waypoint generator, controller, Kalman filter
Note
The controller and observer don’t know or care whether /odom comes from our simulator, Gazebo, or a real robot!
Conceptual Foundation: From Dynamical Systems to Gazebo
Theory: Composition of Dynamical Systems
From the theory notebook, we modeled the system as a composition of dynamical systems:
Each block is a DynamicalSystem with state evolution f and observation h functions. The observer (Extended Kalman Filter) corrects odometry drift.
ROS Deployment: Distributed Nodes
From the ROS deployment notebook, we wrapped these systems as ROS nodes:
Each dynamical system became a ROS node communicating via topics. The TurtleBot simulator node provided /odom measurements.
Gazebo Integration: Same Architecture, Physics Engine
Now we replace the software simulator with Gazebo’s physics simulation:
Notice: The Gazebo node (in red) replaces the TurtleBot simulator node. Everything else remains identical—waypoint generator, controller, and Kalman filter don’t know whether /odom comes from our simulator or Gazebo!
This is the power of ROS: Interface-based modularity allows swapping implementations without changing dependent nodes.
Teleoperation with Gazebo
Just as in the ROS deployment notebook, teleoperation provides direct velocity control by publishing commands straight to /cmd_vel:
Architecture Modes:
Autonomous Navigation (this notebook): Waypoint Generator → Velocity Controller →
/cmd_vel→ GazeboTeleoperation: Keyboard Teleop →
/cmd_vel→ Gazebo
When using teleoperation with Gazebo, the teleop node bypasses the waypoint generator and position controller entirely, sending velocity commands directly to Gazebo’s /cmd_vel topic. The Kalman filter continues running to provide state estimates for monitoring.
Note
This tutorial implements the autonomous navigation architecture. For teleoperation with Gazebo, you would simply run a teleop node (e.g., teleop_twist_keyboard) that publishes to /cmd_vel, and the Gazebo simulation would respond exactly as it does to our controller commands!
What Gazebo Provides
When we launch Gazebo with the TurtleBot3 Waffle model:
Input Topics (Gazebo subscribes):
/cmd_vel(Twist) - Velocity commands for the robot
Output Topics (Gazebo publishes):
/odom(Odometry) - Wheel odometry with realistic noise/scan(LaserScan) - 360° LiDAR data/imu(Imu) - Inertial measurement unit/camera/image_raw(Image) - Camera feed/tf(TFMessage) - Coordinate frame transforms
For our state estimation, we only need /cmd_vel (input) and /odom (output).
Physics Simulation Features:
Wheel slip and friction
Robot inertia and dynamics
Realistic sensor noise models
Collision detection
Same code works on real hardware!
Setup: Imports
import numpy as np
import matplotlib.pyplot as plt
from pykal import DynamicalSystem
from pykal.ros.ros_node import ROSNode
from pykal.algorithm_library.estimators.kf import KF
from pykal.gazebo import start_gazebo, stop_gazebo
# ROS message types
from geometry_msgs.msg import Twist, PoseStamped, Vector3
from nav_msgs.msg import Odometry
import rclpy
import time
print("Imports successful! Ready to integrate with Gazebo.")
Component Functions (Reuse from ROS Deployment)
# ============================================================================
# TurtleBot Dynamics (for Kalman filter prediction)
# ============================================================================
def turtlebot_f(xk: np.ndarray, uk: np.ndarray, dt: float) -> np.ndarray:
"""Unicycle kinematics: [x, y, theta, v, omega]."""
x, y, theta, v, omega = xk.flatten()
v_cmd, omega_cmd = uk.flatten()
x_new = x + v * np.cos(theta) * dt
y_new = y + v * np.sin(theta) * dt
theta_new = np.arctan2(np.sin(theta + omega * dt), np.cos(theta + omega * dt))
return np.array([[x_new], [y_new], [theta_new], [v_cmd], [omega_cmd]])
def turtlebot_h(xk: np.ndarray) -> np.ndarray:
"""Measurement: [x, y, theta] from odometry."""
return xk[:3, :]
def compute_F_jacobian(xhat: np.ndarray, dt: float) -> np.ndarray:
"""Jacobian of dynamics."""
_, _, theta, v, _ = xhat.flatten()
return np.array([
[1, 0, -v * np.sin(theta) * dt, np.cos(theta) * dt, 0],
[0, 1, v * np.cos(theta) * dt, np.sin(theta) * dt, 0],
[0, 0, 1, 0, dt],
[0, 0, 0, 1, 0],
[0, 0, 0, 0, 1]
])
def compute_H_jacobian() -> np.ndarray:
"""Jacobian of measurement."""
return np.array([
[1, 0, 0, 0, 0],
[0, 1, 0, 0, 0],
[0, 0, 1, 0, 0]
])
# Noise covariances
Q_turtlebot = np.diag([0.01, 0.01, 0.02, 0.1, 0.1])
R_turtlebot = np.diag([0.05, 0.05, 0.1])
print("TurtleBot dynamics and Jacobians defined!")
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[1], line 5
1 # ============================================================================
2 # TurtleBot Dynamics (for Kalman filter prediction)
3 # ============================================================================
----> 5 def turtlebot_f(xk: np.ndarray, uk: np.ndarray, dt: float) -> np.ndarray:
6 """Unicycle kinematics: [x, y, theta, v, omega]."""
7 x, y, theta, v, omega = xk.flatten()
NameError: name 'np' is not defined
Step 1: Launch Gazebo with TurtleBot3
We use pykal.gazebo.start_gazebo() to launch Gazebo with the TurtleBot3 Waffle model.
This replaces our software simulator node!
# Launch Gazebo
print("Launching Gazebo with TurtleBot3 Waffle...")
print("This may take 10-20 seconds...\n")
gz = start_gazebo(
robot='turtlebot3',
world='empty_world', # Simple empty world for testing
headless=False, # Set True to run without GUI (faster)
x_pose=0.0,
y_pose=0.0,
z_pose=0.01,
yaw=0.0
)
print("✓ Gazebo launched successfully!")
print(f"\nGazebo is now running and publishing:")
print(f" - /odom (Odometry): Wheel odometry with noise")
print(f" - /scan (LaserScan): LiDAR data")
print(f" - /imu (Imu): IMU data")
print(f" - /tf (TF): Coordinate transforms")
print(f"\nGazebo is listening on:")
print(f" - /cmd_vel (Twist): Velocity commands")
# Give Gazebo time to initialize
import time as pytime
pytime.sleep(3)
print("\nGazebo initialization complete!")
Step 2: Node Architecture (Without Simulator)
We create the same control nodes as before, except the simulator:
Changes from software simulation:
❌ Remove
turtlebot_simulatornode (Gazebo replaces it)✓ Keep
waypoint_generator(unchanged)✓ Keep
velocity_controller(unchanged)✓ Keep
kalman_filter(unchanged, subscribes to Gazebo’s/odom)
Power of ROS: Same controller and observer code works with Gazebo!
Node 1: Waypoint Generator (Unchanged)
def create_waypoint_generator_node(waypoints, switch_time=15.0, rate_hz=10.0):
"""Same as before - generates reference poses."""
current_idx = 0
time_at_waypoint = 0.0
last_tk = 0.0
def waypoint_callback(tk):
nonlocal current_idx, time_at_waypoint, last_tk
dt = tk - last_tk if last_tk > 0 else 0.0
last_tk = tk
time_at_waypoint += dt
if time_at_waypoint >= switch_time:
current_idx = (current_idx + 1) % len(waypoints)
time_at_waypoint = 0.0
x_r, y_r, theta_r = waypoints[current_idx]
qx, qy = 0.0, 0.0
qz = np.sin(theta_r / 2.0)
qw = np.cos(theta_r / 2.0)
reference = np.array([tk, x_r, y_r, 0.0, qx, qy, qz, qw])
return {'reference': reference}
node = ROSNode(
node_name='waypoint_generator',
callback=waypoint_callback,
subscribes_to=[],
publishes_to=[('reference', PoseStamped, '/reference')],
rate_hz=rate_hz
)
return node
# Create waypoint generator
square_waypoints = [
(1.0, 0.0, 0.0),
(1.0, 1.0, np.pi/2),
(0.0, 1.0, np.pi),
(0.0, 0.0, -np.pi/2)
]
waypoint_node = create_waypoint_generator_node(
waypoints=square_waypoints,
switch_time=15.0,
rate_hz=10.0
)
print("✓ Waypoint generator node created (unchanged from software version)")
Node 2: Velocity Controller (Unchanged)
def create_velocity_controller_node(Kv=0.3, Komega=1.0, rate_hz=50.0):
"""Same as before - generates velocity commands."""
def controller_callback(tk, reference, estimate):
x_r, y_r = reference[1], reference[2]
qz_r, qw_r = reference[6], reference[7]
theta_r = np.arctan2(2.0 * qw_r * qz_r, 1.0 - 2.0 * qz_r**2)
x, y = estimate[0], estimate[1]
qz, qw = estimate[5], estimate[6]
theta = np.arctan2(2.0 * qw * qz, 1.0 - 2.0 * qz**2)
dx = x_r - x
dy = y_r - y
distance = np.sqrt(dx**2 + dy**2)
heading_error = theta_r - theta
heading_error = np.arctan2(np.sin(heading_error), np.cos(heading_error))
v_cmd = Kv * distance
omega_cmd = Komega * heading_error
v_cmd = np.clip(v_cmd, -0.22, 0.22)
omega_cmd = np.clip(omega_cmd, -2.84, 2.84)
cmd_vel = np.array([v_cmd, 0.0, 0.0, 0.0, 0.0, omega_cmd])
return {'cmd_vel': cmd_vel}
node = ROSNode(
node_name='velocity_controller',
callback=controller_callback,
subscribes_to=[
('/reference', PoseStamped, 'reference'),
('/estimate', Odometry, 'estimate'),
],
publishes_to=[('cmd_vel', Twist, '/cmd_vel')],
rate_hz=rate_hz,
required_topics={'reference', 'estimate'}
)
return node
controller_node = create_velocity_controller_node(Kv=0.3, Komega=1.0, rate_hz=50.0)
print("✓ Velocity controller node created (unchanged from software version)")
print(" → Publishes to /cmd_vel (Gazebo will receive these commands!)")
Node 3: Kalman Filter (Uses Gazebo’s /odom)
The Kalman filter is unchanged algorithmically, but now subscribes to Gazebo’s /odom instead of our simulator’s /odom.
Key Point: The KF doesn’t know the difference! It just processes Odometry messages.
def create_kalman_filter_node(dt=0.1, rate_hz=10.0, Q=None, R=None):
"""Kalman filter using Gazebo's /odom measurements."""
if Q is None:
Q = Q_turtlebot
if R is None:
R = R_turtlebot
xhat = np.array([[0.0], [0.0], [0.0], [0.0], [0.0]])
P = np.diag([0.1, 0.1, 0.1, 1.0, 1.0])
def filter_callback(tk, odom, cmd_vel):
"""
NOTE: Now 'odom' comes from Gazebo, not our simulator!
"""
nonlocal xhat, P
# Extract measurement from Gazebo's Odometry message
x_meas, y_meas = odom[0], odom[1]
qz_meas, qw_meas = odom[5], odom[6]
theta_meas = np.arctan2(2.0 * qw_meas * qz_meas, 1.0 - 2.0 * qz_meas**2)
yk = np.array([[x_meas], [y_meas], [theta_meas]])
uk = np.array([[cmd_vel[0]], [cmd_vel[5]]])
Fk = compute_F_jacobian(xhat, dt)
Hk = compute_H_jacobian()
f_params = {'xk': xhat, 'uk': uk, 'dt': dt}
h_params = {'xk': xhat}
xhat_new, P_new = KF.f(
x_P=(xhat, P),
y=yk,
u=uk,
f=turtlebot_f,
F=lambda **params: Fk,
Q=lambda **params: Q,
h=turtlebot_h,
H=lambda **params: Hk,
R=lambda **params: R,
f_params=f_params,
F_params={},
Q_params={},
h_params=h_params,
H_params={},
R_params={}
)
xhat = xhat_new
P = P_new
x_est, y_est, theta_est, v_est, omega_est = xhat.flatten()
qx, qy = 0.0, 0.0
qz_est = np.sin(theta_est / 2.0)
qw_est = np.cos(theta_est / 2.0)
estimate = np.array([
x_est, y_est, 0.0,
qx, qy, qz_est, qw_est,
v_est * np.cos(theta_est), v_est * np.sin(theta_est), 0.0,
0.0, 0.0, omega_est
])
return {'estimate': estimate}
node = ROSNode(
node_name='kalman_filter',
callback=filter_callback,
subscribes_to=[
('/odom', Odometry, 'odom'), # ← FROM GAZEBO!
('/cmd_vel', Twist, 'cmd_vel'),
],
publishes_to=[('estimate', Odometry, '/estimate')],
rate_hz=rate_hz,
required_topics={'odom', 'cmd_vel'}
)
return node
kf_node = create_kalman_filter_node(dt=0.1, rate_hz=10.0)
print("✓ Kalman filter node created")
print(" → Subscribes to /odom (provided by Gazebo!)")
print(" → Subscribes to /cmd_vel (to predict state)")
print(" → Publishes /estimate")
Step 3: Run the Integrated System
Now we start our nodes and let them interact with Gazebo:
# Initialize ROS2
rclpy.init()
# Create nodes
print("Creating ROS2 nodes...")
waypoint_node.create_node()
controller_node.create_node()
kf_node.create_node()
print("All nodes created!")
# Start nodes
print("\nStarting nodes...")
waypoint_node.start()
print(" ✓ Waypoint generator running")
kf_node.start()
print(" ✓ Kalman filter running (using Gazebo's /odom!)")
controller_node.start()
print(" ✓ Velocity controller running (sending /cmd_vel to Gazebo!)")
print("\n🚀 TurtleBot Gazebo integration is live!")
print("\nSystem Architecture:")
print(" waypoint_generator → /reference")
print(" ↓")
print(" velocity_controller → /cmd_vel → GAZEBO (physics simulation)")
print(" ↓")
print(" /odom (with realistic noise)")
print(" ↓")
print(" kalman_filter → /estimate")
print(" ↓ (feedback)")
print(" velocity_controller")
print("\n💡 Tip: Open Gazebo GUI to watch the TurtleBot move!")
print("💡 Tip: Run 'rqt_graph' to see Gazebo nodes in the ROS graph!")
Data Logger (For Analysis)
# Create data logger
def create_data_logger_node():
data_log = {
'time': [],
'reference': [],
'cmd_vel': [],
'odom': [],
'estimate': []
}
def logger_callback(tk, reference, cmd_vel, odom, estimate):
data_log['time'].append(tk)
data_log['reference'].append(reference.copy())
data_log['cmd_vel'].append(cmd_vel.copy())
data_log['odom'].append(odom.copy())
data_log['estimate'].append(estimate.copy())
return {}
node = ROSNode(
node_name='data_logger',
callback=logger_callback,
subscribes_to=[
('/reference', PoseStamped, 'reference'),
('/cmd_vel', Twist, 'cmd_vel'),
('/odom', Odometry, 'odom'),
('/estimate', Odometry, 'estimate'),
],
publishes_to=[],
rate_hz=10.0
)
return node, data_log
logger_node, data_log = create_data_logger_node()
logger_node.create_node()
logger_node.start()
print("Data logger started!")
# Run for 60 seconds
T_sim = 60.0
print(f"\nRunning Gazebo simulation for {T_sim} seconds...")
print("Watch the TurtleBot in Gazebo GUI!\n")
pytime.sleep(T_sim)
print(f"\nSimulation complete! Collected {len(data_log['time'])} samples.")
Stop the System
# Stop ROS nodes
print("Stopping ROS nodes...")
logger_node.stop()
waypoint_node.stop()
controller_node.stop()
kf_node.stop()
print("All nodes stopped.")
# Shutdown ROS2
rclpy.shutdown()
print("ROS2 shutdown complete.")
# Stop Gazebo
print("\nStopping Gazebo...")
stop_gazebo(gz)
print("Gazebo stopped.")
Visualization: Gazebo vs Software Simulation
Summary: The Complete Pipeline
We’ve completed the full Theory → Software → ROS → Gazebo pipeline!
Step 1: Theory → Software (theory_to_python)
Designed system as
DynamicalSystemcomponentsTested with NumPy arrays in Python
Direct function calls, shared parameter dictionary
Step 2: Software → ROS (python_to_ros)
Wrapped
DynamicalSystemasROSNodeCreated software simulator node
Distributed ROS system with topics
Step 3: ROS → Gazebo (this notebook)
Removed software simulator
Connected to Gazebo’s physics simulation
Same controller and observer code!
Step 4: Gazebo → Hardware (not shown, but trivial!)
Remove Gazebo launch
Connect to real TurtleBot’s
/odomtopicSame controller and observer code!
What Changed at Each Step:
Step |
Changed |
Unchanged |
|---|---|---|
Theory → ROS |
Wrapped as ROSNode |
Dynamics, KF, controller |
ROS → Gazebo |
Removed simulator |
Waypoint, controller, KF |
Gazebo → Hardware |
Launch command |
Waypoint, controller, KF |
ROS Graph Evolution:
Software: our_nodes → turtlebot_simulator → /odom
Gazebo: our_nodes → GAZEBO → /odom
Hardware: our_nodes → ROBOT → /odom
The Beauty of ROS: The controller and observer are completely decoupled from the source of /odom. They work identically with:
Software simulation (Python)
Physics simulation (Gazebo)
Real hardware (TurtleBot3)
This is the essence of modular robotics architecture!
Architecture 2: Teleoperation with Gazebo
In the previous section, we demonstrated autonomous navigation with Gazebo. Now we’ll demonstrate teleoperation mode with Gazebo, where a human operator directly controls the robot using keyboard input.
Key Architectural Difference:
Autonomous (previous): Waypoint Gen → Controller →
/cmd_vel→ Gazebo → ObserverTeleoperation (this section): Keyboard Teleop →
/cmd_vel→ Gazebo → Observer
The beauty of ROS: Gazebo doesn’t care whether /cmd_vel comes from an autonomous controller or a teleop node—it responds identically!
Nodes Used in Teleoperation with Gazebo:
🔴 Keyboard Teleop - External tool (
teleop_twist_keyboard)🔴 Gazebo - Physics simulation (responds to
/cmd_vel)✅ Kalman Filter - Continues providing state estimates
❌ Waypoint Generator - Not needed (bypassed)
❌ Velocity Controller - Not needed (bypassed)
Note
This demonstrates the same teleoperation architecture from the ROS deployment notebook, but with Gazebo’s physics simulation instead of the Python simulator!
# Launch Gazebo (if not already running from previous section)
print("Launching Gazebo with TurtleBot3...")
print("This may take 10-15 seconds...\n")
gz_teleop = start_gazebo(
robot='turtlebot3',
world='empty_world',
headless=False, # Keep GUI for visual feedback during teleop
x_pose=0.0,
y_pose=0.0,
z_pose=0.0,
yaw=0.0
)
print("✓ Gazebo launched successfully!")
print("\nGazebo is publishing:")
print(" /odom (Odometry)")
print("\nGazebo is listening on:")
print(" /cmd_vel (Twist) ← waiting for teleop or controller")
# Give Gazebo time to initialize
import time as pytime
pytime.sleep(3)
# Re-initialize ROS2 if needed
if not rclpy.ok():
rclpy.init()
# Create only Kalman filter node (NO waypoint gen, NO controller)
print("\nCreating Kalman filter for teleoperation mode...")
kf_gazebo_teleop = create_kalman_filter_node(dt=0.1, rate_hz=10.0, Q=Q_turtlebot, R=R_turtlebot)
kf_gazebo_teleop.create_node()
kf_gazebo_teleop.start()
print("✓ Kalman filter running")
print("\n🚀 System ready for teleoperation with Gazebo!")
print("\nActive Architecture:")
print(" (waiting for teleop) → /cmd_vel → GAZEBO → /odom → kalman_filter")
print("\nNOTE: Waypoint generator and controller are NOT running (bypassed!)")
# Create a logger to monitor the system during teleoperation
# (In real usage, you would manually control with keyboard)
def create_gazebo_teleop_logger():
"""Logger for Gazebo teleoperation monitoring."""
gazebo_teleop_data = {
"time": [],
"odom": [],
"estimate": [],
}
def logger_callback(tk, odom, estimate):
gazebo_teleop_data["time"].append(tk)
gazebo_teleop_data["odom"].append(odom.copy())
gazebo_teleop_data["estimate"].append(estimate.copy())
return {}
node = ROSNode(
node_name="gazebo_teleop_logger",
callback=logger_callback,
subscribes_to=[
("/odom", Odometry, "odom"),
("/estimate", Odometry, "estimate"),
],
publishes_to=[],
rate_hz=10.0)
return node, gazebo_teleop_data
# Create and start logger
gazebo_teleop_logger, gazebo_teleop_data = create_gazebo_teleop_logger()
gazebo_teleop_logger.create_node()
gazebo_teleop_logger.start()
print("Logger started!")
print("\n" + "="*60)
print("TELEOPERATION MODE ACTIVE")
print("="*60)
print("\nTo control the robot manually:")
print(" 1. Open a new terminal")
print(" 2. Run: ros2 run teleop_twist_keyboard teleop_twist_keyboard")
print(" 3. Use i/j/k/l keys to drive the TurtleBot in Gazebo")
print("\nThe Kalman filter is running and monitoring your commands!")
print("\nFor this demo, we'll collect 30 seconds of data...")
print("(In real usage, you would be driving the robot manually)")
print("="*60)
# Collect data for demonstration
T_gazebo_teleop = 30.0
print(f"\nCollecting {T_gazebo_teleop} seconds of teleoperation data...")
pytime.sleep(T_gazebo_teleop)
print(f"\nData collection complete! Collected {len(gazebo_teleop_data['time'])} samples.")
Summary: Dual Architectures with Gazebo
This notebook demonstrated two control architectures with Gazebo physics simulation:
Architecture 2: Teleoperation (Manual Control)
Nodes Active:
🔴 Keyboard Teleop (external:
teleop_twist_keyboard)🔴 Gazebo (physics simulation)
✅ Kalman Filter (
pykal.ROSNode)
Nodes Bypassed:
❌ Waypoint Generator
❌ Velocity Controller
Data Flow: Teleop → /cmd_vel → Gazebo → /odom → Observer
Use Case: Manual control with physics simulation for testing/intervention
Key Insights
Gazebo Agnostic: Gazebo doesn’t care whether
/cmd_velcomes from autonomous or teleopSame Interface: Both modes use
/cmd_vel, demonstrating ROS modularityPhysics Preserved: Realistic dynamics, friction, inertia in both modes
Observer Monitoring: KF provides state estimates in both autonomous and manual modes
Red Nodes: Gazebo and teleop shown in RED (external tools, not
pykal.ROSNode)
Architecture Consistency Across Pipeline:
Theory (TurtleBot State Estimation): Pure Python DynamicalSystems
ROS (ROS Deployment): pykal.ROSNode wrappers
Gazebo (this notebook): Same ROS nodes + Gazebo physics
Hardware (next step): Same code, real robot!
The same teleoperation pattern works across software simulation, Gazebo simulation, and hardware deployment—demonstrating true architecture portability!
# Stop nodes
print("Stopping teleoperation nodes...")
gazebo_teleop_logger.stop()
kf_gazebo_teleop.stop()
# Shutdown ROS2
rclpy.shutdown()
print("ROS2 shutdown complete.")
# Stop Gazebo
print("\nStopping Gazebo...")
stop_gazebo(gz_teleop)
print("Gazebo stopped.")
Stopping Teleoperation Mode
Running Keyboard Teleop with Gazebo
In a separate terminal, run the keyboard teleop tool:
ros2 run teleop_twist_keyboard teleop_twist_keyboard
Keyboard Controls:
i- Move forwardk- Stop,- Move backwardj- Turn leftl- Turn right
What Happens:
You press a key →
teleop_twist_keyboardpublishes to/cmd_velGazebo receives
/cmd_vel→ Updates robot physicsGazebo publishes
/odom→ Kalman filter estimates stateYou see the TurtleBot move in the Gazebo GUI!
Tip
Try This:
Open Gazebo GUI (if using
headless=False)Run the teleop command in a terminal
Drive the robot around manually!
Watch the Kalman filter track your movements
For this notebook demo, we’ll create a programmatic logger to demonstrate the architecture (in real usage, you would manually control the robot):
Setting Up Teleoperation with Gazebo
For teleoperation mode, we only need:
Gazebo - Already provides the TurtleBot physics simulation
Kalman Filter - For state estimation and monitoring
Keyboard Teleop - External tool for manual control
We do NOT need the waypoint generator or velocity controller.