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)]
30pub struct TagValue {
31 pub tag_id: String,
33 pub value: String,
39 pub quality: String,
41 pub timestamp: String,
43}
44
45#[derive(Debug, Clone, PartialEq)]
56pub enum OpcValue {
57 String(String),
59 Int(i32),
61 Float(f64),
63 Bool(bool),
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct WriteResult {
83 pub tag_id: String,
85 pub success: bool,
87 pub error: Option<String>,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum BrowseNamespace {
94 Flat,
96 Hierarchical,
98 Unknown,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub struct BrowseCapabilities {
106 pub namespace: BrowseNamespace,
108 pub supports_da3: bool,
110 pub supports_da2: bool,
112 pub max_page_size: u32,
114}
115
116macro_rules! opaque_browse_token {
117 ($name:ident, $doc:literal) => {
118 #[doc = $doc]
119 #[derive(Clone, Copy, PartialEq, Eq, Hash)]
120 pub struct $name(Uuid);
121
122 impl $name {
123 pub(crate) fn new() -> Self {
124 Self(Uuid::new_v4())
125 }
126
127 pub fn parse(value: &str) -> Result<Self, uuid::Error> {
132 value.parse()
133 }
134 }
135
136 impl std::fmt::Debug for $name {
137 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138 formatter
139 .debug_tuple(stringify!($name))
140 .field(&self.0)
141 .finish()
142 }
143 }
144
145 impl std::fmt::Display for $name {
146 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147 self.0.fmt(formatter)
148 }
149 }
150
151 impl std::str::FromStr for $name {
152 type Err = uuid::Error;
153
154 fn from_str(value: &str) -> Result<Self, Self::Err> {
155 value.parse().map(Self)
156 }
157 }
158 };
159}
160
161opaque_browse_token!(
162 BrowseSessionToken,
163 "Opaque identifier for a browse session owned by the COM worker."
164);
165opaque_browse_token!(
166 BrowseNodeToken,
167 "Opaque identifier for a node returned by a browse session."
168);
169opaque_browse_token!(
170 BrowsePageToken,
171 "Opaque continuation token for the next bounded browse page."
172);
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub enum BrowseNodeKind {
177 Branch,
179 Item,
181 BranchAndItem,
183}
184
185impl BrowseNodeKind {
186 pub fn has_children(self) -> bool {
188 matches!(self, Self::Branch | Self::BranchAndItem)
189 }
190
191 pub fn is_item(self) -> bool {
193 matches!(self, Self::Item | Self::BranchAndItem)
194 }
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq)]
199pub enum BrowseNodeFilter {
200 Branches,
202 Items,
204 All,
206}
207
208#[derive(Debug, Clone, PartialEq, Eq)]
210pub struct BrowseNode {
211 pub token: BrowseNodeToken,
213 pub name: String,
215 pub item_id: Option<String>,
217 pub kind: BrowseNodeKind,
219}
220
221#[derive(Debug, Clone, Copy, PartialEq, Eq)]
223pub struct BrowsePageRequest {
224 pub parent: Option<BrowseNodeToken>,
226 pub filter: BrowseNodeFilter,
228 pub max_elements: u32,
230 pub continuation: Option<BrowsePageToken>,
232}
233
234#[derive(Debug, Clone, PartialEq, Eq)]
236pub struct BrowsePage {
237 pub nodes: Vec<BrowseNode>,
239 pub continuation: Option<BrowsePageToken>,
241}
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq)]
245pub struct InventoryOptions {
246 pub batch_size: u32,
248 pub max_entries: Option<u64>,
250}
251
252impl Default for InventoryOptions {
253 fn default() -> Self {
254 Self {
255 batch_size: 100,
256 max_entries: None,
257 }
258 }
259}
260
261#[derive(Debug, Clone, PartialEq, Eq)]
263pub struct InventoryEntry {
264 pub display_name: String,
266 pub item_id: String,
268 pub kind: BrowseNodeKind,
270 pub breadcrumbs: Vec<String>,
272}
273
274#[derive(Debug, Clone, PartialEq)]
276pub struct InventoryProgress {
277 pub branches_visited: u64,
278 pub entries_seen: u64,
279 pub unique_items: u64,
280 pub active_time_ms: u64,
281 pub paused_time_ms: u64,
282 pub items_per_second: f64,
283 pub estimated_remaining_ms: Option<u64>,
284}
285
286#[derive(Debug, Clone, PartialEq, Eq)]
288pub struct InventoryCompleted {
289 pub complete: bool,
290 pub cancelled: bool,
291 pub truncated: bool,
292 pub warning: Option<String>,
293 pub capabilities: BrowseCapabilities,
294}
295
296#[derive(Debug, Clone, PartialEq)]
298pub enum InventoryEvent {
299 Entry(InventoryEntry),
300 Progress(InventoryProgress),
301 Completed(InventoryCompleted),
302}
303
304#[derive(Debug)]
305struct InventoryControlState {
306 cancelled: AtomicBool,
307 paused: AtomicBool,
308}
309
310#[derive(Clone, Debug)]
312pub struct InventoryControl {
313 state: Arc<InventoryControlState>,
314}
315
316impl InventoryControl {
317 pub(crate) fn new() -> Self {
318 Self {
319 state: Arc::new(InventoryControlState {
320 cancelled: AtomicBool::new(false),
321 paused: AtomicBool::new(false),
322 }),
323 }
324 }
325
326 pub fn cancel(&self) {
328 self.state.cancelled.store(true, Ordering::Release);
329 }
330
331 pub fn pause(&self) {
333 self.state.paused.store(true, Ordering::Release);
334 }
335
336 pub fn resume(&self) {
338 self.state.paused.store(false, Ordering::Release);
339 }
340
341 pub fn is_cancelled(&self) -> bool {
343 self.state.cancelled.load(Ordering::Acquire)
344 }
345
346 pub(crate) fn is_paused(&self) -> bool {
347 self.state.paused.load(Ordering::Acquire)
348 }
349}
350
351pub struct InventoryStream {
353 receiver: mpsc::Receiver<OpcResult<InventoryEvent>>,
354 control: InventoryControl,
355 worker: Option<std::thread::JoinHandle<()>>,
356}
357
358impl InventoryStream {
359 pub(crate) fn new(
360 receiver: mpsc::Receiver<OpcResult<InventoryEvent>>,
361 control: InventoryControl,
362 worker: std::thread::JoinHandle<()>,
363 ) -> Self {
364 Self {
365 receiver,
366 control,
367 worker: Some(worker),
368 }
369 }
370
371 pub async fn message(&mut self) -> Option<OpcResult<InventoryEvent>> {
377 self.receiver.recv().await
378 }
379
380 pub fn control(&self) -> InventoryControl {
382 self.control.clone()
383 }
384
385 pub fn cancel(&self) {
387 self.control.cancel();
388 }
389
390 pub fn pause(&self) {
392 self.control.pause();
393 }
394
395 pub fn resume(&self) {
397 self.control.resume();
398 }
399}
400
401impl Drop for InventoryStream {
402 fn drop(&mut self) {
403 self.receiver.close();
406 self.control.cancel();
407 if let Some(worker) = self.worker.take() {
408 let _ = worker.join();
409 }
410 }
411}
412
413#[cfg(test)]
414mod inventory_stream_tests {
415 use super::*;
416
417 #[test]
418 fn dropping_inventory_stream_cancels_and_joins_worker() {
419 let control = InventoryControl::new();
420 let worker_control = control.clone();
421 let finished = Arc::new(AtomicBool::new(false));
422 let worker_finished = Arc::clone(&finished);
423 let (_sender, receiver) = mpsc::channel(1);
424 let worker = std::thread::spawn(move || {
425 while !worker_control.is_cancelled() {
426 std::thread::yield_now();
427 }
428 worker_finished.store(true, Ordering::Release);
429 });
430
431 drop(InventoryStream::new(receiver, control, worker));
432 assert!(finished.load(Ordering::Acquire));
433 }
434}
435
436#[cfg(test)]
437mod read_display_fallback_tests {
438 use super::*;
439
440 struct FallbackProvider;
441
442 #[async_trait]
443 impl OpcProvider for FallbackProvider {
444 async fn list_servers(&self, _host: &str) -> OpcResult<Vec<String>> {
445 Ok(Vec::new())
446 }
447
448 async fn browse_tags(
449 &self,
450 _server: &str,
451 _max_tags: usize,
452 _progress: Arc<AtomicUsize>,
453 _tags_sink: Arc<std::sync::Mutex<Vec<String>>>,
454 ) -> OpcResult<Vec<String>> {
455 Ok(Vec::new())
456 }
457
458 async fn read_tag_values(
459 &self,
460 _server: &str,
461 tag_ids: Vec<String>,
462 ) -> OpcResult<Vec<TagValue>> {
463 Ok(tag_ids
464 .into_iter()
465 .map(|tag_id| TagValue {
466 tag_id,
467 value: "AUT".to_string(),
468 quality: "Good".to_string(),
469 timestamp: String::new(),
470 })
471 .collect())
472 }
473
474 async fn write_tag_value(
475 &self,
476 _server: &str,
477 tag_id: &str,
478 _value: OpcValue,
479 ) -> OpcResult<WriteResult> {
480 Ok(WriteResult {
481 tag_id: tag_id.to_string(),
482 success: true,
483 error: None,
484 })
485 }
486 }
487
488 #[tokio::test]
489 async fn display_read_defaults_to_semantic_read() {
490 let values = FallbackProvider
491 .read_tag_values_for_display("Server", vec!["Tag".to_string()])
492 .await
493 .unwrap();
494
495 assert_eq!(values[0].value, "AUT");
496 }
497}
498
499#[cfg_attr(feature = "test-support", automock)]
504#[async_trait]
505pub trait OpcProvider: Send + Sync {
506 async fn list_servers(&self, host: &str) -> OpcResult<Vec<String>>;
512
513 async fn browse_tags(
519 &self,
520 server: &str,
521 max_tags: usize,
522 progress: Arc<AtomicUsize>,
523 tags_sink: Arc<std::sync::Mutex<Vec<String>>>,
524 ) -> OpcResult<Vec<String>>;
525
526 async fn browse_capabilities(&self, server: &str) -> OpcResult<BrowseCapabilities> {
532 let _ = server;
533 Err(OpcError::NotImplemented(
534 "Native browsing is not implemented by this provider".to_string(),
535 ))
536 }
537
538 async fn open_browse_session(&self, server: &str) -> OpcResult<BrowseSessionToken> {
544 let _ = server;
545 Err(OpcError::NotImplemented(
546 "Native browsing is not implemented by this provider".to_string(),
547 ))
548 }
549
550 async fn browse_page(
556 &self,
557 session: &BrowseSessionToken,
558 request: BrowsePageRequest,
559 ) -> OpcResult<BrowsePage> {
560 let _ = (session, request);
561 Err(OpcError::NotImplemented(
562 "Native browsing is not implemented by this provider".to_string(),
563 ))
564 }
565
566 async fn close_browse_session(&self, session: &BrowseSessionToken) -> OpcResult<()> {
571 let _ = session;
572 Err(OpcError::NotImplemented(
573 "Native browsing is not implemented by this provider".to_string(),
574 ))
575 }
576
577 async fn start_inventory(
583 &self,
584 server: &str,
585 options: InventoryOptions,
586 ) -> OpcResult<InventoryStream> {
587 let _ = (server, options);
588 Err(OpcError::NotImplemented(
589 "Namespace inventory is not implemented by this provider".to_string(),
590 ))
591 }
592
593 async fn read_tag_values(&self, server: &str, tag_ids: Vec<String>)
602 -> OpcResult<Vec<TagValue>>;
603
604 async fn read_tag_values_for_display(
614 &self,
615 server: &str,
616 tag_ids: Vec<String>,
617 ) -> OpcResult<Vec<TagValue>> {
618 self.read_tag_values(server, tag_ids).await
619 }
620
621 async fn write_tag_value(
627 &self,
628 server: &str,
629 tag_id: &str,
630 value: OpcValue,
631 ) -> OpcResult<WriteResult>;
632}