diff --git a/Cargo.lock b/Cargo.lock index e8b17ac..2555634 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -208,9 +208,9 @@ dependencies = [ [[package]] name = "glam" -version = "0.20.2" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4fa84eead97d5412b2a20aed4d66612a97a9e41e08eababdb9ae2bf88667490" +checksum = "518faa5064866338b013ff9b2350dc318e14cc4fcd6cb8206d7e7c9886c98815" dependencies = [ "serde", ] @@ -430,9 +430,11 @@ dependencies = [ "glam", "lazy_static", "lexical-core", + "minijinja", "nom", "regex", "serde", + "serde_json", "thiserror", ] @@ -503,12 +505,29 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +[[package]] +name = "memo-map" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aec276c09560ce4447087aaefc19eb0c18d97e31bd05ebac38881c4723400c40" + [[package]] name = "mime" version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "minijinja" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b118c4731e2ecab6e12019ca1b517eab16447e1ab0e1149faa60d259dd3d57" +dependencies = [ + "memo-map", + "self_cell", + "serde", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -777,6 +796,12 @@ dependencies = [ "base64", ] +[[package]] +name = "rustversion" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" + [[package]] name = "ryu" version = "1.0.9" @@ -793,6 +818,15 @@ dependencies = [ "untrusted", ] +[[package]] +name = "self_cell" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ef965a420fe14fdac7dd018862966a4c14094f900e1650bbc71ddd7d580c8af" +dependencies = [ + "rustversion", +] + [[package]] name = "serde" version = "1.0.133" diff --git a/lib/Cargo.toml b/lib/Cargo.toml index f226b57..7c88123 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -10,7 +10,9 @@ license = "MIT" nom = "7" lexical-core = "^0.7" serde = { version = "^1", features = ["derive"] } -glam = { version = "0.20", features = ["serde"] } +glam = { version = "0.21", features = ["serde"] } thiserror = "1" regex = "1" lazy_static = "1" +minijinja = { version = "^0.18", default-features = false, features = ["source", "sync", "debug"] } +serde_json = "^1" diff --git a/lib/src/firmware_retraction.rs b/lib/src/firmware_retraction.rs index 75f75f8..6550074 100644 --- a/lib/src/firmware_retraction.rs +++ b/lib/src/firmware_retraction.rs @@ -18,7 +18,7 @@ pub struct FirmwareRetractionOptions { pub lift_z: f64, } -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] pub enum FirmwareRetractionState { Unretracted, Retracted { diff --git a/lib/src/gcode.rs b/lib/src/gcode.rs index 1e966d3..cd7b6d6 100644 --- a/lib/src/gcode.rs +++ b/lib/src/gcode.rs @@ -2,6 +2,7 @@ use std::collections::BTreeMap; use std::fmt::Display; use std::io::{self, BufRead}; +use serde::Serialize; use thiserror::Error; #[derive(Debug, PartialEq, PartialOrd, Clone)] @@ -81,7 +82,7 @@ impl Display for GCodeOperation { } } -#[derive(Debug, PartialEq, PartialOrd, Clone)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)] pub struct GCodeTraditionalParams(Vec<(char, String)>); impl GCodeTraditionalParams { @@ -121,7 +122,7 @@ impl Display for GCodeTraditionalParams { } } -#[derive(Debug, PartialEq, PartialOrd, Clone)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Serialize)] pub struct GCodeExtendedParams(BTreeMap); impl GCodeExtendedParams { @@ -279,7 +280,7 @@ mod parser { fn parse(s: &str) -> IResult<&str, GCodeCommand> { let (s, _) = space0(s)?; - let (s, _line_no) = opt(line_number)(s)?; + // let (s, _line_no) = opt(line_number)(s)?; let (s, (op, comment)) = alt(( complete(traditional_gcode), @@ -314,7 +315,7 @@ mod parser { } fn traditional_gcode(s: &str) -> IResult<&str, (GCodeOperation, Option<&str>)> { - let (s, letter) = satisfy(|c| c.is_alphabetic())(s)?; + let (s, letter) = satisfy(|c| matches!(c.to_ascii_lowercase(), 'g' | 'm'))(s)?; let (s, code) = match lexical_core::parse_partial::(s.as_bytes()) { Ok((_, 0)) => return Err(Err::Error(Error::from_error_kind(s, ErrorKind::Digit))), Ok((value, processed)) => (s.slice(processed..), value), diff --git a/lib/src/lib.rs b/lib/src/lib.rs index e9ad168..412e20f 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -4,6 +4,7 @@ extern crate lazy_static; pub mod firmware_retraction; pub mod gcode; mod kind_tracker; +mod macros; pub mod planner; pub mod slicer; diff --git a/lib/src/macros.rs b/lib/src/macros.rs new file mode 100644 index 0000000..2143f18 --- /dev/null +++ b/lib/src/macros.rs @@ -0,0 +1,287 @@ +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +use glam::DVec3 as Vec3; +use glam::{DVec4, Vec4Swizzles}; + +use regex::Regex; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Default, Clone)] +struct MacroToolheadPosition(Arc>); + +impl MacroToolheadPosition { + fn update(&self, position: DVec4) { + let mut toolhead_position = self.0.lock().unwrap(); + *toolhead_position = Position(position.xyz()); + } +} + +#[derive(Debug)] +struct PrinterObj { + config_map: HashMap, + toolhead_position: MacroToolheadPosition, + axis_maximum: Position, +} + +impl std::fmt::Display for PrinterObj { + fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + unimplemented!() + } +} + +impl minijinja::value::Object for PrinterObj { + fn get_attr(&self, name: &str) -> Option { + match name { + "toolhead" => Some(minijinja::value::Value::from_serializable( + &self.get_toolhead(), + )), + name => self.config_map.get(name).map(|v| v.to_owned()), + } + } + + fn attributes(&self) -> &[&str] { + unimplemented!() + } +} + +#[derive(Debug, Clone, Copy, Default, Serialize)] +struct Position(Vec3); + +#[derive(Debug, Serialize)] +struct Toolhead { + position: Position, + axis_maximum: Position, + homed_axes: String, +} + +impl PrinterObj { + fn get_toolhead(&self) -> Toolhead { + let position = *self.toolhead_position.0.lock().unwrap(); + Toolhead { + position, + axis_maximum: self.axis_maximum, + homed_axes: "xyz".to_owned(), + } + } + + fn new( + config: &crate::planner::MacroConfiguration, + toolhead_position: MacroToolheadPosition, + ) -> Self { + #[derive(Debug, Serialize)] + struct Toolhead { + position: Position, + axis_maximum: Position, + homed_axes: String, + } + + let axis_maximum = Position(Vec3::new( + config.config["stepper_x"]["position_max"].as_f64().unwrap() as f64, + config.config["stepper_y"]["position_max"].as_f64().unwrap() as f64, + config.config["stepper_z"]["position_max"].as_f64().unwrap() as f64, + )); + + let mut cfg: HashMap = config + .config + .iter() + .map(|(k, v)| (k.to_owned(), minijinja::value::Value::from_serializable(v))) + .collect(); + + for mac in &config.macros { + let mut name = "gcode_macro ".to_owned(); + name.push_str(&mac.name); + let values = minijinja::value::Value::from_serializable(&mac.variables); + cfg.insert(name, values); + } + + PrinterObj { + config_map: cfg, + toolhead_position, + axis_maximum, + } + } +} + +fn convert_macro_string(gcode: &str) -> String { + lazy_static! { + static ref RE_BRACES: Regex = + Regex::new(r"(?x) ( \{\S\} ) | ( \{[^%] ) | ( [^%]\} )").unwrap(); + } + + RE_BRACES + .replace_all(gcode, |cap: ®ex::Captures| { + if let Some(m) = cap.get(1) { + "{".to_owned() + m.as_str() + "}" + } else if let Some(m) = cap.get(2) { + "{".to_owned() + m.as_str() + } else if let Some(m) = cap.get(3) { + m.as_str().to_owned() + "}" + } else { + unreachable!() + } + }) + .to_ascii_lowercase() +} + +fn read_macros<'a, I>(macros: I) -> minijinja::Source +where + I: IntoIterator, +{ + let mut src = minijinja::Source::new(); + + for mac in macros { + src.add_template(&mac.name, convert_macro_string(&mac.gcode)) + .unwrap(); + } + + src +} + +fn create_macro_environment(src: minijinja::Source) -> minijinja::Environment<'static> { + fn any_id( + _state: &minijinja::State, + value: minijinja::value::Value, + ) -> Result { + Ok(value) + } + + fn min(_state: &minijinja::State, value: Vec) -> Result + where + Item: Ord, + { + value.into_iter().min().ok_or_else(|| { + minijinja::Error::new( + minijinja::ErrorKind::InvalidArguments, + "can't compute minimum of empty list", + ) + }) + } + + fn max(_state: &minijinja::State, value: Vec) -> Result + where + Item: Ord, + { + value.into_iter().max().ok_or_else(|| { + minijinja::Error::new( + minijinja::ErrorKind::InvalidArguments, + "can't compute maximum of empty list", + ) + }) + } + + fn action_nop( + _state: &minijinja::State, + _value: minijinja::value::Value, + ) -> Result<(), minijinja::Error> { + Ok(()) + } + + let mut env = minijinja::Environment::new(); + + env.set_source(src); + + #[cfg(debug_assertions)] + env.set_debug(true); + + env.add_filter("float", any_id); + env.add_filter("int", any_id); + env.add_filter("min", min::); + env.add_filter("max", max::); + + env.add_function("action_respond_info", action_nop); + env.add_function("action_raise_error", action_nop); + + env +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(default)] +pub struct GCodeMacro +where + S: Serialize, +{ + pub name: String, + pub description: Option, + pub variables: HashMap, + pub gcode: String, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(default)] +pub struct MacroConfiguration { + pub macros: Vec, + pub config: HashMap, +} + +#[derive(Debug)] +pub(crate) struct MacroManager { + macro_variables: HashMap>, + macro_environment: minijinja::Environment<'static>, + toolhead_position: crate::macros::MacroToolheadPosition, +} + +impl MacroManager { + pub fn new(config: MacroConfiguration) -> Self { + let toolhead_position = MacroToolheadPosition::default(); + let printer_obj = PrinterObj::new(&config, toolhead_position.clone()); + + let src = read_macros(&config.macros); + + let mut macro_environment = create_macro_environment(src); + macro_environment.add_global("printer", minijinja::value::Value::from_object(printer_obj)); + + let macro_variables = config + .macros + .iter() + .map(|mac| { + ( + mac.name.to_owned(), + mac.variables + .iter() + .map(|(k, v)| (k.to_owned(), minijinja::value::Value::from_serializable(v))) + .collect(), + ) + }) + .collect(); + + Self { + macro_variables, + macro_environment, + toolhead_position, + } + } + + pub fn render_macro( + &self, + name: &str, + params: crate::gcode::GCodeExtendedParams, + position: DVec4, + ) -> Option { + let vars = self.macro_variables.get(name)?.to_owned(); + + // Update toolhead position shared with global PrinterObj + self.toolhead_position.update(position); + + #[derive(Debug, Serialize)] + struct MacroParams { + params: crate::gcode::GCodeExtendedParams, + #[serde(flatten)] + variables: HashMap, + } + + let inner_params = MacroParams { + params, + variables: vars, + }; + + let template = self.macro_environment.get_template(name).ok()?; + match template.render(inner_params) { + Ok(rendered) => Some(rendered), + Err(e) => { + eprintln!("error during template: err={:?}", e); + None + } + } + } +} diff --git a/lib/src/planner.rs b/lib/src/planner.rs index eeaa4b3..6e59c92 100644 --- a/lib/src/planner.rs +++ b/lib/src/planner.rs @@ -18,20 +18,26 @@ pub struct Planner { pub kind_tracker: KindTracker, pub current_kind: Option, pub firmware_retraction: Option, + macro_manager: crate::macros::MacroManager, } impl Planner { - pub fn from_limits(limits: PrinterLimits) -> Planner { - let firmware_retraction = limits + pub fn from_limits(config: PrinterConfiguration) -> Self { + let firmware_retraction = config + .limits .firmware_retraction .as_ref() .map(|_| FirmwareRetractionState::default()); + + let macro_manager = crate::macros::MacroManager::new(config.macro_config); + Planner { operations: OperationSequence::default(), - toolhead_state: ToolheadState::from_limits(limits), + toolhead_state: ToolheadState::from_limits(config.limits), kind_tracker: KindTracker::new(), current_kind: None, firmware_retraction, + macro_manager, } } @@ -143,7 +149,26 @@ impl Planner { fr.set_options(m, params); } } - _ => {} + "save_gcode_state" => { + // todo!() + } + "restore_gcode_state" => { + // todo!() + } + other => { + if let Some(rendered_macro) = self.macro_manager.render_macro( + other, + params.clone(), + self.toolhead_state.position, + ) { + for cmd_line in rendered_macro.lines() { + let gc = crate::gcode::parse_gcode(cmd_line).unwrap(); + self.process_cmd(&gc); + } + } else { + eprintln!("unknown cmd={}", other); + } + } } self.operations.add_fill(); } else if cmd.op.is_nop() && cmd.comment.is_some() { @@ -611,7 +636,7 @@ impl MoveSequence { } fn last_move(&self) -> Option<&PlanningMove> { - self.moves.iter().rev().find_map(|o| match o { + self.moves.back().and_then(|o| match o { MoveSequenceOperation::Move(m) => Some(m), _ => None, }) @@ -680,12 +705,17 @@ impl MoveSequence { next_end_v2 = start_v2; next_smoothed_v2 = smoothed_v2; } + // if !delayed.is_empty() { + // eprintln!("del sz={}", delayed.len()); + // } } if update_flush_count { self.flush_count = 0; } + drop(delayed); + // Advance while the next operation is a fill while self.flush_count < self.moves.len() && self.moves[self.flush_count].is_fill() { self.flush_count += 1; @@ -741,6 +771,16 @@ impl Default for PrinterLimits { } } +// Deserialize +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(default)] +pub struct PrinterConfiguration { + pub limits: PrinterLimits, + pub macro_config: MacroConfiguration, +} + +pub use crate::macros::{GCodeMacro, MacroConfiguration}; + impl PrinterLimits { pub fn set_max_velocity(&mut self, v: f64) { self.max_velocity = v; @@ -768,7 +808,7 @@ impl PrinterLimits { fn scv_to_jd(scv: f64, acceleration: f64) -> f64 { let scv2 = scv * scv; - scv2 * (2.0f64.sqrt() - 1.0) / acceleration + scv2 * (std::f64::consts::SQRT_2 - 1.0) / acceleration } } @@ -784,7 +824,7 @@ impl Default for PositionMode { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct ToolheadState { pub position: Vec4, pub position_modes: [PositionMode; 4], diff --git a/tool/src/main.rs b/tool/src/main.rs index b907a88..b5c14ff 100644 --- a/tool/src/main.rs +++ b/tool/src/main.rs @@ -1,18 +1,17 @@ use std::error::Error; -use lib_klipper::glam::DVec3; -use lib_klipper::planner::{FirmwareRetractionOptions, MoveChecker, Planner, PrinterLimits}; +use lib_klipper::planner::{Planner, PrinterConfiguration, PrinterLimits}; use clap::Parser; use once_cell::sync::OnceCell; -use serde::Deserialize; -use thiserror::Error; -use url::Url; + #[macro_use] extern crate lazy_static; mod cmd; +mod moonraker; + #[derive(Parser, Debug)] #[clap(version = env!("TOOL_VERSION"), author = "Lasse Dalegaard ")] pub struct Opts { @@ -29,11 +28,11 @@ pub struct Opts { cmd: SubCommand, #[clap(skip)] - config: OnceCell, + config: OnceCell, } impl Opts { - fn printer_limits(&self) -> &PrinterLimits { + fn printer_limits(&self) -> &PrinterConfiguration { match self.config.get() { Some(limits) => limits, None => match self.load_config() { @@ -49,26 +48,37 @@ impl Opts { } } - fn load_config(&self) -> Result> { + fn load_config(&self) -> Result> { // Load config file - let mut limits = if let Some(filename) = &self.config_filename { - let src = std::fs::read_to_string(filename)?; - let mut limits: PrinterLimits = deser_hjson::from_str(&src)?; - - // Do any fix-ups - limits.set_square_corner_velocity(limits.square_corner_velocity); - - limits - } else { - PrinterLimits::default() - }; + // let mut limits = if let Some(filename) = &self.config_filename { + // let src = std::fs::read_to_string(filename)?; + // let mut limits: PrinterLimits = deser_hjson::from_str(&src)?; + // + // // Do any fix-ups + // limits.set_square_corner_velocity(limits.square_corner_velocity); + // + // Ok(PrinterConfiguration{ + // limits, + // macros: vec![] + // }) + // } else { + // Ok(PrinterConfiguration::default()) + // }; // Was moonraker config requested? If so, try to grab that first. if let Some(url) = &self.config_moonraker { - moonraker_config(url, self.config_moonraker_api_key.as_deref(), &mut limits)?; + let mut limits = PrinterLimits::default(); + let config = moonraker::moonraker_config( + url, + self.config_moonraker_api_key.as_deref(), + &mut limits, + )?; + Ok(config) + } else { + Ok(PrinterConfiguration::default()) } - Ok(limits) + // Ok(limits) } fn make_planner(&self) -> Planner { @@ -76,153 +86,6 @@ impl Opts { } } -#[derive(Error, Debug)] -pub enum MoonrakerConfigError { - #[error("given URL cannot be a base URL")] - URLCannotBeBase, - #[error("invalid URL: {}", .0)] - URLParseError(#[from] url::ParseError), - #[error("request failed: {}", .0)] - RequestError(#[from] reqwest::Error), -} - -fn moonraker_config( - source_url: &str, - api_key: Option<&str>, - target: &mut PrinterLimits, -) -> Result<(), MoonrakerConfigError> { - let mut url = Url::parse(source_url)?; - url.query_pairs_mut().append_pair("configfile", "settings"); - { - let mut path = url - .path_segments_mut() - .map_err(|_| MoonrakerConfigError::URLCannotBeBase)?; - path.extend(&["printer", "objects", "query"]); - } - - #[derive(Debug, Deserialize)] - struct MoonrakerResultRoot { - result: MoonrakerResult, - } - - #[derive(Debug, Deserialize)] - struct MoonrakerResult { - status: MoonrakerResultStatus, - } - - #[derive(Debug, Deserialize)] - struct MoonrakerResultStatus { - configfile: MoonrakerConfigFile, - } - - #[derive(Debug, Deserialize)] - struct MoonrakerConfigFile { - settings: MoonrakerConfig, - } - - #[derive(Debug, Deserialize)] - struct MoonrakerConfig { - printer: PrinterConfig, - extruder: ExtruderConfig, - firmware_retraction: Option, - } - - #[derive(Debug, Deserialize)] - struct PrinterConfig { - max_velocity: f64, - max_accel: f64, - max_accel_to_decel: f64, - square_corner_velocity: f64, - - max_x_velocity: Option, - max_x_accel: Option, - max_y_velocity: Option, - max_y_accel: Option, - max_z_velocity: Option, - max_z_accel: Option, - } - - #[derive(Debug, Deserialize)] - struct ExtruderConfig { - max_extrude_only_velocity: f64, - max_extrude_only_accel: f64, - instantaneous_corner_velocity: f64, - } - - #[derive(Debug, Deserialize)] - struct FirmwareRetractionConfig { - retract_length: f64, - unretract_extra_length: f64, - unretract_speed: f64, - retract_speed: f64, - #[serde(default)] - lift_z: f64, - } - - let client = reqwest::blocking::Client::new(); - let mut req = client.get(url); - - if let Some(api_key) = api_key { - req = req.header("X-Api-Key", api_key); - } - - let cfg = req - .send()? - .json::()? - .result - .status - .configfile - .settings; - - target.set_max_velocity(cfg.printer.max_velocity); - target.set_max_acceleration(cfg.printer.max_accel); - target.set_max_accel_to_decel(cfg.printer.max_accel_to_decel); - target.set_square_corner_velocity(cfg.printer.square_corner_velocity); - target.set_instant_corner_velocity(cfg.extruder.instantaneous_corner_velocity); - - target.firmware_retraction = cfg.firmware_retraction.map(|fr| FirmwareRetractionOptions { - retract_length: fr.retract_length, - unretract_extra_length: fr.unretract_extra_length, - unretract_speed: fr.unretract_speed, - retract_speed: fr.retract_speed, - lift_z: fr.lift_z, - }); - - let limits = [ - ( - DVec3::X, - cfg.printer.max_x_velocity, - cfg.printer.max_x_accel, - ), - ( - DVec3::Y, - cfg.printer.max_y_velocity, - cfg.printer.max_y_accel, - ), - ( - DVec3::Z, - cfg.printer.max_z_velocity, - cfg.printer.max_z_accel, - ), - ]; - - for (axis, m, a) in limits { - if let (Some(max_velocity), Some(max_accel)) = (m, a) { - target.move_checkers.push(MoveChecker::AxisLimiter { - axis, - max_velocity, - max_accel, - }); - } - } - - target.move_checkers.push(MoveChecker::ExtruderLimiter { - max_velocity: cfg.extruder.max_extrude_only_velocity, - max_accel: cfg.extruder.max_extrude_only_accel, - }); - Ok(()) -} - #[derive(Parser, Debug)] enum SubCommand { Estimate(cmd::estimate::EstimateCmd), diff --git a/tool/src/moonraker.rs b/tool/src/moonraker.rs new file mode 100644 index 0000000..8d29fdc --- /dev/null +++ b/tool/src/moonraker.rs @@ -0,0 +1,238 @@ +use std::collections::HashMap; + +use lib_klipper::glam::DVec3; +use lib_klipper::planner::{ + FirmwareRetractionOptions, GCodeMacro, MacroConfiguration, MoveChecker, PrinterConfiguration, + PrinterLimits, +}; + +use serde::Deserialize; +use thiserror::Error; +use url::Url; + +#[derive(Debug, Deserialize)] +struct MoonrakerResultRoot { + result: MoonrakerResult, +} + +#[derive(Debug, Deserialize)] +struct MoonrakerResult { + status: MoonrakerResultStatus, +} + +#[derive(Debug, Deserialize)] +struct MoonrakerResultStatus { + configfile: MoonrakerConfigFile, +} + +#[derive(Debug, Deserialize)] +struct MoonrakerConfigFile { + // settings: MoonrakerConfig, + settings: HashMap, +} + +#[derive(Debug, Deserialize)] +struct MoonrakerConfig { + printer: PrinterConfig, + extruder: ExtruderConfig, + firmware_retraction: Option, +} + +#[derive(Debug, Deserialize)] +struct PrinterConfig { + max_velocity: f64, + max_accel: f64, + max_accel_to_decel: f64, + square_corner_velocity: f64, + + max_x_velocity: Option, + max_x_accel: Option, + max_y_velocity: Option, + max_y_accel: Option, + max_z_velocity: Option, + max_z_accel: Option, +} + +#[derive(Debug, Deserialize)] +struct ExtruderConfig { + max_extrude_only_velocity: f64, + max_extrude_only_accel: f64, + instantaneous_corner_velocity: f64, +} + +#[derive(Debug, Deserialize)] +struct FirmwareRetractionConfig { + retract_length: f64, + unretract_extra_length: f64, + unretract_speed: f64, + retract_speed: f64, + #[serde(default)] + lift_z: f64, +} + +#[derive(Error, Debug)] +pub enum MoonrakerConfigError { + #[error("given URL cannot be a base URL")] + URLCannotBeBase, + #[error("invalid URL: {}", .0)] + URLParseError(#[from] url::ParseError), + #[error("request failed: {}", .0)] + RequestError(#[from] reqwest::Error), +} + +fn query_moonraker( + source_url: &str, + api_key: Option<&str>, +) -> Result { + let mut url = Url::parse(source_url)?; + url.query_pairs_mut().append_pair("configfile", "settings"); + { + let mut path = url + .path_segments_mut() + .map_err(|_| MoonrakerConfigError::URLCannotBeBase)?; + path.extend(&["printer", "objects", "query"]); + } + + let client = reqwest::blocking::Client::new(); + let mut req = client.get(url); + + if let Some(api_key) = api_key { + req = req.header("X-Api-Key", api_key); + } + + Ok(req + .send()? + .json::()? + .result + .status + .configfile) +} + +pub fn moonraker_config( + source_url: &str, + api_key: Option<&str>, + target: &mut PrinterLimits, +) -> Result { + let cfg_json = query_moonraker(source_url, api_key)?.settings; + + let cfg = { + let cfg_printer: PrinterConfig = { + let cfg_section = cfg_json.get("printer").unwrap().to_owned(); + serde_json::from_value(cfg_section).unwrap() + }; + let cfg_extruder: ExtruderConfig = { + let cfg_section = cfg_json.get("extruder").unwrap().to_owned(); + serde_json::from_value(cfg_section).unwrap() + }; + let cfg_firmware_retraction: Option = { + if let Some(cfg_section) = cfg_json.get("firmware_retraction") { + serde_json::from_value(cfg_section.to_owned()).unwrap() + } else { + None + } + }; + MoonrakerConfig { + printer: cfg_printer, + extruder: cfg_extruder, + firmware_retraction: cfg_firmware_retraction, + } + }; + + fn deserialize_macro<'a>(key: &'a str, val: &'a serde_json::Value) -> Result { + let name = key + .strip_prefix("gcode_macro ") + .map(|x| x.to_owned()) + .ok_or(())?; + let description = val["description"].as_str().map(|x| x.to_owned()); + let gcode = val["gcode"].as_str().map(|x| x.to_owned()).ok_or(())?; + + fn fixup_value(val: serde_json::Value) -> serde_json::Value { + let strval = val.as_str().unwrap().to_owned().to_lowercase(); + let new_val = serde_json::from_str::(&strval); + match new_val { + Ok(new_val) => new_val, + Err(_) => val, + } + } + + let val_obj = val.as_object().ok_or(())?; + let variables: HashMap = val_obj + .iter() + .filter(|(k, _)| k.starts_with("variable_")) + .map(|(k, v)| { + ( + k.strip_prefix("variable_").unwrap().to_owned(), + fixup_value(v.to_owned()), + ) + }) + .collect(); + + Ok(GCodeMacro { + name, + description, + variables, + gcode, + }) + } + + let macros = cfg_json + .iter() + .filter(|(k, _)| k.starts_with("gcode_macro ")) + .map(|(k, v)| deserialize_macro(k, v)) + .collect::, ()>>() + .unwrap(); + + target.set_max_velocity(cfg.printer.max_velocity); + target.set_max_acceleration(cfg.printer.max_accel); + target.set_max_accel_to_decel(cfg.printer.max_accel_to_decel); + target.set_square_corner_velocity(cfg.printer.square_corner_velocity); + target.set_instant_corner_velocity(cfg.extruder.instantaneous_corner_velocity); + + target.firmware_retraction = cfg.firmware_retraction.map(|fr| FirmwareRetractionOptions { + retract_length: fr.retract_length, + unretract_extra_length: fr.unretract_extra_length, + unretract_speed: fr.unretract_speed, + retract_speed: fr.retract_speed, + lift_z: fr.lift_z, + }); + + let limits = [ + ( + DVec3::X, + cfg.printer.max_x_velocity, + cfg.printer.max_x_accel, + ), + ( + DVec3::Y, + cfg.printer.max_y_velocity, + cfg.printer.max_y_accel, + ), + ( + DVec3::Z, + cfg.printer.max_z_velocity, + cfg.printer.max_z_accel, + ), + ]; + + for (axis, m, a) in limits { + if let (Some(max_velocity), Some(max_accel)) = (m, a) { + target.move_checkers.push(MoveChecker::AxisLimiter { + axis, + max_velocity, + max_accel, + }); + } + } + + target.move_checkers.push(MoveChecker::ExtruderLimiter { + max_velocity: cfg.extruder.max_extrude_only_velocity, + max_accel: cfg.extruder.max_extrude_only_accel, + }); + Ok(PrinterConfiguration { + limits: target.clone(), + macro_config: MacroConfiguration { + macros, + config: cfg_json, + }, + }) +}