diff --git a/crates/hiroz-codegen/src/hashing.rs b/crates/hiroz-codegen/src/hashing.rs index a4b64ce59..7a48409ab 100644 --- a/crates/hiroz-codegen/src/hashing.rs +++ b/crates/hiroz-codegen/src/hashing.rs @@ -37,11 +37,11 @@ pub fn calculate_service_type_hash( service_event_info_desc: &TypeDescription, resolved_deps: &BTreeMap, ) -> Result { - // ROS2 uses slash format, not :: format - // Detect if this is an action service (contains SendGoal/GetResult/CancelGoal) - let is_action = service_name.contains("SendGoal") - || service_name.contains("GetResult") - || service_name.contains("CancelGoal"); + // Per-action synthetic services (Foo_SendGoal / Foo_GetResult) live under + // `{pkg}/action/…`. CancelGoal is the shared action_msgs *srv* + // (`action_msgs/srv/CancelGoal`) — do NOT treat it as /action/ or the RIHS + // hash diverges from rmw_zenoh and cancel queries never match the server. + let is_action = service_name.contains("SendGoal") || service_name.contains("GetResult"); let path = if is_action { "action" } else { "srv" }; // Action services use /action/ path, regular services use /srv/ path diff --git a/crates/hiroz-codegen/src/resolver.rs b/crates/hiroz-codegen/src/resolver.rs index d9f6720c5..5c78fa83b 100644 --- a/crates/hiroz-codegen/src/resolver.rs +++ b/crates/hiroz-codegen/src/resolver.rs @@ -734,19 +734,31 @@ impl Resolver { }], }; - // CancelGoal_Response: return_code (int8) + // CancelGoal_Response: return_code (int8) + goals_canceling (GoalInfo[]) let response_desc = TypeDescription { type_name: "action_msgs/srv/CancelGoal_Response".to_string(), - fields: vec![FieldDescription { - name: "return_code".to_string(), - field_type: FieldTypeDescription { - type_id: 2, // int8 - capacity: 0, - string_capacity: 0, - nested_type_name: String::new(), + fields: vec![ + FieldDescription { + name: "return_code".to_string(), + field_type: FieldTypeDescription { + type_id: 2, // int8 + capacity: 0, + string_capacity: 0, + nested_type_name: String::new(), + }, + default_value: String::new(), }, - default_value: String::new(), - }], + FieldDescription { + name: "goals_canceling".to_string(), + field_type: FieldTypeDescription { + type_id: 145, // NESTED_TYPE_UNBOUNDED_SEQUENCE + capacity: 0, + string_capacity: 0, + nested_type_name: "action_msgs/msg/GoalInfo".to_string(), + }, + default_value: String::new(), + }, + ], }; // Get dependencies @@ -762,6 +774,28 @@ impl Resolver { deps.insert(time_desc.type_name.clone(), time_desc.clone()); } + // GoalInfo also nests UUID — required for a matching RIHS hash. + if let Some(uuid_desc) = self.type_descriptions.get("unique_identifier_msgs/UUID") { + deps.insert(uuid_desc.type_name.clone(), uuid_desc.clone()); + } else { + // Fallback matching calculate_status_hash when UUID isn't resolved yet. + use crate::hashing::TypeId; + let uuid_desc = TypeDescription { + type_name: "unique_identifier_msgs/msg/UUID".to_string(), + fields: vec![FieldDescription { + name: "uuid".to_string(), + field_type: FieldTypeDescription { + type_id: TypeId::UINT8_ARRAY, + capacity: 16, + string_capacity: 0, + nested_type_name: String::new(), + }, + default_value: String::new(), + }], + }; + deps.insert(uuid_desc.type_name.clone(), uuid_desc); + } + // Get ServiceEventInfo let service_event_info_desc = self .type_descriptions diff --git a/crates/hiroz-codegen/tests/action_hash_check.rs b/crates/hiroz-codegen/tests/action_hash_check.rs index eb44b71d1..ed30c6b70 100644 --- a/crates/hiroz-codegen/tests/action_hash_check.rs +++ b/crates/hiroz-codegen/tests/action_hash_check.rs @@ -56,3 +56,51 @@ fn test_fibonacci_get_result_hash() { "status hash mismatch" ); } + +#[test] +fn test_cancel_goal_hash_matches_ros() { + use hiroz_codegen::{ + discovery::{discover_actions, discover_messages}, + resolver::Resolver, + }; + + // Same package set as Fibonacci hash check; CancelGoal hash is shared + // across all actions (action_msgs/srv/CancelGoal). + let assets = assets_dir(); + let packages = [ + "builtin_interfaces", + "unique_identifier_msgs", + "action_msgs", + "service_msgs", + "action_tutorials_interfaces", + ]; + let mut all_messages = Vec::new(); + for pkg in &packages { + let pkg_path = assets.join(pkg); + let msgs = discover_messages(&pkg_path, pkg).unwrap_or_default(); + all_messages.extend(msgs); + } + + let mut resolver = Resolver::new(false); + resolver + .resolve_messages(all_messages) + .expect("resolve messages"); + + let pkg_path = assets.join("action_tutorials_interfaces"); + let actions = + discover_actions(&pkg_path, "action_tutorials_interfaces").expect("discover actions"); + let resolved = resolver.resolve_actions(actions).expect("resolve actions"); + let fib = resolved + .iter() + .find(|a| a.parsed.name == "Fibonacci") + .expect("Fibonacci action"); + + let hash = fib.cancel_goal_hash.to_rihs_string(); + println!("cancel_goal_hash: {hash}"); + // From /opt/ros/lyrical/share/action_msgs/srv/CancelGoal.json + assert_eq!( + hash, + "RIHS01_573d8b0a534451d7bc2ac8c5ffde8ac14b8593b7001175d0cd6516dcbeb8689a", + "CancelGoal hash must match ROS action_msgs/srv/CancelGoal.json" + ); +} diff --git a/crates/hiroz/src/node.rs b/crates/hiroz/src/node.rs index 90435aeed..97717784f 100644 --- a/crates/hiroz/src/node.rs +++ b/crates/hiroz/src/node.rs @@ -853,7 +853,7 @@ impl ZNode { let get_result_client = self.create_raw_service_client(&get_result_service, &get_result_type, result_hash)?; let cancel_goal_client = - self.create_raw_service_client(&cancel_goal_service, &cancel_goal_type, "")?; + self.create_raw_service_client(&cancel_goal_service, &cancel_goal_type, CANCEL_GOAL_TYPE_HASH)?; // Feedback subscriber (no-op callback for now; Go handles via polling or separate mechanism) let feedback_sub = @@ -904,7 +904,7 @@ impl ZNode { let get_result_server = self.create_raw_service_server(&get_result_service, &get_result_type, result_hash)?; let cancel_goal_server = - self.create_raw_service_server(&cancel_goal_service, &cancel_goal_type, "")?; + self.create_raw_service_server(&cancel_goal_service, &cancel_goal_type, CANCEL_GOAL_TYPE_HASH)?; let feedback_pub = self.create_raw_publisher(&feedback_topic, &feedback_type_dds, feedback_hash)?; @@ -1368,6 +1368,12 @@ impl ZNode { } } +/// ROS 2 Humble/Jazzy RIHS01 type hash for `action_msgs/srv/CancelGoal`. +/// Must match rmw_zenoh keyexpr hashing or cancel requests never reach the server. +#[cfg(feature = "ffi")] +const CANCEL_GOAL_TYPE_HASH: &str = + "RIHS01_573d8b0a534451d7bc2ac8c5ffde8ac14b8593b7001175d0cd6516dcbeb8689a"; + /// Parse an action type string like `"example_interfaces/action/Fibonacci"` into /// `(package, action_name)` — i.e., `("example_interfaces", "Fibonacci")`. /// These are used to construct DDS-style type names for rmw_zenoh_cpp graph discovery.