Skip to content
Closed
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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ zenoh = { version = "1.9.0", default-features = false, features = [
"transport_tcp",
"transport_serial",
] }
zenoh-ext = { version = "1.9.0", features = ["unstable"] }
zenoh-ext = { version = "1.9.0", default-features = false, features = ["unstable"] }
zenoh-buffers = { version = "1.9.0" }

# CLI
Expand Down
2 changes: 1 addition & 1 deletion crates/hiroz-protocol/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ categories = ["network-programming", "science::robotics"]

[features]
default = ["std", "rmw-zenoh"]
std = ["zenoh/default"]
std = []
rmw-zenoh = []
no-type-hash = [] # ROS 2 Humble doesn't support type hashing

Expand Down
16 changes: 16 additions & 0 deletions crates/hiroz-protocol/src/format/rmw_zenoh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,22 @@ mod tests {
assert_eq!(decoded.durability, QosDurability::TransientLocal);
}

#[test]
fn parse_liveliness_with_verbatim_rmw_system_default_qos() {
let key: KeyExpr<'_> = "@ros2_lv/123/1234567890abcdef1234567890abcdef/1/2/MP/%/%/res_gateway/%res%statuslight%autonomy/frost_msgs%msg%StatuslightRpdo1/RIHS01_0000000000000000000000000000000000000000000000000000000000000000/::,:,:,:,,:"
.try_into()
.unwrap();

let Entity::Endpoint(endpoint) = RmwZenohFormatter::parse_liveliness(&key).unwrap() else {
panic!("expected endpoint entity");
};

assert_eq!(endpoint.node.unwrap().domain_id, 123);
assert_eq!(endpoint.kind, EndpointKind::Publisher);
assert_eq!(endpoint.topic, "/res/statuslight/autonomy");
assert_eq!(endpoint.qos, QosProfile::default());
}

/// Test topic key expression format matches rmw_zenoh.
///
/// rmw_zenoh format: `<domain_id>/<topic>/<type>/<hash>`
Expand Down
123 changes: 107 additions & 16 deletions crates/hiroz-protocol/src/qos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,36 +77,53 @@ impl QosProfile {

// Parse reliability (RMW values: 1=Reliable, 2=BestEffort)
let reliability = match fields[0] {
"" => default_qos.reliability,
"" | "0" => default_qos.reliability,
"1" => QosReliability::Reliable,
"2" => QosReliability::BestEffort,
_ => return Err(QosDecodeError::InvalidReliability),
};

// Parse durability (RMW values: 1=TransientLocal, 2=Volatile)
let durability = match fields[1] {
"" => default_qos.durability,
"" | "0" => default_qos.durability,
"1" => QosDurability::TransientLocal,
"2" => QosDurability::Volatile,
_ => return Err(QosDecodeError::InvalidDurability),
};

// Parse history: <kind>,<depth>
let history_parts: alloc::vec::Vec<&str> = fields[2].split(',').collect();
if history_parts.len() < 2 {
return Err(QosDecodeError::InvalidHistory);
}
// Parse history: <kind>,<depth>. rmw_zenoh_cpp omits QoS sub-fields
// whose value is SYSTEM_DEFAULT, so the history field can be just `,`.
let history = match fields[2] {
"," => default_qos.history,
// An omitted history field is only meaningful in the complete
// six-field wire representation. Keep rejecting truncated `::`.
"" if fields.len() >= 6 => default_qos.history,
encoded => {
let (kind, encoded_depth) = encoded
.split_once(',')
.ok_or(QosDecodeError::InvalidHistory)?;

let history = match history_parts[0] {
"" | "1" => {
// KeepLast - parse depth
let depth = history_parts[1]
.parse::<usize>()
.map_err(|_| QosDecodeError::InvalidHistory)?;
QosHistory::KeepLast(depth)
match kind {
"" | "0" | "1" => {
let depth = if encoded_depth.is_empty() {
default_qos.history.depth()
} else {
encoded_depth
.parse::<usize>()
.map_err(|_| QosDecodeError::InvalidHistory)?
};
// A zero depth represents an unspecified/default depth
// at the ROS boundary; KeepLast(0) is not useful.
QosHistory::KeepLast(if depth == 0 {
default_qos.history.depth()
} else {
depth
})
}
"2" => QosHistory::KeepAll,
_ => return Err(QosDecodeError::InvalidHistory),
}
}
"2" => QosHistory::KeepAll,
_ => return Err(QosDecodeError::InvalidHistory),
};

Ok(QosProfile {
Expand Down Expand Up @@ -182,3 +199,77 @@ impl Display for QosDecodeError {
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn decode_rmw_compact_qos_corpus() {
let cases = [
("::,:,:,:,,", QosProfile::default()),
(":::,:,:,,", QosProfile::default()),
("::1,:,:,:,,", QosProfile::default()),
("::,10:,:,:,,", QosProfile::default()),
(
"::2,:,:,:,,",
QosProfile {
history: QosHistory::KeepAll,
..QosProfile::default()
},
),
(
"1:1:,5:,:,:,,",
QosProfile {
durability: QosDurability::TransientLocal,
history: QosHistory::KeepLast(5),
..QosProfile::default()
},
),
(
"2::,1:,:,:,,",
QosProfile {
reliability: QosReliability::BestEffort,
history: QosHistory::KeepLast(1),
..QosProfile::default()
},
),
("0:0:0,0:,:,:,,", QosProfile::default()),
];

for (encoded, expected) in cases {
assert_eq!(QosProfile::decode(encoded), Ok(expected), "{encoded}");
}
}

#[test]
fn qos_round_trip() {
let profiles = [
QosProfile::default(),
QosProfile {
reliability: QosReliability::BestEffort,
durability: QosDurability::TransientLocal,
history: QosHistory::KeepLast(5),
},
QosProfile {
history: QosHistory::KeepAll,
..QosProfile::default()
},
];

for profile in profiles {
assert_eq!(QosProfile::decode(&profile.encode()), Ok(profile));
}
}

#[test]
fn reject_invalid_history() {
for encoded in ["::3,1:,:,:,,", "::1,-1:,:,:,,", "::"] {
assert_eq!(
QosProfile::decode(encoded),
Err(QosDecodeError::InvalidHistory),
"{encoded}"
);
}
}
}
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
9 changes: 5 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,6 +262,8 @@ 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,
node_id: id,
Expand Down
22 changes: 12 additions & 10 deletions crates/hiroz/src/parameter/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ 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 node_id: usize,
Expand Down Expand Up @@ -284,6 +286,8 @@ impl ParameterService {
let ParameterServiceConfig {
session,
graph,
domain_id,
keyexpr_format,
node_name,
namespace,
node_id,
Expand All @@ -297,7 +301,7 @@ impl ParameterService {
wire_types::register_parameter_schemas(tds);
}
let node_entity = NodeEntity::new(
0,
domain_id,
session.zid(),
node_id,
node_name.to_string(),
Expand All @@ -323,8 +327,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 +353,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 +382,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 +402,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 +422,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 +442,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 +462,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 +482,7 @@ impl ParameterService {
entity,
session: session.clone(),
clock: clock.clone(),
keyexpr_format: ke_format.clone(),
keyexpr_format,
_phantom_data: Default::default(),
};
builder.build_with_callback(move |query| {
Expand Down
Loading
Loading