Skip to main content

bhtune_runtime/tune/
mod.rs

1//! `bhtune tune`/`bhtune simulate`: runs a full MRFT test end-to-end — resolving a template,
2//! deriving tags, transitioning the loop to Manual, polling the driver and driving a real
3//! [`bhtune_core::MrftEngine`], persisting every tick and the final calculated results, then
4//! restoring the loop and optionally writing back the chosen PID constants.
5//!
6//! The separate `bhtune check` preflight validates the same request inputs and reads
7//! controller state without preparing or persisting a run. It uses a read-only database
8//! connection and `ReadOnlyDriver` so it cannot mutate the database or controller.
9//!
10//! Mirrors the legacy `MRFTstart`/`ReadInitialOPCvalues`/`ChangeControllerModeToMan`/
11//! `ResetOPC` sequence from `OPCClass.cs`. The mode-transition and write-back steps
12//! automatically no-op for the simulator driver, since its [`bhtune_core::LoopTags`] has no
13//! setpoint/mode/mode-attribute/PID-constant tags at all (see `build_loop_tags` below) — no
14//! separate "is this the simulator?" branching is needed in that logic.
15
16#![allow(rustdoc::broken_intra_doc_links)]
17
18mod actuation;
19mod config;
20mod outcome;
21mod poll;
22mod preflight;
23mod prepare;
24mod quality;
25mod recovery;
26mod request;
27mod restore;
28mod timing;
29mod writeback;
30
31#[cfg(test)]
32use crate::cancel::CtrlC;
33
34pub use actuation::MV_ACTUATION_CONFIRMATION_SECS;
35pub use config::validate_restore_timeout_secs;
36pub use outcome::{
37    AbortReason, RunOutcome, TuneOutcome, TuneRunReport, WriteBackOutcome, tune_outcome_for_run,
38};
39pub use preflight::{
40    PreflightCheck, PreflightCheckStatus, PreflightReport, PreflightTagRead, preflight,
41};
42pub use prepare::{PreparedTune, drive, drive_report, prepare, prepare_owned};
43pub use quality::sample_quality_from_driver;
44pub use recovery::{
45    RecoveryStepReport, RecoveryStepStatus, RestoreLoopReport, RestoreLoopRequest,
46    StartupOrphanSweepReport, recover_startup_orphans, restore_loop,
47};
48pub use request::{
49    DEFAULT_SIM_DEAD_TIME, DEFAULT_SIM_GAIN, DEFAULT_SIM_INITIAL_VALUE, DEFAULT_SIM_NOISE,
50    DEFAULT_SIM_SEED, DEFAULT_SIM_TAU, DriverKind, TuneRequest, TuneRequestValidationError,
51    UnsupportedDriverKind, ValidatedTuneRequest, validate_finite_f32, validate_positive_u32,
52};
53pub use writeback::{
54    PidWriteOutcome, PidWritePreview, WriteBackHandler, WriteBackSelection, WriteBackSkipReason,
55    controller_pid_for_result, pid_parameters_for_result, pid_write_preview, write_pid_values,
56    write_pid_values_with_owner,
57};
58#[allow(unused_imports)]
59pub(crate) use writeback::{read_previous_pid_values, write_and_verify_pid_value};
60
61/// Turns a missing value that the caller already checked into a recoverable error.
62///
63/// A panic after the loop may already be in manual would skip restore. The message names
64/// the invariant that failed so the error is actionable in logs.
65trait RequireInvariant<T> {
66    fn require_invariant(self, message: &'static str) -> anyhow::Result<T>;
67}
68impl<T> RequireInvariant<T> for Option<T> {
69    fn require_invariant(self, message: &'static str) -> anyhow::Result<T> {
70        let Some(value) = self else {
71            return Err(anyhow::anyhow!(message));
72        };
73        Ok(value)
74    }
75}
76#[cfg(test)]
77async fn run(
78    pool: &bhtune_db::SqlitePool,
79    request: TuneRequest,
80    config: &crate::config::BhtuneConfig,
81) -> anyhow::Result<TuneOutcome> {
82    run_with_ctrl_c(pool, request, config, &mut CtrlC::never()).await
83}
84
85#[cfg(test)]
86async fn run_with_ctrl_c(
87    pool: &bhtune_db::SqlitePool,
88    request: TuneRequest,
89    config: &crate::config::BhtuneConfig,
90    ctrl_c: &mut CtrlC,
91) -> anyhow::Result<TuneOutcome> {
92    let prepared = prepare(pool, request, config).await?;
93    let report = drive_report(pool, prepared, ctrl_c, None).await?;
94    Ok(tune_outcome_for_run(&report.outcome))
95}
96
97#[cfg(test)]
98#[test]
99fn mv_actuation_abort_format_falls_back_for_other_abort_reasons() {
100    assert_eq!(
101        outcome::format_mv_actuation_abort_reason(&AbortReason::UserInterrupt),
102        "UserInterrupt"
103    );
104}
105
106#[test]
107fn require_invariant_returns_the_value_or_an_error() {
108    assert_eq!(Some(3).require_invariant("missing").unwrap(), 3);
109    let err = None::<i32>
110        .require_invariant("missing invariant")
111        .unwrap_err();
112    assert!(err.to_string().contains("missing invariant"));
113}
114
115#[cfg(test)]
116mod tests;