diff --git a/Cargo.lock b/Cargo.lock index 88409af..f1d0703 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "adler2" @@ -32,6 +32,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + [[package]] name = "bytemuck" version = "1.18.0" @@ -56,6 +62,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "crc32fast" version = "1.4.2" @@ -122,12 +134,51 @@ dependencies = [ "png", ] +[[package]] +name = "io-kit-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b" +dependencies = [ + "core-foundation-sys", + "mach2", +] + [[package]] name = "libc" version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +[[package]] +name = "libudev" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b324152da65df7bb95acfcaab55e3097ceaab02fb19b228a9eb74d55f135e0" +dependencies = [ + "libc", + "libudev-sys", +] + +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "mach2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +dependencies = [ + "libc", +] + [[package]] name = "macroquad" version = "0.4.13" @@ -186,6 +237,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1bcdd74c20ad5d95aacd60ef9ba40fdf77f767051040541df557b7a9b2a2121" +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -213,13 +275,19 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + [[package]] name = "png" version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52f9d46a34a05a6a57566bc2bfae066ef07585a6e3fa30fbbdff5936380623f0" dependencies = [ - "bitflags", + "bitflags 1.3.2", "crc32fast", "fdeflate", "flate2", @@ -246,6 +314,7 @@ name = "quad" version = "0.1.0" dependencies = [ "macroquad", + "serialport", ] [[package]] @@ -263,6 +332,30 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serialport" +version = "4.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ba776acc8c373b9175829206229366273225436845c04f9c20aab8099960e2e" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "core-foundation-sys", + "io-kit-sys", + "libudev", + "mach2", + "nix", + "scopeguard", + "unescaper", + "winapi", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -289,12 +382,41 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "ttf-parser" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b3e06c9b9d80ed6b745c7159c40b311ad2916abb34a49e9be2653b90db0d8dd" +[[package]] +name = "unescaper" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c878a167baa8afd137494101a688ef8c67125089ff2249284bd2b5f9bfedb815" +dependencies = [ + "thiserror", +] + [[package]] name = "unicode-ident" version = "1.0.13" diff --git a/Cargo.toml b/Cargo.toml index 10010e4..93af9a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,3 +5,5 @@ 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 c1d6031..306a1d5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,44 +1,61 @@ +#![feature(random)] use core::f32; -use std::io::stdin; +use std::{ + fs::read_dir, + path::PathBuf, + sync::mpsc::{self, Receiver, Sender}, + thread::{self, JoinHandle}, + time::Duration, +}; use macroquad::prelude::*; -const DOT_RADIUS: f32 = 1.0; +const THICKNESS: f32 = 2.0; +const TAIL_LEN: usize = 200; +const BAUD_RATE: u32 = 57600; +static mut PORT: Option = None; -struct Dot { +#[derive(Clone, Copy, PartialEq)] +struct Point { x: f32, y: f32, } - -impl Dot { +impl Point { fn new(x: f32, y: f32) -> Self { - Self {x,y} - } - fn draw(&self) { - draw_circle(self.x, self.y,DOT_RADIUS, GREEN); + 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 }); @@ -47,78 +64,377 @@ impl DebugWindow { } } +enum Axis { + One, + Two, +} + +/// # Graph +/// Holds 1 contiguous line. Can be graphed on 1 or 2 axis. +struct Graph<'a> { + font: Option<&'a Font>, + points: [Point; TAIL_LEN], + head: usize, + axises: Axis, + /// What the tail treats as 0 + head_tracker: f32, + /// The graph's local origin + x_origin: f32, + /// The graph's local origin + y_origin: f32, + /// How fast X scrolls when using a since axis tail + px_per_s: f32, +} +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() / 2.), + Axis::Two => (screen_width() / 2., screen_height() / 2.), + }; + + Self { + font, + // start all the dots off screen + points: [Point::new(-1., -1.); TAIL_LEN], + head: 0, + axises, + x_origin: x_offset, + y_origin: y_offset, + 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, + ); + // Vertical line + // (x_origin, 0) -> (x_origin, max) + 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 { + Axis::One => { + // x will scroll while y gets displaced + if self.head_tracker >= screen_width() { + self.head_tracker = 0.; + } 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, + )); + } + Axis::Two => { + // both x and y will get displaced + 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 { + self.head = 0 + } else { + self.head += 1 + } + + 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, + }; + + 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); + } + }); + } + fn previous(&self, index: usize) -> Point { + let prev = if index > 0 { + index - 1 + } else { + 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 y_offset = screen_height() / 2.0; - let mut x_offset = screen_width(); - let mut trail: Vec = Vec::new(); + let font = get_font().await; + + // Dot params + let mut graph = Graph::new(Axis::One, font.as_ref()); - // Selection box - let mut inital_x_pos = 0.; - let mut inital_y_pos = 0.; - // Tooling - let mut show_debug = false; - let mut px_per_s: f32 = 100.; + let mut show_debug = true; + let mut plot_delta_time = 0.; + let mut pause = false; - - let stdin = stdin(); - let handle= stdin.lock(); + // Serial + let (rx, _handle) = get_serial_port_or_demo(); loop { clear_background(BLACK); - // handle. - let delta_time = get_frame_time(); + graph.recalculate_origin(); + graph.draw_axises(); + graph.draw_time(); - if x_offset >= screen_width() { - // wrap once you've hit the end of the line - x_offset = 0.; - trail.clear(); - } else { - x_offset = x_offset + (delta_time * px_per_s); + let frame_delta_time = get_frame_time(); + // keep track of the delta time since last plot + plot_delta_time += frame_delta_time; + + 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; + } + }, + } } - let x = 0.+x_offset; - let y = 0.+y_offset; + graph.draw(); - let dot = Dot::new(x,y); - trail.push(dot); - trail.iter().for_each(|d| d.draw()); - - if is_key_pressed(KeyCode::D) { + if is_key_pressed(KeyCode::F3) { show_debug = !show_debug; } - if is_key_pressed(KeyCode::Up) { - px_per_s += 5.; + if is_key_pressed(KeyCode::F1) { + pause = !pause; } - if is_key_pressed(KeyCode::Down) { - px_per_s -= 5.; - } - // selection box - if is_mouse_button_pressed(MouseButton::Left) { - (inital_x_pos, inital_y_pos) = mouse_position(); - } - if is_mouse_button_down(MouseButton::Left) { - let (x, y) = mouse_position(); - let width = x - inital_x_pos; - let height = y - inital_y_pos; + let (_, my) = mouse_wheel(); + graph.px_per_s += 10. * my; - draw_rectangle(inital_x_pos, inital_y_pos, width, height, Color { r: 0.2, g: 0.1, b: 1., a: 0.3 }); - } // toggle debug box if show_debug { - let mut debug = DebugWindow::new(); - debug.add_line(format!("FPS {}, Latency {:.2}ms", get_fps(), delta_time * 1000.0)); - debug.add_line(format!("px/s {}", px_per_s)); - debug.add_line(format!("Dots {}", trail.len())); + 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; } } +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 +}