use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpcUaConfig {
pub endpoint_url: String,
pub application_name: String,
pub application_uri: String,
pub security_policy: SecurityPolicy,
pub security_mode: MessageSecurityMode,
pub user_identity: UserIdentity,
pub session_timeout_ms: u64,
pub publishing_interval_ms: u32,
pub sampling_interval_ms: u32,
pub queue_size: u32,
pub client_certificate: Option<CertificateConfig>,
pub server_certificate: Option<String>,
pub accept_untrusted_certs: bool,
pub session_renewal: SessionRenewalConfig,
}
impl Default for OpcUaConfig {
fn default() -> Self {
Self {
endpoint_url: "opc.tcp://localhost:4840".to_string(),
application_name: format!("OxiRS-OPC-UA-{}", uuid::Uuid::new_v4()),
application_uri: "urn:OxiRS:OpcUaClient".to_string(),
security_policy: SecurityPolicy::None,
security_mode: MessageSecurityMode::None,
user_identity: UserIdentity::Anonymous,
session_timeout_ms: 60000,
publishing_interval_ms: 1000,
sampling_interval_ms: 100,
queue_size: 10,
client_certificate: None,
server_certificate: None,
accept_untrusted_certs: false,
session_renewal: SessionRenewalConfig::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum SecurityPolicy {
None,
Basic128Rsa15,
Basic256,
Basic256Sha256,
Aes128Sha256RsaOaep,
Aes256Sha256RsaPss,
}
impl SecurityPolicy {
pub fn to_uri(&self) -> String {
let base = "http://opcfoundation.org/UA/SecurityPolicy#";
match self {
SecurityPolicy::None => format!("{}None", base),
SecurityPolicy::Basic128Rsa15 => format!("{}Basic128Rsa15", base),
SecurityPolicy::Basic256 => format!("{}Basic256", base),
SecurityPolicy::Basic256Sha256 => format!("{}Basic256Sha256", base),
SecurityPolicy::Aes128Sha256RsaOaep => format!("{}Aes128_Sha256_RsaOaep", base),
SecurityPolicy::Aes256Sha256RsaPss => format!("{}Aes256_Sha256_RsaPss", base),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum MessageSecurityMode {
None,
Sign,
SignAndEncrypt,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum UserIdentity {
Anonymous,
UserPassword { username: String, password: String },
X509Certificate { cert_path: String, key_path: String },
IssuedToken {
token: Vec<u8>,
encryption_algorithm: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CertificateConfig {
pub cert_path: String,
pub key_path: String,
pub format: CertificateFormat,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum CertificateFormat {
Pem,
Der,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionRenewalConfig {
pub auto_renew: bool,
pub renew_at_ratio: f64,
pub retry_count: u32,
pub retry_delay_ms: u64,
}
impl Default for SessionRenewalConfig {
fn default() -> Self {
Self {
auto_renew: true,
renew_at_ratio: 0.75,
retry_count: 3,
retry_delay_ms: 5000,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeSubscription {
pub node_id: String,
pub browse_name: Option<String>,
pub display_name: Option<String>,
pub rdf_subject: String,
pub rdf_predicate: String,
pub rdf_graph: Option<String>,
pub unit_uri: Option<String>,
pub samm_property: Option<String>,
pub deadband: Option<Deadband>,
pub data_change_filter: Option<DataChangeFilter>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Deadband {
pub deadband_type: DeadbandType,
pub value: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum DeadbandType {
None,
Absolute,
Percent,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DataChangeFilter {
Status,
StatusValue,
StatusValueTimestamp,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpcUaDataChange {
pub node_id: String,
pub value: OpcUaValue,
pub status_code: u32,
pub source_timestamp: Option<chrono::DateTime<chrono::Utc>>,
pub server_timestamp: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum OpcUaValue {
Boolean {
value: bool,
},
SByte {
value: i8,
},
Byte {
value: u8,
},
Int16 {
value: i16,
},
UInt16 {
value: u16,
},
Int32 {
value: i32,
},
UInt32 {
value: u32,
},
Int64 {
value: i64,
},
UInt64 {
value: u64,
},
Float {
value: f32,
},
Double {
value: f64,
},
String {
value: String,
},
DateTime {
value: chrono::DateTime<chrono::Utc>,
},
Guid {
value: String,
},
ByteString {
value: Vec<u8>,
},
StatusCode {
value: u32,
},
QualifiedName {
namespace_index: u16,
name: String,
},
LocalizedText {
locale: Option<String>,
text: String,
},
}
impl OpcUaValue {
pub fn to_rdf_literal(&self) -> String {
match self {
OpcUaValue::Boolean { value } => {
format!("\"{}\"^^<http://www.w3.org/2001/XMLSchema#boolean>", value)
}
OpcUaValue::Int32 { value: _ } | OpcUaValue::Int16 { value: _ } => {
format!(
"\"{}\"^^<http://www.w3.org/2001/XMLSchema#integer>",
match self {
OpcUaValue::Int32 { value } => *value as i64,
OpcUaValue::Int16 { value } => *value as i64,
_ => 0,
}
)
}
OpcUaValue::Float { value } => {
format!("\"{}\"^^<http://www.w3.org/2001/XMLSchema#float>", value)
}
OpcUaValue::Double { value } => {
format!("\"{}\"^^<http://www.w3.org/2001/XMLSchema#double>", value)
}
OpcUaValue::String { value } => format!("\"{}\"", value),
OpcUaValue::DateTime { value } => {
format!(
"\"{}\"^^<http://www.w3.org/2001/XMLSchema#dateTime>",
value.to_rfc3339()
)
}
_ => format!("\"{}\"", serde_json::to_string(self).unwrap_or_default()),
}
}
pub fn xsd_datatype(&self) -> &'static str {
match self {
OpcUaValue::Boolean { .. } => "http://www.w3.org/2001/XMLSchema#boolean",
OpcUaValue::Int32 { .. } | OpcUaValue::Int16 { .. } | OpcUaValue::Int64 { .. } => {
"http://www.w3.org/2001/XMLSchema#integer"
}
OpcUaValue::UInt32 { .. } | OpcUaValue::UInt16 { .. } | OpcUaValue::UInt64 { .. } => {
"http://www.w3.org/2001/XMLSchema#nonNegativeInteger"
}
OpcUaValue::Float { .. } => "http://www.w3.org/2001/XMLSchema#float",
OpcUaValue::Double { .. } => "http://www.w3.org/2001/XMLSchema#double",
OpcUaValue::String { .. } => "http://www.w3.org/2001/XMLSchema#string",
OpcUaValue::DateTime { .. } => "http://www.w3.org/2001/XMLSchema#dateTime",
_ => "http://www.w3.org/2001/XMLSchema#string",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct OpcUaStats {
pub data_changes_received: u64,
pub events_received: u64,
pub session_count: u64,
pub session_renewals: u64,
pub subscription_count: u64,
pub monitored_items_count: u64,
pub last_connected_at: Option<chrono::DateTime<chrono::Utc>>,
pub last_disconnected_at: Option<chrono::DateTime<chrono::Utc>>,
pub publish_requests: u64,
pub read_operations: u64,
pub write_operations: u64,
pub browse_operations: u64,
pub error_count: u64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_security_policy_uri() {
assert_eq!(
SecurityPolicy::None.to_uri(),
"http://opcfoundation.org/UA/SecurityPolicy#None"
);
assert_eq!(
SecurityPolicy::Basic256Sha256.to_uri(),
"http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256"
);
}
#[test]
fn test_opcua_value_to_rdf() {
let value = OpcUaValue::Double { value: 25.5 };
let rdf = value.to_rdf_literal();
assert!(rdf.contains("25.5"));
assert!(rdf.contains("double"));
let value = OpcUaValue::Boolean { value: true };
let rdf = value.to_rdf_literal();
assert!(rdf.contains("true"));
assert!(rdf.contains("boolean"));
}
#[test]
fn test_default_config() {
let config = OpcUaConfig::default();
assert!(config.endpoint_url.starts_with("opc.tcp://"));
assert_eq!(config.security_policy, SecurityPolicy::None);
assert!(matches!(config.user_identity, UserIdentity::Anonymous));
}
}