1use crate::core::status::{GcodeState, PrinterStatus};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum CaptureAction {
15 Continue,
17 Stop,
20 Capture { frame_no: u64, layer: i64 },
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Default)]
28struct PrintIdentity {
29 task_id: Option<String>,
30 subtask_id: Option<String>,
31 gcode_file: Option<String>,
32}
33
34impl PrintIdentity {
35 fn of(s: &PrinterStatus) -> Self {
36 Self {
37 task_id: s.task_id.clone(),
38 subtask_id: s.subtask_id.clone(),
39 gcode_file: s.gcode_file.clone(),
40 }
41 }
42 fn is_meaningful(&self) -> bool {
45 [&self.task_id, &self.subtask_id, &self.gcode_file]
46 .into_iter()
47 .any(|f| f.as_deref().is_some_and(|v| !v.is_empty() && v != "0"))
48 }
49}
50
51pub struct CaptureSession {
57 every: u64,
58 wait: bool,
59 last_layer: Option<i64>,
60 frame_no: u64,
61 seen_active: bool,
62 identity: Option<PrintIdentity>,
63}
64
65impl CaptureSession {
66 pub fn new(every: u64, wait: bool) -> Self {
70 Self {
71 every: every.max(1),
72 wait,
73 last_layer: None,
74 frame_no: 0,
75 seen_active: false,
76 identity: None,
77 }
78 }
79
80 pub fn frames(&self) -> u64 {
82 self.frame_no
83 }
84
85 pub fn observe(&mut self, s: &PrinterStatus) -> CaptureAction {
87 let state = s.state();
88 let active = is_active(state);
89 if active {
90 self.seen_active = true;
91 let id = PrintIdentity::of(s);
94 if id.is_meaningful() && self.identity.as_ref() != Some(&id) {
95 self.identity = Some(id);
96 self.last_layer = None;
97 }
98 }
99
100 if self.should_stop(state, s.error.is_some()) {
103 return CaptureAction::Stop;
104 }
105
106 if active
107 && let Some(layer) = s.layer_num
108 && self.last_layer != Some(layer)
109 {
110 self.last_layer = Some(layer);
111 if layer >= 0 && (layer as u64).is_multiple_of(self.every) {
113 self.frame_no += 1;
114 return CaptureAction::Capture {
115 frame_no: self.frame_no,
116 layer,
117 };
118 }
119 }
120 CaptureAction::Continue
121 }
122
123 fn should_stop(&self, state: Option<GcodeState>, has_error: bool) -> bool {
124 should_stop(self.wait, self.seen_active, state, has_error)
125 }
126}
127
128fn is_active(state: Option<GcodeState>) -> bool {
131 matches!(
132 state,
133 Some(GcodeState::Running | GcodeState::Prepare | GcodeState::Pause)
134 )
135}
136
137fn should_stop(wait: bool, seen_active: bool, state: Option<GcodeState>, has_error: bool) -> bool {
142 if wait && !seen_active {
143 return false;
144 }
145 if has_error {
146 return true;
147 }
148 matches!(
149 state,
150 Some(GcodeState::Finish | GcodeState::Failed | GcodeState::Idle)
151 )
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
157pub enum ActivityAction {
158 Capture,
160 Idle,
162 Stop,
164}
165
166pub struct PrintActivitySession {
171 wait: bool,
172 seen_active: bool,
173}
174
175impl PrintActivitySession {
176 pub fn new(wait: bool) -> Self {
179 Self {
180 wait,
181 seen_active: false,
182 }
183 }
184
185 pub fn observe(&mut self, s: &PrinterStatus) -> ActivityAction {
189 let state = s.state();
190 let active = is_active(state);
191 if active {
192 self.seen_active = true;
193 }
194 if should_stop(self.wait, self.seen_active, state, s.error.is_some()) {
195 return ActivityAction::Stop;
196 }
197 if active {
198 ActivityAction::Capture
199 } else {
200 ActivityAction::Idle
201 }
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use crate::core::status::DeviceError;
209
210 fn st(state: &str, layer: Option<i64>) -> PrinterStatus {
211 PrinterStatus {
212 gcode_state: Some(state.to_string()),
213 layer_num: layer,
214 ..Default::default()
215 }
216 }
217
218 fn cap(a: CaptureAction) -> Option<(u64, i64)> {
219 match a {
220 CaptureAction::Capture { frame_no, layer } => Some((frame_no, layer)),
221 _ => None,
222 }
223 }
224
225 #[test]
226 fn idle_does_not_capture_a_stale_layer() {
227 let mut s = CaptureSession::new(1, true);
228 assert_eq!(s.observe(&st("IDLE", Some(42))), CaptureAction::Continue);
230 assert_eq!(s.frames(), 0);
231 }
232
233 #[test]
234 fn first_active_layer_captures_once_then_no_recapture_on_same_layer() {
235 let mut s = CaptureSession::new(1, false);
236 assert_eq!(cap(s.observe(&st("RUNNING", Some(1)))), Some((1, 1)));
237 assert_eq!(s.observe(&st("RUNNING", Some(1))), CaptureAction::Continue);
239 assert_eq!(cap(s.observe(&st("RUNNING", Some(2)))), Some((2, 2)));
241 }
242
243 #[test]
244 fn every_n_filters_layers_including_layer_zero() {
245 let mut s = CaptureSession::new(2, false);
246 assert_eq!(cap(s.observe(&st("RUNNING", Some(0)))), Some((1, 0))); assert_eq!(s.observe(&st("RUNNING", Some(1))), CaptureAction::Continue); assert_eq!(cap(s.observe(&st("RUNNING", Some(2)))), Some((2, 2)));
249 assert_eq!(s.observe(&st("RUNNING", Some(3))), CaptureAction::Continue);
250 }
251
252 #[test]
253 fn negative_or_missing_layer_does_nothing() {
254 let mut s = CaptureSession::new(1, false);
255 assert_eq!(s.observe(&st("RUNNING", Some(-1))), CaptureAction::Continue);
256 assert_eq!(s.observe(&st("RUNNING", None)), CaptureAction::Continue);
257 assert_eq!(s.frames(), 0);
258 }
259
260 #[test]
261 fn without_wait_idle_or_finish_stops_immediately() {
262 assert_eq!(
263 CaptureSession::new(1, false).observe(&st("IDLE", None)),
264 CaptureAction::Stop
265 );
266 assert_eq!(
267 CaptureSession::new(1, false).observe(&st("FINISH", Some(100))),
268 CaptureAction::Stop
269 );
270 }
271
272 #[test]
273 fn with_wait_sits_through_idle_finish_and_stale_error_until_active() {
274 let mut s = CaptureSession::new(1, true);
275 assert_eq!(s.observe(&st("IDLE", None)), CaptureAction::Continue);
276 assert_eq!(s.observe(&st("FINISH", Some(100))), CaptureAction::Continue);
277 let mut errd = st("FINISH", Some(100));
279 errd.error = DeviceError::from_code(0x05004003);
280 assert_eq!(s.observe(&errd), CaptureAction::Continue);
281 assert_eq!(cap(s.observe(&st("RUNNING", Some(1)))), Some((1, 1)));
283 }
284
285 #[test]
286 fn after_active_a_terminal_or_error_stops() {
287 let mut s = CaptureSession::new(1, true);
288 assert!(cap(s.observe(&st("RUNNING", Some(1)))).is_some());
289 assert_eq!(s.observe(&st("FINISH", Some(1))), CaptureAction::Stop);
290
291 let mut s = CaptureSession::new(1, true);
292 assert!(cap(s.observe(&st("RUNNING", Some(1)))).is_some());
293 let mut errd = st("RUNNING", Some(1));
294 errd.error = DeviceError::from_code(0x1234);
295 assert_eq!(s.observe(&errd), CaptureAction::Stop);
296 }
297
298 #[test]
299 fn pause_counts_as_active() {
300 let mut s = CaptureSession::new(1, false);
301 assert_eq!(cap(s.observe(&st("PAUSE", Some(5)))), Some((1, 5)));
302 }
303
304 #[test]
305 fn a_new_print_identity_resets_layer_tracking() {
306 let mut s = CaptureSession::new(1, true);
307 let mut a = st("RUNNING", Some(10));
308 a.task_id = Some("task-A".into());
309 assert!(cap(s.observe(&a)).is_some()); let mut b = st("RUNNING", Some(10));
315 b.task_id = Some("task-B".into());
316 assert!(
317 cap(s.observe(&b)).is_some(),
318 "a new print at the same layer number must capture (identity reset)"
319 );
320 }
321
322 #[test]
325 fn plain_waits_through_idle_then_captures_while_active() {
326 let mut a = PrintActivitySession::new(true);
327 assert_eq!(a.observe(&st("IDLE", None)), ActivityAction::Idle); assert_eq!(a.observe(&st("RUNNING", None)), ActivityAction::Capture);
329 assert_eq!(a.observe(&st("PAUSE", None)), ActivityAction::Capture); }
331
332 #[test]
333 fn plain_stops_when_the_print_finishes() {
334 let mut a = PrintActivitySession::new(true);
335 assert_eq!(a.observe(&st("RUNNING", None)), ActivityAction::Capture);
336 assert_eq!(a.observe(&st("FINISH", None)), ActivityAction::Stop);
337 }
338
339 #[test]
340 fn plain_stops_on_a_device_error() {
341 let mut a = PrintActivitySession::new(true);
342 a.observe(&st("RUNNING", None));
343 let mut e = st("RUNNING", None);
344 e.error = Some(DeviceError::from_code(0x1200_8016).unwrap());
345 assert_eq!(a.observe(&e), ActivityAction::Stop);
346 }
347
348 #[test]
349 fn plain_without_wait_stops_if_never_active() {
350 let mut a = PrintActivitySession::new(false);
351 assert_eq!(a.observe(&st("IDLE", None)), ActivityAction::Stop);
352 }
353}