From df88b5a40cb8db30aec904ddcdd65f9c24b3c08d Mon Sep 17 00:00:00 2001 From: zaubentrucker <frederik.menke@rwth-aachen.de> Date: Sat, 22 Feb 2025 20:45:58 +0100 Subject: [PATCH] Handle position auto-reports --- red/src/main.rs | 4 +-- red/src/printer/gcode/m114.rs | 31 ++---------------- red/src/printer/gcode/mod.rs | 62 +++++++++++++++++++++++++++++++++-- red/src/printer/mod.rs | 49 +++++++++++++++++++-------- 4 files changed, 99 insertions(+), 47 deletions(-) diff --git a/red/src/main.rs b/red/src/main.rs index d9a1bd2..1452a15 100644 --- a/red/src/main.rs +++ b/red/src/main.rs @@ -4,7 +4,7 @@ use anyhow::{Context, Result, anyhow}; use futures::never::Never; use red::gamepad::Gamepad; use red::jogger; -use red::printer::{AutoReport, Printer}; +use red::printer::{AutoReportSetting, Printer}; use std::path::Path; fn main() -> Result<()> { @@ -38,7 +38,7 @@ fn jog() -> Result<()> { let mut printer = Printer::connect_to_path(&printer_tty_path) .with_context(|| anyhow!("Initializing printer connection"))?; - printer.set_position_auto_report(AutoReport::EverySeconds(1))?; + printer.set_position_auto_report(AutoReportSetting::EverySeconds(1))?; jogger::jog(&mut gamepad, printer).with_context(|| anyhow!("Running jog mode")) } diff --git a/red/src/printer/gcode/m114.rs b/red/src/printer/gcode/m114.rs index 60516df..cc150f3 100644 --- a/red/src/printer/gcode/m114.rs +++ b/red/src/printer/gcode/m114.rs @@ -15,39 +15,12 @@ impl M114Command { } impl GcodeCommand for M114Command { - type Reply = PrinterPosition; + type Reply = (); fn command(&self) -> String { "M114".into() } fn parse_reply(&self, reply: &str) -> Result<Self::Reply> { - lazy_static! { - static ref RE_SET: Vec<Regex> = vec![ - Regex::new(r"X:(\d+(?:\.\d+))").unwrap(), - Regex::new(r"Y:(\d+(?:\.\d+))").unwrap(), - Regex::new(r"Z:(\d+(?:\.\d+))").unwrap(), - Regex::new(r"E:(\d+(?:\.\d+))").unwrap(), - ]; - } - - let fields: Vec<Result<f64>> = RE_SET - .iter() - .map(|re| { - re.captures(reply) - .and_then(|cpt| cpt.get(1).map(|mtch| mtch.as_str().to_string())) - .and_then(|s| s.parse().ok()) - .ok_or(GcodeReplyError { - parsed_input: reply.to_string(), - problem: format!("Failed to match to regex {}", re.as_str()), - sent_command: self.command(), - }) - }) - .collect(); - - Ok(PrinterPosition { - x: fields[0].clone()?, - y: fields[1].clone()?, - z: fields[2].clone()?, - }) + super::parse_empty_reply(self.command(), reply) } } diff --git a/red/src/printer/gcode/mod.rs b/red/src/printer/gcode/mod.rs index fdf7f2e..1149cce 100644 --- a/red/src/printer/gcode/mod.rs +++ b/red/src/printer/gcode/mod.rs @@ -14,16 +14,19 @@ mod g28; mod g90; mod g91; mod m114; -mod m997; mod m154; +mod m997; +use crate::printer::PrinterPosition; pub use g0::G0Command; pub use g28::G28Command; pub use g90::G90Command; pub use g91::G91Command; +use lazy_static::lazy_static; pub use m114::M114Command; -pub use m997::M997Command; pub use m154::M154Command; +pub use m997::M997Command; +use regex::Regex; use std::fmt::Debug; type Result<T> = std::result::Result<T, GcodeReplyError>; @@ -67,3 +70,58 @@ fn parse_empty_reply(sent_command: String, reply: &str) -> Result<()> { Ok(()) } } + +pub enum AutoReport { + NotRecognized, + /// Auto-report contained printer position. The position might not contain all axes + Position(Option<f64>, Option<f64>, Option<f64>), +} + +/// Parse all known kinds of auto-reports that can be sent by marlin +/// TODO: implement temp readings +pub fn parse_autoreport_line(line: &str) -> AutoReport { + match parse_position_line(line) { + Ok((x, y, z)) => AutoReport::Position(x, y, z), + Err(_) => AutoReport::NotRecognized, + } +} + +/// Can the line be interpreted as an auto-report by the printer +fn is_auto_report(line: &str) -> bool { + matches!(parse_autoreport_line(line), AutoReport::NotRecognized) +} + +fn parse_position_line(line: &str) -> Result<(Option<f64>, Option<f64>, Option<f64>)> { + lazy_static! { + static ref RE_SET: Vec<Regex> = vec![ + Regex::new(r"X:(-?\d+(?:\.\d+))").unwrap(), + Regex::new(r"Y:(-?\d+(?:\.\d+))").unwrap(), + Regex::new(r"Z:(-?\d+(?:\.\d+))").unwrap(), + Regex::new(r"E:(-?\d+(?:\.\d+))").unwrap(), + ]; + } + + let fields: Vec<Result<f64>> = RE_SET + .iter() + .map(|re| { + re.captures(line) + .and_then(|cpt| cpt.get(1).map(|mtch| mtch.as_str().to_string())) + .and_then(|s| s.parse().ok()) + .ok_or(GcodeReplyError { + parsed_input: line.to_string(), + problem: format!("Failed to match to regex {}", re.as_str()), + sent_command: "".into(), + }) + }) + .collect(); + + if fields.iter().all(|r| r.is_err()) { + return Err(fields[0].clone().unwrap_err()); + } + + Ok(( + fields[0].clone().ok(), + fields[1].clone().ok(), + fields[2].clone().ok(), + )) +} diff --git a/red/src/printer/mod.rs b/red/src/printer/mod.rs index 37b7a9d..80f2259 100644 --- a/red/src/printer/mod.rs +++ b/red/src/printer/mod.rs @@ -1,7 +1,7 @@ pub mod gcode; use lazy_static::lazy_static; -use crate::printer::gcode::{G91Command, M114Command}; +use crate::printer::gcode::{AutoReport, G91Command, M114Command}; pub use gcode::GcodeCommand; use regex::Regex; use serialport::SerialPort; @@ -20,7 +20,7 @@ use std::sync::mpsc::TryRecvError; use std::time::{Duration, Instant}; use std::{io, str}; use std::sync::mpsc::RecvTimeoutError; -use self::gcode::{G0Command, G28Command, G90Command, GcodeReplyError, M154Command}; +use self::gcode::{G0Command, G28Command, G90Command, GcodeReplyError, M154Command, parse_autoreport_line}; /// Recv buffer string will be initialized with this capacity. /// This should fit a simple "OK Pnn Bn" reply for GCODE commands that @@ -41,7 +41,7 @@ pub enum Port { Path(String), } -pub enum AutoReport { +pub enum AutoReportSetting { Disabled, EverySeconds(u64), } @@ -221,9 +221,14 @@ impl Printer { } /// Update the internal position by asking the printer for it - fn update_position(&mut self) -> Result<PrinterPosition, PrinterError> { - let res = self.send_gcode(M114Command)?; - self.state.lock().unwrap().deref_mut().position = res; + pub fn update_position(&mut self) -> Result<PrinterPosition, PrinterError> { + self.send_gcode(M114Command)?; + // The printer will handle the position reply within the IO-Thread. + // + // I do this because I detect and filter out auto-reports by parsing a regex, and thus I + // can't differentiate between a position report that was polled for and one that was + // auto-reported. (see `.::handle_printer_autoreport`) + let res = self.state.lock().unwrap().deref_mut().position; Ok(res) } @@ -346,10 +351,10 @@ impl Printer { /// Set an interval at which to report the printer position or disable automatic position /// updates - pub fn set_position_auto_report(&mut self, report: AutoReport) -> Result<(), PrinterError> { + pub fn set_position_auto_report(&mut self, report: AutoReportSetting) -> Result<(), PrinterError> { match report { - AutoReport::Disabled => self.send_gcode(M154Command { interval: 0 }), - AutoReport::EverySeconds(n) => self.send_gcode(M154Command { interval: n }), + AutoReportSetting::Disabled => self.send_gcode(M154Command { interval: 0 }), + AutoReportSetting::EverySeconds(n) => self.send_gcode(M154Command { interval: n }), } } @@ -364,23 +369,27 @@ impl Printer { mut port: TTYPort, state: Arc<Mutex<State>>, ) { + // Bytes that were read from the port that don't constitute a whole line + let mut partial_reads: Vec<u8> = Vec::new(); loop { match from_user_thread.try_recv() { Ok(user_command) => handle_user_command( &mut port, user_command, state.clone(), + &mut partial_reads, to_user_thread.clone(), ), Err(TryRecvError::Disconnected) => break, - Err(TryRecvError::Empty) => handle_printer_autoreport(&mut port, state.clone()), + Err(TryRecvError::Empty) => handle_printer_autoreport(&mut port, &mut partial_reads, state.clone()), } } } } /// Check for auto-report messages coming in from the printer and update the `state` accordingly -fn handle_printer_autoreport(port: &mut TTYPort, state: Arc<Mutex<State>>) { +fn handle_printer_autoreport(port: &mut TTYPort, partial_reads: &mut Vec<u8>, state: Arc<Mutex<State>>) { + return; if port .bytes_to_read() .expect("`handle_printer_autoreport`: Failed to check for available data") @@ -461,6 +470,7 @@ fn handle_user_command( port: &mut TTYPort, user_command: String, state: Arc<Mutex<State>>, + partial_line: &mut Vec<u8>, to_user_thread: Sender<Vec<u8>>, ) { // TODO: Add timeout? @@ -470,19 +480,30 @@ fn handle_user_command( port.flush().unwrap(); let mut already_read_lines = Vec::new(); - let mut rest = Vec::new(); let start_time = Instant::now(); loop { if start_time.elapsed() > DEFAULT_COMMAND_TIMEOUT { panic!("No reply from printer within timeout"); } - let line = read_line(port, &mut rest, DEFAULT_COMMAND_TIMEOUT) + let line = read_line(port, partial_line, DEFAULT_COMMAND_TIMEOUT) .expect("Failed to read from printer"); let str_line = String::from_utf8(line.clone()).expect("Read line was no valid utf8"); println!("<< {:?}", str_line); - if str_line.starts_with("ok") { + if let AutoReport::Position(x, y, z) = parse_autoreport_line(&str_line) { + let mut position_guard = state.lock().unwrap(); + if let Some(x) = x { + position_guard.position.x = x; + } + if let Some(y) = y { + position_guard.position.y = y; + } + if let Some(z) = z { + position_guard.position.z = z; + } + } + else if str_line.starts_with("ok") { state.lock().unwrap().last_buffer_capacity = Printer::parse_ok(&str_line).expect("Couldn't parse line as 'ok'-message"); to_user_thread