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}