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