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,5 +1,6 @@
## Latest Changes
* Added Zenoh as a third ROS 2 middleware backend, selectable at runtime via `--rmw=zenoh` alongside `--rmw=fastdds` and `--rmw=cyclonedds`.
* Added a `--ros-domain-id=<N>` server option to set the ROS2 domain ID at startup. The value (0 to 232) is stored in the middleware abstraction layer and honored by every middleware (FastDDS, CycloneDDS, and Zenoh), so future middlewares pick it up automatically. When the option is omitted, the server falls back to the `ROS_DOMAIN_ID` environment variable, and then to the default domain 0. The resolution order is: `--ros-domain-id`, then `ROS_DOMAIN_ID`, then 0.
* 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
1 change: 1 addition & 0 deletions Docs/ext_quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,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`, `zenoh`. Default: `fastdds`. Only valid together with `--ros2`. CycloneDDS and Zenoh are Linux only; on Windows only `fastdds` is supported.
* `--ros-domain-id=<N>` - Set the ROS2 domain id (`N` must be an integer in the range 0-232) used by the native connector. Only valid together with `--ros2`. When omitted, the server falls back to the `ROS_DOMAIN_ID` environment variable if it is defined, and otherwise uses the default domain 0. The resolution order is `--ros-domain-id`, then `ROS_DOMAIN_ID`, then 0. Out-of-range values (on either the option or the environment variable) are ignored and the next source is used.
* `-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
13 changes: 11 additions & 2 deletions LibCarla/source/carla/ros2/ROS2.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ enum ESensors {
HSSLidar
};

bool ROS2::Enable(bool enable, Middleware middleware) {
bool ROS2::Enable(bool enable, Middleware middleware, int domain_id) {
std::lock_guard<std::recursive_mutex> lock(_mutex);
if (enable) {
auto resolve = MiddlewareFactory::ResolveMiddleware(middleware);
Expand All @@ -80,8 +80,17 @@ bool ROS2::Enable(bool enable, Middleware middleware) {
return false;
}
MiddlewareFactory::SetMiddleware(middleware);
// Configure the domain id before any transport context is created (the
// shared participants are created lazily on first publisher/subscriber).
MiddlewareConfig::SetDomainId(domain_id);
const ResolvedDomainId resolved = MiddlewareConfig::ResolveEffective();
const char* domain_source =
(resolved.source == DomainIdSource::CommandLine) ? "--ros-domain-id"
: (resolved.source == DomainIdSource::Environment) ? "ROS_DOMAIN_ID"
: "default";
log_info("ROS2: using middleware: ",
MiddlewareToString(middleware));
MiddlewareToString(middleware), ", domain id: ", resolved.id,
" (", domain_source, ")");
_clock_publisher = std::make_shared<CarlaClockPublisher>();
}
_enabled = enable;
Expand Down
6 changes: 5 additions & 1 deletion LibCarla/source/carla/ros2/ROS2.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "carla/geom/Transform.h"
#include "carla/ros2/ROS2CallbackData.h"
#include "carla/ros2/middleware/Middleware.h"
#include "carla/ros2/middleware/MiddlewareConfig.h"
#include "carla/streaming/detail/Types.h"

#include <mutex>
Expand Down Expand Up @@ -59,7 +60,10 @@ class ROS2
// General
// Returns true when enabling succeeds (middleware compiled in), false otherwise.
// Callers pass enable=false to shut down; the return value is always true in that case.
bool Enable(bool enable, Middleware middleware = Middleware::FastDDS);
// domain_id selects the ROS 2 domain id for the chosen middleware; kUnsetDomainId
// (the default) keeps each middleware's native default.
bool Enable(bool enable, Middleware middleware = Middleware::FastDDS,
int domain_id = kUnsetDomainId);
void Shutdown();

bool IsEnabled() { return _enabled; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ namespace ros2 {
/// Type-erased abstract interface for a publisher middleware.
/// Concrete implementations handle all vendor-specific entity creation,
/// type registration, and data writing.
///
/// Contract: when an implementation establishes its process-wide transport
/// context (a DDS DomainParticipant, a Zenoh session, ...) it must honor
/// carla::ros2::MiddlewareConfig::GetDomainId(), mapping kUnsetDomainId to its
/// own native default. This keeps the ROS 2 domain id configurable and
/// middleware-agnostic (see MiddlewareConfig.h).
class IPublisherMiddleware {
public:
virtual ~IPublisherMiddleware() = default;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ namespace ros2 {
/// Type-erased abstract interface for a subscriber middleware.
/// Concrete implementations write received messages directly into the caller-provided
/// storage (message_ptr / new_message_flag) to avoid an extra copy.
///
/// Contract: when an implementation establishes its process-wide transport
/// context (a DDS DomainParticipant, a Zenoh session, ...) it must honor
/// carla::ros2::MiddlewareConfig::GetDomainId(), mapping kUnsetDomainId to its
/// own native default. This keeps the ROS 2 domain id configurable and
/// middleware-agnostic (see MiddlewareConfig.h).
class ISubscriberMiddleware {
public:
virtual ~ISubscriberMiddleware() = default;
Expand Down
143 changes: 143 additions & 0 deletions LibCarla/source/carla/ros2/middleware/MiddlewareConfig.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// 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 <cstdlib>

namespace carla {
namespace ros2 {

/// Sentinel domain id meaning "no domain id was configured on the command line";
/// each middleware falls back to its own native default.
constexpr int kUnsetDomainId = -1;

/// Lowest valid ROS 2 domain id.
constexpr int kMinDomainId = 0;

/// Highest valid ROS 2 domain id (RTPS maximum).
constexpr int kMaxDomainId = 232;

/// Domain id used when neither the command line nor the environment provides one.
constexpr int kDefaultDomainId = 0;

/// Name of the standard ROS 2 environment variable that selects the domain id.
constexpr char kRosDomainIdEnvVar[] = "ROS_DOMAIN_ID";

/// @return true if @a id is a valid ROS 2 domain id (0..232).
/// The unset sentinel is intentionally NOT valid: callers test for it explicitly.
inline bool IsValidDomainId(int id) {
return id >= kMinDomainId && id <= kMaxDomainId;
}

/// Where the effective domain id came from, in priority order.
enum class DomainIdSource {
CommandLine, ///< From the --ros-domain-id command line option.
Environment, ///< From the ROS_DOMAIN_ID environment variable.
Default ///< Neither was set; the default domain (0) is used.
};

/// The resolved domain id together with the source it came from.
struct ResolvedDomainId {
int id;
DomainIdSource source;
};

/// Parse a ROS_DOMAIN_ID-style string into a valid domain id.
/// @return true and writes @a out only when @a text is a base-10 integer that is
/// a valid domain id (0..232). nullptr, empty, non-numeric, trailing-garbage and
/// out-of-range inputs return false and leave @a out untouched.
inline bool TryParseDomainId(const char* text, int& out) {
if (text == nullptr) {
return false;
}
char* end = nullptr;
const long value = std::strtol(text, &end, 10);
if (end == text) {
return false; // No digits were consumed.
}
while (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r') {
++end;
}
if (*end != '\0') {
return false; // Trailing non-numeric characters.
}
if (value < kMinDomainId || value > kMaxDomainId) {
return false;
}
out = static_cast<int>(value);
return true;
}

/// Resolve the effective domain id from a command line value and an environment
/// value, in priority order:
/// 1. @a cli_value, when it is a valid domain id (0..232).
/// 2. @a env_value (e.g. ROS_DOMAIN_ID), when it parses to a valid domain id.
/// 3. kDefaultDomainId (0).
/// This function is pure (no environment access), so the precedence rules can be
/// unit-tested by passing the environment value as a string.
inline ResolvedDomainId ResolveDomainId(int cli_value, const char* env_value) {
if (IsValidDomainId(cli_value)) {
return {cli_value, DomainIdSource::CommandLine};
}
int parsed = 0;
if (TryParseDomainId(env_value, parsed)) {
return {parsed, DomainIdSource::Environment};
}
return {kDefaultDomainId, DomainIdSource::Default};
}

/// Process-wide runtime configuration shared by every ROS 2 middleware
/// implementation (FastDDS, CycloneDDS, and future ones such as Zenoh).
///
/// This belongs to the middleware abstraction layer, not to any single vendor:
/// each concrete middleware reads this configuration when it establishes its
/// transport context (a DDS DomainParticipant, a Zenoh session, ...) and
/// translates the abstract values into its own mechanism, mapping
/// kUnsetDomainId to its native default.
///
/// Set once at startup (via ROS2::Enable) before any publisher or subscriber is
/// created. Reads after that point are stable. Because it is set before any
/// entity exists, no synchronisation is required.
class MiddlewareConfig {
public:
/// Configure the ROS 2 domain id for all subsequent transport contexts.
/// Pass kUnsetDomainId to keep each middleware's native default.
static void SetDomainId(int id) {
Storage().domain_id = id;
}

/// @return The configured domain id, or kUnsetDomainId if none was set.
static int GetDomainId() {
return Storage().domain_id;
}

/// Resolve the effective domain id and its source from the configured value,
/// the ROS_DOMAIN_ID environment variable, and the default. See ResolveDomainId
/// for the priority order.
static ResolvedDomainId ResolveEffective() {
return ResolveDomainId(GetDomainId(), std::getenv(kRosDomainIdEnvVar));
}

/// @return The effective domain id every middleware should use, after applying
/// the command-line value, the ROS_DOMAIN_ID environment variable and the
/// default in that order.
static int GetEffectiveDomainId() {
return ResolveEffective().id;
}

private:
struct Data {
int domain_id = kUnsetDomainId;
};

/// Function-local static: a single instance shared across all translation units.
static Data& Storage() {
static Data data;
return data;
}
};

} // namespace ros2
} // namespace carla
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#ifdef CARLA_ROS2_MIDDLEWARE_CYCLONEDDS

#include "carla/ros2/middleware/cyclonedds/CycloneDDSSertype.h"
#include "carla/ros2/middleware/MiddlewareConfig.h"
#include "carla/Logging.h"

#include <cstdlib>
Expand Down Expand Up @@ -283,8 +284,14 @@ const struct ddsi_sertype_ops carla_cdr_sertype_ops = {
dds_entity_t carla_cdr_get_participant() {
// Function-local static: created once on first call, thread-safe in C++11.
// The participant lives until process exit; CycloneDDS cleans up internally.
static dds_entity_t participant =
dds_create_participant(DDS_DOMAIN_DEFAULT, nullptr, nullptr);
// Use the effective domain id resolved by the abstraction layer
// (--ros-domain-id, else ROS_DOMAIN_ID, else the default domain 0), so the
// domain selection matches FastDDS instead of relying on DDS_DOMAIN_DEFAULT.
static dds_entity_t participant = []() {
const dds_domainid_t domain_id =
static_cast<dds_domainid_t>(MiddlewareConfig::GetEffectiveDomainId());
return dds_create_participant(domain_id, nullptr, nullptr);
}();
return participant;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#ifndef CARLA_ROS2_MIDDLEWARE_TESTING

#include "carla/ros2/middleware/fastdds/FastDDSSharedParticipant.h"
#include "carla/ros2/middleware/MiddlewareConfig.h"
#include "carla/Logging.h"

#include <fastdds/dds/domain/DomainParticipantFactory.hpp>
Expand All @@ -25,8 +26,12 @@ efd::DomainParticipant* FastDDSSharedParticipant::acquire() {
std::lock_guard<std::mutex> lock(_mutex);
if (_refcount == 0u) {
efd::DomainParticipantQos pqos = efd::PARTICIPANT_QOS_DEFAULT;
// Use the effective domain id resolved by the abstraction layer
// (--ros-domain-id, else ROS_DOMAIN_ID, else the default domain 0).
const uint32_t domain_id =
static_cast<uint32_t>(MiddlewareConfig::GetEffectiveDomainId());
_participant =
efd::DomainParticipantFactory::get_instance()->create_participant(0, pqos);
efd::DomainParticipantFactory::get_instance()->create_participant(domain_id, pqos);
if (_participant == nullptr) {
log_error("FastDDSSharedParticipant: Failed to create DomainParticipant");
return nullptr;
Expand Down
37 changes: 12 additions & 25 deletions LibCarla/source/carla/ros2/middleware/zenoh/ZenohWireFormat.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@

#pragma once

#ifndef CARLA_ROS2_MIDDLEWARE_TESTING
#ifdef CARLA_ROS2_MIDDLEWARE_ZENOH
// Pure keyexpr/wire-format helpers with no zenoh-c dependency; also compiled
// under CARLA_ROS2_MIDDLEWARE_TESTING so unit tests cover them.
#if defined(CARLA_ROS2_MIDDLEWARE_ZENOH) || defined(CARLA_ROS2_MIDDLEWARE_TESTING)

#include "carla/Logging.h"
#include "carla/ros2/middleware/MiddlewareConfig.h"

#include <atomic>
#include <cstdint>
#include <cstdlib>
#include <string>
#include <vector>

Expand All @@ -35,25 +35,13 @@ inline uint64_t zenoh_next_entity_id() {
return counter.fetch_add(1, std::memory_order_relaxed);
}

/// ROS_DOMAIN_ID resolved once at startup. Populates the leading segment of
/// every keyexpr so we only match rmw_zenoh peers on the same domain.
/// Falls back to "0" when the env var is unset or non-numeric, matching
/// rclcpp/rclpy and the hardcoded domain used by the FastDDS/CycloneDDS
/// middleware.
inline const std::string& zenoh_ros_domain_id() {
static const std::string id = [] {
const char* env = std::getenv("ROS_DOMAIN_ID");
if (env == nullptr || env[0] == '\0') return std::string("0");
for (const char* p = env; *p; ++p) {
if (*p < '0' || *p > '9') {
log_warning("ROS_DOMAIN_ID='", env,
"' is not numeric; falling back to 0");
return std::string("0");
}
}
return std::string(env);
}();
return id;
/// Effective ROS 2 domain id as a string, resolved by the middleware
/// abstraction layer (--ros-domain-id, else ROS_DOMAIN_ID, else the default
/// domain 0). Populates the leading segment of every keyexpr so we only match
/// rmw_zenoh peers on the same domain, mirroring the domain id the
/// FastDDS/CycloneDDS middlewares pass to their DomainParticipant.
inline std::string zenoh_ros_domain_id() {
return std::to_string(MiddlewareConfig::GetEffectiveDomainId());
}

/// Strip the "rt/" prefix that ROS2-to-DDS mapping adds to message topics.
Expand Down Expand Up @@ -129,5 +117,4 @@ inline std::vector<uint8_t> zenoh_attachment() {
} // namespace ros2
} // namespace carla

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