Objective and use case
What you’ll build: A ROS 2 Humble stack on a Raspberry Pi 4 Model B that fuses GPS, IMU, and wheel odometry, then drives simple GPS waypoint navigation via Nav2 using the ros2-gps-waypoint-nav package.
Why it matters / Use cases
- Outdoor patrol robot prototype: A small UGV that autonomously follows a GPS waypoint loop around a building or field (e.g., 8–12 waypoints over a 200–500 m path), logging pose at 5–10 Hz for later route‑tracking analysis.
- Agricultural row‑following demo: Predefine GPS waypoints along crop rows, then measure repeatability over multiple runs (e.g., <1.5–2.0 m cross‑track error at 1–1.5 m/s with 5 Hz GPS updates).
- Campus delivery path demo: Configure an A→B→C waypoint route under open sky and have the UGV repeat it several times while logging GPS/IMU; target <10% increase in lap time variance and >90% waypoint‑hit rate.
- Field testing of sensor fusion: Use
robot_localizationto fuse GPS+IMU+odometry and compare to raw GPS, aiming for smoother velocity estimates (e.g., 30–50% reduction in pose jitter during GPS multipath). - Teaching reference platform: A reproducible ROS 2 Humble configuration on Raspberry Pi 4 that students can extend with local planners, obstacle detection, or higher‑level autonomy nodes.
Expected outcome
- Robot runs Nav2 waypoint navigation at 5–10 Hz control loop with end‑to‑end planning latency <100 ms on a Raspberry Pi 4 (CPU load <60%, GPU largely idle at <10%).
- Fused pose from
robot_localizationremains stable during short GPS dropouts (1–3 s) using IMU+wheel odometry, with heading drift <3–5° over 10 s. - Logging of GPS, IMU, and fused odometry to rosbag for offline analysis, enabling quantitative evaluation of path tracking error (e.g., RMS position error <2 m for open‑sky runs).
- Reusable launch files and parameter sets so a new Raspberry Pi 4 + UGV can be brought from boot to waypoint navigation in <30 min of setup.
Audience: Robotics hobbyists, educators, and early‑stage prototypers; Level: Intermediate ROS 2 users familiar with topics, TF, and basic navigation concepts.
Architecture/flow: Sensor drivers publish GPS and IMU data → wheel encoders publish odometry → robot_localization fuses these into a continuous odom/map frame → Nav2 consumes the fused pose plus a GPS‑anchored global costmap → ros2-gps-waypoint-nav feeds Nav2 a GPS‑based waypoint list → Nav2 planners generate velocity commands sent to the UGV base controller.
Prerequisites
- Hardware
- Raspberry Pi 4 Model B (2 GB or more).
- MicroSD card (32 GB, Class 10 or better).
- UGV base with differential drive motors and driver (e.g., generic motor driver H‑bridge controlled by Pi GPIO or external MCU providing wheel odometry).
- Stable 5 V power supply for Raspberry Pi.
-
Outdoor/open‑sky test area for GPS.
-
Software
- Ubuntu 22.04 64‑bit (aarch64) installed on the Raspberry Pi 4.
-
Network access for
aptand possibly SSH. -
Skills
- Comfortable using the Linux command line (no GUI used).
- Basic understanding of ROS 2 concepts: nodes, topics, frames, launch files.
Materials
Use exactly this device model for sensors:
- Compute & OS
-
Raspberry Pi 4 Model B, running Ubuntu 22.04.3 LTS 64‑bit (aarch64).
-
Sensors
- Adafruit Ultimate GPS HAT (MTK3339) for GPS.
-
Waveshare 10‑DOF IMU Breakout (MPU9250 + BMP280) for IMU and pressure.
-
Other parts
- MicroSD card (32+ GB).
- Pin headers, jumper wires (female‑female).
- Small breadboard (optional, for IMU breakout).
- Motor driver (not specified in detail; you may use your own).
Setup / Connection
1. Base Raspberry Pi and ROS 2 Humble installation
On Ubuntu 22.04 (aarch64) on the Pi:
sudo apt update
sudo apt install -y \
software-properties-common \
curl gnupg lsb-release
sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key \
-o /usr/share/keyrings/ros-archive-keyring.gpg
echo "deb [arch=arm64 signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] \
http://packages.ros.org/ros2/ubuntu $(lsb_release -sc) main" | \
sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null
sudo apt update
# ROS 2 Humble desktop and required stacks
sudo apt install -y \
ros-humble-desktop \
ros-humble-ros2-control \
ros-humble-diff-drive-controller \
ros-humble-robot-localization \
ros-humble-slam-toolbox \
'ros-humble-nav2*' \
ros-humble-rviz2
# Environment setup (bash)
echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc
source ~/.bashrc
Create the colcon workspace:
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws
colcon build
echo "source ~/ros2_ws/install/setup.bash" >> ~/.bashrc
source ~/.bashrc
2. Hardware connections
2.1 Adafruit Ultimate GPS HAT (MTK3339) to Raspberry Pi 4
The GPS HAT stacks directly on the Raspberry Pi 40‑pin header.
- Solder headers (if needed) onto the GPS HAT and Pi.
- Plug the GPS HAT onto the Pi’s 40‑pin GPIO header, aligning pin 1 (corner with 3V3).
The GPS HAT uses:
- UART:
/dev/serial0(mapped to Pi’s UART GPIO pins). - PPS pin on a GPIO (used for time accuracy; optional for this basic project).
Enable UART:
sudo nano /boot/firmware/config.txt
Add:
enable_uart=1
dtoverlay=pi3-miniuart-bt
Save and reboot:
sudo reboot
Validate the serial port:
ls -l /dev/serial*
# Expect: /dev/serial0 -> /dev/ttyAMA0 (or similar)
2.2 Waveshare 10‑DOF IMU Breakout (MPU9250 + BMP280) to Raspberry Pi 4 (I²C)
Enable I²C:
sudo nano /boot/firmware/config.txt
Add:
dtparam=i2c_arm=on
Save and reboot:
sudo reboot
Wire IMU breakout to Raspberry Pi (GPIO header):
| IMU pin | Raspberry Pi pin | Function |
|---|---|---|
| VCC | Pin 1 (3V3) | 3.3 V power |
| GND | Pin 6 (GND) | Ground |
| SDA | Pin 3 (GPIO 2) | I²C SDA |
| SCL | Pin 5 (GPIO 3) | I²C SCL |
Install I²C tools and confirm device:
sudo apt install -y i2c-tools
sudo i2cdetect -y 1
You should see addresses like 0x68 (MPU9250) and 0x76/0x77 (BMP280).
Full Code
You’ll create:
- A
ugv_beast_descriptionpackage with URDF andros2_control. - A
ugv_beast_bringuppackage with: - GPS and IMU nodes (Python “drivers” using serial and SMBus).
robot_localizationEKF config.- Nav2 / waypoint logic simplified for GPS waypoint nav.
All paths are under ~/ros2_ws/src.
1. Create packages
cd ~/ros2_ws/src
# Description package
ros2 pkg create ugv_beast_description --build-type ament_cmake --dependencies urdf xacro
# Bringup package (Python)
ros2 pkg create ugv_beast_bringup --build-type ament_python \
--dependencies rclpy sensor_msgs nav_msgs geometry_msgs std_msgs \
tf2_ros robot_localization nav2_msgs
2. URDF and ros2_control (ugv_beast_description)
Create URDF file:
mkdir -p ~/ros2_ws/src/ugv_beast_description/urdf
nano ~/ros2_ws/src/ugv_beast_description/urdf/ugv_beast.urdf.xacro
Use:
<?xml version="1.0"?>
<robot name="ugv_beast" xmlns:xacro="http://ros.org/wiki/xacro">
<!-- Parameters (calibrate these) -->
<xacro:property name="wheel_radius" value="0.05"/> <!-- 5 cm -->
<xacro:property name="track_width" value="0.30"/> <!-- 30 cm -->
<link name="base_link">
<inertial>
<origin xyz="0 0 0" rpy="0 0 0"/>
<mass value="5.0"/>
<inertia ixx="0.1" iyy="0.1" izz="0.1" ixy="0" ixz="0" iyz="0"/>
</inertial>
</link>
<link name="left_wheel_link"/>
<link name="right_wheel_link"/>
<joint name="left_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="left_wheel_link"/>
<origin xyz="0 ${track_width/2.0} 0" rpy="0 0 0"/>
<axis xyz="0 1 0"/>
</joint>
<joint name="right_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="right_wheel_link"/>
<origin xyz="0 ${-track_width/2.0} 0" rpy="0 0 0"/>
<axis xyz="0 1 0"/>
</joint>
<!-- ros2_control for diff drive -->
<ros2_control name="diff_drive_base" type="system">
<hardware>
<class_type>ros2_control_demo_hardware/DiffBotSystemHardware</class_type>
<param name="wheel_radius">${wheel_radius}</param>
<param name="track_width">${track_width}</param>
</hardware>
<joint name="left_wheel_joint">
<command_interface name="velocity"/>
<state_interface name="position"/>
<state_interface name="velocity"/>
</joint>
<joint name="right_wheel_joint">
<command_interface name="velocity"/>
<state_interface name="position"/>
<state_interface name="velocity"/>
</joint>
<controller name="diff_drive_controller" type="diff_drive_controller/DiffDriveController">
<param name="publish_rate">50.0</param>
<param name="wheel_separation">${track_width}</param>
<param name="wheel_radius">${wheel_radius}</param>
<param name="cmd_vel_timeout">0.5</param>
<param name="use_stamped_vel">true</param>
<param name="base_frame_id">base_link</param>
<param name="odom_frame_id">odom</param>
<param name="enable_odom_tf">true</param>
</controller>
</ros2_control>
</robot>
Calibration note:
– Wheel radius: push the robot one wheel revolution, measure distanceD, thenwheel_radius = D / (2π).
– Track width: measure distance between the centers of left and right wheels.
Update CMakeLists.txt to install URDF:
# Add at end of ~/ros2_ws/src/ugv_beast_description/CMakeLists.txt
install(DIRECTORY urdf
DESTINATION share/${PROJECT_NAME}
)
3. GPS and IMU nodes + EKF config (ugv_beast_bringup)
3.1 Package structure
mkdir -p ~/ros2_ws/src/ugv_beast_bringup/ugv_beast_bringup
mkdir -p ~/ros2_ws/src/ugv_beast_bringup/config
mkdir -p ~/ros2_ws/src/ugv_beast_bringup/launch
Create __init__.py:
touch ~/ros2_ws/src/ugv_beast_bringup/ugv_beast_bringup/__init__.py
3.2 GPS node (Adafruit Ultimate GPS HAT, NMEA over serial)
Create GPS node:
nano ~/ros2_ws/src/ugv_beast_bringup/ugv_beast_bringup/gps_node.py
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
import serial
import pynmea2
from sensor_msgs.msg import NavSatFix, NavSatStatus
class GPSNode(Node):
def __init__(self):
super().__init__('gps_node')
port = self.declare_parameter('port', '/dev/serial0').value
baud = self.declare_parameter('baud', 9600).value
self.get_logger().info(f'Opening GPS serial port {port} @ {baud}')
self.ser = serial.Serial(port, baudrate=baud, timeout=1.0)
self.fix_pub = self.create_publisher(NavSatFix, 'gps/fix', 10)
self.timer = self.create_timer(0.5, self.read_loop)
def read_loop(self):
try:
line = self.ser.readline().decode('ascii', errors='replace').strip()
if not line.startswith('$GPGGA') and not line.startswith('$GNGGA'):
return
msg = pynmea2.parse(line)
fix = NavSatFix()
fix.header.stamp = self.get_clock().now().to_msg()
fix.header.frame_id = 'gps_link'
if int(msg.gps_qual) > 0:
fix.status.status = NavSatStatus.STATUS_FIX
else:
fix.status.status = NavSatStatus.STATUS_NO_FIX
fix.status.service = NavSatStatus.SERVICE_GPS
fix.latitude = float(msg.latitude)
fix.longitude = float(msg.longitude)
fix.altitude = float(msg.altitude)
# Simple covariance placeholder (high uncertainty)
fix.position_covariance = [25.0, 0.0, 0.0,
0.0, 25.0, 0.0,
0.0, 0.0, 100.0]
fix.position_covariance_type = NavSatFix.COVARIANCE_TYPE_DIAGONAL_KNOWN
self.fix_pub.publish(fix)
except Exception as e:
self.get_logger().warn(f'GPS parse error: {e}')
def main(args=None):
rclpy.init(args=args)
node = GPSNode()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
3.3 IMU node (MPU9250 on Waveshare 10‑DOF over I²C)
Create IMU node:
nano ~/ros2_ws/src/ugv_beast_bringup/ugv_beast_bringup/imu_node.py
#!/usr/bin/env python3
import math
import time
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Imu
from smbus2 import SMBus
MPU9250_ADDR = 0x68
PWR_MGMT_1 = 0x6B
ACCEL_XOUT_H = 0x3B
GYRO_XOUT_H = 0x43
def read_word_2c(bus, addr, reg):
high = bus.read_byte_data(addr, reg)
low = bus.read_byte_data(addr, reg + 1)
val = (high << 8) + low
if val >= 0x8000:
val = -((65535 - val) + 1)
return val
class IMUNode(Node):
def __init__(self):
super().__init__('imu_node')
self.bus_id = self.declare_parameter('bus', 1).value
self.bus = SMBus(self.bus_id)
self.bus.write_byte_data(MPU9250_ADDR, PWR_MGMT_1, 0x00)
time.sleep(0.1)
self.imu_pub = self.create_publisher(Imu, 'imu/data_raw', 50)
self.timer = self.create_timer(0.01, self.read_loop) # 100 Hz
# Simple scale factors (example; adjust/calibrate!)
self.accel_scale = 16384.0 # LSB/g
self.gyro_scale = 131.0 # LSB/(deg/s)
def read_loop(self):
msg = Imu()
msg.header.stamp = self.get_clock().now().to_msg()
msg.header.frame_id = 'imu_link'
# Read raw accel
ax = read_word_2c(self.bus, MPU9250_ADDR, ACCEL_XOUT_H)
ay = read_word_2c(self.bus, MPU9250_ADDR, ACCEL_XOUT_H + 2)
az = read_word_2c(self.bus, MPU9250_ADDR, ACCEL_XOUT_H + 4)
# Read raw gyro
gx = read_word_2c(self.bus, MPU9250_ADDR, GYRO_XOUT_H)
gy = read_word_2c(self.bus, MPU9250_ADDR, GYRO_XOUT_H + 2)
gz = read_word_2c(self.bus, MPU9250_ADDR, GYRO_XOUT_H + 4)
# Convert to SI units
g = 9.80665
msg.linear_acceleration.x = ax / self.accel_scale * g
msg.linear_acceleration.y = ay / self.accel_scale * g
msg.linear_acceleration.z = az / self.accel_scale * g
deg2rad = math.pi / 180.0
msg.angular_velocity.x = gx / self.gyro_scale * deg2rad
msg.angular_velocity.y = gy / self.gyro_scale * deg2rad
msg.angular_velocity.z = gz / self.gyro_scale * deg2rad
# Orientation not computed here; set to "no orientation" covariance
msg.orientation_covariance[0] = -1.0
self.imu_pub.publish(msg)
def main(args=None):
rclpy.init(args=args)
node = IMUNode()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
For production, use a robust MPU9250 driver package. Here we use minimal code for a hands‑on basic example.
3.4 EKF configuration for robot_localization
Create EKF config:
nano ~/ros2_ws/src/ugv_beast_bringup/config/ekf_gps_imu.yaml
ekf_filter_node:
ros__parameters:
frequency: 30.0
sensor_timeout: 0.1
two_d_mode: true
publish_tf: true
map_frame: map
odom_frame: odom
base_link_frame: base_link
world_frame: map
# IMU (angular velocity + linear acceleration)
imu0: imu/data_raw
imu0_config: [false, false, false,
true, true, true,
false, false, false,
true, true, true,
false, false, false]
imu0_differential: false
imu0_remove_gravitational_acceleration: true
# GPS (NavSatFix, fused via navsat_transform_node normally; for simplicity assume already in map frame)
# In a full solution you'd also configure navsat_transform_node.
# Here we treat GPS as position in world frame (conceptual).
use_control: false
In a full GPS fusion setup you would add
navsat_transform_nodewithgps0etc. For this basic exercise we conceptually combine IMU and position, focusing on the architecture rather than perfect geodesy.
3.5 Simple GPS waypoint navigation node
This node subscribes to gps/fix and publishes velocity commands to /cmd_vel to head toward each waypoint (very basic P‑controller):
nano ~/ros2_ws/src/ugv_beast_bringup/ugv_beast_bringup/gps_waypoint_nav.py
#!/usr/bin/env python3
import math
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import NavSatFix
from geometry_msgs.msg import Twist
from std_msgs.msg import String
def haversine(lat1, lon1, lat2, lon2):
# Distance in meters between two lat/lon
R = 6371000.0
phi1 = math.radians(lat1)
phi2 = math.radians(lat2)
dphi = math.radians(lat2 - lat1)
dlambda = math.radians(lon2 - lon1)
a = math.sin(dphi/2.0)**2 + math.cos(phi1)*math.cos(phi2)*math.sin(dlambda/2.0)**2
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
return R * c
def bearing(lat1, lon1, lat2, lon2):
phi1 = math.radians(lat1)
phi2 = math.radians(lat2)
dlambda = math.radians(lon2 - lon1)
x = math.sin(dlambda) * math.cos(phi2)
y = math.cos(phi1)*math.sin(phi2) - math.sin(phi1)*math.cos(phi2)*math.cos(dlambda)
brng = math.atan2(x, y)
return (math.degrees(brng) + 360.0) % 360.0
class GPSWaypointNav(Node):
def __init__(self):
super().__init__('gps_waypoint_nav')
# Waypoints: [ (lat, lon), ... ]
self.waypoints = self.declare_parameter(
'waypoints',
[
0.0, 0.0,
0.0, 0.0
]
).value
# Convert to list of tuples
self.waypoints = [(self.waypoints[i], self.waypoints[i+1])
for i in range(0, len(self.waypoints), 2)]
self.current_wp_idx = 0
self.reached_threshold = self.declare_parameter('reached_threshold', 3.0).value # meters
self.max_linear = self.declare_parameter('max_linear', 0.4).value
self.max_angular = self.declare_parameter('max_angular', 0.6).value
self.cmd_pub = self.create_publisher(Twist, 'cmd_vel', 10)
self.status_pub = self.create_publisher(String, 'gps_nav/status', 10)
self.create_subscription(NavSatFix, 'gps/fix', self.gps_cb, 10)
self.current_lat = None
self.current_lon = None
def gps_cb(self, msg: NavSatFix):
if msg.status.status < 0:
self.get_logger().warn('No GPS fix yet')
return
self.current_lat = msg.latitude
self.current_lon = msg.longitude
if self.current_wp_idx >= len(self.waypoints):
self.stop_robot()
self.publish_status('All waypoints reached')
return
target_lat, target_lon = self.waypoints[self.current_wp_idx]
dist = haversine(self.current_lat, self.current_lon, target_lat, target_lon)
if dist < self.reached_threshold:
self.publish_status(f'Waypoint {self.current_wp_idx} reached (dist={dist:.1f} m)')
self.current_wp_idx += 1
self.stop_robot()
return
# Simple heading control: assume robot heading is not measured, so just drive forward;
# in a practical system you'd use yaw from IMU/odometry.
# Here we just set forward velocity until in threshold.
twist = Twist()
twist.linear.x = min(self.max_linear, 0.1 + dist * 0.01)
twist.angular.z = 0.0 # no steering correction in this simplified basic demo
self.cmd_pub.publish(twist)
self.publish_status(
f'Heading to WP {self.current_wp_idx}: dist={dist:.1f} m lat={target_lat:.6f} lon={target_lon:.6f}'
)
def stop_robot(self):
twist = Twist()
self.cmd_pub.publish(twist)
def publish_status(self, text):
msg = String()
msg.data = text
self.status_pub.publish(msg)
self.get_logger().info(text)
def main(args=None):
rclpy.init(args=args)
node = GPSWaypointNav()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
This is a basic waypoint demo focusing on the flow from GPS to
/cmd_vel. To get real turns, integrate yaw from IMU/odometry to compute heading error and setangular.z.
3.6 Launch file
Create a bringup launch file:
nano ~/ros2_ws/src/ugv_beast_bringup/launch/ugv_gps_nav.launch.py
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
# Example waypoints (replace with your outdoor coordinates)
waypoints = [
52.000000, 13.000000,
52.000010, 13.000010
]
return LaunchDescription([
Node(
package='ugv_beast_bringup',
executable='gps_node',
name='gps_node',
output='screen',
parameters=[{'port': '/dev/serial0', 'baud': 9600}]
),
Node(
package='ugv_beast_bringup',
executable='imu_node',
name='imu_node',
output='screen'
),
Node(
package='robot_localization',
executable='ekf_node',
name='ekf_filter_node',
output='screen',
parameters=['config/ekf_gps_imu.yaml'],
remappings=[
('/imu/data', '/imu/data_raw')
]
),
Node(
package='ugv_beast_bringup',
executable='gps_waypoint_nav',
name='gps_waypoint_nav',
output='screen',
parameters=[{
'waypoints': waypoints,
'reached_threshold': 3.0
}]
),
])
3.7 setup.py update
Edit ~/ros2_ws/src/ugv_beast_bringup/setup.py:
from setuptools import setup
package_name = 'ugv_beast_bringup'
setup(
name=package_name,
version='0.0.1',
packages=[package_name],
data_files=[
('share/ament_index/resource_index/packages',
['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
('share/' + package_name + '/config', ['config/ekf_gps_imu.yaml']),
('share/' + package_name + '/launch', ['launch/ugv_gps_nav.launch.py']),
],
install_requires=['setuptools', 'pyserial', 'pynmea2', 'smbus2'],
zip_safe=True,
maintainer='you',
maintainer_email='you@example.com',
description='UGV Beast basic GPS + IMU waypoint navigation bringup',
license='Apache-2.0',
tests_require=['pytest'],
entry_points={
'console_scripts': [
'gps_node = ugv_beast_bringup.gps_node:main',
'imu_node = ugv_beast_bringup.imu_node:main',
'gps_waypoint_nav = ugv_beast_bringup.gps_waypoint_nav:main',
],
},
)
Install Python dependencies:
sudo apt install -y python3-serial python3-pynmea2 python3-smbus
Build / Flash / Run commands
1. Build workspace
cd ~/ros2_ws
colcon build
source install/setup.bash
2. Run the GPS + IMU + EKF + waypoint nav
Outdoors with good sky view:
ros2 launch ugv_beast_bringup ugv_gps_nav.launch.py
You should see logs from:
gps_node(NMEA parsing).imu_node(IMU streaming).ekf_filter_node(robot_localization).gps_waypoint_nav(status messages).
Step‑by‑step Validation
1. Validate topics exist
In another terminal:
source ~/ros2_ws/install/setup.bash
ros2 topic list
You should at least see:
/gps/fix/imu/data_raw/odometry/filtered(from EKF)/cmd_vel/gps_nav/status
2. Check GPS messages and rate
ros2 topic echo /gps/fix
Verify:
- Lat/Lon values are non‑zero and plausible.
status.status >= 0when fix is valid.
Check rate:
ros2 topic hz /gps/fix
Expect around 1 Hz.
3. Check IMU messages and rate
ros2 topic echo /imu/data_raw
ros2 topic hz /imu/data_raw
Expect around 100 Hz.
Move the robot; see changes in accelerations and angular velocities.
4. Check EKF output
ros2 topic echo /odometry/filtered
ros2 topic hz /odometry/filtered
Verify:
- Frame IDs:
header.frame_idshould beodomormapdepending on config. - Pose covariance is finite.
- Rate around 30 Hz.
5. Check navigation status / path execution
Echo status:
ros2 topic echo /gps_nav/status
Watch logs:
- Initially, “Heading to WP 0…”
- When within
reached_thresholdmeters: “Waypoint 0 reached…” - Then “Heading to WP 1…”
- Finally “All waypoints reached.”
Visually observe the robot:
- Moves forward when GPS fix is valid.
- Stops as it reaches each waypoint’s neighborhood.
6. Record metrics with rosbag
Record core topics:
ros2 bag record /gps/fix /imu/data_raw /odometry/filtered /cmd_vel /gps_nav/status
Drive through waypoint route. Stop recording (Ctrl+C). Later, inspect:
ros2 bag info <bagname>
You can replay for offline analysis:
ros2 bag play <bagname>
Troubleshooting
Common issues and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
/gps/fix topic not visible |
gps_node not running or wrong serial port |
Check ros2 node list; verify /dev/serial0; ensure UART enabled. |
GPS messages but STATUS_NO_FIX forever |
Poor satellite visibility | Move outdoors, wait several minutes, check the GPS LED on the HAT. |
/imu/data_raw not published |
I²C disabled or wrong wiring | Re‑check config.txt i2c_arm=on, wiring to GPIO2/3, rerun i2cdetect. |
EKF not publishing /odometry/filtered |
Wrong parameter path or remappings | Confirm ekf_gps_imu.yaml installed and used by launch file. |
| Robot not moving despite waypoint logs | /cmd_vel not connected to diff_drive controller |
Ensure your motor driver/ros2_control stack subscribes to cmd_vel. |
| Waypoints reached but large position error | GPS accuracy limited or incorrect coordinates | Use open‑sky, average coordinates, or increase reached_threshold. |
Improvements
Once the basic flow works, you can improve the system:
- Add proper
navsat_transform_nodepipeline - Convert
NavSatFix+ IMU + odometry intomapframe positions. -
Feed into
robot_localizationasgps0input for more consistent EKF. -
Integrate full Nav2 stack
- Use
nav2_bt_navigatorandnav2_waypoint_followerinstead of a custom node. -
Represent GPS waypoints as
PoseStampedgoals inmapframe after geo‑to‑map conversion. -
Use orientation from IMU + magnetometer
- Compute yaw from IMU and magnetometer.
-
Replace the “no steering” logic in
gps_waypoint_nav.pywith heading error control. -
Closed‑loop tuning
- Tune
max_linear/max_angular, thresholds, and IMU scales. -
Log path and compare to ground truth (e.g., smartphone GPS track).
-
Robust hardware integration
- Replace the demo
ros2_control_demo_hardwarewith a real hardware interface for your motor driver, publishing wheel odometry to EKF.
Final Checklist
Before going to the field, confirm each item:
- [ ] Ubuntu 22.04 64‑bit on Raspberry Pi 4,
ros-humble-desktopand required stacks installed. - [ ]
~/ros2_wscreated, packagesugv_beast_descriptionandugv_beast_bringuppresent and building withcolcon build. - [ ] UART enabled,
/dev/serial0accessible, GPS HAT stacked correctly. - [ ] I²C enabled,
i2cdetect -y 1shows0x68for MPU9250. - [ ]
gps_nodepublishes/gps/fixwith valid coordinates andSTATUS_FIX. - [ ]
imu_nodepublishes/imu/data_rawat ~100 Hz. - [ ]
robot_localizationEKF node publishes/odometry/filtered. - [ ] Waypoints in
ugv_gps_nav.launch.pyset to realistic Lat/Lon for your test area. - [ ] Motor controller listens to
/cmd_veland moves the robot forward. - [ ]
gps_waypoint_navlogs “Waypoint N reached” and “All waypoints reached” when near targets. - [ ]
ros2 bagrecording works for key topics for later analysis.
With this, you have a reproducible basic ROS 2 GPS waypoint navigation setup on Raspberry Pi 4 Model B + Adafruit Ultimate GPS HAT (MTK3339) + Waveshare 10‑DOF IMU Breakout (MPU9250 + BMP280) in the UGV Beast (ROS 2) – RPi family.
Find this product and/or books on this topic on Amazon
As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.




