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
101 changes: 96 additions & 5 deletions crates/hiroz/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,8 @@ impl RemapRules {
}
}

#[derive(Default)]
pub struct ZContextBuilder {
domain_id: usize,
domain_id: DomainId,
namespace: String,
enclave: String,
zenoh_config: Option<zenoh::Config>,
Expand All @@ -86,10 +85,84 @@ pub struct ZContextBuilder {
clock: Option<ZClock>,
}

/// The builder's resolved (or pending) ROS domain id.
///
/// `Invalid` only exists between `ZContextBuilder::default()` and
/// `.build()`/`.with_domain_id()` -- a live `ZContext` always carries a
/// concrete `usize`. Modeling "haven't resolved an invalid ROS_DOMAIN_ID
/// yet" as a variant, rather than a `usize` plus a side-channel error
/// field, makes it a state the type carries instead of an invariant call
/// sites have to remember to check (and that `with_domain_id()` has to
/// remember to clear).
#[derive(Debug, Clone, PartialEq, Eq)]
enum DomainId {
/// A concrete domain, either explicit or read from `ROS_DOMAIN_ID`.
Value(usize),
/// `ROS_DOMAIN_ID` was set but is not a valid non-negative integer.
/// `build()` rejects this unless `.with_domain_id()` overrides it
/// first. Falling back to domain 0 silently would put a node on the
/// wrong ROS graph after an operator typo; `rcl_get_default_domain_id`
/// treats this the same way, returning an error that aborts
/// `rcl_init` rather than defaulting.
Invalid(String),
}

impl DomainId {
/// Matches `rclcpp`/`rclpy`: read `ROS_DOMAIN_ID` from the environment,
/// so the normal ROS 2 deployment story (set the env var, don't touch
/// source) works here too. `.with_domain_id()` called after
/// `default()` still overrides this, same precedence as every other
/// ROS 2 client library.
fn from_env() -> Self {
Self::parse(std::env::var("ROS_DOMAIN_ID").ok())
}

/// Pure parsing, taking the env var's value directly rather than
/// reading it -- so this is unit-testable without mutating (and
/// racing on) real process-global state.
fn parse(value: Option<String>) -> Self {
match value {
Some(val) => match val.parse::<usize>() {
Ok(id) => Self::Value(id),
Err(_) => Self::Invalid(val),
},
None => Self::Value(0),
}
}
}

impl std::fmt::Display for DomainId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Value(id) => write!(f, "{id}"),
Self::Invalid(raw) => write!(f, "<invalid ROS_DOMAIN_ID {raw:?}>"),
}
}
}

impl Default for ZContextBuilder {
fn default() -> Self {
Self {
domain_id: DomainId::from_env(),
namespace: String::default(),
enclave: String::default(),
zenoh_config: None,
config_file: None,
config_overrides: Vec::default(),
remap_rules: RemapRules::default(),
enable_logging: bool::default(),
shm_config: None,
keyexpr_format: hiroz_protocol::KeyExprFormat::default(),
clock: None,
}
}
}

impl ZContextBuilder {
/// Set the ROS domain ID
/// Set the ROS domain ID, overriding `ROS_DOMAIN_ID` (and any error
/// parsing it) with an explicit value.
pub fn with_domain_id(mut self, domain_id: usize) -> Self {
self.domain_id = domain_id;
self.domain_id = DomainId::Value(domain_id);
self
}

Expand Down Expand Up @@ -476,6 +549,15 @@ impl Builder for ZContextBuilder {
// 4. **NEW DEFAULT**: ROS session config (connects to router at tcp/localhost:7447)
// This matches rmw_zenoh_cpp behavior

let DomainId::Value(_) = &self.domain_id else {
return Err(format!(
"{}: not a valid non-negative integer; set ROS_DOMAIN_ID to a \
valid domain or call .with_domain_id() explicitly",
self.domain_id
)
.into());
};

debug!(
"[CTX] Building context: domain_id={}, has_config={}",
self.domain_id,
Expand Down Expand Up @@ -550,7 +632,9 @@ impl Builder for ZContextBuilder {
}
}

let domain_id = builder.domain_id;
let DomainId::Value(domain_id) = builder.domain_id else {
unreachable!("build() already rejected a non-Value domain_id above")
};
let graph = Arc::new(Graph::new(
&session,
domain_id,
Expand Down Expand Up @@ -667,6 +751,13 @@ impl ZContext {
&self.graph
}

/// The resolved ROS domain id this context was built with -- either
/// explicit via `.with_domain_id()`, from `ROS_DOMAIN_ID`, or the
/// default of 0.
pub fn domain_id(&self) -> usize {
self.domain_id
}

/// Access the context clock used by nodes and runtime helpers.
pub fn clock(&self) -> &ZClock {
&self.clock
Expand Down
49 changes: 37 additions & 12 deletions crates/hiroz/src/dynamic/type_description_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,11 @@ pub struct TypeDescriptionService {
}

impl TypeDescriptionService {
/// Create a new TypeDescriptionService.
/// Create a domain-0 TypeDescriptionService using the default key-expression format.
///
/// Nodes created by [`ZNodeBuilder`](crate::node::ZNodeBuilder) inherit their context via
/// [`Self::new_with_node`]. Use that constructor when creating this service manually for a
/// non-default domain or key-expression format.
///
/// # Arguments
///
Expand All @@ -455,6 +459,34 @@ impl TypeDescriptionService {
node_id: usize,
counter: &crate::context::GlobalCounter,
clock: &crate::time::ZClock,
) -> ZResult<Self> {
let node = crate::entity::NodeEntity::new(
0,
session.zid(),
node_id,
node_name.to_string(),
namespace.to_string(),
String::new(),
);
Self::new_with_node(
session,
node,
counter,
clock,
hiroz_protocol::KeyExprFormat::default(),
)
}

/// Create a TypeDescriptionService from an existing node identity and key-expression format.
///
/// `node.z_id` must identify `session`. Passing the owning node's entity avoids silently
/// resetting its domain, namespace, enclave, or other discovery identity.
pub fn new_with_node(
session: Arc<Session>,
node: crate::entity::NodeEntity,
counter: &crate::context::GlobalCounter,
clock: &crate::time::ZClock,
keyexpr_format: hiroz_protocol::KeyExprFormat,
) -> ZResult<Self> {
let schemas: Arc<RwLock<HashMap<String, RegisteredSchema>>> =
Arc::new(RwLock::new(HashMap::new()));
Expand All @@ -464,19 +496,12 @@ impl TypeDescriptionService {
// which expands to /{namespace}/{node_name}/get_type_description
let service_name = "~get_type_description";

// Create the node entity for the service
let node_entity = crate::entity::NodeEntity::new(
0, // domain_id
session.zid(),
node_id,
node_name.to_string(),
namespace.to_string(),
String::new(), // enclave (empty, normalized to "%" in liveliness token)
);
let node_name = node.name.clone();
let namespace = node.namespace.clone();

let entity = crate::entity::EndpointEntity {
id: counter.increment(),
node: Some(node_entity),
node: Some(node),
kind: crate::entity::EndpointKind::Service,
topic: service_name.to_string(),
type_info: Some(GetTypeDescription::service_type_info()),
Expand All @@ -488,7 +513,7 @@ impl TypeDescriptionService {
entity,
session,
clock: clock.clone(),
keyexpr_format: hiroz_protocol::KeyExprFormat::default(),
keyexpr_format,
_phantom_data: Default::default(),
};

Expand Down
10 changes: 6 additions & 4 deletions crates/hiroz/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,13 +241,12 @@ impl Builder for ZNodeBuilder {
// Create type description service if enabled
let type_desc_service = if self.enable_type_desc_service {
debug!("[NOD] Creating type description service");
let service = TypeDescriptionService::new(
let service = TypeDescriptionService::new_with_node(
self.session.clone(),
&self.name,
&self.namespace,
id,
node.clone(),
&self.counter,
&self.clock,
self.keyexpr_format.clone(),
)?;

info!("[NOD] TypeDescriptionService created (callback mode)");
Expand All @@ -263,8 +262,11 @@ impl Builder for ZNodeBuilder {
let service = ParameterService::new(ParameterServiceConfig {
session: self.session.clone(),
graph: self.graph.clone(),
domain_id: self.domain_id,
keyexpr_format: self.keyexpr_format.clone(),
node_name: &self.name,
namespace: &self.namespace,
enclave: &node.enclave,
node_id: id,
counter: &self.counter,
clock: &self.clock,
Expand Down
26 changes: 15 additions & 11 deletions crates/hiroz/src/parameter/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,11 @@ type BoxedServer = Arc<dyn std::any::Any + Send + Sync>;
pub(crate) struct ParameterServiceConfig<'a> {
pub session: Arc<Session>,
pub graph: Arc<crate::graph::Graph>,
pub domain_id: usize,
pub keyexpr_format: hiroz_protocol::KeyExprFormat,
pub node_name: &'a str,
pub namespace: &'a str,
pub enclave: &'a str,
pub node_id: usize,
pub counter: &'a GlobalCounter,
pub clock: &'a crate::time::ZClock,
Expand Down Expand Up @@ -284,8 +287,11 @@ impl ParameterService {
let ParameterServiceConfig {
session,
graph,
domain_id,
keyexpr_format,
node_name,
namespace,
enclave,
node_id,
counter,
clock,
Expand All @@ -297,12 +303,12 @@ impl ParameterService {
wire_types::register_parameter_schemas(tds);
}
let node_entity = NodeEntity::new(
0,
domain_id,
session.zid(),
node_id,
node_name.to_string(),
namespace.to_string(),
String::new(),
enclave.to_string(),
);

// Compute node fully-qualified name for parameter events
Expand All @@ -323,8 +329,6 @@ impl ParameterService {
qos: Default::default(),
};

let ke_format = hiroz_protocol::KeyExprFormat::default();

// ── /parameter_events publisher ───────────────────────────────────────
let pub_entity = EndpointEntity {
id: counter.increment(),
Expand All @@ -351,7 +355,7 @@ impl ParameterService {
clock: clock.clone(),
with_attachment: true,
shm_config: None,
keyexpr_format: ke_format.clone(),
keyexpr_format: keyexpr_format.clone(),
dyn_schema: None,
encoding: None,
_phantom_data: Default::default(),
Expand Down Expand Up @@ -380,7 +384,7 @@ impl ParameterService {
entity,
session: session.clone(),
clock: clock.clone(),
keyexpr_format: ke_format.clone(),
keyexpr_format: keyexpr_format.clone(),
_phantom_data: Default::default(),
};
builder.build_with_callback(move |query| {
Expand All @@ -400,7 +404,7 @@ impl ParameterService {
entity,
session: session.clone(),
clock: clock.clone(),
keyexpr_format: ke_format.clone(),
keyexpr_format: keyexpr_format.clone(),
_phantom_data: Default::default(),
};
builder.build_with_callback(move |query| {
Expand All @@ -420,7 +424,7 @@ impl ParameterService {
entity,
session: session.clone(),
clock: clock.clone(),
keyexpr_format: ke_format.clone(),
keyexpr_format: keyexpr_format.clone(),
_phantom_data: Default::default(),
};
builder.build_with_callback(move |query| {
Expand All @@ -440,7 +444,7 @@ impl ParameterService {
entity,
session: session.clone(),
clock: clock.clone(),
keyexpr_format: ke_format.clone(),
keyexpr_format: keyexpr_format.clone(),
_phantom_data: Default::default(),
};
builder.build_with_callback(move |query| {
Expand All @@ -460,7 +464,7 @@ impl ParameterService {
entity,
session: session.clone(),
clock: clock.clone(),
keyexpr_format: ke_format.clone(),
keyexpr_format: keyexpr_format.clone(),
_phantom_data: Default::default(),
};
builder.build_with_callback(move |query| {
Expand All @@ -480,7 +484,7 @@ impl ParameterService {
entity,
session: session.clone(),
clock: clock.clone(),
keyexpr_format: ke_format.clone(),
keyexpr_format: keyexpr_format.clone(),
_phantom_data: Default::default(),
};
builder.build_with_callback(move |query| {
Expand Down
Loading
Loading