1use bhtune_core::{
2 ResponseLevel,
3 tuning_math::{
4 CheckedTuningResult, PidParameters, TuningResult, TuningResultInvalidReason,
5 TuningResultStatus,
6 },
7};
8use sqlx::{Row, SqlitePool, sqlite::SqliteRow};
9
10use crate::{
11 convert::{enum_to_text, option_enum_text, text_to_enum},
12 error::{DbError, DbResult},
13};
14
15#[derive(Debug, Clone, Copy, PartialEq)]
24pub struct TuneResultRow {
25 pub id: i64,
26 pub run_id: i64,
27 pub response_level: ResponseLevel,
28 pub kp: Option<f32>,
29 pub ti_minutes: Option<f32>,
30 pub td_minutes: Option<f32>,
31 pub proportional: Option<f32>,
32 pub integral: Option<f32>,
33 pub derivative: Option<f32>,
34 pub status: TuningResultStatus,
35 pub invalid_reason: Option<TuningResultInvalidReason>,
36}
37
38impl TuneResultRow {
39 pub fn from_calculated(run_id: i64, tuning: TuningResult, pid: PidParameters) -> TuneResultRow {
50 assert_eq!(
51 tuning.response_level, pid.response_level,
52 "TuningResult and PidParameters must be for the same ResponseLevel"
53 );
54 TuneResultRow {
55 id: 0,
56 run_id,
57 response_level: tuning.response_level,
58 kp: Some(tuning.kp),
59 ti_minutes: Some(tuning.ti_minutes),
60 td_minutes: Some(tuning.td_minutes),
61 proportional: Some(pid.proportional),
62 integral: Some(pid.integral),
63 derivative: Some(pid.derivative),
64 status: TuningResultStatus::Valid,
65 invalid_reason: None,
66 }
67 }
68
69 pub fn from_checked(run_id: i64, checked: CheckedTuningResult) -> TuneResultRow {
73 let values = checked.usable_values();
74 TuneResultRow {
75 id: 0,
76 run_id,
77 response_level: checked.response_level,
78 kp: values.map(|(tuning, _)| tuning.kp),
79 ti_minutes: values.map(|(tuning, _)| tuning.ti_minutes),
80 td_minutes: values.map(|(tuning, _)| tuning.td_minutes),
81 proportional: values.map(|(_, pid)| pid.proportional),
82 integral: values.map(|(_, pid)| pid.integral),
83 derivative: values.map(|(_, pid)| pid.derivative),
84 status: checked.status,
85 invalid_reason: checked.invalid_reason,
86 }
87 }
88
89 pub async fn insert(pool: &SqlitePool, row: &TuneResultRow) -> DbResult<TuneResultRow> {
94 let inserted = sqlx::query(
95 r"
96 INSERT INTO tune_results (
97 run_id, response_level, kp, ti_minutes, td_minutes,
98 proportional, integral, derivative, status, invalid_reason
99 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
100 RETURNING *
101 ",
102 )
103 .bind(row.run_id)
104 .bind(enum_to_text(&row.response_level)?)
105 .bind(row.kp)
106 .bind(row.ti_minutes)
107 .bind(row.td_minutes)
108 .bind(row.proportional)
109 .bind(row.integral)
110 .bind(row.derivative)
111 .bind(enum_to_text(&row.status)?)
112 .bind(option_enum_text(row.invalid_reason.as_ref())?)
113 .fetch_one(pool)
114 .await
115 .map_err(DbError::Query)?;
116
117 row_to_tune_result(inserted)
118 }
119
120 pub async fn list_for_run(pool: &SqlitePool, run_id: i64) -> DbResult<Vec<TuneResultRow>> {
125 let rows =
126 sqlx::query("SELECT * FROM tune_results WHERE run_id = ? ORDER BY response_level")
127 .bind(run_id)
128 .fetch_all(pool)
129 .await
130 .map_err(DbError::Query)?;
131 rows.into_iter().map(row_to_tune_result).collect()
132 }
133}
134
135fn row_to_tune_result(row: SqliteRow) -> DbResult<TuneResultRow> {
136 let response_level: String = row.try_get("response_level").map_err(DbError::Query)?;
137 Ok(TuneResultRow {
138 id: row.try_get("id").map_err(DbError::Query)?,
139 run_id: row.try_get("run_id").map_err(DbError::Query)?,
140 response_level: text_to_enum("response_level", &response_level)?,
141 kp: row.try_get("kp").map_err(DbError::Query)?,
142 ti_minutes: row.try_get("ti_minutes").map_err(DbError::Query)?,
143 td_minutes: row.try_get("td_minutes").map_err(DbError::Query)?,
144 proportional: row.try_get("proportional").map_err(DbError::Query)?,
145 integral: row.try_get("integral").map_err(DbError::Query)?,
146 derivative: row.try_get("derivative").map_err(DbError::Query)?,
147 status: {
148 let status: String = row.try_get("status").map_err(DbError::Query)?;
149 text_to_enum("status", &status)?
150 },
151 invalid_reason: {
152 let reason: Option<String> = row.try_get("invalid_reason").map_err(DbError::Query)?;
153 reason
154 .as_deref()
155 .map(|reason| text_to_enum("invalid_reason", reason))
156 .transpose()?
157 },
158 })
159}
160#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn from_calculated_builds_matching_row() {
168 let tuning = TuningResult {
169 response_level: ResponseLevel::Moderate,
170 kp: 1.0,
171 ti_minutes: 2.0,
172 td_minutes: 0.0,
173 };
174 let pid = PidParameters {
175 response_level: ResponseLevel::Moderate,
176 proportional: 3.0,
177 integral: 4.0,
178 derivative: 0.0,
179 };
180 let row = TuneResultRow::from_calculated(42, tuning, pid);
181 assert_eq!(row.run_id, 42);
182 assert_eq!(row.response_level, ResponseLevel::Moderate);
183 assert_eq!(row.kp, Some(1.0));
184 assert_eq!(row.proportional, Some(3.0));
185 }
186
187 #[test]
188 #[should_panic(expected = "same ResponseLevel")]
189 fn from_calculated_panics_on_mismatched_response_level() {
190 let tuning = TuningResult {
191 response_level: ResponseLevel::Aggressive,
192 kp: 1.0,
193 ti_minutes: 2.0,
194 td_minutes: 0.0,
195 };
196 let pid = PidParameters {
197 response_level: ResponseLevel::Sluggish,
198 proportional: 3.0,
199 integral: 4.0,
200 derivative: 0.0,
201 };
202 TuneResultRow::from_calculated(1, tuning, pid);
203 }
204}