Skip to main content

bhtune_runtime/tune/
config.rs

1#![allow(rustdoc::broken_intra_doc_links)]
2
3use bhtune_core::{ControllerType, DcsTemplate, LoopConfig, LoopTags, ProcessType, TagOrValue};
4use bhtune_db::models::EffectiveTuning;
5
6use crate::driver::{SIMULATOR_MV_TAG, SIMULATOR_PV_TAG};
7use crate::tune::{DriverKind, TuneRequest};
8
9/// Concrete timing policy frozen during [`prepare`] and carried unchanged through the
10/// complete tune lifecycle.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub(super) struct EffectiveTiming {
13    pub(super) mrft_delay_secs: u32,
14    pub(super) poll_interval_ms: u64,
15    pub(super) timeout_secs: u64,
16    pub(super) op_timeout_secs: u64,
17    pub(super) restore_timeout_secs: u64,
18}
19impl From<crate::config::EffectiveTuningConfig> for EffectiveTiming {
20    fn from(value: crate::config::EffectiveTuningConfig) -> Self {
21        Self {
22            mrft_delay_secs: value.mrft_delay_secs,
23            poll_interval_ms: value.poll_interval_ms,
24            timeout_secs: value.timeout_secs,
25            op_timeout_secs: value.op_timeout_secs,
26            restore_timeout_secs: value.restore_timeout_secs,
27        }
28    }
29}
30impl From<EffectiveTiming> for EffectiveTuning {
31    fn from(value: EffectiveTiming) -> Self {
32        Self {
33            mrft_delay_secs: value.mrft_delay_secs,
34            poll_interval_ms: value.poll_interval_ms,
35            timeout_secs: value.timeout_secs,
36            op_timeout_secs: value.op_timeout_secs,
37            restore_timeout_secs: value.restore_timeout_secs,
38        }
39    }
40}
41#[cfg(test)]
42pub(super) fn test_effective_timing(args: &TuneRequest) -> EffectiveTiming {
43    EffectiveTiming {
44        mrft_delay_secs: args.mrft_delay,
45        poll_interval_ms: args.poll_interval_ms,
46        timeout_secs: args.timeout_secs,
47        op_timeout_secs: args.op_timeout_secs,
48        restore_timeout_secs: args.restore_timeout_secs,
49    }
50}
51/// Validates the restore budget shared by CLI and HTTP-started tunes.
52///
53/// Every driver requires a positive timeout. OPC DA additionally needs the complete fixed MV
54/// confirmation window so the authoritative restore can be read back before the operation is
55/// declared successful.
56pub fn validate_restore_timeout_secs(
57    driver: DriverKind,
58    restore_timeout_secs: u64,
59) -> anyhow::Result<()> {
60    if restore_timeout_secs == 0 {
61        anyhow::bail!("[tuning].restore_timeout_secs must be greater than zero");
62    }
63    if driver == DriverKind::Opcda
64        && restore_timeout_secs < crate::config::MIN_OPC_RESTORE_TIMEOUT_SECS
65    {
66        anyhow::bail!(
67            "[tuning].restore_timeout_secs must be at least {} seconds for OPC DA MV confirmation",
68            crate::config::MIN_OPC_RESTORE_TIMEOUT_SECS
69        );
70    }
71    Ok(())
72}
73pub(super) fn build_loop_config_with_timing(
74    args: &TuneRequest,
75    timing: EffectiveTiming,
76) -> anyhow::Result<LoopConfig> {
77    let process_type: ProcessType = args.process_type;
78    let controller_type: ControllerType = args.controller_type;
79
80    if !controller_type.is_allowed_for(process_type) {
81        anyhow::bail!(
82            "{controller_type:?} controller is not valid for {process_type:?} (PID is only offered for the two Temperature process types)"
83        );
84    }
85
86    let config = LoopConfig {
87        process_type,
88        controller_type,
89        relay_amp_percent: args.relay_amp,
90        num_cycles_skip: args
91            .cycles_skip
92            .unwrap_or_else(|| process_type.default_cycles_skip()),
93        num_cycles_count: args
94            .cycles_count
95            .unwrap_or_else(|| process_type.default_cycles_test()),
96        noise_protection_secs: args
97            .noise_protection_secs
98            .unwrap_or_else(|| process_type.default_noise_protection_secs()),
99        mrft_delay_secs: timing.mrft_delay_secs,
100    };
101    // Real range validation at the model level (see `LoopConfig::validate`), not just this
102    // flag parse -- catches an out-of-range `--relay-amp` (including the legacy predecessor's
103    // "not blank" bug of a stray debug shortcut reaching this field) before any driver
104    // connection or database write.
105    config.validate()?;
106    Ok(config)
107}
108#[cfg(test)]
109pub(super) fn build_loop_config(args: &TuneRequest) -> anyhow::Result<LoopConfig> {
110    build_loop_config_with_timing(args, test_effective_timing(args))
111}
112/// Builds the loop's full tag set. For `--driver opcda`, derives from `--tagname` and the
113/// template, then layers any explicit `--pv-range-*`/`--mv-range-*`/`--direction` overrides
114/// on top. For `--driver simulator`, `SimulatorDriver`'s fixed two-tag contract means the
115/// range/direction overrides are mandatory (normally supplied by the `bhtune simulate`
116/// adapter); a direct `bhtune tune --driver simulator` invocation
117/// missing any of them is a clear usage error rather than a confusing runtime failure.
118pub(super) fn build_loop_tags(
119    args: &TuneRequest,
120    template: &DcsTemplate,
121) -> anyhow::Result<LoopTags> {
122    match args.driver {
123        DriverKind::Opcda => {
124            let mut tags = LoopTags::derive_from_pv_tag(&args.tagname, template);
125            if let Some(overrides) = &args.tag_overrides {
126                overrides.apply_to(&mut tags);
127            }
128            // Fixed values are applied after custom read-tag overrides, so an explicit fixed
129            // request remains authoritative when input contains both.
130            if let Some(v) = args.pv_range_high {
131                tags.upper_pv_range = TagOrValue::Value(v);
132            }
133            if let Some(v) = args.pv_range_low {
134                tags.lower_pv_range = TagOrValue::Value(v);
135            }
136            if let Some(v) = args.mv_range_high {
137                tags.upper_mv_range = TagOrValue::Value(v);
138            }
139            if let Some(v) = args.mv_range_low {
140                tags.lower_mv_range = TagOrValue::Value(v);
141            }
142            if let Some(d) = args.direction {
143                tags.controller_direction = TagOrValue::Value(d);
144            }
145            Ok(tags)
146        }
147        DriverKind::Simulator => {
148            let pv_range_high = args.pv_range_high.ok_or_else(|| {
149                anyhow::anyhow!(
150                    "--pv-range-high is required with --driver simulator (or use `bhtune simulate`)"
151                )
152            })?;
153            let pv_range_low = args.pv_range_low.ok_or_else(|| {
154                anyhow::anyhow!(
155                    "--pv-range-low is required with --driver simulator (or use `bhtune simulate`)"
156                )
157            })?;
158            let mv_range_high = args.mv_range_high.ok_or_else(|| {
159                anyhow::anyhow!(
160                    "--mv-range-high is required with --driver simulator (or use `bhtune simulate`)"
161                )
162            })?;
163            let mv_range_low = args.mv_range_low.ok_or_else(|| {
164                anyhow::anyhow!(
165                    "--mv-range-low is required with --driver simulator (or use `bhtune simulate`)"
166                )
167            })?;
168            let direction = args.direction.ok_or_else(|| {
169                anyhow::anyhow!(
170                    "--direction is required with --driver simulator (or use `bhtune simulate`)"
171                )
172            })?;
173
174            Ok(LoopTags {
175                process_variable: SIMULATOR_PV_TAG.to_string(),
176                manipulated_variable: SIMULATOR_MV_TAG.to_string(),
177                setpoint_variable: None,
178                controller_mode: None,
179                mode_attribute: None,
180                upper_pv_range: TagOrValue::Value(pv_range_high),
181                lower_pv_range: TagOrValue::Value(pv_range_low),
182                upper_mv_range: TagOrValue::Value(mv_range_high),
183                lower_mv_range: TagOrValue::Value(mv_range_low),
184                controller_direction: TagOrValue::Value(direction),
185                proportional_constant: None,
186                integral_constant: None,
187                derivative_constant: None,
188            })
189        }
190    }
191}