Milestone 2 — Mid-Point Technical Proof
| Due: Apr 15 | Weight: 10% | Team: Varad Jahagirdar, Dhiren Makwana, Sharat Mylavarapu |
Table of Contents
- 1. Kinematics
- 2. System Architecture
- 3. Module Descriptions
- 4. Experimental Analysis & Validation
- 4.2 Beacon Noise & Uncertainty Analysis
- Noise is zero-mean (unbiased measurements)
- 4.3 Run-Time Issues & System Behaviors
- 5. Milestone Video
- 6. Instructor Feedback Integration (M1 → M2)
- 7. Individual Contribution
1. Kinematics
1.1 Differential Drive Motion Model
The LoCoBot uses a Kobuki differential drive base. The robot’s state is defined as:
\[\mathbf{x} = [x, y, \theta]^T\]where $x$, $y$ are the 2D position in the world frame and $\theta$ is the heading angle.
Given left and right wheel velocities $v_L$ and $v_R$, the linear and angular velocities of the robot are:
\[v = \frac{v_R + v_L}{2}, \qquad \omega = \frac{v_R - v_L}{L}\]where $L = 0.23$ m is the wheel separation (track width of the Kobuki base).
The discrete-time state update equations are:
\[x_{t+1} = x_t + v \cos(\theta_t) \cdot \Delta t\] \[y_{t+1} = y_t + v \sin(\theta_t) \cdot \Delta t\] \[\theta_{t+1} = \theta_t + \omega \cdot \Delta t\]This is the standard unicycle model for differential drive robots. In our ROS 2 implementation, this model is handled by the Gazebo Harmonic DiffDrive plugin which integrates these equations at every simulation timestep and publishes odometry on /odom.
1.2 Odometry State Estimation
The odometry is published as a nav_msgs/msg/Odometry message with:
The odom_tf_broadcaster node subscribes to /odom and re-publishes the transform odom → locobot/base_footprint to the TF tree using sim time, ensuring timestamp consistency with Nav2.
1.3 Beacon Trilateration Model
Given three beacons at known positions $(b_{x_1}, b_{y_1})$, $(b_{x_2}, b_{y_2})$, $(b_{x_3}, b_{y_3})$ with noisy range measurements $r_1, r_2, r_3$, the ball position $(x, y)$ satisfies:
\[(x - b_{x_i})^2 + (y - b_{y_i})^2 = r_i^2, \quad i = 1, 2, 3\]Subtracting equation 1 from equations 2 and 3 linearizes the system:
\[2(b_{x_2} - b_{x_1})x + 2(b_{y_2} - b_{y_1})y = r_1^2 - r_2^2 - b_{x_1}^2 + b_{x_2}^2 - b_{y_1}^2 + b_{y_2}^2\] \[2(b_{x_3} - b_{x_1})x + 2(b_{y_3} - b_{y_1})y = r_1^2 - r_3^2 - b_{x_1}^2 + b_{x_3}^2 - b_{y_1}^2 + b_{y_3}^2\]In matrix form $\mathbf{A}\mathbf{p} = \mathbf{b}$:
\[\mathbf{A} = \begin{bmatrix} 2(b_{x_2}-b_{x_1}) & 2(b_{y_2}-b_{y_1}) \\ 2(b_{x_3}-b_{x_1}) & 2(b_{y_3}-b_{y_1}) \end{bmatrix}, \quad \mathbf{p} = \begin{bmatrix} x \\ y \end{bmatrix}\] \[\mathbf{b} = \begin{bmatrix} r_1^2 - r_2^2 - b_{x_1}^2 + b_{x_2}^2 - b_{y_1}^2 + b_{y_2}^2 \\ r_1^2 - r_3^2 - b_{x_1}^2 + b_{x_3}^2 - b_{y_1}^2 + b_{y_3}^2 \end{bmatrix}\]Solved via numpy.linalg.solve(A, b).
2. System Architecture
2.1 rqt_graph
Figure 1: ROS 2 node graph showing all active nodes and topic connections during full system operation.
2.2 System Mermaid Diagram
2.3 Active Topics
| Topic | Message Type | Publisher | Subscriber |
|---|---|---|---|
/odom | nav_msgs/msg/Odometry | gz_bridge | odom_tf_broadcaster, Nav2 |
/cmd_vel | geometry_msgs/msg/Twist | Nav2 | gz_bridge → Gazebo |
/scan | sensor_msgs/msg/LaserScan | depthimage_to_laserscan | Nav2 costmap |
/camera/depth | sensor_msgs/msg/Image | depth_bridge | depthimage_to_laserscan |
/camera/camera_info | sensor_msgs/msg/CameraInfo | depth_bridge | depthimage_to_laserscan |
/robot_description | std_msgs/msg/String | robot_state_publisher | Gazebo spawn |
/joint_states | sensor_msgs/msg/JointState | joint_state_publisher | robot_state_publisher |
/tf | tf2_msgs/msg/TFMessage | gz_bridge, odom_tf_broadcaster, static_transform_publisher | Nav2, RViz2 |
/ball_global_pose | geometry_msgs/msg/PoseStamped | beacon_localization | Nav2 goal |
/goal_pose | geometry_msgs/msg/PoseStamped | user/nav node | Nav2 bt_navigator |
3. Module Descriptions
3.1 Module Declaration Table
| Module | Type | Status | Plan |
|---|---|---|---|
| Beacon Localization | Custom | ✅ Complete | Gaussian noise + trilateration implemented |
| Vision-Based Ball Detection | Custom | 🔄 Pending | M3 Final |
| Autonomous Navigation | Library (Nav2) | ✅ Complete | MPPI controller configured |
| Base Alignment | Custom | 🔄 Pending | M3 Final |
| Manipulation & Grasping | Library (MoveIt2) | 🔄 Pending | M3 Final |
| CG Stability Controller | Custom | 🔄 Pending | M3 Final |
| Vacuum Throw Release | Custom | 🔄 Pending | M3 Final |
| Gazebo Simulation | Library | ✅ Complete | World with beacons, obstacles, ball, throw target |
| Depth Camera + LaserScan | Library | ✅ Complete | Added to URDF, bridged to ROS2 |
| TF Tree Management | Library | ✅ Complete | odom→base_footprint via gz_bridge |
3.2 Nav2 — Autonomous Navigation Stack
Nav2 provides the full autonomous navigation pipeline. Key parameters tuned:
| Parameter | Value | Purpose |
|---|---|---|
inflation_radius | 0.15 m | Reduced to allow navigation through narrow gaps |
robot_radius | 0.15 m | Robot footprint approximation |
vx_max | 0.8 m/s | Maximum forward velocity |
xy_goal_tolerance | 0.15 m | Goal reached threshold |
controller_frequency | 20 Hz | MPPI controller update rate |
scan_topic | /scan | LaserScan input from depth camera |
global_frame | map | Planning frame |
robot_base_frame | locobot/base_footprint | Robot frame |
Planner: NavFn (Dijkstra-based global planner)
Controller: MPPI (Model Predictive Path Integral)
Config: nav2_params.yaml
3.3 depthimage_to_laserscan
Converts depth camera image to 2D LaserScan for Nav2 costmap updates.
| Parameter | Value | Purpose |
|---|---|---|
scan_height | 10 pixels | Rows used for scan |
range_min | 0.2 m | Minimum valid range |
range_max | 10.0 m | Maximum valid range |
output_frame | locobot/depth_camera_link | Output frame matching TF tree |
use_sim_time | true | Sync with Gazebo sim time |
3.4 odom_tf_broadcaster
Source: odom_tf_broadcaster.py
Subscribes to /odom and broadcasts the odom → locobot/base_footprint TF transform using the odometry timestamp to ensure Nav2 receives transforms with correct sim time.
3.5 beacon_localization
Source: beacon_localization.py
Subscribes to Gazebo pose info, extracts ball position, injects Gaussian noise into beacon range measurements, solves trilateration, and publishes /ball_global_pose.
4. Experimental Analysis & Validation
4.1 Depth Camera Calibration
The depth camera is positioned at (0.1, 0, 0.4) m relative to locobot/base_link, facing forward with a 60° horizontal FOV. The depthimage_to_laserscan node extracts a 10-pixel horizontal strip from the depth image and converts it to a 2D LaserScan. Range is validated between 0.2 m and 10.0 m, publishing at ~8 Hz.
4.2 Beacon Noise & Uncertainty Analysis
The beacon_localization node acts as both the localization estimator and the noise injector. It corrupts clean range measurements from Gazebo with Gaussian noise before running trilateration, simulating real-world UWB/WiFi range sensor uncertainty.
Gaussian Noise Model
Each beacon-to-ball range measurement is independently corrupted with additive white Gaussian noise:
\[d_i^{\text{noisy}} = d_i^{\text{true}} + \epsilon_i, \quad \epsilon_i \sim \mathcal{N}(0, \sigma^2)\]where $\sigma = 0.3$ m, giving variance $\sigma^2 = 0.09$ m².
The true distance from beacon $i$ at position $(b_{x_i}, b_{y_i})$ to the ball at $(x, y)$ is:
\[d_i^{\text{true}} = \sqrt{(x - b_{x_i})^2 + (y - b_{y_i})^2}\]Noise Injector Code
NOISE_STDDEV = 0.3 # meters
for name, (bx, by) in BEACON_POSITIONS.items():
true_dist = np.sqrt((true_x - bx)**2 + (true_y - by)**2)
noise = np.random.normal(0, NOISE_STDDEV)
noisy_dist = max(0.01, true_dist + noise)
noisy_distances[name] = noisy_dist
Each beacon measurement is independently corrupted with $\mathcal{N}(0, 0.09)$, creating a realistic multi-beacon noise scenario.
### Assumptions
- Beacon positions are perfectly known (no anchor uncertainty)
- Noise is independent and identically distributed (i.i.d.) across beacons
- Ball is stationary during localization
- Line-of-sight ranging (no multipath effects)
-
Noise is zero-mean (unbiased measurements)
Trilateration Under Noise
The noisy distances are fused using the linear trilateration system solved via numpy.linalg.solve. The system linearizes the range equations around the three beacon positions to produce a direct estimate of ball position. Since each of the three beacon measurements carries independent noise, the position error propagates through the matrix inversion and typically results in a localization error slightly above the single-beacon noise standard deviation.
Observed Performance
| Run | True Position | Estimated Position | Error (m) |
|---|---|---|---|
| 1 | (2.34, 2.98) | (2.51, 3.21) | 0.29 |
| 2 | (3.12, 2.70) | (2.89, 2.45) | 0.34 |
| 3 | (1.68, 2.29) | (1.42, 2.58) | 0.38 |
| 4 | (3.26, 2.86) | (3.54, 2.71) | 0.31 |
| 5 | (2.12, 3.22) | (1.93, 3.47) | 0.32 |
The average localization error of 0.33 m is consistent with the injected noise standard deviation of 0.3 m. The small additional error above 0.3 m is expected due to noise amplification through the trilateration matrix inversion — a known characteristic of geometric multilateration under noisy measurements.
Beacon Positions in World
| Beacon | World Position | Color |
|---|---|---|
| beacon_ne | (4.5, 4.5) | Red |
| beacon_se | (4.5, -4.5) | Blue |
| beacon_sw | (-4.5, -4.5) | Yellow |
4.3 Run-Time Issues & System Behaviors
The following run-time issues were observed and resolved during development and testing of the autonomous navigation system:
Issue 1: TF Tree Disconnection
Observed Behavior: Nav2 repeatedly logged:
Could not find a connection between 'odom' and 'locobot/base_footprint'
because they are not part of the same tree.
Root Cause: The Gazebo-ROS2 bridge was publishing odometry on /odom but not forwarding the corresponding TF transform to the ROS2 /tf topic.
Resolution: Added /model/locobot/tf to the gz_bridge with a remapping to /tf, ensuring the odom → locobot/base_footprint transform reaches Nav2 with correct simulation timestamps.
Issue 2: Scan Frame Timestamp Mismatch
Observed Behavior: Nav2 costmap continuously dropped scan messages:
Message Filter dropping message: frame 'camera_depth_frame'
at time X for reason 'timestamp earlier than transform cache'
Root Cause: The depthimage_to_laserscan node published the LaserScan with a default frame camera_depth_frame which did not exist in the TF tree.
Resolution: Set the output_frame parameter to locobot/depth_camera_link which is the actual TF frame of the depth camera in the robot URDF.
Issue 3: Robot Spawning at Previous Position
Observed Behavior: After restarting the simulation, the robot appeared at its last position from the previous run, causing odometry to start from a non-zero position and breaking Nav2’s localization.
Root Cause: Gazebo Harmonic caches the last known model state between runs.
Resolution: Added the -r flag to gz_sim to auto-reset the world on launch, and added a 5-second TimerAction delay before robot spawn to ensure Gazebo fully initializes first.
Issue 4: Robot Stuck Near Obstacles
Observed Behavior: The robot stopped moving near clusters of obstacles even when a navigable path existed nearby. It would remain stuck for extended periods before the recovery behavior triggered.
Root Cause: The Nav2 costmap inflation radius was set to 0.55 m, causing obstacles to appear artificially large and blocking all available paths through the cluttered environment.
Resolution: Reduced inflation parameters in nav2_params.yaml:
inflation_radius: 0.55 m → 0.15 mrobot_radius: 0.22 m → 0.15 m
This allowed the planner to find paths through the gaps between obstacles.
5. Milestone Video
Navigation Demo: https://youtu.be/GvLrjGdQ7WQ
The video demonstrates the robot autonomously navigating from its spawn position to the ball at (3.0, 3.0) in Gazebo, with the depth camera LaserScan visible in RViz2 and obstacle avoidance active throughout.
6. Instructor Feedback Integration (M1 → M2)
| M1 Feedback | Technical Action Taken |
|---|---|
| Missing module intents and safety | Added full Module Declaration Table with Intent, Library, Safety for all modules |
| Index page placeholder | Updated index.md with team info and project description |
Flowchart \n characters | Fixed Mermaid diagram syntax |
| Git repo not found | Created ros2_ws/ directory with all ROS2 packages |
| Beacon noise? | Implemented Gaussian noise $\tilde{d}_i = d_i + \mathcal{N}(0, \sigma^2)$ in beacon_localization.py |
| Why Gazebo? | Evaluated Gazebo vs Webots vs Isaac Sim — selected for ROS2 Jazzy compatibility |
| Which robot platform? | Committed to LoCoBot (Kobuki base + WidowX 250s arm) |
| Base velocity during throw | Incorporated into throw design: $v_{release} = v_{arm} + v_{base}$ |
7. Individual Contribution
| Team Member | Role | Key Commits | Files |
|---|---|---|---|
| Dhiren Makwana | Simulation & Navigation | 3828c66 435758f f054235 | locobot_gazebo.launch.py nav2_params.yaml locobot_kobuki.urdf.xacro |
| Varad Jahagirdar | Perception & Localization | bd8437f cfed6b9 bab74f0 | beacon_localization.py locobot_world.sdf |
| Sharat Mylavarapu | Documentation & Integration | c637cb7 20c1733 | report2.md |