use crate::opc_da::errors::{OpcError, OpcResult};
use async_trait::async_trait;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use tokio::sync::mpsc;
use uuid::Uuid;
#[cfg(feature = "test-support")]
use mockall::automock;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TagValue {
pub tag_id: String,
pub value: String,
pub quality: String,
pub timestamp: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum OpcValue {
String(String),
Int(i32),
Float(f64),
Bool(bool),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WriteResult {
pub tag_id: String,
pub success: bool,
pub error: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BrowseNamespace {
Flat,
Hierarchical,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BrowseCapabilities {
pub namespace: BrowseNamespace,
pub supports_da3: bool,
pub supports_da2: bool,
pub max_page_size: u32,
}
macro_rules! opaque_browse_token {
($name:ident, $doc:literal) => {
#[doc = $doc]
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct $name(Uuid);
impl $name {
pub(crate) fn new() -> Self {
Self(Uuid::new_v4())
}
pub fn parse(value: &str) -> Result<Self, uuid::Error> {
value.parse()
}
}
impl std::fmt::Debug for $name {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_tuple(stringify!($name))
.field(&self.0)
.finish()
}
}
impl std::fmt::Display for $name {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(formatter)
}
}
impl std::str::FromStr for $name {
type Err = uuid::Error;
fn from_str(value: &str) -> Result<Self, Self::Err> {
value.parse().map(Self)
}
}
};
}
opaque_browse_token!(
BrowseSessionToken,
"Opaque identifier for a browse session owned by the COM worker."
);
opaque_browse_token!(
BrowseNodeToken,
"Opaque identifier for a node returned by a browse session."
);
opaque_browse_token!(
BrowsePageToken,
"Opaque continuation token for the next bounded browse page."
);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BrowseNodeKind {
Branch,
Item,
BranchAndItem,
}
impl BrowseNodeKind {
pub fn has_children(self) -> bool {
matches!(self, Self::Branch | Self::BranchAndItem)
}
pub fn is_item(self) -> bool {
matches!(self, Self::Item | Self::BranchAndItem)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BrowseNodeFilter {
Branches,
Items,
All,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BrowseNode {
pub token: BrowseNodeToken,
pub name: String,
pub item_id: Option<String>,
pub kind: BrowseNodeKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BrowsePageRequest {
pub parent: Option<BrowseNodeToken>,
pub filter: BrowseNodeFilter,
pub max_elements: u32,
pub continuation: Option<BrowsePageToken>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BrowsePage {
pub nodes: Vec<BrowseNode>,
pub continuation: Option<BrowsePageToken>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InventoryOptions {
pub batch_size: u32,
pub max_entries: Option<u64>,
}
impl Default for InventoryOptions {
fn default() -> Self {
Self {
batch_size: 100,
max_entries: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InventoryEntry {
pub display_name: String,
pub item_id: String,
pub kind: BrowseNodeKind,
pub breadcrumbs: Vec<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct InventoryProgress {
pub branches_visited: u64,
pub entries_seen: u64,
pub unique_items: u64,
pub active_time_ms: u64,
pub paused_time_ms: u64,
pub items_per_second: f64,
pub estimated_remaining_ms: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InventoryCompleted {
pub complete: bool,
pub cancelled: bool,
pub truncated: bool,
pub warning: Option<String>,
pub capabilities: BrowseCapabilities,
}
#[derive(Debug, Clone, PartialEq)]
pub enum InventoryEvent {
Entry(InventoryEntry),
Progress(InventoryProgress),
Completed(InventoryCompleted),
}
#[derive(Debug)]
struct InventoryControlState {
cancelled: AtomicBool,
paused: AtomicBool,
}
#[derive(Clone, Debug)]
pub struct InventoryControl {
state: Arc<InventoryControlState>,
}
impl InventoryControl {
pub(crate) fn new() -> Self {
Self {
state: Arc::new(InventoryControlState {
cancelled: AtomicBool::new(false),
paused: AtomicBool::new(false),
}),
}
}
pub fn cancel(&self) {
self.state.cancelled.store(true, Ordering::Release);
}
pub fn pause(&self) {
self.state.paused.store(true, Ordering::Release);
}
pub fn resume(&self) {
self.state.paused.store(false, Ordering::Release);
}
pub fn is_cancelled(&self) -> bool {
self.state.cancelled.load(Ordering::Acquire)
}
pub(crate) fn is_paused(&self) -> bool {
self.state.paused.load(Ordering::Acquire)
}
}
pub struct InventoryStream {
receiver: mpsc::Receiver<OpcResult<InventoryEvent>>,
control: InventoryControl,
worker: Option<std::thread::JoinHandle<()>>,
}
impl InventoryStream {
pub(crate) fn new(
receiver: mpsc::Receiver<OpcResult<InventoryEvent>>,
control: InventoryControl,
worker: std::thread::JoinHandle<()>,
) -> Self {
Self {
receiver,
control,
worker: Some(worker),
}
}
pub async fn message(&mut self) -> Option<OpcResult<InventoryEvent>> {
self.receiver.recv().await
}
pub fn control(&self) -> InventoryControl {
self.control.clone()
}
pub fn cancel(&self) {
self.control.cancel();
}
pub fn pause(&self) {
self.control.pause();
}
pub fn resume(&self) {
self.control.resume();
}
}
impl Drop for InventoryStream {
fn drop(&mut self) {
self.receiver.close();
self.control.cancel();
if let Some(worker) = self.worker.take() {
let _ = worker.join();
}
}
}
#[cfg(test)]
mod inventory_stream_tests {
use super::*;
#[test]
fn dropping_inventory_stream_cancels_and_joins_worker() {
let control = InventoryControl::new();
let worker_control = control.clone();
let finished = Arc::new(AtomicBool::new(false));
let worker_finished = Arc::clone(&finished);
let (_sender, receiver) = mpsc::channel(1);
let worker = std::thread::spawn(move || {
while !worker_control.is_cancelled() {
std::thread::yield_now();
}
worker_finished.store(true, Ordering::Release);
});
drop(InventoryStream::new(receiver, control, worker));
assert!(finished.load(Ordering::Acquire));
}
}
#[cfg(test)]
mod read_display_fallback_tests {
use super::*;
struct FallbackProvider;
#[async_trait]
impl OpcProvider for FallbackProvider {
async fn list_servers(&self, _host: &str) -> OpcResult<Vec<String>> {
Ok(Vec::new())
}
async fn browse_tags(
&self,
_server: &str,
_max_tags: usize,
_progress: Arc<AtomicUsize>,
_tags_sink: Arc<std::sync::Mutex<Vec<String>>>,
) -> OpcResult<Vec<String>> {
Ok(Vec::new())
}
async fn read_tag_values(
&self,
_server: &str,
tag_ids: Vec<String>,
) -> OpcResult<Vec<TagValue>> {
Ok(tag_ids
.into_iter()
.map(|tag_id| TagValue {
tag_id,
value: "AUT".to_string(),
quality: "Good".to_string(),
timestamp: String::new(),
})
.collect())
}
async fn write_tag_value(
&self,
_server: &str,
tag_id: &str,
_value: OpcValue,
) -> OpcResult<WriteResult> {
Ok(WriteResult {
tag_id: tag_id.to_string(),
success: true,
error: None,
})
}
}
#[tokio::test]
async fn display_read_defaults_to_semantic_read() {
let values = FallbackProvider
.read_tag_values_for_display("Server", vec!["Tag".to_string()])
.await
.unwrap();
assert_eq!(values[0].value, "AUT");
}
}
#[cfg_attr(feature = "test-support", automock)]
#[async_trait]
pub trait OpcProvider: Send + Sync {
async fn list_servers(&self, host: &str) -> OpcResult<Vec<String>>;
async fn browse_tags(
&self,
server: &str,
max_tags: usize,
progress: Arc<AtomicUsize>,
tags_sink: Arc<std::sync::Mutex<Vec<String>>>,
) -> OpcResult<Vec<String>>;
async fn browse_capabilities(&self, server: &str) -> OpcResult<BrowseCapabilities> {
let _ = server;
Err(OpcError::NotImplemented(
"Native browsing is not implemented by this provider".to_string(),
))
}
async fn open_browse_session(&self, server: &str) -> OpcResult<BrowseSessionToken> {
let _ = server;
Err(OpcError::NotImplemented(
"Native browsing is not implemented by this provider".to_string(),
))
}
async fn browse_page(
&self,
session: &BrowseSessionToken,
request: BrowsePageRequest,
) -> OpcResult<BrowsePage> {
let _ = (session, request);
Err(OpcError::NotImplemented(
"Native browsing is not implemented by this provider".to_string(),
))
}
async fn close_browse_session(&self, session: &BrowseSessionToken) -> OpcResult<()> {
let _ = session;
Err(OpcError::NotImplemented(
"Native browsing is not implemented by this provider".to_string(),
))
}
async fn start_inventory(
&self,
server: &str,
options: InventoryOptions,
) -> OpcResult<InventoryStream> {
let _ = (server, options);
Err(OpcError::NotImplemented(
"Namespace inventory is not implemented by this provider".to_string(),
))
}
async fn read_tag_values(&self, server: &str, tag_ids: Vec<String>)
-> OpcResult<Vec<TagValue>>;
async fn read_tag_values_for_display(
&self,
server: &str,
tag_ids: Vec<String>,
) -> OpcResult<Vec<TagValue>> {
self.read_tag_values(server, tag_ids).await
}
async fn write_tag_value(
&self,
server: &str,
tag_id: &str,
value: OpcValue,
) -> OpcResult<WriteResult>;
}