diff --git a/red/Cargo.toml b/red/Cargo.toml index 66ca83f..99628e4 100644 --- a/red/Cargo.toml +++ b/red/Cargo.toml @@ -13,6 +13,8 @@ gilrs = "0.10.1" i2c-linux = "0.1.2" lazy_static = "1.4.0" regex = "1.6.0" +serialport = "4.6.1" tokio = { version = "1.21.0", features = ["full"] } tokio-serial = "5.4.3" tokio-util = { version = "0.7.4", features = ["codec"] } + diff --git a/red/src/gamepad.rs b/red/src/gamepad.rs index 81e690b..a5f9c31 100644 --- a/red/src/gamepad.rs +++ b/red/src/gamepad.rs @@ -4,11 +4,7 @@ use gilrs::Button::LeftTrigger2; use gilrs::Button::RightTrigger2; use gilrs::EventType::*; use gilrs::Gilrs; -use std::sync::mpsc; -use std::sync::Arc; -use std::sync::Mutex; -use std::thread::sleep; -use std::time::Duration; + #[derive(Debug)] pub enum Axis { diff --git a/red/src/jogger.rs b/red/src/jogger.rs index 7949579..9452b83 100644 --- a/red/src/jogger.rs +++ b/red/src/jogger.rs @@ -1,8 +1,9 @@ +use std::ops::DerefMut; use crate::gamepad::Gamepad; use crate::printer::Printer; use euclid::{vec3, Vector3D}; use futures::never::Never; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use std::time::Duration; use tokio::io::{AsyncRead, AsyncWrite}; use tokio::time::sleep; @@ -33,11 +34,11 @@ pub struct PrinterUnits; pub type PrinterVec = Vector3D<f64, PrinterUnits>; /// Jog the gantry by pumping loads of gcode into the printer board -pub async fn jog<T: AsyncRead + AsyncWrite + Sized>(gamepad: Arc<Gamepad>, mut printer: Printer<T>) -> Never { - printer.use_absolute_movements().await.unwrap(); +pub async fn jog<T: AsyncRead + AsyncWrite + Sized>(gamepad: Arc<Mutex<Gamepad>>, mut printer: Printer) -> Never { + printer.use_absolute_movements().unwrap(); println!("Using absolute movements"); loop { - let (setpoint_x, setpoint_y, setpoint_z) = gamepad.speed_setpoint(); + let (setpoint_x, setpoint_y, setpoint_z) = gamepad.lock().unwrap().deref_mut().speed_setpoint(&[]); let distance: PrinterVec = vec3( FULL_SCALE_SPEED_XY * (TIME_PER_MOVEMENT.as_secs_f64() / 60.0) * (setpoint_x as f64), @@ -49,16 +50,14 @@ pub async fn jog<T: AsyncRead + AsyncWrite + Sized>(gamepad: Arc<Gamepad>, mut p continue; } let velocity = distance.length() / (TIME_PER_MOVEMENT.as_secs_f64() / 60.0); - let old_postion = printer.state.position; + let old_postion = (*printer.state.lock().unwrap()).position; printer .move_absolute( old_postion.x + distance.x, old_postion.y + distance.y, old_postion.z + distance.z, velocity.into(), - ) - .await - .expect("Failed to send movement command!"); + ).expect("Failed to send movement command!"); println!( "New position {pos:?}", diff --git a/red/src/main.rs b/red/src/main.rs index dacf37b..2e290f1 100644 --- a/red/src/main.rs +++ b/red/src/main.rs @@ -1,38 +1,15 @@ #![warn(rust_2018_idioms)] -use std::os::fd::RawFd; -use futures::never::Never; -use red::gamepad; -use red::jogger; use red::printer::Printer; use std::path::Path; +use futures::never::Never; use tokio_serial::SerialPortInfo; -#[tokio::main] -async fn main() -> Never { - let args: Vec<String> = std::env::args().collect(); - - if args.len() > 1 { - inherit_printer().await - } - else { - look_for_printer().await - } - +fn main() -> Never { + look_for_printer() } -async fn inherit_printer() -> Never { - let args: Vec<String> = std::env::args().collect(); - - let fd: RawFd = args[1].parse().expect("Not a valid FD"); - let gamepad = gamepad::Gamepad::new().await.unwrap(); - let printer = Printer::connect_to_raw_fd(fd) - .await - .unwrap(); - jogger::jog(gamepad, printer).await -} - -async fn look_for_printer() -> Never { +fn look_for_printer() -> Never { println!("Entering App"); let serial_ports = tokio_serial::available_ports() .expect("Could not list serial ports") @@ -46,9 +23,8 @@ async fn look_for_printer() -> Never { }; let port_path = Path::new("/dev").join(Path::new(&port.port_name).file_name().unwrap()); println!("Found serial port: {:?}", &port_path); - let gamepad = gamepad::Gamepad::new().await.unwrap(); - let printer = Printer::connect_to_path(&port_path.as_os_str().to_string_lossy()) - .await - .unwrap(); - jogger::jog(gamepad, printer).await -} \ No newline at end of file + let printer = Printer::connect_to_path(&port_path.as_os_str().to_string_lossy()).unwrap(); + loop { + println!("{}", printer.printer_state()); + } +} diff --git a/red/src/printer/mod.rs b/red/src/printer/mod.rs index 854b096..b10fe5e 100644 --- a/red/src/printer/mod.rs +++ b/red/src/printer/mod.rs @@ -2,18 +2,23 @@ pub mod gcode; use lazy_static::lazy_static; use bytes::BytesMut; -use futures::sink::SinkExt; -use futures::stream::{SplitSink, SplitStream, StreamExt}; +use core::panic; use regex::Regex; +use serialport::SerialPort; +use serialport::TTYPort; +use std::io::Read; +use std::io::Write; +use std::ops::{Deref, DerefMut, Index}; use std::os::fd::{FromRawFd, RawFd}; +use std::sync::mpsc; +use std::sync::mpsc::Receiver; +use std::sync::mpsc::Sender; +use std::sync::mpsc::TryRecvError; +use std::sync::Arc; +use std::sync::Mutex; use std::time::{Duration, Instant}; -use std::{fmt::Write, io, str}; +use std::{io, str}; use tokio; -use tokio::fs::File; -use tokio::io::{AsyncRead, AsyncWrite}; -use tokio::time::timeout; -use tokio_serial::{SerialPortBuilderExt, SerialStream}; -use tokio_util::codec::{Decoder, Encoder, Framed}; pub use gcode::{GcodeCommand, GcodeReply}; @@ -29,6 +34,12 @@ const BAUD_RATE: u32 = 115200; const DEFAULT_COMMAND_TIMEOUT: Duration = Duration::from_secs(2); +/// Timeout for reads and writes to the serial port +const SERIALPORT_TIMEOUT: Duration = Duration::from_millis(10); + +/// Time the IO-thread may take to reply to the user thread after issuing a command +const IO_THREAD_TIMEOUT: Duration = Duration::from_millis(300); + pub enum Port { OpenFd(RawFd), Path(String), @@ -62,110 +73,94 @@ pub enum MovementMode { pub struct State { pub position: PrinterPosition, pub movement_mode: MovementMode, + pub last_buffer_capacity: usize, } -pub struct Printer<T: AsyncRead + AsyncWrite + Sized> { - pub state: State, - serial_tx: SplitSink<Framed<T, LineCodec>, String>, - serial_rx: SplitStream<Framed<T, LineCodec>>, - last_buffer_capacity: usize, +pub struct Printer { + pub state: Arc<Mutex<State>>, + /// Queue a command to be sent to the IO thread + to_io_thread: std::sync::mpsc::Sender<String>, + /// Used read replies as received from the printer. Should not be written to + from_io_thread: std::sync::mpsc::Receiver<Vec<u8>>, maximum_buffer_capacity: usize, } -impl<S: AsyncRead + AsyncWrite + Sized> Printer<S> { +impl Printer { /// Send gcode to the printer and parse its reply - async fn send_gcode<T: GcodeCommand>( + fn send_gcode<T: GcodeCommand>( &mut self, command: T, ignore_pos: bool, timeout: Duration, ) -> Result<T::Reply, PrinterError> { - let command_text = format!("{}\n", command.command()); - self.serial_tx.send(command_text.clone()).await.unwrap(); - self.serial_tx.flush().await.unwrap(); - let mut reply = String::with_capacity(RECV_BUFFER_CAPACITY); - loop { - let mut line = None; - let timeout = Instant::now() + timeout; - while line.is_none() && Instant::now() < timeout { - line = self.serial_rx.next().await; - if line.is_none() { - tokio::time::sleep(Duration::from_millis(5)).await; - } - } - if let Some(line) = line { - match line { - Ok(line) => { - if ignore_pos && (line.starts_with(" T:") || line.starts_with("X:")) { - continue; - } - if line.contains("ok") { - self.last_buffer_capacity = Self::parse_ok(&line)?; - let reply = command.parse_reply(&reply); - return reply.map_err(PrinterError::GcodeReply); - } else { - reply.push_str(&line); - } - } - Err(e) => { - println!("Failed to read from Printer: {}", e) - } - } + let command_text = command.command() + "\n"; + self.to_io_thread + .send(command_text.clone()) + .expect("Printer IO-Thread hung up its incoming mpsc"); + let reply = self.from_io_thread.recv_timeout(IO_THREAD_TIMEOUT); + match reply { + Ok(reply) => Ok(command + .parse_reply(&String::from_utf8(reply).expect("Invalid UTF-8 reply from printer")) + .expect("Could not parse reply from printer")), + + Err(e) => { + panic!("Printer didn't reply in time: {}", e) } } } - pub async fn connect(port: S) -> Result<Self, PrinterError> { - let connection = LineCodec.framed(port); - let (serial_tx, mut serial_rx) = connection.split(); - + pub fn connect(mut port: TTYPort) -> Result<Self, PrinterError> { // The printer will send some info after connecting for the first time. We need to wait for this // to be received as it will otherwise stop responding for some reason: - loop { - if let Ok(message) = timeout(Duration::from_millis(100), serial_rx.next()).await { - match message { - Some(Ok(reply)) => { - println!("got stuff: {:?}", reply); - if reply.contains("Loaded") { - break; - } - if reply.contains("ok") { - break; - } - if reply.starts_with("X:") { - break; - } - } - Some(Err(e)) => { - println!("Error reading from serial port: {:?}", e); - } - None => (), + + port.set_timeout(SERIALPORT_TIMEOUT) + .expect("Cannot set serial port timeout"); + + let mut buf = [0; 1024]; + let deadline = Instant::now() + Duration::from_millis(200); + let mut initial_msg = Vec::new(); + while Instant::now() < deadline { + if let Ok(message) = port.read(&mut buf) { + if message > 0 { + initial_msg.extend(&buf[0..message]); } - } else { - println!( - "Reading from serial port timed out. Printer might already be initialized." - ); - break; } } - let mut res = Printer { - serial_rx, - serial_tx, - state: State { - position: PrinterPosition { - x: 0.0, - y: 0.0, - z: 0.0, - }, - movement_mode: MovementMode::AbsoluteMovements, + if let Ok(initial_msg) = String::from_utf8(initial_msg.clone()) { + println!("Initial message from printer was:\n{initial_msg}") + } else { + println!("Initial message from printer was not valid UTF8: {initial_msg:?}") + } + + let (to_io_thread, from_user_thread) = mpsc::channel(); + let (to_user_thread, from_io_thread) = mpsc::channel(); + + let state = Arc::new(Mutex::new(State { + position: PrinterPosition { + x: 0.0, + y: 0.0, + z: 0.0, }, + movement_mode: MovementMode::AbsoluteMovements, last_buffer_capacity: 0, // this is updated on the next call to `send_gcode()` + })); + + //TODO: Spawn IO-Thread + let state_for_io = state.clone(); + std::thread::spawn(move || { + Self::io_thread_work(to_user_thread, from_user_thread, port, state_for_io) + }); + + let mut res = Printer { + from_io_thread, + to_io_thread, + state: state.clone(), maximum_buffer_capacity: 0, // this is updated on the next call to `send_gcode()` }; // This implicitly sets `res.last_buffer_capacity` - res.use_absolute_movements().await.map_err(|err| { + res.use_absolute_movements().map_err(|err| { PrinterError::InitializationError(format!( "Failed to set absolute movements mode: {:?}", err @@ -173,9 +168,9 @@ impl<S: AsyncRead + AsyncWrite + Sized> Printer<S> { })?; // since we never sent any positioning GCODE, we should be at max-capacity now. - res.maximum_buffer_capacity = res.last_buffer_capacity; + res.maximum_buffer_capacity = res.state.lock().unwrap().deref().last_buffer_capacity; - res.update_position().await.map_err(|err| { + res.update_position().map_err(|err| { PrinterError::InitializationError(format!( "Failed to get the current position: {:?}", err @@ -186,7 +181,7 @@ impl<S: AsyncRead + AsyncWrite + Sized> Printer<S> { } pub fn printer_state(&self) -> &State { - &self.state + &self.state.lock().unwrap().deref().clone() } /// The maximum capacity of the machines GCODE buffer. @@ -197,7 +192,7 @@ impl<S: AsyncRead + AsyncWrite + Sized> Printer<S> { /// The remaining capacity of the machines GCODE buffer. /// This value is refreshed after each sent command. pub fn remaining_capacity(&self) -> usize { - self.last_buffer_capacity + self.state.lock().unwrap().deref().last_buffer_capacity } /// Parse the "Ok" confirmation line that the printer sends after every successfully received @@ -219,9 +214,9 @@ impl<S: AsyncRead + AsyncWrite + Sized> Printer<S> { } /// Update the internal position by asking the printer for it - async fn update_position(&mut self) -> Result<PrinterPosition, PrinterError> { - let res = self.send_gcode(M114Command, false, DEFAULT_COMMAND_TIMEOUT).await?; - self.state.position = res; + fn update_position(&mut self) -> Result<PrinterPosition, PrinterError> { + let res = self.send_gcode(M114Command, false, DEFAULT_COMMAND_TIMEOUT)?; + self.state.lock().unwrap().deref_mut().position = res; Ok(res) } @@ -231,9 +226,9 @@ impl<S: AsyncRead + AsyncWrite + Sized> Printer<S> { /// Otherwise, the motion planner on the printer will stop the printer between all movements /// as `self.move_absolute()` will call `use_absolute_movements` and `use_relative_movements` /// itself. (See its documentation) - pub async fn use_absolute_movements(&mut self) -> Result<(), PrinterError> { - self.state.movement_mode = MovementMode::AbsoluteMovements; - self.send_gcode(G90Command, true, DEFAULT_COMMAND_TIMEOUT).await + pub fn use_absolute_movements(&mut self) -> Result<(), PrinterError> { + self.state.lock().unwrap().deref_mut().movement_mode = MovementMode::AbsoluteMovements; + self.send_gcode(G90Command, true, DEFAULT_COMMAND_TIMEOUT) } /// Switch the printer to relative movement mode. @@ -242,9 +237,9 @@ impl<S: AsyncRead + AsyncWrite + Sized> Printer<S> { /// Otherwise, the motion planner on the printer will stop the printer between all movements /// as `self.move_relative()` will call `use_absolute_movements` and `use_relative_movements` /// itself. (See its documentation) - pub async fn use_relative_movements(&mut self) -> Result<(), PrinterError> { - self.state.movement_mode = MovementMode::RelativeMovements; - self.send_gcode(G91Command, true, DEFAULT_COMMAND_TIMEOUT).await + pub fn use_relative_movements(&mut self) -> Result<(), PrinterError> { + self.state.lock().unwrap().deref_mut().movement_mode = MovementMode::RelativeMovements; + self.send_gcode(G91Command, true, DEFAULT_COMMAND_TIMEOUT) } /// Home the printer using the hardware endstops @@ -252,9 +247,9 @@ impl<S: AsyncRead + AsyncWrite + Sized> Printer<S> { /// # Arguments /// * `x, y, z` - Whether the axis should be homed. Axis that are set to `false` will not be /// homed., - pub async fn auto_home(&mut self, x: bool, y: bool, z: bool) -> Result<(), PrinterError> { - self.send_gcode(G28Command::new(x, y, z), true, DEFAULT_COMMAND_TIMEOUT).await?; - self.state.position = PrinterPosition { + pub fn auto_home(&mut self, x: bool, y: bool, z: bool) -> Result<(), PrinterError> { + self.send_gcode(G28Command::new(x, y, z), true, DEFAULT_COMMAND_TIMEOUT)?; + self.state.lock().unwrap().deref_mut().position = PrinterPosition { x: 0.0, y: 0.0, z: 0.0, @@ -272,7 +267,7 @@ impl<S: AsyncRead + AsyncWrite + Sized> Printer<S> { /// # Arguments /// * `x, y, z` - Position offset in printer units (mm) /// * `velocity` - Velocity that the printer should move at (mm/min) - pub async fn move_relative( + pub fn move_relative( &mut self, x: f64, y: f64, @@ -286,18 +281,20 @@ impl<S: AsyncRead + AsyncWrite + Sized> Printer<S> { e: None, // Machine has no e velocity, }; - if let MovementMode::AbsoluteMovements = self.state.movement_mode { - self.use_relative_movements().await?; - let res = self.send_gcode(command, true, DEFAULT_COMMAND_TIMEOUT).await; - self.use_absolute_movements().await?; + if let MovementMode::AbsoluteMovements = + self.state.lock().unwrap().deref_mut().movement_mode + { + self.use_relative_movements()?; + let res = self.send_gcode(command, true, DEFAULT_COMMAND_TIMEOUT); + self.use_absolute_movements()?; res } else { - self.send_gcode(command, true, DEFAULT_COMMAND_TIMEOUT).await + self.send_gcode(command, true, DEFAULT_COMMAND_TIMEOUT) }?; - self.state.position.x += x; - self.state.position.y += y; - self.state.position.z += z; + self.state.lock().unwrap().deref_mut().position.x += x; + self.state.lock().unwrap().deref_mut().position.y += y; + self.state.lock().unwrap().deref_mut().position.z += z; Ok(()) } @@ -310,7 +307,7 @@ impl<S: AsyncRead + AsyncWrite + Sized> Printer<S> { /// /// * `x, y, z` - New position in printer units (mm) /// * `velocity` - Velocity that the printer should move at (mm/min) - pub async fn move_absolute( + pub fn move_absolute( &mut self, x: f64, y: f64, @@ -324,83 +321,155 @@ impl<S: AsyncRead + AsyncWrite + Sized> Printer<S> { e: None, // Machine has no e velocity, }; - if let MovementMode::RelativeMovements = self.state.movement_mode { - self.use_absolute_movements().await?; - let res = self.send_gcode(command, true, DEFAULT_COMMAND_TIMEOUT).await; - self.use_relative_movements().await?; + if let MovementMode::RelativeMovements = self.state.lock().unwrap().deref().movement_mode { + self.use_absolute_movements()?; + let res = self.send_gcode(command, true, DEFAULT_COMMAND_TIMEOUT); + self.use_relative_movements()?; res } else { - self.send_gcode(command, true, DEFAULT_COMMAND_TIMEOUT).await + self.send_gcode(command, true, DEFAULT_COMMAND_TIMEOUT) }?; - self.state.position.x = x; - self.state.position.y = y; - self.state.position.z = z; + self.state.lock().unwrap().deref_mut().position.x = x; + self.state.lock().unwrap().deref_mut().position.y = y; + self.state.lock().unwrap().deref_mut().position.z = z; Ok(()) } + + /// Background thread that handles direct communication with the printer serial port + /// + /// Parameters + /// * `state` - State that is shared between background thread and the thread that owns the + /// according `Printer` struct + fn io_thread_work( + to_user_thread: Sender<Vec<u8>>, + from_user_thread: Receiver<String>, + mut port: TTYPort, + state: Arc<Mutex<State>>, + ) { + loop { + match from_user_thread.try_recv() { + Ok(user_command) => handle_user_command(&mut port, user_command, state.clone()), + Err(TryRecvError::Disconnected) => break, + Err(TryRecvError::Empty) => handle_printer_autoreport(&mut port, state.clone()), + } + } + } } -impl Printer<SerialStream> { - pub async fn connect_to_path(port_path: &str) -> Result<Self, PrinterError> { - let mut port = tokio_serial::new(port_path, BAUD_RATE) - .open_native_async() +/// 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>>) { + todo!() +} + +/// Read a line from `port` unless it timeouts +/// +/// Parameters +/// * `port` - The port to read from +/// * `already_read` - Read bytes from a previous call. May not contain `\n` +/// * `timeout` - Time to wait before raising a timeout error +/// +/// Returns +/// `(potential_line, rest)`, where +/// - `potential_line` is `None` if no complete line was read or `Some(line)` if a whole +/// line was received from the port +/// - `rest` contains the bytes that were read since the last occurence of `\n` +fn read_line( + port: &mut TTYPort, + mut already_read: Vec<u8>, + timeout: Duration, +) -> io::Result<(Option<Vec<u8>>, Vec<u8>)> { + let deadline = Instant::now() + timeout; + while Instant::now() < deadline { + match port.read(&mut already_read) { + Err(e) => { + if let io::ErrorKind::TimedOut = e.kind() { + continue; + } else { + return Err(e); + } + } + Ok(0) => panic!("TTYPort returned 0 bytes!"), + Ok(n) => { + if let Some(line_break_idx) = already_read[..n].iter().position(|x| *x == b'\n') { + return Ok(( + Some(already_read[..line_break_idx].into()), + already_read[line_break_idx..].into(), + )); + } + } + } + } + Err(io::Error::new( + io::ErrorKind::TimedOut, + "Couldn't read a line within timeout!", + )) +} + +fn handle_user_command(port: &mut TTYPort, user_command: String, state: Arc<Mutex<State>>) { + port.write(user_command.as_bytes()) + .expect("Printer IO-Thread hung up its incoming mpsc"); + port.flush().unwrap(); + let ignore_pos = false; + let mut reply = String::with_capacity(RECV_BUFFER_CAPACITY); + let mut already_read = Vec::new(); + loop { + let (mut line, mut rest) = read_line(port, Vec::new(), DEFAULT_COMMAND_TIMEOUT).unwrap(); + let timeout = Instant::now() + DEFAULT_COMMAND_TIMEOUT; + while line.is_none() && Instant::now() < timeout { + let buf = Vec::new(); + _ = port.read(&mut buf); + if line.is_none() { + std::thread::sleep(Duration::from_millis(5)); + } + } + if let Some(line) = line { + let line = String::from_utf8(line).unwrap(); + if line.starts_with("ok") { + (*state.lock().unwrap()).last_buffer_capacity = Printer::parse_ok(&line)?; + let reply = command_parser(&reply); + return reply.map_err(PrinterError::GcodeReply); + } else { + reply.push_str(&line); + } + } + } +} + +impl Printer { + pub fn connect_to_raw_fd(port: RawFd) -> Result<Self, PrinterError> { + let port = unsafe { serialport::TTYPort::from_raw_fd(port) }; + // We can't set port exclusive here as in `connect_to_path()`, since a parent + // process might still have the fd open. + + Self::connect(port) + } + + pub fn connect_to_path(port_path: &str) -> Result<Self, PrinterError> { + let mut port = serialport::new(port_path, BAUD_RATE) + .timeout(Duration::from_millis(100)) + .open_native() .expect("Unable to open serial port"); - port.set_exclusive(true) - .expect("Unable to set serial port exclusive to false"); - - Self::connect(port).await - } -} - -impl Printer<File> { - pub async fn connect_to_raw_fd(port: RawFd) -> Result<Self, PrinterError> { - let port = unsafe { File::from_raw_fd(port) }; - Self::connect(port).await - } -} - -struct LineCodec; - -impl Decoder for LineCodec { - type Item = String; - type Error = io::Error; - - fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> { - let newline = src.as_ref().iter().position(|b| *b == b'\n'); - if let Some(n) = newline { - let line = src.split_to(n + 1); - return match str::from_utf8(line.as_ref()) { - Ok(s) => { - println!(">>{}", s); - Ok(Some(s.to_string())) - } - Err(_) => Err(io::Error::new(io::ErrorKind::Other, "Invalid String")), - }; - } - Ok(None) - } -} - -impl Encoder<String> for LineCodec { - type Error = io::Error; - - fn encode(&mut self, item: String, dst: &mut BytesMut) -> Result<(), Self::Error> { - println!("<<{}", item); - dst.write_str(&item) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; - Ok(()) + Self::connect(port) } } #[cfg(test)] mod test { - use super::Printer; + use super::{Printer, IO_THREAD_TIMEOUT, SERIALPORT_TIMEOUT}; #[test] fn test_parse_ok() { let buffer_cap = Printer::parse_ok("ok P10 B4564").unwrap(); - assert!(buffer_cap == 10); + assert_eq!(buffer_cap, 10); + } + + #[test] + fn check_timeout_sanity() { + // The user thread will wait for the IO-thread. The IO-thread then needs to have enough time + // to recognize a serial port timeout and propagate that to the user thread. + assert!(IO_THREAD_TIMEOUT > SERIALPORT_TIMEOUT) } }