1use crate::opc_da::errors::{OpcError, OpcResult};
2use async_trait::async_trait;
3use std::sync::Arc;
4use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
5use std::time::Duration;
6use tokio::sync::mpsc;
7use uuid::Uuid;
8
9#[cfg(feature = "test-support")]
10use mockall::automock;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct TagValue {
32 pub tag_id: String,
34 pub value: String,
40 pub quality: String,
42 pub timestamp: String,
44}
45
46#[derive(Debug, Clone, PartialEq)]
57pub enum OpcValue {
58 String(String),
60 Int(i32),
62 Float(f64),
64 Bool(bool),
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct WriteResult {
84 pub tag_id: String,
86 pub success: bool,
88 pub error: Option<String>,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum BrowseNamespace {
95 Flat,
97 Hierarchical,
99 Unknown,
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub struct BrowseCapabilities {
107 pub namespace: BrowseNamespace,
109 pub supports_da3: bool,
111 pub supports_da2: bool,
113 pub max_page_size: u32,
115}
116
117macro_rules! opaque_browse_token {
118 ($name:ident, $doc:literal) => {
119 #[doc = $doc]
120 #[derive(Clone, Copy, PartialEq, Eq, Hash)]
121 pub struct $name(Uuid);
122
123 impl $name {
124 pub(crate) fn new() -> Self {
125 Self(Uuid::new_v4())
126 }
127
128 pub fn parse(value: &str) -> Result<Self, uuid::Error> {
133 value.parse()
134 }
135 }
136
137 impl std::fmt::Debug for $name {
138 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139 formatter
140 .debug_tuple(stringify!($name))
141 .field(&self.0)
142 .finish()
143 }
144 }
145
146 impl std::fmt::Display for $name {
147 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 self.0.fmt(formatter)
149 }
150 }
151
152 impl std::str::FromStr for $name {
153 type Err = uuid::Error;
154
155 fn from_str(value: &str) -> Result<Self, Self::Err> {
156 value.parse().map(Self)
157 }
158 }
159 };
160}
161
162opaque_browse_token!(
163 BrowseSessionToken,
164 "Opaque identifier for a browse session owned by the COM worker."
165);
166opaque_browse_token!(
167 BrowseNodeToken,
168 "Opaque identifier for a node returned by a browse session."
169);
170opaque_browse_token!(
171 BrowsePageToken,
172 "Opaque continuation token for the next bounded browse page."
173);
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177pub enum BrowseNodeKind {
178 Branch,
180 Item,
182 BranchAndItem,
184}
185
186impl BrowseNodeKind {
187 pub fn has_children(self) -> bool {
189 matches!(self, Self::Branch | Self::BranchAndItem)
190 }
191
192 pub fn is_item(self) -> bool {
194 matches!(self, Self::Item | Self::BranchAndItem)
195 }
196}
197
198#[derive(Debug, Clone, Copy, PartialEq, Eq)]
200pub enum BrowseNodeFilter {
201 Branches,
203 Items,
205 All,
207}
208
209#[derive(Debug, Clone, PartialEq, Eq)]
211pub struct BrowseNode {
212 pub token: BrowseNodeToken,
214 pub name: String,
216 pub item_id: Option<String>,
218 pub kind: BrowseNodeKind,
220}
221
222#[derive(Debug, Clone, Copy, PartialEq, Eq)]
224pub struct BrowsePageRequest {
225 pub parent: Option<BrowseNodeToken>,
227 pub filter: BrowseNodeFilter,
229 pub max_elements: u32,
231 pub continuation: Option<BrowsePageToken>,
233}
234
235#[derive(Debug, Clone, PartialEq, Eq)]
237pub struct BrowsePage {
238 pub nodes: Vec<BrowseNode>,
240 pub continuation: Option<BrowsePageToken>,
242}
243
244pub const MAX_INVENTORY_BATCH_SIZE: u32 = 1_000;
246
247#[derive(Debug, Clone, Copy, PartialEq, Eq)]
249pub struct InventoryOptions {
250 pub batch_size: u32,
252 pub max_entries: Option<u64>,
254}
255
256impl Default for InventoryOptions {
257 fn default() -> Self {
258 Self {
259 batch_size: 100,
260 max_entries: None,
261 }
262 }
263}
264
265#[derive(Debug, Clone, PartialEq, Eq)]
267pub struct InventoryEntry {
268 pub display_name: String,
270 pub item_id: String,
272 pub kind: BrowseNodeKind,
274 pub breadcrumbs: Vec<String>,
276}
277
278#[derive(Debug, Clone, PartialEq)]
280pub struct InventoryProgress {
281 pub branches_visited: u64,
282 pub entries_seen: u64,
283 pub unique_items: u64,
284 pub active_time_ms: u64,
285 pub paused_time_ms: u64,
286 pub items_per_second: f64,
287 pub estimated_remaining_ms: Option<u64>,
288}
289
290#[derive(Debug, Clone, Copy, PartialEq, Eq)]
292pub enum InventorySliceBackend {
293 Da3,
295 Da2,
297}
298
299#[derive(Debug, Clone, PartialEq, Eq)]
305pub struct InventorySliceObservation {
306 pub sequence: u64,
308 pub backend: InventorySliceBackend,
310 pub nodes_returned: u64,
312 pub has_more: bool,
314 pub native_operations: u64,
316 pub elapsed_ms: u64,
318 pub entries_seen: u64,
320 pub unique_items: u64,
322}
323
324#[derive(Debug, Clone, PartialEq, Eq)]
326pub struct InventoryCompleted {
327 pub complete: bool,
328 pub cancelled: bool,
329 pub truncated: bool,
330 pub warning: Option<String>,
331 pub capabilities: BrowseCapabilities,
332}
333
334#[derive(Debug, Clone, PartialEq)]
336pub enum InventoryEvent {
337 Entry(InventoryEntry),
338 Progress(InventoryProgress),
339 Slice(InventorySliceObservation),
340 Completed(InventoryCompleted),
341}
342
343#[derive(Debug)]
344struct InventoryControlState {
345 cancelled: AtomicBool,
346 paused: AtomicBool,
347 pacing_interval_ns: AtomicU64,
348 batch_size: AtomicUsize,
349}
350
351#[derive(Debug, Clone, Copy, PartialEq, Eq)]
353pub struct InventoryPacing {
354 pub min_interval: Duration,
356}
357
358impl Default for InventoryPacing {
359 fn default() -> Self {
360 Self {
361 min_interval: Duration::ZERO,
362 }
363 }
364}
365
366#[derive(Clone, Debug)]
368pub struct InventoryControl {
369 state: Arc<InventoryControlState>,
370}
371
372impl InventoryControl {
373 pub(crate) fn new() -> Self {
374 Self {
375 state: Arc::new(InventoryControlState {
376 cancelled: AtomicBool::new(false),
377 paused: AtomicBool::new(false),
378 pacing_interval_ns: AtomicU64::new(0),
379 batch_size: AtomicUsize::new(0),
380 }),
381 }
382 }
383
384 pub(crate) fn new_with_batch_size(batch_size: u32) -> Self {
385 debug_assert!((1..=MAX_INVENTORY_BATCH_SIZE).contains(&batch_size));
386 let control = Self::new();
387 control
388 .state
389 .batch_size
390 .store(batch_size as usize, Ordering::Release);
391 control
392 }
393
394 pub fn cancel(&self) {
396 self.state.cancelled.store(true, Ordering::Release);
397 }
398
399 pub fn pause(&self) {
401 self.state.paused.store(true, Ordering::Release);
402 }
403
404 pub fn resume(&self) {
406 self.state.paused.store(false, Ordering::Release);
407 }
408
409 pub fn set_pacing(&self, pacing: InventoryPacing) {
413 let nanos = u64::try_from(pacing.min_interval.as_nanos().min(u128::from(u64::MAX)))
414 .unwrap_or(u64::MAX);
415 self.state
416 .pacing_interval_ns
417 .store(nanos, Ordering::Release);
418 }
419
420 pub fn pacing(&self) -> InventoryPacing {
422 InventoryPacing {
423 min_interval: Duration::from_nanos(
424 self.state.pacing_interval_ns.load(Ordering::Acquire),
425 ),
426 }
427 }
428
429 pub fn set_batch_size(&self, batch_size: u32) -> OpcResult<()> {
433 if !(1..=MAX_INVENTORY_BATCH_SIZE).contains(&batch_size) {
434 return Err(OpcError::InvalidState(format!(
435 "Inventory batch size must be between 1 and {MAX_INVENTORY_BATCH_SIZE}"
436 )));
437 }
438 self.state
439 .batch_size
440 .store(batch_size as usize, Ordering::Release);
441 Ok(())
442 }
443
444 pub(crate) fn batch_size(&self) -> Option<u32> {
445 let batch_size = self.state.batch_size.load(Ordering::Acquire);
446 u32::try_from(batch_size).ok().filter(|value| *value != 0)
447 }
448
449 pub fn is_cancelled(&self) -> bool {
451 self.state.cancelled.load(Ordering::Acquire)
452 }
453
454 pub(crate) fn is_paused(&self) -> bool {
455 self.state.paused.load(Ordering::Acquire)
456 }
457}
458
459pub struct InventoryStream {
461 receiver: mpsc::Receiver<OpcResult<InventoryEvent>>,
462 control: InventoryControl,
463 worker: Option<std::thread::JoinHandle<()>>,
464}
465
466impl InventoryStream {
467 pub(crate) fn new(
468 receiver: mpsc::Receiver<OpcResult<InventoryEvent>>,
469 control: InventoryControl,
470 worker: std::thread::JoinHandle<()>,
471 ) -> Self {
472 Self {
473 receiver,
474 control,
475 worker: Some(worker),
476 }
477 }
478
479 pub async fn message(&mut self) -> Option<OpcResult<InventoryEvent>> {
485 self.receiver.recv().await
486 }
487
488 pub fn control(&self) -> InventoryControl {
490 self.control.clone()
491 }
492
493 pub fn cancel(&self) {
495 self.control.cancel();
496 }
497
498 pub fn pause(&self) {
500 self.control.pause();
501 }
502
503 pub fn resume(&self) {
505 self.control.resume();
506 }
507
508 pub fn set_pacing(&self, pacing: InventoryPacing) {
510 self.control.set_pacing(pacing);
511 }
512
513 pub fn set_batch_size(&self, batch_size: u32) -> OpcResult<()> {
515 self.control.set_batch_size(batch_size)
516 }
517}
518
519impl Drop for InventoryStream {
520 fn drop(&mut self) {
521 self.receiver.close();
524 self.control.cancel();
525 if let Some(worker) = self.worker.take() {
526 let _ = worker.join();
527 }
528 }
529}
530
531#[cfg(test)]
532mod inventory_stream_tests {
533 use super::*;
534
535 #[test]
536 fn dropping_inventory_stream_cancels_and_joins_worker() {
537 let control = InventoryControl::new();
538 let worker_control = control.clone();
539 let finished = Arc::new(AtomicBool::new(false));
540 let worker_finished = Arc::clone(&finished);
541 let (_sender, receiver) = mpsc::channel(1);
542 let worker = std::thread::spawn(move || {
543 while !worker_control.is_cancelled() {
544 std::thread::yield_now();
545 }
546 worker_finished.store(true, Ordering::Release);
547 });
548
549 drop(InventoryStream::new(receiver, control, worker));
550 assert!(finished.load(Ordering::Acquire));
551 }
552}
553
554#[cfg(test)]
555mod read_display_fallback_tests {
556 use super::*;
557
558 struct FallbackProvider;
559
560 #[async_trait]
561 impl OpcProvider for FallbackProvider {
562 async fn list_servers(&self, _host: &str) -> OpcResult<Vec<String>> {
563 Ok(Vec::new())
564 }
565
566 async fn browse_tags(
567 &self,
568 _server: &str,
569 _max_tags: usize,
570 _progress: Arc<AtomicUsize>,
571 _tags_sink: Arc<std::sync::Mutex<Vec<String>>>,
572 ) -> OpcResult<Vec<String>> {
573 Ok(Vec::new())
574 }
575
576 async fn read_tag_values(
577 &self,
578 _server: &str,
579 tag_ids: Vec<String>,
580 ) -> OpcResult<Vec<TagValue>> {
581 Ok(tag_ids
582 .into_iter()
583 .map(|tag_id| TagValue {
584 tag_id,
585 value: "AUT".to_string(),
586 quality: "Good".to_string(),
587 timestamp: String::new(),
588 })
589 .collect())
590 }
591
592 async fn write_tag_value(
593 &self,
594 _server: &str,
595 tag_id: &str,
596 _value: OpcValue,
597 ) -> OpcResult<WriteResult> {
598 Ok(WriteResult {
599 tag_id: tag_id.to_string(),
600 success: true,
601 error: None,
602 })
603 }
604 }
605
606 #[tokio::test]
607 async fn display_read_defaults_to_semantic_read() {
608 let values = FallbackProvider
609 .read_tag_values_for_display("Server", vec!["Tag".to_string()])
610 .await
611 .unwrap();
612
613 assert_eq!(values[0].value, "AUT");
614 }
615}
616
617#[cfg_attr(feature = "test-support", automock)]
622#[async_trait]
623pub trait OpcProvider: Send + Sync {
624 async fn list_servers(&self, host: &str) -> OpcResult<Vec<String>>;
630
631 async fn browse_tags(
637 &self,
638 server: &str,
639 max_tags: usize,
640 progress: Arc<AtomicUsize>,
641 tags_sink: Arc<std::sync::Mutex<Vec<String>>>,
642 ) -> OpcResult<Vec<String>>;
643
644 async fn browse_capabilities(&self, server: &str) -> OpcResult<BrowseCapabilities> {
650 let _ = server;
651 Err(OpcError::NotImplemented(
652 "Native browsing is not implemented by this provider".to_string(),
653 ))
654 }
655
656 async fn open_browse_session(&self, server: &str) -> OpcResult<BrowseSessionToken> {
662 let _ = server;
663 Err(OpcError::NotImplemented(
664 "Native browsing is not implemented by this provider".to_string(),
665 ))
666 }
667
668 async fn browse_page(
674 &self,
675 session: &BrowseSessionToken,
676 request: BrowsePageRequest,
677 ) -> OpcResult<BrowsePage> {
678 let _ = (session, request);
679 Err(OpcError::NotImplemented(
680 "Native browsing is not implemented by this provider".to_string(),
681 ))
682 }
683
684 async fn close_browse_session(&self, session: &BrowseSessionToken) -> OpcResult<()> {
689 let _ = session;
690 Err(OpcError::NotImplemented(
691 "Native browsing is not implemented by this provider".to_string(),
692 ))
693 }
694
695 async fn start_inventory(
701 &self,
702 server: &str,
703 options: InventoryOptions,
704 ) -> OpcResult<InventoryStream> {
705 let _ = (server, options);
706 Err(OpcError::NotImplemented(
707 "Namespace inventory is not implemented by this provider".to_string(),
708 ))
709 }
710
711 async fn read_tag_values(&self, server: &str, tag_ids: Vec<String>)
720 -> OpcResult<Vec<TagValue>>;
721
722 async fn read_tag_values_for_display(
732 &self,
733 server: &str,
734 tag_ids: Vec<String>,
735 ) -> OpcResult<Vec<TagValue>> {
736 self.read_tag_values(server, tag_ids).await
737 }
738
739 async fn write_tag_value(
745 &self,
746 server: &str,
747 tag_id: &str,
748 value: OpcValue,
749 ) -> OpcResult<WriteResult>;
750}