Skip to main content

bhtune_server/routes/runs/
dto.rs

1use bhtune_core::{ControllerDirection, ControllerType, ProcessType, ResponseLevel, TagOverrides};
2use bhtune_db::models::TuneDriver;
3use bhtune_runtime::tune::{
4    DEFAULT_SIM_DEAD_TIME, DEFAULT_SIM_GAIN, DEFAULT_SIM_INITIAL_VALUE, DEFAULT_SIM_NOISE,
5    DEFAULT_SIM_SEED, DEFAULT_SIM_TAU, PreflightCheck, PreflightCheckStatus, PreflightReport,
6    PreflightTagRead,
7};
8use serde::{Deserialize, Serialize};
9use utoipa::ToSchema;
10
11fn default_sim_gain() -> f32 {
12    DEFAULT_SIM_GAIN
13}
14fn default_sim_tau() -> f32 {
15    DEFAULT_SIM_TAU
16}
17fn default_sim_dead_time() -> f32 {
18    DEFAULT_SIM_DEAD_TIME
19}
20fn default_sim_noise() -> f32 {
21    DEFAULT_SIM_NOISE
22}
23fn default_sim_seed() -> u64 {
24    DEFAULT_SIM_SEED
25}
26fn default_sim_initial_value() -> f32 {
27    DEFAULT_SIM_INITIAL_VALUE
28}
29
30/// The bodies of `POST /api/runs` and `POST /api/runs/preflight` contain the per-run tune
31/// inputs. Operational timing values are intentionally absent: both preparation and preflight
32/// resolve them from the global `[tuning]` configuration, just as they are for a CLI invocation.
33/// Every field that has a CLI default (`--sim-gain`, etc.) repeats that exact default here via
34/// `#[serde(default = "...")]`, so an HTTP caller that omits a field gets identical behavior to a
35/// CLI invocation that omits the matching flag. `Option<T>` fields need no `#[serde(default)]`
36/// of their own -- serde already treats a missing key as `None` for an `Option` field.
37///
38/// Also derives `Serialize` so the exact same type can serve as `GET /api/runs/last-request`'s
39/// response (`ui-prefill-last-run`, in `routes::history::last_request`): that endpoint parses
40/// a run's stored `request_json` straight into a `StartRunRequest` rather than duplicating
41/// its ~30 fields into a second struct, giving a "what you `GET` is exactly what you'd `POST`
42/// to repeat it" symmetry in both the Rust types and the generated OpenAPI schema. This is
43/// safe precisely because `request_json` is *already* built to this exact shape -- the
44/// runtime's `RequestSnapshot` serializes the same transport-neutral request fields.
45#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
46#[serde(deny_unknown_fields)]
47pub struct StartRunRequest {
48    /// PV tag prefix; ignored for `driver: "simulator"`.
49    pub tagname: String,
50    /// DCS/PLC template name (see `GET /api/templates`).
51    pub template: String,
52    pub process_type: ProcessType,
53    pub controller_type: ControllerType,
54    /// Relay amplitude, as a percentage of the MV range.
55    pub relay_amp: f32,
56    /// Relay cycles to skip before counting begins (default: looked up per `process_type`).
57    pub cycles_skip: Option<u32>,
58    /// Relay cycles to count once the skip period ends (default: looked up per
59    /// `process_type`).
60    pub cycles_count: Option<u32>,
61    /// Seconds a switch must persist before it's accepted (default: looked up per
62    /// `process_type`).
63    pub noise_protection_secs: Option<u32>,
64    /// Which driver drives this tune. `"replay"` is rejected -- that driver exists only
65    /// for offline golden-trace validation, not for starting a live/simulated run.
66    pub driver: TuneDriver,
67    /// opcda-bridge gateway address. Only meaningful with `driver: "opcda"` (default:
68    /// resolved the same way the CLI resolves `--bridge-host`, via this process's own
69    /// config/env).
70    pub bridge_host: Option<String>,
71    /// OPC DA server ProgID. Required with `driver: "opcda"`.
72    pub server: Option<String>,
73    /// Simulator process gain (`driver: "simulator"` only).
74    #[serde(default = "default_sim_gain")]
75    pub sim_gain: f32,
76    /// Simulator process time constant, in seconds (`driver: "simulator"` only).
77    #[serde(default = "default_sim_tau")]
78    pub sim_tau: f32,
79    /// Simulator dead time, in seconds (`driver: "simulator"` only).
80    #[serde(default = "default_sim_dead_time")]
81    pub sim_dead_time: f32,
82    /// Simulator measurement noise amplitude (`driver: "simulator"` only).
83    #[serde(default = "default_sim_noise")]
84    pub sim_noise: f32,
85    /// Simulator RNG seed, for reproducible noise (`driver: "simulator"` only).
86    #[serde(default = "default_sim_seed")]
87    pub sim_seed: u64,
88    /// Simulator initial PV (`driver: "simulator"` only).
89    #[serde(default = "default_sim_initial_value")]
90    pub sim_initial_pv: f32,
91    /// Simulator initial MV (`driver: "simulator"` only).
92    #[serde(default = "default_sim_initial_value")]
93    pub sim_initial_mv: f32,
94    /// Fixed PV range high, overriding a live tag read. Required for `driver: "simulator"`,
95    /// which has no range tags at all.
96    pub pv_range_high: Option<f32>,
97    /// Fixed PV range low, overriding a live tag read.
98    pub pv_range_low: Option<f32>,
99    /// Fixed MV range high, overriding a live tag read.
100    pub mv_range_high: Option<f32>,
101    /// Fixed MV range low, overriding a live tag read.
102    pub mv_range_low: Option<f32>,
103    /// Fixed controller direction, overriding a live tag read.
104    pub direction: Option<ControllerDirection>,
105    /// Per-tune replacements for template-derived OPC tag names. Blank or missing fields use
106    /// the template-derived tag.
107    pub tag_overrides: Option<TagOverrides>,
108    /// Operator notes to attach to this run. Notes can be edited or cleared later through
109    /// the run-history endpoints.
110    #[serde(default)]
111    pub notes: Option<String>,
112    /// Confirm an unattended PID write-back. Required alongside `write_pid` -- the request
113    /// is rejected otherwise, identically to `--write-pid` without `--yes` on the CLI.
114    #[serde(default)]
115    pub yes: bool,
116    /// Non-interactively write this response level's calculated PID parameters back to the
117    /// DCS. Requires `yes: true`.
118    pub write_pid: Option<ResponseLevel>,
119}
120
121/// Overall severity of a read-only preflight report.
122#[derive(Debug, Clone, Copy, Serialize, ToSchema)]
123#[serde(rename_all = "snake_case")]
124pub enum PreflightStatus {
125    Pass,
126    Warn,
127    Fail,
128}
129
130impl From<PreflightCheckStatus> for PreflightStatus {
131    fn from(status: PreflightCheckStatus) -> Self {
132        match status {
133            PreflightCheckStatus::Pass => Self::Pass,
134            PreflightCheckStatus::Warn => Self::Warn,
135            PreflightCheckStatus::Fail => Self::Fail,
136        }
137    }
138}
139
140/// One check and its operator-facing detail in a preflight report.
141#[derive(Debug, Clone, Serialize, ToSchema)]
142pub struct PreflightCheckResponse {
143    /// Check name.
144    pub name: String,
145    /// Result severity.
146    pub status: PreflightStatus,
147    /// Diagnostic detail.
148    pub detail: String,
149}
150
151impl From<PreflightCheck> for PreflightCheckResponse {
152    fn from(check: PreflightCheck) -> Self {
153        Self {
154            name: check.name,
155            status: check.status.into(),
156            detail: check.detail,
157        }
158    }
159}
160
161/// One derived tag read and its operator-facing detail in a preflight report.
162#[derive(Debug, Clone, Serialize, ToSchema)]
163pub struct PreflightTagReadResponse {
164    /// Exact derived tag name.
165    pub tag: String,
166    /// Roles this tag serves in the requested loop.
167    pub roles: Vec<String>,
168    /// Value returned by the driver, if any.
169    pub value: Option<String>,
170    /// Driver quality, if any.
171    pub quality: Option<String>,
172    /// Result severity.
173    pub status: PreflightStatus,
174    /// Diagnostic detail.
175    pub detail: String,
176}
177
178impl From<PreflightTagRead> for PreflightTagReadResponse {
179    fn from(read: PreflightTagRead) -> Self {
180        Self {
181            tag: read.tag,
182            roles: read.roles,
183            value: read.value,
184            quality: read.quality,
185            status: read.status.into(),
186            detail: read.detail,
187        }
188    }
189}
190
191/// The summarized pass/warn/fail result returned by the read-only preflight endpoint.
192#[derive(Debug, Clone, Serialize, ToSchema)]
193pub struct PreflightResponse {
194    /// Highest severity in the checks and tag reads.
195    pub status: PreflightStatus,
196    /// Configuration, template, gateway, and loop checks.
197    pub checks: Vec<PreflightCheckResponse>,
198    /// Per-tag values, qualities, and validation results.
199    pub tag_reads: Vec<PreflightTagReadResponse>,
200}
201
202impl From<PreflightReport> for PreflightResponse {
203    fn from(report: PreflightReport) -> Self {
204        let status = report.status().into();
205        Self {
206            status,
207            checks: report.checks.into_iter().map(Into::into).collect(),
208            tag_reads: report.tag_reads.into_iter().map(Into::into).collect(),
209        }
210    }
211}
212
213/// The body of `PUT /api/runs/{id}/notes`.
214#[derive(Debug, Deserialize, ToSchema)]
215pub struct UpdateNotesRequest {
216    /// Replacement note text. Blank or whitespace-only text clears the note.
217    pub notes: String,
218}
219
220/// The body of `POST /api/runs/{id}/write`.
221#[derive(Debug, Deserialize, ToSchema)]
222pub struct WriteRunRequest {
223    /// Which of the run's three calculated candidate result sets to write.
224    pub response_level: ResponseLevel,
225}