Skip to main content

oxihuman_export/
biometric_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5/// A single biometric data sample.
6pub struct BiometricSample {
7    pub time_s: f32,
8    pub heart_rate_bpm: f32,
9    pub spo2_percent: f32,
10    pub skin_temp_c: f32,
11    pub respiratory_rate: f32,
12}
13
14pub fn new_biometric_sample(t: f32, hr: f32, spo2: f32) -> BiometricSample {
15    BiometricSample {
16        time_s: t,
17        heart_rate_bpm: hr,
18        spo2_percent: spo2,
19        skin_temp_c: 36.5,
20        respiratory_rate: 15.0,
21    }
22}
23
24pub fn biometric_to_csv_line(s: &BiometricSample) -> String {
25    format!(
26        "{:.4},{:.2},{:.2},{:.2},{:.2}",
27        s.time_s, s.heart_rate_bpm, s.spo2_percent, s.skin_temp_c, s.respiratory_rate
28    )
29}
30
31pub fn biometric_sequence_to_csv(samples: &[BiometricSample]) -> String {
32    let header = "time_s,heart_rate_bpm,spo2_percent,skin_temp_c,respiratory_rate\n";
33    let rows: Vec<String> = samples.iter().map(biometric_to_csv_line).collect();
34    format!("{}{}", header, rows.join("\n"))
35}
36
37pub fn biometric_average_hr(samples: &[BiometricSample]) -> f32 {
38    if samples.is_empty() {
39        return 0.0;
40    }
41    samples.iter().map(|s| s.heart_rate_bpm).sum::<f32>() / samples.len() as f32
42}
43
44pub fn biometric_min_spo2(samples: &[BiometricSample]) -> f32 {
45    samples
46        .iter()
47        .map(|s| s.spo2_percent)
48        .fold(f32::INFINITY, f32::min)
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54
55    #[test]
56    fn test_new_biometric_sample() {
57        let s = new_biometric_sample(0.0, 72.0, 98.5);
58        assert!((s.heart_rate_bpm - 72.0).abs() < 1e-5);
59        assert!((s.spo2_percent - 98.5).abs() < 1e-5);
60    }
61
62    #[test]
63    fn test_biometric_to_csv_line_fields() {
64        let s = new_biometric_sample(1.0, 80.0, 97.0);
65        let line = biometric_to_csv_line(&s);
66        assert!(line.contains("1.0000"));
67        assert!(line.contains("80.00"));
68    }
69
70    #[test]
71    fn test_biometric_sequence_to_csv_header() {
72        let csv = biometric_sequence_to_csv(&[]);
73        assert!(csv.starts_with("time_s"));
74    }
75
76    #[test]
77    fn test_biometric_average_hr() {
78        let samples = vec![
79            new_biometric_sample(0.0, 60.0, 98.0),
80            new_biometric_sample(1.0, 80.0, 98.0),
81        ];
82        assert!((biometric_average_hr(&samples) - 70.0).abs() < 1e-4);
83    }
84
85    #[test]
86    fn test_biometric_min_spo2() {
87        let samples = vec![
88            new_biometric_sample(0.0, 70.0, 99.0),
89            new_biometric_sample(1.0, 70.0, 95.0),
90            new_biometric_sample(2.0, 70.0, 97.0),
91        ];
92        assert!((biometric_min_spo2(&samples) - 95.0).abs() < 1e-4);
93    }
94
95    #[test]
96    fn test_biometric_average_hr_empty() {
97        assert!((biometric_average_hr(&[]) - 0.0).abs() < 1e-6);
98    }
99
100    #[test]
101    fn test_biometric_sequence_csv_row_count() {
102        let samples = vec![
103            new_biometric_sample(0.0, 70.0, 98.0),
104            new_biometric_sample(1.0, 75.0, 99.0),
105        ];
106        let csv = biometric_sequence_to_csv(&samples);
107        let lines: Vec<&str> = csv.lines().collect();
108        assert_eq!(lines.len(), 3); /* header + 2 rows */
109    }
110}