aboutsummaryrefslogtreecommitdiff
path: root/test_fw/ui/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'test_fw/ui/src/main.rs')
-rw-r--r--test_fw/ui/src/main.rs287
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");
+ }
+}