Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
## Latest Changes
* Added Zenoh as a third ROS 2 middleware backend, selectable at runtime via `--rmw=zenoh` alongside `--rmw=fastdds` and `--rmw=cyclonedds`.
* 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.
* 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
* Fixed North/South latitude inversion in geo-coordinate conversion for Transverse Mercator and UTM projections
Expand Down
2 changes: 1 addition & 1 deletion Docs/ext_quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ There are some configuration options available when launching CARLA and they can
```

* `--ros2` - Launch CARLA with the native ROS2 connector enabled
* `--rmw=<middleware>` - Select the ROS2 middleware (RMW) used by the native connector. Accepted values: `fastdds`, `cyclonedds`. Default: `fastdds`. Only valid together with `--ros2`. CycloneDDS is Linux only; on Windows only `fastdds` is supported.
* `--rmw=<middleware>` - Select the ROS2 middleware (RMW) used by the native connector. Accepted values: `fastdds`, `cyclonedds`, `zenoh`. Default: `fastdds`. Only valid together with `--ros2`. CycloneDDS and Zenoh are Linux only; on Windows only `fastdds` is supported.
* `-carla-rpc-port=N` - Listen for client connections at port `N`. Streaming port is set to `N+1` by default.
* `-carla-streaming-port=N` - Specify the port for sensor data streaming. Use 0 to get a random unused port. The second port will be automatically set to `N+1`.
* `-quality-level={Low,Epic}` - Change graphics quality level. Find out more in [rendering options](adv_rendering_options.md).
Expand Down
1 change: 1 addition & 0 deletions LibCarla/cmake/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ elseif (CMAKE_BUILD_TYPE STREQUAL "ros2")
add_subdirectory("ros2")
add_subdirectory("fast_dds")
add_subdirectory("cyclone_dds")
add_subdirectory("zenoh")
else ()
message(FATAL_ERROR "Unknown build type '${CMAKE_BUILD_TYPE}'")
endif ()
Expand Down
19 changes: 12 additions & 7 deletions LibCarla/cmake/ros2/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
cmake_minimum_required(VERSION 3.5.1)
project(libcarla_ros2)

# All ros2 source files compiled ONCE with both middleware macros defined.
# All ros2 source files compiled ONCE with every middleware macro defined.
# This ensures ROS2::Enable, MiddlewareFactory::IsMiddlewareAvailable,
# and all CreatePublisher<T>/CreateSubscriber<S> instantiations see both
# CARLA_ROS2_MIDDLEWARE_FASTDDS and CARLA_ROS2_MIDDLEWARE_CYCLONEDDS at compile time.
# cmake/fast_dds and cmake/cyclone_dds are install-only (no library targets);
# and all CreatePublisher<T>/CreateSubscriber<S> instantiations see
# CARLA_ROS2_MIDDLEWARE_FASTDDS, CARLA_ROS2_MIDDLEWARE_CYCLONEDDS, and CARLA_ROS2_MIDDLEWARE_ZENOH at compile time.
# cmake/fast_dds, cmake/cyclone_dds, cmake/zenoh are install-only (no library targets);
# all compiled code lives here.

file(GLOB libcarla_ros2_sources
Expand All @@ -15,7 +15,8 @@ file(GLOB libcarla_ros2_sources
"${libcarla_source_path}/carla/ros2/listeners/*.cpp"
"${libcarla_source_path}/carla/ros2/types/*.cpp"
"${libcarla_source_path}/carla/ros2/middleware/fastdds/*.cpp"
"${libcarla_source_path}/carla/ros2/middleware/cyclonedds/*.cpp")
"${libcarla_source_path}/carla/ros2/middleware/cyclonedds/*.cpp"
"${libcarla_source_path}/carla/ros2/middleware/zenoh/*.cpp")

# ==============================================================================
# Create targets for debug and release in the same build type.
Expand All @@ -26,7 +27,8 @@ if (LIBCARLA_BUILD_RELEASE)

target_compile_definitions(carla_ros2 PRIVATE
CARLA_ROS2_MIDDLEWARE_FASTDDS
CARLA_ROS2_MIDDLEWARE_CYCLONEDDS)
CARLA_ROS2_MIDDLEWARE_CYCLONEDDS
CARLA_ROS2_MIDDLEWARE_ZENOH)
target_compile_options(carla_ros2 PRIVATE -fexceptions)

target_include_directories(carla_ros2 SYSTEM PRIVATE
Expand All @@ -36,6 +38,7 @@ if (LIBCARLA_BUILD_RELEASE)
target_include_directories(carla_ros2 PRIVATE
"${FASTDDS_INCLUDE_PATH}"
"${CYCLONEDDS_INCLUDE_PATH}"
"${ZENOH_INCLUDE_PATH}"
"${libcarla_source_path}/carla/ros2")

install(TARGETS carla_ros2 DESTINATION lib)
Expand All @@ -48,7 +51,8 @@ if (LIBCARLA_BUILD_DEBUG)

target_compile_definitions(carla_ros2_debug PRIVATE
CARLA_ROS2_MIDDLEWARE_FASTDDS
CARLA_ROS2_MIDDLEWARE_CYCLONEDDS)
CARLA_ROS2_MIDDLEWARE_CYCLONEDDS
CARLA_ROS2_MIDDLEWARE_ZENOH)
target_compile_options(carla_ros2_debug PRIVATE -fexceptions)

target_include_directories(carla_ros2_debug SYSTEM PRIVATE
Expand All @@ -58,6 +62,7 @@ if (LIBCARLA_BUILD_DEBUG)
target_include_directories(carla_ros2_debug PRIVATE
"${FASTDDS_INCLUDE_PATH}"
"${CYCLONEDDS_INCLUDE_PATH}"
"${ZENOH_INCLUDE_PATH}"
"${libcarla_source_path}/carla/ros2")

install(TARGETS carla_ros2_debug DESTINATION lib)
Expand Down
29 changes: 29 additions & 0 deletions LibCarla/cmake/zenoh/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
cmake_minimum_required(VERSION 3.5.1)
project(libcarla_zenoh)

# Install headers.

file(GLOB libcarla_carla_ros2_headers "${libcarla_source_path}/carla/ros2/*.h")
file(GLOB libcarla_carla_publishers_headers "${libcarla_source_path}/carla/ros2/publishers/*.h")
file(GLOB libcarla_carla_subscribers_headers "${libcarla_source_path}/carla/ros2/subscribers/*.h")
file(GLOB libcarla_carla_listeners_headers "${libcarla_source_path}/carla/ros2/listeners/*.h")
file(GLOB libcarla_carla_types_headers "${libcarla_source_path}/carla/ros2/types/*.h")
file(GLOB libcarla_carla_types_msg_headers "${libcarla_source_path}/carla/ros2/types/msg/*.h")
file(GLOB libcarla_carla_ros2_middleware_headers "${libcarla_source_path}/carla/ros2/middleware/*.h")
file(GLOB libcarla_carla_ros2_middleware_zenoh_headers "${libcarla_source_path}/carla/ros2/middleware/zenoh/*.h")

install(FILES ${libcarla_carla_ros2_headers} DESTINATION include/carla/ros2)
install(FILES ${libcarla_carla_publishers_headers} DESTINATION include/carla/ros2/publishers)
install(FILES ${libcarla_carla_subscribers_headers} DESTINATION include/carla/ros2/subscribers)
install(FILES ${libcarla_carla_listeners_headers} DESTINATION include/carla/ros2/listeners)
install(FILES ${libcarla_carla_types_headers} DESTINATION include/carla/ros2/types)
install(FILES ${libcarla_carla_types_msg_headers} DESTINATION include/carla/ros2/types/msg)
install(FILES ${libcarla_carla_ros2_middleware_headers} DESTINATION include/carla/ros2/middleware)
install(FILES ${libcarla_carla_ros2_middleware_zenoh_headers} DESTINATION include/carla/ros2/middleware/zenoh)

file(GLOB zenoh_dependencies "${ZENOH_LIB_PATH}/*.a")
install(FILES ${zenoh_dependencies} DESTINATION lib)
install(DIRECTORY "${ZENOH_INCLUDE_PATH}/" DESTINATION include)

# Note: middleware/zenoh/ compiled sources (ZenohSharedSession.cpp) are included
# in cmake/ros2/ (carla_ros2), compiled once with both middleware macros defined.
14 changes: 13 additions & 1 deletion LibCarla/source/carla/ros2/middleware/Middleware.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ namespace ros2 {
/// Once set, the middleware cannot be changed without restarting.
enum class Middleware {
FastDDS,
CycloneDDS
CycloneDDS,
Zenoh
};

/// Convert a Middleware enum value to a readable string.
Expand All @@ -24,6 +25,8 @@ inline const char* MiddlewareToString(Middleware middleware) {
return "FastDDS";
case Middleware::CycloneDDS:
return "CycloneDDS";
case Middleware::Zenoh:
return "Zenoh";
}
return "Unknown";
}
Expand All @@ -43,6 +46,9 @@ inline MiddlewareParseResult MiddlewareFromString(const std::string& name) {
if (name == "cyclonedds") {
return {true, Middleware::CycloneDDS};
}
if (name == "zenoh") {
return {true, Middleware::Zenoh};
}
return {false, Middleware::FastDDS};
}

Expand All @@ -57,6 +63,12 @@ inline std::string GetAvailableMiddlewareString() {
result += ", ";
}
result += "CycloneDDS";
#endif
#if defined(CARLA_ROS2_MIDDLEWARE_ZENOH)
if (!result.empty()) {
result += ", ";
}
result += "Zenoh";
#endif
if (result.empty()) {
result = "none";
Expand Down
27 changes: 27 additions & 0 deletions LibCarla/source/carla/ros2/middleware/MiddlewareFactory.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
# include "carla/ros2/middleware/cyclonedds/CycloneDDSSubscriberMiddleware.h"
#endif

#if defined(CARLA_ROS2_MIDDLEWARE_ZENOH) && !defined(CARLA_ROS2_MIDDLEWARE_TESTING)
# include "carla/ros2/middleware/zenoh/ZenohPublisherMiddleware.h"
# include "carla/ros2/middleware/zenoh/ZenohSubscriberMiddleware.h"
#endif

namespace carla {
namespace ros2 {

Expand Down Expand Up @@ -55,6 +60,12 @@ class MiddlewareFactory {
return true;
#else
return false;
#endif
case Middleware::Zenoh:
#if defined(CARLA_ROS2_MIDDLEWARE_ZENOH)
return true;
#else
return false;
#endif
}
return false;
Expand Down Expand Up @@ -102,6 +113,14 @@ class MiddlewareFactory {
#else
log_error("MiddlewareFactory: CycloneDDS not compiled in");
return nullptr;
#endif
case Middleware::Zenoh:
#if defined(CARLA_ROS2_MIDDLEWARE_ZENOH) && !defined(CARLA_ROS2_MIDDLEWARE_TESTING)
return std::unique_ptr<IPublisherMiddleware>(
new ZenohPublisherMiddleware<T>());
#else
log_error("MiddlewareFactory: Zenoh not compiled in");
return nullptr;
#endif
}
return nullptr;
Expand All @@ -128,6 +147,14 @@ class MiddlewareFactory {
#else
log_error("MiddlewareFactory: CycloneDDS not compiled in");
return nullptr;
#endif
case Middleware::Zenoh:
#if defined(CARLA_ROS2_MIDDLEWARE_ZENOH) && !defined(CARLA_ROS2_MIDDLEWARE_TESTING)
return std::unique_ptr<ISubscriberMiddleware>(
new ZenohSubscriberMiddleware<S>());
#else
log_error("MiddlewareFactory: Zenoh not compiled in");
return nullptr;
#endif
}
return nullptr;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// Copyright (c) 2026 Computer Vision Center (CVC) at the Universitat Autonoma de Barcelona (UAB).
// This work is licensed under the terms of the MIT license.
// For a copy, see <https://opensource.org/licenses/MIT>.

#pragma once

#include "carla/ros2/middleware/IPublisherMiddleware.h"
#include "carla/ros2/middleware/zenoh/ZenohSharedSession.h"
#include "carla/ros2/middleware/zenoh/ZenohWireFormat.h"
#include "carla/ros2/types/CdrSerialization.h"
#include "carla/ros2/types/CdrTopicInfo.h"
#include "carla/Logging.h"

#include <atomic>
#include <mutex>
#include <string>
#include <vector>

#ifndef CARLA_ROS2_MIDDLEWARE_TESTING
#ifdef CARLA_ROS2_MIDDLEWARE_ZENOH

#include <zenoh.h>

namespace carla {
namespace ros2 {

/// Zenoh implementation of IPublisherMiddleware, compatible with rmw_zenoh.
///
/// Serializes msg::* POD structs to CDR via CdrSerialization.h and publishes
/// them on a keyexpr formatted per the rmw_zenoh design doc:
/// "<domain>/<topic>/<type>/<hash>". A liveliness token is declared so the
/// endpoint shows up in rmw_zenoh's graph cache.
///
/// Parameterized on a traits type T that provides:
/// T::msg_type — the message type (a carla::ros2::msg::* POD struct)
template<typename T>
class ZenohPublisherMiddleware : public IPublisherMiddleware {
public:
using msg_type = typename T::msg_type;

ZenohPublisherMiddleware() {
// Gravestone owned handles so destructor's z_drop is safe if Init never ran.
z_internal_null(&_publisher);
z_internal_null(&_liveliness_token);
z_internal_null(&_matching_listener);
}
ZenohPublisherMiddleware(const ZenohPublisherMiddleware&) = delete;
ZenohPublisherMiddleware& operator=(const ZenohPublisherMiddleware&) = delete;

~ZenohPublisherMiddleware() override {
// zenoh-c's z_drop on a publisher does not wait for in-flight z_publisher_put
// calls (unlike FastDDS/CycloneDDS writers, which synchronize internally), so
// serialize against Publish() to avoid a use-after-free when a render-thread
// publish races with sensor teardown on the main thread.
std::lock_guard<std::mutex> lock(_publish_mutex);
z_drop(z_move(_matching_listener));
z_drop(z_move(_liveliness_token));
z_drop(z_move(_publisher));
}

bool Init(const std::string& topic_name) override {
const z_loaned_session_t* session = zenoh_get_shared_session();

const std::string topic_no_rt = zenoh_strip_rt_prefix(topic_name);
const char* type_name = CdrTopicInfo<msg_type>::type_name();
const char* type_hash = CdrTopicInfo<msg_type>::type_hash();
if (type_hash == nullptr) {
log_error("ZenohPublisherMiddleware: no type_hash for topic '",
topic_name, "'");
return false;
}

const std::string ke_str = zenoh_make_topic_keyexpr(
zenoh_ros_domain_id(), topic_no_rt, type_name, type_hash);
z_view_keyexpr_t ke;
if (z_view_keyexpr_from_str(&ke, ke_str.c_str()) != Z_OK) {
log_error("ZenohPublisherMiddleware (", topic_name,
"): invalid keyexpr '", ke_str, "'");
return false;
}

z_publisher_options_t pub_opts;
z_publisher_options_default(&pub_opts);
if (z_declare_publisher(session, &_publisher, z_loan(ke), &pub_opts)
!= Z_OK) {
log_error("ZenohPublisherMiddleware (", topic_name,
"): z_declare_publisher failed");
return false;
}

const std::string lv_ke_str = zenoh_make_topic_liveliness_keyexpr(
zenoh_ros_domain_id(), zenoh_session_zid(), zenoh_next_entity_id(),
kZenohEntityKindPub, topic_no_rt, type_name, type_hash,
kZenohDefaultQos);
z_view_keyexpr_t lv_ke;
if (z_view_keyexpr_from_str(&lv_ke, lv_ke_str.c_str()) != Z_OK) {
log_error("ZenohPublisherMiddleware (", topic_name,
"): invalid liveliness keyexpr");
return false;
}
if (z_liveliness_declare_token(session, &_liveliness_token,
z_loan(lv_ke), nullptr) != Z_OK) {
log_error("ZenohPublisherMiddleware (", topic_name,
"): z_liveliness_declare_token failed");
return false;
}

z_owned_closure_matching_status_t closure;
z_closure_matching_status(&closure, &on_matching_status, nullptr, this);
if (z_publisher_declare_matching_listener(
z_loan(_publisher), &_matching_listener, z_move(closure))
!= Z_OK) {
log_error("ZenohPublisherMiddleware (", topic_name,
"): z_publisher_declare_matching_listener failed");
return false;
}

_topic_name = topic_name;
return true;
}

bool Publish(void* message_data) override {
std::lock_guard<std::mutex> lock(_publish_mutex);
if (!z_internal_check(_publisher)) {
return false;
}

const msg_type* msg = static_cast<const msg_type*>(message_data);
std::vector<uint8_t> cdr = serialize_to_cdr(*msg);

z_owned_bytes_t payload;
z_bytes_copy_from_buf(&payload, cdr.data(), cdr.size());

const std::vector<uint8_t> attach = zenoh_attachment();
z_owned_bytes_t attachment;
z_bytes_copy_from_buf(&attachment, attach.data(), attach.size());

z_publisher_put_options_t opts;
z_publisher_put_options_default(&opts);
opts.attachment = z_move(attachment);

if (z_publisher_put(z_loan(_publisher), z_move(payload), &opts) != Z_OK) {
log_error("ZenohPublisherMiddleware::Publish (", _topic_name,
"): z_publisher_put failed");
return false;
}
return true;
}

bool IsAlive() const override {
return _alive.load(std::memory_order_relaxed);
}

std::string GetTopicName() const override {
return _topic_name;
}

private:
static void on_matching_status(const z_matching_status_t* status,
void* context) {
auto* self = static_cast<ZenohPublisherMiddleware*>(context);
self->_alive.store(status->matching, std::memory_order_relaxed);
}

z_owned_publisher_t _publisher;
z_owned_liveliness_token_t _liveliness_token;
z_owned_matching_listener_t _matching_listener;

mutable std::mutex _publish_mutex;
std::atomic<bool> _alive { false };
std::string _topic_name;
};

} // namespace ros2
} // namespace carla

#endif // CARLA_ROS2_MIDDLEWARE_ZENOH
#endif // !CARLA_ROS2_MIDDLEWARE_TESTING
Loading
Loading