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}