Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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