1use std::collections::HashMap;
7
8use serde::{Deserialize, Serialize};
9
10use mabi_core::{
11 config::{DeviceConfig, DataPointConfig},
12 device::BoxedDevice,
13 error::{Error, Result},
14 factory::{DeviceFactory, FactoryMetadata, FactoryRegistry, Plugin},
15 protocol::Protocol,
16 types::DataPointDef,
17 value::Value,
18};
19
20use crate::config::OpcUaServerConfig;
21use crate::device::OpcUaDevice;
22use crate::nodes::{AddressSpaceConfig, NodeCacheConfig};
23use crate::services::HistoryStoreConfig;
24use crate::types::{NodeId, Variant};
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct OpcUaDeviceMetadata {
29 #[serde(default)]
31 pub server_config: OpcUaServerConfig,
32 #[serde(default)]
34 pub address_space_config: Option<AddressSpaceConfig>,
35 #[serde(default)]
37 pub cache_config: Option<NodeCacheConfig>,
38 #[serde(default)]
40 pub history_config: Option<HistoryStoreConfig>,
41 #[serde(default = "default_namespace")]
43 pub namespace_uri: String,
44 #[serde(default = "default_namespace_index")]
46 pub namespace_index: u16,
47}
48
49fn default_namespace() -> String {
50 "urn:trap-simulator:opcua".to_string()
51}
52
53fn default_namespace_index() -> u16 {
54 2
55}
56
57impl Default for OpcUaDeviceMetadata {
58 fn default() -> Self {
59 Self {
60 server_config: OpcUaServerConfig::default(),
61 address_space_config: None,
62 cache_config: None,
63 history_config: None,
64 namespace_uri: default_namespace(),
65 namespace_index: default_namespace_index(),
66 }
67 }
68}
69
70pub struct OpcUaDeviceFactory {
72 default_config: OpcUaServerConfig,
74}
75
76impl OpcUaDeviceFactory {
77 pub fn new() -> Self {
79 Self {
80 default_config: OpcUaServerConfig::default(),
81 }
82 }
83
84 pub fn with_config(config: OpcUaServerConfig) -> Self {
86 Self {
87 default_config: config,
88 }
89 }
90
91 fn parse_metadata(&self, config: &DeviceConfig) -> OpcUaDeviceMetadata {
93 if let Some(meta) = config.metadata.get("opcua") {
94 serde_json::from_str(meta).unwrap_or_default()
96 } else {
97 OpcUaDeviceMetadata::default()
98 }
99 }
100
101 fn point_to_variant(&self, config: &DataPointConfig) -> Variant {
103 match config.data_type.to_lowercase().as_str() {
105 "bool" | "boolean" => Variant::Boolean(false),
106 "i8" | "int8" | "sbyte" => Variant::SByte(0),
107 "u8" | "uint8" | "byte" => Variant::Byte(0),
108 "i16" | "int16" => Variant::Int16(0),
109 "u16" | "uint16" => Variant::UInt16(0),
110 "i32" | "int32" => Variant::Int32(0),
111 "u32" | "uint32" => Variant::UInt32(0),
112 "i64" | "int64" => Variant::Int64(0),
113 "u64" | "uint64" => Variant::UInt64(0),
114 "f32" | "float" | "float32" => Variant::Float(0.0),
115 "f64" | "double" | "float64" => Variant::Double(0.0),
116 "string" => Variant::String(String::new()),
117 "datetime" => Variant::DateTime(chrono::Utc::now()),
118 "bytestring" | "bytes" => Variant::ByteString(Vec::new()),
119 _ => Variant::Double(0.0), }
121 }
122
123 fn config_to_def(&self, config: &DataPointConfig) -> DataPointDef {
125 use mabi_core::types::{AccessMode, Address, DataType};
126
127 let data_type = match config.data_type.to_lowercase().as_str() {
128 "bool" | "boolean" => DataType::Bool,
129 "i8" | "int8" | "sbyte" => DataType::Int8,
130 "u8" | "uint8" | "byte" => DataType::UInt8,
131 "i16" | "int16" => DataType::Int16,
132 "u16" | "uint16" => DataType::UInt16,
133 "i32" | "int32" => DataType::Int32,
134 "u32" | "uint32" => DataType::UInt32,
135 "i64" | "int64" => DataType::Int64,
136 "u64" | "uint64" => DataType::UInt64,
137 "f32" | "float" | "float32" => DataType::Float32,
138 "f64" | "double" | "float64" => DataType::Float64,
139 "string" => DataType::String,
140 "datetime" => DataType::DateTime,
141 "bytestring" | "bytes" => DataType::ByteString,
142 _ => DataType::Float64,
143 };
144
145 let access = match config.access.to_lowercase().as_str() {
146 "r" | "ro" | "readonly" => AccessMode::ReadOnly,
147 "w" | "wo" | "writeonly" => AccessMode::WriteOnly,
148 _ => AccessMode::ReadWrite,
149 };
150
151 let mut def = DataPointDef::new(&config.id, &config.name, data_type)
152 .with_access(access);
153
154 if let Some(unit) = &config.units {
155 def = def.with_units(unit);
156 }
157 if let (Some(min), Some(max)) = (config.min, config.max) {
158 def = def.with_range(min, max);
159 }
160 if let Some(addr) = &config.address {
161 def = def.with_address(Address::OpcUa { node_id: addr.clone() });
162 }
163
164 def
165 }
166
167 fn variant_to_value(&self, variant: &Variant) -> Value {
169 variant.clone().into()
170 }
171}
172
173impl Default for OpcUaDeviceFactory {
174 fn default() -> Self {
175 Self::new()
176 }
177}
178
179impl DeviceFactory for OpcUaDeviceFactory {
180 fn protocol(&self) -> Protocol {
181 Protocol::OpcUa
182 }
183
184 fn create(&self, config: DeviceConfig) -> Result<BoxedDevice> {
185 let _metadata = self.parse_metadata(&config);
186
187 let mut device = OpcUaDevice::new(&config.id, &config.name);
189
190 for point_config in &config.points {
192 let initial_variant = self.point_to_variant(point_config);
193 let initial_value = self.variant_to_value(&initial_variant);
194 let point_def = self.config_to_def(point_config);
195 device.add_node(point_def, initial_value);
196 }
197
198 Ok(Box::new(device))
199 }
200
201 fn validate(&self, config: &DeviceConfig) -> Result<()> {
202 if config.id.is_empty() {
203 return Err(Error::Config("Device ID cannot be empty".into()));
204 }
205 if config.name.is_empty() {
206 return Err(Error::Config("Device name cannot be empty".into()));
207 }
208 if config.protocol != Protocol::OpcUa {
209 return Err(Error::Config(format!(
210 "Protocol mismatch: expected OpcUa, got {:?}",
211 config.protocol
212 )));
213 }
214
215 if let Some(meta_str) = config.metadata.get("opcua") {
217 let _: OpcUaDeviceMetadata = serde_json::from_str(meta_str)
218 .map_err(|e| Error::Config(format!("Invalid OPC UA metadata: {}", e)))?;
219 }
220
221 for (i, point) in config.points.iter().enumerate() {
223 if point.id.is_empty() {
224 return Err(Error::Config(format!("Point {} has empty ID", i)));
225 }
226 }
227
228 Ok(())
229 }
230
231 fn metadata(&self) -> FactoryMetadata {
232 FactoryMetadata {
233 protocol: Protocol::OpcUa,
234 version: env!("CARGO_PKG_VERSION").to_string(),
235 description: "OPC UA server simulator factory".to_string(),
236 capabilities: vec![
237 "subscription".to_string(),
238 "history".to_string(),
239 "browse".to_string(),
240 "read".to_string(),
241 "write".to_string(),
242 "monitored_items".to_string(),
243 "sessions".to_string(),
244 ],
245 }
246 }
247}
248
249pub struct OpcUaDeviceBuilder {
251 id: String,
252 name: String,
253 description: Option<String>,
254 server_config: OpcUaServerConfig,
255 address_space_config: AddressSpaceConfig,
256 cache_config: Option<NodeCacheConfig>,
257 history_config: Option<HistoryStoreConfig>,
258 variables: Vec<VariableSpec>,
259 namespace_uri: String,
260}
261
262#[derive(Debug, Clone)]
264pub struct VariableSpec {
265 pub id: String,
267 pub name: String,
269 pub node_id: NodeId,
271 pub initial_value: Variant,
273 pub writable: bool,
275 pub unit: Option<String>,
277}
278
279impl OpcUaDeviceBuilder {
280 pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
282 Self {
283 id: id.into(),
284 name: name.into(),
285 description: None,
286 server_config: OpcUaServerConfig::default(),
287 address_space_config: AddressSpaceConfig::default(),
288 cache_config: None,
289 history_config: None,
290 variables: Vec::new(),
291 namespace_uri: "urn:trap-simulator:opcua".to_string(),
292 }
293 }
294
295 pub fn description(mut self, desc: impl Into<String>) -> Self {
297 self.description = Some(desc.into());
298 self
299 }
300
301 pub fn server_config(mut self, config: OpcUaServerConfig) -> Self {
303 self.server_config = config;
304 self
305 }
306
307 pub fn address_space_config(mut self, config: AddressSpaceConfig) -> Self {
309 self.address_space_config = config;
310 self
311 }
312
313 pub fn with_cache(mut self, config: NodeCacheConfig) -> Self {
315 self.cache_config = Some(config);
316 self
317 }
318
319 pub fn with_history(mut self, config: HistoryStoreConfig) -> Self {
321 self.history_config = Some(config);
322 self
323 }
324
325 pub fn namespace_uri(mut self, uri: impl Into<String>) -> Self {
327 self.namespace_uri = uri.into();
328 self
329 }
330
331 pub fn add_variable(mut self, spec: VariableSpec) -> Self {
333 self.variables.push(spec);
334 self
335 }
336
337 pub fn add_analog_variable(
339 mut self,
340 id: impl Into<String>,
341 name: impl Into<String>,
342 node_id: NodeId,
343 initial_value: f64,
344 ) -> Self {
345 self.variables.push(VariableSpec {
346 id: id.into(),
347 name: name.into(),
348 node_id,
349 initial_value: Variant::Double(initial_value),
350 writable: true,
351 unit: None,
352 });
353 self
354 }
355
356 pub fn add_discrete_variable(
358 mut self,
359 id: impl Into<String>,
360 name: impl Into<String>,
361 node_id: NodeId,
362 initial_state: u32,
363 ) -> Self {
364 self.variables.push(VariableSpec {
365 id: id.into(),
366 name: name.into(),
367 node_id,
368 initial_value: Variant::UInt32(initial_state),
369 writable: true,
370 unit: None,
371 });
372 self
373 }
374
375 pub fn add_boolean_variable(
377 mut self,
378 id: impl Into<String>,
379 name: impl Into<String>,
380 node_id: NodeId,
381 initial_value: bool,
382 ) -> Self {
383 self.variables.push(VariableSpec {
384 id: id.into(),
385 name: name.into(),
386 node_id,
387 initial_value: Variant::Boolean(initial_value),
388 writable: true,
389 unit: None,
390 });
391 self
392 }
393
394 pub fn add_string_variable(
396 mut self,
397 id: impl Into<String>,
398 name: impl Into<String>,
399 node_id: NodeId,
400 initial_value: impl Into<String>,
401 ) -> Self {
402 self.variables.push(VariableSpec {
403 id: id.into(),
404 name: name.into(),
405 node_id,
406 initial_value: Variant::String(initial_value.into()),
407 writable: true,
408 unit: None,
409 });
410 self
411 }
412
413 pub fn build(self) -> OpcUaDevice {
415 let mut device = OpcUaDevice::new(&self.id, &self.name);
416
417 for var_spec in self.variables {
419 use mabi_core::types::AccessMode;
420
421 let mut point_def = DataPointDef::new(
422 var_spec.id.clone(),
423 var_spec.name.clone(),
424 Self::variant_to_data_type(&var_spec.initial_value),
425 );
426
427 if var_spec.writable {
428 point_def = point_def.with_access(AccessMode::ReadWrite);
429 } else {
430 point_def = point_def.with_access(AccessMode::ReadOnly);
431 }
432
433 if let Some(unit) = var_spec.unit {
434 point_def = point_def.with_units(unit);
435 }
436
437 let value = Value::from(var_spec.initial_value);
438 device.add_node(point_def, value);
439 }
440
441 device
442 }
443
444 fn variant_to_data_type(variant: &Variant) -> mabi_core::types::DataType {
446 use mabi_core::types::DataType;
447
448 match variant {
449 Variant::Boolean(_) => DataType::Bool,
450 Variant::SByte(_) => DataType::Int8,
451 Variant::Byte(_) => DataType::UInt8,
452 Variant::Int16(_) => DataType::Int16,
453 Variant::UInt16(_) => DataType::UInt16,
454 Variant::Int32(_) => DataType::Int32,
455 Variant::UInt32(_) => DataType::UInt32,
456 Variant::Int64(_) => DataType::Int64,
457 Variant::UInt64(_) => DataType::UInt64,
458 Variant::Float(_) => DataType::Float32,
459 Variant::Double(_) => DataType::Float64,
460 Variant::String(_) | Variant::Guid(_) => DataType::String,
461 Variant::DateTime(_) => DataType::DateTime,
462 Variant::ByteString(_) => DataType::ByteString,
463 _ => DataType::Float64, }
465 }
466}
467
468pub struct OpcUaPlugin {
470 name: String,
471 factory_config: Option<OpcUaServerConfig>,
472}
473
474impl OpcUaPlugin {
475 pub fn new() -> Self {
477 Self {
478 name: "opcua".to_string(),
479 factory_config: None,
480 }
481 }
482
483 pub fn with_name(name: impl Into<String>) -> Self {
485 Self {
486 name: name.into(),
487 factory_config: None,
488 }
489 }
490
491 pub fn with_factory_config(mut self, config: OpcUaServerConfig) -> Self {
493 self.factory_config = Some(config);
494 self
495 }
496}
497
498impl Default for OpcUaPlugin {
499 fn default() -> Self {
500 Self::new()
501 }
502}
503
504impl Plugin for OpcUaPlugin {
505 fn name(&self) -> &str {
506 &self.name
507 }
508
509 fn version(&self) -> &str {
510 env!("CARGO_PKG_VERSION")
511 }
512
513 fn description(&self) -> &str {
514 "OPC UA server simulator plugin providing OPC UA device support"
515 }
516
517 fn register_factories(&self, registry: &FactoryRegistry) -> Result<()> {
518 let factory = if let Some(config) = &self.factory_config {
519 OpcUaDeviceFactory::with_config(config.clone())
520 } else {
521 OpcUaDeviceFactory::new()
522 };
523
524 registry.register(factory)
525 }
526}
527
528pub fn create_opcua_config(
530 id: impl Into<String>,
531 name: impl Into<String>,
532 points: Vec<DataPointConfig>,
533) -> DeviceConfig {
534 DeviceConfig {
535 id: id.into(),
536 name: name.into(),
537 description: String::new(),
538 protocol: Protocol::OpcUa,
539 address: None,
540 points,
541 metadata: HashMap::new(),
542 }
543}
544
545pub fn create_opcua_config_with_metadata(
547 id: impl Into<String>,
548 name: impl Into<String>,
549 points: Vec<DataPointConfig>,
550 metadata: OpcUaDeviceMetadata,
551) -> DeviceConfig {
552 let mut meta_map = HashMap::new();
553 if let Ok(value) = serde_json::to_string(&metadata) {
554 meta_map.insert("opcua".to_string(), value);
555 }
556
557 DeviceConfig {
558 id: id.into(),
559 name: name.into(),
560 description: String::new(),
561 protocol: Protocol::OpcUa,
562 address: None,
563 points,
564 metadata: meta_map,
565 }
566}
567
568#[cfg(test)]
569mod tests {
570 use super::*;
571 use mabi_core::device::Device;
572
573 fn create_test_point(id: &str, name: &str, data_type: &str) -> DataPointConfig {
574 DataPointConfig {
575 id: id.to_string(),
576 name: name.to_string(),
577 data_type: data_type.to_string(),
578 access: "rw".to_string(),
579 address: None,
580 initial_value: None,
581 units: None,
582 min: None,
583 max: None,
584 }
585 }
586
587 #[test]
588 fn test_factory_protocol() {
589 let factory = OpcUaDeviceFactory::new();
590 assert_eq!(factory.protocol(), Protocol::OpcUa);
591 }
592
593 #[test]
594 fn test_factory_create_device() {
595 let factory = OpcUaDeviceFactory::new();
596
597 let mut point = create_test_point("temp", "Temperature", "f64");
598 point.units = Some("°C".to_string());
599
600 let config = create_opcua_config("server-1", "OPC UA Server 1", vec![point]);
601
602 let device = factory.create(config).unwrap();
603 assert_eq!(device.id(), "server-1");
604 assert_eq!(device.name(), "OPC UA Server 1");
605 }
606
607 #[test]
608 fn test_factory_validation() {
609 let factory = OpcUaDeviceFactory::new();
610
611 let config = DeviceConfig {
613 id: String::new(),
614 name: "Test".to_string(),
615 description: String::new(),
616 protocol: Protocol::OpcUa,
617 address: None,
618 points: vec![],
619 metadata: HashMap::new(),
620 };
621 assert!(factory.validate(&config).is_err());
622
623 let config = DeviceConfig {
625 id: "test".to_string(),
626 name: "Test".to_string(),
627 description: String::new(),
628 protocol: Protocol::ModbusTcp,
629 address: None,
630 points: vec![],
631 metadata: HashMap::new(),
632 };
633 assert!(factory.validate(&config).is_err());
634 }
635
636 #[test]
637 fn test_device_builder() {
638 let device = OpcUaDeviceBuilder::new("server-1", "Test Server")
639 .description("A test OPC UA server")
640 .add_analog_variable(
641 "temp",
642 "Temperature",
643 NodeId::numeric(2, 1001),
644 25.0,
645 )
646 .add_boolean_variable(
647 "status",
648 "Status",
649 NodeId::numeric(2, 1002),
650 true,
651 )
652 .build();
653
654 assert_eq!(device.info().id, "server-1");
655 assert!(device.point_definition("temp").is_some());
656 assert!(device.point_definition("status").is_some());
657 }
658
659 #[test]
660 fn test_plugin() {
661 let registry = FactoryRegistry::new();
662 let plugin = OpcUaPlugin::new();
663
664 assert_eq!(plugin.name(), "opcua");
665 plugin.register_factories(®istry).unwrap();
666
667 assert!(registry.has(Protocol::OpcUa));
668 }
669
670 #[test]
671 fn test_factory_metadata() {
672 let factory = OpcUaDeviceFactory::new();
673 let metadata = factory.metadata();
674
675 assert_eq!(metadata.protocol, Protocol::OpcUa);
676 assert!(metadata.capabilities.contains(&"subscription".to_string()));
677 assert!(metadata.capabilities.contains(&"history".to_string()));
678 }
679}