Skip to main content

opc_da_client/
provider.rs

1use crate::opc_da::errors::{OpcError, OpcResult};
2use async_trait::async_trait;
3use std::sync::Arc;
4use std::sync::atomic::AtomicUsize;
5use uuid::Uuid;
6
7#[cfg(feature = "test-support")]
8use mockall::automock;
9
10/// A single tag's read result.
11///
12/// Returned by [`OpcProvider::read_tag_values`].
13///
14/// # Examples
15///
16/// ```
17/// use opc_da_client::TagValue;
18///
19/// let tv = TagValue {
20///     tag_id: "Simulation.Random.1".to_string(),
21///     value: "42.5".to_string(),
22///     quality: "Good".to_string(),
23///     timestamp: "2026-01-01 00:00:00".to_string(),
24/// };
25/// assert_eq!(tv.tag_id, "Simulation.Random.1");
26/// ```
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct TagValue {
29    /// The fully qualified tag identifier (e.g., `"Channel1.Device1.Tag1"`).
30    pub tag_id: String,
31    /// The current value as a display string.
32    pub value: String,
33    /// OPC quality indicator (e.g., `"Good"`, `"Bad"`, or `"Uncertain"`).
34    pub quality: String,
35    /// Timestamp of the last value change, formatted as a local time string.
36    pub timestamp: String,
37}
38
39/// Typed value to write to an OPC DA tag.
40///
41/// # Examples
42///
43/// ```
44/// use opc_da_client::OpcValue;
45///
46/// let v = OpcValue::Float(3.14);
47/// assert_eq!(v, OpcValue::Float(3.14));
48/// ```
49#[derive(Debug, Clone, PartialEq)]
50pub enum OpcValue {
51    /// String value (`VT_BSTR`) — server may coerce to target type.
52    String(String),
53    /// 32-bit integer (`VT_I4`).
54    Int(i32),
55    /// 64-bit float (`VT_R8`).
56    Float(f64),
57    /// Boolean (`VT_BOOL`).
58    Bool(bool),
59}
60
61/// Result of a single write operation.
62///
63/// # Examples
64///
65/// ```
66/// use opc_da_client::WriteResult;
67///
68/// let wr = WriteResult {
69///     tag_id: "Tag1".to_string(),
70///     success: true,
71///     error: None,
72/// };
73/// assert!(wr.success);
74/// ```
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct WriteResult {
77    /// The tag that was written to.
78    pub tag_id: String,
79    /// Whether the write succeeded.
80    pub success: bool,
81    /// Error message if the write failed, `None` on success.
82    pub error: Option<String>,
83}
84
85/// OPC DA address-space organization reported by a server.
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum BrowseNamespace {
88    /// All item IDs live in one flat namespace.
89    Flat,
90    /// Items are organized under browsable branches.
91    Hierarchical,
92    /// The server supports DA 3.0 browsing but does not expose the DA 2.x
93    /// namespace query needed to classify its organization.
94    Unknown,
95}
96
97/// Native browse features available from an OPC DA server.
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub struct BrowseCapabilities {
100    /// Address-space organization reported by the server.
101    pub namespace: BrowseNamespace,
102    /// Whether native OPC DA 3.0 `IOPCBrowse` is available.
103    pub supports_da3: bool,
104    /// Whether OPC DA 2.x `IOPCBrowseServerAddressSpace` is available.
105    pub supports_da2: bool,
106    /// Largest page size accepted by [`OpcProvider::browse_page`].
107    pub max_page_size: u32,
108}
109
110macro_rules! opaque_browse_token {
111    ($name:ident, $doc:literal) => {
112        #[doc = $doc]
113        #[derive(Clone, Copy, PartialEq, Eq, Hash)]
114        pub struct $name(Uuid);
115
116        impl $name {
117            pub(crate) fn new() -> Self {
118                Self(Uuid::new_v4())
119            }
120
121            /// Parse a token previously encoded with [`ToString::to_string`].
122            ///
123            /// This allows transport adapters to round-trip opaque tokens
124            /// without accessing their internal representation.
125            pub fn parse(value: &str) -> Result<Self, uuid::Error> {
126                value.parse()
127            }
128        }
129
130        impl std::fmt::Debug for $name {
131            fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132                formatter
133                    .debug_tuple(stringify!($name))
134                    .field(&self.0)
135                    .finish()
136            }
137        }
138
139        impl std::fmt::Display for $name {
140            fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141                self.0.fmt(formatter)
142            }
143        }
144
145        impl std::str::FromStr for $name {
146            type Err = uuid::Error;
147
148            fn from_str(value: &str) -> Result<Self, Self::Err> {
149                value.parse().map(Self)
150            }
151        }
152    };
153}
154
155opaque_browse_token!(
156    BrowseSessionToken,
157    "Opaque identifier for a browse session owned by the COM worker."
158);
159opaque_browse_token!(
160    BrowseNodeToken,
161    "Opaque identifier for a node returned by a browse session."
162);
163opaque_browse_token!(
164    BrowsePageToken,
165    "Opaque continuation token for the next bounded browse page."
166);
167
168/// Kinds of nodes returned by a native browse.
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170pub enum BrowseNodeKind {
171    /// A branch that can have children but is not itself an item.
172    Branch,
173    /// An item that has no browsable children.
174    Item,
175    /// A node that is both an item and a branch.
176    BranchAndItem,
177}
178
179impl BrowseNodeKind {
180    /// Returns whether the node can be used as the parent of another browse.
181    pub fn has_children(self) -> bool {
182        matches!(self, Self::Branch | Self::BranchAndItem)
183    }
184}
185
186/// Node-kind filter for a one-level browse request.
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub enum BrowseNodeFilter {
189    /// Return only branches.
190    Branches,
191    /// Return only items.
192    Items,
193    /// Return both branches and items.
194    All,
195}
196
197/// One address-space node returned by [`OpcProvider::browse_page`].
198#[derive(Debug, Clone, PartialEq, Eq)]
199pub struct BrowseNode {
200    /// Opaque token used to browse this node's immediate children.
201    pub token: BrowseNodeToken,
202    /// Display name relative to the requested parent.
203    pub name: String,
204    /// Exact fully-qualified item ID when the server supplies one.
205    pub item_id: Option<String>,
206    /// Whether this node is a branch, item, or both.
207    pub kind: BrowseNodeKind,
208}
209
210/// Parameters for one bounded, non-recursive browse operation.
211#[derive(Debug, Clone, Copy, PartialEq, Eq)]
212pub struct BrowsePageRequest {
213    /// Parent node, or `None` to browse the root.
214    pub parent: Option<BrowseNodeToken>,
215    /// Node kinds to return.
216    pub filter: BrowseNodeFilter,
217    /// Maximum number of nodes to return.
218    pub max_elements: u32,
219    /// Opaque continuation returned by the preceding page.
220    pub continuation: Option<BrowsePageToken>,
221}
222
223/// One bounded page of immediate address-space children.
224#[derive(Debug, Clone, PartialEq, Eq)]
225pub struct BrowsePage {
226    /// Nodes returned in this page.
227    pub nodes: Vec<BrowseNode>,
228    /// Opaque token for the next page, or `None` when enumeration is complete.
229    pub continuation: Option<BrowsePageToken>,
230}
231
232/// Async trait for OPC DA operations.
233///
234/// This is the stable public API. Backend implementations provide
235/// the actual COM/DCOM interaction.
236#[cfg_attr(feature = "test-support", automock)]
237#[async_trait]
238pub trait OpcProvider: Send + Sync {
239    /// List available OPC DA servers on the given host.
240    ///
241    /// # Errors
242    /// Returns `Err` if COM initialization fails or the server registry
243    /// cannot be enumerated.
244    async fn list_servers(&self, host: &str) -> OpcResult<Vec<String>>;
245
246    /// Browse tags recursively, pushing discoveries to `tags_sink`.
247    ///
248    /// # Errors
249    /// Returns `Err` if the server connection fails, the `ProgID` cannot be
250    /// resolved, or the namespace walk encounters an unrecoverable error.
251    async fn browse_tags(
252        &self,
253        server: &str,
254        max_tags: usize,
255        progress: Arc<AtomicUsize>,
256        tags_sink: Arc<std::sync::Mutex<Vec<String>>>,
257    ) -> OpcResult<Vec<String>>;
258
259    /// Return the native browse capabilities of an OPC DA server.
260    ///
261    /// # Errors
262    /// Returns `Err` if the server cannot be connected or exposes no supported
263    /// OPC DA browse interface.
264    async fn browse_capabilities(&self, server: &str) -> OpcResult<BrowseCapabilities> {
265        let _ = server;
266        Err(OpcError::NotImplemented(
267            "Native browsing is not implemented by this provider".to_string(),
268        ))
269    }
270
271    /// Open an isolated native browse session with its own server connection.
272    ///
273    /// # Errors
274    /// Returns `Err` if the server cannot be connected, browsing is unsupported,
275    /// or the worker's bounded session capacity has been reached.
276    async fn open_browse_session(&self, server: &str) -> OpcResult<BrowseSessionToken> {
277        let _ = server;
278        Err(OpcError::NotImplemented(
279            "Native browsing is not implemented by this provider".to_string(),
280        ))
281    }
282
283    /// Browse one bounded level of an open native browse session.
284    ///
285    /// # Errors
286    /// Returns `Err` for invalid, closed, or expired tokens; invalid page sizes;
287    /// unsupported requests; or underlying OPC browse failures.
288    async fn browse_page(
289        &self,
290        session: &BrowseSessionToken,
291        request: BrowsePageRequest,
292    ) -> OpcResult<BrowsePage> {
293        let _ = (session, request);
294        Err(OpcError::NotImplemented(
295            "Native browsing is not implemented by this provider".to_string(),
296        ))
297    }
298
299    /// Explicitly close a native browse session and release its server connection.
300    ///
301    /// # Errors
302    /// Returns `Err` if the session token is invalid, expired, or already closed.
303    async fn close_browse_session(&self, session: &BrowseSessionToken) -> OpcResult<()> {
304        let _ = session;
305        Err(OpcError::NotImplemented(
306            "Native browsing is not implemented by this provider".to_string(),
307        ))
308    }
309
310    /// Read current values for the given tag IDs.
311    ///
312    /// # Errors
313    /// Returns `Err` if the server connection fails, no items can be added
314    /// to the OPC group, or the synchronous read operation fails.
315    async fn read_tag_values(&self, server: &str, tag_ids: Vec<String>)
316    -> OpcResult<Vec<TagValue>>;
317
318    /// Write a value to a single OPC DA tag.
319    ///
320    /// # Errors
321    /// Returns `Err` if the server connection fails, the tag cannot be added
322    /// to the OPC group, or the synchronous write operation fails.
323    async fn write_tag_value(
324        &self,
325        server: &str,
326        tag_id: &str,
327        value: OpcValue,
328    ) -> OpcResult<WriteResult>;
329}