make fanicer

This commit is contained in:
2026-01-29 13:13:59 -07:00
parent 3604a9405b
commit 7cee5eb4c5
3 changed files with 306 additions and 97 deletions

View File

@@ -6,3 +6,4 @@ edition = "2021"
[dependencies] [dependencies]
macroquad = "0.4.13" macroquad = "0.4.13"
serialport = "4.5.1" serialport = "4.5.1"

2
rust-toolchain.toml Normal file
View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -1,43 +1,61 @@
#![feature(random)]
use core::f32; 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 macroquad::prelude::*;
use serialport::SerialPort;
const THICKNESS: f32 = 2.0; const THICKNESS: f32 = 2.0;
const TAIL_LEN: usize = 500; const TAIL_LEN: usize = 200;
const BAUD_RATE: u32 = 57600;
static mut PORT: Option<String> = None;
#[derive(Clone,Copy, PartialEq)] #[derive(Clone, Copy, PartialEq)]
struct Point { struct Point {
x: f32, x: f32,
y: f32, y: f32,
} }
impl Point { impl Point {
fn new(x: f32, y: f32) -> Self { fn new(x: f32, y: f32) -> Self {
Self {x,y} Self { x, y }
} }
} }
struct DebugWindow { struct DebugWindow<'a> {
text: Vec<String>, text: Vec<String>,
text_margin: f32, text_margin: f32,
font: Option<&'a Font>,
} }
impl DebugWindow { impl<'a> DebugWindow<'a> {
fn new() -> Self { fn new(font: Option<&'a Font>) -> Self {
Self { Self {
text: Vec::new(), text: Vec::new(),
text_margin: 20.0, text_margin: 20.0,
font,
} }
} }
fn add_line(&mut self, line: String) { fn add_line(&mut self, line: String) {
self.text.push(line); self.text.push(line);
} }
fn get_params(&self) -> TextParams {
let params = TextParams {
color: WHITE,
font: self.font,
..Default::default()
};
params
}
fn draw(&self) { fn draw(&self) {
let params = TextParams { color: WHITE, ..Default::default()} ; let params = self.get_params();
self.text.iter().enumerate().for_each(|(index, text)| { self.text.iter().enumerate().for_each(|(index, text)| {
let x = self.text_margin; 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); // 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 }); // 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 { enum Axis {
One, One,
Two Two,
} }
/// # Graph /// # Graph
/// Holds 1 contiguous line. Can be graphed on 1 or 2 axis. /// 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], points: [Point; TAIL_LEN],
head: usize, head: usize,
axises: Axis, axises: Axis,
@@ -66,35 +85,79 @@ struct Graph {
/// How fast X scrolls when using a since axis tail /// How fast X scrolls when using a since axis tail
px_per_s: f32, px_per_s: f32,
} }
impl Graph { impl<'a> Graph<'a> {
fn new(axises: Axis) -> Self { 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 { let (x_offset, y_offset) = match axises {
Axis::One => { Axis::One => (0., screen_height() / 2.),
(0., screen_height()*0.5) Axis::Two => (screen_width() / 2., screen_height() / 2.),
},
Axis::Two => {
(screen_width()/2.,screen_height()/2.)
},
}; };
Self { Self {
font,
// start all the dots off screen // start all the dots off screen
points: [Point::new(-1.,-1.); TAIL_LEN], points: [Point::new(-1., -1.); TAIL_LEN],
head: 0, head: 0,
axises, axises,
x_origin: x_offset, x_origin: x_offset,
y_origin: y_offset, y_origin: y_offset,
px_per_s: 750., px_per_s: 800.,
head_tracker: 0., 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) { fn draw_axises(&self) {
// Horzontal line // Horzontal line
// (0, y_origin) -> (max, y_origin) // (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 // Vertical line
// (x_origin, 0) -> (x_origin, max) // (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) { fn place_next(&mut self, delta_time: f32, x_displacement: f32, y_displacement: f32) {
match self.axises { match self.axises {
@@ -105,16 +168,22 @@ impl Graph {
} else { } else {
self.head_tracker = self.head_tracker + (delta_time * self.px_per_s) 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 => { Axis::Two => {
// both x and y will get displaced // 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) { fn push(&mut self, dot: Point) {
if self.head >= self.points.len() -1 { if self.head >= self.points.len() - 1 {
self.head = 0 self.head = 0
} else { } else {
self.head += 1 self.head += 1
@@ -123,72 +192,144 @@ impl Graph {
self.points[self.head] = dot; self.points[self.head] = dot;
} }
fn draw(&self) { fn draw(&self) {
self.points.into_iter().enumerate().for_each(|(index, point) |{ self.points
// dots fade .into_iter()
let virtual_index = if index > self.head { .enumerate()
index-self.head .for_each(|(index, point)| {
} else { // dots fade
index + (self.points.len() - self.head) let virtual_index = if index > self.head {
}; index - self.head
let faded = virtual_index as f32 / self.points.len() as f32; } else {
let faded_inverse = 1. - faded; index + (self.points.len() - self.head)
let color = Color { r: faded_inverse, g: faded, b: 0.2, a: faded+0.1}; };
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. { 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 // Don't have lines that cross the whole screen while using 1D graph
// draw_circle(point.x, point.y,THICKNESS, color); // draw_circle(point.x, point.y,THICKNESS, color);
} else if self.head == index { } else if self.head == index {
// Draw the head differently // Draw the head differently
draw_circle(point.x, point.y,THICKNESS*2., color); draw_circle(point.x, point.y, THICKNESS * 2., color);
draw_line(point.x, point.y, prev.x, prev.y, THICKNESS, color); draw_line(point.x, point.y, prev.x, prev.y, THICKNESS, color);
} else if prev == self.points[self.head] { } else if prev == self.points[self.head] {
// Prevent the tail from connecting to the head // Prevent the tail from connecting to the head
draw_circle(point.x, point.y,THICKNESS, color); draw_circle(point.x, point.y, THICKNESS, color);
} else { } else {
draw_line(point.x, point.y, prev.x, prev.y, THICKNESS, color); draw_line(point.x, point.y, prev.x, prev.y, THICKNESS, color);
} }
}); });
} }
fn previous(&self, index: usize) -> Point { fn previous(&self, index: usize) -> Point {
let prev = if index > 0 { let prev = if index > 0 {
index -1 index - 1
} else { } else {
self.points.len() -1 self.points.len() - 1
}; };
self.points[prev] self.points[prev]
} }
} }
async fn get_font() -> Option<Font> {
#[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::<Vec<PathBuf>>();
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::<Vec<PathBuf>>();
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")] #[macroquad::main("Graph")]
async fn main() { async fn main() {
// Dot params let font = get_font().await;
let mut graph = Graph::new(Axis::One);
// Dot params
let mut graph = Graph::new(Axis::One, font.as_ref());
// Tooling // Tooling
let mut show_debug = true; let mut show_debug = true;
let mut plot_delta_time = 0.; let mut plot_delta_time = 0.;
let mut pause = false;
// Serial // Serial
let mut port = get_serial_port(); let (rx, _handle) = get_serial_port_or_demo();
let mut serial_buf: Vec<u8> = vec![0; 8];
loop { loop {
clear_background(BLACK); clear_background(BLACK);
graph.recalculate_origin();
graph.draw_axises(); graph.draw_axises();
graph.draw_time();
let frame_delta_time = get_frame_time(); let frame_delta_time = get_frame_time();
// keep track of the delta time since last plot // keep track of the delta time since last plot
plot_delta_time += frame_delta_time; plot_delta_time += frame_delta_time;
if let Ok(x) = port.read(serial_buf.as_mut_slice()) { if !pause {
let s: String = serial_buf.iter().take(x-1).map(|x| *x as char).collect(); match rx.try_recv() {
if let Ok(parse) = s.parse::<f32>() { Ok(x) => {
// Only draw when requied graph.place_next(plot_delta_time, 0., x as f32);
graph.place_next(plot_delta_time, 0., parse); plot_delta_time = 0.;
plot_delta_time = 0.; }
} Err(x) => match x {
mpsc::TryRecvError::Empty => {}
mpsc::TryRecvError::Disconnected => {
eprintln!("Sender hung-up.");
return;
}
},
}
} }
graph.draw(); graph.draw();
@@ -196,39 +337,104 @@ async fn main() {
if is_key_pressed(KeyCode::F3) { if is_key_pressed(KeyCode::F3) {
show_debug = !show_debug; show_debug = !show_debug;
} }
// toggle debug box if is_key_pressed(KeyCode::F1) {
if show_debug { pause = !pause;
// 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();
} }
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; next_frame().await;
} }
} }
fn get_serial_port() -> Box<dyn SerialPort> { type INT = i32;
let port = serialport::available_ports()
.expect("No ports found!") fn get_serial_port_or_demo() -> (Receiver<INT>, JoinHandle<()>) {
.iter() let (tx, rx): (Sender<INT>, Receiver<INT>) = mpsc::channel();
.filter(|x| x.port_name.contains("ACM"))
.fold(None, |_, x| { let handle = match serialport::available_ports() {
let x = serialport::new(x.port_name.clone(), 57600) Ok(ok) => {
.timeout(Duration::from_millis(10)) let x = ok
.open().expect("Failed to open port"); .iter()
Some(x) .filter(|x| x.port_name.contains("ACM"))
}); .take(1)
let port = port.expect("Failed to find valid serial port"); .collect::<Vec<&serialport::SerialPortInfo>>();
port 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<u8> = 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<INT>) -> 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
}