Skip to main content

opc_da_client/backend/
connector.rs

1//! Abstractions for OPC DA server connectivity.
2//!
3//! Defines the [`ServerConnector`], [`ConnectedServer`], and [`ConnectedGroup`]
4//! traits that decouple [`super::opc_da::OpcDaWrapper`] from concrete COM types.
5//! This enables mock implementations for unit testing without a live COM server.
6
7pub use crate::bindings::da::tagOPCITEMDEF;
8pub use crate::bindings::da::{tagOPCITEMRESULT, tagOPCITEMSTATE};
9use crate::opc_da::client::StringIterator;
10pub use crate::opc_da::utils::RemoteArray;
11pub use windows::Win32::System::Variant::VARIANT;
12
13/// Factory for connecting to OPC DA servers.
14///
15/// Abstracts the concrete `v2::Client` usage so that tests can inject mocks
16/// that return pre-configured server/group results without a live COM runtime.
17///
18/// # Errors
19///
20/// All methods return `anyhow::Result` — implementations should wrap COM errors
21/// with contextual messages.
22pub trait ServerConnector: Send + Sync {
23    /// The server facade type returned by [`Self::connect`].
24    type Server: ConnectedServer;
25
26    /// Enumerate all OPC DA server ProgIDs on the local machine.
27    ///
28    /// # Errors
29    ///
30    /// Returns an error if the COM registry enumeration fails.
31    fn enumerate_servers(&self) -> anyhow::Result<Vec<String>>;
32
33    /// Connect to the named OPC DA server and return a server facade.
34    ///
35    /// # Errors
36    ///
37    /// Returns an error if the COM server cannot be created or connected.
38    fn connect(&self, server_name: &str) -> anyhow::Result<Self::Server>;
39}
40
41/// Facade over a connected OPC DA server instance.
42///
43/// Wraps namespace browsing and group management operations in Rust-native types.
44///
45/// # Errors
46///
47/// All methods return `anyhow::Result` — COM errors are propagated with context.
48pub trait ConnectedServer {
49    /// The group facade type returned by [`Self::add_group`].
50    type Group: ConnectedGroup;
51
52    /// Query the server's namespace organization type.
53    ///
54    /// Returns `OPC_NS_FLAT` or `OPC_NS_HIERARCHICAL` as a `u32`.
55    ///
56    /// # Errors
57    ///
58    /// Returns an error if the COM call fails.
59    fn query_organization(&self) -> anyhow::Result<u32>;
60
61    /// Browse the server's address space for item IDs of the given type.
62    ///
63    /// # Errors
64    ///
65    /// Returns an error if the COM browse call fails.
66    fn browse_opc_item_ids(
67        &self,
68        browse_type: u32,
69        filter: Option<&str>,
70        data_type: u16,
71        access_rights: u32,
72    ) -> anyhow::Result<StringIterator>;
73
74    /// Change the current browse position (e.g., navigate into/out of branches).
75    ///
76    /// # Errors
77    ///
78    /// Returns an error if the position change is rejected by the server.
79    fn change_browse_position(&self, direction: u32, name: &str) -> anyhow::Result<()>;
80
81    /// Resolve a browse name to its fully-qualified item ID.
82    ///
83    /// # Errors
84    ///
85    /// Returns an error if the server cannot resolve the item name.
86    fn get_item_id(&self, item_name: &str) -> anyhow::Result<String>;
87
88    /// Add a new OPC group to this server connection.
89    ///
90    /// # Errors
91    ///
92    /// Returns an error if the group creation fails.
93    #[allow(clippy::too_many_arguments)]
94    fn add_group(
95        &self,
96        name: &str,
97        active: bool,
98        update_rate: u32,
99        client_handle: u32,
100        time_bias: i32,
101        percent_deadband: f32,
102        locale_id: u32,
103        revised_update_rate: &mut u32,
104        server_handle: &mut u32,
105    ) -> anyhow::Result<Self::Group>;
106
107    /// Remove an OPC group by its server-assigned handle.
108    ///
109    /// # Errors
110    ///
111    /// Returns an error if the group removal fails.
112    fn remove_group(&self, server_group: u32, force: bool) -> anyhow::Result<()>;
113}
114
115/// Facade over an OPC DA group for item management and I/O.
116///
117/// # Errors
118///
119/// All methods return `anyhow::Result` — COM errors are propagated with context.
120pub trait ConnectedGroup {
121    /// Add items to this group for monitoring.
122    ///
123    /// # Errors
124    ///
125    /// Returns an error if the COM `AddItems` call fails.
126    fn add_items(
127        &self,
128        items: &[tagOPCITEMDEF],
129    ) -> anyhow::Result<(
130        RemoteArray<tagOPCITEMRESULT>,
131        RemoteArray<windows::core::HRESULT>,
132    )>;
133
134    /// Perform a synchronous read of the given server handles.
135    ///
136    /// # Errors
137    ///
138    /// Returns an error if the COM `Read` call fails.
139    fn read(
140        &self,
141        source: crate::bindings::da::tagOPCDATASOURCE,
142        server_handles: &[u32],
143    ) -> anyhow::Result<(
144        RemoteArray<tagOPCITEMSTATE>,
145        RemoteArray<windows::core::HRESULT>,
146    )>;
147
148    /// Write values to the given server handles.
149    ///
150    /// # Errors
151    ///
152    /// Returns an error if the COM `Write` call fails.
153    fn write(
154        &self,
155        server_handles: &[u32],
156        values: &[VARIANT],
157    ) -> anyhow::Result<RemoteArray<windows::core::HRESULT>>;
158}
159
160// ── COM-backed implementations ──────────────────────────────────────
161
162use crate::opc_da::client::v2::{Client, Group, Server};
163use crate::opc_da::client::{
164    BrowseServerAddressSpaceTrait, ClientTrait, ItemMgtTrait, ServerTrait, SyncIoTrait,
165};
166use anyhow::Context;
167
168/// Real COM-backed server connector implementation.
169///
170/// Uses `v2::Client` to enumerate and connect to OPC DA servers via Windows COM.
171pub struct ComConnector;
172
173impl ServerConnector for ComConnector {
174    type Server = ComServer;
175
176    fn enumerate_servers(&self) -> anyhow::Result<Vec<String>> {
177        let client = Client;
178        let guid_iter = client
179            .get_servers()
180            .context("Failed to enumerate OPC DA servers from registry")?;
181
182        let mut servers = Vec::new();
183        for guid in guid_iter.flatten() {
184            // SAFETY: `crate::opc_da::GUID` and `windows::core::GUID` are both
185            // `#[repr(C)]` structs with identical layout (4-byte, 2-byte, 2-byte,
186            // 8-byte array). This is validated by a `const_assert_eq!` in
187            // `opc_da/client/iterator.rs`.
188            let win_guid: windows::core::GUID = unsafe { std::mem::transmute_copy(&guid) };
189            if win_guid == windows::core::GUID::zeroed() {
190                continue;
191            }
192
193            if let Ok(progid) = crate::helpers::guid_to_progid(&win_guid)
194                && !progid.is_empty()
195            {
196                servers.push(progid);
197            }
198        }
199        servers.sort();
200        servers.dedup();
201        Ok(servers)
202    }
203
204    fn connect(&self, server_name: &str) -> anyhow::Result<Self::Server> {
205        let opc_server = crate::helpers::connect_server(server_name)?;
206        Ok(ComServer(opc_server))
207    }
208}
209
210/// COM-backed [`ConnectedServer`] wrapping a `v2::Server`.
211pub struct ComServer(Server);
212
213impl ConnectedServer for ComServer {
214    type Group = ComGroup;
215
216    fn query_organization(&self) -> anyhow::Result<u32> {
217        Ok(self.0.query_organization()?.0.cast_unsigned())
218    }
219
220    fn browse_opc_item_ids(
221        &self,
222        browse_type: u32,
223        filter: Option<&str>,
224        data_type: u16,
225        access_rights: u32,
226    ) -> anyhow::Result<StringIterator> {
227        #[allow(clippy::cast_possible_wrap)]
228        let enum_str = self.0.browse_opc_item_ids(
229            crate::bindings::da::tagOPCBROWSETYPE(browse_type as i32),
230            filter,
231            data_type,
232            access_rights,
233        )?;
234        Ok(StringIterator::new(enum_str))
235    }
236
237    fn change_browse_position(&self, direction: u32, name: &str) -> anyhow::Result<()> {
238        #[allow(clippy::cast_possible_wrap)]
239        Ok(self.0.change_browse_position(
240            crate::bindings::da::tagOPCBROWSEDIRECTION(direction as i32),
241            name,
242        )?)
243    }
244
245    fn get_item_id(&self, item_name: &str) -> anyhow::Result<String> {
246        Ok(self.0.get_item_id(item_name)?)
247    }
248
249    fn add_group(
250        &self,
251        name: &str,
252        active: bool,
253        update_rate: u32,
254        client_handle: u32,
255        time_bias: i32,
256        percent_deadband: f32,
257        locale_id: u32,
258        revised_update_rate: &mut u32,
259        server_handle: &mut u32,
260    ) -> anyhow::Result<Self::Group> {
261        let group = self.0.add_group(
262            name,
263            active,
264            client_handle,
265            update_rate,
266            locale_id,
267            time_bias,
268            percent_deadband,
269            revised_update_rate,
270            server_handle,
271        )?;
272        Ok(ComGroup(group))
273    }
274
275    fn remove_group(&self, server_group: u32, force: bool) -> anyhow::Result<()> {
276        Ok(self.0.remove_group(server_group, force)?)
277    }
278}
279
280/// COM-backed [`ConnectedGroup`] wrapping a `v2::Group`.
281pub struct ComGroup(Group);
282
283impl ConnectedGroup for ComGroup {
284    fn add_items(
285        &self,
286        items: &[tagOPCITEMDEF],
287    ) -> anyhow::Result<(
288        RemoteArray<tagOPCITEMRESULT>,
289        RemoteArray<windows::core::HRESULT>,
290    )> {
291        Ok(self.0.add_items(items)?)
292    }
293
294    fn read(
295        &self,
296        source: crate::bindings::da::tagOPCDATASOURCE,
297        server_handles: &[u32],
298    ) -> anyhow::Result<(
299        RemoteArray<tagOPCITEMSTATE>,
300        RemoteArray<windows::core::HRESULT>,
301    )> {
302        Ok(self.0.read(source, server_handles)?)
303    }
304
305    fn write(
306        &self,
307        server_handles: &[u32],
308        values: &[VARIANT],
309    ) -> anyhow::Result<RemoteArray<windows::core::HRESULT>> {
310        Ok(self.0.write(server_handles, values)?)
311    }
312}