diff --git a/src/async_client.rs b/src/async_client.rs index 8106c2c2..b771d365 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -271,6 +271,53 @@ impl AsyncClient { Ok(results) } + /// Reads historizied values of a node for a given period of time. + /// + /// # Errors + /// + /// This fails only when the entire request fails (e.g. communication error). When the node does + /// not exist or its value attribute cannot be read, the server returns a corresponding + /// [`DataValue`] with the appropriate [`status()`] and with [`value()`] unset. + /// + /// [`status()`]: DataValue::status + /// [`value()`]: DataValue::value + pub async fn read_history_raw( + &self, + node_id: &ua::NodeId, + start_time: ua::DateTime, + end_time: ua::DateTime, + ) -> Result { + let nodes_to_read = [node_id] + .iter() + .map(|node_id| ua::HistoryReadValueId::init().with_node_id(node_id)) + .collect::>(); + + let history_read_details = ua::ReadRawModifiedDetails::init() + .with_is_read_modified(ua::Boolean::new(false)) + .with_start_time(start_time) + .with_end_time(end_time) + .with_num_values_per_node(ua::UInt32::new(0)) + .with_return_bounds(ua::Boolean::new(false)); + let request = ua::HistoryReadRequest::init() + // TODO: Add method argument for this? We return timestamps in `DataValue` and they + // should not end up always being `None` by default. + .with_timestamps_to_return(&ua::TimestampsToReturn::BOTH) + .with_history_read_details(&history_read_details) + .with_nodes_to_read(&nodes_to_read); + + let response = self.service_request(request).await?; + + let Some(results) = response.results() else { + return Err(Error::internal("read should return results")); + }; + + results + .as_slice() + .first() + .map(std::borrow::ToOwned::to_owned) + .ok_or(Error::internal("unexpected number of read results")) + } + /// Writes node value. /// /// # Errors diff --git a/src/ua/data_types.rs b/src/ua/data_types.rs index 92913441..8cd64c98 100644 --- a/src/ua/data_types.rs +++ b/src/ua/data_types.rs @@ -48,6 +48,11 @@ mod expanded_node_id; mod extension_object; mod filter_operator; mod guid; +mod history_data; +mod history_read_request; +mod history_read_response; +mod history_read_result; +mod history_read_value_id; mod issued_identity_token; mod literal_operand; mod localized_text; @@ -62,6 +67,7 @@ mod node_id; mod node_id_type; mod qualified_name; mod range; +mod read_raw_modified_details; mod read_request; mod read_response; mod read_value_id; @@ -136,6 +142,11 @@ pub use self::{ extension_object::ExtensionObject, filter_operator::FilterOperator, guid::Guid, + history_data::HistoryData, + history_read_request::HistoryReadRequest, + history_read_response::HistoryReadResponse, + history_read_result::HistoryReadResult, + history_read_value_id::HistoryReadValueId, issued_identity_token::IssuedIdentityToken, literal_operand::LiteralOperand, localized_text::LocalizedText, @@ -154,6 +165,7 @@ pub use self::{ node_id_type::NodeIdType, qualified_name::QualifiedName, range::Range, + read_raw_modified_details::ReadRawModifiedDetails, read_request::ReadRequest, read_response::ReadResponse, read_value_id::ReadValueId, diff --git a/src/ua/data_types/history_data.rs b/src/ua/data_types/history_data.rs new file mode 100644 index 00000000..b639bb90 --- /dev/null +++ b/src/ua/data_types/history_data.rs @@ -0,0 +1,23 @@ +use crate::{DataType as _, ua}; + +crate::data_type!(HistoryData); + +impl HistoryData { + #[must_use] + pub const fn is_empty(&self) -> bool { + self.0.dataValuesSize == 0 + } + #[must_use] + pub const fn len(&self) -> usize { + self.0.dataValuesSize + } + + #[must_use] + pub fn get(&self, i: usize) -> Option { + if i < self.0.dataValuesSize { + Some(unsafe { ua::DataValue::clone_raw(&self.0.dataValues.add(i).read()) }) + } else { + None + } + } +} diff --git a/src/ua/data_types/history_read_request.rs b/src/ua/data_types/history_read_request.rs new file mode 100644 index 00000000..b8eafc6d --- /dev/null +++ b/src/ua/data_types/history_read_request.rs @@ -0,0 +1,44 @@ +use crate::{DataType as _, ServiceRequest, ua}; + +crate::data_type!(HistoryReadRequest); + +impl HistoryReadRequest { + #[must_use] + pub fn with_timestamps_to_return( + mut self, + timestamps_to_return: &ua::TimestampsToReturn, + ) -> Self { + timestamps_to_return.clone_into_raw(&mut self.0.timestampsToReturn); + self + } + + #[must_use] + pub fn with_nodes_to_read(mut self, nodes_to_read: &[ua::HistoryReadValueId]) -> Self { + let array = ua::Array::from_slice(nodes_to_read); + array.move_into_raw(&mut self.0.nodesToReadSize, &mut self.0.nodesToRead); + self + } + + #[must_use] + pub fn with_history_read_details( + mut self, + history_read_details: &ua::ReadRawModifiedDetails, + ) -> Self { + history_read_details + .to_extension_object() + .clone_into_raw(&mut self.0.historyReadDetails); + self + } +} + +impl ServiceRequest for HistoryReadRequest { + type Response = ua::HistoryReadResponse; + + 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) + } +} diff --git a/src/ua/data_types/history_read_response.rs b/src/ua/data_types/history_read_response.rs new file mode 100644 index 00000000..fce2f2f4 --- /dev/null +++ b/src/ua/data_types/history_read_response.rs @@ -0,0 +1,19 @@ +use crate::{DataType as _, ServiceResponse, ua}; + +crate::data_type!(HistoryReadResponse); + +impl HistoryReadResponse { + #[must_use] + pub fn results(&self) -> Option> { + // TODO: Adjust signature to return non-owned value instead. + ua::Array::from_raw_parts(self.0.resultsSize, self.0.results) + } +} + +impl ServiceResponse for HistoryReadResponse { + type Request = ua::HistoryReadRequest; + + fn response_header(&self) -> &ua::ResponseHeader { + ua::ResponseHeader::raw_ref(&self.0.responseHeader) + } +} diff --git a/src/ua/data_types/history_read_result.rs b/src/ua/data_types/history_read_result.rs new file mode 100644 index 00000000..98d2d560 --- /dev/null +++ b/src/ua/data_types/history_read_result.rs @@ -0,0 +1,20 @@ +use crate::{DataType as _, ua}; + +crate::data_type!(HistoryReadResult); + +impl HistoryReadResult { + #[must_use] + pub fn status(&self) -> &ua::StatusCode { + ua::StatusCode::raw_ref(&self.0.statusCode) + } + + #[must_use] + pub fn continuation_point(&self) -> &ua::String { + ua::String::raw_ref(&self.0.continuationPoint) + } + + #[must_use] + pub fn history_data(&mut self) -> &ua::ExtensionObject { + ua::ExtensionObject::raw_ref(&self.0.historyData) + } +} diff --git a/src/ua/data_types/history_read_value_id.rs b/src/ua/data_types/history_read_value_id.rs new file mode 100644 index 00000000..ae701638 --- /dev/null +++ b/src/ua/data_types/history_read_value_id.rs @@ -0,0 +1,11 @@ +use crate::{DataType as _, ua}; + +crate::data_type!(HistoryReadValueId); + +impl HistoryReadValueId { + #[must_use] + pub fn with_node_id(mut self, node_id: &ua::NodeId) -> Self { + node_id.clone_into_raw(&mut self.0.nodeId); + self + } +} diff --git a/src/ua/data_types/read_raw_modified_details.rs b/src/ua/data_types/read_raw_modified_details.rs new file mode 100644 index 00000000..354661ba --- /dev/null +++ b/src/ua/data_types/read_raw_modified_details.rs @@ -0,0 +1,40 @@ +use crate::{DataType as _, ua}; + +crate::data_type!(ReadRawModifiedDetails); + +impl ReadRawModifiedDetails { + #[must_use] + pub fn with_is_read_modified(mut self, is_read_modified: ua::Boolean) -> Self { + is_read_modified.move_into_raw(&mut self.0.isReadModified); + self + } + + #[must_use] + pub fn with_start_time(mut self, start_time: ua::DateTime) -> Self { + start_time.move_into_raw(&mut self.0.startTime); + self + } + + #[must_use] + pub fn with_end_time(mut self, end_time: ua::DateTime) -> Self { + end_time.move_into_raw(&mut self.0.endTime); + self + } + + #[must_use] + pub fn with_num_values_per_node(mut self, num_values_per_node: ua::UInt32) -> Self { + num_values_per_node.move_into_raw(&mut self.0.numValuesPerNode); + self + } + + #[must_use] + pub fn with_return_bounds(mut self, return_bounds: ua::Boolean) -> Self { + return_bounds.move_into_raw(&mut self.0.returnBounds); + self + } + + #[must_use] + pub fn to_extension_object(&self) -> ua::ExtensionObject { + ua::ExtensionObject::new(self) + } +}