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