From 207339b6dcc1ad045ab9ee5f75c2cf646d9dd287 Mon Sep 17 00:00:00 2001 From: yuanyuyuan Date: Fri, 21 Aug 2026 17:11:22 +0800 Subject: [PATCH] feat(hiroz): let the examples advertise their message types A plain hiroz node does not serve the type description service unless it asks. rclcpp and rclpy serve it by default, and hiroz's own RMW layer forces it on, so the examples were the only nodes on a hiroz graph that runtime-typed consumers could not decode: `hu meter echo`, dynamic subscribers and bridges all had no schema to obtain. Every publishing example now opts in, and `ZNodeBuilder` documents why you probably want to. Two additions the plugin host needs from outside the crate: `ZLifecycleNodeBuilder::with_type_description_service()`, so lifecycle publishers register their schemas too, and `ros_type_name_from_dds`, which becomes public so out-of-crate consumers normalise graph-reported type names the same way rather than re-deriving it. Rider: z_pubsub's nodes are renamed `Sub`/`Pub` to `listener`/`talker`. That is a user-visible rename, not a type-description change, and the docs always claimed the new names. --- crates/hiroz/examples/demo_nodes/talker.rs | 5 ++++- crates/hiroz/examples/laser_scan.rs | 5 ++++- crates/hiroz/examples/lifecycle/talker.rs | 5 ++++- crates/hiroz/examples/shm_pointcloud2.rs | 15 ++++++++++++--- crates/hiroz/examples/twist_pub.rs | 5 ++++- crates/hiroz/examples/z_cache/talker.rs | 5 ++++- crates/hiroz/examples/z_custom_message.rs | 5 ++++- crates/hiroz/examples/z_pubsub.rs | 7 +++++-- crates/hiroz/src/context.rs | 1 + crates/hiroz/src/dynamic/mod.rs | 3 +++ crates/hiroz/src/dynamic/type_info.rs | 8 +++++++- crates/hiroz/src/lifecycle/node.rs | 14 ++++++++++++++ crates/hiroz/src/node.rs | 9 +++++++++ 13 files changed, 75 insertions(+), 12 deletions(-) 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