1use crate::opc_da::errors::{OpcError, OpcResult};
2use async_trait::async_trait;
3use std::sync::Arc;
4use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
5use tokio::sync::mpsc;
6use uuid::Uuid;
7
8#[cfg(feature = "test-support")]
9use mockall::automock;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct TagValue {
30 pub tag_id: String,
32 pub value: String,
34 pub quality: String,
36 pub timestamp: String,
38}
39
40#[derive(Debug, Clone, PartialEq)]
51pub enum OpcValue {
52 String(String),
54 Int(i32),
56 Float(f64),
58 Bool(bool),
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct WriteResult {
78 pub tag_id: String,
80 pub success: bool,
82 pub error: Option<String>,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum BrowseNamespace {
89 Flat,
91 Hierarchical,
93 Unknown,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub struct BrowseCapabilities {
101 pub namespace: BrowseNamespace,
103 pub supports_da3: bool,
105 pub supports_da2: bool,
107 pub max_page_size: u32,
109}
110
111macro_rules! opaque_browse_token {
112 ($name:ident, $doc:literal) => {
113 #[doc = $doc]
114 #[derive(Clone, Copy, PartialEq, Eq, Hash)]
115 pub struct $name(Uuid);
116
117 impl $name {
118 pub(crate) fn new() -> Self {
119 Self(Uuid::new_v4())
120 }
121
122 pub fn parse(value: &str) -> Result<Self, uuid::Error> {
127 value.parse()
128 }
129 }
130
131 impl std::fmt::Debug for $name {
132 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133 formatter
134 .debug_tuple(stringify!($name))
135 .field(&self.0)
136 .finish()
137 }
138 }
139
140 impl std::fmt::Display for $name {
141 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142 self.0.fmt(formatter)
143 }
144 }
145
146 impl std::str::FromStr for $name {
147 type Err = uuid::Error;
148
149 fn from_str(value: &str) -> Result<Self, Self::Err> {
150 value.parse().map(Self)
151 }
152 }
153 };
154}
155
156opaque_browse_token!(
157 BrowseSessionToken,
158 "Opaque identifier for a browse session owned by the COM worker."
159);
160opaque_browse_token!(
161 BrowseNodeToken,
162 "Opaque identifier for a node returned by a browse session."
163);
164opaque_browse_token!(
165 BrowsePageToken,
166 "Opaque continuation token for the next bounded browse page."
167);
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub enum BrowseNodeKind {
172 Branch,
174 Item,
176 BranchAndItem,
178}
179
180impl BrowseNodeKind {
181 pub fn has_children(self) -> bool {
183 matches!(self, Self::Branch | Self::BranchAndItem)
184 }
185
186 pub fn is_item(self) -> bool {
188 matches!(self, Self::Item | Self::BranchAndItem)
189 }
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194pub enum BrowseNodeFilter {
195 Branches,
197 Items,
199 All,
201}
202
203#[derive(Debug, Clone, PartialEq, Eq)]
205pub struct BrowseNode {
206 pub token: BrowseNodeToken,
208 pub name: String,
210 pub item_id: Option<String>,
212 pub kind: BrowseNodeKind,
214}
215
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
218pub struct BrowsePageRequest {
219 pub parent: Option<BrowseNodeToken>,
221 pub filter: BrowseNodeFilter,
223 pub max_elements: u32,
225 pub continuation: Option<BrowsePageToken>,
227}
228
229#[derive(Debug, Clone, PartialEq, Eq)]
231pub struct BrowsePage {
232 pub nodes: Vec<BrowseNode>,
234 pub continuation: Option<BrowsePageToken>,
236}
237
238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
240pub struct InventoryOptions {
241 pub batch_size: u32,
243 pub max_entries: Option<u64>,
245}
246
247impl Default for InventoryOptions {
248 fn default() -> Self {
249 Self {
250 batch_size: 100,
251 max_entries: None,
252 }
253 }
254}
255
256#[derive(Debug, Clone, PartialEq, Eq)]
258pub struct InventoryEntry {
259 pub display_name: String,
261 pub item_id: String,
263 pub kind: BrowseNodeKind,
265 pub breadcrumbs: Vec<String>,
267}
268
269#[derive(Debug, Clone, PartialEq)]
271pub struct InventoryProgress {
272 pub branches_visited: u64,
273 pub entries_seen: u64,
274 pub unique_items: u64,
275 pub active_time_ms: u64,
276 pub paused_time_ms: u64,
277 pub items_per_second: f64,
278 pub estimated_remaining_ms: Option<u64>,
279}
280
281#[derive(Debug, Clone, PartialEq, Eq)]
283pub struct InventoryCompleted {
284 pub complete: bool,
285 pub cancelled: bool,
286 pub truncated: bool,
287 pub warning: Option<String>,
288 pub capabilities: BrowseCapabilities,
289}
290
291#[derive(Debug, Clone, PartialEq)]
293pub enum InventoryEvent {
294 Entry(InventoryEntry),
295 Progress(InventoryProgress),
296 Completed(InventoryCompleted),
297}
298
299#[derive(Debug)]
300struct InventoryControlState {
301 cancelled: AtomicBool,
302 paused: AtomicBool,
303}
304
305#[derive(Clone, Debug)]
307pub struct InventoryControl {
308 state: Arc<InventoryControlState>,
309}
310
311impl InventoryControl {
312 pub(crate) fn new() -> Self {
313 Self {
314 state: Arc::new(InventoryControlState {
315 cancelled: AtomicBool::new(false),
316 paused: AtomicBool::new(false),
317 }),
318 }
319 }
320
321 pub fn cancel(&self) {
323 self.state.cancelled.store(true, Ordering::Release);
324 }
325
326 pub fn pause(&self) {
328 self.state.paused.store(true, Ordering::Release);
329 }
330
331 pub fn resume(&self) {
333 self.state.paused.store(false, Ordering::Release);
334 }
335
336 pub fn is_cancelled(&self) -> bool {
338 self.state.cancelled.load(Ordering::Acquire)
339 }
340
341 pub(crate) fn is_paused(&self) -> bool {
342 self.state.paused.load(Ordering::Acquire)
343 }
344}
345
346pub struct InventoryStream {
348 receiver: mpsc::Receiver<OpcResult<InventoryEvent>>,
349 control: InventoryControl,
350 worker: Option<std::thread::JoinHandle<()>>,
351}
352
353impl InventoryStream {
354 pub(crate) fn new(
355 receiver: mpsc::Receiver<OpcResult<InventoryEvent>>,
356 control: InventoryControl,
357 worker: std::thread::JoinHandle<()>,
358 ) -> Self {
359 Self {
360 receiver,
361 control,
362 worker: Some(worker),
363 }
364 }
365
366 pub async fn message(&mut self) -> Option<OpcResult<InventoryEvent>> {
372 self.receiver.recv().await
373 }
374
375 pub fn control(&self) -> InventoryControl {
377 self.control.clone()
378 }
379
380 pub fn cancel(&self) {
382 self.control.cancel();
383 }
384
385 pub fn pause(&self) {
387 self.control.pause();
388 }
389
390 pub fn resume(&self) {
392 self.control.resume();
393 }
394}
395
396impl Drop for InventoryStream {
397 fn drop(&mut self) {
398 self.receiver.close();
401 self.control.cancel();
402 if let Some(worker) = self.worker.take() {
403 let _ = worker.join();
404 }
405 }
406}
407
408#[cfg(test)]
409mod inventory_stream_tests {
410 use super::*;
411
412 #[test]
413 fn dropping_inventory_stream_cancels_and_joins_worker() {
414 let control = InventoryControl::new();
415 let worker_control = control.clone();
416 let finished = Arc::new(AtomicBool::new(false));
417 let worker_finished = Arc::clone(&finished);
418 let (_sender, receiver) = mpsc::channel(1);
419 let worker = std::thread::spawn(move || {
420 while !worker_control.is_cancelled() {
421 std::thread::yield_now();
422 }
423 worker_finished.store(true, Ordering::Release);
424 });
425
426 drop(InventoryStream::new(receiver, control, worker));
427 assert!(finished.load(Ordering::Acquire));
428 }
429}
430
431#[cfg_attr(feature = "test-support", automock)]
436#[async_trait]
437pub trait OpcProvider: Send + Sync {
438 async fn list_servers(&self, host: &str) -> OpcResult<Vec<String>>;
444
445 async fn browse_tags(
451 &self,
452 server: &str,
453 max_tags: usize,
454 progress: Arc<AtomicUsize>,
455 tags_sink: Arc<std::sync::Mutex<Vec<String>>>,
456 ) -> OpcResult<Vec<String>>;
457
458 async fn browse_capabilities(&self, server: &str) -> OpcResult<BrowseCapabilities> {
464 let _ = server;
465 Err(OpcError::NotImplemented(
466 "Native browsing is not implemented by this provider".to_string(),
467 ))
468 }
469
470 async fn open_browse_session(&self, server: &str) -> OpcResult<BrowseSessionToken> {
476 let _ = server;
477 Err(OpcError::NotImplemented(
478 "Native browsing is not implemented by this provider".to_string(),
479 ))
480 }
481
482 async fn browse_page(
488 &self,
489 session: &BrowseSessionToken,
490 request: BrowsePageRequest,
491 ) -> OpcResult<BrowsePage> {
492 let _ = (session, request);
493 Err(OpcError::NotImplemented(
494 "Native browsing is not implemented by this provider".to_string(),
495 ))
496 }
497
498 async fn close_browse_session(&self, session: &BrowseSessionToken) -> OpcResult<()> {
503 let _ = session;
504 Err(OpcError::NotImplemented(
505 "Native browsing is not implemented by this provider".to_string(),
506 ))
507 }
508
509 async fn start_inventory(
515 &self,
516 server: &str,
517 options: InventoryOptions,
518 ) -> OpcResult<InventoryStream> {
519 let _ = (server, options);
520 Err(OpcError::NotImplemented(
521 "Namespace inventory is not implemented by this provider".to_string(),
522 ))
523 }
524
525 async fn read_tag_values(&self, server: &str, tag_ids: Vec<String>)
531 -> OpcResult<Vec<TagValue>>;
532
533 async fn write_tag_value(
539 &self,
540 server: &str,
541 tag_id: &str,
542 value: OpcValue,
543 ) -> OpcResult<WriteResult>;
544}