Skip to content

Commit 13868ea

Browse files
committed
feat(LibCarla/ros2): publish traffic light info and status as latched topics
A driving stack running against the native ROS 2 interface is blind to traffic lights: the simulator knows every light and its state, but none of it reaches the ROS graph, so planners fall back on the Python API or on the deprecated external bridge. The simulator now describes its traffic lights on two latched topics whenever ROS 2 is enabled. /carla/traffic_lights/info is published once per episode and carries the static picture of every light: its id, its pose in the world and its trigger volume, the same data the Python API exposes. /carla/traffic_lights/status carries the state of every light and is republished only when a state changes; since the last sample is latched, a subscriber that joins between changes still receives the current state immediately. Both topics reuse the message definitions already published in ros-carla-msgs, so nothing new is required on the ROS side and no flag beyond --ros2 is involved. As the closing piece of the data-output series, this also adds a reference of every topic the native interface publishes and subscribes to under Docs/ros2/topics.md, together with the consolidated changelog entry for the series.
1 parent aaf1f1c commit 13868ea

21 files changed

Lines changed: 995 additions & 2 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
## Latest Changes
2+
* Extended the native ROS2 interface (`--ros2`) with the data outputs standard AD stacks need: a latched OpenDRIVE map on `/carla/map`, ego vehicle odometry, status and info topics, a REP-105 TF tree (`map -> odom -> vehicle -> sensors`), and always-on traffic light info/status topics. Every topic uses standard or ros-carla-msgs message definitions and no new CARLA flags are introduced; see `Docs/ros2/topics.md` for the full topic reference.
23
* Added Zenoh as a third ROS 2 middleware backend, selectable at runtime via `--rmw=zenoh` alongside `--rmw=fastdds` and `--rmw=cyclonedds`.
34
* Renamed the ROS2 abstraction layer from dds/DDS* to generic middleware/Middleware* naming to support future non-DDS backends like Zenoh. FastDDS and CycloneDDS vendor classes are unchanged.
45
* Added NumPy 2 compatibility to the PythonAPI: replaced removed aliases (`np.bool`, `np.matrix`) in example scripts and upgraded Boost to 1.90.0, which ships the upstream NumPy 2 C ABI fix (boostorg/python#432) so the C extension builds against both NumPy 1.x (>=1.18.4) and NumPy 2.x

Docs/ros2/topics.md

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# Native ROS 2 topic reference
2+
3+
This page lists every topic the native ROS 2 interface publishes and
4+
subscribes to when CARLA runs with `--ros2` (middleware selectable with
5+
`--rmw=fastdds|cyclonedds|zenoh`). The interface follows one scope rule:
6+
it exposes the data a real drive-by-wire vehicle would produce and accepts
7+
the control commands such a vehicle would accept. Simulation control
8+
(spawning, weather, synchronous ticking) stays in the Python API.
9+
10+
Standard message types ship with every ROS 2 distribution. The
11+
`carla_msgs/*` types are the ones published from the bridge era in
12+
[ros-carla-msgs](https://github.com/carla-simulator/ros-carla-msgs);
13+
install that package on the ROS side to deserialize them.
14+
15+
Topic names below are the ROS names. On the DDS wire each topic carries
16+
the standard `rt/` prefix (for example `rt/carla/map`), which ROS 2 tools
17+
hide.
18+
19+
## Simulation-wide topics
20+
21+
Published as soon as the simulator starts; no actor opt-in needed.
22+
23+
| Topic | Type | QoS | Cadence |
24+
|-------|------|-----|---------|
25+
| `/clock` | `rosgraph_msgs/Clock` | volatile | every frame |
26+
| `/carla/map` | `std_msgs/String` | transient_local | once per episode |
27+
| `/carla/traffic_lights/info` | `carla_msgs/CarlaTrafficLightInfoList` | transient_local | once per episode |
28+
| `/carla/traffic_lights/status` | `carla_msgs/CarlaTrafficLightStatusList` | transient_local | on state change |
29+
| `/tf_static` | `tf2_msgs/TFMessage` | transient_local | on hero registration |
30+
| `/tf` | `tf2_msgs/TFMessage` | volatile | every frame |
31+
32+
- `/carla/map` carries the full OpenDRIVE description of the current map,
33+
the same string `carla.Map.to_opendrive()` returns. Latched, so late
34+
joiners receive it without waiting for a map change.
35+
- `/carla/traffic_lights/info` describes every traffic light once per
36+
episode: actor id, world pose, and the trigger volume as a center
37+
relative to that pose plus a full box size, matching what the Python API
38+
exposes as `actor.trigger_volume`.
39+
- `/carla/traffic_lights/status` carries the state of every light
40+
(`RED=0, YELLOW=1, GREEN=2, OFF=3, UNKNOWN=4`). The full list is
41+
republished only when at least one light changed state; the latched
42+
sample keeps the current state available to late joiners between
43+
changes.
44+
45+
## Ego vehicle topics
46+
47+
Created when a vehicle with `role_name` `hero` is spawned. `<ros_name>`
48+
is the vehicle's `ros_name` attribute (defaults to `actor<id>` when
49+
unset). Destroying the vehicle removes the topics.
50+
51+
| Topic | Direction | Type | QoS | Cadence |
52+
|-------|-----------|------|-----|---------|
53+
| `/carla/<ros_name>/odometry` | publish | `nav_msgs/Odometry` | volatile | every frame |
54+
| `/carla/<ros_name>/vehicle_status` | publish | `carla_msgs/CarlaEgoVehicleStatus` | volatile | every frame |
55+
| `/carla/<ros_name>/vehicle_info` | publish | `carla_msgs/CarlaEgoVehicleInfo` | transient_local | once at spawn |
56+
| `/carla/<ros_name>/vehicle_control_cmd` | subscribe | `carla_msgs/CarlaEgoVehicleControl` | volatile | consumer-driven |
57+
| `/carla/<ros_name>/ackermann_control_cmd` | subscribe | `ackermann_msgs/AckermannDriveStamped` | volatile | consumer-driven |
58+
59+
- Odometry pose is ground truth in ROS coordinates with
60+
`frame_id: odom` and `child_frame_id: <ros_frame_id>`; the twist is
61+
expressed in the vehicle body frame.
62+
- Vehicle status carries speed, acceleration (computed from the velocity
63+
history), world orientation, and an echo of the last applied control.
64+
- Vehicle info is the latched static description from the vehicle physics
65+
control: mass, wheels, gearbox, center of mass.
66+
67+
## TF tree
68+
69+
The transform topics compose a REP-105 tree:
70+
71+
```
72+
map -> odom -> <vehicle frame> -> <sensor frames>
73+
```
74+
75+
- `map -> odom`: identity, latched on `/tf_static` when the hero vehicle
76+
registers. CARLA publishes ground truth, so the `odom` frame never
77+
drifts from `map`.
78+
- `odom -> <vehicle frame>`: published on `/tf` every frame for the hero
79+
vehicle.
80+
- `<parent frame> -> <sensor frame>`: published on `/tf` every frame for
81+
each sensor with ROS 2 enabled, where the parent is the actor the
82+
sensor is attached to (or `map` for unattached sensors).
83+
84+
A sensor's frame defaults to its `ros_name` and can be overridden with
85+
the `ros_frame_id` attribute; per-actor TF publishing can be disabled
86+
with the `ros_publish_tf` attribute.
87+
88+
## Sensor topics
89+
90+
Sensor publishing is opt-in per actor: call `enable_for_ros()` on the
91+
sensor from the Python API (`ros2_native.py` in `PythonAPI/examples/ros2`
92+
does this for a whole sensor stack). The base name composes the
93+
attachment hierarchy: `/carla/<parent ros_name>/<sensor ros_name>` for an
94+
attached sensor, `/carla/<sensor ros_name>` otherwise. All sensor topics
95+
are volatile and publish at the sensor's own tick rate.
96+
97+
| Sensor | Topics | Types |
98+
|--------|--------|-------|
99+
| RGB camera | `<base>/image`, `<base>/camera_info` | `sensor_msgs/Image`, `sensor_msgs/CameraInfo` |
100+
| Depth camera | `<base>/image`, `<base>/camera_info` | `sensor_msgs/Image`, `sensor_msgs/CameraInfo` |
101+
| Semantic segmentation camera | `<base>/image`, `<base>/camera_info` | `sensor_msgs/Image`, `sensor_msgs/CameraInfo` |
102+
| Instance segmentation camera | `<base>/image`, `<base>/camera_info` | `sensor_msgs/Image`, `sensor_msgs/CameraInfo` |
103+
| Normals camera | `<base>/image`, `<base>/camera_info` | `sensor_msgs/Image`, `sensor_msgs/CameraInfo` |
104+
| Optical flow camera | `<base>/image`, `<base>/camera_info` | `sensor_msgs/Image`, `sensor_msgs/CameraInfo` |
105+
| DVS camera | `<base>/image`, `<base>/camera_info`, `<base>/point_cloud` | `sensor_msgs/Image`, `sensor_msgs/CameraInfo`, `sensor_msgs/PointCloud2` |
106+
| LiDAR (ray cast) | `<base>/point_cloud` | `sensor_msgs/PointCloud2` |
107+
| Semantic LiDAR | `<base>/point_cloud` | `sensor_msgs/PointCloud2` |
108+
| Hybrid solid-state LiDAR | `<base>/point_cloud` | `sensor_msgs/PointCloud2` |
109+
| Radar | `<base>/point_cloud` | `sensor_msgs/PointCloud2` |
110+
| GNSS | `<base>` | `sensor_msgs/NavSatFix` |
111+
| IMU | `<base>` | `sensor_msgs/Imu` |
112+
| Collision | `<base>` | `carla_msgs/CarlaCollisionEvent` |
113+
114+
Lane invasion, obstacle detection and RSS sensors have no native ROS 2
115+
publisher; read them through the Python API.
116+
117+
## QoS summary
118+
119+
Every publisher is reliable with keep-last history. Topics marked
120+
transient_local above are latched with depth 1: a subscriber that joins
121+
late immediately receives the last published sample. Match the
122+
durability when echoing them, for example:
123+
124+
```sh
125+
ros2 topic echo --qos-durability transient_local --qos-reliability reliable /carla/map
126+
```

LibCarla/source/carla/ros2/ROS2.cpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
#include "publishers/CarlaSemanticLidarPublisher.h"
3939
#include "publishers/CarlaSSCameraPublisher.h"
4040
#include "publishers/CarlaStaticTransformPublisher.h"
41+
#include "publishers/CarlaTrafficLightInfoPublisher.h"
42+
#include "publishers/CarlaTrafficLightStatusPublisher.h"
4143
#include "publishers/CarlaTransformPublisher.h"
4244

4345
#include "subscribers/AckermannControlSubscriber.h"
@@ -568,6 +570,33 @@ void ROS2::ProcessVehicleInfo(
568570
it->second.info->Publish();
569571
}
570572

573+
void ROS2::ProcessTrafficLightStates(const std::vector<TrafficLightState> &states) {
574+
std::lock_guard<std::recursive_mutex> lock(_mutex);
575+
if (!_enabled) {
576+
return;
577+
}
578+
if (!_traffic_lights_status_publisher) {
579+
_traffic_lights_status_publisher = std::make_shared<CarlaTrafficLightStatusPublisher>();
580+
}
581+
// Write reports whether any light changed state since the last publish;
582+
// the latched sample keeps the previous list available to late joiners.
583+
if (_traffic_lights_status_publisher->Write(states)) {
584+
_traffic_lights_status_publisher->Publish();
585+
}
586+
}
587+
588+
void ROS2::ProcessTrafficLightInfo(const std::vector<TrafficLightInfo> &info) {
589+
std::lock_guard<std::recursive_mutex> lock(_mutex);
590+
if (!_enabled) {
591+
return;
592+
}
593+
if (!_traffic_lights_info_publisher) {
594+
_traffic_lights_info_publisher = std::make_shared<CarlaTrafficLightInfoPublisher>();
595+
}
596+
_traffic_lights_info_publisher->Write(info);
597+
_traffic_lights_info_publisher->Publish();
598+
}
599+
571600
void ROS2::Shutdown() {
572601
std::lock_guard<std::recursive_mutex> lock(_mutex);
573602
// Destroy publishers first so DataWriter unregister-dispose messages are
@@ -581,6 +610,8 @@ void ROS2::Shutdown() {
581610
_clock_publisher.reset();
582611
_map_publisher.reset();
583612
_static_tf_publisher.reset();
613+
_traffic_lights_info_publisher.reset();
614+
_traffic_lights_status_publisher.reset();
584615

585616
_subscribers.clear();
586617

LibCarla/source/carla/ros2/ROS2.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include "carla/BufferView.h"
1111
#include "carla/geom/Transform.h"
1212
#include "carla/ros2/ROS2CallbackData.h"
13+
#include "carla/ros2/TrafficLightData.h"
1314
#include "carla/ros2/middleware/Middleware.h"
1415
#include "carla/streaming/detail/Types.h"
1516

@@ -52,6 +53,8 @@ namespace ros2 {
5253
class CarlaEgoVehicleStatusPublisher;
5354
class CarlaEgoVehicleInfoPublisher;
5455
class CarlaStaticTransformPublisher;
56+
class CarlaTrafficLightInfoPublisher;
57+
class CarlaTrafficLightStatusPublisher;
5558

5659
class ROS2
5760
{
@@ -171,6 +174,12 @@ class ROS2
171174
const std::string &role_name,
172175
const carla::geom::Transform vehicle_transform,
173176
const carla::rpc::VehiclePhysicsControl &physics_control);
177+
// Publishes the full traffic light status list, but only when any light
178+
// changed state since the last publish. Called once per frame.
179+
void ProcessTrafficLightStates(const std::vector<TrafficLightState> &states);
180+
// Publishes the latched static description of every traffic light.
181+
// Called once per episode, after the lights spawn.
182+
void ProcessTrafficLightInfo(const std::vector<TrafficLightInfo> &info);
174183

175184
private:
176185
std::shared_ptr<CarlaTransformPublisher> GetOrCreateTransformPublisher(void *actor);
@@ -217,6 +226,9 @@ class ROS2
217226
};
218227
std::unordered_map<void *, VehiclePublishers> _vehicle_publishers;
219228
std::shared_ptr<CarlaStaticTransformPublisher> _static_tf_publisher;
229+
230+
std::shared_ptr<CarlaTrafficLightInfoPublisher> _traffic_lights_info_publisher;
231+
std::shared_ptr<CarlaTrafficLightStatusPublisher> _traffic_lights_status_publisher;
220232
};
221233

222234
} // namespace ros2
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) 2026 Computer Vision Center (CVC) at the Universitat Autonoma de Barcelona (UAB).
2+
// This work is licensed under the terms of the MIT license.
3+
// For a copy, see <https://opensource.org/licenses/MIT>.
4+
5+
#pragma once
6+
7+
#include <cstdint>
8+
9+
#include "carla/geom/Location.h"
10+
#include "carla/geom/Transform.h"
11+
#include "carla/geom/Vector3D.h"
12+
13+
namespace carla {
14+
namespace ros2 {
15+
16+
/// Snapshot of one traffic light's state, gathered by the simulator each
17+
/// frame. The state encoding follows carla_msgs/CarlaTrafficLightStatus:
18+
/// RED=0, YELLOW=1, GREEN=2, OFF=3, UNKNOWN=4 (same ordinals as
19+
/// carla::rpc::TrafficLightState).
20+
struct TrafficLightState {
21+
uint32_t id = 0u;
22+
uint8_t state = 0u;
23+
};
24+
25+
/// Static description of one traffic light, gathered once per episode.
26+
/// All values use UE coordinates in meters; the ROS conversion happens in
27+
/// the publisher.
28+
struct TrafficLightInfo {
29+
uint32_t id = 0u;
30+
/// World pose of the light.
31+
carla::geom::Transform transform;
32+
/// Trigger volume center, relative to the light transform.
33+
carla::geom::Location trigger_center;
34+
/// Trigger volume half-extent.
35+
carla::geom::Vector3D trigger_extent;
36+
};
37+
38+
} // namespace ros2
39+
} // namespace carla
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c) 2026 Computer Vision Center (CVC) at the Universitat Autonoma de Barcelona (UAB).
2+
// This work is licensed under the terms of the MIT license.
3+
// For a copy, see <https://opensource.org/licenses/MIT>.
4+
5+
#include "CarlaTrafficLightInfoPublisher.h"
6+
7+
#include "carla/ros2/publishers/UeToRosConversions.h"
8+
9+
namespace carla {
10+
namespace ros2 {
11+
12+
bool CarlaTrafficLightInfoPublisher::Write(
13+
const std::vector<TrafficLightInfo> &info) {
14+
auto message = _impl->GetMessage();
15+
16+
message->traffic_lights.clear();
17+
message->traffic_lights.reserve(info.size());
18+
for (const auto &light : info) {
19+
msg::CarlaTrafficLightInfo light_info;
20+
light_info.id = light.id;
21+
22+
light_info.transform.position.x = light.transform.location.x;
23+
light_info.transform.position.y = -light.transform.location.y;
24+
light_info.transform.position.z = light.transform.location.z;
25+
light_info.transform.orientation =
26+
ue_rotation_to_ros_quaternion(light.transform.rotation);
27+
28+
// Center stays relative to the light transform, flipped to right-handed;
29+
// the size is the full box extent and carries no axis sign.
30+
light_info.trigger_volume.center = ue_vector_to_ros_vector(light.trigger_center);
31+
light_info.trigger_volume.size.x = light.trigger_extent.x * 2.0;
32+
light_info.trigger_volume.size.y = light.trigger_extent.y * 2.0;
33+
light_info.trigger_volume.size.z = light.trigger_extent.z * 2.0;
34+
35+
message->traffic_lights.push_back(light_info);
36+
}
37+
38+
return true;
39+
}
40+
41+
} // namespace ros2
42+
} // namespace carla
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (c) 2026 Computer Vision Center (CVC) at the Universitat Autonoma de Barcelona (UAB).
2+
// This work is licensed under the terms of the MIT license.
3+
// For a copy, see <https://opensource.org/licenses/MIT>.
4+
5+
#pragma once
6+
7+
#include <memory>
8+
#include <vector>
9+
10+
#include "carla/ros2/TrafficLightData.h"
11+
#include "carla/ros2/publishers/BasePublisher.h"
12+
#include "carla/ros2/publishers/PublisherImpl.h"
13+
14+
#include "carla/ros2/types/msg/CarlaTrafficLightInfoList.h"
15+
16+
namespace carla {
17+
namespace ros2 {
18+
19+
/// Publishes the static description of every traffic light in the map as a
20+
/// latched carla_msgs/CarlaTrafficLightInfoList on
21+
/// rt/carla/traffic_lights/info. Published once per episode; the
22+
/// transient_local durability lets late-joining subscribers receive the
23+
/// list without CARLA re-publishing it.
24+
class CarlaTrafficLightInfoPublisher : public BasePublisher {
25+
public:
26+
struct InfoListMsgTraits {
27+
using msg_type = msg::CarlaTrafficLightInfoList;
28+
};
29+
30+
CarlaTrafficLightInfoPublisher() :
31+
BasePublisher("rt/carla/traffic_lights/info"),
32+
_impl(std::make_shared<PublisherImpl<InfoListMsgTraits>>()) {
33+
PublisherQos qos;
34+
qos.durability = DurabilityKind::TransientLocal;
35+
if (!_impl->Init(GetBaseTopicName(), qos)) {
36+
log_warning("CarlaTrafficLightInfoPublisher: Init failed for topic: ", GetBaseTopicName());
37+
}
38+
}
39+
40+
bool Publish() {
41+
return _impl->Publish();
42+
}
43+
44+
bool Write(const std::vector<TrafficLightInfo> &info);
45+
46+
private:
47+
std::shared_ptr<PublisherImpl<InfoListMsgTraits>> _impl;
48+
};
49+
50+
} // namespace ros2
51+
} // namespace carla
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) 2026 Computer Vision Center (CVC) at the Universitat Autonoma de Barcelona (UAB).
2+
// This work is licensed under the terms of the MIT license.
3+
// For a copy, see <https://opensource.org/licenses/MIT>.
4+
5+
#include "CarlaTrafficLightStatusPublisher.h"
6+
7+
namespace carla {
8+
namespace ros2 {
9+
10+
bool CarlaTrafficLightStatusPublisher::Write(
11+
const std::vector<TrafficLightState> &states) {
12+
if (_has_last_states && StatesEqual(states, _last_states)) {
13+
return false;
14+
}
15+
auto message = _impl->GetMessage();
16+
17+
message->traffic_lights.clear();
18+
message->traffic_lights.reserve(states.size());
19+
for (const auto &state : states) {
20+
msg::CarlaTrafficLightStatus status;
21+
status.id = state.id;
22+
status.state = state.state;
23+
message->traffic_lights.push_back(status);
24+
}
25+
26+
_last_states = states;
27+
_has_last_states = true;
28+
return true;
29+
}
30+
31+
} // namespace ros2
32+
} // namespace carla

0 commit comments

Comments
 (0)