1use bhtune_driver::{Driver, FopdtConfig, OpcDaDriver, SimulatorDriver};
4
5use crate::tune::{DriverKind, TuneRequest};
6
7pub const SIMULATOR_PV_TAG: &str = "Sim.PV";
10pub const SIMULATOR_MV_TAG: &str = "Sim.MV";
11
12pub async fn build_with_poll_interval(
16 args: &TuneRequest,
17 poll_interval_ms: u64,
18) -> anyhow::Result<Box<dyn Driver>> {
19 match args.driver {
20 DriverKind::Opcda => {
21 let server = args
22 .server
23 .clone()
24 .ok_or_else(|| anyhow::anyhow!("--server is required with --driver opcda"))?;
25 let bridge_host = args
31 .bridge_host
32 .as_deref()
33 .unwrap_or(crate::config::DEFAULT_BRIDGE_HOST);
34 tracing::info!(bridge_host, server = %server, "connecting to opcda-bridge gateway");
35 let driver = OpcDaDriver::connect(bridge_host, server).await?;
36 Ok(Box::new(driver))
37 }
38 DriverKind::Simulator => {
39 tracing::info!(
40 gain = args.sim_gain,
41 tau = args.sim_tau,
42 dead_time = args.sim_dead_time,
43 "constructing simulator driver"
44 );
45 let config = FopdtConfig::new(
46 args.sim_gain,
47 args.sim_tau,
48 args.sim_dead_time,
49 poll_interval_ms as f32 / 1000.0,
50 )
51 .with_noise_amplitude(args.sim_noise);
52 let driver = SimulatorDriver::new(
53 SIMULATOR_PV_TAG,
54 SIMULATOR_MV_TAG,
55 config,
56 args.sim_initial_pv,
57 args.sim_initial_mv,
58 args.sim_seed,
59 );
60 Ok(Box::new(driver))
61 }
62 }
63}
64
65#[cfg(test)]
66pub async fn build(args: &TuneRequest) -> anyhow::Result<Box<dyn Driver>> {
67 build_with_poll_interval(args, 800).await
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73 use bhtune_core::{ControllerDirection, ControllerType, ProcessType};
74 use bhtune_driver::TagWrite;
75
76 fn sim_args() -> TuneRequest {
77 TuneRequest {
78 tagname: "ignored".to_string(),
79 template: "Yokogawa CentumVP".to_string(),
80 process_type: ProcessType::Flow,
81 controller_type: ControllerType::Pi,
82 relay_amp: 10.0,
83 cycles_skip: None,
84 cycles_count: None,
85 noise_protection_secs: None,
86 mrft_delay: 0,
87 poll_interval_ms: 800,
88 timeout_secs: 3600,
89 op_timeout_secs: 30,
90 restore_timeout_secs: 30,
91 driver: DriverKind::Simulator,
92 bridge_host: None,
93 server: None,
94 sim_gain: 1.0,
95 sim_tau: 2.0,
96 sim_dead_time: 5.0,
97 sim_noise: 0.0,
98 sim_seed: 0,
99 sim_initial_pv: 50.0,
100 sim_initial_mv: 50.0,
101 pv_range_high: Some(100.0),
102 pv_range_low: Some(0.0),
103 mv_range_high: Some(100.0),
104 mv_range_low: Some(0.0),
105 direction: Some(ControllerDirection::Reverse),
106 tag_overrides: None,
107 notes: None,
108 yes: false,
109 write_pid: None,
110 }
111 }
112
113 #[tokio::test]
114 async fn builds_a_working_simulator_driver() {
115 let driver = build(&sim_args()).await.unwrap();
116 let values = driver.read(&[SIMULATOR_MV_TAG.to_string()]).await.unwrap();
117 assert_eq!(values[0].value, "50");
118 }
119
120 #[tokio::test]
121 async fn simulator_uses_poll_interval_in_seconds_for_process_dynamics() {
122 let mut args = sim_args();
123 args.sim_tau = 1_000.0;
124 args.sim_dead_time = 0.0;
125 args.sim_initial_pv = 0.0;
126 args.sim_initial_mv = 0.0;
127 let driver = build_with_poll_interval(&args, 800).await.unwrap();
128 let outcome = driver
129 .write(&SIMULATOR_MV_TAG.to_string(), TagWrite::Float(100.0))
130 .await
131 .unwrap();
132 assert!(outcome.success);
133
134 let values = driver.read(&[SIMULATOR_PV_TAG.to_string()]).await.unwrap();
135 let pv = values[0].value.parse::<f32>().unwrap();
136 let expected = 100.0 * (1.0 - (-0.8_f32 / 1_000.0).exp());
137 assert!(
138 (pv - expected).abs() < 0.001,
139 "expected one 800 ms process step ({expected}), got {pv}"
140 );
141 }
142
143 #[tokio::test]
144 async fn opcda_driver_requires_a_server_flag() {
145 let mut args = sim_args();
146 args.driver = DriverKind::Opcda;
147 args.bridge_host = Some("127.0.0.1:1".to_string());
148 args.server = None;
149 let result = build(&args).await;
150 assert!(result.is_err());
151 assert!(result.err().unwrap().to_string().contains("--server"));
152 }
153
154 #[tokio::test]
155 async fn opcda_driver_falls_back_to_the_default_bridge_host_when_unset() {
156 let mut args = sim_args();
161 args.driver = DriverKind::Opcda;
162 args.bridge_host = None;
163 args.server = Some("MockServer".to_string());
164 let err = build(&args).await.err().unwrap();
165 assert!(!err.to_string().is_empty());
168 }
169
170 #[tokio::test]
171 async fn opcda_driver_connects_and_reads_through_a_mock_bridge() {
172 use crate::test_support::{MockBridgeService, start_mock_server};
173 use opcda_bridge_proto::bridge::{ReadResponse, TagValue as ProtoTagValue};
174
175 let (host, server) = start_mock_server(MockBridgeService {
176 read_response: ReadResponse {
177 values: vec![ProtoTagValue {
178 tag_id: "Sim.MV".to_string(),
179 value: "50".to_string(),
180 quality: "Good".to_string(),
181 timestamp: "2024-01-15 10:23:45".to_string(),
182 }],
183 },
184 ..Default::default()
185 })
186 .await;
187
188 let mut args = sim_args();
189 args.driver = DriverKind::Opcda;
190 args.bridge_host = Some(host);
191 args.server = Some("MockServer".to_string());
192
193 let driver = build(&args).await.unwrap();
196 let values = driver.read(&["Sim.MV".to_string()]).await.unwrap();
197 assert_eq!(values[0].value, "50");
198
199 server.shutdown().await;
200 }
201}