use crate::core::command::Command;
use crate::core::status::{GcodeState, PrinterStatus};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EffectStatus {
Observed,
NewError(i64),
Pending,
}
pub fn has_observable_effect(cmd: &Command) -> bool {
matches!(
cmd,
Command::ProjectFile(_)
| Command::GcodeFile(_)
| Command::Calibration { .. }
| Command::Pause
| Command::Resume
| Command::Stop
| Command::Led { .. }
| Command::IpcamTimelapse(_)
| Command::PrintSpeed(_)
)
}
pub fn evaluate(
cmd: &Command,
status: &PrinterStatus,
baseline_print_error: Option<i64>,
) -> EffectStatus {
let state = status.state();
if matches!(cmd, Command::Stop) {
return if matches!(
state,
Some(GcodeState::Idle | GcodeState::Finish | GcodeState::Failed)
) {
EffectStatus::Observed
} else {
EffectStatus::Pending
};
}
let current = status.print_error.unwrap_or(0);
let baseline = baseline_print_error.unwrap_or(0);
if current != 0 && current != baseline {
return EffectStatus::NewError(current);
}
let observed = match cmd {
Command::ProjectFile(_) | Command::GcodeFile(_) | Command::Calibration { .. } => {
matches!(
state,
Some(GcodeState::Prepare | GcodeState::Running | GcodeState::Slicing)
)
}
Command::Pause => state == Some(GcodeState::Pause),
Command::Resume => state == Some(GcodeState::Running),
Command::Led { node, on } => {
let want = if *on { "on" } else { "off" };
status.light_mode(node.as_str()) == Some(want)
}
Command::IpcamTimelapse(control) => status.timelapse_mode() == Some(control.as_str()),
Command::PrintSpeed(level) => status.spd_lvl == Some(level.level()),
Command::Stop => unreachable!("Stop handled before the new-error check"),
Command::PushAll
| Command::GetVersion
| Command::GcodeLine(_)
| Command::Reboot
| Command::AmsControl(_)
| Command::AmsChangeFilament { .. }
| Command::AmsUserSetting { .. }
| Command::AmsFilamentSetting(_)
| Command::CleanPrintError => false,
};
if observed {
EffectStatus::Observed
} else {
EffectStatus::Pending
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::command::ProjectFile;
fn status(gcode_state: &str, print_error: i64) -> PrinterStatus {
PrinterStatus {
gcode_state: Some(gcode_state.to_string()),
print_error: Some(print_error),
..Default::default()
}
}
fn project() -> Command {
Command::ProjectFile(ProjectFile::new("ftp:///model/x.gcode.3mf", 1, "x"))
}
#[test]
fn which_commands_have_an_observable_effect() {
assert!(has_observable_effect(&project()));
assert!(has_observable_effect(&Command::Pause));
assert!(has_observable_effect(&Command::Calibration {
bed_level: true,
vibration: false,
motor_noise: false
}));
assert!(has_observable_effect(&Command::Led {
node: crate::core::command::LedNode::ChamberLight,
on: true
}));
assert!(!has_observable_effect(&Command::GcodeLine("G28".into())));
assert!(!has_observable_effect(&Command::PushAll));
assert!(!has_observable_effect(&Command::Reboot));
}
#[test]
fn ipcam_timelapse_effect_reads_the_ipcam_node_not_the_ack() {
use crate::core::command::TimelapseControl;
use crate::core::status::Ipcam;
let with_timelapse = |mode: &str| PrinterStatus {
ipcam: Some(Ipcam {
timelapse: Some(mode.to_string()),
..Default::default()
}),
..Default::default()
};
assert_eq!(
evaluate(
&Command::IpcamTimelapse(TimelapseControl::Enable),
&with_timelapse("enable"),
Some(0)
),
EffectStatus::Observed
);
assert_eq!(
evaluate(
&Command::IpcamTimelapse(TimelapseControl::Enable),
&with_timelapse("disable"),
Some(0)
),
EffectStatus::Pending
);
assert!(has_observable_effect(&Command::IpcamTimelapse(
TimelapseControl::Disable
)));
}
#[test]
fn print_speed_effect_reads_spd_lvl() {
use crate::core::command::SpeedLevel;
let at = |lvl: i64| PrinterStatus {
spd_lvl: Some(lvl),
..Default::default()
};
assert_eq!(
evaluate(&Command::PrintSpeed(SpeedLevel::Sport), &at(3), Some(0)),
EffectStatus::Observed
);
assert_eq!(
evaluate(&Command::PrintSpeed(SpeedLevel::Sport), &at(2), Some(0)),
EffectStatus::Pending
);
assert!(has_observable_effect(&Command::PrintSpeed(
SpeedLevel::Silent
)));
}
#[test]
fn chamber_light_effect_reads_lights_report_not_the_ack() {
use crate::core::command::LedNode;
use crate::core::status::LightReport;
let chamber = |node: LedNode, on: bool| Command::Led { node, on };
let lit = |mode: &str| PrinterStatus {
lights: vec![LightReport {
node: "chamber_light".to_string(),
mode: mode.to_string(),
}],
..Default::default()
};
assert_eq!(
evaluate(&chamber(LedNode::ChamberLight, true), &lit("on"), Some(0)),
EffectStatus::Observed
);
assert_eq!(
evaluate(&chamber(LedNode::ChamberLight, true), &lit("off"), Some(0)),
EffectStatus::Pending
);
assert_eq!(
evaluate(&chamber(LedNode::ChamberLight, false), &lit("off"), Some(0)),
EffectStatus::Observed
);
assert_eq!(
evaluate(&chamber(LedNode::WorkLight, true), &lit("on"), Some(0)),
EffectStatus::Pending
);
}
#[test]
fn project_file_running_is_observed() {
assert_eq!(
evaluate(&project(), &status("RUNNING", 0), Some(0)),
EffectStatus::Observed
);
assert_eq!(
evaluate(&project(), &status("PREPARE", 0), Some(0)),
EffectStatus::Observed
);
}
#[test]
fn project_file_idle_with_no_error_is_pending_not_observed() {
assert_eq!(
evaluate(&project(), &status("IDLE", 0), Some(0)),
EffectStatus::Pending
);
}
#[test]
fn new_print_error_after_send_is_reported() {
assert_eq!(
evaluate(&project(), &status("IDLE", 0x0500C010), Some(0)),
EffectStatus::NewError(0x0500C010)
);
}
#[test]
fn preexisting_error_is_not_blamed_on_this_command() {
assert_eq!(
evaluate(&project(), &status("IDLE", 0x0500C010), Some(0x0500C010)),
EffectStatus::Pending
);
}
#[test]
fn pause_resume_stop_effects() {
assert_eq!(
evaluate(&Command::Pause, &status("PAUSE", 0), Some(0)),
EffectStatus::Observed
);
assert_eq!(
evaluate(&Command::Resume, &status("RUNNING", 0), Some(0)),
EffectStatus::Observed
);
assert_eq!(
evaluate(&Command::Stop, &status("FINISH", 0), Some(0)),
EffectStatus::Observed
);
assert_eq!(
evaluate(&Command::Pause, &status("RUNNING", 0), Some(0)),
EffectStatus::Pending
);
}
#[test]
fn stop_tolerates_a_transient_abort_error() {
assert_eq!(
evaluate(&Command::Stop, &status("FAILED", 0x0300400C), Some(0)),
EffectStatus::Observed
);
assert_eq!(
evaluate(&Command::Stop, &status("PAUSE", 0x0300400C), Some(0)),
EffectStatus::Pending
);
}
#[test]
fn a_new_error_beats_an_otherwise_observed_effect() {
assert_eq!(
evaluate(&Command::Resume, &status("RUNNING", 0x0500C010), Some(0)),
EffectStatus::NewError(0x0500C010)
);
}
}