diff options
Diffstat (limited to 'test_fw/ui/src/main.rs')
| -rw-r--r-- | test_fw/ui/src/main.rs | 287 |
1 files changed, 287 insertions, 0 deletions
diff --git a/test_fw/ui/src/main.rs b/test_fw/ui/src/main.rs new file mode 100644 index 0000000..b65b3dc --- /dev/null +++ b/test_fw/ui/src/main.rs @@ -0,0 +1,287 @@ +#![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::<Vec<_>>(); + + if split.len() != 2 { + continue; + } + + let name = split[0].to_string(); + let value = split[1].parse::<f32>()?; + + 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<String>, + chart_by_name: HashMap<String, CpuUsageChart>, +} + +impl State { + fn new(channel: mpsc::Receiver<Message>, chart_by_name: impl IntoIterator<Item = (String, CpuUsageChart)>) -> (Self, Task<Message>) { + let elems = chart_by_name.into_iter().collect::<Vec<_>>(); + ( + 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<f32>, + range: Range<f32>, + name: String, +} + +impl CpuUsageChart { + fn new(name: String, range: Range<f32>) -> 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<Message> { + 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<Message> for CpuUsageChart { + type State = (); + + #[inline] + fn draw<R: Renderer, F: Fn(&mut Frame)>( + &self, + renderer: &R, + bounds: Size, + draw_fn: F, + ) -> Geometry { + renderer.draw_cache(&self.cache, bounds, draw_fn) + } + + fn build_chart<DB: DrawingBackend>(&self, _state: &Self::State, mut chart: ChartBuilder<DB>) { + 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"); + } +} |
