From b89849555f6c67328b3cfdc705419305f10e769e Mon Sep 17 00:00:00 2001 From: Sameer Puri Date: Mon, 30 Mar 2020 15:56:51 -0400 Subject: [PATCH] Begin translation to macro-based GCode impl (does not compile) --- Cargo.lock | 64 +++++++++ Cargo.toml | 1 + README.md | 4 + src/code.rs | 379 ++++++++++++++++++++++++++----------------------- src/machine.rs | 72 ++++------ 5 files changed, 299 insertions(+), 221 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 34fec5e..4a51842 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -142,11 +142,52 @@ dependencies = [ "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "paste" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "paste-impl 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro-hack 0.5.14 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "paste-impl" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro-hack 0.5.14 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "proc-macro2" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "quick-error" version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "quote" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "regex" version = "1.3.6" @@ -202,6 +243,7 @@ dependencies = [ "env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "lyon_geom 0.15.1 (registry+https://github.com/rust-lang/crates.io-index)", + "paste 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "svgdom 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)", "uom 0.27.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -230,6 +272,16 @@ dependencies = [ "siphasher 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "syn" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "termcolor" version = "1.1.0" @@ -264,6 +316,11 @@ name = "unicode-width" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "unicode-xid" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "uom" version = "0.27.0" @@ -335,7 +392,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum lyon_geom 0.15.1 (registry+https://github.com/rust-lang/crates.io-index)" = "76579b1f0a1ab9c3565c48faf951152d772d67ea35292fa9d36a991916b19a21" "checksum memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" "checksum num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" +"checksum paste 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "092d791bf7847f70bbd49085489fba25fc2c193571752bff9e36e74e72403932" +"checksum paste-impl 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "406c23fb4c45cc6f68a9bbabb8ec7bd6f8cfcbd17e9e8f72c2460282f8325729" +"checksum proc-macro-hack 0.5.14 (registry+https://github.com/rust-lang/crates.io-index)" = "fcfdefadc3d57ca21cf17990a28ef4c0f7c61383a28cb7604cf4a18e6ede1420" +"checksum proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)" = "df246d292ff63439fea9bc8c0a270bed0e390d5ebd4db4ba15aba81111b5abe3" "checksum quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +"checksum quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f" "checksum regex 1.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7f6946991529684867e47d86474e3a6d0c0ab9b82d5821e314b1ede31fa3a4b3" "checksum regex-syntax 0.6.17 (registry+https://github.com/rust-lang/crates.io-index)" = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" "checksum roxmltree 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "330d8f80a274bc3cb608908ee345970e7e24b96907f1ad69615a498bec57871c" @@ -345,11 +407,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" "checksum svgdom 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e4ae7d6df72b3a36c0f003f1f8919c96be43d20d7c015f8dcf2f82531a4fc33b" "checksum svgtypes 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9c536faaff1a10837cfe373142583f6e27d81e96beba339147e77b67c9f260ff" +"checksum syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)" = "0df0eb663f387145cab623dea85b09c2c5b4b0aef44e945d928e682fce71bb03" "checksum termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" "checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" "checksum thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" "checksum typenum 1.11.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6d2783fe2d6b8c1101136184eb41be8b1ad379e4657050b8aaff0c79ee7575f9" "checksum unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" +"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" "checksum uom 0.27.0 (registry+https://github.com/rust-lang/crates.io-index)" = "51fc04fb44bcb7806da71885872cb15d123b681e459a476ef8a0bab287bee0cd" "checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" "checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" diff --git a/Cargo.toml b/Cargo.toml index ba305e8..4b8579b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,3 +13,4 @@ log = "0" env_logger = "0" uom = "0" regex = "1" +paste = "0" diff --git a/README.md b/README.md index e98ddc1..05183f5 100644 --- a/README.md +++ b/README.md @@ -46,3 +46,7 @@ cat out.gcode * What about a generic PPD driver for using a plotter as a printer? I thought about doing something like this where you package ghostscript + inkscape + svg2gcode but since plotter dimensions and capabilities vary, this is an exercise left to the reader for now. +## Reference Documents + +* [RepRap G-code](https://reprap.org/wiki/G-code) +* [G-Code and M-Code Reference List for Milling](https://www.cnccookbook.com/g-code-m-code-reference-list-cnc-mills/) diff --git a/src/code.rs b/src/code.rs index 9c507af..1631c75 100644 --- a/src/code.rs +++ b/src/code.rs @@ -1,215 +1,236 @@ -/// TODO: Documentation - use std::io::{self, Write}; -use std::ops::AddAssign; -// TODO: Documentation -#[derive(Clone, PartialEq, Eq)] -pub enum Direction { - Clockwise, - AntiClockwise, +/// Fields are the basic unit of GCode. +trait Field { + /// An uppercase letter + const LETTER: char; + /// A number if the field has a fixed number. + const NUMBER: Option; + /// A fraction if the field has a fixed fraction following a fixed number. + const FRACTION: Option; + + fn from_arguments<'a>(arguments: &[Argument<'a>]) -> Self; + fn into_arguments<'a>(&'a self) -> Vec>; } -// TODO: Documentation -#[derive(Copy, Clone, PartialEq, Eq)] -pub enum Tool { - Off, - On, +/// Arguments, described by a letter and a value, belong to a field. +pub struct Argument<'a> { + letter: char, + value: &'a str, } -// TODO: Documentation -#[derive(Clone, PartialEq)] -pub enum Distance { - Absolute, - Incremental, +macro_rules! field { + ($(#[$outer:meta])* $fieldName: ident {$letter: pat, $number: pat, $fraction: pat, {$($(#[$inner:meta])* $argument: ident : $type: ty), *} }) => { + $(#[$outer])* + struct $fieldName { + $( + $(#[$inner])* + $argument: Option<$type>, + )* + } + + paste::item! { + impl Field for $fieldName { + const LETTER: char = $letter; + const NUMBER: Option = $number; + const FRACTION: Option = $fraction; + fn from_arguments<'a>(arguments: &[Argument<'a>]) -> Self { + let mut field = Self { + $($argument: None,)* + }; + for arg in arguments.iter() { + $(if arg.letter == stringify!([<$argument:upper>]).chars().next().unwrap() { + field.$argument = Some(arg.value.parse().unwrap()); + })* + } + field + } + fn into_arguments<'a>(&'a self) -> Vec> { + let mut args = vec![]; + $( + if let Some(value) = self.$argument { + args.push(Argument { + letter: stringify!([<$argument:upper>]).chars().next().unwrap(), + value: &value.to_string() + }); + } + )* + args + } + } + } + }; } -// TODO: Documentation -#[derive(Default, PartialEq, Clone)] -pub struct Program(Vec); +field!( + /// Moves the head at the fastest possible speed to the desired speed. + /// Never enter a cut with rapid positioning. + /// Some older machines may "dog leg" rapid positioning, moving one axis at a time. + RapidPositioning { + 'G', Some(0), None, { + x: f64, + y: f64, + z: f64, + e: f64, + f: f64, + h: f64, + r: f64, + s: f64, + a: f64, + b: f64, + c: f64 + } +}); -// TODO: Documentation -impl std::ops::Deref for Program { - type Target = [GCode]; +field!( + /// Typically used for "cutting" motion + LinearInterpolation { + 'G', Some(1), None, { + x: f64, + y: f64, + z: f64, + e: f64, + f: f64, + h: f64, + r: f64, + s: f64, + a: f64, + b: f64, + c: f64 + } +}); - // TODO: Documentation - fn deref(&self) -> &Self::Target { - self.0.deref() +field!( + /// This will keep the axes unmoving for the period of time in seconds specified by the P number. + Dwell { + 'G', Some(4), None, { + /// Time in seconds + p: f64 } -} +}); + +field!( + /// Use inches for length units + UnitsInches { + 'G', Some(20), None, {} +}); + +field!( + /// Use millimeters for length units + UnitsMillimeters { + 'G', Some(21), None, {} +}); + +field!( + /// In absolute distance mode, axis numbers usually represent positions in terms of the currently active coordinate system. + AbsoluteDistanceMode { + 'G', Some(90), None, {} +}); + +field!( + /// In incremental distance mode, axis numbers usually represent increments from the current values of the numbers. + IncrementalDistanceMode { + 'G', Some(91), None, {} +}); + +field!( + /// Start spinning the spindle clockwise with speed `p` + StartSpindleClockwise { + 'M', Some(3), None, { + /// Speed + p: f64 + } + } +); + +field!( + /// Start spinning the spindle counterclockwise with speed `p` + StartSpindleCounterclockwise { + 'M', Some(4), None, { + /// Speed + p: f64 + } + } +); -// TODO: Documentation -impl AddAssign for Program { - fn add_assign(&mut self, mut other: Program) { - self.0.extend(other.0.drain(..)); +field!( + /// Stop spinning the spindle + StopSpindle { + 'M', Some(5), None, {} } -} +); -// TODO: Documentation -impl From> for Program { - fn from(v: Vec) -> Self { - Self(v) +field!( + /// Signals the end of a program + ProgramEnd { + 'M', Some(20), None, {} } +); + +/// Checksums are used by some GCode generators at the end of each line +struct Checksum { + /// Checksum value + value: u8, } -// TODO: Documentation -impl Program { - pub fn push(&mut self, g: GCode) { - self.0.push(g) +impl Field for Checksum { + const LETTER: char = '*'; + const NUMBER: Option = None; + const FRACTION: Option = None; + fn from_arguments<'a>(arguments: &[Argument<'a>]) -> Self { + Self { value: 0 } + } + fn into_arguments(&self) -> Vec { + vec![] } } -// TODO: Documentation -macro_rules! write_if_some { - ($w:expr, $s:expr, $v:ident) => { - if let Some(v) = $v { - write!($w, $s, v) - } else { - Ok(()) - } - }; +/// A line number is the letter N followed by an integer (with no sign) between 0 and 99999 written with no more than five digits +struct LineNumber { + /// Line number + value: u16, } -// TODO: Documentation -// Rudimentary regular expression GCode validator. +impl Field for LineNumber { + const LETTER: char = 'N'; + const NUMBER: Option = None; + const FRACTION: Option = None; + fn from_arguments<'a>(arguments: &[Argument<'a>]) -> Self { + Self { value: 0 } + } + fn into_arguments(&self) -> Vec { + vec![] + } +} + +/// Rudimentary regular expression GCode validator. pub fn validate_gcode(gcode: &&str) -> bool { use regex::Regex; let re = Regex::new(r##"^(?:(?:%|\(.*\)|(?:[A-Z^E^U][+-]?\d+(?:\.\d*)?))\h*)*$"##).unwrap(); gcode.lines().all(|line| re.is_match(line)) } -// TODO: Documentation -#[derive(Clone, PartialEq)] -pub enum GCode { - RapidPositioning { - x: Option, - y: Option, - }, - LinearInterpolation { - x: Option, - y: Option, - z: Option, - f: Option, - }, - Dwell { - p: f64, - }, - UnitsInches, - UnitsMillimeters, - ProgramEnd, - StartSpindle { - d: Direction, - s: f64, - }, - StopSpindle, - DistanceMode(Distance), - Comment(Box), - Raw(Box), -} - // TODO: Documentation // TODO: This function is too large pub fn program2gcode(p: &Program, mut w: W) -> io::Result<()> { - use GCode::*; + let mut last_feedrate: Option = None; - for code in p.iter() { - match code { - RapidPositioning { x, y } => { - if let (None, None) = (x, y) { - continue; - } - write!(w, "G0")?; - write_if_some!(w, " X{}", x)?; - write_if_some!(w, " Y{}", y)?; - writeln!(w)?; - } - LinearInterpolation { x, y, z, f } => { - if let (None, None, None, None) = (x, y, z, f) { - continue; + let letter = '*'; + let number = Some(0); + let fraction = None; + + macro_rules! match_field { + ($($fieldName: ident)*) => { + match (letter, number, fraction) { + $(($fieldName::LETTER, $fieldName::NUMBER, $fieldName::FRACTION) => { + Some($fieldName::from_arguments(arguments)) + },)* + _ => {None} } - - let f = match (last_feedrate, f) { - (None, None) => { - return Err(io::Error::new( - io::ErrorKind::Other, - "Linear interpolation without previously set feedrate", - )) - } - (Some(last), Some(new)) => { - if (last - *new).abs() < std::f64::EPSILON { - last_feedrate = Some(*new); - Some(new) - } else { - None - } - } - (None, Some(new)) => { - last_feedrate = Some(*new); - Some(new) - } - (Some(_), None) => None, - }; - write!(w, "G1")?; - write_if_some!(w, " X{}", x)?; - write_if_some!(w, " Y{}", y)?; - write_if_some!(w, " Z{}", z)?; - write_if_some!(w, " F{}", f)?; - writeln!(w)?; - } - Dwell { p } => { - writeln!(w, "G4 P{}", p)?; - } - UnitsInches => { - writeln!(w, "G20")?; - } - UnitsMillimeters => { - writeln!(w, "G21")?; - } - ProgramEnd => { - writeln!(w, "M20")?; - } - StartSpindle { d, s } => { - let d = match d { - Direction::Clockwise => 3, - Direction::AntiClockwise => 4, - }; - writeln!(w, "M{} S{}", d, s)?; - } - StopSpindle => { - writeln!(w, "M5")?; - } - DistanceMode(mode) => { - writeln!( - w, - "G{}", - match mode { - Distance::Absolute => 90, - Distance::Incremental => 91, - } - )?; - } - Comment(name) => { - writeln!(w, "({})", name)?; - } - Raw(raw) => { - writeln!(w, "{}", raw)?; - } - } + }; } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_validate_gcode() { - panic!("TODO: basic passing test"); - } - - #[test] - fn test_program2gcode() { - panic!("TODO: basic passing test"); + for code in p.iter() { + match_field!(LineNumber); } + Ok(()) } diff --git a/src/machine.rs b/src/machine.rs index 3ecda79..b6daf32 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -1,9 +1,27 @@ -/// TODO: Documentation - use crate::code::*; -// TODO: Documentation -// Generic machine state simulation, assuming nothing is known about the machine when initialized. +//// Direction of the machine spindle +#[derive(Clone, PartialEq, Eq)] +pub enum Direction { + Clockwise, + Counterclockwise, +} + +/// Whether the tool is active (i.ee. cutting) +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum Tool { + Off, + On, +} + +/// The distance mode for movement commands +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum Distance { + Absolute, + Relative, +} + +/// Generic machine state simulation, assuming nothing is known about the machine when initialized. pub struct Machine { tool_state: Option, distance_mode: Option, @@ -11,9 +29,8 @@ pub struct Machine { pub tool_off_action: Vec, } -// TODO: Documentation -// Assigns reasonable default settings that apply to most gcode applications. -impl Default for Machine { +/// Assigns reasonable default settings that apply to most gcode applications. +impl Machine { fn default() -> Self { Self { tool_state: None, @@ -66,13 +83,13 @@ impl Machine { if is_absolute { self.absolute() } else { - self.incremental() + self.relative() } } // Outputs gcode command to use absolute motion pub fn absolute(&mut self) -> Vec { - if self.distance_mode == Some(Distance::Incremental) || self.distance_mode == None { + if self.distance_mode == Some(Distance::Relative) || self.distance_mode == None { self.distance_mode = Some(Distance::Absolute); vec![GCode::DistanceMode(Distance::Absolute)] } else { @@ -80,43 +97,14 @@ impl Machine { } } - // Outputs gcode command to use relative motion - pub fn incremental(&mut self) -> Vec { + /// Set the distance mode to relative + pub fn relative(&mut self) -> Vec { if self.distance_mode == Some(Distance::Absolute) || self.distance_mode == None { - self.distance_mode = Some(Distance::Incremental); - vec![GCode::DistanceMode(Distance::Incremental)] + self.distance_mode = Some(Distance::Relative); + vec![GCode::DistanceMode(Distance::Relative)] } else { vec![] } } } -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_tool_on() { - panic!("TODO: basic passing test"); - } - - #[test] - fn test_tool_off() { - panic!("TODO: basic passing test"); - } - - #[test] - fn test_distance() { - panic!("TODO: basic passing test"); - } - - #[test] - fn test_absolute() { - panic!("TODO: basic passing test"); - } - - #[test] - fn test_incremental() { - panic!("TODO: basic passing test"); - } -}