#[cfg(feature = "opc-da-backend")]
use crate::opc_da::client::ClientTrait;
use anyhow::Context;
use windows::Win32::Foundation::{FILETIME, VARIANT_BOOL};
use windows::Win32::System::Com::{CLSIDFromProgID, CoTaskMemFree, ProgIDFromCLSID};
use windows::Win32::System::Ole::{
SafeArrayAccessData, SafeArrayGetDim, SafeArrayGetElemsize, SafeArrayGetLBound,
SafeArrayGetUBound, SafeArrayUnaccessData,
};
use windows::Win32::System::Variant::{VARIANT, VT_BOOL, VT_BSTR, VT_I4, VT_R8};
use windows::core::{BSTR, PCWSTR};
use crate::provider::OpcValue;
pub fn friendly_com_hint(error: &anyhow::Error) -> Option<&'static str> {
let msg = format!("{error:?}");
if msg.contains("0x80040112") {
Some("Server license does not permit OPC client connections")
} else if msg.contains("0x80080005") {
Some("Server process failed to start — check if it is installed and running")
} else if msg.contains("0x80070005") {
Some("Access denied — DCOM launch/activation permissions not configured for this user")
} else if msg.contains("0x800706BA") {
Some("RPC server unavailable — the target host may be offline or blocking RPC")
} else if msg.contains("0x800706F4") {
Some("COM marshalling error — try restarting the OPC server")
} else if msg.contains("0x80040154") {
Some("Server is not registered on this machine")
} else if msg.contains("0x80004003") {
Some("Invalid pointer (E_POINTER)")
} else if msg.contains("0xC0040004") {
Some("Server rejected write — the item may be read-only (OPC_E_BADRIGHTS)")
} else if msg.contains("0xC0040006") {
Some("Data type mismatch — server cannot convert the written value (OPC_E_BADTYPE)")
} else if msg.contains("0xC0040007") {
Some("Item ID not found in server address space (OPC_E_UNKNOWNITEMID)")
} else if msg.contains("0xC0040008") {
Some("Item ID syntax is invalid for this server (OPC_E_INVALIDITEMID)")
} else {
None
}
}
#[allow(clippy::cast_sign_loss)]
pub fn format_hresult(hr: windows::core::HRESULT) -> String {
let hex = format!("0x{:08X}", hr.0 as u32);
match friendly_com_hint(&anyhow::anyhow!("{hex}")) {
Some(hint) => format!("{hex}: {hint}"),
None => hex,
}
}
const _: () = assert!(
std::mem::size_of::<windows::core::GUID>() == 16,
"windows::core::GUID must be 16 bytes for COM compatibility"
);
const _: () = assert!(
std::mem::align_of::<windows::core::GUID>() >= 4,
"windows::core::GUID must be at least 4-byte aligned"
);
pub fn guid_to_progid(guid: &windows::core::GUID) -> anyhow::Result<String> {
unsafe {
let progid = ProgIDFromCLSID(guid).context("Failed to get ProgID from CLSID")?;
let result = if progid.is_null() {
String::new()
} else {
progid
.to_string()
.map_err(|e| anyhow::anyhow!("Failed into convert PWSTR: {e}"))?
};
if !progid.is_null() {
CoTaskMemFree(Some(progid.as_ptr() as *const _));
}
Ok(result)
}
}
#[allow(clippy::too_many_lines)]
pub fn variant_to_string(variant: &VARIANT) -> String {
unsafe {
let vt = variant.Anonymous.Anonymous.vt;
let base_type = vt.0 & 0x0FFF; let is_array = (vt.0 & 0x2000) != 0;
if is_array {
let parray = variant.Anonymous.Anonymous.Anonymous.parray;
if parray.is_null() {
return "Array[?]".to_string();
}
let dims = SafeArrayGetDim(parray);
if dims == 0 {
return "Array[0]".to_string();
}
if dims == 1 {
let lb = SafeArrayGetLBound(parray, 1).unwrap_or(0);
let ub = SafeArrayGetUBound(parray, 1).unwrap_or(-1);
let count = (ub - lb + 1).max(0);
let mut elements = Vec::new();
let display_count = count.min(20);
if base_type == windows::Win32::System::Variant::VT_VARIANT.0 {
let mut data_ptr: *mut std::ffi::c_void = std::ptr::null_mut();
if SafeArrayAccessData(parray, &raw mut data_ptr).is_ok() {
#[allow(clippy::cast_sign_loss)]
let vars =
std::slice::from_raw_parts(data_ptr as *const VARIANT, count as usize);
for i in 0..display_count {
#[allow(clippy::cast_sign_loss)]
elements.push(variant_to_string(&vars[i as usize]));
}
let _ = SafeArrayUnaccessData(parray);
}
} else {
let elem_size = SafeArrayGetElemsize(parray) as usize;
let mut data_ptr: *mut std::ffi::c_void = std::ptr::null_mut();
if SafeArrayAccessData(parray, &raw mut data_ptr).is_ok() {
for i in 0..display_count {
let mut temp_var = VARIANT::default();
(*temp_var.Anonymous.Anonymous).vt =
windows::Win32::System::Variant::VARENUM(base_type);
#[allow(clippy::cast_sign_loss)]
let src_ptr = (data_ptr as *const u8).add((i as usize) * elem_size);
let dst_ptr =
std::ptr::addr_of_mut!((*temp_var.Anonymous.Anonymous).Anonymous)
.cast::<u8>();
std::ptr::copy_nonoverlapping(src_ptr, dst_ptr, elem_size.min(16));
elements.push(variant_to_string(&temp_var));
}
let _ = SafeArrayUnaccessData(parray);
}
}
let elided = if count > 20 { ", ..." } else { "" };
return format!("[{}{elided}]", elements.join(", "));
}
return format!("Array[{dims}D]");
}
match vt.0 {
0 => "Empty".to_string(), 1 => "Null".to_string(), 2 => format!("{val}", val = variant.Anonymous.Anonymous.Anonymous.iVal), 3 => format!("{val}", val = variant.Anonymous.Anonymous.Anonymous.lVal), 4 => format!(
"{val:.2}",
val = variant.Anonymous.Anonymous.Anonymous.fltVal
), 5 => format!(
"{val:.2}",
val = variant.Anonymous.Anonymous.Anonymous.dblVal
), 6 => {
let raw = variant.Anonymous.Anonymous.Anonymous.cyVal.int64;
let whole = raw / 10_000;
let frac = (raw % 10_000).unsigned_abs();
format!("{whole}.{frac:04}")
}
7 => {
let ole_date = variant.Anonymous.Anonymous.Anonymous.date;
ole_date_to_string(ole_date)
}
8 => {
let bstr = &variant.Anonymous.Anonymous.Anonymous.bstrVal;
if bstr.is_empty() {
"\"\"".to_string()
} else {
format!("\"{}\"", &**bstr)
}
}
10 => {
let scode = variant.Anonymous.Anonymous.Anonymous.scode;
let hr = windows::core::HRESULT(scode);
#[allow(clippy::cast_sign_loss)]
let hr_str = format!("0x{:08X}", hr.0 as u32);
match friendly_com_hint(&anyhow::anyhow!("{hr_str}")) {
Some(msg) => format!("Error: {msg} ({hr_str})"),
None => format!("Error ({hr_str})"),
}
}
11 => format!(
"{val}",
val = variant.Anonymous.Anonymous.Anonymous.boolVal.0 != 0
), 16 => {
#[allow(clippy::cast_possible_wrap)]
let val = variant.Anonymous.Anonymous.Anonymous.bVal as i8;
format!("{val}")
} 17 => format!("{val}", val = variant.Anonymous.Anonymous.Anonymous.bVal), 18 => format!("{val}", val = variant.Anonymous.Anonymous.Anonymous.uiVal), 19 => format!("{val}", val = variant.Anonymous.Anonymous.Anonymous.ulVal), 20 => {
let p = (&raw const variant.Anonymous.Anonymous.Anonymous).cast::<i64>();
let val = *p;
format!("{val}")
}
21 => {
let p = (&raw const variant.Anonymous.Anonymous.Anonymous).cast::<u64>();
let val = *p;
format!("{val}")
}
_ => format!("(VT {vt:?})"),
}
}
}
#[allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_possible_wrap
)]
fn ole_date_to_string(ole_date: f64) -> String {
const OLE_EPOCH_DAYS: i64 = 25569; let total_secs = (ole_date - OLE_EPOCH_DAYS as f64) * 86400.0;
chrono::DateTime::from_timestamp(total_secs as i64, 0).map_or_else(
|| format!("{ole_date:.6}"),
|utc| {
utc.with_timezone(&chrono::Local)
.format("%Y-%m-%d %H:%M:%S")
.to_string()
},
)
}
pub fn quality_to_string(quality: u16) -> String {
let quality_bits = quality & 0xC0; match quality_bits {
0xC0 => "Good".to_string(),
0x00 => "Bad".to_string(),
0x40 => "Uncertain".to_string(),
_ => format!("Unknown(0x{quality:04X})"),
}
}
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
pub fn filetime_to_string(ft: FILETIME) -> String {
if ft.dwHighDateTime == 0 && ft.dwLowDateTime == 0 {
return "N/A".to_string();
}
let intervals = (u64::from(ft.dwHighDateTime) << 32) | u64::from(ft.dwLowDateTime);
let unix_secs = (intervals / 10_000_000).saturating_sub(11_644_473_600);
let nanos = ((intervals % 10_000_000) * 100) as u32;
chrono::DateTime::from_timestamp(unix_secs as i64, nanos).map_or_else(
|| "Invalid".to_string(),
|utc| {
utc.with_timezone(&chrono::Local)
.format("%Y-%m-%d %H:%M:%S")
.to_string()
},
)
}
pub fn opc_value_to_variant(value: &OpcValue) -> VARIANT {
let mut variant = VARIANT::default();
unsafe {
match value {
OpcValue::String(s) => {
(*variant.Anonymous.Anonymous).vt = VT_BSTR;
(*variant.Anonymous.Anonymous).Anonymous.bstrVal =
std::mem::ManuallyDrop::new(BSTR::from(s));
}
OpcValue::Int(i) => {
(*variant.Anonymous.Anonymous).vt = VT_I4;
(*variant.Anonymous.Anonymous).Anonymous.lVal = *i;
}
OpcValue::Float(f) => {
(*variant.Anonymous.Anonymous).vt = VT_R8;
(*variant.Anonymous.Anonymous).Anonymous.dblVal = *f;
}
OpcValue::Bool(b) => {
(*variant.Anonymous.Anonymous).vt = VT_BOOL;
(*variant.Anonymous.Anonymous).Anonymous.boolVal =
VARIANT_BOOL(if *b { -1 } else { 0 });
}
}
}
variant
}
pub fn connect_server(server_name: &str) -> anyhow::Result<crate::opc_da::client::v2::Server> {
let clsid_raw = unsafe {
let server_wide: Vec<u16> = server_name
.encode_utf16()
.chain(std::iter::once(0))
.collect();
CLSIDFromProgID(PCWSTR(server_wide.as_ptr()))
.with_context(|| format!("Failed to resolve ProgID '{server_name}' to CLSID"))?
};
let clsid = unsafe { std::mem::transmute_copy(&clsid_raw) };
let client = crate::opc_da::client::v2::Client;
let server = client
.create_server(clsid, crate::opc_da::def::ClassContext::All)
.map_err(|e| {
let hint = friendly_com_hint(&anyhow::anyhow!("{e:?}"))
.unwrap_or("Check DCOM configuration and server status");
tracing::error!(error = ?e, server = %server_name, hint, "create_server failed");
anyhow::anyhow!(e)
})?;
tracing::debug!(server = %server_name, "Connected to OPC DA server");
Ok(server)
}
#[cfg(test)]
mod tests {
#![allow(
clippy::single_char_pattern,
clippy::cast_possible_wrap,
clippy::ptr_as_ptr,
clippy::borrow_as_ptr,
clippy::mixed_attributes_style
)]
use super::*;
#[test]
fn test_friendly_com_hint_known_codes() {
let err = anyhow::anyhow!("COM error 0x800706F4");
assert_eq!(
friendly_com_hint(&err),
Some("COM marshalling error — try restarting the OPC server")
);
let err = anyhow::anyhow!("COM error 0x80040154");
assert_eq!(
friendly_com_hint(&err),
Some("Server is not registered on this machine")
);
let err = anyhow::anyhow!("COM error 0xC0040004");
assert_eq!(
friendly_com_hint(&err),
Some("Server rejected write — the item may be read-only (OPC_E_BADRIGHTS)"),
);
let err = anyhow::anyhow!("HRESULT(0xC0040006)");
assert_eq!(
friendly_com_hint(&err),
Some("Data type mismatch — server cannot convert the written value (OPC_E_BADTYPE)"),
);
let err = anyhow::anyhow!("HRESULT(0xC0040007)");
assert_eq!(
friendly_com_hint(&err),
Some("Item ID not found in server address space (OPC_E_UNKNOWNITEMID)"),
);
let err = anyhow::anyhow!("HRESULT(0xC0040008)");
assert_eq!(
friendly_com_hint(&err),
Some("Item ID syntax is invalid for this server (OPC_E_INVALIDITEMID)"),
);
}
#[test]
fn test_friendly_com_hint_unknown_code() {
let err = anyhow::anyhow!("Some other error");
assert_eq!(friendly_com_hint(&err), None);
}
#[test]
fn test_filetime_to_string_zero() {
let ft = FILETIME {
dwHighDateTime: 0,
dwLowDateTime: 0,
};
assert_eq!(filetime_to_string(ft), "N/A");
}
#[test]
fn test_filetime_to_string_nonzero() {
let ft = FILETIME {
dwHighDateTime: 0x01DC_9EF1,
dwLowDateTime: 0x0A3B_DF80,
};
let result = filetime_to_string(ft);
assert!(result.contains("-"));
}
#[test]
fn test_opc_value_to_variant_int() {
let v = opc_value_to_variant(&OpcValue::Int(42));
unsafe {
assert_eq!(v.Anonymous.Anonymous.vt, VT_I4);
assert_eq!(v.Anonymous.Anonymous.Anonymous.lVal, 42);
}
}
#[test]
fn test_opc_value_to_variant_float() {
let v = opc_value_to_variant(&OpcValue::Float(3.5));
unsafe {
assert_eq!(v.Anonymous.Anonymous.vt, VT_R8);
assert!((v.Anonymous.Anonymous.Anonymous.dblVal - 3.5).abs() < f64::EPSILON);
}
}
#[test]
fn test_opc_value_to_variant_bool_true() {
let v = opc_value_to_variant(&OpcValue::Bool(true));
unsafe {
assert_eq!(v.Anonymous.Anonymous.vt, VT_BOOL);
assert_eq!(v.Anonymous.Anonymous.Anonymous.boolVal.0, -1);
}
}
#[test]
fn test_opc_value_to_variant_bool_false() {
let v = opc_value_to_variant(&OpcValue::Bool(false));
unsafe {
assert_eq!(v.Anonymous.Anonymous.vt, VT_BOOL);
assert_eq!(v.Anonymous.Anonymous.Anonymous.boolVal.0, 0);
}
}
#[test]
fn test_opc_value_to_variant_string() {
let v = opc_value_to_variant(&OpcValue::String("hello".into()));
unsafe {
assert_eq!(v.Anonymous.Anonymous.vt, VT_BSTR);
let bstr = &v.Anonymous.Anonymous.Anonymous.bstrVal;
assert_eq!(&**bstr, "hello");
}
}
#[test]
fn test_variant_roundtrip() {
let v = opc_value_to_variant(&OpcValue::Int(99));
assert_eq!(variant_to_string(&v), "99");
let v = opc_value_to_variant(&OpcValue::Float(3.5));
assert_eq!(variant_to_string(&v), "3.50");
let v = opc_value_to_variant(&OpcValue::Bool(true));
assert_eq!(variant_to_string(&v), "true");
let v = opc_value_to_variant(&OpcValue::Bool(false));
assert_eq!(variant_to_string(&v), "false");
let v = opc_value_to_variant(&OpcValue::String("world".into()));
assert_eq!(variant_to_string(&v), "\"world\"");
}
#[test]
fn test_variant_to_string_cy() {
use std::mem::ManuallyDrop;
use windows::Win32::System::Com::CY;
use windows::Win32::System::Variant::{
VARIANT, VARIANT_0, VARIANT_0_0, VARIANT_0_0_0, VT_CY,
};
{
let cy_val = CY { int64: 123_456_789 };
let inner_union = VARIANT_0_0_0 { cyVal: cy_val };
let middle_struct = VARIANT_0_0 {
vt: VT_CY,
wReserved1: 0,
wReserved2: 0,
wReserved3: 0,
Anonymous: inner_union,
};
let outer_union = VARIANT_0 {
Anonymous: ManuallyDrop::new(middle_struct),
};
let v = VARIANT {
Anonymous: outer_union,
};
assert_eq!(variant_to_string(&v), "12345.6789");
}
{
let cy_val = CY { int64: -500_001 };
let inner_union = VARIANT_0_0_0 { cyVal: cy_val };
let middle_struct = VARIANT_0_0 {
vt: VT_CY,
wReserved1: 0,
wReserved2: 0,
wReserved3: 0,
Anonymous: inner_union,
};
let outer_union = VARIANT_0 {
Anonymous: ManuallyDrop::new(middle_struct),
};
let v = VARIANT {
Anonymous: outer_union,
};
assert_eq!(variant_to_string(&v), "-50.0001");
}
}
#[test]
fn test_variant_to_string_empty() {
let v = VARIANT::default();
assert_eq!(variant_to_string(&v), "Empty");
}
#[test]
fn quality_good() {
assert_eq!(quality_to_string(0xC0), "Good");
assert_eq!(quality_to_string(0xC4), "Good"); }
#[test]
fn quality_bad() {
assert_eq!(quality_to_string(0x00), "Bad");
assert_eq!(quality_to_string(0x04), "Bad"); }
#[test]
fn quality_uncertain() {
assert_eq!(quality_to_string(0x40), "Uncertain");
}
#[test]
fn quality_unknown() {
let result = quality_to_string(0x80);
assert!(result.starts_with("Unknown("));
}
#[test]
fn test_variant_to_string_null() {
use std::mem::ManuallyDrop;
use windows::Win32::System::Variant::{
VARIANT, VARIANT_0, VARIANT_0_0, VARIANT_0_0_0, VT_NULL,
};
let inner = VARIANT_0_0_0 { llVal: 0 };
let middle = VARIANT_0_0 {
vt: VT_NULL,
wReserved1: 0,
wReserved2: 0,
wReserved3: 0,
Anonymous: inner,
};
let outer = VARIANT_0 {
Anonymous: ManuallyDrop::new(middle),
};
let v = VARIANT { Anonymous: outer };
assert_eq!(variant_to_string(&v), "Null");
}
#[test]
fn test_variant_to_string_i2_and_r4() {
use std::mem::ManuallyDrop;
use windows::Win32::System::Variant::{
VARIANT, VARIANT_0, VARIANT_0_0, VARIANT_0_0_0, VT_I2, VT_R4,
};
let inner = VARIANT_0_0_0 { iVal: -42 };
let middle = VARIANT_0_0 {
vt: VT_I2,
wReserved1: 0,
wReserved2: 0,
wReserved3: 0,
Anonymous: inner,
};
let outer = VARIANT_0 {
Anonymous: ManuallyDrop::new(middle),
};
let v = VARIANT { Anonymous: outer };
assert_eq!(variant_to_string(&v), "-42");
let inner = VARIANT_0_0_0 { fltVal: 1.5 };
let middle = VARIANT_0_0 {
vt: VT_R4,
wReserved1: 0,
wReserved2: 0,
wReserved3: 0,
Anonymous: inner,
};
let outer = VARIANT_0 {
Anonymous: ManuallyDrop::new(middle),
};
let v = VARIANT { Anonymous: outer };
assert_eq!(variant_to_string(&v), "1.50");
}
#[test]
fn test_variant_to_string_unknown_vt() {
use std::mem::ManuallyDrop;
use windows::Win32::System::Variant::{
VARENUM, VARIANT, VARIANT_0, VARIANT_0_0, VARIANT_0_0_0,
};
let inner = VARIANT_0_0_0 { llVal: 0 };
let middle = VARIANT_0_0 {
vt: VARENUM(999),
wReserved1: 0,
wReserved2: 0,
wReserved3: 0,
Anonymous: inner,
};
let outer = VARIANT_0 {
Anonymous: ManuallyDrop::new(middle),
};
let v = VARIANT { Anonymous: outer };
let result = variant_to_string(&v);
assert!(
result.starts_with("(VT "),
"Expected '(VT ...)' but got: {}",
result
);
}
#[test]
fn test_variant_to_string_safearray_i4() {
use std::ffi::c_void;
use std::mem::ManuallyDrop;
use windows::Win32::System::Ole::{
SafeArrayAccessData, SafeArrayCreateVector, SafeArrayUnaccessData,
};
use windows::Win32::System::Variant::{VARIANT, VARIANT_0, VARIANT_0_0, VT_ARRAY, VT_I4};
unsafe {
let parray = SafeArrayCreateVector(VT_I4, 0, 3);
let mut ptr: *mut c_void = std::ptr::null_mut();
SafeArrayAccessData(parray, &mut ptr).unwrap();
let slice = std::slice::from_raw_parts_mut(ptr as *mut i32, 3);
slice[0] = 10;
slice[1] = 20;
slice[2] = 30;
SafeArrayUnaccessData(parray).unwrap();
let mut middle = VARIANT_0_0 {
vt: windows::Win32::System::Variant::VARENUM(VT_I4.0 | VT_ARRAY.0),
..Default::default()
};
middle.Anonymous.parray = parray;
let v = VARIANT {
Anonymous: VARIANT_0 {
Anonymous: ManuallyDrop::new(middle),
},
};
assert_eq!(variant_to_string(&v), "[10, 20, 30]");
}
}
#[test]
fn test_variant_to_string_vt_error_known() {
use std::mem::ManuallyDrop;
use windows::Win32::System::Variant::{
VARENUM, VARIANT, VARIANT_0, VARIANT_0_0, VARIANT_0_0_0,
};
let inner = VARIANT_0_0_0 {
scode: -1_073_479_673,
}; let middle = VARIANT_0_0 {
vt: VARENUM(10), wReserved1: 0,
wReserved2: 0,
wReserved3: 0,
Anonymous: inner,
};
let outer = VARIANT_0 {
Anonymous: ManuallyDrop::new(middle),
};
let v = VARIANT { Anonymous: outer };
assert_eq!(
super::variant_to_string(&v),
"Error: Item ID not found in server address space (OPC_E_UNKNOWNITEMID) (0xC0040007)"
);
}
#[test]
fn test_variant_to_string_vt_error_unknown() {
use std::mem::ManuallyDrop;
use windows::Win32::System::Variant::{
VARENUM, VARIANT, VARIANT_0, VARIANT_0_0, VARIANT_0_0_0,
};
let inner = VARIANT_0_0_0 {
scode: -559_038_737,
}; let middle = VARIANT_0_0 {
vt: VARENUM(10), wReserved1: 0,
wReserved2: 0,
wReserved3: 0,
Anonymous: inner,
};
let outer = VARIANT_0 {
Anonymous: ManuallyDrop::new(middle),
};
let v = VARIANT { Anonymous: outer };
assert_eq!(super::variant_to_string(&v), "Error (0xDEADBEEF)");
}
#[test]
fn test_format_hresult_known() {
let hr = windows::core::HRESULT(0x8004_0154_u32 as i32);
assert_eq!(
super::format_hresult(hr),
"0x80040154: Server is not registered on this machine"
);
}
#[test]
fn test_format_hresult_unknown() {
let hr = windows::core::HRESULT(0x1234_5678_u32 as i32);
assert_eq!(super::format_hresult(hr), "0x12345678");
}
}