Skip to main content

bhtune_runtime/
export.rs

1//! Shared serialization for persisted run samples.
2
3use 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
43/// Serializes a run's recorded samples to CSV or JSON bytes. The CLI and HTTP adapters use
44/// this function so their export formats stay identical.
45pub 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}