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