piper_client/
recording.rs1use piper_driver::recording::TimestampedFrame;
43use std::path::PathBuf;
44use std::sync::Arc;
45use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
46use std::time::Instant;
47
48pub struct RecordingHandle {
62 rx: crossbeam_channel::Receiver<TimestampedFrame>,
64
65 dropped_frames: Arc<AtomicU64>,
67
68 frame_counter: Arc<AtomicU64>,
70
71 stop_requested: Arc<AtomicBool>,
73
74 output_path: PathBuf,
76
77 start_time: Instant,
79}
80
81impl RecordingHandle {
82 pub(super) fn new(
90 rx: crossbeam_channel::Receiver<TimestampedFrame>,
91 dropped_frames: Arc<AtomicU64>,
92 frame_counter: Arc<AtomicU64>,
93 output_path: PathBuf,
94 start_time: Instant,
95 stop_requested: Option<Arc<AtomicBool>>,
96 ) -> Self {
97 Self {
98 rx,
99 dropped_frames,
100 frame_counter,
101 stop_requested: stop_requested.unwrap_or_else(|| Arc::new(AtomicBool::new(false))),
102 output_path,
103 start_time,
104 }
105 }
106
107 pub fn frame_count(&self) -> u64 {
113 self.frame_counter.load(Ordering::Relaxed)
114 }
115
116 pub fn dropped_count(&self) -> u64 {
118 self.dropped_frames.load(Ordering::Relaxed)
119 }
120
121 pub fn is_stop_requested(&self) -> bool {
123 self.stop_requested.load(Ordering::Relaxed)
124 }
125
126 pub fn stop(&self) {
128 self.stop_requested.store(true, Ordering::SeqCst);
129 }
130
131 pub fn elapsed(&self) -> std::time::Duration {
133 self.start_time.elapsed()
134 }
135
136 pub fn output_path(&self) -> &PathBuf {
138 &self.output_path
139 }
140
141 pub(super) fn receiver(&self) -> &crossbeam_channel::Receiver<TimestampedFrame> {
143 &self.rx
144 }
145}
146
147impl Drop for RecordingHandle {
148 fn drop(&mut self) {
153 tracing::debug!("RecordingHandle dropped, receiver closed");
156 }
157}
158
159#[derive(Debug, Clone)]
161pub struct RecordingConfig {
162 pub output_path: PathBuf,
164
165 pub stop_condition: StopCondition,
167
168 pub metadata: RecordingMetadata,
170}
171
172#[derive(Debug, Clone)]
174pub enum StopCondition {
175 Duration(u64),
177
178 Manual,
180
181 OnCanId(u32),
183
184 FrameCount(usize),
186}
187
188#[derive(Debug, Clone)]
190pub struct RecordingMetadata {
191 pub notes: String,
192 pub operator: String,
193}
194
195#[derive(Debug, Clone)]
197pub struct RecordingStats {
198 pub frame_count: usize,
199 pub duration: std::time::Duration,
200 pub dropped_frames: u64,
201 pub output_path: PathBuf,
202}
203
204#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn test_stop_condition_duration() {
213 let condition = StopCondition::Duration(10);
214 match condition {
215 StopCondition::Duration(s) => assert_eq!(s, 10),
216 _ => panic!("Wrong condition"),
217 }
218 }
219
220 #[test]
221 fn test_stop_condition_manual() {
222 let condition = StopCondition::Manual;
223 match condition {
224 StopCondition::Manual => {}, _ => panic!("Wrong condition"),
226 }
227 }
228
229 #[test]
230 fn test_stop_condition_on_can_id() {
231 let condition = StopCondition::OnCanId(0x1A1);
232 match condition {
233 StopCondition::OnCanId(id) => assert_eq!(id, 0x1A1),
234 _ => panic!("Wrong condition"),
235 }
236 }
237
238 #[test]
239 fn test_stop_condition_frame_count() {
240 let condition = StopCondition::FrameCount(1000);
241 match condition {
242 StopCondition::FrameCount(count) => assert_eq!(count, 1000),
243 _ => panic!("Wrong condition"),
244 }
245 }
246
247 #[test]
248 fn test_recording_metadata() {
249 let metadata = RecordingMetadata {
250 notes: "Test".to_string(),
251 operator: "Alice".to_string(),
252 };
253 assert_eq!(metadata.notes, "Test");
254 assert_eq!(metadata.operator, "Alice");
255 }
256
257 #[test]
258 fn test_recording_metadata_empty_strings() {
259 let metadata = RecordingMetadata {
260 notes: "".to_string(),
261 operator: "".to_string(),
262 };
263 assert_eq!(metadata.notes, "");
264 assert_eq!(metadata.operator, "");
265 }
266
267 #[test]
268 fn test_recording_config() {
269 let config = RecordingConfig {
270 output_path: "/tmp/test.bin".into(),
271 stop_condition: StopCondition::Duration(10),
272 metadata: RecordingMetadata {
273 notes: "Test".to_string(),
274 operator: "Bob".to_string(),
275 },
276 };
277
278 assert_eq!(
279 config.output_path,
280 std::path::PathBuf::from("/tmp/test.bin")
281 );
282 match config.stop_condition {
283 StopCondition::Duration(s) => assert_eq!(s, 10),
284 _ => panic!("Wrong condition"),
285 }
286 assert_eq!(config.metadata.notes, "Test");
287 assert_eq!(config.metadata.operator, "Bob");
288 }
289
290 #[test]
291 fn test_recording_stats() {
292 let stats = RecordingStats {
293 frame_count: 1000,
294 duration: std::time::Duration::from_secs(10),
295 dropped_frames: 5,
296 output_path: "/tmp/test.bin".into(),
297 };
298
299 assert_eq!(stats.frame_count, 1000);
300 assert_eq!(stats.duration.as_secs(), 10);
301 assert_eq!(stats.dropped_frames, 5);
302 assert_eq!(stats.output_path, std::path::PathBuf::from("/tmp/test.bin"));
303 }
304
305 #[test]
306 fn test_recording_stats_clone() {
307 let stats = RecordingStats {
308 frame_count: 100,
309 duration: std::time::Duration::from_millis(500),
310 dropped_frames: 0,
311 output_path: "/tmp/clone_test.bin".into(),
312 };
313
314 let cloned = stats.clone();
315 assert_eq!(cloned.frame_count, stats.frame_count);
316 assert_eq!(cloned.duration, stats.duration);
317 assert_eq!(cloned.dropped_frames, stats.dropped_frames);
318 assert_eq!(cloned.output_path, stats.output_path);
319 }
320}