#![feature(try_blocks)] use iced::application::Update; use iced::futures::channel::mpsc; use iced::futures::{SinkExt, StreamExt}; use iced::{font, padding, widget::{ canvas::{Cache, Frame, Geometry}, Column, Container, Text, }, Alignment, Element, Font, Length, Size, Subscription, Task}; use plotters::prelude::ChartBuilder; use plotters_backend::DrawingBackend; use plotters_iced::{Chart, ChartWidget, Renderer}; use serialport::{new, SerialPort}; use std::collections::HashMap; use std::io::{BufRead, Write}; use std::ops::Range; use std::{collections::VecDeque, time::Duration}; use iced::widget::Row; use tokio::io::AsyncBufReadExt; use tokio_serial::SerialPortBuilderExt; const PLOT_DEPTH: usize = 500; const TITLE_FONT_SIZE: u16 = 22; const FPS: u64 = 144; const FONT_BOLD: Font = Font { family: font::Family::Name("Noto Sans"), weight: font::Weight::Bold, ..Font::DEFAULT }; macro_rules! named_range { ($($name:literal, $min:expr, $max:expr);*$(;)?) => { $( ($name.to_string(), CpuUsageChart::new($name.to_string(), ($min as f32)..($max as f32))) )* } } fn main() { let (mut tx, rx) = mpsc::channel(4); let runtime = tokio::runtime::Builder::new_multi_thread() .enable_all() .build().unwrap(); runtime.spawn(async move { loop { let result: eyre::Result<()> = try { let mut ser = tokio_serial::new("COM14", 115200) .timeout(Duration::from_millis(10)) .open_native_async()?; println!("opened port ok"); ser.flush()?; ser.clear_break()?; ser.write_data_terminal_ready(true)?; println!("configured port"); let mut reader = tokio::io::BufReader::new(ser); let mut line = String::new(); loop { line.clear(); reader.read_line(&mut line).await?; for item in line.split(",") { let item = item.trim(); if item.len() == 0 { continue; } let split = item.split(":").collect::>(); if split.len() != 2 { continue; } let name = split[0].to_string(); let value = split[1].parse::()?; tx.send(Message::Line { name, data: value, }).await.unwrap(); } } }; if let Err(e) = result { eprintln!("err: {e}"); tokio::time::sleep(Duration::from_millis(100)).await; } } }); let app = iced::application("ocularium data viewer", State::update, State::view) .antialiasing(true) .default_font(Font::with_name("Noto Sans")); app .run_with(|| State::new(rx, [ named_range!("az", -4, 4), named_range!("ay", -4, 4), named_range!("ax", -4, 4), named_range!("gx", -2000, 2000), named_range!("gy", -2000, 2000), named_range!("gz", -2000, 2000), named_range!("temp", 10, 40), named_range!("hum", 0, 100), named_range!("pres", 90000, 110000), named_range!("gas", 0, 100000), // named_range!("iaq", 0, 100), ])) .unwrap(); } #[derive(Debug)] enum Message { Line { name: String, data: f32, } } struct State { order: Vec, chart_by_name: HashMap, } impl State { fn new(channel: mpsc::Receiver, chart_by_name: impl IntoIterator) -> (Self, Task) { let elems = chart_by_name.into_iter().collect::>(); ( Self { order: elems.iter().map(|x| x.0.clone()).collect(), chart_by_name: elems.into_iter().collect(), }, Task::stream(channel), ) } fn update(&mut self, message: Message) { match message { Message::Line { name, data } => { if let Some(chart) = self.chart_by_name.get_mut(&name) { chart.push_data(data); } }, } } fn view(&self) -> Element<'_, Message> { let n_columns = (self.order.len() as f32 / 3.).ceil() as usize; let mut columns = vec![]; for _column in 0..n_columns { columns.push(Some(Column::new() .spacing(20) .align_x(Alignment::Start) .width(Length::Fill) .height(Length::Fill))); } for (i, chart) in self.order.iter().enumerate() { let column = &mut columns[i / 3]; let column = column.take().unwrap(); let chart = self.chart_by_name.get(chart).unwrap(); columns[i / 3].insert(column.push(chart.view())); } let mut row = Row::new() .height(Length::Fill) .width(Length::Fill) ; for column in columns { row = row.push_maybe(column); } Container::new(row) .padding(5) .center_x(Length::Fill) .center_y(Length::Fill) .into() } } struct CpuUsageChart { cache: Cache, data_points: VecDeque, range: Range, name: String, } impl CpuUsageChart { fn new(name: String, range: Range) -> Self { Self { cache: Cache::new(), data_points: VecDeque::new(), range, name, } } fn push_data(&mut self, value: f32) { self.data_points.push_back(value); while self.data_points.len() > PLOT_DEPTH { self.data_points.pop_front(); } self.cache.clear(); } fn view(&self) -> Element { Column::new() .width(Length::Fill) .height(Length::Shrink) .spacing(5) .align_x(Alignment::Center) .push(Text::new(self.name.clone())) .push(ChartWidget::new(self).height(Length::Fill).width(Length::Fill)) .into() } } impl Chart for CpuUsageChart { type State = (); #[inline] fn draw( &self, renderer: &R, bounds: Size, draw_fn: F, ) -> Geometry { renderer.draw_cache(&self.cache, bounds, draw_fn) } fn build_chart(&self, _state: &Self::State, mut chart: ChartBuilder) { use plotters::prelude::*; const PLOT_LINE_COLOR: RGBColor = RGBColor(0, 175, 255); let mut chart = chart .x_label_area_size(0) .y_label_area_size(28) .margin(20) .build_cartesian_2d(0..self.data_points.len(), self.range.clone()) .expect("failed to build chart"); chart .configure_mesh() .bold_line_style(BLUE.mix(0.1)) .light_line_style(BLUE.mix(0.05)) .axis_style(ShapeStyle::from(BLUE.mix(0.7)).stroke_width(1)) .y_labels(10) .y_label_style( ("sans-serif", 15) .into_font() .color(&BLUE.mix(0.9)) .transform(FontTransform::Rotate90), ) .y_label_formatter(&|y| format!("{}", y)) .draw() .expect("failed to draw chart mesh"); chart .draw_series( AreaSeries::new( self.data_points.iter().enumerate().map(|x| (x.0, *x.1)), 0f32, PLOT_LINE_COLOR.mix(0.175), ) .border_style(ShapeStyle::from(PLOT_LINE_COLOR).stroke_width(2)), ) .expect("failed to draw chart data"); } }