Skip to main content

piper_client/
recording.rs

1//! 标准录制 API
2//!
3//! 本模块提供易用的录制功能,适用于大多数用户场景。
4//!
5//! # 设计理念
6//!
7//! - **类型安全**:与类型状态机完全集成
8//! - **RAII 语义**:自动管理资源
9//! - **易于使用**:适合常规录制场景
10//!
11//! # 使用示例
12//!
13//! ```rust,no_run
14//! use piper_client::{PiperBuilder, recording::{RecordingConfig, RecordingMetadata, StopCondition}};
15//!
16//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
17//! let robot = PiperBuilder::new()
18//!     .interface("can0")
19//!     .build()?;
20//!
21//! let active = robot.enable_position_mode(Default::default())?;
22//!
23//! // 启动录制(Active 状态)
24//! let (active, handle) = active.start_recording(RecordingConfig {
25//!     output_path: "demo.bin".into(),
26//!     stop_condition: StopCondition::Duration(10),
27//!     metadata: RecordingMetadata {
28//!         notes: "Test recording".to_string(),
29//!         operator: "Alice".to_string(),
30//!     },
31//! })?;
32//!
33//! // 执行操作(会被录制,包含控制指令帧)
34//! // active.send_position_command(...)?;
35//!
36//! // 停止录制并保存
37//! let _active = active.stop_recording(handle)?;
38//! # Ok(())
39//! # }
40//! ```
41
42use piper_driver::recording::TimestampedFrame;
43use std::path::PathBuf;
44use std::sync::Arc;
45use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
46use std::time::Instant;
47
48/// 录制句柄(用于控制和监控)
49///
50/// # Drop 语义
51///
52/// 当 `RecordingHandle` 被丢弃时:
53/// - ✅ 自动 flush 缓冲区中的数据
54/// - ✅ 自动关闭接收端
55/// - ❌ 不会自动保存文件(需要显式调用 `stop_recording()`)
56///
57/// # Panics
58///
59/// 如果在 Drop 时发生 I/O 错误,错误会被静默忽略(Drop 上下文无法处理错误)。
60/// 建议始终显式调用 `stop_recording()` 以获取错误结果。
61pub struct RecordingHandle {
62    /// 接收端(用于读取录制的帧)
63    rx: crossbeam_channel::Receiver<TimestampedFrame>,
64
65    /// 丢帧计数器
66    dropped_frames: Arc<AtomicU64>,
67
68    /// 帧计数器(从 Driver 层传递)
69    frame_counter: Arc<AtomicU64>,
70
71    /// 停止请求标记(用于 Manual 停止)
72    stop_requested: Arc<AtomicBool>,
73
74    /// 输出文件路径
75    output_path: PathBuf,
76
77    /// 录制开始时间
78    start_time: Instant,
79}
80
81impl RecordingHandle {
82    /// 创建新的录制句柄(内部使用)
83    ///
84    /// # 参数
85    ///
86    /// - `stop_requested`: 可选的外部停止标志(用于 Driver 层的 `OnCanId` 停止条件)
87    ///   - `None`: 创建新的内部停止标志(用于 `Manual` 停止条件)
88    ///   - `Some(external)`: 使用 Driver 层提供的停止标志(用于 `OnCanId` 停止条件)
89    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    /// 获取当前已录制的帧数(线程安全,无阻塞)
108    ///
109    /// # 返回
110    ///
111    /// 当前已成功录制的帧数
112    pub fn frame_count(&self) -> u64 {
113        self.frame_counter.load(Ordering::Relaxed)
114    }
115
116    /// 获取当前丢帧数量
117    pub fn dropped_count(&self) -> u64 {
118        self.dropped_frames.load(Ordering::Relaxed)
119    }
120
121    /// 检查是否已请求停止(用于循环条件判断)
122    pub fn is_stop_requested(&self) -> bool {
123        self.stop_requested.load(Ordering::Relaxed)
124    }
125
126    /// 手动停止录制(请求停止)
127    pub fn stop(&self) {
128        self.stop_requested.store(true, Ordering::SeqCst);
129    }
130
131    /// 获取录制时长
132    pub fn elapsed(&self) -> std::time::Duration {
133        self.start_time.elapsed()
134    }
135
136    /// 获取输出文件路径
137    pub fn output_path(&self) -> &PathBuf {
138        &self.output_path
139    }
140
141    /// 获取接收端的引用(用于 stop_recording)
142    pub(super) fn receiver(&self) -> &crossbeam_channel::Receiver<TimestampedFrame> {
143        &self.rx
144    }
145}
146
147impl Drop for RecordingHandle {
148    /// ⚠️ Drop 语义:自动清理资源
149    ///
150    /// 注意:这里只关闭接收端,不保存文件。
151    /// 文件保存必须在 `stop_recording()` 中显式完成。
152    fn drop(&mut self) {
153        // 接收端会在 Drop 时自动关闭
154        // 这里只是显式标记(用于调试)
155        tracing::debug!("RecordingHandle dropped, receiver closed");
156    }
157}
158
159/// 录制配置
160#[derive(Debug, Clone)]
161pub struct RecordingConfig {
162    /// 输出文件路径
163    pub output_path: PathBuf,
164
165    /// 自动停止条件
166    pub stop_condition: StopCondition,
167
168    /// 元数据
169    pub metadata: RecordingMetadata,
170}
171
172/// 停止条件
173#[derive(Debug, Clone)]
174pub enum StopCondition {
175    /// 时长限制(秒)
176    Duration(u64),
177
178    /// 手动停止
179    Manual,
180
181    /// 接收到特定 CAN ID 时停止
182    OnCanId(u32),
183
184    /// 接收到特定数量的帧后停止
185    FrameCount(usize),
186}
187
188/// 录制元数据
189#[derive(Debug, Clone)]
190pub struct RecordingMetadata {
191    pub notes: String,
192    pub operator: String,
193}
194
195/// 录制统计
196#[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// 以下方法将在 state/machine.rs 的 impl 中实现
205// 因为它们需要访问私有字段
206
207#[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 => {}, // OK
225            _ => 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}