diff --git a/Cargo.toml b/Cargo.toml index 0aec73c..93af9a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,4 @@ edition = "2021" [dependencies] macroquad = "0.4.13" serialport = "4.5.1" + diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..5d56faf --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" diff --git a/src/main.rs b/src/main.rs index 57bdd2c..306a1d5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,43 +1,61 @@ +#![feature(random)] use core::f32; -use std::time::Duration; +use std::{ + fs::read_dir, + path::PathBuf, + sync::mpsc::{self, Receiver, Sender}, + thread::{self, JoinHandle}, + time::Duration, +}; use macroquad::prelude::*; -use serialport::SerialPort; const THICKNESS: f32 = 2.0; -const TAIL_LEN: usize = 500; +const TAIL_LEN: usize = 200; +const BAUD_RATE: u32 = 57600; +static mut PORT: Option = None; -#[derive(Clone,Copy, PartialEq)] +#[derive(Clone, Copy, PartialEq)] struct Point { x: f32, y: f32, } impl Point { fn new(x: f32, y: f32) -> Self { - Self {x,y} + Self { x, y } } } -struct DebugWindow { +struct DebugWindow<'a> { text: Vec, text_margin: f32, + font: Option<&'a Font>, } -impl DebugWindow { - fn new() -> Self { +impl<'a> DebugWindow<'a> { + fn new(font: Option<&'a Font>) -> Self { Self { text: Vec::new(), text_margin: 20.0, + font, } } fn add_line(&mut self, line: String) { self.text.push(line); } + fn get_params(&self) -> TextParams { + let params = TextParams { + color: WHITE, + font: self.font, + ..Default::default() + }; + params + } fn draw(&self) { - let params = TextParams { color: WHITE, ..Default::default()} ; + let params = self.get_params(); self.text.iter().enumerate().for_each(|(index, text)| { let x = self.text_margin; - let y = self.text_margin+20.*(index as f32 + 1.); + let y = self.text_margin + 20. * (index as f32 + 1.); // let measurement = measure_text(text, None, params.font_size, params.font_scale); // draw_rectangle(x,y,measurement.width,-measurement.height, Color { r: 1., g: 1., b: 1., a: 0.25 }); @@ -48,12 +66,13 @@ impl DebugWindow { enum Axis { One, - Two + Two, } /// # Graph /// Holds 1 contiguous line. Can be graphed on 1 or 2 axis. -struct Graph { +struct Graph<'a> { + font: Option<&'a Font>, points: [Point; TAIL_LEN], head: usize, axises: Axis, @@ -66,35 +85,79 @@ struct Graph { /// How fast X scrolls when using a since axis tail px_per_s: f32, } -impl Graph { - fn new(axises: Axis) -> Self { +impl<'a> Graph<'a> { + fn recalculate_origin(&mut self) { + let (x_offset, y_offset) = match self.axises { + Axis::One => (0., screen_height() / 2.), + Axis::Two => (screen_width() / 2., screen_height() / 2.), + }; + self.x_origin = x_offset; + self.y_origin = y_offset; + } + fn new(axises: Axis, font: Option<&'a Font>) -> Self { let (x_offset, y_offset) = match axises { - Axis::One => { - (0., screen_height()*0.5) - }, - Axis::Two => { - (screen_width()/2.,screen_height()/2.) - }, + Axis::One => (0., screen_height() / 2.), + Axis::Two => (screen_width() / 2., screen_height() / 2.), }; Self { + font, // start all the dots off screen - points: [Point::new(-1.,-1.); TAIL_LEN], + points: [Point::new(-1., -1.); TAIL_LEN], head: 0, axises, x_origin: x_offset, y_origin: y_offset, - px_per_s: 750., + px_per_s: 800., head_tracker: 0., } } + fn draw_time(&self) { + let ms_per_px = self.px_per_s / 1000.; + // how many ms per segment + let segment_len_ms = 100.; + let px_offset = ms_per_px * segment_len_ms; + let segments = screen_width() / px_offset; + + let mut params = TextParams::default(); + params.font = self.font; + params.font_size = 20; + params.color = DARKGRAY; + + let y2 = screen_height(); + for i in 0..=segments as i32 { + let x = i as f32 * px_offset; + draw_line(x, 0., x, y2, 2., DARKGRAY); + + draw_text_ex( + &format!("{}ms", segment_len_ms * i as f32), + x, + y2 / 2., + params.clone(), + ); + } + } fn draw_axises(&self) { // Horzontal line // (0, y_origin) -> (max, y_origin) - draw_line(0.,self.y_origin,screen_width(),self.y_origin,1.,DARKGRAY); + draw_line( + 0., + self.y_origin, + screen_width(), + self.y_origin, + 1., + DARKGRAY, + ); // Vertical line // (x_origin, 0) -> (x_origin, max) - draw_line(self.x_origin,0.,self.x_origin,screen_height(),1.,DARKGRAY); + draw_line( + self.x_origin, + 0., + self.x_origin, + screen_height(), + 1., + DARKGRAY, + ); } fn place_next(&mut self, delta_time: f32, x_displacement: f32, y_displacement: f32) { match self.axises { @@ -105,16 +168,22 @@ impl Graph { } else { self.head_tracker = self.head_tracker + (delta_time * self.px_per_s) } - self.push(Point::new(self.head_tracker, self.y_origin-y_displacement)); - }, + self.push(Point::new( + self.head_tracker, + self.y_origin - y_displacement, + )); + } Axis::Two => { // both x and y will get displaced - self.push(Point::new(self.x_origin+x_displacement,self.y_origin-y_displacement)) - }, - } - } + self.push(Point::new( + self.x_origin + x_displacement, + self.y_origin - y_displacement, + )) + } + } + } fn push(&mut self, dot: Point) { - if self.head >= self.points.len() -1 { + if self.head >= self.points.len() - 1 { self.head = 0 } else { self.head += 1 @@ -123,72 +192,144 @@ impl Graph { self.points[self.head] = dot; } fn draw(&self) { - self.points.into_iter().enumerate().for_each(|(index, point) |{ - // dots fade - let virtual_index = if index > self.head { - index-self.head - } else { - index + (self.points.len() - self.head) - }; - let faded = virtual_index as f32 / self.points.len() as f32; - let faded_inverse = 1. - faded; - let color = Color { r: faded_inverse, g: faded, b: 0.2, a: faded+0.1}; + self.points + .into_iter() + .enumerate() + .for_each(|(index, point)| { + // dots fade + let virtual_index = if index > self.head { + index - self.head + } else { + index + (self.points.len() - self.head) + }; + let faded = virtual_index as f32 / self.points.len() as f32; + let faded_inverse = 1. - faded; + let color = Color { + r: faded_inverse, + g: faded, + b: 0.2, + a: faded + 0.1, + }; - let prev = self.previous(index); + let prev = self.previous(index); - if point.x == 0. || point.y == 0. || prev.x == -1. || prev.y == -1. { - // Don't have lines that cross the whole screen while using 1D graph - // draw_circle(point.x, point.y,THICKNESS, color); - } else if self.head == index { - // Draw the head differently - draw_circle(point.x, point.y,THICKNESS*2., color); - draw_line(point.x, point.y, prev.x, prev.y, THICKNESS, color); - } else if prev == self.points[self.head] { - // Prevent the tail from connecting to the head - draw_circle(point.x, point.y,THICKNESS, color); - } else { - draw_line(point.x, point.y, prev.x, prev.y, THICKNESS, color); - } - }); + if point.x == 0. || point.y == 0. || prev.x == -1. || prev.y == -1. { + // Don't have lines that cross the whole screen while using 1D graph + // draw_circle(point.x, point.y,THICKNESS, color); + } else if self.head == index { + // Draw the head differently + draw_circle(point.x, point.y, THICKNESS * 2., color); + draw_line(point.x, point.y, prev.x, prev.y, THICKNESS, color); + } else if prev == self.points[self.head] { + // Prevent the tail from connecting to the head + draw_circle(point.x, point.y, THICKNESS, color); + } else { + draw_line(point.x, point.y, prev.x, prev.y, THICKNESS, color); + } + }); } fn previous(&self, index: usize) -> Point { let prev = if index > 0 { - index -1 + index - 1 } else { - self.points.len() -1 + self.points.len() - 1 }; self.points[prev] } } +async fn get_font() -> Option { + #[cfg(target_os = "windows")] + let fonts_path = "C:\\Windows\\Fonts"; + + #[cfg(target_os = "linux")] + let fonts_path = "/usr/share/fonts/"; + + if let Ok(dir) = read_dir(fonts_path) { + let selection = dir + .filter_map(|f| f.ok()) + .map(|f| (f.file_name(), f.path())) + .map(|(a, b)| (a.into_string(), b)) + .filter(|(a, _)| a.is_ok()) + .map(|(a, b)| (a.unwrap(), b)) + // Arial is a good windows default, Adwaita is a good Linux default + .filter(|(a, _)| a.contains("Arial") | a.contains("Adwaita")) + .map(|(_, b)| b) + .collect::>(); + if selection.len() > 0 { + if let Ok(dir) = read_dir(&selection[0]) { + let ttfs = dir + .filter_map(|f| f.ok()) + .map(|f| (f.file_name(), f.path())) + .map(|(a, b)| (a.into_string(), b)) + .filter(|(a, _)| a.is_ok()) + .map(|(a, b)| (a.unwrap(), b)) + .filter(|(a, _)| a.contains("Regular")) + .filter(|(a, _)| a.contains("Mono")) + .filter(|(a, _)| a.contains("ttf")) + .map(|(_, b)| b) + .collect::>(); + + if ttfs.len() > 0 { + let selection = ttfs[0].to_str().expect("Bad path"); + let ttf = load_ttf_font(selection) + .await + .expect("Cannot load ttf {selection}"); + println!("Loaded \"{:?}\"", ttfs[0]); + return Some(ttf); + } else { + eprintln!( + "So sutable fonts found in the sub dir \"{:?}\"", + selection[0] + ); + } + } + } else { + eprintln!("No fonts found"); + } + } + None +} + #[macroquad::main("Graph")] async fn main() { - // Dot params - let mut graph = Graph::new(Axis::One); + let font = get_font().await; + + // Dot params + let mut graph = Graph::new(Axis::One, font.as_ref()); // Tooling let mut show_debug = true; let mut plot_delta_time = 0.; + let mut pause = false; // Serial - let mut port = get_serial_port(); - let mut serial_buf: Vec = vec![0; 8]; + let (rx, _handle) = get_serial_port_or_demo(); loop { clear_background(BLACK); + graph.recalculate_origin(); graph.draw_axises(); + graph.draw_time(); let frame_delta_time = get_frame_time(); // keep track of the delta time since last plot plot_delta_time += frame_delta_time; - if let Ok(x) = port.read(serial_buf.as_mut_slice()) { - let s: String = serial_buf.iter().take(x-1).map(|x| *x as char).collect(); - if let Ok(parse) = s.parse::() { - // Only draw when requied - graph.place_next(plot_delta_time, 0., parse); - plot_delta_time = 0.; - } + if !pause { + match rx.try_recv() { + Ok(x) => { + graph.place_next(plot_delta_time, 0., x as f32); + plot_delta_time = 0.; + } + Err(x) => match x { + mpsc::TryRecvError::Empty => {} + mpsc::TryRecvError::Disconnected => { + eprintln!("Sender hung-up."); + return; + } + }, + } } graph.draw(); @@ -196,39 +337,104 @@ async fn main() { if is_key_pressed(KeyCode::F3) { show_debug = !show_debug; } - // toggle debug box - if show_debug { - // Debug cursor information - let (mouse_x, mouse_y) = mouse_position(); - let size = measure_text(&format!("x{mouse_x}, y{}",-(mouse_y-graph.y_origin)), None, 30,1.); - draw_rectangle(mouse_x, mouse_y+(size.offset_y/4.), size.width, -(size.height), WHITE); - draw_text(&format!("x{}, y{}", mouse_x+graph.x_origin,-(mouse_y-graph.y_origin)), mouse_x, mouse_y, 30., BLACK); - - // Debug textbox - let mut debug = DebugWindow::new(); - debug.add_line(format!("FPS {:04}, Frametime {:05.2}ms", get_fps(), frame_delta_time * 1000.0)); - debug.add_line(format!("Tail Length {TAIL_LEN}, px/s {:.2}", graph.px_per_s)); - debug.add_line(format!("Cursor Pos {:?}", mouse_position())); - debug.add_line(format!("Serial Port {}, Baud Rate {}", port.name().unwrap_or("Unknown".to_string()), port.baud_rate().map_or(String::from("N/A"), |f| f.to_string()))); - debug.draw(); + if is_key_pressed(KeyCode::F1) { + pause = !pause; } + let (_, my) = mouse_wheel(); + graph.px_per_s += 10. * my; + + // toggle debug box + if show_debug { + let mut debug = DebugWindow::new(font.as_ref()); + let params = debug.get_params(); + // Debug cursor information + let (mouse_x, mouse_y) = mouse_position(); + let text = &format!("x{mouse_x}, y{}", -(mouse_y - graph.y_origin)); + let size = measure_text(text, params.font, params.font_size, params.font_scale); + draw_rectangle( + mouse_x, + mouse_y + (size.offset_y / 4.), + size.width, + -(size.height), + BLUE, + ); + draw_text_ex(text, mouse_x, mouse_y, params); + + // Debug textbox + debug.add_line(format!( + "FPS {:04}, Frametime {:05.2}ms", + get_fps(), + frame_delta_time * 1000.0 + )); + debug.add_line(format!( + "Tail Length {TAIL_LEN}, px/s {:.2}", + graph.px_per_s + )); + debug.add_line(format!("Cursor Pos {:?}", mouse_position())); + + if let Some(port_name) = unsafe { PORT.clone() } { + debug.add_line(format!("Serial Port {port_name}, Baud Rate {BAUD_RATE}")); + } + + debug.draw(); + } next_frame().await; } } -fn get_serial_port() -> Box { - let port = serialport::available_ports() - .expect("No ports found!") - .iter() - .filter(|x| x.port_name.contains("ACM")) - .fold(None, |_, x| { - let x = serialport::new(x.port_name.clone(), 57600) - .timeout(Duration::from_millis(10)) - .open().expect("Failed to open port"); - Some(x) - }); - let port = port.expect("Failed to find valid serial port"); - port +type INT = i32; + +fn get_serial_port_or_demo() -> (Receiver, JoinHandle<()>) { + let (tx, rx): (Sender, Receiver) = mpsc::channel(); + + let handle = match serialport::available_ports() { + Ok(ok) => { + let x = ok + .iter() + .filter(|x| x.port_name.contains("ACM")) + .take(1) + .collect::>(); + if x.len() > 0 { + let port = x[0]; + unsafe { + PORT = Some(port.port_name.clone()); + } + + let mut x = serialport::new(port.port_name.clone(), BAUD_RATE) + .timeout(Duration::from_millis(10)) + .open() + .expect("Failed to open port"); + + let handle = thread::spawn(move || loop { + let mut serial_buf: Vec = vec![0; 8]; + if let Ok(x) = x.read(serial_buf.as_mut_slice()) { + for i in &serial_buf[..x - 1] { + // this data may need to be cleansed + let _err = tx.send(i.clone() as INT); + } + } + }); + + handle + } else { + dummy_port(tx) + } + } + Err(_err) => dummy_port(tx), + }; + + (rx, handle) } +fn dummy_port(tx: Sender) -> JoinHandle<()> { + let handle = thread::spawn(move || loop { + let a: u8 = std::random::random(..); + // center around y origin + let a: INT = a as INT - ((u8::MAX / 2) as INT); + + let _err = tx.send(a); + thread::sleep(Duration::from_millis(10)) + }); + handle +}