1use crate::args::HistoryCommand;
4use crate::output::OutputFormat;
5use bhtune_db::SqlitePool;
6use bhtune_db::models::{
7 MvActuationKind, MvActuationStatus, Pagination, SampleQuality, SamplingAdequacy, TimingSummary,
8 TuneMvActuationRow, TuneResultRow, TuneRunFilter, TuneRunRow, TuneSampleRow, TuneWriteRow,
9 WriteKind,
10};
11use bhtune_runtime::tune::pid_write_preview;
12
13pub async fn run(
14 pool: &SqlitePool,
15 command: HistoryCommand,
16 config: &crate::config::BhtuneConfig,
17) -> anyhow::Result<()> {
18 match command {
19 HistoryCommand::List {
20 outcome,
21 limit,
22 offset,
23 output,
24 } => list(pool, outcome.map(Into::into), limit, offset, output).await,
25 HistoryCommand::Show { run_id, output } => show(pool, run_id, output).await,
26 HistoryCommand::Revert {
27 run_id,
28 bridge_host,
29 server,
30 yes,
31 output,
32 } => revert(pool, run_id, bridge_host, server, yes, output, config).await,
33 HistoryCommand::Prune {
34 older_than_days,
35 dry_run,
36 output,
37 } => prune(pool, config, older_than_days, dry_run, output).await,
38 }
39}
40
41#[derive(serde::Serialize)]
45struct RunSummaryJson {
46 id: i64,
47 tag_name: String,
48 notes: Option<String>,
49 driver: bhtune_db::models::TuneDriver,
50 outcome: bhtune_db::models::TuneOutcome,
51 process_type: bhtune_core::ProcessType,
52 started_at: chrono::DateTime<chrono::Utc>,
53}
54
55#[derive(serde::Serialize)]
56struct RunListJson {
57 runs: Vec<RunSummaryJson>,
58 returned: usize,
61 total: i64,
62}
63
64#[derive(serde::Serialize)]
68struct InitialReadingsJson {
69 pv_ini: f32,
70 mv_ini: f32,
71 mv_range_low: f32,
72 mv_range_high: f32,
73 pv_range_high: f32,
74 pv_range_low: f32,
75 controller_direction: bhtune_core::ControllerDirection,
76 mode_raw: Option<String>,
77 mode_attribute_raw: Option<String>,
78 setpoint_ini: Option<f32>,
79}
80
81impl From<bhtune_db::models::TuneRunInitialReadings> for InitialReadingsJson {
82 fn from(r: bhtune_db::models::TuneRunInitialReadings) -> Self {
83 Self {
84 pv_ini: r.pv_ini,
85 mv_ini: r.mv_ini,
86 mv_range_low: r.mv_range_low,
87 mv_range_high: r.mv_range_high,
88 pv_range_high: r.pv_range_high,
89 pv_range_low: r.pv_range_low,
90 controller_direction: r.controller_direction,
91 mode_raw: r.mode_raw,
92 mode_attribute_raw: r.mode_attribute_raw,
93 setpoint_ini: r.setpoint_ini,
94 }
95 }
96}
97
98#[derive(serde::Serialize)]
102struct ResultJson {
103 response_level: bhtune_core::ResponseLevel,
104 kp: Option<f32>,
105 ti_minutes: Option<f32>,
106 td_minutes: Option<f32>,
107 proportional: Option<f32>,
108 integral: Option<f32>,
109 derivative: Option<f32>,
110 status: bhtune_core::TuningResultStatus,
111 invalid_reason: Option<bhtune_core::TuningResultInvalidReason>,
112 controller_values: Option<bhtune_core::ControllerPidValues>,
113 controller_target_error: Option<String>,
114}
115
116impl ResultJson {
117 fn from_recorded(r: &TuneResultRow, run: &TuneRunRow) -> Self {
118 let preview = pid_write_preview(r, run.config.controller_type, &run.template);
119 Self {
120 response_level: r.response_level,
121 kp: r.kp,
122 ti_minutes: r.ti_minutes,
123 td_minutes: r.td_minutes,
124 proportional: r.proportional,
125 integral: r.integral,
126 derivative: r.derivative,
127 status: r.status,
128 invalid_reason: r.invalid_reason,
129 controller_values: preview.values,
130 controller_target_error: preview.unavailable_reason,
131 }
132 }
133}
134
135#[derive(serde::Serialize)]
139struct WriteJson {
140 kind: WriteKind,
143 response_level: bhtune_core::ResponseLevel,
144 written_at: chrono::DateTime<chrono::Utc>,
145 proportional_previous: Option<f32>,
148 integral_previous: Option<f32>,
149 derivative_previous: Option<f32>,
150 proportional_written: Option<f32>,
153 integral_written: Option<f32>,
154 derivative_written: Option<f32>,
155 proportional_readback: Option<f32>,
156 integral_readback: Option<f32>,
157 derivative_readback: Option<f32>,
158 success: bool,
159 error_message: Option<String>,
160 rollback_state: Option<bhtune_db::models::RollbackState>,
165 rollback_error: Option<String>,
166}
167
168impl From<&TuneWriteRow> for WriteJson {
169 fn from(w: &TuneWriteRow) -> Self {
170 Self {
171 kind: w.kind,
172 response_level: w.response_level,
173 written_at: w.written_at,
174 proportional_previous: w.previous.map(|p| p.proportional),
175 integral_previous: w.previous.map(|p| p.integral),
176 derivative_previous: w.previous.map(|p| p.derivative),
177 proportional_written: w.proportional_written,
178 integral_written: w.integral_written,
179 derivative_written: w.derivative_written,
180 proportional_readback: w.proportional_readback,
181 integral_readback: w.integral_readback,
182 derivative_readback: w.derivative_readback,
183 success: w.success,
184 error_message: w.error_message.clone(),
185 rollback_state: w.rollback_state,
186 rollback_error: w.rollback_error.clone(),
187 }
188 }
189}
190
191#[derive(serde::Serialize)]
192struct MvActuationJson {
193 id: i64,
194 sequence: i64,
195 kind: MvActuationKind,
196 commanded_at: chrono::DateTime<chrono::Utc>,
197 target_mv: f32,
198 previous_commanded_mv: Option<f32>,
199 tolerance: f32,
200 confirmation_due_at: chrono::DateTime<chrono::Utc>,
201 last_checked_at: Option<chrono::DateTime<chrono::Utc>>,
202 readback_mv: Option<f32>,
203 readback_quality: Option<SampleQuality>,
204 attempt_count: i64,
205 status: MvActuationStatus,
206 detail: Option<String>,
207}
208
209impl From<&TuneMvActuationRow> for MvActuationJson {
210 fn from(row: &TuneMvActuationRow) -> Self {
211 Self {
212 id: row.id,
213 sequence: row.sequence,
214 kind: row.kind,
215 commanded_at: row.commanded_at,
216 target_mv: row.target_mv,
217 previous_commanded_mv: row.previous_commanded_mv,
218 tolerance: row.tolerance,
219 confirmation_due_at: row.confirmation_due_at,
220 last_checked_at: row.last_checked_at,
221 readback_mv: row.readback_mv,
222 readback_quality: row.readback_quality,
223 attempt_count: row.attempt_count,
224 status: row.status,
225 detail: row.detail.clone(),
226 }
227 }
228}
229
230#[derive(serde::Serialize)]
231struct RunDetailJson {
232 id: i64,
233 tag_name: String,
234 notes: Option<String>,
235 driver: bhtune_db::models::TuneDriver,
236 outcome: bhtune_db::models::TuneOutcome,
237 failure_reason: Option<String>,
238 started_at: chrono::DateTime<chrono::Utc>,
239 completed_at: Option<chrono::DateTime<chrono::Utc>>,
240 template_name: String,
244 template_origin: bhtune_db::models::TemplateOrigin,
245 config: bhtune_core::LoopConfig,
246 pid_rounding: bhtune_core::PidRounding,
247 opc_server: Option<String>,
251 bridge_host: Option<String>,
253 gateway_compatibility: Option<bhtune_driver::OpcDaGatewayCompatibility>,
255 initial_readings: Option<InitialReadingsJson>,
256 timing_metrics: Option<bhtune_db::models::TimingMetrics>,
257 samples_recorded: usize,
258 results: Vec<ResultJson>,
259 writes: Vec<WriteJson>,
260 mv_actuations: Vec<MvActuationJson>,
261 restore_status: Option<bhtune_db::models::RestoreStatus>,
264 restore_detail: Option<String>,
265}
266
267async fn list(
268 pool: &SqlitePool,
269 outcome: Option<bhtune_db::models::TuneOutcome>,
270 limit: i64,
271 offset: i64,
272 output: OutputFormat,
273) -> anyhow::Result<()> {
274 let mut filter = TuneRunFilter::default();
275 if let Some(outcome) = outcome {
276 filter = filter.with_outcome(outcome);
277 }
278 let pagination = Pagination::new(limit, offset);
279 let runs = TuneRunRow::list(pool, &filter, pagination).await?;
280 let total = TuneRunRow::count(pool, &filter).await?;
281
282 match output {
283 OutputFormat::Table => {
284 if runs.is_empty() {
285 println!("No runs found.");
286 return Ok(());
287 }
288
289 println!(
290 "{:<5} {:<30} {:<10} {:<10} {:<10} {:<25}",
291 "ID", "TAG NAME", "DRIVER", "OUTCOME", "PROCESS", "STARTED"
292 );
293 for run in &runs {
294 println!(
295 "{:<5} {:<30} {:<10} {:<10} {:<10} {:<25}",
296 run.id,
297 run.loop_name,
298 format!("{:?}", run.driver),
299 format!("{:?}", run.outcome),
300 format!("{:?}", run.config.process_type),
301 run.started_at.to_rfc3339(),
302 );
303 }
304 println!("Showing {} of {total} total run(s).", runs.len());
305 }
306 OutputFormat::Json => {
307 let json = RunListJson {
308 returned: runs.len(),
309 total,
310 runs: runs
311 .iter()
312 .map(|run| RunSummaryJson {
313 id: run.id,
314 tag_name: run.loop_name.clone(),
315 notes: run.notes.clone(),
316 driver: run.driver,
317 outcome: run.outcome,
318 process_type: run.config.process_type,
319 started_at: run.started_at,
320 })
321 .collect(),
322 };
323 println!("{}", serde_json::to_string_pretty(&json)?);
324 }
325 }
326 Ok(())
327}
328
329fn has_rows<T>(rows: &[T]) -> bool {
330 !rows.is_empty()
331}
332
333fn has_recorded_connection(opc_server: Option<&str>, bridge_host: Option<&str>) -> bool {
334 opc_server.is_some() || bridge_host.is_some()
335}
336
337fn is_table_output(output: OutputFormat) -> bool {
338 output == OutputFormat::Table
339}
340
341async fn prune(
359 pool: &SqlitePool,
360 config: &crate::config::BhtuneConfig,
361 older_than_days: Option<u32>,
362 dry_run: bool,
363 output: OutputFormat,
364) -> anyhow::Result<()> {
365 let days = older_than_days.or(config.retention_days).ok_or_else(|| {
366 anyhow::anyhow!(
367 "no retention policy configured; pass --older-than-days, or set --retention-days \
368 / BHTUNE_RETENTION_DAYS / the config file's retention_days key"
369 )
370 })?;
371 let now = chrono::Utc::now();
372 let cutoff = crate::retention::cutoff_for(days, now);
373
374 if dry_run {
375 let count = TuneRunRow::count_deletable_matching(
376 pool,
377 &TuneRunFilter::default().with_started_before(cutoff),
378 )
379 .await?;
380 match output {
381 OutputFormat::Table => {
382 println!(
383 "Would delete {count} run(s) started at or before {} ({days} day(s)).",
384 cutoff.to_rfc3339()
385 );
386 }
387 OutputFormat::Json => {
388 let json = PruneJson {
389 retention_days: days,
390 cutoff,
391 dry_run: true,
392 deleted: count as u64,
393 };
394 println!("{}", serde_json::to_string_pretty(&json)?);
395 }
396 }
397 return Ok(());
398 }
399
400 let deleted = crate::retention::sweep_retention(pool, days, now).await?;
401 match output {
402 OutputFormat::Table => {
403 println!(
404 "Deleted {deleted} run(s) started at or before {} ({days} day(s)).",
405 cutoff.to_rfc3339()
406 );
407 }
408 OutputFormat::Json => {
409 let json = PruneJson {
410 retention_days: days,
411 cutoff,
412 dry_run: false,
413 deleted,
414 };
415 println!("{}", serde_json::to_string_pretty(&json)?);
416 }
417 }
418 Ok(())
419}
420
421#[derive(serde::Serialize)]
422struct PruneJson {
423 retention_days: u32,
424 cutoff: chrono::DateTime<chrono::Utc>,
425 dry_run: bool,
426 deleted: u64,
427}
428
429fn fmt_opt_f32(value: Option<f32>) -> String {
432 match value {
433 Some(v) => format!("{v:.4}"),
434 None => "-".to_string(),
435 }
436}
437
438fn fmt_opt_f64(value: Option<f64>) -> String {
439 match value {
440 Some(v) => format!("{v:.3}"),
441 None => "-".to_string(),
442 }
443}
444
445fn sampling_adequacy_label(adequacy: SamplingAdequacy) -> &'static str {
446 match adequacy {
447 SamplingAdequacy::Adequate => "adequate",
448 SamplingAdequacy::Marginal => "marginal",
449 SamplingAdequacy::NotAssessed => "not assessed",
450 }
451}
452
453fn print_latency_summary(label: &str, summary: TimingSummary) {
454 if summary.count == 0 {
455 println!(" {label:<26} none");
456 return;
457 }
458
459 println!(
460 " {label:<26} count={} mean={} ms max={} ms",
461 summary.count,
462 fmt_opt_f64(summary.mean_ms),
463 fmt_opt_f64(summary.max_ms),
464 );
465}
466
467fn print_show_table(
468 run: &TuneRunRow,
469 samples: &[TuneSampleRow],
470 results: &[TuneResultRow],
471 writes: &[TuneWriteRow],
472 mv_actuations: &[TuneMvActuationRow],
473) -> anyhow::Result<()> {
474 let gateway_compatibility = run
475 .gateway_compatibility_json
476 .as_deref()
477 .map(crate::gateway::parse_stored_compatibility)
478 .transpose()?;
479
480 println!("Run #{} — Tag name: {}", run.id, run.loop_name);
481 println!(" Notes: {}", run.notes.as_deref().unwrap_or("—"));
482 println!(" Driver: {:?}", run.driver);
483 println!(" Outcome: {:?}", run.outcome);
484 if let Some(reason) = &run.failure_reason {
485 println!(" Failure reason: {reason}");
486 }
487 println!(" Started at: {}", run.started_at.to_rfc3339());
488 if let Some(completed_at) = run.completed_at {
489 println!(" Completed at: {}", completed_at.to_rfc3339());
490 }
491 println!(
492 " Template: {} ({:?})",
493 run.template.name, run.template_origin
494 );
495 if has_recorded_connection(run.opc_server.as_deref(), run.bridge_host.as_deref()) {
496 println!(
497 " Connection: server={} bridge_host={}",
498 run.opc_server.as_deref().unwrap_or("-"),
499 run.bridge_host.as_deref().unwrap_or("-"),
500 );
501 }
502 if let Some(snapshot) = gateway_compatibility {
503 println!(
504 " Gateway compatibility: {} ({})",
505 snapshot.status.as_str(),
506 snapshot
507 .gateway_version
508 .as_deref()
509 .unwrap_or("version not reported"),
510 );
511 }
512 println!(
513 " Process/controller: {:?} / {:?}",
514 run.config.process_type, run.config.controller_type
515 );
516 println!(" Relay amplitude: {}%", run.config.relay_amp_percent);
517 println!(
518 " Cycles skip/count: {} / {}",
519 run.config.num_cycles_skip, run.config.num_cycles_count
520 );
521
522 if let Some(readings) = &run.initial_readings {
523 println!(
524 " Initial PV / MV: {} / {}",
525 readings.pv_ini, readings.mv_ini
526 );
527 println!(
528 " MV range: {} - {}",
529 readings.mv_range_low, readings.mv_range_high
530 );
531 println!(
532 " PV range: {} - {}",
533 readings.pv_range_low, readings.pv_range_high
534 );
535 println!(" Direction: {:?}", readings.controller_direction);
536 }
537
538 println!(" Samples recorded: {}", samples.len());
539
540 if let Some(timing) = run.timing_metrics {
541 println!(" Timing:");
542 println!(" Basis: {:?}", timing.basis);
543 println!(
544 " Requested interval: {} ms",
545 timing.requested_interval_ms
546 );
547 println!(" Observed sample gaps: {}", timing.sample_gap_count);
548 println!(
549 " Mean / max sample gap: {} / {} ms",
550 fmt_opt_f64(timing.mean_sample_gap_ms),
551 fmt_opt_f64(timing.max_sample_gap_ms)
552 );
553 println!(
554 " Missed poll opportunities: {}",
555 timing.missed_poll_opportunity_count
556 );
557 println!(
558 " Oscillation period: {} ms",
559 fmt_opt_f64(timing.measured_oscillation_period_ms)
560 );
561 println!(
562 " Approx. samples / period: {}",
563 fmt_opt_f64(timing.approximate_samples_per_period)
564 );
565 println!(
566 " Sampling adequacy: {}",
567 sampling_adequacy_label(timing.sampling_adequacy)
568 );
569 if let Some(poll_latency) = timing.poll_latency {
570 println!(" Poll latency:");
571 print_latency_summary("PV reads:", poll_latency.pv_read);
572 print_latency_summary("MV writes:", poll_latency.mv_write);
573 print_latency_summary("MV verification:", poll_latency.mv_verification);
574 print_latency_summary("Sample persistence:", poll_latency.sample_persist);
575 print_latency_summary("Total tick work:", poll_latency.tick_work);
576 }
577 }
578
579 match (run.restore_status, &run.restore_detail) {
580 (Some(bhtune_db::models::RestoreStatus::Confirmed), _) => {
581 println!(" Restore: confirmed");
582 }
583 (Some(bhtune_db::models::RestoreStatus::Incomplete), detail) => {
584 println!(
585 " Restore: INCOMPLETE -- {}",
586 detail.as_deref().unwrap_or("no detail recorded")
587 );
588 }
589 (None, _) => {}
590 }
591
592 print_show_results(run, results);
593 print_show_writes(writes);
594 print_show_mv_actuations(mv_actuations);
595 Ok(())
596}
597
598fn print_show_results(run: &TuneRunRow, results: &[TuneResultRow]) {
599 if !has_rows(results) {
600 return;
601 }
602 println!(" Calculated results:");
603 println!(
604 " PROP / INTEGRAL / DERIV are controller-ready targets; KP / TI / TD are raw calculations."
605 );
606 println!(
607 " {:<12} {:<10} {:<10} {:<10} {:<10} {:<12} {:<10} {:<10} REASON",
608 "LEVEL", "STATUS", "KP", "TI(min)", "TD(min)", "PROP", "INTEGRAL", "DERIV"
609 );
610 for result in results {
611 let preview = pid_write_preview(result, run.config.controller_type, &run.template);
612 let values = preview.values.as_ref();
613 println!(
614 " {:<12} {:<10} {:<10} {:<10} {:<10} {:<12} {:<10} {:<10} {}",
615 format!("{:?}", result.response_level),
616 format!("{:?}", result.status),
617 fmt_opt_f32(result.kp),
618 fmt_opt_f32(result.ti_minutes),
619 fmt_opt_f32(result.td_minutes),
620 values
621 .map(|values| values.proportional.display.as_str())
622 .unwrap_or("-"),
623 values
624 .map(|values| values.integral.display.as_str())
625 .unwrap_or("-"),
626 values
627 .map(|values| values.derivative.display.as_str())
628 .unwrap_or("-"),
629 preview.unavailable_reason.as_deref().unwrap_or("-"),
630 );
631 }
632}
633
634fn print_show_writes(writes: &[TuneWriteRow]) {
635 if !has_rows(writes) {
636 return;
637 }
638 println!(" PID write-back audit:");
639 for write in writes {
640 println!(
641 " [{}] {:?} ({:?} level): success={} previous(P={} I={} D={}) \
642 written(P={} I={} D={}) readback(P={} I={} D={}){}",
643 write.written_at.to_rfc3339(),
644 write.kind,
645 write.response_level,
646 write.success,
647 fmt_pid_recorded(write.previous.map(|p| p.proportional)),
648 fmt_pid_recorded(write.previous.map(|p| p.integral)),
649 fmt_pid_recorded(write.previous.map(|p| p.derivative)),
650 fmt_pid_recorded(write.proportional_written),
651 fmt_pid_recorded(write.integral_written),
652 fmt_pid_recorded(write.derivative_written),
653 fmt_pid_recorded(write.proportional_readback),
654 fmt_pid_recorded(write.integral_readback),
655 fmt_pid_recorded(write.derivative_readback),
656 write
657 .error_message
658 .as_ref()
659 .map(|message| format!(" error={message}"))
660 .unwrap_or_default(),
661 );
662 print_show_rollback(write);
663 }
664}
665
666fn fmt_pid_recorded(value: Option<f32>) -> String {
667 value
668 .map(|value| value.to_string())
669 .unwrap_or_else(|| "-".to_string())
670}
671
672fn print_show_rollback(write: &TuneWriteRow) {
673 let Some(rollback_state) = write.rollback_state else {
674 return;
675 };
676 println!(
677 " rollback: {rollback_state:?}{}",
678 write
679 .rollback_error
680 .as_ref()
681 .map(|message| format!(" error={message}"))
682 .unwrap_or_default(),
683 );
684}
685
686fn print_show_mv_actuations(actuations: &[TuneMvActuationRow]) {
687 if !has_rows(actuations) {
688 return;
689 }
690 println!(" MV actuation verification:");
691 println!(
692 " {:<5} {:<8} {:<25} {:<10} {:<10} {:<10} {:<10} {:<12} DETAIL",
693 "SEQ", "KIND", "COMMANDED", "TARGET", "READBACK", "TOLERANCE", "ATTEMPTS", "STATUS"
694 );
695 for actuation in actuations {
696 println!(
697 " {:<5} {:<8} {:<25} {:<10} {:<10} {:<10} {:<10} {:<12} {}",
698 actuation.sequence,
699 format!("{:?}", actuation.kind),
700 actuation.commanded_at.to_rfc3339(),
701 fmt_opt_f32(Some(actuation.target_mv)),
702 fmt_opt_f32(actuation.readback_mv),
703 fmt_opt_f32(Some(actuation.tolerance)),
704 actuation.attempt_count,
705 format!("{:?}", actuation.status),
706 actuation.detail.as_deref().unwrap_or("-"),
707 );
708 println!(
709 " confirmation due: {} last checked: {} quality: {}",
710 actuation.confirmation_due_at.to_rfc3339(),
711 actuation
712 .last_checked_at
713 .map(|value| value.to_rfc3339())
714 .unwrap_or_else(|| "-".to_string()),
715 actuation
716 .readback_quality
717 .map(|quality| format!("{quality:?}"))
718 .unwrap_or_else(|| "-".to_string()),
719 );
720 }
721}
722
723fn print_show_json(
724 run: &TuneRunRow,
725 samples: &[TuneSampleRow],
726 results: &[TuneResultRow],
727 writes: &[TuneWriteRow],
728 mv_actuations: &[TuneMvActuationRow],
729) -> anyhow::Result<()> {
730 let json = RunDetailJson {
731 id: run.id,
732 tag_name: run.loop_name.clone(),
733 notes: run.notes.clone(),
734 driver: run.driver,
735 outcome: run.outcome,
736 failure_reason: run.failure_reason.clone(),
737 started_at: run.started_at,
738 completed_at: run.completed_at,
739 template_name: run.template.name.clone(),
740 template_origin: run.template_origin,
741 config: run.config,
742 pid_rounding: run.template.pid_rounding,
743 opc_server: run.opc_server.clone(),
744 bridge_host: run.bridge_host.clone(),
745 gateway_compatibility: run
746 .gateway_compatibility_json
747 .as_deref()
748 .map(crate::gateway::parse_stored_compatibility)
749 .transpose()?,
750 initial_readings: run
751 .initial_readings
752 .as_ref()
753 .map(|readings| InitialReadingsJson::from(readings.clone())),
754 timing_metrics: run.timing_metrics,
755 samples_recorded: samples.len(),
756 results: results
757 .iter()
758 .map(|result| ResultJson::from_recorded(result, run))
759 .collect(),
760 writes: writes.iter().map(WriteJson::from).collect(),
761 mv_actuations: mv_actuations.iter().map(MvActuationJson::from).collect(),
762 restore_status: run.restore_status,
763 restore_detail: run.restore_detail.clone(),
764 };
765 println!("{}", serde_json::to_string_pretty(&json)?);
766 Ok(())
767}
768
769async fn show(pool: &SqlitePool, run_id: i64, output: OutputFormat) -> anyhow::Result<()> {
770 let run = TuneRunRow::get(pool, run_id)
771 .await?
772 .ok_or_else(|| anyhow::anyhow!("no run with id {run_id}"))?;
773 let samples = TuneSampleRow::list_for_run(pool, run_id).await?;
774 let results = TuneResultRow::list_for_run(pool, run_id).await?;
775 let writes = TuneWriteRow::list_for_run(pool, run_id).await?;
776 let mv_actuations = TuneMvActuationRow::list_for_run(pool, run_id).await?;
777
778 match output {
779 OutputFormat::Table => print_show_table(&run, &samples, &results, &writes, &mv_actuations)?,
780 OutputFormat::Json => print_show_json(&run, &samples, &results, &writes, &mv_actuations)?,
781 }
782
783 Ok(())
784}
785
786#[derive(serde::Serialize)]
790struct RevertJson {
791 run_id: i64,
792 response_level: bhtune_core::ResponseLevel,
795 reverted_to: RevertedTargetJson,
796 success: bool,
797 error_message: Option<String>,
798}
799
800#[derive(serde::Serialize)]
801struct RevertedTargetJson {
802 proportional: f32,
803 integral: f32,
804 derivative: f32,
805}
806
807#[cfg(test)]
817fn resolve_revert_connection(
818 run: &TuneRunRow,
819 bridge_host_flag: Option<&str>,
820 server_flag: Option<&str>,
821) -> anyhow::Result<(String, String)> {
822 bhtune_runtime::history::resolve_revert_connection(run, bridge_host_flag, server_flag)
823}
824
825#[allow(clippy::too_many_arguments)]
844async fn revert(
845 pool: &SqlitePool,
846 run_id: i64,
847 bridge_host: Option<String>,
848 server: Option<String>,
849 yes: bool,
850 output: OutputFormat,
851 config: &crate::config::BhtuneConfig,
852) -> anyhow::Result<()> {
853 let result = bhtune_runtime::history::revert_run_with_progress(
854 pool,
855 bhtune_runtime::history::RevertRequest {
856 run_id,
857 bridge_host,
858 server,
859 confirmed: yes,
860 },
861 config.allow_uncertain_quality,
862 |progress| {
863 if is_table_output(output) {
864 println!(
865 "Reverting run {}'s {:?} PID write-back on tag '{}' to \
866 P={} I={} D={}...",
867 progress.run_id,
868 progress.response_level,
869 progress.loop_name,
870 progress.target.proportional,
871 progress.target.integral,
872 progress.target.derivative
873 );
874 }
875 },
876 )
877 .await?;
878 let response_level = result.response_level;
879 let target = result.target;
880
881 match result.outcome {
882 bhtune_runtime::tune::PidWriteOutcome::Written => {
883 tracing::info!(run_id, ?response_level, "PID revert succeeded");
884 match output {
885 OutputFormat::Table => {
886 println!(
887 "Reverted and confirmed run {run_id}'s {response_level:?} PID write-back."
888 );
889 }
890 OutputFormat::Json => {
891 let json = RevertJson {
892 run_id,
893 response_level,
894 reverted_to: RevertedTargetJson {
895 proportional: target.proportional,
896 integral: target.integral,
897 derivative: target.derivative,
898 },
899 success: true,
900 error_message: None,
901 };
902 println!("{}", serde_json::to_string_pretty(&json)?);
903 }
904 }
905 Ok(())
906 }
907 bhtune_runtime::tune::PidWriteOutcome::Failed { detail } => {
908 tracing::error!(run_id, ?response_level, error_message = %detail, "PID revert failed partway through; the loop may hold a mismatched set of PID constants -- see `history show` for the recorded partial state");
909 anyhow::bail!(
910 "revert failed partway through: {detail} (the loop may now hold a mismatched \
911 set of PID constants -- see `history show {run_id}` for the recorded partial \
912 state)"
913 );
914 }
915 }
916}
917
918#[cfg(test)]
919mod tests;