Before writing pipeline code, two things had to be right: the architecture (how the pieces will talk), and the environment (getting heavyweight tools running under tight constraints).
The project moves data from CARLA to an AGL dashboard. Within the robotics stack, the nodes talk to each other over ROS2's native transport, DDS — that's normal and stays. The real architectural question is the last hop: how the data leaves the ROS world and reaches the AGL HMI.
The AGL HMI is a UI application, not a ROS node — it shouldn't carry a full DDS stack, message definitions, and a colcon build just to draw a screen. So the standard pattern is for it to consume ROS data over rosbridge: a server that exposes the ROS2 graph as plain WebSocket + JSON on TCP port 9090. The HMI is then simply a WebSocket client, fully decoupled from ROS internals. This is the design the project targets regardless of where it runs.
The decision: DDS inside the robotics stack; rosbridge (WebSocket/JSON) at the HMI boundary. This is the correct, intended pattern — and it also happens to make the emulated setup work for free: the AGL VM uses QEMU's user-mode (“slirp”) networking, which DDS multicast discovery can't cross, but rosbridge's plain TCP passes straight through. The right architecture and the practical one are the same.
The full intended pipeline — note DDS carries the data between the ROS2 nodes, and rosbridge appears only at the final hop to the HMI:
A "bridge" is the translator that pulls state out of CARLA (over its RPC API) and publishes it onto ROS2 topics. CARLA ships an official, batteries-included one — carla_ros_bridge — that exposes every sensor and the vehicle state automatically. The problem is that it's a single program that must import carla and import rclpy in the same process — and those two cannot coexist here.
The reason is an ABI lock, not a preference: CARLA 0.9.15's Python bindings are a binary compiled against CPython 3.7 (a cp37 artifact that physically won't load on newer interpreters), while ROS2 Humble's rclpy needs Python 3.10+. There is no single interpreter where both imports succeed:
| Run the bridge on… | import carla | import rclpy |
|---|---|---|
| Python 3.7 | ✓ | ✗ (Humble won't run) |
| Python 3.11 | ✗ (no 3.7 binary loads) | ✓ |
If both libraries can't live in one process, we put them in two — each in the Python version it needs, talking over a local UDP socket carrying JSON. One process imports only carla; the other imports only rclpy; neither ever sees the other's library, so the clash simply never arises. The JSON-over-UDP seam is an "airlock" between the two Python worlds (full implementation in Week 2).
To run two Python versions side by side cleanly, we use Miniforge — a conda installer living entirely in the home folder — giving a carla env (Python 3.7) and a ros2 env (Python 3.11, with ROS2 from the conda-packaged RoboStack distribution).
The two-process bridge is a deliberate, constraint-driven shortcut — ideal for streaming one small topic, but a poor fit for large binary data like a LiDAR point cloud. The proper end state is to eliminate the clash so the official single-process bridge runs:
carla and rclpy share one interpreter. Heavy build, but fully supported.docker/Dockerfile or using a well-maintained community image — the version alignment is then solved inside the container.Practically, the right call differs by stage: the two-process bridge is correct for the lightweight, GPU-less odometry demo here; rebuilding the PythonAPI to run the official carla_ros_bridge is the right move once the GPU-bound LiDAR pipeline begins.
CARLA is built on Unreal Engine and “recommends” an NVIDIA card. The server has neither a discrete GPU nor /dev/dri. The insight: CARLA can run with off-screen rendering, where the engine still launches and simulates physics on the CPU — only GPU sensors (cameras, LiDAR) need real graphics hardware. For vehicle physics and odometry, no GPU is required.
# CARLA 0.9.15 — the old AWS download is dead, use the mirror
mkdir -p ~/carla-sim && cd ~/carla-sim
wget https://carla-releases.s3.us-east-005.backblazeb2.com/Linux/CARLA_0.9.15.tar.gz
tar -xzf CARLA_0.9.15.tar.gz
# the Python-3.7 environment for the CARLA client
conda create -y -n carla python=3.7 && conda activate carla
pip install ~/carla-sim/PythonAPI/carla/dist/carla-0.9.15-cp37-cp37m-*.whl
# launch headless (no display), software-rendered
./CarlaUE4.sh -RenderOffScreen -nosound -carla-rpc-port=2000
The second conda environment provides ROS2 Humble via RoboStack (ROS2 packaged for conda-forge):
conda create -y -n ros2 python=3.11 && conda activate ros2
conda config --env --add channels robostack-staging
conda config --env --add channels conda-forge
mamba install -y ros-humble-ros-base
The two environments — carla (3.7) and ros2 (3.11) — are the foundation of the bridge: one reads the simulator, the other publishes ROS2 topics, and they communicate over a local UDP socket.
AGL is assembled with the Yocto/BitBake build system; the ROS2 integration comes from the meta-ros layers. After evaluating the available release branches, the choice was the unagi release — it's internally consistent (Yocto scarthgap throughout) and ships the meta-agl-ros2 layer out of the box.
mkdir -p ~/AGL/master && cd ~/AGL/master
repo init -u https://gerrit.automotivelinux.org/gerrit/AGL/AGL-repo -b unagi
repo sync -j4
# configure a QEMU x86-64 build with the ROS2 feature
source meta-agl/scripts/aglsetup.sh -m qemux86-64 -b build-qemu-ros agl-ros2
End of community bonding The pieces are chosen and installed: a clear architecture (rosbridge over slirp), two Python environments, CARLA running GPU-less, and an AGL/ROS2 build tree configured. Everything is in place to start building the actual pipeline — which is where the real bugs begin.