use crate::core::status::{GcodeState, PrinterStatus};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CaptureAction {
Continue,
Stop,
Capture { frame_no: u64, layer: i64 },
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
struct PrintIdentity {
task_id: Option<String>,
subtask_id: Option<String>,
gcode_file: Option<String>,
}
impl PrintIdentity {
fn of(s: &PrinterStatus) -> Self {
Self {
task_id: s.task_id.clone(),
subtask_id: s.subtask_id.clone(),
gcode_file: s.gcode_file.clone(),
}
}
fn is_meaningful(&self) -> bool {
[&self.task_id, &self.subtask_id, &self.gcode_file]
.into_iter()
.any(|f| f.as_deref().is_some_and(|v| !v.is_empty() && v != "0"))
}
}
pub struct CaptureSession {
every: u64,
wait: bool,
last_layer: Option<i64>,
frame_no: u64,
seen_active: bool,
identity: Option<PrintIdentity>,
}
impl CaptureSession {
pub fn new(every: u64, wait: bool) -> Self {
Self {
every: every.max(1),
wait,
last_layer: None,
frame_no: 0,
seen_active: false,
identity: None,
}
}
pub fn frames(&self) -> u64 {
self.frame_no
}
pub fn observe(&mut self, s: &PrinterStatus) -> CaptureAction {
let state = s.state();
let active = is_active(state);
if active {
self.seen_active = true;
let id = PrintIdentity::of(s);
if id.is_meaningful() && self.identity.as_ref() != Some(&id) {
self.identity = Some(id);
self.last_layer = None;
}
}
if self.should_stop(state, s.error.is_some()) {
return CaptureAction::Stop;
}
if active
&& let Some(layer) = s.layer_num
&& self.last_layer != Some(layer)
{
self.last_layer = Some(layer);
if layer >= 0 && (layer as u64).is_multiple_of(self.every) {
self.frame_no += 1;
return CaptureAction::Capture {
frame_no: self.frame_no,
layer,
};
}
}
CaptureAction::Continue
}
fn should_stop(&self, state: Option<GcodeState>, has_error: bool) -> bool {
should_stop(self.wait, self.seen_active, state, has_error)
}
}
fn is_active(state: Option<GcodeState>) -> bool {
matches!(
state,
Some(GcodeState::Running | GcodeState::Prepare | GcodeState::Pause)
)
}
fn should_stop(wait: bool, seen_active: bool, state: Option<GcodeState>, has_error: bool) -> bool {
if wait && !seen_active {
return false;
}
if has_error {
return true;
}
matches!(
state,
Some(GcodeState::Finish | GcodeState::Failed | GcodeState::Idle)
)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ActivityAction {
Capture,
Idle,
Stop,
}
pub struct PrintActivitySession {
wait: bool,
seen_active: bool,
}
impl PrintActivitySession {
pub fn new(wait: bool) -> Self {
Self {
wait,
seen_active: false,
}
}
pub fn observe(&mut self, s: &PrinterStatus) -> ActivityAction {
let state = s.state();
let active = is_active(state);
if active {
self.seen_active = true;
}
if should_stop(self.wait, self.seen_active, state, s.error.is_some()) {
return ActivityAction::Stop;
}
if active {
ActivityAction::Capture
} else {
ActivityAction::Idle
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::status::DeviceError;
fn st(state: &str, layer: Option<i64>) -> PrinterStatus {
PrinterStatus {
gcode_state: Some(state.to_string()),
layer_num: layer,
..Default::default()
}
}
fn cap(a: CaptureAction) -> Option<(u64, i64)> {
match a {
CaptureAction::Capture { frame_no, layer } => Some((frame_no, layer)),
_ => None,
}
}
#[test]
fn idle_does_not_capture_a_stale_layer() {
let mut s = CaptureSession::new(1, true);
assert_eq!(s.observe(&st("IDLE", Some(42))), CaptureAction::Continue);
assert_eq!(s.frames(), 0);
}
#[test]
fn first_active_layer_captures_once_then_no_recapture_on_same_layer() {
let mut s = CaptureSession::new(1, false);
assert_eq!(cap(s.observe(&st("RUNNING", Some(1)))), Some((1, 1)));
assert_eq!(s.observe(&st("RUNNING", Some(1))), CaptureAction::Continue);
assert_eq!(cap(s.observe(&st("RUNNING", Some(2)))), Some((2, 2)));
}
#[test]
fn every_n_filters_layers_including_layer_zero() {
let mut s = CaptureSession::new(2, false);
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)));
assert_eq!(s.observe(&st("RUNNING", Some(3))), CaptureAction::Continue);
}
#[test]
fn negative_or_missing_layer_does_nothing() {
let mut s = CaptureSession::new(1, false);
assert_eq!(s.observe(&st("RUNNING", Some(-1))), CaptureAction::Continue);
assert_eq!(s.observe(&st("RUNNING", None)), CaptureAction::Continue);
assert_eq!(s.frames(), 0);
}
#[test]
fn without_wait_idle_or_finish_stops_immediately() {
assert_eq!(
CaptureSession::new(1, false).observe(&st("IDLE", None)),
CaptureAction::Stop
);
assert_eq!(
CaptureSession::new(1, false).observe(&st("FINISH", Some(100))),
CaptureAction::Stop
);
}
#[test]
fn with_wait_sits_through_idle_finish_and_stale_error_until_active() {
let mut s = CaptureSession::new(1, true);
assert_eq!(s.observe(&st("IDLE", None)), CaptureAction::Continue);
assert_eq!(s.observe(&st("FINISH", Some(100))), CaptureAction::Continue);
let mut errd = st("FINISH", Some(100));
errd.error = DeviceError::from_code(0x05004003);
assert_eq!(s.observe(&errd), CaptureAction::Continue);
assert_eq!(cap(s.observe(&st("RUNNING", Some(1)))), Some((1, 1)));
}
#[test]
fn after_active_a_terminal_or_error_stops() {
let mut s = CaptureSession::new(1, true);
assert!(cap(s.observe(&st("RUNNING", Some(1)))).is_some());
assert_eq!(s.observe(&st("FINISH", Some(1))), CaptureAction::Stop);
let mut s = CaptureSession::new(1, true);
assert!(cap(s.observe(&st("RUNNING", Some(1)))).is_some());
let mut errd = st("RUNNING", Some(1));
errd.error = DeviceError::from_code(0x1234);
assert_eq!(s.observe(&errd), CaptureAction::Stop);
}
#[test]
fn pause_counts_as_active() {
let mut s = CaptureSession::new(1, false);
assert_eq!(cap(s.observe(&st("PAUSE", Some(5)))), Some((1, 5)));
}
#[test]
fn a_new_print_identity_resets_layer_tracking() {
let mut s = CaptureSession::new(1, true);
let mut a = st("RUNNING", Some(10));
a.task_id = Some("task-A".into());
assert!(cap(s.observe(&a)).is_some()); let mut b = st("RUNNING", Some(10));
b.task_id = Some("task-B".into());
assert!(
cap(s.observe(&b)).is_some(),
"a new print at the same layer number must capture (identity reset)"
);
}
#[test]
fn plain_waits_through_idle_then_captures_while_active() {
let mut a = PrintActivitySession::new(true);
assert_eq!(a.observe(&st("IDLE", None)), ActivityAction::Idle); assert_eq!(a.observe(&st("RUNNING", None)), ActivityAction::Capture);
assert_eq!(a.observe(&st("PAUSE", None)), ActivityAction::Capture); }
#[test]
fn plain_stops_when_the_print_finishes() {
let mut a = PrintActivitySession::new(true);
assert_eq!(a.observe(&st("RUNNING", None)), ActivityAction::Capture);
assert_eq!(a.observe(&st("FINISH", None)), ActivityAction::Stop);
}
#[test]
fn plain_stops_on_a_device_error() {
let mut a = PrintActivitySession::new(true);
a.observe(&st("RUNNING", None));
let mut e = st("RUNNING", None);
e.error = Some(DeviceError::from_code(0x1200_8016).unwrap());
assert_eq!(a.observe(&e), ActivityAction::Stop);
}
#[test]
fn plain_without_wait_stops_if_never_active() {
let mut a = PrintActivitySession::new(false);
assert_eq!(a.observe(&st("IDLE", None)), ActivityAction::Stop);
}
}