1use serde::Serialize;
20use serde_json::Value;
21
22use crate::core::command::Command;
23use crate::core::report::{ReportState, is_full_snapshot_message};
24use crate::core::status::PrinterStatus;
25use crate::core::verify::{self, EffectStatus};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
29#[serde(rename_all = "snake_case")]
30pub enum VerifyStage {
31 Ack,
33 Effect,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
45#[serde(tag = "outcome", rename_all = "snake_case")]
46pub enum CommandOutcome {
47 Verified,
49 Rejected { reason: String },
52 Unverified { stage: VerifyStage },
56}
57
58pub struct VerifySession {
60 cmd: Command,
61 seq: String,
62 state: ReportState,
63 acked: bool,
64 baseline_error: Option<i64>,
65}
66
67impl VerifySession {
68 pub fn new(cmd: Command, seq: impl Into<String>) -> Self {
70 Self {
71 cmd,
72 seq: seq.into(),
73 state: ReportState::new(),
74 acked: false,
75 baseline_error: None,
76 }
77 }
78
79 pub fn observe(&mut self, message: Value) -> Option<CommandOutcome> {
83 let full = is_full_snapshot_message(&message);
84 self.state.apply(message);
85
86 if self.baseline_error.is_none() && full {
89 self.baseline_error = Some(
90 PrinterStatus::from_state(self.state.get())
91 .print_error
92 .unwrap_or(0),
93 );
94 }
95
96 let cat = self.cmd.category();
97
98 if !self.acked {
100 let echoed = self
101 .state
102 .pointer(&format!("/{cat}/sequence_id"))
103 .and_then(|v| v.as_str())
104 == Some(self.seq.as_str());
105 if let (true, Some(result)) = (
106 echoed,
107 self.state
108 .pointer(&format!("/{cat}/result"))
109 .and_then(|v| v.as_str()),
110 ) {
111 if !result.eq_ignore_ascii_case("success") {
112 let reason = self
113 .state
114 .pointer(&format!("/{cat}/reason"))
115 .and_then(|v| v.as_str())
116 .unwrap_or(result)
117 .to_string();
118 return Some(CommandOutcome::Rejected { reason });
119 }
120 self.acked = true;
121 if !verify::has_observable_effect(&self.cmd) {
123 return Some(CommandOutcome::Verified);
124 }
125 }
126 }
127
128 if self.acked {
130 let status = PrinterStatus::from_state(self.state.get());
131 match verify::evaluate(&self.cmd, &status, self.baseline_error) {
132 EffectStatus::Observed => return Some(CommandOutcome::Verified),
133 EffectStatus::NewError(code) => {
134 return Some(CommandOutcome::Rejected {
135 reason: format!(
136 "device reported error 0x{code:08X} after the command; \
137 the effect was not observed (state unchanged)"
138 ),
139 });
140 }
141 EffectStatus::Pending => {}
142 }
143 }
144 None
145 }
146
147 pub fn timed_out(&self) -> CommandOutcome {
150 CommandOutcome::Unverified {
151 stage: if self.acked {
152 VerifyStage::Effect
153 } else {
154 VerifyStage::Ack
155 },
156 }
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use crate::core::command::{Command, ProjectFile, SpeedLevel};
164 use crate::core::fake::FakePrinter;
165
166 fn project() -> Command {
167 Command::ProjectFile(ProjectFile::new("ftp:///model/x.gcode.3mf", 1, "x"))
168 }
169
170 fn run(cmd: Command, seq: &str, msgs: Vec<Value>) -> CommandOutcome {
173 let mut s = VerifySession::new(cmd, seq);
174 for m in msgs {
175 if let Some(o) = s.observe(m) {
176 return o;
177 }
178 }
179 s.timed_out()
180 }
181
182 #[test]
183 fn print_start_acked_and_running_is_verified() {
184 let mut p = FakePrinter::idle();
185 let msgs = vec![
186 p.snapshot(),
187 p.ack(&project(), "1", true, "success"),
188 p.effect_delta(&project()), ];
190 assert_eq!(run(project(), "1", msgs), CommandOutcome::Verified);
191 }
192
193 #[test]
194 fn rejected_ack_is_rejected_with_reason() {
195 let p = FakePrinter::idle();
196 let msgs = vec![
197 p.snapshot(),
198 p.ack(&project(), "1", false, "print_id error"),
199 ];
200 assert_eq!(
201 run(project(), "1", msgs),
202 CommandOutcome::Rejected {
203 reason: "print_id error".to_string()
204 }
205 );
206 }
207
208 #[test]
209 fn acked_but_no_effect_times_out_as_unverified_effect() {
210 let p = FakePrinter::idle();
211 let msgs = vec![p.snapshot(), p.ack(&project(), "1", true, "success")];
213 assert_eq!(
214 run(project(), "1", msgs),
215 CommandOutcome::Unverified {
216 stage: VerifyStage::Effect
217 }
218 );
219 }
220
221 #[test]
222 fn never_acked_times_out_as_unverified_ack() {
223 let p = FakePrinter::idle();
224 let msgs = vec![p.snapshot()];
226 assert_eq!(
227 run(project(), "1", msgs),
228 CommandOutcome::Unverified {
229 stage: VerifyStage::Ack
230 }
231 );
232 }
233
234 #[test]
235 fn new_print_error_after_command_is_rejected() {
236 let mut p = FakePrinter::idle();
237 let msgs = vec![
238 p.snapshot(),
239 p.ack(&project(), "1", true, "success"),
240 p.new_error_delta(0x0500C010), ];
242 assert!(matches!(
243 run(project(), "1", msgs),
244 CommandOutcome::Rejected { .. }
245 ));
246 }
247
248 #[test]
249 fn preexisting_error_is_not_blamed_on_the_command() {
250 let p = FakePrinter::with_error(0x0500C010);
253 let msgs = vec![p.snapshot(), p.ack(&project(), "1", true, "success")];
254 assert_eq!(
255 run(project(), "1", msgs),
256 CommandOutcome::Unverified {
257 stage: VerifyStage::Effect
258 }
259 );
260 }
261
262 #[test]
263 fn ack_only_command_is_verified_on_ack_without_effect() {
264 let cmd = Command::GcodeLine("G28".to_string());
266 let p = FakePrinter::idle();
267 let msgs = vec![p.snapshot(), p.ack(&cmd, "1", true, "success")];
268 assert_eq!(run(cmd, "1", msgs), CommandOutcome::Verified);
269 }
270
271 #[test]
272 fn effect_verified_via_spd_lvl_for_print_speed() {
273 let cmd = Command::PrintSpeed(SpeedLevel::Sport);
274 let mut p = FakePrinter::idle();
275 let msgs = vec![
276 p.snapshot(),
277 p.ack(&cmd, "1", true, "success"),
278 p.effect_delta(&cmd), ];
280 assert_eq!(run(cmd, "1", msgs), CommandOutcome::Verified);
281 }
282}