oxihuman_export/
telemetry_export.rs1#![allow(dead_code)]
4
5#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub struct TelemetryChannel {
11 pub id: u32,
12 pub name: String,
13 pub unit: String,
14 pub data_type: String,
15}
16
17#[allow(dead_code)]
19#[derive(Debug, Clone)]
20pub struct TelemetryFrame {
21 pub timestamp_us: u64,
22 pub values: Vec<f64>,
23}
24
25#[allow(dead_code)]
27pub struct TelemetryExport {
28 pub session_id: String,
29 pub channels: Vec<TelemetryChannel>,
30 pub frames: Vec<TelemetryFrame>,
31}
32
33impl TelemetryExport {
34 #[allow(dead_code)]
35 pub fn new(session_id: &str) -> Self {
36 Self {
37 session_id: session_id.to_string(),
38 channels: Vec::new(),
39 frames: Vec::new(),
40 }
41 }
42}
43
44#[allow(dead_code)]
46pub fn add_telemetry_channel(
47 export: &mut TelemetryExport,
48 name: &str,
49 unit: &str,
50 data_type: &str,
51) -> u32 {
52 let id = export.channels.len() as u32;
53 export.channels.push(TelemetryChannel {
54 id,
55 name: name.to_string(),
56 unit: unit.to_string(),
57 data_type: data_type.to_string(),
58 });
59 id
60}
61
62#[allow(dead_code)]
64pub fn record_frame(export: &mut TelemetryExport, timestamp_us: u64, values: Vec<f64>) {
65 export.frames.push(TelemetryFrame {
66 timestamp_us,
67 values,
68 });
69}
70
71#[allow(dead_code)]
73pub fn export_telemetry_csv(export: &TelemetryExport) -> String {
74 let mut out = String::new();
75 out.push_str("timestamp_us");
76 for ch in &export.channels {
77 out.push_str(&format!(",{}", ch.name));
78 }
79 out.push('\n');
80 for frame in &export.frames {
81 out.push_str(&frame.timestamp_us.to_string());
82 for v in &frame.values {
83 out.push_str(&format!(",{v}"));
84 }
85 out.push('\n');
86 }
87 out
88}
89
90#[allow(dead_code)]
92pub fn export_telemetry_meta(export: &TelemetryExport) -> String {
93 let mut out = format!("{{\"session_id\":\"{}\",\"channels\":[", export.session_id);
94 for (i, ch) in export.channels.iter().enumerate() {
95 if i > 0 {
96 out.push(',');
97 }
98 out.push_str(&format!(
99 "{{\"id\":{},\"name\":\"{}\",\"unit\":\"{}\",\"type\":\"{}\"}}",
100 ch.id, ch.name, ch.unit, ch.data_type
101 ));
102 }
103 out.push_str("]}");
104 out
105}
106
107#[allow(dead_code)]
109pub fn frame_count_tl(export: &TelemetryExport) -> usize {
110 export.frames.len()
111}
112
113#[allow(dead_code)]
115pub fn channel_count_tl(export: &TelemetryExport) -> usize {
116 export.channels.len()
117}
118
119#[allow(dead_code)]
121pub fn session_duration_us(export: &TelemetryExport) -> u64 {
122 if export.frames.is_empty() {
123 return 0;
124 }
125 let min_t = export
126 .frames
127 .iter()
128 .map(|f| f.timestamp_us)
129 .min()
130 .unwrap_or(0);
131 let max_t = export
132 .frames
133 .iter()
134 .map(|f| f.timestamp_us)
135 .max()
136 .unwrap_or(0);
137 max_t.saturating_sub(min_t)
138}
139
140#[allow(dead_code)]
142pub fn channel_average(export: &TelemetryExport, channel_id: usize) -> f64 {
143 let values: Vec<f64> = export
144 .frames
145 .iter()
146 .filter_map(|f| f.values.get(channel_id).copied())
147 .collect();
148 if values.is_empty() {
149 0.0
150 } else {
151 values.iter().sum::<f64>() / values.len() as f64
152 }
153}
154
155#[allow(dead_code)]
157pub fn find_channel_by_name<'a>(
158 export: &'a TelemetryExport,
159 name: &str,
160) -> Option<&'a TelemetryChannel> {
161 export.channels.iter().find(|c| c.name == name)
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 fn sample_export() -> TelemetryExport {
169 let mut e = TelemetryExport::new("test_session");
170 add_telemetry_channel(&mut e, "velocity", "m/s", "f64");
171 add_telemetry_channel(&mut e, "rpm", "rpm", "f64");
172 record_frame(&mut e, 0, vec![1.0, 3000.0]);
173 record_frame(&mut e, 1000, vec![2.0, 3200.0]);
174 record_frame(&mut e, 2000, vec![1.5, 3100.0]);
175 e
176 }
177
178 #[test]
179 fn channel_count_correct() {
180 let e = sample_export();
181 assert_eq!(channel_count_tl(&e), 2);
182 }
183
184 #[test]
185 fn frame_count_correct() {
186 let e = sample_export();
187 assert_eq!(frame_count_tl(&e), 3);
188 }
189
190 #[test]
191 fn session_duration_2000us() {
192 let e = sample_export();
193 assert_eq!(session_duration_us(&e), 2000);
194 }
195
196 #[test]
197 fn channel_average_velocity() {
198 let e = sample_export();
199 let avg = channel_average(&e, 0);
200 assert!((avg - 1.5).abs() < 1e-5);
201 }
202
203 #[test]
204 fn csv_has_header() {
205 let e = sample_export();
206 let csv = export_telemetry_csv(&e);
207 assert!(csv.starts_with("timestamp_us,velocity,rpm"));
208 }
209
210 #[test]
211 fn csv_line_count() {
212 let e = sample_export();
213 let csv = export_telemetry_csv(&e);
214 let lines: Vec<&str> = csv.trim().split('\n').collect();
215 assert_eq!(lines.len(), 4);
216 }
217
218 #[test]
219 fn meta_contains_session_id() {
220 let e = sample_export();
221 let meta = export_telemetry_meta(&e);
222 assert!(meta.contains("test_session"));
223 }
224
225 #[test]
226 fn find_channel_by_name_some() {
227 let e = sample_export();
228 let ch = find_channel_by_name(&e, "velocity");
229 assert!(ch.is_some());
230 }
231
232 #[test]
233 fn find_channel_by_name_none() {
234 let e = sample_export();
235 let ch = find_channel_by_name(&e, "nonexistent");
236 assert!(ch.is_none());
237 }
238
239 #[test]
240 fn empty_session_duration_zero() {
241 let e = TelemetryExport::new("empty");
242 assert_eq!(session_duration_us(&e), 0);
243 }
244}