diff --git a/crates/hiroz/examples/demo_nodes/talker.rs b/crates/hiroz/examples/demo_nodes/talker.rs index 66608d238..d6cf26737 100644 --- a/crates/hiroz/examples/demo_nodes/talker.rs +++ b/crates/hiroz/examples/demo_nodes/talker.rs @@ -27,7 +27,10 @@ pub async fn run_talker( ) -> Result<()> { // --8<-- [start:node_setup] // Create a node named "talker" - let node = ctx.create_node("talker").build()?; + let node = ctx + .create_node("talker") + .with_type_description_service() + .build()?; // --8<-- [end:node_setup] // --8<-- [start:publisher_setup] diff --git a/crates/hiroz/examples/laser_scan.rs b/crates/hiroz/examples/laser_scan.rs index f52de32d5..d6392289d 100644 --- a/crates/hiroz/examples/laser_scan.rs +++ b/crates/hiroz/examples/laser_scan.rs @@ -25,7 +25,10 @@ fn main() -> Result<()> { fn run_publisher() -> Result<()> { let ctx = ZContextBuilder::default().build()?; - let node = ctx.create_node("laser_scan_publisher").build()?; + let node = ctx + .create_node("laser_scan_publisher") + .with_type_description_service() + .build()?; let zpub = node.create_pub::("scan").build()?; println!("Publishing LaserScan messages on /scan..."); diff --git a/crates/hiroz/examples/lifecycle/talker.rs b/crates/hiroz/examples/lifecycle/talker.rs index 96eab4aef..841e9bae1 100644 --- a/crates/hiroz/examples/lifecycle/talker.rs +++ b/crates/hiroz/examples/lifecycle/talker.rs @@ -18,7 +18,10 @@ fn main() -> Result<()> { // Build a Zenoh context and create a lifecycle node. // The node starts in the Unconfigured state. let ctx = ZContextBuilder::default().build()?; - let mut node = ctx.create_lifecycle_node("lifecycle_talker").build()?; + let mut node = ctx + .create_lifecycle_node("lifecycle_talker") + .with_type_description_service() + .build()?; // Register callbacks for each lifecycle transition. // Each callback receives the previous state and must return diff --git a/crates/hiroz/examples/shm_pointcloud2.rs b/crates/hiroz/examples/shm_pointcloud2.rs index eef24569d..4048d1bc9 100644 --- a/crates/hiroz/examples/shm_pointcloud2.rs +++ b/crates/hiroz/examples/shm_pointcloud2.rs @@ -68,7 +68,10 @@ fn demo_user_managed_shm() -> zenoh::Result<()> { // Step 3: Create node and publisher let ctx = ZContextBuilder::default().build()?; - let node = ctx.create_node("pointcloud_publisher").build()?; + let node = ctx + .create_node("pointcloud_publisher") + .with_type_description_service() + .build()?; let publisher = node .create_pub::("cloud/user_managed") .build()?; @@ -95,7 +98,10 @@ fn demo_automatic_shm() -> zenoh::Result<()> { .build()?; println!(" ✓ Context configured with automatic SHM (threshold: 10KB)"); - let node = ctx.create_node("pointcloud_publisher").build()?; + let node = ctx + .create_node("pointcloud_publisher") + .with_type_description_service() + .build()?; let publisher = node.create_pub::("cloud/automatic").build()?; // Generate point cloud normally (using Vec) @@ -126,7 +132,10 @@ fn demo_automatic_shm() -> zenoh::Result<()> { fn demo_publisher_shm_override() -> zenoh::Result<()> { // Context has no SHM, but publisher has its own config let ctx = ZContextBuilder::default().build()?; - let node = ctx.create_node("pointcloud_publisher").build()?; + let node = ctx + .create_node("pointcloud_publisher") + .with_type_description_service() + .build()?; // Create SHM provider for this publisher only let provider = Arc::new(ShmProviderBuilder::new(30 * 1024 * 1024).build()?); diff --git a/crates/hiroz/examples/twist_pub.rs b/crates/hiroz/examples/twist_pub.rs index 2960b5ffd..8efb62466 100644 --- a/crates/hiroz/examples/twist_pub.rs +++ b/crates/hiroz/examples/twist_pub.rs @@ -5,7 +5,10 @@ use hiroz_msgs::geometry_msgs::{Twist, Vector3}; fn main() -> Result<()> { let ctx = ZContextBuilder::default().build()?; - let node = ctx.create_node("twist_publisher").build()?; + let node = ctx + .create_node("twist_publisher") + .with_type_description_service() + .build()?; let zpub = node.create_pub::("cmd_vel").build()?; println!("Publishing Twist messages on /cmd_vel..."); diff --git a/crates/hiroz/examples/z_cache/talker.rs b/crates/hiroz/examples/z_cache/talker.rs index 2c21937fb..db38a6018 100644 --- a/crates/hiroz/examples/z_cache/talker.rs +++ b/crates/hiroz/examples/z_cache/talker.rs @@ -15,7 +15,10 @@ use hiroz::{Builder, Result, context::ZContextBuilder}; use hiroz_msgs::std_msgs::String as RosString; pub async fn run(ctx: hiroz::context::ZContext, topic: String, count: usize) -> Result<()> { - let node = ctx.create_node("cache_talker").build()?; + let node = ctx + .create_node("cache_talker") + .with_type_description_service() + .build()?; let publisher = node.create_pub::(&topic).build()?; println!("[talker] publishing on '{}' every 100 ms", topic); diff --git a/crates/hiroz/examples/z_custom_message.rs b/crates/hiroz/examples/z_custom_message.rs index 85e6d5202..eb8d39092 100644 --- a/crates/hiroz/examples/z_custom_message.rs +++ b/crates/hiroz/examples/z_custom_message.rs @@ -200,7 +200,10 @@ async fn run_status_subscriber() -> Result<()> { pub fn run_navigation_server(ctx: hiroz::context::ZContext) -> Result<()> { println!("Starting navigation service server..."); - let node = ctx.create_node("navigation_server").build()?; + let node = ctx + .create_node("navigation_server") + .with_type_description_service() + .build()?; let mut zsrv = node.create_service::("/navigate_to").build()?; println!("Navigation server ready, waiting for requests..."); diff --git a/crates/hiroz/examples/z_pubsub.rs b/crates/hiroz/examples/z_pubsub.rs index 7f954eabe..f22e5af68 100644 --- a/crates/hiroz/examples/z_pubsub.rs +++ b/crates/hiroz/examples/z_pubsub.rs @@ -11,7 +11,7 @@ use hiroz_msgs::std_msgs::String as RosString; async fn run_subscriber(ctx: ZContext, topic: String) -> Result<()> { // Create a ROS 2 node - the fundamental unit of computation // Nodes are logical groupings of publishers, subscribers, services, etc. - let node = ctx.create_node("Sub").build()?; + let node = ctx.create_node("listener").build()?; // Create a subscriber for the specified topic // The type parameter RosString determines what message type we'll receive @@ -33,7 +33,10 @@ async fn run_publisher( payload: String, ) -> Result<()> { // Create a ROS 2 node for publishing - let node = ctx.create_node("Pub").build()?; + let node = ctx + .create_node("talker") + .with_type_description_service() + .build()?; // Create a publisher for the specified topic // The type parameter RosString determines what message type we'll send diff --git a/crates/hiroz/src/context.rs b/crates/hiroz/src/context.rs index c5d6e7693..ba6d02f22 100644 --- a/crates/hiroz/src/context.rs +++ b/crates/hiroz/src/context.rs @@ -629,6 +629,7 @@ impl ZContext { Some(self.namespace.clone()) }, enable_communication_interface: true, + type_description_service: false, } } diff --git a/crates/hiroz/src/dynamic/mod.rs b/crates/hiroz/src/dynamic/mod.rs index 182d2bd48..fbd31ab85 100644 --- a/crates/hiroz/src/dynamic/mod.rs +++ b/crates/hiroz/src/dynamic/mod.rs @@ -77,6 +77,8 @@ pub use message::{DynamicMessage, DynamicMessageBuilder}; #[cfg(feature = "dynamic-schema-loader")] pub use registry::load_schema; pub use registry::{SchemaRegistry, get_schema, has_schema, register_schema}; +// Exported next to `load_schema`: the two are used as a pair -- demangle a +// graph-reported type name, then load its schema. pub use schema::{FieldSchema, FieldType, MessageSchema, MessageSchemaBuilder}; pub use serdes::DynamicSerdeCdrSerdes; pub use serialization::SerializationFormat; @@ -88,6 +90,7 @@ pub use type_description_service::{ WireKeyValue, WireTypeDescription, WireTypeSource, schema_to_wire_type_description, wire_to_schema_type_description, }; +pub use type_info::ros_type_name_from_dds; pub use value::{DynamicValue, FromDynamic, IntoDynamic}; pub(crate) use discovery::{SchemaDiscovery, discovered_schema_type_info}; diff --git a/crates/hiroz/src/dynamic/type_info.rs b/crates/hiroz/src/dynamic/type_info.rs index bc1604817..816d298fb 100644 --- a/crates/hiroz/src/dynamic/type_info.rs +++ b/crates/hiroz/src/dynamic/type_info.rs @@ -12,7 +12,13 @@ pub(crate) fn dds_type_name_from_schema(schema: &MessageSchema) -> String { + "_" } -pub(crate) fn ros_type_name_from_dds(dds_name: &str) -> String { +/// Convert a DDS-mangled type name as it appears in liveliness tokens and the +/// graph (`std_msgs::msg::dds_::String_`) into the canonical ROS form the schema +/// registry and `.msg` loader expect (`std_msgs/msg/String`). Public because +/// out-of-crate consumers (e.g. `hu`'s WASM host) resolve graph-reported types +/// against `load_schema` and must use this exact normalisation rather than +/// re-deriving one -- see issue #172. +pub fn ros_type_name_from_dds(dds_name: &str) -> String { dds_name .replace("::msg::dds_::", "/msg/") .replace("::srv::dds_::", "/srv/") diff --git a/crates/hiroz/src/lifecycle/node.rs b/crates/hiroz/src/lifecycle/node.rs index ccc39f84e..ebf882cbc 100644 --- a/crates/hiroz/src/lifecycle/node.rs +++ b/crates/hiroz/src/lifecycle/node.rs @@ -276,6 +276,7 @@ pub struct ZLifecycleNodeBuilder { pub(crate) name: String, pub(crate) namespace: Option, pub enable_communication_interface: bool, + pub(crate) type_description_service: bool, } impl ZLifecycleNodeBuilder { @@ -284,6 +285,16 @@ impl ZLifecycleNodeBuilder { self } + /// Pass-through to [`ZNodeBuilder::with_type_description_service`] on the + /// inner node, so publishers created via [`ZLifecycleNode::create_publisher`] + /// register their schemas and runtime-typed consumers can decode them. + /// + /// [`ZNodeBuilder::with_type_description_service`]: crate::node::ZNodeBuilder::with_type_description_service + pub fn with_type_description_service(mut self) -> Self { + self.type_description_service = true; + self + } + pub fn disable_communication_interface(mut self) -> Self { self.enable_communication_interface = false; self @@ -298,6 +309,9 @@ impl Builder for ZLifecycleNodeBuilder { if let Some(ns) = self.namespace { node_builder = node_builder.with_namespace(ns); } + if self.type_description_service { + node_builder = node_builder.with_type_description_service(); + } let inner = node_builder.build()?; // Shared state machine for service closures diff --git a/crates/hiroz/src/node.rs b/crates/hiroz/src/node.rs index 8a1088a65..2a14cb899 100644 --- a/crates/hiroz/src/node.rs +++ b/crates/hiroz/src/node.rs @@ -136,6 +136,15 @@ impl ZNodeBuilder { /// // Static publishers also auto-register when their message type provides /// // MessageTypeInfo::message_schema() (e.g. generated hiroz messages). /// ``` + /// + /// # Why this is opt-in, and why you probably want it + /// + /// ROS 2 (rclcpp/rclpy) serves the equivalent service by default, and + /// hiroz's own RMW layer forces it on for every node it creates. A plain + /// hiroz node does not: it must opt in here. Without it, runtime-typed + /// consumers that have no compiled knowledge of the message — `hu meter + /// echo`, dynamic subscribers, bridges — cannot obtain the schema and + /// therefore cannot decode this node's messages. pub fn with_type_description_service(mut self) -> Self { self.enable_type_desc_service = true; self