Skip to content
Open
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
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ verbose_file_reads = "warn"
name = "async_browse"
required-features = ["tokio"]

[[example]]
name = "async_translate_browse_path"
required-features = ["tokio"]

[[example]]
name = "async_call"
required-features = ["tokio"]
Expand Down
111 changes: 111 additions & 0 deletions examples/async_translate_browse_path.rs
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

Copy link
Copy Markdown
Collaborator

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.

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)
}
87 changes: 87 additions & 0 deletions src/async_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(

@uklotzde uklotzde Jan 12, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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 self.translate_many_browse_paths(). Even if it has a minor performance overhead.

See also: read_attributes()/read_many_attributes()

&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();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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
Expand Down Expand Up @@ -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)
}
4 changes: 4 additions & 0 deletions src/ua/data_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ mod status_code;
mod string;
mod structure_definition;
mod timestamps_to_return;
mod translate_browse_paths_to_node_ids_request;
mod translate_browse_paths_to_node_ids_response;
mod user_name_identity_token;
mod variant;
mod write_request;
Expand Down Expand Up @@ -164,6 +166,8 @@ pub use self::{
string::String,
structure_definition::StructureDefinition,
timestamps_to_return::TimestampsToReturn,
translate_browse_paths_to_node_ids_request::TranslateBrowsePathsToNodeIdsRequest,
translate_browse_paths_to_node_ids_response::TranslateBrowsePathsToNodeIdsResponse,
user_name_identity_token::UserNameIdentityToken,
variant::Variant,
write_request::WriteRequest,
Expand Down
24 changes: 24 additions & 0 deletions src/ua/data_types/translate_browse_paths_to_node_ids_request.rs
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)
}
}
18 changes: 18 additions & 0 deletions src/ua/data_types/translate_browse_paths_to_node_ids_response.rs
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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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)
}
}