1use bhtune_db::models::{SampleQuality, TuneSampleRow};
4use serde::Serialize;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum SampleExportFormat {
8 Csv,
9 Json,
10}
11
12#[derive(Serialize)]
13pub struct SampleRecord {
14 tick: i64,
15 time: chrono::DateTime<chrono::Utc>,
16 pv: f32,
17 pv_quality: SampleQuality,
18 hysteresis: f32,
19 mv_value_current: f32,
20 mv_sign_next_step: i8,
21 counter_all_switches: u32,
22 cycles_completed: i32,
23 cycles_remaining: i32,
24}
25
26impl From<&TuneSampleRow> for SampleRecord {
27 fn from(row: &TuneSampleRow) -> Self {
28 Self {
29 tick: row.tick_index,
30 time: row.sample.time,
31 pv: row.sample.pv,
32 pv_quality: row.pv_quality,
33 hysteresis: row.state.hysteresis,
34 mv_value_current: row.state.mv_value_current,
35 mv_sign_next_step: row.state.mv_sign_next_step,
36 counter_all_switches: row.state.counter_all_switches,
37 cycles_completed: row.state.cycles_completed,
38 cycles_remaining: row.state.cycles_remaining,
39 }
40 }
41}
42
43pub fn samples_to_bytes(
46 samples: &[TuneSampleRow],
47 format: SampleExportFormat,
48) -> anyhow::Result<Vec<u8>> {
49 let records: Vec<SampleRecord> = samples.iter().map(SampleRecord::from).collect();
50 match format {
51 SampleExportFormat::Csv => {
52 let mut writer = csv::Writer::from_writer(Vec::new());
53 for record in &records {
54 writer.serialize(record)?;
55 }
56 Ok(writer.into_inner()?)
57 }
58 SampleExportFormat::Json => Ok(serde_json::to_vec_pretty(&records)?),
59 }
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65 use bhtune_core::mrft::{MrftState, Tick};
66
67 async fn pool_with_one_sample() -> (bhtune_db::SqlitePool, i64) {
68 let pool = bhtune_db::connect_in_memory().await.unwrap();
69 let now = chrono::Utc::now();
70 let template = bhtune_core::built_in_templates().remove(0);
71 let tags = bhtune_core::LoopTags::derive_from_pv_tag("Unit1.LIC101.PV", &template);
72 let run = bhtune_db::models::TuneRunRow::start(
73 &pool,
74 None,
75 "Unit1.LIC101.PV",
76 bhtune_db::models::TuneDriver::Simulator,
77 bhtune_core::LoopConfig {
78 process_type: bhtune_core::ProcessType::Flow,
79 controller_type: bhtune_core::ControllerType::Pi,
80 relay_amp_percent: 10.0,
81 num_cycles_skip: 1,
82 num_cycles_count: 2,
83 noise_protection_secs: 3,
84 mrft_delay_secs: 0,
85 },
86 bhtune_db::models::TemplateOrigin::Builtin,
87 &template,
88 &tags,
89 now,
90 )
91 .await
92 .unwrap();
93
94 TuneSampleRow::insert(
95 &pool,
96 run.id,
97 0,
98 Tick {
99 time: now,
100 pv: 50.0,
101 },
102 MrftState {
103 hysteresis: 0.0,
104 mv_value_current: 50.0,
105 mv_sign_next_step: 1,
106 counter_all_switches: 0,
107 cycles_completed: 0,
108 cycles_remaining: 2,
109 },
110 SampleQuality::Good,
111 )
112 .await
113 .unwrap();
114
115 (pool, run.id)
116 }
117
118 #[tokio::test]
119 async fn serializes_samples_to_csv_with_ordered_columns() {
120 let (pool, run_id) = pool_with_one_sample().await;
121 let samples = TuneSampleRow::list_for_run(&pool, run_id).await.unwrap();
122 let bytes = samples_to_bytes(&samples, SampleExportFormat::Csv).unwrap();
123 let text = String::from_utf8(bytes).unwrap();
124 let mut lines = text.lines();
125 assert_eq!(
126 lines.next().unwrap(),
127 "tick,time,pv,pv_quality,hysteresis,mv_value_current,mv_sign_next_step,counter_all_switches,cycles_completed,cycles_remaining"
128 );
129 assert!(lines.next().unwrap().starts_with("0,"));
130 }
131
132 #[tokio::test]
133 async fn serializes_samples_to_json_with_the_cli_export_shape() {
134 let (pool, run_id) = pool_with_one_sample().await;
135 let samples = TuneSampleRow::list_for_run(&pool, run_id).await.unwrap();
136 let bytes = samples_to_bytes(&samples, SampleExportFormat::Json).unwrap();
137 let parsed: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
138 assert_eq!(parsed[0]["pv"], 50.0);
139 assert_eq!(parsed[0]["tick"], 0);
140 }
141}