Week 11 · Coding Period

Behind the wheel

For the whole project so far the simulator has been three compromises at once: a recording played back, on a machine with no graphics card, through a bridge I wrote myself. This week all three change together. A real GPU workstation, the official CARLA to ROS bridge, and a car I drive by hand, live.

The project has run on a single GPU-less server since the start. That was always a stand-in, and the project's own home page says so: the ideal is a proper workstation running CARLA and the official bridge, feeding a separate target board over a real network. This week the workstation half of that picture finally arrived, and with it the sim stopped being a passive replay and became something I steer.

Two of the gaps in that ideal-versus-built table close at once here. The GPU-less box becomes a real RTX workstation, and the custom two-process bridge I built to dodge a Python-version clash becomes the actual carla_ros_bridge. The dashboard also learned to speak ROS the way every other project does, which retires a piece of my own code. None of it is on the board yet. That is next week's wire. But the source end of the pipeline is now the real thing.

1. A workstation, and a car you steer

On the GPU-less server, CARLA rendered in software at about a third of a frame per second, which is fine for proving a pipeline and painful for anything else. On the workstation it runs on the actual GPU, in its own window, at real frame rates. More importantly, it is no longer a recorded bag being replayed. It runs CARLA's own manual control example: a small window opens, a car spawns into the town, and I drive it with the keyboard.

The car comes up tagged hero. A LiDAR sensor rides on it, and its point cloud goes out on /carla/hero/lidar as the same sensor_msgs/PointCloud2 the dashboard already knows how to read. So the data the app sees is now generated by a vehicle moving because I am moving it, not by a file on disk.

CARLA (GPU window) → a car 'hero' I drive with W / A / S / D │ ▼ LiDAR rides on 'hero' carla_ros_bridge → /carla/hero/lidar (sensor_msgs/PointCloud2) │ ▼ the same dashboard the project has been building

Two gaps closed  The bridge here is the real one: the community carla_ros_bridge fork that tracks this CARLA release and ROS 2 Humble, not the hand-rolled two-process airlock the server needed. A real GPU workstation and the official bridge were two separate stand-ins on the project's ideal-versus-built table. Both are now the genuine article.

2. The ordering rule that keeps it alive

The official bridge has one habit that will crash the whole thing if you do not know about it. When it connects, it compares its town parameter to the map CARLA currently has loaded, and if they differ it reloads the map. Reloading the map while a driver is connected to the world crashes CARLA outright, with a signal 11, and it deletes the car you were driving in the process. My first attempt started the bridge first and then the driver, and it crashed every time with a car that no longer existed.

Three rules make it stable, and they are now baked into a single launch script:

# read the map CARLA already has loaded, and hand it straight back to the bridge
# so town matches and the bridge never reloads (a reload here = crash + no car)
MAP=$(python3 -c "import carla;print(carla.Client('127.0.0.1',2000).get_world().get_map().name)")

ros2 launch carla_ros_bridge carla_ros_bridge.launch.py synchronous_mode:=False town:="$MAP"
# then attach the LiDAR to the EXISTING 'hero' car, do not spawn a second one
ros2 launch carla_spawn_objects ... spawn_sensors_only:=True

The last line matters as much as the order. The sensors are attached to the car that is already there, rather than spawning a fresh vehicle, so there is exactly one car in the world and it is the one I am driving.

3. The memory trap

A good chunk of the day went to a failure that looked like CARLA being flaky and was not CARLA's fault at all. The workstation has fifteen gigabytes of memory and a small swap. A separate machine-learning training job was already running on it, and when I stopped it, it came back. It turned out to be a disowned wrapper script that, on being killed, chained straight into a second trainer. Together they held around six gigabytes.

With that much memory already gone, launching CARLA pushed the machine over the edge and the system killed CARLA to save itself. The symptom was maddening: the server would appear, then vanish a moment later, with swap pinned full. The fix was to stop the entire training process group rather than a single process, because killing one leaf of it simply let the wrapper relaunch the rest.

Read the tree first  This cost time because the symptom pointed at the wrong program. CARLA vanishing looked like a CARLA bug; it was the kernel reclaiming memory from an unrelated job that kept resurrecting itself. Before blaming the thing that died, it is worth checking what else on the box is holding memory, and whether it is designed to come back.

4. The dashboard learns to speak ROS properly

On the application side, one long-standing piece of debt got paid off. The dashboard has always talked to rosbridge through a WebSocket client I wrote by hand: open the socket, send the subscribe message as JSON, and decode every incoming frame myself. It worked and it had no dependencies, but it was bespoke, and reinventing a protocol that already has a library is exactly the kind of thing that looks fine until someone else has to read it.

So I moved the app onto the community library that essentially every Flutter-meets-ROS project uses: a maintained Dart port of the standard rosbridge client. Now the app declares a connection and a topic and hands over a callback, and the library handles the handshake, the subscribe, and the framing. My hand-written point-cloud decoder stays exactly as it was, because that part is specific to this data. Only the connection plumbing changed.

// before: open a raw socket, send the subscribe JSON, decode frames by hand
// after: the standard client does the protocol; I just get the message
final ros = Ros(url: kRosbridgeUrl);
final topic = Topic(ros: ros, name: kLidarTopic, type: kLidarType);
await ros.connect();                 // wait for the connection FIRST
await topic.subscribe(onLidarFrame); // then subscribe, and points flow

There was one sharp edge, and it is a good example of why swapping to a library is not free. The client connects asynchronously, and my first cut subscribed the instant I asked it to, before the connection had actually finished opening, which threw an error the moment it ran. The fix was to wait for the connection to establish before subscribing, and to add a single guard at the top of the app so that a bridge that is down or slow shows a status indicator instead of taking the whole app down with it. With that in place it connects, subscribes, and streams exactly as the old client did, with far less of my code sitting in the path.

5. Reproduce it

The whole workstation side, reduced to what matters, lives behind one command that brings up CARLA with its window, spawns and lets me drive the car, and then attaches the bridge and the LiDAR in the safe order.

# on the workstation, one command: CARLA window -> drive the car -> bridge -> LiDAR
/home/…/carla-ros/drive.sh

# it does, in order:
#   1. launch CARLA with its GUI window on the GPU
#   2. open the manual-control window and spawn the drivable car 'hero'
#   3. read the loaded map name, start carla_ros_bridge with town:=<that map> (no reload)
#   4. attach the LiDAR to 'hero' -> /carla/hero/lidar

# stop everything, in reverse: bridge, then driver, then the engine
pkill -9 -f carla-ros/install       # bridge + sensor nodes
pkill -9 -f manual_control.py       # the driver window
pkill -9 -f CarlaUE4-Linux-Shipping # CARLA itself

One engine per port  Only one CARLA can hold the control port at a time. A second launch while one is still up fails to bind the port and takes the new process down with a signal 11. If a start mysteriously dies, check for a stale engine still holding the port before anything else.

6. Where it stands, and what's next

Status: the source end of the pipeline is now the real thing. CARLA runs on a proper GPU workstation, a car I drive by hand streams its LiDAR live to ROS 2 through the official carla_ros_bridge, and the dashboard reads that stream through the standard community client instead of hand-written socket code. Two of the compromises the project has carried since the start, a GPU-less machine and a custom bridge, are retired.

Week 10 put the application on the board. This week put a real car, a real GPU, and the real bridge at the other end of the pipe. The two ends now exist and both are genuine. Next week is about the cable between them.

← Week 10
Home →