1use std::time::Duration;
9
10use crate::client::LanMqttClient;
11use crate::config::ResolvedTarget;
12use crate::core::command::{AmsControl, Command as ProtoCommand, LedNode, SpeedLevel};
13use crate::core::session::{CommandOutcome, VerifyStage};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum HomeAxes {
18 All,
19 X,
20 Y,
21 Z,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum Axis {
27 X,
28 Y,
29 Z,
30}
31
32impl Axis {
33 pub fn as_str(self) -> &'static str {
35 match self {
36 Axis::X => "X",
37 Axis::Y => "Y",
38 Axis::Z => "Z",
39 }
40 }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum TempPart {
46 Nozzle,
47 Bed,
48}
49
50pub fn temp_line(part: TempPart, celsius: u32) -> String {
52 match part {
53 TempPart::Nozzle => format!("M104 S{celsius}"),
54 TempPart::Bed => format!("M140 S{celsius}"),
55 }
56}
57
58#[derive(Debug, Clone)]
60pub enum ControlAction {
61 Pause,
62 Resume,
63 Stop,
64 ClearError,
67 Light {
68 node: LedNode,
69 on: bool,
70 },
71 Speed(SpeedLevel),
72 Gcode(String),
73 Home(HomeAxes),
74 Move {
75 axis: Axis,
76 delta: f64,
77 feedrate: u32,
78 },
79 Extrude {
80 delta: f64,
81 feedrate: u32,
82 },
83 SetTemp {
84 part: TempPart,
85 celsius: u32,
86 },
87 Calibrate {
88 bed_level: bool,
89 vibration: bool,
90 motor_noise: bool,
91 },
92 Ams(AmsControl),
93 AmsChange {
97 target: u32,
98 curr_temp: i64,
99 tar_temp: i64,
100 },
101 Reboot,
102 DisableSteppers,
103}
104
105impl ControlAction {
106 fn into_command(self) -> ProtoCommand {
107 match self {
108 ControlAction::Pause => ProtoCommand::Pause,
109 ControlAction::Resume => ProtoCommand::Resume,
110 ControlAction::Stop => ProtoCommand::Stop,
111 ControlAction::ClearError => ProtoCommand::CleanPrintError,
112 ControlAction::Light { node, on } => ProtoCommand::Led { node, on },
113 ControlAction::Speed(level) => ProtoCommand::PrintSpeed(level),
114 ControlAction::Gcode(line) => ProtoCommand::GcodeLine(line),
115 ControlAction::Home(axes) => ProtoCommand::GcodeLine(
116 match axes {
117 HomeAxes::All => "G28",
118 HomeAxes::X => "G28 X",
119 HomeAxes::Y => "G28 Y",
120 HomeAxes::Z => "G28 Z",
121 }
122 .to_string(),
123 ),
124 ControlAction::Move {
127 axis,
128 delta,
129 feedrate,
130 } => ProtoCommand::GcodeLine(format!(
131 "G91\nG1 {}{delta} F{feedrate}\nG90",
132 axis.as_str()
133 )),
134 ControlAction::Extrude { delta, feedrate } => {
136 ProtoCommand::GcodeLine(format!("M83\nG1 E{delta} F{feedrate}\nM82"))
137 }
138 ControlAction::SetTemp { part, celsius } => {
139 ProtoCommand::GcodeLine(temp_line(part, celsius))
140 }
141 ControlAction::Calibrate {
142 bed_level,
143 vibration,
144 motor_noise,
145 } => ProtoCommand::Calibration {
146 bed_level,
147 vibration,
148 motor_noise,
149 },
150 ControlAction::Ams(action) => ProtoCommand::AmsControl(action),
151 ControlAction::AmsChange {
152 target,
153 curr_temp,
154 tar_temp,
155 } => ProtoCommand::AmsChangeFilament {
156 target,
157 curr_temp,
158 tar_temp,
159 },
160 ControlAction::Reboot => ProtoCommand::Reboot,
161 ControlAction::DisableSteppers => ProtoCommand::GcodeLine("M84".to_string()),
162 }
163 }
164
165 fn needs_verify(&self) -> bool {
168 !matches!(self, ControlAction::Reboot)
169 }
170}
171
172#[derive(Debug)]
175pub enum ControlError {
176 Transport(String),
178}
179
180pub type ControlResult = Result<CommandOutcome, ControlError>;
181
182pub trait Controller: Send + Sync {
184 fn execute(&self, action: ControlAction) -> ControlResult;
185}
186
187pub struct LiveController {
189 target: ResolvedTarget,
190 timeout: Duration,
191}
192
193impl LiveController {
194 pub fn new(target: ResolvedTarget) -> Self {
195 Self {
196 target,
197 timeout: Duration::from_secs(15),
198 }
199 }
200}
201
202impl Controller for LiveController {
203 fn execute(&self, action: ControlAction) -> ControlResult {
204 let client = LanMqttClient::new(self.target.clone()).with_timeout(self.timeout);
205 let needs_verify = action.needs_verify();
206 let cmd = action.into_command();
207 if needs_verify {
208 client
209 .send_and_verify(&cmd)
210 .map_err(|e| ControlError::Transport(e.to_string()))
211 } else {
212 client
214 .send_fire(&cmd)
215 .map(|()| CommandOutcome::Unverified {
216 stage: VerifyStage::Ack,
217 })
218 .map_err(|e| ControlError::Transport(e.to_string()))
219 }
220 }
221}
222
223pub struct FakeController {
225 outcome: Result<CommandOutcome, String>,
226}
227
228impl FakeController {
229 pub fn verified() -> Self {
231 Self {
232 outcome: Ok(CommandOutcome::Verified),
233 }
234 }
235
236 #[cfg(test)]
237 pub fn returning(outcome: CommandOutcome) -> Self {
238 Self {
239 outcome: Ok(outcome),
240 }
241 }
242
243 #[cfg(test)]
244 pub fn failing() -> Self {
245 Self {
246 outcome: Err("fake transport failure".to_string()),
247 }
248 }
249}
250
251impl Controller for FakeController {
252 fn execute(&self, _action: ControlAction) -> ControlResult {
253 match &self.outcome {
254 Ok(o) => Ok(o.clone()),
255 Err(e) => Err(ControlError::Transport(e.clone())),
256 }
257 }
258}