-
Notifications
You must be signed in to change notification settings - Fork 16
feat: implemented TranslateBrowsePathsToNodeIds service #309
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| use anyhow::{Context as _, anyhow}; | ||
| use open62541::ua::{BrowsePath, RelativePath, RelativePathElement}; | ||
| use open62541::{AsyncClient, DataType, ua}; | ||
| use open62541_sys::{UA_NS0ID_HIERARCHICALREFERENCES, UA_NS0ID_ROOTFOLDER}; | ||
|
|
||
| #[tokio::main] | ||
| async fn main() -> anyhow::Result<()> { | ||
| env_logger::init(); | ||
|
|
||
| let client = AsyncClient::new("opc.tcp://opcuademo.sterfive.com:26543").context("connect")?; | ||
|
|
||
| let paths = vec![ | ||
| "/Root/0:Objects/2:DeviceSet/1:CoffeeMachineA/7:Parameters/17:CoffeeBeanLevel", | ||
| "/Root/0:Objects/1:MyDevices/1:Pressure", | ||
| "/Root/0:Objects/2:DeviceSet/1:RFIDScanner/4:ScanActive", | ||
| ]; | ||
|
|
||
| // translate single browse path | ||
| let path_with_expected_nodes = paths.iter().zip(vec![ | ||
| ua::NodeId::numeric(1, 1068), | ||
| ua::NodeId::string(1, "Pressure"), | ||
| ua::NodeId::string(1, "RFIDScanner-ScanActive"), | ||
| ]); | ||
|
|
||
| for (path, expected_node_id) in path_with_expected_nodes { | ||
| let result_node_id = translate_browse_path(&client, path).await?; | ||
| if result_node_id.node_id() != &expected_node_id { | ||
| Err(anyhow!( | ||
| "Expected browse path {:?} to resolve to node_id {:?}, got {:?}", | ||
| path, | ||
| expected_node_id, | ||
| result_node_id | ||
| ))?; | ||
| } | ||
| } | ||
|
|
||
| // translate many browse paths | ||
| translate_many_browse_path(&client, paths).await?; | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| async fn translate_many_browse_path(client: &AsyncClient, paths: Vec<&str>) -> anyhow::Result<()> { | ||
| println!("\ntranslate_many_browse_paths: "); | ||
|
|
||
| let browse_paths: Vec<BrowsePath> = paths | ||
| .iter() | ||
| .map(|p| create_browse_path(p)) | ||
| .collect::<Result<_, _>>()?; | ||
|
|
||
| let browse_results = client.translate_many_browse_paths(&browse_paths).await?; | ||
|
|
||
| for (i, browse_result) in browse_results.into_iter().enumerate() { | ||
| println!("path: {:?} resulted in: {:?}", paths.get(i), browse_result); | ||
| } | ||
| Ok(()) | ||
| } | ||
|
|
||
| async fn translate_browse_path( | ||
| client: &AsyncClient, | ||
| path: &str, | ||
| ) -> anyhow::Result<ua::ExpandedNodeId> { | ||
| let browse_path = create_browse_path(path)?; | ||
| let browse_targets = client.translate_browse_path(&browse_path).await?; | ||
|
|
||
| let Some(target) = browse_targets.first() else { | ||
| Err(anyhow!("Expected one target, got {}", browse_targets.len()))? | ||
| }; | ||
|
|
||
| if let Some(remaining) = target.remaining_path_index() { | ||
| Err(anyhow!( | ||
| "Expected remaining path index to be None, got {remaining}" | ||
| ))?; | ||
| } | ||
|
|
||
| let node_id = target.target_id(); | ||
| println!("translated browse path: {path:?} to node_id: {node_id:?}"); | ||
|
|
||
| Ok(node_id.clone()) | ||
| } | ||
|
|
||
| fn create_browse_path(path: &str) -> anyhow::Result<BrowsePath> { | ||
| let objects_path = if path.starts_with("/Root") { | ||
| path.strip_prefix("/Root").unwrap() | ||
| } else { | ||
| Err(anyhow!("Invalid browse path must start with /Root"))? | ||
| }; | ||
|
|
||
| let elements = objects_path | ||
| .split('/') | ||
| .filter(|seg| !seg.is_empty()) | ||
| .filter_map(|seg| seg.split_once(':')) | ||
| .filter_map(|(ns, name)| ns.parse::<u16>().ok().map(|ns| (ns, name))) | ||
| .map(|(ns, name)| { | ||
| RelativePathElement::init() | ||
| .with_reference_type_id(&ua::NodeId::ns0(UA_NS0ID_HIERARCHICALREFERENCES)) | ||
| .with_is_inverse(false) | ||
| .with_include_subtypes(true) | ||
| .with_target_name(&ua::QualifiedName::new(ns, name)) | ||
| }) | ||
| .collect::<Vec<_>>(); | ||
|
|
||
| let r_path = RelativePath::init().with_elements(&elements); | ||
|
|
||
| let browse_path = BrowsePath::init() | ||
| .with_starting_node(&ua::NodeId::ns0(UA_NS0ID_ROOTFOLDER)) | ||
| //.with_starting_node(&ua::NodeId::ns0(UA_NS0ID_OBJECTSFOLDER)) | ||
| .with_relative_path(&r_path); | ||
|
|
||
| Ok(browse_path) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -449,6 +449,77 @@ impl AsyncClient { | |
| Ok(results) | ||
| } | ||
|
|
||
| /// Translates a given browse path into a vector of `BrowsePathTarget` objects | ||
| /// using the OPC UA `TranslateBrowsePathsToNodeIds` service. | ||
| /// | ||
| /// This function takes a reference to a `ua::BrowsePath` and sends a request to the OPC UA server | ||
| /// to translate the browse path into corresponding `BrowsePathTarget` objects. | ||
| /// | ||
| /// # Errors | ||
| /// This fails only when the entire request fails. When a path does not exist or cannot be | ||
| /// translated, an inner `Err` is returned. | ||
| pub async fn translate_browse_path( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is just a convenience method for very simple use cases. We should strive to reduce the amount of redundant code by calling See also: |
||
| &self, | ||
| browse_path: &ua::BrowsePath, | ||
| ) -> Result<Vec<ua::BrowsePathTarget>> { | ||
| let request = ua::TranslateBrowsePathsToNodeIdsRequest::init() | ||
| .with_browse_paths(slice::from_ref(browse_path)); | ||
|
|
||
| let response = self.service_request(request).await?; | ||
|
|
||
| let Some(results) = response.results() else { | ||
| return Err(Error::internal( | ||
| "translate_browse_path should return results", | ||
| )); | ||
| }; | ||
|
|
||
| let Some(result) = results.as_slice().first() else { | ||
| return Err(Error::internal( | ||
| "translate_browse_path should return a result", | ||
| )); | ||
| }; | ||
|
|
||
| let targets = to_browse_path_result(result)?; | ||
|
|
||
| Ok(targets) | ||
| } | ||
|
|
||
| /// Translates multiple OPC UA browse paths into corresponding node IDs, if applicable. | ||
| /// | ||
| /// This issues only a single request to the OPC UA server (and should be preferred over several | ||
| /// individual requests with [`translate_browse_path()`] when browsing multiple paths). | ||
| /// | ||
| /// The size and order of the result list matches the size and order of the given paths list. | ||
| /// | ||
| /// # Errors | ||
| /// This fails only when the entire request fails. When a node does not exist or cannot be | ||
| /// browsed, an inner `Err` is returned. | ||
| /// | ||
| /// [`translate_browse_path()`]: Self::translate_browse_path | ||
| pub async fn translate_many_browse_paths( | ||
| &self, | ||
| browse_paths: &[ua::BrowsePath], | ||
| ) -> Result<Vec<Result<Vec<ua::BrowsePathTarget>>>> { | ||
| let request = | ||
| ua::TranslateBrowsePathsToNodeIdsRequest::init().with_browse_paths(browse_paths); | ||
|
|
||
| let response = self.service_request(request).await?; | ||
|
|
||
| let Some(results) = response.results() else { | ||
| return Err(Error::internal( | ||
| "translate_browse_path should return results", | ||
| )); | ||
| }; | ||
|
|
||
| if results.len() != browse_paths.len() { | ||
| return Err(Error::internal("unexpected number of browse path results")); | ||
| } | ||
|
|
||
| let targets: Vec<_> = results.iter().map(to_browse_path_result).collect(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's use the turbofish variant here. See also: #312 |
||
|
|
||
| Ok(targets) | ||
| } | ||
|
|
||
| /// Creates new [subscription](AsyncSubscription). | ||
| /// | ||
| /// # Errors | ||
|
|
@@ -738,3 +809,19 @@ fn to_browse_result(result: &ua::BrowseResult, node_id: Option<&ua::NodeId>) -> | |
|
|
||
| Ok((references, result.continuation_point())) | ||
| } | ||
|
|
||
| /// Converts [`ua::BrowsePathResult`] to a result type of [`Vec<ua::BrowsePathTarget>`] | ||
| fn to_browse_path_result(result: &ua::BrowsePathResult) -> Result<Vec<ua::BrowsePathTarget>> { | ||
| // Make sure to verify the inner status code inside `BrowseResult`. The service request finishes | ||
| // without error, even when browsing the node has failed. | ||
| Error::verify_good(&result.status_code())?; | ||
|
|
||
| let targets = if let Some(targets) = result.targets() { | ||
| targets.into_vec() | ||
| } else { | ||
| log::debug!("Translate browse paths returned unset targets, assuming none exist",); | ||
| Vec::new() | ||
| }; | ||
|
|
||
| Ok(targets) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| use crate::{DataType as _, ServiceRequest, ua}; | ||
|
|
||
| crate::data_type!(TranslateBrowsePathsToNodeIdsRequest); | ||
|
|
||
| impl TranslateBrowsePathsToNodeIdsRequest { | ||
| #[must_use] | ||
| pub fn with_browse_paths(mut self, browse_paths: &[ua::BrowsePath]) -> Self { | ||
| let array = ua::Array::from_slice(browse_paths); | ||
| array.move_into_raw(&mut self.0.browsePathsSize, &mut self.0.browsePaths); | ||
| self | ||
| } | ||
| } | ||
|
|
||
| impl ServiceRequest for TranslateBrowsePathsToNodeIdsRequest { | ||
| type Response = ua::TranslateBrowsePathsToNodeIdsResponse; | ||
|
|
||
| fn request_header(&self) -> &ua::RequestHeader { | ||
| ua::RequestHeader::raw_ref(&self.0.requestHeader) | ||
| } | ||
|
|
||
| fn request_header_mut(&mut self) -> &mut ua::RequestHeader { | ||
| ua::RequestHeader::raw_mut(&mut self.0.requestHeader) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| use crate::{DataType as _, ServiceResponse, ua}; | ||
|
|
||
| crate::data_type!(TranslateBrowsePathsToNodeIdsResponse); | ||
|
|
||
| impl TranslateBrowsePathsToNodeIdsResponse { | ||
| #[must_use] | ||
| pub fn results(&self) -> Option<ua::Array<ua::BrowsePathResult>> { | ||
| ua::Array::from_raw_parts(self.0.resultsSize, self.0.results) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add the TODO comments about returning "non-owned" results from similar methods. Just for consistency. |
||
| } | ||
| } | ||
|
|
||
| impl ServiceResponse for TranslateBrowsePathsToNodeIdsResponse { | ||
| type Request = ua::TranslateBrowsePathsToNodeIdsRequest; | ||
|
|
||
| fn response_header(&self) -> &ua::ResponseHeader { | ||
| ua::ResponseHeader::raw_ref(&self.0.responseHeader) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We use sentence-style line comments for consistency and readability. Starting with a capital letter and ending with a period. Even if the comment is not a complete sentence.