1use crate::core::command::Command;
21use crate::core::status::{GcodeState, PrinterStatus};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum EffectStatus {
26 Observed,
28 NewError(i64),
30 Pending,
33}
34
35pub fn has_observable_effect(cmd: &Command) -> bool {
38 matches!(
39 cmd,
40 Command::ProjectFile(_)
41 | Command::GcodeFile(_)
42 | Command::Calibration { .. }
43 | Command::Pause
44 | Command::Resume
45 | Command::Stop
46 | Command::Led { .. }
47 | Command::IpcamTimelapse(_)
48 | Command::PrintSpeed(_)
49 )
50}
51
52pub fn evaluate(
56 cmd: &Command,
57 status: &PrinterStatus,
58 baseline_print_error: Option<i64>,
59) -> EffectStatus {
60 let state = status.state();
61
62 if matches!(cmd, Command::Stop) {
67 return if matches!(
68 state,
69 Some(GcodeState::Idle | GcodeState::Finish | GcodeState::Failed)
70 ) {
71 EffectStatus::Observed
72 } else {
73 EffectStatus::Pending
74 };
75 }
76
77 let current = status.print_error.unwrap_or(0);
80 let baseline = baseline_print_error.unwrap_or(0);
81 if current != 0 && current != baseline {
82 return EffectStatus::NewError(current);
83 }
84
85 let observed = match cmd {
86 Command::ProjectFile(_) | Command::GcodeFile(_) | Command::Calibration { .. } => {
89 matches!(
90 state,
91 Some(GcodeState::Prepare | GcodeState::Running | GcodeState::Slicing)
92 )
93 }
94 Command::Pause => state == Some(GcodeState::Pause),
95 Command::Resume => state == Some(GcodeState::Running),
96 Command::Led { node, on } => {
100 let want = if *on { "on" } else { "off" };
101 status.light_mode(node.as_str()) == Some(want)
102 }
103 Command::IpcamTimelapse(control) => status.timelapse_mode() == Some(control.as_str()),
107 Command::PrintSpeed(level) => status.spd_lvl == Some(level.level()),
109 Command::Stop => unreachable!("Stop handled before the new-error check"),
111 Command::PushAll
116 | Command::GetVersion
117 | Command::GcodeLine(_)
118 | Command::Reboot
119 | Command::AmsControl(_)
120 | Command::AmsChangeFilament { .. }
121 | Command::AmsUserSetting { .. }
122 | Command::AmsFilamentSetting(_)
123 | Command::CleanPrintError => false,
127 };
128
129 if observed {
130 EffectStatus::Observed
131 } else {
132 EffectStatus::Pending
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use crate::core::command::ProjectFile;
140
141 fn status(gcode_state: &str, print_error: i64) -> PrinterStatus {
142 PrinterStatus {
143 gcode_state: Some(gcode_state.to_string()),
144 print_error: Some(print_error),
145 ..Default::default()
146 }
147 }
148
149 fn project() -> Command {
150 Command::ProjectFile(ProjectFile::new("ftp:///model/x.gcode.3mf", 1, "x"))
151 }
152
153 #[test]
154 fn which_commands_have_an_observable_effect() {
155 assert!(has_observable_effect(&project()));
156 assert!(has_observable_effect(&Command::Pause));
157 assert!(has_observable_effect(&Command::Calibration {
158 bed_level: true,
159 vibration: false,
160 motor_noise: false
161 }));
162 assert!(has_observable_effect(&Command::Led {
164 node: crate::core::command::LedNode::ChamberLight,
165 on: true
166 }));
167 assert!(!has_observable_effect(&Command::GcodeLine("G28".into())));
169 assert!(!has_observable_effect(&Command::PushAll));
170 assert!(!has_observable_effect(&Command::Reboot));
171 }
172
173 #[test]
174 fn ipcam_timelapse_effect_reads_the_ipcam_node_not_the_ack() {
175 use crate::core::command::TimelapseControl;
176 use crate::core::status::Ipcam;
177 let with_timelapse = |mode: &str| PrinterStatus {
178 ipcam: Some(Ipcam {
179 timelapse: Some(mode.to_string()),
180 ..Default::default()
181 }),
182 ..Default::default()
183 };
184 assert_eq!(
186 evaluate(
187 &Command::IpcamTimelapse(TimelapseControl::Enable),
188 &with_timelapse("enable"),
189 Some(0)
190 ),
191 EffectStatus::Observed
192 );
193 assert_eq!(
196 evaluate(
197 &Command::IpcamTimelapse(TimelapseControl::Enable),
198 &with_timelapse("disable"),
199 Some(0)
200 ),
201 EffectStatus::Pending
202 );
203 assert!(has_observable_effect(&Command::IpcamTimelapse(
204 TimelapseControl::Disable
205 )));
206 }
207
208 #[test]
209 fn print_speed_effect_reads_spd_lvl() {
210 use crate::core::command::SpeedLevel;
211 let at = |lvl: i64| PrinterStatus {
212 spd_lvl: Some(lvl),
213 ..Default::default()
214 };
215 assert_eq!(
217 evaluate(&Command::PrintSpeed(SpeedLevel::Sport), &at(3), Some(0)),
218 EffectStatus::Observed
219 );
220 assert_eq!(
222 evaluate(&Command::PrintSpeed(SpeedLevel::Sport), &at(2), Some(0)),
223 EffectStatus::Pending
224 );
225 assert!(has_observable_effect(&Command::PrintSpeed(
226 SpeedLevel::Silent
227 )));
228 }
229
230 #[test]
231 fn chamber_light_effect_reads_lights_report_not_the_ack() {
232 use crate::core::command::LedNode;
233 use crate::core::status::LightReport;
234 let chamber = |node: LedNode, on: bool| Command::Led { node, on };
235 let lit = |mode: &str| PrinterStatus {
236 lights: vec![LightReport {
237 node: "chamber_light".to_string(),
238 mode: mode.to_string(),
239 }],
240 ..Default::default()
241 };
242 assert_eq!(
244 evaluate(&chamber(LedNode::ChamberLight, true), &lit("on"), Some(0)),
245 EffectStatus::Observed
246 );
247 assert_eq!(
250 evaluate(&chamber(LedNode::ChamberLight, true), &lit("off"), Some(0)),
251 EffectStatus::Pending
252 );
253 assert_eq!(
254 evaluate(&chamber(LedNode::ChamberLight, false), &lit("off"), Some(0)),
255 EffectStatus::Observed
256 );
257 assert_eq!(
259 evaluate(&chamber(LedNode::WorkLight, true), &lit("on"), Some(0)),
260 EffectStatus::Pending
261 );
262 }
263
264 #[test]
265 fn project_file_running_is_observed() {
266 assert_eq!(
267 evaluate(&project(), &status("RUNNING", 0), Some(0)),
268 EffectStatus::Observed
269 );
270 assert_eq!(
271 evaluate(&project(), &status("PREPARE", 0), Some(0)),
272 EffectStatus::Observed
273 );
274 }
275
276 #[test]
277 fn project_file_idle_with_no_error_is_pending_not_observed() {
278 assert_eq!(
281 evaluate(&project(), &status("IDLE", 0), Some(0)),
282 EffectStatus::Pending
283 );
284 }
285
286 #[test]
287 fn new_print_error_after_send_is_reported() {
288 assert_eq!(
290 evaluate(&project(), &status("IDLE", 0x0500C010), Some(0)),
291 EffectStatus::NewError(0x0500C010)
292 );
293 }
294
295 #[test]
296 fn preexisting_error_is_not_blamed_on_this_command() {
297 assert_eq!(
300 evaluate(&project(), &status("IDLE", 0x0500C010), Some(0x0500C010)),
301 EffectStatus::Pending
302 );
303 }
304
305 #[test]
306 fn pause_resume_stop_effects() {
307 assert_eq!(
308 evaluate(&Command::Pause, &status("PAUSE", 0), Some(0)),
309 EffectStatus::Observed
310 );
311 assert_eq!(
312 evaluate(&Command::Resume, &status("RUNNING", 0), Some(0)),
313 EffectStatus::Observed
314 );
315 assert_eq!(
316 evaluate(&Command::Stop, &status("FINISH", 0), Some(0)),
317 EffectStatus::Observed
318 );
319 assert_eq!(
321 evaluate(&Command::Pause, &status("RUNNING", 0), Some(0)),
322 EffectStatus::Pending
323 );
324 }
325
326 #[test]
327 fn stop_tolerates_a_transient_abort_error() {
328 assert_eq!(
332 evaluate(&Command::Stop, &status("FAILED", 0x0300400C), Some(0)),
333 EffectStatus::Observed
334 );
335 assert_eq!(
337 evaluate(&Command::Stop, &status("PAUSE", 0x0300400C), Some(0)),
338 EffectStatus::Pending
339 );
340 }
341
342 #[test]
343 fn a_new_error_beats_an_otherwise_observed_effect() {
344 assert_eq!(
347 evaluate(&Command::Resume, &status("RUNNING", 0x0500C010), Some(0)),
348 EffectStatus::NewError(0x0500C010)
349 );
350 }
351}