> ## Documentation Index
> Fetch the complete documentation index at: https://dragonwingdocs.qualcomm.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Drive a TurtleBot3 with ROS 2 on Dragonwing

> Bring up a TurtleBot3 Burger on a Dragonwing SBC, build a map with Cartographer SLAM, and drive it autonomously with Nav2, all on ROS 2 Jazzy.

<div
  style={{
width: "100%",
borderRadius: "14px",
overflow: "hidden",
background: "linear-gradient(135deg, #0b7285 0%, #31017D 58%, #6f62ff 100%)",
display: "flex",
alignItems: "center",
justifyContent: "center",
position: "relative",
marginBottom: "1.5rem"
}}
>
  <img src="https://mintcdn.com/qualcomm-prod/ZfLYzCq58l7pq2VV/images/tutorials/tb3_ros_iq9.jpg?fit=max&auto=format&n=ZfLYzCq58l7pq2VV&q=85&s=6281c2f69916309b67d547f9965e3e6c" alt="A TurtleBot3 Burger carrying a Qualcomm Dragonwing single-board computer with a 360-degree LiDAR on top, on a workbench beside an AprilTag calibration mat" style={{ maxHeight: "460px", width: "auto", maxWidth: "100%", objectFit: "contain", display: "block" }} width="4000" height="3000" data-path="images/tutorials/tb3_ros_iq9.jpg" />

  <div
    style={{
position: "absolute",
bottom: "16px",
left: "50%",
transform: "translateX(-50%)",
background: "rgba(255,255,255,0.15)",
border: "1px solid rgba(255,255,255,0.4)",
color: "#fff",
fontSize: "0.75rem",
fontWeight: 700,
letterSpacing: "1px",
padding: "5px 14px",
borderRadius: "20px",
textTransform: "uppercase",
whiteSpace: "nowrap",
zIndex: 1
}}
  >
    Dragonwing · Qualcomm
  </div>
</div>

<div style={{ marginBottom: "2rem" }}>
  <div
    style={{
fontSize: "0.72rem",
fontWeight: 700,
color: "#31017D",
letterSpacing: "1.5px",
textTransform: "uppercase",
marginBottom: "0.5rem"
}}
  >
    Robotics
  </div>

  <div style={{ fontSize: "0.85rem", color: "#888", display: "flex", gap: "0.5rem", flexWrap: "wrap", alignItems: "center" }}>
    <span>Dragonwing Team</span>
    <span>·</span>
    <span>Jun 26, 2026</span>
    <span>·</span>
    <a href="/tutorial" style={{ color: "#31017D", fontWeight: 600, textDecoration: "none" }}>← All posts</a>
  </div>
</div>

<hr style={{ border: "none", borderTop: "1px solid #eee", margin: "0 0 2rem" }} />

The [TurtleBot3 Burger](https://emanual.robotis.com/docs/en/platform/turtlebot3/overview/) is the classic "hello world" robot for ROS 2: a small differential-drive platform with a 360-degree LiDAR that is just capable enough to do real autonomous navigation. In this post we put one on top of a Qualcomm Dragonwing SBC, install ROS 2 Jazzy from scratch, bring the robot up, build a map of a room with Cartographer, and then let Nav2 drive the robot to a goal on its own.

Everything here runs on the board itself. The Dragonwing SBC is the robot's compute, so there is no laptop in the loop except, optionally, to watch the map come together in RViz.

<Note>
  This walkthrough targets a **TurtleBot3 Burger** with a **pre-flashed OpenCR** controller and an **LDS-02 LiDAR**, running **ROS 2 Jazzy** on Ubuntu. If your LiDAR or controller revision differs, adjust the model variables in the setup step accordingly.
</Note>

## What you will do

1. Install ROS 2 Jazzy on the Dragonwing device.
2. Build the TurtleBot3 workspace and wire up the serial devices.
3. Bring the robot up and drive it by keyboard.
4. Build a map of your space with Cartographer SLAM and save it.
5. Hand the map to Nav2 and send the robot to a goal autonomously.
6. View everything in RViz, on the board's own screen or from another machine.

## Prerequisites

Before you begin, make sure you have:

* A Dragonwing device that has completed first-time setup:
  * [Dragonwing IQ8 setup](/Ubuntu/devices/iq8275-evk/setup)
  * [Dragonwing IQ9 setup](/Ubuntu/devices/iq9075-evk/setup)
* Access to the device by SSH, or by a directly connected display, keyboard, and mouse. The setup guides above cover networking, serial console, display, and SSH access.
* A TurtleBot3 Burger with the OpenCR board pre-flashed and an LDS-02 LiDAR.
* Two free USB ports on the SBC: one for the OpenCR controller, one for the LiDAR.

## Install ROS 2 Jazzy

This is a one-time step on a fresh board. It follows the standard ROS 2 Jazzy install for Ubuntu, so if you already have ROS 2 Jazzy on the device you can skip straight to [setting up the TurtleBot3](#set-up-the-turtlebot3). The commands below mirror the [official ROS 2 Jazzy installation guide](https://docs.ros.org/en/jazzy/Installation/Ubuntu-Install-Debs.html) exactly.

First, add the ROS 2 apt repository:

```bash theme={null}
sudo apt update
sudo apt install -y curl gnupg2 lsb-release ca-certificates software-properties-common locales
sudo locale-gen en_US en_US.UTF-8
sudo update-locale LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8

sudo add-apt-repository universe -y
sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key \
  -o /usr/share/keyrings/ros-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] \
  http://packages.ros.org/ros2/ubuntu $(. /etc/os-release && echo $UBUNTU_CODENAME) main" \
  | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null
```

Then install the desktop bundle plus the build tooling:

```bash theme={null}
sudo apt update
sudo apt install -y ros-jazzy-desktop \
  python3-colcon-common-extensions \
  python3-rosdep \
  python3-argcomplete
sudo rosdep init || true
rosdep update
```

Finally, source ROS in every new shell by appending it to `~/.bashrc`:

```bash theme={null}
echo 'source /opt/ros/jazzy/setup.bash' >> ~/.bashrc
source ~/.bashrc
```

## Set up the TurtleBot3

With ROS in place, it is time to give the robot its software and tell it which hardware it has.

### Plug in and check the hardware

The Burger talks to the SBC over two USB connections:

| Connection         | Device               |
| ------------------ | -------------------- |
| OpenCR → SBC       | USB → `/dev/ttyACM0` |
| LDS-02 LiDAR → SBC | USB → `/dev/ttyUSB0` |

Confirm both devices enumerated before going any further:

```bash theme={null}
ls /dev/ttyACM0 /dev/ttyUSB0
```

### Build the TurtleBot3 workspace

Clone the three TurtleBot3 packages for Jazzy and build them with colcon. The full upstream instructions live in the [ROBOTIS TurtleBot3 documentation](https://emanual.robotis.com/docs/en/platform/turtlebot3/quick-start/).

```bash theme={null}
mkdir -p ~/turtlebot3_ws/src
cd ~/turtlebot3_ws/src
git clone -b jazzy https://github.com/ROBOTIS-GIT/DynamixelSDK.git
git clone -b jazzy https://github.com/ROBOTIS-GIT/turtlebot3_msgs.git
git clone -b jazzy https://github.com/ROBOTIS-GIT/turtlebot3.git

cd ~/turtlebot3_ws
source /opt/ros/jazzy/setup.bash
rosdep install --from-paths src --ignore-src -r -y
colcon build --symlink-install
```

### Tell ROS about your robot

The TurtleBot3 stack is configured through environment variables. Append them to `~/.bashrc` so every terminal picks them up:

```bash theme={null}
echo 'source ~/turtlebot3_ws/install/setup.bash' >> ~/.bashrc
echo 'export TURTLEBOT3_MODEL=burger' >> ~/.bashrc
echo 'export OPENCR_PORT=/dev/ttyACM0' >> ~/.bashrc
echo 'export LDS_MODEL=LDS-02' >> ~/.bashrc
source ~/.bashrc
```

Here is what each one does:

| Variable           | Value          | Purpose                              |
| ------------------ | -------------- | ------------------------------------ |
| `TURTLEBOT3_MODEL` | `burger`       | Selects the robot URDF and config    |
| `OPENCR_PORT`      | `/dev/ttyACM0` | Serial port to the OpenCR controller |
| `LDS_MODEL`        | `LDS-02`       | Selects the LiDAR driver             |

### Grant serial permissions

Your user needs access to the serial ports, and `ModemManager` has to stay away from the OpenCR's CDC ACM device. Add yourself to the `dialout` group, disable `ModemManager`, and install the TurtleBot3 udev rules:

```bash theme={null}
sudo usermod -aG dialout $USER
sudo systemctl disable --now ModemManager

sudo cp $(ros2 pkg prefix turtlebot3_bringup)/share/turtlebot3_bringup/script/99-turtlebot3-cdc.rules \
  /etc/udev/rules.d/
sudo udevadm control --reload-rules
sudo udevadm trigger
```

<Tip>**Log out and back in** (or reboot) after this step so the `dialout` group membership takes effect. If you need to keep working in the same session before logging out, see the serial-permission workaround in [Troubleshooting](#troubleshooting).</Tip>

## Bring the robot up

Launch the bringup. This starts the OpenCR driver, the LiDAR driver, and the differential-drive controller:

```bash theme={null}
ros2 launch turtlebot3_bringup robot.launch.py
```

A healthy bringup logs something like this:

```
[DynamixelSDKWrapper]: Succeeded to open the port(/dev/ttyACM0)!
[ld08_driver]: FOUND LDS-02
[ld08_driver]: LDS-02 started successfully
[turtlebot3_node]: Run!
[diff_drive_controller]: Run!
```

### Drive it by keyboard

Leave bringup running and open a second terminal. The TurtleBot3 teleop node is the recommended way to drive the robot, including while you are mapping:

```bash theme={null}
ros2 run turtlebot3_teleop teleop_keyboard
```

Use the `W/A/S/D/X` keys to move, and press `S` or `X` to stop before you close the window.

## Build a map with Cartographer SLAM

Now for the fun part. With the robot driving, you can build an occupancy-grid map of the space using Cartographer.

Keep [bringup](#bring-the-robot-up) running in its own terminal, then start Cartographer:

```bash theme={null}
ros2 launch turtlebot3_cartographer cartographer.launch.py use_sim_time:=false
```

RViz will fail to open if there is no display connected, and that is fine. Cartographer and the `/map` topic keep working headlessly while you drive.

Now drive the robot around with [keyboard teleop](#drive-it-by-keyboard), or by publishing velocity commands. The more of the space you cover, the better the resulting map.

When you are happy with the coverage, save the map:

```bash theme={null}
ros2 run nav2_map_server map_saver_cli -t /map -f ~/my_map
```

That writes two files:

* `~/my_map.pgm` is the occupancy-grid image.
* `~/my_map.yaml` is the map metadata (resolution, origin).

Stop Cartographer once the map is saved.

## Drive autonomously with Nav2

With a saved map in hand, hand it to Nav2 and let the robot navigate on its own. (Bringup still needs to be running.)

```bash theme={null}
ros2 launch turtlebot3_navigation2 navigation2.launch.py \
  use_sim_time:=false \
  map:=$HOME/my_map.yaml
```

In RViz, set a **2D Pose Estimate** so Nav2 knows where the robot is on the map, then send a navigation goal and watch it plan a path and drive there.

No display? You can do the same thing headlessly by sending the goal as a ROS 2 action:

```bash theme={null}
ros2 action send_goal /navigate_to_pose nav2_msgs/action/NavigateToPose \
  '{pose: {header: {frame_id: "map"}, pose: {position: {x: 1.0, y: 0.5}}}}'
```

## Watch it in RViz

RViz is the easiest way to see the map and the robot's plan. There are two ways to run it: on the board's own screen, or from another machine on the network.

### On the board's screen over SSH

The GNOME desktop uses Wayland, but RViz2's Ogre renderer needs GLX (X11). GNOME runs XWayland automatically, but the auth file (`XAUTHORITY`) has a random suffix that changes on every reboot. The cleanest fix is a small helper in `~/.bashrc` that finds it for you:

```bash theme={null}
cat >> ~/.bashrc << 'EOF'

export-display() {
  local xauth
  xauth=$(find /run/user/1000 -name '.mutter-Xwaylandauth.*' 2>/dev/null | head -1)
  export DISPLAY=:0
  export XAUTHORITY="$xauth"
  export XDG_RUNTIME_DIR=/run/user/1000
  echo "DISPLAY=$DISPLAY  XAUTHORITY=$XAUTHORITY"
}
EOF
source ~/.bashrc
```

Run `export-display` in any SSH session before you start RViz:

```bash theme={null}
export-display
```

It prints the resolved environment:

```
DISPLAY=:0  XAUTHORITY=/run/user/1000/.mutter-Xwaylandauth.XXXXXX
```

Putting it together, mapping with RViz on the connected screen looks like this:

```bash theme={null}
# Terminal 1 – bringup
ros2 launch turtlebot3_bringup robot.launch.py

# Terminal 2 – SLAM + RViz on the connected screen
export-display
ros2 launch turtlebot3_cartographer cartographer.launch.py use_sim_time:=false
```

<Tip>If you open a terminal directly on the GNOME desktop rather than over SSH, `DISPLAY` and `XAUTHORITY` are already set. Run RViz normally without `export-display`.</Tip>

### From another machine on the network

To run RViz on a laptop or workstation on the same network:

1. On **both** machines, set the same domain ID:
   ```bash theme={null}
   export ROS_DOMAIN_ID=30
   ```
2. Install ROS 2 Jazzy on the remote machine.
3. On the remote machine, open RViz:
   ```bash theme={null}
   rviz2
   ```
   Add a **Map** display and set the topic to `/map`. ROS 2 discovers the map data automatically over the network, so no extra configuration is needed.

## Troubleshooting

A few things tend to trip people up the first time. Here are the usual suspects and their fixes.

### `/dev/ttyACM0` permission denied

You have not logged out since being added to the `dialout` group. Use this wrapper until you do:

```bash theme={null}
sg dialout -c '. /opt/ros/jazzy/setup.sh; . ~/turtlebot3_ws/install/setup.sh; \
  export TURTLEBOT3_MODEL=burger OPENCR_PORT=/dev/ttyACM0 LDS_MODEL=LDS-02; \
  ros2 launch turtlebot3_bringup robot.launch.py'
```

After one logout and login, the wrapper is no longer needed.

### LiDAR scan empty or Cartographer stuck

`Queue waiting for data: (0, scan)` usually means the wrong LDS model is set.

```bash theme={null}
echo $LDS_MODEL    # must be LDS-02
ls /dev/ttyUSB0    # must exist
```

If `LDS_MODEL=LDS-01`, fix it:

```bash theme={null}
sed -i 's/export LDS_MODEL=LDS-01/export LDS_MODEL=LDS-02/' ~/.bashrc
source ~/.bashrc
```

### `package 'ld08_driver' not found`

```bash theme={null}
sudo apt install -y ros-jazzy-ld08-driver
```

### `cmd_vel` commands do nothing

ROS 2 Jazzy changed the `cmd_vel` topic type from `geometry_msgs/Twist` to `geometry_msgs/TwistStamped`. If you publish `Twist` directly (for example from a custom node or an older tutorial), add a `header` field:

```python theme={null}
from geometry_msgs.msg import TwistStamped
from builtin_interfaces.msg import Time

msg = TwistStamped()
msg.header.stamp = self.get_clock().now().to_msg()
msg.twist.linear.x = 0.2
msg.twist.angular.z = 0.0
publisher.publish(msg)
```

The built-in `turtlebot3_teleop teleop_keyboard` node already handles this correctly, so this only affects custom publishers.

### Map saver fails silently

Always pass `-t /map` explicitly:

```bash theme={null}
ros2 run nav2_map_server map_saver_cli -t /map -f ~/my_map
```

### `apt` locked by a background process

```bash theme={null}
sudo systemctl stop apt-daily.service apt-daily-upgrade.service packagekit.service
sudo pkill -f aptd || true
sudo apt update
```

## Next steps

You now have a robot that maps a room and drives itself, all from a Dragonwing board. From here you can:

* Run hardware-accelerated AI on the robot's camera. The [Depth Estimation on the NPU](/Ubuntu/robotics-workflows/npu-workflows) walkthrough builds a ROS 2 node that runs a quantized model on the Hexagon HTP NPU.
* Explore the [QRB ROS ecosystem](/Ubuntu/robotics-workflows/qrb-ros-overview) for zero-copy transport, reference samples, Gazebo simulation, and benchmarking.
* Start from the [Robotics Workflow Overview](/Ubuntu/robotics-workflows/robotics-workflows_overview) for the full picture of robotics development on Dragonwing.
